Compare commits

...
385 Commits
Author SHA1 Message Date
欣南科技andGitHub ff560a317c Merge pull request #1894 from xinnan-tech/update-doc
update:添加阿里云流式推荐
2025-07-22 18:22:05 +08:00
hrz 6892befa15 update:添加阿里云流式推荐 2025-07-22 18:21:09 +08:00
hrzandGitHub d9b632c031 Merge pull request #1892 from xinnan-tech/py_fix_mcp
update:添加Home Assistant插件超时处理和错误日志
2025-07-22 17:43:33 +08:00
hrzandGitHub 35fd4d2770 Merge pull request #1890 from xinnan-tech/manager-api-mcp
修复mcp工具太多会获取不到内容的bug
2025-07-22 16:45:05 +08:00
3030332422 1b8963f311 update:添加Home Assistant插件超时处理和错误日志 2025-07-22 16:28:12 +08:00
欣南科技andGitHub 710218b5b5 Merge pull request #1716 from xinnan-tech/py_test_tts
update:添加阿里云流式asr
2025-07-22 13:27:41 +08:00
hrz 60cbe1571c update:修复cosyvoice非流式合成方法 2025-07-22 13:25:54 +08:00
hrzandGitHub 6740d6a723 Merge pull request #1885 from xinnan-tech/py_prompt_emoji
Py prompt emoji
2025-07-21 23:18:30 +08:00
hrzandGitHub eb14e50941 Merge pull request #1882 from pupa880/main
添加最简化配置时配置声纹的教程
2025-07-21 23:14:08 +08:00
hrzandGitHub 37ac778ff4 Update voiceprint-integration.md 2025-07-21 23:13:45 +08:00
Sakura-RanChen bc5586a077 update: HuoshanTTS服务器资源释放 2025-07-21 17:46:22 +08:00
JianYu Zheng eb7ac93e72 修复mcp工具太多会获取不到内容的bug
--WebSocketClientManager.java
1.构建websocket对象的时候,添加可以设置缓冲区大小
2.断开连接的日志添加断开原因的内容
--AgentMcpAccessPointServiceImpl.java
1.设置每个websocket的缓冲区为1m,bug原因:缓存区太小,数据量太大,导致直接断开连接
2025-07-21 16:07:56 +08:00
pupa bf8a8bddf1 添加最简化配置时配置声纹的教程 2025-07-21 14:13:54 +08:00
hrz d1badcb28c update:修改版本号 2025-07-21 13:29:17 +08:00
hrz f6e79e17b7 update:更正cosyvoice大模型节点 2025-07-21 12:03:53 +08:00
Sakura-RanChen eead126f7a update: 表情由llm发送,长文本进行约束 2025-07-21 09:30:23 +08:00
hrzandGitHub e5ef61dfd3 Merge pull request #1873 from xinnan-tech/py_test
修复声纹识别场景下的意图识别问题、日志文件的补充
2025-07-20 20:23:29 +08:00
hrz eb130aa57f Merge branch 'py_test_tts' of https://github.com/xinnan-tech/xiaozhi-esp32-server into py_test_tts 2025-07-20 00:29:25 +08:00
hrz c9b10494d8 update:优化配置文件 2025-07-20 00:29:00 +08:00
3030332422 b8136ac1dc fix:修复阿里云流式ASR音频传递问题 2025-07-19 18:17:21 +08:00
3030332422 6d5b53fcd5 fix: 修复阿里云流式ASR终止协议缺失问题 2025-07-19 12:16:12 +08:00
hrz 5cf8eb5e71 update:优化OpenaiASR、GroqASR配置 2025-07-19 01:03:36 +08:00
3030332422 f037f4c7de fix: 修复声纹识别场景下的意图识别问题 2025-07-18 11:34:14 +08:00
3030332422 618be6d108 update:日志文件的补充 2025-07-17 16:08:13 +08:00
hrz 7fb028205f Merge branch 'main' into py_test_tts
# Conflicts:
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-07-17 14:32:50 +08:00
Sakura-RanChen a09e56bbfd Merge branch 'py_test_tts' of https://github.com/xinnan-tech/xiaozhi-esp32-server into py_test_tts 2025-07-17 11:43:49 +08:00
Sakura-RanChen c0c01a0285 数据集回滚 2025-07-17 11:42:51 +08:00
hrzandGitHub af0854a840 Merge pull request #1812 from xinnan-tech/openai-asr
OpenAI asr
2025-07-17 11:31:54 +08:00
hrzandGitHub f70652f165 Merge branch 'py_test_tts' into openai-asr 2025-07-17 11:31:44 +08:00
hrzandGitHub 7c440f47b6 Merge pull request #1852 from xinnan-tech/py_fix_DoubaoStreamASR
修复关于声纹识别的若干问题
2025-07-17 11:22:59 +08:00
hrzandGitHub 6967cc2293 Merge pull request #1855 from pursue-wind/fix_mcp_proxy_err
fix: 修复mcp-proxy命令不存在
2025-07-17 11:22:28 +08:00
hrzandGitHub 97ad1f6f22 Merge pull request #1856 from xinnan-tech/manager-api-mcp
修改智控台删除声纹,声纹服务不在线,无法删除的业务逻辑错误
2025-07-17 11:07:49 +08:00
JianYu Zheng f62f530f97 优化
--AgentVoicePrintServiceImpl.java
 1.重写了构造器引入系统配置线程池对象
 2.删除的时候异步处理删除声纹数据的方法,加快删除接口的返回
2025-07-17 10:49:51 +08:00
JianYu Zheng 815647ae02 补充日志
--AgentVoicePrintServiceImpl.java
2025-07-17 09:49:05 +08:00
JianYu Zheng ce1e3fb04d 修改业务逻辑
--AgentVoicePrintServiceImpl.java 删除声纹的时间,声纹服务不在线或者删除失败,都允许删除
2025-07-17 09:45:27 +08:00
chan 1a86828ae6 fix: 修复mcp-proxy命令不存在 2025-07-17 09:23:41 +08:00
3030332422 8831ea22b7 update:删去一些重复、不用的代码 2025-07-16 16:43:49 +08:00
3030332422 1a411da133 fix:修复FunASR多设备连接时声纹识别配置相互覆盖的问题 2025-07-16 15:42:21 +08:00
Sakura-RanChen e2a7534aa5 update: 优化相关处理 2025-07-16 15:03:53 +08:00
欣南科技andGitHub f8a92e1710 Merge pull request #1845 from xinnan-tech/manager-api-mcp
优化mcp错误提示
2025-07-15 18:36:45 +08:00
3030332422 953a232654 fix: 修复使用豆包流式ASR时,不能使用声纹识别功能的问题。 2025-07-15 18:13:01 +08:00
JianYu Zheng 4afa29554f 优化map错误提示
--AgentMcpAccessPointServiceImpl.java 优化提示
2025-07-15 15:44:43 +08:00
3030332422 dc78057823 fix:修复多模块部署时远程ASR声纹识别功能未初始化的问题 2025-07-15 14:52:56 +08:00
欣南科技andGitHub 53ca586847 Merge pull request #1826 from xinnan-tech/py_fix_log
fix:日志优化的补充
2025-07-15 14:40:54 +08:00
欣南科技andGitHub 4f7e96ed7d Merge pull request #1838 from xinnan-tech/update-doc
update:更新文档
2025-07-15 14:39:47 +08:00
hrz 0397ef3fad update:更新文档 2025-07-15 13:09:04 +08:00
欣南科技andGitHub aa38591d90 Merge pull request #1837 from xinnan-tech/update-doc
update:增加开发团队信息
2025-07-15 12:00:10 +08:00
hrz b87b82bec9 update:增加开发团队信息 2025-07-15 11:59:13 +08:00
hrzandGitHub 2e719e2ccc Merge pull request #1830 from hsycc/main
fix: 修复配置声纹识别地址为域名不带端口, 请求url错误的问题
2025-07-14 23:16:54 +08:00
hsycc 909b41174b fix: 修复配置声纹识别地址为域名不带端口, 请求url错误的问题 2025-07-14 20:13:48 +08:00
Sakura-RanChen 2c33ee5b32 update: 优化文本处理 2025-07-14 17:34:42 +08:00
3030332422 82beae0e59 fix:日志优化的补充 2025-07-14 07:28:05 +08:00
欣南科技andGitHub 7f979e2f52 Merge pull request #1820 from xinnan-tech/update-doc
update:补充声纹识别部署细节
2025-07-12 17:43:59 +08:00
hrz e94eb302a8 update:补充声纹识别部署细节 2025-07-12 17:43:27 +08:00
欣南科技andGitHub 8ffbb614f7 Merge pull request #1819 from xinnan-tech/update-doc
update:补充声纹服务启动的细节
2025-07-12 16:50:10 +08:00
hrz 7a589f4a7b update:补充声纹服务启动的细节 2025-07-12 16:49:32 +08:00
Sakura-RanChen 04ed5ed980 update: 同步非流式处理 10秒超时链接不复用 2025-07-11 18:04:00 +08:00
欣南科技andGitHub e37b094d1e Merge pull request #1813 from xinnan-tech/py_fix_log
Py fix log
2025-07-11 17:29:49 +08:00
欣南科技andGitHub a491ce96cb Merge branch 'main' into py_fix_log 2025-07-11 17:29:37 +08:00
hrz 7be79519b1 update:增加声纹识别演示视频 2025-07-11 17:23:13 +08:00
3030332422 47e4c0f190 fix:日志文件的优化 2025-07-11 16:44:50 +08:00
Sakura-RanChen 8e5d933745 update: 优化音频播放 文本发送 2025-07-11 16:36:47 +08:00
欣南科技andGitHub 6b24844172 Merge pull request #1810 from xinnan-tech/hot-fix
update:优化总结
2025-07-11 15:43:39 +08:00
hrz c6225f3ab8 update:优化总结 2025-07-11 15:42:46 +08:00
欣南科技andGitHub 60c4aa943b Merge pull request #1807 from xinnan-tech/hot-fix
add:声纹识别开启教程
2025-07-11 14:14:32 +08:00
hrz 81d0adf73c add:声纹识别开启教程 2025-07-11 14:13:39 +08:00
欣南科技andGitHub 7dc3929f0e Merge pull request #1806 from xinnan-tech/hot-fix
update:优化声纹识别有结果时内容的展示
2025-07-11 12:15:05 +08:00
hrz 09be3a4de3 update:优化声纹识别有结果时内容的展示 2025-07-11 12:14:25 +08:00
欣南科技andGitHub f7407e46c5 Merge pull request #1805 from xinnan-tech/hot-fix
update:修复二次声纹注册bug
2025-07-11 11:57:25 +08:00
hrz 8f685967d3 update:修复二次声纹注册bug 2025-07-11 11:49:53 +08:00
Sakura-RanChen 23cd453af8 update: aliyun双流改造 待优化长连接机制和文本生成反馈为空 2025-07-11 11:36:58 +08:00
欣南科技andGitHub 8f6dd22a6d Merge pull request #1804 from xinnan-tech/mangger-api-voice-print
声纹识别
2025-07-11 10:19:15 +08:00
hrz 108c87f395 update:优化提示词 2025-07-11 10:15:14 +08:00
hrz cd58b3a39a update:发行新版本 2025-07-11 09:14:34 +08:00
Sakura-RanChen 26a3b51162 fix: 补充文档 2025-07-10 16:07:06 +08:00
3030332422 2fbee33185 update:python对接声纹识别优化 2025-07-10 15:03:49 +08:00
hrz 6da3138814 Merge branch 'main' into py_test_tts 2025-07-10 13:47:53 +08:00
luruxian b2e6156bbb chore(db): 添加新的数据库变更集202507101201
添加新的数据库变更脚本202507101201.sql到变更日志中
2025-07-10 11:44:49 +08:00
luruxian fbd4a22e3e feat(asr): 添加OpenAI和Groq语音识别支持
新增GPT语音识别模型供应器和配置,实现语音转文本功能
更新相关文档说明,包含API申请步骤和使用注意事项
2025-07-10 11:41:20 +08:00
Sakura-RanChen dede73e9a5 fix: id一致 方法流程 2025-07-10 11:34:31 +08:00
JianYu Zheng f499040955 Merge remote-tracking branch 'origin/mangger-api-voice-print' into mangger-api-voice-print 2025-07-10 11:33:28 +08:00
JianYu Zheng 008c446966 添加判断
--AgentVoicePrintServiceImpl.java
 1.声纹音频保存,增加了保存前识别一下声纹是否有相似的声音,有则不保存
 2.声纹音频修改,增加了保存前识别一下声纹是否有相似的声音且相似声纹id和本次修改id不一样的,则不修改
 3.添加了识别声纹http的请求的方法
 --IdentifyVoicePrintResponse.java 识别声纹响应类型
