ArkTS 流式响应与UI布局

举报
yd_241646257 发表于 2025/06/04 19:50:36 2025/06/04
【摘要】 接到任务,要做一款很牛逼的应用,具体有多牛逼,HarmonyOS Plus AI。有点类似于 ChatGPT,能进行问答交互,接入专门的大模型接口,快速生成相关领域知识。

ArkTS 流式响应与UI布局

  1. 背景说明
      接到任务,要做一款很牛逼的应用,具体有多牛逼,HarmonyOS Plus AI。有点类似于 ChatGPT,能进行问答交互,接入专门的大模型接口,快速生成相关领域知识。

 

  1. 流式响应
      - 核心功能用户和应用交互,需要实现类似打字机输出方式的流式返回,而不是等待很久,一次性显示回复信息。这就要用到了 SSE 类型的网络请求。
      - SSEServer-Sent Events)是一种基于 HTTP 协议的协议,允许服务器向客户端推送事件。这意味着客户端不再需要不断地向服务器请求数据,服务器可以主动将数据推送给客户端,并且只支持服务器到客户端的单向通信。
      - 工作原理: 客户端通过普通的 HTTP 请求连接到服务器,并通过特定的 HTTP 头信息告知服务器它希望保持连接以便接收实时数据。服务器接收到这个请求后,保持连接不断开,并周期性地向客户端发送消息。

请求响应示例:

 

  - 前后端约定传输数据格式,根据每条数据的特定标识符获取数据,如:event=message,根据特定的标识符判断是否为结尾数据,如:event=message_end

  - 前端并根据对应后端返回响应数据的格式,解析 json 字符串,提取回复的文本进行拼接。

 

  1. ArkTS 流式响应
  • @kit.NetworkKit中导入 http 命名空间。

TypeScript
import { http } from '@kit.NetworkKit';

  • 调用createHttp()方法,创建一个HttpRequest对象。

TypeScript
let httpRequest = http.createHttp();

  • 调用on()方法,可以根据业务需要订阅HTTP响应头事件、HTTP流式响应数据接收事件、HTTP流式响应数据接收进度事件和HTTP流式响应数据接收完毕事件。

TypeScript
// 用于订阅HTTP流式响应数据接收进度事件
httpClient.on('dataReceiveProgress', (data: Data) => {
      console.log(TAG, "dataReceiveProgress receiveSize:" + data.receiveSize + ", totalSize:" + data.totalSize);
});
// 用于订阅HTTP响应头事件
httpRequest.on('headersReceive', (header: Object) => {
        console.info('header: ' + JSON.stringify(header));
});
// 用于订阅HTTP流式响应数据接收事件
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
   
});
// 用于订阅HTTP流式响应数据接收完毕事件
httpRequest.on('dataEnd', () => {
        console.info('No more data in response, data receive end');
});

  • 调用该对象的requestInStream()方法,传入http请求的url地址和可选参数,发起网络请求。

TypeScript
let httpRequestOptions: http.HttpRequestOptions = {
        method: http.RequestMethod.POST,
        header: {
                "Content-Type": 'application/json',
                "Authorization":
Bearer ${AppStorage.get<string>('docToken')}
        },
        extraData: JSON.stringify({
        query: this.prompt,
                inputs: new Map(),
                response_mode: "streaming",
                user:'mythcsj'
        })
}
httpRequest.requestInStream(url, httpRequestOptions).then((data: number) => {
        // ……
});

  • 按照实际业务需要,可以解析返回的响应码。

TypeScript
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
        const decoder = util.TextDecoder.create(
"utf-8"
);
        const str = decoder.decodeWithStream(new Uint8Array(data));
        let resultArr: Array<string> = str.split('\n');
        let answer = ''
        for (const result of resultArr) {
                if (result == '' || result.indexOf('event: ping') >= 0) {
                        break;
                }
                if (result.indexOf('*') >= 0) {
                        break;
                }
                const messageMeta = JSON.parse(result.split('data:')[1]) as MessageMeta
                if (messageMeta.event == 'message') {
                        answer += messageMeta.answer
                        AppStorage.setOrCreate<string>('currentTaskId', messageMeta.task_id);
                } else if (messageMeta.event == 'message_end') {
                        AppStorage.setOrCreate<string>('currentMessageId', messageMeta.message_id);
                        AppStorage.setOrCreate<string>('currentConversationId', messageMeta.conversation_id);
                        const retriever_resources = messageMeta.metadata.retriever_resources
                        for (const retrieverResource of retriever_resources) {
                                answer += "\n\n相关文献:【" + retrieverResource.dataset_name + "】 (" + retrieverResource.content + ")"
                        }
                }
        }
        console.log(
resultArr:${resultArr}
)
        return answer;
});

  • 调用该对象的off()方法,取消订阅响应事件。

