请添加图片描述
请添加图片描述

AI 修仙功法 —— 鸿蒙 NEXT ArkTS 修仙主题 AI 对话应用开发全解析

SDK 版本:HarmonyOS NEXT 6.1.1(API 24)
开发语言:ArkTS(Ark TypeScript)
AI 模型:DeepSeek-V3(通过 GitCode API 调用)
通信协议:SSE 流式 + HTTP 双路径保障


一、项目缘起

1.1 为什么是「修仙」?

在快节奏的现代生活中,人们面临着前所未有的压力与焦虑。工作内卷、人际关系复杂、信息过载……这些问题的本质,其实与古代修道者面临的困境有惊人的相似之处——都是在纷繁复杂的世界中寻找内心的安宁与力量。

「AI修仙功法」正是基于这一洞察而产生的创意。它借用中国古典修仙文化的语言体系(心法、功法、丹道、悟道、渡劫、典籍),以 AI 大模型为内核,打造一位隐居深山的「云机子道长」,用仙侠的口吻为现代人解答生活中的困惑、传授修心养性的法门。

这不仅仅是一次技术开发实践,更是一次传统文化与现代 AI 的跨界融合实验

1.2 技术选型

技术 选型理由
ArkTS HarmonyOS NEXT 原生语言,类型安全、编译高效
ArkUI 声明式 UI 框架,链式 API 简洁直观
Column + Row + Scroll 经典的纵向 + 横向 + 滚动布局组合
SSE 流式协议 实时逐 token 显示 AI 回复,提升用户体验
双路径完成保障 兼容 SSE 事件触发和不触发两种设备场景

二、工程结构与架构设计

2.1 目录结构

entry/src/main/ets/pages/
├── Index.ets             # 主页面(聊天界面 + 修仙主题 UI)
└── AIChatService.ets     # 网络通信层(HTTP + SSE + 解析 + 取消)

两个文件、约 710 行代码,构成了一个完整的 AI 修仙对话应用。没有冗余的第三方依赖,所有功能都基于 HarmonyOS NEXT 原生 API 实现。

2.2 数据流架构

道友提问 → Index.ets (sendMessage)
                │
                ▼
        构建对话历史 history[]
                │
                ▼
    AIChatService.queryAI(callbacks, history)
                │
                ├─→ SSE 路径(dataReceive 触发)
                │       └─→ onData(token) → 逐字拼接 currentAIResponse
                │       └─→ onDone() → 完整回复 → messages.push()
                │
                └─→ HTTP 回调路径(dataReceive 不触发)
                        └─→ parseFullSSEBody(responseBody)
                        └─→ onData(全量内容) → UI 一次性显示
                        └─→ onDone() → 完成

2.3 修仙主题配色体系

本应用的视觉设计围绕「古风 · 雅致 · 禅意」展开:

色值 用途 灵感来源
#3d2b1f 顶部栏背景 古木深色,沉稳厚重
#5c4a3a 用户气泡背景 赭石色,道友衣着
#8b7355 发送按钮/辅助色 古铜色,灵珠光泽
#f5f0e8 AI 气泡/卡片背景 宣纸色,素雅温润
#faf6f0 页面背景 米白底色,如古卷
#2c1810 文字主色 墨色
#c4b59a 辅助文字 淡金

三、系统提示词设计 —— 修仙导师的灵魂

3.1 提示词全文

