dsh-plugin-mobile-gateway 0.7.6 → 0.8.1

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
@@ -1,6 +1,6 @@
1
1
  # dsh Mobile Gateway — WebSocket 协议参考
2
2
 
3
- 移动端通过经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop 提问与操作审批、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现。当前源码仅适配 DSH 0.1.5-rc.2 / Session format 3,移动端接入变更见 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md)。
3
+ 移动端通过经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop 提问与操作审批、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现。当前源码适配 DSH 0.1.7-rc.2 / Session format 4;移动端实时流接入见 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md)。
4
4
 
5
5
  - **本机端点**:`ws://127.0.0.1:3080/ws/mobile`(与 dsh web GUI 同端口)
6
6
  - **局域网端点**:`ws://<电脑的私有局域网 IP>:3081/ws/mobile`(插件独立监听,只提供经过鉴权的 WebSocket)
@@ -100,7 +100,7 @@ const pairingText = Buffer.from(JSON.stringify(payload), 'utf8').toString('base6
100
100
  | `gatewayName` | 同上 | 展示名称,允许改变,不能作为身份或鉴权依据 |
101
101
  | `endpoints` | 配对载荷、`GET /mgw/status` | 规范化去重的 URL 字符串数组;不在 `hello` 或 `paired` 返回 |
102
102
 
103
- 配对仍保留 `publicUrl`,且它排在本次 `endpoints` 第一项。其后按本机配对请求 `endpoints`、插件配置 `endpoints`、公网配置、已监听 LAN 地址的顺序合并。每个输入列表及合并结果最多 16 项,每项最多 2048 字符。允许 HTTP(S) 输入并转为 WS(S),拒绝凭证、query、fragment、未指定监听地址及公网明文 WS。
103
+ 配对仍保留 `publicUrl`,且它排在本次 `endpoints` 第一项。其后按本机配对请求 `endpoints`、插件配置 `endpoints`、公网配置、已取得的 Cloudflare Tunnel 地址、已监听 LAN 地址的顺序合并。每个输入列表及合并结果最多 16 项,每项最多 2048 字符。允许 HTTP(S) 输入并转为 WS(S),拒绝凭证、query、fragment、未指定监听地址及公网明文 WS。
104
104
 
105
105
  `POST /mgw/pair` 可额外传入 `endpoints: string[]`,只用于本次配对,不写入配置。移动端对新地址必须在发送凭证前建立信任;`hello` 的 ID 校验发生在鉴权后,不能替代 TLS 和地址确认。多个网关的 token 独立签发、保存和撤销;App 以网关连接上下文路由原有业务请求。
106
106
 
@@ -108,6 +108,8 @@ const pairingText = Buffer.from(JSON.stringify(payload), 'utf8').toString('base6
108
108
 
109
109
  `GET /mgw/status` 增加身份、地址列表及 `gatewayMode`。模式的持久化不影响配对码有效期和文件传输超时。自动超时关闭若遇到磁盘写入失败,会保持当前进程关闭并记录错误;重启可能仍按旧保存模式运行,需修复存储后再次保存选择。
110
110
 
111
+ Windows/macOS PC 可从本机管理接口 `POST /mgw/cloudflare` 一键启用 Quick Tunnel:`{"enabled":true,"mode":"quick"}`,无需账户、域名或 Token;返回的随机公网地址在 `GET /mgw/status` 的 `cloudflare.publicUrl` 中,隧道重启后可能变化,App 需重新扫码确认。固定地址可用命名 Tunnel:`{"enabled":true,"mode":"named","hostname":"gateway.example.com","token":"<Tunnel Token>"}`;后续更新可省略 Token 或传空字符串以保留已保存的 Token,关闭时只需 `{"enabled":false}`。状态字段返回模式、支持状态、公开地址、本机入口端口、连接状态和错误,但绝不返回 Token。启用时网关自动切为常驻模式;配置与 Token 保存在独立的 `0600` 文件中。插件会在需要时下载并校验 Cloudflare 官方 `cloudflared` 二进制。两种模式仅在 `127.0.0.1:3082`(可配置)接受已鉴权的 `/ws/mobile` WebSocket,其他 HTTP 路径返回 404;网关关闭或插件卸载时该入口及受管进程一同停止。
112
+
111
113
  完整配对示例、旧 App 迁移、地址信任与错误处理见 [App 对接说明](docs/multi-gateway-app-integration.md)。
112
114
 
113
115
  #### iOS 对接示例
@@ -174,6 +176,8 @@ func connectAuthenticated(publicURL: URL, token: String) -> URLSessionWebSocketT
174
176
  `subscribed` 之后,服务端会紧接着发送该 Session 尚未处理的
175
177
  `question-requested` / `approval-requested`,并标记 `replay: true`。客户端必须按
176
178
  `rpcId` 去重。这保证移动端在审批产生后才打开已有 Session 时仍能显示待处理卡片。
179
+ 启用 `split-channels` 时,`subscribe` 仍由 conversation 连接发送,但上述重放帧会
180
+ 桥接到 control 连接;交互卡片不得发送到 conversation 连接。
177
181
 
178
182
  ---
179
183
 
@@ -186,9 +190,16 @@ Human-in-the-loop 分为两条独立通道:
186
190
 
187
191
  二者都是 Host waterfall 的临时请求,不属于持久化的 `session/event`,且都必须以插件为该次请求生成的 `rpcId` 通过专用响应帧回答,不能作为普通 `message` 发送。该内部实现不改变移动端帧格式。
188
192
 
193
+ 同一请求遵循“任意端先完成、所有端同步收起”的单胜方规则:Mobile 任一设备先完成时,Gateway 会结束 Host 的 WebUI 远端请求并让所有 WebUI 实例收起;任一 WebUI 先完成时,Host 会取消其余 WebUI 实例,Gateway 同时向所有 Mobile 设备广播终态。迟到响应统一视为 `not-pending`。
194
+
189
195
  ### 3.1 提问与回答
190
196
 
191
- Agent 调用 DSH 的 `ask_user_question` 工具时,插件直接接入 Host 的 `user-questions/request` waterfall,并把临时请求投影给移动端。若没有可处理该 Session 的移动连接,插件调用 `next()`,由 WebUI 或后续 Host answerer 处理。
197
+ Agent 调用 DSH 的 `ask_user_question` 工具时,插件接入 Host 的
198
+ `user-questions/request` waterfall,把临时请求投影给移动端,同时继续启动 WebUI
199
+ 或后续 Host answerer。任一端先给出有效答案即完成该请求;Gateway 会向所有移动端
200
+ 广播 `question-resolved`。Mobile 先完成时,Gateway 同时取消 WebUI 的对应远端请求,
201
+ 使其收起半屏弹窗;WebUI 先完成时,移动端收到终态后收起对应卡片。若 Mobile 已断开,
202
+ 则由仍在等待的 Host answerer 继续处理。
192
203
 
193
204
  #### `question-requested` — 服务端推送问题
194
205
 
@@ -292,7 +303,12 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件直接接入 Host 的
292
303
 
293
304
  ### 3.2 操作审批
294
305
 
295
- 当 DSH 的工具管线要求人工授权时,插件会接入一次 Host `approval/request` waterfall,并向移动端投影为 `approval-requested`。这正是 Web UI 中“等待审批”卡片对应的能力:`reason` 是面向用户的审批说明,`toolName` 标识请求操作的工具,`callId` 可用于与实时工具调用轨迹关联。
306
+ 当 DSH 的工具管线要求人工授权时,插件会接入一次 Host `approval/request`
307
+ waterfall,并向移动端投影为 `approval-requested`,同时保留 WebUI answerer;WebUI
308
+ 与 Mobile 中任一端先提交的有效决定生效。Gateway 随后向所有移动端广播
309
+ `approval-resolved`;若 Mobile 先完成,还会取消 WebUI 的对应远端请求以收起审批弹窗。
310
+ `reason` 是面向用户的审批说明,`toolName` 标识请求操作的工具,`callId` 可用于与实时
311
+ 工具调用轨迹关联。
296
312
 
297
313
  #### `approval-requested` — 服务端推送待审批操作
298
314
 
@@ -305,6 +321,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件直接接入 Host 的
305
321
  "toolName": "bash",
306
322
  "callId": "call-42",
307
323
  "reason": "escalate sandbox to danger-full-access",
324
+ "displayReason": { "en": "Allow this command?", "zh-CN": "允许执行此命令?" },
308
325
  "replay": true
309
326
  }
310
327
  ```
@@ -312,7 +329,8 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件直接接入 Host 的
312
329
  - `rpcId`:本次可回答请求的稳定 RPC ID;提交决定时必须原样返回。
313
330
  - `approvalId`:移动网关生成的本次审批关联 ID;同样必须原样返回,并用于将最终状态关联到本地审批卡片。
314
331
  - `toolName`:请求审批的工具名。
315
- - `callId` / `reason`:可选。前者可关联工具调用,后者应直接显示为待审批原因。
332
+ - `callId` / `reason`:可选。前者可关联工具调用,后者是原始审批说明。
333
+ - `displayReason`:rc.2 新增的可选多语言说明;客户端优先按当前语言显示,缺失时回退到 `reason`。该字段也会随待处理请求重放。
316
334
  - `replay: true`:表示当前仍未决定的审批在移动端连接或切换 Session 后重放。客户端应按 `rpcId` 去重。
317
335
 
318
336
  审批请求不含工具完整参数;移动端应将 `reason` 与可见的工具调用轨迹作为展示依据,不应自行推断或构造命令。
@@ -554,7 +572,7 @@ let image = [
554
572
 
555
573
  ### 排队消息同步与修改
556
574
 
557
- `hello.capabilities` 包含 `queue-control` 时,控制连接会收到 Host 当前待处理消息。连接初始化或 Host 控制流重连后,网关发送完整快照:
575
+ `hello.capabilities` 包含 `queue-control` 时,控制连接会收到 Host 当前待处理消息。DSH 0.1.7-rc.1 从 `session/control` 的 `projections.values.inbox` 提供待处理消息:`next-turn` 映射为 `queued`,`next-step` 按来源映射为 `steering` 或 `context`。连接初始化或 Host 控制流重连后,网关发送完整快照:
558
576
 
559
577
  ```json
560
578
  {
@@ -575,7 +593,7 @@ let image = [
575
593
  }
576
594
  ```
577
595
 
578
- 之后某个 Session 的队列发生变化时,网关发送该 Session 的完整替换值:
596
+ 之后某个 Session 的 `inbox` 投影发生变化时,网关发送该 Session 的完整替换值:
579
597
 
580
598
  ```json
581
599
  { "kind": "session-queue", "sessionId": "session-abc", "items": [] }
@@ -682,8 +700,8 @@ WebUI、App 或其他客户端造成的变化通过以下帧主动推送:
682
700
  - 返回**原始 SessionEvent**(`{type, seq, time, data}`,方案A),可选裁剪
683
701
  - 图片不会内联进历史页。`user/message.data.content[]` 中的图片块为 `{ "type":"image", "attachment": ImageAttachmentRef }`;iOS 使用其中的 `attachmentId` 请求图片数据
684
702
  - `maxBytes`:单帧字节预算,默认 **4 MiB**;超预算保留最新部分并给出 `nextBeforeSeq` 续页(客户端 16 MiB 上限的安全余量)
685
- - `view: "conversation"`:**对话裁剪模式**——隐藏系统消息与 request/header、request/context,移除 Assistant 事件的内嵌 `data.stream`,`tool/result` 嵌套文本截断到 2000 字符
686
- - 分页:`hasMore` 为真时用 `beforeSeq: nextBeforeSeq, historyFormatVersion: 3` 请求更早一页
703
+ - `view: "conversation"`:**对话裁剪模式**——隐藏系统消息与 request/header、request/context,移除 Assistant 事件的内嵌 `data.stream`,`tool/result` 文本截断到 2000 字符
704
+ - 分页:`hasMore` 为真时用 `beforeSeq: nextBeforeSeq, historyFormatVersion: 4` 请求更早一页
687
705
 
688
706
  ```json
689
707
  → { "kind": "history", "sessionId": "session-abc", "events": [ ...原始事件... ],
@@ -940,7 +958,7 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
940
958
 
941
959
  | type | 参数 | 说明 |
942
960
  |---|---|---|
943
- | `permission-options` | `sessionId?` | 可用权限 preset 列表(`namespace`)+ 该会话当前生效值(`sessionPermissions`) |
961
+ | `permission-options` | `sessionId?` | Host `permissionPresets/catalog` 的 `options`、`defaultOptions`、`defaultPreset`,以及该会话当前生效值(`sessionPermissions`) |
944
962
  | `permission` | `sessionId`, `name` | 切换**该会话**的权限 preset(走官方 `/permission` 命令,不触发模型) |
945
963
 
946
964
  ```json
@@ -958,17 +976,19 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
958
976
  | `agent-presets` | — | preset 名册(含 `isDefault` 标记) |
959
977
  | `session-agent-preset` | `sessionId`, `requestId?` | 读取当前会话的 `agentPreset` 和 `locked` |
960
978
  | `select-agent-preset` | `sessionId`, `agentPreset`, `requestId?` | 在首次对话前修改当前会话模式,保持 session ID |
961
- | `defaults` | — | 读取默认 agent 预设 + 默认权限 |
979
+ | `defaults` | — | 读取默认 Agent 预设、默认权限及动态可选默认权限(`permissionDefaultOptions`) |
962
980
  | `set-default` | `target`(agent-preset\|permission), `value` | 修改默认预设/默认权限(全局) |
963
981
 
964
982
  ```json
965
983
  { "type": "defaults" }
966
- → { "kind": "defaults", "agentPresetDefault": "standard", "permissionDefault": "ask" }
984
+ → { "kind": "defaults", "agentPresetDefault": "standard", "permissionDefault": "ask", "permissionDefaultOptions": [{ "value": "ask", "name": "Ask" }], "modeSelectionEnabled": true }
967
985
 
968
986
  { "type": "set-default", "target": "agent-preset", "value": "minimal" }
969
987
  → { "kind": "set-default", "target": "agent-preset", "value": "minimal", "applied": true }
970
988
  ```
971
989
 
990
+ DSH 0.1.7-rc.2 的 Agent preset 名册不再包含 `modeSelectionEnabled`。网关在字段缺失时仍允许移动端选择和保存默认模式;旧版 Host 明确返回 `false` 时沿用其禁用语义。这个移动端字段不代表桌面端「代码工作工具」的本地开关状态。
991
+
972
992
  ### 当前会话模式选择
973
993
 
974
994
  `hello.capabilities` 包含 `session-agent-preset` 时,可以进入已创建的空白会话后选模式。
@@ -1015,7 +1035,7 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1015
1035
  ## 11. 分支(fork)
1016
1036
 
1017
1037
  ```json
1018
- { "type": "fork", "sessionId": "session-abc", "atSeq": 42, "historyFormatVersion": 3 }
1038
+ { "type": "fork", "sessionId": "session-abc", "atSeq": 42, "historyFormatVersion": 4 }
1019
1039
  → { "kind": "fork", "sessionId": "session-分支新会话" }
1020
1040
  ```
1021
1041
  - `atSeq`:从该消息所在的**完整一轮**分叉(省略 = 最近完成的 turn);进行中的 turn 分叉会报 `fork-unavailable`
@@ -1048,6 +1068,7 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1048
1068
  | `session-archives` | Host 的完整 Session 归档集合在连接初始化或 WebUI/App 归档后变化 |
1049
1069
  | `session-title-changed` | 任意客户端写入新的持久化 Session 名称 |
1050
1070
  | `tasks-updated` / `goal-updated` | 当前会话的任务列表或目标 projection 发生变化 |
1071
+ | `schedule-changed` | Host 定时任务目录已变化;无载荷,重新请求 `schedule-catalog` 或当前 Session 的 `schedule-list` |
1051
1072
  | `question-requested` / `question-resolved` | Human-in-the-loop 问题请求与最终状态 |
1052
1073
  | `approval-requested` / `approval-resolved` | Human-in-the-loop 操作审批请求与最终状态 |
1053
1074
  | `pong` / `subscribed` / `sent` | 对应请求的回复 |
@@ -1076,7 +1097,7 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1076
1097
  3. `{"type":"subscribe","sessionId":"session-abc","assistantStream":true}`,等待 `session-snapshot`
1077
1098
  4. `{"type":"message","sessionId":"session-abc","text":"帮我查一下deepseek"}` → `sent`
1078
1099
  5. 盯着 Messages 面板:独立 `assistant-stream` 展示 token,`event` 流提供已提交的消息和工具事件
1079
- 6. `{"type":"history","sessionId":"session-abc","view":"conversation","maxMessages":60}` → 最近历史(自动分页用 `beforeSeq: nextBeforeSeq, historyFormatVersion: 3`)
1100
+ 6. `{"type":"history","sessionId":"session-abc","view":"conversation","maxMessages":60}` → 最近历史(自动分页用 `beforeSeq: nextBeforeSeq, historyFormatVersion: 4`)
1080
1101
  7. `{"type":"session-stats","sessionId":"session-abc"}` → 统计条数据
1081
1102
  8. 完事 `{"type":"unsubscribe"}` 或 Disconnect
1082
1103
 
@@ -1101,6 +1122,7 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1101
1122
 
1102
1123
  | 版本 | 新增 |
1103
1124
  |---|---|
1125
+ | v0.8.1(当前源码) | PC 端 Cloudflare Quick / 命名 Tunnel;插件私有 `cloudflared` 缓存及校验;配对入口选择指引;网关重启与 DSH Web 停止工具 |
1104
1126
  | v0.7.2 | 独立对话/控制连接;空 Session 创建;停止生成与稍后继续;排队消息同步及编辑/删除/Steer;App 归档/重命名 Session;WebUI 归档集合和名称变化实时同步到 App |
1105
1127
  | v0.1.5 | workspace-create / directories / host |
1106
1128
  | v0.1.6 | 修复消息分发器遗漏(host/directories/workspace-create 未路由) |
@@ -1129,6 +1151,24 @@ Control/旧单连接接收全局模式状态增量,不受所订阅会话过滤
1129
1151
 
1130
1152
  ---
1131
1153
 
1154
+ ## 17. 定时任务(DSH 0.1.7-rc.2)
1155
+
1156
+ 以下请求走控制连接,网关映射到 Host `schedule` Remote namespace。Host 未启用定时任务时返回统一的 `kind: error`,`requestType` 保留原请求类型。定时任务与第 6 节的 Session `todos` 是不同的数据源。
1157
+
1158
+ | 请求 type | 参数 | 成功响应 |
1159
+ |---|---|---|
1160
+ | `schedule-catalog` | — | `kind: schedule-catalog`, `items`: 全宿主活动与已结束提醒,包含原 `sessionId`、`status` 和可选最近投递回执 |
1161
+ | `schedule-list` | `sessionId` | `kind: schedule-list`, `sessionId`, `items`: 当前会话活动提醒 |
1162
+ | `schedule-history` | `sessionId`, `id`, `limit` 1–100, `before?` | `kind: schedule-history`, `records`(从新到旧)、`retention`、`nextBefore?`;任务或游标未找到时在成功帧中返回 `code` |
1163
+ | `schedule-update` | `sessionId`, `id`, 完整的 `expected`, `title?`, `prompt?`, `change?`, `requestId?` | `kind: schedule-update`, `updated`, `record`;并发冲突、已结束或未找到时返回 `updated:false` 与 `code` |
1164
+ | `schedule-delete` | `sessionId`, `id`, `requestId?` | `kind: schedule-delete`, `deleted`;未找到时返回 `deleted:false` 与 `code` |
1165
+
1166
+ `expected` 必须是编辑前从 `schedule-list` 或 `schedule-catalog` 取得的完整任务记录,`id` 必须匹配;不要只传 id 或重建部分字段。网关会去掉目录行额外的 `sessionId`、`status`、`lastDelivery` 元数据后调用 Host。`change` 使用 Host 的 `at`、`every`、`daily`、`weekly`、`cron` 判别结构。更新与删除只对原会话绑定生效;`requestId` 若提供会原样回显。删除会同时移除该任务的投递记录,不撤回已入队的消息。
1167
+
1168
+ rc.2 没有 `schedule/create` Remote 方法。新提醒由 Agent 的 `schedule_create` 工具创建;到期提醒作为 `source.kind: schedule` 的消息进入原 Session。Host 的无载荷 `schedule/changed` 被投影为移动端 `schedule-changed`;实现定时任务管理页面的客户端应在收到通知、重连或完成更新/删除后重新查询目录。当前移动端只接入协议请求、解码与变更帧识别,尚未提供管理页面。`hello.capabilities` 中的 `schedule-management` 表示网关支持这些请求,实际使用仍取决于 Host 是否启用定时任务。
1169
+
1170
+ ---
1171
+
1132
1172
  *协议与插件源码同源维护:`dsh-plugin-mobile-gateway/lib/index.mjs` 顶部注释即协议摘要。*
1133
1173
 
1134
1174
  ### 提前创建空会话
package/README.md CHANGED
@@ -6,21 +6,23 @@
6
6
 
7
7
  DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排队消息同步及编辑/删除/Steer、Session 归档和重命名的双向同步、停止当前生成并稍后继续、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
8
8
 
9
- > 当前源码以 **DSH 0.1.5-rc.2** 为唯一适配基线,使用 Session format 3;不再兼容更早的 Host 版本。
9
+ > 当前源码以 **DSH 0.1.7-rc.2** 为适配基线,使用 Session format 4;旧版历史游标需重新取得基线。
10
10
  >
11
11
  > 实时流已改为独立 `assistant-stream` 帧,移动端需要按 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md) 更新订阅、缓存与分页处理。当前修改尚未发布。
12
12
  >
13
+ > v0.8.1(当前源码):PC 端可在面板开启 Cloudflare Quick / 命名 Tunnel,使用插件私有缓存中的 `cloudflared` 并校验文件;配对面板增加局域网与外网入口选择指引;工具菜单可重启网关或停止 DSH Web,重启时不再打开新标签页。
14
+ >
13
15
  > v0.7.3:优化移动网关运行模式下拉框的箭头间距。
14
16
  >
15
17
  > v0.7.2:新增独立对话/控制连接、空 Session 创建、停止生成与稍后继续、排队消息同步及编辑/删除/Steer,以及 Session 归档和重命名的双向同步。
16
18
 
17
19
  ## 协议与 DSH 兼容层
18
20
 
19
- 移动端连接的是本项目维护的 `dsh-mobile-v1`,不是 DSH 的内部 Remote 协议。插件内部通过独立 Host Adapter 对接 DSH 0.1.5-rc.2 的 Remote Gateway;Session、Workspace、Settings、Commands、Goals 等 namespace 和严格参数只存在于该适配层。
21
+ 移动端连接的是本项目维护的 `dsh-mobile-v1`,不是 DSH 的内部 Remote 协议。插件内部通过独立 Host Adapter 对接 DSH 0.1.7-rc.2 的 Remote Gateway;Session、Workspace、Settings、Commands、Goals、Schedule、Permission Presets 等 namespace 和严格参数只存在于该适配层。
20
22
 
21
23
  配对鉴权和 `dsh-mobile-v1` / `hello.protocol = 3` 保持不变。新版实时 token 不占用持久事件的 `seq`:客户端显式订阅 `assistantStream: true`,接收原子的 `session-snapshot` 和独立增量;普通 `event` 只携带持久事件。未接入新订阅的客户端只能收到持久消息。
22
24
 
23
- 历史响应包含 `historyFormatVersion` 和 `cursor`。客户端格式变化时应清理历史缓存、重新安装基线;携带 `beforeSeq` 的分页或携带 `atSeq` 的 fork 必须同时发送 `historyFormatVersion: 3`。
25
+ 历史响应包含 `historyFormatVersion` 和 `cursor`。客户端格式变化时应清理历史缓存、重新安装基线;携带 `beforeSeq` 的分页或携带 `atSeq` 的 fork 必须同时发送 `historyFormatVersion: 4`。
24
26
  当前协议接入与验收见 [rc.2 移动端接入说明](docs/dsh-rc2-mobile-integration.md);早期迁移记录见 [Remote Gateway 重构实施计划](docs/remote-gateway-refactor-plan.md)。
25
27
 
26
28
  - WebSocket:`/ws/mobile`
@@ -111,7 +113,7 @@ dsh web
111
113
  1. 打开 WebUI 的“移动设备”。
112
114
  2. 将“网关运行模式”设为“常驻开启”(短期配对也可选“临时开启”)。
113
115
  3. 保持“设备鉴权”开启。
114
- 4. 确认面板显示 `ws://<电脑局域网 IP>:3081/ws/mobile`。
116
+ 4. 在“配对连接方式”选择“局域网直连”,确认 WebSocket 地址为 `ws://<电脑局域网 IP>:3081/ws/mobile`。即使 Cloudflare Tunnel 已开启,也无需关闭它。选择框旁的问号会按当前网关环境解释各入口并给出场景建议,不会自动更改选择。
115
117
  5. 填写设备名称并点击“生成配对二维码”。
116
118
  6. 在 iOS 客户端打开“设备认证”,扫描二维码。
117
119
  7. WebUI 的可信设备显示“在线”后即完成。
@@ -183,9 +185,23 @@ http://127.0.0.1:<本地端口>
183
185
 
184
186
  ## Windows / macOS 家用电脑远程连接
185
187
 
186
- 家用电脑通常没有固定公网 IP,不建议配置路由器端口转发。可以使用 Tailscale 长期连接,或使用 Cloudflare Quick Tunnel 临时调试。两种方式都转发到插件专用的 `3081` 端口,不会公开 DSH WebUI。
188
+ 家用电脑通常没有固定公网 IP,不建议配置路由器端口转发。Windows / macOS 面板提供两种可选入口:无需账户或域名的 Quick Tunnel,以及使用固定域名的命名 Tunnel。插件会在首次开启时自动下载并校验 Cloudflare 官方 `cloudflared`,保存在当前用户的缓存目录;无需手动安装系统服务。
189
+
190
+ 使用前先启动 `dsh web`。电脑必须保持开机、联网且 DSH 进程运行;休眠或进程退出后外网连接会中断。
191
+
192
+ ### Cloudflare Quick Tunnel(一键临时接入)
193
+
194
+ 在「移动设备 → Cloudflare Tunnel」选择「Quick Tunnel · 无需域名」,点击「一键开启」。面板会在首次运行时下载 `cloudflared`,连接后显示随机的 `wss://<名称>.trycloudflare.com/ws/mobile`,并自动将其填为配对地址。需要在局域网内直连时,将下方“配对连接方式”改为“局域网直连”;Tunnel 可以继续开启。生成二维码,用手机 App 扫码即可。关闭开关会停止隧道;DSH 再次启动时会恢复已开启的 Quick 模式并取得**新的随机地址**。
187
195
 
188
- 使用前先启动 `dsh web`,并在“移动设备”面板开启“允许移动设备连接”和“设备鉴权”。
196
+ Quick Tunnel 无需 Cloudflare 账户、Token 或自己的公网域名,但地址在 DSH 重启或隧道重连后可能变化。手机 App 只信任已确认的地址,因此地址变化后需在面板重新生成二维码并扫码更新配对;**仅凭 PC 在线不能保证旧地址继续可用**。Cloudflare 将 Quick Tunnel 定位于测试和开发,当前有并发请求上限且不提供可用性保证。参见 [Cloudflare Quick Tunnel 文档](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)。
197
+
198
+ ### Cloudflare 命名 Tunnel(固定地址)
199
+
200
+ 1. 在 [Cloudflare 控制台创建远程管理的 Tunnel](https://developers.cloudflare.com/tunnel/get-started/),添加一个公开域名(例如 `gateway.example.com`),将该路由的 **Service URL** 设为 `http://127.0.0.1:3082`。无需安装 Cloudflare 系统服务,也无需开放路由器入站端口。
201
+ 2. 从 Tunnel 的「Add a replica」安装命令中复制 Token,在 DSH「移动设备 → Cloudflare Tunnel」选择「命名 Tunnel · 固定域名」,输入公开域名和 Token 后开启。插件会将网关设为常驻模式,启动本机专用入口和 `cloudflared`。面板显示「已连接 Cloudflare」后使用自动填入的 `wss://gateway.example.com/ws/mobile` 生成配对二维码。
202
+ 3. 以后启动 `dsh web` 会自动恢复已开启的 Tunnel;在面板点击「关闭 Cloudflare Tunnel」会停止插件管理的 `cloudflared` 并关闭本机专用入口。若手动关闭网关,Tunnel 会暂停,重新开启网关后恢复。
203
+
204
+ 两种 Cloudflare 入口都只接受 `/ws/mobile` WebSocket,其他 HTTP 路径(包括 DSH WebUI 和 `/mgw` 管理接口)返回 404;设备凭证始终必需,即使本机 Debug 鉴权被关闭也一样。命名 Tunnel 的 Token 保存在 `<deviceFile>.cloudflare.json`(默认 `~/.dsh/mobile-gateway-devices.json.cloudflare.json`),权限为 `0600`,不会返回给浏览器。默认使用 DSH 私有的 `cloudflared-bin` 缓存,不读取系统或其他 App 的 `cloudflared`;每次启动 Tunnel 都校验缓存文件,只有缓存缺失或损坏时才下载固定版本并核对 Cloudflare 官方发布资产的 SHA-256。仅在显式配置 `cloudflaredPath` 时使用外部程序。使用自定义 `cloudflarePort` 时,命名 Tunnel 路由的 Service URL 端口也要相应修改。
189
205
 
190
206
  ### Tailscale(推荐长期使用)
191
207
 
@@ -201,20 +217,6 @@ tailscale serve --bg 3081
201
217
 
202
218
  Tailscale Serve 只允许同一 Tailnet 中符合访问规则的设备连接,并自动提供 HTTPS。可用 `tailscale serve reset` 停止转发。参见 [Tailscale Serve 文档](https://tailscale.com/docs/reference/tailscale-cli/serve)。
203
219
 
204
- ### Cloudflare Quick Tunnel(仅临时调试)
205
-
206
- 1. 安装 [cloudflared](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/)。
207
- 2. 在 Windows PowerShell 或 macOS 终端执行:
208
-
209
- ```bash
210
- cloudflared tunnel --url http://127.0.0.1:3081
211
- ```
212
-
213
- 3. 命令行会显示随机的 `https://<随机名称>.trycloudflare.com` 地址。
214
- 4. 将地址改为 `wss://<随机名称>.trycloudflare.com/ws/mobile`,填入 WebUI 的“WebSocket 地址”,再生成二维码配对。
215
-
216
- 保持该命令运行;停止命令后隧道立即失效。Quick Tunnel 的地址每次可能变化,且没有可用性保证,不适合正式或长期使用。公网调试时必须保持“设备鉴权”开启。参见 [Cloudflare Quick Tunnel 文档](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/)。
217
-
218
220
  ## 网关配置方式总览
219
221
 
220
222
  | 使用场景 | 推荐入口 | iOS WebSocket 地址 | 需要的额外配置 | 端口与鉴权 |
@@ -222,7 +224,9 @@ cloudflared tunnel --url http://127.0.0.1:3081
222
224
  | 同一局域网 | 插件局域网入口 | `ws://<电脑局域网 IP>:3081/ws/mobile` | 无需 Helper 或 Nginx;电脑与 iPhone 位于可互访的局域网 | 仅对私有网络放行 TCP `3081`;保持鉴权开启 |
223
225
  | 本机 iOS 模拟器 | DSH WebUI 本地入口 | `ws://127.0.0.1:<DSH WebUI 端口>/ws/mobile` | 无需 Helper、Nginx 或独立的 `3081` 端口 | 不开放任何外部端口;仅 Debug 时可关闭鉴权 |
224
226
  | Linux 公网服务器 | 插件 Helper + Nginx + TLS | `wss://<服务器公网 IPv4>/ws/mobile` | 执行 `init`,再从 WebUI 填写公网 IPv4 | 云安全组放行 TCP `80/443`;不要公开 DSH 端口和 `3081`;必须鉴权 |
225
- | 家用 Windows / macOS | Tailscale Serve;临时调试可用 Quick Tunnel | `wss://<Tailscale 域名>/ws/mobile` 或 `wss://<随机名称>.trycloudflare.com/ws/mobile` | 隧道转发到 `127.0.0.1:3081`,将生成的地址填入 WebUI | 无需路由器端口转发;保持鉴权开启 |
227
+ | 家用 Windows / macOS | Cloudflare 命名 Tunnel | `wss://<公开域名>/ws/mobile` | 在 Cloudflare 建立公开域名到 `127.0.0.1:3082` 的路由;面板填写域名与 Token 并开启 | 无需路由器端口转发;独立入口强制设备鉴权 |
228
+ | 家用 Windows / macOS | Cloudflare Quick Tunnel | `wss://<随机名称>.trycloudflare.com/ws/mobile` | 面板一键开启;重启或重连后地址变化需重新扫码 | 无需域名、账户或端口转发;独立入口强制设备鉴权 |
229
+ | 家用 Windows / macOS | Tailscale Serve | `wss://<Tailscale 域名>/ws/mobile` | 手动转发到 `127.0.0.1:3081`,将生成的地址填入 WebUI | 无需路由器端口转发;保持鉴权开启 |
226
230
 
227
231
  ## Linux 服务器公网入口管理
228
232
 
@@ -269,6 +273,10 @@ sudo env "PATH=$PATH" npx --yes dsh-plugin-mobile-gateway@latest remove-helper
269
273
  | iOS 收到 `401` | 在 WebUI 重新生成二维码并配对 |
270
274
  | Linux 服务器公网连接超时 | 检查云安全组、服务器防火墙和 TCP `80/443` |
271
275
  | Linux 服务器公网地址没有显示 | 在“移动设备 → 公网接入”填写公网 IPv4 并点击更新 |
276
+ | Cloudflare 下载失败 | 检查 PC 到 GitHub Releases 的连接;也可自行安装 cloudflared,并把 `cloudflaredPath` 设为可执行文件的绝对路径 |
277
+ | Quick Tunnel 重启后 App 无法连接 | Quick 地址可能已变化,在面板重新生成二维码并让 App 扫码更新配对;需要固定地址时改用命名 Tunnel |
278
+ | Quick Tunnel 未能启动且 PC 已有 Cloudflare 配置 | Cloudflare 当前不支持 Quick Tunnel 与 `~/.cloudflared/config.yaml` 共用该配置目录;检查面板错误并按 [官方说明](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) 处理 |
279
+ | Cloudflare 已连接但 App 无法访问 | 确认公开域名路由的 Service URL 是面板显示的 `http://127.0.0.1:<端口>`,并检查 DNS 与设备配对凭证 |
272
280
  | 需要查看服务端日志 | 执行 `tail -f /tmp/mobile-gateway.log` |
273
281
 
274
282
  ## 源码开发
@@ -278,4 +286,4 @@ dsh plugin --profile web add file:/absolute/path/to/dsh-plugin-mobile-gateway
278
286
  npm test
279
287
  ```
280
288
 
281
- 源码修改后需要重新安装插件并重启 `dsh web`。
289
+ 面板右上角“工具”菜单提供“重启当前网关”和“停止 DSH Web”。重启只在通过 `dsh web` 启动时可用,会结束当前 Web 进程并沿用原启动参数重新运行;重启时自动加入 `--no-open`,不会再打开新的浏览器标签页,当前标签页会等待恢复并刷新。重启后的进程在后台运行,不再由原 Terminal 前台任务管理;需要关闭时可在同一菜单点击“停止 DSH Web”,或在 Terminal 找到监听 Web 端口的 DSH 进程并发送 `SIGTERM`。重启期间连接会短暂中断,Quick Tunnel 的外网地址可能随之改变。源码修改后需重新安装插件并重启 `dsh web`。
package/cordis.patch.yml CHANGED
@@ -27,6 +27,10 @@
27
27
  lanEnabled: true
28
28
  lanHost: 0.0.0.0
29
29
  lanPort: 3081
30
+ # PC only: the UI can enable Quick or named Cloudflare Tunnel.
31
+ # Both use a separate authenticated loopback-only mobile endpoint.
32
+ # cloudflarePort: 3082
33
+ # cloudflaredPath: /path/to/cloudflared
30
34
  # Written automatically by `sudo npx dsh-plugin-mobile-gateway setup`.
31
35
  publicUrlFile: /etc/dsh-mobile-gateway/public-url
32
36
 
@@ -0,0 +1,52 @@
1
+ # DSH 0.1.7-rc.1 网关与移动应用兼容性审计
2
+
3
+ > 更新(2026-09-24):下文“当前网关”的结论记录的是适配前基线。现已在网关工作树完成 Session V4、`tool/result`、动态权限目录和默认预设配置适配;`dsh-mobile` 已完成 V4 历史解析及 Android/iOS 动态权限选项。网关完整测试、共享层 iOS 模拟器测试、Android 单元测试和 iOS 模拟器构建通过。尚未使用真实 rc.1 会话验证持久历史迁移、SSH 文件访问和运行中归档。新增能力表仍是后续功能规划。
4
+
5
+ 核对日期:2026-09-24。网关基线:`3c2ac52`、npm 版本 `0.7.7`。目标:[官方 v0.1.7-rc.1 发布说明](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.7-rc.1),发布提交 `46a7f68`。本次为静态审计和一个最小复现;没有升级本机 DSH、迁移会话或修改生产代码。相邻的 `dsh-mobile` 应用仓库有未提交改动,本次只读取了其共享层和 Android 代码。
6
+
7
+ ## 结论
8
+
9
+ **当前网关不能支持 v0.1.7-rc.1 的完整会话协议。** 官方 Session 格式是 V4;网关 `lib/dsh-host-adapter.mjs` 固定要求 V3。用 V4 `session/follow` 快照调用生产 `readSessionSnapshot()`,实际抛出 `unsupported-session-format`。因此历史、订阅快照、模型查询和会话预设查询等以快照为前置的功能确定失败。普通 `session/list` 等不读快照的调用可能仍可用,但不能据此判断消息收发全链路可用。
10
+
11
+ V4 的 `tool/result` 消息改为扁平工具角色结构。网关的实时事件映射及应用共享层的原始历史解码仍按 V3 嵌套包装读取,修好版本门槛后还会丢工具结果正文和部分失败状态。之前 0.1.6 审计指出的权限目录问题也仍在。其余 Remote API 与配置语义需要在精确 rc.1 发布包或真实宿主上联调,不能只靠旧测试夹具宣布兼容。
12
+
13
+ ## 已确认的协议差异
14
+
15
+ | 优先级 | 差异与证据 | 当前影响 | 改法 |
16
+ |---|---|---|---|
17
+ | P0 | [官方 Session V4 常量](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.7-rc.1/packages/core/session/src/types.ts)为 `4`;网关 `lib/dsh-host-adapter.mjs:17,33` 固定 `3`,`lib/session-follower.mjs:22` 复用该校验 | V4 快照被拒;`history`、实时 follow,以及读取快照的模型、预设状态失败 | 以实际快照 `header.version` 返回格式版本,明确只接收经过验证的版本;同步更新历史/fork 游标校验和 `hello.historyFormatVersion`,让 App 清除旧缓存并重建基线。不能只把常量改成 `4` |
18
+ | P0 | [V3→V4 迁移规则](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.1.7-rc.1/packages/session/session-format-v3-to-v4/README.md):`tool/result` 的 `message.role` 变为 `tool`,结果块直接放在 `message.content`,`isError` 位于 `message.isError` | `lib/index.mjs` 的 `buildWireEvent()` 只遍历旧的 `block.content`,故实时 `preview` 为空;失败判定只看旧包装或 `data.error`。应用 `GatewayDtos.kt` 的原始事件解析同样依赖旧包装 | 网关实时映射同时识别 V4 扁平结果及必要的旧数据;应用原始历史解码支持 V4 `message.content`、`message.isError`,保留 `data.error.code/reason` 供失败展示 |
19
+ | P0 | 旧 [0.1.6 审计](dsh-0.1.6-alpha.1-compatibility-audit.md)已确认候选权限从 `projections.values.permissions.options` 移到 `permissionPresets/catalog`;当前 `lib/index.mjs:1223` 仍要求旧字段 | `command-options(permission)` 与 `command-select(permission)` 失败;旧 `permission-options` 响应不含完整动态候选 | 经 Host Adapter 查询独立目录,与会话 `currentValue` 合并;动态 Auto 候选需使用实时目录,不能写死 |
20
+ | P1 | V3→V4 迁移可能插入中断的 `turn/end` 和子代理目录记录,随后重映射 `seq` 引用;上游负责迁移 | 手机保留 V3 `beforeSeq`、`atSeq`、事件缓存可能指向 V4 的另一位置 | 网关发送真实版本;App 已有格式变更重置机制,需用 V3 缓存升级 V4 的分页、fork 和重连场景验证 |
21
+ | P1 | 官方新增归档运行中任务的确认与 `stopActivity` 语义;网关目前只传 `{sessionId}` | 归档运行中会话可能返回 `workspace/session-active`;App 目前只有直接归档入口 | 透出受影响活动清单和明确的二次确认,再按新版参数请求停止;保留错误详情,不自动强制结束工作 |
22
+ | P1 | `lib/dsh-host-adapter.mjs:16` 写死 `DSH_VERSION='0.1.5-rc.2'`,`lib/index.mjs` 将其放进 `hello` | rc.1 宿主会被误报,诊断和能力判断不可靠 | 从宿主可验证元数据获取实际版本,或把该字段改为明确的网关适配目标,避免声称是运行版本 |
23
+
24
+ 版本含义需分清:`dsh-mobile-v1` 是 WebSocket 子协议,`hello.protocol=3` 是网关协议,`Session header.version=4` 是上游持久日志格式;升级 Session 格式不必机械地改变前两者。
25
+
26
+ ## 需在 rc.1 宿主复核的范围
27
+
28
+ - `session/create`、`prompt`、`follow/page/control`、`commands/execute`、`settings/describe/update`、`agentPresets/list/select`、`workspace/follow/archiveSession` 的精确参数和返回形状。发布说明明确客户端 Session 多实例及 slot 改变,但本插件使用 Host Remote Gateway;不能直接认定客户端 slot 变化作用于这里,也不能未经发布包检查就认定无影响。
29
+ - 设置已改为当前 Profile 的插件配置,Agent 预设改由插件组合包声明。网关的 `defaults` 与 `set-default` 假设旧 `agent-presets`、`permission` namespace 和字段;需要核实生效默认值、可修改字段和实时更新结果。此前 `modeSelectionEnabled=false` 时保存值不等于实际默认值的问题仍需处理。
30
+ - 新版归档、置顶、恢复的 Workspace Remote 能力,以及列表和 `workspace/follow` 的增量形状。当前只转发 `archivedSessionIds`,没有恢复或置顶入口。
31
+ - 真实升级场景:V3 持久会话迁移成 V4 后的 seq、工具结果、失败尝试、子代理记录、活动流快照、历史分页、fork 和应用缓存重建。当前 `npm test` 的 Host 假数据仍以 V3 为主,不能证明 rc.1 兼容。
32
+ - SSH 工作区的文件读取。当前 `lib/index.mjs` 用本机 `realpath/readdir` 和文件句柄实现 `file-list`、下载,不支持新版 SSH 文件提供方;需要改走 Host 文件能力或在远程工作区禁用相关入口。
33
+
34
+ ## 可添加到网关与应用的功能
35
+
36
+ | 顺序 | 能力 | 网关工作 | 移动应用工作 |
37
+ |---|---|---|---|
38
+ | 1 | 归档管理:列表、筛选、恢复、置顶 | 接入 Workspace 归档/恢复/置顶 Remote 方法、状态流及运行中归档确认 | 增加归档页、置顶排序、恢复操作和受影响任务确认 |
39
+ | 2 | 后台任务与工作流 | 接入 Job Controller 的任务列表、实时输出、取消/唤醒状态 | 增加任务面板和完成通知;与现有会话重连状态对齐 |
40
+ | 3 | 文件改动审阅 | 提供经 Session 授权的文件改动清单与 diff,按需分页或分块传输 | 增加文件卡片、逐行/并排 diff,支持从聊天定位;先做文本 diff |
41
+ | 4 | 文件与文档预览 | 改用 Host `readBytes`/二进制流能力,避免本机 `fs` 对 SSH 路径的错误映射;继续做路径授权和流量限制 | 在现有文件浏览/下载基础上增加图片、PDF、Office、CSV/TSV 预览及缩放 |
42
+ | 5 | Subagent/Team 可观测性 | 透出成员、任务和子会话标识及更新流,保持看板只读 | 展示成员状态、切换子会话、跳转任务,不提供直接篡改 Agent 看板的 UI |
43
+ | 6 | 终端与浏览器侧栏 | 若要远程交互,新增单独鉴权的终端/浏览器会话接口、显式授权与生命周期管理 | 多标签终端、Shell 选择、URL 打开和会话恢复;属于高权限功能,应单独设计 |
44
+ | 7 | 插件管理与 MCP 资源 | 接入插件安装/启停配置及 MCP 资源列表/读取,分离只读与写操作 | 可先做资源浏览,再做插件管理;展示兼容性检查和安装任务状态 |
45
+
46
+ Headless `--json`、Messages/Files API、MCP SDK v2、PTC 命名/隔离、Browser/Computer Use、Auto review 等多数变化发生在宿主内部。网关无需复制其底层协议;若要让手机操作或展示这些功能,应新增明确的 Remote 映射和移动端能力声明。对涉及终端、插件安装、浏览器和 Computer Use 的入口,沿用现有设备认证还不够,需要单独核定授权边界。
47
+
48
+ ## 验证记录
49
+
50
+ 最小复现输入 `readSessionSnapshot({type:'snapshot', header:{version:4,id:'s1'}, cursor:0, records:[], projections:{}}, 's1')`,实际输出:`unsupported-session-format: mobile-gateway requires DSH 0.1.5-rc.2 Session format 3`。
51
+
52
+ 本次没有可连接的 rc.1 宿主;本机 `git ls-remote` 因网络代理不可达而失败。因此本文将源码可确定的阻断与待联调项目分开,不把旧单元测试通过等同于新版兼容。
@@ -57,12 +57,13 @@
57
57
 
58
58
  ## 3. 地址语义与信任规则
59
59
 
60
- `publicUrl` 是本次配对的首选地址,兼容旧 App。`endpoints` 的来源顺序为:首选地址 → 本次本机配对请求指定的地址 → 插件配置地址 → 已配置公网地址 → 已启动局域网监听的地址。管理状态没有本次配对地址,只返回配置与监听产生的候选地址。
60
+ `publicUrl` 是本次配对的首选地址,兼容旧 App。`endpoints` 的来源顺序为:首选地址 → 本次本机配对请求指定的地址 → 插件配置地址 → 已配置公网地址 → 已取得的 Cloudflare Tunnel 地址 → 已启动局域网监听的地址。管理状态没有本次配对地址,只返回配置与监听产生的候选地址。
61
61
 
62
62
  - `http` / `https` 输入由插件规范化为 `ws` / `wss`。不允许用户名密码、查询参数、fragment 或 `0.0.0.0` / `::` 监听地址;公网明文 WS 被拒绝。
63
63
  - 每个输入列表最多 16 项,每项最多 2048 字符;合并后超过 16 个也会拒绝。候选地址可能包含手机不可达的 loopback 地址,App 应过滤或提示,不可据此判断网关离线。
64
64
  - 地址顺序不保证网络可达或代表实时延迟;App 可以优先尝试最近成功且已确认可信的地址。
65
65
  - 只有来自用户认可的配对二维码/手动导入,或用户单独确认的地址,才可使用凭证。`gatewayId` 是关联标识,不是证书或密码学身份凭据。
66
+ - Quick Tunnel 每次启动或重连可能取得新的 `trycloudflare.com` 地址;旧地址不能自动推导新地址。App 应让用户重新扫码更新同一 `gatewayId` 的已确认地址,不能向未确认的随机域名自动发送旧 Token。
66
67
  - `hello` 在鉴权之后返回,不能依赖“先发送 token、再比较 ID”保护 token 不被恶意端点窃取;TLS 校验和发送前的地址信任判断必须先完成。局域网明文 WS 仅用于用户信任的网络。
67
68
  - 地址被重定向时不得把 token 自动转发到其他来源。
68
69
  - 配对码是单次使用。候选地址上的配对请求必须串行;若发生网络断开且无法确定配对是否已被消费,应重新生成二维码,不能对多个地址并行消耗同一码。