Compare commits

..
Author SHA1 Message Date
hrz d4343cf691 字段名里补上申请网址
两个字段名里写上和风天气控制台的地址,填表的人在界面上能直接照着点。文档末尾提到内部改动编号的两句改成大白话,读文档的人不用知道内部单号。
2026-10-24 07:52:24 +00:00
hrz 7a711564f1 写清楚为什么标 runOnChange
两条清理脚本在这个 PR 里改过内容,跑过旧版的库启动时靠 runOnChange 直接重跑新版。补一句说明在脚本头上,免得以后有人当成手滑删掉。
2026-10-24 07:28:06 +00:00
hrz 1df75f6882 文档写清哪些凭据会被清掉
补上清理的范围、没清掉的那一类情况和管理员自己改的办法,写明要等另外两个改动先落地。
2026-10-24 07:14:36 +00:00
hrz ba5e074bce 清理天气凭据时避开管理员自己填的值
之前那版脚本把当前默认值当成内置值,管理员改过的也会被清掉。现在先看这一行有没有被人编辑过,没动过才拿它的默认值当基准去清智能体参数,动过的一律不碰。内容改了会自动重跑,跑过旧版的库不会因为校验不一致起不来。
2026-10-24 07:14:36 +00:00
hrz bb392930b8 文档说明智控台那两栏为什么是空的
智控台这边插件参数的 API 密钥和 API Host 两栏默认是空的,数据库变更脚本已经把内置值清掉了,得照着本文件里的申请指南自己申请一套再填。之前只说默认是空的,没说清是谁清的。
2026-10-24 06:06:44 +00:00
hrz cdb4c34c4c 系统参数里的天气凭据也顺手置空
那两行后来已经被删掉了,新装的库里没有。这里再置一次空只是兜底,正常不影响任何一行。
2026-10-24 01:14:18 +00:00
hrz 36e34994ba 天气插件的凭据改成自己填,不再预置
模型提供方里内置的和风天气密钥和主机地址清空,字段名后面写清去哪儿申请。已有智能体参数里还等于内置值的那些也清掉,用户自己填的不动。