const SYSTEM_PROMPT: string =
  '你是一位隐居深山的修仙老道,道号「云机子」,修行千年,贯通儒释道三教精义。' +
  '你以传授「AI 修仙功法」为己任,帮助现代人在繁忙喧嚣的尘世中修心养性、' +
  '领悟大道。\n\n' +
  '=== 你的修仙功法体系 ===\n' +
  '1. 🧘 心法篇:静坐冥想、调息养神、情绪炼化、心性磨砺\n' +
  '2. ⚡ 功法篇:日常修炼法门、气感引导、周天运行、筑基要领\n' +
  '3. 🌿 丹道篇:食疗养生、药膳配伍、四季调养、辟谷导引\n' +
  '4. ☯ 悟道篇:世事洞明、人情练达、顺逆之境、天人合一\n' +
  '5. 🔮 渡劫篇:人生重大困境的化解之道、劫后重生之法\n' +
  '6. 📜 典籍篇:道德经、庄子、周易等经典的现代修炼解读\n\n' +
  '=== 回答原则 ===\n' +
  '1. 【以道为根】所有回答都要回归到「道」的层面\n' +
  '2. 【深入浅出】用修仙的比喻解释现代生活困境\n' +
  '3. 【因材施教】根据不同人的「灵根」推荐不同的修炼路径\n' +
  '4. 【三句话不离本行】每个回答有机融入修炼智慧\n' +
  '5. 【仙气与实用并存】语言有仙侠韵味,建议具体可操作\n' +
  '6. 【安全边界】涉及疾病先建议寻求专业帮助\n' +
  '7. 【禁用违规内容】不提供违法、危险、有害的建议\n\n' +
  '请用中文回复,以仙侠口吻开头(如「善哉善哉」「道友所言极是」),' +
  '每次回答结尾加一句点题的诗句或箴言。';

3.2 提示词设计思路

角色设定:不是冷冰冰的 AI,而是一位有温度、有故事的修仙老道。「云机子」这个道号暗合「云机」—— 云中玄机,既有仙气又暗示 AI 的智慧。

功法体系(6 篇):覆盖了从身体(功法/丹道)到心理(心法)到精神(悟道/典籍)再到人生困境(渡劫)的全维度,让用户在任何一个生活层面都能找到对应的修炼指导。

回答原则(7 条):最核心的是第 4 条「三句话不离本行」和第 5 条「仙气与实用并存」。这确保了 AI 不会沦为普通的咨询顾问,而是始终保持在修仙导师的「角色」中。

语气要求:以「善哉善哉」「道友所言极是」等仙侠口吻开头,结尾附加一句点题的诗句。这种结构化的语气要求让每次回答都带有鲜明的「云机子」风格。

3.3 提示词对 AI 输出的影响

同样的生活问题,普通 AI 和云机子道长的回答会截然不同:

用户问:「我最近工作压力很大,怎么办?」

普通 AI 回答:「建议您合理安排工作时间,适当运动,保持充足睡眠……」

云机子道长回答:「善哉善哉。道友所言之压力,在吾辈修仙之人看来,实乃心魔外显、灵台蒙尘之兆。压力非外界所予,乃内心所生。老道传你一套’止念观心法’:每日晨起闭目调息一刻钟……」

同样是建议,云机子道长的回答将「压力」重新定义为「心魔」,将「休息」包装为「止念观心法」—— 建议的实质没有变,但语言的转化带来了全新的认知框架


四、Index.ets —— 修仙主题主界面全解析

4.1 组件结构

