Compare commits

..
279 Commits
Author SHA1 Message Date
欣南科技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
hrzandGitHub ed07793c47 Merge pull request #2288 from xinnan-tech/sm2_test
登录、注册页面添加非对称加密
2025-09-30 11:12:50 +08:00
hrz 2ed2e9c391 update:优化移动端按钮 2025-09-30 11:10:48 +08:00
3030332422 2b9fed2609 update:新增SM2解密工具类,优化登录注册密码找回功能 2025-09-30 09:07:06 +08:00
3030332422 848ce65d13 update:web端重置密码页面加密 2025-09-29 17:57:53 +08:00
hrzandGitHub f0512ce1cc Merge pull request #2302 from xinnan-tech/CSS_update
调整底框位置
2025-09-29 17:57:24 +08:00
LiJinHui 313fad1f81 样式调整 2025-09-29 17:56:19 +08:00
3030332422 ff26a84988 fix:完善重置密码页面多语言支持 2025-09-29 16:51:23 +08:00
3030332422 f7ee54598e fix:修复移动端创建智能体时显示问题 2025-09-29 16:09:28 +08:00
LiJinHui 7a62462c1b 调整底框位置 2025-09-29 15:18:46 +08:00
3030332422 a4433ebe68 fix:修复移动端注册页面的一些bug 2025-09-29 14:21:41 +08:00
3030332422 48feea55fe update:移动端注册页面加密 2025-09-29 10:29:28 +08:00
3030332422 6e5f708f5a update:移动端登录页面加密 2025-09-28 22:40:05 +08:00
hrzandGitHub f2ed645f9c Merge pull request #2290 from xinnan-tech/jia_mi_miyao
模型秘钥加密
2025-09-28 18:32:53 +08:00
hrz d47664e670 update:优化输入框提示 2025-09-28 18:25:29 +08:00
LiJinHui 8f3229ddda 调整 input onfocus 判断 2025-09-28 17:37:06 +08:00
LiJinHui 4f2b7524aa 改用精准匹配生成提示词 2025-09-28 17:20:09 +08:00
LiJinHui 4e99e176e9 删除多余样式 2025-09-28 17:05:17 +08:00
LiJinHui cfc7047967 页面优化 2025-09-28 16:52:28 +08:00
LiJinHui 776b5fb3b4 修复加密的bug 2025-09-28 15:27:46 +08:00
3030332422 f16d42d2b5 Merge branch 'sm2_test' of https://github.com/xinnan-tech/xiaozhi-esp32-server into sm2_test 2025-09-28 14:18:44 +08:00
3030332422 344d183b6f fix:修复web端手机号码注册时遇到的问题 2025-09-28 14:06:13 +08:00
LiJinHui feb668c96c 还原Python的端口 2025-09-28 10:28:01 +08:00
LiJinHui f345568675 修复秘钥污染的bug 2025-09-28 09:32:54 +08:00
hrz 11207f793d update:登录类型按钮居中显示 2025-09-26 23:56:40 +08:00
hrz ce09eb48c4 update:共钥信息从旧接口获取,去除getServerPublicKey方法 2025-09-26 23:39:27 +08:00
hrzandGitHub 6925732f72 Merge pull request #2285 from myifeng/prompt-template-args
提示词模板支持更多参数
2025-09-26 19:07:41 +08:00
hrzandGitHub dd9d08c8c3 Merge pull request #2291 from xinnan-tech/performance
update:更新测试工具以测试讯飞流式ASR、TTS首词响应时间
2025-09-26 19:06:28 +08:00
hrzandGitHub 1443c883f1 Merge pull request #2292 from xinnan-tech/mobile-i18n
updata:移动端添加语言切换功能
2025-09-26 19:05:21 +08:00
hrzandGitHub 30676e2170 Merge branch 'main' into sm2_test 2025-09-26 19:03:23 +08:00
rainv123 e41e6d4284 updata:删除无用功能 2025-09-26 17:10:47 +08:00
rainv123 1a7b1d24fa updata:移动端添加语言切换功能 2025-09-26 17:03:17 +08:00
rainv123 99443d1c9f updata:移动端添加语言切换功能 2025-09-26 14:54:56 +08:00
rainv123 a84b8d818e updata:移动端添加语言切换功能 2025-09-26 14:53:16 +08:00
LiJinHui ef4abf08f0 格式调整 2025-09-26 14:44:20 +08:00
rainv123 dfcac51312 updata:移动端添加语言切换功能 2025-09-26 14:33:06 +08:00
3030332422 c963a2de18 fix:修复验证图形验证码 2025-09-26 12:58:28 +08:00
3030332422 9ec8ecf7f4 update:SM2非对称加密的优化 2025-09-26 11:10:42 +08:00
FAN-yeB 5d77c96d76 update:更新测试工具以测试讯飞流式ASR、TTS首词响应时间 2025-09-26 11:05:00 +08:00
LiJinHui a8932f3743 秘钥加密 2025-09-26 10:04:43 +08:00
欣南科技andGitHub d022dd385a Merge pull request #2286 from xinnan-tech/Data_display_update
Data display update
2025-09-25 16:12:42 +08:00
hrz b2dfbb7973 update:优化判断 2025-09-25 16:12:02 +08:00
LiJinHui 7fee922796 更新设备数量显示(实时) 2025-09-25 15:56:51 +08:00
3030332422 9b6acf1ffc fix:修复SM2非对称加密的各种问题 2025-09-25 15:50:32 +08:00
hrzandGitHub a0e7cb56dc Merge pull request #2284 from xinnan-tech/mqtt_doc
update:更新mqtt网关集成文档
2025-09-25 14:54:51 +08:00
hrz f8447033b9 update:优化 2025-09-25 14:54:05 +08:00
hrz bb07015feb update:抽离公共教程到最前面 2025-09-25 14:51:34 +08:00
Chingfeng Li 111d945a9a 提示词模板支持更多参数 2025-09-25 14:35:01 +08:00
FAN-yeB d1587cc645 update:更新mqtt网关集成文档 2025-09-25 14:31:23 +08:00
欣南科技andGitHub e50e21c240 Merge pull request #2279 from xinnan-tech/update_doc
update:更新文档
2025-09-25 12:26:14 +08:00
hrz 9bbcf7f31a update:更新文档 2025-09-25 12:25:36 +08:00
hrzandGitHub 4f29e375c3 Merge pull request #2277 from xinnan-tech/py_fix_tts
fix: Markdown调整
2025-09-25 11:05:12 +08:00
Sakura-RanChen e15c784bdb fix: 文档链接 2025-09-25 10:57:12 +08:00
Sakura-RanChen e77a3b79cd fix: Markdown调整 2025-09-25 10:17:56 +08:00
3030332422 7ac5113fef update:非对称加密注册功能 2025-09-25 09:15:41 +08:00
hrzandGitHub ae58537979 Merge pull request #2275 from xinnan-tech/py_indexTTS
update: 更新文档
2025-09-24 17:01:02 +08:00
Sakura-RanChen ebf10828f0 update: 更新文档 2025-09-24 16:35:26 +08:00
3030332422 33bad7ed40 update:前端SM2非对称加密登录功能 2025-09-23 20:22:12 +08:00
hrzandGitHub af9ed0cd86 Merge pull request #2266 from xinnan-tech/py_mqtt
update:增加单模块mqtt-gateway支持
2025-09-23 11:51:41 +08:00
FAN-yeB 14a0829190 更新ota下发mqtt协议逻辑 2025-09-23 11:37:28 +08:00
FAN-yeB ca400d8ea7 Revert "更改下发配置"
This reverts commit 8891a96731.
2025-09-23 11:29:05 +08:00
hrzandGitHub d28750dae0 Merge pull request #2264 from xinnan-tech/py_xunfei_tts
update:添加讯飞语音识别、大模型、语音合成供应器
2025-09-23 11:06:00 +08:00
hrzandGitHub ef81a17311 Merge pull request #2273 from xinnan-tech/i18n-fix
fix:修改用户管理界面用户的密码重置、禁用、删除的逻辑
2025-09-23 11:05:17 +08:00
FAN-yeB 8891a96731 更改下发配置 2025-09-23 10:45:25 +08:00
rainv123 49e69ecad4 fix:修改用户管理界面用户的密码重置、禁用、删除的逻辑 2025-09-23 10:16:17 +08:00
rainv123 1db2483557 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-23 10:08:55 +08:00
rainv123 7067050006 Revert "fix:修改用户管理界面的用户密码重置、禁用、删除的逻辑"
This reverts commit 9f303d840a.
2025-09-23 10:03:43 +08:00
FAN-yeBandGitHub 55b455216c Merge pull request #2267 from xinnan-tech/i18n-fix
i18n-fix
2025-09-23 09:58:36 +08:00
3030332422 ffba2f4fa3 update:添加SM2国密加密的登录接口 2025-09-23 09:58:27 +08:00
rainv123 9f303d840a fix:修改用户管理界面的用户密码重置、禁用、删除的逻辑 2025-09-23 09:49:13 +08:00
FAN-yeB 86b4e61c1c 添加讯飞星火大模型的配置示例 2025-09-23 09:06:27 +08:00
欣南科技andGitHub 3232b36e15 Merge pull request #2271 from xinnan-tech/fix-docker-web
update:优化编译
2025-09-22 21:35:04 +08:00
hrz 83329cf9db update:优化编译 2025-09-22 21:34:09 +08:00
rainv123 4608c81ea6 updata:补充国际化键名 2025-09-22 15:04:49 +08:00
rainv123 41679f4e1d updata:调整文本顺序 2025-09-22 14:55:22 +08:00
FAN-yeB 559699ec19 update:增加单模块mqtt-gateway支持 2025-09-22 14:18:03 +08:00
hrzandGitHub f785219a43 Merge pull request #2265 from xinnan-tech/LJH
update:修改排序
2025-09-22 11:24:41 +08:00
FAN-yeB bf2b6d7554 还原错误更改 2025-09-22 10:10:46 +08:00
FAN-yeB 9a6348c71a 更新 202509220958.sql 2025-09-22 10:01:30 +08:00
FAN-yeB 646f4cbfe9 更新讯飞星火认知大模型配置的说明文档 2025-09-22 10:00:29 +08:00
LiJinHui d4c6b6840e 增加排序更新逻辑 2025-09-22 09:52:21 +08:00
LiJinHui 758aa1c78c 修改排序的逻辑 2025-09-22 09:41:18 +08:00
FAN-yeB ec2b52ccac 优化讯飞asr参数 2025-09-22 09:34:05 +08:00
FAN-yeB 38e1eb0c18 更新 202509191732.sql 2025-09-22 09:25:43 +08:00
hrzandGitHub 00e8478f61 Merge pull request #2251 from xinnan-tech/LJH
update:新增调整角色模版的功能,添加新功能的国际化
2025-09-21 18:10:05 +08:00
hrzandGitHub 0c556b1424 Merge branch 'main' into LJH 2025-09-21 18:09:48 +08:00
hrz 9987555a72 Bump to v0.8.3 2025-09-21 18:07:48 +08:00
hrz ebb4ba5595 update:调整功能顺序 2025-09-21 18:04:56 +08:00
LiJinHui 7e338b28df 删除多余定义 2025-09-21 16:44:18 +08:00
LiJinHui c4b3e30c5d 删除多的定义 2025-09-21 16:41:53 +08:00
LiJinHui 4d283c3504 去掉调试代码 2025-09-21 01:51:35 +08:00
LiJinHui ffc1541343 删除多余代码 2025-09-21 00:25:23 +08:00
LiJinHui 588d75763c 删除多余样式 2025-09-20 23:43:58 +08:00
LiJinHui 9e218281da 调整代码 2025-09-20 17:30:00 +08:00
欣南科技andGitHub 6fa58c7948 Merge pull request #2257 from xinnan-tech/update_doc
update:优化图片
2025-09-20 08:13:51 +08:00
hrz 7fe40579b9 update:优化图片 2025-09-20 08:13:04 +08:00
欣南科技andGitHub 7e2782ce22 Merge pull request #2256 from xinnan-tech/update_doc
update:架构图增加mqtt
2025-09-20 08:04:30 +08:00
hrz 1fbe8dab77 update:架构图增加mqtt 2025-09-20 08:04:00 +08:00
欣南科技andGitHub 89ef36711a Merge pull request #2255 from xinnan-tech/update-version
update:推出0.8.2
2025-09-20 01:32:28 +08:00
hrz 0ba3e2c964 update:推出0.8.2 2025-09-20 01:31:48 +08:00
LiJinHui 0aec742bd3 格式统一 2025-09-19 18:33:54 +08:00
LiJinHui a6f5d868dd 格式调整 2025-09-19 18:18:18 +08:00
FAN-yeB 966d8a8a84 update:xunfei asr 2025-09-19 17:54:52 +08:00
LiJinHui 611d1a65e7 Update index.js
权限调整
2025-09-19 17:53:43 +08:00
LiJinHui b23e31bb4b Delete DefaultRoleDialog.vue
删除多余文件
2025-09-19 17:41:11 +08:00
Sakura-RanChen bec1c7b88f update: 增加讯飞双流式TTS 2025-09-19 17:29:09 +08:00
LiJinHui 5d407b9227 调整模版排序的漏洞 2025-09-19 17:18:24 +08:00
CGDandGitHub e5bc029d87 Merge pull request #2252 from xinnan-tech/manager-web-fix
updata:删除多余的isEnglish方法
2025-09-19 16:06:39 +08:00
LiJinHui 608abb7dc3 调整页面 2025-09-19 15:41:28 +08:00
LiJinHui 5fcc7592c9 优化代码,调整权限设置( 非管理员打开提示拒绝访问并自动跳转到首页),修复bug; 2025-09-19 15:34:43 +08:00
rainv123 3fb925f13d updata:删除多余的getServerList方法,删除重复定义的computed块 2025-09-19 11:37:24 +08:00
rainv123 4e958ce57c updata:删除多余的isEnglish方法 2025-09-19 11:15:11 +08:00
rainv123 7f6ce89e39 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-19 09:33:47 +08:00
欣南科技andGitHub 71274cf0a3 Merge pull request #2249 from xinnan-tech/hot-fix
update:优化未配置mqtt网关时的显示
2025-09-18 19:08:56 +08:00
hrz 58127cdfdc update:优化未配置mqtt网关时的显示 2025-09-18 19:08:24 +08:00
LiJinHui 09d5d37867 完善导航栏的语言切换 2025-09-18 16:54:00 +08:00
LiJinHui 6a8c4ccbf8 添加国际化的功能,调整页面 2025-09-18 16:43:52 +08:00
hrzandGitHub 6d7bbfca95 Merge pull request #2247 from xinnan-tech/mqtt-call
Mqtt 远程工具调用
2025-09-18 15:02:45 +08:00
hrzandGitHub 3de39c8cdc Merge branch 'main' into mqtt-call 2025-09-18 15:02:32 +08:00
3030332422 387398a069 update:添加多语言支持并增强搜索功能 2025-09-18 14:42:03 +08:00
3030332422 f17011939c update:优化设备工具调用界面与执行结果展示体验 2025-09-18 10:56:41 +08:00
rainv123 9b6f564d30 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-18 10:54:30 +08:00
rainv123andGitHub e23989add0 Merge pull request #2239 from xinnan-tech/py_AliBL_TTS
update: 阿里百炼流式TTS 减少调试日志显示
2025-09-18 10:22:42 +08:00
LiJinHui a199e47701 页面布局调整 2025-09-18 09:35:03 +08:00
3030332422 4f64316144 update: 实现设备工具动态调用和执行功能 2025-09-17 17:40:20 +08:00
rainv123 68e10f42f8 Merge branch 'py_AliBL_TTS' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-17 17:02:47 +08:00
FAN-yeBandGitHub 3d1cae4692 Merge pull request #2242 from xinnan-tech/manager-api-msg-i18n
updata:实现后端msg的国际化处理,修改用户注册页面和添加模型页面的硬编码为国际化键名
2025-09-17 16:46:45 +08:00
3030332422 31e84dadd1 update:动态获取设备工具列表并优化显示逻辑 2025-09-17 16:25:46 +08:00
LiJinHui c331b8322c 新增调整角色模版的功能 2025-09-17 15:38:23 +08:00
rainv123 ba4423684b Merge branch 'manager-api-msg-i18n' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-17 15:36:31 +08:00
rainv123 62eaf46f34 updata:实现后端msg的国际化处理,修改用户注册页面和添加模型页面的硬编码为国际化键名 2025-09-17 15:35:21 +08:00
FAN-yeB 3767bf8c81 更新 messages.properties 2025-09-17 15:31:54 +08:00
rainv123 300a33ddc2 fix:智能体添加设备页面的按钮文字修正 2025-09-17 14:10:07 +08:00
rainv123 34d7bd92ca updata:实现后端msg的国际化处理,修改用户注册页面和添加模型页面的硬编码为国际化键名 2025-09-17 11:36:25 +08:00
3030332422 44e7380c6b update: 添加设备管理页面实时在线状态显示功能 2025-09-17 11:36:20 +08:00
FAN-yeB 15ca08ce14 update:接口 2025-09-17 10:04:19 +08:00
hrzandGitHub 83b88eeaa0 Merge pull request #2240 from myifeng/prompt-device-id
添加设备ID信息到提示词
2025-09-16 18:21:02 +08:00
Chingfeng Li 290c92ee39 添加设备ID信息到提示词 2025-09-16 17:42:15 +08:00
Sakura-RanChen c2985c0db3 update: 阿里百炼流式TTS 减少调试日志显示
fix: 音频发送误差修正
2025-09-16 17:07:00 +08:00
Sakura-RanChenandGitHub 334dfc7917 Merge pull request #2231 from qingshuiyuyu/bugfix
修复-火山双流式语音合成-additions参数配置
2025-09-16 10:43:45 +08:00
hrz 03b51f73e7 update:远程工具调用面板 2025-09-16 00:06:57 +08:00
欣南科技andGitHub af079d6c18 Merge pull request #2236 from xinnan-tech/mqtt-sign-key
update:mqtt密钥禁止包含弱密码
2025-09-15 21:32:05 +08:00
hrz 99008cf813 update:mqtt密钥禁止包含弱密码 2025-09-15 21:31:22 +08:00
CGDandGitHub 5fcb670821 Merge pull request #2233 from xinnan-tech/manage-web-i18n-fix
fix:优化智控台页面的多语言支持
2025-09-15 14:49:38 +08:00
hrzandGitHub 3bdf8e20c7 Merge pull request #2232 from xinnan-tech/py_fix_index
fix: 参数修正
2025-09-15 13:20:16 +08:00
rainv123 e8d44b13cf fix:优化智控台页面的多语言支持 2025-09-15 11:32:57 +08:00
Sakura-RanChen c6a7871e5b fix: 参数修正 2025-09-15 10:33:56 +08:00
shiyin 4c9824b16f 修复-火山双流式语音合成-additions参数配置 2025-09-15 09:46:07 +08:00
CGDandGitHub 3e54a3272a Merge pull request #2229 from kerenskiy/patch-1
Update requirements.txt
2025-09-15 09:04:57 +08:00
kerenskiyandGitHub 8851406f84 Update requirements.txt
Fix VoskASR dependencies
2025-09-14 20:18:40 +03:00
欣南科技andGitHub fe85ea049a Merge pull request #2224 from xinnan-tech/hot-fix
update:mqtt密钥复杂度校验
2025-09-13 10:19:37 +08:00
hrz 221c01e642 update:mqtt密钥复杂度校验 2025-09-13 10:18:41 +08:00
欣南科技andGitHub 753aa280f6 Merge pull request #2223 from xinnan-tech/hot-fix
fix:保存完参数后,按钮还在saving状态的bug
2025-09-13 09:22:41 +08:00
hrz 64f3062eb2 fix:保存完参数后,按钮还在saving状态的bug 2025-09-13 09:22:11 +08:00
欣南科技andGitHub f5143d60b7 Merge pull request #2221 from xinnan-tech/i18n-update
update:优化英文显示
2025-09-13 00:49:12 +08:00
hrz e9fb36bf8f update:优化英文显示 2025-09-13 00:47:48 +08:00
欣南科技andGitHub 11e8736e66 Merge pull request #2219 from xinnan-tech/i18n-update
Bump to v0.8.1
2025-09-12 20:52:52 +08:00
hrz 30ce36b169 Bump to v0.8.1 2025-09-12 20:52:20 +08:00
欣南科技andGitHub 1541128e03 Merge pull request #2218 from xinnan-tech/i18n-update
update:优化英文显示
2025-09-12 18:44:32 +08:00
hrz 9d808ec733 update:优化英文显示 2025-09-12 18:43:40 +08:00
欣南科技andGitHub 8f526820a8 Merge pull request #2217 from xinnan-tech/i18n-update
update:优化英文显示
2025-09-12 18:33:45 +08:00
hrz c0689c6916 update:优化英文显示 2025-09-12 18:33:14 +08:00
欣南科技andGitHub 893872e509 Merge pull request #2216 from xinnan-tech/manager-web-i18n
Manager web i18n
2025-09-12 18:11:15 +08:00
hrz 2c4b92f941 Merge branch 'main' into manager-web-i18n 2025-09-12 18:09:58 +08:00
hrz 21cc1153b0 update:优化显示 2025-09-12 18:09:09 +08:00
CGDandGitHub 3c35380e9d Merge pull request #2215 from xinnan-tech/manager-web-i18n
fix:修复用户管理页面多语言支持的问题
2025-09-12 17:56:06 +08:00
rainv123 2b65cfd369 fix:修复用户管理页面多语言支持的问题 2025-09-12 17:43:22 +08:00
CGDandGitHub 8a44aaa7f9 Merge pull request #2211 from xinnan-tech/manager-web-i18n
updata:智控台页面添加语言切换,支持中英繁
2025-09-12 16:55:53 +08:00
rainv123 4e0bf5542c updata:智控台页面添加语言切换,支持中英繁 2025-09-12 16:14:44 +08:00
rainv123 f788e45bc1 updata:智控台页面添加语言切换,支持中英繁 2025-09-12 15:11:56 +08:00
rainv123 5b4381c257 updata:智控台页面添加语言切换,支持中英繁 2025-09-12 11:41:31 +08:00
rainv123 7396605844 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-12 11:36:24 +08:00
hrzandGitHub b0308af05d Merge pull request #2210 from xinnan-tech/pertext
update:更新流式ASR首词延迟测试工具
2025-09-11 18:01:32 +08:00
rainv123 98a1483032 updata:智控台页面添加语言切换,支持中英繁。 2025-09-11 17:10:50 +08:00
FAN-yeB 07242dbfc0 update:更新流式ASR首词延迟测试工具 2025-09-11 16:32:07 +08:00
hrzandGitHub a9b0775469 Merge pull request #2195 from myifeng/upgrade-requirements
更新一些常用依赖库
2025-09-11 15:06:17 +08:00
hrzandGitHub 26a49ba34c numpy1.26.4和tourch2.2.2兼容,其他版本与兼容性问题 2025-09-11 14:56:53 +08:00
hrzandGitHub f3431ea977 Merge pull request #2203 from xinnan-tech/py_fix_intentllm
fix:修复LLM意图识别的若干问题
2025-09-11 14:46:10 +08:00
3030332422 f0a7ee1c0c fix:修复LLM意图识别 2025-09-11 14:38:27 +08:00
欣南科技andGitHub ad5f085c1f Merge pull request #2208 from xinnan-tech/update_doc
fix:补充文档链接
2025-09-11 13:50:18 +08:00
hrz e7f5f5da34 fix:补充文档链接 2025-09-11 13:49:42 +08:00
欣南科技andGitHub d10b632875 Merge pull request #2207 from xinnan-tech/update_doc
update:添加mqtt网关架设教程
2025-09-11 13:47:38 +08:00
hrz 8dd39c5a5b update:添加mqtt网关架设教程 2025-09-11 13:46:35 +08:00
hrzandGitHub cdd1345c19 Merge pull request #1759 from xinnan-tech/mqtt
Mqtt gateway function
2025-09-10 22:32:37 +08:00
hrzandGitHub 84067a1f57 Merge branch 'main' into mqtt 2025-09-10 22:30:25 +08:00
hrz 26fd3110b4 update:优化代码 2025-09-10 22:20:43 +08:00
hrz 5bd78cac1a update:优化代码 2025-09-10 21:40:25 +08:00
3030332422 0738c52d14 fix:修复LLM意图识别的若干问题 2025-09-10 18:00:39 +08:00
FAN-yeB c93cb3bb52 Reapply "Merge branch 'mqtt' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mqtt"
This reverts commit eea7689c3d.
2025-09-10 17:51:45 +08:00
FAN-yeB eea7689c3d Revert "Merge branch 'mqtt' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mqtt"
This reverts commit 7b9e34c3e5, reversing
changes made to f81be33539.
2025-09-10 17:48:31 +08:00
FAN-yeB 7b9e34c3e5 Merge branch 'mqtt' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mqtt 2025-09-10 17:47:13 +08:00
FAN-yeB f81be33539 优化mqtt启用 2025-09-10 17:46:55 +08:00
hrzandGitHub 21a1cb65fc Merge pull request #2202 from xinnan-tech/mqtt2
update:优化智控台参数名称
2025-09-10 17:39:24 +08:00
hrz ef02a01439 update:优化智控台参数名称 2025-09-10 17:38:29 +08:00
hrz 0b22f4bbd2 Merge branch 'mqtt2' into mqtt
# Conflicts:
#	main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
#	main/manager-api/src/main/java/xiaozhi/modules/device/controller/OTAController.java
#	main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceReportRespDTO.java
#	main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-09-10 12:30:52 +08:00
hrzandGitHub 9f8ae62ebb Merge pull request #2197 from ifnofi/tts_aliyun_cosyvoice
添加阿里云CosyVoice 和 修复文件编码
2025-09-10 11:54:00 +08:00
hrzandGitHub cec71a28eb 已有阿里云cosyvoice流式接口
已有阿里云cosyvoice流式接口,位于main/xiaozhi-server/core/providers/tts/aliyun_stream.py
2025-09-10 11:52:09 +08:00
hrzandGitHub 7d060e9994 Merge pull request #2194 from xinnan-tech/Qwen3_asr
update:更新Qwen-asr-flash语音识别
2025-09-10 11:42:51 +08:00
hrzandGitHub a3a6e889ee Merge branch 'main' into Qwen3_asr 2025-09-10 11:42:42 +08:00
ifnofi 80283db9bd 添加阿里云CosyVoice 和 修复文件编码
添加阿里云的CosyVoice功能
更新wakeup_word.py,在读写配置文件时指定UTF-8编码,以确保正确处理非ascii字符。
2025-09-10 11:17:56 +08:00
FAN-yeB 906a322675 更新Qwen3-ASR-Flash模型配置的说明文档 2025-09-10 11:10:34 +08:00
hrzandGitHub bc829d9ef0 Merge pull request #2170 from xinnan-tech/fix
fix:增加检测到手机浏览器跳转h5页面,修改h5页面图标
2025-09-09 22:52:03 +08:00
myifengandGitHub c5e6a46450 Update requirements.txt 2025-09-09 19:35:32 +08:00
FAN-yeB 77f0bdc4ca update:更新Qwen-asr-flash语音识别 2025-09-09 17:56:05 +08:00
CGDandGitHub 8b9f13283b Merge pull request #2186 from xinnan-tech/py_MinmaxStreamTTS_test
Py minmax stream tts test
2025-09-09 13:17:55 +08:00
CGDandGitHub c3c65708e0 Merge branch 'main' into py_MinmaxStreamTTS_test 2025-09-09 13:17:09 +08:00
3030332422 6bc64aa4aa update:删除非流式MiniMax TTS配置,保留流式版本 2025-09-09 11:38:55 +08:00
FAN-yeB eee907b1a2 兼容udp和websocket协议传输音频 2025-09-09 09:37:31 +08:00
hrzandGitHub ddd63f4136 Merge pull request #2190 from xinnan-tech/py_fix_voiceprint
update:声纹识别添加相似度阈值检查机制
2025-09-09 09:15:29 +08:00
CGDandGitHub b12c1efc90 Merge branch 'main' into py_fix_voiceprint 2025-09-08 18:13:51 +08:00
hrzandGitHub 01838f598c Merge pull request #2189 from myifeng/fix-voiceprint-enable
修正声纹识别开启校验
2025-09-08 17:57:07 +08:00
hrzandGitHub aef6d5dc1d 在不增加额外配置的情况下进行优化 2025-09-08 17:55:48 +08:00
hrzandGitHub 9262963f20 在不增加额外配置的情况下进行优化 2025-09-08 17:55:41 +08:00
Sakura-RanChenandGitHub d3e2aaacb9 Merge pull request #2126 from xinnan-tech/py_test
添加vosk
2025-09-08 17:47:04 +08:00
3030332422 755c81286c update:修改vosk 2025-09-08 17:35:18 +08:00
hrzandGitHub 9dd2d998e2 Merge branch 'main' into fix 2025-09-08 16:59:54 +08:00
Chingfeng Li abbb13161e Merge branch 'fix-voiceprint-enable' of https://github.com/myifeng/xiaozhi-esp32-server into fix-voiceprint-enable 2025-09-08 16:27:38 +08:00
Chingfeng Li 23dc6f1ea9 声纹识别功能开关 2025-09-08 16:26:39 +08:00
3030332422 8aed23f21a update:智控台添加相似度阈值配置 2025-09-08 16:25:08 +08:00
hrzandGitHub ae117e3579 Merge pull request #2188 from myifeng/weather-config
优化天气配置
2025-09-08 15:49:37 +08:00
Chingfeng Li 0d19c693a9 优化天气配置 2025-09-08 14:24:50 +08:00
hrzandGitHub 283e57ff71 Merge pull request #2165 from myifeng/sse-mcp-fix
优化SSE Client连接;增加SSE MCP示例
2025-09-08 13:28:25 +08:00
FAN-yeB eed9503391 mqtt 2025-09-08 11:04:42 +08:00
3030332422 d66db60177 update:修改声纹日志 2025-09-08 09:32:10 +08:00
3030332422 25d9e6c8cf update: 声纹识别添加相似度阈值检查机制 2025-09-07 15:30:04 +08:00
Sakura-RanChen a81f433228 update: 增加相关配置 2025-09-05 17:52:30 +08:00
3030332422 257e365299 Merge remote-tracking branch 'origin/main' into py_test 2025-09-05 14:56:08 +08:00
Sakura-RanChen 554afa3f46 Merge branch 'main' into py_MinmaxStreamTTS_test 2025-09-05 14:53:02 +08:00
欣南科技andGitHub d04ec9d510 Merge pull request #2181 from xinnan-tech/update_version
update:升级版本号
2025-09-05 13:59:14 +08:00
hrz 4a802e00e4 update:升级版本号 2025-09-05 13:58:26 +08:00
CGDandGitHub a7007f5ae2 Merge pull request #2178 from xinnan-tech/future
fix: 方法修正
2025-09-05 10:37:21 +08:00
Sakura-RanChen 2b3cacd5b0 fix: 方法修正 2025-09-05 10:31:47 +08:00
CGDandGitHub 49565b4b3c Merge pull request #2158 from xinnan-tech/future
Future
2025-09-05 10:16:39 +08:00
CGDandGitHub bd8ae27f92 Merge branch 'main' into future 2025-09-05 10:14:52 +08:00
rainv123andruoyemuzimi 3fe0b8b669 fix:修改手机浏览器访问web后台跳转至h5页面的逻辑 2025-09-05 10:09:32 +08:00
Sakura-RanChenandGitHub f0e0b97217 Merge pull request #2166 from xinnan-tech/py_test_texthandle
textHandle.py 消息解耦
2025-09-05 09:14:32 +08:00
rainv123andruoyemuzimi 67e07c916e docs:添加火山双向流式TTS+声音克隆配置教程 2025-09-04 16:37:28 +08:00
rainv123andruoyemuzimi b3a6ae2441 fix:增加检测到手机浏览器跳转h5页面,修改h5页面图标 2025-09-04 09:32:53 +08:00
Chingfeng Li 74f321a0fd 补充兼容方法和过期警告 2025-09-03 17:53:02 +08:00
hrzandGitHub 64c841d00a Merge pull request #2156 from xinnan-tech/py_fix_FishTTS
fix: 变量名冲突
2025-09-03 17:49:23 +08:00
Sakura-RanChen 5afa2fdc98 fix: 降低误判 2025-09-03 17:29:21 +08:00
Chingfeng Li 74bc5dbb7d 优化SSE Client连接;增加SSE MCP示例 2025-09-03 15:41:35 +08:00
CGDandGitHub 5c67f2ecfd Merge pull request #2162 from myifeng/main
textHandle.py 消息解耦
2025-09-03 14:02:40 +08:00
Sakura-RanChen 66f382039c fix: 打断时状态未回正,提示无法正确播报 2025-09-03 10:46:24 +08:00
Chingfeng Li 3cfc1cf97a textHandle.py 消息解耦 2025-09-03 09:43:34 +08:00
Sakura-RanChen b88b102d38 修改注解 2025-09-02 17:08:56 +08:00
Sakura-RanChen fa3ddc2e2b 去除无用变量 2025-09-02 17:06:19 +08:00
Sakura-RanChen f45bc5d9c9 优化音频 2025-09-02 15:24:51 +08:00
Sakura-RanChen 79956ade97 fix: 变量名冲突 2025-09-02 14:36:13 +08:00
Sakura-RanChen b9df6d4ad2 fix: 优化 2025-09-02 14:34:26 +08:00
hrzandGitHub fa79e2b379 Merge pull request #2153 from xinnan-tech/py_test_if
docs: 添加天气插件使用指南
2025-09-02 14:05:36 +08:00
hrzandGitHub 5db638c075 Update weather-integration.md 2025-09-02 14:04:42 +08:00
3030332422 0f0a0814b7 fix:补充 2025-09-02 12:45:51 +08:00
3030332422 a842f0688f fix:优化天气插件指南 2025-09-02 12:36:04 +08:00
Sakura-RanChen c3c2e7c28e fix: 提示尾音播放和单模块第一次唤醒时等待tts初始化 2025-09-02 12:00:04 +08:00
3030332422 0364d1199b docs: 添加天气插件使用指南 2025-09-02 11:23:46 +08:00
Sakura-RanChen c428a9ddee fix: 唤醒机制 2025-09-02 11:18:04 +08:00
hrzandGitHub dba571727a Merge pull request #2149 from xinnan-tech/py_fix_news
fix:添加user-agent修复新闻403错误
2025-09-01 17:01:02 +08:00
3030332422 d31761e78a fix:添加user-agent修复新闻403错误 2025-09-01 16:08:51 +08:00
欣南科技andGitHub 63dfcde4c8 Merge pull request #2143 from xinnan-tech/update-doc
update:添加编译源
2025-08-31 08:07:23 +08:00
hrz 32e6ecf00d update:添加编译源 2025-08-31 08:06:41 +08:00
欣南科技andGitHub 2481509dc5 Merge pull request #2142 from xinnan-tech/update-doc
update:优化编译
2025-08-31 07:25:53 +08:00
hrz 44dd7fc58c update:优化编译 2025-08-31 07:25:14 +08:00
hrz 87f50e5253 update:更新支持的组件 2025-08-31 02:56:22 +08:00
3030332422 c82cf670de update:删去默认模型配置 2025-08-27 16:42:17 +08:00
3030332422 a3827aed6e fix:修改使用说明 2025-08-27 15:54:25 +08:00
3030332422 cc7ba0872c update:添加vosk 2025-08-27 14:39:05 +08:00
Sakura-RanChen 0676b87832 update: 增加MinMaxStreamTTS 2025-08-20 09:17:04 +08:00
marlonz 5f96f56574 remove gitignore 2025-07-05 13:50:47 +09:30
marlonz 076aaae944 mqtt gateway project 2025-07-05 13:47:49 +09:30
marlonz aed54bb3a6 tech review change 2025-07-05 13:30:14 +09:30
marlonz 7c03677295 update change log 2025-07-05 13:25:42 +09:30
marlonz 3d3875f14f update lq base sql 2025-07-05 12:34:57 +09:30
grincoolandGitHub f6dd493997 Merge pull request #1758 from grincool/xiaozhi-mqtt-dev
提交mqtt后端修改
2025-07-05 12:27:35 +09:30
grincoolandGitHub d5e3989ed1 Merge branch 'mqtt' into xiaozhi-mqtt-dev 2025-07-05 12:24:59 +09:30
marlonz b7c0201ca3 提交mqtt后端修改 2025-06-17 16:40:17 +09:30
207 changed files with 18320 additions and 3974 deletions
+12 -1
View File
@@ -31,6 +31,9 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -60,6 +63,10 @@ jobs:
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) }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_PROGRESS=plain
# 构建 manager-api 镜像
- name: Build and push manager-web
@@ -70,4 +77,8 @@ jobs:
push: true
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) }}
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_PROGRESS=plain
+9 -2
View File
@@ -3,10 +3,17 @@ FROM python:3.10-slim AS builder
WORKDIR /app
# 配置pip使用国内镜像源(阿里云)并设置超时和重试
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
pip config set global.trusted-host mirrors.aliyun.com && \
pip config set global.timeout 120 && \
pip config set install.retries 5
COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
# 安装Python依赖,使用并行下载
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
# 第二阶段:生产镜像
FROM python:3.10-slim
+10 -4
View File
@@ -18,12 +18,18 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
# 安装Nginx和字体库
RUN apk update && \
apk add --no-cache nginx bash && \
apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \
rm -rf /var/cache/apk/*
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
nginx \
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 && fc-cache -f -v
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v
# 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
+19 -20
View File
@@ -6,7 +6,7 @@
本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
支持MCP接入点声纹识别
支持MQTT+UDP协议、Websocket协议、MCP接入点声纹识别
</p>
<p align="center">
@@ -63,6 +63,13 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
@@ -84,13 +91,6 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture>
<img alt="成本最低配置" src="docs/images/demo4.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
@@ -241,7 +241,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
![请参考-全模块安装架构图](docs/images/deploy2.png)
| 功能模块 | 描述 |
|:---:|:---|
| 核心架构 | 基于WebSocketHTTP服务器,提供完整的控制台管理和认证系统 |
| 核心架构 | 基于[MQTT+UDP网关](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md)、WebSocketHTTP服务器,提供完整的控制台管理和认证系统 |
| 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
@@ -249,7 +249,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 意图识别 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 |
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
| 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理 |
| 指令下发 | 依托MQTT协议,支持从智控台将MCP指令下发到ESP32设备 |
| 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理;界面支持中文简体、中文繁体、英文显示 |
| 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 |
| 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 |
| 插件系统 | 支持功能插件扩展、自定义插件开发和插件热加载 |
@@ -263,13 +264,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
---
## 产品生态 👬
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
| 项目名称 | 项目地址 | 项目描述 |
|:---------------------|:--------|:--------|
| 小智安卓客户端 | [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/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的[优秀项目](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)
---
@@ -283,6 +278,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| dify 接口调用 | Dify | - |
| fastgpt 接口调用 | Fastgpt | - |
| coze 接口调用 | Coze | - |
| xinference 接口调用 | Xinference | - |
| homeassistant 接口调用 | HomeAssistant | - |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
@@ -302,8 +299,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:|
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、阿里云流式TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS、MinimaxTTS、火山双流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、Index-TTS、PaddleSpeech | Index-TTS、PaddleSpeech、FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3 |
---
@@ -320,7 +317,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
| 接口调用 | DoubaoASR、Doubao流式ASR、FunASRServer、TencentASR、AliyunASR、Aliyun流式ASR、百度ASR、OpenAI ASR | FunASRServer |
---
@@ -338,6 +335,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
| Memory | nomem | 无记忆模式 | 免费 | |
---
@@ -347,6 +345,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
| Intent | nointent | 无意图模式 | 免费 | 不进行意图识别,直接返回对话结果 |
---
+13 -12
View File
@@ -62,6 +62,13 @@ Want to see the usage effects? Click the videos below 🎥
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT command issuance" src="docs/images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
@@ -83,13 +90,6 @@ Want to see the usage effects? Click the videos below 🎥
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture>
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
@@ -237,15 +237,16 @@ This project provides the following testing tools to help you verify the system
![请参考-全模块安装架构图](docs/images/deploy2.png)
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
@@ -259,7 +260,7 @@ If you are a software developer, here is an [Open Letter to Developers](docs/con
---
## 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 |
|:---------------------|:--------|:--------|
@@ -280,7 +281,7 @@ Xiaozhi is an ecosystem. When using this product, you can also check out other e
| FastGPT interface calls | FastGPT | - |
| 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 |
|:---:|:---:|:---:|
| 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 |
---
+2 -1
View File
@@ -476,7 +476,8 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
8、[天气插件使用指南](./weather-integration.md)<br/>
## 语音克隆、本地语音部署相关教程
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
+1
View File
@@ -62,6 +62,7 @@ VAD:
### 7、部署相关教程
1、[如何进行最简化部署](./Deployment.md)<br/>
2、[如何进行全模块部署](./Deployment_all.md)<br/>
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
+29
View File
@@ -0,0 +1,29 @@
# 火山双向流式TTS+声音克隆配置教程
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
### 1.开通火山引擎服务
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App IdAccess TokenCluster ID以及声音ID(S_xxxxx)
### 2.克隆音色
克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy
准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py```
在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称
```python
train(appid=appid, token=token, audio_path=r".\audios\xiaohe.wav", spk_id=spk_id)
```
运行以下命令生成test_submit.mp3,点击播放试听克隆效果
```python
python uploadAndStatus.py
python tts_http_demo.py
```
回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。
### 3.填写配置文件
将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中
修改 resource_id的参数为``` volc.megatts.default```
(参考官方文档 https://www.volcengine.com/docs/6561/1329505
speaker的参数填入声音ID(S_xxxxx)
启动服务,唤醒小智发出的声音是克隆的音色即成功。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 260 KiB

+37 -14
View File
@@ -1,9 +1,12 @@
# IndexStreamTTS 使用指南
## 环境准备
### 1. 克隆项目 (这里使用的为VLLM版本)
### 1. 克隆项目 注意这里使用的为VLLM1.0的Releases版本)
```bash
git clone https://github.com/Ksuriuri/index-tts-vllm.git
https://github.com/Ksuriuri/index-tts-vllm/releases/tag/IndexTTS-vLLM-1.0
```
进入解压后的目录
```bash
cd index-tts-vllm
```
@@ -13,7 +16,7 @@ conda create -n index-tts-vllm python=3.12
conda activate index-tts-vllm
```
### 3. 安装PyTorch
### 3. 安装PyTorch 需要版本为2.8.0(最新版)
#### 查看显卡最高支持的版本和实际安装的版本
```bash
nvidia-smi
@@ -27,12 +30,11 @@ CUDA Version: 12.8
```bash
Cuda compilation tools, release 12.8, V12.8.89
```
#### 那么对应的安装命令 (请注意不要横跨大版本!!!)
#### 那么对应的安装命令pytorch默认给的是12.8的驱动版本)
```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.5.1(对应 vllm 0.7.3),并将 requirements.txt 中 vllm==0.9.0 修改为 vllm==0.7.3
需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/)
### 4. 安装依赖
```bash
@@ -54,7 +56,7 @@ git lfs install
```
创建模型目录,并拉取模型
```bash
mkkdir model_dir
mkdir model_dir
cd model_dir
git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git
```
@@ -71,6 +73,9 @@ bash convert_hf_format.sh model_dir/IndexTTS-1.5
### 6. 更改接口适配一下项目
接口返回数据与项目不适配需要调整一下,使其直接返回音频数据
```bash
vi api_server.py
```
```bash
@app.post("/tts", responses={
200: {"content": {"application/octet-stream": {}}},
@@ -104,7 +109,7 @@ async def tts_api(request: Request):
vi start_api.sh
```
### 将下面内容粘贴进去并按:输入wq保存
#### 脚本中的/home/system/indexTTS/index-tts-vllm/model_dir/IndexTTS-1.5 请自行修改为实际路径
#### 脚本中的/home/system/index-tts-vllm/model_dir/IndexTTS-1.5 请自行修改为实际路径
```bash
# 激活conda环境
conda activate index-tts-vllm
@@ -129,17 +134,35 @@ else
echo "已终止进程 $PID_VLLM"
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目录(如果不存在)
mkdir -p tmp
# 后台运行api_server.py,日志重定向到tmp/server.log
export VLLM_USE_V1=0
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 &
nohup python api_server.py --model_dir /home/system/index-tts-vllm/model_dir/IndexTTS-1.5 --port 11996 > tmp/server.log 2>&1 &
echo "api_server.py 已在后台运行,日志请查看 tmp/server.log"
```
给脚本执行权限并运行脚本
```bash
chmod +x tmp
chmod +x start_api.sh
./start_api.sh
```
日志会在tmp/server.log中输出,可以通过以下命令查看日志情况
@@ -148,7 +171,7 @@ tail -f tmp/server.log
```
## 音色配置
index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。
在项目根目录下的assets/speaker.json文件中配置自定义音色
在项目根目录下的assets/speaker.json文件中配置自定义音色
### 配置格式说明
```bash
{
@@ -161,5 +184,5 @@ index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和
]
}
```
### 注意
### 注意 (需重启服务进行注册)
添加后需在智控台中添加相应的说话人(单模块则更换相应的voice)
+179
View File
@@ -0,0 +1,179 @@
# MQTT 网关部署教程
`xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。
本教程分为三部分,你可以根据你是全模块部署还是单模块部署,选择对应的部分接入MQTT网关:
- 第一部分:部署MQTT网关
- 第二部分:全模块运行实现小智硬件MQTT+UDP连接
- 第三部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接
## 准备阶段
准备好你的`xiaozhi-server``mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址
1、如果你是源码部署,你的`mqtt-websocket`地址是:
```
ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway
```
2、如果你是docker部署,你的`mqtt-websocket`地址是
```
ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway
```
## 重要提示
如果你是服务器部署,需要确保服务器`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)
```bash
git clone https://ghfast.top/https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git
cd xiaozhi-mqtt-gateway
```
2. 安装依赖:
```bash
npm install
npm install -g pm2
```
3. 配置 `config.json`
```bash
cp config/mqtt.json.example config/mqtt.json
```
4. 编辑配置文件 config/mqtt.json,把你在`本文准备阶段``mqtt-websocket`地址替换到`chat_servers`里。例如源码部署的`xiaozhi-server`就是如下配置:
```
{
"production": {
"chat_servers": [
"ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway"
]
},
"debug": false,
"max_mqtt_payload_size": 8192,
"mcp_client": {
"capabilities": {
},
"client_info": {
"name": "xiaozhi-mqtt-client",
"version": "1.0.0"
},
"max_tools_count": 128
}
}
```
5. 在项目根目录创建下`.env`文件,并设置以下环境变量:
```
PUBLIC_IP=your-ip # 服务器公网IP
MQTT_PORT=1883 # MQTT服务器端口
UDP_PORT=8884 # UDP服务器端口
API_PORT=8007 # 管理API端口
MQTT_SIGNATURE_KEY=test # MQTT签名密钥
```
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
`MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,最好是设置成8个字符以上且同时包含大小写字母,这个密钥稍后还会用到。
- 注意不要用简单的密码,比如`123456`、`test`等。
- 注意不要用简单的密码,比如`123456`、`test`等。
- 注意不要用简单的密码,比如`123456`、`test`等。
6. 启动MQTT网关
```
# 启动服务
pm2 start ecosystem.config.js
# 查看日志
pm2 logs xz-mqtt
```
当你看到如下日志,说明MQTT网关启动成功:
```
0|xz-mqtt | 2025-09-11T12:14:48: MQTT 服务器正在监听端口 1883
0|xz-mqtt | 2025-09-11T12:14:48: UDP 服务器正在监听 x.x.x.x:8884
```
如果需要重启MQTT网关,执行如下命令:
```
pm2 restart xz-mqtt
```
## 第二部分:全模块运行实现小智硬件MQTT+UDP连接
查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。
1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
```
192.168.0.7:1883
```
2. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_signature_key`,点击编辑,填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。
3. 在智控台顶部,点击`参数管理`,搜索`server.udp_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
```
192.168.0.7:8884
```
4. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_manager_api`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
```
192.168.0.7:8007
```
上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址
```
curl 'http://localhost:8002/xiaozhi/ota/' \
-H 'Content-Type: application/json' \
-H 'Client-Id: 7b94d69a-9808-4c59-9c9b-704333b38aff' \
-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":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-gateway的日志,确认是否有连接成功的日志。
```
pm2 logs xz-mqtt
```
+16 -6
View File
@@ -1,5 +1,9 @@
# PaddleSpeechTTS集成xiaozhi服务
## 重点说明
- 优点:本地离线部署、速度快
- 缺点:截止2025年9月25日,默认的模型是中文模型,不支持英文转语音。如果含英文会发不出声音,如需同时支持中英文需要自己训练。
## 一、基础环境要求
操作系统:Windows / Linux / WSL 2
@@ -16,15 +20,21 @@ git clone https://github.com/PaddlePaddle/PaddleSpeech.git
```
### 2.建立虚拟环境
```bash
#请根据Paddle官方支持的python版本建立环境 ```https://www.paddlepaddle.org.cn/install```
conda create -n paddle_env python=3.10 -y
conda activate paddle_env
```
### 3.进入paddlespeech目录
### 3.安装paddle
因CPU架构、GPU架构不同,请根据Paddle官方支持的python版本建立环境
```
https://www.paddlepaddle.org.cn/install
```
### 4.进入paddlespeech目录
```bash
cd PaddleSpeech
```
### 4.安装paddlespeech
### 5.安装paddlespeech
```bash
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 paddlespeech -i https://pypi.tuna.tsinghua.edu.cn/simple
```
### 5.使用命令自动下载语音模型
### 6.使用命令自动下载语音模型
```bash
paddlespeech tts --input "你好,这是一次测试"
```
此步骤会自动下载模型缓存至本地 .paddlespeech/models 目录
### 6.修改tts_online_application.yaml配置
### 7.修改tts_online_application.yaml配置
参考目录 ```"PaddleSpeech\demos\streaming_tts_server\conf\tts_online_application.yaml"```
选择```tts_online_application.yaml```文件用编辑器打开,设置```protocol```为```websocket```
### 7.启动服务
### 8.启动服务
```yaml
paddlespeech_server start --config_file ./demos/streaming_tts_server/conf/tts_online_application.yaml
#官方默认启动命令:
+64
View File
@@ -0,0 +1,64 @@
# 天气插件使用指南
## 概述
天气插件 `get_weather` 是小智ESP32语音助手的核心功能之一,支持通过语音查询全国各地的天气信息。插件基于和风天气API,提供实时天气和7天天气预报功能。
## API Key 申请指南
### 1. 注册和风天气账号
1. 访问 [和风天气控制台](https://console.qweather.com/)
2. 注册账号并完成邮箱验证
3. 登录控制台
### 2. 创建应用获取API Key
1. 进入控制台后,点击右侧["项目管理"](https://console.qweather.com/project?lang=zh) → "创建项目"
2. 填写项目信息:
- **项目名称**:如"小智语音助手"
3. 点击保存
4. 项目创建完成后,在该项目中点击"创建凭据"
5. 填写凭据信息:
- **凭据名称**:如"小智语音助手"
- **身份认证方式**:选择"API Key"
6. 点击保存
7. 在凭据中复制`API Key`,这是第一个关键的配置信息
### 3. 获取API Host
1. 在控制台中点击["设置"](https://console.qweather.com/setting?lang=zh) → "API Host"
2. 查看分配给你的专属`API Host`地址,这个是第二个关键的配置信息
以上操作,会得到两个重要的配置信息:`API Key``API Host`
## 配置方式(任选一种)
### 方式1. 如果你使用了智控台部署(推荐)
1. 登录智控台
2. 进入"角色配置"页面
3. 选择要配置的智能体
4. 点击"编辑功能"按钮
5. 在右侧参数配置区域找到"天气查询"插件
6. 勾选"天气查询"
7. 将复制过来的第一个关键配置`API Key`,填入到`天气插件 API 密钥`
8. 将复制过来的第二个关键配置`API Host`,填入到`开发者 API Host`
9. 保存配置,再保存智能体配置
### 方式2. 如果你只是单模块xiaozhi-server部署
`data/.config.yaml` 中配置:
1. 将复制过来的第一个关键配置`API Key`,填入到`api_key`
2. 将复制过来的第二个关键配置`API Host`,填入到`api_host`
3. 将你所在的城市填入到`default_location`里,例如`广州`
```yaml
plugins:
get_weather:
api_key: "你的和风天气API密钥"
api_host: "你的和风天气API主机地址"
default_location: "你的默认查询城市"
```
+6
View File
@@ -199,6 +199,12 @@
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- BouncyCastle SM2加密 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78</version>
</dependency>
<!-- springdoc -->
<dependency>
<groupId>org.springdoc</groupId>
@@ -86,11 +86,26 @@ public interface Constant {
*/
String SERVER_SECRET = "server.secret";
/**
* SM2公钥
*/
String SM2_PUBLIC_KEY = "server.public_key";
/**
* SM2私钥
*/
String SM2_PRIVATE_KEY = "server.private_key";
/**
* websocket地址
*/
String SERVER_WEBSOCKET = "server.websocket";
/**
* mqtt gateway 配置
*/
String SERVER_MQTT_GATEWAY = "server.mqtt_gateway";
/**
* ota地址
*/
@@ -121,6 +136,11 @@ public interface Constant {
*/
String SERVER_VOICE_PRINT = "server.voice_print";
/**
* mqtt密钥
*/
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
/**
* 无记忆
*/
@@ -237,7 +257,7 @@ public interface Constant {
/**
* 版本号
*/
public static final String VERSION = "0.7.6";
public static final String VERSION = "0.8.3";
/**
* 无效固件URL
@@ -55,4 +55,124 @@ public interface ErrorCode {
int OTA_DEVICE_NOT_FOUND = 10041;
int OTA_DEVICE_NEED_BIND = 10042;
// 新增错误编码
int DELETE_DATA_FAILED = 10043;
int USER_NOT_LOGIN = 10044;
int WEB_SOCKET_CONNECT_FAILED = 10045;
int VOICE_PRINT_SAVE_ERROR = 10046;
int TODAY_SMS_LIMIT_REACHED = 10047;
int OLD_PASSWORD_ERROR = 10048;
int INVALID_LLM_TYPE = 10049;
int TOKEN_GENERATE_ERROR = 10050;
int RESOURCE_NOT_FOUND = 10051;
// 新增错误编码
int DEFAULT_AGENT_NOT_FOUND = 10052;
int AGENT_NOT_FOUND = 10053;
int VOICEPRINT_API_NOT_CONFIGURED = 10054;
int SMS_SEND_FAILED = 10055;
int SMS_CONNECTION_FAILED = 10056;
int AGENT_VOICEPRINT_CREATE_FAILED = 10057;
int AGENT_VOICEPRINT_UPDATE_FAILED = 10058;
int AGENT_VOICEPRINT_DELETE_FAILED = 10059;
int SMS_SEND_TOO_FREQUENTLY = 10060;
int ACTIVATION_CODE_EMPTY = 10061;
int ACTIVATION_CODE_ERROR = 10062;
int DEVICE_ALREADY_ACTIVATED = 10063;
// 默认模型删除错误
int DEFAULT_MODEL_DELETE_ERROR = 10064;
// 设备相关错误码
int MAC_ADDRESS_ALREADY_EXISTS = 10090; // Mac地址已存在
// 模型相关错误码
int MODEL_PROVIDER_NOT_EXIST = 10091; // 供应器不存在
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
// 登录相关错误码
int ADD_DATA_FAILED = 10065; // 新增数据失败
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
int SMS_CAPTCHA_ERROR = 10067; // 短信验证码错误
int MOBILE_REGISTER_DISABLED = 10068; // 未开启手机注册
int USERNAME_NOT_PHONE = 10069; // 用户名不是手机号码
int PHONE_ALREADY_REGISTERED = 10070; // 手机号码已注册
int PHONE_NOT_REGISTERED = 10071; // 手机号码未注册
int USER_REGISTER_DISABLED = 10072; // 不允许用户注册
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
int SMS_CODE_ERROR = 10075; // 手机验证码错误
// 字典类型相关错误码
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
// 资源处理相关错误码
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
// 智能体相关错误码
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
// 声纹相关错误码
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
int VOICEPRINT_UPDATE_NOT_ALLOWED = 10082; // 声纹修改不允许,声音已注册
int VOICEPRINT_UPDATE_ADMIN_ERROR = 10083; // 修改声纹错误,请联系管理员
int VOICEPRINT_API_URI_ERROR = 10084; // 声纹接口地址错误
int VOICEPRINT_AUDIO_NOT_BELONG_AGENT = 10085; // 音频数据不属于智能体
int VOICEPRINT_AUDIO_EMPTY = 10086; // 音频数据为空
int VOICEPRINT_REGISTER_REQUEST_ERROR = 10087; // 声纹保存请求失败
int VOICEPRINT_REGISTER_PROCESS_ERROR = 10088; // 声纹保存处理失败
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
// 服务端管理相关错误码
int INVALID_SERVER_ACTION = 10095; // 无效服务端操作
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
// 参数验证相关错误码
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
int WEBSOCKET_URL_FORMAT_ERROR = 10100; // WebSocket地址格式不正确
int WEBSOCKET_CONNECTION_FAILED = 10101; // WebSocket连接测试失败
int OTA_URL_EMPTY = 10102; // OTA地址不能为空
int OTA_URL_LOCALHOST = 10103; // OTA地址不能使用localhost或127.0.0.1
int OTA_URL_PROTOCOL_ERROR = 10104; // OTA地址必须以http或https开头
int OTA_URL_FORMAT_ERROR = 10105; // OTA地址必须以/ota/结尾
int OTA_INTERFACE_ACCESS_FAILED = 10106; // OTA接口访问失败
int OTA_INTERFACE_FORMAT_ERROR = 10107; // OTA接口返回内容格式不正确
int OTA_INTERFACE_VALIDATION_FAILED = 10108; // OTA接口验证失败
int MCP_URL_EMPTY = 10109; // MCP地址不能为空
int MCP_URL_LOCALHOST = 10110; // MCP地址不能使用localhost或127.0.0.1
int MCP_URL_INVALID = 10111; // 不是正确的MCP地址
int MCP_INTERFACE_ACCESS_FAILED = 10112; // MCP接口访问失败
int MCP_INTERFACE_FORMAT_ERROR = 10113; // MCP接口返回内容格式不正确
int MCP_INTERFACE_VALIDATION_FAILED = 10114; // MCP接口验证失败
int VOICEPRINT_URL_EMPTY = 10115; // 声纹接口地址不能为空
int VOICEPRINT_URL_LOCALHOST = 10116; // 声纹接口地址不能使用localhost或127.0.0.1
int VOICEPRINT_URL_INVALID = 10117; // 不是正确的声纹接口地址
int VOICEPRINT_URL_PROTOCOL_ERROR = 10118; // 声纹接口地址必须以http或https开头
int VOICEPRINT_INTERFACE_ACCESS_FAILED = 10119; // 声纹接口访问失败
int VOICEPRINT_INTERFACE_FORMAT_ERROR = 10120; // 声纹接口返回内容格式不正确
int VOICEPRINT_INTERFACE_VALIDATION_FAILED = 10121; // 声纹接口验证失败
int MQTT_SECRET_EMPTY = 10122; // mqtt密钥不能为空
int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全
int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母
int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复
int SM2_KEY_NOT_CONFIGURED = 10129; // SM2密钥未配置
int SM2_DECRYPT_ERROR = 10130; // SM2解密失败
int MODEL_TYPE_PROVIDE_CODE_NOT_NULL = 10131; // modelType和provideCode不能为空
// 聊天记录相关错误码
int CHAT_HISTORY_NO_PERMISSION = 10132; // 没有权限查看该智能体的聊天记录
int CHAT_HISTORY_SESSION_ID_NOT_NULL = 10133; // 会话ID不能为空
int CHAT_HISTORY_AGENT_ID_NOT_NULL = 10134; // 智能体ID不能为空
int CHAT_HISTORY_DOWNLOAD_FAILED = 10135; // 聊天记录下载失败
int DOWNLOAD_LINK_EXPIRED = 10136; // 下载链接已过期或无效
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
int CHAT_ROLE_USER = 10138; // 用户角色
int CHAT_ROLE_AGENT = 10139; // 智能体角色
}
@@ -13,6 +13,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.utils.MessageUtils;
import xiaozhi.common.utils.Result;
/**
@@ -62,7 +63,7 @@ public class RenExceptionHandler {
@ExceptionHandler(NoResourceFoundException.class)
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
return new Result<Void>().error(404, "资源不存在");
return new Result<Void>().error(404, MessageUtils.getMessage(ErrorCode.RESOURCE_NOT_FOUND));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@@ -76,7 +77,7 @@ public class RenExceptionHandler {
})
.filter(Objects::nonNull)
.findFirst()
.orElse("请求参数错误!");
.orElse(MessageUtils.getMessage(ErrorCode.PARAM_VALUE_NULL));
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
}
@@ -139,4 +139,11 @@ public class RedisKeys {
return "sms:Validate:Code:" + phone + ":today_count";
}
/**
* 聊天记录UUID映射的Key
*/
public static String getChatHistoryKey(String uuid) {
return "agent:chat:history:" + uuid;
}
}
@@ -6,6 +6,7 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import java.io.BufferedReader;
@@ -37,7 +38,7 @@ public class ResourcesUtils {
}
} catch (IOException e){
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
throw new RenException("读取资源失败");
throw new RenException(ErrorCode.RESOURCE_READ_ERROR);
}
return luaScriptBuilder.toString();
}
@@ -0,0 +1,130 @@
package xiaozhi.common.utils;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* SM2加密工具类(采用十六进制格式,与chancheng-archive-service项目保持一致)
*/
public class SM2Utils {
/**
* 公钥常量
*/
public static final String KEY_PUBLIC_KEY = "publicKey";
/**
* 私钥返回值常量
*/
public static final String KEY_PRIVATE_KEY = "privateKey";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* SM2加密算法
*
* @param publicKey 十六进制公钥
* @param data 明文数据
* @return 十六进制密文
*/
public static String encrypt(String publicKey, String data) {
try {
// 获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
// 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
//提取公钥点
ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
// 设置sm2为加密模式
sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
byte[] in = data.getBytes(StandardCharsets.UTF_8);
byte[] arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
return Hex.toHexString(arrayOfBytes);
} catch (Exception e) {
throw new RuntimeException("SM2加密失败", e);
}
}
/**
* SM2解密算法
*
* @param privateKey 十六进制私钥
* @param cipherData 十六进制密文数据
* @return 明文
*/
public static String decrypt(String privateKey, String cipherData) {
try {
// 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上
if (!cipherData.startsWith("04")) {
cipherData = "04" + cipherData;
}
byte[] cipherDataByte = Hex.decode(cipherData);
BigInteger privateKeyD = new BigInteger(privateKey, 16);
//获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
//构造domain参数
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
// 设置sm2为解密模式
sm2Engine.init(false, privateKeyParameters);
byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
return new String(arrayOfBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM2解密失败", e);
}
}
/**
* 生成密钥对
*/
public static Map<String, String> createKey() {
try {
ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
// 获取一个椭圆曲线类型的密钥对生成器
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
// 使用SM2参数初始化生成器
kpg.initialize(sm2Spec);
// 获取密钥对
KeyPair keyPair = kpg.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
BCECPublicKey p = (BCECPublicKey) publicKey;
PrivateKey privateKey = keyPair.getPrivate();
BCECPrivateKey s = (BCECPrivateKey) privateKey;
Map<String, String> result = new HashMap<>();
result.put(KEY_PUBLIC_KEY, Hex.toHexString(p.getQ().getEncoded(false)));
result.put(KEY_PRIVATE_KEY, Hex.toHexString(s.getD().toByteArray()));
return result;
} catch (Exception e) {
throw new RuntimeException("生成SM2密钥对失败", e);
}
}
}
@@ -0,0 +1,164 @@
package xiaozhi.common.utils;
import cn.hutool.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
/**
* 敏感数据处理工具类
*/
public class SensitiveDataUtils {
// 敏感字段列表
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList(
"api_key", "personal_access_token", "access_token", "token",
"secret", "access_key_secret", "secret_key"));
/**
* 检查字段是否为敏感字段
*/
public static boolean isSensitiveField(String fieldName) {
return StringUtils.isNotBlank(fieldName) && SENSITIVE_FIELDS.contains(fieldName.toLowerCase());
}
/**
* 隐藏字符串中间部分
*/
public static String maskMiddle(String value) {
if (StringUtils.isBlank(value)) {
return value;
}
int length = value.length();
if (length <= 8) {
// 短字符串保留前2后2
return value.substring(0, 2) + "****" + value.substring(length - 2);
} else {
// 长字符串保留前4后4
int maskLength = length - 8;
StringBuilder maskBuilder = new StringBuilder();
for (int i = 0; i < maskLength; i++) {
maskBuilder.append('*');
}
return value.substring(0, 4) + maskBuilder.toString() + value.substring(length - 4);
}
}
/**
* 判断字符串是否是被掩码处理过的值
*/
public static boolean isMaskedValue(String value) {
if (StringUtils.isBlank(value)) {
return false;
}
// 掩码值至少包含4个连续的*
return value.contains("****");
}
/**
* 处理JSONObject中的敏感字段
*/
public static JSONObject maskSensitiveFields(JSONObject jsonObject) {
if (jsonObject == null) {
return null;
}
JSONObject result = new JSONObject();
for (String key : jsonObject.keySet()) {
Object value = jsonObject.get(key);
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
result.put(key, maskMiddle((String) value));
} else if (value instanceof JSONObject) {
result.put(key, maskSensitiveFields((JSONObject) value));
} else {
result.put(key, value);
}
}
return result;
}
/**
* 比较两个JSONObject的敏感字段是否相同
* 特别针对api_key等敏感字段进行单独比较
*/
public static boolean isSensitiveDataEqual(JSONObject original, JSONObject updated) {
if (original == null && updated == null) {
return true;
}
if (original == null || updated == null) {
return false;
}
// 提取并比较特定敏感字段
return compareSpecificSensitiveFields(original, updated, "api_key") &&
compareSpecificSensitiveFields(original, updated, "personal_access_token") &&
compareSpecificSensitiveFields(original, updated, "access_token") &&
compareSpecificSensitiveFields(original, updated, "token") &&
compareSpecificSensitiveFields(original, updated, "secret") &&
compareSpecificSensitiveFields(original, updated, "access_key_secret") &&
compareSpecificSensitiveFields(original, updated, "secret_key");
}
/**
* 比较两个JSON对象中特定敏感字段是否相同
* 遍历整个JSON对象树,查找并比较指定敏感字段
*/
private static boolean compareSpecificSensitiveFields(JSONObject original, JSONObject updated, String fieldName) {
// 提取原始对象中的指定敏感字段
Map<String, String> originalFields = new HashMap<>();
extractSpecificSensitiveField(original, originalFields, fieldName, "");
// 提取更新对象中的指定敏感字段
Map<String, String> updatedFields = new HashMap<>();
extractSpecificSensitiveField(updated, updatedFields, fieldName, "");
// 如果字段数量不同,说明有增删
if (originalFields.size() != updatedFields.size()) {
return false;
}
// 比较每个字段的值
for (Map.Entry<String, String> entry : originalFields.entrySet()) {
String key = entry.getKey();
String originalValue = entry.getValue();
String updatedValue = updatedFields.get(key);
if (updatedValue == null || !updatedValue.equals(originalValue)) {
return false;
}
}
return true;
}
/**
* 递归提取JSON对象中指定名称的敏感字段
*/
private static void extractSpecificSensitiveField(JSONObject jsonObject, Map<String, String> fieldsMap,
String targetFieldName, String parentPath) {
if (jsonObject == null) {
return;
}
for (String key : jsonObject.keySet()) {
String fullPath = parentPath.isEmpty() ? key : parentPath + "." + key;
Object value = jsonObject.get(key);
if (value instanceof JSONObject) {
// 递归处理嵌套JSON对象
extractSpecificSensitiveField((JSONObject) value, fieldsMap, targetFieldName, fullPath);
} else if (value instanceof String && key.equalsIgnoreCase(targetFieldName)) {
// 找到目标敏感字段,保存其路径和值
fieldsMap.put(fullPath, (String) value);
}
}
}
}
@@ -0,0 +1,61 @@
package xiaozhi.common.utils;
import org.apache.commons.lang3.StringUtils;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* SM2解密和验证码验证工具类
* 封装了重复的SM2解密、验证码提取和验证逻辑
*/
public class Sm2DecryptUtil {
/**
* 验证码长度
*/
private static final int CAPTCHA_LENGTH = 5;
/**
* 解密SM2加密内容,提取验证码并验证
* @param encryptedPassword SM2加密的密码字符串
* @param captchaId 验证码ID
* @param captchaService 验证码服务
* @param sysParamsService 系统参数服务
* @return 解密后的实际密码
*/
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
CaptchaService captchaService, SysParamsService sysParamsService) {
// 获取SM2私钥
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
if (StringUtils.isBlank(privateKeyStr)) {
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
}
// 使用SM2私钥解密密码
String decryptedContent;
try {
decryptedContent = SM2Utils.decrypt(privateKeyStr, encryptedPassword);
} catch (Exception e) {
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
}
// 分离验证码和密码:前5位是验证码,后面是密码
if (decryptedContent.length() > CAPTCHA_LENGTH) {
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
// 验证嵌入的验证码是否正确
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
if (!embeddedCaptchaValid) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
return actualPassword;
} else {
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
}
}
}
@@ -1,5 +1,21 @@
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@@ -7,18 +23,37 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
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.modules.agent.dto.AgentChatHistoryDTO;
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.security.user.SecurityUser;
@Tag(name = "智能体聊天历史管理")
@RequiredArgsConstructor
@RestController
@RequestMapping("/agent/chat-history")
public class AgentChatHistoryController {
private static final Logger logger = LoggerFactory.getLogger(AgentChatHistoryController.class);
private final AgentChatHistoryBizService agentChatHistoryBizService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentService agentService;
private final RedisUtils redisUtils;
/**
* 小智服务聊天上报请求
@@ -33,4 +68,182 @@ public class AgentChatHistoryController {
Boolean result = agentChatHistoryBizService.report(request);
return new Result<Boolean>().ok(result);
}
/**
* 获取聊天记录下载链接
*
* @param agentId 智能体ID
* @param sessionId 会话ID
* @return UUID作为下载标识
*/
@Operation(summary = "获取聊天记录下载链接")
@RequiresPermissions("sys:role:normal")
@PostMapping("/getDownloadUrl/{agentId}/{sessionId}")
public Result<String> getDownloadUrl(@PathVariable("agentId") String agentId,
@PathVariable("sessionId") String sessionId) {
// 获取当前用户
UserDetail user = SecurityUser.getUser();
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
throw new RenException(ErrorCode.CHAT_HISTORY_NO_PERMISSION);
}
// 生成UUID
String uuid = UUID.randomUUID().toString();
// 存储agentId和sessionId到Redis,格式为agentId:sessionId
redisUtils.set(RedisKeys.getChatHistoryKey(uuid), agentId + ":" + sessionId);
return new Result<String>().ok(uuid);
}
/**
* 下载本会话聊天记录
*
* @param uuid 下载标识
* @param response HTTP响应
*/
@Operation(summary = "下载本会话聊天记录")
@GetMapping("/download/{uuid}/current")
public void downloadCurrentSession(@PathVariable("uuid") String uuid,
HttpServletResponse response) {
// 从Redis获取agentId和sessionId
String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid));
if (StringUtils.isBlank(agentSessionInfo)) {
throw new RenException(ErrorCode.DOWNLOAD_LINK_EXPIRED);
}
try {
// 解析agentId和sessionId
String[] parts = agentSessionInfo.split(":");
if (parts.length != 2) {
throw new RenException(ErrorCode.DOWNLOAD_LINK_INVALID);
}
String agentId = parts[0];
String sessionId = parts[1];
// 执行下载
downloadChatHistory(agentId, List.of(sessionId), response);
} finally {
// 下载完成后删除UUID,防止盗刷
redisUtils.delete(RedisKeys.getChatHistoryKey(uuid));
}
}
/**
* 下载本会话及前20条会话聊天记录
*
* @param uuid 下载标识
* @param response HTTP响应
*/
@Operation(summary = "下载本会话及前20条会话聊天记录")
@GetMapping("/download/{uuid}/previous")
public void downloadCurrentSessionWithPrevious(@PathVariable("uuid") String uuid,
HttpServletResponse response) {
// 从Redis获取agentId和sessionId
String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid));
if (StringUtils.isBlank(agentSessionInfo)) {
throw new RenException("下载链接已过期或无效");
}
try {
// 解析agentId和sessionId
String[] parts = agentSessionInfo.split(":");
if (parts.length != 2) {
throw new RenException("下载链接无效");
}
String agentId = parts[0];
String sessionId = parts[1];
// 获取所有会话列表
Map<String, Object> params = Map.of(
"agentId", agentId,
Constant.PAGE, 1,
Constant.LIMIT, 1000 // 获取足够多的会话
);
PageData<AgentChatSessionDTO> sessionPage = agentChatHistoryService.getSessionListByAgentId(params);
List<AgentChatSessionDTO> allSessions = sessionPage.getList();
// 查找当前会话在列表中的位置
int currentIndex = -1;
for (int i = 0; i < allSessions.size(); i++) {
if (allSessions.get(i).getSessionId().equals(sessionId)) {
currentIndex = i;
break;
}
}
// 如果找到了当前会话,收集当前会话及前20条会话ID
List<String> sessionIdsToDownload = new ArrayList<>();
if (currentIndex != -1) {
// 从当前会话开始,向后(数组后面)取最多20条会话(包括当前会话)
int endIndex = Math.min(allSessions.size() - 1, currentIndex + 20); // 确保不越界
for (int i = currentIndex; i <= endIndex; i++) {
sessionIdsToDownload.add(allSessions.get(i).getSessionId());
}
}
// 如果没有找到当前会话,至少下载当前会话
if (sessionIdsToDownload.isEmpty()) {
sessionIdsToDownload.add(sessionId);
}
downloadChatHistory(agentId, sessionIdsToDownload, response);
} finally {
// 下载完成后删除UUID,防止盗刷
redisUtils.delete(RedisKeys.getChatHistoryKey(uuid));
}
}
/**
* 下载指定会话的聊天记录
*
* @param agentId 智能体ID
* @param sessionIds 会话ID列表
* @param response HTTP响应
*/
private void downloadChatHistory(String agentId, List<String> sessionIds, HttpServletResponse response) {
try {
// 设置响应头
response.setContentType("text/plain;charset=UTF-8");
String fileName = URLEncoder.encode("history.txt", StandardCharsets.UTF_8.toString());
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 获取聊天记录并写入响应流
try (OutputStream out = response.getOutputStream()) {
// 为每个会话生成聊天记录
for (String sessionId : sessionIds) {
// 获取该会话的所有聊天记录
List<AgentChatHistoryDTO> chatHistoryList = agentChatHistoryService
.getChatHistoryBySessionId(agentId, sessionId);
// 从聊天记录中获取第一条消息的创建时间作为会话时间
if (!chatHistoryList.isEmpty()) {
Date firstMessageTime = chatHistoryList.get(0).getCreatedAt();
String sessionTimeStr = DateUtils.format(firstMessageTime, DateUtils.DATE_TIME_PATTERN);
out.write((sessionTimeStr + "\n").getBytes(StandardCharsets.UTF_8));
}
for (AgentChatHistoryDTO message : chatHistoryList) {
String role = message.getChatType() == 1 ? MessageUtils.getMessage(ErrorCode.CHAT_ROLE_USER)
: MessageUtils.getMessage(ErrorCode.CHAT_ROLE_AGENT);
String direction = message.getChatType() == 1 ? ">>" : "<<";
Date messageTime = message.getCreatedAt();
String messageTimeStr = DateUtils.format(messageTime, DateUtils.DATE_TIME_PATTERN);
String content = message.getContent();
String line = "[" + role + "]-[" + messageTimeStr + "]" + direction + ":" + content + "\n";
out.write(line.getBytes(StandardCharsets.UTF_8));
}
// 会话之间添加空行分隔
if (sessionIds.indexOf(sessionId) < sessionIds.size() - 1) {
out.write("\n".getBytes(StandardCharsets.UTF_8));
}
}
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,158 @@
package xiaozhi.modules.agent.controller;
import java.util.List;
import java.util.Map;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentTemplateVO;
@Tag(name = "智能体模板管理")
@AllArgsConstructor
@RestController
@RequestMapping("/agent/template")
public class AgentTemplateController {
private final AgentTemplateService agentTemplateService;
@GetMapping("/page")
@Operation(summary = "获取模板分页列表")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
@Parameter(name = "agentName", description = "模板名称,模糊查询")
})
public Result<PageData<AgentTemplateVO>> getAgentTemplatesPage(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
// 创建分页对象
int page = Integer.parseInt(params.getOrDefault(Constant.PAGE, "1").toString());
int limit = Integer.parseInt(params.getOrDefault(Constant.LIMIT, "10").toString());
Page<AgentTemplateEntity> pageInfo = new Page<>(page, limit);
// 创建查询条件
QueryWrapper<AgentTemplateEntity> wrapper = new QueryWrapper<>();
String agentName = (String) params.get("agentName");
if (agentName != null && !agentName.isEmpty()) {
wrapper.like("agent_name", agentName);
}
wrapper.orderByAsc("sort");
// 执行分页查询
IPage<AgentTemplateEntity> pageResult = agentTemplateService.page(pageInfo, wrapper);
// 使用ConvertUtils转换为VO列表
List<AgentTemplateVO> voList = ConvertUtils.sourceToTarget(pageResult.getRecords(), AgentTemplateVO.class);
// 修复:使用构造函数创建PageData对象,而不是无参构造+setter
PageData<AgentTemplateVO> pageData = new PageData<>(voList, pageResult.getTotal());
return new Result<PageData<AgentTemplateVO>>().ok(pageData);
}
@GetMapping("/{id}")
@Operation(summary = "获取模板详情")
@RequiresPermissions("sys:role:superAdmin")
public Result<AgentTemplateVO> getAgentTemplateById(@PathVariable("id") String id) {
AgentTemplateEntity template = agentTemplateService.getById(id);
if (template == null) {
return ResultUtils.error("模板不存在");
}
// 使用ConvertUtils转换为VO
AgentTemplateVO vo = ConvertUtils.sourceToTarget(template, AgentTemplateVO.class);
return ResultUtils.success(vo);
}
@PostMapping
@Operation(summary = "创建模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<AgentTemplateEntity> createAgentTemplate(@Valid @RequestBody AgentTemplateEntity template) {
// 设置排序值为下一个可用的序号
template.setSort(agentTemplateService.getNextAvailableSort());
boolean saved = agentTemplateService.save(template);
if (saved) {
return ResultUtils.success(template);
} else {
return ResultUtils.error("创建模板失败");
}
}
@PutMapping
@Operation(summary = "更新模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<AgentTemplateEntity> updateAgentTemplate(@Valid @RequestBody AgentTemplateEntity template) {
boolean updated = agentTemplateService.updateById(template);
if (updated) {
return ResultUtils.success(template);
} else {
return ResultUtils.error("更新模板失败");
}
}
@DeleteMapping("/{id}")
@Operation(summary = "删除模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> deleteAgentTemplate(@PathVariable("id") String id) {
// 先查询要删除的模板信息,获取其排序值
AgentTemplateEntity template = agentTemplateService.getById(id);
if (template == null) {
return ResultUtils.error("模板不存在");
}
Integer deletedSort = template.getSort();
// 执行删除操作
boolean deleted = agentTemplateService.removeById(id);
if (deleted) {
// 删除成功后,重新排序剩余模板
agentTemplateService.reorderTemplatesAfterDelete(deletedSort);
return ResultUtils.success("删除模板成功");
} else {
return ResultUtils.error("删除模板失败");
}
}
// 添加新的批量删除方法,使用不同的URL
@PostMapping("/batch-remove")
@Operation(summary = "批量删除模板")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> batchRemoveAgentTemplates(@RequestBody List<String> ids) {
boolean deleted = agentTemplateService.removeByIds(ids);
if (deleted) {
return ResultUtils.success("批量删除成功");
} else {
return ResultUtils.error("批量删除模板失败");
}
}
}
@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
@@ -42,7 +43,7 @@ public class AgentVoicePrintController {
if (b) {
return new Result<>();
}
return new Result<Void>().error("智能体的声纹创建失败");
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_CREATE_FAILED);
}
@PutMapping
@@ -54,7 +55,7 @@ public class AgentVoicePrintController {
if (b) {
return new Result<>();
}
return new Result<Void>().error("智能体的对应声纹更新失败");
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_UPDATE_FAILED);
}
@DeleteMapping("/{id}")
@@ -67,7 +68,7 @@ public class AgentVoicePrintController {
if (delete) {
return new Result<>();
}
return new Result<Void>().error("智能体的对应声纹删除失败");
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_DELETE_FAILED);
}
@GetMapping("/list/{id}")
@@ -76,7 +77,7 @@ public class AgentVoicePrintController {
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)");
throw new RenException(ErrorCode.VOICEPRINT_API_NOT_CONFIGURED);
}
Long userId = SecurityUser.getUserId();
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
@@ -25,4 +25,18 @@ public interface AgentTemplateService extends IService<AgentTemplateEntity> {
* @param modelId 模型ID
*/
void updateDefaultTemplateModelId(String modelType, String modelId);
/**
* 删除模板后重新排序剩余模板
*
* @param deletedSort 被删除模板的排序值
*/
void reorderTemplatesAfterDelete(Integer deletedSort);
/**
* 获取下一个可用的排序序号(寻找最小的未使用序号)
*
* @return 下一个可用的排序序号
*/
Integer getNextAvailableSort();
}
@@ -19,6 +19,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
@@ -74,7 +75,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
AgentInfoVO agent = agentDao.selectAgentInfoById(id);
if (agent == null) {
throw new RenException("智能体不存在");
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
@@ -182,6 +183,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
@Override
public boolean checkAgentPermission(String agentId, Long userId) {
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
return false;
}
// 获取智能体信息
AgentEntity agent = getAgentById(agentId);
if (agent == null) {
@@ -204,7 +208,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 先查询现有实体
AgentEntity existingEntity = this.getAgentById(agentId);
if (existingEntity == null) {
throw new RuntimeException("智能体不存在");
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
// 只更新提供的非空字段
@@ -328,7 +332,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
if (!b) {
throw new RenException("LLM大模型和Intent意图识别,选择参数不匹配");
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
}
this.updateById(existingEntity);
}
@@ -10,6 +10,11 @@ import xiaozhi.modules.agent.dao.AgentTemplateDao;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
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
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service实现
@@ -69,4 +74,48 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
wrapper.ge("sort", 0);
update(wrapper);
}
@Override
public void reorderTemplatesAfterDelete(Integer deletedSort) {
if (deletedSort == null) {
return;
}
// 查询所有排序值大于被删除模板的记录
UpdateWrapper<AgentTemplateEntity> updateWrapper = new UpdateWrapper<>();
updateWrapper.gt("sort", deletedSort)
.setSql("sort = sort - 1");
// 执行批量更新,将这些记录的排序值减1
this.update(updateWrapper);
}
@Override
public Integer getNextAvailableSort() {
// 查询所有已存在的排序值并按升序排序
List<Integer> sortValues = baseMapper.selectList(new QueryWrapper<AgentTemplateEntity>())
.stream()
.map(AgentTemplateEntity::getSort)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
// 如果没有排序值,返回1
if (sortValues.isEmpty()) {
return 1;
}
// 寻找最小的未使用序号
int expectedSort = 1;
for (Integer sort : sortValues) {
if (sort > expectedSort) {
// 找到空缺的序号
return expectedSort;
}
expectedSort = sort + 1;
}
// 如果没有空缺,返回最大序号+1
return expectedSort;
}
}
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
@@ -78,7 +79,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
throw new RenException(ErrorCode.VOICEPRINT_ALREADY_REGISTERED, existingUserName);
}
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
// 开启事务
@@ -100,7 +101,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("保存声纹错误原因:{}", e.getMessage());
throw new RenException("保存声纹错误,请联系管理员");
throw new RenException(ErrorCode.VOICE_PRINT_SAVE_ERROR);
}
}));
}
@@ -123,7 +124,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("删除声纹存在错误原因:{}", e.getMessage());
throw new RenException("删除声纹出现了错误");
throw new RenException(ErrorCode.VOICEPRINT_DELETE_ERROR);
}
}));
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据
@@ -179,7 +180,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + "");
throw new RenException(ErrorCode.VOICEPRINT_UPDATE_NOT_ALLOWED, existingUserName);
}
}
} else {
@@ -208,7 +209,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("修改声纹错误原因:{}", e.getMessage());
throw new RenException("修改声纹错误,请联系管理员");
throw new RenException(ErrorCode.VOICEPRINT_UPDATE_ADMIN_ERROR);
}
}));
}
@@ -225,7 +226,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
return new URI(voicePrint);
} catch (URISyntaxException e) {
log.error("路径格式不正确路径:{}\n错误信息:{}", voicePrint, e.getMessage());
throw new RuntimeException("声纹接口的地址存在错误,请进入参数管理修改声纹接口地址");
throw new RenException(ErrorCode.VOICEPRINT_API_URI_ERROR);
}
}
@@ -270,13 +271,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 判断这个音频是否属于当前智能体
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
if (!b) {
throw new RenException("音频数据不属于这个智能体");
throw new RenException(ErrorCode.VOICEPRINT_AUDIO_NOT_BELONG_AGENT);
}
// 获取到音频数据
byte[] audio = agentChatAudioService.getAudio(audioId);
// 如果音频数据为空的直接报错不进行下去
if (audio == null || audio.length == 0) {
throw new RenException("音频数据是空的请检查上传数据");
throw new RenException(ErrorCode.VOICEPRINT_AUDIO_EMPTY);
}
// 将字节数组包装为资源,返回
return new ByteArrayResource(audio) {
@@ -314,13 +315,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注册失败,请求路径:{}", requestUrl);
throw new RenException("声纹保存失败,请求不成功");
throw new RenException(ErrorCode.VOICEPRINT_REGISTER_REQUEST_ERROR);
}
// 检查响应内容
String responseBody = response.getBody();
if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹保存失败,请求处理失败");
throw new RenException(ErrorCode.VOICEPRINT_REGISTER_PROCESS_ERROR);
}
}
@@ -344,13 +345,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
String.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注销失败,请求路径:{}", requestUrl);
throw new RenException("声纹注销失败,请求不成功");
throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_REQUEST_ERROR);
}
// 检查响应内容
String responseBody = response.getBody();
if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹注销失败,请求处理失败");
throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_PROCESS_ERROR);
}
}
@@ -398,7 +399,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹识别请求失败,请求路径:{}", requestUrl);
throw new RenException("声纹识别失败,请求不成功");
throw new RenException(ErrorCode.VOICEPRINT_IDENTIFY_REQUEST_ERROR);
}
// 检查响应内容
String responseBody = response.getBody();
@@ -298,6 +298,20 @@ public class ConfigServiceImpl implements ConfigService {
Map<String, Object> voiceprintConfig = new HashMap<>();
voiceprintConfig.put("url", voiceprintUrl);
voiceprintConfig.put("speakers", speakers);
// 获取声纹识别相似度阈值,默认0.4
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
try {
double threshold = Double.parseDouble(thresholdStr);
voiceprintConfig.put("similarity_threshold", threshold);
} catch (NumberFormatException e) {
// 如果解析失败,使用默认值0.4
voiceprintConfig.put("similarity_threshold", 0.4);
}
} else {
voiceprintConfig.put("similarity_threshold", 0.4);
}
result.put("voiceprint", voiceprintConfig);
} catch (Exception e) {
@@ -336,7 +350,7 @@ public class ConfigServiceImpl implements ConfigService {
* @param result 结果Map
*/
private void buildModuleConfig(
String assistantName,
String assistantName,
String prompt,
String summaryMemory,
String voice,
@@ -352,17 +366,18 @@ public class ConfigServiceImpl implements ConfigService {
Map<String, Object> result,
boolean isCache) {
Map<String, String> selectedModule = new HashMap<>();
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
String intentLLMModelId = null;
String memLocalShortLLMModelId = null;
for (int i = 0; i < modelIds.length; i++) {
if (modelIds[i] == null) {
continue;
}
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
// 关键:第三个参数传false,确保获取原始密钥
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache, false);
if (model == null) {
continue;
}
@@ -410,14 +425,16 @@ public class ConfigServiceImpl implements ConfigService {
if ("LLM".equals(modelTypes[i])) {
if (StringUtils.isNotBlank(intentLLMModelId)) {
if (!typeConfig.containsKey(intentLLMModelId)) {
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
// 修改这里:添加isMaskSensitive=false参数
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache, false);
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
}
}
if (StringUtils.isNotBlank(memLocalShortLLMModelId)) {
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
// 修改这里:添加isMaskSensitive=false参数
ModelConfigEntity memLocalShortLLM = modelConfigService
.getModelById(memLocalShortLLMModelId, isCache);
.getModelById(memLocalShortLLMModelId, isCache, false);
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
}
}
@@ -5,6 +5,10 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -12,32 +16,45 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "设备管理")
@AllArgsConstructor
@RestController
@RequestMapping("/device")
public class DeviceController {
private final DeviceService deviceService;
private final RedisUtils redisUtils;
private final SysParamsService sysParamsService;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
RestTemplate restTemplate, ObjectMapper objectMapper) {
this.deviceService = deviceService;
this.redisUtils = redisUtils;
this.sysParamsService = sysParamsService;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
@PostMapping("/bind/{agentId}/{deviceCode}")
@Operation(summary = "绑定设备")
@@ -75,6 +92,88 @@ public class DeviceController {
return new Result<List<DeviceEntity>>().ok(devices);
}
@PostMapping("/bind/{agentId}")
@Operation(summary = "转发POST请求到MQTT网关")
@RequiresPermissions("sys:role:normal")
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
try {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return new Result<>();
}
// 获取当前用户的设备列表
UserDetail user = SecurityUser.getUser();
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
// 构建deviceIds数组
java.util.List<String> deviceIds = new java.util.ArrayList<>();
for (DeviceEntity device : devices) {
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
// 替换冒号为下划线
groupId = groupId.replace(":", "_");
macAddress = macAddress.replace(":", "_");
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
deviceIds.add(mqttClientId);
}
// 构建完整的URL
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 生成Bearer令牌
String token = generateBearerToken();
if (token == null) {
return new Result<String>().error("令牌生成失败");
}
headers.set("Authorization", "Bearer " + token);
// 构建请求体JSON
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, headers);
// 发送POST请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
// 返回响应
return new Result<String>().ok(response.getBody());
} catch (Exception e) {
return new Result<String>().error("转发请求失败: " + e.getMessage());
}
}
private String generateBearerToken() {
try {
// 获取当前日期,格式为yyyy-MM-dd
String dateStr = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 获取MQTT签名密钥
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
return null;
}
// 将日期字符串与MQTT_SIGNATURE_KEY连接
String tokenContent = dateStr + signatureKey;
// 对连接后的字符串进行SHA256哈希计算
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
return token;
} catch (Exception e) {
return null;
}
}
@PostMapping("/unbind")
@Operation(summary = "解绑设备")
@RequiresPermissions("sys:role:normal")
@@ -109,4 +208,63 @@ public class DeviceController {
deviceService.manualAddDevice(user.getId(), dto);
return new Result<>();
}
@PostMapping("/commands/{deviceId}")
@Operation(summary = "发送设备指令")
@RequiresPermissions("sys:role:normal")
public Result<String> sendDeviceCommand(@PathVariable String deviceId, @RequestBody String command) {
try {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return new Result<String>().error("MQTT网关地址未配置");
}
// 构建完整的URL
// 获取设备信息以构建mqttClientId
DeviceEntity deviceById = deviceService.selectById(deviceId);
if (!deviceById.getUserId().equals(SecurityUser.getUser().getId())) {
return new Result<String>().error("设备不存在");
}
String macAddress = deviceById != null ? deviceById.getMacAddress() : "unknown";
String groupId = deviceById != null ? deviceById.getBoard() : null;
if (groupId == null) {
groupId = "GID_default";
}
groupId = groupId.replace(":", "_");
macAddress = macAddress.replace(":", "_");
// 拼接为groupId@@@macAddress@@@deviceId格式
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
String url = "http://" + mqttGatewayUrl + "/api/commands/" + mqttClientId;
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 生成Bearer令牌
String dateStr = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
return new Result<String>().error("MQTT签名密钥未配置");
}
String tokenContent = dateStr + signatureKey;
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
headers.set("Authorization", "Bearer " + token);
// 构建请求体
HttpEntity<String> requestEntity = new HttpEntity<>(command, headers);
// 发送POST请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
// 返回响应
return new Result<String>().ok(response.getBody());
} catch (Exception e) {
return new Result<String>().error("发送指令失败: " + e.getMessage());
}
}
}
@@ -77,6 +77,10 @@ public class OTAController {
@GetMapping
@Hidden
public ResponseEntity<String> getOTA() {
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
if (StringUtils.isBlank(mqttUdpConfig)) {
return ResponseEntity.ok("OTA接口不正常,缺少mqtt_gateway地址,请登录智控台,在参数管理找到【server.mqtt_gateway】配置");
}
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
@@ -23,6 +23,9 @@ public class DeviceReportRespDTO {
@Schema(description = "WebSocket配置")
private Websocket websocket;
@Schema(description = "MQTT Gateway配置")
private MQTT mqtt;
@Getter
@Setter
public static class Firmware {
@@ -70,4 +73,21 @@ public class DeviceReportRespDTO {
@Schema(description = "WebSocket服务器地址")
private String url;
}
}
@Getter
@Setter
public static class MQTT {
@Schema(description = "MQTT 配置网址")
private String endpoint;
@Schema(description = "MQTT 客户端唯一标识符")
private String client_id;
@Schema(description = "MQTT 认证用户名")
private String username;
@Schema(description = "MQTT 认证密码")
private String password;
@Schema(description = "ESP32 发布消息的主题")
private String publish_topic;
@Schema(description = "ESP32 订阅的主题")
private String subscribe_topic;
}
}
@@ -1,6 +1,8 @@
package xiaozhi.modules.device.service.impl;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -8,6 +10,9 @@ import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.AopContext;
import org.springframework.scheduling.annotation.Async;
@@ -24,6 +29,7 @@ import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
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;
@@ -33,6 +39,7 @@ import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils;
import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
@@ -44,7 +51,6 @@ import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserUtilService;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
@Slf4j
@Service
@@ -78,27 +84,27 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
public Boolean deviceActivation(String agentId, String activationCode) {
if (StringUtils.isBlank(activationCode)) {
throw new RenException("激活码不能为空");
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
}
String deviceKey = "ota:activation:code:" + activationCode;
Object cacheDeviceId = redisUtils.get(deviceKey);
if (cacheDeviceId == null) {
throw new RenException("激活码错误");
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
}
String deviceId = (String) cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
if (cacheMap == null) {
throw new RenException("激活码错误");
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
}
String cachedCode = (String) cacheMap.get("activation_code");
if (!activationCode.equals(cachedCode)) {
throw new RenException("激活码错误");
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
}
// 检查设备有没有被激活
if (selectById(deviceId) != null) {
throw new RenException("设备已激活");
throw new RenException(ErrorCode.DEVICE_ALREADY_ACTIVATED);
}
String macAddress = (String) cacheMap.get("mac_address");
@@ -106,7 +112,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
String appVersion = (String) cacheMap.get("app_version");
UserDetail user = SecurityUser.getUser();
if (user.getId() == null) {
throw new RenException("用户未登录");
throw new RenException(ErrorCode.USER_NOT_LOGIN);
}
Date currentTime = new Date();
@@ -128,6 +134,10 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 清理redis缓存
redisUtils.delete(cacheDeviceKey);
redisUtils.delete(deviceKey);
// 添加:清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId));
return true;
}
@@ -176,6 +186,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
response.setWebsocket(websocket);
// 添加MQTT UDP配置
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
try {
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
: "GID_default";
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, groupId);
if (mqtt != null) {
mqtt.setEndpoint(mqttUdpConfig);
response.setMqtt(mqtt);
}
} catch (Exception e) {
log.error("生成MQTT配置失败: {}", e.getMessage());
}
}
if (deviceById != null) {
// 如果设备存在,则异步更新上次连接时间和版本信息
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
@@ -202,6 +229,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
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<>();
wrapper.eq("user_id", userId);
wrapper.eq("id", deviceId);
@@ -419,7 +456,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
wrapper.eq("mac_address", dto.getMacAddress());
DeviceEntity exist = baseDao.selectOne(wrapper);
if (exist != null) {
throw new RenException("该Mac地址已存在");
throw new RenException(ErrorCode.MAC_ADDRESS_ALREADY_EXISTS);
}
Date now = new Date();
DeviceEntity entity = new DeviceEntity();
@@ -436,5 +473,77 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
entity.setUpdater(userId);
entity.setAutoUpdate(1);
baseDao.insert(entity);
// 添加:清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId()));
}
/**
* 生成MQTT密码签名
*
* @param content 签名内容 (clientId + '|' + username)
* @param secretKey 密钥
* @return Base64编码的HMAC-SHA256签名
*/
private String generatePasswordSignature(String content, String secretKey) throws Exception {
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmac.init(keySpec);
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature);
}
/**
* 构建MQTT配置信息
*
* @param macAddress MAC地址
* @param groupId 分组ID
* @return MQTT配置对象
*/
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
throws Exception {
// 从环境变量或系统参数获取签名密钥
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
return null;
}
// 构建客户端ID格式:groupId@@@macAddress@@@uuid
String groupIdSafeStr = groupId.replace(":", "_");
String deviceIdSafeStr = macAddress.replace(":", "_");
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, deviceIdSafeStr);
// 构建用户数据(包含IP等信息)
Map<String, String> userData = new HashMap<>();
// 尝试获取客户端IP
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String clientIp = request.getRemoteAddr();
userData.put("ip", clientIp);
}
} catch (Exception e) {
userData.put("ip", "unknown");
}
// 将用户数据编码为Base64 JSON
String userDataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(userData);
String username = Base64.getEncoder().encodeToString(userDataJson.getBytes(StandardCharsets.UTF_8));
// 生成密码签名
String password = generatePasswordSignature(mqttClientId + "|" + username, signatureKey);
// 构建MQTT配置
DeviceReportRespDTO.MQTT mqtt = new DeviceReportRespDTO.MQTT();
mqtt.setClient_id(mqttClientId);
mqtt.setUsername(username);
mqtt.setPassword(password);
mqtt.setPublish_topic("device-server");
mqtt.setSubscribe_topic("devices/p2p/" + deviceIdSafeStr);
return mqtt;
}
}
@@ -35,17 +35,27 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
/**
* 根据ID获取模型配置
*
* @param id 模型ID
* @param isCache 是否缓存
* @param id 模型ID
* @param isCache 是否缓存
* @param isMaskSensitive 是否掩码敏感信息
* @return 模型配置实体
*/
ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive);
/**
* 根据ID获取模型配置(默认掩码敏感信息)
*
* @param id 模型ID
* @param isCache 是否缓存
* @return 模型配置实体
*/
ModelConfigEntity getModelById(String id, boolean isCache);
/**
* 设置默认模型
*
* @param modelType 模型类型
* @param isDefault 是否默认
* @param modelType 模型类型
* @param isDefault 是否默认1:是,0:否)
*/
void setDefaultModel(String modelType, int isDefault);
}
@@ -1,5 +1,6 @@
package xiaozhi.modules.model.service.impl;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -11,16 +12,21 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.json.JSONObject;
import lombok.AllArgsConstructor;
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.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.SensitiveDataUtils;
import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.model.dao.ModelConfigDao;
@@ -62,7 +68,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
.eq("is_enabled", 1)
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
.select("id", "model_name", "config_json"));
// 处理获取到的内容
return entities.stream().map(item -> {
LlmModelBasicInfoDTO dto = new LlmModelBasicInfoDTO();
dto.setId(item.getId());
@@ -75,90 +81,400 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
@Override
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.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(
getPage(params, "sort", true),
pageInfo,
new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType)
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%"));
return getPageData(modelConfigEntityIPage, ModelConfigDTO.class);
}
@Override
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO 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("供应器不存在");
}
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
// 1. 参数验证
validateEditParameters(modelType, provideCode, id, modelConfigBodyDTO);
// 再保存供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setModelType(modelType);
modelConfigEntity.setIsDefault(0);
modelConfigDao.insert(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
// 2. 验证模型提供者
validateModelProvider(modelType, provideCode);
// 3. 获取原始配置(不经过敏感数据处理)
ModelConfigEntity originalEntity = getOriginalConfigFromDb(id);
// 4. 验证LLM配置
validateLlmConfiguration(modelConfigBodyDTO);
// 5. 准备更新实体并处理敏感数据
ModelConfigEntity modelConfigEntity = prepareUpdateEntity(modelConfigBodyDTO, originalEntity, modelType, id);
// 6. 执行数据库更新
modelConfigDao.updateById(modelConfigEntity);
// 7. 清除缓存
clearModelCache(id);
// 8. 返回处理后的数据(包含敏感数据掩码)
return buildResponseDTO(modelConfigEntity);
}
@Override
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
// 先验证有没有供应器
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
if (modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
ModelConfigEntity modelConfigEntity = modelConfigDao.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
.eq(ModelConfigEntity::getId, llm));
String selectModelType = (modelConfigEntity == null || modelConfigEntity.getModelType() == null) ? null
: modelConfigEntity.getModelType().toUpperCase();
if (modelConfigEntity == null || !"LLM".equals(selectModelType)) {
throw new RenException("设置的LLM不存在");
}
String type = modelConfigEntity.getConfigJson().get("type").toString();
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
if (!"openai".equals(type) && !"ollama".equals(type)) {
throw new RenException("设置的LLM不是openai和ollama");
}
}
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
validateAddParameters(modelType, provideCode, modelConfigBodyDTO);
// 再更新供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setId(id);
modelConfigEntity.setModelType(modelType);
modelConfigDao.updateById(modelConfigEntity);
// 清除缓存
redisUtils.delete(RedisKeys.getModelConfigById(modelConfigEntity.getId()));
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
validateModelProvider(modelType, provideCode);
ModelConfigEntity modelConfigEntity = prepareAddEntity(modelConfigBodyDTO, modelType);
modelConfigDao.insert(modelConfigEntity);
return buildResponseDTO(modelConfigEntity);
}
@Override
public void delete(String id) {
// 查看是否是默认
if (StringUtils.isBlank(id)) {
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
}
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
throw new RenException(ErrorCode.DEFAULT_MODEL_DELETE_ERROR);
}
// 验证是否有引用
checkAgentReference(id);
checkIntentConfigReference(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 getModelById(String id, boolean isCache) {
return getModelById(id, isCache, true);
}
@Override
public ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive) {
if (StringUtils.isBlank(id)) {
return null;
}
ModelConfigEntity entity = null;
String cacheKey = RedisKeys.getModelConfigById(id);
if (isCache) {
// 从缓存获取
entity = (ModelConfigEntity) redisUtils.get(cacheKey);
if (entity != null) {
// 修改:根据isMaskSensitive参数重新处理敏感信息
if (!isMaskSensitive && entity.getConfigJson() != null) {
// 如果需要获取原始配置,但缓存的是掩码后的配置,则从数据库重新获取
ModelConfigEntity originalEntity = modelConfigDao.selectById(id);
if (originalEntity != null) {
entity.setConfigJson(originalEntity.getConfigJson());
}
}
return entity;
}
}
// 从数据库获取数据
entity = modelConfigDao.selectById(id);
if (entity != null) {
// 根据isMaskSensitive参数决定是否掩码敏感信息
if (isMaskSensitive && entity.getConfigJson() != null) {
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
}
if (isCache) {
redisUtils.set(cacheKey, entity);
}
}
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) {
List<AgentEntity> agents = agentDao.selectList(
@@ -180,14 +496,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
String agentNames = agents.stream()
.map(AgentEntity::getAgentName)
.collect(Collectors.joining(""));
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
throw new RenException(ErrorCode.MODEL_REFERENCED_BY_AGENT, agentNames);
}
}
/**
* 检查意图识别配置是否有引用
*
* @param modelId 模型ID
*/
private void checkIntentConfigReference(String modelId) {
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
@@ -198,58 +512,8 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
.eq("model_type", "Intent")
.like("config_json", "%" + modelId + "%"));
if (!intentConfigs.isEmpty()) {
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
throw new RenException(ErrorCode.LLM_REFERENCED_BY_INTENT);
}
}
}
@Override
public String getModelNameById(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
String cachedName = (String) redisUtils.get(RedisKeys.getModelNameById(id));
if (StringUtils.isNotBlank(cachedName)) {
return cachedName;
}
ModelConfigEntity entity = modelConfigDao.selectById(id);
if (entity != null) {
String modelName = entity.getModelName();
if (StringUtils.isNotBlank(modelName)) {
redisUtils.set(RedisKeys.getModelNameById(id), modelName);
}
return modelName;
}
return null;
}
@Override
public ModelConfigEntity getModelById(String id, boolean isCache) {
if (StringUtils.isBlank(id)) {
return null;
}
if (isCache) {
ModelConfigEntity cachedConfig = (ModelConfigEntity) redisUtils.get(RedisKeys.getModelConfigById(id));
if (cachedConfig != null) {
return ConvertUtils.sourceToTarget(cachedConfig, ModelConfigEntity.class);
}
}
ModelConfigEntity entity = modelConfigDao.selectById(id);
if (entity != null) {
redisUtils.set(RedisKeys.getModelConfigById(id), entity);
}
return entity;
}
@Override
public void setDefaultModel(String modelType, int isDefault) {
ModelConfigEntity entity = new ModelConfigEntity();
entity.setIsDefault(isDefault);
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType));
}
}
@@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.json.JSONArray;
import lombok.AllArgsConstructor;
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;
@@ -109,7 +110,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
modelProviderDTO.setFields(modelProviderDTO.getFields());
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
if (modelProviderDao.insert(entity) == 0) {
throw new RenException("新增数据失败");
throw new RenException(ErrorCode.ADD_DATA_FAILED);
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
@@ -122,7 +123,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
modelProviderDTO.setUpdateDate(new Date());
if (modelProviderDao
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
throw new RenException("修改数据失败");
throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
}
@@ -130,14 +131,14 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
@Override
public void delete(String id) {
if (modelProviderDao.deleteById(id) == 0) {
throw new RenException("删除数据失败");
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
}
}
@Override
public void delete(List<String> ids) {
if (modelProviderDao.deleteBatchIds(ids) == 0) {
throw new RenException("删除数据失败");
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
}
}
@@ -88,6 +88,7 @@ public class ShiroConfig {
// 将config路径使用server服务过滤器
filterMap.put("/config/**", "server");
filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/saveMemory/**", "server");
filterMap.put("/agent/play/**", "anon");
filterMap.put("/**", "oauth2");
@@ -2,8 +2,11 @@ package xiaozhi.modules.security.config;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
@@ -12,8 +15,10 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -95,5 +100,47 @@ public class WebMvcConfig implements WebMvcConfigurer {
converter.setObjectMapper(mapper);
return converter;
}
/**
* 国际化配置 - 根据请求头中的Accept-Language设置语言环境
*/
@Bean
public LocaleResolver localeResolver() {
return new AcceptHeaderLocaleResolver() {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.isEmpty()) {
return Locale.getDefault();
}
// 解析Accept-Language请求头中的首选语言
String[] languages = acceptLanguage.split(",");
if (languages.length > 0) {
// 提取第一个语言代码,去除可能的质量值(q=...)
String[] parts = languages[0].split(";" + "\\s*");
String primaryLanguage = parts[0].trim();
// 根据前端发送的语言代码直接创建Locale对象
if (primaryLanguage.equals("zh-CN")) {
return Locale.SIMPLIFIED_CHINESE;
} else if (primaryLanguage.equals("zh-TW")) {
return Locale.TRADITIONAL_CHINESE;
} else if (primaryLanguage.equals("en-US")) {
return Locale.US;
} else if (primaryLanguage.startsWith("zh")) {
// 对于其他中文变体,默认使用简体中文
return Locale.SIMPLIFIED_CHINESE;
} else if (primaryLanguage.startsWith("en")) {
// 对于其他英文变体,默认使用美式英语
return Locale.US;
}
}
// 如果没有匹配的语言,使用默认语言
return Locale.getDefault();
}
};
}
}
@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
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.SysUserTokenService;
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.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
@@ -42,6 +45,7 @@ import xiaozhi.modules.sys.vo.SysDictDataItem;
/**
* 登录控制层
*/
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/user")
@@ -66,14 +70,15 @@ public class LoginController {
@Operation(summary = "短信验证码")
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) {
throw new RenException("图形验证码错误");
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
throw new RenException(ErrorCode.MOBILE_REGISTER_DISABLED);
}
// 发送短信验证码
captchaService.sendSMSValidateCode(dto.getPhone());
@@ -83,30 +88,44 @@ public class LoginController {
@PostMapping("/login")
@Operation(summary = "登录")
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误,请重新获取");
}
String password = login.getPassword();
// 使用工具类解密并验证验证码
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
password, login.getCaptchaId(), captchaService, sysParamsService);
login.setPassword(actualPassword);
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
// 判断用户是否存在
if (userDTO == null) {
throw new RenException("请检测用户和密码是否输入错误");
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
}
// 判断密码是否正确,不一样则进入if
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
throw new RenException("请检测用户和密码是否输入错误");
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
}
return sysUserTokenService.createToken(userDTO.getId());
}
@PostMapping("/register")
@Operation(summary = "注册")
public Result<Void> register(@RequestBody LoginDTO login) {
if (!sysUserService.getAllowUserRegister()) {
throw new RenException("当前不允许普通用户注册");
throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
}
String password = login.getPassword();
// 使用工具类解密并验证验证码
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
password, login.getCaptchaId(), captchaService, sysParamsService);
login.setPassword(actualPassword);
// 是否开启手机注册
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
@@ -115,25 +134,19 @@ public class LoginController {
// 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
if (!validPhone) {
throw new RenException("用户名不是手机号码,请重新输入");
throw new RenException(ErrorCode.USERNAME_NOT_PHONE);
}
// 验证短信验证码是否正常
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
if (!validate) {
throw new RenException("手机验证码错误,请重新获取");
}
} else {
// 验证是否正确输入验证码
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误,请重新获取");
throw new RenException(ErrorCode.SMS_CODE_ERROR);
}
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null) {
throw new RenException("此手机号码已经注册过");
throw new RenException(ErrorCode.PHONE_ALREADY_REGISTERED);
}
userDTO = new SysUserDTO();
userDTO.setUsername(login.getUsername());
@@ -168,28 +181,36 @@ public class LoginController {
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用找回密码功能");
throw new RenException(ErrorCode.RETRIEVE_PASSWORD_DISABLED);
}
// 判断非空
ValidatorUtils.validateEntity(dto);
// 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
if (!validPhone) {
throw new RenException("输入的手机号码格式不正确");
throw new RenException(ErrorCode.PHONE_FORMAT_ERROR);
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
if (userDTO == null) {
throw new RenException("输入的手机号码未注册");
throw new RenException(ErrorCode.PHONE_NOT_REGISTERED);
}
// 验证短信验证码是否正常
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
// 判断是否通过验证
if (!validate) {
throw new RenException("输入的手机验证码错误");
throw new RenException(ErrorCode.SMS_CODE_ERROR);
}
String password = dto.getPassword();
// 使用工具类解密并验证验证码
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
password, dto.getCaptchaId(), captchaService, sysParamsService);
dto.setPassword(actualPassword);
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
return new Result<>();
}
@@ -208,6 +229,13 @@ public class LoginController {
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("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);
}
@@ -21,10 +21,6 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}")
private String password;
@Schema(description = "验证码")
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "手机验证码")
private String mobileCaptcha;
@@ -18,6 +18,7 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.utils.HttpContextUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.MessageUtils;
import xiaozhi.common.utils.Result;
/**
@@ -82,8 +83,8 @@ public class Oauth2Filter extends AuthenticatingFilter {
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED, throwable.getMessage());
// 使用国际化消息替代直接使用异常消息
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED);
String json = JsonUtils.toJsonString(r);
httpResponse.getWriter().print(json);
@@ -3,6 +3,7 @@ package xiaozhi.modules.security.oauth2;
import java.security.MessageDigest;
import java.util.UUID;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
/**
@@ -38,7 +39,7 @@ public class TokenGenerator {
byte[] messageDigest = algorithm.digest();
return toHexString(messageDigest);
} catch (Exception e) {
throw new RenException("token invalid", e);
throw new RenException(ErrorCode.TOKEN_GENERATE_ERROR, e);
}
}
}
@@ -16,6 +16,7 @@ import com.wf.captcha.base.Captcha;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
@@ -88,7 +89,7 @@ public class CaptchaServiceImpl implements CaptchaService {
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - lastSendTimeLong;
if (timeDiff < 60000) {
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
throw new RenException(ErrorCode.SMS_SEND_TOO_FREQUENTLY, String.valueOf((60000 - timeDiff) / 1000));
}
}
@@ -108,7 +109,7 @@ public class CaptchaServiceImpl implements CaptchaService {
}
if (todayCount >= maxSendCount) {
throw new RenException("今日发送次数已达上限");
throw new RenException(ErrorCode.TODAY_SMS_LIMIT_REACHED);
}
String key = RedisKeys.getSMSValidateCodeKey(phone);
@@ -9,6 +9,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
@@ -45,7 +46,7 @@ public class ALiYunSmsService implements SmsService {
redisUtils.delete(todayCountKey);
// 错误 message
log.error(e.getMessage());
throw new RenException("短信发送失败");
throw new RenException(ErrorCode.SMS_SEND_FAILED);
}
}
@@ -70,7 +71,7 @@ public class ALiYunSmsService implements SmsService {
}catch (Exception e){
// 错误 message
log.error(e.getMessage());
throw new RenException("短信连接建立失败");
throw new RenException(ErrorCode.SMS_CONNECTION_FAILED);
}
}
}
@@ -22,6 +22,7 @@ import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.annotation.LogOperation;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
@@ -64,17 +65,17 @@ public class ServerSideManageController {
@RequiresPermissions("sys:role:superAdmin")
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
if (emitSeverActionDTO.getAction() == null) {
throw new RenException("无效服务端操作");
throw new RenException(ErrorCode.INVALID_SERVER_ACTION);
}
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsText)) {
throw new RenException("未配置服务端WebSocket地址");
throw new RenException(ErrorCode.SERVER_WEBSOCKET_NOT_CONFIGURED);
}
String targetWs = emitSeverActionDTO.getTargetWs();
String[] wsList = wsText.split(";");
// 找到需要发起的
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
throw new RenException("目标WebSocket地址不存在");
throw new RenException(ErrorCode.TARGET_WEBSOCKET_NOT_EXIST);
}
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
}
@@ -114,7 +115,7 @@ public class ServerSideManageController {
});
} catch (Exception e) {
// 捕获全部错误,由全局异常处理器返回
throw new RenException("WebSocket连接失败或连接超时");
throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED);
}
return true;
}
@@ -25,6 +25,7 @@ import lombok.AllArgsConstructor;
import xiaozhi.common.annotation.LogOperation;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils;
@@ -107,9 +108,12 @@ public class SysParamsController {
// 验证MCP地址
validateMcpUrl(dto.getParamCode(), dto.getParamValue());
//
// 验证声纹地址
validateVoicePrint(dto.getParamCode(), dto.getParamValue());
// 校验mqtt密钥长度
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
sysParamsService.update(dto);
configService.getConfig(false);
return new Result<Void>();
@@ -127,23 +131,23 @@ public class SysParamsController {
}
String[] wsUrls = urls.split("\\;");
if (wsUrls.length == 0) {
throw new RenException("WebSocket地址列表不能为空");
throw new RenException(ErrorCode.WEBSOCKET_URLS_EMPTY);
}
for (String url : wsUrls) {
if (StringUtils.isNotBlank(url)) {
// 检查是否包含localhost或127.0.0.1
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
throw new RenException(ErrorCode.WEBSOCKET_URL_LOCALHOST);
}
// 验证WebSocket地址格式
if (!WebSocketValidator.validateUrlFormat(url)) {
throw new RenException("WebSocket地址格式不正确: " + url);
throw new RenException(ErrorCode.WEBSOCKET_URL_FORMAT_ERROR);
}
// 测试WebSocket连接
if (!WebSocketValidator.testConnection(url)) {
throw new RenException("WebSocket连接测试失败: " + url);
throw new RenException(ErrorCode.WEBSOCKET_CONNECTION_FAILED);
}
}
}
@@ -170,35 +174,35 @@ public class SysParamsController {
return;
}
if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("OTA地址不能为空");
throw new RenException(ErrorCode.OTA_URL_EMPTY);
}
// 检查是否包含localhost或127.0.0.1
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
throw new RenException(ErrorCode.OTA_URL_LOCALHOST);
}
// 验证URL格式
if (!url.toLowerCase().startsWith("http")) {
throw new RenException("OTA地址必须以http或https开头");
throw new RenException(ErrorCode.OTA_URL_PROTOCOL_ERROR);
}
if (!url.endsWith("/ota/")) {
throw new RenException("OTA地址必须以/ota/结尾");
throw new RenException(ErrorCode.OTA_URL_FORMAT_ERROR);
}
try {
// 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
throw new RenException(ErrorCode.OTA_INTERFACE_ACCESS_FAILED);
}
// 检查响应内容是否包含OTA相关信息
String body = response.getBody();
if (body == null || !body.contains("OTA")) {
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
throw new RenException(ErrorCode.OTA_INTERFACE_FORMAT_ERROR);
}
} catch (Exception e) {
throw new RenException("OTA接口验证失败:" + e.getMessage());
throw new RenException(ErrorCode.OTA_INTERFACE_VALIDATION_FAILED);
}
}
@@ -207,61 +211,86 @@ public class SysParamsController {
return;
}
if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("MCP地址不能为空");
throw new RenException(ErrorCode.MCP_URL_EMPTY);
}
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("MCP地址不能使用localhost或127.0.0.1");
throw new RenException(ErrorCode.MCP_URL_LOCALHOST);
}
if (!url.toLowerCase().contains("key")) {
throw new RenException("不是正确的MCP地址");
throw new RenException(ErrorCode.MCP_URL_INVALID);
}
try {
// 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode());
throw new RenException(ErrorCode.MCP_INTERFACE_ACCESS_FAILED);
}
// 检查响应内容是否包含mcp相关信息
String body = response.getBody();
if (body == null || !body.contains("success")) {
throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口");
throw new RenException(ErrorCode.MCP_INTERFACE_FORMAT_ERROR);
}
} catch (Exception e) {
throw new RenException("MCP接口验证失败:" + e.getMessage());
throw new RenException(ErrorCode.MCP_INTERFACE_VALIDATION_FAILED);
}
}
// 验证声纹接口地址是否正常
private void validateVoicePrint(String paramCode, String url) {
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
return;
}
if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("声纹接口地址不能为空");
throw new RenException(ErrorCode.VOICEPRINT_URL_EMPTY);
}
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("声纹接口地址不能使用localhost或127.0.0.1");
throw new RenException(ErrorCode.VOICEPRINT_URL_LOCALHOST);
}
if (!url.toLowerCase().contains("key")) {
throw new RenException("不是正确的声纹接口地址");
throw new RenException(ErrorCode.VOICEPRINT_URL_INVALID);
}
// 验证URL格式
if (!url.toLowerCase().startsWith("http")) {
throw new RenException("声纹接口地址必须以http或https开头");
throw new RenException(ErrorCode.VOICEPRINT_URL_PROTOCOL_ERROR);
}
try {
// 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode());
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_ACCESS_FAILED);
}
// 检查响应内容
String body = response.getBody();
if (body == null || !body.contains("healthy")) {
throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口");
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_FORMAT_ERROR);
}
} catch (Exception e) {
throw new RenException("声纹接口验证失败:" + e.getMessage());
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_VALIDATION_FAILED);
}
}
// 校验mqtt密钥长度和复杂度
private void validateMqttSecretLength(String paramCode, String secret) {
if (!paramCode.equals(Constant.SERVER_MQTT_SECRET)) {
return;
}
if (StringUtils.isBlank(secret) || secret.equals("null")) {
throw new RenException(ErrorCode.MQTT_SECRET_EMPTY);
}
if (secret.length() < 8) {
throw new RenException(ErrorCode.MQTT_SECRET_LENGTH_INSECURE);
}
// 检查是否同时包含大小写字母
if (!secret.matches(".*[a-z].*") || !secret.matches(".*[A-Z].*")) {
throw new RenException(ErrorCode.MQTT_SECRET_CHARACTER_INSECURE);
}
// 不允许包含弱密码
String[] weakPasswords = { "test", "1234", "admin", "password", "qwerty", "xiaozhi" };
for (String weakPassword : weakPasswords) {
if (secret.toLowerCase().contains(weakPassword)) {
throw new RenException(ErrorCode.MQTT_SECRET_WEAK_PASSWORD);
}
}
}
}
@@ -25,6 +25,10 @@ public class RetrievePasswordDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}")
private String password;
@Schema(description = "图形验证码ID")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;
}
@@ -17,6 +17,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
@@ -153,7 +154,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
}
boolean exists = baseDao.exists(queryWrapper);
if (exists) {
throw new RenException("字典标签重复");
throw new RenException(ErrorCode.DICT_LABEL_DUPLICATE);
}
}
@@ -18,6 +18,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
@@ -66,7 +67,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
public SysDictTypeVO get(Long id) {
SysDictTypeEntity entity = baseDao.selectById(id);
if (entity == null) {
throw new RenException("字典类型不存在");
throw new RenException(ErrorCode.DICT_TYPE_NOT_EXIST);
}
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
@@ -147,7 +148,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
}
boolean exists = baseDao.exists(queryWrapper);
if (exists) {
throw new RenException("字典类型编码重复");
throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE);
}
}
}
@@ -21,6 +21,7 @@ import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.SM2Utils;
import xiaozhi.modules.sys.dao.SysParamsDao;
import xiaozhi.modules.sys.dto.SysParamsDTO;
import xiaozhi.modules.sys.entity.SysParamsEntity;
@@ -206,6 +207,31 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
String newSecret = UUID.randomUUID().toString();
updateValueByCode(Constant.SERVER_SECRET, newSecret);
}
// 初始化SM2密钥对
initSM2KeyPair();
}
/**
* 初始化SM2密钥对
*/
private void initSM2KeyPair() {
// 获取SM2公钥
String publicKey = getValue(Constant.SM2_PUBLIC_KEY, false);
// 获取SM2私钥
String privateKey = getValue(Constant.SM2_PRIVATE_KEY, false);
// 如果公钥或私钥为空,则生成新的密钥对
if (StringUtils.isBlank(publicKey) || StringUtils.isBlank(privateKey) ||
"null".equals(publicKey) || "null".equals(privateKey)) {
Map<String, String> keyPair = SM2Utils.createKey();
String newPublicKey = keyPair.get(SM2Utils.KEY_PUBLIC_KEY);
String newPrivateKey = keyPair.get(SM2Utils.KEY_PRIVATE_KEY);
// 更新数据库中的密钥对
updateValueByCode(Constant.SM2_PUBLIC_KEY, newPublicKey);
updateValueByCode(Constant.SM2_PRIVATE_KEY, newPrivateKey);
}
}
/**
@@ -115,7 +115,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
// 判断旧密码是否正确
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
throw new RenException("旧密码输入错误");
throw new RenException(ErrorCode.OLD_PASSWORD_ERROR);
}
// 新密码强度
@@ -0,0 +1,24 @@
-- VOSK ASR模型供应器
delete from `ai_model_provider` where id = 'SYSTEM_ASR_VoskASR';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_VoskASR', 'ASR', 'vosk', 'VOSK离线语音识别', '[{"key": "model_path", "type": "string", "label": "模型路径"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]', 11, 1, NOW(), 1, NOW());
-- VOSK ASR模型配置
delete from `ai_model_config` where id = 'ASR_VoskASR';
INSERT INTO `ai_model_config` VALUES ('ASR_VoskASR', 'ASR', 'VoskASR', 'VOSK离线语音识别', 0, 1, '{\"type\": \"vosk\", \"model_path\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 11, NULL, NULL, NULL, NULL);
-- 更新VOSK ASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://alphacephei.com/vosk/',
`remark` = 'VOSK ASR配置说明:
1. VOSK是一个离线语音识别库,支持多种语言
2. 需要先下载模型文件:https://alphacephei.com/vosk/models
3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22
4. 完全离线运行,无需网络连接
5. 输出文件保存在tmp/目录
使用步骤:
1. 访问 https://alphacephei.com/vosk/models 下载中文模型
2. 解压模型文件到项目目录下的models/vosk/文件夹
3. 在配置中指定正确的模型路径
4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格
' WHERE `id` = 'ASR_VoskASR';
@@ -0,0 +1,45 @@
-- 添加 MinimaxHTTPStream 流式 TTS 供应器
delete from `ai_model_provider` where id = 'SYSTEM_TTS_MinimaxStreamTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_MinimaxStreamTTS', 'TTS', 'minimax_httpstream', 'Minimax流式语音合成', '[{"key":"group_id","label":"组ID","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice_id","label":"音色ID","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice_setting","label":"音色设置","type":"dict","dict_name":"voice_setting"},{"key":"pronunciation_dict","label":"发音字典","type":"dict","dict_name":"pronunciation_dict"},{"key":"audio_setting","label":"音频设置","type":"dict","dict_name":"audio_setting"},{"key":"timber_weights","label":"音色权重","type":"string"}]', 18, 1, NOW(), 1, NOW());
-- 添加Minimax流式TTS模型配置
delete from `ai_model_config` where id = 'TTS_MinimaxStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxStreamTTS', 'TTS', 'MinimaxStreamTTS', 'Minimax流式语音合成', 0, 1, '{"type": "minimax_httpstream", "group_id": "", "api_key": "", "model": "speech-01-turbo", "voice_id": "female-shaonv", "output_dir": "tmp/", "voice_setting": {"speed": 1, "vol": 1, "pitch": 0, "emotion": "happy"}, "pronunciation_dict": {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}, "audio_setting": {"sample_rate": 24000, "bitrate": 128000, "format": "pcm", "channel": 1}}', NULL, NULL, 21, NULL, NULL, NULL, NULL);
-- 更新Minimax流式TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://platform.minimaxi.com/',
`remark` = 'Minimax流式TTS配置说明:
1. 需要先申请Minimax API Key
2. 需要填写Group ID
3. 支持多种音色设置和音频参数调整
4. 支持实时流式合成,具有较低的延迟
5. 支持自定义发音字典和音色权重
6. 隐藏参数配置:声音设定(voice_setting)、发音字典(pronunciation_dict)、音色权重(timber_weights)
- 语速(speed): 范围[0.5,2],默认1.0,取值越大语速越快
- 音量(vol): 范围(0,10],默认1.0,取值越大音量越高
- 音调(pitch): 范围[-12,12],默认0,取值需为整数
- 情绪(emotion): 控制合成语音的情绪,支持7种值:["happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm"],该参数仅对 speech-2.5-hd-preview、speech-2.5-turbo-preview、speech-02-hd、speech-02-turbo、speech-01-turbo、speech-01-hd 生效
- timbre_weights与voice_id二选一必填
- voice_id(请求的音色id,须和weight参数同步填写)
- weight(权重,最多支持4种音色混合。范围[1,100])
' WHERE `id` = 'TTS_MinimaxStreamTTS';
-- 添加Minimax流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_MinimaxStreamTTS';
-- 默认音色
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0001', 'TTS_MinimaxStreamTTS', '少女音', 'female-shaonv', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0002', 'TTS_MinimaxStreamTTS', '成熟女声', 'female-chengshu', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0003', 'TTS_MinimaxStreamTTS', '霸道少爷', 'badao_shaoye', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0004', 'TTS_MinimaxStreamTTS', '病娇弟弟', 'bingjiao_didi', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0005', 'TTS_MinimaxStreamTTS', '纯真学弟', 'chunzhen_xuedi', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0006', 'TTS_MinimaxStreamTTS', '冷淡学长', 'lengdan_xiongzhang', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0007', 'TTS_MinimaxStreamTTS', '甜美小玲', 'tianxin_xiaoling', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0008', 'TTS_MinimaxStreamTTS', '俏皮萌妹', 'qiaopi_mengmei', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0009', 'TTS_MinimaxStreamTTS', '妩媚御姐', 'wumei_yujie', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0010', 'TTS_MinimaxStreamTTS', '嗲嗲学妹', 'diadia_xuemei', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
@@ -0,0 +1,8 @@
delete from `sys_params` where param_code = 'server.mqtt_gateway';
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (116, 'server.mqtt_gateway', 'null', 'string', 1, 'mqtt gateway 配置');
delete from `sys_params` where param_code = 'server.mqtt_signature_key';
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (117, 'server.mqtt_signature_key', 'null', 'string', 1, 'mqtt 密钥 配置');
delete from `sys_params` where param_code = 'server.udp_gateway';
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (118, 'server.udp_gateway', 'null', 'string', 1, 'udp gateway 配置');
@@ -0,0 +1,4 @@
-- 添加声纹识别相似度阈值参数配置
delete from `sys_params` where id = 115;
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
VALUES (115, 'server.voiceprint_similarity_threshold', '0.4', 'string', 1, '声纹识别相似度阈值,范围0.0-1.0,默认0.4,数值越高越严格');
@@ -0,0 +1,10 @@
-- 删除非流式MiniMax TTS配置,保留流式版本
-- 删除旧的非流式MiniMax TTS模型配置
DELETE FROM `ai_model_config` WHERE `id` = 'TTS_MinimaxTTS';
-- 删除旧的非流式MiniMax TTS供应器配置
DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_TTS_minimax';
-- 删除旧的非流式MiniMax TTS音色配置
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_MinimaxTTS';
@@ -0,0 +1,16 @@
-- 添加通义千问Qwen3-ASR-Flash语音识别服务配置
delete from `ai_model_provider` where id = 'SYSTEM_ASR_Qwen3Flash';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_Qwen3Flash', 'ASR', 'qwen3_asr_flash', 'Qwen3-ASR-Flash语音识别', '[{"key":"api_key","label":"API密钥","type":"password"},{"key":"base_url","label":"服务地址","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 17, 1, NOW(), 1, NOW());
delete from `ai_model_config` where id = 'ASR_Qwen3Flash';
INSERT INTO `ai_model_config` VALUES ('ASR_Qwen3Flash', 'ASR', 'Qwen3-ASR-Flash', '通义千问语音识别服务', 0, 1, '{"type": "qwen3_asr_flash", "api_key": "", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model_name": "qwen3-asr-flash", "output_dir": "tmp/", "enable_lid": true, "enable_itn": true}', 'https://help.aliyun.com/zh/bailian/', '支持多语言识别、歌唱识别、噪声拒识功能', 20, NULL, NULL, NULL, NULL);
-- 更新Qwen3-ASR-Flash模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1&tab=doc#/doc/?type=model&url=2979031',
`remark` = '通义千问Qwen3-ASR-Flash配置说明:
1. 登录阿里云百炼平台https://bailian.console.aliyun.com/
2. 创建API-KEY https://bailian.console.aliyun.com/#/api-key
3.Qwen3-ASR-Flash基于通义千问多模态基座,支持多语言识别、歌唱识别、噪声拒识等功能
' WHERE `id` = 'ASR_Qwen3Flash';
@@ -0,0 +1,3 @@
delete from `sys_params` where param_code = 'server.mqtt_manager_api';
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
VALUES (119, 'server.mqtt_manager_api', 'null', 'string', 1, 'MQTT网关管理API的地址');
@@ -0,0 +1,54 @@
-- 添加阿里百炼流式TTS 供应器
delete from `ai_model_provider` where id = 'SYSTEM_TTS_AliBLStreamTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_AliBLStreamTTS', 'TTS', 'alibl_stream', '阿里百炼流式语音合成', '[{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "rate", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"}]', 19, 1, NOW(), 1, NOW());
-- 添加阿里百炼流式TTS模型配置
delete from `ai_model_config` where id = 'TTS_AliBLStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_AliBLStreamTTS', 'TTS', 'AliBLStreamTTS', '阿里百炼流式语音合成', 0, 1, '{\"type\": \"alibl_stream\", \"appkey\": \"\", \"output_dir\": \"tmp/\", \"model\": \"cosyvoice-v2\", \"voice\": \"longcheng_v2\", \"format\": \"pcm\", \"sample_rate\": 24000, \"volume\": 50, \"rate\": 1, \"pitch\": 1}', NULL, NULL, 22, NULL, NULL, NULL, NULL);
-- 更新阿里百炼流式TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
`remark` = '阿里百炼流式TTS说明:
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key 创建项目并获取appkey
2. 支持实时流式合成,具有较低的延迟
3. 支持多种音色设置和音频参数调整
4. 支持CosyVoice-V3大模型音色,价格实惠(0.4元/万字符)
5. 支持实时调节音量、语速、音调等参数
6. 如果需要使用CosyVoice-V3模型和一些限制类型的音色,需要联系阿里百炼客服申请
' WHERE `id` = 'TTS_AliBLStreamTTS';
-- 添加阿里百炼流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_AliBLStreamTTS';
-- 语音助手
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0001', 'TTS_AliBLStreamTTS', '龙小淳-知性积极女', 'longxiaochun_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0002', 'TTS_AliBLStreamTTS', '龙小夏-沉稳权威女', 'longxiaoxia_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
-- 直播带货
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0003', 'TTS_AliBLStreamTTS', '龙安燃-活泼质感女', 'longanran', '中文及中英文混合', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0004', 'TTS_AliBLStreamTTS', '龙安宣-经典直播女', 'longanxuan', '中文及中英文混合', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
-- 社交陪伴
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0005', 'TTS_AliBLStreamTTS', '龙寒-温暖痴情男', 'longhan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0006', 'TTS_AliBLStreamTTS', '龙颜-温暖春风女', 'longyan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0007', 'TTS_AliBLStreamTTS', '龙菲菲-甜美矫情女', 'longfeifei_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
-- 方言
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0008', 'TTS_AliBLStreamTTS', '龙老铁-东北直率男', 'longlaotie_v2', '中文(东北)及中英文混合', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0009', 'TTS_AliBLStreamTTS', '龙嘉怡-知性粤语女', 'longjiayi_v2', '中文(粤语)及中英文混合', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
-- 童声
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0010', 'TTS_AliBLStreamTTS', '龙杰力豆-阳光顽皮男', 'longjielidou_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0011', 'TTS_AliBLStreamTTS', '龙铃-稚气呆板女', 'longling_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
-- 诗歌朗诵
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0012', 'TTS_AliBLStreamTTS', '李白-古代诗仙男', 'libai_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
-- 出海营销
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0013', 'TTS_AliBLStreamTTS', 'loongeva-知性英文女', 'loongeva_v2', '英式英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0014', 'TTS_AliBLStreamTTS', 'loongbrian-沉稳英文男', 'loongbrian_v2', '英式英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0015', 'TTS_AliBLStreamTTS', 'loongkyong-韩语女', 'loongkyong_v2', '韩语', NULL, NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0016', 'TTS_AliBLStreamTTS', 'loongtomoka-日语女', 'loongtomoka_v2', '日语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0017', 'TTS_AliBLStreamTTS', 'loongtomoya-日语男', 'loongtomoya_v2', '日语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
@@ -0,0 +1,41 @@
-- 添加讯飞流式TTS供应器
delete from `ai_model_provider` where id = 'SYSTEM_TTS_XunFeiStreamTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_XunFeiStreamTTS', 'TTS', 'xunfei_stream', '讯飞流式语音合成', '[{"key":"app_id","label":"APP_ID","type":"string"},{"key":"api_secret","label":"API_Secret","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "speed", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"},{"key": "oral_level", "type": "number", "label": "口语化等级"},{"key": "spark_assist", "type": "number", "label": "是否口语化"},{"key": "stop_split", "type": "number", "label": "服务端拆句"},{"key": "remain", "type": "number", "label": "保留书面语"}]', 20, 1, NOW(), 1, NOW());
-- 添加讯飞流式TTS模型配置
delete from `ai_model_config` where id = 'TTS_XunFeiStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_XunFeiStreamTTS', 'TTS', 'XunFeiStreamTTS', '讯飞流式语音合成', 0, 1, '{\"type\": \"xunfei_stream\", \"app_id\": \"\", \"api_secret\": \"\", \"api_key\": \"\", \"output_dir\": \"tmp/\", \"voice\": \"x5_lingxiaoxuan_flow\", \"format\": \"raw\", \"sample_rate\": 24000, \"volume\": 50, \"speed\": 50, \"pitch\": 50, \"oral_level\": \"mid\", \"spark_assist\": 1, \"stop_split\": 0, \"remain\": 0}', NULL, NULL, 23, NULL, NULL, NULL, NULL);
-- 更新讯飞流式TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.xfyun.cn/app/myapp',
`remark` = '讯飞流式TTS说明:
1. 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用
2. 选择需要的服务获取api相关配置 https://console.xfyun.cn/services/uts
3. 为需要使用的应用(APPID)购买相关服务 例如:超拟人合成 https://console.xfyun.cn/services/uts
5. 支持实时双流式通信,具有较低的延迟
6. 支持口语化设置和音频参数调整 注意:V5音色不支持相关口语化配置
7. 支持实时调节音量、语速、音调等参数
' WHERE `id` = 'TTS_XunFeiStreamTTS';
-- 添加讯飞流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_XunFeiStreamTTS';
-- 基础角色
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0001', 'TTS_XunFeiStreamTTS', '聆小璇', 'x5_lingxiaoxuan_flow', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0002', 'TTS_XunFeiStreamTTS', '聆飞逸', 'x5_lingfeiyi_flow', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0003', 'TTS_XunFeiStreamTTS', '聆小玥', 'x5_lingxiaoyue_flow', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0004', 'TTS_XunFeiStreamTTS', '聆玉昭', 'x5_lingyuzhao_flow', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0005', 'TTS_XunFeiStreamTTS', '聆玉言', 'x5_lingyuyan_flow', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
-- 需要添加对应的角色音色
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0006', 'TTS_XunFeiStreamTTS', '聆飞哲', 'x4_lingfeizhe_oral', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0007', 'TTS_XunFeiStreamTTS', '聆小璃', 'x4_lingxiaoli_oral', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0008', 'TTS_XunFeiStreamTTS', '聆小糖', 'x5_lingxiaotang_flow', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0009', 'TTS_XunFeiStreamTTS', '聆小琪', 'x4_lingxiaoqi_oral', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0010', 'TTS_XunFeiStreamTTS', '聆佑佑-童年女声', 'x4_lingyouyou_oral', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0011', 'TTS_XunFeiStreamTTS', '子津', 'x4_zijin_oral', '天津话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0012', 'TTS_XunFeiStreamTTS', '子阳', 'x4_ziyang_oral', '东北话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0013', 'TTS_XunFeiStreamTTS', 'Grant', 'x5_EnUs_Grant_flow', '英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0014', 'TTS_XunFeiStreamTTS', 'Lila', 'x5_EnUs_Lila_flow', '英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
@@ -0,0 +1,26 @@
-- 添加讯飞流式语音识别服务配置
delete from `ai_model_provider` where id = 'SYSTEM_ASR_XunfeiStream';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_XunfeiStream', 'ASR', 'xunfei_stream', '讯飞流式语音识别', '[{"key":"app_id","label":"应用ID","type":"string"},{"key":"api_key","label":"API_KEY","type":"password"},{"key":"api_secret","label":"API_SECRET","type":"password"},{"key":"domain","label":"识别领域","type":"string"},{"key":"language","label":"识别语言","type":"string"},{"key":"accent","label":"方言","type":"string"},{"key":"dwa","label":"动态修正","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW());
delete from `ai_model_config` where id = 'ASR_XunfeiStream';
INSERT INTO `ai_model_config` VALUES ('ASR_XunfeiStream', 'ASR', '讯飞流式语音识别', '讯飞流式语音识别服务', 0, 1, '{"type": "xunfei_stream", "app_id": "", "api_key": "", "api_secret": "", "domain": "slm", "language": "zh_cn", "accent": "mandarin", "dwa": "wpgs", "output_dir": "tmp/"}', 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html', '支持实时流式语音识别,适用于中文普通话及多种方言识别', 21, NULL, NULL, NULL, NULL);
-- 更新讯飞流式语音识别模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html',
`remark` = '讯飞流式语音识别配置说明:
1. 登录讯飞开放平台 https://www.xfyun.cn/
2. 创建语音识别应用获取APPID、APISecret、APIKey
3. 参数说明:
- app_id: 应用ID,在讯飞开放平台创建应用后获得
- api_key: API密钥,用于接口鉴权
- api_secret: API密钥,用于生成签名
- domain: 识别领域,默认slm(智能化语音转写)
- language: 识别语言,默认zh_cn(中文)
- accent: 方言类型,默认mandarin(普通话),支持cantonese(粤语)等
- dwa: 动态修正,默认wpgs(开启动态修正)
- output_dir: 音频文件输出目录,默认tmp/
4. 支持实时流式识别,适用于实时语音交互场景
5. 支持多种方言和语言识别
' WHERE `id` = 'ASR_XunfeiStream';
@@ -0,0 +1,19 @@
delete from `ai_model_config` where id = 'LLM_XunfeiSparkLLM';
INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火认知大模型', '讯飞星火认知大模型', 0, 1, '{"type": "openai", "model_name": "generalv3.5", "base_url": "https://spark-api-open.xf-yun.com/v1", "api_password": "你的api_password", "temperature": 0.5, "max_tokens": 2048, "top_p": 1.0, "frequency_penalty": 0.0}', 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', '讯飞星火认知大模型,支持多轮对话、文本生成等功能', 14, NULL, NULL, NULL, NULL);
-- 更新讯飞星火认知大模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html',
`remark` = '讯飞星火认知大模型配置说明:
1. 登录讯飞开放平台 https://www.xfyun.cn/,每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password
2. 创建星火认知大模型应用获取API Password
3. 参数说明:
- api_password: API Password,在讯飞开放平台创建应用后获得
- model_name: 模型名称,支持generalv3.5、generalv3等版本
- base_url: API地址,默认https://spark-api-open.xf-yun.com/v1
- temperature: 温度参数,控制生成随机性,范围0-1,默认0.5
- max_tokens: 最大输出token数,默认2048
- top_p: 核心采样参数,控制词汇多样性,默认1.0
- frequency_penalty: 频率惩罚,降低重复内容,默认0.0
4. 每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password。
' WHERE `id` = 'LLM_XunfeiSparkLLM';
@@ -0,0 +1,7 @@
-- 添加SM2国密算法密钥参数
-- 用于服务器端SM2加密解密功能
-- 添加SM2密钥参数
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
(120, 'server.public_key', '', 'string', 1, '服务器SM2公钥'),
(121, 'server.private_key', '', 'string', 1, '服务器SM2私钥');
@@ -303,3 +303,88 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202508131557.sql
- changeSet:
id: 202508271113
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202508271113.sql
- changeSet:
id: 202509051745
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509051745.sql
- changeSet:
id: 202509081140
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509081140.sql
- changeSet:
id: 202509091042
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509091042.sql
- changeSet:
id: 202509091633
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509091633.sql
- changeSet:
id: 202509080922
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509080922.sql
- changeSet:
id: 202509161610
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509161610.sql
- changeSet:
id: 202509161701
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509161701.sql
- changeSet:
id: 202509221530
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509221530.sql
- changeSet:
id: 202509191545
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509191545.sql
- changeSet:
id: 202509220926
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509220926.sql
- changeSet:
id: 202509220958
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509220958.sql
@@ -1,47 +1,146 @@
#Default
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=\u672A\u6388\u6743
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u624B\u673A\u53F7\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5F31\u5BC6\u7801
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
#简体中文
500=服务器内部异常
401=未授权
403=拒绝访问,没有权限
10041=\u8BBE\u5907\u672A\u627E\u5230
10001={0}不能为空
10002=数据库中已存在该记录
10003=获取参数失败
10004=账号或密码错误
10005=账号已被停用
10006=唯一标识不能为空
10007=验证码不正确
10008=先删除子菜单或按钮
10009=原密码不正确
10010=账号或密码不正确,您还有可以尝试{0}次
10011=上级部门选择错误
10012=上级菜单不能为自身
10013=数据权限接口,只能是Map类型参数
10014=请先删除下级部门
10015=请先删除部门下的用户
10016=部署失败,没有流程
10017=模型图不正确,请检查
10018=导出失败,模型ID为{0}
10019=请上传文件
10020=token不能为空
10021=token失效,请重新登录
10022=账号已被锁定
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
10024=上传文件失败{0}
10025=发送短信失败{0}
10026=邮件模板不存在
10027=Redis服务异常
10028=定时任务失败
10029=不能包含非法字符
10030=密码长度不足{0}位
10031=密码必须同时包含数字、大小写字母和特殊字符组成
10032=删除本数据异常
10033=设备验证码错误
10034=参数值不能为空
10035=参数类型不能为空
10036=不支持的参数类型
10037=参数值必须是有效的数字
10038=参数值必须是true或false
10039=参数值必须是有效的JSON数组格式
10040=参数值必须是有效的JSON格式
10041=设备未找到
10042={0}
10043=删除数据失败
10044=用户未登录
10045=WebSocket连接失败或连接超时
10046=保存声纹错误,请联系管理员
10047=每日发送次数已达上限
10048=旧密码输入错误
10049=设置的LLM不是openai和ollama
10050=token生成失败
10051=资源不存在
10052=默认智能体未找到
10053=智能体未找到
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
10055=短信发送失败
10056=短信连接建立失败
10057=智能体的声纹创建失败
10058=智能体的对应声纹更新失败
10059=智能体的对应声纹删除失败
10060=发送太频繁,请{0}秒后再试
10061=激活码不能为空
10062=激活码错误
10063=设备已激活
10064=该模型为默认模型,请先设置其他模型为默认模型
10065=新增数据失败
10066=修改数据失败
10067=图形验证码错误
10068=没有开启手机注册,没法使用短信验证码功能
10069=用户名不是手机号码,请重新输入
10070=此手机号码已经注册过
10071=输入的手机号码未注册
10072=当前不允许普通用户注册
10073=没有开启手机注册,没法使用找回密码功能
10074=输入的手机号码格式不正确
10075=输入的手机验证码错误
10076=字典类型不存在
10077=字典类型编码重复
10078=读取资源失败
10079=LLM大模型和Intent意图识别,选择参数不匹配
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
10081=删除声纹出现错误
10082=此次修改不允许,此声音已经注册为声纹了({0})
10083=修改声纹错误,请联系管理员
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
10085=音频数据不属于这个智能体
10086=音频数据是空的请检查上传数据
10087=声纹保存失败,请求不成功
10088=声纹保存失败,请求处理失败
10089=声纹注销失败,请求不成功
10090=声纹注销失败,请求处理失败
10091=供应器不存在
10092=设置的LLM不存在
10093=该模型配置已被智能体{0}引用,无法删除
10094=该LLM模型已被意图识别配置引用,无法删除
10095=无效服务端操作
10096=未配置服务端WebSocket地址
10097=目标WebSocket地址不存在
10098=WebSocket地址列表不能为空
10099=WebSocket地址不能使用localhost或127.0.0.1
10100=WebSocket地址格式不正确
10101=WebSocket连接测试失败
10102=OTA地址不能为空
10103=OTA地址不能使用localhost或127.0.0.1
10104=OTA地址必须以http或https开头
10105=OTA地址必须以/ota/结尾
10106=OTA接口访问失败
10107=OTA接口返回内容格式不正确
10108=OTA接口验证失败
10109=MCP地址不能为空
10110=MCP地址不能使用localhost或127.0.0.1
10111=不是正确的MCP地址
10112=MCP接口访问失败
10113=MCP接口返回内容格式不正确
10114=MCP接口验证失败
10115=声纹接口地址不能为空
10116=声纹接口地址不能使用localhost或127.0.0.1
10117=不是正确的声纹接口地址
10118=声纹接口地址必须以http或https开头
10119=声纹接口访问失败
10120=声纹接口返回内容格式不正确
10121=声纹接口验证失败
10122=mqtt密钥不能为空
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
10125=您的mqtt密钥包含弱密码
10128=字典标签重复
10129=modelType和provideCode不能为空
10132=没有权限查看该智能体的聊天记录
10133=会话ID不能为空
10134=智能体ID不能为空
10135=聊天记录下载失败
10136=下载链接已过期或无效
10137=下载链接无效
10138=用户
10139=智体
@@ -11,19 +11,26 @@
10007=The verification code is incorrect
10008=First delete submenu or button
10009=The original password is incorrect
10010=The account or password is incorrect, you can try {0} more times
10011=The superior department made a wrong choice
10012=Upper menu cannot be for itself
10013=Data permission interface, which can only be a Map type parameter.
10014=Please delete the subordinate department first
10015=Please delete the user under the department first
10016=Deployment failed, no process
10017=The model diagram is incorrect, please check
10018=Export failed, model ID is {0}
10019=Please upload a file
10020=token cannot be empty
10021=token is invalid, please log in again
10022=The account has been locked
10023=Please upload files in zip, bar, bpmn, bpmn20.xml format
10024=Failed to upload file {0}
10025=Failed to send SMS {0}
10026=Email template does not exist
10027=Redis service exception
10028=Timed task failed
@@ -43,4 +50,99 @@
10040=Parameter value must be a valid JSON format
10041=Device not found
10042={0}
10042={0}
10043=Failed to delete data
10044=User not logged in
10045=WebSocket connection failed or timeout
10046=Failed to save voiceprint, please contact administrator
10047=Daily sending limit reached
10048=Old password is incorrect
10049=The set LLM is not openai or ollama
10050=Failed to generate token
10051=Resource not found
10052=Default agent not found
10053=Agent not found
10054=Voiceprint interface not configured, please configure voiceprint interface address (server.voice_print) in parameter configuration first
10055=Failed to send SMS
10056=Failed to establish SMS connection
10057=Failed to create voiceprint for agent
10058=Failed to update voiceprint for agent
10059=Failed to delete voiceprint for agent
10060=Sending too frequently, please try again after {0} seconds
10061=Activation code cannot be empty
10062=Activation code error
10063=Device already activated
10064=This model is the default model, please set another model as default first
10065=Failed to add data
10066=Failed to update data
10067=Graphic verification code error
10068=Mobile registration is not enabled, SMS verification code function cannot be used
10069=Username is not a mobile phone number, please re-enter
10070=This mobile phone number has already been registered
10071=The entered mobile phone number is not registered
10072=Ordinary user registration is not allowed currently
10073=Mobile registration is not enabled, password retrieval function cannot be used
10074=The entered mobile phone number format is incorrect
10075=The entered SMS verification code is incorrect
10076=Dictionary type does not exist
10077=Dictionary type code is duplicated
10078=Failed to read resource
10079=LLM model and Intent recognition, parameter selection does not match
10080=This voiceprint belongs to {0} who has already registered, please select another voice
10081=Error occurred while deleting voiceprint
10082=Modification not allowed, this voice has already been registered as a voiceprint ({0})
10083=Error modifying voiceprint, please contact administrator
10084=Voiceprint interface address error, please go to parameter management to modify the voiceprint interface address
10085=Audio data does not belong to this agent
10086=Audio data is empty, please check uploaded data
10087=Voiceprint registration failed, request unsuccessful
10088=Voiceprint registration failed, request processing failed
10089=Voiceprint cancellation failed, request unsuccessful
10090=Voiceprint cancellation failed, request processing failed
10091=Model provider does not exist
10092=The configured LLM does not exist
10093=This model configuration is referenced by agent {0} and cannot be deleted
10094=This LLM model is referenced by intent recognition configuration and cannot be deleted
10095=Invalid server operation
10096=Server WebSocket address not configured
10097=Target WebSocket address does not exist
10098=WebSocket address list cannot be empty
10099=WebSocket address cannot use localhost or 127.0.0.1
10100=WebSocket address format is incorrect
10101=WebSocket connection test failed
10102=OTA address cannot be empty
10103=OTA address cannot use localhost or 127.0.0.1
10104=OTA address must start with http or https
10105=OTA address must end with /ota/
10106=OTA interface access failed
10107=OTA interface return content format is incorrect
10108=OTA interface validation failed
10109=MCP address cannot be empty
10110=MCP address cannot use localhost or 127.0.0.1
10111=Not a valid MCP address
10112=MCP interface access failed
10113=MCP interface return content format is incorrect
10114=MCP interface validation failed
10115=Voiceprint interface address cannot be empty
10116=Voiceprint interface address cannot use localhost or 127.0.0.1
10117=Not a valid voiceprint interface address
10118=Voiceprint interface address must start with http or https
10119=Voiceprint interface access failed
10120=Voiceprint interface return content format is incorrect
10121=Voiceprint interface validation failed
10122=MQTT secret cannot be empty
10123=Your MQTT secret is not secure, it needs to be at least 8 characters and must contain both uppercase and lowercase letters
10124=Your MQTT secret is not secure, MQTT secret must contain both uppercase and lowercase letters
10125=Your MQTT secret contains weak password
10128=Dictionary label is duplicated
10129=SM2 key not configured
10130=SM2 decryption failed
10131=modelType and provideCode cannot be empty
10132=No permission to view the chat history of this agent
10133=Session ID cannot be empty
10134=Agent ID cannot be empty
10135=Chat history download failed
10136=Download link expired or invalid
10137=Download link invalid
10138=User
10139=Agent
@@ -1,46 +1,148 @@
#\u7B80\u4F53\u4E2D\u6587
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=\u672A\u6388\u6743
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
#简体中文
500=服务器内部异常
401=未授权
403=拒绝访问,没有权限
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10001={0}不能为空
10002=数据库中已存在该记录
10003=获取参数失败
10004=账号或密码错误
10005=账号已被停用
10006=唯一标识不能为空
10007=验证码不正确
10008=先删除子菜单或按钮
10009=原密码不正确
10010=账号或密码不正确,您还有可以尝试{0}次
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10011=上级部门选择错误
10012=上级菜单不能为自身
10013=数据权限接口,只能是Map类型参数
10014=请先删除下级部门
10015=请先删除部门下的用户
10016=部署失败,没有流程
10017=模型图不正确,请检查
10018=导出失败,模型ID为{0}
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10019=请上传文件
10020=token不能为空
10021=token失效,请重新登录
10022=账号已被锁定
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10024=上传文件失败{0}
10025=发送短信失败{0}
10026=邮件模板不存在
10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10027=Redis服务异常
10028=定时任务失败
10029=不能包含非法字符
10030=密码长度不足{0}位
10031=密码必须同时包含数字、大小写字母和特殊字符组成
10032=删除本数据异常
10033=设备验证码错误
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10034=参数值不能为空
10035=参数类型不能为空
10036=不支持的参数类型
10037=参数值必须是有效的数字
10038=参数值必须是true或false
10039=参数值必须是有效的JSON数组格式
10040=参数值必须是有效的JSON格式
10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0}
10041=设备未找到
10042={0}
10043=删除数据失败
10044=用户未登录
10045=WebSocket连接失败或连接超时
10046=保存声纹错误,请联系管理员
10047=每日发送次数已达上限
10048=旧密码输入错误
10049=设置的LLM不是openai和ollama
10050=token生成失败
10051=资源不存在
10052=默认智能体未找到
10053=智能体未找到
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
10055=短信发送失败
10056=短信连接建立失败
10057=智能体的声纹创建失败
10058=智能体的对应声纹更新失败
10059=智能体的对应声纹删除失败
10060=发送太频繁,请{0}秒后再试
10061=激活码不能为空
10062=激活码错误
10063=设备已激活
10064=该模型为默认模型,请先设置其他模型为默认模型
10065=新增数据失败
10066=修改数据失败
10067=图形验证码错误
10068=没有开启手机注册,没法使用短信验证码功能
10069=用户名不是手机号码,请重新输入
10070=此手机号码已经注册过
10071=输入的手机号码未注册
10072=当前不允许普通用户注册
10073=没有开启手机注册,没法使用找回密码功能
10074=输入的手机号码格式不正确
10075=输入的手机验证码错误
10076=字典类型不存在
10077=字典类型编码重复
10078=读取资源失败
10079=LLM大模型和Intent意图识别,选择参数不匹配
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
10081=删除声纹出现错误
10082=此次修改不允许,此声音已经注册为声纹了({0})
10083=修改声纹错误,请联系管理员
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
10085=音频数据不属于这个智能体
10086=音频数据是空的请检查上传数据
10087=声纹保存失败,请求不成功
10088=声纹保存失败,请求处理失败
10089=声纹注销失败,请求不成功
10090=声纹注销失败,请求处理失败
10091=供应器不存在
10092=设置的LLM不存在
10093=该模型配置已被智能体{0}引用,无法删除
10094=该LLM模型已被意图识别配置引用,无法删除
10095=无效服务端操作
10096=未配置服务端WebSocket地址
10097=目标WebSocket地址不存在
10098=WebSocket地址列表不能为空
10099=WebSocket地址不能使用localhost或127.0.0.1
10100=WebSocket地址格式不正确
10101=WebSocket连接测试失败
10102=OTA地址不能为空
10103=OTA地址不能使用localhost或127.0.0.1
10104=OTA地址必须以http或https开头
10105=OTA地址必须以/ota/结尾
10106=OTA接口访问失败
10107=OTA接口返回内容格式不正确
10108=OTA接口验证失败
10109=MCP地址不能为空
10110=MCP地址不能使用localhost或127.0.0.1
10111=不是正确的MCP地址
10112=MCP接口访问失败
10113=MCP接口返回内容格式不正确
10114=MCP接口验证失败
10115=声纹接口地址不能为空
10116=声纹接口地址不能使用localhost或127.0.0.1
10117=不是正确的声纹接口地址
10118=声纹接口地址必须以http或https开头
10119=声纹接口访问失败
10120=声纹接口返回内容格式不正确
10121=声纹接口验证失败
10122=mqtt密钥不能为空
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
10125=您的mqtt密钥包含弱密码
10128=字典标签重复
10129=SM2密钥未配置
10130=SM2解密失败
10131=modelType和provideCode不能为空
10132=没有权限查看该智能体的聊天记录
10133=会话ID不能为空
10134=智能体ID不能为空
10135=聊天记录下载失败
10136=下载链接已过期或无效
10137=下载链接无效
10138=用户
10139=智体
@@ -1,46 +1,148 @@
#\u7E41\u4F53\u4E2D\u6587
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
401=\u672A\u6388\u6B0A
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650
#繁體中文
500=服務器內部異常
401=未授權
403=拒絕訪問,沒有权限
10001={0}\u4E0D\u80FD\u70BA\u7A7A
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
10001={0}不能為空
10002=數據庫中已存在該記錄
10003=獲取參數失敗
10004=賬號或密碼錯誤
10005=賬號已被停用
10006=唯一標識不能為空
10007=驗證碼不正確
10008=先刪除子菜單或按鈕
10009=原密碼不正確
10010=帳號或密碼不正確,您還有可以嘗試{0}次
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
10011=上級部門選擇錯誤
10012=上級菜單不能為自身
10013=數據權限接口,只能是Map類型參數
10014=請先刪除下級部門
10015=請先刪除部門下的用戶
10016=部署失敗,沒有流程
10017=模型圖不正確,請檢查
10018=導出失敗,模型ID為{0}
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
10020=token\u4E0D\u80FD\u70BA\u7A7A
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
10019=請上傳文件
10020=token不能為空
10021=token失效,請重新登錄
10022=賬號已被鎖定
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
10024=上傳文件失敗{0}
10025=發送短信失敗{0}
10026=郵件模板不存在
10027=Redis\u670D\u52D9\u7570\u5E38
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
10027=Redis服務異常
10028=定時任務失敗
10029=不能包含非法字符
10030=密碼長度不足{0}位
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成
10032=刪除本數據異常
10033=設備驗證碼錯誤
10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
10036=\u4E0D\u652F\u63F4\u7684\u53C3\u6578\u985E\u578B
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10034=參數值不能為空
10035=參數類型不能為空
10036=不支持的參數類型
10037=參數值必須是有效的數字
10038=參數值必須是true或false
10039=參數值必須是有效的JSON數組格式
10040=參數值必須是有效的JSON格式
10041=\u8A2D\u5099\u672A\u627E\u5230
10042={0}
10041=設備未找到
10042={0}
10043=刪除數據失敗
10044=用戶未登錄
10045=WebSocket連接失敗或連接超時
10046=保存聲紋錯誤,請聯繫管理員
10047=每日發送次數已達上限
10048=舊密碼輸入錯誤
10049=設置的LLM不是openai和ollama
10050=token生成失敗
10051=資源不存在
10052=默認智能體未找到
10053=智能體未找到
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print)
10055=短信發送失敗
10056=短信連接建立失敗
10057=智能體的聲紋創建失敗
10058=智能體的對應聲紋更新失敗
10059=智能體的對應聲紋刪除失敗
10060=發送太頻繁,請{0}秒後再試
10061=激活碼不能為空
10062=激活碼錯誤
10063=設備已激活
10064=該模型為預設模型,請先設置其他模型為預設模型
10065=新增數據失敗
10066=更新數據失敗
10067=圖形驗證碼錯誤
10068=未開啟手機註冊,無法使用短信驗證碼功能
10069=用戶名不是手機號碼,請重新輸入
10070=該手機號碼已註冊
10071=輸入的手機號碼未註冊
10072=目前不允許普通用戶註冊
10073=未開啟手機註冊,無法使用密碼找回功能
10074=輸入的手機號碼格式不正確
10075=輸入的短信驗證碼不正確
10076=字典類型不存在
10077=字典類型編碼重複
10078=讀取資源失敗
10079=LLM模型與意圖識別、參數選擇不匹配
10080=此聲紋屬於已註冊的{0},請選擇其他聲音
10081=刪除聲紋時發生錯誤
10082=不允許修改,此聲音已註冊為聲紋({0})
10083=修改聲紋錯誤,請聯繫管理員
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址
10085=音頻數據不屬於此智能體
10086=音頻數據為空,請檢查上傳數據
10087=聲紋註冊失敗,請求未成功
10088=聲紋註冊失敗,請求處理失敗
10089=聲紋註銷失敗,請求未成功
10090=聲紋註銷失敗,請求處理失敗
10091=模型提供商不存在
10092=配置的LLM不存在
10093=此模型配置被智能體{0}引用,無法刪除
10094=此LLM模型被意圖識別配置引用,無法刪除
10095=無效的服務器操作
10096=未配置服務器WebSocket地址
10097=目標WebSocket地址不存在
10098=WebSocket地址列表不能為空
10099=WebSocket地址不能使用localhost或127.0.0.1
10100=WebSocket地址格式不正確
10101=WebSocket連接測試失敗
10102=OTA地址不能為空
10103=OTA地址不能使用localhost或127.0.0.1
10104=OTA地址必須以http或https開頭
10105=OTA地址必須以/ota/結尾
10106=OTA接口訪問失敗
10107=OTA接口返回內容格式不正確
10108=OTA接口驗證失敗
10109=MCP地址不能為空
10110=MCP地址不能使用localhost或127.0.0.1
10111=不是有效的MCP地址
10112=MCP接口訪問失敗
10113=MCP接口返回內容格式不正確
10114=MCP接口驗證失敗
10115=聲紋接口地址不能為空
10116=聲紋接口地址不能使用localhost或127.0.0.1
10117=不是有效的聲紋接口地址
10118=聲紋接口地址必須以http或https開頭
10119=聲紋接口訪問失敗
10120=聲紋接口返回內容格式不正確
10121=聲紋接口驗證失敗
10122=MQTT密鑰不能為空
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母
10125=您的MQTT密鑰包含弱密碼
10128=字典標籤重複
10129=SM2密鑰未配置
10130=SM2解密失敗
10131=modelType和provideCode不能為空
10132=沒有權限查看該智能體的聊天記錄
10133=會話ID不能為空
10134=智能體ID不能為空
10135=聊天記錄下載失敗
10136=下載鏈接已過期或無效
10137=下載鏈接無效
10138=用戶
10139=智體
@@ -24,7 +24,6 @@ class loginControllerTest {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("手机号码");
loginDTO.setPassword("密码");
loginDTO.setCaptcha("123456");
loginController.register(loginDTO);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 159 KiB

+1
View File
@@ -100,6 +100,7 @@
"js-cookie": "^3.0.5",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"sm-crypto": "^0.3.13",
"vue": "^3.4.21",
"wot-design-uni": "^1.9.1",
"z-paging": "2.8.7"
+55
View File
@@ -1,12 +1,16 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { watch, onMounted } from 'vue'
import { usePageAuth } from '@/hooks/usePageAuth'
import { useConfigStore } from '@/store'
import { t } from '@/i18n'
import { useLangStore } from '@/store/lang'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
usePageAuth()
const configStore = useConfigStore()
const langStore = useLangStore()
onLaunch(() => {
console.log('App Launch')
@@ -17,7 +21,58 @@ onLaunch(() => {
})
onShow(() => {
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(() => {
console.log('App Hide')
})
+1 -3
View File
@@ -4,7 +4,6 @@ import { http } from '@/http/request/alova'
export interface LoginData {
username: string
password: string
captcha: string
captchaId: string
areaCode?: string
mobile?: string
@@ -68,6 +67,7 @@ export interface PublicConfig {
beianIcpNum: string
beianGaNum: string
name: string
sm2PublicKey: string
}
// 获取用户信息
@@ -94,8 +94,6 @@ export function getPublicConfig() {
export interface RegisterData {
username: string
password: string
confirmPassword: string
captcha: string
captchaId: string
areaCode: string
mobile: string
+419
View File
@@ -0,0 +1,419 @@
// 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': 'did not have an account?',
'login.registerNow': 'Register Now',
'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',
// 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',
// 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 0.8.3',
'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: '繁體中文' },
]
}
+419
View File
@@ -0,0 +1,419 @@
// 简体中文语言包
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.registerNow': '立即注册',
'login.enterPhone': '请输入手机号码',
'login.selectCountry': '选择国家/地区',
'login.confirm': '确认',
'login.serverSetting': '服务端设置',
'login.requiredUsername': '用户名不能为空',
'login.requiredPassword': '密码不能为空',
'login.requiredCaptcha': '验证码不能为空',
'login.requiredMobile': '请输入正确的手机号码',
'login.captchaError': '图形验证码错误',
// 注册页面
'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': '播放失败',
}
+419
View File
@@ -0,0 +1,419 @@
// 繁體中文語言包
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.registerNow': '立即註冊',
'login.enterPhone': '請輸入手機號碼',
'login.selectCountry': '選擇國家/地區',
'login.confirm': '確認',
'login.serverSetting': '服務端設置',
'login.requiredUsername': '用戶名不能為空',
'login.requiredPassword': '密碼不能為空',
'login.requiredCaptcha': '驗證碼不能為空',
'login.requiredMobile': '請輸入正確的手機號碼',
'login.captchaError': '圖形驗證碼錯誤',
// 註冊頁面
'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 0.8.3',
'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 'virtual:uno.css'
// 导入国际化相关功能
import { initI18n } from './i18n'
import { useLangStore } from './store/lang'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
app.use(routeInterceptor)
app.use(VueQueryPlugin)
// 初始化国际化
initI18n()
return {
app,
}
+4 -4
View File
@@ -1,7 +1,7 @@
{
"globalStyle": {
"navigationStyle": "default",
"navigationBarTitleText": "小智智控台",
"navigationBarTitleText": "智控台",
"navigationBarBackgroundColor": "#f8f8f8",
"navigationBarTextStyle": "black",
"backgroundColor": "#FFFFFF"
@@ -104,7 +104,7 @@
"path": "pages/device-config/index",
"type": "page",
"style": {
"navigationBarTitleText": "设备配",
"navigationBarTitleText": "设备配",
"navigationStyle": "custom"
}
},
@@ -114,7 +114,7 @@
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "登陆"
"navigationBarTitleText": "Login"
}
},
{
@@ -155,4 +155,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 { usePluginStore } from '@/store'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({
name: 'AgentEdit',
@@ -37,14 +38,15 @@ const formData = ref<Partial<AgentDetail>>({
// 显示名称数据
const displayNames = ref({
vad: '请选择',
asr: '请选择',
llm: '请选择',
vllm: '请选择',
intent: '请选择',
memory: '请选择',
tts: '请选择',
voiceprint: '请选择',
// 显示名称数据
vad: t('agent.pleaseSelect'),
asr: t('agent.pleaseSelect'),
llm: t('agent.pleaseSelect'),
vllm: t('agent.pleaseSelect'),
intent: t('agent.pleaseSelect'),
memory: t('agent.pleaseSelect'),
tts: t('agent.pleaseSelect'),
voiceprint: t('agent.pleaseSelect'),
})
// 角色模板数据
@@ -143,7 +145,7 @@ async function loadAgentDetail() {
}
catch (error) {
console.error('加载智能体详情失败:', error)
toast.error('加载失败')
toast.error(t('agent.loadFail'))
}
finally {
loading.value = false
@@ -358,12 +360,12 @@ function getModelDisplayName(modelType: string, modelId: string) {
// 保存智能体
async function saveAgent() {
if (!formData.value.agentName?.trim()) {
toast.warning('请输入助手昵称')
toast.warning(t('agent.pleaseInputAgentName'))
return
}
if (!formData.value.systemPrompt?.trim()) {
toast.warning('请输入角色介绍')
toast.warning(t('agent.pleaseInputRoleDescription'))
return
}
@@ -371,11 +373,11 @@ async function saveAgent() {
saving.value = true
await updateAgent(agentId.value, formData.value)
toast.success('保存成功')
toast.success(t('agent.saveSuccess'))
}
catch (error) {
console.error('保存失败:', error)
toast.error('保存失败')
toast.error(t('agent.saveFail'))
}
finally {
saving.value = false
@@ -441,10 +443,10 @@ onMounted(async () => {
<template>
<view class="bg-[#f5f7fb] px-[20rpx]">
<!-- 基础信息标题 -->
<!--// 基础信息标题
<view class="pb-[20rpx] first:pt-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
基础信息
{{ t('agent.basicInfo') }}
</text>
</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] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
助手昵称
</text>
{{ t('agent.agentName') }}
</text>
<input
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]"
type="text"
placeholder="请输入助手昵称"
:placeholder="t('agent.inputAgentName')"
>
</view>
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
角色模式
</text>
{{ t('agent.roleMode') }}
</text>
<view class="mt-0 flex flex-wrap gap-[12rpx]">
<view
v-for="template in roleTemplates"
@@ -483,12 +485,12 @@ onMounted(async () => {
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
角色介绍
</text>
{{ t('agent.roleDescription') }}
</text>
<textarea
v-model="formData.systemPrompt"
: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]"
/>
<view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]">
@@ -500,7 +502,7 @@ onMounted(async () => {
<!-- 模型配置标题 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
模型配置
{{ t('agent.modelConfig') }}
</text>
</view>
@@ -509,7 +511,7 @@ onMounted(async () => {
<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')">
<text class="text-[28rpx] text-[#232338] font-medium">
语音活动检测
{{ t('agent.vad') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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')">
<text class="text-[28rpx] text-[#232338] font-medium">
语音识别
{{ t('agent.asr') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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')">
<text class="text-[28rpx] text-[#232338] font-medium">
大语言模型
{{ t('agent.llm') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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')">
<text class="text-[28rpx] text-[#232338] font-medium">
视觉大模型
{{ t('agent.vllm') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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')">
<text class="text-[28rpx] text-[#232338] font-medium">
意图识别
{{ t('agent.intent') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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')">
<text class="text-[28rpx] text-[#232338] font-medium">
记忆
{{ t('agent.memory') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.memory }}
@@ -572,7 +574,7 @@ onMounted(async () => {
<!-- 语音设置标题 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
语音设置
{{ t('agent.voiceSettings') }}
</text>
</view>
@@ -581,7 +583,7 @@ onMounted(async () => {
<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')">
<text class="text-[28rpx] text-[#232338] font-medium">
语音合成
{{ t('agent.tts') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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')">
<text class="text-[28rpx] text-[#232338] font-medium">
角色音色
{{ t('agent.voiceprint') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ 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="text-[28rpx] text-[#232338] font-medium">
插件
{{ t('agent.plugins') }}
</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">
<text>编辑功能</text>
<text>{{ t('agent.editFunctions') }}</text>
</view>
</view>
</view>
@@ -613,7 +615,7 @@ onMounted(async () => {
<!-- 记忆历史标题 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
历史记忆
{{ t('agent.historyMemory') }}
</text>
</view>
@@ -622,7 +624,7 @@ onMounted(async () => {
<view class="mb-[24rpx] last:mb-0">
<textarea
v-model="formData.summaryMemory"
placeholder="记忆内容"
:placeholder="t('agent.memoryContent')"
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"
/>
@@ -638,7 +640,7 @@ onMounted(async () => {
custom-class="w-full h-[80rpx] rounded-[16rpx] text-[30rpx] font-semibold bg-[#336cff] active:bg-[#2d5bd1]"
@click="saveAgent"
>
{{ saving ? '保存中...' : '保存' }}
{{ saving ? t('agent.saving') : t('agent.save') }}
</wd-button>
</view>
<!-- 模型选择器 -->
@@ -11,6 +11,7 @@
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { t } from '@/i18n'
import CustomTabs from '@/components/custom-tabs/index.vue'
import ChatHistory from '@/pages/chat-history/index.vue'
import DeviceManagement from '@/pages/device/index.vue'
@@ -42,7 +43,6 @@ systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
// 智能体ID
const currentAgentId = ref('default')
@@ -65,25 +65,25 @@ const voiceprintRef = ref()
// Tab 配置
const tabList = [
{
label: '角色配置',
label: t('agent.roleConfig'),
value: 'agent-config',
icon: '/static/tabbar/robot.png',
activeIcon: '/static/tabbar/robot_activate.png',
},
{
label: '设备管理',
label: t('agent.deviceManagement'),
value: 'device-management',
icon: '/static/tabbar/device.png',
activeIcon: '/static/tabbar/device_activate.png',
},
{
label: '聊天记录',
label: t('agent.chatHistory'),
value: 'chat-history',
icon: '/static/tabbar/chat.png',
activeIcon: '/static/tabbar/chat_activate.png',
},
{
label: '声纹管理',
label: t('agent.voiceprintManagement'),
value: 'voiceprint-management',
icon: '/static/tabbar/microphone.png',
activeIcon: '/static/tabbar/microphone_activate.png',
@@ -160,7 +160,7 @@ onMounted(async () => {
<template>
<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>
<wd-icon name="arrow-left" size="18" @click="goBack" />
</template>
+35 -34
View File
@@ -3,8 +3,8 @@
"layout": "default",
"style": {
"navigationBarTitleText": "编辑功能",
"navigationStyle": "custom",
},
"navigationStyle": "custom"
}
}
</route>
@@ -12,12 +12,13 @@
import { useMessage } from 'wot-design-uni'
import { getMcpAddress, getMcpTools } from '@/api/agent/agent'
import { usePluginStore } from '@/store'
import { t } from '@/i18n'
const message = useMessage()
const pluginStore = usePluginStore()
const segmentedList = ref<string[]>(['未选', '已选'])
const currentSegmented = ref('未选')
const segmentedList = ref<string[]>([t('agent.tools.notSelected'), t('agent.tools.selected')])
const currentSegmented = ref(t('agent.tools.notSelected'))
const notSelectedList = ref<any[]>([])
const selectedList = ref<any[]>([])
@@ -191,7 +192,7 @@ function handleJsonChange(key: string, value: string, field: any) {
}
}
catch {
message.alert('JSON格式错误')
message.alert(t('agent.tools.jsonFormatError'))
}
}
@@ -220,7 +221,7 @@ function goBack() {
// 复制MCP地址
function copyMcpAddress() {
if (!mcpAddress.value) {
message.alert('暂无MCP地址可复制')
message.alert(t('agent.tools.noMcpAddressToCopy'))
return
}
@@ -228,11 +229,11 @@ function copyMcpAddress() {
data: mcpAddress.value,
showToast: false,
success: () => {
message.alert('MCP地址已复制到剪贴板')
},
message.alert(t('agent.tools.mcpAddressCopied'))
},
fail: () => {
message.alert('复制失败,请重试')
},
message.alert(t('agent.tools.copyFailed'))
},
})
}
@@ -248,7 +249,7 @@ function getFieldDisplayValue(field: any, value: any) {
function getFieldRemark(field: any) {
let description = field.label || ''
if (field.default) {
description += `默认值${field.default}`
description += `${t('agent.tools.defaultValue')}${field.default}`
}
return description
}
@@ -284,7 +285,7 @@ onMounted(async () => {
<!-- 内置插件区域 -->
<view class="mt-[20rpx] flex flex-1 flex-col">
<view class="text-[32rpx] text-[#333] font-medium">
内置插件
{{ t('agent.tools.builtInPlugins') }}
</view>
<view
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">
<!-- 未选插件 -->
<scroll-view
v-if="currentSegmented === '未选'"
v-if="currentSegmented === t('agent.tools.notSelected')"
class="max-h-[600rpx] bg-transparent"
scroll-y
>
@@ -307,7 +308,7 @@ onMounted(async () => {
v-if="notSelectedList.length === 0"
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 v-else class="p-[20rpx] space-y-[20rpx]">
<view
@@ -343,7 +344,7 @@ onMounted(async () => {
v-if="selectedList.length === 0"
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 v-else class="p-[20rpx] space-y-[20rpx]">
<view
@@ -359,7 +360,7 @@ onMounted(async () => {
{{ func.name }}
</view>
<view class="text-[24rpx] text-[#1677ff]">
点击配置参数
{{ t('agent.tools.clickToConfigure') }}
</view>
</view>
<view class="flex space-x-[20rpx]">
@@ -393,7 +394,7 @@ onMounted(async () => {
<!-- MCP接入点区域 -->
<view class="mt-[20rpx] flex flex-1 flex-col">
<view class="text-[32rpx] text-[#333] font-medium">
mcp接入点
{{ t('agent.tools.mcpEndpoint') }}
</view>
<view
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]"
>
<view
class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white"
@click="copyMcpAddress"
>
复制
</view>
class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white"
@click="copyMcpAddress"
>
{{ t('agent.tools.copy') }}
</view>
</view>
<!-- 工具列表 -->
<view class="mt-[20rpx] flex-1 overflow-hidden">
@@ -419,7 +420,7 @@ onMounted(async () => {
v-if="mcpTools && mcpTools.length === 0"
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 v-else class="p-[20rpx]">
<view class="flex flex-wrap">
@@ -441,7 +442,7 @@ onMounted(async () => {
<!-- 参数编辑弹窗 -->
<wd-action-sheet
v-model="showParamDialog"
:title="`参数配置 - ${currentFunction?.name || ''}`"
:title="`${t('agent.tools.paramConfiguration')} - ${currentFunction?.name || ''}`"
custom-header-class="h-[75vh]"
@close="closeParamEdit"
>
@@ -460,7 +461,7 @@ onMounted(async () => {
class="h-[400rpx] flex items-center justify-center"
>
<text class="text-[28rpx] text-[#999]">
{{ currentFunction?.name }} 无需配置参数
{{ currentFunction?.name }} {{ t('agent.tools.noParamsNeeded') }}
</text>
</view>
@@ -490,7 +491,7 @@ onMounted(async () => {
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]"
type="text"
:placeholder="`请输入${field.label}`"
:placeholder="`${t('agent.tools.pleaseInput')}${field.label}`"
@input="
handleParamChange(field.key, $event.detail.value, field)
"
@@ -499,12 +500,12 @@ onMounted(async () => {
<!-- 数组类型 -->
<view v-else-if="field.type === 'array'">
<text class="mb-[16rpx] block text-[24rpx] text-[#65686f]">
每行输入一个项目
{{ t('agent.tools.eachLineOneItem') }}
</text>
<textarea
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]"
:placeholder="`请输入${field.label},每行一个`"
:placeholder="`${t('agent.tools.pleaseInput')}${field.label}${t('agent.tools.eachLineOneItem')}`"
@input="
handleArrayChange(field.key, $event.detail.value, field)
"
@@ -514,12 +515,12 @@ onMounted(async () => {
<!-- JSON类型 -->
<view v-else-if="field.type === 'json'">
<text class="mb-[16rpx] block text-[24rpx] text-[#65686f]">
请输入有效的JSON格式
{{ t('agent.tools.pleaseInputValidJson') }}
</text>
<textarea
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]"
placeholder="请输入合法的JSON格式"
:placeholder="t('agent.tools.pleaseInputValidJson')"
@blur="
handleJsonChange(field.key, $event.detail.value, field)
"
@@ -532,7 +533,7 @@ onMounted(async () => {
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]"
type="number"
:placeholder="`请输入${field.label}`"
:placeholder="`${t('agent.tools.pleaseInput')}${field.label}`"
@input="
handleParamChange(
field.key,
@@ -549,10 +550,10 @@ onMounted(async () => {
>
<view class="flex-1">
<text class="mb-[8rpx] block text-[28rpx] text-[#232338]">
启用功能
{{ t('agent.tools.enableFunction') }}
</text>
<text class="block text-[24rpx] text-[#65686f]">
开启或关闭此功能
{{ t('agent.tools.toggleFunction') }}
</text>
</view>
<switch
@@ -569,7 +570,7 @@ onMounted(async () => {
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]"
type="text"
:placeholder="`请输入${field.label}`"
:placeholder="`${t('agent.tools.pleaseInput')}${field.label}`"
@input="
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 { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({
name: 'ChatDetail',
@@ -49,7 +50,7 @@ const agentId = ref('')
const currentAgent = computed(() => {
return {
id: agentId.value,
agentName: '智能助手',
agentName: t('chatHistory.assistantName'),
}
})
@@ -80,7 +81,7 @@ async function loadChatHistory() {
}
catch (error) {
console.error('获取聊天记录失败:', error)
toast.error('获取聊天记录失败')
toast.error(t('chatHistory.loadFailed'))
}
finally {
loading.value = false
@@ -114,10 +115,10 @@ function getMessageContent(message: ChatMessage): string {
function getSpeakerName(message: ChatMessage): string {
if (message.chatType === 1) {
const parsed = parseUserMessage(message.content)
return parsed ? parsed.speaker : '用户'
return parsed ? parsed.speaker : t('chatHistory.userName')
}
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) {
if (!audioId) {
toast.error('音频ID无效')
toast.error(t('chatHistory.invalidAudioId'))
return
}
@@ -168,7 +169,7 @@ async function playAudio(audioId: string) {
// 监听播放错误
audioContext.value.onError((error) => {
console.error('音频播放失败:', error)
toast.error('音频播放失败')
toast.error(t('chatHistory.audioPlayFailed'))
playingAudioId.value = null
if (audioContext.value) {
audioContext.value.destroy()
@@ -181,7 +182,7 @@ async function playAudio(audioId: string) {
}
catch (error) {
console.error('播放音频失败:', error)
toast.error('播放音频失败')
toast.error(t('chatHistory.playAudioFailed'))
playingAudioId.value = null
}
}
@@ -194,7 +195,7 @@ onLoad((options) => {
}
else {
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` }" />
<!-- 导航栏 -->
<wd-navbar title="聊天详情">
<wd-navbar :title="t('chatHistory.pageTitle')">
<template #left>
<wd-icon name="arrow-left" size="18" @click="goBack" />
</template>
@@ -230,7 +231,7 @@ onUnload(() => {
<view v-if="loading" class="flex flex-col items-center justify-center gap-[20rpx] p-[100rpx_0]">
<wd-loading />
<text class="text-[28rpx] text-[#65686f]">
加载中...
{{ t('chatHistory.loading') }}
</text>
</view>
@@ -2,6 +2,7 @@
import type { ChatSession } from '@/api/chat-history/types'
import { computed, onMounted, ref } from 'vue'
import { getChatSessions } from '@/api/chat-history/chat-history'
import { t } from '@/i18n'
defineOptions({
name: 'ChatHistory',
@@ -53,11 +54,11 @@ const currentAgentId = computed(() => {
// 加载聊天会话列表
async function loadChatSessions(page = 1, isRefresh = false) {
try {
console.log('获取聊天会话列表', { page, isRefresh })
console.log(t('chatHistory.getChatSessions'), { page, isRefresh })
// 检查是否有当前选中的智能体
if (!currentAgentId.value) {
console.warn('没有选中的智能体')
console.warn(t('chatHistory.noSelectedAgent'))
sessionList.value = []
return
}
@@ -86,7 +87,7 @@ async function loadChatSessions(page = 1, isRefresh = false) {
currentPage.value = page
}
catch (error) {
console.error('获取聊天会话列表失败:', error)
console.error(t('chatHistory.getChatSessionsFailed'), error)
if (page === 1) {
sessionList.value = []
}
@@ -115,48 +116,48 @@ async function loadMore() {
// 格式化时间
function formatTime(timeStr: string) {
if (!timeStr)
return '未知时间'
return t('chatHistory.unknownTime')
// 处理时间字符串,确保格式正确
const date = new Date(timeStr.replace(' ', 'T')) // 转换为ISO格式
const now = new Date()
// 检查日期是否有效
if (Number.isNaN(date.getTime())) {
return timeStr // 如果解析失败,直接返回原字符串
}
const diff = now.getTime() - date.getTime()
// 小于1分钟
if (diff < 60000)
return '刚刚'
return t('chatHistory.justNow')
// 小于1小时
if (diff < 3600000)
return `${Math.floor(diff / 60000)}分钟前`
return t('chatHistory.minutesAgo', { minutes: Math.floor(diff / 60000) })
// 小于1天(24小时)
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}小时前`
return t('chatHistory.hoursAgo', { hours: Math.floor(diff / 3600000) })
// 小于7天
if (diff < 604800000) {
const days = Math.floor(diff / 86400000)
return `${days}天前`
return t('chatHistory.daysAgo', { days })
}
// 超过7天,显示具体日期
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const currentYear = now.getFullYear()
// 如果是当前年份,不显示年份
if (year === currentYear) {
return `${month}-${day}`
}
return `${year}-${month}-${day}`
}
@@ -186,8 +187,8 @@ defineExpose({
<view v-if="loading && sessionList.length === 0" class="loading-container">
<wd-loading color="#336cff" />
<text class="loading-text">
加载中...
</text>
{{ t('chatHistory.loading') }}
</text>
</view>
<!-- 会话列表 -->
@@ -204,7 +205,7 @@ defineExpose({
<view class="session-info">
<view class="session-header">
<text class="session-title">
对话记录 {{ session.sessionId.substring(0, 8) }}...
{{ t('chatHistory.conversationRecord') }} {{ session.sessionId.substring(0, 8) }}...
</text>
<text class="session-time">
{{ formatTime(session.createdAt) }}
@@ -212,7 +213,7 @@ defineExpose({
</view>
<view class="session-meta">
<text class="chat-count">
{{ session.chatCount }} 条对话
{{ t('chatHistory.totalChats', { count: session.chatCount }) }}
</text>
</view>
</view>
@@ -225,14 +226,14 @@ defineExpose({
<view v-if="loadingMore" class="loading-more">
<wd-loading color="#336cff" size="24" />
<text class="loading-more-text">
加载中...
{{ t('chatHistory.loading') }}
</text>
</view>
<!-- 没有更多数据 -->
<view v-else-if="!hasMore && sessionList.length > 0" class="no-more">
<text class="no-more-text">
没有更多数据了
{{ t('chatHistory.noMoreData') }}
</text>
</view>
</view>
@@ -241,11 +242,11 @@ defineExpose({
<view v-else-if="!loading" class="empty-state">
<wd-icon name="chat" custom-class="empty-icon" />
<text class="empty-text">
暂无聊天记录
</text>
<text class="empty-desc">
与智能体的对话记录会显示在这里
</text>
{{ t('chatHistory.noChatRecords') }}
</text>
<text class="empty-desc">
{{ t('chatHistory.chatRecordsDescription') }}
</text>
</view>
</view>
</template>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { t } from '@/i18n'
// 类型定义
interface WiFiNetwork {
@@ -53,7 +54,7 @@ const audioLengthText = computed(() => {
const textBytes = stringToBytes(dataStr)
const totalBits = (START_BYTES.length + textBytes.length + 1 + END_BYTES.length) * 8
const duration = Math.ceil(totalBits / BIT_RATE)
return `${duration}`
return `${t('deviceConfig.about')}${duration}${t('deviceConfig.seconds')}`
})
// 字符串转字节数组 - uniapp兼容版本
@@ -227,15 +228,15 @@ async function generateAndPlay() {
generating.value = true
try {
console.log('生成超声波配网音频...')
console.log(t('deviceConfig.generatingUltrasonicConfigAudio') + '...')
// 准备配网数据 - 参考HTML文件格式
const dataStr = `${props.selectedNetwork.ssid}\n${props.password}`
const textBytes = stringToBytes(dataStr)
const fullBytes = [...START_BYTES, ...textBytes, checksum(textBytes), ...END_BYTES]
console.log('配网数据:', { ssid: props.selectedNetwork.ssid, password: props.password })
console.log('数据字节长度:', textBytes.length)
console.log(t('deviceConfig.configData') + ':', { ssid: props.selectedNetwork.ssid, password: props.password })
console.log(t('deviceConfig.dataBytesLength') + ':', textBytes.length)
// 转换为比特流
let bits: number[] = []
@@ -243,7 +244,7 @@ async function generateAndPlay() {
bits = bits.concat(toBits(b))
})
console.log('比特流长度:', bits.length)
console.log(t('deviceConfig.bitStreamLength') + ':', bits.length)
// AFSK调制 - 减少采样率降低文件大小
const reducedSampleRate = 22050 // 降低采样率
@@ -266,19 +267,19 @@ async function generateAndPlay() {
const base64 = arrayBufferToBase64(wavBuffer)
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
throw new Error('音频文件过大,请缩短SSID或密码长度')
throw new Error(t('deviceConfig.audioFileTooLarge'))
}
audioFilePath.value = dataUri
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 () => {
@@ -286,8 +287,8 @@ async function generateAndPlay() {
}, 800) // 增加延迟时间
}
catch (error) {
console.error('音频生成失败:', error)
toast.error(`声波生成失败: ${error.message || error}`)
console.error(t('deviceConfig.audioGenerationFailed') + ':', error)
toast.error(`${t('deviceConfig.soundWaveGenerationFailed')}: ${error.message || error}`)
}
finally {
generating.value = false
@@ -344,7 +345,7 @@ function buildWavOptimized(pcm: Uint8Array, sampleRate: number): ArrayBuffer {
// 播放音频
async function playAudio() {
if (!audioFilePath.value) {
toast.error('请先生成音频')
toast.error(t('deviceConfig.pleaseGenerateAudioFirst'))
return
}
@@ -356,7 +357,7 @@ async function playAudio() {
await new Promise(resolve => setTimeout(resolve, 200))
playing.value = true
console.log('开始播放超声波配网音频')
console.log(t('deviceConfig.startPlayingUltrasonicConfigAudio'))
// 创建新的音频上下文
const innerAudioContext = uni.createInnerAudioContext()
@@ -370,12 +371,12 @@ async function playAudio() {
// 简化的事件监听
innerAudioContext.onPlay(() => {
console.log('超声波音频开始播放')
toast.success('开始播放配网声波')
console.log(t('deviceConfig.ultrasonicAudioStartedPlaying'))
toast.success(t('deviceConfig.startPlayingConfigSoundWave'))
})
innerAudioContext.onEnded(() => {
console.log('超声波音频播放结束')
console.log(t('deviceConfig.ultrasonicAudioPlaybackEnded'))
if (!autoLoop.value) {
playing.value = false
cleanupAudio()
@@ -383,18 +384,18 @@ async function playAudio() {
})
innerAudioContext.onError((error) => {
console.error('音频播放失败:', error)
console.error(t('deviceConfig.audioPlaybackFailed') + ':', error)
playing.value = false
let errorMsg = '音频播放失败'
let errorMsg = t('deviceConfig.audioPlaybackFailed')
if (error.errCode === -99) {
errorMsg = '音频资源繁忙,请稍后重试'
errorMsg = t('deviceConfig.audioResourceBusy')
}
else if (error.errCode === 10004) {
errorMsg = '音频格式不支持,可能是data URI问题'
errorMsg = t('deviceConfig.audioFormatNotSupported')
}
else if (error.errCode === 10003) {
errorMsg = '音频文件错误'
errorMsg = t('deviceConfig.audioFileError')
}
toast.error(errorMsg)
@@ -416,10 +417,10 @@ async function playAudio() {
}, 300)
}
catch (error) {
console.error('播放音频异常:', error)
playing.value = false
await cleanupAudio()
toast.error(`播放失败: ${error.message}`)
console.error(t('deviceConfig.audioPlaybackError') + ':', error)
playing.value = false
await cleanupAudio()
toast.error(`${t('deviceConfig.playbackFailed')}: ${error.message}`)
}
}
@@ -429,10 +430,10 @@ async function cleanupAudio() {
try {
audioContext.value.pause()
audioContext.value.destroy()
console.log('清理音频上下文')
console.log(t('deviceConfig.cleaningUpAudioContext'))
}
catch (e) {
console.log('清理音频上下文失败:', e)
console.log(t('deviceConfig.cleaningUpAudioContextFailed') + ':', e)
}
finally {
audioContext.value = null
@@ -445,8 +446,8 @@ async function stopAudio() {
playing.value = false
await cleanupAudio()
console.log('停止播放超声波音频')
toast.success('已停止播放')
console.log(t('deviceConfig.stoppedPlayingUltrasonicAudio'))
toast.success(t('deviceConfig.stoppedPlaying'))
}
</script>
@@ -456,18 +457,18 @@ async function stopAudio() {
<view v-if="props.selectedNetwork" class="selected-network">
<view class="network-info">
<view class="network-name">
选中网络: {{ props.selectedNetwork.ssid }}
{{ t('deviceConfig.selectedNetwork') }}: {{ props.selectedNetwork.ssid }}
</view>
<view class="network-details">
<text class="network-signal">
信号: {{ props.selectedNetwork.rssi }}dBm
{{ t('deviceConfig.signal') }}: {{ props.selectedNetwork.rssi }}dBm
</text>
<text class="network-security">
{{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }}
{{ props.selectedNetwork.authmode === 0 ? t('deviceConfig.openNetwork') : t('deviceConfig.encryptedNetwork') }}
</text>
</view>
<view v-if="props.password" class="network-password">
密码: {{ '*'.repeat(props.password.length) }}
{{ t('deviceConfig.password') }}: {{ '*'.repeat(props.password.length) }}
</view>
</view>
</view>
@@ -482,81 +483,81 @@ async function stopAudio() {
:disabled="!canGenerate"
@click="generateAndPlay"
>
{{ generating ? '生成中...' : '🎵 生成并播放声波' }}
</wd-button>
{{ generating ? t('deviceConfig.generating') : '🎵 ' + t('deviceConfig.generateAndPlaySoundWave') }}
</wd-button>
<wd-button
v-if="audioGenerated"
type="success"
size="large"
block
:loading="playing"
@click="playAudio"
>
{{ playing ? '播放中...' : '🔊 播放声波' }}
</wd-button>
<wd-button
v-if="audioGenerated"
type="success"
size="large"
block
:loading="playing"
@click="playAudio"
>
{{ playing ? t('deviceConfig.playing') : '🔊 ' + t('deviceConfig.playSoundWave') }}
</wd-button>
<wd-button
v-if="playing"
type="warning"
size="large"
block
@click="stopAudio"
>
停止播放
</wd-button>
<wd-button
v-if="playing"
type="warning"
size="large"
block
@click="stopAudio"
>
{{ t('deviceConfig.stopPlaying') }}
</wd-button>
</view>
<!-- 音频控制选项 -->
<view v-if="audioGenerated" class="audio-options">
<view class="option-item">
<wd-checkbox v-model="autoLoop">
自动循环播放声波
</wd-checkbox>
</view>
<wd-checkbox v-model="autoLoop">
{{ t('deviceConfig.autoLoopPlaySoundWave') }}
</wd-checkbox>
</view>
</view>
<!-- 音频播放器 -->
<view v-if="audioGenerated" class="audio-player">
<view class="player-info">
<text class="audio-title">
配网音频文件
{{ t('deviceConfig.configAudioFile') }}
</text>
<text class="audio-duration">
时长: {{ audioLengthText }}
{{ t('deviceConfig.duration') }}: {{ audioLengthText }}
</text>
</view>
</view>
<!-- 使用说明 -->
<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 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>
</template>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { t } from '@/i18n'
// 类型定义
interface WiFiNetwork {
@@ -44,7 +45,7 @@ async function checkESP32Connection() {
return response.statusCode === 200
}
catch (error) {
console.log('ESP32连接检查失败:', error)
console.log(t('deviceConfig.esp32ConnectionCheckFailed') + ':', error)
return false
}
}
@@ -57,12 +58,12 @@ async function submitConfig() {
// 检查ESP32连接
const connected = await checkESP32Connection()
if (!connected) {
toast.error('请先连接xiaozhi热点')
return
}
toast.error(t('deviceConfig.connectXiaozhiHotspot'))
return
}
configuring.value = true
console.log('开始WiFi配网:', props.selectedNetwork.ssid)
console.log(t('deviceConfig.startWifiConfig') + ':', props.selectedNetwork.ssid)
try {
const response = await uni.request({
@@ -81,16 +82,16 @@ async function submitConfig() {
console.log('WiFi配网响应:', response)
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 {
const errorMsg = (response.data as any)?.error || '配网失败'
const errorMsg = (response.data as any)?.error || t('deviceConfig.configFailed')
toast.error(errorMsg)
}
}
catch (error) {
console.error('WiFi配网失败:', error)
toast.error('配网失败,请检查网络连接')
console.error(t('deviceConfig.wifiConfigFailed') + ':', error)
toast.error(`${t('deviceConfig.configFailed')}${t('deviceConfig.pleaseCheckNetworkConnection')}`)
}
finally {
configuring.value = false
@@ -104,14 +105,14 @@ async function submitConfig() {
<view v-if="props.selectedNetwork" class="selected-network">
<view class="network-info">
<view class="network-name">
选中网络: {{ props.selectedNetwork.ssid }}
{{ t('deviceConfig.selectedNetwork') }}: {{ props.selectedNetwork.ssid }}
</view>
<view class="network-details">
<text class="network-signal">
信号: {{ props.selectedNetwork.rssi }}dBm
{{ t('deviceConfig.signal') }}: {{ props.selectedNetwork.rssi }}dBm
</text>
<text class="network-security">
{{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }}
{{ props.selectedNetwork.authmode === 0 ? t('deviceConfig.openNetwork') : t('deviceConfig.encryptedNetwork') }}
</text>
</view>
</view>
@@ -127,30 +128,30 @@ async function submitConfig() {
:disabled="!canSubmit"
@click="submitConfig"
>
{{ configuring ? '配网中...' : '开始WiFi配网' }}
{{ configuring ? t('deviceConfig.configuring') : t('deviceConfig.startWifiConfigButton') }}
</wd-button>
</view>
<!-- 使用说明 -->
<view class="help-section">
<view class="help-title">
WiFi配网说明
</view>
<view class="help-title">
{{ t('deviceConfig.wifiConfigInstructions') }}
</view>
<view class="help-content">
<text class="help-item">
1. 手机连接xiaozhi热点 (xiaozhi-XXXXXX)
1. {{ t('deviceConfig.phoneConnectXiaozhiHotspot') }} (xiaozhi-XXXXXX)
</text>
<text class="help-item">
2. 选择要配网的目标WiFi网络
2. {{ t('deviceConfig.selectTargetWifiNetwork') }}
</text>
<text class="help-item">
3. 输入WiFi密码如果需要
3. {{ t('deviceConfig.enterWifiPasswordIfNeeded') }}
</text>
<text class="help-item">
4. 点击开始配网等待设备连接
4. {{ t('deviceConfig.clickStartConfigAndWait') }}
</text>
<text class="help-tip">
配网成功后设备会自动重启并连接目标WiFi
{{ t('deviceConfig.afterConfigSuccessDeviceWillRestart') }}
</text>
</view>
</view>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, defineEmits, defineExpose, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { t } from '@/i18n'
// 类型定义
interface WiFiNetwork {
@@ -40,7 +41,7 @@ const selectorExpanded = ref(false)
// 计算属性
const networkDisplayText = computed(() => {
if (!selectedNetwork.value)
return '请选择WiFi网络'
return t('deviceConfig.selectWifiNetwork')
return selectedNetwork.value.ssid
})
@@ -55,7 +56,7 @@ async function checkESP32Connection() {
})
isConnectedToESP32.value = response.statusCode === 200
emit('connection-status', isConnectedToESP32.value)
console.log('xiaozhi连接状态:', isConnectedToESP32.value)
console.log(`${t('deviceConfig.xiaozhi')}连接状态:`, isConnectedToESP32.value)
}
catch (error) {
isConnectedToESP32.value = false
@@ -70,9 +71,9 @@ async function checkESP32Connection() {
// 扫描WiFi网络
async function scanWifi() {
if (!isConnectedToESP32.value) {
toast.error('请先连接xiaozhi热点')
return
}
toast.error(t('deviceConfig.connectXiaozhiHotspot'))
return
}
scanning.value = true
console.log('开始扫描WiFi网络')
@@ -84,13 +85,13 @@ async function scanWifi() {
timeout: 10000,
})
console.log('WiFi扫描响应:', response)
console.log(t('deviceConfig.wifiScanResponse') + ':', response)
if (response.statusCode === 200 && response.data) {
const data = response.data as any
if (data.success && Array.isArray(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)) {
// 兼容旧格式
@@ -110,8 +111,8 @@ async function scanWifi() {
}
}
catch (error) {
console.error('WiFi扫描失败:', error)
toast.error('扫描失败,请检查xiaozhi连接')
console.error(t('deviceConfig.wifiScanFailed') + ':', error)
toast.error(t('deviceConfig.scanFailedCheckConnection'))
}
finally {
scanning.value = false
@@ -124,9 +125,9 @@ async function showNetworkSelector() {
await checkESP32Connection()
if (!isConnectedToESP32.value) {
toast.error('请先连接xiaozhi热点')
return
}
toast.error(t('deviceConfig.connectXiaozhiHotspot'))
return
}
selectorExpanded.value = true
@@ -172,12 +173,12 @@ function reset() {
// 获取信号强度描述
function getSignalStrength(rssi: number): string {
if (rssi >= -50)
return '信号强'
return t('deviceConfig.signalStrong')
if (rssi >= -60)
return '信号良好'
return t('deviceConfig.signalGood')
if (rssi >= -70)
return '信号一般'
return '信号弱'
return t('deviceConfig.signalFair')
return t('deviceConfig.signalWeak')
}
// 获取信号强度颜色
@@ -212,81 +213,81 @@ onMounted(() => {
<!-- Xiaozhi连接状态 -->
<view v-if="props.autoConnect" class="connection-status">
<view v-if="!isConnectedToESP32" class="status-warning">
<view class="status-content">
<text class="warning-text">
请先连接xiaozhi热点 (xiaozhi-XXXXXX)
</text>
<wd-button
size="small"
type="primary"
:loading="checkingConnection"
@click="checkESP32Connection"
>
{{ checkingConnection ? '检测中...' : '重新检测' }}
</wd-button>
<view class="status-content">
<text class="warning-text">
{{ t('deviceConfig.connectXiaozhiHotspot') }} (xiaozhi-XXXXXX)
</text>
<wd-button
size="small"
type="primary"
:loading="checkingConnection"
@click="checkESP32Connection"
>
{{ checkingConnection ? t('deviceConfig.checking') : t('deviceConfig.reCheck') }}
</wd-button>
</view>
</view>
</view>
<view v-else class="status-success">
<view class="status-content">
<text class="success-text">
已连接xiaozhi热点
</text>
<wd-button
size="small"
:loading="checkingConnection"
@click="checkESP32Connection"
>
{{ checkingConnection ? '检测中...' : '刷新状态' }}
</wd-button>
<view class="status-content">
<text class="success-text">
{{ t('deviceConfig.connectedXiaozhiHotspot') }}
</text>
<wd-button
size="small"
:loading="checkingConnection"
@click="checkESP32Connection"
>
{{ checkingConnection ? t('deviceConfig.checking') : t('deviceConfig.refreshStatus') }}
</wd-button>
</view>
</view>
</view>
</view>
<!-- WiFi网络选择器 -->
<view class="network-selector">
<view class="selector-item" @click="showNetworkSelector">
<text class="selector-label">
WiFi网络
</text>
<text class="selector-value">
{{ networkDisplayText }}
</text>
<wd-icon name="arrow-right" custom-class="arrow-icon" />
<view class="selector-item" @click="showNetworkSelector">
<text class="selector-label">
{{ t('deviceConfig.wifiNetwork') }}
</text>
<text class="selector-value">
{{ networkDisplayText }}
</text>
<wd-icon name="arrow-right" custom-class="arrow-icon" />
</view>
</view>
</view>
<!-- 展开的网络列表 -->
<view v-if="selectorExpanded" class="network-list-overlay">
<view class="network-list-container">
<view class="list-header">
<text class="list-title">
选择WiFi网络
</text>
<view class="list-actions">
<wd-button
type="primary"
size="small"
:loading="scanning"
@click="scanWifi"
>
{{ scanning ? '扫描中...' : '刷新扫描' }}
</wd-button>
<wd-button
size="small"
@click="selectorExpanded = false"
>
取消
</wd-button>
<text class="list-title">
{{ t('deviceConfig.selectWifiNetwork') }}
</text>
<view class="list-actions">
<wd-button
type="primary"
size="small"
:loading="scanning"
@click="scanWifi"
>
{{ scanning ? t('deviceConfig.scanning') : t('deviceConfig.refreshScan') }}
</wd-button>
<wd-button
size="small"
@click="selectorExpanded = false"
>
{{ t('deviceConfig.cancel') }}
</wd-button>
</view>
</view>
</view>
<view class="network-list">
<view v-if="wifiNetworks.length === 0 && !scanning" class="empty-state">
<text class="empty-text">
暂无WiFi网络
{{ t('deviceConfig.noWifiNetworks') }}
</text>
<text class="empty-tip">
请点击刷新扫描
{{ t('deviceConfig.clickRefreshScan') }}
</text>
</view>
@@ -303,16 +304,16 @@ onMounted(() => {
</view>
<view class="wifi-details">
<text class="wifi-signal">
信号: {{ network.rssi }}dBm
{{ t('deviceConfig.signal') }}: {{ network.rssi }}dBm
</text>
<text class="wifi-channel">
频道: {{ network.channel }}
{{ t('deviceConfig.channel') }}: {{ network.channel }}
</text>
</view>
</view>
<view class="wifi-security">
<text class="security-icon">
{{ network.authmode === 0 ? '开放' : '加密' }}
{{ network.authmode === 0 ? t('deviceConfig.open') : t('deviceConfig.encrypted') }}
</text>
</view>
</view>
@@ -324,17 +325,17 @@ onMounted(() => {
<!-- 密码输入 -->
<view v-if="selectedNetwork && selectedNetwork.authmode > 0" class="password-section">
<view class="password-item">
<text class="password-label">
网络密码
</text>
<wd-input
v-model="password"
placeholder="请输入WiFi密码"
show-password
clearable
@input="onPasswordChange"
/>
</view>
<text class="password-label">
{{ t('deviceConfig.networkPassword') }}
</text>
<wd-input
v-model="password"
:placeholder="t('deviceConfig.enterWifiPassword')"
show-password
clearable
@input="onPasswordChange"
/>
</view>
</view>
</view>
</template>
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { t } from '@/i18n'
import UltrasonicConfig from './components/ultrasonic-config.vue'
import WifiConfig from './components/wifi-config.vue'
import WifiSelector from './components/wifi-selector.vue'
@@ -33,11 +34,11 @@ const selectedWifiInfo = ref<{
// 配网模式选项
const configTypeOptions = [
{
name: 'WiFi配网',
name: t('deviceConfig.wifiConfig'),
value: 'wifi' as const,
},
// {
// name: '超声波配网',
// name: t('deviceConfig.ultrasonicConfig'),
// value: 'ultrasonic' as const,
// },
]
@@ -67,28 +68,36 @@ function onNetworkSelected(network: WiFiNetwork | null, password: string) {
function onConnectionStatusChange(connected: boolean) {
console.log('ESP32连接状态:', connected)
}
// 在组件挂载后设置导航栏标题
import { onMounted } from 'vue'
onMounted(() => {
uni.setNavigationBarTitle({
title: t('deviceConfig.pageTitle')
})
})
</script>
<template>
<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="pb-[20rpx] first:pt-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
配网方式
</text>
{{ t('deviceConfig.configMethod') }}
</text>
</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="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>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ configType === 'wifi' ? 'WiFi配网' : '超声波配网' }}
</text>
{{ t('deviceConfig.configMethod') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ configType === 'wifi' ? t('deviceConfig.wifiConfig') : t('deviceConfig.ultrasonicConfig') }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
</view>
@@ -96,8 +105,8 @@ function onConnectionStatusChange(connected: boolean) {
<!-- WiFi网络选择 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
网络配置
</text>
{{ t('deviceConfig.networkConfig') }}
</text>
</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);">
@@ -139,7 +148,7 @@ function onConnectionStatusChange(connected: boolean) {
<route lang="jsonc" type="page">
{
"style": {
"navigationBarTitleText": "设备配",
"navigationBarTitleText": "设备配",
"navigationStyle": "custom"
}
}
+34 -33
View File
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni'
import { bindDevice, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({
name: 'DeviceManage',
@@ -91,19 +92,19 @@ function getDeviceTypeName(boardKey: string): string {
// 格式化时间
function formatTime(timeStr: string) {
if (!timeStr)
return '从未连接'
return t('device.neverConnected')
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000)
return '刚刚'
return t('device.justNow')
if (diff < 3600000)
return `${Math.floor(diff / 60000)}分钟前`
return t('device.minutesAgo', { minutes: Math.floor(diff / 60000) })
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}小时前`
return t('device.hoursAgo', { hours: Math.floor(diff / 3600000) })
if (diff < 604800000)
return `${Math.floor(diff / 86400000)}天前`
return t('device.daysAgo', { days: Math.floor(diff / 86400000) })
return date.toLocaleDateString()
}
@@ -114,11 +115,11 @@ async function toggleAutoUpdate(device: Device) {
const newStatus = device.autoUpdate === 1 ? 0 : 1
await updateDeviceAutoUpdate(device.id, newStatus)
device.autoUpdate = newStatus
toast.success(newStatus === 1 ? 'OTA自动升级已开启' : 'OTA自动升级已关闭')
toast.success(newStatus === 1 ? t('device.otaAutoUpdateEnabled') : t('device.otaAutoUpdateDisabled'))
}
catch (error: any) {
console.error('更新设备OTA状态失败:', error)
toast.error('操作失败,请重试')
toast.error(t('device.operationFailed'))
}
}
@@ -127,21 +128,21 @@ async function handleUnbindDevice(device: Device) {
try {
await unbindDevice(device.id)
await loadDeviceList()
toast.success('设备已解绑')
toast.success(t('device.deviceUnbound'))
}
catch (error: any) {
console.error('解绑设备失败:', error)
toast.error('解绑失败,请重试')
toast.error(t('device.unbindFailed'))
}
}
// 确认解绑设备
function confirmUnbindDevice(device: Device) {
message.confirm({
title: '解绑设备',
msg: `确定要解绑设备 "${device.macAddress}" 吗?`,
confirmButtonText: '确定解绑',
cancelButtonText: '取消',
title: t('device.unbindDevice'),
msg: t('device.confirmUnbindDevice', { macAddress: device.macAddress }),
confirmButtonText: t('device.confirmUnbind'),
cancelButtonText: t('device.cancel'),
}).then(() => {
handleUnbindDevice(device)
}).catch(() => {
@@ -153,18 +154,18 @@ function confirmUnbindDevice(device: Device) {
async function handleBindDevice(code: string) {
try {
if (!currentAgentId.value) {
toast.error('请先选择智能体')
toast.error(t('device.pleaseSelectAgent'))
return
}
await bindDevice(currentAgentId.value, code.trim())
await loadDeviceList()
toast.success('设备绑定成功!')
toast.success(t('device.deviceBindSuccess'))
}
catch (error: any) {
console.error('绑定设备失败:', error)
const errorMessage = error?.message || '绑定失败,请检查验证码是否正确'
toast.error(errorMessage)
toast.error(errorMessage || t('device.bindFailed'))
}
}
@@ -172,12 +173,12 @@ async function handleBindDevice(code: string) {
function openBindDialog() {
message
.prompt({
title: '绑定设备',
inputPlaceholder: '请输入设备验证码',
title: t('device.bindDevice'),
inputPlaceholder: t('device.enterDeviceCode'),
inputValue: '',
inputPattern: /^\d{6}$/,
confirmButtonText: '立即绑定',
cancelButtonText: '取消',
confirmButtonText: t('device.bindNow'),
cancelButtonText: t('device.cancel'),
})
.then(async (result: any) => {
if (result.value && String(result.value).trim()) {
@@ -219,7 +220,7 @@ defineExpose({
<view v-if="loading && deviceList.length === 0" class="loading-container">
<wd-loading color="#336cff" />
<text class="loading-text">
加载中...
{{ t('device.loading') }}
</text>
</view>
@@ -240,19 +241,19 @@ defineExpose({
<view class="mb-[20rpx]">
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
MAC地址{{ device.macAddress }}
</text>
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
固件版本{{ device.appVersion }}
</text>
<text class="block text-[28rpx] text-[#65686f] leading-[1.4]">
最近对话{{ formatTime(device.lastConnectedAt) }}
</text>
{{ t('device.macAddress') }}{{ device.macAddress }}
</text>
<text class="mb-[12rpx] block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.firmwareVersion') }}{{ device.appVersion }}
</text>
<text class="block text-[28rpx] text-[#65686f] leading-[1.4]">
{{ t('device.lastConnection') }}{{ formatTime(device.lastConnectedAt) }}
</text>
</view>
<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">
OTA升级
{{ t('device.otaUpdate') }}
</text>
<wd-switch
:model-value="device.autoUpdate === 1"
@@ -271,7 +272,7 @@ defineExpose({
@click.stop="confirmUnbindDevice(device)"
>
<wd-icon name="delete" />
<text>解绑</text>
<text>{{ t('device.unbind') }}</text>
</view>
</view>
</template>
@@ -285,10 +286,10 @@ defineExpose({
<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]" />
<text class="mb-[16rpx] text-[32rpx] text-[#666666] font-medium">
暂无设备
{{ t('device.noDevice') }}
</text>
<text class="text-[26rpx] text-[#999999] leading-[1.5]">
点击右下角 + 号绑定您的第一个设备
{{ t('device.clickToBindFirstDevice') }}
</text>
</view>
</view>
+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 { createAgent, deleteAgent, getAgentList } from '@/api/agent/agent'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({
name: 'Home',
@@ -78,11 +79,11 @@ async function handleCreateAgent(agentName: string) {
await createAgent({ agentName: agentName.trim() })
// 创建成功后刷新列表
pagingRef.value.reload()
toast.success(`智能体"${agentName}"创建成功!`)
toast.success(`${t('home.agentName')}"${agentName}"${t('message.saveSuccess')}`)
}
catch (error: any) {
console.error('创建智能体失败:', error)
const errorMessage = error?.message || '创建失败,请重试'
const errorMessage = error?.message || t('message.saveFail')
toast.error(errorMessage)
}
}
@@ -93,11 +94,11 @@ async function handleDeleteAgent(agent: Agent) {
await deleteAgent(agent.id)
// 删除成功后刷新列表
pagingRef.value.reload()
toast.success(`智能体"${agent.agentName}"已删除`)
toast.success(`${t('home.agentName')}${t('message.deleteSuccess')}`)
}
catch (error: any) {
console.error('删除智能体失败:', error)
const errorMessage = error?.message || '删除失败,请重试'
const errorMessage = error?.message || t('message.deleteFail')
toast.error(errorMessage)
}
}
@@ -119,13 +120,14 @@ function handleCardClick(agent: Agent) {
function openCreateDialog() {
message
.prompt({
title: '创建智能体',
title: t('home.dialogTitle'),
msg: '',
inputPlaceholder: '例如:客服助手、语音助理、知识问答',
inputPlaceholder: t('home.inputPlaceholder'),
inputValue: '',
inputPattern: /^[\u4E00-\u9FA5a-z0-9\s]{1,50}$/i,
confirmButtonText: '立即创建',
cancelButtonText: '取消',
inputError: t('home.createError'),
confirmButtonText: t('home.createNow'),
cancelButtonText: t('common.cancel'),
})
.then(async (result: any) => {
if (result.value && String(result.value).trim()) {
@@ -144,12 +146,12 @@ function formatTime(timeStr: string) {
const diff = now.getTime() - date.getTime()
if (diff < 60000)
return '刚刚'
return t('home.justNow')
if (diff < 3600000)
return `${Math.floor(diff / 60000)}分钟前`
return `${Math.floor(diff / 60000)}${t('home.minutesAgo')}`
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}小时前`
return `${Math.floor(diff / 86400000)}天前`
return `${Math.floor(diff / 3600000)}${t('home.hoursAgo')}`
return `${Math.floor(diff / 86400000)}${t('home.daysAgo')}`
}
// 页面显示时刷新列表
@@ -159,12 +161,20 @@ onShow(() => {
pagingRef.value.reload()
}
})
// 在组件挂载后设置导航栏标题
import { onMounted } from 'vue'
onMounted(() => {
uni.setNavigationBarTitle({
title: t('home.pageTitle')
})
})
</script>
<template>
<z-paging
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="{
backgroundColor: '#fff',
borderRadius: '50%',
@@ -178,16 +188,13 @@ onShow(() => {
<view class="banner-content">
<view class="welcome-info">
<text class="greeting">
你好小智
{{ t('home.greeting') }}
</text>
<text class="subtitle">
让我们度过 <text class="highlight">
美好的一天
{{ t('home.subtitle') }} <text class="highlight">
{{ t('home.wonderfulDay') }}
</text>
</text>
<text class="english-subtitle">
Hello, Let's have a wonderful day!
</text>
</view>
<view class="wave-decoration">
<!-- 添加波浪装饰 -->
@@ -227,13 +234,13 @@ onShow(() => {
<view class="stat-chip">
<wd-icon name="phone" custom-class="chip-icon" />
<text class="chip-text">
设备管理({{ agent.deviceCount }})
{{ t('home.deviceManagement') }}({{ agent.deviceCount }})
</text>
</view>
<view v-if="agent.lastConnectedAt" class="stat-chip">
<wd-icon name="time" custom-class="chip-icon" />
<text class="chip-text">
最近对话{{ formatTime(agent.lastConnectedAt) }}
{{ t('home.lastConversation') }}{{ formatTime(agent.lastConnectedAt) }}
</text>
</view>
</view>
@@ -247,7 +254,7 @@ onShow(() => {
<view class="swipe-actions">
<view class="action-btn delete-btn" @click.stop="handleDeleteAgent(agent)">
<wd-icon name="delete" />
<text>删除</text>
<text>{{ t('home.delete') }}</text>
</view>
</view>
</template>
@@ -260,10 +267,10 @@ onShow(() => {
<view class="empty-state">
<wd-icon name="robot" custom-class="empty-icon" />
<text class="empty-text">
暂无智能体
{{ t('home.emptyState') }}
</text>
<text class="empty-desc">
点击右下角 + 号创建您的第一个智能体
{{ t('home.createFirstAgent') }}
</text>
</view>
</template>
+186 -47
View File
@@ -3,7 +3,7 @@
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "登陆"
"navigationBarTitleText": "Login"
}
}
</route>
@@ -15,6 +15,11 @@ import { login } from '@/api/auth'
import { useConfigStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
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
@@ -39,7 +44,7 @@ systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
// 表单数据
const formData = ref<LoginData>({
const formData = ref({
username: '',
password: '',
captcha: '',
@@ -73,6 +78,11 @@ const areaCodeList = computed(() => {
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
})
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey
})
// 切换登录方式
function toggleLoginType() {
loginType.value = loginType.value === 'username' ? 'mobile' : 'username'
@@ -136,40 +146,64 @@ async function handleLogin() {
// 表单验证
if (loginType.value === 'username') {
if (!formData.value.username) {
toast.warning('请输入用户名')
toast.warning(t('login.enterUsername'))
return
}
}
else {
if (!formData.value.mobile) {
toast.warning('请输入手机号')
toast.warning(t('login.enterPhone'))
return
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning('请输入正确的手机号')
toast.warning(t('login.enterPhone'))
return
}
}
if (!formData.value.password) {
toast.warning('请输入密码')
toast.warning(t('login.enterPassword'))
return
}
if (!formData.value.captcha) {
toast.warning('请输入验证码')
toast.warning(t('login.enterCaptcha'))
return
}
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
toast.warning(t('sm2.publicKeyNotConfigured'))
return
}
try {
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字段
if (loginType.value === 'mobile') {
loginData.username = `${selectedAreaCode.value}${formData.value.mobile}`
} else {
loginData.username = formData.value.username
}
const response = await login(loginData)
@@ -177,7 +211,7 @@ async function handleLogin() {
uni.setStorageSync('token', response.token)
uni.setStorageSync('expire', response.expire)
toast.success('登录成功')
toast.success(t('message.loginSuccess'))
// 跳转到主页
setTimeout(() => {
@@ -189,6 +223,14 @@ async function handleLogin() {
catch (error: any) {
// 登录失败重新获取验证码
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 {
loading.value = false
@@ -200,14 +242,26 @@ onLoad(() => {
refreshCaptcha()
})
// 语言切换相关
const showLanguageSheet = ref(false)
const supportedLanguages = getSupportedLanguages()
// 初始化国际化
initI18n()
// 切换语言
function handleLanguageChange(lang: Language) {
changeLanguage(lang)
showLanguageSheet.value = false
}
// 组件挂载时确保配置已加载
onMounted(async () => {
if (!configStore.config.name) {
try {
await configStore.fetchPublicConfig()
}
catch (error) {
console.error('获取配置失败:', error)
} catch (error) {
console.error(t('login.fetchConfigError'), error)
}
}
})
@@ -219,21 +273,25 @@ onMounted(async () => {
<view class="logo-section">
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
<text class="welcome-text">
欢迎回来
{{ t('login.welcomeBack') }}
</text>
<text class="subtitle">
请登录您的账户
{{ t('login.pleaseLogin') }}
</text>
</view>
</view>
<!-- 右上角服务端设置按钮 -->
<view
class="server-btn"
:style="{ top: `${safeAreaInsets?.top + 10}px` }"
@click="goToServerSetting"
>
<wd-icon name="setting" custom-class="server-icon" />
<!-- 右上角按钮 -->
<view class="top-right-buttons" :style="{ top: `${safeAreaInsets?.top + 10}px` }">
<!-- 语言切换按钮 -->
<view class="lang-btn" @click="showLanguageSheet = true">
<text class="lang-text-icon">{{ t('login.selectLanguageTip') }}</text>
</view>
<!-- 服务端设置按钮 -->
<view class="server-btn" @click="goToServerSetting">
<wd-icon name="setting" custom-class="server-icon" />
</view>
</view>
<view class="form-container">
@@ -253,7 +311,7 @@ onMounted(async () => {
v-model="formData.mobile"
custom-class="styled-input"
no-border
placeholder="请输入手机号码"
:placeholder="t('login.enterPhone')"
type="number"
:maxlength="11"
/>
@@ -270,7 +328,7 @@ onMounted(async () => {
v-model="formData.username"
custom-class="styled-input"
no-border
placeholder="请输入用户名"
:placeholder="t('login.enterUsername')"
/>
</view>
</view>
@@ -282,7 +340,7 @@ onMounted(async () => {
v-model="formData.password"
custom-class="styled-input"
no-border
placeholder="请输入密码"
:placeholder="t('login.enterPassword')"
clearable
show-password
:maxlength="20"
@@ -296,7 +354,7 @@ onMounted(async () => {
v-model="formData.captcha"
custom-class="styled-input"
no-border
placeholder="请输入验证码"
:placeholder="t('login.enterCaptcha')"
:maxlength="6"
/>
<view class="captcha-image" @click="refreshCaptcha">
@@ -304,26 +362,19 @@ onMounted(async () => {
</view>
</view>
</view>
<view class="forgot-password">
<text class="forgot-text">
忘记密码
</text>
</view>
<view
class="login-btn"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
{{ loading ? t('login.loggingIn') : t('login.loginButton') }}
</view>
<view class="register-hint">
<text class="hint-text">
还没有账户
{{ t('login.noAccount') }}
</text>
<text class="register-link" @click="goToRegister">
立即注册
{{ t('login.registerNow') }}
</text>
</view>
@@ -352,7 +403,7 @@ onMounted(async () => {
<!-- 区号选择弹窗 -->
<wd-action-sheet
v-model="showAreaCodeSheet"
title="选择国家/地区"
:title="t('login.selectCountry')"
:close-on-click-modal="true"
@close="closeAreaCodeSheet"
>
@@ -386,11 +437,33 @@ onMounted(async () => {
custom-class="confirm-btn"
@click="closeAreaCodeSheet"
>
确认
{{ t('login.confirm') }}
</wd-button>
</view>
</view>
</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>
</template>
@@ -498,7 +571,7 @@ onMounted(async () => {
&.captcha-wrapper {
.captcha-image {
margin-left: 20rpx;
width: 120rpx;
width: 150rpx;
height: 60rpx;
border-radius: 8rpx;
overflow: hidden;
@@ -793,20 +866,52 @@ onMounted(async () => {
}
}
}
.server-btn {
// 右上角按钮组
.top-right-buttons {
position: absolute;
right: 20rpx; // 距离右边距
top: 40rpx; // 顶部稍微下移,不贴状态栏
right: 20rpx;
display: flex;
gap: 20rpx;
z-index: 999;
}
// 语言切换按钮
.lang-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
cursor: pointer;
background: rgba(255, 255, 255, 0.15); // 半透明背景,更好看
border-radius: 24rpx; // 圆形按钮
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2); // 阴影
background: rgba(255, 255, 255, 0.15);
border-radius: 24rpx;
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 {
transform: scale(0.95);
@@ -814,11 +919,45 @@ onMounted(async () => {
.server-icon {
font-size: 28rpx;
color: #FFFFFF; // 白色图标
color: #FFFFFF;
}
&: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>
+126 -96
View File
@@ -14,6 +14,10 @@ import { register, sendSmsCode } from '@/api/auth'
import { useConfigStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
// 导入国际化相关功能
import { t, initI18n } from '@/i18n'
// 导入SM2加密工具
import { sm2Encrypt } from '@/utils'
// 获取屏幕边界到安全区域距离
let safeAreaInsets
@@ -88,6 +92,11 @@ const areaCodeList = computed(() => {
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
})
// SM2公钥
const sm2PublicKey = computed(() => {
return configStore.config.sm2PublicKey
})
// 切换注册方式
function toggleRegisterType() {
registerType.value = registerType.value === 'username' ? 'mobile' : 'username'
@@ -134,18 +143,18 @@ async function refreshCaptcha() {
// 发送短信验证码
async function sendSmsVerification() {
if (!formData.value.mobile) {
toast.warning('请输入手机号')
toast.warning(t('register.enterPhone'))
return
}
if (!formData.value.captcha) {
toast.warning('请输入图形验证码')
toast.warning(t('register.enterCode'))
return
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning('请输入正确的手机号')
toast.warning(t('register.enterPhone'))
return
}
@@ -157,7 +166,7 @@ async function sendSmsVerification() {
captchaId: formData.value.captchaId,
})
toast.success('验证码发送成功')
toast.success(t('register.captchaSendSuccess'))
// 开始倒计时
smsCountdown.value = 60
@@ -169,6 +178,10 @@ async function sendSmsVerification() {
}, 1000)
}
catch (error: any) {
// 处理验证码错误 - 从error.message中解析错误码
if (error.message.includes('请求错误[10067]')) {
toast.warning(t('login.captchaError'))
}
// 发送失败重新获取图形验证码
refreshCaptcha()
}
@@ -180,58 +193,76 @@ async function sendSmsVerification() {
// 注册
async function handleRegister() {
// 表单验证
if (registerType.value === 'username') {
if (!formData.value.username) {
toast.warning('请输入用户名')
return
}
}
else {
if (enableMobileRegister.value) {
// 手机号注册验证
if (!formData.value.mobile) {
toast.warning('请输入手机号')
toast.warning(t('register.enterPhone'))
return
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) {
toast.warning('请输入正确的手机号')
toast.warning(t('register.enterPhone'))
return
}
if (!formData.value.mobileCaptcha) {
toast.warning('请输入短信验证码')
toast.warning(t('register.enterCode'))
return
}
}
else {
// 用户名注册验证
if (!formData.value.username) {
toast.warning(t('register.enterUsername'))
return
}
}
if (!formData.value.password) {
toast.warning('请输入密码')
toast.warning(t('register.enterPassword'))
return
}
if (!formData.value.confirmPassword) {
toast.warning('请确认密码')
toast.warning(t('register.confirmPassword'))
return
}
if (formData.value.password !== formData.value.confirmPassword) {
toast.warning('两次输入的密码不一致')
toast.warning(t('register.confirmPassword'))
return
}
if (!formData.value.captcha) {
toast.warning('请输入验证码')
toast.warning(t('register.enterCode'))
return
}
// 检查SM2公钥是否配置
if (!sm2PublicKey.value) {
toast.warning(t('sm2.publicKeyNotConfigured'))
return
}
try {
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 = {
username: registerType.value === 'mobile' ? `${selectedAreaCode.value}${formData.value.mobile}` : formData.value.username,
password: formData.value.password,
confirmPassword: formData.value.confirmPassword,
captcha: formData.value.captcha,
username: enableMobileRegister.value ? `${selectedAreaCode.value}${formData.value.mobile}` : formData.value.username,
password: encryptedPassword,
captchaId: formData.value.captchaId,
areaCode: formData.value.areaCode,
mobile: formData.value.mobile,
@@ -239,14 +270,24 @@ async function handleRegister() {
}
await register(registerData)
toast.success('注册成功')
toast.success(t('message.registerSuccess'))
// 跳转到登录页
setTimeout(() => {
uni.navigateBack()
uni.redirectTo({
url: '/pages/login/index'
})
}, 1000)
}
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()
}
@@ -257,7 +298,9 @@ async function handleRegister() {
// 返回登录
function goBack() {
uni.navigateBack()
uni.redirectTo({
url: '/pages/login/index'
})
}
// 页面加载时获取验证码
@@ -275,7 +318,11 @@ onMounted(async () => {
console.error('获取配置失败:', error)
}
}
// 初始化国际化
initI18n()
})
</script>
<template>
@@ -287,10 +334,10 @@ onMounted(async () => {
<view class="logo-section">
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
<text class="welcome-text">
欢迎注册
{{ t('register.pageTitle') }}
</text>
<text class="subtitle">
创建您的新账户
{{ t('register.createAccount') }}
</text>
</view>
</view>
@@ -298,7 +345,7 @@ onMounted(async () => {
<view class="form-container">
<view class="form">
<!-- 手机号注册 -->
<template v-if="registerType === 'mobile'">
<template v-if="enableMobileRegister">
<view class="input-group">
<view class="input-wrapper mobile-wrapper">
<view class="area-code-selector" @click="openAreaCodeSheet">
@@ -312,7 +359,7 @@ onMounted(async () => {
v-model="formData.mobile"
custom-class="styled-input"
no-border
placeholder="请输入手机号码"
:placeholder="t('register.enterPhone')"
type="number"
:maxlength="11"
/>
@@ -329,7 +376,7 @@ onMounted(async () => {
v-model="formData.username"
custom-class="styled-input"
no-border
placeholder="请输入用户名"
:placeholder="t('register.enterUsername')"
/>
</view>
</view>
@@ -338,63 +385,63 @@ onMounted(async () => {
<view class="input-group">
<view class="input-wrapper">
<wd-input
v-model="formData.password"
custom-class="styled-input"
no-border
placeholder="请输入密码"
show-password
:maxlength="20"
/>
</view>
v-model="formData.password"
custom-class="styled-input"
no-border
:placeholder="t('register.enterPassword')"
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="请确认密码"
show-password
:maxlength="20"
/>
</view>
v-model="formData.confirmPassword"
custom-class="styled-input"
no-border
:placeholder="t('register.confirmPassword')"
show-password
:maxlength="20"
/>
</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="请输入验证码"
:maxlength="6"
/>
<view class="captcha-image" @click="refreshCaptcha">
<image :src="captchaImage" class="captcha-img" />
</view>
v-model="formData.captcha"
custom-class="styled-input"
no-border
:placeholder="t('register.enterCode')"
:maxlength="6"
/>
<view class="captcha-image" @click="refreshCaptcha">
<image :src="captchaImage" class="captcha-img" />
</view>
</view>
</view>
<!-- 手机验证码输入框 -->
<view v-if="registerType === 'mobile'" class="input-group">
<view v-if="enableMobileRegister" class="input-group">
<view class="input-wrapper sms-wrapper">
<wd-input
v-model="formData.mobileCaptcha"
custom-class="styled-input"
no-border
placeholder="请输入短信验证码"
type="number"
:maxlength="6"
/>
<wd-button
:loading="smsLoading"
:disabled="smsCountdown > 0"
custom-class="sms-btn"
@click="sendSmsVerification"
>
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</wd-button>
v-model="formData.mobileCaptcha"
custom-class="styled-input"
no-border
:placeholder="t('register.enterCode')"
type="number"
:maxlength="6"
/>
<wd-button
:loading="smsLoading"
:disabled="smsCountdown > 0"
custom-class="sms-btn"
@click="sendSmsVerification"
>
{{ smsCountdown > 0 ? `${smsCountdown}s` : t('register.getCode') }}
</wd-button>
</view>
</view>
@@ -404,44 +451,25 @@ onMounted(async () => {
:loading="loading"
@click="handleRegister"
>
{{ loading ? '注册中...' : '注册' }}
{{ loading ? t('register.registering') : t('register.registerButton') }}
</view>
<view class="login-hint">
<text class="hint-text">
已有账户
{{ t('register.haveAccount') }}
</text>
<text class="login-link" @click="goBack">
立即登录
{{ t('register.loginNow') }}
</text>
</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>
<!-- 区号选择弹窗 -->
<wd-action-sheet
v-model="showAreaCodeSheet"
title="选择国家/地区"
:title="t('register.selectCountry')"
:close-on-click-modal="true"
@close="closeAreaCodeSheet"
>
@@ -475,11 +503,13 @@ onMounted(async () => {
custom-class="confirm-btn"
@click="closeAreaCodeSheet"
>
确认
{{ t('register.confirm') }}
</wd-button>
</view>
</view>
</wd-action-sheet>
</view>
</template>
@@ -612,7 +642,7 @@ onMounted(async () => {
&.captcha-wrapper {
.captcha-image {
margin-left: 20rpx;
width: 120rpx;
width: 150rpx;
height: 60rpx;
border-radius: 8rpx;
overflow: hidden;
+163 -77
View File
@@ -11,6 +11,8 @@ import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, se
import { isMp } from '@/utils/platform'
import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
import type { Language } from '@/store/lang'
defineOptions({
name: 'SettingsPage',
@@ -56,13 +58,13 @@ function getCacheInfo() {
// 验证URL格式
function validateUrl() {
urlError.value = ''
if (!baseUrlInput.value) {
return
}
if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
urlError.value = '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)'
urlError.value = t('settings.validServerUrl')
}
}
@@ -70,7 +72,7 @@ function validateUrl() {
async function testServerBaseUrl() {
// 先清除错误信息
urlError.value = ''
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
return false
}
@@ -85,12 +87,12 @@ async function testServerBaseUrl() {
if (response.statusCode === 200) {
return true
} else {
toast.error('无效地址,请检查服务端是否启动或网络连接是否正常')
toast.error(t('message.invalidAddress'))
return false
}
} catch (error) {
console.error('测试服务端地址失败:', error)
toast.error('无效地址,请检查服务端是否启动或网络连接是否正常')
toast.error(t('message.invalidAddress'))
return false
}
}
@@ -98,7 +100,7 @@ async function testServerBaseUrl() {
// 保存服务端地址
async function saveServerBaseUrl() {
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
toast.warning('请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)')
toast.warning(t('settings.validServerUrl'))
return
}
@@ -113,20 +115,31 @@ async function saveServerBaseUrl() {
clearAllCacheAfterUrlChange()
uni.showModal({
title: '重启应用',
content: '服务端地址已保存并清空缓存,是否立即重启生效?',
confirmText: '立即重启',
cancelText: '稍后',
success: (res) => {
if (res.confirm) {
restartApp()
}
else {
toast.success('已保存,可稍后手动重启应用')
}
},
})
}
title: t('settings.restartApp'),
content: t('settings.serverUrlSavedAndCacheCleared'),
confirmText: t('settings.restartNow'),
cancelText: t('settings.restartLater'),
success: (res) => {
if (res.confirm) {
restartApp()
}
else {
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 默认
function resetServerBaseUrl() {
@@ -137,16 +150,16 @@ function resetServerBaseUrl() {
clearAllCacheAfterUrlChange()
uni.showModal({
title: '重启应用',
content: '已重置为默认地址并清空缓存,是否立即重启生效?',
confirmText: '立即重启',
cancelText: '稍后',
title: t('settings.restartApp'),
content: t('settings.resetToDefaultAndCacheCleared'),
confirmText: t('settings.restartNow'),
cancelText: t('settings.restartLater'),
success: (res) => {
if (res.confirm) {
restartApp()
}
else {
toast.success('已重置,可稍后手动重启应用')
toast.success(t('settings.resetSuccess'))
}
},
})
@@ -185,8 +198,7 @@ function clearAllCacheAfterUrlChange() {
// 重新获取缓存信息
getCacheInfo()
}
catch (error) {
} catch (error) {
console.error('清除缓存失败:', error)
}
}
@@ -195,12 +207,14 @@ function clearAllCacheAfterUrlChange() {
async function clearCache() {
try {
uni.showModal({
title: '确认清除',
content: '确定要清除所有缓存吗?这将删除所有数据包括登录状态,需要重新登录。',
title: t('settings.confirmClear'),
content: t('settings.confirmClearMessage'),
confirmText: t('common.confirm'),
cancelText: t('common.cancel'),
success: (res) => {
if (res.confirm) {
clearAllCacheAfterUrlChange()
toast.success('缓存清除成功,即将跳转到登录页')
toast.success(t('settings.cacheCleared'))
// 延迟跳转到登录页
setTimeout(() => {
@@ -209,22 +223,22 @@ async function clearCache() {
}
},
})
}
catch (error) {
} catch (error) {
console.error('清除缓存失败:', error)
toast.error('清除缓存失败')
toast.error(t('settings.clearCacheFailed'))
}
}
// 关于我们
function showAbout() {
uni.showModal({
title: `关于${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`,
title: `关于小智智控台`,
content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.7.6`,
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE,
version: '0.8.3'
}),
showCancel: false,
confirmText: '确定',
confirmText: t('common.confirm'),
})
}
@@ -234,19 +248,24 @@ onMounted(async () => {
loadServerBaseUrl()
}
getCacheInfo()
// 动态设置导航栏标题为国际化文本
uni.setNavigationBarTitle({
title: t('settings.title')
})
})
</script>
<template>
<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 v-if="!isMp" class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold">
网络设置
{{ t('settings.networkSettings') }}
</text>
</view>
@@ -254,26 +273,19 @@ onMounted(async () => {
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view class="mb-[24rpx]">
<text class="text-[28rpx] text-[#232338] font-semibold">
服务端接口地址
{{ t('settings.serverApiUrl') }}
</text>
<text class="mt-[8rpx] block text-[24rpx] text-[#9d9ea3]">
修改后将自动清空缓存并重启应用
{{ t('settings.modifyWillClearCache') }}
</text>
</view>
<view class="mb-[24rpx]">
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
<wd-input
v-model="baseUrlInput"
type="text"
clearable
:maxlength="200"
placeholder="输入服务端地址,如 https://example.com/xiaozhi"
custom-class="!border-none !bg-transparent h-[88rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]"
@input="validateUrl"
@blur="validateUrl"
/>
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
:placeholder="t('settings.enterServerUrl')"
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
</view>
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
{{ urlError }}
@@ -284,12 +296,12 @@ onMounted(async () => {
<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"
@click="saveServerBaseUrl">
保存设置
{{ t('settings.saveSettings') }}
</wd-button>
<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]"
@click="resetServerBaseUrl">
恢复默认
{{ t('settings.resetDefault') }}
</wd-button>
</view>
</view>
@@ -299,7 +311,7 @@ onMounted(async () => {
<view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold">
缓存管理
{{ t('settings.cacheManagement') }}
</text>
</view>
@@ -311,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]">
<view>
<text class="text-[28rpx] text-[#232338] font-medium">
总缓存大小
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
应用数据总大小
</text>
{{ t('settings.totalCacheSize') }}
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.appDataSize') }}
</text>
</view>
<text class="text-[28rpx] text-[#65686f] font-semibold">
{{ cacheInfo.storageSize }}
@@ -327,16 +339,16 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
<view>
<text class="text-[28rpx] text-[#232338] font-medium">
缓存清理
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
清空所有缓存数据
</text>
{{ t('settings.cacheClear') }}
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.clearAllCache') }}
</text>
</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"
@click="clearCache">
清除缓存
{{ t('settings.clearCache') }}
</view>
</view>
</view>
@@ -347,8 +359,8 @@ onMounted(async () => {
<view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold">
应用信息
</text>
{{ t('settings.appInfo') }}
</text>
</view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
@@ -358,23 +370,97 @@ onMounted(async () => {
@click="showAbout">
<view>
<text class="text-[28rpx] text-[#232338] font-medium">
关于我们
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
应用版本与团队信息
</text>
{{ t('settings.aboutUs') }}
</text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.appVersion') }}
</text>
</view>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</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);" />
</view>
</view>
</template>
<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 { useToast } from 'wot-design-uni/components/wd-toast'
import { createVoicePrint, deleteVoicePrint, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint'
import { t } from '@/i18n'
defineOptions({
name: 'VoicePrintManage',
@@ -81,7 +82,7 @@ async function loadVoicePrintList() {
// 检查是否有当前选中的智能体
if (!currentAgentId.value) {
console.warn('没有选中的智能体')
console.warn(t('voiceprint.noSelectedAgent'))
voicePrintList.value = []
return
}
@@ -117,7 +118,7 @@ async function refresh() {
async function loadChatHistory() {
try {
if (!currentAgentId.value) {
toast.error('请先选择智能体')
toast.error(t('voiceprint.pleaseSelectAgent'))
return
}
@@ -133,24 +134,47 @@ async function loadChatHistory() {
}
catch (error) {
console.error('获取对话记录失败:', error)
toast.error('获取对话记录失败')
toast.error(t('voiceprint.fetchHistoryFailed'))
}
}
// 打开添加弹窗
function openAddDialog() {
if (!currentAgentId.value) {
toast.error('请先选择智能体')
toast.error(t('voiceprint.pleaseSelectAgent'))
return
}
addForm.value = {
agentId: currentAgentId.value,
audioId: '',
sourceName: '',
introduce: '',
// 检查声纹接口是否配置(通过尝试获取声纹列表来检测)
const checkVoicePrintConfig = async () => {
try {
await getVoicePrintList(currentAgentId.value)
// 接口正常,继续打开添加弹窗
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) {
if (!audioId)
return '点击选择声纹向量'
return t('voiceprint.clickToSelectVector')
const chatItem = chatHistoryList.value.find(item => item.audioId === audioId)
return chatItem ? chatItem.content : `已选择: ${audioId.substring(0, 8)}...`
}
@@ -181,34 +205,34 @@ function selectAudioId({ item }: { item: any }) {
// 提交添加说话人
async function submitAdd() {
if (!addForm.value.sourceName.trim()) {
toast.error('请输入姓名')
toast.error(t('voiceprint.pleaseInputName'))
return
}
if (!addForm.value.audioId) {
toast.error('请选择声纹向量')
toast.error(t('voiceprint.pleaseSelectVector'))
return
}
try {
await createVoicePrint(addForm.value)
toast.success('添加成功')
toast.success(t('voiceprint.addSuccess'))
showAddDialog.value = false
await loadVoicePrintList()
}
catch (error) {
console.error('添加说话人失败:', error)
toast.error('添加说话人失败')
toast.error(t('voiceprint.addFailed'))
}
}
// 提交编辑说话人
async function submitEdit() {
if (!editForm.value.sourceName.trim()) {
toast.error('请输入姓名')
toast.error(t('voiceprint.pleaseInputName'))
return
}
if (!editForm.value.audioId) {
toast.error('请选择声纹向量')
toast.error(t('voiceprint.pleaseSelectVector'))
return
}
@@ -220,13 +244,13 @@ async function submitEdit() {
introduce: editForm.value.introduce,
createDate: editForm.value.createDate,
})
toast.success('编辑成功')
toast.success(t('voiceprint.editSuccess'))
showEditDialog.value = false
await loadVoicePrintList()
}
catch (error) {
console.error('编辑说话人失败:', error)
toast.error('编辑说话人失败')
toast.error(t('voiceprint.editFailed'))
}
}
@@ -239,11 +263,11 @@ function handleEdit(item: VoicePrint) {
// 删除声纹
async function handleDelete(id: string) {
message.confirm({
msg: '确定要删除这个说话人吗?',
title: '确认删除',
msg: t('voiceprint.deleteConfirmMsg'),
title: t('voiceprint.deleteConfirmTitle'),
}).then(async () => {
await deleteVoicePrint(id)
toast.success('删除成功')
toast.success(t('voiceprint.deleteSuccess'))
await loadVoicePrintList()
}).catch(() => {
console.log('点击了取消按钮')
@@ -268,7 +292,7 @@ defineExpose({
<view v-if="loading && voicePrintList.length === 0" class="loading-container">
<wd-loading color="#336cff" />
<text class="loading-text">
加载中...
{{ t('voiceprint.loading') }}
</text>
</view>
@@ -302,7 +326,7 @@ defineExpose({
@click="handleDelete(item.id)"
>
<wd-icon name="delete" />
删除
{{ t('voiceprint.delete') }}
</view>
</view>
</template>
@@ -315,11 +339,11 @@ defineExpose({
<view v-else-if="!loading" class="empty-container">
<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]" />
<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 class="text-[26rpx] text-[#999999] leading-[1.5]">
点击右下角 + 号添加您的第一个说话人
{{ t('voiceprint.emptyDesc') }}
</text>
</view>
</view>
@@ -341,7 +365,7 @@ defineExpose({
<view>
<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">
添加说话人
{{ t('voiceprint.addSpeaker') }}
</text>
</view>
@@ -349,7 +373,7 @@ defineExpose({
<!-- 声纹向量选择 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 声纹向量
* {{ t('voiceprint.voiceVector') }}
</text>
<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]"
@@ -368,22 +392,22 @@ defineExpose({
<!-- 姓名 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 姓名
* {{ t('voiceprint.name') }}
</text>
<input
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]"
type="text" placeholder="请输入姓名"
type="text" :placeholder="t('voiceprint.pleaseInputName')"
>
</view>
<!-- 描述 -->
<view>
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 描述
* {{ t('voiceprint.description') }}
</text>
<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]"
/>
<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]">
<wd-button type="info" custom-class="flex-1" @click="showAddDialog = false">
取消
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitAdd">
保存
</wd-button>
{{ t('voiceprint.cancel') }}
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitAdd">
{{ t('voiceprint.save') }}
</wd-button>
</view>
</view>
</wd-popup>
@@ -411,7 +435,7 @@ defineExpose({
<view>
<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">
编辑说话人
{{ t('voiceprint.editSpeaker') }}
</text>
</view>
@@ -419,7 +443,7 @@ defineExpose({
<!-- 声纹向量选择 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 声纹向量
* {{ t('voiceprint.voiceVector') }}
</text>
<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]"
@@ -438,22 +462,22 @@ defineExpose({
<!-- 姓名 -->
<view class="mb-[32rpx]">
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 姓名
* {{ t('voiceprint.name') }}
</text>
<input
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]"
type="text" placeholder="请输入姓名"
type="text" :placeholder="t('voiceprint.pleaseInputName')"
>
</view>
<!-- 描述 -->
<view>
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
* 描述
* {{ t('voiceprint.description') }}
</text>
<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]"
/>
<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]">
<wd-button type="info" custom-class="flex-1" @click="showEditDialog = false">
取消
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitEdit">
保存
</wd-button>
{{ t('voiceprint.cancel') }}
</wd-button>
<wd-button type="primary" custom-class="flex-1" @click="submitEdit">
{{ t('voiceprint.save') }}
</wd-button>
</view>
</view>
</wd-popup>
<!-- 语音对话记录选择动作面板 -->
<wd-action-sheet
v-model="showChatHistoryDialog" :actions="chatHistoryActions" title="选择声纹向量"
v-model="showChatHistoryDialog" :actions="chatHistoryActions" :title="t('voiceprint.selectVector')"
@select="selectAudioId"
/>
</template>
+2 -1
View File
@@ -1,7 +1,7 @@
import type { PublicConfig } from '@/api/auth'
import { getPublicConfig } from '@/api/auth'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getPublicConfig } from '@/api/auth'
// 初始化状态
const initialConfigState: PublicConfig = {
@@ -12,6 +12,7 @@ const initialConfigState: PublicConfig = {
mobileAreaList: [],
beianIcpNum: '',
beianGaNum: '',
sm2PublicKey: '',
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
}
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);
}
+5 -1
View File
@@ -1 +1,5 @@
VUE_APP_API_BASE_URL=/xiaozhi
VUE_APP_API_BASE_URL=/xiaozhi
#1.如需web后台适应手机浏览器,请先启动manager-api,得到服务端地址(http://局域网ip:8002/xiaozhi
#2.在manager-mobile里的.env中,将VITE_SERVER_BASEURL修改成自己的服务端地址,然后运行pnpm dev:h5得到h5页面地址,再将其填入下方的VUE_APP_H5_URL
#3.此时手机浏览器访问web页面,即可跳转至适应手机浏览器的h5页面
VUE_APP_H5_URL=

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