Files
xiaoqu_mcp_service/xiaoqu-agent-service/doc/agent-mcp-service-design.md
T

269 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xiaoqu-agent-service 设计方案
> 面向智能体(Kimi Code / Claude 等 MCP 客户端)的业务数据服务:独立新建系统,远程部署,照搬现有 SaaS 的行级数据权限,以"作用域"控制工具可见性,带轻量管理后台。
>
> 状态:讨论定稿(2026-09-01),待进入实施。
---
## 1. 背景与目标
现有 SaaS(xiaoqu-franchisee)是面向 PC 后台人工操作的系统,接口鉴权(uuid + 短信登录态 + 签名)不适合机器调用。本方案新建一套**独立系统**,将仓库、客户等业务数据以 **MCP 服务**形式暴露给智能体,同时提供 CLI 兜底与 Web 管理后台。
设计约束(讨论中明确):
- 与现有系统**零代码耦合**:不改老系统一行代码,耦合面只有数据库(只读)。
- **照搬现有行级数据权限**(`getByIsAgent` 那套规则),不移植字段级/页面级功能权限。
- 远程部署,MCP over Streamable HTTP。
- 用户熟悉 Java:技术栈定为 JDK 17 + Spring Boot 3 + Vue 3。
## 2. 总体架构
```
Kimi Code ──┐
├─ MCP (Streamable HTTP, Bearer ak) ─┐
Claude ─────┤ │
│ ▼
xq CLI ─────┘ (Go 单二进制, 同为 MCP 客户端) ┌─────────────────────────┐
│ xiaoqu-agent-service │
│ (单 jar, Spring Boot 3) │
│ │
┌── Web 管理后台 ──────►│ /mcp MCP 端点 │
│ (Vue3 内嵌打包) │ /api/** 后台 REST │
│ │ static/ 管理页面 │
│ ├─────────────────────────┤
│ │ 鉴权层: ak→人, scope校验 │
│ │ 权限引擎: 行级数据过滤 │
│ │ 数据访问层: 3×MySQL/Redis/ES│
│ └───────┬─────────┬───────┘
│ │ 只读 │ 读写
│ ┌───────▼──┐ ┌───▼────────┐
└────────────────────►│ 现有三库 │ │ 自有库表 │
(user/agent等) │ m0/m1/m2 │ │ token/scope│
│ Redis/ES │ │ tool/audit │
└──────────┘ └────────────┘
```
关键分层:**业务工具只写一遍**,MCP 端点(Kimi/Claude/CLI 同为 MCP 客户端)、后台页面共享同一套鉴权、权限引擎与数据访问层。
## 3. 权限模型(三层)
```
人员(user) 1──n Token(ak) n──n 作用域(Scope) n──n 工具(Tool)
│
└── 行级数据权限: 跟着"人"走,照搬现有系统
```
### 3.1 概念定义
- **Tool**:MCP 暴露的工具(如 `warehouse.stock.query`)。代码中用注解声明,启动时同步进 `tool` 表。读写属性在注册时标注。
- **Scope(作用域)**:命名的工具集合(如"运营作用域""销售作用域")。**纯手工配置**:管理页面上创建作用域、勾选工具。新工具上线默认不属于任何作用域 = 任何 token 都调不到,必须管理员显式开通(安全优点)。
- **Token(ak)**:属于某个人,一人多把(Kimi 一把、Claude 一把)。每把 token 勾选任意组合的作用域。可长期有效(`expire_time` 可空),靠"禁用"回收;可选填过期时间供临时外发场景自毁。
- **职责分离**:token/scope 只决定"能用哪些工具";**能看哪些行永远由 token 背后的人决定**。同一人多把 token,数据视野一致,能力面不同。
### 3.2 调用判定链
1. ak 有效?(SHA-256 哈希匹配、未禁用、未过期)
2. 找得到人?(token → user,user 未删除;用户资料优先读 Redis `entity:User:{id}` 热缓存,变更即时生效,回源 m0 `user` 表)
3. 工具在 token 作用域并集内?→ 否则拒绝(服务端强制,补齐老系统 PermissionInterceptor 空壳的短板)
4. 执行查询,按"人"的数据权限规则加行过滤
5. 审计落库(人、token、工具、入参摘要、结果、耗时)
注:`tools/list` 按作用域过滤(2026-09-04 决策,避免"看得到却调不了"的二义性):MCP SDK 工具清单在启动时静态注册,无法按请求过滤,故由 `ToolsListScopeFilter` 在 servlet 层重写 tools/list 响应。第 3 步的强制点仍是 `tools/call`——绕过 list 直接调未授权工具,得到的仍是可读的拒绝信息 + denied 审计。
### 3.3 行级数据权限移植规范(照搬现有系统)
来源:`WrapperUtils.getByIsAgent` 系列 + `authority.isagent`。规则:
| 条件 | 行过滤 |
|---|---|
| 权限组 `isagent=0`(总部/管理员) | 不过滤,看全部 |
| `isagent=1`(代理商) | `agent_id IN (自己 + 直属下级代理)`,下级只取一层(`agent.supervisor_id`) |
| 代管扩展 | 并入 `warehouse_agent_auth` 中该用户被授权的 `manager_agent_id`;物业维度再并 `agent_manage_district_auth` |
| 物业用户(`enterprise_type=2`) | 再限 `property_id = 自己` |
| 销售部门非负责人(dept 命中字典、post 不在白名单) | 只看 `create_id = 自己` |
实现方式:权限引擎输出中性的"可见 agent id 集合 + 过滤字段",由各数据源适配器翻译成 SQL `IN` 或 ES `terms`。**不做跨库 JOIN**——先算 id 集合,再拼到目标库查询(与老系统实际行为一致)。
## 4. 数据源
| 源 | 内容 | 接入方式 |
|---|---|---|
| m0(权限库) | user、agent、authority、agent_manage_district_auth | 只读连接池 |
| m1(商城库) | 商城业务数据 | 只读连接池(按需启用) |
| m2(业务库) | 仓库/客户/工单等、warehouse_agent_auth | 只读连接池 |
| Redis | `entity:User:{id}` 用户热缓存、Dictionary 字典 | 只读 |
| ES | 部分列表查询 | 预留,一期可后置 |
- 全部使用**只读数据库账号**(物理封死误写);将来写工具单独配有写权限的账号。
- 配置按环境分文件(dev/test/prd);dev 环境密码明文写在 `application-dev.yml`(内网 dev 库,与现有 pom.xml dev profile 明文同一惯例),生产/测试环境不得在配置文件写明文密码。
### 4.1 Entity 生成
不依赖 `xiaoqu-publicbeanandmap` jar(MyBatis-Plus 3.4 旧依赖链与 Boot 3 冲突)。新工程内置 `mybatis-plus-generator`(run-once),**表白名单制**生成 Entity/Mapper,按来源库分包 `entity/m0|m1|m2`,生成物提交进仓库归新系统所有。Mapper XML 基本不用,`LambdaQueryWrapper` 为主。
## 5. 对外形态
### 5.1 MCP(主形态)
- Streamable HTTP 传输,端点 `https://mcp.<域名>/mcp`。
- 客户端配置(Kimi Code `~/.kimi-code/mcp.json` 或项目级 `.kimi-code/mcp.json`,ak 明文直写;项目级文件已加入 `.gitignore`):
```json
{
"mcpServers": {
"xiaoqu": {
"url": "https://mcp.<域名>/mcp",
"headers": {
"Authorization": "Bearer <ak>"
}
}
}
}
```
- Claude Code:`claude mcp add --transport http xiaoqu https://mcp.<域名>/mcp --header "Authorization: Bearer <ak>"`。
- ak 明文只出现在服务端签发响应和运行时;服务端保存 SHA-256 哈希及加密密文,含 ak 的客户端配置文件不得提交版本库。
### 5.2 Web 管理后台(四个页面)
- **用户与 Token 管理**:人员选自现有 `user` 表,**只列企业账号**(`enterprise_type` 0=企业内部 / 1=代理,历史 NULL 保留),物业账号(=2)不进列表——Token 是给能查代理商/客户数据的人用的;每人名下签发/禁用 token,展示 key 前缀、作用域、最后使用时间;选中人员展示其**数据权限归属**(权限组/isagent、所属代理、企业类型、可见范围说明,只读,便于排查"查不到数据")。**一作用域一 Token**:签发时选 N 个作用域生成 N 把 token(每把独立绑定单作用域,独立吊销/审计,明文逐把展示一次);编辑存量 token 时作用域收敛为单选。
- **工具管理**:工具列表来自服务端注册自动同步(工具名/分类/读写类型页面只读);展示每个工具被哪些作用域引用;「停用」为全局紧急开关。分类(category)在代码注册时由开发者声明(工具的业务域归属是客观属性,不允许页面上修改,避免与代码漂移)。
- **作用域管理**:作用域 CRUD + 勾选工具(工具按 category 分组展示)。
- **审计日志**:按人/token/工具/时间筛选。
原型见 `doc/prototype/admin-prototype.html`(浏览器直接打开)。
后台登录为独立的管理员账号密码(session/JWT),与 ak 体系分离。
### 5.3 CLI(兜底,P3 之后实施)
**核心决策:CLI 不是第二套服务,而是 `/mcp` 端点的一个 MCP 客户端**(Streamable HTTP + `tools/call`)。服务端零新增暴露面,ak/作用域/数据权限/审计自动生效;CLI 启动时通过 `tools/list` 自发现工具,服务端新增工具无需升级 CLI。
- **语言:Go**(与服务端 Java 栈解耦,语言自由)。单文件静态二进制分发,交叉编译覆盖 Mac/Linux,调用环境无需任何运行时。备选 Python(官方 MCP SDK 最全,但依赖本机 Python/uv)。
- **命令规范**:从工具名派生,不手工维护——`warehouse.stock.query` → `xq warehouse stock query --warehouse-id 7`;参数与 help 从 `tools/list` 的 schema 生成。
- **输出约定**:stdout 恒为 JSON(`--pretty` 给人看);错误统一 `{"error":{code,message}}` 且 exit code 非 0;`--schema` 吐入参 JSON Schema 供 agent 自发现。
- **探测命令**:`xq whoami`(ak 归属的人 + 作用域,兼作连通性自检)、`xq tools`(列出本 ak 可用工具)。
- **配置**:`~/.xq/config.yaml` 存 base-url、多环境 profile(`--env dev|test|prd`)与 ak 明文(本机文件,权限 600),与 mcp.json 明文直写同一风格。
- **服务端配套改动**(仅两点):审计表加 `client_type`(mcp-kimi / mcp-claude / cli,从请求头识别);错误消息面向"人和 agent 都可读"(如权限拒绝时提示应由管理员在哪个作用域开通)。
### 5.4 Open API(服务间接口)
供其他业务系统/智能体平台在服务端到服务端场景下获取用户 Token,路径 `/api/open/**`,与 ak 体系、后台 session 体系完全分离。
- **鉴权**:静态服务间密钥,header `X-Open-Key`,配置项 `app.open.api-key`(dev 明文写 `application-dev.yml`;配置为空 = 接口关闭)。常量时间比较防时序侧信道。
- **`POST /api/open/token/ensure`**(get-or-create):
- 入参:`mobile`、`email`(至少一个,手机号优先)、`scopeCode`(作用域编码,必填)。
- 人员只认企业账号(`enterprise_type` 0/1,NULL 保留,与后台用户列表同口径);查不到 → 404。
- 该人员在该作用域下**已存在**有效 Token(正常且未过期)→ `created=false`,只回 `keyPrefix`/`expireTime` 等元信息,明文不回传。
- **不存在** → 自动创建永久 Token(一作用域一 Token,memo 标记 `open-api 自动签发`),`created=true` 返回明文,仅此一次。
- 全程写 `audit_log`(`tool_name=open.token.ensure`,`client_type=open-api`,参数摘要脱敏)。
## 6. 技术栈与工程结构
- **JDK 17 + Spring Boot 3.x**(Spring AI MCP starter 的硬性要求)
- MCP:`spring-ai-starter-mcp-server-webmvc`,`protocol: STREAMABLE`;工具 `@McpTool` 注解声明
- 数据:MyBatis-Plus 3.5(`mybatis-plus-spring-boot3-starter`)三数据源、Redis、ES 客户端
- 前端:Vue 3 + Element Plus,`yarn build` 产物打入 jar `static/`(`frontend-maven-plugin` 联动构建),单 jar 部署,Nginx 终结 TLS
- ⚠️ 开工先验证 Spring Boot / Spring AI 版本组合(Spring Boot 3.5.x 曾报 streamable NPE:spring-projects/spring-ai#6274),跑通 demo 再铺开
```
xiaoqu-agent-service/
├── src/main/java/me/iiv/agent/
│ ├── config/ # 三数据源、Redis、ES、MCP server
│ ├── mcp/ # @McpTool 工具(按业务域分包) + 注册同步
│ ├── security/ # ak Filter、身份上下文、scope 校验
│ ├── permission/ # 行级数据权限引擎(移植 getByIsAgent)
│ ├── admin/ # 后台 REST: 用户/token、作用域、审计
│ ├── entity/ # m0/m1/m2 生成实体
│ └── common/ # 审计 AOP、限流、结果包装
├── src/main/resources/ # application-{dev,test,prd}.yml
├── codegen/ # mybatis-plus-generator 配置(run-once)
└── front/ # Vue3 + Element Plus
```
## 7. 自有库表(新 schema,如 `xiaoqu_agent_svc`)
| 表 | 关键字段 | 说明 |
|---|---|---|
| `agent_api_key` | user_id、key_prefix、key_hash(SHA-256)、status、expire_time(可空=永久)、last_used_time | token;user_id 逻辑关联 m0.user |
| `scope` | name、code、memo、status | 作用域 |
| `scope_tool` | scope_id、tool_id | 作用域-工具 |
| `token_scope` | token_id、scope_id | token-作用域 |
| `tool` | name、category、description、kind(read/write)、status | 工具注册表,启动时同步;category 代码声明 |
| `audit_log` | user_id、token_id、tool_name、params_digest、result_status、cost_ms、client_ip、client_type | 审计;client_type 区分 mcp-kimi/mcp-claude/cli |
| `admin_user` | account、password_hash | 后台管理员 |
均带标准审计五件套(create/modify id/time、soft_delete)。
## 8. 安全基线
- dev/内网测试阶段允许裸跑 HTTP(客户端 URL 用 `http://`);**对外/生产必须 HTTPS**(Nginx 终结 TLS,ak 在 HTTP 下明文可被抓包)
- ak 只存哈希;审计不记录 ak;限流按 token 维度(防 agent 死循环打爆库)
- 数据访问全部只读账号;scope 最小化发放,新工具默认不可用
- 可选:Nginx IP 白名单(客户端出口固定时)
## 9. 阶段规划
| 阶段 | 内容 | 验收 |
|---|---|---|
| P1 骨架 | 工程初始化、版本组合验证、三数据源连通、codegen 跑通 | demo 工具经 MCP 调通 |
| P2 权限 | ak 鉴权 Filter、scope 校验、数据权限引擎、审计 | 不同权限组的人查询结果隔离正确 |
| P3 首批工具 | 代理商查询 + 客户(物业)查询(定义见 §9.1)+ tool 表同步 | Kimi/Claude 双端实测 |
| P4 管理后台 | 用户/token、工具、作用域、审计四页面(Vue3 内嵌) | 管理员全流程自助配置 |
| P5 加固 | 限流、ES 接入(按需)、写工具评审开放、CLI(Go 单二进制) | 生产上线 |
### 9.1 首期工具定义(P3,均已确认)
对应现有接口 `/agent/listByPage` 与 `/property/listByPage`,数据均查 m0 库,行级权限照搬 §3.3。入参精选(不做 beanToWrapper 全字段放行),枚举值写入工具 description 供模型使用;名称富化(supervisorName/agentName/createName)批量查询,消除老实现的 N+1。
**`agent.list.query`**(分类 customer,读)
- 语义照搬现有页面:isagent=1 时只查**下级代理**(`supervisor_id = 自己agentId`);isagent=0 看全部。额外允许入参传 `id` 精确查询(可查到自身)。
- 入参:`id`(精确)、`name`(模糊)、`level`(0总部/1一级/2二级)、`channelNo`(Z/F/D)、`province`、`city`、`serialnumber`、`page`(默认1)、`rows`(默认20,上限100)。**不含 `type`**(已确认去掉)。
- 出参:id、name、level、channelNo、serialnumber、supervisorName、province/city/countyseat、contact、phone、agencyarea、memo、createTime。
- 已裁剪:开票信息、银行账户、营业执照图片 URL(OSS 依赖,二期再议)。
**`property.list.query`**(分类 customer,读)
- 语义照搬:完整版 `getByIsAgent`(下级代理 + warehouse_agent_auth 代管 + agent_manage_district_auth 代管物业 + 销售非负责人规则)。销售非负责人规则 2026-09-03 决策:**1:1 复刻老代码** saleFlag 分支(WrapperUtils.java:716-735)——可见范围 = 自己创建的物业(create_id=自己)∪ 代管授权非空时 agent_id IN (下级+代管,移除自己) 的物业,即 `id IN (...)` 结构,且与其余规则互斥(saleFlag 命中即 return);不再按 §3.3 表格收窄为 AND create_id=自己。**不含** enterprise_type=2(物业用户限自己物业)限制——物业用户不查此列表,与老接口 `/property/listByPage`(PropertyController.java:95)语义一致(2026-09-03 决策;§3.3 通用规则表保留该条,仅此处不适用)。
- 入参:`name`(模糊)、`agentId`、`serialnumber`、`province`、`city`、`contact`、`phone`、`page`、`rows`。
- 出参:id、name、serialnumber、agentName、createName(销售)、province/city/countyseat、address、contact、phone、createTime。
- 已裁剪:开票信息、银行账户。
## 10. 已明确的取舍记录
- 不移植字段级(page_field 六表)功能权限 → 用 scope 替代,更简单且服务端强制
- 作用域纯手工配置,不做代码内默认归属 → 新工具默认关闭
- 工具分类(category)代码注册时声明,页面只读 → 分类与代码业务归属不漂移
- token 可长期有效 → 靠禁用回收,过期时间可选
- CLI 复用 `/mcp` 端点做 MCP 客户端,不开第二套服务 → 权限逻辑只有一份;Go 单二进制分发,与服务端 Java 栈解耦
- 不依赖老 jar,Entity 自行生成 → 与老系统零代码依赖
- 前后端一体打包单 jar → 可逆决定,将来可拆
- 一作用域一 Token(2026-09-04)→ 收回某能力不需整把吊销;数据模型不变(token_scope 每把只写一行)
- 长期 Token 仅按用户首次创建返回明文一次;后续复用元信息,不对外提供二次获取明文接口(服务端仅用于禁用/审计与治理)
- Open API 鉴权用配置文件静态密钥(dev)→ 服务间调用方少,落库+轮换留给生产阶段
- 列表类接口一律服务端分页,pageSize/rows 上限 100(超出截断为 100)→ 防止一次性拉全表,MCP 工具与后台 API 同一尺度
## 11. 开发环境(dev)
**本机即开发环境并兼任部署机**:程序直接跑在本机,dev 阶段裸跑 HTTP(§8),端口避开老系统的 8091。数据库/Redis/ES 连接信息沿用 `xiaoqu-franchisee/pom.xml` dev profile:
| 组件 | 地址 | 账号 | 库/DB |
|---|---|---|---|
| MySQL m0(权限库) | 192.168.1.181:3306 | root | `xiaoqu_comples_d`(user/agent/authority/代管物业表在此) |
| MySQL m1(商城库) | 192.168.1.181:3306 | root | `xiaoqu_mall_d`(按需启用) |
| MySQL m2(业务库) | 192.168.1.181:3306 | root | `xiaoqu_franchisee_d`(warehouse_agent_auth 在此) |
| Redis | 192.168.1.181:6379 | 密码见 pom dev | db5(`entity:User:{id}`、Dictionary)、db13 |
| ES | 192.168.1.152:9200 | 无认证 | 预留,一期不用 |
- 密码等敏感值不入库本文档;dev 环境 DB/Redis 连接信息明文写在新系统 `application-dev.yml`(内网 dev 库,与 `pom.xml` dev profile 明文同一惯例)。
- 自有库表(§7)建在同实例新 schema:dev 用 `xiaoqu_agent_svc_d`(沿用 `_d` 后缀约定),首次部署前手工建库。
- 新系统与老的 dev 后端(Jetty 8091)可同时运行,互不干扰——注意两边连的是**同一套** dev 数据库和 Redis,老系统dev库的数据变动会同时影响两边的查询结果。
## 12. 开发流程约定
- **一律用 git worktree 开发**:每个需求从 main 拉分支到 `../oldproject-worktrees/<feature>/`,在 worktree 里改代码、跑测试;验证通过后合并回 main 再推送远程。主检出(本目录)保持可随时部署的干净状态。
- 合并回 main 后用 `xiaoqu-agent-service/deploy/deploy.sh restart` 完成构建+重启,接口实测通过再 `git push`。
- 后台列表页统一服务端分页(page 从 1 开始,pageSize 默认 20、上限 100);MCP 工具 `rows` 同样上限 100,超出截断并在 `notice` 字段提示。
- **新增/修改 MCP 工具必须同步更新 `doc/agent-mcp-tools-reference.md`**(入参、出参、权限语义、示例),工具对外契约以该文档为准。