2026-09-15.md 44 KB

2026-09-15

编写仓库级 AI Agent 协作指南

  • 产出:E:\workspace\zsjz-ai\AI_AGENT.md(仓库根,12 节)
  • 内容:项目定位(清鉴 = 纪检监察/经侦智能资金数据研判平台)、仓库结构、前后端技术栈、 后端分层落位约定、主/从数据源(PG master + DuckDB slave)、Agent 子系统(AgentScope 工具/SSE 协议/硬约束)、 dm 清洗模块、前端域目录约定、常用命令、Do/Don't、已知待清理项、提交检查清单。
  • 关键区分:ai-server/src/main/resources/prompts/AGENTS.md 与 QingJian/agent/AGENTS.md 是产品运行时 AI 人格提示词,不是编码助手约定,勿混改。
  • 发现的问题(已写入文档第 11 节):
    1. common/config/Result.java 有未使用且无依赖的 import io.milvus.param.R;
    2. Result 的 SUCCEED_CODE/FAILURE_CODE 非 final
    3. app.yml 为 Solon 遗留配置,已失效;context-path: "!/js/a/" 语义可疑
    4. ai-server 未见 @MapperScan

环境备忘

  • Bash 工具在本机异常(dirname: command not found,PATH 损坏),改用 Glob/Read/Grep/PowerShell。
  • PowerShell 的 stdout 在该环境下未能回传,目录列表类操作优先用 Glob。

AI 数据分析应用:旧 AI 清除 + 全新实现(前端 3 页 + 后端补口)

旧 AI 代码移除

  • 前端:删 src/cms/(11 文件);App.vue 重写(去掉 AI 悬浮面板全部状态/函数/样式); menu.json 删「AI 助手」IFRAME 节点(10261);package.json 删 @jeesite/cms-lib。
  • 后端:删 templates/chat.html、static/js/chat.js、marked.min.js、static/css/chat.css、 theme.css、static/layui/;static/ 目录整体移除。
  • 文档:AI_AGENT.md §6 前端消费方改为 ai-frontend/src/ai/api/。

★ 关键发现:context-path: "!/js/a/" 导致 ai-server 无法启动

实测(直接 new TomcatServletWebServerFactory().setContextPath(v),Spring Boot 3.5.16):

  • !/js/a/ → IllegalArgumentException: ContextPath must start with '/' and not end with '/'
  • /js/a/ → 同样被拒(尾部斜杠不合法)
  • /js/a → 通过

已改为 context-path: /js/a。结论:接口实际前缀就是 /js/a, 前端 = urlPrefix(/js) + adminPath(/a),与 defHttp 约定一致。 (此前文档把它标为"语义可疑",现已实证。)

后端补充(mvn -pl ai-server compile 通过)

  • sql/agent_chat_session_add_user_id.sql:加 user_id + 索引,不回填(回填会把历史会话判成他人会话)
  • AgentChatSession 加 userId;assertOwner 改比 userId(NULL 历史行宽放+告警); listSessions 加 (user_id = ? OR user_id IS NULL);createSession 写入 userId
  • 新增 AgentResultController(/chat/results/{resultId},裸返回,404=已过期)
  • 新增 AgentPythonFileController(/py/files/{wsId}/{execId}/{file},白名单+normalize 防穿越)
  • PythonExecutor.IMAGE_URL_PREFIX:/api/v1/py/files/ → /js/a/py/files/
  • 顺手修 SqlResultStore 既有缺陷:sql 未存入 CachedResult,导致 PageData.sql 恒 null

前端新模块 src/ai/(27 文件)

  • api/(types/http/chatApi/modelApi)、utils/(constants/scanner/parseMessageContent/reportExport/echartsSetup/useAiChart)、 store/chatStream.ts、styles/ai.less、components/(6 个 blocks + 9 个业务组件)、views/(3 页)
  • 菜单:menu.json 加一级分组 10263 + 子节点 10264/10265/10266; routeHelper.ts 的 explicitDynamicViewMap 加 3 条显式映射

