# 多网关第一阶段：Gateway 验收报告

## 结论与范围

2026-09-09：第一阶段 Gateway 插件能力已实现，自动化回归通过。多网关待办 G1–G4 已勾选；App A1–A4、第二/三阶段及真实多机人工验收保持未完成。

本次只修改插件仓库，未修改 App，未部署到用户的服务器/PC，未发布 npm 新版本。当前 package.json 仍为 0.7.2，因此验收应使用本次工作区构建/打包产物，不能直接用 npm 上的同版本发布包推断已包含改动。

## 交付内容

| 能力 | 实现位置 | 结果 |
|---|---|---|
| 三种模式、界面选择、状态持久化及启动优先级 | `lib/index.mjs`、`lib/client.js`、`lib/gateway-state.mjs` | 已实现 |
| 稳定网关 UUID、配置名称、损坏时拒绝启动 | `lib/gateway-state.mjs`、`lib/index.mjs` | 已实现 |
| 配对候选地址合并、去重、URL 校验 | `lib/index.mjs` | 已实现 |
| 配对/paired/hello 身份扩展及旧协议保留 | `lib/index.mjs`、`PROTOCOL.md` | 已实现 |
| App 字段、迁移、路由及安全存储约定 | [App 对接说明](multi-gateway-app-integration.md) | 已交付文档；App 尚未实现 |
| 实施范围及勾选状态 | [多网关待办](multi-gateway-todo.md) | 仅勾选已完成的 Gateway 项 |

## 自动化验证

执行环境：macOS arm64、Node.js v24.19.0；兼容性样例使用 Apple Swift 6.3.3。集成测试启动本地 HTTP/WebSocket 服务，DSH Host 使用测试替身，未连接真实 DSH 服务。测试因沙箱禁止监听本机端口，使用经批准的沙箱外执行完成。

| 验证 | 结果 | 证据/范围 |
|---|---|---|
| `npm test` | 通过 | setup-ip、host-adapter、gateway、auth、lan、multi-gateway 六个测试脚本 |
| 原业务分发回归 | 通过 | `FULL GATEWAY DISPATCH TESTS PASSED (115)` |
| 新增多网关验证 | 通过 | `multi-gateway: 11 checks passed`，见下方明细 |
| LAN 与主监听身份一致 | 通过 | LAN 配对取得 token 后连接本机监听，比较 gatewayId；验证两入口仍按原规则鉴权 |
| 旧 iOS 配对解码兼容 | 通过 | 从兄弟仓库提取实际 `GatewayPairingPayload` 和 `PairingPayloadParser`，使用 Swift 执行带 gatewayId/gatewayName/endpoints 的 v2 样例 |
| JavaScript 语法与补丁空白检查 | 通过 | `node --check` 检查修改后的 JS/MJS；`git diff --check` |
| 真机扫码、真实界面布局及真实多机联调 | 未执行 | 按下方步骤人工验收 |

新增 11 组行为检查：

1. Schema 模式校验、身份及模式持久化、文件权限 0600、损坏文件拒绝加载且保留原文件。
2. 旧启动配置 true 仍表示常驻；不同实例 ID 不同。
3. 主动关闭后连接返回 503，重启及更改启动配置不会覆盖已保存关闭状态；非法请求返回 400。
4. 旧 enabled 接口映射临时模式，临时模式重启重新计时，自动关闭结果持久化。
5. 切换常驻取消旧计时器，重启后模式与身份保持；名称变化不改变身份。
6. v2 Base64URL 配对载荷保留旧字段，新增地址规范化/去重；非法地址和过量列表返回 400。
7. 配对载荷、paired、控制 hello、会话 hello 身份一致，后续 token 连接不重复发送 paired。
8. 有客户端时切换临时模式不会设置等待计时器，断线后保持开启；重新配对轮换 token 并复用本机设备记录。
9. 同一客户端安装 ID 在两个网关分别配对，token 不能跨网关使用；撤销 A 不影响 B。
10. 主动关闭断开现有 WebSocket，close code 为 4004。
11. 模拟持久化失败返回 500，当前运行模式不被改变。

说明：临时超时测试直接调用插件时使用 120ms 等短测试值以缩短执行时间；真实 Cordis Schema 最小值仍为 30000ms。常驻测试证明其不会被等待首次连接的计时器关闭，不代表完成了长时间运行稳定性或压力测试。

复验命令（在插件仓库执行）：

```bash
npm test
node --check lib/index.mjs
node --check lib/client.js
node --check lib/gateway-state.mjs
git diff --check
```

## 人工验收准备

1. 准备两台可访问的机器 A/B；完整目标验收再增加第三台 C（例如一台服务器、两台 PC）。每台运行包含本次改动的插件及兼容 DSH。
2. 使用独立的测试设备注册文件和 `gatewayStateFile`；不要与日常使用实例共用文件。给每台配置不同的 `gatewayName`。
3. 为测试实例设置 `gatewayWaitTimeoutMs: 30000`，保留 `requireAuth: true`；实际超时等待 35 秒。测试结束恢复所需生产值。
4. 打开各自本机 WebUI 的“移动设备”。准备旧版 App 做兼容验收；多网关客户端步骤需等待 App 按对接说明实现。
5. 若使用公网入口，先准备有效 WSS 和手机可达的网络。不要为了测试把 `/mgw/*` 管理接口开放到公网。
6. 记录插件来源、DSH/App 版本、机器与网络、执行时间。下方全部人工项尚未执行，应由验收者填写实测结果。

可在运行该实例的电脑上检查状态。默认示例端口为 3080，实际端口不同时替换：

```bash
curl --fail-with-body http://127.0.0.1:3080/mgw/status
```

### M1. 常驻选择、无连接等待与重启