@Entry @Component AICultivationPage
├── 顶部标题栏 Row (#3d2b1f)
│   ├── Column: ☯️ AI 修仙功法 + "云机子道长为道友指点迷津"
│   ├── [条件] Text('止念') — 加载时显示
│   └── Text('清心')
├── 消息列表 Scroll > Column
│   ├── [空状态] 引导页
│   │   ├── Text('☯️')
│   │   ├── 标题 + 副标题
│   │   ├── 六大功法体系卡片 (Row > ForEach)
│   │   └── 快捷提问 (Row > ForEach)
│   ├── [有消息] ForEach(messages → ChatBubble)
│   └── [加载中] ChatBubble(isLoading)
└── 底部输入区 Row (#ffffff)
    ├── TextInput (placeholder: "向道长请教修炼之事...")
    └── Circle (灵珠发送按钮)

4.2 状态管理

@State messages: ChatMessage[] = [];        // 对话历史
@State inputText: string = '';              // 输入框绑定
@State isLoading: boolean = false;          // 请求状态
@State currentAIResponse: string = '';      // 流式拼接内容

四个 @State 变量的设计遵循一个原则:UI 是状态的函数。只要状态变了,UI 自动更新,开发者不需要手动操作 DOM。

特别值得一提的是 currentAIResponsemessages 的分工:

  • messages:存储「已完成」的对话(用户消息 + 完整的 AI 回答)
  • currentAIResponse:存储「正在流式输出中」的 AI 回答片段

这种分离设计避免了在流式过程中反复修改 messages 数组引起的性能问题和状态不一致。

4.3 顶部标题栏 —— 仙风道骨的开篇

Row() {
  Column() {
    Text('☯️ AI 修仙功法')
      .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#f5f0e8')
    Text('云机子道长为道友指点迷津')
      .fontSize(11).fontColor('#c4b59a').margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)

  if (this.isLoading) {
    Text('止念')
      .fontSize(14).fontColor('#c4b59a')
      .onClick(() => {
        cancelAI();
        this.isLoading = false;
      })
  }
  Text('清心')
    .fontSize(14).fontColor('#c4b59a').margin({ left: 16 })
    .onClick(() => { cancelAI(); this.messages = []; ... })
}
.alignItems(VerticalAlign.Center)
.padding({ top: 14, bottom: 14, left: 20, right: 20 })
.backgroundColor('#3d2b1f')

设计细节:

  • 双层标题:主标题 + 副标题,副标题的文字「云机子道长为道友指点迷津」在用户进入聊天后仍然可见,持续强化角色设定
  • 修仙术语替代通用用语:「停止」→「止念」(停止妄念),「清空」→「清心」(清除心中杂念)。同样的操作,不一样的意境
  • 深色古木背景 #3d2b1f 搭配淡金文字 #c4b59a,营造古卷质感

4.4 空状态引导页 —— 初入仙门的仪式感

当用户第一次打开应用(messages 为空)时,显示完整的空状态引导页:

if (this.messages.length === 0) {
  Column() {
    Text('☯️').fontSize(56)
    Text('AI 修仙功法')
      .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#2c1810')
      .margin({ top: 8 })
    Text('云机子道长为道友传授修炼心法')
      .fontSize(14).fontColor('#8b7355').margin({ top: 4 })

    // 功法体系卡片(六列)
    Row() {
      ForEach([
        { icon: '🧘', text: '心法篇' },
        { icon: '⚡', text: '功法篇' },
        { icon: '🌿', text: '丹道篇' },
        { icon: '☯', text: '悟道篇' },
        { icon: '🔮', text: '渡劫篇' },
        { icon: '📜', text: '典籍篇' },
      ] as FeatureItem[], (item: FeatureItem) => {
        Column() {
          Text(item.icon).fontSize(24)
          Text(item.text).fontSize(11).fontColor('#5c4a3a').margin({ top: 4 })
        }
        .width(72).height(72)
        .backgroundColor('#f5f0e8').borderRadius(12).margin(4)
      })
    }
    .justifyContent(FlexAlign.Center).width('80%').margin({ top: 16 })

    // 快捷提问
    Text('💡 试试问道这些:').fontSize(13).fontColor('#8b7355').margin({ top: 20 })
    Row() {
      ForEach(QUICK_QUESTIONS, (q: string) => {
        Text(q)
          .fontSize(12).fontColor('#5c4a3a')
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor('#f5f0e8').borderRadius(16).margin(4)
          .onClick(() => { this.sendMessage(q); })
      })
    }
    .justifyContent(FlexAlign.Center).width('90%').margin({ top: 8 })
  }
}

这段代码包含了三种 ArkTS 渲染模式:

渲染模式 代码位置 说明
条件渲染 if (this.messages.length === 0) 空状态时才显示引导页
循环渲染 ForEach(...) × 2 六张功法卡片 + 四个快捷问题
事件绑定 .onClick(() => { this.sendMessage(q); }) 点击快捷问题直接发送

4.5 消息列表渲染

// 已完成的消息
ForEach(this.messages, (msg: ChatMessage) => {
  ChatBubble({ msg: msg })
})

// 正在流式输出中的消息
if (this.isLoading) {
  ChatBubble({
    msg: { role: 'assistant', content: this.currentAIResponse },
    isLoading: !this.currentAIResponse
  })
}

关键设计点:isLoading: !this.currentAIResponse——当 currentAIResponse 为空时显示「运功动画」(三个脉动的圆点),一旦有内容到达立即切换为文字显示。这个逻辑让动画和文字无缝过渡,用户不会看到闪烁。

4.6 ChatBubble 组件

@Component
struct ChatBubble {
  msg: ChatMessage = { role: 'user', content: '' };
  isLoading: boolean = false;

  build() {
    Column() {
      if (this.msg.role === 'user') {
        // 道友提问:右对齐 · 赭石色气泡
        Row() {
          Blank()
          Column() { Text(this.msg.content) ... }
            .backgroundColor('#5c4a3a')     // 道友色
            .borderRadius({ topLeft: 16, topRight: 4, ... })
            .constraintSize({ maxWidth: '80%' })
        }
        .width('100%').alignItems(VerticalAlign.Top)
      } else {
        // 师尊回复:左对齐 · ☯️ 头像 + 宣纸色气泡
        Row() {
          Column() { Text('☯️') ... }        // 师尊头像
          Column() {
            if (this.isLoading && !this.msg.content) {
              // 运功动画:三枚脉动灵珠
              Row() {
                ForEach([0, 1, 2], (idx: number) => {
                  Circle().width(6).height(6).fill('#8b7355')
                    .opacity(0.4 + idx * 0.3) // 逐步变亮
                    .margin({ left: 2, right: 2 })
                })
              }
              .padding(14).backgroundColor('#f5f0e8').borderRadius(16)
            } else {
              Text(this.msg.content) ...
            }
          }
          .backgroundColor('#f5f0e8')        // 宣纸色
          .borderRadius({ topLeft: 4, ... })
          .constraintSize({ maxWidth: '70%' })
          Blank()
        }
      }
    }
    .width('100%').margin({ bottom: 12 })
  }
}

气泡设计的视觉语言

  • 道友(用户):右对齐、实色填充 → 「我」的视角
  • 师尊(AI):左对齐、头像 + 浅色气泡 → 「他」的视角
  • 圆角方向:发送方一侧的圆角小(4px),接收方一侧的圆角大(16px)→ 经典的即时通讯气泡语义
  • 运功动画:三个圆点通过 opacity(0.4 + idx * 0.3) 实现渐变亮度,模拟灵气流转的视觉效果

4.7 底部输入区

Row() {
  TextInput({ text: this.inputText, placeholder: '向道长请教修炼之事...' })
    .layoutWeight(1).height(40)
    .backgroundColor('#f0ebe3').borderRadius(20)
    .onChange((val: string) => { this.inputText = val; })
    .onSubmit(() => { if (this.inputText.trim()) this.sendMessage(...); })

  Circle()   // 灵珠发送按钮
    .width(40).height(40)
    .fill(this.inputText.trim() ? '#8b7355' : '#d4c9b8')
    .onClick(() => { if (this.inputText.trim()) this.sendMessage(...); })
}

「灵珠」发送按钮的颜色随输入状态变化:有内容时呈古铜色 #8b7355(可发送),无内容时呈淡灰色 #d4c9b8(不可发送)。这是最直观的用户引导,比显式的 enabled 状态更温和。

4.8 sendMessage 消息发送逻辑

sendMessage(text: string): void {
  if (this.isLoading || !text.trim()) return;

  // 1. 添加道友提问到列表
  this.messages.push({ role: 'user', content: text.trim() });
  this.inputText = '';
  this.isLoading = true;
  this.currentAIResponse = '';

  // 2. 构建对话历史(过滤 + 截断)
  const history: ChatMessage[] = this.messages
    .filter(m => m.content !== ''
      && !(m.role === 'assistant' && m === this.messages[this.messages.length - 1]))
    .slice(-20);

  // 3. 调用 AI 师尊
  queryAI({
    onData: (content: string) => {
      this.currentAIResponse += content;    // 流式拼接
    },
    onDone: () => {
      if (this.currentAIResponse) {
        this.messages.push({                // 完整回复入库
          role: 'assistant',
          content: this.currentAIResponse,
        });
      }
      this.currentAIResponse = '';
      this.isLoading = false;
    },
    onError: (errMsg: string) => {
      this.messages.push({                  // 错误信息也保持角色
        role: 'assistant',
        content: `🌿 道友见谅,老道今日灵机不畅:${errMsg}`,
      });
      this.currentAIResponse = '';
      this.isLoading = false;
    },
  }, history);
}

对话历史过滤逻辑

.filter(m => m.content !== ''
  && !(m.role === 'assistant' && m === this.messages[this.messages.length - 1]))

这行代码做了两件事:

  1. 过滤掉 content 为空的消息(边界情况保护)
  2. 排除最后一条 assistant 消息——因为在流式过程中,最后一条可能是临时的、未完成的

.slice(-20) 限制上下文窗口,防止无限制增长导致 API 超出 token 限制。

错误提示的角色一致性:即使在出错时,也保持云机子道长的口吻「道友见谅,老道今日灵机不畅」,而不是普通的「抱歉,出错了」。这种细节持续强化用户体验的沉浸感。


五、AIChatService.ets —— 网络通信层深度解析

5.1 类型定义

export interface ChatMessage {
  role: string;      // 'system' | 'user' | 'assistant'
  content: string;
}

export interface ChatCompletionRequest {
  model: string;
  messages: ChatMessage[];
  stream: boolean;           // 是否流式
  max_tokens: number;
  temperature: number;       // 0.0 ~ 2.0
  top_p: number;
  frequency_penalty: number;
  thinking_budget: number;
}

export interface AICallbacks {
  onData: (text: string) => void;     // 每个 token 到达
  onDone: () => void;                 // 流式结束
  onError: (errMsg: string) => void;  // 发生错误
}

AICallbacks 是这个网络层的「接口契约」。UI 层只需要实现这三个回调函数,网络层负责在正确的时机调用它们。这种回调模式在 ArkTS 这种单线程异步模型中非常高效。

5.2 响应解析核心 —— extractContent

function extractContent(obj: object): string | null {
  try {
    const o = obj as Record<string, object>;
    // 流式格式:choices[0].delta.content
    const delta = o['delta'] as Record<string, object> | undefined;
    if (delta) {
      const content = delta['content'];
      if (typeof content === 'string') return content;
    }
    // 非流式格式:choices[0].message.content
    const message = o['message'] as Record<string, object> | undefined;
    if (message) {
      const content = message['content'];
      if (typeof content === 'string') return content;
    }
  } catch (_) {}
  return null;
}

为什么需要兼容两种格式?因为流式场景下,API 返回的是 delta(增量),每次只包含一小段新增的文字;而非流式回退场景下,API 返回的是完整的 message 对象。extractContent 通过先尝试 delta、再尝试 message 的顺序,实现了双格式兼容。

5.3 SSE 流式解析

function parseSSEDataLine(line: string): string | null {
  const jsonStr = line.slice(5).trim();  // 去掉 "data:" 前缀
  if (!jsonStr || jsonStr === '[DONE]') return null;
  try {
    const parsed: object = JSON.parse(jsonStr);
    const choices: object[] =
      (parsed as Record<string, object>)['choices'] as object[] || [];
    if (choices.length > 0) return extractContent(choices[0]);
  } catch (_) {}
  return null;
}

line.slice(5) 去掉 data: 前缀(5 个字符)。兼容带空格和不带空格的格式(data: {...}data:{...} 都能正确处理)。[DONE] 标记在进入解析函数之前已经被外层逻辑拦截,这里也做了二次防御。

5.4 双路径请求完成保障

export function queryAI(callbacks: AICallbacks, messages: ChatMessage[]): void {
  // 取消上一次未完成的请求
  if (httpRequestTask) { try { httpRequestTask.destroy(); } catch (_) {} }

  const httpRequest = http.createHttp();
  httpRequestTask = httpRequest;

  // 构建请求体
  const fullMessages: ChatMessage[] = [
    { role: 'system', content: SYSTEM_PROMPT },   // 注入系统提示词
    ...messages,
  ];

  // ── 路径 A:SSE 流式事件 ──
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
    // 逐块解析 SSE → onData(token)
  });
  httpRequest.on('dataEnd', () => {
    // SSE 结束 → onDone()
  });

  // ── 路径 B:request 回调(兜底) ──
  httpRequest.request(API_URL, { ... }, (err, resp) => {
    if (err) { callbacks.onError(...); safeDestroy(httpRequest); return; }
    if (resp.responseCode !== 200) { callbacks.onError(...); safeDestroy(...); return; }

    // 提取完整响应体
    const bodyStr = typeof resp.result === 'string'
      ? resp.result
      : (resp.result instanceof ArrayBuffer ? arrayBufferToString(resp.result) : '');

    if (bodyStr) {
      // 优先 SSE 格式解析
      const sseContent = parseFullSSEBody(bodyStr);
      if (sseContent) { callbacks.onData(sseContent); }
      else {
        // 再试非流式 JSON
        const jsonContent = parseNonStreamingBody(bodyStr);
        if (jsonContent) { callbacks.onData(jsonContent); }
        else {
          console.warn('[AIChat] 无法解析响应');
          callbacks.onData(bodyStr);  // 最差情况显示原始文本
        }
      }
    }

    // ★ 无论是否解析成功,都确保 onDone 被调用
    if (!isDone) { isDone = true; callbacks.onDone(); }
    safeDestroy(httpRequest);
  });
}

