Compare commits

...
Author SHA1 Message Date
Tyke Chen 942d55118b test(manager-api): clean test runtime warnings 2026-07-22 16:41:44 +08:00
CGDandGitHub f47f3b050b Merge pull request #3297 from xinnan-tech/fix/manager-api-compiler-warnings
fix(manager-api): 清理编译器告警
2026-07-22 16:22:46 +08:00
wengzhandGitHub e3453b1a87 Merge pull request #3298 from xinnan-tech/py-fix-intent
Py fix intent
2026-07-21 15:16:19 +08:00
Tyke Chen d760465408 fix(manager-api): make JSON collections type-safe 2026-07-21 14:43:25 +08:00
Tyke Chen bdb2538239 fix(manager-api): clean deprecated and generic warnings 2026-07-21 14:43:25 +08:00
CGDandGitHub ea6c144e43 Merge pull request #3294 from xinnan-tech/fix/manager-api-startup-warnings
fix(manager-api): 消除启动阶段告警
2026-07-21 14:37:22 +08:00
CGDandGitHub a79aa455d6 Merge pull request #3292 from xinnan-tech/fix/manager-api-test-i18n
test: 修复 manager-api 国际化测试配置
2026-07-21 14:29:30 +08:00
wengzh 6e92d169ec refactor(get_news_from_newsnow): 改用流式处理解析网页内容
将原有的直接转换响应内容改为流式读取字节流并传入参数,适配MarkItDown的convert_stream接口,优化大内容处理时的内存占用
2026-07-20 15:51:23 +08:00
Sakura-RanChen ed89c05245 fix: intent阻塞主线程 2026-07-20 14:48:26 +08:00
Tyke Chen b500d1c6bd fix(manager-api): avoid duplicate address book insert mapping 2026-07-20 14:46:14 +08:00
Tyke Chen 0598b09629 fix(manager-api): remove invalid logback startup directives 2026-07-20 14:46:14 +08:00
Tyke Chen e4907121f4 fix(manager-api): eliminate premature bean initialization warnings 2026-07-20 14:46:14 +08:00
CGDandGitHub 7c58fa37b2 Merge pull request #3293 from xinnan-tech/refactor/mybatis-plus-single-source
refactor(manager-api): consolidate MyBatis-Plus 3.5.17 integration
2026-07-20 14:42:31 +08:00
Tyke Chen 2a618d2f8f test: 修复 manager-api 国际化测试配置 2026-07-20 10:34:33 +08:00
Tyke Chen 10a67c772f refactor(manager-api): consolidate mybatis-plus 3.5.17 integration 2026-07-20 10:31:08 +08:00
CGDandGitHub 7e8e5d34b8 Merge pull request #3279 from xinnan-tech/fix/issue-3276-context-provider
fix: 修复 ContextProviderListTypeHandler 多版本兼容问题
2026-07-17 09:28:14 +08:00
CGDandGitHub c6bc20c05e Merge pull request #3283 from xinnan-tech/fix/device-timezone-consistency
fix: 修复跨时区场景下设备时间与在线状态异常
2026-07-16 16:58:49 +08:00
wengzhandGitHub 1998807b69 Merge pull request #3285 from xinnan-tech/py-aec
perf: 优化相关逻辑减少重复计算
2026-07-14 16:36:16 +08:00
Sakura-RanChen 55c0495661 perf: 优化相关逻辑减少重复计算 2026-07-14 15:52:30 +08:00
Tyke Chen dbeda01697 fix: 修复设备跨时区时间与在线状态 2026-07-14 10:08:23 +08:00
Tyke Chen 023dea2441 fix: 修复上下文源类型处理器兼容问题 2026-07-13 16:03:39 +08:00
CGDandGitHub ad3fb4d8c8 Merge pull request #3278 from xinnan-tech/agent-snapshot-history-fixes
fix: 完善智能体配置历史与恢复逻辑
2026-07-13 15:47:08 +08:00
3030332422 176785830a update:修改部分样式 2026-07-13 15:44:06 +08:00
Tyke Chen d78ecfce6f Merge remote-tracking branch 'origin/main' into agent-snapshot-history-fixes 2026-07-10 20:26:30 +08:00
Tyke Chen 179281e49c fix: harden agent snapshot restore flow 2026-07-10 20:21:44 +08:00
hrzandGitHub bc1aa1a089 Merge pull request #3267 from xinnan-tech/fix-agent-idor
fix: prevent agent IDOR access
2026-07-10 17:36:00 +08:00
wengzhandGitHub c5b64c3ef0 Merge pull request #3270 from xinnan-tech/fix-voiceprint-dialogue-bug
fix: 修改提示词与说话人注入逻辑,减少模型回答带着说话人信息频率
2026-07-10 15:55:58 +08:00
DaGou12138 2aa6c5ee49 fix:
1.优化context注入方式,合并为一个system,解决qwen本地部署模型多system异常400问题
2026-07-10 15:05:02 +08:00
Tyke Chen 687b6db96b fix: resolve mobile type and lint errors 2026-07-10 11:45:16 +08:00
Tyke Chen a5aee109fe feat: improve agent snapshot history 2026-07-10 11:44:35 +08:00
hrzandGitHub 8ec5851027 Merge pull request #3273 from xinnan-tech/agent-delete-name-confirm
feat: 删除智能体前需要用户进行确认
2026-07-10 10:39:47 +08:00
Tyke Chen 48d629c79c fix: localize agent deletion success message 2026-07-10 10:18:48 +08:00
wengzh 14e57a1add fix(powermem): 修复用户配置启用状态的类型判断问题
将配置的启用值统一转为小写字符串后判断是否为true,避免非布尔类型配置导致的判断失效
2026-07-10 10:06:07 +08:00
hrzandGitHub 634967b2fe Merge pull request #3274 from xinnan-tech/i18n-duplicate-check
test: 增加 i18n 重复 key 检测
2026-07-10 09:59:33 +08:00
Tyke Chen e648453c67 fix: rename i18n check command 2026-07-10 09:55:53 +08:00
Tyke Chen d11cfc6923 Merge remote-tracking branch 'origin/main' into fix-agent-idor
# Conflicts:
#	main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java
#	main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java
#	main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
2026-07-09 20:21:37 +08:00
DaGou12138 32fbbf92fe Merge branch 'refs/heads/main' into fix-voiceprint-dialogue-bug 2026-07-09 16:56:22 +08:00
wengzhandGitHub bd981aac79 Merge pull request #3275 from xinnan-tech/py-audio-aec
feat: 服务端AEC功能
2026-07-09 16:24:03 +08:00
Sakura-RanChenandGitHub adee070bb0 Merge branch 'main' into py-audio-aec 2026-07-09 15:51:01 +08:00
wengzhandGitHub 03ec3bac3f Merge pull request #3271 from xinnan-tech/py-fix-bug
Py fix bug
2026-07-09 15:36:59 +08:00
Tyke Chen af5fbb5282 test: add i18n duplicate key check 2026-07-09 15:25:36 +08:00
Tyke Chen 9301c1889f feat: require agent name confirmation before delete 2026-07-09 15:03:12 +08:00
hrzandGitHub ef2ed07c1c Merge pull request #3266 from xinnan-tech/agent-snapshot-history
Add agent configuration history
2026-07-09 14:54:27 +08:00
Tyke Chen 1e2c4c6a28 fix: 修复智能体快照基线与查看逻辑 2026-07-09 11:16:07 +08:00
Sakura-RanChen a32500a6a4 fix: 消除死锁隐患 2026-07-09 10:29:41 +08:00
hrz 772af6c918 Merge branch 'agent-snapshot-history' of https://github.com/xinnan-tech/xiaozhi-esp32-server into agent-snapshot-history 2026-07-08 17:46:50 +08:00
hrz 2a917c1e62 update:对齐i18n文件行数 2026-07-08 17:44:10 +08:00
Tyke Chen c32fbe69c5 Remove wakeup word audio asset 2026-07-08 16:33:36 +08:00
DaGou12138 3b6e8f0e4b fix:
1.优化声纹称呼人提示词和注入逻辑,减少次次都称呼的频率
2026-07-08 16:14:10 +08:00
Tyke Chen aae2ef2152 fix: prevent agent IDOR access 2026-07-08 15:52:47 +08:00
Tyke Chen c82b351457 Fix agent snapshot history issues 2026-07-08 15:15:26 +08:00
DaGou12138 04c10ef225 fix:
1.修改提示词与说话人注入,减少模型每次回答都带着说话人信息
2026-07-08 14:12:16 +08:00
Sakura-RanChen 0b8bb728e2 同步方法进行包装避免阻塞 2026-07-08 11:47:04 +08:00
mie 35ec3ba803 Add agent configuration history 2026-07-07 17:49:34 +08:00
wengzhandGitHub 8808b69303 Merge pull request #3265 from xinnan-tech/ha-muisc-timeout
Ha muisc timeout
2026-07-07 16:25:12 +08:00
Sakura-RanChen fc573a72ea fix: 修复设备时间戳上报逻辑 2026-07-07 15:50:27 +08:00
Sakura-RanChen 59f6566cfb 同步工具异步化处理 2026-07-07 14:45:50 +08:00
Sakura-RanChen d8c97a9f81 fix: 修复携程死锁 2026-07-06 16:44:41 +08:00
Sakura-RanChen 3d204fb181 feat: 服务端AEC功能
ref: 入口解码为pcm音频数据,vad和asr可直接使用
2026-07-06 16:21:24 +08:00
hrzandGitHub 209fd3fbc8 Merge pull request #3260 from xinnan-tech/test-funasr
FunASR识别指定语言
2026-07-06 13:56:48 +08:00
3030332422 bc0ffd1bd3 update:修改sql文件,删去测试文件 2026-07-01 15:02:26 +08:00
hrzandGitHub f0d61442b8 Merge pull request #3259 from xinnan-tech/update-theme
顶部下拉菜单图标间距调整
2026-06-30 18:45:14 +08:00
CGDandGitHub e6c2fac7ec Merge pull request #3255 from LauraGPT/funasr-language-option
fix: allow configuring FunASR recognition language
2026-06-30 14:57:31 +08:00
zhifu gao 5a9b5af441 fix: add FunASR language config migration 2026-06-30 14:07:16 +08:00
欣南科技andGitHub 16e5dc8c78 Merge pull request #3258 from xinnan-tech/update-version
update:Bump to 0.9.5
2026-06-30 13:10:18 +08:00
hrz f77970dd7d update:Bump to 0.9.5 2026-06-30 13:10:26 +08:00
zhuoqinglian af3ca4f609 update: 顶部下拉菜单图标间距调整 2026-06-30 09:47:28 +08:00
hrzandGitHub 1ebfcd1f13 Merge pull request #3256 from xinnan-tech/update-theme
解决智能体管理查询不展示的问题&删除知识库旧版废弃文件
2026-06-30 09:36:42 +08:00
zhuoqinglian 3ca322961e fix: 解决智能体管理查询不展示的问题 2026-06-30 09:22:05 +08:00
zhuoqinglian 0eb0a600a7 update: 删除知识库旧版废弃文件 2026-06-30 09:19:26 +08:00
wengzhandGitHub 3ed06a9e34 Merge pull request #3244 from xinnan-tech/base_prompt-update
feat: 将系统提示词指令逻辑英文化,中文样本保留
2026-06-30 09:06:10 +08:00
LauraGPT 2fae96d60c fix: allow configuring FunASR recognition language 2026-06-29 20:58:51 +00:00
wengzhandGitHub 67dde7cc47 Merge pull request #3254 from xinnan-tech/update-theme
智能体管理搜索恢复查询历史功能
2026-06-29 16:58:52 +08:00
zhuoqinglian efedc18f02 update: 智能体管理搜索恢复查询历史功能 2026-06-29 16:01:14 +08:00
zhuoqinglian 0270de1844 update: 滚动条样式抽离复用 2026-06-29 15:42:52 +08:00
wengzhandGitHub 06b0ff139b Merge pull request #3247 from xinnan-tech/update-theme
Update theme
2026-06-26 17:58:18 +08:00
zhuoqinglian 799ea28d82 Merge branch 'main' into update-theme 2026-06-26 17:30:15 +08:00
zhuoqinglian 5a68252d22 update: 更新所有页面背景颜色 2026-06-26 17:28:06 +08:00
zhuoqinglian 97404e53fe update: 顶部导航栏搜索放到首页 2026-06-26 17:26:16 +08:00
zhuoqinglian b052158cee feat: 完成导航菜单组件样式优化 2026-06-26 17:10:49 +08:00
zhuoqinglian 979ef24b79 update: 音色克隆多语言配置 2026-06-26 15:32:48 +08:00
zhuoqinglian 0f4f0f01fd update: 音色克隆页面展现形式修改 2026-06-26 14:45:23 +08:00
wengzhandGitHub 18e1de4f45 Merge pull request #3245 from xinnan-tech/py-add-minimax-voice
Py add minimax voice
2026-06-26 11:42:59 +08:00
Sakura-RanChen aa846a096f 名称长度调整 2026-06-26 11:41:46 +08:00
Sakura-RanChen a679412230 增加试听音色 2026-06-26 11:40:52 +08:00
3030332422 cf3e14bb07 feat: 将系统提示词指令逻辑英文化,中文样本保留 2026-06-26 11:06:18 +08:00
wengzhandGitHub 3a73a17253 Merge pull request #3243 from xinnan-tech/py-add-minnimax-voice
调整minimax音色
2026-06-26 10:50:26 +08:00
Sakura-RanChen 3b27ebcd35 调整minimax音色 2026-06-26 10:29:07 +08:00
wengzhandGitHub fa6074d284 Merge pull request #3242 from xinnan-tech/update-theme
Update theme
2026-06-25 17:29:55 +08:00
zhuoqinglian d809cfcb01 Merge branch 'main' into update-theme 2026-06-25 17:27:40 +08:00
zhuoqinglian eb2c09bbdc feat: 完成聊天记录、修改密码页面样式改造 2026-06-25 17:26:52 +08:00
zhuoqinglian 2cf8f79482 update: 模型配置新增、编辑弹窗样式优化 2026-06-25 17:09:29 +08:00
zhuoqinglian 1c94844bb9 feat: 完成声纹管理页面样式改造 2026-06-25 09:18:38 +08:00
wengzhandGitHub 0c53b8f543 Merge pull request #3240 from xinnan-tech/update-theme
Update theme
2026-06-24 17:58:13 +08:00
zhuoqinglian 1518dfca30 feat: 完成设备管理页面样式改造 2026-06-24 17:37:37 +08:00
zhuoqinglian 7529c500a4 feat: 完成音色克隆页面样式改造 2026-06-24 11:43:36 +08:00
zhuoqinglian b6ed220e48 feat: 完成音色资源页面样式改造 2026-06-23 17:23:11 +08:00
wengzhandGitHub b152c5dc23 Merge pull request #3239 from xinnan-tech/update-theme
模型配置页面样式调整
2026-06-23 16:31:30 +08:00
zhuoqinglian 04bfddf457 update: 按钮样式风格统一 2026-06-23 14:54:45 +08:00
zhuoqinglian 9d78ea29c3 Merge branch 'main' into update-theme 2026-06-23 14:15:57 +08:00
zhuoqinglian f82f364189 feat: 完成模型配置页面样式改造 2026-06-23 14:15:32 +08:00
zhuoqinglian 9705149b2b feat: 按钮组件新增add类型 2026-06-23 14:14:29 +08:00
wengzhandGitHub 7986148013 Merge pull request #3238 from xinnan-tech/py-add-edge-config
补充edge相关配置
2026-06-23 10:40:40 +08:00
Sakura-RanChen 19f5cb5b4f 补充edge相关配置 2026-06-23 10:37:48 +08:00
zhuoqinglian ea48b948bf update: 模型配置新增弹窗样式调整 2026-06-22 17:58:35 +08:00
zhuoqinglian 3c5ca9f143 update: 模型配置编辑弹窗样式调整 2026-06-22 17:31:09 +08:00
wengzhandGitHub da72ecaa1b Merge pull request #3235 from xinnan-tech/py-fix-tts
fix: 超时情况不与activate_session同一判断条件导致的监听任务未清除问题
2026-06-22 11:21:03 +08:00
Sakura-RanChen f297e1b648 fix: 超时情况不与activate_session同一判断条件导致的监听任务未清除问题 2026-06-22 11:16:25 +08:00
wengzhandGitHub a1973e07b7 Merge pull request #3227 from xinnan-tech/update-theme
feat: 完成字典管理页面样式改造
2026-06-18 16:57:49 +08:00
zhuoqinglian cd58b2c3cb Merge branch 'main' into update-theme 2026-06-18 16:55:33 +08:00
zhuoqinglian a5be67d04b feat: 完成字典管理页面样式改造 2026-06-18 16:49:15 +08:00
wengzhandGitHub e6dc6187c8 Merge pull request #3226 from xinnan-tech/update-theme
页面样式改造
2026-06-18 16:39:41 +08:00
zhuoqinglian 7f4011f9de feat: 完成字段管理页面样式改造 2026-06-18 16:37:42 +08:00
zhuoqinglian 211cf891af Merge branch 'main' into update-theme 2026-06-18 10:26:57 +08:00
zhuoqinglian 89a1229b53 feat: 完成服务端管理页面样式改造 2026-06-18 10:26:06 +08:00
wengzhandGitHub 071ed99a32 Merge pull request #3224 from xinnan-tech/py-fix-bug
fix: 通讯录页面时间不显示
2026-06-17 14:36:48 +08:00
Sakura-RanChen ea14be27e6 fix: 通讯录页面时间不显示
fix: listen消息相关状态处理
2026-06-17 14:34:42 +08:00
zhuoqinglian a834f163b7 feat: 完成替换词页面样式改造 2026-06-17 14:20:48 +08:00
zhuoqinglian 7849e6b030 update: 模板保存添加必填校验 2026-06-17 10:36:02 +08:00
wengzhandGitHub 0d869baaa0 Merge pull request #3223 from xinnan-tech/update-theme
页面样式调整
2026-06-16 17:01:10 +08:00
zhuoqinglian 164eb5481c feat: 完成默认角色管理页面样式改造,并添加编辑保存弹窗 2026-06-16 16:58:44 +08:00
zhuoqinglian 969ee3cb12 feat: 完成字段管理页面样式改造 2026-06-16 16:18:12 +08:00
zhuoqinglian ee54d87383 feat: 完成固件管理页面样式改造 2026-06-16 14:55:08 +08:00
wengzhandGitHub 849026c015 Merge pull request #3222 from xinnan-tech/update-theme
Update theme
2026-06-16 10:42:24 +08:00
zhuoqinglian e5bb3e7791 update: 优化表格组件,增加代码的复用性 2026-06-16 10:40:44 +08:00
zhuoqinglian 1ffc40e71d feat: 完成用户管理页面样式改造 2026-06-16 10:39:52 +08:00
zhuoqinglian 45fd850757 update: 表格组件添加阴影效果 2026-06-16 09:46:25 +08:00
zhuoqinglian 8a5dac181e update: 知识库顶部标题合并 2026-06-15 17:37:53 +08:00
zhuoqinglian ecf20300ff update: 通讯录初始化加载默认选中第一项 2026-06-15 17:16:50 +08:00
zhuoqinglian 74def844f0 Merge branch 'main' into update-theme 2026-06-15 17:05:28 +08:00
zhuoqinglian a5a9bd1abe fix: 修复element组件多语言切换不生效的问题 2026-06-15 17:05:04 +08:00
zhuoqinglian 2d9b915d51 feat: 完成参数管理页面改造 2026-06-15 15:44:09 +08:00
hrzandGitHub 5f87fc8361 Merge pull request #3204 from xinnan-tech/py-fix-calling
优化相关逻辑
2026-06-15 14:06:51 +08:00
zhuoqinglian 6859fdc90e update: 系统功能配置图标大小优化 2026-06-15 11:27:12 +08:00
zhuoqinglian b8ae0568e9 update: 系统功能配置页面适配调整 2026-06-15 11:23:31 +08:00
wengzhandGitHub 1dc6f143f3 Merge pull request #3219 from xinnan-tech/update-theme
完成系统功能页面样式改造
2026-06-12 14:43:23 +08:00
zhuoqinglian 67532dc230 update: 系统功能配置完善多语言 2026-06-12 14:23:25 +08:00
zhuoqinglian b2876def99 Merge branch 'main' into update-theme 2026-06-12 14:14:59 +08:00
zhuoqinglian 0c73a8b6a4 feat: 完成系统功能页面样式改造 2026-06-12 14:13:21 +08:00
zhuoqinglian c857a3164d feat: 完成按钮组件抽离 2026-06-12 11:49:22 +08:00
hrzandGitHub a964192eb0 Merge pull request #3213 from xinnan-tech/fix-knowledge-base-ragflow-bug
#3212 修复知识库RAGFlow相关逻辑BUG
2026-06-12 11:15:35 +08:00
DaGou12138 32da03818e #3212 fix:
1.修改知识库前端卡片提示样式,优化警示效果
2026-06-12 10:35:40 +08:00
DaGou12138 7f17e3e08e Merge branch 'refs/heads/main' into fix-knowledge-base-ragflow-bug 2026-06-12 10:29:17 +08:00
DaGou12138 c237e1f323 Merge remote-tracking branch 'origin/fix-knowledge-base-ragflow-bug' into fix-knowledge-base-ragflow-bug 2026-06-12 09:51:03 +08:00
DaGou12138 087a202e31 #3212 fix:
1.优化知识库连接RAGFlow异常提示,修改前端卡片提示样式
2026-06-12 09:50:01 +08:00
hrz 4b1460d938 update:调整同步数据集错误日志的级别 2026-06-11 19:10:21 +08:00
hrz f2aec524d2 del:无用类 2026-06-11 18:57:30 +08:00
wengzhandGitHub 1a97ca137c Merge pull request #3215 from xinnan-tech/update-theme
Update theme
2026-06-11 17:45:58 +08:00
zhuoqinglian 2b6f457ae6 update: 知识库页面间距优化 2026-06-11 17:44:17 +08:00
DaGou12138 3f4e0b4aa0 #3212 fix:
1.修复RAGFlow连接异常导致级联删除问题
2.修复RAGFlow获取数量逻辑问题
3.远程失败不再删除本地影子记录,避免产生RAGFlow端孤儿文档
4.优化前端提示语
2026-06-11 17:17:59 +08:00
DaGou12138 e30ded4de9 fix:删除知识库rag相关非必要实体 2026-06-11 16:08:17 +08:00
hrzandGitHub 07242a84ff Merge pull request #3210 from xinnan-tech/fix-knowledge-base-web-bug
fix:修复知识库模块弹框组件样式全局覆盖导致显示异常问题
2026-06-10 15:32:08 +08:00
DaGou12138 a2074dc0d8 fix:修复知识库模块弹框组件样式全局覆盖导致显示异常问题 2026-06-10 15:00:29 +08:00
zhuoqinglian 28b6c85650 fix: 修复弹窗展示位置异常的问题 2026-06-10 10:47:52 +08:00
wengzhandGitHub af300138eb Merge pull request #3209 from xinnan-tech/update-theme
Update theme
2026-06-09 17:24:16 +08:00
zhuoqinglian 2bf5725048 update: 知识库页面复用弹窗、分页组件,同步修改样式 2026-06-09 17:22:18 +08:00
hrzandGitHub 3d73141788 Merge pull request #3206 from xinnan-tech/web_search-Document
docs:新增联网搜索插件配置文档
2026-06-09 17:12:16 +08:00
hrz 945ab29bc3 add:增加联网搜索的教程和自编译docker镜像的教程 2026-06-09 17:11:35 +08:00
zhuoqinglian fe0f7dcf75 feat: 完成分页组件抽离 2026-06-09 15:05:16 +08:00
zhuoqinglian 1a3ffbc8e2 feat: 完成弹窗组件抽离 2026-06-09 14:25:49 +08:00
3030332422 71278b1898 docs:新增联网搜索插件配置文档 2026-06-08 11:38:11 +08:00
zhuoqinglian 0173c7abe1 update: 知识库样式优化、添加多语言配置 2026-06-05 17:37:21 +08:00
zhuoqinglian 8a50eea4ce update: 知识库整体页面样式调整 2026-06-03 16:49:08 +08:00
Sakura-RanChen b9891bb795 优化相关逻辑 2026-06-03 16:13:03 +08:00
wengzhandGitHub b903a88cd5 Merge pull request #3203 from xinnan-tech/mac-address-mask
feat: mac地址添加马赛克功能
2026-06-03 14:41:34 +08:00
zhuoqinglian f3fcf2fedf feat: mac地址添加马赛克功能 2026-06-03 14:40:01 +08:00
欣南科技andGitHub a825f0874c Merge pull request #3202 from xinnan-tech/update-doc-video
update:更新演示视频
2026-06-03 13:58:14 +08:00
hrz 8a5ca46418 update:更新演示视频 2026-06-03 13:56:34 +08:00
欣南科技andGitHub 5fcfc529c0 Merge pull request #3201 from xinnan-tech/0.9.4
Bump to 0.9.4
2026-06-03 09:08:43 +08:00
hrz 370d148a4e Bump to 0.9.4 2026-06-03 09:07:39 +08:00
欣南科技andGitHub 3088ddce38 Merge pull request #3200 from xinnan-tech/update_doc
update:补充设备间通话开启备注
2026-06-02 18:23:49 +08:00
hrz 8e3faf20d3 update:补充设备间通话开启备注 2026-06-02 18:22:31 +08:00
欣南科技andGitHub f42088eff4 Merge pull request #3199 from xinnan-tech/py-device-calling
update:如果通讯权限 alias 为空,就用默认使用设备名称
2026-06-01 23:53:29 +08:00
hrz a0181cc723 update:如果通讯权限 alias 为空,就用默认使用设备名称 2026-06-01 23:52:38 +08:00
欣南科技andGitHub 941aaf10ce Merge pull request #3198 from xinnan-tech/py-device-calling
fix:用户别名缺失bug
2026-06-01 22:57:24 +08:00
hrz 235ec8a25d update:更换通讯录图标 2026-06-01 22:55:03 +08:00
hrz effe95ba8f fix:用户别名缺失bug 2026-06-01 22:54:34 +08:00
CGDandGitHub 4c4ba0aa08 Merge pull request #3196 from xinnan-tech/py-device-calling
Py device calling
2026-06-01 16:34:02 +08:00
3030332422 7af4b41881 update:设备呼叫工具的接听触发提示词约束 2026-06-01 16:25:52 +08:00
hrz cfcf68d4ce Merge branch 'py-device-calling' of https://github.com/xinnan-tech/xiaozhi-esp32-server into py-device-calling 2026-05-31 10:30:54 +08:00
hrz 7f2675f9df update:增加设备呼叫使用条件 2026-05-31 10:30:10 +08:00
Sakura-RanChen 69fd5a1ea8 不开启通讯录功能时不显示相关工具
关闭通讯录功能清除相关状态
2026-05-29 17:34:21 +08:00
Sakura-RanChen 4392edf27d 调整相关说明文档 2026-05-29 15:56:52 +08:00
zhuoqinglian e4ea9c8eff Merge branch 'style-device-calling' into py-device-calling 2026-05-29 14:37:55 +08:00
zhuoqinglian 2adf51ddf9 fix: 修复顶部导航栏遮挡的问题 2026-05-29 14:36:14 +08:00
CGDandGitHub 3d1b133162 Merge pull request #3195 from xinnan-tech/fix-timezone-unify
fix:修复时区导致的设备状态不同步的问题
2026-05-29 10:29:02 +08:00
DaGou12138andGitHub f539d46de3 Merge pull request #3194 from xinnan-tech/fix(model-config)
fix(model-config): 修复合并JSON时的类型异常并添加日志
2026-05-29 10:10:21 +08:00
baigao 1701bfbfca fix(model-config): 修复合并JSON时的类型异常并添加日志
1. 为mergeJson方法添加空值检查和类型校验逻辑
2. 增加日志打印便于排查问题
3. 优化代码换行和注释格式
4. 修复原有代码中潜在的类型转换异常问题
2026-05-29 10:07:11 +08:00
zhuoqinglian bb48e7efab Merge branch 'style-device-calling' into py-device-calling 2026-05-29 10:05:24 +08:00
zhuoqinglian 9ea67d4306 update: 通讯录页面样式调整 2026-05-29 10:04:54 +08:00
rainv123 3a0ce6998e fix:修复设备型号不显示的问题 2026-05-29 09:50:09 +08:00
hrz 0678521b16 update:优化addressBook配置的的存储方式 2026-05-28 19:36:26 +08:00
hrz 84187d4e68 update:优化翻译 2026-05-28 15:18:54 +08:00
hrz 38958b2d06 add:通讯录i18n行数对齐 2026-05-28 15:02:20 +08:00
hrz 1e17e5ac8a fixed:当数据库的就数据不支持新特性时,增加新特性 2026-05-28 14:41:06 +08:00
rainv123 ec5063f050 fix: 设备最后连接时间返回 UTC 时间戳,修复时区显示问题 2026-05-27 17:35:10 +08:00
Sakura-RanChen d03583f493 增加工具配置说明 2026-05-27 17:28:58 +08:00
Sakura-RanChen 4a55662e91 fix: 修复智能体在线状态获取
补充相关国际化
2026-05-27 16:57:48 +08:00
Sakura-RanChen de78e87c77 调整逻辑使用java端作为中转发送 2026-05-27 15:34:17 +08:00
wengzhandGitHub ff011d3c44 Merge pull request #3192 from xinnan-tech/fix_rag_page_size
fix:修复知识库文档切片的分页显示问题
2026-05-27 10:30:13 +08:00
rainv123 a90229a22a fix:修复知识库文档切片的分页显示问题 2026-05-27 10:26:50 +08:00
wengzhandGitHub 559d47a229 Merge pull request #3189 from xinnan-tech/digital-human_prompt
update:根据数字人 emoji 开关动态渲染系统提示词
2026-05-26 16:38:46 +08:00
Sakura-RanChen ce68c83eea feat: 新增设备互相通讯功能
feat: 新增通讯录页面
2026-05-26 16:37:39 +08:00
3030332422 7e63b7e852 update:根据数字人 emoji 开关动态渲染系统提示词 2026-05-26 11:43:18 +08:00
wengzhandGitHub 7f73dae1bf Merge pull request #3183 from xinnan-tech/digital-human_fix_emoji
fix:情绪表情默认改为false
2026-05-20 17:49:02 +08:00
3030332422 48e8839a1e fix:情绪表情默认改为false 2026-05-20 17:43:55 +08:00
wengzhandGitHub 73f20ed9b0 Merge pull request #3182 from xinnan-tech/perf-tool-add-online-search
fix:修复deepseek模型下工具名重复导致错误问题
2026-05-20 15:03:26 +08:00
DaGou12138 48a2bd5a5a fix:修复deepseek模型下工具名重复导致错误问题 2026-05-20 14:52:21 +08:00
335 changed files with 28505 additions and 16027 deletions
+5
View File
@@ -112,12 +112,17 @@ celerybeat.pid
# Environments # Environments
.env .env
.venv .venv
/.venv-*/
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Repository-local runtimes and package-manager caches
/.runtime/
/main/manager-web/.npm-cache/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
+52 -67
View File
@@ -55,110 +55,95 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /> <img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture> <picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /> <img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="播放音乐" src="docs/images/demo7.png" /> <img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="docs/images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank"> <a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture> <picture>
<img alt="实时打断" src="docs/images/demo10.png" /> <img alt="实时打断" src="docs/images/demo10.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /> <img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="docs/images/demo2.png" /> <img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
+3
View File
@@ -65,6 +65,7 @@ VAD:
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/> 3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/> 4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/> 5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
### 8、编译固件相关教程 ### 8、编译固件相关教程
1、[如何自己编译小智固件](./firmware-build.md)<br/> 1、[如何自己编译小智固件](./firmware-build.md)<br/>
@@ -84,6 +85,8 @@ VAD:
10、[如何部署上下文源](./context-provider-integration.md)<br/> 10、[如何部署上下文源](./context-provider-integration.md)<br/>
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/> 11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
12、[如何配置天气插件查询天气](./weather-integration.md)<br/> 12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
### 10、数字人相关教程 ### 10、数字人相关教程
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/> 1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
+148
View File
@@ -0,0 +1,148 @@
# 设备间相互呼叫插件使用指南
## 概述
设备呼叫功能允许两个已配置设备之间通过语音/数据通道进行双向通信。设备A呼叫设备B时,系统通过以下流程实现:
```
设备A → 授权校验 → MQTT网关 → 设备B远程唤醒 → 建立连接 → 通话建立
```
## 使用这个功能的前提条件
1. 你必须要有至少两个设备,每个设备型号必须是`ESP32-S3`,因为只有`ESP32-S3`才支持远程唤醒功能。
2. 你的设备必须要有`两个麦克风`。但是如果你的设备只有`单个麦克风`,你只是想体验一下这个功能,也是可以的,但是会有强烈的卡顿感。
3. 你必须使用[全模块部署](Deployment_all.md)本项目,因为你需要`智控台`来管理设备的权限和通信。
4. 你必须安装并配置好`2026年5月27日`以后的[MQTT网关服务](mqtt-gateway-integration.md),如果你已经部署了MQTT网关服务,请确认代码的版本是`2026年5月27日`之后的版本。
以上是使用这个功能的硬性条件,接下来会详细介绍。
## 配置步骤
### 第一步:开启通讯录功能
1. 确认你的智控台版本是`0.9.4`或以上版本。
2. 登录智控台后台
3. 进入 **系统功能配置**
4. 在左侧功能列表中勾选 **通讯录**
5. 点击 **保存配置** 确认
### 第二步:配置设备间呼叫权限
1. 在智控台顶部菜单点击 **通讯录**
2. 在左侧智能体下,设备列表中选择你的设备A(支持按 MAC地址 或 备注名 搜索)
3. 在右侧详情面板中,找到目标设备B的称呼设置,例如 **"小王"**
4. 勾选设备B的 **呼叫权限** 复选框
5. 点击 **保存**
**双向授权说明:** 如需设备A和设备B互相通信,必须在两侧智控台分别配置对方权限。例如:
- 在设备A的配置中勾选设备B → 设备A可与设备B通信
- 在设备B的配置中勾选设备A → 设备B可与设备A通信
### 第三步:在智能体配置添加呼叫工具
1. 在智控台顶部菜单点击 **智能体管理**
2. 在刚刚配置设备联系人的相关智能体中点击 **编辑角色**
3. 在右侧详情面板中,点击 **编辑功能**
4. 勾选 **设备呼叫设备** 工具
5. 点击 **保存配置** 确认
6. 在外侧再次点击 **保存配置** ,随即重启设备
### 第四步:固件端添加远程唤醒工具
1. 在[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 代码的基础上增加远程唤醒工具MCP,版本支持为2.1.0至2.2.62026年5月29日的版本)
2. 在application.h文件中添加远程唤醒函数声明
```cpp
void RemoteWakeup(const std::string& reason);
```
3. 在application.cc文件中添加远程唤醒函数
```cpp
void Application::RemoteWakeup(const std::string& reason){
if (!protocol_) {
return;
}
auto state = GetDeviceState();
if (state == kDeviceStateIdle) {
audio_service_.EncodeWakeWord();
if (!protocol_->IsAudioChannelOpened()) {
SetDeviceState(kDeviceStateConnecting);
if (!protocol_->OpenAudioChannel()) {
audio_service_.EnableWakeWordDetection(true);
return;
}
}
std::string wake_word = reason;
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
// Encode and send the wake word data to the server
while (auto packet = audio_service_.PopWakeWordPacket()) {
protocol_->SendAudio(std::move(packet));
}
// Set the chat state to wake word detected
protocol_->SendWakeWordDetected(wake_word);
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
#else
// Set flag to play popup sound after state changes to listening
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
play_popup_on_listening_ = true;
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
#endif
} else if (state == kDeviceStateSpeaking) {
AbortSpeaking(kAbortReasonWakeWordDetected);
SetDeviceState(kDeviceStateIdle);
} else if (state == kDeviceStateActivating) {
SetDeviceState(kDeviceStateIdle);
}
}
```
4. 在mcp_server.cc文件中添加远程唤醒工具
```cpp
AddUserOnlyTool("self.remote_wakeup", "Remote wakeup function with configurable parameters",
PropertyList({
Property("reason", kPropertyTypeString, "Wakeup reason"),
}),
[this](const PropertyList& properties) -> ReturnValue {
std::string reason = properties["reason"].value<std::string>();
ESP_LOGI(TAG, "Wakeup reason=%s", reason.c_str());
auto& app = Application::GetInstance();
app.RemoteWakeup(reason);
return true;
```
5. 按照 [固件编译烧录指南](firmware-build.md) 完成固件烧录
6. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
7. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
8. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
### 第五步:配置MQTT网关服务
1. 部署MQTT网关服务,参考 [MQTT网关集成文档](mqtt-gateway-integration.md)
2. 如果已经部署请确认代码的版本是2026年5月27日的版本
## 呼叫流程说明
准备两个设备,在智控台上面配置好通讯权限和在智能体中添加呼叫工具之后,在其中一个小智对话那里对他说:”呼叫XXX“,观察设备B是否响应。
## 常见问题
### Q: 设备B没有响应呼叫?
- 检查设备B是否在线(智控台设备状态)
- 确认设备B的固件已正确集成远程唤醒工具
- 检查MQTT网关连接是否正常
- 验证双向权限配置是否完整
### Q: 提示"无呼叫权限"
- 在智控台确认设备A已勾选设备B的呼叫权限
- 确认配置已保存(非仅修改未保存)
### Q: 如何确认通讯录功能已开启?
- 智控台顶部菜单如显示"通讯录"入口,则表示已开启
### Q: 我叫他呼叫"张山",但是他老是识别成"张三",怎么办?
- 可以查阅你使用的asr服务的文档,确认是否支持热词识别。
- 如果你用的是`FunASRServer`,可以在容器里的`热词文件`里添加"张山",然后重启容器。
- 如果你用的是`火山引擎`的服务,可以在`火山引擎的控制台`里添加`热词文件`,然后回到智控台的`模型配置页面`,把`热词文件名称`配置在`火山引擎的tts`上去。
+66 -13
View File
@@ -1,21 +1,74 @@
# 本地编译docker镜像方法 # 本地编译docker镜像方法
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的 现在本项目已经使用`github``自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像需求,那就忽略本文档
1、安装docker 如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
```
## 1、环境准备
安装docker
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
``` ```
2、编译docker镜像
```
#进入项目根目录
# 编译server
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
# 编译web
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
# 编译完成后,可以使用docker-compose启动项目 ## 2、编译镜像
# docker-compose.yml你需要修改成自己编译的镜像版本
当你修改好代码后,需要编译新的镜像时,需要按照以下步骤操作:
准备好你的`你的用户名``新的版本号`
- 这个`你的用户名`是你在`docker hub`注册的用户名,例如`xiaozhi`。当然,如果你不需要推送到`docker hub`,你可以自由定义。
- 这个`新的版本号`是你编译的镜像版本,例如`1.2.3`,你可以根据需要自定义或者使用日期格式(例如`20260609`)主要是方便和现在运行的版本号做区分,同时也方便下次回忆你是什么时候构建的,不要和现在你本机运行的版本号相同。
进入`xiaozhi-esp32-server`项目根目录,编译 server 和 web 两个镜像:
```bash
cd 项目根目录
# 编译server镜像
docker build -f Dockerfile-server -t 你的用户名/xiaozhi-esp32-server:新的版本号 .
# 编译web镜像
docker build -f Dockerfile-web -t 你的用户名/xiaozhi-esp32-server-web:新的版本号 .
```
## 3、修改docker-compose配置
```bash
cd main/xiaozhi-server cd main/xiaozhi-server
docker compose up -d ```
编辑 `docker-compose_all.yml` 文件,将镜像版本替换为你刚才编译的版本:
```yaml
services:
xiaozhi-esp32-server:
image: 你的用户名/xiaozhi-esp32-server:新的版本号 # 修改为你的镜像地址
...
xiaozhi-esp32-server-web:
image: 你的用户名/xiaozhi-esp32-server-web:新的版本号 #修改为你的镜像地址
...
```
## 4、重启服务
```bash
# 停止旧容器
docker compose -f docker-compose_all.yml down
# 启动新容器
docker compose -f docker-compose_all.yml up -d
```
## 5、验证
查看日志确认服务启动正常:
```bash
# 查看server日志
docker logs -f -n 50 xiaozhi-esp32-server
# 查看web日志
docker logs -f -n 50 xiaozhi-esp32-server-web
``` ```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 108 KiB

