138 lines
9.3 KiB
Markdown
138 lines
9.3 KiB
Markdown
# xiaoqu-agent-service
|
||
|
||
面向 MCP 客户端(Kimi Code / Claude 等)的小趣业务数据服务:独立 Spring Boot 应用,只读查询现有三库(m0 权限库 / m1 商城库 / m2 业务库),以 MCP(Streamable HTTP)形式对外暴露工具。
|
||
|
||
设计方案见 `../xiaoqu-franchisee/doc/agent-mcp-service-design.md`。当前进度:**P2 权限层已完成**(ak 鉴权、scope 校验、行级数据权限引擎、审计落库,均经 curl 实测)。管理后台为 P4,未实现。
|
||
|
||
## 技术栈与已验证版本组合
|
||
|
||
| 组件 | 版本 |
|
||
|---|---|
|
||
| JDK | 17(Temurin 17.0.20.1) |
|
||
| Spring Boot | 3.5.16 |
|
||
| Spring AI(MCP server webmvc) | 1.1.8 |
|
||
| MyBatis-Plus | 3.5.12(`mybatis-plus-spring-boot3-starter`) |
|
||
| MySQL 驱动 | mysql-connector-j(Boot 管理) |
|
||
| Redis | spring-boot-starter-data-redis(db5) |
|
||
|
||
> ⚠️ 版本组合说明:Spring Boot 3.5.x + Spring AI 1.0.0 的 streamable 传输存在 NPE([spring-projects/spring-ai#6274](https://github.com/spring-projects/spring-ai/issues/6274)),本工程选用已修复的 **Spring AI 1.1.8**,2026-09-03 实测 STREAMABLE 握手(initialize → tools/list → tools/call)通过。
|
||
|
||
## 工程结构
|
||
|
||
```
|
||
xiaoqu-agent-service/
|
||
├── mvnw / .mvn/wrapper/ # Maven Wrapper(本机无全局 mvn)
|
||
├── pom.xml
|
||
├── sql/
|
||
│ ├── V1__init.sql # 自有库 xiaoqu_agent_svc_d 建表(手工执行,未接 flyway)
|
||
│ └── seed-dev.sql # dev 种子数据(测试 ak + 运营作用域授权)
|
||
├── src/main/java/me/iiv/agent/
|
||
│ ├── AgentServiceApplication.java
|
||
│ ├── config/ # M0/M1/M2/SvcDataSourceConfig(独立 SqlSessionFactory + MapperScan)、McpToolConfig
|
||
│ ├── mcp/ # ToolObject 标记、@XqTool 元数据、ToolSyncService(启动同步 tool 表,只插不删)
|
||
│ │ ├── GuardedToolCallback(Provider) # 统一包装:scope 强制校验 + 审计
|
||
│ │ └── tool/ # @Tool 工具(SysTools: sys.ping;AgentTools: agent.count / agent.list.query)
|
||
│ ├── security/ # AkAuthFilter(/mcp ak→401/AuthContext)、AuthService(ak 解析+用户加载)、ToolGuard
|
||
│ ├── permission/ # PermissionService:行级数据权限引擎(移植 getByIsAgent)
|
||
│ ├── common/ # AuditService(audit_log 落库)
|
||
│ ├── admin/ # P4 占位包
|
||
│ ├── entity/m0、m2、svc mapper/m0、m1、m2、svc # codegen 生成物(提交进仓库)
|
||
├── src/main/resources/
|
||
│ ├── application.yml # MCP server: protocol=STREAMABLE, type=SYNC, 端点 /mcp;默认 dev profile
|
||
│ └── application-dev.yml # 四数据源 + Redis,密码用环境变量占位符带默认值
|
||
└── src/test/java/me/iiv/agent/codegen/CodeGenerator.java # run-once 代码生成器
|
||
```
|
||
|
||
数据源分包约定:`entity/m0|m1|m2|svc` 与 `mapper/...` 一一对应各自的 `SqlSessionFactory`(`@MapperScan` + `sqlSessionFactoryRef`)。m0/m1/m2 只读,svc(自有库)读写。
|
||
|
||
## 鉴权(P2)
|
||
|
||
- `/mcp` 全路径要求 `Authorization: Bearer <ak>`,缺失/无效/禁用/过期 → 401 JSON。
|
||
- 判定链(设计文档 §3.2):ak → SHA-256 hex → `agent_api_key` 匹配 → 解析出人(Redis db5 `entity:User:{id}` 热缓存优先,fastjson camelCase JSON 用 Jackson 解析;读不到回源 m0 `user` 表)→ AuthContext 存 ThreadLocal → 工具调用前 scope 校验 → 审计落库。
|
||
- **ThreadLocal 实测结论**:`spring.ai.mcp.server.type=SYNC`(application.yml 已显式设置)时,Spring AI 1.1.8 的工具回调在 servlet 请求线程(`nio-8088-exec-*`)上同步执行,ThreadLocal 的 AuthContext 与 RequestContextHolder 均有效(已用真实 curl 验证:agent.list.query 按人过滤、audit_log 正确落 user_id/token_id/client_ip)。AkAuthFilter 在请求结束 finally 清理。
|
||
- **已知限制**:`tools/list` 暂不按 key 过滤工具列表,所有工具可见;`tools/call` 时的 scope 强制校验是底线。
|
||
- client_type 识别:`X-Client-Type` 头优先,否则 User-Agent 含 kimi→mcp-kimi / claude→mcp-claude / curl→cli,其余 unknown。
|
||
|
||
## 行级数据权限引擎(permission/PermissionService)
|
||
|
||
移植老系统 `WrapperUtils.getByIsAgent` 语义,输出中性 id 集合,调用方拼 wrapper:
|
||
|
||
| 方法 | 老系统来源 | 语义 |
|
||
|---|---|---|
|
||
| `isAgentAuthority(authorityId)` | `AuthorityServiceImpl.checkAuthorityIsagent` | 权限组 isagent=1 → 代理商视角 |
|
||
| `visibleAgentIds(user)` | `getByIsAgent` 主重载 | isagent!=1 返回 null(不过滤);否则 自己 + 直属下级(supervisor_id 一层)+ m2 warehouse_agent_auth 代管 manager_agent_id |
|
||
| `isSalesNonLeader(user)` | saleFlag 逻辑(:382-427) | Redis db5 `Dictionary` hash 的 authDeptId 命中 ",deptId," 且 authDeptPostId 不含 "postId,";字典缺失按不限制处理并告警 |
|
||
| `isPropertyUser(user)` | enterprise_type=2 规则 | 物业用户(后续 property 查询再限 property_id=自己) |
|
||
|
||
`agent.list.query` 的行权限照搬老系统 `AgentController.listByPage`:isagent=1 → `supervisor_id = 用户agentId`(只查直属下级);isagent=0 → 不过滤;入参传 `id` → 精确查询(可查到自身,不加 supervisor 限制)。
|
||
|
||
## MCP 工具
|
||
|
||
| 工具名 | 分类 | 读写 | 说明 |
|
||
|---|---|---|---|
|
||
| `sys.ping` | system | read | 服务自检:服务名、版本、当前时间、Redis PING |
|
||
| `agent.count` | system | read | 查 m0 库 `agent` 表 `soft_delete=0` 总数 |
|
||
| `agent.list.query` | customer | read | 代理商分页查询(行级权限),入参 id/name/level/channelNo/page/rows |
|
||
|
||
工具名使用点号分隔(MCP 规范允许 `[a-zA-Z0-9._-]`,实测 Spring AI 1.1.8 与握手流程均正常)。工具的 category/kind 由代码 `@XqTool` 注解声明,启动时同步进 `tool` 表(只插不删,管理员停用状态不会被覆盖)。新工具默认不属于任何作用域 = 任何 token 调不到,需管理员配 scope。
|
||
|
||
## 启动
|
||
|
||
```bash
|
||
# 首次:建自有库 + 种子数据(dev)
|
||
mysql -h192.168.1.181 -uroot -p < sql/V1__init.sql
|
||
# 启动一次让 ToolSyncService 同步 tool 表后:
|
||
mysql -h192.168.1.181 -uroot -p xiaoqu_agent_svc_d < sql/seed-dev.sql
|
||
|
||
./mvnw clean package -DskipTests
|
||
java -jar target/xiaoqu-agent-service.jar
|
||
# 或
|
||
./mvnw spring-boot:run
|
||
```
|
||
|
||
- 默认 `spring.profiles.active=dev`,端口 **8088**,MCP 端点 `http://localhost:8088/mcp`。
|
||
- 密码默认值仅供本地 dev,环境变量覆盖:`M0_DB_PASSWORD` / `M1_DB_PASSWORD` / `M2_DB_PASSWORD` / `SVC_DB_PASSWORD` / `REDIS_PASSWORD`。
|
||
|
||
## 测试 ak(仅 dev,明文只在此处与 sql/seed-dev.sql 出现,服务端只存 SHA-256)
|
||
|
||
| 用途 | 用户 | ak |
|
||
|---|---|---|
|
||
| 总部视角(isagent=0) | 46 代理商管理端 | `xqak_1d21057ad83203dbeb767eb5069701046b27debdc08a733c` |
|
||
| 代理商视角(isagent=1,agent_id=1,26 个直属下级) | 1000 斤斤计较 | `xqak_7df02ccee2133dc2cf951db12e3958557bdf2d30a91e7f08` |
|
||
| 无任何 scope(验证拒绝) | 1000 斤斤计较 | `xqak_5bdc0eac8c1cab535f22dc3e9757f72b3127de43a14997a7` |
|
||
|
||
## curl 验证(Streamable HTTP 握手)
|
||
|
||
```bash
|
||
# 1) initialize(从响应头取 Mcp-Session-Id)
|
||
curl -i -X POST http://localhost:8088/mcp \
|
||
-H 'Authorization: Bearer <ak>' \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json, text/event-stream' \
|
||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
|
||
|
||
# 2) notifications/initialized → 3) tools/list → 4) tools/call,均带:
|
||
# -H 'Authorization: Bearer <ak>' -H 'Mcp-Session-Id: <session-id>'
|
||
curl -X POST http://localhost:8088/mcp \
|
||
-H 'Authorization: Bearer <ak>' \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json, text/event-stream' \
|
||
-H 'Mcp-Session-Id: <session-id>' \
|
||
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"agent.list.query","arguments":{"rows":100}}}'
|
||
```
|
||
|
||
响应为 SSE 帧(`event: message` / `data: {...}`)。2026-09-03 实测:无 ak/无效 ak → 401;总部 ak 查 agent.list.query total=62(全量);代理商 ak total=26(与 `SELECT COUNT(*) FROM agent WHERE soft_delete=0 AND supervisor_id=1` 一致);无 scope 的 ak → `isError:true` + "该 token 的作用域不包含工具 [agent.list.query],请联系管理员在对应作用域开通";audit_log 记录 success/denied 与 client_type。
|
||
|
||
## codegen(run-once)
|
||
|
||
表白名单:m0 `user, agent, authority, properyt, agent_manage_district_auth`;m2 `warehouse_agent_auth`;svc `agent_api_key, agent_scope, tool, scope_tool, token_scope, audit_log, admin_user`(注:物业表真实表名为老系统遗留拼写 `properyt`)。生成物落在 `src/main/java` 对应分包并提交进仓库。
|
||
|
||
```bash
|
||
# 生成(依赖已下载后执行;连 dev 库)
|
||
./mvnw -q dependency:build-classpath -Dmdep.includeScope=test -Dmdep.outputFile=target/cp.txt
|
||
javac -cp "$(cat target/cp.txt)" -d target/codegen-classes src/test/java/me/iiv/agent/codegen/CodeGenerator.java
|
||
java -cp "target/codegen-classes:$(cat target/cp.txt)" me.iiv.agent.codegen.CodeGenerator
|
||
```
|
||
|
||
重新生成前请先删除目标 entity/mapper 文件,避免覆盖人工改动。
|