本文件是仓库级约定,供 AI 编码助手(WorkBuddy / Claude Code / Cursor / Copilot 等)在本仓库内改代码时遵守。
⚠️ 别和下面两个文件搞混 —— 它们是产品运行时的 AI 人格提示词,不是给编码助手看的:
ai-server/src/main/resources/prompts/AGENTS.mdQingJian/agent/AGENTS.md改那两个文件 = 改产品里 AI 研判专家的人设与话术;改本文件 = 改编码助手的行为约定。
清鉴(QingJian) —— 面向纪检监察 / 经侦机关的智能资金数据研判平台。
业务闭环:
多源异构数据导入(话单 CDR / 银行账单 / 第三方支付 / 基站轨迹 / 外部库表)
→ 清洗治理(dm 模块)
→ 建模分析(通话·资金·轨迹·人物画像·图谱)
→ 可视化(ECharts / G6 关系图 / 离线栅格地图)
→ AI 对话式研判(AgentScope Agent + RAG + SQL 工具)
领域术语(写注释/命名时沿用它,不要自创):
资金回流、闭环交易、快进快出、通联异常、时空伴随、白手套、攻守同盟。
zsjz-ai/
├─ pom.xml # 父 POM(packaging=pom),Spring Boot 3.5.16,仅聚合 ai-server
├─ ai-server/ # 后端:Spring Boot 3 + MyBatis-Plus + AgentScope
├─ ai-frontend/ # 前端:Vue3 + Vite7 + TS + Ant Design Vue(JeeSite 脚手架血统)
├─ sql/ # 建表 / 初始化脚本(*.sql)
├─ QingJian/ # 运行期工作空间素材(agent 提示词、skills、conf、db、license)
├─ cmd/ lib/ pmtiles.exe # 本机工具(regInfo.exe 授权、mbtiles→pmtiles 转换等)
└─ enc.md readme.md
QingJian/ 不是源码目录,是运行时数据目录(含 .db、license 证书、导入模板),改动需谨慎。
| 项 | 值 |
|---|---|
| JDK | 25 |
| 框架 | Spring Boot 3.5.16 |
| 持久层 | MyBatis-Plus 3.5.17 + dynamic-datasource 4.5.0 |
| 主库 | PostgreSQL + pgvector(master) |
| 分析库 | DuckDB(slave,按工作空间切换) |
| 缓存 | Redis |
| 图库 | Neo4j(配置存在,非全链路强依赖) |
| 鉴权 | Sa-Token 1.46 |
| Agent | AgentScope 2.0.1(harness + dashscope/ollama/openai + mem0 + rag-simple + redis) |
| 检索 | Lucene 10(core/queryparser/analysis-common/highlighter) |
| 其他 | fesod-sheet(Excel)、Hutool、Guava、MapStruct 1.6.3、commons-compress、JNA |
| 启动类 | com.zsjz.ai.App |
| 端口 | 8980 |
ai-server/src/main/resources/app.yml是 Solon 时代遗留配置,已不生效,只作历史参考,不要照它改。 生效配置是application.yaml+application-dev.yaml。
preinstall: only-allow pnpm,禁止 npm/yarn)@relation-graph/vue;图表 ECharts 6>=20.19 || >=22.12接口前缀 /js,开发环境由 .env.development 代理到 http://127.0.0.1:8980:
VITE_PROXY = [["/js","http://127.0.0.1:8980/js",false]]
VITE_GLOB_API_URL_PREFIX = /js
VITE_GLOB_ADMIN_PATH = /a
请求最终落到 /js/a/...(urlPrefix(/js) + adminPath(/a)),
与后端 server.servlet.context-path: /js/a 严格对应 —— 两者必须同步改。
调接口时用 url: adminPath + '/xxx'(defHttp 会自动补 /js);
不走 defHttp 的场景(fetch / <img src>)用
glob.apiUrl + glob.urlPrefix + glob.adminPath + path。
包根:com.zsjz.ai
com.zsjz.ai
├─ App.java 启动类
├─ common/ 跨模块复用
│ ├─ config/ Result、WebConfig、MybatisPlusConfig、LicenseFilter、
│ │ DuckdbUnpooledDataSource、BigDecimal 序列化器
│ ├─ base/ Query、TreeNode、BasicColumn、Cleaned、ErrorEnum
│ ├─ enums/ constants/ utils/
│ └─ model/<domain>/{dto,entity,query,vo}/ ★ DTO / 实体统一放这里
└─ module/<domain>/
├─ controller/ 仅收参 + 调 service
├─ service/ service/impl/
└─ mapper/ (XML 在 resources/mappers/<domain>/*.xml)
<domain> 固定取值:agent call dm external govern graph otg person plat track trans
配套资源:
ai-server/src/main/resources/mappers/<domain>/<Xxx>Mapper.xml
common.model.<domain>.query.XxxQuerycommon.model.<domain>.dto.XxxDTOcommon.model.<domain>.entity.XxxXxxService,实现 XxxServiceImplPage<T>(PaginationInnerInterceptor(DbType.DUCKDB) 已注册)。Result<T>:Result.succeed(data) / Result.failure(msg)。
> 存量 Controller(如 CallNightController)有直接返回裸对象的历史写法;新增代码一律用 Result,存量代码按需渐进改造。BigDecimal(已配 BigDecimalSerializer / BigDecimalDeserializer),禁用 double。StpUtil.getLoginIdAsLong()。会话、消息、模型配置等用户级资源必须带 userId 做归属校验,不能只按 id 查。@DS("slave")(参考 SqlAnalysisTool 依赖的 SqlQueryMapper);不标注则走 master。HasInnerEnum,与存量保持一致。| 数据源 | 用途 |
|---|---|
| master(PostgreSQL + pgvector) | 平台元数据、table_info/table_field、Agent 会话/消息、模型与 Provider 配置、RAG 索引状态、向量检索 |
| slave(DuckDB) | 按工作空间切换的案情库;Agent 的 execute_sql 只在这里跑 |
| Redis | 会话/缓存、Sa-Token、mem0 |
| Neo4j | 图谱(配置在 application-dev.yaml) |
meta_raw_sheet → table_info → table_fieldsql/,新增表结构务必同步补脚本 和 元数据三表mapUnderscoreToCamelCase 已开启application-dev.yaml 内含本机数据库密码 / 内网 IP,属于开发便利配置。
module/agent)★ 核心AgentScope HarnessAgent + Toolkit + 中间件:
controller/ AgentChatController(REST + SSE,裸返回)
AgentModelController(模型/厂商 CRUD,Result 包装)
AgentResultController(execute_sql 结果集分页,裸返回)
AgentPythonFileController(Python 产出图片文件服务)
service/ AgentChatService、AgentModelService、AgentModelProviderService
tools/ SqlAnalysisTool、RagSchemaSearchTool、PythonAnalysisTool、
GraphRenderTool、WorkspaceInfoTool
intent/ 意图识别(IntentService / IntentMiddleware / IntentEnum)
followup/ 追问生成(FollowupService / FollowupMiddleware)
rag/ RagSchemaService、EmbeddingModelFactory、RagProperties
python/ PythonExecutor(沙箱执行)
sql/ SqlResultStore(结果集缓存,供前端分页;按登录用户归属)
prompt/ PromptLoader、PromptHelper
scaffold/ WorkspaceScaffolder(工作空间初始化)
mapper/ Agent*Mapper、SqlQueryMapper、RagVectorMapper
entity/ AgentChatSession、AgentMessage、AgentModel、AgentModelProvider、RagIndexStatus
@Tool 注解)| 工具 | 说明 |
|---|---|
execute_sql |
在当前工作空间 DuckDB 上执行只读 SQL,返回 resultId + 列定义 + 当前页 + 总行数 |
list_tables |
列出工作空间全部表(中英文名、字段类型与中文注释) |
search_table_schema |
基于 pgvector 的表结构语义检索 |
render_chart |
校验并返回 ECharts 图表配置(禁止用于关系图) |
render_graph |
校验并返回力导向关系图谱 JSON —— 关系图只能走这里 |
execute_python |
在沙箱中执行 Python 脚本分析,产出图片经 /py/files/** 访问 |
get_current_workspace / get_user_selected_workspace |
读取当前/用户所选工作空间信息 |
get_call_records |
通话原始明细(CallRecordService#getCallRecord) |
stat_call_night_summary / stat_call_night_detail |
夜间通话一/二层(CallNightService) |
stat_call_continuous_summary / stat_call_continuous_detail |
持续联系一/二层(CallContinuousService) |
stat_call_sensitive_summary / stat_call_sensitive_detail |
敏感日期联系一/二层(CallSensitiveService) |
AgentToolRegistry,共 77 个)六个域的只读 service 由 AgentToolRegistry 统一注册为 6 个工具组,默认只装备 person 组,
其余组由模型调用 reset_equipped_tools 按需切换 —— 避免每轮把 77 份 JSON Schema 塞进上下文。
| 组名 | 数量 | 覆盖能力 | 工具类 / 入参类 |
|---|---|---|---|
person(默认装备) |
24 | 人员枚举与分组、通话/交易画像、常联系/常交易 TOP、汇总、运营商归属、银行卡、交易趋势、9 类亲密度行为 | PersonAnalysisTool / PersonToolSpecs |
call |
7 | 通话明细、夜间/连续/敏感通话的汇总与明细 | CallAnalysisTool / CallToolSpecs |
trans |
19 | 交易明细、大额、共同/相互交易、代持卡、现金流、连续交易、快速资金流转、理财、定期存款、频次、资金流向图与追踪、敏感交易 | TransAnalysisTool / TransToolSpecs |
track |
16 | 基站轨迹、境外本地昼夜活动、快递、碰面、同住、同行出行 | TrackAnalysisTool / TrackToolSpecs |
otg |
5 | 案件材料全文检索、特殊日期清单、行为时间轴及跳转/明细 | OtgAnalysisTool / OtgToolSpecs |
graph |
6 | 关系图谱取数、图谱资金边/通话边/节点明细、图谱历史 | GraphAnalysisTool / GraphToolSpecs |
分组机制要点(
AgentToolRegistry注释里有完整说明):
- 组必须是
ToolGroupScope.META,否则reset_equipped_tools会拒绝("not manageable by this tool") 且组名不会出现在该工具to_activate的 enum 里;- 未激活组里的工具不进
getToolSchemas(activeGroups),模型完全看不到;- 新会话的初始激活组 =
ReActAgent在 build 时捕获的toolkit.getActiveGroups(), 所以「默认装备哪一组」只由AgentToolRegistry#DEFAULT_ACTIVE_GROUPS决定;reset_equipped_tools是全量替换语义(不在to_activate里的组一律卸载),AgentService#appendRenderPrompt里已提醒模型「把还要用的组一并列出,用完切回」。- 已知限制:
ReActAgent在会话恢复/结束时用共享Toolkit的全局 activeGroups 做set/get, 同一 agent 实例的并发会话理论上可能互相覆盖全局状态(每轮实际按会话状态过滤,影响有限)。通话分析 7 件套由
CallAnalysisTool包装module/call的既有 service,只做适配不改业务, 保证「页面上的结论」与「AI 的结论」同口径;返回形态与execute_sql一致 (resultId+columns/rows/totalRows,前端DataTableBlock直接渲染可翻页表格), 单次取数上限 10,000 行(比execute_sql收紧,因SqlResultStore是 LRU 50 的共享缓存), 超限返回可读报错而不是静默截断。入参是专用 POJO(CallToolSpecs), 不暴露orderKey/sort/tableName(Mapper XML 里是${}拼接,属注入面)。 其余 5 个域同构:入参*ToolSpecs、出参ToolResultTable、上限 10,000 行。唯一例外:
get_case_graph返回{nodeList, edgeList}结构化 JSON(不表格化,因为render_graph要的就是这个结构),且边数超过 500 时按权重降序截断并附truncated/totalEdges/note,防止 1 万条边的 JSON(约 30 万 token)冲爆上下文。
LIMIT / WHEREpage_size 默认 50,上限 500select / with / pragma / describe / show / explain,禁止多语句(含分号)render_chart 会硬拦截 series.type == "graph",强制改走 render_graphPOST /chat/stream,事件按序:tool_call → tool_input → tool_result → token(多次) → done / error
payload 结构:
{ type, data?, toolCallId?, toolName?, toolInput?, toolResult?, error?, sessionKey? }
前端消费方:ai-frontend/src/ai/api/(chatApi.ts / types.ts)。改动协议必须前后端同步。
ai-server/src/main/resources/prompts/AGENTS.mdai-frontend/src/ai/)后端 AgentService#appendRenderPrompt 强制的输出契约 → 前端块的映射:
| AI 输出形式 | 前端块 | 组件 |
|---|---|---|
裸 JSON 混在 markdown 正文(execute_sql 结果,禁止围栏) |
table |
blocks/DataTableBlock.vue(按 resultId 调 /chat/results/{id} 翻页) |
`echarts 围栏(ECharts option JSON) |
echarts |
blocks/EChartsBlock.vue |
`graph 围栏({title,nodes,edges,categories}) |
graph |
blocks/GraphBlock.vue(@relation-graph/vue) |
`html 围栏 / 裸 <html>·<svg> |
html |
blocks/HtmlBlock.vue(sandbox iframe) |
| 其余代码围栏 / markdown 正文 | code / markdown |
blocks/CodeBlock.vue / blocks/MarkdownBlock.vue |
关键实现约束:因为表格是裸 JSON 而非围栏,utils/scanner.ts 必须做字符级花括号配平扫描
(字符串/转义感知)并按载荷形态校验,不能只按围栏切分。流式期间的增量策略是
「已完成块冻结(尾部 8KB 窗口外)+ 只重扫尾部 + 尾部块按 key 复用对象」,
块 key 用 kind:start(内容只追加 → key 稳定,避免图表实例重建、表格分页被重置)。
改渲染契约(appendRenderPrompt)时必须同步改前端 scanner.ts 与对应块组件。
module/dm)dm/
├─ pre/ PreTask、PreDataListener、DateNumberConverter、Str2NullConverter
├─ clean/
│ ├─ DataCleaner(接口)、CleanResult、CleanCache、DirtyDataHandler、StrategyType
│ ├─ impl/cleaner/ 各数据源清洗器(Call*/Trans*/Track*/External*/...)
│ ├─ impl/loader/ 数据装载器
│ └─ func/ 可复用清洗函数:FunAbs/FunRadix/FunReplace/FunTime/FunMultiplier/...
└─ mapper/ CleanErrorLogMapper、DmMapper、PersonRelEdgeMapper、EdgeConfMapper
Loader + Cleaner(命名对齐 XxxDataLoader / XxxDataCleaner)CleanErrorLog,不要静默吞掉func/Fun* 而不是在 Cleaner 里硬编码目录按业务域划分,与后端 <domain> 基本对齐:
ai-frontend/src/
├─ core/ 脚手架通用层:components/、layouts/、api/、hooks/、store/、router
├─ ai/ AI 数据分析应用(api/、utils/、store/、styles/、components/、views/)
├─ case/ 案件:数据导入/清洗/治理/人物画像/线索推送
├─ call/ 话单分析(night、continuous、sensitive...)
├─ trans/ 资金分析
├─ track/ 轨迹分析
├─ person/ 人员
├─ plat/ 平台/模板
├─ dm/ 清洗类型定义
└─ graph/ 图谱(G6 编辑器、力导向布局 worker、资金流向图)
每个业务域内:api/(请求 + types/dto.ts、types/query.ts)、views/、components/、hooks/、types/。
src/core/components/*(BasicTable / BasicForm / BasicModal / Description / AnalysisPagination 等),不要新造轮子。<script setup lang="ts">,defineProps / defineEmits 显式类型化。src/<domain>/api/types/{dto,query}.ts 的 TS 类型,与后端 DTO 字段保持一致(改后端字段时同步改这里)。<style lang="less" scoped>)。uno.config.ts 的 content.pipeline.include 只扫
**/*.{vue,tsx,ts},menu.json 不在扫描范围内;而 Icon 组件是运行时拼
i-<collection>:<name> 类名的。所以在 menu.json 里配的图标不会生成 CSS。
新增图标时同步把 i- 前缀的完整类名加入 uno.config.ts 的 safelist
(menu.json 里可直接写 "icon": "i-mdi:history",Icon 见到 i- 前缀会原样使用)。src/core/router/helper/routeHelper.ts 的 explicitDynamicViewMap
加一条 '/name/index': () => import('@/<domain>/views/<name>/index.vue')。
默认的 import.meta.glob('**/views/**') 会命中 node_modules 里的同名 views。.jeesite-layout-content 有确定像素高度,页面根用
height: 100% 即可撑满;要全出血再配 width: calc(100% + 24px); margin: 0 -12px;
(容器 padding: 12px 12px 0)。module/agent 的 /chat/**(含 /chat/results/**)是裸返回,
defHttp 默认 transform 会把非 code===200 的结构当错误抛,所以必须传
{ isTransformResponse: false };/models/** 是 Result<T> 包装,用默认 transform。
鉴权靠 Sa-Token,token 走 x-token 请求头(后端 sa-token.is-read-cookie=false,
仅 Header 模式)。defHttp 的请求拦截器会自动注入;不走 defHttp 的场景
(fetch / EventSource)必须手动带上 'x-token': getToken()。
⚠️ 原生 EventSource 无法自定义请求头,SSE 一律用 fetch + body.getReader() 读增量。
未登录时后端返回 HTTP 200 + body.code=401(不是 HTTP 401),前端据此跳登录页。# 编译
mvn -pl ai-server -am clean compile
# 打包(跳过测试)
mvn -pl ai-server -am clean package -DskipTests
# 运行
mvn -pl ai-server spring-boot:run
# 只跑 ai-server 的测试
mvn -pl ai-server test
启动后:http://localhost:8980
cd ai-frontend
pnpm install # = pnpm bootstrap
pnpm dev # 开发服务器
pnpm type:check # vue-tsc --noEmit
pnpm lint:eslint
pnpm lint:prettier
pnpm lint:stylelint
pnpm lint:all # 全量(含 install)
pnpm build # 生产构建
pnpm preview:dist # 预览 :3100
PostgreSQL(需装 pgvector 扩展)、Redis、Neo4j(按需)。
module/<domain>/{controller,service,mapper} + common/model/<domain>/{dto,entity,query,vo} 落位resources/mappers/**/*.xmlsrc/<domain>/api/types/*.tssql/ 脚本 + table_info / table_field 元数据@Tool(description=...) 与 @ToolParam,返回结构化 JSON + 中文说明LIMIT / WHERE,分页返回type:checkai-server/src/main/resources/app.yml(Solon 遗留,已失效)LambdaQueryWrapper)execute_sql 的只读白名单、行数上限、单元格截断render_chart 渲染关系图prompts/AGENTS.md / QingJian/agent/AGENTS.md 的人格与输出格式common/config/Result.java 顶部有 import io.milvus.param.R; —— 未被使用且 pom 中无 milvus 依赖,属遗留脏引用,建议直接删除。Result 的 SUCCEED_CODE / FAILURE_CODE 是非 final 的 public static 字段,可被外部篡改,建议改 final 或换成常量/枚举。common/config/WebConfig.java 与 application.yaml 的 context-path: "!/js/a/" 写法源自 Solon 语法,在 Spring Boot 下语义可疑AbstractServletWebServerFactory.checkContextPath() 会拒绝不合法的 context path。
实测 new TomcatServletWebServerFactory().setContextPath(v)(Spring Boot 3.5.16):
!/js/a/ → 抛 IllegalArgumentException;/js/a/ → 同样被拒(尾部斜杠非法);/js/a → 通过。
即原配置会让 ai-server 完全无法启动。已改为 context-path: /js/a,
与前端 ctxPath(/js) + adminPath(/a) 及 defHttp 约定一致。ai-server 未声明 @MapperScan,确认 mapper 是靠 @Mapper 注解还是 XML 绑定注册;新增 Mapper 时跟随现有写法。pom.xml 中 testcontainers 等依赖已声明但未见到对应测试,可评估是否裁剪。src/main/resources/mappers/**/*.xml 全是 com.qingjian.* 命名空间com.qingjian,但有 85 个 com.zsjz.* Mapper 接口与 85 个 XML 一一对应,
属包名迁移时漏改 XML,导致 MyBatis 启动即炸
(Failed to parse mapping resource: mappers/ai/SkillMapper.xml → 无法解析 resultType),
ai-server 在本仓库根本无法启动。
处理:删除 mappers/ai/(8 个文件,QingJian 旧 AI 模块,现 AI 能力在 module/agent 用 BaseMapper);
其余 77 个文件、160 处 com.qingjian → com.zsjz.ai。
新增 / 迁移 Mapper XML 时务必确认 namespace 与 resultType 用的是 com.zsjz.ai.*。application.yaml 的 mybatis-plus.type-aliases-package: com.qingjian.ai.server.model.*.entity.*
仍指向不存在的包(当前无实际影响:XML 全用全限定名)。
若要启用别名,先确认 com.zsjz.ai.common.model.*.entity 下无重名简单类名,否则 MyBatis 会因别名冲突启动失败。AppLoadEndEventListener(CommandLineRunner)依赖平台库结构(table_info / table_field)
与客户端运行时资产(RocksDB 目录、基站库文件)。当前开发库 zsjz-ai 缺平台库结构
(平台表在 etl-2,两个库不重合),故裸跑仓库目录会在启动后置任务阶段失败:
PSQLException: 关系 "table_info" 不存在。
完整本地运行需先补齐库结构与客户端资产(见 PathConst)。mvn -pl ai-server -am clean compile 通过pnpm type:check + pnpm lint:eslint 无错sql/ 补脚本 + 元数据三表同步description 更新,必要时同步提示词StpUtil.getLoginIdAsLong() 归属校验System.out.println 残留