Compare commits

..
237 Commits
Author SHA1 Message Date
欣南科技andGitHub 625d079168 Merge pull request #1403 from xinnan-tech/hot-fix
update:修复退出卡壳问题
2025-05-28 21:35:33 +08:00
hrz 2586843654 update:修复退出卡壳问题 2025-05-28 21:34:21 +08:00
欣南科技andGitHub b37d8abf62 Merge pull request #1400 from xinnan-tech/hot-fix
update:优化资源释放判断
2025-05-28 18:23:15 +08:00
hrz cd930edd06 update:优化资源释放判断 2025-05-28 18:22:31 +08:00
欣南科技andGitHub 4956aa6d70 Merge pull request #1399 from xinnan-tech/hot-fix
update:增强TTS流会话关闭和开启逻辑
2025-05-28 18:14:19 +08:00
hrz fd17465d94 update:增强TTS流会话关闭和开启逻辑 2025-05-28 18:07:58 +08:00
欣南科技andGitHub 87a9425241 Merge pull request #1398 from xinnan-tech/huoshan_double_stream
update:优化第一句话
2025-05-28 16:20:47 +08:00
hrz b225e8afd5 update:优化第一句话 2025-05-28 16:19:21 +08:00
hrzandGitHub 297b6e69d5 Merge pull request #1397 from xinnan-tech/huoshan_double_stream
火山双向流式tts优化
2025-05-28 15:52:58 +08:00
王华侨 d7f0e88801 火山双向流式tts优化 2025-05-28 15:47:49 +08:00
欣南科技andGitHub 14ecad720f update:修复web测试工具Maximum call stack size exceeded 的bug (#1394) 2025-05-28 11:35:32 +08:00
hrzandGitHub 81689c04a5 Merge pull request #1393 from wenbindu/main
修改注释
2025-05-28 11:30:06 +08:00
hrzandGitHub de6b9de341 Update silero.py 2025-05-28 11:29:48 +08:00
hrz 7f864eb84d update:修复web测试工具Maximum call stack size exceeded 的bug 2025-05-28 11:23:39 +08:00
wenbindu 4af0c1e2ce 修改注释 2025-05-28 10:59:59 +08:00
欣南科技andGitHub 6a4ed78812 Merge pull request #1386 from xinnan-tech/py_server_log
update:日志配置动态获取
2025-05-28 01:14:40 +08:00
hrzandGitHub 23b881f364 Merge branch 'main' into py_server_log 2025-05-28 01:13:25 +08:00
hrz 22054cf8bc update:更新日志标识位置 2025-05-28 01:00:48 +08:00
欣南科技andGitHub fc236b1d96 Merge pull request #1383 from xinnan-tech/tts-response
TTS流式框架改造:支持豆包语音双流式+支持传统非流式
2025-05-27 23:34:11 +08:00
hrz 4f81454f93 update:更新sql脚本 2025-05-27 23:30:07 +08:00
hrz b3d6f173f1 update:智控台支持火山双流式配置 2025-05-27 23:29:11 +08:00
hrz 8d2ba39ab8 优化双流式TTS时的声音 2025-05-27 18:51:08 +08:00
hrz 4260e5a1a7 update:优化音频播放 2025-05-27 18:06:44 +08:00
Sakura-RanChen 17585c8294 update:日志配置动态获取 2025-05-27 16:17:13 +08:00
hrzandGitHub d242a9e5c0 Merge branch 'main' into tts-response 2025-05-27 15:36:42 +08:00
欣南科技andGitHub 9ca98f6391 Merge pull request #1351 from xinnan-tech/py_memory_llm
update: 记忆模块使用独立LLM openai增加超参
2025-05-27 15:29:22 +08:00
hrz be7ef08f40 update:智控台添加独立记忆模型配置 2025-05-27 15:28:17 +08:00
hrz 626692df29 update:优化阿里云tts数字参数读取方式 2025-05-27 13:47:07 +08:00
hrz 631787a4f1 update:这几天tts流式改造合并后,self.executor的主要任务将是用来上报聊天记录,因此这里其实可以合并在self.executor 2025-05-27 13:41:53 +08:00
hrzandGitHub 09cf6cdc2d Merge pull request #1372 from tiamohummer/main
增加阿里云TTS private_voice 判断
2025-05-27 13:14:41 +08:00
hrzandGitHub baf174b059 Merge pull request #1223 from GOODDAYDAY/feature/muti_upload
feat: 增加上报线程池
2025-05-27 13:06:55 +08:00
tiamohummerandGitHub 49eb3178a9 Merge branch 'xinnan-tech:main' into main 2025-05-27 09:08:34 +08:00
hrzandGitHub 6a6aceff1d Tts respone update (#1382)
* update:优化

* update:添加编码解码

* update:优化流式tts

* update:优化

* update:优化

* update:旧非流失兼容改造

* update:优化线程

* update:优化

* update:优化公共方法

* update:优化火山双流式tts

* update:优化代码

* update:修改版本号

* update:合并双流式
2025-05-26 22:31:39 +08:00
hrz b8349af3da update:合并双流式 2025-05-26 22:30:45 +08:00
goodyhao 258d783f8e Merge remote-tracking branch 'origin/main' into feature/muti_upload 2025-05-26 20:28:41 +08:00
goodyhao 761fc05331 feat: 增加上报时间 2025-05-26 20:27:22 +08:00
hrz 0c8e943d1b update:修改版本号 2025-05-26 16:18:35 +08:00
hrz 9787ca60da update:优化代码 2025-05-26 16:12:38 +08:00
Sakura-RanChenandGitHub b8e57aeff4 Merge pull request #1377 from xinnan-tech/py_test_page.html_fix
Py test page.html fix
2025-05-26 15:43:12 +08:00
CGD 9a0240ef3e update:优化了页面样式 2025-05-26 15:34:21 +08:00
tiamohummerandGitHub cf5ccafe7e Merge branch 'xinnan-tech:main' into main 2025-05-26 13:42:37 +08:00
hrz ae64233986 update:优化火山双流式tts 2025-05-26 12:48:43 +08:00
hrz 6dda79ee10 update:优化公共方法 2025-05-26 11:57:55 +08:00
CGD 335a855968 Merge branch 'py_test_page.html_fix' of https://github.com/xinnan-tech/xiaozhi-esp32-server into py_test_page.html_fix 2025-05-26 11:26:44 +08:00
CGD 94c92c5f38 fix:修复测试页面音频重复播放问题 2025-05-26 11:23:42 +08:00
CGDandGitHub 35fa1493b3 Merge pull request #1373 from Minamiyama/ENH/test-page-localstorage
refactor(test_page.html): 优化UI布局和输入提示,增加本地存储功能
2025-05-26 11:13:36 +08:00
hrz f78f6fc529 update:优化 2025-05-26 11:04:13 +08:00
hrz 5be65216e2 update:优化线程 2025-05-26 10:44:35 +08:00
hrz 24526ad206 update:旧非流失兼容改造 2025-05-26 02:20:38 +08:00
Minamiyama d7564a65f7 refactor(test_page.html): 优化UI布局和输入提示,增加本地存储功能
- 在OTA和WebSocket服务器地址输入框中添加更详细的占位符提示
- 增加本地存储功能,保存并恢复OTA和WebSocket服务器地址
2025-05-25 15:24:06 +08:00
goodyhao 866d61cfaf Merge branch 'refs/heads/main' into feature/muti_upload
# Conflicts:
#	main/xiaozhi-server/core/connection.py
2025-05-25 13:22:51 +08:00
欣南科技andGitHub 3f3f3fdaa6 Merge pull request #1371 from xinnan-tech/hot-fix
修复测试页面音频重复播放
2025-05-25 09:10:47 +08:00
hrz e0da59096a 修复测试页面音频重复播放 2025-05-25 09:10:11 +08:00
hrz 40632019ac update:优化 2025-05-25 08:56:58 +08:00
hrzandGitHub 3c5563b62f Merge pull request #1369 from xinnan-tech/py_test
fix:修复“小智服务器测试页面播放语音问题”
2025-05-25 08:46:57 +08:00
CGD 562424b74d fix:修复“小智服务器测试页面播放语音问题” 2025-05-25 00:09:36 +08:00
hrz 7a598d5839 update:优化 2025-05-24 23:43:16 +08:00
hrz 574d34bc2c update:优化流式tts 2025-05-24 17:50:03 +08:00
hrz 16a4ccdb12 update:添加编码解码 2025-05-24 14:52:27 +08:00
hrz 920cf4f897 update:优化 2025-05-24 12:11:13 +08:00
欣南科技andGitHub 76ee2c5365 Merge pull request #1366 from xinnan-tech/update-fix
合并chat和chat_with_function_calling
2025-05-24 09:40:28 +08:00
hrz 472106390d update:优化音频播放方法 2025-05-24 09:39:10 +08:00
hrz d97f8b2e9a update:供应器管理更名为字段管理,防止混淆模型管理 2025-05-24 02:17:49 +08:00
hrz 98e2526f8a update:合并chat和chat_with_function_calling 2025-05-24 02:15:06 +08:00
tiamohummer c30c4649a4 增加阿里云TTS private_voice 判断 2025-05-23 17:41:22 +08:00
欣南科技andGitHub ede8676979 Merge pull request #1363 from xinnan-tech/hot-fix
update:修复iotbug
2025-05-23 16:10:21 +08:00
hrz 17fb60b7ae update:修复iotbug 2025-05-23 16:03:15 +08:00
hrz 8119897818 Merge branch 'main' into tts-response
# Conflicts:
#	main/xiaozhi-server/core/handle/intentHandler.py
2025-05-23 09:15:15 +08:00
欣南科技andGitHub 0a9e5d2cea Merge pull request #1357 from xinnan-tech/update-docs
update:更新视频教程
2025-05-22 22:23:21 +08:00
hrz a5f74d7767 update:更新视频教程 2025-05-22 22:22:43 +08:00
Sakura-RanChen 02cb9c35b3 update: 记忆模块使用独立LLM openai增加超参 2025-05-22 17:14:58 +08:00
欣南科技andGitHub d934e0a69d Merge pull request #1348 from xinnan-tech/custom_tts_api
Custom tts api
2025-05-22 11:26:26 +08:00
hrz 2ecce6485c update:增加自定义的TTS接口服务的使用说明 2025-05-22 11:26:00 +08:00
hrz 1ad8aeeaa7 add:CustomTTS提供一个示例 2025-05-22 11:22:16 +08:00
hrz 46a15082d6 update:优化按钮 2025-05-22 10:41:56 +08:00
hrzandGitHub 153ecf0b41 Merge pull request #1253 from hsoftxl/custom_tts_api
自定义tts接口增加POST请求支持
2025-05-22 09:58:47 +08:00
hrzandGitHub f0f7304ae2 Merge branch 'custom_tts_api' into custom_tts_api 2025-05-22 09:58:38 +08:00
欣南科技andGitHub 997e171118 Merge pull request #1327 from xinnan-tech/web-pages-modifiy
增加fastgpt字段处理,搜索图标下移
2025-05-22 09:44:11 +08:00
欣南科技andGitHub 04a8fe8af2 Merge pull request #1342 from xinnan-tech/fix-global-iot
fix:修复全局变量all_function_registry导致iot描述无法更新bug
2025-05-22 09:43:13 +08:00
hrzandGitHub a0822a356c Merge pull request #1345 from CaixyPromise/chore/server-manager
chore: 服务端管理接口代码健壮性调整,补充注释与删除署名注释。
2025-05-22 09:01:28 +08:00
caixypromise b78d337fd0 chore: 服务端管理接口代码健壮性优化,补充注释与删除署名配置。 2025-05-22 00:49:56 +08:00
hrzandGitHub 13c5cb3f1d Merge pull request #1340 from xinnan-tech/py_symbol_fix
解决符号误识别问题,添加与退出相关的提示词
2025-05-21 22:12:52 +08:00
hrzandGitHub 197c0532fa Merge pull request #1341 from xinnan-tech/py_Recognizing-Intent_fix
update:修复intent_llm识别意图不精准问题,handle_device改名
2025-05-21 22:09:56 +08:00
hrz 76342b09ff fix:修复全局变量all_function_registry导致iot描述无法更新bug 2025-05-21 18:35:34 +08:00
CGD 241d96f6ac update:修复intent_llm识别意图不精准问题,handle_device改名 2025-05-21 18:17:08 +08:00
hrz fdc6dcb26e update:去掉无用代码 2025-05-21 16:25:22 +08:00
hrz 9fd010e91e update:调试 2025-05-21 15:59:24 +08:00
hrz c900498ce8 update:合并main分支 2025-05-21 15:55:40 +08:00
hrz c4c84e44e1 update:合并main分支 2025-05-21 14:52:24 +08:00
hrz 191ac47353 合并main分支 2025-05-21 14:13:52 +08:00
hrz 851365fb58 update:合并非tts代码 2025-05-21 13:18:12 +08:00
hrz ede2bc6a4e update:更新manager-api模块 2025-05-21 11:55:56 +08:00
hrz 38780b6daa update:忽略本地文件 2025-05-21 11:46:26 +08:00
欣南科技andGitHub 0dc267c109 Merge pull request #1337 from xinnan-tech/connect-server-and-api
智控台控制xiaozhi-server重启和更新配置
2025-05-21 11:01:43 +08:00
hrz bc6c85c6c6 update:增加ws连接时长,避免ws时间过短到只conn对象销毁丢失无法使用 2025-05-21 10:43:48 +08:00
Sakura-RanChen 915f4cac5e 对通用dict类型的字段进行处理 2025-05-21 09:55:15 +08:00
hrz b8da0714c2 update:优化无用组件 2025-05-20 22:58:42 +08:00
hrzandGitHub f1105e6822 Merge pull request #1331 from CaixyPromise/feature/connect-server-and-api
feat: 新增智控台管理websocket服务器端【配置拉取】和【重启】
2025-05-20 22:09:29 +08:00
hrzandGitHub 20296148eb Merge pull request #1332 from CaixyPromise/fix/dockerfile-arm
fix: 修复docker镜像不兼容arm64架构。
2025-05-20 22:01:52 +08:00
caixypromise 2138c0fb4f fix: 修复docker镜像不兼容arm开发板架构。
更新第三阶段运行状态的运行jdk镜像,切换包管理器为apk。
2025-05-20 20:13:41 +08:00
caixypromise 34d9857089 feat: 新增智控台管理websocket服务器端【配置拉取】和【重启】 2025-05-20 20:02:16 +08:00
欣南科技andGitHub 888b71a196 Merge pull request #1330 from xinnan-tech/hot-fix
update:优化ai_model_provider的fields字段
2025-05-20 18:05:10 +08:00
hrz 5f16c871dc update:优化ai_model_provider的fields字段 2025-05-20 18:04:33 +08:00
Sakura-RanChen 595a338de7 增加fastgpt字段处理,搜索图标下移 2025-05-20 16:57:48 +08:00
欣南科技andGitHub fe47fd1910 Merge pull request #1323 from xinnan-tech/hot-fix
fix:参数名称变更
2025-05-20 11:31:28 +08:00
hrz ac9abf8ede fix:参数名称变更 2025-05-20 11:30:57 +08:00
欣南科技andGitHub 282706dbc8 Merge pull request #1320 from xinnan-tech/manager-icp-num
可自定义智控台底部的系统名称、ICP备案号、公安备案号
2025-05-20 11:15:38 +08:00
hrz 5360cbf19c update:优化asr测速源:从已有的几个文件中测速度 2025-05-20 11:14:20 +08:00
hrz 84ac1b6686 update:可自定义智控台底部系统名称 2025-05-20 10:59:22 +08:00
CGD ebc16207f4 update:intent_llm添加与退出相关的提示词 2025-05-20 10:54:48 +08:00
欣南科技andGitHub 7611e98156 Merge pull request #1312 from xinnan-tech/manager-api-aly-message
修复短信并发漏洞
2025-05-19 21:23:49 +08:00
hrz 848f73715f update:完成密码找回功能 2025-05-19 21:19:24 +08:00
hrzandGitHub 6f30dfb963 Merge pull request #1315 from qjh9527/add-asr-performance
fix: stt中 test_count 计算逻辑错误
2025-05-19 20:32:03 +08:00
QJH c8aa903e3e fix: stt中 test_count 计算逻辑错误 2025-05-19 18:43:34 +08:00
剑雨 f0e55037de 剔除已经删除的类的引用和使用 2025-05-19 18:33:02 +08:00
hrzandGitHub 1335b368d1 Merge pull request #1314 from xinnan-tech/web-forget-password
增加找回密码的功能
2025-05-19 17:57:55 +08:00
CGD a647f3c105 fix:解决符号误识别问题 2025-05-19 15:18:01 +08:00
剑雨 c936372105 优化发送短信失败的情况,回退发送次数
--ALiYunSmsService.java 使用redis加1原子操作
--CaptchaServiceImpl.java 使用redis减1原子操作
2025-05-19 14:41:53 +08:00
Sakura-RanChen f242d43f73 增加找回密码的功能 2025-05-19 14:15:28 +08:00
hrzandGitHub c216012304 Merge pull request #1296 from qjh9527/add-asr-performance
Add ASR performance
2025-05-19 14:04:45 +08:00
hrzandGitHub 87c2f0e38e Merge pull request #1304 from 2331892928/main
ICP备案号添加
2025-05-19 13:51:15 +08:00
欣南科技andGitHub d9cc75edc0 Merge pull request #1307 from xinnan-tech/ali-msm-doc
增加阿里云短信集成使用说明
2025-05-19 11:44:27 +08:00
hrz 1f91ec32da update:优化说明 2025-05-19 11:43:28 +08:00
hrz 92b60b7f25 update:优化阿里云短信管理文档 2025-05-19 11:36:59 +08:00
hrz 8104d02a3a add:阿里云短信集群指南 2025-05-19 11:31:53 +08:00
剑雨 83c4add375 优化清空redis的key方法。添加加1,减1方法
--RedisUtils.java 新的方法
--emptyAll.lua 清空redis的key的lua脚本
2025-05-19 11:20:57 +08:00
剑雨 c19084b7b4 修复并发漏洞,获取和设置短信发送间隔时间,不是原子性的,并发请求过来会导致验证有漏洞被通过
--CaptchaServiceImpl.java 设置获取和设置发现间隔的方法为原子性
2025-05-19 10:51:39 +08:00
剑雨 2761e97c6a 新增获取在redis指定key的值,如果值为空,着设置key的默认值的方法
--RedisUtils.java 添加新方法
--getKeyOrCreate.lua 此方法的lua脚本
2025-05-19 10:46:59 +08:00
剑雨 0cdfd1d114 新建资源读取工具类
--ResourcesUtils.java 读取资源获取到字符串的方法
2025-05-19 10:41:02 +08:00
AMEN 8566e048b6 Merge remote-tracking branch 'origin/main' 2025-05-18 23:34:53 +08:00
AMEN 31fc731af6 前端
修改参数查询的input提示
增加公安备案号和网站备案号显示
2025-05-18 23:33:28 +08:00
AMEN ff95764824 后端
参数管理新增参数:
server.beian_icp_num
server.beian_ga_num
修改:
参数查询支持备注查询
2025-05-18 23:00:01 +08:00
欣南科技andGitHub 737c4a6b5d Merge pull request #1303 from xinnan-tech/hot-fix
修复:测试用例参数错误
2025-05-18 22:25:46 +08:00
hrz db68871646 修复:测试用例参数错误 2025-05-18 22:25:09 +08:00
剑雨 748f17b777 开启手机注册,验证相关参数不可以为空
--SysParamsServiceImpl.java 开启手机注册参数添加验证
2025-05-18 18:44:11 +08:00
欣南科技andGitHub bbba24c7bc Merge pull request #1301 from xinnan-tech/hot-fix
fix:登录失败错误信息显示
2025-05-18 17:59:37 +08:00
hrz 5bba864faa fix:登录失败错误信息显示 2025-05-18 17:59:08 +08:00
欣南科技andGitHub 33b7fe6072 Merge pull request #1300 from xinnan-tech/hot-fix
update:补充缺失参数
2025-05-18 17:55:53 +08:00
hrz 4d0bef62ed update:补充缺失参数 2025-05-18 17:55:18 +08:00
欣南科技andGitHub e09ba81cfa Merge pull request #1290 from xinnan-tech/manager-api-aly-message
阿里云短信发送功能,手机注册和找回密码接口
2025-05-18 17:42:30 +08:00
hrzandGitHub baf15450a3 Merge branch 'main' into manager-api-aly-message 2025-05-18 17:42:09 +08:00
hrz d24419cfb7 update:发送手机验证码 2025-05-18 17:39:12 +08:00
hrz fe88db2094 add:登录页面和注册页面增加手机号码选项 2025-05-18 17:38:20 +08:00
hrz 91d2ab7d1d update:优化 2025-05-18 11:09:58 +08:00
QJH 9a405a756f fix: "ASR", "LLM", "TTS"为空时的情况 2025-05-18 05:56:16 +08:00
QJH 86f5be23fe add: ASR测试任务 2025-05-18 05:48:31 +08:00
欣南科技andGitHub 9f4ca1aa6b Merge pull request #1294 from xinnan-tech/model-provider-service
Model provider service
2025-05-17 23:56:47 +08:00
hrz 624b1cce74 update:优化已存在的Group注解 2025-05-17 23:54:25 +08:00
hrz 29216768e1 update:优化模型供应器查询 2025-05-17 23:18:19 +08:00
hrz 59e18dc1c1 Merge branch 'main' into model-provider-service 2025-05-17 22:41:25 +08:00
欣南科技andGitHub 858a85f952 Merge pull request #1293 from xinnan-tech/hit-fix
update:修复fishspeech seed参数类型转换
2025-05-17 17:08:01 +08:00
hrz 1cbd68f1e3 update:修复fishspeech seed参数类型转换 2025-05-17 17:07:19 +08:00
欣南科技andGitHub 324bf62bb8 Merge pull request #1291 from xinnan-tech/hot-fix
update:恢复旧文件,解决已部署用户sql出错问题
2025-05-17 16:00:35 +08:00
hrz dd1d7d5dc1 update:回复旧文件,解决已部署用户sql出错问题 2025-05-17 15:58:53 +08:00
hrzandGitHub e6ad9e23cc Merge pull request #1288 from ljwwd2/main
供应器管理
2025-05-17 15:47:42 +08:00
hrzandGitHub a557322dc6 Merge pull request #1277 from 2331892928/main
用户管理页面的搜索功能增加模糊搜索
2025-05-17 15:36:20 +08:00
剑雨 7d7324ddbc 测试短信发送
--loginControllerTest.java 短信测试的测试类
2025-05-17 11:00:02 +08:00
剑雨 680ed4596f 测试,修复业务流程bug,添加新的错误代码和说明
--ErrorCode.java 新的错误代码
--LoginController.java 修复业务流程bug
--messages.properties 添加新的业务说明
2025-05-17 10:59:00 +08:00
剑雨 e66ab647e1 优化短信发送的异常处理
--ALiYunSmsService.java 优化异常处理
2025-05-17 10:57:02 +08:00
ljwwd2andGitHub 253f4c59a3 Merge branch 'xinnan-tech:main' into main 2025-05-17 01:19:12 +08:00
ljwwd2 38a97d6b5a 供应器管理-前端页面 2025-05-17 01:18:19 +08:00
ljwwd2 ae8f11d5a8 供应器管理-增删改查api 2025-05-17 01:18:03 +08:00
CGDandGitHub dcfd82f84e Merge pull request #1283 from xinnan-tech/py_text_audio
样式修改
2025-05-16 17:48:00 +08:00
TOM88812andGitHub 25a531341a Merge pull request #1281 from jwhna1/main
fix:修复智控台fish-speech无法按声音播放bug
2025-05-16 17:23:22 +08:00
曾能混andGitHub f421f6dee8 Merge branch 'xinnan-tech:main' into main 2025-05-16 17:16:44 +08:00
曾能混andGitHub a5332ccc7d Update 202504112044.sql
fix:修复智控台fish-speech无法按声音播放bug
2025-05-16 17:10:55 +08:00
Sakura-RanChen 76c3f401ea 样式修改 2025-05-16 14:55:53 +08:00
Sakura-RanChenandGitHub 5173dded2a Merge pull request #1280 from xinnan-tech/py_test_fix
update:在识别到continue_chat意图时,清理对话历史中的工具调用相关消息
2025-05-16 14:47:04 +08:00
CGD bdd8597f69 update:在识别到continue_chat意图时,清理对话历史中的工具调用相关消息 2025-05-16 14:33:37 +08:00
lvjinfa 46bba64a94 用户管理页面的搜索功能增加模糊搜索 2025-05-16 12:28:32 +08:00
XL d0694a0e70 兼容yaml配置和管理端配置 2025-05-16 11:56:54 +08:00
hsoftxlandGitHub bd12b0e81f Merge branch 'xinnan-tech:main' into custom_tts_api 2025-05-16 10:51:49 +08:00
TOM88812andGitHub 2f4fb0fc52 Merge pull request #1250 from jwhna1/main
Update ollama.py 为本地ollama部署qwen3大语言模型添加适配!
2025-05-16 10:51:17 +08:00
ljwwd2 cbabf01902 供应器管理-增删改查api 2025-05-16 03:04:52 +08:00
ljwwd2 c51c8b4af0 供应器管理-增删改查api 2025-05-16 01:42:19 +08:00
剑雨 2741e11078 新增发送短信验证码接口,找回密码接口,修改注册接口,如果开启手机注册模式,开启短信验证模式
--LoginController.java 添加和修改方法
--RetrievePasswordDTO.java 添加找回密码dto
2025-05-16 00:16:20 +08:00
剑雨 aa251aa011 添加校验单参数是否是正确的手机号的方法
--ValidatorUtils.java 添加新方法
2025-05-15 22:47:57 +08:00
剑雨 0509ddb129 验证模块添加发送短信验证码和验证短信验证码
--CaptchaService.java 新增发送短信验证码和验证短信验证码方法定义
--CaptchaServiceImpl.java 实现新定义定义的方法
--RedisKeys.java 添加短信验证码key
2025-05-15 21:45:33 +08:00
欣南科技andGitHub 5a180836b7 Merge pull request #1273 from xinnan-tech/fix-firmware-default-url
修复:固件要求firmware的url不能为空
2025-05-15 21:42:23 +08:00
hrz 9c7c6b9ea7 修复:固件要求firmware的url不能为空 2025-05-15 21:41:40 +08:00
欣南科技andGitHub d64eb0b087 Merge pull request #1268 from xinnan-tech/openrz-patch-2
docker-compose设置redis开机自启动
2025-05-15 17:41:54 +08:00
hrzandGitHub 28e4318f8a Update docker-compose_all.yml 2025-05-15 17:41:16 +08:00
欣南科技andGitHub 36f0821ea0 Merge pull request #1267 from xinnan-tech/openrz-patch-1
Update requirements.txt
2025-05-15 17:22:11 +08:00
hrzandGitHub 293a690ccd Update requirements.txt 2025-05-15 17:21:46 +08:00
欣南科技andGitHub 1c96ff7d3b Merge pull request #1266 from xinnan-tech/web-headerbar-modify
update:优化文档
2025-05-15 17:07:17 +08:00
hrz db8a2bb872 update:优化文档 2025-05-15 17:06:06 +08:00
hrz dfbe4fc7ee update:增加mcp-proxy依赖 2025-05-15 16:30:57 +08:00
欣南科技andGitHub 3cccc4ec64 Merge pull request #1265 from xinnan-tech/web-headerbar-modify
update:优化文档
2025-05-15 16:23:26 +08:00
hrz e29684e672 update:优化文档 2025-05-15 16:22:41 +08:00
XL eadb69bdc0 修改使用说明 2025-05-15 15:46:41 +08:00
XL e73223ca8a Merge remote-tracking branch 'origin/custom_tts_api' into custom_tts_api
# Conflicts:
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-05-15 15:39:47 +08:00
XL 6dd66dbae9 自定义tts接口参数转dict
支持post请求
2025-05-15 15:39:06 +08:00
XL 073b69f94a 自定义tts接口参数转dict 2025-05-15 15:38:34 +08:00
XL 1a0a0777a3 自定义tts接口参数转dict
支持post请求
2025-05-15 15:35:45 +08:00
XL a971ae2168 自定义tts接口参数转dict 2025-05-15 10:23:40 +08:00
曾能混andGitHub 6b8d2502b7 Update ollama.py
为本地ollama部署qwen3大语言模型添加适配!在识别到启用为qwen3大模型的时候自动添加no_think停用推理模式,并过滤<think></think>标签转入给TTS(因为qwen3虽然用no_think停用推理后还是会输出<think></think>转入到TTS)
2025-05-15 06:32:47 +08:00
剑雨 e4194cdf4d 添加使用阿里云平台发送验证码短信的方法
--pom.xml 添加短信发送sdk
--Constant.java 添加发送短信的参数枚举
--ALiYunSmsService.java 阿里云实现短信方法
--SmsService.java 短信方法定义
2025-05-14 14:18:01 +08:00
剑雨 c8392c9b47 添加发送短信参数,添加到数据库里
--202505141129.sql
--db.changelog-master.yaml
2025-05-14 12:22:13 +08:00
goodyhao 885b7a0b05 feat: 增加上报线程池 2025-05-13 15:54:00 +08:00
CoderMonkeyandGitHub 92e117c10a Merge pull request #737 from HiCoderMonkey/tts-response
Tts response 双向流tts,fix 播放音乐
2025-04-11 11:12:12 +08:00
lizhongxiang 71db92ef61 fix: 播放音乐 2025-04-11 11:08:22 +08:00
lizhongxiang 118bb728a9 fix: 播放咋音问题 2025-04-11 10:25:43 +08:00
hrz d7aac5e10f Merge branch 'main' into tts-response
# Conflicts:
#	main/manager-web/.env.development
2025-04-09 21:46:59 +08:00
CoderMonkeyandGitHub 4359f1d5b3 Merge pull request #719 from HiCoderMonkey/tts-response
Tts response ,fix 播放有咋音
2025-04-09 14:38:02 +08:00
lizhongxiang 8bb888c58e fix: 播放咋音问题 2025-04-09 14:32:32 +08:00
lizhongxiang 4c81585e32 merge oeigin/tts-response 2025-04-07 18:15:09 +08:00
hrz 0e6d1cd677 Merge branch 'main' into tts-response 2025-04-04 02:02:30 +08:00
hrz 98368cbe06 fix:文字回复卡住bug 2025-04-04 02:01:48 +08:00
hrz 0b4a4df1af update:合并最新代码 2025-04-04 00:27:04 +08:00
hrz c8a3d378b7 update:合并main分支 2025-04-03 23:06:59 +08:00
CoderMonkeyandGitHub aa34473560 Merge pull request #640 from HiCoderMonkey/tts-response
fix:删除多余方法
2025-04-02 11:37:23 +08:00
lizhongxiang f479a789db fix:删除多余方法 2025-04-02 11:35:49 +08:00
CoderMonkeyandGitHub d4ce501c75 Merge pull request #639 from HiCoderMonkey/tts-response
fix:播放音乐不会停
2025-04-02 11:26:43 +08:00
lizhongxiang c2643d0b80 fix:merge main 2025-04-02 11:18:26 +08:00
lizhongxiang 078605c188 fix:播放音乐不会停 2025-04-02 11:11:16 +08:00
lizhongxiang cd3e9ed494 fix:播放音乐不会停 2025-04-02 11:07:04 +08:00
CoderMonkeyandGitHub 8a5a9cc0b3 Merge pull request #625 from HiCoderMonkey/tts-response
fix:火山声音复刻
2025-04-01 15:09:12 +08:00
lizhongxiang 347fc4ca44 fix:火上声音复刻 2025-04-01 15:07:16 +08:00
hrz 61db1192e2 update:格式化代码 2025-04-01 14:27:17 +08:00
CoderMonkeyandGitHub d9d973b6aa Merge pull request #612 from HiCoderMonkey/tts-response
fix:aliyun,cozecn,custom,doubao,gpt_sovits_v2,gpt_sovits_v3,minimax,o…
2025-03-31 16:32:08 +08:00
lizhongxiang cc4aed5a8e fix:aliyun,cozecn,custom,doubao,gpt_sovits_v2,gpt_sovits_v3,minimax,openai,siliconflow,ttson,这些tts兼容 2025-03-31 16:31:06 +08:00
CoderMonkeyandGitHub b44d52fb3e Merge pull request #610 from HiCoderMonkey/tts-response
fix:普通聊天调试
2025-03-31 15:35:33 +08:00
lizhongxiang 93265be974 fix:普通聊天调试 2025-03-31 15:33:20 +08:00
CoderMonkeyandGitHub 2028405af6 Merge pull request #606 from HiCoderMonkey/tts-response
tts 双向流改造
2025-03-31 15:21:38 +08:00
lizhongxiang bb9df3f54c add:双向流tts结构改造,火山双向tts语音流式输入输出 2025-03-31 15:15:59 +08:00
lizhongxiang 264487574b add:火山双向tts语音流式输入输出 2025-03-29 14:17:09 +08:00
CoderMonkeyandGitHub 6a00895af8 Merge pull request #443 from HiCoderMonkey/tts-response
Tts 改造
2025-03-20 10:16:47 +08:00
lizhongxiang 1136ca4b24 mergin main 2025-03-20 10:14:49 +08:00
lizhongxiang b153bc11f2 mergin main,速度提升一下 2025-03-20 09:39:25 +08:00
lizhongxiang a067a6ba87 fix: 流式tts,function聊天时候的支持 2025-03-13 17:50:29 +08:00
lizhongxiang c271b88c87 fix: 流式tts,function聊天时候的支持 2025-03-13 17:44:10 +08:00
lizhongxiang 1dfa619243 fix: 流式流畅度问题,英文符号切分,空格切分 2025-03-13 15:30:00 +08:00
lizhongxiang 91bdc05a38 merge main 2025-03-13 14:24:18 +08:00
lizhongxiang 0e8edfe002 merge main,fix gitignore 2025-03-10 11:12:17 +08:00
lizhongxiang c1084923f7 merge main 2025-03-10 10:51:25 +08:00
lizhongxiang b2381fc689 add: 语音流式输出 2025-03-05 17:29:26 +08:00
lizhongxiang fa75f56ffb add: 语音流式输出 2025-03-05 17:26:32 +08:00
151 changed files with 6051 additions and 1643 deletions
+10 -2
View File
@@ -107,6 +107,7 @@ celerybeat.pid
*.sage.py *.sage.py
# Environments # Environments
.env
.venv .venv
env/ env/
venv/ venv/
@@ -145,24 +146,31 @@ tmp
.history .history
.DS_Store .DS_Store
main/xiaozhi-server/data main/xiaozhi-server/data
main/xiaozhi-server/config/assets/wakeup_words.*
main/manager-web/node_modules main/manager-web/node_modules
.config.yaml .config.yaml
.secrets.yaml .secrets.yaml
.private_config.yaml .private_config.yaml
.env.development
# model files # model files
main/xiaozhi-server/models/SenseVoiceSmall/model.pt main/xiaozhi-server/models/SenseVoiceSmall/model.pt
main/xiaozhi-server/models/sherpa-onnx* main/xiaozhi-server/models/sherpa-onnx*
/main/xiaozhi-server/audio_ref/
/audio_ref/
/asr-models/iic/SenseVoiceSmall/
/main/xiaozhi-server/asr-models/iic/SenseVoiceSmall/
/models/SenseVoiceSmall/model.pt
my_wakeup_words.mp3 my_wakeup_words.mp3
!main/xiaozhi-server/config/assets/bind_code.wav !main/xiaozhi-server/config/assets/bind_code.wav
!main/xiaozhi-server/config/assets/wakeup_words.wav
!main/xiaozhi-server/config/assets/bind_not_found.wav !main/xiaozhi-server/config/assets/bind_not_found.wav
!main/xiaozhi-server/config/assets/bind_code/*.wav !main/xiaozhi-server/config/assets/bind_code/*.wav
!main/xiaozhi-server/config/assets/max_output_size.wav !main/xiaozhi-server/config/assets/max_output_size.wav
main/manager-api/.vscode main/manager-api/.vscode
# Ignore webpack cache directory # Ignore webpack cache directory
main/manager-web/.webpack_cache/ main/manager-web/.webpack_cache/
main/xiaozhi-server/mysql main/xiaozhi-server/mysql
uploadfile uploadfile
*.json
.vscode .vscode
+10 -7
View File
@@ -7,20 +7,23 @@ COPY main/manager-web .
RUN npm run build RUN npm run build
# 第二阶段:构建Java后端 # 第二阶段:构建Java后端
FROM maven:3-eclipse-temurin-21-alpine as api-builder FROM maven:3.9.4-eclipse-temurin-21 as api-builder
WORKDIR /app WORKDIR /app
COPY main/manager-api/pom.xml . COPY main/manager-api/pom.xml .
COPY main/manager-api/src ./src COPY main/manager-api/src ./src
RUN mvn clean package -Dmaven.test.skip=true RUN mvn clean package -Dmaven.test.skip=true
# 第三阶段:构建最终镜像 # 第三阶段:构建最终镜像
FROM eclipse-temurin:21-jdk-jammy FROM bellsoft/liberica-runtime-container:jre-21-glibc
# 安装Nginx并清理缓存 # 安装Nginx和字体库
RUN apt-get update && \ RUN apk update && \
apt-get install -y nginx && \ apk add --no-cache nginx bash && \
apt-get clean && \ apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \
rm -rf /var/lib/apt/lists/* rm -rf /var/cache/apk/*
# 更新字体缓存
RUN printf 'YES\n' | update-ms-fonts && fc-cache -f -v
# 配置Nginx # 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
+5 -4
View File
@@ -142,10 +142,11 @@
#### 🚀 部署方式选择 #### 🚀 部署方式选择
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 | | 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 | 视频教程 |
|---------|------|---------|---------|---------| |---------|------|---------|---------|---------|---------|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| | **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| - |
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | | **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据 > 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
+5 -1
View File
@@ -104,7 +104,11 @@ VAD:
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md) 参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
### 8、更多问题,可联系我们反馈 💬 ### 8、如何开启手机注册智控台 📱
参考教程[阿里云短信集成指南](./ali-sms-integration.md)
### 9、更多问题,可联系我们反馈 💬
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。 可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
+44
View File
@@ -0,0 +1,44 @@
# 阿里云短信集成指南
登录阿里云控制台,进入“短信服务”页面:https://dysms.console.aliyun.com/overview
## 第一步 添加签名
![步骤](images/alisms/sms-01.png)
![步骤](images/alisms/sms-02.png)
以上步骤,会得到签名,请把它写入到智控台参数,`aliyun.sms.sign_name`
## 第二步 添加模版
![步骤](images/alisms/sms-11.png)
以上步骤,会得到模版code,请把它写入到智控台参数,`aliyun.sms.sms_code_template_code`
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
可以等报备成功后,再继续往下操作。
## 第三步 创建短信账户和开通权限
登录阿里云控制台,进入“访问控制”页面:https://ram.console.aliyun.com/overview?activeTab=overview
![步骤](images/alisms/sms-21.png)
![步骤](images/alisms/sms-22.png)
![步骤](images/alisms/sms-23.png)
![步骤](images/alisms/sms-24.png)
![步骤](images/alisms/sms-25.png)
以上步骤,会得到access_key_id和access_key_secret,请把它写入到智控台参数,`aliyun.sms.access_key_id``aliyun.sms.access_key_secret`
## 第四步 启动手机注册功能
1、正常来说,以上信息都填完后,会有这个效果,如果没有,可能缺少了某个步骤
![步骤](images/alisms/sms-31.png)
2、开启允许非管理员用户可注册,将参数`server.allow_user_register`设置成`true`
3、开启手机注册功能,将参数`server.enable_mobile_register`设置成`true`
![步骤](images/alisms/sms-32.png)
+2 -1
View File
@@ -1,7 +1,7 @@
# 基于虾哥编译好的固件配置自定义服务器 # 基于虾哥编译好的固件配置自定义服务器
## 第1步 确认版本 ## 第1步 确认版本
烧录虾哥已经编译好的1.6.1版本固件 烧录虾哥已经编译好的[1.6.1版本以上固件](https://github.com/78/xiaozhi-esp32/releases)
## 第2步 准备你的ota地址 ## 第2步 准备你的ota地址
如果你按照教程使用的是全模块部署,就应该会有ota地址。 如果你按照教程使用的是全模块部署,就应该会有ota地址。
@@ -29,6 +29,7 @@ wss://2662r3426b.vicp.fun/xiaozhi/v1/
## 第3步 进入配网模式 ## 第3步 进入配网模式
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备 进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
![请参考-OTA地址设置](../docs/images/firmware-setting-ota.png)
## 第4步 唤醒小智,查看日志输出 ## 第4步 唤醒小智,查看日志输出
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

+16 -1
View File
@@ -28,6 +28,8 @@
<captcha.version>1.6.2</captcha.version> <captcha.version>1.6.2</captcha.version>
<guava.version>33.0.0-jre</guava.version> <guava.version>33.0.0-jre</guava.version>
<liquibase-core.version>4.20.0</liquibase-core.version> <liquibase-core.version>4.20.0</liquibase-core.version>
<aliyun-sms-version>4.1.0</aliyun-sms-version>
<okio-version>3.4.0</okio-version>
</properties> </properties>
<dependencies> <dependencies>
@@ -37,6 +39,7 @@
<classifier>jakarta</classifier> <classifier>jakarta</classifier>
<version>${shiro.version}</version> <version>${shiro.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.shiro</groupId> <groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId> <artifactId>shiro-spring</artifactId>
@@ -201,6 +204,18 @@
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
</dependency> </dependency>
<!-- 阿里云短信sdk -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>${aliyun-sms-version}</version>
</dependency>
<!-- 阿里云短信sdk使用的okio:1.15.0版本有安全漏洞:GHSA-w33c-445m-f8w7 现在升级成3.4.0安全版本-->
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>${okio-version}</version>
</dependency>
</dependencies> </dependencies>
<!-- 阿里云maven仓库 --> <!-- 阿里云maven仓库 -->
@@ -243,4 +258,4 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>
@@ -118,29 +118,17 @@ public interface Constant {
enum SysBaseParam { enum SysBaseParam {
/** /**
* 系统全称 * ICP备案号
*/ */
SYS_NAME("SYS_NAME"), BEIAN_ICP_NUM("server.beian_icp_num"),
/** /**
* 系统简称 * GA备案号
*/ */
SYS_SHORT_NAME("SYS_SHORT_NAME"), BEIAN_GA_NUM("server.beian_ga_num"),
/** /**
* 系统描述 * 系统名称
*/ */
SYS_DES("SYS_DES"), SERVER_NAME("server.name");
/**
* 登录失败几次锁定
*/
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
/**
* 账号失败锁定分钟数
*/
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
/**
* TOKEN强验证
*/
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
private String value; private String value;
@@ -153,6 +141,46 @@ public interface Constant {
} }
} }
/**
* 系统短信
*/
enum SysMSMParam {
/**
* 阿里云授权keyID
*/
ALIYUN_SMS_ACCESS_KEY_ID("aliyun.sms.access_key_id"),
/**
* 阿里云授权密钥
*/
ALIYUN_SMS_ACCESS_KEY_SECRET("aliyun.sms.access_key_secret"),
/**
* 阿里云短信签名
*/
ALIYUN_SMS_SIGN_NAME("aliyun.sms.sign_name"),
/**
* 阿里云短信模板
*/
ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE("aliyun.sms.sms_code_template_code"),
/**
* 单号码最大短信发送条数
*/
SERVER_SMS_MAX_SEND_COUNT("server.sms_max_send_count"),
/**
* 是否开启手机注册
*/
SERVER_ENABLE_MOBILE_REGISTER("server.enable_mobile_register");
private String value;
SysMSMParam(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
/** /**
* 数据状态 * 数据状态
*/ */
@@ -199,5 +227,30 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.4.3"; public static final String VERSION = "0.5.1";
/**
* 无效固件URL
*/
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
/**
* 字典类型
*/
enum DictType {
/**
* 手机区号
*/
MOBILE_AREA("MOBILE_AREA");
private String value;
DictType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
} }
@@ -20,7 +20,7 @@ public interface ErrorCode {
int ACCOUNT_DISABLE = 10005; int ACCOUNT_DISABLE = 10005;
int IDENTIFIER_NOT_NULL = 10006; int IDENTIFIER_NOT_NULL = 10006;
int CAPTCHA_ERROR = 10007; int CAPTCHA_ERROR = 10007;
int SUB_MENU_EXIST = 10008; int PHONE_NOT_NULL = 10008;
int PASSWORD_ERROR = 10009; int PASSWORD_ERROR = 10009;
int SUPERIOR_DEPT_ERROR = 10011; int SUPERIOR_DEPT_ERROR = 10011;
@@ -1,7 +1,12 @@
package xiaozhi.common.exception; package xiaozhi.common.exception;
import java.util.List;
import java.util.Objects;
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException;
@@ -60,4 +65,20 @@ public class RenExceptionHandler {
return new Result<Void>().error(404, "资源不存在"); return new Result<Void>().error(404, "资源不存在");
} }
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
String errorMsg = allErrors.stream()
.filter(Objects::nonNull)
.map(err -> {
String msg = err.getDefaultMessage();
return (msg != null && !msg.trim().isEmpty()) ? msg : null;
})
.filter(Objects::nonNull)
.findFirst()
.orElse("请求参数错误!");
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
}
} }
@@ -117,4 +117,26 @@ public class RedisKeys {
public static String getAgentAudioIdKey(String uuid) { public static String getAgentAudioIdKey(String uuid) {
return "agent:audio:id:" + uuid; return "agent:audio:id:" + uuid;
} }
/**
* 获取短信验证码的缓存key
*/
public static String getSMSValidateCodeKey(String phone) {
return "sms:Validate:Code:" + phone;
}
/**
* 获取短信验证码最后发送时间的缓存key
*/
public static String getSMSLastSendTimeKey(String phone) {
return "sms:Validate:Code:" + phone + ":last_send_time";
}
/**
* 获取短信验证码今日发送次数的缓存key
*/
public static String getSMSTodayCountKey(String phone) {
return "sms:Validate:Code:" + phone + ":today_count";
}
} }
@@ -6,12 +6,14 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import xiaozhi.common.utils.ResourcesUtils;
/** /**
* Redis工具类 * Redis工具类
@@ -23,6 +25,9 @@ public class RedisUtils {
@Resource @Resource
private RedisTemplate<String, Object> redisTemplate; private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ResourcesUtils resourceUtils;
/** /**
* 默认过期时长为24小时,单位:秒 * 默认过期时长为24小时,单位:秒
*/ */
@@ -40,6 +45,24 @@ public class RedisUtils {
*/ */
public final static long NOT_EXPIRE = -1L; public final static long NOT_EXPIRE = -1L;
public Long increment(String key, long expire) {
Long increment = redisTemplate.opsForValue().increment(key, 1L);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
return increment;
}
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key, 1L);
}
public Long decrement(String key) {
return redisTemplate.opsForValue().decrement(key, 1L);
}
public void set(String key, Object value, long expire) { public void set(String key, Object value, long expire) {
redisTemplate.opsForValue().set(key, value); redisTemplate.opsForValue().set(key, value);
if (expire != NOT_EXPIRE) { if (expire != NOT_EXPIRE) {
@@ -134,7 +157,7 @@ public class RedisUtils {
*/ */
public void emptyAll() { public void emptyAll() {
// Lua 脚本 FLUSHALL是redis清空所有库的命令 // Lua 脚本 FLUSHALL是redis清空所有库的命令
String luaScript ="redis.call('FLUSHALL')"; String luaScript =resourceUtils.loadString("lua/emptyAll.lua");
// 创建 DefaultRedisScript 对象 // 创建 DefaultRedisScript 对象
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>(); DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
@@ -147,4 +170,26 @@ public class RedisUtils {
} }
/**
* 获取在redis指定key的值,如果值为空,着设置key的默认值
* @param key redis的key
* @param defaultValue 默认值
* @param expiresInSecond 过期时间
* @return 返回key的值
*/
public String getKeyOrCreate(String key, String defaultValue,Long expiresInSecond) {
// Lua 脚本
String luaScript = resourceUtils.loadString("lua/getKeyOrCreate.lua");
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(String.class);
// 执行 Lua 脚本
List<String> keys = Collections.singletonList(key);
return redisTemplate.execute(redisScript, keys, defaultValue,expiresInSecond);
}
} }
@@ -1,6 +1,7 @@
package xiaozhi.common.service.impl; package xiaozhi.common.service.impl;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.StringUtils;
@@ -45,6 +47,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
* @param params 分页查询参数 * @param params 分页查询参数
* @param defaultOrderField 默认排序字段 * @param defaultOrderField 默认排序字段
* @param isAsc 排序方式 * @param isAsc 排序方式
* @see xiaozhi.common.constant.Constant
* params.put(Constant.PAGE, "1");
* params.put(Constant.LIMIT, "10");
* params.put(Constant.ORDER_FIELD, "field"); // 单个字段
* params.put(Constant.ORDER_FIELD, List.of("field1", "field2")); // 多个字段
* params.put(Constant.ORDER, "asc");
*/ */
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) { protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
// 分页参数 // 分页参数
@@ -65,28 +73,34 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
params.put(Constant.PAGE, page); params.put(Constant.PAGE, page);
// 排序字段 // 排序字段
String orderField = (String) params.get(Constant.ORDER_FIELD); Object orderField = params.get(Constant.ORDER_FIELD);
String order = (String) params.get(Constant.ORDER); String order = (String) params.get(Constant.ORDER);
// 前端字段排序 List<String> orderFields = new ArrayList<>();
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
if (Constant.ASC.equalsIgnoreCase(order)) { // 处理排序字段
return page.addOrder(OrderItem.asc(orderField)); if (orderField instanceof String) {
orderFields.add((String) orderField);
} else if (orderField instanceof List) {
orderFields.addAll((List<String>) orderField);
}
// 有排序字段则排序
if (CollectionUtils.isNotEmpty(orderFields)) {
if (StringUtils.isNotBlank(order) && Constant.ASC.equalsIgnoreCase(order)) {
return page.addOrder(OrderItem.ascs(orderFields.toArray(new String[0])));
} else { } else {
return page.addOrder(OrderItem.desc(orderField)); return page.addOrder(OrderItem.descs(orderFields.toArray(new String[0])));
} }
} }
// 没有排序字段,则不排序 // 没有排序字段,使用默认排序
if (StringUtils.isBlank(defaultOrderField)) { if (StringUtils.isNotBlank(defaultOrderField)) {
return page; if (isAsc) {
} page.addOrder(OrderItem.asc(defaultOrderField));
} else {
// 默认排序 page.addOrder(OrderItem.desc(defaultOrderField));
if (isAsc) { }
page.addOrder(OrderItem.asc(defaultOrderField));
} else {
page.addOrder(OrderItem.desc(defaultOrderField));
} }
return page; return page;
@@ -21,6 +21,8 @@ public class DateUtils {
* 时间格式(yyyy-MM-dd HH:mm:ss) * 时间格式(yyyy-MM-dd HH:mm:ss)
*/ */
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public final static String DATE_TIME_MILLIS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
/** /**
* 日期格式化 日期格式为:yyyy-MM-dd * 日期格式化 日期格式为:yyyy-MM-dd
@@ -63,6 +65,19 @@ public class DateUtils {
return null; return null;
} }
public static String getDateTimeNow() {
return getDateTimeNow(DATE_TIME_PATTERN);
}
public static String getDateTimeNow(String pattern) {
return format(new Date(), pattern);
}
public static String millsToSecond(long mills) {
return String.format("%.3f", mills / 1000.0);
}
/** /**
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒 * 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
* @param date * @param date
@@ -0,0 +1,44 @@
package xiaozhi.common.utils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import xiaozhi.common.exception.RenException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 资源处理工具
*/
@AllArgsConstructor
@Slf4j
@Component
public class ResourcesUtils {
private ResourceLoader resourceLoader;
/**
* 读取资源,返回字符串
* @param fileName 资源路径:resources下开始
* @return 字符串
*/
public String loadString(String fileName) {
Resource resource = resourceLoader.getResource("classpath:" + fileName);
StringBuilder luaScriptBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(resource.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
luaScriptBuilder.append(line).append("\n");
}
} catch (IOException e){
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
throw new RenException("读取资源失败");
}
return luaScriptBuilder.toString();
}
}
@@ -2,6 +2,7 @@ package xiaozhi.common.validator;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
@@ -45,4 +46,32 @@ public class ValidatorUtils {
throw new RenException(constraint.getMessage()); throw new RenException(constraint.getMessage());
} }
} }
/**
* 国际手机号正则表达式
* 要求必须带国际区号,格式:+[国家代码][手机号]
* 例如:
* - +8613800138000
* - +12345678900
* - +447123456789
*/
private static final String INTERNATIONAL_PHONE_REGEX = "^\\+[1-9]\\d{0,3}[1-9]\\d{4,14}$";
/**
* 校验手机号是否有效
* 要求必须带国际区号,格式:+[国家代码][手机号]
* 例如:+8613800138000
*
* @param phone 手机号
* @return boolean
*/
public static boolean isValidPhone(String phone) {
if (phone == null || phone.isEmpty()) {
return false;
}
// 验证必须带国际区号的手机号格式
Pattern pattern = Pattern.compile(INTERNATIONAL_PHONE_REGEX);
return pattern.matcher(phone).matches();
}
} }
@@ -28,4 +28,6 @@ public class AgentChatHistoryReportDTO {
private String content; private String content;
@Schema(description = "base64编码的opus音频数据", example = "") @Schema(description = "base64编码的opus音频数据", example = "")
private String audioBase64; private String audioBase64;
@Schema(description = "上报时间,十位时间戳,空时默认使用当前时间", example = "1745657732")
private Long reportTime;
} }
@@ -27,4 +27,4 @@ public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
* @return 音频数据 * @return 音频数据
*/ */
byte[] getAudio(String audioId); byte[] getAudio(String audioId);
} }
@@ -47,7 +47,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
public Boolean report(AgentChatHistoryReportDTO report) { public Boolean report(AgentChatHistoryReportDTO report) {
String macAddress = report.getMacAddress(); String macAddress = report.getMacAddress();
Byte chatType = report.getChatType(); Byte chatType = report.getChatType();
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType); Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000 : System.currentTimeMillis();
log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis);
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报 // 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress); AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
@@ -59,10 +60,10 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
String agentId = agentEntity.getId(); String agentId = agentEntity.getId();
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) { if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
saveChatText(report, agentId, macAddress, null); saveChatText(report, agentId, macAddress, null, reportTimeMillis);
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) { } else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
String audioId = saveChatAudio(report); String audioId = saveChatAudio(report);
saveChatText(report, agentId, macAddress, audioId); saveChatText(report, agentId, macAddress, audioId, reportTimeMillis);
} }
// 更新设备最后对话时间 // 更新设备最后对话时间
@@ -92,8 +93,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
/** /**
* 组装上报数据 * 组装上报数据
*/ */
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) { private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId, Long reportTime) {
// 构建聊天记录实体 // 构建聊天记录实体
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder() AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
.macAddress(macAddress) .macAddress(macAddress)
@@ -102,6 +102,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
.chatType(report.getChatType()) .chatType(report.getChatType())
.content(report.getContent()) .content(report.getContent())
.audioId(audioId) .audioId(audioId)
.createdAt(new Date(reportTime))
// NOTE(haotian): 2025/5/26 updateAt可以不设置,重点是createAt,而且这样可以看到上报延迟
.build(); .build();
// 保存数据 // 保存数据
@@ -31,4 +31,4 @@ public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao,
AgentChatAudioEntity entity = getById(audioId); AgentChatAudioEntity entity = getById(audioId);
return entity != null ? entity.getAudio() : null; return entity != null ? entity.getAudio() : null;
} }
} }
@@ -251,6 +251,7 @@ public class ConfigServiceImpl implements ConfigService {
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" }; String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" };
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId }; String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId };
String intentLLMModelId = null; String intentLLMModelId = null;
String memLocalShortLLMModelId = null;
for (int i = 0; i < modelIds.length; i++) { for (int i = 0; i < modelIds.length; i++) {
if (modelIds[i] == null) { if (modelIds[i] == null) {
@@ -269,7 +270,7 @@ public class ConfigServiceImpl implements ConfigService {
Map<String, Object> map = (Map<String, Object>) model.getConfigJson(); Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
if ("intent_llm".equals(map.get("type"))) { if ("intent_llm".equals(map.get("type"))) {
intentLLMModelId = (String) map.get("llm"); intentLLMModelId = (String) map.get("llm");
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) { if (StringUtils.isNotBlank(intentLLMModelId) && intentLLMModelId.equals(llmModelId)) {
intentLLMModelId = null; intentLLMModelId = null;
} }
} }
@@ -281,10 +282,31 @@ public class ConfigServiceImpl implements ConfigService {
} }
} }
} }
if ("Memory".equals(modelTypes[i])) {
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
if ("mem_local_short".equals(map.get("type"))) {
memLocalShortLLMModelId = (String) map.get("llm");
if (StringUtils.isNotBlank(memLocalShortLLMModelId)
&& memLocalShortLLMModelId.equals(llmModelId)) {
memLocalShortLLMModelId = null;
}
}
}
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型 // 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
if ("LLM".equals(modelTypes[i]) && intentLLMModelId != null) { if ("LLM".equals(modelTypes[i])) {
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache); if (StringUtils.isNotBlank(intentLLMModelId)) {
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson()); if (!typeConfig.containsKey(intentLLMModelId)) {
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
}
}
if (StringUtils.isNotBlank(memLocalShortLLMModelId)) {
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
ModelConfigEntity memLocalShortLLM = modelConfigService
.getModelById(memLocalShortLLMModelId, isCache);
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
}
}
} }
} }
result.put(modelTypes[i], typeConfig); result.put(modelTypes[i], typeConfig);
@@ -142,7 +142,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
if (deviceById == null) { if (deviceById == null) {
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware(); DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
firmware.setVersion(deviceReport.getApplication().getVersion()); firmware.setVersion(deviceReport.getApplication().getVersion());
firmware.setUrl("http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"); firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
response.setFirmware(firmware); response.setFirmware(firmware);
} else { } else {
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息 // 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
@@ -378,7 +378,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
} }
firmware.setVersion(ota == null ? currentVersion : ota.getVersion()); firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
firmware.setUrl(downloadUrl == null ? "" : downloadUrl); firmware.setUrl(downloadUrl == null ? Constant.INVALID_FIRMWARE_URL : downloadUrl);
return firmware; return firmware;
} }
@@ -0,0 +1,68 @@
package xiaozhi.modules.model.controller;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelProviderService;
@AllArgsConstructor
@RestController
@RequestMapping("/models/provider")
@Tag(name = "模型供应器")
public class ModelProviderController {
private final ModelProviderService modelProviderService;
@GetMapping
@Operation(summary = "获取模型供应器列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<PageData<ModelProviderDTO>> getListPage(ModelProviderDTO modelProviderDTO,
@RequestParam(required = true, defaultValue = "0") String page,
@RequestParam(required = true, defaultValue = "10") String limit) {
return new Result<PageData<ModelProviderDTO>>()
.ok(modelProviderService.getListPage(modelProviderDTO, page, limit));
}
@PostMapping
@Operation(summary = "新增模型供应器")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelProviderDTO> add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) {
ModelProviderDTO resp = modelProviderService.add(modelProviderDTO);
return new Result<ModelProviderDTO>().ok(resp);
}
@PutMapping
@Operation(summary = "修改模型供应器")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelProviderDTO> edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) {
ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO);
return new Result<ModelProviderDTO>().ok(resp);
}
@PostMapping("/delete")
@Operation(summary = "删除模型供应器")
@RequiresPermissions("sys:role:superAdmin")
@Parameter(name = "ids", description = "ID数组", required = true)
public Result<Void> delete(@RequestBody List<String> ids) {
modelProviderService.delete(ids);
return new Result<>();
}
}
@@ -8,29 +8,37 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import xiaozhi.common.validator.group.UpdateGroup;
@Data @Data
@Schema(description = "模型供应器/商") @Schema(description = "模型供应器/商")
public class ModelProviderDTO implements Serializable { public class ModelProviderDTO implements Serializable {
// @Schema(description = "主键")
// @Schema(description = "主键") @NotBlank(message = "id不能为空", groups = UpdateGroup.class)
// private Long id; private String id;
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)") @Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
@NotBlank(message = "modelType不能为空")
private String modelType; private String modelType;
@Schema(description = "供应器类型") @Schema(description = "供应器类型")
@NotBlank(message = "providerCode不能为空")
private String providerCode; private String providerCode;
@Schema(description = "供应器名称") @Schema(description = "供应器名称")
@NotBlank(message = "name不能为空")
private String name; private String name;
@Schema(description = "供应器字段列表(JSON格式)") @Schema(description = "供应器字段列表(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = JacksonTypeHandler.class)
@NotBlank(message = "fields(JSON格式)不能为空")
private String fields; private String fields;
@Schema(description = "排序") @Schema(description = "排序")
@NotNull(message = "sort不能为空")
private Integer sort; private Integer sort;
@Schema(description = "更新者") @Schema(description = "更新者")
@@ -3,10 +3,8 @@ package xiaozhi.modules.model.entity;
import java.util.Date; import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -30,7 +28,6 @@ public class ModelProviderEntity {
private String name; private String name;
@Schema(description = "供应器字段列表(JSON格式)") @Schema(description = "供应器字段列表(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
private String fields; private String fields;
@Schema(description = "排序") @Schema(description = "排序")
@@ -2,8 +2,8 @@ package xiaozhi.modules.model.service;
import java.util.List; import java.util.List;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
public interface ModelProviderService { public interface ModelProviderService {
@@ -11,11 +11,15 @@ public interface ModelProviderService {
List<ModelProviderDTO> getListByModelType(String modelType); List<ModelProviderDTO> getListByModelType(String modelType);
ModelProviderDTO add(ModelProviderEntity modelProviderEntity); ModelProviderDTO add(ModelProviderDTO modelProviderDTO);
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity); ModelProviderDTO edit(ModelProviderDTO modelProviderDTO);
void delete(); void delete(String id);
void delete(List<String> id);
PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit);
List<ModelProviderDTO> getList(String modelType, String provideCode); List<ModelProviderDTO> getList(String modelType, String provideCode);
} }
@@ -1,19 +1,29 @@
package xiaozhi.modules.model.service.impl; package xiaozhi.modules.model.service.impl;
import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.json.JSONArray;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.model.dao.ModelProviderDao; import xiaozhi.modules.model.dao.ModelProviderDao;
import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity; import xiaozhi.modules.model.entity.ModelProviderEntity;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@@ -32,18 +42,78 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
} }
@Override @Override
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) { public PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) {
return null;
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, page);
params.put(Constant.LIMIT, limit);
params.put(Constant.ORDER_FIELD, List.of("model_type", "sort"));
params.put(Constant.ORDER, "asc");
IPage<ModelProviderEntity> pageParam = getPage(params, null, true);
QueryWrapper<ModelProviderEntity> wrapper = new QueryWrapper<ModelProviderEntity>();
if (StringUtils.isNotBlank(modelProviderDTO.getModelType())) {
wrapper.eq("model_type", modelProviderDTO.getModelType());
}
if (StringUtils.isNotBlank(modelProviderDTO.getName())) {
wrapper.and(w -> w.like("name", modelProviderDTO.getName())
.or()
.like("provider_code", modelProviderDTO.getName()));
}
return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class);
}
public static void main(String[] args) {
String jsonString = "\"[]\"";
JSONArray jsonArray = new JSONArray(jsonString);
System.out.println("字符串转 JSONArray: " + jsonArray.toString());
} }
@Override @Override
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) { public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) {
return null; UserDetail user = SecurityUser.getUser();
modelProviderDTO.setCreator(user.getId());
modelProviderDTO.setUpdater(user.getId());
modelProviderDTO.setCreateDate(new Date());
modelProviderDTO.setUpdateDate(new Date());
// 去除Fields左右的双引号
modelProviderDTO.setFields(modelProviderDTO.getFields());
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
if (modelProviderDao.insert(entity) == 0) {
throw new RenException("新增数据失败");
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
} }
@Override @Override
public void delete() { public ModelProviderDTO edit(ModelProviderDTO modelProviderDTO) {
UserDetail user = SecurityUser.getUser();
modelProviderDTO.setUpdater(user.getId());
modelProviderDTO.setUpdateDate(new Date());
if (modelProviderDao
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
throw new RenException("修改数据失败");
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
}
@Override
public void delete(String id) {
if (modelProviderDao.deleteById(id) == 0) {
throw new RenException("删除数据失败");
}
}
@Override
public void delete(List<String> ids) {
if (modelProviderDao.deleteBatchIds(ids) == 0) {
throw new RenException("删除数据失败");
}
} }
@Override @Override
@@ -80,9 +80,11 @@ public class ShiroConfig {
filterMap.put("/doc.html", "anon"); filterMap.put("/doc.html", "anon");
filterMap.put("/favicon.ico", "anon"); filterMap.put("/favicon.ico", "anon");
filterMap.put("/user/captcha", "anon"); filterMap.put("/user/captcha", "anon");
filterMap.put("/user/smsVerification", "anon");
filterMap.put("/user/login", "anon"); filterMap.put("/user/login", "anon");
filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/pub-config", "anon");
filterMap.put("/user/register", "anon"); filterMap.put("/user/register", "anon");
filterMap.put("/user/retrieve-password", "anon");
// 将config路径使用server服务过滤器 // 将config路径使用server服务过滤器
filterMap.put("/config/**", "server"); filterMap.put("/config/**", "server");
filterMap.put("/agent/chat-history/report", "server"); filterMap.put("/agent/chat-history/report", "server");
@@ -1,7 +1,9 @@
package xiaozhi.modules.security.controller; package xiaozhi.modules.security.controller;
import java.io.IOException; import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -24,13 +26,18 @@ import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils; import xiaozhi.common.validator.AssertUtils;
import xiaozhi.common.validator.ValidatorUtils; import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.security.dto.LoginDTO; import xiaozhi.modules.security.dto.LoginDTO;
import xiaozhi.modules.security.dto.SmsVerificationDTO;
import xiaozhi.modules.security.password.PasswordUtils; import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.service.CaptchaService; import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.security.service.SysUserTokenService; import xiaozhi.modules.security.service.SysUserTokenService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.dto.PasswordDTO; import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysDictDataService;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.SysDictDataItem;
/** /**
* 登录控制层 * 登录控制层
@@ -43,24 +50,43 @@ public class LoginController {
private final SysUserService sysUserService; private final SysUserService sysUserService;
private final SysUserTokenService sysUserTokenService; private final SysUserTokenService sysUserTokenService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final SysParamsService sysParamsService;
private final SysDictDataService sysDictDataService;
@GetMapping("/captcha") @GetMapping("/captcha")
@Operation(summary = "验证码") @Operation(summary = "验证码")
public void captcha(HttpServletResponse response, String uuid) throws IOException { public void captcha(HttpServletResponse response, String uuid) throws IOException {
// uuid不能为空 // uuid不能为空
AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL); AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL);
// 生成验证码 // 生成验证码
captchaService.create(response, uuid); captchaService.create(response, uuid);
} }
@PostMapping("/smsVerification")
@Operation(summary = "短信验证码")
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
// 验证图形验证码
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误");
}
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
}
// 发送短信验证码
captchaService.sendSMSValidateCode(dto.getPhone());
return new Result<>();
}
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "登录") @Operation(summary = "登录")
public Result<TokenDTO> login(@RequestBody LoginDTO login) { public Result<TokenDTO> login(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码 // 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha()); boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) { if (!validate) {
throw new RenException("验证码错误,请重新获取"); throw new RenException("图形验证码错误,请重新获取");
} }
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
@@ -81,11 +107,29 @@ public class LoginController {
if (!sysUserService.getAllowUserRegister()) { if (!sysUserService.getAllowUserRegister()) {
throw new RenException("当前不允许普通用户注册"); throw new RenException("当前不允许普通用户注册");
} }
// 验证是否正确输入验证码 // 是否开启手机注册
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha()); Boolean isMobileRegister = sysParamsService
if (!validate) { .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
throw new RenException("验证码错误,请重新获取"); boolean validate;
if (isMobileRegister) {
// 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
if (!validPhone) {
throw new RenException("用户名不是手机号码,请重新输入");
}
// 验证短信验证码是否正常
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("图形验证码错误,请重新获取");
}
} }
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null) { if (userDTO != null) {
@@ -96,7 +140,6 @@ public class LoginController {
userDTO.setPassword(login.getPassword()); userDTO.setPassword(login.getPassword());
sysUserService.save(userDTO); sysUserService.save(userDTO);
return new Result<>(); return new Result<>();
} }
@GetMapping("/info") @GetMapping("/info")
@@ -118,12 +161,54 @@ public class LoginController {
return new Result<>(); return new Result<>();
} }
@PutMapping("/retrieve-password")
@Operation(summary = "找回密码")
public Result<?> retrievePassword(@RequestBody RetrievePasswordDTO dto) {
// 是否开启手机注册
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用找回密码功能");
}
// 判断非空
ValidatorUtils.validateEntity(dto);
// 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
if (!validPhone) {
throw new RenException("输入的手机号码格式不正确");
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
if (userDTO == null) {
throw new RenException("输入的手机号码未注册");
}
// 验证短信验证码是否正常
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
// 判断是否通过验证
if (!validate) {
throw new RenException("输入的手机验证码错误");
}
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
return new Result<>();
}
@GetMapping("/pub-config") @GetMapping("/pub-config")
@Operation(summary = "公共配置") @Operation(summary = "公共配置")
public Result<Map<String, Object>> pubConfig() { public Result<Map<String, Object>> pubConfig() {
Map<String, Object> config = new HashMap<>(); Map<String, Object> config = new HashMap<>();
config.put("enableMobileRegister", sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class));
config.put("version", Constant.VERSION); config.put("version", Constant.VERSION);
config.put("year", "©" + Calendar.getInstance().get(Calendar.YEAR));
config.put("allowUserRegister", sysUserService.getAllowUserRegister()); config.put("allowUserRegister", sysUserService.getAllowUserRegister());
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(Constant.DictType.MOBILE_AREA.getValue());
config.put("mobileAreaList", list);
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));
return new Result<Map<String, Object>>().ok(config); return new Result<Map<String, Object>>().ok(config);
} }
} }
@@ -25,6 +25,9 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "{sysuser.captcha.require}") @NotBlank(message = "{sysuser.captcha.require}")
private String captcha; private String captcha;
@Schema(description = "手机验证码")
private String mobileCaptcha;
@Schema(description = "唯一标识") @Schema(description = "唯一标识")
@NotBlank(message = "{sysuser.uuid.require}") @NotBlank(message = "{sysuser.uuid.require}")
private String captchaId; private String captchaId;
@@ -0,0 +1,28 @@
package xiaozhi.modules.security.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 短信验证码请求DTO
*/
@Data
@Schema(description = "短信验证码请求")
public class SmsVerificationDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "手机号码")
@NotBlank(message = "{sysuser.username.require}")
private String phone;
@Schema(description = "验证码")
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "唯一标识")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;
}
@@ -18,10 +18,28 @@ public interface CaptchaService {
/** /**
* 验证码效验 * 验证码效验
* *
* @param uuid uuid * @param uuid uuid
* @param code 验证码 * @param code 验证码
* @param delete 是否删除验证码
* @return true:成功 false:失败 * @return true:成功 false:失败
*/ */
boolean validate(String uuid, String code); boolean validate(String uuid, String code, Boolean delete);
/**
* 发送短信验证码
*
* @param phone 手机
*/
void sendSMSValidateCode(String phone);
/**
* 验证短信验证码
*
* @param phone 手机
* @param code 验证码
* @param delete 是否删除验证码
* @return true:成功 false:失败
*/
boolean validateSMSValidateCode(String phone, String code, Boolean delete);
} }
@@ -1,6 +1,7 @@
package xiaozhi.modules.security.service.impl; package xiaozhi.modules.security.service.impl;
import java.io.IOException; import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -14,9 +15,13 @@ import com.wf.captcha.base.Captcha;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.security.service.CaptchaService; import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.sms.service.SmsService;
import xiaozhi.modules.sys.service.SysParamsService;
/** /**
* 验证码 * 验证码
@@ -25,6 +30,10 @@ import xiaozhi.modules.security.service.CaptchaService;
public class CaptchaServiceImpl implements CaptchaService { public class CaptchaServiceImpl implements CaptchaService {
@Resource @Resource
private RedisUtils redisUtils; private RedisUtils redisUtils;
@Resource
private SmsService smsService;
@Resource
private SysParamsService sysParamsService;
@Value("${renren.redis.open}") @Value("${renren.redis.open}")
private boolean open; private boolean open;
/** /**
@@ -51,12 +60,12 @@ public class CaptchaServiceImpl implements CaptchaService {
} }
@Override @Override
public boolean validate(String uuid, String code) { public boolean validate(String uuid, String code, Boolean delete) {
if (StringUtils.isBlank(code)) { if (StringUtils.isBlank(code)) {
return false; return false;
} }
// 获取验证码 // 获取验证码
String captcha = getCache(uuid); String captcha = getCache(uuid, delete);
// 效验成功 // 效验成功
if (code.equalsIgnoreCase(captcha)) { if (code.equalsIgnoreCase(captcha)) {
@@ -66,21 +75,97 @@ public class CaptchaServiceImpl implements CaptchaService {
return false; return false;
} }
@Override
public void sendSMSValidateCode(String phone) {
// 检查发送间隔
String lastSendTimeKey = RedisKeys.getSMSLastSendTimeKey(phone);
// 获取是否发送过,如果没有设置最后发送时间(60秒)
String lastSendTime = redisUtils
.getKeyOrCreate(lastSendTimeKey,
String.valueOf(System.currentTimeMillis()), 60L);
if (lastSendTime != null) {
long lastSendTimeLong = Long.parseLong(lastSendTime);
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - lastSendTimeLong;
if (timeDiff < 60000) {
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
}
}
// 检查今日发送次数
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
Integer todayCount = (Integer) redisUtils.get(todayCountKey);
if (todayCount == null) {
todayCount = 0;
}
// 获取最大发送次数限制
Integer maxSendCount = sysParamsService.getValueObject(
Constant.SysMSMParam.SERVER_SMS_MAX_SEND_COUNT.getValue(),
Integer.class);
if (maxSendCount == null) {
maxSendCount = 5; // 默认值
}
if (todayCount >= maxSendCount) {
throw new RenException("今日发送次数已达上限");
}
String key = RedisKeys.getSMSValidateCodeKey(phone);
String validateCodes = generateValidateCode(6);
// 设置验证码
setCache(key, validateCodes);
// 更新今日发送次数
if (todayCount == 0) {
redisUtils.increment(todayCountKey, RedisUtils.DEFAULT_EXPIRE);
} else {
redisUtils.increment(todayCountKey);
}
// 发送验证码短信
smsService.sendVerificationCodeSms(phone, validateCodes);
}
@Override
public boolean validateSMSValidateCode(String phone, String code, Boolean delete) {
String key = RedisKeys.getSMSValidateCodeKey(phone);
return validate(key, code, delete);
}
/**
* 生成指定数量的随机数验证码
*
* @param length 数量
* @return 随机码
*/
private String generateValidateCode(Integer length) {
String chars = "0123456789"; // 字符范围可以自定义:数字
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
private void setCache(String key, String value) { private void setCache(String key, String value) {
if (open) { if (open) {
key = RedisKeys.getCaptchaKey(key); key = RedisKeys.getCaptchaKey(key);
// 设置5分钟过期
redisUtils.set(key, value, 300); redisUtils.set(key, value, 300);
} else { } else {
localCache.put(key, value); localCache.put(key, value);
} }
} }
private String getCache(String key) { private String getCache(String key, Boolean delete) {
if (open) { if (open) {
key = RedisKeys.getCaptchaKey(key); key = RedisKeys.getCaptchaKey(key);
String captcha = (String) redisUtils.get(key); String captcha = (String) redisUtils.get(key);
// 删除验证码 // 删除验证码
if (captcha != null) { if (captcha != null && delete) {
redisUtils.delete(key); redisUtils.delete(key);
} }
@@ -0,0 +1,17 @@
package xiaozhi.modules.sms.service;
/**
* 短信服务的方法定义接口
*
* @author zjy
* @since 2025-05-12
*/
public interface SmsService {
/**
* 发送验证码短信
* @param phone 手机号码
* @param VerificationCode 验证码
*/
void sendVerificationCodeSms(String phone, String VerificationCode) ;
}
@@ -0,0 +1,76 @@
package xiaozhi.modules.sms.service.imp;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.sms.service.SmsService;
import xiaozhi.modules.sys.service.SysParamsService;
@Service
@AllArgsConstructor
@Slf4j
public class ALiYunSmsService implements SmsService {
private final SysParamsService sysParamsService;
private final RedisUtils redisUtils;
@Override
public void sendVerificationCodeSms(String phone, String VerificationCode) {
Client client = createClient();
String SignName = sysParamsService.getValue(Constant.SysMSMParam
.ALIYUN_SMS_SIGN_NAME.getValue(),true);
String TemplateCode = sysParamsService.getValue(Constant.SysMSMParam
.ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE.getValue(),true);
try {
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(SignName)
.setTemplateCode(TemplateCode)
.setPhoneNumbers(phone)
.setTemplateParam(String.format("{\"code\":\"%s\"}", VerificationCode));
RuntimeOptions runtime = new RuntimeOptions();
// 复制代码运行请自行打印 API 的返回值
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
log.info("发送短信响应的requestID: {}", sendSmsResponse.getBody().getRequestId());
} catch (Exception e) {
// 如果发送失败了退还这次发送数
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
redisUtils.delete(todayCountKey);
// 错误 message
log.error(e.getMessage());
throw new RenException("短信发送失败");
}
}
/**
* 创建阿里云连接
* @return 返回连接对象
*/
private Client createClient(){
String ACCESS_KEY_ID = sysParamsService.getValue(Constant.SysMSMParam
.ALIYUN_SMS_ACCESS_KEY_ID.getValue(),true);
String ACCESS_KEY_SECRET = sysParamsService.getValue(Constant.SysMSMParam
.ALIYUN_SMS_ACCESS_KEY_SECRET.getValue(),true);
try {
Config config = new Config()
.setAccessKeyId(ACCESS_KEY_ID)
.setAccessKeySecret(ACCESS_KEY_SECRET);
// 配置 Endpoint。中国站请使用dysmsapi.aliyuncs.com
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
}catch (Exception e){
// 错误 message
log.error(e.getMessage());
throw new RenException("短信连接建立失败");
}
}
}
@@ -0,0 +1,121 @@
package xiaozhi.modules.sys.controller;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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.socket.WebSocketHttpHeaders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
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.annotation.LogOperation;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
import xiaozhi.modules.sys.dto.ServerActionPayloadDTO;
import xiaozhi.modules.sys.dto.ServerActionResponseDTO;
import xiaozhi.modules.sys.enums.ServerActionEnum;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.utils.WebSocketClientManager;
/**
* 服务端管理控制器
*/
@RestController
@RequestMapping("/admin/server")
@Tag(name = "服务端管理")
@AllArgsConstructor
public class ServerSideManageController {
private final SysParamsService sysParamsService;
private static final ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
// 忽略json字符串中存在,但pojo中不存在对应字段的情况
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Operation(summary = "获取Ws服务端列表")
@GetMapping("/server-list")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<String>> getWsServerList() {
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsText)) {
return new Result<List<String>>().ok(Collections.emptyList());
}
return new Result<List<String>>().ok(Arrays.asList(wsText.split(";")));
}
@Operation(summary = "通知python服务端更新配置")
@PostMapping("/emit-action")
@LogOperation("通知python服务端更新配置")
@RequiresPermissions("sys:role:superAdmin")
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
if (emitSeverActionDTO.getAction() == null) {
throw new RenException("无效服务端操作");
}
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsText)) {
throw new RenException("未配置服务端WebSocket地址");
}
String targetWs = emitSeverActionDTO.getTargetWs();
String[] wsList = wsText.split(";");
// 找到需要发起的
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
throw new RenException("目标WebSocket地址不存在");
}
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
}
private Boolean emitServerActionByWs(String targetWsUri, ServerActionEnum actionEnum) {
if (StringUtils.isBlank(targetWsUri) || actionEnum == null) {
return false;
}
String serverSK = sysParamsService.getValue(Constant.SERVER_SECRET, true);
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
headers.add("device-id", UUID.randomUUID().toString());
headers.add("client-id", UUID.randomUUID().toString());
try (WebSocketClientManager client = new WebSocketClientManager.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.maxSessionDuration(120, TimeUnit.SECONDS)
.uri(targetWsUri)
.headers(headers)
.build()) {
// 如果连接成功则发送一个json数据包并等待服务端响应
client.sendJson(
ServerActionPayloadDTO.build(
actionEnum,
Map.of("secret", serverSK)));
// 等待服务端响应并持续监听信息
client.listener((jsonText) -> {
if (StringUtils.isBlank(jsonText)) {
return false;
}
try {
ServerActionResponseDTO response = objectMapper.readValue(jsonText, ServerActionResponseDTO.class);
Boolean isSuccess = ServerActionResponseDTO.isSuccess(response);
return isSuccess;
} catch (JsonProcessingException e) {
return false;
}
});
} catch (Exception e) {
// 捕获全部错误,由全局异常处理器返回
throw new RenException("WebSocket连接失败或连接超时");
}
return true;
}
}
@@ -59,7 +59,7 @@ public class SysParamsController {
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"), @Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"),
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"), @Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"),
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"), @Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"),
@Parameter(name = "paramCode", description = "参数编码", in = ParameterIn.QUERY, ref = "String") @Parameter(name = "paramCode", description = "参数编码或参数备注", in = ParameterIn.QUERY, ref = "String")
}) })
@RequiresPermissions("sys:role:superAdmin") @RequiresPermissions("sys:role:superAdmin")
public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) { public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
@@ -111,7 +111,7 @@ public class SysParamsController {
/** /**
* 验证WebSocket地址列表 * 验证WebSocket地址列表
* *
* @param urls WebSocket地址列表,以分号分隔 * @param urls WebSocket地址列表,以分号分隔
* @return 验证结果 * @return 验证结果
*/ */
@@ -143,6 +143,19 @@ public class SysParamsController {
} }
} }
@PostMapping("/delete")
@Operation(summary = "删除")
@LogOperation("删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@RequestBody String[] ids) {
// 效验数据
AssertUtils.isArrayEmpty(ids, "id");
sysParamsService.delete(ids);
configService.getConfig(false);
return new Result<Void>();
}
/** /**
* 验证OTA地址 * 验证OTA地址
*/ */
@@ -182,17 +195,4 @@ public class SysParamsController {
throw new RenException("OTA接口验证失败:" + e.getMessage()); throw new RenException("OTA接口验证失败:" + e.getMessage());
} }
} }
@PostMapping("/delete")
@Operation(summary = "删除")
@LogOperation("删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@RequestBody String[] ids) {
// 效验数据
AssertUtils.isArrayEmpty(ids, "id");
sysParamsService.delete(ids);
configService.getConfig(false);
return new Result<Void>();
}
} }
@@ -0,0 +1,26 @@
package xiaozhi.modules.sys.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import xiaozhi.modules.sys.enums.ServerActionEnum;
/**
* 发送python服务端操作DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmitSeverActionDTO
{
@Schema(description = "目标ws地址")
@NotEmpty(message = "目标ws地址不能为空")
private String targetWs;
@Schema(description = "指定操作")
@NotNull(message = "操作不能为空")
private ServerActionEnum action;
}
@@ -0,0 +1,30 @@
package xiaozhi.modules.sys.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 找回密码DTO
*/
@Data
@Schema(description = "找回密码")
public class RetrievePasswordDTO implements Serializable {
@Schema(description = "手机号码")
@NotBlank(message = "{sysuser.password.require}")
private String phone;
@Schema(description = "验证码")
@NotBlank(message = "{sysuser.password.require}")
private String code;
@Schema(description = "新密码")
@NotBlank(message = "{sysuser.password.require}")
private String password;
}
@@ -0,0 +1,36 @@
package xiaozhi.modules.sys.dto;
import lombok.Data;
import xiaozhi.modules.sys.enums.ServerActionEnum;
import java.util.Map;
/**
* 服务端动作DTO
*/
@Data
public class ServerActionPayloadDTO
{
/**
* 类型(智控台发往服务端的都是server)
*/
private String type;
/**
* 动作
*/
private ServerActionEnum action;
/**
* 内容
*/
private Map<String, Object> content;
public static ServerActionPayloadDTO build(ServerActionEnum action, Map<String, Object> content) {
ServerActionPayloadDTO serverActionPayloadDTO = new ServerActionPayloadDTO();
serverActionPayloadDTO.setAction(action);
serverActionPayloadDTO.setContent(content);
serverActionPayloadDTO.setType("server");
return serverActionPayloadDTO;
}
// 私有化
private ServerActionPayloadDTO() {}
}
@@ -0,0 +1,34 @@
package xiaozhi.modules.sys.dto;
import lombok.Data;
import xiaozhi.modules.sys.enums.ServerActionResponseEnum;
import java.util.Map;
/**
* 服务端动作响应体
*/
@Data
public class ServerActionResponseDTO
{
private ServerActionResponseEnum status;
private String message;
private String type;
private Map<String, Object> content; // 后续这个字段可以移除,并把这个类作为基类,针对业务写自己的content类型
public static final String DEFAULT_TYPE_FORM_SERVER = "server";
public static Boolean isSuccess(ServerActionResponseDTO actionResponseDTO) {
System.out.println(actionResponseDTO);
if (actionResponseDTO == null) {
return false;
}
if (actionResponseDTO.getStatus() == null || !actionResponseDTO.getStatus().equals(ServerActionResponseEnum.SUCCESS)) {
return false;
}
Object actionType = actionResponseDTO.getContent().get("action");
if (actionType == null) {
return false;
}
return actionResponseDTO.getType() != null && actionResponseDTO.getType().equals(DEFAULT_TYPE_FORM_SERVER);
}
}
@@ -0,0 +1,40 @@
package xiaozhi.modules.sys.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import xiaozhi.common.exception.RenException;
/**
* 服务端动作枚举
*/
public enum ServerActionEnum
{
RESTART("restart"),
UPDATE_CONFIG("update_config");
private final String value;
ServerActionEnum(String value)
{
this.value = value;
}
@JsonValue
public String getValue()
{
return value;
}
@JsonCreator
public static ServerActionEnum fromValue(String value)
{
for (ServerActionEnum action : ServerActionEnum.values())
{
if (action.value.equalsIgnoreCase(value))
{
return action;
}
}
return null;
}
}
@@ -0,0 +1,46 @@
package xiaozhi.modules.sys.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/**
* 服务端调用响应枚举
*/
public enum ServerActionResponseEnum
{
SUCCESS("success"), FAIL("fail");
private final String value;
ServerActionResponseEnum(String value) {
this.value = value;
}
@JsonValue
public String getValue()
{
return value;
}
@JsonCreator
public static ServerActionResponseEnum fromValue(String value) {
ServerActionResponseEnum byValue = getByValue(value);
if (byValue == null) {
throw new IllegalArgumentException("Unknown enum value: " + value);
}
return byValue;
}
public static ServerActionResponseEnum getByValue(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
for (ServerActionResponseEnum action : ServerActionResponseEnum.values()) {
if (action.value.equals(value)) {
return action;
}
}
return null;
}
}
@@ -1,5 +1,6 @@
package xiaozhi.modules.sys.service.impl; package xiaozhi.modules.sys.service.impl;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -55,7 +56,9 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
QueryWrapper<SysParamsEntity> wrapper = new QueryWrapper<>(); QueryWrapper<SysParamsEntity> wrapper = new QueryWrapper<>();
wrapper.eq("param_type", 1); wrapper.eq("param_type", 1);
wrapper.like(StringUtils.isNotBlank(paramCode), "param_code", paramCode); wrapper.nested(StringUtils.isNotBlank(paramCode), i -> i.like("param_code", paramCode)
.or()
.like("remark", paramCode));
return wrapper; return wrapper;
} }
@@ -82,7 +85,7 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void update(SysParamsDTO dto) { public void update(SysParamsDTO dto) {
validateParamValue(dto); validateParamValue(dto);
detectingSMSParameters(dto.getParamCode(), dto.getParamValue());
SysParamsEntity entity = ConvertUtils.sourceToTarget(dto, SysParamsEntity.class); SysParamsEntity entity = ConvertUtils.sourceToTarget(dto, SysParamsEntity.class);
updateById(entity); updateById(entity);
@@ -204,4 +207,41 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
updateValueByCode(Constant.SERVER_SECRET, newSecret); updateValueByCode(Constant.SERVER_SECRET, newSecret);
} }
} }
/**
* 检测短信参数是否符合要求
*
* @param paramCode 参数编码
* @param paramValue 参数值
* @return 是否通过
*/
private boolean detectingSMSParameters(String paramCode, String paramValue) {
// 判断是否是开启手机注册的参数编码,如果不是参数编码,着不需要检测其他短信参数,直接返回true
if (!Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue().equals(paramCode)) {
return true;
}
// 判断是否为关闭,如果是关闭短信注册,着不需要检测其他短信参数,直接返回true
if ("false".equalsIgnoreCase(paramValue)) {
return true;
}
// 检测短信关联参数是否为空
ArrayList<String> list = new ArrayList<String>();
list.add(Constant.SysMSMParam.SERVER_SMS_MAX_SEND_COUNT.getValue());
list.add(Constant.SysMSMParam.ALIYUN_SMS_ACCESS_KEY_ID.getValue());
list.add(Constant.SysMSMParam.ALIYUN_SMS_ACCESS_KEY_SECRET.getValue());
list.add(Constant.SysMSMParam.ALIYUN_SMS_SIGN_NAME.getValue());
list.add(Constant.SysMSMParam.ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE.getValue());
StringBuilder str = new StringBuilder();
list.forEach(item -> {
if (!StringUtils.isNoneBlank(item)) {
str.append(",").append(item);
}
});
if (!str.isEmpty()) {
String promptStr = "%s这些参数不可以为空";
String substring = str.substring(1, str.length());
throw new RenException(promptStr.formatted(substring));
}
return true;
}
} }
@@ -133,6 +133,10 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void changePasswordDirectly(Long userId, String password) { public void changePasswordDirectly(Long userId, String password) {
// 新密码强度
if (!isStrongPassword(password)) {
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
}
SysUserEntity sysUserEntity = new SysUserEntity(); SysUserEntity sysUserEntity = new SysUserEntity();
sysUserEntity.setId(userId); sysUserEntity.setId(userId);
sysUserEntity.setPassword(PasswordUtils.encode(password)); sysUserEntity.setPassword(PasswordUtils.encode(password));
@@ -159,7 +163,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
params.put(Constant.LIMIT, dto.getLimit()); params.put(Constant.LIMIT, dto.getLimit());
IPage<SysUserEntity> page = baseDao.selectPage( IPage<SysUserEntity> page = baseDao.selectPage(
getPage(params, "id", true), getPage(params, "id", true),
new QueryWrapper<SysUserEntity>().eq(StringUtils.isNotBlank(dto.getMobile()), "username", new QueryWrapper<SysUserEntity>().like(StringUtils.isNotBlank(dto.getMobile()), "username",
dto.getMobile())); dto.getMobile()));
// 循环处理page获取回来的数据,返回需要的字段 // 循环处理page获取回来的数据,返回需要的字段
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> { List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
@@ -0,0 +1,315 @@
package xiaozhi.modules.sys.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.socket.*;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import xiaozhi.common.utils.DateUtils;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* WebSocketClientResource:支持 try-with-resources 模式
*/
@Slf4j
public class WebSocketClientManager implements Closeable
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// 全局回调线程池
private static final ExecutorService CALLBACK_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
private final AtomicInteger cnt = new AtomicInteger();
public Thread newThread(Runnable r)
{
Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement());
t.setDaemon(true);
return t;
}
});
private volatile WebSocketSession session;
private final BlockingQueue<String> textMessageQueue;
private final BlockingQueue<byte[]> binaryMessageQueue;
private final CompletableFuture<Void> errorFuture;
private final long maxSessionDuration;
private final TimeUnit maxSessionDurationUnit;
private volatile Consumer<String> onText;
private volatile Consumer<byte[]> onBinary;
private volatile Consumer<Throwable> onError;
private final String uri;
private final WebSocketHttpHeaders headers;
private final long connectTimeout;
private final TimeUnit connectUnit;
private final int queueCapacity;
// 私有构造,仅由 Builder 调用
private WebSocketClientManager(Builder b) {
this.uri = b.uri;
this.headers = b.headers != null ? b.headers : new WebSocketHttpHeaders();
this.connectTimeout = b.connectTimeout;
this.connectUnit = b.connectUnit;
this.maxSessionDuration = b.maxSessionDuration;
this.maxSessionDurationUnit = b.maxSessionDurationUnit;
this.queueCapacity = b.queueCapacity;
this.textMessageQueue = new LinkedBlockingQueue<>(queueCapacity);
this.binaryMessageQueue = new LinkedBlockingQueue<>(queueCapacity);
this.errorFuture = new CompletableFuture<>();
}
public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException {
WebSocketClientManager ws = new WebSocketClientManager(b);
StandardWebSocketClient client = new StandardWebSocketClient();
CompletableFuture<WebSocketSession> future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri));
WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit);
if (sess == null || !sess.isOpen())
{
throw new IOException("握手失败或会话未打开");
}
ws.session = sess;
return ws;
}
/**
* 发送 Text
*/
public void sendText(String text) throws IOException {
session.sendMessage(new TextMessage(text));
}
public void sendBinary(byte[] data) throws IOException {
session.sendMessage(new BinaryMessage(data));
}
public void sendJson(Object payload) throws IOException {
String json = OBJECT_MAPPER.writeValueAsString(payload);
session.sendMessage(new TextMessage(json));
}
private <T> List<T> listenerCustom(
BlockingQueue<T> queue,
Predicate<T> predicate)
throws InterruptedException, TimeoutException, ExecutionException
{
List<T> collected = new ArrayList<>();
long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration);
while (true) {
if (errorFuture.isDone()) {
errorFuture.get();
}
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
throw new TimeoutException("等待批量消息超时");
}
T msg = queue.poll(remaining, TimeUnit.MILLISECONDS);
if (msg == null) {
throw new TimeoutException("等待批量消息超时");
}
collected.add(msg);
if (predicate.test(msg)) {
break;
}
}
close();
return collected;
}
/**
* 同步接收多条消息,直到 predicate 为 true 或超时抛异常;
* @return 返回监听期间的所有消息列表
*/
public List<String> listener(Predicate<String> predicate)
throws InterruptedException, TimeoutException, ExecutionException
{
return listenerCustom(textMessageQueue, predicate);
}
public List<byte[]> listenerBinary(Predicate<byte[]> predicate)
throws InterruptedException, TimeoutException, ExecutionException
{
return listenerCustom(binaryMessageQueue, predicate);
}
/**
* 注册文本回调
*/
public WebSocketClientManager onText(Consumer<String> c) {
this.onText = c;
return this;
}
/**
* 注册二进制回调
*/
public WebSocketClientManager onBinary(Consumer<byte[]> c) {
this.onBinary = c;
return this;
}
/**
* 注册错误回调
*/
public WebSocketClientManager onError(Consumer<Throwable> c) {
this.onError = c;
return this;
}
/**
* 关闭会话,try-with-resources / finally 自动调用
*/
@Override
public void close() {
try {
if (session != null && session.isOpen()) {
session.close(CloseStatus.NORMAL);
}
}
catch (IOException ignored) {}
textMessageQueue.clear();
binaryMessageQueue.clear();
errorFuture.completeExceptionally(new IOException("WebSocket 已关闭"));
}
private class InternalHandler extends AbstractWebSocketHandler {
private final String targetUri;
private final StopWatch stopWatch;
InternalHandler(String targetUri) {
this.targetUri = targetUri;
this.stopWatch = new StopWatch();
}
/**
* 连接建立时回调
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 保存会话
WebSocketClientManager.this.session = session;
this.stopWatch.start();
log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN));
}
/**
* 处理文本消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
// 入队
textMessageQueue.offer(payload);
// 回调用户注册的 onText
if (onText != null) {
CALLBACK_EXECUTOR.submit(() -> onText.accept(payload));
}
}
/**
* 处理二进制消息
*/
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
ByteBuffer buf = message.getPayload();
byte[] data = new byte[buf.remaining()];
buf.get(data);
// 入队
binaryMessageQueue.offer(data);
// 回调用户注册的 onBinary
if (onBinary != null) {
CALLBACK_EXECUTOR.submit(() -> onBinary.accept(data));
}
}
/**
* 传输错误时回调
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
// 保持原有逻辑:完成 errorFuture、回调 onError、关闭会话、异步通知连接失败
errorFuture.completeExceptionally(exception);
if (onError != null) {
CALLBACK_EXECUTOR.submit(() -> onError.accept(exception));
}
session.close(CloseStatus.SERVER_ERROR);
}
/**
* 连接关闭时回调
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
if (stopWatch.isRunning()) {
stopWatch.stop();
}
log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s",
targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis()));
}
}
public static class Builder {
private String uri; // 目标 WS URI
private long connectTimeout = 3; // 请求连接等待时间
private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位
private long maxSessionDuration = 5; // 最大连线时间,默认5秒
private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位
private int queueCapacity = 100; // 消息队列容量
private WebSocketHttpHeaders headers; // 请求头
/**
* 目标 WS URI
*/
public Builder uri(String uri) {
this.uri = Objects.requireNonNull(uri);
return this;
}
public Builder headers(WebSocketHttpHeaders h) {
this.headers = h;
return this;
}
public Builder connectTimeout(long t, TimeUnit u) {
this.connectTimeout = t;
this.connectUnit = u;
return this;
}
public Builder maxSessionDuration(long t, TimeUnit u) {
this.maxSessionDuration = t;
this.maxSessionDurationUnit = u;
return this;
}
public Builder queueCapacity(int c) {
this.queueCapacity = c;
return this;
}
public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException {
return WebSocketClientManager.build(this);
}
}
}
@@ -48,4 +48,4 @@ spring:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制) max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接 max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接 min-idle: 0 # 连接池中的最小空闲连接
shutdown-timeout: 100ms # 客户端优雅关闭的等待时间 shutdown-timeout: 100ms # 客户端优雅关闭的等待时间
@@ -0,0 +1,18 @@
-- 修改自定义TTS接口请求定义
update `ai_model_provider` set `fields` =
'[{"key":"url","label":"服务地址","type":"string"},{"key":"method","label":"请求方式","type":"string"},{"key":"params","label":"请求参数","type":"dict","dict_name":"params"},{"key":"headers","label":"请求头","type":"dict","dict_name":"headers"},{"key":"format","label":"音频格式","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]'
where `id` = 'SYSTEM_TTS_custom';
-- 修改自定义TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '自定义TTS配置说明:
1. 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务
2. 以本地部署的KokoroTTS为例
3. 如果只有cpu运行:docker run -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest
4. 如果只有gpu运行:docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
配置说明:
1. 在params中配置请求参数,使用JSON格式
例如KokoroTTS{ "input": "{prompt_text}", "speed": 1, "voice": "zm_yunxi", "stream": true, "download_format": "mp3", "response_format": "mp3", "return_download_link": true }
2. 在headers中配置请求头
3. 设置返回音频格式' WHERE `id` = 'TTS_CustomTTS';
@@ -0,0 +1,65 @@
-- 添加手机短信注册功能的需要的参数
delete from sys_params where id in (108, 109, 110, 111, 112, 113, 114, 115);
delete from sys_params where id in (610, 611, 612, 613);
INSERT INTO sys_params
(id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date)
VALUES
(108, 'server.name', 'xiaozhi-esp32-server', 'string', 1, '系统名称', NULL, NULL, NULL, NULL),
(109, 'server.beian_icp_num', 'null', 'string', 1, 'icp备案号,填写null则不设置', NULL, NULL, NULL, NULL),
(110, 'server.beian_ga_num', 'null', 'string', 1, '公安备案号,填写null则不设置', NULL, NULL, NULL, NULL),
(111, 'server.enable_mobile_register', 'false', 'boolean', 1, '是否开启手机注册', NULL, NULL, NULL, NULL),
(112, 'server.sms_max_send_count', '10', 'number', 1, '单号码单日最大短信发送条数', NULL, NULL, NULL, NULL),
(610, 'aliyun.sms.access_key_id', '', 'string', 1, '阿里云平台access_key', NULL, NULL, NULL, NULL),
(611, 'aliyun.sms.access_key_secret', '', 'string', 1, '阿里云平台access_key_secret', NULL, NULL, NULL, NULL),
(612, 'aliyun.sms.sign_name', '', 'string', 1, '阿里云短信签名', NULL, NULL, NULL, NULL),
(613, 'aliyun.sms.sms_code_template_code', '', 'string', 1, '阿里云短信模板', NULL, NULL, NULL, NULL);
update sys_params set remark = '是否允许管理员以外的人注册' where param_code = 'server.allow_user_register';
-- 增加手机区域字典
-- 插入固件类型字典类型
delete from `sys_dict_type` where `id` = 102;
INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
(102, 'MOBILE_AREA', '手机区域', '手机区域字典', 0, 1, NOW(), 1, NOW());
-- 插入固件类型字典数据
delete from `sys_dict_data` where `dict_type_id` = 102;
INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
(102001, 102, '中国大陆', '+86', '中国大陆', 1, 1, NOW(), 1, NOW()),
(102002, 102, '中国香港', '+852', '中国香港', 2, 1, NOW(), 1, NOW()),
(102003, 102, '中国澳门', '+853', '中国澳门', 3, 1, NOW(), 1, NOW()),
(102004, 102, '中国台湾', '+886', '中国台湾', 4, 1, NOW(), 1, NOW()),
(102005, 102, '美国/加拿大', '+1', '美国/加拿大', 5, 1, NOW(), 1, NOW()),
(102006, 102, '英国', '+44', '英国', 6, 1, NOW(), 1, NOW()),
(102007, 102, '法国', '+33', '法国', 7, 1, NOW(), 1, NOW()),
(102008, 102, '意大利', '+39', '意大利', 8, 1, NOW(), 1, NOW()),
(102009, 102, '德国', '+49', '德国', 9, 1, NOW(), 1, NOW()),
(102010, 102, '波兰', '+48', '波兰', 10, 1, NOW(), 1, NOW()),
(102011, 102, '瑞士', '+41', '瑞士', 11, 1, NOW(), 1, NOW()),
(102012, 102, '西班牙', '+34', '西班牙', 12, 1, NOW(), 1, NOW()),
(102013, 102, '丹麦', '+45', '丹麦', 13, 1, NOW(), 1, NOW()),
(102014, 102, '马来西亚', '+60', '马来西亚', 14, 1, NOW(), 1, NOW()),
(102015, 102, '澳大利亚', '+61', '澳大利亚', 15, 1, NOW(), 1, NOW()),
(102016, 102, '印度尼西亚', '+62', '印度尼西亚', 16, 1, NOW(), 1, NOW()),
(102017, 102, '菲律宾', '+63', '菲律宾', 17, 1, NOW(), 1, NOW()),
(102018, 102, '新西兰', '+64', '新西兰', 18, 1, NOW(), 1, NOW()),
(102019, 102, '新加坡', '+65', '新加坡', 19, 1, NOW(), 1, NOW()),
(102020, 102, '泰国', '+66', '泰国', 20, 1, NOW(), 1, NOW()),
(102021, 102, '日本', '+81', '日本', 21, 1, NOW(), 1, NOW()),
(102022, 102, '韩国', '+82', '韩国', 22, 1, NOW(), 1, NOW()),
(102023, 102, '越南', '+84', '越南', 23, 1, NOW(), 1, NOW()),
(102024, 102, '印度', '+91', '印度', 24, 1, NOW(), 1, NOW()),
(102025, 102, '巴基斯坦', '+92', '巴基斯坦', 25, 1, NOW(), 1, NOW()),
(102026, 102, '尼日利亚', '+234', '尼日利亚', 26, 1, NOW(), 1, NOW()),
(102027, 102, '孟加拉国', '+880', '孟加拉国', 27, 1, NOW(), 1, NOW()),
(102028, 102, '沙特阿拉伯', '+966', '沙特阿拉伯', 28, 1, NOW(), 1, NOW()),
(102029, 102, '阿联酋', '+971', '阿联酋', 29, 1, NOW(), 1, NOW()),
(102030, 102, '巴西', '+55', '巴西', 30, 1, NOW(), 1, NOW()),
(102031, 102, '墨西哥', '+52', '墨西哥', 31, 1, NOW(), 1, NOW()),
(102032, 102, '智利', '+56', '智利', 32, 1, NOW(), 1, NOW()),
(102033, 102, '阿根廷', '+54', '阿根廷', 33, 1, NOW(), 1, NOW()),
(102034, 102, '埃及', '+20', '埃及', 34, 1, NOW(), 1, NOW()),
(102035, 102, '南非', '+27', '南非', 35, 1, NOW(), 1, NOW()),
(102036, 102, '肯尼亚', '+254', '肯尼亚', 36, 1, NOW(), 1, NOW()),
(102037, 102, '坦桑尼亚', '+255', '坦桑尼亚', 37, 1, NOW(), 1, NOW()),
(102038, 102, '哈萨克斯坦', '+7', '哈萨克斯坦', 38, 1, NOW(), 1, NOW());
@@ -0,0 +1,3 @@
-- 更新ai_model_provider的fields字段,将type为dict的改为string
update ai_model_provider set fields = replace(fields, '"type": "dict"', '"type": "string"') where id not in ('SYSTEM_LLM_fastgpt', 'SYSTEM_TTS_custom');
update ai_model_provider set fields = replace(fields, '"type":"dict"', '"type": "string"') where id not in ('SYSTEM_LLM_fastgpt', 'SYSTEM_TTS_custom');
@@ -0,0 +1,51 @@
-- 本地短期记忆配置可以设置独立的LLM
update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"}]' where id = 'SYSTEM_Memory_mem_local_short';
update `ai_model_config` set config_json = '{\"type\": \"mem_local_short\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Memory_mem_local_short';
-- 增加火山双流式TTS供应器和模型配置
delete from `ai_model_provider` where id = 'SYSTEM_TTS_HSDSTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_HSDSTTS', 'TTS', 'huoshan_double_stream', '火山双流式语音合成', '[{"key":"ws_url","label":"WebSocket地址","type":"string"},{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"resource_id","label":"资源ID","type":"string"},{"key":"speaker","label":"默认音色","type":"string"}]', 13, 1, NOW(), 1, NOW());
delete from `ai_model_config` where id = 'TTS_HuoshanDoubleStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_HuoshanDoubleStreamTTS', 'TTS', 'HuoshanDoubleStreamTTS', '火山双流式语音合成', 0, 1, '{\"type\": \"huoshan_double_stream\", \"ws_url\": \"wss://openspeech.bytedance.com/api/v3/tts/bidirection\", \"appid\": \"你的火山引擎语音合成服务appid\", \"access_token\": \"你的火山引擎语音合成服务access_token\", \"resource_id\": \"volc.service_type.10029\", \"speaker\": \"zh_female_wanwanxiaohe_moon_bigtts\"}', NULL, NULL, 16, NULL, NULL, NULL, NULL);
-- 火山双流式TT模型配置说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.volcengine.com/speech/service/10007',
`remark` = '火山引擎语音合成服务配置说明:
1. 访问 https://www.volcengine.com/ 注册并开通火山引擎账号
2. 访问 https://console.volcengine.com/speech/service/10007 开通语音合成大模型,购买音色
3. 在页面底部获取appid和access_token
5. 资源ID固定为:volc.service_type.10029(大模型语音合成及混音)
6. 填入配置文件中' WHERE `id` = 'TTS_HuoshanDoubleStreamTTS';
-- 添加火山双流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_HuoshanDoubleStreamTTS';
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0001', 'TTS_HuoshanDoubleStreamTTS', '爽快思思/Skye', 'zh_female_shuangkuaisisi_moon_bigtts', '中文、英文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Skye.mp3', NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0002', 'TTS_HuoshanDoubleStreamTTS', '温暖阿虎/Alvin', 'zh_male_wennuanahu_moon_bigtts', '中文、英文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Alvin.mp3', NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0003', 'TTS_HuoshanDoubleStreamTTS', '少年梓辛/Brayan', 'zh_male_shaonianzixin_moon_bigtts', '中文、英文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Brayan.mp3', NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0004', 'TTS_HuoshanDoubleStreamTTS', '邻家女孩', 'zh_female_linjianvhai_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E9%82%BB%E5%AE%B6%E5%A5%B3%E5%AD%A9.mp3', NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0005', 'TTS_HuoshanDoubleStreamTTS', '渊博小叔', 'zh_male_yuanboxiaoshu_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B8%8A%E5%8D%9A%E5%B0%8F%E5%8F%94.mp3', NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0006', 'TTS_HuoshanDoubleStreamTTS', '阳光青年', 'zh_male_yangguangqingnian_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E9%98%B3%E5%85%89%E9%9D%92%E5%B9%B4.mp3', NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0007', 'TTS_HuoshanDoubleStreamTTS', '京腔侃爷/Harmony', 'zh_male_jingqiangkanye_moon_bigtts', '中文、英文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Harmony.mp3', NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0008', 'TTS_HuoshanDoubleStreamTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0009', 'TTS_HuoshanDoubleStreamTTS', '湾区大叔', 'zh_female_wanqudashu_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E5%8C%BA%E5%A4%A7%E5%8F%94.mp3', NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0010', 'TTS_HuoshanDoubleStreamTTS', '呆萌川妹', 'zh_female_daimengchuanmei_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E5%91%86%E8%90%8C%E5%B7%9D%E5%A6%B9.mp3', NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0011', 'TTS_HuoshanDoubleStreamTTS', '广州德哥', 'zh_male_guozhoudege_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E5%B9%BF%E5%B7%9E%E5%BE%B7%E5%93%A5.mp3', NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0012', 'TTS_HuoshanDoubleStreamTTS', '北京小爷', 'zh_male_beijingxiaoye_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E5%8C%97%E4%BA%AC%E5%B0%8F%E7%88%B7.mp3', NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0013', 'TTS_HuoshanDoubleStreamTTS', '浩宇小哥', 'zh_male_haoyuxiaoge_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B5%A9%E5%AE%87%E5%B0%8F%E5%93%A5.mp3', NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0014', 'TTS_HuoshanDoubleStreamTTS', '广西远舟', 'zh_male_guangxiyuanzhou_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E5%B9%BF%E8%A5%BF%E8%BF%9C%E8%88%9F.mp3', NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0015', 'TTS_HuoshanDoubleStreamTTS', '妹坨洁儿', 'zh_female_meituojieer_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E5%A6%B9%E5%9D%A8%E6%B4%81%E5%84%BF.mp3', NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0016', 'TTS_HuoshanDoubleStreamTTS', '豫州子轩', 'zh_male_yuzhouzixuan_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E8%B1%AB%E5%B7%9E%E5%AD%90%E8%BD%A9.mp3', NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0017', 'TTS_HuoshanDoubleStreamTTS', '高冷御姐', 'zh_female_gaolengyujie_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E9%AB%98%E5%86%B7%E5%BE%A1%E5%A7%90.mp3', NULL, 17, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0018', 'TTS_HuoshanDoubleStreamTTS', '傲娇霸总', 'zh_male_aojiaobazong_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E5%82%B2%E5%A8%87%E9%9C%B8%E6%80%BB.mp3', NULL, 18, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0019', 'TTS_HuoshanDoubleStreamTTS', '魅力女友', 'zh_female_meilinvyou_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E9%AD%85%E5%8A%9B%E5%A5%B3%E5%8F%8B.mp3', NULL, 19, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0020', 'TTS_HuoshanDoubleStreamTTS', '深夜播客', 'zh_male_shenyeboke_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B7%B1%E5%A4%9C%E6%92%AD%E5%AE%A2.mp3', NULL, 20, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0021', 'TTS_HuoshanDoubleStreamTTS', '柔美女友', 'zh_female_sajiaonvyou_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%9F%94%E7%BE%8E%E5%A5%B3%E5%8F%8B.mp3', NULL, 21, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0022', 'TTS_HuoshanDoubleStreamTTS', '撒娇学妹', 'zh_female_yuanqinvyou_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%92%92%E5%A8%87%E5%AD%A6%E5%A6%B9.mp3', NULL, 22, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0023', 'TTS_HuoshanDoubleStreamTTS', 'かずね(和音)', 'multi_male_jingqiangkanye_moon_bigtts', '日语、西语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Javier.wav', NULL, 23, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0024', 'TTS_HuoshanDoubleStreamTTS', 'はるこ(晴子)', 'multi_female_shuangkuaisisi_moon_bigtts', '日语、西语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Esmeralda.mp3', NULL, 24, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0025', 'TTS_HuoshanDoubleStreamTTS', 'あけみ(朱美)', 'multi_female_gaolengyujie_moon_bigtts', '日语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%9C%B1%E7%BE%8E.mp3', NULL, 25, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HuoshanDoubleStreamTTS_0026', 'TTS_HuoshanDoubleStreamTTS', 'ひろし(広志)', 'multi_male_wanqudashu_moon_bigtts', '日语、西语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/Roberto.wav', NULL, 26, NULL, NULL, NULL, NULL);
@@ -142,3 +142,31 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505142037.sql path: classpath:db/changelog/202505142037.sql
- changeSet:
id: 202505182234
author: amen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505182234.sql
- changeSet:
id: 202505201744
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505201744.sql
- changeSet:
id: 202505151451
author: hsoftxl
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505151451.sql
- changeSet:
id: 202505271414
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505271414.sql
@@ -9,6 +9,7 @@
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u624B\u673A\u53F7\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
@@ -0,0 +1 @@
redis.call('FLUSHALL')
@@ -0,0 +1,11 @@
local value = redis.call('GET', KEYS[1])
-- value 如果为空着设置值
if not value then
local result = redis.call('SET', KEYS[1], ARGV[1])
-- 检查 ARGV[2] 是否存在且大于 0
local expireTime = tonumber(ARGV[2])
if expireTime and expireTime > 0 then
redis.call('EXPIRE', KEYS[1], expireTime)
end
end
return value
@@ -0,0 +1,58 @@
package xiaozhi.modules.sys;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.modules.security.controller.LoginController;
import xiaozhi.modules.security.dto.LoginDTO;
import xiaozhi.modules.security.dto.SmsVerificationDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
@Slf4j
@SpringBootTest
@ActiveProfiles("dev")
class loginControllerTest {
@Autowired
LoginController loginController;
@Test
public void testRegister() {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("手机号码");
loginDTO.setPassword("密码");
loginDTO.setCaptcha("123456");
loginController.register(loginDTO);
}
@Test
public void testSmsVerification() {
try {
SmsVerificationDTO smsVerificationDTO = new SmsVerificationDTO();
smsVerificationDTO.setPhone("手机号码");
smsVerificationDTO.setCaptchaId("123456");
smsVerificationDTO.setCaptcha("123456");
loginController.smsVerification(smsVerificationDTO);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
@Test
public void testRetrievePassword() {
try {
RetrievePasswordDTO retrievePasswordDTO = new RetrievePasswordDTO();
retrievePasswordDTO.setCode("123456");
retrievePasswordDTO.setPhone("手机号码");
retrievePasswordDTO.setPassword("密码");
loginController.retrievePassword(retrievePasswordDTO);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
+17 -8
View File
@@ -20,6 +20,7 @@ function sendRequest() {
return { return {
_sucCallback: null, _sucCallback: null,
_failCallback: null, _failCallback: null,
_networkFailCallback: null,
_method: 'GET', _method: 'GET',
_data: {}, _data: {},
_header: { 'content-type': 'application/json; charset=utf-8' }, _header: { 'content-type': 'application/json; charset=utf-8' },
@@ -36,7 +37,7 @@ function sendRequest() {
headers: this._header, headers: this._header,
responseType: this._responseType responseType: this._responseType
}).then((res) => { }).then((res) => {
const error = httpHandlerError(res, this._failCallback); const error = httpHandlerError(res, this._failCallback, this._networkFailCallback);
if (error) { if (error) {
return return
} }
@@ -47,7 +48,7 @@ function sendRequest() {
}).catch((res) => { }).catch((res) => {
// 打印失败响应 // 打印失败响应
console.log('catch', res) console.log('catch', res)
httpHandlerError(res, this._failCallback) httpHandlerError(res, this._failCallback, this._networkFailCallback)
}) })
return this return this
}, },
@@ -59,6 +60,10 @@ function sendRequest() {
this._failCallback = callback this._failCallback = callback
return this return this
}, },
'networkFail'(callback) {
this._networkFailCallback = callback
return this
},
'url'(url) { 'url'(url) {
if (url) { if (url) {
url = url.replaceAll('$', '/') url = url.replaceAll('$', '/')
@@ -95,11 +100,11 @@ function sendRequest() {
/** /**
* Info 请求完成后返回信息 * Info 请求完成后返回信息
* callBack 回调函数 * failCallback 回调函数
* errTip 自定义错误信息 * networkFailCallback 回调函数
*/ */
// 在错误处理函数中添加日志 // 在错误处理函数中添加日志
function httpHandlerError(info, callBack) { function httpHandlerError(info, failCallback, networkFailCallback) {
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */ /** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
let networkError = false let networkError = false
@@ -111,12 +116,16 @@ function httpHandlerError(info, callBack) {
goToPage(Constant.PAGE.LOGIN, true); goToPage(Constant.PAGE.LOGIN, true);
return true return true
} else { } else {
showDanger(info.data.msg) if (failCallback) {
failCallback(info)
} else {
showDanger(info.data.msg)
}
return true return true
} }
} }
if (callBack) { if (networkFailCallback) {
callBack(info) networkFailCallback(info)
} else { } else {
showDanger(`网络请求出现了错误【${info.status}`) showDanger(`网络请求出现了错误【${info.status}`)
} }
+39 -7
View File
@@ -18,7 +18,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('请求失败:', err) console.error('请求失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getUserList(callback) this.getUserList(callback)
@@ -34,7 +34,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除失败:', err) console.error('删除失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.deleteUser(id, callback) this.deleteUser(id, callback)
@@ -50,7 +50,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('重置密码失败:', err) console.error('重置密码失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.resetUserPassword(id, callback) this.resetUserPassword(id, callback)
@@ -72,7 +72,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取参数列表失败:', err) console.error('获取参数列表失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getParamsList(params, callback) this.getParamsList(params, callback)
@@ -89,7 +89,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('添加参数失败:', err) console.error('添加参数失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.addParam(data, callback) this.addParam(data, callback)
@@ -106,7 +106,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新参数失败:', err) console.error('更新参数失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.updateParam(data, callback) this.updateParam(data, callback)
@@ -123,12 +123,44 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除参数失败:', err) console.error('删除参数失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.deleteParam(ids, callback) this.deleteParam(ids, callback)
}) })
}).send() }).send()
}, },
// 获取ws服务端列表
getWsServerList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/server/server-list`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
console.error('获取ws服务端列表失败:', err)
RequestService.reAjaxFun(() => {
this.getWsServerList(params, callback)
})
}).send();
},
// 发送ws服务器动作指令
sendWsServerAction(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/server/emit-action`)
.method('POST')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
RequestService.reAjaxFun(() => {
this.sendWsServerAction(data, callback)
})
}).send();
}
} }
+9 -9
View File
@@ -12,7 +12,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getAgentList(callback); this.getAgentList(callback);
}); });
@@ -28,7 +28,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.addAgent(agentName, callback); this.addAgent(agentName, callback);
}); });
@@ -43,7 +43,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.deleteAgent(agentId, callback); this.deleteAgent(agentId, callback);
}); });
@@ -58,7 +58,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取配置失败:', err); console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getDeviceConfig(agentId, callback); this.getDeviceConfig(agentId, callback);
@@ -75,7 +75,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.updateAgentConfig(agentId, configData, callback); this.updateAgentConfig(agentId, configData, callback);
}); });
@@ -90,7 +90,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取模板失败:', err); console.error('获取模板失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getAgentTemplate(callback); this.getAgentTemplate(callback);
@@ -107,7 +107,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getAgentSessions(agentId, params, callback); this.getAgentSessions(agentId, params, callback);
}); });
@@ -122,7 +122,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getAgentChatHistory(agentId, sessionId, callback); this.getAgentChatHistory(agentId, sessionId, callback);
}); });
@@ -137,7 +137,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getAudioId(audioId, callback); this.getAudioId(audioId, callback);
}); });
+4 -4
View File
@@ -11,7 +11,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取设备列表失败:', err); console.error('获取设备列表失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getAgentBindDevices(agentId, callback); this.getAgentBindDevices(agentId, callback);
@@ -28,7 +28,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('解绑设备失败:', err); console.error('解绑设备失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback); this.unbindDevice(device_id, callback);
@@ -44,7 +44,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('绑定设备失败:', err); console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.bindDevice(agentId, deviceCode, callback); this.bindDevice(agentId, deviceCode, callback);
@@ -59,7 +59,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新OTA状态失败:', err) console.error('更新OTA状态失败:', err)
this.$message.error(err.msg || '更新OTA状态失败') this.$message.error(err.msg || '更新OTA状态失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
+11 -11
View File
@@ -18,7 +18,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取字典类型列表失败:', err) console.error('获取字典类型列表失败:', err)
this.$message.error(err.msg || '获取字典类型列表失败') this.$message.error(err.msg || '获取字典类型列表失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -36,7 +36,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取字典类型详情失败:', err) console.error('获取字典类型详情失败:', err)
this.$message.error(err.msg || '获取字典类型详情失败') this.$message.error(err.msg || '获取字典类型详情失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -55,7 +55,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('新增字典类型失败:', err) console.error('新增字典类型失败:', err)
this.$message.error(err.msg || '新增字典类型失败') this.$message.error(err.msg || '新增字典类型失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -74,7 +74,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新字典类型失败:', err) console.error('更新字典类型失败:', err)
this.$message.error(err.msg || '更新字典类型失败') this.$message.error(err.msg || '更新字典类型失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -93,7 +93,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除字典类型失败:', err) console.error('删除字典类型失败:', err)
this.$message.error(err.msg || '删除字典类型失败') this.$message.error(err.msg || '删除字典类型失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -119,7 +119,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取字典数据列表失败:', err) console.error('获取字典数据列表失败:', err)
this.$message.error(err.msg || '获取字典数据列表失败') this.$message.error(err.msg || '获取字典数据列表失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -137,7 +137,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取字典数据详情失败:', err) console.error('获取字典数据详情失败:', err)
this.$message.error(err.msg || '获取字典数据详情失败') this.$message.error(err.msg || '获取字典数据详情失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -156,7 +156,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('新增字典数据失败:', err) console.error('新增字典数据失败:', err)
this.$message.error(err.msg || '新增字典数据失败') this.$message.error(err.msg || '新增字典数据失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -175,7 +175,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新字典数据失败:', err) console.error('更新字典数据失败:', err)
this.$message.error(err.msg || '更新字典数据失败') this.$message.error(err.msg || '更新字典数据失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -194,7 +194,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除字典数据失败:', err) console.error('删除字典数据失败:', err)
this.$message.error(err.msg || '删除字典数据失败') this.$message.error(err.msg || '删除字典数据失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -217,7 +217,7 @@ export default {
reject(new Error(res.data?.msg || '获取字典数据列表失败')) reject(new Error(res.data?.msg || '获取字典数据列表失败'))
} }
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取字典数据列表失败:', err) console.error('获取字典数据列表失败:', err)
reject(err) reject(err)
}).send() }).send()
+131 -24
View File
@@ -18,7 +18,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取模型列表失败:', err) console.error('获取模型列表失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getModelList(params, callback) this.getModelList(params, callback)
@@ -34,7 +34,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res.data?.data || []) callback(res.data?.data || [])
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取供应器列表失败:', err) console.error('获取供应器列表失败:', err)
this.$message.error('获取供应器列表失败') this.$message.error('获取供应器列表失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -65,7 +65,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('新增模型失败:', err) console.error('新增模型失败:', err)
this.$message.error(err.msg || '新增模型失败') this.$message.error(err.msg || '新增模型失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -82,7 +82,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除模型失败:', err) console.error('删除模型失败:', err)
this.$message.error(err.msg || '删除模型失败') this.$message.error(err.msg || '删除模型失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -100,7 +100,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getModelNames(modelType, modelName, callback); this.getModelNames(modelType, modelName, callback);
}); });
@@ -118,7 +118,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getModelVoices(modelId, voiceName, callback); this.getModelVoices(modelId, voiceName, callback);
}); });
@@ -133,7 +133,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取模型配置失败:', err) console.error('获取模型配置失败:', err)
this.$message.error(err.msg || '获取模型配置失败') this.$message.error(err.msg || '获取模型配置失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -150,7 +150,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新模型状态失败:', err) console.error('更新模型状态失败:', err)
this.$message.error(err.msg || '更新模型状态失败') this.$message.error(err.msg || '更新模型状态失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
@@ -166,20 +166,20 @@ export default {
configJson: formData.configJson configJson: formData.configJson
}; };
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`) .url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
.method('PUT') .method('PUT')
.data(payload) .data(payload)
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新模型失败:', err); console.error('更新模型失败:', err);
this.$message.error(err.msg || '更新模型失败'); this.$message.error(err.msg || '更新模型失败');
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.updateModel(params, callback); this.updateModel(params, callback);
}); });
}).send(); }).send();
}, },
// 设置默认模型 // 设置默认模型
setDefaultModel(id, callback) { setDefaultModel(id, callback) {
@@ -190,12 +190,119 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('设置默认模型失败:', err) console.error('设置默认模型失败:', err)
this.$message.error(err.msg || '设置默认模型失败') this.$message.error(err.msg || '设置默认模型失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.setDefaultModel(id, callback) this.setDefaultModel(id, callback)
}) })
}).send() }).send()
} },
/**
* 获取模型配置列表(支持查询参数)
* @param {Object} params - 查询参数对象,例如 { name: 'test', modelType: 1 }
* @param {Function} callback - 回调函数
*/
getModelProvidersPage(params, callback) {
// 构建查询参数
const queryParams = new URLSearchParams();
if (params.name) queryParams.append('name', params.name);
if (params.modelType !== undefined) queryParams.append('modelType', params.modelType);
if (params.page !== undefined) queryParams.append('page', params.page);
if (params.limit !== undefined) queryParams.append('limit', params.limit);
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/provider?${queryParams.toString()}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
this.$message.error(err.msg || '获取供应器列表失败');
RequestService.reAjaxFun(() => {
this.getModelProviders(params, callback);
});
}).send();
},
/**
* 新增模型供应器配置
* @param {Object} params - 请求参数对象,例如 { modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 }
* @param {Function} callback - 成功回调函数
*/
addModelProvider(params, callback) {
const postData = {
modelType: params.modelType || '',
providerCode: params.providerCode || '',
name: params.name || '',
fields: JSON.stringify(params.fields || []),
sort: params.sort || 0
};
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/provider`)
.method('POST')
.data(postData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('新增模型供应器失败:', err)
this.$message.error(err.msg || '新增模型供应器失败')
RequestService.reAjaxFun(() => {
this.addModelProvider(params, callback);
});
}).send();
},
/**
* 更新模型供应器配置
* @param {Object} params - 请求参数对象,例如 { id: '111', modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 }
* @param {Function} callback - 成功回调函数
*/
updateModelProvider(params, callback) {
const putData = {
id: params.id || '',
modelType: params.modelType || '',
providerCode: params.providerCode || '',
name: params.name || '',
fields: JSON.stringify(params.fields || []),
sort: params.sort || 0
};
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/provider`)
.method('PUT')
.data(putData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
this.$message.error(err.msg || '更新模型供应器失败')
RequestService.reAjaxFun(() => {
this.updateModelProvider(params, callback);
});
}).send();
},
// 删除
deleteModelProviderByIds(ids, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/provider/delete`)
.method('POST')
.data(ids)
.success((res) => {
RequestService.clearRequestTime()
callback(res);
})
.networkFail((err) => {
this.$message.error(err.msg || '删除模型供应器失败')
RequestService.reAjaxFun(() => {
this.deleteModelProviderByIds(ids, callback)
})
}).send()
},
} }
+7 -7
View File
@@ -12,7 +12,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取OTA固件列表失败:', err); console.error('获取OTA固件列表失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getOtaList(params, callback); this.getOtaList(params, callback);
@@ -28,7 +28,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取OTA固件信息失败:', err); console.error('获取OTA固件信息失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getOtaInfo(id, callback); this.getOtaInfo(id, callback);
@@ -45,7 +45,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('保存OTA固件信息失败:', err); console.error('保存OTA固件信息失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.saveOta(entity, callback); this.saveOta(entity, callback);
@@ -62,7 +62,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('更新OTA固件信息失败:', err); console.error('更新OTA固件信息失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.updateOta(id, entity, callback); this.updateOta(id, entity, callback);
@@ -78,7 +78,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除OTA固件失败:', err); console.error('删除OTA固件失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.deleteOta(id, callback); this.deleteOta(id, callback);
@@ -97,7 +97,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('上传固件文件失败:', err); console.error('上传固件文件失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.uploadFirmware(file, callback); this.uploadFirmware(file, callback);
@@ -113,7 +113,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取下载链接失败:', err); console.error('获取下载链接失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getDownloadUrl(id, callback); this.getDownloadUrl(id, callback);
+5 -5
View File
@@ -1,4 +1,4 @@
import {getServiceUrl} from '../api'; import { getServiceUrl } from '../api';
import RequestService from '../httpRequest'; import RequestService from '../httpRequest';
export default { export default {
@@ -18,7 +18,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res.data || []); callback(res.data || []);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取音色列表失败:', err); console.error('获取音色列表失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getVoiceList(params, callback); this.getVoiceList(params, callback);
@@ -42,7 +42,7 @@ export default {
.success((res) => { .success((res) => {
callback(res.data); callback(res.data);
}) })
.fail((err) => { .networkFail((err) => {
console.error('保存音色失败:', err); console.error('保存音色失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.saveVoice(params, callback); this.saveVoice(params, callback);
@@ -59,7 +59,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('删除音色失败:', err); console.error('删除音色失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.deleteVoice(ids, callback); this.deleteVoice(ids, callback);
@@ -82,7 +82,7 @@ export default {
.success((res) => { .success((res) => {
callback(res.data); callback(res.data);
}) })
.fail((err) => { .networkFail((err) => {
console.error('修改音色失败:', err); console.error('修改音色失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.updateVoice(params, callback); this.updateVoice(params, callback);
+65 -10
View File
@@ -4,7 +4,7 @@ import RequestService from '../httpRequest'
export default { export default {
// 登录 // 登录
login(loginForm, callback) { login(loginForm, callback, failCallback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/user/login`) .url(`${getServiceUrl()}/user/login`)
.method('POST') .method('POST')
@@ -13,7 +13,11 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail(() => { .fail((err) => {
RequestService.clearRequestTime()
failCallback(err)
})
.networkFail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.login(loginForm, callback) this.login(loginForm, callback)
}) })
@@ -34,12 +38,32 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { // 添加错误参数 .networkFail((err) => { // 添加错误参数
}).send() }).send()
}, },
// 发送短信验证码
sendSmsVerification(data, callback, failCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/smsVerification`)
.method('POST')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
RequestService.clearRequestTime()
failCallback(err)
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.sendSmsVerification(data, callback, failCallback)
})
}).send()
},
// 注册账号 // 注册账号
register(registerForm, callback) { register(registerForm, callback, failCallback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/user/register`) .url(`${getServiceUrl()}/user/register`)
.method('POST') .method('POST')
@@ -48,7 +72,14 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail(() => { .fail((err) => {
RequestService.clearRequestTime()
failCallback(err)
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.register(registerForm, callback, failCallback)
})
}).send() }).send()
}, },
// 保存设备配置 // 保存设备配置
@@ -61,7 +92,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('保存配置失败:', err); console.error('保存配置失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.saveDeviceConfig(device_id, configData, callback); this.saveDeviceConfig(device_id, configData, callback);
@@ -77,7 +108,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
}) })
.fail((err) => { .networkFail((err) => {
console.error('接口请求失败:', err) console.error('接口请求失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getUserInfo(callback) this.getUserInfo(callback)
@@ -97,7 +128,7 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
successCallback(res); successCallback(res);
}) })
.fail((error) => { .networkFail((error) => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.changePassword(oldPassword, newPassword, successCallback, errorCallback); this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
}); });
@@ -115,7 +146,7 @@ export default {
RequestService.clearRequestTime() RequestService.clearRequestTime()
successCallback(res); successCallback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('修改用户状态失败:', err) console.error('修改用户状态失败:', err)
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.changeUserStatus(status, userIds) this.changeUserStatus(status, userIds)
@@ -131,11 +162,35 @@ export default {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail((err) => { .networkFail((err) => {
console.error('获取公共配置失败:', err); console.error('获取公共配置失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getPubConfig(callback); this.getPubConfig(callback);
}); });
}).send(); }).send();
}, },
// 找回用户密码
retrievePassword(passwordData, callback, failCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/retrieve-password`)
.method('PUT')
.data({
phone: passwordData.phone,
code: passwordData.code,
password: passwordData.password
})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
RequestService.clearRequestTime();
failCallback(err);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.retrievePassword(passwordData, callback, failCallback);
});
}).send()
}
} }
@@ -28,7 +28,7 @@
</div> </div>
<div class="settings-btn" @click="handleChatHistory" <div class="settings-btn" @click="handleChatHistory"
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }"> :class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="开启记忆" placement="top"> <el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="请先在“配置角色”界面开启记忆" placement="top">
<span>聊天记录</span> <span>聊天记录</span>
</el-tooltip> </el-tooltip>
<span v-else>聊天记录</span> <span v-else>聊天记录</span>
+16 -3
View File
@@ -35,10 +35,11 @@
OTA管理 OTA管理
</div> </div>
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown" <el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' }" @visible-change="handleParamDropdownVisibleChange"> :class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' }"
@visible-change="handleParamDropdownVisibleChange">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/param_management.png" <img loading="lazy" alt="" src="@/assets/header/param_management.png"
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' ? 'brightness(0) invert(1)' : 'None' }" />
参数字典 参数字典
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i> <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
</span> </span>
@@ -50,7 +51,10 @@
字典管理 字典管理
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goProviderManagement"> <el-dropdown-item @click.native="goProviderManagement">
供应器管理 字段管理
</el-dropdown-item>
<el-dropdown-item @click.native="goServerSideManagement">
服务端管理
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
@@ -145,6 +149,9 @@ export default {
goProviderManagement() { goProviderManagement() {
this.$router.push('/provider-management') this.$router.push('/provider-management')
}, },
goServerSideManagement() {
this.$router.push('/server-side-management')
},
// 获取用户信息 // 获取用户信息
fetchUserInfo() { fetchUserInfo() {
userApi.getUserInfo(({ data }) => { userApi.getUserInfo(({ data }) => {
@@ -322,6 +329,12 @@ export default {
line-height: 30px; line-height: 30px;
} }
.custom-search-input::v-deep .el-input__suffix-inner {
display: flex;
align-items: center;
height: 100%;
}
.avatar-img { .avatar-img {
width: 21px; width: 21px;
height: 21px; height: 21px;
@@ -66,9 +66,12 @@
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;"> <div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" <el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
style="flex: 1;"> style="flex: 1;">
<el-input v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type" <template v-if="field.type === 'json-textarea'">
class="custom-input-bg" :show-password="field.type === 'password'"> <el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3" placeholder="请输入JSON格式变量(示例:{'key':'value'})"
</el-input> class="custom-input-bg" @change="(val) => handleJsonChange(field.prop, val)"></el-input>
</template>
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
class="custom-input-bg" :show-password="field.type === 'password'"></el-input>
</el-form-item> </el-form-item>
</div> </div>
</template> </template>
@@ -112,6 +115,7 @@ export default {
pendingProviderType: null, pendingProviderType: null,
pendingModelData: null, pendingModelData: null,
dynamicCallInfoFields: [], dynamicCallInfoFields: [],
fieldJsonMap: {}, // 用于存储JSON字段的字符串形式
form: { form: {
id: "", id: "",
modelType: "", modelType: "",
@@ -175,9 +179,7 @@ export default {
sort: 0, sort: 0,
configJson: {} configJson: {}
}; };
this.dynamicCallInfoFields.forEach(field => { this.fieldJsonMap = {};
this.$set(this.form.configJson, field.prop, '');
});
}, },
resetProviders() { resetProviders() {
this.providers = []; this.providers = [];
@@ -203,7 +205,14 @@ export default {
handleSave() { handleSave() {
this.saving = true; // 开始保存加载 this.saving = true; // 开始保存加载
const provideCode = this.form.configJson.type; // 处理所有JSON字段
Object.keys(this.fieldJsonMap).forEach(key => {
const parsed = this.validateJson(this.fieldJsonMap[key]);
if (parsed !== null) {
this.form.configJson[key] = parsed;
}
});
const formData = { const formData = {
id: this.modelData.id, id: this.modelData.id,
modelCode: this.form.modelCode, modelCode: this.form.modelCode,
@@ -213,13 +222,11 @@ export default {
docLink: this.form.docLink, docLink: this.form.docLink,
remark: this.form.remark, remark: this.form.remark,
sort: this.form.sort || 0, sort: this.form.sort || 0,
configJson: { configJson: { ...this.form.configJson }
...this.form.configJson,
}
}; };
this.$emit("save", { this.$emit("save", {
provideCode, provideCode: this.form.configJson.type,
formData, formData,
done: () => { done: () => {
this.saving = false; // 保存完成后回调 this.saving = false; // 保存完成后回调
@@ -240,7 +247,6 @@ export default {
value: String(item.providerCode) value: String(item.providerCode)
})); }));
this.providersLoaded = true; this.providersLoaded = true;
this.allProvidersData = data; this.allProvidersData = data;
if (this.pendingProviderType) { if (this.pendingProviderType) {
@@ -255,7 +261,7 @@ export default {
this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({ this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({
label: f.label, label: f.label,
prop: f.key, prop: f.key,
type: f.type === 'password' ? 'password' : 'text', type: f.type === 'dict' ? 'json-textarea' : (f.type === 'password' ? 'password' : 'text'),
placeholder: `请输入${f.label}` placeholder: `请输入${f.label}`
})); }));
@@ -272,6 +278,9 @@ export default {
this.dynamicCallInfoFields.forEach(field => { this.dynamicCallInfoFields.forEach(field => {
if (!configJson.hasOwnProperty(field.prop)) { if (!configJson.hasOwnProperty(field.prop)) {
configJson[field.prop] = ''; configJson[field.prop] = '';
} else if (field.type === 'json-textarea') {
this.$set(this.fieldJsonMap, field.prop, this.formatJson(configJson[field.prop]));
configJson[field.prop] = this.ensureObject(configJson[field.prop]);
} else if (typeof configJson[field.prop] !== 'string') { } else if (typeof configJson[field.prop] !== 'string') {
configJson[field.prop] = String(configJson[field.prop]); configJson[field.prop] = String(configJson[field.prop]);
} }
@@ -287,10 +296,43 @@ export default {
docLink: model.docLink, docLink: model.docLink,
remark: model.remark, remark: model.remark,
sort: Number(model.sort) || 0, sort: Number(model.sort) || 0,
configJson: { configJson: { ...configJson }
...configJson
}
}; };
},
handleJsonChange(field, value) {
const parsed = this.validateJson(value);
if (parsed !== null) {
this.form.configJson[field] = parsed;
}
},
validateJson(value) {
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return parsed;
}
this.$message.error({
message: '必须输入字典格式(如 {"key":"value"}),保存则使用原数据',
showClose: true
});
return null;
} catch (e) {
this.$message.error({
message: 'JSON格式错误(如 {"key":"value"}),保存则使用原数据',
showClose: true
});
return null;
}
},
formatJson(obj) {
try {
return JSON.stringify(obj, null, 2);
} catch {
return '';
}
},
ensureObject(value) {
return typeof value === 'object' ? value : {};
} }
} }
}; };
@@ -316,7 +358,6 @@ export default {
justify-content: center; justify-content: center;
} }
.custom-close-btn { .custom-close-btn {
position: absolute; position: absolute;
top: 20px; top: 20px;
@@ -452,21 +493,14 @@ export default {
height: 32px; height: 32px;
} }
.custom-form .el-form-item { .custom-form .el-form-item {
margin-bottom: 20px; margin-bottom: 20px;
} }
.custom-input-bg .el-input__inner {
height: 32px;
}
.custom-form .el-form-item__label { .custom-form .el-form-item__label {
color: #3d4566; color: #3d4566;
font-weight: normal; font-weight: normal;
text-align: right; text-align: right;
padding-right: 20px; padding-right: 20px;
} }
</style> </style>
@@ -1,5 +1,6 @@
<template> <template>
<el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog" :show-close="false" class="center-dialog"> <el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog"
:show-close="false" class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;"> <div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -15px; margin-bottom: 20px; text-align: center;"> <div style="font-size: 30px; color: #3d4566; margin-top: -15px; margin-bottom: 20px; text-align: center;">
@@ -10,15 +11,15 @@
<el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form"> <el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 20px;"> <div style="display: flex; gap: 20px; margin-bottom: 20px;">
<el-form-item label="类别" prop="model_type" style="flex: 1;"> <el-form-item label="类别" prop="modelType" style="flex: 1;">
<el-select v-model="form.model_type" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;"> <el-select v-model="form.modelType" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;">
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value"> <el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="供应器编码" prop="provider_code" style="flex: 1;"> <el-form-item label="供应器编码" prop="providerCode" style="flex: 1;">
<el-input v-model="form.provider_code" placeholder="请输入供应器编码" class="custom-input-bg"></el-input> <el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
@@ -27,20 +28,24 @@
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input> <el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="排序" prop="sort" style="flex: 1;"> <el-form-item label="排序" prop="sort" style="flex: 1;">
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg" style="width: 100%;"></el-input-number> <el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
style="width: 100%;"></el-input-number>
</el-form-item> </el-form-item>
</div> </div>
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;"> <div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
字段配置 字段配置
<div style="display: inline-block; float: right;"> <div style="display: inline-block; float: right;">
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;" :disabled="hasIncompleteFields"> <el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
:disabled="hasIncompleteFields">
添加 添加
</el-button> </el-button>
<el-button type="primary" @click="toggleSelectAllFields" size="small" style="background: #5f70f3; border: none; margin-left: 10px;"> <el-button type="primary" @click="toggleSelectAllFields" size="small"
style="background: #5f70f3; border: none; margin-left: 10px;">
{{ isAllFieldsSelected ? '取消全选' : '全选' }} {{ isAllFieldsSelected ? '取消全选' : '全选' }}
</el-button> </el-button>
<el-button type="danger" @click="batchRemoveFields" size="small" style="background: red; border: none; margin-left: 10px;"> <el-button type="danger" @click="batchRemoveFields" size="small"
style="background: red; border: none; margin-left: 10px;">
批量删除 批量删除
</el-button> </el-button>
</div> </div>
@@ -135,9 +140,9 @@ export default {
return { return {
saving: false, saving: false,
rules: { rules: {
model_type: [{required: true, message: '请选择类别', trigger: 'change'}], modelType: [{ required: true, message: '请选择类别', trigger: 'change' }],
provider_code: [{required: true, message: '请输入供应器编码', trigger: 'blur'}], providerCode: [{ required: true, message: '请输入供应器编码', trigger: 'blur' }],
name: [{required: true, message: '请输入供应器名称', trigger: 'blur'}] name: [{ required: true, message: '请输入供应器名称', trigger: 'blur' }]
}, },
isAllFieldsSelected: false, isAllFieldsSelected: false,
tableKey: 0 // 用于强制表格重新渲染 tableKey: 0 // 用于强制表格重新渲染
@@ -419,11 +424,12 @@ export default {
display: none; display: none;
} }
.el-table th, .el-table td { .el-table th,
.el-table td {
padding: 8px 0; padding: 8px 0;
} }
.el-button.is-circle{ .el-button.is-circle {
border-radius: 2px; border-radius: 2px;
} }
</style> </style>
@@ -1,23 +1,74 @@
<template> <template>
<div class="copyright"> <div class="copyright">
©2025 xiaozhi-esp32-server v{{ version }} <div class="footer-content">
<span>{{ year }} {{ name }} {{ version }}</span>
<template v-if="beianGaNum !== 'null'">
<span v-if="beianIcpNum !== 'null' || name">|</span>
<a :href="'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=' + beianGaNum" target="_blank"
rel="noopener" class="beian-link">
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAMAAABhEH5lAAACXlBMVEUAAADax4rGxqP556zmsVD/6KXu5JvwvFPglEFnY3nnpEjosU1+XFTmuVfu13Hrv1jpsFa1k3mne2HlvnPdqmPqz3fu3YrJoWv04J/mz5z246aSi3i2p2/0zGj82WDrtFjar1O7klO4dkfimUJcWnbho0rgr1vntFfmvFflvGOZdFKXZkOUemnmv2XkuWWVe2ziqlXnt1Tr0HDpwl6ObmCkbEKqhVyOc2jdoVHmuljfrmDq0nPapWDUnErqy3XjtWXesXLrzHjz34rqzYfmw3fy4YrqyYDevW/dzoTIhkndsXLRsIneuHjlyIzQroTjuXjlyYfktWvw2prhvoHTmGfx45j3773TuYT/25LdijyKf321oXU+Nm3cumXfuWLtwlzpwVz40FvpvlvqvFfouFXjrlHipEjcmEfknEXPbzfbZyzZQR/YMBrWKBrhJhLdHxDTGg5HV4xBT4YYKYaWk4SDgYIWIYBybH0qLn1KS3tlZHpNS3WGfHIcHXEwLHAnF28rKW07NGuzn2pNQ2ruzWnDrGnuy2jryGjAnGgoH2j1x2RCNGPcrmIsEGGQdl/pu17YtV7FoV3muVxoRVzWqFvSmVp2SVbyw1XbkVTuvFPfo1PgmVPpt1LcnFLWkVLlp1F7W1HpsU92QE5oMUtFAUrpqUjnjUFOAEHUgz+DUj14FzvbczieMTdhATfodjXWcDXcbjXVcTS/Xi/hay7WXS3cWS3vYyvaVSrUVymnJSjXSyeKDSbNSiWfHCPMTyLFQiHkSR/lPxvVLRrPLBnUJRfUHhbaJxXVGxTZJxPNFhBdOhm/AAAAWXRSTlMABQIU/hIJ/v79/Pj29PPx4cC6s7CNfmxZWRv+/v7+/v7+/v78/Pr6+fPz8vHq6ujl5eTj4ODe2NTQzMW6ubKsnJeQkJCJiIOBgX9ubGpoW1lMREM0JR8dDgvYx1gAAAE7SURBVBjTYgADJmZJQUFJZiYGOGD2EzLV1jIT8paCibCJ86zctGPD1D4ecUaICKuPyYLMYznZOfNqzP0jwEJBOt1KB4/mHjqZ3dGsHwxW5M4Zk5l/PPdIQcGMShUPVqBlgQLlMSvyDq+P3HVidjWnkQQTA5MXV1SFYtb+jZGrl02si2J3Y2JgFMmILl68efueLVvXLSqLXirCCBgDow1HT+GS/AP7srblLS+K5bIHCjmodpXUr929d+eaVb3SsuouQCHhufPjShsbJk/rrK2KW8hiBxRyVkvnkI9tnaDQJDNFOU1DDOjRMEfe9Mjpiewz5RIzZmmKMoPcKqGbkJqSkBSfnDqpXS+ACeRpJ76W/uSUpDlpLPFtfK5soLAKFbU05Odm4eblN7YWk4KEGWOIp62FgYCVsG84SAAAL7BaooX965sAAAAASUVORK5CYII="
class="beian-icon" alt="备案图标">
<span class="beian-text">{{ beianGaNum }}</span>
</a>
</template>
<template v-if="beianIcpNum !== 'null'">
<span v-if="name">|</span>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="beian-link">
<span class="beian-text">{{ beianIcpNum }}</span>
</a>
</template>
</div> </div>
</div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
name: 'VersionFooter', name: 'VersionFooter',
computed: { computed: {
...mapState({ ...mapState({
version: state => state.pubConfig.version version: state => state.pubConfig.version,
}) name: state => state.pubConfig.name,
}, beianIcpNum: state => state.pubConfig.beianIcpNum,
mounted() { beianGaNum: state => state.pubConfig.beianGaNum,
this.$store.dispatch('fetchPubConfig'); year: state => state.pubConfig.year
} })
},
mounted() {
this.$store.dispatch('fetchPubConfig')
}
} }
</script> </script>
<style scoped></style> <style scoped>
.copyright {
padding: 10px 0;
}
.footer-content {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.beian-link {
text-decoration: none;
display: inline-flex;
align-items: center;
}
.beian-icon {
width: 16px;
height: 16px;
vertical-align: middle;
}
.beian-text {
color: #000;
font-size: 12px;
vertical-align: middle;
}
</style>
+19
View File
@@ -39,6 +39,13 @@ const routes = [
return import('../views/register.vue') return import('../views/register.vue')
} }
}, },
{
path: '/retrieve-password',
name: 'RetrievePassword',
component: function () {
return import('../views/retrievePassword.vue')
}
},
// 设备管理页面路由 // 设备管理页面路由
{ {
path: '/device-management', path: '/device-management',
@@ -73,6 +80,18 @@ const routes = [
title: '参数管理' title: '参数管理'
} }
}, },
{
path: '/server-side-management',
name: 'ServerSideManager',
component: function () {
return import('../views/ServerSideManager.vue')
},
meta: {
requiresAuth: true,
title: '服务端管理'
}
},
{ {
path: '/ota-management', path: '/ota-management',
name: 'OtaManagement', name: 'OtaManagement',
+2
View File
@@ -13,6 +13,8 @@ export default new Vuex.Store({
isSuperAdmin: false, // 添加superAdmin状态 isSuperAdmin: false, // 添加superAdmin状态
pubConfig: { // 添加公共配置存储 pubConfig: { // 添加公共配置存储
version: '', version: '',
beianIcpNum: 'null',
beianGaNum: 'null',
allowUserRegister: false allowUserRegister: false
} }
}, },
+64
View File
@@ -136,3 +136,67 @@ export function getUUID() {
}) })
} }
/**
* 验证手机号格式
* @param {string} mobile 手机号
* @param {string} areaCode 区号
* @returns {boolean}
*/
export function validateMobile(mobile, areaCode) {
// 移除所有非数字字符
const cleanMobile = mobile.replace(/\D/g, '');
// 根据不同区号使用不同的验证规则
switch (areaCode) {
case '+86': // 中国大陆
return /^1[3-9]\d{9}$/.test(cleanMobile);
case '+852': // 中国香港
return /^[569]\d{7}$/.test(cleanMobile);
case '+853': // 中国澳门
return /^6\d{7}$/.test(cleanMobile);
case '+886': // 中国台湾
return /^9\d{8}$/.test(cleanMobile);
case '+1': // 美国/加拿大
return /^[2-9]\d{9}$/.test(cleanMobile);
case '+44': // 英国
return /^7[1-9]\d{8}$/.test(cleanMobile);
case '+81': // 日本
return /^[7890]\d{8}$/.test(cleanMobile);
case '+82': // 韩国
return /^1[0-9]\d{7}$/.test(cleanMobile);
case '+65': // 新加坡
return /^[89]\d{7}$/.test(cleanMobile);
case '+61': // 澳大利亚
return /^[4578]\d{8}$/.test(cleanMobile);
case '+49': // 德国
return /^1[5-7]\d{8}$/.test(cleanMobile);
case '+33': // 法国
return /^[67]\d{8}$/.test(cleanMobile);
case '+39': // 意大利
return /^3[0-9]\d{8}$/.test(cleanMobile);
case '+34': // 西班牙
return /^[6-9]\d{8}$/.test(cleanMobile);
case '+55': // 巴西
return /^[1-9]\d{10}$/.test(cleanMobile);
case '+91': // 印度
return /^[6-9]\d{9}$/.test(cleanMobile);
case '+971': // 阿联酋
return /^[5]\d{8}$/.test(cleanMobile);
case '+966': // 沙特阿拉伯
return /^[5]\d{8}$/.test(cleanMobile);
case '+880': // 孟加拉国
return /^1[3-9]\d{8}$/.test(cleanMobile);
case '+234': // 尼日利亚
return /^[789]\d{9}$/.test(cleanMobile);
case '+254': // 肯尼亚
return /^[17]\d{8}$/.test(cleanMobile);
case '+255': // 坦桑尼亚
return /^[67]\d{8}$/.test(cleanMobile);
case '+7': // 哈萨克斯坦
return /^[67]\d{9}$/.test(cleanMobile);
default:
// 其他国际号码:至少5位,最多15位
return /^\d{5,15}$/.test(cleanMobile);
}
}
@@ -48,8 +48,7 @@
<el-table ref="dictDataTable" :data="dictDataList" style="width: 100%" <el-table ref="dictDataTable" :data="dictDataList" style="width: 100%"
v-loading="dictDataLoading" element-loading-text="拼命加载中" v-loading="dictDataLoading" element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table"
class="data-table"
header-row-class-name="table-header"> header-row-class-name="table-header">
<el-table-column label="选择" align="center" width="55"> <el-table-column label="选择" align="center" width="55">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -546,6 +545,7 @@ export default {
:deep(.el-table__body-wrapper) { :deep(.el-table__body-wrapper) {
max-height: calc(var(--table-max-height) - 40px); max-height: calc(var(--table-max-height) - 40px);
overflow-y: auto;
} }
:deep(.el-table__body) { :deep(.el-table__body) {
+1 -1
View File
@@ -467,7 +467,7 @@ export default {
.main-wrapper { .main-wrapper {
margin: 5px 22px; margin: 5px 22px;
border-radius: 15px; border-radius: 15px;
min-height: calc(100vh - 24vh); min-height: calc(100vh - 26vh);
height: auto; height: auto;
max-height: 80vh; max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
@@ -5,7 +5,7 @@
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">参数管理</h2> <h2 class="page-title">参数管理</h2>
<div class="right-operations"> <div class="right-operations">
<el-input placeholder="请输入参数编码查询" v-model="searchCode" class="search-input" <el-input placeholder="请输入参数编码或备注查询" v-model="searchCode" class="search-input"
@keyup.enter.native="handleSearch" clearable /> @keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">搜索</el-button> <el-button class="btn-search" @click="handleSearch">搜索</el-button>
</div> </div>
+135 -195
View File
@@ -1,13 +1,14 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<HeaderBar/> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">供应器管理</h2> <h2 class="page-title">字段管理</h2>
<div class="right-operations"> <div class="right-operations">
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange"> <el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
<el-button class="category-btn"> <el-button class="category-btn">
类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down':DropdownVisible }"></i> 类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
:class="{ 'rotate-down': DropdownVisible }"></i>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">全部</el-dropdown-item> <el-dropdown-item command="">全部</el-dropdown-item>
@@ -16,7 +17,8 @@
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch" clearable/> <el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
clearable />
<el-button class="btn-search" @click="handleSearch">搜索</el-button> <el-button class="btn-search" @click="handleSearch">搜索</el-button>
</div> </div>
</div> </div>
@@ -24,37 +26,39 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="content-panel"> <div class="content-panel">
<div class="content-area"> <div class="content-area">
<el-card class="params-card" shadow="never"> <el-card class="provider-card" shadow="never">
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading" <el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
:header-cell-class-name="headerCellClassName">
<el-table-column label="选择" align="center" width="120"> <el-table-column label="选择" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类别" prop="model_type" align="center" width="200">
<el-table-column label="类别" prop="modelType" align="center" width="200">
<template slot="header" slot-scope="scope"> <template slot="header" slot-scope="scope">
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="isDropdownOpen = $event"> <el-dropdown trigger="click" @command="handleSelectModelType"
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }"> @visible-change="isDropdownOpen = $event">
类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow" :class="{ 'is-active': isDropdownOpen }"></i> <span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
</span> 类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
<el-dropdown-menu slot="dropdown"> :class="{ 'is-active': isDropdownOpen }"></i>
<el-dropdown-item command="">全部</el-dropdown-item> </span>
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value"> <el-dropdown-menu slot="dropdown">
{{ item.label }} <el-dropdown-item command="">全部</el-dropdown-item>
</el-dropdown-item> <el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
</el-dropdown-menu> {{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown> </el-dropdown>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag :type="getModelTypeTag(scope.row.model_type)"> <el-tag :type="getModelTypeTag(scope.row.modelType)">
{{ getModelTypeLabel(scope.row.model_type) }} {{ getModelTypeLabel(scope.row.modelType) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="供应器编码" prop="provider_code" align="center" width="150"></el-table-column> <el-table-column label="供应器编码" prop="providerCode" align="center" width="150"></el-table-column>
<el-table-column label="名称" prop="name" align="center"></el-table-column> <el-table-column label="名称" prop="name" align="center"></el-table-column>
<el-table-column label="字段配置" align="center"> <el-table-column label="字段配置" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -97,7 +101,8 @@
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页 上一页
</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" :class="{ active: page === currentPage }" @click="goToPage(page)"> <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext"> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
@@ -112,33 +117,35 @@
</div> </div>
<!-- 新增/编辑供应器对话框 --> <!-- 新增/编辑供应器对话框 -->
<provider-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="providerForm" :model-types="modelTypes" @submit="handleSubmit" @cancel="dialogVisible = false"/> <provider-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="providerForm" :model-types="modelTypes"
@submit="handleSubmit" @cancel="dialogVisible = false" />
<el-footer> <el-footer>
<version-footer/> <version-footer />
</el-footer> </el-footer>
</div> </div>
</template> </template>
<script> <script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import ProviderDialog from "@/components/ProviderDialog.vue"; import ProviderDialog from "@/components/ProviderDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue"; import VersionFooter from "@/components/VersionFooter.vue";
export default { export default {
components: {HeaderBar, ProviderDialog, VersionFooter}, components: { HeaderBar, ProviderDialog, VersionFooter },
data() { data() {
return { return {
searchName: "", searchName: "",
searchModelType: "", searchModelType: "",
providersList: [], providersList: [],
modelTypes: [ modelTypes: [
{value: "ASR", label: "语音识别"}, { value: "ASR", label: "语音识别" },
{value: "TTS", label: "语音合成"}, { value: "TTS", label: "语音合成" },
{value: "LLM", label: "大语言模型"}, { value: "LLM", label: "大语言模型" },
{value: "Intent", label: "意图识别"}, { value: "Intent", label: "意图识别" },
{value: "Memory", label: "记忆模块"}, { value: "Memory", label: "记忆模块" },
{value: "VAD", label: "语音活动检测"} { value: "VAD", label: "语音活动检测" }
], ],
currentPage: 1, currentPage: 1,
loading: false, loading: false,
@@ -152,8 +159,8 @@ export default {
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"], sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
providerForm: { providerForm: {
id: null, id: null,
model_type: "", modelType: "",
provider_code: "", providerCode: "",
name: "", name: "",
fields: [], fields: [],
sort: 0 sort: 0
@@ -189,123 +196,50 @@ export default {
return pages; return pages;
}, },
filteredProvidersList() { filteredProvidersList() {
let list = this.providersList.filter(item => { return this.providersList;
const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
return nameMatch && typeMatch;
});
list.sort((a, b) => a.sort - b.sort); // let list = this.providersList.filter(item => {
// const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
// const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
// return nameMatch && typeMatch;
// });
// 分页处理 // list.sort((a, b) => a.sort - b.sort);
const start = (this.currentPage - 1) * this.pageSize;
return list.slice(start, start + this.pageSize); // // 分页处理
// const start = (this.currentPage - 1) * this.pageSize;
// return list.slice(start, start + this.pageSize);
} }
}, },
methods: { methods: {
fetchProviders() { fetchProviders() {
this.loading = true; this.loading = true;
// 模拟API请求延迟 Api.model.getModelProvidersPage(
setTimeout(() => { {
this.loading = false; page: this.currentPage,
limit: this.pageSize,
// 模拟数据 - 从数据库结构中提取 name: this.searchName,
this.providersList = [ modelType: this.searchModelType
{ },
id: "SYSTEM_ASR_DoubaoASR", ({ data }) => {
model_type: "ASR", this.loading = false;
provider_code: "doubao", if (data.code === 0) {
name: "火山引擎语音识别", this.providersList = data.data.list.map(item => {
fields: JSON.parse('[{"key": "appid", "type": "string", "label": "应用ID"}, {"key": "access_token", "type": "string", "label": "访问令牌"}, {"key": "cluster", "type": "string", "label": "集群"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]'), return {
sort: 3, ...item,
selected: false selected: false,
}, fields: JSON.parse(item.fields)
{ };
id: "SYSTEM_ASR_FunASR", });
model_type: "ASR", this.total = data.data.total;
provider_code: "fun_local", } else {
name: "FunASR语音识别", this.$message.error({
fields: JSON.parse('[{"key": "model_dir", "type": "string", "label": "模型目录"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]'), message: data.msg || '获取参数列表失败'
sort: 1, });
selected: false
},
{
id: "SYSTEM_LLM_openai",
model_type: "LLM",
provider_code: "openai",
name: "OpenAI接口",
fields: JSON.parse('[{"key": "base_url", "type": "string", "label": "基础URL"}, {"key": "model_name", "type": "string", "label": "模型名称"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "temperature", "type": "number", "label": "温度"}, {"key": "max_tokens", "type": "number", "label": "最大令牌数"}]'),
sort: 1,
selected: false
},
{
id: "SYSTEM_TTS_edge",
model_type: "TTS",
provider_code: "edge",
name: "Edge TTS",
fields: JSON.parse('[{"key": "voice", "type": "string", "label": "音色"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]'),
sort: 1,
selected: false
},
{
id: "SYSTEM_Memory_mem0ai",
model_type: "Memory",
provider_code: "mem0ai",
name: "Mem0AI记忆",
fields: JSON.parse('[{"key": "api_key", "type": "string", "label": "API密钥"}]'),
sort: 1,
selected: false
},
{
id: "SYSTEM_VAD_SileroVAD",
model_type: "VAD",
provider_code: "silero",
name: "SileroVAD语音活动检测",
fields: JSON.parse('[{"key": "threshold", "type": "number", "label": "检测阈值"}, {"key": "model_dir", "type": "string", "label": "模型目录"}, {"key": "min_silence_duration_ms", "type": "number", "label": "最小静音时长"}]'),
sort: 1,
selected: false
},
{
id: "SYSTEM_Intent_nointent",
model_type: "Intent",
provider_code: "nointent",
name: "无意图识别",
fields: [],
sort: 1,
selected: false
},
{
id: "SYSTEM_TTS_aliyun",
model_type: "TTS",
provider_code: "aliyun",
name: "阿里云TTS",
fields: JSON.parse('[{"key": "output_dir", "type": "string", "label": "输出目录"}, {"key": "appkey", "type": "string", "label": "应用密钥"}, {"key": "token", "type": "string", "label": "访问令牌"}, {"key": "voice", "type": "string", "label": "音色"}, {"key": "access_key_id", "type": "string", "label": "访问密钥ID"}, {"key": "access_key_secret", "type": "string", "label": "访问密钥密码"}]'),
sort: 9,
selected: false
},
{
id: "SYSTEM_LLM_coze",
model_type: "LLM",
provider_code: "coze",
name: "Coze接口",
fields: JSON.parse('[{"key": "bot_id", "type": "string", "label": "机器人ID"}, {"key": "user_id", "type": "string", "label": "用户ID"}, {"key": "personal_access_token", "type": "string", "label": "个人访问令牌"}]'),
sort: 6,
selected: false
},
{
id: "SYSTEM_TTS_TencentTTS",
model_type: "TTS",
provider_code: "tencent",
name: "腾讯语音合成",
fields: JSON.parse('[{"key": "appid", "type": "string", "label": "应用ID"}, {"key": "secret_id", "type": "string", "label": "Secret ID"}, {"key": "secret_key", "type": "string", "label": "Secret Key"}, {"key": "output_dir", "type": "string", "label": "输出目录"}, {"key": "region", "type": "string", "label": "区域"}, {"key": "voice", "type": "string", "label": "音色ID"}]'),
sort: 5,
selected: false
} }
]; }
);
this.total = this.providersList.length;
}, 500);
}, },
handleSearch() { handleSearch() {
this.currentPage = 1; this.currentPage = 1;
@@ -326,8 +260,8 @@ export default {
this.dialogTitle = "新增供应器"; this.dialogTitle = "新增供应器";
this.providerForm = { this.providerForm = {
id: null, id: null,
model_type: "", modelType: "",
provider_code: "", providerCode: "",
name: "", name: "",
fields: [], fields: [],
sort: 0 sort: 0
@@ -342,43 +276,36 @@ export default {
}; };
this.dialogVisible = true; this.dialogVisible = true;
}, },
handleSubmit({form, done}) { handleSubmit({ form, done }) {
this.loading = true; this.loading = true;
setTimeout(() => { if (form.id) {
this.loading = false; // 编辑
Api.model.updateModelProvider(form, ({ data }) => {
if (form.id) { if (data.code === 0) {
// 模拟编辑操作 this.fetchProviders(); // 刷新表格
const index = this.providersList.findIndex(p => p.id === form.id);
if (index !== -1) {
this.providersList.splice(index, 1, {
...form,
fields: typeof form.fields === 'string' ? JSON.parse(form.fields) : form.fields
});
this.$message.success({ this.$message.success({
message: "修改成功", message: "修改成功",
showClose: true showClose: true
}); });
} }
} else { });
// 模拟新增操作 } else {
const newId = `SYSTEM_${form.model_type}_${form.provider_code}`; // 新增
this.providersList.unshift({ Api.model.addModelProvider(form, ({ data }) => {
...form, if (data.code === 0) {
id: newId, this.fetchProviders(); // 刷新表格
fields: typeof form.fields === 'string' ? JSON.parse(form.fields) : form.fields, this.$message.success({
selected: false message: "新增成功",
}); showClose: true
this.total += 1; });
this.$message.success({ this.total += 1;
message: "新增成功", }
showClose: true });
}); }
} this.loading = false;
this.dialogVisible = false;
this.dialogVisible = false; done && done();
done && done();
}, 500);
}, },
deleteSelectedProviders() { deleteSelectedProviders() {
const selectedRows = this.providersList.filter(row => row.selected); const selectedRows = this.providersList.filter(row => row.selected);
@@ -398,16 +325,25 @@ export default {
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', { this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
}).then(() => { }).then(() => {
const ids = providers.map(provider => provider.id); const ids = providers.map(provider => provider.id);
// 模拟删除操作 Api.model.deleteModelProviderByIds(ids, ({ data }) => {
this.providersList = this.providersList.filter(p => !ids.includes(p.id)); if (data.code === 0) {
this.total = this.providersList.length;
this.$message.success({ this.isAllSelected = false;
message: `成功删除${providerCount}个供应器`, this.fetchProviders(); // 刷新表格
showClose: true
this.$message.success({
message: `成功删除${providerCount}个参数`,
showClose: true
});
} else {
this.$message.error({
message: data.msg || '删除失败,请重试',
showClose: true
});
}
}); });
}).catch(() => { }).catch(() => {
this.$message({ this.$message({
@@ -434,8 +370,9 @@ export default {
return typeItem ? typeItem.label : type; return typeItem ? typeItem.label : type;
}, },
isSensitiveField(fieldKey) { isSensitiveField(fieldKey) {
if (typeof fieldKey !== 'string') return false;
return this.sensitive_keys.some(key => return this.sensitive_keys.some(key =>
fieldKey.toLowerCase().includes(key.toLowerCase()) fieldKey.toLowerCase().includes(key.toLowerCase())
); );
}, },
handlePageSizeChange(val) { handlePageSizeChange(val) {
@@ -443,7 +380,7 @@ export default {
this.currentPage = 1; this.currentPage = 1;
this.fetchProviders(); this.fetchProviders();
}, },
headerCellClassName({columnIndex}) { headerCellClassName({ columnIndex }) {
if (columnIndex === 0) { if (columnIndex === 0) {
return "custom-selection-header"; return "custom-selection-header";
} }
@@ -461,6 +398,7 @@ export default {
}, },
goNext() { goNext() {
if (this.currentPage < this.pageCount) { if (this.currentPage < this.pageCount) {
console.log("this.currentPage", this.currentPage);
this.currentPage++; this.currentPage++;
this.fetchProviders(); this.fetchProviders();
} }
@@ -552,7 +490,7 @@ export default {
flex-direction: column; flex-direction: column;
} }
.el-card{ .el-card {
border: none; border: none;
} }
@@ -563,6 +501,7 @@ export default {
flex-direction: column; flex-direction: column;
box-shadow: none; box-shadow: none;
overflow: hidden; overflow: hidden;
::v-deep .el-card__body { ::v-deep .el-card__body {
padding: 15px; padding: 15px;
display: flex; display: flex;
@@ -748,7 +687,7 @@ export default {
padding-bottom: 16px; padding-bottom: 16px;
} }
& + tr { &+tr {
margin-top: 10px; margin-top: 10px;
} }
} }
@@ -918,19 +857,20 @@ export default {
.el-icon-arrow-down { .el-icon-arrow-down {
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.dropdown-trigger {
font-size: 14px;
color: #303133;
cursor: pointer;
display: flex;
align-items: center;
&:hover { .dropdown-trigger {
color: #409EFF; font-size: 14px;
} color: #303133;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
color: #409EFF;
}
} }
.dropdown-trigger.active { .dropdown-trigger.active {
color: #409EFF; color: #409EFF;
} }
</style> </style>
@@ -0,0 +1,475 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">服务端管理</h2>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
<el-table-column label="选择" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="ws地址" prop="address" align="center"></el-table-column>
<el-table-column label="操作" prop="operator" align="center" show-overflow-tooltip>
<template slot-scope="scope">
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">重启</el-button>
<el-button size="medium" type="text"
@click="emitAction(scope.row, actionMap.update_config)">更新配置</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</div>
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import ParamDialog from "@/components/ParamDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
export default {
components: { HeaderBar, ParamDialog, VersionFooter },
data() {
return {
paramsList: [],
actionMap: {
restart: {
value: 'restart',
title: "重启服务端",
message: "确定要重启服务端吗?",
confirmText: "重启",
},
update_config: {
value: 'update_config',
title: "更新配置",
message: "确定要更新配置吗?",
confirmText: "更新",
}
},
currentPage: 1,
loading: false,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
dialogVisible: false,
dialogTitle: "新增参数",
isAllSelected: false,
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
paramForm: {
id: null,
paramCode: "",
paramValue: "",
remark: ""
},
};
},
created() {
this.fetchParams();
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
},
methods: {
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchParams();
},
fetchParams() {
this.loading = true;
Api.admin.getWsServerList(
{},
({ data }) => {
this.loading = false;
if (data.code === 0) {
this.paramsList = data.data.map(item => ({ address: item }));
this.total = data.data.length;
} else {
this.$message.error({
message: data.msg || '获取参数列表失败',
showClose: true
});
}
}
);
},
emitAction(rowItem, actionItem) {
if (actionItem === undefined || rowItem.address === undefined) {
return;
}
// 弹开询问框
this.$confirm(actionItem.message, actionItem.title, {
confirmButtonText: actionItem.confirmText, // 确认按钮文本
}).then(() => {
// 用户点击了确认按钮
Api.admin.sendWsServerAction({
targetWs: rowItem.address,
action: actionItem.value
}, ({ data }) => {
if (data.code !== 0) {
this.$message.error({
message: data.msg || '操作失败',
showClose: true
});
return;
}
this.$message.success({
message: `${actionItem.title}成功`,
showClose: true
})
})
})
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
return "";
}
},
};
</script>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
margin: 5px 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow: auto;
background-color: white;
display: flex;
flex-direction: column;
}
.params-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-bottom: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
}
&::before {
display: none;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
}
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
:deep(.el-table .el-button--text) {
color: #7079aa;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
.el-button--success {
background: #5bc98c;
color: white;
}
:deep(.el-table .cell) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.el-table) {
.el-table__body-wrapper {
transition: height 0.3s ease;
}
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .circular) {
width: 28px;
height: 28px;
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
:deep(.el-loading-text) {
color: #6b8cff !important;
font-size: 14px;
margin-top: 8px;
}
</style>
+1 -14
View File
@@ -66,20 +66,7 @@
cursor: pointer; cursor: pointer;
color: #fff; color: #fff;
line-height: 35px; line-height: 35px;
margin: 35px 15px 15px; margin: 15px 30px 15px 30px;
}
.code-send {
width: 70px;
height: 32px;
border-radius: 10px;
background: #e6ebff;
line-height: 32px;
font-weight: 400;
font-size: 14px;
color: #5778ff;
flex-shrink: 0;
cursor: pointer;
} }
.input-box { .input-box {
+105 -19
View File
@@ -20,10 +20,27 @@
</div> </div>
</div> </div>
<div style="padding: 0 30px;"> <div style="padding: 0 30px;">
<div class="input-box"> <!-- 用户名登录 -->
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" /> <template v-if="!isMobileLogin">
<el-input v-model="form.username" placeholder="请输入用户名" /> <div class="input-box">
</div> <img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
<el-input v-model="form.username" placeholder="请输入用户名" />
</div>
</template>
<!-- 手机号登录 -->
<template v-else>
<div class="input-box">
<div style="display: flex; align-items: center; width: 100%;">
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" />
</el-select>
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
</div>
</div>
</template>
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.password" placeholder="请输入密码" type="password" /> <el-input v-model="form.password" placeholder="请输入密码" type="password" />
@@ -39,9 +56,23 @@
<div <div
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;"> style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div> <div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
<div style="cursor: pointer;" @click="goToForgetPassword" v-if="enableMobileRegister">忘记密码?</div>
</div> </div>
</div> </div>
<div class="login-btn" @click="login">登录</div> <div class="login-btn" @click="login">登录</div>
<!-- 登录方式切换按钮 -->
<div class="login-type-container" v-if="enableMobileRegister">
<el-tooltip content="手机号码登录" placement="bottom">
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
@click="switchLoginType('mobile')"></el-button>
</el-tooltip>
<el-tooltip content="用户名登录" placement="bottom">
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
@click="switchLoginType('username')"></el-button>
</el-tooltip>
</div>
<div style="font-size: 14px;color: #979db1;"> <div style="font-size: 14px;color: #979db1;">
登录即同意 登录即同意
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div> <div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div>
@@ -60,7 +91,7 @@
<script> <script>
import Api from '@/apis/api'; import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue'; import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils'; import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
@@ -70,7 +101,9 @@ export default {
}, },
computed: { computed: {
...mapState({ ...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister allowUserRegister: state => state.pubConfig.allowUserRegister,
enableMobileRegister: state => state.pubConfig.enableMobileRegister,
mobileAreaList: state => state.pubConfig.mobileAreaList
}) })
}, },
data() { data() {
@@ -80,15 +113,21 @@ export default {
username: '', username: '',
password: '', password: '',
captcha: '', captcha: '',
captchaId: '' captchaId: '',
areaCode: '+86',
mobile: ''
}, },
captchaUuid: '', captchaUuid: '',
captchaUrl: '' captchaUrl: '',
isMobileLogin: false
} }
}, },
mounted() { mounted() {
this.fetchCaptcha(); this.fetchCaptcha();
this.$store.dispatch('fetchPubConfig'); this.$store.dispatch('fetchPubConfig').then(() => {
// 根据配置决定默认登录方式
this.isMobileLogin = this.enableMobileRegister;
});
}, },
methods: { methods: {
fetchCaptcha() { fetchCaptcha() {
@@ -110,6 +149,17 @@ export default {
} }
}, },
// 切换登录方式
switchLoginType(type) {
this.isMobileLogin = type === 'mobile';
// 清空表单
this.form.username = '';
this.form.mobile = '';
this.form.password = '';
this.form.captcha = '';
this.fetchCaptcha();
},
// 封装输入验证逻辑 // 封装输入验证逻辑
validateInput(input, message) { validateInput(input, message) {
if (!input.trim()) { if (!input.trim()) {
@@ -120,10 +170,21 @@ export default {
}, },
async login() { async login() {
// 验证用户名 if (this.isMobileLogin) {
if (!this.validateInput(this.form.username, '用户名不能为空')) { // 手机号登录验证
return; if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码');
return;
}
// 拼接手机号作为用户名
this.form.username = this.form.areaCode + this.form.mobile;
} else {
// 用户名登录验证
if (!this.validateInput(this.form.username, '用户名不能为空')) {
return;
}
} }
// 验证密码 // 验证密码
if (!this.validateInput(this.form.password, '密码不能为空')) { if (!this.validateInput(this.form.password, '密码不能为空')) {
return; return;
@@ -135,12 +196,13 @@ export default {
this.form.captchaId = this.captchaUuid this.form.captchaId = this.captchaUuid
Api.user.login(this.form, ({ data }) => { Api.user.login(this.form, ({ data }) => {
if (data.code === 0) { showSuccess('登录成功!');
showSuccess('登录成功!'); this.$store.commit('setToken', JSON.stringify(data.data));
this.$store.commit('setToken', JSON.stringify(data.data)); goToPage('/home');
goToPage('/home'); }, (err) => {
} else { showDanger(err.data.msg || '登录失败')
showDanger(data.msg || '登录失败'); if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
this.fetchCaptcha()
} }
}) })
@@ -153,8 +215,32 @@ export default {
goToRegister() { goToRegister() {
goToPage('/register') goToPage('/register')
}, },
goToForgetPassword() {
goToPage('/retrieve-password')
},
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import './auth.scss'; // 添加这行引用</style> @import './auth.scss';
.login-type-container {
margin: 10px 20px;
}
:deep(.el-button--primary) {
background-color: #5778ff;
border-color: #5778ff;
&:hover,
&:focus {
background-color: #4a6ae8;
border-color: #4a6ae8;
}
&:active {
background-color: #3d5cd6;
border-color: #3d5cd6;
}
}
</style>
+180 -48
View File
@@ -23,38 +23,78 @@
</div> </div>
<div style="padding: 0 30px;"> <div style="padding: 0 30px;">
<!-- 用户名输入框 --> <form @submit.prevent="register">
<div class="input-box"> <!-- 用户名/手机号输入框 -->
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" /> <div class="input-box" v-if="!enableMobileRegister">
<el-input v-model="form.username" placeholder="请输入用户名" /> <img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
</div> <el-input v-model="form.username" placeholder="请输入用户名" />
<!-- 密码输入框 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.password" placeholder="请输入密码" type="password" />
</div>
<!-- 新增确认密码 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" />
</div>
<!-- 验证码部分保持相同 -->
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
</div> </div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
</div>
<!-- 修改底部链接 --> <!-- 手机号注册部分 -->
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;"> <template v-if="enableMobileRegister">
<div style="cursor: pointer;" @click="goToLogin">已有账号立即登录</div> <div class="input-box">
</div> <div style="display: flex; align-items: center; width: 100%;">
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" />
</el-select>
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
</div>
</div>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
</div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
</div>
<!-- 手机验证码 -->
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
</div>
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
@click="sendMobileCaptcha">
<span>
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
</span>
</el-button>
</div>
</template>
<!-- 密码输入框 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.password" placeholder="请输入密码" type="password" />
</div>
<!-- 新增确认密码 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" />
</div>
<!-- 验证码部分保持相同 -->
<div v-if="!enableMobileRegister"
style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
</div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
</div>
<!-- 修改底部链接 -->
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
<div style="cursor: pointer;" @click="goToLogin">已有账号立即登录</div>
</div>
</form>
</div> </div>
<!-- 修改按钮文本 --> <!-- 修改按钮文本 -->
@@ -81,7 +121,7 @@
<script> <script>
import Api from '@/apis/api'; import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue'; import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils'; import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
@@ -91,8 +131,13 @@ export default {
}, },
computed: { computed: {
...mapState({ ...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister allowUserRegister: state => state.pubConfig.allowUserRegister,
}) enableMobileRegister: state => state.pubConfig.enableMobileRegister,
mobileAreaList: state => state.pubConfig.mobileAreaList
}),
canSendMobileCaptcha() {
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
}
}, },
data() { data() {
return { return {
@@ -101,9 +146,14 @@ export default {
password: '', password: '',
confirmPassword: '', confirmPassword: '',
captcha: '', captcha: '',
captchaId: '' captchaId: '',
areaCode: '+86',
mobile: '',
mobileCaptcha: ''
}, },
captchaUrl: '' captchaUrl: '',
countdown: 0,
timer: null
} }
}, },
mounted() { mounted() {
@@ -141,12 +191,70 @@ export default {
} }
return true; return true;
}, },
// 注册逻辑
register() { // 发送手机验证码
// 验证用户名 sendMobileCaptcha() {
if (!this.validateInput(this.form.username, '用户名不能为空')) { if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码');
return; return;
} }
// 验证图形验证码
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
this.fetchCaptcha();
return;
}
// 清除可能存在的旧定时器
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
// 开始倒计时
this.countdown = 60;
this.timer = setInterval(() => {
if (this.countdown > 0) {
this.countdown--;
} else {
clearInterval(this.timer);
this.timer = null;
}
}, 1000);
// 调用发送验证码接口
Api.user.sendSmsVerification({
phone: this.form.areaCode + this.form.mobile,
captcha: this.form.captcha,
captchaId: this.form.captchaId
}, (res) => {
showSuccess('验证码发送成功');
}, (err) => {
showDanger(err.data.msg || '验证码发送失败');
this.countdown = 0;
this.fetchCaptcha();
});
},
// 注册逻辑
register() {
if (this.enableMobileRegister) {
// 手机号注册验证
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码');
return;
}
if (!this.form.mobileCaptcha) {
showDanger('请输入手机验证码');
return;
}
} else {
// 用户名注册验证
if (!this.validateInput(this.form.username, '用户名不能为空')) {
return;
}
}
// 验证密码 // 验证密码
if (!this.validateInput(this.form.password, '密码不能为空')) { if (!this.validateInput(this.form.password, '密码不能为空')) {
return; return;
@@ -160,26 +268,50 @@ export default {
return; return;
} }
if (this.enableMobileRegister) {
this.form.username = this.form.areaCode + this.form.mobile
}
Api.user.register(this.form, ({ data }) => { Api.user.register(this.form, ({ data }) => {
if (data.code === 0) { showSuccess('注册成功!')
showSuccess('注册成功!') goToPage('/login')
goToPage('/login') }, (err) => {
} else { showDanger(err.data.msg || '注册失败')
showDanger(data.msg || '注册失败') if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
this.fetchCaptcha() this.fetchCaptcha()
} }
}) })
setTimeout(() => {
this.fetchCaptcha()
}, 1000)
}, },
goToLogin() { goToLogin() {
goToPage('/login') goToPage('/login')
} }
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import './auth.scss'; // 修改为导入新建的SCSS文件</style> @import './auth.scss';
.send-captcha-btn {
margin-right: -5px;
min-width: 100px;
height: 40px;
line-height: 40px;
border-radius: 4px;
font-size: 14px;
background: rgb(87, 120, 255);
border: none;
padding: 0px;
&:disabled {
background: #c0c4cc;
cursor: not-allowed;
}
}
</style>
@@ -0,0 +1,275 @@
<template>
<div class="welcome" @keyup.enter="retrievePassword">
<el-container style="height: 100%;">
<!-- 保持相同的头部 -->
<el-header>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
</div>
</el-header>
<div class="login-person">
<img loading="lazy" alt="" src="@/assets/login/register-person.png" style="width: 100%;" />
</div>
<el-main style="position: relative;">
<form @submit.prevent="retrievePassword">
<div class="login-box">
<!-- 修改标题部分 -->
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
<div class="login-text">重置密码</div>
<div class="login-welcome">
PASSWORD RETRIEVE
</div>
</div>
<div style="padding: 0 30px;">
<!-- 手机号输入 -->
<div class="input-box">
<div style="display: flex; align-items: center; width: 100%;">
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" />
</el-select>
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
</div>
</div>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
</div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
</div>
<!-- 手机验证码 -->
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
</div>
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
@click="sendMobileCaptcha">
<span>
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
</span>
</el-button>
</div>
<!-- 新密码 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" />
</div>
<!-- 确认新密码 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.confirmPassword" placeholder="请确认新密码" type="password" />
</div>
<!-- 修改底部链接 -->
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
<div style="cursor: pointer;" @click="goToLogin">返回登录</div>
</div>
</div>
<!-- 修改按钮文本 -->
<div class="login-btn" @click="retrievePassword">立即修改</div>
<!-- 保持相同的协议声明 -->
<div style="font-size: 14px;color: #979db1;">
同意
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div>
<div style="display: inline-block;color: #5778FF;cursor: pointer;">隐私政策</div>
</div>
</div>
</form>
</el-main>
<!-- 保持相同的页脚 -->
<el-footer>
<version-footer />
</el-footer>
</el-container>
</div>
</template>
<script>
import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
import { mapState } from 'vuex';
export default {
name: 'retrieve',
components: {
VersionFooter
},
computed: {
...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister,
mobileAreaList: state => state.pubConfig.mobileAreaList
}),
canSendMobileCaptcha() {
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
}
},
data() {
return {
form: {
areaCode: '+86',
mobile: '',
captcha: '',
captchaId: '',
smsCode: '',
newPassword: '',
confirmPassword: ''
},
captchaUrl: '',
countdown: 0,
timer: null
}
},
mounted() {
this.fetchCaptcha();
},
methods: {
// 复用验证码获取方法
fetchCaptcha() {
this.form.captchaId = getUUID();
Api.user.getCaptcha(this.form.captchaId, (res) => {
if (res.status === 200) {
const blob = new Blob([res.data], { type: res.data.type });
this.captchaUrl = URL.createObjectURL(blob);
} else {
console.error('验证码加载异常:', error);
showDanger('验证码加载失败,点击刷新');
}
});
},
// 封装输入验证逻辑
validateInput(input, message) {
if (!input.trim()) {
showDanger(message);
return false;
}
return true;
},
// 发送手机验证码
sendMobileCaptcha() {
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码');
return;
}
// 验证图形验证码
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
this.fetchCaptcha();
return;
}
// 清除可能存在的旧定时器
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
// 开始倒计时
this.countdown = 60;
this.timer = setInterval(() => {
if (this.countdown > 0) {
this.countdown--;
} else {
clearInterval(this.timer);
this.timer = null;
}
}, 1000);
// 调用发送验证码接口
Api.user.sendSmsVerification({
phone: this.form.areaCode + this.form.mobile,
captcha: this.form.captcha,
captchaId: this.form.captchaId
}, (res) => {
showSuccess('验证码发送成功');
}, (err) => {
showDanger(err.data.msg || '验证码发送失败');
this.countdown = 0;
this.fetchCaptcha();
});
},
// 修改逻辑
retrievePassword() {
// 验证逻辑
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码');
return;
}
if (!this.form.captcha) {
showDanger('请输入图形验证码');
return;
}
if (!this.form.mobileCaptcha) {
showDanger('请输入短信验证码');
return;
}
if (this.form.newPassword !== this.form.confirmPassword) {
showDanger('两次输入的密码不一致');
return;
}
Api.user.retrievePassword({
phone: this.form.areaCode + this.form.mobile,
password: this.form.newPassword,
code: this.form.mobileCaptcha
}, (res) => {
showSuccess('密码重置成功');
goToPage('/login');
}, (err) => {
showDanger(err.data.msg || '重置失败');
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
this.fetchCaptcha()
}
});
},
goToLogin() {
goToPage('/login')
}
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
}
}
</script>
<style lang="scss" scoped>
@import './auth.scss';
.send-captcha-btn {
margin-right: -5px;
min-width: 100px;
height: 40px;
line-height: 40px;
border-radius: 4px;
font-size: 14px;
background: rgb(87, 120, 255);
border: none;
padding: 0;
&:disabled {
background: #c0c4cc;
cursor: not-allowed;
}
}
</style>
+1 -1
View File
@@ -102,7 +102,7 @@ async def main():
await asyncio.wait( await asyncio.wait(
[stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task], [stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task],
timeout=3.0, timeout=3.0,
return_when=asyncio.ALL_COMPLETED return_when=asyncio.ALL_COMPLETED,
) )
print("服务器已关闭,程序退出。") print("服务器已关闭,程序退出。")
+37 -12
View File
@@ -85,6 +85,7 @@ module_test:
# 唤醒词,用于识别唤醒词还是讲话内容 # 唤醒词,用于识别唤醒词还是讲话内容
wakeup_words: wakeup_words:
- "你好小智" - "你好小智"
- "嘿你好呀"
- "你好小志" - "你好小志"
- "小爱同学" - "小爱同学"
- "你好小鑫" - "你好小鑫"
@@ -218,8 +219,12 @@ Memory:
# 不想使用记忆功能,可以使用nomem # 不想使用记忆功能,可以使用nomem
type: nomem type: nomem
mem_local_short: mem_local_short:
# 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器 # 本地记忆功能,通过selected_module的llm总结,数据保存在本地服务器,不会上传到外部服务器
type: mem_local_short type: mem_local_short
# 配备记忆存储独立的思考模型
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
# 如果你的不想使用selected_module.LLM记忆存储,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
llm: ChatGLMLLM
ASR: ASR:
FunASR: FunASR:
@@ -325,7 +330,7 @@ LLM:
# 定义LLM API类型 # 定义LLM API类型
type: openai type: openai
# 先开通服务,打开以下网址,开通的服务搜索Doubao-1.5-pro,开通它 # 先开通服务,打开以下网址,开通的服务搜索Doubao-1.5-pro,开通它
# 开通地址:https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false # 开通地址:https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false
# 免费额度500000token # 免费额度500000token
# 开通后,进入这里获取密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D # 开通后,进入这里获取密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D
base_url: https://ark.cn-beijing.volces.com/api/v3 base_url: https://ark.cn-beijing.volces.com/api/v3
@@ -411,7 +416,7 @@ LLM:
base_url: https://host/api/v1 base_url: https://host/api/v1
# 你可以在这里找到你的api_key # 你可以在这里找到你的api_key
# https://cloud.tryfastgpt.ai/account/apikey # https://cloud.tryfastgpt.ai/account/apikey
api_key: fastgpt-xxx api_key: 你的fastgpt密钥
variables: variables:
k: "v" k: "v"
k2: "v2" k2: "v2"
@@ -452,6 +457,19 @@ TTS:
speed_ratio: 1.0 speed_ratio: 1.0
volume_ratio: 1.0 volume_ratio: 1.0
pitch_ratio: 1.0 pitch_ratio: 1.0
#火山tts,支持双向流式tts
HuoshanDoubleStreamTTS:
type: huoshan_double_stream
# 访问 https://console.volcengine.com/speech/service/10007 开通语音合成大模型,购买音色
# 在页面底部获取appid和access_token
# 资源ID固定为:volc.service_type.10029(大模型语音合成及混音)
# 如果是机智云,把接口地址换成wss://bytedance.gizwitsapi.com/api/v3/tts/bidirection
# 机智云不需要天填 appid
ws_url: wss://openspeech.bytedance.com/api/v3/tts/bidirection
appid: 你的火山引擎语音合成服务appid
access_token: 你的火山引擎语音合成服务access_token
resource_id: volc.service_type.10029
speaker: zh_female_wanwanxiaohe_moon_bigtts
CosyVoiceSiliconflow: CosyVoiceSiliconflow:
type: siliconflow type: siliconflow
# 硅基流动TTS # 硅基流动TTS
@@ -675,17 +693,24 @@ TTS:
speed: 1 speed: 1
output_dir: tmp/ output_dir: tmp/
CustomTTS: CustomTTS:
# 自定义的TTS接口服务,请求参数可自定义 # 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务
# 要求接口使用GET方式请求,并返回音频文件 # 以本地部署的KokoroTTS为例
# 如果只有cpu运行:docker run -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest
# 如果只有gpu运行:docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
# 要求接口使用POST方式请求,并返回音频文件
type: custom type: custom
url: "http://127.0.0.1:9880/tts" method: POST
url: "http://127.0.0.1:8880/v1/audio/speech"
params: # 自定义请求参数 params: # 自定义请求参数
# text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容 input: "{prompt_text}"
# speaker: jok老师 response_format: "mp3"
# speed: 1 download_format: "mp3"
# foo: bar voice: "zf_xiaoxiao"
# testabc: 123456 lang_code: "z"
return_download_link: true
speed: 1
stream: false
headers: # 自定义请求头 headers: # 自定义请求头
# Authorization: Bearer xxxx # Authorization: Bearer xxxx
format: wav # 接口返回的音频格式 format: mp3 # 接口返回的音频格式
output_dir: tmp/ output_dir: tmp/
@@ -82,6 +82,8 @@ def ensure_directories(config):
# ASR/TTS模块输出目录 # ASR/TTS模块输出目录
for module in ["ASR", "TTS"]: for module in ["ASR", "TTS"]:
if config.get(module) is None:
continue
for provider in config.get(module, {}).values(): for provider in config.get(module, {}).values():
output_dir = provider.get("output_dir", "") output_dir = provider.get("output_dir", "")
if output_dir: if output_dir:
@@ -93,6 +95,10 @@ def ensure_directories(config):
selected_provider = selected_modules.get(module_type) selected_provider = selected_modules.get(module_type)
if not selected_provider: if not selected_provider:
continue continue
if config.get(module) is None:
continue
if config.get(selected_provider) is None:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {}) provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get("output_dir") output_dir = provider_config.get("output_dir")
if output_dir: if output_dir:
+80 -31
View File
@@ -4,7 +4,8 @@ from loguru import logger
from config.config_loader import load_config from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
SERVER_VERSION = "0.4.3" SERVER_VERSION = "0.5.1"
_logger_initialized = False
def get_module_abbreviation(module_name, module_dict): def get_module_abbreviation(module_name, module_dict):
@@ -43,41 +44,89 @@ def setup_logging():
"""从配置文件中读取日志配置,并设置日志输出格式和级别""" """从配置文件中读取日志配置,并设置日志输出格式和级别"""
config = load_config() config = load_config()
log_config = config["log"] log_config = config["log"]
log_format = log_config.get( global _logger_initialized
"log_format",
"<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>",
)
log_format_file = log_config.get(
"log_format_file",
"{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}",
)
selected_module_str = build_module_string(config.get("selected_module", {}))
log_format = log_format.replace("{version}", SERVER_VERSION) # 第一次初始化时配置日志
log_format = log_format.replace("{selected_module}", selected_module_str) if not _logger_initialized:
log_format_file = log_format_file.replace("{version}", SERVER_VERSION) logger.configure(extra={"selected_module": log_config.get("selected_module", "00000000000000")}) # 新增配置
log_format_file = log_format_file.replace("{selected_module}", selected_module_str) log_format = log_config.get(
"log_format",
"<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{extra[selected_module]}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>",
)
log_format_file = log_config.get(
"log_format_file",
"{time:YYYY-MM-DD HH:mm:ss} - {version_{extra[selected_module]}} - {name} - {level} - {extra[tag]} - {message}",
)
selected_module_str = logger._core.extra["selected_module"]
log_level = log_config.get("log_level", "INFO") log_format = log_format.replace("{version}", SERVER_VERSION)
log_dir = log_config.get("log_dir", "tmp") log_format = log_format.replace("{selected_module}", selected_module_str)
log_file = log_config.get("log_file", "server.log") log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
data_dir = log_config.get("data_dir", "data") log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
os.makedirs(log_dir, exist_ok=True) log_level = log_config.get("log_level", "INFO")
os.makedirs(data_dir, exist_ok=True) log_dir = log_config.get("log_dir", "tmp")
log_file = log_config.get("log_file", "server.log")
data_dir = log_config.get("data_dir", "data")
# 配置日志输出 os.makedirs(log_dir, exist_ok=True)
logger.remove() os.makedirs(data_dir, exist_ok=True)
# 输出到控制台 # 配置日志输出
logger.add(sys.stdout, format=log_format, level=log_level, filter=formatter) logger.remove()
# 输出到文件 # 输出到控制台
logger.add( logger.add(sys.stdout, format=log_format, level=log_level, filter=formatter)
os.path.join(log_dir, log_file),
format=log_format_file, # 输出到文件
level=log_level, logger.add(
filter=formatter, os.path.join(log_dir, log_file),
) format=log_format_file,
level=log_level,
filter=formatter,
)
_logger_initialized = True # 标记为已初始化
return logger return logger
def update_module_string(selected_module_str):
"""更新模块字符串并重新配置日志处理器"""
logger.debug(f"更新日志配置组件")
current_module = logger._core.extra["selected_module"]
if current_module == selected_module_str:
return
try:
logger.configure(extra={"selected_module": selected_module_str})
config = load_config()
log_config = config["log"]
log_format = log_config.get(
"log_format",
"<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{extra[selected_module]}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>",
)
log_format_file = log_config.get(
"log_format_file",
"{time:YYYY-MM-DD HH:mm:ss} - {version_{extra[selected_module]}} - {name} - {level} - {extra[tag]} - {message}",
)
log_format = log_format.replace("{version}", SERVER_VERSION)
log_format = log_format.replace("{selected_module}", selected_module_str)
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
logger.remove()
logger.add(sys.stdout, format=log_format, level=log_config.get("log_level", "INFO"), filter=formatter)
logger.add(
os.path.join(log_config.get("log_dir", "tmp"), log_config.get("log_file", "server.log")),
format=log_format_file,
level=log_config.get("log_level", "INFO"),
filter=formatter
)
except Exception as e:
logger.error(f"日志配置更新失败: {str(e)}")
raise
@@ -160,7 +160,7 @@ def save_mem_local_short(mac_address: str, short_momery: str) -> Optional[Dict]:
def report( def report(
mac_address: str, session_id: str, chat_type: int, content: str, audio mac_address: str, session_id: str, chat_type: int, content: str, audio, report_time
) -> Optional[Dict]: ) -> Optional[Dict]:
"""带熔断的业务方法示例""" """带熔断的业务方法示例"""
if not content or not ManageApiClient._instance: if not content or not ManageApiClient._instance:
@@ -174,6 +174,7 @@ def report(
"sessionId": session_id, "sessionId": session_id,
"chatType": chat_type, "chatType": chat_type,
"content": content, "content": content,
"reportTime": report_time,
"audioBase64": ( "audioBase64": (
base64.b64encode(audio).decode("utf-8") if audio else None base64.b64encode(audio).decode("utf-8") if audio else None
), ),
+230 -427
View File
@@ -1,39 +1,40 @@
import os import os
import sys
import copy import copy
import json import json
import subprocess
import sys
import uuid import uuid
import time import time
import queue import queue
import asyncio import asyncio
import traceback
import threading import threading
import traceback
import subprocess
import websockets import websockets
from typing import Dict, Any
from plugins_func.loadplugins import auto_import_modules
from config.logger import setup_logging
from core.utils.dialogue import Message, Dialogue
from core.handle.textHandle import handleTextMessage
from core.utils.util import ( from core.utils.util import (
get_string_no_punctuation_or_emoji,
extract_json_from_string, extract_json_from_string,
initialize_modules, initialize_modules,
check_vad_update, check_vad_update,
check_asr_update, check_asr_update,
filter_sensitive_info,
initialize_tts,
) )
from concurrent.futures import ThreadPoolExecutor, TimeoutError from typing import Dict, Any
from core.handle.sendAudioHandle import sendAudioMessage from core.mcp.manager import MCPManager
from core.handle.receiveAudioHandle import handleAudioMessage from core.handle.reportHandle import report
from core.providers.tts.default import DefaultTTS
from concurrent.futures import ThreadPoolExecutor
from core.utils.dialogue import Message, Dialogue
from core.handle.textHandle import handleTextMessage
from core.handle.functionHandler import FunctionHandler from core.handle.functionHandler import FunctionHandler
from plugins_func.loadplugins import auto_import_modules
from plugins_func.register import Action, ActionResponse from plugins_func.register import Action, ActionResponse
from core.auth import AuthMiddleware, AuthenticationError from core.auth import AuthMiddleware, AuthenticationError
from core.mcp.manager import MCPManager
from config.config_loader import get_private_config_from_api from config.config_loader import get_private_config_from_api
from core.handle.receiveAudioHandle import handleAudioMessage
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
from config.logger import setup_logging, build_module_string, update_module_string
from config.manage_api_client import DeviceNotFoundException, DeviceBindException from config.manage_api_client import DeviceNotFoundException, DeviceBindException
from core.utils.output_counter import add_device_output
from core.handle.reportHandle import enqueue_tts_report, report
TAG = __name__ TAG = __name__
@@ -51,7 +52,6 @@ class ConnectionHandler:
_vad, _vad,
_asr, _asr,
_llm, _llm,
_tts,
_memory, _memory,
_intent, _intent,
server=None, server=None,
@@ -84,24 +84,22 @@ class ConnectionHandler:
# 线程任务相关 # 线程任务相关
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.tts_queue = queue.Queue() self.executor = ThreadPoolExecutor(max_workers=5)
self.audio_play_queue = queue.Queue()
self.executor = ThreadPoolExecutor(max_workers=10)
# 上报线程 # 添加上报线程
self.report_queue = queue.Queue() self.report_queue = queue.Queue()
self.report_thread = None self.report_thread = None
# TODO(haotian): 2025/5/12 可以通过修改此处,调节asr的上报和tts的上报 # 未来可以通过修改此处,调节asr的上报和tts的上报,目前默认都开启
self.report_asr_enable = self.read_config_from_api self.report_asr_enable = self.read_config_from_api
self.report_tts_enable = self.read_config_from_api self.report_tts_enable = self.read_config_from_api
# 依赖的组件 # 依赖的组件
self.vad = None self.vad = None
self.asr = None self.asr = None
self.tts = None
self._asr = _asr self._asr = _asr
self._vad = _vad self._vad = _vad
self.llm = _llm self.llm = _llm
self.tts = _tts
self.memory = _memory self.memory = _memory
self.intent = _intent self.intent = _intent
@@ -117,12 +115,11 @@ class ConnectionHandler:
self.asr_server_receive = True self.asr_server_receive = True
# llm相关变量 # llm相关变量
self.llm_finish_task = False self.llm_finish_task = True
self.dialogue = Dialogue() self.dialogue = Dialogue()
# tts相关变量 # tts相关变量
self.tts_first_text_index = -1 self.sentence_id = None
self.tts_last_text_index = -1
# iot相关变量 # iot相关变量
self.iot_descriptors = {} self.iot_descriptors = {}
@@ -193,17 +190,6 @@ class ConnectionHandler:
self._initialize_private_config() self._initialize_private_config()
# 异步初始化 # 异步初始化
self.executor.submit(self._initialize_components) self.executor.submit(self._initialize_components)
# tts 消化线程
self.tts_priority_thread = threading.Thread(
target=self._tts_priority_thread, daemon=True
)
self.tts_priority_thread.start()
# 音频播放 消化线程
self.audio_play_priority_thread = threading.Thread(
target=self._audio_play_priority_thread, daemon=True
)
self.audio_play_priority_thread.start()
try: try:
async for message in self.websocket: async for message in self.websocket:
@@ -273,9 +259,10 @@ class ConnectionHandler:
await self.websocket.send( await self.websocket.send(
json.dumps( json.dumps(
{ {
"type": "server_response", "type": "server",
"status": "success", "status": "success",
"message": "服务器重启中...", "message": "服务器重启中...",
"content": {"action": "restart"},
} }
) )
) )
@@ -302,33 +289,48 @@ class ConnectionHandler:
await self.websocket.send( await self.websocket.send(
json.dumps( json.dumps(
{ {
"type": "server_response", "type": "server",
"status": "error", "status": "error",
"message": f"Restart failed: {str(e)}", "message": f"Restart failed: {str(e)}",
"content": {"action": "restart"},
} }
) )
) )
def _initialize_components(self): def _initialize_components(self):
"""初始化组件""" try:
if self.config.get("prompt") is not None: self.selected_module_str = build_module_string(
self.prompt = self.config["prompt"] self.config.get("selected_module", {})
self.change_system_prompt(self.prompt) )
self.logger.bind(tag=TAG).info( update_module_string(self.selected_module_str)
f"初始化组件: prompt成功 {self.prompt[:50]}..." """初始化组件"""
if self.config.get("prompt") is not None:
self.prompt = self.config["prompt"]
self.change_system_prompt(self.prompt)
self.logger.bind(tag=TAG).info(
f"初始化组件: prompt成功 {self.prompt[:50]}..."
)
"""初始化本地组件"""
if self.vad is None:
self.vad = self._vad
if self.asr is None:
self.asr = self._asr
if self.tts is None:
self.tts = self._initialize_tts()
# 使用事件循环运行异步方法
asyncio.run_coroutine_threadsafe(
self.tts.open_audio_channels(self), self.loop
) )
"""初始化本地组件""" """加载记忆"""
if self.vad is None: self._initialize_memory()
self.vad = self._vad """加载意图识别"""
if self.asr is None: self._initialize_intent()
self.asr = self._asr """初始化上报线程"""
"""加载记忆""" self._init_report_threads()
self._initialize_memory() except Exception as e:
"""加载意图识别""" self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}")
self._initialize_intent()
"""初始化上报线程"""
self._init_report_threads()
def _init_report_threads(self): def _init_report_threads(self):
"""初始化ASR和TTS上报线程""" """初始化ASR和TTS上报线程"""
@@ -343,6 +345,17 @@ class ConnectionHandler:
self.report_thread.start() self.report_thread.start()
self.logger.bind(tag=TAG).info("TTS上报线程已启动") self.logger.bind(tag=TAG).info("TTS上报线程已启动")
def _initialize_tts(self):
"""初始化TTS"""
tts = None
if not self.need_bind:
tts = initialize_tts(self.config)
if tts is None:
tts = DefaultTTS(self.config, delete_audio_file=True)
return tts
def _initialize_private_config(self): def _initialize_private_config(self):
"""如果是从配置文件获取,则进行二次实例化""" """如果是从配置文件获取,则进行二次实例化"""
if not self.read_config_from_api: if not self.read_config_from_api:
@@ -449,6 +462,37 @@ class ConnectionHandler:
save_to_file=not self.read_config_from_api, save_to_file=not self.read_config_from_api,
) )
# 获取记忆总结配置
memory_config = self.config["Memory"]
memory_type = self.config["Memory"][self.config["selected_module"]["Memory"]][
"type"
]
# 如果使用 nomen,直接返回
if memory_type == "nomem":
return
# 使用 mem_local_short 模式
elif memory_type == "mem_local_short":
memory_llm_name = memory_config[self.config["selected_module"]["Memory"]][
"llm"
]
if memory_llm_name and memory_llm_name in self.config["LLM"]:
# 如果配置了专用LLM,则创建独立的LLM实例
from core.utils import llm as llm_utils
memory_llm_config = self.config["LLM"][memory_llm_name]
memory_llm_type = memory_llm_config.get("type", memory_llm_name)
memory_llm = llm_utils.create_instance(
memory_llm_type, memory_llm_config
)
self.logger.bind(tag=TAG).info(
f"为记忆总结创建了专用LLM: {memory_llm_name}, 类型: {memory_llm_type}"
)
self.memory.set_llm(memory_llm)
else:
# 否则使用主LLM
self.memory.set_llm(self.llm)
self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
def _initialize_intent(self): def _initialize_intent(self):
self.intent_type = self.config["Intent"][ self.intent_type = self.config["Intent"][
self.config["selected_module"]["Intent"] self.config["selected_module"]["Intent"]
@@ -503,106 +547,20 @@ class ConnectionHandler:
# 更新系统prompt至上下文 # 更新系统prompt至上下文
self.dialogue.update_system_message(self.prompt) self.dialogue.update_system_message(self.prompt)
def chat(self, query): def chat(self, query, tool_call=False):
self.logger.bind(tag=TAG).info(f"大模型收到用户消息: {query}")
self.dialogue.put(Message(role="user", content=query))
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
try:
# 使用带记忆的对话
memory_str = None
if self.memory is not None:
future = asyncio.run_coroutine_threadsafe(
self.memory.query_memory(query), self.loop
)
memory_str = future.result()
self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
llm_responses = self.llm.response(
self.session_id, self.dialogue.get_llm_dialogue_with_memory(memory_str)
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None
self.llm_finish_task = False self.llm_finish_task = False
text_index = 0
for content in llm_responses:
response_message.append(content)
if self.client_abort:
break
# 合并当前全部文本并处理未分割部分
full_text = "".join(response_message)
current_text = full_text[processed_chars:] # 从未处理的位置开始
# 查找最后一个有效标点
punctuations = ("", ".", "", "?", "", "!", "", ";", "")
last_punct_pos = -1
number_flag = True
for punct in punctuations:
pos = current_text.rfind(punct)
prev_char = current_text[pos - 1] if pos - 1 >= 0 else ""
# 如果.前面是数字统一判断为小数
if prev_char.isdigit() and punct == ".":
number_flag = False
if pos > last_punct_pos and number_flag:
last_punct_pos = pos
# 找到分割点则处理
if last_punct_pos != -1:
segment_text_raw = current_text[: last_punct_pos + 1]
segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
if segment_text:
# 强制设置空字符,测试TTS出错返回语音的健壮性
# if text_index % 2 == 0:
# segment_text = " "
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(
self.speak_and_play, segment_text, text_index
)
self.tts_queue.put((future, text_index))
processed_chars += len(segment_text_raw) # 更新已处理字符位置
# 处理最后剩余的文本
full_text = "".join(response_message)
remaining_text = full_text[processed_chars:]
if remaining_text:
segment_text = get_string_no_punctuation_or_emoji(remaining_text)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(
self.speak_and_play, segment_text, text_index
)
self.tts_queue.put((future, text_index))
self.llm_finish_task = True
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
self.logger.bind(tag=TAG).debug(
json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)
)
return True
def chat_with_function_calling(self, query, tool_call=False):
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
"""Chat with function calling for intent detection using streaming"""
if not tool_call: if not tool_call:
self.dialogue.put(Message(role="user", content=query)) self.dialogue.put(Message(role="user", content=query))
# Define intent functions # Define intent functions
functions = None functions = None
if hasattr(self, "func_handler"): if self.intent_type == "function_call" and hasattr(self, "func_handler"):
functions = self.func_handler.get_functions() functions = self.func_handler.get_functions()
response_message = [] response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
try: try:
start_time = time.time()
# 使用带记忆的对话 # 使用带记忆的对话
memory_str = None memory_str = None
if self.memory is not None: if self.memory is not None:
@@ -611,94 +569,79 @@ class ConnectionHandler:
) )
memory_str = future.result() memory_str = future.result()
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}") uuid_str = str(uuid.uuid4()).replace("-", "")
self.sentence_id = uuid_str
# 使用支持functions的streaming接口 if functions is not None:
llm_responses = self.llm.response_with_functions( # 使用支持functions的streaming接口
self.session_id, llm_responses = self.llm.response_with_functions(
self.dialogue.get_llm_dialogue_with_memory(memory_str), self.session_id,
functions=functions, self.dialogue.get_llm_dialogue_with_memory(memory_str),
) functions=functions,
)
else:
llm_responses = self.llm.response(
self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str),
)
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}") self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None return None
self.llm_finish_task = False
text_index = 0
# 处理流式响应 # 处理流式响应
tool_call_flag = False tool_call_flag = False
function_name = None function_name = None
function_id = None function_id = None
function_arguments = "" function_arguments = ""
content_arguments = "" content_arguments = ""
text_index = 0
for response in llm_responses: for response in llm_responses:
content, tools_call = response if functions is not None:
content, tools_call = response
if "content" in response:
content = response["content"]
tools_call = None
if content is not None and len(content) > 0:
content_arguments += content
if "content" in response: if not tool_call_flag and content_arguments.startswith("<tool_call>"):
content = response["content"] # print("content_arguments", content_arguments)
tools_call = None tool_call_flag = True
if content is not None and len(content) > 0:
content_arguments += content
if not tool_call_flag and content_arguments.startswith("<tool_call>"):
# print("content_arguments", content_arguments)
tool_call_flag = True
if tools_call is not None:
tool_call_flag = True
if tools_call[0].id is not None:
function_id = tools_call[0].id
if tools_call[0].function.name is not None:
function_name = tools_call[0].function.name
if tools_call[0].function.arguments is not None:
function_arguments += tools_call[0].function.arguments
if tools_call is not None:
tool_call_flag = True
if tools_call[0].id is not None:
function_id = tools_call[0].id
if tools_call[0].function.name is not None:
function_name = tools_call[0].function.name
if tools_call[0].function.arguments is not None:
function_arguments += tools_call[0].function.arguments
else:
content = response
if content is not None and len(content) > 0: if content is not None and len(content) > 0:
if not tool_call_flag: if not tool_call_flag:
response_message.append(content) response_message.append(content)
if self.client_abort: if self.client_abort:
break break
end_time = time.time() if text_index == 0:
# self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}") self.tts.tts_text_queue.put(
TTSMessageDTO(
# 处理文本分段和TTS逻辑 sentence_id=self.sentence_id,
# 合并当前全部文本并处理未分割部分 sentence_type=SentenceType.FIRST,
full_text = "".join(response_message) content_type=ContentType.ACTION,
current_text = full_text[processed_chars:] # 从未处理的位置开始
# 查找最后一个有效标点
punctuations = ("", ".", "", "?", "", "!", "", ";", "")
last_punct_pos = -1
number_flag = True
for punct in punctuations:
pos = current_text.rfind(punct)
prev_char = current_text[pos - 1] if pos - 1 >= 0 else ""
# 如果.前面是数字统一判断为小数
if prev_char.isdigit() and punct == ".":
number_flag = False
if pos > last_punct_pos and number_flag:
last_punct_pos = pos
# 找到分割点则处理
if last_punct_pos != -1:
segment_text_raw = current_text[: last_punct_pos + 1]
segment_text = get_string_no_punctuation_or_emoji(
segment_text_raw
)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(
self.speak_and_play, segment_text, text_index
) )
self.tts_queue.put((future, text_index)) )
# 更新已处理字符位置 self.tts.tts_text_queue.put(
processed_chars += len(segment_text_raw) TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.TEXT,
content_detail=content,
)
)
text_index += 1
# 处理function call # 处理function call
if tool_call_flag: if tool_call_flag:
bHasError = False bHasError = False
@@ -741,27 +684,21 @@ class ConnectionHandler:
result = self.func_handler.handle_llm_function_call( result = self.func_handler.handle_llm_function_call(
self, function_call_data self, function_call_data
) )
self._handle_function_result(result, function_call_data, text_index + 1) self._handle_function_result(result, function_call_data)
# 处理最后剩余的文本
full_text = "".join(response_message)
remaining_text = full_text[processed_chars:]
if remaining_text:
segment_text = get_string_no_punctuation_or_emoji(remaining_text)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(
self.speak_and_play, segment_text, text_index
)
self.tts_queue.put((future, text_index))
# 存储对话内容 # 存储对话内容
if len(response_message) > 0: if len(response_message) > 0:
self.dialogue.put( self.dialogue.put(
Message(role="assistant", content="".join(response_message)) Message(role="assistant", content="".join(response_message))
) )
if text_index > 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
)
self.llm_finish_task = True self.llm_finish_task = True
self.logger.bind(tag=TAG).debug( self.logger.bind(tag=TAG).debug(
json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False) json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)
@@ -811,12 +748,10 @@ class ConnectionHandler:
return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="") return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="")
def _handle_function_result(self, result, function_call_data, text_index): def _handle_function_result(self, result, function_call_data):
if result.action == Action.RESPONSE: # 直接回复前端 if result.action == Action.RESPONSE: # 直接回复前端
text = result.response text = result.response
self.recode_first_last_text(text, text_index) self.tts.tts_one_sentence(self, ContentType.TEXT, content_detail=text)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put((future, text_index))
self.dialogue.put(Message(role="assistant", content=text)) self.dialogue.put(Message(role="assistant", content=text))
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复 elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
text = result.result text = result.result
@@ -850,111 +785,14 @@ class ConnectionHandler:
content=text, content=text,
) )
) )
self.chat_with_function_calling(text, tool_call=True) self.chat(text, tool_call=True)
elif result.action == Action.NOTFOUND or result.action == Action.ERROR: elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
text = result.result text = result.result
self.recode_first_last_text(text, text_index) self.tts.tts_one_sentence(self, ContentType.TEXT, content_detail=text)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put((future, text_index))
self.dialogue.put(Message(role="assistant", content=text)) self.dialogue.put(Message(role="assistant", content=text))
else: else:
pass pass
def _tts_priority_thread(self):
while not self.stop_event.is_set():
text = None
try:
try:
item = self.tts_queue.get(timeout=1)
if item is None:
continue
future, text_index = item # 解包获取 Future 和 text_index
except queue.Empty:
if self.stop_event.is_set():
break
continue
if future is None:
continue
text = None
audio_datas, tts_file = [], None
try:
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
tts_timeout = int(self.config.get("tts_timeout", 10))
tts_file, text, _ = future.result(timeout=tts_timeout)
if text is None or len(text) <= 0:
self.logger.bind(tag=TAG).error(
f"TTS出错:{text_index}: tts text is empty"
)
elif tts_file is None:
self.logger.bind(tag=TAG).error(
f"TTS出错: file is empty: {text_index}: {text}"
)
else:
self.logger.bind(tag=TAG).debug(
f"TTS生成:文件路径: {tts_file}"
)
if os.path.exists(tts_file):
if self.audio_format == "pcm":
audio_datas, _ = self.tts.audio_to_pcm_data(tts_file)
else:
audio_datas, _ = self.tts.audio_to_opus_data(tts_file)
# 在这里上报TTS数据
enqueue_tts_report(self, text, audio_datas)
else:
self.logger.bind(tag=TAG).error(
f"TTS出错:文件不存在{tts_file}"
)
except TimeoutError:
self.logger.bind(tag=TAG).error("TTS超时")
except Exception as e:
self.logger.bind(tag=TAG).error(f"TTS出错: {e}")
if not self.client_abort:
# 如果没有中途打断就发送语音
self.audio_play_queue.put((audio_datas, text, text_index))
if (
self.tts.delete_audio_file
and tts_file is not None
and os.path.exists(tts_file)
):
os.remove(tts_file)
except Exception as e:
self.logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}")
self.clearSpeakStatus()
asyncio.run_coroutine_threadsafe(
self.websocket.send(
json.dumps(
{
"type": "tts",
"state": "stop",
"session_id": self.session_id,
}
)
),
self.loop,
)
self.logger.bind(tag=TAG).error(
f"tts_priority priority_thread: {text} {e}"
)
def _audio_play_priority_thread(self):
while not self.stop_event.is_set():
text = None
try:
try:
audio_datas, text, text_index = self.audio_play_queue.get(timeout=1)
except queue.Empty:
if self.stop_event.is_set():
break
continue
future = asyncio.run_coroutine_threadsafe(
sendAudioMessage(self, audio_datas, text, text_index), self.loop
)
future.result()
except Exception as e:
self.logger.bind(tag=TAG).error(
f"audio_play_priority priority_thread: {text} {e}"
)
def _report_worker(self): def _report_worker(self):
"""聊天记录上报工作线程""" """聊天记录上报工作线程"""
while not self.stop_event.is_set(): while not self.stop_event.is_set():
@@ -964,16 +802,18 @@ class ConnectionHandler:
if item is None: # 检测毒丸对象 if item is None: # 检测毒丸对象
break break
type, text, audio_data = item type, text, audio_data, report_time = item
try: try:
# 执行上报(传入二进制数据) # 检查线程池状态
report(self, type, text, audio_data) if self.executor is None:
continue
# 提交任务到线程池
self.executor.submit(
self._process_report, type, text, audio_data, report_time
)
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"聊天记录上报线程异常: {e}") self.logger.bind(tag=TAG).error(f"聊天记录上报线程异常: {e}")
finally:
# 标记任务完成
self.report_queue.task_done()
except queue.Empty: except queue.Empty:
continue continue
except Exception as e: except Exception as e:
@@ -981,82 +821,79 @@ class ConnectionHandler:
self.logger.bind(tag=TAG).info("聊天记录上报线程已退出") self.logger.bind(tag=TAG).info("聊天记录上报线程已退出")
def speak_and_play(self, text, text_index=0): def _process_report(self, type, text, audio_data, report_time):
if text is None or len(text) <= 0: """处理上报任务"""
self.logger.bind(tag=TAG).info(f"无需tts转换,query为空,{text}") try:
return None, text, text_index # 执行上报(传入二进制数据)
tts_file = self.tts.to_tts(text) report(self, type, text, audio_data, report_time)
if tts_file is None: except Exception as e:
self.logger.bind(tag=TAG).error(f"tts转换失败,{text}") self.logger.bind(tag=TAG).error(f"上报处理异常: {e}")
return None, text, text_index finally:
self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}") # 标记任务完成
if self.max_output_size > 0: self.report_queue.task_done()
add_device_output(self.headers.get("device-id"), len(text))
return tts_file, text, text_index
def clearSpeakStatus(self): def clearSpeakStatus(self):
self.logger.bind(tag=TAG).debug(f"清除服务端讲话状态") self.logger.bind(tag=TAG).debug(f"清除服务端讲话状态")
self.asr_server_receive = True self.asr_server_receive = True
self.tts_last_text_index = -1
self.tts_first_text_index = -1
def recode_first_last_text(self, text, text_index=0):
if self.tts_first_text_index == -1:
self.logger.bind(tag=TAG).info(f"大模型说出第一句话: {text}")
self.tts_first_text_index = text_index
self.tts_last_text_index = text_index
async def close(self, ws=None): async def close(self, ws=None):
"""资源清理方法""" """资源清理方法"""
try:
# 取消超时任务
if self.timeout_task:
self.timeout_task.cancel()
self.timeout_task = None
# 取消超时任务 # 清理MCP资源
if self.timeout_task: if hasattr(self, "mcp_manager") and self.mcp_manager:
self.timeout_task.cancel() await self.mcp_manager.cleanup_all()
self.timeout_task = None
# 清理MCP资源 # 触发停止事件
if hasattr(self, "mcp_manager") and self.mcp_manager: if self.stop_event:
await self.mcp_manager.cleanup_all() self.stop_event.set()
# 触发停止事件 # 清空任务队列
if self.stop_event: self.clear_queues()
self.stop_event.set()
# 清空任务队列 # 关闭WebSocket连接
self.clear_queues() if ws:
await ws.close()
elif self.websocket:
await self.websocket.close()
# 关闭WebSocket连接 # 最后关闭线程池(避免阻塞)
if ws: if self.executor:
await ws.close() self.executor.shutdown(wait=False)
elif self.websocket: self.executor = None
await self.websocket.close()
# 最后关闭线程池(避免阻塞) self.logger.bind(tag=TAG).info("连接资源已释放")
if self.executor: except Exception as e:
self.executor.shutdown(wait=False) self.logger.bind(tag=TAG).error(f"关闭连接时出错: {e}")
self.executor = None
self.logger.bind(tag=TAG).info("连接资源已释放")
def clear_queues(self): def clear_queues(self):
"""清空所有任务队列""" """清空所有任务队列"""
self.logger.bind(tag=TAG).debug( if self.tts:
f"开始清理: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}" self.logger.bind(tag=TAG).debug(
) f"开始清理: TTS队列大小={self.tts.tts_text_queue.qsize()}, 音频队列大小={self.tts.tts_audio_queue.qsize()}"
)
# 使用非阻塞方式清空队列 # 使用非阻塞方式清空队列
for q in [self.tts_queue, self.audio_play_queue]: for q in [
if not q: self.tts.tts_text_queue,
continue self.tts.tts_audio_queue,
while True: self.report_queue,
try: ]:
q.get_nowait() if not q:
except queue.Empty: continue
break while True:
try:
q.get_nowait()
except queue.Empty:
break
self.logger.bind(tag=TAG).debug( self.logger.bind(tag=TAG).debug(
f"清理结束: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}" f"清理结束: TTS队列大小={self.tts.tts_text_queue.qsize()}, 音频队列大小={self.tts.tts_audio_queue.qsize()}"
) )
def reset_vad_states(self): def reset_vad_states(self):
self.client_audio_buffer = bytearray() self.client_audio_buffer = bytearray()
@@ -1087,37 +924,3 @@ class ConnectionHandler:
break break
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}") self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}")
def filter_sensitive_info(config: dict) -> dict:
"""
过滤配置中的敏感信息
Args:
config: 原始配置字典
Returns:
过滤后的配置字典
"""
sensitive_keys = [
"api_key",
"personal_access_token",
"access_token",
"token",
"secret",
"access_key_secret",
"secret_key",
]
def _filter_dict(d: dict) -> dict:
filtered = {}
for k, v in d.items():
if any(sensitive in k.lower() for sensitive in sensitive_keys):
filtered[k] = "***"
elif isinstance(v, dict):
filtered[k] = _filter_dict(v)
elif isinstance(v, list):
filtered[k] = [_filter_dict(i) if isinstance(i, dict) else i for i in v]
else:
filtered[k] = v
return filtered
return _filter_dict(copy.deepcopy(config))
@@ -1,6 +1,12 @@
from config.logger import setup_logging from config.logger import setup_logging
import json import json
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType from plugins_func.register import (
FunctionRegistry,
ActionResponse,
Action,
ToolType,
DeviceTypeRegistry,
)
from plugins_func.functions.hass_init import append_devices_to_prompt from plugins_func.functions.hass_init import append_devices_to_prompt
TAG = __name__ TAG = __name__
@@ -10,6 +16,7 @@ class FunctionHandler:
def __init__(self, conn): def __init__(self, conn):
self.conn = conn self.conn = conn
self.config = conn.config self.config = conn.config
self.device_type_registry = DeviceTypeRegistry()
self.function_registry = FunctionRegistry() self.function_registry = FunctionRegistry()
self.register_nessary_functions() self.register_nessary_functions()
self.register_config_functions() self.register_config_functions()
@@ -54,7 +61,7 @@ class FunctionHandler:
self.function_registry.register_function("plugin_loader") self.function_registry.register_function("plugin_loader")
self.function_registry.register_function("get_time") self.function_registry.register_function("get_time")
self.function_registry.register_function("get_lunar") self.function_registry.register_function("get_lunar")
self.function_registry.register_function("handle_device") self.function_registry.register_function("handle_speaker_volume_or_screen_brightness")
def register_config_functions(self): def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置""" """注册配置中的函数,可以不同客户端使用不同的配置"""

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