# G5 Host Bridge：方案 B 批量补全基础层

- 日期：2026-09-02
- 状态：**G5-2.1～G5-2.5、Agent-owned nested execution、真实 OAuth/QCC 主路径、token 自然到期刷新与 401/429/配额故障注入已验收**
- 发布状态：✅ 已随 `v0.4.0` 发布到 npm 与 GitHub Release
- 决策依据：`docs/adr/0002-programmatic-mcp-tool-execution.md`

## 本阶段交付

`lib/qcc-command.js` 与 `lib/qcc.js` 基于 DSH 公共工具运行时实现 Host Bridge。2026-09-04
在真实 rc.2 Code Mode 验证发现：动态 MCP 工具必须作为 Agent-owned 父工具调用的 nested execution
运行，普通 Web handler 不能直接成为其父执行。因此正式 UI 路径采用「Host 暂存 + 类型化会话意图 +
高层工具」：

1. 仅允许 `qcc_oauth_*`、规范 `mcp__qcc-*__*` 与 OAuth 0.1.7 已验证 legacy serverName，拒绝任意工具代理。
2. 每次调用重新解析工具，兼容 OAuth 刷新造成的注销/重注册窗口；只对 `UNKNOWN_TOOL` 做一次安全重试，其他失败不自动重试，避免重复计费。
3. 统一 call ID、AbortSignal、超时和 ToolRuntime `isError`，错误响应不携带工具原始业务数据。
4. 解析 MCP `structuredContent` 或 QCC 文本 JSON，复用一期字段契约。
5. 批量输入按企业名去重调用；唯一精确主体才继续工商/风险补全，多候选进入 `reviewQueue`，未匹配保留为 `unresolved`。
6. 单企业失败隔离，不中断其他企业；原始/补全明细只在 Host/Web 同源边界内流转。
7. Web 暂存任务不产生 QCC 调用；发送给模型的提示只含 commandId/taskId/kind，不含企业名单和字段值。
8. `data_cleaning_qcc_run` 由 Agent 调用一次，并把父执行的 `token / agent / rootCallId` 传给每个
   动态 QCC nested execution；重复调用同一 commandId 复用同一 Promise/结果，不重复计费。

## 数据流

```text
同源 Web 请求（显式确认使用当前用户自己的 QCC 额度）
  → Host 暂存 rows，返回不含 rows 的 commandId 类型化提示（零 QCC 调用）
  → 原生 conversation.send(prompt)
  → Agent 调用一次 data_cleaning_qcc_run(commandId)
    → QccHostBridge.enrichRows
      → 企业名去重 + 受控并发（1–4）
      → ctx.tools.get（每次重新解析）
      → ctx.tools.execute(..., parent=exec.token, agent=exec.agent)
        ├─ 唯一精确 → 锁定信用代码 → 工商详情 → 可选风险扫描
        ├─ 多候选   → reviewQueue，停止该主体下游调用
        └─ 未匹配   → unresolved
  → 高层工具只向对话返回摘要
  → 同源 UI 轮询 commandId，取得 Host run 明细并进入候选/补全/导出
```

## Web 契约

### 被动能力探测

`GET /data-cleaning/api/g5/capabilities`

只检查工具是否注册，不调用 OAuth 或任何计费 QCC 工具。返回 Bridge marker、连接态推断和批量限制。
同时声明 `idempotencyRequired / candidateResume / manualRetry`，run 状态仅为 `host-memory`。
还会返回 `agentCommandTool / agentCommandToolRegistered / agentOwnedExecutionRequired`；若当前 Host
不支持工具注册，命令准备接口以 `503 QCC_AGENT_COMMAND_UNAVAILABLE` 关闭，不会产生 QCC 调用。

### Agent-owned 命令（正式工作台路径）

- `POST /data-cleaning/api/g5/commands`：0.8.14 起 enrich 暂存不要求额度勾选，Host 自动检查所选工具、行数和调用上界；
  resolve/retry 暂存仍要求 `confirmPaidCalls:true`。返回 commandId、Host estimate 与不含企业名单的可编辑提示，本步骤 `paidCalls:false`。
  生成说明不代表已执行，用户发送可见说明后才由 Agent-owned 工具执行。执行阶段重新检查工具和上限，草稿不伪造“已确认额度”状态。
