在这里插入图片描述

开发环境: DevEco Studio 5.0+ / HarmonyOS NEXT 6.1.1(API 24)
开发语言: ArkTS(基于 TypeScript 的鸿蒙原生声明式语言)
网络框架: @kit.NetworkKit(原生 HTTP + SSE 流式解析)
AI 模型: DeepSeek-V3(兼容 OpenAI API 格式)


一、引言

在移动互联网时代,AI 对话助手已经成为应用开发的标配功能。从通用的 ChatGPT 到垂直领域的专业助手,AI 正在改变用户与应用的交互方式。HarmonyOS NEXT 作为新一代国产操作系统,提供了完整的原生网络通信能力,使得开发者可以在 ArkTS 中直接实现 AI API 的调用和流式响应展示。

本文将通过一个女友对话助手应用的完整实现,系统性地讲解在 HarmonyOS NEXT 上如何:

  • 构建聊天式的 UI 界面,包括消息气泡、输入框、流式文字展示
  • 使用 @kit.NetworkKit 原生 HTTP 模块调用 AI API
  • 解析 SSE(Server-Sent Events)流式响应
  • 实现非流式回退机制,兼容不同版本的网络行为
  • 管理复杂的异步状态(加载、流式追加、取消、错误处理)

1.1 应用功能预览

女友对话助手是一款专注于帮助男性用户分析女友话语、提供高情商回复建议的 AI 工具。用户输入女友说的话,AI 会:

  1. 共情分析:理解女友当下的情绪状态
  2. 需求解读:指出话语背后隐含的情感需求
  3. 回复建议:提供 1~3 条具体可用的回复话术

应用提供了丰富的示例场景供用户快速尝试:

"女朋友说「我没事」的时候到底是什么意思?"
"女友生气了说「别管我」,该怎么回?"
"她问我「你前女友好看还是我好看」该怎么回答?"
"女友说「今天好累」怎么回复最暖心?"

二、项目结构

2.1 文件组织

女友对话助手涉及两个核心文件:

entry/src/main/ets/pages/
├── index4.ets           # 主页面:聊天 UI + 交互逻辑
└── AIChatService.ets    # AI 服务层:网络请求 + SSE 解析

这种分层架构将 UI 层(index4.ets)和网络层(AIChatService.ets)分离,职责清晰:

文件 职责 代码量
index4.ets UI 布局、状态管理、用户交互、Builder 组件 400 行
AIChatService.ets HTTP 请求、SSE 流式解析、非流式回退、取消逻辑 330 行

2.2 页面路由注册

main_pages.json 中注册新页面:

{
  "src": [
    "pages/Index",
    "pages/index1",
    "pages/index3",
    "pages/index4"
  ]
}

三、AI 服务层设计(AIChatService.ets)

3.1 架构概览

AIChatService.ets 是整个应用的网络通信层,负责:

  1. 构建符合 OpenAI API 格式的请求体
  2. 通过 @kit.NetworkKithttp 模块发起 POST 请求
  3. 解析 SSE(Server-Sent Events)流式响应,逐 token 回调
  4. 提供非流式回退机制作为兼容方案
  5. 支持请求取消

3.2 配置与数据类型

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

/** API 配置常量 */
const API_URL = 'https://api-ai.gitcode.com/v1/chat/completions';
const API_KEY = 'YOUR_API_KEY_HERE';  // 需要替换为真实的 API Key

关键类型定义:

/** 聊天消息结构 */
export interface ChatMessage {
  role: string;     // "system" | "user" | "assistant"
  content: string;
}

/** 请求体结构 */
export interface ChatCompletionRequest {
  model: string;
  messages: ChatMessage[];
  stream: boolean;
  max_tokens: number;
  temperature: number;
  top_p: number;
  frequency_penalty: number;
  thinking_budget: number;
}

/** SSE 解析回调 */
export interface AICallbacks {
  onData: (text: string) => void;     // 收到新 token
  onDone: () => void;                 // 流式完成
  onError: (errMsg: string) => void;  // 错误处理
}

