Compare commits

...
202 Commits
Author SHA1 Message Date
欣南科技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
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
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
LiJinHui 608abb7dc3 调整页面 2025-09-19 15:41:28 +08:00
LiJinHui 5fcc7592c9 优化代码,调整权限设置( 非管理员打开提示拒绝访问并自动跳转到首页),修复bug; 2025-09-19 15:34:43 +08:00
LiJinHui 09d5d37867 完善导航栏的语言切换 2025-09-18 16:54:00 +08:00
LiJinHui 6a8c4ccbf8 添加国际化的功能,调整页面 2025-09-18 16:43:52 +08:00
LiJinHui a199e47701 页面布局调整 2025-09-18 09:35:03 +08:00
LiJinHui c331b8322c 新增调整角色模版的功能 2025-09-17 15:38:23 +08:00
149 changed files with 14669 additions and 2652 deletions
+3 -3
View File
@@ -62,7 +62,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 +77,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
+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 |
--- ---
+1 -1
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
+1 -1
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
+4 -3
View File
@@ -80,9 +80,10 @@ VAD:
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/> 10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
### 9、语音克隆、本地语音部署相关教程 ### 9、语音克隆、本地语音部署相关教程
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、性能测试教程 ### 10、性能测试教程
1、[各组件速度测试指南](./performance_tester.md)<br/> 1、[各组件速度测试指南](./performance_tester.md)<br/>
+49 -22
View File
@@ -1,29 +1,56 @@
# 火山双流式TTS+声音克隆配置教程 # 智控台 火山双流式语音合成+音色克隆配置教程
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
本教程分为3个阶段:准备阶段、配置阶段、克隆阶段、使用阶段。主要是介绍通过智控台配置火山双流式语音合成+音色克隆的过程。
## 第一阶段:准备阶段
超级管理员先预先把火山引擎服务开通好,获取到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)
+50 -7
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
@@ -95,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`。类似这样
``` ```
@@ -127,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地址
*/ */
@@ -136,6 +146,11 @@ public interface Constant {
*/ */
String MEMORY_NO_MEM = "Memory_nomem"; String MEMORY_NO_MEM = "Memory_nomem";
/**
* 火山引擎双声道语音克隆
*/
String VOICE_CLONE_HUOSHAN_DOUBLE_STREAM = "huoshan_double_stream";
enum SysBaseParam { enum SysBaseParam {
/** /**
* ICP备案号 * ICP备案号
@@ -161,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;
}
}
/** /**
* 系统短信 * 系统短信
*/ */
@@ -247,7 +294,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.8.2"; public static final String VERSION = "0.8.5";
/** /**
* 无效固件URL * 无效固件URL
@@ -55,7 +55,7 @@ public interface ErrorCode {
int OTA_DEVICE_NOT_FOUND = 10041; int OTA_DEVICE_NOT_FOUND = 10041;
int OTA_DEVICE_NEED_BIND = 10042; int OTA_DEVICE_NEED_BIND = 10042;
// 新增错误编码 // 新增错误编码
int DELETE_DATA_FAILED = 10043; int DELETE_DATA_FAILED = 10043;
int USER_NOT_LOGIN = 10044; int USER_NOT_LOGIN = 10044;
@@ -66,7 +66,7 @@ public interface ErrorCode {
int INVALID_LLM_TYPE = 10049; int INVALID_LLM_TYPE = 10049;
int TOKEN_GENERATE_ERROR = 10050; int TOKEN_GENERATE_ERROR = 10050;
int RESOURCE_NOT_FOUND = 10051; int RESOURCE_NOT_FOUND = 10051;
// 新增错误编码 // 新增错误编码
int DEFAULT_AGENT_NOT_FOUND = 10052; int DEFAULT_AGENT_NOT_FOUND = 10052;
int AGENT_NOT_FOUND = 10053; int AGENT_NOT_FOUND = 10053;
@@ -89,7 +89,7 @@ public interface ErrorCode {
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在 int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除 int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除 int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
// 登录相关错误码 // 登录相关错误码
int ADD_DATA_FAILED = 10065; // 新增数据失败 int ADD_DATA_FAILED = 10065; // 新增数据失败
int UPDATE_DATA_FAILED = 10066; // 修改数据失败 int UPDATE_DATA_FAILED = 10066; // 修改数据失败
@@ -102,17 +102,17 @@ public interface ErrorCode {
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能 int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确 int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
int SMS_CODE_ERROR = 10075; // 手机验证码错误 int SMS_CODE_ERROR = 10075; // 手机验证码错误
// 字典类型相关错误码 // 字典类型相关错误码
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在 int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复 int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
// 资源处理相关错误码 // 资源处理相关错误码
int RESOURCE_READ_ERROR = 10078; // 读取资源失败 int RESOURCE_READ_ERROR = 10078; // 读取资源失败
// 智能体相关错误码 // 智能体相关错误码
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配 int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
// 声纹相关错误码 // 声纹相关错误码
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册 int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误 int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
@@ -126,12 +126,12 @@ public interface ErrorCode {
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败 int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败 int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败 int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
// 服务端管理相关错误码 // 服务端管理相关错误码
int INVALID_SERVER_ACTION = 10095; // 无效服务端操作 int INVALID_SERVER_ACTION = 10095; // 无效服务端操作
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址 int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在 int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
// 参数验证相关错误码 // 参数验证相关错误码
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空 int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1 int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
@@ -161,7 +161,41 @@ public interface ErrorCode {
int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全 int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全
int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母 int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母
int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码 int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码
// 字典相关错误码
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复 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格式错误
} }
@@ -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());
} }
} }
@@ -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("批量删除模板失败");
}
}
}
@@ -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,8 +19,8 @@ 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.RenException;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
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;
@@ -42,6 +42,7 @@ import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.dto.VoiceDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity; import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
@@ -183,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) {
@@ -370,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;
}
} }
@@ -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
@@ -70,7 +73,7 @@ public class ConfigServiceImpl implements ConfigService {
// 查询默认智能体 // 查询默认智能体
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate(); AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
if (agent == null) { if (agent == null) {
throw new RenException(ErrorCode.DEFAULT_AGENT_NOT_FOUND); throw new RenException("默认智能体未找到");
} }
// 构建模块配置 // 构建模块配置
@@ -113,7 +116,7 @@ public class ConfigServiceImpl implements ConfigService {
// 获取智能体信息 // 获取智能体信息
AgentEntity agent = agentService.getAgentById(device.getAgentId()); AgentEntity agent = agentService.getAgentById(device.getAgentId());
if (agent == null) { if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND); throw new RenException("智能体未找到");
} }
// 获取音色信息 // 获取音色信息
String voice = null; String voice = null;
@@ -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());
} }
} }
@@ -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
@@ -134,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;
} }
@@ -165,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/";
@@ -225,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);
@@ -459,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()));
} }
/** /**
@@ -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,8 +12,11 @@ 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.ErrorCode;
@@ -22,6 +26,7 @@ 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;
@@ -63,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());
@@ -76,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(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
}
// 再保存供应器提供的模型 // 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(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
}
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(ErrorCode.LLM_NOT_EXIST);
}
String type = modelConfigEntity.getConfigJson().get("type").toString();
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
if (!"openai".equals(type) && !"ollama".equals(type)) {
throw new RenException(ErrorCode.INVALID_LLM_TYPE);
}
}
// 再更新供应器提供的模型 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(ErrorCode.DEFAULT_MODEL_DELETE_ERROR); 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(
@@ -187,8 +473,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
/** /**
* 检查意图识别配置是否有引用 * 检查意图识别配置是否有引用
*
* @param modelId 模型ID
*/ */
private void checkIntentConfigReference(String modelId) { private void checkIntentConfigReference(String modelId) {
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId); ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
@@ -204,53 +488,11 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
} }
} }
/**
* 获取符合条件的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));
} }
} }
@@ -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);
@@ -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,10 +70,11 @@ 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(ErrorCode.SMS_CAPTCHA_ERROR); 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) {
@@ -83,11 +88,14 @@ 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(ErrorCode.SMS_CAPTCHA_ERROR); String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
} password, login.getCaptchaId(), captchaService, sysParamsService);
login.setPassword(actualPassword);
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
// 判断用户是否存在 // 判断用户是否存在
@@ -100,6 +108,8 @@ public class LoginController {
} }
return sysUserTokenService.createToken(userDTO.getId()); return sysUserTokenService.createToken(userDTO.getId());
} }
@PostMapping("/register") @PostMapping("/register")
@Operation(summary = "注册") @Operation(summary = "注册")
@@ -107,6 +117,15 @@ public class LoginController {
if (!sysUserService.getAllowUserRegister()) { if (!sysUserService.getAllowUserRegister()) {
throw new RenException(ErrorCode.USER_REGISTER_DISABLED); 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);
@@ -122,12 +141,6 @@ public class LoginController {
if (!validate) { if (!validate) {
throw new RenException(ErrorCode.SMS_CODE_ERROR); throw new RenException(ErrorCode.SMS_CODE_ERROR);
} }
} else {
// 验证是否正确输入验证码
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
} }
// 按照用户名获取用户 // 按照用户名获取用户
@@ -190,6 +203,14 @@ public class LoginController {
throw new RenException(ErrorCode.SMS_CODE_ERROR); 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;
@@ -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;
} }
@@ -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);
}
} }
/** /**
@@ -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,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';
@@ -359,3 +359,46 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202509161701.sql 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,137 +1,169 @@
#简体中文 #\u7B80\u4F53\u4E2D\u6587
500=服务器内部异常 500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=未授权 401=\u672A\u6388\u6743
403=拒绝访问,没有权限 403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}不能为空 10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=数据库中已存在该记录 10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=获取参数失败 10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=账号或密码错误 10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=账号已被停用 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=唯一标识不能为空 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=验证码不正确 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=先删除子菜单或按钮 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=原密码不正确 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=账号或密码不正确,您还有可以尝试{0}次 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=上级部门选择错误 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=上级菜单不能为自身 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=数据权限接口,只能是Map类型参数 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=请先删除下级部门 10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=请先删除部门下的用户 10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=部署失败,没有流程 10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=模型图不正确,请检查 10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=导出失败,模型ID为{0} 10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=请上传文件 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token不能为空 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token失效,请重新登录 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=账号已被锁定 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件 10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上传文件失败{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=发送短信失败{0} 10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=邮件模板不存在 10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服务异常 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=定时任务失败 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=不能包含非法字符 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=密码长度不足{0}位 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=密码必须同时包含数字、大小写字母和特殊字符组成 10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=删除本数据异常 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=设备验证码错误 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=参数值不能为空 10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=参数类型不能为空 10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=不支持的参数类型 10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=参数值必须是有效的数字 10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=参数值必须是true或false 10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=参数值必须是有效的JSON数组格式 10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=参数值必须是有效的JSON格式 10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=设备未找到 10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0} 10042={0}
10043=删除数据失败 10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=用户未登录 10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket连接失败或连接超时 10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=保存声纹错误,请联系管理员 10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=每日发送次数已达上限 10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=旧密码输入错误 10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=设置的LLM不是openai和ollama 10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token生成失败 10050=token\u751F\u6210\u5931\u8D25
10051=资源不存在 10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=默认智能体未找到 10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=智能体未找到 10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print) 10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=短信发送失败 10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=短信连接建立失败 10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=智能体的声纹创建失败 10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=智能体的对应声纹更新失败 10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=智能体的对应声纹删除失败 10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=发送太频繁,请{0}秒后再试 10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=激活码不能为空 10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=激活码错误 10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=设备已激活 10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=该模型为默认模型,请先设置其他模型为默认模型 10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=新增数据失败 10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=修改数据失败 10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=图形验证码错误 10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=没有开启手机注册,没法使用短信验证码功能 10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=用户名不是手机号码,请重新输入 10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=此手机号码已经注册过 10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=输入的手机号码未注册 10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=当前不允许普通用户注册 10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=没有开启手机注册,没法使用找回密码功能 10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=输入的手机号码格式不正确 10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=输入的手机验证码错误 10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=字典类型不存在 10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=字典类型编码重复 10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=读取资源失败 10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM大模型和Intent意图识别,选择参数不匹配 10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册 10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
10081=删除声纹出现错误 10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=此次修改不允许,此声音已经注册为声纹了({0}) 10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=修改声纹错误,请联系管理员 10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址 10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10085=音频数据不属于这个智能体 10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=音频数据是空的请检查上传数据 10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=声纹保存失败,请求不成功 10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=声纹保存失败,请求处理失败 10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=声纹注销失败,请求不成功 10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=声纹注销失败,请求处理失败 10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=供应器不存在 10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=设置的LLM不存在 10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=该模型配置已被智能体{0}引用,无法删除 10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=该LLM模型已被意图识别配置引用,无法删除 10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=无效服务端操作 10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=未配置服务端WebSocket地址 10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=目标WebSocket地址不存在 10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket地址列表不能为空 10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket地址不能使用localhost127.0.0.1 10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket地址格式不正确 10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket连接测试失败 10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA地址不能为空 10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA地址不能使用localhost127.0.0.1 10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA地址必须以http或https开头 10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA地址必须以/ota/结尾 10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA接口访问失败 10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA接口返回内容格式不正确 10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA接口验证失败 10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP地址不能为空 10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP地址不能使用localhost127.0.0.1 10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=不是正确的MCP地址 10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP接口访问失败 10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP接口返回内容格式不正确 10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP接口验证失败 10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=声纹接口地址不能为空 10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=声纹接口地址不能使用localhost127.0.0.1 10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=不是正确的声纹接口地址 10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=声纹接口地址必须以http或https开头 10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=声纹接口访问失败 10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=声纹接口返回内容格式不正确 10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=声纹接口验证失败 10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt密钥不能为空 10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母 10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母 10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=您的mqtt密钥包含弱密码 10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=字典标签重复 10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=SM2\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
@@ -134,4 +134,36 @@
10123=Your MQTT secret is not secure, it needs to be at least 8 characters and must contain both uppercase and lowercase letters 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 10124=Your MQTT secret is not secure, MQTT secret must contain both uppercase and lowercase letters
10125=Your MQTT secret contains weak password 10125=Your MQTT secret contains weak password
10128=Dictionary label is duplicated 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_
@@ -1,137 +1,169 @@
#简体中文 #\u7B80\u4F53\u4E2D\u6587
500=服务器内部异常 500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=未授权 401=\u672A\u6388\u6743
403=拒绝访问,没有权限 403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}不能为空 10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=数据库中已存在该记录 10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=获取参数失败 10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=账号或密码错误 10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=账号已被停用 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=唯一标识不能为空 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=验证码不正确 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=先删除子菜单或按钮 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=原密码不正确 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=账号或密码不正确,您还有可以尝试{0}次 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=上级部门选择错误 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=上级菜单不能为自身 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=数据权限接口,只能是Map类型参数 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=请先删除下级部门 10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=请先删除部门下的用户 10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=部署失败,没有流程 10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=模型图不正确,请检查 10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=导出失败,模型ID为{0} 10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=请上传文件 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token不能为空 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token失效,请重新登录 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=账号已被锁定 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件 10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上传文件失败{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=发送短信失败{0} 10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=邮件模板不存在 10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服务异常 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=定时任务失败 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=不能包含非法字符 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=密码长度不足{0}位 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=密码必须同时包含数字、大小写字母和特殊字符组成 10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=删除本数据异常 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=设备验证码错误 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=参数值不能为空 10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=参数类型不能为空 10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=不支持的参数类型 10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=参数值必须是有效的数字 10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=参数值必须是true或false 10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=参数值必须是有效的JSON数组格式 10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=参数值必须是有效的JSON格式 10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=设备未找到 10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0} 10042={0}
10043=删除数据失败 10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=用户未登录 10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket连接失败或连接超时 10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=保存声纹错误,请联系管理员 10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=每日发送次数已达上限 10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=旧密码输入错误 10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=设置的LLM不是openai和ollama 10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token生成失败 10050=token\u751F\u6210\u5931\u8D25
10051=资源不存在 10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=默认智能体未找到 10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=智能体未找到 10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print) 10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=短信发送失败 10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=短信连接建立失败 10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=智能体的声纹创建失败 10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=智能体的对应声纹更新失败 10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=智能体的对应声纹删除失败 10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=发送太频繁,请{0}秒后再试 10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=激活码不能为空 10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=激活码错误 10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=设备已激活 10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=该模型为默认模型,请先设置其他模型为默认模型 10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=新增数据失败 10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=修改数据失败 10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=图形验证码错误 10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=没有开启手机注册,没法使用短信验证码功能 10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=用户名不是手机号码,请重新输入 10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=此手机号码已经注册过 10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=输入的手机号码未注册 10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=当前不允许普通用户注册 10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=没有开启手机注册,没法使用找回密码功能 10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=输入的手机号码格式不正确 10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=输入的手机验证码错误 10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=字典类型不存在 10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=字典类型编码重复 10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=读取资源失败 10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM大模型和Intent意图识别,选择参数不匹配 10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册 10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
10081=删除声纹出现错误 10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=此次修改不允许,此声音已经注册为声纹了({0}) 10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=修改声纹错误,请联系管理员 10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址 10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10085=音频数据不属于这个智能体 10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=音频数据是空的请检查上传数据 10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=声纹保存失败,请求不成功 10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=声纹保存失败,请求处理失败 10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=声纹注销失败,请求不成功 10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=声纹注销失败,请求处理失败 10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=供应器不存在 10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=设置的LLM不存在 10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=该模型配置已被智能体{0}引用,无法删除 10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=该LLM模型已被意图识别配置引用,无法删除 10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=无效服务端操作 10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=未配置服务端WebSocket地址 10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=目标WebSocket地址不存在 10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket地址列表不能为空 10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket地址不能使用localhost127.0.0.1 10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket地址格式不正确 10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket连接测试失败 10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA地址不能为空 10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA地址不能使用localhost127.0.0.1 10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA地址必须以http或https开头 10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA地址必须以/ota/结尾 10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA接口访问失败 10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA接口返回内容格式不正确 10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA接口验证失败 10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP地址不能为空 10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP地址不能使用localhost127.0.0.1 10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=不是正确的MCP地址 10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP接口访问失败 10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP接口返回内容格式不正确 10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP接口验证失败 10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=声纹接口地址不能为空 10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=声纹接口地址不能使用localhost127.0.0.1 10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=不是正确的声纹接口地址 10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=声纹接口地址必须以http或https开头 10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=声纹接口访问失败 10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=声纹接口返回内容格式不正确 10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=声纹接口验证失败 10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt密钥不能为空 10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母 10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母 10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=您的mqtt密钥包含弱密码 10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=字典标签重复 10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=SM2\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
@@ -1,137 +1,169 @@
#繁體中文 #\u7E41\u9AD4\u4E2D\u6587
500=服務器內部異常 500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
401=未授權 401=\u672A\u6388\u6B0A
403=拒絕訪問,沒有权限 403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6743\u9650
10001={0}不能為空 10001={0}\u4E0D\u80FD\u70BA\u7A7A
10002=數據庫中已存在該記錄 10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
10003=獲取參數失敗 10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
10004=賬號或密碼錯誤 10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
10005=賬號已被停用 10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
10006=唯一標識不能為空 10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
10007=驗證碼不正確 10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10008=先刪除子菜單或按鈕 10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
10009=原密碼不正確 10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
10010=帳號或密碼不正確,您還有可以嘗試{0}次 10010=\u5E33\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
10011=上級部門選擇錯誤 10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
10012=上級菜單不能為自身 10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
10013=數據權限接口,只能是Map類型參數 10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
10014=請先刪除下級部門 10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
10015=請先刪除部門下的用戶 10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
10016=部署失敗,沒有流程 10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
10017=模型圖不正確,請檢查 10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
10018=導出失敗,模型ID為{0} 10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
10019=請上傳文件 10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
10020=token不能為空 10020=token\u4E0D\u80FD\u70BA\u7A7A
10021=token失效,請重新登錄 10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
10022=賬號已被鎖定 10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件 10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上傳文件失敗{0} 10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
10025=發送短信失敗{0} 10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
10026=郵件模板不存在 10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服務異常 10027=Redis\u670D\u52D9\u7570\u5E38
10028=定時任務失敗 10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
10029=不能包含非法字符 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=密碼長度不足{0}位 10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成 10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5BEB\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
10032=刪除本數據異常 10032=\u522A\u9664\u672C\u6578\u64DA\u7570\u5E38
10033=設備驗證碼錯誤 10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
10034=參數值不能為空 10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
10035=參數類型不能為空 10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
10036=不支持的參數類型 10036=\u4E0D\u652F\u6301\u7684\u53C3\u6578\u985E\u578B
10037=參數值必須是有效的數字 10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=參數值必須是true或false 10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=參數值必須是有效的JSON數組格式 10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=參數值必須是有效的JSON格式 10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=設備未找到 10041=\u8A2D\u5099\u672A\u627E\u5230
10042={0} 10042={0}
10043=刪除數據失敗 10043=\u522A\u9664\u6578\u64DA\u5931\u6557
10044=用戶未登錄 10044=\u7528\u6236\u672A\u767B\u9304
10045=WebSocket連接失敗或連接超時 10045=WebSocket\u9023\u63A5\u5931\u6557\u6216\u9023\u63A5\u8D85\u6642
10046=保存聲紋錯誤,請聯繫管理員 10046=\u4FDD\u5B58\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10047=每日發送次數已達上限 10047=\u6BCF\u65E5\u767C\u9001\u6B21\u6578\u5DF2\u9054\u4E0A\u9650
10048=舊密碼輸入錯誤 10048=\u820A\u5BC6\u78BC\u8F38\u5165\u932F\u8AA4
10049=設置的LLM不是openai和ollama 10049=\u8A2D\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token生成失敗 10050=token\u751F\u6210\u5931\u6557
10051=資源不存在 10051=\u8CC7\u6E90\u4E0D\u5B58\u5728
10052=默認智能體未找到 10052=\u9ED8\u8A8D\u667A\u80FD\u9AD4\u672A\u627E\u5230
10053=智能體未找到 10053=\u667A\u80FD\u9AD4\u672A\u627E\u5230
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print) 10054=\u8072\u7D0B\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8ACB\u5148\u5728\u53C3\u6578\u914D\u7F6E\u4E2D\u914D\u7F6E\u8072\u7D0B\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=短信發送失敗 10055=\u77ED\u4FE1\u767C\u9001\u5931\u6557
10056=短信連接建立失敗 10056=\u77ED\u4FE1\u9023\u63A5\u5EFA\u7ACB\u5931\u6557
10057=智能體的聲紋創建失敗 10057=\u667A\u80FD\u9AD4\u7684\u8072\u7D0B\u5275\u5EFA\u5931\u6557
10058=智能體的對應聲紋更新失敗 10058=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u66F4\u65B0\u5931\u6557
10059=智能體的對應聲紋刪除失敗 10059=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u522A\u9664\u5931\u6557
10060=發送太頻繁,請{0}秒後再試 10060=\u767C\u9001\u592A\u983B\u7E41\uFF0C\u8ACB{0}\u79D2\u5F8C\u518D\u8A66
10061=激活碼不能為空 10061=\u6FC0\u6D3B\u78BC\u4E0D\u80FD\u70BA\u7A7A
10062=激活碼錯誤 10062=\u6FC0\u6D3B\u78BC\u932F\u8AA4
10063=設備已激活 10063=\u8A2D\u5099\u5DF2\u6FC0\u6D3B
10064=該模型為預設模型,請先設置其他模型為預設模型 10064=\u8A72\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B\uFF0C\u8ACB\u5148\u8A2D\u7F6E\u5176\u4ED6\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B
10065=新增數據失敗 10065=\u65B0\u589E\u6578\u64DA\u5931\u6557
10066=更新數據失敗 10066=\u66F4\u65B0\u6578\u64DA\u5931\u6557
10067=圖形驗證碼錯誤 10067=\u5716\u5F62\u9A57\u8B49\u78BC\u932F\u8AA4
10068=未開啟手機註冊,無法使用短信驗證碼功能 10068=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A57\u8B49\u78BC\u529F\u80FD
10069=用戶名不是手機號碼,請重新輸入 10069=\u7528\u6236\u540D\u4E0D\u662F\u624B\u6A5F\u865F\u78BC\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165
10070=該手機號碼已註冊 10070=\u8A72\u624B\u6A5F\u865F\u78BC\u5DF2\u8A3B\u518A
10071=輸入的手機號碼未註冊 10071=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u672A\u8A3B\u518A
10072=目前不允許普通用戶註冊 10072=\u76EE\u524D\u4E0D\u5141\u8A31\u666E\u901A\u7528\u6236\u8A3B\u518A
10073=未開啟手機註冊,無法使用密碼找回功能 10073=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u5BC6\u78BC\u627E\u56DE\u529F\u80FD
10074=輸入的手機號碼格式不正確 10074=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u683C\u5F0F\u4E0D\u6B63\u78BA
10075=輸入的短信驗證碼不正確 10075=\u8F38\u5165\u7684\u77ED\u4FE1\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10076=字典類型不存在 10076=\u5B57\u5178\u985E\u578B\u4E0D\u5B58\u5728
10077=字典類型編碼重複 10077=\u5B57\u5178\u985E\u578B\u7DE8\u78BC\u91CD\u8907
10078=讀取資源失敗 10078=\u8B80\u53D6\u8CC7\u6E90\u5931\u6557
10079=LLM模型與意圖識別、參數選擇不匹配 10079=LLM\u6A21\u578B\u8207\u610F\u5716\u8B58\u5225\u3001\u53C3\u6578\u9078\u64C7\u4E0D\u5339\u914D
10080=此聲紋屬於已註冊的{0},請選擇其他聲音 10080=\u6B64\u8072\u7D0B\u5C6C\u65BC\u5DF2\u8A3B\u518A\u7684{0}\uFF0C\u8ACB\u9078\u64C7\u5176\u4ED6\u8072\u97F3
10081=刪除聲紋時發生錯誤 10081=\u522A\u9664\u8072\u7D0B\u6642\u767C\u751F\u932F\u8AA4
10082=不允許修改,此聲音已註冊為聲紋({0}) 10082=\u4E0D\u5141\u8A31\u4FEE\u6539\uFF0C\u6B64\u8072\u97F3\u5DF2\u8A3B\u518A\u70BA\u8072\u7D0B({0})
10083=修改聲紋錯誤,請聯繫管理員 10083=\u4FEE\u6539\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址 10084=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u932F\u8AA4\uFF0C\u8ACB\u524D\u5F80\u53C3\u6578\u7BA1\u7406\u4FEE\u6539\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10085=音頻數據不屬於此智能體 10085=\u97F3\u983B\u6578\u64DA\u4E0D\u5C6C\u65BC\u6B64\u667A\u80FD\u9AD4
10086=音頻數據為空,請檢查上傳數據 10086=\u97F3\u983B\u6578\u64DA\u70BA\u7A7A\uFF0C\u8ACB\u6AA2\u67E5\u4E0A\u50B3\u6578\u64DA
10087=聲紋註冊失敗,請求未成功 10087=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10088=聲紋註冊失敗,請求處理失敗 10088=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10089=聲紋註銷失敗,請求未成功 10089=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10090=聲紋註銷失敗,請求處理失敗 10090=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10091=模型提供商不存在 10091=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
10092=配置的LLM不存在 10092=\u914D\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=此模型配置被智能體{0}引用,無法刪除 10093=\u6B64\u6A21\u578B\u914D\u7F6E\u88AB\u667A\u80FD\u9AD4{0}\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10094=此LLM模型被意圖識別配置引用,無法刪除 10094=\u6B64LLM\u6A21\u578B\u88AB\u610F\u5716\u8B58\u5225\u914D\u7F6E\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10095=無效的服務器操作 10095=\u7121\u6548\u7684\u670D\u52D9\u5668\u64CD\u4F5C
10096=未配置服務器WebSocket地址 10096=\u672A\u914D\u7F6E\u670D\u52D9\u5668WebSocket\u5730\u5740
10097=目標WebSocket地址不存在 10097=\u76EE\u6A19WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket地址列表不能為空 10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u70BA\u7A7A
10099=WebSocket地址不能使用localhost127.0.0.1 10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket地址格式不正確 10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA
10101=WebSocket連接測試失敗 10101=WebSocket\u9023\u63A5\u6E2C\u8A66\u5931\u6557
10102=OTA地址不能為空 10102=OTA\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10103=OTA地址不能使用localhost127.0.0.1 10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA地址必須以http或https開頭 10104=OTA\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10105=OTA地址必須以/ota/結尾 10105=OTA\u5730\u5740\u5FC5\u9808\u4EE5/ota/\u7D50\u5C3E
10106=OTA接口訪問失敗 10106=OTA\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10107=OTA接口返回內容格式不正確 10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10108=OTA接口驗證失敗 10108=OTA\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10109=MCP地址不能為空 10109=MCP\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10110=MCP地址不能使用localhost127.0.0.1 10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=不是有效的MCP地址 10111=\u4E0D\u662F\u6709\u6548\u7684MCP\u5730\u5740
10112=MCP接口訪問失敗 10112=MCP\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10113=MCP接口返回內容格式不正確 10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10114=MCP接口驗證失敗 10114=MCP\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10115=聲紋接口地址不能為空 10115=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10116=聲紋接口地址不能使用localhost127.0.0.1 10116=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=不是有效的聲紋接口地址 10117=\u4E0D\u662F\u6709\u6548\u7684\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10118=聲紋接口地址必須以http或https開頭 10118=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10119=聲紋接口訪問失敗 10119=\u8072\u7D0B\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10120=聲紋接口返回內容格式不正確 10120=\u8072\u7D0B\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10121=聲紋接口驗證失敗 10121=\u8072\u7D0B\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10122=MQTT密鑰不能為空 10122=MQTT\u5BC6\u9470\u4E0D\u80FD\u70BA\u7A7A
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母 10123=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0C\u9700\u81F3\u5C118\u500B\u5B57\u7B26\u4E14\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母 10124=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0CMQTT\u5BC6\u9470\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10125=您的MQTT密鑰包含弱密碼 10125=\u60A8\u7684MQTT\u5BC6\u9470\u5305\u542B\u5F31\u5BC6\u78BC
10128=字典標籤重複 10128=\u5B57\u5178\u6A19\u7C64\u91CD\u8907
10129=SM2\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
@@ -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 @@
] ]
} }
] ]
} }
+40 -38
View File
@@ -4,6 +4,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { getAgentDetail, getModelOptions, getPluginFunctions, getRoleTemplates, getTTSVoices, updateAgent } from '@/api/agent/agent' import { getAgentDetail, getModelOptions, getPluginFunctions, getRoleTemplates, getTTSVoices, updateAgent } from '@/api/agent/agent'
import { usePluginStore } from '@/store' import { usePluginStore } from '@/store'
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({ defineOptions({
name: 'AgentEdit', name: 'AgentEdit',
@@ -37,14 +38,15 @@ const formData = ref<Partial<AgentDetail>>({
// 显示名称数据 // 显示名称数据
const displayNames = ref({ const displayNames = ref({
vad: '请选择', // 显示名称数据
asr: '请选择', vad: t('agent.pleaseSelect'),
llm: '请选择', asr: t('agent.pleaseSelect'),
vllm: '请选择', llm: t('agent.pleaseSelect'),
intent: '请选择', vllm: t('agent.pleaseSelect'),
memory: '请选择', intent: t('agent.pleaseSelect'),
tts: '请选择', memory: t('agent.pleaseSelect'),
voiceprint: '请选择', tts: t('agent.pleaseSelect'),
voiceprint: t('agent.pleaseSelect'),
}) })
// 角色模板数据 // 角色模板数据
@@ -143,7 +145,7 @@ async function loadAgentDetail() {
} }
catch (error) { catch (error) {
console.error('加载智能体详情失败:', error) console.error('加载智能体详情失败:', error)
toast.error('加载失败') toast.error(t('agent.loadFail'))
} }
finally { finally {
loading.value = false loading.value = false
@@ -358,12 +360,12 @@ function getModelDisplayName(modelType: string, modelId: string) {
// 保存智能体 // 保存智能体
async function saveAgent() { async function saveAgent() {
if (!formData.value.agentName?.trim()) { if (!formData.value.agentName?.trim()) {
toast.warning('请输入助手昵称') toast.warning(t('agent.pleaseInputAgentName'))
return return
} }
if (!formData.value.systemPrompt?.trim()) { if (!formData.value.systemPrompt?.trim()) {
toast.warning('请输入角色介绍') toast.warning(t('agent.pleaseInputRoleDescription'))
return return
} }
@@ -371,11 +373,11 @@ async function saveAgent() {
saving.value = true saving.value = true
await updateAgent(agentId.value, formData.value) await updateAgent(agentId.value, formData.value)
toast.success('保存成功') toast.success(t('agent.saveSuccess'))
} }
catch (error) { catch (error) {
console.error('保存失败:', error) console.error('保存失败:', error)
toast.error('保存失败') toast.error(t('agent.saveFail'))
} }
finally { finally {
saving.value = false saving.value = false
@@ -441,10 +443,10 @@ onMounted(async () => {
<template> <template>
<view class="bg-[#f5f7fb] px-[20rpx]"> <view class="bg-[#f5f7fb] px-[20rpx]">
<!-- 基础信息标题 --> <!--// 基础信息标题
<view class="pb-[20rpx] first:pt-[20rpx]"> <view class="pb-[20rpx] first:pt-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
基础信息 {{ t('agent.basicInfo') }}
</text> </text>
</view> </view>
@@ -452,20 +454,20 @@ onMounted(async () => {
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);"> <view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
<view class="mb-[24rpx] last:mb-0"> <view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
助手昵称 {{ t('agent.agentName') }}
</text> </text>
<input <input
v-model="formData.agentName" v-model="formData.agentName"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text" type="text"
placeholder="请输入助手昵称" :placeholder="t('agent.inputAgentName')"
> >
</view> </view>
<view class="mb-[24rpx] last:mb-0"> <view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
角色模式 {{ t('agent.roleMode') }}
</text> </text>
<view class="mt-0 flex flex-wrap gap-[12rpx]"> <view class="mt-0 flex flex-wrap gap-[12rpx]">
<view <view
v-for="template in roleTemplates" v-for="template in roleTemplates"
@@ -483,12 +485,12 @@ onMounted(async () => {
<view class="mb-[24rpx] last:mb-0"> <view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
角色介绍 {{ t('agent.roleDescription') }}
</text> </text>
<textarea <textarea
v-model="formData.systemPrompt" v-model="formData.systemPrompt"
:maxlength="2000" :maxlength="2000"
placeholder="请输入角色介绍" :placeholder="t('agent.inputRoleDescription')"
class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
/> />
<view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]"> <view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]">
@@ -500,7 +502,7 @@ onMounted(async () => {
<!-- 模型配置标题 --> <!-- 模型配置标题 -->
<view class="pb-[20rpx]"> <view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
模型配置 {{ t('agent.modelConfig') }}
</text> </text>
</view> </view>
@@ -509,7 +511,7 @@ onMounted(async () => {
<view class="flex flex-col gap-[16rpx]"> <view class="flex flex-col gap-[16rpx]">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vad')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vad')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
语音活动检测 {{ t('agent.vad') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.vad }} {{ displayNames.vad }}
@@ -519,7 +521,7 @@ onMounted(async () => {
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('asr')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('asr')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
语音识别 {{ t('agent.asr') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.asr }} {{ displayNames.asr }}
@@ -529,7 +531,7 @@ onMounted(async () => {
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('llm')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('llm')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
大语言模型 {{ t('agent.llm') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.llm }} {{ displayNames.llm }}
@@ -539,7 +541,7 @@ onMounted(async () => {
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
视觉大模型 {{ t('agent.vllm') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.vllm }} {{ displayNames.vllm }}
@@ -549,7 +551,7 @@ onMounted(async () => {
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('intent')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('intent')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
意图识别 {{ t('agent.intent') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.intent }} {{ displayNames.intent }}
@@ -559,7 +561,7 @@ onMounted(async () => {
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('memory')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('memory')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
记忆 {{ t('agent.memory') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.memory }} {{ displayNames.memory }}
@@ -572,7 +574,7 @@ onMounted(async () => {
<!-- 语音设置标题 --> <!-- 语音设置标题 -->
<view class="pb-[20rpx]"> <view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
语音设置 {{ t('agent.voiceSettings') }}
</text> </text>
</view> </view>
@@ -581,7 +583,7 @@ onMounted(async () => {
<view class="flex flex-col gap-[16rpx]"> <view class="flex flex-col gap-[16rpx]">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('tts')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('tts')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
语音合成 {{ t('agent.tts') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.tts }} {{ displayNames.tts }}
@@ -591,7 +593,7 @@ onMounted(async () => {
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('voiceprint')"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('voiceprint')">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
角色音色 {{ t('agent.voiceprint') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.voiceprint }} {{ displayNames.voiceprint }}
@@ -601,10 +603,10 @@ onMounted(async () => {
<view class="flex items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx]"> <view class="flex items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx]">
<view class="text-[28rpx] text-[#232338] font-medium"> <view class="text-[28rpx] text-[#232338] font-medium">
插件 {{ t('agent.plugins') }}
</view> </view>
<view class="cursor-pointer rounded-[20rpx] bg-[rgba(51,108,255,0.1)] px-[24rpx] py-[12rpx] text-[24rpx] text-[#336cff] transition-all duration-300 active:bg-[#336cff] active:text-white" @click="handleTools"> <view class="cursor-pointer rounded-[20rpx] bg-[rgba(51,108,255,0.1)] px-[24rpx] py-[12rpx] text-[24rpx] text-[#336cff] transition-all duration-300 active:bg-[#336cff] active:text-white" @click="handleTools">
<text>编辑功能</text> <text>{{ t('agent.editFunctions') }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -613,7 +615,7 @@ onMounted(async () => {
<!-- 记忆历史标题 --> <!-- 记忆历史标题 -->
<view class="pb-[20rpx]"> <view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
历史记忆 {{ t('agent.historyMemory') }}
</text> </text>
</view> </view>
@@ -622,7 +624,7 @@ onMounted(async () => {
<view class="mb-[24rpx] last:mb-0"> <view class="mb-[24rpx] last:mb-0">
<textarea <textarea
v-model="formData.summaryMemory" v-model="formData.summaryMemory"
placeholder="记忆内容" :placeholder="t('agent.memoryContent')"
disabled disabled
class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f0f0f0] p-[20rpx] text-[26rpx] text-[#65686f] leading-[1.6] opacity-80 outline-none" class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f0f0f0] p-[20rpx] text-[26rpx] text-[#65686f] leading-[1.6] opacity-80 outline-none"
/> />
@@ -638,7 +640,7 @@ onMounted(async () => {
custom-class="w-full h-[80rpx] rounded-[16rpx] text-[30rpx] font-semibold bg-[#336cff] active:bg-[#2d5bd1]" custom-class="w-full h-[80rpx] rounded-[16rpx] text-[30rpx] font-semibold bg-[#336cff] active:bg-[#2d5bd1]"
@click="saveAgent" @click="saveAgent"
> >
{{ saving ? '保存中...' : '保存' }} {{ saving ? t('agent.saving') : t('agent.save') }}
</wd-button> </wd-button>
</view> </view>
<!-- 模型选择器 --> <!-- 模型选择器 -->
@@ -11,6 +11,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { t } from '@/i18n'
import CustomTabs from '@/components/custom-tabs/index.vue' import CustomTabs from '@/components/custom-tabs/index.vue'
import ChatHistory from '@/pages/chat-history/index.vue' import ChatHistory from '@/pages/chat-history/index.vue'
import DeviceManagement from '@/pages/device/index.vue' import DeviceManagement from '@/pages/device/index.vue'
@@ -42,7 +43,6 @@ systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets safeAreaInsets = systemInfo.safeAreaInsets
// #endif // #endif
// 智能体ID // 智能体ID
const currentAgentId = ref('default') const currentAgentId = ref('default')
@@ -65,25 +65,25 @@ const voiceprintRef = ref()
// Tab 配置 // Tab 配置
const tabList = [ const tabList = [
{ {
label: '角色配置', label: t('agent.roleConfig'),
value: 'agent-config', value: 'agent-config',
icon: '/static/tabbar/robot.png', icon: '/static/tabbar/robot.png',
activeIcon: '/static/tabbar/robot_activate.png', activeIcon: '/static/tabbar/robot_activate.png',
}, },
{ {
label: '设备管理', label: t('agent.deviceManagement'),
value: 'device-management', value: 'device-management',
icon: '/static/tabbar/device.png', icon: '/static/tabbar/device.png',
activeIcon: '/static/tabbar/device_activate.png', activeIcon: '/static/tabbar/device_activate.png',
}, },
{ {
label: '聊天记录', label: t('agent.chatHistory'),
value: 'chat-history', value: 'chat-history',
icon: '/static/tabbar/chat.png', icon: '/static/tabbar/chat.png',
activeIcon: '/static/tabbar/chat_activate.png', activeIcon: '/static/tabbar/chat_activate.png',
}, },
{ {
label: '声纹管理', label: t('agent.voiceprintManagement'),
value: 'voiceprint-management', value: 'voiceprint-management',
icon: '/static/tabbar/microphone.png', icon: '/static/tabbar/microphone.png',
activeIcon: '/static/tabbar/microphone_activate.png', activeIcon: '/static/tabbar/microphone_activate.png',
@@ -160,7 +160,7 @@ onMounted(async () => {
<template> <template>
<view class="h-screen flex flex-col bg-[#f5f7fb]"> <view class="h-screen flex flex-col bg-[#f5f7fb]">
<!-- 导航栏 --> <!-- 导航栏 -->
<wd-navbar title="智能体" safe-area-inset-top> <wd-navbar :title="t('agent.pageTitle')" safe-area-inset-top>
<template #left> <template #left>
<wd-icon name="arrow-left" size="18" @click="goBack" /> <wd-icon name="arrow-left" size="18" @click="goBack" />
</template> </template>
+35 -34
View File
@@ -3,8 +3,8 @@
"layout": "default", "layout": "default",
"style": { "style": {
"navigationBarTitleText": "编辑功能", "navigationBarTitleText": "编辑功能",
"navigationStyle": "custom", "navigationStyle": "custom"
}, }
} }
</route> </route>
@@ -12,12 +12,13 @@
import { useMessage } from 'wot-design-uni' import { useMessage } from 'wot-design-uni'
import { getMcpAddress, getMcpTools } from '@/api/agent/agent' import { getMcpAddress, getMcpTools } from '@/api/agent/agent'
import { usePluginStore } from '@/store' import { usePluginStore } from '@/store'
import { t } from '@/i18n'
const message = useMessage() const message = useMessage()
const pluginStore = usePluginStore() const pluginStore = usePluginStore()
const segmentedList = ref<string[]>(['未选', '已选']) const segmentedList = ref<string[]>([t('agent.tools.notSelected'), t('agent.tools.selected')])
const currentSegmented = ref('未选') const currentSegmented = ref(t('agent.tools.notSelected'))
const notSelectedList = ref<any[]>([]) const notSelectedList = ref<any[]>([])
const selectedList = ref<any[]>([]) const selectedList = ref<any[]>([])
@@ -191,7 +192,7 @@ function handleJsonChange(key: string, value: string, field: any) {
} }
} }
catch { catch {
message.alert('JSON格式错误') message.alert(t('agent.tools.jsonFormatError'))
} }
} }
@@ -220,7 +221,7 @@ function goBack() {
// 复制MCP地址 // 复制MCP地址
function copyMcpAddress() { function copyMcpAddress() {
if (!mcpAddress.value) { if (!mcpAddress.value) {
message.alert('暂无MCP地址可复制') message.alert(t('agent.tools.noMcpAddressToCopy'))
return return
} }
@@ -228,11 +229,11 @@ function copyMcpAddress() {
data: mcpAddress.value, data: mcpAddress.value,
showToast: false, showToast: false,
success: () => { success: () => {
message.alert('MCP地址已复制到剪贴板') message.alert(t('agent.tools.mcpAddressCopied'))
}, },
fail: () => { fail: () => {
message.alert('复制失败,请重试') message.alert(t('agent.tools.copyFailed'))
}, },
}) })
} }
@@ -248,7 +249,7 @@ function getFieldDisplayValue(field: any, value: any) {
function getFieldRemark(field: any) { function getFieldRemark(field: any) {
let description = field.label || '' let description = field.label || ''
if (field.default) { if (field.default) {
description += `默认值${field.default}` description += `${t('agent.tools.defaultValue')}${field.default}`
} }
return description return description
} }
@@ -284,7 +285,7 @@ onMounted(async () => {
<!-- 内置插件区域 --> <!-- 内置插件区域 -->
<view class="mt-[20rpx] flex flex-1 flex-col"> <view class="mt-[20rpx] flex flex-1 flex-col">
<view class="text-[32rpx] text-[#333] font-medium"> <view class="text-[32rpx] text-[#333] font-medium">
内置插件 {{ t('agent.tools.builtInPlugins') }}
</view> </view>
<view <view
class="mt-[20rpx] box-border flex flex-1 flex-col rounded-[10rpx] bg-white p-[20rpx]" class="mt-[20rpx] box-border flex flex-1 flex-col rounded-[10rpx] bg-white p-[20rpx]"
@@ -299,7 +300,7 @@ onMounted(async () => {
<view class="mt-[20rpx] flex-1 overflow-hidden"> <view class="mt-[20rpx] flex-1 overflow-hidden">
<!-- 未选插件 --> <!-- 未选插件 -->
<scroll-view <scroll-view
v-if="currentSegmented === '未选'" v-if="currentSegmented === t('agent.tools.notSelected')"
class="max-h-[600rpx] bg-transparent" class="max-h-[600rpx] bg-transparent"
scroll-y scroll-y
> >
@@ -307,7 +308,7 @@ onMounted(async () => {
v-if="notSelectedList.length === 0" v-if="notSelectedList.length === 0"
class="h-[400rpx] flex items-center justify-center" class="h-[400rpx] flex items-center justify-center"
> >
<wd-status-tip image="content" tip="暂无更多插件" /> <wd-status-tip image="content" tip="{{ t('agent.tools.noMorePlugins') }}" />
</view> </view>
<view v-else class="p-[20rpx] space-y-[20rpx]"> <view v-else class="p-[20rpx] space-y-[20rpx]">
<view <view
@@ -343,7 +344,7 @@ onMounted(async () => {
v-if="selectedList.length === 0" v-if="selectedList.length === 0"
class="h-[400rpx] flex items-center justify-center" class="h-[400rpx] flex items-center justify-center"
> >
<wd-status-tip image="content" tip="请选择插件功能" /> <wd-status-tip image="content" tip="{{ t('agent.tools.pleaseSelectPlugin') }}" />
</view> </view>
<view v-else class="p-[20rpx] space-y-[20rpx]"> <view v-else class="p-[20rpx] space-y-[20rpx]">
<view <view
@@ -359,7 +360,7 @@ onMounted(async () => {
{{ func.name }} {{ func.name }}
</view> </view>
<view class="text-[24rpx] text-[#1677ff]"> <view class="text-[24rpx] text-[#1677ff]">
点击配置参数 {{ t('agent.tools.clickToConfigure') }}
</view> </view>
</view> </view>
<view class="flex space-x-[20rpx]"> <view class="flex space-x-[20rpx]">
@@ -393,7 +394,7 @@ onMounted(async () => {
<!-- MCP接入点区域 --> <!-- MCP接入点区域 -->
<view class="mt-[20rpx] flex flex-1 flex-col"> <view class="mt-[20rpx] flex flex-1 flex-col">
<view class="text-[32rpx] text-[#333] font-medium"> <view class="text-[32rpx] text-[#333] font-medium">
mcp接入点 {{ t('agent.tools.mcpEndpoint') }}
</view> </view>
<view <view
class="mt-[20rpx] box-border flex flex-1 flex-col rounded-[10rpx] bg-white p-[20rpx]" class="mt-[20rpx] box-border flex flex-1 flex-col rounded-[10rpx] bg-white p-[20rpx]"
@@ -406,11 +407,11 @@ onMounted(async () => {
class="flex-1 rounded-[10rpx] bg-[#f5f7fb] p-[20rpx]" class="flex-1 rounded-[10rpx] bg-[#f5f7fb] p-[20rpx]"
> >
<view <view
class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white" class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white"
@click="copyMcpAddress" @click="copyMcpAddress"
> >
复制 {{ t('agent.tools.copy') }}
</view> </view>
</view> </view>
<!-- 工具列表 --> <!-- 工具列表 -->
<view class="mt-[20rpx] flex-1 overflow-hidden"> <view class="mt-[20rpx] flex-1 overflow-hidden">
@@ -419,7 +420,7 @@ onMounted(async () => {
v-if="mcpTools && mcpTools.length === 0" v-if="mcpTools && mcpTools.length === 0"
class="h-[400rpx] flex items-center justify-center" class="h-[400rpx] flex items-center justify-center"
> >
<wd-status-tip image="content" tip="暂无工具" /> <wd-status-tip image="content" :tip="t('agent.tools.noTools')" />
</view> </view>
<view v-else class="p-[20rpx]"> <view v-else class="p-[20rpx]">
<view class="flex flex-wrap"> <view class="flex flex-wrap">
@@ -441,7 +442,7 @@ onMounted(async () => {
<!-- 参数编辑弹窗 --> <!-- 参数编辑弹窗 -->
<wd-action-sheet <wd-action-sheet
v-model="showParamDialog" v-model="showParamDialog"
:title="`参数配置 - ${currentFunction?.name || ''}`" :title="`${t('agent.tools.paramConfiguration')} - ${currentFunction?.name || ''}`"
custom-header-class="h-[75vh]" custom-header-class="h-[75vh]"
@close="closeParamEdit" @close="closeParamEdit"
> >
@@ -460,7 +461,7 @@ onMounted(async () => {
class="h-[400rpx] flex items-center justify-center" class="h-[400rpx] flex items-center justify-center"
> >
<text class="text-[28rpx] text-[#999]"> <text class="text-[28rpx] text-[#999]">
{{ currentFunction?.name }} 无需配置参数 {{ currentFunction?.name }} {{ t('agent.tools.noParamsNeeded') }}
</text> </text>
</view> </view>
@@ -490,7 +491,7 @@ onMounted(async () => {
v-model="tempParams[field.key]" v-model="tempParams[field.key]"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text" type="text"
:placeholder="`请输入${field.label}`" :placeholder="`${t('agent.tools.pleaseInput')}${field.label}`"
@input=" @input="
handleParamChange(field.key, $event.detail.value, field) handleParamChange(field.key, $event.detail.value, field)
" "
@@ -499,12 +500,12 @@ onMounted(async () => {
<!-- 数组类型 --> <!-- 数组类型 -->
<view v-else-if="field.type === 'array'"> <view v-else-if="field.type === 'array'">
<text class="mb-[16rpx] block text-[24rpx] text-[#65686f]"> <text class="mb-[16rpx] block text-[24rpx] text-[#65686f]">
每行输入一个项目 {{ t('agent.tools.eachLineOneItem') }}
</text> </text>
<textarea <textarea
v-model="arrayTextCache[field.key]" v-model="arrayTextCache[field.key]"
class="box-border min-h-[200rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border min-h-[200rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
:placeholder="`请输入${field.label},每行一个`" :placeholder="`${t('agent.tools.pleaseInput')}${field.label}${t('agent.tools.eachLineOneItem')}`"
@input=" @input="
handleArrayChange(field.key, $event.detail.value, field) handleArrayChange(field.key, $event.detail.value, field)
" "
@@ -514,12 +515,12 @@ onMounted(async () => {
<!-- JSON类型 --> <!-- JSON类型 -->
<view v-else-if="field.type === 'json'"> <view v-else-if="field.type === 'json'">
<text class="mb-[16rpx] block text-[24rpx] text-[#65686f]"> <text class="mb-[16rpx] block text-[24rpx] text-[#65686f]">
请输入有效的JSON格式 {{ t('agent.tools.pleaseInputValidJson') }}
</text> </text>
<textarea <textarea
v-model="jsonTextCache[field.key]" v-model="jsonTextCache[field.key]"
class="box-border min-h-[300rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] font-mono focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border min-h-[300rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] font-mono focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
placeholder="请输入合法的JSON格式" :placeholder="t('agent.tools.pleaseInputValidJson')"
@blur=" @blur="
handleJsonChange(field.key, $event.detail.value, field) handleJsonChange(field.key, $event.detail.value, field)
" "
@@ -532,7 +533,7 @@ onMounted(async () => {
v-model="tempParams[field.key]" v-model="tempParams[field.key]"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="number" type="number"
:placeholder="`请输入${field.label}`" :placeholder="`${t('agent.tools.pleaseInput')}${field.label}`"
@input=" @input="
handleParamChange( handleParamChange(
field.key, field.key,
@@ -549,10 +550,10 @@ onMounted(async () => {
> >
<view class="flex-1"> <view class="flex-1">
<text class="mb-[8rpx] block text-[28rpx] text-[#232338]"> <text class="mb-[8rpx] block text-[28rpx] text-[#232338]">
启用功能 {{ t('agent.tools.enableFunction') }}
</text> </text>
<text class="block text-[24rpx] text-[#65686f]"> <text class="block text-[24rpx] text-[#65686f]">
开启或关闭此功能 {{ t('agent.tools.toggleFunction') }}
</text> </text>
</view> </view>
<switch <switch
@@ -569,7 +570,7 @@ onMounted(async () => {
v-model="tempParams[field.key]" v-model="tempParams[field.key]"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text" type="text"
:placeholder="`请输入${field.label}`" :placeholder="`${t('agent.tools.pleaseInput')}${field.label}`"
@input=" @input="
handleParamChange(field.key, $event.detail.value, field) handleParamChange(field.key, $event.detail.value, field)
" "
@@ -15,6 +15,7 @@ import { computed, ref } from 'vue'
import { getAudioId, getChatHistory } from '@/api/chat-history/chat-history' import { getAudioId, getChatHistory } from '@/api/chat-history/chat-history'
import { getEnvBaseUrl } from '@/utils' import { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({ defineOptions({
name: 'ChatDetail', name: 'ChatDetail',
@@ -49,7 +50,7 @@ const agentId = ref('')
const currentAgent = computed(() => { const currentAgent = computed(() => {
return { return {
id: agentId.value, id: agentId.value,
agentName: '智能助手', agentName: t('chatHistory.assistantName'),
} }
}) })
@@ -80,7 +81,7 @@ async function loadChatHistory() {
} }
catch (error) { catch (error) {
console.error('获取聊天记录失败:', error) console.error('获取聊天记录失败:', error)
toast.error('获取聊天记录失败') toast.error(t('chatHistory.loadFailed'))
} }
finally { finally {
loading.value = false loading.value = false
@@ -114,10 +115,10 @@ function getMessageContent(message: ChatMessage): string {
function getSpeakerName(message: ChatMessage): string { function getSpeakerName(message: ChatMessage): string {
if (message.chatType === 1) { if (message.chatType === 1) {
const parsed = parseUserMessage(message.content) const parsed = parseUserMessage(message.content)
return parsed ? parsed.speaker : '用户' return parsed ? parsed.speaker : t('chatHistory.userName')
} }
else { else {
return currentAgent.value?.agentName || 'AI助手' return currentAgent.value?.agentName || t('chatHistory.aiAssistantName')
} }
} }
@@ -130,7 +131,7 @@ function formatTime(timeStr: string) {
// 播放音频 // 播放音频
async function playAudio(audioId: string) { async function playAudio(audioId: string) {
if (!audioId) { if (!audioId) {
toast.error('音频ID无效') toast.error(t('chatHistory.invalidAudioId'))
return return
} }
@@ -168,7 +169,7 @@ async function playAudio(audioId: string) {
// 监听播放错误 // 监听播放错误
audioContext.value.onError((error) => { audioContext.value.onError((error) => {
console.error('音频播放失败:', error) console.error('音频播放失败:', error)
toast.error('音频播放失败') toast.error(t('chatHistory.audioPlayFailed'))
playingAudioId.value = null playingAudioId.value = null
if (audioContext.value) { if (audioContext.value) {
audioContext.value.destroy() audioContext.value.destroy()
@@ -181,7 +182,7 @@ async function playAudio(audioId: string) {
} }
catch (error) { catch (error) {
console.error('播放音频失败:', error) console.error('播放音频失败:', error)
toast.error('播放音频失败') toast.error(t('chatHistory.playAudioFailed'))
playingAudioId.value = null playingAudioId.value = null
} }
} }
@@ -194,7 +195,7 @@ onLoad((options) => {
} }
else { else {
console.error('缺少必要参数') console.error('缺少必要参数')
toast.error('页面参数错误') toast.error(t('chatHistory.parameterError'))
} }
}) })
@@ -214,7 +215,7 @@ onUnload(() => {
<view class="w-full bg-white" :style="{ height: `${safeAreaInsets?.top}px` }" /> <view class="w-full bg-white" :style="{ height: `${safeAreaInsets?.top}px` }" />
<!-- 导航栏 --> <!-- 导航栏 -->
<wd-navbar title="聊天详情"> <wd-navbar :title="t('chatHistory.pageTitle')">
<template #left> <template #left>
<wd-icon name="arrow-left" size="18" @click="goBack" /> <wd-icon name="arrow-left" size="18" @click="goBack" />
</template> </template>
@@ -230,7 +231,7 @@ onUnload(() => {
<view v-if="loading" class="flex flex-col items-center justify-center gap-[20rpx] p-[100rpx_0]"> <view v-if="loading" class="flex flex-col items-center justify-center gap-[20rpx] p-[100rpx_0]">
<wd-loading /> <wd-loading />
<text class="text-[28rpx] text-[#65686f]"> <text class="text-[28rpx] text-[#65686f]">
加载中... {{ t('chatHistory.loading') }}
</text> </text>
</view> </view>
@@ -2,6 +2,7 @@
import type { ChatSession } from '@/api/chat-history/types' import type { ChatSession } from '@/api/chat-history/types'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { getChatSessions } from '@/api/chat-history/chat-history' import { getChatSessions } from '@/api/chat-history/chat-history'
import { t } from '@/i18n'
defineOptions({ defineOptions({
name: 'ChatHistory', name: 'ChatHistory',
@@ -53,11 +54,11 @@ const currentAgentId = computed(() => {
// 加载聊天会话列表 // 加载聊天会话列表
async function loadChatSessions(page = 1, isRefresh = false) { async function loadChatSessions(page = 1, isRefresh = false) {
try { try {
console.log('获取聊天会话列表', { page, isRefresh }) console.log(t('chatHistory.getChatSessions'), { page, isRefresh })
// 检查是否有当前选中的智能体 // 检查是否有当前选中的智能体
if (!currentAgentId.value) { if (!currentAgentId.value) {
console.warn('没有选中的智能体') console.warn(t('chatHistory.noSelectedAgent'))
sessionList.value = [] sessionList.value = []
return return
} }
@@ -86,7 +87,7 @@ async function loadChatSessions(page = 1, isRefresh = false) {
currentPage.value = page currentPage.value = page
} }
catch (error) { catch (error) {
console.error('获取聊天会话列表失败:', error) console.error(t('chatHistory.getChatSessionsFailed'), error)
if (page === 1) { if (page === 1) {
sessionList.value = [] sessionList.value = []
} }
@@ -115,48 +116,48 @@ async function loadMore() {
// 格式化时间 // 格式化时间
function formatTime(timeStr: string) { function formatTime(timeStr: string) {
if (!timeStr) if (!timeStr)
return '未知时间' return t('chatHistory.unknownTime')
// 处理时间字符串,确保格式正确 // 处理时间字符串,确保格式正确
const date = new Date(timeStr.replace(' ', 'T')) // 转换为ISO格式 const date = new Date(timeStr.replace(' ', 'T')) // 转换为ISO格式
const now = new Date() const now = new Date()
// 检查日期是否有效 // 检查日期是否有效
if (Number.isNaN(date.getTime())) { if (Number.isNaN(date.getTime())) {
return timeStr // 如果解析失败,直接返回原字符串 return timeStr // 如果解析失败,直接返回原字符串
} }
const diff = now.getTime() - date.getTime() const diff = now.getTime() - date.getTime()
// 小于1分钟 // 小于1分钟
if (diff < 60000) if (diff < 60000)
return '刚刚' return t('chatHistory.justNow')
// 小于1小时 // 小于1小时
if (diff < 3600000) if (diff < 3600000)
return `${Math.floor(diff / 60000)}分钟前` return t('chatHistory.minutesAgo', { minutes: Math.floor(diff / 60000) })
// 小于1天(24小时) // 小于1天(24小时)
if (diff < 86400000) if (diff < 86400000)
return `${Math.floor(diff / 3600000)}小时前` return t('chatHistory.hoursAgo', { hours: Math.floor(diff / 3600000) })
// 小于7天 // 小于7天
if (diff < 604800000) { if (diff < 604800000) {
const days = Math.floor(diff / 86400000) const days = Math.floor(diff / 86400000)
return `${days}天前` return t('chatHistory.daysAgo', { days })
} }
// 超过7天,显示具体日期 // 超过7天,显示具体日期
const year = date.getFullYear() const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0')
const currentYear = now.getFullYear() const currentYear = now.getFullYear()
// 如果是当前年份,不显示年份 // 如果是当前年份,不显示年份
if (year === currentYear) { if (year === currentYear) {
return `${month}-${day}` return `${month}-${day}`
} }
return `${year}-${month}-${day}` return `${year}-${month}-${day}`
} }
@@ -186,8 +187,8 @@ defineExpose({
<view v-if="loading && sessionList.length === 0" class="loading-container"> <view v-if="loading && sessionList.length === 0" class="loading-container">
<wd-loading color="#336cff" /> <wd-loading color="#336cff" />
<text class="loading-text"> <text class="loading-text">
加载中... {{ t('chatHistory.loading') }}
</text> </text>
</view> </view>
<!-- 会话列表 --> <!-- 会话列表 -->
@@ -204,7 +205,7 @@ defineExpose({
<view class="session-info"> <view class="session-info">
<view class="session-header"> <view class="session-header">
<text class="session-title"> <text class="session-title">
对话记录 {{ session.sessionId.substring(0, 8) }}... {{ t('chatHistory.conversationRecord') }} {{ session.sessionId.substring(0, 8) }}...
</text> </text>
<text class="session-time"> <text class="session-time">
{{ formatTime(session.createdAt) }} {{ formatTime(session.createdAt) }}
@@ -212,7 +213,7 @@ defineExpose({
</view> </view>
<view class="session-meta"> <view class="session-meta">
<text class="chat-count"> <text class="chat-count">
{{ session.chatCount }} 条对话 {{ t('chatHistory.totalChats', { count: session.chatCount }) }}
</text> </text>
</view> </view>
</view> </view>
@@ -225,14 +226,14 @@ defineExpose({
<view v-if="loadingMore" class="loading-more"> <view v-if="loadingMore" class="loading-more">
<wd-loading color="#336cff" size="24" /> <wd-loading color="#336cff" size="24" />
<text class="loading-more-text"> <text class="loading-more-text">
加载中... {{ t('chatHistory.loading') }}
</text> </text>
</view> </view>
<!-- 没有更多数据 --> <!-- 没有更多数据 -->
<view v-else-if="!hasMore && sessionList.length > 0" class="no-more"> <view v-else-if="!hasMore && sessionList.length > 0" class="no-more">
<text class="no-more-text"> <text class="no-more-text">
没有更多数据了 {{ t('chatHistory.noMoreData') }}
</text> </text>
</view> </view>
</view> </view>
@@ -241,11 +242,11 @@ defineExpose({
<view v-else-if="!loading" class="empty-state"> <view v-else-if="!loading" class="empty-state">
<wd-icon name="chat" custom-class="empty-icon" /> <wd-icon name="chat" custom-class="empty-icon" />
<text class="empty-text"> <text class="empty-text">
暂无聊天记录 {{ t('chatHistory.noChatRecords') }}
</text> </text>
<text class="empty-desc"> <text class="empty-desc">
与智能体的对话记录会显示在这里 {{ t('chatHistory.chatRecordsDescription') }}
</text> </text>
</view> </view>
</view> </view>
</template> </template>
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { t } from '@/i18n'
// 类型定义 // 类型定义
interface WiFiNetwork { interface WiFiNetwork {
@@ -53,7 +54,7 @@ const audioLengthText = computed(() => {
const textBytes = stringToBytes(dataStr) const textBytes = stringToBytes(dataStr)
const totalBits = (START_BYTES.length + textBytes.length + 1 + END_BYTES.length) * 8 const totalBits = (START_BYTES.length + textBytes.length + 1 + END_BYTES.length) * 8
const duration = Math.ceil(totalBits / BIT_RATE) const duration = Math.ceil(totalBits / BIT_RATE)
return `${duration}` return `${t('deviceConfig.about')}${duration}${t('deviceConfig.seconds')}`
}) })
// 字符串转字节数组 - uniapp兼容版本 // 字符串转字节数组 - uniapp兼容版本
@@ -227,15 +228,15 @@ async function generateAndPlay() {
generating.value = true generating.value = true
try { try {
console.log('生成超声波配网音频...') console.log(t('deviceConfig.generatingUltrasonicConfigAudio') + '...')
// 准备配网数据 - 参考HTML文件格式 // 准备配网数据 - 参考HTML文件格式
const dataStr = `${props.selectedNetwork.ssid}\n${props.password}` const dataStr = `${props.selectedNetwork.ssid}\n${props.password}`
const textBytes = stringToBytes(dataStr) const textBytes = stringToBytes(dataStr)
const fullBytes = [...START_BYTES, ...textBytes, checksum(textBytes), ...END_BYTES] const fullBytes = [...START_BYTES, ...textBytes, checksum(textBytes), ...END_BYTES]
console.log('配网数据:', { ssid: props.selectedNetwork.ssid, password: props.password }) console.log(t('deviceConfig.configData') + ':', { ssid: props.selectedNetwork.ssid, password: props.password })
console.log('数据字节长度:', textBytes.length) console.log(t('deviceConfig.dataBytesLength') + ':', textBytes.length)
// 转换为比特流 // 转换为比特流
let bits: number[] = [] let bits: number[] = []
@@ -243,7 +244,7 @@ async function generateAndPlay() {
bits = bits.concat(toBits(b)) bits = bits.concat(toBits(b))
}) })
console.log('比特流长度:', bits.length) console.log(t('deviceConfig.bitStreamLength') + ':', bits.length)
// AFSK调制 - 减少采样率降低文件大小 // AFSK调制 - 减少采样率降低文件大小
const reducedSampleRate = 22050 // 降低采样率 const reducedSampleRate = 22050 // 降低采样率
@@ -266,19 +267,19 @@ async function generateAndPlay() {
const base64 = arrayBufferToBase64(wavBuffer) const base64 = arrayBufferToBase64(wavBuffer)
const dataUri = `data:audio/wav;base64,${base64}` const dataUri = `data:audio/wav;base64,${base64}`
console.log('base64长度:', base64.length, '约', Math.round(base64.length / 1024), 'KB') console.log(t('deviceConfig.base64Length') + ':', base64.length, t('deviceConfig.about'), Math.round(base64.length / 1024), 'KB')
// 检查数据大小 // 检查数据大小
if (base64.length > 1024 * 1024) { // 超过1MB if (base64.length > 1024 * 1024) { // 超过1MB
throw new Error('音频文件过大,请缩短SSID或密码长度') throw new Error(t('deviceConfig.audioFileTooLarge'))
} }
audioFilePath.value = dataUri audioFilePath.value = dataUri
audioGenerated.value = true audioGenerated.value = true
console.log('音频生成成功,比特流长度:', bits.length, '采样点数:', floatBuf.length) console.log(t('deviceConfig.audioGenerationSuccess') + ',比特流长度:', bits.length, t('deviceConfig.samplePoints') + ':', floatBuf.length)
toast.success('声波生成成功') toast.success(t('deviceConfig.soundWaveGenerationSuccess'))
// 延迟播放 // 延迟播放
setTimeout(async () => { setTimeout(async () => {
@@ -286,8 +287,8 @@ async function generateAndPlay() {
}, 800) // 增加延迟时间 }, 800) // 增加延迟时间
} }
catch (error) { catch (error) {
console.error('音频生成失败:', error) console.error(t('deviceConfig.audioGenerationFailed') + ':', error)
toast.error(`声波生成失败: ${error.message || error}`) toast.error(`${t('deviceConfig.soundWaveGenerationFailed')}: ${error.message || error}`)
} }
finally { finally {
generating.value = false generating.value = false
@@ -344,7 +345,7 @@ function buildWavOptimized(pcm: Uint8Array, sampleRate: number): ArrayBuffer {
// 播放音频 // 播放音频
async function playAudio() { async function playAudio() {
if (!audioFilePath.value) { if (!audioFilePath.value) {
toast.error('请先生成音频') toast.error(t('deviceConfig.pleaseGenerateAudioFirst'))
return return
} }
@@ -356,7 +357,7 @@ async function playAudio() {
await new Promise(resolve => setTimeout(resolve, 200)) await new Promise(resolve => setTimeout(resolve, 200))
playing.value = true playing.value = true
console.log('开始播放超声波配网音频') console.log(t('deviceConfig.startPlayingUltrasonicConfigAudio'))
// 创建新的音频上下文 // 创建新的音频上下文
const innerAudioContext = uni.createInnerAudioContext() const innerAudioContext = uni.createInnerAudioContext()
@@ -370,12 +371,12 @@ async function playAudio() {
// 简化的事件监听 // 简化的事件监听
innerAudioContext.onPlay(() => { innerAudioContext.onPlay(() => {
console.log('超声波音频开始播放') console.log(t('deviceConfig.ultrasonicAudioStartedPlaying'))
toast.success('开始播放配网声波') toast.success(t('deviceConfig.startPlayingConfigSoundWave'))
}) })
innerAudioContext.onEnded(() => { innerAudioContext.onEnded(() => {
console.log('超声波音频播放结束') console.log(t('deviceConfig.ultrasonicAudioPlaybackEnded'))
if (!autoLoop.value) { if (!autoLoop.value) {
playing.value = false playing.value = false
cleanupAudio() cleanupAudio()
@@ -383,18 +384,18 @@ async function playAudio() {
}) })
innerAudioContext.onError((error) => { innerAudioContext.onError((error) => {
console.error('音频播放失败:', error) console.error(t('deviceConfig.audioPlaybackFailed') + ':', error)
playing.value = false playing.value = false
let errorMsg = '音频播放失败' let errorMsg = t('deviceConfig.audioPlaybackFailed')
if (error.errCode === -99) { if (error.errCode === -99) {
errorMsg = '音频资源繁忙,请稍后重试' errorMsg = t('deviceConfig.audioResourceBusy')
} }
else if (error.errCode === 10004) { else if (error.errCode === 10004) {
errorMsg = '音频格式不支持,可能是data URI问题' errorMsg = t('deviceConfig.audioFormatNotSupported')
} }
else if (error.errCode === 10003) { else if (error.errCode === 10003) {
errorMsg = '音频文件错误' errorMsg = t('deviceConfig.audioFileError')
} }
toast.error(errorMsg) toast.error(errorMsg)
@@ -416,10 +417,10 @@ async function playAudio() {
}, 300) }, 300)
} }
catch (error) { catch (error) {
console.error('播放音频异常:', error) console.error(t('deviceConfig.audioPlaybackError') + ':', error)
playing.value = false playing.value = false
await cleanupAudio() await cleanupAudio()
toast.error(`播放失败: ${error.message}`) toast.error(`${t('deviceConfig.playbackFailed')}: ${error.message}`)
} }
} }
@@ -429,10 +430,10 @@ async function cleanupAudio() {
try { try {
audioContext.value.pause() audioContext.value.pause()
audioContext.value.destroy() audioContext.value.destroy()
console.log('清理音频上下文') console.log(t('deviceConfig.cleaningUpAudioContext'))
} }
catch (e) { catch (e) {
console.log('清理音频上下文失败:', e) console.log(t('deviceConfig.cleaningUpAudioContextFailed') + ':', e)
} }
finally { finally {
audioContext.value = null audioContext.value = null
@@ -445,8 +446,8 @@ async function stopAudio() {
playing.value = false playing.value = false
await cleanupAudio() await cleanupAudio()
console.log('停止播放超声波音频') console.log(t('deviceConfig.stoppedPlayingUltrasonicAudio'))
toast.success('已停止播放') toast.success(t('deviceConfig.stoppedPlaying'))
} }
</script> </script>
@@ -456,18 +457,18 @@ async function stopAudio() {
<view v-if="props.selectedNetwork" class="selected-network"> <view v-if="props.selectedNetwork" class="selected-network">
<view class="network-info"> <view class="network-info">
<view class="network-name"> <view class="network-name">
选中网络: {{ props.selectedNetwork.ssid }} {{ t('deviceConfig.selectedNetwork') }}: {{ props.selectedNetwork.ssid }}
</view> </view>
<view class="network-details"> <view class="network-details">
<text class="network-signal"> <text class="network-signal">
信号: {{ props.selectedNetwork.rssi }}dBm {{ t('deviceConfig.signal') }}: {{ props.selectedNetwork.rssi }}dBm
</text> </text>
<text class="network-security"> <text class="network-security">
{{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }} {{ props.selectedNetwork.authmode === 0 ? t('deviceConfig.openNetwork') : t('deviceConfig.encryptedNetwork') }}
</text> </text>
</view> </view>
<view v-if="props.password" class="network-password"> <view v-if="props.password" class="network-password">
密码: {{ '*'.repeat(props.password.length) }} {{ t('deviceConfig.password') }}: {{ '*'.repeat(props.password.length) }}
</view> </view>
</view> </view>
</view> </view>
@@ -482,81 +483,81 @@ async function stopAudio() {
:disabled="!canGenerate" :disabled="!canGenerate"
@click="generateAndPlay" @click="generateAndPlay"
> >
{{ generating ? '生成中...' : '🎵 生成并播放声波' }} {{ generating ? t('deviceConfig.generating') : '🎵 ' + t('deviceConfig.generateAndPlaySoundWave') }}
</wd-button> </wd-button>
<wd-button <wd-button
v-if="audioGenerated" v-if="audioGenerated"
type="success" type="success"
size="large" size="large"
block block
:loading="playing" :loading="playing"
@click="playAudio" @click="playAudio"
> >
{{ playing ? '播放中...' : '🔊 播放声波' }} {{ playing ? t('deviceConfig.playing') : '🔊 ' + t('deviceConfig.playSoundWave') }}
</wd-button> </wd-button>
<wd-button <wd-button
v-if="playing" v-if="playing"
type="warning" type="warning"
size="large" size="large"
block block
@click="stopAudio" @click="stopAudio"
> >
停止播放 {{ t('deviceConfig.stopPlaying') }}
</wd-button> </wd-button>
</view> </view>
<!-- 音频控制选项 --> <!-- 音频控制选项 -->
<view v-if="audioGenerated" class="audio-options"> <view v-if="audioGenerated" class="audio-options">
<view class="option-item"> <view class="option-item">
<wd-checkbox v-model="autoLoop"> <wd-checkbox v-model="autoLoop">
自动循环播放声波 {{ t('deviceConfig.autoLoopPlaySoundWave') }}
</wd-checkbox> </wd-checkbox>
</view> </view>
</view> </view>
<!-- 音频播放器 --> <!-- 音频播放器 -->
<view v-if="audioGenerated" class="audio-player"> <view v-if="audioGenerated" class="audio-player">
<view class="player-info"> <view class="player-info">
<text class="audio-title"> <text class="audio-title">
配网音频文件 {{ t('deviceConfig.configAudioFile') }}
</text> </text>
<text class="audio-duration"> <text class="audio-duration">
时长: {{ audioLengthText }} {{ t('deviceConfig.duration') }}: {{ audioLengthText }}
</text> </text>
</view> </view>
</view> </view>
<!-- 使用说明 --> <!-- 使用说明 -->
<view class="help-section"> <view class="help-section">
<view class="help-title"> <view class="help-title">
超声波配网说明 {{ t('deviceConfig.ultrasonicConfigInstructions') }}
</view>
<view class="help-content">
<text class="help-item">
1. {{ t('deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered') }}
</text>
<text class="help-item">
2. {{ t('deviceConfig.clickGenerateAndPlaySoundWave') }}
</text>
<text class="help-item">
3. {{ t('deviceConfig.bringPhoneCloseToXiaozhiDevice') }}
</text>
<text class="help-item">
4. {{ t('deviceConfig.duringAudioPlaybackXiaozhiWillReceive') }}
</text>
<text class="help-item">
5. {{ t('deviceConfig.afterConfigSuccessDeviceWillConnect') }}
</text>
<text class="help-tip">
{{ t('deviceConfig.usesAfskModulation') }}
</text>
<text class="help-tip">
{{ t('deviceConfig.ensureModeratePhoneVolume') }}
</text>
</view>
</view> </view>
<view class="help-content">
<text class="help-item">
1. 确保已选择WiFi网络并输入密码
</text>
<text class="help-item">
2. 点击生成并播放声波系统会将配网信息编码为音频
</text>
<text class="help-item">
3. 将手机靠近xiaozhi设备距离1-2
</text>
<text class="help-item">
4. 音频播放时xiaozhi会接收并解码配网信息
</text>
<text class="help-item">
5. 配网成功后设备会自动连接WiFi网络
</text>
<text class="help-tip">
使用AFSK调制技术通过1800Hz和1500Hz频率传输数据
</text>
<text class="help-tip">
请确保手机音量适中避免环境噪音干扰
</text>
</view>
</view>
</view> </view>
</template> </template>
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { t } from '@/i18n'
// 类型定义 // 类型定义
interface WiFiNetwork { interface WiFiNetwork {
@@ -44,7 +45,7 @@ async function checkESP32Connection() {
return response.statusCode === 200 return response.statusCode === 200
} }
catch (error) { catch (error) {
console.log('ESP32连接检查失败:', error) console.log(t('deviceConfig.esp32ConnectionCheckFailed') + ':', error)
return false return false
} }
} }
@@ -57,12 +58,12 @@ async function submitConfig() {
// 检查ESP32连接 // 检查ESP32连接
const connected = await checkESP32Connection() const connected = await checkESP32Connection()
if (!connected) { if (!connected) {
toast.error('请先连接xiaozhi热点') toast.error(t('deviceConfig.connectXiaozhiHotspot'))
return return
} }
configuring.value = true configuring.value = true
console.log('开始WiFi配网:', props.selectedNetwork.ssid) console.log(t('deviceConfig.startWifiConfig') + ':', props.selectedNetwork.ssid)
try { try {
const response = await uni.request({ const response = await uni.request({
@@ -81,16 +82,16 @@ async function submitConfig() {
console.log('WiFi配网响应:', response) console.log('WiFi配网响应:', response)
if (response.statusCode === 200 && (response.data as any)?.success) { if (response.statusCode === 200 && (response.data as any)?.success) {
toast.success(`配网成功!设备将连接到 ${props.selectedNetwork.ssid},设备会自动重启。请断开xiaozhi热点连接。`) toast.success(`${t('deviceConfig.configSuccess')}${t('deviceConfig.deviceWillConnectTo')} ${props.selectedNetwork.ssid}${t('deviceConfig.deviceWillRestart')}${t('deviceConfig.pleaseDisconnectXiaozhiHotspot')}`)
} }
else { else {
const errorMsg = (response.data as any)?.error || '配网失败' const errorMsg = (response.data as any)?.error || t('deviceConfig.configFailed')
toast.error(errorMsg) toast.error(errorMsg)
} }
} }
catch (error) { catch (error) {
console.error('WiFi配网失败:', error) console.error(t('deviceConfig.wifiConfigFailed') + ':', error)
toast.error('配网失败,请检查网络连接') toast.error(`${t('deviceConfig.configFailed')}${t('deviceConfig.pleaseCheckNetworkConnection')}`)
} }
finally { finally {
configuring.value = false configuring.value = false
@@ -104,14 +105,14 @@ async function submitConfig() {
<view v-if="props.selectedNetwork" class="selected-network"> <view v-if="props.selectedNetwork" class="selected-network">
<view class="network-info"> <view class="network-info">
<view class="network-name"> <view class="network-name">
选中网络: {{ props.selectedNetwork.ssid }} {{ t('deviceConfig.selectedNetwork') }}: {{ props.selectedNetwork.ssid }}
</view> </view>
<view class="network-details"> <view class="network-details">
<text class="network-signal"> <text class="network-signal">
信号: {{ props.selectedNetwork.rssi }}dBm {{ t('deviceConfig.signal') }}: {{ props.selectedNetwork.rssi }}dBm
</text> </text>
<text class="network-security"> <text class="network-security">
{{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }} {{ props.selectedNetwork.authmode === 0 ? t('deviceConfig.openNetwork') : t('deviceConfig.encryptedNetwork') }}
</text> </text>
</view> </view>
</view> </view>
@@ -127,30 +128,30 @@ async function submitConfig() {
:disabled="!canSubmit" :disabled="!canSubmit"
@click="submitConfig" @click="submitConfig"
> >
{{ configuring ? '配网中...' : '开始WiFi配网' }} {{ configuring ? t('deviceConfig.configuring') : t('deviceConfig.startWifiConfigButton') }}
</wd-button> </wd-button>
</view> </view>
<!-- 使用说明 --> <!-- 使用说明 -->
<view class="help-section"> <view class="help-section">
<view class="help-title"> <view class="help-title">
WiFi配网说明 {{ t('deviceConfig.wifiConfigInstructions') }}
</view> </view>
<view class="help-content"> <view class="help-content">
<text class="help-item"> <text class="help-item">
1. 手机连接xiaozhi热点 (xiaozhi-XXXXXX) 1. {{ t('deviceConfig.phoneConnectXiaozhiHotspot') }} (xiaozhi-XXXXXX)
</text> </text>
<text class="help-item"> <text class="help-item">
2. 选择要配网的目标WiFi网络 2. {{ t('deviceConfig.selectTargetWifiNetwork') }}
</text> </text>
<text class="help-item"> <text class="help-item">
3. 输入WiFi密码如果需要 3. {{ t('deviceConfig.enterWifiPasswordIfNeeded') }}
</text> </text>
<text class="help-item"> <text class="help-item">
4. 点击开始配网等待设备连接 4. {{ t('deviceConfig.clickStartConfigAndWait') }}
</text> </text>
<text class="help-tip"> <text class="help-tip">
配网成功后设备会自动重启并连接目标WiFi {{ t('deviceConfig.afterConfigSuccessDeviceWillRestart') }}
</text> </text>
</view> </view>
</view> </view>
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineEmits, defineExpose, onMounted, ref } from 'vue' import { computed, defineEmits, defineExpose, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { t } from '@/i18n'
// 类型定义 // 类型定义
interface WiFiNetwork { interface WiFiNetwork {
@@ -40,7 +41,7 @@ const selectorExpanded = ref(false)
// 计算属性 // 计算属性
const networkDisplayText = computed(() => { const networkDisplayText = computed(() => {
if (!selectedNetwork.value) if (!selectedNetwork.value)
return '请选择WiFi网络' return t('deviceConfig.selectWifiNetwork')
return selectedNetwork.value.ssid return selectedNetwork.value.ssid
}) })
@@ -55,7 +56,7 @@ async function checkESP32Connection() {
}) })
isConnectedToESP32.value = response.statusCode === 200 isConnectedToESP32.value = response.statusCode === 200
emit('connection-status', isConnectedToESP32.value) emit('connection-status', isConnectedToESP32.value)
console.log('xiaozhi连接状态:', isConnectedToESP32.value) console.log(`${t('deviceConfig.xiaozhi')}连接状态:`, isConnectedToESP32.value)
} }
catch (error) { catch (error) {
isConnectedToESP32.value = false isConnectedToESP32.value = false
@@ -70,9 +71,9 @@ async function checkESP32Connection() {
// 扫描WiFi网络 // 扫描WiFi网络
async function scanWifi() { async function scanWifi() {
if (!isConnectedToESP32.value) { if (!isConnectedToESP32.value) {
toast.error('请先连接xiaozhi热点') toast.error(t('deviceConfig.connectXiaozhiHotspot'))
return return
} }
scanning.value = true scanning.value = true
console.log('开始扫描WiFi网络') console.log('开始扫描WiFi网络')
@@ -84,13 +85,13 @@ async function scanWifi() {
timeout: 10000, timeout: 10000,
}) })
console.log('WiFi扫描响应:', response) console.log(t('deviceConfig.wifiScanResponse') + ':', response)
if (response.statusCode === 200 && response.data) { if (response.statusCode === 200 && response.data) {
const data = response.data as any const data = response.data as any
if (data.success && Array.isArray(data.networks)) { if (data.success && Array.isArray(data.networks)) {
wifiNetworks.value = data.networks wifiNetworks.value = data.networks
console.log(`扫描成功,发现 ${data.networks.length} 个网络`) console.log(`${t('deviceConfig.scanSuccess')},发现 ${data.networks.length} ${t('deviceConfig.networks')}`)
} }
else if (Array.isArray(response.data)) { else if (Array.isArray(response.data)) {
// 兼容旧格式 // 兼容旧格式
@@ -110,8 +111,8 @@ async function scanWifi() {
} }
} }
catch (error) { catch (error) {
console.error('WiFi扫描失败:', error) console.error(t('deviceConfig.wifiScanFailed') + ':', error)
toast.error('扫描失败,请检查xiaozhi连接') toast.error(t('deviceConfig.scanFailedCheckConnection'))
} }
finally { finally {
scanning.value = false scanning.value = false
@@ -124,9 +125,9 @@ async function showNetworkSelector() {
await checkESP32Connection() await checkESP32Connection()
if (!isConnectedToESP32.value) { if (!isConnectedToESP32.value) {
toast.error('请先连接xiaozhi热点') toast.error(t('deviceConfig.connectXiaozhiHotspot'))
return return
} }
selectorExpanded.value = true selectorExpanded.value = true
@@ -172,12 +173,12 @@ function reset() {
// 获取信号强度描述 // 获取信号强度描述
function getSignalStrength(rssi: number): string { function getSignalStrength(rssi: number): string {
if (rssi >= -50) if (rssi >= -50)
return '信号强' return t('deviceConfig.signalStrong')
if (rssi >= -60) if (rssi >= -60)
return '信号良好' return t('deviceConfig.signalGood')
if (rssi >= -70) if (rssi >= -70)
return '信号一般' return t('deviceConfig.signalFair')
return '信号弱' return t('deviceConfig.signalWeak')
} }
// 获取信号强度颜色 // 获取信号强度颜色
@@ -212,81 +213,81 @@ onMounted(() => {
<!-- Xiaozhi连接状态 --> <!-- Xiaozhi连接状态 -->
<view v-if="props.autoConnect" class="connection-status"> <view v-if="props.autoConnect" class="connection-status">
<view v-if="!isConnectedToESP32" class="status-warning"> <view v-if="!isConnectedToESP32" class="status-warning">
<view class="status-content"> <view class="status-content">
<text class="warning-text"> <text class="warning-text">
请先连接xiaozhi热点 (xiaozhi-XXXXXX) {{ t('deviceConfig.connectXiaozhiHotspot') }} (xiaozhi-XXXXXX)
</text> </text>
<wd-button <wd-button
size="small" size="small"
type="primary" type="primary"
:loading="checkingConnection" :loading="checkingConnection"
@click="checkESP32Connection" @click="checkESP32Connection"
> >
{{ checkingConnection ? '检测中...' : '重新检测' }} {{ checkingConnection ? t('deviceConfig.checking') : t('deviceConfig.reCheck') }}
</wd-button> </wd-button>
</view>
</view> </view>
</view>
<view v-else class="status-success"> <view v-else class="status-success">
<view class="status-content"> <view class="status-content">
<text class="success-text"> <text class="success-text">
已连接xiaozhi热点 {{ t('deviceConfig.connectedXiaozhiHotspot') }}
</text> </text>
<wd-button <wd-button
size="small" size="small"
:loading="checkingConnection" :loading="checkingConnection"
@click="checkESP32Connection" @click="checkESP32Connection"
> >
{{ checkingConnection ? '检测中...' : '刷新状态' }} {{ checkingConnection ? t('deviceConfig.checking') : t('deviceConfig.refreshStatus') }}
</wd-button> </wd-button>
</view>
</view> </view>
</view>
</view> </view>
<!-- WiFi网络选择器 --> <!-- WiFi网络选择器 -->
<view class="network-selector"> <view class="network-selector">
<view class="selector-item" @click="showNetworkSelector"> <view class="selector-item" @click="showNetworkSelector">
<text class="selector-label"> <text class="selector-label">
WiFi网络 {{ t('deviceConfig.wifiNetwork') }}
</text> </text>
<text class="selector-value"> <text class="selector-value">
{{ networkDisplayText }} {{ networkDisplayText }}
</text> </text>
<wd-icon name="arrow-right" custom-class="arrow-icon" /> <wd-icon name="arrow-right" custom-class="arrow-icon" />
</view>
</view> </view>
</view>
<!-- 展开的网络列表 --> <!-- 展开的网络列表 -->
<view v-if="selectorExpanded" class="network-list-overlay"> <view v-if="selectorExpanded" class="network-list-overlay">
<view class="network-list-container"> <view class="network-list-container">
<view class="list-header"> <view class="list-header">
<text class="list-title"> <text class="list-title">
选择WiFi网络 {{ t('deviceConfig.selectWifiNetwork') }}
</text> </text>
<view class="list-actions"> <view class="list-actions">
<wd-button <wd-button
type="primary" type="primary"
size="small" size="small"
:loading="scanning" :loading="scanning"
@click="scanWifi" @click="scanWifi"
> >
{{ scanning ? '扫描中...' : '刷新扫描' }} {{ scanning ? t('deviceConfig.scanning') : t('deviceConfig.refreshScan') }}
</wd-button> </wd-button>
<wd-button <wd-button
size="small" size="small"
@click="selectorExpanded = false" @click="selectorExpanded = false"
> >
取消 {{ t('deviceConfig.cancel') }}
</wd-button> </wd-button>
</view>
</view> </view>
</view>
<view class="network-list"> <view class="network-list">
<view v-if="wifiNetworks.length === 0 && !scanning" class="empty-state"> <view v-if="wifiNetworks.length === 0 && !scanning" class="empty-state">
<text class="empty-text"> <text class="empty-text">
暂无WiFi网络 {{ t('deviceConfig.noWifiNetworks') }}
</text> </text>
<text class="empty-tip"> <text class="empty-tip">
请点击刷新扫描 {{ t('deviceConfig.clickRefreshScan') }}
</text> </text>
</view> </view>
@@ -303,16 +304,16 @@ onMounted(() => {
</view> </view>
<view class="wifi-details"> <view class="wifi-details">
<text class="wifi-signal"> <text class="wifi-signal">
信号: {{ network.rssi }}dBm {{ t('deviceConfig.signal') }}: {{ network.rssi }}dBm
</text> </text>
<text class="wifi-channel"> <text class="wifi-channel">
频道: {{ network.channel }} {{ t('deviceConfig.channel') }}: {{ network.channel }}
</text> </text>
</view> </view>
</view> </view>
<view class="wifi-security"> <view class="wifi-security">
<text class="security-icon"> <text class="security-icon">
{{ network.authmode === 0 ? '开放' : '加密' }} {{ network.authmode === 0 ? t('deviceConfig.open') : t('deviceConfig.encrypted') }}
</text> </text>
</view> </view>
</view> </view>
@@ -324,17 +325,17 @@ onMounted(() => {
<!-- 密码输入 --> <!-- 密码输入 -->
<view v-if="selectedNetwork && selectedNetwork.authmode > 0" class="password-section"> <view v-if="selectedNetwork && selectedNetwork.authmode > 0" class="password-section">
<view class="password-item"> <view class="password-item">
<text class="password-label"> <text class="password-label">
网络密码 {{ t('deviceConfig.networkPassword') }}
</text> </text>
<wd-input <wd-input
v-model="password" v-model="password"
placeholder="请输入WiFi密码" :placeholder="t('deviceConfig.enterWifiPassword')"
show-password show-password
clearable clearable
@input="onPasswordChange" @input="onPasswordChange"
/> />
</view> </view>
</view> </view>
</view> </view>
</template> </template>
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { t } from '@/i18n'
import UltrasonicConfig from './components/ultrasonic-config.vue' import UltrasonicConfig from './components/ultrasonic-config.vue'
import WifiConfig from './components/wifi-config.vue' import WifiConfig from './components/wifi-config.vue'
import WifiSelector from './components/wifi-selector.vue' import WifiSelector from './components/wifi-selector.vue'
@@ -33,11 +34,11 @@ const selectedWifiInfo = ref<{
// 配网模式选项 // 配网模式选项
const configTypeOptions = [ const configTypeOptions = [
{ {
name: 'WiFi配网', name: t('deviceConfig.wifiConfig'),
value: 'wifi' as const, value: 'wifi' as const,
}, },
// { // {
// name: '超声波配网', // name: t('deviceConfig.ultrasonicConfig'),
// value: 'ultrasonic' as const, // value: 'ultrasonic' as const,
// }, // },
] ]
@@ -67,28 +68,36 @@ function onNetworkSelected(network: WiFiNetwork | null, password: string) {
function onConnectionStatusChange(connected: boolean) { function onConnectionStatusChange(connected: boolean) {
console.log('ESP32连接状态:', connected) console.log('ESP32连接状态:', connected)
} }
// 在组件挂载后设置导航栏标题
import { onMounted } from 'vue'
onMounted(() => {
uni.setNavigationBarTitle({
title: t('deviceConfig.pageTitle')
})
})
</script> </script>
<template> <template>
<view class="min-h-screen bg-[#f5f7fb]"> <view class="min-h-screen bg-[#f5f7fb]">
<wd-navbar title="设备配网" safe-area-inset-top /> <wd-navbar :title="t('deviceConfig.pageTitle')" safe-area-inset-top />
<view class="box-border px-[20rpx]"> <view class="box-border px-[20rpx]">
<!-- 配网方式选择 --> <!-- 配网方式选择 -->
<view class="pb-[20rpx] first:pt-[20rpx]"> <view class="pb-[20rpx] first:pt-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
配网方式 {{ t('deviceConfig.configMethod') }}
</text> </text>
</view> </view>
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);"> <view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:border-[#336cff] active:bg-[#eef3ff]" @click="showConfigTypeSelector"> <view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:border-[#336cff] active:bg-[#eef3ff]" @click="showConfigTypeSelector">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
配网方式 {{ t('deviceConfig.configMethod') }}
</text> </text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]"> <text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ configType === 'wifi' ? 'WiFi配网' : '超声波配网' }} {{ configType === 'wifi' ? t('deviceConfig.wifiConfig') : t('deviceConfig.ultrasonicConfig') }}
</text> </text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" /> <wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view> </view>
</view> </view>
@@ -96,8 +105,8 @@ function onConnectionStatusChange(connected: boolean) {
<!-- WiFi网络选择 --> <!-- WiFi网络选择 -->
<view class="pb-[20rpx]"> <view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
网络配置 {{ t('deviceConfig.networkConfig') }}
</text> </text>
</view> </view>
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);"> <view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
@@ -139,7 +148,7 @@ function onConnectionStatusChange(connected: boolean) {
<route lang="jsonc" type="page"> <route lang="jsonc" type="page">
{ {
"style": { "style": {
"navigationBarTitleText": "设备配", "navigationBarTitleText": "设备配",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
} }
+34 -33
View File
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni' import { useMessage } from 'wot-design-uni'
import { bindDevice, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device' import { bindDevice, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device'
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({ defineOptions({
name: 'DeviceManage', name: 'DeviceManage',
@@ -91,19 +92,19 @@ function getDeviceTypeName(boardKey: string): string {
// 格式化时间 // 格式化时间
function formatTime(timeStr: string) { function formatTime(timeStr: string) {
if (!timeStr) if (!timeStr)
return '从未连接' return t('device.neverConnected')
const date = new Date(timeStr) const date = new Date(timeStr)
const now = new Date() const now = new Date()
const diff = now.getTime() - date.getTime() const diff = now.getTime() - date.getTime()
if (diff < 60000) if (diff < 60000)
return '刚刚' return t('device.justNow')
if (diff < 3600000) if (diff < 3600000)
return `${Math.floor(diff / 60000)}分钟前` return t('device.minutesAgo', { minutes: Math.floor(diff / 60000) })
if (diff < 86400000) if (diff < 86400000)
return `${Math.floor(diff / 3600000)}小时前` return t('device.hoursAgo', { hours: Math.floor(diff / 3600000) })
if (diff < 604800000) if (diff < 604800000)
return `${Math.floor(diff / 86400000)}天前` return t('device.daysAgo', { days: Math.floor(diff / 86400000) })
return date.toLocaleDateString() return date.toLocaleDateString()
} }
@@ -114,11 +115,11 @@ async function toggleAutoUpdate(device: Device) {
const newStatus = device.autoUpdate === 1 ? 0 : 1 const newStatus = device.autoUpdate === 1 ? 0 : 1
await updateDeviceAutoUpdate(device.id, newStatus) await updateDeviceAutoUpdate(device.id, newStatus)
device.autoUpdate = newStatus device.autoUpdate = newStatus
toast.success(newStatus === 1 ? 'OTA自动升级已开启' : 'OTA自动升级已关闭') toast.success(newStatus === 1 ? t('device.otaAutoUpdateEnabled') : t('device.otaAutoUpdateDisabled'))
} }
catch (error: any) { catch (error: any) {
console.error('更新设备OTA状态失败:', error) console.error('更新设备OTA状态失败:', error)
toast.error('操作失败,请重试') toast.error(t('device.operationFailed'))
} }
} }
@@ -127,21 +128,21 @@ async function handleUnbindDevice(device: Device) {
try { try {
await unbindDevice(device.id) await unbindDevice(device.id)
await loadDeviceList() await loadDeviceList()
toast.success('设备已解绑') toast.success(t('device.deviceUnbound'))
} }
catch (error: any) { catch (error: any) {
console.error('解绑设备失败:', error) console.error('解绑设备失败:', error)
toast.error('解绑失败,请重试') toast.error(t('device.unbindFailed'))
} }
} }
// 确认解绑设备 // 确认解绑设备
function confirmUnbindDevice(device: Device) { function confirmUnbindDevice(device: Device) {
message.confirm({ message.confirm({
title: '解绑设备', title: t('device.unbindDevice'),
msg: `确定要解绑设备 "${device.macAddress}" 吗?`, msg: t('device.confirmUnbindDevice', { macAddress: device.macAddress }),
confirmButtonText: '确定解绑', confirmButtonText: t('device.confirmUnbind'),
cancelButtonText: '取消', cancelButtonText: t('device.cancel'),
}).then(() => { }).then(() => {
handleUnbindDevice(device) handleUnbindDevice(device)
}).catch(() => { }).catch(() => {
@@ -153,18 +154,18 @@ function confirmUnbindDevice(device: Device) {
async function handleBindDevice(code: string) { async function handleBindDevice(code: string) {
try { try {
if (!currentAgentId.value) { if (!currentAgentId.value) {
toast.error('请先选择智能体') toast.error(t('device.pleaseSelectAgent'))
return return
} }
await bindDevice(currentAgentId.value, code.trim()) await bindDevice(currentAgentId.value, code.trim())
await loadDeviceList() await loadDeviceList()
toast.success('设备绑定成功!') toast.success(t('device.deviceBindSuccess'))
} }
catch (error: any) { catch (error: any) {
console.error('绑定设备失败:', error) console.error('绑定设备失败:', error)
const errorMessage = error?.message || '绑定失败,请检查验证码是否正确' const errorMessage = error?.message || '绑定失败,请检查验证码是否正确'
toast.error(errorMessage) toast.error(errorMessage || t('device.bindFailed'))
} }
} }
@@ -172,12 +173,12 @@ async function handleBindDevice(code: string) {
function openBindDialog() { function openBindDialog() {
message message
.prompt({ .prompt({
title: '绑定设备', title: t('device.bindDevice'),
inputPlaceholder: '请输入设备验证码', inputPlaceholder: t('device.enterDeviceCode'),
inputValue: '', inputValue: '',
inputPattern: /^\d{6}$/, inputPattern: /^\d{6}$/,
confirmButtonText: '立即绑定', confirmButtonText: t('device.bindNow'),
cancelButtonText: '取消', cancelButtonText: t('device.cancel'),
}) })
.then(async (result: any) => { .then(async (result: any) => {
if (result.value && String(result.value).trim()) { if (result.value && String(result.value).trim()) {
@@ -219,7 +220,7 @@ defineExpose({
<view v-if="loading && deviceList.length === 0" class="loading-container"> <view v-if="loading && deviceList.length === 0" class="loading-container">
<wd-loading color="#336cff" /> <wd-loading color="#336cff" />
<text class="loading-text"> <text class="loading-text">
加载中... {{ t('device.loading') }}
</text> </text>
</view> </view>
@@ -240,19 +241,19 @@ defineExpose({
<view class="mb-[20rpx]"> <view class="mb-[20rpx]">
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]"> <text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
MAC地址{{ device.macAddress }} {{ t('device.macAddress') }}{{ device.macAddress }}
</text> </text>
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]"> <text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
固件版本{{ device.appVersion }} {{ t('device.firmwareVersion') }}{{ device.appVersion }}
</text> </text>
<text class="block text-[28rpx] text-[#65686f] leading-[1.4]"> <text class="block text-[28rpx] text-[#65686f] leading-[1.4]">
最近对话{{ formatTime(device.lastConnectedAt) }} {{ t('device.lastConnection') }}{{ formatTime(device.lastConnectedAt) }}
</text> </text>
</view> </view>
<view class="flex items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx]"> <view class="flex items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx]">
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
OTA升级 {{ t('device.otaUpdate') }}
</text> </text>
<wd-switch <wd-switch
:model-value="device.autoUpdate === 1" :model-value="device.autoUpdate === 1"
@@ -271,7 +272,7 @@ defineExpose({
@click.stop="confirmUnbindDevice(device)" @click.stop="confirmUnbindDevice(device)"
> >
<wd-icon name="delete" /> <wd-icon name="delete" />
<text>解绑</text> <text>{{ t('device.unbind') }}</text>
</view> </view>
</view> </view>
</template> </template>
@@ -285,10 +286,10 @@ defineExpose({
<view class="flex flex-col items-center justify-center p-[100rpx_40rpx] text-center"> <view class="flex flex-col items-center justify-center p-[100rpx_40rpx] text-center">
<wd-icon name="phone" custom-class="text-[120rpx] text-[#d9d9d9] mb-[32rpx]" /> <wd-icon name="phone" custom-class="text-[120rpx] text-[#d9d9d9] mb-[32rpx]" />
<text class="mb-[16rpx] text-[32rpx] text-[#666666] font-medium"> <text class="mb-[16rpx] text-[32rpx] text-[#666666] font-medium">
暂无设备 {{ t('device.noDevice') }}
</text> </text>
<text class="text-[26rpx] text-[#999999] leading-[1.5]"> <text class="text-[26rpx] text-[#999999] leading-[1.5]">
点击右下角 + 号绑定您的第一个设备 {{ t('device.clickToBindFirstDevice') }}
</text> </text>
</view> </view>
</view> </view>
@@ -0,0 +1,848 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "ForgotPassword"
}
}
</route>
<script lang="ts" setup>
import { computed, onMounted, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useConfigStore } from "@/store";
import { getEnvBaseUrl, sm2Encrypt } from "@/utils";
import { toast } from "@/utils/toast";
// 导入国际化相关功能
import { t, initI18n } from "@/i18n";
// 导入API接口
import { retrievePassword, sendSmsCode } from "@/api/auth";
// 获取屏幕边界到安全区域距离
let safeAreaInsets;
let systemInfo;
// #ifdef MP-WEIXIN
// 微信小程序使用新的API
systemInfo = uni.getWindowInfo();
safeAreaInsets = systemInfo.safeArea
? {
top: systemInfo.safeArea.top,
right: systemInfo.windowWidth - systemInfo.safeArea.right,
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
left: systemInfo.safeArea.left,
}
: null;
// #endif
// #ifndef MP-WEIXIN
// 其他平台继续使用uni API
systemInfo = uni.getSystemInfoSync();
safeAreaInsets = systemInfo.safeAreaInsets;
// #endif
// 表单数据
interface ForgotPasswordData {
mobile: string;
captcha: string;
captchaId: string;
mobileCaptcha: string;
newPassword: string;
confirmPassword: string;
areaCode: string;
}
const formData = ref<ForgotPasswordData>({
mobile: "",
captcha: "",
captchaId: "",
mobileCaptcha: "",
newPassword: "",
confirmPassword: "",
areaCode: "+86",
});
// 验证码图片
const captchaImage = ref("");
const loading = ref(false);
// 获取配置store
const configStore = useConfigStore();
// State for area code action sheet
const showAreaCodeSheet = ref(false);
const selectedAreaCode = ref("+86");
const areaCodeList = computed(() =>
(configStore.config.mobileAreaList || []).map((item) => {
return {
value: item.key,
label: `${item.name} (${item.key})`,
};
})
);
const canSendMobileCaptcha = computed(() => {
const mobile = formData.value.mobile;
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(mobile) && smsCountdown.value === 0;
});
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey;
});
// 短信验证码倒计时
const smsCountdown = ref(0);
const smsLoading = ref(false);
// 打开区号选择弹窗
function openAreaCodeSheet() {
showAreaCodeSheet.value = true;
}
// 选择区号
function selectAreaCode(item: { value: string; label: string }) {
selectedAreaCode.value = item.value;
formData.value.areaCode = item.value;
showAreaCodeSheet.value = false;
}
// 关闭区号选择弹窗
function closeAreaCodeSheet() {
showAreaCodeSheet.value = false;
}
// 生成UUID
function generateUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// 获取图形验证码
async function refreshCaptcha() {
const uuid = generateUUID();
formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
}
// 发送短信验证码
async function handleSendSmsCode() {
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning(t("retrievePassword.inputCorrectMobile"));
return;
}
if (!formData.value.captcha) {
toast.warning(t("retrievePassword.captchaRequired"));
return;
}
try {
smsLoading.value = true;
// 将手机号转换为国际格式
const internationalPhone = formData.value.areaCode + formData.value.mobile;
// 调用发送短信验证码API
await sendSmsCode({
phone: internationalPhone,
captcha: formData.value.captcha,
captchaId: formData.value.captchaId
})
toast.success(t("retrievePassword.captchaSendSuccess"));
// 开始倒计时
smsCountdown.value = 60;
const timer = setInterval(() => {
smsCountdown.value--;
if (smsCountdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
} catch (error: any) {
// 处理验证码错误
if (error.message.includes("请求错误[10067]")) {
toast.warning(t("login.captchaError"));
}
// 发送失败重新获取图形验证码
refreshCaptcha();
} finally {
smsLoading.value = false;
}
}
// 重置密码
async function handleResetPassword() {
// 表单验证
if (!formData.value.mobile) {
toast.warning(t("retrievePassword.mobileRequired"));
return;
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning(t("retrievePassword.inputCorrectMobile"));
return;
}
// 将手机号转换为国际格式
const internationalPhone = formData.value.areaCode + formData.value.mobile;
if (!formData.value.captcha) {
toast.warning(t("retrievePassword.captchaRequired"));
return;
}
if (!formData.value.mobileCaptcha) {
toast.warning(t("retrievePassword.mobileCaptchaRequired"));
return;
}
if (!formData.value.newPassword) {
toast.warning(t("retrievePassword.newPasswordRequired"));
return;
}
if (!formData.value.confirmPassword) {
toast.warning(t("retrievePassword.confirmNewPasswordRequired"));
return;
}
if (formData.value.newPassword !== formData.value.confirmPassword) {
toast.warning(t("retrievePassword.passwordsNotMatch"));
return;
}
try {
loading.value = true;
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
toast.warning(t("sm2.publicKeyNotConfigured"));
return;
}
// 加密密码
let encryptedPassword;
try {
// 拼接图形验证码和新密码进行加密
const captchaAndPassword = formData.value.captcha + formData.value.newPassword;
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword);
} catch (error) {
console.error("密码加密失败:", error);
toast.warning(t("sm2.encryptionFailed"));
return;
}
// 调用重置密码API
await retrievePassword({
phone: internationalPhone,
code: formData.value.mobileCaptcha,
password: encryptedPassword,
captchaId: formData.value.captchaId
})
toast.success(t("retrievePassword.passwordUpdateSuccess"));
// 跳转到登录页
setTimeout(() => {
uni.redirectTo({
url: "/pages/login/index",
});
}, 1000);
} catch (error: any) {
// 处理验证码错误
if (error.message.includes("请求错误[10067]")) {
toast.warning(t("login.captchaError"));
}
// 重置失败重新获取验证码
refreshCaptcha();
} finally {
loading.value = false;
}
}
// 返回登录
function goBack() {
uni.redirectTo({
url: "/pages/login/index",
});
}
// 页面加载时获取验证码
onLoad(() => {
refreshCaptcha();
});
// 组件挂载时确保配置已加载
onMounted(async () => {
if (!configStore.config.name) {
try {
await configStore.fetchPublicConfig();
} catch (error) {
console.error("获取配置失败:", error);
}
}
// 初始化国际化
initI18n();
});
</script>
<template>
<view
class="app-container box-border h-screen w-full"
:style="{ paddingTop: `${safeAreaInsets?.top}px` }"
>
<view class="header">
<view class="back-button" @click="goBack">
<wd-icon name="arrow-left" custom-class="back-icon" />
</view>
<view class="logo-section">
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
<text class="welcome-text">
{{ t("retrievePassword.title") }}
</text>
<text class="subtitle">
{{ t("retrievePassword.subtitle") }}
</text>
</view>
</view>
<view class="form-container">
<view class="form">
<!-- 手机号输入 -->
<view class="input-group">
<view class="input-wrapper mobile-wrapper">
<view class="area-code-selector" @click="openAreaCodeSheet">
<text class="area-code-text">
{{ selectedAreaCode }}
</text>
<wd-icon name="arrow-down" custom-class="area-code-arrow" />
</view>
<view class="mobile-input-wrapper">
<wd-input
v-model="formData.mobile"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.mobilePlaceholder')"
type="number"
:maxlength="11"
/>
</view>
</view>
</view>
<!-- 图形验证码 -->
<view class="input-group">
<view class="input-wrapper captcha-wrapper">
<wd-input
v-model="formData.captcha"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.captchaPlaceholder')"
:maxlength="6"
/>
<view class="captcha-image" @click="refreshCaptcha">
<image :src="captchaImage" class="captcha-img" />
</view>
</view>
</view>
<!-- 短信验证码 -->
<view class="input-group">
<view class="input-wrapper sms-wrapper">
<wd-input
v-model="formData.mobileCaptcha"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.mobileCaptchaPlaceholder')"
:maxlength="6"
/>
<view class="sms-button" @click="handleSendSmsCode">
<text class="sms-text">
{{
smsCountdown > 0
? `${smsCountdown}s`
: t("retrievePassword.getMobileCaptcha")
}}
</text>
</view>
</view>
</view>
<!-- 新密码 -->
<view class="input-group">
<view class="input-wrapper">
<wd-input
v-model="formData.newPassword"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.newPasswordPlaceholder')"
show-password
:maxlength="20"
/>
</view>
</view>
<!-- 确认新密码 -->
<view class="input-group">
<view class="input-wrapper">
<wd-input
v-model="formData.confirmPassword"
custom-class="styled-input"
no-border
:placeholder="t('retrievePassword.confirmNewPasswordPlaceholder')"
show-password
:maxlength="20"
/>
</view>
</view>
<!-- 重置密码按钮 -->
<view class="reset-btn" @click="handleResetPassword">
{{ loading ? t("common.loading") : t("retrievePassword.resetButton") }}
</view>
<!-- 返回登录链接 -->
<view class="back-login-hint" @click="goBack">
<text class="hint-text">
{{ t("retrievePassword.goToLogin") }}
</text>
</view>
</view>
</view>
<!-- 区号选择弹窗 -->
<wd-action-sheet
v-model="showAreaCodeSheet"
:title="t('login.selectCountry')"
:close-on-click-modal="true"
@close="closeAreaCodeSheet"
>
<view class="area-code-sheet">
<scroll-view scroll-y class="area-code-list">
<view
v-for="item in areaCodeList"
:key="item.value"
class="area-code-item"
:class="{ selected: selectedAreaCode === item.value }"
@click="selectAreaCode(item)"
>
<view class="area-info">
<text class="area-name">
{{ item.label }}
</text>
</view>
<wd-icon
v-if="selectedAreaCode === item.value"
name="check"
custom-class="check-icon"
/>
</view>
</scroll-view>
<view class="sheet-footer">
<wd-button
type="primary"
custom-class="confirm-btn"
@click="closeAreaCodeSheet"
>
{{ t("login.confirm") }}
</wd-button>
</view>
</view>
</wd-action-sheet>
</view>
</template>
<style lang="scss" scoped>
.app-container {
background: linear-gradient(145deg, #f5f8fd, #6baaff, #9ebbfc, #f5f8fd);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
min-height: 100vh;
&::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
animation: float 6s ease-in-out infinite;
}
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
}
.header {
flex: 0 0 auto;
min-height: 280rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 15% 0 40rpx 0;
position: relative;
.back-button {
position: absolute;
left: 30rpx;
top: 50rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.back-icon {
font-size: 32rpx;
color: #ffffff;
}
}
.logo-section {
text-align: center;
.logo {
margin-bottom: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}
.welcome-text {
display: block;
color: #ffffff;
font-size: 40rpx;
font-weight: 600;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.subtitle {
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 26rpx;
font-weight: 400;
}
}
}
.form-container {
padding: 0 40rpx 40rpx 40rpx;
flex: 1;
display: flex;
align-items: flex-start;
.form {
width: 100%;
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 40rpx 30rpx 30rpx 30rpx;
backdrop-filter: blur(10rpx);
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.2);
max-height: calc(100vh - 350rpx);
overflow-y: auto;
.input-group {
margin-bottom: 24rpx;
.input-wrapper {
position: relative;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
transition: all 0.3s ease;
display: flex;
align-items: center;
&:focus-within {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
}
&.mobile-wrapper {
padding: 0;
background: transparent;
border: none;
display: flex;
gap: 20rpx;
.area-code-selector {
flex: 0 0 160rpx;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
height: 45rpx;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
}
.area-code-text {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
:deep(.area-code-arrow) {
font-size: 24rpx;
color: #999999;
transition: transform 0.3s ease;
}
}
.mobile-input-wrapper {
flex: 1;
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx 16rpx;
border: 2rpx solid #e9ecef;
transition: all 0.3s ease;
&:focus-within {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
}
}
}
&.captcha-wrapper {
.captcha-image {
margin-left: 20rpx;
width: 150rpx;
height: 60rpx;
border-radius: 8rpx;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: #e9ecef;
border: 1rpx solid #ddd;
.captcha-img {
width: 100%;
height: 100%;
}
}
}
&.sms-wrapper {
.sms-button {
margin-left: 20rpx;
width: 180rpx;
height: 60rpx;
border-radius: 12rpx;
background: #667eea;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
background: #5a6fd8;
transform: scale(0.98);
}
.sms-text {
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
}
}
:deep(.styled-input) {
background: transparent !important;
border: none !important;
outline: none !important;
font-size: 32rpx;
color: #333333;
flex: 1;
&::placeholder {
color: #999999;
font-size: 28rpx;
}
}
}
}
.reset-btn {
width: 100%;
height: 80rpx;
border: none;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
background-color: var(
--wot-button-primary-bg-color,
var(--wot-color-theme, #4d80f0)
);
text-align: center;
line-height: 80rpx;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.back-login-hint {
text-align: center;
.hint-text {
color: #667eea;
font-size: 26rpx;
font-weight: 500;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}
}
// 区号选择弹窗样式
.area-code-sheet {
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
.sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 40rpx 20rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
.sheet-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
}
:deep(.close-icon) {
font-size: 32rpx;
color: #999999;
cursor: pointer;
padding: 10rpx;
&:hover {
color: #333333;
}
}
}
.area-code-list {
max-height: 60vh;
padding: 0 40rpx;
.area-code-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 1rpx solid #f8f9fa;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #f8f9fa;
}
&.selected {
background-color: rgba(102, 126, 234, 0.05);
.area-name {
color: #667eea;
font-weight: 500;
}
.area-code {
color: #667eea;
}
}
.area-info {
display: flex;
flex-direction: column;
gap: 8rpx;
.area-name {
font-size: 32rpx;
color: #333333;
}
.area-code {
font-size: 28rpx;
color: #666666;
}
}
:deep(.check-icon) {
font-size: 32rpx;
color: #667eea;
}
}
}
.sheet-footer {
padding: 30rpx 40rpx 40rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
:deep(.confirm-btn) {
width: 100%;
height: 88rpx;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
}
}
}
</style>
+31 -24
View File
@@ -17,6 +17,7 @@ import { useMessage } from 'wot-design-uni'
import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js' import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
import { createAgent, deleteAgent, getAgentList } from '@/api/agent/agent' import { createAgent, deleteAgent, getAgentList } from '@/api/agent/agent'
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({ defineOptions({
name: 'Home', name: 'Home',
@@ -78,11 +79,11 @@ async function handleCreateAgent(agentName: string) {
await createAgent({ agentName: agentName.trim() }) await createAgent({ agentName: agentName.trim() })
// 创建成功后刷新列表 // 创建成功后刷新列表
pagingRef.value.reload() pagingRef.value.reload()
toast.success(`智能体"${agentName}"创建成功!`) toast.success(`${t('home.agentName')}"${agentName}"${t('message.saveSuccess')}`)
} }
catch (error: any) { catch (error: any) {
console.error('创建智能体失败:', error) console.error('创建智能体失败:', error)
const errorMessage = error?.message || '创建失败,请重试' const errorMessage = error?.message || t('message.saveFail')
toast.error(errorMessage) toast.error(errorMessage)
} }
} }
@@ -93,11 +94,11 @@ async function handleDeleteAgent(agent: Agent) {
await deleteAgent(agent.id) await deleteAgent(agent.id)
// 删除成功后刷新列表 // 删除成功后刷新列表
pagingRef.value.reload() pagingRef.value.reload()
toast.success(`智能体"${agent.agentName}"已删除`) toast.success(`${t('home.agentName')}${t('message.deleteSuccess')}`)
} }
catch (error: any) { catch (error: any) {
console.error('删除智能体失败:', error) console.error('删除智能体失败:', error)
const errorMessage = error?.message || '删除失败,请重试' const errorMessage = error?.message || t('message.deleteFail')
toast.error(errorMessage) toast.error(errorMessage)
} }
} }
@@ -119,13 +120,14 @@ function handleCardClick(agent: Agent) {
function openCreateDialog() { function openCreateDialog() {
message message
.prompt({ .prompt({
title: '创建智能体', title: t('home.dialogTitle'),
msg: '', msg: '',
inputPlaceholder: '例如:客服助手、语音助理、知识问答', inputPlaceholder: t('home.inputPlaceholder'),
inputValue: '', inputValue: '',
inputPattern: /^[\u4E00-\u9FA5a-z0-9\s]{1,50}$/i, inputPattern: /^[\u4E00-\u9FA5a-z0-9\s]{1,50}$/i,
confirmButtonText: '立即创建', inputError: t('home.createError'),
cancelButtonText: '取消', confirmButtonText: t('home.createNow'),
cancelButtonText: t('common.cancel'),
}) })
.then(async (result: any) => { .then(async (result: any) => {
if (result.value && String(result.value).trim()) { if (result.value && String(result.value).trim()) {
@@ -144,12 +146,12 @@ function formatTime(timeStr: string) {
const diff = now.getTime() - date.getTime() const diff = now.getTime() - date.getTime()
if (diff < 60000) if (diff < 60000)
return '刚刚' return t('home.justNow')
if (diff < 3600000) if (diff < 3600000)
return `${Math.floor(diff / 60000)}分钟前` return `${Math.floor(diff / 60000)}${t('home.minutesAgo')}`
if (diff < 86400000) if (diff < 86400000)
return `${Math.floor(diff / 3600000)}小时前` return `${Math.floor(diff / 3600000)}${t('home.hoursAgo')}`
return `${Math.floor(diff / 86400000)}天前` return `${Math.floor(diff / 86400000)}${t('home.daysAgo')}`
} }
// 页面显示时刷新列表 // 页面显示时刷新列表
@@ -159,12 +161,20 @@ onShow(() => {
pagingRef.value.reload() pagingRef.value.reload()
} }
}) })
// 在组件挂载后设置导航栏标题
import { onMounted } from 'vue'
onMounted(() => {
uni.setNavigationBarTitle({
title: t('home.pageTitle')
})
})
</script> </script>
<template> <template>
<z-paging <z-paging
ref="pagingRef" v-model="agentList" :refresher-enabled="true" :auto-show-back-to-top="true" ref="pagingRef" v-model="agentList" :refresher-enabled="true" :auto-show-back-to-top="true"
:loading-more-enabled="false" :show-loading-more="false" :hide-empty-view="false" empty-view-text="暂无智能体" :loading-more-enabled="false" :show-loading-more="false" :hide-empty-view="false" :empty-view-text="t('home.emptyState')"
empty-view-img="" :refresher-threshold="80" :back-to-top-style="{ empty-view-img="" :refresher-threshold="80" :back-to-top-style="{
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: '50%', borderRadius: '50%',
@@ -178,16 +188,13 @@ onShow(() => {
<view class="banner-content"> <view class="banner-content">
<view class="welcome-info"> <view class="welcome-info">
<text class="greeting"> <text class="greeting">
你好小智 {{ t('home.greeting') }}
</text> </text>
<text class="subtitle"> <text class="subtitle">
让我们度过 <text class="highlight"> {{ t('home.subtitle') }} <text class="highlight">
美好的一天 {{ t('home.wonderfulDay') }}
</text> </text>
</text> </text>
<text class="english-subtitle">
Hello, Let's have a wonderful day!
</text>
</view> </view>
<view class="wave-decoration"> <view class="wave-decoration">
<!-- 添加波浪装饰 --> <!-- 添加波浪装饰 -->
@@ -227,13 +234,13 @@ onShow(() => {
<view class="stat-chip"> <view class="stat-chip">
<wd-icon name="phone" custom-class="chip-icon" /> <wd-icon name="phone" custom-class="chip-icon" />
<text class="chip-text"> <text class="chip-text">
设备管理({{ agent.deviceCount }}) {{ t('home.deviceManagement') }}({{ agent.deviceCount }})
</text> </text>
</view> </view>
<view v-if="agent.lastConnectedAt" class="stat-chip"> <view v-if="agent.lastConnectedAt" class="stat-chip">
<wd-icon name="time" custom-class="chip-icon" /> <wd-icon name="time" custom-class="chip-icon" />
<text class="chip-text"> <text class="chip-text">
最近对话{{ formatTime(agent.lastConnectedAt) }} {{ t('home.lastConversation') }}{{ formatTime(agent.lastConnectedAt) }}
</text> </text>
</view> </view>
</view> </view>
@@ -247,7 +254,7 @@ onShow(() => {
<view class="swipe-actions"> <view class="swipe-actions">
<view class="action-btn delete-btn" @click.stop="handleDeleteAgent(agent)"> <view class="action-btn delete-btn" @click.stop="handleDeleteAgent(agent)">
<wd-icon name="delete" /> <wd-icon name="delete" />
<text>删除</text> <text>{{ t('home.delete') }}</text>
</view> </view>
</view> </view>
</template> </template>
@@ -260,10 +267,10 @@ onShow(() => {
<view class="empty-state"> <view class="empty-state">
<wd-icon name="robot" custom-class="empty-icon" /> <wd-icon name="robot" custom-class="empty-icon" />
<text class="empty-text"> <text class="empty-text">
暂无智能体 {{ t('home.emptyState') }}
</text> </text>
<text class="empty-desc"> <text class="empty-desc">
点击右下角 + 号创建您的第一个智能体 {{ t('home.createFirstAgent') }}
</text> </text>
</view> </view>
</template> </template>
+218 -71
View File
@@ -3,18 +3,23 @@
"layout": "default", "layout": "default",
"style": { "style": {
"navigationStyle": "custom", "navigationStyle": "custom",
"navigationBarTitleText": "登陆" "navigationBarTitleText": "Login"
} }
} }
</route> </route>
<script lang="ts" setup> <script lang="ts" setup>
import type { LoginData } from '@/api/auth' import type { LoginData } from '@/api/auth';
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue';
import { login } from '@/api/auth' import { login } from '@/api/auth';
import { useConfigStore } from '@/store' import { useConfigStore } from '@/store';
import { getEnvBaseUrl } from '@/utils' import { getEnvBaseUrl } from '@/utils';
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast';
// 导入国际化相关功能
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n';
import type { Language } from '@/store/lang';
// 导入SM2加密工具
import { sm2Encrypt } from '@/utils'
// 获取屏幕边界到安全区域距离 // 获取屏幕边界到安全区域距离
let safeAreaInsets let safeAreaInsets
@@ -39,7 +44,7 @@ systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets safeAreaInsets = systemInfo.safeAreaInsets
// #endif // #endif
// 表单数据 // 表单数据
const formData = ref<LoginData>({ const formData = ref({
username: '', username: '',
password: '', password: '',
captcha: '', captcha: '',
@@ -73,6 +78,11 @@ const areaCodeList = computed(() => {
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }] return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
}) })
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey
})
// 切换登录方式 // 切换登录方式
function toggleLoginType() { function toggleLoginType() {
loginType.value = loginType.value === 'username' ? 'mobile' : 'username' loginType.value = loginType.value === 'username' ? 'mobile' : 'username'
@@ -106,6 +116,13 @@ function goToRegister() {
}) })
} }
// 跳转到忘记密码页面
function goToForgotPassword() {
uni.navigateTo({
url: '/pages/forgot-password/index',
})
}
// 生成UUID // 生成UUID
function generateUUID() { function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
@@ -126,9 +143,9 @@ function goToServerSetting() {
// 获取验证码 // 获取验证码
async function refreshCaptcha() { async function refreshCaptcha() {
const uuid = generateUUID() const uuid = generateUUID();
formData.value.captchaId = uuid formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}` captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
} }
// 登录 // 登录
@@ -136,40 +153,64 @@ async function handleLogin() {
// 表单验证 // 表单验证
if (loginType.value === 'username') { if (loginType.value === 'username') {
if (!formData.value.username) { if (!formData.value.username) {
toast.warning('请输入用户名') toast.warning(t('login.enterUsername'))
return return
} }
} }
else { else {
if (!formData.value.mobile) { if (!formData.value.mobile) {
toast.warning('请输入手机号') toast.warning(t('login.enterPhone'))
return return
} }
// 手机号格式验证 // 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/ const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) { if (!phoneRegex.test(formData.value.mobile)) {
toast.warning('请输入正确的手机号') toast.warning(t('login.enterPhone'))
return return
} }
} }
if (!formData.value.password) { if (!formData.value.password) {
toast.warning('请输入密码') toast.warning(t('login.enterPassword'))
return return
} }
if (!formData.value.captcha) { if (!formData.value.captcha) {
toast.warning('请输入验证码') toast.warning(t('login.enterCaptcha'))
return
}
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
toast.warning(t('sm2.publicKeyNotConfigured'))
return return
} }
try { try {
loading.value = true loading.value = true
// 加密密码
let encryptedPassword
try {
// 拼接验证码和密码
const captchaAndPassword = formData.value.captcha + formData.value.password
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
} catch (error) {
console.error('密码加密失败:', error)
toast.warning(t('sm2.encryptionFailed'))
return
}
// 构建登录数据 // 构建登录数据
const loginData = { ...formData.value } const loginData: LoginData = {
username: '',
password: encryptedPassword,
captchaId: formData.value.captchaId
}
// 如果是手机号登录,将区号+手机号拼接到username字段 // 如果是手机号登录,将区号+手机号拼接到username字段
if (loginType.value === 'mobile') { if (loginType.value === 'mobile') {
loginData.username = `${selectedAreaCode.value}${formData.value.mobile}` loginData.username = `${selectedAreaCode.value}${formData.value.mobile}`
} else {
loginData.username = formData.value.username
} }
const response = await login(loginData) const response = await login(loginData)
@@ -177,7 +218,7 @@ async function handleLogin() {
uni.setStorageSync('token', response.token) uni.setStorageSync('token', response.token)
uni.setStorageSync('expire', response.expire) uni.setStorageSync('expire', response.expire)
toast.success('登录成功') toast.success(t('message.loginSuccess'))
// 跳转到主页 // 跳转到主页
setTimeout(() => { setTimeout(() => {
@@ -189,6 +230,14 @@ async function handleLogin() {
catch (error: any) { catch (error: any) {
// 登录失败重新获取验证码 // 登录失败重新获取验证码
refreshCaptcha() refreshCaptcha()
// 处理验证码错误 - 从error.message中解析错误码
if (error.message.includes('请求错误[10067]')) {
toast.warning(t('login.captchaError'))
}
// 处理账号或密码错误
else if (error.message.includes('请求错误[10004]')) {
toast.warning(t('message.passwordError'))
}
} }
finally { finally {
loading.value = false loading.value = false
@@ -200,14 +249,26 @@ onLoad(() => {
refreshCaptcha() refreshCaptcha()
}) })
// 语言切换相关
const showLanguageSheet = ref(false)
const supportedLanguages = getSupportedLanguages()
// 初始化国际化
initI18n()
// 切换语言
function handleLanguageChange(lang: Language) {
changeLanguage(lang)
showLanguageSheet.value = false
}
// 组件挂载时确保配置已加载 // 组件挂载时确保配置已加载
onMounted(async () => { onMounted(async () => {
if (!configStore.config.name) { if (!configStore.config.name) {
try { try {
await configStore.fetchPublicConfig() await configStore.fetchPublicConfig()
} } catch (error) {
catch (error) { console.error(t('login.fetchConfigError'), error)
console.error('获取配置失败:', error)
} }
} }
}) })
@@ -219,21 +280,25 @@ onMounted(async () => {
<view class="logo-section"> <view class="logo-section">
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" /> <wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
<text class="welcome-text"> <text class="welcome-text">
欢迎回来 {{ t('login.welcomeBack') }}
</text> </text>
<text class="subtitle"> <text class="subtitle">
请登录您的账户 {{ t('login.pleaseLogin') }}
</text> </text>
</view> </view>
</view> </view>
<!-- 右上角服务端设置按钮 --> <!-- 右上角按钮 -->
<view <view class="top-right-buttons" :style="{ top: `${safeAreaInsets?.top + 10}px` }">
class="server-btn" <!-- 语言切换按钮 -->
:style="{ top: `${safeAreaInsets?.top + 10}px` }" <view class="lang-btn" @click="showLanguageSheet = true">
@click="goToServerSetting" <text class="lang-text-icon">{{ t('login.selectLanguageTip') }}</text>
> </view>
<wd-icon name="setting" custom-class="server-icon" />
<!-- 服务端设置按钮 -->
<view class="server-btn" @click="goToServerSetting">
<wd-icon name="setting" custom-class="server-icon" />
</view>
</view> </view>
<view class="form-container"> <view class="form-container">
@@ -253,7 +318,7 @@ onMounted(async () => {
v-model="formData.mobile" v-model="formData.mobile"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入手机号码" :placeholder="t('login.enterPhone')"
type="number" type="number"
:maxlength="11" :maxlength="11"
/> />
@@ -270,7 +335,7 @@ onMounted(async () => {
v-model="formData.username" v-model="formData.username"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入用户名" :placeholder="t('login.enterUsername')"
/> />
</view> </view>
</view> </view>
@@ -282,7 +347,7 @@ onMounted(async () => {
v-model="formData.password" v-model="formData.password"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入密码" :placeholder="t('login.enterPassword')"
clearable clearable
show-password show-password
:maxlength="20" :maxlength="20"
@@ -296,7 +361,7 @@ onMounted(async () => {
v-model="formData.captcha" v-model="formData.captcha"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入验证码" :placeholder="t('login.enterCaptcha')"
:maxlength="6" :maxlength="6"
/> />
<view class="captcha-image" @click="refreshCaptcha"> <view class="captcha-image" @click="refreshCaptcha">
@@ -304,27 +369,25 @@ onMounted(async () => {
</view> </view>
</view> </view>
</view> </view>
<view class="forgot-password">
<text class="forgot-text">
忘记密码
</text>
</view>
<view <view
class="login-btn" class="login-btn"
@click="handleLogin" @click="handleLogin"
> >
{{ loading ? '登录中...' : '登录' }} {{ loading ? t('login.loggingIn') : t('login.loginButton') }}
</view> </view>
<view class="register-hint"> <view class="hint-row">
<text class="hint-text"> <view class="register-hint">
还没有账户 <text class="register-link" @click="goToRegister">
</text> {{ t('login.noAccount') }}
<text class="register-link" @click="goToRegister"> </text>
立即注册 </view>
</text>
<view class="forgot-password">
<text class="forgot-text" @click="goToForgotPassword">
{{ t('login.forgotPassword') }}
</text>
</view>
</view> </view>
<!-- 登录方式切换 --> <!-- 登录方式切换 -->
@@ -352,7 +415,7 @@ onMounted(async () => {
<!-- 区号选择弹窗 --> <!-- 区号选择弹窗 -->
<wd-action-sheet <wd-action-sheet
v-model="showAreaCodeSheet" v-model="showAreaCodeSheet"
title="选择国家/地区" :title="t('login.selectCountry')"
:close-on-click-modal="true" :close-on-click-modal="true"
@close="closeAreaCodeSheet" @close="closeAreaCodeSheet"
> >
@@ -386,11 +449,33 @@ onMounted(async () => {
custom-class="confirm-btn" custom-class="confirm-btn"
@click="closeAreaCodeSheet" @click="closeAreaCodeSheet"
> >
确认 {{ t('login.confirm') }}
</wd-button> </wd-button>
</view> </view>
</view> </view>
</wd-action-sheet> </wd-action-sheet>
<!-- 语言选择弹窗 -->
<wd-action-sheet
v-model="showLanguageSheet"
:title="t('login.selectLanguage')"
:close-on-click-modal="true"
>
<view class="language-sheet">
<scroll-view scroll-y class="language-list">
<view
v-for="lang in supportedLanguages"
:key="lang.code"
class="language-item"
@click="handleLanguageChange(lang.code)"
>
<text class="language-name">
{{ lang.name }}
</text>
</view>
</scroll-view>
</view>
</wd-action-sheet>
</view> </view>
</template> </template>
@@ -498,7 +583,7 @@ onMounted(async () => {
&.captcha-wrapper { &.captcha-wrapper {
.captcha-image { .captcha-image {
margin-left: 20rpx; margin-left: 20rpx;
width: 120rpx; width: 150rpx;
height: 60rpx; height: 60rpx;
border-radius: 8rpx; border-radius: 8rpx;
overflow: hidden; overflow: hidden;
@@ -592,10 +677,14 @@ onMounted(async () => {
} }
} }
.forgot-password { .hint-row {
text-align: right; display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx; margin-bottom: 30rpx;
}
.forgot-password {
.forgot-text { .forgot-text {
color: #667eea; color: #667eea;
font-size: 26rpx; font-size: 26rpx;
@@ -633,14 +722,6 @@ onMounted(async () => {
} }
.register-hint { .register-hint {
text-align: center;
.hint-text {
color: #666666;
font-size: 26rpx;
margin-right: 8rpx;
}
.register-link { .register-link {
color: #667eea; color: #667eea;
font-size: 26rpx; font-size: 26rpx;
@@ -793,20 +874,52 @@ onMounted(async () => {
} }
} }
} }
.server-btn { // 右上角按钮组
.top-right-buttons {
position: absolute; position: absolute;
right: 20rpx; // 距离右边距 right: 20rpx;
top: 40rpx; // 顶部稍微下移,不贴状态栏 display: flex;
gap: 20rpx;
z-index: 999;
}
// 语言切换按钮
.lang-btn {
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 999;
cursor: pointer; cursor: pointer;
background: rgba(255, 255, 255, 0.15); // 半透明背景,更好看 background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx; // 圆形按钮 border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2); // 阴影 box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
&:active {
transform: scale(0.95);
}
.lang-text-icon {
font-size: 18rpx;
color: #FFFFFF;
}
&:hover {
background: rgba(255, 255, 255, 0.25);
}
}
// 服务端设置按钮
.server-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
&:active { &:active {
transform: scale(0.95); transform: scale(0.95);
@@ -814,11 +927,45 @@ onMounted(async () => {
.server-icon { .server-icon {
font-size: 28rpx; font-size: 28rpx;
color: #FFFFFF; // 白色图标 color: #FFFFFF;
} }
&:hover { &:hover {
background: rgba(255, 255, 255, 0.25); // 悬停效果 background: rgba(255, 255, 255, 0.25);
}
}
// 语言选择弹窗样式
.language-sheet {
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
overflow: hidden;
.language-list {
max-height: 60vh;
padding: 0 40rpx;
.language-item {
display: flex;
align-items: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f8f9fa;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #f8f9fa;
}
&:last-child {
border-bottom: none;
}
.language-name {
font-size: 32rpx;
color: #333333;
}
}
} }
} }
</style> </style>
+134 -104
View File
@@ -9,11 +9,15 @@
</route> </route>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue';
import { register, sendSmsCode } from '@/api/auth' import { register, sendSmsCode } from '@/api/auth';
import { useConfigStore } from '@/store' import { useConfigStore } from '@/store';
import { getEnvBaseUrl } from '@/utils' import { getEnvBaseUrl } from '@/utils';
import { toast } from '@/utils/toast' import { toast } from '@/utils/toast';
// 导入国际化相关功能
import { t, initI18n } from '@/i18n';
// 导入SM2加密工具
import { sm2Encrypt } from '@/utils';
// 获取屏幕边界到安全区域距离 // 获取屏幕边界到安全区域距离
let safeAreaInsets let safeAreaInsets
@@ -88,6 +92,11 @@ const areaCodeList = computed(() => {
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }] return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
}) })
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey
})
// 切换注册方式 // 切换注册方式
function toggleRegisterType() { function toggleRegisterType() {
registerType.value = registerType.value === 'username' ? 'mobile' : 'username' registerType.value = registerType.value === 'username' ? 'mobile' : 'username'
@@ -126,26 +135,26 @@ function generateUUID() {
// 获取验证码 // 获取验证码
async function refreshCaptcha() { async function refreshCaptcha() {
const uuid = generateUUID() const uuid = generateUUID();
formData.value.captchaId = uuid formData.value.captchaId = uuid;
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}` captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
} }
// 发送短信验证码 // 发送短信验证码
async function sendSmsVerification() { async function sendSmsVerification() {
if (!formData.value.mobile) { if (!formData.value.mobile) {
toast.warning('请输入手机号') toast.warning(t('register.enterPhone'))
return return
} }
if (!formData.value.captcha) { if (!formData.value.captcha) {
toast.warning('请输入图形验证码') toast.warning(t('register.enterCode'))
return return
} }
// 手机号格式验证 // 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/ const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) { if (!phoneRegex.test(formData.value.mobile)) {
toast.warning('请输入正确的手机号') toast.warning(t('register.enterPhone'))
return return
} }
@@ -157,7 +166,7 @@ async function sendSmsVerification() {
captchaId: formData.value.captchaId, captchaId: formData.value.captchaId,
}) })
toast.success('验证码发送成功') toast.success(t('register.captchaSendSuccess'))
// 开始倒计时 // 开始倒计时
smsCountdown.value = 60 smsCountdown.value = 60
@@ -169,6 +178,10 @@ async function sendSmsVerification() {
}, 1000) }, 1000)
} }
catch (error: any) { catch (error: any) {
// 处理验证码错误 - 从error.message中解析错误码
if (error.message.includes('请求错误[10067]')) {
toast.warning(t('login.captchaError'))
}
// 发送失败重新获取图形验证码 // 发送失败重新获取图形验证码
refreshCaptcha() refreshCaptcha()
} }
@@ -180,58 +193,76 @@ async function sendSmsVerification() {
// 注册 // 注册
async function handleRegister() { async function handleRegister() {
// 表单验证 // 表单验证
if (registerType.value === 'username') { if (enableMobileRegister.value) {
if (!formData.value.username) { // 手机号注册验证
toast.warning('请输入用户名')
return
}
}
else {
if (!formData.value.mobile) { if (!formData.value.mobile) {
toast.warning('请输入手机号') toast.warning(t('register.enterPhone'))
return return
} }
// 手机号格式验证 // 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/ const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) { if (!phoneRegex.test(formData.value.mobile)) {
toast.warning('请输入正确的手机号') toast.warning(t('register.enterPhone'))
return return
} }
if (!formData.value.mobileCaptcha) { if (!formData.value.mobileCaptcha) {
toast.warning('请输入短信验证码') toast.warning(t('register.enterCode'))
return
}
}
else {
// 用户名注册验证
if (!formData.value.username) {
toast.warning(t('register.enterUsername'))
return return
} }
} }
if (!formData.value.password) { if (!formData.value.password) {
toast.warning('请输入密码') toast.warning(t('register.enterPassword'))
return return
} }
if (!formData.value.confirmPassword) { if (!formData.value.confirmPassword) {
toast.warning('请确认密码') toast.warning(t('register.confirmPassword'))
return return
} }
if (formData.value.password !== formData.value.confirmPassword) { if (formData.value.password !== formData.value.confirmPassword) {
toast.warning('两次输入的密码不一致') toast.warning(t('register.confirmPassword'))
return return
} }
if (!formData.value.captcha) { if (!formData.value.captcha) {
toast.warning('请输入验证码') toast.warning(t('register.enterCode'))
return
}
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
toast.warning(t('sm2.publicKeyNotConfigured'))
return return
} }
try { try {
loading.value = true loading.value = true
// 加密密码
let encryptedPassword
try {
// 拼接验证码和密码
const captchaAndPassword = formData.value.captcha + formData.value.password
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
} catch (error) {
console.error('密码加密失败:', error)
toast.warning(t('sm2.encryptionFailed'))
return
}
// 构建注册数据 // 构建注册数据
const registerData = { const registerData = {
username: registerType.value === 'mobile' ? `${selectedAreaCode.value}${formData.value.mobile}` : formData.value.username, username: enableMobileRegister.value ? `${selectedAreaCode.value}${formData.value.mobile}` : formData.value.username,
password: formData.value.password, password: encryptedPassword,
confirmPassword: formData.value.confirmPassword,
captcha: formData.value.captcha,
captchaId: formData.value.captchaId, captchaId: formData.value.captchaId,
areaCode: formData.value.areaCode, areaCode: formData.value.areaCode,
mobile: formData.value.mobile, mobile: formData.value.mobile,
@@ -239,14 +270,24 @@ async function handleRegister() {
} }
await register(registerData) await register(registerData)
toast.success('注册成功') toast.success(t('message.registerSuccess'))
// 跳转到登录页 // 跳转到登录页
setTimeout(() => { setTimeout(() => {
uni.navigateBack() uni.redirectTo({
url: '/pages/login/index'
})
}, 1000) }, 1000)
} }
catch (error: any) { catch (error: any) {
// 处理验证码错误 - 从error.message中解析错误码
if (error.message.includes('请求错误[10067]')) {
toast.warning(t('login.captchaError'))
}
// 处理手机号码已注册错误
else if (error.message.includes('请求错误[10070]')) {
toast.warning(t('message.phoneRegistered'))
}
// 注册失败重新获取验证码 // 注册失败重新获取验证码
refreshCaptcha() refreshCaptcha()
} }
@@ -257,7 +298,9 @@ async function handleRegister() {
// 返回登录 // 返回登录
function goBack() { function goBack() {
uni.navigateBack() uni.redirectTo({
url: '/pages/login/index'
})
} }
// 页面加载时获取验证码 // 页面加载时获取验证码
@@ -275,7 +318,11 @@ onMounted(async () => {
console.error('获取配置失败:', error) console.error('获取配置失败:', error)
} }
} }
// 初始化国际化
initI18n()
}) })
</script> </script>
<template> <template>
@@ -287,10 +334,10 @@ onMounted(async () => {
<view class="logo-section"> <view class="logo-section">
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" /> <wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
<text class="welcome-text"> <text class="welcome-text">
欢迎注册 {{ t('register.pageTitle') }}
</text> </text>
<text class="subtitle"> <text class="subtitle">
创建您的新账户 {{ t('register.createAccount') }}
</text> </text>
</view> </view>
</view> </view>
@@ -298,7 +345,7 @@ onMounted(async () => {
<view class="form-container"> <view class="form-container">
<view class="form"> <view class="form">
<!-- 手机号注册 --> <!-- 手机号注册 -->
<template v-if="registerType === 'mobile'"> <template v-if="enableMobileRegister">
<view class="input-group"> <view class="input-group">
<view class="input-wrapper mobile-wrapper"> <view class="input-wrapper mobile-wrapper">
<view class="area-code-selector" @click="openAreaCodeSheet"> <view class="area-code-selector" @click="openAreaCodeSheet">
@@ -312,7 +359,7 @@ onMounted(async () => {
v-model="formData.mobile" v-model="formData.mobile"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入手机号码" :placeholder="t('register.enterPhone')"
type="number" type="number"
:maxlength="11" :maxlength="11"
/> />
@@ -329,7 +376,7 @@ onMounted(async () => {
v-model="formData.username" v-model="formData.username"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入用户名" :placeholder="t('register.enterUsername')"
/> />
</view> </view>
</view> </view>
@@ -338,63 +385,63 @@ onMounted(async () => {
<view class="input-group"> <view class="input-group">
<view class="input-wrapper"> <view class="input-wrapper">
<wd-input <wd-input
v-model="formData.password" v-model="formData.password"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入密码" :placeholder="t('register.enterPassword')"
show-password show-password
:maxlength="20" :maxlength="20"
/> />
</view> </view>
</view> </view>
<view class="input-group"> <view class="input-group">
<view class="input-wrapper"> <view class="input-wrapper">
<wd-input <wd-input
v-model="formData.confirmPassword" v-model="formData.confirmPassword"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请确认密码" :placeholder="t('register.confirmPassword')"
show-password show-password
:maxlength="20" :maxlength="20"
/> />
</view> </view>
</view> </view>
<view class="input-group"> <view class="input-group">
<view class="input-wrapper captcha-wrapper"> <view class="input-wrapper captcha-wrapper">
<wd-input <wd-input
v-model="formData.captcha" v-model="formData.captcha"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入验证码" :placeholder="t('register.enterCode')"
:maxlength="6" :maxlength="6"
/> />
<view class="captcha-image" @click="refreshCaptcha"> <view class="captcha-image" @click="refreshCaptcha">
<image :src="captchaImage" class="captcha-img" /> <image :src="captchaImage" class="captcha-img" />
</view> </view>
</view> </view>
</view> </view>
<!-- 手机验证码输入框 --> <!-- 手机验证码输入框 -->
<view v-if="registerType === 'mobile'" class="input-group"> <view v-if="enableMobileRegister" class="input-group">
<view class="input-wrapper sms-wrapper"> <view class="input-wrapper sms-wrapper">
<wd-input <wd-input
v-model="formData.mobileCaptcha" v-model="formData.mobileCaptcha"
custom-class="styled-input" custom-class="styled-input"
no-border no-border
placeholder="请输入短信验证码" :placeholder="t('register.enterCode')"
type="number" type="number"
:maxlength="6" :maxlength="6"
/> />
<wd-button <wd-button
:loading="smsLoading" :loading="smsLoading"
:disabled="smsCountdown > 0" :disabled="smsCountdown > 0"
custom-class="sms-btn" custom-class="sms-btn"
@click="sendSmsVerification" @click="sendSmsVerification"
> >
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }} {{ smsCountdown > 0 ? `${smsCountdown}s` : t('register.getCode') }}
</wd-button> </wd-button>
</view> </view>
</view> </view>
@@ -404,44 +451,25 @@ onMounted(async () => {
:loading="loading" :loading="loading"
@click="handleRegister" @click="handleRegister"
> >
{{ loading ? '注册中...' : '注册' }} {{ loading ? t('register.registering') : t('register.registerButton') }}
</view> </view>
<view class="login-hint"> <view class="login-hint">
<text class="hint-text"> <text class="hint-text">
已有账户 {{ t('register.haveAccount') }}
</text> </text>
<text class="login-link" @click="goBack"> <text class="login-link" @click="goBack">
立即登录 {{ t('register.loginNow') }}
</text> </text>
</view> </view>
<!-- 注册方式切换 -->
<view v-if="enableMobileRegister" class="register-type-switch">
<view class="switch-tabs">
<view
class="switch-tab"
:class="{ active: registerType === 'username' }"
@click="toggleRegisterType"
>
<wd-icon name="user" />
</view>
<view
class="switch-tab"
:class="{ active: registerType === 'mobile' }"
@click="toggleRegisterType"
>
<wd-icon name="phone" />
</view>
</view>
</view>
</view> </view>
</view> </view>
<!-- 区号选择弹窗 --> <!-- 区号选择弹窗 -->
<wd-action-sheet <wd-action-sheet
v-model="showAreaCodeSheet" v-model="showAreaCodeSheet"
title="选择国家/地区" :title="t('register.selectCountry')"
:close-on-click-modal="true" :close-on-click-modal="true"
@close="closeAreaCodeSheet" @close="closeAreaCodeSheet"
> >
@@ -475,11 +503,13 @@ onMounted(async () => {
custom-class="confirm-btn" custom-class="confirm-btn"
@click="closeAreaCodeSheet" @click="closeAreaCodeSheet"
> >
确认 {{ t('register.confirm') }}
</wd-button> </wd-button>
</view> </view>
</view> </view>
</wd-action-sheet> </wd-action-sheet>
</view> </view>
</template> </template>
@@ -612,7 +642,7 @@ onMounted(async () => {
&.captcha-wrapper { &.captcha-wrapper {
.captcha-image { .captcha-image {
margin-left: 20rpx; margin-left: 20rpx;
width: 120rpx; width: 150rpx;
height: 60rpx; height: 60rpx;
border-radius: 8rpx; border-radius: 8rpx;
overflow: hidden; overflow: hidden;
+149 -56
View File
@@ -11,6 +11,8 @@ import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, se
import { isMp } from '@/utils/platform' import { isMp } from '@/utils/platform'
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
import type { Language } from '@/store/lang'
defineOptions({ defineOptions({
name: 'SettingsPage', name: 'SettingsPage',
@@ -62,7 +64,7 @@ function validateUrl() {
} }
if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) { if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
urlError.value = '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)' urlError.value = t('settings.validServerUrl')
} }
} }
@@ -85,12 +87,12 @@ async function testServerBaseUrl() {
if (response.statusCode === 200) { if (response.statusCode === 200) {
return true return true
} else { } else {
toast.error('无效地址,请检查服务端是否启动或网络连接是否正常') toast.error(t('message.invalidAddress'))
return false return false
} }
} catch (error) { } catch (error) {
console.error('测试服务端地址失败:', error) console.error('测试服务端地址失败:', error)
toast.error('无效地址,请检查服务端是否启动或网络连接是否正常') toast.error(t('message.invalidAddress'))
return false return false
} }
} }
@@ -98,7 +100,7 @@ async function testServerBaseUrl() {
// 保存服务端地址 // 保存服务端地址
async function saveServerBaseUrl() { async function saveServerBaseUrl() {
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) { if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
toast.warning('请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)') toast.warning(t('settings.validServerUrl'))
return return
} }
@@ -113,21 +115,32 @@ async function saveServerBaseUrl() {
clearAllCacheAfterUrlChange() clearAllCacheAfterUrlChange()
uni.showModal({ uni.showModal({
title: '重启应用', title: t('settings.restartApp'),
content: '服务端地址已保存并清空缓存,是否立即重启生效?', content: t('settings.serverUrlSavedAndCacheCleared'),
confirmText: '立即重启', confirmText: t('settings.restartNow'),
cancelText: '稍后', cancelText: t('settings.restartLater'),
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
restartApp() restartApp()
} }
else { else {
toast.success('已保存,可稍后手动重启应用') toast.success(t('settings.restartSuccess'))
} }
}, },
}) })
} }
// 语言切换
const supportedLanguages = getSupportedLanguages()
const currentLanguage = ref<Language>(getCurrentLanguage())
const showLanguageSheet = ref(false)
function handleLanguageChange(lang: Language) {
changeLanguage(lang)
showLanguageSheet.value = false
toast.success(t('settings.languageChanged'))
}
// 重置为 env 默认 // 重置为 env 默认
function resetServerBaseUrl() { function resetServerBaseUrl() {
clearServerBaseUrlOverride() clearServerBaseUrlOverride()
@@ -137,16 +150,16 @@ function resetServerBaseUrl() {
clearAllCacheAfterUrlChange() clearAllCacheAfterUrlChange()
uni.showModal({ uni.showModal({
title: '重启应用', title: t('settings.restartApp'),
content: '已重置为默认地址并清空缓存,是否立即重启生效?', content: t('settings.resetToDefaultAndCacheCleared'),
confirmText: '立即重启', confirmText: t('settings.restartNow'),
cancelText: '稍后', cancelText: t('settings.restartLater'),
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
restartApp() restartApp()
} }
else { else {
toast.success('已重置,可稍后手动重启应用') toast.success(t('settings.resetSuccess'))
} }
}, },
}) })
@@ -185,8 +198,7 @@ function clearAllCacheAfterUrlChange() {
// 重新获取缓存信息 // 重新获取缓存信息
getCacheInfo() getCacheInfo()
} } catch (error) {
catch (error) {
console.error('清除缓存失败:', error) console.error('清除缓存失败:', error)
} }
} }
@@ -195,12 +207,14 @@ function clearAllCacheAfterUrlChange() {
async function clearCache() { async function clearCache() {
try { try {
uni.showModal({ uni.showModal({
title: '确认清除', title: t('settings.confirmClear'),
content: '确定要清除所有缓存吗?这将删除所有数据包括登录状态,需要重新登录。', content: t('settings.confirmClearMessage'),
confirmText: t('common.confirm'),
cancelText: t('common.cancel'),
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
clearAllCacheAfterUrlChange() clearAllCacheAfterUrlChange()
toast.success('缓存清除成功,即将跳转到登录页') toast.success(t('settings.cacheCleared'))
// 延迟跳转到登录页 // 延迟跳转到登录页
setTimeout(() => { setTimeout(() => {
@@ -209,22 +223,22 @@ async function clearCache() {
} }
}, },
}) })
} } catch (error) {
catch (error) {
console.error('清除缓存失败:', error) console.error('清除缓存失败:', error)
toast.error('清除缓存失败') toast.error(t('settings.clearCacheFailed'))
} }
} }
// 关于我们 // 关于我们
function showAbout() { function showAbout() {
uni.showModal({ uni.showModal({
title: `关于${import.meta.env.VITE_APP_TITLE}`, title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: `${import.meta.env.VITE_APP_TITLE}\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server`, content: t('settings.aboutContent', {
title: `关于小智智控台`, appName: import.meta.env.VITE_APP_TITLE,
content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.2`, version: '0.8.5'
}),
showCancel: false, showCancel: false,
confirmText: '确定', confirmText: t('common.confirm'),
}) })
} }
@@ -234,19 +248,24 @@ onMounted(async () => {
loadServerBaseUrl() loadServerBaseUrl()
} }
getCacheInfo() getCacheInfo()
// 动态设置导航栏标题为国际化文本
uni.setNavigationBarTitle({
title: t('settings.title')
})
}) })
</script> </script>
<template> <template>
<view class="min-h-screen bg-[#f5f7fb]"> <view class="min-h-screen bg-[#f5f7fb]">
<wd-navbar title="设置" placeholder safe-area-inset-top fixed /> <wd-navbar :title="t('settings.title')" placeholder safe-area-inset-top fixed />
<view class="p-[24rpx]"> <view class="p-[24rpx]">
<!-- 网络设置 - 仅在非小程序环境显示 --> <!-- 网络设置 - 仅在非小程序环境显示 -->
<view v-if="!isMp" class="mb-[32rpx]"> <view v-if="!isMp" class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
网络设置 {{ t('settings.networkSettings') }}
</text> </text>
</view> </view>
@@ -254,19 +273,19 @@ onMounted(async () => {
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"> style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view class="mb-[24rpx]"> <view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#232338] font-semibold"> <text class="text-[28rpx] text-[#232338] font-semibold">
服务端接口地址 {{ t('settings.serverApiUrl') }}
</text> </text>
<text class="mt-[8rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[8rpx] block text-[24rpx] text-[#9d9ea3]">
修改后将自动清空缓存并重启应用 {{ t('settings.modifyWillClearCache') }}
</text> </text>
</view> </view>
<view class="mb-[24rpx]"> <view class="mb-[24rpx]">
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden"> <view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200" <wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
placeholder="输入服务端地址,如 https://example.com/xiaozhi" :placeholder="t('settings.enterServerUrl')"
custom-class="!border-none !bg-transparent h-[88rpx] px-[24rpx] items-center" custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" /> input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
</view> </view>
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]"> <text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
{{ urlError }} {{ urlError }}
@@ -277,12 +296,12 @@ onMounted(async () => {
<wd-button type="primary" <wd-button type="primary"
custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-[#336cff] border-none shadow-[0_4rpx_16rpx_rgba(51,108,255,0.3)] active:shadow-[0_2rpx_8rpx_rgba(51,108,255,0.4)] active:scale-98" custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-[#336cff] border-none shadow-[0_4rpx_16rpx_rgba(51,108,255,0.3)] active:shadow-[0_2rpx_8rpx_rgba(51,108,255,0.4)] active:scale-98"
@click="saveServerBaseUrl"> @click="saveServerBaseUrl">
保存设置 {{ t('settings.saveSettings') }}
</wd-button> </wd-button>
<wd-button type="default" <wd-button type="default"
custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-white border-[#eeeeee] text-[#65686f] active:bg-[#f5f7fb]" custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-white border-[#eeeeee] text-[#65686f] active:bg-[#f5f7fb]"
@click="resetServerBaseUrl"> @click="resetServerBaseUrl">
恢复默认 {{ t('settings.resetDefault') }}
</wd-button> </wd-button>
</view> </view>
</view> </view>
@@ -292,7 +311,7 @@ onMounted(async () => {
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
缓存管理 {{ t('settings.cacheManagement') }}
</text> </text>
</view> </view>
@@ -304,11 +323,11 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"> class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
总缓存大小 {{ t('settings.totalCacheSize') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
应用数据总大小 {{ t('settings.appDataSize') }}
</text> </text>
</view> </view>
<text class="text-[28rpx] text-[#65686f] font-semibold"> <text class="text-[28rpx] text-[#65686f] font-semibold">
{{ cacheInfo.storageSize }} {{ cacheInfo.storageSize }}
@@ -320,16 +339,16 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]"> class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
缓存清理 {{ t('settings.cacheClear') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
清空所有缓存数据 {{ t('settings.clearAllCache') }}
</text> </text>
</view> </view>
<view <view
class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white" class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white"
@click="clearCache"> @click="clearCache">
清除缓存 {{ t('settings.clearCache') }}
</view> </view>
</view> </view>
</view> </view>
@@ -340,8 +359,8 @@ onMounted(async () => {
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
应用信息 {{ t('settings.appInfo') }}
</text> </text>
</view> </view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]" <view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
@@ -351,17 +370,68 @@ onMounted(async () => {
@click="showAbout"> @click="showAbout">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
关于我们 {{ t('settings.aboutUs') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
应用版本与团队信息 {{ t('settings.appVersion') }}
</text> </text>
</view> </view>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" /> <wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view> </view>
</view> </view>
</view> </view>
<!-- 语言设置 -->
<view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold">
{{ t('settings.languageSettings') }}
</text>
</view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]" @click="showLanguageSheet = true">
<view>
<text class="text-[32rpx] text-[#232338] font-medium">
{{ t('settings.language') }}
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.selectLanguage') }}
</text>
</view>
<view class="flex items-center">
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }}
</text>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view>
</view>
</view>
</view>
<!-- 语言选择弹窗 -->
<wd-action-sheet
v-model="showLanguageSheet"
:title="t('settings.selectLanguage')"
:close-on-click-modal="true"
>
<view class="language-sheet">
<scroll-view scroll-y class="language-list">
<view
v-for="lang in supportedLanguages"
:key="lang.code"
class="language-item"
@click="handleLanguageChange(lang.code)"
>
<text class="language-name">
{{ lang.name }}
</text>
</view>
</scroll-view>
</view>
</wd-action-sheet>
<!-- 底部安全距离 --> <!-- 底部安全距离 -->
<!-- 底部安全距离 --> <!-- 底部安全距离 -->
<view style="height: env(safe-area-inset-bottom);" /> <view style="height: env(safe-area-inset-bottom);" />
@@ -370,4 +440,27 @@ onMounted(async () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// 保持与 edit.vue 一致的风格,样式主要通过类名控制</style> // 保持与 edit.vue 一致的风格,样式主要通过类名控制
// 语言选择弹窗样式
.language-sheet {
.language-list {
max-height: 50vh;
.language-item {
padding: 30rpx 0;
text-align: center;
border-bottom: 1rpx solid #f0f0f0;
.language-name {
font-size: 28rpx;
color: #333;
}
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f5f7fb;
}
}
}
}
</style>
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni' import { useMessage } from 'wot-design-uni'
import { useToast } from 'wot-design-uni/components/wd-toast' import { useToast } from 'wot-design-uni/components/wd-toast'
import { createVoicePrint, deleteVoicePrint, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint' import { createVoicePrint, deleteVoicePrint, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint'
import { t } from '@/i18n'
defineOptions({ defineOptions({
name: 'VoicePrintManage', name: 'VoicePrintManage',
@@ -81,7 +82,7 @@ async function loadVoicePrintList() {
// 检查是否有当前选中的智能体 // 检查是否有当前选中的智能体
if (!currentAgentId.value) { if (!currentAgentId.value) {
console.warn('没有选中的智能体') console.warn(t('voiceprint.noSelectedAgent'))
voicePrintList.value = [] voicePrintList.value = []
return return
} }
@@ -117,7 +118,7 @@ async function refresh() {
async function loadChatHistory() { async function loadChatHistory() {
try { try {
if (!currentAgentId.value) { if (!currentAgentId.value) {
toast.error('请先选择智能体') toast.error(t('voiceprint.pleaseSelectAgent'))
return return
} }
@@ -133,24 +134,47 @@ async function loadChatHistory() {
} }
catch (error) { catch (error) {
console.error('获取对话记录失败:', error) console.error('获取对话记录失败:', error)
toast.error('获取对话记录失败') toast.error(t('voiceprint.fetchHistoryFailed'))
} }
} }
// 打开添加弹窗 // 打开添加弹窗
function openAddDialog() { function openAddDialog() {
if (!currentAgentId.value) { if (!currentAgentId.value) {
toast.error('请先选择智能体') toast.error(t('voiceprint.pleaseSelectAgent'))
return return
} }
addForm.value = { // 检查声纹接口是否配置(通过尝试获取声纹列表来检测)
agentId: currentAgentId.value, const checkVoicePrintConfig = async () => {
audioId: '', try {
sourceName: '', await getVoicePrintList(currentAgentId.value)
introduce: '', // 接口正常,继续打开添加弹窗
addForm.value = {
agentId: currentAgentId.value,
audioId: '',
sourceName: '',
introduce: '',
}
showAddDialog.value = true
} catch (error: any) {
// 捕捉声纹接口未配置错误
if (error.message && error.message.includes('请求错误[10054]')) {
toast.error(t('voiceprint.voiceprintInterfaceNotConfigured'))
} else {
// 其他错误,继续打开弹窗
addForm.value = {
agentId: currentAgentId.value,
audioId: '',
sourceName: '',
introduce: '',
}
showAddDialog.value = true
}
}
} }
showAddDialog.value = true
checkVoicePrintConfig()
} }
// 打开编辑弹窗 // 打开编辑弹窗
@@ -162,7 +186,7 @@ function openEditDialog(item: VoicePrint) {
// 获取选中音频的显示内容 // 获取选中音频的显示内容
function getSelectedAudioContent(audioId: string) { function getSelectedAudioContent(audioId: string) {
if (!audioId) if (!audioId)
return '点击选择声纹向量' return t('voiceprint.clickToSelectVector')
const chatItem = chatHistoryList.value.find(item => item.audioId === audioId) const chatItem = chatHistoryList.value.find(item => item.audioId === audioId)
return chatItem ? chatItem.content : `已选择: ${audioId.substring(0, 8)}...` return chatItem ? chatItem.content : `已选择: ${audioId.substring(0, 8)}...`
} }
@@ -181,34 +205,34 @@ function selectAudioId({ item }: { item: any }) {
// 提交添加说话人 // 提交添加说话人
async function submitAdd() { async function submitAdd() {
if (!addForm.value.sourceName.trim()) { if (!addForm.value.sourceName.trim()) {
toast.error('请输入姓名') toast.error(t('voiceprint.pleaseInputName'))
return return
} }
if (!addForm.value.audioId) { if (!addForm.value.audioId) {
toast.error('请选择声纹向量') toast.error(t('voiceprint.pleaseSelectVector'))
return return
} }
try { try {
await createVoicePrint(addForm.value) await createVoicePrint(addForm.value)
toast.success('添加成功') toast.success(t('voiceprint.addSuccess'))
showAddDialog.value = false showAddDialog.value = false
await loadVoicePrintList() await loadVoicePrintList()
} }
catch (error) { catch (error) {
console.error('添加说话人失败:', error) console.error('添加说话人失败:', error)
toast.error('添加说话人失败') toast.error(t('voiceprint.addFailed'))
} }
} }
// 提交编辑说话人 // 提交编辑说话人
async function submitEdit() { async function submitEdit() {
if (!editForm.value.sourceName.trim()) { if (!editForm.value.sourceName.trim()) {
toast.error('请输入姓名') toast.error(t('voiceprint.pleaseInputName'))
return return
} }
if (!editForm.value.audioId) { if (!editForm.value.audioId) {
toast.error('请选择声纹向量') toast.error(t('voiceprint.pleaseSelectVector'))
return return
} }
@@ -220,13 +244,13 @@ async function submitEdit() {
introduce: editForm.value.introduce, introduce: editForm.value.introduce,
createDate: editForm.value.createDate, createDate: editForm.value.createDate,
}) })
toast.success('编辑成功') toast.success(t('voiceprint.editSuccess'))
showEditDialog.value = false showEditDialog.value = false
await loadVoicePrintList() await loadVoicePrintList()
} }
catch (error) { catch (error) {
console.error('编辑说话人失败:', error) console.error('编辑说话人失败:', error)
toast.error('编辑说话人失败') toast.error(t('voiceprint.editFailed'))
} }
} }
@@ -239,11 +263,11 @@ function handleEdit(item: VoicePrint) {
// 删除声纹 // 删除声纹
async function handleDelete(id: string) { async function handleDelete(id: string) {
message.confirm({ message.confirm({
msg: '确定要删除这个说话人吗?', msg: t('voiceprint.deleteConfirmMsg'),
title: '确认删除', title: t('voiceprint.deleteConfirmTitle'),
}).then(async () => { }).then(async () => {
await deleteVoicePrint(id) await deleteVoicePrint(id)
toast.success('删除成功') toast.success(t('voiceprint.deleteSuccess'))
await loadVoicePrintList() await loadVoicePrintList()
}).catch(() => { }).catch(() => {
console.log('点击了取消按钮') console.log('点击了取消按钮')
@@ -268,7 +292,7 @@ defineExpose({
<view v-if="loading && voicePrintList.length === 0" class="loading-container"> <view v-if="loading && voicePrintList.length === 0" class="loading-container">
<wd-loading color="#336cff" /> <wd-loading color="#336cff" />
<text class="loading-text"> <text class="loading-text">
加载中... {{ t('voiceprint.loading') }}
</text> </text>
</view> </view>
@@ -302,7 +326,7 @@ defineExpose({
@click="handleDelete(item.id)" @click="handleDelete(item.id)"
> >
<wd-icon name="delete" /> <wd-icon name="delete" />
删除 {{ t('voiceprint.delete') }}
</view> </view>
</view> </view>
</template> </template>
@@ -315,11 +339,11 @@ defineExpose({
<view v-else-if="!loading" class="empty-container"> <view v-else-if="!loading" class="empty-container">
<view class="flex flex-col items-center justify-center p-[100rpx_40rpx] text-center"> <view class="flex flex-col items-center justify-center p-[100rpx_40rpx] text-center">
<wd-icon name="voice" custom-class="text-[120rpx] text-[#d9d9d9] mb-[32rpx]" /> <wd-icon name="voice" custom-class="text-[120rpx] text-[#d9d9d9] mb-[32rpx]" />
<text class="mb-[16rpx] text-[32rpx] text-[#666666] font-medium"> <text class="mb-[32rpx] text-[32rpx] text-[#666666] font-medium">
暂无声纹数据 {{ t('voiceprint.emptyTitle') }}
</text> </text>
<text class="text-[26rpx] text-[#999999] leading-[1.5]"> <text class="text-[26rpx] text-[#999999] leading-[1.5]">
点击右下角 + 号添加您的第一个说话人 {{ t('voiceprint.emptyDesc') }}
</text> </text>
</view> </view>
</view> </view>
@@ -341,7 +365,7 @@ defineExpose({
<view> <view>
<view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]"> <view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]">
<text class="w-full text-center text-[32rpx] text-[#232338] font-semibold"> <text class="w-full text-center text-[32rpx] text-[#232338] font-semibold">
添加说话人 {{ t('voiceprint.addSpeaker') }}
</text> </text>
</view> </view>
@@ -349,7 +373,7 @@ defineExpose({
<!-- 声纹向量选择 --> <!-- 声纹向量选择 -->
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 声纹向量 * {{ t('voiceprint.voiceVector') }}
</text> </text>
<view <view
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
@@ -368,22 +392,22 @@ defineExpose({
<!-- 姓名 --> <!-- 姓名 -->
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 姓名 * {{ t('voiceprint.name') }}
</text> </text>
<input <input
v-model="addForm.sourceName" v-model="addForm.sourceName"
class="box-border h-[80rpx] w-full border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[80rpx] w-full border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text" placeholder="请输入姓名" type="text" :placeholder="t('voiceprint.pleaseInputName')"
> >
</view> </view>
<!-- 描述 --> <!-- 描述 -->
<view> <view>
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 描述 * {{ t('voiceprint.description') }}
</text> </text>
<textarea <textarea
v-model="addForm.introduce" :maxlength="100" placeholder="请输入描述" v-model="addForm.introduce" :maxlength="100" :placeholder="t('voiceprint.pleaseInputDescription')"
class="box-border h-[200rpx] w-full resize-none border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[200rpx] w-full resize-none border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
/> />
<view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]"> <view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]">
@@ -394,11 +418,11 @@ defineExpose({
<view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]"> <view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]">
<wd-button type="info" custom-class="flex-1" @click="showAddDialog = false"> <wd-button type="info" custom-class="flex-1" @click="showAddDialog = false">
取消 {{ t('voiceprint.cancel') }}
</wd-button> </wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitAdd"> <wd-button type="primary" custom-class="flex-1" @click="submitAdd">
保存 {{ t('voiceprint.save') }}
</wd-button> </wd-button>
</view> </view>
</view> </view>
</wd-popup> </wd-popup>
@@ -411,7 +435,7 @@ defineExpose({
<view> <view>
<view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]"> <view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]">
<text class="w-full text-center text-[32rpx] text-[#232338] font-semibold"> <text class="w-full text-center text-[32rpx] text-[#232338] font-semibold">
编辑说话人 {{ t('voiceprint.editSpeaker') }}
</text> </text>
</view> </view>
@@ -419,7 +443,7 @@ defineExpose({
<!-- 声纹向量选择 --> <!-- 声纹向量选择 -->
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 声纹向量 * {{ t('voiceprint.voiceVector') }}
</text> </text>
<view <view
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
@@ -438,22 +462,22 @@ defineExpose({
<!-- 姓名 --> <!-- 姓名 -->
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 姓名 * {{ t('voiceprint.name') }}
</text> </text>
<input <input
v-model="editForm.sourceName" v-model="editForm.sourceName"
class="box-border h-[80rpx] w-full border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[80rpx] w-full border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text" placeholder="请输入姓名" type="text" :placeholder="t('voiceprint.pleaseInputName')"
> >
</view> </view>
<!-- 描述 --> <!-- 描述 -->
<view> <view>
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium"> <text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 描述 * {{ t('voiceprint.description') }}
</text> </text>
<textarea <textarea
v-model="editForm.introduce" :maxlength="100" placeholder="请输入描述" v-model="editForm.introduce" :maxlength="100" :placeholder="t('voiceprint.pleaseInputDescription')"
class="box-border h-[200rpx] w-full resize-none border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]" class="box-border h-[200rpx] w-full resize-none border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
/> />
<view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]"> <view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]">
@@ -464,18 +488,18 @@ defineExpose({
<view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]"> <view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]">
<wd-button type="info" custom-class="flex-1" @click="showEditDialog = false"> <wd-button type="info" custom-class="flex-1" @click="showEditDialog = false">
取消 {{ t('voiceprint.cancel') }}
</wd-button> </wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitEdit"> <wd-button type="primary" custom-class="flex-1" @click="submitEdit">
保存 {{ t('voiceprint.save') }}
</wd-button> </wd-button>
</view> </view>
</view> </view>
</wd-popup> </wd-popup>
<!-- 语音对话记录选择动作面板 --> <!-- 语音对话记录选择动作面板 -->
<wd-action-sheet <wd-action-sheet
v-model="showChatHistoryDialog" :actions="chatHistoryActions" title="选择声纹向量" v-model="showChatHistoryDialog" :actions="chatHistoryActions" :title="t('voiceprint.selectVector')"
@select="selectAudioId" @select="selectAudioId"
/> />
</template> </template>
+2 -1
View File
@@ -1,7 +1,7 @@
import type { PublicConfig } from '@/api/auth' import type { PublicConfig } from '@/api/auth'
import { getPublicConfig } from '@/api/auth'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { getPublicConfig } from '@/api/auth'
// 初始化状态 // 初始化状态
const initialConfigState: PublicConfig = { const initialConfigState: PublicConfig = {
@@ -12,6 +12,7 @@ const initialConfigState: PublicConfig = {
mobileAreaList: [], mobileAreaList: [],
beianIcpNum: '', beianIcpNum: '',
beianGaNum: '', beianGaNum: '',
sm2PublicKey: '',
name: import.meta.env.VITE_APP_TITLE, name: import.meta.env.VITE_APP_TITLE,
} }
+41
View File
@@ -0,0 +1,41 @@
import { ref } from 'vue'
import { ref } from 'vue'
import { defineStore } from 'pinia'
// 支持的语言类型
export type Language = 'zh_CN' | 'en' | 'zh_TW'
export interface LangStore {
currentLang: Language
changeLang: (lang: Language) => void
}
export const useLangStore = defineStore(
'lang',
(): LangStore => {
// 从本地存储获取语言设置,如果没有则使用默认值
const savedLang = uni.getStorageSync('app_language') as Language | null
const currentLang = ref<Language>(savedLang || 'zh_CN')
// 切换语言
const changeLang = (lang: Language) => {
currentLang.value = lang
// 将语言设置保存到本地存储
uni.setStorageSync('app_language', lang)
}
return {
currentLang,
changeLang,
}
},
{
persist: {
key: 'lang',
serializer: {
serialize: state => JSON.stringify(state.currentLang),
deserialize: value => ({ currentLang: JSON.parse(value) }),
},
},
},
)
+57
View File
@@ -196,3 +196,60 @@ export function getEnvBaseUploadUrl() {
return baseUploadUrl return baseUploadUrl
} }
import smCrypto from 'sm-crypto'
/**
* 生成SM2密钥对(十六进制格式)
* @returns {Object} 包含公钥和私钥的对象
*/
export function generateSm2KeyPairHex() {
// 使用sm-crypto库生成SM2密钥对
const sm2 = smCrypto.sm2;
const keypair = sm2.generateKeyPairHex();
return {
publicKey: keypair.publicKey,
privateKey: keypair.privateKey,
clientPublicKey: keypair.publicKey, // 客户端公钥
clientPrivateKey: keypair.privateKey // 客户端私钥
};
}
/**
* SM2公钥加密
* @param {string} publicKey 公钥(十六进制格式)
* @param {string} plainText 明文
* @returns {string} 加密后的密文(十六进制格式)
*/
export function sm2Encrypt(publicKey: string, plainText: string): string {
if (!publicKey) {
throw new Error('公钥不能为null或undefined');
}
if (!plainText) {
throw new Error('明文不能为空');
}
const sm2 = smCrypto.sm2;
// SM2加密,添加04前缀表示未压缩公钥
const encrypted = sm2.doEncrypt(plainText, publicKey, 1);
// 转换为十六进制格式(与后端保持一致,添加04前缀)
const result = "04" + encrypted;
return result;
}
/**
* SM2私钥解密
* @param {string} privateKey 私钥(十六进制格式)
* @param {string} cipherText 密文(十六进制格式)
* @returns {string} 解密后的明文
*/
export function sm2Decrypt(privateKey: string, cipherText: string): string {
const sm2 = smCrypto.sm2;
// 移除04前缀(与后端保持一致)
const dataWithoutPrefix = cipherText.startsWith("04") ? cipherText.substring(2) : cipherText;
// SM2解密
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1);
}
+44 -15
View File
@@ -10,13 +10,16 @@
"dependencies": { "dependencies": {
"core-js": "^3.41.0", "core-js": "^3.41.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.5.0",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"flyio": "^0.6.14", "flyio": "^0.6.14",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"opus-decoder": "^0.7.7", "opus-decoder": "^0.7.7",
"opus-recorder": "^8.0.5", "opus-recorder": "^8.0.5",
"sm-crypto": "^0.3.13",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-i18n": "^8.28.2",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"xiaozhi": "file:" "xiaozhi": "file:"
@@ -2540,6 +2543,16 @@
} }
} }
}, },
"node_modules/@vue/cli-service/node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=10"
}
},
"node_modules/@vue/cli-shared-utils": { "node_modules/@vue/cli-shared-utils": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", "resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz",
@@ -4776,12 +4789,15 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "10.0.0", "version": "16.6.1",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true, "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=10" "node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-expand": { "node_modules/dotenv-expand": {
@@ -10114,6 +10130,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sm-crypto": {
"version": "0.3.13",
"resolved": "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.3.13.tgz",
"integrity": "sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==",
"license": "MIT",
"dependencies": {
"jsbn": "^1.1.0"
}
},
"node_modules/sm-crypto/node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/smob": { "node_modules/smob": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/smob/-/smob-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/smob/-/smob-1.5.0.tgz",
@@ -10137,7 +10168,7 @@
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true, "dev": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "bin/uuid"
} }
}, },
"node_modules/source-list-map": { "node_modules/source-list-map": {
@@ -11065,15 +11096,6 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/validate-npm-package-license": { "node_modules/validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -11131,6 +11153,13 @@
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
"dev": true "dev": true
}, },
"node_modules/vue-i18n": {
"version": "8.28.2",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.28.2.tgz",
"integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==",
"deprecated": "Vue I18n v8.x has reached EOL and is no longer actively maintained. About maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
"license": "MIT"
},
"node_modules/vue-loader": { "node_modules/vue-loader": {
"version": "17.4.2", "version": "17.4.2",
"resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.4.2.tgz", "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.4.2.tgz",
+1
View File
@@ -16,6 +16,7 @@
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"opus-decoder": "^0.7.7", "opus-decoder": "^0.7.7",
"opus-recorder": "^8.0.5", "opus-recorder": "^8.0.5",
"sm-crypto": "^0.3.13",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-i18n": "^8.28.2", "vue-i18n": "^8.28.2",
+7 -1
View File
@@ -7,6 +7,10 @@ import model from './module/model.js'
import ota from './module/ota.js' import ota from './module/ota.js'
import timbre from "./module/timbre.js" import timbre from "./module/timbre.js"
import user from './module/user.js' import user from './module/user.js'
import voiceClone from './module/voiceClone.js'
import voiceResource from './module/voiceResource.js'
/** /**
* 接口地址 * 接口地址
@@ -33,5 +37,7 @@ export default {
model, model,
timbre, timbre,
ota, ota,
dict dict,
voiceResource,
voiceClone
} }

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