- `data_cleaning_qcc_run({commandId})`：只能在 Agent tool execution 内运行，缺少父执行 token/Session
  时返回 `QCC_AGENT_EXECUTION_REQUIRED`。
- `GET /data-cleaning/api/g5/commands/<commandId>`：同源读取 prepared/running/completed/failed 和完成后的
  Host run，不调用 QCC。

命令默认 30 分钟过期、最多 50 个；同一 commandId 在运行中和完成后都不会重复派发。

### 兼容批量端点（非 Code Mode UI 主路径）

`POST /data-cleaning/api/g5/enrich`

```json
{
  "idempotencyKey": "client-generated-unique-key",
  "confirmPaidCalls": true,
  "rows": [{ "name": "示例企业" }],
  "headers": ["name"],
  "nameField": "name",
  "includeRisk": false,
  "concurrency": 2
}
```

约束：

- 该端点保留给 Mock/Contract、Runner 与非 Code Mode 兼容场景；rc.2 Code Mode 的工作台不得用它
  直接派发动态 QCC 工具，应使用上面的 Agent-owned 命令路径。

- `confirmPaidCalls` 必须严格为 `true`，否则在任何工具调用前返回 `QCC_CONFIRM_REQUIRED`。
- `idempotencyKey` 必填；相同键与相同请求复用首个结果，不重复调用工具；同键不同请求返回冲突。
- 单批最多 100 行，并发范围 1–4；重复企业只检索一次。
- 响应包含 `summary / reviewQueue / errors / rows / csv`；完整明细不得转发给模型。
- 多候选不会自动取第一项；响应提供 `runId`，状态为 `awaiting-review`。

### 候选确认、人工重试与 run 查询

- `POST /data-cleaning/api/g5/resolve`：传入 `runId / companyName / selectedCreditNo / idempotencyKey / confirmPaidCalls:true`。信用代码必须存在于该公司的待复核候选列表；成功后直接调用工商详情和可选风险，不重复实体检索。
- `POST /data-cleaning/api/g5/retry`：传入 `runId / companyNames / idempotencyKey / confirmPaidCalls:true`。只允许重试错误队列中 `retryable:true` 的企业，且不会自动触发。
- `GET /data-cleaning/api/g5/run/<runId>`：读取当前同源 run 状态，不调用 QCC 工具。

run 状态为 `awaiting-review / needs-retry / completed-with-errors / completed`。明细、候选和幂等结果仅保存在 Host 内存，默认 TTL 30 分钟、最多 50 个 run；Host 重启后失效。

## 错误分类与安全审计

Host Bridge 把上游错误归一为稳定错误码：

- 401/Token 失效 → `QCC_AUTH_REQUIRED`
- 403/资源域未授权 → `QCC_PERMISSION_DENIED`
- 429 → `QCC_RATE_LIMITED`
- 配额不足 → `QCC_QUOTA_EXHAUSTED`
- 超时 → `QCC_TIMEOUT`
- 工具刷新消失 → `QCC_TOOL_UNAVAILABLE`
- 5xx/连接故障 → `QCC_UPSTREAM_UNAVAILABLE`
- 参数契约拒绝 → `QCC_UPSTREAM_REJECTED`

错误响应不复述上游原始 message。每次物理工具调用只记录 toolName、callId、attempt、结果、稳定错误码和耗时；不记录参数、企业名或工具响应。

## E2E Runner

`scripts/g5-e2e.mjs` 默认关闭、仅允许回环 DSH Host；真实 enrich 还要求独立的付费确认变量。报告只包含脱敏摘要，详见 `docs/G5-E2E-RUNBOOK.md`。

## 已通过的 Mock/Contract 门

