Compare commits

...
157 Commits
Author SHA1 Message Date
欣南科技andGitHub b1e1a29145 Merge pull request #2446 from xinnan-tech/update_version
Bump to 0.8.6
2025-11-01 11:16:33 +08:00
hrz e59df53c99 Bump to 0.8.6 2025-11-01 11:15:58 +08:00
hrzandGitHub cdf0f91251 Merge pull request #2443 from xinnan-tech/py_fix_prompt_template
fix:修复全模块时缺少prompt_template配置的问题
2025-10-31 21:23:50 +08:00
3030332422 32ecbe55fc fix:修复全模块时缺少prompt_template配置的问题 2025-10-31 20:36:55 +08:00
hrzandGitHub cc9d5306e6 Merge pull request #2437 from xinnan-tech/py_mcp_server_fix
update:服务端MCP新增支持Streamable HTTP传输协议
2025-10-31 10:09:02 +08:00
3030332422 5149a8049a update:服务端MCP新增支持Streamable HTTP传输协议 2025-10-29 23:14:42 +08:00
欣南科技andGitHub cde093dd20 Merge pull request #2430 from xinnan-tech/mac_safe
update:默认版本不提供设备ID敏感信息,按需开启
2025-10-28 15:03:19 +08:00
hrz 455b3df5ae update:默认版本不提供设备ID敏感信息,按需开启 2025-10-28 15:02:01 +08:00
hrzandGitHub 4d14caf0d4 Merge pull request #2416 from xinnan-tech/test-page-add-mcp
添加MCP工具测试页面
2025-10-25 09:25:16 +08:00
Chingfeng Li 47b44bb124 修复未定义的方法 2025-10-24 16:01:12 +08:00
Chingfeng Li eb743ab577 从json中加载默认工具,方便自定义tools 2025-10-24 15:58:01 +08:00
Chingfeng Li d6344bde5e 添加MCP测试功能,支持自定义添加编辑MCP Tools 2025-10-24 10:17:32 +08:00
Chingfeng Li 7ce1672d98 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-10-24 09:02:09 +08:00
欣南科技andGitHub 4e3701e62c Merge pull request #2410 from xinnan-tech/fix-xunfei-asr
修复:讯飞asr最后一个字丢失的bug
2025-10-23 17:05:06 +08:00
hrz a035ed525a 修复:讯飞asr最后一个字丢失的bug 2025-10-23 17:04:33 +08:00
hrzandGitHub dbea7ae11e Merge pull request #2403 from xinnan-tech/main-aliyun-nls-taskid
阿里云NLS协议中message_id每次发送都必须唯一;task_id是会话id,整个请求中需要保持一致。
2025-10-23 15:12:19 +08:00
hrzandGitHub 26127e9b4a Merge pull request #2401 from xinnan-tech/py_fix_huoshan
fix: 未等待服务端残留状态问题
2025-10-23 14:32:17 +08:00
Chingfeng Li 7035a57cf1 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-10-23 13:05:10 +08:00
hrzandGitHub aee9bdc514 Merge pull request #2399 from xinnan-tech/main-agent-base-prompt
常量可配置:允许自定义agent-base-prompt.txt文件
2025-10-23 12:26:08 +08:00
欣南科技andGitHub 9018422ecb Merge pull request #2405 from xinnan-tech/hot-fix
update:优化文档
2025-10-23 12:24:52 +08:00
hrz da8d8c53c0 update:优化文档 2025-10-23 12:24:16 +08:00
欣南科技andGitHub 2c428a380e Merge pull request #2404 from xinnan-tech/hot-fix
update:该功能易被坏人滥用,违背开发者初心
2025-10-23 11:47:22 +08:00
hrz bde50e661d update:改功能易引发坏人滥用,违背开发者初心 2025-10-23 11:43:49 +08:00
Chingfeng Li 2beeb825cf 阿里云NLS协议中message_id每次发送都必须唯一;task_id是会话id,整个请求中需要保持一致。 2025-10-22 18:21:34 +08:00
Sakura-RanChen 371dab1282 fix: 未等待服务端残留状态问题 2025-10-22 17:26:36 +08:00
Chingfeng Li 3ba437c937 常量可配置:允许自定义agent-base-prompt.txt文件 2025-10-22 14:53:05 +08:00
hrzandGitHub 4b2d7da4e5 Merge pull request #2357 from kkroid/feature_tts-audio-send-delay
feat: 添加可配置的TTS音频发送延迟功能
2025-10-22 11:48:34 +08:00
hrzandGitHub d00592020e Update config.yaml 2025-10-22 11:47:54 +08:00
hrzandGitHub e0b24f4e59 Merge pull request #2383 from xinnan-tech/docker-server-base
构建base镜像,只有当依赖发生变化时再进行重新构建,否则只使用之前的base镜像进行构建,能够使本地使用缓存的镜像
2025-10-22 11:41:35 +08:00
欣南科技andGitHub 4b7be99837 Merge pull request #2378 from xinnan-tech/py_update_vad
update: 优化vad判断逻辑
2025-10-22 11:19:33 +08:00
hrz 99db948b96 update:恢复vad判断 2025-10-22 10:46:57 +08:00
欣南科技andGitHub 2b3e205cda Merge pull request #2397 from xinnan-tech/pip_version
Pip version
2025-10-22 09:17:29 +08:00
hrz c9b7d77094 updte:python3.10最高只支持torch==2.2.2、torchaudio==2.2.2 2025-10-22 09:16:33 +08:00
hrzandGitHub f3818167f3 Merge pull request #2380 from KurobaKaitou/main
chore: 补充在 Linux 环境下进行源码部署时出现缺失动态库的解决方案以及保留检测 ffmpeg 环境时的原有报错信息
2025-10-21 17:42:52 +08:00
hrzandGitHub 55d6c2a193 Merge pull request #2387 from xinnan-tech/dependabot/pip/main/xiaozhi-server/ormsgpack-1.11.0
build(deps): bump ormsgpack from 1.7.0 to 1.11.0 in /main/xiaozhi-server
2025-10-21 17:42:09 +08:00
hrzandGitHub 6cfb6a8f43 Merge pull request #2386 from xinnan-tech/dependabot/pip/main/xiaozhi-server/dashscope-1.24.6
build(deps): bump dashscope from 1.23.1 to 1.24.6 in /main/xiaozhi-server
2025-10-21 17:41:38 +08:00
hrzandGitHub 396c3eb18b Merge pull request #2388 from xinnan-tech/dependabot/pip/main/xiaozhi-server/openai-2.5.0
build(deps): bump openai from 1.107.0 to 2.5.0 in /main/xiaozhi-server
2025-10-21 17:41:06 +08:00
hrzandGitHub d8c4c1f207 Merge branch 'pip_version' into dependabot/pip/main/xiaozhi-server/openai-2.5.0 2025-10-21 17:40:47 +08:00
hrzandGitHub 268ce9fff3 Merge pull request #2389 from xinnan-tech/dependabot/pip/main/xiaozhi-server/torchaudio-2.9.0
build(deps): bump torchaudio from 2.2.2 to 2.9.0 in /main/xiaozhi-server
2025-10-21 17:39:12 +08:00
hrzandGitHub bbc3b44336 Merge pull request #2390 from xinnan-tech/dependabot/pip/main/xiaozhi-server/mem0ai-1.0.0
build(deps): bump mem0ai from 0.1.62 to 1.0.0 in /main/xiaozhi-server
2025-10-21 17:38:48 +08:00
HanabiandGitHub fb223984d3 Merge branch 'xinnan-tech:main' into main 2025-10-21 00:52:25 +08:00
欣南科技andGitHub 9f21711cae Merge pull request #2392 from xinnan-tech/hot-fix
update:修复manager-api错误码重复问题
2025-10-20 23:39:43 +08:00
hrz c139701468 update:修复manager-api错误码重复问题 2025-10-20 23:38:59 +08:00
欣南科技andGitHub 66d3bcfa13 Merge pull request #2379 from xinnan-tech/main-fix-auth_key
如果已经配置了auth_key,则不使用uuid生成; 适配单模块启动,防止每次服务重启设备都无法连接问题
2025-10-20 22:19:10 +08:00
hrzandGitHub ff048797a1 auth_key优先级:配置文件server.auth_key > manager-api.secret > 自动生成 2025-10-20 21:25:37 +08:00
dependabot[bot]andGitHub ad6607b7f5 build(deps): bump mem0ai from 0.1.62 to 1.0.0 in /main/xiaozhi-server
Bumps [mem0ai](https://github.com/mem0ai/mem0) from 0.1.62 to 1.0.0.
- [Release notes](https://github.com/mem0ai/mem0/releases)
- [Changelog](https://github.com/mem0ai/mem0/blob/main/docs/changelog.mdx)
- [Commits](https://github.com/mem0ai/mem0/compare/0.1.62...v1.0.0)

---
updated-dependencies:
- dependency-name: mem0ai
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 13:19:42 +00:00
dependabot[bot]andGitHub c992ed48da build(deps): bump torchaudio from 2.2.2 to 2.9.0 in /main/xiaozhi-server
Bumps [torchaudio](https://github.com/pytorch/audio) from 2.2.2 to 2.9.0.
- [Release notes](https://github.com/pytorch/audio/releases)
- [Commits](https://github.com/pytorch/audio/compare/v2.2.2...v2.9.0)

---
updated-dependencies:
- dependency-name: torchaudio
  dependency-version: 2.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 13:19:35 +00:00
HanabiandGitHub bdf9cda4c5 Merge branch 'xinnan-tech:main' into main 2025-10-20 20:23:54 +08:00
dependabot[bot]andGitHub c85ffcc181 build(deps): bump openai from 1.107.0 to 2.5.0 in /main/xiaozhi-server
Bumps [openai](https://github.com/openai/openai-python) from 1.107.0 to 2.5.0.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v1.107.0...v2.5.0)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 11:56:58 +00:00
dependabot[bot]andGitHub c53e3ed965 build(deps): bump ormsgpack from 1.7.0 to 1.11.0 in /main/xiaozhi-server
Bumps [ormsgpack](https://github.com/aviramha/ormsgpack) from 1.7.0 to 1.11.0.
- [Release notes](https://github.com/aviramha/ormsgpack/releases)
- [Changelog](https://github.com/aviramha/ormsgpack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aviramha/ormsgpack/compare/1.7.0...1.11.0)

---
updated-dependencies:
- dependency-name: ormsgpack
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 11:51:52 +00:00
dependabot[bot]andGitHub d374b77baa build(deps): bump dashscope in /main/xiaozhi-server
Bumps [dashscope](https://dashscope.aliyun.com/) from 1.23.1 to 1.24.6.

---
updated-dependencies:
- dependency-name: dashscope
  dependency-version: 1.24.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 11:51:47 +00:00
hrzandGitHub f3b2339803 Merge pull request #2384 from xinnan-tech/fix-dependabot
修正检测周期
2025-10-20 18:47:30 +08:00
hrzandGitHub 27ed540113 Merge pull request #2385 from xinnan-tech/fixmqtt
fix:修复单模块mqtt下发错误
2025-10-20 18:47:05 +08:00
FAN-yeB 434fba55f7 fix:修复单模块下发逻辑 2025-10-20 18:21:12 +08:00
Chingfeng Li 280c04e753 修正检测周期 2025-10-20 15:54:50 +08:00
Chingfeng Li 24832822a5 默认main分支 2025-10-20 15:11:23 +08:00
Chingfeng Li a357f0818d 依赖baseAction 2025-10-20 15:10:19 +08:00
Chingfeng Li e4bf5d2d21 构建base镜像,只有当依赖发生变化时再进行重新构建,否则只使用之前的base镜像进行构建,能够使本地使用缓存的镜像 2025-10-20 15:07:02 +08:00
HanabiandGitHub 8b261d473a fix: 补充README文档 334行的 ffmpeg 单词缺失
补充README文档 334行的 ffmpeg 单词缺失
2025-10-20 12:53:40 +08:00
HanabiandGitHub bba84382ec docs: 补充源码部署时安装 ffmpeg 环境可能会出现缺失动态库的解决方案
补充说明
2025-10-20 12:32:09 +08:00
HanabiandGitHub d081803434 chore(util): 优化项目启动时检测ffmpeg环境时在某些场景下的提示
此次变动主要针对 Linux (Ubuntu 22.04) 环境下根据官方提供的README文档以及conda安装 ffmpeg 环境后出现缺失动态库libiconv.so.2的问题 报错完整信息如下:
`ffmpeg: error while loading shared libraries: libiconv.so.2: cannot open shared object file: No such file or directory` 基于以上报错信息 优化了util.py中的check_ffmpeg_installed函数
- 保留了原有命令行的提示信息 [定位问题更加友好]
- 抓取报错中的 libiconv.so.2 文本内容 进行提示用户通过conda安装 libiconv 动态库
2025-10-20 12:28:22 +08:00
Chingfeng Li 9194ffa6b7 如果已经配置了auth_key,则不使用uuid生成; 适配单模块启动,防止每次服务重启设备都无法连接问题 2025-10-20 11:57:18 +08:00
Sakura-RanChen c06a7b1db6 update: 优化vad判断逻辑 2025-10-20 11:45:50 +08:00
欣南科技andGitHub 1736e10189 Merge pull request #2374 from xinnan-tech/myifeng-patch-1
更新 dependabot.yml
2025-10-20 11:37:55 +08:00
hrzandGitHub 43b79c50a0 每个月更新组件版本 2025-10-20 11:35:31 +08:00
欣南科技andGitHub 2e6aced1bd Merge pull request #2377 from xinnan-tech/update-doc
fix:克隆教程文字错误
2025-10-20 10:17:55 +08:00
hrz 2dc80c8432 fix:克隆教程文字错误 2025-10-20 10:16:10 +08:00
myifengandGitHub 01e7a7d3b8 更新 dependabot.yml 2025-10-19 16:06:44 +08:00
欣南科技andGitHub e5e9f2bdea Merge pull request #2373 from xinnan-tech/update-doc
update:立即复刻增加loading效果
2025-10-19 14:34:53 +08:00
hrz 06eff425f1 update:立即复刻增加loading效果 2025-10-19 14:34:05 +08:00
欣南科技andGitHub d2aae057f6 Merge pull request #2372 from xinnan-tech/update-doc
update:常见问题增加音色克隆链接
2025-10-19 13:18:39 +08:00
hrz b7b39de445 update:常见问题增加音色克隆链接 2025-10-19 13:18:07 +08:00
欣南科技andGitHub 4aedcb8419 Merge pull request #2371 from xinnan-tech/voice-clone
update:升级版本号
2025-10-19 13:14:20 +08:00
hrz 6c5bd395cb update:升级版本号 2025-10-19 13:13:44 +08:00
欣南科技andGitHub 6ad9ae8d1b Merge pull request #2353 from xinnan-tech/voice-clone
uptate:音色复刻功能完善
2025-10-19 13:11:23 +08:00
hrz 48e2369964 update:修改音色克隆教程 2025-10-19 13:10:07 +08:00
hrz e753c242ad update:将本人的克隆音色自动添加模型配置中的音色选择列表 2025-10-19 11:53:15 +08:00
hrz c1e0b5181e update:优化克隆失败显示 2025-10-19 01:45:08 +08:00
欣南科技andGitHub d3371c2875 Merge pull request #2370 from xinnan-tech/ota-test
fix:修复【启用】模型和【默认】模型时,密钥变*bug
2025-10-19 00:37:08 +08:00
hrz 2488678f34 fix:修复【启用】模型和【默认】模型时,密钥变*bug 2025-10-19 00:36:24 +08:00
3030332422 958d50c119 fix:修改音色克隆详细内容 2025-10-18 21:47:14 +08:00
欣南科技andGitHub 19c16cfcd8 Merge pull request #2369 from xinnan-tech/ota-test
Ota test
2025-10-18 21:25:27 +08:00
hrz c66c92be73 update:智控台版本ota增加token返回字段 2025-10-18 21:24:19 +08:00
hrzandGitHub 4f9ea49f8e Merge pull request #2368 from myifeng/mian-ota-test
更新test页面,适配OTA认证
2025-10-18 14:28:39 +08:00
3030332422 b776c545e3 fix:声音复刻后自动刷新页面状态和错误详情 2025-10-17 23:31:36 +08:00
3030332422 d65969a2b2 fix: 声音复刻成功后自动刷新页面状态 2025-10-17 22:29:30 +08:00
3030332422 d3eb8df7f0 update:声音复刻功能的部分修改 2025-10-17 18:22:38 +08:00
Chingfeng Li 89e299764d 调整测试认证 2025-10-17 18:06:22 +08:00
rainv123 66fb7933b7 update:增加音色克隆页面详情按钮 2025-10-17 17:55:56 +08:00
Chingfeng Li 49e1bbbf15 更新test测试页面,使用ota返回的连接信息和认证信息 2025-10-17 17:51:56 +08:00
3030332422 af02d53718 update:修复复刻失败问题 2025-10-17 17:08:41 +08:00
3030332422 f8b1e8199c fix:修复复刻失败的问题 2025-10-17 15:56:33 +08:00
rainv123 008456d7cd update:新增音色资源时判断声音id是否已经存在 2025-10-17 11:31:13 +08:00
hrzandGitHub aa7c07167c Merge pull request #2346 from xinnan-tech/mcp_add_macaddress_test
update:通过JSON-RPC参数传递设备MAC地址
2025-10-17 09:42:01 +08:00
rainv123 d451fe7a0b uptate:修改训练状态 2025-10-17 09:22:08 +08:00
will.zhang 3b8bbb5c5f feat: 添加可配置的TTS音频发送延迟功能
- 修改 sendAudio() 函数,新增 tts_audio_send_delay 配置参数(毫秒)
- 0: 使用原有逻辑
- 大于0: 使用固定延迟发送(降低首包延迟,提升响应速度)
2025-10-14 18:30:17 +08:00
hrzandGitHub 29cd69415a Merge pull request #2355 from xinnan-tech/py_fix_uuid
update:暂时恢复先前的随机生成uuid
2025-10-14 18:19:33 +08:00
欣南科技andGitHub bc99da5b28 Merge pull request #2356 from xinnan-tech/update-doc
update:优化demo
2025-10-14 18:13:39 +08:00
hrz 5b38d92cc6 update:优化demo 2025-10-14 18:12:58 +08:00
3030332422 0e1392de35 update:暂时恢复先前的随机生成uuid 2025-10-14 18:06:57 +08:00
rainv123andGitHub a033b56b8d Update:修正翻译 2025-10-14 15:51:34 +08:00
rainv123andGitHub c449d29de0 Update:更新火山双流式语音合成+音色克隆配置文档 2025-10-14 15:23:48 +08:00
rainv123 afc142b397 update:更新火山双流式语音合成+音色克隆配置文档 2025-10-14 15:16:51 +08:00
rainv123 55348ffe65 updata:更新火山双向流式TTS+声音克隆配置文档 2025-10-14 15:13:22 +08:00
rainv123 174b9f5898 updata:更新火山双向流式TTS+声音克隆配置教程 2025-10-14 15:05:02 +08:00
rainv123 3b69629a61 fix:补充键的翻译 2025-10-14 11:04:49 +08:00
rainv123 56eb25f8e2 fix:更正翻译 2025-10-14 10:59:54 +08:00
rainv123 1400e48fc7 Merge branch 'voice-clone' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-10-14 10:27:02 +08:00
rainv123 bccda3e7b2 fix:补充音色克隆界面弹窗按钮翻译 2025-10-14 10:21:50 +08:00
rainv123 dfb0a2b213 update:修改音色编码排序逻辑 2025-10-14 10:19:44 +08:00
3030332422 f94b376f31 update:通过JSON-RPC参数传递设备MAC地址 2025-10-13 18:00:58 +08:00
hrzandGitHub 13ce67e4c7 Merge pull request #2342 from xinnan-tech/py_fix_wakeup
fix: 相关状态未重置
2025-10-13 17:57:49 +08:00
欣南科技andGitHub eece86ab71 Merge pull request #2345 from xinnan-tech/ota-auth
Ota auth
2025-10-13 17:57:18 +08:00
hrz 4fd294dc80 update:
1、把ota_auth内容合并到auth.py
2、精简auth密钥,简化部署
2025-10-13 17:52:28 +08:00
Sakura-RanChen 4952ccfbf7 fix: 相关状态未重置 2025-10-13 16:47:05 +08:00
hrz 471ad864b6 update:优化 2025-10-13 15:31:49 +08:00
hrz bec32bd085 update:把传递name改成传递speaker_id 2025-10-13 15:26:26 +08:00
rainv123 087fd5d547 update:音色克隆 2025-10-13 14:17:53 +08:00
hrzandGitHub 99f178274d Merge pull request #2339 from xinnan-tech/cc
fix:修复长按聆听设备松开没有声音的问题
2025-10-13 13:23:47 +08:00
1cccux 450560f92a fix:修复长按聆听设备松开没有声音的问题 2025-10-13 10:57:48 +08:00
hrzandGitHub 691a0529b0 Merge pull request #2330 from myifeng/ota-auth
OTA接口授权与认证
2025-10-13 10:51:34 +08:00
欣南科技andGitHub 38e63eb665 Merge pull request #2338 from xinnan-tech/uuid
UUID
2025-10-13 10:49:44 +08:00
hrzandGitHub ad8390301e Merge pull request #2337 from Yuerchu/main
使用uuid包来生成uuidv4
2025-10-13 10:35:19 +08:00
于小丘 acee7e27c6 在管理网页也修改uuid生成方法 2025-10-13 09:49:56 +08:00
于小丘 e790b668ef 删除原uuid库的导入 2025-10-13 09:42:59 +08:00
于小丘 432a5d01f8 改成 crypto.randomUUID(); 生成uuid 2025-10-13 09:35:39 +08:00
于小丘 374f2a92aa 使用uuid包来生成uuidv4 2025-10-13 09:26:16 +08:00
欣南科技andGitHub a22ad985dc Merge pull request #2336 from xinnan-tech/hot-fix
fix:回复词太短
2025-10-12 23:25:25 +08:00
hrz 44f41ebd11 fix:回复词太短 2025-10-12 23:24:43 +08:00
Chingfeng Li a10e94bef6 先认证,后创建连接,防止无效连接建立 2025-10-11 17:52:15 +08:00
hrzandGitHub e7cb67f846 Merge pull request #2331 from xinnan-tech/py_wave_fix
update:修改唤醒词
2025-10-11 15:57:11 +08:00
3030332422 b5a3cc1c78 update:修改唤醒词 2025-10-11 15:48:27 +08:00
FAN-yeBandGitHub 1d107fb39b Merge pull request #2326 from xinnan-tech/css
修复修改密码无法重复点击和恢复样式
2025-10-11 15:45:29 +08:00
LiJinHui 1ec8f639cb 补充修改 2025-10-11 15:28:53 +08:00
Chingfeng Li c15069a9b6 OTA接口授权与认证 2025-10-11 15:27:18 +08:00
LiJinHui 13ae5e3ade 补充样式 2025-10-11 15:02:01 +08:00
LiJinHui 9cffc8b0ec 修复交互bug 2025-10-11 09:26:08 +08:00
hrzandGitHub c8c5b1095b Merge pull request #2323 from xinnan-tech/py_update_document
update: 更新说明文档
2025-10-10 15:09:46 +08:00
Sakura-RanChen 9803b3c690 update: 更新说明文档 2025-10-10 14:45:30 +08:00
hrzandGitHub 341c0087b7 Merge pull request #2322 from xinnan-tech/web-i18n-updata
update:调整翻译文本顺序,删除重复翻译键
2025-10-10 14:39:56 +08:00
rainv123 0cd752be05 update:调整翻译文本顺序,删除重复翻译键 2025-10-10 14:35:35 +08:00
rainv123andGitHub 2e39b1c6f5 Merge pull request #2313 from xinnan-tech/mobile_forgot_password
添加移动端忘记密码页面
2025-10-10 11:28:02 +08:00
LiJinHui c4026ef9ac 样式的恢复和调整 2025-10-10 10:02:11 +08:00
欣南科技andGitHub 271a2e3e96 Merge pull request #2317 from xinnan-tech/hot-fix
fix:python config api no need maskSensitive
2025-10-08 22:45:22 +08:00
hrz bc3e10f09b fix:python config api no need maskSensitive 2025-10-08 22:43:45 +08:00
3030332422 fed3501bf6 update:补充 2025-10-08 10:49:02 +08:00
欣南科技andGitHub 877cb0d645 Merge pull request #2312 from xinnan-tech/voice-clone
优化:用户管理密码生成策略
2025-10-08 00:17:16 +08:00
hrz e930935202 优化:用户管理密码生成策略 2025-10-08 00:16:24 +08:00
欣南科技andGitHub 0af81fa366 Merge pull request #2311 from xinnan-tech/voice-clone
update:智控台音色克隆
2025-10-07 23:57:56 +08:00
hrz 3249970b7b update:智控台音色克隆 2025-10-07 23:54:42 +08:00
3030332422 1466806339 update:移动端忘记密码页面的前后端对接 2025-10-06 20:01:06 +08:00
3030332422 05389b1368 update:移动端忘记密码前端页面添加地区区号选择和SM2加密 2025-10-03 11:35:44 +08:00
欣南科技andGitHub bef7f79f40 Merge pull request #2307 from xinnan-tech/update_doc
update:更新docker安装提示
2025-10-02 02:28:21 +08:00
hrz 8d7394cd46 update:更新docker安装提示 2025-10-02 02:25:58 +08:00
欣南科技andGitHub 65053e98e2 Merge pull request #2306 from xinnan-tech/openrz-patch-1
Update docker-image.yml
2025-10-02 02:14:31 +08:00
hrzandGitHub c1bea43598 Update docker-image.yml 2025-10-02 02:13:45 +08:00
3030332422 f10fddc59a update:新增移动端忘记密码前端页面 2025-09-30 17:52:38 +08:00
114 changed files with 8117 additions and 3477 deletions
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: pip
directory: /main/xiaozhi-server
schedule:
interval: weekly
+49
View File
@@ -0,0 +1,49 @@
name: Build Base Image
on:
push:
branches:
- main
paths:
- 'main/xiaozhi-server/requirements.txt'
- 'Dockerfile-server-base'
- '.github/workflows/build-base-image.yml'
jobs:
build-base:
name: Build and push server base image
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TOKEN }}
- name: Build and push server-base
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile-server-base
push: true
tags: ghcr.io/${{ github.repository }}:server-base
platforms: linux/amd64
cache-from: type=gha,scope=server-base
cache-to: type=gha,mode=max,scope=server-base
build-args: |
BUILDKIT_PROGRESS=plain
- name: Output image info
run: |
echo "✅ Base image built and pushed successfully!"
echo "📦 Tag: ghcr.io/${{ github.repository }}:server-base"
+7 -3
View File
@@ -5,6 +5,10 @@ on:
tags: tags:
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0 - 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
workflow_dispatch: workflow_dispatch:
workflow_run:
workflows: ["Build Base Image"]
types:
- completed
jobs: jobs:
release: release:
@@ -62,7 +66,7 @@ jobs:
push: true push: true
tags: | tags: |
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }} ${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-args: | build-args: |
@@ -77,8 +81,8 @@ jobs:
push: true push: true
tags: | tags: |
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }} ${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-args: | build-args: |
BUILDKIT_PROGRESS=plain BUILDKIT_PROGRESS=plain
+2 -32
View File
@@ -1,36 +1,6 @@
# 第一阶段:构建Python依赖 # 生产镜像,仅包含应用代码
FROM python:3.10-slim AS builder FROM ghcr.io/xinnan-tech/xiaozhi-esp32-server:server-base
WORKDIR /app
# 配置pip使用国内镜像源(阿里云)并设置超时和重试
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
pip config set global.trusted-host mirrors.aliyun.com && \
pip config set global.timeout 120 && \
pip config set install.retries 5
COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖,使用并行下载
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
# 第二阶段:生产镜像
FROM python:3.10-slim
WORKDIR /opt/xiaozhi-esp32-server
# 安装系统依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/bin/mcp-proxy /usr/local/bin/mcp-proxy
# 复制应用代码
COPY main/xiaozhi-server . COPY main/xiaozhi-server .
# 启动应用 # 启动应用
+24
View File
@@ -0,0 +1,24 @@
# Dockerfile-server-base
# 基础镜像,包含系统依赖和Python包
FROM python:3.10-slim
# 安装系统依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 配置pip使用国内镜像源(阿里云)并设置超时和重试
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
pip config set global.trusted-host mirrors.aliyun.com && \
pip config set global.timeout 120 && \
pip config set install.retries 5
WORKDIR /opt/xiaozhi-esp32-server
# 复制requirements.txt
COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
+34 -34
View File
@@ -56,10 +56,24 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
<table> <table>
<tr> <tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="小智医疗救护车场景" src="docs/images/demo1.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -71,19 +85,14 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="docs/images/demo2.png" /> <img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" />
</picture> </picture>
</a> </a>
</td> </td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank"> <a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture> <picture>
@@ -91,12 +100,17 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
</picture> </picture>
</a> </a>
</td> </td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="docs/images/demo6.png" /> <img alt="MCP接入点" src="docs/images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -114,20 +128,6 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
</picture> </picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture>
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a>
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@@ -145,23 +145,23 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="多指令任务" src="docs/images/demo11.png" /> <img alt="自定义音色" src="docs/images/demo2.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank"> <a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture> <picture>
<img alt="声纹识别" src="docs/images/demo14.png" /> <img alt="播报新闻" src="docs/images/demo0.png" />
</picture> </picture>
</a> </a>
</td> </td>
+44 -44
View File
@@ -58,72 +58,72 @@ Want to see the usage effects? Click the videos below 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="Xiaozhi ESP32 connecting to own backend model" src="docs/images/demo1.png" /> <img alt="响应速度感受" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank"> <a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture> <picture>
<img alt="MQTT command issuance" src="docs/images/demo4.png" /> <img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture> <picture>
<img alt="Custom voice timbre" src="docs/images/demo2.png" /> <img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="Using Cantonese for communication" src="docs/images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Controlling home appliances" src="docs/images/demo5.png" />
</picture> </picture>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture> <picture>
<img alt="Custom voice timbre" src="docs/images/demo6.png" /> <img alt="控制家电开关" src="docs/images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="Playing music" src="docs/images/demo7.png" /> <img alt="播放音乐" src="docs/images/demo7.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank"> <a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture> <picture>
<img alt="Weather plugin" src="docs/images/demo8.png" /> <img alt="天气插件" src="docs/images/demo8.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture>
<img alt="IOT command control devices" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="News broadcasting" src="docs/images/demo0.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -132,35 +132,35 @@ Want to see the usage effects? Click the videos below 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank"> <a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture> <picture>
<img alt="Real-time interruption" src="docs/images/demo10.png" /> <img alt="实时打断" src="docs/images/demo10.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="Photo recognition of objects" src="docs/images/demo12.png" /> <img alt="拍照识物品" src="docs/images/demo12.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="Multi-command tasks" src="docs/images/demo11.png" /> <img alt="自定义音色" src="docs/images/demo2.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="MCP endpoint" src="docs/images/demo13.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank"> <a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture> <picture>
<img alt="Voiceprint recognition" src="docs/images/demo14.png" /> <img alt="播报新闻" src="docs/images/demo0.png" />
</picture> </picture>
</a> </a>
</td> </td>
+12 -6
View File
@@ -2,7 +2,7 @@
![请参考-最简化架构图](../docs/images/deploy1.png) ![请参考-最简化架构图](../docs/images/deploy1.png)
# 方式一:Docker只运行Server # 方式一:Docker只运行Server
docker镜像支持x86架构arm64架构的CPU,支持在国产操作系统上运行 `0.8.2`版本开始,本项目发行的docker镜像支持`x86架构`,如果需要在`arm64架构`的CPU上部署,可按照[这个教程](docker-build.md)在本机编译`arm64的镜像`
## 1. 安装docker ## 1. 安装docker
@@ -139,6 +139,9 @@ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/
conda install libopus -y conda install libopus -y
conda install ffmpeg -y conda install ffmpeg -y
# 在 Linux 环境下进行部署时,如出现类似缺失 libiconv.so.2 动态库的报错 请通过以下命令进行安装
conda install libiconv -y
``` ```
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。 请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
@@ -267,7 +270,8 @@ LLM:
6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/> 6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/>
## 部署相关教程 ## 部署相关教程
1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/> 1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
2、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/> 2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
3、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
## 拓展相关教程 ## 拓展相关教程
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/> 1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/> 2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
@@ -275,11 +279,13 @@ LLM:
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/> 4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/> 5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
6、[如何开启声纹识别](./voiceprint-integration.md)<br/> 6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/> 7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
8、[天气插件使用指南](./weather-integration.md)<br/>
## 语音克隆、本地语音部署相关教程 ## 语音克隆、本地语音部署相关教程
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/> 1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/> 2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/> 3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
## 性能测试教程 ## 性能测试教程
1、[各组件速度测试指南](./performance_tester.md)<br/> 1、[各组件速度测试指南](./performance_tester.md)<br/>
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/> 2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
+10 -5
View File
@@ -1,7 +1,7 @@
# 部署架构图 # 部署架构图
![请参考-全模块安装架构图](../docs/images/deploy2.png) ![请参考-全模块安装架构图](../docs/images/deploy2.png)
# 方式一:Docker运行全模块 # 方式一:Docker运行全模块
docker镜像支持x86架构arm64架构的CPU,支持在国产操作系统上运行 `0.8.2`版本开始,本项目发行的docker镜像支持`x86架构`,如果需要在`arm64架构`的CPU上部署,可按照[这个教程](docker-build.md)在本机编译`arm64的镜像`
## 1. 安装docker ## 1. 安装docker
@@ -355,6 +355,9 @@ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/
conda install libopus -y conda install libopus -y
conda install ffmpeg -y conda install ffmpeg -y
# 在 Linux 环境下进行部署时,如出现类似缺失 libiconv.so.2 动态库的报错 请通过以下命令进行安装
conda install libiconv -y
``` ```
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。 请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
@@ -468,7 +471,8 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/> 6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/>
## 部署相关教程 ## 部署相关教程
1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/> 1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
2、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/> 2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
3、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
## 拓展相关教程 ## 拓展相关教程
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/> 1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/> 2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
@@ -479,9 +483,10 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/> 7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
8、[天气插件使用指南](./weather-integration.md)<br/> 8、[天气插件使用指南](./weather-integration.md)<br/>
## 语音克隆、本地语音部署相关教程 ## 语音克隆、本地语音部署相关教程
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/> 1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/> 2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/> 3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
## 性能测试教程 ## 性能测试教程
1、[各组件速度测试指南](./performance_tester.md)<br/> 1、[各组件速度测试指南](./performance_tester.md)<br/>
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/> 2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
+14 -12
View File
@@ -62,29 +62,31 @@ VAD:
### 7、部署相关教程 ### 7、部署相关教程
1、[如何进行最简化部署](./Deployment.md)<br/> 1、[如何进行最简化部署](./Deployment.md)<br/>
2、[如何进行全模块部署](./Deployment_all.md)<br/> 2、[如何进行全模块部署](./Deployment_all.md)<br/>
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/> 3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/> 4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/> 5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
### 8、编译固件相关教程 ### 9、编译固件相关教程
1、[如何自己编译小智固件](./firmware-build.md)<br/> 1、[如何自己编译小智固件](./firmware-build.md)<br/>
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/> 2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
### 8、拓展相关教程 ### 10、拓展相关教程
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/> 1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/> 2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
3、[如何开启视觉模型实现拍照识物](./mcp-vision-integration.md)<br/> 3、[如何开启视觉模型实现拍照识物](./mcp-vision-integration.md)<br/>
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/> 4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/> 5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
6、[如何开启声纹识别](./voiceprint-integration.md)<br/> 6、[MCP方法如何获取设备信息](./mcp-get-device-info.md)<br/>
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/> 7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
### 9、语音克隆、本地语音部署相关教程 ### 11、语音克隆、本地语音部署相关教程
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/> 1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/> 2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/> 3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
### 10、性能测试教程 ### 12、性能测试教程
1、[各组件速度测试指南](./performance_tester.md)<br/> 1、[各组件速度测试指南](./performance_tester.md)<br/>
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/> 2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
+49 -22
View File
@@ -1,29 +1,56 @@
# 火山双流式TTS+声音克隆配置教程 # 智控台 火山双流式语音合成+音色克隆配置教程
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
本教程分为4个阶段:准备阶段、配置阶段、克隆阶段、使用阶段。主要是介绍通过智控台配置火山双流式语音合成+音色克隆的过程。
## 第一阶段:准备阶段
超级管理员先预先把火山引擎服务开通好,获取到App Id,Access Token。默认火上引擎会赠送一个音色资源。这个音色资源需要把它复制到本项目里。
如果你想克隆多个音色,需要购买开通多个音色资源。只要把每个音色资源的声音ID(S_xxxxx)复制到本项目。然后分配给系统的账号使用即可。以下是详细步骤:
### 1.开通火山引擎服务 ### 1.开通火山引擎服务
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App IdAccess TokenCluster ID以及声音ID(S_xxxxx) 访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型
### 2.克隆音色
克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy
准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py``` ### 2.获取音色资源ID
访问 https://console.volcengine.com/speech/service/9999 复制三项内容,分别是App IdAccess Token以及声音ID(S_xxxxx)。如图
在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称 ![获取音色资源](images/image-clone-integration-01.png)
```python
train(appid=appid, token=token, audio_path=r".\audios\xiaohe.wav", spk_id=spk_id)
```
运行以下命令生成test_submit.mp3,点击播放试听克隆效果 ## 第二阶段:配置火山引擎服务
```python ### 1.填写火山引擎配置
python uploadAndStatus.py
python tts_http_demo.py
```
回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。
### 3.填写配置文件
将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中
修改 resource_id的参数为``` volc.megatts.default``` 使用超级管理员账号登录智控台,点击顶部【模型配置】,再点击模型配置页面左侧的【语音合成】,搜索找到“火山双流式语音合成”,点击修改,将你火山引擎的`App Id`填入到【应用ID】字段里,将`Access Token`填入到【访问令牌】字段里。然后保存。
(参考官方文档 https://www.volcengine.com/docs/6561/1329505
speaker的参数填入声音ID(S_xxxxx)
启动服务,唤醒小智发出的声音是克隆的音色即成功。 ### 2.将音色资源ID分配给系统账号
使用超级管理员账号登录智控台,点击顶部【音色克隆】、【音色资源】。
点击新增按钮,在【平台名称】选择“火山双流式语音合成”;
在【音色资源ID】填入你火山引擎的声音资源ID(S_xxxxx),填入后按回车;
在【归属账号】选择你要分配给的系统账号,你可以分配给你自己。然后点击保存
## 第三阶段:克隆阶段
如果登录后,点击顶部【音色克隆】》【音色克隆】,显示【您的账号暂无音色资源请联系管理员分配音色资源】,说明你在第二阶段还没有把音色资源ID分配给这个账号。那就是回到第二阶段,分配音色资源给对应的账号。
如果登录后,点击顶部【音色克隆】》【音色克隆】,能看到对应的音色列表。请继续。
在列表里会看到对应的音色列表。选择其中一个音色资源,点击【上传音频】按钮。上传后,可以试听一下声音或者截取某段声音。确认后点击【上传音频】按钮。
![上传音频](images/image-clone-integration-02.png)
上传音频后,在列表里会看到对应的音色会变成“待复刻”状态。点击【立即复刻】按钮。等1~2秒会返回结果。
如果复刻失败,请将鼠标放到“错误信息”图标上,会显示失败的原因。
如果复刻成功,在列表里会看到对应的音色会变成“训练成功”状态。此时你可以点击【声音名称】栏的修改按钮,修改音色资源的名称,方便后期选择使用。
## 第四阶段:使用阶段
点击顶部【智能体管理】,选择任意一个智能体,点击【配置角色】按钮。
语音合成(TTS)选择“火山双流式语音合成”。在列表里,找到名字带有“克隆音色”的音色资源(如图),选择它,点击保存。
![选择音色](images/image-clone-integration-03.png)
接下来,可以唤醒小智和它对话。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+13 -6
View File
@@ -1,14 +1,18 @@
# IndexStreamTTS 使用指南 # IndexStreamTTS 使用指南
## 环境准备 ## 环境准备
### 1. 克隆项目 (注意这里使用的为VLLM1.0的Releases版本) ### 1. 克隆项目
```bash ```bash
https://github.com/Ksuriuri/index-tts-vllm/releases/tag/IndexTTS-vLLM-1.0 git clone https://github.com/Ksuriuri/index-tts-vllm.git
``` ```
进入解压后的目录 进入解压后的目录
```bash ```bash
cd index-tts-vllm cd index-tts-vllm
``` ```
切换到指定版本 (使用VLLM-0.10.2的历史版本)
```bash
git checkout 224e8d5e5c8f66801845c66b30fa765328fd0be3
```
### 2. 创建并激活 conda 环境 ### 2. 创建并激活 conda 环境
```bash ```bash
@@ -34,7 +38,7 @@ Cuda compilation tools, release 12.8, V12.8.89
```bash ```bash
pip install torch torchvision pip install torch torchvision
``` ```
需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/) 需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/)
### 4. 安装依赖 ### 4. 安装依赖
```bash ```bash
@@ -42,6 +46,7 @@ pip install -r requirements.txt
``` ```
### 5. 下载模型权重 ### 5. 下载模型权重
### 方案一:下载官方权重文件后转换
此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重 此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重
| HuggingFace | ModelScope | | HuggingFace | ModelScope |
|---------------------------------------------------------------|---------------------------------------------------------------------| |---------------------------------------------------------------|---------------------------------------------------------------------|
@@ -49,7 +54,7 @@ pip install -r requirements.txt
| [IndexTTS-1.5](https://huggingface.co/IndexTeam/IndexTTS-1.5) | [IndexTTS-1.5](https://modelscope.cn/models/IndexTeam/IndexTTS-1.5) | | [IndexTTS-1.5](https://huggingface.co/IndexTeam/IndexTTS-1.5) | [IndexTTS-1.5](https://modelscope.cn/models/IndexTeam/IndexTTS-1.5) |
下面以ModelScope的安装方法为例 下面以ModelScope的安装方法为例
### 请注意:git需要安装并初始化启用lfs(如已安装可以跳过) #### 请注意:git需要安装并初始化启用lfs(如已安装可以跳过)
```bash ```bash
sudo apt-get install git-lfs sudo apt-get install git-lfs
git lfs install git lfs install
@@ -61,7 +66,7 @@ cd model_dir
git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git
``` ```
### 5. 模型权重转换 #### 模型权重转换
```bash ```bash
bash convert_hf_format.sh /path/to/your/model_dir bash convert_hf_format.sh /path/to/your/model_dir
``` ```
@@ -169,6 +174,8 @@ chmod +x start_api.sh
```bash ```bash
tail -f tmp/server.log tail -f tmp/server.log
``` ```
如果显卡内存足够,可在脚本中添加启动参数 ----gpu_memory_utilization 来调整显存占用比例,默认值为 0.25
## 音色配置 ## 音色配置
index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。 index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。
在项目根目录下的assets/speaker.json文件中配置自定义音色 在项目根目录下的assets/speaker.json文件中配置自定义音色
@@ -184,5 +191,5 @@ index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和
] ]
} }
``` ```
### 注意 (需重启服务进行注册) ### 注意 配置角色后需重启服务进行音色注册)
添加后需在智控台中添加相应的说话人(单模块则更换相应的voice) 添加后需在智控台中添加相应的说话人(单模块则更换相应的voice)
+40
View File
@@ -0,0 +1,40 @@
# MCP 方法如何获取设备信息
本教程将指导你如何使用MCP方法获取设备信息。
第一步:自定义你的`agent-base-prompt.txt`文件
把xiaozhi-server目录的`agent-base-prompt.txt`文件内容复制到你的`data`目录下,并重命名为`.agent-base-prompt.txt`
第二步:修改`data/.agent-base-prompt.txt`文件,找到`<context>`标签,在标签内容中添加以下代码内容:
```
- **设备ID** {{device_id}}
```
添加完成后,你的`data/.agent-base-prompt.txt`文件的`<context>`标签内容大致如下:
```
<context>
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
- **设备ID** {{device_id}}
- **当前时间:** {{current_time}}
- **今天日期:** {{today_date}} ({{today_weekday}})
- **今天农历:** {{lunar_date}}
- **用户所在城市:** {{local_address}}
- **当地未来7天天气:** {{weather_info}}
</context>
```
第三步:修改`data/.config.yaml`文件,找到`agent-base-prompt`配置,修改前内容如下:
```
prompt_template: agent-base-prompt.txt
```
修改成
```
prompt_template: data/.agent-base-prompt.txt
```
第四步:重启你的xiaozhi-server服务。
第五步:在你的mcp方法增加名称为`device_id`,类型为`string`,描述为`设备ID`的参数。
第六步:重新唤醒小智,让他调用mcp方法,查看你的mcp方法是否可以获取`设备ID`
@@ -146,6 +146,11 @@ public interface Constant {
*/ */
String MEMORY_NO_MEM = "Memory_nomem"; String MEMORY_NO_MEM = "Memory_nomem";
/**
* 火山引擎双声道语音克隆
*/
String VOICE_CLONE_HUOSHAN_DOUBLE_STREAM = "huoshan_double_stream";
enum SysBaseParam { enum SysBaseParam {
/** /**
* ICP备案号 * ICP备案号
@@ -171,6 +176,38 @@ public interface Constant {
} }
} }
/**
* 训练状态
*/
enum TrainStatus {
/**
* 未训练
*/
NOT_TRAINED(0),
/**
* 训练中
*/
TRAINING(1),
/**
* 已训练
*/
TRAINED(2),
/**
* 训练失败
*/
TRAIN_FAILED(3);
private final int code;
TrainStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
/** /**
* 系统短信 * 系统短信
*/ */
@@ -257,7 +294,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.8.3"; public static final String VERSION = "0.8.6";
/** /**
* 无效固件URL * 无效固件URL
@@ -55,7 +55,7 @@ public interface ErrorCode {
int OTA_DEVICE_NOT_FOUND = 10041; int OTA_DEVICE_NOT_FOUND = 10041;
int OTA_DEVICE_NEED_BIND = 10042; int OTA_DEVICE_NEED_BIND = 10042;
// 新增错误编码 // 新增错误编码
int DELETE_DATA_FAILED = 10043; int DELETE_DATA_FAILED = 10043;
int USER_NOT_LOGIN = 10044; int USER_NOT_LOGIN = 10044;
@@ -66,7 +66,7 @@ public interface ErrorCode {
int INVALID_LLM_TYPE = 10049; int INVALID_LLM_TYPE = 10049;
int TOKEN_GENERATE_ERROR = 10050; int TOKEN_GENERATE_ERROR = 10050;
int RESOURCE_NOT_FOUND = 10051; int RESOURCE_NOT_FOUND = 10051;
// 新增错误编码 // 新增错误编码
int DEFAULT_AGENT_NOT_FOUND = 10052; int DEFAULT_AGENT_NOT_FOUND = 10052;
int AGENT_NOT_FOUND = 10053; int AGENT_NOT_FOUND = 10053;
@@ -82,14 +82,6 @@ public interface ErrorCode {
int DEVICE_ALREADY_ACTIVATED = 10063; int DEVICE_ALREADY_ACTIVATED = 10063;
// 默认模型删除错误 // 默认模型删除错误
int DEFAULT_MODEL_DELETE_ERROR = 10064; int DEFAULT_MODEL_DELETE_ERROR = 10064;
// 设备相关错误码
int MAC_ADDRESS_ALREADY_EXISTS = 10090; // Mac地址已存在
// 模型相关错误码
int MODEL_PROVIDER_NOT_EXIST = 10091; // 供应器不存在
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
// 登录相关错误码 // 登录相关错误码
int ADD_DATA_FAILED = 10065; // 新增数据失败 int ADD_DATA_FAILED = 10065; // 新增数据失败
int UPDATE_DATA_FAILED = 10066; // 修改数据失败 int UPDATE_DATA_FAILED = 10066; // 修改数据失败
@@ -102,17 +94,17 @@ public interface ErrorCode {
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能 int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确 int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
int SMS_CODE_ERROR = 10075; // 手机验证码错误 int SMS_CODE_ERROR = 10075; // 手机验证码错误
// 字典类型相关错误码 // 字典类型相关错误码
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在 int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复 int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
// 资源处理相关错误码 // 资源处理相关错误码
int RESOURCE_READ_ERROR = 10078; // 读取资源失败 int RESOURCE_READ_ERROR = 10078; // 读取资源失败
// 智能体相关错误码 // 智能体相关错误码
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配 int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
// 声纹相关错误码 // 声纹相关错误码
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册 int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误 int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
@@ -126,12 +118,20 @@ public interface ErrorCode {
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败 int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败 int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败 int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
// 设备相关错误码
int MAC_ADDRESS_ALREADY_EXISTS = 10161; // Mac地址已存在
// 模型相关错误码
int MODEL_PROVIDER_NOT_EXIST = 10162; // 供应器不存在
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
// 服务端管理相关错误码 // 服务端管理相关错误码
int INVALID_SERVER_ACTION = 10095; // 无效服务端操作 int INVALID_SERVER_ACTION = 10095; // 无效服务端操作
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址 int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在 int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
// 参数验证相关错误码 // 参数验证相关错误码
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空 int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1 int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
@@ -175,4 +175,27 @@ public interface ErrorCode {
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效 int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
int CHAT_ROLE_USER = 10138; // 用户角色 int CHAT_ROLE_USER = 10138; // 用户角色
int CHAT_ROLE_AGENT = 10139; // 智能体角色 int CHAT_ROLE_AGENT = 10139; // 智能体角色
// 声音克隆相关错误码
int VOICE_CLONE_AUDIO_EMPTY = 10140; // 音频文件不能为空
int VOICE_CLONE_NOT_AUDIO_FILE = 10141; // 只支持音频文件
int VOICE_CLONE_AUDIO_TOO_LARGE = 10142; // 音频文件大小不能超过10MB
int VOICE_CLONE_UPLOAD_FAILED = 10143; // 上传失败
int VOICE_CLONE_RECORD_NOT_EXIST = 10144; // 声音克隆记录不存在
int VOICE_RESOURCE_INFO_EMPTY = 10145; // 音色资源信息不能为空
int VOICE_RESOURCE_PLATFORM_NAME_EMPTY = 10146; // 平台名称不能为空
int VOICE_RESOURCE_ID_EMPTY = 10147; // 音色ID不能为空
int VOICE_RESOURCE_ACCOUNT_EMPTY = 10148; // 归属账号不能为空
int VOICE_RESOURCE_DELETE_ID_EMPTY = 10149; // 删除的音色资源ID不能为空
int VOICE_RESOURCE_NO_PERMISSION = 10150; // 您没有权限操作该记录
int VOICE_CLONE_AUDIO_NOT_UPLOADED = 10151; // 请先上传音频文件
int VOICE_CLONE_MODEL_CONFIG_NOT_FOUND = 10152; // 模型配置未找到
int VOICE_CLONE_MODEL_TYPE_NOT_FOUND = 10153; // 模型类型未找到
int VOICE_CLONE_TRAINING_FAILED = 10154; // 训练失败
int VOICE_CLONE_HUOSHAN_CONFIG_MISSING = 10155; // 火山引擎缺少配置
int VOICE_CLONE_RESPONSE_FORMAT_ERROR = 10156; // 响应格式错误
int VOICE_CLONE_REQUEST_FAILED = 10157; // 请求失败
int VOICE_CLONE_PREFIX = 10158; // 复刻音色前缀
int VOICE_ID_ALREADY_EXISTS = 10159; // 音色ID已存在
int VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR = 10160; // 火山引擎音色ID格式错误
} }
@@ -146,4 +146,10 @@ public class RedisKeys {
return "agent:chat:history:" + uuid; return "agent:chat:history:" + uuid;
} }
/**
* 获取音色克隆音频ID的缓存key
*/
public static String getVoiceCloneAudioIdKey(String uuid) {
return "voiceClone:audio:id:" + uuid;
}
} }
@@ -11,15 +11,15 @@ import org.springframework.context.i18n.LocaleContextHolder;
public class MessageUtils { public class MessageUtils {
private static MessageSource messageSource; private static MessageSource messageSource;
static {
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
}
public static String getMessage(int code) { public static String getMessage(int code) {
return getMessage(code, new String[0]); return getMessage(code, new String[0]);
} }
public static String getMessage(int code, String... params) { public static String getMessage(int code, String... params) {
if (messageSource == null) {
// 延迟初始化,确保Spring上下文已完全初始化
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
}
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale()); return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
} }
} }
@@ -12,8 +12,6 @@ import java.util.UUID;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -49,7 +47,6 @@ import xiaozhi.modules.security.user.SecurityUser;
@RestController @RestController
@RequestMapping("/agent/chat-history") @RequestMapping("/agent/chat-history")
public class AgentChatHistoryController { public class AgentChatHistoryController {
private static final Logger logger = LoggerFactory.getLogger(AgentChatHistoryController.class);
private final AgentChatHistoryBizService agentChatHistoryBizService; private final AgentChatHistoryBizService agentChatHistoryBizService;
private final AgentChatHistoryService agentChatHistoryService; private final AgentChatHistoryService agentChatHistoryService;
private final AgentService agentService; private final AgentService agentService;
@@ -42,6 +42,7 @@ import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.dto.VoiceDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity; import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
@@ -373,6 +374,22 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
entity.setLlmModelId(template.getLlmModelId()); entity.setLlmModelId(template.getLlmModelId());
entity.setVllmModelId(template.getVllmModelId()); entity.setVllmModelId(template.getVllmModelId());
entity.setTtsModelId(template.getTtsModelId()); entity.setTtsModelId(template.getTtsModelId());
if (template.getTtsVoiceId() == null && template.getTtsModelId() != null) {
ModelConfigEntity ttsModel = modelConfigService.selectById(template.getTtsModelId());
if (ttsModel != null && ttsModel.getConfigJson() != null) {
Map<String, Object> config = ttsModel.getConfigJson();
String voice = (String) config.get("voice");
if (StringUtils.isBlank(voice)) {
voice = (String) config.get("speaker");
}
VoiceDTO timbre = timbreModelService.getByVoiceCode(template.getTtsModelId(), voice);
if (timbre != null) {
template.setTtsVoiceId(timbre.getId());
}
}
}
entity.setTtsVoiceId(template.getTtsVoiceId()); entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId()); entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId()); entity.setIntentModelId(template.getIntentModelId());
@@ -38,6 +38,8 @@ import xiaozhi.modules.sys.dto.SysParamsDTO;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.timbre.service.TimbreService; import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO; import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@@ -51,6 +53,7 @@ public class ConfigServiceImpl implements ConfigService {
private final TimbreService timbreService; private final TimbreService timbreService;
private final AgentPluginMappingService agentPluginMappingService; private final AgentPluginMappingService agentPluginMappingService;
private final AgentMcpAccessPointService agentMcpAccessPointService; private final AgentMcpAccessPointService agentMcpAccessPointService;
private final VoiceCloneService cloneVoiceService;
private final AgentVoicePrintDao agentVoicePrintDao; private final AgentVoicePrintDao agentVoicePrintDao;
@Override @Override
@@ -124,6 +127,11 @@ public class ConfigServiceImpl implements ConfigService {
voice = timbre.getTtsVoice(); voice = timbre.getTtsVoice();
referenceAudio = timbre.getReferenceAudio(); referenceAudio = timbre.getReferenceAudio();
referenceText = timbre.getReferenceText(); referenceText = timbre.getReferenceText();
} else {
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
if (voice_print != null) {
voice = voice_print.getVoiceId();
}
} }
// 构建返回数据 // 构建返回数据
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
@@ -298,7 +306,7 @@ public class ConfigServiceImpl implements ConfigService {
Map<String, Object> voiceprintConfig = new HashMap<>(); Map<String, Object> voiceprintConfig = new HashMap<>();
voiceprintConfig.put("url", voiceprintUrl); voiceprintConfig.put("url", voiceprintUrl);
voiceprintConfig.put("speakers", speakers); voiceprintConfig.put("speakers", speakers);
// 获取声纹识别相似度阈值,默认0.4 // 获取声纹识别相似度阈值,默认0.4
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true); String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) { if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
@@ -350,7 +358,7 @@ public class ConfigServiceImpl implements ConfigService {
* @param result 结果Map * @param result 结果Map
*/ */
private void buildModuleConfig( private void buildModuleConfig(
String assistantName, String assistantName,
String prompt, String prompt,
String summaryMemory, String summaryMemory,
String voice, String voice,
@@ -366,18 +374,18 @@ public class ConfigServiceImpl implements ConfigService {
Map<String, Object> result, Map<String, Object> result,
boolean isCache) { boolean isCache) {
Map<String, String> selectedModule = new HashMap<>(); Map<String, String> selectedModule = new HashMap<>();
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" }; String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId }; String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
String intentLLMModelId = null; String intentLLMModelId = null;
String memLocalShortLLMModelId = null; String memLocalShortLLMModelId = null;
for (int i = 0; i < modelIds.length; i++) { for (int i = 0; i < modelIds.length; i++) {
if (modelIds[i] == null) { if (modelIds[i] == null) {
continue; continue;
} }
// 关键:第三个参数传false,确保获取原始密钥 // 关键:第三个参数传false,确保获取原始密钥
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache, false); ModelConfigEntity model = modelConfigService.getModelByIdFromCache(modelIds[i]);
if (model == null) { if (model == null) {
continue; continue;
} }
@@ -392,6 +400,15 @@ public class ConfigServiceImpl implements ConfigService {
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio); ((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
if (referenceText != null) if (referenceText != null)
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText); ((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
// 火山引擎声音克隆需要替换resource_id
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(map.get("type"))) {
// 如果voice是”S_“开头的,使用seed-icl-1.0
if (voice != null && voice.startsWith("S_")) {
map.put("resource_id", "seed-icl-1.0");
}
}
} }
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型 // 如果是Intent类型,且type=intent_llm,则给他添加附加模型
if ("Intent".equals(modelTypes[i])) { if ("Intent".equals(modelTypes[i])) {
@@ -426,7 +443,7 @@ public class ConfigServiceImpl implements ConfigService {
if (StringUtils.isNotBlank(intentLLMModelId)) { if (StringUtils.isNotBlank(intentLLMModelId)) {
if (!typeConfig.containsKey(intentLLMModelId)) { if (!typeConfig.containsKey(intentLLMModelId)) {
// 修改这里:添加isMaskSensitive=false参数 // 修改这里:添加isMaskSensitive=false参数
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache, false); ModelConfigEntity intentLLM = modelConfigService.getModelByIdFromCache(intentLLMModelId);
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson()); typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
} }
} }
@@ -434,7 +451,7 @@ public class ConfigServiceImpl implements ConfigService {
if (!typeConfig.containsKey(memLocalShortLLMModelId)) { if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
// 修改这里:添加isMaskSensitive=false参数 // 修改这里:添加isMaskSensitive=false参数
ModelConfigEntity memLocalShortLLM = modelConfigService ModelConfigEntity memLocalShortLLM = modelConfigService
.getModelById(memLocalShortLLMModelId, isCache, false); .getModelByIdFromCache(memLocalShortLLMModelId);
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson()); typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
} }
} }
@@ -93,7 +93,7 @@ public class DeviceController {
} }
@PostMapping("/bind/{agentId}") @PostMapping("/bind/{agentId}")
@Operation(summary = "转发POST请求到MQTT网关") @Operation(summary = "设备在线接口")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) { public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
try { try {
@@ -208,63 +208,4 @@ public class DeviceController {
deviceService.manualAddDevice(user.getId(), dto); deviceService.manualAddDevice(user.getId(), dto);
return new Result<>(); return new Result<>();
} }
@PostMapping("/commands/{deviceId}")
@Operation(summary = "发送设备指令")
@RequiresPermissions("sys:role:normal")
public Result<String> sendDeviceCommand(@PathVariable String deviceId, @RequestBody String command) {
try {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return new Result<String>().error("MQTT网关地址未配置");
}
// 构建完整的URL
// 获取设备信息以构建mqttClientId
DeviceEntity deviceById = deviceService.selectById(deviceId);
if (!deviceById.getUserId().equals(SecurityUser.getUser().getId())) {
return new Result<String>().error("设备不存在");
}
String macAddress = deviceById != null ? deviceById.getMacAddress() : "unknown";
String groupId = deviceById != null ? deviceById.getBoard() : null;
if (groupId == null) {
groupId = "GID_default";
}
groupId = groupId.replace(":", "_");
macAddress = macAddress.replace(":", "_");
// 拼接为groupId@@@macAddress@@@deviceId格式
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
String url = "http://" + mqttGatewayUrl + "/api/commands/" + mqttClientId;
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 生成Bearer令牌
String dateStr = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
return new Result<String>().error("MQTT签名密钥未配置");
}
String tokenContent = dateStr + signatureKey;
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
headers.set("Authorization", "Bearer " + token);
// 构建请求体
HttpEntity<String> requestEntity = new HttpEntity<>(command, headers);
// 发送POST请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
// 返回响应
return new Result<String>().ok(response.getBody());
} catch (Exception e) {
return new Result<String>().error("发送指令失败: " + e.getMessage());
}
}
} }
@@ -19,7 +19,7 @@ public class DeviceReportRespDTO {
@Schema(description = "固件版本信息") @Schema(description = "固件版本信息")
private Firmware firmware; private Firmware firmware;
@Schema(description = "WebSocket配置") @Schema(description = "WebSocket配置")
private Websocket websocket; private Websocket websocket;
@@ -66,12 +66,14 @@ public class DeviceReportRespDTO {
@Schema(description = "时区偏移量,单位为分钟") @Schema(description = "时区偏移量,单位为分钟")
private Integer timezone_offset; private Integer timezone_offset;
} }
@Getter @Getter
@Setter @Setter
public static class Websocket { public static class Websocket {
@Schema(description = "WebSocket服务器地址") @Schema(description = "WebSocket服务器地址")
private String url; private String url;
@Schema(description = "WebSocket 认证 token")
private String token;
} }
@Getter @Getter
@@ -169,6 +169,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket(); DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
// 从系统参数获取WebSocket URL,如果未配置则使用默认值 // 从系统参数获取WebSocket URL,如果未配置则使用默认值
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
websocket.setToken("");
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) { if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置"); log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/"; wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
@@ -21,7 +21,12 @@ import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.config.service.ConfigService; import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.model.dto.*; import xiaozhi.modules.model.dto.LlmModelBasicInfoDTO;
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.dto.VoiceDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity; import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
@@ -124,6 +129,8 @@ public class ModelController {
if (entity == null) { if (entity == null) {
return new Result<Void>().error("模型配置不存在"); return new Result<Void>().error("模型配置不存在");
} }
// 不更新ConfigJson字段
entity.setConfigJson(null);
entity.setIsEnabled(status); entity.setIsEnabled(status);
modelConfigService.updateById(entity); modelConfigService.updateById(entity);
return new Result<Void>(); return new Result<Void>();
@@ -141,6 +148,8 @@ public class ModelController {
modelConfigService.setDefaultModel(entity.getModelType(), 0); modelConfigService.setDefaultModel(entity.getModelType(), 0);
entity.setIsEnabled(1); entity.setIsEnabled(1);
entity.setIsDefault(1); entity.setIsDefault(1);
// 不更新ConfigJson字段
entity.setConfigJson(null);
modelConfigService.updateById(entity); modelConfigService.updateById(entity);
// 更新模板表中对应的模型ID // 更新模板表中对应的模型ID
@@ -1,6 +1,7 @@
package xiaozhi.modules.model.dao; package xiaozhi.modules.model.dao;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -15,4 +16,9 @@ public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
* get model_code list * get model_code list
*/ */
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName); List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
/**
* 获取符合条件的TTS平台列表(id和modelName)
*/
List<Map<String, Object>> getTtsPlatformList();
} }
@@ -1,6 +1,7 @@
package xiaozhi.modules.model.service; package xiaozhi.modules.model.service;
import java.util.List; import java.util.List;
import java.util.Map;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
@@ -35,27 +36,23 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
/** /**
* 根据ID获取模型配置 * 根据ID获取模型配置
* *
* @param id 模型ID * @param id 模型ID
* @param isCache 是否缓存
* @param isMaskSensitive 是否掩码敏感信息
* @return 模型配置实体 * @return 模型配置实体
*/ */
ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive); ModelConfigEntity getModelByIdFromCache(String id);
/**
* 根据ID获取模型配置(默认掩码敏感信息)
*
* @param id 模型ID
* @param isCache 是否缓存
* @return 模型配置实体
*/
ModelConfigEntity getModelById(String id, boolean isCache);
/** /**
* 设置默认模型 * 设置默认模型
* *
* @param modelType 模型类型 * @param modelType 模型类型
* @param isDefault 是否默认(1:是,0:否) * @param isDefault 是否默认(1:是,0:否)
*/ */
void setDefaultModel(String modelType, int isDefault); void setDefaultModel(String modelType, int isDefault);
/**
* 获取符合条件的TTS平台列表
*
* @return TTS平台列表(id和modelName)
*/
List<Map<String, Object>> getTtsPlatformList();
} }
@@ -211,44 +211,15 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
} }
@Override @Override
public ModelConfigEntity getModelById(String id, boolean isCache) { public ModelConfigEntity getModelByIdFromCache(String id) {
return getModelById(id, isCache, true);
}
@Override
public ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive) {
if (StringUtils.isBlank(id)) { if (StringUtils.isBlank(id)) {
return null; return null;
} }
ModelConfigEntity entity = null;
String cacheKey = RedisKeys.getModelConfigById(id); String cacheKey = RedisKeys.getModelConfigById(id);
ModelConfigEntity entity = (ModelConfigEntity) redisUtils.get(cacheKey);
if (isCache) { if (entity == null) {
// 从缓存获取 entity = modelConfigDao.selectById(id);
entity = (ModelConfigEntity) redisUtils.get(cacheKey);
if (entity != null) { if (entity != null) {
// 修改:根据isMaskSensitive参数重新处理敏感信息
if (!isMaskSensitive && entity.getConfigJson() != null) {
// 如果需要获取原始配置,但缓存的是掩码后的配置,则从数据库重新获取
ModelConfigEntity originalEntity = modelConfigDao.selectById(id);
if (originalEntity != null) {
entity.setConfigJson(originalEntity.getConfigJson());
}
}
return entity;
}
}
// 从数据库获取数据
entity = modelConfigDao.selectById(id);
if (entity != null) {
// 根据isMaskSensitive参数决定是否掩码敏感信息
if (isMaskSensitive && entity.getConfigJson() != null) {
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
}
if (isCache) {
redisUtils.set(cacheKey, entity); redisUtils.set(cacheKey, entity);
} }
} }
@@ -367,19 +338,19 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
// 2. 只更新非敏感字段 // 2. 只更新非敏感字段
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName()); modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
modelConfigEntity.setSort(modelConfigBodyDTO.getSort()); modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled()); modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段 // 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) { if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
JSONObject originalJson = originalEntity.getConfigJson(); JSONObject originalJson = originalEntity.getConfigJson();
JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改 JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改
// 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段 // 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段
for (String key : modelConfigBodyDTO.getConfigJson().keySet()) { for (String key : modelConfigBodyDTO.getConfigJson().keySet()) {
Object value = modelConfigBodyDTO.getConfigJson().get(key); Object value = modelConfigBodyDTO.getConfigJson().get(key);
// 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值) // 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值)
if (SensitiveDataUtils.isSensitiveField(key)) { if (SensitiveDataUtils.isSensitiveField(key)) {
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) { if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
updatedJson.put(key, value); updatedJson.put(key, value);
} }
@@ -391,10 +362,10 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
updatedJson.put(key, value); updatedJson.put(key, value);
} }
} }
modelConfigEntity.setConfigJson(updatedJson); modelConfigEntity.setConfigJson(updatedJson);
} }
return modelConfigEntity; return modelConfigEntity;
} }
@@ -516,4 +487,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
} }
} }
} }
/**
* 获取符合条件的TTS平台列表
*/
@Override
public List<Map<String, Object>> getTtsPlatformList() {
return modelConfigDao.getTtsPlatformList();
}
} }
@@ -91,6 +91,7 @@ public class ShiroConfig {
filterMap.put("/agent/chat-history/download/**", "anon"); filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/saveMemory/**", "server"); filterMap.put("/agent/saveMemory/**", "server");
filterMap.put("/agent/play/**", "anon"); filterMap.put("/agent/play/**", "anon");
filterMap.put("/voiceClone/play/**", "anon");
filterMap.put("/**", "oauth2"); filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap); shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -197,10 +197,31 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
*/ */
private String generatePassword() { private String generatePassword() {
StringBuilder password = new StringBuilder(); StringBuilder password = new StringBuilder();
for (int i = 0; i < 12; i++) {
// 确保包含至少一个数字
password.append("0123456789".charAt(random.nextInt(10)));
// 确保包含至少一个小写字母
password.append("abcdefghijklmnopqrstuvwxyz".charAt(random.nextInt(26)));
// 确保包含至少一个大写字母
password.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(random.nextInt(26)));
// 确保包含至少一个特殊符号
password.append("!@#$%^&*()".charAt(random.nextInt(10)));
// 生成剩余的8个字符
for (int i = 4; i < 12; i++) {
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length()))); password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
} }
return password.toString();
// 打乱密码中字符的顺序
char[] passwordChars = password.toString().toCharArray();
for (int i = 0; i < passwordChars.length; i++) {
int randomIndex = random.nextInt(passwordChars.length);
char temp = passwordChars[i];
passwordChars[i] = passwordChars[randomIndex];
passwordChars[randomIndex] = temp;
}
return new String(passwordChars);
} }
@Override @Override
@@ -64,4 +64,13 @@ public interface TimbreService extends BaseService<TimbreEntity> {
* @return 音色名称 * @return 音色名称
*/ */
String getTimbreNameById(String id); String getTimbreNameById(String id);
/**
* 根据音色编码获取音色信息
*
* @param ttsModelId 音色模型ID
* @param voiceCode 音色编码
* @return 音色信息
*/
VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode);
} }
@@ -1,9 +1,11 @@
package xiaozhi.modules.timbre.service.impl; package xiaozhi.modules.timbre.service.impl;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,18 +17,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.MessageUtils;
import xiaozhi.modules.model.dto.VoiceDTO; import xiaozhi.modules.model.dto.VoiceDTO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.timbre.dao.TimbreDao; import xiaozhi.modules.timbre.dao.TimbreDao;
import xiaozhi.modules.timbre.dto.TimbreDataDTO; import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO; import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.entity.TimbreEntity; import xiaozhi.modules.timbre.entity.TimbreEntity;
import xiaozhi.modules.timbre.service.TimbreService; import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO; import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
/** /**
* 音色的业务层的实现 * 音色的业务层的实现
@@ -39,6 +46,7 @@ import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService { public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
private final TimbreDao timbreDao; private final TimbreDao timbreDao;
private final VoiceCloneDao voiceCloneDao;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
@Override @Override
@@ -121,11 +129,30 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
queryWrapper.like("name", voiceName); queryWrapper.like("name", voiceName);
} }
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper); List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
if (CollectionUtil.isEmpty(timbreEntities)) { if (timbreEntities == null) {
return null; timbreEntities = new ArrayList<>();
}
List<VoiceDTO> voiceDTOs = timbreEntities.stream()
.map(entity -> new VoiceDTO(entity.getId(), entity.getName()))
.collect(Collectors.toList());
// 获取当前登录用户ID
Long currentUserId = SecurityUser.getUser().getId();
if (currentUserId != null) {
// 查询用户的所有克隆音色记录
List<VoiceDTO> cloneEntities = voiceCloneDao.getTrainSuccess(ttsModelId, currentUserId);
for (VoiceDTO entity : cloneEntities) {
// 只添加训练成功的克隆音色,且模型ID匹配
VoiceDTO voiceDTO = new VoiceDTO();
voiceDTO.setId(entity.getId());
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
RedisUtils.NOT_EXPIRE);
voiceDTOs.add(0, voiceDTO);
}
} }
return ConvertUtils.sourceToTarget(timbreEntities, VoiceDTO.class); return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs;
} }
/** /**
@@ -154,8 +181,30 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
redisUtils.set(RedisKeys.getTimbreNameById(id), name); redisUtils.set(RedisKeys.getTimbreNameById(id), name);
} }
return name; return name;
} else {
VoiceCloneEntity cloneEntity = voiceCloneDao.selectById(id);
if (cloneEntity != null) {
String name = MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + cloneEntity.getName();
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
return name;
}
} }
return null; return null;
} }
@Override
public VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode) {
if (StringUtils.isBlank(voiceCode)) {
return null;
}
QueryWrapper<TimbreEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("tts_model_id", ttsModelId);
queryWrapper.eq("tts_voice", voiceCode);
List<TimbreEntity> list = timbreDao.selectList(queryWrapper);
if (list.isEmpty()) {
return null;
}
return new VoiceDTO(list.get(0).getId(), list.get(0).getName());
}
} }
@@ -0,0 +1,193 @@
package xiaozhi.modules.voiceclone.controller;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/voiceClone")
public class VoiceCloneController {
private final VoiceCloneService voiceCloneService;
private final RedisUtils redisUtils;
@GetMapping
@Operation(summary = "分页查询音色资源")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
})
@RequiresPermissions("sys:role:normal")
public Result<PageData<VoiceCloneResponseDTO>> page(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
UserDetail user = SecurityUser.getUser();
params.put("userId", user.getId().toString());
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
}
@PostMapping("/upload")
@Operation(summary = "上传音频进行声音克隆")
@Parameters({
@Parameter(name = "id", description = "声音克隆记录ID", required = true),
@Parameter(name = "voiceFile", description = "音频文件", required = true)
})
@RequiresPermissions("sys:role:normal")
public Result<String> uploadVoice(
@RequestParam("id") String id,
@RequestParam("voiceFile") MultipartFile voiceFile) {
try {
// 验证文件
if (voiceFile == null || voiceFile.isEmpty()) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_EMPTY);
}
// 验证文件类型
String contentType = voiceFile.getContentType();
if (contentType == null || !contentType.startsWith("audio/")) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_NOT_AUDIO_FILE);
}
// 验证文件大小 (最大10MB)
if (voiceFile.getSize() > 10 * 1024 * 1024) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
}
// 检查权限
checkPermission(id);
// 调用服务层处理
voiceCloneService.uploadVoice(id, voiceFile);
return new Result<String>();
} catch (Exception e) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_UPLOAD_FAILED, e.getMessage());
}
}
@PostMapping("/updateName")
@Operation(summary = "更新声音克隆名称")
@RequiresPermissions("sys:role:normal")
public Result<String> updateName(@RequestBody Map<String, String> params) {
try {
String id = params.get("id");
String name = params.get("name");
if (id == null || id.isEmpty()) {
return new Result<String>().error(ErrorCode.IDENTIFIER_NOT_NULL, "唯一标识不能为空");
}
if (name == null) {
return new Result<String>().error(ErrorCode.NOT_NULL, "名称不能为空");
}
// 检查权限
checkPermission(id);
voiceCloneService.updateName(id, name);
redisUtils.delete(RedisKeys.getTimbreNameById(id));
return new Result<String>();
} catch (Exception e) {
return new Result<String>().error(ErrorCode.UPDATE_DATA_FAILED, "更新失败: " + e.getMessage());
}
}
@PostMapping("/audio/{id}")
@Operation(summary = "获取音频下载ID")
@RequiresPermissions("sys:role:normal")
public Result<String> getAudioId(@PathVariable("id") String id) {
// 检查权限
checkPermission(id);
byte[] audioData = voiceCloneService.getVoiceData(id);
if (audioData == null) {
return new Result<String>().error(ErrorCode.RESOURCE_NOT_FOUND, "音频不存在");
}
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getVoiceCloneAudioIdKey(uuid), id);
return new Result<String>().ok(uuid);
}
@GetMapping("/play/{uuid}")
@Operation(summary = "播放音频")
public void playVoice(@PathVariable("uuid") String uuid, HttpServletResponse response) {
try {
String id = (String) redisUtils.get(RedisKeys.getVoiceCloneAudioIdKey(uuid));
redisUtils.delete(RedisKeys.getVoiceCloneAudioIdKey(uuid));
if (StringUtils.isBlank(id)) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 获取音频数据
byte[] voiceData = voiceCloneService.getVoiceData(id);
if (voiceData == null || voiceData.length == 0) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("audio/wav");
response.setContentLength(voiceData.length);
response.setHeader("Content-Disposition", "inline; filename=voice.wav");
// 写入音频数据
response.getOutputStream().write(voiceData);
response.getOutputStream().flush();
} catch (Exception e) {
log.error("播放音频失败", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/cloneAudio")
@Operation(summary = "复刻音频")
@RequiresPermissions("sys:role:normal")
public Result<String> cloneAudio(@RequestBody Map<String, String> params) {
String cloneId = params.get("cloneId");
checkPermission(cloneId);
// 调用服务层进行语音克隆训练
voiceCloneService.cloneAudio(cloneId);
return new Result<String>();
}
private void checkPermission(String id) {
VoiceCloneEntity voiceClone = voiceCloneService.selectById(id);
if (voiceClone == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
if (!voiceClone.getUserId().equals(SecurityUser.getUser().getId())) {
throw new RenException(ErrorCode.VOICE_RESOURCE_NO_PERMISSION);
}
}
}
@@ -0,0 +1,116 @@
package xiaozhi.modules.voiceclone.controller;
import java.util.List;
import java.util.Map;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/voiceResource")
public class VoiceResourceController {
private final VoiceCloneService voiceCloneService;
private final ModelConfigService modelConfigService;
@GetMapping
@Operation(summary = "分页查询音色资源")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
})
@RequiresPermissions("sys:role:superAdmin")
public Result<PageData<VoiceCloneResponseDTO>> page(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
}
@GetMapping("{id}")
@Operation(summary = "获取音色资源详情")
@RequiresPermissions("sys:role:superAdmin")
public Result<VoiceCloneResponseDTO> get(@PathVariable("id") String id) {
VoiceCloneResponseDTO data = voiceCloneService.getByIdWithNames(id);
return new Result<VoiceCloneResponseDTO>().ok(data);
}
@PostMapping
@Operation(summary = "新增音色资源")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody VoiceCloneDTO dto) {
if (dto == null) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_INFO_EMPTY);
}
if (dto.getModelId() == null || dto.getModelId().isEmpty()) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_PLATFORM_NAME_EMPTY);
}
if (dto.getVoiceIds() == null || dto.getVoiceIds().isEmpty()) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ID_EMPTY);
}
if (dto.getUserId() == null) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ACCOUNT_EMPTY);
}
try {
voiceCloneService.save(dto);
return new Result<Void>();
} catch (xiaozhi.common.exception.RenException e) {
return new Result<Void>().error(e.getCode(), e.getMsg());
} catch (RuntimeException e) {
return new Result<Void>().error(ErrorCode.ADD_DATA_FAILED, e.getMessage());
}
}
@DeleteMapping("/{id}")
@Operation(summary = "删除音色资源")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable("id") String[] ids) {
if (ids == null || ids.length == 0) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_DELETE_ID_EMPTY);
}
voiceCloneService.delete(ids);
return new Result<Void>();
}
@GetMapping("/user/{userId}")
@Operation(summary = "根据用户ID获取音色资源列表")
@RequiresPermissions("sys:role:normal")
public Result<List<VoiceCloneResponseDTO>> getByUserId(@PathVariable("userId") Long userId) {
List<VoiceCloneResponseDTO> list = voiceCloneService.getByUserIdWithNames(userId);
return new Result<List<VoiceCloneResponseDTO>>().ok(list);
}
@GetMapping("/ttsPlatforms")
@Operation(summary = "获取TTS平台列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<Map<String, Object>>> getTtsPlatformList() {
List<Map<String, Object>> list = modelConfigService.getTtsPlatformList();
return new Result<List<Map<String, Object>>>().ok(list);
}
}
@@ -0,0 +1,26 @@
package xiaozhi.modules.voiceclone.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.model.dto.VoiceDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
/**
* 声音克隆
*/
@Mapper
public interface VoiceCloneDao extends BaseMapper<VoiceCloneEntity> {
/**
* 获取用户训练成功的音色列表
*
* @param modelId 模型ID
* @param userId 用户ID
* @return 训练成功的音色列表
*/
List<VoiceDTO> getTrainSuccess(String modelId, Long userId);
}
@@ -0,0 +1,20 @@
package xiaozhi.modules.voiceclone.dto;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "声音克隆DTO")
public class VoiceCloneDTO {
@Schema(description = "模型ID")
private String modelId;
@Schema(description = "音色ID列表")
private List<String> voiceIds;
@Schema(description = "用户ID")
private Long userId;
}
@@ -0,0 +1,48 @@
package xiaozhi.modules.voiceclone.dto;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 声音克隆响应DTO
* 用于向前端展示声音克隆信息,包含模型名称和用户名称
*/
@Data
@Schema(description = "声音克隆响应DTO")
public class VoiceCloneResponseDTO {
@Schema(description = "唯一标识")
private String id;
@Schema(description = "声音名称")
private String name;
@Schema(description = "模型id")
private String modelId;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "声音id")
private String voiceId;
@Schema(description = "用户ID(关联用户表)")
private Long userId;
@Schema(description = "用户名称")
private String userName;
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
private Integer trainStatus;
@Schema(description = "训练错误原因")
private String trainError;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "是否有音频数据")
private Boolean hasVoice;
}
@@ -0,0 +1,53 @@
package xiaozhi.modules.voiceclone.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_voice_clone")
@Schema(description = "声音克隆")
public class VoiceCloneEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "唯一标识")
private String id;
@Schema(description = "声音名称")
private String name;
@Schema(description = "模型id")
private String modelId;
@Schema(description = "声音id")
private String voiceId;
@Schema(description = "用户 ID(关联用户表)")
private Long userId;
@Schema(description = "声音")
private byte[] voice;
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
private Integer trainStatus;
@Schema(description = "训练错误原因")
private String trainError;
@Schema(description = "创建者")
@TableField(fill = FieldFill.INSERT)
private Long creator;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date createDate;
}
@@ -0,0 +1,78 @@
package xiaozhi.modules.voiceclone.service;
import java.util.List;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
/**
* 声音克隆管理
*/
public interface VoiceCloneService extends BaseService<VoiceCloneEntity> {
/**
* 分页查询
*/
PageData<VoiceCloneEntity> page(Map<String, Object> params);
/**
* 保存声音克隆
*/
void save(VoiceCloneDTO dto);
/**
* 批量删除
*/
void delete(String[] ids);
/**
* 根据用户ID查询声音克隆列表
*
* @param userId 用户ID
* @return 声音克隆列表
*/
List<VoiceCloneEntity> getByUserId(Long userId);
/**
* 分页查询带模型名称和用户名称的声音克隆列表
*/
PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params);
/**
* 根据ID查询带模型名称和用户名称的声音克隆信息
*/
VoiceCloneResponseDTO getByIdWithNames(String id);
/**
* 根据用户ID查询带模型名称的声音克隆列表
*/
List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId);
/**
* 上传音频文件
*/
void uploadVoice(String id, MultipartFile voiceFile) throws Exception;
/**
* 更新声音克隆名称
*/
void updateName(String id, String name);
/**
* 获取音频数据
*/
byte[] getVoiceData(String id);
/**
* 克隆音频,调用火山引擎进行语音复刻训练
*
* @param cloneId 语音克隆记录ID
*/
void cloneAudio(String cloneId);
}
@@ -0,0 +1,374 @@
package xiaozhi.modules.voiceclone.service.impl;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Slf4j
@Service
@RequiredArgsConstructor
public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceCloneEntity>
implements VoiceCloneService {
private final ModelConfigService modelConfigService;
private final SysUserService sysUserService;
private final ObjectMapper objectMapper;
@Override
public PageData<VoiceCloneEntity> page(Map<String, Object> params) {
IPage<VoiceCloneEntity> page = baseDao.selectPage(
getPage(params, "create_date", true),
getWrapper(params));
return new PageData<>(page.getRecords(), page.getTotal());
}
private QueryWrapper<VoiceCloneEntity> getWrapper(Map<String, Object> params) {
String name = (String) params.get("name");
String userId = (String) params.get("userId");
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(userId), "user_id", userId);
if (StringUtils.isNotBlank(name)) {
wrapper.and(w -> w.like("name", name)
.or().eq("voice_id", name));
}
return wrapper;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(VoiceCloneDTO dto) {
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(dto.getModelId());
if (modelConfig == null || modelConfig.getConfigJson() == null) {
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_CONFIG_NOT_FOUND);
}
Map<String, Object> config = modelConfig.getConfigJson();
String type = (String) config.get("type");
if (StringUtils.isBlank(type)) {
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND);
}
// 检查Voice ID是否已经被使用
for (String voiceId : dto.getVoiceIds()) {
if (StringUtils.isBlank(voiceId)) {
continue;
}
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) {
if (voiceId.indexOf("S_") == -1) {
throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR);
}
}
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
wrapper.eq("voice_id", voiceId);
wrapper.eq("model_id", dto.getModelId());
Long count = baseDao.selectCount(wrapper);
if (count > 0) {
throw new RenException(ErrorCode.VOICE_ID_ALREADY_EXISTS, "音色ID " + voiceId + " 已存在");
}
}
// 遍历选择的音色ID,为每个音色ID创建一条记录
int index = 0;
String namePrefix = DateUtils.format(new java.util.Date(), "MMddHHmm");
for (String voiceId : dto.getVoiceIds()) {
index++;
VoiceCloneEntity entity = new VoiceCloneEntity();
entity.setModelId(dto.getModelId());
entity.setVoiceId(voiceId);
entity.setName(namePrefix + "_" + index);
entity.setUserId(dto.getUserId());
entity.setTrainStatus(0); // 默认训练中
baseDao.insert(entity);
}
}
@Override
public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
}
@Override
public List<VoiceCloneEntity> getByUserId(Long userId) {
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
wrapper.orderByDesc("created_at");
return baseDao.selectList(wrapper);
}
@Override
public PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params) {
// 先查询分页数据
IPage<VoiceCloneEntity> page = baseDao.selectPage(
getPage(params, "create_date", true),
getWrapper(params));
// 将实体列表转换为DTO列表
List<VoiceCloneResponseDTO> dtoList = convertToResponseDTOList(page.getRecords());
return new PageData<>(dtoList, page.getTotal());
}
@Override
public VoiceCloneResponseDTO getByIdWithNames(String id) {
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
return null;
}
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
// 设置模型名称
if (StringUtils.isNotBlank(entity.getModelId())) {
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
}
// 设置用户名称
if (entity.getUserId() != null) {
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
}
return dto;
}
@Override
public List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId) {
List<VoiceCloneEntity> entityList = getByUserId(userId);
return convertToResponseDTOList(entityList);
}
/**
* 将VoiceCloneEntity列表转换为VoiceCloneResponseDTO列表
*/
private List<VoiceCloneResponseDTO> convertToResponseDTOList(List<VoiceCloneEntity> entityList) {
if (entityList == null || entityList.isEmpty()) {
return new ArrayList<>();
}
List<VoiceCloneResponseDTO> dtoList = new ArrayList<>(entityList.size());
// 转换每个实体为DTO
for (VoiceCloneEntity entity : entityList) {
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
// 设置模型名称
if (StringUtils.isNotBlank(entity.getModelId())) {
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
}
// 设置用户名称
if (entity.getUserId() != null) {
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
}
// 设置是否有音频数据
dto.setHasVoice(entity.getVoice() != null);
dtoList.add(dto);
}
return dtoList;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void uploadVoice(String id, MultipartFile voiceFile) throws Exception {
// 查询声音克隆记录
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
// 读取音频文件并转为字节数组
byte[] voiceData = voiceFile.getBytes();
// 更新voice字段
entity.setVoice(voiceData);
// 更新训练状态为待训练
entity.setTrainStatus(0);
// 保存到数据库
baseDao.updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateName(String id, String name) {
// 查询声音克隆记录
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
// 更新名称
entity.setName(name);
baseDao.updateById(entity);
}
@Override
public byte[] getVoiceData(String id) {
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
return null;
}
return entity.getVoice();
}
@Override
// @Transactional(rollbackFor = Exception.class)
public void cloneAudio(String cloneId) {
VoiceCloneEntity entity = baseDao.selectById(cloneId);
if (entity == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
if (entity.getVoice() == null || entity.getVoice().length == 0) {
throw new RenException(ErrorCode.VOICE_CLONE_AUDIO_NOT_UPLOADED);
}
try {
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(entity.getModelId());
if (modelConfig == null || modelConfig.getConfigJson() == null) {
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_CONFIG_NOT_FOUND);
}
Map<String, Object> config = modelConfig.getConfigJson();
String type = (String) config.get("type");
if (StringUtils.isBlank(type)) {
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND);
}
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) {
huoshanClone(config, entity);
}
} catch (RenException re) {
entity.setTrainStatus(3);
entity.setTrainError(re.getMsg());
baseDao.updateById(entity);
throw re;
} catch (Exception e) {
e.printStackTrace();
entity.setTrainStatus(3);
entity.setTrainError(e.getMessage());
baseDao.updateById(entity);
throw new RenException(ErrorCode.VOICE_CLONE_TRAINING_FAILED, e.getMessage());
}
}
/**
* 调用火山引擎进行语音复刻训练
*
* @param config 模型配置
* @param entity 语音克隆记录实体
* @throws Exception
*/
private void huoshanClone(Map<String, Object> config, VoiceCloneEntity entity) throws Exception {
String appid = (String) config.get("appid");
String accessToken = (String) config.get("access_token");
if (StringUtils.isAnyBlank(appid, accessToken)) {
throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_CONFIG_MISSING);
}
String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice());
Map<String, Object> reqBody = new HashMap<>();
reqBody.put("appid", appid);
List<Map<String, String>> audios = new ArrayList<>();
Map<String, String> audioMap = new HashMap<>();
audioMap.put("audio_bytes", audioBase64);
audioMap.put("audio_format", "wav");
audios.add(audioMap);
reqBody.put("audios", audios);
reqBody.put("source", 2);
reqBody.put("language", 0);
reqBody.put("model_type", 1);
reqBody.put("speaker_id", entity.getVoiceId());
String apiUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer;" + accessToken)
.header("Resource-Id", "seed-icl-1.0")
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody)))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(">>> HTTP status = " + response.statusCode());
System.out.println(">>> response body = " + response.body());
Map<String, Object> rsp = objectMapper.readValue(response.body(),
new TypeReference<Map<String, Object>>() {
});
// 获取BaseResp对象
Map<String, Object> baseResp = objectMapper.convertValue(rsp.get("BaseResp"),
new TypeReference<Map<String, Object>>() {
});
if (baseResp != null) {
Integer statusCode = objectMapper.convertValue(baseResp.get("StatusCode"), Integer.class);
String statusMessage = objectMapper.convertValue(baseResp.getOrDefault("StatusMessage", ""),
String.class);
// 获取speaker_id
String speakerId = objectMapper.convertValue(rsp.get("speaker_id"), String.class);
// StatusCode == 0 表示成功
if (statusCode != null && statusCode == 0 && StringUtils.isNotBlank(speakerId)) {
entity.setTrainStatus(2);
entity.setVoiceId(speakerId);
entity.setTrainError("");
baseDao.updateById(entity);
} else {
// 失败时使用StatusMessage作为错误信息
String errorMsg = StringUtils.isNotBlank(statusMessage) ? statusMessage : "训练失败";
throw new RenException(errorMsg);
}
} else {
String errorMsg = objectMapper.convertValue(rsp.get("message"),
new TypeReference<String>() {
});
if (StringUtils.isNotBlank(errorMsg)) {
throw new RenException(errorMsg);
}
throw new RenException(ErrorCode.VOICE_CLONE_RESPONSE_FORMAT_ERROR);
}
}
}
@@ -0,0 +1,19 @@
-- 声音克隆表
DROP TABLE IF EXISTS `ai_voice_clone`;
CREATE TABLE `ai_voice_clone` (
`id` VARCHAR(32) NOT NULL COMMENT '唯一标识',
`name` VARCHAR(64) COMMENT '声音名称',
`model_id` VARCHAR(32) COMMENT '模型id',
`voice_id` VARCHAR(32) COMMENT '声音id',
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
`voice` LONGBLOB COMMENT '声音',
`train_status` TINYINT(1) DEFAULT 0 COMMENT '训练状态:0待训练 1训练中 2训练成功 3训练失败',
`train_error` VARCHAR(255) COMMENT '训练错误原因',
`creator` BIGINT COMMENT '创建者 ID',
`create_date` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`),
INDEX idx_ai_voice_clone_user_id_model_id_train_status (model_id,user_id, train_status),
INDEX idx_ai_voice_clone_voice_id (voice_id),
INDEX idx_ai_voice_clone_user_id (user_id),
INDEX idx_ai_voice_clone_model_id_voice_id (model_id, voice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声音克隆表';
@@ -0,0 +1,2 @@
UPDATE `ai_model_config` SET model_code = 'XunfeiSparkLLM' WHERE id = 'LLM_XunfeiSparkLLM';
UPDATE `ai_model_config` SET model_code = 'XunfeiStreamASR' WHERE id = 'ASR_XunfeiStream';
@@ -388,3 +388,17 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202509220958.sql path: classpath:db/changelog/202509220958.sql
- changeSet:
id: 202510071522
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202510071522.sql
- changeSet:
id: 202510191042
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202510191042.sql
@@ -1,146 +1,171 @@
#简体中文 #\u7B80\u4F53\u4E2D\u6587
500=服务器内部异常 500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=未授权 401=\u672A\u6388\u6743
403=拒绝访问,没有权限 403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}不能为空 10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=数据库中已存在该记录 10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=获取参数失败 10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=账号或密码错误 10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=账号已被停用 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=唯一标识不能为空 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=验证码不正确 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=先删除子菜单或按钮 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=原密码不正确 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=账号或密码不正确,您还有可以尝试{0}次 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=上级部门选择错误 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=上级菜单不能为自身 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=数据权限接口,只能是Map类型参数 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=请先删除下级部门 10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=请先删除部门下的用户 10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=部署失败,没有流程 10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=模型图不正确,请检查 10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=导出失败,模型ID为{0} 10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=请上传文件 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token不能为空 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token失效,请重新登录 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=账号已被锁定 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件 10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上传文件失败{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=发送短信失败{0} 10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=邮件模板不存在 10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服务异常 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=定时任务失败 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=不能包含非法字符 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=密码长度不足{0}位 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=密码必须同时包含数字、大小写字母和特殊字符组成 10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=删除本数据异常 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=设备验证码错误 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=参数值不能为空 10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=参数类型不能为空 10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=不支持的参数类型 10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=参数值必须是有效的数字 10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=参数值必须是true或false 10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=参数值必须是有效的JSON数组格式 10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=参数值必须是有效的JSON格式 10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=设备未找到 10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0} 10042={0}
10043=删除数据失败 10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=用户未登录 10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket连接失败或连接超时 10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=保存声纹错误,请联系管理员 10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=每日发送次数已达上限 10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=旧密码输入错误 10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=设置的LLM不是openai和ollama 10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token生成失败 10050=token\u751F\u6210\u5931\u8D25
10051=资源不存在 10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=默认智能体未找到 10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=智能体未找到 10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print) 10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=短信发送失败 10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=短信连接建立失败 10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=智能体的声纹创建失败 10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=智能体的对应声纹更新失败 10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=智能体的对应声纹删除失败 10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=发送太频繁,请{0}秒后再试 10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=激活码不能为空 10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=激活码错误 10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=设备已激活 10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=该模型为默认模型,请先设置其他模型为默认模型 10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=新增数据失败 10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=修改数据失败 10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=图形验证码错误 10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=没有开启手机注册,没法使用短信验证码功能 10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=用户名不是手机号码,请重新输入 10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=此手机号码已经注册过 10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=输入的手机号码未注册 10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=当前不允许普通用户注册 10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=没有开启手机注册,没法使用找回密码功能 10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=输入的手机号码格式不正确 10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=输入的手机验证码错误 10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=字典类型不存在 10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=字典类型编码重复 10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=读取资源失败 10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM大模型和Intent意图识别,选择参数不匹配 10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册 10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
10081=删除声纹出现错误 10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=此次修改不允许,此声音已经注册为声纹了({0}) 10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=修改声纹错误,请联系管理员 10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址 10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10085=音频数据不属于这个智能体 10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=音频数据是空的请检查上传数据 10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=声纹保存失败,请求不成功 10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=声纹保存失败,请求处理失败 10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=声纹注销失败,请求不成功 10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=声纹注销失败,请求处理失败 10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=供应器不存在 10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=设置的LLM不存在 10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=该模型配置已被智能体{0}引用,无法删除 10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=该LLM模型已被意图识别配置引用,无法删除 10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=无效服务端操作 10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=未配置服务端WebSocket地址 10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=目标WebSocket地址不存在 10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket地址列表不能为空 10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket地址不能使用localhost127.0.0.1 10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket地址格式不正确 10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket连接测试失败 10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA地址不能为空 10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA地址不能使用localhost127.0.0.1 10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA地址必须以http或https开头 10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA地址必须以/ota/结尾 10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA接口访问失败 10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA接口返回内容格式不正确 10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA接口验证失败 10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP地址不能为空 10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP地址不能使用localhost127.0.0.1 10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=不是正确的MCP地址 10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP接口访问失败 10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP接口返回内容格式不正确 10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP接口验证失败 10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=声纹接口地址不能为空 10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=声纹接口地址不能使用localhost127.0.0.1 10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=不是正确的声纹接口地址 10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=声纹接口地址必须以http或https开头 10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=声纹接口访问失败 10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=声纹接口返回内容格式不正确 10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=声纹接口验证失败 10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt密钥不能为空 10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母 10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母 10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=您的mqtt密钥包含弱密码 10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=字典标签重复 10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=modelType和provideCode不能为空 10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
10132=没有权限查看该智能体的聊天记录 10130=SM2\u89E3\u5BC6\u5931\u8D25
10133=会话ID不能为空 10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
10134=智能体ID不能为空 10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
10135=聊天记录下载失败 10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
10136=下载链接已过期或无效 10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
10137=下载链接无效 10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
10138=用户 10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
10139=智体 10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
10138=\u7528\u6237
10139=\u667A\u4F53
10140=\u97F3\u9891\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A
10141=\u53EA\u652F\u6301\u97F3\u9891\u6587\u4EF6
10142=\u97F3\u9891\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC710MB
10143=\u4E0A\u4F20\u5931\u8D25
10144=\u58F0\u97F3\u514B\u9686\u8BB0\u5F55\u4E0D\u5B58\u5728
10145=\u97F3\u8272\u8D44\u6E90\u4FE1\u606F\u4E0D\u80FD\u4E3A\u7A7A
10146=\u5E73\u53F0\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A
10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A
10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55
10151=\u8BF7\u5148\u4E0A\u4F20\u97F3\u9891\u6587\u4EF6
10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230
10153=\u6A21\u578B\u7C7B\u578B\u672A\u627E\u5230
10154=\u8BAD\u7EC3\u5931\u8D25
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11\u914D\u7F6E
10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
10157=\u8BF7\u6C42\u5931\u8D25
10158=\u8BF7\u6C42\u5931\u8D25\uFF0C\u72B6\u6001\u7801{0}
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5S_\u5F00\u5934
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
10162=\u6A21\u578B\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
@@ -145,4 +145,27 @@
10136=Download link expired or invalid 10136=Download link expired or invalid
10137=Download link invalid 10137=Download link invalid
10138=User 10138=User
10139=Agent 10139=Agent
10140=Audio file cannot be empty
10141=Only audio files are supported
10142=Audio file size cannot exceed 10MB
10143=Upload failed
10144=Voice clone record does not exist
10145=Voice resource information cannot be empty
10146=TTS platform name cannot be empty
10147=Voice ID cannot be empty
10148=Owner account cannot be empty
10149=Voice resource ID to delete cannot be empty
10150=You do not have permission to operate this record
10151=Please upload audio file first
10152=Model configuration not found
10153=Model type not found
10154=Training failed: {0}
10155=Huoshan Engine configuration missing
10156=Response format error, missing BaseResp field
10157=Request failed
10158=Clone Voice:
10159=Voice ID already exists
10160=Huoshan Engine voice ID format error, must start with S_
10161=Mac address already exists
10162=Model provider does not exist
@@ -1,148 +1,171 @@
#简体中文 #\u7B80\u4F53\u4E2D\u6587
500=服务器内部异常 500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=未授权 401=\u672A\u6388\u6743
403=拒绝访问,没有权限 403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}不能为空 10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=数据库中已存在该记录 10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=获取参数失败 10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=账号或密码错误 10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=账号已被停用 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=唯一标识不能为空 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=验证码不正确 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=先删除子菜单或按钮 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=原密码不正确 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=账号或密码不正确,您还有可以尝试{0}次 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=上级部门选择错误 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=上级菜单不能为自身 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=数据权限接口,只能是Map类型参数 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=请先删除下级部门 10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=请先删除部门下的用户 10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=部署失败,没有流程 10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=模型图不正确,请检查 10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=导出失败,模型ID为{0} 10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=请上传文件 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token不能为空 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token失效,请重新登录 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=账号已被锁定 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件 10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上传文件失败{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=发送短信失败{0} 10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=邮件模板不存在 10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服务异常 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=定时任务失败 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=不能包含非法字符 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=密码长度不足{0}位 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=密码必须同时包含数字、大小写字母和特殊字符组成 10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=删除本数据异常 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=设备验证码错误 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=参数值不能为空 10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=参数类型不能为空 10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=不支持的参数类型 10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=参数值必须是有效的数字 10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=参数值必须是true或false 10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=参数值必须是有效的JSON数组格式 10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=参数值必须是有效的JSON格式 10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=设备未找到 10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0} 10042={0}
10043=删除数据失败 10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=用户未登录 10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket连接失败或连接超时 10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=保存声纹错误,请联系管理员 10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=每日发送次数已达上限 10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=旧密码输入错误 10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=设置的LLM不是openai和ollama 10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token生成失败 10050=token\u751F\u6210\u5931\u8D25
10051=资源不存在 10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=默认智能体未找到 10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=智能体未找到 10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print) 10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=短信发送失败 10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=短信连接建立失败 10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=智能体的声纹创建失败 10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=智能体的对应声纹更新失败 10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=智能体的对应声纹删除失败 10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=发送太频繁,请{0}秒后再试 10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=激活码不能为空 10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=激活码错误 10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=设备已激活 10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=该模型为默认模型,请先设置其他模型为默认模型 10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=新增数据失败 10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=修改数据失败 10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=图形验证码错误 10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=没有开启手机注册,没法使用短信验证码功能 10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=用户名不是手机号码,请重新输入 10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=此手机号码已经注册过 10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=输入的手机号码未注册 10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=当前不允许普通用户注册 10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=没有开启手机注册,没法使用找回密码功能 10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=输入的手机号码格式不正确 10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=输入的手机验证码错误 10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=字典类型不存在 10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=字典类型编码重复 10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=读取资源失败 10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM大模型和Intent意图识别,选择参数不匹配 10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册 10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
10081=删除声纹出现错误 10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=此次修改不允许,此声音已经注册为声纹了({0}) 10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=修改声纹错误,请联系管理员 10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址 10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10085=音频数据不属于这个智能体 10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=音频数据是空的请检查上传数据 10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=声纹保存失败,请求不成功 10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=声纹保存失败,请求处理失败 10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=声纹注销失败,请求不成功 10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=声纹注销失败,请求处理失败 10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=供应器不存在 10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=设置的LLM不存在 10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=该模型配置已被智能体{0}引用,无法删除 10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=该LLM模型已被意图识别配置引用,无法删除 10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=无效服务端操作 10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=未配置服务端WebSocket地址 10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=目标WebSocket地址不存在 10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket地址列表不能为空 10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket地址不能使用localhost127.0.0.1 10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket地址格式不正确 10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket连接测试失败 10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA地址不能为空 10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA地址不能使用localhost127.0.0.1 10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA地址必须以http或https开头 10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA地址必须以/ota/结尾 10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA接口访问失败 10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA接口返回内容格式不正确 10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA接口验证失败 10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP地址不能为空 10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP地址不能使用localhost127.0.0.1 10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=不是正确的MCP地址 10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP接口访问失败 10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP接口返回内容格式不正确 10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP接口验证失败 10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=声纹接口地址不能为空 10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=声纹接口地址不能使用localhost127.0.0.1 10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=不是正确的声纹接口地址 10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=声纹接口地址必须以http或https开头 10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=声纹接口访问失败 10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=声纹接口返回内容格式不正确 10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=声纹接口验证失败 10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt密钥不能为空 10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母 10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母 10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=您的mqtt密钥包含弱密码 10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=字典标签重复 10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=SM2密钥未配置 10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
10130=SM2解密失败 10130=SM2\u89E3\u5BC6\u5931\u8D25
10131=modelTypeprovideCode不能为空 10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
10132=没有权限查看该智能体的聊天记录 10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
10133=会话ID不能为空 10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
10134=智能体ID不能为空 10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
10135=聊天记录下载失败 10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
10136=下载链接已过期或无效 10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
10137=下载链接无效 10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
10138=用户 10138=\u7528\u6237
10139=智体 10139=\u667A\u4F53
10140=\u97F3\u9891\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A
10141=\u53EA\u652F\u6301\u97F3\u9891\u6587\u4EF6
10142=\u97F3\u9891\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC710MB
10143=\u4E0A\u4F20\u5931\u8D25
10144=\u58F0\u97F3\u514B\u9686\u8BB0\u5F55\u4E0D\u5B58\u5728
10145=\u97F3\u8272\u8D44\u6E90\u4FE1\u606F\u4E0D\u80FD\u4E3A\u7A7A
10146=\u5E73\u53F0\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A
10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A
10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55
10151=\u8BF7\u5148\u4E0A\u4F20\u97F3\u9891\u6587\u4EF6
10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230
10153=\u6A21\u578B\u7C7B\u578B\u672A\u627E\u5230
10154=\u8BAD\u7EC3\u5931\u8D25
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11\u914D\u7F6E
10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
10157=\u8BF7\u6C42\u5931\u8D25
10158=\u514B\u9686\u8272\u97F3:
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5S_\u5F00\u5934
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
10162=\u6A21\u578B\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
@@ -1,148 +1,172 @@
#繁體中文 #\u7E41\u9AD4\u4E2D\u6587
500=服務器內部異常 500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
401=未授權 401=\u672A\u6388\u6B0A
403=拒絕訪問,沒有权限 403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6743\u9650
10001={0}不能為空 10001={0}\u4E0D\u80FD\u70BA\u7A7A
10002=數據庫中已存在該記錄 10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
10003=獲取參數失敗 10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
10004=賬號或密碼錯誤 10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
10005=賬號已被停用 10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
10006=唯一標識不能為空 10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
10007=驗證碼不正確 10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10008=先刪除子菜單或按鈕 10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
10009=原密碼不正確 10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
10010=帳號或密碼不正確,您還有可以嘗試{0}次 10010=\u5E33\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
10011=上級部門選擇錯誤 10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
10012=上級菜單不能為自身 10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
10013=數據權限接口,只能是Map類型參數 10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
10014=請先刪除下級部門 10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
10015=請先刪除部門下的用戶 10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
10016=部署失敗,沒有流程 10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
10017=模型圖不正確,請檢查 10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
10018=導出失敗,模型ID為{0} 10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
10019=請上傳文件 10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
10020=token不能為空 10020=token\u4E0D\u80FD\u70BA\u7A7A
10021=token失效,請重新登錄 10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
10022=賬號已被鎖定 10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件 10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上傳文件失敗{0} 10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
10025=發送短信失敗{0} 10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
10026=郵件模板不存在 10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服務異常 10027=Redis\u670D\u52D9\u7570\u5E38
10028=定時任務失敗 10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
10029=不能包含非法字符 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=密碼長度不足{0}位 10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成 10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5BEB\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
10032=刪除本數據異常 10032=\u522A\u9664\u672C\u6578\u64DA\u7570\u5E38
10033=設備驗證碼錯誤 10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
10034=參數值不能為空 10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
10035=參數類型不能為空 10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
10036=不支持的參數類型 10036=\u4E0D\u652F\u6301\u7684\u53C3\u6578\u985E\u578B
10037=參數值必須是有效的數字 10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=參數值必須是true或false 10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=參數值必須是有效的JSON數組格式 10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=參數值必須是有效的JSON格式 10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=設備未找到 10041=\u8A2D\u5099\u672A\u627E\u5230
10042={0} 10042={0}
10043=刪除數據失敗 10043=\u522A\u9664\u6578\u64DA\u5931\u6557
10044=用戶未登錄 10044=\u7528\u6236\u672A\u767B\u9304
10045=WebSocket連接失敗或連接超時 10045=WebSocket\u9023\u63A5\u5931\u6557\u6216\u9023\u63A5\u8D85\u6642
10046=保存聲紋錯誤,請聯繫管理員 10046=\u4FDD\u5B58\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10047=每日發送次數已達上限 10047=\u6BCF\u65E5\u767C\u9001\u6B21\u6578\u5DF2\u9054\u4E0A\u9650
10048=舊密碼輸入錯誤 10048=\u820A\u5BC6\u78BC\u8F38\u5165\u932F\u8AA4
10049=設置的LLM不是openai和ollama 10049=\u8A2D\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token生成失敗 10050=token\u751F\u6210\u5931\u6557
10051=資源不存在 10051=\u8CC7\u6E90\u4E0D\u5B58\u5728
10052=默認智能體未找到 10052=\u9ED8\u8A8D\u667A\u80FD\u9AD4\u672A\u627E\u5230
10053=智能體未找到 10053=\u667A\u80FD\u9AD4\u672A\u627E\u5230
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print) 10054=\u8072\u7D0B\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8ACB\u5148\u5728\u53C3\u6578\u914D\u7F6E\u4E2D\u914D\u7F6E\u8072\u7D0B\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=短信發送失敗 10055=\u77ED\u4FE1\u767C\u9001\u5931\u6557
10056=短信連接建立失敗 10056=\u77ED\u4FE1\u9023\u63A5\u5EFA\u7ACB\u5931\u6557
10057=智能體的聲紋創建失敗 10057=\u667A\u80FD\u9AD4\u7684\u8072\u7D0B\u5275\u5EFA\u5931\u6557
10058=智能體的對應聲紋更新失敗 10058=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u66F4\u65B0\u5931\u6557
10059=智能體的對應聲紋刪除失敗 10059=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u522A\u9664\u5931\u6557
10060=發送太頻繁,請{0}秒後再試 10060=\u767C\u9001\u592A\u983B\u7E41\uFF0C\u8ACB{0}\u79D2\u5F8C\u518D\u8A66
10061=激活碼不能為空 10061=\u6FC0\u6D3B\u78BC\u4E0D\u80FD\u70BA\u7A7A
10062=激活碼錯誤 10062=\u6FC0\u6D3B\u78BC\u932F\u8AA4
10063=設備已激活 10063=\u8A2D\u5099\u5DF2\u6FC0\u6D3B
10064=該模型為預設模型,請先設置其他模型為預設模型 10064=\u8A72\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B\uFF0C\u8ACB\u5148\u8A2D\u7F6E\u5176\u4ED6\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B
10065=新增數據失敗 10065=\u65B0\u589E\u6578\u64DA\u5931\u6557
10066=更新數據失敗 10066=\u66F4\u65B0\u6578\u64DA\u5931\u6557
10067=圖形驗證碼錯誤 10067=\u5716\u5F62\u9A57\u8B49\u78BC\u932F\u8AA4
10068=未開啟手機註冊,無法使用短信驗證碼功能 10068=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A57\u8B49\u78BC\u529F\u80FD
10069=用戶名不是手機號碼,請重新輸入 10069=\u7528\u6236\u540D\u4E0D\u662F\u624B\u6A5F\u865F\u78BC\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165
10070=該手機號碼已註冊 10070=\u8A72\u624B\u6A5F\u865F\u78BC\u5DF2\u8A3B\u518A
10071=輸入的手機號碼未註冊 10071=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u672A\u8A3B\u518A
10072=目前不允許普通用戶註冊 10072=\u76EE\u524D\u4E0D\u5141\u8A31\u666E\u901A\u7528\u6236\u8A3B\u518A
10073=未開啟手機註冊,無法使用密碼找回功能 10073=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u5BC6\u78BC\u627E\u56DE\u529F\u80FD
10074=輸入的手機號碼格式不正確 10074=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u683C\u5F0F\u4E0D\u6B63\u78BA
10075=輸入的短信驗證碼不正確 10075=\u8F38\u5165\u7684\u77ED\u4FE1\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10076=字典類型不存在 10076=\u5B57\u5178\u985E\u578B\u4E0D\u5B58\u5728
10077=字典類型編碼重複 10077=\u5B57\u5178\u985E\u578B\u7DE8\u78BC\u91CD\u8907
10078=讀取資源失敗 10078=\u8B80\u53D6\u8CC7\u6E90\u5931\u6557
10079=LLM模型與意圖識別、參數選擇不匹配 10079=LLM\u6A21\u578B\u8207\u610F\u5716\u8B58\u5225\u3001\u53C3\u6578\u9078\u64C7\u4E0D\u5339\u914D
10080=此聲紋屬於已註冊的{0},請選擇其他聲音 10080=\u6B64\u8072\u7D0B\u5C6C\u65BC\u5DF2\u8A3B\u518A\u7684{0}\uFF0C\u8ACB\u9078\u64C7\u5176\u4ED6\u8072\u97F3
10081=刪除聲紋時發生錯誤 10081=\u522A\u9664\u8072\u7D0B\u6642\u767C\u751F\u932F\u8AA4
10082=不允許修改,此聲音已註冊為聲紋({0}) 10082=\u4E0D\u5141\u8A31\u4FEE\u6539\uFF0C\u6B64\u8072\u97F3\u5DF2\u8A3B\u518A\u70BA\u8072\u7D0B({0})
10083=修改聲紋錯誤,請聯繫管理員 10083=\u4FEE\u6539\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址 10084=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u932F\u8AA4\uFF0C\u8ACB\u524D\u5F80\u53C3\u6578\u7BA1\u7406\u4FEE\u6539\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10085=音頻數據不屬於此智能體 10085=\u97F3\u983B\u6578\u64DA\u4E0D\u5C6C\u65BC\u6B64\u667A\u80FD\u9AD4
10086=音頻數據為空,請檢查上傳數據 10086=\u97F3\u983B\u6578\u64DA\u70BA\u7A7A\uFF0C\u8ACB\u6AA2\u67E5\u4E0A\u50B3\u6578\u64DA
10087=聲紋註冊失敗,請求未成功 10087=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10088=聲紋註冊失敗,請求處理失敗 10088=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10089=聲紋註銷失敗,請求未成功 10089=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10090=聲紋註銷失敗,請求處理失敗 10090=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10091=模型提供商不存在 10091=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
10092=配置的LLM不存在 10092=\u914D\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=此模型配置被智能體{0}引用,無法刪除 10093=\u6B64\u6A21\u578B\u914D\u7F6E\u88AB\u667A\u80FD\u9AD4{0}\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10094=此LLM模型被意圖識別配置引用,無法刪除 10094=\u6B64LLM\u6A21\u578B\u88AB\u610F\u5716\u8B58\u5225\u914D\u7F6E\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10095=無效的服務器操作 10095=\u7121\u6548\u7684\u670D\u52D9\u5668\u64CD\u4F5C
10096=未配置服務器WebSocket地址 10096=\u672A\u914D\u7F6E\u670D\u52D9\u5668WebSocket\u5730\u5740
10097=目標WebSocket地址不存在 10097=\u76EE\u6A19WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket地址列表不能為空 10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u70BA\u7A7A
10099=WebSocket地址不能使用localhost127.0.0.1 10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket地址格式不正確 10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA
10101=WebSocket連接測試失敗 10101=WebSocket\u9023\u63A5\u6E2C\u8A66\u5931\u6557
10102=OTA地址不能為空 10102=OTA\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10103=OTA地址不能使用localhost127.0.0.1 10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA地址必須以http或https開頭 10104=OTA\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10105=OTA地址必須以/ota/結尾 10105=OTA\u5730\u5740\u5FC5\u9808\u4EE5/ota/\u7D50\u5C3E
10106=OTA接口訪問失敗 10106=OTA\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10107=OTA接口返回內容格式不正確 10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10108=OTA接口驗證失敗 10108=OTA\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10109=MCP地址不能為空 10109=MCP\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10110=MCP地址不能使用localhost127.0.0.1 10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=不是有效的MCP地址 10111=\u4E0D\u662F\u6709\u6548\u7684MCP\u5730\u5740
10112=MCP接口訪問失敗 10112=MCP\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10113=MCP接口返回內容格式不正確 10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10114=MCP接口驗證失敗 10114=MCP\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10115=聲紋接口地址不能為空 10115=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10116=聲紋接口地址不能使用localhost127.0.0.1 10116=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=不是有效的聲紋接口地址 10117=\u4E0D\u662F\u6709\u6548\u7684\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10118=聲紋接口地址必須以http或https開頭 10118=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10119=聲紋接口訪問失敗 10119=\u8072\u7D0B\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10120=聲紋接口返回內容格式不正確 10120=\u8072\u7D0B\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10121=聲紋接口驗證失敗 10121=\u8072\u7D0B\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10122=MQTT密鑰不能為空 10122=MQTT\u5BC6\u9470\u4E0D\u80FD\u70BA\u7A7A
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母 10123=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0C\u9700\u81F3\u5C118\u500B\u5B57\u7B26\u4E14\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母 10124=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0CMQTT\u5BC6\u9470\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10125=您的MQTT密鑰包含弱密碼 10125=\u60A8\u7684MQTT\u5BC6\u9470\u5305\u542B\u5F31\u5BC6\u78BC
10128=字典標籤重複 10128=\u5B57\u5178\u6A19\u7C64\u91CD\u8907
10129=SM2密鑰未配置 10129=SM2\u5BC6\u9470\u672A\u914D\u7F6E
10130=SM2解密失敗 10130=SM2\u89E3\u5BC6\u5931\u6557
10131=modelTypeprovideCode不能為空 10131=modelType\u548CprovideCode\u4E0D\u80FD\u70BA\u7A7A
10132=沒有權限查看該智能體的聊天記錄 10132=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684\u804A\u5929\u8A18\u9304
10133=會話ID不能為空 10133=\u6703\u8A71ID\u4E0D\u80FD\u70BA\u7A7A
10134=智能體ID不能為空 10134=\u667A\u80FD\u9AD4ID\u4E0D\u80FD\u70BA\u7A7A
10135=聊天記錄下載失敗 10135=\u804A\u5929\u8A18\u9304\u4E0B\u8F09\u5931\u6557
10136=下載鏈接已過期或無效 10136=\u4E0B\u8F09\u93C8\u63A5\u5DF2\u904E\u671F\u6216\u7121\u6548
10137=下載鏈接無效 10137=\u4E0B\u8F09\u93C8\u63A5\u7121\u6548
10138=用戶 10138=\u7528\u6236
10139=智體 10139=\u667A\u9AD4
10140=\u97F3\u983B\u6587\u4EF6\u4E0D\u80FD\u70BA\u7A7A
10141=\u53EA\u652F\u6301\u97F3\u983B\u6587\u4EF6
10142=\u97F3\u983B\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u904E10MB
10143=\u4E0A\u50B3\u5931\u6557
10144=\u8072\u97F3\u514B\u9686\u8A18\u9304\u4E0D\u5B58\u5728
10145=\u97F3\u8272\u8CC7\u6E90\u4FE1\u606F\u4E0D\u80FD\u70BA\u7A7A
10146=\u5E73\u53F0\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
10147=\u97F3\u8272ID\u4E0D\u80FD\u70BA\u7A7A
10148=\u6B78\u5C6C\u5E33\u865F\u4E0D\u80FD\u70BA\u7A7A
10149=\u522A\u9664\u7684\u97F3\u8272\u8CC7\u6E90ID\u4E0D\u80FD\u70BA\u7A7A
10150=\u60A8\u6C92\u6709\u6B0A\u9650\u64CD\u4F5C\u8A72\u8A18\u9304
10151=\u8ACB\u5148\u4E0A\u50B3\u97F3\u983B\u6587\u4EF6
10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230
10153=\u6A21\u578B\u985E\u578B\u672A\u627E\u5230
10154=\u6A21\u578B\u8AB2\u7DF4\u5931\u6557: {0}
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11appid\u6216access_token
10156=\u97FF\u61C9\u683C\u5F0F\u932F\u8AA4\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
10157=\u8ACB\u6C42\u5931\u6557
10158=\u514B\u9686\u8A9E\u97F3:
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u932F\u8AA4\uFF0C\u5FC5\u9808\u4EE5S_\u958B\u982D
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
10162=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
@@ -23,4 +23,11 @@
select model_name from ai_model_config where model_type = #{modelType} select model_name from ai_model_config where model_type = #{modelType}
<if test="modelName != null">and model_name = #{modelName}</if> <if test="modelName != null">and model_name = #{modelName}</if>
</select> </select>
<!-- 获取符合条件的TTS平台列表 -->
<select id="getTtsPlatformList" resultType="map">
select id, model_name as modelName from ai_model_config
where model_type = 'TTS'
and JSON_EXTRACT(config_json, '$.type') = 'huoshan_double_stream'
</select>
</mapper> </mapper>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.voiceclone.dao.VoiceCloneDao">
<select id="getTrainSuccess" resultType="xiaozhi.modules.model.dto.VoiceDTO">
select id, name
from ai_voice_clone
where model_id = #{modelId} and user_id = #{userId} and train_status = 2
</select>
</mapper>
+18
View File
@@ -123,3 +123,21 @@ export function register(data: RegisterData) {
}, },
}) })
} }
// 忘记密码数据类型
export interface ForgotPasswordData {
phone: string
code: string
password: string
captchaId: string
}
// 忘记密码(找回密码)
export function retrievePassword(data: ForgotPasswordData) {
return http.Put('/user/retrieve-password', data, {
meta: {
ignoreAuth: true,
toast: true,
},
})
}
+24 -3
View File
@@ -19,8 +19,7 @@ export default {
'login.enterCaptcha': 'Please enter verification code', 'login.enterCaptcha': 'Please enter verification code',
'login.loginButton': 'Login', 'login.loginButton': 'Login',
'login.loggingIn': 'Logging in...', 'login.loggingIn': 'Logging in...',
'login.noAccount': 'did not have an account?', 'login.noAccount': 'Sign Up',
'login.registerNow': 'Register Now',
'login.enterPhone': 'Please enter phone number', 'login.enterPhone': 'Please enter phone number',
'login.selectCountry': 'Select Country/Region', 'login.selectCountry': 'Select Country/Region',
'login.confirm': 'Confirm', 'login.confirm': 'Confirm',
@@ -30,6 +29,7 @@ export default {
'login.requiredCaptcha': 'Verification code cannot be empty', 'login.requiredCaptcha': 'Verification code cannot be empty',
'login.requiredMobile': 'Please enter a valid phone number', 'login.requiredMobile': 'Please enter a valid phone number',
'login.captchaError': 'Graphic verification code error', 'login.captchaError': 'Graphic verification code error',
'login.forgotPassword': 'Forgot Password',
// Register page // Register page
'register.pageTitle': 'Register', 'register.pageTitle': 'Register',
@@ -189,6 +189,27 @@ export default {
'common.unknownError': 'Unknown error', 'common.unknownError': 'Unknown error',
'common.networkError': 'Network error', 'common.networkError': 'Network error',
// Retrieve Password page
'retrievePassword.title': 'Reset Password',
'retrievePassword.subtitle': 'Recover your account password via mobile number',
'retrievePassword.mobileRequired': 'Please enter mobile number',
'retrievePassword.inputCorrectMobile': 'Please enter a valid mobile number',
'retrievePassword.captchaRequired': 'Please enter graphic verification code',
'retrievePassword.mobileCaptchaRequired': 'Please enter SMS verification code',
'retrievePassword.newPasswordRequired': 'Please enter new password',
'retrievePassword.confirmNewPasswordRequired': 'Please confirm new password',
'retrievePassword.passwordsNotMatch': 'Passwords do not match',
'retrievePassword.mobilePlaceholder': 'Please enter mobile number',
'retrievePassword.captchaPlaceholder': 'Please enter graphic verification code',
'retrievePassword.mobileCaptchaPlaceholder': 'Please enter SMS verification code',
'retrievePassword.newPasswordPlaceholder': 'Please enter new password',
'retrievePassword.confirmNewPasswordPlaceholder': 'Please confirm new password',
'retrievePassword.getMobileCaptcha': 'Get Code',
'retrievePassword.captchaSendSuccess': 'Verification code sent successfully',
'retrievePassword.passwordUpdateSuccess': 'Password reset successfully',
'retrievePassword.resetButton': 'Reset Password',
'retrievePassword.goToLogin': 'Back to Login',
// SM2 encryption related error messages // SM2 encryption related error messages
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator', 'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
'sm2.encryptionFailed': 'Password encryption failed', 'sm2.encryptionFailed': 'Password encryption failed',
@@ -240,7 +261,7 @@ export default {
'settings.restartLater': 'Later', 'settings.restartLater': 'Later',
// About us // About us
'settings.aboutApp': 'About XiaoZhi Console', 'settings.aboutApp': 'About XiaoZhi Console',
'settings.aboutContent': 'XiaoZhi Console\n\nA cross-platform mobile management app built with Vue.js 3 + uni-app, providing device management, agent configuration and other functions for xiaozhi ESP32 smart hardware.\n\n© 2025 xiaozhi-esp32-server 0.8.3', 'settings.aboutContent': 'XiaoZhi Console\n\nA cross-platform mobile management app built with Vue.js 3 + uni-app, providing device management, agent configuration and other functions for xiaozhi ESP32 smart hardware.\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': 'Saved, you can manually restart the app later', 'settings.restartSuccess': 'Saved, you can manually restart the app later',
'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared', 'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared',
'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared', 'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared',
+23 -2
View File
@@ -19,8 +19,7 @@ export default {
'login.enterCaptcha': '请输入验证码', 'login.enterCaptcha': '请输入验证码',
'login.loginButton': '登录', 'login.loginButton': '登录',
'login.loggingIn': '登录中...', 'login.loggingIn': '登录中...',
'login.noAccount': '还没有账户?', 'login.noAccount': '新用户注册',
'login.registerNow': '立即注册',
'login.enterPhone': '请输入手机号码', 'login.enterPhone': '请输入手机号码',
'login.selectCountry': '选择国家/地区', 'login.selectCountry': '选择国家/地区',
'login.confirm': '确认', 'login.confirm': '确认',
@@ -30,6 +29,7 @@ export default {
'login.requiredCaptcha': '验证码不能为空', 'login.requiredCaptcha': '验证码不能为空',
'login.requiredMobile': '请输入正确的手机号码', 'login.requiredMobile': '请输入正确的手机号码',
'login.captchaError': '图形验证码错误', 'login.captchaError': '图形验证码错误',
'login.forgotPassword': '忘记密码',
// 注册页面 // 注册页面
'register.pageTitle': '注册', 'register.pageTitle': '注册',
@@ -265,6 +265,27 @@ export default {
'settings.selectLanguage': '选择语言', 'settings.selectLanguage': '选择语言',
'settings.languageChanged': '语言切换成功', 'settings.languageChanged': '语言切换成功',
// 重置密码页面
'retrievePassword.title': '重置密码',
'retrievePassword.subtitle': '通过手机号找回您的账户密码',
'retrievePassword.mobileRequired': '请输入手机号码',
'retrievePassword.inputCorrectMobile': '请输入正确的手机号码',
'retrievePassword.captchaRequired': '请输入图形验证码',
'retrievePassword.mobileCaptchaRequired': '请输入短信验证码',
'retrievePassword.newPasswordRequired': '请输入新密码',
'retrievePassword.confirmNewPasswordRequired': '请确认新密码',
'retrievePassword.passwordsNotMatch': '两次输入的密码不一致',
'retrievePassword.mobilePlaceholder': '请输入手机号码',
'retrievePassword.captchaPlaceholder': '请输入图形验证码',
'retrievePassword.mobileCaptchaPlaceholder': '请输入短信验证码',
'retrievePassword.newPasswordPlaceholder': '请输入新密码',
'retrievePassword.confirmNewPasswordPlaceholder': '请确认新密码',
'retrievePassword.getMobileCaptcha': '获取验证码',
'retrievePassword.captchaSendSuccess': '验证码发送成功',
'retrievePassword.passwordUpdateSuccess': '密码重置成功',
'retrievePassword.resetButton': '重置密码',
'retrievePassword.goToLogin': '返回登录',
// 消息提示 // 消息提示
'message.loginSuccess': '登录成功!', 'message.loginSuccess': '登录成功!',
'message.loginFail': '登录失败', 'message.loginFail': '登录失败',
+28 -3
View File
@@ -19,8 +19,7 @@ export default {
'login.enterCaptcha': '請輸入驗證碼', 'login.enterCaptcha': '請輸入驗證碼',
'login.loginButton': '登錄', 'login.loginButton': '登錄',
'login.loggingIn': '登錄中...', 'login.loggingIn': '登錄中...',
'login.noAccount': '還沒有賬戶?', 'login.noAccount': '新用戶註冊',
'login.registerNow': '立即註冊',
'login.enterPhone': '請輸入手機號碼', 'login.enterPhone': '請輸入手機號碼',
'login.selectCountry': '選擇國家/地區', 'login.selectCountry': '選擇國家/地區',
'login.confirm': '確認', 'login.confirm': '確認',
@@ -30,6 +29,32 @@ export default {
'login.requiredCaptcha': '驗證碼不能為空', 'login.requiredCaptcha': '驗證碼不能為空',
'login.requiredMobile': '請輸入正確的手機號碼', 'login.requiredMobile': '請輸入正確的手機號碼',
'login.captchaError': '圖形驗證碼錯誤', 'login.captchaError': '圖形驗證碼錯誤',
'login.forgotPassword': '忘記密碼',
// 忘記密碼頁面
'retrievePassword.title': '重置密碼',
'retrievePassword.subtitle': '請輸入您的手機號碼以重置密碼',
'retrievePassword.mobileRequired': '請輸入手機號碼',
'retrievePassword.inputCorrectMobile': '請輸入正確的手機號碼',
'retrievePassword.captchaRequired': '請輸入圖形驗證碼',
'retrievePassword.mobileCaptchaRequired': '請輸入手機驗證碼',
'retrievePassword.newPasswordRequired': '請輸入新密碼',
'retrievePassword.confirmNewPasswordRequired': '請確認新密碼',
'retrievePassword.passwordsNotMatch': '兩次輸入的密碼不一致',
'retrievePassword.mobilePlaceholder': '請輸入手機號碼',
'retrievePassword.captchaPlaceholder': '請輸入圖形驗證碼',
'retrievePassword.mobileCaptchaPlaceholder': '請輸入手機驗證碼',
'retrievePassword.newPasswordPlaceholder': '請輸入新密碼',
'retrievePassword.confirmNewPasswordPlaceholder': '請確認新密碼',
'retrievePassword.confirmPasswordPlaceholder': '請確認新密碼',
'retrievePassword.captchaSendSuccess': '驗證碼發送成功',
'retrievePassword.passwordUpdateSuccess': '密碼更新成功',
'retrievePassword.getCaptcha': '獲取驗證碼',
'retrievePassword.getMobileCaptcha': '獲取驗證碼',
'retrievePassword.resend': '重新發送',
'retrievePassword.resetPasswordButton': '重置密碼',
'retrievePassword.resetButton': '重置密碼',
'retrievePassword.goToLogin': '返回登錄',
// 註冊頁面 // 註冊頁面
'register.pageTitle': '註冊', 'register.pageTitle': '註冊',
@@ -240,7 +265,7 @@ export default {
'settings.restartLater': '稍後', 'settings.restartLater': '稍後',
// 關於我們 // 關於我們
'settings.aboutApp': '關於小智智控台', 'settings.aboutApp': '關於小智智控台',
'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.3', 'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': '已保存,可稍後手動重啟應用', 'settings.restartSuccess': '已保存,可稍後手動重啟應用',
'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除', 'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除',
'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除', 'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除',
+9
View File
@@ -108,6 +108,15 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "pages/forgot-password/index",
"type": "page",
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "ForgotPassword"
}
},
{ {
"path": "pages/login/index", "path": "pages/login/index",
"type": "page", "type": "page",
@@ -0,0 +1,848 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "ForgotPassword"
}
}
</route>
<script lang="ts" setup>
import { computed, onMounted, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useConfigStore } from "@/store";
import { getEnvBaseUrl, sm2Encrypt } from "@/utils";
import { toast } from "@/utils/toast";
// 导入国际化相关功能
import { t, initI18n } from "@/i18n";
// 导入API接口
import { retrievePassword, sendSmsCode } from "@/api/auth";
// 获取屏幕边界到安全区域距离
let safeAreaInsets;
let systemInfo;
// #ifdef MP-WEIXIN
// 微信小程序使用新的API
systemInfo = uni.getWindowInfo();
safeAreaInsets = systemInfo.safeArea
? {
top: systemInfo.safeArea.top,
right: systemInfo.windowWidth - systemInfo.safeArea.right,
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
left: systemInfo.safeArea.left,
}
: null;
// #endif
// #ifndef MP-WEIXIN
// 其他平台继续使用uni API
systemInfo = uni.getSystemInfoSync();
safeAreaInsets = systemInfo.safeAreaInsets;
// #endif
// 表单数据
interface ForgotPasswordData {
mobile: string;
captcha: string;
captchaId: string;
mobileCaptcha: string;
newPassword: string;
confirmPassword: string;
areaCode: string;
}
const formData = ref<ForgotPasswordData>({
mobile: "",
captcha: "",
captchaId: "",
mobileCaptcha: "",
newPassword: "",
confirmPassword: "",
areaCode: "+86",
});
// 验证码图片
const captchaImage = ref("");
const loading = ref(false);
// 获取配置store
const configStore = useConfigStore();
// State for area code action sheet
const showAreaCodeSheet = ref(false);
const selectedAreaCode = ref("+86");
const areaCodeList = computed(() =>
(configStore.config.mobileAreaList || []).map((item) => {
return {
value: item.key,
label: `${item.name} (${item.key})`,
};
})
);
const canSendMobileCaptcha = computed(() => {
const mobile = formData.value.mobile;
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(mobile) && smsCountdown.value === 0;
});
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey;
});
// 短信验证码倒计时
const smsCountdown = ref(0);
const smsLoading = ref(false);
// 打开区号选择弹窗
function openAreaCodeSheet() {
showAreaCodeSheet.value = true;
}
// 选择区号
function selectAreaCode(item: { value: string; label: string }) {
selectedAreaCode.value = item.value;
formData.value.areaCode = item.value;
showAreaCodeSheet.value = false;
}
// 关闭区号选择弹窗
function closeAreaCodeSheet() {
showAreaCodeSheet.value = false;
}
// 生成UUID
function generateUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// 获取图形验证码
async function refreshCaptcha() {
const uuid = generateUUID();
formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
}
// 发送短信验证码
async function handleSendSmsCode() {
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning(t("retrievePassword.inputCorrectMobile"));
return;
}
if (!formData.value.captcha) {
toast.warning(t("retrievePassword.captchaRequired"));
return;
}
try {
smsLoading.value = true;
// 将手机号转换为国际格式
const internationalPhone = formData.value.areaCode + formData.value.mobile;
// 调用发送短信验证码API
await sendSmsCode({
phone: internationalPhone,
captcha: formData.value.captcha,
captchaId: formData.value.captchaId
})
toast.success(t("retrievePassword.captchaSendSuccess"));
// 开始倒计时
smsCountdown.value = 60;
const timer = setInterval(() => {
smsCountdown.value--;
if (smsCountdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
} catch (error: any) {
// 处理验证码错误
if (error.message.includes("请求错误[10067]")) {
toast.warning(t("login.captchaError"));
}
// 发送失败重新获取图形验证码
refreshCaptcha();
} finally {
smsLoading.value = false;
}
}
// 重置密码
async function handleResetPassword() {
// 表单验证
if (!formData.value.mobile) {
toast.warning(t("retrievePassword.mobileRequired"));
return;
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning(t("retrievePassword.inputCorrectMobile"));
return;
}
// 将手机号转换为国际格式
const internationalPhone = formData.value.areaCode + formData.value.mobile;
if (!formData.value.captcha) {
toast.warning(t("retrievePassword.captchaRequired"));
return;
}
if (!formData.value.mobileCaptcha) {
toast.warning(t("retrievePassword.mobileCaptchaRequired"));
return;
}
if (!formData.value.newPassword) {
toast.warning(t("retrievePassword.newPasswordRequired"));
return;
}
if (!formData.value.confirmPassword) {
toast.warning(t("retrievePassword.confirmNewPasswordRequired"));
return;
}
if (formData.value.newPassword !== formData.value.confirmPassword) {
toast.warning(t("retrievePassword.passwordsNotMatch"));
return;
}
try {
loading.value = true;
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
toast.warning(t("sm2.publicKeyNotConfigured"));
return;
}
// 加密密码
let encryptedPassword;
try {
// 拼接图形验证码和新密码进行加密
const captchaAndPassword = formData.value.captcha + formData.value.newPassword;
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword);
} catch (error) {
console.error("密码加密失败:", error);
toast.warning(t("sm2.encryptionFailed"));
return;
}
// 调用重置密码API
await retrievePassword({
phone: internationalPhone,
code: formData.value.mobileCaptcha,
password: encryptedPassword,
captchaId: formData.value.captchaId
})
toast.success(t("retrievePassword.passwordUpdateSuccess"));
// 跳转到登录页
setTimeout(() => {
uni.redirectTo({
url: "/pages/login/index",
});
}, 1000);
} catch (error: any) {
// 处理验证码错误
if (error.message.includes("请求错误[10067]")) {
toast.warning(t("login.captchaError"));
}
// 重置失败重新获取验证码
refreshCaptcha();
} finally {
loading.value = false;
}
}
// 返回登录
function goBack() {
uni.redirectTo({
url: "/pages/login/index",
});
}
// 页面加载时获取验证码
onLoad(() => {
refreshCaptcha();
});
// 组件挂载时确保配置已加载
onMounted(async () => {
if (!configStore.config.name) {
try {
await configStore.fetchPublicConfig();
} catch (error) {
console.error("获取配置失败:", error);
}
}
// 初始化国际化
initI18n();
});
</script>
<template>
<view
class="app-container box-border h-screen w-full"
:style="{ paddingTop: `${safeAreaInsets?.top}px` }"
>
<view class="header">
<view class="back-button" @click="goBack">
<wd-icon name="arrow-left" custom-class="back-icon" />
</view>
<view class="logo-section">
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
<text class="welcome-text">
{{ t("retrievePassword.title") }}
</text>
<text class="subtitle">
{{ t("retrievePassword.subtitle") }}
</text>
</view>
</view>
<view class="form-container">
<view class="form">
<!-- 手机号输入 -->
<view class="input-group">
<view class="input-wrapper mobile-wrapper">
<view class="area-code-selector" @click="openAreaCodeSheet">
<text class="area-code-text">
{{ selectedAreaCode }}
</text>
<wd-icon name="arrow-down" custom-class="area-code-arrow" />
</view>
<view class="mobile-input-wrapper">
<wd-input
v-model="formData.mobile"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.mobilePlaceholder')"
type="number"
:maxlength="11"
/>
</view>
</view>
</view>
<!-- 图形验证码 -->
<view class="input-group">
<view class="input-wrapper captcha-wrapper">
<wd-input
v-model="formData.captcha"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.captchaPlaceholder')"
:maxlength="6"
/>
<view class="captcha-image" @click="refreshCaptcha">
<image :src="captchaImage" class="captcha-img" />
</view>
</view>
</view>
<!-- 短信验证码 -->
<view class="input-group">
<view class="input-wrapper sms-wrapper">
<wd-input
v-model="formData.mobileCaptcha"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.mobileCaptchaPlaceholder')"
:maxlength="6"
/>
<view class="sms-button" @click="handleSendSmsCode">
<text class="sms-text">
{{
smsCountdown > 0
? `${smsCountdown}s`
: t("retrievePassword.getMobileCaptcha")
}}
</text>
</view>
</view>
</view>
<!-- 新密码 -->
<view class="input-group">
<view class="input-wrapper">
<wd-input
v-model="formData.newPassword"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.newPasswordPlaceholder')"
show-password
:maxlength="20"
/>
</view>
</view>
<!-- 确认新密码 -->
<view class="input-group">
<view class="input-wrapper">
<wd-input
v-model="formData.confirmPassword"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.confirmNewPasswordPlaceholder')"
show-password
:maxlength="20"
/>
</view>
</view>
<!-- 重置密码按钮 -->
<view class="reset-btn" @click="handleResetPassword">
{{ loading ? t("common.loading") : t("retrievePassword.resetButton") }}
</view>
<!-- 返回登录链接 -->
<view class="back-login-hint" @click="goBack">
<text class="hint-text">
{{ t("retrievePassword.goToLogin") }}
</text>
</view>
</view>
</view>
<!-- 区号选择弹窗 -->
<wd-action-sheet
v-model="showAreaCodeSheet"
:title="t('login.selectCountry')"
:close-on-click-modal="true"
@close="closeAreaCodeSheet"
>
<view class="area-code-sheet">
<scroll-view scroll-y class="area-code-list">
<view
v-for="item in areaCodeList"
:key="item.value"
class="area-code-item"
:class="{ selected: selectedAreaCode === item.value }"
@click="selectAreaCode(item)"
>
<view class="area-info">
<text class="area-name">
{{ item.label }}
</text>
</view>
<wd-icon
v-if="selectedAreaCode === item.value"
name="check"
custom-class="check-icon"
/>
</view>
</scroll-view>
<view class="sheet-footer">
<wd-button
type="primary"
custom-class="confirm-btn"
@click="closeAreaCodeSheet"
>
{{ t("login.confirm") }}
</wd-button>
</view>
</view>
</wd-action-sheet>
</view>
</template>
<style lang="scss" scoped>
.app-container {
background: linear-gradient(145deg, #f5f8fd, #6baaff, #9ebbfc, #f5f8fd);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
min-height: 100vh;
&::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
animation: float 6s ease-in-out infinite;
}
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
}
.header {
flex: 0 0 auto;
min-height: 280rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 15% 0 40rpx 0;
position: relative;
.back-button {
position: absolute;
left: 30rpx;
top: 50rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.back-icon {
font-size: 32rpx;
color: #ffffff;
}
}
.logo-section {
text-align: center;
.logo {
margin-bottom: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}
.welcome-text {
display: block;
color: #ffffff;
font-size: 40rpx;
font-weight: 600;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.subtitle {
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 26rpx;
font-weight: 400;
}
}
}
.form-container {
padding: 0 40rpx 40rpx 40rpx;
flex: 1;
display: flex;
align-items: flex-start;
.form {
width: 100%;
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 40rpx 30rpx 30rpx 30rpx;
backdrop-filter: blur(10rpx);
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.2);
max-height: calc(100vh - 350rpx);
overflow-y: auto;
.input-group {
margin-bottom: 24rpx;
.input-wrapper {
position: relative;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
transition: all 0.3s ease;
display: flex;
align-items: center;
&:focus-within {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
}
&.mobile-wrapper {
padding: 0;
background: transparent;
border: none;
display: flex;
gap: 20rpx;
.area-code-selector {
flex: 0 0 160rpx;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
height: 45rpx;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
}
.area-code-text {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
:deep(.area-code-arrow) {
font-size: 24rpx;
color: #999999;
transition: transform 0.3s ease;
}
}
.mobile-input-wrapper {
flex: 1;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
transition: all 0.3s ease;
&:focus-within {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
}
}
}
&.captcha-wrapper {
.captcha-image {
margin-left: 20rpx;
width: 150rpx;
height: 60rpx;
border-radius: 8rpx;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: #e9ecef;
border: 1rpx solid #ddd;
.captcha-img {
width: 100%;
height: 100%;
}
}
}
&.sms-wrapper {
.sms-button {
margin-left: 20rpx;
width: 180rpx;
height: 60rpx;
border-radius: 12rpx;
background: #667eea;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
background: #5a6fd8;
transform: scale(0.98);
}
.sms-text {
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
}
}
:deep(.styled-input) {
background: transparent !important;
border: none !important;
outline: none !important;
font-size: 32rpx;
color: #333333;
flex: 1;
&::placeholder {
color: #999999;
font-size: 28rpx;
}
}
}
}
.reset-btn {
width: 100%;
height: 80rpx;
border: none;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
background-color: var(
--wot-button-primary-bg-color,
var(--wot-color-theme, #4d80f0)
);
text-align: center;
line-height: 80rpx;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.back-login-hint {
text-align: center;
.hint-text {
color: #667eea;
font-size: 26rpx;
font-weight: 500;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}
}
// 区号选择弹窗样式
.area-code-sheet {
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
.sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 40rpx 20rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
.sheet-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
}
:deep(.close-icon) {
font-size: 32rpx;
color: #999999;
cursor: pointer;
padding: 10rpx;
&:hover {
color: #333333;
}
}
}
.area-code-list {
max-height: 60vh;
padding: 0 40rpx;
.area-code-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 1rpx solid #f8f9fa;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #f8f9fa;
}
&.selected {
background-color: rgba(102, 126, 234, 0.05);
.area-name {
color: #667eea;
font-weight: 500;
}
.area-code {
color: #667eea;
}
}
.area-info {
display: flex;
flex-direction: column;
gap: 8rpx;
.area-name {
font-size: 32rpx;
color: #333333;
}
.area-code {
font-size: 28rpx;
color: #666666;
}
}
:deep(.check-icon) {
font-size: 32rpx;
color: #667eea;
}
}
}
.sheet-footer {
padding: 30rpx 40rpx 40rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
:deep(.confirm-btn) {
width: 100%;
height: 88rpx;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
}
}
}
</style>
+36 -28
View File
@@ -9,15 +9,15 @@
</route> </route>
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginData } from '@/api/auth' import type { LoginData } from '@/api/auth';
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue';
import { login } from '@/api/auth' import { login } from '@/api/auth';
import { useConfigStore } from '@/store' import { useConfigStore } from '@/store';
import { getEnvBaseUrl } from '@/utils' import { getEnvBaseUrl } from '@/utils';
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast';
// 导入国际化相关功能 // 导入国际化相关功能
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n' import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n';
import type { Language } from '@/store/lang' import type { Language } from '@/store/lang';
// 导入SM2加密工具 // 导入SM2加密工具
import { sm2Encrypt } from '@/utils' import { sm2Encrypt } from '@/utils'
@@ -116,6 +116,13 @@ function goToRegister() {
}) })
} }
// 跳转到忘记密码页面
function goToForgotPassword() {
uni.navigateTo({
url: '/pages/forgot-password/index',
})
}
// 生成UUID // 生成UUID
function generateUUID() { function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
@@ -136,9 +143,9 @@ function goToServerSetting() {
// 获取验证码 // 获取验证码
async function refreshCaptcha() { async function refreshCaptcha() {
const uuid = generateUUID() const uuid = generateUUID();
formData.value.captchaId = uuid formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}` captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
} }
// 登录 // 登录
@@ -369,13 +376,18 @@ onMounted(async () => {
{{ loading ? t('login.loggingIn') : t('login.loginButton') }} {{ loading ? t('login.loggingIn') : t('login.loginButton') }}
</view> </view>
<view class="register-hint"> <view class="hint-row">
<text class="hint-text"> <view class="register-hint">
{{ t('login.noAccount') }} <text class="register-link" @click="goToRegister">
</text> {{ t('login.noAccount') }}
<text class="register-link" @click="goToRegister"> </text>
{{ t('login.registerNow') }} </view>
</text>
<view class="forgot-password">
<text class="forgot-text" @click="goToForgotPassword">
{{ t('login.forgotPassword') }}
</text>
</view>
</view> </view>
<!-- 登录方式切换 --> <!-- 登录方式切换 -->
@@ -665,10 +677,14 @@ onMounted(async () => {
} }
} }
.forgot-password { .hint-row {
text-align: right; display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx; margin-bottom: 30rpx;
}
.forgot-password {
.forgot-text { .forgot-text {
color: #667eea; color: #667eea;
font-size: 26rpx; font-size: 26rpx;
@@ -706,14 +722,6 @@ onMounted(async () => {
} }
.register-hint { .register-hint {
text-align: center;
.hint-text {
color: #666666;
font-size: 26rpx;
margin-right: 8rpx;
}
.register-link { .register-link {
color: #667eea; color: #667eea;
font-size: 26rpx; font-size: 26rpx;
@@ -9,15 +9,15 @@
</route> </route>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue';
import { register, sendSmsCode } from '@/api/auth' import { register, sendSmsCode } from '@/api/auth';
import { useConfigStore } from '@/store' import { useConfigStore } from '@/store';
import { getEnvBaseUrl } from '@/utils' import { getEnvBaseUrl } from '@/utils';
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast';
// 导入国际化相关功能 // 导入国际化相关功能
import { t, initI18n } from '@/i18n' import { t, initI18n } from '@/i18n';
// 导入SM2加密工具 // 导入SM2加密工具
import { sm2Encrypt } from '@/utils' import { sm2Encrypt } from '@/utils';
// 获取屏幕边界到安全区域距离 // 获取屏幕边界到安全区域距离
let safeAreaInsets let safeAreaInsets
@@ -135,9 +135,9 @@ function generateUUID() {
// 获取验证码 // 获取验证码
async function refreshCaptcha() { async function refreshCaptcha() {
const uuid = generateUUID() const uuid = generateUUID();
formData.value.captchaId = uuid formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}` captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
} }
// 发送短信验证码 // 发送短信验证码
@@ -7,12 +7,12 @@
}</route> }</route>
<script lang="ts" setup> <script lang="ts" setup>
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, t } from '@/i18n'
import type { Language } from '@/store/lang'
import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils' import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils'
import { isMp } from '@/utils/platform' import { isMp } from '@/utils/platform'
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
import type { Language } from '@/store/lang'
defineOptions({ defineOptions({
name: 'SettingsPage', name: 'SettingsPage',
@@ -233,9 +233,9 @@ async function clearCache() {
function showAbout() { function showAbout() {
uni.showModal({ uni.showModal({
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }), title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', { content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE, appName: import.meta.env.VITE_APP_TITLE,
version: '0.8.3' version: '0.8.6'
}), }),
showCancel: false, showCancel: false,
confirmText: t('common.confirm'), confirmText: t('common.confirm'),
@@ -248,7 +248,7 @@ onMounted(async () => {
loadServerBaseUrl() loadServerBaseUrl()
} }
getCacheInfo() getCacheInfo()
// 动态设置导航栏标题为国际化文本 // 动态设置导航栏标题为国际化文本
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: t('settings.title') title: t('settings.title')
@@ -283,9 +283,9 @@ onMounted(async () => {
<view class="mb-[24rpx]"> <view class="mb-[24rpx]">
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden"> <view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200" <wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
:placeholder="t('settings.enterServerUrl')" :placeholder="t('settings.enterServerUrl')"
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center" custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" /> input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
</view> </view>
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]"> <text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
{{ urlError }} {{ urlError }}
@@ -323,11 +323,11 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"> class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.totalCacheSize') }} {{ t('settings.totalCacheSize') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.appDataSize') }} {{ t('settings.appDataSize') }}
</text> </text>
</view> </view>
<text class="text-[28rpx] text-[#65686f] font-semibold"> <text class="text-[28rpx] text-[#65686f] font-semibold">
{{ cacheInfo.storageSize }} {{ cacheInfo.storageSize }}
@@ -339,11 +339,11 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]"> class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.cacheClear') }} {{ t('settings.cacheClear') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.clearAllCache') }} {{ t('settings.clearAllCache') }}
</text> </text>
</view> </view>
<view <view
class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white" class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white"
@@ -359,8 +359,8 @@ onMounted(async () => {
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
{{ t('settings.appInfo') }} {{ t('settings.appInfo') }}
</text> </text>
</view> </view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]" <view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
@@ -370,11 +370,11 @@ onMounted(async () => {
@click="showAbout"> @click="showAbout">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.aboutUs') }} {{ t('settings.aboutUs') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.appVersion') }} {{ t('settings.appVersion') }}
</text> </text>
</view> </view>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" /> <wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view> </view>
@@ -385,24 +385,26 @@ onMounted(async () => {
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
{{ t('settings.languageSettings') }} {{ t('settings.languageSettings') }}
</text> </text>
</view> </view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]" <view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"> style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]" @click="showLanguageSheet = true"> <view
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
@click="showLanguageSheet = true">
<view> <view>
<text class="text-[32rpx] text-[#232338] font-medium"> <text class="text-[32rpx] text-[#232338] font-medium">
{{ t('settings.language') }} {{ t('settings.language') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.selectLanguage') }} {{ t('settings.selectLanguage') }}
</text> </text>
</view> </view>
<view class="flex items-center"> <view class="flex items-center">
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]"> <text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }} {{supportedLanguages.find(lang => lang.code === currentLanguage)?.name}}
</text> </text>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" /> <wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view> </view>
@@ -411,19 +413,11 @@ onMounted(async () => {
</view> </view>
<!-- 语言选择弹窗 --> <!-- 语言选择弹窗 -->
<wd-action-sheet <wd-action-sheet v-model="showLanguageSheet" :title="t('settings.selectLanguage')" :close-on-click-modal="true">
v-model="showLanguageSheet"
:title="t('settings.selectLanguage')"
:close-on-click-modal="true"
>
<view class="language-sheet"> <view class="language-sheet">
<scroll-view scroll-y class="language-list"> <scroll-view scroll-y class="language-list">
<view <view v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
v-for="lang in supportedLanguages" @click="handleLanguageChange(lang.code)">
:key="lang.code"
class="language-item"
@click="handleLanguageChange(lang.code)"
>
<text class="language-name"> <text class="language-name">
{{ lang.name }} {{ lang.name }}
</text> </text>
@@ -446,17 +440,21 @@ onMounted(async () => {
.language-sheet { .language-sheet {
.language-list { .language-list {
max-height: 50vh; max-height: 50vh;
.language-item { .language-item {
padding: 30rpx 0; padding: 30rpx 0;
text-align: center; text-align: center;
border-bottom: 1rpx solid #f0f0f0; border-bottom: 1rpx solid #f0f0f0;
.language-name { .language-name {
font-size: 28rpx; font-size: 28rpx;
color: #333; color: #333;
} }
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
&:active { &:active {
background-color: #f5f7fb; background-color: #f5f7fb;
} }
+1 -10
View File
@@ -10168,7 +10168,7 @@
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true, "dev": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "bin/uuid"
} }
}, },
"node_modules/source-list-map": { "node_modules/source-list-map": {
@@ -11096,15 +11096,6 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/validate-npm-package-license": { "node_modules/validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+7 -1
View File
@@ -7,6 +7,10 @@ import model from './module/model.js'
import ota from './module/ota.js' import ota from './module/ota.js'
import timbre from "./module/timbre.js" import timbre from "./module/timbre.js"
import user from './module/user.js' import user from './module/user.js'
import voiceClone from './module/voiceClone.js'
import voiceResource from './module/voiceResource.js'
/** /**
* 接口地址 * 接口地址
@@ -33,5 +37,7 @@ export default {
model, model,
timbre, timbre,
ota, ota,
dict dict,
voiceResource,
voiceClone
} }
@@ -102,21 +102,4 @@ export default {
}); });
}).send(); }).send();
}, },
// 发送设备指令
sendDeviceCommand(deviceId, mcpData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/commands/${deviceId}`)
.method('POST')
.data(mcpData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('发送设备指令失败:', err);
RequestService.reAjaxFun(() => {
this.sendDeviceCommand(deviceId, mcpData, callback);
});
}).send();
},
} }
@@ -0,0 +1,107 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 分页查询音色资源
getVoiceCloneList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone`)
.method('GET')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音色列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceCloneList(params, callback);
});
}).send();
},
// 上传音频文件
uploadVoice(formData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/upload`)
.method('POST')
.data(formData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('上传音频失败:', err);
RequestService.reAjaxFun(() => {
this.uploadVoice(formData, callback);
});
}).send();
},
// 更新音色名称
updateName(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/updateName`)
.method('POST')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('更新名称失败:', err);
RequestService.reAjaxFun(() => {
this.updateName(params, callback);
});
}).send();
},
// 获取音频下载ID
getAudioId(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/audio/${id}`)
.method('POST')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音频ID失败:', err);
RequestService.reAjaxFun(() => {
this.getAudioId(id, callback);
});
}).send();
},
// 获取音频播放URL
getPlayVoiceUrl(uuid) {
return `${getServiceUrl()}/voiceClone/play/${uuid}`;
},
// 复刻音频
cloneAudio(params, callback, errorCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/cloneAudio`)
.method('POST')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((res) => {
// 业务失败回调
RequestService.clearRequestTime();
if (errorCallback) {
errorCallback(res);
} else {
callback(res);
}
})
.networkFail((err) => {
console.error('上传失败:', err);
RequestService.reAjaxFun(() => {
this.cloneAudio(params, callback, errorCallback);
});
}).send();
}
}
@@ -0,0 +1,103 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 分页查询音色资源
getVoiceResourceList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource`)
.method('GET')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音色资源列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceResourceList(params, callback);
});
}).send();
},
// 获取单个音色资源信息
getVoiceResourceInfo(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/${id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音色资源信息失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceResourceInfo(id, callback);
});
}).send();
},
// 保存音色资源
saveVoiceResource(entity, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource`)
.method('POST')
.data(entity)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('保存音色资源失败:', err);
RequestService.reAjaxFun(() => {
this.saveVoiceResource(entity, callback);
});
}).send();
},
// 删除音色资源
deleteVoiceResource(ids, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/${ids}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('删除音色资源失败:', err);
RequestService.reAjaxFun(() => {
this.deleteVoiceResource(ids, callback);
});
}).send();
},
// 根据用户ID获取音色资源列表
getVoiceResourceByUserId(userId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/user/${userId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取用户音色资源列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceResourceByUserId(userId, callback);
});
}).send();
},
// 获取TTS平台列表
getTtsPlatformList(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/ttsPlatforms`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取TTS平台列表失败:', err);
RequestService.reAjaxFun(() => {
this.getTtsPlatformList(callback);
});
}).send();
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,11 +1,13 @@
<template> <template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen"> <el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
@open="handleOpen">
<el-form ref="form" :model="form" :rules="rules" label-width="auto"> <el-form ref="form" :model="form" :rules="rules" label-width="auto">
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName"> <el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input> <el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type"> <el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;" filterable :disabled="isTypeDisabled"> <el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;"
filterable :disabled="isTypeDisabled">
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option> <el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -14,7 +16,7 @@
</el-form-item> </el-form-item>
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath"> <el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload" <el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true" :before-upload="beforeUpload" :accept="'.bin,.apk,.wav'" :limit="1" :multiple="false" :auto-upload="true"
:on-remove="handleRemove"> :on-remove="handleRemove">
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button> <el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div> <div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
@@ -26,7 +28,8 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark"> <el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
<el-input type="textarea" v-model="form.remark" :placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input> <el-input type="textarea" v-model="form.remark"
:placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
@@ -128,13 +131,13 @@ export default {
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext)) const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
if (!isValidType) { if (!isValidType) {
this.$message.error(this.$t('firmwareDialog.invalidFileType')) this.$message.error(this.$t('firmwareDialog.invalidFileType'))
return false return false
} }
if (!isValidSize) { if (!isValidSize) {
this.$message.error(this.$t('firmwareDialog.invalidFileSize')) this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
return false return false
} }
return true return true
}, },
handleUpload(options) { handleUpload(options) {
+471 -183
View File
@@ -9,55 +9,183 @@
<!-- 中间导航菜单 --> <!-- 中间导航菜单 -->
<div class="header-center"> <div class="header-center">
<div class="equipment-management" <div
:class="{ 'active-tab': $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' }" class="equipment-management"
@click="goHome"> :class="{
<img loading="lazy" alt="" src="@/assets/header/robot.png" 'active-tab':
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" /> $route.path === '/home' ||
<span class="nav-text">{{ $t('header.smartManagement') }}</span> $route.path === '/role-config' ||
$route.path === '/device-management',
}"
@click="goHome"
>
<img
loading="lazy"
alt=""
src="@/assets/header/robot.png"
:style="{
filter:
$route.path === '/home' ||
$route.path === '/role-config' ||
$route.path === '/device-management'
? 'brightness(0) invert(1)'
: 'None',
}"
/>
<span class="nav-text">{{ $t("header.smartManagement") }}</span>
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" <!-- 普通用户显示音色克隆 -->
@click="goModelConfig"> <div
<img loading="lazy" alt="" src="@/assets/header/model_config.png" v-if="!isSuperAdmin"
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" /> class="equipment-management"
<span class="nav-text">{{ $t('header.modelConfig') }}</span> :class="{ 'active-tab': $route.path === '/voice-clone-management' }"
@click="goVoiceCloneManagement"
>
<img
loading="lazy"
alt=""
src="@/assets/header/voice.png"
:style="{
filter:
$route.path === '/voice-clone-management'
? 'brightness(0) invert(1)'
: 'None',
}"
/>
<span class="nav-text">{{ $t("header.voiceCloneManagement") }}</span>
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement"> <!-- 超级管理员显示音色克隆下拉菜单 -->
<img loading="lazy" alt="" src="@/assets/header/user_management.png" <el-dropdown
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" /> v-if="isSuperAdmin"
<span class="nav-text">{{ $t('header.userManagement') }}</span> trigger="click"
</div> class="equipment-management more-dropdown"
<div v-if="isSuperAdmin" class="equipment-management" :class="{
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement"> 'active-tab':
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png" $route.path === '/voice-clone-management' ||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" /> $route.path === '/voice-resource-management',
<span class="nav-text">{{ $t('header.otaManagement') }}</span> }"
</div> @visible-change="handleVoiceCloneDropdownVisibleChange"
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown" >
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' }"
@visible-change="handleParamDropdownVisibleChange">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/param_management.png" <img
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' ? 'brightness(0) invert(1)' : 'None' }" /> loading="lazy"
<span class="nav-text">{{ $t('header.paramDictionary') }}</span> alt=""
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i> src="@/assets/header/voice.png"
:style="{
filter:
$route.path === '/voice-clone-management' ||
$route.path === '/voice-resource-management'
? 'brightness(0) invert(1)'
: 'None',
}"
/>
<span class="nav-text">{{ $t("header.voiceCloneManagement") }}</span>
<i
class="el-icon-arrow-down el-icon--right"
:class="{ 'rotate-down': voiceCloneDropdownVisible }"
></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goVoiceCloneManagement">
{{ $t("header.voiceCloneManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goVoiceResourceManagement">
{{ $t("header.voiceResourceManagement") }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div
v-if="isSuperAdmin"
class="equipment-management"
:class="{ 'active-tab': $route.path === '/model-config' }"
@click="goModelConfig"
>
<img
loading="lazy"
alt=""
src="@/assets/header/model_config.png"
:style="{
filter:
$route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None',
}"
/>
<span class="nav-text">{{ $t("header.modelConfig") }}</span>
</div>
<div
v-if="isSuperAdmin"
class="equipment-management"
:class="{ 'active-tab': $route.path === '/user-management' }"
@click="goUserManagement"
>
<img
loading="lazy"
alt=""
src="@/assets/header/user_management.png"
:style="{
filter:
$route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None',
}"
/>
<span class="nav-text">{{ $t("header.userManagement") }}</span>
</div>
<el-dropdown
v-if="isSuperAdmin"
trigger="click"
class="equipment-management more-dropdown"
:class="{
'active-tab':
$route.path === '/dict-management' ||
$route.path === '/params-management' ||
$route.path === '/provider-management' ||
$route.path === '/server-side-management' ||
$route.path === '/agent-template-management' ||
$route.path === '/ota-management',
}"
@visible-change="handleParamDropdownVisibleChange"
>
<span class="el-dropdown-link">
<img
loading="lazy"
alt=""
src="@/assets/header/param_management.png"
:style="{
filter:
$route.path === '/dict-management' ||
$route.path === '/params-management' ||
$route.path === '/provider-management' ||
$route.path === '/server-side-management' ||
$route.path === '/agent-template-management' ||
$route.path === '/ota-management'
? 'brightness(0) invert(1)'
: 'None',
}"
/>
<span class="nav-text">{{ $t("header.paramDictionary") }}</span>
<i
class="el-icon-arrow-down el-icon--right"
:class="{ 'rotate-down': paramDropdownVisible }"
></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goOtaManagement">
{{ $t("header.otaManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goParamManagement"> <el-dropdown-item @click.native="goParamManagement">
{{ $t('header.paramManagement') }} {{ $t("header.paramManagement") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goDictManagement"> <el-dropdown-item @click.native="goDictManagement">
{{ $t('header.dictManagement') }} {{ $t("header.dictManagement") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goProviderManagement"> <el-dropdown-item @click.native="goProviderManagement">
{{ $t('header.providerManagement') }} {{ $t("header.providerManagement") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goAgentTemplateManagement"> <el-dropdown-item @click.native="goAgentTemplateManagement">
{{ $t('header.agentTemplate') }} {{ $t("header.agentTemplate") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goServerSideManagement"> <el-dropdown-item @click.native="goServerSideManagement">
{{ $t('header.serverSideManagement') }} {{ $t("header.serverSideManagement") }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
@@ -65,63 +193,89 @@
<!-- 右侧元素 --> <!-- 右侧元素 -->
<div class="header-right"> <div class="header-right">
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)"> <div
class="search-container"
v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)"
>
<div class="search-wrapper"> <div class="search-wrapper">
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input" <el-input
@keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable v-model="search"
ref="searchInput"> :placeholder="$t('header.searchPlaceholder')"
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i> class="custom-search-input"
@keyup.enter.native="handleSearch"
@focus="showSearchHistory"
@blur="hideSearchHistory"
clearable
ref="searchInput"
>
<i
slot="suffix"
class="el-icon-search search-icon"
@click="handleSearch"
></i>
</el-input> </el-input>
<!-- 搜索历史下拉框 --> <!-- 搜索历史下拉框 -->
<div v-if="showHistory && searchHistory.length > 0" class="search-history-dropdown"> <div
v-if="showHistory && searchHistory.length > 0"
class="search-history-dropdown"
>
<div class="search-history-header"> <div class="search-history-header">
<span>{{ $t('header.searchHistory') }}</span> <span>{{ $t("header.searchHistory") }}</span>
<el-button type="text" size="small" class="clear-history-btn" @click="clearSearchHistory"> <el-button
{{ $t('header.clearHistory') }} type="text"
size="small"
class="clear-history-btn"
@click="clearSearchHistory"
>
{{ $t("header.clearHistory") }}
</el-button> </el-button>
</div> </div>
<div class="search-history-list"> <div class="search-history-list">
<div v-for="(item, index) in searchHistory" :key="index" class="search-history-item" <div
@click.stop="selectSearchHistory(item)"> v-for="(item, index) in searchHistory"
:key="index"
class="search-history-item"
@click.stop="selectSearchHistory(item)"
>
<span class="history-text">{{ item }}</span> <span class="history-text">{{ item }}</span>
<i class="el-icon-close clear-item-icon" @click.stop="removeSearchHistory(index)"></i> <i
class="el-icon-close clear-item-icon"
@click.stop="removeSearchHistory(index)"
></i>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 语言切换下拉菜单 --> <img
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange"> loading="lazy"
<span class="el-dropdown-link"> alt=""
<span class="current-language-text">{{ currentLanguageText }}</span> src="@/assets/home/avatar.png"
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i> class="avatar-img"
</span> @click="handleAvatarClick"
<el-dropdown-menu slot="dropdown"> />
<el-dropdown-item @click.native="changeLanguage('zh_CN')"> <span class="el-dropdown-link" @click="handleAvatarClick">
{{ $t('language.zhCN') }} {{ userInfo.username || "加载中..." }}
</el-dropdown-item> <i
<el-dropdown-item @click.native="changeLanguage('zh_TW')"> class="el-icon-arrow-down el-icon--right"
{{ $t('language.zhTW') }} :class="{ 'rotate-down': userMenuVisible }"
</el-dropdown-item> ></i>
<el-dropdown-item @click.native="changeLanguage('en')"> </span>
{{ $t('language.en') }} <el-cascader
</el-dropdown-item> :options="userMenuOptions"
</el-dropdown-menu> trigger="click"
</el-dropdown> :props="cascaderProps"
style="width: 0px; overflow: hidden"
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" /> :show-all-levels="false"
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange"> @change="handleCascaderChange"
<span class="el-dropdown-link"> @visible-change="handleUserMenuVisibleChange"
{{ userInfo.username || '加载中...' }} ref="userCascader"
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i> >
</span> <template slot-scope="{ data }">
<el-dropdown-menu slot="dropdown"> <span>{{ data.label }}</span>
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword') </template>
}}</el-dropdown-item> </el-cascader>
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div> </div>
</div> </div>
@@ -131,109 +285,155 @@
</template> </template>
<script> <script>
import userApi from '@/apis/module/user'; import userApi from "@/apis/module/user";
import i18n, { changeLanguage } from '@/i18n'; import i18n, { changeLanguage } from "@/i18n";
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from "vuex";
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件 import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件
export default { export default {
name: 'HeaderBar', name: "HeaderBar",
components: { components: {
ChangePasswordDialog ChangePasswordDialog,
}, },
props: ['devices'], // 接收父组件设备列表 props: ["devices"], // 接收父组件设备列表
data() { data() {
return { return {
search: '', search: "",
userInfo: { userInfo: {
username: '', username: "",
mobile: '' mobile: "",
}, },
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示 isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
userDropdownVisible: false,
paramDropdownVisible: false, paramDropdownVisible: false,
languageDropdownVisible: false, voiceCloneDropdownVisible: false,
userMenuVisible: false, // 添加用户菜单可见状态
isSmallScreen: false, isSmallScreen: false,
// 搜索历史相关 // 搜索历史相关
searchHistory: [], searchHistory: [],
showHistory: false, showHistory: false,
SEARCH_HISTORY_KEY: 'xiaozhi_search_history', SEARCH_HISTORY_KEY: "xiaozhi_search_history",
MAX_HISTORY_COUNT: 3 MAX_HISTORY_COUNT: 3,
} // Cascader 配置
cascaderProps: {
expandTrigger: "click",
value: "value",
label: "label",
children: "children",
},
};
}, },
computed: { computed: {
...mapGetters(['getIsSuperAdmin']), ...mapGetters(["getIsSuperAdmin"]),
isSuperAdmin() { isSuperAdmin() {
return this.getIsSuperAdmin; return this.getIsSuperAdmin;
}, },
// 获取当前语言 // 获取当前语言
currentLanguage() { currentLanguage() {
return i18n.locale || 'zh_CN'; return i18n.locale || "zh_CN";
}, },
// 获取当前语言显示文本 // 获取当前语言显示文本
currentLanguageText() { currentLanguageText() {
const currentLang = this.currentLanguage; const currentLang = this.currentLanguage;
switch (currentLang) { switch (currentLang) {
case 'zh_CN': case "zh_CN":
return this.$t('language.zhCN'); return this.$t("language.zhCN");
case 'zh_TW': case "zh_TW":
return this.$t('language.zhTW'); return this.$t("language.zhTW");
case 'en': case "en":
return this.$t('language.en'); return this.$t("language.en");
default: default:
return this.$t('language.zhCN'); return this.$t("language.zhCN");
} }
} },
// 用户菜单选项
userMenuOptions() {
return [
{
label: this.currentLanguageText,
value: "language",
children: [
{
label: this.$t("language.zhCN"),
value: "zh_CN",
},
{
label: this.$t("language.zhTW"),
value: "zh_TW",
},
{
label: this.$t("language.en"),
value: "en",
},
],
},
{
label: this.$t("header.changePassword"),
value: "changePassword",
},
{
label: this.$t("header.logout"),
value: "logout",
},
];
},
}, },
mounted() { mounted() {
this.fetchUserInfo(); this.fetchUserInfo();
this.checkScreenSize(); this.checkScreenSize();
window.addEventListener('resize', this.checkScreenSize); window.addEventListener("resize", this.checkScreenSize);
// 从localStorage加载搜索历史 // 从localStorage加载搜索历史
this.loadSearchHistory(); this.loadSearchHistory();
}, },
//移除事件监听器 //移除事件监听器
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.checkScreenSize); window.removeEventListener("resize", this.checkScreenSize);
}, },
methods: { methods: {
goHome() { goHome() {
// 跳转到首页 // 跳转到首页
this.$router.push('/home') this.$router.push("/home");
}, },
goUserManagement() { goUserManagement() {
this.$router.push('/user-management') this.$router.push("/user-management");
}, },
goModelConfig() { goModelConfig() {
this.$router.push('/model-config') this.$router.push("/model-config");
},
goVoiceCloneManagement() {
this.$router.push("/voice-clone-management");
}, },
goParamManagement() { goParamManagement() {
this.$router.push('/params-management') this.$router.push("/params-management");
}, },
goOtaManagement() { goOtaManagement() {
this.$router.push('/ota-management') this.$router.push("/ota-management");
}, },
goDictManagement() { goDictManagement() {
this.$router.push('/dict-management') this.$router.push("/dict-management");
}, },
goProviderManagement() { goProviderManagement() {
this.$router.push('/provider-management') this.$router.push("/provider-management");
}, },
goServerSideManagement() { goServerSideManagement() {
this.$router.push('/server-side-management') this.$router.push("/server-side-management");
},
// 跳转到音色资源管理
goVoiceResourceManagement() {
this.$router.push("/voice-resource-management");
}, },
// 添加默认角色模板管理导航方法 // 添加默认角色模板管理导航方法
goAgentTemplateManagement() { goAgentTemplateManagement() {
this.$router.push('/agent-template-management') this.$router.push("/agent-template-management");
}, },
// 获取用户信息 // 获取用户信息
fetchUserInfo() { fetchUserInfo() {
userApi.getUserInfo(({ data }) => { userApi.getUserInfo(({ data }) => {
this.userInfo = data.data this.userInfo = data.data;
if (data.data.superAdmin !== undefined) { if (data.data.superAdmin !== undefined) {
this.$store.commit('setUserInfo', data.data); this.$store.commit("setUserInfo", data.data);
} }
}) });
}, },
checkScreenSize() { checkScreenSize() {
this.isSmallScreen = window.innerWidth <= 1386; this.isSmallScreen = window.innerWidth <= 1386;
@@ -244,7 +444,7 @@ export default {
// 如果搜索内容为空,触发重置事件 // 如果搜索内容为空,触发重置事件
if (!searchValue) { if (!searchValue) {
this.$emit('search-reset'); this.$emit("search-reset");
return; return;
} }
@@ -253,14 +453,14 @@ export default {
try { try {
// 创建不区分大小写的正则表达式 // 创建不区分大小写的正则表达式
const regex = new RegExp(searchValue, 'i'); const regex = new RegExp(searchValue, "i");
// 触发搜索事件,将正则表达式传递给父组件 // 触发搜索事件,将正则表达式传递给父组件
this.$emit('search', regex); this.$emit("search", regex);
} catch (error) { } catch (error) {
console.error('正则表达式创建失败:', error); console.error("正则表达式创建失败:", error);
this.$message.error({ this.$message.error({
message: this.$t('message.error'), message: this.$t("message.error"),
showClose: true showClose: true,
}); });
} }
@@ -291,7 +491,7 @@ export default {
this.searchHistory = JSON.parse(history); this.searchHistory = JSON.parse(history);
} }
} catch (error) { } catch (error) {
console.error('加载搜索历史失败:', error); console.error("加载搜索历史失败:", error);
this.searchHistory = []; this.searchHistory = [];
} }
}, },
@@ -314,7 +514,7 @@ export default {
try { try {
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory)); localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
} catch (error) { } catch (error) {
console.error('保存搜索历史失败:', error); console.error("保存搜索历史失败:", error);
} }
}, },
@@ -330,7 +530,7 @@ export default {
try { try {
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory)); localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
} catch (error) { } catch (error) {
console.error('更新搜索历史失败:', error); console.error("更新搜索历史失败:", error);
} }
}, },
@@ -340,12 +540,14 @@ export default {
try { try {
localStorage.removeItem(this.SEARCH_HISTORY_KEY); localStorage.removeItem(this.SEARCH_HISTORY_KEY);
} catch (error) { } catch (error) {
console.error('清空搜索历史失败:', error); console.error("清空搜索历史失败:", error);
} }
}, },
// 显示修改密码弹窗 // 显示修改密码弹窗
showChangePasswordDialog() { showChangePasswordDialog() {
this.isChangePasswordDialogVisible = true; this.isChangePasswordDialogVisible = true;
// 添加:显示修改密码弹窗后重置用户菜单可见状态
this.userMenuVisible = false;
}, },
// 退出登录 // 退出登录
async handleLogout() { async handleLogout() {
@@ -353,42 +555,147 @@ export default {
// 调用 Vuex 的 logout action // 调用 Vuex 的 logout action
await this.logout(); await this.logout();
this.$message.success({ this.$message.success({
message: this.$t('message.success'), message: this.$t("message.success"),
showClose: true showClose: true,
}); });
} catch (error) { } catch (error) {
console.error('退出登录失败:', error); console.error("退出登录失败:", error);
this.$message.error({ this.$message.error({
message: this.$t('message.error'), message: this.$t("message.error"),
showClose: true showClose: true,
}); });
} }
}, },
handleUserDropdownVisibleChange(visible) { // 监听参数字典下拉菜单的可见状态变化
this.userDropdownVisible = visible;
},
// 监听第二个下拉菜单的可见状态变化
handleParamDropdownVisibleChange(visible) { handleParamDropdownVisibleChange(visible) {
this.paramDropdownVisible = visible; this.paramDropdownVisible = visible;
}, },
// 监听语言下拉菜单的可见状态变化
handleLanguageDropdownVisibleChange(visible) { // 监听音色克隆下拉菜单的可见状态变化
this.languageDropdownVisible = visible; handleVoiceCloneDropdownVisibleChange(visible) {
this.voiceCloneDropdownVisible = visible;
}, },
// 在data中添加一个key用于强制重新渲染组件
// 处理 Cascader 选择变化
handleCascaderChange(value) {
if (!value || value.length === 0) {
return;
}
const action = value[value.length - 1];
// 处理语言切换
if (value.length === 2 && value[0] === "language") {
this.changeLanguage(action);
} else {
// 处理其他操作
switch (action) {
case "changePassword":
this.showChangePasswordDialog();
break;
case "logout":
this.handleLogout();
break;
}
}
// 操作完成后立即清空选择
setTimeout(() => {
this.completeResetCascader();
}, 300);
},
// 切换语言 // 切换语言
changeLanguage(lang) { changeLanguage(lang) {
changeLanguage(lang); changeLanguage(lang);
this.languageDropdownVisible = false;
this.$message.success({ this.$message.success({
message: this.$t('message.success'), message: this.$t("message.success"),
showClose: true showClose: true,
}); });
// 添加:切换语言后重置用户菜单可见状态
this.userMenuVisible = false;
},
// 完全重置级联选择器
completeResetCascader() {
if (this.$refs.userCascader) {
try {
// 尝试所有可能的方法来清空选择
// 1. 尝试使用组件提供的clearValue方法
if (this.$refs.userCascader.clearValue) {
this.$refs.userCascader.clearValue();
}
// 2. 直接清空内部属性
if (this.$refs.userCascader.$data) {
this.$refs.userCascader.$data.selectedPaths = [];
this.$refs.userCascader.$data.displayLabels = [];
this.$refs.userCascader.$data.inputValue = "";
this.$refs.userCascader.$data.checkedValue = [];
this.$refs.userCascader.$data.showAllLevels = false;
}
// 3. 操作DOM清除选中状态
const menuElement = this.$refs.userCascader.$refs.menu;
if (menuElement && menuElement.$el) {
const activeItems = menuElement.$el.querySelectorAll(
".el-cascader-node.is-active"
);
activeItems.forEach((item) => item.classList.remove("is-active"));
const checkedItems = menuElement.$el.querySelectorAll(
".el-cascader-node.is-checked"
);
checkedItems.forEach((item) => item.classList.remove("is-checked"));
}
console.log("Cascader values cleared");
} catch (error) {
console.error("清空选择值失败:", error);
}
}
},
// 点击头像触发cascader下拉菜单
handleAvatarClick() {
if (this.$refs.userCascader) {
// 切换菜单可见状态
this.userMenuVisible = !this.userMenuVisible;
// 菜单收起时清空选择值
if (!this.userMenuVisible) {
this.completeResetCascader();
}
// 直接设置菜单的显隐状态
try {
// 尝试使用toggleDropDownVisible方法
this.$refs.userCascader.toggleDropDownVisible(this.userMenuVisible);
} catch (error) {
// 如果toggle方法失败,尝试直接设置属性
if (this.$refs.userCascader.$refs.menu) {
this.$refs.userCascader.$refs.menu.showMenu(this.userMenuVisible);
} else {
console.error("Cannot access menu component");
}
}
}
},
// 处理用户菜单可见性变化
handleUserMenuVisibleChange(visible) {
this.userMenuVisible = visible;
// 如果菜单关闭了,也要清空选择值
if (!visible) {
this.completeResetCascader();
}
}, },
// 使用 mapActions 引入 Vuex 的 logout action // 使用 mapActions 引入 Vuex 的 logout action
...mapActions(['logout']) ...mapActions(["logout"]),
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -430,7 +737,7 @@ export default {
align-items: center; align-items: center;
gap: 25px; gap: 25px;
position: absolute; position: absolute;
left: 44%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
@@ -542,6 +849,12 @@ export default {
visibility: hidden; visibility: hidden;
} }
.more-dropdown .el-dropdown-link {
display: flex;
align-items: center;
gap: 7px;
}
.search-history-item:hover .clear-item-icon { .search-history-item:hover .clear-item-icon {
visibility: visible; visibility: visible;
} }
@@ -550,7 +863,7 @@ export default {
color: #ff4949; color: #ff4949;
} }
.custom-search-input>>>.el-input__inner { .custom-search-input >>> .el-input__inner {
height: 18px; height: 18px;
border-radius: 9px; border-radius: 9px;
background-color: #fff; background-color: #fff;
@@ -579,41 +892,7 @@ export default {
width: 21px; width: 21px;
height: 21px; height: 21px;
flex-shrink: 0; flex-shrink: 0;
} cursor: pointer;
.user-dropdown {
flex-shrink: 0;
}
.language-dropdown {
flex-shrink: 0;
margin-right: 5px;
}
.current-language-text {
margin-left: 4px;
margin-right: 4px;
font-size: 12px;
color: #3d4566;
}
.more-dropdown {
padding-right: 20px;
}
.more-dropdown .el-dropdown-link {
display: flex;
align-items: center;
gap: 7px;
}
.rotate-down {
transform: rotate(180deg);
transition: transform 0.3s ease;
}
.el-icon-arrow-down {
transition: transform 0.3s ease;
} }
/* 导航文本样式 - 支持中英文换行 */ /* 导航文本样式 - 支持中英文换行 */
@@ -654,4 +933,13 @@ export default {
color: #606266; color: #606266;
white-space: nowrap; white-space: nowrap;
} }
</style> /* 添加倒三角旋转样式 */
.rotate-down {
transform: rotate(180deg);
transition: transform 0.3s ease;
}
.el-icon-arrow-down {
transition: transform 0.3s ease;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,694 @@
<template>
<el-dialog :title="$t('voiceClone.dialogTitle')" :visible.sync="visible" width="900px" top="10vh"
:before-close="handleClose" class="voice-clone-dialog">
<div class="dialog-content">
<!-- 步骤指示器 -->
<div class="steps-header">
<div class="step-item" :class="{ 'active': currentStep === 1, 'completed': currentStep > 1 }">
<div class="step-number">
<i class="el-icon-check" v-if="currentStep > 1"></i>
<span v-else>1</span>
</div>
<div class="step-label">{{ $t('voiceClone.stepUpload') }}</div>
<div class="step-line"></div>
</div>
<div class="step-item" :class="{ 'active': currentStep === 2, 'completed': currentStep > 2 }">
<div class="step-number">
<i class="el-icon-check" v-if="currentStep > 2"></i>
<span v-else>2</span>
</div>
<div class="step-label">{{ $t('voiceClone.stepEdit') }}</div>
</div>
</div>
<!-- 步骤1: 音频上传 -->
<div v-if="currentStep === 1" class="step-content">
<div class="upload-area">
<el-upload class="audio-uploader" drag :action="uploadAction" :auto-upload="false"
:on-change="handleFileChange" :show-file-list="false" accept="audio/*">
<i class="el-icon-upload"></i>
<div class="el-upload__text">{{ $t('voiceClone.dragOrClick') }}</div>
<div class="el-upload__tip">{{ $t('voiceClone.uploadTip') }}</div>
</el-upload>
</div>
</div>
<!-- 步骤2: 音频编辑 -->
<div v-if="currentStep === 2" class="step-content">
<div class="audio-edit-area">
<div class="edit-tips">
<p>{{ $t('voiceClone.editTip1') }}</p>
<p>{{ $t('voiceClone.editTip2') }}</p>
</div>
<!-- 波形显示区域 -->
<div class="waveform-container">
<canvas ref="waveformCanvas" class="waveform-canvas" @mousedown="handleWaveformMouseDown"
@mousemove="handleWaveformMouseMove" @mouseup="handleWaveformMouseUp"></canvas>
<div class="selection-overlay" v-if="isSelecting || selectionStart !== null"
:style="selectionStyle"></div>
<div class="duration-display">
{{ $t('voiceClone.selectedDuration', { duration: selectedDuration.toFixed(1) }) }}
</div>
</div>
<!-- 音频控制按钮 -->
<div class="audio-controls">
<el-button size="small" :icon="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'"
@click="togglePlay" type="primary">
{{ isPlaying ? $t('voiceClone.pause') : $t('voiceClone.play') }}
</el-button>
<el-button size="small" icon="el-icon-scissors" @click="handleTrim"
:disabled="selectionStart === null">
{{ $t('voiceClone.trim') }}
</el-button>
<el-button size="small" icon="el-icon-refresh-left" @click="handleReset">
{{ $t('voiceClone.reset') }}
</el-button>
</div>
<!-- 音频元素 -->
<audio ref="audioPlayer" @timeupdate="handleTimeUpdate" @ended="handleAudioEnded"
style="display: none;"></audio>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep')
}}</el-button>
<el-button type="primary" @click="handleNext" :loading="uploading">
{{ currentStep === 1 ? $t('voiceClone.nextStep') : $t('voiceClone.upload') }}
</el-button>
</span>
</el-dialog>
</template>
<script>
import Api from "@/apis/api";
export default {
name: 'VoiceCloneDialog',
props: {
visible: {
type: Boolean,
default: false
},
voiceCloneData: {
type: Object,
default: () => ({})
}
},
data() {
return {
currentStep: 1,
uploadAction: '',
audioFile: null,
originalAudioFile: null,
audioBuffer: null,
originalAudioBuffer: null,
isPlaying: false,
uploading: false,
// 波形相关
waveformData: [],
// 选择相关
isSelecting: false,
selectionStart: null,
selectionEnd: null,
mouseStartX: 0,
// 音频上下文
audioContext: null,
audioSource: null,
};
},
computed: {
selectedDuration() {
if (this.selectionStart !== null && this.selectionEnd !== null && this.audioBuffer) {
const duration = this.audioBuffer.duration;
const start = Math.min(this.selectionStart, this.selectionEnd) * duration;
const end = Math.max(this.selectionStart, this.selectionEnd) * duration;
return end - start;
}
return this.audioBuffer ? this.audioBuffer.duration : 0;
},
selectionStyle() {
if (this.selectionStart === null) return {};
const canvas = this.$refs.waveformCanvas;
if (!canvas) return {};
const start = Math.min(this.selectionStart, this.selectionEnd || this.selectionStart);
const end = Math.max(this.selectionStart, this.selectionEnd || this.selectionStart);
return {
left: `${start * 100}%`,
width: `${(end - start) * 100}%`
};
}
},
methods: {
handleClose() {
if (this.currentStep === 2) {
this.currentStep = 1;
} else {
this.resetDialog();
this.$emit('update:visible', false);
}
},
resetDialog() {
this.currentStep = 1;
this.audioFile = null;
this.originalAudioFile = null;
this.audioBuffer = null;
this.originalAudioBuffer = null;
this.isPlaying = false;
this.uploading = false;
this.waveformData = [];
this.selectionStart = null;
this.selectionEnd = null;
if (this.audioSource) {
this.audioSource.stop();
this.audioSource = null;
}
},
async handleFileChange(file) {
if (!file || !file.raw) return;
this.audioFile = file.raw;
this.originalAudioFile = file.raw;
// 先进入第二步,确保DOM已渲染
this.currentStep = 2;
// 等待DOM更新后再加载音频
await this.$nextTick();
await this.loadAudio(file.raw);
},
async loadAudio(file) {
try {
const arrayBuffer = await file.arrayBuffer();
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer.slice(0));
this.originalAudioBuffer = await this.audioContext.decodeAudioData(await file.arrayBuffer());
// 设置音频播放器
if (this.$refs.audioPlayer) {
const audioUrl = URL.createObjectURL(file);
this.$refs.audioPlayer.src = audioUrl;
// 加载音频元数据
this.$refs.audioPlayer.load();
}
// 生成波形数据
await this.generateWaveform();
} catch (error) {
console.error('加载音频失败:', error);
this.$message.error(this.$t('voiceClone.loadAudioFailed'));
}
},
async generateWaveform() {
if (!this.audioBuffer) return;
await this.$nextTick();
const canvas = this.$refs.waveformCanvas;
if (!canvas) {
console.error('Canvas元素不存在');
return;
}
// 设置canvas大小
const containerWidth = canvas.parentElement.offsetWidth;
const containerHeight = canvas.parentElement.offsetHeight;
canvas.width = containerWidth || 800;
canvas.height = containerHeight || 200;
const canvasWidth = canvas.width;
const channelData = this.audioBuffer.getChannelData(0);
const step = Math.floor(channelData.length / canvasWidth);
const waveformData = [];
for (let i = 0; i < canvasWidth; i++) {
let sum = 0;
for (let j = 0; j < step; j++) {
sum += Math.abs(channelData[i * step + j] || 0);
}
waveformData.push(sum / step);
}
this.waveformData = waveformData;
this.drawWaveform();
},
drawWaveform() {
const canvas = this.$refs.waveformCanvas;
if (!canvas) {
console.error('绘制波形时Canvas不存在');
return;
}
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 绘制背景
ctx.fillStyle = '#e0f2ff';
ctx.fillRect(0, 0, width, height);
if (this.waveformData.length === 0) {
console.error('波形数据为空');
return;
}
// 找到最大值用于归一化
const maxValue = Math.max(...this.waveformData);
// 绘制波形
ctx.fillStyle = '#4ade80';
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 1;
const barWidth = width / this.waveformData.length;
this.waveformData.forEach((value, index) => {
// 归一化并放大,使用80%的高度
const normalizedValue = maxValue > 0 ? value / maxValue : 0;
const barHeight = Math.max(1, normalizedValue * height * 0.8);
const x = index * barWidth;
const y = (height - barHeight) / 2;
ctx.fillRect(x, y, Math.max(1, barWidth - 1), barHeight);
});
},
handleWaveformMouseDown(e) {
const canvas = this.$refs.waveformCanvas;
const rect = canvas.getBoundingClientRect();
this.mouseStartX = e.clientX - rect.left;
this.selectionStart = this.mouseStartX / rect.width;
this.selectionEnd = this.selectionStart;
this.isSelecting = true;
},
handleWaveformMouseMove(e) {
if (!this.isSelecting) return;
const canvas = this.$refs.waveformCanvas;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
this.selectionEnd = Math.max(0, Math.min(1, x / rect.width));
},
handleWaveformMouseUp() {
this.isSelecting = false;
},
async handleTrim() {
if (this.selectionStart === null || this.selectionEnd === null || !this.audioBuffer) return;
const start = Math.min(this.selectionStart, this.selectionEnd);
const end = Math.max(this.selectionStart, this.selectionEnd);
// 创建新的音频buffer
const duration = this.audioBuffer.duration;
const startTime = start * duration;
const endTime = end * duration;
const startOffset = Math.floor(startTime * this.audioBuffer.sampleRate);
const endOffset = Math.floor(endTime * this.audioBuffer.sampleRate);
const newLength = endOffset - startOffset;
const newBuffer = this.audioContext.createBuffer(
this.audioBuffer.numberOfChannels,
newLength,
this.audioBuffer.sampleRate
);
for (let channel = 0; channel < this.audioBuffer.numberOfChannels; channel++) {
const oldData = this.audioBuffer.getChannelData(channel);
const newData = newBuffer.getChannelData(channel);
for (let i = 0; i < newLength; i++) {
newData[i] = oldData[startOffset + i];
}
}
this.audioBuffer = newBuffer;
// 更新音频文件
await this.bufferToFile(newBuffer);
// 重置选择
this.selectionStart = null;
this.selectionEnd = null;
// 重新生成波形
this.generateWaveform();
this.$message.success(this.$t('voiceClone.trimSuccess'));
},
async handleReset() {
if (!this.originalAudioFile) return;
this.audioFile = this.originalAudioFile;
await this.loadAudio(this.originalAudioFile);
this.selectionStart = null;
this.selectionEnd = null;
this.$message.success(this.$t('voiceClone.resetSuccess'));
},
togglePlay() {
const audio = this.$refs.audioPlayer;
if (this.isPlaying) {
audio.pause();
this.isPlaying = false;
} else {
audio.play();
this.isPlaying = true;
}
},
handleTimeUpdate() {
// 可以在这里更新播放进度
},
handleAudioEnded() {
this.isPlaying = false;
},
async bufferToFile(buffer) {
// 将AudioBuffer转换为WAV文件
const wav = this.audioBufferToWav(buffer);
const blob = new Blob([wav], { type: 'audio/wav' });
this.audioFile = new File([blob], 'audio.wav', { type: 'audio/wav' });
// 更新播放器
await this.$nextTick();
if (this.$refs.audioPlayer) {
const audioUrl = URL.createObjectURL(blob);
this.$refs.audioPlayer.src = audioUrl;
}
},
audioBufferToWav(buffer) {
const length = buffer.length * buffer.numberOfChannels * 2 + 44;
const arrayBuffer = new ArrayBuffer(length);
const view = new DataView(arrayBuffer);
const channels = [];
let offset = 0;
let pos = 0;
// 写入WAV文件头
const setUint16 = (data) => {
view.setUint16(pos, data, true);
pos += 2;
};
const setUint32 = (data) => {
view.setUint32(pos, data, true);
pos += 4;
};
// "RIFF" chunk descriptor
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
// "fmt " sub-chunk
setUint32(0x20746d66); // "fmt "
setUint32(16); // SubChunk1Size = 16
setUint16(1); // AudioFormat = 1 (PCM)
setUint16(buffer.numberOfChannels);
setUint32(buffer.sampleRate);
setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // byte rate
setUint16(buffer.numberOfChannels * 2); // block align
setUint16(16); // bits per sample
// "data" sub-chunk
setUint32(0x61746164); // "data"
setUint32(length - pos - 4); // SubChunk2Size
// 写入音频数据
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
while (pos < length) {
for (let i = 0; i < buffer.numberOfChannels; i++) {
let sample = Math.max(-1, Math.min(1, channels[i][offset]));
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(pos, sample, true);
pos += 2;
}
offset++;
}
return arrayBuffer;
},
async handleNext() {
if (this.currentStep === 1) {
// 验证是否已选择文件
if (!this.audioFile) {
this.$message.warning(this.$t('voiceClone.pleaseSelectAudio'));
return;
}
this.currentStep = 2;
} else {
// 上传音频
await this.uploadAudio();
}
},
async uploadAudio() {
if (!this.audioFile) {
this.$message.warning(this.$t('voiceClone.pleaseSelectAudio'));
return;
}
// 验证音频时长(8-60秒)
if (this.audioBuffer) {
const duration = this.audioBuffer.duration;
if (duration < 8 || duration > 60) {
this.$message.warning(this.$t('voiceClone.durationError'));
return;
}
}
this.uploading = true;
try {
const formData = new FormData();
formData.append('voiceFile', this.audioFile);
formData.append('id', this.voiceCloneData.id);
await Api.voiceClone.uploadVoice(formData, (res) => {
this.uploading = false;
res = res.data;
if (res.code === 0) {
this.$message.success(this.$t('voiceClone.uploadSuccess'));
this.resetDialog();
this.$emit('update:visible', false);
this.$emit('success');
} else {
this.$message.error(res.msg || this.$t('voiceClone.uploadFailed'));
}
});
} catch (error) {
this.uploading = false;
console.error('上传音频失败:', error);
this.$message.error(this.$t('voiceClone.uploadFailed'));
}
}
},
mounted() {
// 设置canvas大小
this.$nextTick(() => {
const canvas = this.$refs.waveformCanvas;
if (canvas) {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
});
},
beforeDestroy() {
if (this.audioContext) {
this.audioContext.close();
}
}
};
</script>
<style lang="scss" scoped>
.voice-clone-dialog {
::v-deep .el-dialog__body {
padding: 20px 30px;
}
}
.steps-header {
display: flex;
justify-content: center;
align-items: center;
padding: 0 50px;
}
.step-item {
display: flex;
align-items: center;
position: relative;
&.active {
.step-number {
background: #6b8cff;
color: white;
}
.step-label {
color: #333;
font-weight: 600;
}
}
&.completed {
.step-number {
background: #67c23a;
color: white;
}
.step-line {
background: #67c23a;
}
}
}
.step-number {
width: 36px;
height: 36px;
border-radius: 50%;
background: #e4e7ed;
color: #909399;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
z-index: 1;
}
.step-label {
margin-left: 12px;
color: #909399;
font-size: 14px;
}
.step-line {
width: 200px;
height: 2px;
background: #e4e7ed;
margin-left: 12px;
}
.step-content {
padding: 20px 0;
}
.upload-area {
padding: 40px 0;
}
.audio-uploader {
::v-deep .el-upload {
width: 100%;
}
::v-deep .el-upload-dragger {
width: 100%;
height: 280px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.el-icon-upload {
font-size: 67px;
color: #c0c4cc;
margin: 0 0 16px;
line-height: 50px;
}
}
.el-upload__text {
color: #606266;
font-size: 14px;
margin-bottom: 8px;
}
.el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 7px;
}
}
.audio-edit-area {
padding: 0 20px;
}
.edit-tips {
margin-bottom: 20px;
padding: 12px;
background: #f0f9ff;
border-radius: 4px;
border-left: 3px solid #1890ff;
p {
margin: 4px 0;
font-size: 13px;
color: #606266;
&:first-child {
font-weight: 500;
}
}
}
.waveform-container {
position: relative;
width: 100%;
height: 200px;
margin-bottom: 20px;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
cursor: crosshair;
}
.waveform-canvas {
width: 100%;
height: 100%;
}
.selection-overlay {
position: absolute;
top: 0;
bottom: 0;
background: rgba(107, 140, 255, 0.3);
border: 1px solid #6b8cff;
pointer-events: none;
}
.duration-display {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.audio-controls {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -0,0 +1,154 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
@open="handleOpen" width="500px">
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item :label="$t('voiceClone.platformName')" prop="modelId">
<el-select v-model="form.modelId" :placeholder="$t('voiceClone.platformNamePlaceholder')" filterable
style="width: 100%" @change="handlePlatformChange">
<el-option v-for="model in platformList" :key="model.id" :label="model.modelName" :value="model.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('voiceClone.voiceId')" prop="voiceIds">
<el-select v-model="form.voiceIds" :placeholder="$t('voiceClone.voiceIdPlaceholder')" filterable multiple
allow-create default-first-option style="width: 100%">
<el-option v-for="voiceId in voiceIdList" :key="voiceId" :label="voiceId" :value="voiceId">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('voiceClone.userId')" prop="userId">
<el-select v-model="form.userId" :placeholder="$t('voiceClone.userIdPlaceholder')" filterable remote
:remote-method="remoteSearchUser" :loading="userLoading" style="width: 100%">
<el-option v-for="user in userList" :key="user.userid" :label="user.mobile" :value="user.userid">
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel">{{ $t('voiceClone.operationCancelled') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('voiceClone.addNew') }}</el-button>
</div>
</el-dialog>
</template>
<script>
import Api from '@/apis/api';
export default {
name: 'VoiceCloneDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
form: {
type: Object,
default: () => ({})
}
},
data() {
return {
dialogVisible: this.visible,
platformList: [],
voiceIdList: [],
userList: [],
userLoading: false,
rules: {
modelId: [
{ required: true, message: this.$t('voiceClone.platformNameRequired'), trigger: 'change' }
],
voiceIds: [
{ required: true, message: this.$t('voiceClone.voiceIdRequired'), trigger: 'change' }
],
userId: [
{ required: true, message: this.$t('voiceClone.userIdRequired'), trigger: 'change' }
]
}
}
},
watch: {
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
}
},
methods: {
handleClose() {
this.dialogVisible = false;
this.$emit('cancel');
},
handleCancel() {
this.$refs.form.clearValidate();
this.$emit('cancel');
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('submit', this.form);
}
});
},
handleOpen() {
// 对话框打开时加载平台列表
this.fetchPlatformList();
// 重置音色ID列表
this.voiceIdList = [];
},
handlePlatformChange(modelId) {
// 清空音色ID选择
this.form.voiceIds = [];
},
// 获取TTS平台列表
fetchPlatformList() {
Api.voiceResource.getTtsPlatformList((res) => {
if (res.data.code === 0) {
this.platformList = res.data.data;
}
});
},
// 远程搜索用户
remoteSearchUser(query) {
if (query !== '') {
this.userLoading = true;
const params = {
page: 1,
limit: 20,
mobile: query
};
Api.admin.getUserList(params, (res) => {
this.userLoading = false;
if (res.data.code === 0) {
this.userList = res.data.data.list;
}
});
} else {
this.userList = [];
}
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
border-radius: 20px;
}
</style>
+138 -86
View File
@@ -9,14 +9,21 @@ export default {
// HeaderBar组件文本 // HeaderBar组件文本
'header.smartManagement': 'Agents', 'header.smartManagement': 'Agents',
'header.modelConfig': 'Models', 'header.modelConfig': 'Models',
'header.voiceCloneManagement': 'Voice Clone',
'header.voiceResourceManagement': 'Voice Resource',
'header.userManagement': 'Users', 'header.userManagement': 'Users',
'header.otaManagement': 'OTA', 'header.otaManagement': 'OTA Management',
'header.paramDictionary': 'More', 'header.paramDictionary': 'More',
'header.paramManagement': 'Params Management', 'header.paramManagement': 'Params Management',
'header.dictManagement': 'Dict Management', 'header.dictManagement': 'Dict Management',
'header.agentTemplate': 'Default Role Templates', // 添加这一行 'header.agentTemplate': 'Default Role Templates', // 添加这一行
'header.searchHistory': 'Search History', 'header.searchHistory': 'Search History',
'header.clearHistory': 'Clear History', 'header.clearHistory': 'Clear History',
'header.providerManagement': 'Provider Management',
'header.serverSideManagement': 'Server Management',
'header.changePassword': 'Change Password',
'header.logout': 'Logout',
'header.searchPlaceholder': 'Search by name..',
// McpToolCallDialog component text // McpToolCallDialog component text
'mcpToolCall.title': 'Tool Call', 'mcpToolCall.title': 'Tool Call',
@@ -189,6 +196,23 @@ export default {
'firmwareDialog.firmwareNamePlaceholder': 'Please enter firmware name (board + version)', 'firmwareDialog.firmwareNamePlaceholder': 'Please enter firmware name (board + version)',
'firmwareDialog.firmwareType': 'Firmware Type', 'firmwareDialog.firmwareType': 'Firmware Type',
'firmwareDialog.firmwareTypePlaceholder': 'Please select firmware type', 'firmwareDialog.firmwareTypePlaceholder': 'Please select firmware type',
'firmwareDialog.version': 'Version',
'firmwareDialog.versionPlaceholder': 'Please enter version (x.x.x format)',
'firmwareDialog.firmwareFile': 'Firmware File',
'firmwareDialog.clickUpload': 'Click to Upload',
'firmwareDialog.uploadTip': 'Only firmware files (.bin/.apk) are supported, and size should not exceed 100MB',
'firmwareDialog.uploadHint': 'Warm reminder: Please upload the pre-merged xiaozhi.bin file, not the merged merged-binary.bin file',
'firmwareDialog.remark': 'Remark',
'firmwareDialog.remarkPlaceholder': 'Please enter remark information',
'firmwareDialog.requiredFirmwareName': 'Please enter firmware name (board + version)',
'firmwareDialog.requiredFirmwareType': 'Please select firmware type',
'firmwareDialog.requiredVersion': 'Please enter version',
'firmwareDialog.versionFormatError': 'Version format is incorrect, please enter x.x.x format',
'firmwareDialog.requiredFirmwareFile': 'Please upload firmware file',
'firmwareDialog.invalidFileType': 'Only .bin/.apk format firmware files are supported!',
'firmwareDialog.invalidFileSize': 'Firmware file size cannot exceed 100MB!',
'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully',
'firmwareDialog.uploadFailed': 'File upload failed',
// Voice print dialog related // Voice print dialog related
'voicePrintDialog.addSpeaker': 'Add Speaker', 'voicePrintDialog.addSpeaker': 'Add Speaker',
@@ -230,6 +254,21 @@ export default {
// Manual add device dialog related // Manual add device dialog related
'manualAddDeviceDialog.title': 'Manual Add Device', 'manualAddDeviceDialog.title': 'Manual Add Device',
'manualAddDeviceDialog.deviceType': 'Device Type',
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type',
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version',
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version',
'manualAddDeviceDialog.macAddress': 'Mac Address',
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
'manualAddDeviceDialog.confirm': 'Confirm',
'manualAddDeviceDialog.cancel': 'Cancel',
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
'manualAddDeviceDialog.invalidMacAddress': 'Please enter correct Mac address format, e.g.: 00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': 'Please select device type',
'manualAddDeviceDialog.requiredFirmwareVersion': 'Please enter firmware version',
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Failed to get firmware type',
'manualAddDeviceDialog.addSuccess': 'Device added successfully',
'manualAddDeviceDialog.addFailed': 'Failed to add',
// AddModelDialog component related // AddModelDialog component related
'addModelDialog.requiredSupplier': 'Please select a supplier', 'addModelDialog.requiredSupplier': 'Please select a supplier',
@@ -261,16 +300,12 @@ export default {
'register.requiredPassword': 'Password cannot be empty', 'register.requiredPassword': 'Password cannot be empty',
'register.requiredCaptcha': 'Captcha cannot be empty', 'register.requiredCaptcha': 'Captcha cannot be empty',
'register.requiredMobileCaptcha': 'Please enter SMS verification code', 'register.requiredMobileCaptcha': 'Please enter SMS verification code',
'register.invalidCaptcha': 'Invalid verification code',
'manualAddDeviceDialog.deviceType': 'Device Type', 'register.passwordLength': 'Password length must be between 6-20 characters',
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type', 'register.usernameFormat': 'Invalid username format',
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version', 'register.confirmPasswordRequired': 'Confirm password cannot be empty',
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version', 'register.sendCaptcha': 'Get Verification Code',
'manualAddDeviceDialog.macAddress': 'Mac Address', 'register.secondsLater': 'seconds later',
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
'manualAddDeviceDialog.confirm': 'Confirm',
'manualAddDeviceDialog.cancel': 'Cancel',
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
// Parameter dialog related // Parameter dialog related
'paramDialog.paramCode': 'Parameter Code', 'paramDialog.paramCode': 'Parameter Code',
@@ -291,34 +326,6 @@ export default {
'paramDialog.booleanType': 'Boolean', 'paramDialog.booleanType': 'Boolean',
'paramDialog.arrayType': 'Array', 'paramDialog.arrayType': 'Array',
'paramDialog.jsonType': 'JSON Object', 'paramDialog.jsonType': 'JSON Object',
'manualAddDeviceDialog.invalidMacAddress': 'Please enter correct Mac address format, e.g.: 00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': 'Please select device type',
'manualAddDeviceDialog.requiredFirmwareVersion': 'Please enter firmware version',
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Failed to get firmware type',
'manualAddDeviceDialog.addSuccess': 'Device added successfully',
'manualAddDeviceDialog.addFailed': 'Failed to add',
'firmwareDialog.version': 'Version',
'firmwareDialog.versionPlaceholder': 'Please enter version (x.x.x format)',
'firmwareDialog.firmwareFile': 'Firmware File',
'firmwareDialog.clickUpload': 'Click to Upload',
'firmwareDialog.uploadTip': 'Only firmware files (.bin/.apk) are supported, and size should not exceed 100MB',
'firmwareDialog.uploadHint': 'Warm reminder: Please upload the pre-merged xiaozhi.bin file, not the merged merged-binary.bin file',
'firmwareDialog.remark': 'Remark',
'firmwareDialog.remarkPlaceholder': 'Please enter remark information',
'firmwareDialog.requiredFirmwareName': 'Please enter firmware name (board + version)',
'firmwareDialog.requiredFirmwareType': 'Please select firmware type',
'firmwareDialog.requiredVersion': 'Please enter version',
'firmwareDialog.versionFormatError': 'Version format is incorrect, please enter x.x.x format',
'firmwareDialog.requiredFirmwareFile': 'Please upload firmware file',
'firmwareDialog.invalidFileType': 'Only .bin/.apk format firmware files are supported!',
'firmwareDialog.invalidFileSize': 'Firmware file size cannot exceed 100MB!',
'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully',
'firmwareDialog.uploadFailed': 'File upload failed',
'header.providerManagement': 'Provider Management',
'header.serverSideManagement': 'Server Management',
'header.changePassword': 'Change Password',
'header.logout': 'Logout',
'header.searchPlaceholder': 'Search by name..',
// Login page text // Login page text
'login.title': 'Login', 'login.title': 'Login',
@@ -342,52 +349,6 @@ export default {
'login.privacyPolicy': 'Privacy Policy', 'login.privacyPolicy': 'Privacy Policy',
'login.registerAccount': 'Register Account', 'login.registerAccount': 'Register Account',
// Register page text
'register.title': 'Register Account',
'register.welcome': 'WELCOME TO REGISTER',
'register.username': 'Username',
'register.usernamePlaceholder': 'Please enter username',
'register.mobile': 'Mobile',
'register.mobilePlaceholder': 'Please enter mobile number',
'register.password': 'Password',
'register.passwordPlaceholder': 'Please enter password',
'register.confirmPassword': 'Confirm Password',
'register.confirmPasswordPlaceholder': 'Please confirm password',
'register.captcha': 'Verification Code',
'register.captchaPlaceholder': 'Please enter verification code',
'register.mobileCaptcha': 'Mobile Verification Code',
'register.mobileCaptchaPlaceholder': 'Please enter mobile verification code',
'register.getMobileCaptcha': 'Get Verification Code',
'register.registerButton': 'Register',
'register.login': 'Login',
'register.inputCorrectMobile': 'Please enter correct mobile number',
'register.inputCaptcha': 'Please enter captcha',
'register.captchaSendSuccess': 'Verification code sent successfully',
'register.invalidCaptcha': 'Invalid verification code',
'register.passwordsNotMatch': 'Two password entries do not match',
'register.passwordLength': 'Password length must be between 6-20 characters',
'register.usernameRequired': 'Username cannot be empty',
'register.usernameFormat': 'Invalid username format',
'register.mobileRequired': 'Mobile number cannot be empty',
'register.passwordRequired': 'Password cannot be empty',
'register.confirmPasswordRequired': 'Confirm password cannot be empty',
'register.goToLogin': 'Already have an account, go to login',
'register.agreeTo': 'register implies acceptance of',
'register.userAgreement': 'User Agreement',
'register.and': 'and',
'register.privacyPolicy': 'Privacy Policy',
'register.sendCaptcha': 'Get Verification Code',
'register.notAllowRegister': 'User registration is not allowed currently',
'register.captchaLoadFailed': 'Verification code loading failed, click to refresh',
'register.inputCorrectMobile': 'Please enter correct mobile number',
'register.inputCaptcha': 'Please enter captcha',
'register.captchaSendSuccess': 'Verification code sent successfully',
'register.captchaSendFailed': 'Failed to send verification code',
'register.registerSuccess': 'Registration successful!',
'register.registerFailed': 'Registration failed',
'register.passwordsNotMatch': 'The two passwords do not match',
'register.secondsLater': 'seconds later',
// Retrieve password page text // Retrieve password page text
'retrievePassword.title': 'Reset Password', 'retrievePassword.title': 'Reset Password',
'retrievePassword.welcome': 'PASSWORD RETRIEVE', 'retrievePassword.welcome': 'PASSWORD RETRIEVE',
@@ -505,6 +466,7 @@ export default {
'chatHistory.downloadCurrentSession': 'Download current session chat history', 'chatHistory.downloadCurrentSession': 'Download current session chat history',
'chatHistory.downloadCurrentWithPreviousSessions': 'Download current and previous 20 sessions chat history', 'chatHistory.downloadCurrentWithPreviousSessions': 'Download current and previous 20 sessions chat history',
'chatHistory.downloadLinkFailed': 'Failed to get download link', 'chatHistory.downloadLinkFailed': 'Failed to get download link',
'cache.status': 'Cache Status', 'cache.status': 'Cache Status',
'cache.cdnEnabled': 'CDN Mode Enabled', 'cache.cdnEnabled': 'CDN Mode Enabled',
'cache.cdnDisabled': 'CDN Mode Disabled', 'cache.cdnDisabled': 'CDN Mode Disabled',
@@ -914,6 +876,8 @@ export default {
'ttsModel.operationClosed': 'Operation closed', 'ttsModel.operationClosed': 'Operation closed',
'ttsModel.edit': 'Edit', 'ttsModel.edit': 'Edit',
'ttsModel.delete': 'Delete', 'ttsModel.delete': 'Delete',
'ttsModel.deleteVoiceSuccess': 'Successfully deleted {count} voices',
'ttsModel.deleteFailed': 'Failed to delete voices',
'ttsModel.save': 'Save', 'ttsModel.save': 'Save',
'ttsModel.deselectAll': 'Deselect All', 'ttsModel.deselectAll': 'Deselect All',
'ttsModel.selectAll': 'Select All', 'ttsModel.selectAll': 'Select All',
@@ -923,6 +887,8 @@ export default {
'ttsModel.unnamedVoice': 'Unnamed Voice', 'ttsModel.unnamedVoice': 'Unnamed Voice',
'ttsModel.finishEditingFirst': 'Please finish current editing first', 'ttsModel.finishEditingFirst': 'Please finish current editing first',
'ttsModel.selectVoiceToDelete': 'Please select voices to delete', 'ttsModel.selectVoiceToDelete': 'Please select voices to delete',
'ttsModel.warning': 'Warning',
'ttsModel.confirmDeleteVoice': 'Are you sure to delete {count} voices?',
// OTA Management Page Text // OTA Management Page Text
'otaManagement.firmwareManagement': 'Firmware Management', 'otaManagement.firmwareManagement': 'Firmware Management',
@@ -1057,11 +1023,97 @@ export default {
'warning': 'Warning', 'warning': 'Warning',
'info': 'Info', 'info': 'Info',
'common.networkError': 'Network request failed', 'common.networkError': 'Network request failed',
// SM2 Encryption
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator', 'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
'sm2.encryptionFailed': 'Password encryption failed', 'sm2.encryptionFailed': 'Password encryption failed',
'sm2.keyGenerationFailed': 'Key pair generation failed', 'sm2.keyGenerationFailed': 'Key pair generation failed',
'sm2.invalidPublicKey': 'Invalid public key format', 'sm2.invalidPublicKey': 'Invalid public key format',
'sm2.encryptionError': 'Error occurred during encryption', 'sm2.encryptionError': 'Error occurred during encryption',
'sm2.publicKeyRetry': 'Retrying to get public key...', 'sm2.publicKeyRetry': 'Retrying to get public key...',
'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed' 'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed',
// Voice Clone and Resource Management
'voiceClone.title': 'Voice Clone',
'voiceResource.title': 'Voice Resource',
'voiceClone.platformName': 'Platform Name',
'voiceClone.voiceId': 'Voice ID',
'voiceClone.userId': 'Account Owner',
'voiceClone.name': 'Voice Name',
'voiceClone.clone': 'Clone Voice',
'voiceClone.action': 'Action',
'voiceClone.modelId': 'Model ID',
'voiceClone.trainStatus': 'Training Status',
'voiceClone.trainError': 'Training Error',
'voiceClone.createdAt': 'Creation Time',
'voiceClone.search': 'Search',
'voiceClone.searchPlaceholder': 'Please enter voice name or voice ID',
'voiceClone.addNew': 'Add',
'voiceClone.delete': 'Delete',
'voiceClone.selectAll': 'Select All',
'voiceClone.deselectAll': 'Deselect All',
'voiceClone.addVoiceClone': 'Add Voice Resource',
'voiceClone.confirmDelete': 'Are you sure you want to delete the selected {count} voice resources?',
'voiceClone.deleteSuccess': 'Successfully deleted {count} voice resources',
'voiceClone.deleteFailed': 'Deletion failed',
'voiceClone.addSuccess': 'Add successful',
'voiceClone.addFailed': 'Add failed',
'voiceClone.updateSuccess': 'Update successful',
'voiceClone.updateFailed': 'Update failed',
'voiceClone.selectFirst': 'Please select voice resources to delete first',
'voiceClone.operationCancelled': 'Cancel',
'voiceClone.operationClosed': 'Popup closed',
'voiceClone.cloneSuccess': 'Clone successful',
'voiceClone.cloneFailed': 'Clone failed',
'voiceClone.confirmClone': 'Are you sure you want to clone this voice?',
'voiceClone.onlySuccessCanClone': 'Only successfully trained voices can be cloned',
'common.insufficient': 'Insufficient',
'voiceClone.platformNameRequired': 'Please select platform name',
'voiceClone.voiceIdRequired': 'Please select voice ID',
'voiceClone.userIdRequired': 'Please select account owner',
'voiceClone.platformNamePlaceholder': 'Please select platform name',
'voiceClone.voiceIdPlaceholder': 'Please enter voice ID and press Enter',
'voiceClone.userIdPlaceholder': 'Please enter keyword to select account owner',
'voiceClone.waitingUpload': 'Waiting for upload',
'voiceClone.waitingTraining': 'Waiting for clone',
'voiceClone.training': 'Training',
'voiceClone.trainSuccess': 'Training successful',
'voiceClone.trainFailed': 'Training failed',
'voiceClone.itemsPerPage': '{items} items per page',
'voiceClone.firstPage': 'First Page',
'voiceClone.prevPage': 'Previous Page',
'voiceClone.nextPage': 'Next Page',
'voiceClone.totalRecords': '{total} records in total',
'voiceClone.noVoiceCloneAssigned': 'Your account has no voice resources assigned',
'voiceClone.contactAdmin': 'Please contact administrator for voice resource assignment',
'voiceClone.dialogTitle': 'Voice Clone',
'voiceClone.stepUpload': 'Prepare Audio',
'voiceClone.stepEdit': 'Audio Edit',
'voiceClone.dragOrClick': 'Drag audio file here, or click to upload',
'voiceClone.uploadTip': 'Support all mainstream audio formats, selected duration must be between 8-60 seconds',
'voiceClone.editTip1': 'Please confirm if the uploaded audio is satisfactory',
'voiceClone.editTip2': 'You can listen and trim the audio, if not satisfied you can go back to re-record or upload',
'voiceClone.selectedDuration': 'Selected valid segment: {duration} seconds',
'voiceClone.trim': 'Trim selected segment',
'voiceClone.reset': 'Reset',
'voiceClone.play': 'Play',
'voiceClone.pause': 'Pause',
'voiceClone.stop': 'Stop',
'voiceClone.cancel': 'Cancel',
'voiceClone.warning': 'Warning',
'voiceClone.ok': 'OK',
'voiceClone.nextStep': 'Next',
'voiceClone.prevStep': 'Previous',
'voiceClone.upload': 'Upload Audio',
'voiceClone.pleaseSelectAudio': 'Please select an audio file first',
'voiceClone.durationError': 'Audio duration must be between 8-60 seconds',
'voiceClone.loadAudioFailed': 'Failed to load audio',
'voiceClone.trimSuccess': 'Trim successful',
'voiceClone.resetSuccess': 'Reset successful',
'voiceClone.uploadSuccess': 'Upload successful',
'voiceClone.uploadFailed': 'Upload failed',
'voiceClone.updateNameSuccess': 'Name updated successfully',
'voiceClone.updateNameFailed': 'Failed to update name',
'voiceClone.playFailed': 'Play failed',
'voiceClone.Details': 'Error Details',
} }
+138 -87
View File
@@ -8,6 +8,8 @@ export default {
// HeaderBar组件文本 // HeaderBar组件文本
'header.smartManagement': '智能体管理', 'header.smartManagement': '智能体管理',
'header.voiceCloneManagement': '音色克隆',
'header.voiceResourceManagement': '音色资源',
'header.modelConfig': '模型配置', 'header.modelConfig': '模型配置',
'header.userManagement': '用户管理', 'header.userManagement': '用户管理',
'header.otaManagement': 'OTA管理', 'header.otaManagement': 'OTA管理',
@@ -17,6 +19,11 @@ export default {
'header.agentTemplate': '默认角色模板', 'header.agentTemplate': '默认角色模板',
'header.searchHistory': '搜索历史', 'header.searchHistory': '搜索历史',
'header.clearHistory': '清空历史', 'header.clearHistory': '清空历史',
'header.providerManagement': '字段管理',
'header.serverSideManagement': '服务端管理',
'header.changePassword': '修改密码',
'header.logout': '退出登录',
'header.searchPlaceholder': '输入名称搜索..',
// McpToolCallDialog组件文本 // McpToolCallDialog组件文本
'mcpToolCall.title': '工具调用', 'mcpToolCall.title': '工具调用',
@@ -189,6 +196,23 @@ export default {
'firmwareDialog.firmwareNamePlaceholder': '请输入固件名称(板子+版本号)', 'firmwareDialog.firmwareNamePlaceholder': '请输入固件名称(板子+版本号)',
'firmwareDialog.firmwareType': '固件类型', 'firmwareDialog.firmwareType': '固件类型',
'firmwareDialog.firmwareTypePlaceholder': '请选择固件类型', 'firmwareDialog.firmwareTypePlaceholder': '请选择固件类型',
'firmwareDialog.version': '版本号',
'firmwareDialog.versionPlaceholder': '请输入版本号(x.x.x格式)',
'firmwareDialog.firmwareFile': '固件文件',
'firmwareDialog.clickUpload': '点击上传',
'firmwareDialog.uploadTip': '只能上传固件文件(.bin/.apk),且不超过100MB',
'firmwareDialog.uploadHint': '温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件',
'firmwareDialog.remark': '备注',
'firmwareDialog.remarkPlaceholder': '请输入备注信息',
'firmwareDialog.requiredFirmwareName': '请输入固件名称(板子+版本号)',
'firmwareDialog.requiredFirmwareType': '请选择固件类型',
'firmwareDialog.requiredVersion': '请输入版本号',
'firmwareDialog.versionFormatError': '版本号格式不正确,请输入x.x.x格式',
'firmwareDialog.requiredFirmwareFile': '请上传固件文件',
'firmwareDialog.invalidFileType': '只能上传.bin/.apk格式的固件文件!',
'firmwareDialog.invalidFileSize': '固件文件大小不能超过100MB!',
'firmwareDialog.uploadSuccess': '固件文件上传成功',
'firmwareDialog.uploadFailed': '文件上传失败',
// 声纹对话框相关 // 声纹对话框相关
'voicePrintDialog.addSpeaker': '添加说话人', 'voicePrintDialog.addSpeaker': '添加说话人',
@@ -230,6 +254,21 @@ export default {
// 手動添加設備對話框相關 // 手動添加設備對話框相關
'manualAddDeviceDialog.title': '手动添加设备', 'manualAddDeviceDialog.title': '手动添加设备',
'manualAddDeviceDialog.deviceType': '设备型号',
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
'manualAddDeviceDialog.firmwareVersion': '固件版本',
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本',
'manualAddDeviceDialog.macAddress': 'Mac地址',
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
'manualAddDeviceDialog.confirm': '确定',
'manualAddDeviceDialog.cancel': '取消',
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
'manualAddDeviceDialog.invalidMacAddress': '请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': '请选择设备型号',
'manualAddDeviceDialog.requiredFirmwareVersion': '请输入固件版本',
'manualAddDeviceDialog.getFirmwareTypeFailed': '获取固件类型失败',
'manualAddDeviceDialog.addSuccess': '设备添加成功',
'manualAddDeviceDialog.addFailed': '添加失败',
// AddModelDialog组件相关 // AddModelDialog组件相关
'addModelDialog.requiredSupplier': '请选择供应器', 'addModelDialog.requiredSupplier': '请选择供应器',
@@ -249,28 +288,24 @@ export default {
'register.userAgreement': '用户协议', 'register.userAgreement': '用户协议',
'register.privacyPolicy': '隐私政策', 'register.privacyPolicy': '隐私政策',
'register.notAllowRegister': '当前不允许用户注册', 'register.notAllowRegister': '当前不允许用户注册',
'register.captchaLoadFailed': '验证码加载失败', 'register.captchaLoadFailed': '验证码加载失败,点击刷新',
'register.inputCaptcha': '请输入验证码', 'register.inputCaptcha': '请输入验证码',
'register.inputCorrectMobile': '请输入正确的手机号码', 'register.inputCorrectMobile': '请输入正确的手机号码',
'register.captchaSendSuccess': '验证码发送成功', 'register.captchaSendSuccess': '验证码发送成功',
'register.captchaSendFailed': '验证码发送失败', 'register.captchaSendFailed': '验证码发送失败',
'register.passwordsNotMatch': '两次输入的密码不一致',
'register.registerSuccess': '注册成功!', 'register.registerSuccess': '注册成功!',
'register.registerFailed': '注册失败', 'register.registerFailed': '注册失败',
'register.passwordsNotMatch': '两次输入的密码不一致',
'register.requiredUsername': '用户名不能为空', 'register.requiredUsername': '用户名不能为空',
'register.requiredPassword': '密码不能为空', 'register.requiredPassword': '密码不能为空',
'register.requiredCaptcha': '验证码不能为空', 'register.requiredCaptcha': '验证码不能为空',
'register.requiredMobileCaptcha': '请输入短信验证码', 'register.requiredMobileCaptcha': '请输入短信验证码',
'register.invalidCaptcha': '验证码不正确',
'manualAddDeviceDialog.deviceType': '设备型号', 'register.passwordLength': '密码长度必须在6-20位之间',
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号', 'register.usernameFormat': '用户名格式不正确',
'manualAddDeviceDialog.firmwareVersion': '固件版本', 'register.confirmPasswordRequired': '确认密码不能为空',
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本', 'register.sendCaptcha': '获取验证码',
'manualAddDeviceDialog.macAddress': 'Mac地址', 'register.secondsLater': '秒后',
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
'manualAddDeviceDialog.confirm': '确定',
'manualAddDeviceDialog.cancel': '取消',
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
// 参数对话框相关 // 参数对话框相关
'paramDialog.paramCode': '参数编码', 'paramDialog.paramCode': '参数编码',
@@ -291,34 +326,6 @@ export default {
'paramDialog.booleanType': '布尔值(boolean)', 'paramDialog.booleanType': '布尔值(boolean)',
'paramDialog.arrayType': '数组(array)', 'paramDialog.arrayType': '数组(array)',
'paramDialog.jsonType': 'JSON对象(json)', 'paramDialog.jsonType': 'JSON对象(json)',
'manualAddDeviceDialog.invalidMacAddress': '请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': '请选择设备型号',
'manualAddDeviceDialog.requiredFirmwareVersion': '请输入固件版本',
'manualAddDeviceDialog.getFirmwareTypeFailed': '获取固件类型失败',
'manualAddDeviceDialog.addSuccess': '设备添加成功',
'manualAddDeviceDialog.addFailed': '添加失败',
'firmwareDialog.version': '版本号',
'firmwareDialog.versionPlaceholder': '请输入版本号(x.x.x格式)',
'firmwareDialog.firmwareFile': '固件文件',
'firmwareDialog.clickUpload': '点击上传',
'firmwareDialog.uploadTip': '只能上传固件文件(.bin/.apk),且不超过100MB',
'firmwareDialog.uploadHint': '温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件',
'firmwareDialog.remark': '备注',
'firmwareDialog.remarkPlaceholder': '请输入备注信息',
'firmwareDialog.requiredFirmwareName': '请输入固件名称(板子+版本号)',
'firmwareDialog.requiredFirmwareType': '请选择固件类型',
'firmwareDialog.requiredVersion': '请输入版本号',
'firmwareDialog.versionFormatError': '版本号格式不正确,请输入x.x.x格式',
'firmwareDialog.requiredFirmwareFile': '请上传固件文件',
'firmwareDialog.invalidFileType': '只能上传.bin/.apk格式的固件文件!',
'firmwareDialog.invalidFileSize': '固件文件大小不能超过100MB!',
'firmwareDialog.uploadSuccess': '固件文件上传成功',
'firmwareDialog.uploadFailed': '文件上传失败',
'header.providerManagement': '字段管理',
'header.serverSideManagement': '服务端管理',
'header.changePassword': '修改密码',
'header.logout': '退出登录',
'header.searchPlaceholder': '输入名称搜索..',
// 登录页面文本 // 登录页面文本
'login.title': '登录', 'login.title': '登录',
@@ -342,52 +349,6 @@ export default {
'login.privacyPolicy': '《隐私政策》', 'login.privacyPolicy': '《隐私政策》',
'login.registerAccount': '注册账号', 'login.registerAccount': '注册账号',
// 注册页面文本
'register.title': '注册账号',
'register.welcome': '欢迎注册',
'register.username': '用户名',
'register.usernamePlaceholder': '请输入用户名',
'register.mobile': '手机号',
'register.mobilePlaceholder': '请输入手机号码',
'register.password': '密码',
'register.passwordPlaceholder': '请输入密码',
'register.confirmPassword': '确认密码',
'register.confirmPasswordPlaceholder': '请确认密码',
'register.captcha': '验证码',
'register.captchaPlaceholder': '请输入验证码',
'register.mobileCaptcha': '手机验证码',
'register.mobileCaptchaPlaceholder': '请输入手机验证码',
'register.getMobileCaptcha': '获取验证码',
'register.registerButton': '注册',
'register.login': '登录',
'register.inputCorrectMobile': '请输入正确的手机号码',
'register.inputCaptcha': '请输入图形验证码',
'register.captchaSendSuccess': '验证码发送成功',
'register.invalidCaptcha': '验证码不正确',
'register.passwordsNotMatch': '两次输入的密码不一致',
'register.passwordLength': '密码长度必须在6-20位之间',
'register.usernameRequired': '用户名不能为空',
'register.usernameFormat': '用户名格式不正确',
'register.mobileRequired': '手机号不能为空',
'register.passwordRequired': '密码不能为空',
'register.confirmPasswordRequired': '确认密码不能为空',
'register.goToLogin': '已有账号,去登录',
'register.agreeTo': '注册即同意',
'register.userAgreement': '《用户协议》',
'register.and': '和',
'register.privacyPolicy': '《隐私政策》',
'register.sendCaptcha': '获取验证码',
'register.notAllowRegister': '当前不允许普通用户注册',
'register.captchaLoadFailed': '验证码加载失败,点击刷新',
'register.inputCorrectMobile': '请输入正确的手机号码',
'register.inputCaptcha': '请输入图形验证码',
'register.captchaSendSuccess': '验证码发送成功',
'register.captchaSendFailed': '验证码发送失败',
'register.registerSuccess': '注册成功!',
'register.registerFailed': '注册失败',
'register.passwordsNotMatch': '两次输入的密码不一致',
'register.secondsLater': '秒后',
// 忘记密码页面文本 // 忘记密码页面文本
'retrievePassword.title': '重置密码', 'retrievePassword.title': '重置密码',
'retrievePassword.welcome': '密码找回', 'retrievePassword.welcome': '密码找回',
@@ -505,6 +466,7 @@ export default {
'chatHistory.downloadCurrentSession': '下载本会话聊天记录', 'chatHistory.downloadCurrentSession': '下载本会话聊天记录',
'chatHistory.downloadCurrentWithPreviousSessions': '下载本会话及前20条会话聊天记录', 'chatHistory.downloadCurrentWithPreviousSessions': '下载本会话及前20条会话聊天记录',
'chatHistory.downloadLinkFailed': '获取下载链接失败', 'chatHistory.downloadLinkFailed': '获取下载链接失败',
'cache.status': '缓存状态', 'cache.status': '缓存状态',
'cache.cdnEnabled': 'CDN模式已启用', 'cache.cdnEnabled': 'CDN模式已启用',
'cache.cdnDisabled': 'CDN模式已禁用', 'cache.cdnDisabled': 'CDN模式已禁用',
@@ -914,6 +876,8 @@ export default {
'ttsModel.operationClosed': '操作已关闭', 'ttsModel.operationClosed': '操作已关闭',
'ttsModel.edit': '编辑', 'ttsModel.edit': '编辑',
'ttsModel.delete': '删除', 'ttsModel.delete': '删除',
'ttsModel.deleteVoiceSuccess': '成功删除{count}个音色',
'ttsModel.deleteFailed': '删除音色失败',
'ttsModel.save': '保存', 'ttsModel.save': '保存',
'ttsModel.deselectAll': '取消全选', 'ttsModel.deselectAll': '取消全选',
'ttsModel.selectAll': '全选', 'ttsModel.selectAll': '全选',
@@ -923,6 +887,8 @@ export default {
'ttsModel.unnamedVoice': '未命名音色', 'ttsModel.unnamedVoice': '未命名音色',
'ttsModel.finishEditingFirst': '请先完成当前编辑', 'ttsModel.finishEditingFirst': '请先完成当前编辑',
'ttsModel.selectVoiceToDelete': '请选择要删除的音色', 'ttsModel.selectVoiceToDelete': '请选择要删除的音色',
'ttsModel.warning': '警告',
'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?',
// OTA管理页面文本 // OTA管理页面文本
'otaManagement.firmwareManagement': '固件管理', 'otaManagement.firmwareManagement': '固件管理',
@@ -1057,6 +1023,7 @@ export default {
'warning': '警告', 'warning': '警告',
'info': '提示', 'info': '提示',
'common.networkError': '网络请求失败', 'common.networkError': '网络请求失败',
// SM2加密相关错误消息 // SM2加密相关错误消息
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员', 'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
'sm2.encryptionFailed': '密码加密失败', 'sm2.encryptionFailed': '密码加密失败',
@@ -1064,5 +1031,89 @@ export default {
'sm2.invalidPublicKey': '无效的公钥格式', 'sm2.invalidPublicKey': '无效的公钥格式',
'sm2.encryptionError': '加密过程中发生错误', 'sm2.encryptionError': '加密过程中发生错误',
'sm2.publicKeyRetry': '正在重试获取公钥...', 'sm2.publicKeyRetry': '正在重试获取公钥...',
'sm2.publicKeyRetryFailed': '公钥获取重试失败' 'sm2.publicKeyRetryFailed': '公钥获取重试失败',
// 音色资源管理
'voiceClone.title': '音色克隆',
'voiceResource.title': '音色资源',
'voiceClone.platformName': '平台名称',
'voiceClone.voiceId': '声音ID',
'voiceClone.userId': '归属账号',
'voiceClone.name': '声音名称',
'voiceClone.clone': '立即复刻',
'voiceClone.modelId': '模型ID',
'voiceClone.trainStatus': '训练状态',
'voiceClone.trainError': '训练错误',
'voiceClone.createdAt': '创建时间',
'voiceClone.search': '搜索',
'voiceClone.searchPlaceholder': '请输入声音名称或音色ID',
'voiceClone.addNew': '新增',
'voiceClone.delete': '删除',
'voiceClone.selectAll': '全选',
'voiceClone.deselectAll': '取消全选',
'voiceClone.addVoiceClone': '新增音色资源',
'voiceClone.confirmDelete': '确定要删除选中的 {count} 条音色资源吗?',
'voiceClone.deleteSuccess': '成功删除 {count} 条音色资源',
'voiceClone.deleteFailed': '删除失败',
'voiceClone.addSuccess': '添加成功',
'voiceClone.addFailed': '添加失败',
'voiceClone.updateSuccess': '更新成功',
'voiceClone.updateFailed': '更新失败',
'voiceClone.selectFirst': '请先选择要删除的音色资源',
'voiceClone.operationCancelled': '取消',
'voiceClone.operationClosed': '已关闭弹窗',
'voiceClone.action': '操作',
'voiceClone.cloneSuccess': '复刻成功',
'voiceClone.cloneFailed': '复刻失败',
'voiceClone.confirmClone': '确定要复刻此音色吗?',
'voiceClone.onlySuccessCanClone': '只有训练成功的音色才能复刻',
'common.insufficient': '不足',
'voiceClone.platformNameRequired': '请选择平台名称',
'voiceClone.voiceIdRequired': '请选择音色ID',
'voiceClone.userIdRequired': '请选择归属账号',
'voiceClone.platformNamePlaceholder': '请选择平台名称',
'voiceClone.voiceIdPlaceholder': '请输入音色ID并按回车',
'voiceClone.userIdPlaceholder': '请输入关键词选择归属账号',
'voiceClone.waitingUpload': '待上传',
'voiceClone.waitingTraining': '待复刻',
'voiceClone.training': '训练中',
'voiceClone.trainSuccess': '训练成功',
'voiceClone.trainFailed': '训练失败',
'voiceClone.itemsPerPage': '每页 {items} 条',
'voiceClone.firstPage': '首页',
'voiceClone.prevPage': '上一页',
'voiceClone.nextPage': '下一页',
'voiceClone.totalRecords': '共 {total} 条',
'voiceClone.noVoiceCloneAssigned': '您的账号暂无音色资源',
'voiceClone.contactAdmin': '请联系管理员分配音色资源',
'voiceClone.dialogTitle': '声音复刻',
'voiceClone.stepUpload': '准备音频',
'voiceClone.stepEdit': '音频编辑',
'voiceClone.dragOrClick': '将音频文件拖到此处,或点击上传',
'voiceClone.uploadTip': '支持所有主流音频格式,选区时长需要在8-60秒之间',
'voiceClone.editTip1': '请确认上传音频是否满意',
'voiceClone.editTip2': '您可以试听并裁剪音频,如果不满意可以返回重新录制或上传',
'voiceClone.selectedDuration': '已选择有效片段:{duration}秒',
'voiceClone.trim': '对选择区域进行裁剪',
'voiceClone.reset': '重置',
'voiceClone.play': '播放',
'voiceClone.pause': '暂停',
'voiceClone.stop': '停止',
'voiceClone.cancel': '取消',
'voiceClone.warning': '警告',
'voiceClone.ok': '确定',
'voiceClone.nextStep': '下一步',
'voiceClone.prevStep': '上一步',
'voiceClone.upload': '上传音频',
'voiceClone.pleaseSelectAudio': '请先选择音频文件',
'voiceClone.durationError': '音频时长必须在8-60秒之间',
'voiceClone.loadAudioFailed': '加载音频失败',
'voiceClone.trimSuccess': '裁剪成功',
'voiceClone.resetSuccess': '重置成功',
'voiceClone.uploadSuccess': '上传成功',
'voiceClone.uploadFailed': '上传失败',
'voiceClone.updateNameSuccess': '名称更新成功',
'voiceClone.updateNameFailed': '名称更新失败',
'voiceClone.playFailed': '播放失败',
'voiceClone.Details': '错误详情',
} }
+135 -85
View File
@@ -10,13 +10,20 @@ export default {
'header.smartManagement': '智能體管理', 'header.smartManagement': '智能體管理',
'header.modelConfig': '模型配置', 'header.modelConfig': '模型配置',
'header.userManagement': '用戶管理', 'header.userManagement': '用戶管理',
'header.voiceCloneManagement': '音色克隆',
'header.voiceResourceManagement': '音色資源',
'header.otaManagement': 'OTA管理', 'header.otaManagement': 'OTA管理',
'header.paramDictionary': '參數字典', 'header.paramDictionary': '參數字典',
'header.paramManagement': '參數管理', 'header.paramManagement': '參數管理',
'header.dictManagement': '字典管理', 'header.dictManagement': '字典管理',
'header.agentTemplate': '預設角色模板', // 添加这一行 'header.agentTemplate': '預設角色模板',
'header.searchHistory': '搜索歷史', 'header.searchHistory': '搜索歷史',
'header.clearHistory': '清空歷史', 'header.clearHistory': '清空歷史',
'header.providerManagement': '字段管理',
'header.serverSideManagement': '服務端管理',
'header.changePassword': '修改密碼',
'header.logout': '退出登錄',
'header.searchPlaceholder': '輸入名稱搜索..',
// McpToolCallDialog组件文本 // McpToolCallDialog组件文本
'mcpToolCall.title': '工具調用', 'mcpToolCall.title': '工具調用',
@@ -189,6 +196,23 @@ export default {
'firmwareDialog.firmwareNamePlaceholder': '請輸入固件名稱(板子+版本號)', 'firmwareDialog.firmwareNamePlaceholder': '請輸入固件名稱(板子+版本號)',
'firmwareDialog.firmwareType': '固件類型', 'firmwareDialog.firmwareType': '固件類型',
'firmwareDialog.firmwareTypePlaceholder': '請選擇固件類型', 'firmwareDialog.firmwareTypePlaceholder': '請選擇固件類型',
'firmwareDialog.version': '版本號',
'firmwareDialog.versionPlaceholder': '請輸入版本號(x.x.x格式)',
'firmwareDialog.firmwareFile': '固件文件',
'firmwareDialog.clickUpload': '點擊上傳',
'firmwareDialog.uploadTip': '只能上傳固件文件(.bin/.apk),且不超過100MB',
'firmwareDialog.uploadHint': '溫馨提示:請上傳合併前的xiaozhi.bin文件,而不是合併後的merged-binary.bin文件',
'firmwareDialog.remark': '備註',
'firmwareDialog.remarkPlaceholder': '請輸入備註信息',
'firmwareDialog.requiredFirmwareName': '請輸入固件名稱(板子+版本號)',
'firmwareDialog.requiredFirmwareType': '請選擇固件類型',
'firmwareDialog.requiredVersion': '請輸入版本號',
'firmwareDialog.versionFormatError': '版本號格式不正確,請輸入x.x.x格式',
'firmwareDialog.requiredFirmwareFile': '請上傳固件文件',
'firmwareDialog.invalidFileType': '只能上傳.bin/.apk格式的固件文件!',
'firmwareDialog.invalidFileSize': '固件文件大小不能超過100MB!',
'firmwareDialog.uploadSuccess': '固件文件上傳成功',
'firmwareDialog.uploadFailed': '文件上傳失敗',
// 聲紋對話框相關 // 聲紋對話框相關
'voicePrintDialog.addSpeaker': '添加說話人', 'voicePrintDialog.addSpeaker': '添加說話人',
@@ -230,6 +254,21 @@ export default {
// 手動添加設備對話框相關 // 手動添加設備對話框相關
'manualAddDeviceDialog.title': '手動添加設備', 'manualAddDeviceDialog.title': '手動添加設備',
'manualAddDeviceDialog.deviceType': '設備型號',
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號',
'manualAddDeviceDialog.firmwareVersion': '固件版本',
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本',
'manualAddDeviceDialog.macAddress': 'Mac地址',
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
'manualAddDeviceDialog.confirm': '確定',
'manualAddDeviceDialog.cancel': '取消',
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
'manualAddDeviceDialog.invalidMacAddress': '請輸入正確的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': '請選擇設備型號',
'manualAddDeviceDialog.requiredFirmwareVersion': '請輸入固件版本',
'manualAddDeviceDialog.getFirmwareTypeFailed': '獲取固件類型失敗',
'manualAddDeviceDialog.addSuccess': '設備添加成功',
'manualAddDeviceDialog.addFailed': '添加失敗',
// AddModelDialog組件相關 // AddModelDialog組件相關
'addModelDialog.requiredSupplier': '請選擇供應器', 'addModelDialog.requiredSupplier': '請選擇供應器',
@@ -262,16 +301,11 @@ export default {
'register.requiredPassword': '密碼不能為空', 'register.requiredPassword': '密碼不能為空',
'register.requiredCaptcha': '驗證碼不能為空', 'register.requiredCaptcha': '驗證碼不能為空',
'register.requiredMobileCaptcha': '請輸入簡訊驗證碼', 'register.requiredMobileCaptcha': '請輸入簡訊驗證碼',
'register.invalidCaptcha': '驗證碼不正確',
'manualAddDeviceDialog.deviceType': '設備型號', 'register.passwordLength': '密碼長度必須在6-20位之間',
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號', 'register.usernameFormat': '用戶名格式不正確',
'manualAddDeviceDialog.firmwareVersion': '固件版本', 'register.confirmPasswordRequired': '確認密碼不能為空',
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本', 'register.sendCaptcha': '獲取驗證碼',
'manualAddDeviceDialog.macAddress': 'Mac地址',
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
'manualAddDeviceDialog.confirm': '確定',
'manualAddDeviceDialog.cancel': '取消',
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
// 參數對話框相關 // 參數對話框相關
'paramDialog.paramCode': '參數編碼', 'paramDialog.paramCode': '參數編碼',
@@ -292,34 +326,6 @@ export default {
'paramDialog.booleanType': '布林值(boolean)', 'paramDialog.booleanType': '布林值(boolean)',
'paramDialog.arrayType': '陣列(array)', 'paramDialog.arrayType': '陣列(array)',
'paramDialog.jsonType': 'JSON物件(json)', 'paramDialog.jsonType': 'JSON物件(json)',
'manualAddDeviceDialog.invalidMacAddress': '請輸入正確的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
'manualAddDeviceDialog.requiredDeviceType': '請選擇設備型號',
'manualAddDeviceDialog.requiredFirmwareVersion': '請輸入固件版本',
'manualAddDeviceDialog.getFirmwareTypeFailed': '獲取固件類型失敗',
'manualAddDeviceDialog.addSuccess': '設備添加成功',
'manualAddDeviceDialog.addFailed': '添加失敗',
'firmwareDialog.version': '版本號',
'firmwareDialog.versionPlaceholder': '請輸入版本號(x.x.x格式)',
'firmwareDialog.firmwareFile': '固件文件',
'firmwareDialog.clickUpload': '點擊上傳',
'firmwareDialog.uploadTip': '只能上傳固件文件(.bin/.apk),且不超過100MB',
'firmwareDialog.uploadHint': '溫馨提示:請上傳合併前的xiaozhi.bin文件,而不是合併後的merged-binary.bin文件',
'firmwareDialog.remark': '備註',
'firmwareDialog.remarkPlaceholder': '請輸入備註信息',
'firmwareDialog.requiredFirmwareName': '請輸入固件名稱(板子+版本號)',
'firmwareDialog.requiredFirmwareType': '請選擇固件類型',
'firmwareDialog.requiredVersion': '請輸入版本號',
'firmwareDialog.versionFormatError': '版本號格式不正確,請輸入x.x.x格式',
'firmwareDialog.requiredFirmwareFile': '請上傳固件文件',
'firmwareDialog.invalidFileType': '只能上傳.bin/.apk格式的固件文件!',
'firmwareDialog.invalidFileSize': '固件文件大小不能超過100MB!',
'firmwareDialog.uploadSuccess': '固件文件上傳成功',
'firmwareDialog.uploadFailed': '文件上傳失敗',
'header.providerManagement': '字段管理',
'header.serverSideManagement': '服務端管理',
'header.changePassword': '修改密碼',
'header.logout': '退出登錄',
'header.searchPlaceholder': '輸入名稱搜索..',
// 登录页面文本 // 登录页面文本
'login.title': '登錄', 'login.title': '登錄',
@@ -343,51 +349,6 @@ export default {
'login.privacyPolicy': '《隱私政策》', 'login.privacyPolicy': '《隱私政策》',
'login.registerAccount': '註冊賬號', 'login.registerAccount': '註冊賬號',
// 注册页面文本
'register.title': '註冊賬號',
'register.welcome': '歡迎註冊',
'register.username': '用戶名',
'register.usernamePlaceholder': '請輸入用戶名',
'register.mobile': '手機號',
'register.mobilePlaceholder': '請輸入手機號碼',
'register.password': '密碼',
'register.passwordPlaceholder': '請輸入密碼',
'register.confirmPassword': '確認密碼',
'register.confirmPasswordPlaceholder': '請確認密碼',
'register.captcha': '驗證碼',
'register.captchaPlaceholder': '請輸入驗證碼',
'register.mobileCaptcha': '手機驗證碼',
'register.mobileCaptchaPlaceholder': '請輸入手機驗證碼',
'register.getMobileCaptcha': '獲取驗證碼',
'register.registerButton': '註冊',
'register.login': '登錄',
'register.inputCorrectMobile': '請輸入正確的手機號碼',
'register.inputCaptcha': '請輸入圖形驗證碼',
'register.captchaSendSuccess': '驗證碼發送成功',
'register.invalidCaptcha': '驗證碼不正確',
'register.passwordsNotMatch': '兩次輸入的密碼不一致',
'register.passwordLength': '密碼長度必須在6-20位之間',
'register.usernameRequired': '用戶名不能為空',
'register.usernameFormat': '用戶名格式不正確',
'register.mobileRequired': '手機號不能為空',
'register.passwordRequired': '密碼不能為空',
'register.confirmPasswordRequired': '確認密碼不能為空',
'register.goToLogin': '已有賬號,去登錄',
'register.agreeTo': '註冊即同意',
'register.userAgreement': '《用戶協議》',
'register.and': '和',
'register.privacyPolicy': '《隱私政策》',
'register.sendCaptcha': '獲取驗證碼',
'register.notAllowRegister': '當前不允許普通用戶註冊',
'register.captchaLoadFailed': '驗證碼加載失敗,點擊刷新',
'register.inputCorrectMobile': '請輸入正確的手機號碼',
'register.inputCaptcha': '請輸入圖形驗證碼',
'register.captchaSendSuccess': '驗證碼發送成功',
'register.captchaSendFailed': '驗證碼發送失敗',
'register.registerSuccess': '註冊成功!',
'register.registerFailed': '註冊失敗',
'register.passwordsNotMatch': '兩次輸入的密碼不一致',
// 忘记密码页面文本 // 忘记密码页面文本
'retrievePassword.title': '重置密碼', 'retrievePassword.title': '重置密碼',
'retrievePassword.welcome': '密碼找回', 'retrievePassword.welcome': '密碼找回',
@@ -505,6 +466,7 @@ export default {
'chatHistory.downloadCurrentSession': '下載本會話聊天記錄', 'chatHistory.downloadCurrentSession': '下載本會話聊天記錄',
'chatHistory.downloadCurrentWithPreviousSessions': '下載本會話及前20條會話聊天記錄', 'chatHistory.downloadCurrentWithPreviousSessions': '下載本會話及前20條會話聊天記錄',
'chatHistory.downloadLinkFailed': '獲取下載鏈接失敗', 'chatHistory.downloadLinkFailed': '獲取下載鏈接失敗',
'cache.status': '緩存狀態', 'cache.status': '緩存狀態',
'cache.cdnEnabled': 'CDN模式已啟用', 'cache.cdnEnabled': 'CDN模式已啟用',
'cache.cdnDisabled': 'CDN模式已禁用', 'cache.cdnDisabled': 'CDN模式已禁用',
@@ -912,6 +874,8 @@ export default {
'ttsModel.operation': '操作', 'ttsModel.operation': '操作',
'ttsModel.edit': '編輯', 'ttsModel.edit': '編輯',
'ttsModel.delete': '刪除', 'ttsModel.delete': '刪除',
'ttsModel.deleteVoiceSuccess': '成功刪除{count}個音色',
'ttsModel.deleteFailed': '刪除音色失敗',
'ttsModel.save': '保存', 'ttsModel.save': '保存',
'ttsModel.deselectAll': '取消全選', 'ttsModel.deselectAll': '取消全選',
'ttsModel.selectAll': '全選', 'ttsModel.selectAll': '全選',
@@ -921,6 +885,8 @@ export default {
'ttsModel.unnamedVoice': '未命名音色', 'ttsModel.unnamedVoice': '未命名音色',
'ttsModel.finishEditingFirst': '請先完成目前編輯', 'ttsModel.finishEditingFirst': '請先完成目前編輯',
'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色', 'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色',
'ttsModel.warning': '警告',
'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?',
'ttsModel.operationFailed': '操作失敗', 'ttsModel.operationFailed': '操作失敗',
'ttsModel.operationClosed': '操作已關閉', 'ttsModel.operationClosed': '操作已關閉',
@@ -1065,5 +1031,89 @@ export default {
'sm2.invalidPublicKey': '無效的公鑰格式', 'sm2.invalidPublicKey': '無效的公鑰格式',
'sm2.encryptionError': '加密過程中發生錯誤', 'sm2.encryptionError': '加密過程中發生錯誤',
'sm2.publicKeyRetry': '正在重試獲取公鑰...', 'sm2.publicKeyRetry': '正在重試獲取公鑰...',
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗' 'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗',
// 音色資源管理
'voiceClone.title': '音色克隆',
'voiceResource.title': '音色資源',
'voiceClone.platformName': '平台名稱',
'voiceClone.voiceId': '聲音ID',
'voiceClone.userId': '歸屬帳號',
'voiceClone.name': '聲音名稱',
'voiceClone.clone': '立即複刻',
'voiceClone.action': '操作',
'voiceClone.modelId': '模型ID',
'voiceClone.trainStatus': '訓練狀態',
'voiceClone.trainError': '訓練錯誤',
'voiceClone.createdAt': '建立時間',
'voiceClone.search': '搜尋',
'voiceClone.searchPlaceholder': '請輸入聲音名稱或音色ID',
'voiceClone.addNew': '新增',
'voiceClone.delete': '刪除',
'voiceClone.selectAll': '全選',
'voiceClone.deselectAll': '取消全選',
'voiceClone.addVoiceClone': '新增音色資源',
'voiceClone.confirmDelete': '確定要刪除選中的 {count} 條音色資源嗎?',
'voiceClone.deleteSuccess': '成功刪除 {count} 條音色資源',
'voiceClone.deleteFailed': '刪除失敗',
'voiceClone.addSuccess': '新增成功',
'voiceClone.addFailed': '新增失敗',
'voiceClone.updateSuccess': '更新成功',
'voiceClone.updateFailed': '更新失敗',
'voiceClone.selectFirst': '請先選擇要刪除的音色資源',
'voiceClone.operationCancelled': '取消',
'voiceClone.operationClosed': '已關閉彈窗',
'voiceClone.cloneSuccess': '複刻成功',
'voiceClone.cloneFailed': '複刻失敗',
'voiceClone.confirmClone': '確定要複刻此音色嗎?',
'voiceClone.onlySuccessCanClone': '只有訓練成功的音色才能複刻',
'common.insufficient': '不足',
'voiceClone.platformNameRequired': '請選擇平台名稱',
'voiceClone.voiceIdRequired': '請選擇音色ID',
'voiceClone.userIdRequired': '請選擇歸屬帳號',
'voiceClone.platformNamePlaceholder': '請選擇平台名稱',
'voiceClone.voiceIdPlaceholder': '請輸入音色ID並按回车',
'voiceClone.userIdPlaceholder': '請輸入关键词選擇歸屬帳號',
'voiceClone.waitingUpload': '待上傳',
'voiceClone.waitingTraining': '待複刻',
'voiceClone.training': '訓練中',
'voiceClone.trainSuccess': '訓練成功',
'voiceClone.trainFailed': '訓練失敗',
'voiceClone.itemsPerPage': '每頁 {items} 條',
'voiceClone.firstPage': '首頁',
'voiceClone.prevPage': '上一頁',
'voiceClone.nextPage': '下一頁',
'voiceClone.totalRecords': '共 {total} 條',
'voiceClone.noVoiceCloneAssigned': '您的帳號暂无音色資源',
'voiceClone.contactAdmin': '請聯繫管理員分配音色資源',
'voiceClone.dialogTitle': '聲音複刻',
'voiceClone.stepUpload': '準備音頻',
'voiceClone.stepEdit': '音頻編輯',
'voiceClone.dragOrClick': '將音頻文件拖到此處,或點擊上傳',
'voiceClone.uploadTip': '支持所有主流音頻格式,選區時長需要在8-60秒之間',
'voiceClone.editTip1': '請確認上傳音頻是否滿意',
'voiceClone.editTip2': '您可以試聽並裁剪音頻,如果不滿意可以返回重新錄製或上傳',
'voiceClone.selectedDuration': '已選擇有效片段:{duration}秒',
'voiceClone.trim': '對選擇區域進行裁剪',
'voiceClone.reset': '重置',
'voiceClone.play': '播放',
'voiceClone.pause': '暫停',
'voiceClone.stop': '停止',
'voiceClone.cancel': '取消',
'voiceClone.warning': '警告',
'voiceClone.ok': '確定',
'voiceClone.nextStep': '下一步',
'voiceClone.prevStep': '上一步',
'voiceClone.upload': '上傳音頻',
'voiceClone.pleaseSelectAudio': '請先選擇音頻文件',
'voiceClone.durationError': '音頻時長必須在8-60秒之間',
'voiceClone.loadAudioFailed': '加載音頻失敗',
'voiceClone.trimSuccess': '裁剪成功',
'voiceClone.resetSuccess': '重置成功',
'voiceClone.uploadSuccess': '上傳成功',
'voiceClone.uploadFailed': '上傳失敗',
'voiceClone.updateNameSuccess': '名稱更新成功',
'voiceClone.updateNameFailed': '名稱更新失敗',
'voiceClone.playFailed': '播放失敗',
'voiceClone.Details': '錯誤詳情',
} }
+23 -1
View File
@@ -18,7 +18,7 @@ const routes = [
return import('../views/roleConfig.vue') return import('../views/roleConfig.vue')
} }
}, },
{ {
path: '/voice-print', path: '/voice-print',
name: 'VoicePrint', name: 'VoicePrint',
component: function () { component: function () {
@@ -110,6 +110,28 @@ const routes = [
title: 'OTA管理' title: 'OTA管理'
} }
}, },
{
path: '/voice-resource-management',
name: 'VoiceResourceManagement',
component: function () {
return import('../views/VoiceResourceManagement.vue')
},
meta: {
requiresAuth: true,
title: '音色资源开通'
}
},
{
path: '/voice-clone-management',
name: 'VoiceCloneManagement',
component: function () {
return import('../views/VoiceCloneManagement.vue')
},
meta: {
requiresAuth: true,
title: '音色克隆管理'
}
},
{ {
path: '/dict-management', path: '/dict-management',
name: 'DictManagement', name: 'DictManagement',
+1
View File
@@ -136,6 +136,7 @@ export function getUUID() {
}) })
} }
/** /**
* 验证手机号格式 * 验证手机号格式
* @param {string} mobile 手机号 * @param {string} mobile 手机号
+23 -33
View File
@@ -64,9 +64,6 @@
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)"> <el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
{{ $t('device.unbind') }} {{ $t('device.unbind') }}
</el-button> </el-button>
<el-button v-if="scope.row.deviceStatus === 'online'" size="mini" type="text" @click="handleMcpToolCall(scope.row.device_id)">
{{ $t('device.toolCall') }}
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -82,9 +79,9 @@
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice"> <el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
{{ $t('device.manualAdd') }} {{ $t('device.manualAdd') }}
</el-button> </el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{ <el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">
$t('device.unbind') {{ $t('device.unbind') }}
}}</el-button> </el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
@@ -92,20 +89,22 @@
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item"> :label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
</el-option> </el-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{ <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
$t('dictManagement.firstPage') {{ $t('dictManagement.firstPage') }}
}}</button> </button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{ <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
$t('dictManagement.prevPage') {{ $t('dictManagement.prevPage') }}
}}</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{ <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
$t('dictManagement.nextPage') }}</button> {{ $t('dictManagement.nextPage') }}
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) </button>
}}</span> <span class="total-text">
{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}
</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -117,7 +116,6 @@
@refresh="fetchBindDevices(currentAgentId)" /> @refresh="fetchBindDevices(currentAgentId)" />
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId" <ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
@refresh="fetchBindDevices(currentAgentId)" /> @refresh="fetchBindDevices(currentAgentId)" />
<McpToolCallDialog :visible.sync="mcpToolCallDialogVisible" :device-id="selectedDeviceId" />
</div> </div>
</template> </template>
@@ -127,20 +125,17 @@ import Api from '@/apis/api';
import AddDeviceDialog from "@/components/AddDeviceDialog.vue"; import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue"; import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
import McpToolCallDialog from "@/components/McpToolCallDialog.vue";
export default { export default {
components: { components: {
HeaderBar, HeaderBar,
AddDeviceDialog, AddDeviceDialog,
ManualAddDeviceDialog, ManualAddDeviceDialog
McpToolCallDialog
}, },
data() { data() {
return { return {
addDeviceDialogVisible: false, addDeviceDialogVisible: false,
manualAddDeviceDialogVisible: false, manualAddDeviceDialogVisible: false,
mcpToolCallDialogVisible: false,
selectedDeviceId: '', selectedDeviceId: '',
searchKeyword: "", searchKeyword: "",
activeSearchKeyword: "", activeSearchKeyword: "",
@@ -280,11 +275,6 @@ export default {
handleManualAddDevice() { handleManualAddDevice() {
this.manualAddDeviceDialogVisible = true; this.manualAddDeviceDialogVisible = true;
}, },
handleMcpToolCall(deviceId) {
this.selectedDeviceId = deviceId;
this.mcpToolCallDialogVisible = true;
},
submitRemark(row) { submitRemark(row) {
if (row._submitting) return; if (row._submitting) return;
@@ -383,7 +373,7 @@ export default {
.sort((a, b) => a.rawBindTime - b.rawBindTime); .sort((a, b) => a.rawBindTime - b.rawBindTime);
this.activeSearchKeyword = ""; this.activeSearchKeyword = "";
this.searchKeyword = ""; this.searchKeyword = "";
// 获取设备列表后,立即获取设备状态 // 获取设备列表后,立即获取设备状态
this.fetchDeviceStatus(agentId); this.fetchDeviceStatus(agentId);
} else { } else {
@@ -391,7 +381,7 @@ export default {
} }
}); });
}, },
// 获取设备状态 // 获取设备状态
fetchDeviceStatus(agentId) { fetchDeviceStatus(agentId) {
Api.device.getDeviceStatus(agentId, ({ data }) => { Api.device.getDeviceStatus(agentId, ({ data }) => {
@@ -399,7 +389,7 @@ export default {
try { try {
// 解析后端返回的设备状态JSON // 解析后端返回的设备状态JSON
const statusData = JSON.parse(data.data); const statusData = JSON.parse(data.data);
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装) // 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
if (statusData && typeof statusData === 'object') { if (statusData && typeof statusData === 'object') {
// 更新设备状态 // 更新设备状态
@@ -411,7 +401,7 @@ export default {
} }
}); });
}, },
// 根据API响应更新设备状态 // 根据API响应更新设备状态
updateDeviceStatusFromResponse(deviceStatusMap) { updateDeviceStatusFromResponse(deviceStatusMap) {
this.deviceList.forEach(device => { this.deviceList.forEach(device => {
@@ -419,11 +409,11 @@ export default {
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown'; const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default'; const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`; const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
// 从状态映射中获取设备状态 // 从状态映射中获取设备状态
if (deviceStatusMap[mqttClientId]) { if (deviceStatusMap[mqttClientId]) {
const statusInfo = deviceStatusMap[mqttClientId]; const statusInfo = deviceStatusMap[mqttClientId];
let isOnline = false; let isOnline = false;
if (statusInfo.isAlive === true) { if (statusInfo.isAlive === true) {
isOnline = true; isOnline = true;
@@ -434,7 +424,7 @@ export default {
} else { } else {
isOnline = false; isOnline = false;
} }
device.deviceStatus = isOnline ? 'online' : 'offline'; device.deviceStatus = isOnline ? 'online' : 'offline';
} else { } else {
// 如果没有找到对应的状态信息,默认为离线 // 如果没有找到对应的状态信息,默认为离线
@@ -0,0 +1,881 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('voiceClone.title') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('voiceClone.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('voiceClone.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<!-- 显示表格或空状态 -->
<el-card class="params-card" shadow="never" v-if="total > 0">
<el-table ref="paramsTable" :data="voiceCloneList" class="transparent-table" v-loading="loading"
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column :label="$t('voiceClone.voiceId')" prop="voiceId"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.name')" align="center">
<template #default="{ row }">
<el-input v-show="row.isEdit" v-model="row.name" size="mini" maxlength="64"
show-word-limit @blur="onNameBlur(row)" @keyup.enter.native="onNameEnter(row)"
ref="nameInput" />
<span v-show="!row.isEdit" class="name-view">
<i class="el-icon-edit" @click="handleEditName(row)"
style="cursor: pointer;"></i>
<span @click="handleEditName(row)">
{{ row.name || '-' }}
</span>
</span>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
<template slot-scope="scope">
<div class="status-button" :class="getStatusButtonClass(scope.row)">
<span>{{ getTrainStatusText(scope.row) }}</span>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.Details')" align="center" width="80">
<template slot-scope="scope">
<el-tooltip :content="getTooltipContent(scope.row)" placement="top">
<el-button size="mini" type="text" icon="el-icon-info"
@click="handleViewDetails(scope.row)">
</el-button>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.action')" align="center" width="230">
<template slot-scope="scope">
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
@click="handlePlay(scope.row)">
{{ playingRowId === scope.row.id ? $t('voiceClone.stop') : $t('voiceClone.play') }}
</el-button>
<el-button size="mini" type="text" @click="handleUpload(scope.row)">
{{ $t('voiceClone.upload') }}
</el-button>
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
@click="handleClone(scope.row)" :loading="scope.row._cloning">
{{ $t('voiceClone.clone') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('voiceClone.itemsPerPage', { items: item })" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('voiceClone.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('voiceClone.prevPage') }}
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('voiceClone.nextPage') }}
</button>
<span class="total-text">{{ $t('voiceClone.totalRecords', { total }) }}</span>
</div>
</div>
</el-card>
<!-- 空状态提示 -->
<div v-else-if="!loading" class="empty-state-wrapper">
<div class="empty-state">
<div class="empty-icon">
<i class="el-icon-microphone" style="font-size: 48px;"></i>
</div>
<div class="empty-text">
{{ $t('voiceClone.noVoiceCloneAssigned') }}
</div>
<div class="empty-desc">
{{ $t('voiceClone.contactAdmin') }}
</div>
</div>
</div>
</div>
</div>
</div>
<el-footer>
<version-footer />
</el-footer>
<!-- 复刻弹框 -->
<VoiceCloneDialog :visible.sync="cloneDialogVisible" :voiceCloneData="currentVoiceClone"
@success="handleCloneSuccess" />
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import VoiceCloneDialog from "@/components/VoiceCloneDialog.vue";
import { formatDate } from "@/utils/format";
export default {
components: { HeaderBar, VersionFooter, VoiceCloneDialog },
data() {
return {
searchName: "",
loading: false,
voiceCloneList: [],
currentPage: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
dialogVisible: false,
cloneDialogVisible: false,
currentVoiceClone: {},
isAllSelected: false,
voiceCloneForm: {
modelId: "",
voiceIds: [],
userId: null
},
// 音频播放相关
currentAudio: null, // 当前正在播放的音频对象
playingRowId: null // 当前正在播放的行 ID
};
},
created() {
this.fetchVoiceCloneList();
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
},
methods: {
getTooltipContent(row) {
if (!row.hasVoice) {
return '待上传';
}
switch (row.trainStatus) {
case 0:
return '待复刻';
case 2:
return '训练成功';
case 3:
// 训练失败时,根据错误信息智能展示
if (row.trainError) {
return `训练失败:${row.trainError}`;
}
return '训练失败';
default:
return '';
}
},
handleViewDetails(row) {
console.log('查看详情:', row);
// 可以在这里添加查看详情的逻辑
},
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchVoiceCloneList();
},
fetchVoiceCloneList() {
this.loading = true;
const params = {
pageNum: this.currentPage,
pageSize: this.pageSize,
name: this.searchName || "",
orderField: "create_date",
order: "desc"
};
Api.voiceClone.getVoiceCloneList(params, (res) => {
this.loading = false;
res = res.data
if (res.code === 0) {
this.voiceCloneList = res.data.list;
this.total = res.data.total || 0;
} else {
this.voiceCloneList = [];
this.total = 0;
this.$message.error({
message: res?.data?.msg || this.$t('voiceClone.deleteFailed'),
showClose: true
});
}
});
},
handleSearch() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
goFirst() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchVoiceCloneList();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchVoiceCloneList();
}
},
goToPage(page) {
this.currentPage = page;
this.fetchVoiceCloneList();
},
formatDate,
getTrainStatusText(row) {
if (!row.hasVoice) {
return this.$t('voiceClone.waitingUpload');
}
switch (row.trainStatus) {
case 0:
return this.$t('voiceClone.waitingTraining');
case 2:
return this.$t('voiceClone.trainSuccess');
case 3:
return this.$t('voiceClone.trainFailed');
default:
return '';
}
},
// 获取状态按钮样式
getStatusButtonClass(row) {
if (!row.hasVoice || row.trainStatus === 0) {
return 'status-waiting';
} else if (row.trainStatus === 2) {
return 'status-success';
} else if (row.trainStatus === 3) {
return 'status-failed';
}
return '';
},
// 处理复刻操作
handleClone(row) {
// 防止重复提交
if (row._cloning) {
return;
}
this.$set(row, '_cloning', true);
const params = {
cloneId: row.id
};
try {
Api.voiceClone.cloneAudio(params, (res) => {
try {
res = res.data;
if (res.code === 0) {
this.$message.success(this.$t('message.success'));
// 复刻成功后刷新列表
this.fetchVoiceCloneList();
} else {
// 复刻失败时刷新列表以获取完整的错误信息
console.log('API返回错误,刷新列表获取详细错误信息');
this.$message.error(res.msg || this.$t('message.error'));
// 刷新列表以获取后端保存的完整错误详情
this.fetchVoiceCloneList();
}
} catch (error) {
// 处理响应时出错,刷新列表
console.error('处理响应时出错:', error);
this.$message.error('处理响应时出错');
this.fetchVoiceCloneList();
} finally {
this.$set(row, '_cloning', false);
}
}, (error) => {
// API调用失败,刷新列表以获取最新状态
console.error('API调用失败:', error);
this.$message.error('克隆失败,请将鼠标悬停在错误提示上,查看错误详情');
this.fetchVoiceCloneList();
this.$set(row, '_cloning', false);
});
} catch (error) {
// 调用API时出错,刷新列表
console.error('调用API时出错:', error);
this.$message.error('调用API时出错');
this.fetchVoiceCloneList();
this.$set(row, '_cloning', false);
}
},
// 更新行状态并触发视图更新
updateRowStatus(row, status, statusCode = null) {
// 在Vue中直接修改数组中的对象属性可能不会触发视图更新
const index = this.voiceCloneList.findIndex(item => item.id === row.id);
const updateData = {
trainStatus: status
};
// 如果提供了状态码,也更新状态码信息
if (statusCode !== null) {
updateData.statusCode = statusCode;
}
if (index !== -1) {
// 使用Vue.set来确保响应式更新
this.$set(this.voiceCloneList, index, {
...this.voiceCloneList[index],
...updateData
});
// 强制表格重新渲染
if (this.$refs.paramsTable) {
this.$refs.paramsTable.doLayout();
}
} else {
// 如果找不到索引,直接更新row对象
row.trainStatus = status;
if (statusCode !== null) {
row.statusCode = statusCode;
}
// 强制整个表格重新渲染
this.$forceUpdate();
}
console.log('更新行状态:', row.id, '状态:', status, '状态码:', statusCode);
},
// 复刻成功后的回调
handleCloneSuccess() {
this.fetchVoiceCloneList();
},
// 进入编辑模式
handleEditName(row) {
this.$set(row, 'isEdit', true);
this.$nextTick(() => {
// 聚焦到输入框
const input = this.$refs.nameInput;
if (input) {
// nameInput 可能是一个数组
if (Array.isArray(input)) {
const idx = this.voiceCloneList.indexOf(row);
if (input[idx]) {
input[idx].focus();
}
} else {
input.focus();
}
}
});
},
// 提交名称修改
submitName(row) {
// 防止重复提交
if (row._submitting) {
return;
}
row._submitting = true;
const params = {
id: row.id,
name: row.name
};
Api.voiceClone.updateName(params, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success(this.$t('voiceClone.updateNameSuccess') || '名称更新成功');
} else {
this.$message.error(res.msg || this.$t('voiceClone.updateNameFailed') || '名称更新失败');
// 失败时恢复原值
this.fetchVoiceCloneList();
}
row._submitting = false;
});
},
// 名称输入框:失焦时提交
onNameBlur(row) {
row.isEdit = false;
setTimeout(() => {
this.submitName(row);
}, 100); // 延迟 100ms,避开 enter+blur 同时触发的窗口
},
// 名称输入框:按回车时提交
onNameEnter(row) {
row.isEdit = false;
this.submitName(row);
},
// 播放音频
handlePlay(row) {
// 如果点击的是正在播放的行,则停止播放
if (this.playingRowId === row.id && this.currentAudio) {
this.stopCurrentAudio();
return;
}
// 停止当前正在播放的音频(如果有)
this.stopCurrentAudio();
// 先获取音频下载ID
Api.voiceClone.getAudioId(row.id, (res) => {
res = res.data;
if (res.code === 0) {
const uuid = res.data;
// 使用获取到的uuid播放音频
const audioUrl = Api.voiceClone.getPlayVoiceUrl(uuid);
const audio = new Audio(audioUrl);
// 设置当前播放状态
this.currentAudio = audio;
this.playingRowId = row.id;
// 播放结束时清除状态
audio.addEventListener('ended', () => {
this.playingRowId = null;
this.currentAudio = null;
});
// 播放出错时清除状态
audio.addEventListener('error', () => {
this.playingRowId = null;
this.currentAudio = null;
});
audio.play().catch(err => {
console.error('播放失败:', err);
this.$message.error(this.$t('voiceClone.playFailed') || '播放失败');
this.playingRowId = null;
this.currentAudio = null;
});
} else {
this.$message.error(res.msg || this.$t('voiceClone.audioNotExist') || '音频不存在');
}
});
},
// 停止当前音频播放
stopCurrentAudio() {
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio.currentTime = 0;
this.currentAudio = null;
}
this.playingRowId = null;
},
// 上传音频
handleUpload(row) {
this.currentVoiceClone = row;
this.cloneDialogVisible = true;
}
},
};
</script>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
margin: 5px 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow: auto;
background-color: white;
display: flex;
flex-direction: column;
}
.params-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
}
.custom-pagination {
display: flex;
align-items: center;
gap: 5px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2),
.pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
}
.empty-state-wrapper {
margin-top: 20vh;
}
.total-text {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
font-weight: 600;
height: 40px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
padding: 8px 0;
height: 40px;
color: #606266;
font-size: 14px;
}
}
.el-table__row:hover>td {
background-color: #f5f7fa !important;
}
&::before {
display: none;
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5 !important;
}
/* 状态按钮样式 */
.status-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-waiting {
background-color: #f5f7fa;
color: #909399;
border: 1px solid #e4e7ed;
}
.status-success {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.status-failed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
.name-view {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
i {
color: #909399;
font-size: 14px;
&:hover {
color: #5a64b5;
}
}
span {
&:hover {
color: #5a64b5;
}
}
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
</style>
@@ -0,0 +1,704 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('voiceResource.title') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('voiceClone.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('voiceClone.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="voiceCloneList" class="transparent-table" v-loading="loading"
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.voiceId')" prop="voiceId"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.name')" prop="name"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.userId')" prop="userName"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.platformName')" prop="modelName"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
<template slot-scope="scope">
{{ getTrainStatusText(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.createdAt')" prop="createdAt" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.createDate) }}
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.action')" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="deleteVoiceClone(scope.row)">{{
$t('voiceClone.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? $t('voiceClone.deselectAll') : $t('voiceClone.selectAll') }}
</el-button>
<el-button size="mini" type="success" @click="showAddDialog"
style="background: #5bc98c;border: None;">{{
$t('voiceClone.addNew') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="deleteSelectedVoiceClones">{{
$t('voiceClone.delete') }}</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('voiceClone.itemsPerPage', { items: item })" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('voiceClone.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('voiceClone.prevPage') }}
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('voiceClone.nextPage') }}
</button>
<span class="total-text">{{ $t('voiceClone.totalRecords', { total }) }}</span>
</div>
</div>
</el-card>
</div>
</div>
</div>
<!-- 新增音色资源对话框 -->
<voice-clone-dialog :title="$t('voiceClone.addVoiceClone')" :visible.sync="dialogVisible" :form="voiceCloneForm"
@submit="handleSubmit" @cancel="dialogVisible = false" />
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import VoiceCloneDialog from "@/components/VoiceResourceDialog.vue";
import { formatDate } from "@/utils/format";
export default {
components: { HeaderBar, VoiceCloneDialog, VersionFooter },
data() {
return {
searchName: "",
loading: false,
voiceCloneList: [],
currentPage: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
dialogVisible: false,
isAllSelected: false,
voiceCloneForm: {
modelId: "",
voiceIds: [],
userId: null
}
};
},
created() {
this.fetchVoiceCloneList();
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
},
methods: {
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchVoiceCloneList();
},
fetchVoiceCloneList() {
this.loading = true;
const params = {
pageNum: this.currentPage,
pageSize: this.pageSize,
name: this.searchName || "",
orderField: "create_date",
order: "desc"
};
Api.voiceResource.getVoiceResourceList(params, (res) => {
this.loading = false;
res = res.data
if (res.code === 0) {
this.voiceCloneList = res.data.list.map(item => ({
...item,
selected: false
}));
this.total = res.data.total || 0;
} else {
this.voiceCloneList = [];
this.total = 0;
this.$message.error({
message: res?.data?.msg || this.$t('voiceClone.deleteFailed'),
showClose: true
});
}
});
},
handleSearch() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
handleSelectAll() {
this.isAllSelected = !this.isAllSelected;
this.voiceCloneList.forEach(row => {
row.selected = this.isAllSelected;
});
},
showAddDialog() {
this.voiceCloneForm = {
modelId: "",
voiceIds: [],
userId: null
};
this.$nextTick(() => {
if (this.$refs.voiceCloneForm) {
this.$refs.voiceCloneForm.clearValidate();
}
});
this.dialogVisible = true;
},
handleSubmit(formData) {
Api.voiceResource.saveVoiceResource(formData, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success({
message: this.$t('voiceClone.addSuccess'),
showClose: true
});
this.dialogVisible = false;
this.fetchVoiceCloneList();
} else {
this.$message.error({
message: res.msg || this.$t('voiceClone.addFailed'),
showClose: true
});
}
});
},
deleteSelectedVoiceClones() {
const selectedRows = this.voiceCloneList.filter(row => row.selected);
if (selectedRows.length === 0) {
this.$message.warning({
message: this.$t('voiceClone.selectFirst'),
showClose: true
});
return;
}
this.deleteVoiceClone(selectedRows);
},
deleteVoiceClone(row) {
const items = Array.isArray(row) ? row : [row];
if (Array.isArray(row) && row.length === 0) {
this.$message.warning({
message: this.$t('voiceClone.selectFirst'),
showClose: true
});
return;
}
const itemCount = items.length;
this.$confirm(this.$t('voiceClone.confirmDelete', { count: itemCount }), this.$t('voiceClone.warning'), {
confirmButtonText: this.$t('voiceClone.ok'),
cancelButtonText: this.$t('voiceClone.cancel'),
type: 'warning',
distinguishCancelAndClose: true
}).then(() => {
const ids = items.map(item => item.id);
if (ids.some(id => !id)) {
this.$message.error({
message: this.$t('voiceClone.deleteFailed'),
showClose: true
});
return;
}
Api.voiceResource.deleteVoiceResource(ids, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success({
message: this.$t('voiceClone.deleteSuccess', { count: itemCount }),
showClose: true
});
this.fetchVoiceCloneList();
} else {
this.$message.error({
message: res.msg || this.$t('voiceClone.deleteFailed'),
showClose: true
});
}
});
}).catch(action => {
if (action === 'cancel') {
this.$message({
type: 'info',
message: this.$t('voiceClone.operationCancelled'),
duration: 1000
});
} else {
this.$message({
type: 'info',
message: this.$t('voiceClone.operationClosed'),
duration: 1000
});
}
});
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
return "";
},
goFirst() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchVoiceCloneList();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchVoiceCloneList();
}
},
goToPage(page) {
this.currentPage = page;
this.fetchVoiceCloneList();
},
formatDate,
getTrainStatusText(row) {
if (!row.hasVoice) {
return this.$t('voiceClone.waitingUpload');
}
switch (row.trainStatus) {
case 0:
return this.$t('voiceClone.waitingTraining');
case 1:
return this.$t('voiceClone.training');
case 2:
return this.$t('voiceClone.trainSuccess');
case 3:
return this.$t('voiceClone.trainFailed');
default:
return '';
}
}
},
};
</script>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
margin: 5px 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow: auto;
background-color: white;
display: flex;
flex-direction: column;
}
.params-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
.custom-pagination {
display: flex;
align-items: center;
gap: 5px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2),
.pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
}
.total-text {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
font-weight: 600;
height: 40px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
padding: 8px 0;
height: 40px;
color: #606266;
font-size: 14px;
}
}
.el-table__row:hover>td {
background-color: #f5f7fa !important;
}
&::before {
display: none;
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5 !important;
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
</style>
@@ -67,7 +67,6 @@
<context> <context>
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】 【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
- **设备ID** {{device_id}}
- **当前时间:** {{current_time}} - **当前时间:** {{current_time}}
- **今天日期:** {{today_date}} ({{today_weekday}}) - **今天日期:** {{today_date}} ({{today_weekday}})
- **今天农历:** {{lunar_date}} - **今天农历:** {{lunar_date}}
+11 -5
View File
@@ -46,12 +46,18 @@ async def main():
check_ffmpeg_installed() check_ffmpeg_installed()
config = load_config() config = load_config()
# 默认使用manager-apisecret作为auth_key # auth_key优先级:配置文件server.auth_key > manager-api.secret > 自动生成
# 如果secret为空,则生成随机密钥 # auth_key用于jwt认证,比如视觉分析接口的jwt认证、ota接口的token生成与websocket认证
# auth_key用于jwt认证,比如视觉分析接口的jwt认证 # 获取配置文件中的auth_key
auth_key = config.get("manager-api", {}).get("secret", "") auth_key = config["server"].get("auth_key", "")
# 验证auth_key,无效则尝试使用manager-api.secret
if not auth_key or len(auth_key) == 0 or "" in auth_key: if not auth_key or len(auth_key) == 0 or "" in auth_key:
auth_key = str(uuid.uuid4().hex) auth_key = config.get("manager-api", {}).get("secret", "")
# 验证secret,无效则生成随机密钥
if not auth_key or len(auth_key) == 0 or "" in auth_key:
auth_key = str(uuid.uuid4().hex)
config["server"]["auth_key"] = auth_key config["server"]["auth_key"] = auth_key
# 添加 stdin 监控任务 # 添加 stdin 监控任务
+14 -11
View File
@@ -31,16 +31,10 @@ server:
auth: auth:
# 是否启用认证 # 是否启用认证
enabled: false enabled: false
# 设备的token,可以在编译固件的环节,写入你自己定义的token # 白名单设备ID列表
# 固件上的token和以下的token如果能对应,才能连接本服务端 # 如果属于白名单内的设备,不校验token,直接放行
tokens: allowed_devices:
- token: "your-token1" # 设备1的token - "11:22:33:44:55:66"
name: "your-device-name1" # 设备1标识
- token: "your-token2" # 设备2的token
name: "your-device-name2" # 设备2标识
# 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。
#allowed_devices:
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
# MQTT网关配置,用于通过OTA下发到设备,根据mqtt_gateway的.env文件配置,格式为host:port # MQTT网关配置,用于通过OTA下发到设备,根据mqtt_gateway的.env文件配置,格式为host:port
mqtt_gateway: null mqtt_gateway: null
# MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置 # MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置
@@ -76,6 +70,12 @@ enable_stop_tts_notify: false
# 说完话是否开启提示音,音效地址 # 说完话是否开启提示音,音效地址
stop_tts_notify_voice: "config/assets/tts_notify.mp3" stop_tts_notify_voice: "config/assets/tts_notify.mp3"
# TTS音频发送延迟配置
# tts_audio_send_delay: 控制音频包发送间隔
# 0: 使用精确时间控制,严格匹配音频帧率(默认,运行时按音频帧率计算)
# > 0: 使用固定延迟(毫秒)发送,例如: 60
tts_audio_send_delay: 0
exit_commands: exit_commands:
- "退出" - "退出"
- "关闭" - "关闭"
@@ -179,6 +179,9 @@ prompt: |
- 长篇大论,叽叽歪歪 - 长篇大论,叽叽歪歪
- 长时间严肃对话 - 长时间严肃对话
# 默认系统提示词模板文件
prompt_template: agent-base-prompt.txt
# 结束语prompt # 结束语prompt
end_prompt: end_prompt:
enable: true # 是否开启结束语 enable: true # 是否开启结束语
@@ -993,4 +996,4 @@ TTS:
# sample_rate: 24000 # 采样率:16000, 8000, 24000 # sample_rate: 24000 # 采样率:16000, 8000, 24000
# volume: 50 # 音量:0-100 # volume: 50 # 音量:0-100
# speed: 50 # 语速:0-100 # speed: 50 # 语速:0-100
# pitch: 50 # 语调:0-100 # pitch: 50 # 语调:0-100
@@ -68,6 +68,9 @@ def get_config_from_api(config):
"vision_explain": config["server"].get("vision_explain", ""), "vision_explain": config["server"].get("vision_explain", ""),
"auth_key": config["server"].get("auth_key", ""), "auth_key": config["server"].get("auth_key", ""),
} }
# 如果服务器没有prompt_template,则从本地配置读取
if not config_data.get("prompt_template"):
config_data["prompt_template"] = config.get("prompt_template")
return config_data return config_data
+1 -1
View File
@@ -5,7 +5,7 @@ from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
from datetime import datetime from datetime import datetime
SERVER_VERSION = "0.8.3" SERVER_VERSION = "0.8.6"
_logger_initialized = False _logger_initialized = False
+3 -1
View File
@@ -22,4 +22,6 @@ manager-api:
# 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi # 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi
url: http://127.0.0.1:8002/xiaozhi url: http://127.0.0.1:8002/xiaozhi
# 你的manager-api的token,就是刚才复制出来的server.secret # 你的manager-api的token,就是刚才复制出来的server.secret
secret: 你的server.secret值 secret: 你的server.secret值
# 默认系统提示词模板文件
prompt_template: agent-base-prompt.txt
+48 -19
View File
@@ -4,6 +4,8 @@ import base64
import hashlib import hashlib
import hmac import hmac
from aiohttp import web from aiohttp import web
from core.auth import AuthManager
from core.utils.util import get_local_ip from core.utils.util import get_local_ip
from core.api.base_handler import BaseHandler from core.api.base_handler import BaseHandler
@@ -13,21 +15,30 @@ TAG = __name__
class OTAHandler(BaseHandler): class OTAHandler(BaseHandler):
def __init__(self, config: dict): def __init__(self, config: dict):
super().__init__(config) super().__init__(config)
auth_config = config["server"].get("auth", {})
self.auth_enable = auth_config.get("enabled", False)
# 设备白名单
self.allowed_devices = set(auth_config.get("allowed_devices", []))
secret_key = config["server"]["auth_key"]
expire_seconds = auth_config.get("expire_seconds")
self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
def generate_password_signature(self, content: str, secret_key: str) -> str: def generate_password_signature(self, content: str, secret_key: str) -> str:
"""生成MQTT密码签名 """生成MQTT密码签名
Args: Args:
content: 签名内容 (clientId + '|' + username) content: 签名内容 (clientId + '|' + username)
secret_key: 密钥 secret_key: 密钥
Returns: Returns:
str: Base64编码的HMAC-SHA256签名 str: Base64编码的HMAC-SHA256签名
""" """
try: try:
hmac_obj = hmac.new(secret_key.encode('utf-8'), content.encode('utf-8'), hashlib.sha256) hmac_obj = hmac.new(
secret_key.encode("utf-8"), content.encode("utf-8"), hashlib.sha256
)
signature = hmac_obj.digest() signature = hmac_obj.digest()
return base64.b64encode(signature).decode('utf-8') return base64.b64encode(signature).decode("utf-8")
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}") self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}")
return "" return ""
@@ -64,6 +75,12 @@ class OTAHandler(BaseHandler):
else: else:
raise Exception("OTA请求设备ID为空") raise Exception("OTA请求设备ID为空")
client_id = request.headers.get("client-id", "")
if client_id:
self.logger.bind(tag=TAG).info(f"OTA请求ClientID: {client_id}")
else:
raise Exception("OTA请求ClientID为空")
data_json = json.loads(data) data_json = json.loads(data)
server_config = self.config["server"] server_config = self.config["server"]
@@ -80,10 +97,9 @@ class OTAHandler(BaseHandler):
"url": "", "url": "",
}, },
} }
mqtt_gateway_endpoint = server_config.get("mqtt_gateway") mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
if mqtt_gateway_endpoint: # 如果配置了非空字符串 if mqtt_gateway_endpoint: # 如果配置了非空字符串
# 尝试从请求数据中获取设备型号 # 尝试从请求数据中获取设备型号
device_model = "default" device_model = "default"
@@ -101,12 +117,12 @@ class OTAHandler(BaseHandler):
mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}"
# 构建用户数据 # 构建用户数据
user_data = { user_data = {"ip": "unknown"}
"ip": "unknown"
}
try: try:
user_data_json = json.dumps(user_data) user_data_json = json.dumps(user_data)
username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') username = base64.b64encode(user_data_json.encode("utf-8")).decode(
"utf-8"
)
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}")
username = "" username = ""
@@ -115,30 +131,43 @@ class OTAHandler(BaseHandler):
password = "" password = ""
signature_key = server_config.get("mqtt_signature_key", "") signature_key = server_config.get("mqtt_signature_key", "")
if signature_key: if signature_key:
password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) password = self.generate_password_signature(
mqtt_client_id + "|" + username, signature_key
)
if not password: if not password:
password = "" # 签名失败则留空,由设备决定是否允许无密码 password = "" # 签名失败则留空,由设备决定是否允许无密码
else: else:
self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空") self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空")
# 构建MQTT配置(直接使用 mqtt_gateway 字符串) # 构建MQTT配置(直接使用 mqtt_gateway 字符串)
return_json["mqtt_gateway"] = { return_json["mqtt"] = {
"endpoint": mqtt_gateway_endpoint, "endpoint": mqtt_gateway_endpoint,
"client_id": mqtt_client_id, "client_id": mqtt_client_id,
"username": username, "username": username,
"password": password, "password": password,
"publish_topic": "device-server", "publish_topic": "device-server",
"subscribe_topic": f"devices/p2p/{mac_address_safe}" "subscribe_topic": f"devices/p2p/{mac_address_safe}",
} }
self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置") self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置")
else: # 未配置 mqtt_gateway,下发 WebSocket else: # 未配置 mqtt_gateway,下发 WebSocket
# 如果开启了认证,则进行认证校验
token = ""
if self.auth_enable:
if self.allowed_devices:
if device_id not in self.allowed_devices:
token = self.auth.generate_token(client_id, device_id)
else:
token = self.auth.generate_token(client_id, device_id)
return_json["websocket"] = { return_json["websocket"] = {
"url": self._get_websocket_url(local_ip, port), "url": self._get_websocket_url(local_ip, port),
"token": token,
} }
self.logger.bind(tag=TAG).info(f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置") self.logger.bind(tag=TAG).info(
f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置"
)
self.logger.bind(tag=TAG).info(f"{return_json}")
response = web.Response( response = web.Response(
text=json.dumps(return_json, separators=(",", ":")), text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json", content_type="application/json",
@@ -167,4 +196,4 @@ class OTAHandler(BaseHandler):
response = web.Response(text="OTA接口异常", content_type="text/plain") response = web.Response(text="OTA接口异常", content_type="text/plain")
finally: finally:
self._add_cors_headers(response) self._add_cors_headers(response)
return response return response
+63 -45
View File
@@ -1,54 +1,72 @@
from config.logger import setup_logging import hmac
import base64
TAG = __name__ import hashlib
logger = setup_logging() import time
class AuthenticationError(Exception): class AuthenticationError(Exception):
"""认证异常""" """认证异常"""
pass pass
class AuthMiddleware: class AuthManager:
def __init__(self, config): """
self.config = config 统一授权认证管理器
self.auth_config = config["server"].get("auth", {}) 生成与验证 client_id device_id tokenHMAC-SHA256认证三元组
# 构建token查找表 token 中不含明文 client_id/device_id只携带签名 + 时间戳; client_id/device_id在连接时传递
self.tokens = { MQTT client_id: client_id, username: device_id, password: token
item["token"]: item["name"] Websocket header:{Device-ID: device_id, Client-ID: client_id, Authorization: Bearer token, ......}
for item in self.auth_config.get("tokens", []) """
}
# 设备白名单 def __init__(self, secret_key: str, expire_seconds: int = 60 * 60 * 24 * 30):
self.allowed_devices = set( if not expire_seconds or expire_seconds < 0:
self.auth_config.get("allowed_devices", []) self.expire_seconds = 60 * 60 * 24 * 30
) else:
self.expire_seconds = expire_seconds
self.secret_key = secret_key
def _sign(self, content: str) -> str:
"""HMAC-SHA256签名并Base64编码"""
sig = hmac.new(
self.secret_key.encode("utf-8"), content.encode("utf-8"), hashlib.sha256
).digest()
return base64.urlsafe_b64encode(sig).decode("utf-8").rstrip("=")
def generate_token(self, client_id: str, username: str) -> str:
"""
生成 token
Args:
client_id: 设备连接ID
username: 设备用户名通常为deviceId
Returns:
str: token字符串
"""
ts = int(time.time())
content = f"{client_id}|{username}|{ts}"
signature = self._sign(content)
# token仅包含签名与时间戳,不包含明文信息
token = f"{signature}.{ts}"
return token
def verify_token(self, token: str, client_id: str, username: str) -> bool:
"""
验证token有效性
Args:
token: 客户端传入的token
client_id: 连接使用的client_id
username: 连接使用的username
"""
try:
sig_part, ts_str = token.split(".")
ts = int(ts_str)
if int(time.time()) - ts > self.expire_seconds:
return False # 过期
expected_sig = self._sign(f"{client_id}|{username}|{ts}")
if not hmac.compare_digest(sig_part, expected_sig):
return False
async def authenticate(self, headers):
"""验证连接请求"""
# 检查是否启用认证
if not self.auth_config.get("enabled", False):
return True return True
except Exception:
# 检查设备是否在白名单中 return False
device_id = headers.get("device-id", "")
if self.allowed_devices and device_id in self.allowed_devices:
return True
# 验证Authorization header
auth_header = headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
logger.bind(tag=TAG).error("Missing or invalid Authorization header")
raise AuthenticationError("Missing or invalid Authorization header")
token = auth_header.split(" ")[1]
if token not in self.tokens:
logger.bind(tag=TAG).error(f"Invalid token: {token}")
raise AuthenticationError("Invalid token")
logger.bind(tag=TAG).info(f"Authentication successful - Device: {device_id}, Token: {self.tokens[token]}")
return True
def get_token_name(self, token):
"""获取token对应的设备名称"""
return self.tokens.get(token)
+5 -26
View File
@@ -10,6 +10,7 @@ import threading
import traceback import traceback
import subprocess import subprocess
import websockets import websockets
from core.utils.util import ( from core.utils.util import (
extract_json_from_string, extract_json_from_string,
check_vad_update, check_vad_update,
@@ -31,8 +32,8 @@ from core.providers.asr.dto.dto import InterfaceType
from core.handle.textHandle import handleTextMessage from core.handle.textHandle import handleTextMessage
from core.providers.tools.unified_tool_handler import UnifiedToolHandler from core.providers.tools.unified_tool_handler import UnifiedToolHandler
from plugins_func.loadplugins import auto_import_modules from plugins_func.loadplugins import auto_import_modules
from plugins_func.register import Action, ActionResponse from plugins_func.register import Action
from core.auth import AuthMiddleware, AuthenticationError from core.auth import AuthenticationError
from config.config_loader import get_private_config_from_api from config.config_loader import get_private_config_from_api
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
from config.logger import setup_logging, build_module_string, create_connection_logger from config.logger import setup_logging, build_module_string, create_connection_logger
@@ -67,7 +68,6 @@ class ConnectionHandler:
self.logger = setup_logging() self.logger = setup_logging()
self.server = server # 保存server实例的引用 self.server = server # 保存server实例的引用
self.auth = AuthMiddleware(config)
self.need_bind = False self.need_bind = False
self.bind_code = None self.bind_code = None
self.read_config_from_api = self.config.get("read_config_from_api", False) self.read_config_from_api = self.config.get("read_config_from_api", False)
@@ -115,9 +115,9 @@ class ConnectionHandler:
# vad相关变量 # vad相关变量
self.client_audio_buffer = bytearray() self.client_audio_buffer = bytearray()
self.client_have_voice = False self.client_have_voice = False
self.client_voice_window = deque(maxlen=5)
self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒) self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
self.client_voice_stop = False self.client_voice_stop = False
self.client_voice_window = deque(maxlen=5)
self.last_is_voice = False self.last_is_voice = False
# asr相关变量 # asr相关变量
@@ -164,25 +164,6 @@ class ConnectionHandler:
try: try:
# 获取并验证headers # 获取并验证headers
self.headers = dict(ws.request.headers) self.headers = dict(ws.request.headers)
if self.headers.get("device-id", None) is None:
# 尝试从 URL 的查询参数中获取 device-id
from urllib.parse import parse_qs, urlparse
# 从 WebSocket 请求中获取路径
request_path = ws.request.path
if not request_path:
self.logger.bind(tag=TAG).error("无法获取请求路径")
return
parsed_url = urlparse(request_path)
query_params = parse_qs(parsed_url.query)
if "device-id" in query_params:
self.headers["device-id"] = query_params["device-id"][0]
self.headers["client-id"] = query_params["client-id"][0]
else:
await ws.send("端口正常,如需测试连接,请使用test_page.html")
await self.close(ws)
return
real_ip = self.headers.get("x-real-ip") or self.headers.get( real_ip = self.headers.get("x-real-ip") or self.headers.get(
"x-forwarded-for" "x-forwarded-for"
) )
@@ -194,12 +175,10 @@ class ConnectionHandler:
f"{self.client_ip} conn - Headers: {self.headers}" f"{self.client_ip} conn - Headers: {self.headers}"
) )
# 进行认证 self.device_id = self.headers.get("device-id", None)
await self.auth.authenticate(self.headers)
# 认证通过,继续处理 # 认证通过,继续处理
self.websocket = ws self.websocket = ws
self.device_id = self.headers.get("device-id", None)
# 检查是否来自MQTT连接 # 检查是否来自MQTT连接
request_path = ws.request.path request_path = ws.request.path
+20 -18
View File
@@ -6,7 +6,7 @@ from core.utils.dialogue import Message
from core.utils.util import audio_to_data from core.utils.util import audio_to_data
from core.providers.tts.dto.dto import SentenceType from core.providers.tts.dto.dto import SentenceType
from core.utils.wakeup_word import WakeupWordsConfig from core.utils.wakeup_word import WakeupWordsConfig
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message from core.handle.sendAudioHandle import sendAudioMessage, send_tts_message
from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
from core.providers.tools.device_mcp import ( from core.providers.tools.device_mcp import (
MCPClient, MCPClient,
@@ -17,8 +17,18 @@ from core.providers.tools.device_mcp import (
TAG = __name__ TAG = __name__
WAKEUP_CONFIG = { WAKEUP_CONFIG = {
"refresh_time": 5, "refresh_time": 10,
"words": ["你好", "你好啊", "嘿,你好", ""], "responses": [
"我一直都在呢,您请说。",
"在的呢,请随时吩咐我。",
"来啦来啦,请告诉我吧。",
"您请说,我正听着。",
"请您讲话,我准备好了。",
"请您说出指令吧。",
"我认真听着呢,请讲。",
"请问您需要什么帮助?",
"我在这里,等候您的指令。",
],
} }
# 创建全局的唤醒词配置管理器 # 创建全局的唤醒词配置管理器
@@ -73,7 +83,7 @@ async def checkWakeupWords(conn, text):
return False return False
conn.just_woken_up = True conn.just_woken_up = True
await send_stt_message(conn, text) await send_tts_message(conn, "start")
# 获取当前音色 # 获取当前音色
voice = getattr(conn.tts, "voice", "default") voice = getattr(conn.tts, "voice", "default")
@@ -85,9 +95,9 @@ async def checkWakeupWords(conn, text):
if not response or not response.get("file_path"): if not response or not response.get("file_path"):
response = { response = {
"voice": "default", "voice": "default",
"file_path": "config/assets/wakeup_words.wav", "file_path": "config/assets/wakeup_words_short.wav",
"time": 0, "time": 0,
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦", "text": "我在这里哦!",
} }
# 获取音频数据 # 获取音频数据
@@ -110,7 +120,7 @@ async def checkWakeupWords(conn, text):
async def wakeupWordsResponse(conn): async def wakeupWordsResponse(conn):
if not conn.tts or not conn.llm or not conn.llm.response_no_stream: if not conn.tts:
return return
try: try:
@@ -118,16 +128,8 @@ async def wakeupWordsResponse(conn):
if not await _wakeup_response_lock.acquire(): if not await _wakeup_response_lock.acquire():
return return
# 生成唤醒词回复 # 从预定义回复列表中随机选择一个回复
wakeup_word = random.choice(WAKEUP_CONFIG["words"]) result = random.choice(WAKEUP_CONFIG["responses"])
question = (
"此刻用户正在和你说```"
+ wakeup_word
+ "```。\n请你根据以上用户的内容进行20-30字回复。要符合系统设置的角色情感和态度,不要像机器人一样说话。\n"
+ "请勿对这条内容本身进行任何解释和回应,请勿返回表情符号,仅返回对用户的内容的回复。"
)
result = conn.llm.response_no_stream(conn.config["prompt"], question)
if not result or len(result) == 0: if not result or len(result) == 0:
return return
@@ -148,4 +150,4 @@ async def wakeupWordsResponse(conn):
finally: finally:
# 确保在任何情况下都释放锁 # 确保在任何情况下都释放锁
if _wakeup_response_lock.locked(): if _wakeup_response_lock.locked():
_wakeup_response_lock.release() _wakeup_response_lock.release()
@@ -14,15 +14,16 @@ async def handleAudioMessage(conn, audio):
# 当前片段是否有人说话 # 当前片段是否有人说话
have_voice = conn.vad.is_vad(conn, audio) have_voice = conn.vad.is_vad(conn, audio)
# 如果设备刚刚被唤醒,短暂忽略VAD检测 # 如果设备刚刚被唤醒,短暂忽略VAD检测
if have_voice and hasattr(conn, "just_woken_up") and conn.just_woken_up: if hasattr(conn, "just_woken_up") and conn.just_woken_up:
have_voice = False have_voice = False
# 设置一个短暂延迟后恢复VAD检测 # 设置一个短暂延迟后恢复VAD检测
conn.asr_audio.clear() conn.asr_audio.clear()
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done(): if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn)) conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
return return
# manual 模式下不打断正在播放的内容
if have_voice: if have_voice:
if conn.client_is_speaking: if conn.client_is_speaking and conn.client_listen_mode != "manual":
await handleAbortMessage(conn) await handleAbortMessage(conn)
# 设备长时间空闲检测,用于say goodbye # 设备长时间空闲检测,用于say goodbye
await no_voice_close_connect(conn, have_voice) await no_voice_close_connect(conn, have_voice)
@@ -32,7 +33,7 @@ async def handleAudioMessage(conn, audio):
async def resume_vad_detection(conn): async def resume_vad_detection(conn):
# 等待2秒后恢复VAD检测 # 等待2秒后恢复VAD检测
await asyncio.sleep(1) await asyncio.sleep(2)
conn.just_woken_up = False conn.just_woken_up = False
@@ -73,7 +74,8 @@ async def startToChat(conn, text):
): ):
await max_out_size(conn) await max_out_size(conn)
return return
if conn.client_is_speaking: # manual 模式下不打断正在播放的内容
if conn.client_is_speaking and conn.client_listen_mode != "manual":
await handleAbortMessage(conn) await handleAbortMessage(conn)
# 首先进行意图分析,使用实际文本内容 # 首先进行意图分析,使用实际文本内容
@@ -90,6 +90,9 @@ async def sendAudio(conn, audios, frame_duration=60):
if audios is None or len(audios) == 0: if audios is None or len(audios) == 0:
return return
# 获取发送延迟配置
send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0
if isinstance(audios, bytes): if isinstance(audios, bytes):
if conn.client_abort: if conn.client_abort:
return return
@@ -107,16 +110,21 @@ async def sendAudio(conn, audios, frame_duration=60):
flow_control = conn.audio_flow_control flow_control = conn.audio_flow_control
current_time = time.perf_counter() current_time = time.perf_counter()
# 计算预期发送时间
expected_time = flow_control["start_time"] + ( if send_delay > 0:
flow_control["packet_count"] * frame_duration / 1000 # 使用固定延迟
) await asyncio.sleep(send_delay)
delay = expected_time - current_time
if delay > 0:
await asyncio.sleep(delay)
else: else:
# 纠正误差 # 计算预期发送时间
flow_control["start_time"] += abs(delay) expected_time = flow_control["start_time"] + (
flow_control["packet_count"] * frame_duration / 1000
)
delay = expected_time - current_time
if delay > 0:
await asyncio.sleep(delay)
else:
# 纠正误差
flow_control["start_time"] += abs(delay)
if conn.conn_from_mqtt_gateway: if conn.conn_from_mqtt_gateway:
# 计算时间戳和序列号 # 计算时间戳和序列号
@@ -164,12 +172,16 @@ async def sendAudio(conn, audios, frame_duration=60):
# 重置没有声音的状态 # 重置没有声音的状态
conn.last_activity_time = time.time() * 1000 conn.last_activity_time = time.time() * 1000
# 计算预期发送时间 if send_delay > 0:
expected_time = start_time + (play_position / 1000) # 固定延迟模式
current_time = time.perf_counter() await asyncio.sleep(send_delay)
delay = expected_time - current_time else:
if delay > 0: # 计算预期发送时间
await asyncio.sleep(delay) expected_time = start_time + (play_position / 1000)
current_time = time.perf_counter()
delay = expected_time - current_time
if delay > 0:
await asyncio.sleep(delay)
if conn.conn_from_mqtt_gateway: if conn.conn_from_mqtt_gateway:
# 计算时间戳和序列号(使用当前的数据包索引确保连续性) # 计算时间戳和序列号(使用当前的数据包索引确保连续性)
@@ -96,6 +96,8 @@ class ASRProvider(ASRProviderBase):
self.delete_audio_file = delete_audio_file self.delete_audio_file = delete_audio_file
self.expire_time = None self.expire_time = None
self.task_id = uuid.uuid4().hex
# Token管理 # Token管理
if self.access_key_id and self.access_key_secret: if self.access_key_id and self.access_key_secret:
self._refresh_token() self._refresh_token()
@@ -169,19 +171,23 @@ class ASRProvider(ASRProviderBase):
ping_timeout=None, ping_timeout=None,
close_timeout=5, close_timeout=5,
) )
self.task_id = uuid.uuid4().hex
logger.bind(tag=TAG).info(f"WebSocket连接建立成功, task_id: {self.task_id}")
self.is_processing = True self.is_processing = True
self.server_ready = False # 重置服务器准备状态 self.server_ready = False # 重置服务器准备状态
self.forward_task = asyncio.create_task(self._forward_results(conn)) self.forward_task = asyncio.create_task(self._forward_results(conn))
# 发送开始请求 # 发送开始请求
start_request = { start_request = {
"header": { "header": {
"namespace": "SpeechTranscriber", "namespace": "SpeechTranscriber",
"name": "StartTranscription", "name": "StartTranscription",
"status": 20000000, "status": 20000000,
"message_id": ''.join(random.choices('0123456789abcdef', k=32)), "message_id": uuid.uuid4().hex,
"task_id": ''.join(random.choices('0123456789abcdef', k=32)), "task_id": self.task_id,
"status_text": "Gateway:SUCCESS:Success.", "status_text": "Gateway:SUCCESS:Success.",
"appkey": self.appkey "appkey": self.appkey
}, },
@@ -292,7 +298,8 @@ class ASRProvider(ASRProviderBase):
"namespace": "SpeechTranscriber", "namespace": "SpeechTranscriber",
"name": "StopTranscription", "name": "StopTranscription",
"status": 20000000, "status": 20000000,
"message_id": ''.join(random.choices('0123456789abcdef', k=32)), "message_id": uuid.uuid4().hex,
"task_id": self.task_id,
"status_text": "Client:Stop", "status_text": "Client:Stop",
"appkey": self.appkey "appkey": self.appkey
} }
@@ -68,7 +68,7 @@ class ASRProviderBase(ABC):
conn.asr_audio.clear() conn.asr_audio.clear()
conn.reset_vad_states() conn.reset_vad_states()
if len(asr_audio_task) > 15: if len(asr_audio_task) > 15 or conn.client_listen_mode == "manual":
await self.handle_voice_stop(conn, asr_audio_task) await self.handle_voice_stop(conn, asr_audio_task)
# 处理语音停止 # 处理语音停止
@@ -22,6 +22,7 @@ STATUS_FIRST_FRAME = 0 # 第一帧的标识
STATUS_CONTINUE_FRAME = 1 # 中间帧标识 STATUS_CONTINUE_FRAME = 1 # 中间帧标识
STATUS_LAST_FRAME = 2 # 最后一帧的标识 STATUS_LAST_FRAME = 2 # 最后一帧的标识
class ASRProvider(ASRProviderBase): class ASRProvider(ASRProviderBase):
def __init__(self, config, delete_audio_file): def __init__(self, config, delete_audio_file):
super().__init__() super().__init__()
@@ -35,6 +36,7 @@ class ASRProvider(ASRProviderBase):
self.server_ready = False self.server_ready = False
self.last_frame_sent = False # 标记是否已发送最终帧 self.last_frame_sent = False # 标记是否已发送最终帧
self.best_text = "" # 保存最佳识别结果 self.best_text = "" # 保存最佳识别结果
self.has_final_result = False # 标记是否收到最终识别结果
# 讯飞配置 # 讯飞配置
self.app_id = config.get("app_id") self.app_id = config.get("app_id")
@@ -50,19 +52,15 @@ class ASRProvider(ASRProviderBase):
"language": config.get("language", "zh_cn"), "language": config.get("language", "zh_cn"),
"accent": config.get("accent", "mandarin"), "accent": config.get("accent", "mandarin"),
"dwa": config.get("dwa", "wpgs"), "dwa": config.get("dwa", "wpgs"),
"result": { "result": {"encoding": "utf8", "compress": "raw", "format": "plain"},
"encoding": "utf8",
"compress": "raw",
"format": "plain"
}
} }
self.output_dir = config.get("output_dir", "tmp/") self.output_dir = config.get("output_dir", "tmp/")
self.delete_audio_file = delete_audio_file self.delete_audio_file = delete_audio_file
def create_url(self) -> str: def create_url(self) -> str:
"""生成认证URL""" """生成认证URL"""
url = 'ws://iat.cn-huabei-1.xf-yun.com/v1' url = "ws://iat.cn-huabei-1.xf-yun.com/v1"
# 生成RFC1123格式的时间戳 # 生成RFC1123格式的时间戳
now = datetime.now() now = datetime.now()
date = format_date_time(mktime(now.timetuple())) date = format_date_time(mktime(now.timetuple()))
@@ -73,23 +71,30 @@ class ASRProvider(ASRProviderBase):
signature_origin += "GET " + "/v1 " + "HTTP/1.1" signature_origin += "GET " + "/v1 " + "HTTP/1.1"
# 进行hmac-sha256进行加密 # 进行hmac-sha256进行加密
signature_sha = hmac.new(self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'), signature_sha = hmac.new(
digestmod=hashlib.sha256).digest() self.api_secret.encode("utf-8"),
signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8') signature_origin.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
signature_sha = base64.b64encode(signature_sha).decode(encoding="utf-8")
authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % ( authorization_origin = (
self.api_key, "hmac-sha256", "host date request-line", signature_sha) 'api_key="%s", algorithm="%s", headers="%s", signature="%s"'
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') % (self.api_key, "hmac-sha256", "host date request-line", signature_sha)
)
authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode(
encoding="utf-8"
)
# 将请求的鉴权参数组合为字典 # 将请求的鉴权参数组合为字典
v = { v = {
"authorization": authorization, "authorization": authorization,
"date": date, "date": date,
"host": "iat.cn-huabei-1.xf-yun.com" "host": "iat.cn-huabei-1.xf-yun.com",
} }
# 拼接鉴权参数,生成url # 拼接鉴权参数,生成url
url = url + '?' + urlencode(v) url = url + "?" + urlencode(v)
return url return url
async def open_audio_channels(self, conn): async def open_audio_channels(self, conn):
@@ -100,7 +105,7 @@ class ASRProvider(ASRProviderBase):
await super().receive_audio(conn, audio, audio_have_voice) await super().receive_audio(conn, audio, audio_have_voice)
# 存储音频数据用于声纹识别 # 存储音频数据用于声纹识别
if not hasattr(conn, 'asr_audio_for_voiceprint'): if not hasattr(conn, "asr_audio_for_voiceprint"):
conn.asr_audio_for_voiceprint = [] conn.asr_audio_for_voiceprint = []
conn.asr_audio_for_voiceprint.append(audio) conn.asr_audio_for_voiceprint.append(audio)
@@ -146,8 +151,10 @@ class ASRProvider(ASRProviderBase):
# 发送首帧音频 # 发送首帧音频
if conn.asr_audio and len(conn.asr_audio) > 0: if conn.asr_audio and len(conn.asr_audio) > 0:
first_audio = conn.asr_audio[-1] if conn.asr_audio else b'' first_audio = conn.asr_audio[-1] if conn.asr_audio else b""
pcm_frame = self.decoder.decode(first_audio, 960) if first_audio else b'' pcm_frame = (
self.decoder.decode(first_audio, 960) if first_audio else b""
)
await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME) await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME)
self.server_ready = True self.server_ready = True
logger.bind(tag=TAG).info("已发送首帧,开始识别") logger.bind(tag=TAG).info("已发送首帧,开始识别")
@@ -176,23 +183,14 @@ class ASRProvider(ASRProviderBase):
if not self.asr_ws: if not self.asr_ws:
return return
audio_b64 = base64.b64encode(audio_data).decode('utf-8') audio_b64 = base64.b64encode(audio_data).decode("utf-8")
frame_data = { frame_data = {
"header": { "header": {"status": status, "app_id": self.app_id},
"status": status, "parameter": {"iat": self.iat_params},
"app_id": self.app_id
},
"parameter": {
"iat": self.iat_params
},
"payload": { "payload": {
"audio": { "audio": {"audio": audio_b64, "sample_rate": 16000, "encoding": "raw"}
"audio": audio_b64, },
"sample_rate": 16000,
"encoding": "raw"
}
}
} }
await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False)) await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False))
@@ -207,11 +205,13 @@ class ASRProvider(ASRProviderBase):
try: try:
while self.asr_ws and not conn.stop_event.is_set(): while self.asr_ws and not conn.stop_event.is_set():
# 获取当前连接的音频数据 # 获取当前连接的音频数据
audio_data = getattr(conn, 'asr_audio_for_voiceprint', []) audio_data = getattr(conn, "asr_audio_for_voiceprint", [])
try: try:
# 如果已发送最终帧,增加超时时间等待完整结果 # 如果已发送最终帧,增加超时时间等待完整结果
timeout = 3.0 if self.last_frame_sent else 30.0 timeout = 3.0 if self.last_frame_sent else 30.0
response = await asyncio.wait_for(self.asr_ws.recv(), timeout=timeout) response = await asyncio.wait_for(
self.asr_ws.recv(), timeout=timeout
)
result = json.loads(response) result = json.loads(response)
logger.bind(tag=TAG).debug(f"收到ASR结果: {result}") logger.bind(tag=TAG).debug(f"收到ASR结果: {result}")
@@ -221,7 +221,9 @@ class ASRProvider(ASRProviderBase):
status = header.get("status", 0) status = header.get("status", 0)
if code != 0: if code != 0:
logger.bind(tag=TAG).error(f"识别错误,错误码: {code}, 消息: {header.get('message', '')}") logger.bind(tag=TAG).error(
f"识别错误,错误码: {code}, 消息: {header.get('message', '')}"
)
if code in [10114, 10160]: # 连接问题 if code in [10114, 10160]: # 连接问题
break break
continue continue
@@ -231,52 +233,124 @@ class ASRProvider(ASRProviderBase):
text_data = payload["result"]["text"] text_data = payload["result"]["text"]
if text_data: if text_data:
# 解码base64文本 # 解码base64文本
decoded_text = base64.b64decode(text_data).decode('utf-8') decoded_text = base64.b64decode(text_data).decode("utf-8")
text_json = json.loads(decoded_text) text_json = json.loads(decoded_text)
# 提取文本内容 # 提取文本内容
text_ws = text_json.get('ws', []) text_ws = text_json.get("ws", [])
result_text = '' result_text = ""
for i in text_ws: for i in text_ws:
for j in i.get("cw", []): for j in i.get("cw", []):
w = j.get("w", "") w = j.get("w", "")
result_text += w result_text += w
# 更新识别文本 - 实时更新策略 # 更新识别文本 - 实时更新策略
if result_text and result_text.strip() not in ['', '', '.', ',', '']: # 只检查是否为空字符串,不再过滤任何标点符号
# 这样可以确保所有识别到的内容,包括标点符号都能被实时更新
if result_text and result_text.strip():
# 实时更新:正常情况下都更新,提高响应速度 # 实时更新:正常情况下都更新,提高响应速度
should_update = True should_update = True
# 保存最佳文本(最长的有意义文本) # 保存最佳文本
if (len(result_text) > len(self.best_text) and # 1. 如果是识别完成状态或最终帧后收到的结果,优先保存
result_text.strip() not in ['', '?', '', '.']): # 2. 否则保存最长的有意义文本
self.best_text = result_text # 取消对标点符号的过滤,只检查是否为空
logger.bind(tag=TAG).debug(f"保存最佳文本: {self.best_text}") # 这样可以保留所有识别到的内容,包括各种标点符号
is_valid_text = len(result_text.strip()) > 0
# 如果已发送最终帧,只过滤明显的无效结果 if (
self.last_frame_sent or status == 2
) and is_valid_text:
self.best_text = result_text
self.has_final_result = True # 标记已收到最终结果
logger.bind(tag=TAG).debug(
f"保存最终识别结果: {self.best_text}"
)
elif (
len(result_text) > len(self.best_text)
and is_valid_text
and not self.has_final_result
):
self.best_text = result_text
logger.bind(tag=TAG).debug(
f"保存中间最佳文本: {self.best_text}"
)
# 如果已发送最终帧,只过滤空文本
if self.last_frame_sent: if self.last_frame_sent:
# 最终帧后拒绝问号等明显错误的结果 # 只拒绝完全空的结果
if result_text.strip() in ['', '?', '', '.']: if not result_text.strip():
should_update = False should_update = False
logger.bind(tag=TAG).warning(f"最终帧后拒绝无效文本: {result_text}") logger.bind(tag=TAG).warning(
f"最终帧后拒绝空文本"
)
if should_update: if should_update:
self.text = result_text # 处理流式识别结果,避免简单替换导致内容丢失
logger.bind(tag=TAG).info(f"实时更新识别文本: {self.text} (最终帧已发送: {self.last_frame_sent})") # 1. 如果是中间状态(非最终帧后),可能需要替换为更完整的识别
# 2. 如果是最终帧后收到的结果,可能是对前面文本的补充
if self.last_frame_sent:
# 最终帧后收到的结果可能是标点符号等补充内容
# 检查是否需要合并文本而不是替换
# 如果当前文本是纯标点而前面已有内容,应该追加而不是替换
if len(
self.text
) > 0 and result_text.strip() in [
"",
".",
"?",
"",
"!",
"",
",",
"",
";",
"",
]:
# 对于标点符号,追加到现有文本后
self.text = (
self.text.rstrip().rstrip("。.")
+ result_text
)
else:
# 其他情况保持替换逻辑
self.text = result_text
else:
# 中间状态替换为新的识别结果
self.text = result_text
logger.bind(tag=TAG).info(
f"实时更新识别文本: {self.text} (最终帧已发送: {self.last_frame_sent})"
)
# 识别完成,但如果还没发送最终帧,继续等待 # 识别完成,但如果还没发送最终帧,继续等待
if status == 2: if status == 2:
logger.bind(tag=TAG).info(f"识别完成状态已到达,当前识别文本: {self.text}") logger.bind(tag=TAG).info(
f"识别完成状态已到达,当前识别文本: {self.text}"
)
# 如果还没发送最终帧,继续等待 # 如果还没发送最终帧,继续等待
if not self.last_frame_sent: if not self.last_frame_sent:
logger.bind(tag=TAG).info("识别完成但最终帧未发送,继续等待...") logger.bind(tag=TAG).info(
"识别完成但最终帧未发送,继续等待..."
)
continue continue
# 已发送最终帧且收到完成状态,使用最佳文本作为最终结果 # 已发送最终帧且收到完成状态,使用最佳策略选择最终结果
if self.best_text and len(self.best_text) > len(self.text): # 优先使用识别完成状态下的最新结果,而不是仅仅基于长度
logger.bind(tag=TAG).info(f"使用最佳文本作为最终结果: {self.text} -> {self.best_text}") if self.best_text:
self.text = self.best_text # 如果当前文本是在最终帧发送后或识别完成状态下收到的,优先使用
if (
self.last_frame_sent or status == 2
) and self.text.strip():
logger.bind(tag=TAG).info(
f"使用完成状态下的最新识别结果: {self.text}"
)
elif len(self.best_text) > len(self.text):
logger.bind(tag=TAG).info(
f"使用更长的最佳文本作为最终结果: {self.text} -> {self.best_text}"
)
self.text = self.best_text
logger.bind(tag=TAG).info(f"获取到最终完整文本: {self.text}") logger.bind(tag=TAG).info(f"获取到最终完整文本: {self.text}")
conn.reset_vad_states() conn.reset_vad_states()
@@ -289,9 +363,13 @@ class ASRProvider(ASRProviderBase):
if self.last_frame_sent: if self.last_frame_sent:
# 超时时也使用最佳文本 # 超时时也使用最佳文本
if self.best_text and len(self.best_text) > len(self.text): if self.best_text and len(self.best_text) > len(self.text):
logger.bind(tag=TAG).info(f"超时,使用最佳文本: {self.text} -> {self.best_text}") logger.bind(tag=TAG).info(
f"超时,使用最佳文本: {self.text} -> {self.best_text}"
)
self.text = self.best_text self.text = self.best_text
logger.bind(tag=TAG).info(f"最终帧后超时,使用结果: {self.text}") logger.bind(tag=TAG).info(
f"最终帧后超时,使用结果: {self.text}"
)
break break
# 如果还没发送最终帧,继续等待 # 如果还没发送最终帧,继续等待
continue continue
@@ -316,11 +394,11 @@ class ASRProvider(ASRProviderBase):
self.asr_ws = None self.asr_ws = None
self.is_processing = False self.is_processing = False
if conn: if conn:
if hasattr(conn, 'asr_audio_for_voiceprint'): if hasattr(conn, "asr_audio_for_voiceprint"):
conn.asr_audio_for_voiceprint = [] conn.asr_audio_for_voiceprint = []
if hasattr(conn, 'asr_audio'): if hasattr(conn, "asr_audio"):
conn.asr_audio = [] conn.asr_audio = []
if hasattr(conn, 'has_valid_voice'): if hasattr(conn, "has_valid_voice"):
conn.has_valid_voice = False conn.has_valid_voice = False
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]): async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
@@ -330,7 +408,7 @@ class ASRProvider(ASRProviderBase):
if self.asr_ws and self.is_processing: if self.asr_ws and self.is_processing:
try: try:
# 取最后一个有效的音频帧作为最后一帧数据 # 取最后一个有效的音频帧作为最后一帧数据
last_frame = b'' last_frame = b""
if asr_audio_task: if asr_audio_task:
last_audio = asr_audio_task[-1] last_audio = asr_audio_task[-1]
last_frame = self.decoder.decode(last_audio, 960) last_frame = self.decoder.decode(last_audio, 960)
@@ -338,8 +416,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).info("已发送最后一帧") logger.bind(tag=TAG).info("已发送最后一帧")
# 发送最终帧后,给_forward_results适当时间处理最终结果 # 发送最终帧后,给_forward_results适当时间处理最终结果
# 减少等待时间,提高响应速度 await asyncio.sleep(0.25)
await asyncio.sleep(0.1) # 从3秒减少到1秒
logger.bind(tag=TAG).info(f"准备处理最终识别结果: {self.text}") logger.bind(tag=TAG).info(f"准备处理最终识别结果: {self.text}")
except Exception as e: except Exception as e:
@@ -350,8 +427,9 @@ class ASRProvider(ASRProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理语音停止失败: {e}") logger.bind(tag=TAG).error(f"处理语音停止失败: {e}")
import traceback import traceback
logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}") logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}")
def stop_ws_connection(self): def stop_ws_connection(self):
if self.asr_ws: if self.asr_ws:
asyncio.create_task(self.asr_ws.close()) asyncio.create_task(self.asr_ws.close())
@@ -360,12 +438,14 @@ class ASRProvider(ASRProviderBase):
async def _cleanup(self, conn): async def _cleanup(self, conn):
"""清理资源""" """清理资源"""
logger.bind(tag=TAG).info(f"开始ASR会话清理 | 当前状态: processing={self.is_processing}, server_ready={self.server_ready}") logger.bind(tag=TAG).info(
f"开始ASR会话清理 | 当前状态: processing={self.is_processing}, server_ready={self.server_ready}"
)
# 发送最后一帧 # 发送最后一帧
if self.asr_ws and self.is_processing: if self.asr_ws and self.is_processing:
try: try:
await self._send_audio_frame(b'', STATUS_LAST_FRAME) await self._send_audio_frame(b"", STATUS_LAST_FRAME)
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
logger.bind(tag=TAG).info("已发送最后一帧") logger.bind(tag=TAG).info("已发送最后一帧")
except Exception as e: except Exception as e:
@@ -376,6 +456,7 @@ class ASRProvider(ASRProviderBase):
self.server_ready = False self.server_ready = False
self.last_frame_sent = False self.last_frame_sent = False
self.best_text = "" self.best_text = ""
self.has_final_result = False
logger.bind(tag=TAG).info("ASR状态已重置") logger.bind(tag=TAG).info("ASR状态已重置")
# 清理任务 # 清理任务
@@ -403,11 +484,11 @@ class ASRProvider(ASRProviderBase):
# 清理连接的音频缓存 # 清理连接的音频缓存
if conn: if conn:
if hasattr(conn, 'asr_audio_for_voiceprint'): if hasattr(conn, "asr_audio_for_voiceprint"):
conn.asr_audio_for_voiceprint = [] conn.asr_audio_for_voiceprint = []
if hasattr(conn, 'asr_audio'): if hasattr(conn, "asr_audio"):
conn.asr_audio = [] conn.asr_audio = []
if hasattr(conn, 'has_valid_voice'): if hasattr(conn, "has_valid_voice"):
conn.has_valid_voice = False conn.has_valid_voice = False
logger.bind(tag=TAG).info("ASR会话清理完成") logger.bind(tag=TAG).info("ASR会话清理完成")
@@ -432,11 +513,11 @@ class ASRProvider(ASRProviderBase):
self.forward_task = None self.forward_task = None
self.is_processing = False self.is_processing = False
# 清理所有连接的音频缓冲区 # 清理所有连接的音频缓冲区
if hasattr(self, '_connections'): if hasattr(self, "_connections"):
for conn in self._connections.values(): for conn in self._connections.values():
if hasattr(conn, 'asr_audio_for_voiceprint'): if hasattr(conn, "asr_audio_for_voiceprint"):
conn.asr_audio_for_voiceprint = [] conn.asr_audio_for_voiceprint = []
if hasattr(conn, 'asr_audio'): if hasattr(conn, "asr_audio"):
conn.asr_audio = [] conn.asr_audio = []
if hasattr(conn, 'has_valid_voice'): if hasattr(conn, "has_valid_voice"):
conn.has_valid_voice = False conn.has_valid_voice = False
@@ -358,7 +358,9 @@ async def call_mcp_endpoint_tool(
} }
message = json.dumps(payload) message = json.dumps(payload)
logger.bind(tag=TAG).info(f"发送MCP接入点工具调用请求: {actual_name},参数: {args}") logger.bind(tag=TAG).info(
f"发送MCP接入点工具调用请求: {actual_name},参数: {json.dumps(arguments, ensure_ascii=False)}"
)
await mcp_client.send_message(message) await mcp_client.send_message(message)
try: try:

Some files were not shown because too many files have changed in this diff Show More