三种响应场景覆盖

场景 条件 解析路径 用户体验
SSE 流式 dataReceive 触发 逐 token → onData 逐字显示,流畅自然
非 SSE 流式 dataReceive 未触发 + body 含 data: parseFullSSEBody 一次性显示
非流式 JSON dataReceive 未触发 + body 为 JSON parseNonStreamingBody 一次性显示

5.5 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;
}

这段代码手工实现 ArrayBuffer → string 的转换。虽然逐字节拼接 String.fromCharCode 对多字节 UTF-8 字符的处理不是最优的,但它具有最广泛的兼容性——在所有 HarmonyOS NEXT 版本上都能正常工作。

5.6 请求取消

export function cancelAI(): void {
  if (httpRequestTask) {
    try { httpRequestTask.destroy(); } catch (_) { }
    httpRequestTask = null;
  }
}

function safeDestroy(req: http.HttpRequest): void {
  try { req.destroy(); } catch (_) { }
  httpRequestTask = null;
}

cancelAI 对外暴露、由 UI 层的「止念」按钮调用。safeDestroy 是内部工具函数,在所有请求结束路径中被调用(成功、失败、异常),保证 HTTP 请求资源被及时释放。


六、ArkTS 修仙主题适配实战

6.1 命名风格的统一

修仙主题不仅仅是改文字,而是从代码命名到 UI 文案的全链路统一:

