# 技术规格 ## 技术栈与边界 - Gateway:Python 3,核心运行模块只使用标准库;stdio 使用逐行 JSON-RPC,公网使用 `ThreadingHTTPServer`。 - 后端:ThinkPHP 6.0 + PHP 7.1+,按 Controller / Logic / Model / Validate 分层实现业务工具。 - 存储:Redis 保存本地 token 或公网 Gateway session;MySQL、MongoDB 和文件存储只由所属业务系统访问。 - Gateway 不连接业务数据库;跨仓调用只通过明确的 HTTP 路由和服务端会话进行。 ## 组件 | 组件 | 职责 | |---|---| | `app.py` | 本地 `GatewayApp`、stdio/CLI 入口、12 个工具注册与参数转发 | | `public_gateway.py` | 公网 `PublicGatewayApp`、设备会话解析后的 scoped 调用、同组 12 个工具注册 | | `public_server.py` | `/mcp` HTTP JSON-RPC、可信追踪号、审计上下文、限流和 `/health` | | `mcp_protocol.py` | MCP 握手、`tools/list`、`tools/call`、旧/新结果分流和协议错误 | | `services/api_client.py` | 本地 token 上下文下的 ThinkPHP 工具请求与动态列表请求 | | `services/scoped_api_client.py` | 公网请求级 `mcp_token` 转发,避免进程级身份串用 | | `services/gateway_session_store.py` | `GWS_xxx` 到服务端员工会话的 Redis 映射 | | `services/output_presenter.py` | 11 个安全工具的白名单 DTO、错误映射和文本渲染 | | `tools/*.py` | 工具名称、说明、输入 Schema、路由和调用封装 | ## 本地与公网会话 本地 stdio 流程: 1. `GatewayApp` 从 Redis 或开发用文件 store 读取本机 session。 2. token 临近过期时通过 base 刷新。 3. `tools/list` 和 `tools/call` 携带该 token 请求 fmsoperate。 公网流程: 1. Workbuddy 通过 Header、Bearer 或 Cookie 提交 `GWS_xxx`。 2. `RequestContextParser` 提取设备凭据,`GatewaySessionStore` 从 Redis 获取服务端 `mcp_token`。 3. `PublicGatewayApp` 为单次请求创建 scoped API 调用上下文,不把 token 放进进程全局状态。 4. fmsoperate 再校验 token、员工状态、公司、工具权限和业务数据范围。 会话缺失、过期或后端动态列表不可用时关闭访问。公网不得使用文件 token store、固定 `FMS_SESSION_KEY` 或共享进程级员工 token。 ## 动态工具可见性 - `GatewayApp` 与 `PublicGatewayApp` 的候选注册顺序和名称必须完全一致,当前各为 12 个。 - `tools/list` 调用 `/mcp/tools/listEnabledTools`,只返回本地注册与后端启用代码的交集。 - `tools/call` 重新读取启用集合,避免工具被禁用后继续调用。 - MCP 返回 `tools.listChanged=false`,因此工具或 Schema 变更必须通过 Gateway 重启和客户端重连刷新。 ## Presenter 分类 `query_order` 走旧协议分支,保留 `columns/records/meta` 和既有文本行为。`OutputPresenter.SAFE_TOOLS` 显式处理其余 11 个工具: | 类型 | 工具 | DTO | |---|---|---| | 表格 | `query_order_exact`、`query_track`、`query_customs_declaration_files`、`query_outbound_list` | `headers + rows + pagination` | | 排舱详情 | `query_outbound_detail` | `summary + details`,分页位于 `details.pagination` | | 订单详情 | `query_order_detail` | 中文订单号、详情模块、固定概览分组或明细及分页 | | 筛选项 | `list_order_filter_options`、`list_outbound_filter_options`、`list_pending_outbound_export_filter_options` | `value + label + code` 三列 | | 导出 | `export_pending_outbound_orders`、`export_out_of_province_port_data` | `files[].label + files[].url` | Presenter 对列定义、记录类型、详情汇总、附件结构和导出 URL 做白名单校验。未知工具、未允许列、畸形响应或未知异常返回安全 `isError=true`;真实异常仅记录在服务端日志。后端业务错误映射为固定消息,不透传原始 `msg/data`。 订单详情 Presenter 不依赖原始请求参数,而是校验后端规范 `section`、固定分组及每行完整键集合。“全部”响应校验概览和十个明细分组,并将每个明细分组的分页信息独立展示。状态和时间类型只接受已知枚举;入库重量根据后端固定模式显示为单箱重量或总重量。 ## 追踪、日志与限流 - stdio 自动生成 `rq_*`;公网入口始终生成可信 `rq_http_*`,不采信调用方 `X-Request-Id`。 - 调用方 `X-Request-Id` 只记录 SHA-256 短哈希 `client_request_id_hash`,用于关联客户端反馈。 - 追踪号贯穿动态工具列表、工具调用、MCP `_meta` 和结构化日志。 - 审计日志只记录必要的 request/jsonrpc ID、协议方法、工具代码、员工/公司标识和脱敏客户端标识;禁止记录凭据与原始响应。 - `initialize` 与 `tools/list` 不进入限流器;`tools/call` 使用 `gateway_session_id + tool_name` 分桶,并通过 `FMS_MAX_IN_FLIGHT_PER_TOOL` 控制可释放的同时执行数。`FMS_RATE_LIMIT_MAX_REQUESTS=0` 时关闭累计窗口限制,超限统一返回 JSON-RPC `-32029`。 - 限流配置为 `FMS_RATE_LIMIT_ENABLED`、`FMS_RATE_LIMIT_MAX_REQUESTS`、`FMS_RATE_LIMIT_WINDOW_SECONDS`;生产环境还应在反向代理层限流。 ## 关键业务参数 - `query_outbound_list.outbound_status` 必填,只暴露后台业务阶段;`shipping_method` 无默认值,未传表示全部运输方式。 - `query_order_detail.order_number` 必填;`section` 接受 11 个中文模块或“全部”,页码和每页数量均为 1 至 100。 - `query_outbound_list.warehouse_id` 接受正整数或 `-1`(客户仓),拒绝 0 与其他负数。 - `list_outbound_filter_options.filter_type` 使用七个中文枚举;仓库分支由 fmsoperate 复用 `OrderModel::getSortWarehouse()`,所有分支先校验 `admin/Outbound/index`。 - 排舱筛选响应保留 `value/label/code` 供跨工具传参,面向用户只展示中文标签。 ## 验证基线 ```powershell python -m unittest discover -s tests -p "test_*.py" python -m coverage run -m unittest discover -s tests -p "test_*.py" python -m coverage report -m --fail-under=100 git diff --check ``` 运行 coverage 会更新本地 `.coverage` 文件;只做文档治理时使用已有只读报告,不应制造覆盖率产物变更。