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

213 lines
14 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`。当前进度:**P4 管理后台完成**(P1 骨架 + P2 权限层 + P3 首批工具 + P4 Vue 管理后台)。
## 技术栈与已验证版本组合
| 组件 | 版本 |
|---|---|
| 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) |
| 管理后台 | Vue 3 + Element Plus + Vite 7 |
> ⚠️ 版本组合说明: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/
│ ├── README.md # migration 编写、回滚与不可变规则
│ ├── bootstrap/create-database.sql # 首次创建数据库(结构不在这里维护)
│ ├── rollback/ # 与每个版本配套的人工回滚方案
│ └── seed-dev.sql # dev 种子数据,不进入 Flyway
├── front/ # Vue 3 管理后台,Maven 打包时自动构建进 jar
├── 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/ # 后台登录、会话/CSRF、Token/工具/作用域/审计 API
│ ├── entity/m0、m2、svc mapper/m0、m1、m2、svc # codegen 生成物(提交进仓库)
├── src/main/resources/
│ ├── application.yml # MCP server: protocol=STREAMABLE, type=SYNC,端点 /mcp
│ ├── db/migration/ # Flyway V1/V2... + 历史文件校验和清单
│ └── application-dev.yml # 四数据源 + Redis,账号密码必须由环境变量提供
└── src/test/java/me/iiv/agent/ # 权限回归测试 + 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 → 代理商;isagent=0 → 总部;缺失/非法配置拒绝访问 |
| `visibleAgentIds(user)` | `getByIsAgent` 主重载 | isagent!=1 返回 null(不过滤);否则 自己 + 直属下级(supervisor_id 一层)+ m2 warehouse_agent_auth 代管 manager_agent_id |
| `isSalesNonLeader(user)` | saleFlag 判定(:703-710) | Redis db5 `Dictionary` hash 的 authDeptId 命中 ",deptId," 且 authDeptPostId 不含 "postId,";字典缺失时拒绝访问 |
| `applyPropertyRowFilter` 销售分支 | saleFlag 分支(:716-735,互斥 return) | **1:1 复刻(2026-09-03 决策)**:id IN (create_id=自己 ∪ [代管授权非空时] agent_id IN (下级+代管移除自己) 的物业),空集合则 id IN (0);命中后不再叠加 agent_id/代管物业 OR 结构 |
| `isPropertyUser(user)` | enterprise_type=2 规则(§3.3 通用规则,保留备用) | 物业用户;**property.list.query 不使用本规则**(物业用户不查此列表,与老 /property/listByPage 一致,2026-09-03 决策) |
`agent.list.query` 的行权限照搬老系统 `AgentController.listByPage`:isagent=1 默认只查直属下级;传 `id` 时只能精确查询自己或直属下级;isagent=0 可按任意合法 id 查询。
## 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/province/city/serialnumber/page/rows;出参 id/name/level/channelNo/serialnumber/supervisorName/province/city/countyseat/contact/phone/agencyarea/memo/createTime。行权限:isagent=1 只查直属下级,id 精确查可看自身。supervisorName 批量 IN 富化 |
| `property.list.query` | customer | read | 物业(客户)分页查询(m0 properyt 表)。入参 name/agentId/serialnumber/province/city/contact/phone/page/rows;出参 id/name/serialnumber/agentName/createName/province/city/countyseat/address/contact/phone/createTime。行权限走 PermissionService 完整版 getByIsAgent 语义(不含 enterprise_type=2 限制,与老 /property/listByPage 一致)。agentName/createName 批量 IN 富化 |
分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。
工具名使用点号分隔(MCP 规范允许 `[a-zA-Z0-9._-]`,实测 Spring AI 1.1.8 与握手流程均正常)。工具的 category/kind 由代码 `@XqTool` 注解声明,启动时同步进 `tool` 表(只插不删,管理员停用状态不会被覆盖)。新工具默认不属于任何作用域 = 任何 token 调不到,需管理员配 scope。
## 启动
```bash
# 首次仅由数据库管理员创建空库;表结构由应用启动时的 Flyway 自动迁移
mysql -h192.168.1.181 -u<svc_db_admin> -p < sql/bootstrap/create-database.sql
# 启动一次让 ToolSyncService 同步 tool 表后:
mysql -h192.168.1.181 -u<svc_db_admin> -p xiaoqu_agent_svc_d < sql/seed-dev.sql
# 数据库/Redis 连接信息已明文写在 application-dev.yml(内网 dev 库);
# 仍需显式指定 profile
export SPRING_PROFILES_ACTIVE=dev
# 已有 V1 库首次接入时设 true;确认 flyway_schema_history 后可设 false
export SVC_FLYWAY_BASELINE_ON_MIGRATE=true
# 仅首次、且 admin_user 为空时创建管理员;创建完成后可移除这三个变量
export ADMIN_BOOTSTRAP_ACCOUNT=<admin_account>
export ADMIN_BOOTSTRAP_PASSWORD=<strong_password>
export ADMIN_BOOTSTRAP_NAME=<display_name>
./mvnw clean package
java -jar target/xiaoqu-agent-service.jar
# 或
./mvnw spring-boot:run
# 后台常驻(本机兼任 dev 部署机时):
nohup java -jar target/xiaoqu-agent-service.jar > target/app.log 2>&1 &
```
- 不提供默认 profile;数据库/Redis 连接信息明文写在 `application-dev.yml`(内网 dev 库,与 xiaoqu-franchisee pom.xml dev profile 同一惯例);生产/测试环境不得在配置文件写明文密码。
- dev 端口 **8088**,管理后台 `http://localhost:8088/`,MCP 端点 `http://localhost:8088/mcp`。
- Flyway 在 Web 服务启动前执行 `validate + migrate`;checksum、版本或 SQL 执行失败时应用不会启动。
- 新增 SQL 必须遵循 [sql/README.md](sql/README.md),禁止修改已合并的历史 migration。
### 部署脚本(推荐,本机 dev 部署)
```bash
cp deploy/.env.example deploy/.env.dev # 填写 dev 凭据(已 gitignore,不入库)
./deploy/deploy.sh build # 全量测试 + 构建 jar(含 migration 治理测试)
./deploy/deploy.sh restart # 停旧起新;启动先迁移,失败立即报告并阻断
./deploy/deploy.sh status | log | stop # 状态 / 跟踪日志 / 停止
```
- `.env.dev` 只需管理员引导和 Flyway 开关;数据库/Redis 已走配置文件。日志在 `logs/agent-service.log`,pid 在 `deploy/app.pid`。
- 注意应用启动较慢(多数据源初始化,约 60-90s),脚本健康检查窗口已放宽到 180s。
## 测试 ak(仅 dev)
仓库不提供固定 ak。每位开发者在本地生成随机 ak,只把 SHA-256 写入 `agent_api_key`,再用 `token_scope` 关联需要的作用域;明文只保存在本地环境变量中,不提交版本库。仓库历史中曾出现的 dev ak 必须视为已泄露并在数据库中禁用。
```bash
export XQ_API_KEY="xqak_$(openssl rand -hex 24)"
printf %s "$XQ_API_KEY" | shasum -a 256
```
## 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。
property.list.query 权限隔离实测(2026-09-03,均与直连 SQL 核对一致):
| 条件 | 总部 ak | 代理商 ak |
|---|---|---|
| 无过滤 | total=141 | total=73 |
| province=北京市 | total=38 | total=31 |
| name 模糊 '%测试%' | — | total=7(与 SQL 一致) |
agent.list.query 抽测:channel_no='D' + province='北京市' → 总部 11 / 代理商 9(均与 SQL 一致);rows=500 → 截断为 100 并返回 notice。安全修复后,代理商精确查询仅允许自身或直属下级,权限外 id 返回空结果。
## 客户端接入(dev 阶段裸跑 HTTP)
dev 服务地址示例:`http://192.168.4.236:8088/mcp`(本机内网 IP;本机自测可用 localhost)。生产必须 HTTPS(Nginx 终结 TLS)。
**Kimi Code** — 项目级 `.kimi-code/mcp.json`:
```json
{
"mcpServers": {
"xiaoqu": {
"url": "http://192.168.4.236:8088/mcp",
"bearerTokenEnvVar": "XQ_API_KEY"
}
}
}
```
shell 里 `export XQ_API_KEY=<测试ak>` 后启动 Kimi Code 即可自发现工具。
**Claude Code**:
```bash
export XQ_API_KEY=<测试ak>
claude mcp add --transport http xiaoqu http://192.168.4.236:8088/mcp \
--header "Authorization: Bearer $XQ_API_KEY"
```
## 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 库)
export CODEGEN_DB_USERNAME=<codegen_user>
export CODEGEN_DB_PASSWORD=<password>
./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 文件,避免覆盖人工改动。