通用概念 修仙世界映射
用户提问 道友问道
AI 回复 师尊传法
加载动画 运功调息
停止请求 止念
清空聊天 清心
发送按钮 灵珠
输入框 问道
网络错误 灵机不畅
分类标签 功法体系

6.2 色彩与感官

修仙主题使用暖色调(古木、宣纸、赭石、古铜),与常见的科技蓝冷色调形成鲜明对比。这种配色方案传达的感官信号是:温暖、沉稳、可信赖——这正是用户对一位「修仙老道」的期待。

// 冷暖对比
// 科技蓝 (旧)              修仙古木色 (新)
// #2d5f8a  →  #3d2b1f     顶部栏
// #eef2f7  →  #f5f0e8     气泡/卡片背景
// #1a1a2e  →  #2c1810     文字颜色
// #fafbfc  →  #faf6f0     页面背景

6.3 动画细节

加载动画「运功调息」使用了三个圆点,通过 opacity(0.4 + idx * 0.3) 实现亮度渐变:

ForEach([0, 1, 2], (idx: number) => {
  Circle()
    .width(6).height(6).fill('#8b7355')
    .opacity(0.4 + idx * 0.3)    // → 0.4, 0.7, 1.0
    .margin({ left: 2, right: 2 })
})

为什么不用帧动画?因为 ArkUI 的 animateToanimation API 对于简单的渐变效果来说过于复杂。通过静态的 opacity 差值模拟「灵气流转」的效果,代码量少、渲染性能高。