TypeScript
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 取消订阅HTTP流式响应数据接收事件
httpRequest.off('dataReceive');
// 取消订阅HTTP流式响应数据接收进度事件
httpRequest.off('dataReceiveProgress');
// 取消订阅HTTP流式响应数据接收完毕事件
httpRequest.off('dataEnd');

  • 当该请求使用完毕时,调用destroy()方法主动销毁。

HTML
// 请求使用完毕,调用destroy方法主动销毁
httpRequest.destroy();

 

  1. ArkUI 流式响应
  • 会话存储对象

TypeScript
export interface Chat {
  groupId: string
  type: ChatType,
  content: string
}
……
@State arrayList: Chat[] = []

  • 在响应完成之后才有会状态响应码,可以用 Promise 封装返回状态,用于页面逻辑处理

TypeScript
getHttpStreamResult(callback: (result: string) => void) {
        return new Promise<number>((resolve, reject) => {
                this.httpClient.on('dataReceive', (data: ArrayBuffer) => {
                        ……
                        callback(resultStr);
                });
                let httpRequestOptions: http.HttpRequestOptions = {
                        method: http.RequestMethod.POST,
                        header: {
                                "Content-Type": 'application/json',
                                "Authorization": `Bearer ${AppStorage.get<string>('docToken')}`
                        },
                        extraData: JSON.stringify({
                                query: this.prompt,
                                inputs: new Map(),
                                response_mode: "streaming",
                                user:'mythcsj'
                        })
                }
                const httpStatus = await this.httpClient.requestInStream(url,requestOption);
        })
}

onClick 事件调用流式响应接口

TypeScript
// 缓存提问框文本
const query = this.query
if (query == '') {
        promptAction.showToast({
                message: "请输入问题"
        })
        return
}
// 清空提问框文本
this.query = ''
this.send = true
this.currentGroupId = UUID();
// 记录用户提问
this.arrayList.push({ groupId: this.currentGroupId, type: ChatType.USER, content: query })
// 拼接接口返回数据
this.answer = ''
const doc = new DocHttp(query)
const httpStatus = await doc.getHttpStreamResult((result: string) => {
        this.answer += result
})
// 请求完毕后,记录 AI 对话记录
if (httpStatus == 200) {
        this.arrayList.push({ groupId: this.currentGroupId, type: ChatType.BOT, content: this.answer })
        this.currentGroupId = ''
        this.send = false
}

自定义对话组件

TypeScript
@Component
export struct BotChat {
        @Prop answer: string = ''
        build() {
                ListItem() {
                        Row({ space: 10 }) {
                                Image($r('app.media.logo'))
                                        .width(80)
                                        .height(80)
                                        .borderRadius(40)
                                        .objectFit(ImageFit.Contain)
                                Text(this.answer).chatStyle()
                        }.alignItems(VerticalAlign.Top)
                }.width('100%')
        }
}
@Component
export struct UserChat {
        @Prop query: string = ''
        build() {
                ListItem() {
                        Row({ space: 10 }) {
                                Text(this.query).chatStyle()
                                Image($r('app.media.user')).chatPhotoStyle()
                        }.alignItems(VerticalAlign.Top)
                }.width('100%')
        }
}
@Extend(Image)
function chatPhotoStyle() {
        .width(80)
        .height(80)
        .borderRadius(40)
        .borderWidth(1)
        .borderColor(Color.Red)
        .fillColor(Color.Red)
        .objectFit(ImageFit.Contain)
}
@Extend(Text)
function chatStyle() {
        .borderRadius(10)
        .backgroundColor('#fbebfc')
        .fontSize(20)
        .width('70%')
        .padding(20)
}

Index.ts

TypeScript
@State @Watch('onAnswer') answer: string = ''
private scroller: Scroller = new Scroller()
// 设置监听变量,流式响应时,滚动条始终在底部
onAnswer() {
        this.scroller.scrollEdge(Edge.Bottom)
}
……
// 加载显示对话数据
List({ space: 10, scroller: this.scroller }) {
        ForEach(this.arrayList, (item: Chat) => {
                //用户提问
                if (item.type == ChatType.USER) {
                        UserChat({ query: item.content })
                }
                //当前 GroupId 等于当前遍历对象,说明是接口响应中,展示响应是文本,否则展示存储好的对话记录
                if (this.currentGroupId == item.groupId) {
                        BotChat({ answer: this.answer })
                } else if (item.type == ChatType.BOT) {
                        BotChat({ answer: item.content })
                }
        })
}

 

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。