# 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 `,缺失/无效/禁用/过期 → 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 -p < sql/bootstrap/create-database.sql # 启动一次让 ToolSyncService 同步 tool 表后: mysql -h192.168.1.181 -u -p xiaoqu_agent_svc_d < sql/seed-dev.sql # 必须显式指定环境及凭据;m0/m1/m2 使用数据库只读账号 export SPRING_PROFILES_ACTIVE=dev export M0_DB_USERNAME= export M0_DB_PASSWORD= export M1_DB_USERNAME= export M1_DB_PASSWORD= export M2_DB_USERNAME= export M2_DB_PASSWORD= export SVC_DB_USERNAME= export SVC_DB_PASSWORD= # 已有 V1 库首次接入时设 true;确认 flyway_schema_history 后可设 false export SVC_FLYWAY_BASELINE_ON_MIGRATE=true export REDIS_PASSWORD= # 仅首次、且 admin_user 为空时创建管理员;创建完成后可移除这三个变量 export ADMIN_BOOTSTRAP_ACCOUNT= export ADMIN_BOOTSTRAP_PASSWORD= export ADMIN_BOOTSTRAP_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、数据库账号或密码;缺失配置时不得启动。 - 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 # 状态 / 跟踪日志 / 停止 ``` - 凭据从 `deploy/.env.dev` 读取,无需手工 export;日志在 `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 ' \ -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 ' -H 'Mcp-Session-Id: ' curl -X POST http://localhost:8088/mcp \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'Mcp-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= export CODEGEN_DB_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 文件,避免覆盖人工改动。