Compare commits

...
222 Commits
Author SHA1 Message Date
欣南科技andGitHub 497def8055 Merge pull request #569 from xinnan-tech/add-tag
update:0.1.16版本
2025-03-28 18:48:05 +08:00
hrz 62be26ff47 update:0.1.16版本
- 增加mcp服务
- 支持Dify、Coze时使用独立的意图识别
2025-03-28 18:47:20 +08:00
hrzandGitHub 6a81d80778 Merge pull request #492 from xinnan-tech/mcp-client
增加mcp的tool功能,可自行配置mcp服务,实现命令控制
2025-03-28 18:42:24 +08:00
hrzandGitHub 60c38a8dea Merge pull request #562 from mkanako/fix_handle_device
fix:修复handle_device.py 参数类型定义错误导致DoubaoLLM调用报 400 错误
2025-03-28 17:51:04 +08:00
欣南科技andGitHub 4e0934debf Merge pull request #567 from xinnan-tech/fix_intent_llm
fix:Dify、Coze可使用独立意图识别
2025-03-28 17:49:27 +08:00
hrz 17126303fa fix:已经使用了intent_llm,不应该再回到functioncall的chat中 2025-03-28 17:17:14 +08:00
CGDandGitHub 98fa95190c Merge pull request #563 from xinnan-tech/web_UserManagement_optimize
优化了“用户管理”,分页还未调整
2025-03-28 15:42:03 +08:00
Ran_Chen 3a2261107c 优化了“用户管理”,分页还未调整 2025-03-28 15:24:53 +08:00
kanakoandGitHub b1b64abed9 fix:修复handle_device.py 参数类型定义错误导致DoubaoLLM调用报 400 错误 2025-03-28 14:20:11 +08:00
hrz 05356b6652 update:简化intent_llm代码 2025-03-28 14:13:54 +08:00
CGDandGitHub cbd0c6615d Merge pull request #558 from xinnan-tech/web-HeaderBar-optimize
优化“导航栏”响应调整
2025-03-28 11:35:52 +08:00
Sakura-RanChenandGitHub 90e389efa2 Merge pull request #560 from xinnan-tech/web-api-AgentTemplate-get
完成“获取智能体模板”
2025-03-28 11:34:36 +08:00
CGD 00a0b26797 完成“获取智能体模板” 2025-03-28 11:20:08 +08:00
Ran_Chen 69b5ffe3bf 优化“导航栏”响应调整 2025-03-28 11:08:12 +08:00
玄凤科技 31539294eb 调整mcp等待时间为15秒 2025-03-28 09:52:27 +08:00
玄凤科技 d1a2307134 Merge branch 'mcp-client' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mcp-client
# Conflicts:
#	main/xiaozhi-server/core/connection.py
#	main/xiaozhi-server/requirements.txt
2025-03-28 09:11:48 +08:00
hrzandGitHub 97b777f1b2 Merge pull request #550 from Ike-li/fix_Dockerfile-server
修复 Dockerfile-server 注释
2025-03-27 22:30:26 +08:00
Sakura-RanChenandGitHub a63137abc9 Merge pull request #551 from xinnan-tech/web-API-device
Web api device
2025-03-27 17:59:54 +08:00
CGD 3ca06f8d50 完成”绑定设备“功能 2025-03-27 17:54:05 +08:00
Ike-li e5b0e202be 修复 Dockerfile-server 注释 2025-03-27 15:39:11 +08:00
CGDandGitHub c66385d1f7 Merge pull request #547 from xinnan-tech/web-AddModel-optimize
优化“添加模型”页面布局
2025-03-27 11:57:01 +08:00
Ran_Chen b0304fd875 优化“添加模型”页面布局 2025-03-27 11:54:05 +08:00
CGD 5bfc3efc9c 完成“解绑设备” 2025-03-27 11:24:49 +08:00
hrzandGitHub 62845cfcf1 Merge pull request #515 from koalalgx/main
意图识别intent_llm单独配置独立的LLM,意图识别intent_llm增加天气和新闻查询function
2025-03-27 10:43:49 +08:00
Sakura-RanChenandGitHub 5c536e72df Merge pull request #546 from xinnan-tech/web-ModelConfig-modify
优化了”模型配置“页面
2025-03-27 09:13:41 +08:00
欣南科技andGitHub 7cfece1592 Merge pull request #544 from xinnan-tech/hot-fix
fix:函数调用时有时参数为空的bug
2025-03-27 01:08:50 +08:00
hrzandGitHub f393b2c9ba Merge pull request #542 from flashily/manger-api-flashily
修复Long id写入string的bug
2025-03-27 01:07:02 +08:00
hrzandGitHub 164f2e5472 Merge branch 'main' into manger-api-flashily 2025-03-27 01:06:42 +08:00
hrz 1434ecbc2e fix:函数调用时有时参数为空的bug 2025-03-27 01:03:38 +08:00
flashily c2e443d4ac 修复Long id写入string的bug 2025-03-27 00:26:37 +08:00
hrzandGitHub c079125458 Merge pull request #536 from xinnan-tech/manage-server-aliTTSToken
增加token过期刷新,修复过期bug
2025-03-27 00:21:01 +08:00
欣南科技andGitHub 4c1d7b34b9 Merge pull request #541 from xinnan-tech/hot-fix
merge:manag-api
2025-03-27 00:11:37 +08:00
hrzandGitHub 53772b3fe3 Merge pull request #540 from xinnan-tech/manager-api-agent-jsonL
agent api
2025-03-27 00:06:03 +08:00
hrz 28d3c40835 fixed:modelConfigEntity.setId是String类型 2025-03-27 00:02:26 +08:00
hrzandGitHub a01faecd0c Delete main/manager-api/.vscode/settings.json 2025-03-26 23:59:48 +08:00
hrzandGitHub a1b071e38e Merge pull request #518 from xinnan-tech/manager-api-admin
管理员功能重置密码,删除用户接口完成,分页和查询设备需等设备管理完成后调用实现
2025-03-26 23:12:56 +08:00
hrzandGitHub 571622a0f4 Merge pull request #483 from xinnan-tech/manager-api-model
manager-api 模块新增 /api/v1/models/ 接口
2025-03-26 23:10:36 +08:00
CGD 4ab84d7209 优化了”模型配置“页面 2025-03-26 21:43:07 +08:00
hrzandGitHub 30a938e348 update:更换iot演示demo图 (#538) 2025-03-26 18:31:23 +08:00
Sakura-RanChenandGitHub f6f4384e42 Merge pull request #537 from xinnan-tech/web-vue-modelconfig
优化了”模型配置“页面
2025-03-26 17:15:15 +08:00
CGD 911b11211e 优化了”模型配置“页面 2025-03-26 17:10:06 +08:00
Ken 8bb596c74f 增加token过期刷新,修复 2025-03-26 16:51:26 +08:00
CGDandGitHub d0fdeb33e0 Merge pull request #533 from xinnan-tech/web-model-AddModelDialog
完成添加模型组件
2025-03-26 16:43:19 +08:00
Ran_Chen ae7f41032e 完成添加模型组件 2025-03-26 15:53:47 +08:00
剑雨 f30cb6badb 完善删除,删除用户时,删除其他关联数据(等待其他功能方法调用)
--SysUserServiceImpl.java
2025-03-26 14:49:22 +08:00
欣南科技andGitHub 97a9c1e08a Merge pull request #532 from xinnan-tech/test-server-pr
merge:农历插件优化、异步消费线程不销毁的BUG
2025-03-26 13:14:43 +08:00
hrzandGitHub 1e699c027d Merge pull request #504 from suofang/fix_not_clear_threads
fix:解决客户端websocket连接断开后异步消费线程不销毁的BUG
2025-03-26 11:40:06 +08:00
hrzandGitHub 9bd731729d Merge pull request #528 from journey-ad/refactor-plugins
优化获取日期和音量亮度控制插件
2025-03-26 11:39:40 +08:00
hrzandGitHub dc17eccf76 add:在日志里添加版本信息 (#529) 2025-03-26 10:15:00 +08:00
Jad 3649a33afb feat(plugin_func): 重构音量控制插件,支持亮度调整
支持立创实战派等设备亮度调整
2025-03-26 04:24:47 +08:00
Jad d76bcc1366 feat(plugin_func): 拆分get_time和get_lunar插件 2025-03-26 01:32:43 +08:00
欣南科技andGitHub 2987f1c680 Merge pull request #519 from xinnan-tech/manage-server-edgetts-stream
edge-tts 流式写
2025-03-26 00:40:15 +08:00
hrz ab93ee0406 update:调整加载顺序 2025-03-26 00:39:34 +08:00
hrzandGitHub 69dd933a7d Merge pull request #526 from xinnan-tech/fix-wakeup_words
Fix wakeup words
2025-03-25 23:48:53 +08:00
欣南科技andGitHub 5440de0453 Merge pull request #525 from xinnan-tech/flashily-web
Flashily web
2025-03-25 22:44:42 +08:00
hrzandGitHub f2e130c70e Merge pull request #517 from flashily/flashily-web
与mock连接
2025-03-25 22:41:38 +08:00
Sakura-RanChenandGitHub 4c845c26f7 Merge pull request #521 from xinnan-tech/web-device-bind
Web device bind
2025-03-25 19:52:23 +08:00
CGD a43acca375 优化了user.js文件 2025-03-25 18:59:05 +08:00
Ken 635520aa85 edge-tts 流式写入,避免一次性加载完整语音到内存,无需等待完整音频生成再保存,音频数据块(chunk)生成后立即写入文件,相比等待完整音频生成后再保存,能更早得到部分结果 2025-03-25 18:05:25 +08:00
剑雨 fce9759e93 修复bug
--AdminController.java 重置密码添加返回新密码
--AdminPageUserDTO.java 取消参数验证
--SysUserServiceImpl.java 修改分页查询条件
2025-03-25 18:00:50 +08:00
flashily 18243cd4b0 与mock连接 2025-03-25 16:52:40 +08:00
剑雨 75a6e51e68 管理员控制层,添加接口,删除用户,重置密码,查找所有设备(未完成)
--AdminController.java
2025-03-25 16:50:47 +08:00
剑雨 312edd4b15 用户业务层为添加新方法,重置密码,直接修改密码
--SysUserService.java 定义2个方法
--SysUserServiceImpl.java 实现重置密码,直接修改密码
2025-03-25 16:39:38 +08:00
CGD be3e67b2c1 完成“已绑设备”功能 2025-03-25 16:23:28 +08:00
剑雨 a5765f81fe 控制层添加了新接口
--AdminController.java 添加管理员分页接口
2025-03-25 15:22:56 +08:00
剑雨 4bee92abb0 用户业务层添加了新功能:分页查找
--AdminPageUserDTO.java 管理员分页用户参数DTO
--AdminPageUserVO.java 管理员分页用户展示VO
--SysUserService.java 添加分页方法定义
--SysUserServiceImpl.java 实现了分页方法
2025-03-25 15:04:48 +08:00
CGDandGitHub d9dcb90745 Merge pull request #512 from xinnan-tech/web-change-password
完成修改密码
2025-03-25 14:33:03 +08:00
koalalgxandGitHub fbd25353d5 Update get_weather.py 2025-03-25 14:29:28 +08:00
koalalgxandGitHub 44c3e73577 Update intentHandler.py 2025-03-25 14:28:59 +08:00
koalalgxandGitHub 9cbad1a448 Update get_news.py 2025-03-25 14:28:32 +08:00
koalalgxandGitHub 3a3ddbc2bc Update connection.py 2025-03-25 14:27:48 +08:00
koalalgxandGitHub 63a3c33df9 Update base.py 2025-03-25 14:27:10 +08:00
koalalgxandGitHub 32d7400f7d Update intent_llm.py 2025-03-25 14:26:46 +08:00
koalalgxandGitHub 38191f976b Create xinference.py 2025-03-25 14:25:31 +08:00
CGDandGitHub c380e0e110 Merge branch 'main' into web-change-password 2025-03-25 14:24:58 +08:00
koalalgxandGitHub 2199b8eca7 Update config.yaml 2025-03-25 14:24:05 +08:00
koalalgxandGitHub 88455604a2 Update config.yaml 2025-03-25 14:22:34 +08:00
Sakura-RanChenandGitHub 63eb69091f Merge pull request #505 from xinnan-tech/web-agent-put
Web agent put
2025-03-25 14:19:33 +08:00
欣南科技andGitHub fc668810bd Merge pull request #510 from xinnan-tech/web-flashily
Web flashily
2025-03-25 11:32:57 +08:00
hrzandGitHub 2773e3a48a Merge pull request #506 from flashily/web-flashily
优化界面与细节
2025-03-25 11:28:31 +08:00
flashily 18a03f6ccc 优化界面与细节 2025-03-25 03:15:04 +08:00
CGD e9348f5512 优化了”配置智能体“ 2025-03-25 00:01:13 +08:00
suofang ce65226ea4 fix:解决客户端websocket连接断开后异步消费线程不销毁的BUG 2025-03-24 23:53:56 +08:00
CGD a3b3079b63 完成“配置智能体“功能 2025-03-24 22:41:32 +08:00
hrzandGitHub 22de4bb13b update:修复音乐播放卡顿和断连的bug
修复音乐播放卡顿和断连的bug
2025-03-24 21:55:02 +08:00
zhishengandGitHub eef7bb0cad Delete main/manager-api/src/main/resources/db/changelog/data.sql~ 2025-03-24 20:13:14 +08:00
pengzhisheng 8f1fbbdeef 去除多余文件,优化接口权限,优化 model 实体类主键生成策略 2025-03-24 20:08:33 +08:00
pengzhisheng f3562e091f 去除多余文件,优化接口权限,优化 model 实体类主键生成策略 2025-03-24 20:03:56 +08:00
Ran_Chen 2001258ef4 完成修改密码 2025-03-24 18:08:05 +08:00
CGD 52a66ea304 完成获取智能体配置 2025-03-24 17:27:08 +08:00
欣南科技andGitHub 21ddb1541f update:优化天气、农历插件
update:优化天气、农历插件
2025-03-24 17:16:34 +08:00
hrzandGitHub a3ec6d79b0 feat: 优化查询天气插件
feat: 优化查询天气插件
2025-03-24 16:32:11 +08:00
hrzandGitHub bb8c315ffc Merge pull request #495 from journey-ad/improve-func-time
feat: 支持查询农历日期和黄历信息
2025-03-24 16:30:55 +08:00
hrzandGitHub f2ae398d3f Merge pull request #493 from xinnan-tech/web-agent-configuration
完成删除智能体
2025-03-24 15:31:01 +08:00
Jad 02e33f68b4 feat: 支持查询农历日期和黄历信息 2025-03-24 13:12:46 +08:00
玄凤科技 8c4c9d88da 增加网站浏览mcp 2025-03-24 10:28:48 +08:00
李健豪 49cb98a606 修复音乐播放卡顿和断连的bug 2025-03-24 09:28:01 +08:00
玄凤科技 32ab716f0d Merge branch 'main' into mcp-client
# Conflicts:
#	main/xiaozhi-server/core/connection.py
#	main/xiaozhi-server/requirements.txt
2025-03-24 09:26:14 +08:00
LFNL-scholarandGitHub fe24529fce Merge branch 'xinnan-tech:main' into main 2025-03-24 09:24:30 +08:00
CGD b59c097bca 完成删除智能体 2025-03-24 09:16:48 +08:00
玄凤科技 6d6414eb62 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-03-24 08:44:29 +08:00
Jad 5b1c20d625 feat: 优化查询天气插件
7天预报包含每日天气
2025-03-24 02:09:01 +08:00
hrzandGitHub af6c8ab935 update:优化唤醒词缓存文件
* update:增加私有唤醒答复文件

* update:优化唤醒词缓存文件
2025-03-23 22:59:09 +08:00
hrzandGitHub 4acb5f924a update:增加私有唤醒答复文件 (#488) 2025-03-23 22:40:31 +08:00
b1b99f1066 update:忽略提示词缓存文件
* update:开启唤醒词加速

* update:忽略提示词缓存文件

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-03-23 22:12:11 +08:00
hrzandGitHub 004ee6f143 update:开启唤醒词加速 (#486) 2025-03-23 22:02:13 +08:00
欣南科技andGitHub 19455f89d3 update:优化唤醒词答复
update:优化唤醒词答复
2025-03-23 17:49:04 +08:00
hrz 18d98e3bd0 update:优化对唤醒词的识别 2025-03-23 17:47:39 +08:00
pengzhisheng af038b7fc8 manager-api 模块新增 /api/v1/models/ 接口:
1、新增模型配置对应接口;
2、优化 mapper 目录文件结构;
3、优化启动时自动执行数据库初始化脚本,新增默认账号 admin
2025-03-23 17:32:59 +08:00
hrz 4a9297dc65 update:优化提示音配置,这只成可配置项 2025-03-23 17:20:36 +08:00
HuangandGitHub 49fefb41a6 update:优化唤醒词响应时间,修复初始化函数异步bug (#482) 2025-03-23 16:20:20 +08:00
HuangandGitHub 2fa64dfdd0 Merge pull request #476 from journey-ad/optional-greeting
优化打招呼和进入聆听模式的反馈
2025-03-23 12:09:55 +08:00
HuangandGitHub e0f777b9b4 Merge pull request #472 from xinnan-tech/web-vue-add
Web vue add
2025-03-23 10:52:58 +08:00
HuangandGitHub dfe3de14d8 Merge branch 'main' into web-vue-add 2025-03-23 10:52:36 +08:00
HuangandGitHub abe234c7ce update:补充音乐插件的配置 (#479) 2025-03-23 10:46:15 +08:00
HuangandGitHub d7bf67fd0e Merge pull request #478 from xinnan-tech/XuSenfeng-patch-1
Update play_music.py 原有提示词在模型在未明确指定音乐的时候不能正确识别
2025-03-23 10:45:21 +08:00
Jiao HaoyangandGitHub 8218996f04 Update play_music.py 原有提示词在模型在未指定音乐的时候不能正确识别 2025-03-23 09:35:57 +08:00
Jad ed91e5d607 feat: 开场是否回复唤醒词支持配置 2025-03-23 01:26:31 +08:00
Jad 0bf0b0d381 feat: TTS播放结束提示音 2025-03-23 01:26:29 +08:00
欣南科技andGitHub d88559f5de update:homeassistant插件化改造
update:homeassistant插件化改造
2025-03-22 21:40:17 +08:00
hrz f398115df3 update:更新Home Assistant接入方式 2025-03-22 21:38:40 +08:00
hrz 82413fecab update:优化home_assistant提示词加载 2025-03-22 20:39:52 +08:00
HuangandGitHub d5a5b24223 Merge pull request #418 from c1pher-cn/main
服务端支持function的形式调用homeassistant控制智能设备
2025-03-22 01:40:27 +08:00
HuangandGitHub 472dba2665 Merge branch 'test-pr-homeassistant' into main 2025-03-22 01:40:15 +08:00
CGD a5444b11b3 优化了各个界面 2025-03-22 00:22:12 +08:00
HuangandGitHub 5a05a68524 add:AliAppLLM
add:AliAppLLM
2025-03-21 22:04:24 +08:00
欣南科技andGitHub 36be61bbe1 merge:音色管理功能
音色管理功能
2025-03-21 21:46:11 +08:00
hrz ec6ab983f7 udate:规范包名,小写包名 2025-03-21 21:44:40 +08:00
HuangandGitHub 69630c2be0 update:配置字段统一小写 2025-03-21 21:09:54 +08:00
HuangandGitHub 8e605dd1d4 update:配置字段统一小写 2025-03-21 21:09:32 +08:00
HuangandGitHub 8bb0cf02ad update:配置字段统一小写 2025-03-21 21:08:50 +08:00
HuangandGitHub 60c054f457 update:将manger-web启动设置成8001端口
Update vue.config.js
2025-03-21 21:01:06 +08:00
CGD 5648ee974a 完成三个组件 2025-03-21 18:08:47 +08:00
玄凤科技 dc4968f961 添加命令行服务 2025-03-21 17:31:29 +08:00
剑雨 36d1370720 添加了请求参数验证
--TimbreController.java 验证参数
--TimbreDataDTO.java
--TimbreDataDTO.java 定义参数验证
--validation.properties
--validation_en_US.properties
--validation_zh_CN.properties
--validation_zh_TW.properties 定义参数验证失败后的错误说明
2025-03-21 17:26:16 +08:00
玄凤科技 7ead5537f6 提交requirements.txt中mcp版本依赖 2025-03-21 17:15:54 +08:00
sunweigongandGitHub 7fe797d44d Update vue.config.js
Bug 修复:在启动manger-web连接服务端时,默认为8080端口。此处应按main/README.md文档说明,指定端口为 8001。
2025-03-21 16:42:00 +08:00
剑雨 a1e959420d 为接口添加权限控制
--TimbreController.java 添加权限
2025-03-21 16:07:17 +08:00
剑雨 4c99b40257 修复错误
--TimbreController.java 修复分页获取不到请求参数的错误
--TimbreDetailsVO.java 统一分页参数为string
--TimbrePageDTO.java 序列化
2025-03-21 15:58:39 +08:00
剑雨 ead0958d18 Merge branch 'refs/heads/main' into manager-api-modelAndTimbre-api 2025-03-21 15:04:11 +08:00
李健豪 220af8068e 修复音乐播放断开bug
- 修复音乐播放速度太快导致设备内存溢出断开连接的问题
2025-03-21 14:59:01 +08:00
剑雨 a1730b2d52 添加音色管理的功能
--TimbreController.java 音色控制层,添加了分页,删除,保存,修改接口
--TimbreDetailsVO.java 音色详情展示对象
--TimbrePageDTO.java 音色分页参数
--TimbreDataDTO.java 音色保存数据参数
--TimbreDao.java 音色持久定义
--TimbreService.java 音色业务层定义,添加了分页,删除,保存,修改,获取详情
--TimbreServiceImpl.java
--TimbreEntity.java 音色表对应的实体
2025-03-21 14:57:22 +08:00
jsonL 1a1d6c0835 agent api 2025-03-21 13:00:17 +08:00
Miyokiss 06cbffdea4 Add:AliAppLLM 2025-03-21 03:12:18 +08:00
HuangandGitHub c45e1faf42 update:默认开启意图识别 (#460) 2025-03-20 23:19:31 +08:00
欣南科技andGitHub 732d42f174 Merge pull request #459 from xinnan-tech/test-server-pr
@CaixyPromise
fix(tts.base): 修复 AudioSegment.from_file 读取音频时阻塞的问题 
@CaixyPromise
feature(#377): 实现清理大模型AI的Markdown语法功能,优化TTS自然度。关联Issue #377 
@yanyige
feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403
2025-03-20 22:31:30 +08:00
HuangandGitHub 524fde99f8 Merge pull request #457 from yanyige/feat-support-news
feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403
2025-03-20 22:14:19 +08:00
yanyige 649246dc6a feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403 2025-03-20 21:55:07 +08:00
HuangandGitHub 6c7ac25388 Merge pull request #449 from BoBoloverrrr/bobo-xiaozhi
代码复用:将输入验证逻辑封装成一个独立的函数validateInput ,提高代码的复用性。
2025-03-20 21:46:36 +08:00
HuangandGitHub cb1d6ddeec Merge pull request #455 from CaixyPromise/feature/clean-markdown-for-tts
feature/clean markdown for tts 实现清理大模型AI的Markdown语法功能,
2025-03-20 21:45:19 +08:00
HuangandGitHub 58313889ee Merge pull request #454 from CaixyPromise/fix/tts.base-blocking-bug
fix(tts.base): 修复 AudioSegment.from_file 读取音频时阻塞的问题
2025-03-20 21:44:53 +08:00
caixypromise 5ebc62a084 feature(#377): 实现清理大模型AI的Markdown语法功能,优化TTS自然度。关联Issue #377
close #377
2025-03-20 21:29:53 +08:00
caixypromise fcba384a15 fix(tts.base): 修复 AudioSegment.from_file 读取音频时阻塞的问题
添加 `-nostdin` 参数,防止 FFmpeg 在 Windows 环境下等待标准输入,导致阻塞执行。
2025-03-20 21:10:04 +08:00
玄凤科技 3ea95ffa48 mcp模板文件 2025-03-20 18:20:50 +08:00
玄凤科技 898fd36eca 增加mcp调用超时检测逻辑 2025-03-20 18:20:09 +08:00
CGD 6cd88fb98f 完成模型配置-页面1 2025-03-20 18:11:37 +08:00
欣南科技andGitHub 396de72c5d Merge pull request #451 from xinnan-tech/hot-fix
fix:修复输出日志logger不存在bug
2025-03-20 16:59:33 +08:00
hrz 9932d60dee fix:修复输出日志logger不存在bug 2025-03-20 16:58:12 +08:00
bobo 1ad93ce1fd 右上角用户名显示,新增下拉菜单样式
接收username字段并显示在右上角。
右上角添加了下拉菜单样式,包含个人中心,修改密码以及退出登录。
2025-03-20 16:57:38 +08:00
bobo 2041513676 代码复用:将输入验证逻辑封装成一个独立的函数validateInput ,提高代码的复用性。
validateInput 函数:该函数用于验证输入是否为空,若为空则显示错误提示并返回 false,否则返回 true。
2025-03-20 16:05:06 +08:00
CGD 3ea1a2bf2f 完成用户管理页面 2025-03-20 14:08:38 +08:00
玄凤科技 2b81ebca8e 调通mcp tool 2025-03-20 11:52:37 +08:00
欣南科技andGitHub 27df97d710 Merge pull request #441 from xinnan-tech/fixbug-gettime
修复gettime插件导致chatglm卡住问题
2025-03-20 09:12:33 +08:00
玄凤科技 a25b7b9b3f 修复gettime插件导致chatglm卡住问题 2025-03-20 09:09:47 +08:00
玄凤科技 de9809ca5a 修复时间插件导致chatglm卡住问题 2025-03-20 09:06:36 +08:00
玄凤科技 8504c181c0 mcp初步调试 2025-03-20 08:59:45 +08:00
欣南科技andGitHub 8a005ffb00 Merge pull request #440 from xinnan-tech/test-server-pr
Test server pr
2025-03-20 08:58:24 +08:00
hrz 80ffa7a577 updtge:优化超参 2025-03-20 08:57:39 +08:00
HuangandGitHub ca8f9a7e4a Merge pull request #406 from diaoling6665/main
允许修改openai接口的超参
2025-03-19 23:27:57 +08:00
HuangandGitHub 8d19d78297 Merge pull request #419 from aileenfun/main
gemini独立代理模式
2025-03-19 23:27:14 +08:00
欣南科技andGitHub 483e5eb85a 合并:ASR增加sherpa-onnx模型
feat: ASR增加sherpa-onnx模型 #315 (#379)
2025-03-19 23:22:17 +08:00
HuangandGitHub a4b4f491d2 Merge branch 'main' into test-pr-sherpa-asr 2025-03-19 23:21:23 +08:00
HuangandGitHub 08093f509a update:优化配置文件缩进 2025-03-19 22:39:23 +08:00
HuangandGitHub 9a96223d12 fix:部分支持openai的LLM的url配置名不同 2025-03-19 22:21:11 +08:00
欣南科技andGitHub 0c2b194444 add:manage-web智能体功能
添加智能体功能
2025-03-19 21:48:04 +08:00
HuangandGitHub a06055168f Merge branch 'main' into web-agent-add 2025-03-19 21:40:52 +08:00
欣南科技andGitHub e04546a9ae 合并:机智云湾湾小何
合并:机智云湾湾小何
2025-03-19 20:28:23 +08:00
hrz f6da1a045d update:湾湾小何音色新渠道 2025-03-19 20:27:15 +08:00
CGD acd386bb09 添加智能体功能 2025-03-19 18:13:03 +08:00
HuangandGitHub 45225c0ce1 Merge pull request #427 from insrat/support-gizwitsapi
add:支持Gizwits API渠道接口
2025-03-19 17:44:21 +08:00
HuangandGitHub 0bd884d309 update:manage-web对接用户信息、获取智能体列表,搜索接口
完成前端获取用户信息、获取智能体列表,搜索功能开发
2025-03-19 17:38:38 +08:00
欣南科技andGitHub aea2ae8d79 合并提交,修复dify不支持工作流的bug 2025-03-19 17:25:10 +08:00
hrz 99ec5ab161 update:修正dify saas平台地址 2025-03-19 17:23:25 +08:00
CGD 4431e7470f 完成前端获取用户信息、获取智能体列表,搜索功能开发 2025-03-19 16:16:29 +08:00
jzhuang ed775405c6 add:支持Gizwits API渠道接口 2025-03-19 13:57:42 +08:00
欣南科技andGitHub 4aefbe5aa8 add:获取用户信息、修改密码 (#394)
Manager api user
2025-03-19 01:45:56 +08:00
hrz 786c467464 update:格式化代码 2025-03-19 01:44:25 +08:00
hrz 50182ef940 update:优化获取用户信息的代码 2025-03-19 01:42:36 +08:00
HuangandGitHub bf3aa2df09 dify默认应该是chat模式 2025-03-19 00:20:47 +08:00
HuangandGitHub 0b744a2599 update:dify默认应是chat模式 2025-03-19 00:20:08 +08:00
HuangandGitHub a75b8b40e2 Merge pull request #423 from xinnan-tech/XuSenfeng-patch-2
iot状态更新
2025-03-18 23:57:17 +08:00
HuangandGitHub 2e23c08608 Merge pull request #421 from xinnan-tech/web-vue-modify
Web vue modify
2025-03-18 23:55:54 +08:00
Jiao HaoyangandGitHub 178f45ee26 iot状态更新 2025-03-18 23:55:47 +08:00
CGD 7b16edc99c 优化了前端代码 2025-03-18 23:08:13 +08:00
CGD f3738a1630 修改了前端页面 2025-03-18 21:49:57 +08:00
aileenfun 34fa65132d gemini独立代理模式
在代理模式下手动组成request,无论 Steam 是否为 True,返回的均是非steam值。
不设置代理时自动切换为google的官方api,走 stream模式。

全局代理容易让系统中其他模块也使用代理,造成无法访问的问题。
测试最新的 gemini flash 2.0可以使用。
2025-03-18 21:04:21 +08:00
c1pher-cn 19fe2d2f03 服务端支持function的形式调用homeassistant控制智能设备 2025-03-18 20:42:09 +08:00
Jiao HaoyangandGitHub b7251e2be2 Merge pull request #413 from XuSenfeng/DifyBug
Dify bug添加工作流和文本生成模式的处理
2025-03-18 17:18:51 +08:00
Jiao HaoyangandGitHub 6efede79c7 Merge branch 'xinnan-tech:main' into DifyBug 2025-03-18 17:09:51 +08:00
Xvsenfeng e58da80f8e 添加工作流和文本生成模式 2025-03-18 15:58:31 +08:00
HaoDuoYuandGitHub ea4966f494 Merge branch 'xinnan-tech:main' into main 2025-03-18 12:08:16 +08:00
HaoDuoYuandGitHub f5d9b478d5 Update config.yaml 2025-03-18 12:07:53 +08:00
HaoDuoYuandGitHub 3d97fab16d Add files via upload 2025-03-18 12:05:33 +08:00
aileenfun 2c1fc30bfb gemini可以单独走代理,避免全局代理。
全局代理容易让系统中其他模块也使用代理,造成无法访问的问题。
测试最新的 gemini flash 2.0可以使用。
2025-03-18 00:53:03 +08:00
pengzhisheng c7dec8045e 后端新增 /api/v1/user/change-password 接口 2025-03-17 23:25:24 +08:00
pengzhisheng 5d7d12415c Merge branch 'main' into dev 2025-03-17 19:01:16 +08:00
JadandGitHub 08e57936fc feat: ASR增加sherpa-onnx模型 #315 (#379) 2025-03-17 13:45:49 +08:00
pengzhisheng 7d474e17fa Merge branch 'main' into dev 2025-03-17 09:59:11 +08:00
pengzhisheng d3571942d7 Merge branch 'main' into dev 2025-03-16 17:01:25 +08:00
pengzhisheng 66f5f8ab12 merge main 2025-03-15 10:00:23 +08:00
pengzhisheng 74ce9f7a86 Merge branch 'main' into dev 2025-03-14 10:15:29 +08:00
pengzhisheng 5b2957e7eb 优化登录 token 校验:
1、后端新增 /api/v1/user/info 接口,优化 Oauth2Filter.getRequestToken 逻辑;
2、前端 /api/v1/user/login 登录成功后,保存 token 至浏览器本地;
3、前端请求添加本地 token。
2025-03-14 09:54:54 +08:00
pengzhisheng 40eb4ea4ec merge main 2025-03-14 05:47:08 +08:00
pengzhisheng 0f41039b99 update package 2025-03-12 17:14:48 +08:00
pengzhisheng 8f76db9e41 优化登录注册流程,跑通注册登录、接口 2025-03-12 16:56:51 +08:00
pengzhisheng fc922e996c Merge branch 'main' into dev-manager-api-local 2025-03-12 15:31:20 +08:00
pengzhisheng 6142d325c8 Merge branch 'main' into dev-manager-api-local 2025-03-10 13:46:34 +08:00
pengzhisheng aa21aa686d Merge branch 'main' into dev-manager-api-local 2025-03-09 02:24:58 +08:00
pengzhisheng ec2719a572 Merge branch 'main' into dev 2025-03-08 02:54:17 +08:00
pengzhisheng a466126d63 Merge branch 'main' into dev 2025-03-07 13:25:59 +08:00
pengzhisheng 568f322a25 Merge branch 'main' into dev 2025-03-06 21:20:46 +08:00
pengzhisheng 1b64959e2c fix manager-api bug 2025-03-06 21:16:31 +08:00
135 changed files with 7107 additions and 1066 deletions
+6 -1
View File
@@ -141,13 +141,18 @@ music/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
*.iml *.iml
model.pt
tmp tmp
.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 .env.development
# model files
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
main/xiaozhi-server/models/sherpa-onnx*
my_wakeup_words.mp3
+3 -3
View File
@@ -5,15 +5,15 @@ WORKDIR /app
COPY main/xiaozhi-server/requirements.txt . COPY main/xiaozhi-server/requirements.txt .
# 优化apt安装 # 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# 第阶段:生产镜像 # 第阶段:生产镜像
FROM python:3.10-slim FROM python:3.10-slim
WORKDIR /opt/xiaozhi-esp32-server WORKDIR /opt/xiaozhi-esp32-server
# 优化apt安装 # 安装系统依赖
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \ apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \ apt-get clean && \
+44 -3
View File
@@ -51,7 +51,7 @@
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank"> <a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture> <picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /> <img alt="控制家电开关" src="docs/images/demo5.png" />
</picture> </picture>
@@ -95,6 +95,11 @@
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -185,7 +190,6 @@ server:
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 | | LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) | | LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 | | LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。 实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
@@ -201,6 +205,7 @@ server:
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav | | TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) | | TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav | | TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
| TTS | GizwitsTTS | 接口调用 | 消耗 token | [点击创建密钥](https://agentrouter.gizwitsapi.com) |
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) | | TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 | | TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 | | TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
@@ -223,8 +228,10 @@ server:
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
| ASR | FunASR | 本地使用 | 免费 | | | ASR | FunASR | 本地使用 | 免费 | |
| ASR | SherpaASR | 本地使用 | 免费 | |
| ASR | DoubaoASR | 接口调用 | 收费 | | | ASR | DoubaoASR | 接口调用 | 收费 | |
--- ---
### Memory 记忆存储 ### Memory 记忆存储
@@ -367,7 +374,41 @@ VAD:
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡 ### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。 本项目,支持以工具调用的方式控制HomeAssistant设备
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key``base_url`等信息。例如:
``` yaml
plugins
home_assistant:
devices:
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
base_url: http://你的homeassistant地址:8123
api_key: 你的home assistant api访问令牌
```
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
``` yaml
Intent:
...
function_call:
type: nointent
functions:
- change_role
- get_weather
- get_news
- hass_get_state
- hass_set_state
- hass_play_music
```
### 7、更多问题,可联系我们反馈 💬 ### 7、更多问题,可联系我们反馈 💬
+1
View File
@@ -165,6 +165,7 @@ In fact, any LLM that supports OpenAI API calls can be integrated.
| Type | Platform Name | Usage Method | Pricing Model | Remarks | | Type | Platform Name | Usage Method | Pricing Model | Remarks |
|:----:|:-------------------:|:------------:|:-------------:|:-------:| |:----:|:-------------------:|:------------:|:-------------:|:-------:|
| ASR | FunASR | Local | Free | | | ASR | FunASR | Local | Free | |
| ASR | SherpaASR | Local | Free | |
| ASR | DoubaoASR | API call | Paid | | | ASR | DoubaoASR | API call | Paid | |
--- ---
Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 298 KiB

@@ -1,99 +0,0 @@
package xiaozhi.common.aspect;
import cn.hutool.core.collection.CollUtil;
import xiaozhi.common.annotation.DataFilter;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.interceptor.DataScope;
import xiaozhi.common.user.UserDetail;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* 数据过滤,切面处理类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Aspect
@Component
public class DataFilterAspect {
@Pointcut("@annotation(xiaozhi.common.annotation.DataFilter)")
public void dataFilterCut() {
}
@Before("dataFilterCut()")
public void dataFilter(JoinPoint point) {
Object params = point.getArgs()[0];
if (params != null && params instanceof Map) {
UserDetail user = SecurityUser.getUser();
//如果是超级管理员,则不进行数据过滤
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
return;
}
try {
//否则进行数据过滤
Map map = (Map) params;
String sqlFilter = getSqlFilter(user, point);
map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
} catch (Exception e) {
}
return;
}
throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
}
/**
* 获取数据过滤的SQL
*/
private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
DataFilter dataFilter = method.getAnnotation(DataFilter.class);
//获取表的别名
String tableAlias = dataFilter.tableAlias();
if (StringUtils.isNotBlank(tableAlias)) {
tableAlias += ".";
}
StringBuilder sqlFilter = new StringBuilder();
sqlFilter.append(" (");
//部门ID列表
List<Long> deptIdList = user.getDeptIdList();
if (CollUtil.isNotEmpty(deptIdList)) {
sqlFilter.append(tableAlias).append(dataFilter.deptId());
sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")");
}
//查询本人数据
if (CollUtil.isNotEmpty(deptIdList)) {
sqlFilter.append(" or ");
}
sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
sqlFilter.append(")");
return sqlFilter.toString();
}
}
@@ -73,10 +73,6 @@ public interface Constant {
* 排序方式 * 排序方式
*/ */
String ORDER = "order"; String ORDER = "order";
/**
* token header
*/
String TOKEN_HEADER = "token";
String AUTHORIZATION = "Authorization"; String AUTHORIZATION = "Authorization";
@@ -34,9 +34,6 @@ public class FieldMetaObjectHandler implements MetaObjectHandler {
//创建时间 //创建时间
strictInsertFill(metaObject, CREATE_DATE, Date.class, date); strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
//创建者所属部门
strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId());
//更新者 //更新者
strictInsertFill(metaObject, UPDATER, Long.class, user.getId()); strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
//更新时间 //更新时间
@@ -3,7 +3,6 @@ package xiaozhi.common.user;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* 登录用户信息 * 登录用户信息
@@ -14,19 +13,7 @@ import java.util.List;
public class UserDetail implements Serializable { public class UserDetail implements Serializable {
private Long id; private Long id;
private String username; private String username;
private String realName;
private String headUrl;
private Integer gender;
private String email;
private String mobile;
private Long deptId;
private String password;
private Integer status;
private Integer superAdmin; private Integer superAdmin;
private String token; private String token;
/** private Integer status;
* 部门数据权限
*/
private List<Long> deptIdList;
} }
@@ -1,6 +1,5 @@
package xiaozhi.common.utils; package xiaozhi.common.utils;
import xiaozhi.common.constant.Constant;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -8,6 +7,8 @@ import org.springframework.util.DigestUtils;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import java.util.Date; import java.util.Date;
import java.util.Enumeration; import java.util.Enumeration;
@@ -30,13 +31,14 @@ public class HttpContextUtils {
return ((ServletRequestAttributes) requestAttributes).getRequest(); return ((ServletRequestAttributes) requestAttributes).getRequest();
} }
public static String getToken() { public static String getToken(String authorization) {
HttpServletRequest httpRequest = getHttpServletRequest(); String token;
String token = httpRequest.getHeader(Constant.TOKEN_HEADER); if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
throw new RenException(ErrorCode.UNAUTHORIZED);
//如果header中不存在token,则从参数中获取token }
token = authorization.replace("Bearer ", "");
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(Constant.TOKEN_HEADER); throw new RenException(ErrorCode.TOKEN_NOT_EMPTY);
} }
return token; return token;
} }
@@ -1,16 +1,10 @@
package xiaozhi.common.utils; package xiaozhi.common.utils;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.security.user.SecurityUser;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.StringUtils; import xiaozhi.common.exception.ErrorCode;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* 响应数据 * 响应数据
@@ -42,9 +36,6 @@ public class Result<T> implements Serializable {
return this; return this;
} }
public boolean success() {
return code == 0;
}
public Result<T> error() { public Result<T> error() {
this.code = ErrorCode.INTERNAL_SERVER_ERROR; this.code = ErrorCode.INTERNAL_SERVER_ERROR;
@@ -0,0 +1,84 @@
package xiaozhi.modules.admin.contrloler;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.AdminPageUserVO;
import java.util.Map;
/**
* 管理员控制层
*
* @author zjy
* @since 2025-3-25
*/
@AllArgsConstructor
@RestController
@RequestMapping("/admin")
@Tag(name = "管理员管理")
public class AdminController {
private final SysUserService sysUserService;
@GetMapping("/users")
@Operation(summary = "分页查找用户")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = "mobile", description = "用户手机号码", required = true),
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<AdminPageUserVO>> pageUser(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
AdminPageUserDTO dto = new AdminPageUserDTO();
dto.setMobile((String) params.get("mobile"));
dto.setLimit((String) params.get(Constant.LIMIT));
dto.setPage((String) params.get("pages"));
ValidatorUtils.validateEntity(dto);
PageData<AdminPageUserVO> page = sysUserService.page(dto);
return new Result<PageData<AdminPageUserVO>>().ok(page);
}
@PutMapping("/users/{id}")
@Operation(summary = "重置密码")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> update(
@PathVariable Long id) {
String password = sysUserService.resetPassword(id);
return new Result<String>().ok(password);
}
@DeleteMapping("/users/{id}")
@Operation(summary = "用户删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable Long id) {
sysUserService.delete(new Long[]{id});
return new Result<>();
}
@GetMapping("/device/all")
@Operation(summary = "分页查找设备")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = "keywords", description = "用户手机号码", required = true),
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<Void> pageDevice(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
//TODO 等设备功能模块写好
return new Result<Void>().error(600,"等设备功能模块写好");
}
}
@@ -0,0 +1,149 @@
package xiaozhi.modules.agent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.security.user.SecurityUser;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Tag(name = "智能体管理")
@AllArgsConstructor
@RestController
@RequestMapping("/agent")
public class AgentController {
private final AgentService agentService;
@GetMapping("/list")
@Operation(summary = "获取用户智能体列表")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentEntity>> getUserAgents() {
UserDetail user = SecurityUser.getUser();
List<AgentEntity> agents = agentService.getUserAgents(user.getId());
return new Result<List<AgentEntity>>().ok(agents);
}
@GetMapping("/all")
@Operation(summary = "智能体列表(管理员)")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<AgentEntity>> adminAgentList(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
PageData<AgentEntity> page = agentService.adminAgentList(params);
return new Result<PageData<AgentEntity>>().ok(page);
}
@GetMapping("/{id}")
@Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentEntity> getAgentById(@PathVariable("id") String id) {
AgentEntity agent = agentService.getAgentById(id);
return new Result<AgentEntity>().ok(agent);
}
@PostMapping
@Operation(summary = "创建智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
// 设置用户ID和创建者信息
UserDetail user = SecurityUser.getUser();
entity.setUserId(user.getId());
entity.setCreator(user.getId());
entity.setCreatedAt(new Date());
// ID、智能体编码和排序会在Service层自动生成
agentService.insert(entity);
return new Result<>();
}
@PutMapping
@Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> update(@RequestBody @Valid AgentUpdateDTO dto) {
// 先查询现有实体
AgentEntity existingEntity = agentService.getAgentById(dto.getId());
if (existingEntity == null) {
return new Result<Void>().error("智能体不存在");
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
if (dto.getAsrModelId() != null) {
existingEntity.setAsrModelId(dto.getAsrModelId());
}
if (dto.getVadModelId() != null) {
existingEntity.setVadModelId(dto.getVadModelId());
}
if (dto.getLlmModelId() != null) {
existingEntity.setLlmModelId(dto.getLlmModelId());
}
if (dto.getTtsModelId() != null) {
existingEntity.setTtsModelId(dto.getTtsModelId());
}
if (dto.getTtsVoiceId() != null) {
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
}
if (dto.getMemModelId() != null) {
existingEntity.setMemModelId(dto.getMemModelId());
}
if (dto.getIntentModelId() != null) {
existingEntity.setIntentModelId(dto.getIntentModelId());
}
if (dto.getSystemPrompt() != null) {
existingEntity.setSystemPrompt(dto.getSystemPrompt());
}
if (dto.getLangCode() != null) {
existingEntity.setLangCode(dto.getLangCode());
}
if (dto.getLanguage() != null) {
existingEntity.setLanguage(dto.getLanguage());
}
if (dto.getSort() != null) {
existingEntity.setSort(dto.getSort());
}
// 设置更新者信息
UserDetail user = SecurityUser.getUser();
existingEntity.setUpdater(user.getId());
existingEntity.setUpdatedAt(new Date());
agentService.updateById(existingEntity);
return new Result<>();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> delete(@PathVariable String id) {
agentService.deleteById(id);
return new Result<>();
}
}
@@ -0,0 +1,10 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity;
@Mapper
public interface AgentDao extends BaseDao<AgentEntity> {
}
@@ -0,0 +1,51 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 智能体创建DTO
* 专用于新增智能体,不包含id、agentCode和sort字段,这些字段由系统自动生成/设置默认值
*/
@Data
@Schema(description = "智能体创建对象")
public class AgentCreateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "智能体名称", example = "客服助手")
@NotBlank(message = "智能体名称不能为空")
private String agentName;
@Schema(description = "语音识别模型标识", example = "asr_model_01")
private String asrModelId;
@Schema(description = "语音活动检测标识", example = "vad_model_01")
private String vadModelId;
@Schema(description = "大语言模型标识", example = "llm_model_01")
private String llmModelId;
@Schema(description = "语音合成模型标识", example = "tts_model_01")
private String ttsModelId;
@Schema(description = "音色标识", example = "voice_01")
private String ttsVoiceId;
@Schema(description = "记忆模型标识", example = "mem_model_01")
private String memModelId;
@Schema(description = "意图模型标识", example = "intent_model_01")
private String intentModelId;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题")
private String systemPrompt;
@Schema(description = "语言编码", example = "zh_CN")
private String langCode;
@Schema(description = "交互语种", example = "中文")
private String language;
}
@@ -0,0 +1,61 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 智能体更新DTO
* 专用于更新智能体,id字段是必需的,用于标识要更新的智能体
* 其他字段均为非必填,只更新提供的字段
*/
@Data
@Schema(description = "智能体更新对象")
public class AgentUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "智能体唯一标识", example = "a1b2c3d4e5f6", required = true)
@NotBlank(message = "智能体ID不能为空")
private String id;
@Schema(description = "智能体编码", example = "AGT_1234567890", required = false)
private String agentCode;
@Schema(description = "智能体名称", example = "客服助手", required = false)
private String agentName;
@Schema(description = "语音识别模型标识", example = "asr_model_02", required = false)
private String asrModelId;
@Schema(description = "语音活动检测标识", example = "vad_model_02", required = false)
private String vadModelId;
@Schema(description = "大语言模型标识", example = "llm_model_02", required = false)
private String llmModelId;
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", required = false)
private String ttsVoiceId;
@Schema(description = "记忆模型标识", example = "mem_model_02", required = false)
private String memModelId;
@Schema(description = "意图模型标识", example = "intent_model_02", required = false)
private String intentModelId;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
private String systemPrompt;
@Schema(description = "语言编码", example = "zh_CN", required = false)
private String langCode;
@Schema(description = "交互语种", example = "中文", required = false)
private String language;
@Schema(description = "排序", example = "1", required = false)
private Integer sort;
}
@@ -0,0 +1,69 @@
package xiaozhi.modules.agent.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@TableName("ai_agent")
@Schema(description = "智能体信息")
public class AgentEntity {
@Schema(description = "智能体唯一标识")
private String id;
@Schema(description = "所属用户ID")
private Long userId;
@Schema(description = "智能体编码")
private String agentCode;
@Schema(description = "智能体名称")
private String agentName;
@Schema(description = "语音识别模型标识")
private String asrModelId;
@Schema(description = "语音活动检测标识")
private String vadModelId;
@Schema(description = "大语言模型标识")
private String llmModelId;
@Schema(description = "语音合成模型标识")
private String ttsModelId;
@Schema(description = "音色标识")
private String ttsVoiceId;
@Schema(description = "记忆模型标识")
private String memModelId;
@Schema(description = "意图模型标识")
private String intentModelId;
@Schema(description = "角色设定参数")
private String systemPrompt;
@Schema(description = "语言编码")
private String langCode;
@Schema(description = "交互语种")
private String language;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updatedAt;
}
@@ -0,0 +1,25 @@
package xiaozhi.modules.agent.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.entity.AgentEntity;
import java.util.List;
import java.util.Map;
public interface AgentService extends BaseService<AgentEntity> {
/**
* 根据用户ID获取智能体列表
*/
List<AgentEntity> getUserAgents(Long userId);
/**
* 管理员获取所有智能体列表(分页)
*/
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
/**
* 获取智能体详情
*/
AgentEntity getAgentById(String id);
}
@@ -0,0 +1,64 @@
package xiaozhi.modules.agent.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.stereotype.Service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.service.AgentService;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
private final AgentDao agentDao;
public AgentServiceImpl(AgentDao agentDao) {
this.agentDao = agentDao;
}
@Override
public List<AgentEntity> getUserAgents(Long userId) {
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
return agentDao.selectList(wrapper);
}
@Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
IPage<AgentEntity> page = agentDao.selectPage(
getPage(params, "sort", true),
new QueryWrapper<>()
);
return new PageData<>(page.getRecords(), page.getTotal());
}
@Override
public AgentEntity getAgentById(String id) {
return agentDao.selectById(id);
}
@Override
public boolean insert(AgentEntity entity) {
// 如果ID为空,自动生成一个UUID作为ID
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
entity.setId(UUID.randomUUID().toString().replace("-", ""));
}
// 如果智能体编码为空,自动生成一个带前缀的编码
if (entity.getAgentCode() == null || entity.getAgentCode().trim().isEmpty()) {
entity.setAgentCode("AGT_" + System.currentTimeMillis());
}
// 如果排序字段为空,设置默认值0
if (entity.getSort() == null) {
entity.setSort(0);
}
return super.insert(entity);
}
}
@@ -25,11 +25,10 @@ import xiaozhi.modules.security.user.SecurityUser;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@Tag(name = "设备管理")
@AllArgsConstructor @AllArgsConstructor
@RestController @RestController
@RequestMapping("/device") @RequestMapping("/device")
@Tag(name = "设备管理")
public class DeviceController { public class DeviceController {
private final DeviceService deviceService; private final DeviceService deviceService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
@@ -0,0 +1,2 @@
package xiaozhi.modules.model.controller;public class ModelConfigController {
}
@@ -0,0 +1,106 @@
package xiaozhi.modules.model.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import java.util.List;
@AllArgsConstructor
@RestController
@RequestMapping("/models")
@Tag(name = "模型配置")
public class ModelController {
private final ModelProviderService modelProviderService;
private final ModelConfigService modelConfigService;
@GetMapping("/models/names")
@Operation(summary = "获取所有模型名称")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<String>> getModelNames(@RequestParam String modelType,
@RequestParam(required = false) String modelName) {
List<String> modelNameList = modelConfigService.getModelCodeList(modelType, modelName);
return new Result<List<String>>().ok(modelNameList);
}
@GetMapping("/{modelType}/provideTypes")
@Operation(summary = "获取模型供应器列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<ModelProviderDTO>> getModelProviderList(@PathVariable String modelType) {
List<ModelProviderDTO> modelProviderDTOS = modelProviderService.getListByModelType(modelType);
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
}
@GetMapping("/{modelType}/{provideCode}/fields")
@Operation(summary = "获取模型供应器字段")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<String>> getModelProviderFields(@PathVariable String modelType, @PathVariable String provideCode) {
List<String> fieldList = modelProviderService.getFieldList(modelType, provideCode);
return new Result<List<String>>().ok(fieldList);
}
@GetMapping("/models/list")
@Operation(summary = "获取模型配置列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<PageData<ModelConfigDTO>> getModelConfigList(@RequestParam String modelType,
@RequestParam(required = false) String modelName,
@RequestParam(required = false, defaultValue = "0") Integer page,
@RequestParam(required = false,defaultValue = "10") Integer limit) {
PageData<ModelConfigDTO> pageList = modelConfigService.getPageList(modelType, modelName, page, limit);
return new Result<PageData<ModelConfigDTO>>().ok(pageList);
}
@PostMapping("/models/{modelType}/{provideCode}")
@Operation(summary = "新增模型配置")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelConfigDTO> addModelConfig(@PathVariable String modelType,
@PathVariable String provideCode,
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
}
@PutMapping("/models/{modelType}/{provideCode}/{id}")
@Operation(summary = "编辑模型配置")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelConfigDTO> editModelConfig(@PathVariable String modelType,
@PathVariable String provideCode,
@PathVariable String id,
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
}
@DeleteMapping("/models/{modelType}/{provideCode}/{id}")
@Operation(summary = "删除模型配置")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> deleteModelConfig(@PathVariable String modelType, @PathVariable String provideCode, @PathVariable String id) {
modelConfigService.delete(modelType, provideCode, id);
return new Result<>();
}
@GetMapping("/models/{modelName}/voices")
@Operation(summary = "获取模型音色")
@RequiresPermissions("sys:role:normal")
public Result<List<String>> getVoiceList(@PathVariable String modelName,
@RequestParam(required = false) String voiceName) {
List<String> voiceList = modelConfigService.getVoiceList(modelName, voiceName);
return new Result<List<String>>().ok(voiceList);
}
}
@@ -0,0 +1,17 @@
package xiaozhi.modules.model.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import java.util.List;
@Mapper
public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
/**
* get model_code list
*/
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
}
@@ -0,0 +1,14 @@
package xiaozhi.modules.model.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.model.entity.ModelProviderEntity;
import java.util.List;
@Mapper
public interface ModelProviderDao extends BaseDao<ModelProviderEntity> {
List<String> getFieldList(@Param("modelType") String modelType, @Param("provideCode") String provideCode);
}
@@ -0,0 +1,41 @@
package xiaozhi.modules.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
@Data
@Schema(description = "模型供应器/商")
public class ModelConfigBodyDTO {
@Serial
private static final long serialVersionUID = 1L;
// @Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
// private String modelType;
//
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
private String modelCode;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "是否默认配置(0否 1是)")
private Integer isDefault;
@Schema(description = "是否启用")
private Integer isEnabled;
@Schema(description = "模型配置(JSON格式)")
private String configJson;
@Schema(description = "官方文档链接")
private String docLink;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
}
@@ -0,0 +1,45 @@
package xiaozhi.modules.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
@Schema(description = "模型供应器/商")
public class ModelConfigDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
private Long id;
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
private String modelType;
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
private String modelCode;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "是否默认配置(0否 1是)")
private Integer isDefault;
@Schema(description = "是否启用")
private Integer isEnabled;
@Schema(description = "模型配置(JSON格式)")
private String configJson;
@Schema(description = "官方文档链接")
private String docLink;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
}
@@ -0,0 +1,45 @@
package xiaozhi.modules.model.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@Schema(description = "模型供应器/商")
public class ModelProviderDTO implements Serializable {
//
// @Schema(description = "主键")
// private Long id;
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
private String modelType;
@Schema(description = "供应器类型")
private String providerCode;
@Schema(description = "供应器名称")
private String name;
@Schema(description = "供应器字段列表(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
private String fields;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,62 @@
package xiaozhi.modules.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@TableName("ai_model_config")
@Schema(description = "模型配置表")
public class ModelConfigEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "主键")
private String id;
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
private String modelType;
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
private String modelCode;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "是否默认配置(0否 1是)")
private Integer isDefault;
@Schema(description = "是否启用")
private Integer isEnabled;
@TableField(typeHandler = JacksonTypeHandler.class)
@Schema(description = "模型配置(JSON格式)")
private String configJson;
@Schema(description = "官方文档链接")
private String docLink;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,49 @@
package xiaozhi.modules.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@TableName("ai_model_provider")
@Schema(description = "模型供应器表")
public class ModelProviderEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "主键")
private String id;
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
private String modelType;
@Schema(description = "供应器类型,如 openai、")
private String providerCode;
@Schema(description = "供应器名称")
private String name;
@Schema(description = "供应器字段列表(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
private String fields;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,22 @@
package xiaozhi.modules.model.service;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
import java.util.List;
public interface ModelConfigService {
List<String> getModelCodeList(String modelType, String modelName);
PageData<ModelConfigDTO> getPageList(String modelType, String modelName, Integer page, Integer limit);
ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO);
ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO);
void delete(String modelType, String provideCode, String id);
List<String> getVoiceList(String modelName, String voiceName);
}
@@ -0,0 +1,23 @@
package xiaozhi.modules.model.service;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
import java.util.List;
public interface ModelProviderService {
// List<String> getModelNames(String modelType, String modelName);
List<ModelProviderDTO> getListByModelType(String modelType);
ModelProviderDTO add(ModelProviderEntity modelProviderEntity);
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity);
void delete();
List<ModelProviderDTO> getList(String modelType, String provideCode);
List<String> getFieldList(String modelType, String provideCode);
}
@@ -0,0 +1,124 @@
package xiaozhi.modules.model.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.model.dao.ModelConfigDao;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.timbre.service.TimbreService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@AllArgsConstructor
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity> implements ModelConfigService {
private final ModelConfigDao modelConfigDao;
private final ModelProviderService modelProviderService;
private final TimbreService timbreService;
private static final Logger logger = LoggerFactory.getLogger(ModelConfigServiceImpl.class);
@Override
public List<String> getModelCodeList(String modelType, String modelName) {
return modelConfigDao.getModelCodeList(modelType, modelName);
}
@Override
public PageData<ModelConfigDTO> getPageList(String modelType, String modelName, Integer page, Integer limit) {
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, page);
params.put(Constant.LIMIT, limit);
IPage<ModelConfigEntity> modelConfigEntityIPage = modelConfigDao.selectPage(
getPage(params, "sort", true),
new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType)
.like(StringUtils.isNotBlank(modelName), "model_name", modelName)
);
return getPageData(modelConfigEntityIPage, ModelConfigDTO.class);
}
@Override
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
// 先验证有没有供应器
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
// 再保存供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setModelType(modelType);
modelConfigDao.insert(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
}
@Override
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
// 先验证有没有供应器
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
// 再更新供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setId(id);
modelConfigEntity.setModelType(modelType);
modelConfigDao.updateById(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
}
@Override
public void delete(String modelType, String provideCode, String id) {
// 先验证有没有供应器
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
modelConfigDao.deleteById(Long.getLong(id));
}
@Override
public List<String> getVoiceList(String modelName, String voiceName) {
QueryWrapper<ModelConfigEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("model_name", StringUtils.isBlank(modelName) ? "" : modelName);
queryWrapper.eq("model_type", "TTS");
List<ModelConfigEntity> modelConfigEntities = modelConfigDao.selectList(queryWrapper);
if (CollectionUtil.isEmpty(modelConfigEntities)) {
logger.warn("没有找到模型配置信息");
return null;
}
ModelConfigEntity modelConfigEntity = modelConfigEntities.get(0);
String id = modelConfigEntity.getId();
return timbreService.getVoiceNames(id, voiceName);
}
}
@@ -0,0 +1,59 @@
package xiaozhi.modules.model.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.model.dao.ModelProviderDao;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
import xiaozhi.modules.model.service.ModelProviderService;
import java.util.List;
@Service
@AllArgsConstructor
public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao, ModelProviderEntity> implements ModelProviderService {
private final ModelProviderDao modelProviderDao;
@Override
public List<ModelProviderDTO> getListByModelType(String modelType) {
QueryWrapper<ModelProviderEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) {
return null;
}
@Override
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) {
return null;
}
@Override
public void delete() {
}
@Override
public List<ModelProviderDTO> getList(String modelType, String provideCode) {
QueryWrapper<ModelProviderEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
queryWrapper.eq("provide_code", StringUtils.isBlank(provideCode) ? "" : provideCode);
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override
public List<String> getFieldList(String modelType, String provideCode) {
return modelProviderDao.getFieldList(modelType, provideCode);
}
}
@@ -70,7 +70,9 @@ public class ShiroConfig {
filterMap.put("/v3/api-docs/**", "anon"); filterMap.put("/v3/api-docs/**", "anon");
filterMap.put("/doc.html", "anon"); filterMap.put("/doc.html", "anon");
filterMap.put("/favicon.ico", "anon"); filterMap.put("/favicon.ico", "anon");
filterMap.put("/user/**", "anon"); filterMap.put("/user/captcha", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/**", "oauth2"); filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap); shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -4,20 +4,21 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.TokenDTO; import xiaozhi.common.page.TokenDTO;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils; import xiaozhi.common.validator.AssertUtils;
import xiaozhi.modules.security.dao.SysUserTokenDao;
import xiaozhi.modules.security.dto.LoginDTO; import xiaozhi.modules.security.dto.LoginDTO;
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.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.sys.service.SysUserService;
@@ -26,7 +27,6 @@ import java.io.IOException;
/** /**
* 登录控制层 * 登录控制层
*/ */
@Tag(name = "登录管理")
@AllArgsConstructor @AllArgsConstructor
@RestController @RestController
@RequestMapping("/user") @RequestMapping("/user")
@@ -36,7 +36,6 @@ public class LoginController {
private final SysUserTokenService sysUserTokenService; private final SysUserTokenService sysUserTokenService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@GetMapping("/captcha") @GetMapping("/captcha")
@Operation(summary = "验证码") @Operation(summary = "验证码")
@@ -79,32 +78,31 @@ public class LoginController {
} }
// 按照用户名获取用户 // 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername()); SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null){ if (userDTO != null) {
throw new RenException("此手机号码已经注册过"); throw new RenException("此手机号码已经注册过");
} }
userDTO = new SysUserDTO(); userDTO = new SysUserDTO();
userDTO.setUsername(login.getUsername()); userDTO.setUsername(login.getUsername());
userDTO.setPassword(login.getPassword()); userDTO.setPassword(login.getPassword());
sysUserService.save(userDTO); sysUserService.save(userDTO);
return new Result<Void>(); return new Result<>();
} }
@GetMapping("/info") @GetMapping("/info")
@Operation(summary = "用户信息获取") @Operation(summary = "用户信息获取")
public Result<SysUserDTO> info(@RequestHeader("Authorization")String authorization) { public Result<UserDetail> info() {
logger.info("the authorization:{}", authorization); UserDetail user = SecurityUser.getUser();
Result<UserDetail> result = new Result<>();
result.setData(user);
return result;
}
String token; @PutMapping("/change-password")
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) { @Operation(summary = "修改用户密码")
throw new RenException(ErrorCode.UNAUTHORIZED); public Result<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
} Long userId = SecurityUser.getUserId();
token = authorization.replace("Bearer ", ""); sysUserTokenService.changePassword(userId, passwordDTO);
if (StringUtils.isBlank(token)) { return new Result<>();
throw new RenException(ErrorCode.UNAUTHORIZED);
}
SysUserDTO sysUserDTO = sysUserTokenService.getUserByToken(token);
Result result = new Result<SysUserDTO>();
return result.ok(sysUserDTO);
} }
} }
@@ -8,10 +8,11 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.HttpContextUtils; import xiaozhi.common.utils.HttpContextUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
@@ -25,12 +26,15 @@ import java.io.IOException;
*/ */
public class Oauth2Filter extends AuthenticatingFilter { public class Oauth2Filter extends AuthenticatingFilter {
private static final Logger logger = LoggerFactory.getLogger(Oauth2Filter.class);
@Override @Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token //获取请求token
String token = getRequestToken((HttpServletRequest) request); String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
logger.warn("createToken:token is empty");
return null; return null;
} }
@@ -50,7 +54,15 @@ public class Oauth2Filter extends AuthenticatingFilter {
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401 //获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request); String token = getRequestToken((HttpServletRequest) request);
// TODO 调试接口,临时取消登录限制,需要 token 参数的除外
if (true) {
return true;
}
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
logger.warn("onAccessDenied:token is empty");
HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
@@ -74,13 +86,14 @@ public class Oauth2Filter extends AuthenticatingFilter {
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try { try {
//处理登录失败的异常 //处理登录失败的异常
logger.error("onLoginFailure:登录失败!", e);
Throwable throwable = e.getCause() == null ? e : e.getCause(); Throwable throwable = e.getCause() == null ? e : e.getCause();
Result r = new Result().error(ErrorCode.UNAUTHORIZED, throwable.getMessage()); Result r = new Result().error(ErrorCode.UNAUTHORIZED, throwable.getMessage());
String json = JsonUtils.toJsonString(r); String json = JsonUtils.toJsonString(r);
httpResponse.getWriter().print(json); httpResponse.getWriter().print(json);
} catch (IOException e1) { } catch (IOException e1) {
logger.error("onLoginFailure:登录失败! msg:{}", e1.getMessage(), e1);
} }
return false; return false;
@@ -90,21 +103,10 @@ public class Oauth2Filter extends AuthenticatingFilter {
* 获取请求的token * 获取请求的token
*/ */
private String getRequestToken(HttpServletRequest httpRequest) { private String getRequestToken(HttpServletRequest httpRequest) {
String token; String token = null;
//从header中获取token //从header中获取token
String authorization = httpRequest.getHeader(Constant.AUTHORIZATION); String authorization = httpRequest.getHeader(Constant.AUTHORIZATION);
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) { if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
throw new RenException(ErrorCode.UNAUTHORIZED);
}
token = authorization.replace("Bearer ", "");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
authorization = httpRequest.getParameter(Constant.AUTHORIZATION);
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
throw new RenException(ErrorCode.UNAUTHORIZED);
}
token = authorization.replace("Bearer ", ""); token = authorization.replace("Bearer ", "");
} }
return token; return token;
@@ -14,12 +14,12 @@ import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.MessageUtils; import xiaozhi.common.utils.MessageUtils;
import xiaozhi.modules.security.controller.LoginController;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.security.service.ShiroService; import xiaozhi.modules.security.service.ShiroService;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.sys.enums.SuperAdminEnum;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
/** /**
@@ -48,7 +48,7 @@ public class Oauth2Realm extends AuthorizingRealm {
UserDetail user = (UserDetail) principals.getPrimaryPrincipal(); UserDetail user = (UserDetail) principals.getPrimaryPrincipal();
//用户权限列表 //用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(user); Set<String> permsSet = new HashSet<>();
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) { if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
permsSet.add("sys:role:superAdmin"); permsSet.add("sys:role:superAdmin");
@@ -82,8 +82,6 @@ public class Oauth2Realm extends AuthorizingRealm {
//转换成UserDetail对象 //转换成UserDetail对象
UserDetail userDetail = ConvertUtils.sourceToTarget(userEntity, UserDetail.class); UserDetail userDetail = ConvertUtils.sourceToTarget(userEntity, UserDetail.class);
//获取用户对应的部门数据权限
userDetail.setDeptIdList(null);
userDetail.setToken(accessToken); userDetail.setToken(accessToken);
//账号锁定 //账号锁定
@@ -1,22 +1,14 @@
package xiaozhi.modules.security.service; package xiaozhi.modules.security.service;
import xiaozhi.common.user.UserDetail;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import java.util.List;
import java.util.Set;
/** /**
* shiro相关接口 * shiro相关接口
* Copyright (c) 人人开源 All rights reserved. * Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io * Website: https://www.renren.io
*/ */
public interface ShiroService { public interface ShiroService {
/**
* 获取用户权限列表
*/
Set<String> getUserPermissions(UserDetail user);
SysUserTokenEntity getByToken(String token); SysUserTokenEntity getByToken(String token);
@@ -1,14 +1,12 @@
package xiaozhi.modules.security.service; package xiaozhi.modules.security.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.page.TokenDTO; import xiaozhi.common.page.TokenDTO;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import java.util.Map;
/** /**
* 用户Token * 用户Token
* Copyright (c) 人人开源 All rights reserved. * Copyright (c) 人人开源 All rights reserved.
@@ -32,4 +30,12 @@ public interface SysUserTokenService extends BaseService<SysUserTokenEntity> {
*/ */
void logout(Long userId); void logout(Long userId);
/**
* 修改密码
*
* @param userId
* @param passwordDTO
*/
void changePassword(Long userId, PasswordDTO passwordDTO);
} }
@@ -4,8 +4,11 @@ import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.wf.captcha.SpecCaptcha; import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha; import com.wf.captcha.base.Captcha;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.security.oauth2.Oauth2Realm;
import xiaozhi.modules.security.service.CaptchaService; import xiaozhi.modules.security.service.CaptchaService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@@ -29,6 +32,7 @@ public class CaptchaServiceImpl implements CaptchaService {
* Local Cache 5分钟过期 * Local Cache 5分钟过期
*/ */
Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(5, TimeUnit.MINUTES).build(); Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(5, TimeUnit.MINUTES).build();
private static final Logger logger = LoggerFactory.getLogger(Oauth2Realm.class);
@Override @Override
public void create(HttpServletResponse response, String uuid) throws IOException { public void create(HttpServletResponse response, String uuid) throws IOException {
@@ -19,22 +19,6 @@ public class ShiroServiceImpl implements ShiroService {
private final SysUserDao sysUserDao; private final SysUserDao sysUserDao;
private final SysUserTokenDao sysUserTokenDao; private final SysUserTokenDao sysUserTokenDao;
@Override
public Set<String> getUserPermissions(UserDetail user) {
//系统管理员,拥有最高权限
// TODO: 暂时写死,后续改成从数据库查询
List<String> permissionsList = new ArrayList<>();
//用户权限列表
Set<String> permsSet = new HashSet<>();
for (String permissions : permissionsList) {
if (StringUtils.isBlank(permissions)) {
continue;
}
permsSet.addAll(Arrays.asList(permissions.trim().split(",")));
}
return permsSet;
}
@Override @Override
public SysUserTokenEntity getByToken(String token) { public SysUserTokenEntity getByToken(String token) {
@@ -2,6 +2,7 @@ package xiaozhi.modules.security.service.impl;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.TokenDTO; import xiaozhi.common.page.TokenDTO;
@@ -12,7 +13,7 @@ import xiaozhi.modules.security.dao.SysUserTokenDao;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.security.oauth2.TokenGenerator; import xiaozhi.modules.security.oauth2.TokenGenerator;
import xiaozhi.modules.security.service.SysUserTokenService; import xiaozhi.modules.security.service.SysUserTokenService;
import org.springframework.stereotype.Service; import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.sys.service.SysUserService;
@@ -81,6 +82,9 @@ public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, Sy
@Override @Override
public SysUserDTO getUserByToken(String token) { public SysUserDTO getUserByToken(String token) {
SysUserTokenEntity userToken = baseDao.getByToken(token); SysUserTokenEntity userToken = baseDao.getByToken(token);
if (null == userToken) {
throw new RenException(ErrorCode.TOKEN_INVALID);
}
Date now = new Date(); Date now = new Date();
if (userToken.getExpireDate().before(now)) { if (userToken.getExpireDate().before(now)) {
@@ -97,4 +101,14 @@ public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, Sy
Date expireDate = DateUtil.offsetMinute(new Date(), -1); Date expireDate = DateUtil.offsetMinute(new Date(), -1);
baseDao.logout(userId, expireDate); baseDao.logout(userId, expireDate);
} }
@Override
public void changePassword(Long userId, PasswordDTO passwordDTO) {
// 修改密码
sysUserService.changePassword(userId, passwordDTO);
// 使 token 失效,后需要重新登录
Date expireDate = DateUtil.offsetMinute(new Date(), -1);
baseDao.logout(userId, expireDate);
}
} }
@@ -46,11 +46,4 @@ public class SecurityUser {
public static Long getUserId() { public static Long getUserId() {
return getUser().getId(); return getUser().getId();
} }
/**
* 获取部门ID
*/
public static Long getDeptId() {
return getUser().getDeptId();
}
} }
@@ -0,0 +1,25 @@
package xiaozhi.modules.sys.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 管理员分页用户的参数DTO
* @author zjy
* @since 2025-3-21
*/
@Data
@Schema(description = "音色分页参数")
public class AdminPageUserDTO {
@Schema(description = "手机号码")
private String mobile;
@Schema(description = "页数")
private String page;
@Schema(description = "显示列数")
private String limit;
}
@@ -3,9 +3,9 @@ package xiaozhi.modules.sys.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import xiaozhi.common.entity.BaseEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import xiaozhi.common.entity.BaseEntity;
import java.util.Date; import java.util.Date;
@@ -32,11 +32,6 @@ public class SysUserEntity extends BaseEntity {
* 状态 0:停用 1:正常 * 状态 0:停用 1:正常
*/ */
private Integer status; private Integer status;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT)
private Date createDate;
/** /**
* 更新者 * 更新者
*/ */
@@ -47,10 +42,5 @@ public class SysUserEntity extends BaseEntity {
*/ */
@TableField(fill = FieldFill.INSERT_UPDATE) @TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateDate; private Date updateDate;
/**
* 部门名称
*/
@TableField(exist = false)
private String deptName;
} }
@@ -1,8 +1,12 @@
package xiaozhi.modules.sys.service; package xiaozhi.modules.sys.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.vo.AdminPageUserVO;
/** /**
@@ -18,4 +22,30 @@ public interface SysUserService extends BaseService<SysUserEntity> {
void delete(Long[] ids); void delete(Long[] ids);
/**
* 验证是否允许修改密码更改
* @param userId 用户id
* @param passwordDTO 验证密码的参数
*/
void changePassword(Long userId, PasswordDTO passwordDTO);
/**
* 直接修改密码,不需要验证
* @param userId 用户id
* @param password 密码
*/
void changePasswordDirectly(Long userId, String password);
/**
* 重置密码
* @param userId 用户id
* @return 随机生成符合规范的密码
*/
String resetPassword(Long userId);
/**
* 管理员分页用户信息
* @param dto 分页查找参数
* @return 用户列表分页数据
*/
PageData<AdminPageUserVO> page(AdminPageUserDTO dto);
} }
@@ -1,22 +1,28 @@
package xiaozhi.modules.sys.service.impl; package xiaozhi.modules.sys.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.security.password.PasswordUtils; import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.AdminPageUserVO;
import java.util.Arrays; import java.util.*;
import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -79,6 +85,51 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
public void delete(Long[] ids) { public void delete(Long[] ids) {
//删除用户 //删除用户
baseDao.deleteBatchIds(Arrays.asList(ids)); baseDao.deleteBatchIds(Arrays.asList(ids));
//TODO 除了要删除用户还要删除用户关联的设备,对话,智能体。等此3个功能完善在添加
}
@Override
@Transactional(rollbackFor = Exception.class)
public void changePassword(Long userId, PasswordDTO passwordDTO) {
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
if (null == sysUserEntity) {
throw new RenException(ErrorCode.TOKEN_INVALID);
}
// 判断旧密码是否正确
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
throw new RenException("旧密码输入错误");
}
//新密码强度
if (!isStrongPassword(passwordDTO.getNewPassword())) {
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
}
//密码加密
String password = PasswordUtils.encode(passwordDTO.getNewPassword());
sysUserEntity.setPassword(password);
updateById(sysUserEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void changePasswordDirectly(Long userId, String password) {
SysUserEntity sysUserEntity = new SysUserEntity();
sysUserEntity.setId(userId);
sysUserEntity.setPassword(PasswordUtils.encode(password));
updateById(sysUserEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String resetPassword(Long userId) {
String password = generatePassword();
changePasswordDirectly(userId,password);
return password;
} }
private Long getUserCount() { private Long getUserCount() {
@@ -86,6 +137,27 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
return baseDao.selectCount(queryWrapper); return baseDao.selectCount(queryWrapper);
} }
@Override
public PageData<AdminPageUserVO> page(AdminPageUserDTO dto) {
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, dto.getPage());
params.put(Constant.LIMIT,dto.getLimit());
IPage<SysUserEntity> page = baseDao.selectPage(
getPage(params, "id", true),
//定义查询条件
new QueryWrapper<SysUserEntity>()
//必须按照手机号码查找
.eq(StringUtils.isNotBlank(dto.getMobile()),"username",dto.getMobile()));
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
AdminPageUserVO adminPageUserVO = new AdminPageUserVO();
adminPageUserVO.setUserid(user.getId().toString());
adminPageUserVO.setMobile(user.getUsername());
//TODO 2. 等设备功能写好,获取对应数据
adminPageUserVO.setDeviceCount("0");
return adminPageUserVO;
}).toList();
return new PageData<>(list, page.getTotal());
}
private boolean isStrongPassword(String password) { private boolean isStrongPassword(String password) {
// 弱密码的正则表达式 // 弱密码的正则表达式
@@ -94,4 +166,19 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
Matcher matcher = pattern.matcher(password); Matcher matcher = pattern.matcher(password);
return matcher.matches(); return matcher.matches();
} }
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
private static final Random random = new Random();
/**
* 生成随机密码
* @return 随机生成的密码
*/
private String generatePassword(){
StringBuilder password = new StringBuilder();
for (int i = 0; i < 10; i++) {
int randomIndex = random.nextInt(CHARACTERS.length());
password.append(CHARACTERS.charAt(randomIndex));
}
return password.toString();
}
} }
@@ -0,0 +1,22 @@
package xiaozhi.modules.sys.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 管理员分页展示用户的VO
* @ zjy
* @since 2025-3-25
*/
@Data
public class AdminPageUserVO {
@Schema(description = "设备数量")
private String deviceCount;
@Schema(description = "手机号码")
private String mobile;
@Schema(description = "用户id")
private String userid;
}
@@ -0,0 +1,85 @@
package xiaozhi.modules.timbre.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import java.util.Map;
/**
* 音色控制层
*
* @author zjy
* @since 2025-3-21
*/
@AllArgsConstructor
@RestController
@RequestMapping("/ttsVoice")
@Tag(name = "音色管理")
public class TimbreController {
private final TimbreService timbreService;
@GetMapping
@Operation(summary = "分页查找")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = "ttsModelId", description = "对应 TTS 模型主键", required = true),
@Parameter(name = "name", description = "音色名称"),
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<TimbreDetailsVO>> page(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
TimbrePageDTO dto = new TimbrePageDTO();
dto.setTtsModelId((String) params.get("ttsModelId"));
dto.setName((String) params.get("name"));
dto.setLimit((String) params.get(Constant.LIMIT));
dto.setPage((String) params.get(Constant.PAGE));
ValidatorUtils.validateEntity(dto);
PageData<TimbreDetailsVO> page = timbreService.page(dto);
return new Result<PageData<TimbreDetailsVO>>().ok(page);
}
@PostMapping
@Operation(summary = "音色保存")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody TimbreDataDTO dto) {
ValidatorUtils.validateEntity(dto);
timbreService.save(dto);
return new Result<>();
}
@PutMapping("/{id}")
@Operation(summary = "音色修改")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> update(
@PathVariable Long id,
@RequestBody TimbreDataDTO dto) {
ValidatorUtils.validateEntity(dto);
timbreService.update(id, dto);
return new Result<>();
}
@DeleteMapping("/{id}")
@Operation(summary = "音色删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable Long id) {
timbreService.delete(new Long[]{id});
return new Result<>();
}
}
@@ -0,0 +1,14 @@
package xiaozhi.modules.timbre.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.modules.timbre.entity.TimbreEntity;
/**
* 音色持久层定义
* @author zjy
* @since 2025-3-21
*/
@Mapper
public interface TimbreDao extends BaseMapper<TimbreEntity> {
}
@@ -0,0 +1,45 @@
package xiaozhi.modules.timbre.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 音色表数据DTO
* @author zjy
* @since 2025-3-21
*/
@Data
@Schema(description = "音色表信息")
public class TimbreDataDTO {
@Schema(description = "语言")
@NotBlank(message = "{timbre.languages.require}")
private String languages;
@Schema(description = "音色名称")
@NotBlank(message = "{timbre.name.require}")
private String name;
@Schema(description = "备注")
@NotBlank(message = "{timbre.remark.require}")
private String remark;
@Schema(description = "排序")
@Min(value = 0, message = "{sort.number}")
private long sort;
@Schema(description = "对应 TTS 模型主键")
@NotBlank(message = "{timbre.ttsModelId.require}")
private String ttsModelId;
@Schema(description = "音色编码")
@NotBlank(message = "{timbre.ttsVoice.require}")
private String ttsVoice;
@Schema(description = "音频播放地址")
@NotBlank(message = "{timbre.voiceDemo.require}")
private String voiceDemo;
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.timbre.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 音色分页参数DTO
* @author zjy
* @since 2025-3-21
*/
@Data
@Schema(description = "音色分页参数")
public class TimbrePageDTO {
@Schema(description = "对应 TTS 模型主键")
@NotBlank(message = "{timbre.ttsModelId.require}")
private String ttsModelId;
@Schema(description = "音色名称")
private String name;
@Schema(description = "页数")
private String page;
@Schema(description = "显示列数")
private String limit;
}
@@ -0,0 +1,48 @@
package xiaozhi.modules.timbre.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import xiaozhi.common.entity.BaseEntity;
import java.util.Date;
/**
* 音色表实体类
* @author zjy
* @since 2025-3-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_tts_voice")
@Schema(description = "音色信息")
public class TimbreEntity extends BaseEntity {
@Schema(description = "语言")
private String languages;
@Schema(description = "音色名称")
private String name;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private long sort;
@Schema(description = "对应 TTS 模型主键")
private String ttsModelId;
@Schema(description = "音色编码")
private String ttsVoice;
@Schema(description = "音频播放地址")
private String voiceDemo;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,53 @@
package xiaozhi.modules.timbre.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.entity.TimbreEntity;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import java.util.List;
/**
* 音色的业务层的定义
* @author zjy
* @since 2025-3-21
*/
public interface TimbreService extends BaseService<TimbreEntity> {
/**
* 分页获取音色指定tts的下的音色
* @param dto 分页查找参数
* @return 音色列表分页数据
*/
PageData<TimbreDetailsVO> page(TimbrePageDTO dto);
/**
* 获取音色指定id的详情信息
* @param timbreId 音色表id
* @return 音色信息
*/
TimbreDetailsVO get(Long timbreId);
/**
* 保存音色信息
* @param dto 需要保存数据
*/
void save(TimbreDataDTO dto);
/**
* 保存音色信息
* @param timbreId 需要修改的id
* @param dto 需要修改的数据
*/
void update(Long timbreId,TimbreDataDTO dto);
/**
* 批量删除音色
* @param ids 需要被删除的音色id列表
*/
void delete(Long[] ids);
List<String> getVoiceNames(String ttsModelId, String voiceName);
}
@@ -0,0 +1,105 @@
package xiaozhi.modules.timbre.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.timbre.dao.TimbreDao;
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.entity.TimbreEntity;
import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 音色的业务层的实现
* @author zjy
* @since 2025-3-21
*/
@AllArgsConstructor
@Service
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
private final TimbreDao timbreDao;
@Override
public PageData<TimbreDetailsVO> page(TimbrePageDTO dto) {
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, dto.getPage());
params.put(Constant.LIMIT,dto.getLimit());
IPage<TimbreEntity> page = baseDao.selectPage(
getPage(params, "sort", true),
//定义查询条件
new QueryWrapper<TimbreEntity>()
//必须按照ttsID查找
.eq("tts_model_id",dto.getTtsModelId())
//如果有音色名字,按照音色名模糊查找
.like(StringUtils.isNotBlank(dto.getName()),"name",dto.getName())
);
return getPageData(page, TimbreDetailsVO.class);
}
@Override
public TimbreDetailsVO get(Long timbreId) {
TimbreEntity entity = baseDao.selectById(timbreId);
return ConvertUtils.sourceToTarget(entity, TimbreDetailsVO.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(TimbreDataDTO dto) {
isTtsModelId(dto.getTtsModelId());
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
baseDao.insert(timbreEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(Long timbreId, TimbreDataDTO dto) {
isTtsModelId(dto.getTtsModelId());
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
timbreEntity.setId(timbreId);
baseDao.updateById(timbreEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
}
@Override
public List<String> getVoiceNames(String ttsModelId, String voiceName) {
QueryWrapper<TimbreEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("ttsModelId", StringUtils.isBlank(ttsModelId) ? "" : ttsModelId);
if (StringUtils.isNotBlank(voiceName)) {
queryWrapper.like("name", voiceName);
}
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
if (CollectionUtil.isEmpty(timbreEntities)) {
return null;
}
return timbreEntities.stream().map(TimbreEntity::getName).toList();
}
/**
* 处理是不是tts模型的id
*/
private void isTtsModelId(String ttsModelId){
//等模型配置那边写好调用方法判断
}
}
@@ -0,0 +1,39 @@
package xiaozhi.modules.timbre.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 音色详情展示VO
* @author zjy
* @since 2025-3-21
*/
@Data
public class TimbreDetailsVO implements Serializable {
@Schema(description = "音色id")
private Long id;
@Schema(description = "语言")
private String languages;
@Schema(description = "音色名称")
private String name;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private long sort;
@Schema(description = "对应 TTS 模型主键")
private String ttsModelId;
@Schema(description = "音色编码")
private String ttsVoice;
@Schema(description = "音频播放地址")
private String voiceDemo;
}
@@ -29,7 +29,7 @@ spring:
database: 0 database: 0
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
password: # 密码(默认为空) password: # 密码(默认为空)
timeout: 6000ms # 连接超时时长(毫秒) timeout: 6000ms # 连接超时时长(毫秒)
lettuce: lettuce:
pool: pool:
@@ -39,7 +39,6 @@ spring:
min-idle: 5 # 连接池中的最小空闲连接 min-idle: 5 # 连接池中的最小空闲连接
main: main:
allow-bean-definition-overriding: true allow-bean-definition-overriding: true
knife4j: knife4j:
enable: true enable: true
basic: basic:
@@ -20,3 +20,12 @@ sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1 sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
@@ -20,3 +20,11 @@ sysuser.deptId.require=Departments cannot be empty
sysuser.status.range=State ranges from 0 to 1 sysuser.status.range=State ranges from 0 to 1
sysuser.captcha.require=The captcha cannot be empty sysuser.captcha.require=The captcha cannot be empty
sysuser.uuid.require=The unique identifier cannot be empty sysuser.uuid.require=The unique identifier cannot be empty
timbre.languages.require=The language of the timbre cannot be empty
timbre.name.require=The name of the timbre cannot be empty
timbre.remark.require=The remark of the timbre cannot be empty
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
@@ -21,3 +21,10 @@ sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.remark.require=\u97F3\u8272\u7684\u5099\u6CE8\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
@@ -20,3 +20,10 @@ sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1 sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.model.dao.ModelConfigDao">
<!-- 获取模型供应器字段 -->
<select id="getModelCodeList" resultType="String">
select model_name from ai_model_config where model_type = #{modelType}
<if test="modelName != null">and model_name = #{modelName}</if>
</select>
</mapper>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.model.dao.ModelProviderDao">
<!-- 获取模型供应器字段 -->
<select id="getFieldList" resultType="string">
select fields from ai_model_provider where model_type = #{modelType} and provider_code = #{providerCode};
</select>
</mapper>
+1 -1
View File
@@ -1,6 +1,6 @@
// 引入各个模块的请求 // 引入各个模块的请求
import user from './module/user.js' import user from './module/user.js'
import admin from './module/admin.js'
/** /**
* 接口地址 * 接口地址
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替 * 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
+20
View File
@@ -0,0 +1,20 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
// 用户列表
getUserList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/admin/users`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getList()
})
}).send()
},
}
+156 -81
View File
@@ -5,7 +5,8 @@ import {getServiceUrl} from '../api'
export default { export default {
// 登录 // 登录
login(loginForm, callback) { login(loginForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`) RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/login`)
.method('POST') .method('POST')
.data(loginForm) .data(loginForm)
.success((res) => { .success((res) => {
@@ -18,65 +19,6 @@ export default {
}) })
}).send() }).send()
}, },
// 获取用户信息
getUserInfo(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
// 获取设备信息
getHomeList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send()
},
// 绑定设备
bindDevice(deviceCode, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/bind/${deviceCode}`)
.method('POST')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(deviceCode, callback);
});
}).send();
},
// 获取验证码 // 获取验证码
getCaptcha(uuid, callback) { getCaptcha(uuid, callback) {
@@ -85,9 +27,9 @@ export default {
.method('GET') .method('GET')
.type('blob') .type('blob')
.header({ .header({
'Content-Type': 'image/gif', 'Content-Type': 'image/gif',
'Pragma': 'No-cache', 'Pragma': 'No-cache',
'Cache-Control': 'no-cache' 'Cache-Control': 'no-cache'
}) })
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
@@ -99,7 +41,9 @@ export default {
}, },
// 注册账号 // 注册账号
register(registerForm, callback) { register(registerForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/register`).method('POST') RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/register`)
.method('POST')
.data(registerForm) .data(registerForm)
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
@@ -126,10 +70,92 @@ export default {
}); });
}).send(); }).send();
}, },
// 获取设备配置 // 获取智能体列表
getDeviceConfig(device_id, callback) { getAgentList(callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`) .url(`${getServiceUrl()}/api/v1/user/agent`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getAgentList(callback);
});
}).send();
},
// 用户信息获取
getUserInfo(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/info`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('接口请求失败:', err)
RequestService.reAjaxFun(() => {
this.getUserInfo(callback)
})
}).send()
},
// 添加智能体
addAgent(agentName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent`)
.method('POST')
.data({name: agentName})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.addAgent(agentName, callback);
});
}).send();
},
// 删除智能体
deleteAgent(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.deleteAgent(agentId, callback);
});
}).send();
},
// 修改用户密码
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/change-password`)
.method('PUT')
.data({
old_password: oldPassword,
new_password: newPassword,
})
.success((res) => {
RequestService.clearRequestTime();
successCallback(res);
})
.fail((error) => {
RequestService.reAjaxFun(() => {
this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
});
})
.send();
},
// 获取智能体配置
getDeviceConfig(deviceId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${deviceId}`)
.method('GET') .method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
@@ -138,40 +164,89 @@ export default {
.fail((err) => { .fail((err) => {
console.error('获取配置失败:', err); console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getDeviceConfig(device_id, callback); this.getDeviceConfig(deviceId, callback);
}); });
}).send(); }).send();
}, },
// 获取所有模型名称 // 配置智能体
getModelNames(callback) { updateAgentConfig(agentId, configData, callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/names`) .url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('GET') .method('PUT')
.data(configData)
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .fail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getModelNames(callback); this.updateAgentConfig(agentId, configData, callback);
}); });
}).send(); }).send();
}, },
// 已绑设备
// 获取模型音色 getAgentBindDevices(agentId, callback) {
getModelVoices(modelName, callback) { RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取设备列表失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentBindDevices(agentId, callback);
});
}).send();
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('解绑设备失败:', err);
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send();
},
// 绑定设备
bindDevice(agentId, code, callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/${modelName}/voices`) .url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('POST')
.data({ code })
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(agentId, code, callback);
});
}).send();
},
// 新增方法:获取智能体模板
getAgentTemplate(templateName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/templateId`)
.method('GET') .method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .fail((err) => {
console.error('获取模板失败:', err);
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.getModelVoices(modelName, callback); this.getAgentTemplate(templateName, callback);
}); });
}).send(); }).send();
}, },
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -1,21 +1,22 @@
<template> <template>
<el-dialog :visible.sync="visible" width="480px" center> <el-dialog :visible.sync="visible" width="400px" center >
<div style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;"> <div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;"> <div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" /> <img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div> </div>
添加设备 添加设备
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 30px 20px;"> <div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>验证码 <div style="color: red;display: inline-block;">*</div>
<span style="font-size: 11px"> 验证码</span>
</div> </div>
<div class="input-46" style="margin-top: 10px;"> <div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" /> <el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
</div> </div>
</div> </div>
<div style="display: flex;margin: 0 20px;gap: 10px;"> <div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm"> <div class="dialog-btn" @click="confirm">
确定 确定
</div> </div>
@@ -32,16 +33,46 @@
export default { export default {
name: 'AddDeviceDialog', name: 'AddDeviceDialog',
props: { props: {
visible: { type: Boolean, required: true } visible: { type: Boolean, required: true },
agentId: { type: String, required: true }
}, },
data() { data() {
return { deviceCode: "" } return {
deviceCode: "",
loading: false,
}
}, },
methods: { methods: {
confirm() { confirm() {
this.$emit('update:visible', false) if (!/^\d{6}$/.test(this.deviceCode)) {
this.$emit('added', this.deviceCode) this.$message.error('请输入6位数字验证码');
this.deviceCode = "" return;
}
this.loading = true;
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.bindDevice(
this.agentId,
this.deviceCode, ({data}) => {
this.loading = false;
if (data.code === 0) {
this.$emit('refresh');
this.$message.success('设备绑定成功');
this.closeDialog();
} else {
this.$message.error(data.msg || '绑定失败');
}
}
);
}).catch((err) => {
this.loading = false;
console.error('API模块加载失败:', err);
this.$message.error('绑定服务不可用');
});
},
closeDialog() {
this.$emit('update:visible', false);
this.deviceCode = '';
}, },
cancel() { cancel() {
this.$emit('update:visible', false) this.$emit('update:visible', false)
@@ -53,7 +84,6 @@ export default {
<style scoped> <style scoped>
.input-46 { .input-46 {
border: 1px solid #e4e6ef; border: 1px solid #e4e6ef;
background: #f6f8fb; background: #f6f8fb;
@@ -65,11 +95,26 @@ export default {
flex: 1; flex: 1;
border-radius: 23px; border-radius: 23px;
background: #5778ff; background: #5778ff;
height: 46px; height: 40px;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 12px;
color: #fff; color: #fff;
line-height: 46px; line-height: 40px;
text-align: center; text-align: center;
} }
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header{
padding: 10px;
}
</style> </style>
@@ -0,0 +1,387 @@
<template>
<el-dialog
:visible.sync="visible"
width="975px"
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="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
添加模型
</div>
<!-- 关闭按钮 -->
<button class="custom-close-btn" @click="handleClose">
×
</button>
<!-- 模型信息部分 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
<div style="display: flex; align-items: center; gap: 20px;">
<div style="display: flex; align-items: center;">
<span style="margin-right: 8px;">是否启用</span>
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
</div>
<div style="display: flex; align-items: center;">
<span style="margin-right: 8px;">设为默认</span>
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
</div>
</div>
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<!-- 第一行模型名称和模型编码 -->
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
<el-input
v-model="formData.modelName"
placeholder="请输入模型名称"
class="custom-input-bg"
></el-input>
</el-form-item>
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
<el-input
v-model="formData.modelCode"
placeholder="请输入模型编码"
class="custom-input-bg"
></el-input>
</el-form-item>
</div>
<!-- 第二行供应器和排序号 -->
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
<el-select
v-model="formData.supplier"
placeholder="请选择"
class="custom-select custom-input-bg"
style="width: 100%;"
>
<el-option label="硅基流动" value="硅基流动"></el-option>
<el-option label="智脑科技" value="智脑科技"></el-option>
<el-option label="云智科技" value="云智科技"></el-option>
<el-option label="其他" value="其他"></el-option>
</el-select>
</el-form-item>
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
<el-input
v-model="formData.sort"
placeholder="请输入排序号"
class="custom-input-bg"
></el-input>
</el-form-item>
</div>
<!-- 文档地址 -->
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
<el-input
v-model="formData.docLink"
placeholder="请输入文档地址"
class="custom-input-bg"
></el-input>
</el-form-item>
<!-- 备注 -->
<el-form-item label="备注" prop="remark" class="prop-remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入模型备注"
class="custom-input-bg"
></el-input>
</el-form-item>
</el-form>
<!-- 调用信息部分 -->
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 15px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<!-- 第一行模型名称和接口地址 -->
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
<el-input
v-model="formData.configJson.param1"
placeholder="请输入model_name"
class="custom-input-bg"
></el-input>
</el-form-item>
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
<el-input
v-model="formData.configJson.param2"
placeholder="请输入base_url"
class="custom-input-bg"
></el-input>
</el-form-item>
</div>
<!-- 秘钥信息 -->
<el-form-item label="秘钥信息" prop="apiKey">
<el-input
v-model="formData.configJson.apiKey"
placeholder="请输入api_key"
show-password
class="custom-input-bg"
></el-input>
</el-form-item>
</el-form>
</div>
<!-- 保存按钮 -->
<div style="display: flex;justify-content: center;">
<el-button
type="primary"
@click="confirm"
class="save-btn"
>
保存
</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'AddModelDialog',
props: {
visible: {type: Boolean, required: true},
modelType: {type: String, required: true}
},
data() {
return {
formData: {
modelName: '',
modelCode: '',
supplier: '',
sort: 1,
docLink: '',
remark: '',
isEnabled: true,
isDefault: true,
configJson: {
param1: '',
param2: '',
apiKey: ''
}
}
}
},
methods: {
confirm() {
if (!this.formData.modelName || !this.formData.modelCode || !this.formData.supplier ||
!this.formData.configJson.param1 || !this.formData.configJson.param2 || !this.formData.configJson.apiKey) {
this.$message.error('请填写所有必填字段');
return;
}
this.$emit('confirm', {
...this.formData,
provideType: this.formData.supplier
});
this.$emit('update:visible', false);
this.resetForm();
},
resetForm() {
this.formData = {
modelName: '',
modelCode: '',
supplier: '',
sort: 1,
docLink: '',
remark: '',
isEnabled: true,
isDefault: true,
configJson: {
param1: '',
param2: '',
apiKey: ''
}
};
},
handleClose() {
this.resetForm();
this.$emit('update:visible', false);
}
}
}
</script>
<style>
.custom-dialog {
position: relative;
border-radius: 20px;
overflow: hidden;
background: white;
padding-bottom: 17px;
}
.custom-dialog .el-dialog__header {
padding: 0;
border-bottom: none;
}
.center-dialog {
display: flex;
align-items: center;
justify-content: center;
}
.center-dialog .el-dialog {
margin: 4% 0 auto !important;
display: flex;
flex-direction: column;
}
.custom-close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 30px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
padding: 0;
outline: none;
}
.custom-close-btn:hover {
color: #409EFF;
border-color: #409EFF;
}
.custom-select .el-input__suffix {
background: #e6e8ea;
right: 6px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 9px;
}
.custom-select .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.custom-select .el-icon-arrow-up:before {
content: "";
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid #c0c4cc;
position: relative;
top: -2px;
transform: rotate(180deg);
}
.custom-form .el-form-item {
margin-bottom: 20px; /* 统一设置所有表单项的间距 */
}
.custom-form .el-form-item__label {
color: #3d4566;
font-weight: normal;
text-align: right;
padding-right: 20px;
}
.custom-form .el-form-item.prop-remark .el-form-item__label {
margin-top: -4px;
}
/* 修改placeholder颜色 */
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
color: #9c9f9e;
}
/* 输入框背景色 */
.custom-input-bg .el-input__inner,
.custom-input-bg .el-textarea__inner {
background-color: #f6f8fc;
}
.save-btn {
background: #e6f0fd;
color: #237ff4;
border: 1px solid #b3d1ff;
width: 150px;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
}
.save-btn:hover {
background: linear-gradient(to right, #237ff4, #9c40d5);
color: white;
border: none;
}
/* 修改开关样式 */
.custom-switch .el-switch__core {
border-radius: 20px;
height: 23px;
background-color: #c0ccda;
width: 35px;
padding: 0 20px; /* 调整左右内边距 */
}
.custom-switch .el-switch__core:after {
width: 15px;
height: 15px;
background-color: white;
top: 3px;
left: 4px;
transition: all .3s;
}
.custom-switch.is-checked .el-switch__core {
border-color: #b5bcf0;
background-color: #cfd7fa;
padding: 0 20px; /* 确保启用状态也有相同的间隔 */
}
.custom-switch.is-checked .el-switch__core:after {
left: 100%;
margin-left: -18px;
background-color: #1b47ee;
}
/* 调整flex布局的gap */
[style*="display: flex"] {
gap: 20px; /* 扩大flex项间距 */
}
/* 调整输入框高度 */
.custom-input-bg .el-input__inner {
height: 32px; /* 固定输入框高度 */
}
</style>
@@ -0,0 +1,97 @@
<template>
<el-dialog :visible.sync="visible" width="400px" center>
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div>
添加智慧体
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div> 智慧体名称
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入智慧体名称.." v-model="wisdomBodyName" />
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
</div>
</div>
</el-dialog>
</template>
<script>
import userApi from '@/apis/module/user';
export default {
name: 'AddWisdomBodyDialog',
props: {
visible: { type: Boolean, required: true }
},
data() {
return { wisdomBodyName: "" }
},
methods: {
confirm() {
if (!this.wisdomBodyName.trim()) {
this.$message.error('请输入智慧体名称');
return;
}
userApi.addAgent(this.wisdomBodyName, (res) => {
this.$message.success('添加成功');
this.$emit('confirm', res);
this.$emit('update:visible', false);
this.wisdomBodyName = "";
});
},
cancel() {
this.$emit('update:visible', false)
this.wisdomBodyName = ""
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header{
padding: 10px;
}
</style>
@@ -0,0 +1,141 @@
<template>
<el-dialog :visible.sync="visible" width="400px" center>
<div
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
</div>
修改密码
</div>
<div style="height: 1px;background: #e8f0ff;"/>
<div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>
旧密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password/>
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password/>
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
确认新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password/>
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
</div>
</div>
</el-dialog>
</template>
<script>
import userApi from '@/apis/module/user';
export default {
name: 'ChangePasswordDialog',
props: {
visible: {type: Boolean, required: true}
},
data() {
return {
oldPassword: "",
newPassword: "",
confirmNewPassword: ""
}
},
methods: {
confirm() {
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
this.$message.error('请填写所有字段');
return;
}
if (this.newPassword !== this.confirmNewPassword) {
this.$message.error('两次输入的新密码不一致');
return;
}
if (this.newPassword === this.oldPassword) {
this.$message.error('新密码不能与旧密码相同');
return;
}
// 直接调用修改密码接口
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
if (res.code === 0) {
this.$message.error(res.msg || '密码修改失败');
} else {
this.$message.success('密码修改成功');
this.$emit('confirm', res);
this.$emit('update:visible', false);
this.resetForm();
}
}, (err) => {
this.$message.error(err.msg || '密码修改失败');
});
},
cancel() {
this.$emit('update:visible', false);
this.resetForm();
},
resetForm() {
this.oldPassword = "";
this.newPassword = "";
this.confirmNewPassword = "";
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header {
padding: 10px;
}
</style>
+35 -20
View File
@@ -1,20 +1,23 @@
<template> <template>
<div class="device-item"> <div class="device-item">
<div style="display: flex;justify-content: space-between;"> <div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;"> <div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
{{ device.mac }} {{ device.agentName }}
</div> </div>
<div> <div>
<img src="@/assets/home/delete.png" alt="" <img src="@/assets/home/delete.png" alt=""
style="width: 24px;height: 24px;margin-right: 10px;" /> style="width: 18px;height: 18px;margin-right: 10px;" @click.stop="handleDelete" />
<img src="@/assets/home/info.png" alt="" style="width: 24px;height: 24px;" /> <img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</div> </div>
</div> </div>
<div class="device-name"> <div class="device-name">
设备型号{{ device.model }} 设备型号{{ device.ttsModelName }}
</div>
<div class="device-name">
音色模型{{ device.ttsVoiceName }}
</div> </div>
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="$emit('configure')"> <div class="settings-btn" @click="handleConfigure">
配置角色 配置角色
</div> </div>
<div class="settings-btn"> <div class="settings-btn">
@@ -23,12 +26,12 @@
<div class="settings-btn"> <div class="settings-btn">
历史对话 历史对话
</div> </div>
<el-switch v-model="switchValue" inactive-text="OTA升级:" :width="42" <div class="settings-btn" @click="handleDeviceManage">
style="margin-left: auto;" /> 设备管理
</div>
</div> </div>
<div class="version-info"> <div class="version-info">
<div>最近对话{{ device.lastConversation }}</div> <div>最近对话{{ device.lastConnectedAt }}</div>
<div>APP版本{{ device.appVersion }}</div>
</div> </div>
</div> </div>
</template> </template>
@@ -41,33 +44,44 @@ export default {
}, },
data() { data() {
return { switchValue: false } return { switchValue: false }
},
methods: {
handleDelete() {
this.$emit('delete', this.device.agentId)
},
handleConfigure() {
this.$router.push({ path: '/role-config', query: { agentId: this.device.agentId } });
},
handleDeviceManage() {
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
}
} }
} }
</script> </script>
<style scoped> <style scoped>
.device-item { .device-item {
width: 455px; width: 342px;
border-radius: 20px; border-radius: 20px;
background: #fafcfe; background: #fafcfe;
padding: 30px; padding: 22px;
box-sizing: border-box; box-sizing: border-box;
} }
.device-name { .device-name {
margin: 10px 0 14px; margin: 7px 0 10px;
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 11px;
color: #3d4566; color: #3d4566;
text-align: left; text-align: left;
} }
.settings-btn { .settings-btn {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 10px;
color: #5778ff; color: #5778ff;
background: #e6ebff; background: #e6ebff;
width: 76px; width: 57px;
height: 28px; height: 21px;
line-height: 28px; line-height: 21px;
cursor: pointer; cursor: pointer;
border-radius: 14px; border-radius: 14px;
} }
@@ -75,9 +89,10 @@ export default {
.version-info { .version-info {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 20px; margin-top: 15px;
font-size: 14px; font-size: 10px;
color: #979db1; color: #979db1;
font-weight: 400; font-weight: 400;
} }
</style> </style>
@@ -0,0 +1,149 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="音色编码" prop="voiceCode">
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="音色名称" prop="voiceName">
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="语言类型" prop="languageType">
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号" prop="sortNumber">
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
></el-input>
<div class="audio-controls">
<div class="audio-player">
<audio
:src="audioUrl"
controls
preload="metadata"
class="custom-audio"
></audio>
</div>
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button type="primary" @click="handleClose">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'EditVoiceDialog',
props: {
showDialog: Boolean,
voiceData: {
type: Object,
default: () => ({
voiceCode: 'wawaxiaohe',
voiceName: '湾湾小何',
languageType: '中文',
sortNumber: 123
})
}
},
data() {
return {
dialogVisible: this.showDialog,
voiceForm: { ...this.voiceData },
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
generatedAudio: null,
rules: {
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
}
}
},
watch: {
showDialog(newVal) {
this.dialogVisible = newVal
if (newVal) this.voiceForm = { ...this.voiceData }
}
},
methods: {
handleClose() {
this.dialogVisible = false
this.$emit('update:showDialog', false)
},
handleSave() {
this.$refs.voiceForm.validate(valid => {
if (valid) this.$emit('save', this.voiceForm)
})
},
}
}
</script>
<style scoped>
.compact-dialog {
/deep/ .el-dialog__body {
padding: 20px;
}
.el-form-item {
margin-bottom: 16px;
}
.compact-input {
width: 100%;
}
.compact-number {
width: 100%;
/deep/ .el-input__inner {
padding-right: 10px;
}
}
.compact-textarea {
width: 100%;
margin-bottom: 8px;
}
.audio-controls {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
margin-top: 8px;
.preview-btn {
padding: 7px 15px;
}
}
.dialog-footer {
padding: 16px 20px 0;
text-align: right;
border-top: 1px solid #EBEEF5;
.el-button {
min-width: 80px;
}
}
}
</style>
+249 -80
View File
@@ -1,122 +1,291 @@
<template> <template>
<el-header class="header"> <el-header class="header">
<div style="display: flex;justify-content: space-between;"> <div class="header-container">
<div style="display: flex;align-items: center;gap: 10px;"> <!-- 左侧元素 -->
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 56px;height: 56px;" /> <div class="header-left">
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 78px;height: 16px;" /> <img alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
<div class="equipment-management" @click="goHome"> <img alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" /> </div>
设备管理
<!-- 中间导航菜单 -->
<div class="header-center">
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
<img alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
智能体管理
</div> </div>
<div class="console"> <div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
<i class="el-icon-s-grid" style="font-size: 14px;color: #979db1;" /> <img alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
控制台 用户管理
</div> </div>
<div class="equipment-management2"> <div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
设备管理 <img alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
<img src="@/assets/home/close.png" alt="" style="width: 8px;height: 8px;" /> 模型配置
</div> </div>
</div> </div>
<div style="display: flex;align-items: center;gap: 10px;">
<div class="serach-box"> <!-- 右侧元素 -->
<el-input placeholder="输入名称搜索.." v-model="serach" /> <div class="header-right">
<img src="@/assets/home/search.png" alt="" <div class="search-container">
style="width: 16px;height: 16px;margin-right: 15px;cursor: pointer;" /> <el-input
</div> v-model="serach"
<img src="@/assets/home/avatar.png" alt="" style="width: 28px;height: 28px;" /> placeholder="输入名称搜索.."
<div class="user-info"> class="custom-search-input"
158 3632 4642 @keyup.enter.native="handleSearch"
>
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
</el-input>
</div> </div>
<img alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
<el-dropdown trigger="click" class="user-dropdown">
<span class="el-dropdown-link">
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div> </div>
</div> </div>
<!-- 修改密码弹窗 -->
<ChangePasswordDialog :visible.sync="isChangePasswordDialogVisible"/>
</el-header> </el-header>
</template> </template>
<script> <script>
import userApi from '@/apis/module/user';
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
export default { export default {
name: 'HeaderBar', name: 'HeaderBar',
components: {
ChangePasswordDialog
},
props: ['devices'], // 接收父组件设备列表
data() { data() {
return { serach: '' } return {
serach: '',
userInfo: {
username: '',
mobile: ''
},
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
}
},
mounted() {
this.fetchUserInfo()
}, },
methods: { methods: {
goHome() { goHome() {
// 跳转到首页 // 跳转到首页
this.$router.push('/') this.$router.push('/')
},
goUserManagement() {
this.$router.push('/user-management')
},
goModelConfig() {
this.$router.push('/model-config')
},
// 获取用户信息
fetchUserInfo() {
userApi.getUserInfo(({data}) => {
this.userInfo = data.data
})
},
// 处理搜索
handleSearch() {
const searchValue = this.serach.trim();
let filteredDevices;
if (!searchValue) {
// 当搜索内容为空时,显示原始完整列表
filteredDevices = this.$parent.originalDevices;
} else {
// 过滤逻辑
filteredDevices = this.devices.filter(device => {
return device.agentName.includes(searchValue) ||
device.ttsModelName.includes(searchValue) ||
device.ttsVoiceName.includes(searchValue);
});
}
this.$emit('search-result', filteredDevices);
},
// 显示修改密码弹窗
showChangePasswordDialog() {
this.isChangePasswordDialogVisible = true;
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.equipment-management,
.equipment-management2 {
cursor: pointer;
}
.equipment-management {
width: 110px;
height: 32px;
border-radius: 16px;
background: #5778ff;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-weight: 500;
color: #fff;
font-size: 14px;
}
.equipment-management2 {
width: 116px;
height: 30px;
border-radius: 15px;
background: #fff;
display: flex;
justify-content: center;
font-size: 12px;
color: #979db1;
font-weight: 400;
gap: 10px;
color: #3d4566;
margin-left: 20px;
align-items: center;
}
.header { .header {
background: #f6fcfe66; background: #f6fcfe66;
border: 1px solid #fff; border: 1px solid #fff;
height: 53px !important;
min-width: 900px; /* 设置最小宽度防止过度压缩 */
overflow: hidden;
} }
.serach-box { .header-container {
display: flex; display: flex;
width: 306px; justify-content: space-between;
height: 40px;
border-radius: 20px;
background-color: #e2f5f7;
align-items: center; align-items: center;
height: 100%;
padding: 0 10px;
} }
.user-info { .header-left {
font-weight: 600; display: flex;
font-size: 16px; align-items: center;
letter-spacing: -0.02px; gap: 10px;
text-align: left; min-width: 120px;
color: #3d4566;
} }
.console {
width: 120px; .logo-img {
width: 42px;
height: 42px;
}
.brand-img {
width: 58px;
height: 12px;
}
.header-center {
display: flex;
align-items: center;
gap: 25px;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.header-right {
display: flex;
align-items: center;
gap: 7px;
min-width: 300px;
justify-content: flex-end;
}
.equipment-management {
width: 82px;
height: 24px;
border-radius: 12px;
background: #deeafe;
display: flex;
justify-content: center;
font-size: 10px;
font-weight: 500;
gap: 7px;
color: #3d4566;
margin-left: 1px;
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
flex-shrink: 0; /* 防止导航按钮被压缩 */
}
.equipment-management.active-tab {
background: #5778ff !important;
color: #fff !important;
}
.equipment-management img {
width: 15px;
height: 13px;
}
.search-container {
margin-right: 15px;
min-width: 150px;
flex-grow: 1;
max-width: 220px;
}
.custom-search-input >>> .el-input__inner {
height: 30px; height: 30px;
border-radius: 15px; border-radius: 15px;
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%); background-color: #e2e5f8;
display: flex; border: 1px solid #e4e6ef;
align-items: center; padding-left: 15px;
justify-content: center;
font-size: 12px; font-size: 12px;
color: #979db1; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-weight: 400; width: 100%;
gap: 10px; }
color: #979db1;
margin-left: 20px; .search-icon {
cursor: pointer;
color: #909399;
margin-right: 8px;
font-size: 14px;
line-height: 30px;
}
.avatar-img {
width: 21px;
height: 21px;
flex-shrink: 0;
}
.user-dropdown {
flex-shrink: 0;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.header-center {
gap: 14px;
}
.equipment-management {
width: 70px;
font-size: 9px;
}
}
@media (max-width: 1024px) {
.search-container {
margin-right: 10px;
max-width: 150px;
}
.header-right {
gap: 5px;
}
}
@media (max-width: 900px) {
.header-left {
margin-right: auto;
}
.search-container {
max-width: 150px;
}
}
@media (max-width: 768px) {
.search-container {
max-width: 145px;
}
.custom-search-input >>> .el-input__inner {
padding-left: 10px;
font-size: 11px;
}
}
@media (max-width: 600px) {
.search-container {
max-width: 120px;
min-width: 100px;
}
} }
</style> </style>
@@ -0,0 +1,159 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="800px" center>
<el-form :model="form" ref="form" label-width="70px">
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="模型编码">
<el-input v-model="form.code" placeholder="请输入内容" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型名称">
<el-input v-model="form.name" placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
<!-- 供应商 -->
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="供应器">
<el-select v-model="form.supplier" placeholder="请选择">
<el-option label="openai" value="openai" />
<el-option label="dify" value="dify" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<div style="display: flex; align-items: center;">
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
<el-switch v-model="form.isEnable" />
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 文档 -->
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="文档地址">
<el-input v-model="form.docUrl" placeholder="请输入内容" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号">
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 备注 -->
<el-form-item label="备注" class="form-row">
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
/>
</el-form-item>
<div class="vertical-fields">
<el-form-item label="接口地址">
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
</el-form-item>
<el-form-item label="模型名称">
<el-input v-model="form.modelName" placeholder="请输入model_name" />
</el-form-item>
<el-form-item label="密钥信息">
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password/>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "ModelConfigDialog",
props: {
visible: { type: Boolean, default: false },
configData: { type: Object, default: () => ({}) }
},
data() {
return {
dialogVisible: this.visible,
form: {
code: "",
name: "",
supplier: "",
isDefault: true,
isEnable: true,
docUrl: "",
sort: 123,
remark: "",
apiUrl: "",
modelName: "",
apiKey: ""
}
};
},
watch: {
dialogVisible(val) {
this.$emit('update:visible', val);
},
visible(val) {
this.dialogVisible = val;
if (val) this.form = { ...this.form, ...this.configData };
}
},
methods: {
handleSave() {
this.$emit("submit", this.form);
this.dialogVisible = false;
}
}
};
</script>
<style scoped>
.dialog-footer {
margin-top: -30px;
text-align: center;
}
.el-form-item {
margin-bottom: 18px;
}
.el-input-number {
width: 100%;
}
.form-row {
margin-bottom: 12px;
}
.vertical-fields {
margin-top: 8px;
}
.dialog-footer {
margin-top: -10px;
text-align: center;
}
.el-input-number {
width: 100%;
}
.el-input__inner {
height: 36px;
line-height: 36px;
}
.el-form-item__label {
text-align: left;
padding-right: 10px;
}
</style>
@@ -0,0 +1,194 @@
<template>
<el-dialog :visible.sync="visible" width="80%" @close="handleClose">
<el-row class="main-container">
<el-col :span="4">
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
</el-menu>
</el-col>
<el-col :span="20">
<div class="search-operate">
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
</div>
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose" size="medium">关闭</el-button>
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
</div>
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
</el-col>
</el-row>
</el-dialog>
</template>
<script>
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
export default {
components: { EditVoiceDialog },
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
activeModel: 'EdgeTTS',
searchQuery: '',
editDialogVisible: false,
editVoiceData: {},
ttsModels: [
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
computed: {
filteredTtsModels() {
return this.ttsModels.filter(model =>
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
);
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
handleModelSelect(index) {
this.activeModel = index;
// 可根据选中模型加载对应数据
},
handleSearch() {
// 搜索
},
handleAddVoice() {
// 添加音色
},
handleBatchDelete() {
// 批量删除
},
editVoice(voice) {
this.editVoiceData = { ...voice };
this.editDialogVisible = true;
},
handleSaveEditedVoice(voiceForm) {
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
if (index !== -1) {
this.ttsModels.splice(index, 1, voiceForm);
}
},
deleteVoice(voice) {
// 删除
},
handleImportExport() {
// 导入导出
}
}
};
</script>
<style scoped>
.main-container {
padding: 20px;
}
.model-menu {
border-right: 1px solid #ebeef5;
height: calc(100vh - 300px);
background-color: #fafafa;
}
.search-operate {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.pagination-container {
margin: 20px 0;
display: flex;
justify-content: right;
}
.dialog-footer {
text-align: right;
padding: 20px 20px 0;
border-top: 1px solid #ebeef5;
}
::v-deep .table-header th {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
background-color: #fafafa;
}
::v-deep .el-table--border td, ::v-deep .el-table--border th {
border-right: 1px solid #ebeef5;
}
::v-deep .el-pagination {
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
background-color: #f5f7fa;
}
::v-deep .el-pagination .el-pager li {
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
margin: 0 2px;
}
::v-deep .el-pagination .el-pager li:hover {
background-color: #f5f7fa;
}
::v-deep .el-pagination .el-pager li.active {
background-color: #409EFF;
color: #fff;
border-color: #409EFF;
}
</style>
+24
View File
@@ -39,6 +39,30 @@ const routes = [
return import('../views/register.vue') return import('../views/register.vue')
} }
}, },
// 设备管理页面路由
{
path: '/device-management',
name: 'DeviceManagement',
component: function () {
return import('../views/DeviceManagement.vue')
}
},
// 添加用户管理路由
{
path: '/user-management',
name: 'UserManagement',
component: function () {
return import('../views/UserManagement.vue')
}
},
{
path: '/model-config',
name: 'ModelConfig',
component: function () {
return import('../views/ModelConfig.vue')
}
},
] ]
const router = new VueRouter({ const router = new VueRouter({
@@ -0,0 +1,215 @@
<template>
<div class="welcome">
<HeaderBar />
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<div class="table-container">
<h3 class="device-list-title">设备列表</h3>
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
+ 添加设备
</el-button>
<el-table :data="paginatedDeviceList" style="width: 100%; margin-top: 20px" border stripe>
<el-table-column label="设备型号" prop="model" flex></el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
<el-table-column label="备注" width="180">
<template slot-scope="scope">
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
<span v-else>
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
<span v-else @click="startEditRemark(scope.$index, scope.row)">
{{ scope.row.remark }}
</span>
</span>
</template>
</el-table-column>
<el-table-column label="OTA升级" width="120">
<template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)" style="color: #ff4949">
解绑
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="deviceList.length"
></el-pagination>
</div>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
export default {
components: {HeaderBar, AddDeviceDialog },
data() {
return {
addDeviceDialogVisible: false,
currentAgentId: this.$route.query.agentId || '',
currentPage: 1,
pageSize: 5,
deviceList: [],
loading: false,
userApi: null,
};
},
computed: {
paginatedDeviceList() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.deviceList.slice(start, end);
}
},
mounted() {
const agentId = this.$route.query.agentId;
import('@/apis/module/user').then(({ default: userApi }) => {
this.userApi = userApi;
if (agentId) {
this.fetchBindDevices(agentId);
}
});
},
methods: {
handleAddDevice() {
this.addDeviceDialogVisible = true;
},
startEditRemark(index, row) {
this.deviceList[index].isEdit = true;
},
stopEditRemark(index) {
this.deviceList[index].isEdit = false;
},
handleUnbind(device_id) {
if (!this.userApi) {
this.$message.error('功能模块加载失败');
return;
}
this.$confirm('确认要解绑该设备吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.userApi.unbindDevice(device_id, ({ data }) => {
if (data.code === 0) {
this.$message.success('设备解绑成功');
this.fetchBindDevices(this.$route.query.agentId);
} else {
this.$message.error(data.msg || '设备解绑失败');
}
});
});
},
handleSizeChange(val) {
this.pageSize = val;
},
handleCurrentChange(val) {
this.currentPage = val;
},
fetchBindDevices(agentId) {
this.loading = true;
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentBindDevices(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
this.deviceList = data.data[0].list.map(device => ({
device_id: device.id,
model: device.device_type,
firmwareVersion: device.app_version,
macAddress: device.mac_address,
lastConversation: device.recent_chat_time,
remark: '',
isEdit: false,
otaSwitch: device.ota_upgrade === 1
}));
} else {
this.$message.error(data.msg || '获取设备列表失败');
}
});
}).catch(error => {
console.error('模块加载失败:', error);
this.$message.error('功能模块加载失败');
});
},
}
};
</script>
<style scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.table-container {
background: #f9fafc;
padding: 20px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.add-device-btn {
float: right;
background: #409eff;
border: none;
border-radius: 10px;
width: 105px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
gap: 8px;
margin-bottom: 15px;
&:hover {
background: #3a8ee6;
}
}
.device-list-title {
float: left;
font-size: 18px;
font-weight: 700;
margin: 5px;
color: #2c3e50;
}
.el-icon-edit {
color: #409eff;
cursor: pointer;
font-size: 14px;
vertical-align: middle;
}
.pagination {
margin-top: 20px;
text-align: right;
}
</style>
+401
View File
@@ -0,0 +1,401 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<div class="right-operations">
<el-button v-if="activeTab === 'tts'" type="primary" plain size="small" @click="ttsDialogVisible = true">
语音设置
</el-button>
<el-button type="primary" plain size="small" @click="handleImport">
导入配置
</el-button>
<el-button type="success" plain size="small" @click="handleExport">
导出配置
</el-button>
</div>
</div>
<!-- 主体内容 -->
<div class="main-wrapper">
<div class="content-panel">
<!-- 左侧导航 -->
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
style="background-size: cover; background-position: center;">
<el-menu-item index="vad">
<span class="menu-text">语言活动检测</span>
</el-menu-item>
<el-menu-item index="asr">
<span class="menu-text">语音识别</span>
</el-menu-item>
<el-menu-item index="llm">
<span class="menu-text">大语言模型</span>
</el-menu-item>
<el-menu-item index="intent">
<span class="menu-text">意图识别</span>
</el-menu-item>
<el-menu-item index="tts">
<span class="menu-text">语音合成</span>
</el-menu-item>
<el-menu-item index="memory">
<span class="menu-text">记忆</span>
</el-menu-item>
</el-menu>
<!-- 右侧内容 -->
<div class="content-area">
<div class="title-bar">
<div class="title-wrapper">
<h2 class="model-title">大语言模型LLM</h2>
<el-button type="primary" size="small" @click="addModel" class="add-btn">
添加
</el-button>
</div>
<div class="action-group">
<div class="search-group">
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable/>
<el-button type="primary" size="small" class="search-btn" @click="handleSearch">
查询
</el-button>
</div>
</div>
</div>
<el-table :header-cell-style="{background: 'transparent'}" :data="modelList" border class="data-table" header-row-class-name="table-header" >
<el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column label="模型名称" prop="candidateName" align="center"></el-table-column>
<el-table-column label="模型编码" prop="code" align="center"></el-table-column>
<el-table-column label="提供商" prop="supplier" align="center"></el-table-column>
<el-table-column label="是否启用" align="center" width="120">
<template slot-scope="scope">
<el-switch v-model="scope.row.isApplied" class="custom-switch" :active-color="null" :inactive-color="null"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
修改
</el-button>
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<div class="batch-actions">
<el-button size="mini" @click="selectAll">全选</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
删除
</el-button>
</div>
<div class="pagination-container">
<el-pagination @current-change="handleCurrentChange" background :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next" :total="total"/>
</div>
</div>
</div>
</div>
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
<TtsModel :visible.sync="ttsDialogVisible" />
<AddModelDialog :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
</div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue";
import AddModelDialog from "@/components/AddModelDialog.vue";
export default {
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
data() {
return {
addDialogVisible: false,
activeTab: 'llm',
search: '',
editDialogVisible: false,
editModelData: {},
ttsDialogVisible: false,
modelList: [
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
methods: {
handleMenuSelect(index) {
this.activeTab = index;
},
handleSearch() {
console.log('查询:', this.search);
},
batchDelete() {
console.log('批量删除');
},
addModel() {
this.addDialogVisible = true;
},
editModel(model) {
this.editModelData = {
code: model.code,
name: model.candidateName,
supplier: model.supplier,
};
this.editDialogVisible = true;
},
deleteModel(model) {
console.log('删除:', model);
},
handleCurrentChange(page) {
this.currentPage = page;
console.log('当前页码:', page);
},
handleImport() {
console.log('导入配置');
},
handleExport() {
console.log('导出配置');
},
handleModelSave(formData) {
console.log('保存的模型数据:', formData);
},
selectAll() {
console.log('全选');
},
handleAddConfirm(newModel) {
console.log('新增模型数据:', newModel);
}
},
};
</script>
<style scoped>
::v-deep .el-table tr{
background: transparent;
}
.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;
}
.main-wrapper {
margin: 5px 60px;
background-image: url("@/assets/home/background.png");
border-radius: 15px;
min-height: 600px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
}
.operation-bar {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #ebeef5;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
}
.nav-panel {
width: 18%;
min-width: 200px;
height: 100%;
border-right: 1px solid #ebeef5;
background-size: cover;
background: url("../assets/model/model.png") no-repeat center;
padding: 16px 0;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.nav-panel .el-menu-item {
height: 48px;
line-height: 40px;
border-radius: 4px;
transition: all 0.3s;
display: flex !important;
justify-content: flex-end;
padding-right: 12px !important;
width: fit-content;
margin: 8px 12px 8px auto;
min-width: unset;
}
.nav-panel .el-menu-item.is-active {
background: #ecf5ff;
color: #409EFF;
border-right: 3px solid #409EFF;
}
.menu-text {
font-size: 14px;
color: #606266;
text-align: right;
width: 100%;
padding-right: 8px;
}
.content-area {
flex: 1;
padding: 24px;
height: 100%;
min-width: 600px;
overflow-x: auto;
}
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: nowrap;
}
.model-title {
font-size: 18px;
color: #303133;
margin: 0;
}
.action-group {
display: flex;
align-items: center;
gap: 16px;
}
.search-group {
display: flex;
gap: 8px;
}
.search-input {
width: 240px;
}
.search-btn {
background: linear-gradient(135deg, #6B8CFF, #A966FF);
border: none;
color: white;
}
.data-table {
border-radius: 6px;
overflow: hidden;
background-color: transparent !important;
}
.data-table /deep/ .el-table__row {
background-color: transparent !important;
}
.table-header th {
background-color: transparent !important;
color: #606266;
font-weight: 600;
}
.table-footer {
margin-top: 24px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
width: 100%;
}
.batch-actions {
display: flex;
gap: 8px;
}
.copyright {
text-align: center;
color: #979db1;
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
}
.edit-btn {
color: #409EFF !important;
}
.delete-btn {
color: #F56C6C !important;
}
.add-btn {
background: #87CEFA;
border: none;
color: white;
padding: 8px 16px;
}
.title-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.batch-actions .el-button:first-child {
background: linear-gradient(135deg, #409EFF, #6B8CFF);
border: none;
color: white;
}
.batch-actions .el-button:first-child:hover {
background: linear-gradient(135deg, #3A8EE6, #5A7CFF);
}
.el-table th /deep/ .el-table__cell {
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
background-color: transparent !important;
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,271 @@
<template>
<div class="welcome">
<HeaderBar />
<el-main class="main" style="padding: 20px; display: flex; flex-direction: column;">
<div class="top-area">
<div class="page-title">用户管理</div>
<div class="page-search">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" />
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
<!-- <el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button> -->
</div>
</div>
<el-card class="user-card" shadow="never">
<!-- <div class="user-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
<el-button @click="handleSearch">查询</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button>
</div> -->
<el-table :data="userList" class="transparent-table" :header-cell-class-name="headerCellClassName">
<el-table-column label="选择" type="selection" width="55"></el-table-column>
<el-table-column label="用户Id" prop="user_id"></el-table-column>
<el-table-column label="手机号码" prop="mobile"></el-table-column>
<el-table-column label="设备数量" prop="device_count"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)" style="color: #989fdd">重置密码</el-button>
<el-button size="mini" type="text"
v-if="scope.row.status === '正常'"
@click="disableUser(scope.row)">禁用</el-button>
<el-button size="mini" type="text"
v-if="scope.row.status === '禁用'"
@click="restoreUser(scope.row)">恢复</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #989fdd">删除用户</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" style="width: 72px; background: #5f70f3">全选</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check" style="background: #5bc98c">启用</el-button>
<el-button size="mini" type="warning" style="color: black; background: #f6d075"><i class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" style="background: #fd5b63">删除</el-button>
</div>
<div class="pagination-container">
<el-pagination
background
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15]"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
/>
</div>
</div>
</el-card>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import Api from '@/apis/api';
import adminApi from '@/apis/module/admin';
export default {
components: { HeaderBar },
data() {
return {
searchPhone: '',
userList: [
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 10 },
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 9 },
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 7 },
{ userId: '123456', phone: '13800138000', status: '禁用', deviceCount: 7 }
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
created() {
adminApi.getUserList(({data}) => {
//mock偶尔会返回-1导致出错,又会返回两个list,所以这里只取第一个
this.userList = data.data[0].list;
console.log('用户列表:', this.userList);
})
},
methods: {
handleSearch() {
// 模拟搜索逻辑
console.log('执行查询,搜索号码:', this.searchPhone);
},
batchDelete() {
console.log('执行批量删除操作');
},
batchDisable() {
console.log('执行批量禁用操作');
},
resetPassword(row) {
console.log('重置用户密码,用户:', row);
},
disableUser(row) {
row.status = '禁用';
console.log('禁用用户:', row);
},
restoreUser(row) {
row.status = '正常';
console.log('恢复用户:', row);
},
deleteUser(row) {
console.log('删除用户:', row);
},
handleCurrentChange(page) {
this.currentPage = page;
console.log('当前页码:', page);
},
headerCellClassName({ column, columnIndex }) {
if (columnIndex === 0) {
return 'custom-selection-header'
}
return ''
},
}
};
</script>
<style lang="scss" scoped>
$table-bg-color: #ecf1fd;
.main {
padding: 20px; display: flex; flex-direction: column;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
}
.top-area {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
}
.page-search {
display: flex;
align-items: center;
.btn-search {
margin-left: 10px;
background: linear-gradient(to right, #5778ff, #c793f3);
width: 100px;
color: #fff;
}
}
}
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.user-search-operate {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.user-search-operate > * {
margin-right: 10px;
}
.el-table__header th {
background-color: #f5f7fa;
color: #606266;
}
.user-card {
background: $table-bg-color;
border-radius: 12px;
padding: 20px;
//box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
.ctrl_btn {
display: flex;
align-items: center;
}
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
.rotated-icon {
display: inline-block;
transform: rotate(45deg);
margin-right: 4px;
color: black;
}
:deep(.el-table) {
background: $table-bg-color;
&.transparent-table {
.el-table__header th {
background: $table-bg-color !important;
color: black;
}
.el-table__body tr {
background-color: $table-bg-color;
}
}
}
.search-input {
width: 300px;
margin-right: 10px;
:deep(.el-input__inner) {
background-color: transparent;
&:focus {
border-color: #409eff; // 保持聚焦状态下的边框颜色
}
//文字颜色
&::placeholder {
color: #606266;
opacity: 0.7;
}
}
}
:deep(.custom-selection-header) {
.el-checkbox {
display: none !important;
}
&::after {
content: '选择';
display: inline-block;
color: black;
font-weight: bold;
padding-bottom: 18px;
}
}
</style>
+83 -38
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<!-- 公共头部 --> <!-- 公共头部 -->
<HeaderBar /> <HeaderBar :devices="devices" @search-result="handleSearchResult" />
<el-main style="padding: 20px;display: flex;flex-direction: column;"> <el-main style="padding: 20px;display: flex;flex-direction: column;">
<div> <div>
<!-- 首页内容 --> <!-- 首页内容 -->
@@ -21,23 +21,27 @@
</div> </div>
<div class="add-device-btn" @click="showAddDialog"> <div class="add-device-btn" @click="showAddDialog">
<div class="left-add"> <div class="left-add">
添加设备 添加智能体
</div> </div>
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" /> <div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
<div class="right-add"> <div class="right-add">
<i class="el-icon-right" style="font-size: 30px;color: #fff;" /> <i class="el-icon-right" style="font-size: 20px;color: #fff;" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: space-between;box-sizing: border-box;"> <div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item" @configure="goToRoleConfig" /> <DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
@configure="goToRoleConfig"
@deviceManage="handleDeviceManage"
@delete="handleDeleteAgent"
/>
</div> </div>
</div> </div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;"> <div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
©2025 xiaozhi-esp32-server ©2025 xiaozhi-esp32-server
</div> </div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" /> <AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
</el-main> </el-main>
</div> </div>
@@ -45,24 +49,24 @@
<script> <script>
import DeviceItem from '@/components/DeviceItem.vue' import DeviceItem from '@/components/DeviceItem.vue'
import AddDeviceDialog from '@/components/AddDeviceDialog.vue' import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue' import HeaderBar from '@/components/HeaderBar.vue'
export default { export default {
name: 'HomePage', name: 'HomePage',
components: { DeviceItem, AddDeviceDialog, HeaderBar }, components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
data() { data() {
return { return {
addDeviceDialogVisible: false, addDeviceDialogVisible: false,
// 此处模拟设备列表(10条数据) devices: [],
devices: Array.from({ length: 10 }, (_, i) => ({ originalDevices: [],
id: i,
mac: 'CC:ba:97:11:a6:ac',
model: 'esp32-s3-touch-amoled-1.8',
lastConversation: '6天前',
appVersion: '1.1.0'
}))
} }
}, },
mounted() {
this.fetchAgentList();
},
methods: { methods: {
showAddDialog() { showAddDialog() {
this.addDeviceDialogVisible = true this.addDeviceDialogVisible = true
@@ -71,9 +75,48 @@ export default {
// 点击配置角色后跳转到角色配置页 // 点击配置角色后跳转到角色配置页
this.$router.push('/role-config') this.$router.push('/role-config')
}, },
handleDeviceAdded(deviceCode) { handleWisdomBodyAdded(res) {
// 根据需要处理添加设备后逻辑,比如刷新设备列表等 console.log('新增智能体响应', res);
console.log('设备验证码', deviceCode) this.fetchAgentList();
this.addDeviceDialogVisible = false;
},
handleDeviceManage() {
this.$router.push('/device-management');
},
// 获取智能体列表
fetchAgentList() {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentList(({data}) => {
this.originalDevices = data.data.map(item => ({
...item,
agentId: item.id // 字段映射
}));
this.devices = this.originalDevices;
});
});
},
// 搜索更新智能体列表
handleSearchResult(filteredList) {
this.devices = filteredList; // 更新设备列表
},
// 删除智能体
handleDeleteAgent(agentId) {
this.$confirm('确定要删除该智能体吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.deleteAgent(agentId, (res) => {
if (res.data.code === 0) {
this.$message.success('删除成功');
this.fetchAgentList(); // 刷新列表
} else {
this.$message.error(res.data.msg || '删除失败');
}
});
});
}).catch(() => {});
} }
} }
} }
@@ -81,9 +124,11 @@ export default {
<style scoped> <style scoped>
.welcome { .welcome {
min-width: 1200px; min-width: 900px;
min-height: 675px; min-height: 506px;
height: 100vh; height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png"); background-image: url("@/assets/home/background.png");
background-size: cover; background-size: cover;
/* 确保背景图像覆盖整个元素 */ /* 确保背景图像覆盖整个元素 */
@@ -95,8 +140,8 @@ export default {
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
} }
.add-device { .add-device {
height: 260px; height: 195px;
border-radius: 20px; border-radius: 15px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: linear-gradient( background: linear-gradient(
@@ -122,48 +167,48 @@ export default {
box-sizing: border-box; box-sizing: border-box;
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
.hellow-text { .hellow-text {
margin-left: 100px; margin-left: 75px;
color: #3d4566; color: #3d4566;
font-size: 44px; font-size: 33px;
font-weight: 700; font-weight: 700;
letter-spacing: 0; letter-spacing: 0;
} }
.hi-hint { .hi-hint {
font-weight: 400; font-weight: 400;
font-size: 12px; font-size: 10px;
text-align: left; text-align: left;
color: #818cae; color: #818cae;
margin-left: 100px; margin-left: 75px;
margin-top: 7px; margin-top: 5px;
} }
} }
.add-device-btn { .add-device-btn {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 100px; margin-left: 75px;
margin-top: 20px; margin-top: 15px;
cursor: pointer; cursor: pointer;
.left-add { .left-add {
width: 140px; width: 105px;
height: 46px; height: 34px;
border-radius: 23px; border-radius: 17px;
background: #5778ff; background: #5778ff;
color: #fff; color: #fff;
font-size: 14px; font-size: 10px;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
line-height: 46px; line-height: 34px;
} }
.right-add { .right-add {
width: 46px; width: 34px;
height: 46px; height: 34px;
border-radius: 50%; border-radius: 50%;
background: #5778ff; background: #5778ff;
margin-left: -8px; margin-left: -6px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
+30 -20
View File
@@ -4,15 +4,15 @@
<el-header> <el-header>
<div <div
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;"> style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/> <img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/> <img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
</div> </div>
</el-header> </el-header>
<el-main style="position: relative;"> <el-main style="position: relative;">
<div class="login-box"> <div class="login-box">
<div <div
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/> <img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
<div class="login-text">登录</div> <div class="login-text">登录</div>
<div class="login-welcome"> <div class="login-welcome">
WELCOME TO LOGIN WELCOME TO LOGIN
@@ -20,16 +20,16 @@
</div> </div>
<div style="padding: 0 30px;"> <div style="padding: 0 30px;">
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/username.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/username.png"/>
<el-input v-model="form.username" placeholder="请输入用户名"/> <el-input v-model="form.username" placeholder="请输入用户名"/>
</div> </div>
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.password" type="password" placeholder="请输入密码"/> <el-input v-model="form.password" placeholder="请输入密码" type="password"/>
</div> </div>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <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;"> <div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img src="@/assets/login/shield.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/shield.png"/>
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/> <el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
</div> </div>
<img v-if="captchaUrl" <img v-if="captchaUrl"
@@ -105,33 +105,43 @@ export default {
} }
}, },
// 封装输入验证逻辑
validateInput(input, message) {
if (!input.trim()) {
showDanger(message);
return false;
}
return true;
},
async login() { async login() {
if (!this.form.username.trim()) { // 替换isNull校验 // 验证用户名
showDanger('用户名不能为空') if (!this.validateInput(this.form.username, '用户名不能为空')) {
return return;
} }
if (!this.form.password.trim()) { // 替换isNull校验 // 验证密码
showDanger('密码不能为空') if (!this.validateInput(this.form.password, '密码不能为空')) {
return return;
} }
if (!this.form.captcha.trim()) { // 替换isNull校验 // 验证验证码
showDanger('验证码不能为空') if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
return return;
} }
this.form.captchaId = this.captchaUuid this.form.captchaId = this.captchaUuid
Api.user.login(this.form, ({data}) => { Api.user.login(this.form, ({data}) => {
console.log(data) console.log(data)
showSuccess('登陆成功!') showSuccess('登陆成功!')
// 将令牌存储到 Vuex 中
this.$store.commit('setToken', JSON.stringify(data.data)) this.$store.commit('setToken', JSON.stringify(data.data))
goToPage('/home') goToPage('/home')
}) })
// 重新获取验证码
setTimeout(() => { setTimeout(() => {
this.fetchCaptcha() this.fetchCaptcha();
}, 1000) }, 1000);
}, },
goToRegister() { goToRegister() {
@@ -140,6 +150,6 @@ export default {
} }
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import './auth.scss'; // 添加这行引用 @import './auth.scss'; // 添加这行引用
</style> </style>
+33 -24
View File
@@ -4,8 +4,8 @@
<!-- 保持相同的头部 --> <!-- 保持相同的头部 -->
<el-header> <el-header>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;"> <div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/> <img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/> <img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
</div> </div>
</el-header> </el-header>
@@ -13,7 +13,7 @@
<div class="login-box"> <div class="login-box">
<!-- 修改标题部分 --> <!-- 修改标题部分 -->
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> <div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/> <img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
<div class="login-text">注册</div> <div class="login-text">注册</div>
<div class="login-welcome"> <div class="login-welcome">
WELCOME TO REGISTER WELCOME TO REGISTER
@@ -23,33 +23,33 @@
<div style="padding: 0 30px;"> <div style="padding: 0 30px;">
<!-- 用户名输入框 --> <!-- 用户名输入框 -->
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/username.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/username.png"/>
<el-input v-model="form.username" placeholder="请输入用户名"/> <el-input v-model="form.username" placeholder="请输入用户名"/>
</div> </div>
<!-- 密码输入框 --> <!-- 密码输入框 -->
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.password" type="password" placeholder="请输入密码"/> <el-input v-model="form.password" placeholder="请输入密码" type="password"/>
</div> </div>
<!-- 新增确认密码 --> <!-- 新增确认密码 -->
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码"/> <el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
</div> </div>
<!-- 验证码部分保持相同 --> <!-- 验证码部分保持相同 -->
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <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;"> <div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img src="@/assets/login/shield.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/shield.png"/>
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/> <el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
</div> </div>
<img v-if="captchaUrl" <img v-if="captchaUrl"
:src="captchaUrl" :src="captchaUrl"
alt="验证码" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" style="width: 150px; height: 40px; cursor: pointer;"
@click="fetchCaptcha" @click="fetchCaptcha"
/> />
</div> </div>
@@ -119,24 +119,33 @@ export default {
}); });
}, },
// 封装输入验证逻辑
validateInput(input, message) {
if (!input.trim()) {
showDanger(message);
return false;
}
return true;
},
// 注册逻辑 // 注册逻辑
register() { register() {
if (!this.form.username.trim()) { // 验证用户名
showDanger('用户名不能为空') if (!this.validateInput(this.form.username, '用户名不能为空')) {
return return;
} }
if (!this.form.password.trim()) { // 验证密码
showDanger('密码不能为空') if (!this.validateInput(this.form.password, '密码不能为空')) {
return return;
} }
if (this.form.password !== this.form.confirmPassword) { if (this.form.password !== this.form.confirmPassword) {
showDanger('两次输入的密码不一致') showDanger('两次输入的密码不一致')
return return
} }
if (!this.form.captcha.trim()) { // 验证验证码
showDanger('验证码不能为空') if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
return return;
} }
Api.user.register(this.form, ({data}) => { Api.user.register(this.form, ({data}) => {
console.log(data) console.log(data)
if (data.code === 0) { if (data.code === 0) {
@@ -159,6 +168,6 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import './auth.scss'; // 修改为导入新建的SCSS文件 @import './auth.scss'; // 修改为导入新建的SCSS文件
</style> </style>
+230 -98
View File
@@ -1,48 +1,35 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<!-- 公共头部 -->
<HeaderBar/> <HeaderBar/>
<el-main style="padding: 20px;display: flex;flex-direction: column;"> <el-main style="padding: 16px;display: flex;flex-direction: column;">
<div style="border-radius: 20px;background: #fafcfe;"> <div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
<div <div
style="padding: 19px 30px;font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;display: flex;gap: 16px;align-items: center;"> style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
<div <div
style="width: 46px;height: 46px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;"> style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/setting-user.png" alt="" style="width: 24px;height: 24px;"/> <img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
</div> </div>
{{ deviceMac }} {{ form.agentName }}
</div> </div>
<div style="height: 1px;background: #e8f0ff;"/> <div style="height: 1px;background: #e8f0ff;"/>
<el-form ref="form" :model="form" label-width="90px"> <el-form ref="form" :model="form" label-width="72px">
<div style="padding: 20px 30px;max-width: 990px;"> <div style="padding: 16px 24px;max-width: 792px;">
<el-form-item label="助手昵称:"> <el-form-item label="助手昵称:">
<div class="input-46"> <div class="input-46" style="width: 100%; max-width: 412px;">
<el-input v-model="form.name"/> <el-input v-model="form.agentName"/>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色模版:"> <el-form-item label="角色模版:">
<div style="display: flex;gap: 10px;"> <div style="display: flex;gap: 8px;">
<div class="template-item"> <div v-for="template in templates" :key="template" class="template-item" :class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
台湾女友 {{ template }}
</div>
<div class="template-item">
土豆子
</div>
<div class="template-item">
英语老师
</div>
<div class="template-item">
好奇小男孩
</div>
<div class="template-item">
汪汪队队长
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色音色:"> <el-form-item label="角色音色:">
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 8px;align-items: center;">
<div class="input-46" style="flex:1.4;"> <div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;"> <el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label" <el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value"> :value="item.value">
</el-option> </el-option>
@@ -56,45 +43,39 @@
</el-form-item> </el-form-item>
<el-form-item label="角色介绍:"> <el-form-item label="角色介绍:">
<div class="textarea-box"> <div class="textarea-box">
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容" <el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit/> v-model="form.systemPrompt" maxlength="2000" show-word-limit/>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="记忆体:"> <el-form-item label="记忆体:">
<div class="textarea-box"> <div class="textarea-box">
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容" <el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000"/> v-model="form.langCode" maxlength="1000"/>
<div class="prompt-bottom"> <div class="prompt-bottom" @click="clearMemory">
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 8px;align-items: center;">
<div style="color: #979db1;font-size: 14px;">当前记忆每次对话后重新生成</div> <div style="color: #979db1;font-size: 11px;">当前记忆每次对话后重新生成</div>
<div class="clear-btn"> <div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 14px;"/> <i class="el-icon-delete-solid" style="font-size: 11px;"/>
清除 清除
</div> </div>
</div> </div>
<div style="color: #979db1;font-size:14px;">{{ form.prompt.length }}/1000</div> <div style="color: #979db1;font-size:11px;">{{ form.langCode.length }}/1000</div>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="语言模型(内测):" class="lh-form-item"> <el-form-item v-for="model in models" :key="model.label" :label="model.label" class="model-item">
<div style="display: flex;gap: 10px;"> <el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
<div class="input-46" style="width: 100%;"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"/>
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;"> </el-select>
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="" class="lh-form-item"> <el-form-item label="" class="lh-form-item" style="margin-top: -25px;">
<div style="color: #979db1;text-align: left;">除了Qwen <div style="color: #979db1;text-align: left;">除了Qwen
实时其他模型通常会增加约1秒的延迟改变模型后建议清空记忆体以免影响体验 实时其他模型通常会增加约1秒的延迟改变模型后建议清空记忆体以免影响体验
</div> </div>
</el-form-item> </el-form-item>
</div> </div>
</el-form> </el-form>
<div style="display: flex;padding: 20px;gap: 10px;align-items: center;"> <div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
<div class="save-btn" @click="saveConfig"> <div class="save-btn" @click="saveConfig">
保存配置 保存配置
</div> </div>
@@ -102,12 +83,12 @@
重制 重制
</div> </div>
<div class="clear-text"> <div class="clear-text">
<img src="@/assets/home/red-info.png" alt="" style="width: 24px;height: 24px;"/> <img src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
保存配置后需要重启设备新的配置才会生效 保存配置后需要重启设备新的配置才会生效
</div> </div>
</div> </div>
</div> </div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;"> <div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 24px;color: #979db1;">
©2025 xiaozhi-esp32-server ©2025 xiaozhi-esp32-server
</div> </div>
</el-main> </el-main>
@@ -122,27 +103,65 @@ export default {
components: {HeaderBar}, components: {HeaderBar},
data() { data() {
return { return {
deviceMac: 'CC:ba:97:11:a6:ac',
form: { form: {
name: "", agentCode: "",
timbre: "", agentName: "",
introduction: "", ttsVoiceId: "",
prompt: "", systemPrompt: "",
model: "" langCode: "",
language: "",
sort: "",
model: {
ttsModelId: "",
vadModelId: "",
asrModelId: "",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
}, },
options: [{ options: [
value: '选项1', {value: '选项1', label: '黄金糕'},
label: '黄金糕' {value: '选项2', label: '双皮奶'}
}, { ],
value: '选项2', models: [
label: '双皮奶' {label: '大语言模型(LLM)', key: 'llmModelId'},
}] {label: '语音识别(ASR)', key: 'asrModelId'},
{label: '语音活动检测模型(VAD)', key: 'vadModelId'},
{label: '语音合成模型(TTS)', key: 'ttsModelId'},
{label: '意图识别模型(Intent)', key: 'intentModelId'},
{label: '记忆模型(Memory)', key: 'memModelId'}
],
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长'],
loadingTemplate: false
} }
}, },
methods: { methods: {
saveConfig() { saveConfig() {
// 此处写保存配置逻辑 const configData = {
this.$message.success('配置已保存') agentCode: this.form.agentCode,
agentName: this.form.agentName,
asrModelId: this.form.model.asrModelId,
vadModelId: this.form.model.vadModelId,
llmModelId: this.form.model.llmModelId,
ttsModelId: this.form.model.ttsModelId,
ttsVoiceId: this.form.ttsVoiceId,
memModelId: this.form.model.memModelId,
intentModelId: this.form.model.intentModelId,
systemPrompt: this.form.systemPrompt,
langCode: this.form.langCode,
language: this.form.language,
sort: this.form.sort
};
import('@/apis/module/user').then(({default: userApi}) => {
userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
if (data.code === 0) {
this.$message.success('配置保存成功');
} else {
this.$message.error(data.msg || '配置保存失败');
}
});
});
}, },
resetConfig() { resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', { this.$confirm('确定要重置配置吗?', '提示', {
@@ -152,15 +171,99 @@ export default {
}).then(() => { }).then(() => {
// 重置表单 // 重置表单
this.form = { this.form = {
name: "", agentCode: "",
timbre: "", agentName: "",
introduction: "", ttsVoiceId: "",
prompt: "", systemPrompt: "",
model: "" langCode: "",
language: "",
sort: "",
model: {
ttsModelId: "",
vadModelId: "",
asrModelId: "",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
} }
this.$message.success('配置已重置') this.$message.success('配置已重置')
}).catch(() => { }).catch(() => {
}) })
},
selectTemplate(templateName) {
if (this.loadingTemplate) return;
this.loadingTemplate = true;
import('@/apis/module/user').then(({default: userApi}) => {
userApi.getAgentTemplate(
{templateName},
(response) => {
this.loadingTemplate = false;
if (response.data.code === 0 && response.data.data.length > 0) {
this.applyTemplateData(response.data.data[0]);
this.$message.success(`${templateName}」模板已应用`);
} else {
this.$message.warning(`未找到「${templateName}」模板`);
}
}
);
}).catch((error) => {
this.loadingTemplate = false;
this.$message.error('模板加载失败');
console.error('接口异常:', error);
});
},
applyTemplateData(templateData) {
this.form = {
...this.form,
agentName: templateData.agentName || this.form.agentName,
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
langCode: templateData.langCode || this.form.langCode,
model: {
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
vadModelId: templateData.vadModelId || this.form.model.vadModelId,
asrModelId: templateData.asrModelId || this.form.model.asrModelId,
llmModelId: templateData.llmModelId || this.form.model.llmModelId,
memModelId: templateData.memModelId || this.form.model.memModelId,
intentModelId: templateData.intentModelId || this.form.model.intentModelId
}
};
},
fetchAgentConfig(agentId) {
import('@/apis/module/user').then(({default: userApi}) => {
userApi.getDeviceConfig(agentId, ({data}) => {
if (data.code === 0) {
this.form = {
...this.form,
...data.data,
model: {
ttsModelId: data.data.ttsModelId,
vadModelId: data.data.vadModelId,
asrModelId: data.data.asrModelId,
llmModelId: data.data.llmModelId,
memModelId: data.data.memModelId,
intentModelId: data.data.intentModelId
}
};
} else {
this.$message.error(data.msg || '获取配置失败');
}
});
});
},
// 清空记忆体内容
clearMemory() {
this.form.langCode = "";
this.$message.success("记忆体已清空");
},
},
mounted() {
const agentId = this.$route.query.agentId;
console.log('agentId2222',agentId);
if (agentId) {
this.fetchAgentConfig(agentId);
} }
} }
} }
@@ -168,9 +271,11 @@ export default {
<style scoped> <style scoped>
.welcome { .welcome {
min-width: 1200px; min-width: 900px;
min-height: 675px; min-height: 506px;
height: 100vh; height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png"); background-image: url("@/assets/home/background.png");
background-size: cover; background-size: cover;
/* 确保背景图像覆盖整个元素 */ /* 确保背景图像覆盖整个元素 */
@@ -181,71 +286,97 @@ export default {
-o-background-size: cover; -o-background-size: cover;
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
} }
.el-form-item ::v-deep .el-form-item__label {
font-size: 10px !important;
color: #3d4566 !important;
font-weight: 400;
line-height: 22px;
padding-bottom: 2px;
}
.select-field{
width: 100%;
max-width: 720px;
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
height: 36px !important;
}
.audio-box { .audio-box {
flex: 1; flex: 1;
height: 46px; height: 37px;
border-radius: 10px; border-radius: 20px;
border: 1px solid #e4e6ef; border: 1px solid #e4e6ef;
} }
.clear-btn { .clear-btn {
width: 60px; width: 48px;
height: 24px; height: 19px;
background: #fd8383; background: #fd8383;
border-radius: 12px; border-radius: 10px;
line-height: 24px; line-height: 19px;
font-size: 14px; font-size: 11px;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
} }
.clear-text { .clear-text {
color: #979db1; color: #979db1;
font-size: 14px; font-size: 11px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
margin-left: 20px; margin-left: 16px;
} }
.template-item { .template-item {
height: 46px; height: 37px;
width: 100px; width: 76px;
border-radius: 10px; border-radius: 8px;
background: #e6ebff; background: #e6ebff;
line-height: 46px; line-height: 37px;
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 11px;
text-align: center; text-align: center;
color: #5778ff; color: #5778ff;
cursor: pointer;
transition: background-color 0.3s ease;
}
.template-item:hover {
background-color: #d0d8ff;
} }
.prompt-bottom { .prompt-bottom {
margin-bottom: 5px; margin-bottom: 4px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0 20px; padding: 0 16px;
align-items: center; align-items: center;
} }
.input-46 { .input-46 {
border: 1px solid #e4e6ef; border: 1px solid #e4e6ef;
background: #f6f8fb; background: #f6f8fb;
border-radius: 10px; border-radius: 8px;
height: 36px !important;
} }
.save-btn, .save-btn,
.reset-btn { .reset-btn {
width: 140px; width: 112px;
height: 46px; height: 37px;
border-radius: 23px; border-radius: 18px;
line-height: 46px; line-height: 37px;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
font-size: 11px
} }
.save-btn { .save-btn {
border-radius: 23px; border-radius: 18px;
background: #5778ff; background: #5778ff;
color: #fff; color: #fff;
} }
@@ -258,7 +389,8 @@ export default {
.textarea-box { .textarea-box {
border: 1px solid #e4e6ef; border: 1px solid #e4e6ef;
border-radius: 10px; border-radius: 8px;
background: #f6f8fb; background: #f6f8fb;
} }
</style> </style>
+1
View File
@@ -7,6 +7,7 @@ dotenv.config();
module.exports = defineConfig({ module.exports = defineConfig({
devServer: { devServer: {
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL // Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
port: 8001, // 指定端口为 8001
proxy: { proxy: {
'/xiaozhi-esp32-api': { '/xiaozhi-esp32-api': {
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
+120 -35
View File
@@ -24,9 +24,9 @@ server:
# - "24:0A:C4:1D:3B:F0" # MAC地址列表 # - "24:0A:C4:1D:3B:F0" # MAC地址列表
log: log:
# 设置控制台输出的日志格式,时间、日志级别、标签、消息 # 设置控制台输出的日志格式,时间、日志级别、标签、消息
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>" 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_simple: "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}" log_format_file: "{time:YYYY-MM-DD HH:mm:ss} - {version}_{selected_module} - {name} - {level} - {extra[tag]} - {message}"
# 设置日志等级:INFO、DEBUG # 设置日志等级:INFO、DEBUG
log_level: INFO log_level: INFO
# 设置日志路径 # 设置日志路径
@@ -49,7 +49,7 @@ prompt: |
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。 请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
现在我正在和你进行语音聊天,我们开始吧。 现在我正在和你进行语音聊天,我们开始吧。
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
# 使用完声音文件后删除文件(Delete the sound file when you are done using it) # 使用完声音文件后删除文件(Delete the sound file when you are done using it)
delete_audio: true delete_audio: true
@@ -57,6 +57,14 @@ delete_audio: true
close_connection_no_voice_time: 120 close_connection_no_voice_time: 120
# TTS请求超时时间(秒) # TTS请求超时时间(秒)
tts_timeout: 10 tts_timeout: 10
# 开启唤醒词加速
enable_wakeup_words_response_cache: true
# 开场是否回复唤醒词
enable_greeting: true
# 说完话是否开启提示音
enable_stop_tts_notify: false
# 说完话是否开启提示音,音效地址
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
CMD_exit: CMD_exit:
- "退出" - "退出"
@@ -74,11 +82,12 @@ selected_module:
TTS: EdgeTTS TTS: EdgeTTS
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
Memory: nomem Memory: nomem
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令 # 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间 # 不想开通意图识别,就设置成:nointent
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快 # 意图识别使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
# 如果意图识别设置成 function_call建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 # 意图识别可使用function_call缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
Intent: nointent # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
Intent: function_call
# 意图识别,是用于理解用户意图的模块,例如:播放音乐 # 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent: Intent:
@@ -89,15 +98,26 @@ Intent:
intent_llm: intent_llm:
# 不需要动type # 不需要动type
type: intent_llm type: intent_llm
# 配备意图识别独立的思考模型
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
# 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
llm: ChatGLMLLM
function_call: function_call:
# 不需要动type # 不需要动type
type: nointent type: function_call
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用 # plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载 # 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
# 下面是加载查天气、角色切换的插件示例 # 下面是加载查天气、角色切换、加载查新闻的插件示例
functions: functions:
- change_role - change_role
- get_weather - get_weather
- get_news
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
# 如果用了hass_play_music,就不要开启play_music,两者只留一个
- play_music
#- hass_get_state
#- hass_set_state
#- hass_play_music
# 插件的基础配置 # 插件的基础配置
plugins: plugins:
@@ -106,6 +126,28 @@ plugins:
# 想稳定一点就自行申请替换,每天有1000次免费调用 # 想稳定一点就自行申请替换,每天有1000次免费调用
# 申请地址:https://console.qweather.com/#/apps/create-key/over # 申请地址:https://console.qweather.com/#/apps/create-key/over
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" } get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
get_news:
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
category_urls:
society: "https://www.chinanews.com.cn/rss/society.xml"
world: "https://www.chinanews.com.cn/rss/world.xml"
finance: "https://www.chinanews.com.cn/rss/finance.xml"
home_assistant:
devices:
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
base_url: http://homeassistant.local:8123
api_key: 你的home assistant api访问令牌
play_music:
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
music_ext: # 音乐文件类型,p3格式效率最高
- ".mp3"
- ".wav"
- ".p3"
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
Memory: Memory:
mem0ai: mem0ai:
@@ -125,6 +167,10 @@ ASR:
type: fun_local type: fun_local
model_dir: models/SenseVoiceSmall model_dir: models/SenseVoiceSmall
output_dir: tmp/ output_dir: tmp/
SherpaASR:
type: sherpa_onnx_local
model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17
output_dir: tmp/
DoubaoASR: DoubaoASR:
type: doubao type: doubao
appid: 你的火山引擎语音合成服务appid appid: 你的火山引擎语音合成服务appid
@@ -138,6 +184,7 @@ VAD:
min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些 min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些
LLM: LLM:
# 所有openai类型均可以修改超参,以AliLLM为例
# 当前支持的type为openai、dify、ollama,可自行适配 # 当前支持的type为openai、dify、ollama,可自行适配
AliLLM: AliLLM:
# 定义LLM API类型 # 定义LLM API类型
@@ -146,6 +193,23 @@ LLM:
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
model_name: qwen-turbo model_name: qwen-turbo
api_key: 你的deepseek web key api_key: 你的deepseek web key
temperature: 0.7 # 温度值
max_tokens: 500 # 最大生成token数
top_p: 1
top_k: 50
frequency_penalty: 0 # 频率惩罚
AliAppLLM:
# 定义LLM API类型
type: AliBL
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
app_id: 你的app_id
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
api_key: 你的api_key
# 是否不使用本地prompttrue|false (默不用请在百练应用中设置prompt)
is_no_prompt: true
# Ali_memory_idfalse(不使用)|你的memory_id(请在百练应用中设置中获取)
# Tips!:Ali_memory未实现多用户存储记忆(记忆按id调用)
ali_memory_id: false
DoubaoLLM: DoubaoLLM:
# 定义LLM API类型 # 定义LLM API类型
type: openai type: openai
@@ -181,8 +245,12 @@ LLM:
type: dify type: dify
# 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限 # 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限
# 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词 # 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词
base_url: https://api.dify.cn/v1 base_url: https://api.dify.ai/v1
api_key: 你的DifyLLM web key api_key: 你的DifyLLM web key
# 使用的对话模式 可以选择工作流 workflows/run 对话模式 chat-messages 文本生成 completion-messages
# 使用workflows进行返回的时候输入参数为 query 返回参数的名字要设置为 answer
# 文本生成的默认输入参数也是query
mode: chat-messages
GeminiLLM: GeminiLLM:
type: gemini type: gemini
# 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key # 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key
@@ -190,7 +258,9 @@ LLM:
# token申请地址: https://aistudio.google.com/apikey # token申请地址: https://aistudio.google.com/apikey
# 若部署地无法访问接口,需要开启科学上网 # 若部署地无法访问接口,需要开启科学上网
api_key: 你的gemini web key api_key: 你的gemini web key
model_name: "gemini-1.5-pro" # gemini-1.5-pro 是免费的 model_name: "gemini-2.0-flash"
http_proxy: "" #"http://127.0.0.1:10808"
https_proxy: "" #http://127.0.0.1:10808"
CozeLLM: CozeLLM:
# 定义LLM API类型 # 定义LLM API类型
type: coze type: coze
@@ -204,12 +274,6 @@ LLM:
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载 model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
url: http://localhost:1234/v1 # LM Studio服务地址 url: http://localhost:1234/v1 # LM Studio服务地址
api_key: lm-studio # LM Studio服务的固定API Key api_key: lm-studio # LM Studio服务的固定API Key
HomeAssistant:
# 定义LLM API类型
type: homeassistant
base_url: http://homeassistant.local:8123
agent_id: conversation.chatgpt
api_key: 你的home assistant api访问令牌
FastgptLLM: FastgptLLM:
# 定义LLM API类型 # 定义LLM API类型
type: fastgpt type: fastgpt
@@ -219,6 +283,18 @@ LLM:
variables: variables:
k: "v" k: "v"
k2: "v2" k2: "v2"
XinferenceLLM:
# 定义LLM API类型
type: xinference
# Xinference服务地址和模型名称
model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型
base_url: http://localhost:9997 # Xinference服务地址
XinferenceSmallLLM:
# 定义轻量级LLM API类型,用于意图识别
type: xinference
# Xinference服务地址和模型名称
model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
base_url: http://localhost:9997 # Xinference服务地址
TTS: TTS:
# 当前支持的type为edge、doubao,可自行适配 # 当前支持的type为edge、doubao,可自行适配
EdgeTTS: EdgeTTS:
@@ -232,7 +308,8 @@ TTS:
# 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token # 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token
# 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误 # 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
# 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。 # 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
# 地址https://console.volcengine.com/speech/service/8 # 普通音色在这里开通https://console.volcengine.com/speech/service/8
# 湾湾小何音色在这里开通:https://console.volcengine.com/speech/service/10007,开通后将下面的voice设置成zh_female_wanwanxiaohe_moon_bigtts
api_url: https://openspeech.bytedance.com/api/v1/tts api_url: https://openspeech.bytedance.com/api/v1/tts
voice: BV001_streaming voice: BV001_streaming
output_dir: tmp/ output_dir: tmp/
@@ -396,9 +473,21 @@ TTS:
type: doubao type: doubao
api_url: https://api.302ai.cn/doubao/tts_hd api_url: https://api.302ai.cn/doubao/tts_hd
authorization: "Bearer " authorization: "Bearer "
# 湾湾小何音色
voice: "zh_female_wanwanxiaohe_moon_bigtts" voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_dir: tmp/ output_dir: tmp/
access_token: "你的302API密钥" access_token: "你的302API密钥"
GizwitsTTS:
type: doubao
# 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务
# 前一万名注册的用户,将送5元体验金额
# 获取API Key地址:https://agentrouter.gizwitsapi.com/panel/token
api_url: https://bytedance.gizwitsapi.com/api/v1/tts
authorization: "Bearer "
# 湾湾小何音色
voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_dir: tmp/
access_token: "你的机智云API key"
ACGNTTS: ACGNTTS:
#在线网址:https://acgn.ttson.cn/ #在线网址:https://acgn.ttson.cn/
#token购买:www.ttson.cn #token购买:www.ttson.cn
@@ -451,19 +540,15 @@ module_test:
- "What's the weather like today?" - "What's the weather like today?"
- "请用100字概括量子计算的基本原理和应用前景" - "请用100字概括量子计算的基本原理和应用前景"
# 本地音乐播放配置 # 唤醒词,用于识别唤醒词还是讲话内容
music: wakeup_words:
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 - "你好小智"
music_ext: # 音乐文件类型,p3格式效率最高 - "你好小志"
- ".mp3" - "小爱同学"
- ".wav" - "你好小鑫"
- ".p3" - "你好小新"
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 - "小美同学"
- "小龙小龙"
# 以下配置在小于等于0.0.9版本中的docker容器中可用 - "喵喵同学"
# 0.0.9以后的新版本源码部署已经无法奏效 - "小滨小滨"
manager: - "小冰小冰"
enabled: false
ip: 0.0.0.0
port: 8002
use_private_config: false
Binary file not shown.
Binary file not shown.
+15 -3
View File
@@ -3,12 +3,24 @@ import sys
from loguru import logger from loguru import logger
from config.settings import load_config from config.settings import load_config
SERVER_VERSION = "0.1.16"
def setup_logging(): def setup_logging():
"""从配置文件中读取日志配置,并设置日志输出格式和级别""" """从配置文件中读取日志配置,并设置日志输出格式和级别"""
config = load_config() config = load_config()
log_config = config["log"] log_config = config["log"]
log_format = log_config.get("log_format", "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>") log_format = log_config.get("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_simple = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}") 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 = config.get("selected_module")
selected_module_str = ''.join([value[0] + value[1] for key, value in selected_module.items()])
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)
log_level = log_config.get("log_level", "INFO") log_level = log_config.get("log_level", "INFO")
log_dir = log_config.get("log_dir", "tmp") log_dir = log_config.get("log_dir", "tmp")
log_file = log_config.get("log_file", "server.log") log_file = log_config.get("log_file", "server.log")
@@ -24,6 +36,6 @@ def setup_logging():
logger.add(sys.stdout, format=log_format, level=log_level) logger.add(sys.stdout, format=log_format, level=log_level)
# 输出到文件 # 输出到文件
logger.add(os.path.join(log_dir, log_file), format=log_format_simple, level=log_level) logger.add(os.path.join(log_dir, log_file), format=log_format_file, level=log_level)
return logger return logger
+156 -38
View File
@@ -9,7 +9,7 @@ import traceback
import threading import threading
import websockets import websockets
from typing import Dict, Any from typing import Dict, Any
import plugins_func.loadplugins from plugins_func.loadplugins import auto_import_modules
from config.logger import setup_logging from config.logger import setup_logging
from core.utils.dialogue import Message, Dialogue from core.utils.dialogue import Message, Dialogue
from core.handle.textHandle import handleTextMessage from core.handle.textHandle import handleTextMessage
@@ -18,13 +18,16 @@ from concurrent.futures import ThreadPoolExecutor, TimeoutError
from core.handle.sendAudioHandle import sendAudioMessage from core.handle.sendAudioHandle import sendAudioMessage
from core.handle.receiveAudioHandle import handleAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage
from core.handle.functionHandler import FunctionHandler from core.handle.functionHandler import FunctionHandler
from plugins_func.register import Action from plugins_func.register import Action, ActionResponse
from config.private_config import PrivateConfig from config.private_config import PrivateConfig
from core.auth import AuthMiddleware, AuthenticationError from core.auth import AuthMiddleware, AuthenticationError
from core.utils.auth_code_gen import AuthCodeGenerator from core.utils.auth_code_gen import AuthCodeGenerator
from core.mcp.manager import MCPManager
TAG = __name__ TAG = __name__
auto_import_modules('plugins_func.functions')
class TTSException(RuntimeError): class TTSException(RuntimeError):
pass pass
@@ -98,6 +101,8 @@ class ConnectionHandler:
self.use_function_call_mode = False self.use_function_call_mode = False
if self.config["selected_module"]["Intent"] == 'function_call': if self.config["selected_module"]["Intent"] == 'function_call':
self.use_function_call_mode = True self.use_function_call_mode = True
self.mcp_manager = MCPManager(self)
async def handle_connection(self, ws): async def handle_connection(self, ws):
try: try:
@@ -109,11 +114,15 @@ class ConnectionHandler:
# 进行认证 # 进行认证
await self.auth.authenticate(self.headers) await self.auth.authenticate(self.headers)
device_id = self.headers.get("device-id", None) device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
self.intent.set_llm(self.llm)
# 认证通过,继续处理
self.websocket = ws
self.session_id = str(uuid.uuid4())
self.welcome_msg = self.config["xiaozhi"]
self.welcome_msg["session_id"] = self.session_id
await self.websocket.send(json.dumps(self.welcome_msg))
# Load private configuration if device_id is provided # Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False) bUsePrivateConfig = self.config.get("use_private_config", False)
self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}") self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
@@ -141,43 +150,32 @@ class ConnectionHandler:
self.private_config = None self.private_config = None
raise raise
# 认证通过,继续处理
self.websocket = ws
self.session_id = str(uuid.uuid4())
self.welcome_msg = self.config["xiaozhi"]
self.welcome_msg["session_id"] = self.session_id
await self.websocket.send(json.dumps(self.welcome_msg))
# 异步初始化 # 异步初始化
await self.loop.run_in_executor(None, self._initialize_components) self.executor.submit(self._initialize_components)
# tts 消化线程 # tts 消化线程
tts_priority = threading.Thread(target=self._tts_priority_thread, daemon=True) self.tts_priority_thread = threading.Thread(target=self._tts_priority_thread, daemon=True)
tts_priority.start() self.tts_priority_thread.start()
# 音频播放 消化线程 # 音频播放 消化线程
audio_play_priority = threading.Thread(target=self._audio_play_priority_thread, daemon=True) self.audio_play_priority_thread = threading.Thread(target=self._audio_play_priority_thread, daemon=True)
audio_play_priority.start() self.audio_play_priority_thread.start()
try: try:
async for message in self.websocket: async for message in self.websocket:
await self._route_message(message) await self._route_message(message)
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
self.logger.bind(tag=TAG).info("客户端断开连接") self.logger.bind(tag=TAG).info("客户端断开连接")
await self.close()
except AuthenticationError as e: except AuthenticationError as e:
self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}") self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
await ws.close()
return return
except Exception as e: except Exception as e:
stack_trace = traceback.format_exc() stack_trace = traceback.format_exc()
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}") self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
await ws.close()
return return
finally: finally:
await self.memory.save_memory(self.dialogue.dialogue) await self.memory.save_memory(self.dialogue.dialogue)
await self.close(ws)
async def _route_message(self, message): async def _route_message(self, message):
"""消息路由""" """消息路由"""
@@ -187,16 +185,53 @@ class ConnectionHandler:
await handleAudioMessage(self, message) await handleAudioMessage(self, message)
def _initialize_components(self): def _initialize_components(self):
"""加载提示词"""
self.prompt = self.config["prompt"] self.prompt = self.config["prompt"]
if self.private_config: if self.private_config:
self.prompt = self.private_config.private_config.get("prompt", self.prompt) self.prompt = self.private_config.private_config.get("prompt", self.prompt)
self.client_ip_info = get_ip_info(self.client_ip)
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt + f"\n我在:{self.client_ip_info}"
self.dialogue.put(Message(role="system", content=self.prompt)) self.dialogue.put(Message(role="system", content=self.prompt))
self.func_handler = FunctionHandler(self.config) """加载插件"""
self.func_handler = FunctionHandler(self)
"""加载记忆"""
device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
"""为意图识别设置LLM,优先使用专用LLM"""
# 检查是否配置了专用的意图识别LLM
intent_llm_name = self.config["Intent"]["intent_llm"]["llm"]
# 记录开始初始化意图识别LLM的时间
intent_llm_init_start = time.time()
if not self.use_function_call_mode and intent_llm_name and intent_llm_name in self.config["LLM"]:
# 如果配置了专用LLM,则创建独立的LLM实例
from core.utils import llm as llm_utils
intent_llm_config = self.config["LLM"][intent_llm_name]
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config)
self.logger.bind(tag=TAG).info(f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}")
self.intent.set_llm(intent_llm)
else:
# 否则使用主LLM
self.intent.set_llm(self.llm)
self.logger.bind(tag=TAG).info("意图识别使用主LLM")
# 记录意图识别LLM初始化耗时
intent_llm_init_time = time.time() - intent_llm_init_start
self.logger.bind(tag=TAG).info(f"意图识别LLM初始化完成,耗时: {intent_llm_init_time:.4f}")
"""加载位置信息"""
self.client_ip_info = get_ip_info(self.client_ip)
if self.client_ip_info is not None and "city" in self.client_ip_info:
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
self.dialogue.update_system_message(self.prompt)
"""加载MCP工具"""
asyncio.run_coroutine_threadsafe(self.mcp_manager.initialize_servers(), self.loop)
def change_system_prompt(self, prompt): def change_system_prompt(self, prompt):
self.prompt = prompt self.prompt = prompt
@@ -316,8 +351,9 @@ class ConnectionHandler:
self.dialogue.put(Message(role="user", content=query)) self.dialogue.put(Message(role="user", content=query))
# Define intent functions # Define intent functions
functions = self.func_handler.get_functions() functions = None
if hasattr(self, 'func_handler'):
functions = self.func_handler.get_functions()
response_message = [] response_message = []
processed_chars = 0 # 跟踪已处理的字符位置 processed_chars = 0 # 跟踪已处理的字符位置
@@ -351,6 +387,9 @@ class ConnectionHandler:
content_arguments = "" content_arguments = ""
for response in llm_responses: for response in llm_responses:
content, tools_call = response content, tools_call = response
if "content" in response:
content = response["content"]
tools_call = None
if content is not None and len(content) > 0: if content is not None and len(content) > 0:
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content): if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
tool_call_flag = True tool_call_flag = True
@@ -429,8 +468,14 @@ class ConnectionHandler:
"id": function_id, "id": function_id,
"arguments": function_arguments "arguments": function_arguments
} }
result = self.func_handler.handle_llm_function_call(self, function_call_data)
self._handle_function_result(result, function_call_data, text_index + 1) # 处理MCP工具调用
if self.mcp_manager.is_mcp_tool(function_name):
result = self._handle_mcp_tool_call(function_call_data)
else:
# 处理系统函数
result = self.func_handler.handle_llm_function_call(self, function_call_data)
self._handle_function_result(result, function_call_data, text_index+1)
# 处理最后剩余的文本 # 处理最后剩余的文本
full_text = "".join(response_message) full_text = "".join(response_message)
@@ -452,6 +497,42 @@ class ConnectionHandler:
return True return True
def _handle_mcp_tool_call(self, function_call_data):
function_arguments = function_call_data["arguments"]
function_name = function_call_data["name"]
try:
args_dict = function_arguments
if isinstance(function_arguments, str):
try:
args_dict = json.loads(function_arguments)
except json.JSONDecodeError:
self.logger.bind(tag=TAG).error(f"无法解析 function_arguments: {function_arguments}")
return ActionResponse(action=Action.REQLLM, result="参数解析失败", response="")
tool_result = asyncio.run_coroutine_threadsafe(self.mcp_manager.execute_tool(
function_name,
args_dict
), self.loop).result()
# meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False
content_text = ""
if tool_result is not None and tool_result.content is not None:
for content in tool_result.content:
content_type = content.type
if content_type == "text":
content_text = content.text
elif content_type == "image":
pass
if len(content_text) > 0:
return ActionResponse(action=Action.REQLLM, result=content_text, response="")
except Exception as e:
self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}")
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, text_index):
if result.action == Action.RESPONSE: # 直接回复前端 if result.action == Action.RESPONSE: # 直接回复前端
text = result.response text = result.response
@@ -492,7 +573,12 @@ class ConnectionHandler:
while not self.stop_event.is_set(): while not self.stop_event.is_set():
text = None text = None
try: try:
future = self.tts_queue.get() try:
future = self.tts_queue.get(timeout=1)
except queue.Empty:
if self.stop_event.is_set():
break
continue
if future is None: if future is None:
continue continue
text = None text = None
@@ -533,7 +619,12 @@ class ConnectionHandler:
while not self.stop_event.is_set(): while not self.stop_event.is_set():
text = None text = None
try: try:
opus_datas, text, text_index = self.audio_play_queue.get() try:
opus_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, opus_datas, text, text_index), future = asyncio.run_coroutine_threadsafe(sendAudioMessage(self, opus_datas, text, text_index),
self.loop) self.loop)
future.result() future.result()
@@ -563,16 +654,43 @@ class ConnectionHandler:
self.tts_first_text_index = text_index self.tts_first_text_index = text_index
self.tts_last_text_index = text_index self.tts_last_text_index = text_index
async def close(self): async def close(self, ws=None):
"""资源清理方法""" """资源清理方法"""
# 清理MCP资源
await self.mcp_manager.cleanup_all()
# 清理其他资源 # 触发停止事件并清理资源
self.stop_event.set() if self.stop_event:
self.executor.shutdown(wait=False) self.stop_event.set()
if self.websocket:
# 立即关闭线程池
if self.executor:
self.executor.shutdown(wait=False, cancel_futures=True)
self.executor = None
# 清空任务队列
self._clear_queues()
if ws:
await ws.close()
elif self.websocket:
await self.websocket.close() await self.websocket.close()
self.logger.bind(tag=TAG).info("连接资源已释放") self.logger.bind(tag=TAG).info("连接资源已释放")
def _clear_queues(self):
# 清空所有任务队列
for q in [self.tts_queue, self.audio_play_queue]:
if not q:
continue
while not q.empty():
try:
q.get_nowait()
except queue.Empty:
continue
q.queue.clear()
# 添加毒丸信号到队列,确保线程退出
# q.queue.put(None)
def reset_vad_states(self): def reset_vad_states(self):
self.client_audio_buffer = bytes() self.client_audio_buffer = bytes()
self.client_have_voice = False self.client_have_voice = False
@@ -1,16 +1,16 @@
import asyncio
from enum import Enum
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
from plugins_func.functions.hass_init import append_devices_to_prompt
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
class FunctionHandler: class FunctionHandler:
def __init__(self, config): def __init__(self, conn):
self.config = config self.conn = conn
self.config = conn.config
self.function_registry = FunctionRegistry() self.function_registry = FunctionRegistry()
self.register_nessary_functions() self.register_nessary_functions()
self.register_config_functions() self.register_config_functions()
@@ -26,9 +26,10 @@ class FunctionHandler:
func_names = ",".join(surport_plugins) func_names = ",".join(surport_plugins)
for function_desc in self.functions_desc: for function_desc in self.functions_desc:
if function_desc["function"]["name"] == "plugin_loader": if function_desc["function"]["name"] == "plugin_loader":
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names) function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]",
func_names)
break break
def upload_functions_desc(self): def upload_functions_desc(self):
self.functions_desc = self.function_registry.get_all_function_desc() self.functions_desc = self.function_registry.get_all_function_desc()
@@ -47,16 +48,19 @@ class FunctionHandler:
def register_nessary_functions(self): def register_nessary_functions(self):
"""注册必要的函数""" """注册必要的函数"""
self.function_registry.register_function("handle_exit_intent") self.function_registry.register_function("handle_exit_intent")
self.function_registry.register_function("play_music")
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("raise_and_lower_the_volume") self.function_registry.register_function("get_lunar")
self.function_registry.register_function("handle_device")
def register_config_functions(self): def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置""" """注册配置中的函数,可以不同客户端使用不同的配置"""
for func in self.config["Intent"]["function_call"].get("functions", []): for func in self.config["Intent"]["function_call"].get("functions", []):
self.function_registry.register_function(func) self.function_registry.register_function(func)
"""home assistant需要初始化提示词"""
append_devices_to_prompt(self.conn)
def get_function(self, name): def get_function(self, name):
return self.function_registry.get_function(name) return self.function_registry.get_function(name)
@@ -81,4 +85,4 @@ class FunctionHandler:
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理function call错误: {e}") logger.bind(tag=TAG).error(f"处理function call错误: {e}")
return None return None
@@ -1,8 +1,74 @@
import json import json
from config.logger import setup_logging from config.logger import setup_logging
from core.handle.sendAudioHandle import send_stt_message
from core.utils.util import remove_punctuation_and_length
import shutil
import asyncio
import os
import random
import time
logger = setup_logging() logger = setup_logging()
WAKEUP_CONFIG = {"dir": "config/assets/", "file_name": "wakeup_words", "create_time": time.time(), "refresh_time": 10,
"words": ["很高兴见到你", "你好啊", "我们又见面了", "最近可好?", "很高兴再次和你谈话", "在干嘛"]}
async def handleHelloMessage(conn): async def handleHelloMessage(conn):
await conn.websocket.send(json.dumps(conn.welcome_msg)) await conn.websocket.send(json.dumps(conn.welcome_msg))
async def checkWakeupWords(conn, text):
enable_wakeup_words_response_cache = conn.config["enable_wakeup_words_response_cache"]
"""是否开启唤醒词加速"""
if not enable_wakeup_words_response_cache:
return False
"""检查是否是唤醒词"""
_, text = remove_punctuation_and_length(text)
if text in conn.config.get("wakeup_words"):
await send_stt_message(conn, text)
conn.tts_first_text_index = 0
conn.tts_last_text_index = 0
conn.llm_finish_task = True
file = getWakeupWordFile(WAKEUP_CONFIG["file_name"])
if file is None:
asyncio.create_task(wakeupWordsResponse(conn))
return False
opus_packets, duration = conn.tts.audio_to_opus_data(file)
conn.audio_play_queue.put((opus_packets, text, 0))
if time.time() - WAKEUP_CONFIG["create_time"] > WAKEUP_CONFIG["refresh_time"]:
asyncio.create_task(wakeupWordsResponse(conn))
return True
return False
def getWakeupWordFile(file_name):
for file in os.listdir(WAKEUP_CONFIG["dir"]):
if file.startswith("my_" + file_name):
"""避免缓存文件是一个空文件"""
if os.stat(f"config/assets/{file}").st_size > (5 * 1024):
return f"config/assets/{file}"
"""查找config/assets/目录下名称为wakeup_words的文件"""
for file in os.listdir(WAKEUP_CONFIG["dir"]):
if file.startswith(file_name):
return f"config/assets/{file}"
return None
async def wakeupWordsResponse(conn):
"""唤醒词响应"""
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
if tts_file is not None and os.path.exists(tts_file):
file_type = os.path.splitext(tts_file)[1]
if file_type:
file_type = file_type.lstrip('.')
old_file = getWakeupWordFile("my_" + WAKEUP_CONFIG["file_name"])
if old_file is not None:
os.remove(old_file)
"""将文件挪到"wakeup_words.mp3"""
shutil.move(tts_file, WAKEUP_CONFIG["dir"] + "my_" + WAKEUP_CONFIG["file_name"] + "." + file_type)
WAKEUP_CONFIG["create_time"] = time.time()
@@ -2,7 +2,10 @@ from config.logger import setup_logging
import json import json
import uuid import uuid
from core.handle.sendAudioHandle import send_stt_message from core.handle.sendAudioHandle import send_stt_message
from core.handle.helloHandle import checkWakeupWords
from core.utils.util import remove_punctuation_and_length from core.utils.util import remove_punctuation_and_length
from core.utils.dialogue import Message
from loguru import logger
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -12,15 +15,19 @@ async def handle_user_intent(conn, text):
# 检查是否有明确的退出命令 # 检查是否有明确的退出命令
if await check_direct_exit(conn, text): if await check_direct_exit(conn, text):
return True return True
# 检查是否是唤醒词
if await checkWakeupWords(conn, text):
return True
if conn.use_function_call_mode: if conn.use_function_call_mode:
# 使用支持function calling的聊天方法,不再进行意图分析 # 使用支持function calling的聊天方法,不再进行意图分析
return False return False
# 使用LLM进行意图分析 # 使用LLM进行意图分析
intent = await analyze_intent_with_llm(conn, text) intent_result = await analyze_intent_with_llm(conn, text)
if not intent: if not intent_result:
return False return False
# 处理各种意图 # 处理各种意图
return await process_intent_result(conn, intent, text) return await process_intent_result(conn, intent_result, text)
async def check_direct_exit(conn, text): async def check_direct_exit(conn, text):
@@ -45,52 +52,65 @@ async def analyze_intent_with_llm(conn, text):
dialogue = conn.dialogue dialogue = conn.dialogue
try: try:
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text) intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
# 尝试解析JSON结果 return intent_result
try:
intent_data = json.loads(intent_result)
if "intent" in intent_data:
return intent_data["intent"]
except json.JSONDecodeError:
# 如果不是JSON格式,尝试直接获取意图文本
return intent_result.strip()
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}") logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
return None return None
async def process_intent_result(conn, intent, original_text): async def process_intent_result(conn, intent_result, original_text):
"""处理意图识别结果""" """处理意图识别结果"""
# 处理退出意图 try:
if "结束聊天" in intent: # 尝试将结果解析为JSON
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}") intent_data = json.loads(intent_result)
# 如果是明确的离别意图,发送告别语并关闭连接
await send_stt_message(conn, original_text)
conn.executor.submit(conn.chat_and_close, original_text)
return True
# 处理播放音乐意图 # 检查是否有function_call
if "播放音乐" in intent: if "function_call" in intent_data:
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}") # 直接从意图识别获取了function_call
# 调用play_music函数来播放音乐 logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}")
song_name = extract_text_in_brackets(intent) function_name = intent_data["function_call"]["name"]
function_id = str(uuid.uuid4().hex) if function_name == "continue_chat":
function_name = "play_music" return False
function_arguments = '{ "song_name": "' + song_name + '" }' function_args = None
if "arguments" in intent_data["function_call"]:
function_args = intent_data["function_call"]["arguments"]
# 确保参数是字符串格式的JSON
if isinstance(function_args, dict):
function_args = json.dumps(function_args)
function_call_data = { function_call_data = {
"name": function_name, "name": function_name,
"id": function_id, "id": str(uuid.uuid4().hex),
"arguments": function_arguments "arguments": function_args
} }
conn.func_handler.handle_llm_function_call(conn, function_call_data)
return True
# 其他意图处理可以在这里扩展 await send_stt_message(conn, original_text)
# 默认返回False,表示继续常规聊天流程 # 使用executor执行函数调用和结果处理
return False def process_function_call():
conn.dialogue.put(Message(role="user", content=original_text))
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result and function_name != 'play_music':
# 获取当前最新的文本索引
text = result.response
if text is None:
text = result.result
if text is not None:
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
conn.recode_first_last_text(text, text_index)
future = conn.executor.submit(conn.speak_and_play, text, text_index)
conn.llm_finish_task = True
conn.tts_queue.put(future)
conn.dialogue.put(Message(role="assistant", content=text))
# 将函数执行放在线程池中
conn.executor.submit(process_function_call)
return True
return False
except json.JSONDecodeError as e:
logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
return False
def extract_text_in_brackets(s): def extract_text_in_brackets(s):
@@ -106,4 +126,4 @@ def extract_text_in_brackets(s):
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index: if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
return s[left_bracket_index + 1:right_bracket_index] return s[left_bracket_index + 1:right_bracket_index]
else: else:
return "" return ""
@@ -51,8 +51,6 @@ def create_iot_function(device_name, method_name, method_info):
# 生成结果信息 # 生成结果信息
result = f"{device_name}{method_name}操作执行成功" result = f"{device_name}{method_name}操作执行成功"
# 根据方法名尝试自动更新状态
await update_state_by_method(conn, device_name, method_name, params)
# 处理响应中可能的占位符 # 处理响应中可能的占位符
response = response_success response = response_success
@@ -114,35 +112,6 @@ def create_iot_query_function(device_name, prop_name, prop_info):
return wrap_async_function(iot_query_function) return wrap_async_function(iot_query_function)
async def update_state_by_method(conn, device_name, method_name, params):
"""根据方法和参数自动更新设备状态"""
try:
# 规则1: 方法名为TurnOn,设置power为True
if method_name == "TurnOn":
await set_iot_status(conn, device_name, "power", True)
# 规则2: 方法名为TurnOff,设置power为False
elif method_name == "TurnOff":
await set_iot_status(conn, device_name, "power", False)
# 规则3: Set开头的方法,尝试更新对应参数
elif method_name.startswith("Set"):
# 从参数中找到可能的状态值
for param_name, param_value in params.items():
# 尝试更新对应名称的属性
await set_iot_status(conn, device_name, param_name, param_value)
# 其他方法,尝试直接从参数更新状态
else:
for param_name, param_value in params.items():
# 检查设备是否有此属性
status = await get_iot_status(conn, device_name, param_name)
if status is not None: # 属性存在
await set_iot_status(conn, device_name, param_name, param_value)
except Exception as e:
logger.bind(tag=TAG).warning(f"自动更新状态失败: {e}")
class IotDescriptor: class IotDescriptor:
""" """
A class to represent an IoT descriptor. A class to represent an IoT descriptor.

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