17 KiB
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 |
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. 工作方式
- 授权:
POST /mcp/auth/grant(账号 + 密码)校验与后台登录相同的密码算法,再检查:未停用、已完成首次改密(is_paw=1)、企微强制绑定规则、拥有ai.mcp/access权限点。通过后签发zyt_ai_开头的随机令牌,库里只存 SHA-256。- 与后台登录会话(
zyt_admin_session)完全独立:不占终端、不受 IP 绑定影响,不会挤掉浏览器、医生工作站或企微客服端。 - 失败锁定按账号计(5 次 / 30 分钟),另按来源 IP 限速;账号不存在与密码错误给同样提示。
- 同一客户端实例重新绑定时旧令牌自动作废。
- 与后台登录会话(
- 每次调用都实时校验:令牌有效期(默认 90 天)、闲置(默认 30 天)、账号未删除/未停用、密码未修改(签发时记录密码指纹,改密即失效)、仍有
ai.mcp/access。角色权限实时计算,调整角色立即生效。 - 查询执行: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)。
- 权限点必须已在菜单登记、未停用,且该账号拥有(默认拒绝;不沿用后台“未登记接口任何人可访问”的规则,也不沿用
- 数据目录:
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_my_patients、zyt_roster,和业绩工具zyt_perf_assistants、zyt_perf_doctors、zyt_stats_performance、zyt_perf_trend(见下)。
业绩工具(2026-09-24 增补,service/PerfTools.php)
线上发现“查业绩”很慢:模型为了排行逐部门、逐医助、逐天调用明细接口(一次问答 40 多次调用),还因为参数写法、未登记的子权限点和并发时的缓存读写失败而中断。业绩工具一次调用给出排名、合计和口径,并附带统计图:
| 工具 | 数据来源(以调用账号身份执行后台原接口,口径与页面一致) | 权限(自身未登记时依次回退) |
|---|---|---|
zyt_perf_assistants 医助业绩排行 |
业绩看板·医助排行榜 YejiStatsLogic::assistantLeaderboards:诊金、成交订单数(后台“接诊诊单”)、面诊完成数(后台“接诊单数”)、预约数、被指派数、进线数、每进线诊金;排名、合计、部门小计;单人时附处方业务订单列表对账 |
stats.yejiStats/leaderboard → stats.yejiStats/tabLeaderboard → fans/yeji |
zyt_perf_doctors 医生业绩排行 |
业绩看板·医生统计 DoctorDailyStatsLogic::overview:成交金额、成交订单数(后台“接诊诊单”)、客单价、挂号总数/面诊完成/过号/取消、挂号成交率、系统/手动开方 |
stats.doctorDailyStats/overview → stats.yejiStats/tabDoctor → fans/yeji |
zyt_stats_performance 部门业绩看板 |
业绩看板·甄养堂诊金 YejiStatsLogic::overview:各部门合计业绩、成交订单数、面诊完成数、预约数、进线数、被指派数、投放成本、ROI |
stats.yejiStats/overview → stats.yejiStats/tabZyyt → fans/yeji |
zyt_perf_trend 业绩走势 |
一条按日分组的只读聚合 SQL(在同样的只读事务里),按日/周/月归并,可对比最多 4 人。医助口径同排行榜诊金(订单创建人,剔除取消/拒收/退款);医生口径同医生统计成交金额(开方医生,另剔除发生过退款的订单) | 按医助同医助排行,按医生同医生统计 |
- 参数:
period(today / yesterday / this_week / last_week / this_month / last_month / last_7_days / last_30_days,由服务器按当天计算)或start_date/end_date;dept可写部门名称(只在账号可见的部门里匹配);sort_by、top、name/*_id;走势另有by、names/ids、granularity、metric。 - 数据范围:与后台一致(经理看本部门及下级、医助只看自己、医生统计只含可见医生);走势指定的人必须在账号可见范围内。
- 统计图:结果文字里带一个
```chart代码块(JSON:typebar/column/line、title、subtitle、unit、labels、series),行知把它画成统计图(可切换表格);structuredContent.chart同时提供。只有一行时不画图。 - 权限回退(审核文件
perm_fallback):子接口自身已登记时与后台完全一致;未登记时后台对该接口不校验、只靠 Tab/页面权限控制可见,这里改用第一个已登记的 Tab/页面权限,不会比后台页面更宽。同样的回退也加在了业绩看板的部门/渠道下拉和各明细子接口上。 - 业绩看板、医生统计、提成结算、综合转化的单条 SQL 超时放宽到 30 秒(审核文件
timeout;后台控制器自己放宽到 120 秒)。 - 指标命名(同日第二次修正):线上有人问“洛阳高坤艳本月才 21 单,为什么显示 96 单”。96 是后台排行榜的“接诊单数”——面诊完成的挂号人次,不是订单;订单数是“接诊诊单”(她的处方业务订单列表共 21 条,顶部业绩 ¥25,212 对应的是扣掉拒收/退款后的订单)。工具原先照搬后台列名还标成“单”,模型就把 96 说成了 96 单。现在输出一律用说清楚“数的是什么”的名称并附后台列名对账:面诊完成数(人次,后台“接诊单数”)、成交订单数(单,后台“接诊诊单”)、预约数(后台“预约诊单”)、每进线诊金(后台“接诊率”)、挂号成交率(后台“挂号率”)等;结果带
definitions(每个指标的口径),按非订单指标排名时摘要和图下说明都会提示“不是订单数”。 - 单人对账:
zyt_perf_assistants只匹配到一位医助时,同时按“医助(订单创建人)+ 同一时间段”查处方业务订单列表,返回order_list(列表条数含全部状态、金额、其中计入业绩的金额、未计入业绩的条数),摘要写明“列表共 N 条……业绩只算未取消、未拒收、未退款的订单,所以成交订单是 M 单”。医生同理(按开方医生),但只对能看全量业务订单的账号给出,因为非全量角色在列表里按医生筛选只能看到自己创建的订单。 - 统计明细翻页:进线明细、被指派明细等“统计类”资源自己分页,以前
zyt_query的外层page/page_size对它们不生效,模型翻页每次都拿到第一页,看起来像“同一条记录反复出现”;现在外层参数会传给这些接口,结果带result.paging(总数、页码、是否还有下一页)。另外同一页里 ID 重复的行会去重并在结果里说明。 - 同时修复:
params里写的page/page_size自动当作分页;限流与每日行数计数在缓存读写出错时放行并记日志(文件缓存下并发调用可能读到写了一半的文件,之前会让整次查询变成“内部错误”);内部错误提示带上异常类型,便于对照服务器日志。 - 授权接口:
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)
[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. 部署顺序
- 备份数据库;执行
server/database/migrations/2026_09_24_ai_mcp.sql(默认前缀zyt_,可重复执行,只新增表和菜单)。 - 同步
server/app/mcp/与后台前端(admin重新构建,新增三个页面)。代码同步后ENABLED仍为false,对现有功能无影响。 - 在“权限管理 > 角色”中给试点角色勾选“允许 AI 助手查询”(
ai.mcp/access),管理员角色勾选“AI 授权管理 / AI 访问日志 / AI 数据目录”。默认不授予任何角色。 - 在预发/测试库上以 root 账号运行
php app/mcp/cli/probe.php --admin=<root 的 ID>与php app/mcp/cli/coverage.php,确认没有writes(只读保护拦截)结果、没有未覆盖的表;有的话在对应review/*.php把该资源改为待整改或改用只读 Logic,或用coverage.php --write-tables补数据表资源。 .env设置[AI_MCP] ENABLED = true与ALLOWED_IPS;nginx 对/mcp与/mcp/auth/grant加limit_req,并确认不缓冲响应。- 在行知管理员页面配置组织连接器: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. 验证
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
AI_MCP_TEST_MYSQL=1 AI_MCP_TEST_BASE_URL=http://127.0.0.1:8099 php server/tests/AiMcpPerfTest.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。
业绩工具(同日增补):AiMcpPerfTest 通过(医助诊金计入部分退款单、医生成交金额不计;取消单都不计;经理看两名下属医助/医生、医助只看自己;部门名称解析;按日/周/月走势;图表输出;排行榜权限点停用时回退到 Tab 权限);原有三个测试与探测脚本无回归;行知端到端(真实行知后端 + MCP 客户端 + 本模块)确认模型拿到 4 个业绩工具、图表代码块完整到达并存入回答。上线只需同步 server/app/mcp/(新增 service/PerfTools.php,改动 service/{Tools,Catalog,Dispatcher,RateLimiter,Protocol}.php、catalog/review/{stats.php,README.md}),不涉及数据库。第二次修正(指标命名、单人对账、统计明细翻页)只改 service/{PerfTools,Tools,Protocol}.php 与 tests/AiMcpPerfTest.php,四个测试全部通过。
7. 回退
把 .env 的 [AI_MCP] ENABLED 改为 false:/mcp 与授权接口立即返回 503,行知侧查询自动失败并提示。新表和菜单保留即可,不需要回滚数据库。需要彻底停用时,在“AI 授权管理”撤销全部授权。
8. 已知限制与后续
- 后台部分接口本身缺少逐条权限校验或存在扩大范围的参数(见行知方案文档“zyt 安全前置整改”一节)。MCP 已按“默认拒绝 + 参数白名单 + 行守卫”规避,但后台网页仍受影响,建议另行修复。
- 未登记为菜单权限点的接口在 MCP 中一律不开放;如需开放,先按
2026_08_12_call_transcription_permissions.sql的做法登记权限点。 - 阶段三可选:接入 IAM(Keycloak)授权码 + PKCE 绑定,密码不再经过行知。