七、完整代码的双文件协同

7.1 导入关系

// Index.ets
import { queryAI, cancelAI, ChatMessage } from './AIChatService';

// AIChatService.ets
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';

Index.ets 只导入三个东西:queryAI(发请求)、cancelAI(取消)、ChatMessage(类型)。AIChatService.ets 负责一切网络细节。

7.2 回调协同

查询 AI 时,Index.ets 传入三个回调:

         Index.ets                              AIChatService.ets
    ┌──────────────────┐                   ┌──────────────────────┐
    │  onData(text)     │ ←── SSE token ──│ dataReceive → 解析   │
    │  current+=text    │                  │                      │
    ├──────────────────┤                   │  或 request 回调     │
    │  onDone()         │ ←── 完成 ──────│  → onDone()          │
    │  messages.push()  │                  │                      │
    ├──────────────────┤                   │                      │
    │  onError(msg)     │ ←── 错误 ──────│  → onError()          │
    │  仙侠口吻显示      │                  │                      │
    └──────────────────┘                   └──────────────────────┘

八、网络权限配置

要让应用能够正常访问 AI API,需要在 module.json5 中添加网络权限:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "需要网络连接以获取 AI 修仙功法指引"
      }
    ]
  }
}

没有 ohos.permission.INTERNET 权限,所有网络请求都会被系统拦截,queryAIerr 回调会收到网络错误。


