当前位置: 首页 > news >正文

鸿蒙5.0项目开发——接入有道大模型翻译

鸿蒙5.0项目开发——接入有道大模型翻译

【高心星出品】

项目效果图

在这里插入图片描述

项目功能

  1. 文本翻译功能

    • 支持文本输入和翻译结果显示

    • 使用有道翻译API进行翻译

    • 支持自动检测语言(auto)

    • 支持双向翻译(源语言和目标语言可互换)

  2. 文本操作功能

    • 支持文本复制

    • 支持文本全选

    • 支持长按选择文本

    • 支持滚动查看长文本

  3. 生词本功能

    • 可以将翻译结果保存到生词本

    • 支持保存选中文本的翻译

    • 支持保存整个翻译结果

    • 记录保存时间

  4. 用户界面特点

    • 采用上下布局,上方为输入区,下方为结果显示区

    • 支持实时翻译

    • 提供清晰的视觉反馈

    • 支持长按菜单操作

  5. 数据存储

    • 使用鸿蒙系统的数据存储能力

    • 支持生词本的本地存储

    • 支持历史记录保存

  6. 网络功能

    • 集成有道翻译API

    • 支持HTTP请求

    • 支持错误处理

    • 支持数据流式传输

  7. 安全特性

    • 使用API密钥进行身份验证

    • 支持数据加密传输

    • 实现签名验证机制

  8. 其他功能

    • 支持剪贴板操作

    • 提供操作提示(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
  • 请求参数:
字段名类型含义必填备注
itext待翻译文本True必须是UTF-8编码,限制5000字符
prompttext提示词False必须是UTF-8编码,限制1200字符、400单词
fromtext源语言True参考下方支持语言 (可设置为auto)
totext目标语言True参考下方支持语言
streamTypetext流式返回类型False参考下方 流式返回类型
appKeytext应用IDTrue可在应用管理 查看
salttext随机字符串,可使用UUID进行生产Trueuuid (可使用uuid生成)
signtext签名Truesha256(应用ID+input+salt+curtime+应用密钥)
signTypetext签名类型Truev3
curtimetext当前UTC时间戳(秒)TrueTimeStamp
handleOptiontext处理模式选项False参考下方 处理模式选项
polishOptiontext润色选项False参考下方 润色选项
expandOptiontext扩写选项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

相关文章:

  • 运维_麒麟_国产系统桌面版安装
  • Python同步异步问题三:一个小错误而可能造成无法营业
  • 5月26日day37打卡
  • 15.2【基础项目】使用 TypeScript 实现密码显示与隐藏功能
  • 基于 uni-app + <movable-view>拖拽实现的标签排序-适用于微信小程序、H5等多端
  • TypeScript 针对 iOS 不支持 JIT 的优化策略总结
  • iOS 响应者链详解
  • GitLab 从 17.10 到 18.0.1 的升级指南
  • OpenSSL 签名格式全攻略:深入解析与应用要点
  • 【东枫科技】基于Docker,Nodejs,GitSite构建一个KB站点
  • Android 之 kotlin 语言学习笔记一
  • AI智能分析网关V4室内消防逃生通道占用检测算法打造住宅/商业/工业园区等场景应用方案
  • 快递实时查询API开发:物流轨迹地图集成教程
  • RPA 自动化程序深度解析:从入门到企业级应用实战指南
  • Parasoft C++Test软件单元测试_实例讲解(局部静态变量的处理)
  • node入门:安装和npm使用
  • 如何创建和使用汇编语言,以及下载编译汇编软件(Notepad++,NASM的安装)
  • 小米玄戒O1架构深度解析(一):十核异构设计与缓存层次详解
  • 《软件工程》第 12 章 - 软件测试
  • 【QT】QString和QStringList去掉空格的方法总结
  • 高端品牌网站建设服务/seo培训学院
  • 行业网站的特点/小学生摘抄新闻2024
  • 个人主页界面网站/网站建设高端公司
  • 雅安建设机械网站/怎样进行seo优化
  • 做百度手机网站优化点/湖南有实力seo优化哪家好
  • 制作音乐网站实验报告/临沂seo推广外包