Compare commits

...
316 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
欣南科技andGitHub 31aeb63f89 Merge pull request #2305 from xinnan-tech/his_download
add:首页智能体添加搜索记录
2025-10-01 02:53:09 +08:00
hrz 0175a7a1d4 add:首页智能体添加搜索记录 2025-10-01 02:52:32 +08:00
欣南科技andGitHub 1417ee017e Merge pull request #2304 from xinnan-tech/his_download
update:下载智能体的聊天记录
2025-10-01 01:33:01 +08:00
hrz 5b4d79902f update:下载智能体的聊天记录 2025-10-01 01:24:13 +08:00
3030332422 f10fddc59a update:新增移动端忘记密码前端页面 2025-09-30 17:52:38 +08:00
hrzandGitHub ed07793c47 Merge pull request #2288 from xinnan-tech/sm2_test
登录、注册页面添加非对称加密
2025-09-30 11:12:50 +08:00
hrz 2ed2e9c391 update:优化移动端按钮 2025-09-30 11:10:48 +08:00
3030332422 2b9fed2609 update:新增SM2解密工具类,优化登录注册密码找回功能 2025-09-30 09:07:06 +08:00
3030332422 848ce65d13 update:web端重置密码页面加密 2025-09-29 17:57:53 +08:00
hrzandGitHub f0512ce1cc Merge pull request #2302 from xinnan-tech/CSS_update
调整底框位置
2025-09-29 17:57:24 +08:00
LiJinHui 313fad1f81 样式调整 2025-09-29 17:56:19 +08:00
3030332422 ff26a84988 fix:完善重置密码页面多语言支持 2025-09-29 16:51:23 +08:00
3030332422 f7ee54598e fix:修复移动端创建智能体时显示问题 2025-09-29 16:09:28 +08:00
LiJinHui 7a62462c1b 调整底框位置 2025-09-29 15:18:46 +08:00
3030332422 a4433ebe68 fix:修复移动端注册页面的一些bug 2025-09-29 14:21:41 +08:00
3030332422 48feea55fe update:移动端注册页面加密 2025-09-29 10:29:28 +08:00
3030332422 6e5f708f5a update:移动端登录页面加密 2025-09-28 22:40:05 +08:00
hrzandGitHub f2ed645f9c Merge pull request #2290 from xinnan-tech/jia_mi_miyao
模型秘钥加密
2025-09-28 18:32:53 +08:00
hrz d47664e670 update:优化输入框提示 2025-09-28 18:25:29 +08:00
LiJinHui 8f3229ddda 调整 input onfocus 判断 2025-09-28 17:37:06 +08:00
LiJinHui 4f2b7524aa 改用精准匹配生成提示词 2025-09-28 17:20:09 +08:00
LiJinHui 4e99e176e9 删除多余样式 2025-09-28 17:05:17 +08:00
LiJinHui cfc7047967 页面优化 2025-09-28 16:52:28 +08:00
LiJinHui 776b5fb3b4 修复加密的bug 2025-09-28 15:27:46 +08:00
3030332422 f16d42d2b5 Merge branch 'sm2_test' of https://github.com/xinnan-tech/xiaozhi-esp32-server into sm2_test 2025-09-28 14:18:44 +08:00
3030332422 344d183b6f fix:修复web端手机号码注册时遇到的问题 2025-09-28 14:06:13 +08:00
LiJinHui feb668c96c 还原Python的端口 2025-09-28 10:28:01 +08:00
LiJinHui f345568675 修复秘钥污染的bug 2025-09-28 09:32:54 +08:00
hrz 11207f793d update:登录类型按钮居中显示 2025-09-26 23:56:40 +08:00
hrz ce09eb48c4 update:共钥信息从旧接口获取,去除getServerPublicKey方法 2025-09-26 23:39:27 +08:00
hrzandGitHub 6925732f72 Merge pull request #2285 from myifeng/prompt-template-args
提示词模板支持更多参数
2025-09-26 19:07:41 +08:00
hrzandGitHub dd9d08c8c3 Merge pull request #2291 from xinnan-tech/performance
update:更新测试工具以测试讯飞流式ASR、TTS首词响应时间
2025-09-26 19:06:28 +08:00
hrzandGitHub 1443c883f1 Merge pull request #2292 from xinnan-tech/mobile-i18n
updata:移动端添加语言切换功能
2025-09-26 19:05:21 +08:00
hrzandGitHub 30676e2170 Merge branch 'main' into sm2_test 2025-09-26 19:03:23 +08:00
rainv123 e41e6d4284 updata:删除无用功能 2025-09-26 17:10:47 +08:00
rainv123 1a7b1d24fa updata:移动端添加语言切换功能 2025-09-26 17:03:17 +08:00
rainv123 99443d1c9f updata:移动端添加语言切换功能 2025-09-26 14:54:56 +08:00
rainv123 a84b8d818e updata:移动端添加语言切换功能 2025-09-26 14:53:16 +08:00
LiJinHui ef4abf08f0 格式调整 2025-09-26 14:44:20 +08:00
rainv123 dfcac51312 updata:移动端添加语言切换功能 2025-09-26 14:33:06 +08:00
3030332422 c963a2de18 fix:修复验证图形验证码 2025-09-26 12:58:28 +08:00
3030332422 9ec8ecf7f4 update:SM2非对称加密的优化 2025-09-26 11:10:42 +08:00
FAN-yeB 5d77c96d76 update:更新测试工具以测试讯飞流式ASR、TTS首词响应时间 2025-09-26 11:05:00 +08:00
LiJinHui a8932f3743 秘钥加密 2025-09-26 10:04:43 +08:00
欣南科技andGitHub d022dd385a Merge pull request #2286 from xinnan-tech/Data_display_update
Data display update
2025-09-25 16:12:42 +08:00
hrz b2dfbb7973 update:优化判断 2025-09-25 16:12:02 +08:00
LiJinHui 7fee922796 更新设备数量显示(实时) 2025-09-25 15:56:51 +08:00
3030332422 9b6acf1ffc fix:修复SM2非对称加密的各种问题 2025-09-25 15:50:32 +08:00
hrzandGitHub a0e7cb56dc Merge pull request #2284 from xinnan-tech/mqtt_doc
update:更新mqtt网关集成文档
2025-09-25 14:54:51 +08:00
hrz f8447033b9 update:优化 2025-09-25 14:54:05 +08:00
hrz bb07015feb update:抽离公共教程到最前面 2025-09-25 14:51:34 +08:00
Chingfeng Li 111d945a9a 提示词模板支持更多参数 2025-09-25 14:35:01 +08:00
FAN-yeB d1587cc645 update:更新mqtt网关集成文档 2025-09-25 14:31:23 +08:00
欣南科技andGitHub e50e21c240 Merge pull request #2279 from xinnan-tech/update_doc
update:更新文档
2025-09-25 12:26:14 +08:00
hrz 9bbcf7f31a update:更新文档 2025-09-25 12:25:36 +08:00
hrzandGitHub 4f29e375c3 Merge pull request #2277 from xinnan-tech/py_fix_tts
fix: Markdown调整
2025-09-25 11:05:12 +08:00
Sakura-RanChen e15c784bdb fix: 文档链接 2025-09-25 10:57:12 +08:00
Sakura-RanChen e77a3b79cd fix: Markdown调整 2025-09-25 10:17:56 +08:00
3030332422 7ac5113fef update:非对称加密注册功能 2025-09-25 09:15:41 +08:00
hrzandGitHub ae58537979 Merge pull request #2275 from xinnan-tech/py_indexTTS
update: 更新文档
2025-09-24 17:01:02 +08:00
Sakura-RanChen ebf10828f0 update: 更新文档 2025-09-24 16:35:26 +08:00
3030332422 33bad7ed40 update:前端SM2非对称加密登录功能 2025-09-23 20:22:12 +08:00
hrzandGitHub af9ed0cd86 Merge pull request #2266 from xinnan-tech/py_mqtt
update:增加单模块mqtt-gateway支持
2025-09-23 11:51:41 +08:00
FAN-yeB 14a0829190 更新ota下发mqtt协议逻辑 2025-09-23 11:37:28 +08:00
FAN-yeB ca400d8ea7 Revert "更改下发配置"
This reverts commit 8891a96731.
2025-09-23 11:29:05 +08:00
hrzandGitHub d28750dae0 Merge pull request #2264 from xinnan-tech/py_xunfei_tts
update:添加讯飞语音识别、大模型、语音合成供应器
2025-09-23 11:06:00 +08:00
hrzandGitHub ef81a17311 Merge pull request #2273 from xinnan-tech/i18n-fix
fix:修改用户管理界面用户的密码重置、禁用、删除的逻辑
2025-09-23 11:05:17 +08:00
FAN-yeB 8891a96731 更改下发配置 2025-09-23 10:45:25 +08:00
rainv123 49e69ecad4 fix:修改用户管理界面用户的密码重置、禁用、删除的逻辑 2025-09-23 10:16:17 +08:00
rainv123 1db2483557 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-23 10:08:55 +08:00
rainv123 7067050006 Revert "fix:修改用户管理界面的用户密码重置、禁用、删除的逻辑"
This reverts commit 9f303d840a.
2025-09-23 10:03:43 +08:00
FAN-yeBandGitHub 55b455216c Merge pull request #2267 from xinnan-tech/i18n-fix
i18n-fix
2025-09-23 09:58:36 +08:00
3030332422 ffba2f4fa3 update:添加SM2国密加密的登录接口 2025-09-23 09:58:27 +08:00
rainv123 9f303d840a fix:修改用户管理界面的用户密码重置、禁用、删除的逻辑 2025-09-23 09:49:13 +08:00
FAN-yeB 86b4e61c1c 添加讯飞星火大模型的配置示例 2025-09-23 09:06:27 +08:00
欣南科技andGitHub 3232b36e15 Merge pull request #2271 from xinnan-tech/fix-docker-web
update:优化编译
2025-09-22 21:35:04 +08:00
hrz 83329cf9db update:优化编译 2025-09-22 21:34:09 +08:00
rainv123 4608c81ea6 updata:补充国际化键名 2025-09-22 15:04:49 +08:00
rainv123 41679f4e1d updata:调整文本顺序 2025-09-22 14:55:22 +08:00
FAN-yeB 559699ec19 update:增加单模块mqtt-gateway支持 2025-09-22 14:18:03 +08:00
hrzandGitHub f785219a43 Merge pull request #2265 from xinnan-tech/LJH
update:修改排序
2025-09-22 11:24:41 +08:00
FAN-yeB bf2b6d7554 还原错误更改 2025-09-22 10:10:46 +08:00
FAN-yeB 9a6348c71a 更新 202509220958.sql 2025-09-22 10:01:30 +08:00
FAN-yeB 646f4cbfe9 更新讯飞星火认知大模型配置的说明文档 2025-09-22 10:00:29 +08:00
LiJinHui d4c6b6840e 增加排序更新逻辑 2025-09-22 09:52:21 +08:00
LiJinHui 758aa1c78c 修改排序的逻辑 2025-09-22 09:41:18 +08:00
FAN-yeB ec2b52ccac 优化讯飞asr参数 2025-09-22 09:34:05 +08:00
FAN-yeB 38e1eb0c18 更新 202509191732.sql 2025-09-22 09:25:43 +08:00
hrzandGitHub 00e8478f61 Merge pull request #2251 from xinnan-tech/LJH
update:新增调整角色模版的功能,添加新功能的国际化
2025-09-21 18:10:05 +08:00
hrzandGitHub 0c556b1424 Merge branch 'main' into LJH 2025-09-21 18:09:48 +08:00
hrz 9987555a72 Bump to v0.8.3 2025-09-21 18:07:48 +08:00
hrz ebb4ba5595 update:调整功能顺序 2025-09-21 18:04:56 +08:00
LiJinHui 7e338b28df 删除多余定义 2025-09-21 16:44:18 +08:00
LiJinHui c4b3e30c5d 删除多的定义 2025-09-21 16:41:53 +08:00
LiJinHui 4d283c3504 去掉调试代码 2025-09-21 01:51:35 +08:00
LiJinHui ffc1541343 删除多余代码 2025-09-21 00:25:23 +08:00
LiJinHui 588d75763c 删除多余样式 2025-09-20 23:43:58 +08:00
LiJinHui 9e218281da 调整代码 2025-09-20 17:30:00 +08:00
欣南科技andGitHub 6fa58c7948 Merge pull request #2257 from xinnan-tech/update_doc
update:优化图片
2025-09-20 08:13:51 +08:00
hrz 7fe40579b9 update:优化图片 2025-09-20 08:13:04 +08:00
欣南科技andGitHub 7e2782ce22 Merge pull request #2256 from xinnan-tech/update_doc
update:架构图增加mqtt
2025-09-20 08:04:30 +08:00
hrz 1fbe8dab77 update:架构图增加mqtt 2025-09-20 08:04:00 +08:00
欣南科技andGitHub 89ef36711a Merge pull request #2255 from xinnan-tech/update-version
update:推出0.8.2
2025-09-20 01:32:28 +08:00
hrz 0ba3e2c964 update:推出0.8.2 2025-09-20 01:31:48 +08:00
LiJinHui 0aec742bd3 格式统一 2025-09-19 18:33:54 +08:00
LiJinHui a6f5d868dd 格式调整 2025-09-19 18:18:18 +08:00
FAN-yeB 966d8a8a84 update:xunfei asr 2025-09-19 17:54:52 +08:00
LiJinHui 611d1a65e7 Update index.js
权限调整
2025-09-19 17:53:43 +08:00
LiJinHui b23e31bb4b Delete DefaultRoleDialog.vue
删除多余文件
2025-09-19 17:41:11 +08:00
Sakura-RanChen bec1c7b88f update: 增加讯飞双流式TTS 2025-09-19 17:29:09 +08:00
LiJinHui 5d407b9227 调整模版排序的漏洞 2025-09-19 17:18:24 +08:00
CGDandGitHub e5bc029d87 Merge pull request #2252 from xinnan-tech/manager-web-fix
updata:删除多余的isEnglish方法
2025-09-19 16:06:39 +08:00
LiJinHui 608abb7dc3 调整页面 2025-09-19 15:41:28 +08:00
LiJinHui 5fcc7592c9 优化代码,调整权限设置( 非管理员打开提示拒绝访问并自动跳转到首页),修复bug; 2025-09-19 15:34:43 +08:00
rainv123 3fb925f13d updata:删除多余的getServerList方法,删除重复定义的computed块 2025-09-19 11:37:24 +08:00
rainv123 4e958ce57c updata:删除多余的isEnglish方法 2025-09-19 11:15:11 +08:00
rainv123 7f6ce89e39 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-19 09:33:47 +08:00
欣南科技andGitHub 71274cf0a3 Merge pull request #2249 from xinnan-tech/hot-fix
update:优化未配置mqtt网关时的显示
2025-09-18 19:08:56 +08:00
hrz 58127cdfdc update:优化未配置mqtt网关时的显示 2025-09-18 19:08:24 +08:00
LiJinHui 09d5d37867 完善导航栏的语言切换 2025-09-18 16:54:00 +08:00
LiJinHui 6a8c4ccbf8 添加国际化的功能,调整页面 2025-09-18 16:43:52 +08:00
hrzandGitHub 6d7bbfca95 Merge pull request #2247 from xinnan-tech/mqtt-call
Mqtt 远程工具调用
2025-09-18 15:02:45 +08:00
hrzandGitHub 3de39c8cdc Merge branch 'main' into mqtt-call 2025-09-18 15:02:32 +08:00
3030332422 387398a069 update:添加多语言支持并增强搜索功能 2025-09-18 14:42:03 +08:00
3030332422 f17011939c update:优化设备工具调用界面与执行结果展示体验 2025-09-18 10:56:41 +08:00
rainv123 9b6f564d30 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-18 10:54:30 +08:00
rainv123andGitHub e23989add0 Merge pull request #2239 from xinnan-tech/py_AliBL_TTS
update: 阿里百炼流式TTS 减少调试日志显示
2025-09-18 10:22:42 +08:00
LiJinHui a199e47701 页面布局调整 2025-09-18 09:35:03 +08:00
3030332422 4f64316144 update: 实现设备工具动态调用和执行功能 2025-09-17 17:40:20 +08:00
rainv123 68e10f42f8 Merge branch 'py_AliBL_TTS' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-17 17:02:47 +08:00
FAN-yeBandGitHub 3d1cae4692 Merge pull request #2242 from xinnan-tech/manager-api-msg-i18n
updata:实现后端msg的国际化处理,修改用户注册页面和添加模型页面的硬编码为国际化键名
2025-09-17 16:46:45 +08:00
3030332422 31e84dadd1 update:动态获取设备工具列表并优化显示逻辑 2025-09-17 16:25:46 +08:00
LiJinHui c331b8322c 新增调整角色模版的功能 2025-09-17 15:38:23 +08:00
rainv123 ba4423684b Merge branch 'manager-api-msg-i18n' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-17 15:36:31 +08:00
rainv123 62eaf46f34 updata:实现后端msg的国际化处理,修改用户注册页面和添加模型页面的硬编码为国际化键名 2025-09-17 15:35:21 +08:00
FAN-yeB 3767bf8c81 更新 messages.properties 2025-09-17 15:31:54 +08:00
rainv123 300a33ddc2 fix:智能体添加设备页面的按钮文字修正 2025-09-17 14:10:07 +08:00
rainv123 34d7bd92ca updata:实现后端msg的国际化处理,修改用户注册页面和添加模型页面的硬编码为国际化键名 2025-09-17 11:36:25 +08:00
3030332422 44e7380c6b update: 添加设备管理页面实时在线状态显示功能 2025-09-17 11:36:20 +08:00
FAN-yeB 15ca08ce14 update:接口 2025-09-17 10:04:19 +08:00
hrzandGitHub 83b88eeaa0 Merge pull request #2240 from myifeng/prompt-device-id
添加设备ID信息到提示词
2025-09-16 18:21:02 +08:00
Chingfeng Li 290c92ee39 添加设备ID信息到提示词 2025-09-16 17:42:15 +08:00
Sakura-RanChen c2985c0db3 update: 阿里百炼流式TTS 减少调试日志显示
fix: 音频发送误差修正
2025-09-16 17:07:00 +08:00
Sakura-RanChenandGitHub 334dfc7917 Merge pull request #2231 from qingshuiyuyu/bugfix
修复-火山双流式语音合成-additions参数配置
2025-09-16 10:43:45 +08:00
hrz 03b51f73e7 update:远程工具调用面板 2025-09-16 00:06:57 +08:00
欣南科技andGitHub af079d6c18 Merge pull request #2236 from xinnan-tech/mqtt-sign-key
update:mqtt密钥禁止包含弱密码
2025-09-15 21:32:05 +08:00
hrz 99008cf813 update:mqtt密钥禁止包含弱密码 2025-09-15 21:31:22 +08:00
CGDandGitHub 5fcb670821 Merge pull request #2233 from xinnan-tech/manage-web-i18n-fix
fix:优化智控台页面的多语言支持
2025-09-15 14:49:38 +08:00
hrzandGitHub 3bdf8e20c7 Merge pull request #2232 from xinnan-tech/py_fix_index
fix: 参数修正
2025-09-15 13:20:16 +08:00
rainv123 e8d44b13cf fix:优化智控台页面的多语言支持 2025-09-15 11:32:57 +08:00
Sakura-RanChen c6a7871e5b fix: 参数修正 2025-09-15 10:33:56 +08:00
shiyin 4c9824b16f 修复-火山双流式语音合成-additions参数配置 2025-09-15 09:46:07 +08:00
CGDandGitHub 3e54a3272a Merge pull request #2229 from kerenskiy/patch-1
Update requirements.txt
2025-09-15 09:04:57 +08:00
kerenskiyandGitHub 8851406f84 Update requirements.txt
Fix VoskASR dependencies
2025-09-14 20:18:40 +03:00
欣南科技andGitHub fe85ea049a Merge pull request #2224 from xinnan-tech/hot-fix
update:mqtt密钥复杂度校验
2025-09-13 10:19:37 +08:00
hrz 221c01e642 update:mqtt密钥复杂度校验 2025-09-13 10:18:41 +08:00
欣南科技andGitHub 753aa280f6 Merge pull request #2223 from xinnan-tech/hot-fix
fix:保存完参数后,按钮还在saving状态的bug
2025-09-13 09:22:41 +08:00
hrz 64f3062eb2 fix:保存完参数后,按钮还在saving状态的bug 2025-09-13 09:22:11 +08:00
欣南科技andGitHub f5143d60b7 Merge pull request #2221 from xinnan-tech/i18n-update
update:优化英文显示
2025-09-13 00:49:12 +08:00
hrz e9fb36bf8f update:优化英文显示 2025-09-13 00:47:48 +08:00
200 changed files with 17735 additions and 2768 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
+9 -3
View File
@@ -18,9 +18,15 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
# 安装Nginx和字体库 # 安装Nginx和字体库
RUN apk update && \ RUN apk update && \
apk add --no-cache nginx bash fontconfig ttf-dejavu && \ apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ msttcorefonts-installer || true && \ nginx \
rm -rf /var/cache/apk/* bash \
fontconfig \
ttf-dejavu \
msttcorefonts-installer \
&& ACCEPT_EULA=Y apk add --no-cache msttcorefonts-installer \
&& fc-cache -f -v \
&& rm -rf /var/cache/apk/*
# 更新字体缓存 # 更新字体缓存
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v
+35 -40
View File
@@ -56,27 +56,43 @@ 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>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="docs/images/demo2.png" /> <img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture> <picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /> <img alt="声纹识别" src="docs/images/demo14.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>
@@ -85,18 +101,16 @@ 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/BV1kgA2eYEQ9" target="_blank"> <a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture> <picture>
<img alt="成本最低配置" src="docs/images/demo4.png" /> <img alt="MCP接入点" src="docs/images/demo13.png" />
</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/BV1TJ7WzzEo6" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="docs/images/demo6.png" /> <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>
@@ -241,7 +241,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
![请参考-全模块安装架构图](docs/images/deploy2.png) ![请参考-全模块安装架构图](docs/images/deploy2.png)
| 功能模块 | 描述 | | 功能模块 | 描述 |
|:---:|:---| |:---:|:---|
| 核心架构 | 基于MQTT+UDP网关、WebSocket、HTTP服务器,提供完整的控制台管理和认证系统 | | 核心架构 | 基于[MQTT+UDP网关](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md)、WebSocket、HTTP服务器,提供完整的控制台管理和认证系统 |
| 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 | | 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 | | 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 | | 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
@@ -249,7 +249,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 意图识别 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 | | 意图识别 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 |
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 | | 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 | | 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
| 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理 | | 指令下发 | 依托MQTT协议,支持从智控台将MCP指令下发到ESP32设备 |
| 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理;界面支持中文简体、中文繁体、英文显示 |
| 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 | | 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 |
| 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 | | 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 |
| 插件系统 | 支持功能插件扩展、自定义插件开发和插件热加载 | | 插件系统 | 支持功能插件扩展、自定义插件开发和插件热加载 |
@@ -263,13 +264,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
--- ---
## 产品生态 👬 ## 产品生态 👬
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目 小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的[优秀项目](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE)
| 项目名称 | 项目地址 | 项目描述 |
|:---------------------|:--------|:--------|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
--- ---
+41 -40
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> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture> <picture>
<img alt="Custom voice timbre" src="docs/images/demo2.png" /> <img alt="速度优化秘诀" src="docs/images/demo6.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="Using Cantonese for communication" src="docs/images/demo3.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank"> <a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture> <picture>
<img alt="Controlling home appliances" src="docs/images/demo5.png" /> <img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank"> <a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture> <picture>
<img alt="Lowest cost configuration" src="docs/images/demo4.png" /> <img alt="声纹识别" src="docs/images/demo14.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>
@@ -237,15 +237,16 @@ This project provides the following testing tools to help you verify the system
![请参考-全模块安装架构图](docs/images/deploy2.png) ![请参考-全模块安装架构图](docs/images/deploy2.png)
| Feature Module | Description | | Feature Module | Description |
|:---:|:---| |:---:|:---|
| Core Architecture | Based on WebSocket and HTTP servers, provides complete console management and authentication system | | Core Architecture | Based on [MQTT+UDP gateway](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md), WebSocket and HTTP servers, provides complete console management and authentication system |
| Voice Interaction | Supports streaming ASR(speech recognition), streaming TTS(speech synthesis), VAD(voice activity detection), supports multi-language recognition and voice processing | | Voice Interaction | Supports streaming ASR(speech recognition), streaming TTS(speech synthesis), VAD(voice activity detection), supports multi-language recognition and voice processing |
| Voiceprint Recognition | Supports multi-user voiceprint registration, management, and recognition, processes in parallel with ASR, real-time speaker identity recognition and passes to LLM for personalized responses | | Voiceprint Recognition | Supports multi-user voiceprint registration, management, and recognition, processes in parallel with ASR, real-time speaker identity recognition and passes to LLM for personalized responses |
| Intelligent Dialogue | Supports multiple LLM(large language models), implements intelligent dialogue | | Intelligent Dialogue | Supports multiple LLM(large language models), implements intelligent dialogue |
| Visual Perception | Supports multiple VLLM(vision large models), implements multimodal interaction | | Visual Perception | Supports multiple VLLM(vision large models), implements multimodal interaction |
| Intent Recognition | Supports LLM intent recognition, Function Call function calling, provides plugin-based intent processing mechanism | | Intent Recognition | Supports LLM intent recognition, Function Call function calling, provides plugin-based intent processing mechanism |
| Memory System | Supports local short-term memory, mem0ai interface memory, with memory summarization functionality | | Memory System | Supports local short-term memory, mem0ai interface memory, with memory summarization functionality |
| Command Delivery | Supports MCP command delivery to ESP32 devices via MQTT protocol from Smart Console |
| Tool Calling | Supports client IOT protocol, client MCP protocol, server MCP protocol, MCP endpoint protocol, custom tool functions | | Tool Calling | Supports client IOT protocol, client MCP protocol, server MCP protocol, MCP endpoint protocol, custom tool functions |
| Management Backend | Provides Web management interface, supports user management, system configuration, and device management | | Management Backend | Provides Web management interface, supports user management, system configuration and device management; Supports Simplified Chinese, Traditional Chinese and English display |
| Testing Tools | Provides performance testing tools, vision model testing tools, and audio interaction testing tools | | Testing Tools | Provides performance testing tools, vision model testing tools, and audio interaction testing tools |
| Deployment Support | Supports Docker deployment and local deployment, provides complete configuration file management | | Deployment Support | Supports Docker deployment and local deployment, provides complete configuration file management |
| Plugin System | Supports functional plugin extensions, custom plugin development, and plugin hot-loading | | Plugin System | Supports functional plugin extensions, custom plugin development, and plugin hot-loading |
@@ -259,7 +260,7 @@ If you are a software developer, here is an [Open Letter to Developers](docs/con
--- ---
## Product Ecosystem 👬 ## Product Ecosystem 👬
Xiaozhi is an ecosystem. When using this product, you can also check out other excellent projects in this ecosystem Xiaozhi is an ecosystem. When using this product, you can also check out other [excellent projects](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) in this ecosystem
| Project Name | Project Address | Project Description | | Project Name | Project Address | Project Description |
|:---------------------|:--------|:--------| |:---------------------|:--------|:--------|
@@ -280,7 +281,7 @@ Xiaozhi is an ecosystem. When using this product, you can also check out other e
| FastGPT interface calls | FastGPT | - | | FastGPT interface calls | FastGPT | - |
| Coze interface calls | Coze | - | | Coze interface calls | Coze | - |
In fact, any LLM that supports OpenAI interface calls can be integrated and used. In fact, any LLM that supports OpenAI interface calls can be integrated and used, including Xinference and HomeAssistant interfaces.
--- ---
@@ -298,7 +299,7 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use
| Usage Method | Supported Platforms | Free Platforms | | Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| Interface calls | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS, Lingxi Streaming TTS | Lingxi Streaming TTS, EdgeTTS, CosyVoiceSiliconflow(partial) | | Interface calls | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, AliYun Stream TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS, Lingxi Streaming TTS, MinimaxTTS | Lingxi Streaming TTS, EdgeTTS, CosyVoiceSiliconflow(partial) |
| Local services | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | | Local services | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
--- ---
+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: 404 KiB

After

Width:  |  Height:  |  Size: 84 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.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 260 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

+45 -15
View File
@@ -1,11 +1,18 @@
# IndexStreamTTS 使用指南 # IndexStreamTTS 使用指南
## 环境准备 ## 环境准备
### 1. 克隆项目 (这里使用的为VLLM的版本) ### 1. 克隆项目
```bash ```bash
git clone https://github.com/Ksuriuri/index-tts-vllm.git git clone https://github.com/Ksuriuri/index-tts-vllm.git
```
进入解压后的目录
```bash
cd index-tts-vllm cd index-tts-vllm
``` ```
切换到指定版本 (使用VLLM-0.10.2的历史版本)
```bash
git checkout 224e8d5e5c8f66801845c66b30fa765328fd0be3
```
### 2. 创建并激活 conda 环境 ### 2. 创建并激活 conda 环境
```bash ```bash
@@ -13,7 +20,7 @@ conda create -n index-tts-vllm python=3.12
conda activate index-tts-vllm conda activate index-tts-vllm
``` ```
### 3. 安装PyTorch ### 3. 安装PyTorch 需要版本为2.8.0(最新版)
#### 查看显卡最高支持的版本和实际安装的版本 #### 查看显卡最高支持的版本和实际安装的版本
```bash ```bash
nvidia-smi nvidia-smi
@@ -27,12 +34,11 @@ CUDA Version: 12.8
```bash ```bash
Cuda compilation tools, release 12.8, V12.8.89 Cuda compilation tools, release 12.8, V12.8.89
``` ```
#### 那么对应的安装命令 (请注意不要横跨大版本!!!) #### 那么对应的安装命令pytorch默认给的是12.8的驱动版本)
```bash ```bash
pip install torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu128 pip install torch torchvision
``` ```
优先建议安装 pytorch 2.7.0(对应 vllm 0.9.0),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/]\) 需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/)
若显卡不支持,请安装 pytorch 2.5.1(对应 vllm 0.7.3),并将 requirements.txt 中 vllm==0.9.0 修改为 vllm==0.7.3
### 4. 安装依赖 ### 4. 安装依赖
```bash ```bash
@@ -40,6 +46,7 @@ pip install -r requirements.txt
``` ```
### 5. 下载模型权重 ### 5. 下载模型权重
### 方案一:下载官方权重文件后转换
此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重 此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重
| HuggingFace | ModelScope | | HuggingFace | ModelScope |
|---------------------------------------------------------------|---------------------------------------------------------------------| |---------------------------------------------------------------|---------------------------------------------------------------------|
@@ -47,19 +54,19 @@ 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
``` ```
创建模型目录,并拉取模型 创建模型目录,并拉取模型
```bash ```bash
mkkdir model_dir mkdir model_dir
cd model_dir 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
``` ```
@@ -71,6 +78,9 @@ bash convert_hf_format.sh model_dir/IndexTTS-1.5
### 6. 更改接口适配一下项目 ### 6. 更改接口适配一下项目
接口返回数据与项目不适配需要调整一下,使其直接返回音频数据 接口返回数据与项目不适配需要调整一下,使其直接返回音频数据
```bash
vi api_server.py
```
```bash ```bash
@app.post("/tts", responses={ @app.post("/tts", responses={
200: {"content": {"application/octet-stream": {}}}, 200: {"content": {"application/octet-stream": {}}},
@@ -104,7 +114,7 @@ async def tts_api(request: Request):
vi start_api.sh vi start_api.sh
``` ```
### 将下面内容粘贴进去并按:输入wq保存 ### 将下面内容粘贴进去并按:输入wq保存
#### 脚本中的/home/system/indexTTS/index-tts-vllm/model_dir/IndexTTS-1.5 请自行修改为实际路径 #### 脚本中的/home/system/index-tts-vllm/model_dir/IndexTTS-1.5 请自行修改为实际路径
```bash ```bash
# 激活conda环境 # 激活conda环境
conda activate index-tts-vllm conda activate index-tts-vllm
@@ -129,26 +139,46 @@ else
echo "已终止进程 $PID_VLLM" echo "已终止进程 $PID_VLLM"
fi fi
# 查找占用VLLM::EngineCore进程
GPU_PIDS=$(ps aux | grep -E "VLLM|EngineCore" | grep -v grep | awk '{print $2}')
# 检查是否找到进程号
if [ -z "$GPU_PIDS" ]; then
echo "没有找到VLLM相关进程"
else
echo "找到VLLM相关进程,进程号为: $GPU_PIDS"
# 先尝试普通kill,等待2秒
kill $GPU_PIDS
sleep 2
# 检查进程是否还在
if ps -p $GPU_PIDS > /dev/null; then
echo "进程仍在运行,强制终止..."
kill -9 $GPU_PIDS
fi
echo "已终止进程 $GPU_PIDS"
fi
# 创建tmp目录(如果不存在) # 创建tmp目录(如果不存在)
mkdir -p tmp mkdir -p tmp
# 后台运行api_server.py,日志重定向到tmp/server.log # 后台运行api_server.py,日志重定向到tmp/server.log
export VLLM_USE_V1=0 nohup python api_server.py --model_dir /home/system/index-tts-vllm/model_dir/IndexTTS-1.5 --port 11996 > tmp/server.log 2>&1 &
nohup python api_server.py --model_dir /home/system/indexTTS/index-tts-vllm/model_dir/IndexTTS-1.5 --port 11996 > tmp/server.log 2>&1 &
echo "api_server.py 已在后台运行,日志请查看 tmp/server.log" echo "api_server.py 已在后台运行,日志请查看 tmp/server.log"
``` ```
给脚本执行权限并运行脚本 给脚本执行权限并运行脚本
```bash ```bash
chmod +x tmp chmod +x start_api.sh
./start_api.sh ./start_api.sh
``` ```
日志会在tmp/server.log中输出,可以通过以下命令查看日志情况 日志会在tmp/server.log中输出,可以通过以下命令查看日志情况
```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文件中配置自定义音色
### 配置格式说明 ### 配置格式说明
```bash ```bash
{ {
@@ -161,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`
+60 -8
View File
@@ -1,11 +1,12 @@
# MQTT 网关部署教程 # MQTT 网关部署教程
`xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。 `xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。
本教程分为三部分,你可以根据你是全模块部署还是单模块部署,选择对应的部分接入MQTT网关:
- 第一部分:部署MQTT网关
- 第二部分:全模块运行实现小智硬件MQTT+UDP连接
- 第三部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接
## 准备工作 ## 准备阶段
查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。
准备好你的`xiaozhi-server``mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址 准备好你的`xiaozhi-server``mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址
1、如果你是源码部署,你的`mqtt-websocket`地址是: 1、如果你是源码部署,你的`mqtt-websocket`地址是:
@@ -18,7 +19,16 @@ ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway
ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway
``` ```
## 第一步 部署MQTT网关 ## 重要提示
如果你是服务器部署,需要确保服务器`1883``8884``8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`
如果你是服务器部署,需要确保服务器`1883``8884``8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`
如果你是服务器部署,需要确保服务器`1883``8884``8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`
## 第一部分:部署MQTT网关
1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git) 1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git)
```bash ```bash
@@ -64,11 +74,16 @@ cp config/mqtt.json.example config/mqtt.json
PUBLIC_IP=your-ip # 服务器公网IP PUBLIC_IP=your-ip # 服务器公网IP
MQTT_PORT=1883 # MQTT服务器端口 MQTT_PORT=1883 # MQTT服务器端口
UDP_PORT=8884 # UDP服务器端口 UDP_PORT=8884 # UDP服务器端口
API_PORT=8007 # 管理API端口
MQTT_SIGNATURE_KEY=test # MQTT签名密钥 MQTT_SIGNATURE_KEY=test # MQTT签名密钥
``` ```
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。 请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
`MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,这个密钥稍后还会用到。 `MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,最好是设置成8个字符以上且同时包含大小写字母,这个密钥稍后还会用到。
- 注意不要用简单的密码,比如`123456`、`test`等。
- 注意不要用简单的密码,比如`123456`、`test`等。
- 注意不要用简单的密码,比如`123456`、`test`等。
6. 启动MQTT网关 6. 启动MQTT网关
``` ```
@@ -90,7 +105,9 @@ pm2 logs xz-mqtt
pm2 restart xz-mqtt pm2 restart xz-mqtt
``` ```
## 第二步 配置智控台 ## 第二部分:全模块运行实现小智硬件MQTT+UDP连接
查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。
1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样 1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
``` ```
@@ -102,6 +119,10 @@ pm2 restart xz-mqtt
``` ```
192.168.0.7:8884 192.168.0.7:8884
``` ```
4. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_manager_api`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
```
192.168.0.7:8007
```
上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址 上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址
``` ```
@@ -118,7 +139,38 @@ curl 'http://localhost:8002/xiaozhi/ota/' \
{"server_time":{"timestamp":1757567894012,"timeZone":"Asia/Shanghai","timezone_offset":480},"activation":{"code":"460609","message":"http://xiaozhi.server.com\n460609","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.4.23:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.0.7:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@7b94d69a-9808-4c59-9c9b-704333b38aff","username":"eyJpcCI6IjA6MDowOjA6MDowOjA6MSJ9","password":"Y8XP9xcUhVIN9OmbCHT9ETBiYNE3l3Z07Wk46wV9PE8=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}} {"server_time":{"timestamp":1757567894012,"timeZone":"Asia/Shanghai","timezone_offset":480},"activation":{"code":"460609","message":"http://xiaozhi.server.com\n460609","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.4.23:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.0.7:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@7b94d69a-9808-4c59-9c9b-704333b38aff","username":"eyJpcCI6IjA6MDowOjA6MDowOjA6MSJ9","password":"Y8XP9xcUhVIN9OmbCHT9ETBiYNE3l3Z07Wk46wV9PE8=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}}
``` ```
## 第三步 重启小智设备 由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
```
pm2 logs xz-mqtt
```
## 第三部分:全模块运行实现小智硬件MQTT+UDP连接
打开你的`data/.config.yaml`文件,在`server`下找到`mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
```
192.168.0.7:1883
```
在`server`下找到`mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。
在`server`下找到`udp_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
```
192.168.0.7:8884
```
上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址
```
curl 'http://localhost:8002/xiaozhi/ota/' \
-H 'Device-Id: 11:22:33:44:55:66' \
--data-raw $'{\n "application": {\n "version": "1.0.1",\n "elf_sha256": "1"\n },\n "board": {\n "mac": "11:22:33:44:55:66"\n }\n}'
```
如果返回的内容包含`mqtt`相关的配置,说明配置成功。类似这样
```
{"server_time":{"timestamp":1758781561083,"timeZone":"GMT+08:00","timezone_offset":480},"activation":{"code":"527111","message":"http://xiaozhi.server.com\n527111","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.1.15:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.1.15:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@11_22_33_44_55_66","username":"eyJpcCI6IjE5Mi4xNjguMS4xNSJ9","password":"fjAYs49zTJecWqJ3jBt+kqxVn/x7vkXRAc85ak/va7Y=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}}
```
由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。 由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。 唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
+16 -6
View File
@@ -1,5 +1,9 @@
# PaddleSpeechTTS集成xiaozhi服务 # PaddleSpeechTTS集成xiaozhi服务
## 重点说明
- 优点:本地离线部署、速度快
- 缺点:截止2025年9月25日,默认的模型是中文模型,不支持英文转语音。如果含英文会发不出声音,如需同时支持中英文需要自己训练。
## 一、基础环境要求 ## 一、基础环境要求
操作系统:Windows / Linux / WSL 2 操作系统:Windows / Linux / WSL 2
@@ -16,15 +20,21 @@ git clone https://github.com/PaddlePaddle/PaddleSpeech.git
``` ```
### 2.建立虚拟环境 ### 2.建立虚拟环境
```bash ```bash
#请根据Paddle官方支持的python版本建立环境 ```https://www.paddlepaddle.org.cn/install```
conda create -n paddle_env python=3.10 -y conda create -n paddle_env python=3.10 -y
conda activate paddle_env conda activate paddle_env
``` ```
### 3.进入paddlespeech目录 ### 3.安装paddle
因CPU架构、GPU架构不同,请根据Paddle官方支持的python版本建立环境
```
https://www.paddlepaddle.org.cn/install
```
### 4.进入paddlespeech目录
```bash ```bash
cd PaddleSpeech cd PaddleSpeech
``` ```
### 4.安装paddlespeech ### 5.安装paddlespeech
```bash ```bash
pip install pytest-runner -i https://pypi.tuna.tsinghua.edu.cn/simple pip install pytest-runner -i https://pypi.tuna.tsinghua.edu.cn/simple
@@ -32,17 +42,17 @@ pip install pytest-runner -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple
pip install paddlespeech -i https://pypi.tuna.tsinghua.edu.cn/simple pip install paddlespeech -i https://pypi.tuna.tsinghua.edu.cn/simple
``` ```
### 5.使用命令自动下载语音模型 ### 6.使用命令自动下载语音模型
```bash ```bash
paddlespeech tts --input "你好,这是一次测试" paddlespeech tts --input "你好,这是一次测试"
``` ```
此步骤会自动下载模型缓存至本地 .paddlespeech/models 目录 此步骤会自动下载模型缓存至本地 .paddlespeech/models 目录
### 6.修改tts_online_application.yaml配置 ### 7.修改tts_online_application.yaml配置
参考目录 ```"PaddleSpeech\demos\streaming_tts_server\conf\tts_online_application.yaml"``` 参考目录 ```"PaddleSpeech\demos\streaming_tts_server\conf\tts_online_application.yaml"```
选择```tts_online_application.yaml```文件用编辑器打开,设置```protocol```为```websocket``` 选择```tts_online_application.yaml```文件用编辑器打开,设置```protocol```为```websocket```
### 7.启动服务 ### 8.启动服务
```yaml ```yaml
paddlespeech_server start --config_file ./demos/streaming_tts_server/conf/tts_online_application.yaml paddlespeech_server start --config_file ./demos/streaming_tts_server/conf/tts_online_application.yaml
#官方默认启动命令: #官方默认启动命令:
+6
View File
@@ -199,6 +199,12 @@
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version> <version>${knife4j.version}</version>
</dependency> </dependency>
<!-- BouncyCastle SM2加密 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78</version>
</dependency>
<!-- springdoc --> <!-- springdoc -->
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
@@ -86,6 +86,16 @@ public interface Constant {
*/ */
String SERVER_SECRET = "server.secret"; String SERVER_SECRET = "server.secret";
/**
* SM2公钥
*/
String SM2_PUBLIC_KEY = "server.public_key";
/**
* SM2私钥
*/
String SM2_PRIVATE_KEY = "server.private_key";
/** /**
* websocket地址 * websocket地址
*/ */
@@ -126,11 +136,21 @@ public interface Constant {
*/ */
String SERVER_VOICE_PRINT = "server.voice_print"; String SERVER_VOICE_PRINT = "server.voice_print";
/**
* mqtt密钥
*/
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
/** /**
* 无记忆 * 无记忆
*/ */
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备案号
@@ -156,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;
}
}
/** /**
* 系统短信 * 系统短信
*/ */
@@ -242,7 +294,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.8.1"; public static final String VERSION = "0.8.6";
/** /**
* 无效固件URL * 无效固件URL
@@ -55,4 +55,147 @@ 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 USER_NOT_LOGIN = 10044;
int WEB_SOCKET_CONNECT_FAILED = 10045;
int VOICE_PRINT_SAVE_ERROR = 10046;
int TODAY_SMS_LIMIT_REACHED = 10047;
int OLD_PASSWORD_ERROR = 10048;
int INVALID_LLM_TYPE = 10049;
int TOKEN_GENERATE_ERROR = 10050;
int RESOURCE_NOT_FOUND = 10051;
// 新增错误编码
int DEFAULT_AGENT_NOT_FOUND = 10052;
int AGENT_NOT_FOUND = 10053;
int VOICEPRINT_API_NOT_CONFIGURED = 10054;
int SMS_SEND_FAILED = 10055;
int SMS_CONNECTION_FAILED = 10056;
int AGENT_VOICEPRINT_CREATE_FAILED = 10057;
int AGENT_VOICEPRINT_UPDATE_FAILED = 10058;
int AGENT_VOICEPRINT_DELETE_FAILED = 10059;
int SMS_SEND_TOO_FREQUENTLY = 10060;
int ACTIVATION_CODE_EMPTY = 10061;
int ACTIVATION_CODE_ERROR = 10062;
int DEVICE_ALREADY_ACTIVATED = 10063;
// 默认模型删除错误
int DEFAULT_MODEL_DELETE_ERROR = 10064;
// 登录相关错误码
int ADD_DATA_FAILED = 10065; // 新增数据失败
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
int SMS_CAPTCHA_ERROR = 10067; // 短信验证码错误
int MOBILE_REGISTER_DISABLED = 10068; // 未开启手机注册
int USERNAME_NOT_PHONE = 10069; // 用户名不是手机号码
int PHONE_ALREADY_REGISTERED = 10070; // 手机号码已注册
int PHONE_NOT_REGISTERED = 10071; // 手机号码未注册
int USER_REGISTER_DISABLED = 10072; // 不允许用户注册
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
int SMS_CODE_ERROR = 10075; // 手机验证码错误
// 字典类型相关错误码
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
// 资源处理相关错误码
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
// 智能体相关错误码
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
// 声纹相关错误码
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
int VOICEPRINT_UPDATE_NOT_ALLOWED = 10082; // 声纹修改不允许,声音已注册
int VOICEPRINT_UPDATE_ADMIN_ERROR = 10083; // 修改声纹错误,请联系管理员
int VOICEPRINT_API_URI_ERROR = 10084; // 声纹接口地址错误
int VOICEPRINT_AUDIO_NOT_BELONG_AGENT = 10085; // 音频数据不属于智能体
int VOICEPRINT_AUDIO_EMPTY = 10086; // 音频数据为空
int VOICEPRINT_REGISTER_REQUEST_ERROR = 10087; // 声纹保存请求失败
int VOICEPRINT_REGISTER_PROCESS_ERROR = 10088; // 声纹保存处理失败
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
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 SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
// 参数验证相关错误码
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
int WEBSOCKET_URL_FORMAT_ERROR = 10100; // WebSocket地址格式不正确
int WEBSOCKET_CONNECTION_FAILED = 10101; // WebSocket连接测试失败
int OTA_URL_EMPTY = 10102; // OTA地址不能为空
int OTA_URL_LOCALHOST = 10103; // OTA地址不能使用localhost或127.0.0.1
int OTA_URL_PROTOCOL_ERROR = 10104; // OTA地址必须以http或https开头
int OTA_URL_FORMAT_ERROR = 10105; // OTA地址必须以/ota/结尾
int OTA_INTERFACE_ACCESS_FAILED = 10106; // OTA接口访问失败
int OTA_INTERFACE_FORMAT_ERROR = 10107; // OTA接口返回内容格式不正确
int OTA_INTERFACE_VALIDATION_FAILED = 10108; // OTA接口验证失败
int MCP_URL_EMPTY = 10109; // MCP地址不能为空
int MCP_URL_LOCALHOST = 10110; // MCP地址不能使用localhost或127.0.0.1
int MCP_URL_INVALID = 10111; // 不是正确的MCP地址
int MCP_INTERFACE_ACCESS_FAILED = 10112; // MCP接口访问失败
int MCP_INTERFACE_FORMAT_ERROR = 10113; // MCP接口返回内容格式不正确
int MCP_INTERFACE_VALIDATION_FAILED = 10114; // MCP接口验证失败
int VOICEPRINT_URL_EMPTY = 10115; // 声纹接口地址不能为空
int VOICEPRINT_URL_LOCALHOST = 10116; // 声纹接口地址不能使用localhost或127.0.0.1
int VOICEPRINT_URL_INVALID = 10117; // 不是正确的声纹接口地址
int VOICEPRINT_URL_PROTOCOL_ERROR = 10118; // 声纹接口地址必须以http或https开头
int VOICEPRINT_INTERFACE_ACCESS_FAILED = 10119; // 声纹接口访问失败
int VOICEPRINT_INTERFACE_FORMAT_ERROR = 10120; // 声纹接口返回内容格式不正确
int VOICEPRINT_INTERFACE_VALIDATION_FAILED = 10121; // 声纹接口验证失败
int MQTT_SECRET_EMPTY = 10122; // mqtt密钥不能为空
int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全
int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母
int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复
int SM2_KEY_NOT_CONFIGURED = 10129; // SM2密钥未配置
int SM2_DECRYPT_ERROR = 10130; // SM2解密失败
int MODEL_TYPE_PROVIDE_CODE_NOT_NULL = 10131; // modelType和provideCode不能为空
// 聊天记录相关错误码
int CHAT_HISTORY_NO_PERMISSION = 10132; // 没有权限查看该智能体的聊天记录
int CHAT_HISTORY_SESSION_ID_NOT_NULL = 10133; // 会话ID不能为空
int CHAT_HISTORY_AGENT_ID_NOT_NULL = 10134; // 智能体ID不能为空
int CHAT_HISTORY_DOWNLOAD_FAILED = 10135; // 聊天记录下载失败
int DOWNLOAD_LINK_EXPIRED = 10136; // 下载链接已过期或无效
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
int CHAT_ROLE_USER = 10138; // 用户角色
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格式错误
} }
@@ -13,6 +13,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.utils.MessageUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
/** /**
@@ -62,7 +63,7 @@ public class RenExceptionHandler {
@ExceptionHandler(NoResourceFoundException.class) @ExceptionHandler(NoResourceFoundException.class)
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) { public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage()); log.warn("Resource not found: {}", ex.getMessage());
return new Result<Void>().error(404, "资源不存在"); return new Result<Void>().error(404, MessageUtils.getMessage(ErrorCode.RESOURCE_NOT_FOUND));
} }
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
@@ -76,7 +77,7 @@ public class RenExceptionHandler {
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.findFirst() .findFirst()
.orElse("请求参数错误!"); .orElse(MessageUtils.getMessage(ErrorCode.PARAM_VALUE_NULL));
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg); return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
} }
@@ -139,4 +139,17 @@ public class RedisKeys {
return "sms:Validate:Code:" + phone + ":today_count"; return "sms:Validate:Code:" + phone + ":today_count";
} }
/**
* 聊天记录UUID映射的Key
*/
public static String getChatHistoryKey(String 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());
} }
} }
@@ -6,6 +6,7 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import java.io.BufferedReader; import java.io.BufferedReader;
@@ -37,7 +38,7 @@ public class ResourcesUtils {
} }
} catch (IOException e){ } catch (IOException e){
log.error("方法:loadString()读取资源失败--{}",e.getMessage()); log.error("方法:loadString()读取资源失败--{}",e.getMessage());
throw new RenException("读取资源失败"); throw new RenException(ErrorCode.RESOURCE_READ_ERROR);
} }
return luaScriptBuilder.toString(); return luaScriptBuilder.toString();
} }
@@ -0,0 +1,130 @@
package xiaozhi.common.utils;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* SM2加密工具类(采用十六进制格式,与chancheng-archive-service项目保持一致)
*/
public class SM2Utils {
/**
* 公钥常量
*/
public static final String KEY_PUBLIC_KEY = "publicKey";
/**
* 私钥返回值常量
*/
public static final String KEY_PRIVATE_KEY = "privateKey";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* SM2加密算法
*
* @param publicKey 十六进制公钥
* @param data 明文数据
* @return 十六进制密文
*/
public static String encrypt(String publicKey, String data) {
try {
// 获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
// 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
//提取公钥点
ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
// 设置sm2为加密模式
sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
byte[] in = data.getBytes(StandardCharsets.UTF_8);
byte[] arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
return Hex.toHexString(arrayOfBytes);
} catch (Exception e) {
throw new RuntimeException("SM2加密失败", e);
}
}
/**
* SM2解密算法
*
* @param privateKey 十六进制私钥
* @param cipherData 十六进制密文数据
* @return 明文
*/
public static String decrypt(String privateKey, String cipherData) {
try {
// 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上
if (!cipherData.startsWith("04")) {
cipherData = "04" + cipherData;
}
byte[] cipherDataByte = Hex.decode(cipherData);
BigInteger privateKeyD = new BigInteger(privateKey, 16);
//获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
//构造domain参数
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
// 设置sm2为解密模式
sm2Engine.init(false, privateKeyParameters);
byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
return new String(arrayOfBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM2解密失败", e);
}
}
/**
* 生成密钥对
*/
public static Map<String, String> createKey() {
try {
ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
// 获取一个椭圆曲线类型的密钥对生成器
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
// 使用SM2参数初始化生成器
kpg.initialize(sm2Spec);
// 获取密钥对
KeyPair keyPair = kpg.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
BCECPublicKey p = (BCECPublicKey) publicKey;
PrivateKey privateKey = keyPair.getPrivate();
BCECPrivateKey s = (BCECPrivateKey) privateKey;
Map<String, String> result = new HashMap<>();
result.put(KEY_PUBLIC_KEY, Hex.toHexString(p.getQ().getEncoded(false)));
result.put(KEY_PRIVATE_KEY, Hex.toHexString(s.getD().toByteArray()));
return result;
} catch (Exception e) {
throw new RuntimeException("生成SM2密钥对失败", e);
}
}
}
@@ -0,0 +1,164 @@
package xiaozhi.common.utils;
import cn.hutool.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
/**
* 敏感数据处理工具类
*/
public class SensitiveDataUtils {
// 敏感字段列表
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList(
"api_key", "personal_access_token", "access_token", "token",
"secret", "access_key_secret", "secret_key"));
/**
* 检查字段是否为敏感字段
*/
public static boolean isSensitiveField(String fieldName) {
return StringUtils.isNotBlank(fieldName) && SENSITIVE_FIELDS.contains(fieldName.toLowerCase());
}
/**
* 隐藏字符串中间部分
*/
public static String maskMiddle(String value) {
if (StringUtils.isBlank(value)) {
return value;
}
int length = value.length();
if (length <= 8) {
// 短字符串保留前2后2
return value.substring(0, 2) + "****" + value.substring(length - 2);
} else {
// 长字符串保留前4后4
int maskLength = length - 8;
StringBuilder maskBuilder = new StringBuilder();
for (int i = 0; i < maskLength; i++) {
maskBuilder.append('*');
}
return value.substring(0, 4) + maskBuilder.toString() + value.substring(length - 4);
}
}
/**
* 判断字符串是否是被掩码处理过的值
*/
public static boolean isMaskedValue(String value) {
if (StringUtils.isBlank(value)) {
return false;
}
// 掩码值至少包含4个连续的*
return value.contains("****");
}
/**
* 处理JSONObject中的敏感字段
*/
public static JSONObject maskSensitiveFields(JSONObject jsonObject) {
if (jsonObject == null) {
return null;
}
JSONObject result = new JSONObject();
for (String key : jsonObject.keySet()) {
Object value = jsonObject.get(key);
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
result.put(key, maskMiddle((String) value));
} else if (value instanceof JSONObject) {
result.put(key, maskSensitiveFields((JSONObject) value));
} else {
result.put(key, value);
}
}
return result;
}
/**
* 比较两个JSONObject的敏感字段是否相同
* 特别针对api_key等敏感字段进行单独比较
*/
public static boolean isSensitiveDataEqual(JSONObject original, JSONObject updated) {
if (original == null && updated == null) {
return true;
}
if (original == null || updated == null) {
return false;
}
// 提取并比较特定敏感字段
return compareSpecificSensitiveFields(original, updated, "api_key") &&
compareSpecificSensitiveFields(original, updated, "personal_access_token") &&
compareSpecificSensitiveFields(original, updated, "access_token") &&
compareSpecificSensitiveFields(original, updated, "token") &&
compareSpecificSensitiveFields(original, updated, "secret") &&
compareSpecificSensitiveFields(original, updated, "access_key_secret") &&
compareSpecificSensitiveFields(original, updated, "secret_key");
}
/**
* 比较两个JSON对象中特定敏感字段是否相同
* 遍历整个JSON对象树,查找并比较指定敏感字段
*/
private static boolean compareSpecificSensitiveFields(JSONObject original, JSONObject updated, String fieldName) {
// 提取原始对象中的指定敏感字段
Map<String, String> originalFields = new HashMap<>();
extractSpecificSensitiveField(original, originalFields, fieldName, "");
// 提取更新对象中的指定敏感字段
Map<String, String> updatedFields = new HashMap<>();
extractSpecificSensitiveField(updated, updatedFields, fieldName, "");
// 如果字段数量不同,说明有增删
if (originalFields.size() != updatedFields.size()) {
return false;
}
// 比较每个字段的值
for (Map.Entry<String, String> entry : originalFields.entrySet()) {
String key = entry.getKey();
String originalValue = entry.getValue();
String updatedValue = updatedFields.get(key);
if (updatedValue == null || !updatedValue.equals(originalValue)) {
return false;
}
}
return true;
}
/**
* 递归提取JSON对象中指定名称的敏感字段
*/
private static void extractSpecificSensitiveField(JSONObject jsonObject, Map<String, String> fieldsMap,
String targetFieldName, String parentPath) {
if (jsonObject == null) {
return;
}
for (String key : jsonObject.keySet()) {
String fullPath = parentPath.isEmpty() ? key : parentPath + "." + key;
Object value = jsonObject.get(key);
if (value instanceof JSONObject) {
// 递归处理嵌套JSON对象
extractSpecificSensitiveField((JSONObject) value, fieldsMap, targetFieldName, fullPath);
} else if (value instanceof String && key.equalsIgnoreCase(targetFieldName)) {
// 找到目标敏感字段,保存其路径和值
fieldsMap.put(fullPath, (String) value);
}
}
}
}
@@ -0,0 +1,61 @@
package xiaozhi.common.utils;
import org.apache.commons.lang3.StringUtils;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* SM2解密和验证码验证工具类
* 封装了重复的SM2解密、验证码提取和验证逻辑
*/
public class Sm2DecryptUtil {
/**
* 验证码长度
*/
private static final int CAPTCHA_LENGTH = 5;
/**
* 解密SM2加密内容,提取验证码并验证
* @param encryptedPassword SM2加密的密码字符串
* @param captchaId 验证码ID
* @param captchaService 验证码服务
* @param sysParamsService 系统参数服务
* @return 解密后的实际密码
*/
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
CaptchaService captchaService, SysParamsService sysParamsService) {
// 获取SM2私钥
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
if (StringUtils.isBlank(privateKeyStr)) {
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
}
// 使用SM2私钥解密密码
String decryptedContent;
try {
decryptedContent = SM2Utils.decrypt(privateKeyStr, encryptedPassword);
} catch (Exception e) {
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
}
// 分离验证码和密码:前5位是验证码,后面是密码
if (decryptedContent.length() > CAPTCHA_LENGTH) {
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
// 验证嵌入的验证码是否正确
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
if (!embeddedCaptchaValid) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
return actualPassword;
} else {
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
}
}
}
@@ -1,5 +1,19 @@
package xiaozhi.modules.agent.controller; package xiaozhi.modules.agent.controller;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -7,11 +21,26 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
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.DateUtils;
import xiaozhi.common.utils.MessageUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体聊天历史管理") @Tag(name = "智能体聊天历史管理")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -19,6 +48,9 @@ import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
@RequestMapping("/agent/chat-history") @RequestMapping("/agent/chat-history")
public class AgentChatHistoryController { public class AgentChatHistoryController {
private final AgentChatHistoryBizService agentChatHistoryBizService; private final AgentChatHistoryBizService agentChatHistoryBizService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentService agentService;
private final RedisUtils redisUtils;
/** /**
* 小智服务聊天上报请求 * 小智服务聊天上报请求
@@ -33,4 +65,182 @@ public class AgentChatHistoryController {
Boolean result = agentChatHistoryBizService.report(request); Boolean result = agentChatHistoryBizService.report(request);
return new Result<Boolean>().ok(result); return new Result<Boolean>().ok(result);
} }
/**
* 获取聊天记录下载链接
*
* @param agentId 智能体ID
* @param sessionId 会话ID
* @return UUID作为下载标识
*/
@Operation(summary = "获取聊天记录下载链接")
@RequiresPermissions("sys:role:normal")
@PostMapping("/getDownloadUrl/{agentId}/{sessionId}")
public Result<String> getDownloadUrl(@PathVariable("agentId") String agentId,
@PathVariable("sessionId") String sessionId) {
// 获取当前用户
UserDetail user = SecurityUser.getUser();
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
throw new RenException(ErrorCode.CHAT_HISTORY_NO_PERMISSION);
}
// 生成UUID
String uuid = UUID.randomUUID().toString();
// 存储agentId和sessionId到Redis,格式为agentId:sessionId
redisUtils.set(RedisKeys.getChatHistoryKey(uuid), agentId + ":" + sessionId);
return new Result<String>().ok(uuid);
}
/**
* 下载本会话聊天记录
*
* @param uuid 下载标识
* @param response HTTP响应
*/
@Operation(summary = "下载本会话聊天记录")
@GetMapping("/download/{uuid}/current")
public void downloadCurrentSession(@PathVariable("uuid") String uuid,
HttpServletResponse response) {
// 从Redis获取agentId和sessionId
String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid));
if (StringUtils.isBlank(agentSessionInfo)) {
throw new RenException(ErrorCode.DOWNLOAD_LINK_EXPIRED);
}
try {
// 解析agentId和sessionId
String[] parts = agentSessionInfo.split(":");
if (parts.length != 2) {
throw new RenException(ErrorCode.DOWNLOAD_LINK_INVALID);
}
String agentId = parts[0];
String sessionId = parts[1];
// 执行下载
downloadChatHistory(agentId, List.of(sessionId), response);
} finally {
// 下载完成后删除UUID,防止盗刷
redisUtils.delete(RedisKeys.getChatHistoryKey(uuid));
}
}
/**
* 下载本会话及前20条会话聊天记录
*
* @param uuid 下载标识
* @param response HTTP响应
*/
@Operation(summary = "下载本会话及前20条会话聊天记录")
@GetMapping("/download/{uuid}/previous")
public void downloadCurrentSessionWithPrevious(@PathVariable("uuid") String uuid,
HttpServletResponse response) {
// 从Redis获取agentId和sessionId
String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid));
if (StringUtils.isBlank(agentSessionInfo)) {
throw new RenException("下载链接已过期或无效");
}
try {
// 解析agentId和sessionId
String[] parts = agentSessionInfo.split(":");
if (parts.length != 2) {
throw new RenException("下载链接无效");
}
String agentId = parts[0];
String sessionId = parts[1];
// 获取所有会话列表
Map<String, Object> params = Map.of(
"agentId", agentId,
Constant.PAGE, 1,
Constant.LIMIT, 1000 // 获取足够多的会话
);
PageData<AgentChatSessionDTO> sessionPage = agentChatHistoryService.getSessionListByAgentId(params);
List<AgentChatSessionDTO> allSessions = sessionPage.getList();
// 查找当前会话在列表中的位置
int currentIndex = -1;
for (int i = 0; i < allSessions.size(); i++) {
if (allSessions.get(i).getSessionId().equals(sessionId)) {
currentIndex = i;
break;
}
}
// 如果找到了当前会话,收集当前会话及前20条会话ID
List<String> sessionIdsToDownload = new ArrayList<>();
if (currentIndex != -1) {
// 从当前会话开始,向后(数组后面)取最多20条会话(包括当前会话)
int endIndex = Math.min(allSessions.size() - 1, currentIndex + 20); // 确保不越界
for (int i = currentIndex; i <= endIndex; i++) {
sessionIdsToDownload.add(allSessions.get(i).getSessionId());
}
}
// 如果没有找到当前会话,至少下载当前会话
if (sessionIdsToDownload.isEmpty()) {
sessionIdsToDownload.add(sessionId);
}
downloadChatHistory(agentId, sessionIdsToDownload, response);
} finally {
// 下载完成后删除UUID,防止盗刷
redisUtils.delete(RedisKeys.getChatHistoryKey(uuid));
}
}
/**
* 下载指定会话的聊天记录
*
* @param agentId 智能体ID
* @param sessionIds 会话ID列表
* @param response HTTP响应
*/
private void downloadChatHistory(String agentId, List<String> sessionIds, HttpServletResponse response) {
try {
// 设置响应头
response.setContentType("text/plain;charset=UTF-8");
String fileName = URLEncoder.encode("history.txt", StandardCharsets.UTF_8.toString());
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 获取聊天记录并写入响应流
try (OutputStream out = response.getOutputStream()) {
// 为每个会话生成聊天记录
for (String sessionId : sessionIds) {
// 获取该会话的所有聊天记录
List<AgentChatHistoryDTO> chatHistoryList = agentChatHistoryService
.getChatHistoryBySessionId(agentId, sessionId);
// 从聊天记录中获取第一条消息的创建时间作为会话时间
if (!chatHistoryList.isEmpty()) {
Date firstMessageTime = chatHistoryList.get(0).getCreatedAt();
String sessionTimeStr = DateUtils.format(firstMessageTime, DateUtils.DATE_TIME_PATTERN);
out.write((sessionTimeStr + "\n").getBytes(StandardCharsets.UTF_8));
}
for (AgentChatHistoryDTO message : chatHistoryList) {
String role = message.getChatType() == 1 ? MessageUtils.getMessage(ErrorCode.CHAT_ROLE_USER)
: MessageUtils.getMessage(ErrorCode.CHAT_ROLE_AGENT);
String direction = message.getChatType() == 1 ? ">>" : "<<";
Date messageTime = message.getCreatedAt();
String messageTimeStr = DateUtils.format(messageTime, DateUtils.DATE_TIME_PATTERN);
String content = message.getContent();
String line = "[" + role + "]-[" + messageTimeStr + "]" + direction + ":" + content + "\n";
out.write(line.getBytes(StandardCharsets.UTF_8));
}
// 会话之间添加空行分隔
if (sessionIds.indexOf(sessionId) < sessionIds.size() - 1) {
out.write("\n".getBytes(StandardCharsets.UTF_8));
}
}
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
} }
@@ -0,0 +1,158 @@
package xiaozhi.modules.agent.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.PutMapping;
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 com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentTemplateVO;
@Tag(name = "智能体模板管理")
@AllArgsConstructor
@RestController
@RequestMapping("/agent/template")
public class AgentTemplateController {
private final AgentTemplateService agentTemplateService;
@GetMapping("/page")
@Operation(summary = "获取模板分页列表")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
@Parameter(name = "agentName", description = "模板名称,模糊查询")
})
public Result<PageData<AgentTemplateVO>> getAgentTemplatesPage(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
// 创建分页对象
int page = Integer.parseInt(params.getOrDefault(Constant.PAGE, "1").toString());
int limit = Integer.parseInt(params.getOrDefault(Constant.LIMIT, "10").toString());
Page<AgentTemplateEntity> pageInfo = new Page<>(page, limit);
// 创建查询条件
QueryWrapper<AgentTemplateEntity> wrapper = new QueryWrapper<>();
String agentName = (String) params.get("agentName");
if (agentName != null && !agentName.isEmpty()) {
wrapper.like("agent_name", agentName);
}
wrapper.orderByAsc("sort");
// 执行分页查询
IPage<AgentTemplateEntity> pageResult = agentTemplateService.page(pageInfo, wrapper);
// 使用ConvertUtils转换为VO列表
List<AgentTemplateVO> voList = ConvertUtils.sourceToTarget(pageResult.getRecords(), AgentTemplateVO.class);
// 修复:使用构造函数创建PageData对象,而不是无参构造+setter
PageData<AgentTemplateVO> pageData = new PageData<>(voList, pageResult.getTotal());
return new Result<PageData<AgentTemplateVO>>().ok(pageData);
}
@GetMapping("/{id}")
@Operation(summary = "获取模板详情")
@RequiresPermissions("sys:role:superAdmin")
public Result<AgentTemplateVO> getAgentTemplateById(@PathVariable("id") String id) {
AgentTemplateEntity template = agentTemplateService.getById(id);
if (template == null) {
return ResultUtils.error("模板不存在");
}
// 使用ConvertUtils转换为VO
AgentTemplateVO vo = ConvertUtils.sourceToTarget(template, AgentTemplateVO.class);
return ResultUtils.success(vo);
}
@PostMapping
@Operation(summary = "创建模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<AgentTemplateEntity> createAgentTemplate(@Valid @RequestBody AgentTemplateEntity template) {
// 设置排序值为下一个可用的序号
template.setSort(agentTemplateService.getNextAvailableSort());
boolean saved = agentTemplateService.save(template);
if (saved) {
return ResultUtils.success(template);
} else {
return ResultUtils.error("创建模板失败");
}
}
@PutMapping
@Operation(summary = "更新模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<AgentTemplateEntity> updateAgentTemplate(@Valid @RequestBody AgentTemplateEntity template) {
boolean updated = agentTemplateService.updateById(template);
if (updated) {
return ResultUtils.success(template);
} else {
return ResultUtils.error("更新模板失败");
}
}
@DeleteMapping("/{id}")
@Operation(summary = "删除模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> deleteAgentTemplate(@PathVariable("id") String id) {
// 先查询要删除的模板信息,获取其排序值
AgentTemplateEntity template = agentTemplateService.getById(id);
if (template == null) {
return ResultUtils.error("模板不存在");
}
Integer deletedSort = template.getSort();
// 执行删除操作
boolean deleted = agentTemplateService.removeById(id);
if (deleted) {
// 删除成功后,重新排序剩余模板
agentTemplateService.reorderTemplatesAfterDelete(deletedSort);
return ResultUtils.success("删除模板成功");
} else {
return ResultUtils.error("删除模板失败");
}
}
// 添加新的批量删除方法,使用不同的URL
@PostMapping("/batch-remove")
@Operation(summary = "批量删除模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> batchRemoveAgentTemplates(@RequestBody List<String> ids) {
boolean deleted = agentTemplateService.removeByIds(ids);
if (deleted) {
return ResultUtils.success("批量删除成功");
} else {
return ResultUtils.error("批量删除模板失败");
}
}
}
@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO; import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
@@ -42,7 +43,7 @@ public class AgentVoicePrintController {
if (b) { if (b) {
return new Result<>(); return new Result<>();
} }
return new Result<Void>().error("智能体的声纹创建失败"); return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_CREATE_FAILED);
} }
@PutMapping @PutMapping
@@ -54,7 +55,7 @@ public class AgentVoicePrintController {
if (b) { if (b) {
return new Result<>(); return new Result<>();
} }
return new Result<Void>().error("智能体的对应声纹更新失败"); return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_UPDATE_FAILED);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@@ -67,7 +68,7 @@ public class AgentVoicePrintController {
if (delete) { if (delete) {
return new Result<>(); return new Result<>();
} }
return new Result<Void>().error("智能体的对应声纹删除失败"); return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_DELETE_FAILED);
} }
@GetMapping("/list/{id}") @GetMapping("/list/{id}")
@@ -76,7 +77,7 @@ public class AgentVoicePrintController {
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) { public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true); String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) { if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)"); throw new RenException(ErrorCode.VOICEPRINT_API_NOT_CONFIGURED);
} }
Long userId = SecurityUser.getUserId(); Long userId = SecurityUser.getUserId();
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id); List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
@@ -25,4 +25,18 @@ public interface AgentTemplateService extends IService<AgentTemplateEntity> {
* @param modelId 模型ID * @param modelId 模型ID
*/ */
void updateDefaultTemplateModelId(String modelType, String modelId); void updateDefaultTemplateModelId(String modelType, String modelId);
/**
* 删除模板后重新排序剩余模板
*
* @param deletedSort 被删除模板的排序值
*/
void reorderTemplatesAfterDelete(Integer deletedSort);
/**
* 获取下一个可用的排序序号(寻找最小的未使用序号)
*
* @return 下一个可用的排序序号
*/
Integer getNextAvailableSort();
} }
@@ -19,6 +19,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
@@ -41,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;
@@ -74,7 +76,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
AgentInfoVO agent = agentDao.selectAgentInfoById(id); AgentInfoVO agent = agentDao.selectAgentInfoById(id);
if (agent == null) { if (agent == null) {
throw new RenException("智能体不存在"); throw new RenException(ErrorCode.AGENT_NOT_FOUND);
} }
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
@@ -182,6 +184,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
@Override @Override
public boolean checkAgentPermission(String agentId, Long userId) { public boolean checkAgentPermission(String agentId, Long userId) {
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
return false;
}
// 获取智能体信息 // 获取智能体信息
AgentEntity agent = getAgentById(agentId); AgentEntity agent = getAgentById(agentId);
if (agent == null) { if (agent == null) {
@@ -204,7 +209,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 先查询现有实体 // 先查询现有实体
AgentEntity existingEntity = this.getAgentById(agentId); AgentEntity existingEntity = this.getAgentById(agentId);
if (existingEntity == null) { if (existingEntity == null) {
throw new RuntimeException("智能体不存在"); throw new RenException(ErrorCode.AGENT_NOT_FOUND);
} }
// 只更新提供的非空字段 // 只更新提供的非空字段
@@ -328,7 +333,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
if (!b) { if (!b) {
throw new RenException("LLM大模型和Intent意图识别,选择参数不匹配"); throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
} }
this.updateById(existingEntity); this.updateById(existingEntity);
} }
@@ -369,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());
@@ -10,6 +10,11 @@ import xiaozhi.modules.agent.dao.AgentTemplateDao;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.service.AgentTemplateService;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
/** /**
* @author chenerlei * @author chenerlei
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service实现 * @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service实现
@@ -69,4 +74,48 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
wrapper.ge("sort", 0); wrapper.ge("sort", 0);
update(wrapper); update(wrapper);
} }
@Override
public void reorderTemplatesAfterDelete(Integer deletedSort) {
if (deletedSort == null) {
return;
}
// 查询所有排序值大于被删除模板的记录
UpdateWrapper<AgentTemplateEntity> updateWrapper = new UpdateWrapper<>();
updateWrapper.gt("sort", deletedSort)
.setSql("sort = sort - 1");
// 执行批量更新,将这些记录的排序值减1
this.update(updateWrapper);
}
@Override
public Integer getNextAvailableSort() {
// 查询所有已存在的排序值并按升序排序
List<Integer> sortValues = baseMapper.selectList(new QueryWrapper<AgentTemplateEntity>())
.stream()
.map(AgentTemplateEntity::getSort)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
// 如果没有排序值,返回1
if (sortValues.isEmpty()) {
return 1;
}
// 寻找最小的未使用序号
int expectedSort = 1;
for (Integer sort : sortValues) {
if (sort > expectedSort) {
// 找到空缺的序号
return expectedSort;
}
expectedSort = sort + 1;
}
// 如果没有空缺,返回最大序号+1
return expectedSort;
}
} }
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
@@ -78,7 +79,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 根据识别出的声纹ID查询对应的用户信息 // 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId()); AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户"; String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册"); throw new RenException(ErrorCode.VOICEPRINT_ALREADY_REGISTERED, existingUserName);
} }
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class); AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
// 开启事务 // 开启事务
@@ -100,7 +101,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} catch (Exception e) { } catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
log.error("保存声纹错误原因:{}", e.getMessage()); log.error("保存声纹错误原因:{}", e.getMessage());
throw new RenException("保存声纹错误,请联系管理员"); throw new RenException(ErrorCode.VOICE_PRINT_SAVE_ERROR);
} }
})); }));
} }
@@ -123,7 +124,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} catch (Exception e) { } catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
log.error("删除声纹存在错误原因:{}", e.getMessage()); log.error("删除声纹存在错误原因:{}", e.getMessage());
throw new RenException("删除声纹出现了错误"); throw new RenException(ErrorCode.VOICEPRINT_DELETE_ERROR);
} }
})); }));
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据 // 数据库声纹数据删除成功才继续执行删除声纹服务的数据
@@ -179,7 +180,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 根据识别出的声纹ID查询对应的用户信息 // 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId()); AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户"; String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + ""); throw new RenException(ErrorCode.VOICEPRINT_UPDATE_NOT_ALLOWED, existingUserName);
} }
} }
} else { } else {
@@ -208,7 +209,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} catch (Exception e) { } catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
log.error("修改声纹错误原因:{}", e.getMessage()); log.error("修改声纹错误原因:{}", e.getMessage());
throw new RenException("修改声纹错误,请联系管理员"); throw new RenException(ErrorCode.VOICEPRINT_UPDATE_ADMIN_ERROR);
} }
})); }));
} }
@@ -225,7 +226,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
return new URI(voicePrint); return new URI(voicePrint);
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
log.error("路径格式不正确路径:{}\n错误信息:{}", voicePrint, e.getMessage()); log.error("路径格式不正确路径:{}\n错误信息:{}", voicePrint, e.getMessage());
throw new RuntimeException("声纹接口的地址存在错误,请进入参数管理修改声纹接口地址"); throw new RenException(ErrorCode.VOICEPRINT_API_URI_ERROR);
} }
} }
@@ -270,13 +271,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 判断这个音频是否属于当前智能体 // 判断这个音频是否属于当前智能体
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId); boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
if (!b) { if (!b) {
throw new RenException("音频数据不属于这个智能体"); throw new RenException(ErrorCode.VOICEPRINT_AUDIO_NOT_BELONG_AGENT);
} }
// 获取到音频数据 // 获取到音频数据
byte[] audio = agentChatAudioService.getAudio(audioId); byte[] audio = agentChatAudioService.getAudio(audioId);
// 如果音频数据为空的直接报错不进行下去 // 如果音频数据为空的直接报错不进行下去
if (audio == null || audio.length == 0) { if (audio == null || audio.length == 0) {
throw new RenException("音频数据是空的请检查上传数据"); throw new RenException(ErrorCode.VOICEPRINT_AUDIO_EMPTY);
} }
// 将字节数组包装为资源,返回 // 将字节数组包装为资源,返回
return new ByteArrayResource(audio) { return new ByteArrayResource(audio) {
@@ -314,13 +315,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注册失败,请求路径:{}", requestUrl); log.error("声纹注册失败,请求路径:{}", requestUrl);
throw new RenException("声纹保存失败,请求不成功"); throw new RenException(ErrorCode.VOICEPRINT_REGISTER_REQUEST_ERROR);
} }
// 检查响应内容 // 检查响应内容
String responseBody = response.getBody(); String responseBody = response.getBody();
if (responseBody == null || !responseBody.contains("true")) { if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody); log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹保存失败,请求处理失败"); throw new RenException(ErrorCode.VOICEPRINT_REGISTER_PROCESS_ERROR);
} }
} }
@@ -344,13 +345,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
String.class); String.class);
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注销失败,请求路径:{}", requestUrl); log.error("声纹注销失败,请求路径:{}", requestUrl);
throw new RenException("声纹注销失败,请求不成功"); throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_REQUEST_ERROR);
} }
// 检查响应内容 // 检查响应内容
String responseBody = response.getBody(); String responseBody = response.getBody();
if (responseBody == null || !responseBody.contains("true")) { if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody); log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹注销失败,请求处理失败"); throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_PROCESS_ERROR);
} }
} }
@@ -398,7 +399,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹识别请求失败,请求路径:{}", requestUrl); log.error("声纹识别请求失败,请求路径:{}", requestUrl);
throw new RenException("声纹识别失败,请求不成功"); throw new RenException(ErrorCode.VOICEPRINT_IDENTIFY_REQUEST_ERROR);
} }
// 检查响应内容 // 检查响应内容
String responseBody = response.getBody(); String responseBody = response.getBody();
@@ -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)) {
@@ -376,7 +384,8 @@ public class ConfigServiceImpl implements ConfigService {
if (modelIds[i] == null) { if (modelIds[i] == null) {
continue; continue;
} }
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache); // 关键:第三个参数传false,确保获取原始密钥
ModelConfigEntity model = modelConfigService.getModelByIdFromCache(modelIds[i]);
if (model == null) { if (model == null) {
continue; continue;
} }
@@ -391,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])) {
@@ -424,14 +442,16 @@ public class ConfigServiceImpl implements ConfigService {
if ("LLM".equals(modelTypes[i])) { if ("LLM".equals(modelTypes[i])) {
if (StringUtils.isNotBlank(intentLLMModelId)) { if (StringUtils.isNotBlank(intentLLMModelId)) {
if (!typeConfig.containsKey(intentLLMModelId)) { if (!typeConfig.containsKey(intentLLMModelId)) {
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache); // 修改这里:添加isMaskSensitive=false参数
ModelConfigEntity intentLLM = modelConfigService.getModelByIdFromCache(intentLLMModelId);
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson()); typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
} }
} }
if (StringUtils.isNotBlank(memLocalShortLLMModelId)) { if (StringUtils.isNotBlank(memLocalShortLLMModelId)) {
if (!typeConfig.containsKey(memLocalShortLLMModelId)) { if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
// 修改这里:添加isMaskSensitive=false参数
ModelConfigEntity memLocalShortLLM = modelConfigService ModelConfigEntity memLocalShortLLM = modelConfigService
.getModelById(memLocalShortLLMModelId, isCache); .getModelByIdFromCache(memLocalShortLLMModelId);
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson()); typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
} }
} }
@@ -5,6 +5,10 @@ import java.util.List;
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.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
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;
@@ -12,32 +16,45 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DeviceRegisterDTO; import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO; import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO; import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "设备管理") @Tag(name = "设备管理")
@AllArgsConstructor
@RestController @RestController
@RequestMapping("/device") @RequestMapping("/device")
public class DeviceController { public class DeviceController {
private final DeviceService deviceService; private final DeviceService deviceService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final SysParamsService sysParamsService;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
RestTemplate restTemplate, ObjectMapper objectMapper) {
this.deviceService = deviceService;
this.redisUtils = redisUtils;
this.sysParamsService = sysParamsService;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
@PostMapping("/bind/{agentId}/{deviceCode}") @PostMapping("/bind/{agentId}/{deviceCode}")
@Operation(summary = "绑定设备") @Operation(summary = "绑定设备")
@@ -75,6 +92,88 @@ public class DeviceController {
return new Result<List<DeviceEntity>>().ok(devices); return new Result<List<DeviceEntity>>().ok(devices);
} }
@PostMapping("/bind/{agentId}")
@Operation(summary = "设备在线接口")
@RequiresPermissions("sys:role:normal")
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
try {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return new Result<>();
}
// 获取当前用户的设备列表
UserDetail user = SecurityUser.getUser();
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
// 构建deviceIds数组
java.util.List<String> deviceIds = new java.util.ArrayList<>();
for (DeviceEntity device : devices) {
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
// 替换冒号为下划线
groupId = groupId.replace(":", "_");
macAddress = macAddress.replace(":", "_");
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
deviceIds.add(mqttClientId);
}
// 构建完整的URL
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 生成Bearer令牌
String token = generateBearerToken();
if (token == null) {
return new Result<String>().error("令牌生成失败");
}
headers.set("Authorization", "Bearer " + token);
// 构建请求体JSON
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, 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());
}
}
private String generateBearerToken() {
try {
// 获取当前日期,格式为yyyy-MM-dd
String dateStr = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 获取MQTT签名密钥
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
return null;
}
// 将日期字符串与MQTT_SIGNATURE_KEY连接
String tokenContent = dateStr + signatureKey;
// 对连接后的字符串进行SHA256哈希计算
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
return token;
} catch (Exception e) {
return null;
}
}
@PostMapping("/unbind") @PostMapping("/unbind")
@Operation(summary = "解绑设备") @Operation(summary = "解绑设备")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
@@ -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
@@ -29,6 +29,7 @@ import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
@@ -83,27 +84,27 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override @Override
public Boolean deviceActivation(String agentId, String activationCode) { public Boolean deviceActivation(String agentId, String activationCode) {
if (StringUtils.isBlank(activationCode)) { if (StringUtils.isBlank(activationCode)) {
throw new RenException("激活码不能为空"); throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
} }
String deviceKey = "ota:activation:code:" + activationCode; String deviceKey = "ota:activation:code:" + activationCode;
Object cacheDeviceId = redisUtils.get(deviceKey); Object cacheDeviceId = redisUtils.get(deviceKey);
if (cacheDeviceId == null) { if (cacheDeviceId == null) {
throw new RenException("激活码错误"); throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
} }
String deviceId = (String) cacheDeviceId; String deviceId = (String) cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId); String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey); Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
if (cacheMap == null) { if (cacheMap == null) {
throw new RenException("激活码错误"); throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
} }
String cachedCode = (String) cacheMap.get("activation_code"); String cachedCode = (String) cacheMap.get("activation_code");
if (!activationCode.equals(cachedCode)) { if (!activationCode.equals(cachedCode)) {
throw new RenException("激活码错误"); throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
} }
// 检查设备有没有被激活 // 检查设备有没有被激活
if (selectById(deviceId) != null) { if (selectById(deviceId) != null) {
throw new RenException("设备已激活"); throw new RenException(ErrorCode.DEVICE_ALREADY_ACTIVATED);
} }
String macAddress = (String) cacheMap.get("mac_address"); String macAddress = (String) cacheMap.get("mac_address");
@@ -111,7 +112,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
String appVersion = (String) cacheMap.get("app_version"); String appVersion = (String) cacheMap.get("app_version");
UserDetail user = SecurityUser.getUser(); UserDetail user = SecurityUser.getUser();
if (user.getId() == null) { if (user.getId() == null) {
throw new RenException("用户未登录"); throw new RenException(ErrorCode.USER_NOT_LOGIN);
} }
Date currentTime = new Date(); Date currentTime = new Date();
@@ -133,6 +134,10 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 清理redis缓存 // 清理redis缓存
redisUtils.delete(cacheDeviceKey); redisUtils.delete(cacheDeviceKey);
redisUtils.delete(deviceKey); redisUtils.delete(deviceKey);
// 添加:清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId));
return true; return true;
} }
@@ -164,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/";
@@ -188,7 +194,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
try { try {
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard() String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
: "GID_default"; : "GID_default";
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, clientId, groupId); DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, groupId);
if (mqtt != null) { if (mqtt != null) {
mqtt.setEndpoint(mqttUdpConfig); mqtt.setEndpoint(mqttUdpConfig);
response.setMqtt(mqtt); response.setMqtt(mqtt);
@@ -224,6 +230,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override @Override
public void unbindDevice(Long userId, String deviceId) { public void unbindDevice(Long userId, String deviceId) {
// 先查询设备信息,获取agentId
DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) {
return;
}
if (StringUtils.isNotBlank(device.getAgentId())) {
// 清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
}
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>(); UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("user_id", userId); wrapper.eq("user_id", userId);
wrapper.eq("id", deviceId); wrapper.eq("id", deviceId);
@@ -441,7 +457,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
wrapper.eq("mac_address", dto.getMacAddress()); wrapper.eq("mac_address", dto.getMacAddress());
DeviceEntity exist = baseDao.selectOne(wrapper); DeviceEntity exist = baseDao.selectOne(wrapper);
if (exist != null) { if (exist != null) {
throw new RenException("该Mac地址已存在"); throw new RenException(ErrorCode.MAC_ADDRESS_ALREADY_EXISTS);
} }
Date now = new Date(); Date now = new Date();
DeviceEntity entity = new DeviceEntity(); DeviceEntity entity = new DeviceEntity();
@@ -458,6 +474,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
entity.setUpdater(userId); entity.setUpdater(userId);
entity.setAutoUpdate(1); entity.setAutoUpdate(1);
baseDao.insert(entity); baseDao.insert(entity);
// 添加:清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId()));
} }
/** /**
@@ -479,11 +498,10 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
* 构建MQTT配置信息 * 构建MQTT配置信息
* *
* @param macAddress MAC地址 * @param macAddress MAC地址
* @param clientId 客户端ID (UUID)
* @param groupId 分组ID * @param groupId 分组ID
* @return MQTT配置对象 * @return MQTT配置对象
*/ */
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String clientId, String groupId) private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
throws Exception { throws Exception {
// 从环境变量或系统参数获取签名密钥 // 从环境变量或系统参数获取签名密钥
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false); String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
@@ -495,8 +513,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 构建客户端ID格式:groupId@@@macAddress@@@uuid // 构建客户端ID格式:groupId@@@macAddress@@@uuid
String groupIdSafeStr = groupId.replace(":", "_"); String groupIdSafeStr = groupId.replace(":", "_");
String deviceIdSafeStr = macAddress.replace(":", "_"); String deviceIdSafeStr = macAddress.replace(":", "_");
String clientIdSafeStr = clientId.replace(":", "_"); String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, deviceIdSafeStr);
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, clientIdSafeStr);
// 构建用户数据(包含IP等信息) // 构建用户数据(包含IP等信息)
Map<String, String> userData = new HashMap<>(); Map<String, String> userData = new HashMap<>();
@@ -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,17 +36,23 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
/** /**
* 根据ID获取模型配置 * 根据ID获取模型配置
* *
* @param id 模型ID * @param id 模型ID
* @param isCache 是否缓存
* @return 模型配置实体 * @return 模型配置实体
*/ */
ModelConfigEntity getModelById(String id, boolean isCache); ModelConfigEntity getModelByIdFromCache(String id);
/** /**
* 设置默认模型 * 设置默认模型
* *
* @param modelType 模型类型 * @param modelType 模型类型
* @param isDefault 是否默认 * @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();
} }
@@ -1,5 +1,6 @@
package xiaozhi.modules.model.service.impl; package xiaozhi.modules.model.service.impl;
import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -11,16 +12,21 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONObject;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
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.SensitiveDataUtils;
import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.model.dao.ModelConfigDao; import xiaozhi.modules.model.dao.ModelConfigDao;
@@ -62,7 +68,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
.eq("is_enabled", 1) .eq("is_enabled", 1)
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%") .like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
.select("id", "model_name", "config_json")); .select("id", "model_name", "config_json"));
// 处理获取到的内容
return entities.stream().map(item -> { return entities.stream().map(item -> {
LlmModelBasicInfoDTO dto = new LlmModelBasicInfoDTO(); LlmModelBasicInfoDTO dto = new LlmModelBasicInfoDTO();
dto.setId(item.getId()); dto.setId(item.getId());
@@ -75,90 +81,371 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
@Override @Override
public PageData<ModelConfigDTO> getPageList(String modelType, String modelName, String page, String limit) { public PageData<ModelConfigDTO> getPageList(String modelType, String modelName, String page, String limit) {
Map<String, Object> params = new HashMap<String, Object>(); Map<String, Object> params = new HashMap<>();
params.put(Constant.PAGE, page); params.put(Constant.PAGE, page);
params.put(Constant.LIMIT, limit); params.put(Constant.LIMIT, limit);
long curPage = Long.parseLong(page);
long pageSize = Long.parseLong(limit);
Page<ModelConfigEntity> pageInfo = new Page<>(curPage, pageSize);
// 添加排序规则:先按is_enabled降序,再按sort升序
pageInfo.addOrder(OrderItem.desc("is_enabled"));
pageInfo.addOrder(OrderItem.asc("sort"));
IPage<ModelConfigEntity> modelConfigEntityIPage = modelConfigDao.selectPage( IPage<ModelConfigEntity> modelConfigEntityIPage = modelConfigDao.selectPage(
getPage(params, "sort", true), pageInfo,
new QueryWrapper<ModelConfigEntity>() new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType) .eq("model_type", modelType)
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")); .like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%"));
return getPageData(modelConfigEntityIPage, ModelConfigDTO.class); return getPageData(modelConfigEntityIPage, ModelConfigDTO.class);
} }
@Override @Override
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) { public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
// 先验证有没有供应器 // 1. 参数验证
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) { validateEditParameters(modelType, provideCode, id, modelConfigBodyDTO);
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
// 再保存供应器提供的模型 // 2. 验证模型提供者
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class); validateModelProvider(modelType, provideCode);
modelConfigEntity.setModelType(modelType);
modelConfigEntity.setIsDefault(0); // 3. 获取原始配置(不经过敏感数据处理)
modelConfigDao.insert(modelConfigEntity); ModelConfigEntity originalEntity = getOriginalConfigFromDb(id);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
// 4. 验证LLM配置
validateLlmConfiguration(modelConfigBodyDTO);
// 5. 准备更新实体并处理敏感数据
ModelConfigEntity modelConfigEntity = prepareUpdateEntity(modelConfigBodyDTO, originalEntity, modelType, id);
// 6. 执行数据库更新
modelConfigDao.updateById(modelConfigEntity);
// 7. 清除缓存
clearModelCache(id);
// 8. 返回处理后的数据(包含敏感数据掩码)
return buildResponseDTO(modelConfigEntity);
} }
@Override @Override
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) { public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
// 先验证有没有供应器 validateAddParameters(modelType, provideCode, modelConfigBodyDTO);
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
if (modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
ModelConfigEntity modelConfigEntity = modelConfigDao.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
.eq(ModelConfigEntity::getId, llm));
String selectModelType = (modelConfigEntity == null || modelConfigEntity.getModelType() == null) ? null
: modelConfigEntity.getModelType().toUpperCase();
if (modelConfigEntity == null || !"LLM".equals(selectModelType)) {
throw new RenException("设置的LLM不存在");
}
String type = modelConfigEntity.getConfigJson().get("type").toString();
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
if (!"openai".equals(type) && !"ollama".equals(type)) {
throw new RenException("设置的LLM不是openai和ollama");
}
}
// 再更新供应器提供的模型 validateModelProvider(modelType, provideCode);
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setId(id); ModelConfigEntity modelConfigEntity = prepareAddEntity(modelConfigBodyDTO, modelType);
modelConfigEntity.setModelType(modelType);
modelConfigDao.updateById(modelConfigEntity); modelConfigDao.insert(modelConfigEntity);
// 清除缓存
redisUtils.delete(RedisKeys.getModelConfigById(modelConfigEntity.getId())); return buildResponseDTO(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
} }
@Override @Override
public void delete(String id) { public void delete(String id) {
// 查看是否是默认 if (StringUtils.isBlank(id)) {
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
}
ModelConfigEntity modelConfig = modelConfigDao.selectById(id); ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
if (modelConfig != null && modelConfig.getIsDefault() == 1) { if (modelConfig != null && modelConfig.getIsDefault() == 1) {
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型"); throw new RenException(ErrorCode.DEFAULT_MODEL_DELETE_ERROR);
} }
// 验证是否有引用
checkAgentReference(id); checkAgentReference(id);
checkIntentConfigReference(id); checkIntentConfigReference(id);
modelConfigDao.deleteById(id); modelConfigDao.deleteById(id);
clearModelCache(id);
}
@Override
public String getModelNameById(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
String cacheKey = RedisKeys.getModelNameById(id);
String cachedName = (String) redisUtils.get(cacheKey);
if (StringUtils.isNotBlank(cachedName)) {
return cachedName;
}
ModelConfigEntity entity = modelConfigDao.selectById(id);
if (entity != null) {
String modelName = entity.getModelName();
if (StringUtils.isNotBlank(modelName)) {
redisUtils.set(cacheKey, modelName);
}
return modelName;
}
return null;
}
@Override
public ModelConfigEntity selectById(Serializable id) {
ModelConfigEntity entity = super.selectById(id);
if (entity != null && entity.getConfigJson() != null) {
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
}
return entity;
}
@Override
protected <D> PageData<D> getPageData(IPage<?> page, Class<D> target) {
List<?> records = page.getRecords();
if (records != null && !records.isEmpty()) {
for (Object record : records) {
if (record instanceof ModelConfigEntity) {
ModelConfigEntity entity = (ModelConfigEntity) record;
if (entity.getConfigJson() != null) {
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
}
}
}
}
return super.getPageData(page, target);
}
@Override
public ModelConfigEntity getModelByIdFromCache(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
String cacheKey = RedisKeys.getModelConfigById(id);
ModelConfigEntity entity = (ModelConfigEntity) redisUtils.get(cacheKey);
if (entity == null) {
entity = modelConfigDao.selectById(id);
if (entity != null) {
redisUtils.set(cacheKey, entity);
}
}
return entity;
}
/**
* 验证编辑参数
*/
private void validateEditParameters(String modelType, String provideCode, String id,
ModelConfigBodyDTO modelConfigBodyDTO) {
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
}
if (StringUtils.isBlank(id)) {
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
}
if (modelConfigBodyDTO == null) {
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
}
}
/**
* 验证添加参数
*/
private void validateAddParameters(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
}
if (modelConfigBodyDTO == null) {
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
}
}
/**
* 设置默认模型
*/
@Override
public void setDefaultModel(String modelType, int isDefault) {
// 参数验证
if (StringUtils.isBlank(modelType)) {
throw new RenException(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
}
ModelConfigEntity entity = new ModelConfigEntity();
entity.setIsDefault(isDefault);
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType));
// 清除相关缓存
clearModelCacheByType(modelType);
}
/**
* 验证模型提供者
*/
private void validateModelProvider(String modelType, String provideCode) {
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
}
}
/**
* 从数据库获取原始配置(不经过敏感数据处理)
*/
private ModelConfigEntity getOriginalConfigFromDb(String id) {
ModelConfigEntity originalEntity = modelConfigDao.selectById(id);
if (originalEntity == null) {
throw new RenException(ErrorCode.RESOURCE_NOT_FOUND);
}
return originalEntity;
}
/**
* 验证LLM配置
*/
private void validateLlmConfiguration(ModelConfigBodyDTO modelConfigBodyDTO) {
if (modelConfigBodyDTO.getConfigJson() != null && modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
ModelConfigEntity modelConfigEntity = modelConfigDao.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
.eq(ModelConfigEntity::getId, llm));
if (modelConfigEntity == null) {
throw new RenException(ErrorCode.LLM_NOT_EXIST);
}
String modelType = modelConfigEntity.getModelType();
if (modelType == null || !"LLM".equals(modelType.toUpperCase())) {
throw new RenException(ErrorCode.LLM_NOT_EXIST);
}
// 验证LLM类型
JSONObject configJson = modelConfigEntity.getConfigJson();
if (configJson != null && configJson.containsKey("type")) {
String type = configJson.get("type").toString();
if (!"openai".equals(type) && !"ollama".equals(type)) {
throw new RenException(ErrorCode.INVALID_LLM_TYPE);
}
}
}
}
/**
* 准备更新实体,处理敏感数据
*/
private ModelConfigEntity prepareUpdateEntity(ModelConfigBodyDTO modelConfigBodyDTO,
ModelConfigEntity originalEntity,
String modelType,
String id) {
// 1. 复制原始实体,保留所有原始数据(包括敏感信息)
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(originalEntity, ModelConfigEntity.class);
modelConfigEntity.setId(id);
modelConfigEntity.setModelType(modelType);
// 2. 只更新非敏感字段
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
JSONObject originalJson = originalEntity.getConfigJson();
JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改
// 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段
for (String key : modelConfigBodyDTO.getConfigJson().keySet()) {
Object value = modelConfigBodyDTO.getConfigJson().get(key);
// 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值)
if (SensitiveDataUtils.isSensitiveField(key)) {
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
updatedJson.put(key, value);
}
} else if (value instanceof JSONObject) {
// 递归处理嵌套JSON
mergeJson(updatedJson, key, (JSONObject) value);
} else {
// 非敏感字段直接更新
updatedJson.put(key, value);
}
}
modelConfigEntity.setConfigJson(updatedJson);
}
return modelConfigEntity;
}
// 辅助方法:判断值是否是掩码格式
private boolean isMaskedValue(String value) {
if (value == null)
return false;
// 简单判断是否包含掩码的特征(***)
return value.contains("***");
}
// 辅助方法:递归合并JSON,保留原始敏感字段
private void mergeJson(JSONObject original, String key, JSONObject updated) {
if (!original.containsKey(key)) {
original.put(key, new JSONObject());
}
JSONObject originalChild = original.getJSONObject(key);
for (String childKey : updated.keySet()) {
Object childValue = updated.get(childKey);
if (childValue instanceof JSONObject) {
mergeJson(originalChild, childKey, (JSONObject) childValue);
} else {
if (!SensitiveDataUtils.isSensitiveField(childKey) ||
(childValue instanceof String && !isMaskedValue((String) childValue))) {
originalChild.put(childKey, childValue);
}
}
}
}
/**
* 准备新增实体
*/
private ModelConfigEntity prepareAddEntity(ModelConfigBodyDTO modelConfigBodyDTO, String modelType) {
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setModelType(modelType);
modelConfigEntity.setIsDefault(0);
return modelConfigEntity;
}
/**
* 构建返回的DTO,处理敏感数据
*/
private ModelConfigDTO buildResponseDTO(ModelConfigEntity entity) {
ModelConfigDTO dto = ConvertUtils.sourceToTarget(entity, ModelConfigDTO.class);
if (dto.getConfigJson() != null) {
dto.setConfigJson(maskSensitiveFields(dto.getConfigJson()));
}
return dto;
}
/**
* 处理敏感字段
*/
private JSONObject maskSensitiveFields(JSONObject configJson) {
return SensitiveDataUtils.maskSensitiveFields(configJson);
}
/**
* 清除模型缓存
*/
private void clearModelCache(String id) {
redisUtils.delete(RedisKeys.getModelConfigById(id));
redisUtils.delete(RedisKeys.getModelNameById(id));
}
/**
* 按模型类型清除缓存
*/
private void clearModelCacheByType(String modelType) {
List<ModelConfigEntity> entities = modelConfigDao.selectList(
new QueryWrapper<ModelConfigEntity>().eq("model_type", modelType));
for (ModelConfigEntity entity : entities) {
clearModelCache(entity.getId());
}
} }
/** /**
* 检查智能体配置是否有引用 * 检查智能体配置是否有引用
*
* @param modelId 模型ID
*/ */
private void checkAgentReference(String modelId) { private void checkAgentReference(String modelId) {
List<AgentEntity> agents = agentDao.selectList( List<AgentEntity> agents = agentDao.selectList(
@@ -180,14 +467,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
String agentNames = agents.stream() String agentNames = agents.stream()
.map(AgentEntity::getAgentName) .map(AgentEntity::getAgentName)
.collect(Collectors.joining("")); .collect(Collectors.joining(""));
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames)); throw new RenException(ErrorCode.MODEL_REFERENCED_BY_AGENT, agentNames);
} }
} }
/** /**
* 检查意图识别配置是否有引用 * 检查意图识别配置是否有引用
*
* @param modelId 模型ID
*/ */
private void checkIntentConfigReference(String modelId) { private void checkIntentConfigReference(String modelId) {
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId); ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
@@ -198,58 +483,16 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
.eq("model_type", "Intent") .eq("model_type", "Intent")
.like("config_json", "%" + modelId + "%")); .like("config_json", "%" + modelId + "%"));
if (!intentConfigs.isEmpty()) { if (!intentConfigs.isEmpty()) {
throw new RenException("该LLM模型已被意图识别配置引用,无法删除"); throw new RenException(ErrorCode.LLM_REFERENCED_BY_INTENT);
} }
} }
} }
/**
* 获取符合条件的TTS平台列表
*/
@Override @Override
public String getModelNameById(String id) { public List<Map<String, Object>> getTtsPlatformList() {
if (StringUtils.isBlank(id)) { return modelConfigDao.getTtsPlatformList();
return null;
}
String cachedName = (String) redisUtils.get(RedisKeys.getModelNameById(id));
if (StringUtils.isNotBlank(cachedName)) {
return cachedName;
}
ModelConfigEntity entity = modelConfigDao.selectById(id);
if (entity != null) {
String modelName = entity.getModelName();
if (StringUtils.isNotBlank(modelName)) {
redisUtils.set(RedisKeys.getModelNameById(id), modelName);
}
return modelName;
}
return null;
}
@Override
public ModelConfigEntity getModelById(String id, boolean isCache) {
if (StringUtils.isBlank(id)) {
return null;
}
if (isCache) {
ModelConfigEntity cachedConfig = (ModelConfigEntity) redisUtils.get(RedisKeys.getModelConfigById(id));
if (cachedConfig != null) {
return ConvertUtils.sourceToTarget(cachedConfig, ModelConfigEntity.class);
}
}
ModelConfigEntity entity = modelConfigDao.selectById(id);
if (entity != null) {
redisUtils.set(RedisKeys.getModelConfigById(id), entity);
}
return entity;
}
@Override
public void setDefaultModel(String modelType, int isDefault) {
ModelConfigEntity entity = new ModelConfigEntity();
entity.setIsDefault(isDefault);
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType));
} }
} }
@@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
@@ -109,7 +110,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
modelProviderDTO.setFields(modelProviderDTO.getFields()); modelProviderDTO.setFields(modelProviderDTO.getFields());
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class); ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
if (modelProviderDao.insert(entity) == 0) { if (modelProviderDao.insert(entity) == 0) {
throw new RenException("新增数据失败"); throw new RenException(ErrorCode.ADD_DATA_FAILED);
} }
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class); return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
@@ -122,7 +123,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
modelProviderDTO.setUpdateDate(new Date()); modelProviderDTO.setUpdateDate(new Date());
if (modelProviderDao if (modelProviderDao
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) { .updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
throw new RenException("修改数据失败"); throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
} }
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class); return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
} }
@@ -130,14 +131,14 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
@Override @Override
public void delete(String id) { public void delete(String id) {
if (modelProviderDao.deleteById(id) == 0) { if (modelProviderDao.deleteById(id) == 0) {
throw new RenException("删除数据失败"); throw new RenException(ErrorCode.DELETE_DATA_FAILED);
} }
} }
@Override @Override
public void delete(List<String> ids) { public void delete(List<String> ids) {
if (modelProviderDao.deleteBatchIds(ids) == 0) { if (modelProviderDao.deleteBatchIds(ids) == 0) {
throw new RenException("删除数据失败"); throw new RenException(ErrorCode.DELETE_DATA_FAILED);
} }
} }
@@ -88,8 +88,10 @@ public class ShiroConfig {
// 将config路径使用server服务过滤器 // 将config路径使用server服务过滤器
filterMap.put("/config/**", "server"); filterMap.put("/config/**", "server");
filterMap.put("/agent/chat-history/report", "server"); filterMap.put("/agent/chat-history/report", "server");
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);
@@ -2,8 +2,11 @@ package xiaozhi.modules.security.config;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.ByteArrayHttpMessageConverter;
@@ -12,8 +15,10 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -95,5 +100,47 @@ public class WebMvcConfig implements WebMvcConfigurer {
converter.setObjectMapper(mapper); converter.setObjectMapper(mapper);
return converter; return converter;
} }
/**
* 国际化配置 - 根据请求头中的Accept-Language设置语言环境
*/
@Bean
public LocaleResolver localeResolver() {
return new AcceptHeaderLocaleResolver() {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.isEmpty()) {
return Locale.getDefault();
}
// 解析Accept-Language请求头中的首选语言
String[] languages = acceptLanguage.split(",");
if (languages.length > 0) {
// 提取第一个语言代码,去除可能的质量值(q=...)
String[] parts = languages[0].split(";" + "\\s*");
String primaryLanguage = parts[0].trim();
// 根据前端发送的语言代码直接创建Locale对象
if (primaryLanguage.equals("zh-CN")) {
return Locale.SIMPLIFIED_CHINESE;
} else if (primaryLanguage.equals("zh-TW")) {
return Locale.TRADITIONAL_CHINESE;
} else if (primaryLanguage.equals("en-US")) {
return Locale.US;
} else if (primaryLanguage.startsWith("zh")) {
// 对于其他中文变体,默认使用简体中文
return Locale.SIMPLIFIED_CHINESE;
} else if (primaryLanguage.startsWith("en")) {
// 对于其他英文变体,默认使用美式英语
return Locale.US;
}
}
// 如果没有匹配的语言,使用默认语言
return Locale.getDefault();
}
};
}
} }
@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
@@ -31,6 +32,8 @@ import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.service.CaptchaService; import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.security.service.SysUserTokenService; import xiaozhi.modules.security.service.SysUserTokenService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.common.utils.Sm2DecryptUtil;
import org.apache.commons.lang3.StringUtils;
import xiaozhi.modules.sys.dto.PasswordDTO; import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO; import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
@@ -42,6 +45,7 @@ import xiaozhi.modules.sys.vo.SysDictDataItem;
/** /**
* 登录控制层 * 登录控制层
*/ */
@Slf4j
@AllArgsConstructor @AllArgsConstructor
@RestController @RestController
@RequestMapping("/user") @RequestMapping("/user")
@@ -66,14 +70,15 @@ public class LoginController {
@Operation(summary = "短信验证码") @Operation(summary = "短信验证码")
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) { public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
// 验证图形验证码 // 验证图形验证码
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true); boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), false);
if (!validate) { if (!validate) {
throw new RenException("图形验证码错误"); throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
} }
Boolean isMobileRegister = sysParamsService Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) { if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用短信验证码功能"); throw new RenException(ErrorCode.MOBILE_REGISTER_DISABLED);
} }
// 发送短信验证码 // 发送短信验证码
captchaService.sendSMSValidateCode(dto.getPhone()); captchaService.sendSMSValidateCode(dto.getPhone());
@@ -83,30 +88,44 @@ public class LoginController {
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "登录") @Operation(summary = "登录")
public Result<TokenDTO> login(@RequestBody LoginDTO login) { public Result<TokenDTO> login(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码 String password = login.getPassword();
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) { // 使用工具类解密并验证验证码
throw new RenException("图形验证码错误,请重新获取"); String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
} password, login.getCaptchaId(), captchaService, sysParamsService);
login.setPassword(actualPassword);
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
// 判断用户是否存在 // 判断用户是否存在
if (userDTO == null) { if (userDTO == null) {
throw new RenException("请检测用户和密码是否输入错误"); throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
} }
// 判断密码是否正确,不一样则进入if // 判断密码是否正确,不一样则进入if
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) { if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
throw new RenException("请检测用户和密码是否输入错误"); throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
} }
return sysUserTokenService.createToken(userDTO.getId()); return sysUserTokenService.createToken(userDTO.getId());
} }
@PostMapping("/register") @PostMapping("/register")
@Operation(summary = "注册") @Operation(summary = "注册")
public Result<Void> register(@RequestBody LoginDTO login) { public Result<Void> register(@RequestBody LoginDTO login) {
if (!sysUserService.getAllowUserRegister()) { if (!sysUserService.getAllowUserRegister()) {
throw new RenException("当前不允许普通用户注册"); throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
} }
String password = login.getPassword();
// 使用工具类解密并验证验证码
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
password, login.getCaptchaId(), captchaService, sysParamsService);
login.setPassword(actualPassword);
// 是否开启手机注册 // 是否开启手机注册
Boolean isMobileRegister = sysParamsService Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
@@ -115,25 +134,19 @@ public class LoginController {
// 验证用户是否是手机号码 // 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername()); boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
if (!validPhone) { if (!validPhone) {
throw new RenException("用户名不是手机号码,请重新输入"); throw new RenException(ErrorCode.USERNAME_NOT_PHONE);
} }
// 验证短信验证码是否正常 // 验证短信验证码是否正常
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false); validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
if (!validate) { if (!validate) {
throw new RenException("手机验证码错误,请重新获取"); throw new RenException(ErrorCode.SMS_CODE_ERROR);
}
} else {
// 验证是否正确输入验证码
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误,请重新获取");
} }
} }
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null) { if (userDTO != null) {
throw new RenException("此手机号码已经注册过"); throw new RenException(ErrorCode.PHONE_ALREADY_REGISTERED);
} }
userDTO = new SysUserDTO(); userDTO = new SysUserDTO();
userDTO.setUsername(login.getUsername()); userDTO.setUsername(login.getUsername());
@@ -168,28 +181,36 @@ public class LoginController {
Boolean isMobileRegister = sysParamsService Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) { if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用找回密码功能"); throw new RenException(ErrorCode.RETRIEVE_PASSWORD_DISABLED);
} }
// 判断非空 // 判断非空
ValidatorUtils.validateEntity(dto); ValidatorUtils.validateEntity(dto);
// 验证用户是否是手机号码 // 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone()); boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
if (!validPhone) { if (!validPhone) {
throw new RenException("输入的手机号码格式不正确"); throw new RenException(ErrorCode.PHONE_FORMAT_ERROR);
} }
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone()); SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
if (userDTO == null) { if (userDTO == null) {
throw new RenException("输入的手机号码未注册"); throw new RenException(ErrorCode.PHONE_NOT_REGISTERED);
} }
// 验证短信验证码是否正常 // 验证短信验证码是否正常
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false); boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
// 判断是否通过验证 // 判断是否通过验证
if (!validate) { if (!validate) {
throw new RenException("输入的手机验证码错误"); throw new RenException(ErrorCode.SMS_CODE_ERROR);
} }
String password = dto.getPassword();
// 使用工具类解密并验证验证码
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
password, dto.getCaptchaId(), captchaService, sysParamsService);
dto.setPassword(actualPassword);
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword()); sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
return new Result<>(); return new Result<>();
} }
@@ -208,6 +229,13 @@ public class LoginController {
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true)); config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true)); config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true)); config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
// SM2公钥
String publicKey = sysParamsService.getValue(Constant.SM2_PUBLIC_KEY, true);
if (StringUtils.isBlank(publicKey)) {
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
}
config.put("sm2PublicKey", publicKey);
return new Result<Map<String, Object>>().ok(config); return new Result<Map<String, Object>>().ok(config);
} }
@@ -21,10 +21,6 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}") @NotBlank(message = "{sysuser.password.require}")
private String password; private String password;
@Schema(description = "验证码")
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "手机验证码") @Schema(description = "手机验证码")
private String mobileCaptcha; private String mobileCaptcha;
@@ -18,6 +18,7 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.utils.HttpContextUtils; import xiaozhi.common.utils.HttpContextUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.MessageUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
/** /**
@@ -82,8 +83,8 @@ public class Oauth2Filter extends AuthenticatingFilter {
httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try { try {
Throwable throwable = e.getCause() == null ? e : e.getCause(); // 使用国际化消息替代直接使用异常消息
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED, throwable.getMessage()); Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED);
String json = JsonUtils.toJsonString(r); String json = JsonUtils.toJsonString(r);
httpResponse.getWriter().print(json); httpResponse.getWriter().print(json);
@@ -3,6 +3,7 @@ package xiaozhi.modules.security.oauth2;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.UUID; import java.util.UUID;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
/** /**
@@ -38,7 +39,7 @@ public class TokenGenerator {
byte[] messageDigest = algorithm.digest(); byte[] messageDigest = algorithm.digest();
return toHexString(messageDigest); return toHexString(messageDigest);
} catch (Exception e) { } catch (Exception e) {
throw new RenException("token invalid", e); throw new RenException(ErrorCode.TOKEN_GENERATE_ERROR, e);
} }
} }
} }
@@ -16,6 +16,7 @@ import com.wf.captcha.base.Captcha;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
@@ -88,7 +89,7 @@ public class CaptchaServiceImpl implements CaptchaService {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - lastSendTimeLong; long timeDiff = currentTime - lastSendTimeLong;
if (timeDiff < 60000) { if (timeDiff < 60000) {
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试"); throw new RenException(ErrorCode.SMS_SEND_TOO_FREQUENTLY, String.valueOf((60000 - timeDiff) / 1000));
} }
} }
@@ -108,7 +109,7 @@ public class CaptchaServiceImpl implements CaptchaService {
} }
if (todayCount >= maxSendCount) { if (todayCount >= maxSendCount) {
throw new RenException("今日发送次数已达上限"); throw new RenException(ErrorCode.TODAY_SMS_LIMIT_REACHED);
} }
String key = RedisKeys.getSMSValidateCodeKey(phone); String key = RedisKeys.getSMSValidateCodeKey(phone);
@@ -9,6 +9,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
@@ -45,7 +46,7 @@ public class ALiYunSmsService implements SmsService {
redisUtils.delete(todayCountKey); redisUtils.delete(todayCountKey);
// 错误 message // 错误 message
log.error(e.getMessage()); log.error(e.getMessage());
throw new RenException("短信发送失败"); throw new RenException(ErrorCode.SMS_SEND_FAILED);
} }
} }
@@ -70,7 +71,7 @@ public class ALiYunSmsService implements SmsService {
}catch (Exception e){ }catch (Exception e){
// 错误 message // 错误 message
log.error(e.getMessage()); log.error(e.getMessage());
throw new RenException("短信连接建立失败"); throw new RenException(ErrorCode.SMS_CONNECTION_FAILED);
} }
} }
} }
@@ -22,6 +22,7 @@ import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.annotation.LogOperation; import xiaozhi.common.annotation.LogOperation;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.sys.dto.EmitSeverActionDTO; import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
@@ -64,17 +65,17 @@ public class ServerSideManageController {
@RequiresPermissions("sys:role:superAdmin") @RequiresPermissions("sys:role:superAdmin")
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) { public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
if (emitSeverActionDTO.getAction() == null) { if (emitSeverActionDTO.getAction() == null) {
throw new RenException("无效服务端操作"); throw new RenException(ErrorCode.INVALID_SERVER_ACTION);
} }
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsText)) { if (StringUtils.isBlank(wsText)) {
throw new RenException("未配置服务端WebSocket地址"); throw new RenException(ErrorCode.SERVER_WEBSOCKET_NOT_CONFIGURED);
} }
String targetWs = emitSeverActionDTO.getTargetWs(); String targetWs = emitSeverActionDTO.getTargetWs();
String[] wsList = wsText.split(";"); String[] wsList = wsText.split(";");
// 找到需要发起的 // 找到需要发起的
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) { if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
throw new RenException("目标WebSocket地址不存在"); throw new RenException(ErrorCode.TARGET_WEBSOCKET_NOT_EXIST);
} }
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction())); return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
} }
@@ -114,7 +115,7 @@ public class ServerSideManageController {
}); });
} catch (Exception e) { } catch (Exception e) {
// 捕获全部错误,由全局异常处理器返回 // 捕获全部错误,由全局异常处理器返回
throw new RenException("WebSocket连接失败或连接超时"); throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED);
} }
return true; return true;
} }
@@ -25,6 +25,7 @@ import lombok.AllArgsConstructor;
import xiaozhi.common.annotation.LogOperation; import xiaozhi.common.annotation.LogOperation;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils; import xiaozhi.common.validator.AssertUtils;
@@ -107,9 +108,12 @@ public class SysParamsController {
// 验证MCP地址 // 验证MCP地址
validateMcpUrl(dto.getParamCode(), dto.getParamValue()); validateMcpUrl(dto.getParamCode(), dto.getParamValue());
// // 验证声纹地址
validateVoicePrint(dto.getParamCode(), dto.getParamValue()); validateVoicePrint(dto.getParamCode(), dto.getParamValue());
// 校验mqtt密钥长度
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
sysParamsService.update(dto); sysParamsService.update(dto);
configService.getConfig(false); configService.getConfig(false);
return new Result<Void>(); return new Result<Void>();
@@ -127,23 +131,23 @@ public class SysParamsController {
} }
String[] wsUrls = urls.split("\\;"); String[] wsUrls = urls.split("\\;");
if (wsUrls.length == 0) { if (wsUrls.length == 0) {
throw new RenException("WebSocket地址列表不能为空"); throw new RenException(ErrorCode.WEBSOCKET_URLS_EMPTY);
} }
for (String url : wsUrls) { for (String url : wsUrls) {
if (StringUtils.isNotBlank(url)) { if (StringUtils.isNotBlank(url)) {
// 检查是否包含localhost或127.0.0.1 // 检查是否包含localhost或127.0.0.1
if (url.contains("localhost") || url.contains("127.0.0.1")) { if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1"); throw new RenException(ErrorCode.WEBSOCKET_URL_LOCALHOST);
} }
// 验证WebSocket地址格式 // 验证WebSocket地址格式
if (!WebSocketValidator.validateUrlFormat(url)) { if (!WebSocketValidator.validateUrlFormat(url)) {
throw new RenException("WebSocket地址格式不正确: " + url); throw new RenException(ErrorCode.WEBSOCKET_URL_FORMAT_ERROR);
} }
// 测试WebSocket连接 // 测试WebSocket连接
if (!WebSocketValidator.testConnection(url)) { if (!WebSocketValidator.testConnection(url)) {
throw new RenException("WebSocket连接测试失败: " + url); throw new RenException(ErrorCode.WEBSOCKET_CONNECTION_FAILED);
} }
} }
} }
@@ -170,35 +174,35 @@ public class SysParamsController {
return; return;
} }
if (StringUtils.isBlank(url) || url.equals("null")) { if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("OTA地址不能为空"); throw new RenException(ErrorCode.OTA_URL_EMPTY);
} }
// 检查是否包含localhost或127.0.0.1 // 检查是否包含localhost或127.0.0.1
if (url.contains("localhost") || url.contains("127.0.0.1")) { if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("OTA地址不能使用localhost或127.0.0.1"); throw new RenException(ErrorCode.OTA_URL_LOCALHOST);
} }
// 验证URL格式 // 验证URL格式
if (!url.toLowerCase().startsWith("http")) { if (!url.toLowerCase().startsWith("http")) {
throw new RenException("OTA地址必须以http或https开头"); throw new RenException(ErrorCode.OTA_URL_PROTOCOL_ERROR);
} }
if (!url.endsWith("/ota/")) { if (!url.endsWith("/ota/")) {
throw new RenException("OTA地址必须以/ota/结尾"); throw new RenException(ErrorCode.OTA_URL_FORMAT_ERROR);
} }
try { try {
// 发送GET请求 // 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class); ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode()); throw new RenException(ErrorCode.OTA_INTERFACE_ACCESS_FAILED);
} }
// 检查响应内容是否包含OTA相关信息 // 检查响应内容是否包含OTA相关信息
String body = response.getBody(); String body = response.getBody();
if (body == null || !body.contains("OTA")) { if (body == null || !body.contains("OTA")) {
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口"); throw new RenException(ErrorCode.OTA_INTERFACE_FORMAT_ERROR);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RenException("OTA接口验证失败:" + e.getMessage()); throw new RenException(ErrorCode.OTA_INTERFACE_VALIDATION_FAILED);
} }
} }
@@ -207,61 +211,86 @@ public class SysParamsController {
return; return;
} }
if (StringUtils.isBlank(url) || url.equals("null")) { if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("MCP地址不能为空"); throw new RenException(ErrorCode.MCP_URL_EMPTY);
} }
if (url.contains("localhost") || url.contains("127.0.0.1")) { if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("MCP地址不能使用localhost或127.0.0.1"); throw new RenException(ErrorCode.MCP_URL_LOCALHOST);
} }
if (!url.toLowerCase().contains("key")) { if (!url.toLowerCase().contains("key")) {
throw new RenException("不是正确的MCP地址"); throw new RenException(ErrorCode.MCP_URL_INVALID);
} }
try { try {
// 发送GET请求 // 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class); ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode()); throw new RenException(ErrorCode.MCP_INTERFACE_ACCESS_FAILED);
} }
// 检查响应内容是否包含mcp相关信息 // 检查响应内容是否包含mcp相关信息
String body = response.getBody(); String body = response.getBody();
if (body == null || !body.contains("success")) { if (body == null || !body.contains("success")) {
throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口"); throw new RenException(ErrorCode.MCP_INTERFACE_FORMAT_ERROR);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RenException("MCP接口验证失败:" + e.getMessage()); throw new RenException(ErrorCode.MCP_INTERFACE_VALIDATION_FAILED);
} }
} }
// 验证声纹接口地址是否正常 // 验证声纹接口地址是否正常
private void validateVoicePrint(String paramCode, String url) { private void validateVoicePrint(String paramCode, String url) {
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) { if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
return; return;
} }
if (StringUtils.isBlank(url) || url.equals("null")) { if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("声纹接口地址不能为空"); throw new RenException(ErrorCode.VOICEPRINT_URL_EMPTY);
} }
if (url.contains("localhost") || url.contains("127.0.0.1")) { if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("声纹接口地址不能使用localhost或127.0.0.1"); throw new RenException(ErrorCode.VOICEPRINT_URL_LOCALHOST);
} }
if (!url.toLowerCase().contains("key")) { if (!url.toLowerCase().contains("key")) {
throw new RenException("不是正确的声纹接口地址"); throw new RenException(ErrorCode.VOICEPRINT_URL_INVALID);
} }
// 验证URL格式 // 验证URL格式
if (!url.toLowerCase().startsWith("http")) { if (!url.toLowerCase().startsWith("http")) {
throw new RenException("声纹接口地址必须以http或https开头"); throw new RenException(ErrorCode.VOICEPRINT_URL_PROTOCOL_ERROR);
} }
try { try {
// 发送GET请求 // 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class); ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) { if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode()); throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_ACCESS_FAILED);
} }
// 检查响应内容 // 检查响应内容
String body = response.getBody(); String body = response.getBody();
if (body == null || !body.contains("healthy")) { if (body == null || !body.contains("healthy")) {
throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口"); throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_FORMAT_ERROR);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RenException("声纹接口验证失败:" + e.getMessage()); throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_VALIDATION_FAILED);
}
}
// 校验mqtt密钥长度和复杂度
private void validateMqttSecretLength(String paramCode, String secret) {
if (!paramCode.equals(Constant.SERVER_MQTT_SECRET)) {
return;
}
if (StringUtils.isBlank(secret) || secret.equals("null")) {
throw new RenException(ErrorCode.MQTT_SECRET_EMPTY);
}
if (secret.length() < 8) {
throw new RenException(ErrorCode.MQTT_SECRET_LENGTH_INSECURE);
}
// 检查是否同时包含大小写字母
if (!secret.matches(".*[a-z].*") || !secret.matches(".*[A-Z].*")) {
throw new RenException(ErrorCode.MQTT_SECRET_CHARACTER_INSECURE);
}
// 不允许包含弱密码
String[] weakPasswords = { "test", "1234", "admin", "password", "qwerty", "xiaozhi" };
for (String weakPassword : weakPasswords) {
if (secret.toLowerCase().contains(weakPassword)) {
throw new RenException(ErrorCode.MQTT_SECRET_WEAK_PASSWORD);
}
} }
} }
} }
@@ -25,6 +25,10 @@ public class RetrievePasswordDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}") @NotBlank(message = "{sysuser.password.require}")
private String password; private String password;
@Schema(description = "图形验证码ID")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;
} }
@@ -17,6 +17,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
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;
@@ -153,7 +154,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
} }
boolean exists = baseDao.exists(queryWrapper); boolean exists = baseDao.exists(queryWrapper);
if (exists) { if (exists) {
throw new RenException("字典标签重复"); throw new RenException(ErrorCode.DICT_LABEL_DUPLICATE);
} }
} }
@@ -18,6 +18,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
@@ -66,7 +67,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
public SysDictTypeVO get(Long id) { public SysDictTypeVO get(Long id) {
SysDictTypeEntity entity = baseDao.selectById(id); SysDictTypeEntity entity = baseDao.selectById(id);
if (entity == null) { if (entity == null) {
throw new RenException("字典类型不存在"); throw new RenException(ErrorCode.DICT_TYPE_NOT_EXIST);
} }
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class); return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
@@ -147,7 +148,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
} }
boolean exists = baseDao.exists(queryWrapper); boolean exists = baseDao.exists(queryWrapper);
if (exists) { if (exists) {
throw new RenException("字典类型编码重复"); throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE);
} }
} }
} }
@@ -21,6 +21,7 @@ import xiaozhi.common.page.PageData;
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.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.SM2Utils;
import xiaozhi.modules.sys.dao.SysParamsDao; import xiaozhi.modules.sys.dao.SysParamsDao;
import xiaozhi.modules.sys.dto.SysParamsDTO; import xiaozhi.modules.sys.dto.SysParamsDTO;
import xiaozhi.modules.sys.entity.SysParamsEntity; import xiaozhi.modules.sys.entity.SysParamsEntity;
@@ -206,6 +207,31 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
String newSecret = UUID.randomUUID().toString(); String newSecret = UUID.randomUUID().toString();
updateValueByCode(Constant.SERVER_SECRET, newSecret); updateValueByCode(Constant.SERVER_SECRET, newSecret);
} }
// 初始化SM2密钥对
initSM2KeyPair();
}
/**
* 初始化SM2密钥对
*/
private void initSM2KeyPair() {
// 获取SM2公钥
String publicKey = getValue(Constant.SM2_PUBLIC_KEY, false);
// 获取SM2私钥
String privateKey = getValue(Constant.SM2_PRIVATE_KEY, false);
// 如果公钥或私钥为空,则生成新的密钥对
if (StringUtils.isBlank(publicKey) || StringUtils.isBlank(privateKey) ||
"null".equals(publicKey) || "null".equals(privateKey)) {
Map<String, String> keyPair = SM2Utils.createKey();
String newPublicKey = keyPair.get(SM2Utils.KEY_PUBLIC_KEY);
String newPrivateKey = keyPair.get(SM2Utils.KEY_PRIVATE_KEY);
// 更新数据库中的密钥对
updateValueByCode(Constant.SM2_PUBLIC_KEY, newPublicKey);
updateValueByCode(Constant.SM2_PRIVATE_KEY, newPrivateKey);
}
} }
/** /**
@@ -115,7 +115,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
// 判断旧密码是否正确 // 判断旧密码是否正确
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) { if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
throw new RenException("旧密码输入错误"); throw new RenException(ErrorCode.OLD_PASSWORD_ERROR);
} }
// 新密码强度 // 新密码强度
@@ -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,3 @@
delete from `sys_params` where param_code = 'server.mqtt_manager_api';
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
VALUES (119, 'server.mqtt_manager_api', 'null', 'string', 1, 'MQTT网关管理API的地址');
@@ -0,0 +1,54 @@
-- 添加阿里百炼流式TTS 供应器
delete from `ai_model_provider` where id = 'SYSTEM_TTS_AliBLStreamTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_AliBLStreamTTS', 'TTS', 'alibl_stream', '阿里百炼流式语音合成', '[{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "rate", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"}]', 19, 1, NOW(), 1, NOW());
-- 添加阿里百炼流式TTS模型配置
delete from `ai_model_config` where id = 'TTS_AliBLStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_AliBLStreamTTS', 'TTS', 'AliBLStreamTTS', '阿里百炼流式语音合成', 0, 1, '{\"type\": \"alibl_stream\", \"appkey\": \"\", \"output_dir\": \"tmp/\", \"model\": \"cosyvoice-v2\", \"voice\": \"longcheng_v2\", \"format\": \"pcm\", \"sample_rate\": 24000, \"volume\": 50, \"rate\": 1, \"pitch\": 1}', NULL, NULL, 22, NULL, NULL, NULL, NULL);
-- 更新阿里百炼流式TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
`remark` = '阿里百炼流式TTS说明:
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key 创建项目并获取appkey
2. 支持实时流式合成,具有较低的延迟
3. 支持多种音色设置和音频参数调整
4. 支持CosyVoice-V3大模型音色,价格实惠(0.4元/万字符)
5. 支持实时调节音量、语速、音调等参数
6. 如果需要使用CosyVoice-V3模型和一些限制类型的音色,需要联系阿里百炼客服申请
' WHERE `id` = 'TTS_AliBLStreamTTS';
-- 添加阿里百炼流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_AliBLStreamTTS';
-- 语音助手
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0001', 'TTS_AliBLStreamTTS', '龙小淳-知性积极女', 'longxiaochun_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0002', 'TTS_AliBLStreamTTS', '龙小夏-沉稳权威女', 'longxiaoxia_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
-- 直播带货
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0003', 'TTS_AliBLStreamTTS', '龙安燃-活泼质感女', 'longanran', '中文及中英文混合', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0004', 'TTS_AliBLStreamTTS', '龙安宣-经典直播女', 'longanxuan', '中文及中英文混合', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
-- 社交陪伴
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0005', 'TTS_AliBLStreamTTS', '龙寒-温暖痴情男', 'longhan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0006', 'TTS_AliBLStreamTTS', '龙颜-温暖春风女', 'longyan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0007', 'TTS_AliBLStreamTTS', '龙菲菲-甜美矫情女', 'longfeifei_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
-- 方言
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0008', 'TTS_AliBLStreamTTS', '龙老铁-东北直率男', 'longlaotie_v2', '中文(东北)及中英文混合', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0009', 'TTS_AliBLStreamTTS', '龙嘉怡-知性粤语女', 'longjiayi_v2', '中文(粤语)及中英文混合', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
-- 童声
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0010', 'TTS_AliBLStreamTTS', '龙杰力豆-阳光顽皮男', 'longjielidou_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0011', 'TTS_AliBLStreamTTS', '龙铃-稚气呆板女', 'longling_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
-- 诗歌朗诵
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0012', 'TTS_AliBLStreamTTS', '李白-古代诗仙男', 'libai_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
-- 出海营销
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0013', 'TTS_AliBLStreamTTS', 'loongeva-知性英文女', 'loongeva_v2', '英式英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0014', 'TTS_AliBLStreamTTS', 'loongbrian-沉稳英文男', 'loongbrian_v2', '英式英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0015', 'TTS_AliBLStreamTTS', 'loongkyong-韩语女', 'loongkyong_v2', '韩语', NULL, NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0016', 'TTS_AliBLStreamTTS', 'loongtomoka-日语女', 'loongtomoka_v2', '日语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0017', 'TTS_AliBLStreamTTS', 'loongtomoya-日语男', 'loongtomoya_v2', '日语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
@@ -0,0 +1,41 @@
-- 添加讯飞流式TTS供应器
delete from `ai_model_provider` where id = 'SYSTEM_TTS_XunFeiStreamTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_XunFeiStreamTTS', 'TTS', 'xunfei_stream', '讯飞流式语音合成', '[{"key":"app_id","label":"APP_ID","type":"string"},{"key":"api_secret","label":"API_Secret","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "speed", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"},{"key": "oral_level", "type": "number", "label": "口语化等级"},{"key": "spark_assist", "type": "number", "label": "是否口语化"},{"key": "stop_split", "type": "number", "label": "服务端拆句"},{"key": "remain", "type": "number", "label": "保留书面语"}]', 20, 1, NOW(), 1, NOW());
-- 添加讯飞流式TTS模型配置
delete from `ai_model_config` where id = 'TTS_XunFeiStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_XunFeiStreamTTS', 'TTS', 'XunFeiStreamTTS', '讯飞流式语音合成', 0, 1, '{\"type\": \"xunfei_stream\", \"app_id\": \"\", \"api_secret\": \"\", \"api_key\": \"\", \"output_dir\": \"tmp/\", \"voice\": \"x5_lingxiaoxuan_flow\", \"format\": \"raw\", \"sample_rate\": 24000, \"volume\": 50, \"speed\": 50, \"pitch\": 50, \"oral_level\": \"mid\", \"spark_assist\": 1, \"stop_split\": 0, \"remain\": 0}', NULL, NULL, 23, NULL, NULL, NULL, NULL);
-- 更新讯飞流式TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.xfyun.cn/app/myapp',
`remark` = '讯飞流式TTS说明:
1. 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用
2. 选择需要的服务获取api相关配置 https://console.xfyun.cn/services/uts
3. 为需要使用的应用(APPID)购买相关服务 例如:超拟人合成 https://console.xfyun.cn/services/uts
5. 支持实时双流式通信,具有较低的延迟
6. 支持口语化设置和音频参数调整 注意:V5音色不支持相关口语化配置
7. 支持实时调节音量、语速、音调等参数
' WHERE `id` = 'TTS_XunFeiStreamTTS';
-- 添加讯飞流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_XunFeiStreamTTS';
-- 基础角色
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0001', 'TTS_XunFeiStreamTTS', '聆小璇', 'x5_lingxiaoxuan_flow', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0002', 'TTS_XunFeiStreamTTS', '聆飞逸', 'x5_lingfeiyi_flow', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0003', 'TTS_XunFeiStreamTTS', '聆小玥', 'x5_lingxiaoyue_flow', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0004', 'TTS_XunFeiStreamTTS', '聆玉昭', 'x5_lingyuzhao_flow', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0005', 'TTS_XunFeiStreamTTS', '聆玉言', 'x5_lingyuyan_flow', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
-- 需要添加对应的角色音色
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0006', 'TTS_XunFeiStreamTTS', '聆飞哲', 'x4_lingfeizhe_oral', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0007', 'TTS_XunFeiStreamTTS', '聆小璃', 'x4_lingxiaoli_oral', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0008', 'TTS_XunFeiStreamTTS', '聆小糖', 'x5_lingxiaotang_flow', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0009', 'TTS_XunFeiStreamTTS', '聆小琪', 'x4_lingxiaoqi_oral', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0010', 'TTS_XunFeiStreamTTS', '聆佑佑-童年女声', 'x4_lingyouyou_oral', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0011', 'TTS_XunFeiStreamTTS', '子津', 'x4_zijin_oral', '天津话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0012', 'TTS_XunFeiStreamTTS', '子阳', 'x4_ziyang_oral', '东北话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0013', 'TTS_XunFeiStreamTTS', 'Grant', 'x5_EnUs_Grant_flow', '英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0014', 'TTS_XunFeiStreamTTS', 'Lila', 'x5_EnUs_Lila_flow', '英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
@@ -0,0 +1,26 @@
-- 添加讯飞流式语音识别服务配置
delete from `ai_model_provider` where id = 'SYSTEM_ASR_XunfeiStream';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_XunfeiStream', 'ASR', 'xunfei_stream', '讯飞流式语音识别', '[{"key":"app_id","label":"应用ID","type":"string"},{"key":"api_key","label":"API_KEY","type":"password"},{"key":"api_secret","label":"API_SECRET","type":"password"},{"key":"domain","label":"识别领域","type":"string"},{"key":"language","label":"识别语言","type":"string"},{"key":"accent","label":"方言","type":"string"},{"key":"dwa","label":"动态修正","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW());
delete from `ai_model_config` where id = 'ASR_XunfeiStream';
INSERT INTO `ai_model_config` VALUES ('ASR_XunfeiStream', 'ASR', '讯飞流式语音识别', '讯飞流式语音识别服务', 0, 1, '{"type": "xunfei_stream", "app_id": "", "api_key": "", "api_secret": "", "domain": "slm", "language": "zh_cn", "accent": "mandarin", "dwa": "wpgs", "output_dir": "tmp/"}', 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html', '支持实时流式语音识别,适用于中文普通话及多种方言识别', 21, NULL, NULL, NULL, NULL);
-- 更新讯飞流式语音识别模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html',
`remark` = '讯飞流式语音识别配置说明:
1. 登录讯飞开放平台 https://www.xfyun.cn/
2. 创建语音识别应用获取APPID、APISecret、APIKey
3. 参数说明:
- app_id: 应用ID,在讯飞开放平台创建应用后获得
- api_key: API密钥,用于接口鉴权
- api_secret: API密钥,用于生成签名
- domain: 识别领域,默认slm(智能化语音转写)
- language: 识别语言,默认zh_cn(中文)
- accent: 方言类型,默认mandarin(普通话),支持cantonese(粤语)等
- dwa: 动态修正,默认wpgs(开启动态修正)
- output_dir: 音频文件输出目录,默认tmp/
4. 支持实时流式识别,适用于实时语音交互场景
5. 支持多种方言和语言识别
' WHERE `id` = 'ASR_XunfeiStream';
@@ -0,0 +1,19 @@
delete from `ai_model_config` where id = 'LLM_XunfeiSparkLLM';
INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火认知大模型', '讯飞星火认知大模型', 0, 1, '{"type": "openai", "model_name": "generalv3.5", "base_url": "https://spark-api-open.xf-yun.com/v1", "api_password": "你的api_password", "temperature": 0.5, "max_tokens": 2048, "top_p": 1.0, "frequency_penalty": 0.0}', 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', '讯飞星火认知大模型,支持多轮对话、文本生成等功能', 14, NULL, NULL, NULL, NULL);
-- 更新讯飞星火认知大模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html',
`remark` = '讯飞星火认知大模型配置说明:
1. 登录讯飞开放平台 https://www.xfyun.cn/,每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password
2. 创建星火认知大模型应用获取API Password
3. 参数说明:
- api_password: API Password,在讯飞开放平台创建应用后获得
- model_name: 模型名称,支持generalv3.5、generalv3等版本
- base_url: API地址,默认https://spark-api-open.xf-yun.com/v1
- temperature: 温度参数,控制生成随机性,范围0-1,默认0.5
- max_tokens: 最大输出token数,默认2048
- top_p: 核心采样参数,控制词汇多样性,默认1.0
- frequency_penalty: 频率惩罚,降低重复内容,默认0.0
4. 每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password。
' WHERE `id` = 'LLM_XunfeiSparkLLM';
@@ -0,0 +1,7 @@
-- 添加SM2国密算法密钥参数
-- 用于服务器端SM2加密解密功能
-- 添加SM2密钥参数
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
(120, 'server.public_key', '', 'string', 1, '服务器SM2公钥'),
(121, 'server.private_key', '', 'string', 1, '服务器SM2私钥');
@@ -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';
@@ -345,3 +345,60 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202509080922.sql path: classpath:db/changelog/202509080922.sql
- changeSet:
id: 202509161610
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509161610.sql
- changeSet:
id: 202509161701
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509161701.sql
- changeSet:
id: 202509221530
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509221530.sql
- changeSet:
id: 202509191545
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509191545.sql
- changeSet:
id: 202509220926
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509220926.sql
- changeSet:
id: 202509220958
author: fyb
changes:
- sqlFile:
encoding: utf8
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,7 +1,8 @@
#Default #\u7B80\u4F53\u4E2D\u6587
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38 500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=\u672A\u6388\u6743 401=\u672A\u6388\u6743
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650 403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}\u4E0D\u80FD\u4E3A\u7A7A 10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55 10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25 10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
@@ -9,9 +10,10 @@
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u624B\u673A\u53F7\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
@@ -20,21 +22,25 @@
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B 10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0} 10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6 10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0} 10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728 10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis\u670D\u52A1\u5F02\u5E38 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5F31\u5BC6\u7801 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=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A 10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A 10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B 10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
@@ -45,3 +51,121 @@
10041=\u8BBE\u5907\u672A\u627E\u5230 10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0} 10042={0}
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token\u751F\u6210\u5931\u8D25
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
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=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
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=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
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=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
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=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
10130=SM2\u89E3\u5BC6\u5931\u8D25
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
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
@@ -11,19 +11,26 @@
10007=The verification code is incorrect 10007=The verification code is incorrect
10008=First delete submenu or button 10008=First delete submenu or button
10009=The original password is incorrect 10009=The original password is incorrect
10010=The account or password is incorrect, you can try {0} more times
10011=The superior department made a wrong choice 10011=The superior department made a wrong choice
10012=Upper menu cannot be for itself 10012=Upper menu cannot be for itself
10013=Data permission interface, which can only be a Map type parameter. 10013=Data permission interface, which can only be a Map type parameter.
10014=Please delete the subordinate department first 10014=Please delete the subordinate department first
10015=Please delete the user under the department first 10015=Please delete the user under the department first
10016=Deployment failed, no process
10017=The model diagram is incorrect, please check
10018=Export failed, model ID is {0}
10019=Please upload a file 10019=Please upload a file
10020=token cannot be empty 10020=token cannot be empty
10021=token is invalid, please log in again 10021=token is invalid, please log in again
10022=The account has been locked 10022=The account has been locked
10023=Please upload files in zip, bar, bpmn, bpmn20.xml format
10024=Failed to upload file {0} 10024=Failed to upload file {0}
10025=Failed to send SMS {0}
10026=Email template does not exist
10027=Redis service exception 10027=Redis service exception
10028=Timed task failed 10028=Timed task failed
@@ -43,4 +50,122 @@
10040=Parameter value must be a valid JSON format 10040=Parameter value must be a valid JSON format
10041=Device not found 10041=Device not found
10042={0} 10042={0}
10043=Failed to delete data
10044=User not logged in
10045=WebSocket connection failed or timeout
10046=Failed to save voiceprint, please contact administrator
10047=Daily sending limit reached
10048=Old password is incorrect
10049=The set LLM is not openai or ollama
10050=Failed to generate token
10051=Resource not found
10052=Default agent not found
10053=Agent not found
10054=Voiceprint interface not configured, please configure voiceprint interface address (server.voice_print) in parameter configuration first
10055=Failed to send SMS
10056=Failed to establish SMS connection
10057=Failed to create voiceprint for agent
10058=Failed to update voiceprint for agent
10059=Failed to delete voiceprint for agent
10060=Sending too frequently, please try again after {0} seconds
10061=Activation code cannot be empty
10062=Activation code error
10063=Device already activated
10064=This model is the default model, please set another model as default first
10065=Failed to add data
10066=Failed to update data
10067=Graphic verification code error
10068=Mobile registration is not enabled, SMS verification code function cannot be used
10069=Username is not a mobile phone number, please re-enter
10070=This mobile phone number has already been registered
10071=The entered mobile phone number is not registered
10072=Ordinary user registration is not allowed currently
10073=Mobile registration is not enabled, password retrieval function cannot be used
10074=The entered mobile phone number format is incorrect
10075=The entered SMS verification code is incorrect
10076=Dictionary type does not exist
10077=Dictionary type code is duplicated
10078=Failed to read resource
10079=LLM model and Intent recognition, parameter selection does not match
10080=This voiceprint belongs to {0} who has already registered, please select another voice
10081=Error occurred while deleting voiceprint
10082=Modification not allowed, this voice has already been registered as a voiceprint ({0})
10083=Error modifying voiceprint, please contact administrator
10084=Voiceprint interface address error, please go to parameter management to modify the voiceprint interface address
10085=Audio data does not belong to this agent
10086=Audio data is empty, please check uploaded data
10087=Voiceprint registration failed, request unsuccessful
10088=Voiceprint registration failed, request processing failed
10089=Voiceprint cancellation failed, request unsuccessful
10090=Voiceprint cancellation failed, request processing failed
10091=Model provider does not exist
10092=The configured LLM does not exist
10093=This model configuration is referenced by agent {0} and cannot be deleted
10094=This LLM model is referenced by intent recognition configuration and cannot be deleted
10095=Invalid server operation
10096=Server WebSocket address not configured
10097=Target WebSocket address does not exist
10098=WebSocket address list cannot be empty
10099=WebSocket address cannot use localhost or 127.0.0.1
10100=WebSocket address format is incorrect
10101=WebSocket connection test failed
10102=OTA address cannot be empty
10103=OTA address cannot use localhost or 127.0.0.1
10104=OTA address must start with http or https
10105=OTA address must end with /ota/
10106=OTA interface access failed
10107=OTA interface return content format is incorrect
10108=OTA interface validation failed
10109=MCP address cannot be empty
10110=MCP address cannot use localhost or 127.0.0.1
10111=Not a valid MCP address
10112=MCP interface access failed
10113=MCP interface return content format is incorrect
10114=MCP interface validation failed
10115=Voiceprint interface address cannot be empty
10116=Voiceprint interface address cannot use localhost or 127.0.0.1
10117=Not a valid voiceprint interface address
10118=Voiceprint interface address must start with http or https
10119=Voiceprint interface access failed
10120=Voiceprint interface return content format is incorrect
10121=Voiceprint interface validation failed
10122=MQTT secret cannot be empty
10123=Your MQTT secret is not secure, it needs to be at least 8 characters and must contain both uppercase and lowercase letters
10124=Your MQTT secret is not secure, MQTT secret must contain both uppercase and lowercase letters
10125=Your MQTT secret contains weak password
10128=Dictionary label is duplicated
10129=SM2 key not configured
10130=SM2 decryption failed
10131=modelType and provideCode cannot be empty
10132=No permission to view the chat history of this agent
10133=Session ID cannot be empty
10134=Agent ID cannot be empty
10135=Chat history download failed
10136=Download link expired or invalid
10137=Download link invalid
10138=User
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
@@ -12,25 +12,32 @@
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8 10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237 10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis\u670D\u52A1\u5F02\u5E38 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210 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=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
@@ -43,4 +50,122 @@
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F 10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=\u8BBE\u5907\u672A\u627E\u5230 10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0} 10042={0}
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token\u751F\u6210\u5931\u8D25
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
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=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
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=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
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=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
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=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
10130=SM2\u89E3\u5BC6\u5931\u8D25
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
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=\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,7 +1,7 @@
#\u7E41\u4F53\u4E2D\u6587 #\u7E41\u9AD4\u4E2D\u6587
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38 500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
401=\u672A\u6388\u6B0A 401=\u672A\u6388\u6B0A
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650 403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6743\u9650
10001={0}\u4E0D\u80FD\u70BA\u7A7A 10001={0}\u4E0D\u80FD\u70BA\u7A7A
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304 10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
@@ -12,35 +12,161 @@
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA 10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215 10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA 10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
10010=\u5E33\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4 10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB 10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578 10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580 10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236 10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6 10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
10020=token\u4E0D\u80FD\u70BA\u7A7A 10020=token\u4E0D\u80FD\u70BA\u7A7A
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304 10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A 10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0} 10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis\u670D\u52D9\u7570\u5E38 10027=Redis\u670D\u52D9\u7570\u5E38
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557 10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D 10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210 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=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38 10032=\u522A\u9664\u672C\u6578\u64DA\u7570\u5E38
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4 10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A 10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A 10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
10036=\u4E0D\u652F\u63F4\u7684\u53C3\u6578\u985E\u578B 10036=\u4E0D\u652F\u6301\u7684\u53C3\u6578\u985E\u578B
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57 10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false 10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F 10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F 10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=\u8A2D\u5099\u672A\u627E\u5230 10041=\u8A2D\u5099\u672A\u627E\u5230
10042={0} 10042={0}
10043=\u522A\u9664\u6578\u64DA\u5931\u6557
10044=\u7528\u6236\u672A\u767B\u9304
10045=WebSocket\u9023\u63A5\u5931\u6557\u6216\u9023\u63A5\u8D85\u6642
10046=\u4FDD\u5B58\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10047=\u6BCF\u65E5\u767C\u9001\u6B21\u6578\u5DF2\u9054\u4E0A\u9650
10048=\u820A\u5BC6\u78BC\u8F38\u5165\u932F\u8AA4
10049=\u8A2D\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token\u751F\u6210\u5931\u6557
10051=\u8CC7\u6E90\u4E0D\u5B58\u5728
10052=\u9ED8\u8A8D\u667A\u80FD\u9AD4\u672A\u627E\u5230
10053=\u667A\u80FD\u9AD4\u672A\u627E\u5230
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=\u77ED\u4FE1\u767C\u9001\u5931\u6557
10056=\u77ED\u4FE1\u9023\u63A5\u5EFA\u7ACB\u5931\u6557
10057=\u667A\u80FD\u9AD4\u7684\u8072\u7D0B\u5275\u5EFA\u5931\u6557
10058=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u66F4\u65B0\u5931\u6557
10059=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u522A\u9664\u5931\u6557
10060=\u767C\u9001\u592A\u983B\u7E41\uFF0C\u8ACB{0}\u79D2\u5F8C\u518D\u8A66
10061=\u6FC0\u6D3B\u78BC\u4E0D\u80FD\u70BA\u7A7A
10062=\u6FC0\u6D3B\u78BC\u932F\u8AA4
10063=\u8A2D\u5099\u5DF2\u6FC0\u6D3B
10064=\u8A72\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B\uFF0C\u8ACB\u5148\u8A2D\u7F6E\u5176\u4ED6\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B
10065=\u65B0\u589E\u6578\u64DA\u5931\u6557
10066=\u66F4\u65B0\u6578\u64DA\u5931\u6557
10067=\u5716\u5F62\u9A57\u8B49\u78BC\u932F\u8AA4
10068=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A57\u8B49\u78BC\u529F\u80FD
10069=\u7528\u6236\u540D\u4E0D\u662F\u624B\u6A5F\u865F\u78BC\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165
10070=\u8A72\u624B\u6A5F\u865F\u78BC\u5DF2\u8A3B\u518A
10071=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u672A\u8A3B\u518A
10072=\u76EE\u524D\u4E0D\u5141\u8A31\u666E\u901A\u7528\u6236\u8A3B\u518A
10073=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u5BC6\u78BC\u627E\u56DE\u529F\u80FD
10074=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u683C\u5F0F\u4E0D\u6B63\u78BA
10075=\u8F38\u5165\u7684\u77ED\u4FE1\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10076=\u5B57\u5178\u985E\u578B\u4E0D\u5B58\u5728
10077=\u5B57\u5178\u985E\u578B\u7DE8\u78BC\u91CD\u8907
10078=\u8B80\u53D6\u8CC7\u6E90\u5931\u6557
10079=LLM\u6A21\u578B\u8207\u610F\u5716\u8B58\u5225\u3001\u53C3\u6578\u9078\u64C7\u4E0D\u5339\u914D
10080=\u6B64\u8072\u7D0B\u5C6C\u65BC\u5DF2\u8A3B\u518A\u7684{0}\uFF0C\u8ACB\u9078\u64C7\u5176\u4ED6\u8072\u97F3
10081=\u522A\u9664\u8072\u7D0B\u6642\u767C\u751F\u932F\u8AA4
10082=\u4E0D\u5141\u8A31\u4FEE\u6539\uFF0C\u6B64\u8072\u97F3\u5DF2\u8A3B\u518A\u70BA\u8072\u7D0B({0})
10083=\u4FEE\u6539\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
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=\u97F3\u983B\u6578\u64DA\u4E0D\u5C6C\u65BC\u6B64\u667A\u80FD\u9AD4
10086=\u97F3\u983B\u6578\u64DA\u70BA\u7A7A\uFF0C\u8ACB\u6AA2\u67E5\u4E0A\u50B3\u6578\u64DA
10087=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10088=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10089=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10090=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10091=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
10092=\u914D\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=\u6B64\u6A21\u578B\u914D\u7F6E\u88AB\u667A\u80FD\u9AD4{0}\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10094=\u6B64LLM\u6A21\u578B\u88AB\u610F\u5716\u8B58\u5225\u914D\u7F6E\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10095=\u7121\u6548\u7684\u670D\u52D9\u5668\u64CD\u4F5C
10096=\u672A\u914D\u7F6E\u670D\u52D9\u5668WebSocket\u5730\u5740
10097=\u76EE\u6A19WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u70BA\u7A7A
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA
10101=WebSocket\u9023\u63A5\u6E2C\u8A66\u5931\u6557
10102=OTA\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10105=OTA\u5730\u5740\u5FC5\u9808\u4EE5/ota/\u7D50\u5C3E
10106=OTA\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10108=OTA\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10109=MCP\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=\u4E0D\u662F\u6709\u6548\u7684MCP\u5730\u5740
10112=MCP\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10114=MCP\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10115=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10116=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=\u4E0D\u662F\u6709\u6548\u7684\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10118=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10119=\u8072\u7D0B\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10120=\u8072\u7D0B\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10121=\u8072\u7D0B\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10122=MQTT\u5BC6\u9470\u4E0D\u80FD\u70BA\u7A7A
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=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0CMQTT\u5BC6\u9470\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10125=\u60A8\u7684MQTT\u5BC6\u9470\u5305\u542B\u5F31\u5BC6\u78BC
10128=\u5B57\u5178\u6A19\u7C64\u91CD\u8907
10129=SM2\u5BC6\u9470\u672A\u914D\u7F6E
10130=SM2\u89E3\u5BC6\u5931\u6557
10131=modelType\u548CprovideCode\u4E0D\u80FD\u70BA\u7A7A
10132=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684\u804A\u5929\u8A18\u9304
10133=\u6703\u8A71ID\u4E0D\u80FD\u70BA\u7A7A
10134=\u667A\u80FD\u9AD4ID\u4E0D\u80FD\u70BA\u7A7A
10135=\u804A\u5929\u8A18\u9304\u4E0B\u8F09\u5931\u6557
10136=\u4E0B\u8F09\u93C8\u63A5\u5DF2\u904E\u671F\u6216\u7121\u6548
10137=\u4E0B\u8F09\u93C8\u63A5\u7121\u6548
10138=\u7528\u6236
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>
@@ -24,7 +24,6 @@ class loginControllerTest {
LoginDTO loginDTO = new LoginDTO(); LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("手机号码"); loginDTO.setUsername("手机号码");
loginDTO.setPassword("密码"); loginDTO.setPassword("密码");
loginDTO.setCaptcha("123456");
loginController.register(loginDTO); loginController.register(loginDTO);
} }
+1
View File
@@ -100,6 +100,7 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"pinia": "2.0.36", "pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1", "pinia-plugin-persistedstate": "3.2.1",
"sm-crypto": "^0.3.13",
"vue": "^3.4.21", "vue": "^3.4.21",
"wot-design-uni": "^1.9.1", "wot-design-uni": "^1.9.1",
"z-paging": "2.8.7" "z-paging": "2.8.7"
+55
View File
@@ -1,12 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app' import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { watch, onMounted } from 'vue'
import { usePageAuth } from '@/hooks/usePageAuth' import { usePageAuth } from '@/hooks/usePageAuth'
import { useConfigStore } from '@/store' import { useConfigStore } from '@/store'
import { t } from '@/i18n'
import { useLangStore } from '@/store/lang'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only' import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
usePageAuth() usePageAuth()
const configStore = useConfigStore() const configStore = useConfigStore()
const langStore = useLangStore()
onLaunch(() => { onLaunch(() => {
console.log('App Launch') console.log('App Launch')
@@ -17,7 +21,58 @@ onLaunch(() => {
}) })
onShow(() => { onShow(() => {
console.log('App Show') console.log('App Show')
// 使用setTimeout延迟执行,确保tabBar已经初始化
setTimeout(() => {
updateTabBarText()
}, 100)
}) })
// 动态更新tabBar文本
function updateTabBarText() {
try {
// 设置首页tabBar文本
uni.setTabBarItem({
index: 0,
text: t('tabBar.home'),
success: () => {},
fail: (err) => {
console.log('设置首页tabBar文本失败:', err)
}
})
// 设置配网tabBar文本
uni.setTabBarItem({
index: 1,
text: t('tabBar.deviceConfig'),
success: () => {},
fail: (err) => {
console.log('设置配网tabBar文本失败:', err)
}
})
// 设置系统tabBar文本
uni.setTabBarItem({
index: 2,
text: t('tabBar.settings'),
success: () => {},
fail: (err) => {
console.log('设置系统tabBar文本失败:', err)
}
})
} catch (error) {
console.log('更新tabBar文本时出错:', error)
}
}
// 监听语言切换事件
onMounted(() => {
// 监听语言变化,当语言改变时自动更新tabBar文本
watch(() => langStore.currentLang, () => {
console.log('语言已切换,更新tabBar文本')
// 语言切换后立即更新tabBar文本
updateTabBarText()
})
})
onHide(() => { onHide(() => {
console.log('App Hide') console.log('App Hide')
}) })
+19 -3
View File
@@ -4,7 +4,6 @@ import { http } from '@/http/request/alova'
export interface LoginData { export interface LoginData {
username: string username: string
password: string password: string
captcha: string
captchaId: string captchaId: string
areaCode?: string areaCode?: string
mobile?: string mobile?: string
@@ -68,6 +67,7 @@ export interface PublicConfig {
beianIcpNum: string beianIcpNum: string
beianGaNum: string beianGaNum: string
name: string name: string
sm2PublicKey: string
} }
// 获取用户信息 // 获取用户信息
@@ -94,8 +94,6 @@ export function getPublicConfig() {
export interface RegisterData { export interface RegisterData {
username: string username: string
password: string password: string
confirmPassword: string
captcha: string
captchaId: string captchaId: string
areaCode: string areaCode: string
mobile: string mobile: string
@@ -125,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,
},
})
}
+440
View File
@@ -0,0 +1,440 @@
// English language pack
export default {
// TabBar
'tabBar.home': 'Home',
'tabBar.deviceConfig': 'Network Config',
'tabBar.settings': 'System',
// Settings page title
'settings.title': 'Settings',
// Login page
'login.pageTitle': 'Login',
'login.navigationTitle': 'Login',
'login.fetchConfigError': 'Failed to fetch configuration:',
'login.selectLanguage': 'Select Language',
'login.selectLanguageTip': '中文',
'login.welcomeBack': 'Welcome Back',
'login.pleaseLogin': 'Please log in to your account',
'login.enterUsername': 'Please enter username',
'login.enterPassword': 'Please enter password',
'login.enterCaptcha': 'Please enter verification code',
'login.loginButton': 'Login',
'login.loggingIn': 'Logging in...',
'login.noAccount': 'Sign Up',
'login.enterPhone': 'Please enter phone number',
'login.selectCountry': 'Select Country/Region',
'login.confirm': 'Confirm',
'login.serverSetting': 'Server Settings',
'login.requiredUsername': 'Username cannot be empty',
'login.requiredPassword': 'Password cannot be empty',
'login.requiredCaptcha': 'Verification code cannot be empty',
'login.requiredMobile': 'Please enter a valid phone number',
'login.captchaError': 'Graphic verification code error',
'login.forgotPassword': 'Forgot Password',
// Register page
'register.pageTitle': 'Register',
'register.createAccount': 'Create Account',
'register.enterUsername': 'Please enter username',
'register.enterPassword': 'Please enter password',
'register.confirmPassword': 'Please confirm password',
'register.enterPhone': 'Please enter phone number',
'register.enterCode': 'Please enter verification code',
'register.getCode': 'Get Code',
'register.agreeTerms': 'I have read and agree to the',
'register.terms': 'User Agreement',
'register.privacy': 'Privacy Policy',
'register.registerButton': 'Register',
'register.registering': 'Registering...',
'register.haveAccount': 'Already have an account?',
'register.loginNow': 'Login Now',
'register.selectCountry': 'Select Country/Region',
'register.confirm': 'Confirm',
'register.captchaSendSuccess': 'Verification code sent successfully',
// Home page
'home.pageTitle': 'Home',
'home.createAgent': 'Create Agent',
'home.agentName': 'Agent',
'home.modelInfo': 'Model Info',
'home.lastActive': 'Last Active',
'home.greeting': 'Hi Jarvis',
'home.subtitle': 'Let\'s have',
'home.wonderfulDay': 'a wonderful day!',
'home.emptyState': 'No agents available',
'home.deviceManagement': 'Device Management',
'home.lastConversation': 'Last Conversation:',
'home.delete': 'Delete',
'home.createFirstAgent': 'Click the + button in the lower right corner to create your first agent',
'home.dialogTitle': 'Create Agent',
'home.inputPlaceholder': 'e.g. Customer Service Assistant, Voice Assistant, Knowledge Q&A',
'home.createError': 'Please input agent name',
'home.createNow': 'Create Now',
'home.justNow': 'Just now',
'home.minutesAgo': 'minutes ago',
'home.hoursAgo': 'hours ago',
'home.daysAgo': 'days ago',
// Agent page
'agent.pageTitle': 'Agent',
'agent.roleConfig': 'Role Configuration',
'agent.deviceManagement': 'Device Management',
'agent.chatHistory': 'Chat History',
'agent.voiceprintManagement': 'Voiceprint Management',
'agent.editTitle': 'Edit Agent',
'agent.toolsTitle': 'Edit Features',
'agent.voiceActivityDetection': 'Voice Activity Detection',
'agent.speechRecognition': 'Speech Recognition',
'agent.largeLanguageModel': 'Large Language Model',
'agent.save': 'Save',
'agent.cancel': 'Cancel',
// Agent Edit Page
'agent.basicInfo': 'Basic Information',
'agent.agentName': 'Agent Name',
'agent.inputAgentName': 'Please input agent name',
'agent.roleMode': 'Role Mode',
'agent.roleDescription': 'Role Description',
'agent.inputRoleDescription': 'Please input role description',
'agent.modelConfig': 'Model Configuration',
'agent.vad': 'Voice Activity Detection',
'agent.asr': 'Speech Recognition',
'agent.llm': 'Large Language Model',
'agent.vllm': 'Vision Language Model',
'agent.intent': 'Intent Recognition',
'agent.memory': 'Memory',
'agent.voiceSettings': 'Voice Settings',
'agent.tts': 'Text-to-Speech',
'agent.voiceprint': 'Agent Voice',
'agent.plugins': 'Plugins',
'agent.editFunctions': 'Edit Functions',
'agent.historyMemory': 'History Memory',
'agent.memoryContent': 'Memory content',
'agent.saving': 'Saving...',
'agent.saveSuccess': 'Save successful',
'agent.saveFail': 'Save failed',
'agent.loadFail': 'Load failed',
'agent.pleaseInputAgentName': 'Please input agent name',
'agent.pleaseInputRoleDescription': 'Please input role description',
'agent.pleaseSelect': 'Please select',
// Chat History Page
'chatHistory.getChatSessions': 'Get chat session list',
'chatHistory.noSelectedAgent': 'No agent selected',
'chatHistory.getChatSessionsFailed': 'Failed to get chat session list:',
'chatHistory.unknownTime': 'Unknown time',
'chatHistory.justNow': 'Just now',
'chatHistory.minutesAgo': '{minutes} minutes ago',
'chatHistory.hoursAgo': '{hours} hours ago',
'chatHistory.daysAgo': '{days} days ago',
'chatHistory.conversationRecord': 'Conversation Record',
'chatHistory.totalChats': '{count} chats in total',
'chatHistory.loading': 'Loading...',
'chatHistory.noMoreData': 'No more data',
'chatHistory.noChatRecords': 'No chat records',
'chatRecordsDescription': 'Conversation records with agents will be displayed here',
// Chat History Detail Page
'chatHistory.pageTitle': 'Chat Detail',
'chatHistory.assistantName': 'Intelligent Assistant',
'chatHistory.userName': 'User',
'chatHistory.aiAssistantName': 'AI Assistant',
'chatHistory.loadFailed': 'Failed to load chat history',
'chatHistory.parameterError': 'Page parameter error',
'chatHistory.invalidAudioId': 'Invalid audio ID',
'chatHistory.audioPlayFailed': 'Audio playback failed',
'chatHistory.playAudioFailed': 'Failed to play audio',
// Device management page
'device.pageTitle': 'Device Management',
'device.noDevices': 'No devices available',
'device.macAddress': 'MAC Address',
'device.firmwareVersion': 'Firmware Version',
'device.lastConnected': 'Last Conversation',
'device.otaUpdate': 'OTA Update',
'device.unbind': 'Unbind',
'device.confirmUnbind': 'Sure',
'device.bindDevice': 'Bind New Device',
'device.deviceType': 'Device Type',
'device.loading': 'Loading...',
'device.neverConnected': 'Never connected',
'device.justNow': 'Just now',
'device.minutesAgo': '{minutes} minutes ago',
'device.hoursAgo': '{hours} hours ago',
'device.daysAgo': '{days} days ago',
'device.otaAutoUpdateEnabled': 'OTA auto update enabled',
'device.otaAutoUpdateDisabled': 'OTA auto update disabled',
'device.operationFailed': 'Operation failed, please retry',
'device.deviceUnbound': 'Device unbound',
'device.unbindFailed': 'Unbind failed, please retry',
'device.unbindDevice': 'Unbind Device',
'device.confirmUnbindDevice': 'Are you sure you want to unbind device "{macAddress}"?',
'device.cancel': 'Cancel',
'device.noDevice': 'No Device',
'device.pleaseSelectAgent': 'Please select an agent first',
'device.deviceBindSuccess': 'Device bound successfully!',
'device.bindFailed': 'Bind failed, please check if the verification code is correct',
'device.enterDeviceCode': 'Please enter device verification code',
'device.bindNow': 'Bind Now',
'device.lastConnection': 'Last Connection',
'device.clickToBindFirstDevice': 'Click the + button in the lower right corner to bind your first device',
// Common
'common.success': 'Success',
'common.fail': 'Failed',
'common.loading': 'Loading...',
'common.confirm': 'Confirm',
'common.cancel': 'Cancel',
'common.delete': 'Delete',
'common.edit': 'Edit',
'common.add': 'Add',
'common.pleaseSelect': 'Please select',
'common.unknownError': 'Unknown 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.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
'sm2.encryptionFailed': 'Password encryption failed',
'sm2.keyGenerationFailed': 'Key pair generation failed',
'sm2.invalidPublicKey': 'Invalid public key format',
'sm2.encryptionError': 'Error occurred during encryption',
'sm2.publicKeyRetry': 'Retrying to get public key...',
'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed',
// Voiceprint page
'voiceprint.noSelectedAgent': 'No agent selected',
'voiceprint.pleaseSelectAgent': 'Please select an agent first',
'voiceprint.fetchHistoryFailed': 'Failed to fetch chat history',
'voiceprint.clickToSelectVector': 'Click to select voiceprint vector',
'voiceprint.pleaseInputName': 'Please input name',
'voiceprint.pleaseSelectVector': 'Please select voiceprint vector',
'voiceprint.addSuccess': 'Add success',
'voiceprint.addFailed': 'Failed to add speaker',
'voiceprint.editSuccess': 'Edit success',
'voiceprint.editFailed': 'Failed to edit speaker',
'voiceprint.deleteConfirmMsg': 'Are you sure to delete this speaker?',
'voiceprint.deleteConfirmTitle': 'Confirm Delete',
'voiceprint.deleteSuccess': 'Delete success',
'voiceprint.loading': 'Loading...',
'voiceprint.delete': 'Delete',
'voiceprint.emptyTitle': 'No voiceprint data',
'voiceprint.emptyDesc': 'Click the + button at the bottom right to add your first speaker',
'voiceprint.addSpeaker': 'Add Speaker',
'voiceprint.voiceVector': 'Voiceprint Vector',
'voiceprint.name': 'Name',
'voiceprint.description': 'Description',
'voiceprint.pleaseInputDescription': 'Please input description',
'voiceprint.cancel': 'Cancel',
'voiceprint.save': 'Save',
'voiceprint.editSpeaker': 'Edit Speaker',
'voiceprint.selectVector': 'Select Voiceprint Vector',
'voiceprint.voiceprintInterfaceNotConfigured': 'Voiceprint interface not configured',
// Settings page
'settings.pageTitle': 'Settings',
'settings.navigationTitle': 'Settings',
'settings.networkSettings': 'Network Settings',
'settings.serverApiUrl': 'Server API URL',
'settings.validServerUrl': 'Please enter a valid server address (starting with http or https and ending with /xiaozhi)',
'settings.saveSettings': 'Save Settings',
'settings.resetDefault': 'Reset Default',
'settings.restartApp': 'Restart App',
'settings.restartNow': 'Restart Now',
'settings.restartLater': 'Later',
// About us
'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 {version}',
'settings.restartSuccess': 'Saved, you can manually restart the app later',
'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared',
'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared',
'settings.resetSuccess': 'Reset successful',
'settings.enterServerUrl': 'Please enter server URL',
'settings.clearCacheFailed': 'Failed to clear cache',
'settings.cacheManagement': 'Cache Management',
'settings.totalCacheSize': 'Total Cache Size',
'settings.appDataSize': 'Total App Data Size',
'settings.cacheClear': 'Cache Clear',
'settings.clearAllCache': 'Clear all cache data',
'settings.clearCache': 'Clear Cache',
'settings.modifyWillClearCache': 'Modifications will clear cache',
'settings.appInfo': 'App Info',
'settings.aboutUs': 'About Us',
'settings.appVersion': 'App Version & Team Info',
'settings.confirmClear': 'Confirm Clear',
'settings.confirmClearMessage': 'Are you sure you want to clear all cache? This will delete all data including login status and require re-login.',
'settings.cacheCleared': 'Cache cleared successfully, redirecting to login page',
'settings.languageSettings': 'Language Settings',
'settings.language': 'Language',
'settings.selectLanguage': 'Select Language',
'settings.languageChanged': 'Language changed successfully',
// Messages
'message.loginSuccess': 'Login successful!',
'message.loginFail': 'Login failed',
'message.registerSuccess': 'Registration successful',
'message.registerFail': 'Registration failed',
'message.saveSuccess': 'Save successful',
'message.saveFail': 'Save failed',
'message.deleteSuccess': 'Delete successful',
'message.deleteFail': 'Delete failed',
'message.bindSuccess': 'Binding successful',
'message.bindFail': 'Binding failed',
'message.unbindSuccess': 'Unbinding successful',
'message.unbindFail': 'Unbinding failed',
'message.networkError': 'Network error, please check your connection',
'message.serverError': 'Server error, please try again later',
'message.invalidAddress': 'Invalid address, please check if the server is started or network connection is normal',
'message.languageChanged': 'Language changed',
'message.passwordError': 'Account or password error',
'message.phoneRegistered': 'This phone number has been registered',
// Agent Tools Page
'agent.tools.pageTitle': 'Agent Tools',
'agent.tools.unselected': 'Unselected',
'agent.tools.selected': 'Selected',
'agent.tools.noMorePlugins': 'No more plugins',
'agent.tools.pleaseSelectPlugin': 'Please select plugin function',
'agent.tools.builtInPlugins': 'Built-in Plugins',
'agent.tools.mcpAccessPoint': 'MCP Access Point',
'agent.tools.copy': 'Copy',
'agent.tools.noTools': 'No tools available',
'agent.tools.parameterConfig': 'Parameter Configuration',
'agent.tools.noParamsNeeded': 'No parameters needed',
'agent.tools.pleaseInput': 'Please input',
'agent.tools.inputOneItemPerLine': 'Input one item per line',
'agent.tools.pleaseInputValidJson': 'Please input valid JSON format',
'agent.tools.enableFunction': 'Enable Function',
'agent.tools.toggleFunction': 'Turn on or off this function',
'agent.tools.jsonFormatError': 'JSON format error',
'agent.tools.noMcpAddressToCopy': 'No MCP address to copy',
'agent.tools.mcpAddressCopied': 'MCP address copied to clipboard',
'agent.tools.copyFailed': 'Copy failed, please try again',
'agent.tools.defaultValue': 'Default value',
'agent.tools.notSelected': 'Unselected',
'agent.tools.clickToConfigure': 'Click to configure',
'agent.tools.mcpEndpoint': 'MCP Endpoint',
'agent.tools.eachLineOneItem': 'Input one item per line',
// Device Config page
'deviceConfig.pageTitle': 'Device Configuration',
'deviceConfig.wifiConfig': 'WiFi Configuration',
'deviceConfig.ultrasonicConfig': 'Ultrasonic Configuration',
'deviceConfig.selectConfigMethod': 'Select Configuration Method',
'deviceConfig.networkConfig': 'Network Configuration',
'deviceConfig.selectedNetwork': 'Selected Network',
'deviceConfig.signal': 'Signal',
'deviceConfig.openNetwork': 'Open Network',
'deviceConfig.encryptedNetwork': 'Encrypted Network',
'deviceConfig.password': 'Password',
'deviceConfig.pleaseEnterPassword': 'Please enter WiFi password',
'deviceConfig.startConfig': 'Start Configuration',
'deviceConfig.connectToXiaozhiHotspot': 'Please connect to xiaozhi hotspot first',
'deviceConfig.detecting': 'Detecting...',
'deviceConfig.reDetect': 'Re-detect',
'deviceConfig.alreadyConnected': 'Connected to xiaozhi hotspot',
'deviceConfig.refreshStatus': 'Refresh Status',
'deviceConfig.wifiNetworks': 'WiFi Networks',
'deviceConfig.selectWifiNetwork': 'Select WiFi Network',
'deviceConfig.refreshScan': 'Refresh Scan',
'deviceConfig.noWifiNetworks': 'No WiFi networks available',
'deviceConfig.clickToRefreshScan': 'Please click Refresh Scan',
'deviceConfig.signalStrong': 'Strong Signal',
'deviceConfig.signalGood': 'Good Signal',
'deviceConfig.signalFair': 'Fair Signal',
'deviceConfig.signalWeak': 'Weak Signal',
'deviceConfig.channel': 'Channel',
'deviceConfig.about': 'about',
'deviceConfig.seconds': 'seconds',
'deviceConfig.generating': 'Generating...',
'deviceConfig.playing': 'Playing...',
'deviceConfig.generateAndPlaySoundWave': 'Generate and Play Sound Wave',
'deviceConfig.playSoundWave': 'Play Sound Wave',
'deviceConfig.stopPlaying': 'Stop Playing',
'deviceConfig.autoLoopPlaySoundWave': 'Auto Loop Play Sound Wave',
'deviceConfig.configAudioFile': 'Configuration Audio File',
'deviceConfig.duration': 'Duration',
'deviceConfig.ultrasonicConfigInstructions': 'Ultrasonic Configuration Instructions',
'deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered': 'Ensure WiFi network is selected and password is entered',
'deviceConfig.clickGenerateAndPlaySoundWave': 'Click Generate and Play Sound Wave, the system will encode configuration information into audio',
'deviceConfig.bringPhoneCloseToXiaozhiDevice': 'Bring phone close to xiaozhi device (1-2 meters distance)',
'deviceConfig.duringAudioPlaybackXiaozhiWillReceive': 'During audio playback, xiaozhi will receive and decode configuration information',
'deviceConfig.afterConfigSuccessDeviceWillConnect': 'After successful configuration, the device will automatically connect to WiFi network',
'deviceConfig.usesAfskModulation': 'Uses AFSK modulation technology, transmitting data through 1800Hz and 1500Hz frequencies',
'deviceConfig.ensureModeratePhoneVolume': 'Please ensure phone volume is moderate to avoid environmental noise interference',
'deviceConfig.generatingUltrasonicConfigAudio': 'Generating ultrasonic configuration audio',
'deviceConfig.configData': 'Configuration data',
'deviceConfig.dataBytesLength': 'Data bytes length',
'deviceConfig.bitStreamLength': 'Bit stream length',
'deviceConfig.base64Length': 'Base64 length',
'deviceConfig.audioFileTooLarge': 'Audio file too large, please shorten SSID or password length',
'deviceConfig.audioGenerationSuccess': 'Audio generation successful',
'deviceConfig.samplePoints': 'Sample points',
'deviceConfig.soundWaveGenerationSuccess': 'Sound wave generation successful',
'deviceConfig.audioGenerationFailed': 'Audio generation failed',
'deviceConfig.soundWaveGenerationFailed': 'Sound wave generation failed',
'deviceConfig.pleaseGenerateAudioFirst': 'Please generate audio first',
'deviceConfig.startPlayingUltrasonicConfigAudio': 'Starting to play ultrasonic configuration audio',
'deviceConfig.ultrasonicAudioStartedPlaying': 'Ultrasonic audio started playing',
'deviceConfig.startPlayingConfigSoundWave': 'Started playing configuration sound wave',
'deviceConfig.ultrasonicAudioPlaybackEnded': 'Ultrasonic audio playback ended',
'deviceConfig.audioPlaybackFailed': 'Audio playback failed',
'deviceConfig.audioResourceBusy': 'Audio resource busy, please try again later',
'deviceConfig.audioFormatNotSupported': 'Audio format not supported, may be a data URI issue',
'deviceConfig.audioFileError': 'Audio file error',
'deviceConfig.cleaningUpAudioContext': 'Cleaning up audio context',
'deviceConfig.cleaningUpAudioContextFailed': 'Failed to clean up audio context',
'deviceConfig.stoppedPlayingUltrasonicAudio': 'Stopped playing ultrasonic audio',
'deviceConfig.stoppedPlaying': 'Stopped playing',
'deviceConfig.configMethod': 'Configuration Method',
'deviceConfig.enterWifiPassword': 'Please enter WiFi password',
'deviceConfig.xiaozhi': 'xiaozhi',
'deviceConfig.connectXiaozhiHotspot': 'Please connect to xiaozhi hotspot',
'deviceConfig.wifiScanResponse': 'WiFi scan response',
'deviceConfig.scanSuccess': 'Scan successful',
'deviceConfig.networks': 'networks',
'deviceConfig.wifiScanFailed': 'WiFi scan failed',
'deviceConfig.scanFailedCheckConnection': 'Scan failed, please check connection',
'deviceConfig.checking': 'Checking',
'deviceConfig.reCheck': 'Re-check',
'deviceConfig.connectedXiaozhiHotspot': 'Connected to xiaozhi hotspot',
'deviceConfig.wifiNetwork': 'WiFi Network',
'deviceConfig.scanning': 'Scanning',
'deviceConfig.cancel': 'Cancel',
'deviceConfig.clickRefreshScan': 'Please click Refresh Scan',
'deviceConfig.esp32ConnectionCheckFailed': 'ESP32 connection check failed',
'deviceConfig.startWifiConfig': 'Starting WiFi configuration',
'deviceConfig.configSuccess': 'Configuration successful',
'deviceConfig.deviceWillConnectTo': 'Device will connect to',
'deviceConfig.deviceWillRestart': 'Device will restart',
'deviceConfig.pleaseDisconnectXiaozhiHotspot': 'Please disconnect from xiaozhi hotspot',
'deviceConfig.configFailed': 'Configuration failed',
'deviceConfig.wifiConfigFailed': 'WiFi configuration failed',
'deviceConfig.pleaseCheckNetworkConnection': 'Please check network connection',
'deviceConfig.startWifiConfigButton': 'Start Configuration',
'deviceConfig.wifiConfigInstructions': 'WiFi Configuration Instructions',
'deviceConfig.phoneConnectXiaozhiHotspot': 'Phone connect to xiaozhi hotspot',
'deviceConfig.selectTargetWifiNetwork': 'Select target WiFi network',
'deviceConfig.enterWifiPasswordIfNeeded': 'Enter WiFi password if needed',
'deviceConfig.clickStartConfigAndWait': 'Click Start Configuration and wait',
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'After successful configuration, device will automatically restart',
'deviceConfig.audioPlaybackError': 'Audio playback error',
'deviceConfig.playbackFailed': 'Playback failed',
}
+70
View File
@@ -0,0 +1,70 @@
import { ref } from 'vue'
import { useLangStore } from '@/store/lang'
import type { Language } from '@/store/lang'
// 导入各个语言的翻译文件
import zhCN from './zh_CN'
import en from './en'
import zhTW from './zh_TW'
// 语言包映射
const messages = {
zh_CN: zhCN,
en,
zh_TW: zhTW,
}
// 当前使用的语言
const currentLang = ref<Language>('zh_CN')
// 初始化语言
export function initI18n() {
const langStore = useLangStore()
currentLang.value = langStore.currentLang
}
// 切换语言
export function changeLanguage(lang: Language) {
currentLang.value = lang
const langStore = useLangStore()
langStore.changeLang(lang)
}
// 获取翻译文本
export function t(key: string, params?: Record<string, string | number>): string {
const langMessages = messages[currentLang.value]
// 直接查找扁平键名
if (langMessages && typeof langMessages === 'object' && key in langMessages) {
let value = langMessages[key]
if (typeof value === 'string') {
// 处理参数替换
if (params) {
let result = value
Object.entries(params).forEach(([paramKey, paramValue]) => {
const regex = new RegExp(`\{${paramKey}\}`, 'g')
result = result.replace(regex, String(paramValue))
})
return result
}
return value
}
return key
}
return key // 如果找不到对应的翻译,返回key本身
}
// 获取当前语言
export function getCurrentLanguage(): Language {
return currentLang.value
}
// 获取支持的语言列表
export function getSupportedLanguages(): {code: Language, name: string}[] {
return [
{ code: 'zh_CN', name: '简体中文' },
{ code: 'en', name: 'English' },
{ code: 'zh_TW', name: '繁體中文' },
]
}
+440
View File
@@ -0,0 +1,440 @@
// 简体中文语言包
export default {
// TabBar
'tabBar.home': '首页',
'tabBar.deviceConfig': '配网',
'tabBar.settings': '系统',
// 设置页面标题
'settings.title': '设置',
// 登录页面
'login.pageTitle': '登录',
'login.navigationTitle': '登录',
'login.fetchConfigError': '获取配置失败:',
'login.selectLanguage': '选择语言',
'login.selectLanguageTip': 'En',
'login.welcomeBack': '欢迎回来',
'login.pleaseLogin': '请登录您的账户',
'login.enterUsername': '请输入用户名',
'login.enterPassword': '请输入密码',
'login.enterCaptcha': '请输入验证码',
'login.loginButton': '登录',
'login.loggingIn': '登录中...',
'login.noAccount': '新用户注册',
'login.enterPhone': '请输入手机号码',
'login.selectCountry': '选择国家/地区',
'login.confirm': '确认',
'login.serverSetting': '服务端设置',
'login.requiredUsername': '用户名不能为空',
'login.requiredPassword': '密码不能为空',
'login.requiredCaptcha': '验证码不能为空',
'login.requiredMobile': '请输入正确的手机号码',
'login.captchaError': '图形验证码错误',
'login.forgotPassword': '忘记密码',
// 注册页面
'register.pageTitle': '注册',
'register.createAccount': '创建账户',
'register.enterUsername': '请输入用户名',
'register.enterPassword': '请输入密码',
'register.confirmPassword': '请确认密码',
'register.enterPhone': '请输入手机号码',
'register.enterCode': '请输入验证码',
'register.getCode': '获取验证码',
'register.agreeTerms': '我已阅读并同意',
'register.terms': '《用户协议》',
'register.privacy': '《隐私政策》',
'register.registerButton': '注册',
'register.registering': '注册中...',
'register.haveAccount': '已有账户?',
'register.loginNow': '立即登录',
'register.selectCountry': '选择国家/地区',
'register.confirm': '确认',
'register.captchaSendSuccess': '验证码发送成功',
// 首页
'home.pageTitle': '首页',
'home.createAgent': '创建智能体',
'home.agentName': '智能体',
'home.modelInfo': '模型信息',
'home.lastActive': '最近活跃',
'home.greeting': '你好,小智',
'home.subtitle': '让我们度过',
'home.wonderfulDay': '美好的一天!',
'home.emptyState': '暂无智能体',
'home.deviceManagement': '设备管理',
'home.lastConversation': '最近对话:',
'home.delete': '删除',
'home.createFirstAgent': '点击右下角 + 号创建您的第一个智能体',
'home.dialogTitle': '创建智能体',
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
'home.createError': '请输入智能体名称',
'home.createNow': '立即创建',
'home.justNow': '刚刚',
'home.minutesAgo': '分钟前',
'home.hoursAgo': '小时前',
'home.daysAgo': '天前',
// Agent页面
'agent.pageTitle': '智能体',
'agent.roleConfig': '角色配置',
'agent.deviceManagement': '设备管理',
'agent.chatHistory': '聊天记录',
'agent.voiceprintManagement': '声纹管理',
'agent.editTitle': '编辑智能体',
'agent.toolsTitle': '编辑功能',
'agent.voiceActivityDetection': '语音活动检测',
'agent.speechRecognition': '语音识别',
'agent.largeLanguageModel': '大语言模型',
'agent.save': '保存',
'agent.cancel': '取消',
// Agent编辑页面
'agent.basicInfo': '基础信息',
'agent.agentName': '助手昵称',
'agent.inputAgentName': '请输入助手昵称',
'agent.roleMode': '角色模式',
'agent.roleDescription': '角色介绍',
'agent.inputRoleDescription': '请输入角色介绍',
'agent.modelConfig': '模型配置',
'agent.vad': '语音活动检测',
'agent.asr': '语音识别',
'agent.llm': '大语言模型',
'agent.vllm': '视觉大模型',
'agent.intent': '意图识别',
'agent.memory': '记忆',
'agent.voiceSettings': '语音设置',
'agent.tts': '语音合成',
'agent.voiceprint': '角色音色',
'agent.plugins': '插件',
'agent.editFunctions': '编辑功能',
'agent.historyMemory': '历史记忆',
'agent.memoryContent': '记忆内容',
'agent.saving': '保存中...',
'agent.saveSuccess': '保存成功',
'agent.saveFail': '保存失败',
'agent.loadFail': '加载失败',
'agent.pleaseInputAgentName': '请输入智能体名称',
'agent.pleaseInputRoleDescription': '请输入角色介绍',
'agent.pleaseSelect': '请选择',
// 聊天历史页面
'chatHistory.getChatSessions': '获取聊天会话列表',
'chatHistory.noSelectedAgent': '没有选中的智能体',
'chatHistory.getChatSessionsFailed': '获取聊天会话列表失败:',
'chatHistory.unknownTime': '未知时间',
'chatHistory.justNow': '刚刚',
'chatHistory.minutesAgo': '{minutes}分钟前',
'chatHistory.hoursAgo': '{hours}小时前',
'chatHistory.daysAgo': '{days}天前',
'chatHistory.conversationRecord': '对话记录',
'chatHistory.totalChats': '共 {count} 条对话',
'chatHistory.loading': '加载中...',
'chatHistory.noMoreData': '没有更多数据了',
'chatHistory.noChatRecords': '暂无聊天记录',
'chatHistory.chatRecordsDescription': '与智能体的对话记录会显示在这里',
// 聊天详情页面
'chatHistory.pageTitle': '聊天详情',
'chatHistory.assistantName': '智能助手',
'chatHistory.userName': '用户',
'chatHistory.aiAssistantName': 'AI助手',
'chatHistory.loadFailed': '获取聊天记录失败',
'chatHistory.parameterError': '页面参数错误',
'chatHistory.invalidAudioId': '音频ID无效',
'chatHistory.audioPlayFailed': '音频播放失败',
'chatHistory.playAudioFailed': '播放音频失败',
// 设备管理页面
'device.pageTitle': '设备管理',
'device.noDevices': '暂无设备',
'device.macAddress': 'MAC地址',
'device.firmwareVersion': '固件版本',
'device.lastConnected': '最近对话',
'device.otaUpdate': 'OTA升级',
'device.unbind': '解绑',
'device.confirmUnbind': '确定要解绑设备',
'device.bindDevice': '绑定新设备',
'device.deviceType': '设备类型',
'device.loading': '加载中...',
'device.neverConnected': '从未连接',
'device.justNow': '刚刚',
'device.minutesAgo': '{minutes}分钟前',
'device.hoursAgo': '{hours}小时前',
'device.daysAgo': '{days}天前',
'device.otaAutoUpdateEnabled': 'OTA自动升级已开启',
'device.otaAutoUpdateDisabled': 'OTA自动升级已关闭',
'device.operationFailed': '操作失败,请重试',
'device.deviceUnbound': '设备已解绑',
'device.unbindFailed': '解绑失败,请重试',
'device.unbindDevice': '解绑设备',
'device.confirmUnbindDevice': '确定要解绑设备 "{macAddress}" 吗?',
'device.cancel': '取消',
'device.noDevice': '暂无设备',
'device.pleaseSelectAgent': '请先选择智能体',
'device.deviceBindSuccess': '设备绑定成功!',
'device.bindFailed': '绑定失败,请检查验证码是否正确',
'device.enterDeviceCode': '请输入设备验证码',
'device.bindNow': '立即绑定',
'device.lastConnection': '最近对话',
'device.clickToBindFirstDevice': '点击右下角 + 号绑定您的第一个设备',
// 通用
'common.success': '成功',
'common.fail': '失败',
'common.loading': '加载中...',
'common.confirm': '确认',
'common.cancel': '取消',
'common.delete': '删除',
'common.edit': '编辑',
'common.add': '添加',
'common.pleaseSelect': '请选择',
'common.unknownError': '未知错误',
'common.networkError': '网络错误',
// SM2加密相关错误消息
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
'sm2.encryptionFailed': '密码加密失败',
'sm2.keyGenerationFailed': '密钥对生成失败',
'sm2.invalidPublicKey': '无效的公钥格式',
'sm2.encryptionError': '加密过程中发生错误',
'sm2.publicKeyRetry': '正在重试获取公钥...',
'sm2.publicKeyRetryFailed': '公钥获取重试失败',
// Voiceprint page
'voiceprint.noSelectedAgent': '没有选中的智能体',
'voiceprint.pleaseSelectAgent': '请先选择智能体',
'voiceprint.fetchHistoryFailed': '获取对话记录失败',
'voiceprint.clickToSelectVector': '点击选择声纹向量',
'voiceprint.pleaseInputName': '请输入姓名',
'voiceprint.pleaseSelectVector': '请选择声纹向量',
'voiceprint.addSuccess': '添加成功',
'voiceprint.addFailed': '添加说话人失败',
'voiceprint.editSuccess': '编辑成功',
'voiceprint.editFailed': '编辑说话人失败',
'voiceprint.deleteConfirmMsg': '确定要删除这个说话人吗?',
'voiceprint.deleteConfirmTitle': '确认删除',
'voiceprint.deleteSuccess': '删除成功',
'voiceprint.loading': '加载中...',
'voiceprint.delete': '删除',
'voiceprint.emptyTitle': '暂无声纹数据',
'voiceprint.emptyDesc': '点击右下角 + 号添加您的第一个说话人',
'voiceprint.addSpeaker': '添加说话人',
'voiceprint.voiceVector': '声纹向量',
'voiceprint.name': '姓名',
'voiceprint.description': '描述',
'voiceprint.pleaseInputDescription': '请输入描述',
'voiceprint.cancel': '取消',
'voiceprint.save': '保存',
'voiceprint.editSpeaker': '编辑说话人',
'voiceprint.selectVector': '选择声纹向量',
'voiceprint.voiceprintInterfaceNotConfigured': '声纹接口未配置',
// 设置页面
'settings.pageTitle': '设置',
'settings.navigationTitle': '设置',
'settings.networkSettings': '网络设置',
'settings.serverApiUrl': '服务端接口地址',
'settings.validServerUrl': '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)',
'settings.saveSettings': '保存设置',
'settings.resetDefault': '恢复默认',
'settings.restartApp': '重启应用',
'settings.restartNow': '立即重启',
'settings.restartLater': '稍后',
// 关于我们
'settings.aboutApp': '关于小智智控台',
'settings.aboutContent': '小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': '已保存,可稍后手动重启应用',
'settings.serverUrlSavedAndCacheCleared': '服务端地址已保存,缓存已清除',
'settings.resetToDefaultAndCacheCleared': '已恢复默认设置,缓存已清除',
'settings.resetSuccess': '重置成功',
'settings.enterServerUrl': '请输入服务端地址',
'settings.clearCacheFailed': '清除缓存失败',
'settings.cacheManagement': '缓存管理',
'settings.totalCacheSize': '总缓存大小',
'settings.appDataSize': '应用数据总大小',
'settings.cacheClear': '缓存清理',
'settings.clearAllCache': '清空所有缓存数据',
'settings.clearCache': '清除缓存',
'settings.modifyWillClearCache': '修改将清除缓存',
'settings.appInfo': '应用信息',
'settings.aboutUs': '关于我们',
'settings.appVersion': '应用版本与团队信息',
'settings.confirmClear': '确认清除',
'settings.confirmClearMessage': '确定要清除所有缓存吗?这将删除所有数据包括登录状态,需要重新登录。',
'settings.cacheCleared': '缓存清除成功,即将跳转到登录页',
'settings.languageSettings': '语言设置',
'settings.language': '语言',
'settings.selectLanguage': '选择语言',
'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.loginFail': '登录失败',
'message.registerSuccess': '注册成功',
'message.registerFail': '注册失败',
'message.saveSuccess': '保存成功',
'message.saveFail': '保存失败',
'message.deleteSuccess': '删除成功',
'message.deleteFail': '删除失败',
'message.bindSuccess': '绑定成功',
'message.bindFail': '绑定失败',
'message.unbindSuccess': '解绑成功',
'message.unbindFail': '解绑失败',
'message.networkError': '网络错误,请检查网络连接',
'message.serverError': '服务器错误,请稍后再试',
'message.invalidAddress': '无效地址,请检查服务端是否启动或网络连接是否正常',
'message.languageChanged': '语言已切换',
'message.passwordError': '账号或密码错误',
'message.phoneRegistered': '此手机号码已经注册过',
// Agent工具页面
'agent.tools.pageTitle': 'Agent工具',
'agent.tools.unselected': '未选',
'agent.tools.selected': '已选',
'agent.tools.noMorePlugins': '暂无更多插件',
'agent.tools.pleaseSelectPlugin': '请选择插件功能',
'agent.tools.builtInPlugins': '内置插件',
'agent.tools.mcpAccessPoint': 'mcp接入点',
'agent.tools.copy': '复制',
'agent.tools.noTools': '暂无工具',
'agent.tools.parameterConfig': '参数配置',
'agent.tools.noParamsNeeded': '无需配置参数',
'agent.tools.pleaseInput': '请输入',
'agent.tools.inputOneItemPerLine': '每行输入一个项目',
'agent.tools.pleaseInputValidJson': '请输入有效的JSON格式',
'agent.tools.enableFunction': '启用功能',
'agent.tools.toggleFunction': '开启或关闭此功能',
'agent.tools.jsonFormatError': 'JSON格式错误',
'agent.tools.noMcpAddressToCopy': '暂无MCP地址可复制',
'agent.tools.mcpAddressCopied': 'MCP地址已复制到剪贴板',
'agent.tools.copyFailed': '复制失败,请重试',
'agent.tools.defaultValue': '默认值',
'agent.tools.notSelected': '未选',
'agent.tools.clickToConfigure': '点击配置',
'agent.tools.mcpEndpoint': 'MCP接入点',
'agent.tools.eachLineOneItem': '每行输入一个项目',
// 设备配置页面
'deviceConfig.pageTitle': '设备配置',
'deviceConfig.wifiConfig': 'WiFi配网',
'deviceConfig.ultrasonicConfig': '超声波配网',
'deviceConfig.selectConfigMethod': '选择配网方式',
'deviceConfig.networkConfig': '网络配置',
'deviceConfig.selectedNetwork': '选中网络',
'deviceConfig.signal': '信号',
'deviceConfig.openNetwork': '开放网络',
'deviceConfig.encryptedNetwork': '加密网络',
'deviceConfig.password': '密码',
'deviceConfig.pleaseEnterPassword': '请输入WiFi密码',
'deviceConfig.startConfig': '开始配网',
'deviceConfig.connectToXiaozhiHotspot': '请先连接xiaozhi热点 (xiaozhi-XXXXXX)',
'deviceConfig.detecting': '检测中...',
'deviceConfig.reDetect': '重新检测',
'deviceConfig.alreadyConnected': '已连接xiaozhi热点',
'deviceConfig.refreshStatus': '刷新状态',
'deviceConfig.wifiNetworks': 'WiFi网络',
'deviceConfig.selectWifiNetwork': '选择WiFi网络',
'deviceConfig.refreshScan': '刷新扫描',
'deviceConfig.noWifiNetworks': '暂无WiFi网络',
'deviceConfig.clickToRefreshScan': '请点击刷新扫描',
'deviceConfig.signalStrong': '信号强',
'deviceConfig.signalGood': '信号良好',
'deviceConfig.signalFair': '信号一般',
'deviceConfig.signalWeak': '信号弱',
'deviceConfig.channel': '频道',
'deviceConfig.about': '约',
'deviceConfig.seconds': '秒',
'deviceConfig.generating': '生成中...',
'deviceConfig.playing': '播放中...',
'deviceConfig.generateAndPlaySoundWave': '生成并播放声波',
'deviceConfig.playSoundWave': '播放声波',
'deviceConfig.stopPlaying': '停止播放',
'deviceConfig.autoLoopPlaySoundWave': '自动循环播放声波',
'deviceConfig.configAudioFile': '配网音频文件',
'deviceConfig.duration': '时长',
'deviceConfig.ultrasonicConfigInstructions': '超声波配网说明',
'deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered': '确保已选择WiFi网络并输入密码',
'deviceConfig.clickGenerateAndPlaySoundWave': '点击生成并播放声波,系统会将配网信息编码为音频',
'deviceConfig.bringPhoneCloseToXiaozhiDevice': '将手机靠近xiaozhi设备(距离1-2米)',
'deviceConfig.duringAudioPlaybackXiaozhiWillReceive': '音频播放时,xiaozhi会接收并解码配网信息',
'deviceConfig.afterConfigSuccessDeviceWillConnect': '配网成功后设备会自动连接WiFi网络',
'deviceConfig.usesAfskModulation': '使用AFSK调制技术,通过1800Hz和1500Hz频率传输数据',
'deviceConfig.ensureModeratePhoneVolume': '请确保手机音量适中,避免环境噪音干扰',
'deviceConfig.generatingUltrasonicConfigAudio': '生成超声波配网音频',
'deviceConfig.configData': '配网数据',
'deviceConfig.dataBytesLength': '数据字节长度',
'deviceConfig.bitStreamLength': '比特流长度',
'deviceConfig.base64Length': 'base64长度',
'deviceConfig.audioFileTooLarge': '音频文件过大,请缩短SSID或密码长度',
'deviceConfig.audioGenerationSuccess': '音频生成成功',
'deviceConfig.samplePoints': '采样点数',
'deviceConfig.soundWaveGenerationSuccess': '声波生成成功',
'deviceConfig.audioGenerationFailed': '音频生成失败',
'deviceConfig.soundWaveGenerationFailed': '声波生成失败',
'deviceConfig.pleaseGenerateAudioFirst': '请先生成音频',
'deviceConfig.startPlayingUltrasonicConfigAudio': '开始播放超声波配网音频',
'deviceConfig.ultrasonicAudioStartedPlaying': '超声波音频开始播放',
'deviceConfig.startPlayingConfigSoundWave': '开始播放配网声波',
'deviceConfig.ultrasonicAudioPlaybackEnded': '超声波音频播放结束',
'deviceConfig.audioPlaybackFailed': '音频播放失败',
'deviceConfig.audioResourceBusy': '音频资源繁忙,请稍后重试',
'deviceConfig.audioFormatNotSupported': '音频格式不支持,可能是data URI问题',
'deviceConfig.audioFileError': '音频文件错误',
'deviceConfig.cleaningUpAudioContext': '清理音频上下文',
'deviceConfig.cleaningUpAudioContextFailed': '清理音频上下文失败',
'deviceConfig.stoppedPlayingUltrasonicAudio': '停止播放超声波音频',
'deviceConfig.stoppedPlaying': '已停止播放',
'deviceConfig.configMethod': '配网方式',
'deviceConfig.enterWifiPassword': '请输入WiFi密码',
'deviceConfig.xiaozhi': 'xiaozhi',
'deviceConfig.connectXiaozhiHotspot': '请连接xiaozhi热点',
'deviceConfig.wifiScanResponse': 'WiFi扫描响应',
'deviceConfig.scanSuccess': '扫描成功',
'deviceConfig.networks': '个网络',
'deviceConfig.wifiScanFailed': 'WiFi扫描失败',
'deviceConfig.scanFailedCheckConnection': '扫描失败,请检查连接',
'deviceConfig.checking': '检查中',
'deviceConfig.reCheck': '重新检查',
'deviceConfig.connectedXiaozhiHotspot': '已连接xiaozhi热点',
'deviceConfig.wifiNetwork': 'WiFi网络',
'deviceConfig.scanning': '扫描中',
'deviceConfig.cancel': '取消',
'deviceConfig.clickRefreshScan': '请点击刷新扫描',
'deviceConfig.esp32ConnectionCheckFailed': 'ESP32连接检查失败',
'deviceConfig.startWifiConfig': '开始WiFi配网',
'deviceConfig.configSuccess': '配网成功',
'deviceConfig.deviceWillConnectTo': '设备将连接到',
'deviceConfig.deviceWillRestart': '设备将重启',
'deviceConfig.pleaseDisconnectXiaozhiHotspot': '请断开xiaozhi热点连接',
'deviceConfig.configFailed': '配网失败',
'deviceConfig.wifiConfigFailed': 'WiFi配网失败',
'deviceConfig.pleaseCheckNetworkConnection': '请检查网络连接',
'deviceConfig.startWifiConfigButton': '开始配网',
'deviceConfig.wifiConfigInstructions': 'WiFi配网说明',
'deviceConfig.phoneConnectXiaozhiHotspot': '手机连接xiaozhi热点',
'deviceConfig.selectTargetWifiNetwork': '选择目标WiFi网络',
'deviceConfig.enterWifiPasswordIfNeeded': '如有需要请输入WiFi密码',
'deviceConfig.clickStartConfigAndWait': '点击开始配网并等待',
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配网成功后设备将自动重启',
'deviceConfig.audioPlaybackError': '音频播放错误',
'deviceConfig.playbackFailed': '播放失败',
}
+444
View File
@@ -0,0 +1,444 @@
// 繁體中文語言包
export default {
// TabBar
'tabBar.home': '首頁',
'tabBar.deviceConfig': '配網',
'tabBar.settings': '系統',
// 設置頁面標題
'settings.title': '設置',
// 登錄頁面
'login.pageTitle': '登錄',
'login.navigationTitle': '登錄',
'login.fetchConfigError': '獲取配置失敗:',
'login.selectLanguage': '選擇語言',
'login.selectLanguageTip': '简体',
'login.welcomeBack': '歡迎回來',
'login.pleaseLogin': '請登錄您的賬戶',
'login.enterUsername': '請輸入用戶名',
'login.enterPassword': '請輸入密碼',
'login.enterCaptcha': '請輸入驗證碼',
'login.loginButton': '登錄',
'login.loggingIn': '登錄中...',
'login.noAccount': '新用戶註冊',
'login.enterPhone': '請輸入手機號碼',
'login.selectCountry': '選擇國家/地區',
'login.confirm': '確認',
'login.serverSetting': '服務端設置',
'login.requiredUsername': '用戶名不能為空',
'login.requiredPassword': '密碼不能為空',
'login.requiredCaptcha': '驗證碼不能為空',
'login.requiredMobile': '請輸入正確的手機號碼',
'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.createAccount': '創建賬戶',
'register.enterUsername': '請輸入用戶名',
'register.enterPassword': '請輸入密碼',
'register.confirmPassword': '請確認密碼',
'register.enterPhone': '請輸入手機號碼',
'register.enterCode': '請輸入驗證碼',
'register.getCode': '獲取驗證碼',
'register.agreeTerms': '我已閱讀並同意',
'register.terms': '《用戶協議》',
'register.privacy': '《隱私政策》',
'register.registerButton': '註冊',
'register.registering': '註冊中...',
'register.haveAccount': '已有賬戶?',
'register.loginNow': '立即登錄',
'register.selectCountry': '選擇國家/地區',
'register.confirm': '確認',
'register.captchaSendSuccess': '驗證碼發送成功',
// 首頁
'home.pageTitle': '首頁',
'home.createAgent': '創建智能體',
'home.agentName': '智能體',
'home.modelInfo': '模型信息',
'home.lastActive': '最近活躍',
'home.greeting': '你好,小智',
'home.subtitle': '讓我們度過',
'home.wonderfulDay': '美好的一天!',
'home.emptyState': '暫無智能體',
'home.deviceManagement': '設備管理',
'home.lastConversation': '最近對話:',
'home.delete': '刪除',
'home.createFirstAgent': '點擊右下角 + 號創建您的第一個智能體',
'home.dialogTitle': '創建智能體',
'home.inputPlaceholder': '例如:客服助手、語音助理、知識問答',
'home.createError': '請輸入智能體暱稱',
'home.createNow': '立即創建',
'home.justNow': '剛剛',
'home.minutesAgo': '分鐘前',
'home.hoursAgo': '小時前',
'home.daysAgo': '天前',
// Agent頁面
'agent.pageTitle': '智能體',
'agent.roleConfig': '角色配置',
'agent.deviceManagement': '設備管理',
'agent.chatHistory': '聊天記錄',
'agent.voiceprintManagement': '聲紋管理',
'agent.editTitle': '編輯智能體',
'agent.toolsTitle': '編輯功能',
'agent.voiceActivityDetection': '語音活動檢測',
'agent.speechRecognition': '語音識別',
'agent.largeLanguageModel': '大語言模型',
'agent.save': '保存',
'agent.cancel': '取消',
// Agent編輯頁面
'agent.basicInfo': '基礎資訊',
'agent.agentName': '助手暱稱',
'agent.inputAgentName': '請輸入助手暱稱',
'agent.roleMode': '角色模式',
'agent.roleDescription': '角色介紹',
'agent.inputRoleDescription': '請輸入角色介紹',
'agent.modelConfig': '模型配置',
'agent.vad': '語音活動檢測',
'agent.asr': '語音識別',
'agent.llm': '大語言模型',
'agent.vllm': '視覺大模型',
'agent.intent': '意圖識別',
'agent.memory': '記憶',
'agent.voiceSettings': '語音設置',
'agent.tts': '語音合成',
'agent.voiceprint': '角色音色',
'agent.plugins': '外掛',
'agent.editFunctions': '編輯功能',
'agent.historyMemory': '歷史記憶',
'agent.memoryContent': '記憶內容',
'agent.saving': '儲存中...',
'agent.saveSuccess': '儲存成功',
'agent.saveFail': '儲存失敗',
'agent.loadFail': '加載失敗',
'agent.pleaseInputAgentName': '請輸入助手暱稱',
'agent.pleaseInputRoleDescription': '請輸入角色介紹',
'agent.pleaseSelect': '請選擇',
// 聊天歷史頁面
'chatHistory.getChatSessions': '獲取聊天會話列表',
'chatHistory.noSelectedAgent': '沒有選中的智能體',
'chatHistory.getChatSessionsFailed': '獲取聊天會話列表失敗:',
'chatHistory.unknownTime': '未知時間',
'chatHistory.justNow': '剛剛',
'chatHistory.minutesAgo': '{minutes}分鐘前',
'chatHistory.hoursAgo': '{hours}小時前',
'chatHistory.daysAgo': '{days}天前',
'chatHistory.conversationRecord': '對話記錄',
'chatHistory.totalChats': '共 {count} 條對話',
'chatHistory.loading': '加載中...',
'chatHistory.noMoreData': '沒有更多數據了',
'chatHistory.noChatRecords': '暫無聊天記錄',
'chatHistory.chatRecordsDescription': '與智能體的對話記錄會顯示在這裡',
// 聊天詳情頁面
'chatHistory.pageTitle': '聊天詳情',
'chatHistory.assistantName': '智能助手',
'chatHistory.userName': '用戶',
'chatHistory.aiAssistantName': 'AI助手',
'chatHistory.loadFailed': '獲取聊天記錄失敗',
'chatHistory.parameterError': '頁面參數錯誤',
'chatHistory.invalidAudioId': '音頻ID無效',
'chatHistory.audioPlayFailed': '音頻播放失敗',
'chatHistory.playAudioFailed': '播放音頻失敗',
// 設備管理頁面
'device.pageTitle': '設備管理',
'device.noDevices': '暫無設備',
'device.macAddress': 'MAC地址',
'device.firmwareVersion': '固件版本',
'device.lastConnected': '最近對話',
'device.otaUpdate': 'OTA升級',
'device.unbind': '解除綁定',
'device.confirmUnbind': '確定要解綁設備',
'device.bindDevice': '綁定新設備',
'device.deviceType': '設備類型',
'device.loading': '加載中...',
'device.neverConnected': '從未連接',
'device.justNow': '剛剛',
'device.minutesAgo': '{minutes}分鐘前',
'device.hoursAgo': '{hours}小時前',
'device.daysAgo': '{days}天前',
'device.otaAutoUpdateEnabled': 'OTA自動升級已開啟',
'device.otaAutoUpdateDisabled': 'OTA自動升級已關閉',
'device.operationFailed': '操作失敗,請重試',
'device.deviceUnbound': '設備已解除綁定',
'device.unbindFailed': '解除綁定失敗,請重試',
'device.unbindDevice': '解除綁定設備',
'device.confirmUnbindDevice': '確定要解除綁定設備 "{macAddress}" 嗎?',
'device.cancel': '取消',
'device.noDevice': '暫無設備',
'device.pleaseSelectAgent': '請先選擇智能體',
'device.deviceBindSuccess': '設備綁定成功!',
'device.bindFailed': '綁定失敗,請檢查驗證碼是否正確',
'device.enterDeviceCode': '請輸入設備驗證碼',
'device.bindNow': '立即綁定',
'device.lastConnection': '最近對話',
'device.clickToBindFirstDevice': '點擊右下角 + 號綁定您的第一個設備',
// 通用
'common.success': '成功',
'common.fail': '失敗',
'common.loading': '加載中...',
'common.confirm': '確認',
'common.cancel': '取消',
'common.delete': '刪除',
'common.edit': '編輯',
'common.add': '添加',
'common.pleaseSelect': '請選擇',
'common.unknownError': '未知錯誤',
'common.networkError': '網路錯誤',
// SM2加密相關錯誤消息
'sm2.publicKeyNotConfigured': 'SM2公鑰未配置,請聯繫管理員',
'sm2.encryptionFailed': '密碼加密失敗',
'sm2.keyGenerationFailed': '金鑰對生成失敗',
'sm2.invalidPublicKey': '無效的公鑰格式',
'sm2.encryptionError': '加密過程中發生錯誤',
'sm2.publicKeyRetry': '正在重試獲取公鑰...',
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗',
// Voiceprint page
'voiceprint.noSelectedAgent': '沒有選中的智能體',
'voiceprint.pleaseSelectAgent': '請先選擇智能體',
'voiceprint.fetchHistoryFailed': '獲取對話記錄失敗',
'voiceprint.clickToSelectVector': '點擊選擇聲紋向量',
'voiceprint.pleaseInputName': '請輸入姓名',
'voiceprint.pleaseSelectVector': '請選擇聲紋向量',
'voiceprint.addSuccess': '添加成功',
'voiceprint.addFailed': '添加說話人失敗',
'voiceprint.editSuccess': '編輯成功',
'voiceprint.editFailed': '編輯說話人失敗',
'voiceprint.deleteConfirmMsg': '確定要刪除這個說話人嗎?',
'voiceprint.deleteConfirmTitle': '確認刪除',
'voiceprint.deleteSuccess': '刪除成功',
'voiceprint.loading': '加載中...',
'voiceprint.delete': '刪除',
'voiceprint.emptyTitle': '暫無聲紋數據',
'voiceprint.emptyDesc': '點擊右下角 + 號添加您的第一個說話人',
'voiceprint.addSpeaker': '添加說話人',
'voiceprint.voiceVector': '聲紋向量',
'voiceprint.name': '姓名',
'voiceprint.description': '描述',
'voiceprint.pleaseInputDescription': '請輸入描述',
'voiceprint.cancel': '取消',
'voiceprint.save': '保存',
'voiceprint.editSpeaker': '編輯說話人',
'voiceprint.selectVector': '選擇聲紋向量',
'voiceprint.voiceprintInterfaceNotConfigured': '聲紋接口未配置',
// 設置頁面
'settings.pageTitle': '設置',
'settings.navigationTitle': '設置',
'settings.networkSettings': '網絡設置',
'settings.serverApiUrl': '服務端接口地址',
'settings.validServerUrl': '請輸入有效的服務端地址(以 http 或 https 開頭,並以 /xiaozhi 結尾)',
'settings.saveSettings': '保存設置',
'settings.resetDefault': '恢復默認',
'settings.restartApp': '重啟應用',
'settings.restartNow': '立即重啟',
'settings.restartLater': '稍後',
// 關於我們
'settings.aboutApp': '關於小智智控台',
'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': '已保存,可稍後手動重啟應用',
'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除',
'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除',
'settings.resetSuccess': '重置成功',
'settings.enterServerUrl': '請輸入服務端地址',
'settings.clearCacheFailed': '清除緩存失敗',
'settings.cacheManagement': '緩存管理',
'settings.totalCacheSize': '總緩存大小',
'settings.appDataSize': '應用數據總大小',
'settings.cacheClear': '緩存清理',
'settings.clearAllCache': '清空所有緩存數據',
'settings.clearCache': '清除緩存',
'settings.modifyWillClearCache': '修改將清除緩存',
'settings.appInfo': '應用信息',
'settings.aboutUs': '關於我們',
'settings.appVersion': '應用版本與團隊信息',
'settings.confirmClear': '確認清除',
'settings.confirmClearMessage': '確定要清除所有緩存嗎?這將刪除所有數據包括登錄狀態,需要重新登錄。',
'settings.cacheCleared': '緩存清除成功,即將跳轉到登錄頁',
'settings.languageSettings': '語言設置',
'settings.language': '語言',
'settings.selectLanguage': '選擇語言',
'settings.languageChanged': '語言切換成功',
// 消息提示
'message.loginSuccess': '登錄成功!',
'message.loginFail': '登錄失敗',
'message.registerSuccess': '註冊成功',
'message.registerFail': '註冊失敗',
'message.saveSuccess': '保存成功',
'message.saveFail': '保存失敗',
'message.deleteSuccess': '刪除成功',
'message.deleteFail': '刪除失敗',
'message.bindSuccess': '綁定成功',
'message.bindFail': '綁定失敗',
'message.unbindSuccess': '解除綁定成功',
'message.unbindFail': '解除綁定失敗',
'message.networkError': '網絡錯誤,請檢查網絡連接',
'message.serverError': '服務器錯誤,請稍後再試',
'message.invalidAddress': '無效地址,請檢查服務端是否啟動或網絡連接是否正常',
'message.languageChanged': '語言已切換',
'message.passwordError': '帳號或密碼錯誤',
'message.phoneRegistered': '此手機號已經註冊過',
// Agent工具頁面
'agent.tools.pageTitle': 'Agent工具',
'agent.tools.unselected': '未選',
'agent.tools.selected': '已選',
'agent.tools.noMorePlugins': '暫無更多插件',
'agent.tools.pleaseSelectPlugin': '請選擇插件功能',
'agent.tools.builtInPlugins': '內置插件',
'agent.tools.mcpAccessPoint': 'mcp接入點',
'agent.tools.copy': '複製',
'agent.tools.noTools': '暫無工具',
'agent.tools.parameterConfig': '參數配置',
'agent.tools.noParamsNeeded': '無需配置參數',
'agent.tools.pleaseInput': '請輸入',
'agent.tools.inputOneItemPerLine': '每行輸入一個項目',
'agent.tools.pleaseInputValidJson': '請輸入有效的JSON格式',
'agent.tools.enableFunction': '啟用功能',
'agent.tools.toggleFunction': '開啟或關閉此功能',
'agent.tools.jsonFormatError': 'JSON格式錯誤',
'agent.tools.noMcpAddressToCopy': '暫無MCP地址可複製',
'agent.tools.mcpAddressCopied': 'MCP地址已複製到剪貼簿',
'agent.tools.copyFailed': '複製失敗,請重試',
'agent.tools.defaultValue': '默認值',
'agent.tools.notSelected': '未選',
'agent.tools.clickToConfigure': '點擊配置',
'agent.tools.mcpEndpoint': 'MCP接入點',
'agent.tools.eachLineOneItem': '每行輸入一個項目',
// 設備配置頁面
'deviceConfig.pageTitle': '設備配置',
'deviceConfig.wifiConfig': 'WiFi配網',
'deviceConfig.ultrasonicConfig': '超聲波配網',
'deviceConfig.selectConfigMethod': '選擇配網方式',
'deviceConfig.networkConfig': '網絡配置',
'deviceConfig.selectedNetwork': '選中網絡',
'deviceConfig.signal': '信號',
'deviceConfig.openNetwork': '開放網絡',
'deviceConfig.encryptedNetwork': '加密網絡',
'deviceConfig.password': '密碼',
'deviceConfig.pleaseEnterPassword': '請輸入WiFi密碼',
'deviceConfig.startConfig': '開始配網',
'deviceConfig.connectToXiaozhiHotspot': '請先連接xiaozhi熱點 (xiaozhi-XXXXXX)',
'deviceConfig.detecting': '檢測中...',
'deviceConfig.reDetect': '重新檢測',
'deviceConfig.alreadyConnected': '已連接xiaozhi熱點',
'deviceConfig.refreshStatus': '刷新狀態',
'deviceConfig.wifiNetworks': 'WiFi網絡',
'deviceConfig.selectWifiNetwork': '選擇WiFi網絡',
'deviceConfig.refreshScan': '刷新掃描',
'deviceConfig.noWifiNetworks': '暫無WiFi網絡',
'deviceConfig.clickToRefreshScan': '請點擊刷新掃描',
'deviceConfig.signalStrong': '信號強',
'deviceConfig.signalGood': '信號良好',
'deviceConfig.signalFair': '信號一般',
'deviceConfig.signalWeak': '信號弱',
'deviceConfig.channel': '頻道',
'deviceConfig.about': '約',
'deviceConfig.seconds': '秒',
'deviceConfig.generating': '生成中...',
'deviceConfig.playing': '播放中...',
'deviceConfig.generateAndPlaySoundWave': '生成並播放聲波',
'deviceConfig.playSoundWave': '播放聲波',
'deviceConfig.stopPlaying': '停止播放',
'deviceConfig.autoLoopPlaySoundWave': '自動循環播放聲波',
'deviceConfig.configAudioFile': '配網音頻文件',
'deviceConfig.duration': '時長',
'deviceConfig.ultrasonicConfigInstructions': '超聲波配網說明',
'deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered': '確保已選擇WiFi網絡並輸入密碼',
'deviceConfig.clickGenerateAndPlaySoundWave': '點擊生成並播放聲波,系統會將配網信息編碼為音頻',
'deviceConfig.bringPhoneCloseToXiaozhiDevice': '將手機靠近xiaozhi設備(距離1-2米)',
'deviceConfig.duringAudioPlaybackXiaozhiWillReceive': '音頻播放時,xiaozhi會接收並解碼配網信息',
'deviceConfig.afterConfigSuccessDeviceWillConnect': '配網成功後設備會自動連接WiFi網絡',
'deviceConfig.usesAfskModulation': '使用AFSK調制技術,通過1800Hz和1500Hz頻率傳輸數據',
'deviceConfig.ensureModeratePhoneVolume': '請確保手機音量適中,避免環境噪音干擾',
'deviceConfig.generatingUltrasonicConfigAudio': '生成超聲波配網音頻',
'deviceConfig.configData': '配網數據',
'deviceConfig.dataBytesLength': '數據字節長度',
'deviceConfig.bitStreamLength': '比特流長度',
'deviceConfig.base64Length': 'base64長度',
'deviceConfig.audioFileTooLarge': '音頻文件過大,請縮短SSID或密碼長度',
'deviceConfig.audioGenerationSuccess': '音頻生成成功',
'deviceConfig.samplePoints': '採樣點數',
'deviceConfig.soundWaveGenerationSuccess': '聲波生成成功',
'deviceConfig.audioGenerationFailed': '音頻生成失敗',
'deviceConfig.soundWaveGenerationFailed': '聲波生成失敗',
'deviceConfig.pleaseGenerateAudioFirst': '請先生成音頻',
'deviceConfig.startPlayingUltrasonicConfigAudio': '開始播放超聲波配網音頻',
'deviceConfig.ultrasonicAudioStartedPlaying': '超聲波音頻開始播放',
'deviceConfig.startPlayingConfigSoundWave': '開始播放配網聲波',
'deviceConfig.ultrasonicAudioPlaybackEnded': '超聲波音頻播放結束',
'deviceConfig.audioPlaybackFailed': '音頻播放失敗',
'deviceConfig.audioResourceBusy': '音頻資源繁忙,請稍後重試',
'deviceConfig.audioFormatNotSupported': '音頻格式不支援,可能是data URI問題',
'deviceConfig.audioFileError': '音頻文件錯誤',
'deviceConfig.cleaningUpAudioContext': '清理音頻上下文',
'deviceConfig.cleaningUpAudioContextFailed': '清理音頻上下文失敗',
'deviceConfig.stoppedPlayingUltrasonicAudio': '停止播放超聲波音頻',
'deviceConfig.stoppedPlaying': '已停止播放',
'deviceConfig.configMethod': '配網方式',
'deviceConfig.enterWifiPassword': '請輸入WiFi密碼',
'deviceConfig.xiaozhi': 'xiaozhi',
'deviceConfig.connectXiaozhiHotspot': '請連接xiaozhi熱點',
'deviceConfig.wifiScanResponse': 'WiFi掃描響應',
'deviceConfig.scanSuccess': '掃描成功',
'deviceConfig.networks': '個網絡',
'deviceConfig.wifiScanFailed': 'WiFi掃描失敗',
'deviceConfig.scanFailedCheckConnection': '掃描失敗,請檢查連接',
'deviceConfig.checking': '檢查中',
'deviceConfig.reCheck': '重新檢查',
'deviceConfig.connectedXiaozhiHotspot': '已連接xiaozhi熱點',
'deviceConfig.wifiNetwork': 'WiFi網絡',
'deviceConfig.scanning': '掃描中',
'deviceConfig.cancel': '取消',
'deviceConfig.clickRefreshScan': '請點擊刷新掃描',
'deviceConfig.esp32ConnectionCheckFailed': 'ESP32連接檢查失敗',
'deviceConfig.startWifiConfig': '開始WiFi配網',
'deviceConfig.configSuccess': '配網成功',
'deviceConfig.deviceWillConnectTo': '設備將連接到',
'deviceConfig.deviceWillRestart': '設備將重啟',
'deviceConfig.pleaseDisconnectXiaozhiHotspot': '請斷開xiaozhi熱點連接',
'deviceConfig.configFailed': '配網失敗',
'deviceConfig.wifiConfigFailed': 'WiFi配網失敗',
'deviceConfig.pleaseCheckNetworkConnection': '請檢查網絡連接',
'deviceConfig.startWifiConfigButton': '開始配網',
'deviceConfig.wifiConfigInstructions': 'WiFi配網說明',
'deviceConfig.phoneConnectXiaozhiHotspot': '手機連接xiaozhi熱點',
'deviceConfig.selectTargetWifiNetwork': '選擇目標WiFi網絡',
'deviceConfig.enterWifiPasswordIfNeeded': '如有需要請輸入WiFi密碼',
'deviceConfig.clickStartConfigAndWait': '點擊開始配網並等待',
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配網成功後設備將自動重啟',
'deviceConfig.audioPlaybackError': '音頻播放錯誤',
'deviceConfig.playbackFailed': '播放失敗',
}
+7
View File
@@ -7,12 +7,19 @@ import store from './store'
import '@/style/index.scss' import '@/style/index.scss'
import 'virtual:uno.css' import 'virtual:uno.css'
// 导入国际化相关功能
import { initI18n } from './i18n'
import { useLangStore } from './store/lang'
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
app.use(store) app.use(store)
app.use(routeInterceptor) app.use(routeInterceptor)
app.use(VueQueryPlugin) app.use(VueQueryPlugin)
// 初始化国际化
initI18n()
return { return {
app, app,
} }
+13 -4
View File
@@ -1,7 +1,7 @@
{ {
"globalStyle": { "globalStyle": {
"navigationStyle": "default", "navigationStyle": "default",
"navigationBarTitleText": "小智智控台", "navigationBarTitleText": "智控台",
"navigationBarBackgroundColor": "#f8f8f8", "navigationBarBackgroundColor": "#f8f8f8",
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
"backgroundColor": "#FFFFFF" "backgroundColor": "#FFFFFF"
@@ -104,17 +104,26 @@
"path": "pages/device-config/index", "path": "pages/device-config/index",
"type": "page", "type": "page",
"style": { "style": {
"navigationBarTitleText": "设备配", "navigationBarTitleText": "设备配",
"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",
"layout": "default", "layout": "default",
"style": { "style": {
"navigationStyle": "custom", "navigationStyle": "custom",
"navigationBarTitleText": "登陆" "navigationBarTitleText": "Login"
} }
}, },
{ {
@@ -155,4 +164,4 @@
] ]
} }
] ]
} }

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