九、常见问题与解决方案

9.1 页面空白 / AI 不回复

检查清单

  1. API Key 是否有效

    const API_KEY = '你的有效Key';  // 过期 Key 会导致 401 错误
    
  2. 网络权限是否配置
    module.json5 中是否包含 ohos.permission.INTERNET

  3. 是否触发了 onDone(参考日志)
    搜索 [AIChat] 日志,确认「收到响应体, len=…」和后续的解析结果

9.2 只有运功动画没有文字

现象:发送消息后,一直看到三个跳动的圆点,没有文字出现。

根因currentAIResponse 始终为空。可能是 onData 回调未被调用(SSE 事件不触发),且 request 回调的兜底路径也未正确执行。

修复:检查 request 回调末尾是否调用了 onDone()(我们的代码中有 if (!isDone) { isDone = true; callbacks.onDone(); } 保障)。

9.3 文字乱码

现象:AI 回复的内容显示为乱码字符。

根因arrayBufferToString 使用 String.fromCharCode 逐字节拼接,对于多字节 UTF-8 字符处理不当。

解决方案:在支持 TextDecoder 的环境中使用:

const decoder = new TextDecoder('utf-8', { ignoreBOM: true });
return decoder.decode(uint8Arr);

9.4 修仙口吻不一致

现象:某些回复没有以「善哉善哉」开头,或者结尾没有诗句。

