Files
xiaoqu_mcp_service/xiaoqu-agent-service/README.md
T

138 lines
9.3 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
面向 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 文件,避免覆盖人工改动。