核心设计决策:

  • ChatMessage 接口完全兼容 OpenAI 的 message 格式,使得可以调用任何兼容的 API
  • AICallbacks 采用回调模式而非 Promise,因为 SSE 流式需要多次回调
  • stream: true 开启流式响应,让 AI 回复逐字显示

3.3 系统提示词设计

系统提示词(System Prompt)是 AI 行为的核心控制手段:

const SYSTEM_PROMPT: string =
  '你是一位资深的恋爱沟通顾问,擅长帮助男生分析女友话语背后的情感需求,' +
  '并提供真诚、体贴、有温度的高情商回复建议。\n\n' +
  '以下是你要遵循的原则:\n' +
  '1. 先共情:理解女友当下的情绪(开心、难过、生气、焦虑等)。\n' +
  '2. 再分析:指出她话语中隐含的需求(被关注、被理解、被重视、安全感等)。\n' +
  '3. 给建议:提供 1~3 条具体可用的回复话术,并说明每条话术的适用场景。\n' +
  '4. 语气温柔但真诚,避免油腻或过度讨好。\n' +
  '5. 如果涉及矛盾冲突,优先建议冷静沟通而非道歉敷衍。\n\n' +
  '请用中文回复,保持简洁实用。';

提示词设计的五大原则覆盖了:

  • 情感分析(情绪 + 需求)
  • 实用输出(具体话术 + 场景说明)
  • 风格控制(温柔真诚,不油腻)
  • 价值观引导(冷静沟通优于敷衍道歉)

3.4 SSE 流式解析

SSE(Server-Sent Events)是一种基于 HTTP 的流式传输协议,AI 模型逐 token 向客户端推送数据:

data: {"choices":[{"delta":{"content":"我"}}]}

data: {"choices":[{"delta":{"content":"理解"}}]}

data: {"choices":[{"delta":{"content":"你的"}}]}

data: [DONE]

单行解析函数:

function parseSSEDataLine(line: string): string | null {
  // 移除 "data:" 前缀(5个字符)
  const jsonStr = line.slice(5).trim();
  if (!jsonStr) return null;

  try {
    const parsed = JSON.parse(jsonStr) as Record<string, Object>;
    const choices = parsed.choices as Object[];
    if (choices && choices.length > 0) {
      const choice = choices[0] as Record<string, Object>;
      // 兼容 delta(流式)和 message(非流式)两种格式
      const delta = choice.delta as Record<string, Object>;
      if (delta) return delta.content as string;

      const message = choice.message as Record<string, Object>;
      if (message) return message.content as string;
    }
  } catch (_) { /* JSON 解析失败,跳过 */ }
  return null;
}

完整响应体解析(非流式回退):

function parseFullSSEBody(body: string): string {
  let result = '';
  const lines = body.split('\n');
  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed.startsWith('data:')) {
      if (trimmed === 'data:[DONE]') break;
      const content = parseSSEDataLine(trimmed);
      if (content) result += content;
    }
  }
  return result;
}

function parseNonStreamingBody(body: string): string | null {
  try {
    const parsed = JSON.parse(body) as Record<string, Object>;
    const choices = parsed.choices as Object[];
    if (choices && choices.length > 0) {
      const choice = choices[0] as Record<string, Object>;
      const message = choice.message as Record<string, Object>;
      if (message) return message.content as string;
    }
  } catch (_) { /* not JSON */ }
  return null;
}

3.5 核心请求逻辑(带流式+回退)