★ 两个技术要点(后续改动务必注意)

  1. 表格是裸 JSON 混在 markdown 正文(后端提示词明确禁止围栏)→ 必须字符级花括号配平扫描 (scanner.ts),不能只按围栏切分。流式未配平时用头部特征 {"resultId" 提前出 pending 占位块。 增量策略:已完成块冻结(尾部 8KB 窗口外)+ 只重扫尾部 + 尾部块按 key 复用对象 (保持 data 引用稳定,否则图表重复 setOption、表格分页被重置)。
  2. UnoCSS 扫不到 menu.json(content.pipeline.include 只含 **/*.{vue,tsx,ts}), 而 Icon 组件是运行时拼 i-<collection>:<name> 类名 → 菜单图标不会生成 CSS(项目既有隐患)。 已在 uno.config.ts 加 safelist 显式列出。

其他踩坑

  • streamdown-vue/style.css 全项目零引入,不引 markdown 无样式(已在 MarkdownBlock 引入)
  • streamdown-vue 默认只放行 http/https 图片,Python 图走同源 /js/a/py/files/ → 必须放行 / 前缀
  • @relation-graph/vue 的 RGOptions 键名是 defaultLineTextOffset_y(下划线), defaultJunctionPoint 是枚举不是字符串;数据靠 graph.setJsonData() 注入
  • 项目 echarts 只注册 Bar/Line/Pie/Map/PictorialBar/Radar + SVGRenderer → 散点/关系图会静默空白
  • 本机 Maven:mvn shell 脚本会把 /d/... 传给 Windows java 导致 classworlds 找不到; 本地仓库是 D:\soft\repository;ai-server/pom.xml 的 <relativePath/> 为空, 需先 mvn -N install 安装父 POM 才能 -pl ai-server 编译
  • katex 未在 package.json 声明(只在 pnpm 虚拟存储),顶层无法解析其 CSS → 数学公式无样式

验证

  • 后端 mvn -pl ai-server compile:BUILD SUCCESS
  • 前端 pnpm type:check 中 src/ai/**:0 错误(项目其他模块有大量既有类型错误)
  • eslint ./src/ai/** + 改动文件:0 问题
  • pnpm build:成功,三页各自独立懒加载 chunk
  • dev server 逐模块冒烟:3 视图 + 15 组件 + 3 utils/store 全部 200,无 transform 错误
  • UnoCSS 白名单生效:产物 CSS 中 i-mdi:history 等含真实内联 SVG

追加:结果集归属隔离 + 文档同步

  • SqlResultStore 加 ownerKey(登录 userId 字符串),get(..., ownerKey) 校验归属, 不匹配/未登录一律 null → 404(刻意不区分"不存在"与"无权限")。 链路:doStream(userId) → getOrCreateAgent → buildAgent(..., ownerKey) → SqlAnalysisTool(..., ownerKey)。 agent 实例本就按 u{userId}-m{modelId} 池化,所以把 userId 绑进工具是安全的。
  • AI_AGENT.md 同步:目录树 cms/→ai/;§6 控制器清单(4 个)+ 工具名纠正 (实际是 execute_python / get_current_workspace / get_user_selected_workspace, 原文写的 python_analysis / workspace_info 是错的)+ 新增"前端渲染管线"映射表; §8 新增"新增页面/菜单的四个坑";§11 context-path 项标记为已实证修复。

环境备忘(补充)

  • 本机 8980 没有服务在跑(curl --noproxy '*' 返回 000;不加 --noproxy 会走沙箱代理得到 502 假象)。
  • 仓库根目录有一个 electric-beacon-darwin-pwxCEck4.md(我的计划/完成记录快照,已被 git add), ~/.workbuddy-ai/plans/ 下的同名文件被系统工具重新生成过 —— 两者内容不同,注意别搞混。

★★ 后端真实联调:发现两个"整仓无法启动"级别的阻塞并修掉一个

PG 192.168.0.109:5432 与 Redis 都可达,于是把 ai-server 真跑起来了。过程中发现:

阻塞 1(已修):mappers XML 全量残留 com.qingjian 包名

  • src/main/resources/mappers/ 下 85 个 XML 全是 com.qingjian.* 命名空间, 而 Java 侧 0 处引用 com.qingjian,有 85 个 com.zsjz.* Mapper 接口(一一对应) → 包名迁移时漏改了 XML。
  • MyBatis 启动即炸:Failed to parse mapping resource: mappers/ai/SkillMapper.xml → Could not resolve type alias 'com.qingjian.common.model.ai.dto.AiStatCallFrequencyTop10'。 即 ai-server 在本仓库根本无法启动。
  • 分析(脚本):144 处唯一引用中 130 处可机械映射 com.qingjian.X → com.zsjz.ai.X(目标类均存在); 14 处无法映射的全部落在 mappers/ai/(6 个不存在的 DTO + 8 个不存在的 Mapper), 该目录是 QingJian 旧 AI 模块,现 AI 能力在 module/agent(MyBatis-Plus BaseMapper,无需 XML)。
  • 修复:删 mappers/ai/(8 文件);其余 77 个文件、160 处引用 com.qingjian → com.zsjz.ai。 文件均在 git 跟踪且改动前无本地修改 → 可 git checkout 完全回退。
  • 之后 mvn -pl ai-server clean compile BUILD SUCCESS,应用成功启动。

阻塞 2(未修,属环境/仓库缺口):缺平台库结构与客户端运行时资产

  • 启动日志已确认:Tomcat started on port 8980 (http) with context path '/js/a' → context-path 修复正确,此前实证的 /js/a 得到运行时验证。
  • 但 AppLoadEndEventListener(CommandLineRunner)随后失败: PSQLException: 关系 "table_info" 不存在。
  • 库结构勘查(写了 JDBC 小工具扫多个库): | 库 | 表数 | table_info/tablefield | agent* | |---|---|---|---| | zsjz-ai(application-dev.yaml 指向它) | 14 | ✗ | ✓ | | etl-2 | 21 | ✓ | ✗ | | lightbot | 48 | ✗ | ✗ | | testai | 2 | ✗ | ✗ | 没有任何库同时具备两者 —— 平台库结构在 etl-2,agent 结构在 zsjz-ai。
  • 启动链还依赖本地运行时资产:GlobalCache.initCache() 需 table_info+table_field、 CaseDataCache 需 person_lib_no、initCaseRocksDbData() 需 RocksDB 目录、 TowerUtils.init() 需基站库文件 —— PathConst.ROOT_PATH = user.dir, 说明 ai-server 是按"客户端应用"设计的,裸跑仓库目录跑不通。
  • 结论:完整端到端联调需要「补齐平台库结构 + 客户端运行时资产」, 超出本次任务范围,未擅自改共享库。已把 sql/agent_chat_session_add_user_id.sql 应用到 zsjz-ai(additive,见下)。

已应用的 DB 变更(唯一一处写共享库)

  • 对 zsjz-ai 执行了 ADD COLUMN user_id BIGINT + COMMENT + CREATE INDEX idx_agent_chat_session_user_case。 均为 additive/幂等;执行前该列不存在、索引不存在。
  • 存量数据现状:4 个会话,user_id 全 NULL、case_id 全有值 → 印证了"NULL 宽放"的设计(回填 case_id 会把这些会话判成他人会话而全部 403)。
  • 库中还有 agent(row_id=1 "数刃")、agent_model(3 条:embedding 1 + llm 2,默认是 Ollama qwen)、 agent_model_provider(4 条:Ollama/OpenAI/通义千问/DeepSeek)。

由联调反查出的 3 个前端真实缺陷(已修)

  1. 模型类型语义搞错:agent_model.type 是用途类型 llm/embedding, 不是厂商协议(协议在 agent_model_provider.type)。我原先把表单选项写成 openai/dashscope/ollama/… → 已改为 llm/embedding。
  2. 状态枚举值搞错:实际是 StatusEnum 的 ACTIVE/SUSPENDED/ARCHIVED/DISABLED/DELETED, 我原先按 '0' 判断停用 → 已改为枚举映射 + 配色。
  3. 聊天模型选择器会把 embedding 模型列出来:库里确实有一条 embedding 模型, 选中后必然失败 → 已过滤 type === 'llm'(默认模型选取也同步过滤)。 另加一道保护:把 embedding 设为默认模型时前端拦截并提示 (后端 getDefaultModelId() 只按 default_model=true AND status=ACTIVE 过滤,不区分 type, 这是个后端待改进点)。

★ 核心逻辑独立验证:196 项断言,挖出 2 个真实 bug 并修掉

项目没有测试框架(无 vitest/jest),但 esbuild 是 devDependency,够用。 做法:esbuild --alias(或 JS API 的 alias + onResolve 插件)把依赖 import.meta.env / axios 的模块换成桩,打包成 CJS 后跑 Node 断言。 (constants.ts 在模块作用域调 useGlobSetting();http.ts 会拉进 axios+antdv+pinia 整条链。) Windows 路径必须 cygpath -w,否则 node/esbuild 把 /c/... 解析到当前盘符。

已验证(196 项)

  • utils/scanner.ts 56 项:围栏(``/~~~/四个反引号/未闭合→pending)、 **裸 JSON 表格识别**(字符串内花括号、多表格、未配平→pending)、HTML/SVG 块、 isTablePayload/findJsonEnd、增量扫描(冻结块**同一对象引用**、key 稳定、data` 引用稳定、 短内容回退全量、流式表格组装)、导出还原。
  • api/chatApi.ts 的 SSE 解析 25 项:mock globalThis.fetch + ReadableStream, 覆盖跨 chunk 分帧、中文/emoji 多字节被切断、\r\n、多行 data、8 种事件、 error 帧、非 SSE 错误响应、末帧无结尾空行、请求体/凭据、Abort 语义。
  • store/chatStream.ts 流式状态机 68 项:pinia 在 Node 下用 setActivePinia(createPinia()) 跑,chatApi 用可编程替身驱动回调。覆盖 init 幂等、自动建会话+乐观插入+流式中状态、token 累积、节流 100ms 中途解析、 工具步骤按 toolCallId 打补丁、intent/suggestions、会话元信息本地更新、 错误帧/传输异常/已有内容不被覆盖、stop() 触发 abort、会话 CRUD、 selectSession 反序列化 toolEvents、本地消息不调接口、reset、无工作空间不发送。
  • utils/reportExport.ts 19 项:单表 sheet 结构与表头、多表分 sheet、 sheet 名唯一/带序号/≤31 字符/不含 Excel 非法字符、无表格返回 null、 缺列定义按行键推断、空行集、撞名去重、空值与缺键容错。
  • SqlResultStore 28 项(后端,直接 java -cp target/classes 跑 main,无需 Spring/DB): ownerKey 归属隔离(本人可读/他人与未登录不可读/不泄露存在性/ownerKey=null 历史结果宽放)、 sql 回传、分页 clamp(超界/0/负数)、自定义 pageSize、空结果、LRU 淘汰、 并发(小规模正确性 + 高竞争下隔离不变量)。

打包上的三个坑

  1. --node-paths(裸包名从项目 node_modules 解析)只有 JS API 支持,CLI 没有; 所以需要它时写 .cjs 脚本 require('绝对路径/esbuild')(脚本在仓库外,ESM 无法用 NODE_PATH)。
  2. 重定向相对导入(如 store 内部的 '../api/chatApi')CLI --alias 匹配不了, 要用 JS API 的 onResolve 插件。
  3. esbuild CLI 失败时错误可能被吞(output: [null,null,null]), 直接调 .pnpm/@esbuild+win32-x64@*/.../esbuild.exe 看真实报错。

挖出并修掉的真实 bug

  1. scanner.ts:围栏正文多带一个结尾换行 → 代码块多一个空行、导出/复制带多余换行。
  2. chatApi.ts:末帧没有结尾空行时整帧被丢弃(SSE 规范允许末帧无空行; createParser 只按 \n\n 分帧,EOF 时没有冲刷缓冲区)→ 会丢 token 文本或 done 事件。 已改为返回 {push, flush},读取循环结束后 parse.push(decoder.decode()) + parse.flush()。

我自己的测试用例也错了 7 处(记录一下,避免下次重复)

  • mock 异步流瞬间 resolve → await sleep(10) 时 send() 已收尾,观察不到"流式中"状态 (mock 必须中途挂起);api.calls[0] 未必是目标调用(前面还有 listSessions); 分页把第 2 页当第 1 页断言;并发用例 1600 次 put 必然触发 LRU(50) 淘汰(与隔离无关); 多行 data: 切在了非法 JSON 位置;等等。

★★★ 移除 sa-token 用户体系 + 端到端打通(本轮)

用户明确:"项目已移除用户体系。移除 sa-token 相关代码。"

1. 移除 sa-token(后端 13 处调用)

  • 3 个 Controller 删 import cn.dev33.satoken.stp.StpUtil 与 13 处 StpUtil.getLoginIdAsLong()
  • AgentChatService 11 个方法签名去掉 Long userId;AgentChatServiceImpl 同步, assertOwner → requireSession(只保留 404,去掉归属判断),listSessions 去掉 user_id 过滤, createSession 去掉 setUserId
  • AgentChatSession 删 userId 字段;ChatSessionVO 删 userId;AgentChatResult 去掉 ownerKey
  • AgentService.getOrCreateAgent 去掉 userId,池化 key 改为 w{ws}-a{agent}-m{model} (原 key u{userId}-m{modelId} 漏了 workspace/agent → 切换案件会复用旧 agent 实例,属真实缺陷)
  • SqlResultStore 去掉 ownerKey 归属隔离(保留 sql 回传修复)
  • 删除 sql/agent_chat_session_add_user_id.sql
  • sa-token 不在任何 pom.xml 中声明(靠传递依赖),所以删完 import 无需改 pom
  • AgentScope 侧仍需"运行时用户标识",统一取常量 RUNTIME_USER_ID = "default" (用于 RuntimeContext.userId() / AgentStateStore 记忆槽位 / usage 统计)

2. ★ 又一个硬阻塞:LicenseFilter 吞掉所有请求

common/config/LicenseFilter.java 的 doFilter() 方法体是空的,从未调用 chain.doFilter, 而它是 @Component implements Filter → Spring Boot 注册到 /*。启动日志实证: Mapping filters: ..., licenseFilter urls=[/*] order=2147483647。 后果:所有 HTTP 请求(含不存在的路径)都返回 200 + 空体,后端"能启动但完全不服务"。 该类无任何其他引用 → 直接删除。修复后不存在的路径正确返回 404。

3. ★ com/zsjz/ai/common/ 目录被删除事件(已恢复)

会话中途发现 ai-server/src/main/java/com/zsjz/ai/common/ 下 243 个文件从工作区消失 (HEAD 中有、工作区 0 个)。非我的 git rm 所致(那次只删了 LicenseFilter 一个文件)。 排查:无 git stash、Code/User/History 无该项目快照、JetBrains LocalHistory 为二进制格式。 处理:git checkout -- <common 目录> 从索引恢复 243 文件,并重新应用两个已知未提交改动:

  • PathConst.RUN_QINGJIAN_PATH = ROOT_PATH.resolve(APP_NAME)(HEAD 里是 = ROOT_PATH)
  • AppLoadEndEventListener 里注释掉 CaseDataCache.initCache() 恢复后 common/ 相对 HEAD 仅剩这 3 处预期差异(含 LicenseFilter 删除),编译 BUILD SUCCESS。 若用户侧有其它未提交改动在该目录内,需其自行核对。

4. 启动方式(关键)

  • 必须从仓库根启动:PathConst.ROOT_PATH = user.dir,而 spring-boot:run 默认以 ai-server 为工作目录(会命中残缺的 ai-server/QingJian/)。需显式: -Dspring-boot.run.workingDirectory=E:/workspace/zsjz-ai
  • 启动日志确认:Tomcat started on port 8980 with context path '/js/a' + Started App
    • initCache success(table_info/table_field 已就位)+ 请先设置基站数据库路径!(TowerUtils 未配,不致命)

5. ★ case_info 表的三处 schema 不匹配(SQLite DDL 套到 PG 造成,非我引入)

问题 现象 修复
id NOT NULL 但无自增/默认 IdType.AUTO 插入留空 → 违反非空约束 ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY
列名 db_Path 为驼峰(带引号创建) 未加引号查询折叠成 db_path → 字段不存在 RENAME COLUMN "db_Path" TO db_path
db 是 varchar(8) 代码写 randomString(8)+id(9+ 字符)→ 值太长 ALTER COLUMN db TYPE varchar(64)

case_info 当时 0 行,改动零数据风险。

全库同类问题扫描结果(未修,仅报告):

  • 驼峰列还有 1 个:meta_raw_sheet.lineNo
  • 缺自增 id 的表还有 9 个:agent_chat_session/agent_message/agent_model/agent_model_provider/ lunar/meta_raw_file/meta_raw_sheet/table_field/table_info (其中 agent_* 由 Java 侧生成 ID,无影响;lunar/meta_raw_* 若走 AUTO 插入会失败)

6. ★ 端到端验证结果(API 级核心链路全通)

  • 建案:POST /case/create → id=2,QingJian/workspace/2/pYRn5q3L2 生成,case_info 落库
  • 开案:POST /case/open?id=2&pwd=123456 → 200(注册 DuckDB slave + CaseDataCache)
  • 灌合成数据(DuckDB 单写者,须先 GET /case/exit 释放文件锁): person_basic_info 8 / call_record 280 / trans_record 199 / rel_node 24 / rel_edge 16
  • 建会话:POST /chat/sessions {workspaceId:2, modelId:DeepSeek} → 200
  • SSE 流式(233KB)事件统计:intent×1、token×1071、tool_call×13、tool_input×13、 tool_result×13、suggestions×1、done×1 —— 无 error
  • intent 帧含意图分类 + 增强问题 + entities;正文按契约输出裸 JSON 表格载荷 (resultId + sql + columns + rows),数据与灌入数据一致
  • 结果集翻页:page=1/pageSize=3 → totalRows=5/totalPages=2;page=2 返回余量 2 行; page=999 clamp 到末页 2;不存在的 resultId → HTTP 404 + 友好提示;sql 正确回传
  • 默认模型(Ollama 10.66.66.66:8080)不可达;api.deepseek.com:443 可达且已配 Key → 验证时用 modelId=3742141657084284816 指定 DeepSeek

7. 待办 / 未完成

  • UI 级验证(起 vite dev + 浏览器自动化截图)未做
  • 未验:echarts `/`graph 围栏渲染、execute_python 出图、模型 CRUD、中断落库
  • 遗留垃圾:QingJian/workspace/1/(首次建案失败留下的空目录)
  • 前端 ChatSession.id 等 Long 字段后端序列化为字符串(防 JS 精度丢失), 而 TS 类型声明为 number → aiAnalysis 的 openFromQuery 用 Number(raw) 与 s.id 比较会失配 (从聊天历史"继续对话"跳转可能定位不到会话),待修

★★★ 2026-09-16 上午:AI Agent 功能测试(14 项)

后端启动注意

上一轮的后端在 2026-09-15 23:29 随会话结束被终止(日志无关闭信息)。重启后需重新 /case/open (slave DuckDB 数据源是进程内状态,不持久化)。

通过项(14)

# 测试 结果
1 图表渲染 render_chart ✅ ```echarts 围栏 817 字符,数据口径正确
2 关系图谱 render_graph ✅ ```graph 围栏 4591 字符 + 28 行表格
3 会话 CRUD(重命名/置顶/收藏/详情/消息列表) ✅ 全 200 且状态生效
4 模型 CRUD(增/改/设默认/删) ✅ 全 200
5 中断落库 ✅ 12s kill → messageType=interrupted
6 用户消息落库(修复后回归) ✅ 1 user + 1 assistant
7 SSE 错误路径(无效/非法 sessionKey、空消息、无效 modelId) ✅ 全部返回规范 error 帧
8 路径穿越防护(..%2f、..%5c、--path-as-is) ✅ 400/404,无文件泄露
9 模型污染修复回归 ✅ 无效 modelId 后会话 model_id 未被写坏
10 多轮记忆延续 ✅ 追问正确引用上一轮结论(280/199)
11 案件隔离(新建空案件问同一问题) ✅ 所有表返回 0 行,未串案件2数据
12 结果集翻页 + 404 ✅(上一轮已验)
13 真实载荷扫描器验证(4 个真实 SSE 输出) ✅ 全部正确识别
14 前端 type:check + ESLint ✅ 0 错误

环境限制(非缺陷)

  • execute_python 未注册:日志 Python 执行环境不可用(command=python,需安装依赖: pip install pandas duckdb matplotlib) → 该 Python 环境缺依赖。故 Python 出图 + /py/files/** 无法验证。 启用方式:给 etlProperties.python.command 指向的 Python 装 pandas/duckdb/matplotlib。

本轮发现并修复的 3 个真实缺陷

  1. 用户消息从不落库(高影响):doStream 只落 assistant,messageMapper.insert(userMessage) 仅在非流式的 /chat/messages 里 → 前端走流式,刷新后用户看不到自己的提问、会话转录缺一半。 修复:新增 persistUserMessage(),在 doStream 解析会话后落库一次(含 messageCount+1)。
  2. 无效 modelId 会永久写坏会话(高影响):doStream 先回写 session.model_id 再校验模型是否存在, 一次带无效 modelId 的请求即把会话钉死在坏模型上,后续不带 modelId 的每轮都失败。 修复:把回写移到 getOrCreateAgent()(内含模型存在性校验)之后。 已修数据:UPDATE agent_chat_session SET model_id=3742141657084284816 WHERE model_id=999999(1 行)。
  3. 围栏包裹的表格载荷被渲染成代码块(中影响):后端契约明确「禁止把结果 JSON 放入 json 或sql 块」, 但实测模型约 1/4 概率违反(包进 或json)。前端 scanner 原先把围栏内容一律判为 code 块 → 退化成不可翻页的代码块。 修复:scanner.ts 围栏分支加容错——若围栏体是合法表格载荷(isTablePayload)且 lang 为 `/json/sql,则按table` 渲染。已用 4 个真实 SSE 输出验证(含那个被围栏包住的用例)。 扫描器断言从 56 项增至 64 项。

本轮发现但未修的问题(需你决策)

  1. Agent 具备 shell 执行能力:Python 测试中 Agent 自行调用了 AgentScope 内置的 execute(shell,11 次)、write_file、glob_files、list_files。对处理案情数据的产品, LLM 能执行任意 shell 命令是显著的安全/范围问题,建议限制 HarnessAgent 的内置工具集。
  2. /sys/** 全部 404:SystemController 是 @Controller 而所有方法都没有 @ResponseBody → 返回 void/String/实体时被当成视图名去转发(日志:View name [sys/health] → Forwarding to [sys/health] → 404)。且前端 main.ts 期望 Result 包装(res?.code === 200)。 影响:waitForBackendReady() 重试 60×2s=卡 2 分钟;resolveStartupRoute() 拿不到 code===200 → 总是跳转 /authorization,前端无法进入应用。 (注:LicenseFilter 修复前该接口返回空 200,同样不满足 code===200,故此问题一直存在。)
  3. /chat/sessions 缺 workspaceId 参数返回 500(MissingServletRequestParameterException 未被 GlobalExceptionHandler 处理),宜为 400。

测试脚本(可复用,均在 temp 目录)

  • agent_test.py:发起 SSE 提问 + 解析事件统计/围栏/裸 JSON 表格/图片引用
  • real-payload.cjs:把真实 SSE 的 token 正文喂给 scanner,验证块识别(防契约漂移)
  • PgMsg*.java / PgSess*.java:核对消息落库与会话状态
  • 关键测试数据:案件 2 = 有数据(8人/280通话/199交易/24节点16边),案件 3 = 空库对照

★★★ 2026-09-16 上午(续):UI 级验证

浏览器工具链

  • agent-browser 的守护进程在本机起不来:open/eval/snapshot/doctor 全部挂死 (~/.agent-browser/default.pid 写了进程号但进程已死、端口未监听)。 Node 派生能力已排除(托管 node22 与系统 node24 都能正常 spawn detached 子进程)。
  • 改用 playwright-core + agent-browser 已下载的 Chrome: chromium.launch({executablePath: 'C:\\Users\\cc\\.agent-browser\\browsers\\chrome-153.0.8010.47\\chrome.exe'}) (playwright-core 装在 temp 目录,不下载浏览器,秒级可用)。
  • 脚本:ui-stage1.cjs(开案+进 AI 页)、ui-stage2.cjs(提问+渲染校验)、ui-probe-case.cjs(DOM 探测)
  • 截图目录:C:\Users\cc\AppData\Local\Temp\ai-verify\shots\

★ 授权门:本机许可证就在仓库里

前端启动强校验 /sys/checkAuth 必须返回 code:200(main.ts 的 shouldRunStartupAuthCheck 只豁免授权页本身,skipAuthCheck 查询参数并未被采纳)。

  • 机器码 A9DEBA7C48078E4B...(GET /sys/code)
  • 许可证文件 lib/windows/license_刘备_A9DEBA7C.xlts 文件名后缀与机器码前缀完全匹配 → 复制到 QingJian/license/license.xlts 后 /sys/checkAuth 返回 {"code":200,"data":"试用"}
  • 另:system_info 表为空 → /sys/info 的 data 为 null;但前端 isAuthorizationExpired(undefined) 返回 false(不拦截),所以不影响进入应用。

UI 阶段发现并修复的 4 个真实缺陷

  1. /sys/** 全部 404(前文已记)→ SystemController 改 @RestController + 全部返回 Result。 修复后 /sys/health、/sys/info、/sys/checkAuth 均 code:200。
  2. /case/* 返回裸对象/数组 → 前端 defHttp 默认 transform 期望 {code,message,data}, 实际抛「接口请求出错,请稍后重试!」且案件列表渲染为空。 修复:CaseInfoController 全部返回 Result;/case/open 额外返回已开案信息。
  3. /case/create|open|updatePwd 参数绑定失败 → 前端以 JSON body 提交, 后端却是简单参数(form/query)→ 日志 parameters={} → 500。 修复:改为 @RequestBody(新建 OpenCaseDTO;create 用 @RequestBody CaseInfo; updatePwd 用 @RequestBody UpdateCasePwdDTO)。 注意:/case/open 现在只认 JSON body,curl -d "id=2&pwd=..." 不再可用。
  4. /chat/sessions 缺 workspaceId 返回 500 → GlobalExceptionHandler 补 MissingServletRequestParameterException → 400。

案件页 DOM 结构(写自动化必知)

案件列表不是表格,是 antd 卡片:

.ant-card(标题 .ant-card-head-title 内含案件名)
  └ ul.ant-card-actions > li > span > div.flex > button  "打开案件"

密码弹窗:input[type="password"] + .ant-modal-footer button.ant-btn-primary。

UI 已确认正常的部分

  • 应用自动跳转 /case,标题「清鉴线索调查工具」,左侧菜单完整 (案件数据/数据画像/通信分析/交易分析/活动轨迹分析/行为时空分析/对象关系分析/基站查询/AI 数据分析/模型管理/聊天历史)
  • AI 数据分析页渲染正常;未开案时显示空状态提示「请先打开一个案件,AI 数据分析将基于当前案件的数据空间工作。」
  • 模型选择器显示「本地模型(默认)」;输入区提示「Enter 发送 · Shift+Enter 换行」

★ UI 端到端验证结果(全部通过)

项 证据
流式逐字输出 21s 完成,中间态截图有「正在理解问题」→ 逐段文本
交互表格 + 分页 表头[号码/机主姓名/通话次数/作为本方/作为对方] + 5 行数据;分页器「共 5 行 · 50 条/页」
ECharts 图表 canvas 850×340 painted=true;堆叠柱状图 + 图例 + 坐标轴 + 保存图片/配置
关系图谱 852×494;meta「节点 8 · 关系 20」;8 个节点 DOM 带姓名与分类;边标签「11笔/¥3260…」
工具调用面板 「3 次工具调用 · 检索表结构、校验图表配置、执行 SQL 查询」+ 每步可展开「详情」
导出下载 菜单 Markdown/Excel/PDF;实下 .xlsx(16938B,合法 OOXML) 与 .md(3646B,含表格 JSON+echarts 围栏,往返无损)
模型选择器 下拉仅 ["本地模型(默认)","ds"] —— 证实只列 llm 模型,embedding 未出现
会话列表 显示案件 2 的全部会话 + 新建会话即时出现,条数含 user 消息(落库修复生效)
错误渲染 模型不可达时气泡显示 ⚠️ HTTP transport error... + 复制/导出/删除
控制台 修复后错误数 1(仅 antd rowKey 弃用警告)

★ UI 阶段又修掉 2 个真实前端缺陷(都在 GraphBlock.vue)

  1. 图谱整体渲染失败:graphOptions 里写了 lineUseTextPath: true,当前 @relation-graph/vue 版本不支持该属性并直接抛错 (Graph options do not support setting "lineUseTextPath" ... Please use "defaultLineTextOnPath"), 导致图谱不出图 + 连带 60+ 个 Vue Cannot set properties of null (setting '__vnode') 错误。 修复:改名为库要求的 defaultLineTextOnPath。修复后控制台错误 67 → 1。
  2. 图谱节点姓名不可见(白字白底):CSS .ai-node { color: var(--node-color) } 是文字颜色, 而模板把节点填充色 node.color(= #ffffff)绑给了它 → 姓名渲染了但看不见。 修复:在 jsonData 的 data 里显式写 accentColor: color,模板改绑 node.data?.accentColor(不依赖图谱库内部字段名)。修复后节点显示姓名 + 分类。

暗色模式:当前工程不可达(非缺陷)

  • 开关组件 AppDarkModeToggle.vue 已导出但未在任何模板挂载(showDarkModeToggle: true 形同虚设)
  • 主题插件在 build/plugins/index.ts:45 被注释掉
  • 故 data-theme=dark 无运行时暗色 CSS 可加载。手动加 dark 类后 body 变深色 (rgb(44,52,74))但 AI 容器仍为白底 —— 因为 --ai-* 是 Less 编译期变量。 结论:暗色未启用,无法验证;若将来启用(恢复插件 + 挂载开关),需用生产构建复验 AI 页配色。

本轮后端又修的契约问题(同属"裸响应 vs Result")

文件 问题 修复
SystemController @Controller 无 @ResponseBody → 全部 404 改 @RestController + 返回 Result
CaseInfoController 返回裸对象/数组 + 入参用简单参数 返回 Result;create/open/updatePwd 改 @RequestBody(新增 OpenCaseDTO)
GovernTreeController 返回裸 List/Map 全部 Result 包装
GlobalExceptionHandler 缺 MissingServletRequestParameterException 补 → 400

注意:/case/open 现在只认 JSON body。

UI 验证补全(模型管理页 / 建案流程)

项 结果
模型管理页 3 行 ✅ 类型列「对话模型/对话模型/向量模型」、状态列「活跃」(枚举映射生效)
向量模型「设为默认」拦截 ✅ 弹窗「无法设为默认模型…请选择「对话模型」作为默认。」
通过 UI 表单建案 ✅ 新案件出现在列表;0 接口错误、0 控制台错误(验证 /case/create 的 @RequestBody 改造)
案件页/模型页控制台 ✅ 0 错误;AI 页仅 1 条 antd rowKey 弃用警告

自动化探测的坑(供下次复用)

  • antd Table 有两种结构:可滚动表是「表头表 + 表体表」两张(.ant-table-header / .ant-table-body), 不可滚动表只有一张(行在 .ant-table-tbody tr)。统计行数要兼容两者,否则误判 0 行。
  • page.locator('button:has-text("x"), text=y') 这种混用 CSS 与文本引擎的写法会报错, 要用 page.getByText('y')。
  • 等待流式结束用「停止生成 按钮消失」,别用"文本长度稳定"。

遗留测试产物

  • 通过 UI 建的案件「UI建案验证-266000」(空库),以及案件 3「AI验证案件-空库对照」, 均为验证产物,可按需删除(密码都是 123456)。

★★★ 全量 Controller Result 包装审计与修复(2026-09-16)

审计结论

写脚本扫描全部 45 个 controller:147 个端点中 122 个未用 Result 包装(涉及 40 个文件)。 再扫前端 250 个调用点:248 个期望 Result(defHttp 默认 transform), 只有 2 个显式 isTransformResponse:false(/sys/info、/sys/openLocalFile)。 ⇒ 这些未包装端点在前端几乎全是坏的(抛「接口请求出错,请稍后重试!」)。

修复方式

写脚本 wrap-result.cjs 批量转换,规则:

  • 只改「带 @XxxMapping 的 public 方法」
  • void → Result<Void> + 末尾补 return Result.succeed();
  • 其它 X → Result<X>,所有 return expr; → return Result.succeed(expr);
  • 跳过:SseEmitter / Flux<...> / ResponseEntity<...> / 已是 Result<...>
  • 跳过 AI 模块 3 个 controller(/chat/**、/chat/results/**、/py/files/** 前端 aiHttp 显式按裸响应读)

结果:未包装端点 122 → 14,剩下的 14 个正是刻意保持裸响应的 (/chat/** 11 个 + /chat/results/** 1 + /py/files/** 1 + /govern/sse 1)。 编译 BUILD SUCCESS。

★ 脚本踩的 4 个坑(重要,下次别重复)

  1. 原始类型不能做泛型参数:boolean → Result<boolean> 编译失败,必须装箱 (boolean→Boolean/int→Integer/long→Long/double→Double/char→Character…)。
  2. indexOf 定位返回类型会命中 URL 里的同名子串: @GetMapping("/createGraphHis") public GraphHis createGraphHis(...) → URL 被改成 /createResult<GraphHis>。必须用精确偏移,改完逐文件比对映射注解。
  3. 文件是 CRLF:/(^import .*;\n)/m 匹配不到(行尾是 \r\n)→ 必须写 \r?\n。
  4. return 语句多消耗了一个 ; → 出现 return Result.succeed(x);;。 处理完表达式后要 i = j + 1 跳过原分号。

安全流程(这次靠它兜住)

  1. 改前完整备份所有 controller 到 temp(backup-controllers/)
  2. 批量转换
  3. 编译(暴露了 30 处错误 → 逐类修脚本)
  4. 与备份逐文件比对映射注解与方法名(audit-diff.cjs)→ 找出 URL 被污染那 1 处
  5. 重新审计(确认只剩预期的 14 个未包装)
  6. 重启后抽样冒烟:/sdc/list、/dm/getGovernConfList、/graph/graphHis、/gt/tree、 /case/list、/models、/models/providers、/cleanLog/list 全部返回 {code,message,data}; /chat/sessions 仍是裸数组 ✓;SSE /chat/stream 正常出帧 ✓

当前阻塞(环境,非代码)

DeepSeek API Key 已失效:Authentication Fails, Your api key: ****dfsf is invalid(HTTP 401)。 08:55 时还能用,之后失效 —— 需要用户更新 Key 才能继续跑 LLM 对话验证。 默认模型 Ollama(10.66.66.66:8080)本就不可达。

运行状态(2026-09-16 09:30 用户要求停止)

  • 后端(8980)与 Vite dev(3100)均已停止,端口已释放;无 java/vite/无头 Chrome 残留进程。
  • 下次要跑:从仓库根 mvn -pl ai-server spring-boot:run -Dspring-boot.run.workingDirectory=E:/workspace/zsjz-ai, 启动后必须重新 POST /js/a/case/open(JSON body)注册 slave 数据源。

移除「聊天历史」菜单(2026-09-16 09:45,用户要求)

用户要求去掉独立的「聊天历史」菜单。共改 4 处:

  1. core/store/modules/menu.json:删掉 AI 分组(/10263)下的 /aiChatHistory/index 子项 (分组 redirect 仍是 /aiAnalysis/index,未受影响)
  2. core/router/helper/routeHelper.ts:删掉 '/aiChatHistory/index' 的组件映射 (AI 模块的视图只走这张映射表,不走通用 glob,删了就不会再有引用)
  3. 删除 ai/views/aiChatHistory/index.vue(该文件已被 git 跟踪,已 git rm)
  4. ai/views/aiAnalysis/index.vue:删掉因此失效的 openFromQuery()(读 ?sessionId= 定位会话) 以及随之无用的 useRoute 导入与 route 变量

验证:menu.json JSON 合法且 AI 菜单变为 ['AI 数据分析','模型管理']; 全仓 grep aiChatHistory|聊天历史 0 命中;i-mdi:history 无残留引用; src/ai/** 类型检查与 ESLint 0 错误;改动的 3 个文件均不在 src/core 既有类型报错列表中。

注意:pnpm type:check 全量跑会报出 src/core/** 的既有错误(getToken/getUserInfo/ getSessionTimeout 等在用户体系移除后失配,约 10+ 处),这是历史遗留、非本次引入; 检查时用 grep "^src/ai/" 过滤即可。


★★★ 案件数据上传:Electron 本地路径 → 纯 Web multipart 上传(2026-09-16)

用户要求:移除 Electron 相关内容,改纯 Web 上传(多选文件 + 文件夹,xls/xlsx/csv,带进度), 后端改单文件上传并解析,解析完查结果复用原结果页,后续流程与参数保持不变。

改造前的关键事实(决定了方案)

  • 原链路前端只把本地绝对路径数组发给 POST /dm/preFile({filePath: string[], isDir}), 后端拿路径读服务端/客户端本机文件 → Web 化必须真正上传字节。
  • filePath 在清洗阶段会被再次读取(CleanTask:87 打开文件、DmService.hasSourceFile 过滤) → 上传文件必须落盘持久化,且 filePath 要回填为服务端可读的绝对 Path。
  • 清洗完成后会递归删除整个 QingJian/tmp → 上传文件绝不能放 tmp 下, 否则「重新清洗」会失效。落盘目录:QingJian/workspace/{caseId}/upload/{batchId}/。
  • PreTask/PreDataListener 本来就接收 Path 入参 → 落盘成 Path 即可整套复用解析逻辑。
  • ⚠️ DmController 所有 POST 端点都缺 @RequestBody(Solon→Spring 迁移遗留) → 现有 preFile 本来就是坏的,必须先补。

后端改动(ai-server)

文件 改动
DmController.java 给 preFile/doClean/getFiles/deletedFiles/saveGovernCon 补 @RequestBody;新增 POST /dm/preFileUpload(单文件 MultipartFile)
common/model/dm/dto/DeletedFilesDTO.java 新增:deletedFiles 前端发 {ids:[]},原签名是简单参数绑定不上
DmService.java 新增上传落盘 + 调 PreTask 解析的方法;后缀白名单 xls/xlsx/csv;filePath 回填服务端绝对路径、fileSize 回填真实大小
StateManager.java 新增 isCaseOpened()(避免在业务代码里硬编码哨兵值 getCaseId()==888888)

注意:getCaseId() 返回 Integer,未开案时是哨兵值 888888 而非 null。

前端改动(ai-frontend)

文件 改动
case/api/govern/governApi.ts 新增 preFileUpload(file, onProgress, data),走 defHttp.uploadFile(底层 axios,可传 onUploadProgress)
case/views/data/index.vue 上传弹窗整体重写(见下)

index.vue 具体改法:

  • 删掉全部 Electron 依赖:extractUploadFilePath / extractUploadFolderPath(读 window.electron.getPathForFile)、 handleSelectFile(ipcRenderer.invoke 选文件夹)、beforeUpload / beforeUploadFolder、 processUploadFilePaths / queueUploadFilePaths / normalizeUploadPaths / pendingUploadPaths / uploadBatchTimer / selecting / uploadProcessing。
  • 把 Upload.Dragger 换成原生拖拽区(@dragover/@dragleave/@drop)+ 两个隐藏 input ([multiple][accept=.xls,.xlsx,.csv] 与 [webkitdirectory])。
  • 文件夹:选择走 webkitdirectory;拖拽走 webkitGetAsEntry() 递归遍历 createReader().readEntries() (注意 readEntries 每次只返回最多 100 条,要循环读到空为止)。
  • 后缀校验 isValidUploadFile,不合规的过滤掉并提示条数。
  • 进度:uploadProgressMap(单文件 %)+ overallProgress(按 (各文件进度和)/总数 计算)+ uploadStatuses(pending/uploading/done/error)。
  • 逐文件串行调用 preFileUpload,累加返回的 FileInfo[],全部结束后 sessionStorage.setItem('case_clean_file_infos', ...) → emitter.emit(CASE_IMPORT_FILE_REFRESH_EVENT) → go(PageEnum.BASE_MATCH) —— 下游(结果页/匹配页)完全没动。

验证结果

  • 后端 mvn compile + mvn package -DskipTests 均 BUILD SUCCESS
  • 前端 pnpm type:check:index.vue / governApi.ts 无新增错误 (残留 3 处是既有:CheckedType×2、Type instantiation×1)
  • 前端 ESLint(--max-warnings 0)0 错误(prettier/vue 格式问题已用 --fix 修掉)
  • 测试进程已全部清理(端口 8980 释放,无 java / 无头 Chrome 残留)

本次踩到的环境坑(复用价值高)

  1. Git Bash 后台跑 spring-boot:run 会随 shell 退出被杀:日志显示已到 Tomcat started on port 8980 + Started App + ACCEPTING_TRAFFIC,但 netstat 查不到、 进程也没了。改用 mvn package -DskipTests 打 jar 后 java -jar 启动即可正常监听 (但同样要避免后台被杀,测试完及时清理)。
  2. mvn 在本机 Git Bash 下直接用会报 找不到或无法加载主类 org.codehaus.plexus.classworlds.launcher.Launcher → 必须用 bash /c/Users/cc/AppData/Local/Temp/cptest/mvn-run.sh(它显式传 classworlds jar 与 -Dmaven.home)。
  3. pip install openpyxl 装到了系统 Python,但 python 命令走的是另一个解释器 → 用 python -m pip install 才装到当前 python 上。