170 lines
28 KiB
Markdown
170 lines
28 KiB
Markdown
# AI 助手(MCP)只读数据查询:实现与部署
|
||
|
||
日期:2026-09-24。状态:只读查询、业绩工具与 AI 后台浏览器已上线;物流查询工具(第 3 节末、第 5 节第 8 步)已在本地一次性测试库完成实现与测试,待同步上线(不涉及数据库)。
|
||
|
||
对接方:行知 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 数据目录 |
|
||
| `server/app/mcp/controller/ConsoleController.php`、`service/ConsoleService.php`、`server/database/migrations/2026_09_24_ai_mcp_console.sql` | AI 后台浏览器会话(`/mcp/console/open|close`)与权限点 `ai.mcp/console`(同日增补,见第 3 节末) |
|
||
|
||
## 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_my_patients`、`zyt_roster`,和业绩工具 `zyt_perf_assistants`、`zyt_perf_doctors`、`zyt_stats_performance`、`zyt_perf_trend`(见下),物流工具 `zyt_logistics_order`、`zyt_logistics_patient`(见第 3 节末)。每个工具带中文显示名(MCP `title` / `annotations.title`,如“医生业绩排行”“订单物流查询”),行知的执行步骤显示它而不是工具标识。
|
||
|
||
### 业绩工具(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:`type` bar/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`。
|
||
|
||
### 物流查询(2026-09-24 增补,`service/LogisticsTools.php`)
|
||
|
||
原来的“物流轨迹”接口(`tcm.prescriptionOrder/logisticsTrace`)一直待整改:库里没有轨迹时会调快递100,逐单校验对任一订单权限直接放行。现在改为两个专用工具,AI 可以直接回答“某个订单/某位患者的药到哪了”:
|
||
|
||
| 工具 | 查询方式 | 返回 |
|
||
|---|---|---|
|
||
| `zyt_logistics_order` 订单物流查询 | 业务订单号(PO…)或快递单号 | 快递公司、运单号、订单履约状态、物流状态、是否签收与签收时间、轨迹(最新在前,默认 30 条)、数据更新时间与来源、快递公司官网查询页 |
|
||
| `zyt_logistics_patient` 患者物流查询 | 患者姓名/手机号或患者ID,可加创建日期范围 | 该患者最近订单(默认 10 单)各自的快递、状态和最新动态;只匹配到一位患者时附最近一单已发货订单的完整轨迹;同名患者分开列出并提示确认 |
|
||
|
||
- 找订单:只通过「处方业务订单」列表(以调用账号身份执行后台原列表),能查到的订单与后台列表完全一致(医助默认只看本人创建的订单,再叠加数据范围);不使用逐单接口的 canAccessOrder。患者姓名、手机号取订单所属诊单。
|
||
- 看轨迹:还需要后台「物流轨迹」权限(`tcm.prescriptionOrder/logisticsTrace`),没有时只给快递公司、单号和订单状态。
|
||
- 数据来源:优先读系统已同步的轨迹(`zyt_express_tracking` / `zyt_express_trace`,由 `express:auto-update`、`gancao:sync-logistics` 等同步任务和甘草、洛阳药房回调写入),在只读事务里查询,并写明这张运单最后一次同步的时间和来源。同步任务需要在服务器 crontab 中运行(代码里只有建议周期);订单已完成/取消或运单已签收后不再更新。
|
||
- 实时查询:系统里还没有这张运单的轨迹时,默认按需实时查询(参数 `live=false` 可关闭):与后台打开订单详情时自动查询的是同一路径(`PrescriptionOrderLogic::logisticsTrace` → 快递100 / 京东官方接口,不写库),每个账号每小时最多 `LOGISTICS_LIVE_PER_HOUR` 次(默认 30),`.env [AI_MCP] LOGISTICS_LIVE = false` 可整体关闭。患者查询只对最近一单做实时查询,列表里的各单只看已同步的最新动态。
|
||
- 脱敏:收件人电话、患者电话按权限脱敏;只给省市区,不给详细地址;轨迹文字里的手机号(如快递员电话)同样脱敏。审计记录返回的订单ID和行知任务号。
|
||
|
||
### AI 后台浏览器(2026-09-24 增补,`controller/ConsoleController.php`、`service/ConsoleService.php`)
|
||
|
||
MCP 工具只能查数据目录里开放的资源。为了让 AI 也能看后台网页上才有的内容,并在成员逐次批准下代为操作,行知在自己的服务器上运行一个内置浏览器(无界面 Edge),用成员已绑定的 AI 授权免密码登录本后台。本模块只负责换取和收回这个登录:
|
||
|
||
- `POST /mcp/console/open`(`Authorization: Bearer zyt_ai_…`,可带 `X-Xingzhi-Task-Id`):为该账号签发“AI 浏览器”专用终端(`terminal = 8`)的后台登录,返回 `token`、`expire_time`、`local_storage`(后台前端读取的 `like_admin_token`)和 `start_path`。令牌只写进行知服务器上的浏览器,不给模型、不进任务记录、不回传网页。
|
||
- 沿用后台原有登录体系(`AdminTokenService::setToken` 与 `zyt_admin_session`),不改任何中间件;独立终端,不会挤掉该账号在电脑、手机、企微客服端的登录(关闭“多处登录”时也一样);同一账号的 AI 浏览器共用一个会话,已有有效会话时直接沿用。
|
||
- 有效期默认 2 小时(`CONSOLE_TTL_MINUTES`);签发时清掉登录缓存,由浏览器第一次请求按它自己的出口 IP 重建,后台的登录 IP 校验照常生效。
|
||
- 前提:AI 授权有效(与 MCP 相同的实时校验),且账号有新权限点 `ai.mcp/console`“允许 AI 使用后台浏览器”(默认不授予任何角色,root 自动拥有)。没有该权限返回 `code = 0`、`data.reason = no_console_permission`,不建会话;授权无效返回 401。
|
||
- `POST /mcp/console/close`:作废该会话(到期时间改为过去并清登录缓存;文件缓存下连后台应用自己的缓存目录 `runtime/adminapi/cache` 一并清理)。行知关闭浏览器、空闲 15 分钟、打开满 2 小时、成员解除或重新绑定时调用;授权已撤销、过期或失去权限时也照样注销(只要令牌确实由本系统签发过)。
|
||
- 自动收回:AI 授权被撤销或失效(后台撤销、改密、停用、删除、过期)时,同时作废该账号的 AI 浏览器会话;账号失去 `ai.mcp/access` 或需要先绑定企微时,授权保留,但会话立即作废。
|
||
- 审计:`zyt_ai_access_log` 记录 `console.open`(成功或拒绝及原因、行知任务号)和 `console.close`。浏览器里的操作走后台自己的接口和操作日志,登录终端为 8,可与员工本人的操作区分。
|
||
- 这是完整的后台登录(按账号自己的菜单权限和数据范围),**不经过** MCP 的只读事务和脱敏。行知侧的保护:只允许打开本后台域名;AI 触发的每个写请求(POST/PUT/PATCH/DELETE 等)先在任务里请成员批准(只能“允许本次”);成员在实时画面里亲自操作的直接提交;无人触发的后台提交、跳到其他网站、WebSocket、下载一律拦截;组织可把后台设为只读(任何写请求都拦截);给模型的页面文字隐藏手机号和身份证号;截图只给成员看。
|
||
|
||
## 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
|
||
CONSOLE_ENABLED = true ; AI 后台浏览器总开关(另需角色勾选 ai.mcp/console);紧急停用设为 false
|
||
CONSOLE_TTL_MINUTES = 120 ; AI 浏览器后台会话有效期(10–480 分钟)
|
||
LOGISTICS_LIVE = true ; 物流查询:系统里没有轨迹时是否实时查询快递公司(快递100 按次计费)
|
||
LOGISTICS_LIVE_PER_HOUR = 30 ; 每个账号每小时最多实时查询几次
|
||
```
|
||
|
||
限流和锁定使用系统缓存;线上建议 `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`,工具名前缀关闭,只读工具自动放行。
|
||
7. **AI 后台浏览器(同日增补)**:同步 `server/app/mcp/`(新增 `controller/ConsoleController.php`、`service/ConsoleService.php`,改动 `service/{GrantService,McpConfig}.php`);执行 `server/database/migrations/2026_09_24_ai_mcp_console.sql`(只新增一个按钮权限点,可重复执行);在“权限管理 > 角色”给需要的角色勾选“允许 AI 使用后台浏览器”(`ai.mcp/console`)。行知侧已配置好组织连接器的“管理后台”:地址 `https://admin.zhenyangtang.com.cn/admin/`,免登录接口 `/mcp/console/open`,退出接口 `/mcp/console/close`,修改逐次审批。
|
||
8. **物流查询(同日增补)**:同步 `server/app/mcp/`(新增 `service/LogisticsTools.php`,改动 `service/{Tools,McpConfig,Protocol}.php`、`catalog/review/tcm.php`)与 `server/tests/AiMcpLogisticsTest.php`,不涉及数据库。需要看轨迹的角色保持勾选后台「物流轨迹」权限;确认服务器 crontab 在运行快递轨迹同步任务(`php think express:auto-update` 约每 10 分钟、`php think gancao:sync-logistics` 约每 30 分钟),否则只能靠实时查询。
|
||
|
||
新增后台接口或页面后:运行 `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
|
||
AI_MCP_TEST_MYSQL=1 AI_MCP_TEST_BASE_URL=http://127.0.0.1:8099 php server/tests/AiMcpPerfTest.php # 业绩工具:口径、排名、数据范围、走势、图表、权限回退
|
||
AI_MCP_TEST_MYSQL=1 AI_MCP_TEST_BASE_URL=http://127.0.0.1:8099 php server/tests/AiMcpConsoleTest.php # AI 后台浏览器(需先执行 2026_09_24_ai_mcp_console.sql)
|
||
AI_MCP_TEST_MYSQL=1 AI_MCP_TEST_BASE_URL=http://127.0.0.1:8099 php server/tests/AiMcpLogisticsTest.php # 物流查询(运行实例须设 PHP_LOGISTICS_KUAIDI100_DISABLE=true PHP_JD_LOGISTICS_DISABLE=true,避免外呼)
|
||
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`,四个测试全部通过。
|
||
|
||
AI 后台浏览器(同日增补):`AiMcpConsoleTest` 通过——无效授权 401;没有 `ai.mcp/console` 被拒且不建会话;签发 terminal=8 会话、有效期 2 小时,关闭“多处登录”时电脑端登录不受影响;换来的令牌能直接访问后台接口;再次打开沿用同一会话;关闭后令牌立即失效、重复关闭无害、重新打开换新令牌;撤销授权同时作废会话;授权失效后仍能注销,未知令牌不能;失去 `ai.mcp/access` 时会话立即作废而授权保留;审计含行知任务号和拒绝记录。原有四个测试无回归。
|
||
|
||
物流查询(同日增补):`AiMcpLogisticsTest` 通过——只查得到列表范围内的订单(别人的订单按订单号、快递单号都查不到);已同步轨迹的状态、签收时间、最新在前、条数限制、来源与更新时间;收件人电话、患者电话和轨迹文字里的手机号脱敏,不返回详细地址;没有「物流轨迹」权限时不给轨迹;未填单号;同名患者分开列出,按患者ID给出最近一单完整轨迹,日期范围筛选;库里没有轨迹时走实时查询且不写库(测试实例关闭了快递100 / 京东接口,不外呼),第 31 次实时查询被拒;参数校验;工具中文名;审计。其余五个测试无回归;行知端确认 MCP 客户端拿到工具中文名。行知联调:行知服务器上的内置浏览器(Playwright 驱动 Edge)用本接口免密码打开本地实例的后台页面并读取内容,AI 触发的写请求停在审批(本地联调时后台前端的生产构建写死了线上接口地址,用请求改写指向本地实例)。
|
||
|
||
## 7. 回退
|
||
|
||
把 `.env` 的 `[AI_MCP] ENABLED` 改为 `false`:`/mcp` 与授权接口立即返回 503,行知侧查询自动失败并提示。新表和菜单保留即可,不需要回滚数据库。需要彻底停用时,在“AI 授权管理”撤销全部授权。
|
||
|
||
只停用 AI 后台浏览器:`.env` 设 `[AI_MCP] CONSOLE_ENABLED = false`(不能再打开新会话),或在角色里取消“允许 AI 使用后台浏览器”。已打开的会话在行知空闲 15 分钟或到期(最长 2 小时)时收回;需要立即收回时,在“AI 授权管理”撤销相关成员的授权。
|
||
|
||
## 8. 已知限制与后续
|
||
|
||
- 后台部分接口本身缺少逐条权限校验或存在扩大范围的参数(见行知方案文档“zyt 安全前置整改”一节)。MCP 已按“默认拒绝 + 参数白名单 + 行守卫”规避,但后台网页仍受影响,建议另行修复。
|
||
- 未登记为菜单权限点的接口在 MCP 中一律不开放;如需开放,先按 `2026_08_12_call_transcription_permissions.sql` 的做法登记权限点。
|
||
- 阶段三可选:接入 IAM(Keycloak)授权码 + PKCE 绑定,密码不再经过行知。
|
||
- AI 后台浏览器按请求方法区分读写:行知只拦截非 GET/HEAD/OPTIONS 请求。后台若有用 GET 修改数据的接口,无法被审批拦住,建议这类接口改为 POST(导出下载在行知浏览器里已禁用)。
|
||
- 取消角色的 `ai.mcp/console` 只阻止新开会话,不会立刻结束已打开的会话(见第 7 节)。
|
||
- 物流轨迹的时效取决于服务器上的同步任务;自营手工填单号的订单可能从未生成轨迹记录,只能靠实时查询(有次数上限)或官网查询页。
|