queryAI 函数是整个服务层的核心,实现了三段式请求策略:

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

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

  // === 第二步:构建请求体 ===
  const fullMessages: ChatMessage[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    ...messages,
  ];
  const requestBody: ChatCompletionRequest = {
    model: 'deepseek-ai/DeepSeek-V3',
    messages: fullMessages,
    stream: true,
    max_tokens: 2048,
    temperature: 0.6,
    top_p: 0.95,
  };

  let isDone = false;
  let receivedAnyData = false;
  let buffer = '';

  // === 第三步:注册事件监听 ===
  // 方式 A:dataReceive 事件 —— 流式数据推送
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
    const text = arrayBufferToString(data);
    buffer += text;
    receivedAnyData = true;

    // 按行拆解(SSE 以 \n 分隔)
    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]') {
        if (!isDone) { isDone = true; callbacks.onDone(); }
        continue;
      }
      const content = parseSSEDataLine(trimmed);
      if (content) callbacks.onData(content);
    }
  });

  // === 第四步:发起 POST 请求 ===
  httpRequest.request(
    API_URL,
    {
      method: http.RequestMethod.POST,
      header: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
      },
      extraData: JSON.stringify(requestBody),
      connectTimeout: 30000,
      readTimeout: 120000,
    },
    (err, resp) => {
      if (err) { /* 错误处理 */ return; }
      if (resp.responseCode !== 200) { /* HTTP 错误处理 */ return; }

      // 方式 B:非流式回退
      if (!receivedAnyData && resp.result) {
        const bodyStr = typeof resp.result === 'string'
          ? resp.result
          : arrayBufferToString(resp.result as ArrayBuffer);

        // 先尝试 SSE 格式解析
        const sseContent = parseFullSSEBody(bodyStr);
        if (sseContent) {
          callbacks.onData(sseContent);
        } else {
          // 再尝试非流式 JSON 格式
          const jsonContent = parseNonStreamingBody(bodyStr);
          if (jsonContent) {
            callbacks.onData(jsonContent);
          }
        }
        if (!isDone) { isDone = true; callbacks.onDone(); }
      }
    },
  );
}

为什么需要非流式回退?

HarmonyOS 不同版本的 http 模块对 SSE 的支持存在差异。在某些版本中,dataReceive 事件可能不会被触发。为了确保兼容性,当 receivedAnyData 标记为 false 时,我们从 request 回调的完整响应体中解析数据。

取消请求:

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

destroy() 方法会立即终止 HTTP 连接,适用于用户主动取消或超时场景。

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

http.on('dataReceive') 回调的数据类型是 ArrayBuffer,需要手动解码为字符串。这个函数逐字节转换,支持包括中文在内的所有 Unicode 字符。


四、主页面设计(index4.ets)

4.1 整体布局

主页面采用经典的三段式聊天 UI 布局:

┌──────────────────────────────────┐
│  💕 女友对话助手                  │
│  高情商回复从这里开始              │ ← 标题栏(白色背景)
├──────────────────────────────────┤
│                                  │
│  ┌─────────────────────────┐     │
│  │ 用户消息气泡(粉红色)     │ →   │
│  └─────────────────────────┘     │
│                                  │
│  ┌─────────────────────────┐     │
│  ← AI回复气泡(白色背景)    │     │ ← 聊天区(Scroll,可滚动)
│  └─────────────────────────┘     │
│                                  │
│  💭 正在思考...                   │ ← 加载动画
│                                  │
│  ⚠️ 错误提示(红色)               │ ← 错误提示
│                                  │
├──────────────────────────────────┤
│  ┌──────────────────┐ ┌────┐    │
│  │ 输入女友说的话...  │ │发送│    │ ← 输入区(白色背景)
│  └──────────────────┘ └────┘    │
│         ⏹ 取消回复               │ ← 取消按钮(加载时显示)
└──────────────────────────────────┘

4.2 状态变量设计

@Component
struct Index {
  @State messageList: ChatMessage[] = [];     // 聊天历史
  @State inputText: string = '';              // 输入框内容
  @State isLoading: boolean = false;           // AI 回复中
  @State streamingContent: string = '';        // 流式实时内容
  @State errorMsg: string = '';                // 错误提示

  private sampleQuestions: string[] = [        // 示例问题列表
    '女朋友说「我没事」的时候到底是什么意思?',
    '女友生气了说「别管我」,该怎么回?',
    '她问我「你前女友好看还是我好看」该怎么回答?',
    '女友说「今天好累」怎么回复最暖心?',
    '她说「你是不是不爱我了」该怎么回应?',
    '女朋友突然不回消息了,我该怎么办?',
  ];
}

状态流转关系:

用户发送 → isLoading=true → 流式追加 streamingContent → 完成 → 加入 messageList → isLoading=false
                                     ↓
                             用户点取消 → cancelAI() → isLoading=false
                                     ↓
                             发生错误 → errorMsg 显示

