鸿蒙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
  • 请求参数:
字段名 类型 含义 必填 备注
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:begin

data:{"requestId":"11","type":"zh-CHS2en"}


event:message

data:{"transFull":null,"transIncre":"The"}


event:message

data:{"transFull":null,"transIncre":" w"}

...............

event:end

data:{"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 + APPSECRET
  let 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.length
  let result: string
  if (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 请求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('') + result
        strs = []
      }
    }
    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 resdata
         ret.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 = true

  build() {
    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 = true
                  this.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)
  }

  /**
   * 生成文本选择菜单
   * 包含复制、全选、添加到生词本等功能
   */
  @Builder
  genselectmenu() {
    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 = false
          if (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 = null
  private 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 列表索引
   */
  @Builder
  genitemend(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 生词信息
   */
  @Builder
  genlistitem(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

你可能感兴趣的:(鸿蒙练手项目,HarmonyOS5.0,鸿蒙5.0,鸿蒙项目开发,鸿蒙翻译,大模型)