dsh-plugin-mobile-gateway 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PROTOCOL.md CHANGED
@@ -46,7 +46,9 @@ WebUI 中有两个互相独立的开关。它们是本机管理设置,iOS 客
46
46
  | 开启 | 开启(默认) | 必须使用一次性配对码或长期设备 token,否则返回 `401 Unauthorized` |
47
47
  | 开启 | 关闭(仅 Debug) | 仅 DSH 本机监听允许无凭证连接,`hello.authenticated` 为 `false`;独立局域网监听仍返回 `401` |
48
48
 
49
- - 移动网关默认关闭。手动开启后,默认 5 分钟内没有客户端成功建立连接就自动关闭。
49
+ - 未保存用户选择且未显式配置时,移动网关默认关闭。管理界面支持关闭、临时开启、常驻开启,选择会持久化。
50
+ - 临时开启后默认 5 分钟内没有客户端成功建立连接才自动关闭;成功连接后本次运行保持开启,重启重新计时。常驻开启没有该计时器。
51
+ - 启动优先级为持久化选择 > `gatewayMode` > 旧 `gatewayEnabled`;旧配置 true 映射常驻,false 映射关闭。
50
52
  - 关闭移动网关会关闭现有连接,WebSocket close code 为 `4004`。
51
53
  - 从 Debug 模式重新开启鉴权时,所有无凭证连接会被关闭,close code 为 `4003`。
52
54
  - Debug 鉴权开关只影响 DSH 自带的本机监听,并且只在当前 DSH 进程中生效;独立局域网监听始终强制设备鉴权。