4.3 build() 主布局

build() {
  Column() {
    // 顶部标题栏
    this.buildHeader();

    // 聊天消息区(弹性填充)
    Column() { this.buildChatArea() }
      .layoutWeight(1);

    // 底部输入区
    this.buildInputArea();
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#FFF0E8')  // 暖粉色背景
}

设计亮点:

  • 暖粉色背景(#FFF0E8)营造温馨感性的聊天氛围
  • layoutWeight(1) 使聊天区占满输入栏以上的所有空间
  • 标题栏和输入区各自独立 @Builder,结构清晰

4.4 标题栏

@Builder
buildHeader() {
  Column() {
    Text('💕 女友对话助手')
      .fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor('#D4587A')       // 主色调:玫瑰粉
      .margin({ top: 12 })

    Text('高情商回复从这里开始')
      .fontSize(13)
      .fontColor('#E8A0B0')       // 副色调:浅粉
      .margin({ top: 2, bottom: 4 })
  }
  .width('100%')
  .padding({ top: 8, bottom: 8 })
  .backgroundColor('#FFFFFF')     // 白色背景
}

标题栏使用双层文字设计:主标题 + 副标语。颜色方案围绕主色 #D4587A(玫瑰粉)展开,配合副色 #E8A0B0(浅粉),形成柔和女性化的视觉感受。

4.5 聊天区

@Builder
buildChatArea() {
  Scroll() {
    Column() {
      // 欢迎消息(消息列表为空时显示)
      if (this.messageList.length === 0) {
        this.buildWelcomeMessage();
      }

      // 历史消息气泡
      ForEach(this.messageList, (msg: ChatMessage) => {
        if (msg.role === 'user') {
          this.buildUserBubble(msg.content);
        } else {
          this.buildAIBubble(msg.content);
        }
      }, (msg: ChatMessage) => msg.role + '#' + msg.content)

      // 流式 AI 回复(实时追加)
      if (this.isLoading && this.streamingContent) {
        this.buildAIBubble(this.streamingContent + ' ▍');
      }

      // 加载中占位
      if (this.isLoading && !this.streamingContent) {
        this.buildLoadingIndicator();
      }

      // 错误提示
      if (this.errorMsg) {
        this.buildErrorBubble(this.errorMsg);
      }

      Blank().height(12)  // 底部留白
    }
    .width('100%')
    .padding(12)
  }
  .scrollBar(BarState.Auto)
}

消息列表渲染要点:

  1. 空状态 → 欢迎界面:当 messageList.length === 0 时显示欢迎消息和示例按钮
  2. 历史消息 → 气泡遍历:使用 ForEach 遍历所有历史消息
  3. 流式内容 → 实时追加:在 isLoading && streamingContent 时显示正在累积的 AI 回复,尾部加 闪烁光标
  4. 加载态 → 占位动画isLoading && !streamingContent 表示等待第一个 token
  5. 错误态 → 红色气泡:显示错误信息

4.6 欢迎界面

欢迎界面是用户打开应用后看到的第一个屏幕,包含示例场景按钮:

@Builder
buildWelcomeMessage() {
  Column() {
    Text('💬').fontSize(48).margin({ top: 24, bottom: 8 })

    Text('还在为不知如何回复女友消息而发愁?')
      .fontSize(15).fontColor('#C08A9A')

    Text('把女友对你说的话贴进来,我帮你分析情绪,给出高情商回复建议 ❤️')
      .fontSize(13).fontColor('#D4A5B3')
      .padding({ left: 20, right: 20 })

    Text('试试这些常见场景 👇')
      .fontSize(13).fontColor('#E8A0B0')
      .margin({ bottom: 8 })

    // 示例问题按钮
    ForEach(this.sampleQuestions, (q: string) => {
      Button(q)
        .fontSize(13).fontColor('#D4587A')
        .backgroundColor('#FFF5F0')
        .border({ width: 1, color: '#F5D0D8' })
        .borderRadius(18).height(40).width('90%')
        .margin({ bottom: 8 })
        .onClick(() => { this.inputText = q; })
    }, (_q: string, i?: number) => (i ?? 0).toString())

    // 换一批按钮
    Button('🎲 换一批场景')
      .fontSize(12).fontColor('#E8A0B0')
      .backgroundColor('transparent')
      .border({ width: 1, color: '#F0D0D8' })
      .borderRadius(16).height(34)
      .margin({ top: 4, bottom: 16 })
      .onClick(() => { this.rotateSamples(); })
  }
  .width('100%')
  .alignItems(HorizontalAlign.Center)
}

示例问题轮换算法:

rotateSamples(): void {
  const arr: string[] = this.sampleQuestions;
  // 确定性洗牌(伪随机,每次结果不同但可重现)
  for (let i: number = 0; i < arr.length; i++) {
    const j: number = (i * 7 + 3) % arr.length;
    const tmp: string = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }
  this.sampleQuestions = [...arr];  // 触发 @State 更新
}

使用线性同余法(LCG)实现伪随机排列,不需要额外的随机数 API。每次调用 rotateSamples 都会产生不同的排列顺序。

4.7 消息气泡设计

用户气泡(右对齐,粉红色):

@Builder
buildUserBubble(content: string) {
  Column() {
    Row() {
      Blank()                     // 左占位,推动气泡靠右
      Text(content)
        .fontSize(15).fontColor('#FFFFFF')
        .padding({ left: 16, right: 16, top: 10, bottom: 10 })
        .backgroundColor('#D4587A')   // 主色粉
        .borderRadius({
          topLeft: 18, topRight: 4,   // 右上角小圆角("尾巴"效果)
          bottomLeft: 18, bottomRight: 18
        })
        .maxLines(20).lineHeight(22)
    }
    .width('100%')
    .margin({ top: 6 })
  }
}

AI 气泡(左对齐,白色):

@Builder
buildAIBubble(content: string) {
  Column() {
    Row() {
      Text(content)
        .fontSize(15).fontColor('#3D2B1F')   // 深棕色文字
        .padding({ left: 16, right: 16, top: 10, bottom: 10 })
        .backgroundColor('#FFFFFF')
        .border({ width: 1, color: '#F5E0E5' })
        .borderRadius({
          topLeft: 4, topRight: 18,   // 左上角小圆角("尾巴"效果)
          bottomLeft: 18, bottomRight: 18
        })
        .lineHeight(22)
      Blank()                         // 右占位,推动气泡靠左
    }
    .width('100%')
    .margin({ top: 6 })
  }
}

气泡设计哲学:

用户气泡和 AI 气泡在视觉上形成镜像对称:

特征 用户气泡 AI 气泡
对齐 右对齐 左对齐
背景色 #D4587A(粉) #FFFFFF(白)
文字色 #FFFFFF #3D2B1F(深棕)
尖角位置 右上角 左上角
边框 #F5E0E5 精致边框

这种对话式 UI的经典设计,让用户能一目了然地分辨消息的发送方。

4.8 加载与错误状态

加载动画:

@Builder
buildLoadingIndicator() {
  Column() {
    Row() {
      Text('💭 正在思考...')
        .fontSize(14).fontColor('#E8A0B0')
        .padding(16)
        .backgroundColor('#FFFFFF')
        .border({ width: 1, color: '#F5E0E5' })
        .borderRadius({ topLeft: 4, topRight: 18, bottomLeft: 18, bottomRight: 18 })
      Blank()
    }
    .width('100%')
    .margin({ top: 6 })
  }
}

错误提示:

@Builder
buildErrorBubble(errMsg: string) {
  Column() {
    Row() {
      Text('⚠️ ' + errMsg)
        .fontSize(13).fontColor('#C94A4A')       // 红色文字
        .padding(12)
        .backgroundColor('#FFF0F0')                // 浅红背景
        .border({ width: 1, color: '#F5C0C0' })
        .borderRadius(12)
      Blank()
    }
    .width('100%')
    .margin({ top: 6 })
  }
}

4.9 输入区

@Builder
buildInputArea() {
  Column() {
    Row() {
      // 文本输入框
      TextInput({
        placeholder: '输入女友说的话...',
        text: this.inputText
      })
        .layoutWeight(1)
        .height(44).fontSize(15)
        .fontColor('#3D2B1F')
        .placeholderColor('#D4C5B0')
        .backgroundColor('#FFFFFF')
        .border({ width: 1, color: '#F0D0D8' })
        .borderRadius(22)               // 胶囊圆角
        .padding({ left: 16 })
        .enabled(!this.isLoading)
        .onChange((val: string) => { this.inputText = val; })
        .onSubmit(() => { this.handleSend(); })

      Blank().width(8)

      // 发送按钮
      Button() {
        Text('发送').fontSize(14).fontColor('#FFFFFF')
      }
      .width(60).height(44)
      .backgroundColor(this.isLoading ? '#E8C0C8' : '#D4587A')
      .borderRadius(22)
      .enabled(!this.isLoading && this.inputText.trim().length > 0)
      .onClick(() => { this.handleSend(); })
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 8, bottom: 8 })
    .backgroundColor('#FFFFFF')
    .border({ width: { top: 1 }, color: '#F0D0D8' })

    // 取消按钮(仅加载时显示)
    if (this.isLoading) {
      Button('⏹ 取消回复')
        .fontSize(13).fontColor('#B0A595')
        .backgroundColor('#F5F0E8')
        .border({ width: 1, color: '#E8DDD0' })
        .borderRadius(16).height(32).width(120)
        .margin({ bottom: 6 })
        .onClick(() => { this.handleCancel(); })
    }
  }
  .width('100%')
}

交互细节:

  1. 发送按钮颜色随状态变化:加载时变为 #E8C0C8(浅粉灰),正常时为 #D4587A(主色)
  2. 双重发送触发:点击按钮 + 键盘回车(onSubmit)均可发送
  3. 输入为空禁用enabled 条件包含 inputText.trim().length > 0,防止发送空白消息
  4. 加载时禁用输入enabled(!this.isLoading) 防止用户在 AI 回复时输入新消息
  5. 取消按钮条件渲染:仅 isLoading 为 true 时显示

五、核心交互逻辑

5.1 发送消息(handleSend)

handleSend(): void {
  const msg: string = this.inputText.trim();
  if (!msg) return;

  // 清空输入
  this.inputText = '';

  // 添加用户消息到历史
  const userMsg: ChatMessage = { role: 'user', content: msg };
  this.messageList = [...this.messageList, userMsg];

  // 重置状态
  this.streamingContent = '';
  this.errorMsg = '';
  this.isLoading = true;

  // 调用 AI(携带完整聊天历史)
  queryAI({
    onData: (text: string) => {
      this.streamingContent += text;  // 流式追加
    },
    onDone: () => {
      if (this.streamingContent) {
        const aiMsg: ChatMessage = {
          role: 'assistant',
          content: this.streamingContent
        };
        this.messageList = [...this.messageList, aiMsg];
      }
      this.streamingContent = '';
      this.isLoading = false;
    },
    onError: (errMsg: string) => {
      this.errorMsg = errMsg;
      this.isLoading = false;
    },
  }, this.messageList);
}

数据流时序图:

用户点击发送
    │
    ├──→ 1. inputText 清空
    ├──→ 2. messageList 追加用户消息
    ├──→ 3. isLoading = true
    ├──→ 4. streamingContent = ''
    │
    ├──→ queryAI() 发起网络请求
    │       │
    │       ├──→ onData(text): streamingContent += text  (多次触发)
    │       │
    │       ├──→ onDone(): 
    │       │       ├──→ messageList 追加 AI 回复
    │       │       ├──→ streamingContent = ''
    │       │       └──→ isLoading = false
    │       │
    │       └──→ onError(errMsg):
    │               ├──→ errorMsg = errMsg
    │               └──→ isLoading = false
    │
    └──→ UI 自动刷新(@State 驱动)

为什么将流式内容复制到 messageList?

streamingContent 是一个临时变量,用于实时显示 AI 正在输出的文字。当 onDone 回调触发时,我们将完整的 streamingContent 作为一个 ChatMessage 加入 messageList。这样做的原因是:

  1. messageList 是消息历史,@State 数组采用不可变更新([...list, newMsg]
  2. 如果逐字追加到 messageList,每次 onData 都会创建新数组,性能开销大
  3. 使用 streamingContent(字符串)做临时累积,只有最终完成时才更新数组

5.2 取消请求(handleCancel)

handleCancel(): void {
  cancelAI();                          // 终止 HTTP 请求
  this.isLoading = false;

  // 保留已收到的流式内容
  if (this.streamingContent) {
    const aiMsg: ChatMessage = {
      role: 'assistant',
      content: this.streamingContent
    };
    this.messageList = [...this.messageList, aiMsg];
  }
  this.streamingContent = '';
}

取消操作的设计原则是保留部分结果而非完全丢弃。如果 AI 已经输出了部分回复,取消后这部分内容仍然作为完整的 AI 回复加入聊天记录。这比完全丢弃给用户更好的体验。


六、网络层与 UI 层的协奏

6.1 模块导出与导入

// AIChatService.ets — 导出
export { queryAI, cancelAI, ChatMessage };

// index4.ets — 导入
import { queryAI, cancelAI, ChatMessage } from './AIChatService';

ArkTS 使用 ES Module 规范的 import/export 语法。同一个模块内的文件使用相对路径引用。

6.2 异步模式的演进

在 ArkTS 中处理网络请求有三种常见模式:

模式 适用场景 本例使用
Callback 回调 一次性事件(如 request 完成) request() 的 err 回调
Event 事件监听 多次触发(如 SSE 流式) on('dataReceive')
Promise async/await 串联异步操作 暂未使用

queryAI 函数采用了事件 + 回调的混合模式:

  • 流式数据通过 httpRequest.on('dataReceive', callback) 事件监听
  • 完成/错误通过 AICallbacks 接口中的回调通知调用方
  • 取消通过独立的 cancelAI() 函数

七、主题与样式系统

7.1 色彩体系

女友对话助手采用了暖粉色调的色彩方案:

色值 用途 样例
#FFF0E8 页面背景 暖白粉底色
#D4587A 主色(标题、用户气泡、发送按钮) 玫瑰粉
#E8A0B0 副色(标语、占位文字) 浅粉
#C08A9A 欢迎文字 中粉
#D4A5B3 欢迎说明 柔和粉
#FFF5F0 示例按钮背景 极浅粉
#F5D0D8 示例按钮边框 粉边框
#FFFFFF AI 气泡背景、标题栏、输入区背景 白色
#3D2B1F AI 气泡文字 深棕色
#C94A4A 错误提示文字 红色
#FFF0F0 错误提示背景 浅红色
#D4C5B0 输入框占位文字 米色

7.2 字体与圆角系统

// 字号规范
标题     → fontSize: 22
主内容   → fontSize: 15
次要内容  → fontSize: 13 ~ 14

// 圆角规范
按钮/气泡 → borderRadius: 18 ~ 22
输入框    → borderRadius: 22(胶囊形)
示例按钮  → borderRadius: 18
小型按钮  → borderRadius: 16

// 线条
边框宽度   → 1px
边框颜色   → '#F0D0D8' / '#F5D0D8'
分割线宽度 → 1px

7.3 消息气泡的差异圆角

// 用户气泡(尖角在右上)
.borderRadius({
  topLeft: 18, topRight: 4,    // 右上角几乎直角
  bottomLeft: 18, bottomRight: 18
})

// AI 气泡(尖角在左上)
.borderRadius({
  topLeft: 4, topRight: 18,    // 左上角几乎直角
  bottomLeft: 18, bottomRight: 18
})

这种差异圆角(asymmetric rounded corners)是 IM 类应用气泡 UI 的标志性设计:指向对侧的角几乎为直角,形成"对话尾巴"的视觉效果。


八、构建验证与调试

8.1 编译检查

hvigorw assembleHap --daemon=false --analyze=false

构建输出示例:

Finished :entry:default@CompileResource...
Finished :entry:default@CompileArkTS... after 1 s 920 ms
Finished :entry:default@PackageHap... after 589 ms
BUILD SUCCESSFUL in 4 s 200 ms

8.2 常见编译问题

问题 1:网络模块未导入

// ❌ 错误:找不到 http
import { http } from '@kit.NetworkKit';

// ✅ 正确:@kit.NetworkKit 是系统内置 Kit,无需额外安装

问题 2:ArrayBuffer 解码性能

arrayBufferToString 中使用逐字节 String.fromCharCode,在小数据量下性能可接受。对于超大响应体,可以考虑使用 TextDecoder API(如果在目标 SDK 版本中可用)。

问题 3:API Key 泄露

当前 API Key 硬编码在源码中,这是一个安全风险。生产环境中应该:

  • 将 API Key 存储在安全配置中
  • 通过后端服务代理转发,避免前端直接持有
  • 使用 HarmonyOS 的安全存储 API(如 @kit.UniversalKeystoreKit)加密存储

九、扩展方向

基于当前架构,可以从以下方向扩展:

9.1 语音消息支持

集成语音识别(ASR)API,支持用户通过语音输入问题。HarmonyOS NEXT 提供了 @kit.VoiceRecognitionKit 可以实现语音转文字。

9.2 多种 AI 角色切换

当前应用专注于"女友对话助手",可以扩展为多角色平台:

┌─────────────────────────┐
│  选择 AI 角色            │
│                         │
│  💕 女友对话助手         │
│  👔 职场沟通导师         │
│  📚 学习辅导老师         │
│  🏠 家庭关系顾问         │
│  🧘 心理咨询师           │
└─────────────────────────┘

每个角色对应不同的系统提示词,切换时替换 SYSTEM_PROMPT 即可。

9.3 会话历史持久化

使用 HarmonyOS 的 Preferences 或关系型数据库(RDB)存储聊天记录,支持用户关闭应用后恢复对话。

9.4 情感分析可视化

在 AI 回复的同时,增加情感分析的可视化展示——用表情符号或百分比显示识别到的情绪维度(开心、难过、生气、焦虑等)。

9.5 消息搜索与收藏

支持用户搜索聊天历史中的关键词,以及收藏有用的回复建议。


十、总结

本文通过一个完整的 AI 对话助手的实现,系统地展示了在 HarmonyOS NEXT 上开发 AI 聊天应用的完整流程。从 AIChatService 网络层的 SSE 流式解析、非流式回退、请求取消,到 index4 UI 层的聊天气泡、流式文字展示、状态管理,涵盖了 ArkTS 开发中的多个关键技术点。

核心知识点回顾

技术点 应用位置 关键代码
SSE 流式解析 AIChatService.ts parseSSEDataLine() + on('dataReceive')
非流式回退 AIChatService.ts parseFullSSEBody() + parseNonStreamingBody()
HTTP 请求取消 AIChatService.ts httpRequest.destroy()
@State 数组更新 index4.ets [...this.messageList, newMsg]
流式文字累积 index4.ets this.streamingContent += text
差异圆角气泡 index4.ets borderRadius({ topLeft, topRight, ... })
条件渲染 index4.ets if (this.isLoading) { ... }
FlexWrap 布局 index4.ets Flex({ wrap: FlexWrap.Wrap })

ArkTS 开发 AI 应用的优势

  1. 原生网络支持@kit.NetworkKit 提供了完整的 HTTP 客户端能力,包括事件驱动的流式数据接收
  2. 声明式 UI 与流式数据天然契合@State + streamingContent 的组合让 AI 逐字展示变得非常简单
  3. 类型安全:ArkTS 的严格类型检查减少了 API 调用中的数据格式错误
  4. 丰富的 UI 组件TextInputButtonScrollFlex 等组件开箱即用

HarmonyOS NEXT 正在逐步完善其 AI 能力生态。随着更多 AI Kit 的开放,未来在鸿蒙原生应用中集成 AI 功能将变得更加便捷。希望本文能为你在 HarmonyOS NEXT 上开发 AI 应用提供有价值的参考和启发。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