- [ ] 已人工通过

操作：所有手机断开 → 面板选择“常驻开启” → 读取状态并记录 gatewayId → 等待 35 秒 → 再读取状态 → 停止并重启测试 DSH → 再读取状态。

预期：每次 `gatewayMode=persistent`、`gatewayEnabled=true`、`waitExpiresAt=null`，gatewayId 不变；面板仍显示常驻。重新扫码或使用已有可信凭证可连接。

### M2. 主动关闭持久化

- [ ] 已人工通过

操作：手机保持连接 → 面板选择“关闭” → 观察手机断线 → 读取状态 → 保持启动配置为 persistent，重启 DSH → 再连接。

预期：连接关闭码为 4004；状态 disabled/false/null；重启后仍关闭，重新连接 Upgrade 返回 503。面板改回常驻后恢复。

### M3. 临时模式的三个分支

- [ ] 已人工通过

操作 A：没有手机连接时选临时开启，等待 35 秒，再重启 DSH。

预期 A：先显示等待截止时间，超时自动变关闭，重启仍关闭。

操作 B：选临时开启，在 30 秒内完成手机连接，再断开手机，等待 35 秒。

预期 B：首次连接清除等待计时器，断开后本次运行仍开启。

操作 C：在 B 的状态重启 DSH，确保手机关闭自动重连，等待 35 秒。

预期 C：重启后临时模式重新计时，未连接则自动关闭。再次开启临时后立即切为常驻，等待 35 秒，确认旧计时器没有将常驻关闭。

### M4. 协议身份及旧 App 兼容

- [ ] 已人工通过

操作：在 A 面板生成新二维码并复制配对字符串；检查解码结果为 version 2，包含原四字段及 gatewayId/gatewayName/endpoints。用旧版 App 扫码，查看工作区、会话并发一条测试消息。

预期：旧版 App 能扫码并正常完成原有业务；服务端使用原子协议和 hello protocol 3。用带帧调试能力的测试客户端核对 paired、control hello、conversation hello 与二维码中的 gatewayId 全部相同。不要把配对码或 token 贴入验收记录。

### M5. 同一网关多个地址

- [ ] 已人工通过

操作：给 A 配置已实际可达的 LAN 和 WSS 地址；重新启动后生成配对码。用测试客户端或新版 App 完成首次配对，然后以所得同一 token 分别连接两个地址。

预期：二维码保留所选 publicUrl，候选地址去重；两地址返回同一 gatewayId。LAN 地址不可达时只影响该地址，不生成另一条网关身份。

反例：在本机配对请求传入下方错误地址，应返回 HTTP 400，不生成可用二维码：

```bash
curl -i http://127.0.0.1:3080/mgw/pair \
  -H 'Content-Type: application/json' \
  --data '{"publicUrl":"wss://0.0.0.0/ws/mobile"}'
```

### M6. 不同网关凭证及撤销隔离

- [ ] 已人工通过

操作：测试客户端使用同一个安装级 `X-DSH-Device-ID` 分别配对 A、B → 确认 ID 不同 → 使用 A token 连接 B（反向也测试）→ 用各自 token 正常连接 → 在 A 面板撤销测试设备。

预期：跨网关 token 均返回 401；A 连接以 4003 关闭且旧 token 不再可用；B 连接、token、业务访问不受影响。

### M7. 名称、配置和文件故障

- [ ] 已人工通过

操作 A：记录 A 身份，修改 gatewayName 与候选地址配置，重启。

预期 A：名称和地址更新，gatewayId 不变。

操作 B（仅独立测试实例）：停止 DSH，备份其状态文件，临时写入无效 JSON，再启动；随后停止失败实例、恢复原备份并重启。

预期 B：损坏时明确报 `failed to load gateway state`，没有静默覆盖文件或换 ID；恢复后原身份仍可用。

操作 C：在独立测试实例模拟状态文件不可写，修改运行模式。

预期 C：管理请求返回 500，界面显示失败，当前模式不变。恢复写权限后可以正常保存。若模拟的是自动超时保存失败，当前进程仍关闭并记录错误；应修复存储并重新保存，不能据此保证重启后的模式。

### M8. 多网关 App 验收（待 App 实现）

- [ ] 已人工通过

操作：新版 App 依次添加 A/B/C → 重启 App → 在三台网关之间切换 → 每台创建不同测试会话并检查工作区、模型配置、审批、文件 → 让 A 离线再切到 B/C → 再次扫描 B 的二维码更新配对。

预期：资料及凭证保留；会话及操作始终属于正确网关；A 离线不影响 B/C；重复扫码更新 B 而不是新增重复资料或覆盖 A/C。进一步构造跨网关相同资源 ID 和切换时旧事件迟到，验证缓存和回调隔离。

本项不是 Gateway 单仓库可完成的验收，本报告不勾选。

### M9. 未知地址与身份不匹配（待 App 实现）

- [ ] 已人工通过

操作：在独立测试环境让 A 资料指向 B 或一个不可信新地址，尝试恢复连接；再验证同一 gatewayId 的正常地址更新流程。

预期：App 不自动向未经确认的地址发送凭证；可信地址返回错误身份时停止业务且保留原记录。不能通过同名或自报 gatewayId 绕过确认与 TLS 校验。

## 人工记录模板

| 编号 | 实际结果 | 通过/失败/未执行 | 证据（脱敏） | 执行人/时间 |
|---|---|---|---|---|
| M1–M9（每项独立填写） | 待填写 | 未执行 | 待填写 | 待填写 |

发布前至少完成 M1–M7。宣称 App 第一阶段整体完成前，还需完成 A1–A4 及 M8–M9。后台推送、并行控制连接、mDNS 发现、中心目录和网络中转不在本次交付范围。
