ArkTS 修仙主题 AI 应用开发 —— 从「AI 修仙功法」剖析鸿蒙原生 chat 应用全流程(HarmonyOS NEXT API 24)



目录
- 前言:当修仙文化遇上 AI
- 系统提示词工程 —— 创造一个有灵魂的 AI 角色
- 整体架构设计
- 网络层深度剖析
- SSE 流式协议的手工实现
- 修仙主题 UI 设计体系
- 聊天界面布局详解
- @State 状态管理与数据流
- 标签云布局 —— FlexWrap 实战
- 消息气泡组件与流式打字效果
- 错误处理与边界情况全梳理
- 性能优化实践
- 编译部署与常见问题
- 总结与扩展方向
1. 前言:当修仙文化遇上 AI
在 HarmonyOS NEXT API 24 上构建 AI 聊天应用,已经不仅仅是调通一个 HTTP 接口那么简单。当我们将中国传统的修仙文化(Xianxia/Cultivation)与现代大语言模型结合时,开发者需要同时掌握:
- 大模型提示词工程:如何用 System Prompt 塑造 AI 的角色人格
- ArkTS 声明式 UI:如何用代码构建沉浸式的修仙界面
- SSE 流式协议:如何解析大模型的逐 token 输出
- 鸿蒙网络编程:如何使用
@kit.NetworkKit的 http 模块 - 状态管理:如何用
@State驱动高频 UI 更新
「AI 修仙功法」是一个完全运行在鸿蒙设备上的 AI 聊天应用,AI 扮演一位来自上古修仙界的散仙,回答关于修炼境界、功法心法、丹药炼制、法器法宝等八大领域的修炼问题。
本文将从提示词工程、网络层实现、UI 主题体系、状态管理与流式渲染四个核心维度,完整剖析这个应用的每一行代码。
2. 系统提示词工程 —— 创造一个有灵魂的 AI 角色
2.1 什么是 System Prompt
System Prompt(系统提示词)是在每次 API 调用时随用户消息一起发送给大模型的指令。它定义了 AI 的角色身份、知识范围、回复风格和行为边界。
在 OpenAI 兼容的 API 中,请求体呈如下结构:
{
"messages": [
{"role": "system", "content": "你是..."},
{"role": "user", "content": "用户问题..."},
{"role": "assistant", "content": "AI回复..."}
]
}
system 消息不在聊天界面中显示,但它从根本上决定了 AI 的行为方式。
2.2 「AI 修仙功法」的 System Prompt 设计
const SYSTEM_PROMPT: string =
'你是一位来自上古修仙界的资深散仙 "AI 修仙功法",' +
'曾在九天十地游历万年,精通人、妖、魔三界各种功法秘籍。' +
'你以指点后辈修士为己任,帮助修仙路上的道友解决各种修炼难题。' +
'你覆盖但不限于以下领域:\n\n' +
'1. **修炼境界**:炼气、筑基、金丹、元婴、化神、合体、大乘、渡劫等各境界突破要领\n' +
// ... 2~7 省略 ...
'8. **秘境探险**:秘境规则、寻宝技巧、禁制破解、逃命秘术\n\n' +
'以下是你要遵循的回复原则:\n' +
'1. 先感知:察觉道友当前的修炼状态和心境,给予理解与鼓励。\n' +
'2. 再指路:分析问题根由,指出修炼方向或功法要领。\n' +
'3. 授秘诀:提供 1~3 条具体的修炼法门或口诀,并说明适用阶段。\n' +
'4. 语气需有仙风道骨,但不可过于晦涩,兼顾修真韵味和易懂。\n' +
'5. 涉及突破大境界、渡劫等危险事项时,务必提醒做好万全准备。\n' +
'6. 回复可适当引用上古传说、仙家典故以增信服力。\n\n' +
'请用中文回复,保留修仙文风但勿过于文言。';
2.3 好 Prompt 的五个要素
从这份 Prompt 中,可以提炼出优秀 System Prompt 的五个核心要素:
| 要素 | 对应内容 | 作用 |
|---|---|---|
| 身份定义 | 「资深散仙」「曾在九天十地游历万年」 | 明确角色,赋予可信度 |
| 能力边界 | 「精通人、妖、魔三界各种功法秘籍」 | 让 AI 知道自己的知识范围 |
| 领域覆盖 | 8 个修炼领域的具体列表 | 引导用户提问方向 |
| 行为规范 | 6 条回复原则 | 控制输出格式和质量 |
| 风格指南 | 「仙风道骨但不可过于晦涩」 | 平衡沉浸感和可用性 |
2.4 为什么使用字符串拼接而非模板字符串
在代码中我们使用了 + 号拼接多行字符串,而非 ES6 的模板字符串(Template Literal)。这主要是因为:
- 格式控制:
+拼接可以精确控制每行的缩进和换行 - 可读性:每行一个句子,便于版本 diff 对比
- 兼容性:某些老版本的 ArkTS 编译器对模板字符串支持不完善
在实际的 API 24 项目中,两种方式均可使用,但推荐字符串拼接 + 明确的
\n换行,因为你可以精确控制最终发给大模型的内容格式。
3. 整体架构设计
3.1 文件职责划分
entry/src/main/ets/pages/
├── Index.ets # UI 层:界面渲染、交互、状态管理
├── AIChatService.ets # 网络层:HTTP 请求、SSE 解析、数据回调
| 文件 | 行数 | 核心 API | 职责 |
|---|---|---|---|
AIChatService.ets |
341 行 | http.createHttp() |
网络通信、数据解析 |
Index.ets |
476 行 | @State、@Builder |
UI 渲染、用户交互 |
3.2 数据流全景
道友输入问题 / 点击修炼标签
│
▼
Index.ets: addUserMessage()
│
▼
Index.ets: callAI()
│ 构建 ChatMessage[] 历史
▼
queryAI(callbacks, history)
│
▼
AIChatService.ets → HTTP POST (SSE) → 服务器
│ │
│ ┌─────────────────────────┘
│ ▼
│ on('dataReceive') 触发?── 是 ──→ SSE 流式逐行解析
│ │ │
│ │ ▼
│ │ 每收到一个 token
│ │ │
│ │ ▼
│ │ callbacks.onData(text)
│ │ │
│ │ ▼
│ │ Index.ets: 追加到消息 + 刷新 UI
│ │
│ └── onData 未触发
│ │
│ └──→ resp.result 非流式回退解析
│ │
│ ▼
│ callbacks.onData(完整文本)
│
▼
callbacks.onDone() → isStreaming = false
4. 网络层深度剖析
4.1 http 模块的导入与使用
在 API 24 中,所有网络请求统一使用 @kit.NetworkKit 包:
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
BusinessError 用于类型化错误处理。
4.2 请求体构建
const requestBody: ChatCompletionRequest = {
model: 'deepseek-ai/DeepSeek-V3',
messages: fullMessages, // system + user + assistant
stream: true, // 启用流式输出
max_tokens: 2048, // 最大输出长度
temperature: 0.6, // 温度:越低越确定,越高越有创意
top_p: 0.95, // 核采样
frequency_penalty: 0, // 频率惩罚
thinking_budget: 2048, // 思考预算(deepseek 特有参数)
};
参数选择说明(API 24 环境下的最佳实践):
temperature: 0.6:介于 0(精确/确定性)和 1(创意/随机性)之间,既保证修仙回复的连贯性,又保留一些意外惊喜max_tokens: 2048:足够覆盖大多数修炼问题的回复,过长会浪费 tokensstream: true:必须开启,否则无法实现打字效果
4.3 请求头设置
header: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream', // ★ 告知服务器我们希望 SSE 流式响应
}
Accept: text/event-stream 是请求 SSE 响应的关键头信息。如果不设置这个头,某些 API 服务器可能不会启用流式模式。
4.4 超时配置
connectTimeout: 30000, // 连接超时:30 秒
readTimeout: 120000, // 读取超时:120 秒(AI 回复可能较长)
AI 大模型的推理速度通常较慢,尤其是复杂问题可能需要 10~30 秒才能开始返回内容。readTimeout 设为 120 秒是一个合理的上限。
5. SSE 流式协议的手工实现
5.1 SSE 协议格式
SSE(Server-Sent Events)的每一行格式如下:
data: {"choices":[{"delta":{"content":"修炼"}}]}
data: {"choices":[{"delta":{"content":"之"}}]}
data: {"choices":[{"delta":{"content":"道"}}]}
data: [DONE]
关键规则:
- 每行以
data:开头(包含冒号后的空格) data: [DONE]表示流结束- 不同事件之间用空行
\n分隔
5.2 ArrayBuffer 解码
httpRequest.on('dataReceive') 返回的是 ArrayBuffer 类型,需要解码为字符串:
function arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Arr = new Uint8Array(buffer);
let text = '';
for (let i = 0; i < uint8Arr.length; i++) {
text += String.fromCharCode(uint8Arr[i]);
}
return text;
}
性能提示:对于大块数据,逐字节拼接字符串可能较慢。如果遇到性能瓶颈,可以考虑使用
TextDecoderAPI(API 24 已支持)。
5.3 缓冲区管理算法
流式数据是分块(chunk)到达的,每个 chunk 可能包含不完整的行。缓冲区管理是 SSE 解析的核心:
let buffer = '';
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
const text = arrayBufferToString(data);
buffer += text; // ★ 追加到缓冲区
const lines = buffer.split('\n');
buffer = lines.pop() ?? ''; // ★ 最后一行可能不完整
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) continue;
if (trimmed === 'data:[DONE]') { /* end */ continue; }
const content = parseSSEDataLine(trimmed);
if (content) callbacks.onData(content);
}
});
为什么最后一行要 pop 出来?
考虑两个连续 chunk:
chunk 1: "data: {"choices":[{"delta":{"conte"
chunk 2: "nt":"修炼"}}]}\n\ndata: [DONE]\n"
如果单纯按 \n 分割,chunk 1 最后一行 data: {"choices":[{"delta":{"conte" 是不完整的 JSON,直接解析会报错。缓冲区方案将不完整的最后一行留到下一个 chunk 到达时再处理。
5.4 SSE 行解析
function parseSSEDataLine(line: string): string | null {
const jsonStr = line.slice(5).trim(); // 去掉 "data:"
if (!jsonStr) return null;
try {
const parsed = JSON.parse(jsonStr);
const choices = parsed.choices;
if (choices?.length > 0) {
const delta = choices[0].delta;
if (delta) return delta.content;
const message = choices[0].message;
if (message) return message.content;
}
} catch (_) { /* ignore */ }
return null;
}
注意 line.slice(5) 截取从第 5 个字符开始的内容——即去掉 data: 后(data: 占 5 个字符,包括冒号)。
5.5 双路径保障机制
在 HarmonyOS 的不同版本上,httpRequest.on('dataReceive') 的行为存在差异。某些版本在 SSE 场景下不会触发该事件。为此,我们设计了双路径保障:
// 路径 A:流式(dataReceive 事件)
httpRequest.on('dataReceive', (data: ArrayBuffer) => { /* SSE 解析 */ });
// 路径 B:非流式回退(request 回调)
if (!receivedAnyData && resp.result) {
// 从完整响应体解析
const bodyStr = typeof resp.result === 'string'
? resp.result
: arrayBufferToString(resp.result);
const sseContent = parseFullSSEBody(bodyStr); // 尝试 SSE
if (!sseContent) {
const jsonContent = parseNonStreamingBody(bodyStr); // 尝试 JSON
}
}
这种设计确保了应用在任何 HarmonyOS 版本上都能正常工作。
6. 修仙主题 UI 设计体系
6.1 配色系统
「AI 修仙功法」使用了一套完整的紫金配色体系:
| 用途 | 色值 | 说明 |
|---|---|---|
| 页面背景 | #0d001a |
深邃夜空般的暗紫色 |
| 标题栏背景 | linear-gradient(135deg, #1a0033, #2d1b69, #1a0033) |
紫金色渐变 |
| 标题文字 | #FFD700 |
金色,象征仙界 |
| 输入框背景 | #1a0a2e |
深紫,金色边框点缀 |
| 用户气泡 | rgba(123,47,190,0.6) |
半透明紫色(金色边框) |
| AI 气泡 | rgba(26,10,46,0.85) |
深紫(金色边框) |
| AI 文字 | #E8D5B7 |
古风米白色 |
| 错误条 | #8B0000 |
暗红色 |
| 次级文字 | #B8966B / #A08060 |
古铜色 |
6.2 在 ArkTS 中实现渐变色背景
在 API 24 中,渐变色通过 backgroundImage 属性实现:
Row()
.backgroundImage('linear-gradient(135deg, #1a0033, #2d1b69, #1a0033)')
.backgroundImageSize({ width: '100%', height: '100%' })
关键点:
backgroundImage支持 CSS 格式的渐变表达式- 必须配合
backgroundImageSize({ width: '100%', height: '100%' })才能正确填充 - API 24 不支持
linearGradient对象语法,只能使用 CSS 字符串
6.3 修仙风格文案设计
| 场景 | 普通文案 | 修仙文案 |
|---|---|---|
| 清空对话 | 新对话 | 重开法坛 |
| 加载中 | 思考中… | 仙尊沉思中… |
| 发送按钮 | 发送 | 问道 |
| 输入框占位 | 输入你的生活难题… | 描述你的修炼难题… |
| 空回复兜底 | 抱歉,我不明白 | 此事颇为玄妙,可否细细道来? |
| 底部箴言 | — | 「修仙之道,贵在持之以恒……」 |
这种文案替换看似简单,但对用户体验的影响是巨大的——它让用户从打开应用的第一秒就进入了修仙世界的沉浸感。
7. 聊天界面布局详解
7.1 三段式布局
┌──────────────────────────────────────┐
│ ☯ AI 修仙功法 │ ← 紫金渐变标题栏 (60vp)
│ 问道求仙 · 指掌之间 [🔄 重开法坛] │
├──────────────────────────────────────┤
│ │
│ 聊天消息 / 欢迎界面 │ ← Scroll + Column
│ (layoutWeight: 1) │ (深紫背景)
│ │
├──────────────────────────────────────┤
│ [错误提示条 - 仅出错时显示] │ ← 条件渲染
├──────────────────────────────────────┤
│ ┌──────────────────┐ ┌──────┐ │
│ │ 描述你的修炼难题... │ │ 问道 │ │ ← 底部输入栏
│ └──────────────────┘ └──────┘ │
└──────────────────────────────────────┘
7.2 layoutWeight 的核心作用
Column() {
// 顶部标题栏(固定高度 60vp)
Scroll() {
// 消息列表
}
.layoutWeight(1) // ★ 撑满剩余空间
.backgroundColor('#0d001a')
// 错误提示条(条件渲染)
// 底部输入栏(固定高度 ~60vp)
}
layoutWeight(1) 是 ArkTS 中最实用的布局属性之一。它让 Scroll 占据标题栏和输入栏之间的全部剩余空间,无需手动计算高度。当软键盘弹出时,这个区域会自动压缩,保障输入栏始终可见。
7.3 条件渲染的空状态 / 聊天态切换
Scroll() {
Column() {
if (this.messages.length === 0 && !this.isLoading) {
// 空状态 → 欢迎界面(含场景标签云 + 底部箴言)
this.buildWelcomeView();
} else {
// 有消息 → 渲染气泡列表
ForEach(this.messages, (msg) => { this.buildMessageBubble(msg); });
// 加载中 → 显示「仙尊沉思中...」
if (this.isLoading) { this.buildLoadingIndicator(); }
}
}
}
这种写法的好处是单一数据源——this.messages 的长度决定了显示欢迎页还是聊天页,不存在「当前页面模式」这样的冗余状态。
8. @State 状态管理与数据流
8.1 @State 状态定义
@State messages: ChatItem[] = []; // 消息列表(UI 核心数据)
@State inputText: string = ''; // 输入框内容
@State isLoading: boolean = false; // 加载标志
@State errorMsg: string = ''; // 错误信息
这四个 @State 驱动了整个 UI 的变化:
| @State 变化 | UI 响应 |
|---|---|
messages 增加/修改 |
消息气泡列表重新渲染 |
inputText 变化 |
输入框内容更新、发送按钮颜色变化 |
isLoading 变为 true |
按钮显示 Loading、发送按钮禁用、出现「仙尊沉思中」 |
isLoading 变为 false |
按钮恢复、隐藏加载指示 |
errorMsg 不为空 |
显示红色错误提示条 |
8.2 @State 的浅比较机制
这是 ArkTS 开发中最容易被忽视的关键点。在 API 24 中,@State 使用**浅比较(Shallow Comparison)**来判定数据是否变化。
// ❌ 这样不会触发 UI 刷新!
this.messages[idx].content += '新的文字';
// messages 数组的引用没有变化,@State 检测不到修改
// ✅ 必须创建新数组!
this.messages[idx].content += '新的文字';
this.messages = [...this.messages]; // 新数组 → 触发刷新
原理:
@State装饰器在每次赋值操作时,比较新旧值的引用(而非内容)。只有引用发生变化时,才会触发build()重新执行。[...arr]创建了数组的浅拷贝,引用变了,UI 随之刷新。
8.3 流式数据的不可变更新
每次 AI 返回一个 token,都需要执行一次「读→改→写」操作:
onData: (text: string) => {
const idx = this.messages.findIndex(m => m.id === aiMsgId);
if (idx !== -1) {
this.messages[idx].content += text; // 改
this.messages = [...this.messages]; // 写(触发刷新)
}
}
对于几十到几百次更新(一次 AI 回复的 token 数),这种模式的性能完全足够。如果 token 数量极大(如生成 8000+ tokens),可以每 3~5 个 token 刷新一次来优化。
8.4 聊天历史的正确构建
发送给 API 的消息历史必须排除正在流式输出的那条消息:
const history: ChatMessage[] = [];
for (const msg of this.messages) {
if (msg.id === aiMsgId) continue; // 跳过当前 AI 消息
if (msg.role === 'user') {
history.push({ role: 'user', content: msg.content });
} else if (msg.role === 'ai' && !msg.isStreaming) {
history.push({ role: 'assistant', content: msg.content });
}
}
9. 标签云布局 —— FlexWrap 实战
9.1 场景标签的设计
8 个修炼标签的排列需求:
- 水平排列,从左到右
- 宽度由内容自适应(不同长度的「丹药炼制」与「天劫渡劫」宽度不同)
- 一行满后自动换行
- 居中对齐
9.2 Flex + FlexWrap.Wrap 实现
Flex({
wrap: FlexWrap.Wrap, // ★ 允许换行
justifyContent: FlexAlign.Center, // ★ 居中对齐
alignContent: FlexAlign.Start,
}) {
ForEach(this.scenes, (scene: SceneEntry) => {
Button() {
Row() {
Text(scene.icon)
Text(scene.label)
}
}
.backgroundColor(scene.color)
.borderRadius(18)
.margin({ left: 4, right: 4, bottom: 8 })
.onClick(() => { this.sendQuestion(scene.question); })
})
}
9.3 为什么不用 GridRow
GridRow 要求所有子项等宽——但在标签云中,每个标签的宽度由其文本长度决定(「天劫渡劫」和「灵兽御兽」宽度显然不同)。Flex + Wrap 是标签云的唯一正确选择。
9.4 标签的 onCick
每个标签的 onClick 都绑定了一个完整的问题,点击后直接发送:
.onClick(() => {
this.sendQuestion(scene.question);
})
这样用户不需要打字,一键就能开始对话。降低交互门槛是提高用户留存率的关键手段。
10. 消息气泡组件与流式打字效果
10.1 道友消息气泡(右对齐)
Row() {
Row() {}.layoutWeight(1) // 将气泡推到右侧
Column() {
Text(msg.content)
.fontSize(15)
.fontColor('#FFE4B5')
.lineHeight(22)
}
.constraintSize({ maxWidth: 300 })
.padding(12)
.backgroundColor('rgba(123,47,190,0.6)')
.border({ width: 1, color: 'rgba(255,215,0,0.2)' })
.borderRadius(16)
.borderRadius({ bottomRight: 4 }) // ★ 尖角效果
Text('🧑🦰').fontSize(22)
}
10.2 仙尊消息气泡(左对齐)
Row() {
Text('🧙').fontSize(24)
Column() {
Text(msg.isStreaming ? msg.content + '▌' : msg.content)
.fontSize(15)
.fontColor('#E8D5B7')
.lineHeight(22)
}
.constraintSize({ maxWidth: 300 })
.padding(12)
.backgroundColor('rgba(26,10,46,0.85)')
.border({ width: 1, color: 'rgba(255,215,0,0.3)' })
.borderRadius(16)
.borderRadius({ bottomLeft: 4 })
.shadow({ radius: 4, color: 'rgba(255,215,0,0.08)', offsetX: 0, offsetY: 2 })
Row() {}.layoutWeight(1)
}
10.3 流式光标效果
// 流式输出中 → 追加 ▌光标
Text(msg.isStreaming ? msg.content + '▌' : msg.content)
// 流式结束 → isStreaming = false → 光标消失
onDone: () => {
this.messages[idx].isStreaming = false;
}
▌(U+258C)字符在文字最后显示为一个闪烁的方块光标,模拟打字效果。真正实现「闪烁」效果需要配合定时器交替显示空格和 ▌,但对于 200~500ms 一次的 token 更新,光标本身的闪烁感已经足够自然。
11. 错误处理与边界情况全梳理
11.1 六种错误场景
| 场景 | 触发条件 | UI 表现 |
|---|---|---|
| 网络不通 | DNS 解析失败/无网络 | 红色错误条:「请求失败: XXX」 |
| 鉴权失败 | API Key 错误/过期 | 红色错误条:「服务器返回错误 (401): …」 |
| 限流 | 请求频率过高 | 红色错误条:「服务器返回错误 (429): …」 |
| 响应格式异常 | 服务器返回了意外的数据格式 | 红色错误条:「无法解析响应: …」 |
| AI 返回空内容 | 模型未输出任何实质内容 | 兜底文本:「此事颇为玄妙,可否细细道来?」 |
| 流式未触发 | 平台差异导致 dataReceive 未触发 | 自动切换到非流式回退路径 |
11.2 请求取消(Cancel)
当用户点击「重开法坛」时,需要取消正在进行的请求:
// Index.ets
clearConversation(): void {
cancelAI(); // ★ 先取消 HTTP 请求
this.messages = [];
this.isLoading = false;
this.errorMsg = '';
}
// AIChatService.ets
export function cancelAI(): void {
if (httpRequestTask) {
httpRequestTask.destroy();
httpRequestTask = null;
}
}
为什么需要 cancel?因为如果不取消,onData 和 onDone 回调仍然会执行,试图更新已经被清空的 messages 数组,可能导致 findIndex 找不到消息而出错。
11.3 防重复点击
sendMessage(): void {
const text = this.inputText.trim();
if (!text || this.isLoading) return; // ★ 正在加载中禁止发送
// ...
}
同时,发送按钮在加载态下 enabled(false) 并显示 LoadingProgress,从 UI 层面阻止用户操作。
12. 性能优化实践
12.1 LazyForEach 替代 ForEach
当消息列表超过 50 条时,推荐使用 LazyForEach 替代 ForEach:
// 需要先实现 IDataSource
class MessageDataSource implements IDataSource {
private data: ChatItem[] = [];
// ... 实现 totalCount / getData / registerDataChangeListener 等方法
}
LazyForEach(this.dataSource, (msg: ChatItem) => {
this.buildMessageBubble(msg);
}, (msg: ChatItem) => msg.id.toString())
LazyForEach 只渲染视口内的组件,离开视口的组件会被回收。对于数百条消息的场景,性能差距可达 5~10 倍。
12.2 控制 UI 刷新频率
在极高频率的流式更新场景(每次 onData 都刷新),UI 线程可能跟不上渲染。可以通过节流来控制刷新频率:
let renderCount = 0;
onData: (text: string) => {
this.messages[idx].content += text;
renderCount++;
if (renderCount % 3 === 0) { // 每 3 个 token 刷新一次
this.messages = [...this.messages];
}
}
onDone: () => {
this.messages = [...this.messages]; // 最终确保刷新
}
12.3 @Builder 避免重复代码
将可复用的 UI 片段抽取为 @Builder,减少代码重复:
@Builder
buildMessageBubble(msg: ChatItem) { /* ... */ }
@Builder
buildWelcomeView() { /* ... */ }
@Builder
buildLoadingIndicator() { /* ... */ }
13. 编译部署与常见问题
13.1 完整编译命令
hvigorw --mode module -p module=entry -p product=default assembleHap
13.2 常见编译错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Cannot find name 'ResourceColor' |
类型未识别 | API 24 可直接使用 string 色值,或在文件头确认无 import 遗漏 |
Property 'maxWidth' does not exist on type 'ColumnAttribute' |
属性不存在 | 改用 constraintSize({ maxWidth: 300 }) |
Argument of type 'string' is not assignable to parameter of type 'ResourceStr' |
类型不匹配 | 字符串直接作为 ResourceStr 使用,或通过 $r() 引用资源 |
Module has no exported member |
导入路径错误 | 确认使用 @kit.* 的 kit 路径,而非 @ohos.* |
13.3 权限配置
在 entry/src/main/module.json5 中添加:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
}
]
}
}
如果漏掉这一步,http 请求会在运行时静默失败,且不会触发 onError 回调(因为请求根本没发出去)。
14. 总结与扩展方向
14.1 本文要点回顾
本文通过「AI 修仙功法」这个完整的项目,系统讲解了在 HarmonyOS NEXT API 24 中使用 ArkTS 构建 AI 聊天应用的 8 个核心技术点:
| 技术点 | 关键内容 | 实战价值 |
|---|---|---|
| 提示词工程 | System Prompt 五要素设计 | 任何 AI 应用的核心 |
| HTTP 网络层 | http 模块 + SSE 流式 | 连接大模型的基础 |
| SSE 解析 | 缓冲区管理 + 双路径保障 | 解决平台兼容问题 |
| 主题 UI 体系 | 渐变色、配色、文案系统 | 提升产品沉浸感 |
| 三段式布局 | Column + Scroll + layoutWeight | 所有聊天界面的模板 |
| @State 管理 | 浅比较机制 + 不可变更新 | 避免「改了但不刷新」bug |
| FlexWrap | 流式标签云布局 | 快速场景入口 |
| 错误处理 | 6 种场景 + 请求取消 | 保障应用稳定性 |
14.2 ArkTS 开发的三个黄金法则
- @State 是不可变数据:永远通过创建新数组/新对象来触发刷新
- 属性链在闭包之前:
Component().attr() { content }而非Component() { content }.attr() - 网络请求永远准备 B 计划:流式 + 非流式双路径保障
14.3 扩展方向
| 功能 | 实现思路 | 涉及 API |
|---|---|---|
| 历史记录持久化 | 使用 Preferences 存储对话 JSON | @kit.StorageKit |
| 语音输入 | 集成语音识别,将用户语音转为文字 | @kit.VoiceKit |
| Markdown 渲染 | AI 回复中的 **粗体**、列表等格式渲染 |
RichText 或自定义解析 |
| 多轮对话记忆 | 当前已实现,只需增加持久化 | — |
| 主题切换 | 修仙/科幻/古风等多套主题 | @Styles + @Extend |
| 流式音频播放 | AI 回复以语音形式朗读 | @kit.AudioKit |
| 对话导出 | 导出为文本或图片分享 | @kit.DialogKit + @kit.MediaKit |
14.4 写在最后
「AI 修仙功法」不仅是一个娱乐化的 AI 聊天应用,更是一份完整的 HarmonyOS NEXT AI 应用开发参考实现。
从提示词工程到 SSE 流式解析,从紫金主题 UI 到 @State 不可变更新,每一个技术点都可以直接迁移到其他类型的 AI 应用中——无论是情感陪伴、知识问答、代码助手,还是本文主题的修仙向导。
修仙之道,贵在持之以恒。An developer’s path is also one of persistent cultivation.
本文所有代码基于 HarmonyOS NEXT API 24(ArkTS 声明式开发范式)。
更多推荐

所有评论(0)