@@ -88,6 +90,26 @@ const pairingText = Buffer.from(JSON.stringify(payload), 'utf8').toString('base6
88
90
 
89
91
  长期 token 默认禁止放在 URL query 中,避免被代理日志、浏览器历史和监控系统记录。缺少凭证、凭证无效、配对码过期或重复使用时,HTTP Upgrade 返回 `401 Unauthorized`。
90
92
 
93
+ #### 多网关身份扩展(当前源码,尚未发布新版本)
94
+
95
+ 保持配对 `version: 2`、子协议 `dsh-mobile-v1` 和 `hello.protocol: 3`。以下新增字段不改变已有业务帧语义:
96
+
97
+ | 新字段 | 返回位置 | 约定 |
98
+ |---|---|---|
99
+ | `gatewayId` | 配对载荷、`paired`、`hello`、`GET /mgw/status` | 持久化 UUID v4;同一网关各访问地址、双通道及重启前后相同 |
100
+ | `gatewayName` | 同上 | 展示名称,允许改变,不能作为身份或鉴权依据 |
101
+ | `endpoints` | 配对载荷、`GET /mgw/status` | 规范化去重的 URL 字符串数组;不在 `hello` 或 `paired` 返回 |
102
+
103
+ 配对仍保留 `publicUrl`,且它排在本次 `endpoints` 第一项。其后按本机配对请求 `endpoints`、插件配置 `endpoints`、公网配置、已监听 LAN 地址的顺序合并。每个输入列表及合并结果最多 16 项,每项最多 2048 字符。允许 HTTP(S) 输入并转为 WS(S),拒绝凭证、query、fragment、未指定监听地址及公网明文 WS。
104
+
105
+ `POST /mgw/pair` 可额外传入 `endpoints: string[]`,只用于本次配对,不写入配置。移动端对新地址必须在发送凭证前建立信任;`hello` 的 ID 校验发生在鉴权后,不能替代 TLS 和地址确认。多个网关的 token 独立签发、保存和撤销;App 以网关连接上下文路由原有业务请求。
106
+
107
+ 本机管理接口 `POST /mgw/gateway` 接受 `{"mode":"disabled|temporary|persistent"}` 中的一个具体值,例如 `{"mode":"persistent"}`。兼容旧 `{"enabled":true}`(临时开启)与 false(关闭);不能同时传 `mode` 和 `enabled`。返回 `gatewayEnabled`、`gatewayMode`、`waitExpiresAt`、`connectedClients`。参数错误返回 400;保存失败返回 500 且不改变当前运行模式。
108
+
109
+ `GET /mgw/status` 增加身份、地址列表及 `gatewayMode`。模式的持久化不影响配对码有效期和文件传输超时。自动超时关闭若遇到磁盘写入失败,会保持当前进程关闭并记录错误;重启可能仍按旧保存模式运行,需修复存储后再次保存选择。
110
+
111
+ 完整配对示例、旧 App 迁移、地址信任与错误处理见 [App 对接说明](docs/multi-gateway-app-integration.md)。
112
+
91
113
  #### iOS 对接示例
92
114
 
93
115
  首次配对时,先对二维码/手动字符串执行 Base64URL 解码,再从 JSON 解析 `publicUrl`、`pairingCode` 和 `expiresAt`,并在过期前连接:
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排队消息同步及编辑/删除/Steer、Session 归档和重命名的双向同步、停止当前生成并稍后继续、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
8
8
 
9
+ > v0.7.3:优化移动网关运行模式下拉框的箭头间距。
10
+ >
9
11
  > v0.7.2:新增独立对话/控制连接、空 Session 创建、停止生成与稍后继续、排队消息同步及编辑/删除/Steer,以及 Session 归档和重命名的双向同步;兼容 DSH v0.1.2-rc.1 与 v0.1.3-alpha.1。
10
12
 
11
13
  ## 协议与 DSH 兼容层
@@ -21,6 +23,37 @@ DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排
21
23
  - Linux 服务器公网:`wss://<公网 IP>/ws/mobile`
22
24
  - 协议文档:[PROTOCOL.md](PROTOCOL.md)
23
25
 
26
+ ## 多网关第一阶段(当前源码)
27
+
28
+ 插件提供稳定 `gatewayId`、可配置 `gatewayName`、配对候选地址列表,以及可持久化的“关闭 / 临时开启 / 常驻开启”运行模式。一个 App 可以分别配对不同机器上的网关;客户端多网关管理仍需按 [App 对接说明](docs/multi-gateway-app-integration.md) 实现。本次源码尚未发布新的 npm 版本。
29
+
30
+ 在“移动设备”面板选择“常驻开启”,网关会持续接受已授权设备连接,重启后保持。常驻需要 DSH 进程运行、机器未休眠且网络可达,不提供自动发现或网络中转。
31
+
32
+ 部署配置示例(对应 mobile-gateway 插件的 `config`):
33
+
34
+ ```yaml
35
+ gatewayMode: persistent
36
+ gatewayName: 家里电脑
37
+ requireAuth: true
38
+ endpoints:
39
+ - wss://gateway.example.com/ws/mobile
40
+ - ws://192.168.1.10:3081/ws/mobile
41
+ ```
42
+
43
+ 运行模式的优先级:**已保存的界面选择 > `gatewayMode` > 旧 `gatewayEnabled`**。没有保存选择时,旧配置 `gatewayEnabled: true` 对应常驻,false 对应关闭。
44
+
45
+ - 关闭:立即断开移动连接,重启后仍关闭。
46
+ - 临时开启:默认 5 分钟没有设备成功连接则关闭并保存关闭状态;成功连接后本次运行保持开启。若以临时模式重启,则重新开始等待首次连接。`gatewayWaitTimeoutMs` 可配置 30 秒至 30 分钟。
47
+ - 常驻开启:没有无人连接关闭计时器。配对码仍默认 5 分钟过期,文件传输超时等独立规则不变。
48
+
49
+ 状态默认保存到 `<deviceFile>.gateway.json`,通常为 `~/.dsh/mobile-gateway-devices.json.gateway.json`;可通过 `gatewayStateFile` 单独配置。文件包含随机 UUID v4 身份及用户选择,权限为 `0600`,使用同目录临时文件原子替换;损坏时启动报错,不能静默生成新身份。每个运行实例必须使用独立的设备注册文件和状态文件。
50
+
51
+ 升级和迁移机器时应一并保留这两个文件。克隆为新的独立网关时,不复制原实例的状态文件和设备注册文件,让新实例生成新身份并重新配对。不要在运行中删除身份文件来恢复默认模式;如需重新使用启动配置,应停止该实例、备份状态文件、仅将其 `mode` 改为 `null`,保留 `version` 和 `gatewayId` 后重启。
52
+
53
+ `gatewayName` 最多 80 字符,未设置时使用主机名。`endpoints` 是额外候选地址,最多 16 项,每项最多 2048 字符;配对时还会合并首选地址、公网配置与已监听的 LAN 地址,合并超过 16 项会报错。所有地址必须指向同一网关;不得填写 `0.0.0.0` / `::`。地址需要手机实际可达,公网使用 WSS。
54
+
55
+ 测试结果与人工步骤见 [验收报告](docs/multi-gateway-phase1-acceptance.md),完成范围见 [多网关待办](docs/multi-gateway-todo.md)。
56
+
24
57
  ## 配套 iOS 客户端
25
58
 
26
59
  [DeepSeek Harness Mobile](https://github.com/Clarklevis1995/dsh-mobile) 是本仓库的兄弟项目。它是面向 iOS 17+ 的 SwiftUI 原生客户端,支持工作区与会话、工作区内创建文件夹、历史和实时对话、图片、Agent 执行轨迹、Human-in-the-loop,以及由网关配置驱动的命令、技能、模型与权限菜单。
@@ -69,7 +102,7 @@ dsh web
69
102
  适用于 DSH 电脑和 iPhone 位于同一个可互访的局域网。
70
103
 
71
104
  1. 打开 WebUI 的“移动设备”。
72
- 2. 开启“允许移动设备连接”。
105
+ 2. 将“网关运行模式”设为“常驻开启”(短期配对也可选“临时开启”)。
73
106
  3. 保持“设备鉴权”开启。
74
107
  4. 确认面板显示 `ws://<电脑局域网 IP>:3081/ws/mobile`。
75
108
  5. 填写设备名称并点击“生成配对二维码”。
package/cordis.patch.yml CHANGED
@@ -9,6 +9,14 @@
9
9
  # Secure by default: every mobile WebSocket must authenticate with a
10
10
  # paired device credential. Pair/revoke APIs remain local-machine only.
11
11
  gatewayEnabled: false
12
+ # Optional startup mode: disabled | temporary | persistent.
13
+ # Saved management-UI choices take precedence over startup config.
14
+ # gatewayMode: persistent
15
+ # gatewayName: Home PC
16
+ # gatewayStateFile: /path/to/instance-gateway.json
17
+ # Optional additional reachable addresses for this SAME gateway:
18
+ # endpoints:
19
+ # - wss://gateway.example.com/ws/mobile
12
20
  gatewayWaitTimeoutMs: 300000
13
21
  requireAuth: true
14
22
  adminLoopbackOnly: true
@@ -0,0 +1,124 @@
1
+ # 多网关第一阶段:App 对接说明
2
+
3
+ 本文对应 Gateway 插件本次源码实现;尚未发布新的 npm 版本。App 第一阶段目标为保存多个网关并切换连接,暂不要求同时保持多个控制通道。无需中央服务器,也不包含自动发现。
4
+
5
+ ## 1. 不变的协议
6
+
7
+ - WebSocket 路径默认 `/ws/mobile`,子协议仍为 `dsh-mobile-v1`,`hello.protocol` 仍为 `3`。
8
+ - 配对载荷仍为 `version: 2` 的 JSON,经 UTF-8、无 padding Base64URL 编码。
9
+ - 首次连接发送子协议 `dsh-mobile-v1, dsh-pair.<pairingCode>`,同时发送 `X-DSH-Device-ID` 安装级 ID。
10
+ - 配对成功只发送一次 `paired.token`;后续使用 `Authorization: Bearer <token>` 或 `dsh-auth.<token>` 子协议。
11
+ - 控制通道发送 `X-DSH-Channel: control`;取得 token 并确认控制通道的 `hello` 后,会话通道使用同一网关 token 与 `X-DSH-Channel: conversation`。
12
+ - App 不调用 `/mgw/*` 管理接口。这些接口仅供 DSH 本机管理界面使用。
13
+
14
+ ## 2. 新增字段
15
+
16
+ 以下字段均为向后兼容的增量字段。新版 App 对旧插件应允许字段缺失,但新版插件返回的身份必须校验格式及一致性。
17
+
18
+ | 字段 | 类型 | 返回位置 | 含义 |
19
+ |---|---|---|---|
20
+ | `gatewayId` | string,UUID v4 | 配对载荷、`paired`、`hello`、管理状态 | 网关安装实例身份,重启和改地址不变 |
21
+ | `gatewayName` | string | 同上 | 服务端展示名称,可变,不用于合并资料或鉴权 |
22
+ | `endpoints` | string[] | 配对载荷、管理状态 | 同一网关的候选地址,规范化后去重,合并最多 16 个 |
23
+ | `gatewayMode` | string | 管理状态及模式修改响应 | `disabled` / `temporary` / `persistent`;不参与移动业务帧 |
24
+
25
+ `hello` 和 `paired` 不返回地址列表。新增可信地址需重新扫码或经用户明确确认,不能从未认证的广播或错误跳转中自动吸收。
26
+
27
+ 配对载荷示例(这里展示的是解码后的 JSON,不能将原始 JSON 直接交给现有扫码解析器):
28
+
29
+ ```json
30
+ {
31
+ "version": 2,
32
+ "publicUrl": "wss://gateway.example.com/ws/mobile",
33
+ "pairingCode": "<一次性配对码>",
34
+ "expiresAt": 4102444800000,
35
+ "gatewayId": "d56a1098-8519-43a1-9dce-fb99863bf5bb",
36
+ "gatewayName": "家里电脑",
37
+ "endpoints": [
38
+ "wss://gateway.example.com/ws/mobile",
39
+ "ws://192.168.1.10:3081/ws/mobile"
40
+ ]
41
+ }
42
+ ```
43
+
44
+ 成功后收到:
45
+
46
+ ```json
47
+ {
48
+ "kind": "paired",
49
+ "token": "<仅返回一次的长期 token>",
50
+ "device": { "id": "<此网关分配的设备 ID>", "name": "Phone" },
51
+ "gatewayId": "d56a1098-8519-43a1-9dce-fb99863bf5bb",
52
+ "gatewayName": "家里电脑"
53
+ }
54
+ ```
55
+
56
+ 之后 `hello` 保留原字段,并增加相同的 `gatewayId`、`gatewayName`。所有业务请求仍按原协议发送,不需要把 `gatewayId` 加入每条消息;App 根据连接上下文路由。
57
+
58
+ ## 3. 地址语义与信任规则
59
+
60
+ `publicUrl` 是本次配对的首选地址,兼容旧 App。`endpoints` 的来源顺序为:首选地址 → 本次本机配对请求指定的地址 → 插件配置地址 → 已配置公网地址 → 已启动局域网监听的地址。管理状态没有本次配对地址,只返回配置与监听产生的候选地址。
61
+
62
+ - `http` / `https` 输入由插件规范化为 `ws` / `wss`。不允许用户名密码、查询参数、fragment 或 `0.0.0.0` / `::` 监听地址;公网明文 WS 被拒绝。
63
+ - 每个输入列表最多 16 项,每项最多 2048 字符;合并后超过 16 个也会拒绝。候选地址可能包含手机不可达的 loopback 地址,App 应过滤或提示,不可据此判断网关离线。
64
+ - 地址顺序不保证网络可达或代表实时延迟;App 可以优先尝试最近成功且已确认可信的地址。
65
+ - 只有来自用户认可的配对二维码/手动导入,或用户单独确认的地址,才可使用凭证。`gatewayId` 是关联标识,不是证书或密码学身份凭据。
66
+ - `hello` 在鉴权之后返回,不能依赖“先发送 token、再比较 ID”保护 token 不被恶意端点窃取;TLS 校验和发送前的地址信任判断必须先完成。局域网明文 WS 仅用于用户信任的网络。
67
+ - 地址被重定向时不得把 token 自动转发到其他来源。
68
+ - 配对码是单次使用。候选地址上的配对请求必须串行;若发生网络断开且无法确定配对是否已被消费,应重新生成二维码,不能对多个地址并行消耗同一码。
69
+
70
+ ## 4. 客户端状态与接入流程
71
+
72
+ 建议保存:
73
+
74
+ ```text
75
+ GatewayProfile
76
+ localId 本地稳定记录 ID
77
+ gatewayId? 远端身份,旧插件允许缺失
78
+ gatewayName 服务端名称
79
+ alias? 用户本地别名
80
+ endpoints[] 已确认的候选地址
81
+ preferredEndpoint? 最近成功地址
82
+ credentialRef 按 localId 隔离的安全存储引用
83
+ remoteDeviceId? 此网关返回的 device.id
84
+ lastConnectedAt?
85
+ ```
86
+
87
+ 1. 将旧单网关配置迁移为一条资料;保留 token、缓存和草稿,迁移要幂等。未配对成功的新资料不覆盖当前网关。
88
+ 2. 解析配对载荷并检查版本、有效期、地址和身份字段。已有相同 `gatewayId` 时提示更新资料,不按名称或 IP 合并。
89
+ 3. 建立控制通道;收到 `paired` 时核对其身份与二维码相符,再立即将 token 写入该资料的安全存储。持久化失败时阻止后续业务,提示重新配对。
90
+ 4. 收到 `hello` 后再次核对身份,保存服务端名称及原协议能力列表。二维码带有身份时,缺失或不一致的返回均视为异常,不能降级为旧网关。
91
+ 5. 根据页面需要使用该网关 token 建立会话通道,再核对其 `hello.gatewayId`。
92
+ 6. 重连仅使用该资料的已确认地址与凭证。网关 ID 不匹配时关闭连接、保留原记录并提示重新确认。
93
+ 7. 再次配对同一安装会轮换该网关的 token。保存新 token 后主动重建该网关现有通道;不影响其他网关。
94
+
95
+ 旧插件完全没有身份字段时,以 `localId` 隔离资料。升级后只经原本可信且认证成功的连接绑定远端 ID,遇到记录冲突须提示,不能静默合并。
96
+
97
+ 第一阶段切换网关时应关闭旧连接、取消订阅与待处理请求。连接代次和网关 ID 一起绑定到异步回调,丢弃旧回调。数据键使用 `(localId, resourceId)`,模型、工作区、会话、任务、审批和文件均需隔离。
98
+
99
+ 发送消息、审批、停止任务等写操作超时后,不自动重发;查询状态后再决定操作,避免重复执行。删除本地网关不等同于撤销服务端授权。
100
+
101
+ ## 5. 错误与状态处理
102
+
103
+ | 结果 | App 行为 |
104
+ |---|---|
105
+ | Upgrade HTTP 503 | 显示网关关闭或不可用;有界退避,提示本机开启 |
106
+ | Upgrade HTTP 401 | 凭证无效、配对过期或已使用;停止用同一凭证无限重试,提供重新配对 |
107
+ | Upgrade HTTP 400 | 检查安装级 ID 等协议参数,显示可理解的错误 |
108
+ | close 4003 | 可能是撤销设备或重新开启鉴权;重新检查授权,不能仅显示普通网络中断 |
109
+ | close 4004 | 显示网关已关闭,避免紧密重连循环 |
110
+ | 网络断开 | 当前资料独立退避重连,不将其他网关标记离线 |
111
+ | 返回身份不匹配 | 停止业务,不覆盖既有身份、地址或 token |
112
+
113
+ ## 6. 已核对的 App 代码入口
114
+
115
+ 在兄弟仓库中只读检查了以下入口,本次没有修改 App:
116
+
117
+ - `DeepSeekHarnessMobile/Core/GatewayModels.swift`:`GatewayPairingPayload` 当前只有四个旧字段;需要增加可选字段,并在业务帧模型补充身份。
118
+ - `DeepSeekHarnessMobile/Core/PairingPayloadParser.swift`:当前严格检查 Base64URL、version 2 和过期时间;应保留原校验并增加新字段规则。
119
+ - `DeepSeekHarnessMobile/Core/GatewayFrameRouter.swift`:`paired` / `hello` 路由需传递网关身份,绑定所属连接上下文。
120
+ - `DeepSeekHarnessMobile/Core/AppStore.swift`:配对入口需要从单网关状态迁移到按资料管理。其余连接、缓存及安全存储调用点需在 App 实施时继续排查。
121
+
122
+ 现有 Codable 解码器会忽略新增字段,已用当前真实配对模型与解析器执行兼容性样例;这不代表 App 已实现多网关,也不能代替真机验收。
123
+
124
+ App 交付验收以 [第一阶段验收报告](multi-gateway-phase1-acceptance.md) 的人工步骤及 [待办](multi-gateway-todo.md) A1–A4 为准。
@@ -0,0 +1,179 @@
1
+ # 多网关第一阶段:Gateway 验收报告
2
+
3
+ ## 结论与范围
4
+
5
+ 2026-09-09:第一阶段 Gateway 插件能力已实现,自动化回归通过。多网关待办 G1–G4 已勾选;App A1–A4、第二/三阶段及真实多机人工验收保持未完成。
6
+
7
+ 本次只修改插件仓库,未修改 App,未部署到用户的服务器/PC,未发布 npm 新版本。当前 package.json 仍为 0.7.2,因此验收应使用本次工作区构建/打包产物,不能直接用 npm 上的同版本发布包推断已包含改动。
8
+
9
+ ## 交付内容
10
+
11
+ | 能力 | 实现位置 | 结果 |
12
+ |---|---|---|
13
+ | 三种模式、界面选择、状态持久化及启动优先级 | `lib/index.mjs`、`lib/client.js`、`lib/gateway-state.mjs` | 已实现 |
14
+ | 稳定网关 UUID、配置名称、损坏时拒绝启动 | `lib/gateway-state.mjs`、`lib/index.mjs` | 已实现 |
15
+ | 配对候选地址合并、去重、URL 校验 | `lib/index.mjs` | 已实现 |
16
+ | 配对/paired/hello 身份扩展及旧协议保留 | `lib/index.mjs`、`PROTOCOL.md` | 已实现 |
17
+ | App 字段、迁移、路由及安全存储约定 | [App 对接说明](multi-gateway-app-integration.md) | 已交付文档;App 尚未实现 |
18
+ | 实施范围及勾选状态 | [多网关待办](multi-gateway-todo.md) | 仅勾选已完成的 Gateway 项 |
19
+
20
+ ## 自动化验证
21
+
22
+ 执行环境:macOS arm64、Node.js v24.19.0;兼容性样例使用 Apple Swift 6.3.3。集成测试启动本地 HTTP/WebSocket 服务,DSH Host 使用测试替身,未连接真实 DSH 服务。测试因沙箱禁止监听本机端口,使用经批准的沙箱外执行完成。
23
+
24
+ | 验证 | 结果 | 证据/范围 |
25
+ |---|---|---|
26
+ | `npm test` | 通过 | setup-ip、host-adapter、gateway、auth、lan、multi-gateway 六个测试脚本 |
27
+ | 原业务分发回归 | 通过 | `FULL GATEWAY DISPATCH TESTS PASSED (115)` |
28
+ | 新增多网关验证 | 通过 | `multi-gateway: 11 checks passed`,见下方明细 |
29
+ | LAN 与主监听身份一致 | 通过 | LAN 配对取得 token 后连接本机监听,比较 gatewayId;验证两入口仍按原规则鉴权 |
30
+ | 旧 iOS 配对解码兼容 | 通过 | 从兄弟仓库提取实际 `GatewayPairingPayload` 和 `PairingPayloadParser`,使用 Swift 执行带 gatewayId/gatewayName/endpoints 的 v2 样例 |
31
+ | JavaScript 语法与补丁空白检查 | 通过 | `node --check` 检查修改后的 JS/MJS;`git diff --check` |
32
+ | 真机扫码、真实界面布局及真实多机联调 | 未执行 | 按下方步骤人工验收 |
33
+
34
+ 新增 11 组行为检查:
35
+
36
+ 1. Schema 模式校验、身份及模式持久化、文件权限 0600、损坏文件拒绝加载且保留原文件。
37
+ 2. 旧启动配置 true 仍表示常驻;不同实例 ID 不同。
38
+ 3. 主动关闭后连接返回 503,重启及更改启动配置不会覆盖已保存关闭状态;非法请求返回 400。
39
+ 4. 旧 enabled 接口映射临时模式,临时模式重启重新计时,自动关闭结果持久化。
40
+ 5. 切换常驻取消旧计时器,重启后模式与身份保持;名称变化不改变身份。
41
+ 6. v2 Base64URL 配对载荷保留旧字段,新增地址规范化/去重;非法地址和过量列表返回 400。
42
+ 7. 配对载荷、paired、控制 hello、会话 hello 身份一致,后续 token 连接不重复发送 paired。
43
+ 8. 有客户端时切换临时模式不会设置等待计时器,断线后保持开启;重新配对轮换 token 并复用本机设备记录。
44
+ 9. 同一客户端安装 ID 在两个网关分别配对,token 不能跨网关使用;撤销 A 不影响 B。
45
+ 10. 主动关闭断开现有 WebSocket,close code 为 4004。
46
+ 11. 模拟持久化失败返回 500,当前运行模式不被改变。
47
+
48
+ 说明:临时超时测试直接调用插件时使用 120ms 等短测试值以缩短执行时间;真实 Cordis Schema 最小值仍为 30000ms。常驻测试证明其不会被等待首次连接的计时器关闭,不代表完成了长时间运行稳定性或压力测试。
49
+
50
+ 复验命令(在插件仓库执行):
51
+
52
+ ```bash
53
+ npm test
54
+ node --check lib/index.mjs
55
+ node --check lib/client.js
56
+ node --check lib/gateway-state.mjs
57
+ git diff --check
58
+ ```
59
+
60
+ ## 人工验收准备
61
+
62
+ 1. 准备两台可访问的机器 A/B;完整目标验收再增加第三台 C(例如一台服务器、两台 PC)。每台运行包含本次改动的插件及兼容 DSH。
63
+ 2. 使用独立的测试设备注册文件和 `gatewayStateFile`;不要与日常使用实例共用文件。给每台配置不同的 `gatewayName`。
64
+ 3. 为测试实例设置 `gatewayWaitTimeoutMs: 30000`,保留 `requireAuth: true`;实际超时等待 35 秒。测试结束恢复所需生产值。
65
+ 4. 打开各自本机 WebUI 的“移动设备”。准备旧版 App 做兼容验收;多网关客户端步骤需等待 App 按对接说明实现。
66
+ 5. 若使用公网入口,先准备有效 WSS 和手机可达的网络。不要为了测试把 `/mgw/*` 管理接口开放到公网。
67
+ 6. 记录插件来源、DSH/App 版本、机器与网络、执行时间。下方全部人工项尚未执行,应由验收者填写实测结果。
68
+
69
+ 可在运行该实例的电脑上检查状态。默认示例端口为 3080,实际端口不同时替换:
70
+
71
+ ```bash
72
+ curl --fail-with-body http://127.0.0.1:3080/mgw/status
73
+ ```
74
+
75
+ ### M1. 常驻选择、无连接等待与重启
76
+
77
+ - [ ] 已人工通过
78
+
79
+ 操作:所有手机断开 → 面板选择“常驻开启” → 读取状态并记录 gatewayId → 等待 35 秒 → 再读取状态 → 停止并重启测试 DSH → 再读取状态。
80
+
81
+ 预期:每次 `gatewayMode=persistent`、`gatewayEnabled=true`、`waitExpiresAt=null`,gatewayId 不变;面板仍显示常驻。重新扫码或使用已有可信凭证可连接。
82
+
83
+ ### M2. 主动关闭持久化
84
+
85
+ - [ ] 已人工通过
86
+
87
+ 操作:手机保持连接 → 面板选择“关闭” → 观察手机断线 → 读取状态 → 保持启动配置为 persistent,重启 DSH → 再连接。
88
+
89
+ 预期:连接关闭码为 4004;状态 disabled/false/null;重启后仍关闭,重新连接 Upgrade 返回 503。面板改回常驻后恢复。
90
+
91
+ ### M3. 临时模式的三个分支
92
+
93
+ - [ ] 已人工通过
94
+
95
+ 操作 A:没有手机连接时选临时开启,等待 35 秒,再重启 DSH。
96
+
97
+ 预期 A:先显示等待截止时间,超时自动变关闭,重启仍关闭。
98
+
99
+ 操作 B:选临时开启,在 30 秒内完成手机连接,再断开手机,等待 35 秒。
100
+
101
+ 预期 B:首次连接清除等待计时器,断开后本次运行仍开启。
102
+
103
+ 操作 C:在 B 的状态重启 DSH,确保手机关闭自动重连,等待 35 秒。
104
+
105
+ 预期 C:重启后临时模式重新计时,未连接则自动关闭。再次开启临时后立即切为常驻,等待 35 秒,确认旧计时器没有将常驻关闭。
106
+
107
+ ### M4. 协议身份及旧 App 兼容
108
+
109
+ - [ ] 已人工通过
110
+
111
+ 操作:在 A 面板生成新二维码并复制配对字符串;检查解码结果为 version 2,包含原四字段及 gatewayId/gatewayName/endpoints。用旧版 App 扫码,查看工作区、会话并发一条测试消息。
112
+
113
+ 预期:旧版 App 能扫码并正常完成原有业务;服务端使用原子协议和 hello protocol 3。用带帧调试能力的测试客户端核对 paired、control hello、conversation hello 与二维码中的 gatewayId 全部相同。不要把配对码或 token 贴入验收记录。
114
+
115
+ ### M5. 同一网关多个地址
116
+
117
+ - [ ] 已人工通过
118
+
119
+ 操作:给 A 配置已实际可达的 LAN 和 WSS 地址;重新启动后生成配对码。用测试客户端或新版 App 完成首次配对,然后以所得同一 token 分别连接两个地址。
120
+
121
+ 预期:二维码保留所选 publicUrl,候选地址去重;两地址返回同一 gatewayId。LAN 地址不可达时只影响该地址,不生成另一条网关身份。
122
+
123
+ 反例:在本机配对请求传入下方错误地址,应返回 HTTP 400,不生成可用二维码:
124
+
125
+ ```bash
126
+ curl -i http://127.0.0.1:3080/mgw/pair \
127
+ -H 'Content-Type: application/json' \
128
+ --data '{"publicUrl":"wss://0.0.0.0/ws/mobile"}'
129
+ ```
130
+
131
+ ### M6. 不同网关凭证及撤销隔离
132
+
133
+ - [ ] 已人工通过
134
+
135
+ 操作:测试客户端使用同一个安装级 `X-DSH-Device-ID` 分别配对 A、B → 确认 ID 不同 → 使用 A token 连接 B(反向也测试)→ 用各自 token 正常连接 → 在 A 面板撤销测试设备。
136
+
137
+ 预期:跨网关 token 均返回 401;A 连接以 4003 关闭且旧 token 不再可用;B 连接、token、业务访问不受影响。
138
+
139
+ ### M7. 名称、配置和文件故障
140
+
141
+ - [ ] 已人工通过
142
+
143
+ 操作 A:记录 A 身份,修改 gatewayName 与候选地址配置,重启。
144
+
145
+ 预期 A:名称和地址更新,gatewayId 不变。
146
+
147
+ 操作 B(仅独立测试实例):停止 DSH,备份其状态文件,临时写入无效 JSON,再启动;随后停止失败实例、恢复原备份并重启。
148
+
149
+ 预期 B:损坏时明确报 `failed to load gateway state`,没有静默覆盖文件或换 ID;恢复后原身份仍可用。
150
+
151
+ 操作 C:在独立测试实例模拟状态文件不可写,修改运行模式。
152
+
153
+ 预期 C:管理请求返回 500,界面显示失败,当前模式不变。恢复写权限后可以正常保存。若模拟的是自动超时保存失败,当前进程仍关闭并记录错误;应修复存储并重新保存,不能据此保证重启后的模式。
154
+
155
+ ### M8. 多网关 App 验收(待 App 实现)
156
+
157
+ - [ ] 已人工通过
158
+
159
+ 操作:新版 App 依次添加 A/B/C → 重启 App → 在三台网关之间切换 → 每台创建不同测试会话并检查工作区、模型配置、审批、文件 → 让 A 离线再切到 B/C → 再次扫描 B 的二维码更新配对。
160
+
161
+ 预期:资料及凭证保留;会话及操作始终属于正确网关;A 离线不影响 B/C;重复扫码更新 B 而不是新增重复资料或覆盖 A/C。进一步构造跨网关相同资源 ID 和切换时旧事件迟到,验证缓存和回调隔离。
162
+
163
+ 本项不是 Gateway 单仓库可完成的验收,本报告不勾选。
164
+
165
+ ### M9. 未知地址与身份不匹配(待 App 实现)
166
+
167
+ - [ ] 已人工通过
168
+
169
+ 操作:在独立测试环境让 A 资料指向 B 或一个不可信新地址,尝试恢复连接;再验证同一 gatewayId 的正常地址更新流程。
170
+
171
+ 预期:App 不自动向未经确认的地址发送凭证;可信地址返回错误身份时停止业务且保留原记录。不能通过同名或自报 gatewayId 绕过确认与 TLS 校验。
172
+
173
+ ## 人工记录模板
174
+
175
+ | 编号 | 实际结果 | 通过/失败/未执行 | 证据(脱敏) | 执行人/时间 |
176
+ |---|---|---|---|---|
177
+ | M1–M9(每项独立填写) | 待填写 | 未执行 | 待填写 | 待填写 |
178
+
179
+ 发布前至少完成 M1–M7。宣称 App 第一阶段整体完成前,还需完成 A1–A4 及 M8–M9。后台推送、并行控制连接、mDNS 发现、中心目录和网络中转不在本次交付范围。
@@ -0,0 +1,137 @@
1
+ # dsh-mobile 多网关支持待办
2
+
3
+ ## 目标与范围
4
+
5
+ 一个 dsh-mobile 客户端可以添加、保存并连接部署在云服务器或不同 PC 上的多个独立 Gateway。各网关拥有独立的凭证、连接状态和业务数据。
6
+
7
+ 第一阶段支持多网关管理与切换连接;第二阶段支持前台同时连接多个网关;第三阶段按需增加局域网自动发现。第一阶段无需中心服务器。
8
+
9
+ 2026-09-09:第一阶段 Gateway 插件 G1–G4 已实现并通过自动化回归,以下已完成项打勾。App 未修改,后续阶段及人工多机验收尚未完成。详见 [App 对接说明](multi-gateway-app-integration.md) 与 [验收报告及人工步骤](multi-gateway-phase1-acceptance.md)。
10
+
11
+ ## 实施前基础
12
+
13
+ - 启动配置 `gatewayEnabled: true` 已支持常驻,不会启动无人连接自动关闭计时器。
14
+ - 管理界面手动开启后,默认 5 分钟无人连接才自动关闭;成功连接后会清除计时器,后续断线不会重新计时。
15
+ - 已有一次性配对码、长期设备 token、设备撤销和独立局域网 WebSocket 监听。
16
+ - 已有 `control` / `conversation` 通道拆分,适合后续多网关并行连接。
17
+ - 当前 `hello` 和配对载荷没有稳定的网关身份字段,局域网监听也不等于自动发现。
18
+
19
+ ## 第一阶段:网关插件待办
20
+
21
+ ### G1. 明确并持久化常驻模式
22
+
23
+ - [x] 将运行模式明确为关闭、临时开启、常驻开启;管理界面显示当前模式。
24
+ - [x] 常驻开启不启动无人连接关闭计时器;临时开启保留当前等待首次连接的超时行为。
25
+ - [x] 持久化用户选择;优先级为已保存模式 > `gatewayMode` > 旧 `gatewayEnabled`。临时模式重启重新计时,自动关闭后保存关闭状态。
26
+ - [x] 管理界面切换到常驻后,重启仍保持常驻;主动关闭后按约定保持关闭。
27
+ - [x] 保留配对码有效期、请求超时和文件传输空闲超时,避免与网关运行模式混淆。
28
+ - [x] 更新管理接口、界面文案、配置说明与部署说明,注明常驻依赖 DSH 进程运行及机器网络可达。
29
+
30
+ ### G2. 增加稳定网关身份
31
+
32
+ - [x] 首次启动生成随机 `gatewayId` 并持久化,重启、升级、地址变化时保持不变。
33
+ - [x] 支持可配置的 `gatewayName`,用于展示机器名称;App 可单独保存本地别名。
34
+ - [x] 明确身份文件的保存位置(默认 `<deviceFile>.gateway.json`)、原子写入与损坏处理,避免损坏后静默生成新身份。
35
+ - [x] 说明克隆部署时必须生成新的网关身份,避免多台机器复用同一 `gatewayId`。
36
+ - [x] 在 `hello`、首次配对成功帧和本机管理状态中增加网关身份信息。
37
+ - [x] 配对载荷携带 `gatewayId`、`gatewayName`,协议字段命名在实现前与 App 对齐。
38
+
39
+ ### G3. 支持同一网关的多个访问地址
40
+
41
+ - [x] 定义可选的 `endpoints: string[]` 字段,在配对载荷及本机管理状态返回候选地址,并保留现有 `publicUrl` 兼容字段。
42
+ - [x] 允许管理员配置并在配对时提供多个候选地址,不把监听地址 `0.0.0.0` 当作可连接地址。
43
+ - [x] 多个地址指向同一网关时返回相同身份,控制和会话通道返回相同身份。
44
+ - [x] 候选地址仅作为连接提示,不作为身份认证依据;地址变化不自动建立信任。
45
+
46
+ ### G4. 协议与兼容性
47
+
48
+ - [x] 更新 `PROTOCOL.md`,给出网关身份、地址列表、常驻模式和配对示例。
49
+ - [x] 优先采用可选字段扩展,保持已有请求与响应含义不变;验证旧 App 的解码行为后再决定是否升级配对载荷版本。
50
+ - [x] 保持每个网关独立签发和撤销设备 token,禁止引入跨网关通用 token。
51
+ - [x] 增加验证:身份持久化、常驻无客户端、临时模式超时、模式切换与重启、旧客户端兼容、双通道身份一致。
52
+
53
+ ## 第一阶段:App 待办
54
+
55
+ ### A1. 多网关资料与迁移
56
+
57
+ - [ ] 建立 `GatewayProfile`:本地记录 ID、远端 `gatewayId`(旧网关允许缺失)、名称、本地别名、候选地址、凭证引用、最近连接时间。
58
+ - [ ] 将现有单网关配置迁移为默认网关,保留原凭证与用户数据;迁移可重复执行且不产生重复记录。
59
+ - [ ] 按网关隔离安全存储中的 token 和服务端返回的 device ID;客户端安装级设备 ID 可继续复用,不能代替鉴权凭证。
60
+ - [ ] 支持添加、重命名、排序、删除网关和选择默认网关。
61
+ - [ ] 再次扫描同一网关时更新已有记录;若重新配对导致 token 轮换,替换凭证并重建该网关连接。
62
+ - [ ] 区分本地删除与服务端撤销授权;本地删除不宣称已撤销服务端设备权限。
63
+
64
+ ### A2. 连接管理与恢复
65
+
66
+ - [ ] 将单一连接管理改为按网关创建连接上下文,包含控制通道、会话通道、请求等待队列和重连任务。
67
+ - [ ] 第一阶段只保持当前网关的业务连接;切换时取消旧订阅与等待请求,并阻止旧回调污染新页面。
68
+ - [ ] 每个网关独立维护未连接、连接中、在线、离线、需要重新配对等状态。
69
+ - [ ] 连接后核对已保存的网关身份;身份不匹配时停止业务操作并提示重新确认,不能自动覆盖原记录。
70
+ - [ ] 新发现或未经确认的地址不直接携带已有 token 探测;制定候选地址信任规则,保留 TLS 校验。
71
+ - [ ] 支持网络恢复后的退避重连与候选地址切换;鉴权失败进入重新配对流程,避免无限重试。
72
+ - [ ] 定义旧网关兼容行为:缺少 `gatewayId` 时使用本地记录 ID 隔离,升级后经可信连接绑定远端身份,不能仅凭名称合并。
73
+
74
+ ### A3. 业务数据隔离
75
+
76
+ - [ ] 会话、工作区、任务、Goal、文件缓存等使用 `(本地网关记录 ID, resourceId)` 作为客户端存储与索引范围。
77
+ - [ ] 所有发送、审批、停止、重命名、归档和文件操作显式携带所属网关上下文。
78
+ - [ ] 页面、订阅、异步回调和待处理请求绑定网关及连接代次,丢弃过期连接的响应。
79
+ - [ ] 切换网关后加载对应配置、模型与能力列表,不复用其他网关的能力判断。
80
+ - [ ] 连接中断后不自动重发发送消息、审批等写操作;结果不确定时先查询状态,避免重复执行。
81
+ - [ ] 明确删除网关时本地缓存、下载文件与会话草稿的处理规则,并在界面说明。
82
+
83
+ ### A4. 界面与验证
84
+
85
+ - [ ] 增加网关列表或切换入口,展示名称、连接状态、当前网关及重新配对入口。
86
+ - [ ] 扫码添加新网关不覆盖已有网关;已存在时明确提示更新。
87
+ - [ ] 会话和审批页面显示所属网关,避免用户向错误机器发送操作。
88
+ - [ ] 增加验证:旧配置迁移、重复配对、切换期间事件到达、跨网关同名同 ID 资源、凭证隔离、撤销后重连、地址与身份不匹配。
89
+
90
+ ## 第二阶段:前台同时连接多个网关
91
+
92
+ ### 网关插件
93
+
94
+ - [ ] 验证仅连接控制通道时的状态同步,以及对审批/提问认领和释放行为的影响。
95
+ - [ ] 验证会话通道按需建立、断开及重建不会影响其他设备或控制通道。
96
+ - [ ] 明确重连后的状态快照与待处理交互恢复约定,有缺口时补充协议或实现。
97
+
98
+ ### App
99
+
100
+ - [ ] 前台为用户启用的多个网关维护控制通道,进入会话时按需连接对应会话通道。
101
+ - [ ] 单个网关连接失败不阻塞其他网关初始化、列表加载或操作。
102
+ - [ ] 如增加聚合任务/会话页面,每项保留来源标识,点击后路由至所属网关。
103
+ - [ ] 限制并发连接与重连频率,处理进入后台、返回前台和网络切换后的状态恢复。
104
+ - [ ] 不将后台常驻 WebSocket 作为可靠通知方案;若需要后台审批通知,另立推送方案待办。
105
+
106
+ ## 第三阶段:局域网自动发现(可选)
107
+
108
+ ### 网关插件
109
+
110
+ - [ ] 增加可选的 mDNS / DNS-SD 广播,约定服务类型、网关 ID、名称、端口、路径与协议版本。
111
+ - [ ] 广播中不包含 token 或配对码;服务关闭时停止广播。
112
+ - [ ] 处理多网卡、地址变化与广播生命周期。
113
+
114
+ ### App
115
+
116
+ - [ ] 实现局域网服务浏览及相应平台权限流程,提供拒绝权限后的扫码/手动添加入口。
117
+ - [ ] 展示“发现但未配对”的网关,发现结果仍需完成配对才能访问业务数据。
118
+ - [ ] 对发现结果去重;广播身份仅用于提示,不据此自动更新可信地址或发送凭证。
119
+
120
+ 跨互联网自动发现不属于本阶段。如果后续需要登录账号后自动列出所有机器,再设计设备目录、登记鉴权、心跳和访问网络方案;目录服务本身不能解决网络不可达问题。
121
+
122
+ ## 实施顺序与验收
123
+
124
+ 1. 先对齐身份字段、地址信任规则与兼容策略,再并行开展网关和 App 实现。
125
+ 2. 完成 G1–G4 与 A1–A4,交付可保存多个网关并可靠切换的版本。
126
+ 3. 再实施前台并行连接,最后按需求增加自动发现。
127
+
128
+ 第一阶段验收清单:
129
+
130
+ - [ ] 同一 App 添加一台服务器和两台 PC,资料及凭证在重启后保留。
131
+ - [ ] 三个网关均可常驻,长时间无客户端后仍能连接;网关重启后身份不变。
132
+ - [ ] 任意切换网关,会话、工作区、模型配置、审批及文件不会串用。
133
+ - [ ] 两个网关返回相同资源 ID 时,缓存和操作仍正确隔离。
134
+ - [ ] 一台机器离线或凭证被撤销,不影响其他网关。
135
+ - [ ] 同一网关更换已确认的访问地址后可恢复连接;不同身份的端点不会被误绑定。
136
+ - [ ] 旧 App 连接新插件、旧单网关配置迁移到新 App 均通过兼容性验证。
137
+ - [ ] 网关插件现有测试与新增行为测试通过,App 完成对应自动化验证及多机联调。