鸿蒙5.0项目开发——接入有道大模型翻译
鸿蒙5.0项目开发——接入有道大模型翻译
【高心星出品】
项目效果图
项目功能
-
文本翻译功能
-
支持文本输入和翻译结果显示
-
使用有道翻译API进行翻译
-
支持自动检测语言(auto)
-
支持双向翻译(源语言和目标语言可互换)
-
-
文本操作功能
-
支持文本复制
-
支持文本全选
-
支持长按选择文本
-
支持滚动查看长文本
-
-
生词本功能
-
可以将翻译结果保存到生词本
-
支持保存选中文本的翻译
-
支持保存整个翻译结果
-
记录保存时间
-
-
用户界面特点
-
采用上下布局,上方为输入区,下方为结果显示区
-
支持实时翻译
-
提供清晰的视觉反馈
-
支持长按菜单操作
-
-
数据存储
-
使用鸿蒙系统的数据存储能力
-
支持生词本的本地存储
-
支持历史记录保存
-
-
网络功能
-
集成有道翻译API
-
支持HTTP请求
-
支持错误处理
-
支持数据流式传输
-
-
安全特性
-
使用API密钥进行身份验证
-
支持数据加密传输
-
实现签名验证机制
-
-
其他功能
-
支持剪贴板操作
-
提供操作提示(Toast提示)
-
支持文本格式化
-
支持多语言界面
-
大模型翻译 API 简介
大型模型翻译:翻译的好助手,使用此服务可以完成翻译、润色、扩写等功能。API可以处理各种复杂的语言结构、词汇和语境,提供高质量的翻译结果。 同时,可以根据用户的需 求和偏好进行定制化的翻译。用户可以通过调整参数、提供上下文信息或者进行反馈,使翻译结果更符合个人或特 定领域的要求,从而实现更加精准、个性化的翻译体验。
接入有道翻译过程
-
首先要注册成为有道智云的开发者并创建应用:https://ai.youdao.com/doc.s#guide,就可以拿到
应用ID
和应用密钥
。 -
大模型翻译API HTTPS地址:
https://openapi.youdao.com/llm_trans
- 请求方式:
规则 | 描述 |
---|---|
传输方式 | HTTPS |
请求方式 | GET/POST |
字符编码 | 统一使用UTF-8 编码 |
请求格式 | 表单 |
响应格式 | text/event-stream |
- 请求参数:
字段名 | 类型 | 含义 | 必填 | 备注 |
---|---|---|---|---|
i | text | 待翻译文本 | True | 必须是UTF-8编码,限制5000字符 |
prompt | text | 提示词 | False | 必须是UTF-8编码,限制1200字符、400单词 |
from | text | 源语言 | True | 参考下方支持语言 (可设置为auto) |
to | text | 目标语言 | True | 参考下方支持语言 |
streamType | text | 流式返回类型 | False | 参考下方 流式返回类型 |
appKey | text | 应用ID | True | 可在应用管理 查看 |
salt | text | 随机字符串,可使用UUID进行生产 | True | uuid (可使用uuid生成) |
sign | text | 签名 | True | sha256(应用ID+input+salt+curtime+应用密钥) |
signType | text | 签名类型 | True | v3 |
curtime | text | 当前UTC时间戳(秒) | True | TimeStamp |
handleOption | text | 处理模式选项 | False | 参考下方 处理模式选项 |
polishOption | text | 润色选项 | False | 参考下方 润色选项 |
expandOption | text | 扩写选项 | False | 参考下方 扩写选项 |
签名生成方法如下: signType=v3; sign=sha256(
应用ID
+input
+salt
+curtime
+应用密钥
); 其中,input的计算方式为:input
=i前10个字符
+i长度
+i后10个字符
(当i长度大于20)或input
=i字符串
(当i长度小于等于20);
- 流式返回类型SSE:
event:begindata:{"requestId":"11","type":"zh-CHS2en"}event:messagedata:{"transFull":null,"transIncre":"The"}event:messagedata:{"transFull":null,"transIncre":" w"}...............event:enddata:{"requestId":"11","type":"zh-CHS2en","eventTokenUsage":{"inputToken":5,"outputToken":7,"totalToken":12}}
网络请求工具封装
由于大模型翻译获取的是增量的翻译结果,一次应答只能获取部分翻译结果,所以我们发送请求的方式要用requestInStream发起流式请求,然后在 req.on(‘dataReceive’, (data) =>{})中获取每次返回的SSE结果。
此工具封装最复杂的就是请求参数的获取,时间戳curtime要获取当前系统时间的秒级结果,并且服务器会将服务器时间与发送请求的时间戳进行对比,如果差距超过15分钟,请求就会失败,所以要关注一下运行该段代码设备的时间。
/*** 生成请求参数* @param q 待翻译的文本* @returns 包含签名等信息的请求参数对象*/
export function genparm(q: string): reqparam {q = q.trim()let salt = util.generateRandomUUID()let curtime = Math.round(new Date().getTime() / 1000) + ''let param: reqparam = {i: q,q: q,from: 'auto',to: 'auto',appKey: APPKEY,curtime: curtime,signType: 'v3',salt: salt,sign: sign(q, curtime, salt)}return param
}/*** 生成签名* @param q 待翻译的文本* @param curtime 当前时间戳* @param salt 随机字符串* @returns SHA256加密后的签名*/
function sign(q: string, curtime: string, salt: string) {let str = APPKEY + getinput(q) + salt + curtime + APPSECRETlet result: string = ''try {let mdAlgName = 'SHA256'; // 使用SHA256算法let md = cryptoFramework.createMd(mdAlgName);// 更新数据md.updateSync({ data: new Uint8Array(buffer.from(str, 'utf-8').buffer) });let mdResult = md.digestSync();// 将摘要结果转换为十六进制字符串result = Array.from(mdResult.data).map(byte => byte.toString(16).padStart(2, '0')).join('');} catch (error) {console.error('SHA256编码失败:', error);}return result
}/*** 处理输入文本* @param q 待处理的文本* @returns 处理后的文本* 如果文本长度大于20,则取前10个字符和后10个字符,中间加上长度*/
function getinput(q: string) {let len = q.lengthlet result: stringif (len <= 20) {result = q} else {let startstr = q.substring(0, 10)let endstr = q.substring(len - 10, len)result = startstr + len + endstr}return result
}/*** 发送LLM翻译请求* @param q 待翻译的文本* @param recievedata 接收数据的回调函数* @returns Promise<number> 请求ID*/
export function postllm(q: string, recievedata: (data: string) => void) {let req = http.createHttp()let param = genparm(q)let opt: http.HttpRequestOptions = {method: http.RequestMethod.POST,header: {'Content-Type': 'application/x-www-form-urlencoded'},extraData: `i=${param.i}&q=${param.q}&from=auto&to=auto&appKey=${param.appKey}&curtime=${param.curtime}&signType=v3&salt=${param.salt}&sign=${param.sign}`}let strs: string[] = []req.on('dataReceive', (data) => {// 处理接收到的数据let result = buffer.from(data).toString()console.log('gxxt result ', result)if (!result.endsWith('\n')) {strs.push(result.substring(result.lastIndexOf('\n') + 1))result = result.substring(0, result.lastIndexOf('\n') + 1)} else {if (strs.length > 0) {result = strs.join('') + resultstrs = []}}console.log('gxxt newresult ', result)recievedata(result)})return new Promise<number>((resolve, reject) => {req.requestInStream(BASEURL, opt).then((num) => {resolve(num)}).catch((e: Error) => {reject(e.message)})})
}
流式返回数据的处理
目前鸿蒙还没有支持解析SSE数据的工具,需要开发者自己封装,其实就是对于字符串的处理。鉴于SSE的结构,可以按照\n\n双换行来切割获取不同的事件类型,然后只处理event:message事件即可。
/*** 解析服务器返回的SSE(Server-Sent Events)数据* @param data 服务器返回的原始数据字符串* @returns 解析后的翻译结果数组,如果发生错误则返回undefined*/export function getResult(data: string) {let ret: string[] = []// 检查是否包含错误信息if (data.lastIndexOf('event:error') !== -1) {return}// 按事件分割数据let events = data.split('\n\n')events.forEach((item: string) => {// 处理消息事件if (item.indexOf('event:message') !== -1) {let data1 = item.split('event:message\n')data1.forEach((itemn: string) => {// 提取JSON数据并解析if (itemn.length > 1 && itemn.indexOf('{') !== -1) {let jsondata = itemn.substring(itemn.indexOf('{'), itemn.indexOf('}') + 1)let res = JSON.parse(jsondata) as resdataret.push(res.transIncre)}})}})return ret}
主界面代码
这是一个名为"星星翻译"的鸿蒙应用主界面,界面设计简洁实用,主要分为三个部分:
顶部是标题栏,显示"星星翻译"的应用名称,采用简洁的白色背景设计。
中间是核心的翻译区域,采用上下分栏布局:
-
上方是文本输入区,用户可以在这里输入需要翻译的内容
-
下方是翻译结果显示区,实时显示翻译结果
-
两个区域之间有一个翻译按钮,点击即可执行翻译操作
-
翻译结果支持长按选择文本,可以进行复制、全选等操作
底部是功能操作栏,提供两个主要功能按钮:
-
复制按钮:可以将翻译结果一键复制到剪贴板
-
生词本按钮:可以将当前的翻译内容保存到生词本中,方便后续复习
/*** 星星翻译应用主页面* 提供文本翻译、复制结果、生词本记录等功能*/
import { postllm } from '../utils/HttpUtils';
import { getResult, gettime } from '../utils/StringUtils';
import { contentview } from '../views/contentview';
import { footerview } from '../views/footerview';
import { headerview } from '../views/headerview';
import { pasteboard } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { saveshengci } from '../utils/Dbutils';
import { common } from '@kit.AbilityKit';/*** 翻译应用主页面组件*/
@Entry
@Component
struct Index {// 源文本和翻译结果状态@State @Watch('clear') sourcetext: string = '' // 源文本,监听变化@State targettext: string = '' // 翻译结果private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext // 应用上下文/*** 源文本清空时的监听函数* 当源文本为空时,清空翻译结果*/clear() {if (this.sourcetext === '') {this.targettext = ''}}/*** 翻译结果回调函数* @param data 翻译返回的数据* @description 处理翻译结果并更新到目标文本*/cb(data: string) {getResult(data)?.forEach((item) => {this.targettext += item})}/*** 执行翻译操作* @description * 1. 检查源文本是否为空* 2. 清空之前的翻译结果* 3. 调用翻译API* 4. 处理可能的错误*/to: () => void = () => {if (this.sourcetext) {this.targettext = ''postllm(this.sourcetext, this.cb.bind(this)).catch((e: string) => {console.error('gxxt ', e)})} else {AlertDialog.show({title: '提示', message: '请输入要翻译的内容', confirm: {value: '确定', action: () => {}}})}}/*** 复制翻译结果到剪贴板* @description * 1. 检查是否有翻译结果* 2. 创建剪贴板数据* 3. 设置到系统剪贴板* 4. 显示操作结果提示*/cpck: () => void = () => {if (this.targettext !== '') {let pasttext = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.targettext)let jianqieban = pasteboard.getSystemPasteboard()jianqieban.setDataSync(pasttext)promptAction.showToast({ message: '已经复制到剪切板' })} else {promptAction.showToast({ message: '没有翻译结果,无法复制到剪切板' })}}/*** 保存到生词本* @description * 1. 记录翻译内容* 2. 记录原文* 3. 记录保存时间*/tobeiwanglu: () => void = () => {saveshengci({content: this.targettext,trans: this.sourcetext,time: gettime()}, this.context)}/*** 构建UI界面* @description * 1. 顶部标题栏* 2. 中间内容区域(源文本和翻译结果)* 3. 底部操作栏(复制和生词本功能)*/build() {Column() {headerview({ text: '星星翻译', isleft: false })contentview({ sourcetext: this.sourcetext, targettext: this.targettext, transopt: this.to })footerview({ clipck: this.cpck, tobeiwanglu: this.tobeiwanglu })}.width('100%').height('100%')}
}
子组件contentview核心代码:
/*** contentview.ets* 翻译内容视图组件* 提供文本输入、翻译结果显示、文本选择、复制、生词本等功能*/import { promptAction } from "@kit.ArkUI"
import { pasteboard } from "@kit.BasicServicesKit"
import { saveshengci } from "../utils/Dbutils"
import { posttxt } from "../utils/HttpUtils"
import { common } from "@kit.AbilityKit"
import { gettime } from "../utils/StringUtils"/***作者:gxx*时间:2025/5/6 14:51*功能:**/
@Preview
@Component
export struct contentview {// 源文本,用于存储用户输入的待翻译文本@Link sourcetext: string// 目标文本,用于存储翻译结果@Link targettext: string// 获取UI上下文private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext// 翻译操作回调函数transopt: () => void = () => {}// 当前选中的文本private selecttext: string = ''// 是否全选状态@State isquanxuan: boolean = false// 是否显示菜单@State ismenushow: boolean = truebuild() {Stack() {Column() {// 输入区域Column() {TextArea({ placeholder: '要翻译的文本' }).width('100%').height('100%').backgroundColor(Color.Transparent).onChange((value) => {this.sourcetext = value.trim()})}.width('100%').height('45%').padding(10).border({ width: 2, color: '#eee' }).borderRadius({ topLeft: 10, topRight: 10 }).backgroundColor(Color.White)// 翻译结果区域Column() {Scroll() {Text(this.targettext === '' ? '翻译结果' : this.targettext).width('100%').copyOption(CopyOptions.InApp).fontWeight(FontWeight.Bolder).backgroundColor(Color.Transparent).fontColor(this.targettext === '' ? Color.Gray : Color.Black)// 绑定长按菜单.bindSelectionMenu(TextSpanType.TEXT, this.genselectmenu(), TextResponseType.LONG_PRESS, {onDisappear: () => {this.ismenushow = truethis.isquanxuan = false}})// 文本选择变化监听.onTextSelectionChange((start: number, end: number) => {this.selecttext = this.targettext.substring(start, end)})// 全选状态控制.selection(this.isquanxuan ? 0 : -1, this.isquanxuan ? this.targettext.length : 0)}.scrollable(ScrollDirection.Vertical)}.width('100%').height('45%').padding(10).backgroundColor('#eee').borderRadius({ bottomLeft: 10, bottomRight: 10 })}.width('100%').height('100%').justifyContent(FlexAlign.SpaceBetween)// 翻译按钮SymbolGlyph($r('sys.symbol.reverse_order')).fontSize(35).fontWeight(FontWeight.Bolder).border({ width: 2, radius: 10 }).padding(5).stateStyles({normal: {.backgroundColor(Color.White)},pressed: {.backgroundColor(Color.Gray)}}).onClick(() => {this.transopt()})}.width('100%').height('80%').padding(10)}/*** 生成文本选择菜单* 包含复制、全选、添加到生词本等功能*/@Buildergenselectmenu() {Row({ space: 15 }) {// 复制按钮Text('复制').fontSize(12).onClick(() => {let pasttext = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.selecttext)let jianqieban = pasteboard.getSystemPasteboard()jianqieban.setDataSync(pasttext)promptAction.showToast({ message: '已经复制到剪切板' })this.ismenushow = false})// 全选按钮(仅在非全选状态显示)if (!this.isquanxuan) {Text('全选').fontSize(12).onClick(() => {this.isquanxuan = true})}// 生词本按钮Text('生词本').fontSize(12).onClick(() => {this.ismenushow = falseif (this.isquanxuan) {// 全选状态下保存整个翻译结果saveshengci({content: this.targettext,trans: this.sourcetext,time: gettime()}, this.context)} else {// 选中状态下翻译选中文本并保存posttxt(this.selecttext).then((value) => {let target = value.query// 拼接好的翻译结果let source = value.translation.join(',')saveshengci({content: target,trans: source,time: gettime()}, this.context)}).catch((e: Error) => {console.error('gxxt 文本翻译结果: ', e.message)})}})}.backgroundColor(Color.White).padding(10).borderRadius(20).border({ width: 1 }).visibility(this.ismenushow ? Visibility.Visible : Visibility.None)}
}
生词本页面代码
这是一个生词本界面,采用简洁现代的设计风格。界面顶部是标题栏,显示"生词本"标题,左侧配有返回按钮,方便用户返回上一页面。
主体部分是一个可滚动的生词列表,每个生词条目以卡片形式展示,包含两个主要信息:上方显示生词内容,采用较大字号和粗体样式;下方显示对应的翻译内容,使用灰色字体。每个条目右侧都有一个箭头图标,提示用户可以点击查看详情。
用户可以通过左滑生词条目来显示删除按钮,点击删除按钮会弹出确认对话框,防止误操作。点击生词条目会弹出一个详情对话框,以更大的字体展示完整的生词内容和翻译,并显示保存时间。如果内容较长,对话框支持滚动查看。
/*** shengciben.ets* 生词本页面组件* 提供生词列表展示、详情查看、删除等功能*/import { shengci } from '../model/shengci';
import { delbyid, querylimit } from '../utils/Dbutils';
import { common } from '@kit.AbilityKit';
import { headerview } from '../views/headerview';
import { ComponentContent, router } from '@kit.ArkUI';/*** 生成生词详情对话框* @param p 对话框参数,包含生词信息、删除回调、滚动高度等*/
@Builder
function gendialog(p: param) {Stack() {// 关闭按钮SymbolGlyph($r('sys.symbol.xmark')).fontSize(20).onClick(() => {p.delck()}).zIndex(2)Column({ space: 10 }) {// 标题Text('生词详情').fontSize(25).fontWeight(FontWeight.Bolder)Divider().color(Color.Grey).margin({ top: 10, bottom: 10 })// 内容区域Scroll() {Column({ space: 10 }) {// 生词内容Text(p.item.content).fontSize(18).fontWeight(FontWeight.Bold)// 翻译内容Text(p.item.trans).fontSize(14).fontColor(Color.Gray)}.alignItems(HorizontalAlign.Start)}.height(p.scrollheight)Divider().color(Color.Grey).margin({ top: 10, bottom: 10 })// 时间信息Text('时间:').fontSize(18).fontWeight(FontWeight.Bold)Text(p.item.time).fontSize(14).fontColor(Color.Gray)}.width('100%').alignItems(HorizontalAlign.Start)}.width('80%').borderRadius(10).alignContent(Alignment.TopEnd).backgroundColor(Color.White).border({ width: 1 }).padding(10)
}/*** 对话框参数接口*/
interface param {item: shengci, // 生词信息delck: () => void, // 删除回调函数scrollheight: Length, // 滚动区域高度
}/*** 生词本页面组件*/
@Entry
@Component
struct Shengciben {@State message: string = 'Hello World';@State datas: shengci[] = [] // 生词列表数据private builder: ComponentContent<param> | null = nullprivate context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext/*** 组件即将出现时加载数据*/aboutToAppear(): void {querylimit(this.context, 1).then((value) => {this.datas = value}).catch((e: Error) => {console.error('gxxt 查询生词本错误: ', e.message)})}build() {Column() {// 顶部标题栏headerview({text: '生词本', isright: false, leftck: () => {router.back()}})// 生词列表List({ space: 5 }) {ForEach(this.datas, (item: shengci, index: number) => {ListItem() {this.genlistitem(item)}.swipeAction({ end: this.genitemend(item.id, index) }) // 左滑显示删除按钮.onClick(() => {// 点击显示详情弹窗let p: param = {item: item,delck: () => {this.getUIContext().getPromptAction().closeCustomDialog(this.builder)},scrollheight: item.content.length + item.trans.length > 150 ? 300 : 'auto'}this.builder = new ComponentContent(this.getUIContext(), wrapBuilder<[param]>(gendialog), p)this.getUIContext().getPromptAction().openCustomDialog(this.builder, {alignment: DialogAlignment.Center})})})}.margin({ top: 5 })}.width('100%').height('100%')}/*** 生成列表项右滑删除按钮* @param id 生词ID* @param index 列表索引*/@Buildergenitemend(id: number, index: number) {Row() {SymbolGlyph($r('sys.symbol.trash_fill')).fontSize(30).fontColor([Color.Red])}.padding({left: 10,top: 5,bottom: 5,right: 10}).border({ width: 1, radius: 10 }).margin(5).onClick(() => {// 删除确认对话框AlertDialog.show({title: '提示',message: '确定要删除吗?',primaryButton: {value: '确定', action: () => {delbyid(id, this.context).then(() => {this.datas.splice(index, 1)})}},secondaryButton: {value: '取消', action: () => {}}})})}/*** 生成列表项内容* @param item 生词信息*/@Buildergenlistitem(item: shengci) {Row() {Column({ space: 5 }) {// 生词内容Text(item.content).fontSize(20).fontWeight(FontWeight.Bolder).width('60%').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1)// 翻译内容Text(item.trans).fontSize(14).fontColor(Color.Gray).width('60%').textOverflow({ overflow: TextOverflow.Ellipsis }).maxLines(1)}Blank()// 右箭头图标SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(20)}.width('100%').padding({left: 5,top: 10,bottom: 10,right: 5}).border({ width: 1, radius: 5 })}
}
完整项目代码:
https://download.csdn.net/download/gao_xin_xing/90892395