新增一条变更记录,没有改动以前那些,校验和不受影响。
2026-10-24 01:12:36 +00:00
hrz 9bd668aebc Merge pull request #3449 from TXyy2023/codex/issue-3050-preserve-model-type
fix(manager-api): 保存模型时保留供应器登记的类型大小写
2026-10-23 22:17:36 +08:00
hrz d027432432 Merge pull request #3458 from monsterlag/fix/issue-3064-b736
fix(manager-web): 修复短屏下模型配置知识库入口被裁剪
2026-10-23 21:53:42 +08:00
hrz 3972df6e4e 加固模型配置导航的契约测试
原来的用例只取第一条样式规则,后面谁再补一条同名规则就能把滚动改回去,测试照样是绿的。这次把导航用到的样式选择器逐条列出来核对,外层容器也要求只能有一条规则,这样有人悄悄覆盖时测试会当场报错。
2026-10-23 13:39:19 +00:00
hrz 8f2dda09cf Bump to 0.9.8 (#3469) 2026-10-23 18:54:05 +08:00
hrz d4ff9c5c17 修掉通讯录的两处越权(读 + 写),并补上三处空指针 (#3460)
* 读取设备通讯录先验设备归属

登录用户传任意 MAC 就能读到别人设备通讯录里的备注和称呼。归属判断收进一个方法,读取通讯录、改别名、改权限都走它,不属于当前用户的设备一律按查不到处理。

* 通讯录存别名不再空指针

对方设备没登记时取备注会直接崩,整条权限授予请求跟着挂掉。取不到就退回尾号称呼,呼叫时主叫没登记也不再崩,顺手删掉一行调试打印。

* 解绑先验归属,通讯录别名不再落空

解绑接口原来不核对设备归属,谁知道对方的设备 id 就能把那台设备的通讯录清空,设备本身倒是因为带了用户条件没删掉。现在先看归属,不是自己的直接不动。

设备登记了但用户没起名时,别名会落成空,缓存刷新把整条跳过,对方再也呼不过来。改成和呼叫那处一样,两级判空都取不到才退回尾号称呼。顺带加了一条回填,把写进去的空值补上。

呼叫那条测试原来会真发请求也没验到东西,改成桩掉网关直接看发出去的称呼。

* 通讯录空别名兜底,撞名加后缀

缓存拼表之前,空别名先用尾号称呼顶上,不再整条跳过,用户没起名的设备也能被呼到。

昵称在同一个人的通讯录里是索引,两台撞名时后写的会盖掉先写的,按昵称呼会呼错设备。加后缀区分,规则和存表时那套一致。取名靠先后,而查询不带排序,所以先按地址排一遍,同样的数据每次刷新结果一样。

数据库没有排序保证,去重全靠顺序,所以这里先排再拼。迁移脚本不用动,运行时的兜底能管住它没覆盖到的行。

* 通讯录取名两条路径对上了

后缀原来只写进缓存,接口直查库返回原值,同一份数据给的是两个名字。
用户照控制台看到的名字呼叫,命中的不是同一台设备。

把取名抽成一个方法,两边都走一遍。顺带把权限位排到地址前面,
撞名时能呼的那台先拿原名,反过来用户怎么点都只会说没权限。

迁移取末两位时改成跟着小写化,和运行期算出的是同一个称呼。

* 同一个人被拆成两本通讯录,一台设备消失

两条记录的归属地址只差大小写时,会被算成两个人,各拿一个同名,
写进同一本时后者盖掉前者,一台设备就从缓存里消失了。

把归属地址归一抽成一个方法,算称呼、写缓存、查缓存都走它。

* 通讯录接口按最近改过的排前,这个约定有用例钉住

智控台页面是按对方地址把备注合进去的,接口返回顺序它并不关心,
所以现在这么排对界面没有效果。

用例留着是防它被当无用代码删掉,删了会有用例报出来。

* 迁移串到别人的私名,撞车的组改走尾号称呼

通讯录回填名字时,设备表里同一个 MAC 有两行(同一台被两个用户各登记一次),
MySQL 自己挑一行填进去,跟这行属于谁无关。改成只有一台时才取智控台名字,
多台就退回按 MAC 末两位算的称呼。名字照样填上,缓存照样刷新,对方还是呼得到。

补了 12 条在真库上跑的用例,另配一条排查重复 MAC 的语句。

* 去掉走不到的分支和没人用的桩,补上末尾换行

getUser() 取不到 subject 或 principal 时返回的是空对象,不是 null,
user == null 走不到,删掉;用例改成桩空对象,测真正的匿名那条路。
归属判断挪进 service 之后,controller 层四处 SecurityUser 的桩没人用了,删掉。
四个文件末尾少了换行,补上。另加一条用例钉住判重分组的比较规则:
只折叠大小写,不折叠分隔符。

* 移除回填通讯录别名的迁移,运行时已经兜住了

这个迁移是本 PR 带进来的。它要把空别名刷成称呼,可运行时的兜底已经做了一样的事,
而且覆盖得更宽:那边连纯空格、地址为空和不足两位的都管。删掉它,功能不受影响。

删它有两条理由。一是重复。二是它按 MAC 去设备表取名字,
而一个 MAC 在设备表里可能有多行——同一台设备被两个用户各登记一次就能造出来,
这时候取到的是别人给自己设备起的名字。写在这里,是怕后来的人以为这个坑不存在。

* 归属校验撞到重复 MAC 不再报错

库里同一 MAC 可能有两行只差大小写,selectOne 会抛 TooManyResultsException,把读通讯录接口整个带崩。改成 selectList 按 id 取第一条做归属校验,撞行时正常返回,不再报错。
2026-10-23 17:43:05 +08:00
wengzh 3709540efd Merge pull request #3445 from xinnan-tech/fix/huoshan-double-stream-4-bugs-y15
huoshan_double_stream 修 4 个会话协议与监听竞态 bug
2026-10-23 17:37:05 +08:00
wengzh 333050ae2e Merge pull request #3464 from xinnan-tech/chore/bump-deps-20261023
chore(deps): 批量升级 15 个依赖,适配 mcp 2.x
2026-10-23 17:05:05 +08:00
Sakura-RanChen 95ce49ffc9 删除 benchmark/ 下 3438~3441 回归验证脚本
四处修复均已验证收敛,脚本不进 CI、裸跑依赖 manager-api,保留仅为仓库噪声;后续若再动 huoshan_double_stream 时以 pytest 覆盖
2026-10-23 17:04:52 +08:00
Sakura-RanChen be75e7bd39 chore(deps): 批量升级 15 个依赖,适配 mcp 2.x
源 PR: #3426 #3427 #3428 #3429

剔除 4 项:
- edge-tts 7.3.1->7.2.8 是降级
- powermem >=1.1.7 要求 Python>=3.11
- openai 3.29.0 与 powermem 冲突(powermem 要求 openai<3.0.0)
- ruamel.yaml 0.19.1 与 hyperpyyaml 冲突

mcp 2.x 适配: streamablehttp_client 更名为 streamable_http_client,headers/timeout
改由 http_client 承载,产出从 3 元组变 2 元组,ProgressFnT 移到 mcp.shared.dispatcher
2026-10-23 15:15:50 +08:00
hrz 24fbf7eee2 登录 token 12h 静默失效补 silent refresh 接口 #3392 (#3402)
* 登录 token 12h 静默失效补 silent refresh 接口 #3392

方案 A:后端 POST /xiaozhi/user/refresh(anon),旧 token 校验通过后强制覆盖为新 token 字符串,旧 token 在 DB 查不到即失效。前端 httpRequest 在 sendRequest 入口检查 token 剩余有效期,剩 1h 内触发 silent refresh(独立 fly 实例 + 单飞锁),成功后 setToken 替换并自动继续发送当前请求,失败走 clearAuth 跳登录。

* refreshToken 改 CAS 防并发 race + 拆前端副作用 #3392

P1:SysUserTokenDao 新增 refreshTokenCAS,条件 UPDATE(oldToken + 未过期 + userId),受影响 0 行抛 TOKEN_INVALID;refreshToken 改调它,避免读-改-写 race。P2:refreshToken 加 @Transactional;httpRequest.getTokenExpireAt 内的 localStorage.setItem 挪到独立 persistTokenExpireAt,由 shouldRefresh 入口调用。

* getTokenExpireAt 改读 localStorage 与持久化同源 #3392

pr01 三改:原代码 getTokenExpireAt 读 store(state.token 缓存),与 persistTokenExpireAt 写 localStorage 不同源,导致 _localStoredAt 缺失 → NaN → shouldRefresh 永 false → silent refresh 永不触发。修复:getTokenExpireAt 改读 localStorage,persistTokenExpireAt 同步刷 store,保证两端一致。e2e 已验证:注入 expire=1800 token → reload → role-config mount 触发 sendRequest → Network 抓到 POST /user/refresh 200 → localStorage token 被替换为新 12h token(含 _localStoredAt)。

* 应用启动同步 localStorage token 到 store #3392

PR #3402 扩展:守卫放行后首次 API 请求时,vuex state.token 还是 null,httpRequest 读 store 取不到 Authorization 头,会被后端 401 → clearAuth 跳登录。在 main.js 应用启动阶段就把 localStorage 里的 token commit 进 store,避免 race。不动 refresh 链路与其它业务文件。
2026-10-23 09:47:41 +08:00
monsterlag 855a64cbb9 fix(manager-web): keep model navigation scrollable on short screens 2026-10-22 04:06:52 +00:00
hrzandSakura-RanChen fdf280f84a 模板 GET 入口迁回 AgentTemplateController #3395 (#3401)
* 模板 GET 入口迁回 AgentTemplateController #3395

根因:AgentController 已存在 GET /agent/template/templateList,AgentTemplateController 缺显式 GET,于是路由虽通却跨 controller 暴露模板数据,且职责错位。修复:删 AgentController.templateList,同名 @GetMapping(无 path)落到 AgentTemplateController,权限 normal、orderByAsc sort、复用现有 service.list。验证:spring-boot 启动期无 Ambiguous;/agent/template、/page、/{id} 三路由均被 Shiro 一致拦截,权限/路径正确。

* 补 P2 注释说明 normal 权限反直觉是有意保留 #3395

在 getAgentTemplates 上方加 2 行注释,说明单独 normal 权限是为保持原行为契约(普通用户也能查可用模板),与类级 superAdmin 故意分离;非疏忽。无逻辑变更。

* 去除未使用引用

---------

Co-authored-by: Sakura-RanChen <1908198662@qq.com>
2026-10-22 11:42:48 +08:00
wengzh 498191519c Merge pull request #3452 from xinnan-tech/py-test-wakeup
Py test wakeup
2026-10-22 11:08:38 +08:00
wengzh f614489d81 Merge pull request #3454 from xinnan-tech/web-test-json
Web test json
2026-10-20 15:44:16 +08:00
Sakura-RanChen 6f958ee4a6 fix: 新增模型页面dict字段错误解析 2026-10-20 15:34:40 +08:00
TXyy2023 b226ee5430 fix(manager-web): decode stored dictionaries before editing (#3450) 2026-10-20 11:53:02 +08:00
Sakura-RanChen 9c30b5e035 文件直接写入wav,无需经过二次编解码 2026-10-20 10:45:31 +08:00
TXyy2023 d531d04281 fix(tts): handle retained audio files in wakeup cache (#3451) 2026-10-20 10:00:20 +08:00
Sakura-RanChen d8f8a4a083 - 取消屏障保证:新连接创建前 _cancel_monitor_task 已等旧监听任务完全退出,
旧任务不可能读新连接或误关新连接(check_huoshan_3440 动态断言覆盖)
- mismatch 分支仅保留纯跳过,删除旧会话结束事件的复位遗留,
  activate_session 写入点收敛到会话生命周期(start / matched 结束 / close)
- 删除 _session_start_future 启动确认机制:start_session 发送即返回,
  SessionFailed 由 monitor 记 error 并复位 activate_session,无挂起风险
- 回归脚本同步调整:3438 重写为新行为断言,3440 用屏障断言替换 snapshot 断言
2026-10-19 17:52:25 +08:00
TXyy2023 37d4330224 test(model): verify registered types with MySQL and Redis 2026-10-18 23:13:39 +08:00
TXyy2023 7873984f9f fix(manager-api): preserve registered model type casing 2026-10-18 17:10:47 +08:00
hrz 6840210ea8 check_huoshan_3439 改 FIRST 分支行为断言
原 test 用正则 grep 源码验存在性,pr02 复评指出这样测不到实际调用。
改成起后台线程跑真 tts_text_priority_thread、构造 FIRST 消息入队、
run_coroutine_threadsafe patch 成立即完成的 future,最后断言
reset_stream_state 与 opus_encoder.reset_state 各被调一次。
2026-10-17 07:57:30 +08:00
hrz 0a54124d0d huoshan_double_stream 修 3 处复评反馈
init 漏写 _pending_prefix 字段、start_session 用已弃用的
get_event_loop、finish_session 留着多余的 getattr 兜底。补字段、
换 get_running_loop、去兜底,3 处都修了。
2026-10-17 07:57:26 +08:00
hrz ddc441bc59 补 huoshan_double_stream 4 bug 复现脚本
四个独立脚本各覆盖一个 issue:start_session 等 future 确认、句尾残留与编码器
冲刷、monitor snapshot 隔离、parser_response 四处协议解析。共享 _huoshan_mock
构造一个绕开父类 init 的假 provider 实例。
2026-10-17 07:39:51 +08:00
hrz 9b7fb3e826 parser_response 四处二进制协议解析修正
serialization_method 错用掩码当移位数恒 0,改成 >>4;message_type 写
== FULL or AUDIO_ONLY_RESPONSE 优先级下恒真,改 in (...);offset 硬编码 4 在
header_size>1 全错位,改成 header_size * 4;SessionCanceled 漏进协议分支被
else 误读,补进统一解析。
2026-10-17 07:39:51 +08:00
hrz a6cdbf9410 监听任务用 snapshot 隔离跨连接
旧 monitor 循环直接 self.ws.recv(),连接重建后旧任务读新下行;finally 无条件
关 self.ws 误关新连接。改成入口绑 monitor_ws / monitor_task,recv 用 snapshot;
旧 session 事件 continue 不再 reset 当前;finally 判 id 相同才关 ws / 清引用。
2026-10-17 07:39:51 +08:00
hrz 75fd5d8cc3 句尾替换词残留先发 + 会话结束冲刷编码器
旧 finish_session 不发 _pending_prefix 残留,句尾替换词被扣时整段丢;monitor / to_tts
收 SessionFinished 不调 is_end=True 冲刷 opus 编码器,每句固定丢尾。改成
finish_session 开头先发残留,monitor / to_tts 调 is_end=True 冲刷,FIRST 同步重置。
2026-10-17 07:39:51 +08:00
hrz 6fe5630346 TTS 会话等 SessionStarted 确认
旧逻辑 start_session 发完 StartSession 即返回;服务端鉴权失败或 14s 内 26005 时
文本线程继续给死会话发文本,整轮无声。改成末尾 await 一个 future:monitor 读到
SessionStarted set True、读到 SessionFailed set False,超时报错让文本线程感知。
2026-10-17 07:39:50 +08:00
hrz eb9dc8ca0f 把 pydub 探针绑到当前解释器同目录的完整版 ffmpeg (#3444)
edge_tts 升 7.3.1;edge.py 撤 WSS_URL patch 改默认
(4.36 跑默认 URL 稳定)。util.py 加 PATH 临时注入,
绕开 Mac 上 Trae CN 注入的精简版 ffmpeg/ffprobe,
让 pydub 拿 conda env 完整版。引用计数加锁防多线
程串。
2026-10-15 15:43:39 +08:00
wengzh 27399a2fb6 Merge pull request #3430 from xinnan-tech/py-text-test
Py text test
2026-10-14 14:11:53 +08:00
wengzh 7c43ecfcb5 Merge pull request #3434 from xinnan-tech/py-test-asr
fix: SenseVoice 日语识别结果按词带空格 (#3412)
2026-10-14 11:54:39 +08:00
korexil 0be634c627 fix: SenseVoice 日语识别结果按词带空格 (#3412)
SenseVoice 识别日语时偶尔按词输出空格(如「今日 の こと を 少し 長め に」),
这些空格会原样进入对话历史并交给 LLM。lang_tag_filter 现在去掉两个
中日文字符之间的空格,韩文、英文的词间空格保持不变。
2026-10-14 10:32:56 +08:00
Sakura-RanChen 932962e8d3 补充注释说明,调整测试样例 2026-10-14 10:10:28 +08:00
wengzh 60c2a7ee17 Merge pull request #3431 from xinnan-tech/py-content-text
Py content text
2026-10-13 17:13:42 +08:00
Sakura-RanChen a01f0aa29b 去除没必要的测试,上游为str不需要防御性增加 2026-10-13 16:54:42 +08:00
korexil 4445aa7510 fix: FunASR 识别结果以 JSON 原样进入对话历史 (#3411)
FunASR 返回 {"content","language","emotion"} 时,startToChat 只在含 speaker
字段时才解包,否则整段 JSON 字符串会作为用户消息交给 LLM 并写入对话历史。
现在没有 speaker 时只取 content。
2026-10-13 16:44:13 +08:00
wengzh dc86a780a2 Merge pull request #3418 from xinnan-tech/py-test
Py test
2026-10-13 15:47:30 +08:00
Sakura-RanChen a5e47f2c4e 补充字符,调整字符判断 2026-10-13 15:29:28 +08:00
Sakura-RanChen 2057b99aeb 迁移测试用例,优化相关注释 2026-10-13 14:31:21 +08:00
wengzh 7957817a72 Merge pull request #3425 from xinnan-tech/chore/dependabot-monthly-groups
Dependabot 改月度更新并按用途分组
2026-10-13 11:41:15 +08:00
hrz a6eeb107f4 fix(deps): 补 dependabot ignore portalocker
requirements.txt 把 portalocker 锁在 3.x,4.x 与 qdrant-client<1.20 的
portalocker<4.0 冲突 (PR #3360)。Dependabot 默认推 4.x 升级会破约束。
依 openrz 评审 P1 在 ignore 列表追加 portalocker 一项。
2026-10-13 11:28:37 +08:00
wengzh 847337cabb Merge pull request #3419 from xinnan-tech/fix/3391-route-guard-whiteList
fix(router): 路由守卫改 anon 白名单反向匹配
2026-10-13 11:15:15 +08:00
Sakura-RanChen b8f23ffa66 调整相关代码 2026-10-13 10:40:47 +08:00
lww9029 dc581d7185 fix: 修复登录状态下还能访问白名单路由问题 2026-10-13 10:36:25 +08:00
hrz 741ec299c0 fix(tts): edge-tts 失声改走 msedgeservices (#3420)
* fix(tts): edge-tts 失声改走 msedgeservices

微软 2026-10 起对 legacy 消费者接口返回 403,
edge-tts 7.2.8 仍走 speech.platform.bing.com 走不通。
上游 PR rany2/edge-tts#491 切到 api.msedgeservices.com 修好,
但 7.2.8 仍是 PyPI 最新版本。

仓库在 edge.py 顶部临时把 WSS_URL 顶住,等 7.2.9 出了删掉。

* fix(tts): patch 还要写 communicate.WSS_URL

edge-tts 7.2.8 仍指 legacy bing 接口,10 月起微软 403。
上游 PR #491 已切到 msedgeservices.com,7.2.9 才进 PyPI,先顶住。
上次只改 constants.WSS_URL 不够,communicate 用 from import 是模块局部名,
LEGB 找不到新值。这次同时改 communicate.WSS_URL 才生效。
2026-10-13 09:58:30 +08:00
hrz 505925fb21 Dependabot 改月度更新并按用途分组
原本每周跑一次会刷很多零碎 PR,看着烦。改成每月一次,并把第三方 AI 服务、本地模型、网络栈拆成几组聚一起,每组出一个 PR 就行。被锁住的 torch/numpy/websockets 几个直接忽略掉,免得它推升级 PR 进来。
2026-10-13 09:34:02 +08:00
hrz 4aa63baf75 fix(router): 路由守卫改 anon 白名单反向匹配
业务路由越加越多,原来硬编码的 8 项守卫名单跟不上了,
未登录直接进十来个业务页看到骨架再被踢,体感抖。

守卫改成只对登录、注册、找回密码、根路径这四页放行,其余一律查 token。
顺手把 9 处没人读的 meta.requiresAuth 和 meta.title 死字段删了。
2026-10-12 18:02:36 +08:00
korexil 70ab19d23f fix: TTS 分句在引号内切开句子 (#3413)
_get_segment_text 会在引号内的标点处切分(如「わぁ!」って、“好!”然后),
把引号拆到两段里,或留下只有「。」的片段。新增 _quote_safe_cut:切分点后的
右引号与句末标点一并带走;位于未闭合引号内或右引号后句子仍在继续时,顺延到
下一个标点。不含引号的文本切分结果不变。
2026-10-12 16:26:40 +08:00
Sakura-RanChen e5f326285e update: 优化client 生命周期对齐业务请求——重试内连接复用、锁外构造、safe_close 可打断重试 2026-10-12 15:42:51 +08:00
hrz 4576e391bb fix(ci): 阿里云 PyPI 镜像 metadata 缺失回退官方源 (#3417)
基镜像构建挂了——日志显示 pip 在装 requirements.txt 阶段退出码 1,
罪魁是阿里云 pypi 镜像对 mcp==1.22.0 的元数据文件 404。

装包那行加 --extra-index-url https://pypi.org/simple,
主源没货时自动去官方源捞。
2026-10-12 10:28:35 +08:00
Yanhan Ruan ef1d24e40d fix(server): 修复临时事件循环结束后 HTTP 客户端未释放的问题 (#3416) 2026-10-12 10:00:28 +08:00
wengzh c30c40bf56 Merge pull request #3415 from xinnan-tech/chore/bump-deps-20261010065106-1659
chore(deps): 批量升级 4 个允许的依赖
2026-10-10 15:24:17 +08:00
Sakura-RanChen 199c4ce365 chore(deps): batch bump 允许升级的依赖 2026-10-10 14:53:30 +08:00
hrz a6cd755cf2 聊天记录弹框底部不再留大量空白 (#3410)
弹框本身和 body 用 !important 撑到视口(body 顶开 size="large" 默认多扣的 56px footer 高度)。body 的高度由 flex: 1 自动撑满,max-height 只用来顶开原来的 950px 上限。
2026-10-09 17:24:39 +08:00
hrz 23d0e64d9d 召回测试弹框与切片弹框样式对齐 (#3409)
召回测试弹框宽度从 900px 调到 1200px 并启用大弹框样式,高度、上下间距与切片弹框保持一致。结果区域改用 flex 填充而不是写死的 400px 高度。
2026-10-09 17:18:06 +08:00
hrz 96c224c4e4 切片和聊天记录弹框统一为大弹框样式 (#3407)
知识库查看切片弹框加 size="large",聊天记录弹框去掉旧样式覆盖并把 body 改成 flex 列布局,底部下载按钮不再绝对定位,两个弹框跟历史版本对齐。
2026-10-09 17:02:49 +08:00
hrz 2b6483662a 召回测试弹框回车不再刷新页面 (#3408)
给 el-form 加阻止默认提交的处理,按回车时不再触发浏览器刷新。
2026-10-09 17:01:52 +08:00
hrz aef973e6b9 7 处 el-select 加 filterable 支持输入筛选 #3405 (#3406)
主诉:手动添加设备下拉无搜索(FIRMWARE_TYPE 字典 54 项)。统一修同类位置:替换词多选加 collapse-tags;登录/注册/找回密码的区号下拉加 filterable(海外 200+ 项)。无新依赖、纯 element-ui 原生属性。
2026-10-08 15:39:46 +08:00
hrz 7a260ee143 弹框关掉后再点不开以及宽度高度统一收口 (#3388)
* 统一管理后台弹框更舒展

把 CustomDialog 默认宽度从 600 加到 720,正文给到至少 360 的高度,
默认弹框就不再显得局促。参数编辑弹框也跟着加宽。智能体历史版
本那条列表、对比和恢复预览都给到至少 420 的最小高度,避免内容
少时框太矮看起来小气。其他像添加智能体、绑定设备这些原本就合适
的弹框没动。

* 弹框外观统一收口

原本几个弹框长得参差不齐:标题字号、按钮文案、留白高度各管各的。这次把新建智能体、新建设备、改模型、改参数这几个弹框接到同一个底座上,按钮统一成「确定」「取消」,输入框字号高度对齐,遮罩背景加了模糊,弹框太高就内部滚动,四个弹框看起来一个模子

* 修复弹框关掉后再点不开

之前几个弹框改用新底座的时候,关闭那一路少了一段转发:父页面用双向绑定来收弹框关闭的信号,子页面应该再把关闭事件抛上去告诉父页面。这一段被删了,下次再点添加按钮时父页面那边状态没复位,弹框就拉不开来。现在把这段补回去。

* 弹框宽度和高度统一收口

同一批弹框的尺寸还是各管各的:模型配置里改参数那个是跟着窗口大小按比例伸缩,历史提示词的源编辑弹框是 900 像素,参数管理里又是 720 像素,智能体变更详情那个上下还留了一大块空白。这次收到同一个共用底子上:宽度都对齐到 720 像素,弹框默认上下贴齐屏幕边、内容多时中间滚动,变更详情那个高度撑到接近屏幕顶到底。

* 管理后台弹框按大小分类统一外观

管理后台里的弹框按大小分成两类来收口。占整页的大弹框(历史版本、变更详情、模型修改、模型参数修正等)撑满整个浏览器高度,下面不再留白;小弹框(添加智能体、添加设备、知识库、字典、角色模板等)改成对齐屏幕正中,不再顶在屏幕上沿。修改模型那条弹框也加宽到 960,左右留白加大到 40,跟历史版本对齐,看起来更舒展。

* 大弹框上下留白收口到十像素并按三档统一宽度

大弹框上下各留十像素让边框和圆角清晰可见。宽度按用途分三档:表单 960、含代码 1080、表格 80% 加宽度上限。供应器、上下文供应器、音色克隆都改 960;TTS 从 90% 改 80%;角色模板从 1200 改 1080;缓存查看从 70% 改 80%。新增模板的大文本框改成自适应高度。

* 弹框样式与文案收口处理

AddDeviceDialog 确认后关闭弹框;CustomDialog 去掉 :global 与紧邻选择器,合并 el-dialog__body 样式块;删除未使用的 --dialog-form/--dialog-code;AddWisdomBodyDialog 的 cancel/handleClose 委托到同一个实现;多语言文案同步去冒号与双点。

* 弹框遮罩样式移到全局作用域

scoped 块里的 ::v-deep .v-modal 打不到 body 层的遮罩,改在 global.scss 用全局选择器。

* 弹框 wrapper 居中样式移到全局作用域

scoped 块里的 ::v-deep .el-dialog__wrapper 同样打不到 element-ui 自渲染的 wrapper,跟 .v-modal 一起挪到 global.scss。

* 弹框提交状态异常收口

设备激活码错误路径不再卡 loading;Provider 与固件弹框补 loading 兜底。

* 弹框视觉异常收口

首页删除确认弹框 wrapper 居中;历史版本弹框去掉内层多余滚动条。

* 设备 API 链式返回修复

bindDevice 等方法补上 return,让 .fail() 兜底回调能正常注册到请求对象上。

* 设备 API 错误回调参数补全

device.js 六个方法加 failCallback 参数;user.js login 网络重试补 failCallback;AddDeviceDialog 改四参数签名。

* 设备状态 API 失败回调引用修复

getDeviceStatus 的 .fail() 块把函数体内 callback(res) 改成与签名一致的 if (failCallback) failCallback(err),避免业务错误时抛 ReferenceError。
2026-10-07 08:03:25 +08:00
欣南科技 87c6df77b0 Merge pull request #3386 from xinnan-tech/0.9.7
Bump to 0.9.7
2026-09-29 10:03:47 +08:00
hrz eb51f420a2 Bump to 0.9.7 2026-09-29 09:13:14 +08:00
DaGou12138 1eefb4ea86 Merge pull request #3385 from xinnan-tech/serpapi
Serpapi
2026-09-28 10:25:33 +08:00
hrz 1211f8202b Merge pull request #3376 from googio/feat/serply-search
feat(web_search): 新增Serply搜索源
2026-09-28 09:36:42 +08:00
wengzh 788f5301fd Merge pull request #3378 from xinnan-tech/fix-deps
fix(deps): 移除 chardet 直接依赖
2026-09-21 09:38:29 +08:00
Sakura-RanChen edad16aedf fix(deps): 移除 chardet 直接依赖 2026-09-20 15:55:21 +08:00
serply 65a3fae9fc feat(web_search): 新增Serply搜索源
联网搜索插件在秘塔和Tavily之外增加Serply,返回谷歌搜索结果。
通过 provider: "serply" 选用,未配置时行为不变。
2026-09-17 16:45:28 -04:00
wengzh f4ba65f224 Merge pull request #3375 from xinnan-tech/fix-snapshot-dialog-diff-style
fix: 修复 v-code-diff 斜体样式干扰 diff 展示
2026-09-17 17:54:02 +08:00
lww9029 87c2bae55a fix: 修复 v-code-diff 斜体样式干扰 diff 展示 2026-09-17 17:50:27 +08:00
wengzh 628d4cedb7 Merge pull request #3373 from xinnan-tech/feat-snapshot-dialog-code-diff
feat: 历史版本快照增加代码对比视图
2026-09-17 17:06:59 +08:00
Sakura-RanChenandSakura-RanChen 6c3f89cae1 chore(deps): 批量升级 4 个允许的依赖 (#3374)
* chore(deps): batch bump 允许升级的依赖

* test: 添加 5 条核心链路测试 + 默认 mock 模式

参照 PR #3370 的整体结构,新增 STT/LLM/TTS/RAG/Memory 五个 test_*.py
覆盖关键链路。conftest.py 默认 mock 模式:

- PYTEST_CONFIG_FILE 指向单模块配置文件(不入库)
- RUN_LIVE_API_TESTS=1 走真请求(必须配齐密钥)
- 真实存在的第三方包(openai/httpx)保留,仅 patch httpx 网络层
- 装了一半会炸的本地模型(torch/funasr/modelscope)强制 shim
- mock load_config / init_service,避免触发远程 manager-api

mock 模式目标:dep-upgrade orchestrator / CI 不需要任何密钥即可跑通。

---------

Co-authored-by: Sakura-RanChen <Sakura-RanChen@local>
2026-09-17 15:57:43 +08:00
lww9029 4745186cea style: 调整窄屏样式 2026-09-17 10:43:26 +08:00
lww9029 1ddc97b626 feat: 快照对话框增加代码对比视图 2026-09-17 10:04:15 +08:00
wengzh 6afc54a17d Merge pull request #3366 from xinnan-tech/feat-tts-alibl
Feat tts alibl
2026-09-11 17:35:08 +08:00
wengzh daccfeb9dd Merge pull request #3364 from xinnan-tech/fix-memory-title
fix: 记忆模式为nomem时错误生成标题
2026-09-11 17:34:43 +08:00
Sakura-RanChen 1846d3e935 调整相关逻辑 2026-09-09 16:17:13 +08:00
Sakura-RanChen b1a7607dd5 Merge pull request #3345 from apexlsai/feature/alibl-workspace-streaming
feat(tts): 完善阿里百炼 Workspace 流式语音合成
2026-09-09 14:22:59 +08:00
Sakura-RanChen d6d2da3d76 fix: 记忆模式为nomem时错误生成标题 2026-09-09 10:33:37 +08:00
CGD 5aa46538d5 Merge pull request #3358 from xinnan-tech/fix-audio-delay
fix: 修复固定延迟不生效,调整相关逻辑
2026-09-08 10:53:25 +08:00
Sakura-RanChen ed98be5347 Merge pull request #3357 from xinnan-tech/py-test-client-toctou
根治ManageApiClient单例TOCTOU竞态,锁保护+统一防护
2026-09-08 10:00:20 +08:00
Sakura-RanChen 79a95af399 fix: 修复固定延迟不生效,调整相关逻辑 2026-09-07 11:38:06 +08:00
3030332422 32cba36237 fix: 修复 ManageApiClient 关闭期间的连接池创建竞态 2026-09-07 11:14:50 +08:00
CGD 82e3c1874f Merge pull request #3353 from muxinshijie/feature/manage-api-client-toctou
fix: 根治ManageApiClient单例TOCTOU竞态,锁保护+统一防护 #3339
2026-09-07 10:31:58 +08:00
欣南科技 c478257517 Merge pull request #3356 from xinnan-tech/fix/docs-typos
docs: 批量修正文档里的错别字和拼写错误
2026-09-06 08:44:01 +08:00
欣南科技 9fdb72cfd7 Merge pull request #3355 from xinnan-tech/fix/3354-建议修改docsmcpvisionintegrationmd
docs: 修正视觉模型配置文档里的错字
2026-09-06 08:43:38 +08:00
hrz 3c31315ecc docs: 批量修正文档里的错别字和拼写错误
扫了仓库里的文档,挑出明显写错的字和拼错的英文,统一改了一遍。包括「Anaconda Prompt」漏字、「火上」→「火山」、「设别」→「设备」、「tababr」→「tabbar」、「aotodl」→「autodl」、「mananger-web」→「manager-web」等十几处。仅文档修改,不影响代码。
2026-09-06 08:37:51 +08:00
hrz 07c6cdf16a docs: 修正视觉模型配置文档里的错字
按 issue #3354,把"视觉打语言模型"改成"视觉大语言模型",跟上下文一致。
2026-09-06 08:34:57 +08:00
hrz 8a3c2c51a0 Merge pull request #3351 from taiwan-dog/pm/disable-default-websocket-keepalive
fix: disable library WebSocket keepalive for device server
2026-09-05 00:37:23 +08:00
hrz 046fdc8aa4 Merge pull request #3350 from taiwan-dog/pm/hello-before-mcp-initialize
fix: send server hello before MCP initialize
2026-09-05 00:26:15 +08:00
muxinshijie 61e4879826 fix: 根治ManageApiClient单例TOCTOU竞态,锁保护+统一防护 #3339 2026-09-05 00:14:09 +08:00
yillkid eda3c23d51 fix: disable library WebSocket keepalive for device server
websockets.serve() defaults to ping_interval=20 and ping_timeout=20.
ESP32 clients that do not answer those pings are dropped with
1011 keepalive ping timeout roughly 40 seconds into a healthy session.
The application-level ping is configured separately via
enable_websocket_ping.

Refs #3348
2026-09-04 19:00:24 +08:00
yillkid a03a0a73db fix: send server hello before MCP initialize
The MCP initialize task was scheduled before the server hello was sent,
so the MCP frame could reach the device first. ESP32 firmware waits for
the server hello before it treats the channel as ready, so the device
never answered the initialize request.

Refs #3347
2026-09-04 19:00:24 +08:00
欣南科技 27e780e4c1 Merge pull request #3342 from xinnan-tech/test-infrastructure
test: 新增三语测试基建(Python/Java/Vue)+ CI
2026-09-04 13:54:31 +08:00
hrz 4b1ccce575 Merge pull request #3346 from taiwan-dog/pm/fix-funasr-result-type
fix: normalize FunASR result type
2026-09-04 13:40:00 +08:00
yillkid 323c2248b4 [pm] fix: normalize FunASR result type 2026-09-04 13:10:58 +08:00
Jacob a31ba7ef6d fix(tts): synchronize Bailian streaming subtitles 2026-09-04 12:05:34 +08:00
Jacob 935184b1d0 feat(tts): support Bailian workspace endpoints 2026-09-04 12:05:09 +08:00
hrz c7b126ca2f Makefile test-web 同时跑契约与 vitest 2026-09-02 22:24:36 +08:00
hrz 95954c8dae 加日期格式化工具测试,24 例 2026-09-02 22:24:36 +08:00
hrz 17c0da7f87 加 featureManager 单元测试,7 例 2026-09-02 22:24:36 +08:00
hrz dcb3d3e0b0 加 deviceTime 工具 vitest 测试,22 例 2026-09-02 22:24:36 +08:00
hrz e58de03be2 配 vitest:jsdom 环境与覆盖率 2026-09-02 22:24:36 +08:00
hrz 975bbe2cfd 前端引入 vitest 测试工具链 2026-09-02 22:24:36 +08:00
hrz 88d6d13a62 加敏感数据工具测试,9 例 2026-09-02 22:24:36 +08:00
hrz c88149a5d4 加知识库适配器工厂测试,6 例 2026-09-02 22:24:36 +08:00
hrz 6a3aaf475e 加纠错词文件服务单元测试,9 例 2026-09-02 22:24:35 +08:00
hrz caab6a145d 加插件包导入冒烟测试,1 例 2026-09-02 22:24:35 +08:00
hrz 7ac454a1b9 加意图/模型/记忆工厂测试,6 例 2026-09-02 22:24:35 +08:00
hrz ba20bebaeb 加对话消息测试 11 例,含异步 2026-09-02 22:24:35 +08:00
hrz 922adbb290 加配置合并函数测试,7 例 2026-09-02 22:24:35 +08:00
hrz 6eca417534 加 current_time 测试 7 例,冻结时间 2026-09-02 22:24:35 +08:00
hrz 93d54f175a 加 output_counter 测试,7 例 2026-09-02 22:24:35 +08:00
hrz 34ab39ba69 加 textUtils 行为测试,11 例 2026-09-02 22:24:35 +08:00
hrz a0dd138e93 配 pytest 与 conftest,修模块路径 2026-09-02 22:24:35 +08:00
hrz b90dd01826 加 Python 冒烟测试,验证 pytest 流水线 2026-09-02 22:24:35 +08:00
hrz 22b1703557 新增 GitHub Actions,并行跑三语测试 2026-09-02 22:24:35 +08:00
hrz 3363965f63 新增顶层 Makefile,统一测试入口 2026-09-02 22:24:35 +08:00
hrz eb4b28c59e Merge pull request #3336 from muxinshijie/feature/fix-noneType-bug
fix: 修复safe_close多线程竞态导致NoneType空指针
2026-09-02 18:38:10 +08:00
muxinshijie 74cc31d1cc 服务关闭时主线程调用safe_close()将instance置None,与守护线程存在竞态条件,因此必须先判空 2026-08-27 00:13:21 +08:00
hrz c02becdc80 Merge pull request #3332 from MsThink/main
refactor(manager-web): 将备注重命名为 设备别名
2026-08-21 21:39:45 +08:00
colorful b16ed2ed36 style(manager-web): 调整通讯录中的设备状态在线/离线状态图标间距,居中 2026-08-20 10:54:15 +08:00
colorful af9abdd771 refactor(manager-web): 将备注重命名为 设备别名
- 更新所有语言包中的相关文案
- 同步更新组件及变量命名
2026-08-19 15:23:55 +08:00
hrz e1876f1ce1 Merge pull request #3315 from xinnan-tech/fix-model-name
更换豆包、Gemini的LLM模型、修复 performance_tester 异步配置加载错误
2026-08-18 16:31:48 +08:00
hrz ced7103639 Merge branch 'main' into fix-model-name 2026-08-18 16:31:21 +08:00
hrz cb0b075304 Merge pull request #3326 from MsThink/main
fix(知识库管理): 上传弹窗禁止点击遮罩关闭并添加提交加载状态
2026-08-18 16:26:46 +08:00
欣南科技 cdfb2a1187 Merge pull request #3331 from xinnan-tech/openrz-patch-1
Delete docs/images/console-params.png
2026-08-18 16:24:32 +08:00
hrz 8b64205522 Delete docs/images/console-params.png 2026-08-18 16:23:50 +08:00
hrz 6529b80c64 Merge pull request #3329 from Jehuty-ML/feat/params-management-group-ui
feat(manager-web): 参数管理按命名空间聚合展示
2026-08-18 16:10:52 +08:00
weirui d76825fae5 fix(manager-web): 参数管理聚合支持分页拉全量并重置空态全选
按 total 循环拉取全部页,避免超过 500 条时缺数据;displaySections 为空时同步重置 isAllSelected。
2026-08-18 10:19:39 +08:00
weirui c55dc8282f docs: 补充参数管理聚合界面截图,便于 PR 审阅对照 2026-08-17 18:56:27 +08:00
weirui 4870b5a0a2 feat(manager-web): 参数管理按命名空间聚合展示
一次拉取全量参数并按 paramCode 前缀分组折叠,避免默认分页难找相关配置。
2026-08-17 17:56:40 +08:00
wengzh ae3f970c92 Merge pull request #3322 from chentyke/fix/tts-timeout-config-propagation
fix(tts): 统一 TTS 请求超时配置
2026-08-17 17:41:45 +08:00
msgpo 8367f9b607 fix(知识库管理): 上传弹窗禁止点击遮罩关闭并添加提交加载状态 2026-08-15 15:02:04 +08:00
3030332422 59b51e021b update:暂时撤回对gemini模型的更替 2026-08-14 20:08:51 +08:00
3030332422 2c89b284b2 fix:修复Liquibase changeset ID误改的问题 2026-08-14 10:37:40 +08:00
Tyke Chen 04b3ad5aae fix(tts): validate timeout range 2026-08-13 15:59:41 +08:00
Tyke Chen 2c43a13d6e fix(tts): unify configurable request timeouts 2026-08-13 15:02:17 +08:00
Tyke Chen 17560a7d29 Merge pull request #3320 from henson957/fix/siliconflow-http-timeout
fix(tts): SiliconFlow provider 增加请求超时与 HTTP 错误处理
2026-08-13 14:25:06 +08:00
henson957 db7db3de67 fix(tts): reuse base tts_timeout config instead of hardcoded 15 2026-08-13 13:28:17 +08:00
henson957 d03d0d0982 fix(tts): add HTTP timeout and error handling to SiliconFlow provider
- requests.request() had no timeout: when the SiliconFlow API slows down,
  the request hangs forever and the device produces no audio intermittently
- non-200 responses (rate limit, error page) were written to disk as audio;
  now the status code is checked and an exception is raised with the body
2026-08-12 17:37:04 +08:00
Tyke Chen 545979873b Merge pull request #3318 from JansenChang/codex/fix-function-dialog-mcp-overlap
fix(manager-web): keep function dialog footer accessible with many MCP tools
2026-08-11 17:27:44 +08:00
ZhengYS 16b6841710 fix: keep function dialog footer accessible 2026-08-10 12:00:30 +08:00
3030332422 2a465f0fb5 fix: 修复 performance_tester 异步配置加载错误 2026-08-07 10:23:25 +08:00
3030332422 ea6b0c3746 update:豆包模型更换为doubao-seed-2-0-lite-260215,Gemini模型更换为gemini-3.6-flash 2026-08-07 09:09:53 +08:00
hrz 7475ea2b2d Merge pull request #3313 from xinnan-tech/fix-deepseek-model
deepseek更改模型为v4-flash并且关闭思考模式
2026-08-06 18:10:01 +08:00
欣南科技 1fa178cd36 Merge pull request #3314 from xinnan-tech/fix-star-img
fixed:Star History Chart
2026-08-06 18:08:59 +08:00
hrz 93885058e9 fixed:Star History Chart 2026-08-06 18:08:18 +08:00
3030332422 88eed86dc2 update:关闭deepseek的思考模式 2026-08-06 13:07:04 +08:00
3030332422 a269953787 fix:deepseek模型名称更改为deepseek-v4-flash 2026-08-06 11:10:31 +08:00
hrz 50895d6e33 Merge pull request #3310 from xinnan-tech/py-fix-pr_3304
展开模块级别插件名为具体函数名,合并 HA 设备控制插件为 hass_state 模块
2026-08-04 09:25:44 +08:00
3030332422 a21a2e34d0 refactor: 合并 HA 设备控制插件为 hass_state 模块 2026-07-29 09:58:26 +08:00
3030332422 7a22b61969 fix:修复与函数同名时遗漏其他函数的问题 2026-07-28 17:05:18 +08:00
CGD 477f85b97c Merge pull request #3304 from MsThink/main
fix(plugins): 展开模块级别插件名为具体函数名
2026-07-28 15:22:58 +08:00
CGD de45f73efd Merge pull request #3303 from chentyke/fix/frontend-contract-consistency
fix(manager): align frontend and backend field contracts
2026-07-28 09:37:39 +08:00
CGD 1dc46e30d7 Merge pull request #3302 from xinnan-tech/refactor/manager-api-tool-dependencies
refactor(manager-api): 升级常用工具依赖并收敛冗余实现
2026-07-28 09:28:00 +08:00
Sakura-RanChen 5e0d853256 Merge pull request #3309 from xinnan-tech/py-fix-reportHandle
refactor(reportHandle): 重构上报处理逻辑,适配多格式音频转换
2026-07-27 15:24:15 +08:00
wengzh 2a9f809700 refactor(reportHandle): 重构上报处理逻辑,适配多格式音频转换
1.  重命名参数与变量,明确上报类型与音频格式
2.  拆分PCM和Opus转WAV逻辑,分别处理不同输入格式
3.  移动校验逻辑到上报队列函数开头,提前拦截无效上报
4.  完善Opus转WAV的解码与资源释放流程
2026-07-27 09:59:32 +08:00
CGD f5ed1aaec8 Merge pull request #3305 from xinnan-tech/update_version
Bump to 0.9.6
2026-07-24 11:32:33 +08:00
colorful 523915f9fe fix(plugins): 展开模块级别插件名为具体函数名
在一个 function 文件中可能注册多个 @register_function,当配置下发或
本地配置使用模块名(文件名)时,需要自动展开为具体的函数名列表。

改动内容:
- register.py: 新增 module_func_map 全局映射,在 register_function
  装饰器中自动记录模块名 → 函数名列表的映射关系
- connection.py: 处理服务端下发的插件配置时,将模块级的 plugins 配置
  复制到各具体函数名 key 下,并将模块名展开写入 Intent functions 列表
- plugin_executor.py: 新增 _expand_plugin_names() 兜底展开逻辑和
  _get_plugin_description() 双层查找方法,确保本地配置场景也能正确展开
2026-07-24 10:47:00 +08:00
Tyke Chen 18e5c47a06 fix(manager): align frontend and backend field contracts 2026-07-24 10:21:05 +08:00
hrz 9da58e254c Bump to 0.9.6 2026-07-24 09:53:03 +08:00
Tyke Chen 4a2c90ec87 refactor(manager-api): 升级常用工具依赖并收敛冗余实现 2026-07-24 09:06:15 +08:00
CGD 27e57631a7 Merge pull request #3300 from xinnan-tech/test/manager-api-runtime-warnings
test(manager-api): 清理测试运行告警
2026-07-23 17:46:04 +08:00
CGD b1e39da9b8 Merge pull request #3301 from chentyke/fix/issue-3299-auto-update-state
fix: 修复设备自动升级开关状态不生效
2026-07-22 17:38:13 +08:00
Tyke Chen e87fc766d2 fix: restore device auto-update switch behavior 2026-07-22 17:10:21 +08:00
Tyke Chen 942d55118b test(manager-api): clean test runtime warnings 2026-07-22 16:41:44 +08:00
CGD f47f3b050b Merge pull request #3297 from xinnan-tech/fix/manager-api-compiler-warnings
fix(manager-api): 清理编译器告警
2026-07-22 16:22:46 +08:00
wengzh e3453b1a87 Merge pull request #3298 from xinnan-tech/py-fix-intent
Py fix intent
2026-07-21 15:16:19 +08:00
Tyke Chen d760465408 fix(manager-api): make JSON collections type-safe 2026-07-21 14:43:25 +08:00
Tyke Chen bdb2538239 fix(manager-api): clean deprecated and generic warnings 2026-07-21 14:43:25 +08:00
CGD ea6c144e43 Merge pull request #3294 from xinnan-tech/fix/manager-api-startup-warnings
fix(manager-api): 消除启动阶段告警
2026-07-21 14:37:22 +08:00
CGD a79aa455d6 Merge pull request #3292 from xinnan-tech/fix/manager-api-test-i18n
test: 修复 manager-api 国际化测试配置
2026-07-21 14:29:30 +08:00
wengzh 6e92d169ec refactor(get_news_from_newsnow): 改用流式处理解析网页内容
将原有的直接转换响应内容改为流式读取字节流并传入参数,适配MarkItDown的convert_stream接口,优化大内容处理时的内存占用
2026-07-20 15:51:23 +08:00
Sakura-RanChen ed89c05245 fix: intent阻塞主线程 2026-07-20 14:48:26 +08:00
Tyke Chen b500d1c6bd fix(manager-api): avoid duplicate address book insert mapping 2026-07-20 14:46:14 +08:00
Tyke Chen 0598b09629 fix(manager-api): remove invalid logback startup directives 2026-07-20 14:46:14 +08:00
Tyke Chen e4907121f4 fix(manager-api): eliminate premature bean initialization warnings 2026-07-20 14:46:14 +08:00
CGD 7c58fa37b2 Merge pull request #3293 from xinnan-tech/refactor/mybatis-plus-single-source
refactor(manager-api): consolidate MyBatis-Plus 3.5.17 integration
2026-07-20 14:42:31 +08:00
Tyke Chen 2a618d2f8f test: 修复 manager-api 国际化测试配置 2026-07-20 10:34:33 +08:00
Tyke Chen 10a67c772f refactor(manager-api): consolidate mybatis-plus 3.5.17 integration 2026-07-20 10:31:08 +08:00
CGD 7e8e5d34b8 Merge pull request #3279 from xinnan-tech/fix/issue-3276-context-provider
fix: 修复 ContextProviderListTypeHandler 多版本兼容问题
2026-07-17 09:28:14 +08:00
CGD c6bc20c05e Merge pull request #3283 from xinnan-tech/fix/device-timezone-consistency
fix: 修复跨时区场景下设备时间与在线状态异常
2026-07-16 16:58:49 +08:00
wengzh 1998807b69 Merge pull request #3285 from xinnan-tech/py-aec
perf: 优化相关逻辑减少重复计算
2026-07-14 16:36:16 +08:00
Sakura-RanChen 55c0495661 perf: 优化相关逻辑减少重复计算 2026-07-14 15:52:30 +08:00
Tyke Chen dbeda01697 fix: 修复设备跨时区时间与在线状态 2026-07-14 10:08:23 +08:00
Tyke Chen 023dea2441 fix: 修复上下文源类型处理器兼容问题 2026-07-13 16:03:39 +08:00
CGD ad3fb4d8c8 Merge pull request #3278 from xinnan-tech/agent-snapshot-history-fixes
fix: 完善智能体配置历史与恢复逻辑
2026-07-13 15:47:08 +08:00
3030332422 176785830a update:修改部分样式 2026-07-13 15:44:06 +08:00
Tyke Chen d78ecfce6f Merge remote-tracking branch 'origin/main' into agent-snapshot-history-fixes 2026-07-10 20:26:30 +08:00
Tyke Chen 179281e49c fix: harden agent snapshot restore flow 2026-07-10 20:21:44 +08:00
hrz bc1aa1a089 Merge pull request #3267 from xinnan-tech/fix-agent-idor
fix: prevent agent IDOR access
2026-07-10 17:36:00 +08:00
wengzh c5b64c3ef0 Merge pull request #3270 from xinnan-tech/fix-voiceprint-dialogue-bug
fix: 修改提示词与说话人注入逻辑,减少模型回答带着说话人信息频率
2026-07-10 15:55:58 +08:00
DaGou12138 2aa6c5ee49 fix:
1.优化context注入方式,合并为一个system,解决qwen本地部署模型多system异常400问题
2026-07-10 15:05:02 +08:00
Tyke Chen 687b6db96b fix: resolve mobile type and lint errors 2026-07-10 11:45:16 +08:00
Tyke Chen a5aee109fe feat: improve agent snapshot history 2026-07-10 11:44:35 +08:00
hrz 8ec5851027 Merge pull request #3273 from xinnan-tech/agent-delete-name-confirm
feat: 删除智能体前需要用户进行确认
2026-07-10 10:39:47 +08:00
Tyke Chen 48d629c79c fix: localize agent deletion success message 2026-07-10 10:18:48 +08:00
wengzh 14e57a1add fix(powermem): 修复用户配置启用状态的类型判断问题
将配置的启用值统一转为小写字符串后判断是否为true,避免非布尔类型配置导致的判断失效
2026-07-10 10:06:07 +08:00
hrz 634967b2fe Merge pull request #3274 from xinnan-tech/i18n-duplicate-check
test: 增加 i18n 重复 key 检测
2026-07-10 09:59:33 +08:00
Tyke Chen e648453c67 fix: rename i18n check command 2026-07-10 09:55:53 +08:00
Tyke Chen d11cfc6923 Merge remote-tracking branch 'origin/main' into fix-agent-idor
# Conflicts:
#	main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java
#	main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java
#	main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
2026-07-09 20:21:37 +08:00
DaGou12138 32fbbf92fe Merge branch 'refs/heads/main' into fix-voiceprint-dialogue-bug 2026-07-09 16:56:22 +08:00
wengzh bd981aac79 Merge pull request #3275 from xinnan-tech/py-audio-aec
feat: 服务端AEC功能
2026-07-09 16:24:03 +08:00
Sakura-RanChen adee070bb0 Merge branch 'main' into py-audio-aec 2026-07-09 15:51:01 +08:00
wengzh 03ec3bac3f Merge pull request #3271 from xinnan-tech/py-fix-bug
Py fix bug
2026-07-09 15:36:59 +08:00
Tyke Chen af5fbb5282 test: add i18n duplicate key check 2026-07-09 15:25:36 +08:00
Tyke Chen 9301c1889f feat: require agent name confirmation before delete 2026-07-09 15:03:12 +08:00
hrz ef2ed07c1c Merge pull request #3266 from xinnan-tech/agent-snapshot-history
Add agent configuration history
2026-07-09 14:54:27 +08:00
Tyke Chen 1e2c4c6a28 fix: 修复智能体快照基线与查看逻辑 2026-07-09 11:16:07 +08:00
Sakura-RanChen a32500a6a4 fix: 消除死锁隐患 2026-07-09 10:29:41 +08:00
hrz 772af6c918 Merge branch 'agent-snapshot-history' of https://github.com/xinnan-tech/xiaozhi-esp32-server into agent-snapshot-history 2026-07-08 17:46:50 +08:00
hrz 2a917c1e62 update:对齐i18n文件行数 2026-07-08 17:44:10 +08:00
Tyke Chen c32fbe69c5 Remove wakeup word audio asset 2026-07-08 16:33:36 +08:00
DaGou12138 3b6e8f0e4b fix:
1.优化声纹称呼人提示词和注入逻辑,减少次次都称呼的频率
2026-07-08 16:14:10 +08:00
Tyke Chen aae2ef2152 fix: prevent agent IDOR access 2026-07-08 15:52:47 +08:00
Tyke Chen c82b351457 Fix agent snapshot history issues 2026-07-08 15:15:26 +08:00
DaGou12138 04c10ef225 fix:
1.修改提示词与说话人注入,减少模型每次回答都带着说话人信息
2026-07-08 14:12:16 +08:00
Sakura-RanChen 0b8bb728e2 同步方法进行包装避免阻塞 2026-07-08 11:47:04 +08:00
mie 35ec3ba803 Add agent configuration history 2026-07-07 17:49:34 +08:00
wengzh 8808b69303 Merge pull request #3265 from xinnan-tech/ha-muisc-timeout
Ha muisc timeout
2026-07-07 16:25:12 +08:00
Sakura-RanChen fc573a72ea fix: 修复设备时间戳上报逻辑 2026-07-07 15:50:27 +08:00
Sakura-RanChen 59f6566cfb 同步工具异步化处理 2026-07-07 14:45:50 +08:00
Sakura-RanChen d8c97a9f81 fix: 修复携程死锁 2026-07-06 16:44:41 +08:00
Sakura-RanChen 3d204fb181 feat: 服务端AEC功能
ref: 入口解码为pcm音频数据,vad和asr可直接使用
2026-07-06 16:21:24 +08:00
hrz 209fd3fbc8 Merge pull request #3260 from xinnan-tech/test-funasr
FunASR识别指定语言
2026-07-06 13:56:48 +08:00
3030332422 bc0ffd1bd3 update:修改sql文件,删去测试文件 2026-07-01 15:02:26 +08:00
hrz f0d61442b8 Merge pull request #3259 from xinnan-tech/update-theme
顶部下拉菜单图标间距调整
2026-06-30 18:45:14 +08:00
CGD e6c2fac7ec Merge pull request #3255 from LauraGPT/funasr-language-option
fix: allow configuring FunASR recognition language
2026-06-30 14:57:31 +08:00
zhifu gao 5a9b5af441 fix: add FunASR language config migration 2026-06-30 14:07:16 +08:00
欣南科技 16e5dc8c78 Merge pull request #3258 from xinnan-tech/update-version
update:Bump to 0.9.5
2026-06-30 13:10:18 +08:00
hrz f77970dd7d update:Bump to 0.9.5 2026-06-30 13:10:26 +08:00
zhuoqinglian af3ca4f609 update: 顶部下拉菜单图标间距调整 2026-06-30 09:47:28 +08:00
hrz 1ebfcd1f13 Merge pull request #3256 from xinnan-tech/update-theme
解决智能体管理查询不展示的问题&删除知识库旧版废弃文件
2026-06-30 09:36:42 +08:00
zhuoqinglian 3ca322961e fix: 解决智能体管理查询不展示的问题 2026-06-30 09:22:05 +08:00
zhuoqinglian 0eb0a600a7 update: 删除知识库旧版废弃文件 2026-06-30 09:19:26 +08:00
wengzh 3ed06a9e34 Merge pull request #3244 from xinnan-tech/base_prompt-update
feat: 将系统提示词指令逻辑英文化,中文样本保留
2026-06-30 09:06:10 +08:00
LauraGPT 2fae96d60c fix: allow configuring FunASR recognition language 2026-06-29 20:58:51 +00:00
wengzh 67dde7cc47 Merge pull request #3254 from xinnan-tech/update-theme
智能体管理搜索恢复查询历史功能
2026-06-29 16:58:52 +08:00
zhuoqinglian efedc18f02 update: 智能体管理搜索恢复查询历史功能 2026-06-29 16:01:14 +08:00
zhuoqinglian 0270de1844 update: 滚动条样式抽离复用 2026-06-29 15:42:52 +08:00
wengzh 06b0ff139b Merge pull request #3247 from xinnan-tech/update-theme
Update theme
2026-06-26 17:58:18 +08:00
zhuoqinglian 799ea28d82 Merge branch 'main' into update-theme 2026-06-26 17:30:15 +08:00
zhuoqinglian 5a68252d22 update: 更新所有页面背景颜色 2026-06-26 17:28:06 +08:00
zhuoqinglian 97404e53fe update: 顶部导航栏搜索放到首页 2026-06-26 17:26:16 +08:00
zhuoqinglian b052158cee feat: 完成导航菜单组件样式优化 2026-06-26 17:10:49 +08:00
zhuoqinglian 979ef24b79 update: 音色克隆多语言配置 2026-06-26 15:32:48 +08:00
zhuoqinglian 0f4f0f01fd update: 音色克隆页面展现形式修改 2026-06-26 14:45:23 +08:00
wengzh 18e1de4f45 Merge pull request #3245 from xinnan-tech/py-add-minimax-voice
Py add minimax voice
2026-06-26 11:42:59 +08:00
Sakura-RanChen aa846a096f 名称长度调整 2026-06-26 11:41:46 +08:00
Sakura-RanChen a679412230 增加试听音色 2026-06-26 11:40:52 +08:00
3030332422 cf3e14bb07 feat: 将系统提示词指令逻辑英文化,中文样本保留 2026-06-26 11:06:18 +08:00
wengzh 3a73a17253 Merge pull request #3243 from xinnan-tech/py-add-minnimax-voice
调整minimax音色
2026-06-26 10:50:26 +08:00
Sakura-RanChen 3b27ebcd35 调整minimax音色 2026-06-26 10:29:07 +08:00
wengzh fa6074d284 Merge pull request #3242 from xinnan-tech/update-theme
Update theme
2026-06-25 17:29:55 +08:00
zhuoqinglian d809cfcb01 Merge branch 'main' into update-theme 2026-06-25 17:27:40 +08:00
zhuoqinglian eb2c09bbdc feat: 完成聊天记录、修改密码页面样式改造 2026-06-25 17:26:52 +08:00
zhuoqinglian 2cf8f79482 update: 模型配置新增、编辑弹窗样式优化 2026-06-25 17:09:29 +08:00
zhuoqinglian 1c94844bb9 feat: 完成声纹管理页面样式改造 2026-06-25 09:18:38 +08:00
wengzh 0c53b8f543 Merge pull request #3240 from xinnan-tech/update-theme
Update theme
2026-06-24 17:58:13 +08:00
zhuoqinglian 1518dfca30 feat: 完成设备管理页面样式改造 2026-06-24 17:37:37 +08:00
zhuoqinglian 7529c500a4 feat: 完成音色克隆页面样式改造 2026-06-24 11:43:36 +08:00
zhuoqinglian b6ed220e48 feat: 完成音色资源页面样式改造 2026-06-23 17:23:11 +08:00
wengzh b152c5dc23 Merge pull request #3239 from xinnan-tech/update-theme
模型配置页面样式调整
2026-06-23 16:31:30 +08:00
zhuoqinglian 04bfddf457 update: 按钮样式风格统一 2026-06-23 14:54:45 +08:00
zhuoqinglian 9d78ea29c3 Merge branch 'main' into update-theme 2026-06-23 14:15:57 +08:00
zhuoqinglian f82f364189 feat: 完成模型配置页面样式改造 2026-06-23 14:15:32 +08:00
zhuoqinglian 9705149b2b feat: 按钮组件新增add类型 2026-06-23 14:14:29 +08:00
wengzh 7986148013 Merge pull request #3238 from xinnan-tech/py-add-edge-config
补充edge相关配置
2026-06-23 10:40:40 +08:00
Sakura-RanChen 19f5cb5b4f 补充edge相关配置 2026-06-23 10:37:48 +08:00
zhuoqinglian ea48b948bf update: 模型配置新增弹窗样式调整 2026-06-22 17:58:35 +08:00
zhuoqinglian 3c5ca9f143 update: 模型配置编辑弹窗样式调整 2026-06-22 17:31:09 +08:00
wengzh da72ecaa1b Merge pull request #3235 from xinnan-tech/py-fix-tts
fix: 超时情况不与activate_session同一判断条件导致的监听任务未清除问题
2026-06-22 11:21:03 +08:00
Sakura-RanChen f297e1b648 fix: 超时情况不与activate_session同一判断条件导致的监听任务未清除问题 2026-06-22 11:16:25 +08:00
wengzh a1973e07b7 Merge pull request #3227 from xinnan-tech/update-theme
feat: 完成字典管理页面样式改造
2026-06-18 16:57:49 +08:00
zhuoqinglian cd58b2c3cb Merge branch 'main' into update-theme 2026-06-18 16:55:33 +08:00
zhuoqinglian a5be67d04b feat: 完成字典管理页面样式改造 2026-06-18 16:49:15 +08:00
wengzh e6dc6187c8 Merge pull request #3226 from xinnan-tech/update-theme
页面样式改造
2026-06-18 16:39:41 +08:00
zhuoqinglian 7f4011f9de feat: 完成字段管理页面样式改造 2026-06-18 16:37:42 +08:00
zhuoqinglian 211cf891af Merge branch 'main' into update-theme 2026-06-18 10:26:57 +08:00
zhuoqinglian 89a1229b53 feat: 完成服务端管理页面样式改造 2026-06-18 10:26:06 +08:00
wengzh 071ed99a32 Merge pull request #3224 from xinnan-tech/py-fix-bug
fix: 通讯录页面时间不显示
2026-06-17 14:36:48 +08:00
Sakura-RanChen ea14be27e6 fix: 通讯录页面时间不显示
fix: listen消息相关状态处理
2026-06-17 14:34:42 +08:00
zhuoqinglian a834f163b7 feat: 完成替换词页面样式改造 2026-06-17 14:20:48 +08:00
zhuoqinglian 7849e6b030 update: 模板保存添加必填校验 2026-06-17 10:36:02 +08:00
wengzh 0d869baaa0 Merge pull request #3223 from xinnan-tech/update-theme
页面样式调整
2026-06-16 17:01:10 +08:00
zhuoqinglian 164eb5481c feat: 完成默认角色管理页面样式改造,并添加编辑保存弹窗 2026-06-16 16:58:44 +08:00
zhuoqinglian 969ee3cb12 feat: 完成字段管理页面样式改造 2026-06-16 16:18:12 +08:00
zhuoqinglian ee54d87383 feat: 完成固件管理页面样式改造 2026-06-16 14:55:08 +08:00
wengzh 849026c015 Merge pull request #3222 from xinnan-tech/update-theme
Update theme
2026-06-16 10:42:24 +08:00
zhuoqinglian e5bb3e7791 update: 优化表格组件,增加代码的复用性 2026-06-16 10:40:44 +08:00
zhuoqinglian 1ffc40e71d feat: 完成用户管理页面样式改造 2026-06-16 10:39:52 +08:00
zhuoqinglian 45fd850757 update: 表格组件添加阴影效果 2026-06-16 09:46:25 +08:00
zhuoqinglian 8a5dac181e update: 知识库顶部标题合并 2026-06-15 17:37:53 +08:00
zhuoqinglian ecf20300ff update: 通讯录初始化加载默认选中第一项 2026-06-15 17:16:50 +08:00
zhuoqinglian 74def844f0 Merge branch 'main' into update-theme 2026-06-15 17:05:28 +08:00
zhuoqinglian a5a9bd1abe fix: 修复element组件多语言切换不生效的问题 2026-06-15 17:05:04 +08:00
zhuoqinglian 2d9b915d51 feat: 完成参数管理页面改造 2026-06-15 15:44:09 +08:00
hrz 5f87fc8361 Merge pull request #3204 from xinnan-tech/py-fix-calling
优化相关逻辑
2026-06-15 14:06:51 +08:00
zhuoqinglian 6859fdc90e update: 系统功能配置图标大小优化 2026-06-15 11:27:12 +08:00
zhuoqinglian b8ae0568e9 update: 系统功能配置页面适配调整 2026-06-15 11:23:31 +08:00
wengzh 1dc6f143f3 Merge pull request #3219 from xinnan-tech/update-theme
完成系统功能页面样式改造
2026-06-12 14:43:23 +08:00
zhuoqinglian 67532dc230 update: 系统功能配置完善多语言 2026-06-12 14:23:25 +08:00
zhuoqinglian b2876def99 Merge branch 'main' into update-theme 2026-06-12 14:14:59 +08:00
zhuoqinglian 0c73a8b6a4 feat: 完成系统功能页面样式改造 2026-06-12 14:13:21 +08:00
zhuoqinglian c857a3164d feat: 完成按钮组件抽离 2026-06-12 11:49:22 +08:00
hrz a964192eb0 Merge pull request #3213 from xinnan-tech/fix-knowledge-base-ragflow-bug
#3212 修复知识库RAGFlow相关逻辑BUG
2026-06-12 11:15:35 +08:00
DaGou12138 32da03818e #3212 fix:
1.修改知识库前端卡片提示样式,优化警示效果
2026-06-12 10:35:40 +08:00
DaGou12138 7f17e3e08e Merge branch 'refs/heads/main' into fix-knowledge-base-ragflow-bug 2026-06-12 10:29:17 +08:00
DaGou12138 c237e1f323 Merge remote-tracking branch 'origin/fix-knowledge-base-ragflow-bug' into fix-knowledge-base-ragflow-bug 2026-06-12 09:51:03 +08:00
DaGou12138 087a202e31 #3212 fix:
1.优化知识库连接RAGFlow异常提示,修改前端卡片提示样式
2026-06-12 09:50:01 +08:00
hrz 4b1460d938 update:调整同步数据集错误日志的级别 2026-06-11 19:10:21 +08:00
hrz f2aec524d2 del:无用类 2026-06-11 18:57:30 +08:00
wengzh 1a97ca137c Merge pull request #3215 from xinnan-tech/update-theme
Update theme
2026-06-11 17:45:58 +08:00
zhuoqinglian 2b6f457ae6 update: 知识库页面间距优化 2026-06-11 17:44:17 +08:00
DaGou12138 3f4e0b4aa0 #3212 fix:
1.修复RAGFlow连接异常导致级联删除问题
2.修复RAGFlow获取数量逻辑问题
3.远程失败不再删除本地影子记录,避免产生RAGFlow端孤儿文档
4.优化前端提示语
2026-06-11 17:17:59 +08:00
DaGou12138 e30ded4de9 fix:删除知识库rag相关非必要实体 2026-06-11 16:08:17 +08:00
hrz 07242a84ff Merge pull request #3210 from xinnan-tech/fix-knowledge-base-web-bug
fix:修复知识库模块弹框组件样式全局覆盖导致显示异常问题
2026-06-10 15:32:08 +08:00
DaGou12138 a2074dc0d8 fix:修复知识库模块弹框组件样式全局覆盖导致显示异常问题 2026-06-10 15:00:29 +08:00
zhuoqinglian 28b6c85650 fix: 修复弹窗展示位置异常的问题 2026-06-10 10:47:52 +08:00
wengzh af300138eb Merge pull request #3209 from xinnan-tech/update-theme
Update theme
2026-06-09 17:24:16 +08:00
zhuoqinglian 2bf5725048 update: 知识库页面复用弹窗、分页组件,同步修改样式 2026-06-09 17:22:18 +08:00
hrz 3d73141788 Merge pull request #3206 from xinnan-tech/web_search-Document
docs:新增联网搜索插件配置文档
2026-06-09 17:12:16 +08:00
hrz 945ab29bc3 add:增加联网搜索的教程和自编译docker镜像的教程 2026-06-09 17:11:35 +08:00
zhuoqinglian fe0f7dcf75 feat: 完成分页组件抽离 2026-06-09 15:05:16 +08:00
zhuoqinglian 1a3ffbc8e2 feat: 完成弹窗组件抽离 2026-06-09 14:25:49 +08:00
3030332422 71278b1898 docs:新增联网搜索插件配置文档 2026-06-08 11:38:11 +08:00
zhuoqinglian 0173c7abe1 update: 知识库样式优化、添加多语言配置 2026-06-05 17:37:21 +08:00
zhuoqinglian 8a50eea4ce update: 知识库整体页面样式调整 2026-06-03 16:49:08 +08:00
Sakura-RanChen b9891bb795 优化相关逻辑 2026-06-03 16:13:03 +08:00
wengzh b903a88cd5 Merge pull request #3203 from xinnan-tech/mac-address-mask
feat: mac地址添加马赛克功能
2026-06-03 14:41:34 +08:00
zhuoqinglian f3fcf2fedf feat: mac地址添加马赛克功能 2026-06-03 14:40:01 +08:00
欣南科技 a825f0874c Merge pull request #3202 from xinnan-tech/update-doc-video
update:更新演示视频
2026-06-03 13:58:14 +08:00
hrz 8a5ca46418 update:更新演示视频 2026-06-03 13:56:34 +08:00
429 changed files with 36090 additions and 17409 deletions

No files matched your search

+37 -1
View File
@@ -3,4 +3,40 @@ updates:
- package-ecosystem: pip
directory: /main/xiaozhi-server
schedule:
interval: weekly
interval: monthly
open-pull-requests-limit: 5
groups:
ai-services:
patterns:
- "openai*"
- "google-generativeai"
- "dashscope"
- "baidu-aip"
- "cozepy"
- "markitdown"
- "mem0ai"
- "powermem"
ai-models:
patterns:
- "funasr"
- "vosk"
- "silero_vad"
- "opuslib_next"
- "sherpa_onnx"
- "modelscope"
web-stack:
patterns:
- "httpx"
- "aiohttp"
- "aiohttp_cors"
- "requests"
- "pysocks"
everything-else:
patterns:
- "*"
ignore:
- dependency-name: "torch"
- dependency-name: "torchaudio"
- dependency-name: "numpy"
- dependency-name: "portalocker"
- dependency-name: "websockets"
+66
View File
@@ -0,0 +1,66 @@
name: Tests
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
python:
name: Python (xiaozhi-server)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
cache-dependency-path: 'main/xiaozhi-server/requirements.txt'
- name: Install dependencies
working-directory: main/xiaozhi-server
run: |
pip install -r requirements.txt
pip install pytest pytest-asyncio freezegun
- name: Run smoke test
working-directory: main/xiaozhi-server
run: |
if [ -f tests/test_smoke.py ]; then
pytest tests/test_smoke.py -x -q
else
echo "::warning::Slice 2 not done yet — tests/test_smoke.py missing"
exit 0
fi
java:
name: Java (manager-api)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven'
- name: Run tests
working-directory: main/manager-api
run: mvn -B -q test -DfailIfNoTests=false
web:
name: Vue (manager-web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: main/manager-web/package-lock.json
- name: Run tests
working-directory: main/manager-web
run: |
npm ci --no-audit --no-fund
npm run test:unit
+5
View File
@@ -112,12 +112,17 @@ celerybeat.pid
# Environments
.env
.venv
/.venv-*/
env/
venv/
ENV/
env.bak/
venv.bak/
# Repository-local runtimes and package-manager caches
/.runtime/
/main/manager-web/.npm-cache/
# Spyder project settings
.spyderproject
.spyproject
+2 -1
View File
@@ -29,4 +29,5 @@ COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
# 阿里云 PyPI 镜像偶有元数据 404,加官方源作 fallback
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5 --extra-index-url https://pypi.org/simple
+21
View File
@@ -0,0 +1,21 @@
.PHONY: test test-fast test-python test-java test-web help
help:
@echo "make test-fast - run all fast unit tests (default)"
@echo "make test - alias for test-fast"
@echo "make test-python - pytest in main/xiaozhi-server"
@echo "make test-java - mvn test in main/manager-api"
@echo "make test-web - npm run test:unit in main/manager-web"
test: test-fast
test-fast: test-python test-java test-web
test-python:
cd main/xiaozhi-server && python -m pytest -x -q
test-java:
cd main/manager-api && mvn -B -q test -DfailIfNoTests=false
test-web:
cd main/manager-web && npm ci --no-audit --no-fund && npm test
+72 -88
View File
@@ -53,113 +53,98 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="docs/images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="docs/images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
@@ -368,11 +353,10 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| <img src="./docs/images/logo_contributors.png" width="160"> | [代码贡献者](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | 感谢[所有代码贡献者](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors)贡献者,你们的付出让项目更加健壮和强大。 |
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
<a href="https://www.star-history.com/?repos=xinnan-tech%2Fxiaozhi-esp32-server&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=xinnan-tech/xiaozhi-esp32-server&type=date&theme=dark&legend=top-left&sealed_token=cQovHKgZjqEnQ-svfYfN392irNvGuq-6pyv4cA8nd2jQEhQLz1ETV4YHTVk2UZyLMFbCQuZA7jduRh3YbeK5WPYaRLrfmIimGQa3lram652jJL9oQk-UuSZA5H6L4dPIhZc8KCc-Ur_UAUNbly7TePpnTR2otGknBLCOjOliD4fk1st6z7tPEDVjSRx5" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=xinnan-tech/xiaozhi-esp32-server&type=date&legend=top-left&sealed_token=cQovHKgZjqEnQ-svfYfN392irNvGuq-6pyv4cA8nd2jQEhQLz1ETV4YHTVk2UZyLMFbCQuZA7jduRh3YbeK5WPYaRLrfmIimGQa3lram652jJL9oQk-UuSZA5H6L4dPIhZc8KCc-Ur_UAUNbly7TePpnTR2otGknBLCOjOliD4fk1st6z7tPEDVjSRx5" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=xinnan-tech/xiaozhi-esp32-server&type=date&legend=top-left&sealed_token=cQovHKgZjqEnQ-svfYfN392irNvGuq-6pyv4cA8nd2jQEhQLz1ETV4YHTVk2UZyLMFbCQuZA7jduRh3YbeK5WPYaRLrfmIimGQa3lram652jJL9oQk-UuSZA5H6L4dPIhZc8KCc-Ur_UAUNbly7TePpnTR2otGknBLCOjOliD4fk1st6z7tPEDVjSRx5" />
</picture>
</a>
+2 -2
View File
@@ -70,7 +70,7 @@ xiaozhi-server
## 2. 配置项目文件
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
接下来,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
[跳转到配置项目文件](#配置项目)
配置完项目文件后,回到本教程继续往下。
@@ -119,7 +119,7 @@ docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
如果确定使用`conda`,则安装好后,开始执行以下命令。
重要提示!windows 用户,可以通过安装`Anaconda`来管理环境。安装好`Anaconda`后,在`开始`那里搜索`anaconda`相关的关键词,
找到`Anaconda Prpmpt`,使用管理员身份运行它。如下图。
找到`Anaconda Prompt`,使用管理员身份运行它。如下图。
![conda_prompt](./images/conda_env_1.png)
+1 -1
View File
@@ -335,7 +335,7 @@ npm run serve
如果确定使用`conda`,则安装好后,开始执行以下命令。
重要提示!windows 用户,可以通过安装`Anaconda`来管理环境。安装好`Anaconda`后,在`开始`那里搜索`anaconda`相关的关键词,
找到`Anaconda Prpmpt`,使用管理员身份运行它。如下图。
找到`Anaconda Prompt`,使用管理员身份运行它。如下图。
![conda_prompt](./images/conda_env_1.png)
+2
View File
@@ -65,6 +65,7 @@ VAD:
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
### 8、编译固件相关教程
1、[如何自己编译小智固件](./firmware-build.md)<br/>
@@ -85,6 +86,7 @@ VAD:
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
### 10、数字人相关教程
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
+1 -1
View File
@@ -7,7 +7,7 @@
`上下文源` 在小智在唤醒那一刻,获取外部系统的数据,并将其动态注入到大模型的系统提示词(System Prompt)中。
让其做到唤醒时感知世界某个事物的状态。
它和MCP、记忆有本质的区别:`上下文源`是强制让小智感知世界的数据;`记忆(Mem)`是让他知道之前聊了什么内容;`MCP(functionc all)`是当需要调用某项能力/知识的时候使用调用。
它和MCP、记忆有本质的区别:`上下文源`是强制让小智感知世界的数据;`记忆(Mem)`是让他知道之前聊了什么内容;`MCP(function_call)`是当需要调用某项能力/知识的时候使用调用。
通过这个功能,在小智唤醒的一刹那,“感知”到:
- 人体健康传感器状态(体温、血压、血氧状态等)
+2 -2
View File
@@ -51,7 +51,7 @@ cp 你原来的model.pt完整路径 /home/system/xiaozhi/xiaozhi-esp32-server/ma
# 第四步 建立三个自动编译文件
## 4.1 自动编译mananger-web模块
## 4.1 自动编译manager-web模块
在`/home/system/xiaozhi/`目录下,创建名字为`update_8001.sh`的文件,内容如下
```
@@ -106,7 +106,7 @@ fi
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev &
tail tail -f nohup.out
tail -f nohup.out
```
保存好后执行赋权命令
+66 -13
View File
@@ -1,21 +1,74 @@
# 本地编译docker镜像方法
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的。
现在本项目已经使用`github`的`自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像的需求,那就忽略本文档。
1、安装docker
```
如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
## 1、环境准备
安装docker:
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
2、编译docker镜像
```
#进入项目根目录
# 编译server
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
# 编译web
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
# 编译完成后,可以使用docker-compose启动项目
# docker-compose.yml你需要修改成自己编译的镜像版本
## 2、编译镜像
当你修改好代码后,需要编译新的镜像时,需要按照以下步骤操作:
准备好你的`你的用户名`和`新的版本号`。
- 这个`你的用户名`是你在`docker hub`注册的用户名,例如`xiaozhi`。当然,如果你不需要推送到`docker hub`,你可以自由定义。
- 这个`新的版本号`是你编译的镜像版本,例如`1.2.3`,你可以根据需要自定义或者使用日期格式(例如`20260609`)主要是方便和现在运行的版本号做区分,同时也方便下次回忆你是什么时候构建的,不要和现在你本机运行的版本号相同。
进入`xiaozhi-esp32-server`项目根目录,编译 server 和 web 两个镜像:
```bash
cd 项目根目录
# 编译server镜像
docker build -f Dockerfile-server -t 你的用户名/xiaozhi-esp32-server:新的版本号 .
# 编译web镜像
docker build -f Dockerfile-web -t 你的用户名/xiaozhi-esp32-server-web:新的版本号 .
```
## 3、修改docker-compose配置
```bash
cd main/xiaozhi-server
docker compose up -d
```
编辑 `docker-compose_all.yml` 文件,将镜像版本替换为你刚才编译的版本:
```yaml
services:
xiaozhi-esp32-server:
image: 你的用户名/xiaozhi-esp32-server:新的版本号 # 修改为你的镜像地址
...
xiaozhi-esp32-server-web:
image: 你的用户名/xiaozhi-esp32-server-web:新的版本号 #修改为你的镜像地址
...
```
## 4、重启服务
```bash
# 停止旧容器
docker compose -f docker-compose_all.yml down
# 启动新容器
docker compose -f docker-compose_all.yml up -d
```
## 5、验证
查看日志确认服务启动正常:
```bash
# 查看server日志
docker logs -f -n 50 xiaozhi-esp32-server
# 查看web日志
docker logs -f -n 50 xiaozhi-esp32-server-web
```
+1 -1
View File
@@ -45,7 +45,7 @@ python download_models.py
python -m tools.api_server --listen 0.0.0.0:6006
```
然后用浏览器去到aotodl实例页面
然后用浏览器去到autodl实例页面
```
https://autodl.com/console/instance/list
```
+2 -2
View File
@@ -68,7 +68,7 @@ http://homeassistant.local:8123
然后在实体中搜索你相关控制的开关,结果出来后,在列表中,点击其中一个结果,这是会出现一个开关的界面。
在开关的界面,我们尝试点击开关,看看是开发会随着我们的点击开/关。如果能操作,说明是正常联网的。
在开关的界面,我们尝试点击开关,看看是否会随着我们的点击开/关。如果能操作,说明是正常联网的。
接着在开关面板找到设置按钮,点击后,可以查看这个开关的`实体标识符`。
@@ -107,7 +107,7 @@ http://homeassistant.local:8123
保存成功后,即可唤醒设备操作。
#### 3. 唤醒设别进行控制
#### 3. 唤醒设备进行控制
尝试和esp32说,“打开XXX灯”
+1 -1
View File
@@ -3,7 +3,7 @@
本教程分为4个阶段:准备阶段、配置阶段、克隆阶段、使用阶段。主要是介绍通过智控台配置火山双流式语音合成+音色克隆的过程。
## 第一阶段:准备阶段
超级管理员先预先把火山引擎服务开通好,获取到App Id,Access Token。默认火上引擎会赠送一个音色资源。这个音色资源需要把它复制到本项目里。
超级管理员先预先把火山引擎服务开通好,获取到App Id,Access Token。默认火山引擎会赠送一个音色资源。这个音色资源需要把它复制到本项目里。
如果你想克隆多个音色,需要购买开通多个音色资源。只要把每个音色资源的声音ID(S_xxxxx)复制到本项目。然后分配给系统的账号使用即可。以下是详细步骤:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 108 KiB

+1 -1
View File
@@ -37,7 +37,7 @@
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-calculatorr-main`
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`mcp-calculator-main`
你需要把它重命名成`mcp-calculator`。接下来,我们用命令行进入项目目录即安装依赖
+1 -1
View File
@@ -113,7 +113,7 @@ server:
那我们需要先登录[智谱AI](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)网站,申请密钥。如果你之前已经申请过了密钥,可以复用这个密钥。
登录`智控台`,顶部菜单点击`模型配置`,在左侧栏点击`视觉打语言模型`,找到`VLLM_ChatGLMVLLM`,点击修改按钮,在弹框中,在`API密钥`输入你密钥,点击保存。
登录`智控台`,顶部菜单点击`模型配置`,在左侧栏点击`视觉大语言模型`,找到`VLLM_ChatGLMVLLM`,点击修改按钮,在弹框中,在`API密钥`输入你密钥,点击保存。
保存成功后,去到你需要测试的智能体哪里,点击`配置角色`,在打开的内容里,查看`视觉大语言模型(VLLM)`是否选择了刚才的视觉模型。点击保存。
+1 -1
View File
@@ -86,7 +86,7 @@ SERVER_SECRET=Te1st12134 # 服务器密钥,请保持和智控台(server.sec
- 注意不要用简单的密码,比如`123456`、`test`等。
- 注意不要用简单的密码,比如`123456`、`test`等。
`SERVER_SECRET` 是用生成websocket连接的认证信息。
`SERVER_SECRET` 是用于生成websocket连接的认证信息。
1、如果你是全模块部署,且你的智控台的参数管理里`server.auth.enabled`设置成了`true`,那么,`SERVER_SECRET`需要和智控台(`server.secret`)保持一致。
+2 -2
View File
@@ -1,6 +1,6 @@
# ragflow 集成指南
本教程主要是是两部分
本教程主要是两部分
- 一、如何部署ragflow
- 二、如何在智控台配置ragflow接口
@@ -12,7 +12,7 @@
ragflow需要依赖`mysql`数据库。如果你之前已经部署`智控台`,说明你已经安装了`mysql`。你可以共用它。
你可以你试一下在宿主机使用`telnet`命令,看看能不能正常访问`mysql`的`3306`端口。
你可以试一下在宿主机使用`telnet`命令,看看能不能正常访问`mysql`的`3306`端口。
``` shell
telnet 127.0.0.1 3306
+68 -83
View File
@@ -52,113 +52,98 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+68 -83
View File
@@ -52,113 +52,98 @@ Want to see the usage effects? Click the videos below 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+68 -83
View File
@@ -52,113 +52,98 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="Cenário médico complexo" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="Reproduzir música" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="Plugin de clima" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="Transmissão de notícias" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+68 -83
View File
@@ -52,113 +52,98 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+1 -1
View File
@@ -20,7 +20,7 @@
声纹识别需要依赖`mysql`数据库。如果你之前已经部署`智控台`,说明你已经安装了`mysql`。你可以共用它。
你可以你试一下在宿主机使用`telnet`命令,看看能不能正常访问`mysql`的`3306`端口。
你可以试一下在宿主机使用`telnet`命令,看看能不能正常访问`mysql`的`3306`端口。
```
telnet 127.0.0.1 3306
```
+14
View File
@@ -46,6 +46,10 @@
8. 将复制过来的第二个关键配置`API Host`,填入到`开发者 API Host`里
9. 保存配置,再保存智能体配置
智控台这边插件参数里的「API 密钥」和「API Host」两栏默认是空的:仓库早期版本内置过一把已经失效的和风天气凭据,数据库变更脚本把它清掉了,得照着本文件的「API Key 申请指南」申请自己的一套再填。字段名后面括号里写了申请路径。
脚本只清仓库内置的那一把,你自己填的凭据原样保留。以后要换凭据,在智控台「角色配置 → 编辑功能 → 天气查询」的参数表单里改完保存就行。清理的范围见文末那节。
### 方式2. 如果你只是单模块xiaozhi-server部署
在 `data/.config.yaml` 中配置:
@@ -62,3 +66,13 @@ plugins:
default_location: "你的默认查询城市"
```
## 智控台那边内置的凭据清到了什么程度
早期版本把一把和风天气的 API Key 和一个 API Host 写在模型提供方「天气插件」那一行的字段默认值里,角色配置的表单拿这个默认值预填,不填就等于沿用仓库里那一份。那把 key 已经失效,仓库也不该带着凭据分发。数据库变更脚本做两件事:
- 把那一行的默认值清空,同时把申请路径写进字段名,用户在界面上能直接看到去哪儿申请
- 把已有智能体的参数里还等于那把内置 key 的项清空
清不掉的有两类。一是管理员自己填的值,脚本不动,误清就是误删人家的凭据。二是这一种:管理员在界面上改过那一行的默认值之后,仓库内置的那把失效 key 就被覆盖了,再也认不出来,早于管理员改动建立、参数里还存着那把旧内置 key 的智能体不会被自动清掉,只能由管理员自己在界面上改。
这两条清理跟着另外两个改动走:一个是插件在没有凭据时给出提示而不是报错,另一个是 CI 真的会跑测试,这条变更脚本才在 CI 里被验证过。两者都要先落地。
+79
View File
@@ -0,0 +1,79 @@
# 联网搜索插件使用指南
## 功能简介
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持三个搜索源:秘塔(Metaso)、Tavily和Serply,用户可根据需要选择其中一个。
## API Key申请指南
目前我们适配了`秘塔搜索`、`Tavily搜索`和`Serply搜索`。
- Tavily搜索:每个月1000次免费额度。
- 秘塔搜索:拥有较为优质的国内数据源。
- Serply搜索:返回谷歌搜索结果,注册赠送2500次免费额度,额度用完后需要付费。
## API Key申请指南
### 方式一:使用秘塔搜索
- 访问 [秘塔搜索API](https://metaso.cn/search-api/api-keys),注册并登录账号
- 在API密钥管理页面,点击"创建新的Key"
- 复制生成的API Key(以 `mk-` 为前缀),这是配置所需的关键信息
### 方式二:使用Tavily搜索
- 访问 [Tavily控制台](https://app.tavily.com/home),注册并登录账号
- 在控制台中创建API Key
- 复制生成的API Key(以 `tvly-` 为前缀),这是配置所需的关键信息
### 方式三:使用Serply搜索
- 访问 [Serply](https://serply.io),注册并登录账号
- 在控制台中创建API Key
- 复制生成的API Key,这是配置所需的关键信息
- 接口说明见 [Serply文档](https://serply.io/docs)
## 配置方式
### 方式1. 使用智控台部署(推荐)
- 登录智控台
- 进入"配置角色"页面,选择要配置的智能体
- 点击"编辑功能"按钮,在右侧参数配置区域找到"联网搜索"插件
- 勾选"联网搜索"
- 填入搜索源(`metaso`、`tavily`或`serply`),并将对应的`API Key`填入配置项
- 保存配置,再保存智能体配置
### 方式2. 单模块xiaozhi-server部署
在 `data/.config.yaml` 中配置:
- 将搜索源填入 `provider`,可选值为 `metaso`、`tavily` 或 `serply`
- 将申请到的API Key填入 `api_key`
```yaml
plugins:
web_search:
provider: "metaso"
api_key: "你的API Key"
```
如需自定义返回结果数量和工具描述,可额外配置 `max_results` 和 `description`:
```yaml
plugins:
web_search:
provider: "metaso"
description: "联网搜索工具。当用户明确需要联网搜索问题时使用此工具。"
max_results: 5
api_key: "你的API Key"
```
同时在 `functions` 列表中确保已启用 `web_search`:
```yaml
plugins:
functions:
- web_search
```
配置完成后重启服务即可生效。
+23 -10
View File
@@ -20,18 +20,20 @@
<java.version>21</java.version>
<junit.version>5.10.1</junit.version>
<druid.version>1.2.20</druid.version>
<mybatisplus.version>3.5.5</mybatisplus.version>
<hutool.version>5.8.24</hutool.version>
<jsoup.version>1.19.1</jsoup.version>
<mybatisplus.version>3.5.17</mybatisplus.version>
<hutool.version>5.8.46</hutool.version>
<jsoup.version>1.22.2</jsoup.version>
<knife4j.version>4.6.0</knife4j.version>
<springdoc.version>2.8.8</springdoc.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<shiro.version>2.0.2</shiro.version>
<captcha.version>1.6.2</captcha.version>
<guava.version>33.0.0-jre</guava.version>
<guava.version>33.6.0-jre</guava.version>
<liquibase-core.version>4.20.0</liquibase-core.version>
<aliyun-sms-version>4.1.0</aliyun-sms-version>
<okio-version>3.4.0</okio-version>
<skipTests>true</skipTests>
<argLine></argLine>
</properties>
<dependencies>
@@ -175,13 +177,13 @@
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
@@ -271,11 +273,22 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>full</proc>
<compilerArgs>
<arg>-Xlint:deprecation,unchecked</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
<skipTests>${skipTests}</skipTests>
<argLine>@{argLine} -Xshare:off -javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar"</argLine>
</configuration>
</plugin>
</plugins>
@@ -324,7 +324,7 @@ public interface Constant {
/**
* 版本号
*/
public static final String VERSION = "0.9.4";
public static final String VERSION = "0.9.8";
/**
* 无效固件URL
@@ -67,7 +67,7 @@ public class DataFilterInterceptor implements InnerInterceptor {
private String getSelect(String buildSql, DataScope scope) {
try {
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
PlainSelect plainSelect = select.getPlainSelect();
Expression expression = plainSelect.getWhere();
if (expression == null) {
@@ -82,4 +82,4 @@ public class DataFilterInterceptor implements InnerInterceptor {
return buildSql;
}
}
}
}
@@ -5,7 +5,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log;
@@ -81,8 +81,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
// 处理排序字段
if (orderField instanceof String) {
orderFields.add((String) orderField);
} else if (orderField instanceof List) {
orderFields.addAll((List<String>) orderField);
} else if (orderField instanceof List<?> fields) {
fields.forEach(field -> orderFields.add(String.class.cast(field)));
}
// 有排序字段则排序
@@ -142,11 +142,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return SqlHelper.retBool(result);
}
protected Class<M> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
protected Class<?> currentMapperClass() {
return ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
}
@Override
@SuppressWarnings("unchecked")
public Class<T> currentModelClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
}
@@ -181,8 +182,9 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
* 执行批量操作
*/
@SuppressWarnings("deprecation")
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiFunction<SqlSession, E, Integer> operation) {
return SqlHelper.executeBatch(SqlHelper.sqlSessionFactory(this.currentModelClass()), this.log, list, batchSize,
operation);
}
@Override
@@ -209,7 +211,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, entity);
sqlSession.update(sqlStatement, param);
return sqlSession.update(sqlStatement, param);
});
}
@@ -225,6 +227,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
@Override
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
return SqlHelper.retBool(baseDao.deleteByIds(idList));
}
}
}
@@ -24,6 +24,7 @@ import xiaozhi.common.utils.ConvertUtils;
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
implements CrudService<T, D> {
@SuppressWarnings("unchecked")
protected Class<D> currentDtoClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
}
@@ -70,6 +71,6 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends Bas
@Override
public void delete(Serializable[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
baseDao.deleteByIds(Arrays.asList(ids));
}
}
}
@@ -1,52 +0,0 @@
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();
}
}
@@ -1,7 +1,9 @@
package xiaozhi.common.utils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -16,6 +18,10 @@ import cn.hutool.core.util.StrUtil;
*/
public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<>() {
};
private static final TypeReference<List<Map<String, Object>>> STRING_OBJECT_MAP_LIST = new TypeReference<>() {
};
public static String toJsonString(Object object) {
try {
@@ -67,4 +73,59 @@ public class JsonUtils {
}
}
public static Map<String, Object> parseMap(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
return parseObject(text, STRING_OBJECT_MAP);
}
public static List<Map<String, Object>> parseMapList(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
return parseObject(text, STRING_OBJECT_MAP_LIST);
}
public static Map<String, Object> toStringObjectMap(Object value) {
if (value == null) {
return null;
}
if (!(value instanceof Map<?, ?> map)) {
throw new ClassCastException("Expected Map but got " + value.getClass().getName());
}
Map<String, Object> result = new LinkedHashMap<>(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
result.put(String.class.cast(entry.getKey()), entry.getValue());
}
return result;
}
public static List<Map<String, Object>> toStringObjectMapList(Object value) {
if (value == null) {
return null;
}
List<?> list = List.class.cast(value);
List<Map<String, Object>> result = new ArrayList<>(list.size());
for (Object item : list) {
result.add(toStringObjectMap(item));
}
return result;
}
public static <T> List<T> toList(Object value, Class<T> elementType) {
if (value == null) {
return null;
}
List<?> list = List.class.cast(value);
List<T> result = new ArrayList<>(list.size());
for (Object item : list) {
result.add(elementType.cast(item));
}
return result;
}
}
@@ -75,11 +75,11 @@ public class SensitiveDataUtils {
Object value = jsonObject.get(key);
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
result.put(key, maskMiddle((String) value));
result.set(key, maskMiddle((String) value));
} else if (value instanceof JSONObject) {
result.put(key, maskSensitiveFields((JSONObject) value));
result.set(key, maskSensitiveFields((JSONObject) value));
} else {
result.put(key, value);
result.set(key, value);
}
}
@@ -162,4 +162,4 @@ public class SensitiveDataUtils {
}
}
}
}
}
@@ -1,89 +0,0 @@
package xiaozhi.common.utils;
import cn.hutool.core.util.ReUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 通用工具类
*/
public class ToolUtil {
private static final Logger logger = LoggerFactory.getLogger(ToolUtil.class);
/**
* 对象是否不为空(新增)
*/
public static boolean isNotEmpty(Object o) {
return !isEmpty(o);
}
/**
* 对象是否为空
*/
public static boolean isEmpty(Object o) {
if (o == null) {
return true;
}
if (o instanceof String) {
if (o.toString().trim().equals("")) {
return true;
}
} else if (o instanceof List) {
if (((List) o).size() == 0) {
return true;
}
} else if (o instanceof Map) {
if (((Map) o).size() == 0) {
return true;
}
} else if (o instanceof Set) {
if (((Set) o).size() == 0) {
return true;
}
} else if (o instanceof Object[]) {
if (((Object[]) o).length == 0) {
return true;
}
} else if (o instanceof int[]) {
if (((int[]) o).length == 0) {
return true;
}
} else if (o instanceof long[]) {
if (((long[]) o).length == 0) {
return true;
}
}
return false;
}
/**
* 对象组中是否存在空对象
*/
public static boolean isOneEmpty(Object... os) {
for (Object o : os) {
if (isEmpty(o)) {
return true;
}
}
return false;
}
/**
* 对象组中是否全是空对象
*/
public static boolean isAllEmpty(Object... os) {
for (Object o : os) {
if (!isEmpty(o)) {
return false;
}
}
return true;
}
}
@@ -22,10 +22,10 @@ public class SqlFilter {
return null;
}
// 去掉'|"|;|\字符
str = StringUtils.replace(str, "'", "");
str = StringUtils.replace(str, "\"", "");
str = StringUtils.replace(str, ";", "");
str = StringUtils.replace(str, "\\", "");
str = str.replace("'", "");
str = str.replace("\"", "");
str = str.replace(";", "");
str = str.replace("\\", "");
// 转换成小写
str = str.toLowerCase();
@@ -0,0 +1,103 @@
package xiaozhi.modules.agent.Enums;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import lombok.Getter;
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
@Getter
public enum AgentSnapshotField {
AGENT_CODE("agentCode", AgentSnapshotDataDTO::getAgentCode, AgentUpdateDTO::getAgentCode,
(agent, data) -> agent.setAgentCode(data.getAgentCode())),
AGENT_NAME("agentName", AgentSnapshotDataDTO::getAgentName, AgentUpdateDTO::getAgentName,
(agent, data) -> agent.setAgentName(data.getAgentName())),
ASR_MODEL_ID("asrModelId", AgentSnapshotDataDTO::getAsrModelId, AgentUpdateDTO::getAsrModelId,
(agent, data) -> agent.setAsrModelId(data.getAsrModelId())),
VAD_MODEL_ID("vadModelId", AgentSnapshotDataDTO::getVadModelId, AgentUpdateDTO::getVadModelId,
(agent, data) -> agent.setVadModelId(data.getVadModelId())),
LLM_MODEL_ID("llmModelId", AgentSnapshotDataDTO::getLlmModelId, AgentUpdateDTO::getLlmModelId,
(agent, data) -> agent.setLlmModelId(data.getLlmModelId())),
SLM_MODEL_ID("slmModelId", AgentSnapshotDataDTO::getSlmModelId, AgentUpdateDTO::getSlmModelId,
(agent, data) -> agent.setSlmModelId(data.getSlmModelId())),
VLLM_MODEL_ID("vllmModelId", AgentSnapshotDataDTO::getVllmModelId, AgentUpdateDTO::getVllmModelId,
(agent, data) -> agent.setVllmModelId(data.getVllmModelId())),
TTS_MODEL_ID("ttsModelId", AgentSnapshotDataDTO::getTtsModelId, AgentUpdateDTO::getTtsModelId,
(agent, data) -> agent.setTtsModelId(data.getTtsModelId())),
TTS_VOICE_ID("ttsVoiceId", AgentSnapshotDataDTO::getTtsVoiceId, AgentUpdateDTO::getTtsVoiceId,
(agent, data) -> agent.setTtsVoiceId(data.getTtsVoiceId())),
TTS_LANGUAGE("ttsLanguage", AgentSnapshotDataDTO::getTtsLanguage, AgentUpdateDTO::getTtsLanguage,
(agent, data) -> agent.setTtsLanguage(data.getTtsLanguage())),
TTS_VOLUME("ttsVolume", AgentSnapshotDataDTO::getTtsVolume, AgentUpdateDTO::getTtsVolume,
(agent, data) -> agent.setTtsVolume(data.getTtsVolume())),
TTS_RATE("ttsRate", AgentSnapshotDataDTO::getTtsRate, AgentUpdateDTO::getTtsRate,
(agent, data) -> agent.setTtsRate(data.getTtsRate())),
TTS_PITCH("ttsPitch", AgentSnapshotDataDTO::getTtsPitch, AgentUpdateDTO::getTtsPitch,
(agent, data) -> agent.setTtsPitch(data.getTtsPitch())),
MEM_MODEL_ID("memModelId", AgentSnapshotDataDTO::getMemModelId, AgentUpdateDTO::getMemModelId,
(agent, data) -> agent.setMemModelId(data.getMemModelId())),
INTENT_MODEL_ID("intentModelId", AgentSnapshotDataDTO::getIntentModelId, AgentUpdateDTO::getIntentModelId,
(agent, data) -> agent.setIntentModelId(data.getIntentModelId())),
CHAT_HISTORY_CONF("chatHistoryConf", AgentSnapshotDataDTO::getChatHistoryConf, AgentUpdateDTO::getChatHistoryConf,
(agent, data) -> agent.setChatHistoryConf(data.getChatHistoryConf())),
SYSTEM_PROMPT("systemPrompt", AgentSnapshotDataDTO::getSystemPrompt, AgentUpdateDTO::getSystemPrompt,
(agent, data) -> agent.setSystemPrompt(data.getSystemPrompt())),
SUMMARY_MEMORY("summaryMemory", AgentSnapshotDataDTO::getSummaryMemory, AgentUpdateDTO::getSummaryMemory,
(agent, data) -> agent.setSummaryMemory(data.getSummaryMemory())),
LANG_CODE("langCode", AgentSnapshotDataDTO::getLangCode, AgentUpdateDTO::getLangCode,
(agent, data) -> agent.setLangCode(data.getLangCode())),
LANGUAGE("language", AgentSnapshotDataDTO::getLanguage, AgentUpdateDTO::getLanguage,
(agent, data) -> agent.setLanguage(data.getLanguage())),
SORT("sort", AgentSnapshotDataDTO::getSort, AgentUpdateDTO::getSort,
(agent, data) -> agent.setSort(data.getSort())),
FUNCTIONS("functions", AgentSnapshotDataDTO::getFunctions, AgentUpdateDTO::getFunctions, null),
CONTEXT_PROVIDERS("contextProviders", AgentSnapshotDataDTO::getContextProviders,
AgentUpdateDTO::getContextProviders, null),
CORRECT_WORD_FILE_IDS("correctWordFileIds", AgentSnapshotDataDTO::getCorrectWordFileIds,
AgentUpdateDTO::getCorrectWordFileIds, null),
TAG_NAMES("tagNames", AgentSnapshotDataDTO::getTagNames, AgentUpdateDTO::getTagNames, null);
private final String fieldName;
private final Function<AgentSnapshotDataDTO, Object> snapshotGetter;
private final Function<AgentUpdateDTO, Object> updateGetter;
private final BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier;
AgentSnapshotField(String fieldName, Function<AgentSnapshotDataDTO, Object> snapshotGetter,
Function<AgentUpdateDTO, Object> updateGetter,
BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier) {
this.fieldName = fieldName;
this.snapshotGetter = snapshotGetter;
this.updateGetter = updateGetter;
this.restoreApplier = restoreApplier;
}
public static List<String> names() {
return Arrays.stream(values()).map(AgentSnapshotField::getFieldName).toList();
}
public static String canonicalName(String fieldName) {
return "tags".equals(fieldName) ? TAG_NAMES.getFieldName() : fieldName;
}
public Object snapshotValue(AgentSnapshotDataDTO data) {
return data == null ? null : snapshotGetter.apply(data);
}
public Object updateValue(AgentUpdateDTO data) {
return data == null ? null : updateGetter.apply(data);
}
public boolean isRestorableAgentField() {
return restoreApplier != null;
}
public void applyTo(AgentEntity agent, AgentSnapshotDataDTO data) {
if (restoreApplier != null) {
restoreApplier.accept(agent, data);
}
}
}
@@ -19,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
@@ -28,10 +26,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -41,22 +42,15 @@ import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.dto.AgentTagDTO;
import xiaozhi.modules.agent.entity.AgentTagEntity;
import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentChatSummaryService;
import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体管理")
@@ -64,17 +58,38 @@ import xiaozhi.modules.security.user.SecurityUser;
@RestController
@RequestMapping("/agent")
public class AgentController {
private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L;
private final AgentService agentService;
private final AgentTemplateService agentTemplateService;
private final DeviceService deviceService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService;
private final AgentPluginMappingService agentPluginMappingService;
private final AgentContextProviderService agentContextProviderService;
private final AgentChatSummaryService agentChatSummaryService;
private final RedisUtils redisUtils;
private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
private void requireAgentPermission(String agentId) {
if (!agentService.checkAgentPermission(agentId, SecurityUser.getUserId())) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
private String requireSessionAgent(String sessionId) {
String agentId = agentChatHistoryService.getAgentIdBySessionId(sessionId);
if (StringUtils.isBlank(agentId)) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
agentService.getAgentById(agentId);
return agentId;
}
private String requireAudioPermission(String audioId) {
String agentId = agentChatHistoryService.getAgentIdByAudioId(audioId);
if (StringUtils.isBlank(agentId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
requireAgentPermission(agentId);
return agentId;
}
@GetMapping("/list")
@Operation(summary = "获取用户智能体列表")
@@ -106,7 +121,7 @@ public class AgentController {
@Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
AgentInfoVO agent = agentService.getAgentById(id);
AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId());
return ResultUtils.success(agent);
}
@@ -120,20 +135,16 @@ public class AgentController {
@PutMapping("/saveMemory/{macAddress}")
@Operation(summary = "根据设备id更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
return new Result<>();
}
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
return new Result<>();
agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId());
return new Result<Void>().ok(null);
}
@PostMapping("/chat-summary/{sessionId}/save")
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
requireSessionAgent(sessionId);
try {
// 异步执行总结生成任务,立即返回成功响应
new Thread(() -> {
@@ -155,6 +166,7 @@ public class AgentController {
@PostMapping("/chat-title/{sessionId}/generate")
@Operation(summary = "根据会话ID生成聊天标题")
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
requireSessionAgent(sessionId);
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
return new Result<Void>().ok(null);
}
@@ -163,7 +175,7 @@ public class AgentController {
@Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
agentService.updateAgentById(id, dto);
agentService.updateAgentById(id, dto, SecurityUser.getUserId());
return new Result<>();
}
@@ -171,30 +183,10 @@ public class AgentController {
@Operation(summary = "删除智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> delete(@PathVariable String id) {
// 先删除关联的设备
deviceService.deleteByAgentId(id);
// 删除关联的聊天记录
agentChatHistoryService.deleteByAgentId(id, true, true);
// 删除关联的插件
agentPluginMappingService.deleteByAgentId(id);
// 删除关联的上下文源配置
agentContextProviderService.deleteByAgentId(id);
// 删除关联的替换词文件关联记录
correctWordFileService.deleteMappingsByAgentId(id);
// 再删除智能体
agentService.deleteById(id);
agentService.deleteAgentById(id, SecurityUser.getUserId());
return new Result<>();
}
@GetMapping("/template")
@Operation(summary = "智能体模板模板列表")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentTemplateEntity>> templateList() {
List<AgentTemplateEntity> list = agentTemplateService
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
return new Result<List<AgentTemplateEntity>>().ok(list);
}
@GetMapping("/{id}/sessions")
@Operation(summary = "获取智能体会话列表")
@RequiresPermissions("sys:role:normal")
@@ -205,6 +197,7 @@ public class AgentController {
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
@PathVariable("id") String id,
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
requireAgentPermission(id);
params.put("agentId", id);
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
@@ -252,6 +245,7 @@ public class AgentController {
@RequiresPermissions("sys:role:normal")
public Result<String> getContentByAudioId(
@PathVariable("id") String id) {
requireAudioPermission(id);
// 查询聊天记录
String data = agentChatHistoryService.getContentByAudioId(id);
return new Result<String>().ok(data);
@@ -261,12 +255,13 @@ public class AgentController {
@Operation(summary = "获取音频下载ID")
@RequiresPermissions("sys:role:normal")
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
requireAudioPermission(audioId);
byte[] audioData = agentChatAudioService.getAudio(audioId);
if (audioData == null) {
return new Result<String>().error("音频不存在");
}
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId, AUDIO_PLAY_TOKEN_EXPIRE_SECONDS);
return new Result<String>().ok(uuid);
}
@@ -322,6 +317,7 @@ public class AgentController {
@Operation(summary = "获取智能体的标签")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
requireAgentPermission(id);
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
return new Result<List<AgentTagDTO>>().ok(tags);
}
@@ -330,10 +326,14 @@ public class AgentController {
@Operation(summary = "保存智能体的标签")
@RequiresPermissions("sys:role:normal")
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
List<String> tagIds = (List<String>) params.get("tagIds");
List<String> tagNames = (List<String>) params.get("tagNames");
agentTagService.saveAgentTags(id, tagIds, tagNames);
requireAgentPermission(id);
List<String> tagIds = JsonUtils.toList(params.get("tagIds"), String.class);
List<String> tagNames = JsonUtils.toList(params.get("tagNames"), String.class);
AgentUpdateDTO dto = new AgentUpdateDTO();
dto.setTagIds(tagIds);
dto.setTagNames(tagNames);
agentService.updateAgentById(id, dto);
return new Result<Void>().ok(null);
}
}
}
@@ -0,0 +1,79 @@
package xiaozhi.modules.agent.controller;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
import xiaozhi.modules.agent.dto.AgentSnapshotRestoreDTO;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentSnapshotService;
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体快照")
@AllArgsConstructor
@RestController
@RequestMapping("/agent/{agentId}/snapshots")
public class AgentSnapshotController {
private final AgentSnapshotService agentSnapshotService;
private final AgentService agentService;
@GetMapping
@Operation(summary = "获取智能体快照列表")
@RequiresPermissions("sys:role:normal")
public Result<PageData<AgentSnapshotVO>> page(
@PathVariable String agentId,
@ParameterObject AgentSnapshotPageDTO params) {
checkPermission(agentId);
return new Result<PageData<AgentSnapshotVO>>().ok(agentSnapshotService.page(agentId, params));
}
@GetMapping("/{snapshotId}")
@Operation(summary = "获取智能体快照详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentSnapshotVO> getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
checkPermission(agentId);
return new Result<AgentSnapshotVO>().ok(agentSnapshotService.getSnapshot(agentId, snapshotId));
}
@PostMapping("/{snapshotId}/restore")
@Operation(summary = "恢复智能体快照")
@RequiresPermissions("sys:role:normal")
public Result<Void> restore(@PathVariable String agentId, @PathVariable String snapshotId,
@RequestBody @Valid AgentSnapshotRestoreDTO request) {
checkPermission(agentId);
agentSnapshotService.restoreSnapshot(agentId, snapshotId, request.getCurrentStateToken());
return new Result<>();
}
@DeleteMapping("/{snapshotId}")
@Operation(summary = "删除智能体历史快照")
@RequiresPermissions("sys:role:normal")
public Result<Void> deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
checkPermission(agentId);
agentSnapshotService.deleteSnapshot(agentId, snapshotId);
return new Result<>();
}
private void checkPermission(String agentId) {
UserDetail user = SecurityUser.getUser();
if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) {
throw new RenException("没有权限访问该智能体快照");
}
}
}
@@ -41,6 +41,17 @@ public class AgentTemplateController {
private final AgentTemplateService agentTemplateService;
// 单独用 normal 权限(而非类级 superAdmin)是有意保留:
// 普通用户也需查询可用模板,与 /page 的管理入口分离,保持原行为契约。
@GetMapping
@Operation(summary = "智能体模板列表")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentTemplateEntity>> getAgentTemplates() {
List<AgentTemplateEntity> list = agentTemplateService
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
return new Result<List<AgentTemplateEntity>>().ok(list);
}
@GetMapping("/page")
@Operation(summary = "获取模板分页列表")
@RequiresPermissions("sys:role:superAdmin")
@@ -36,4 +36,20 @@ public interface AgentDao extends BaseDao<AgentEntity> {
* @param agentId 智能体ID
*/
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
/**
* 锁定智能体主记录,用于串行化同一智能体的配置写入
*
* @param agentId 智能体ID
*/
AgentEntity selectByIdForUpdate(@Param("agentId") String agentId);
/**
* 精确写入快照覆盖的智能体字段,包括目标快照中的 null 值。
* 不更新所属用户、创建信息等不属于快照的字段。
*
* @param agent 已应用目标快照的智能体
* @return 受影响行数
*/
int updateSnapshotFields(@Param("agent") AgentEntity agent);
}
@@ -0,0 +1,29 @@
package xiaozhi.modules.agent.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
@Mapper
public interface AgentSnapshotDao extends BaseDao<AgentSnapshotEntity> {
Integer selectMaxVersionNo(@Param("agentId") String agentId);
AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId);
AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo);
int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot);
int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit);
List<AgentSnapshotEntity> selectLegacyRedactionBatch(@Param("afterId") String afterId,
@Param("limit") int limit,
@Param("targetRedactionVersion") int targetRedactionVersion);
int updateRedactedSnapshots(@Param("snapshots") List<AgentSnapshotEntity> snapshots,
@Param("redactionVersion") int redactionVersion);
}
@@ -39,7 +39,7 @@ public class AgentDTO {
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String summaryMemory;
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
@@ -50,4 +50,4 @@ public class AgentDTO {
@Schema(description = "标签列表")
private List<AgentTagDTO> tags;
}
}
@@ -14,6 +14,6 @@ public class AgentMemoryDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String summaryMemory;
}
}
@@ -0,0 +1,40 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照数据")
public class AgentSnapshotDataDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String agentCode;
private String agentName;
private String asrModelId;
private String vadModelId;
private String llmModelId;
private String slmModelId;
private String vllmModelId;
private String ttsModelId;
private String ttsVoiceId;
private String ttsLanguage;
private Integer ttsVolume;
private Integer ttsRate;
private Integer ttsPitch;
private String memModelId;
private String intentModelId;
private Integer chatHistoryConf;
private String systemPrompt;
private String summaryMemory;
private String langCode;
private String language;
private Integer sort;
private List<AgentUpdateDTO.FunctionInfo> functions;
private List<ContextProviderDTO> contextProviders;
private List<String> correctWordFileIds;
private List<String> tagNames;
private List<AgentSnapshotTagDTO> tags;
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照分页查询参数")
public class AgentSnapshotPageDTO {
@Schema(description = "当前页码,从1开始", example = "1")
private Integer page = 1;
@Schema(description = "每页数量", example = "10")
private Integer limit = 10;
@Schema(description = "版本锚点,只查询小于等于该版本号的历史快照", example = "20")
private Integer maxVersionNo;
public int pageOrDefault() {
return page == null || page < 1 ? 1 : page;
}
public int limitOrDefault() {
if (limit == null || limit < 1) {
return 10;
}
return limit;
}
}
@@ -0,0 +1,13 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "智能体快照恢复请求")
public class AgentSnapshotRestoreDTO {
@NotBlank
@Schema(description = "预览时由服务端生成的当前配置状态指纹")
private String currentStateToken;
}
@@ -0,0 +1,16 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "智能体快照标签")
public class AgentSnapshotTagDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String tagName;
private Integer sort;
}
@@ -4,9 +4,12 @@ import java.io.Serializable;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.common.utils.JsonUtils;
/**
* 智能体更新DTO
@@ -36,10 +39,10 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
private String slmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
@Schema(description = "VLLM模型标识", example = "vllm_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String vllmModelId;
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
@Schema(description = "语音合成模型标识", example = "tts_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", nullable = true)
@@ -91,15 +94,50 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "替换词文件ID列表", nullable = true)
private List<String> correctWordFileIds;
@Schema(description = "标签名称列表", nullable = true)
private List<String> tagNames;
@Schema(description = "标签ID列表", nullable = true)
private List<String> tagIds;
@Data
@Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable {
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
};
@Schema(description = "插件ID", example = "plugin_01")
private String pluginId;
@Schema(description = "函数参数信息", nullable = true)
private HashMap<String, Object> paramInfo;
private HashMap<String, Object> paramInfo = new HashMap<>();
public void setParamInfo(Object paramInfo) {
this.paramInfo = normalizeParamInfo(paramInfo);
}
private static HashMap<String, Object> normalizeParamInfo(Object paramInfo) {
if (paramInfo == null) {
return new HashMap<>();
}
if (paramInfo instanceof String value) {
if (value.trim().isEmpty()) {
return new HashMap<>();
}
return JsonUtils.parseObject(value, PARAM_INFO_TYPE);
}
if (paramInfo instanceof Map<?, ?> value) {
HashMap<String, Object> normalized = new HashMap<>();
value.forEach((key, val) -> {
if (key != null) {
normalized.put(String.valueOf(key), val);
}
});
return normalized;
}
return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE);
}
private static final long serialVersionUID = 1L;
}
}
}
@@ -1,48 +0,0 @@
package xiaozhi.modules.agent.dto;
import lombok.Data;
/**
* MCP JSON-RPC 响应 DTO
*/
@Data
public class McpJsonRpcResponse {
private String jsonrpc = "2.0";
private Integer id;
private McpResult result;
private McpError error;
public McpJsonRpcResponse() {
}
@Data
public static class McpResult {
private String type;
private String message;
private String agent_id;
private McpTool[] tools;
public McpResult() {
}
}
@Data
public static class McpTool {
private String name;
private String description;
private Object inputSchema;
public McpTool() {
}
}
@Data
public static class McpError {
private Integer code;
private String message;
private Object data;
public McpError() {
}
}
}
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.modules.agent.dto.ContextProviderDTO;
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
@Data
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
private String agentId;
@Schema(description = "上下文源配置")
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = ContextProviderListTypeHandler.class)
private List<ContextProviderDTO> contextProviders;
@Schema(description = "创建者")
@@ -74,7 +74,7 @@ public class AgentEntity {
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String summaryMemory;
@Schema(description = "语言编码")
@@ -97,4 +97,4 @@ public class AgentEntity {
@Schema(description = "更新时间")
private Date updatedAt;
}
}
@@ -0,0 +1,53 @@
package xiaozhi.modules.agent.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_agent_snapshot")
@Schema(description = "智能体配置快照")
public class AgentSnapshotEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "快照ID")
private String id;
@Schema(description = "智能体ID")
private String agentId;
@Schema(description = "所属用户ID")
private Long userId;
@Schema(description = "版本号")
private Integer versionNo;
@Schema(description = "快照数据JSON")
private String snapshotData;
@Schema(description = "变更字段JSON")
private String changedFields;
@Schema(description = "快照来源")
private String source;
@Schema(description = "恢复来源快照ID")
private String restoreFromSnapshotId;
@Schema(description = "恢复来源版本号")
private Integer restoreFromVersionNo;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "快照数据脱敏规则版本")
private Integer redactionVersion;
}
@@ -1,6 +1,6 @@
package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
@@ -11,7 +11,7 @@ import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
* @version 1.0, 2025/5/8
* @since 1.0.0
*/
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
public interface AgentChatAudioService extends IRepository<AgentChatAudioEntity> {
/**
* 保存音频数据
*
@@ -3,9 +3,9 @@ package xiaozhi.modules.agent.service;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.common.page.PageData;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
@@ -18,7 +18,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEntity> {
/**
* 根据智能体ID获取会话列表
@@ -37,6 +37,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
*/
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
/**
* 根据会话ID获取智能体ID
*
* @param sessionId 会话ID
* @return 智能体ID
*/
String getAgentIdBySessionId(String sessionId);
/**
* 根据智能体ID删除聊天记录
*
@@ -62,6 +70,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
*/
String getContentByAudioId(String audioId);
/**
* 根据音频ID获取智能体ID
*
* @param audioId 音频ID
* @return 智能体ID
*/
String getAgentIdByAudioId(String audioId);
/**
* 查询此音频id是否属于此智能体
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
@@ -10,7 +10,7 @@ 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> {
public interface AgentPluginMappingService extends IRepository<AgentPluginMapping> {
/**
* 根据智能体id获取插件参数
@@ -7,6 +7,7 @@ import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
@@ -35,6 +36,15 @@ public interface AgentService extends BaseService<AgentEntity> {
*/
AgentInfoVO getAgentById(String id);
/**
* 根据ID获取当前用户有权访问的智能体
*
* @param id 智能体ID
* @param userId 当前用户ID
* @return 智能体实体
*/
AgentInfoVO getAgentById(String id, Long userId);
/**
* 插入智能体
*
@@ -50,6 +60,13 @@ public interface AgentService extends BaseService<AgentEntity> {
*/
void deleteAgentByUserId(Long userId);
/**
* 删除智能体及其关联数据
*
* @param agentId 智能体ID
*/
void deleteAgent(String agentId);
/**
* 获取用户智能体列表
*
@@ -93,6 +110,41 @@ public interface AgentService extends BaseService<AgentEntity> {
*/
void updateAgentById(String agentId, AgentUpdateDTO dto);
/**
* 更新当前用户有权访问的智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
* @param userId 当前用户ID
*/
void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId);
/**
* 根据设备MAC地址更新当前用户有权访问的智能体记忆
*
* @param macAddress 设备MAC地址
* @param dto 智能体记忆
* @param userId 当前用户ID
*/
void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId);
/**
* 删除当前用户有权访问的智能体
*
* @param agentId 智能体ID
* @param userId 当前用户ID
*/
void deleteAgentById(String agentId, Long userId);
/**
* 更新智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
* @param createSnapshot 是否创建配置快照
*/
void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot);
/**
* 创建智能体
*
@@ -0,0 +1,25 @@
package xiaozhi.modules.agent.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
public interface AgentSnapshotService extends BaseService<AgentSnapshotEntity> {
void createSnapshot(String agentId, String source);
PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params);
AgentSnapshotVO getSnapshot(String agentId, String snapshotId);
void restoreSnapshot(String agentId, String snapshotId, String currentStateToken);
void deleteSnapshot(String agentId, String snapshotId);
Integer getCurrentVersionNo(String agentId);
void deleteByAgentId(String agentId);
long redactLegacySnapshots();
}
@@ -1,7 +1,7 @@
package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
/**
@@ -9,7 +9,7 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
* @createDate 2025-03-22 11:48:18
*/
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
public interface AgentTemplateService extends IRepository<AgentTemplateEntity> {
/**
* 获取默认模板
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
@@ -16,7 +16,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
* @since 1.0.0
*/
@Service
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
public class AgentChatAudioServiceImpl extends CrudRepository<AiAgentChatAudioDao, AgentChatAudioEntity>
implements AgentChatAudioService {
@Override
public String saveAudio(byte[] audioData) {
@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -14,13 +15,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -39,7 +39,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
*/
@Service
@RequiredArgsConstructor
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHistoryDao, AgentChatHistoryEntity>
implements AgentChatHistoryService {
private final AgentChatTitleService agentChatTitleService;
@@ -88,13 +88,26 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
}
@Override
public String getAgentIdBySessionId(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return null;
}
AgentChatHistoryEntity entity = baseMapper.selectOne(
new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getAgentId)
.eq(AgentChatHistoryEntity::getSessionId, sessionId)
.last("LIMIT 1"));
return entity == null ? null : entity.getAgentId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
if (deleteAudio) {
// 分批删除音频,避免超时
List<String> audioIds = baseMapper.getAudioIdsByAgentId(agentId);
if (ToolUtil.isNotEmpty(audioIds)) {
if (CollUtil.isNotEmpty(audioIds)) {
// 每批删除1000条
List<List<String>> batch = ListUtil.split(audioIds, 1000);
batch.forEach(dataList -> {
@@ -154,7 +167,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
// 尝试解析为 JSON
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class);
Map<String, Object> jsonMap = JsonUtils.parseMap(content);
if (jsonMap != null && jsonMap.containsKey("content")) {
Object contentObj = jsonMap.get("content");
return contentObj != null ? contentObj.toString() : content;
@@ -176,6 +189,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
}
@Override
public String getAgentIdByAudioId(String audioId) {
if (audioId == null || audioId.isBlank()) {
return null;
}
AgentChatHistoryEntity entity = baseMapper.selectOne(
new LambdaQueryWrapper<AgentChatHistoryEntity>()
.select(AgentChatHistoryEntity::getAgentId)
.eq(AgentChatHistoryEntity::getAudioId, audioId)
.last("LIMIT 1"));
return entity == null ? null : entity.getAgentId();
}
@Override
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
{
setSummaryMemory(summaryDTO.getSummary());
}
});
}, false);
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
} else {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
@@ -518,4 +518,4 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
return null;
}
}
}
}
@@ -12,11 +12,11 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import cn.hutool.crypto.digest.DigestUtil;
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.Enums.XiaoZhiMcpJsonRpcJson;
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
@@ -76,7 +76,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
List<String> initResponses = client.listenerWithoutClose(response -> {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
// 检查是否有result字段,表示初始化成功
return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
@@ -92,7 +92,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
boolean initSucceeded = false;
for (String response : initResponses) {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
if (jsonMap.containsKey("result")) {
log.info("MCP初始化成功,智能体ID: {}", id);
@@ -123,7 +123,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 等待工具列表响应 (id=2)
List<String> toolsResponses = client.listener(response -> {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"));
} catch (Exception e) {
log.warn("解析工具列表响应失败: {}", response, e);
@@ -134,18 +134,18 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 处理工具列表响应
for (String response : toolsResponses) {
try {
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) {
// 检查是否有result字段
Object resultObj = jsonMap.get("result");
if (resultObj instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) resultObj;
if (resultObj instanceof Map<?, ?>) {
Map<String, Object> resultMap = JsonUtils.toStringObjectMap(resultObj);
Object toolsObj = resultMap.get("tools");
if (toolsObj instanceof List) {
List<Map<String, Object>> toolsList = (List<Map<String, Object>>) toolsObj;
if (toolsObj instanceof List<?>) {
List<Map<String, Object>> toolsList = JsonUtils.toStringObjectMapList(toolsObj);
// 提取工具名称列表
List<String> result = toolsList.stream()
.map(tool -> (String) tool.get("name"))
.map(tool -> String.class.cast(tool.get("name")))
.filter(name -> name != null)
.sorted()
.collect(Collectors.toList());
@@ -226,10 +226,10 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
*/
private static String encryptToken(String agentId, String key) {
// 使用md5对智能体id进行加密
String md5 = HashEncryptionUtil.Md5hexDigest(agentId);
String md5 = DigestUtil.md5Hex(agentId);
// aes需要加密文本
String json = "{\"agentId\": \"%s\"}".formatted(md5);
// 加密后成token值
return AESUtils.encrypt(key, json);
}
}
}
@@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,7 +29,7 @@ import xiaozhi.modules.model.service.ModelConfigService;
@Service
@RequiredArgsConstructor
@Slf4j
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
public class AgentPluginMappingServiceImpl extends CrudRepository<AgentPluginMappingMapper, AgentPluginMapping>
implements AgentPluginMappingService {
private final AgentPluginMappingMapper agentPluginMappingMapper;
private final KnowledgeBaseService knowledgeBaseService;
@@ -15,9 +15,10 @@ 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.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.repository.IRepository;
import cn.hutool.core.collection.CollUtil;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
@@ -29,11 +30,11 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dao.AgentTagDao;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentTagDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
@@ -43,9 +44,10 @@ import xiaozhi.modules.agent.entity.AgentTagEntity;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentSnapshotService;
import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
@@ -56,9 +58,9 @@ import xiaozhi.modules.model.dto.VoiceDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.timbre.service.TimbreService;
@Service
@AllArgsConstructor
@@ -73,9 +75,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final AgentChatHistoryService agentChatHistoryService;
private final AgentTemplateService agentTemplateService;
private final ModelProviderService modelProviderService;
private final AgentContextProviderService agentContextProviderService;
private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
private final AgentContextProviderService agentContextProviderService;
private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
private final AgentSnapshotService agentSnapshotService;
@Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -92,6 +95,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
requireCurrentUserPermissionIfPresent(agent);
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
@@ -106,14 +110,74 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
agent.setContextProviders(contextProviderEntity.getContextProviders());
}
// 查询替换词文件ID列表
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
agent.setCorrectWordFileIds(correctWordFileIds);
// 无需额外查询插件列表,已通过SQL查询出来
return agent;
// 查询替换词文件ID列表
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
agent.setCorrectWordFileIds(correctWordFileIds);
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
// 无需额外查询插件列表,已通过SQL查询出来
return agent;
}
@Override
public AgentInfoVO getAgentById(String id, Long userId) {
AgentInfoVO agent = getAgentById(id);
requireAgentPermission(agent, userId);
return agent;
}
private AgentEntity getAgentEntityOrThrow(String agentId) {
AgentEntity agent = agentDao.selectById(agentId);
if (agent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
return agent;
}
private boolean isCurrentUserSuperAdmin() {
UserDetail user = SecurityUser.getUser();
return user != null && Integer.valueOf(SuperAdminEnum.YES.value()).equals(user.getSuperAdmin());
}
private void requireCurrentUserPermissionIfPresent(AgentEntity agent) {
Long userId = SecurityUser.getUserId();
if (userId != null) {
requireAgentPermission(agent, userId);
}
}
private boolean hasAgentPermission(AgentEntity agent, Long userId) {
if (agent == null) {
return false;
}
if (isCurrentUserSuperAdmin()) {
return true;
}
return userId != null && userId.equals(agent.getUserId());
}
private void requireAgentPermission(AgentEntity agent, Long userId) {
if (!hasAgentPermission(agent, userId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
private boolean hasDevicePermission(DeviceEntity device, Long userId) {
if (device == null) {
return false;
}
if (isCurrentUserSuperAdmin()) {
return true;
}
return userId != null && userId.equals(device.getUserId());
}
private void requireDevicePermission(DeviceEntity device, Long userId) {
if (!hasDevicePermission(device, userId)) {
throw new RenException(ErrorCode.NO_PERMISSION);
}
}
@Override
public boolean insert(AgentEntity entity) {
// 如果ID为空,自动生成一个UUID作为ID
@@ -134,15 +198,35 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
return super.insert(entity);
}
@Override
public void deleteAgentByUserId(Long userId) {
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("user_id", userId);
baseDao.delete(wrapper);
}
@Override
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgentByUserId(Long userId) {
List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
.select("id")
.eq("user_id", userId));
for (AgentEntity agent : agents) {
deleteAgent(agent.getId());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgent(String agentId) {
if (agentDao.selectByIdForUpdate(agentId) == null) {
return;
}
deviceService.deleteByAgentId(agentId);
agentChatHistoryService.deleteByAgentId(agentId, true, true);
agentPluginMappingService.deleteByAgentId(agentId);
agentContextProviderService.deleteByAgentId(agentId);
correctWordFileService.deleteMappingsByAgentId(agentId);
agentTagService.deleteAgentTags(agentId);
agentSnapshotService.deleteByAgentId(agentId);
deleteById(agentId);
}
@Override
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
@@ -159,13 +243,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.map(DeviceEntity::getAgentId)
.distinct()
.collect(Collectors.toList());
if (ToolUtil.isNotEmpty(agentIds)) {
if (CollUtil.isNotEmpty(agentIds)) {
w.or().in("id", agentIds);
}
// 按标签名搜索
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
if (ToolUtil.isNotEmpty(tagAgentIds)) {
if (CollUtil.isNotEmpty(tagAgentIds)) {
w.or().in("id", tagAgentIds);
}
});
@@ -207,7 +291,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 获取标签列表
List<AgentTagEntity> tags = agentTagDao.selectByAgentId(agent.getId());
if (ToolUtil.isNotEmpty(tags)) {
if (CollUtil.isNotEmpty(tags)) {
dto.setTags(tags.stream().map(this::convertTagToDTO).collect(Collectors.toList()));
}
@@ -253,42 +337,56 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
}
@Override
public boolean checkAgentPermission(String agentId, Long userId) {
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
return false;
}
// 获取智能体信息
AgentEntity agent = getAgentById(agentId);
if (agent == null) {
return false;
}
// 如果是超级管理员,直接返回true
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
return true;
}
// 检查是否是智能体的所有者
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 RenException(ErrorCode.AGENT_NOT_FOUND);
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
public boolean checkAgentPermission(String agentId, Long userId) {
AgentEntity agent = agentDao.selectById(agentId);
return hasAgentPermission(agent, userId);
}
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
updateAgentById(agentId, dto, true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) {
updateAgentById(agentId, dto, userId, true);
}
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) {
updateAgentById(agentId, dto, null, createSnapshot);
}
private void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId, boolean createSnapshot) {
AgentEntity lockedAgent = agentDao.selectByIdForUpdate(agentId);
if (lockedAgent == null) {
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
}
if (userId == null) {
requireCurrentUserPermissionIfPresent(lockedAgent);
} else {
requireAgentPermission(lockedAgent, userId);
}
// 锁定后查询现有实体和关联配置
AgentEntity existingEntity = this.getAgentById(agentId);
if (createSnapshot) {
int currentVersionNo = agentSnapshotService.getCurrentVersionNo(agentId);
agentSnapshotService.createSnapshot(agentId, currentVersionNo == 0 ? "initial" : "current");
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
if (dto.getAsrModelId() != null) {
existingEntity.setAsrModelId(dto.getAsrModelId());
}
@@ -385,10 +483,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.toList();
if (!toUpdate.isEmpty()) {
agentPluginMappingService.updateBatchById(toUpdate);
agentPluginMappingService.updateBatchById(toUpdate, IRepository.DEFAULT_BATCH_SIZE);
}
if (!toInsert.isEmpty()) {
agentPluginMappingService.saveBatch(toInsert);
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
}
// 5. 删除本次不在提交列表里的插件映射
@@ -397,7 +495,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.map(AgentPluginMapping::getId)
.toList();
if (!toDelete.isEmpty()) {
agentPluginMappingService.removeBatchByIds(toDelete);
agentPluginMappingService.removeByIds(toDelete);
}
}
@@ -426,17 +524,48 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
}
// 更新替换词文件关联
if (dto.getCorrectWordFileIds() != null) {
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
}
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
if (!b) {
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
}
this.updateById(existingEntity);
}
if (dto.getCorrectWordFileIds() != null) {
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
}
// 更新智能体标签
if (dto.getTagNames() != null || dto.getTagIds() != null) {
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
}
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
if (!b) {
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
}
this.updateById(existingEntity);
if (createSnapshot) {
agentSnapshotService.createSnapshot(agentId, "config");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null || StringUtils.isBlank(device.getAgentId()) || dto == null) {
return;
}
requireDevicePermission(device, userId);
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
updateAgentById(device.getAgentId(), agentUpdateDTO, userId, false);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAgentById(String agentId, Long userId) {
AgentEntity agent = getAgentEntityOrThrow(agentId);
requireAgentPermission(agent, userId);
deleteAgent(agentId);
}
/**
* 验证大语言模型和意图识别的参数是否符合匹配
*
@@ -487,16 +616,22 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
template.setTtsVoiceId(timbre.getId());
}
}
}
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
// 根据记忆模型类型设置默认的chatHistoryConf值
if (template.getMemModelId() != null) {
}
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setTtsLanguage(defaultIfBlank(template.getTtsLanguage(),
timbreModelService.getDefaultLanguageById(entity.getTtsVoiceId())));
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
if (Constant.MEMORY_NO_MEM.equals(entity.getMemModelId())
|| Constant.MEMORY_MEM_REPORT_ONLY.equals(entity.getMemModelId())) {
entity.setSummaryMemory("");
}
// 根据记忆模型类型设置默认的chatHistoryConf值
if (template.getMemModelId() != null) {
if (template.getMemModelId().equals("Memory_nomem")) {
// 无记忆功能的模型,默认不记录聊天记录
entity.setChatHistoryConf(0);
@@ -542,7 +677,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
mapping.setPluginId(pluginId);
Map<String, Object> paramInfo = new HashMap<>();
List<Map<String, Object>> fields = JsonUtils.parseObject(provider.getFields(), List.class);
List<Map<String, Object>> fields = JsonUtils.parseMapList(provider.getFields());
if (fields != null) {
for (Map<String, Object> field : fields) {
paramInfo.put((String) field.get("key"), field.get("default"));
@@ -551,13 +686,18 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
mapping.setParamInfo(JsonUtils.toJsonString(paramInfo));
mapping.setAgentId(entity.getId());
toInsert.add(mapping);
}
// 保存默认插件
agentPluginMappingService.saveBatch(toInsert);
return entity.getId();
}
private String getDefaultLLMModelId() {
}
// 保存默认插件
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
agentSnapshotService.createSnapshot(entity.getId(), "initial");
return entity.getId();
}
private String defaultIfBlank(String value, String defaultValue) {
return StringUtils.isBlank(value) ? defaultValue : value;
}
private String getDefaultLLMModelId() {
try {
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
if (llmConfigs == null || llmConfigs.isEmpty()) {
@@ -576,4 +716,4 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
}
}
}
}
@@ -0,0 +1,59 @@
package xiaozhi.modules.agent.service.impl;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.modules.agent.service.AgentSnapshotService;
@Slf4j
@Component
@RequiredArgsConstructor
public class AgentSnapshotRedactionRunner implements SmartInitializingSingleton {
static final long ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS = 5_000;
static final long ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS = 15_000;
private final AgentSnapshotService agentSnapshotService;
@Override
public void afterSingletonsInstantiated() {
redactAndReport("startup");
}
@Scheduled(initialDelay = ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
fixedDelay = ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS)
public void redactLateRollingDeploymentWrites() {
redactAndReport("rolling-deployment");
}
private void redactAndReport(String trigger) {
long startedAt = System.nanoTime();
try {
long migrated = agentSnapshotService.redactLegacySnapshots();
long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
if (migrated > 0) {
log.warn("Agent snapshot legacy redaction trigger={} migrated={} durationMs={}. Rotate credentials "
+ "that may have appeared in historical snapshot URLs, cookies, sessions, or structured "
+ "headers.", trigger, migrated, durationMillis);
} else if ("startup".equals(trigger)) {
log.info("Agent snapshot legacy redaction startup pass completed: migrated=0 durationMs={}; "
+ "rolling-deployment compensation starts after {} ms and repeats every {} ms.",
durationMillis, ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS);
}
} catch (RuntimeException exception) {
if ("startup".equals(trigger)) {
log.error("Agent snapshot legacy redaction failed during startup; blocking application startup "
+ "before it can accept traffic.", exception);
} else {
log.error("Agent snapshot legacy redaction failed during rolling-deployment compensation; the "
+ "scheduler will retry on its next run.", exception);
}
throw exception;
}
}
}
@@ -133,7 +133,7 @@ public class AgentTagServiceImpl extends BaseServiceImpl<AgentTagDao, AgentTagEn
}
if (tagIds != null && !tagIds.isEmpty()) {
List<AgentTagEntity> tagIdEntities = baseDao.selectBatchIds(tagIds);
List<AgentTagEntity> tagIdEntities = baseDao.selectByIds(tagIds);
for (AgentTagEntity tag : tagIdEntities) {
if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
newTagNames.contains(tag.getTagName()))) {
@@ -4,8 +4,8 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
import xiaozhi.modules.agent.dao.AgentTemplateDao;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentTemplateService;
@@ -21,7 +21,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
* @createDate 2025-03-22 11:48:18
*/
@Service
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
public class AgentTemplateServiceImpl extends CrudRepository<AgentTemplateDao, AgentTemplateEntity>
implements AgentTemplateService {
/**
@@ -22,7 +22,7 @@ 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 com.baomidou.mybatisplus.spring.repository.CrudRepository;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
@@ -46,7 +46,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
*/
@Service
@Slf4j
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity>
public class AgentVoicePrintServiceImpl extends CrudRepository<AgentVoicePrintDao, AgentVoicePrintEntity>
implements AgentVoicePrintService {
private final AgentChatAudioService agentChatAudioService;
private final RestTemplate restTemplate;
@@ -0,0 +1,59 @@
package xiaozhi.modules.agent.typehandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import com.fasterxml.jackson.core.type.TypeReference;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dto.ContextProviderDTO;
/**
* JSON type handler for context providers.
*
* <p>Do not extend MyBatis-Plus {@code AbstractJsonTypeHandler}: its constructor and JSON handler
* contract changed in MyBatis-Plus 3.5.6. {@link BaseTypeHandler} is part of MyBatis itself and
* keeps this handler compatible with both 3.5.5 and newer MyBatis-Plus releases.</p>
*/
public class ContextProviderListTypeHandler extends BaseTypeHandler<List<ContextProviderDTO>> {
private static final TypeReference<List<ContextProviderDTO>> CONTEXT_PROVIDER_LIST_TYPE = new TypeReference<>() {
};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<ContextProviderDTO> parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtils.toJsonString(parameter));
}
@Override
public List<ContextProviderDTO> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseNullable(rs.getString(columnName));
}
@Override
public List<ContextProviderDTO> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseNullable(rs.getString(columnIndex));
}
@Override
public List<ContextProviderDTO> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseNullable(cs.getString(columnIndex));
}
private List<ContextProviderDTO> parseNullable(String json) {
if (StringUtils.isBlank(json)) {
return null;
}
List<ContextProviderDTO> providers = JsonUtils.parseObject(json, CONTEXT_PROVIDER_LIST_TYPE);
return providers == null ? Collections.emptyList() : providers;
}
}
@@ -1,7 +1,5 @@
package xiaozhi.modules.agent.vo;
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;
@@ -20,7 +18,6 @@ import java.util.List;
public class AgentInfoVO extends AgentEntity
{
@Schema(description = "插件列表Id")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AgentPluginMapping> functions;
@Schema(description = "上下文源配置")
@@ -28,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
@Schema(description = "替换词文件ID列表")
private List<String> correctWordFileIds;
@Schema(description = "当前配置版本号")
private Integer currentVersionNo;
}
@@ -0,0 +1,34 @@
package xiaozhi.modules.agent.vo;
import java.util.Date;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
@Data
@Schema(description = "智能体配置快照")
public class AgentSnapshotVO {
private String id;
private String agentId;
@Schema(description = "所属用户ID,表示该快照归属的智能体所有者")
private Long userId;
private Integer versionNo;
private List<String> changedFields;
private List<String> fieldOrder;
private String source;
@Schema(description = "恢复来源快照ID,仅恢复结果版本有值")
private String restoreFromSnapshotId;
@Schema(description = "恢复来源版本号,仅恢复结果版本有值")
private Integer restoreFromVersionNo;
@Schema(description = "创建者,表示触发本次快照写入的操作人")
private Long creator;
private Date createdAt;
private AgentSnapshotDataDTO snapshotData;
private AgentSnapshotDataDTO afterSnapshotData;
@Schema(description = "恢复预览对应的脱敏当前配置,仅详情接口有值")
private AgentSnapshotDataDTO currentSnapshotData;
@Schema(description = "恢复预览对应的当前配置状态指纹,仅详情接口有值")
private String currentStateToken;
}
@@ -10,7 +10,7 @@ public interface ConfigService {
* @param isCache 是否缓存
* @return 配置信息
*/
Object getConfig(Boolean isCache);
Map<String, Object> getConfig(Boolean isCache);
/**
* 获取智能体模型配置
@@ -28,4 +28,4 @@ public interface ConfigService {
* @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"]
*/
List<String> getCorrectWords(String macAddress);
}
}
@@ -65,12 +65,12 @@ public class ConfigServiceImpl implements ConfigService {
private final CorrectWordFileService correctWordFileService;
@Override
public Object getConfig(Boolean isCache) {
public Map<String, Object> getConfig(Boolean isCache) {
if (isCache) {
// 先从Redis获取配置
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
if (cachedConfig != null) {
return cachedConfig;
return JsonUtils.toStringObjectMap(cachedConfig);
}
}
@@ -123,7 +123,7 @@ public class ConfigServiceImpl implements ConfigService {
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
// 管理控制台请求,返回getConfig的结果
redisUtils.delete(redisKey); // 使用后清理
return (Map<String, Object>) getConfig(true);
return getConfig(true);
}
// 根据MAC地址查找设备
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
@@ -277,10 +277,10 @@ public class ConfigServiceImpl implements ConfigService {
// 遍历除最后一个key之外的所有key
for (int i = 0; i < keys.length - 1; i++) {
String key = keys[i];
if (!current.containsKey(key)) {
current.put(key, new HashMap<String, Object>());
}
current = (Map<String, Object>) current.get(key);
Object nestedConfig = current.computeIfAbsent(key, ignored -> new HashMap<String, Object>());
Map<String, Object> nestedMap = JsonUtils.toStringObjectMap(nestedConfig);
current.put(key, nestedMap);
current = nestedMap;
}
// 处理最后一个key
@@ -8,6 +8,7 @@ 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.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -124,7 +125,9 @@ public class DeviceController {
return new Result<Void>().error("设备不存在");
}
BeanUtils.copyProperties(deviceUpdateDTO, entity);
deviceService.updateById(entity);
if (!deviceService.updateById(entity)) {
return new Result<Void>().error(ErrorCode.UPDATE_DATA_FAILED);
}
return new Result<Void>();
}
@@ -171,23 +174,30 @@ public class DeviceController {
@Operation(summary = "获取设备通讯录")
@RequiresPermissions("sys:role:normal")
public Result<Object> getAddressBook(@PathVariable String macAddress) {
if (deviceService.getOwnedDeviceByMacAddress(macAddress) == null) {
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
}
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
}
@GetMapping("/address-book/lookup")
@Operation(summary = "根据昵称查找目标设备")
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
@GetMapping("/address-book/call")
@Operation(summary = "根据昵称发起呼叫")
public Result<Map<String, Object>> callByNickname(String callerMac, String nickname,
@RequestParam(required = false, defaultValue = "false") boolean answer) {
Map<String, Object> result = deviceAddressBookService.callByNickname(callerMac, nickname, answer);
if (result == null) {
return new Result<Map<String, String>>().error("未找到对应设备");
return new Result<Map<String, Object>>().error("未找到对应设备");
}
return new Result<Map<String, String>>().ok(result);
return new Result<Map<String, Object>>().ok(result);
}
@PutMapping("/address-book/alias")
@Operation(summary = "更新设备通讯录别名")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
if (deviceService.getOwnedDeviceByMacAddress(dto.getMacAddress()) == null) {
return new Result<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
return new Result<Void>();
}
@@ -196,14 +206,10 @@ public class DeviceController {
@Operation(summary = "更新设备通讯录权限")
@RequiresPermissions("sys:role:normal")
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
if (deviceService.getOwnedDeviceByMacAddress(dto.getMacAddress()) == null) {
return new Result<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
return new Result<Void>();
}
@GetMapping("/call/forward")
@Operation(summary = "转发呼叫请求到网关")
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
return new Result<Map<String, Object>>().ok(result);
}
}
}
@@ -5,8 +5,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -31,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import cn.hutool.crypto.digest.DigestUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
@@ -288,7 +287,7 @@ public class OTAMagController {
// 返回文件路径
return new Result<String>().ok(filePath.toString());
} catch (IOException | NoSuchAlgorithmException e) {
} catch (IOException e) {
return new Result<String>().error("文件上传失败:" + e.getMessage());
}
}
@@ -329,13 +328,7 @@ public class OTAMagController {
return result;
}
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(file.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
private String calculateMD5(MultipartFile file) throws IOException {
return DigestUtil.md5Hex(file.getBytes());
}
}
@@ -12,6 +12,11 @@ import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
@Mapper
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
/**
* 新增设备通讯录记录
*/
int insertAddressBook(DeviceAddressBookEntity entity);
/**
* 获取设备通讯录列表
*/
@@ -26,4 +31,9 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
* 更新权限
*/
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
}
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
}
@@ -1,27 +0,0 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 设备绑定的DTO
*
* @author zjy
* @since 2025-3-28
*/
@Data
@AllArgsConstructor
@Schema(description = "设备连接头信息")
public class DeviceBindDTO {
@Schema(description = "mac地址")
private String macAddress;
@Schema(description = "所属用户id")
private Long userId;
@Schema(description = "智能体id")
private String agentId;
}
@@ -38,10 +38,15 @@ public interface DeviceAddressBookService {
void refreshCache();
/**
* 根据昵称查找目标设备信息
* 根据昵称发起呼叫
* @param callerMac 主叫方MAC地址
* @param nickname 被叫方昵称
* @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方}
* @param isAnswer 是否为接听模式(跳过权限检查)
*/
Map<String, String> lookupByNickname(String callerMac, String nickname);
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(List<String> macAddresses);
}
@@ -83,6 +83,16 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/
DeviceEntity getDeviceByMacAddress(String macAddress);
/**
* 根据MAC地址获取当前登录用户名下的设备信息
* <p>
* 设备不存在或不属于当前用户时返回null,调用方无需再自行比对归属。
*
* @param macAddress MAC地址
* @return 当前用户名下的设备信息,不存在或无权限时返回null
*/
DeviceEntity getOwnedDeviceByMacAddress(String macAddress);
/**
* 根据设备ID获取激活码
*
@@ -138,10 +148,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
/**
* 转发呼叫请求到网关
* @return 网关响应 {status, message}
*/
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
}
}
@@ -1,20 +1,31 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.hutool.json.JSONUtil;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.sys.service.SysParamsService;
@Service
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
@@ -22,17 +33,23 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
private final DeviceAddressBookDao deviceAddressBookDao;
private final RedisUtils redisUtils;
private final DeviceService deviceService;
private final SysParamsService sysParamsService;
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
DeviceService deviceService) {
@Lazy DeviceService deviceService, SysParamsService sysParamsService) {
this.deviceAddressBookDao = deviceAddressBookDao;
this.redisUtils = redisUtils;
this.deviceService = deviceService;
this.sysParamsService = sysParamsService;
}
@Override
public List<DeviceAddressBookEntity> getAddressBookList(String macAddress) {
return deviceAddressBookDao.getAddressBookList(macAddress);
List<DeviceAddressBookEntity> rows = assignUniqueAliases(deviceAddressBookDao.getAddressBookList(macAddress));
// 取名按固定顺序算完了,前端要看的是最近改过的排前面,放回去
rows.sort(Comparator
.comparing(DeviceAddressBookEntity::getUpdateDate, Comparator.nullsLast(Comparator.reverseOrder())));
return rows;
}
@Override
@@ -47,77 +64,136 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
}
@Override
public Map<String, String> lookupByNickname(String callerMac, String nickname) {
public Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer) {
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
Map<String, String> callerBook = allBooks.get(callerMac.toLowerCase());
if (isAnswer) {
return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听");
}
// 主动呼叫模式
Map<String, String> callerBook = allBooks.get(bookKey(callerMac));
if (callerBook == null) {
return null;
return errorResult("未找到备注为'" + nickname + "'的设备");
}
// 从缓存获取 targetMac,格式: "mac|permission"
String targetMacWithPerm = callerBook.get(nickname);
if (targetMacWithPerm == null) {
return null;
return errorResult("未找到备注为'" + nickname + "'的设备");
}
// 解析 targetMac 和 permission
String[] parts = targetMacWithPerm.split("\\|");
String targetMac = parts[0];
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
if (targetBook == null) {
return null;
if (!hasPermission) {
return errorResult("呼叫失败,您没有权限呼叫该设备");
}
// 检查双向关系:目标设备是否也添加了主叫方为联系人
String callerNickname = targetBook.get(callerMac.toLowerCase());
Map<String, String> result = new HashMap<>();
result.put("targetMac", targetMac);
result.put("callerNickname", callerNickname);
result.put("hasPermission", hasPermission ? "true" : "false");
return result;
// 获取目标设备如何称呼主叫方
Map<String, String> targetBook = allBooks.get(bookKey(targetMac));
String callerNickname = null;
if (targetBook != null) {
callerNickname = targetBook.get(bookKey(callerMac));
}
if (StringUtils.isBlank(callerNickname)) {
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(callerMac);
if (callerDevice != null) {
callerNickname = callerDevice.getAlias();
}
if (StringUtils.isBlank(callerNickname)) {
callerNickname = formatMacAsDeviceName(callerMac);
}
}
return postToMqtt("/api/call/request",
Map.of("caller_mac", callerMac, "target_mac", targetMac, "caller_nickname", callerNickname),
"呼叫");
}
@Override
public void refreshCache() {
Map<String, Map<String, String>> result = new HashMap<>();
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
List<DeviceAddressBookEntity> allRecords = assignUniqueAliases(deviceAddressBookDao.selectList(null));
Map<String, String> reverseMap = new HashMap<>();
Map<String, Boolean> permissionMap = new HashMap<>();
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String alias = entity.getAlias();
Boolean hasPermission = entity.getHasPermission();
if (alias != null && !alias.isEmpty()) {
// 反向记录: (macB, macA) = B对A的称呼
reverseMap.put(macB + ":" + macA, alias);
}
// 记录 A 对 B 是否有权限呼叫
permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission);
String macA = bookKey(entity.getMacAddress());
String macB = bookKey(entity.getTargetMac());
// 构建正向映射: A对B的映射 nickname -> macB|permission
Map<String, String> book = result.computeIfAbsent(macA, k -> new HashMap<>());
String permStr = (Boolean.TRUE.equals(entity.getHasPermission())) ? "1" : "0";
book.put(entity.getAlias(), macB + "|" + permStr);
// 构建反向记录用于查找B对A的称呼,用同一个去重后的名字,两边才对得上
reverseMap.put(macB + ":" + macA, entity.getAlias());
}
// 构建反向映射: B对A的映射 macA -> nickname
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String aliasAtoB = entity.getAlias();
result.computeIfAbsent(macA, k -> new HashMap<>());
result.computeIfAbsent(macB, k -> new HashMap<>());
if (aliasAtoB != null && !aliasAtoB.isEmpty()) {
// A对B的映射: nickname -> macB|permission
Boolean perm = entity.getHasPermission();
String permStr = (perm != null && perm) ? "1" : "0";
result.get(macA).put(aliasAtoB, macB + "|" + permStr);
}
// B对A的映射: macA -> nickname(查反向记录)
String macA = bookKey(entity.getMacAddress());
String macB = bookKey(entity.getTargetMac());
String aliasBtoA = reverseMap.get(macA + ":" + macB);
if (aliasBtoA != null && !aliasBtoA.isEmpty()) {
result.computeIfAbsent(macB, k -> new HashMap<>());
result.get(macB).put(macA, aliasBtoA);
}
}
redisUtils.set(RedisKeys.getAddressBookKey(), result);
}
/**
* 给每行算出一个不重名的称呼,返回排好序、alias 已写好的新列表
*
* 昵称在同一个人的通讯录里是索引,同名后写的会盖掉先写的,按昵称呼会呼错设备。
* 库里本来就有重名:空别名补出来的尾号称呼只有 256 种取值,一本通讯录十几条
* 没起名就撞上了。所以显示和呼叫都得走这一份,
* 不然前端看到的字和呼叫时用的名字对不上,用户照着点的名字呼不到人。
* 不改传入的那个列表:它是 DAO 刚查出来的,调用方还要按自己的顺序用
*/
private List<DeviceAddressBookEntity> assignUniqueAliases(List<DeviceAddressBookEntity> rows) {
// 取名要靠先后定谁拿原名,而 selectList 不带 order by,返回顺序不保证。
// 排一次,同样的数据每次算出来的结果一样,缓存不会来回变。
// 按归一后的地址排:同一个人不管库里是哪种写法都排到一起,
// 后面两级比较器再分先后。同一本里 target_mac 不会重复(主键摆着),
// 所以顺序还是由这三个键定死的,不会掉回"跟着数据库返回顺序走"
List<DeviceAddressBookEntity> ordered = new ArrayList<>(rows);
ordered.sort(Comparator
.comparing((DeviceAddressBookEntity row) -> bookKey(row.getMacAddress()),
Comparator.nullsFirst(Comparator.naturalOrder()))
// 权限排在地址前面:撞名时让对方准许呼我的那台先拿原名。
// 排在后面等于不起作用,两条地址不同时谁先谁后是地址定的,
// 没权限那条照样占住原名,用户照前端看到的名字呼叫命中的就是它,
// 界面上只会说没权限,怎么点都不通
.thenComparing(DeviceAddressBookEntity::getHasPermission,
Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(DeviceAddressBookEntity::getTargetMac, Comparator.nullsFirst(Comparator.naturalOrder())));
Map<String, Set<String>> takenByOwner = new HashMap<>();
for (DeviceAddressBookEntity row : ordered) {
// 建表时 alias 默认是空的,用户没起名就是空的,这是默认状态不是异常。
// 补个尾号称呼顶上,整条跳过的话接口照样返回成功,对方却再也呼不过来
String alias = StringUtils.isBlank(row.getAlias())
? formatMacAsDeviceName(row.getTargetMac())
: row.getAlias();
// 加后缀的写法跟 generateUniqueAlias 保持一致,不搞两套命名规则
Set<String> taken = takenByOwner.computeIfAbsent(bookKey(row.getMacAddress()), k -> new HashSet<>());
String uniqueAlias = alias;
int suffix = 1;
while (taken.contains(uniqueAlias)) {
uniqueAlias = alias + suffix++;
}
taken.add(uniqueAlias);
row.setAlias(uniqueAlias);
}
return ordered;
}
@Override
public void updateAlias(String macAddress, String targetMac, String alias) {
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
String finalAlias = generateUniqueAlias(macAddress, targetMac, alias);
deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias);
refreshCache();
}
@@ -136,16 +212,27 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
entity.setMacAddress(macAddress);
entity.setTargetMac(targetMac);
// 如果 alias 为空,就用默认使用设备名称
// 如果 alias 为空,就默认使用设备名称;对方设备尚未登记时退回尾号称呼
if (StringUtils.isBlank(alias)) {
alias = deviceService.getDeviceByMacAddress(targetMac).getAlias();
DeviceEntity targetDevice = deviceService.getDeviceByMacAddress(targetMac);
if (targetDevice != null) {
alias = targetDevice.getAlias();
}
// 设备登记了但用户没起过名,alias 建表就是 null,这是默认状态
// 不是异常。这时候落个 null 进去,缓存刷新会把整条跳过,
// 接口照样返回成功,对方却永远呼不过来
if (StringUtils.isBlank(alias)) {
alias = formatMacAsDeviceName(targetMac);
}
}
// 检查重名
alias = generateUniqueAlias(macAddress, targetMac, alias);
entity.setAlias(alias);
entity.setHasPermission(hasPermission);
deviceAddressBookDao.insert(entity);
deviceAddressBookDao.insertAddressBook(entity);
} else {
if (alias != null) {
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
updateAlias(macAddress, targetMac, alias);
}
if (hasPermission != null) {
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
@@ -153,4 +240,97 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
}
refreshCache();
}
}
@Override
public void deleteByMacAddresses(List<String> macAddresses) {
if (macAddresses == null || macAddresses.isEmpty()) {
return;
}
deviceAddressBookDao.deleteByMacAddresses(macAddresses);
refreshCache();
}
private Map<String, Object> errorResult(String message) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", message);
return result;
}
private Map<String, Object> postToMqtt(String path, Map<String, Object> body, String action) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
String mqttSignatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|| MqttGatewayAuthorization.isMissingSignatureKey(mqttSignatureKey)) {
result.put("message", action + "失败,网关配置缺失");
return result;
}
try {
String url = "http://" + mqttGatewayUrl + path;
String response = MqttGatewayAuthorization.postJson(
url,
JSONUtil.toJsonStr(body),
mqttSignatureKey,
Instant.now(),
5000);
if (StringUtils.isNotBlank(response)) {
Map<String, Object> gwResult = JSONUtil.parseObj(response);
result.put("status", gwResult.get("status"));
result.put("message", gwResult.get("message"));
}
return result;
} catch (Exception e) {
result.put("message", action + "失败,请稍后再试");
return result;
}
}
/**
* 通讯录里当主人用的地址键
*
* <p>
* 写缓存、查缓存、算称呼都靠它分组,三处必须是同一个值。少归一一次,同一个人的两行
* 会被当成两个人:各自拿到同名,写进同一本时后者盖掉前者,一台设备直接从缓存里消失。
* 库里确实能出现这种数据——表是 utf8mb4_0900_ai_ci,主键 (mac_address, target_mac)
* 大小写不敏感,owner 只差大小写而 target 不同的话两行不撞
*/
private static String bookKey(String macAddress) {
return macAddress == null ? null : macAddress.toLowerCase();
}
private String formatMacAsDeviceName(String mac) {
if (StringUtils.isBlank(mac) || mac.length() < 2) {
return mac;
}
// MAC 本身不区分大小写,落库、查库走的都是小写化的值。
// 这里跟着小写化,不跟着的话同一个地址在两条路径上会算出两个称呼,
// 数据搬到智控台上看到的名字就叫不到人了
String lastTwo = mac.substring(mac.length() - 2).toLowerCase();
return "尾号为" + lastTwo + "的设备";
}
private String generateUniqueAlias(String macAddress, String targetMac, String alias) {
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress);
List<DeviceAddressBookEntity> existing = deviceAddressBookDao.selectList(wrapper);
List<String> existNames = existing.stream()
.map(DeviceAddressBookEntity::getAlias)
.filter(a -> a != null && !a.isEmpty())
.collect(Collectors.toList());
if (!existNames.contains(alias)) {
return alias;
}
int suffix = 1;
String newAlias;
while (existNames.contains(newAlias = alias + suffix)) {
suffix++;
}
return newAlias;
}
}
@@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -30,15 +31,10 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
@@ -55,7 +51,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DevicePageUserDTO;
@@ -63,6 +59,7 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.service.OtaService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
@@ -80,6 +77,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
private final SysParamsService sysParamsService;
private final RedisUtils redisUtils;
private final OtaService otaService;
private final DeviceAddressBookService deviceAddressBookService;
@Async
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
@@ -105,15 +103,15 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
}
String deviceKey = RedisKeys.getOtaActivationCode(activationCode);
Object cacheDeviceId = redisUtils.get(deviceKey);
if (ToolUtil.isEmpty(cacheDeviceId)) {
String cacheDeviceId = (String) redisUtils.get(deviceKey);
if (StringUtils.isBlank(cacheDeviceId)) {
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
}
String deviceId = (String) cacheDeviceId;
String deviceId = cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
if (ToolUtil.isEmpty(cacheMap)) {
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey));
if (MapUtil.isEmpty(cacheMap)) {
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
}
String cachedCode = (String) cacheMap.get("activation_code");
@@ -183,15 +181,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.builder(new HashMap<String, Set<String>>())
.put("clientIds", deviceIds).build();
if (ToolUtil.isNotEmpty(deviceIds)) {
// 发送请求
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(params))
.timeout(10000) // 超时,毫秒
.execute().body();
return resultMessage;
if (CollUtil.isNotEmpty(deviceIds)) {
return postToMqttGateway(url, params);
}
// 返回响应
return "";
@@ -211,8 +202,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
response.setFirmware(firmware);
} else {
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
if (deviceById.getAutoUpdate() != 0) {
// 只有在设备已绑定且明确开启自动升级时才返回固件升级信息
if (Integer.valueOf(1).equals(deviceById.getAutoUpdate())) {
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
@@ -301,24 +292,37 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
List<DeviceEntity> devices = getUserDevices(userId, agentId);
return devices.stream().map(device -> {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
vo.setDeviceType(device.getBoard());
// 设置UTC时间戳供前端使用时区转换
if (device.getLastConnectedAt() != null) {
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
}
return vo;
}).toList();
return devices.stream().map(this::toUserShowDeviceListVO).toList();
}
private UserShowDeviceListVO toUserShowDeviceListVO(DeviceEntity device) {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
vo.setDeviceType(device.getBoard());
vo.setBoard(device.getBoard());
vo.setAutoUpdate(device.getAutoUpdate());
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
return vo;
}
private Long toTimestamp(Date date) {
return date == null ? null : date.getTime();
}
@Override
public void unbindDevice(Long userId, String deviceId) {
// 先查询设备信息,获取agentId
// 先查询设备信息,获取agentId和macAddress
DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) {
return;
}
// 后面删通讯录那步是按 mac_address 删的,不看 user_id。
// 设备本身因为带了 user_id 条件删不掉,通讯录却会照删不误,
// 任意登录用户知道对方的设备 id 就能把对方整本通讯录清空
if (userId == null || device.getUserId() == null || !device.getUserId().equals(userId)) {
return;
}
String macAddress = device.getMacAddress();
if (StringUtils.isNotBlank(device.getAgentId())) {
// 清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
@@ -328,6 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
wrapper.eq("user_id", userId);
wrapper.eq("id", deviceId);
baseDao.delete(wrapper);
// 删除设备相关的通讯录权限记录
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
}
@Override
@@ -346,9 +353,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
public void deleteByAgentId(String agentId) {
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("agent_id", agentId);
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
// 删除设备
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("agent_id", agentId);
baseDao.delete(wrapper);
// 批量删除这些设备相关的所有通讯录权限记录
if (!devices.isEmpty()) {
List<String> macAddresses = devices.stream()
.map(DeviceEntity::getMacAddress)
.collect(Collectors.toList());
deviceAddressBookService.deleteByMacAddresses(macAddresses);
}
}
@Override
@@ -364,17 +385,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
// 循环处理page获取回来的数据,返回需要的字段
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
UserShowDeviceListVO vo = toUserShowDeviceListVO(device);
// 把最后修改的时间,改为简短描述的时间
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
sysUserUtilService.assignUsername(device.getUserId(),
vo::setBindUserName);
vo.setDeviceType(device.getBoard());
vo.setBoard(device.getBoard());
// 设置UTC时间戳供前端使用时区转换
if (device.getLastConnectedAt() != null) {
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
}
return vo;
}).toList();
// 计算页数
@@ -391,6 +406,29 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return baseDao.selectOne(wrapper);
}
@Override
public DeviceEntity getOwnedDeviceByMacAddress(String macAddress) {
if (StringUtils.isBlank(macAddress)) {
return null;
}
// 同一 MAC 可能有两行只差大小写,selectOne 会抛 TooManyResultsException
// 把整个读通讯录接口带崩。按 id 取第一条做归属校验,撞行不报错
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress).orderByAsc("id");
List<DeviceEntity> devices = baseDao.selectList(wrapper);
if (devices == null || devices.isEmpty()) {
return null;
}
DeviceEntity device = devices.get(0);
UserDetail user = SecurityUser.getUser();
// getUser() 在取不到 subject、principal 时返回的是空对象,不是 null,
// 所以这里不会拿到 null。匿名调用走的就是空对象那一条,id 是空的,比不上
if (device.getUserId() == null || !device.getUserId().equals(user.getId())) {
return null;
}
return device;
}
private DeviceReportRespDTO.ServerTime buildServerTime() {
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
TimeZone tz = TimeZone.getDefault();
@@ -404,7 +442,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
public String geCodeByDeviceId(String deviceId) {
String dataKey = getDeviceCacheKey(deviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(dataKey));
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code");
return cachedCode;
@@ -678,20 +716,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return mqtt;
}
/**
* 生成BearerToken
*/
private String generateBearerToken() {
try {
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
if (ToolUtil.isEmpty(signatureKey)) {
return null;
}
return DigestUtil.sha256Hex(dateStr + signatureKey);
} catch (Exception e) {
return null;
}
private String postToMqttGateway(String url, Object requestBody) {
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
return MqttGatewayAuthorization.postJson(
url,
JSONUtil.toJsonStr(requestBody),
signatureKey,
Instant.now());
}
@Override
@@ -752,13 +783,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("payload", payload)
.build();
// 发送请求
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(requestBody))
.timeout(10000) // 超时,毫秒
.execute().body();
String resultMessage = postToMqttGateway(url, requestBody);
// 解析响应
if (StringUtils.isBlank(resultMessage)) {
@@ -849,13 +874,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.put("payload", payload)
.build();
// 发送请求
String resultMessage = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
.body(JSONUtil.toJsonStr(requestBody))
.timeout(10000) // 超时,毫秒
.execute().body();
String resultMessage = postToMqttGateway(url, requestBody);
// 解析响应
if (StringUtils.isNotBlank(resultMessage)) {
@@ -891,70 +910,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
return null;
}
@Override
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", "网关配置缺失");
// 从系统参数获取网关配置
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
log.error("MQTT网关地址未配置");
result.put("message", "MQTT网关地址未配置");
return result;
}
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
log.error("MQTT签名密钥未配置");
result.put("message", "MQTT签名密钥未配置");
return result;
}
// 生成token: SHA256(date + secret)
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
String token = hexString.toString();
// 构建请求体
Map<String, Object> body = new HashMap<>();
body.put("caller_mac", callerMac);
body.put("target_mac", targetMac);
body.put("caller_nickname", callerNickname);
// 发送请求并获取响应
String url = "http://" + mqttGatewayUrl + "/api/call/request";
String response = cn.hutool.http.HttpRequest.post(url)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.timeout(5000)
.execute()
.body();
// 解析网关响应
if (StringUtils.isNotBlank(response)) {
Map<String, Object> gwResult = JSONUtil.parseObj(response);
result.put("status", gwResult.get("status"));
result.put("message", gwResult.get("message"));
}
return result;
} catch (Exception e) {
log.error("转发呼叫请求失败: {}", e.getMessage());
result.put("message", "呼叫请求转发失败: " + e.getMessage());
return result;
}
}
}
@@ -0,0 +1,111 @@
package xiaozhi.modules.device.service.impl;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.List;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
final class MqttGatewayAuthorization {
private static final int HTTP_UNAUTHORIZED = 401;
private static final int DEFAULT_TIMEOUT_MILLIS = 10000;
private MqttGatewayAuthorization() {
}
static String postJson(String url, String jsonBody, String signatureKey, Instant now) {
return postJson(url, jsonBody, signatureKey, now, DEFAULT_TIMEOUT_MILLIS);
}
static String postJson(String url, String jsonBody, String signatureKey, Instant now, int timeoutMillis) {
GatewayResponse response = executeWithDateFallback(
signatureKey,
now,
token -> executeRequest(url, jsonBody, token, timeoutMillis));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new GatewayRequestException(
"MQTT Gateway request failed with HTTP status " + response.statusCode(),
response.statusCode());
}
return response.body();
}
static List<String> generateDailyTokens(String signatureKey, Instant now) {
if (isMissingSignatureKey(signatureKey)) {
throw new GatewayRequestException("MQTT Gateway signature key is empty", null);
}
LocalDate utcDate = now.atZone(ZoneOffset.UTC).toLocalDate();
return List.of(
tokenFor(utcDate, signatureKey),
tokenFor(utcDate.minusDays(1), signatureKey),
tokenFor(utcDate.plusDays(1), signatureKey));
}
static GatewayResponse executeWithDateFallback(String signatureKey, Instant now,
Function<String, GatewayResponse> requestExecutor) {
GatewayResponse lastAuthenticationFailure = null;
for (String token : generateDailyTokens(signatureKey, now)) {
GatewayResponse response = requestExecutor.apply(token);
if (!isAuthenticationFailure(response.statusCode())) {
return response;
}
lastAuthenticationFailure = response;
}
Integer statusCode = lastAuthenticationFailure == null ? null : lastAuthenticationFailure.statusCode();
throw new GatewayRequestException(
"MQTT Gateway rejected all daily authorization tokens"
+ (statusCode == null ? "" : " (HTTP " + statusCode + ")"),
statusCode);
}
private static String tokenFor(LocalDate date, String signatureKey) {
return DigestUtil.sha256Hex(date + signatureKey);
}
private static boolean isAuthenticationFailure(int statusCode) {
return statusCode == HTTP_UNAUTHORIZED;
}
static boolean isMissingSignatureKey(String signatureKey) {
return StringUtils.isBlank(signatureKey) || "null".equalsIgnoreCase(signatureKey.trim());
}
private static GatewayResponse executeRequest(String url, String jsonBody, String token, int timeoutMillis) {
try (HttpResponse response = HttpRequest.post(url)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.header(Header.AUTHORIZATION, "Bearer " + token)
.body(jsonBody)
.timeout(timeoutMillis)
.execute()) {
return new GatewayResponse(response.getStatus(), response.body());
}
}
record GatewayResponse(int statusCode, String body) {
}
static final class GatewayRequestException extends RuntimeException {
private final Integer statusCode;
GatewayRequestException(String message, Integer statusCode) {
super(message);
this.statusCode = statusCode;
}
Integer statusCode() {
return statusCode;
}
}
}
@@ -56,7 +56,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
@Override
public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
baseDao.deleteByIds(Arrays.asList(ids));
}
@Override
@@ -82,4 +82,4 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
.last("LIMIT 1");
return baseDao.selectOne(wrapper);
}
}
}
@@ -32,17 +32,20 @@ public class UserShowDeviceListVO {
@Schema(description = "设备别名")
private String alias;
@Schema(description = "开启OTA")
private Integer otaUpgrade;
@Schema(description = "自动更新开关(0关闭/1开启)")
private Integer autoUpdate;
@Schema(description = "最近对话时间")
private String recentChatTime;
@Schema(description = "最后连接时间(UTC毫秒)")
@Schema(description = "最后连接时间戳(毫秒)", type = "string", example = "1783689702000")
private Long lastConnectedAtTimestamp;
@Schema(description = "绑定时间")
@Schema(description = "绑定时间戳(毫秒)", type = "string", example = "1783689702000")
private Long createDateTimestamp;
@Schema(description = "绑定时间(兼容字段,请使用 createDateTimestamp)", deprecated = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private Date createDate;
}
}
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -23,7 +24,6 @@ import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
import xiaozhi.modules.knowledge.service.KnowledgeManagerService;
@@ -140,7 +140,7 @@ public class KnowledgeBaseController {
List<String> idList = Arrays.asList(ids.split(","));
List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList))
.orElseGet(ArrayList::new);
if (ToolUtil.isNotEmpty(knowledgeBaseDTOs)) {
if (CollUtil.isNotEmpty(knowledgeBaseDTOs)) {
knowledgeBaseDTOs.forEach(item -> {
// 检查权限:用户只能删除自己创建的知识库
if (item.getCreator() == null || !item.getCreator().equals(currentUserId)) {
@@ -160,4 +160,4 @@ public class KnowledgeBaseController {
List<ModelConfigEntity> result = knowledgeBaseService.getRAGModels();
return new Result<List<ModelConfigEntity>>().ok(result);
}
}
}
@@ -1,68 +0,0 @@
package xiaozhi.modules.knowledge.dto;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文档 DTO
*/
@Data
@Schema(description = "知识库文档")
public class DocumentDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "本地ID")
private String id;
@Schema(description = "知识库ID")
private String datasetId;
@Schema(description = "RAGFlow文档ID")
private String documentId;
@Schema(description = "文档名称")
private String name;
@Schema(description = "文件大小")
private Long size;
@Schema(description = "文件类型")
private String type;
@Schema(description = "分块方法")
private String chunkMethod;
@Schema(description = "解析配置")
private Map<String, Object> parserConfig;
@Schema(description = "处理状态 (1:解析中 3:成功 4:失败)")
private Integer status;
@Schema(description = "错误信息")
private String error;
@Schema(description = "分块数量")
private Integer chunkCount;
@Schema(description = "Token数量")
private Long tokenCount;
@Schema(description = "是否启用")
private Integer enabled;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新时间")
private Date updatedAt;
@Schema(description = "上传进度 (虚拟字段)")
private Double progress;
@Schema(description = "缩略图/预览图 (虚拟字段)")
private String thumbnail;
}
@@ -67,4 +67,7 @@ public class KnowledgeBaseDTO implements Serializable {
@Schema(description = "文档数量")
private Integer documentCount;
@Schema(description = "异常提示")
private String errorMessage;
}
@@ -1,421 +0,0 @@
package xiaozhi.modules.knowledge.dto.agent;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "智能体 (Agent) 管理聚合 DTO")
public class AgentDTO {
// ========== 1. Agent 管理 (CRUD) - 对应 RAGFlow_Agent接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 创建请求")
public static class CreateReq implements Serializable {
@Schema(description = "Agent 标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "My Agent")
@NotBlank(message = "Agent 标题不能为空")
@JsonProperty("title")
private String title;
@Schema(description = "DSL 定义 (画布 JSON)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "DSL 定义不能为空")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "描述", example = "这是一个测试 Agent")
@JsonProperty("description")
private String description;
@Schema(description = "头像 URL", example = "http://example.com/avatar.png")
@JsonProperty("avatar")
private String avatar;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 更新请求")
public static class UpdateReq implements Serializable {
@Schema(description = "Agent 标题", example = "Updated Agent")
@JsonProperty("title")
private String title;
@Schema(description = "DSL 定义 (画布 JSON)")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "描述")
@JsonProperty("description")
private String description;
@Schema(description = "头像 URL")
@JsonProperty("avatar")
private String avatar;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 列表请求")
public static class ListReq implements Serializable {
@Schema(description = "页码", defaultValue = "1")
@JsonProperty("page")
@Builder.Default
private Integer page = 1;
@Schema(description = "每页大小", defaultValue = "10")
@JsonProperty("page_size")
@Builder.Default
private Integer pageSize = 10;
@Schema(description = "排序字段", defaultValue = "update_time")
@JsonProperty("orderby")
@Builder.Default
private String orderby = "update_time";
@Schema(description = "是否降序", defaultValue = "true")
@JsonProperty("desc")
@Builder.Default
private Boolean desc = true;
@Schema(description = "Agent ID 过滤")
@JsonProperty("id")
private String id;
@Schema(description = "标题模糊搜索")
@JsonProperty("title")
private String title;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 响应对象")
public static class AgentVO implements Serializable {
@Schema(description = "Agent ID")
@JsonProperty("id")
private String id;
@Schema(description = "标题")
@JsonProperty("title")
private String title;
@Schema(description = "描述")
@JsonProperty("description")
private String description;
@Schema(description = "头像")
@JsonProperty("avatar")
private String avatar;
@Schema(description = "DSL 定义")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "创建者 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "画布分类")
@JsonProperty("canvas_category")
private String canvasCategory;
@Schema(description = "创建时间 (时间戳)")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)")
@JsonProperty("update_time")
private Long updateTime;
}
// ========== 2. Webhook 调试与追踪 - 对应 RAGFlow_Agent接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 触发请求 (参数动态)")
public static class WebhookTriggerReq implements Serializable {
@Schema(description = "输入变量", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "输入变量不能为空")
@JsonProperty("inputs")
private Map<String, Object> inputs;
@Schema(description = "查询词", example = "Hello")
@JsonProperty("query")
private String query;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 追踪请求")
public static class WebhookTraceReq implements Serializable {
@Schema(description = "时间戳游标", example = "1700000000.0")
@JsonProperty("since_ts")
private Double sinceTs;
@Schema(description = "Webhook ID")
@JsonProperty("webhook_id")
private String webhookId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 追踪响应")
public static class WebhookTraceVO implements Serializable {
@Schema(description = "Webhook ID")
@JsonProperty("webhook_id")
private String webhookId;
@Schema(description = "是否结束")
@JsonProperty("finished")
private Boolean finished;
@Schema(description = "下一次查询的时间戳游标")
@JsonProperty("next_since_ts")
private Double nextSinceTs;
@Schema(description = "事件列表")
@JsonProperty("events")
private List<TraceEvent> events;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "追踪事件项")
public static class TraceEvent implements Serializable {
@Schema(description = "时间戳")
@JsonProperty("ts")
private Double ts;
@Schema(description = "事件类型")
@JsonProperty("event")
private String event;
@Schema(description = "事件数据")
@JsonProperty("data")
private Object data;
}
}
// ========== 3. Agent 会话 (Session) - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 创建请求")
public static class SessionCreateReq implements Serializable {
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 列表请求")
public static class SessionListReq implements Serializable {
@Schema(description = "页码", defaultValue = "1")
@JsonProperty("page")
@Builder.Default
private Integer page = 1;
@Schema(description = "每页大小", defaultValue = "10")
@JsonProperty("page_size")
@Builder.Default
private Integer pageSize = 10;
@Schema(description = "排序字段", defaultValue = "create_time")
@JsonProperty("orderby")
@Builder.Default
private String orderby = "create_time";
@Schema(description = "是否降序", defaultValue = "true")
@JsonProperty("desc")
@Builder.Default
private Boolean desc = true;
@Schema(description = "Session ID")
@JsonProperty("id")
private String id;
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "是否返回 DSL")
@JsonProperty("dsl")
@Builder.Default
private Boolean dsl = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 批量删除请求")
public static class SessionBatchDeleteReq implements Serializable {
@Schema(description = "会话 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("ids")
@NotEmpty(message = "ID列表不能为空")
private List<String> ids;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 响应对象")
public static class SessionVO implements Serializable {
@Schema(description = "Session ID")
@JsonProperty("id")
private String id;
@Schema(description = "Agent ID")
@JsonProperty("agent_id")
private String agentId;
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "来源")
@JsonProperty("source")
private String source;
@Schema(description = "DSL 定义")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "消息列表")
@JsonProperty("messages")
private List<Map<String, Object>> messages;
}
// ========== 4. Agent 对话 (Completion) - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Completion 对话请求")
public static class CompletionReq implements Serializable {
@Schema(description = "会话 ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "会话 ID 不能为空")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "用户问题")
@JsonProperty("question")
private String question;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
@Schema(description = "是否返回追踪信息", defaultValue = "false")
@JsonProperty("return_trace")
@Builder.Default
private Boolean returnTrace = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Completion 对话响应")
public static class CompletionVO implements Serializable {
@Schema(description = "会话 ID")
@JsonProperty("id")
private String id;
@Schema(description = "回复内容")
@JsonProperty("content")
private String content;
@Schema(description = "引用来源")
@JsonProperty("reference")
private Map<String, Object> reference;
@Schema(description = "追踪信息")
@JsonProperty("trace")
private List<Object> trace;
}
// ========== 5. Dify 兼容检索 - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Dify 兼容检索请求")
public static class DifyRetrievalReq implements Serializable {
@Schema(description = "知识库 ID")
@JsonProperty("knowledge_id")
private String knowledgeId;
@Schema(description = "查询词")
@JsonProperty("query")
private String query;
@Schema(description = "检索设置")
@JsonProperty("retrieval_setting")
private Map<String, Object> retrievalSetting;
@Schema(description = "元数据过滤条件")
@JsonProperty("metadata_condition")
private Map<String, Object> metadataCondition;
@Schema(description = "是否使用知识图谱")
@JsonProperty("use_kg")
private Boolean useKg;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Dify 兼容检索响应")
public static class DifyRetrievalVO implements Serializable {
@Schema(description = "检索结果列表")
@JsonProperty("records")
private List<Record> records;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "检索记录")
public static class Record implements Serializable {
@Schema(description = "内容")
@JsonProperty("content")
private String content;
@Schema(description = "相似度分数")
@JsonProperty("score")
private Double score;
@Schema(description = "标题")
@JsonProperty("title")
private String title;
@Schema(description = "元数据")
@JsonProperty("metadata")
private Map<String, Object> metadata;
}
}
}
@@ -1,126 +0,0 @@
package xiaozhi.modules.knowledge.dto.bot;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "外部机器人 (Bot) 聚合 DTO")
public class BotDTO {
// ========== 1. SearchBot (检索机器人) ==========
// 对应 /api/v1/searchbots/ask
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "SearchBot 提问请求")
public static class SearchAskReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is RAG?")
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "是否返回引用", defaultValue = "false")
@JsonProperty("quote")
@Builder.Default
private Boolean quote = false;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "SearchBot 提问响应")
public static class SearchAskVO implements Serializable {
@Schema(description = "回答内容")
@JsonProperty("answer")
private String answer;
@Schema(description = "引用来源 (Value 结构通常对应 RetrievalDTO.HitVO)")
@JsonProperty("reference")
private Map<String, Object> reference;
}
// 对应 /api/v1/searchbots/related_questions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "相关问题请求")
public static class RelatedQuestionReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
}
// 对应 /api/v1/searchbots/mindmap
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "思维导图请求")
public static class MindMapReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
}
// ========== 2. AgentBot (嵌入式 Agent) ==========
// 对应 /api/v1/agentbots/{id}/inputs
@Data
@Builder
@AllArgsConstructor
@Schema(description = "AgentBot 输入参数请求")
public static class AgentInputsReq implements Serializable {
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AgentBot 输入参数定义响应")
public static class AgentInputsVO implements Serializable {
@Schema(description = "表单变量定义列表")
@JsonProperty("variables")
private List<Map<String, Object>> variables;
}
// 对应 /api/v1/agentbots/{id}/completions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AgentBot 对话请求")
public static class AgentCompletionReq implements Serializable {
@Schema(description = "输入参数值")
@JsonProperty("inputs")
private Map<String, Object> inputs;
@Schema(description = "用户查询", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "查询内容不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
@Schema(description = "会话 ID")
@JsonProperty("session_id")
private String sessionId;
}
}
@@ -1,50 +0,0 @@
package xiaozhi.modules.knowledge.dto.chat;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 聊天对话请求 DTO (OpenAI 兼容格式)
*/
@Data
@Schema(description = "聊天对话请求")
public class ChatCompletionRequest implements Serializable {
@Schema(description = "模型标识 (对应 agent_id 或 bot_id)", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("model")
private String model;
@Schema(description = "对话消息列表", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("messages")
private List<Message> messages;
@Schema(description = "是否流式返回", defaultValue = "false")
@JsonProperty("stream")
private Boolean stream = false;
@Schema(description = "温度系数 (0-1)", defaultValue = "0.7")
@JsonProperty("temperature")
private Double temperature;
@Schema(description = "Session ID (可选,用于延续会话)")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "其他RAGFlow特定参数 (可选)")
private Map<String, Object> extra;
@Data
public static class Message implements Serializable {
@Schema(description = "角色 (system, user, assistant)", requiredMode = Schema.RequiredMode.REQUIRED)
private String role;
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
}
}
@@ -1,523 +0,0 @@
package xiaozhi.modules.knowledge.dto.chat;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
/**
* 对话管理聚合 DTO
* <p>
* 容器类,内含对话助手、会话和消息的所有请求/响应对象。
* </p>
*/
@Schema(description = "对话管理聚合 DTO")
public class ChatDTO {
// ========== 1. 对话助手 (Assistant/Bot) 相关 ==========
/**
* 提示词配置
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "提示词配置")
public static class PromptConfig implements Serializable {
@Schema(description = "系统提示词", example = "你是一个专业的客服助手...")
@JsonProperty("prompt")
private String systemPrompt;
@Schema(description = "开场白", example = "您好,我是您的智能助手,请问有什么可以帮您?")
private String opener;
@Schema(description = "空结果回复", example = "抱歉,我没有找到相关信息。")
@JsonProperty("empty_response")
private String emptyResponse;
@Schema(description = "是否展示引用", example = "true")
@JsonProperty("show_quote")
private Boolean quote;
@Schema(description = "是否启用 TTS", example = "false")
private Boolean tts;
@Schema(description = "相似度阈值 (0.0 - 1.0)", example = "0.2")
@JsonProperty("similarity_threshold")
private Float similarityThreshold;
@Schema(description = "关键词相似度权重 (0.0 - 1.0)", example = "0.7")
@JsonProperty("keywords_similarity_weight")
private Float vectorSimilarityWeight;
@Schema(description = "检索 Top N", example = "6")
@JsonProperty("top_n")
private Integer topK;
@Schema(description = "Rerank 模型", example = "rerank_model_001")
@JsonProperty("rerank_model")
private String rerankId;
@Schema(description = "是否启用多轮对话优化", example = "false")
@JsonProperty("refine_multiturn")
private Boolean refineMultigraph;
@Schema(description = "变量列表")
private List<Map<String, Object>> variables;
}
/**
* LLM 配置
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "LLM 模型配置")
public static class LLMConfig implements Serializable {
@NotBlank(message = "模型名称不能为空")
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-4")
@JsonProperty("model_name")
private String modelName;
@Schema(description = "温度参数 (0.0 - 2.0)", example = "0.7")
private Float temperature;
@Schema(description = "Top P 采样", example = "0.9")
@JsonProperty("top_p")
private Float topP;
@Schema(description = "最大 Token 数", example = "4096")
@JsonProperty("max_tokens")
private Integer maxTokens;
@Schema(description = "存在惩罚", example = "0.0")
@JsonProperty("presence_penalty")
private Float presencePenalty;
@Schema(description = "频率惩罚", example = "0.0")
@JsonProperty("frequency_penalty")
private Float frequencyPenalty;
}
/**
* 创建助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建助手请求")
public static class AssistantCreateReq implements Serializable {
@NotBlank(message = "助手名称不能为空")
@Schema(description = "助手名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能客服助手")
private String name;
@Schema(description = "助手头像 (Base64 编码)", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "助手描述", example = "这是一个智能客服助手")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
}
/**
* 更新助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "更新助手请求")
public static class AssistantUpdateReq implements Serializable {
@Schema(description = "助手名称", example = "智能客服助手 V2")
private String name;
@Schema(description = "助手头像 (Base64 编码)", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "助手描述", example = "这是一个智能客服助手")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
}
/**
* 查询助手列表请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询助手列表请求")
public static class AssistantListReq implements Serializable {
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "按名称过滤 (模糊匹配)", example = "客服")
private String name;
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
@Schema(description = "按 ID 精确筛选", example = "assistant_001")
private String id;
}
/**
* 助手详情 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "助手详情 VO")
public static class AssistantVO implements Serializable {
@Schema(description = "助手 ID", example = "assistant_001")
private String id;
@Schema(description = "租户 ID", example = "tenant_001")
@JsonProperty("tenant_id")
private String tenantId;
@Schema(description = "助手名称", example = "智能客服助手")
private String name;
@Schema(description = "助手头像", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "关联的知识库列表 (详情)")
private List<SimpleDatasetVO> datasets;
@Schema(description = "助手描述")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
}
/**
* 删除助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "删除助手请求")
public static class AssistantDeleteReq implements Serializable {
@Schema(description = "要删除的助手 ID 列表", example = "[\"assistant_001\", \"assistant_002\"]")
private List<String> ids;
}
// ========== 2. 会话 (Session) 相关 ==========
/**
* 创建会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建会话请求")
public static class SessionCreateReq implements Serializable {
@Schema(description = "会话名称", example = "技术咨询会话")
private String name;
@Schema(description = "用户 ID", example = "user_001")
@JsonProperty("user_id")
private String userId;
}
/**
* 更新会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "更新会话请求")
public static class SessionUpdateReq implements Serializable {
@Schema(description = "会话名称", example = "技术咨询会话 - 更新")
private String name;
}
/**
* 查询会话列表请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询会话列表请求")
public static class SessionListReq implements Serializable {
@Schema(description = "助手 ID", example = "assistant_001")
@JsonProperty("assistant_id")
private String assistantId;
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "按名称过滤", example = "技术")
private String name;
@Schema(description = "排序字段", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
@Schema(description = "会话 ID 精确筛选", example = "session_001")
private String id;
@Schema(description = "用户标识筛选", example = "user_001")
@JsonProperty("user_id")
private String userId;
}
/**
* 会话详情 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "会话详情 VO")
public static class SessionVO implements Serializable {
@Schema(description = "会话 ID", example = "session_001")
private String id;
@Schema(description = "助手 ID", example = "assistant_001")
@JsonProperty("chat_id")
private String chatId;
@Schema(description = "助手 ID (兼容旧版)", example = "assistant_001")
@JsonProperty("assistant_id")
private String assistantId;
@Schema(description = "会话名称", example = "技术咨询会话")
private String name;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "创建日期", example = "2024-05-01 10:00:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新日期", example = "2024-05-01 10:00:00")
@JsonProperty("update_date")
private String updateDate;
@Schema(description = "用户 ID", example = "user_001")
@JsonProperty("user_id")
private String userId;
@Schema(description = "对话历史消息列表")
private List<Map<String, Object>> messages;
}
/**
* 删除会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "删除会话请求")
public static class SessionDeleteReq implements Serializable {
@Schema(description = "要删除的会话 ID 列表", example = "[\"session_001\", \"session_002\"]")
private List<String> ids;
}
// ========== 3. 消息/对话 (Completion) 相关 ==========
/**
* 发送消息请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "发送消息请求")
public static class CompletionReq implements Serializable {
@NotBlank(message = "问题内容不能为空")
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "请介绍一下你们的产品")
private String question;
@Schema(description = "是否使用流式响应 (SSE)", example = "true")
@Builder.Default
private Boolean stream = true;
@NotBlank(message = "会话 ID 不能为空")
@Schema(description = "会话 ID (可选,不传则创建新会话)", example = "session_001")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "是否展示引用", example = "true")
private Boolean quote;
@Schema(description = "指定检索的文档 ID 列表 (逗号分隔)", example = "doc_001,doc_002")
@JsonProperty("doc_ids")
private String docIds;
@Schema(description = "元数据过滤条件")
@JsonProperty("metadata_condition")
private Map<String, Object> metadataCondition;
}
/**
* 消息响应 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "消息响应 VO")
public static class CompletionVO implements Serializable {
@Schema(description = "AI 回答内容")
private String answer;
@Schema(description = "引用信息")
private Reference reference;
@Schema(description = "会话 ID", example = "session_001")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "任务 ID (用于流式响应追踪)", example = "task_001")
@JsonProperty("task_id")
private String taskId;
/**
* 引用信息 (检索命中结果)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "引用信息")
public static class Reference implements Serializable {
@Schema(description = "命中的文档块列表")
private List<xiaozhi.modules.knowledge.dto.document.RetrievalDTO.HitVO> chunks;
@Schema(description = "文档聚合信息")
@JsonProperty("doc_aggs")
private List<DocAgg> docAggs;
}
/**
* 文档聚合信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文档聚合信息")
public static class DocAgg implements Serializable {
@Schema(description = "文档 ID", example = "doc_001")
@JsonProperty("doc_id")
private String docId;
@Schema(description = "文档名称", example = "产品手册.pdf")
@JsonProperty("doc_name")
private String docName;
@Schema(description = "命中次数", example = "3")
private Integer count;
}
}
/**
* 简易知识库 VO (用于 Assistant 列表)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "简易知识库 VO")
public static class SimpleDatasetVO implements Serializable {
@Schema(description = "知识库 ID")
private String id;
@Schema(description = "知识库名称")
private String name;
@Schema(description = "头像")
private String avatar;
@Schema(description = "分块数量")
@JsonProperty("chunk_num")
private Integer chunkNum;
}
}
@@ -1,79 +0,0 @@
package xiaozhi.modules.knowledge.dto.common;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "通用扩展功能 DTO")
public class CommonDTO {
// ========== 1. 引用详情 (detail_share_embedded) ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "引用详情请求")
public static class ReferenceDetailReq implements Serializable {
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "切片 ID 不能为空")
@JsonProperty("chunk_id")
private String chunkId;
@Schema(description = "知识库 ID")
@JsonProperty("knowledge_id")
private String knowledgeId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "引用详情响应")
public static class ReferenceDetailVO implements Serializable {
@Schema(description = "切片 ID")
@JsonProperty("chunk_id")
private String chunkId;
@Schema(description = "完整内容")
@JsonProperty("content_with_weight")
private String contentWithWeight;
@Schema(description = "文档名称")
@JsonProperty("doc_name")
private String docName;
@Schema(description = "图片 ID 列表")
@JsonProperty("img_id")
private String imageId; // 注意:RAGFlow 有时返回 String 有时返回 List,需根据实际情况确认,暂定 String 用于 ID
@Schema(description = "文档 ID")
@JsonProperty("doc_id")
private String docId;
}
// ========== 2. 通用问答 (ask_about) - 调试用 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "通用问答请求 (调试用)")
public static class AskAboutReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is this dataset about?")
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "数据集 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "数据集列表不能为空")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
}
// 响应通常复用 String 或者简单的 Map 结构,视具体实现而定,暂不定义专用 VO
}
@@ -1,363 +0,0 @@
package xiaozhi.modules.knowledge.dto.file;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理聚合 DTO
* <p>
* 容器类,内含文件模块所有请求/响应对象的静态内部类定义。
* </p>
*/
@Schema(description = "文件管理聚合 DTO")
public class FileDTO {
// ========== 请求类 ==========
/**
* 文件上传请求 (对应接口 1: upload)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件上传请求")
public static class UploadReq implements Serializable {
@NotNull(message = "文件不能为空")
@Schema(description = "上传的文件", requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile file;
@Schema(description = "父文件夹 ID (为空则上传到根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
}
/**
* 新建文件夹请求 (对应接口 2: create)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "新建文件夹请求")
public static class CreateReq implements Serializable {
@NotBlank(message = "文件夹名称不能为空")
@Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "新建文件夹")
private String name;
@Schema(description = "父文件夹 ID (为空则创建在根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@NotBlank(message = "类型不能为空")
@Schema(description = "类型: FOLDER", requiredMode = Schema.RequiredMode.REQUIRED, example = "FOLDER")
@Builder.Default
private String type = "FOLDER";
}
/**
* 重命名请求 (对应接口 6: rename)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "重命名请求")
public static class RenameReq implements Serializable {
@NotBlank(message = "文件 ID 不能为空")
@Schema(description = "文件/文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "file_001")
@JsonProperty("file_id")
private String fileId;
@NotBlank(message = "新名称不能为空")
@Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重命名后的文件")
private String name;
}
/**
* 移动请求 (对应接口 7: move)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "移动请求")
public static class MoveReq implements Serializable {
@NotEmpty(message = "源文件 ID 列表不能为空")
@Schema(description = "源文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("src_file_ids")
private List<String> srcFileIds;
@NotBlank(message = "目标文件夹 ID 不能为空")
@Schema(description = "目标文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "folder_002")
@JsonProperty("dest_file_id")
private String destFileId;
}
/**
* 批量删除请求 (对应接口 8: rm)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "批量删除请求")
public static class RemoveReq implements Serializable {
@NotEmpty(message = "文件 ID 列表不能为空")
@Schema(description = "文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("file_ids")
private List<String> fileIds;
}
/**
* 导入知识库请求 (对应接口 9: convert)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "导入知识库请求")
public static class ConvertReq implements Serializable {
@NotEmpty(message = "文件 ID 列表不能为空")
@Schema(description = "文件 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("file_ids")
private List<String> fileIds;
@NotEmpty(message = "知识库 ID 列表不能为空")
@Schema(description = "目标知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"kb_001\"]")
@JsonProperty("kb_ids")
private List<String> kbIds;
}
/**
* 列表查询请求 (对应接口 3: list_files)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "列表查询请求")
public static class ListReq implements Serializable {
@Schema(description = "父文件夹 ID (为空则查询根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@Schema(description = "关键词搜索", example = "文档")
private String keywords;
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "排序字段: create_time / update_time / name / size", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
}
// ========== 响应类 ==========
/**
* 文件/文件夹基础信息 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件/文件夹基础信息")
public static class InfoVO implements Serializable {
@Schema(description = "文件/文件夹 ID", example = "file_001")
private String id;
@Schema(description = "父文件夹 ID", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@Schema(description = "租户 ID", example = "tenant_001")
@JsonProperty("tenant_id")
private String tenantId;
@Schema(description = "创建者 ID", example = "user_001")
@JsonProperty("created_by")
private String createdBy;
@Schema(description = "类型: FOLDER / FILE", example = "FOLDER")
private String type;
@Schema(description = "名称", example = "我的文件夹")
private String name;
@Schema(description = "路径位置", example = "/root/folder")
private String location;
@Schema(description = "文件大小 (字节)", example = "1024")
private Long size;
@Schema(description = "来源类型", example = "local")
@JsonProperty("source_type")
private String sourceType;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
@JsonProperty("update_date")
private String updateDate;
@Schema(description = "文件扩展名", example = "pdf")
private String extension;
}
/**
* 列表响应 VO (对应接口 3: list_files)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件列表响应")
public static class ListVO implements Serializable {
@Schema(description = "总记录数", example = "100")
private Long total;
@Schema(description = "当前父文件夹信息")
@JsonProperty("parent_folder")
private InfoVO parentFolder;
@Schema(description = "文件/文件夹列表")
private List<InfoVO> files;
@Schema(description = "面包屑导航路径")
private List<InfoVO> breadcrumb;
}
/**
* 转换结果项 VO (对应接口 9: convert)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件转换结果项")
public static class ConvertVO implements Serializable {
@Schema(description = "转换记录 ID", example = "convert_001")
private String id;
@Schema(description = "源文件 ID", example = "file_001")
@JsonProperty("file_id")
private String fileId;
@Schema(description = "目标文档 ID", example = "doc_001")
@JsonProperty("document_id")
private String documentId;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
@JsonProperty("update_date")
private String updateDate;
}
/**
* 转换状态 VO (对应接口 10: get_convert_status)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件转换状态")
public static class ConvertStatusVO implements Serializable {
@Schema(description = "转换状态: pending / processing / completed / failed", example = "completed")
private String status;
@Schema(description = "转换进度 (0.0 - 1.0)", example = "1.0")
private Float progress;
@Schema(description = "状态消息", example = "转换完成")
private String message;
}
/**
* 面包屑 VO (对应接口 12: all_parent_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "面包屑导航 (所有父文件夹)")
public static class BreadcrumbVO implements Serializable {
@Schema(description = "父文件夹列表 (从根到当前的路径)")
@JsonProperty("parent_folders")
private List<InfoVO> parentFolders;
}
/**
* 根目录信息 VO (对应接口 10: get_root_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "根目录信息")
public static class RootFolderVO implements Serializable {
@Schema(description = "根文件夹信息")
@JsonProperty("root_folder")
private InfoVO rootFolder;
}
/**
* 父目录信息 VO (对应接口 11: get_parent_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "父目录信息")
public static class ParentFolderVO implements Serializable {
@Schema(description = "父文件夹信息")
@JsonProperty("parent_folder")
private InfoVO parentFolder;
}
}
@@ -149,8 +149,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
// 使用 Jackson 将 DTO 转为 Map 作为查询参数
@SuppressWarnings("unchecked")
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
});
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
@@ -174,13 +174,12 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
.pageSize(1)
.build();
@SuppressWarnings("unchecked")
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
});
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
Object dataObj = response.get("data");
if (dataObj instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
if (dataObj instanceof Map<?, ?> dataMap) {
List<?> documents = (List<?>) dataMap.get("docs");
if (documents != null && !documents.isEmpty()) {
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
@@ -485,16 +484,11 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
@Override
public Integer getDocumentCount(String datasetId) {
try {
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
if (info != null && info.getDocumentCount() != null) {
return info.getDocumentCount().intValue();
}
return 0;
} catch (Exception e) {
log.warn("获取文档数量失败: {}", e.getMessage());
return 0;
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
if (info != null && info.getDocumentCount() != null) {
return info.getDocumentCount().intValue();
}
return 0;
}
@Override
@@ -518,7 +512,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return null;
} catch (Exception e) {
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
return null;
throw convertToRenException(e);
}
}
@@ -572,8 +566,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return new PageData<>(new ArrayList<>(), 0);
}
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
List<Map<String, Object>> documents = (List<Map<String, Object>>) dataMap.get("docs");
Map<?, ?> dataMap = Map.class.cast(dataObj);
List<?> documents = List.class.cast(dataMap.get("docs"));
if (documents == null || documents.isEmpty()) {
// RAGFlow 明确返回了空文档列表,这是合法的"真空"
return new PageData<>(new ArrayList<>(), 0);
@@ -684,7 +678,9 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
}
if (info.getParserConfig() != null) {
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), Map.class));
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(),
new TypeReference<Map<String, Object>>() {
}));
}
return dto;
@@ -722,4 +718,4 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return !multipartFile.isEmpty();
}
}
}
}
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
}
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
if (datasetInfo == null) {
// RAGFlow 端已删除 → 本地级联清理
// RAGFlow 端已确认删除 → 本地级联清理
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
// 标记为已删除,让上层从列表中移除
@@ -145,8 +145,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
}
} catch (Exception e) {
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
dto.setDocumentCount(0);
dto.setErrorMessage(e.getMessage());
}
}
Loaded 100 of 429 files, more files were not shown because too many files have changed in this diff. Show more