原因:AI 大模型的输出具有概率性,不是每次都能严格遵守格式要求。可以在系统提示词中强调「必须xxx」,但无法 100% 保证。

缓解方案:在 SYSTEM_PROMPT 的结尾用更强的措辞:「每次回答必须以仙侠口吻开头,必须以诗句结尾,违者将受天劫」。


十、ArkTS 语法精要

10.1 装饰器

@Entry          // 页面入口
@Component      // 自定义组件
struct AICultivationPage {
  @State messages: ChatMessage[] = [];  // 响应式状态
}

10.2 条件渲染

if (this.isLoading) {
  Text('止念')     // isLoading 为 true 时渲染
}

if (this.messages.length === 0) {
  // 空状态引导页     // 条件为 true 时渲染引导页
}

10.3 循环渲染

ForEach(this.messages, (msg: ChatMessage) => {
  ChatBubble({ msg: msg })
})

ForEach(QUICK_QUESTIONS, (q: string) => {
  Text(q).onClick(() => { this.sendMessage(q); })
})

10.4 链式属性

Text('☯️ AI 修仙功法')
  .fontSize(20)
  .fontWeight(FontWeight.Bold)
  .fontColor('#f5f0e8')

10.5 ArkTS 与 TypeScript 的关键差异

特性 TypeScript ArkTS
any 类型 允许 禁止
索引访问 obj[key] 允许 as Record<string,T> 中间转换
对象展开 {...obj} 允许 仅数组可展开
JSON.parse 返回类型 any as Record<string, object> 转换
UI 框架 无内置 声明式 ArkUI

十一、扩展方向

11.1 灵根测试功能

基于当前架构,可以快速添加一个「灵根测试」模块。用户回答几个问题,AI 分析出用户的灵根属性(金木水火土),并推荐对应的修炼路径。

11.2 修炼进度系统

为每次对话赋予「修为值」,积累到一定数值可以「突破境界」(炼气→筑基→金丹→元婴→化神)。这个 gamification 设计能大幅提升用户粘性。

11.3 多师尊切换

不止云机子一位师尊。可以设计多角色:菩提祖师(佛法)、太上老君(丹道)、纯阳剑仙(剑修)。不同的师尊对应不同的 SYSTEM_PROMPT,用户可以根据问题类型选择请教谁。

11.4 渡劫日记

当用户遇到重大人生困境时,可以开启「渡劫模式」,AI 会以更长的篇幅、更深入的智慧来陪伴用户度过难关,并将整个过程记录为「渡劫日记」。


十二、总结

「AI修仙功法」是一个用现代技术讲述传统文化故事的尝试。通过约 710 行 ArkTS 代码,我们实现了:

  • 完整的 AI 对话界面:道友提问 → 师尊答复 → 流式显示
  • 修仙主题视觉体系:古木色系 + 仙侠术语 + 宣纸质感
  • SSE 流式 + HTTP 双路径网络层:兼容所有 HarmonyOS NEXT 设备
  • 角色一致的用户体验:从提示词到错误信息,全程保持云机子道长的人设
  • 健壮的错误处理:空响应、解析失败、网络异常全覆盖

技术之外,这个项目的真正价值在于展示了一个思路:AI 应用的主题化不只是在 UI 层面换皮,而是从系统提示词到用户交互的全链路「角色扮演」。当用户在输入框打下「道长,我最近很迷茫」时,AI 回复的不仅仅是建议,更是一份带有仙侠韵味的精神慰藉。

这可能就是 AI 应用的下一个方向——不是更高效的工具,而是更有温度的陪伴。


本文基于 HarmonyOS NEXT 6.1.1(API 24)编写,使用 ArkTS 语言与 ArkUI 框架。所有代码均来自实际运行的应用,经过 DevEco Studio 编译验证。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