2025-07-10 11:20:32 +08:00
Sakura-RanChen ce358dcd65 fix: 音频播放方法跟随主分支 2025-07-10 10:17:07 +08:00
hrz 01158e67e0 add:智控台返回声纹接口信息 2025-07-10 09:54:16 +08:00
Sakura-RanChen d533fb83eb fix: 插入报错修复,跟随主分支新增列 2025-07-10 09:52:54 +08:00
f4cfa04954 update: TTS复用链接,VAD双阈值判断 (#1742)
* update: TTS复用链接,VAD双阈值判断

* fix: 播放音乐时,引导词卡顿

* update:优化引导词

* update: 优化chat函数流程 优化huoshan处理 会话保持一致性

* fix: 等待时可能已经完成(设置为None),后续对None错误访问

* update:增加书名号

* fix: 打断状态未重置 监听未完成时服务端可能还在发送数据 此时复用链接会接收上个语音的残余 需要两者一同关闭

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-07-09 18:34:04 +08:00
JianYu Zheng ae4387ce3f Merge remote-tracking branch 'origin/mangger-api-voice-print' into mangger-api-voice-print 2025-07-09 18:12:27 +08:00
JianYu Zheng a64b495083 增加判断
--AgentVoicePrintServiceImpl.java 添加,修改声纹音频的是增加了验证这个音频是否属于这个智能体
2025-07-09 17:31:34 +08:00
JianYu Zheng 81cdd0a211 添加新接口和2个新方法
--AgentChatHistoryService.java 添加‘根据音频数据ID获取聊天内容’和‘查询此音频id是否属于此智能体’的方法定义
--AgentChatHistoryServiceImpl.java 实现2个新方法
--AgentController.java 增加一个获取音频内容的接口
2025-07-09 17:29:36 +08:00
Sakura-RanChen 6bfb5f1340 Merge branch 'py_link_huoshan' of https://github.com/xinnan-tech/xiaozhi-esp32-server into py_link_huoshan 2025-07-09 17:05:02 +08:00
Sakura-RanChen 2625133f40 fix: 打断状态未重置 监听未完成时服务端可能还在发送数据 此时复用链接会接收上个语音的残余 需要两者一同关闭 2025-07-09 17:04:42 +08:00
hrzandGitHub 38409b4200 Merge pull request #1755 from xinnan-tech/py_test
update:python单模块部署声纹识别对接
2025-07-09 16:51:26 +08:00
hrzandGitHub 29b4d0daeb Merge branch 'mangger-api-voice-print' into py_test 2025-07-09 16:51:10 +08:00
hrz 8f4f9fe19a update:优化提示词 2025-07-09 16:30:10 +08:00
hrz 7f67459c6e update:增加书名号 2025-07-09 16:26:26 +08:00
JianYu Zheng f47a7b4dbc 修复添加和修改声纹选择的时候,编辑的声纹不在里面,导致无法显示内容的问题
--agent.js 增加查询音频内容的接口
--VoicePrintDialog.vue 修复添加和修改声纹选择的时候,编辑的声纹不在里面,导致无法显示内容的问题
2025-07-09 15:56:07 +08:00
hrz 0bf6926506 update:优化提示词 2025-07-09 15:35:25 +08:00
3030332422 d13fb73c67 update:优化 2025-07-09 14:23:47 +08:00
JianYu Zheng 5e18a46e3f 添加新功能,声纹选择的时候可以听取音频文件
--VoicePrintDialog.vue 添加听取音频的功能
2025-07-09 11:48:02 +08:00
Sakura-RanChen 62e39dd0e2 fix: 等待时可能已经完成(设置为None),后续对None错误访问 2025-07-09 11:18:26 +08:00
hrzandGitHub 203ea89c6d Merge pull request #1788 from xinnan-tech/py_intent_fix
fix: delete未定义的方法报错
2025-07-08 17:54:17 +08:00
Sakura-RanChen 9ddd967776 fix: delete未定义的方法报错 2025-07-08 17:52:51 +08:00
Sakura-RanChen 44593ff3f3 update: 优化chat函数流程 优化huoshan处理 会话保持一致性 2025-07-08 17:48:50 +08:00
JianYu Zheng 3ed13b3ff5 修复漏洞
--AgentChatHistoryServiceImpl.java 用户聊天记录不一定都带有音频,查询时要排除不带音频的最近50条聊天记录
--AgentChatHistoryService.java 修改方法描述
2025-07-08 17:32:44 +08:00
JianYu Zheng 53cfdcc240 Merge remote-tracking branch 'origin/mangger-api-voice-print' into mangger-api-voice-print 2025-07-08 17:15:10 +08:00
JianYu Zheng 0636b95b87 声纹表添加音频字段
--202507081646.sql 添加字段sql
--AgentVoicePrintEntity.java 对应实体更新
--db.changelog-master.yaml 执行添加字段sql的配置
2025-07-08 17:10:57 +08:00
hrzandGitHub cf449a9b60 Merge pull request #1787 from xinnan-tech/system-prompt
强化小智提示词
2025-07-08 17:00:46 +08:00
hrz ab890ada37 update:强化小智提示词 2025-07-08 16:59:38 +08:00
JianYu Zheng b9c136c6ae 修复错误
--VoicePrint.vue
 1.方法调用错误,修改方法名
 2.删除无用的多选框
2025-07-08 16:55:02 +08:00
JianYu Zheng f81a7e8273 智能体设置卡片添加设置声纹的按钮
--DeviceItem.vue
 1.添加声纹设置按钮
 2.添加跳转到声纹页面方法
--home.vue 修复因为添加新按钮导致卡片按钮文字换行样式问题
2025-07-08 16:18:29 +08:00
JianYu Zheng d40d4a08b0 添加声纹管理页面
--VoicePrint.vue 声纹展示页面
--VoicePrintDialog.vue 声纹增改组件
--index.js 注册声纹页面路径
2025-07-08 16:15:52 +08:00
JianYu Zheng 61907d2270 添加新接口方法
--agent.js 添加关于声纹和获取用户列表的接口
 1.添加智能体的声纹
 2.获取指定智能体声纹列表
 3.删除智能体声纹
 4.更新智能体声纹
 5.获取指定智能体用户类型聊天记录
2025-07-08 16:13:13 +08:00
JianYu Zheng 601a90981e 修复错误
--AgentVoicePrintServiceImpl.java
 1.修复事务处理过程出错,返回错误无法被前端页面处理
 2.修复更新声纹方法,更正错误sql
2025-07-08 15:48:20 +08:00
JianYu Zheng 10b7259b13 添加新接口:查看指定智能体最近50条用户聊天记录
--AgentChatHistoryService.java 指定智能体最近50条用户聊天记录定义
--AgentChatHistoryServiceImpl.javaAgentChatHistoryService.java 实现指定智能体最近50条用户聊天记录方法
--AgentChatHistoryType.java 添加智能体聊天记录类型枚举
--AgentChatHistoryUserVO.java 展示用户聊天记录vo
--AgentController.java 添加新接口
2025-07-08 15:44:43 +08:00
CGD 5c83d63fe2 update:声纹识别对接的优化 2025-07-08 11:25:54 +08:00
hrzandGitHub beb7a191c1 Merge pull request #1777 from jlau-ice/feat/add-password-visibility
feat: 登录注册等页面密码输入框添加可见属性
2025-07-07 21:29:10 +08:00
ganbin ee72a328ca feat: 登录注册等页面密码输入框添加可见属性 2025-07-07 18:50:03 +08:00
JianYu Zheng c85419bf06 添加添加声纹增删改查接口
--AgentVoicePrintController.java 添加接口
2025-07-07 15:33:16 +08:00
CGD 155ba92ba6 update:增加了智控台的sql语句,优化了部分配置 2025-07-07 15:29:13 +08:00
JianYu Zheng b201c3cca8 添加数据认证,需要登录用户是此数据所有者才有权限修改,查询,删除
--AgentVoicePrintService.java 修改方法定义
--AgentVoicePrintServiceImpl.java
 1.需要登录用户是此数据所有者才有权限修改,查询,删除
 2.修改发送注册声纹http请求和发送注销声纹的请求的错误说明和添加日志记录
2025-07-07 15:11:37 +08:00
hrzandGitHub c45f47faa0 Update:优化mcp工具读取顺序 (#1775)
* update:发布0.6.3版本

* update:升级sherpa_onnx版本

* update:补充自动更新教程

* update:优化mcp工具读取顺序

* update:优化
2025-07-07 15:04:32 +08:00
JianYu Zheng ee7ea7fca6 添加编程事务,保证数据的统一,提取公共方法
--AgentVoicePrintServiceImpl.java
 1.为保存,修复,删除声纹添加事务
 2.提取发送注销声纹的请求为公共方法
2025-07-07 14:12:28 +08:00
hrzandGitHub 5ee7830f1b Merge pull request #1770 from jlau-ice/fix/vllm-reply-in-English
fix: 修正VLLM本地调用,question参数为空会用英文回复的问题
2025-07-07 11:58:58 +08:00
hrz 8da9dc8a3f update:优化引导词 2025-07-07 11:45:37 +08:00
JianYu Zheng 61ecfd2d92 实现智能体声纹服务层定义的‘获取指定智能体的所有声纹数据’和‘更新智能体的指的声纹数据’定义(未测试)
--AgentVoicePrintServiceImpl.java 实现方法
 1.获取指定智能体的所有声纹数据
 2.更新智能体的指的声纹数据
 3.提取共用方法(获取声纹音频资源数据,发送注册声纹http请求)
2025-07-07 10:58:35 +08:00
ganbin 2535ff6c90 fix: 修正VLLM本地调用,question参数为空会用英文回复的问题 2025-07-07 09:02:59 +08:00
hrzandGitHub 880bc6326a update:补充自动更新教程 (#1766)
* update:发布0.6.3版本

* update:升级sherpa_onnx版本

* update:补充自动更新教程
2025-07-06 15:40:02 +08:00
hrzandGitHub d892212583 Update version (#1765)
* update:发布0.6.3版本

* update:升级sherpa_onnx版本
2025-07-05 23:58:47 +08:00
hrzandGitHub 089acd357d update:发布0.6.3版本 (#1764) 2025-07-05 23:50:26 +08:00
hrzandGitHub d14f6937fa Merge pull request #1753 from myifeng/main
客户端连接成功时不主动发送hello
2025-07-05 20:05:51 +08:00
hrzandGitHub d74813ec80 Update connection.py 2025-07-05 20:05:36 +08:00
hrzandGitHub 1cfba1aa52 update: 单模块部署也能使用mcp接入点 (#1763)
* 优化:OTA接口需要适应最小示例参数 #1757

* update:修复智能体选择的模型不存在导致的500错误 #1752

* update: 单模块部署也能使用mcp接入点
2025-07-05 19:46:04 +08:00
JianYu Zheng 77527d8823 实现智能体声纹服务层定义的‘添加智能体新的声纹’和‘删除智能体的指的声纹’定义
--AgentVoicePrintServiceImpl.java 实现方法
 1.添加智能体新的声纹
 2.删除智能体的指的声纹
 3.共用方法(获取生纹接口URI对象,获取声纹地址基础路径,获取验证Authorization)
2025-07-04 18:04:39 +08:00
JianYu Zheng 5e5ec98995 添加智能体声纹实体,vo,dto,dao,service类
--AgentVoicePrintVO.java 智能体声纹展示vo
--AgentVoicePrintService.java 智能体声纹服务层定义
 1.添加智能体新的声纹
 2.删除智能体的指的声纹
 3.获取指定智能体的所有声纹数据
 4.更新智能体的指的声纹数据
--AgentVoicePrintSaveDTO.java 智能体声纹保存dto
--AgentVoicePrintUpdateDTO.java 智能体声纹修改dto
--AgentVoicePrintEntity.java 智能体声纹实体
--AgentVoicePrintDao.java 智能体声纹持久层
2025-07-04 18:02:19 +08:00
JianYu Zheng 1c2d4f9045 添加新表:智能体声纹表
--202507041018.sql 建表sql语句
--db.changelog-master.yaml 配置文件里添加执行新sql的配置
2025-07-04 17:54:41 +08:00
CGD 3d64152dba update:python单模块部署声纹识别对接 2025-07-04 17:11:44 +08:00
hrzandGitHub 37ecd3da1e 增加docker部署时url地址的说明 (#1754)
* update:优化mcp工具较多时的现实

* update:修改版本号

* update:docker部署manager-api,填写备注
2025-07-04 17:02:19 +08:00
myifeng 22e9e4c352 客户端连接成功时不主动发送hello 2025-07-04 15:27:38 +08:00
Sakura-RanChen 7e5a2b4549 fix: 播放音乐时,引导词卡顿 2025-07-04 11:27:49 +08:00
JianYu Zheng 036dde5bb0 添加新的参数:声纹接口地址
--202507031602.sql 添加声纹接口地址参数sql
--Constant.java 对应新参数的常量
--SysParamsController.java 验证声纹接口地址参数是否符合规则
--db.changelog-master.yaml 添加执行新的sql文件
2025-07-03 16:34:50 +08:00
Sakura-RanChen 69cac9d40a update: TTS复用链接,VAD双阈值判断 2025-07-03 09:16:06 +08:00
hrzandGitHub 04b132816c update:修改版本号
* update:优化mcp工具较多时的现实

* update:修改版本号
2025-07-01 18:16:21 +08:00
CGD 039badd265 update:添加了阿里云CosyVoice流式TTS 2025-07-01 18:14:36 +08:00
hrzandGitHub f08ed1bbb7 Merge pull request #1729 from xinnan-tech/fix-vllm
update:优化mcp工具较多时的显示
2025-07-01 17:31:58 +08:00
欣南科技andGitHub d59a769084 Merge pull request #1728 from xinnan-tech/redis-password
update:允许在docker-compose文件里设置redis密码
2025-07-01 16:35:20 +08:00
hrz 7a8fe1074c update:允许在docker-compose文件里设置redis密码 2025-07-01 16:34:37 +08:00
欣南科技andGitHub 3fc256ed9a Merge pull request #1727 from xinnan-tech/mcp-update
update:支持中文名称的mcp工具
2025-07-01 16:16:03 +08:00
hrz 18eed4417c update:支持中文名称的mcp工具 2025-07-01 16:07:35 +08:00
欣南科技andGitHub 20adbadc02 Merge pull request #1718 from xinnan-tech/newsnow
可配置新闻来源
2025-06-30 17:13:13 +08:00
hrz c68384be69 update:区分本地化部署和非本地化部署tts的配置字段 2025-06-30 16:56:56 +08:00
CGD 90a9631450 update:添加阿里云流式asr 2025-06-30 14:41:53 +08:00
hrz 7619979046 update:简化新闻源配置 2025-06-30 14:24:46 +08:00
hrzandGitHub 54ccae8665 Merge pull request #1518 from shane04111/add_local_tts_change_voice
update: 添加本地tts支援面板的"角色音色"
2025-06-30 10:48:15 +08:00
hrzandGitHub 3015b01740 Merge branch 'newsnow' into add_local_tts_change_voice 2025-06-30 10:48:06 +08:00
hrzandGitHub 43d47a4f09 Merge pull request #1552 from liammazy/main
增加timeout配置项,单位为秒
2025-06-30 10:46:50 +08:00
hrzandGitHub 96abc34a92 Merge branch 'newsnow' into main 2025-06-30 10:44:39 +08:00
hrzandGitHub 26553b8875 Update openai.py 2025-06-30 10:41:15 +08:00
hrzandGitHub 5e3187e36b Merge pull request #1673 from goxofy/main
update: newsnow 插件支持前端配置新闻源  && fix issues/1658
2025-06-30 10:10:28 +08:00
hrzandGitHub eba8c3122f Merge branch 'newsnow' into main 2025-06-30 10:10:19 +08:00
hrzandGitHub f1a3f39782 Merge pull request #1712 from ifhaveif/main
🐞 fix: 只清空当前数据库,不清空整个数据库
2025-06-30 10:04:01 +08:00
xiongyonghui 0880f78002 🐞 fix: 只清空当前数据库,不清空整个数据库
-- emptyAll.lua
1、当redis被多个项目使用时,清空了其它项目的库(影响其它项目)
2025-06-30 00:17:39 +08:00
欣南科技andGitHub 1c16d497f9 Merge pull request #1706 from xinnan-tech/update-doc
update:增加MCP接入点的视频demo
2025-06-28 20:36:44 +08:00
hrz faaae95197 update:增加MCP接入点的视频demo 2025-06-28 20:36:05 +08:00
欣南科技andGitHub d5e0e8ab40 Merge pull request #1704 from xinnan-tech/py_websoket_link
update:优化超时资源优化
2025-06-27 23:50:25 +08:00
hrz 500b0ebde3 update:去除无关日志 2025-06-27 23:47:17 +08:00
hrz 6f6c39da23 update:优化超时资源优化 2025-06-27 23:38:05 +08:00
CGDandGitHub f8ccc7a92f Merge pull request #1703 from xinnan-tech/py_websoket_link
fix: 退出过快问题
2025-06-27 17:26:38 +08:00
Sakura-RanChen 95a678bb18 fix: 退出过快问题 2025-06-27 16:39:53 +08:00
欣南科技andGitHub e43ce6ba3c Merge pull request #1700 from xinnan-tech/manager-api-mcp
update:优化接入点连接跳转方式
2025-06-27 15:31:09 +08:00
hrz 7663b0b969 update:优化接入点连接跳转方式 2025-06-27 15:30:18 +08:00
欣南科技andGitHub ba898b0874 Merge pull request #1699 from xinnan-tech/manager-api-mcp
update:优化mcp地址的校验
2025-06-27 15:16:58 +08:00
hrz 04843010bc update:优化mcp地址的校验 2025-06-27 15:14:41 +08:00
欣南科技andGitHub 41c06efeee Merge pull request #1698 from xinnan-tech/manager-api-mcp
Manager api mcp
2025-06-27 14:59:27 +08:00
hrz d0425fa31a update:优化设备端读取mcp接入点工具 2025-06-27 14:57:49 +08:00
hrz 08753b97df update:页面显示接入点功能 2025-06-27 11:33:12 +08:00
hrz a86fef5f28 update:智能体获取mcp接入点工具列表 2025-06-27 10:06:40 +08:00
hrzandGitHub c5c2f59b31 Merge pull request #1688 from xinnan-tech/opt_tools
fix: 处理config_functions的类型转换,避免初始化错误
2025-06-26 18:32:00 +08:00
hrz 69ee8ab438 添加mcp工具列表接口 2025-06-26 18:29:07 +08:00
CGD 4a24bc8c21 fix: 处理config_functions的类型转换,避免初始化错误 2025-06-26 17:37:51 +08:00
hrz 1d293244d3 update:智控台增加mcp接入点的配置 2025-06-26 16:54:26 +08:00
hrz 6812c5ac40 update:更改版本号 2025-06-26 16:34:30 +08:00
TinKandGitHub eeb5ed920a Merge pull request #10 from goxofy/manual-add-device
fix
2025-06-26 16:29:29 +08:00
hrzandGitHub 90ad3e019f Merge pull request #1680 from xinnan-tech/opt_tools
MCP接入点完成
2025-06-26 16:27:42 +08:00
Tink 3af453c2df fix 2025-06-26 16:26:45 +08:00
hrz 3071d2bdfa update:完成mcp接入点对接 2025-06-26 16:25:39 +08:00
TinKandGitHub 90aad4f131 Merge pull request #9 from goxofy/manual-add-device
fix
2025-06-26 16:24:55 +08:00
Tink f69b1b34f1 fix 2025-06-26 16:22:21 +08:00
TinKandGitHub ece2c4b49f Merge pull request #8 from goxofy/manual-add-device
fix
2025-06-26 16:07:57 +08:00
Tink 033c8f9bee fix 2025-06-26 16:06:36 +08:00
TinKandGitHub 1b9ca95a0e Merge pull request #7 from goxofy/manual-add-device
UpdateWrapper
2025-06-26 16:00:31 +08:00
Tink 7e86a9f8a9 UpdateWrapper 2025-06-26 15:54:26 +08:00
TinKandGitHub 9dee744498 Merge pull request #6 from goxofy/manual-add-device
fix last_connected_at
2025-06-26 15:35:51 +08:00
Tink 32fa71e8f1 fix last_connected_at 2025-06-26 15:24:57 +08:00
hrz ec4694f859 update:优化服务端mcp 2025-06-26 14:35:12 +08:00
TinKandGitHub 717386f10a Merge pull request #5 from goxofy/manual-add-device
fix id in ai_device
2025-06-26 14:14:07 +08:00
Tink fe071fe41a fix id in ai_device 2025-06-26 14:13:29 +08:00
TinKandGitHub 560a7d083b Merge pull request #4 from goxofy/manual-add-device
manualAddDevice
2025-06-26 13:41:46 +08:00
Tink 4428c1a298 manualAddDevice 2025-06-26 13:40:01 +08:00
TinKandGitHub 760fd2fdc5 Merge pull request #3 from goxofy/manual-add-device
手动添加设备增加后端接口
2025-06-26 13:37:06 +08:00
Tink 84c11a281a 手动添加设备增加后端接口 2025-06-26 13:36:11 +08:00
TinKandGitHub ad682ca0c4 Merge pull request #2 from goxofy/manual-add-device
fix RequestService
2025-06-26 12:27:46 +08:00
Tink 2a349ca9ef fix RequestService 2025-06-26 12:25:19 +08:00
hrz da8435cfea update:视觉结果优化 2025-06-26 11:42:13 +08:00
hrz 0f36daf1fd update:优化工具回复 2025-06-26 11:27:23 +08:00
TinKandGitHub 1f3c90e9e5 Merge pull request #1 from goxofy/manual-add-device
manual-add-device
2025-06-26 11:16:23 +08:00
Tink 6ec7a1fe09 manual-add-device 2025-06-26 11:09:17 +08:00
hrz 2f78acaf4d update:优化iot操作 2025-06-26 09:34:00 +08:00
hrz 9412a26bfc update:优化工具目录结构 2025-06-26 09:28:29 +08:00
hrz 2348d9ceb3 update:统一工具注册及调用 2025-06-25 18:27:08 +08:00
JianYu Zheng 61b4b6617b 优化:提取共用方法,实现部分getAgentMcpToolsList内容
--AgentMcpAccessPointServiceImpl.java
1.获取URI对象,统一2个方法获取URI对象的异常处理
2.实现getAgentMcpToolsList部分内容
2025-06-25 17:39:01 +08:00
JianYu Zheng a8620f21a0 优化:方法分割提取
--AgentMcpAccessPointServiceImpl.java 方法分割提取共用
1.获取密钥方法
2.获取智能体mcp接入点url部分
3.获取对智能体id加密的token
2025-06-25 17:17:26 +08:00
Tink 78dc266eee change newsnow plugin doc 2025-06-25 16:49:54 +08:00
JianYu Zheng 0631fe37ea 添加新接口:获取智能体的Mcp接入点地址(/agent/mcp/address/{audioId})
--AgentMcpAccessPointServiceImpl.java 实现获取智能体的mcp接入点地址定义
--AgentMcpAccessPointController.java 添加获取智能体的Mcp接入点地址接口
2025-06-25 16:37:04 +08:00
Tink 8828f8401d newsnow 插件支持前端配置新闻源 2025-06-25 16:32:49 +08:00
JianYu Zheng 21339aba42 添加智能体mcp接入点接口方法定义
--AgentMcpAccessPointService.java
1.获取智能体的mcp接入点地址定义
2.获取智能体的mcp接入点已有的工具列表定义
2025-06-25 16:16:39 +08:00
JianYu Zheng 660231978f 添加一个哈希加密的工具类
--HashEncryptionUtil.java
1.指定哈希算法进行加密方法
2.使用md5进行加密
2025-06-25 16:15:06 +08:00
JianYu Zheng dde203b158 添加了一个新的系统参数,mcp接入点参数
--Constant.java mcp接入点参数的key常量
--SysParamsServiceImpl.java 添加了mcp参数验证方法,修改是进行验证
2025-06-25 11:21:41 +08:00
JianYu Zheng 0ee69fa2b2 添加了http发送工具类
--HttpSendUtils.java
添加了发送get请求,获取返回的body转换成字符串 方法
发送post请求,参数为json格式。取返回的body转换成字符串 方法
2025-06-25 11:06:36 +08:00
hrzandGitHub 8c1a8f7d55 Merge pull request #1663 from xinnan-tech/py_iot_fix
update:iot设备多指令适配
2025-06-24 16:52:10 +08:00
欣南科技andGitHub ef03b665c9 Merge pull request #1666 from xinnan-tech/aes_utils
update:新增AES加密方法,和python端AES一致
2025-06-24 16:48:56 +08:00
hrz b87906234d update:新增AES加密方法,和python端AES一致 2025-06-24 16:26:14 +08:00
CGD 896b318c49 update:iot设备多指令适配 2025-06-24 14:55:34 +08:00
CGDandGitHub b3d441c385 Merge pull request #1661 from xinnan-tech/web_fuction_MCP
update: mcp接入点页面实现
2025-06-24 14:41:12 +08:00
Sakura-RanChen 96de670bfe update: mcp接入点页面实现 2025-06-24 14:39:18 +08:00
欣南科技andGitHub bc2fc35cc9 Merge pull request #1642 from xinnan-tech/py_tts_listen
update:更新版本号
2025-06-20 17:42:53 +08:00
hrz 993d5395b2 update:更新版本号 2025-06-20 17:42:14 +08:00
欣南科技andGitHub 67f0b828ea Merge pull request #1641 from xinnan-tech/py_tts_listen
update:修复部分doubaoasr出现400错误的问题
2025-06-20 17:20:51 +08:00
hrz 879c1267b6 update:修复部分doubaoasr出现400错误的问题 2025-06-20 17:20:08 +08:00
hrzandGitHub 46c53e36a6 Merge pull request #1640 from xinnan-tech/py_tts_listen
fix: 反复打断任务没有被清除,vad四帧语音识别
2025-06-20 17:17:19 +08:00
hrz 09f6605cfe update:更新版本号 2025-06-20 17:17:09 +08:00
Sakura-RanChen 12cee4027a fix: 反复打断任务没有被清除,vad四帧语音识别 2025-06-20 16:25:08 +08:00
欣南科技andGitHub 979fea0d60 Merge pull request #1627 from xinnan-tech/update-remark
更新智控台两款意图识别的说明
2025-06-19 16:56:38 +08:00
hrz 94553c54bd 更新智控台两款意图识别的说明 2025-06-19 16:53:39 +08:00
hrzandGitHub 248db31c8b Merge pull request #1618 from xinnan-tech/py_bug_fix
update:添加MCP重连机制
2025-06-18 22:40:25 +08:00
hrzandGitHub 24bfa1ca15 Merge pull request #1615 from xinnan-tech/py_fix_type
fix: 豆包流式decode错误
2025-06-18 17:46:47 +08:00
欣南科技andGitHub 1e97a8febc Merge pull request #1620 from xinnan-tech/hot-fix
update:修复mcp返回json的bug
2025-06-18 17:28:51 +08:00
hrz 2742f2e1ff update:修复mcp返回json的bug 2025-06-18 17:28:21 +08:00
Sakura-RanChen 0a5ae70a7c 二次错误提醒 2025-06-18 16:32:58 +08:00
CGD 22d53bd36e update:添加MCP重连机制 2025-06-18 16:29:28 +08:00
Sakura-RanChen ebf68929ce update: 错误信息读取 2025-06-18 14:03:18 +08:00
Sakura-RanChen ef3b373211 fix: 豆包流式decode错误 2025-06-18 11:06:11 +08:00
hrzandGitHub 5012a51e1d Merge pull request #1609 from xinnan-tech/py_fix_type
Py fix type
2025-06-18 09:39:38 +08:00
Sakura-RanChen fb1f476a3c fix: 智控台切换默认视觉模型报错 2025-06-18 09:24:52 +08:00
Sakura-RanChen 1a31c8cd1d update: 视觉模块直接识别输出,不经过二次LLM 2025-06-17 16:13:24 +08:00
欣南科技andGitHub 3e491c7d79 Merge pull request #1602 from xinnan-tech/py_fix_bug
update:更新版本号
2025-06-17 00:11:52 +08:00
hrz 33345817d2 update:更新版本号 2025-06-17 00:10:55 +08:00
hrzandGitHub 82973a685a Merge pull request #1601 from xinnan-tech/py_fix_bug
fix:修复“设备使用iot协议,意图识别无法正确调用”的问题
2025-06-16 23:49:58 +08:00
hrz e1d245068c update:优化权限 2025-06-16 23:47:26 +08:00
hrzandGitHub b74517ffb0 Merge pull request #1591 from CaixyPromise/fix/comment-invalid
fix: 智控台备注功能无效
2025-06-16 23:34:24 +08:00
hrz 231ae8dfa6 update:修复iot不更新的bug 2025-06-16 23:16:40 +08:00
CGD 4ee9a47d41 fix:修复“设备使用iot协议,意图识别无法正确调用”的问题 2025-06-16 18:09:13 +08:00
Sakura-RanChen d8bf5cdedf fix: openAi schema不支持list 2025-06-16 16:26:49 +08:00
Sakura-RanChenandGitHub 755e0edd44 Merge pull request #1596 from xinnan-tech/py_fix_bug
fix:修复意图识别因缺少 home_assistant 配置导致部分功能错误将意图识别成“home_assistant ”的bug
2025-06-16 14:52:53 +08:00
CGD 178df82693 fix:修复意图识别因缺少 home_assistant 配置导致功能全部失效的 bug 2025-06-16 14:48:52 +08:00
Sakura-RanChenandGitHub a88c3b2032 Merge pull request #1594 from xinnan-tech/py_fix_bug
fix:修复数据库中两个字符串的排序规则(collation)不一致问题
2025-06-16 11:10:18 +08:00
CGD 513396b905 fix:修复数据库中两个字符串的排序规则(collation)不一致问题 2025-06-16 11:09:01 +08:00
caixypromise 9885d4758b fix: 修复设备管理无法提交备注信息的问题。
chore: 将设备自动更新状态接口重构为设备信息更新接口,以此实现备注、自动更新合并在一个接口内
2025-06-16 03:27:12 +08:00
欣南科技andGitHub f5583717d5 Merge pull request #1573 from xinnan-tech/update-doc
update:优化智控台配置HomeAssistant操作手册
2025-06-13 14:36:46 +08:00
hrz da3076e5f7 update:优化智控台配置HomeAssistant操作手册 2025-06-13 14:29:08 +08:00
欣南科技andGitHub 3b77e1ddf1 Merge pull request #1572 from xinnan-tech/hot-fix
update:优化报错信息
2025-06-13 13:48:45 +08:00
hrz 3763a615f0 update:优化报错信息 2025-06-13 13:22:17 +08:00
欣南科技andGitHub f90e07976e Merge pull request #1570 from xinnan-tech/hot-fix
update:优化视觉接口认证校验
2025-06-13 12:55:37 +08:00
hrz 45b7658634 update:优化视觉接口认证校验 2025-06-13 12:53:58 +08:00
欣南科技andGitHub 70323936e9 Merge pull request #1569 from xinnan-tech/update-doc
Update doc
2025-06-13 11:23:05 +08:00
hrz 44f64fa336 update:修改版本号 2025-06-13 11:21:53 +08:00
hrz 2bd8afcc89 update:优化文档内容 2025-06-13 11:21:00 +08:00
欣南科技andGitHub 5fdab2522c Merge pull request #1567 from xinnan-tech/agent-plugin
update:优化插件代码
2025-06-13 10:50:19 +08:00
hrzandGitHub ef7bd8bd19 Merge pull request #1559 from xinnan-tech/py_fix_mcp-type
修复MCP工具调用参数类型错误问题;"logger中vad和asr动态变化"的补充
2025-06-13 10:49:41 +08:00
hrzandGitHub 9aa76cad69 Merge pull request #1566 from xinnan-tech/py_bug_fix
fix:通过“当会话ID为None时,自动生成新的会话ID”,解决“首次从智控台连接后,无法播放音乐”的问题
2025-06-13 10:48:49 +08:00
hrz f51477bf18 update:优化插件代码 2025-06-13 10:47:47 +08:00
CGD 2a8ec27ba6 update:通过“当会话ID为None时,自动生成新的会话ID”,解决“首次从智控台连接后,无法播放音乐”的问题 2025-06-13 10:34:36 +08:00
欣南科技andGitHub 2ff7fbbe6f Merge pull request #1564 from xinnan-tech/agent-plugin
Agent plugin
2025-06-13 09:48:38 +08:00
hrz 8a2fe7c20e update:设备列表优化 2025-06-13 09:24:13 +08:00
hrz 0fc8f02c6e update:优化空密钥的提示提示方式 2025-06-12 23:11:42 +08:00
hrz 3e8246ac41 update:优化表单样式 2025-06-12 23:07:42 +08:00
hrz 1adb2fe0f6 清除无效变量 2025-06-12 22:00:23 +08:00
hrz 9c3e1a6ff6 update:优化ha工具获取密钥的方式 2025-06-12 21:43:30 +08:00
hrz e943b07344 update:创建智能体设置默认的插件 2025-06-12 16:52:56 +08:00
shane0411 0ca1a6286c Merge remote-tracking branch 'upstream/main' into add_local_tts_change_voice 2025-06-12 12:36:13 +08:00
hrz 535c088404 Merge branch 'agent-plugin' of https://github.com/xinnan-tech/xiaozhi-esp32-server into agent-plugin 2025-06-12 11:12:12 +08:00
hrz f615621ffe update:不限时默认配置 2025-06-12 11:11:43 +08:00
hrzandGitHub f3d0df6772 Merge pull request #1556 from jlau-ice/fix/prevent-dialog-close-on-outside-click
fix: 防止在单击外部未保存的更改时关闭对话框
2025-06-12 11:03:24 +08:00
CGD 3b34d687a6 "fix: 修复MCP工具调用参数类型错误问题,无参调用时提供默认空字典" 2025-06-12 10:52:07 +08:00
CGD d3ba8d4385 uodate:"logger中vad和asr动态变化"的补充 2025-06-12 10:41:14 +08:00
CGDandGitHub 25ccf7fed3 Merge pull request #1557 from xinnan-tech/py_fix_logger
fix:  logger中vad和asr动态变化
2025-06-12 10:16:06 +08:00
Sakura-RanChen 6139c51b60 fix: logger中vad和asr动态变化 2025-06-12 09:54:07 +08:00
ganbin 77ded65157 fix: 防止在单击外部未保存的更改时关闭对话框 2025-06-12 09:03:48 +08:00
hrz 1836284223 Merge branch 'main' into agent-plugin 2025-06-11 23:07:33 +08:00
hrz c43afc6b39 update:优化页面显示 2025-06-11 23:05:38 +08:00
hrzandGitHub 4b0902017b Merge pull request #1554 from xinnan-tech/py_fix_mcp
fix:MCP 工具的支持能力,并优化意图识
2025-06-11 22:42:34 +08:00
hrz b2d26b9481 update:优化插件显示 2025-06-11 22:09:21 +08:00
CGD 68dbc91d54 fix:MCP 工具的支持能力,并优化意图识 2025-06-11 22:02:57 +08:00
shane0411 a4aea694d4 Merge remote-tracking branch 'upstream/main' into add_local_tts_change_voice 2025-06-11 14:29:35 +08:00
Liam Mazy 0f31bfc78f 增加timeout配置项,单位为秒 2025-06-11 14:05:54 +08:00
hrz 80554ecff4 Merge branch 'main' into agent-plugin 2025-06-11 09:32:56 +08:00
欣南科技andGitHub 0e57aad12d Merge pull request #1546 from xinnan-tech/hot-fix
修复创建智能体的默认视觉模型
2025-06-10 23:03:22 +08:00
hrz 742db377f6 修复创建智能体的默认视觉模型 2025-06-10 23:01:31 +08:00
欣南科技andGitHub a0250c8191 Merge pull request #1543 from xinnan-tech/hot-fix
fix:#1538
2025-06-10 22:20:27 +08:00
hrz 65b2656112 fix: #1538 2025-06-10 22:15:46 +08:00
hrz 1c4e5f0eed 修复 #1538 2025-06-10 22:12:39 +08:00
欣南科技andGitHub 224a06803f Merge pull request #1540 from xinnan-tech/py_wakeup_Unicode
update:优化“更新唤醒词回复配置”中的过滤表情字符
2025-06-10 17:24:05 +08:00
欣南科技andGitHub 8534a6d509 Merge pull request #1537 from bigbencat/fix-vllm-update
Fix vllm update:修复更新智能体时无法保存视觉模型的问题
2025-06-10 17:20:32 +08:00
hrzandGitHub f3dbb1f930 Merge branch 'main' into fix-vllm-update 2025-06-10 17:19:55 +08:00
CGD fedb6f2da2 update:优化“更新唤醒词回复配置”中的过滤表情字符 2025-06-10 16:38:21 +08:00
brucelee aa7303c707 fix(agent): 修复更新智能体时无法保存视觉模型的问题 2025-06-10 16:09:27 +08:00
Sakura-RanChenandGitHub 4b99ca5c5a Merge pull request #1534 from xinnan-tech/py_wakeup_Unicode_fix
解决GBK编码保存问题;音频格式修改
2025-06-10 14:55:34 +08:00
CGD d49462152b update:音频格式修改 2025-06-10 14:36:31 +08:00
CGD 33faeee75d update:通过过滤“更新唤醒词响应配置”时文本中的表情符号,解决GBK编码保存问题 2025-06-10 14:14:02 +08:00
欣南科技andGitHub 8cb15dcc7e Merge pull request #1530 from xinnan-tech/update-doc
update:纠正最简化部署的端口号是8003
2025-06-10 11:20:10 +08:00
hrz cef5813318 update:纠正最简化部署的端口号是8003 2025-06-10 11:19:30 +08:00
CGDandGitHub c43ee8025e Merge pull request #1529 from xinnan-tech/py_wakeup_text
Py wakeup text
2025-06-10 11:16:18 +08:00
shane0411 052d8c8962 feat: 添加音频路径音频文本的提示 2025-06-09 19:36:57 +08:00
shane0411 2eb5539c9b feat: 新增音频路径音频文本欄位,提供給本地tts選取音色
https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/1503#issuecomment-2954952028
2025-06-09 19:18:58 +08:00
shane0411 be6fba5963 feat: 添加本地tts支援面板的"角色音色" 2025-06-09 15:20:27 +08:00
Sakura-RanChen b273181ba6 隐藏滑动条 2025-06-09 15:17:37 +08:00
欣南科技andGitHub f5c3c51fe2 Merge pull request #1515 from xinnan-tech/hot-fix
update:优化带有方括号的字符语音转换
2025-06-09 12:36:24 +08:00
hrz 3e7c200a0b update:优化带有方括号的字符语音转换 2025-06-09 12:35:25 +08:00
hrz 152dbe9dec update:字段管理添加VLLM和Plugin选项 2025-06-09 11:39:24 +08:00
hrz 24b744b27e Merge branch 'main' into agent-plugin
# Conflicts:
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-06-09 10:33:46 +08:00
欣南科技andGitHub 250afc0a5c Merge pull request #1511 from xinnan-tech/update-doc
Update doc
2025-06-08 19:33:29 +08:00
hrz c22a3ed092 迁移技术文档到main目录下 2025-06-08 19:32:50 +08:00
hrz 7a23ae4b84 update:更新技术文档 2025-06-08 19:31:47 +08:00
hrzandGitHub 201462d743 Merge pull request #1510 from xinnan-tech/py_HuoshanDoubleStreamTTS_markdown_fix
fix:使用“clear_markdown”方法修复“使用火山流式TTS中markdown 语法被 TTS 读出”的问题
2025-06-08 17:06:13 +08:00
CGD 3d1768100f fix:使用“clear_markdown”方法修复“使用火山流式TTS中markdown 语法被 TTS 读出”的问题 2025-06-08 15:40:18 +08:00
欣南科技andGitHub 3fbfbeaf0f Merge pull request #1508 from xinnan-tech/hot-fix
update:修复windows平台无法使用fcntl模块的问题
2025-06-08 00:21:27 +08:00
hrz 7163abbbe3 update:修复windows平台无法使用fcntl模块的问题 2025-06-09 00:20:49 +08:00
欣南科技andGitHub 423626a6bf Merge pull request #1504 from xinnan-tech/fix-mcp-name
Fix mcp name
2025-06-07 16:00:49 +08:00
hrz 182acc0787 update:替换描述里涉及到的原始方法名称 2025-06-08 15:51:57 +08:00
hrzandGitHub 705aef462c Merge pull request #1502 from whats2000/main
update: 修正 MCP tool naming 問題 (OpenAI SDK 命名規範)
2025-06-07 14:40:08 +08:00
whats2000andGitHub 1f092dd80e update: 修正 MCP tool naming 問題 (OpenAI SDK 命名規範)
Fix MCP tool naming for OpenAI
2025-06-07 13:48:10 +08:00
whats2000 4d0ddd7ff3 fix: sanitize MCP tool names for OpenAI 2025-06-07 13:46:35 +08:00
欣南科技andGitHub 2a212ae759 Merge pull request #1500 from xinnan-tech/fix-stream-mem
Fix stream mem
2025-06-07 11:59:45 +08:00
hrz 1d57e75d28 update:单流式tts聊天记录上报优化 2025-06-08 11:59:06 +08:00
hrz ac1e254eed update:智控台开启唤醒词加速 2025-06-08 11:50:09 +08:00
欣南科技andGitHub 6074431364 Merge pull request #1499 from xinnan-tech/py_Text
update:优化流式前帧发送
2025-06-07 02:34:18 +08:00
hrz d5fc4d48b4 update:优化流失前帧发送 2025-06-08 02:33:07 +08:00
hrz 61232e7dda update:优化空asr语音 2025-06-08 01:09:32 +08:00
hrz 594b4f1d75 add:增加psutil依赖 2025-06-08 00:21:30 +08:00
hrz 98174bcc16 update:【流式tts】增加【非流式】方法,用于测试及生成文件的场景 2025-06-08 00:06:40 +08:00
hrz 29d90df9ea Merge branch 'main' into py_Text
# Conflicts:
#	main/xiaozhi-server/core/handle/helloHandle.py
#	main/xiaozhi-server/core/providers/tts/base.py
2025-06-07 22:48:23 +08:00
hrz 50e6e4817b update:优化唤醒词答复 2025-06-07 22:08:22 +08:00
欣南科技andGitHub c6a88e41e1 Merge pull request #1498 from xinnan-tech/py_Text
update:优化空asr语音
2025-06-07 01:10:13 +08:00
欣南科技andGitHub ae1c41ba82 Merge pull request #1497 from xinnan-tech/py_Text
add:增加psutil依赖
2025-06-07 00:22:11 +08:00
欣南科技andGitHub 837bb74576 Merge pull request #1496 from xinnan-tech/py_Text
优化唤醒词答复速度
2025-06-07 00:12:21 +08:00
Sakura-RanChen a8f559b22c update: tts_one_sentence单句文本分段 2025-06-06 16:29:57 +08:00
brucelee 992eb89d1a 新增依赖文件 requirements_bruce.txt,包含多个库的版本信息以支持项目运行。 2025-06-06 15:39:25 +08:00
hrzandGitHub 1284418c18 Merge pull request #1481 from xinnan-tech/py_AEC_test
funasr添加内存检测判断;修改日志模块,日志轮转(单个文件10MB限制)
2025-06-06 15:02:46 +08:00
hrzandGitHub 3eb3a4502f Update fun_local.py 2025-06-06 14:08:38 +08:00
欣南科技andGitHub 6023af2f21 Merge pull request #1488 from xinnan-tech/fix-wakeup-bug
update:修复被唤醒词打断bug
2025-06-05 20:55:28 +08:00
hrz 2cd90d8066 update:修复被唤醒词打断bug 2025-06-05 20:54:53 +08:00
hrzandGitHub c1dfb540a3 Merge pull request #1486 from pursue-wind/main
fix: 使用deepseek r1-0528模型时,tools_call为空list,tools_call[0]报错
2025-06-05 17:07:59 +08:00
hrzandGitHub bc44ea8757 Update connection.py 2025-06-05 17:07:29 +08:00
Pursue WindandGitHub 77743ef8ba Merge branch 'xinnan-tech:main' into main 2025-06-05 16:34:39 +08:00
chan f8de052d54 fix: 使用deepseek r1-0528模型时,tools_call为空list,tools_call[0]报错 2025-06-05 16:33:47 +08:00
欣南科技andGitHub cb4eb21551 Merge pull request #1485 from xinnan-tech/py_linkerai_single
update:添加免费流式tts(灵犀流式)
2025-06-05 16:26:23 +08:00
hrz 8357d7abc3 update:添加免费流式tts(灵犀流式) 2025-06-05 16:25:40 +08:00
欣南科技andGitHub 5a01bfcdbf Merge pull request #1484 from xinnan-tech/py_linkerai_single
Py linkerai single
2025-06-05 16:13:21 +08:00
hrz 01eb416a03 update:智控台添加免费流式TTS(linkerai) 2025-06-05 16:02:16 +08:00
Sakura-RanChen dca02c1f4b update: 唤醒音频兼容 2025-06-05 14:11:50 +08:00
hrz 2654802bb0 update:流式播放 2025-06-05 11:09:09 +08:00
hrz d7eecfdcea update:优化代码 2025-06-05 10:49:25 +08:00
CGD 8df9846ad7 update:改为采用日志轮转机制,同时添加自动清理 2025-06-05 09:46:54 +08:00
hrz e43c0135a7 Merge branch 'main' into py_linkerai_single 2025-06-05 09:26:00 +08:00
hrz adf1a47945 update:优化配置说明 2025-06-05 09:25:30 +08:00
hrz 492e0b952e update:删除无效引用 2025-06-05 09:03:17 +08:00
hrzandGitHub ecc9d154d8 Merge pull request #1404 from CaixyPromise/main
feat: 智控台智能体级插件/工具调用改造-#1358
2025-06-04 23:52:54 +08:00
hrzandGitHub e776966ef9 Merge branch 'agent-plugin' into main 2025-06-04 23:52:33 +08:00
hrzandGitHub 98df2e5486 Update AgentServiceImpl.java 2025-06-04 23:50:02 +08:00
hrzandGitHub ca1beb956a Merge pull request #1458 from nodexiaoyu/add-technical-overview-doc
It sounds like you're looking for a detailed technical overview of th…
2025-06-04 23:38:37 +08:00
欣南科技andGitHub c694d3d6f1 Merge pull request #1478 from xinnan-tech/hot-fix
update:优化文档
2025-06-04 22:04:07 +08:00
hrz a314d03870 update:优化文档 2025-06-04 22:03:34 +08:00
Sakura-RanChen 23cb7616d9 update: 更改to_tts保存临时文件判断 2025-06-04 16:46:49 +08:00
CGD fba758ea36 update:恢复最初的日志模块 2025-06-04 15:42:52 +08:00
CGD 0268f90e9c update:修改日志模块,使其按日期和大小分割的日志文件名 2025-06-04 14:46:52 +08:00
CGD 4cc7247c37 update:funasr实例之前添加内存检测判断 2025-06-04 14:35:21 +08:00
google-labs-jules[bot] 0269620e66 Sure, here is the rewritten message:
Add detailed technical overview documentation (Chinese)

This document provides a comprehensive technical overview of the xiaozhi-esp32-server project in Chinese, including:
- Overall architecture
- In-depth analysis of xiaozhi-server, manager-api, and manager-web components, covering their purpose, core technologies, and key implementation aspects.
- Data flow and interaction mechanisms.
- Summary of key features.
- Deployment and configuration overview.
2025-06-03 08:50:30 +00:00
google-labs-jules[bot] 10ebe975cf It sounds like you're looking for a detailed technical overview of the xiaozhi-esp32-server project. I can help you put together documentation that includes:
-   The overall architecture.
-   Deep dives into the xiaozhi-server, manager-api, and manager-web components, covering their purpose, core technologies, and key implementation aspects.
-   How data flows and how different parts of the system interact.
-   A summary of the key features.
-   An overview of how to deploy and configure it.

Let me know if this is what you had in mind!
2025-06-03 08:45:40 +00:00
Sakura-RanChen 0bc609cac4 优化实时编码 2025-06-03 11:20:54 +08:00
Sakura-RanChen 92affd6e13 待优化 2025-05-30 15:22:41 +08:00
Sakura-RanChen 3130044909 test 2025-05-29 09:05:08 +08:00
caixypromise fac1c4db7b chore: deleted debug logs. 2025-05-29 01:46:37 +08:00
caixypromise ae1b90dfd7 chore: deleted debug logs. 2025-05-29 01:28:07 +08:00
caixypromise ec55de429c fix: fix the changeSet.id to commit datetime 2025-05-29 01:05:38 +08:00
caixypromise d62b957189 chore: commit to resolve commit conflict 2025-05-29 01:03:26 +08:00
caixypromise 599ce19ace feat: 智控台智能体级插件/工具调用改造。
新增支持从控制台控制大模型插件工具与配置插件工具的管理能力。
关联issue: issue(#1358)
2025-05-29 00:58:20 +08:00
220 changed files with 13517 additions and 3211 deletions
+1
View File
@@ -174,3 +174,4 @@ main/xiaozhi-server/mysql
uploadfile uploadfile
*.json *.json
.vscode .vscode
.cursor
+1
View File
@@ -21,6 +21,7 @@ RUN apt-get update && \
# 从构建阶段复制Python包和前端构建产物 # 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/bin/mcp-proxy /usr/local/bin/mcp-proxy
# 复制应用代码 # 复制应用代码
COPY main/xiaozhi-server . COPY main/xiaozhi-server .
+51 -20
View File
@@ -3,10 +3,10 @@
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1> <h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
<p align="center"> <p align="center">
本项目为开源智能硬件项目 本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/> <a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/> 根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
帮助您快速搭建小智服务器 支持MCP接入点和声纹识别
</p> </p>
<p align="center"> <p align="center">
@@ -37,6 +37,14 @@
</a> </a>
</p> </p>
<p align="center">
By Professor Siyuan Liu Research and Development Group ( South China University of Technology)
</br>
刘思源教授团队研发(华南理工大学)
</br>
<img src="./docs/images/hnlg.jpg" alt="华南理工大学" width="50%">
</p>
--- ---
## 适用人群 👥 ## 适用人群 👥
@@ -144,8 +152,18 @@
</a> </a>
</td> </td>
<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>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -170,8 +188,8 @@
#### 🚀 部署方式选择 #### 🚀 部署方式选择
| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 | | 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|---------|------|---------|---------|---------|---------| |---------|------|---------|---------|---------|---------|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - | | **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) | | **全模块安装** | 智能对话、IOT、MCP接入点、声纹识别、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据 > 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
@@ -186,27 +204,31 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
#### 🚩 配置说明和推荐 #### 🚩 配置说明和推荐
> [!Note] > [!Note]
> 本项目默认的配置是`入门全免费`设置,如果想效果更优,推荐使用`流式配置`。 > 本项目提供两种配置方案:
> >
> 本项目自`0.5.2`版本,已支持使用流式配置,相比`0.5`版本以前,响应速度提升约`2.5秒` > 1. `入门全免费`配置:适合个人家庭使用,所有组件均采用免费方案,无需额外付费。
>
> 2. `流式配置`:适合演示、培训、超过2个并发等场景,采用流式处理技术,响应速度更快,体验更佳。
>
> 自`0.5.2`版本起,项目支持流式配置,相比早期版本,响应速度提升约`2.5秒`,显著改善用户体验。
| 模块名称 | 入门全免费设置 | 流式配置 | | 模块名称 | 入门全免费设置 | 流式配置 |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| ASR(语音识别) | FunASR(本地) | 👍DoubaoStreamASR(火山流式语音识别) | | ASR(语音识别) | FunASR(本地) | 👍FunASRServer 或 👍DoubaoStreamASR |
| LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍DoubaoLLM(火山doubao-1-5-pro-32k-250115) | | LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍DoubaoLLM(火山doubao-1-5-pro-32k-250115) |
| VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) | | VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) |
| TTS(语音合成) | EdgeTTS(微软语音) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) | | TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) 或 👍AliyunStreamTTS(阿里云流式语音合成) |
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) | | Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) | | Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
#### 🔧 测试工具 #### 🔧 测试工具
本项目提供以下测试工具,帮助您验证系统和选择合适的模型: 本项目提供以下测试工具,帮助您验证系统和选择合适的模型:
| 工具名称 | 位置 | 使用方法 | 功能说明 | | 工具名称 | 位置 | 使用方法 | 功能说明 |
|:---:|:---|:---:|:---:| |:---:|:---|:---:|:---:|
| 音频交互测试工具 | 位于`main/xiaozhi-server/test/test_page.html` | 使用谷歌浏览器直接打开 | 测试音频播放和接收功能,验证Python端音频处理是否正常 | | 音频交互测试工具 | mainxiaozhi-servertesttest_page.html | 使用谷歌浏览器直接打开 | 测试音频播放和接收功能,验证Python端音频处理是否正常 |
| 模型响应测试工具1 | 位于`main/xiaozhi-server/performance_tester.py` | 执行 `python performance_tester.py` | 测试ASR(语音识别)、LLM(大模型)、TTS(语音合成)三个核心模块的响应速度 | | 模型响应测试工具1 | mainxiaozhi-serverperformance_tester.py | 执行 `python performance_tester.py` | 测试ASR(语音识别)、LLM(大模型)、TTS(语音合成)三个核心模块的响应速度 |
| 模型响应测试工具2 | 位于`main/xiaozhi-server/performance_tester_vllm.py` | 执行 `python performance_tester_vllm.py` | 测试VLLM(视觉模型)的响应速度 | | 模型响应测试工具2 | mainxiaozhi-serverperformance_tester_vllm.py | 执行 `python performance_tester_vllm.py` | 测试VLLM(视觉模型)的响应速度 |
> 💡 提示:测试模型速度时,只会测试配置了密钥的模型。 > 💡 提示:测试模型速度时,只会测试配置了密钥的模型。
@@ -216,13 +238,14 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 功能模块 | 描述 | | 功能模块 | 描述 |
|:---:|:---| |:---:|:---|
| 核心服务架构 | 基于WebSocket和HTTP服务器,提供完整的控制台管理和认证系统 | | 核心架构 | 基于WebSocket和HTTP服务器,提供完整的控制台管理和认证系统 |
| 语音交互系统 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 | | 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
| 智能对话系统 | 支持多种LLM(大语言模型),实现智能对话 | | 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
| 视觉感知系统 | 支持多种VLLM(视觉大模型),实现多模态交互 | | 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
| 意图识别系统 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 | | 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 |
| 意图识别 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 |
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 | | 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
| IOT/MCP控制协议 | 支持设备注册管理、智能控制接口,同时支持IOT、MCP控制协议 | | 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
| 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理 | | 管理后台 | 提供Web管理界面,支持用户管理、系统配置和设备管理 |
| 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 | | 测试工具 | 提供性能测试工具、视觉模型测试工具和音频交互测试工具 |
| 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 | | 部署支持 | 支持Docker部署和本地部署,提供完整的配置文件管理 |
@@ -277,7 +300,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 使用方式 | 支持平台 | 免费平台 | | 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS | EdgeTTS、CosyVoiceSiliconflow(部分) | | 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | | 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
--- ---
@@ -299,6 +322,14 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
--- ---
### Voiceprint 声纹识别
| 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:|
| 本地使用 | 3D-Speaker | 3D-Speaker |
---
### Memory 记忆存储 ### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
+126 -70
View File
@@ -3,17 +3,17 @@
<h1 align="center">Xiaozhi Backend Service xiaozhi-esp32-server</h1> <h1 align="center">Xiaozhi Backend Service xiaozhi-esp32-server</h1>
<p align="center"> <p align="center">
This project provides backend services for the open-source smart hardware project This project is based on human-machine symbiotic intelligence theory and technology to develop intelligent terminal hardware and software systems<br/>providing backend services for the open-source intelligent hardware project
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/> <a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/>
Implemented using Python, Java, and Vue according to the <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Xiaozhi Communication Protocol</a><br/> Implemented using Python, Java, and Vue according to the <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Xiaozhi Communication Protocol</a><br/>
Helps you quickly set up your Xiaozhi server Supports MCP endpoints and voiceprint recognition
</p> </p>
<p align="center"> <p align="center">
<a href="./README.md">中文</a> <a href="./README.md">中文</a>
· <a href="./docs/FAQ.md">FAQ</a> · <a href="./docs/FAQ.md">FAQ</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a> · <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a>
· <a href="./README_en.md#deployment-documentation">Deployment Guide</a> · <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Deployment Docs</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Release Notes</a> · <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Release Notes</a>
</p> </p>
<p align="center"> <p align="center">
@@ -37,41 +37,49 @@ Helps you quickly set up your Xiaozhi server
</a> </a>
</p> </p>
<p align="center">
By Professor Siyuan Liu Research and Development Group (South China University of Technology)
</br>
刘思源教授团队研发(华南理工大学)
</br>
<img src="./docs/images/hnlg.jpg" alt="South China University of Technology" width="50%">
</p>
--- ---
## Target Users 👥 ## Target Users 👥
This project requires ESP32 hardware devices. If you have purchased ESP32-related hardware, successfully connected to Brother Xia's backend service, and want to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you. This project requires ESP32 hardware devices to work. If you have purchased ESP32-related hardware, successfully connected to Brother Xia's deployed backend service, and want to build your own `xiaozhi-esp32` backend service independently, then this project is perfect for you.
Want to see it in action? Check out these videos 🎥 Want to see the usage effects? Click the videos below 🎥
<table> <table>
<tr> <tr>
<td> <td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank"> <a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture> <picture>
<img alt="Xiaozhi esp32 connecting to own backend model" src="docs/images/demo1.png" /> <img alt="Xiaozhi ESP32 connecting to own backend model" src="docs/images/demo1.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank"> <a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture> <picture>
<img alt="Custom voice" src="docs/images/demo2.png" /> <img alt="Custom voice timbre" 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="Using Cantonese" src="docs/images/demo3.png" /> <img alt="Using Cantonese for communication" src="docs/images/demo3.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank"> <a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture> <picture>
<img alt="Control home appliances" src="docs/images/demo5.png" /> <img alt="Controlling home appliances" src="docs/images/demo5.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -87,14 +95,14 @@ Want to see it in action? Check out these videos 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank"> <a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
<picture> <picture>
<img alt="Custom voice" src="docs/images/demo6.png" /> <img alt="Custom voice timbre" src="docs/images/demo6.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank"> <a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture> <picture>
<img alt="Play music" src="docs/images/demo7.png" /> <img alt="Playing music" src="docs/images/demo7.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -108,14 +116,14 @@ Want to see it in action? Check out these videos 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank"> <a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture> <picture>
<img alt="IOT command control" src="docs/images/demo9.png" /> <img alt="IOT command control devices" src="docs/images/demo9.png" />
</picture> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank"> <a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture> <picture>
<img alt="News broadcast" src="docs/images/demo0.png" /> <img alt="News broadcasting" src="docs/images/demo0.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -131,7 +139,7 @@ Want to see it in action? Check out these videos 🎥
<td> <td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank"> <a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture> <picture>
<img alt="Photo recognition" src="docs/images/demo12.png" /> <img alt="Photo recognition of objects" src="docs/images/demo12.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -143,20 +151,29 @@ Want to see it in action? Check out these videos 🎥
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP endpoint" src="docs/images/demo13.png" />
</picture>
</a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="Voiceprint recognition" src="docs/images/demo14.png" />
</picture>
</a>
</td> </td>
</tr> </tr>
</table> </table>
--- ---
## Warning ⚠️ ## Warnings ⚠️
1. This project is open-source software. This software has no commercial relationship with any third-party API service providers (including but not limited to speech recognition, large models, speech synthesis, and other platforms) and does not provide any form of guarantee for their service quality or financial security. 1. This project is open-source software. This software has no commercial partnership with any third-party API service providers (including but not limited to speech recognition, large models, speech synthesis, and other platforms) that it interfaces with, and does not provide any form of guarantee for their service quality or financial security. It is recommended that users prioritize service providers with relevant business licenses and carefully read their service agreements and privacy policies. This software does not host any account keys, does not participate in fund flows, and does not bear the risk of recharge fund losses.
It is recommended that users prioritize service providers with relevant business licenses and carefully read their service agreements and privacy policies. This software does not host any account keys, does not participate in fund transfers, and does not bear the risk of recharge fund losses.
2. This project's functionality is not complete and has not passed network security testing. Please do not use it in production environments. If you deploy this project for learning in a public network environment, please ensure necessary protection measures are in place. 2. The functionality of this project is not complete and has not passed network security assessment. Please do not use it in production environments. If you deploy this project for learning purposes in a public network environment, please ensure necessary protection measures are in place.
--- ---
@@ -164,18 +181,19 @@ It is recommended that users prioritize service providers with relevant business
![Banners](docs/images/banner2.png) ![Banners](docs/images/banner2.png)
This project provides two deployment methods. Please choose according to your specific needs: This project provides two deployment methods. Please choose based on your specific needs:
#### 🚀 Deployment Method Selection #### 🚀 Deployment Method Selection
| Deployment Method | Features | Suitable Scenarios | Deployment Guide | Requirements | Video Tutorial | | Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|---------|------|---------|---------|---------|---------| |---------|------|---------|---------|---------|---------|
| **Simplified Installation** | Smart dialogue, IOT functionality, data stored in configuration files | Low-configuration environment, no database needed | [Docker Version](./docs/Deployment.md#method-1-docker-server-only) / [Source Code Deployment](./docs/Deployment.md#method-2-local-source-code-server-only) | 2 cores 4G if using `FunASR`, 2 cores 2G if using all APIs | - | | **Simplified Installation** | Intelligent dialogue, IOT, MCP, visual perception | Low-configuration environments, data stored in config files, no database required | [Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
| **Full Module Installation** | Smart dialogue, IOT, OTA, Control Panel, data stored in database | Complete functionality experience | [Docker Version](./docs/Deployment_all.md#method-1-docker-full-modules) / [Source Code Deployment](./docs/Deployment_all.md#method-2-local-source-code-full-modules) | 4 cores 8G if using `FunASR`, 2 cores 4G if using all APIs | [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) / [Local Source Code Auto-Update Tutorial](./docs/dev-ops-integration.md) | | **Full Module Installation** | Intelligent dialogue, IOT, MCP endpoints, voiceprint recognition, visual perception, OTA, intelligent control console | Complete functionality experience, data stored in database |[Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
> 💡 Note: Below are the test platforms deployed with the latest code. You can flash and test if needed. Concurrent users: 6, data will be cleared daily
> 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily.
``` ```
Control Panel Address: https://2662r3426b.vicp.fun Intelligent Control Console Address: https://2662r3426b.vicp.fun
Service Test Tool: https://2662r3426b.vicp.fun/test/ Service Test Tool: https://2662r3426b.vicp.fun/test/
OTA Interface Address: https://2662r3426b.vicp.fun/xiaozhi/ota/ OTA Interface Address: https://2662r3426b.vicp.fun/xiaozhi/ota/
@@ -184,51 +202,69 @@ Websocket Interface Address: wss://2662r3426b.vicp.fun/xiaozhi/v1/
#### 🚩 Configuration Description and Recommendations #### 🚩 Configuration Description and Recommendations
> [!Note] > [!Note]
> The default configuration of this project is `Entry Level Free` settings. For better results, we recommend using `Full Streaming Configuration`. > This project provides two configuration schemes:
> >
> Since version `0.5.2`, this project supports full streaming throughout the entire lifecycle. Compared to versions before `0.5`, response speed has improved by approximately `2.5 seconds` > 1. `Entry Level Free Settings`: Suitable for personal and home use, all components use free solutions, no additional payment required.
>
> 2. `Streaming Configuration`: Suitable for demonstrations, training, scenarios with more than 2 concurrent users, etc. Uses streaming processing technology for faster response speed and better experience.
>
> Starting from version `0.5.2`, the project supports streaming configuration. Compared to earlier versions, response speed is improved by approximately `2.5 seconds`, significantly improving user experience.
| Module Name | Entry Level Free Settings | Full Streaming Configuration | | Module Name | Entry Level Free Settings | Streaming Configuration |
|---------|---------|------| |:---:|:---:|:---:|
| ASR(Speech Recognition) | FunASR(Local) | ✅DoubaoASR(Volcano Streaming Speech Recognition) | | ASR(Speech Recognition) | FunASR(Local) | 👍FunASRServer or 👍DoubaoStreamASR |
| LLM(Large Language Model) | ChatGLMLLM(Zhipu glm-4-flash) | DoubaoLLM(Volcano doubao-1-5-pro-32k-250115) | | LLM(Large Model) | ChatGLMLLM(Zhipu glm-4-flash) | 👍DoubaoLLM(Volcano doubao-1-5-pro-32k-250115) |
| VLLM(Vision Large Model) | ChatGLMVLLM(Zhipu glm-4v-flash) | ✅ChatGLMVLLM(Zhipu glm-4v-flash) | | VLLM(Vision Large Model) | ChatGLMVLLM(Zhipu glm-4v-flash) | 👍QwenVLVLLM(Qwen qwen2.5-vl-3b-instructh) |
| TTS(Speech Synthesis) | EdgeTTS(Microsoft Speech) | HuoshanDoubleStreamTTS(Volcano Double Streaming Speech Synthesis) | | TTS(Speech Synthesis) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano dual-stream speech synthesis) |
| Intent(Intent Recognition) | function_call(Function Call) | function_call(Function Call) | | Intent(Intent Recognition) | function_call(Function calling) | function_call(Function calling) |
| Memory(Memory Function) | mem_local_short(Local Short-term Memory) | mem_local_short(Local Short-term Memory) | | Memory(Memory function) | mem_local_short(Local short-term memory) | mem_local_short(Local short-term memory) |
#### 🔧 Testing Tools
This project provides the following testing tools to help you verify the system and choose suitable models:
| Tool Name | Location | Usage Method | Function Description |
|:---:|:---|:---:|:---:|
| Audio Interaction Test Tool | main》xiaozhi-server》test》test_page.html | Open directly with Google Chrome | Tests audio playback and reception functions, verifies if Python-side audio processing is normal |
| Model Response Test Tool 1 | main》xiaozhi-server》performance_tester.py | Execute `python performance_tester.py` | Tests response speed of three core modules: ASR(speech recognition), LLM(large model), TTS(speech synthesis) |
| Model Response Test Tool 2 | main》xiaozhi-server》performance_tester_vllm.py | Execute `python performance_tester_vllm.py` | Tests VLLM(vision model) response speed |
> 💡 Note: When testing model speed, only models with configured keys will be tested.
--- ---
## Feature List ✨ ## Feature List ✨
### Implemented ✅ ### Implemented ✅
| Feature Module | Description | | Feature Module | Description |
|---------|------| |:---:|:---|
| Communication Protocol | Based on `xiaozhi-esp32` protocol, implements data interaction through WebSocket | | Core Architecture | Based on WebSocket and HTTP servers, provides complete console management and authentication system |
| Dialogue Interaction | Supports wake-up dialogue, manual dialogue, and real-time interruption. Auto-sleep after long periods of no dialogue | | Voice Interaction | Supports streaming ASR(speech recognition), streaming TTS(speech synthesis), VAD(voice activity detection), supports multi-language recognition and voice processing |
| Intent Recognition | Supports LLM intent recognition, function call, reducing hard-coded intent judgment | | Voiceprint Recognition | Supports multi-user voiceprint registration, management, and recognition, processes in parallel with ASR, real-time speaker identity recognition and passes to LLM for personalized responses |
| Multi-language Recognition | Supports Mandarin, Cantonese, English, Japanese, Korean (default using FunASR) | | Intelligent Dialogue | Supports multiple LLM(large language models), implements intelligent dialogue |
| LLM Module | Supports flexible LLM module switching, default using ChatGLMLLM, can also use Ali Bailian, DeepSeek, Ollama, etc. | | Visual Perception | Supports multiple VLLM(vision large models), implements multimodal interaction |
| TTS Module | Supports EdgeTTS (default), Volcano Engine Doubao TTS, and other TTS interfaces | | Intent Recognition | Supports LLM intent recognition, Function Call function calling, provides plugin-based intent processing mechanism |
| Memory Function | Supports ultra-long memory, local summary memory, and no memory modes | | Memory System | Supports local short-term memory, mem0ai interface memory, with memory summarization functionality |
| IOT Function | Supports managing registered device IOT functionality, supports smart IoT control based on dialogue context | | Tool Calling | Supports client IOT protocol, client MCP protocol, server MCP protocol, MCP endpoint protocol, custom tool functions |
| Control Panel | Provides Web management interface, supports agent management, user management, system configuration, etc. | | Management Backend | Provides Web management interface, supports user management, system configuration, and device management |
| Testing Tools | Provides performance testing tools, vision model testing tools, and audio interaction testing tools |
| Deployment Support | Supports Docker deployment and local deployment, provides complete configuration file management |
| Plugin System | Supports functional plugin extensions, custom plugin development, and plugin hot-loading |
### In Development 🚧 ### Under Development 🚧
To learn about specific development progress, [click here](https://github.com/users/xinnan-tech/projects/3) To learn about specific development plan progress, [click here](https://github.com/users/xinnan-tech/projects/3)
If you are a software developer, here is an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join! If you are a software developer, here is an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join!
--- ---
## Product Ecosystem 👬 ## Product Ecosystem 👬
Xiaozhi is an ecosystem. When using this product, you might also want to check out other excellent projects in this ecosystem Xiaozhi is an ecosystem. When using this product, you can also check out other excellent projects in this ecosystem
| Project Name | Project Address | Project Description | | Project Name | Project Address | Project Description |
|:---------------------|:--------|:--------| |:---------------------|:--------|:--------|
| Xiaozhi Android Client | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | A Flutter-based Android and iOS voice dialogue application supporting real-time voice interaction and text dialogue. | | Xiaozhi Android Client | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | An Android and iOS voice dialogue application based on xiaozhi-server, supporting real-time voice interaction and text dialogue.<br/>Currently a Flutter version, connecting iOS and Android platforms. |
| Xiaozhi PC Client | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | This project provides a Python-based Xiaozhi AI client, allowing you to experience Xiaozhi AI's functionality through code even without physical hardware. | | Xiaozhi Desktop Client | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | This project provides a Python-based AI client for beginners, allowing users to experience Xiaozhi AI functionality through code even without physical hardware conditions. |
| Xiaozhi Java Server | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | The Java version of Xiaozhi open-source backend service is a Java-based open-source project.<br/>It includes both frontend and backend services, aiming to provide users with a complete backend service solution. | | Xiaozhi Java Server | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | Xiaozhi open-source backend service Java version is a Java-based open-source project.<br/>It includes frontend and backend services, aiming to provide users with a complete backend service solution. |
--- ---
@@ -238,20 +274,32 @@ Xiaozhi is an ecosystem. When using this product, you might also want to check o
| Usage Method | Supported Platforms | Free Platforms | | Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| openai interface call | Ali Bailian, Volcano Engine Doubao, DeepSeek, Zhipu ChatGLM, Gemini | Zhipu ChatGLM, Gemini | | OpenAI interface calls | Alibaba Bailian, Volcano Engine Doubao, DeepSeek, Zhipu ChatGLM, Gemini | Zhipu ChatGLM, Gemini |
| ollama interface call | Ollama | - | | Ollama interface calls | Ollama | - |
| dify interface call | Dify | - | | Dify interface calls | Dify | - |
| fastgpt interface call | Fastgpt | - | | FastGPT interface calls | FastGPT | - |
| coze interface call | Coze | - | | Coze interface calls | Coze | - |
In fact, any LLM that supports openai interface calls can be integrated and used. In fact, any LLM that supports OpenAI interface calls can be integrated and used.
---
### VLLM Vision Models
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| OpenAI interface calls | Alibaba Bailian, Zhipu ChatGLMVLLM | Zhipu ChatGLMVLLM |
In fact, any VLLM that supports OpenAI interface calls can be integrated and used.
---
### TTS Speech Synthesis ### TTS Speech Synthesis
| Usage Method | Supported Platforms | Free Platforms | | Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| API Call | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) | | Interface calls | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS, Lingxi Streaming TTS | Lingxi Streaming TTS, EdgeTTS, CosyVoiceSiliconflow(partial) |
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | | Local services | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
--- ---
@@ -259,7 +307,7 @@ In fact, any LLM that supports openai interface calls can be integrated and used
| Type | Platform Name | Usage Method | Pricing Model | Notes | | Type | Platform Name | Usage Method | Pricing Model | Notes |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
| VAD | SileroVAD | Local Usage | Free | | | VAD | SileroVAD | Local use | Free | |
--- ---
@@ -267,8 +315,16 @@ In fact, any LLM that supports openai interface calls can be integrated and used
| Usage Method | Supported Platforms | Free Platforms | | Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| Local Usage | FunASR, SherpaASR | FunASR, SherpaASR | | Local use | FunASR, SherpaASR | FunASR, SherpaASR |
| API Call | DoubaoASR, FunASRServer, TencentASR, AliyunASR | FunASRServer | | Interface calls | DoubaoASR, FunASRServer, TencentASR, AliyunASR | FunASRServer |
---
### Voiceprint Recognition
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| Local use | 3D-Speaker | 3D-Speaker |
--- ---
@@ -276,8 +332,8 @@ In fact, any LLM that supports openai interface calls can be integrated and used
| Type | Platform Name | Usage Method | Pricing Model | Notes | | Type | Platform Name | Usage Method | Pricing Model | Notes |
|:------:|:---------------:|:----:|:---------:|:--:| |:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | API Call | 1000 calls/month quota | | | Memory | mem0ai | Interface calls | 1000 times/month quota | |
| Memory | mem_local_short | Local Summary | Free | | | Memory | mem_local_short | Local summarization | Free | |
--- ---
@@ -285,8 +341,8 @@ In fact, any LLM that supports openai interface calls can be integrated and used
| Type | Platform Name | Usage Method | Pricing Model | Notes | | Type | Platform Name | Usage Method | Pricing Model | Notes |
|:------:|:-------------:|:----:|:-------:|:---------------------:| |:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | API Call | Based on LLM pricing | Uses large model for intent recognition, highly versatile | | Intent | intent_llm | Interface calls | Based on LLM pricing | Recognizes intent through large models, strong generalization |
| Intent | function_call | API Call | Based on LLM pricing | Uses large model function calls for intent, fast and effective | | Intent | function_call | Interface calls | Based on LLM pricing | Completes intent through large model function calling, fast speed, good effect |
--- ---
@@ -294,11 +350,11 @@ In fact, any LLM that supports openai interface calls can be integrated and used
| Logo | Project/Company | Description | | Logo | Project/Company | Description |
|:---:|:---:|:---| |:---:|:---:|:---|
| <img src="./docs/images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it | | <img src="./docs/images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project is inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented on its basis |
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for establishing standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practices for the Xiaozhi ecosystem; providing full-chain technical documentation support for this project | | <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for formulating standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practice demonstrations for the Xiaozhi ecosystem; providing full-link technical documentation support for this project |
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing the function call framework, MCP communication protocol, and plugin call mechanism implementation code, significantly improving front-end device (IoT) interaction efficiency and functional extensibility through standardized instruction scheduling system and dynamic expansion capabilities | | <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing function calling framework, MCP communication protocol, and plugin-based calling mechanism implementation code. Through standardized instruction scheduling system and dynamic expansion capabilities, it significantly improves the interaction efficiency and functional extensibility of frontend devices (IoT) |
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, empowering the product user experience with their design experience serving over a thousand enterprises | | <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, using their design practical experience serving over a thousand enterprises to empower this project's product user experience |
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening the visual system of this project, ensuring consistency and extensibility of the overall design style in multi-scenario applications | | <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening this project's visual system, ensuring consistency and extensibility of overall design style in multi-scenario applications |
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date"> <a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
+2 -2
View File
@@ -269,7 +269,7 @@ LLM:
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。 如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
``` ```
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/ 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8003/xiaozhi/ota/
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/ 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问======= 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
@@ -280,7 +280,7 @@ LLM:
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。 但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。 最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/` 如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8003/xiaozhi/ota/`
这个信息很有用的,后面`编译esp32固件`需要用到。 这个信息很有用的,后面`编译esp32固件`需要用到。
+11 -1
View File
@@ -112,7 +112,17 @@ VAD:
参考教程[视觉模型使用指南](./mcp-vision-integration.md) 参考教程[视觉模型使用指南](./mcp-vision-integration.md)
### 10、更多问题,可联系我们反馈 💬 ### 10、如何开启MCP接入点 🔧
1、先参考教程[MCP 接入点部署使用指南](./mcp-endpoint-enable.md)
2、再参考教程[MCP 接入点使用指南](./mcp-endpoint-integration.md)
### 12、如何开启声纹识别 🔊
参考教程[声纹识别启用指南](./voiceprint-integration.md)
### 13、更多问题,可联系我们反馈 💬
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。 可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
+13
View File
@@ -4,6 +4,8 @@
本项目的测试平台`https://2662r3426b.vicp.fun`,从开放以来就使用了该方法,效果良好。 本项目的测试平台`https://2662r3426b.vicp.fun`,从开放以来就使用了该方法,效果良好。
教程可参考B站博主`毕乐labs`发布的视频教程:[《开源小智服务器xiaozhi-server自动更新以及最新版本MCP接入点配置保姆教程》](https://www.bilibili.com/video/BV15H37zHE7Q)
# 开始条件 # 开始条件
- 你的电脑/服务器是linux操作系统 - 你的电脑/服务器是linux操作系统
- 你已经跑通了整个流程 - 你已经跑通了整个流程
@@ -40,6 +42,9 @@ git clone https://ghproxy.net/https://github.com/xinnan-tech/xiaozhi-esp32-serve
此刻你需要把`model.pt`文件复制到新的目录去,你可以这样 此刻你需要把`model.pt`文件复制到新的目录去,你可以这样
``` ```
# 创建需要的目录
mkdir -p /home/system/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server/data/
cp 你原来的.config.yaml完整路径 /home/system/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server/data/.config.yaml cp 你原来的.config.yaml完整路径 /home/system/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server/data/.config.yaml
cp 你原来的model.pt完整路径 /home/system/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server/models/SenseVoiceSmall/model.pt cp 你原来的model.pt完整路径 /home/system/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server/models/SenseVoiceSmall/model.pt
``` ```
@@ -161,3 +166,11 @@ tail -f /home/system/xiaozhi/xiaozhi-esp32-server/main/xiaozhi-server/tmp/server
# 注意事项 # 注意事项
测试平台`https://2662r3426b.vicp.fun`,是使用nginx做了反向代理。nginx.conf详细配置可以[参考这里](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791) 测试平台`https://2662r3426b.vicp.fun`,是使用nginx做了反向代理。nginx.conf详细配置可以[参考这里](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)
## 常见问题
### 1、为什么没有见到8001端口?
回答:8001是开发环境使用的,用于运行前端的端口。如果你是服务器部署,不建议使用`npm run serve`启动8001端口运行前端,而是像本教程一样编译成html文件,然后使用nginx来管理访问。
### 2、每次更新需要更新手动SQL语句吗?
回答:不需要,因为项目使用**Liquibase**管理数据库版本,会自动执行新的sql脚本。
+3 -3
View File
@@ -42,9 +42,9 @@ http {
proxy_set_header Referer $http_referer; proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie; proxy_set_header Cookie $http_cookie;
proxy_connect_timeout 10; proxy_connect_timeout 15;
proxy_send_timeout 10; proxy_send_timeout 15;
proxy_read_timeout 10; proxy_read_timeout 15;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+1
View File
@@ -6,6 +6,7 @@ java -jar /app/xiaozhi-esp32-api.jar \
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \ --spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \ --spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \ --spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
--spring.data.redis.password=${SPRING_DATA_REDIS_PASSWORD} \
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} & --spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
# 启动Nginx(前台运行保持容器存活) # 启动Nginx(前台运行保持容器存活)
+1 -1
View File
@@ -9,7 +9,7 @@
### 如果你用的是简单Server部署 ### 如果你用的是简单Server部署
此刻,请你用浏览器打开你的ota地址,例如我的ota地址 此刻,请你用浏览器打开你的ota地址,例如我的ota地址
``` ```
http://192.168.1.25:8002/xiaozhi/ota/ http://192.168.1.25:8003/xiaozhi/ota/
``` ```
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/ 如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
+9 -39
View File
@@ -89,55 +89,25 @@ http://homeassistant.local:8123
公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1; 公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1;
``` ```
这段字符,我们为“设备清单字符”需要保存好,等一下有用。 这段字符,我们为“设备清单字符”需要保存好,等一下有用。
#### 2. 登录`智控台` #### 2. 登录`智控台`
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来 ![image-20250504051716417](images/image-ha-integration-06.png)
编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去 使用管理员账号,登录`智控台`。在`智能体管理`,找到你的智能体,再点击`配置角色`
将意图识别设置成`函数调用`或`LLM意图识别`。这时你会看到右侧有一个`编辑功能`。点击`编辑功能`按钮,会弹出`功能管理`的框。
编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的 在`功能管理`的框里,你需要勾选`HomeAssistant设备状态查询`和`HomeAssistant设备状态修改`。
``` 勾选后,在`已选功能`点击`HomeAssistant设备状态查询`,然后在`参数配置`里配置你的`HomeAssistant`地址、密钥、设备清单字符。
http://192.168.4.7:8123
```
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去 编辑好后,点击`保存配置`,这时`功能管理`的框会隐藏,这时你再点击保存智能体配置。
保存成功后,即可唤醒设备操作。
#### 3. 设置`意图识别`函数 #### 3. 唤醒设别进行控制
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state”
修改前
```
change_role;get_weather;get_news;play_music
```
修改后
```
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
```
#### 4. 手动重启xiaozhi-server
重启xiaozhi-server程序
#### 5. 确认角色配置是否设置了函数意图识别
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
确认意图识别(Intent),是否选择“函数调用意图识别”
#### 6. 唤醒设别进行控制
尝试和esp32说,“打开XXX灯” 尝试和esp32说,“打开XXX灯”
Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+125
View File
@@ -0,0 +1,125 @@
# MCP 接入点部署使用指南
本教程包含3个部分
- 1、如何部署MCP接入点这个服务
- 2、全模块部署时,怎么配置MCP接入点
- 3、单模块部署时,怎么配置MCP接入点
# 1、如何部署MCP接入点这个服务
## 第一步,下载mcp接入点项目源码
浏览器打开[mcp接入点项目地址](https://github.com/xinnan-tech/mcp-endpoint-server)
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-endpoint-server-main`
你需要把它重命名成`mcp-endpoint-server`
## 第二步,启动程序
这个项目是一个很简单的项目,建议使用docker运行。不过如果你不想使用docker运行,你可以参考[这个页面](https://github.com/xinnan-tech/mcp-endpoint-server/blob/main/README_dev.md)使用源码运行。以下是docker运行的方法
```
# 进入本项目源码根目录
cd mcp-endpoint-server
# 清除缓存
docker compose -f docker-compose.yml down
docker stop mcp-endpoint-server
docker rm mcp-endpoint-server
docker rmi ghcr.nju.edu.cn/xinnan-tech/mcp-endpoint-server:latest
# 启动docker容器
docker compose -f docker-compose.yml up -d
# 查看日志
docker logs -f mcp-endpoint-server
```
此时,日志里会输出类似以下的日志
```
250705 INFO-=====下面的地址分别是智控台/单模块MCP接入点地址====
250705 INFO-智控台MCP参数配置: http://172.22.0.2:8004/mcp_endpoint/health?key=abc
250705 INFO-单模块部署MCP接入点: ws://172.22.0.2:8004/mcp_endpoint/mcp/?token=def
250705 INFO-=====请根据具体部署选择使用,请勿泄露给任何人======
```
请你把两个接口地址复制出来:
由于你是docker部署,切不可直接使用上面的地址!
由于你是docker部署,切不可直接使用上面的地址!
由于你是docker部署,切不可直接使用上面的地址!
你先把地址复制出来,放在一个草稿里,你要知道你的电脑的局域网ip是什么,例如我的电脑局域网ip是`192.168.1.25`,那么
原来我的接口地址
```
智控台MCP参数配置: http://172.22.0.2:8004/mcp_endpoint/health?key=abc
单模块部署MCP接入点: ws://172.22.0.2:8004/mcp_endpoint/mcp/?token=def
```
就要改成
```
智控台MCP参数配置: http://192.168.1.25:8004/mcp_endpoint/health?key=abc
单模块部署MCP接入点: ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=def
```
改好后,请使用浏览器直接访问`智控台MCP参数配置`。当浏览器出现类似这样的代码,说明是成功了。
```
{"result":{"status":"success","connections":{"tool_connections":0,"robot_connections":0,"total_connections":0}},"error":null,"id":null,"jsonrpc":"2.0"}
```
请你保留好上面两个`接口地址`,下一步要用到。
# 2、全模块部署时,怎么配置MCP接入点
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
然后搜索参数`server.mcp_endpoint`,此时,它的值应该是`null`值。
点击修改按钮,把上一步得来的`智控台MCP参数配置`粘贴到`参数值`里。然后保存。
如果能保存成功,说明一切顺利,你可以去智能体查看效果了。如果不成功,说明智控台无法访问mcp接入点,很大概率是网络防火墙,或者没有填写正确的局域网ip。
# 3、单模块部署时,怎么配置MCP接入点
如果你是单模块部署,找到你的配置文件`data/.config.yaml`
在配置文件搜索`mcp_endpoint`,如果没有找到,你就增加`mcp_endpoint`配置。类似我是就是这样
```
server:
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
http_port: 8002
log:
log_level: INFO
# 此处可能还更多配置..
mcp_endpoint: 你的接入点websocket地址
```
这时,请你把`如何部署MCP接入点这个服务`中得到的`单模块部署MCP接入点` 粘贴到 `mcp_endpoint`中。类似这样
```
server:
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
http_port: 8002
log:
log_level: INFO
# 此处可能还更多配置
mcp_endpoint: ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=def
```
配置好后,启动单模块会输出如下的日志。
```
250705[__main__]-INFO-初始化组件: vad成功 SileroVAD
250705[__main__]-INFO-初始化组件: asr成功 FunASRServer
250705[__main__]-INFO-OTA接口是 http://192.168.1.25:8002/xiaozhi/ota/
250705[__main__]-INFO-视觉分析接口是 http://192.168.1.25:8002/mcp/vision/explain
250705[__main__]-INFO-mcp接入点是 ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
250705[__main__]-INFO-Websocket地址是 ws://192.168.1.25:8000/xiaozhi/v1/
250705[__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
250705[__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
250705[__main__]-INFO-=============================================================
```
如上,如果能输出类似的`mcp接入点是``ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc`说明配置成功了。
+94
View File
@@ -0,0 +1,94 @@
# MCP 接入点使用指南
本教程以虾哥开源的mcp计算器功能为示例,介绍如何将自己自定义的mcp服务接入到自己的接入点里。
本教程的前提是,你的`xiaozhi-server`已经启用了mcp接入点功能,如果你还没启用,可以先根据[这个教程](./mcp-endpoint-enable.md)启用。
# 如何为智能体接入一个简单的mcp功能,如计算器功能
### 如果你是全模块部署
如果你是全模块部署,你可以进入智控台,智能体管理,点击`配置角色`,在`意图识别`的右边,有一个`编辑功能`的按钮。
点击这个按钮。在弹出的页面里,位于底部,会有`MCP接入点`,正常来说,会显示这个智能体的`MCP接入点地址`,接下来,我们来给这个智能体扩展一个基于MCP技术的计算器的功能。
这个`MCP接入点地址`很重要,你等一下会用到。
### 如果你是单模块部署
如果你是单模块部署,且你已经在配置文件里配置了MCP接入点地址,那么正常来说,单模块部署启动的时候,会输出如下的日志。
```
250705[__main__]-INFO-初始化组件: vad成功 SileroVAD
250705[__main__]-INFO-初始化组件: asr成功 FunASRServer
250705[__main__]-INFO-OTA接口是 http://192.168.1.25:8002/xiaozhi/ota/
250705[__main__]-INFO-视觉分析接口是 http://192.168.1.25:8002/mcp/vision/explain
250705[__main__]-INFO-mcp接入点是 ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
250705[__main__]-INFO-Websocket地址是 ws://192.168.1.25:8000/xiaozhi/v1/
250705[__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
250705[__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
250705[__main__]-INFO-=============================================================
```
如上,输出`mcp接入点是``ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc`就是你的`MCP接入点地址`
这个`MCP接入点地址`很重要,你等一下会用到。
## 第一步 下载虾哥MCP计算器项目代码
浏览器打开虾哥写的[计算器项目](https://github.com/78/mcp-calculator)
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-calculatorr-main`
你需要把它重命名成`mcp-calculator`。接下来,我们用命令行进入项目目录即安装依赖
```bash
# 进入项目目录
cd mcp-calculator
conda remove -n mcp-calculator --all -y
conda create -n mcp-calculator python=3.10 -y
conda activate mcp-calculator
pip install -r requirements.txt
```
## 第二步 启动
启动前,先从你的智控台的智能体里,复制到了MCP接入点的地址。
例如我的智能体的mcp地址是
```
ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
```
开始输入命令
```bash
export MCP_ENDPOINT=ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
```
输入完后,启动程序
```bash
python mcp_pipe.py calculator.py
```
### 如果你是智控台部署
如果你是智控台部署,启动完后,你再进入智控台,点击刷新MCP的接入状态,就会看到你扩展的功能列表了。
### 如果你是单模块部署
如果你是单模块部署,当设备连接后,会输出类似的日志,说明成功了
```
250705 -INFO-正在初始化MCP接入点: wss://2662r3426b.vicp.fun/mcp_e
250705 -INFO-发送MCP接入点初始化消息
250705 -INFO-MCP接入点连接成功
250705 -INFO-MCP接入点初始化成功
250705 -INFO-统一工具处理器初始化完成
250705 -INFO-MCP接入点服务器信息: name=Calculator, version=1.9.4
250705 -INFO-MCP接入点支持的工具数量: 1
250705 -INFO-所有MCP接入点工具已获取,客户端准备就绪
250705 -INFO-工具缓存已刷新
250705 -INFO-当前支持的函数列表: [ 'get_time', 'get_lunar', 'play_music', 'get_weather', 'handle_exit_intent', 'calculator']
```
如果包含了 `'calculator'`,说明设备将可以根据意图识别,调用计算器这个工具。
+105
View File
@@ -0,0 +1,105 @@
# get_news_from_newsnow 插件新闻源配置指南
## 概述
`get_news_from_newsnow` 插件现在支持通过Web管理界面动态配置新闻源,不再需要修改代码。用户可以在智控台中为每个智能体配置不同的新闻源。
## 配置方式
### 1. 通过Web管理界面配置(推荐)
1. 登录智控台
2. 进入"角色配置"页面
3. 选择要配置的智能体
4. 点击"编辑功能"按钮
5. 在右侧参数配置区域找到"newsnow新闻聚合"插件
6. 在"新闻源配置"字段中输入分号分隔的中文名称
### 2. 配置文件方式
`config.yaml` 中配置:
```yaml
plugins:
get_news_from_newsnow:
url: "https://newsnow.busiyi.world/api/s?id="
news_sources: "澎湃新闻;百度热搜;财联社;微博;抖音"
```
## 新闻源配置格式
新闻源配置使用分号分隔的中文名称,格式为:
```
中文名称1;中文名称2;中文名称3
```
### 配置示例
```
澎湃新闻;百度热搜;财联社;微博;抖音;知乎;36氪
```
## 支持的新闻源
插件支持以下新闻源的中文名称:
- 澎湃新闻
- 百度热搜
- 财联社
- 微博
- 抖音
- 知乎
- 36氪
- 华尔街见闻
- IT之家
- 今日头条
- 虎扑
- 哔哩哔哩
- 快手
- 雪球
- 格隆汇
- 法布财经
- 金十数据
- 牛客
- 少数派
- 稀土掘金
- 凤凰网
- 虫部落
- 联合早报
- 酷安
- 远景论坛
- 参考消息
- 卫星通讯社
- 百度贴吧
- 靠谱新闻
- 以及更多...
## 默认配置
如果未配置新闻源,插件将使用以下默认配置:
```
澎湃新闻;百度热搜;财联社
```
## 使用说明
1. **配置新闻源**:在Web界面或配置文件中设置新闻源的中文名称,用分号分隔
2. **调用插件**:用户可以说"播报新闻"或"获取新闻"
3. **指定新闻源**:用户可以说"播报澎湃新闻"或"获取百度热搜"
4. **获取详情**:用户可以说"详细介绍这条新闻"
## 工作原理
1. 插件接受中文名称作为参数(如"澎湃新闻")
2. 根据配置的新闻源列表,将中文名称转换为对应的英文ID(如"thepaper"
3. 使用英文ID调用API获取新闻数据
4. 返回新闻内容给用户
## 注意事项
1. 配置的中文名称必须与 CHANNEL_MAP 中定义的名称完全一致
2. 配置更改后需要重启服务或重新加载配置
3. 如果配置的新闻源无效,插件会自动使用默认新闻源
4. 多个新闻源之间使用英文分号(;)分隔,不要使用中文分号(;)
+233
View File
@@ -0,0 +1,233 @@
# 声纹识别启用指南
本教程包含3个部分
- 1、如何部署声纹识别这个服务
- 2、全模块部署时,怎么配置声纹识别接口
- 3、最简化部署时,怎么配置声纹识别
# 1、如何部署声纹识别这个服务
## 第一步,下载声纹识别项目源码
浏览器打开[声纹识别项目地址](https://github.com/xinnan-tech/voiceprint-api)
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`voiceprint-api-main`
你需要把它重命名成`voiceprint-api`
## 第二步, 创建数据库和表
声纹识别需要依赖`mysql`数据库。如果你之前已经部署`智控台`,说明你已经安装了`mysql`。你可以共用它。
你可以你试一下在宿主机使用`telnet`命令,看看能不能正常访问`mysql``3306`端口。
```
telnet 127.0.0.1 3306
```
如果能访问到3306端口,请忽略以下的内容,直接进入第三步。
如果不能访问,你需要回忆一下,你的`mysql`是怎么安装的。
如果你的mysql是通过自己使用安装包安装的,说明你的`mysql`做了网络隔离。你可能先解访问`mysql``3306`端口这个问题。
如果你`mysql`是通过本项目的`docker-compose_all.yml`安装的。你需要找一下你当时创建数据库的`docker-compose_all.yml`文件,修改以下的内容
修改前
```
xiaozhi-esp32-server-db:
...
networks:
- default
expose:
- "3306:3306"
```
修改后
```
xiaozhi-esp32-server-db:
...
networks:
- default
ports:
- "3306:3306"
```
注意是将`xiaozhi-esp32-server-db`下面的`expose`改成`ports`。改完后,需要重新启动。以下是重启mysql的命令:
```
# 进入你docker-compose_all.yml所在的文件夹,例如我的是xiaozhi-server
cd xiaozhi-server
docker compose -f docker-compose_all.yml down
docker compose -f docker-compose.yml up -d
```
启动完后,在宿主机再使用`telnet`命令,看看能不能正常访问`mysql``3306`端口。
```
telnet 127.0.0.1 3306
```
正常来说这样就可以访问的了。
## 第三步, 创建数据库和表
如果你的宿主机,能正常访问mysql数据库,那就在mysql上创建一个名字为`voiceprint_db`的数据库和`voiceprints`表。
```
CREATE DATABASE voiceprint_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE voiceprint_db;
CREATE TABLE voiceprints (
id INT AUTO_INCREMENT PRIMARY KEY,
speaker_id VARCHAR(255) NOT NULL UNIQUE,
feature_vector LONGBLOB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_speaker_id (speaker_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
## 第四步, 配置数据库连接
进入`voiceprint-api`文件夹,创建名字为`data`的文件夹。
`voiceprint-api`根目录里的`voiceprint.yaml`,复制到`data`的文件夹,将它重命名为`.voiceprint.yaml`
接下来,你需要重点配置一下`.voiceprint.yaml`里的数据库连接。
```
mysql:
host: "127.0.0.1"
port: 3306
user: "root"
password: "your_password"
database: "voiceprint_db"
```
注意!由于你的声纹识别服务是使用docker部署,`host`需要填写成你`mysql所在机器的局域网ip`
注意!由于你的声纹识别服务是使用docker部署,`host`需要填写成你`mysql所在机器的局域网ip`
注意!由于你的声纹识别服务是使用docker部署,`host`需要填写成你`mysql所在机器的局域网ip`
## 第五步,启动程序
这个项目是一个很简单的项目,建议使用docker运行。不过如果你不想使用docker运行,你可以参考[这个页面](https://github.com/xinnan-tech/voiceprint-api/blob/main/README.md)使用源码运行。以下是docker运行的方法
```
# 进入本项目源码根目录
cd voiceprint-api
# 清除缓存
docker compose -f docker-compose.yml down
docker stop voiceprint-api
docker rm voiceprint-api
docker rmi ghcr.nju.edu.cn/xinnan-tech/voiceprint-api:latest
# 启动docker容器
docker compose -f docker-compose.yml up -d
# 查看日志
docker logs -f voiceprint-api
```
此时,日志里会输出类似以下的日志
```
250711 INFO-🚀 开始: 生产环境服务启动(Uvicorn),监听地址: 0.0.0.0:8005
250711 INFO-============================================================
250711 INFO-声纹接口地址: http://127.0.0.1:8005/voiceprint/health?key=abcd
250711 INFO-============================================================
```
请你把声纹接口地址复制出来:
由于你是docker部署,切不可直接使用上面的地址!
由于你是docker部署,切不可直接使用上面的地址!
由于你是docker部署,切不可直接使用上面的地址!
你先把地址复制出来,放在一个草稿里,你要知道你的电脑的局域网ip是什么,例如我的电脑局域网ip是`192.168.1.25`,那么
原来我的接口地址
```
http://127.0.0.1:8005/voiceprint/health?key=abcd
```
就要改成
```
http://192.168.1.25:8005/voiceprint/health?key=abcd
```
改好后,请使用浏览器直接访问`声纹接口地址`。当浏览器出现类似这样的代码,说明是成功了。
```
{"total_voiceprints":0,"status":"healthy"}
```
请你保留好修改后的`声纹接口地址`,下一步要用到。
# 2、全模块部署时,怎么配置声纹识别
## 第一步 配置接口
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
然后搜索参数`server.voice_print`,此时,它的值应该是`null`值。
点击修改按钮,把上一步得来的`声纹接口地址`粘贴到`参数值`里。然后保存。
如果能保存成功,说明一切顺利,你可以去智能体查看效果了。如果不成功,说明智控台无法访问声纹识别,很大概率是网络防火墙,或者没有填写正确的局域网ip。
## 第二步 设置智能体记忆模式
进入你的智能体的角色配置里,将记忆设置成`本地短期记忆`,一定要开启`上报文字+语音`
## 第三步 和你的智能体聊天
将你的设备通电,然后和他用正常的语速和音调聊天。
## 第四步 设置声纹
在智控台,`智能体管理`页面,在智能体的面板里,有一个`声纹识别`按钮,点击它。在底部有一个`新增按钮`。就可以对某个人说的话进行声纹注册。
在弹出的框里,`描述`这个属性建议填写上,可以是这个人的职业、性格、爱好。方便智能体对说话人进行分析和了解。
## 第三步 和你的智能体聊天
将你的设备通电,问它,你知道我是谁吗?如果他能回答得出,说明声纹识别功能正常。
# 3、最简化部署时,怎么配置声纹识别
## 第一步 配置接口
打开 `xiaozhi-server/data/.config.yaml` 文件(如果没有需要创建),然后添加/修改以下内容:
```
# 声纹识别配置
voiceprint:
# 声纹接口地址
url: 你的声纹接口地址
# 说话人配置:speaker_id,名称,描述
speakers:
- "test1,张三,张三是一个程序员"
- "test2,李四,李四是一个产品经理"
- "test3,王五,王五是一个设计师"
```
把上一步得来的 `声纹接口地址` 粘贴到 `url` 里。然后保存。
`speakers` 参数依据需求添加。这里需要注意这个 `speaker_id` 参数,后面注册声纹会用到。
## 第二步 注册声纹
如果你已经启动了声纹服务,本地浏览器里访问 `http://localhost:8005/voiceprint/docs` 即可查看 API 文档,这里只说明注册声纹的 API 如何使用。
注册声纹的 API 地址为 `http://localhost:8005/voiceprint/register`,请求方式为 POST。
请求头需要包含 Bearer Token 认证,token 为 `声纹接口地址``?key=` 后的部分,比如如果我的声纹注册地址为 `http://127.0.0.1:8005/voiceprint/health?key=abcd`,那么我的 token 就是`abcd`
请求体包含说话人 IDspeaker_id),和 WAV 音频文件(file),请求示例如下:
```
curl -X POST \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-F "speaker_id=your_speaker_id_here" \
-F "file=@/path/to/your/file" \
http://localhost:8005/voiceprint/register
```
这里的 `file` 是要注册的说话人说话的音频文件, `speaker_id` 需要和第一步配置接口的 `speaker_id` 保持一致。比如说我需要注册张三的声纹,在 `.config.yaml` 中填的张三的 `speaker_id``test1`,那么我注册张三声纹的时候,请求体里填的 `speaker_id` 就是 `test1` `file` 填的就是张三说一段话的音频文件。
## 第三步 启动服务
启动小智服务器和声纹服务,即可正常使用。
+457 -7
View File
@@ -1,7 +1,70 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3) # 技术文档:`xiaozhi-esp32-server`
# 项目目录介绍 **目录:**
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
1. [引言](#1-引言)
2. [整体架构](#2-整体架构)
3. [核心组件深度剖析](#3-核心组件深度剖析)
* [3.1. `xiaozhi-server` (核心AI引擎 - Python实现)](#31-xiaozhi-server-核心ai引擎---python实现)
* [3.2. `manager-api` (管理后端 - Java Spring Boot实现)](#32-manager-api-管理后端---java-spring-boot实现)
* [3.3. `manager-web` (Web管理前端 - Vue.js实现)](#33-manager-web-web管理前端---vuejs实现)
4. [数据流与交互机制](#4-数据流与交互机制)
5. [核心功能概要](#5-核心功能概要)
6. [部署与配置概述](#6-部署与配置概述)
---
## 1. 引言
`xiaozhi-esp32-server` 项目是一个专为基于ESP32的智能硬件提供支持的**综合性后端系统**。其核心目标是使开发人员能够快速构建一个强大的服务器基础设施,该设施不仅能够理解自然语言指令,还能与多种AI服务(用于语音识别、自然语言理解及语音合成)进行高效交互、管理物联网(IoT)设备,并提供一个基于Web的用户界面以进行系统配置和管理。通过将多种尖端技术整合到一个高内聚且可扩展的平台中,本项目旨在简化和加速可定制化语音助手及智能控制系统的开发进程。它不仅仅是一个简单的服务器,更是一个连接硬件、AI能力与用户管理的桥梁。
---
## 2. 整体架构
`xiaozhi-esp32-server` 系统采用了一种**分布式、多组件协作**的架构设计,确保了系统的模块化、可维护性和可扩展性。各个核心组件各司其职,协同工作。主要组件包括:
1. **ESP32 硬件 (客户端设备):**
这是终端用户直接与之交互的物理智能硬件设备。其主要职责包括:
* 捕捉用户的语音指令。
* 将捕捉到的原始音频数据安全地发送至 `xiaozhi-server` 进行处理。
* 接收来自 `xiaozhi-server` 合成的语音回复,并通过扬声器播放给用户。
* 根据从 `xiaozhi-server` 收到的指令,控制与之连接的其他外围设备或IoT设备(例如智能灯泡、传感器等)。
2. **`xiaozhi-server` (核心AI引擎 - Python实现):**
这个基于Python的服务器是整个系统的“大脑”,负责处理所有语音相关的逻辑和AI交互。其关键职责细化如下:
* 通过WebSocket协议与ESP32设备建立**稳定、低延迟的实时双向通信链路**。
* 接收来自ESP32的音频流,并利用语音活动检测(VAD)技术精确切分有效的语音片段。
* 集成并调用自动语音识别(ASR)服务(可配置本地或云端),将语音片段转换为文本。
* 通过与大型语言模型(LLM)的交互来解析用户意图、生成智能回复,并支持复杂的自然语言理解任务。
* 管理多轮对话中的上下文信息和用户记忆,以提供连贯的交互体验。
* 调用文本转语音(TTS)服务,将LLM生成的文本回复合成为自然流畅的语音。
* 通过一个灵活的**插件系统**执行自定义命令,包括对IoT设备的控制逻辑。
*`manager-api` 服务获取其详细的运行时操作配置。
3. **`manager-api` (管理后端 - Java实现):**
这是一个基于Java Spring Boot框架构建的应用程序,它为整个系统的管理和配置提供了一套安全的RESTful API。它不仅是 `manager-web` 控制台的后端支撑,也是 `xiaozhi-server` 的配置数据来源。其核心功能包括:
* 为Web控制台提供用户认证(登录、权限验证)和用户账户管理功能。
* ESP32设备的注册、信息管理以及设备特定配置的维护。
* 在**MySQL数据库**中持久化存储系统配置,例如用户选择的AI服务提供商、API密钥、设备参数、插件设置等。
* 提供特定的API端点,供 `xiaozhi-server` 拉取其所需的最新配置。
* 管理TTS音色选项、处理OTAOver-The-Air)固件更新流程及相关元数据。
* 利用 **Redis** 作为高速缓存,存储热点数据(如会话信息、频繁访问的配置),以提升API响应速度和系统整体性能。
4. **`manager-web` (Web控制面板 - Vue.js实现):**
这是一个基于Vue.js构建的单页应用(SPA),为系统管理员提供了一个图形化、用户友好的操作界面。其主要能力包括:
* 便捷地配置 `xiaozhi-server` 所使用的各项AI服务(如ASR、LLM、TTS的提供商切换、参数调整)。
* 管理平台用户账户、角色分配及权限控制。
* 管理已注册的ESP32设备及其相关设置。
* (潜在功能)监控系统运行状态、查看日志、进行故障排查等。
*`manager-api` 提供的所有后端管理功能进行全面的交互。
**高层交互流程概述:**
* **语音交互主线:** **ESP32设备**捕捉到用户语音后,通过**WebSocket**将音频数据实时传输给**`xiaozhi-server`**。`xiaozhi-server`完成一系列AI处理(VAD、ASR、LLM交互、TTS)后,再通过WebSocket将合成的语音回复发送回ESP32设备进行播放。所有与语音直接相关的实时交互均在此链路完成。
* **管理配置主线:** 管理员通过浏览器访问**`manager-web`**控制台。`manager-web`通过调用**`manager-api`**提供的**RESTful HTTP接口**来执行各种管理操作(如修改配置、管理用户或设备)。数据以JSON格式在两者间传递。
* **配置同步:** **`xiaozhi-server`**在启动或特定更新机制触发时,会主动通过HTTP请求从**`manager-api`**拉取其最新的操作配置。这确保了管理员在Web界面上所做的配置更改能够及时有效地应用到核心AI引擎的运行中。
这种**前后端分离、核心服务与管理服务分离**的架构设计,使得 `xiaozhi-server`能够专注于高效的实时AI处理任务,而 `manager-api``manager-web` 则共同提供了一个功能强大且易于使用的管理和配置平台。各组件职责清晰,有利于独立开发、测试、部署和扩展。
``` ```
xiaozhi-esp32-server xiaozhi-esp32-server
@@ -10,10 +73,397 @@ xiaozhi-esp32-server
├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api ├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api
``` ```
# xiaozhi-server 和ESP32通讯协议 ---
https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh ## 3. 核心组件深度剖析
# manager-web 、manager-api接口协议 ### 3.1. `xiaozhi-server` (核心AI引擎 - Python实现)
https://2662r3426b.vicp.fun/xiaozhi/doc.html `xiaozhi-server` 作为系统的智能核心,全权负责处理语音交互、对接各类AI服务以及管理与ESP32设备间的通信。其设计目标是实现高效、灵活且可扩展的语音AI处理能力。
* **核心目标:**
* 为ESP32设备提供实时的语音指令处理服务。
* 深度集成各类AI服务,包括:自动语音识别 (ASR)、大型语言模型 (LLM) 进行自然语言理解 (NLU)、文本转语音 (TTS)、语音活动检测 (VAD)、意图识别 (Intent Recognition) 及对话记忆 (Memory)。
* 精细管理用户与设备间的对话流程及上下文状态。
* 基于用户指令,通过插件化机制执行自定义函数及控制物联网 (IoT) 设备。
* 支持通过 `manager-api`进行动态配置加载与更新。
* **核心技术栈:**
* **Python 3:** 作为主要编程语言,Python以其丰富的AI/ML生态库和快速开发特性被选用。
* **Asyncio:** Python的异步编程框架,是`xiaozhi-server`高性能的关键。它被广泛用于高效处理来自大量ESP32设备的并发WebSocket连接,以及执行与外部AI服务API通信时的非阻塞I/O操作,确保服务器在高并发下的响应能力。
* **`websockets` 库:** 提供WebSocket服务器的具体实现,支持与ESP32客户端进行全双工实时通信。
* **HTTP客户端 (如 `aiohttp`, `httpx`):** 用于异步执行HTTP请求,主要目的是从`manager-api`获取配置信息,以及与云端AI服务的API进行交互。
* **YAML (通常通过 PyYAML 库):** 用于解析本地的 `config.yaml` 配置文件。
* **FFmpeg (外部依赖):**`app.py` 启动时会进行检查 (`check_ffmpeg_installed()`)。FFmpeg通常用于音频处理和格式转换,例如,确保音频数据符合特定AI服务的要求或进行内部处理。
* **关键实现细节:**
1. **AI服务提供者模式 (Provider Pattern - `core/providers/`):**
* **设计思想:** 这是`xiaozhi-server`集成不同AI服务的核心设计模式,极大地增强了系统的灵活性和可扩展性。针对每一种AI服务类型(ASR, TTS, LLM, VAD, Intent, Memory, VLLM),都在其对应子目录下定义了一个抽象基类 (ABC, Abstract Base Class),例如 `core/providers/asr/base.py`。这个基类规定了该类型服务必须实现的通用接口方法(如ASR的 `async def transcribe(self, audio_chunk: bytes) -> str: pass`)。
* **具体实现:** 各种具体的AI服务提供商或本地模型的实现,则以独立的Python类形式存在(例如 `core/providers/asr/fun_local.py` 实现了本地FunASR的逻辑,`core/providers/llm/openai.py` 实现了与OpenAI GPT模型的对接)。这些具体类继承自相应的抽象基类,并实现其定义的接口。部分提供者还使用DTOs (Data Transfer Objects, 存在于各自的 `dto/` 目录) 来结构化与外部服务交换的数据。
* **优势:** 使得核心业务逻辑能够以统一的方式调用不同的AI服务,而无需关心其底层具体实现。用户可以通过配置文件轻松切换AI服务后端。添加对新AI服务的支持也变得相对简单,只需实现对应的Provider接口。
* **动态加载与初始化:** `core/utils/modules_initialize.py` 脚本扮演了工厂的角色。它在服务器启动时,或在接收到配置更新指令时,会根据配置文件中 `selected_module` 及各项服务的具体provider设置,动态地导入并实例化相应的Provider类。
2. **WebSocket通信与连接处理 (`app.py`, `core/websocket_server.py`, `core/connection.py`):**
* **服务器启动与入口 (`app.py`):**
* `app.py` 作为主入口,负责初始化应用环境(如检查FFmpeg、加载配置、设置日志)。
* 它会生成或加载一个 `auth_key` (JWT密钥),用于保护特定的HTTP接口(如视觉分析接口 `/mcp/vision/explain`)。若配置中 `manager-api.secret` 为空,则会生成一个UUID作为 `auth_key`
* 使用 `asyncio.create_task()` 并发启动 `WebSocketServer` (监听如 `ws://0.0.0.0:8000/xiaozhi/v1/`) 和 `SimpleHttpServer` (监听如 `http://0.0.0.0:8003/xiaozhi/ota/`)。
* 包含一个 `monitor_stdin()` 协程,用于在某些环境下保持应用存活或处理终端输入。
* **WebSocket服务器核心 (`core/websocket_server.py`):**
* `WebSocketServer` 类使用 `websockets` 库监听来自ESP32设备的连接请求。
* 对于每一个成功的WebSocket连接,它都会创建一个**独立的 `ConnectionHandler` 实例** (推测定义于 `core/connection.py`)。这种每个连接一个处理程序实例的设计模式,是实现多设备状态隔离和并发处理的关键,确保每个设备的对话流程和上下文信息互不干扰。
* 该服务器还提供一个 `_http_response` 方法,允许在同一端口上对非WebSocket升级的HTTP GET请求做出简单响应(例如返回 "Server is running"),便于进行健康检查。
* **动态配置更新:** `WebSocketServer` 包含一个 `update_config()` 异步方法。此方法使用 `config_lock` (一个 `asyncio.Lock`) 保证配置更新的原子性。它调用 `get_config_from_api()` (可能在 `config_loader.py` 中实现,通过 `manage_api_client.py``manager-api` 通信) 来获取新的配置。通过 `check_vad_update()``check_asr_update()` 等辅助函数判断是否需要重新初始化特定的AI模块,避免不必要的开销。更新后的配置会用于重新调用 `initialize_modules()`,从而实现AI服务提供者的热切换。
3. **消息处理与对话流程控制 (`core/handle/` 和 `ConnectionHandler`):**
* `ConnectionHandler` (推测) 作为每个连接的控制中心,负责接收来自ESP32的消息,并根据消息类型或当前对话状态,将其分发给 `core/handle/` 目录下的相应处理模块。这种模块化的处理器设计使得 `ConnectionHandler` 逻辑更清晰,易于扩展。
* **主要处理模块及其职责:**
* `helloHandle.py`: 处理与ESP32初次连接时的握手协议、设备认证或初始化信息交换。
* `receiveAudioHandle.py`: 接收音频流数据,调用VAD Provider进行语音活动检测,并将有效的音频片段传递给ASR Provider进行识别。
* `textHandle.py` / `intentHandler.py`: 获取ASR识别出的文本后,与Intent Provider (可能利用LLM进行意图识别) 和LLM Provider交互,以理解用户意图并生成初步回复或决策。
* `functionHandler.py`: 当LLM的响应包含执行特定“函数调用”的指令时,此模块负责从插件注册表中查找并执行对应的插件函数。
* `sendAudioHandle.py`: 将LLM最终生成的文本回复交给TTS Provider合成语音,并将音频流通过WebSocket发送回ESP32。
* `abortHandle.py`: 处理来自ESP32的中断请求,例如停止当前的TTS播报。
* `iotHandle.py`, `mcpHandle.py`: 处理与IoT设备控制相关的特定指令或更复杂的模块通信协议 (MCP)。
4. **插件化功能扩展系统 (`plugins_func/`):**
* **设计目的:** 提供一种标准化的方式来扩展语音助手的功能和“技能”,而无需修改核心代码。
* **实现机制:**
* 各个具体功能以独立的Python脚本形式存在于 `plugins_func/functions/` 目录中(例如 `get_weather.py`, `hass_set_state.py` 用于Home Assistant集成)。
* `loadplugins.py` 在服务器启动时负责扫描并加载这些插件模块。
* `register.py` (或插件模块内部的特定装饰器/函数) 可能用于定义每个插件函数的元数据,包括:
* **函数名称 (Function Name):** LLM调用时使用的标识符。
* **功能描述 (Description):** 供LLM理解此函数的作用。
* **参数模式 (Parameters Schema):** 通常是一个JSON Schema,详细定义了函数所需的参数、类型、是否必需以及描述。这是LLM能够正确生成函数调用参数的关键。
* **执行流程:** 当LLM在其思考过程中决定需要调用某个外部工具或函数来获取信息或执行操作时,它会依据预先提供的函数模式生成一个结构化的“函数调用”请求。`xiaozhi-server`中的`functionHandler.py`捕获此请求,从插件注册表中找到对应的Python函数并执行,然后将执行结果返回给LLM,LLM再基于此结果生成最终给用户的自然语言回复。
5. **配置管理 (`config/`):**
* **加载机制:** `config_loader.py` (通过 `settings.py` 被调用) 负责从根目录的 `config.yaml` 文件加载基础配置。
* **远程配置与合并:** 通过 `manage_api_client.py` (使用如`aiohttp`的库与`manager-api`通信) 可以从`manager-api`服务拉取配置。远程配置通常会覆盖本地 `config.yaml` 中的同名设置,从而实现通过Web界面动态调整服务器行为。
* **日志系统:** `logger.py` 初始化应用日志系统(可能使用 `loguru` 或对标准 `logging` 模块进行封装,支持通过 `logger.bind(tag=TAG)` 添加标签,便于追踪和过滤)。
* **静态资源:** `config/assets/` 目录下存放了用于系统提示音的静态音频文件(如设备绑定提示音 `bind_code.wav`、错误提示音等)。
6. **辅助HTTP服务 (`core/http_server.py`):**
* 与WebSocket服务并行运行一个简单的HTTP服务器,用于处理特定的HTTP请求。最主要的功能是为ESP32设备提供OTA (Over-The-Air) 固件更新的下载服务 (通过 `/xiaozhi/ota/` 端点)。此外,也可能承载其他如 `/mcp/vision/explain` (视觉分析) 等工具性HTTP接口。
综上所述,`xiaozhi-server` 是一个采用现代Python异步编程模型构建的、高度模块化、配置驱动的AI应用服务器。其精心设计的Provider模式和插件架构赋予了它强大的适应性和扩展性,能够灵活接入不同的AI能力并支持日益增长的功能需求。
---
### 3.2. `manager-api` (管理后端 - Java Spring Boot实现)
`manager-api` 组件是使用Java和Spring Boot框架构建的强大后端服务,作为整个`xiaozhi-esp32-server`生态系统的中央行政管理和配置中枢。
* **核心目标:**
*`manager-web`(Vue.js前端)提供一套安全、稳定、符合RESTful规范的API接口,使得管理员能够便捷地管理用户、设备、系统配置及其他相关资源。
* 充当`xiaozhi-server`(Python核心AI引擎)的集中化配置数据提供者,允许`xiaozhi-server`实例在启动或运行时获取其最新的操作参数。
* 持久化存储关键数据,例如:用户账户信息、设备注册详情、AI服务提供商配置(包括API密钥、选定的服务模型等)、TTS音色参数,以及OTA固件版本信息等。
* **核心技术栈:**
* **Java 21:** 项目采用的JDK版本,确保了对现代Java特性的支持。
* **Spring Boot 3:** 作为核心开发框架,极大地简化了独立、生产级别的Spring应用的创建和部署。它提供了自动配置、内嵌Web服务器(默认为Tomcat)、依赖管理等关键功能。
* **Spring MVC:** Spring框架中用于构建Web应用和RESTful API的模块。
* **MyBatis-Plus:** 一个对MyBatis进行功能增强的ORM(对象关系映射)框架。它简化了数据库操作,提供了强大的CRUD(增删改查)功能、条件构造器、代码生成器等,并能很好地与Spring Boot集成。
* **MySQL:** 作为主要的后端关系型数据库,用于存储所有需要持久化的管理数据和配置信息。
* **Druid (Alibaba Druid):** 一个功能强大的JDBC连接池实现,提供了丰富的监控功能和优秀的性能,用于高效管理数据库连接。
* **Redis (通过 Spring Data Redis):** 一个高性能的内存数据结构存储,常用于实现数据缓存(例如缓存热点配置数据、用户会话信息),以显著提升API的响应速度。
* **Apache Shiro:** 一个成熟且易用的Java安全框架,负责处理应用的认证(用户身份验证)和授权(API访问权限控制)需求。
* **Liquibase:** 一个用于跟踪、管理和应用数据库 schéma(模式)变更的开源工具。它允许开发者以数据库无关的方式定义和版本化数据库结构变更。
* **Knife4j:** 一个集成了Swagger并增强了UI的API文档生成工具,专为Java MVC框架(尤其是Spring Boot)设计。它能生成美观且易于交互的API文档界面(通常通过 `/xiaozhi/doc.html` 访问)。
* **Maven:** 用于项目的构建自动化和依赖项管理。
* **Lombok:** 一个Java库,通过注解自动生成构造函数、getter/setter、equals/hashCode、toString等样板代码,减少冗余。
* **HuTool / Google Guava:** 提供大量实用工具类,简化常见编程任务。
* **Aliyun Dysmsapi:** 阿里云短信服务SDK,用于集成发送短信功能(如验证码、通知)。
* **关键实现细节:**
1. **模块化项目结构 (`modules/` 包):**
* `manager-api` 的核心业务逻辑被清晰地划分到 `src/main/java/xiaozhi/modules/` 目录下的不同模块中。这种按功能领域划分模块的方式(例如 `sys` 负责系统管理,`agent` 负责智能体配置,`device` 负责设备管理,`config` 负责为`xiaozhi-server`提供配置,`security` 负责安全,`timbre` 负责音色管理,`ota` 负责固件升级)极大地提高了代码的可维护性和可扩展性。
* **各模块内部结构:** 每个业务模块通常遵循经典的三层架构或其变体:
* **Controller (控制层):** 位于 `xiaozhi.modules.[模块名].controller`
* **Service (服务层):** 位于 `xiaozhi.modules.[模块名].service`
* **DAO/Mapper (数据访问层):** 位于 `xiaozhi.modules.[模块名].dao`
* **Entity (实体类):** 位于 `xiaozhi.modules.[模块名].entity`
* **DTO (数据传输对象):** 位于 `xiaozhi.modules.[模块名].dto`
2. **分层架构实现:**
* **Controller层 (`@RestController`):** 这些类使用Spring MVC注解(如 `@GetMapping`, `@PostMapping` 等)来定义API的端点(endpoints)。它们负责接收HTTP请求,将请求体中的JSON数据反序列化为DTO对象,调用相应的Service层方法处理业务逻辑,最后将Service层的返回结果序列化为JSON并作为HTTP响应返回给客户端。
* **Service层 (`@Service`):** 这些类(通常是接口及其实现类的组合)封装了核心的业务规则和操作流程。它们可能会调用一个或多个DAO/Mapper对象来与数据库交互,并常常使用 `@Transactional` 注解来管理数据库事务的原子性。
* **Data Access (DAO/Mapper) 层 (MyBatis-Plus Mappers):** 这些是Java接口,继承自MyBatis-Plus提供的 `BaseMapper<Entity>` 接口。MyBatis-Plus会为这些接口自动提供标准的CRUD方法。对于更复杂的数据库查询,开发者可以通过在Mapper接口中定义方法并使用注解(如 `@Select`, `@Update`)或编写对应的XML映射文件来实现。例如,`UserMapper.selectById(userId)` 会被MyBatis-Plus自动实现。
* **Entity层 (`@TableName`, `@TableId` 等MyBatis-Plus注解):** 这些POJOPlain Old Java Objects)类直接映射到数据库中的表结构。Lombok的 `@Data` 注解常用于自动生成getter/setter等。
* **DTO层:** 用于在各层之间,特别是Controller层与Service层之间,以及API的请求/响应体中传递数据。使用DTO有助于解耦API接口的数据结构与数据库实体的数据结构,使API更稳定。
3. **通用功能与配置 (`common/` 包):**
* `src/main/java/xiaozhi/common/` 包提供了一系列跨模块共享的通用组件和配置:
* **基类:**`BaseDao`, `BaseEntity`, `BaseService`, `CrudService`,为各模块的相应组件提供通用的属性或方法。
* **全局配置:** 包括 `MybatisPlusConfig` (MyBatis-Plus的配置,如分页插件、数据权限插件等)、`RedisConfig` (Redis连接及序列化配置)、`SwaggerConfig` (Knife4j的配置)、`AsyncConfig` (异步任务执行器配置)。
* **自定义注解:** 例如 `@LogOperation` 用于通过AOP记录操作日志,`@DataFilter` 可能用于实现数据范围过滤。
* **AOP切面:**`RedisAspect` 可能用于实现方法级别的缓存逻辑。
* **全局异常处理:** `RenExceptionHandler` (使用 `@ControllerAdvice` 注解) 捕获应用中抛出的特定或所有异常 (如自定义的 `RenException`),并返回统一格式的JSON错误响应给客户端。`ErrorCode` 定义了标准化的错误码。
* **工具类:** 提供了日期转换、JSON处理(Jackson)、IP地址获取、HTTP上下文操作、统一结果封装 (`Result` 类)等多种实用工具。
* **校验工具:** `ValidatorUtils``AssertUtils` 用于简化参数校验逻辑。
* **XSS防护:** `XssFilter` 等组件用于防止跨站脚本攻击。
* **MyBatis-Plus自动填充:** `FieldMetaObjectHandler` 用于在执行插入或更新数据库操作时,自动填充如 `createTime`, `updateTime` 等公共字段。
4. **安全机制 (Apache Shiro):**
* Shiro的配置(通常在 `modules/security/config/``common/config/` 下)定义了如何进行用户认证和授权。
* **Realms (域):** 自定义的Shiro Realm类负责从数据库中查询用户信息(用户名、密码、盐值)进行身份验证,以及获取用户的角色和权限信息用于授权决策。
* **Filters (过滤器):** Shiro过滤器链被应用于保护API端点,确保只有经过认证且拥有足够权限的用户才能访问特定资源。
* **Session/Token Management:** Shiro管理用户会话。对于RESTful API,可能结合OAuth2或JWT等令牌机制实现无状态认证。
5. **数据库版本控制 (Liquibase):**
* 数据库的表结构、索引、初始数据等变更,都通过Liquibase的 `changelog` 文件(通常是XML格式)进行定义和版本化管理。当应用启动时,Liquibase会自动检查并应用必要的数据库结构更新,确保开发、测试和生产环境数据库结构的一致性。
6. **API文档:**
* 完整的API接口文档可通过以下地址访问: https://2662r3426b.vicp.fun/xiaozhi/doc.html
* 该文档使用Knife4j生成,提供了所有RESTful API端点的详细说明、请求/响应示例以及在线测试功能。
`manager-api` 通过这些精心选择的技术和设计模式,构建了一个功能全面、结构清晰、安全可靠且易于维护和扩展的Java后端服务。其模块化的设计特别适合处理具有多种管理功能需求的复杂系统。
---
### 3.3. `manager-web` (Web管理前端 - Vue.js实现)
`manager-web` 组件是一个采用 Vue.js 2 框架构建的单页应用 (SPA - Single Page Application)。它为系统管理员提供了一个功能丰富、交互友好的图形用户界面,用于全面管理和配置 `xiaozhi-esp32-server` 生态系统。
* **核心目标:**
* 提供一个基于Web的集中式控制面板,供管理员进行系统操作与监控。
* 实现对 `xiaozhi-server` 中AI服务提供商(ASR、LLM、TTS等)及其相关API密钥或许可配置的便捷管理。
* 支持用户账户、角色及权限的精细化管理。
* 提供ESP32设备的注册、配置及状态查看功能。
* 允许管理员自定义TTS音色、管理OTA固件更新流程、调整系统级参数及字典数据等。
* 作为 `manager-api` 所暴露各项功能的图形化交互前端。
* **核心技术栈:**
* **Vue.js 2:** 一个渐进式的JavaScript框架,用于构建用户界面。其核心特性包括声明式渲染、组件化系统、数据绑定等,非常适合构建复杂的SPA。
* **Vue CLI (`@vue/cli-service`):** Vue.js的官方命令行工具,用于项目的快速搭建、开发服务器的运行(支持热模块替换HMR)、以及生产环境构建打包(内部集成并配置了Webpack)。
* **Vue Router (`vue-router`):** Vue.js官方的路由管理器。它负责在SPA内部实现不同“页面”或视图组件之间的导航切换,而无需重新加载整个HTML页面,提供了流畅的用户体验。
* **Vuex (`vuex`):** Vue.js官方的状态管理模式和库。它充当了应用中所有组件的“中央数据存储”,用于管理全局共享状态(例如当前登录用户信息、设备列表、应用配置等),特别适用于大型复杂应用。
* **Element UI (`element-ui`):** 一个广受欢迎的基于Vue 2.0的桌面端UI组件库。它提供了大量预先设计和实现的组件(如表单、表格、对话框、导航菜单、按钮、提示等),帮助开发者快速构建出专业且一致的用户界面。
* **JavaScript (ES6+):** 前端逻辑实现的主要编程语言,利用其现代特性进行开发。
* **SCSS (Sassy CSS):** 一种CSS预处理器,它为CSS增加了变量、嵌套规则、混合(Mixin)、继承等高级特性,使得CSS代码更易于组织、维护和复用。
* **HTTP客户端 (Flyio 或 Axios 通过 `vue-axios`):** 用于在浏览器端向 `manager-api` 后端发起异步HTTP(AJAX)请求,以获取数据或提交操作。
* **Webpack:** 一个强大的模块打包工具(由Vue CLI在底层管理和配置)。它将项目中的各种资源(JavaScript文件、CSS、图片、字体等)视为模块,并将它们打包成浏览器可识别的静态文件。
* **Workbox (通过 `workbox-webpack-plugin`):** Google开发的一个库,用于简化Service Worker的编写和PWAProgressive Web App - 渐进式Web应用)的实现。它可以帮助生成Service Worker脚本,实现资源缓存、离线访问等功能。
* **Opus库 (`opus-decoder`, `opus-recorder`):** 这些音频处理库表明前端可能具备一些直接在浏览器中处理Opus格式音频的能力,例如:用于测试麦克风输入、允许管理员录制自定义音频片段(可能用于TTS音色样本或语音指令测试),或播放在管理界面中预览的Opus编码音频。
* **关键实现细节:**
1. **单页应用 (SPA) 结构:**
* 整个前端应用加载一个主HTML文件 (`public/index.html`)。后续的所有页面切换和内容更新都在客户端由Vue Router动态完成,无需每次都从服务器请求新的HTML页面。这种模式能提供更快的页面加载速度和更流畅的交互体验。
2. **组件化架构 (Component-Based Architecture):**
* 用户界面由一系列可复用的Vue组件 (`.vue` 单文件组件) 构成,形成一个组件树。这种方式提高了代码的模块化程度、可维护性和复用性。
* **`src/main.js`:** 应用的入口JS文件。它负责创建和初始化根Vue实例,注册全局插件(如Vue Router, Vuex, Element UI),并把根Vue实例挂载到 `public/index.html` 中的某个DOM元素上(通常是 `#app`)。
* **`src/App.vue`:** 应用的根组件。它通常定义了应用的基础布局结构(如包含导航栏、侧边栏、主内容区),并通过 `<router-view></router-view>` 标签来显示当前路由匹配到的视图组件。
* **视图组件 (`src/views/`):** 这些组件代表了应用中的各个“页面”或主要功能区(例如 `Login.vue` 登录页, `DeviceManagement.vue` 设备管理页, `UserManagement.vue` 用户管理页, `ModelConfig.vue` 模型配置页)。它们通常由Vue Router直接映射。
* **可复用UI组件 (`src/components/`):** 包含了在不同视图之间共享的、更小粒度的UI组件(例如 `HeaderBar.vue` 顶部导航栏, `AddDeviceDialog.vue` 添加设备对话框, `AudioPlayer.vue` 音频播放器组件)。
3. **客户端路由 (`src/router/index.js`):**
* Vue Router在此文件中进行配置,定义了应用的路由表。每个路由规则将一个特定的URL路径映射到一个视图组件。
* 常常包含**导航守卫 (Navigation Guards)**,例如 `beforeEach` 守卫,用于在路由跳转前执行逻辑,如检查用户是否已登录,如果未登录则重定向到登录页面,从而保护需要认证才能访问的页面。
4. **状态管理 (`src/store/index.js`):**
* Vuex被用来构建一个集中的状态管理中心(Store)。这个Store包含了:
* **State:** 存储应用级别的共享数据(例如,当前登录用户的详细信息、从API获取的设备列表、系统配置等)。
* **Getters:** 类似于Vue组件中的计算属性,用于从State派生出一些状态值,方便组件使用。
* **Mutations:** **唯一**可以同步修改State中数据的方法。它们必须是同步函数。
* **Actions:** 用于处理异步操作(如API调用)或封装多个Mutation提交。Actions会调用API,获取数据后,通过 `commit` 一个或多个Mutation来更新State。
* 例如,用户登录时,一个名为 `login` 的Action可能会被调用,它会向后端API发送登录请求,成功后获取到用户信息和token,然后 `commit` 一个名为 `SET_USER_INFO` 的Mutation来更新State中的用户信息和token。
5. **API通信 (`src/apis/`):**
*`manager-api` 后端的所有HTTP通信逻辑被封装在 `src/apis/` 目录下,通常会按照后端API的模块进行组织(例如 `src/apis/module/agent.js`, `src/apis/module/device.js`)。
* 每个模块导出一系列函数,每个函数对应一个具体的API请求。这些函数内部使用配置好的HTTP客户端实例 (例如,在 `src/apis/api.js``src/apis/httpRequest.js` 中统一配置Axios或Flyio实例,可能包含设置请求基地址、请求/响应拦截器等)。
* **拦截器 (Interceptors):** HTTP客户端的请求拦截器常用于在每个请求发送前自动添加认证令牌(如JWT);响应拦截器则可用于全局处理API错误(如权限不足、服务器错误)或对响应数据进行预处理。
6. **样式与资源 (`src/styles/`, `src/assets/`):**
* `Element UI` 提供了基础的组件样式。
* `src/styles/global.scss` 文件用于定义全局共享的SCSS样式、变量、混合(Mixin)等。
* Vue单文件组件内部的 `<style scoped>` 标签允许编写只作用于当前组件的局部样式。
* `src/assets/` 目录存放图片、字体等静态资源。
7. **构建与PWA特性:**
* Vue CLI通过Webpack将所有代码和资源打包成优化的静态文件,用于生产部署。
* `workbox-webpack-plugin` 的使用(体现在 `service-worker.js``registerServiceWorker.js` 文件)表明项目集成了Service Worker技术。Service Worker可以拦截网络请求,实现前端资源的智能缓存(从而加快后续访问速度),甚至在网络断开时提供一定的离线访问能力,是PWA的核心技术之一。
8. **环境配置 (`.env`系列文件):**
* 项目根目录下的 `.env` (以及 `.env.development`, `.env.production` 等) 文件用于定义环境变量。这些变量(例如 `VUE_APP_API_BASE_URL` 来指定 `manager-api` 的基础URL)可以在应用代码中通过 `process.env.VUE_APP_XXX` 的形式访问,从而允许为不同构建环境(开发、测试、生产)配置不同的参数。
`manager-web` 通过这些技术的综合运用,构建了一个功能强大、易于维护且用户体验良好的管理界面,为 `xiaozhi-esp32-server` 系统的配置和监控提供了坚实的前端支持。
---
## 4. 数据流与交互机制
`xiaozhi-esp32-server` 系统通过各组件间定义清晰的数据流和交互协议来协同工作。主要的通信方式依赖于针对实时交互优化的WebSocket协议和适用于客户端-服务器请求的RESTful API。
**4.1.核心语音交互流程 (ESP32设备 <-> `xiaozhi-server`)**
此流程是实时的,主要通过WebSocket进行低延迟、双向的数据交换。
* **通讯协议文档:**
* 详细的通讯协议说明文档可通过以下地址访问: https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh
* 该文档详细描述了ESP32设备与`xiaozhi-server`之间的WebSocket通信协议,包括:
* 连接建立与握手流程
* 音频数据传输格式
* 控制命令格式
* 状态报告格式
* 错误处理机制
* **连接建立与握手:**
* ESP32设备作为客户端,主动向`xiaozhi-server`的指定端点(例如 `ws://<服务器IP>:<WebSocket端口>/xiaozhi/v1/`)发起WebSocket连接请求。
* `xiaozhi-server` (`core/websocket_server.py`) 接收连接,并为每个成功连接的ESP32设备实例化一个独立的`ConnectionHandler`对象来管理该会话的整个生命周期。
* 连接建立后,可能会执行一个初始握手流程(由`core/handle/helloHandle.py`处理),用于交换设备标识、认证信息、协议版本或基本状态。
* **音频上行传输 (ESP32 -> `xiaozhi-server`):**
* 用户对ESP32设备讲话后,设备上的麦克风捕捉原始音频数据(通常是PCM或经过压缩如Opus的格式)。
* ESP32将这些音频数据块(chunks)作为WebSocket的**二进制消息 (binary messages)** 实时推送到`xiaozhi-server`对应的`ConnectionHandler`
* 服务器端的`core/handle/receiveAudioHandle.py`模块负责接收、缓冲并处理这些音频数据。
* **AI核心处理 (在`xiaozhi-server`内部):**
* **VAD (语音活动检测):** `receiveAudioHandle.py`利用配置的VAD提供者(如SileroVAD)分析音频流,以准确识别语音的起始和结束点,滤除静默或噪声片段。
* **ASR (自动语音识别):** 检测到的有效语音片段被送往配置的ASR提供者(本地如FunASR,或云端服务)。ASR引擎将音频信号转换为文本字符串。
* **NLU/LLM (自然语言理解/大型语言模型):** ASR输出的文本,连同从Memory提供者获取的当前对话上下文历史,以及从`plugins_func/`加载的可用函数(工具)的描述模式,一同被传递给配置的LLM提供者。
* **函数调用执行 (若LLM决策需要):** 如果LLM分析后认为需要调用外部函数(例如查询天气、控制家电),它会生成一个结构化的函数调用请求。`core/handle/functionHandler.py`接收此请求,查找并执行在`plugins_func/`中定义的相应Python函数,并将函数的执行结果返回给LLM。LLM随后基于此结果生成最终的自然语言回复。
* **回复生成:** LLM综合所有信息(用户输入、上下文、函数调用结果等)生成最终的文本回复。
* **记忆更新:** 当前轮次的交互(用户问题、LLM回复、可能的功能调用)会被Memory提供者处理,以更新对话历史,供后续交互使用。
* **TTS (文本转语音):** LLM生成的最终文本回复被送往配置的TTS提供者,后者将文本合成为语音数据流(例如MP3或WAV格式)。
* **音频下行响应 (`xiaozhi-server` -> ESP32):**
* 由TTS提供者合成的语音数据流,通过`core/handle/sendAudioHandle.py`模块,作为WebSocket的**二进制消息**实时发送回ESP32设备。
* ESP32设备接收这些音频数据块并立即通过扬声器播放给用户。
* **控制与状态消息 (双向):**
* 除了音频流,ESP32与`xiaozhi-server`之间也通过WebSocket交换**文本消息 (text messages)**,这些消息通常采用JSON格式封装。
* **ESP32 -> Server:** 设备可能发送状态报告(如网络状况、麦克风状态)、错误代码、或特定的控制命令(例如用户按键触发的“停止TTS播报”)。
* **Server -> ESP32:** 服务器可能发送控制指令给设备(如“开始监听”、“停止监听”、调整灵敏度、下发特定配置参数)。
* `core/handle/abortHandle.py`(处理中断请求)、`core/handle/reportHandle.py`(处理设备报告)等模块负责解析和响应这些控制/状态消息。
**4.2.管理与配置流程 (`manager-web` <-> `manager-api` <-> `xiaozhi-server`)**
此流程主要依赖于基于HTTP/HTTPS的RESTful API进行请求-响应式的交互。
* **管理员UI后端交互 (`manager-web` -> `manager-api`):**
* 当管理员在`manager-web`界面执行操作时(例如保存一项配置、添加一个新用户、注册一台ESP32设备):
* Vue.js前端应用 (`manager-web`) 会通过其API封装模块(位于`src/apis/module/`)向`manager-api`的对应REST API端点发起异步HTTP请求(通常是GET, POST, PUT, DELETE)。
* 请求体和响应体通常使用JSON格式。
* `manager-api`中的`@RestController`类接收这些请求。**Apache Shiro**框架会首先对请求进行认证和授权检查。
* 通过验证后,Controller将请求分发给相应的Service层处理业务逻辑。Service层可能会与MySQL数据库(通过MyBatis-Plus)交互,并可能利用Redis进行数据缓存。
* 处理完成后,`manager-api``manager-web`返回一个JSON格式的HTTP响应。
* `manager-web`根据响应结果更新其Vuex状态存储和用户界面显示。
* **配置同步 (`manager-api` -> `xiaozhi-server`):**
* `xiaozhi-server`的运行依赖于从`manager-api`获取的动态配置(例如当前选用的AI服务提供商及其API密钥)。
* **拉取机制 (Pull Mechanism):** `xiaozhi-server`内部的`config/manage_api_client.py`模块,在服务器启动时或通过特定更新触发器(例如`WebSocketServer.update_config()`被调用),会向`manager-api`的一个指定端点(例如由`modules/config/controller/`中的某个Controller提供)发起HTTP GET请求。
* `manager-api`响应该请求,返回`xiaozhi-server`所需的配置数据(JSON格式)。
* `xiaozhi-server`接收到配置后,会更新其内部状态,并可能重新初始化相关的AI服务模块,以使新配置生效。
* **OTA固件更新流程 (概念性描述):**
* 管理员通过`manager-web`界面上传新的ESP32固件包到`manager-api`的特定端点。
* `manager-api`将固件文件存储起来,并记录相关元数据(版本号、适用设备型号等)。
* 当管理员触发对特定设备的OTA更新时:
* `manager-api`可能会通知`xiaozhi-server`(具体通知机制可能是一个轮询检查点,或`xiaozhi-server`暴露一个接收更新通知的API,或者更松耦合的如消息队列)。
* `xiaozhi-server`随后可以通过WebSocket向目标ESP32设备发送一条包含固件下载URL的指令消息。
* ESP32设备收到指令后,通过HTTP GET请求从该URL下载固件。此URL可能指向`xiaozhi-server`自身运行的`SimpleHttpServer`所服务的路径(如`/xiaozhi/ota/`),或者在某些架构中,也可能直接指向`manager-api`或专用的文件服务器。
**4.3. 主要协议总结:**
* **WebSocket:** 被选用于ESP32与`xiaozhi-server`之间的通信链路,因为它非常适合实时、低延迟、双向的数据流传输(尤其是音频),以及异步控制消息的传递。
* **RESTful APIs (基于HTTP/HTTPS,通常使用JSON作为数据交换格式):** 这是Web服务间通信的标准方式。用于`manager-web`(客户端)与`manager-api`(服务器)之间的请求-响应交互,也用于`xiaozhi-server`(作为客户端)从`manager-api`(作为服务器)拉取配置信息。其无状态特性、广泛的库支持和易于理解的语义使其成为此类交互的理想选择。
这种多协议并用的通信策略,确保了系统内不同类型的交互需求都能得到高效和恰当的处理,兼顾了实时性和标准化的请求-响应模式。
---
## 5. 核心功能概要
`xiaozhi-esp32-server` 系统提供了一系列丰富的功能,旨在支持开发者构建先进的语音控制应用:
1. **全面的语音交互后端:** 提供从语音捕获指导到响应生成和动作执行的端到端解决方案。
2. **模块化和可插拔的AI服务:**
* 支持广泛的ASR(自动语音识别)、LLM(大型语言模型)、TTS(文本转语音)、VAD(语音活动检测)、意图识别和记忆提供商。
* 允许动态选择和配置这些服务(包括基于云的API和本地模型),以平衡成本、性能、隐私和语言需求。
3. **高级对话管理:**
* 支持自然交互,具有唤醒词启动对话、手动(按键说话式)对话以及对系统响应的实时打断等功能。
* 包含上下文记忆,以在多轮对话中保持连贯性。
* 在一段时间不活动后具有自动休眠模式。
4. **多语言能力:**
* 支持多种语言的识别和合成,包括普通话、粤语、英语、日语和韩语(具体取决于所选的ASR/LLM/TTS提供商)。
5. **通过插件实现的可扩展功能:**
* 强大的插件系统允许开发人员添加自定义“技能”或函数(例如,获取天气、控制智能家居设备、访问新闻)。
* 这些函数可以由LLM使用其函数调用能力,根据提供的模式来触发。
* 内置对Home Assistant集成的支持。
6. **物联网设备控制:**
* 设计用于通过语音命令管理和控制智能家居设备及其他物联网硬件,并利用插件系统。
7. **基于Web的管理控制台 (`manager-web` & `manager-api`):**
* 提供全面的图形界面,用于:
* 系统配置(AI服务选择、API密钥、操作参数)。
* 基于角色的访问控制的用户管理。
* ESP32设备注册和管理。
* 语音音色/TTS语音定制。
* ESP32设备的OTA(空中下载)固件更新管理。
* 系统参数和字典的管理。
8. **灵活的部署选项:**
* 支持通过Docker容器(用于简化的仅服务器或全栈设置)和直接从源代码部署,以适应各种环境和用户专业知识。
9. **动态远程配置:**
* `xiaozhi-server`可以从`manager-api`获取其配置,允许实时更新AI提供商和设置,而无需重新启动服务器。
10. **开源和社区驱动:**
* 根据MIT许可证授权,鼓励透明、协作和社区贡献。
11. **经济高效的解决方案:**
* 提供“入门全免费设置”路径,利用AI服务的免费套餐或本地模型,使其易于进行实验和个人项目。
12. **渐进式Web应用 (PWA) 特性:**
* `manager-web`控制面板包含Service Worker集成,以增强缓存和潜在的离线访问能力。
13. **详细的API文档:**
* `manager-api`通过Knife4j提供OpenAPI (Swagger) 文档,以便清晰理解和测试其RESTful端点。
这些功能共同使`xiaozhi-esp32-server`成为一个强大、适应性强且用户友好的平台,用于构建复杂的语音交互应用程序。
---
## 6. 部署与配置概述
`xiaozhi-esp32-server`系统在设计上充分考虑了灵活性,提供了多种部署方法和全面的配置选项,以适应不同的使用场景和需求。
**部署选项:**
项目可以通过多种方式部署,主要包括使用Docker简化安装过程,或直接从源代码部署以获得更大的控制权和进行开发。
1. **基于Docker的部署:**
* **简化安装 (仅`xiaozhi-server`):** 此选项仅部署核心的基于Python的`xiaozhi-server`。它适用于主要需要语音AI处理能力和IoT控制,而不需要完整Web管理界面和数据库支持功能(如OTA)的用户。在此模式下,配置通常通过本地文件(`config.yaml`)管理,但如果需要,仍可将其指向一个已存在的`manager-api`实例。
* **全模块安装 (所有组件):** 此方案部署所有核心组件:`xiaozhi-server`、基于Java的`manager-api`、以及基于Vue.js的`manager-web`,同时还包括所需的数据库服务(MySQL和Redis)。这提供了完整的系统体验,包括用于全面配置和管理的Web控制面板。
* 项目为每个服务提供了`Dockerfile`定义,并使用`docker-compose.yml`文件(例如`docker-compose.yml`用于基础版,`docker-compose_all.yml`用于全功能版)来编排和管理多容器的部署。此外,还可能提供一个`docker-setup.sh`脚本来辅助自动化部分Docker环境的搭建工作。
2. **源代码部署:**
* 这种方法需要为每个组件手动设置相应的开发环境:Python环境用于`xiaozhi-server`Java/Maven环境用于`manager-api`Node.js/Vue CLI环境用于`manager-web`
* 对于全模块安装,还需要手动安装和配置MySQL及Redis数据库服务。
* 这种方式通常用于项目开发、深度定制、调试,或者在对环境有特殊要求的生产场景中。
**配置管理:**
配置是定制系统行为的关键,尤其是在选择AI服务提供商和管理API密钥方面。
1. **`xiaozhi-server` 配置:**
* **本地`config.yaml`:** 位于`xiaozhi-server`根目录下的一个主要的YAML格式配置文件。它定义了服务器端口、选定的AI服务提供商(ASR、LLM、TTS、VAD、意图识别、记忆模块等)、它们各自的API密钥或模型路径、插件配置以及日志级别等。
* **通过`manager-api`进行远程配置:** `xiaozhi-server`被设计为可以从`manager-api`获取其运行配置。从`manager-api`获取的设置通常会覆盖本地`config.yaml`中的同名设置。这带来了两大好处:
* **集中管理:** 所有配置都可以通过`manager-web`界面进行统一管理。
* **动态更新:** `xiaozhi-server`可以刷新其配置并重新初始化AI模块,而无需完全重启服务。
* `xiaozhi-server`中的`config/config_loader.py``config/manage_api_client.py`负责处理配置的加载、合并及从`manager-api`拉取的逻辑。
2. **`manager-api` 配置:**
* 作为一个Spring Boot应用,其配置主要通过位于`src/main/resources`目录下的`application.properties``application.yml`文件进行管理。
* 关键配置项包括:数据库连接信息(MySQL的URL、用户名、密码)、Redis服务器地址和端口、应用服务端口(默认为8002)、Apache Shiro安全相关的设置,以及任何集成的第三方服务(如阿里云短信)的配置参数。
3. **`manager-web` 配置:**
* Vue.js前端应用的环境特定设置通过项目根目录下的`.env`系列文件(例如`.env`, `.env.development`, `.env.production`)进行管理。
* 这里最关键的配置通常是`manager-api`后端的API基础URL地址 (例如 `VUE_APP_API_BASE_URL`),前端应用将向此地址发送所有API请求。
4. **预定义的配置方案:**
* 项目文档(通常是README)中会推荐一些常见的配置组合,例如:
* **“入门全免费设置”:** 该方案旨在利用云AI服务的免费套餐额度或完全免费的本地模型,以最大程度地降低用户的初始使用成本和运营费用。
* **“全流式配置”:** 该方案优先考虑系统的响应速度和交互的流畅性,通常会选用支持流式处理的(可能付费的)AI服务。
* 这些预定义方案为用户在`xiaozhi-server`中配置AI服务提供商(通过`manager-web`界面或直接修改`config.yaml`)提供了指导。
在全模块部署的情况下,推荐使用`manager-web`控制面板作为大多数配置任务的主要操作界面,因为它提供了一种用户友好的方式来管理由`manager-api`持久化并最终由`xiaozhi-server`使用的各项设置。
---
+369
View File
@@ -0,0 +1,369 @@
# Technical Documentation: `xiaozhi-esp32-server`
**Table of Contents:**
1. [Introduction](#1-introduction)
2. [Overall Architecture](#2-overall-architecture)
3. [Component Deep Dive](#3-component-deep-dive)
* [3.1. `xiaozhi-server` (Core AI Engine - Python Implementation)](#31-xiaozhi-server-core-ai-engine---python-implementation)
* [3.2. `manager-api` (Management Backend - Java Spring Boot Implementation)](#32-manager-api-management-backend---java-spring-boot-implementation)
* [3.3. `manager-web` (Web Management Frontend - Vue.js Implementation)](#33-manager-web-web-management-frontend---vuejs-implementation)
4. [Data Flow and Interaction Mechanisms](#4-data-flow-and-interaction-mechanisms)
5. [Key Features Summary](#5-key-features-summary)
6. [Deployment and Configuration Overview](#6-deployment-and-configuration-overview)
---
## 1. Introduction
The `xiaozhi-esp32-server` project is a **comprehensive backend system** designed to support intelligent hardware based on ESP32. Its core goal is to enable developers to quickly build a robust server infrastructure that can understand natural language commands, interact efficiently with various AI services (for speech recognition, natural language understanding, and speech synthesis), manage IoT devices, and provide a web-based user interface for system configuration and management. By integrating multiple cutting-edge technologies into a cohesive and extensible platform, this project aims to simplify and accelerate the development process of customizable voice assistants and intelligent control systems. It is not just a simple server, but a bridge connecting hardware, AI capabilities, and user management.
---
## 2. Overall Architecture
The `xiaozhi-esp32-server` system adopts a **distributed, multi-component collaborative** architectural design, ensuring modularity, maintainability, and scalability. Each core component has its specific role and works in coordination. The main components include:
1. **ESP32 Hardware (Client Device):**
This is the physical smart hardware device that end-users directly interact with. Its main responsibilities include:
* Capturing user voice commands.
* Securely sending captured raw audio data to `xiaozhi-server` for processing.
* Receiving synthesized voice responses from `xiaozhi-server` and playing them through speakers.
* Controlling other connected peripherals or IoT devices (such as smart bulbs, sensors, etc.) based on instructions received from `xiaozhi-server`.
2. **`xiaozhi-server` (Core AI Engine - Python Implementation):**
This Python-based server is the "brain" of the entire system, responsible for handling all voice-related logic and AI interactions. Its key responsibilities are detailed as follows:
* Establishing **stable, low-latency real-time bidirectional communication links** with ESP32 devices through the WebSocket protocol.
* Receiving audio streams from ESP32 and using Voice Activity Detection (VAD) technology to precisely segment valid speech segments.
* Integrating and calling Automatic Speech Recognition (ASR) services (configurable for local or cloud), converting speech segments to text.
* Interacting with Large Language Models (LLMs) to parse user intent, generate intelligent responses, and support complex natural language understanding tasks.
* Managing context information and user memory in multi-turn dialogues to provide coherent interaction experiences.
* Calling Text-to-Speech (TTS) services to synthesize natural and fluent speech from LLM-generated text responses.
* Executing custom commands through a flexible **plugin system**, including IoT device control logic.
* Obtaining its detailed runtime operation configuration from the `manager-api` service.
3. **`manager-api` (Management Backend - Java Spring Boot Implementation):**
This is an application built using the Java Spring Boot framework, providing a secure RESTful API for system management and configuration. It serves not only as the backend support for the `manager-web` console but also as the configuration data source for `xiaozhi-server`. Its core functions include:
* Providing user authentication (login, permission verification) and user account management functions for the Web console.
* Registration, information management of ESP32 devices, and maintenance of device-specific configurations.
* Persistently storing system configurations in the **MySQL database**, such as user-selected AI service providers, API keys, device parameters, plugin settings, etc.
* Providing specific API endpoints for `xiaozhi-server` to pull its required latest configuration.
* Managing TTS voice options, handling OTA (Over-The-Air) firmware update processes, and related metadata.
* Utilizing **Redis** as a high-speed cache to store hotspot data (such as session information, frequently accessed configurations) to improve API response speed and overall system performance.
4. **`manager-web` (Web Control Panel - Vue.js Implementation):**
This is a Single Page Application (SPA) built with Vue.js, providing system administrators with a graphical, user-friendly operation interface. Its main capabilities include:
* Conveniently configuring various AI services used by `xiaozhi-server` (such as ASR, LLM, TTS provider switching, parameter adjustment).
* Managing platform user accounts, role assignment, and permission control.
* Managing registered ESP32 devices and their related settings.
* (Potential functionality) Monitoring system operation status, viewing logs, troubleshooting, etc.
* Comprehensive interaction with all backend management functions provided by `manager-api`.
**High-Level Interaction Flow Overview:**
* **Voice Interaction Main Line:** After the **ESP32 device** captures user voice, it transmits audio data in real-time to **`xiaozhi-server`** through **WebSocket**. After `xiaozhi-server` completes a series of AI processing (VAD, ASR, LLM interaction, TTS), it sends the synthesized voice response back to the ESP32 device for playback through WebSocket. All real-time interactions directly related to voice are completed in this link.
* **Management Configuration Main Line:** Administrators access the **`manager-web`** console through a browser. `manager-web` executes various management operations (such as modifying configurations, managing users or devices) by calling **RESTful HTTP interfaces** provided by **`manager-api`**. Data is passed between them in JSON format.
* **Configuration Synchronization:** **`xiaozhi-server`** actively pulls its latest operation configuration from **`manager-api`** through HTTP requests when starting or when specific update mechanisms are triggered. This ensures that configuration changes made by administrators in the Web interface can be effectively applied to the operation of the core AI engine in a timely manner.
This **frontend-backend separation, core service and management service separation** architectural design allows `xiaozhi-server` to focus on efficient real-time AI processing tasks, while `manager-api` and `manager-web` together provide a powerful and easy-to-use management and configuration platform. Each component has clear responsibilities, facilitating independent development, testing, deployment, and expansion.
```
xiaozhi-esp32-server
├─ xiaozhi-server Port 8000 Python development Responsible for ESP32 communication
├─ manager-web Port 8001 Node.js+Vue development Responsible for providing web interface for console
├─ manager-api Port 8002 Java development Responsible for providing console API
```
---
## 3. Component Deep Dive
### 3.1. `xiaozhi-server` (Core AI Engine - Python Implementation)
The `xiaozhi-server` is the intelligent core of the system, responsible for processing voice interactions, interfacing with AI services, and managing communication with ESP32 devices.
* **Purpose:**
* To provide real-time processing of voice commands from ESP32 devices.
* To integrate with various AI services for Speech-to-Text (ASR), Natural Language Understanding (via Large Language Models - LLMs), Text-to-Speech (TTS), Voice Activity Detection (VAD), Intent Recognition, and Memory.
* To manage dialogue flow and context with users.
* To execute custom functions and control IoT devices based on user commands.
* To be dynamically configurable through the `manager-api`.
* **Core Technologies:**
* **Python 3:** The primary programming language.
* **Asyncio:** Python's asynchronous programming framework, crucial for handling concurrent WebSocket connections and non-blocking I/O for AI service API calls.
* **`websockets` Library:** For WebSocket server implementation.
* **HTTP Client (e.g., `aiohttp`, `httpx`):** For asynchronous HTTP requests to `manager-api` and external AI services.
* **YAML (PyYAML):** For local configuration file parsing.
* **Key Implementation Aspects:**
1. **AI Service Provider Pattern (`core/providers/`):**
* **Concept:** A flexible design for integrating AI services. Each service type (ASR, TTS, LLM, etc.) has an abstract base class defining a common interface. Concrete classes implement this interface for specific vendors or local models.
* **Benefit:** Allows easy switching of AI service backends via configuration and simplifies adding new service integrations.
* **Initialization:** `core/utils/modules_initialize.py` acts as a factory to load and instantiate configured providers.
2. **WebSocket Communication & Connection Handling (`core/websocket_server.py`, `core/connection.py`):**
* **Server Setup:** Manages WebSocket connections from ESP32 devices.
* **Connection Isolation:** Each ESP32 client gets a dedicated `ConnectionHandler` instance, isolating its session state and dialogue.
* **Dynamic Configuration Updates:** Can fetch updated configurations from `manager-api` and re-initialize AI service modules live, without a full server restart.
3. **Message Handling & Dialogue Flow (`core/handle/`):**
* Employs a modular handler pattern. The `ConnectionHandler` dispatches message processing to specialized modules based on message type or dialogue phase (e.g., `receiveAudioHandle.py` for audio input, `intentHandler.py` for NLU, `functionHandler.py` for plugin execution, `sendAudioHandle.py` for TTS output).
4. **Plugin System for Extensible Functions (`plugins_func/`):**
* **Purpose:** Allows adding custom "skills" (e.g., weather, news, Home Assistant control).
* **Mechanism:** Plugins define functions and schemas. The LLM can request execution of these functions (function calling). `loadplugins.py` and `register.py` manage plugin discovery and registration.
5. **Configuration Management (`config/`):**
* Loads settings from a local `config.yaml` and merges them with configurations fetched from `manager-api` (via `manage_api_client.py`), enabling remote dynamic configuration.
* `logger.py` sets up structured application logging.
* `config/assets/` stores predefined audio files for system notifications.
6. **Auxiliary HTTP Server (`core/http_server.py`):**
* Handles specific HTTP requests, notably for OTA firmware updates (`/xiaozhi/ota/`) and other utility endpoints.
### 3.2. `manager-api` (Management Backend - Java Spring Boot Implementation)
The `manager-api` component is a backend server built using Java and the Spring Boot framework, serving as the administrative hub.
* **Purpose:**
* Provide a secure RESTful API for the `manager-web` frontend.
* Act as a centralized configuration provider for `xiaozhi-server`.
* Manage persistent data (users, devices, AI configurations, voice timbres, OTA firmware).
* **Core Technologies:**
* **Java 21 & Spring Boot 3:** Core language and framework.
* **Spring MVC:** For building REST controllers.
* **MyBatis-Plus:** ORM for database interaction with MySQL.
* **MySQL:** Relational database.
* **Druid:** JDBC connection pool.
* **Redis (Spring Data Redis):** For caching.
* **Apache Shiro:** Security framework for authentication and authorization.
* **Liquibase:** Database schema migration.
* **Knife4j:** OpenAPI (Swagger) API documentation.
* **Maven:** Build and dependency management.
* **Key Implementation Aspects:**
1. **Modular Architecture (`modules/` package):**
* Business logic is organized into distinct modules (e.g., `sys` for users/roles, `agent` for assistant configs, `device` for ESP32s, `config` for `xiaozhi-server` settings, `security`, `timbre`, `ota`).
* Each module typically follows a layered pattern: Controller, Service, DAO (Mapper), Entity, DTO.
2. **Layered Architecture:**
* **Controller Layer (`@RestController`):** Defines API endpoints, handles HTTP request/response.
* **Service Layer (`@Service`):** Contains business logic, transaction management.
* **Data Access Layer (MyBatis-Plus Mappers):** Interacts with the MySQL database.
3. **Common Functionalities (`common/` package):**
* Provides shared code: base classes, global configurations (Spring, MyBatis, Redis, Knife4j), custom annotations (e.g., `@LogOperation`), AOP aspects, global exception handling, utility classes, and XSS protection.
4. **Security (Apache Shiro):**
* Manages user authentication and permissions for accessing API endpoints. Configured with Shiro Realms and security filters.
5. **Database Schema Management (Liquibase):**
* Ensures consistent database structure across environments through versioned schema changes.
### 3.3. `manager-web` (Web Control Panel - Vue.js Implementation)
The `manager-web` is a Single Page Application (SPA) providing the administrative user interface.
* **Purpose:**
* Offer a web-based control panel for system configuration and management.
* Enable administrators to configure `xiaozhi-server`'s AI services, manage users and devices, customize voice timbres, and handle OTA updates.
* **Core Technologies:**
* **Vue.js 2 & Vue CLI:** Core JavaScript framework and build tools.
* **Vue Router:** For client-side routing within the SPA.
* **Vuex:** For centralized state management.
* **Element UI:** UI component library for a consistent look and feel.
* **SCSS:** CSS preprocessor.
* **HTTP Client (Flyio or Axios):** For API calls to `manager-api`.
* **Workbox:** For PWA features (caching, service worker).
* **Opus Libraries:** For potential in-browser audio recording/playback.
* **Key Implementation Aspects:**
1. **SPA Structure:** Single HTML page with dynamic view updates.
2. **Component-Based Architecture:** UI built from reusable Vue components (`.vue` files in `src/views/` for pages and `src/components/` for smaller elements).
3. **Client-Side Routing (`src/router/index.js`):** Maps browser URLs to view components, with route guards for authentication.
4. **State Management (`src/store/index.js`):** Vuex manages global state (user info, device lists, etc.) via state, getters, mutations, and actions (often involving API calls).
5. **API Communication (`src/apis/`):** Modularized API service files make asynchronous calls to `manager-api`.
6. **Build Process & PWA Features:** Vue CLI (Webpack) bundles assets. Workbox enables PWA features like caching.
7. **Environment Configuration (`.env` files):** Manages settings like the `manager-api` base URL for different environments.
---
## 4. Data Flow and Interaction Mechanisms
The `xiaozhi-esp32-server` system coordinates work through well-defined data flows and interaction protocols between components. The main communication methods rely on WebSocket protocol optimized for real-time interaction and RESTful API suitable for client-server requests.
**4.1. Core Voice Interaction Flow (ESP32 Device <-> `xiaozhi-server`)**
This flow is real-time, primarily using WebSocket for low-latency, bidirectional data exchange.
* **Communication Protocol Documentation:**
* Detailed communication protocol documentation can be accessed at: https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh
* This document details the WebSocket communication protocol between ESP32 devices and `xiaozhi-server`, including:
* Connection establishment and handshake process
* Audio data transmission format
* Control command format
* Status report format
* Error handling mechanism
* **Connection Establishment and Handshake:**
* The ESP32 device, as a client, actively initiates a WebSocket connection request to the specified endpoint of `xiaozhi-server` (e.g., `ws://<server-IP>:<WebSocket-port>/xiaozhi/v1/`).
* `xiaozhi-server` (`core/websocket_server.py`) receives the connection and instantiates an independent `ConnectionHandler` object for each successfully connected ESP32 device to manage the entire lifecycle of that session.
* After the connection is established, an initial handshake process may be executed (handled by `core/handle/helloHandle.py`) to exchange device identification, authentication information, protocol version, or basic status.
* **Audio Uplink Transmission (ESP32 -> `xiaozhi-server`):**
* After a user speaks to the ESP32 device, the device's microphone captures raw audio data (usually in PCM or compressed formats like Opus).
* The ESP32 pushes these audio data chunks as WebSocket **binary messages** in real-time to the corresponding `ConnectionHandler` in `xiaozhi-server`.
* The server-side `core/handle/receiveAudioHandle.py` module is responsible for receiving, buffering, and processing these audio data.
* **AI Core Processing (within `xiaozhi-server`):**
* **VAD (Voice Activity Detection):** `receiveAudioHandle.py` uses the configured VAD provider (such as SileroVAD) to analyze the audio stream, accurately identifying the start and end points of speech, filtering out silent or noise segments.
* **ASR (Automatic Speech Recognition):** Detected valid speech segments are sent to the configured ASR provider (local such as FunASR, or cloud services). The ASR engine converts audio signals into text strings.
* **NLU/LLM (Natural Language Understanding/Large Language Model):** The ASR output text, along with the current dialogue context history obtained from the Memory provider, and the description schemas of available functions (tools) loaded from `plugins_func/`, are passed to the configured LLM provider.
* **Function Call Execution (if LLM decides needed):** If the LLM analysis determines that an external function needs to be called (e.g., querying weather, controlling home appliances), it generates a structured function call request. `core/handle/functionHandler.py` receives this request, finds and executes the corresponding Python function defined in `plugins_func/`, and returns the function's execution result to the LLM. The LLM then generates the final natural language response based on this result.
* **Response Generation:** The LLM synthesizes all information (user input, context, function call results, etc.) to generate the final text response.
* **Memory Update:** The current round of interaction (user question, LLM response, possible function calls) is processed by the Memory provider to update the dialogue history for subsequent interactions.
* **TTS (Text-to-Speech):** The final text response generated by the LLM is sent to the configured TTS provider, which synthesizes the text into a speech data stream (e.g., MP3 or WAV format).
* **Audio Downlink Response (`xiaozhi-server` -> ESP32):**
* The speech data stream synthesized by the TTS provider is sent in real-time as WebSocket **binary messages** back to the ESP32 device through the `core/handle/sendAudioHandle.py` module.
* The ESP32 device receives these audio data chunks and immediately plays them to the user through the speaker.
* **Control and Status Messages (Bidirectional):**
* In addition to audio streams, ESP32 and `xiaozhi-server` also exchange **text messages** through WebSocket, these messages are usually encapsulated in JSON format.
* **ESP32 -> Server:** The device may send status reports (such as network conditions, microphone status), error codes, or specific control commands (e.g., "stop TTS playback" triggered by user button press).
* **Server -> ESP32:** The server may send control instructions to the device (such as "start listening", "stop listening", adjust sensitivity, send specific configuration parameters).
* Modules like `core/handle/abortHandle.py` (handling interrupt requests), `core/handle/reportHandle.py` (handling device reports) are responsible for parsing and responding to these control/status messages.
**4.2. Management and Configuration Flow (`manager-web` <-> `manager-api` <-> `xiaozhi-server`)**
This flow primarily relies on HTTP/HTTPS-based RESTful API for request-response interactions.
* **Administrator UI Backend Interaction (`manager-web` -> `manager-api`):**
* When administrators perform operations in the `manager-web` interface (e.g., saving a configuration, adding a new user, registering an ESP32 device):
* The Vue.js frontend application (`manager-web`) will initiate asynchronous HTTP requests (usually GET, POST, PUT, DELETE) to the corresponding REST API endpoints of `manager-api` through its API encapsulation module (located in `src/apis/module/`).
* Request and response bodies typically use JSON format.
* The `@RestController` classes in `manager-api` receive these requests. The **Apache Shiro** framework will first perform authentication and authorization checks on the requests.
* After verification, the Controller distributes the request to the corresponding Service layer to handle business logic. The Service layer may interact with the MySQL database (through MyBatis-Plus) and may utilize Redis for data caching.
* After processing, `manager-api` returns an HTTP response in JSON format to `manager-web`.
* `manager-web` updates its Vuex state store and user interface display based on the response results.
* **Configuration Synchronization (`manager-api` -> `xiaozhi-server`):**
* The operation of `xiaozhi-server` depends on dynamic configurations obtained from `manager-api` (such as currently selected AI service providers and their API keys).
* **Pull Mechanism:** The `config/manage_api_client.py` module within `xiaozhi-server`, when the server starts or through specific update triggers (e.g., when `WebSocketServer.update_config()` is called), will initiate an HTTP GET request to a specified endpoint of `manager-api` (e.g., provided by a Controller in `modules/config/controller/`).
* `manager-api` responds to this request, returning the configuration data required by `xiaozhi-server` (in JSON format).
* After receiving the configuration, `xiaozhi-server` will update its internal state and may reinitialize relevant AI service modules to make the new configuration effective.
* **OTA Firmware Update Flow (Conceptual Description):**
* Administrators upload new ESP32 firmware packages to specific endpoints of `manager-api` through the `manager-web` interface.
* `manager-api` stores the firmware files and records related metadata (version number, applicable device models, etc.).
* When administrators trigger OTA updates for specific devices:
* `manager-api` may notify `xiaozhi-server` (the specific notification mechanism may be a polling checkpoint, or `xiaozhi-server` exposes an API to receive update notifications, or more loosely coupled like message queues).
* `xiaozhi-server` can then send an instruction message containing the firmware download URL to the target ESP32 device through WebSocket.
* After receiving the instruction, the ESP32 device downloads the firmware through an HTTP GET request from that URL. This URL may point to a path served by the `SimpleHttpServer` running on `xiaozhi-server` itself (such as `/xiaozhi/ota/`), or in some architectures, it may directly point to `manager-api` or a dedicated file server.
**4.3. Main Protocol Summary:**
* **WebSocket:** Selected for the communication link between ESP32 and `xiaozhi-server` because it is very suitable for real-time, low-latency, bidirectional data stream transmission (especially audio), as well as asynchronous control message delivery.
* **RESTful APIs (based on HTTP/HTTPS, usually using JSON as the data exchange format):** This is the standard way for web service communication. Used for request-response interactions between `manager-web` (client) and `manager-api` (server), and also for `xiaozhi-server` (as client) to pull configuration information from `manager-api` (as server). Its stateless nature, wide library support, and easy-to-understand semantics make it an ideal choice for such interactions.
This multi-protocol communication strategy ensures that different types of interaction requirements within the system can be handled efficiently and appropriately, balancing real-time performance and standardized request-response patterns.
---
## 5. Key Features Summary
The `xiaozhi-esp32-server` system provides a series of rich features aimed at supporting developers in building advanced voice control applications:
1. **Comprehensive Voice Interaction Backend:** Provides an end-to-end solution from voice capture guidance to response generation and action execution.
2. **Modular and Pluggable AI Services:**
* Supports a wide range of ASR (Automatic Speech Recognition), LLM (Large Language Model), TTS (Text-to-Speech), VAD (Voice Activity Detection), Intent Recognition, and Memory providers.
* Allows dynamic selection and configuration of these services (including cloud-based APIs and local models) to balance cost, performance, privacy, and language requirements.
3. **Advanced Dialogue Management:**
* Supports natural interaction, with wake word to start dialogue, manual (push-to-talk) dialogue, and real-time interruption of system responses.
* Includes contextual memory to maintain coherence in multi-turn dialogues.
* Has automatic sleep mode after a period of inactivity.
4. **Multi-language Capabilities:**
* Supports recognition and synthesis in multiple languages, including Mandarin, Cantonese, English, Japanese, and Korean (specific capabilities depend on the selected ASR/LLM/TTS providers).
5. **Extensible Functions through Plugins:**
* Powerful plugin system allows developers to add custom "skills" or functions (e.g., getting weather, controlling smart home devices, accessing news).
* These functions can be triggered by the LLM using its function calling capability, based on provided schemas.
* Built-in support for Home Assistant integration.
6. **IoT Device Control:**
* Designed to manage and control smart home devices and other IoT hardware through voice commands, utilizing the plugin system.
7. **Web-based Management Console (`manager-web` & `manager-api`):**
* Provides a comprehensive graphical interface for:
* System configuration (AI service selection, API keys, operation parameters).
* Role-based access control user management.
* ESP32 device registration and management.
* Voice timbre/TTS voice customization.
* OTA (Over-The-Air) firmware update management for ESP32 devices.
* System parameter and dictionary management.
8. **Flexible Deployment Options:**
* Supports deployment through Docker containers (for simplified server-only or full-stack setup) and directly from source code, adapting to various environments and user expertise.
9. **Dynamic Remote Configuration:**
* `xiaozhi-server` can obtain its configuration from `manager-api`, allowing real-time updates of AI providers and settings without restarting the server.
10. **Open Source and Community-Driven:**
* Licensed under MIT License, encouraging transparency, collaboration, and community contribution.
11. **Cost-Effective Solution:**
* Provides an "Entry Level Free Settings" path, utilizing free tiers of AI services or local models, making it easy to experiment and for personal projects.
12. **Progressive Web Application (PWA) Features:**
* The `manager-web` control panel includes Service Worker integration to enhance caching and potential offline access capabilities.
13. **Detailed API Documentation:**
* `manager-api` provides OpenAPI (Swagger) documentation through Knife4j for clear understanding and testing of its RESTful endpoints.
These features together make `xiaozhi-esp32-server` a powerful, adaptable, and user-friendly platform for building complex voice interaction applications.
---
## 6. Deployment and Configuration Overview
The `xiaozhi-esp32-server` system is designed with flexibility in mind, providing multiple deployment methods and comprehensive configuration options to adapt to different usage scenarios and requirements.
**Deployment Options:**
The project can be deployed in multiple ways, mainly including using Docker to simplify the installation process, or deploying directly from source code for greater control and development.
1. **Docker-based Deployment:**
* **Simplified Installation (Only `xiaozhi-server`):** This option only deploys the core Python-based `xiaozhi-server`. It is suitable for users who mainly need voice AI processing capabilities and IoT control, without requiring the complete Web management interface and database support functions (such as OTA). In this mode, configuration is typically managed through local files (`config.yaml`), but if needed, it can still point to an existing `manager-api` instance.
* **Full Module Installation (All Components):** This scheme deploys all core components: `xiaozhi-server`, Java-based `manager-api`, and Vue.js-based `manager-web`, along with required database services (MySQL and Redis). This provides a complete system experience, including a Web control panel for comprehensive configuration and management.
* The project provides `Dockerfile` definitions for each service and uses `docker-compose.yml` files (e.g., `docker-compose.yml` for basic version, `docker-compose_all.yml` for full-featured version) to orchestrate and manage multi-container deployment. Additionally, a `docker-setup.sh` script may be provided to assist in automating part of the Docker environment setup work.
2. **Source Code Deployment:**
* This method requires manual setup of the corresponding development environment for each component: Python environment for `xiaozhi-server`, Java/Maven environment for `manager-api`, Node.js/Vue CLI environment for `manager-web`.
* For full module installation, MySQL and Redis database services also need to be manually installed and configured.
* This approach is typically used for project development, deep customization, debugging, or in production scenarios with special environmental requirements.
**Configuration Management:**
Configuration is key to customizing system behavior, especially in selecting AI service providers and managing API keys.
1. **`xiaozhi-server` Configuration:**
* **Local `config.yaml`:** A main YAML format configuration file located in the `xiaozhi-server` root directory. It defines server ports, selected AI service providers (ASR, LLM, TTS, VAD, Intent Recognition, Memory modules, etc.), their respective API keys or model paths, plugin configurations, and log levels.
* **Remote Configuration through `manager-api`:** `xiaozhi-server` is designed to obtain its operation configuration from `manager-api`. Settings obtained from `manager-api` typically override settings with the same name in the local `config.yaml`. This brings two major benefits:
* **Centralized Management:** All configurations can be managed uniformly through the `manager-web` interface.
* **Dynamic Updates:** `xiaozhi-server` can refresh its configuration and reinitialize AI modules without completely restarting the service.
* `config/config_loader.py` and `config/manage_api_client.py` in `xiaozhi-server` are responsible for handling configuration loading, merging, and pulling logic from `manager-api`.
2. **`manager-api` Configuration:**
* As a Spring Boot application, its configuration is mainly managed through the `application.properties` or `application.yml` file located in the `src/main/resources` directory.
* Key configuration items include: database connection information (MySQL URL, username, password), Redis server address and port, application service port (default 8002), Apache Shiro security-related settings, and configuration parameters for any integrated third-party services (such as Aliyun SMS).
3. **`manager-web` Configuration:**
* Environment-specific settings for the Vue.js frontend application are managed through `.env` series files (e.g., `.env`, `.env.development`, `.env.production`) in the project root directory.
* The most critical configuration here is usually the API base URL address of the `manager-api` backend (e.g., `VUE_APP_API_BASE_URL`), to which the frontend application will send all API requests.
4. **Predefined Configuration Schemes:**
* The project documentation (usually README) will recommend some common configuration combinations, for example:
* **"Entry Level Free Settings":** This scheme aims to utilize free tier quotas of cloud AI services or completely free local models to minimize users' initial usage costs and operating expenses.
* **"Full Streaming Configuration":** This scheme prioritizes system response speed and interaction fluency, typically choosing AI services that support streaming processing (possibly paid).
* These predefined schemes provide guidance for users to configure AI service providers in `xiaozhi-server` (through the `manager-web` interface or directly modifying `config.yaml`).
In the case of full module deployment, it is recommended to use the `manager-web` control panel as the main operation interface for most configuration tasks, as it provides a user-friendly way to manage various settings that are persisted by `manager-api` and ultimately used by `xiaozhi-server`.
---
@@ -111,6 +111,16 @@ public interface Constant {
*/ */
String FILE_EXTENSION_SEG = "."; String FILE_EXTENSION_SEG = ".";
/**
* mcp接入点路径
*/
String SERVER_MCP_ENDPOINT = "server.mcp_endpoint";
/**
* mcp接入点路径
*/
String SERVER_VOICE_PRINT = "server.voice_print";
/** /**
* 无记忆 * 无记忆
*/ */
@@ -227,7 +237,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.5.4"; public static final String VERSION = "0.7.2";
/** /**
* 无效固件URL * 无效固件URL
@@ -0,0 +1,78 @@
package xiaozhi.common.utils;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class AESUtils {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
/**
* AES加密
*
* @param key 密钥(16位、24位或32位)
* @param plainText 待加密字符串
* @return 加密后的Base64字符串
*/
public static String encrypt(String key, String plainText) {
try {
// 确保密钥长度为16、24或32位
byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
} catch (Exception e) {
throw new RuntimeException("AES加密失败", e);
}
}
/**
* AES解密
*
* @param key 密钥(16位、24位或32位)
* @param encryptedText 待解密的Base64字符串
* @return 解密后的字符串
*/
public static String decrypt(String key, String encryptedText) {
try {
// 确保密钥长度为16、24或32位
byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES解密失败", e);
}
}
/**
* 填充密钥到指定长度(16、24或32位)
*
* @param keyBytes 原始密钥字节数组
* @return 填充后的密钥字节数组
*/
private static byte[] padKey(byte[] keyBytes) {
int keyLength = keyBytes.length;
if (keyLength == 16 || keyLength == 24 || keyLength == 32) {
return keyBytes;
}
// 如果密钥长度不足,用0填充;如果超过,截取前32位
byte[] paddedKey = new byte[32];
System.arraycopy(keyBytes, 0, paddedKey, 0, Math.min(keyLength, 32));
return paddedKey;
}
}
@@ -0,0 +1,52 @@
package xiaozhi.common.utils;
import lombok.extern.slf4j.Slf4j;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 哈希加密算法的工具类
* @author zjy
*/
@Slf4j
public class HashEncryptionUtil {
/**
* 使用md5进行加密
* @param context 被加密的内容
* @return 哈希值
*/
public static String Md5hexDigest(String context){
return hexDigest(context,"MD5");
}
/**
* 指定哈希算法进行加密
* @param context 被加密的内容
* @param algorithm 哈希算法
* @return 哈希值
*/
public static String hexDigest(String context,String algorithm ){
// 获取MD5算法实例
MessageDigest md = null;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
log.error("加密失败的算法:{}",algorithm);
throw new RuntimeException("加密失败,"+ algorithm +"哈希算法系统不支持");
}
// 计算智能体id的MD5值
byte[] messageDigest = md.digest(context.getBytes());
// 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
@@ -0,0 +1,31 @@
package xiaozhi.common.utils;
/**
* 返回响应体工具类
*/
public class ResultUtils
{
public static <T> Result<T> success(T data) {
return new Result<T>().ok(data);
}
public static <T> Result<T> error() {
return new Result<T>().error();
}
public static <T> Result<T> error(String msg) {
return new Result<T>().error(msg);
}
public static <T> Result<T> error(int errorCode, String msg) {
return new Result<T>().error(errorCode, msg);
}
public static <T> Result<T> error(int errorCode) {
return new Result<T>().error(errorCode);
}
public static <T> Result<T> empty() {
return new Result<T>();
}
}
@@ -0,0 +1,21 @@
package xiaozhi.modules.agent.Enums;
import lombok.Getter;
/**
* 智能体聊天记录类型
*/
@Getter
public enum AgentChatHistoryType {
USER((byte) 1),
AGENT((byte) 2);
private final byte value;
AgentChatHistoryType(byte i) {
this.value = i;
}
}
@@ -1,6 +1,5 @@
package xiaozhi.modules.agent.controller; package xiaozhi.modules.agent.controller;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -33,8 +32,8 @@ 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.ConvertUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO;
@@ -45,8 +44,11 @@ import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
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.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.agent.vo.AgentChatHistoryUserVO;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
@@ -61,6 +63,7 @@ public class AgentController {
private final DeviceService deviceService; 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 RedisUtils redisUtils; private final RedisUtils redisUtils;
@GetMapping("/list") @GetMapping("/list")
@@ -88,45 +91,17 @@ public class AgentController {
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "获取智能体详情") @Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<AgentEntity> getAgentById(@PathVariable("id") String id) { public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
AgentEntity agent = agentService.getAgentById(id); AgentInfoVO agent = agentService.getAgentById(id);
return new Result<AgentEntity>().ok(agent); return ResultUtils.success(agent);
} }
@PostMapping @PostMapping
@Operation(summary = "创建智能体") @Operation(summary = "创建智能体")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) { public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class); String agentId = agentService.createAgent(dto);
return new Result<String>().ok(agentId);
// 获取默认模板
AgentTemplateEntity template = agentTemplateService.getDefaultTemplate();
if (template != null) {
// 设置模板中的默认值
entity.setAsrModelId(template.getAsrModelId());
entity.setVadModelId(template.getVadModelId());
entity.setLlmModelId(template.getLlmModelId());
entity.setTtsModelId(template.getTtsModelId());
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
entity.setChatHistoryConf(template.getChatHistoryConf());
entity.setLangCode(template.getLangCode());
entity.setLanguage(template.getLanguage());
}
// 设置用户ID和创建者信息
UserDetail user = SecurityUser.getUser();
entity.setUserId(user.getId());
entity.setCreator(user.getId());
entity.setCreatedAt(new Date());
// ID、智能体编码和排序会在Service层自动生成
agentService.insert(entity);
return new Result<String>().ok(entity.getId());
} }
@PutMapping("/saveMemory/{macAddress}") @PutMapping("/saveMemory/{macAddress}")
@@ -138,88 +113,15 @@ public class AgentController {
} }
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
return updateAgentById(device.getAgentId(), agentUpdateDTO); agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
return new Result<>();
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@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) {
return updateAgentById(id, dto); agentService.updateAgentById(id, dto);
}
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
// 先查询现有实体
AgentEntity existingEntity = agentService.getAgentById(id);
if (existingEntity == null) {
return new Result<Void>().error("智能体不存在");
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
if (dto.getAsrModelId() != null) {
existingEntity.setAsrModelId(dto.getAsrModelId());
}
if (dto.getVadModelId() != null) {
existingEntity.setVadModelId(dto.getVadModelId());
}
if (dto.getLlmModelId() != null) {
existingEntity.setLlmModelId(dto.getLlmModelId());
}
if (dto.getVllmModelId() != null) {
existingEntity.setVllmModelId(dto.getVllmModelId());
}
if (dto.getTtsModelId() != null) {
existingEntity.setTtsModelId(dto.getTtsModelId());
}
if (dto.getTtsVoiceId() != null) {
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
}
if (dto.getMemModelId() != null) {
existingEntity.setMemModelId(dto.getMemModelId());
}
if (dto.getIntentModelId() != null) {
existingEntity.setIntentModelId(dto.getIntentModelId());
}
if (dto.getSystemPrompt() != null) {
existingEntity.setSystemPrompt(dto.getSystemPrompt());
}
if (dto.getSummaryMemory() != null) {
existingEntity.setSummaryMemory(dto.getSummaryMemory());
}
if (dto.getChatHistoryConf() != null) {
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
}
if (dto.getLangCode() != null) {
existingEntity.setLangCode(dto.getLangCode());
}
if (dto.getLanguage() != null) {
existingEntity.setLanguage(dto.getLanguage());
}
if (dto.getSort() != null) {
existingEntity.setSort(dto.getSort());
}
// 设置更新者信息
UserDetail user = SecurityUser.getUser();
existingEntity.setUpdater(user.getId());
existingEntity.setUpdatedAt(new Date());
// 更新记忆策略
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
// 删除所有记录
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
existingEntity.setSummaryMemory("");
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
// 删除音频数据
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
}
agentService.updateById(existingEntity);
return new Result<>(); return new Result<>();
} }
@@ -231,6 +133,8 @@ public class AgentController {
deviceService.deleteByAgentId(id); deviceService.deleteByAgentId(id);
// 删除关联的聊天记录 // 删除关联的聊天记录
agentChatHistoryService.deleteByAgentId(id, true, true); agentChatHistoryService.deleteByAgentId(id, true, true);
// 删除关联的插件
agentPluginMappingService.deleteByAgentId(id);
// 再删除智能体 // 再删除智能体
agentService.deleteById(id); agentService.deleteById(id);
return new Result<>(); return new Result<>();
@@ -278,6 +182,33 @@ public class AgentController {
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId); List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
return new Result<List<AgentChatHistoryDTO>>().ok(result); return new Result<List<AgentChatHistoryDTO>>().ok(result);
} }
@GetMapping("/{id}/chat-history/user")
@Operation(summary = "获取智能体聊天记录(用户)")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentChatHistoryUserVO>> getRecentlyFiftyByAgentId(
@PathVariable("id") String id) {
// 获取当前用户
UserDetail user = SecurityUser.getUser();
// 检查权限
if (!agentService.checkAgentPermission(id, user.getId())) {
return new Result<List<AgentChatHistoryUserVO>>().error("没有权限查看该智能体的聊天记录");
}
// 查询聊天记录
List<AgentChatHistoryUserVO> data = agentChatHistoryService.getRecentlyFiftyByAgentId(id);
return new Result<List<AgentChatHistoryUserVO>>().ok(data);
}
@GetMapping("/{id}/chat-history/audio")
@Operation(summary = "获取音频内容")
@RequiresPermissions("sys:role:normal")
public Result<String> getContentByAudioId(
@PathVariable("id") String id) {
// 查询聊天记录
String data = agentChatHistoryService.getContentByAudioId(id);
return new Result<String>().ok(data);
}
@PostMapping("/audio/{audioId}") @PostMapping("/audio/{audioId}")
@Operation(summary = "获取音频下载ID") @Operation(summary = "获取音频下载ID")
@@ -0,0 +1,66 @@
package xiaozhi.modules.agent.controller;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体Mcp接入点管理")
@RequiredArgsConstructor
@RestController
@RequestMapping("/agent/mcp")
public class AgentMcpAccessPointController {
private final AgentMcpAccessPointService agentMcpAccessPointService;
private final AgentService agentService;
/**
* 获取智能体的Mcp接入点地址
*
* @param audioId 智能体id
* @return 返回错误提醒或者Mcp接入点地址
*/
@Operation(summary = "获取智能体的Mcp接入点地址")
@GetMapping("/address/{agentId}")
@RequiresPermissions("sys:role:normal")
public Result<String> getAgentMcpAccessAddress(@PathVariable("agentId") String agentId) {
// 获取当前用户
UserDetail user = SecurityUser.getUser();
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
return new Result<String>().error("没有权限查看该智能体的MCP接入点地址");
}
String agentMcpAccessAddress = agentMcpAccessPointService.getAgentMcpAccessAddress(agentId);
if (agentMcpAccessAddress == null) {
return new Result<String>().ok("请联系管理员进入参数管理配置mcp接入点地址");
}
return new Result<String>().ok(agentMcpAccessAddress);
}
@Operation(summary = "获取智能体的Mcp工具列表")
@GetMapping("/tools/{agentId}")
@RequiresPermissions("sys:role:normal")
public Result<List<String>> getAgentMcpToolsList(@PathVariable("agentId") String agentId) {
// 获取当前用户
UserDetail user = SecurityUser.getUser();
// 检查权限
if (!agentService.checkAgentPermission(agentId, user.getId())) {
return new Result<List<String>>().error("没有权限查看该智能体的MCP工具列表");
}
List<String> agentMcpToolsList = agentMcpAccessPointService.getAgentMcpToolsList(agentId);
return new Result<List<String>>().ok(agentMcpToolsList);
}
}
@@ -0,0 +1,86 @@
package xiaozhi.modules.agent.controller;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.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.utils.Result;
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
import xiaozhi.modules.agent.dto.AgentVoicePrintUpdateDTO;
import xiaozhi.modules.agent.service.AgentVoicePrintService;
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "智能体声纹管理")
@AllArgsConstructor
@RestController
@RequestMapping("/agent/voice-print")
public class AgentVoicePrintController {
private final AgentVoicePrintService agentVoicePrintService;
private final SysParamsService sysParamsService;
@PostMapping
@Operation(summary = "创建智能体的声纹")
@RequiresPermissions("sys:role:normal")
public Result<Void> save(@RequestBody @Valid AgentVoicePrintSaveDTO dto) {
boolean b = agentVoicePrintService.insert(dto);
if (b) {
return new Result<>();
}
return new Result<Void>().error("智能体的声纹创建失败");
}
@PutMapping
@Operation(summary = "更新智能体的对应声纹")
@RequiresPermissions("sys:role:normal")
public Result<Void> update(@RequestBody @Valid AgentVoicePrintUpdateDTO dto) {
Long userId = SecurityUser.getUserId();
boolean b = agentVoicePrintService.update(userId, dto);
if (b) {
return new Result<>();
}
return new Result<Void>().error("智能体的对应声纹更新失败");
}
@DeleteMapping("/{id}")
@Operation(summary = "删除智能体对应声纹")
@RequiresPermissions("sys:role:normal")
public Result<Void> delete(@PathVariable String id) {
Long userId = SecurityUser.getUserId();
// 先删除关联的设备
boolean delete = agentVoicePrintService.delete(userId, id);
if (delete) {
return new Result<>();
}
return new Result<Void>().error("智能体的对应声纹删除失败");
}
@GetMapping("/list/{id}")
@Operation(summary = "获取用户指定智能体声纹列表")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)");
}
Long userId = SecurityUser.getUserId();
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
return new Result<List<AgentVoicePrintVO>>().ok(list);
}
}
@@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import xiaozhi.common.dao.BaseDao; import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
@Mapper @Mapper
public interface AgentDao extends BaseDao<AgentEntity> { public interface AgentDao extends BaseDao<AgentEntity> {
@@ -28,4 +29,11 @@ public interface AgentDao extends BaseDao<AgentEntity> {
" WHERE d.mac_address = #{macAddress} " + " WHERE d.mac_address = #{macAddress} " +
" ORDER BY d.id DESC LIMIT 1") " ORDER BY d.id DESC LIMIT 1")
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress); AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
/**
* 根据id查询agent信息,包括插件信息
*
* @param agentId 智能体ID
*/
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
} }
@@ -0,0 +1,22 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Mapper
* @createDate 2025-05-25 22:33:17
* @Entity xiaozhi.modules.agent.entity.AgentPluginMapping
*/
@Mapper
public interface AgentPluginMappingMapper extends BaseMapper<AgentPluginMapping> {
List<AgentPluginMapping> selectPluginsByAgentId(@Param("agentId") String agentId);
}
@@ -0,0 +1,20 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
/**
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
@Mapper
public interface AgentVoicePrintDao extends BaseMapper<AgentVoicePrintEntity> {
}
@@ -1,6 +1,8 @@
package xiaozhi.modules.agent.dto; package xiaozhi.modules.agent.dto;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -15,19 +17,19 @@ import lombok.Data;
public class AgentUpdateDTO implements Serializable { public class AgentUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "智能体编码", example = "AGT_1234567890", required = false) @Schema(description = "智能体编码", example = "AGT_1234567890", nullable = true)
private String agentCode; private String agentCode;
@Schema(description = "智能体名称", example = "客服助手", required = false) @Schema(description = "智能体名称", example = "客服助手", nullable = true)
private String agentName; private String agentName;
@Schema(description = "语音识别模型标识", example = "asr_model_02", required = false) @Schema(description = "语音识别模型标识", example = "asr_model_02", nullable = true)
private String asrModelId; private String asrModelId;
@Schema(description = "语音活动检测标识", example = "vad_model_02", required = false) @Schema(description = "语音活动检测标识", example = "vad_model_02", nullable = true)
private String vadModelId; private String vadModelId;
@Schema(description = "大语言模型标识", example = "llm_model_02", required = false) @Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
private String llmModelId; private String llmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false) @Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
@@ -36,31 +38,46 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false) @Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
private String ttsModelId; private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", required = false) @Schema(description = "音色标识", example = "voice_02", nullable = true)
private String ttsVoiceId; private String ttsVoiceId;
@Schema(description = "记忆模型标识", example = "mem_model_02", required = false) @Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true)
private String memModelId; private String memModelId;
@Schema(description = "意图模型标识", example = "intent_model_02", required = false) @Schema(description = "意图模型标识", example = "intent_model_02", nullable = true)
private String intentModelId; private String intentModelId;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false) @Schema(description = "插件函数信息", nullable = true)
private List<FunctionInfo> functions;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", nullable = true)
private String systemPrompt; private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n"
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false) + "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", nullable = true)
private String summaryMemory; private String summaryMemory;
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false) @Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", nullable = true)
private Integer chatHistoryConf; private Integer chatHistoryConf;
@Schema(description = "语言编码", example = "zh_CN", required = false) @Schema(description = "语言编码", example = "zh_CN", nullable = true)
private String langCode; private String langCode;
@Schema(description = "交互语种", example = "中文", required = false) @Schema(description = "交互语种", example = "中文", nullable = true)
private String language; private String language;
@Schema(description = "排序", example = "1", required = false) @Schema(description = "排序", example = "1", nullable = true)
private Integer sort; private Integer sort;
@Data
@Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable {
@Schema(description = "插件ID", example = "plugin_01")
private String pluginId;
@Schema(description = "函数参数信息", nullable = true)
private HashMap<String, Object> paramInfo;
private static final long serialVersionUID = 1L;
}
} }
@@ -0,0 +1,28 @@
package xiaozhi.modules.agent.dto;
import lombok.Data;
/**
* 保存智能体声纹的dto
*
* @author zjy
*/
@Data
public class AgentVoicePrintSaveDTO {
/**
* 关联的智能体id
*/
private String agentId;
/**
* 音频文件id
*/
private String audioId;
/**
* 声纹来源的人姓名
*/
private String sourceName;
/**
* 描述声纹来源的人
*/
private String introduce;
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.agent.dto;
import lombok.Data;
/**
* 修改智能体声纹的dto
*
* @author zjy
*/
@Data
public class AgentVoicePrintUpdateDTO {
/**
* 智能体声纹id
*/
private String id;
/**
* 音频文件id
*/
private String audioId;
/**
* 声纹来源的人姓名
*/
private String sourceName;
/**
* 描述声纹来源的人
*/
private String introduce;
}
@@ -0,0 +1,21 @@
package xiaozhi.modules.agent.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 声纹识别接口返回的对象
*/
@Data
public class IdentifyVoicePrintResponse {
/**
* 最匹配的声纹id
*/
@JsonProperty("speaker_id")
private String speakerId;
/**
* 声纹的分数
*/
private Double score;
}
@@ -0,0 +1,32 @@
package xiaozhi.modules.agent.dto;
import lombok.Data;
/**
* MCP JSON-RPC 请求 DTO
*/
@Data
public class McpJsonRpcRequest {
private String jsonrpc = "2.0";
private String method;
private Object params;
private Integer id;
public McpJsonRpcRequest() {
}
public McpJsonRpcRequest(String method) {
this.method = method;
}
public McpJsonRpcRequest(String method, Object params, Integer id) {
this.method = method;
this.params = params;
this.id = id;
}
public McpJsonRpcRequest(String method, Object params) {
this.method = method;
this.params = params;
}
}
@@ -0,0 +1,48 @@
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() {
}
}
}
@@ -0,0 +1,54 @@
package xiaozhi.modules.agent.entity;
import java.io.Serializable;
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;
/**
* Agent与插件的唯一映射表
*
* @TableName ai_agent_plugin_mapping
*/
@Data
@TableName(value = "ai_agent_plugin_mapping")
@Schema(description = "Agent与插件的唯一映射表")
public class AgentPluginMapping implements Serializable {
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "映射信息主键ID")
private Long id;
/**
* 智能体ID
*/
@Schema(description = "智能体ID")
private String agentId;
/**
* 插件ID
*/
@Schema(description = "插件ID")
private String pluginId;
/**
* 插件参数(Json)格式
*/
@Schema(description = "插件参数(Json)格式")
private String paramInfo;
// 冗余字段,用于方便在根据id查询插件时,对照查出插件的Provider_code,详见dao层xml文件
@TableField(exist = false)
@Schema(description = "插件provider_code, 对应表ai_model_provider")
private String providerCode;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,64 @@
package xiaozhi.modules.agent.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 lombok.Data;
/**
* 智能体声纹表
*
* @author zjy
*/
@TableName(value = "ai_agent_voice_print")
@Data
public class AgentVoicePrintEntity {
/**
* 主键id
*/
@TableId(type = IdType.ASSIGN_UUID)
private String id;
/**
* 关联的智能体id
*/
private String agentId;
/**
* 关联的音频id
*/
private String audioId;
/**
* 声纹来源的人姓名
*/
private String sourceName;
/**
* 描述声纹来源的人
*/
private String introduce;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long creator;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createDate;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updater;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateDate;
}
@@ -9,6 +9,7 @@ import xiaozhi.common.page.PageData;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
/** /**
* 智能体聊天记录表处理service * 智能体聊天记录表处理service
@@ -44,4 +45,30 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
* @param deleteText 是否删除文本 * @param deleteText 是否删除文本
*/ */
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText); void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
/**
* 根据智能体ID获取最近50条用户的聊天记录数据(带音频数据)
*
* @param agentId 智能体id
* @return 聊天记录列表(只有用户)
*/
List<AgentChatHistoryUserVO> getRecentlyFiftyByAgentId(String agentId);
/**
* 根据音频数据ID获取聊天内容
*
* @param audioId 音频id
* @return 聊天内容
*/
String getContentByAudioId(String audioId);
/**
* 查询此音频id是否属于此智能体
*
* @param audioId 音频id
* @param agentId 音频id
* @return T:属于 F:不属于
*/
boolean isAudioOwnedByAgent(String audioId,String agentId);
} }
@@ -0,0 +1,25 @@
package xiaozhi.modules.agent.service;
import java.util.List;
/**
* 智能体Mcp接入点处理service
*
* @author zjy
*/
public interface AgentMcpAccessPointService {
/**
* 获取智能体的mcp接入点地址
* @param id 智能体id
* @return mcp接入点地址
*/
String getAgentMcpAccessAddress(String id);
/**
* 获取智能体的mcp接入点已有的工具列表
* @param id 智能体id
* @return 工具列表
*/
List<String> getAgentMcpToolsList(String id);
}
@@ -0,0 +1,29 @@
package xiaozhi.modules.agent.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
* @createDate 2025-05-25 22:33:17
*/
public interface AgentPluginMappingService extends IService<AgentPluginMapping> {
/**
* 根据智能体id获取插件参数
*
* @param agentId
* @return
*/
List<AgentPluginMapping> agentPluginParamsByAgentId(String agentId);
/**
* 根据智能体id删除插件参数
*
* @param agentId
*/
void deleteByAgentId(String agentId);
}
@@ -5,8 +5,11 @@ import java.util.Map;
import xiaozhi.common.page.PageData; 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.AgentDTO; import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
/** /**
* 智能体表处理service * 智能体表处理service
@@ -30,7 +33,7 @@ public interface AgentService extends BaseService<AgentEntity> {
* @param id 智能体ID * @param id 智能体ID
* @return 智能体实体 * @return 智能体实体
*/ */
AgentEntity getAgentById(String id); AgentInfoVO getAgentById(String id);
/** /**
* 插入智能体 * 插入智能体
@@ -79,4 +82,20 @@ public interface AgentService extends BaseService<AgentEntity> {
* @return 是否有权限 * @return 是否有权限
*/ */
boolean checkAgentPermission(String agentId, Long userId); boolean checkAgentPermission(String agentId, Long userId);
/**
* 更新智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
*/
void updateAgentById(String agentId, AgentUpdateDTO dto);
/**
* 创建智能体
*
* @param dto 创建智能体所需的信息
* @return 创建的智能体ID
*/
String createAgent(AgentCreateDTO dto);
} }
@@ -0,0 +1,50 @@
package xiaozhi.modules.agent.service;
import java.util.List;
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
import xiaozhi.modules.agent.dto.AgentVoicePrintUpdateDTO;
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
/**
* 智能体声纹处理service
*
* @author zjy
*/
public interface AgentVoicePrintService {
/**
* 添加智能体新的声纹
*
* @param dto 保存智能体声纹的数据
* @return T:成功 F:失败
*/
boolean insert(AgentVoicePrintSaveDTO dto);
/**
* 删除智能体的指的声纹
*
* @param userId 当前登录的用户id
* @param voicePrintId 声纹id
* @return 是否成功 T:成功 F:失败
*/
boolean delete(Long userId, String voicePrintId);
/**
* 获取指定智能体的所有声纹数据
*
* @param userId 当前登录的用户id
* @param agentId 智能体id
* @return 声纹数据集合
*/
List<AgentVoicePrintVO> list(Long userId, String agentId);
/**
* 更新智能体的指的声纹数据
*
* @param userId 当前登录的用户id
* @param dto 修改的声纹的数据
* @return 是否成功 T:成功 F:失败
*/
boolean update(Long userId, AgentVoicePrintUpdateDTO dto);
}
@@ -19,6 +19,8 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
/** /**
* {@link AgentChatHistoryBizService} impl * {@link AgentChatHistoryBizService} impl
@@ -35,6 +37,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
private final AgentChatHistoryService agentChatHistoryService; private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService; private final AgentChatAudioService agentChatAudioService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final DeviceService deviceService;
/** /**
* 处理聊天记录上报,包括文件上传和相关信息记录 * 处理聊天记录上报,包括文件上传和相关信息记录
@@ -68,6 +71,15 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
// 更新设备最后对话时间 // 更新设备最后对话时间
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date()); redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
// 更新设备最后连接时间
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device != null) {
deviceService.updateDeviceConnectionInfo(agentId, device.getId(), null);
} else {
log.warn("聊天记录上报时,未找到mac地址为 {} 的设备", macAddress);
}
return Boolean.TRUE; return Boolean.TRUE;
} }
@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
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;
@@ -16,11 +17,14 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao; import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
/** /**
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl * 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
@@ -90,4 +94,74 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
} }
} }
@Override
public List<AgentChatHistoryUserVO> getRecentlyFiftyByAgentId(String agentId) {
// 构建查询条件(不添加按照创建时间排序,数据本来就是主键越大创建时间越大
// 不添加这样可以减少排序全部数据在分页的全盘扫描消耗)
LambdaQueryWrapper<AgentChatHistoryEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.select(AgentChatHistoryEntity::getContent, AgentChatHistoryEntity::getAudioId)
.eq(AgentChatHistoryEntity::getAgentId, agentId)
.eq(AgentChatHistoryEntity::getChatType, AgentChatHistoryType.USER.getValue())
.isNotNull(AgentChatHistoryEntity::getAudioId);
// 构建分页查询,查询前50页数据
Page<AgentChatHistoryEntity> pageParam = new Page<>(0, 50);
IPage<AgentChatHistoryEntity> result = this.baseMapper.selectPage(pageParam, wrapper);
return result.getRecords().stream().map(item -> {
AgentChatHistoryUserVO vo = ConvertUtils.sourceToTarget(item, AgentChatHistoryUserVO.class);
// 处理 content 字段,确保只返回聊天内容
if (vo != null && vo.getContent() != null) {
vo.setContent(extractContentFromString(vo.getContent()));
}
return vo;
}).toList();
}
/**
* 从 content 字段中提取聊天内容
* 如果 content 是 JSON 格式(如 {"speaker": "未知说话人", "content": "现在几点了。"}),则提取 content
* 字段
* 如果 content 是普通字符串,则直接返回
*
* @param content 原始内容
* @return 提取的聊天内容
*/
private String extractContentFromString(String content) {
if (content == null || content.trim().isEmpty()) {
return content;
}
// 尝试解析为 JSON
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class);
if (jsonMap != null && jsonMap.containsKey("content")) {
Object contentObj = jsonMap.get("content");
return contentObj != null ? contentObj.toString() : content;
}
} catch (Exception e) {
// 如果不是有效的 JSON,直接返回原内容
}
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
return content;
}
@Override
public String getContentByAudioId(String audioId) {
AgentChatHistoryEntity agentChatHistoryEntity = baseMapper
.selectOne(new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getContent)
.eq(AgentChatHistoryEntity::getAudioId, audioId));
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
}
@Override
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
Long row = baseMapper.selectCount(new LambdaQueryWrapper<AgentChatHistoryEntity>()
.eq(AgentChatHistoryEntity::getAudioId, audioId)
.eq(AgentChatHistoryEntity::getAgentId, agentId));
return row == 1;
}
} }
@@ -0,0 +1,247 @@
package xiaozhi.modules.agent.service.impl;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.utils.AESUtils;
import xiaozhi.common.utils.HashEncryptionUtil;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dto.McpJsonRpcRequest;
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.utils.WebSocketClientManager;
@AllArgsConstructor
@Service
@Slf4j
public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointService {
private SysParamsService sysParamsService;
@Override
public String getAgentMcpAccessAddress(String id) {
// 获取到mcp的地址
String url = sysParamsService.getValue(Constant.SERVER_MCP_ENDPOINT, true);
if (StringUtils.isBlank(url) || "null".equals(url)) {
return null;
}
URI uri = getURI(url);
// 获取智能体mcp的url前缀
String agentMcpUrl = getAgentMcpUrl(uri);
// 获取密钥
String key = getSecretKey(uri);
// 获取加密的token
String encryptToken = encryptToken(id, key);
// 对token进行URL编码
String encodedToken = URLEncoder.encode(encryptToken, StandardCharsets.UTF_8);
// 返回智能体Mcp路径的格式
agentMcpUrl = "%s/mcp/?token=%s".formatted(agentMcpUrl, encodedToken);
return agentMcpUrl;
}
@Override
public List<String> getAgentMcpToolsList(String id) {
String wsUrl = getAgentMcpAccessAddress(id);
if (StringUtils.isBlank(wsUrl)) {
return List.of();
}
// 将 /mcp 替换为 /call
wsUrl = wsUrl.replace("/mcp/", "/call/");
try {
// 创建 WebSocket 连接,增加超时时间到15秒
try (WebSocketClientManager client = WebSocketClientManager.build(
new WebSocketClientManager.Builder()
.uri(wsUrl)
.bufferSize(1024 * 1024)
.connectTimeout(8, TimeUnit.SECONDS)
.maxSessionDuration(10, TimeUnit.SECONDS))) {
// 步骤1: 发送初始化消息并等待响应
log.info("发送MCP初始化消息,智能体ID: {}", id);
McpJsonRpcRequest initializeRequest = new McpJsonRpcRequest("initialize",
Map.of(
"protocolVersion", "2024-11-05",
"capabilities", Map.of(
"roots", Map.of("listChanged", false),
"sampling", Map.of()),
"clientInfo", Map.of(
"name", "xz-mcp-broker",
"version", "0.0.1")),
1);
client.sendJson(initializeRequest);
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
List<String> initResponses = client.listenerWithoutClose(response -> {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
// 检查是否有result字段,表示初始化成功
return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
}
return false;
} catch (Exception e) {
log.warn("解析初始化响应失败: {}", response, e);
return false;
}
});
// 验证初始化响应
boolean initSucceeded = false;
for (String response : initResponses) {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
if (jsonMap.containsKey("result")) {
log.info("MCP初始化成功,智能体ID: {}", id);
initSucceeded = true;
break;
} else if (jsonMap.containsKey("error")) {
log.error("MCP初始化失败,智能体ID: {}, 错误: {}", id, jsonMap.get("error"));
return List.of();
}
}
} catch (Exception e) {
log.warn("处理初始化响应失败: {}", response, e);
}
}
if (!initSucceeded) {
log.error("未收到有效的MCP初始化响应,智能体ID: {}", id);
return List.of();
}
// 步骤2: 发送初始化完成通知 - 只有在收到initialize响应后才发送
log.info("发送MCP初始化完成通知,智能体ID: {}", id);
String notificationJson = "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}";
client.sendText(notificationJson);
// 步骤3: 发送工具列表请求 - 立即发送,无需额外延迟
log.info("发送MCP工具列表请求,智能体ID: {}", id);
McpJsonRpcRequest toolsRequest = new McpJsonRpcRequest("tools/list", null, 2);
client.sendJson(toolsRequest);
// 等待工具列表响应 (id=2)
List<String> toolsResponses = client.listener(response -> {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"));
} catch (Exception e) {
log.warn("解析工具列表响应失败: {}", response, e);
return false;
}
});
// 处理工具列表响应
for (String response : toolsResponses) {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) {
// 检查是否有result字段
Object resultObj = jsonMap.get("result");
if (resultObj instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) resultObj;
Object toolsObj = resultMap.get("tools");
if (toolsObj instanceof List) {
List<Map<String, Object>> toolsList = (List<Map<String, Object>>) toolsObj;
// 提取工具名称列表
List<String> result = toolsList.stream()
.map(tool -> (String) tool.get("name"))
.filter(name -> name != null)
.collect(Collectors.toList());
log.info("成功获取MCP工具列表,智能体ID: {}, 工具数量: {}", id, result.size());
return result;
}
} else if (jsonMap.containsKey("error")) {
log.error("获取工具列表失败,智能体ID: {}, 错误: {}", id, jsonMap.get("error"));
return List.of();
}
}
} catch (Exception e) {
log.warn("处理工具列表响应失败: {}", response, e);
}
}
log.warn("未找到有效的工具列表响应,智能体ID: {}", id);
return List.of();
}
} catch (Exception e) {
log.error("获取智能体 MCP 工具列表失败,智能体ID: {},错误原因:{}", id, e.getMessage());
return List.of();
}
}
/**
* 获取URI对象
*
* @param url 路径
* @return URI对象
*/
private static URI getURI(String url) {
try {
return new URI(url);
} catch (URISyntaxException e) {
log.error("路径格式不正确路径:{}\n错误信息:{}", url, e.getMessage());
throw new RuntimeException("mcp的地址存在错误,请进入参数管理修改mcp接入点地址");
}
}
/**
* 获取密钥
*
* @param uri mcp地址
* @return 密钥
*/
private static String getSecretKey(URI uri) {
// 获取参数
String query = uri.getQuery();
// 获取aes加密密钥
String str = "key=";
return query.substring(query.indexOf(str) + str.length());
}
/**
* 获取智能体mcp接入点url
*
* @param uri mcp地址
* @return 智能体mcp接入点url
*/
private String getAgentMcpUrl(URI uri) {
// 获取协议
String wsScheme = (uri.getScheme().equals("https")) ? "wss" : "ws";
// 获取主机,端口,路径
String path = uri.getSchemeSpecificPart();
// 获取到最后一个/前的path
path = path.substring(0, path.lastIndexOf("/"));
return wsScheme + ":" + path;
}
/**
* 获取对智能体id加密的token
*
* @param agentId 智能体id
* @param key 加密密钥
* @return 加密后token
*/
private static String encryptToken(String agentId, String key) {
// 使用md5对智能体id进行加密
String md5 = HashEncryptionUtil.Md5hexDigest(agentId);
// aes需要加密文本
String json = "{\"agentId\": \"%s\"}".formatted(md5);
// 加密后成token值
return AESUtils.encrypt(key, json);
}
}
@@ -0,0 +1,37 @@
package xiaozhi.modules.agent.service.impl;
import java.util.List;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import xiaozhi.modules.agent.dao.AgentPluginMappingMapper;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service实现
* @createDate 2025-05-25 22:33:17
*/
@Service
@RequiredArgsConstructor
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
implements AgentPluginMappingService {
private final AgentPluginMappingMapper agentPluginMappingMapper;
@Override
public List<AgentPluginMapping> agentPluginParamsByAgentId(String agentId) {
return agentPluginMappingMapper.selectPluginsByAgentId(agentId);
}
@Override
public void deleteByAgentId(String agentId) {
UpdateWrapper<AgentPluginMapping> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("agent_id", agentId);
agentPluginMappingMapper.delete(updateWrapper);
}
}
@@ -1,12 +1,17 @@
package xiaozhi.modules.agent.service.impl; package xiaozhi.modules.agent.service.impl;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
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.conditions.update.UpdateWrapper;
@@ -14,16 +19,30 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
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.vo.AgentInfoVO;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.timbre.service.TimbreService; import xiaozhi.modules.timbre.service.TimbreService;
@@ -36,6 +55,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final ModelConfigService modelConfigService; private final ModelConfigService modelConfigService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final DeviceService deviceService; private final DeviceService deviceService;
private final AgentPluginMappingService agentPluginMappingService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentTemplateService agentTemplateService;
private final ModelProviderService modelProviderService;
@Override @Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) { public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -46,15 +69,20 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
} }
@Override @Override
public AgentEntity getAgentById(String id) { public AgentInfoVO getAgentById(String id) {
AgentEntity agent = agentDao.selectById(id); AgentInfoVO agent = agentDao.selectAgentInfoById(id);
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); if (agent == null) {
} else if (agent != null && agent.getMemModelId() != null throw new RenException("智能体不存在");
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
&& agent.getChatHistoryConf() == null) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
} }
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
if (agent.getChatHistoryConf() == null) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
}
}
// 无需额外查询插件列表,已通过SQL查询出来
return agent; return agent;
} }
@@ -167,4 +195,198 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 检查是否是智能体的所有者 // 检查是否是智能体的所有者
return userId.equals(agent.getUserId()); return userId.equals(agent.getUserId());
} }
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
// 先查询现有实体
AgentEntity existingEntity = this.getAgentById(agentId);
if (existingEntity == null) {
throw new RuntimeException("智能体不存在");
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
if (dto.getAsrModelId() != null) {
existingEntity.setAsrModelId(dto.getAsrModelId());
}
if (dto.getVadModelId() != null) {
existingEntity.setVadModelId(dto.getVadModelId());
}
if (dto.getLlmModelId() != null) {
existingEntity.setLlmModelId(dto.getLlmModelId());
}
if (dto.getVllmModelId() != null) {
existingEntity.setVllmModelId(dto.getVllmModelId());
}
if (dto.getTtsModelId() != null) {
existingEntity.setTtsModelId(dto.getTtsModelId());
}
if (dto.getTtsVoiceId() != null) {
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
}
if (dto.getMemModelId() != null) {
existingEntity.setMemModelId(dto.getMemModelId());
}
if (dto.getIntentModelId() != null) {
existingEntity.setIntentModelId(dto.getIntentModelId());
}
if (dto.getSystemPrompt() != null) {
existingEntity.setSystemPrompt(dto.getSystemPrompt());
}
if (dto.getSummaryMemory() != null) {
existingEntity.setSummaryMemory(dto.getSummaryMemory());
}
if (dto.getChatHistoryConf() != null) {
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
}
if (dto.getLangCode() != null) {
existingEntity.setLangCode(dto.getLangCode());
}
if (dto.getLanguage() != null) {
existingEntity.setLanguage(dto.getLanguage());
}
if (dto.getSort() != null) {
existingEntity.setSort(dto.getSort());
}
// 更新函数插件信息
List<AgentUpdateDTO.FunctionInfo> functions = dto.getFunctions();
if (functions != null) {
// 1. 收集本次提交的 pluginId
List<String> newPluginIds = functions.stream()
.map(AgentUpdateDTO.FunctionInfo::getPluginId)
.toList();
// 2. 查询当前agent现有的所有映射
List<AgentPluginMapping> existing = agentPluginMappingService.list(
new QueryWrapper<AgentPluginMapping>()
.eq("agent_id", agentId));
Map<String, AgentPluginMapping> existMap = existing.stream()
.collect(Collectors.toMap(AgentPluginMapping::getPluginId, Function.identity()));
// 3. 构造所有要 保存或更新 的实体
List<AgentPluginMapping> allToPersist = functions.stream().map(info -> {
AgentPluginMapping m = new AgentPluginMapping();
m.setAgentId(agentId);
m.setPluginId(info.getPluginId());
m.setParamInfo(JsonUtils.toJsonString(info.getParamInfo()));
AgentPluginMapping old = existMap.get(info.getPluginId());
if (old != null) {
// 已存在,设置id表示更新
m.setId(old.getId());
}
return m;
}).toList();
// 4. 拆分:已有ID的走更新,无ID的走插入
List<AgentPluginMapping> toUpdate = allToPersist.stream()
.filter(m -> m.getId() != null)
.toList();
List<AgentPluginMapping> toInsert = allToPersist.stream()
.filter(m -> m.getId() == null)
.toList();
if (!toUpdate.isEmpty()) {
agentPluginMappingService.updateBatchById(toUpdate);
}
if (!toInsert.isEmpty()) {
agentPluginMappingService.saveBatch(toInsert);
}
// 5. 删除本次不在提交列表里的插件映射
List<Long> toDelete = existing.stream()
.filter(old -> !newPluginIds.contains(old.getPluginId()))
.map(AgentPluginMapping::getId)
.toList();
if (!toDelete.isEmpty()) {
agentPluginMappingService.removeBatchByIds(toDelete);
}
}
// 设置更新者信息
UserDetail user = SecurityUser.getUser();
existingEntity.setUpdater(user.getId());
existingEntity.setUpdatedAt(new Date());
// 更新记忆策略
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
// 删除所有记录
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
existingEntity.setSummaryMemory("");
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
// 删除音频数据
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
}
this.updateById(existingEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createAgent(AgentCreateDTO dto) {
// 转换为实体
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
// 获取默认模板
AgentTemplateEntity template = agentTemplateService.getDefaultTemplate();
if (template != null) {
// 设置模板中的默认值
entity.setAsrModelId(template.getAsrModelId());
entity.setVadModelId(template.getVadModelId());
entity.setLlmModelId(template.getLlmModelId());
entity.setVllmModelId(template.getVllmModelId());
entity.setTtsModelId(template.getTtsModelId());
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
entity.setChatHistoryConf(template.getChatHistoryConf());
entity.setLangCode(template.getLangCode());
entity.setLanguage(template.getLanguage());
}
// 设置用户ID和创建者信息
UserDetail user = SecurityUser.getUser();
entity.setUserId(user.getId());
entity.setCreator(user.getId());
entity.setCreatedAt(new Date());
// 保存智能体
insert(entity);
// 设置默认插件
List<AgentPluginMapping> toInsert = new ArrayList<>();
// 播放音乐、查天气、查新闻
String[] pluginIds = new String[] { "SYSTEM_PLUGIN_MUSIC", "SYSTEM_PLUGIN_WEATHER",
"SYSTEM_PLUGIN_NEWS_NEWSNOW" };
for (String pluginId : pluginIds) {
ModelProviderDTO provider = modelProviderService.getById(pluginId);
if (provider == null) {
continue;
}
AgentPluginMapping mapping = new AgentPluginMapping();
mapping.setPluginId(pluginId);
Map<String, Object> paramInfo = new HashMap<>();
List<Map<String, Object>> fields = JsonUtils.parseObject(provider.getFields(), List.class);
if (fields != null) {
for (Map<String, Object> field : fields) {
paramInfo.put((String) field.get("key"), field.get("default"));
}
}
mapping.setParamInfo(JsonUtils.toJsonString(paramInfo));
mapping.setAgentId(entity.getId());
toInsert.add(mapping);
}
// 保存默认插件
agentPluginMappingService.saveBatch(toInsert);
return entity.getId();
}
} }
@@ -56,6 +56,9 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
wrapper.set("tts_model_id", modelId); wrapper.set("tts_model_id", modelId);
wrapper.set("tts_voice_id", null); wrapper.set("tts_voice_id", null);
break; break;
case "VLLM":
wrapper.set("vllm_model_id", modelId);
break;
case "MEMORY": case "MEMORY":
wrapper.set("mem_model_id", modelId); wrapper.set("mem_model_id", modelId);
break; break;
@@ -0,0 +1,410 @@
package xiaozhi.modules.agent.service.impl;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
import xiaozhi.modules.agent.dto.AgentVoicePrintUpdateDTO;
import xiaozhi.modules.agent.dto.IdentifyVoicePrintResponse;
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentVoicePrintService;
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* @author zjy
*/
@Service
@Slf4j
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity>
implements AgentVoicePrintService {
private final AgentChatAudioService agentChatAudioService;
private final RestTemplate restTemplate;
private final SysParamsService sysParamsService;
private final AgentChatHistoryService agentChatHistoryService;
// Springboot提供的编程事务类
private final TransactionTemplate transactionTemplate;
// 识别度
private final Double RECOGNITION = 0.5;
private final Executor taskExecutor;
public AgentVoicePrintServiceImpl(AgentChatAudioService agentChatAudioService, RestTemplate restTemplate,
SysParamsService sysParamsService, AgentChatHistoryService agentChatHistoryService,
TransactionTemplate transactionTemplate, @Qualifier("taskExecutor") Executor taskExecutor) {
this.agentChatAudioService = agentChatAudioService;
this.restTemplate = restTemplate;
this.sysParamsService = sysParamsService;
this.agentChatHistoryService = agentChatHistoryService;
this.transactionTemplate = transactionTemplate;
this.taskExecutor = taskExecutor;
}
@Override
public boolean insert(AgentVoicePrintSaveDTO dto) {
// 获取音频数据
ByteArrayResource resource = getVoicePrintAudioWAV(dto.getAgentId(), dto.getAudioId());
// 识别一下此声音是否注册过
IdentifyVoicePrintResponse response = identifyVoicePrint(dto.getAgentId(), resource);
if (response != null && response.getScore() > RECOGNITION) {
// 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
}
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
// 开启事务
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
try {
// 保存声纹信息
int row = baseMapper.insert(entity);
// 插入一条数据,影响的数据不等于1说明出现了,保存问题回滚
if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚
return false;
}
// 发送注册声纹请求
registerVoicePrint(entity.getId(), resource);
return true;
} catch (RenException e) {
status.setRollbackOnly(); // 标记事务回滚
throw e;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("保存声纹错误原因:{}", e.getMessage());
throw new RenException("保存声纹错误,请联系管理员");
}
}));
}
@Override
public boolean delete(Long userId, String voicePrintId) {
// 开启事务
boolean b = Boolean.TRUE.equals(transactionTemplate.execute(status -> {
try {
// 删除声纹,按照指定当前登录用户和智能体
int row = baseMapper.delete(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId, voicePrintId)
.eq(AgentVoicePrintEntity::getCreator, userId));
if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚
return false;
}
return true;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("删除声纹存在错误原因:{}", e.getMessage());
throw new RenException("删除声纹出现了错误");
}
}));
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据
if(b){
taskExecutor.execute(()-> {
try {
cancelVoicePrint(voicePrintId);
}catch (RuntimeException e) {
log.error("删除声纹存在运行时错误原因:{},id:{}", e.getMessage(),voicePrintId);
}
});
}
return b;
}
@Override
public List<AgentVoicePrintVO> list(Long userId, String agentId) {
// 按照指定当前登录用户和智能体查找数据
List<AgentVoicePrintEntity> list = baseMapper.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getAgentId, agentId)
.eq(AgentVoicePrintEntity::getCreator, userId));
return list.stream().map(entity -> {
// 遍历转换成AgentVoicePrintVO类型
return ConvertUtils.sourceToTarget(entity, AgentVoicePrintVO.class);
}).toList();
}
@Override
public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
AgentVoicePrintEntity agentVoicePrintEntity = baseMapper
.selectOne(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId, dto.getId())
.eq(AgentVoicePrintEntity::getCreator, userId));
if (agentVoicePrintEntity == null) {
return false;
}
// 获取音频Id
String audioId = dto.getAudioId();
// 获取智能体id
String agentId = agentVoicePrintEntity.getAgentId();
ByteArrayResource resource;
// audioId不等于空,且audioId和之前的保存的音频id不一样,则需要重新获取音频数据生成声纹
if (!StringUtils.isEmpty(audioId) && !audioId.equals(agentVoicePrintEntity.getAudioId())) {
resource = getVoicePrintAudioWAV(agentId, audioId);
// 识别一下此声音是否注册过
IdentifyVoicePrintResponse response = identifyVoicePrint(agentId, resource);
// 返回分数高于RECOGNITION说明这个声纹已经有了
if (response != null && response.getScore() > RECOGNITION) {
// 判断返回的id如果不是要修改的声纹id,说明这个声纹id,现在要注册的声音已经存在且不是原来的声纹,不允许修改
if (!response.getSpeakerId().equals(dto.getId())) {
// 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + "");
}
}
} else {
resource = null;
}
// 开启事务
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
try {
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
int row = baseMapper.updateById(entity);
if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚
return false;
}
if (resource != null) {
String id = entity.getId();
// 先注销之前这个声纹id上的声纹向量
cancelVoicePrint(id);
// 发送注册声纹请求
registerVoicePrint(id, resource);
}
return true;
} catch (RenException e) {
status.setRollbackOnly(); // 标记事务回滚
throw e;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("修改声纹错误原因:{}", e.getMessage());
throw new RenException("修改声纹错误,请联系管理员");
}
}));
}
/**
* 获取生纹接口URI对象
*
* @return URI对象
*/
private URI getVoicePrintURI() {
// 获取声纹接口地址
String voicePrint = sysParamsService.getValue(Constant.SERVER_VOICE_PRINT, true);
try {
return new URI(voicePrint);
} catch (URISyntaxException e) {
log.error("路径格式不正确路径:{}\n错误信息:{}", voicePrint, e.getMessage());
throw new RuntimeException("声纹接口的地址存在错误,请进入参数管理修改声纹接口地址");
}
}
/**
* 获取声纹地址基础路径
*
* @param uri 声纹地址uri
* @return 基础路径
*/
private String getBaseUrl(URI uri) {
String protocol = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
if (port == -1) {
return "%s://%s".formatted(protocol, host);
} else {
return "%s://%s:%s".formatted(protocol, host, port);
}
}
/**
* 获取验证Authorization
*
* @param uri 声纹地址uri
* @return Authorization值
*/
private String getAuthorization(URI uri) {
// 获取参数
String query = uri.getQuery();
// 获取aes加密密钥
String str = "key=";
return "Bearer " + query.substring(query.indexOf(str) + str.length());
}
/**
* 获取声纹音频资源数据
*
* @param audioId 音频Id
* @return 声纹音频资源数据
*/
private ByteArrayResource getVoicePrintAudioWAV(String agentId, String audioId) {
// 判断这个音频是否属于当前智能体
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
if (!b) {
throw new RenException("音频数据不属于这个智能体");
}
// 获取到音频数据
byte[] audio = agentChatAudioService.getAudio(audioId);
// 如果音频数据为空的直接报错不进行下去
if (audio == null || audio.length == 0) {
throw new RenException("音频数据是空的请检查上传数据");
}
// 将字节数组包装为资源,返回
return new ByteArrayResource(audio) {
@Override
public String getFilename() {
return "VoicePrint.WAV"; // 设置文件名
}
};
}
/**
* 发送注册声纹http请求
*
* @param id 声纹id
* @param resource 声纹音频资源
*/
private void registerVoicePrint(String id, ByteArrayResource resource) {
// 处理声纹接口地址,获取前缀
URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/register";
// 创建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("speaker_id", id);
body.add("file", resource);
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorization(uri));
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 创建请求体
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
// 发送 POST 请求
ResponseEntity<String> response = restTemplate.postForEntity(requestUrl, requestEntity, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注册失败,请求路径:{}", requestUrl);
throw new RenException("声纹保存失败,请求不成功");
}
// 检查响应内容
String responseBody = response.getBody();
if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹保存失败,请求处理失败");
}
}
/**
* 发送注销声纹的请求
*
* @param voicePrintId 声纹id
*/
private void cancelVoicePrint(String voicePrintId) {
URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/" + voicePrintId;
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorization(uri));
// 创建请求体
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(headers);
// 发送 POST 请求
ResponseEntity<String> response = restTemplate.exchange(requestUrl, HttpMethod.DELETE, requestEntity,
String.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注销失败,请求路径:{}", requestUrl);
throw new RenException("声纹注销失败,请求不成功");
}
// 检查响应内容
String responseBody = response.getBody();
if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹注销失败,请求处理失败");
}
}
/**
* 发送识别声纹http请求
*
* @param agentId 智能体id
* @param resource 声纹音频资源
* @return 返回识别数据
*/
private IdentifyVoicePrintResponse identifyVoicePrint(String agentId, ByteArrayResource resource) {
// 获取该智能体所有注册的声纹
List<AgentVoicePrintEntity> agentVoicePrintList = baseMapper
.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.select(AgentVoicePrintEntity::getId)
.eq(AgentVoicePrintEntity::getAgentId, agentId));
// 声纹数量为0,说明还没注册过声纹不需要发生识别请求
if (agentVoicePrintList.isEmpty()) {
return null;
}
// 处理声纹接口地址,获取前缀
URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/identify";
// 创建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 创建speaker_id参数
String speakerIds = agentVoicePrintList.stream()
.map(AgentVoicePrintEntity::getId)
.collect(Collectors.joining(","));
body.add("speaker_ids", speakerIds);
body.add("file", resource);
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorization(uri));
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 创建请求体
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
// 发送 POST 请求
ResponseEntity<String> response = restTemplate.postForEntity(requestUrl, requestEntity, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹识别请求失败,请求路径:{}", requestUrl);
throw new RenException("声纹识别失败,请求不成功");
}
// 检查响应内容
String responseBody = response.getBody();
if (responseBody != null) {
return JsonUtils.parseObject(responseBody, IdentifyVoicePrintResponse.class);
}
return null;
}
}
@@ -0,0 +1,16 @@
package xiaozhi.modules.agent.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 智能体用户个人聊天数据的VO
*/
@Data
public class AgentChatHistoryUserVO {
@Schema(description = "聊天内容")
private String content;
@Schema(description = "音频ID")
private String audioId;
}
@@ -0,0 +1,24 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import java.util.List;
/**
* Agent信息返回体VO
* 这里直接extend了Agent实体类AgentEntity,后续需要规范返回字段可以copy字段出来
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class AgentInfoVO extends AgentEntity
{
@Schema(description = "插件列表Id")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AgentPluginMapping> functions;
}
@@ -0,0 +1,33 @@
package xiaozhi.modules.agent.vo;
import lombok.Data;
import java.util.Date;
/**
* 展示智能体声纹列表VO
*/
@Data
public class AgentVoicePrintVO {
/**
* 主键id
*/
private String id;
/**
* 音频文件id
*/
private String audioId;
/**
* 声纹来源的人姓名
*/
private String sourceName;
/**
* 描述声纹来源的人
*/
private String introduce;
/**
* 创建时间
*/
private Date createDate;
}
@@ -27,7 +27,7 @@ public class ConfigController {
private final ConfigService configService; private final ConfigService configService;
@PostMapping("server-base") @PostMapping("server-base")
@Operation(summary = "获取配置") @Operation(summary = "服务端获取配置接口")
public Result<Object> getConfig() { public Result<Object> getConfig() {
Object config = configService.getConfig(true); Object config = configService.getConfig(true);
return new Result<Object>().ok(config); return new Result<Object>().ok(config);
@@ -4,21 +4,31 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
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.agent.vo.AgentVoicePrintVO;
import xiaozhi.modules.config.service.ConfigService; import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
@@ -39,6 +49,9 @@ public class ConfigServiceImpl implements ConfigService {
private final AgentTemplateService agentTemplateService; private final AgentTemplateService agentTemplateService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final TimbreService timbreService; private final TimbreService timbreService;
private final AgentPluginMappingService agentPluginMappingService;
private final AgentMcpAccessPointService agentMcpAccessPointService;
private final AgentVoicePrintDao agentVoicePrintDao;
@Override @Override
public Object getConfig(Boolean isCache) { public Object getConfig(Boolean isCache) {
@@ -66,6 +79,8 @@ public class ConfigServiceImpl implements ConfigService {
null, null,
null, null,
null, null,
null,
null,
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
null, null,
@@ -102,9 +117,13 @@ public class ConfigServiceImpl implements ConfigService {
} }
// 获取音色信息 // 获取音色信息
String voice = null; String voice = null;
String referenceAudio = null;
String referenceText = null;
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId()); TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
if (timbre != null) { if (timbre != null) {
voice = timbre.getTtsVoice(); voice = timbre.getTtsVoice();
referenceAudio = timbre.getReferenceAudio();
referenceText = timbre.getReferenceText();
} }
// 构建返回数据 // 构建返回数据
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
@@ -132,12 +151,35 @@ public class ConfigServiceImpl implements ConfigService {
agent.setAsrModelId(null); agent.setAsrModelId(null);
} }
// 添加函数调用参数信息
if (!Objects.equals(agent.getIntentModelId(), "Intent_nointent")) {
String agentId = agent.getId();
List<AgentPluginMapping> pluginMappings = agentPluginMappingService.agentPluginParamsByAgentId(agentId);
if (pluginMappings != null && !pluginMappings.isEmpty()) {
Map<String, Object> pluginParams = new HashMap<>();
for (AgentPluginMapping pluginMapping : pluginMappings) {
pluginParams.put(pluginMapping.getProviderCode(), pluginMapping.getParamInfo());
}
result.put("plugins", pluginParams);
}
}
// 获取mcp接入点地址
String mcpEndpoint = agentMcpAccessPointService.getAgentMcpAccessAddress(agent.getId());
if (StringUtils.isNotBlank(mcpEndpoint) && mcpEndpoint.startsWith("ws")) {
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
result.put("mcp_endpoint", mcpEndpoint);
}
// 获取声纹信息
buildVoiceprintConfig(agent.getId(), result);
// 构建模块配置 // 构建模块配置
buildModuleConfig( buildModuleConfig(
agent.getAgentName(), agent.getAgentName(),
agent.getSystemPrompt(), agent.getSystemPrompt(),
agent.getSummaryMemory(), agent.getSummaryMemory(),
voice, voice,
referenceAudio,
referenceText,
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
agent.getLlmModelId(), agent.getLlmModelId(),
@@ -154,7 +196,7 @@ public class ConfigServiceImpl implements ConfigService {
/** /**
* 构建配置信息 * 构建配置信息
* *
* @param paramsList 系统参数列表 * @param config 系统参数列表
* @return 配置信息 * @return 配置信息
*/ */
private Object buildConfig(Map<String, Object> config) { private Object buildConfig(Map<String, Object> config) {
@@ -222,24 +264,84 @@ public class ConfigServiceImpl implements ConfigService {
return config; return config;
} }
/**
* 构建声纹配置信息
*
* @param agentId 智能体ID
* @param result 结果Map
*/
private void buildVoiceprintConfig(String agentId, Map<String, Object> result) {
try {
// 获取声纹接口地址
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
return;
}
// 获取智能体关联的声纹信息(不需要用户权限验证)
List<AgentVoicePrintVO> voiceprints = getVoiceprintsByAgentId(agentId);
if (voiceprints == null || voiceprints.isEmpty()) {
return;
}
// 构建speakers列表
List<String> speakers = new ArrayList<>();
for (AgentVoicePrintVO voiceprint : voiceprints) {
String speakerStr = String.format("%s,%s,%s",
voiceprint.getId(),
voiceprint.getSourceName(),
voiceprint.getIntroduce() != null ? voiceprint.getIntroduce() : "");
speakers.add(speakerStr);
}
// 构建声纹配置
Map<String, Object> voiceprintConfig = new HashMap<>();
voiceprintConfig.put("url", voiceprintUrl);
voiceprintConfig.put("speakers", speakers);
result.put("voiceprint", voiceprintConfig);
} catch (Exception e) {
// 声纹配置获取失败时不影响其他功能
System.err.println("获取声纹配置失败: " + e.getMessage());
}
}
/**
* 获取智能体关联的声纹信息
*
* @param agentId 智能体ID
* @return 声纹信息列表
*/
private List<AgentVoicePrintVO> getVoiceprintsByAgentId(String agentId) {
LambdaQueryWrapper<AgentVoicePrintEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AgentVoicePrintEntity::getAgentId, agentId);
queryWrapper.orderByAsc(AgentVoicePrintEntity::getCreateDate);
List<AgentVoicePrintEntity> entities = agentVoicePrintDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(entities, AgentVoicePrintVO.class);
}
/** /**
* 构建模块配置 * 构建模块配置
* *
* @param prompt 提示词 * @param prompt 提示词
* @param voice 音色 * @param voice 音色
* @param vadModelId VAD模型ID * @param referenceAudio 参考音频路径
* @param asrModelId ASR模型ID * @param referenceText 参考文本
* @param llmModelId LLM模型ID * @param vadModelId VAD模型ID
* @param ttsModelId TTS模型ID * @param asrModelId ASR模型ID
* @param memModelId 记忆模型ID * @param llmModelId LLM模型ID
* @param intentModelId 意图模型ID * @param ttsModelId TTS模型ID
* @param result 结果Map * @param memModelId 记忆模型ID
* @param intentModelId 意图模型ID
* @param result 结果Map
*/ */
private void buildModuleConfig( private void buildModuleConfig(
String assistantName, String assistantName,
String prompt, String prompt,
String summaryMemory, String summaryMemory,
String voice, String voice,
String referenceAudio,
String referenceText,
String vadModelId, String vadModelId,
String asrModelId, String asrModelId,
String llmModelId, String llmModelId,
@@ -261,12 +363,20 @@ public class ConfigServiceImpl implements ConfigService {
continue; continue;
} }
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache); ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
if (model == null) {
continue;
}
Map<String, Object> typeConfig = new HashMap<>(); Map<String, Object> typeConfig = new HashMap<>();
if (model.getConfigJson() != null) { if (model.getConfigJson() != null) {
typeConfig.put(model.getId(), model.getConfigJson()); typeConfig.put(model.getId(), model.getConfigJson());
// 如果是TTS类型,添加private_voice属性 // 如果是TTS类型,添加private_voice属性
if ("TTS".equals(modelTypes[i]) && voice != null) { if ("TTS".equals(modelTypes[i])) {
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice); if (voice != null)
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
if (referenceAudio != null)
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
if (referenceText != null)
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
} }
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型 // 如果是Intent类型,且type=intent_llm,则给他添加附加模型
if ("Intent".equals(modelTypes[i])) { if ("Intent".equals(modelTypes[i])) {
@@ -284,6 +394,7 @@ public class ConfigServiceImpl implements ConfigService {
map.put("functions", functions); map.put("functions", functions);
} }
} }
System.out.println("map: " + map);
} }
if ("Memory".equals(modelTypes[i])) { if ("Memory".equals(modelTypes[i])) {
Map<String, Object> map = (Map<String, Object>) model.getConfigJson(); Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
@@ -4,6 +4,7 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
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.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
@@ -22,6 +24,8 @@ import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceRegisterDTO; import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO; import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
@@ -80,16 +84,29 @@ public class DeviceController {
return new Result<Void>(); return new Result<Void>();
} }
@PutMapping("/enableOta/{id}/{status}") @PutMapping("/update/{id}")
@Operation(summary = "启用/关闭OTA自动升级") @Operation(summary = "更新设备信息")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) { public Result<Void> updateDeviceInfo(@PathVariable String id, @Valid @RequestBody DeviceUpdateDTO deviceUpdateDTO) {
DeviceEntity entity = deviceService.selectById(id); DeviceEntity entity = deviceService.selectById(id);
if (entity == null) { if (entity == null) {
return new Result<Void>().error("设备不存在"); return new Result<Void>().error("设备不存在");
} }
entity.setAutoUpdate(status); UserDetail user = SecurityUser.getUser();
if (!entity.getUserId().equals(user.getId())) {
return new Result<Void>().error("设备不存在");
}
BeanUtils.copyProperties(deviceUpdateDTO, entity);
deviceService.updateById(entity); deviceService.updateById(entity);
return new Result<Void>(); return new Result<Void>();
} }
@PostMapping("/manual-add")
@Operation(summary = "手动添加设备")
@RequiresPermissions("sys:role:normal")
public Result<Void> manualAddDevice(@RequestBody @Valid DeviceManualAddDTO dto) {
UserDetail user = SecurityUser.getUser();
deviceService.manualAddDevice(user.getId(), dto);
return new Result<>();
}
} }
@@ -51,13 +51,12 @@ public class OTAController {
if (StringUtils.isBlank(clientId)) { if (StringUtils.isBlank(clientId)) {
clientId = deviceId; clientId = deviceId;
} }
String macAddress = deviceReportReqDTO.getMacAddress(); boolean macAddressValid = isMacAddressValid(deviceId);
boolean macAddressValid = isMacAddressValid(macAddress);
// 设备Id和Mac地址应是一致的, 并且必须需要application字段 // 设备Id和Mac地址应是一致的, 并且必须需要application字段
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) { if (!macAddressValid) {
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request")); return createResponse(DeviceReportRespDTO.createError("Invalid device ID"));
} }
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO)); return createResponse(deviceService.checkDeviceActive(deviceId, clientId, deviceReportReqDTO));
} }
@Operation(summary = "设备快速检查激活状态") @Operation(summary = "设备快速检查激活状态")
@@ -0,0 +1,11 @@
package xiaozhi.modules.device.dto;
import lombok.Data;
@Data
public class DeviceManualAddDTO {
private String agentId;
private String board; // 设备型号
private String appVersion; // 固件版本
private String macAddress; // Mac地址
}
@@ -0,0 +1,29 @@
package xiaozhi.modules.device.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
/**
* 设备更新DTO
*/
@Data
public class DeviceUpdateDTO implements Serializable {
/**
* 自动更新状态
*/
@Max(1)
@Min(0)
private Integer autoUpdate;
/**
* 设备别名
*/
@Size(max = 64)
private String alias;
private static final long serialVersionUID = 1L;
}
@@ -8,6 +8,7 @@ import xiaozhi.common.service.BaseService;
import xiaozhi.modules.device.dto.DevicePageUserDTO; import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO; import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.vo.UserShowDeviceListVO; import xiaozhi.modules.device.vo.UserShowDeviceListVO;
@@ -87,5 +88,14 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/ */
Date getLatestLastConnectionTime(String agentId); Date getLatestLastConnectionTime(String agentId);
/**
* 手动添加设备
*/
void manualAddDevice(Long userId, DeviceManualAddDTO dto);
/**
* 更新设备连接信息
*/
void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion);
} }
@@ -44,6 +44,7 @@ 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;
import xiaozhi.modules.sys.service.SysUserUtilService; import xiaozhi.modules.sys.service.SysUserUtilService;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
@Slf4j @Slf4j
@Service @Service
@@ -410,4 +411,30 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
} }
return 0; return 0;
} }
@Override
public void manualAddDevice(Long userId, DeviceManualAddDTO dto) {
// 检查mac是否已存在
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", dto.getMacAddress());
DeviceEntity exist = baseDao.selectOne(wrapper);
if (exist != null) {
throw new RenException("该Mac地址已存在");
}
Date now = new Date();
DeviceEntity entity = new DeviceEntity();
entity.setId(dto.getMacAddress());
entity.setUserId(userId);
entity.setAgentId(dto.getAgentId());
entity.setBoard(dto.getBoard());
entity.setAppVersion(dto.getAppVersion());
entity.setMacAddress(dto.getMacAddress());
entity.setCreateDate(now);
entity.setUpdateDate(now);
entity.setLastConnectedAt(now);
entity.setCreator(userId);
entity.setUpdater(userId);
entity.setAutoUpdate(1);
baseDao.insert(entity);
}
} }
@@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.common.validator.group.UpdateGroup; import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
@@ -65,4 +66,10 @@ public class ModelProviderController {
return new Result<>(); return new Result<>();
} }
@GetMapping("/plugin/names")
@Tag(name = "获取插件名称列表")
public Result<List<ModelProviderDTO>> getPluginNameList() {
return ResultUtils.success(modelProviderService.getPluginList());
}
} }
@@ -1,5 +1,6 @@
package xiaozhi.modules.model.service; package xiaozhi.modules.model.service;
import java.util.Collection;
import java.util.List; import java.util.List;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
@@ -7,7 +8,11 @@ import xiaozhi.modules.model.dto.ModelProviderDTO;
public interface ModelProviderService { public interface ModelProviderService {
// List<String> getModelNames(String modelType, String modelName); List<ModelProviderDTO> getPluginList();
ModelProviderDTO getById(String id);
List<ModelProviderDTO> getPluginListByIds(Collection<String> ids);
List<ModelProviderDTO> getListByModelType(String modelType); List<ModelProviderDTO> getListByModelType(String modelType);
@@ -1,5 +1,6 @@
package xiaozhi.modules.model.service.impl; package xiaozhi.modules.model.service.impl;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -8,6 +9,7 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.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;
@@ -32,6 +34,29 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
private final ModelProviderDao modelProviderDao; private final ModelProviderDao modelProviderDao;
@Override
public List<ModelProviderDTO> getPluginList() {
LambdaQueryWrapper<ModelProviderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin");
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override
public ModelProviderDTO getById(String id) {
ModelProviderEntity entity = modelProviderDao.selectById(id);
return ConvertUtils.sourceToTarget(entity, ModelProviderDTO.class);
}
@Override
public List<ModelProviderDTO> getPluginListByIds(Collection<String> ids) {
LambdaQueryWrapper<ModelProviderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ModelProviderEntity::getId, ids);
queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin");
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override @Override
public List<ModelProviderDTO> getListByModelType(String modelType) { public List<ModelProviderDTO> getListByModelType(String modelType) {
@@ -104,6 +104,12 @@ public class SysParamsController {
// 验证OTA地址 // 验证OTA地址
validateOtaUrl(dto.getParamCode(), dto.getParamValue()); validateOtaUrl(dto.getParamCode(), dto.getParamValue());
// 验证MCP地址
validateMcpUrl(dto.getParamCode(), dto.getParamValue());
//
validateVoicePrint(dto.getParamCode(), dto.getParamValue());
sysParamsService.update(dto); sysParamsService.update(dto);
configService.getConfig(false); configService.getConfig(false);
return new Result<Void>(); return new Result<Void>();
@@ -195,4 +201,67 @@ public class SysParamsController {
throw new RenException("OTA接口验证失败:" + e.getMessage()); throw new RenException("OTA接口验证失败:" + e.getMessage());
} }
} }
private void validateMcpUrl(String paramCode, String url) {
if (!paramCode.equals(Constant.SERVER_MCP_ENDPOINT)) {
return;
}
if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("MCP地址不能为空");
}
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("MCP地址不能使用localhost或127.0.0.1");
}
if (!url.toLowerCase().contains("key")) {
throw new RenException("不是正确的MCP地址");
}
try {
// 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode());
}
// 检查响应内容是否包含mcp相关信息
String body = response.getBody();
if (body == null || !body.contains("success")) {
throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口");
}
} catch (Exception e) {
throw new RenException("MCP接口验证失败:" + e.getMessage());
}
}
// 验证声纹接口地址是否正常
private void validateVoicePrint(String paramCode, String url) {
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
return;
}
if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("声纹接口地址不能为空");
}
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("声纹接口地址不能使用localhost或127.0.0.1");
}
if (!url.toLowerCase().contains("key")) {
throw new RenException("不是正确的声纹接口地址");
}
// 验证URL格式
if (!url.toLowerCase().startsWith("http")) {
throw new RenException("声纹接口地址必须以http或https开头");
}
try {
// 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode());
}
// 检查响应内容
String body = response.getBody();
if (body == null || !body.contains("healthy")) {
throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口");
}
} catch (Exception e) {
throw new RenException("声纹接口验证失败:" + e.getMessage());
}
}
} }
@@ -2,36 +2,29 @@ package xiaozhi.modules.sys.enums;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import xiaozhi.common.exception.RenException;
/** /**
* 服务端动作枚举 * 服务端动作枚举
*/ */
public enum ServerActionEnum public enum ServerActionEnum {
{
RESTART("restart"), RESTART("restart"),
UPDATE_CONFIG("update_config"); UPDATE_CONFIG("update_config");
private final String value; private final String value;
ServerActionEnum(String value) ServerActionEnum(String value) {
{
this.value = value; this.value = value;
} }
@JsonValue @JsonValue
public String getValue() public String getValue() {
{
return value; return value;
} }
@JsonCreator @JsonCreator
public static ServerActionEnum fromValue(String value) public static ServerActionEnum fromValue(String value) {
{ for (ServerActionEnum action : ServerActionEnum.values()) {
for (ServerActionEnum action : ServerActionEnum.values()) if (action.value.equalsIgnoreCase(value)) {
{
if (action.value.equalsIgnoreCase(value))
{
return action; return action;
} }
} }
@@ -1,16 +1,16 @@
package xiaozhi.modules.sys.enums; package xiaozhi.modules.sys.enums;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/** /**
* 服务端调用响应枚举 * 服务端调用响应枚举
*/ */
public enum ServerActionResponseEnum public enum ServerActionResponseEnum {
{
SUCCESS("success"), FAIL("fail"); SUCCESS("success"), FAIL("fail");
private final String value; private final String value;
ServerActionResponseEnum(String value) { ServerActionResponseEnum(String value) {
@@ -18,8 +18,7 @@ public enum ServerActionResponseEnum
} }
@JsonValue @JsonValue
public String getValue() public String getValue() {
{
return value; return value;
} }
@@ -1,13 +1,5 @@
package xiaozhi.modules.sys.utils; package xiaozhi.modules.sys.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.socket.*;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import xiaozhi.common.utils.DateUtils;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@@ -15,30 +7,51 @@ import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.util.StopWatch;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.utils.DateUtils;
/** /**
* WebSocketClientResource:支持 try-with-resources 模式 * WebSocketClientResource:支持 try-with-resources 模式
*/ */
@Slf4j @Slf4j
public class WebSocketClientManager implements Closeable public class WebSocketClientManager implements Closeable {
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// 全局回调线程池 // 全局回调线程池
private static final ExecutorService CALLBACK_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { private static final ExecutorService CALLBACK_EXECUTOR = Executors
private final AtomicInteger cnt = new AtomicInteger(); .newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
private final AtomicInteger cnt = new AtomicInteger();
public Thread newThread(Runnable r) public Thread newThread(Runnable r) {
{ Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement());
Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement()); t.setDaemon(true);
t.setDaemon(true); return t;
return t; }
} });
});
private volatile WebSocketSession session; private volatile WebSocketSession session;
private final BlockingQueue<String> textMessageQueue; private final BlockingQueue<String> textMessageQueue;
@@ -51,18 +64,10 @@ public class WebSocketClientManager implements Closeable
private volatile Consumer<byte[]> onBinary; private volatile Consumer<byte[]> onBinary;
private volatile Consumer<Throwable> onError; private volatile Consumer<Throwable> onError;
private final String uri;
private final WebSocketHttpHeaders headers;
private final long connectTimeout;
private final TimeUnit connectUnit;
private final int queueCapacity; private final int queueCapacity;
// 私有构造,仅由 Builder 调用 // 私有构造,仅由 Builder 调用
private WebSocketClientManager(Builder b) { private WebSocketClientManager(Builder b) {
this.uri = b.uri;
this.headers = b.headers != null ? b.headers : new WebSocketHttpHeaders();
this.connectTimeout = b.connectTimeout;
this.connectUnit = b.connectUnit;
this.maxSessionDuration = b.maxSessionDuration; this.maxSessionDuration = b.maxSessionDuration;
this.maxSessionDurationUnit = b.maxSessionDurationUnit; this.maxSessionDurationUnit = b.maxSessionDurationUnit;
this.queueCapacity = b.queueCapacity; this.queueCapacity = b.queueCapacity;
@@ -71,19 +76,24 @@ public class WebSocketClientManager implements Closeable
this.errorFuture = new CompletableFuture<>(); this.errorFuture = new CompletableFuture<>();
} }
public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException { public static WebSocketClientManager build(Builder b)
throws InterruptedException, ExecutionException, TimeoutException, IOException {
WebSocketClientManager ws = new WebSocketClientManager(b); WebSocketClientManager ws = new WebSocketClientManager(b);
StandardWebSocketClient client = new StandardWebSocketClient(); StandardWebSocketClient client = new StandardWebSocketClient();
CompletableFuture<WebSocketSession> future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri)); CompletableFuture<WebSocketSession> future = client.execute(ws.new InternalHandler(b.uri), b.headers,
URI.create(b.uri));
WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit); WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit);
if (sess == null || !sess.isOpen()) if (sess == null || !sess.isOpen()) {
{
throw new IOException("握手失败或会话未打开"); throw new IOException("握手失败或会话未打开");
} }
// 设置缓冲区
sess.setTextMessageSizeLimit(b.bufferSize);
sess.setBinaryMessageSizeLimit(b.bufferSize);
ws.session = sess; ws.session = sess;
return ws; return ws;
} }
/** /**
* 发送 Text * 发送 Text
*/ */
@@ -100,13 +110,10 @@ public class WebSocketClientManager implements Closeable
session.sendMessage(new TextMessage(json)); session.sendMessage(new TextMessage(json));
} }
private <T> List<T> listenerCustom( private <T> List<T> listenerCustom(
BlockingQueue<T> queue, BlockingQueue<T> queue,
Predicate<T> predicate) Predicate<T> predicate)
throws InterruptedException, TimeoutException, ExecutionException throws InterruptedException, TimeoutException, ExecutionException {
{
List<T> collected = new ArrayList<>(); List<T> collected = new ArrayList<>();
long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration); long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration);
@@ -134,19 +141,60 @@ public class WebSocketClientManager implements Closeable
return collected; return collected;
} }
private <T> List<T> listenerCustomWithoutClose(
BlockingQueue<T> queue,
Predicate<T> predicate)
throws InterruptedException, TimeoutException, ExecutionException {
List<T> collected = new ArrayList<>();
long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration);
while (true) {
if (errorFuture.isDone()) {
errorFuture.get();
}
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
throw new TimeoutException("等待批量消息超时");
}
T msg = queue.poll(remaining, TimeUnit.MILLISECONDS);
if (msg == null) {
throw new TimeoutException("等待批量消息超时");
}
collected.add(msg);
if (predicate.test(msg)) {
break;
}
}
// 不调用 close(),保持连接开放
return collected;
}
/** /**
* 同步接收多条消息,直到 predicate 为 true 或超时抛异常; * 同步接收多条消息,直到 predicate 为 true 或超时抛异常;
*
* @return 返回监听期间的所有消息列表 * @return 返回监听期间的所有消息列表
*/ */
public List<String> listener(Predicate<String> predicate) public List<String> listener(Predicate<String> predicate)
throws InterruptedException, TimeoutException, ExecutionException throws InterruptedException, TimeoutException, ExecutionException {
{
return listenerCustom(textMessageQueue, predicate); return listenerCustom(textMessageQueue, predicate);
} }
/**
* 同步接收多条消息,直到 predicate 为 true 或超时抛异常;
* 不自动关闭连接,适用于需要在同一连接上发送多个消息的场景
*
* @return 返回监听期间的所有消息列表
*/
public List<String> listenerWithoutClose(Predicate<String> predicate)
throws InterruptedException, TimeoutException, ExecutionException {
return listenerCustomWithoutClose(textMessageQueue, predicate);
}
public List<byte[]> listenerBinary(Predicate<byte[]> predicate) public List<byte[]> listenerBinary(Predicate<byte[]> predicate)
throws InterruptedException, TimeoutException, ExecutionException throws InterruptedException, TimeoutException, ExecutionException {
{
return listenerCustom(binaryMessageQueue, predicate); return listenerCustom(binaryMessageQueue, predicate);
} }
@@ -183,8 +231,8 @@ public class WebSocketClientManager implements Closeable
if (session != null && session.isOpen()) { if (session != null && session.isOpen()) {
session.close(CloseStatus.NORMAL); session.close(CloseStatus.NORMAL);
} }
} catch (IOException ignored) {
} }
catch (IOException ignored) {}
textMessageQueue.clear(); textMessageQueue.clear();
binaryMessageQueue.clear(); binaryMessageQueue.clear();
errorFuture.completeExceptionally(new IOException("WebSocket 已关闭")); errorFuture.completeExceptionally(new IOException("WebSocket 已关闭"));
@@ -207,7 +255,8 @@ public class WebSocketClientManager implements Closeable
// 保存会话 // 保存会话
WebSocketClientManager.this.session = session; WebSocketClientManager.this.session = session;
this.stopWatch.start(); this.stopWatch.start();
log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN)); log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri,
DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN));
} }
/** /**
@@ -263,19 +312,22 @@ public class WebSocketClientManager implements Closeable
if (stopWatch.isRunning()) { if (stopWatch.isRunning()) {
stopWatch.stop(); stopWatch.stop();
} }
log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s", log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s,断开原因:{}",
targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis())); targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN),
DateUtils.millsToSecond(stopWatch.getTotalTimeMillis()),status);
} }
} }
public static class Builder { public static class Builder {
private String uri; // 目标 WS URI private String uri; // 目标 WS URI
private long connectTimeout = 3; // 请求连接等待时间 private long connectTimeout = 3; // 请求连接等待时间
private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位 private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位
private long maxSessionDuration = 5; // 最大连线时间,默认5秒 private long maxSessionDuration = 5; // 最大连线时间,默认5秒
private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位 private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位
private int queueCapacity = 100; // 消息队列容量 private int queueCapacity = 100; // 消息队列容量
private WebSocketHttpHeaders headers; // 请求头 private int bufferSize = 8 * 1024; //默认 8kb
private WebSocketHttpHeaders headers; // 请求头
/** /**
* 目标 WS URI * 目标 WS URI
@@ -306,8 +358,13 @@ public class WebSocketClientManager implements Closeable
this.queueCapacity = c; this.queueCapacity = c;
return this; return this;
} }
public Builder bufferSize(int c) {
this.bufferSize = c;
return this;
}
public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException { public WebSocketClientManager build()
throws InterruptedException, ExecutionException, TimeoutException, IOException {
return WebSocketClientManager.build(this); return WebSocketClientManager.build(this);
} }
@@ -26,6 +26,12 @@ public class TimbreDataDTO {
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@Schema(description = "参考音频路径")
private String referenceAudio;
@Schema(description = "參考文本")
private String referenceText;
@Schema(description = "排序") @Schema(description = "排序")
@Min(value = 0, message = "{sort.number}") @Min(value = 0, message = "{sort.number}")
private long sort; private long sort;
@@ -34,6 +34,12 @@ public class TimbreEntity {
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@Schema(description = "参考音频路径")
private String referenceAudio;
@Schema(description = "參考文本")
private String referenceText;
@Schema(description = "排序") @Schema(description = "排序")
private long sort; private long sort;
@@ -25,6 +25,12 @@ public class TimbreDetailsVO implements Serializable {
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@Schema(description = "参考音频路径")
private String referenceAudio;
@Schema(description = "參考文本")
private String referenceText;
@Schema(description = "排序") @Schema(description = "排序")
private long sort; private long sort;
@@ -1,6 +0,0 @@
-- 更新intent_llmM供应器
update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"},{"key":"functions","label":"函数列表","type":"dict","dict_name":"functions"}]' where id = 'SYSTEM_Intent_intent_llm';
-- 更新ChatGLMLLM的意图识别配置
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\", \"functions\": \"get_weather;get_news_from_newsnow;play_music\"}' where id = 'Intent_intent_llm';
-- 更新函数调用意图识别配置
UPDATE `ai_model_config` SET config_json = REPLACE(config_json, ';get_news;', ';get_news_from_newsnow;') WHERE id = 'Intent_function_call';
@@ -0,0 +1,189 @@
-- ===============================
-- 一、在ai_model_provider中插入plugin 记录
-- ===============================
START TRANSACTION;
-- intent_llm和function_call不设置函数列表
update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"}]' where id = 'SYSTEM_Intent_intent_llm';
update `ai_model_provider` set fields = '[]' where id = 'SYSTEM_Intent_function_call';
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
UPDATE `ai_model_config` SET config_json = '{\"type\": \"function_call\"}' WHERE id = 'Intent_function_call';
delete from ai_model_provider where model_type = 'Plugin';
-- 1. 天气查询
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_WEATHER',
'Plugin',
'get_weather',
'天气查询',
JSON_ARRAY(
JSON_OBJECT(
'key', 'api_key',
'type', 'string',
'label', '天气插件 API 密钥',
'default', (SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.api_key')
),
JSON_OBJECT(
'key', 'default_location',
'type', 'string',
'label', '默认查询城市',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.default_location')
),
JSON_OBJECT(
'key', 'api_host',
'type', 'string',
'label', '开发者 API Host',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.api_host')
)
),
10, 0, NOW(), 0, NOW());
-- 6. 本地播放音乐
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_MUSIC',
'Plugin',
'play_music',
'服务器音乐播放',
JSON_ARRAY(),
20, 0, NOW(), 0, NOW());
-- 2. 新闻订阅
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_NEWS_CHINANEWS',
'Plugin',
'get_news_from_chinanews',
'中新网新闻',
JSON_ARRAY(
JSON_OBJECT(
'key', 'default_rss_url',
'type', 'string',
'label', '默认 RSS 源',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_news.default_rss_url')
),
JSON_OBJECT(
'key', 'society_rss_url',
'type', 'string',
'label', '社会新闻 RSS 地址',
'default',
'https://www.chinanews.com.cn/rss/society.xml'
),
JSON_OBJECT(
'key', 'world_rss_url',
'type', 'string',
'label', '国际新闻 RSS 地址',
'default',
'https://www.chinanews.com.cn/rss/world.xml'
),
JSON_OBJECT(
'key', 'finance_rss_url',
'type', 'string',
'label', '财经新闻 RSS 地址',
'default',
'https://www.chinanews.com.cn/rss/finance.xml'
)
),
30, 0, NOW(), 0, NOW());
-- 3. 新闻订阅
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_NEWS_NEWSNOW',
'Plugin',
'get_news_from_newsnow',
'newsnow新闻聚合',
JSON_ARRAY(
JSON_OBJECT(
'key', 'url',
'type', 'string',
'label', '接口地址',
'default',
'https://newsnow.busiyi.world/api/s?id='
)
),
40, 0, NOW(), 0, NOW());
-- 4. HomeAssistant 状态查询
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_GET_STATE',
'Plugin',
'hass_get_state',
'HomeAssistant设备状态查询',
JSON_ARRAY(
JSON_OBJECT(
'key', 'base_url',
'type', 'string',
'label', 'HA 服务器地址',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.base_url')
),
JSON_OBJECT(
'key', 'api_key',
'type', 'string',
'label', 'HA API 访问令牌',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.api_key')
),
JSON_OBJECT(
'key', 'devices',
'type', 'array',
'label', '设备列表(名称,实体ID;…)',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.devices')
)
),
50, 0, NOW(), 0, NOW());
-- 5. HomeAssistant 状态写入
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_SET_STATE',
'Plugin',
'hass_set_state',
'HomeAssistant设备状态修改',
JSON_ARRAY(),
60, 0, NOW(), 0, NOW());
-- 5. HomeAssistant 音乐播放
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_PLAY_MUSIC',
'Plugin',
'hass_play_music',
'HomeAssistant音乐播放',
JSON_ARRAY(),
70, 0, NOW(), 0, NOW());
-- ===============================
-- 二、删除sys_params中旧的plugins.*参数
-- ===============================
DELETE
FROM sys_params
WHERE param_code LIKE 'plugins.%';
-- ===============================
-- 三、添加智能体插件id字段
-- ===============================
CREATE TABLE IF NOT EXISTS ai_agent_plugin_mapping
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
agent_id VARCHAR(32) NOT NULL COMMENT '智能体ID',
plugin_id VARCHAR(32) NOT NULL COMMENT '插件ID',
param_info JSON NOT NULL COMMENT '参数信息',
UNIQUE KEY uk_agent_provider (agent_id, plugin_id)
) COMMENT 'Agent与插件的唯一映射表';
COMMIT;
@@ -0,0 +1,20 @@
-- 增加LinkeraiTTS供应器和模型配置
delete from `ai_model_provider` where id = 'SYSTEM_TTS_LinkeraiTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_LinkeraiTTS', 'TTS', 'linkerai', 'Linkerai语音合成', '[{"key":"api_url","label":"API地址","type":"string"},{"key":"audio_format","label":"音频格式","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"voice","label":"默认音色","type":"string"}]', 14, 1, NOW(), 1, NOW());
delete from `ai_model_config` where id = 'TTS_LinkeraiTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_LinkeraiTTS', 'TTS', 'LinkeraiTTS', 'Linkerai语音合成', 0, 1, '{\"type\": \"linkerai\", \"api_url\": \"https://tts.linkerai.cn/tts\", \"audio_format\": \"pcm\", \"access_token\": \"U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL\", \"voice\": \"OUeAo1mhq6IBExi\"}', NULL, NULL, 17, NULL, NULL, NULL, NULL);
-- LinkeraiTTS模型配置说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://tts.linkerai.cn/docs',
`remark` = 'Linkerai语音合成服务配置说明:
1. 访问 https://linkerai.cn 注册并获取访问令牌
2. 默认的access_token供测试使用,请勿用于商业用途
3. 支持声音克隆功能,可自行上传音频,填入voice参数
4. 如果voice参数为空,将使用默认声音' WHERE `id` = 'TTS_LinkeraiTTS';
delete from `ai_tts_voice` where tts_model_id = 'TTS_LinkeraiTTS';
INSERT INTO `ai_tts_voice` VALUES ('TTS_LinkeraiTTS_0001', 'TTS_LinkeraiTTS', '芷若', 'OUeAo1mhq6IBExi', '中文', NULL, NULL, 1, NULL, NULL, NULL, NULL);
@@ -0,0 +1,3 @@
-- 智控台开启唤醒词加速
update `sys_params` set param_value = '你好小智;你好小志;小爱同学;你好小鑫;你好小新;小美同学;小龙小龙;喵喵同学;小滨小滨;小冰小冰;嘿你好呀' where param_code = 'wakeup_words';
update `sys_params` set param_value = 'true' where param_code = 'enable_wakeup_words_response_cache';
@@ -0,0 +1,3 @@
ALTER TABLE `ai_tts_voice`
ADD COLUMN `reference_audio` VARCHAR(500) DEFAULT NULL COMMENT '参考音频路径' AFTER `remark`,
ADD COLUMN `reference_text` VARCHAR(500) DEFAULT NULL COMMENT '参考文本' AFTER `reference_audio`;
@@ -0,0 +1 @@
ALTER TABLE ai_agent_plugin_mapping CONVERT TO CHARACTER SET utf8mb4;
@@ -0,0 +1,19 @@
-- LLM意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = 'LLM意图识别配置说明:
1. 使用独立的LLM进行意图识别
2. 默认使用selected_module.LLM的模型
3. 可以配置使用独立的LLM(如免费的ChatGLMLLM
4. 通用性强,但会增加处理时间
配置说明:
1. 在llm字段中指定使用的LLM模型
2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm';
-- 函数调用意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '函数调用意图识别配置说明:
1. 使用LLM的function_call功能进行意图识别
2. 需要所选择的LLM支持function_call
3. 按需调用工具,处理速度快' WHERE `id` = 'Intent_function_call';
@@ -0,0 +1,18 @@
-- 更新现有的 get_news_from_newsnow 插件配置
UPDATE ai_model_provider
SET fields = JSON_ARRAY(
JSON_OBJECT(
'key', 'url',
'type', 'string',
'label', '接口地址',
'default', 'https://newsnow.busiyi.world/api/s?id='
),
JSON_OBJECT(
'key', 'news_sources',
'type', 'string',
'label', '新闻源配置',
'default', '澎湃新闻;百度热搜;财联社'
)
)
WHERE provider_code = 'get_news_from_newsnow'
AND model_type = 'Plugin';
@@ -0,0 +1,2 @@
delete from `sys_params` where id = 113;
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (113, 'server.mcp_endpoint', 'null', 'string', 1, 'mcp接入点地址');
@@ -0,0 +1,4 @@
-- 添加声纹接口地址参数配置
delete from `sys_params` where id = 114;
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
VALUES (114, 'server.voice_print', 'null', 'string', 1, '声纹接口地址');
@@ -0,0 +1,12 @@
DROP TABLE IF EXISTS ai_agent_voice_print;
create table ai_agent_voice_print (
id varchar(32) NOT NULL COMMENT '声纹ID',
agent_id varchar(32) NOT NULL COMMENT '关联的智能体ID',
source_name varchar(50) NOT NULL COMMENT '声纹来源的人的姓名',
introduce varchar(200) COMMENT '描述声纹来源的这个人',
create_date DATETIME COMMENT '创建时间',
creator bigint COMMENT '创建者',
update_date DATETIME COMMENT '修改时间',
updater bigint COMMENT '修改者',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体声纹表'
@@ -0,0 +1,26 @@
-- 添加阿里云流式ASR供应器
delete from `ai_model_provider` where id = 'SYSTEM_ASR_AliyunStreamASR';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_AliyunStreamASR', 'ASR', 'aliyun_stream', '阿里云语音识别(流式)', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"host","label":"服务地址","type":"string"},{"key":"max_sentence_silence","label":"断句检测时间","type":"number"},{"key":"output_dir","label":"输出目录","type":"string"}]', 6, 1, NOW(), 1, NOW());
-- 添加阿里云流式ASR模型配置
delete from `ai_model_config` where id = 'ASR_AliyunStreamASR';
INSERT INTO `ai_model_config` VALUES ('ASR_AliyunStreamASR', 'ASR', 'AliyunStreamASR', '阿里云语音识别(流式)', 0, 1, '{\"type\": \"aliyun_stream\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"host\": \"nls-gateway-cn-shanghai.aliyuncs.com\", \"max_sentence_silence\": 800, \"output_dir\": \"tmp/\"}', NULL, NULL, 8, NULL, NULL, NULL, NULL);
-- 更新阿里云流式ASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://nls-portal.console.aliyun.com/',
`remark` = '阿里云流式ASR配置说明:
1. 阿里云ASR和阿里云(流式)ASR的区别是:阿里云ASR是一次性识别,阿里云(流式)ASR是实时流式识别
2. 流式ASR具有更低的延迟和更好的实时性,适合语音交互场景
3. 需要在阿里云智能语音交互控制台创建应用并获取认证信息
4. 支持中文实时语音识别,支持标点符号预测和逆文本规范化
5. 需要网络连接,输出文件保存在tmp/目录
申请步骤:
1. 访问 https://nls-portal.console.aliyun.com/ 开通智能语音交互服务
2. 访问 https://nls-portal.console.aliyun.com/applist 创建项目并获取appkey
3. 访问 https://nls-portal.console.aliyun.com/overview 获取临时token(或配置access_key_id和access_key_secret自动获取)
4. 如需动态token管理,建议配置access_key_id和access_key_secret
5. max_sentence_silence参数控制断句检测时间(毫秒),默认800ms
如需了解更多参数配置,请参考:https://help.aliyun.com/zh/isi/developer-reference/real-time-speech-recognition
' WHERE `id` = 'ASR_AliyunStreamASR';
@@ -0,0 +1,56 @@
-- 添加阿里云流式TTS供应器
delete from `ai_model_provider` where id = 'SYSTEM_TTS_AliyunStreamTTS';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_TTS_AliyunStreamTTS', 'TTS', 'aliyun_stream', '阿里云语音合成(流式)', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"host","label":"服务地址","type":"string"},{"key":"voice","label":"默认音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key":"volume","label":"音量","type":"number"},{"key":"speech_rate","label":"语速","type":"number"},{"key":"pitch_rate","label":"音调","type":"number"},{"key":"output_dir","label":"输出目录","type":"string"}]', 15, 1, NOW(), 1, NOW());
-- 添加阿里云流式TTS模型配置
delete from `ai_model_config` where id = 'TTS_AliyunStreamTTS';
INSERT INTO `ai_model_config` VALUES ('TTS_AliyunStreamTTS', 'TTS', 'AliyunStreamTTS', '阿里云语音合成(流式)', 0, 1, '{\"type\": \"aliyun_stream\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"host\": \"nls-gateway-cn-beijing.aliyuncs.com\", \"voice\": \"longxiaochun\", \"format\": \"pcm\", \"sample_rate\": 16000, \"volume\": 50, \"speech_rate\": 0, \"pitch_rate\": 0, \"output_dir\": \"tmp/\"}', NULL, NULL, 18, NULL, NULL, NULL, NULL);
-- 更新阿里云流式TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://nls-portal.console.aliyun.com/',
`remark` = '阿里云流式TTS配置说明:
1. 阿里云TTS和阿里云(流式)TTS的区别是:阿里云TTS是一次性合成,阿里云(流式)TTS是实时流式合成
2. 流式TTS具有更低的延迟和更好的实时性,适合语音交互场景
3. 需要在阿里云智能语音交互控制台创建应用并获取认证信息
4. 支持CosyVoice大模型音色,音质更加自然
5. 支持实时调节音量、语速、音调等参数
申请步骤:
1. 访问 https://nls-portal.console.aliyun.com/ 开通智能语音交互服务
2. 访问 https://nls-portal.console.aliyun.com/applist 创建项目并获取appkey
3. 访问 https://nls-portal.console.aliyun.com/overview 获取临时token(或配置access_key_id和access_key_secret自动获取)
4. 如需动态token管理,建议配置access_key_id和access_key_secret
5. 可选择北京、上海等不同地域的服务器以优化延迟
6. voice参数支持CosyVoice大模型音色,如longxiaochun、longyueyue等
如需了解更多参数配置,请参考:https://help.aliyun.com/zh/isi/developer-reference/real-time-speech-synthesis
' WHERE `id` = 'TTS_AliyunStreamTTS';
-- 添加阿里云流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_AliyunStreamTTS';
-- 温柔女声系列
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0001', 'TTS_AliyunStreamTTS', '龙小淳-温柔姐姐', 'longxiaochun', '中文及中英文混合', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0002', 'TTS_AliyunStreamTTS', '龙小夏-温柔女声', 'longxiaoxia', '中文及中英文混合', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0003', 'TTS_AliyunStreamTTS', '龙玫-温柔女声', 'longmei', '中文及中英文混合', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0004', 'TTS_AliyunStreamTTS', '龙瑰-温柔女声', 'longgui', '中文及中英文混合', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
-- 御姐女声系列
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0005', 'TTS_AliyunStreamTTS', '龙玉-御姐女声', 'longyu', '中文及中英文混合', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0006', 'TTS_AliyunStreamTTS', '龙娇-御姐女声', 'longjiao', '中文及中英文混合', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
-- 男声系列
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0007', 'TTS_AliyunStreamTTS', '龙臣-译制片男声', 'longchen', '中文及中英文混合', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0008', 'TTS_AliyunStreamTTS', '龙修-青年男声', 'longxiu', '中文及中英文混合', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0009', 'TTS_AliyunStreamTTS', '龙橙-阳光男声', 'longcheng', '中文及中英文混合', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0010', 'TTS_AliyunStreamTTS', '龙哲-成熟男声', 'longzhe', '中文及中英文混合', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
-- 专业播报系列
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0011', 'TTS_AliyunStreamTTS', 'Bella2.0-新闻女声', 'loongbella', '中文及中英文混合', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0012', 'TTS_AliyunStreamTTS', 'Stella2.0-飒爽女声', 'loongstella', '中文及中英文混合', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0013', 'TTS_AliyunStreamTTS', '龙书-新闻男声', 'longshu', '中文及中英文混合', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0014', 'TTS_AliyunStreamTTS', '龙婧-严肃女声', 'longjing', '中文及中英文混合', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
-- 特色音色系列
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0015', 'TTS_AliyunStreamTTS', '龙奇-活泼童声', 'longqi', '中文及中英文混合', NULL, NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0016', 'TTS_AliyunStreamTTS', '龙华-活泼女童', 'longhua', '中文及中英文混合', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0017', 'TTS_AliyunStreamTTS', '龙无-无厘头男声', 'longwu', '中文及中英文混合', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0018', 'TTS_AliyunStreamTTS', '龙大锤-幽默男声', 'longdachui', '中文及中英文混合', NULL, NULL, NULL, NULL, 18, NULL, NULL, NULL, NULL);
-- 粤语系列
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0019', 'TTS_AliyunStreamTTS', '龙嘉怡-粤语女声', 'longjiayi', '粤语及粤英混合', NULL, NULL, NULL, NULL, 19, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunStreamTTS_0020', 'TTS_AliyunStreamTTS', '龙桃-粤语女声', 'longtao', '粤语及粤英混合', NULL, NULL, NULL, NULL, 20, NULL, NULL, NULL, NULL);
@@ -0,0 +1,3 @@
-- 智能体声纹添加新字段
ALTER TABLE ai_agent_voice_print
ADD COLUMN audio_id VARCHAR(32) NOT NULL COMMENT '音频ID';
@@ -0,0 +1,38 @@
-- OpenAI ASR模型供应器
delete from `ai_model_provider` where id = 'SYSTEM_ASR_OpenaiASR';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_OpenaiASR', 'ASR', 'openai', 'OpenAI语音识别', '[{"key": "base_url", "type": "string", "label": "基础URL"}, {"key": "model_name", "type": "string", "label": "模型名称"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]', 9, 1, NOW(), 1, NOW());
-- OpenAI ASR模型配置
delete from `ai_model_config` where id = 'ASR_OpenaiASR';
INSERT INTO `ai_model_config` VALUES ('ASR_OpenaiASR', 'ASR', 'OpenaiASR', 'OpenAI语音识别', 0, 1, '{\"type\": \"openai\", \"api_key\": \"\", \"base_url\": \"https://api.openai.com/v1/audio/transcriptions\", \"model_name\": \"gpt-4o-mini-transcribe\", \"output_dir\": \"tmp/\"}', NULL, NULL, 9, NULL, NULL, NULL, NULL);
-- groq ASR模型配置
delete from `ai_model_config` where id = 'ASR_GroqASR';
INSERT INTO `ai_model_config` VALUES ('ASR_GroqASR', 'ASR', 'GroqASR', 'Groq语音识别', 0, 1, '{\"type\": \"openai\", \"api_key\": \"\", \"base_url\": \"https://api.groq.com/openai/v1/audio/transcriptions\", \"model_name\": \"whisper-large-v3-turbo\", \"output_dir\": \"tmp/\"}', NULL, NULL, 10, NULL, NULL, NULL, NULL);
-- 更新OpenAI ASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://platform.openai.com/docs/api-reference/audio/createTranscription',
`remark` = 'OpenAI ASR配置说明:
1. 需要在OpenAI开放平台创建组织并获取api_key
2. 支持中、英、日、韩等多种语音识别,具体参考文档https://platform.openai.com/docs/guides/speech-to-text
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
**OpenAi ASR申请步骤:**
1.登录OpenAI Platform。https://auth.openai.com/log-in
2.创建api-key https://platform.openai.com/settings/organization/api-keys
3.模型可以选择gpt-4o-transcribe或GPT-4o mini Transcribe
' WHERE `id` = 'ASR_OpenaiASR';
-- 更新Groq ASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.groq.com/docs/speech-to-text',
`remark` = 'Groq ASR配置说明:
1.登录groq Console。https://console.groq.com/home
2.创建api-key https://console.groq.com/keys
3.模型可以选择whisper-large-v3-turbo或whisper-large-v3distil-whisper-large-v3-en仅支持英语转录)
' WHERE `id` = 'ASR_GroqASR';
@@ -107,13 +107,6 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505081146.sql path: classpath:db/changelog/202505081146.sql
- changeSet:
id: 202505091409
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505091409.sql
- changeSet: - changeSet:
id: 202505091555 id: 202505091555
author: whosmyqueen author: whosmyqueen
@@ -170,6 +163,13 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505271414.sql path: classpath:db/changelog/202505271414.sql
- changeSet:
id: 202505292203
author: CAIXYPROMISE
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505292203.sql
- changeSet: - changeSet:
id: 202506010920 id: 202506010920
author: hrz author: hrz
@@ -190,4 +190,95 @@ databaseChangeLog:
changes: changes:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202506032232.sql path: classpath:db/changelog/202506032232.sql
- changeSet:
id: 202506051538
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506051538.sql
- changeSet:
id: 202506080955
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506080955.sql
- changeSet:
id: 202506091720
author: shane0411
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506091720.sql
- changeSet:
id: 202506161101
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506161101.sql
- changeSet:
id: 202506191643
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506191643.sql
- changeSet:
id: 202506251620
author: Tink
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506251620.sql
- changeSet:
id: 202506261637
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506261637.sql
- changeSet:
id: 202507101203
author: luruxian
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202507101203.sql
- changeSet:
id: 202507071130
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202507071130.sql
- changeSet:
id: 202507071530
author: cgd
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202507071530.sql
- changeSet:
id: 202507031602
author: zjy
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202507031602.sql
- changeSet:
id: 202507041018
author: zjy
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202507041018.sql
- changeSet:
id: 202507081646
author: zjy
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202507081646.sql
@@ -1 +1 @@
redis.call('FLUSHALL') redis.call('FLUSHDB')
@@ -5,4 +5,72 @@
<select id="getDeviceCountByAgentId" resultType="java.lang.Integer"> <select id="getDeviceCountByAgentId" resultType="java.lang.Integer">
SELECT COUNT(*) FROM ai_device WHERE agent_id = #{agentId} SELECT COUNT(*) FROM ai_device WHERE agent_id = #{agentId}
</select> </select>
<resultMap id="AgentInfoMap" type="xiaozhi.modules.agent.vo.AgentInfoVO">
<id column="id" property="id"/>
<result column="userId" property="userId"/>
<result column="agentCode" property="agentCode"/>
<result column="agentName" property="agentName"/>
<result column="asrModelId" property="asrModelId"/>
<result column="vadModelId" property="vadModelId"/>
<result column="llmModelId" property="llmModelId"/>
<result column="ttsModelId" property="ttsModelId"/>
<result column="ttsVoiceId" property="ttsVoiceId"/>
<result column="memModelId" property="memModelId"/>
<result column="intentModelId" property="intentModelId"/>
<result column="functions" property="functions"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<result column="chatHistoryConf" property="chatHistoryConf"/>
<result column="systemPrompt" property="systemPrompt"/>
<result column="summaryMemory" property="summaryMemory"/>
<result column="langCode" property="langCode"/>
<result column="language" property="language"/>
<result column="sort" property="sort"/>
<result column="creator" property="creator"/>
<result column="createdAt" property="createdAt"/>
<result column="updater" property="updater"/>
<result column="updatedAt" property="updatedAt"/>
</resultMap>
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
SELECT a.id,
a.user_id AS userId,
a.agent_code AS agentCode,
a.agent_name AS agentName,
a.asr_model_id AS asrModelId,
a.vad_model_id AS vadModelId,
a.llm_model_id AS llmModelId,
a.vllm_model_id AS vllmModelId,
a.tts_model_id AS ttsModelId,
a.tts_voice_id AS ttsVoiceId,
a.mem_model_id AS memModelId,
a.intent_model_id AS intentModelId,
COALESCE(
(SELECT JSON_ARRAYAGG(
JSON_OBJECT(
'id', m.id,
'agentId', m.agent_id,
'pluginId', m.plugin_id,
'paramInfo', m.param_info
)
)
FROM ai_agent_plugin_mapping m
WHERE m.agent_id = a.id),
JSON_ARRAY()
) AS functions,
a.chat_history_conf AS chatHistoryConf,
a.system_prompt AS systemPrompt,
a.summary_memory AS summaryMemory,
a.lang_code AS langCode,
a.language AS language,
a.sort,
a.creator,
a.created_at AS createdAt,
a.updater,
a.updated_at AS updatedAt
FROM ai_agent a
WHERE a.id = #{agentId}
</select>
</mapper> </mapper>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.agent.dao.AgentPluginMappingMapper">
<resultMap id="BaseResultMap" type="xiaozhi.modules.agent.entity.AgentPluginMapping">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="agentId" column="agent_id" jdbcType="VARCHAR"/>
<result property="pluginId" column="plugin_id" jdbcType="VARCHAR"/>
<result property="paramInfo" column="param_info" jdbcType="VARCHAR"/>
</resultMap>
<!-- 用于映射根据agentId查询完整插件信息 -->
<resultMap id="AgentPluginWithCodeMap" type="xiaozhi.modules.agent.entity.AgentPluginMapping">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="agentId" property="agentId" jdbcType="VARCHAR"/>
<result column="pluginId" property="pluginId" jdbcType="VARCHAR"/>
<result column="paramInfo" property="paramInfo" jdbcType="VARCHAR"/>
<result column="providerCode" property="providerCode" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id,agent_id,plugin_id
</sql>
<select id="selectPluginsByAgentId" resultMap="AgentPluginWithCodeMap">
SELECT m.id AS id,
m.agent_id AS agentId,
m.plugin_id AS pluginId,
m.param_info AS paramInfo,
(
SELECT
p.provider_code
FROM
ai_model_provider p
WHERE
p.id = m.plugin_id
LIMIT
1
) AS providerCode
FROM ai_agent_plugin_mapping m
WHERE m.agent_id = #{agentId}
</select>
</mapper>
@@ -0,0 +1,85 @@
package xiaozhi.common.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class AESUtilsTest {
@Test
public void testEncryptAndDecrypt() {
String key = "xiaozhi1234567890";
String plainText = "Hello, 小智!";
System.out.println("原始文本: " + plainText);
System.out.println("密钥: " + key);
// 加密
String encrypted = AESUtils.encrypt(key, plainText);
System.out.println("加密结果: " + encrypted);
// 解密
String decrypted = AESUtils.decrypt(key, encrypted);
System.out.println("解密结果: " + decrypted);
// 验证
assertEquals(plainText, decrypted, "加解密结果应该一致");
System.out.println("加解密一致性: " + plainText.equals(decrypted));
}
@Test
public void testDifferentKeyLengths() {
String[] keys = {
"1234567890123456", // 16位
"123456789012345678901234", // 24位
"12345678901234567890123456789012", // 32位
"short", // 短密钥
"verylongkeythatwillbetruncatedto32bytes" // 长密钥
};
String plainText = "测试文本";
for (String key : keys) {
String encrypted = AESUtils.encrypt(key, plainText);
String decrypted = AESUtils.decrypt(key, encrypted);
assertEquals(plainText, decrypted, "密钥长度: " + key.length());
}
}
@Test
public void testSpecialCharacters() {
String key = "xiaozhi1234567890";
String[] testTexts = {
"Hello World",
"你好世界",
"Hello, 小智!",
"特殊字符: !@#$%^&*()",
"数字123和中文混合",
"Emoji: 😀🎉🚀",
"空字符串测试",
""
};
for (String text : testTexts) {
String encrypted = AESUtils.encrypt(key, text);
String decrypted = AESUtils.decrypt(key, encrypted);
assertEquals(text, decrypted, "测试文本: " + text);
}
}
@Test
public void testCrossLanguageCompatibility() {
// 这些是Python版本生成的加密结果,用于测试跨语言兼容性
String key = "xiaozhi1234567890";
String plainText = "Hello, 小智!";
// Python版本生成的加密结果(需要运行Python测试后获取)
// String pythonEncrypted = "从Python测试中获取的加密结果";
// String decrypted = AESUtils.decrypt(key, pythonEncrypted);
// assertEquals(plainText, decrypted, "Java应该能解密Python加密的结果");
// 生成Java加密结果供Python测试
String javaEncrypted = AESUtils.encrypt(key, plainText);
System.out.println("Java加密结果供Python测试: " + javaEncrypted);
}
}
+12 -12
View File
@@ -42,7 +42,7 @@ nav {
} }
.el-message { .el-message {
top: 45px !important; top: 70px !important;
} }
</style> </style>
<script> <script>
@@ -64,26 +64,26 @@ export default {
if (this.isCDNEnabled) { if (this.isCDNEnabled) {
// 添加全局快捷键Alt+C用于显示缓存查看器 // 添加全局快捷键Alt+C用于显示缓存查看器
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
// 在全局对象上添加缓存检查方法,便于调试 // 在全局对象上添加缓存检查方法,便于调试
window.checkCDNCacheStatus = () => { window.checkCDNCacheStatus = () => {
this.showCacheViewer = true; this.showCacheViewer = true;
}; };
// 在控制台输出提示信息 // 在控制台输出提示信息
console.info( console.info(
'%c[小智服务] CDN缓存检查工具已加载', '%c[小智服务] CDN缓存检查工具已加载',
'color: #409EFF; font-weight: bold;' 'color: #409EFF; font-weight: bold;'
); );
console.info( console.info(
'按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态' '按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态'
); );
// 检查Service Worker状态 // 检查Service Worker状态
this.checkServiceWorkerStatus(); this.checkServiceWorkerStatus();
} else { } else {
console.info( console.info(
'%c[小智服务] CDN模式已禁用,使用本地打包资源', '%c[小智服务] CDN模式已禁用,使用本地打包资源',
'color: #67C23A; font-weight: bold;' 'color: #67C23A; font-weight: bold;'
); );
} }
@@ -108,19 +108,19 @@ export default {
const registrations = await navigator.serviceWorker.getRegistrations(); const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length > 0) { if (registrations.length > 0) {
console.info( console.info(
'%c[小智服务] Service Worker已注册', '%c[小智服务] Service Worker已注册',
'color: #67C23A; font-weight: bold;' 'color: #67C23A; font-weight: bold;'
); );
// 输出缓存状态到控制台 // 输出缓存状态到控制台
setTimeout(async () => { setTimeout(async () => {
const hasCaches = await logCacheStatus(); const hasCaches = await logCacheStatus();
if (!hasCaches) { if (!hasCaches) {
console.info( console.info(
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立', '%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
'color: #E6A23C; font-weight: bold;' 'color: #E6A23C; font-weight: bold;'
); );
// 开发环境下提供额外提示 // 开发环境下提供额外提示
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.info( console.info(
@@ -136,10 +136,10 @@ export default {
}, 2000); }, 2000);
} else { } else {
console.info( console.info(
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存', '%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
'color: #F56C6C; font-weight: bold;' 'color: #F56C6C; font-weight: bold;'
); );
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.info( console.info(
'%c[小智服务] 在开发环境中,这是正常现象', '%c[小智服务] 在开发环境中,这是正常现象',
+1 -1
View File
@@ -7,6 +7,7 @@ import model from './module/model.js'
import ota from './module/ota.js' import ota from './module/ota.js'
import timbre from "./module/timbre.js" import timbre from "./module/timbre.js"
import user from './module/user.js' import user from './module/user.js'
/** /**
* 接口地址 * 接口地址
* 开发时自动读取使用.env.development文件 * 开发时自动读取使用.env.development文件
@@ -22,7 +23,6 @@ export function getServiceUrl() {
return DEV_API_SERVICE return DEV_API_SERVICE
} }
/** request服务封装 */ /** request服务封装 */
export default { export default {
getServiceUrl, getServiceUrl,
+122
View File
@@ -143,4 +143,126 @@ export default {
}); });
}).send(); }).send();
}, },
// 获取智能体的MCP接入点地址
getAgentMcpAccessAddress(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/mcp/address/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getAgentMcpAccessAddress(agentId, callback);
});
}).send();
},
// 获取智能体的MCP工具列表
getAgentMcpToolsList(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/mcp/tools/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getAgentMcpToolsList(agentId, callback);
});
}).send();
},
// 添加智能体的声纹
addAgentVoicePrint(voicePrintData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/voice-print`)
.method('POST')
.data(voicePrintData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.addAgentVoicePrint(voicePrintData, callback);
});
}).send();
},
// 获取指定智能体声纹列表
getAgentVoicePrintList(id,callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/voice-print/list/${id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getAgentVoicePrintList(id,callback);
});
}).send();
},
// 删除智能体声纹
deleteAgentVoicePrint(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/voice-print/${id}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.deleteAgentVoicePrint(id, callback);
});
}).send();
},
// 更新智能体声纹
updateAgentVoicePrint(voicePrintData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/voice-print`)
.method('PUT')
.data(voicePrintData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.updateAgentVoicePrint(voicePrintData, callback);
});
}).send();
},
// 获取指定智能体用户类型聊天记录
getRecentlyFiftyByAgentId(id,callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/${id}/chat-history/user`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getRecentlyFiftyByAgentId(id,callback);
});
}).send();
},
// 获取指定智能体用户类型聊天记录
getContentByAudioId(id,callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/${id}/chat-history/audio`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getContentByAudioId(id,callback);
});
}).send();
},
} }
+21 -3
View File
@@ -51,10 +51,11 @@ export default {
}); });
}).send(); }).send();
}, },
enableOtaUpgrade(id, status, callback) { updateDeviceInfo(id, payload, callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/device/enableOta/${id}/${status}`) .url(`${getServiceUrl()}/device/update/${id}`)
.method('PUT') .method('PUT')
.data(payload)
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
@@ -63,8 +64,25 @@ export default {
console.error('更新OTA状态失败:', err) console.error('更新OTA状态失败:', err)
this.$message.error(err.msg || '更新OTA状态失败') this.$message.error(err.msg || '更新OTA状态失败')
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.enableOtaUpgrade(id, status, callback) this.updateDeviceInfo(id, payload, callback)
}) })
}).send() }).send()
}, },
// 手动添加设备
manualAddDevice(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/manual-add`)
.method('POST')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('手动添加设备失败:', err);
RequestService.reAjaxFun(() => {
this.manualAddDevice(params, callback);
});
}).send();
},
} }
+16
View File
@@ -305,4 +305,20 @@ export default {
}) })
}).send() }).send()
}, },
// 获取插件列表
getPluginFunctionList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/provider/plugin/names`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
this.$message.error(err.msg || '获取插件列表失败')
RequestService.reAjaxFun(() => {
this.getPluginFunctionList(params, callback)
})
}).send()
}
} }
@@ -34,6 +34,8 @@ export default {
languages: params.languageType, languages: params.languageType,
name: params.voiceName, name: params.voiceName,
remark: params.remark, remark: params.remark,
referenceAudio: params.referenceAudio,
referenceText: params.referenceText,
sort: params.sort, sort: params.sort,
ttsModelId: params.ttsModelId, ttsModelId: params.ttsModelId,
ttsVoice: params.voiceCode, ttsVoice: params.voiceCode,
@@ -75,6 +77,8 @@ export default {
languages: params.languageType, languages: params.languageType,
name: params.voiceName, name: params.voiceName,
remark: params.remark, remark: params.remark,
referenceAudio: params.referenceAudio,
referenceText: params.referenceText,
ttsModelId: params.ttsModelId, ttsModelId: params.ttsModelId,
ttsVoice: params.voiceCode, ttsVoice: params.voiceCode,
voiceDemo: params.voiceDemo || '' voiceDemo: params.voiceDemo || ''
@@ -24,7 +24,7 @@
<img :src="message.chatType === 1 ? getUserAvatar(currentSessionId) : require('@/assets/xiaozhi-logo.png')" <img :src="message.chatType === 1 ? getUserAvatar(currentSessionId) : require('@/assets/xiaozhi-logo.png')"
class="avatar" /> class="avatar" />
<div class="message-content"> <div class="message-content">
{{ message.content }} {{ extractContentFromString(message.content) }}
<i v-if="message.audioId" :class="getAudioIconClass(message)" <i v-if="message.audioId" :class="getAudioIconClass(message)"
@click="playAudio(message)" class="audio-icon"></i> @click="playAudio(message)" class="audio-icon"></i>
</div> </div>
@@ -129,6 +129,32 @@ export default {
} }
}, },
methods: { methods: {
/**
* 从 content 字段中提取聊天内容
* 如果 content 是 JSON 格式(如 {"speaker": "未知说话人", "content": "现在几点了。"}),则提取 content 字段
* 如果 content 是普通字符串,则直接返回
*
* @param {string} content 原始内容
* @returns {string} 提取的聊天内容
*/
extractContentFromString(content) {
if (!content || content.trim() === '') {
return content;
}
// 尝试解析为 JSON
try {
const jsonObj = JSON.parse(content);
if (jsonObj && typeof jsonObj === 'object' && jsonObj.content) {
return jsonObj.content;
}
} catch (e) {
// 如果不是有效的 JSON,直接返回原内容
}
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
return content;
},
resetData() { resetData() {
this.sessions = []; this.sessions = [];
this.messages = []; this.messages = [];

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