改动文件:
ai-frontend/src/ai/utils/useBlockFullscreen.ts(公共 composable:浮层全屏 / Esc 退出 / 锁 body 滚动 / 切换后 onResize 回调)ai-frontend/src/ai/styles/ai.less 里的 .ai-block-fullscreen 浮层基类ai-frontend/src/ai/components/blocks/GraphBlock.vueai-frontend/src/ai/components/blocks/EChartsBlock.vueai-frontend/uno.config.ts要点:
position: fixed + width/height:100%)
而非 Fullscreen API —— 产品可能嵌在 iframe 里,requestFullscreen 会被拒;浮层还能保留头部操作栏与详情栏。.ai-block-fullscreen(ai.less,全局类),各块只在 scoped 样式里补自己的重排规则。calc(var(--ai-z-modal) + 10),低于 --ai-z-toast。await nextTick() → 量 stageRef 尺寸 → graph.refresh(false)(重新测量+居中,不重跑力导向布局)→ graph.zoomToFit()。
注意 refresh() 默认 doLayout=true 会让节点位置跳动,必须传 false。overflow: hidden,退出/卸载还原。viewSize 不在公开 RGOptions 类型里(只在内部 RGOptionsFull),
graph.setOptions({ viewSize }) 会 TS2353 报错。改用 graph.refresh(false) + zoomToFit()。uno.config.ts 的 safelist 必须手工登记动态拼接的图标类。
Icon 组件运行时拼 i-<collection>:<name>,UnoCSS 静态扫描不到 → 图标空白。
本次新增 i-mdi:fullscreen、i-mdi:fullscreen-exit。以后再往 AI 组件里加 mdi 图标,记得同步 safelist。inset(declaration-property-value-disallowed-list),
同时 declaration-block-no-redundant-longhand-properties 又要求简写 —— 二者冲突。
项目既有写法是 position: fixed; top: 0; left: 0; width: 100%; height: 100%;(只给 top/left 两个长手,绕开简写规则)。./node_modules/.bin/vue-tsc --noEmit --skipLibCheck、eslint、stylelint <file> --custom-syntax postcss-html。resize()(useAiChart 返回的 resize);图谱则走 refresh(false) + zoomToFit()。改动:GraphBlock.vue
defaultLineShape: RGLineShape.StandardStraight(原来是 StandardCurve)。.ai-graph__tools):节点搜索(名称/分类/ID,最多 8 条,回车取第一条,Esc 清空,点击外部收起)
flex: 0 1 240px; min-width: 150px,工具条 flex-wrap: wrap,适配内联 900px 列宽。focusId);搜索结果点击后 moveToCenter([node]) 居中(不缩放)。_dataUpdated() 几乎什么都不做:_doSomethingAfterDataUpdated 在
useReactiveDataToAutoUpdateView && !performanceMode 时只调 updateMiniView()。
真正的重绘靠 options 是响应式对象(optionsRef)以及 runtimeDATA4ShouldRenderItems
这个 ref 被赋新数组。所以:
setOptions/updateOptions(canvasZoom/canvasOffset/checkedNodeId…)。
已核实:画布 transform 来自 computed(() => optionsRef.value.canvasOffset/canvasZoom)(RGCanvas),
所以 moveToCenter([node]) → setCanvasCenter → setCanvasOffset → updateOptions({canvasOffset}) 确实会重绘,搜索定位可用。updateNode/updateLine(id, {opacity}) 不可靠:非 performanceMode 下
getShouldRenderNodes()/getShouldRenderLines() 返回的是同一个数组引用,
a.nodes = sameRef 不触发;连线的 lineConfig 还是 computed(() => generateLineConfig(line)),无响应式依赖 → 永久缓存。performanceMode 默认 false。.rg-node-peel[data-id="<id>"],透明度取 --rg-node-opacity.rg-line-peel[data-id="<line id>"],透明度取 --rg-line-opacity(自带 transition: opacity .2s).ai-graph__stage.is-highlight .rg-node-peel 是 (0,3,0) → 无需 !important。.rg-node-peel 的 transition 时要把 transform .15s ease 一起写上,否则会吃掉库的位移动画。foreignObject 里的 .rg-line-peel,同样带 data-id,会被同一条规则压暗。\ 与 "(防 CSS 注入)。getNodeById/getLineById、moveToCenter([node])(居中不缩放)、
focusNodeById(会强制 100% 缩放,慎用)、getNodeRelatedNodes、refresh(false)。defaultLineShape 默认就是 StandardStraight,我们原来显式设成 StandardCurve 反而是偏离默认。useTextOnPath 会被自动降级:useTextOnPath && lineShape !== StandardStraight 才走 <textPath>,
直线时自动改用普通 <text> + translate/rotate(deg),所以设了 defaultLineTextOnPath: true 也不会坏,标签照常显示。curl http://localhost:3100/... 做编译层验证:
curl http://localhost:3100/src/xxx.vue 返回 200 即该 SFC 编译通过(失败会返回 500 + 错误信息)。curl http://localhost:3100/__uno.css,图标类在产物里是转义冒号形式
i-mdi\:fullscreen,用 grep "i-mdi:fullscreen" 搜不到,要搜 i-mdi\\\\:fullscreen 或直接 grep -o "i-mdi[^ ,{]*"。open 一律 SIGTERM(沙箱内、沙箱外都失败),无法做浏览器视觉验证。
node_modules 里也没有 jsdom/happy-dom/playwright。需要视觉验证时只能留一个预览页给用户手动看。
(本次曾建 __preview-graph.html + src/__preview-graph.ts 预览页,已按用户要求删除;下次可临时再建。)graph.refresh is not a function(GraphBlock.vue)<RelationGraph> 上同时写了 ref="graphRef",而 setup 里也用同一个 graphRef
手动持有 @on-ready 给的实例。Vue 的 setRef(@vue/runtime-core,isRef 分支)在每次重渲染
都无条件 ref.value = 组件公开实例,没有"值相同就跳过"的守卫 → 切全屏时 isFullscreen 变化触发重渲染,
实例句柄被覆写成 RelationGraph 组件实例,紧接着 fitAfterResize 调 .refresh 就炸。
之所以只在全屏时暴露:applyData() 是在 onReady 里同步调用的,那一刻实例还没被覆写。ref="graphRef"(并加注释防止回归),所有取用点改走
getGraph() —— 内部做 typeof zoomToFit === 'function' 能力校验,拿不到实例就放弃本次适配
(库自带 ResizeObserver 仍会 resetViewSize(),不会白屏)。@on-ready 编译产物是 onOnReady,与 emit("onReady", inst) 的
toHandlerKey("onReady") 匹配,所以 onReady(graph) 拿到的确实是实例。ai-frontend/src/ai/components/blocks/GraphBlock.vue(fitView/relayout/applyData/focusOnNode 一并收口)。vue-tsc 无 src/ai/ 错误、eslint 干净、stylelint 干净;dev server 产物已无 ref: 绑定。__probe-rg.mjs / __rg-nocss.mjs 已删除。src/call/views/components/CallRecordModal.vue 与
src/trans/views/components/TransRecordModal.vue(都基于 QueryTableModal,
只需 v-model:open + title + z-index + base-query + fetch-api + filter-fields)。
接口:POST /cr/getCallRecord、POST /trans/getTransRecord,personName/otherName 精确匹配。z-index=1015,否则全屏查看时会被 .ai-block-fullscreen(1010)盖住。.rg-line-bg(热区 lineWidth + 6px),
.rg-line-peel 是 pointer-events: none;onLineClick(line, link, nativeEvent) 原生事件在第三个参数。<Icon> 运行时赋类,新增 mdi:phone-outline / mdi:bank-transfer 必须进 uno safelist。vue-tsc 无 src/ai/ 报错、eslint/stylelint 干净、dev server 产物 200 且
onOnLineClick 已挂上,两个新图标已在 /__uno.css 中生成。common/enums/GraphEdgeType.java:CALL("call","通话") / TRANS("trans","交易") / OTHER("other","其他"),
带 from() 宽松匹配(code 或中文、大小写/空白不敏感)与 allowedCodes()。GraphRenderTool.renderGraph 改为解析成 ObjectNode → 校验+规范化 → Json.toStr(root) 返回:
edges[].type 必填,就地 put 成 code;缺失/非法直接报错并把允许取值写进错误信息。AgentService.appendRenderPrompt 第 3 条同步补上 type 字段与「按边实际来自哪张表填、不要猜 label」的说明。GraphBlock.vue:GraphEdge 加 type?;resolveEdgeKind() 先查 type(含中文别名 Map,
用 Map 避免 Record 原型链污染),查不到才退回 detectRecordKindByLabel()(只服务历史消息);
other 不自动开弹框,弹兜底菜单并带说明文案;activeNodeKind 只统计 call/trans。ai-server/src/test/java/com/zsjz/ai/module/agent/tools/GraphRenderToolTest.java(8 例全绿),
含数字 value 不被序列化成字符串的回归断言。mvn -pl ai-server compile SUCCESS、-Dtest=GraphRenderToolTest 8/8 通过;
前端 vue-tsc / eslint / stylelint 干净,dev server 产物 200。agentscope-core-2.0.1-sources.jar(本地仓库有 sources,比翻文档快)+ 官方文档确认。@ToolParam 参数声明成 POJO → 框架用 victools 自动生成嵌套 JSON Schema,并在调用前
用 networknt-schema 校验(required/type/嵌套),错误文案是中文且带 JSON Pointer 路径。String 会被 DefaultToolResultConverter 再 toJson 一次 → LLM 收到
"{\"a\":1}" 这种转义字符串字面量。返回 POJO 或 ToolResultBlock 才是干净 JSON。@Tool(strict=true) 对 OpenAI 原生要求 additionalProperties:false + 全 required,否则 400,
DeepSeek/GLM/Kimi 又丢弃 strict → 不开。Enum.name()(@JsonProperty/@JsonValue 都不影响),
所以 type 保持 String + 工具内归一化。Number 而非 Double(否则整数变 3.0)。module/agent/tools/GraphSpec.java(public 字段 + @ToolParam + @JsonInclude(NON_NULL),
含 Node/Edge/Category 嵌套类),作为 render_graph 的入参契约。GraphRenderTool.renderGraph 签名改为 (GraphSpec graph),返回 ToolResultBlock
(成功 SUCCESS + 干净 JSON 文本,失败 ERROR + 问题清单);语义校验改为操作 POJO。AgentService.appendRenderPrompt 第 3 条删掉手写结构串,改为「入参结构以该工具的 schema 为准」。SqlAnalysisTool 的拒绝话术同步措辞。GraphRenderToolTest 重写为 12 例:语义校验 + 输出形态(干净 JSON、NON_NULL、数字不变形)mvn -pl ai-server compile BUILD SUCCESS(625 文件);-Dtest=GraphRenderToolTest 12/12 通过;
另写探针用真实 Toolkit 走通 schema 生成→校验→绑定→执行 9 个场景(含中文 type 归一化、
缺 required 被框架拦、value 传字符串被拦、旧式字符串入参被拒),结果全部符合预期。SqlAnalysisTool 的 execute_sql / list_tables / render_chart 同样返回 String,
存在同一处二次序列化问题;因后端跑在用户 IDEA 调试实例上(8980 不能重启),未一并改动。type 契约与 value 数字形态不变。module/agent/tools/ToolResults.java(ok(String) / error(String)),
统一 @Tool 返回 ToolResultBlock;GraphRenderTool、SqlAnalysisTool 改用它。SqlAnalysisTool:
execute_sql / list_tables / render_chart 返回值 String → ToolResultBlock
(validateReadOnly 改为返回纯问题描述,不再返回 error JSON);render_chart 的入参 String option → JsonNode option(schema {"type":"object"}),
消除「JSON 字符串套 JSON」的入参转义;containsGraphSeries 签名同步清理。WorkspaceInfoTool(workspaceJson 改静态、去掉未用 error helper)、
RagSchemaSearchTool、PythonAnalysisTool。WorkspaceInfoTool 里那段「把 @Tool 塞进 javadoc」的停用代码改成规整块注释,
并写清恢复步骤(缺 UserMapper/UserEntity 依赖)。AgentService.appendRenderPrompt 第 2 条补「option 是结构化 JSON 对象,按 schema 传,不要传字符串」。SqlAnalysisToolTest(8 例):render_chart 回吐原样 JSON / 拒绝 graph series / 拒绝空值 /
option schema 为 object;execute_sql 白名单与多语句拦截 / 成功干净 JSON / 10 万行上限;
list_tables 干净 JSON。用 Mockito mock mapper,@BeforeAll 里 TableInfoHelper.initTableInfo
解决 can not find lambda cache。mvn -pl ai-server test -Dtest=SqlAnalysisToolTest,GraphRenderToolTest → 20/20 通过,BUILD SUCCESS。
另写全工具探针(真实 Toolkit + 真实工具类 + Mockito 依赖替身)跑通 7 个工具,
全部 state=SUCCESS 且结果文本不含转义引号。ReActAgent.emitToolResultDelta 取 TextBlock 文本)。需求:把 module/call 的业务 service 暴露给 Agent,且不能影响原 HTTP 接口。
已确认的 4 个决策:1:1 细粒度 7 个工具 / 返回复用 resultId 分页契约 / 入参用专用 POJO /
CallOpenInfoService(仅基类 CRUD)跳过。
新增文件:
module/agent/tools/ToolResultTable.java(package-private):从 SqlAnalysisTool 抽出的
列推导 / 单元格清洗(500 截断)/ 分页切片 / 首页 JSON 组装,新增 toRows(List<?>)
(走全局 ObjectMapper,日期与 Long 的字符串形态与 execute_sql 一致)与
store(store, sql, records, page, pageSize) 一步出口。sql 传 null 时该键不输出
(前端 DataTableBlock 是 v-if="data.sql")。module/agent/tools/CallToolSpecs.java:5 个 public 字段 POJO
(CallRecordSpec / NightSpec / ContinuousSpec / SensitiveSummarySpec / SensitiveDetailSpec),
覆盖 7 个工具;不暴露 orderKey/sort/tableName。module/agent/tools/CallAnalysisTool.java:7 个 @Tool
(get_call_records、stat_call_night_summary/detail、stat_call_continuous_summary/detail、
stat_call_sensitive_summary/detail),只做适配、业务仍走原 service。test/.../CallAnalysisToolTest.java:12 例。修改:
SqlAnalysisTool:4 个私有静态方法改为委托 ToolResultTable,公开行为与 JSON 键序不变。AgentService:构造器注入 4 个 call service;buildAgent 注册 CallAnalysisTool;
appendRenderPrompt 第 1 条补入 7 个 call 工具名并强调「专用工具优先于手写 SQL」。AI_AGENT.md:工具清单表补通话 7 件套 + 设计说明。关键实现细节(踩坑点):
SqlResultStore 是 LRU 50 进程级共享)。
取数用 limit = FETCH_LIMIT + 1 让 DB 只回上限内数据,再用 Page#getTotal() 判超限,
超限报错而不是静默截断(截断会让模型基于残缺数据下结论)。
statSecondCallNight 的 SQL 无 LIMIT,只能事后判 size。personName/otherPhone(sensitive 二层、night 二层)只判
!= null,传空串会拼出 = '' 恒空结果。statFirstCallSensitive 的 XML 只读 keyword(otherName = keyword OR otherPhone = keyword),
otherName/otherPhone 在该方法无效;二层反之。工具按此分别映射。selectCallRecordPage 的 XML 只有 personPhones(List),没有单值 personPhone;
前端弹框也是把逗号串转数组传 personPhones。symbol == 'ge' 反而走 duration <= 值(语义反转),
且前端通话记录弹框实际只用 personName/personPhone/otherName/otherPhone/callDirection/日期。get_call_records 内部硬编码 orderKey=callDateTime/sort=DESC(不来自模型)。GovernConfService.getCallTableName() 缓存 key 含 StateManager.getCaseId(),开/关案清缓存,
不会跨案件串表(已核实 StrConsts.cache_get_call_table_name())。String.valueOf(数字也带引号,callCount → "7"),与 execute_sql 同规则。验证:
mvn -pl ai-server test -Dtest=CallAnalysisToolTest,SqlAnalysisToolTest,GraphRenderToolTest
→ 32/32 通过(12 新 + 12 graph + 8 sql),SqlAnalysisToolTest 全绿证明抽取未破坏原契约。properties.query.type == "object"、
整段 schema JSON 不含 orderKey/sort/tableName(锁死不暴露注入面)。TransToolSpecs + TransAnalysisTool(19 个工具)TrackToolSpecs + TrackAnalysisTool(16 个工具)OtgToolSpecs + OtgAnalysisTool(5 个工具)GraphToolSpecs + GraphAnalysisTool(6 个工具)PersonToolSpecs + PersonAnalysisTool(24 个工具)AgentToolRegistry(新增 @Component,注入 30 个业务 service,统一注册 6 个工具组)AgentToolRegistryTest(6 例)AgentService(构造器改为注入 AgentToolRegistry;buildAgent 调
agentToolRegistry.registerBusinessTools(toolkit) + b.enableMetaTool(true);
appendRenderPrompt 第 1 条改写为「按域分组 + reset_equipped_tools 用法」)AI_AGENT.md(新增「业务域工具分组」章节)ToolGroupScope.META,否则 reset_equipped_tools 直接拒绝
("Error: Group 'x' is not manageable by this tool."),且组名不会进该工具
to_activate 的 enum(MetaToolFactory 用 getMetaGroupNames() 填 enum)。getToolSchemas(activeGroups)(ToolSchemaProvider#buildSchemas
只保留「未分组」或「在激活组里」的工具)。ReActAgent 构造时 initialActiveToolGroups = List.copyOf(toolkit.getActiveGroups()),
新会话 freshState 用它填 ToolContextState.activatedGroups ——
所以「默认装备哪一组」只由 AgentToolRegistry#DEFAULT_ACTIVE_GROUPS 决定。reset_equipped_tools 是全量替换语义(replaceMetaActiveGroups)。toolkit.getToolSchemas(state.getToolContext().getActivatedGroups())(会话级),
但会话恢复/结束时用 toolkit.setActiveGroups/getActiveGroups(共享 Toolkit 的全局状态)——
同一 agent 实例并发多会话理论上可能互相覆盖全局状态,已在 AI_AGENT.md 注明为已知限制。person 组(24 个:人员枚举/画像/TOP/汇总/趋势/9 类亲密度),其余 5 组按需激活。
77 个工具若全量进 schema 约 2.5~3 万 token,分组后默认只加载 24+1 个。GraphService 把 startMoney/endMoney/callNum 直接拼进 SQL;
TrackMeetMapper 有 INTERVAL '${query.timeInterval} minutes';
GraphCallDetailQuery.getTransAmountSymbol() 把枚举直接取成字符串拼进 SQL。
以上字段一律声明为 Number 或走 SymbolEnum 白名单转换,绝不透传模型字符串。graph() 里 query.getPersonNames().forEach(...)(null 会 NPE),
且 type 为空直接返回空 Map;type 的交易类(101/102/103)与通话类(201/202/203)
各最多生效一个(else if 链)。工具做了人员库预检(CaseDataCache)。GraphService#transDetail 在 lx ∈ {2,3} 时对 personCardNo 调 contains("|") → 必填。get_case_graph 不表格化(render_graph 要的就是 {nodeList, edgeList} 结构),
且新增了 500 条边的截断保护:超限按 num 降序保留前 500 + 附
truncated/totalEdges/note(原样返回 1 万条边约 30 万 token,会冲爆上下文)。TrackEnLocalService#setTime 把 nightTime/morningTime 拼成 "null:00:00" 时夜间条件恒假
→ 工具层必须兜底默认值(20 / 8)。TrackTogetherTravelService#statSecondTogetherTravel 遍历 query.getIds() → null 会 NPE;
statThreeTogetherTravel 的 switch(type) 对 null 抛 NPE → 工具校验 type 白名单。TrackTogetherLiveService#hotelStayInfoDetail 用 personBasicInfoMapper.selectById(personName)
(人名当主键),togetherLiveInfoDetail 的 eq(queryName, liveWithName) 传 null 恒假 → 两者必填。TrackExpressInfoService#statSecondExpressInfo 的 orderKey 直接进 OrderItem.setColumn
→ 不暴露;statFirstExpressInfo 是内存分页。OtgAnalysisTool:TimeSeriesMapper 的 selectTimeSeriesList 硬编码 limit 20,
applyFetchWindow 对它无效(工具描述已写明一次最多 20 行);
rowIdSymbol 是 ${} 拼接 → 走 SymbolEnum 白名单。TransCashFlowService#statTransCashCallBeforeAfterTimeLine
(已 @Deprecated、前端无引用、gapDay 为 null 会 NPE)。mvn -o -pl ai-server compile → BUILD SUCCESSmvn -o -pl ai-server test -Dtest='AgentToolRegistryTest,CallAnalysisToolTest,GraphRenderToolTest,SqlAnalysisToolTest'
→ 38/38 通过(6 + 12 + 12 + 8)AgentToolRegistryTest 断言:77 个工具全部注册且不重复归组、getActiveGroups() 恰为 [person]、
空激活列表下只剩 reset_equipped_tools、激活 person 组后 call 工具不可见、
多组并集正确、6 个组名都在 to_activate 的 enum 里。AiServerApplicationTests(Spring 上下文)失败:PhoneIspMapper bean 找不到 ——
与本次改动无关(PhoneIspMapper 只被 GlobalCache 用 SpringUtil.getBean 取,无注入点),
属既有问题。git stash push -- <AgentService.java> 时命令被 SIGTERM 中断,
触发了 git 的 gc --auto:repack 删掉了旧 pack 但没写完新 pack,
导致 .git/objects/pack/*.pack 丢失、.git/refs 目录消失、本地 5 个提交(message 都是 "1")的
对象全部不可读。工作区文件未受影响。mkdir .git/refs/{heads,tags,remotes} → 从 .git/logs/refs/heads/dev_1
reflog 取回旧 hash 写回 → 移除指向已丢对象的 ref →
git -c gc.auto=0 -c maintenance.auto=false fetch origin --force --prune --tags 拉回远程对象 →
git update-ref refs/heads/dev_1 refs/remotes/origin/dev_1 → git reset --mixed HEAD 重建 index。8a50fb2(origin/dev_1 的旧点)。agent 模块在 git 里变成未跟踪,
因为承载它的本地提交已丢;代码内容全在工作区,需要时重新提交即可。git stash 这类会触发 gc 的命令;
必要时加 -c gc.auto=0。 本机 maven 也一样:本地仓库在 D:\soft\repository,
必须用 java -classpath <maven>/boot/plexus-classworlds-2.9.0.jar ... classworlds.launcher.Launcher
启动(直接 mvn 会报找不到 Launcher 主类)。AgentToolRegistryTest,8 例)noSchemaLeaksInjectionSurface:6 组全激活后遍历 77 份 schema,断言每份有 description(≥30 字)、
parameters.type == object、序列化 JSON 不含 "orderKey" / "sort" / "tableName" / "keyword"。
sort by 之类描述文本和 search_global 自己的 keyword
业务参数(走 #{} 绑定,不是注入面)误伤,两例失败 → 改成带引号匹配 JSON key 后通过。nestedSpecFieldsAreValidJsonSchemaTypes:抽查 get_call_records / get_trans_records /
get_person_call_profile,确认嵌套 POJO 入参的 query 在 schema 里是 object。TrackAnalysisTool#flatten 静默丢字段(真 bug)parent.remove(nestedKey) 再展开子列表。当嵌套值不是「元素为 Map 的列表」时
(如 TravelTimelineDTO.idList 是 List<String>),该字段被移除后既不展开也不回填 ——
整列静默消失,表格照渲染,测试断言写粗一点根本发现不了。nested instanceof List && !isEmpty && first instanceof Map 才 remove 并展开;
其它形态(字符串数组、单对象、空列表、null)原样以 prefix + key 留在父字段上。TrackAnalysisToolTest(5 例):摊平 + 父字段加前缀防同名覆盖、子列表为空保父行、
非对象列表原样保留、无嵌套键的记录保留、空输入不抛异常。mvn -o -pl ai-server test -Dtest='AgentToolRegistryTest,TrackAnalysisToolTest,CallAnalysisToolTest,GraphRenderToolTest,SqlAnalysisToolTest'
→ 45/45 通过(8 + 5 + 12 + 12 + 8),BUILD SUCCESS。PersonToolSpecs 字段干净,orderKey/sort 只出现在注释里,无注入面泄露。
~/.workbuddy-ai/skills/qingjian-ai-dev/SKILL.md:
flatten 的坑与修法(「只对元素是 Map 的非空列表才 remove 展开」);keyword 走 #{} 不算注入面);TrackAnalysisToolTest。需求:把模型的思考过程也输出到聊天界面上。
ThinkingBlock(消息块)+ ThinkingBlockStart/Delta/EndEvent
(ReActAgent.ModelCallBlockLifecycle 里按 model call 发射,blockId 恒为 "thinking")。
HarnessAgent.streamEvents 直通 ReActAgent,事件不会被过滤。delta.reasoning_content 并转成 ThinkingBlock
(OpenAIResponseParser:432),回传时 OpenAIMessageConverter.convertAssistantMessage 也会
把它写回 reasoningContent —— 所以走 openai: 前缀(本项目 provider=DeepSeek 就是走这条)
一样能拿到思考内容,不需要改成 deepseek: 前缀。{"thinking":{"type":"enabled|disabled"}},
模型名示例就是用户库里配的 deepseek-flash。⇒ 之前模型一直在吐 reasoning_content,只是后端把它丢了。ModelCreationContext.enableThinking 是 AgentScope 的思考开关;DeepSeek/GLM provider 会据此
下发 thinking body param,为 null 时不下发(保持厂商默认)。后端:
module/agent/service/impl/AgentChatServiceImpl.java
ThinkingBlockStart/Delta/EndEvent 分支 → SSE thinking 帧:
{type:'thinking', phase:'start'|'delta'|'end', data?};
多段思考之间后端补 \n\n 分隔。AtomicReference<StringBuilder> accumulatedThinking,随 AgentResultEvent /
doOnCancel 落库。buildThinkingMetadata(String)(→ {"thinking":"..."},空白返回 null)
与 mergeThinkingMetadata(baseJson, thinkingJson)(中断消息保留原标志位)。persistAssistantMessage / persistPartialMessage 增加 thinkingJson 参数,
写入 agent_message.metadata(不新建列,jsonb 直接放)。module/agent/service/AgentModelFactory.java
readEnableThinking(AgentModel):从 agent_model.config 读 enableThinking(true/false),
有值才 .enableThinking(...),缺省/非法 JSON 一律 null(不改既有行为)。{"enableThinking": false}。前端:
ai/api/types.ts:SseEventName 加 'thinking';新增 SseThinkingData;SseHandlers.onThinking。ai/api/chatApi.ts:createParser 增加 case 'thinking'。ai/utils/parseMessageContent.ts:新增 parseThinkingFromMetadata(raw)(安全解析 metadata)。ai/store/chatStream.ts:ChatMessage 增 thinking? / thinkingActive?;
onThinking 按 phase 累积;hydrateMessage 从 metadata 还原(无 thinking 时不写键);
finally / stop() 复位 thinkingActive。ai/components/ThinkingPanel.vue:折叠面板(与 ToolCallPanel 同交互),
思考中自动展开 + 自动滚到底 + 脉冲点,结束后自动收起(用户手动点开过则不收起),
纯空白不渲染。ai/components/ChatMessageItem.vue:面板置于工具面板之上;
StreamStatusBar 增加 thinkingActive prop → 阶段文案「正在深度思考」。mvn -o -pl ai-server compile BUILD SUCCESS。vue-tsc(src/ai 零错误)/ eslint / stylelint 全干净;dev server(:3100)逐模块 200。chatApi SSE 解析 6/6:thinking 生命周期、多段+工具/正文混排、末帧无空行、多字节跨 chunk、CRLF、无 phase。chatStream store 14/14:累积、段间空行、空 delta、裸增量、hydrate 还原(含中断消息)、
无 thinking 不产生空键、非法/缺失 metadata 不抛、stop 复位。messageToMarkdown 只导正文)。thinking 帧。