+52 -67
View File
@@ -54,110 +54,95 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="响应速度感受" src="../images/demo9.png" /> <img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture> <picture>
<img alt="速度优化秘诀" src="../images/demo6.png" /> <img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="复杂医疗场景" src="../images/demo1.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="播放音乐" src="../images/demo7.png" /> <img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank"> <a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture> <picture>
<img alt="实时打断" src="../images/demo10.png" /> <img alt="实时打断" src="docs/images/demo10.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="拍照识物品" src="../images/demo12.png" /> <img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="../images/demo2.png" /> <img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="使用粤语交流" src="../images/demo3.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
+52 -67
View File
@@ -54,110 +54,95 @@ Want to see the usage effects? Click the videos below 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="响应速度感受" src="../images/demo9.png" /> <img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture> <picture>
<img alt="速度优化秘诀" src="../images/demo6.png" /> <img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="复杂医疗场景" src="../images/demo1.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="播放音乐" src="../images/demo7.png" /> <img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank"> <a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture> <picture>
<img alt="实时打断" src="../images/demo10.png" /> <img alt="实时打断" src="docs/images/demo10.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="拍照识物品" src="../images/demo12.png" /> <img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="../images/demo2.png" /> <img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="使用粤语交流" src="../images/demo3.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
+52 -67
View File
@@ -54,110 +54,95 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" /> <img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture> <picture>
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" /> <img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="Cenário médico complexo" src="../images/demo1.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="Reproduzir música" src="../images/demo7.png" /> <img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="Plugin de clima" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank"> <a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture> <picture>
<img alt="Interrupção em tempo real" src="../images/demo10.png" /> <img alt="实时打断" src="docs/images/demo10.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" /> <img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="Timbre de voz personalizado" src="../images/demo2.png" /> <img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="Comunicação em cantonês" src="../images/demo3.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="Transmissão de notícias" src="../images/demo0.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
+52 -67
View File
@@ -54,110 +54,95 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="响应速度感受" src="../images/demo9.png" /> <img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank"> <a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture> <picture>
<img alt="速度优化秘诀" src="../images/demo6.png" /> <img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank"> <a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture> <picture>
<img alt="复杂医疗场景" src="../images/demo1.png" /> <img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="播放音乐" src="../images/demo7.png" /> <img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank"> <a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture> <picture>
<img alt="实时打断" src="../images/demo10.png" /> <img alt="实时打断" src="docs/images/demo10.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="拍照识物品" src="../images/demo12.png" /> <img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</picture> </a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="自定义音色" src="../images/demo2.png" /> <img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank"> <a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture> <picture>
<img alt="使用粤语交流" src="../images/demo3.png" /> <img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a> </a>
</td> </td>
</tr> </tr>
+71
View File
@@ -0,0 +1,71 @@
# 联网搜索插件使用指南
## 功能简介
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持两个搜索源:秘塔(Metaso)和Tavily,用户可根据需要选择其中一个。
## API Key申请指南
目前我们适配了`秘塔搜索``Tavily搜索`
- Tavily搜索:每个月1000次免费额度。
- 秘塔搜索:拥有较为优质的国内数据源。
## API Key申请指南
### 方式一:使用秘塔搜索
- 访问 [秘塔搜索API](https://metaso.cn/search-api/api-keys),注册并登录账号
- 在API密钥管理页面,点击"创建新的Key"
- 复制生成的API Key(以 `mk-` 为前缀),这是配置所需的关键信息
### 方式二:使用Tavily搜索
- 访问 [Tavily控制台](https://app.tavily.com/home),注册并登录账号
- 在控制台中创建API Key
- 复制生成的API Key(以 `tvly-` 为前缀),这是配置所需的关键信息
## 配置方式
### 方式1. 使用智控台部署(推荐)
- 登录智控台
- 进入"配置角色"页面,选择要配置的智能体
- 点击"编辑功能"按钮,在右侧参数配置区域找到"联网搜索"插件
- 勾选"联网搜索"
- 填入搜索源(`metaso``tavily`),并将对应的`API Key`填入配置项
- 保存配置,再保存智能体配置
### 方式2. 单模块xiaozhi-server部署
`data/.config.yaml` 中配置:
- 将搜索源填入 `provider`,可选值为 `metaso``tavily`
- 将申请到的API Key填入 `api_key`
```yaml
plugins:
web_search:
provider: "metaso"
api_key: "你的API Key"
```
如需自定义返回结果数量和工具描述,可额外配置 `max_results``description`
```yaml
plugins:
web_search:
provider: "metaso"
description: "联网搜索工具。当用户明确需要联网搜索问题时使用此工具。"
max_results: 5
api_key: "你的API Key"
```
同时在 `functions` 列表中确保已启用 `web_search`
```yaml
plugins:
functions:
- web_search
```
配置完成后重启服务即可生效。
+1 -1
View File
@@ -168,7 +168,7 @@
<label for="emojiEnabled">情绪表情:</label> <label for="emojiEnabled">情绪表情:</label>
<select id="emojiEnabled" class="model-select"> <select id="emojiEnabled" class="model-select">
<option value="true">启用</option> <option value="true">启用</option>
<option value="false">禁用</option> <option value="false" selected>禁用</option>
</select> </select>
</div> </div>
</div> </div>
+19 -6
View File
@@ -20,7 +20,7 @@
<java.version>21</java.version> <java.version>21</java.version>
<junit.version>5.10.1</junit.version> <junit.version>5.10.1</junit.version>
<druid.version>1.2.20</druid.version> <druid.version>1.2.20</druid.version>
<mybatisplus.version>3.5.5</mybatisplus.version> <mybatisplus.version>3.5.17</mybatisplus.version>
<hutool.version>5.8.24</hutool.version> <hutool.version>5.8.24</hutool.version>
<jsoup.version>1.19.1</jsoup.version> <jsoup.version>1.19.1</jsoup.version>
<knife4j.version>4.6.0</knife4j.version> <knife4j.version>4.6.0</knife4j.version>
@@ -32,6 +32,8 @@
<liquibase-core.version>4.20.0</liquibase-core.version> <liquibase-core.version>4.20.0</liquibase-core.version>
<aliyun-sms-version>4.1.0</aliyun-sms-version> <aliyun-sms-version>4.1.0</aliyun-sms-version>
<okio-version>3.4.0</okio-version> <okio-version>3.4.0</okio-version>
<skipTests>true</skipTests>
<argLine></argLine>
</properties> </properties>
<dependencies> <dependencies>
@@ -175,13 +177,13 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatisplus.version}</version> <version>${mybatisplus.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mybatis</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-spring</artifactId> <artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>3.0.3</version> <version>${mybatisplus.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
@@ -271,11 +273,22 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>full</proc>
<compilerArgs>
<arg>-Xlint:deprecation,unchecked</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<skipTests>true</skipTests> <skipTests>${skipTests}</skipTests>
<argLine>@{argLine} -Xshare:off -javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar"</argLine>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
@@ -146,6 +146,11 @@ public interface Constant {
*/ */
String SERVER_AUTH_ENABLED = "server.auth.enabled"; String SERVER_AUTH_ENABLED = "server.auth.enabled";
/**
* 系统功能菜单配置
*/
String SYSTEM_WEB_MENU = "system-web.menu";
/** /**
* 无记忆 * 无记忆
*/ */
@@ -319,7 +324,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.9.3"; public static final String VERSION = "0.9.5";
/** /**
* 无效固件URL * 无效固件URL
@@ -67,7 +67,7 @@ public class DataFilterInterceptor implements InnerInterceptor {
private String getSelect(String buildSql, DataScope scope) { private String getSelect(String buildSql, DataScope scope) {
try { try {
Select select = (Select) CCJSqlParserUtil.parse(buildSql); Select select = (Select) CCJSqlParserUtil.parse(buildSql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); PlainSelect plainSelect = select.getPlainSelect();
Expression expression = plainSelect.getWhere(); Expression expression = plainSelect.getWhere();
if (expression == null) { if (expression == null) {
@@ -188,4 +188,11 @@ public class RedisKeys {
return "ota:upload:count:" + username; return "ota:upload:count:" + username;
} }
/**
* 设备通讯录缓存Key
*/
public static String getAddressBookKey() {
return "device:address_book:all";
}
} }
@@ -5,7 +5,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiFunction;
import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.Log;
@@ -81,8 +81,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
// 处理排序字段 // 处理排序字段
if (orderField instanceof String) { if (orderField instanceof String) {
orderFields.add((String) orderField); orderFields.add((String) orderField);
} else if (orderField instanceof List) { } else if (orderField instanceof List<?> fields) {
orderFields.addAll((List<String>) orderField); fields.forEach(field -> orderFields.add(String.class.cast(field)));
} }
// 有排序字段则排序 // 有排序字段则排序
@@ -142,11 +142,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return SqlHelper.retBool(result); return SqlHelper.retBool(result);
} }
protected Class<M> currentMapperClass() { protected Class<?> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0); return ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
} }
@Override @Override
@SuppressWarnings("unchecked")
public Class<T> currentModelClass() { public Class<T> currentModelClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1); return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
} }
@@ -181,8 +182,9 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
* 执行批量操作 * 执行批量操作
*/ */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) { protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiFunction<SqlSession, E, Integer> operation) {
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer); return SqlHelper.executeBatch(SqlHelper.sqlSessionFactory(this.currentModelClass()), this.log, list, batchSize,
operation);
} }
@Override @Override
@@ -209,7 +211,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return executeBatch(entityList, batchSize, (sqlSession, entity) -> { return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>(); MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, entity); param.put(Constants.ENTITY, entity);
sqlSession.update(sqlStatement, param); return sqlSession.update(sqlStatement, param);
}); });
} }
@@ -225,6 +227,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
@Override @Override
public boolean deleteBatchIds(Collection<? extends Serializable> idList) { public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
return SqlHelper.retBool(baseDao.deleteBatchIds(idList)); return SqlHelper.retBool(baseDao.deleteByIds(idList));
} }
} }
@@ -24,6 +24,7 @@ import xiaozhi.common.utils.ConvertUtils;
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
implements CrudService<T, D> { implements CrudService<T, D> {
@SuppressWarnings("unchecked")
protected Class<D> currentDtoClass() { protected Class<D> currentDtoClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2); return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
} }
@@ -70,6 +71,6 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends Bas
@Override @Override
public void delete(Serializable[] ids) { public void delete(Serializable[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids)); baseDao.deleteByIds(Arrays.asList(ids));
} }
} }
@@ -1,7 +1,9 @@
package xiaozhi.common.utils; package xiaozhi.common.utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -16,6 +18,10 @@ import cn.hutool.core.util.StrUtil;
*/ */
public class JsonUtils { public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
private static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<>() {
};
private static final TypeReference<List<Map<String, Object>>> STRING_OBJECT_MAP_LIST = new TypeReference<>() {
};
public static String toJsonString(Object object) { public static String toJsonString(Object object) {
try { try {
@@ -67,4 +73,59 @@ public class JsonUtils {
} }
} }
public static Map<String, Object> parseMap(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
return parseObject(text, STRING_OBJECT_MAP);
}
public static List<Map<String, Object>> parseMapList(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
return parseObject(text, STRING_OBJECT_MAP_LIST);
}
public static Map<String, Object> toStringObjectMap(Object value) {
if (value == null) {
return null;
}
if (!(value instanceof Map<?, ?> map)) {
throw new ClassCastException("Expected Map but got " + value.getClass().getName());
}
Map<String, Object> result = new LinkedHashMap<>(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
result.put(String.class.cast(entry.getKey()), entry.getValue());
}
return result;
}
public static List<Map<String, Object>> toStringObjectMapList(Object value) {
if (value == null) {
return null;
}
List<?> list = List.class.cast(value);
List<Map<String, Object>> result = new ArrayList<>(list.size());
for (Object item : list) {
result.add(toStringObjectMap(item));
}
return result;
}
public static <T> List<T> toList(Object value, Class<T> elementType) {
if (value == null) {
return null;
}
List<?> list = List.class.cast(value);
List<T> result = new ArrayList<>(list.size());
for (Object item : list) {
result.add(elementType.cast(item));
}
return result;
}
} }
@@ -75,11 +75,11 @@ public class SensitiveDataUtils {
Object value = jsonObject.get(key); Object value = jsonObject.get(key);
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) { if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
result.put(key, maskMiddle((String) value)); result.set(key, maskMiddle((String) value));
} else if (value instanceof JSONObject) { } else if (value instanceof JSONObject) {
result.put(key, maskSensitiveFields((JSONObject) value)); result.set(key, maskSensitiveFields((JSONObject) value));
} else { } else {
result.put(key, value); result.set(key, value);
} }
} }
@@ -22,10 +22,10 @@ public class SqlFilter {
return null; return null;
} }
// 去掉'|"|;|\字符 // 去掉'|"|;|\字符
str = StringUtils.replace(str, "'", ""); str = str.replace("'", "");
str = StringUtils.replace(str, "\"", ""); str = str.replace("\"", "");
str = StringUtils.replace(str, ";", ""); str = str.replace(";", "");
str = StringUtils.replace(str, "\\", ""); str = str.replace("\\", "");
// 转换成小写 // 转换成小写
str = str.toLowerCase(); str = str.toLowerCase();
@@ -0,0 +1,103 @@
package xiaozhi.modules.agent.Enums;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import lombok.Getter;
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
@Getter
public enum AgentSnapshotField {
AGENT_CODE("agentCode", AgentSnapshotDataDTO::getAgentCode, AgentUpdateDTO::getAgentCode,
(agent, data) -> agent.setAgentCode(data.getAgentCode())),
AGENT_NAME("agentName", AgentSnapshotDataDTO::getAgentName, AgentUpdateDTO::getAgentName,
(agent, data) -> agent.setAgentName(data.getAgentName())),
ASR_MODEL_ID("asrModelId", AgentSnapshotDataDTO::getAsrModelId, AgentUpdateDTO::getAsrModelId,
(agent, data) -> agent.setAsrModelId(data.getAsrModelId())),
VAD_MODEL_ID("vadModelId", AgentSnapshotDataDTO::getVadModelId, AgentUpdateDTO::getVadModelId,
(agent, data) -> agent.setVadModelId(data.getVadModelId())),
LLM_MODEL_ID("llmModelId", AgentSnapshotDataDTO::getLlmModelId, AgentUpdateDTO::getLlmModelId,
(agent, data) -> agent.setLlmModelId(data.getLlmModelId())),
SLM_MODEL_ID("slmModelId", AgentSnapshotDataDTO::getSlmModelId, AgentUpdateDTO::getSlmModelId,
(agent, data) -> agent.setSlmModelId(data.getSlmModelId())),
VLLM_MODEL_ID("vllmModelId", AgentSnapshotDataDTO::getVllmModelId, AgentUpdateDTO::getVllmModelId,
(agent, data) -> agent.setVllmModelId(data.getVllmModelId())),
TTS_MODEL_ID("ttsModelId", AgentSnapshotDataDTO::getTtsModelId, AgentUpdateDTO::getTtsModelId,
(agent, data) -> agent.setTtsModelId(data.getTtsModelId())),
TTS_VOICE_ID("ttsVoiceId", AgentSnapshotDataDTO::getTtsVoiceId, AgentUpdateDTO::getTtsVoiceId,
(agent, data) -> agent.setTtsVoiceId(data.getTtsVoiceId())),
TTS_LANGUAGE("ttsLanguage", AgentSnapshotDataDTO::getTtsLanguage, AgentUpdateDTO::getTtsLanguage,
(agent, data) -> agent.setTtsLanguage(data.getTtsLanguage())),
TTS_VOLUME("ttsVolume", AgentSnapshotDataDTO::getTtsVolume, AgentUpdateDTO::getTtsVolume,
(agent, data) -> agent.setTtsVolume(data.getTtsVolume())),
TTS_RATE("ttsRate", AgentSnapshotDataDTO::getTtsRate, AgentUpdateDTO::getTtsRate,
(agent, data) -> agent.setTtsRate(data.getTtsRate())),
TTS_PITCH("ttsPitch", AgentSnapshotDataDTO::getTtsPitch, AgentUpdateDTO::getTtsPitch,
(agent, data) -> agent.setTtsPitch(data.getTtsPitch())),
MEM_MODEL_ID("memModelId", AgentSnapshotDataDTO::getMemModelId, AgentUpdateDTO::getMemModelId,
(agent, data) -> agent.setMemModelId(data.getMemModelId())),
INTENT_MODEL_ID("intentModelId", AgentSnapshotDataDTO::getIntentModelId, AgentUpdateDTO::getIntentModelId,
(agent, data) -> agent.setIntentModelId(data.getIntentModelId())),
CHAT_HISTORY_CONF("chatHistoryConf", AgentSnapshotDataDTO::getChatHistoryConf, AgentUpdateDTO::getChatHistoryConf,
(agent, data) -> agent.setChatHistoryConf(data.getChatHistoryConf())),
SYSTEM_PROMPT("systemPrompt", AgentSnapshotDataDTO::getSystemPrompt, AgentUpdateDTO::getSystemPrompt,
(agent, data) -> agent.setSystemPrompt(data.getSystemPrompt())),
SUMMARY_MEMORY("summaryMemory", AgentSnapshotDataDTO::getSummaryMemory, AgentUpdateDTO::getSummaryMemory,
(agent, data) -> agent.setSummaryMemory(data.getSummaryMemory())),
LANG_CODE("langCode", AgentSnapshotDataDTO::getLangCode, AgentUpdateDTO::getLangCode,
(agent, data) -> agent.setLangCode(data.getLangCode())),
LANGUAGE("language", AgentSnapshotDataDTO::getLanguage, AgentUpdateDTO::getLanguage,
(agent, data) -> agent.setLanguage(data.getLanguage())),
SORT("sort", AgentSnapshotDataDTO::getSort, AgentUpdateDTO::getSort,
(agent, data) -> agent.setSort(data.getSort())),
FUNCTIONS("functions", AgentSnapshotDataDTO::getFunctions, AgentUpdateDTO::getFunctions, null),
CONTEXT_PROVIDERS("contextProviders", AgentSnapshotDataDTO::getContextProviders,
AgentUpdateDTO::getContextProviders, null),
CORRECT_WORD_FILE_IDS("correctWordFileIds", AgentSnapshotDataDTO::getCorrectWordFileIds,
AgentUpdateDTO::getCorrectWordFileIds, null),
TAG_NAMES("tagNames", AgentSnapshotDataDTO::getTagNames, AgentUpdateDTO::getTagNames, null);
private final String fieldName;
private final Function<AgentSnapshotDataDTO, Object> snapshotGetter;
private final Function<AgentUpdateDTO, Object> updateGetter;
private final BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier;
AgentSnapshotField(String fieldName, Function<AgentSnapshotDataDTO, Object> snapshotGetter,
Function<AgentUpdateDTO, Object> updateGetter,
BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier) {
this.fieldName = fieldName;
this.snapshotGetter = snapshotGetter;
this.updateGetter = updateGetter;
this.restoreApplier = restoreApplier;
}
public static List<String> names() {
return Arrays.stream(values()).map(AgentSnapshotField::getFieldName).toList();
}
public static String canonicalName(String fieldName) {
return "tags".equals(fieldName) ? TAG_NAMES.getFieldName() : fieldName;
}
public Object snapshotValue(AgentSnapshotDataDTO data) {
return data == null ? null : snapshotGetter.apply(data);
}
public Object updateValue(AgentUpdateDTO data) {
return data == null ? null : updateGetter.apply(data);
}
public boolean isRestorableAgentField() {
return restoreApplier != null;
}
public void applyTo(AgentEntity agent, AgentSnapshotDataDTO data) {
if (restoreApplier != null) {
restoreApplier.accept(agent, data);
}
}
}
@@ -28,10 +28,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils; import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -48,15 +51,10 @@ import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentChatSummaryService; import xiaozhi.modules.agent.service.AgentChatSummaryService;
import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO; import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体管理") @Tag(name = "智能体管理")
@@ -64,17 +62,39 @@ import xiaozhi.modules.security.user.SecurityUser;
@RestController @RestController
@RequestMapping("/agent") @RequestMapping("/agent")
public class AgentController { public class AgentController {
private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L;
private final AgentService agentService; private final AgentService agentService;
private final AgentTemplateService agentTemplateService; private final AgentTemplateService agentTemplateService;
private final DeviceService deviceService;
private final AgentChatHistoryService agentChatHistoryService; private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService; private final AgentChatAudioService agentChatAudioService;
private final AgentPluginMappingService agentPluginMappingService;
private final AgentContextProviderService agentContextProviderService;
private final AgentChatSummaryService agentChatSummaryService; private final AgentChatSummaryService agentChatSummaryService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final AgentTagService agentTagService; private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
private void requireAgentPermission(String agentId) {
if (!agentService.checkAgentPermission(agentId, SecurityUser.getUserId())) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
private String requireSessionAgent(String sessionId) {
String agentId = agentChatHistoryService.getAgentIdBySessionId(sessionId);
if (StringUtils.isBlank(agentId)) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
agentService.getAgentById(agentId);
return agentId;
}
private String requireAudioPermission(String audioId) {
String agentId = agentChatHistoryService.getAgentIdByAudioId(audioId);
if (StringUtils.isBlank(agentId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
requireAgentPermission(agentId);
return agentId;
}
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "获取用户智能体列表") @Operation(summary = "获取用户智能体列表")
@@ -106,7 +126,7 @@ public class AgentController {
@Operation(summary = "获取智能体详情") @Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) { public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
AgentInfoVO agent = agentService.getAgentById(id); AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId());
return ResultUtils.success(agent); return ResultUtils.success(agent);
} }
@@ -120,20 +140,16 @@ public class AgentController {
@PutMapping("/saveMemory/{macAddress}") @PutMapping("/saveMemory/{macAddress}")
@Operation(summary = "根据设备id更新智能体") @Operation(summary = "根据设备id更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) { public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId());
if (device == null) { return new Result<Void>().ok(null);
return new Result<>();
}
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
return new Result<>();
} }
@PostMapping("/chat-summary/{sessionId}/save") @PostMapping("/chat-summary/{sessionId}/save")
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)") @Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) { public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
requireSessionAgent(sessionId);
try { try {
// 异步执行总结生成任务,立即返回成功响应 // 异步执行总结生成任务,立即返回成功响应
new Thread(() -> { new Thread(() -> {
@@ -155,6 +171,7 @@ public class AgentController {
@PostMapping("/chat-title/{sessionId}/generate") @PostMapping("/chat-title/{sessionId}/generate")
@Operation(summary = "根据会话ID生成聊天标题") @Operation(summary = "根据会话ID生成聊天标题")
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) { public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
requireSessionAgent(sessionId);
agentChatSummaryService.generateAndSaveChatTitle(sessionId); agentChatSummaryService.generateAndSaveChatTitle(sessionId);
return new Result<Void>().ok(null); return new Result<Void>().ok(null);
} }
@@ -163,7 +180,7 @@ public class AgentController {
@Operation(summary = "更新智能体") @Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) { public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
agentService.updateAgentById(id, dto); agentService.updateAgentById(id, dto, SecurityUser.getUserId());
return new Result<>(); return new Result<>();
} }
@@ -171,18 +188,7 @@ public class AgentController {
@Operation(summary = "删除智能体") @Operation(summary = "删除智能体")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<Void> delete(@PathVariable String id) { public Result<Void> delete(@PathVariable String id) {
// 先删除关联的设备 agentService.deleteAgentById(id, SecurityUser.getUserId());
deviceService.deleteByAgentId(id);
// 删除关联的聊天记录
agentChatHistoryService.deleteByAgentId(id, true, true);
// 删除关联的插件
agentPluginMappingService.deleteByAgentId(id);
// 删除关联的上下文源配置
agentContextProviderService.deleteByAgentId(id);
// 删除关联的替换词文件关联记录
correctWordFileService.deleteMappingsByAgentId(id);
// 再删除智能体
agentService.deleteById(id);
return new Result<>(); return new Result<>();
} }
@@ -205,6 +211,7 @@ public class AgentController {
public Result<PageData<AgentChatSessionDTO>> getAgentSessions( public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
@PathVariable("id") String id, @PathVariable("id") String id,
@Parameter(hidden = true) @RequestParam Map<String, Object> params) { @Parameter(hidden = true) @RequestParam Map<String, Object> params) {
requireAgentPermission(id);
params.put("agentId", id); params.put("agentId", id);
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params); PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
return new Result<PageData<AgentChatSessionDTO>>().ok(page); return new Result<PageData<AgentChatSessionDTO>>().ok(page);
@@ -252,6 +259,7 @@ public class AgentController {
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<String> getContentByAudioId( public Result<String> getContentByAudioId(
@PathVariable("id") String id) { @PathVariable("id") String id) {
requireAudioPermission(id);
// 查询聊天记录 // 查询聊天记录
String data = agentChatHistoryService.getContentByAudioId(id); String data = agentChatHistoryService.getContentByAudioId(id);
return new Result<String>().ok(data); return new Result<String>().ok(data);
@@ -261,12 +269,13 @@ public class AgentController {
@Operation(summary = "获取音频下载ID") @Operation(summary = "获取音频下载ID")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<String> getAudioId(@PathVariable("audioId") String audioId) { public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
requireAudioPermission(audioId);
byte[] audioData = agentChatAudioService.getAudio(audioId); byte[] audioData = agentChatAudioService.getAudio(audioId);
if (audioData == null) { if (audioData == null) {
return new Result<String>().error("音频不存在"); return new Result<String>().error("音频不存在");
} }
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId); redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId, AUDIO_PLAY_TOKEN_EXPIRE_SECONDS);
return new Result<String>().ok(uuid); return new Result<String>().ok(uuid);
} }
@@ -322,6 +331,7 @@ public class AgentController {
@Operation(summary = "获取智能体的标签") @Operation(summary = "获取智能体的标签")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) { public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
requireAgentPermission(id);
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id); List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
return new Result<List<AgentTagDTO>>().ok(tags); return new Result<List<AgentTagDTO>>().ok(tags);
} }
@@ -330,9 +340,13 @@ public class AgentController {
@Operation(summary = "保存智能体的标签") @Operation(summary = "保存智能体的标签")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) { public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
List<String> tagIds = (List<String>) params.get("tagIds"); requireAgentPermission(id);
List<String> tagNames = (List<String>) params.get("tagNames"); List<String> tagIds = JsonUtils.toList(params.get("tagIds"), String.class);
agentTagService.saveAgentTags(id, tagIds, tagNames); List<String> tagNames = JsonUtils.toList(params.get("tagNames"), String.class);
AgentUpdateDTO dto = new AgentUpdateDTO();
dto.setTagIds(tagIds);
dto.setTagNames(tagNames);
agentService.updateAgentById(id, dto);
return new Result<Void>().ok(null); return new Result<Void>().ok(null);
} }
@@ -0,0 +1,79 @@
package xiaozhi.modules.agent.controller;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
import xiaozhi.modules.agent.dto.AgentSnapshotRestoreDTO;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentSnapshotService;
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体快照")
@AllArgsConstructor
@RestController
@RequestMapping("/agent/{agentId}/snapshots")
public class AgentSnapshotController {
private final AgentSnapshotService agentSnapshotService;
private final AgentService agentService;
@GetMapping
@Operation(summary = "获取智能体快照列表")
@RequiresPermissions("sys:role:normal")
public Result<PageData<AgentSnapshotVO>> page(
@PathVariable String agentId,
@ParameterObject AgentSnapshotPageDTO params) {
checkPermission(agentId);
return new Result<PageData<AgentSnapshotVO>>().ok(agentSnapshotService.page(agentId, params));
}
@GetMapping("/{snapshotId}")
@Operation(summary = "获取智能体快照详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentSnapshotVO> getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
checkPermission(agentId);
return new Result<AgentSnapshotVO>().ok(agentSnapshotService.getSnapshot(agentId, snapshotId));
}
@PostMapping("/{snapshotId}/restore")
@Operation(summary = "恢复智能体快照")
@RequiresPermissions("sys:role:normal")
public Result<Void> restore(@PathVariable String agentId, @PathVariable String snapshotId,
@RequestBody @Valid AgentSnapshotRestoreDTO request) {
checkPermission(agentId);
agentSnapshotService.restoreSnapshot(agentId, snapshotId, request.getCurrentStateToken());
return new Result<>();
}
@DeleteMapping("/{snapshotId}")
@Operation(summary = "删除智能体历史快照")
@RequiresPermissions("sys:role:normal")
public Result<Void> deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
checkPermission(agentId);
agentSnapshotService.deleteSnapshot(agentId, snapshotId);
return new Result<>();
}
private void checkPermission(String agentId) {
UserDetail user = SecurityUser.getUser();
if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) {
throw new RenException("没有权限访问该智能体快照");
}
}
}
@@ -36,4 +36,20 @@ public interface AgentDao extends BaseDao<AgentEntity> {
* @param agentId 智能体ID * @param agentId 智能体ID
*/ */
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId); AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
/**
* 锁定智能体主记录,用于串行化同一智能体的配置写入
*
* @param agentId 智能体ID
*/
AgentEntity selectByIdForUpdate(@Param("agentId") String agentId);
/**
* 精确写入快照覆盖的智能体字段,包括目标快照中的 null 值。
* 不更新所属用户、创建信息等不属于快照的字段。
*
* @param agent 已应用目标快照的智能体
* @return 受影响行数
*/
int updateSnapshotFields(@Param("agent") AgentEntity agent);
} }
@@ -0,0 +1,29 @@
package xiaozhi.modules.agent.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
@Mapper
public interface AgentSnapshotDao extends BaseDao<AgentSnapshotEntity> {
Integer selectMaxVersionNo(@Param("agentId") String agentId);
AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId);
AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo);
int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot);
int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit);
List<AgentSnapshotEntity> selectLegacyRedactionBatch(@Param("afterId") String afterId,
@Param("limit") int limit,
@Param("targetRedactionVersion") int targetRedactionVersion);
int updateRedactedSnapshots(@Param("snapshots") List<AgentSnapshotEntity> snapshots,
@Param("redactionVersion") int redactionVersion);
}
@@ -39,7 +39,7 @@ public class AgentDTO {
private String systemPrompt; private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false) "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String summaryMemory; private String summaryMemory;
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00") @Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
@@ -14,6 +14,6 @@ public class AgentMemoryDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false) "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String summaryMemory; private String summaryMemory;
} }
@@ -0,0 +1,40 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照数据")
public class AgentSnapshotDataDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String agentCode;
private String agentName;
private String asrModelId;
private String vadModelId;
private String llmModelId;
private String slmModelId;
private String vllmModelId;
private String ttsModelId;
private String ttsVoiceId;
private String ttsLanguage;
private Integer ttsVolume;
private Integer ttsRate;
private Integer ttsPitch;
private String memModelId;
private String intentModelId;
private Integer chatHistoryConf;
private String systemPrompt;
private String summaryMemory;
private String langCode;
private String language;
private Integer sort;
private List<AgentUpdateDTO.FunctionInfo> functions;
private List<ContextProviderDTO> contextProviders;
private List<String> correctWordFileIds;
private List<String> tagNames;
private List<AgentSnapshotTagDTO> tags;
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照分页查询参数")
public class AgentSnapshotPageDTO {
@Schema(description = "当前页码,从1开始", example = "1")
private Integer page = 1;
@Schema(description = "每页数量", example = "10")
private Integer limit = 10;
@Schema(description = "版本锚点,只查询小于等于该版本号的历史快照", example = "20")
private Integer maxVersionNo;
public int pageOrDefault() {
return page == null || page < 1 ? 1 : page;
}
public int limitOrDefault() {
if (limit == null || limit < 1) {
return 10;
}
return limit;
}
}
@@ -0,0 +1,13 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "智能体快照恢复请求")
public class AgentSnapshotRestoreDTO {
@NotBlank
@Schema(description = "预览时由服务端生成的当前配置状态指纹")
private String currentStateToken;
}
@@ -0,0 +1,16 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照标签")
public class AgentSnapshotTagDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String tagName;
private Integer sort;
}
@@ -4,9 +4,12 @@ import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import xiaozhi.common.utils.JsonUtils;
/** /**
* 智能体更新DTO * 智能体更新DTO
@@ -36,10 +39,10 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true) @Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
private String slmModelId; private String slmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false) @Schema(description = "VLLM模型标识", example = "vllm_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String vllmModelId; private String vllmModelId;
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false) @Schema(description = "语音合成模型标识", example = "tts_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String ttsModelId; private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", nullable = true) @Schema(description = "音色标识", example = "voice_02", nullable = true)
@@ -91,14 +94,49 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "替换词文件ID列表", nullable = true) @Schema(description = "替换词文件ID列表", nullable = true)
private List<String> correctWordFileIds; private List<String> correctWordFileIds;
@Schema(description = "标签名称列表", nullable = true)
private List<String> tagNames;
@Schema(description = "标签ID列表", nullable = true)
private List<String> tagIds;
@Data @Data
@Schema(description = "插件函数信息") @Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable { public static class FunctionInfo implements Serializable {
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
};
@Schema(description = "插件ID", example = "plugin_01") @Schema(description = "插件ID", example = "plugin_01")
private String pluginId; private String pluginId;
@Schema(description = "函数参数信息", nullable = true) @Schema(description = "函数参数信息", nullable = true)
private HashMap<String, Object> paramInfo; private HashMap<String, Object> paramInfo = new HashMap<>();
public void setParamInfo(Object paramInfo) {
this.paramInfo = normalizeParamInfo(paramInfo);
}
private static HashMap<String, Object> normalizeParamInfo(Object paramInfo) {
if (paramInfo == null) {
return new HashMap<>();
}
if (paramInfo instanceof String value) {
if (value.trim().isEmpty()) {
return new HashMap<>();
}
return JsonUtils.parseObject(value, PARAM_INFO_TYPE);
}
if (paramInfo instanceof Map<?, ?> value) {
HashMap<String, Object> normalized = new HashMap<>();
value.forEach((key, val) -> {
if (key != null) {
normalized.put(String.valueOf(key), val);
}
});
return normalized;
}
return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE);
}
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
} }
@@ -1,48 +0,0 @@
package xiaozhi.modules.agent.dto;
import lombok.Data;
/**
* MCP JSON-RPC 响应 DTO
*/
@Data
public class McpJsonRpcResponse {
private String jsonrpc = "2.0";
private Integer id;
private McpResult result;
private McpError error;
public McpJsonRpcResponse() {
}
@Data
public static class McpResult {
private String type;
private String message;
private String agent_id;
private McpTool[] tools;
public McpResult() {
}
}
@Data
public static class McpTool {
private String name;
private String description;
private Object inputSchema;
public McpTool() {
}
}
@Data
public static class McpError {
private Integer code;
private String message;
private Object data;
public McpError() {
}
}
}
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import xiaozhi.modules.agent.dto.ContextProviderDTO; import xiaozhi.modules.agent.dto.ContextProviderDTO;
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
@Data @Data
@TableName(value = "ai_agent_context_provider", autoResultMap = true) @TableName(value = "ai_agent_context_provider", autoResultMap = true)
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
private String agentId; private String agentId;
@Schema(description = "上下文源配置") @Schema(description = "上下文源配置")
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = ContextProviderListTypeHandler.class)
private List<ContextProviderDTO> contextProviders; private List<ContextProviderDTO> contextProviders;
@Schema(description = "创建者") @Schema(description = "创建者")
@@ -74,7 +74,7 @@ public class AgentEntity {
private String systemPrompt; private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false) "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String summaryMemory; private String summaryMemory;
@Schema(description = "语言编码") @Schema(description = "语言编码")
@@ -0,0 +1,53 @@
package xiaozhi.modules.agent.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_agent_snapshot")
@Schema(description = "智能体配置快照")
public class AgentSnapshotEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "快照ID")
private String id;
@Schema(description = "智能体ID")
private String agentId;
@Schema(description = "所属用户ID")
private Long userId;
@Schema(description = "版本号")
private Integer versionNo;
@Schema(description = "快照数据JSON")
private String snapshotData;
@Schema(description = "变更字段JSON")
private String changedFields;
@Schema(description = "快照来源")
private String source;
@Schema(description = "恢复来源快照ID")
private String restoreFromSnapshotId;
@Schema(description = "恢复来源版本号")
private Integer restoreFromVersionNo;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "快照数据脱敏规则版本")
private Integer redactionVersion;
}
@@ -1,6 +1,6 @@
package xiaozhi.modules.agent.service; package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity; import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
@@ -11,7 +11,7 @@ import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
* @version 1.0, 2025/5/8 * @version 1.0, 2025/5/8
* @since 1.0.0 * @since 1.0.0
*/ */
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> { public interface AgentChatAudioService extends IRepository<AgentChatAudioEntity> {
/** /**
* 保存音频数据 * 保存音频数据
* *
@@ -3,7 +3,7 @@ package xiaozhi.modules.agent.service;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -18,7 +18,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
* @version 1.0, 2025/4/30 * @version 1.0, 2025/4/30
* @since 1.0.0 * @since 1.0.0
*/ */
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> { public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEntity> {
/** /**
* 根据智能体ID获取会话列表 * 根据智能体ID获取会话列表
@@ -37,6 +37,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
*/ */
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId); List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
/**
* 根据会话ID获取智能体ID
*
* @param sessionId 会话ID
* @return 智能体ID
*/
String getAgentIdBySessionId(String sessionId);
/** /**
* 根据智能体ID删除聊天记录 * 根据智能体ID删除聊天记录
* *
@@ -62,6 +70,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
*/ */
String getContentByAudioId(String audioId); String getContentByAudioId(String audioId);
/**
* 根据音频ID获取智能体ID
*
* @param audioId 音频ID
* @return 智能体ID
*/
String getAgentIdByAudioId(String audioId);
/** /**
* 查询此音频id是否属于此智能体 * 查询此音频id是否属于此智能体
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service;
import java.util.List; import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.entity.AgentPluginMapping;
@@ -10,7 +10,7 @@ import xiaozhi.modules.agent.entity.AgentPluginMapping;
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service * @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
* @createDate 2025-05-25 22:33:17 * @createDate 2025-05-25 22:33:17
*/ */
public interface AgentPluginMappingService extends IService<AgentPluginMapping> { public interface AgentPluginMappingService extends IRepository<AgentPluginMapping> {
/** /**
* 根据智能体id获取插件参数 * 根据智能体id获取插件参数
@@ -26,4 +26,11 @@ public interface AgentPluginMappingService extends IService<AgentPluginMapping>
* @param agentId * @param agentId
*/ */
void deleteByAgentId(String agentId); void deleteByAgentId(String agentId);
/**
* 根据插件ID删除所有智能体的插件映射
*
* @param pluginId 插件ID
*/
void deleteByPluginId(String pluginId);
} }
@@ -7,6 +7,7 @@ import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.agent.vo.AgentInfoVO;
@@ -35,6 +36,15 @@ public interface AgentService extends BaseService<AgentEntity> {
*/ */
AgentInfoVO getAgentById(String id); AgentInfoVO getAgentById(String id);
/**
* 根据ID获取当前用户有权访问的智能体
*
* @param id 智能体ID
* @param userId 当前用户ID
* @return 智能体实体
*/
AgentInfoVO getAgentById(String id, Long userId);
/** /**
* 插入智能体 * 插入智能体
* *
@@ -50,6 +60,13 @@ public interface AgentService extends BaseService<AgentEntity> {
*/ */
void deleteAgentByUserId(Long userId); void deleteAgentByUserId(Long userId);
/**
* 删除智能体及其关联数据
*
* @param agentId 智能体ID
*/
void deleteAgent(String agentId);
/** /**
* 获取用户智能体列表 * 获取用户智能体列表
* *
@@ -93,6 +110,41 @@ public interface AgentService extends BaseService<AgentEntity> {
*/ */
void updateAgentById(String agentId, AgentUpdateDTO dto); void updateAgentById(String agentId, AgentUpdateDTO dto);
/**
* 更新当前用户有权访问的智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
* @param userId 当前用户ID
*/
void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId);
/**
* 根据设备MAC地址更新当前用户有权访问的智能体记忆
*
* @param macAddress 设备MAC地址
* @param dto 智能体记忆
* @param userId 当前用户ID
*/
void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId);
/**
* 删除当前用户有权访问的智能体
*
* @param agentId 智能体ID
* @param userId 当前用户ID
*/
void deleteAgentById(String agentId, Long userId);
/**
* 更新智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
* @param createSnapshot 是否创建配置快照
*/
void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot);
/** /**
* 创建智能体 * 创建智能体
* *
@@ -0,0 +1,25 @@
package xiaozhi.modules.agent.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
public interface AgentSnapshotService extends BaseService<AgentSnapshotEntity> {
void createSnapshot(String agentId, String source);
PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params);
AgentSnapshotVO getSnapshot(String agentId, String snapshotId);
void restoreSnapshot(String agentId, String snapshotId, String currentStateToken);
void deleteSnapshot(String agentId, String snapshotId);
Integer getCurrentVersionNo(String agentId);
void deleteByAgentId(String agentId);
long redactLegacySnapshots();
}
@@ -1,6 +1,6 @@
package xiaozhi.modules.agent.service; package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
@@ -9,7 +9,7 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service * @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
* @createDate 2025-03-22 11:48:18 * @createDate 2025-03-22 11:48:18
*/ */
public interface AgentTemplateService extends IService<AgentTemplateEntity> { public interface AgentTemplateService extends IRepository<AgentTemplateEntity> {
/** /**
* 获取默认模板 * 获取默认模板
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service.impl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao; import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity; import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
@@ -16,7 +16,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
* @since 1.0.0 * @since 1.0.0
*/ */
@Service @Service
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity> public class AgentChatAudioServiceImpl extends CrudRepository<AiAgentChatAudioDao, AgentChatAudioEntity>
implements AgentChatAudioService { implements AgentChatAudioService {
@Override @Override
public String saveAudio(byte[] audioData) { public String saveAudio(byte[] audioData) {
@@ -14,7 +14,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
@@ -39,7 +39,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity> public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHistoryDao, AgentChatHistoryEntity>
implements AgentChatHistoryService { implements AgentChatHistoryService {
private final AgentChatTitleService agentChatTitleService; private final AgentChatTitleService agentChatTitleService;
@@ -88,6 +88,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class); return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
} }
@Override
public String getAgentIdBySessionId(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return null;
}
AgentChatHistoryEntity entity = baseMapper.selectOne(
new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getAgentId)
.eq(AgentChatHistoryEntity::getSessionId, sessionId)
.last("LIMIT 1"));
return entity == null ? null : entity.getAgentId();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) { public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
@@ -154,7 +167,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
// 尝试解析为 JSON // 尝试解析为 JSON
try { try {
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class); Map<String, Object> jsonMap = JsonUtils.parseMap(content);
if (jsonMap != null && jsonMap.containsKey("content")) { if (jsonMap != null && jsonMap.containsKey("content")) {
Object contentObj = jsonMap.get("content"); Object contentObj = jsonMap.get("content");
return contentObj != null ? contentObj.toString() : content; return contentObj != null ? contentObj.toString() : content;
@@ -176,6 +189,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent(); return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
} }
@Override
public String getAgentIdByAudioId(String audioId) {
if (audioId == null || audioId.isBlank()) {
return null;
}
AgentChatHistoryEntity entity = baseMapper.selectOne(
new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getAgentId)
.eq(AgentChatHistoryEntity::getAudioId, audioId)
.last("LIMIT 1"));
return entity == null ? null : entity.getAgentId();
}
@Override @Override
public boolean isAudioOwnedByAgent(String audioId, String agentId) { public boolean isAudioOwnedByAgent(String audioId, String agentId) {
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体 // 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
{ {
setSummaryMemory(summaryDTO.getSummary()); setSummaryMemory(summaryDTO.getSummary());
} }
}); }, false);
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId); log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
} else { } else {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage()); log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
@@ -76,7 +76,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动 // 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
List<String> initResponses = client.listenerWithoutClose(response -> { List<String> initResponses = client.listenerWithoutClose(response -> {
try { try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class); Map<String, Object> jsonMap = JsonUtils.parseMap(response);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) { if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
// 检查是否有result字段,表示初始化成功 // 检查是否有result字段,表示初始化成功
return jsonMap.containsKey("result") && !jsonMap.containsKey("error"); return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
@@ -92,7 +92,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
boolean initSucceeded = false; boolean initSucceeded = false;
for (String response : initResponses) { for (String response : initResponses) {
try { try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class); Map<String, Object> jsonMap = JsonUtils.parseMap(response);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) { if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
if (jsonMap.containsKey("result")) { if (jsonMap.containsKey("result")) {
log.info("MCP初始化成功,智能体ID: {}", id); log.info("MCP初始化成功,智能体ID: {}", id);
@@ -123,7 +123,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 等待工具列表响应 (id=2) // 等待工具列表响应 (id=2)
List<String> toolsResponses = client.listener(response -> { List<String> toolsResponses = client.listener(response -> {
try { try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class); Map<String, Object> jsonMap = JsonUtils.parseMap(response);
return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id")); return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"));
} catch (Exception e) { } catch (Exception e) {
log.warn("解析工具列表响应失败: {}", response, e); log.warn("解析工具列表响应失败: {}", response, e);
@@ -134,18 +134,18 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 处理工具列表响应 // 处理工具列表响应
for (String response : toolsResponses) { for (String response : toolsResponses) {
try { try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class); Map<String, Object> jsonMap = JsonUtils.parseMap(response);
if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) { if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) {
// 检查是否有result字段 // 检查是否有result字段
Object resultObj = jsonMap.get("result"); Object resultObj = jsonMap.get("result");
if (resultObj instanceof Map) { if (resultObj instanceof Map<?, ?>) {
Map<String, Object> resultMap = (Map<String, Object>) resultObj; Map<String, Object> resultMap = JsonUtils.toStringObjectMap(resultObj);
Object toolsObj = resultMap.get("tools"); Object toolsObj = resultMap.get("tools");
if (toolsObj instanceof List) { if (toolsObj instanceof List<?>) {
List<Map<String, Object>> toolsList = (List<Map<String, Object>>) toolsObj; List<Map<String, Object>> toolsList = JsonUtils.toStringObjectMapList(toolsObj);
// 提取工具名称列表 // 提取工具名称列表
List<String> result = toolsList.stream() List<String> result = toolsList.stream()
.map(tool -> (String) tool.get("name")) .map(tool -> String.class.cast(tool.get("name")))
.filter(name -> name != null) .filter(name -> name != null)
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -29,7 +29,7 @@ import xiaozhi.modules.model.service.ModelConfigService;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping> public class AgentPluginMappingServiceImpl extends CrudRepository<AgentPluginMappingMapper, AgentPluginMapping>
implements AgentPluginMappingService { implements AgentPluginMappingService {
private final AgentPluginMappingMapper agentPluginMappingMapper; private final AgentPluginMappingMapper agentPluginMappingMapper;
private final KnowledgeBaseService knowledgeBaseService; private final KnowledgeBaseService knowledgeBaseService;
@@ -104,4 +104,11 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappin
agentPluginMappingMapper.delete(updateWrapper); agentPluginMappingMapper.delete(updateWrapper);
} }
@Override
public void deleteByPluginId(String pluginId) {
UpdateWrapper<AgentPluginMapping> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("plugin_id", pluginId);
agentPluginMappingMapper.delete(updateWrapper);
}
} }
@@ -15,8 +15,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
@@ -34,6 +34,7 @@ import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dao.AgentTagDao; import xiaozhi.modules.agent.dao.AgentTagDao;
import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentTagDTO; import xiaozhi.modules.agent.dto.AgentTagDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentContextProviderEntity; import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
@@ -45,6 +46,7 @@ import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentContextProviderService; import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentSnapshotService;
import xiaozhi.modules.agent.service.AgentTagService; import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.agent.vo.AgentInfoVO;
@@ -76,6 +78,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final AgentContextProviderService agentContextProviderService; private final AgentContextProviderService agentContextProviderService;
private final AgentTagService agentTagService; private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService; private final CorrectWordFileService correctWordFileService;
private final AgentSnapshotService agentSnapshotService;
@Override @Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) { public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -92,6 +95,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
if (agent == null) { if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND); throw new RenException(ErrorCode.AGENT_NOT_FOUND);
} }
requireCurrentUserPermissionIfPresent(agent);
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
@@ -109,11 +113,71 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 查询替换词文件ID列表 // 查询替换词文件ID列表
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id); List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
agent.setCorrectWordFileIds(correctWordFileIds); agent.setCorrectWordFileIds(correctWordFileIds);
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
// 无需额外查询插件列表,已通过SQL查询出来 // 无需额外查询插件列表,已通过SQL查询出来
return agent; return agent;
} }
@Override
public AgentInfoVO getAgentById(String id, Long userId) {
AgentInfoVO agent = getAgentById(id);
requireAgentPermission(agent, userId);
return agent;
}
private AgentEntity getAgentEntityOrThrow(String agentId) {
AgentEntity agent = agentDao.selectById(agentId);
if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
return agent;
}
private boolean isCurrentUserSuperAdmin() {
UserDetail user = SecurityUser.getUser();
return user != null && Integer.valueOf(SuperAdminEnum.YES.value()).equals(user.getSuperAdmin());
}
private void requireCurrentUserPermissionIfPresent(AgentEntity agent) {
Long userId = SecurityUser.getUserId();
if (userId != null) {
requireAgentPermission(agent, userId);
}
}
private boolean hasAgentPermission(AgentEntity agent, Long userId) {
if (agent == null) {
return false;
}
if (isCurrentUserSuperAdmin()) {
return true;
}
return userId != null && userId.equals(agent.getUserId());
}
private void requireAgentPermission(AgentEntity agent, Long userId) {
if (!hasAgentPermission(agent, userId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
private boolean hasDevicePermission(DeviceEntity device, Long userId) {
if (device == null) {
return false;
}
if (isCurrentUserSuperAdmin()) {
return true;
}
return userId != null && userId.equals(device.getUserId());
}
private void requireDevicePermission(DeviceEntity device, Long userId) {
if (!hasDevicePermission(device, userId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
@Override @Override
public boolean insert(AgentEntity entity) { public boolean insert(AgentEntity entity) {
// 如果ID为空,自动生成一个UUID作为ID // 如果ID为空,自动生成一个UUID作为ID
@@ -135,10 +199,30 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgentByUserId(Long userId) { public void deleteAgentByUserId(Long userId) {
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>(); List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
wrapper.eq("user_id", userId); .select("id")
baseDao.delete(wrapper); .eq("user_id", userId));
for (AgentEntity agent : agents) {
deleteAgent(agent.getId());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgent(String agentId) {
if (agentDao.selectByIdForUpdate(agentId) == null) {
return;
}
deviceService.deleteByAgentId(agentId);
agentChatHistoryService.deleteByAgentId(agentId, true, true);
agentPluginMappingService.deleteByAgentId(agentId);
agentContextProviderService.deleteByAgentId(agentId);
correctWordFileService.deleteMappingsByAgentId(agentId);
agentTagService.deleteAgentTags(agentId);
agentSnapshotService.deleteByAgentId(agentId);
deleteById(agentId);
} }
@Override @Override
@@ -254,33 +338,47 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
@Override @Override
public boolean checkAgentPermission(String agentId, Long userId) { public boolean checkAgentPermission(String agentId, Long userId) {
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) { AgentEntity agent = agentDao.selectById(agentId);
return false; return hasAgentPermission(agent, userId);
}
// 获取智能体信息
AgentEntity agent = getAgentById(agentId);
if (agent == null) {
return false;
}
// 如果是超级管理员,直接返回true
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
return true;
}
// 检查是否是智能体的所有者
return userId.equals(agent.getUserId());
} }
// 根据id更新智能体信息 // 根据id更新智能体信息
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto) { public void updateAgentById(String agentId, AgentUpdateDTO dto) {
// 先查询现有实体 updateAgentById(agentId, dto, true);
AgentEntity existingEntity = this.getAgentById(agentId); }
if (existingEntity == null) {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) {
updateAgentById(agentId, dto, userId, true);
}
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) {
updateAgentById(agentId, dto, null, createSnapshot);
}
private void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId, boolean createSnapshot) {
AgentEntity lockedAgent = agentDao.selectByIdForUpdate(agentId);
if (lockedAgent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND); throw new RenException(ErrorCode.AGENT_NOT_FOUND);
} }
if (userId == null) {
requireCurrentUserPermissionIfPresent(lockedAgent);
} else {
requireAgentPermission(lockedAgent, userId);
}
// 锁定后查询现有实体和关联配置
AgentEntity existingEntity = this.getAgentById(agentId);
if (createSnapshot) {
int currentVersionNo = agentSnapshotService.getCurrentVersionNo(agentId);
agentSnapshotService.createSnapshot(agentId, currentVersionNo == 0 ? "initial" : "current");
}
// 只更新提供的非空字段 // 只更新提供的非空字段
if (dto.getAgentName() != null) { if (dto.getAgentName() != null) {
@@ -385,10 +483,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.toList(); .toList();
if (!toUpdate.isEmpty()) { if (!toUpdate.isEmpty()) {
agentPluginMappingService.updateBatchById(toUpdate); agentPluginMappingService.updateBatchById(toUpdate, IRepository.DEFAULT_BATCH_SIZE);
} }
if (!toInsert.isEmpty()) { if (!toInsert.isEmpty()) {
agentPluginMappingService.saveBatch(toInsert); agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
} }
// 5. 删除本次不在提交列表里的插件映射 // 5. 删除本次不在提交列表里的插件映射
@@ -397,7 +495,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.map(AgentPluginMapping::getId) .map(AgentPluginMapping::getId)
.toList(); .toList();
if (!toDelete.isEmpty()) { if (!toDelete.isEmpty()) {
agentPluginMappingService.removeBatchByIds(toDelete); agentPluginMappingService.removeByIds(toDelete);
} }
} }
@@ -430,11 +528,42 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds()); correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
} }
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); // 更新智能体标签
if (dto.getTagNames() != null || dto.getTagIds() != null) {
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
}
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
if (!b) { if (!b) {
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
} }
this.updateById(existingEntity); this.updateById(existingEntity);
if (createSnapshot) {
agentSnapshotService.createSnapshot(agentId, "config");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null || StringUtils.isBlank(device.getAgentId()) || dto == null) {
return;
}
requireDevicePermission(device, userId);
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
updateAgentById(device.getAgentId(), agentUpdateDTO, userId, false);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgentById(String agentId, Long userId) {
AgentEntity agent = getAgentEntityOrThrow(agentId);
requireAgentPermission(agent, userId);
deleteAgent(agentId);
} }
/** /**
@@ -490,10 +619,16 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
} }
entity.setTtsVoiceId(template.getTtsVoiceId()); entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setTtsLanguage(defaultIfBlank(template.getTtsLanguage(),
timbreModelService.getDefaultLanguageById(entity.getTtsVoiceId())));
entity.setMemModelId(template.getMemModelId()); entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId()); entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt()); entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory()); entity.setSummaryMemory(template.getSummaryMemory());
if (Constant.MEMORY_NO_MEM.equals(entity.getMemModelId())
|| Constant.MEMORY_MEM_REPORT_ONLY.equals(entity.getMemModelId())) {
entity.setSummaryMemory("");
}
// 根据记忆模型类型设置默认的chatHistoryConf值 // 根据记忆模型类型设置默认的chatHistoryConf值
if (template.getMemModelId() != null) { if (template.getMemModelId() != null) {
@@ -542,7 +677,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
mapping.setPluginId(pluginId); mapping.setPluginId(pluginId);
Map<String, Object> paramInfo = new HashMap<>(); Map<String, Object> paramInfo = new HashMap<>();
List<Map<String, Object>> fields = JsonUtils.parseObject(provider.getFields(), List.class); List<Map<String, Object>> fields = JsonUtils.parseMapList(provider.getFields());
if (fields != null) { if (fields != null) {
for (Map<String, Object> field : fields) { for (Map<String, Object> field : fields) {
paramInfo.put((String) field.get("key"), field.get("default")); paramInfo.put((String) field.get("key"), field.get("default"));
@@ -553,10 +688,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
toInsert.add(mapping); toInsert.add(mapping);
} }
// 保存默认插件 // 保存默认插件
agentPluginMappingService.saveBatch(toInsert); agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
agentSnapshotService.createSnapshot(entity.getId(), "initial");
return entity.getId(); return entity.getId();
} }
private String defaultIfBlank(String value, String defaultValue) {
return StringUtils.isBlank(value) ? defaultValue : value;
}
private String getDefaultLLMModelId() { private String getDefaultLLMModelId() {
try { try {
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM"); List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
@@ -0,0 +1,59 @@
package xiaozhi.modules.agent.service.impl;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.modules.agent.service.AgentSnapshotService;
@Slf4j
@Component
@RequiredArgsConstructor
public class AgentSnapshotRedactionRunner implements SmartInitializingSingleton {
static final long ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS = 5_000;
static final long ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS = 15_000;
private final AgentSnapshotService agentSnapshotService;
@Override
public void afterSingletonsInstantiated() {
redactAndReport("startup");
}
@Scheduled(initialDelay = ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
fixedDelay = ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS)
public void redactLateRollingDeploymentWrites() {
redactAndReport("rolling-deployment");
}
private void redactAndReport(String trigger) {
long startedAt = System.nanoTime();
try {
long migrated = agentSnapshotService.redactLegacySnapshots();
long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
if (migrated > 0) {
log.warn("Agent snapshot legacy redaction trigger={} migrated={} durationMs={}. Rotate credentials "
+ "that may have appeared in historical snapshot URLs, cookies, sessions, or structured "
+ "headers.", trigger, migrated, durationMillis);
} else if ("startup".equals(trigger)) {
log.info("Agent snapshot legacy redaction startup pass completed: migrated=0 durationMs={}; "
+ "rolling-deployment compensation starts after {} ms and repeats every {} ms.",
durationMillis, ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS);
}
} catch (RuntimeException exception) {
if ("startup".equals(trigger)) {
log.error("Agent snapshot legacy redaction failed during startup; blocking application startup "
+ "before it can accept traffic.", exception);
} else {
log.error("Agent snapshot legacy redaction failed during rolling-deployment compensation; the "
+ "scheduler will retry on its next run.", exception);
}
throw exception;
}
}
}
@@ -133,7 +133,7 @@ public class AgentTagServiceImpl extends BaseServiceImpl<AgentTagDao, AgentTagEn
} }
if (tagIds != null && !tagIds.isEmpty()) { if (tagIds != null && !tagIds.isEmpty()) {
List<AgentTagEntity> tagIdEntities = baseDao.selectBatchIds(tagIds); List<AgentTagEntity> tagIdEntities = baseDao.selectByIds(tagIds);
for (AgentTagEntity tag : tagIdEntities) { for (AgentTagEntity tag : tagIdEntities) {
if (tag != null && (currentTagNames.contains(tag.getTagName()) || if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
newTagNames.contains(tag.getTagName()))) { newTagNames.contains(tag.getTagName()))) {
@@ -4,7 +4,7 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import xiaozhi.modules.agent.dao.AgentTemplateDao; import xiaozhi.modules.agent.dao.AgentTemplateDao;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
@@ -21,7 +21,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
* @createDate 2025-03-22 11:48:18 * @createDate 2025-03-22 11:48:18
*/ */
@Service @Service
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity> public class AgentTemplateServiceImpl extends CrudRepository<AgentTemplateDao, AgentTemplateEntity>
implements AgentTemplateService { implements AgentTemplateService {
/** /**
@@ -22,7 +22,7 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
@@ -46,7 +46,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
*/ */
@Service @Service
@Slf4j @Slf4j
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity> public class AgentVoicePrintServiceImpl extends CrudRepository<AgentVoicePrintDao, AgentVoicePrintEntity>
implements AgentVoicePrintService { implements AgentVoicePrintService {
private final AgentChatAudioService agentChatAudioService; private final AgentChatAudioService agentChatAudioService;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
@@ -0,0 +1,59 @@
package xiaozhi.modules.agent.typehandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import com.fasterxml.jackson.core.type.TypeReference;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dto.ContextProviderDTO;
/**
* JSON type handler for context providers.
*
* <p>Do not extend MyBatis-Plus {@code AbstractJsonTypeHandler}: its constructor and JSON handler
* contract changed in MyBatis-Plus 3.5.6. {@link BaseTypeHandler} is part of MyBatis itself and
* keeps this handler compatible with both 3.5.5 and newer MyBatis-Plus releases.</p>
*/
public class ContextProviderListTypeHandler extends BaseTypeHandler<List<ContextProviderDTO>> {
private static final TypeReference<List<ContextProviderDTO>> CONTEXT_PROVIDER_LIST_TYPE = new TypeReference<>() {
};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<ContextProviderDTO> parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtils.toJsonString(parameter));
}
@Override
public List<ContextProviderDTO> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseNullable(rs.getString(columnName));
}
@Override
public List<ContextProviderDTO> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseNullable(rs.getString(columnIndex));
}
@Override
public List<ContextProviderDTO> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseNullable(cs.getString(columnIndex));
}
private List<ContextProviderDTO> parseNullable(String json) {
if (StringUtils.isBlank(json)) {
return null;
}
List<ContextProviderDTO> providers = JsonUtils.parseObject(json, CONTEXT_PROVIDER_LIST_TYPE);
return providers == null ? Collections.emptyList() : providers;
}
}
@@ -1,7 +1,5 @@
package xiaozhi.modules.agent.vo; package xiaozhi.modules.agent.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -20,7 +18,6 @@ import java.util.List;
public class AgentInfoVO extends AgentEntity public class AgentInfoVO extends AgentEntity
{ {
@Schema(description = "插件列表Id") @Schema(description = "插件列表Id")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AgentPluginMapping> functions; private List<AgentPluginMapping> functions;
@Schema(description = "上下文源配置") @Schema(description = "上下文源配置")
@@ -28,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
@Schema(description = "替换词文件ID列表") @Schema(description = "替换词文件ID列表")
private List<String> correctWordFileIds; private List<String> correctWordFileIds;
@Schema(description = "当前配置版本号")
private Integer currentVersionNo;
} }
@@ -0,0 +1,34 @@
package xiaozhi.modules.agent.vo;
import java.util.Date;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
@Data
@Schema(description = "智能体配置快照")
public class AgentSnapshotVO {
private String id;
private String agentId;
@Schema(description = "所属用户ID,表示该快照归属的智能体所有者")
private Long userId;
private Integer versionNo;
private List<String> changedFields;
private List<String> fieldOrder;
private String source;
@Schema(description = "恢复来源快照ID,仅恢复结果版本有值")
private String restoreFromSnapshotId;
@Schema(description = "恢复来源版本号,仅恢复结果版本有值")
private Integer restoreFromVersionNo;
@Schema(description = "创建者,表示触发本次快照写入的操作人")
private Long creator;
private Date createdAt;
private AgentSnapshotDataDTO snapshotData;
private AgentSnapshotDataDTO afterSnapshotData;
@Schema(description = "恢复预览对应的脱敏当前配置,仅详情接口有值")
private AgentSnapshotDataDTO currentSnapshotData;
@Schema(description = "恢复预览对应的当前配置状态指纹,仅详情接口有值")
private String currentStateToken;
}
@@ -9,6 +9,7 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.config.service.ConfigService; import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
@Configuration @Configuration
@@ -24,6 +25,9 @@ public class SystemInitConfig {
@Autowired @Autowired
private RedisUtils redisUtils; private RedisUtils redisUtils;
@Autowired
private DeviceAddressBookService deviceAddressBookService;
@PostConstruct @PostConstruct
public void init() { public void init() {
// 检查版本号 // 检查版本号
@@ -37,5 +41,8 @@ public class SystemInitConfig {
sysParamsService.initServerSecret(); sysParamsService.initServerSecret();
configService.getConfig(false); configService.getConfig(false);
// 初始化设备通讯录缓存
deviceAddressBookService.refreshCache();
} }
} }
@@ -10,7 +10,7 @@ public interface ConfigService {
* @param isCache 是否缓存 * @param isCache 是否缓存
* @return 配置信息 * @return 配置信息
*/ */
Object getConfig(Boolean isCache); Map<String, Object> getConfig(Boolean isCache);
/** /**
* 获取智能体模型配置 * 获取智能体模型配置
@@ -65,12 +65,12 @@ public class ConfigServiceImpl implements ConfigService {
private final CorrectWordFileService correctWordFileService; private final CorrectWordFileService correctWordFileService;
@Override @Override
public Object getConfig(Boolean isCache) { public Map<String, Object> getConfig(Boolean isCache) {
if (isCache) { if (isCache) {
// 先从Redis获取配置 // 先从Redis获取配置
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey()); Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
if (cachedConfig != null) { if (cachedConfig != null) {
return cachedConfig; return JsonUtils.toStringObjectMap(cachedConfig);
} }
} }
@@ -123,7 +123,7 @@ public class ConfigServiceImpl implements ConfigService {
if (isAdminRequest != null && "true".equals(isAdminRequest)) { if (isAdminRequest != null && "true".equals(isAdminRequest)) {
// 管理控制台请求,返回getConfig的结果 // 管理控制台请求,返回getConfig的结果
redisUtils.delete(redisKey); // 使用后清理 redisUtils.delete(redisKey); // 使用后清理
return (Map<String, Object>) getConfig(true); return getConfig(true);
} }
// 根据MAC地址查找设备 // 根据MAC地址查找设备
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
@@ -277,10 +277,10 @@ public class ConfigServiceImpl implements ConfigService {
// 遍历除最后一个key之外的所有key // 遍历除最后一个key之外的所有key
for (int i = 0; i < keys.length - 1; i++) { for (int i = 0; i < keys.length - 1; i++) {
String key = keys[i]; String key = keys[i];
if (!current.containsKey(key)) { Object nestedConfig = current.computeIfAbsent(key, ignored -> new HashMap<String, Object>());
current.put(key, new HashMap<String, Object>()); Map<String, Object> nestedMap = JsonUtils.toStringObjectMap(nestedConfig);
} current.put(key, nestedMap);
current = (Map<String, Object>) current.get(key); current = nestedMap;
} }
// 处理最后一个key // 处理最后一个key
@@ -8,6 +8,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -22,13 +23,17 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceAddressBookAliasDTO;
import xiaozhi.modules.device.dto.DeviceAddressBookPermissionDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DeviceRegisterDTO; import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO; import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO; import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO; import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
@@ -37,11 +42,14 @@ import xiaozhi.modules.sys.service.SysParamsService;
@RequestMapping("/device") @RequestMapping("/device")
public class DeviceController { public class DeviceController {
private final DeviceService deviceService; private final DeviceService deviceService;
private final DeviceAddressBookService deviceAddressBookService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final SysParamsService sysParamsService; private final SysParamsService sysParamsService;
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) { public DeviceController(DeviceService deviceService, DeviceAddressBookService deviceAddressBookService,
RedisUtils redisUtils, SysParamsService sysParamsService) {
this.deviceService = deviceService; this.deviceService = deviceService;
this.deviceAddressBookService = deviceAddressBookService;
this.redisUtils = redisUtils; this.redisUtils = redisUtils;
this.sysParamsService = sysParamsService; this.sysParamsService = sysParamsService;
} }
@@ -78,10 +86,10 @@ public class DeviceController {
@GetMapping("/bind/{agentId}") @GetMapping("/bind/{agentId}")
@Operation(summary = "获取已绑定设备") @Operation(summary = "获取已绑定设备")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<List<DeviceEntity>> getUserDevices(@PathVariable String agentId) { public Result<List<UserShowDeviceListVO>> getUserDevices(@PathVariable String agentId) {
UserDetail user = SecurityUser.getUser(); UserDetail user = SecurityUser.getUser();
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId); List<UserShowDeviceListVO> devices = deviceService.getUserDeviceList(user.getId(), agentId);
return new Result<List<DeviceEntity>>().ok(devices); return new Result<List<UserShowDeviceListVO>>().ok(devices);
} }
@PostMapping("/bind/{agentId}") @PostMapping("/bind/{agentId}")
@@ -159,4 +167,48 @@ public class DeviceController {
response.setMsg("Tools called successfully"); response.setMsg("Tools called successfully");
return response.ok(result); return response.ok(result);
} }
@GetMapping("/address-book/{macAddress}")
@Operation(summary = "获取设备通讯录")
@RequiresPermissions("sys:role:normal")
public Result<Object> getAddressBook(@PathVariable String macAddress) {
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
}
@GetMapping("/address-book/call")
@Operation(summary = "根据昵称发起呼叫")
public Result<Map<String, Object>> callByNickname(String callerMac, String nickname,
@RequestParam(required = false, defaultValue = "false") boolean answer) {
Map<String, Object> result = deviceAddressBookService.callByNickname(callerMac, nickname, answer);
if (result == null) {
return new Result<Map<String, Object>>().error("未找到对应设备");
}
return new Result<Map<String, Object>>().ok(result);
}
@PutMapping("/address-book/alias")
@Operation(summary = "更新设备通讯录别名")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
UserDetail user = SecurityUser.getUser();
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
return new Result<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
return new Result<Void>();
}
@PutMapping("/address-book/permission")
@Operation(summary = "更新设备通讯录权限")
@RequiresPermissions("sys:role:normal")
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
UserDetail user = SecurityUser.getUser();
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
return new Result<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
return new Result<Void>();
}
} }
@@ -0,0 +1,39 @@
package xiaozhi.modules.device.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
@Mapper
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
/**
* 新增设备通讯录记录
*/
int insertAddressBook(DeviceAddressBookEntity entity);
/**
* 获取设备通讯录列表
*/
List<DeviceAddressBookEntity> getAddressBookList(@Param("macAddress") String macAddress);
/**
* 更新别名
*/
void updateAlias(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("alias") String alias);
/**
* 更新权限
*/
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
}
@@ -0,0 +1,21 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "更新设备通讯录别名")
public class DeviceAddressBookAliasDTO {
@NotBlank(message = "MAC地址不能为空")
@Schema(description = "本设备MAC地址")
private String macAddress;
@NotBlank(message = "目标MAC地址不能为空")
@Schema(description = "对方设备MAC地址")
private String targetMac;
@Schema(description = "我对对方的称呼")
private String alias;
}
@@ -0,0 +1,21 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "更新设备通讯录权限")
public class DeviceAddressBookPermissionDTO {
@NotBlank(message = "MAC地址不能为空")
@Schema(description = "本设备MAC地址")
private String macAddress;
@NotBlank(message = "目标MAC地址不能为空")
@Schema(description = "对方设备MAC地址")
private String targetMac;
@Schema(description = "是否有权限呼叫")
private Boolean hasPermission;
}
@@ -1,27 +0,0 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 设备绑定的DTO
*
* @author zjy
* @since 2025-3-28
*/
@Data
@AllArgsConstructor
@Schema(description = "设备连接头信息")
public class DeviceBindDTO {
@Schema(description = "mac地址")
private String macAddress;
@Schema(description = "所属用户id")
private Long userId;
@Schema(description = "智能体id")
private String agentId;
}
@@ -0,0 +1,49 @@
package xiaozhi.modules.device.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_device_address_book")
@Schema(description = "设备通讯录")
public class DeviceAddressBookEntity {
@TableId(type = IdType.INPUT)
@Schema(description = "本设备MAC地址")
private String macAddress;
@Schema(description = "对方设备MAC地址")
private String targetMac;
@Schema(description = "我对对方的称呼")
private String alias;
@Schema(description = "是否有权限呼叫")
private Boolean hasPermission;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private Long creator;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private Date createDate;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新人")
private Long updater;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,52 @@
package xiaozhi.modules.device.service;
import java.util.List;
import java.util.Map;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
public interface DeviceAddressBookService {
/**
* 获取设备通讯录列表
*/
List<DeviceAddressBookEntity> getAddressBookList(String macAddress);
/**
* 获取所有设备的通讯录(全局缓存用)
*/
Map<String, Map<String, String>> getAllAddressBooks();
/**
* 更新别名
*/
void updateAlias(String macAddress, String targetMac, String alias);
/**
* 更新权限
*/
void updatePermission(String macAddress, String targetMac, Boolean hasPermission);
/**
* 添加或更新通讯录记录
*/
void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission);
/**
* 刷新通讯录缓存
*/
void refreshCache();
/**
* 根据昵称发起呼叫
* @param callerMac 主叫方MAC地址
* @param nickname 被叫方昵称
* @param isAnswer 是否为接听模式(跳过权限检查)
*/
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(List<String> macAddresses);
}
@@ -30,6 +30,11 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/ */
List<DeviceEntity> getUserDevices(Long userId, String agentId); List<DeviceEntity> getUserDevices(Long userId, String agentId);
/**
* 获取用户指定智能体的设备列表(带时区处理),
*/
List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId);
/** /**
* 解绑设备 * 解绑设备
*/ */
@@ -133,4 +138,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/ */
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments); Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
} }
@@ -0,0 +1,258 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.hutool.json.JSONUtil;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.sys.service.SysParamsService;
@Service
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
private final DeviceAddressBookDao deviceAddressBookDao;
private final RedisUtils redisUtils;
private final DeviceService deviceService;
private final SysParamsService sysParamsService;
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
@Lazy DeviceService deviceService, SysParamsService sysParamsService) {
this.deviceAddressBookDao = deviceAddressBookDao;
this.redisUtils = redisUtils;
this.deviceService = deviceService;
this.sysParamsService = sysParamsService;
}
@Override
public List<DeviceAddressBookEntity> getAddressBookList(String macAddress) {
return deviceAddressBookDao.getAddressBookList(macAddress);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Map<String, String>> getAllAddressBooks() {
Object cached = redisUtils.get(RedisKeys.getAddressBookKey());
if (cached != null) {
return (Map<String, Map<String, String>>) cached;
}
refreshCache();
return (Map<String, Map<String, String>>) redisUtils.get(RedisKeys.getAddressBookKey());
}
@Override
public Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer) {
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
if (isAnswer) {
return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听");
}
// 主动呼叫模式
Map<String, String> callerBook = allBooks.get(callerMac.toLowerCase());
if (callerBook == null) {
return errorResult("未找到备注为'" + nickname + "'的设备");
}
String targetMacWithPerm = callerBook.get(nickname);
if (targetMacWithPerm == null) {
return errorResult("未找到备注为'" + nickname + "'的设备");
}
String[] parts = targetMacWithPerm.split("\\|");
String targetMac = parts[0];
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
if (!hasPermission) {
return errorResult("呼叫失败,您没有权限呼叫该设备");
}
// 获取目标设备如何称呼主叫方
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
String callerNickname = null;
if (targetBook != null) {
callerNickname = targetBook.get(callerMac.toLowerCase());
}
if (StringUtils.isBlank(callerNickname)) {
callerNickname = deviceService.getDeviceByMacAddress(callerMac).getAlias();
if (StringUtils.isBlank(callerNickname)) {
callerNickname = formatMacAsDeviceName(callerMac);
}
}
return postToMqtt("/api/call/request",
Map.of("caller_mac", callerMac, "target_mac", targetMac, "caller_nickname", callerNickname),
"呼叫");
}
@Override
public void refreshCache() {
Map<String, Map<String, String>> result = new HashMap<>();
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
Map<String, String> reverseMap = new HashMap<>();
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String alias = entity.getAlias();
Boolean hasPermission = entity.getHasPermission();
// 构建正向映射: A对B的映射 nickname -> macB|permission
if (alias != null && !alias.isEmpty()) {
result.computeIfAbsent(macA, k -> new HashMap<>());
String permStr = (hasPermission != null && hasPermission) ? "1" : "0";
result.get(macA).put(alias, macB + "|" + permStr);
}
// 构建反向记录用于查找B对A的称呼
if (alias != null && !alias.isEmpty()) {
reverseMap.put(macB + ":" + macA, alias);
}
}
// 构建反向映射: B对A的映射 macA -> nickname
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String aliasBtoA = reverseMap.get(macA + ":" + macB);
if (aliasBtoA != null && !aliasBtoA.isEmpty()) {
result.computeIfAbsent(macB, k -> new HashMap<>());
result.get(macB).put(macA, aliasBtoA);
}
}
redisUtils.set(RedisKeys.getAddressBookKey(), result);
}
@Override
public void updateAlias(String macAddress, String targetMac, String alias) {
String finalAlias = generateUniqueAlias(macAddress, targetMac, alias);
deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias);
refreshCache();
}
@Override
public void updatePermission(String macAddress, String targetMac, Boolean hasPermission) {
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
refreshCache();
}
@Override
public void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission) {
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac);
DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper);
System.out.println("saveOrUpdate - mac=" + macAddress + ", targetMac=" + targetMac + ", alias=" + alias + ", record=" + (record == null ? "null" : "not null"));
if (record == null) {
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
entity.setMacAddress(macAddress);
entity.setTargetMac(targetMac);
// 如果 alias 为空,就默认使用设备名称
if (StringUtils.isBlank(alias)) {
alias = deviceService.getDeviceByMacAddress(targetMac).getAlias();
}
// 检查重名
alias = generateUniqueAlias(macAddress, targetMac, alias);
entity.setAlias(alias);
entity.setHasPermission(hasPermission);
deviceAddressBookDao.insertAddressBook(entity);
} else {
if (alias != null) {
updateAlias(macAddress, targetMac, alias);
}
if (hasPermission != null) {
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
}
}
refreshCache();
}
@Override
public void deleteByMacAddresses(List<String> macAddresses) {
if (macAddresses == null || macAddresses.isEmpty()) {
return;
}
deviceAddressBookDao.deleteByMacAddresses(macAddresses);
refreshCache();
}
private Map<String, Object> errorResult(String message) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", message);
return result;
}
private Map<String, Object> postToMqtt(String path, Map<String, Object> body, String action) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
String mqttSignatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|| MqttGatewayAuthorization.isMissingSignatureKey(mqttSignatureKey)) {
result.put("message", action + "失败,网关配置缺失");
return result;
}
try {
String url = "http://" + mqttGatewayUrl + path;
String response = MqttGatewayAuthorization.postJson(
url,
JSONUtil.toJsonStr(body),
mqttSignatureKey,
Instant.now(),
5000);
if (StringUtils.isNotBlank(response)) {
Map<String, Object> gwResult = JSONUtil.parseObj(response);
result.put("status", gwResult.get("status"));
result.put("message", gwResult.get("message"));
}
return result;
} catch (Exception e) {
result.put("message", action + "失败,请稍后再试");
return result;
}
}
private String formatMacAsDeviceName(String mac) {
if (StringUtils.isBlank(mac) || mac.length() < 2) {
return mac;
}
String lastTwo = mac.substring(mac.length() - 2);
return "尾号为" + lastTwo + "的设备";
}
private String generateUniqueAlias(String macAddress, String targetMac, String alias) {
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress);
List<DeviceAddressBookEntity> existing = deviceAddressBookDao.selectList(wrapper);
List<String> existNames = existing.stream()
.map(DeviceAddressBookEntity::getAlias)
.filter(a -> a != null && !a.isEmpty())
.collect(Collectors.toList());
if (!existNames.contains(alias)) {
return alias;
}
int suffix = 1;
String newAlias;
while (existNames.contains(newAlias = alias + suffix)) {
suffix++;
}
return newAlias;
}
}
@@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -30,15 +31,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
@@ -55,6 +50,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil; import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.device.dao.DeviceDao; import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.dto.DeviceManualAddDTO;
@@ -63,6 +59,7 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.entity.OtaEntity; import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.service.OtaService; import xiaozhi.modules.device.service.OtaService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO; import xiaozhi.modules.device.vo.UserShowDeviceListVO;
@@ -80,6 +77,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
private final SysParamsService sysParamsService; private final SysParamsService sysParamsService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final OtaService otaService; private final OtaService otaService;
private final DeviceAddressBookService deviceAddressBookService;
@Async @Async
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) { public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
@@ -112,7 +110,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
String deviceId = (String) cacheDeviceId; String deviceId = (String) cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId); String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey); Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey));
if (ToolUtil.isEmpty(cacheMap)) { if (ToolUtil.isEmpty(cacheMap)) {
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
} }
@@ -184,14 +182,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("clientIds", deviceIds).build(); .put("clientIds", deviceIds).build();
if (ToolUtil.isNotEmpty(deviceIds)) { if (ToolUtil.isNotEmpty(deviceIds)) {
// 发送请求 return postToMqttGateway(url, params);
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(params))
.timeout(10000) // 超时,毫秒
.execute().body();
return resultMessage;
} }
// 返回响应 // 返回响应
return ""; return "";
@@ -298,13 +289,33 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return baseDao.selectList(wrapper); return baseDao.selectList(wrapper);
} }
@Override
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
List<DeviceEntity> devices = getUserDevices(userId, agentId);
return devices.stream().map(this::toUserShowDeviceListVO).toList();
}
private UserShowDeviceListVO toUserShowDeviceListVO(DeviceEntity device) {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
vo.setDeviceType(device.getBoard());
vo.setBoard(device.getBoard());
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
return vo;
}
private Long toTimestamp(Date date) {
return date == null ? null : date.getTime();
}
@Override @Override
public void unbindDevice(Long userId, String deviceId) { public void unbindDevice(Long userId, String deviceId) {
// 先查询设备信息,获取agentId // 先查询设备信息,获取agentId和macAddress
DeviceEntity device = baseDao.selectById(deviceId); DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) { if (device == null) {
return; return;
} }
String macAddress = device.getMacAddress();
if (StringUtils.isNotBlank(device.getAgentId())) { if (StringUtils.isNotBlank(device.getAgentId())) {
// 清除智能体设备数量缓存 // 清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId())); redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
@@ -314,6 +325,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
wrapper.eq("user_id", userId); wrapper.eq("user_id", userId);
wrapper.eq("id", deviceId); wrapper.eq("id", deviceId);
baseDao.delete(wrapper); baseDao.delete(wrapper);
// 删除设备相关的通讯录权限记录
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
} }
@Override @Override
@@ -332,9 +346,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override @Override
public void deleteByAgentId(String agentId) { public void deleteByAgentId(String agentId) {
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("agent_id", agentId);
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
// 删除设备
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>(); UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("agent_id", agentId); wrapper.eq("agent_id", agentId);
baseDao.delete(wrapper); baseDao.delete(wrapper);
// 批量删除这些设备相关的所有通讯录权限记录
if (!devices.isEmpty()) {
List<String> macAddresses = devices.stream()
.map(DeviceEntity::getMacAddress)
.collect(Collectors.toList());
deviceAddressBookService.deleteByMacAddresses(macAddresses);
}
} }
@Override @Override
@@ -350,12 +378,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords())); .like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
// 循环处理page获取回来的数据,返回需要的字段 // 循环处理page获取回来的数据,返回需要的字段
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> { List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class); UserShowDeviceListVO vo = toUserShowDeviceListVO(device);
// 把最后修改的时间,改为简短描述的时间 // 把最后修改的时间,改为简短描述的时间
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate())); vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
sysUserUtilService.assignUsername(device.getUserId(), sysUserUtilService.assignUsername(device.getUserId(),
vo::setBindUserName); vo::setBindUserName);
vo.setDeviceType(device.getBoard());
return vo; return vo;
}).toList(); }).toList();
// 计算页数 // 计算页数
@@ -385,7 +412,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
public String geCodeByDeviceId(String deviceId) { public String geCodeByDeviceId(String deviceId) {
String dataKey = getDeviceCacheKey(deviceId); String dataKey = getDeviceCacheKey(deviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey); Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(dataKey));
if (cacheMap != null && cacheMap.containsKey("activation_code")) { if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code"); String cachedCode = (String) cacheMap.get("activation_code");
return cachedCode; return cachedCode;
@@ -659,20 +686,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return mqtt; return mqtt;
} }
/** private String postToMqttGateway(String url, Object requestBody) {
* 生成BearerToken
*/
private String generateBearerToken() {
try {
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false); String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
if (ToolUtil.isEmpty(signatureKey)) { return MqttGatewayAuthorization.postJson(
return null; url,
} JSONUtil.toJsonStr(requestBody),
return DigestUtil.sha256Hex(dateStr + signatureKey); signatureKey,
} catch (Exception e) { Instant.now());
return null;
}
} }
@Override @Override
@@ -733,13 +753,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("payload", payload) .put("payload", payload)
.build(); .build();
// 发送请求 String resultMessage = postToMqttGateway(url, requestBody);
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(requestBody))
.timeout(10000) // 超时,毫秒
.execute().body();
// 解析响应 // 解析响应
if (StringUtils.isBlank(resultMessage)) { if (StringUtils.isBlank(resultMessage)) {
@@ -830,13 +844,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("payload", payload) .put("payload", payload)
.build(); .build();
// 发送请求 String resultMessage = postToMqttGateway(url, requestBody);
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(requestBody))
.timeout(10000) // 超时,毫秒
.execute().body();
// 解析响应 // 解析响应
if (StringUtils.isNotBlank(resultMessage)) { if (StringUtils.isNotBlank(resultMessage)) {
@@ -0,0 +1,111 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.List;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
final class MqttGatewayAuthorization {
private static final int HTTP_UNAUTHORIZED = 401;
private static final int DEFAULT_TIMEOUT_MILLIS = 10000;
private MqttGatewayAuthorization() {
}
static String postJson(String url, String jsonBody, String signatureKey, Instant now) {
return postJson(url, jsonBody, signatureKey, now, DEFAULT_TIMEOUT_MILLIS);
}
static String postJson(String url, String jsonBody, String signatureKey, Instant now, int timeoutMillis) {
GatewayResponse response = executeWithDateFallback(
signatureKey,
now,
token -> executeRequest(url, jsonBody, token, timeoutMillis));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new GatewayRequestException(
"MQTT Gateway request failed with HTTP status " + response.statusCode(),
response.statusCode());
}
return response.body();
}
static List<String> generateDailyTokens(String signatureKey, Instant now) {
if (isMissingSignatureKey(signatureKey)) {
throw new GatewayRequestException("MQTT Gateway signature key is empty", null);
}
LocalDate utcDate = now.atZone(ZoneOffset.UTC).toLocalDate();
return List.of(
tokenFor(utcDate, signatureKey),
tokenFor(utcDate.minusDays(1), signatureKey),
tokenFor(utcDate.plusDays(1), signatureKey));
}
static GatewayResponse executeWithDateFallback(String signatureKey, Instant now,
Function<String, GatewayResponse> requestExecutor) {
GatewayResponse lastAuthenticationFailure = null;
for (String token : generateDailyTokens(signatureKey, now)) {
GatewayResponse response = requestExecutor.apply(token);
if (!isAuthenticationFailure(response.statusCode())) {
return response;
}
lastAuthenticationFailure = response;
}
Integer statusCode = lastAuthenticationFailure == null ? null : lastAuthenticationFailure.statusCode();
throw new GatewayRequestException(
"MQTT Gateway rejected all daily authorization tokens"
+ (statusCode == null ? "" : " (HTTP " + statusCode + ")"),
statusCode);
}
private static String tokenFor(LocalDate date, String signatureKey) {
return DigestUtil.sha256Hex(date + signatureKey);
}
private static boolean isAuthenticationFailure(int statusCode) {
return statusCode == HTTP_UNAUTHORIZED;
}
static boolean isMissingSignatureKey(String signatureKey) {
return StringUtils.isBlank(signatureKey) || "null".equalsIgnoreCase(signatureKey.trim());
}
private static GatewayResponse executeRequest(String url, String jsonBody, String token, int timeoutMillis) {
try (HttpResponse response = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + token)
.body(jsonBody)
.timeout(timeoutMillis)
.execute()) {
return new GatewayResponse(response.getStatus(), response.body());
}
}
record GatewayResponse(int statusCode, String body) {
}
static final class GatewayRequestException extends RuntimeException {
private final Integer statusCode;
GatewayRequestException(String message, Integer statusCode) {
super(message);
this.statusCode = statusCode;
}
Integer statusCode() {
return statusCode;
}
}
}
@@ -56,7 +56,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
@Override @Override
public void delete(String[] ids) { public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids)); baseDao.deleteByIds(Arrays.asList(ids));
} }
@Override @Override
@@ -1,5 +1,9 @@
package xiaozhi.modules.device.vo; package xiaozhi.modules.device.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -16,16 +20,32 @@ public class UserShowDeviceListVO {
@Schema(description = "设备型号") @Schema(description = "设备型号")
private String deviceType; private String deviceType;
@Schema(description = "设备型号(board)")
private String board;
@Schema(description = "设备唯一标识符") @Schema(description = "设备唯一标识符")
private String id; private String id;
@Schema(description = "mac地址") @Schema(description = "mac地址")
private String macAddress; private String macAddress;
@Schema(description = "设备别名")
private String alias;
@Schema(description = "开启OTA") @Schema(description = "开启OTA")
private Integer otaUpgrade; private Integer otaUpgrade;
@Schema(description = "最近对话时间") @Schema(description = "最近对话时间")
private String recentChatTime; private String recentChatTime;
@Schema(description = "最后连接时间戳(毫秒)", type = "string", example = "1783689702000")
private Long lastConnectedAtTimestamp;
@Schema(description = "绑定时间戳(毫秒)", type = "string", example = "1783689702000")
private Long createDateTimestamp;
@Schema(description = "绑定时间(兼容字段,请使用 createDateTimestamp", deprecated = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private Date createDate;
} }
@@ -167,16 +167,21 @@ public class KnowledgeFilesController {
public Result<ChunkDTO.ListVO> listChunks( public Result<ChunkDTO.ListVO> listChunks(
@PathVariable("dataset_id") String datasetId, @PathVariable("dataset_id") String datasetId,
@PathVariable("document_id") String documentId, @PathVariable("document_id") String documentId,
@ParameterObject ChunkDTO.ListReq req) { @RequestParam(name = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(name = "page_size", required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) String id) {
// 验证权限 (内部已包含知识库存在性校验与归属权校验) // 验证权限 (内部已包含知识库存在性校验与归属权校验)
validateKnowledgeBasePermission(datasetId); validateKnowledgeBasePermission(datasetId);
// 设置默认值 // 构建请求对象
if (req.getPage() == null) ChunkDTO.ListReq req = ChunkDTO.ListReq.builder()
req.setPage(1); .page(page)
if (req.getPageSize() == null) .pageSize(pageSize)
req.setPageSize(50); .keywords(keywords)
.id(id)
.build();
// 调用服务层获取强类型切片列表 // 调用服务层获取强类型切片列表
ChunkDTO.ListVO result = knowledgeFilesService.listChunks(datasetId, documentId, req); ChunkDTO.ListVO result = knowledgeFilesService.listChunks(datasetId, documentId, req);
@@ -1,68 +0,0 @@
package xiaozhi.modules.knowledge.dto;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文档 DTO
*/
@Data
@Schema(description = "知识库文档")
public class DocumentDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "本地ID")
private String id;
@Schema(description = "知识库ID")
private String datasetId;
@Schema(description = "RAGFlow文档ID")
private String documentId;
@Schema(description = "文档名称")
private String name;
@Schema(description = "文件大小")
private Long size;
@Schema(description = "文件类型")
private String type;
@Schema(description = "分块方法")
private String chunkMethod;
@Schema(description = "解析配置")
private Map<String, Object> parserConfig;
@Schema(description = "处理状态 (1:解析中 3:成功 4:失败)")
private Integer status;
@Schema(description = "错误信息")
private String error;
@Schema(description = "分块数量")
private Integer chunkCount;
@Schema(description = "Token数量")
private Long tokenCount;
@Schema(description = "是否启用")
private Integer enabled;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新时间")
private Date updatedAt;
@Schema(description = "上传进度 (虚拟字段)")
private Double progress;
@Schema(description = "缩略图/预览图 (虚拟字段)")
private String thumbnail;
}
@@ -67,4 +67,7 @@ public class KnowledgeBaseDTO implements Serializable {
@Schema(description = "文档数量") @Schema(description = "文档数量")
private Integer documentCount; private Integer documentCount;
@Schema(description = "异常提示")
private String errorMessage;
} }
@@ -1,421 +0,0 @@
package xiaozhi.modules.knowledge.dto.agent;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "智能体 (Agent) 管理聚合 DTO")
public class AgentDTO {
// ========== 1. Agent 管理 (CRUD) - 对应 RAGFlow_Agent接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 创建请求")
public static class CreateReq implements Serializable {
@Schema(description = "Agent 标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "My Agent")
@NotBlank(message = "Agent 标题不能为空")
@JsonProperty("title")
private String title;
@Schema(description = "DSL 定义 (画布 JSON)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "DSL 定义不能为空")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "描述", example = "这是一个测试 Agent")
@JsonProperty("description")
private String description;
@Schema(description = "头像 URL", example = "http://example.com/avatar.png")
@JsonProperty("avatar")
private String avatar;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 更新请求")
public static class UpdateReq implements Serializable {
@Schema(description = "Agent 标题", example = "Updated Agent")
@JsonProperty("title")
private String title;
@Schema(description = "DSL 定义 (画布 JSON)")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "描述")
@JsonProperty("description")
private String description;
@Schema(description = "头像 URL")
@JsonProperty("avatar")
private String avatar;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 列表请求")
public static class ListReq implements Serializable {
@Schema(description = "页码", defaultValue = "1")
@JsonProperty("page")
@Builder.Default
private Integer page = 1;
@Schema(description = "每页大小", defaultValue = "10")
@JsonProperty("page_size")
@Builder.Default
private Integer pageSize = 10;
@Schema(description = "排序字段", defaultValue = "update_time")
@JsonProperty("orderby")
@Builder.Default
private String orderby = "update_time";
@Schema(description = "是否降序", defaultValue = "true")
@JsonProperty("desc")
@Builder.Default
private Boolean desc = true;
@Schema(description = "Agent ID 过滤")
@JsonProperty("id")
private String id;
@Schema(description = "标题模糊搜索")
@JsonProperty("title")
private String title;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 响应对象")
public static class AgentVO implements Serializable {
@Schema(description = "Agent ID")
@JsonProperty("id")
private String id;
@Schema(description = "标题")
@JsonProperty("title")
private String title;
@Schema(description = "描述")
@JsonProperty("description")
private String description;
@Schema(description = "头像")
@JsonProperty("avatar")
private String avatar;
@Schema(description = "DSL 定义")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "创建者 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "画布分类")
@JsonProperty("canvas_category")
private String canvasCategory;
@Schema(description = "创建时间 (时间戳)")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)")
@JsonProperty("update_time")
private Long updateTime;
}
// ========== 2. Webhook 调试与追踪 - 对应 RAGFlow_Agent接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 触发请求 (参数动态)")
public static class WebhookTriggerReq implements Serializable {
@Schema(description = "输入变量", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "输入变量不能为空")
@JsonProperty("inputs")
private Map<String, Object> inputs;
@Schema(description = "查询词", example = "Hello")
@JsonProperty("query")
private String query;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 追踪请求")
public static class WebhookTraceReq implements Serializable {
@Schema(description = "时间戳游标", example = "1700000000.0")
@JsonProperty("since_ts")
private Double sinceTs;
@Schema(description = "Webhook ID")
@JsonProperty("webhook_id")
private String webhookId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 追踪响应")
public static class WebhookTraceVO implements Serializable {
@Schema(description = "Webhook ID")
@JsonProperty("webhook_id")
private String webhookId;
@Schema(description = "是否结束")
@JsonProperty("finished")
private Boolean finished;
@Schema(description = "下一次查询的时间戳游标")
@JsonProperty("next_since_ts")
private Double nextSinceTs;
@Schema(description = "事件列表")
@JsonProperty("events")
private List<TraceEvent> events;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "追踪事件项")
public static class TraceEvent implements Serializable {
@Schema(description = "时间戳")
@JsonProperty("ts")
private Double ts;
@Schema(description = "事件类型")
@JsonProperty("event")
private String event;
@Schema(description = "事件数据")
@JsonProperty("data")
private Object data;
}
}
// ========== 3. Agent 会话 (Session) - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 创建请求")
public static class SessionCreateReq implements Serializable {
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 列表请求")
public static class SessionListReq implements Serializable {
@Schema(description = "页码", defaultValue = "1")
@JsonProperty("page")
@Builder.Default
private Integer page = 1;
@Schema(description = "每页大小", defaultValue = "10")
@JsonProperty("page_size")
@Builder.Default
private Integer pageSize = 10;
@Schema(description = "排序字段", defaultValue = "create_time")
@JsonProperty("orderby")
@Builder.Default
private String orderby = "create_time";
@Schema(description = "是否降序", defaultValue = "true")
@JsonProperty("desc")
@Builder.Default
private Boolean desc = true;
@Schema(description = "Session ID")
@JsonProperty("id")
private String id;
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "是否返回 DSL")
@JsonProperty("dsl")
@Builder.Default
private Boolean dsl = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 批量删除请求")
public static class SessionBatchDeleteReq implements Serializable {
@Schema(description = "会话 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("ids")
@NotEmpty(message = "ID列表不能为空")
private List<String> ids;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 响应对象")
public static class SessionVO implements Serializable {
@Schema(description = "Session ID")
@JsonProperty("id")
private String id;
@Schema(description = "Agent ID")
@JsonProperty("agent_id")
private String agentId;
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "来源")
@JsonProperty("source")
private String source;
@Schema(description = "DSL 定义")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "消息列表")
@JsonProperty("messages")
private List<Map<String, Object>> messages;
}
// ========== 4. Agent 对话 (Completion) - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Completion 对话请求")
public static class CompletionReq implements Serializable {
@Schema(description = "会话 ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "会话 ID 不能为空")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "用户问题")
@JsonProperty("question")
private String question;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
@Schema(description = "是否返回追踪信息", defaultValue = "false")
@JsonProperty("return_trace")
@Builder.Default
private Boolean returnTrace = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Completion 对话响应")
public static class CompletionVO implements Serializable {
@Schema(description = "会话 ID")
@JsonProperty("id")
private String id;
@Schema(description = "回复内容")
@JsonProperty("content")
private String content;
@Schema(description = "引用来源")
@JsonProperty("reference")
private Map<String, Object> reference;
@Schema(description = "追踪信息")
@JsonProperty("trace")
private List<Object> trace;
}
// ========== 5. Dify 兼容检索 - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Dify 兼容检索请求")
public static class DifyRetrievalReq implements Serializable {
@Schema(description = "知识库 ID")
@JsonProperty("knowledge_id")
private String knowledgeId;
@Schema(description = "查询词")
@JsonProperty("query")
private String query;
@Schema(description = "检索设置")
@JsonProperty("retrieval_setting")
private Map<String, Object> retrievalSetting;
@Schema(description = "元数据过滤条件")
@JsonProperty("metadata_condition")
private Map<String, Object> metadataCondition;
@Schema(description = "是否使用知识图谱")
@JsonProperty("use_kg")
private Boolean useKg;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Dify 兼容检索响应")
public static class DifyRetrievalVO implements Serializable {
@Schema(description = "检索结果列表")
@JsonProperty("records")
private List<Record> records;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "检索记录")
public static class Record implements Serializable {
@Schema(description = "内容")
@JsonProperty("content")
private String content;
@Schema(description = "相似度分数")
@JsonProperty("score")
private Double score;
@Schema(description = "标题")
@JsonProperty("title")
private String title;
@Schema(description = "元数据")
@JsonProperty("metadata")
private Map<String, Object> metadata;
}
}
}
@@ -1,126 +0,0 @@
package xiaozhi.modules.knowledge.dto.bot;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "外部机器人 (Bot) 聚合 DTO")
public class BotDTO {
// ========== 1. SearchBot (检索机器人) ==========
// 对应 /api/v1/searchbots/ask
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "SearchBot 提问请求")
public static class SearchAskReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is RAG?")
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "是否返回引用", defaultValue = "false")
@JsonProperty("quote")
@Builder.Default
private Boolean quote = false;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "SearchBot 提问响应")
public static class SearchAskVO implements Serializable {
@Schema(description = "回答内容")
@JsonProperty("answer")
private String answer;
@Schema(description = "引用来源 (Value 结构通常对应 RetrievalDTO.HitVO)")
@JsonProperty("reference")
private Map<String, Object> reference;
}
// 对应 /api/v1/searchbots/related_questions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "相关问题请求")
public static class RelatedQuestionReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
}
// 对应 /api/v1/searchbots/mindmap
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "思维导图请求")
public static class MindMapReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
}
// ========== 2. AgentBot (嵌入式 Agent) ==========
// 对应 /api/v1/agentbots/{id}/inputs
@Data
@Builder
@AllArgsConstructor
@Schema(description = "AgentBot 输入参数请求")
public static class AgentInputsReq implements Serializable {
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AgentBot 输入参数定义响应")
public static class AgentInputsVO implements Serializable {
@Schema(description = "表单变量定义列表")
@JsonProperty("variables")
private List<Map<String, Object>> variables;
}
// 对应 /api/v1/agentbots/{id}/completions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AgentBot 对话请求")
public static class AgentCompletionReq implements Serializable {
@Schema(description = "输入参数值")
@JsonProperty("inputs")
private Map<String, Object> inputs;
@Schema(description = "用户查询", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "查询内容不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
@Schema(description = "会话 ID")
@JsonProperty("session_id")
private String sessionId;
}
}
@@ -1,50 +0,0 @@
package xiaozhi.modules.knowledge.dto.chat;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 聊天对话请求 DTO (OpenAI 兼容格式)
*/
@Data
@Schema(description = "聊天对话请求")
public class ChatCompletionRequest implements Serializable {
@Schema(description = "模型标识 (对应 agent_id 或 bot_id)", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("model")
private String model;
@Schema(description = "对话消息列表", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("messages")
private List<Message> messages;
@Schema(description = "是否流式返回", defaultValue = "false")
@JsonProperty("stream")
private Boolean stream = false;
@Schema(description = "温度系数 (0-1)", defaultValue = "0.7")
@JsonProperty("temperature")
private Double temperature;
@Schema(description = "Session ID (可选,用于延续会话)")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "其他RAGFlow特定参数 (可选)")
private Map<String, Object> extra;
@Data
public static class Message implements Serializable {
@Schema(description = "角色 (system, user, assistant)", requiredMode = Schema.RequiredMode.REQUIRED)
private String role;
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
}
}
@@ -1,523 +0,0 @@
package xiaozhi.modules.knowledge.dto.chat;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
/**
* 对话管理聚合 DTO
* <p>
* 容器类,内含对话助手、会话和消息的所有请求/响应对象。
* </p>
*/
@Schema(description = "对话管理聚合 DTO")
public class ChatDTO {
// ========== 1. 对话助手 (Assistant/Bot) 相关 ==========
/**
* 提示词配置
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "提示词配置")
public static class PromptConfig implements Serializable {
@Schema(description = "系统提示词", example = "你是一个专业的客服助手...")
@JsonProperty("prompt")
private String systemPrompt;
@Schema(description = "开场白", example = "您好,我是您的智能助手,请问有什么可以帮您?")
private String opener;
@Schema(description = "空结果回复", example = "抱歉,我没有找到相关信息。")
@JsonProperty("empty_response")
private String emptyResponse;
@Schema(description = "是否展示引用", example = "true")
@JsonProperty("show_quote")
private Boolean quote;
@Schema(description = "是否启用 TTS", example = "false")
private Boolean tts;
@Schema(description = "相似度阈值 (0.0 - 1.0)", example = "0.2")
@JsonProperty("similarity_threshold")
private Float similarityThreshold;
@Schema(description = "关键词相似度权重 (0.0 - 1.0)", example = "0.7")
@JsonProperty("keywords_similarity_weight")
private Float vectorSimilarityWeight;
@Schema(description = "检索 Top N", example = "6")
@JsonProperty("top_n")
private Integer topK;
@Schema(description = "Rerank 模型", example = "rerank_model_001")
@JsonProperty("rerank_model")
private String rerankId;
@Schema(description = "是否启用多轮对话优化", example = "false")
@JsonProperty("refine_multiturn")
private Boolean refineMultigraph;
@Schema(description = "变量列表")
private List<Map<String, Object>> variables;
}
/**
* LLM 配置
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "LLM 模型配置")
public static class LLMConfig implements Serializable {
@NotBlank(message = "模型名称不能为空")
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-4")
@JsonProperty("model_name")
private String modelName;
@Schema(description = "温度参数 (0.0 - 2.0)", example = "0.7")
private Float temperature;
@Schema(description = "Top P 采样", example = "0.9")
@JsonProperty("top_p")
private Float topP;
@Schema(description = "最大 Token 数", example = "4096")
@JsonProperty("max_tokens")
private Integer maxTokens;
@Schema(description = "存在惩罚", example = "0.0")
@JsonProperty("presence_penalty")
private Float presencePenalty;
@Schema(description = "频率惩罚", example = "0.0")
@JsonProperty("frequency_penalty")
private Float frequencyPenalty;
}
/**
* 创建助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建助手请求")
public static class AssistantCreateReq implements Serializable {
@NotBlank(message = "助手名称不能为空")
@Schema(description = "助手名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能客服助手")
private String name;
@Schema(description = "助手头像 (Base64 编码)", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "助手描述", example = "这是一个智能客服助手")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
}
/**
* 更新助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "更新助手请求")
public static class AssistantUpdateReq implements Serializable {
@Schema(description = "助手名称", example = "智能客服助手 V2")
private String name;
@Schema(description = "助手头像 (Base64 编码)", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "助手描述", example = "这是一个智能客服助手")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
}
/**
* 查询助手列表请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询助手列表请求")
public static class AssistantListReq implements Serializable {
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "按名称过滤 (模糊匹配)", example = "客服")
private String name;
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
@Schema(description = "按 ID 精确筛选", example = "assistant_001")
private String id;
}
/**
* 助手详情 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "助手详情 VO")
public static class AssistantVO implements Serializable {
@Schema(description = "助手 ID", example = "assistant_001")
private String id;
@Schema(description = "租户 ID", example = "tenant_001")
@JsonProperty("tenant_id")
private String tenantId;
@Schema(description = "助手名称", example = "智能客服助手")
private String name;
@Schema(description = "助手头像", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "关联的知识库列表 (详情)")
private List<SimpleDatasetVO> datasets;
@Schema(description = "助手描述")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
}
/**
* 删除助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "删除助手请求")
public static class AssistantDeleteReq implements Serializable {
@Schema(description = "要删除的助手 ID 列表", example = "[\"assistant_001\", \"assistant_002\"]")
private List<String> ids;
}
// ========== 2. 会话 (Session) 相关 ==========
/**
* 创建会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建会话请求")
public static class SessionCreateReq implements Serializable {
@Schema(description = "会话名称", example = "技术咨询会话")
private String name;
@Schema(description = "用户 ID", example = "user_001")
@JsonProperty("user_id")
private String userId;
}
/**
* 更新会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "更新会话请求")
public static class SessionUpdateReq implements Serializable {
@Schema(description = "会话名称", example = "技术咨询会话 - 更新")
private String name;
}
/**
* 查询会话列表请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询会话列表请求")
public static class SessionListReq implements Serializable {
@Schema(description = "助手 ID", example = "assistant_001")
@JsonProperty("assistant_id")
private String assistantId;
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "按名称过滤", example = "技术")
private String name;
@Schema(description = "排序字段", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
@Schema(description = "会话 ID 精确筛选", example = "session_001")
private String id;
@Schema(description = "用户标识筛选", example = "user_001")
@JsonProperty("user_id")
private String userId;
}
/**
* 会话详情 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "会话详情 VO")
public static class SessionVO implements Serializable {
@Schema(description = "会话 ID", example = "session_001")
private String id;
@Schema(description = "助手 ID", example = "assistant_001")
@JsonProperty("chat_id")
private String chatId;
@Schema(description = "助手 ID (兼容旧版)", example = "assistant_001")
@JsonProperty("assistant_id")
private String assistantId;
@Schema(description = "会话名称", example = "技术咨询会话")
private String name;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "创建日期", example = "2024-05-01 10:00:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新日期", example = "2024-05-01 10:00:00")
@JsonProperty("update_date")
private String updateDate;
@Schema(description = "用户 ID", example = "user_001")
@JsonProperty("user_id")
private String userId;
@Schema(description = "对话历史消息列表")
private List<Map<String, Object>> messages;
}
/**
* 删除会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "删除会话请求")
public static class SessionDeleteReq implements Serializable {
@Schema(description = "要删除的会话 ID 列表", example = "[\"session_001\", \"session_002\"]")
private List<String> ids;
}
// ========== 3. 消息/对话 (Completion) 相关 ==========
/**
* 发送消息请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "发送消息请求")
public static class CompletionReq implements Serializable {
@NotBlank(message = "问题内容不能为空")
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "请介绍一下你们的产品")
private String question;
@Schema(description = "是否使用流式响应 (SSE)", example = "true")
@Builder.Default
private Boolean stream = true;
@NotBlank(message = "会话 ID 不能为空")
@Schema(description = "会话 ID (可选,不传则创建新会话)", example = "session_001")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "是否展示引用", example = "true")
private Boolean quote;
@Schema(description = "指定检索的文档 ID 列表 (逗号分隔)", example = "doc_001,doc_002")
@JsonProperty("doc_ids")
private String docIds;
@Schema(description = "元数据过滤条件")
@JsonProperty("metadata_condition")
private Map<String, Object> metadataCondition;
}
/**
* 消息响应 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "消息响应 VO")
public static class CompletionVO implements Serializable {
@Schema(description = "AI 回答内容")
private String answer;
@Schema(description = "引用信息")
private Reference reference;
@Schema(description = "会话 ID", example = "session_001")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "任务 ID (用于流式响应追踪)", example = "task_001")
@JsonProperty("task_id")
private String taskId;
/**
* 引用信息 (检索命中结果)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "引用信息")
public static class Reference implements Serializable {
@Schema(description = "命中的文档块列表")
private List<xiaozhi.modules.knowledge.dto.document.RetrievalDTO.HitVO> chunks;
@Schema(description = "文档聚合信息")
@JsonProperty("doc_aggs")
private List<DocAgg> docAggs;
}
/**
* 文档聚合信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文档聚合信息")
public static class DocAgg implements Serializable {
@Schema(description = "文档 ID", example = "doc_001")
@JsonProperty("doc_id")
private String docId;
@Schema(description = "文档名称", example = "产品手册.pdf")
@JsonProperty("doc_name")
private String docName;
@Schema(description = "命中次数", example = "3")
private Integer count;
}
}
/**
* 简易知识库 VO (用于 Assistant 列表)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "简易知识库 VO")
public static class SimpleDatasetVO implements Serializable {
@Schema(description = "知识库 ID")
private String id;
@Schema(description = "知识库名称")
private String name;
@Schema(description = "头像")
private String avatar;
@Schema(description = "分块数量")
@JsonProperty("chunk_num")
private Integer chunkNum;
}
}
@@ -1,79 +0,0 @@
package xiaozhi.modules.knowledge.dto.common;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "通用扩展功能 DTO")
public class CommonDTO {
// ========== 1. 引用详情 (detail_share_embedded) ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "引用详情请求")
public static class ReferenceDetailReq implements Serializable {
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "切片 ID 不能为空")
@JsonProperty("chunk_id")
private String chunkId;
@Schema(description = "知识库 ID")
@JsonProperty("knowledge_id")
private String knowledgeId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "引用详情响应")
public static class ReferenceDetailVO implements Serializable {
@Schema(description = "切片 ID")
@JsonProperty("chunk_id")
private String chunkId;
@Schema(description = "完整内容")
@JsonProperty("content_with_weight")
private String contentWithWeight;
@Schema(description = "文档名称")
@JsonProperty("doc_name")
private String docName;
@Schema(description = "图片 ID 列表")
@JsonProperty("img_id")
private String imageId; // 注意:RAGFlow 有时返回 String 有时返回 List,需根据实际情况确认,暂定 String 用于 ID
@Schema(description = "文档 ID")
@JsonProperty("doc_id")
private String docId;
}
// ========== 2. 通用问答 (ask_about) - 调试用 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "通用问答请求 (调试用)")
public static class AskAboutReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is this dataset about?")
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "数据集 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "数据集列表不能为空")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
}
// 响应通常复用 String 或者简单的 Map 结构,视具体实现而定,暂不定义专用 VO
}
@@ -1,363 +0,0 @@
package xiaozhi.modules.knowledge.dto.file;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理聚合 DTO
* <p>
* 容器类,内含文件模块所有请求/响应对象的静态内部类定义。
* </p>
*/
@Schema(description = "文件管理聚合 DTO")
public class FileDTO {
// ========== 请求类 ==========
/**
* 文件上传请求 (对应接口 1: upload)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件上传请求")
public static class UploadReq implements Serializable {
@NotNull(message = "文件不能为空")
@Schema(description = "上传的文件", requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile file;
@Schema(description = "父文件夹 ID (为空则上传到根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
}
/**
* 新建文件夹请求 (对应接口 2: create)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "新建文件夹请求")
public static class CreateReq implements Serializable {
@NotBlank(message = "文件夹名称不能为空")
@Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "新建文件夹")
private String name;
@Schema(description = "父文件夹 ID (为空则创建在根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@NotBlank(message = "类型不能为空")
@Schema(description = "类型: FOLDER", requiredMode = Schema.RequiredMode.REQUIRED, example = "FOLDER")
@Builder.Default
private String type = "FOLDER";
}
/**
* 重命名请求 (对应接口 6: rename)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "重命名请求")
public static class RenameReq implements Serializable {
@NotBlank(message = "文件 ID 不能为空")
@Schema(description = "文件/文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "file_001")
@JsonProperty("file_id")
private String fileId;
@NotBlank(message = "新名称不能为空")
@Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重命名后的文件")
private String name;
}
/**
* 移动请求 (对应接口 7: move)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "移动请求")
public static class MoveReq implements Serializable {
@NotEmpty(message = "源文件 ID 列表不能为空")
@Schema(description = "源文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("src_file_ids")
private List<String> srcFileIds;
@NotBlank(message = "目标文件夹 ID 不能为空")
@Schema(description = "目标文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "folder_002")
@JsonProperty("dest_file_id")
private String destFileId;
}
/**
* 批量删除请求 (对应接口 8: rm)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "批量删除请求")
public static class RemoveReq implements Serializable {
@NotEmpty(message = "文件 ID 列表不能为空")
@Schema(description = "文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("file_ids")
private List<String> fileIds;
}
/**
* 导入知识库请求 (对应接口 9: convert)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "导入知识库请求")
public static class ConvertReq implements Serializable {
@NotEmpty(message = "文件 ID 列表不能为空")
@Schema(description = "文件 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("file_ids")
private List<String> fileIds;
@NotEmpty(message = "知识库 ID 列表不能为空")
@Schema(description = "目标知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"kb_001\"]")
@JsonProperty("kb_ids")
private List<String> kbIds;
}
/**
* 列表查询请求 (对应接口 3: list_files)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "列表查询请求")
public static class ListReq implements Serializable {
@Schema(description = "父文件夹 ID (为空则查询根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@Schema(description = "关键词搜索", example = "文档")
private String keywords;
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "排序字段: create_time / update_time / name / size", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
}
// ========== 响应类 ==========
/**
* 文件/文件夹基础信息 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件/文件夹基础信息")
public static class InfoVO implements Serializable {
@Schema(description = "文件/文件夹 ID", example = "file_001")
private String id;
@Schema(description = "父文件夹 ID", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@Schema(description = "租户 ID", example = "tenant_001")
@JsonProperty("tenant_id")
private String tenantId;
@Schema(description = "创建者 ID", example = "user_001")
@JsonProperty("created_by")
private String createdBy;
@Schema(description = "类型: FOLDER / FILE", example = "FOLDER")
private String type;
@Schema(description = "名称", example = "我的文件夹")
private String name;
@Schema(description = "路径位置", example = "/root/folder")
private String location;
@Schema(description = "文件大小 (字节)", example = "1024")
private Long size;
@Schema(description = "来源类型", example = "local")
@JsonProperty("source_type")
private String sourceType;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
@JsonProperty("update_date")
private String updateDate;
@Schema(description = "文件扩展名", example = "pdf")
private String extension;
}
/**
* 列表响应 VO (对应接口 3: list_files)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件列表响应")
public static class ListVO implements Serializable {
@Schema(description = "总记录数", example = "100")
private Long total;
@Schema(description = "当前父文件夹信息")
@JsonProperty("parent_folder")
private InfoVO parentFolder;
@Schema(description = "文件/文件夹列表")
private List<InfoVO> files;
@Schema(description = "面包屑导航路径")
private List<InfoVO> breadcrumb;
}
/**
* 转换结果项 VO (对应接口 9: convert)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件转换结果项")
public static class ConvertVO implements Serializable {
@Schema(description = "转换记录 ID", example = "convert_001")
private String id;
@Schema(description = "源文件 ID", example = "file_001")
@JsonProperty("file_id")
private String fileId;
@Schema(description = "目标文档 ID", example = "doc_001")
@JsonProperty("document_id")
private String documentId;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
@JsonProperty("update_date")
private String updateDate;
}
/**
* 转换状态 VO (对应接口 10: get_convert_status)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件转换状态")
public static class ConvertStatusVO implements Serializable {
@Schema(description = "转换状态: pending / processing / completed / failed", example = "completed")
private String status;
@Schema(description = "转换进度 (0.0 - 1.0)", example = "1.0")
private Float progress;
@Schema(description = "状态消息", example = "转换完成")
private String message;
}
/**
* 面包屑 VO (对应接口 12: all_parent_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "面包屑导航 (所有父文件夹)")
public static class BreadcrumbVO implements Serializable {
@Schema(description = "父文件夹列表 (从根到当前的路径)")
@JsonProperty("parent_folders")
private List<InfoVO> parentFolders;
}
/**
* 根目录信息 VO (对应接口 10: get_root_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "根目录信息")
public static class RootFolderVO implements Serializable {
@Schema(description = "根文件夹信息")
@JsonProperty("root_folder")
private InfoVO rootFolder;
}
/**
* 父目录信息 VO (对应接口 11: get_parent_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "父目录信息")
public static class ParentFolderVO implements Serializable {
@Schema(description = "父文件夹信息")
@JsonProperty("parent_folder")
private InfoVO parentFolder;
}
}
@@ -149,8 +149,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId); log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
// 使用 Jackson 将 DTO 转为 Map 作为查询参数 // 使用 Jackson 将 DTO 转为 Map 作为查询参数
@SuppressWarnings("unchecked") Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
Map<String, Object> params = objectMapper.convertValue(req, Map.class); });
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params); Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
@@ -174,13 +174,12 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
.pageSize(1) .pageSize(1)
.build(); .build();
@SuppressWarnings("unchecked") Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
Map<String, Object> params = objectMapper.convertValue(req, Map.class); });
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params); Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
Object dataObj = response.get("data"); Object dataObj = response.get("data");
if (dataObj instanceof Map) { if (dataObj instanceof Map<?, ?> dataMap) {
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
List<?> documents = (List<?>) dataMap.get("docs"); List<?> documents = (List<?>) dataMap.get("docs");
if (documents != null && !documents.isEmpty()) { if (documents != null && !documents.isEmpty()) {
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class); return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
@@ -485,16 +484,11 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
@Override @Override
public Integer getDocumentCount(String datasetId) { public Integer getDocumentCount(String datasetId) {
try {
DatasetDTO.InfoVO info = getDatasetInfo(datasetId); DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
if (info != null && info.getDocumentCount() != null) { if (info != null && info.getDocumentCount() != null) {
return info.getDocumentCount().intValue(); return info.getDocumentCount().intValue();
} }
return 0; return 0;
} catch (Exception e) {
log.warn("获取文档数量失败: {}", e.getMessage());
return 0;
}
} }
@Override @Override
@@ -518,7 +512,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return null; return null;
} catch (Exception e) { } catch (Exception e) {
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage()); log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
return null; throw convertToRenException(e);
} }
} }
@@ -572,8 +566,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return new PageData<>(new ArrayList<>(), 0); return new PageData<>(new ArrayList<>(), 0);
} }
Map<String, Object> dataMap = (Map<String, Object>) dataObj; Map<?, ?> dataMap = Map.class.cast(dataObj);
List<Map<String, Object>> documents = (List<Map<String, Object>>) dataMap.get("docs"); List<?> documents = List.class.cast(dataMap.get("docs"));
if (documents == null || documents.isEmpty()) { if (documents == null || documents.isEmpty()) {
// RAGFlow 明确返回了空文档列表,这是合法的"真空" // RAGFlow 明确返回了空文档列表,这是合法的"真空"
return new PageData<>(new ArrayList<>(), 0); return new PageData<>(new ArrayList<>(), 0);
@@ -684,7 +678,9 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase()); dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
} }
if (info.getParserConfig() != null) { if (info.getParserConfig() != null) {
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), Map.class)); dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(),
new TypeReference<Map<String, Object>>() {
}));
} }
return dto; return dto;
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
} }
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId()); DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
if (datasetInfo == null) { if (datasetInfo == null) {
// RAGFlow 端已删除 → 本地级联清理 // RAGFlow 端已确认删除 → 本地级联清理
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId()); log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
cleanupLocalDataset(dto.getDatasetId(), dto.getId()); cleanupLocalDataset(dto.getDatasetId(), dto.getId());
// 标记为已删除,让上层从列表中移除 // 标记为已删除,让上层从列表中移除
@@ -145,8 +145,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage()); log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
dto.setDocumentCount(0); dto.setDocumentCount(0);
dto.setErrorMessage(e.getMessage());
} }
} }
@@ -529,7 +529,8 @@ public class KnowledgeFilesServiceImpl extends BaseServiceImpl<DocumentDao, Docu
adapter.deleteDocument(datasetId, req); adapter.deleteDocument(datasetId, req);
log.info("远程批量删除请求成功"); log.info("远程批量删除请求成功");
} catch (Exception e) { } catch (Exception e) {
log.warn("远程删除请求部分或全部失败: {}", e.getMessage()); log.error("远程删除请求失败,中止本地清理以避免数据不一致: {}", e.getMessage());
throw new RenException(e.getMessage());
} }
// 4. 原子化清理本地影子记录并同步统计数据 // 4. 原子化清理本地影子记录并同步统计数据
@@ -116,13 +116,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
Map<String, Object> requestBody = new HashMap<>(); Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : "gpt-3.5-turbo"); requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
Map<String, Object>[] messages = new Map[1];
Map<String, Object> message = new HashMap<>(); Map<String, Object> message = new HashMap<>();
message.put("role", "user"); message.put("role", "user");
message.put("content", prompt); message.put("content", prompt);
messages[0] = message;
requestBody.put("messages", messages); requestBody.put("messages", List.of(message));
requestBody.put("temperature", temperature != null ? temperature : 0.7); requestBody.put("temperature", temperature != null ? temperature : 0.7);
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000); requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
@@ -212,13 +210,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
Map<String, Object> requestBody = new HashMap<>(); Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : "gpt-3.5-turbo"); requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
Map<String, Object>[] messages = new Map[1];
Map<String, Object> message = new HashMap<>(); Map<String, Object> message = new HashMap<>();
message.put("role", "user"); message.put("role", "user");
message.put("content", prompt); message.put("content", prompt);
messages[0] = message;
requestBody.put("messages", messages); requestBody.put("messages", List.of(message));
requestBody.put("temperature", 0.2); requestBody.put("temperature", 0.2);
requestBody.put("max_tokens", 2000); requestBody.put("max_tokens", 2000);
@@ -368,13 +364,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
Map<String, Object> requestBody = new HashMap<>(); Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : "gpt-3.5-turbo"); requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
Map<String, Object>[] messages = new Map[1];
Map<String, Object> message = new HashMap<>(); Map<String, Object> message = new HashMap<>();
message.put("role", "user"); message.put("role", "user");
message.put("content", prompt); message.put("content", prompt);
messages[0] = message;
requestBody.put("messages", messages); requestBody.put("messages", List.of(message));
requestBody.put("temperature", 0.3); requestBody.put("temperature", 0.3);
requestBody.put("max_tokens", 50); requestBody.put("max_tokens", 50);
@@ -39,9 +39,11 @@ import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity; import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
import lombok.extern.slf4j.Slf4j;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity> public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity>
implements ModelConfigService { implements ModelConfigService {
@@ -360,20 +362,21 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
if (SensitiveDataUtils.isSensitiveField(key)) { if (SensitiveDataUtils.isSensitiveField(key)) {
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) { if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
updatedJson.put(key, value); updatedJson.set(key, value);
} }
} else if (value instanceof JSONObject) { } else if (value instanceof JSONObject) {
// 递归处理嵌套JSON // 递归处理嵌套JSON
mergeJson(updatedJson, key, (JSONObject) value); mergeJson(updatedJson, key, (JSONObject) value);
} else { } else {
// 非敏感字段直接更新 // 非敏感字段直接更新
updatedJson.put(key, value); updatedJson.set(key, value);
} }
} }
// 删除在新JSON中不存在的非敏感字段 // 删除在新JSON中不存在的非敏感字段
for (String oldKey : originalJson.keySet().toArray(new String[0])) { for (String oldKey : originalJson.keySet().toArray(new String[0])) {
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey) && !SensitiveDataUtils.isSensitiveField(oldKey)) { if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey)
&& !SensitiveDataUtils.isSensitiveField(oldKey)) {
updatedJson.remove(oldKey); updatedJson.remove(oldKey);
} }
} }
@@ -392,12 +395,33 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
return value.contains("***"); return value.contains("***");
} }
// 辅助方法:递归合并JSON,保留原始敏感字段 // 辅助方法:递归合并 JSON,保留原始敏感字段
private void mergeJson(JSONObject original, String key, JSONObject updated) { private void mergeJson(JSONObject original, String key, JSONObject updated) {
// 空值检查
if (original == null || updated == null) {
log.warn("mergeJson: original 或 updated 为 null");
return;
}
// 如果 original 中不存在 key,创建一个新的 JSON 对象
if (!original.containsKey(key)) { if (!original.containsKey(key)) {
original.put(key, new JSONObject()); original.set(key, new JSONObject());
}
// 获取 original 中的子对象
Object originalValue = original.get(key);
JSONObject originalChild;
// 检查 originalValue 是否是 JSONObject 类型
if (originalValue instanceof JSONObject) {
originalChild = (JSONObject) originalValue;
} else {
// 如果不是 JSONObject 类型,记录警告并创建新的 JSON 对象
log.warn("mergeJson: key '{}' 的值不是 JSONObject 类型 (实际类型:{}),将创建新对象",
key, originalValue != null ? originalValue.getClass().getSimpleName() : "null");
originalChild = new JSONObject();
original.set(key, originalChild);
} }
JSONObject originalChild = original.getJSONObject(key);
for (String childKey : updated.keySet()) { for (String childKey : updated.keySet()) {
Object childValue = updated.get(childKey); Object childValue = updated.get(childKey);
@@ -406,12 +430,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
} else { } else {
if (!SensitiveDataUtils.isSensitiveField(childKey) || if (!SensitiveDataUtils.isSensitiveField(childKey) ||
(childValue instanceof String && !isMaskedValue((String) childValue))) { (childValue instanceof String && !isMaskedValue((String) childValue))) {
originalChild.put(childKey, childValue); originalChild.set(childKey, childValue);
} }
} }
} }
// 删除在新JSON中不存在的非敏感子字段 // 删除在新 JSON 中不存在的非敏感子字段
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) { for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) { if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
originalChild.remove(oldChildKey); originalChild.remove(oldChildKey);
@@ -164,7 +164,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
@Override @Override
public void delete(List<String> ids) { public void delete(List<String> ids) {
if (modelProviderDao.deleteBatchIds(ids) == 0) { if (modelProviderDao.deleteByIds(ids) == 0) {
throw new RenException(ErrorCode.DELETE_DATA_FAILED); throw new RenException(ErrorCode.DELETE_DATA_FAILED);
} }
} }
@@ -4,16 +4,19 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.config.ShiroFilterConfiguration; import org.apache.shiro.web.config.ShiroFilterConfiguration;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Role;
import jakarta.servlet.Filter; import jakarta.servlet.Filter;
import xiaozhi.modules.security.oauth2.Oauth2Filter; import xiaozhi.modules.security.oauth2.Oauth2Filter;
@@ -39,7 +42,7 @@ public class ShiroConfig {
} }
@Bean("securityManager") @Bean("securityManager")
public SecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) { public WebSecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm); securityManager.setRealm(oAuth2Realm);
securityManager.setSessionManager(sessionManager); securityManager.setSessionManager(sessionManager);
@@ -48,7 +51,8 @@ public class ShiroConfig {
} }
@Bean("shiroFilter") @Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) { public static ShiroFilterFactoryBean shirFilter(@Lazy WebSecurityManager securityManager,
@Lazy SysParamsService sysParamsService) {
ShiroFilterConfiguration config = new ShiroFilterConfiguration(); ShiroFilterConfiguration config = new ShiroFilterConfiguration();
config.setFilterOncePerRequest(true); config.setFilterOncePerRequest(true);
@@ -87,6 +91,7 @@ public class ShiroConfig {
filterMap.put("/user/retrieve-password", "anon"); filterMap.put("/user/retrieve-password", "anon");
// 将config路径使用server服务过滤器 // 将config路径使用server服务过滤器
filterMap.put("/config/**", "server"); filterMap.put("/config/**", "server");
filterMap.put("/device/address-book/call", "server");
filterMap.put("/agent/chat-history/report", "server"); filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/agent/chat-history/download/**", "anon"); filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/chat-summary/**", "server"); filterMap.put("/agent/chat-summary/**", "server");
@@ -100,12 +105,14 @@ public class ShiroConfig {
} }
@Bean("lifecycleBeanPostProcessor") @Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor(); return new LifecycleBeanPostProcessor();
} }
@Bean @Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
@Lazy WebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager); advisor.setSecurityManager(securityManager);
return advisor; return advisor;
@@ -114,7 +114,12 @@ public class SysParamsController {
// 校验mqtt密钥长度 // 校验mqtt密钥长度
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue()); validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
// 如果是系统功能菜单配置,使用特殊处理
if (Constant.SYSTEM_WEB_MENU.equals(dto.getParamCode())) {
sysParamsService.updateSystemWebMenu(dto.getParamValue());
} else {
sysParamsService.update(dto); sysParamsService.update(dto);
}
configService.getConfig(false); configService.getConfig(false);
return new Result<Void>(); return new Result<Void>();
} }
@@ -31,7 +31,7 @@ public class SysUserDTO implements Serializable {
@NotNull(message = "{id.require}", groups = UpdateGroup.class) @NotNull(message = "{id.require}", groups = UpdateGroup.class)
private Long id; private Long id;
@Schema(description = "用户名", required = true) @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{sysuser.username.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysuser.username.require}", groups = DefaultGroup.class)
private String username; private String username;
@@ -40,14 +40,14 @@ public class SysUserDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}", groups = AddGroup.class) @NotBlank(message = "{sysuser.password.require}", groups = AddGroup.class)
private String password; private String password;
@Schema(description = "姓名", required = true) @Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{sysuser.realname.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysuser.realname.require}", groups = DefaultGroup.class)
private String realName; private String realName;
@Schema(description = "头像") @Schema(description = "头像")
private String headUrl; private String headUrl;
@Schema(description = "性别 0:男 1:女 2:保密", required = true) @Schema(description = "性别 0:男 1:女 2:保密", requiredMode = Schema.RequiredMode.REQUIRED)
@Range(min = 0, max = 2, message = "{sysuser.gender.range}", groups = DefaultGroup.class) @Range(min = 0, max = 2, message = "{sysuser.gender.range}", groups = DefaultGroup.class)
private Integer gender; private Integer gender;
@@ -58,11 +58,11 @@ public class SysUserDTO implements Serializable {
@Schema(description = "手机号") @Schema(description = "手机号")
private String mobile; private String mobile;
@Schema(description = "部门ID", required = true) @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{sysuser.deptId.require}", groups = DefaultGroup.class) @NotNull(message = "{sysuser.deptId.require}", groups = DefaultGroup.class)
private Long deptId; private Long deptId;
@Schema(description = "状态 0:停用 1:正常", required = true) @Schema(description = "状态 0:停用 1:正常", requiredMode = Schema.RequiredMode.REQUIRED)
@Range(min = 0, max = 1, message = "{sysuser.status.range}", groups = DefaultGroup.class) @Range(min = 0, max = 1, message = "{sysuser.status.range}", groups = DefaultGroup.class)
private Integer status; private Integer status;
@@ -53,4 +53,19 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
* 初始化服务器密钥 * 初始化服务器密钥
*/ */
void initServerSecret(); void initServerSecret();
/**
* 获取系统功能菜单配置
*
* @param fromCache 是否从缓存获取
* @return 系统功能菜单配置JSON字符串
*/
String getSystemWebMenu(boolean fromCache);
/**
* 更新系统功能菜单配置(自动处理功能相关的插件清理)
*
* @param configJson 新的系统功能菜单配置JSON
*/
void updateSystemWebMenu(String configJson);
} }
@@ -20,6 +20,7 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil; import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysDictDataDao;
import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dao.SysUserDao;
@@ -138,7 +139,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
// 设置更新者和创建者名称 // 设置更新者和创建者名称
if (!userIds.isEmpty()) { if (!userIds.isEmpty()) {
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds); List<SysUserEntity> sysUserEntities = sysUserDao.selectByIds(userIds);
// 把List转成MapMap<Long, String> // 把List转成MapMap<Long, String>
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
SysUserEntity::getUsername, (existing, replacement) -> existing)); SysUserEntity::getUsername, (existing, replacement) -> existing));
@@ -170,7 +171,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
// 先从Redis获取缓存 // 先从Redis获取缓存
String key = RedisKeys.getDictDataByTypeKey(dictType); String key = RedisKeys.getDictDataByTypeKey(dictType);
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key); List<SysDictDataItem> cachedData = JsonUtils.toList(redisUtils.get(key), SysDictDataItem.class);
if (cachedData != null) { if (cachedData != null) {
return cachedData; return cachedData;
} }
@@ -128,7 +128,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
// 设置更新者和创建者名称 // 设置更新者和创建者名称
if (!userIds.isEmpty()) { if (!userIds.isEmpty()) {
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds); List<SysUserEntity> sysUserEntities = sysUserDao.selectByIds(userIds);
// 把List转成MapMap<Long, String> // 把List转成MapMap<Long, String>
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
SysUserEntity::getUsername, (existing, replacement) -> existing)); SysUserEntity::getUsername, (existing, replacement) -> existing));
@@ -22,6 +22,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.SM2Utils; import xiaozhi.common.utils.SM2Utils;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.sys.dao.SysParamsDao; import xiaozhi.modules.sys.dao.SysParamsDao;
import xiaozhi.modules.sys.dto.SysParamsDTO; import xiaozhi.modules.sys.dto.SysParamsDTO;
import xiaozhi.modules.sys.entity.SysParamsEntity; import xiaozhi.modules.sys.entity.SysParamsEntity;
@@ -35,6 +36,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
@Service @Service
public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParamsEntity> implements SysParamsService { public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParamsEntity> implements SysParamsService {
private final SysParamsRedis sysParamsRedis; private final SysParamsRedis sysParamsRedis;
private final AgentPluginMappingService agentPluginMappingService;
@Override @Override
public PageData<SysParamsDTO> page(Map<String, Object> params) { public PageData<SysParamsDTO> page(Map<String, Object> params) {
@@ -270,4 +272,60 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
} }
return true; return true;
} }
@Override
public String getSystemWebMenu(boolean fromCache) {
return getValue(Constant.SYSTEM_WEB_MENU, fromCache);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateSystemWebMenu(String configJson) {
// 获取当前配置
String currentConfig = getSystemWebMenu(false);
Map<String, Object> currentMap = null;
Map<String, Object> newMap = null;
try {
if (StringUtils.isNotBlank(currentConfig)) {
currentMap = JsonUtils.parseMap(currentConfig);
}
if (StringUtils.isNotBlank(configJson)) {
newMap = JsonUtils.parseMap(configJson);
}
} catch (Exception e) {
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
}
// 检查addressBook功能是否被关闭
if (currentMap != null && newMap != null) {
Map<?, ?> currentFeatures = Map.class.cast(currentMap.get("features"));
Map<?, ?> newFeatures = Map.class.cast(newMap.get("features"));
if (currentFeatures != null && newFeatures != null) {
Object currentAddressBookObj = currentFeatures.get("addressBook");
Object newAddressBookObj = newFeatures.get("addressBook");
Boolean currentEnabled = false;
Boolean newEnabled = false;
if (currentAddressBookObj instanceof Map<?, ?> currentAddressBook) {
Object enabled = currentAddressBook.get("enabled");
currentEnabled = enabled != null ? Boolean.class.cast(enabled) : false;
}
if (newAddressBookObj instanceof Map<?, ?> newAddressBook) {
Object enabled = newAddressBook.get("enabled");
newEnabled = enabled != null ? Boolean.class.cast(enabled) : false;
}
// 如果之前是启用状态,现在被禁用,删除所有call_device插件
if (Boolean.TRUE.equals(currentEnabled) && !Boolean.TRUE.equals(newEnabled)) {
agentPluginMappingService.deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE");
}
}
}
// 更新配置
updateValueByCode(Constant.SYSTEM_WEB_MENU, configJson);
}
} }
@@ -57,6 +57,14 @@ public interface TimbreService extends BaseService<TimbreEntity> {
List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName); List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName);
/**
* 获取普通音色或克隆音色配置的首个有效语言。
*
* @param id 音色ID
* @return 默认语言;音色不存在或未配置有效语言时返回null
*/
String getDefaultLanguageById(String id);
/** /**
* 根据ID获取音色名称 * 根据ID获取音色名称
* *
@@ -1,6 +1,7 @@
package xiaozhi.modules.timbre.service.impl; package xiaozhi.modules.timbre.service.impl;
import java.util.*; import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -41,6 +42,8 @@ import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
@Service @Service
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService { public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
private static final Pattern LANGUAGE_SEPARATOR = Pattern.compile("[、;;,]");
private final TimbreDao timbreDao; private final TimbreDao timbreDao;
private final VoiceCloneDao voiceCloneDao; private final VoiceCloneDao voiceCloneDao;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
@@ -114,7 +117,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void delete(String[] ids) { public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids)); baseDao.deleteByIds(Arrays.asList(ids));
} }
@Override @Override
@@ -158,6 +161,32 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs; return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs;
} }
@Override
public String getDefaultLanguageById(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
TimbreEntity timbre = timbreDao.selectById(id);
if (timbre != null) {
return firstNonBlankLanguage(timbre.getLanguages());
}
VoiceCloneEntity voiceClone = voiceCloneDao.selectById(id);
return voiceClone == null ? null : firstNonBlankLanguage(voiceClone.getLanguages());
}
private String firstNonBlankLanguage(String languages) {
if (StringUtils.isBlank(languages)) {
return null;
}
return LANGUAGE_SEPARATOR.splitAsStream(languages)
.map(StringUtils::trimToNull)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
/** /**
* 处理是不是tts模型的id * 处理是不是tts模型的id
*/ */
@@ -127,7 +127,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
@Override @Override
public void delete(String[] ids) { public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids)); baseDao.deleteByIds(Arrays.asList(ids));
} }
@Override @Override

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