# MCP 移除与多数据源 / 案件隔离修复计划 > 状态:Phase 0-3 已实施,Phase 4 自动验证已完成(人工场景见 §6.2) > 背景:生产不允许同账号多端登录(联调可用环境变量开启);MCP 相关能力不再使用,全部移除。 > 关联审计:`CaseRoutingDataSource` / `CaseDataSourceRegistry` / `CaseContextHolder` / 跨案件接口. --- ## 1. MCP 盘点与移除 ### 1.1 盘点结果 | 位置 | 内容 | |---|---| | 后端主代码 | `module/agent/mcp/`:`AgentScopeMcpToolProvider`、`AgentScopeToolCallback`、`McpCaseSession`;`vo/McpServerInfoVO` | | 后端接口 | `GET /capability/mcp` 及 `AgentCapabilityService.mcpServerInfo()` | | 配置 | `application.yaml` 的 `spring.ai.mcp.server` 段;`SaTokenWebConfig` 的 4 条 MCP 白名单 | | 依赖 | `spring-ai-starter-mcp-server-webmvc`、`spring-ai-starter-mcp-server` | | 测试 | `AgentScopeMcpToolProviderTest`、`McpCaseSessionSlotsTest` | | 前端 | 无功能性 MCP;仅 4 个 MCP 专用工具展示名和注释 | | 数据库 | 无 MCP 表/列/种子数据 | ### 1.2 已执行 - [x] 删除 4 个后端 MCP 主类 + 2 个 MCP 测试类 - [x] 删除 `/capability/mcp` 接口、service 方法、VO 及 Impl 中的 `@Value`/字段/`joinPath` - [x] 删除 `spring.ai` 配置段、2 个 Maven starter、sa-token MCP 白名单 - [x] 保留 `spring-ai-bom` 与 `spring-ai-tika-document-reader`(Tika 仍被文件解析使用) - [x] 重跑 `python ai-server/scripts/gen_field_labels.py`,清除 `field-labels.json` 中的 MCP 类 - [x] 前端删除 4 个 MCP 工具展示名与 MCP 注释;不删除「插件与技能」页面(它是技能/工具,不是 MCP) - [x] 删除本机运行时 `ai-electron/data/codex-home/config.toml` 的 `[mcp_servers.mcpsse]` - [x] 同步 `QUICK_START.md`、`AI_AGENT.md`、`docs/design/ai-gateway-user-sticky-routing.md` - [x] 全仓 `grep -ri mcp ai-server/src ai-electron/frontend/src ai-electron/electron ai-server/pom.xml` 无业务命中 ### 1.3 说明 `agentscope-harness` 会传递引入 `io.modelcontextprotocol.sdk:mcp`。本期已在 `ai-server/pom.xml` 对 `agentscope-harness` 增加 exclusion,产物 jar 中不再包含任何 `mcp-*.jar`;`AiServerApplicationTests.contextLoads()` 与全量单测已回归通过。 --- ## 2. 生产单端登录 - [x] 整个 `sa-token` 配置从 `application-dev.yaml` 移到基础 `application.yaml`,避免新增 prod/test profile 时漏配而退回 Sa-Token 默认值 - [x] `is-concurrent: ${SA_TOKEN_CONCURRENT:false}`:默认生产单端;联调启动加 `SA_TOKEN_CONCURRENT=true` 可多端 - [x] `is-share: false`、`max-login-count: -1` 保留并补注释 - [x] 同步 `SysUserService`、`VerifyPwdDTO`、`QUICK_START.md` 的说明 - [x] 案件数据源归属增加 `ownerToken`:联调多端时,一个 token 退出/换案不会关闭另一个 token 正在使用的案件 - `CaseDataSourceRegistry.OpenedCase` 增加 `ownerToken` - `CaseInfoService.open/closeOwnedCases` 按 token 过滤 - `SysUserService.releaseOpenedCases(userId, token)`:登出按 token,管理员踢人按用户 - `CaseSessionListener.doLogout` 按 token;`doKickout/doReplaced` 按用户 --- ## 3. 案件上下文一致性 ### 3.1 CaseAccessGuard 新增 `common/context/CaseAccessGuard`: - `requireOpenCaseId()`:无案件 400 - `requireCurrentCase(requestedCaseId)`:请求 caseId 与当前案件不一致 409 - `requireOwnedCurrentCase(caseId)`:再校验 `case_info.owner_id` - 历史 `owner_id == null` 的案件记 warn 并放行,待数据迁移后收紧 ### 3.2 已接入的入口 - [x] `AgentChatServiceImpl`:createSession / listSessions / getSession / listMessages / `requireSessionInOpenCase` - [x] `InsightServiceImpl`:createSession / listSessions / listClues / getMessageTrace / requireSession - [x] `AiCleanService`:run / judge / revert / dataPage - [x] `ChatAttachmentService.upload`:请求 caseId 必须等于当前案件 - [x] `AgentPythonFileController`:`workspaceId` 必须等于当前案件 - [x] `requireSessionInOpenCase` / `InsightServiceImpl.requireSession` 去掉“无案件放行” ### 3.3 前端换案同步 - [x] `appWindow.ts` 新增 `reloadSmartWindowIfOpen()`:只在工作台已打开时 `reloadIfOpen` - [x] `case.ts` 的 `afteropenCaseAction` 换案后调用,避免工作台继续用旧 `workspaceId` - [x] 退出/登出仍通过 `APP_RESET` 关闭工作台 --- ## 4. 写错库与文件路径 ### 4.1 SpecialDate(确定的写错库) - [x] `SpecialDateService.create/update` 增加 `@DS("slave")` + `@Transactional` - 原因:`saveBatch` 走 `SqlSession.insert`,不经过 mapper 代理,mapper 上的 `@DS` 不生效 - 不能只加在 private 的 `saveSpecialDate`(self-invocation 不生效) - [x] `EntryRecMapper` 补 `@DS("slave")` ### 4.2 文件路径 - [x] 新增 `common/utils/WorkspacePathGuard`:归一化 + 符号链接 + `toRealPath` 三层校验 - [x] `/dm/preFile`:目录/文件都必须在 `workspace/<当前caseId>` 内 - [x] `/dm/doClean`:优先用 DB 中 fileId 对应路径,并限制在当前案件;不再信任客户端 `filePath` - [x] `/sys/downloadFile`:下载路径必须在当前案件 workspace 内 - [x] `/py/files/{workspaceId}/...`:workspaceId 必须等于当前案件 ### 4.3 案件库路径 - [x] `CaseInfoService.open`:dbPath 必须存在、非空;**不限制所在目录**(允许历史案件/外部挂载目录) - [x] `CaseDataSourceRegistry.open`:文件不存在/为空直接拒绝,避免 DuckDB 静默建空库 --- ## 5. 生命周期与缓存 - [x] `CaseInfoService.open`:先校验容量与 dbPath → 先打开新数据源 → 再关闭旧数据源;失败不丢旧案件 - [x] 换案/退案/删案前调用 `CaseActivityProbe`,有在途清洗/治理任务返回 409 - [x] `CaseDataCache.initCache` 失败只记日志,不阻断开案 - [x] `delete` 删除当前案件时清 token、SSE、`GOVERN_CONFIG` - [x] 新增 `CaseDataCleanupService`:按 caseId 清理聊天/研判/AI 清洗/附件(行、文件、向量) - [x] `GovernCache.getSql()` 不再写静态 `TABLE_MAPPINGS`,消除并发串表名 - [x] `CaseDataCache.cleanGovernConfig` 在 exit/delete/closeOwnedCases/登出/回收路径调用 - [x] `SseDTO` 增加 `caseId`,发送时按当前上下文补齐;前端进度页丢弃非当前案件的帧 - [x] `strict: true`:`@DS` 拼错直接报错,不再静默回退主库 --- ## 6. 验证 ### 6.1 已通过 - [x] `mvn -pl ai-server -am -DskipTests compile` / `package` - [x] 全量单测:467 个,0 失败、0 错误、17 skipped(BUILD SUCCESS) - [x] `SqlAnalysisToolTest`:9/9(含 `read_csv`/`glob`/`read_parquet` 拒绝) - [x] `AgentCapabilityToolkitTest`:1/1 - [x] `SpecialDateServiceRoutingTest`:1/1(@DS + @Transactional) - [x] `CaseAccessGuardTest`:4/4(无案件/上下文不一致/他人案件/本人案件) - [x] `AiServerApplicationTests.contextLoads()`:1/1(含 strict=true、MCP SDK exclusion) - [x] 前端 `pnpm build` 通过 - [x] 前端 `pnpm type:check`:改动文件无新增错误(仓库存在既有 `src/trans` 等类型错误) - [x] 全仓源码 MCP 引用清零;产物 jar 中无 `mcp-*.jar`、无 MCP class ### 6.2 待人工验收 1. MCP:`/mcpstreamable`、`/mcpsse`、`/capability/mcp` 均不可用;Agent 对话/工具正常 2. 单端登录:第二次登录旧 token 401;旧案件数据源释放 3. 多窗口:主窗口 A→B,工作台自动 reload,后续请求全部作用于 B 4. 跨案件:A 传 B 的 workspaceId/sessionId/jobId 返回 403/404;无案件访问聊天/研判 400 5. SpecialDate:新增/修改写案件 DuckDB;主库无 `special_date`;失败回滚 6. 文件:`../../other-case/x.xlsx`、绝对路径、符号链接均被拒绝 7. 生命周期:有清洗/治理任务时换案/退案/删案返回 409 8. SSE:旧案件进度不显示在新案件页面 ### 6.3 数据审计 SQL(发布前执行) ```sql -- 无归属案件 select id, name, db_path from case_info where owner_id is null; -- db_path 为空(无法开案;目录位置不再限制,只要文件存在即可) select id, name, db_path from case_info where db_path is null or trim(db_path) = ''; -- 主库孤儿 AI 数据(示例:不存在的 case) select distinct case_id from agent_chat_session where case_id not in (select id from case_info); select distinct case_id from agent_insight_session where case_id not in (select id from case_info); select distinct case_id from ai_clean_job where case_id not in (select id from case_info); ``` --- ## 7. 剩余风险 1. `owner_id == null` 的历史案件仍按 warn 放行,需迁移后收紧。 2. Python 工具是兜底黑名单,不是真沙箱;生产建议关闭或容器化。 3. 删除案件的主库数据清理是尽力而为,失败会记 error 日志,需按 caseId 补删。 4. 工作台刷新依赖 Electron IPC;浏览器多标签页仍共享同一 token,换案后需手动刷新另一个标签页。