- Bridge 单元测试覆盖允许列表、每次解析、唯一 call ID、动态工具恢复、取消、超时、细分错误归一化、安全审计、响应解码、消歧、字段映射、锁定候选、去重批量、部分失败、未连接和批量上限。
- Run/Web 测试覆盖并发幂等、同键冲突、候选合法性、续跑、人工重试、状态过期、capabilities、确认门和 CSV。
- Runner/脱敏测试覆盖默认关闭、回环限制、付费确认、摘要报告及凭据/企业标识清洗。
- Web 路由级故障注入覆盖 401、429、配额耗尽：确认不自动重试、人工重试门与安全审计语义。
- 测试夹具只使用虚构企业与虚构信用代码，不含真实 token 或业务数据。

## DSH rc.2 隔离 Host 冒烟

2026-09-01 将当前工作树打包后安装到临时 `DSH_HOME`，在隔离端口 `43140` 启动 DSH `0.1.1-rc.2`；为规避本机文件监听器的 `EMFILE`，仅在该测试进程设置 `CHOKIDAR_USEPOLLING=1`。结果：

- Host 成功执行插件 `apply()`；MVP seam 返回 `qccBridgeMounted:true`、`enrichSkillRegistered:true`，原有三工具与两个 Skill 均保持注册。
- `GET /data-cleaning/api/g5/capabilities` 返回 `200`、marker `g5-host-bridge`、`ready:false`、`state:oauth-plugin-missing`，未调用 OAuth 或 QCC 工具。
- 未传 `confirmPaidCalls:true` 的补全请求返回 `409 QCC_CONFIRM_REQUIRED`，证明计费确认门在工具调用前生效。
- 显式确认后，因隔离 Host 未安装 QCC 动态工具而返回 `503 QCC_NOT_CONNECTED`，没有真实 OAuth、token 刷新或 QCC 请求。
- 测试 Host 已停止；生产 GUI 端口 `43120` 未触碰。

G5-2 在同日将更新后的 27 文件 tarball 安装到隔离 Profile，并在端口 `43141` 追加验证：

- capabilities 返回 `idempotencyRequired:true / candidateResume:true / manualRetry:true / runPersistence:host-memory`。
- 已确认计费但缺少幂等键时返回 `400 QCC_IDEMPOTENCY_REQUIRED`，零 QCC 工具调用。
- 带合法幂等键时，由于隔离 Host 未安装 OAuth/QCC 工具，安全返回 `503 QCC_NOT_CONNECTED`。
- `G5_E2E_MODE=preflight` Runner 成功生成权限 `0600` 的脱敏报告，只含 capabilities 摘要。
- 测试 Host 已停止；没有安装 QCC OAuth、没有真实 QCC 调用，生产端口 `43120` 未触碰。

## 真实 E2E 验收门

2026-09-01 已通过：OAuth PKCE 首连、授权跨重启恢复、真实 company/history 工具调用、
20 家公开企业/400 次调用、每企业当前最低 15 维与历史 4 维、证据脱敏边界。

发布前验收状态：

1. 未授权 host：返回 `QCC_NOT_CONNECTED` 并正确引导 `qcc_oauth_connect`。
2. 已授权 host：用脱敏名单跑真实 `get_company_by_query` 与 `get_company_registration_info`。✅
3. 多候选真实响应：不发生下游工商/风险调用。
4. token 临期刷新：刷新期间工具短暂消失后恢复，且无重复计费调用。✅ 自然到期真实 refresh + 续期后 1 行调用
5. 401/限流/配额不足/超时：错误分类、部分失败和人工重试符合契约。✅ 401/429/配额故障注入 + 其余契约测试
6. includeRisk：风险因子计数逐字引用，不自行加总或推断。
7. 审计：日志、响应错误、测试证据均不泄露 token 或未脱敏原始名单。✅ 主路径与故障注入均已验证

2026-09-02 的刷新验收使用隔离 DSH `0.1.1-rc.2`、端口 `43159`：启动前 access token 已自然过期；
启动后持久 grant 更新时间与到期时间均前移，16+4 工具恢复为 ready，随后 1 行真实 enrich 1/1 成功。
测试 Host 已停止，生产 `43120` 未触碰。错误注入只走本地 Mock ToolRuntime，没有新增真实计费故障调用。
