Files
zyt/docs/plans/ai-mcp-2026-09-24.md
T
2026-09-24 09:45:44 +08:00

100 lines
10 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.
# AI 助手(MCP)只读数据查询:实现与部署
日期:2026-09-24。状态:已在本地一次性测试库完成实现与测试;未部署线上、未迁移线上数据库。
对接方:行知 AI 工作助手(方案见行知项目 `docs/zyt-mcp-plan.md`)。员工在行知里用甄养堂后台账号密码绑定,之后 AI 按该账号自己的权限和数据范围只读查询甄养堂数据。
## 1. 改动范围
**只新增文件,不修改任何已有接口、控制器、Logic、中间件或配置文件。**
| 位置 | 内容 |
|---|---|
| `server/app/mcp/controller/` | `IndexController`(`POST /mcp`,MCP 端点)、`AuthController`(`/mcp/auth/grant|revoke|whoami`)、`AdminController`(后台管理页用的 `/mcp/admin/*`) |
| `server/app/mcp/service/` | 授权令牌、权限判断(默认拒绝)、数据目录、进程内调用(只读事务)、字段脱敏、审计、限流、MCP 协议、工具 |
| `server/app/mcp/catalog/` | `generated.php`(全部后台接口盘点,脚本生成)、`resources.php` + `review/*.php`(人工审核结论) |
| `server/app/mcp/cli/` | `catalog.php`(重新盘点接口)、`probe.php`(以某账号身份在只读事务里逐个试跑资源,用于审核)、`coverage.php`(逐张表检查覆盖,`--write-tables` 为没有后台页面的业务表生成数据表资源) |
| `server/database/migrations/2026_09_24_ai_mcp.sql` | 新表 `zyt_ai_grant`、`zyt_ai_access_log`;“AI 助手”菜单及权限点 |
| `server/tests/AiMcp*Test.php` | 单元测试、只读保护测试、HTTP 契约测试 |
| `admin/src/api/ai_mcp.ts`、`admin/src/views/ai_mcp/` | 后台页面:AI 授权管理、AI 访问日志、AI 数据目录 |
## 2. 工作方式
1. **授权**:`POST /mcp/auth/grant`(账号 + 密码)校验与后台登录相同的密码算法,再检查:未停用、已完成首次改密(`is_paw=1`)、企微强制绑定规则、拥有 `ai.mcp/access` 权限点。通过后签发 `zyt_ai_` 开头的随机令牌,库里只存 SHA-256。
- 与后台登录会话(`zyt_admin_session`)完全独立:不占终端、不受 IP 绑定影响,不会挤掉浏览器、医生工作站或企微客服端。
- 失败锁定按账号计(5 次 / 30 分钟),另按来源 IP 限速;账号不存在与密码错误给同样提示。
- 同一客户端实例重新绑定时旧令牌自动作废。
2. **每次调用都实时校验**:令牌有效期(默认 90 天)、闲置(默认 30 天)、账号未删除/未停用、密码未修改(签发时记录密码指纹,改密即失效)、仍有 `ai.mcp/access`。角色权限实时计算,调整角色立即生效。
3. **查询执行**:AI 只能查询“数据目录”里已开放的资源。每次查询:
- 权限点必须已在菜单登记、未停用,且该账号拥有(**默认拒绝**;不沿用后台“未登记接口任何人可访问”的规则,也不沿用 `progress_board` 等旁路);
- 参数白名单:去掉导出、关闭分页、扩大数据范围的参数,分页强制 ≤ 50 条,日期跨度 ≤ 366 天;
- 在当前进程内构造一个只含白名单参数的 GET 请求,挂上与登录中间件同结构的 `adminInfo`,调用**后台原有的控制器方法**(或审核文件指定的只读 Logic 方法),数据范围逻辑原样生效;
- 整个调用包在 `READ ONLY` 事务里,结束一律回滚:任何写库都会报错并撤销,AI 查询不会改动数据;单条 SQL 10 秒超时;
- 返回前脱敏:删除密码、盐、令牌、密钥、证书、加密字段;手机号、身份证号、住址、银行卡、附件地址按权限脱敏(拥有 `tcm.diagnosis/phonePlain` 可见明文手机号,拥有 `ai.mcp/sensitive` 可见全部);
- 写 `zyt_ai_access_log`:账号、工具、资源、参数(已脱敏)、返回记录 ID、行知任务号(`X-Xingzhi-Task-Id`)。
4. **数据目录**:`generated.php` 盘点了全部 510 个后台接口;`review/*.php` 逐个给出结论(开放 / 待整改+原因 / 不开放+原因),2026-09-24 审核 250 条:开放 137(含 19 张数据表资源)、待整改 27、不开放 86;另有 231 个写操作接口自动不开放。137 张表:67 张经接口覆盖、19 张经数据表资源覆盖(默认仅 root,权限点 `ai.mcp/tables`)、51 张为凭据/配置/日志等系统表。未审核的接口按保守规则处理:写操作、POST、免登录、系统配置/工具类一律不开放;详情类、调用外部接口、疑似写库、权限点未登记的一律待整改。后台“AI 数据目录”页可查看每个资源的状态和原因。
## 3. MCP 接口
- 端点:`POST https://admin.zhenyangtang.com.cn/mcp`,Streamable HTTP,只返回 JSON,无会话;支持协议 2025-11-25 / 2025-06-18 / 2025-03-26;`GET` 返回 405。
- 请求头:`Authorization: Bearer zyt_ai_…`(必需)、`MCP-Protocol-Version`、`X-Xingzhi-Task-Id`(可选,写入审计)。浏览器 `Origin` 不在白名单一律 403。
- 工具(全部标注 `readOnlyHint`):`zyt_whoami`、`zyt_catalog`、`zyt_describe`、`zyt_query`、`zyt_get`、`zyt_count`、`zyt_file`,以及按权限出现的快捷统计工具 `zyt_stats_appointments`、`zyt_stats_doctor_workload`、`zyt_stats_orders`、`zyt_stats_prescription_orders`、`zyt_stats_performance`、`zyt_my_patients`、`zyt_roster`。
- 授权接口:`POST /mcp/auth/grant`、`POST /mcp/auth/revoke`(Bearer)、`GET /mcp/auth/whoami`(Bearer),返回与后台一致的 `{code, show, msg, data}`;失败时 `data.reason` 为 `invalid_credentials / disabled / need_change_password / need_bind_wecom / no_ai_permission / locked / feature_disabled / ip_not_allowed / invalid_request`。
## 4. 配置(服务器私密 `server/.env`)
```ini
[AI_MCP]
ENABLED = false ; 默认关闭,验证通过后再改为 true
TOKEN_TTL_DAYS = 90
TOKEN_IDLE_DAYS = 30
ALLOWED_IPS = ; 行知服务器出口 IP,逗号分隔;为空不限制(生产建议填写)
ALLOWED_ORIGINS = ; 一般留空:服务端调用不带 Origin
RATE_PER_MINUTE = 60 ; 每个账号每分钟调用次数
DAILY_ROWS = 5000 ; 每个账号每天通过 AI 返回的最大行数
MAX_PAGE_SIZE = 50
MAX_RANGE_DAYS = 366
LOG_RETENTION_DAYS = 180 ; 访问日志保留天数(《网络安全法》要求不少于六个月)
LOCK_FAILURES = 5
LOCK_MINUTES = 30
REQUIRE_PASSWORD_CHANGED = true
```
限流和锁定使用系统缓存;线上建议 `cache.driver = redis`(文件缓存下计数为近似值)。服务在负载均衡或 CDN 之后时,需先让 `request()->ip()` 取到真实客户端 IP,`ALLOWED_IPS` 才有意义。
## 5. 部署顺序
1. 备份数据库;执行 `server/database/migrations/2026_09_24_ai_mcp.sql`(默认前缀 `zyt_`,可重复执行,只新增表和菜单)。
2. 同步 `server/app/mcp/` 与后台前端(`admin` 重新构建,新增三个页面)。代码同步后 `ENABLED` 仍为 `false`,对现有功能无影响。
3. 在“权限管理 > 角色”中给试点角色勾选“允许 AI 助手查询”(`ai.mcp/access`),管理员角色勾选“AI 授权管理 / AI 访问日志 / AI 数据目录”。默认不授予任何角色。
4. 在预发/测试库上以 root 账号运行 `php app/mcp/cli/probe.php --admin=<root 的 ID>` 与 `php app/mcp/cli/coverage.php`,确认没有 `writes`(只读保护拦截)结果、没有未覆盖的表;有的话在对应 `review/*.php` 把该资源改为待整改或改用只读 Logic,或用 `coverage.php --write-tables` 补数据表资源。
5. `.env` 设置 `[AI_MCP] ENABLED = true` 与 `ALLOWED_IPS`;nginx 对 `/mcp` 与 `/mcp/auth/grant` 加 `limit_req`,并确认不缓冲响应。
6. 在行知管理员页面配置组织连接器:MCP 地址 `https://admin.zhenyangtang.com.cn/mcp`,授权/撤销/身份接口为同域的 `/mcp/auth/grant|revoke|whoami`,工具名前缀关闭,只读工具自动放行。
新增后台接口或页面后:运行 `php app/mcp/cli/catalog.php --write` 重新盘点,并在 `review/` 给新资源写结论;`php server/tests/AiMcpUnitTest.php` 会报告尚未审核的只读接口数量。
## 6. 验证
```sh
php server/tests/AiMcpUnitTest.php
# 以下两项需要一次性测试库(库名以 _test 结尾)与指向它的运行实例
AI_MCP_TEST_MYSQL=1 php server/tests/AiMcpReadOnlyTest.php
AI_MCP_TEST_MYSQL=1 AI_MCP_TEST_BASE_URL=http://127.0.0.1:8099 php server/tests/AiMcpHttpContractTest.php
php app/mcp/cli/probe.php --admin=<ID> [--only=tcm.] # 默认不执行不开放的、会调外部接口的资源
php app/mcp/cli/coverage.php
```
注意:只读事务只能挡住写库,挡不住起进程、写缓存、调外部接口;这类接口在审核中一律不开放或待整改,探测脚本默认也不执行。
2026-09-24 本地结果(PHP 8.2.34 + MariaDB 10.11.19,表结构由仓库 SQL 重建):三个测试全部通过;契约测试覆盖授权门禁与锁定、协议协商、401/403/405、医生/医助/经理/root 各自的数据范围、脱敏与明文权限、扩大范围参数拦截、撤销/改密/停用/闲置/去权限后立即失效、审计记录与后台管理接口。与行知的端到端联调(真实行知后端与任务引擎 + 本模块)通过:两名行知用户分别绑定医生、医助账号,各自任务只拿到自己数据范围内的挂号记录,手机号已脱敏,审计日志记录了行知任务号和返回的记录 ID。
## 7. 回退
把 `.env` 的 `[AI_MCP] ENABLED` 改为 `false`:`/mcp` 与授权接口立即返回 503,行知侧查询自动失败并提示。新表和菜单保留即可,不需要回滚数据库。需要彻底停用时,在“AI 授权管理”撤销全部授权。
## 8. 已知限制与后续
- 后台部分接口本身缺少逐条权限校验或存在扩大范围的参数(见行知方案文档“zyt 安全前置整改”一节)。MCP 已按“默认拒绝 + 参数白名单 + 行守卫”规避,但后台网页仍受影响,建议另行修复。
- 未登记为菜单权限点的接口在 MCP 中一律不开放;如需开放,先按 `2026_08_12_call_transcription_permissions.sql` 的做法登记权限点。
- 阶段三可选:接入 IAM(Keycloak)授权码 + PKCE 绑定,密码不再经过行知。