dsh-plugin-mobile-gateway 0.7.0 → 0.7.2

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` 实现(v0.7.0)。
3
+ 移动端通过经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop 提问与操作审批、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现(v0.7.2)。
4
4
 
5
5
  - **本机端点**:`ws://127.0.0.1:3080/ws/mobile`(与 dsh web GUI 同端口)
6
6
  - **局域网端点**:`ws://<电脑的私有局域网 IP>:3081/ws/mobile`(插件独立监听,只提供经过鉴权的 WebSocket)
@@ -77,7 +77,7 @@ const pairingText = Buffer.from(JSON.stringify(payload), 'utf8').toString('base6
77
77
  ```json
78
78
  { "kind": "paired", "token": "<长期设备 token>",
79
79
  "device": { "id": "...", "name": "iPhone", "createdAt": 1787111700000 } }
80
- { "kind": "hello", "protocol": 3, "capabilities": ["images", "commands", "tasks", "goals", "file-downloads"], "authenticated": true,
80
+ { "kind": "hello", "protocol": 3, "capabilities": ["split-channels", "images", "commands", "tasks", "goals", "session-cancel", "queue-control", "session-archive", "session-rename", "file-downloads"], "authenticated": true,
81
81
  "device": { "id": "...", "name": "iPhone" }, "port": 3080, "clients": 1 }
82
82
  ```
83
83
 
@@ -162,11 +162,11 @@ Human-in-the-loop 分为两条独立通道:
162
162
  - **提问**:Agent 的 `ask_user_question` 工具向用户收集答案。
163
163
  - **审批**:高风险工具操作(例如沙箱升权)请求一次性允许或拒绝。
164
164
 
165
- 二者都是 API Gateway 的临时请求,不属于持久化的 `session/event`,且都必须以其原始 `rpcId` 通过专用响应帧回答,不能作为普通 `message` 发送。
165
+ 二者都是 Host waterfall 的临时请求,不属于持久化的 `session/event`,且都必须以插件为该次请求生成的 `rpcId` 通过专用响应帧回答,不能作为普通 `message` 发送。该内部实现不改变移动端帧格式。
166
166
 
167
167
  ### 3.1 提问与回答
168
168
 
169
- Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway 的 `events.mux()` 收到临时的待回答请求,并推送给移动端。
169
+ Agent 调用 DSH 的 `ask_user_question` 工具时,插件直接接入 Host 的 `user-questions/request` waterfall,并把临时请求投影给移动端。若没有可处理该 Session 的移动连接,插件调用 `next()`,由 WebUI 或后续 Host answerer 处理。
170
170
 
171
171
  #### `question-requested` — 服务端推送问题
172
172
 
@@ -191,7 +191,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
191
191
  }
192
192
  ```
193
193
 
194
- - `rpcId`:API Gateway 为这一整批问题生成的稳定 ID。回答或取消时必须原样返回,客户端不得自行生成。
194
+ - `rpcId`:移动网关为这一整批问题生成的不透明稳定 ID。回答或取消时必须原样返回,客户端不得自行生成或解析。
195
195
  - `questions`:一次工具调用中的完整问题批次;可能包含多题。
196
196
  - `id`:问题 ID,必须在对应答案中原样返回。
197
197
  - `header` / `detail`:可选展示信息。
@@ -226,7 +226,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
226
226
  }
227
227
  ```
228
228
 
229
- 提交规则由 API Gateway 严格校验:
229
+ 提交规则由移动网关在进入 Host waterfall 前严格校验:
230
230
 
231
231
  - 必须一次提交这一批中的全部问题,`answers` 数量、顺序和 `id` 必须与 `questions` 一致。
232
232
  - `selected` 中的值必须与原始 `options[].label` 完全一致,且不能重复。
@@ -264,13 +264,13 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
264
264
  "outcome":"answered" }
265
265
  ```
266
266
 
267
- `outcome` 为 `answered` 或 `cancelled`。WebUI、iOS 或其他客户端中的第一个合法响应获胜;所有移动连接都会收到最终状态并应关闭对应选择界面。移动端断线重连后,API Gateway 会重放仍待回答的问题;DSH 进程重启则会取消这些仅存在于运行时的问题。
267
+ `outcome` 为 `answered` 或 `cancelled`。存在匹配 Session 的移动连接时,移动网关优先认领请求,多台移动设备中的第一个合法响应获胜;若没有匹配连接或连接全部断开,则通过 `next()` 回退给 WebUI/后续 Host answerer。所有移动连接都会收到由移动端完成的最终状态并应关闭对应选择界面。重连时网关会重放仍由它持有的问题;DSH 进程重启则会取消这些仅存在于运行时的问题。
268
268
 
269
269
  ---
270
270
 
271
271
  ### 3.2 操作审批
272
272
 
273
- 当 DSH 的工具管线要求人工授权时,插件会从 API Gateway 收到一次 `approval/requested`。这正是 Web UI 中“等待审批”卡片对应的事件:`reason` 是面向用户的审批说明,`toolName` 标识请求操作的工具,`callId` 可用于与实时工具调用轨迹关联。
273
+ 当 DSH 的工具管线要求人工授权时,插件会接入一次 Host `approval/request` waterfall,并向移动端投影为 `approval-requested`。这正是 Web UI 中“等待审批”卡片对应的能力:`reason` 是面向用户的审批说明,`toolName` 标识请求操作的工具,`callId` 可用于与实时工具调用轨迹关联。
274
274
 
275
275
  #### `approval-requested` — 服务端推送待审批操作
276
276
 
@@ -288,7 +288,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
288
288
  ```
289
289
 
290
290
  - `rpcId`:本次可回答请求的稳定 RPC ID;提交决定时必须原样返回。
291
- - `approvalId`:审批审计 ID;同样必须原样返回,并用于将最终状态关联到本地审批卡片。
291
+ - `approvalId`:移动网关生成的本次审批关联 ID;同样必须原样返回,并用于将最终状态关联到本地审批卡片。
292
292
  - `toolName`:请求审批的工具名。
293
293
  - `callId` / `reason`:可选。前者可关联工具调用,后者应直接显示为待审批原因。
294
294
  - `replay: true`:表示当前仍未决定的审批在移动端连接或切换 Session 后重放。客户端应按 `rpcId` 去重。
@@ -321,7 +321,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
321
321
  "approvalId":"approval-1", "outcome":"allowed-once", "accepted":true }
322
322
  ```
323
323
 
324
- 如果 Web UI 或另一台移动设备已经先作出决定,则回执为 `accepted:false`,并附带 `reason:"not-pending"`。收到错误帧或未被接受的回执时,客户端应保留当前状态,等待最终状态或重新打开事件流。
324
+ 如果另一台移动设备已经先作出决定,或请求已经回退给 WebUI/后续 answerer,则回执为 `accepted:false`,并附带 `reason:"not-pending"`。收到错误帧或未被接受的回执时,客户端应保留当前状态,等待最终状态或重新打开事件流。
325
325
 
326
326
  #### `approval-resolved` — 服务端广播最终状态
327
327
 
@@ -330,7 +330,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
330
330
  "approvalId":"approval-1", "outcome":"allowed-once" }
331
331
  ```
332
332
 
333
- `outcome` 为 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`。所有移动连接都会收到最终状态并关闭对应审批卡片。移动端断线重连后,API Gateway 会重放仍待决定的审批;已决审批不会重放。
333
+ `outcome` 为 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`。所有移动连接都会收到由移动网关完成的最终状态并关闭对应审批卡片。移动端断线重连后,网关会重放仍由它持有的审批;已决或已回退的审批不会重放。
334
334
 
335
335
  ---
336
336
 
@@ -530,6 +530,73 @@ let image = [
530
530
  → { "kind": "sent", "sessionId": "session-abc", "mode": "queue", "command": { "kind": "success", "text": "..." } } // 斜杠命令时
531
531
  ```
532
532
 
533
+ ### 排队消息同步与修改
534
+
535
+ `hello.capabilities` 包含 `queue-control` 时,控制连接会收到 Host 当前待处理消息。连接初始化或 Host 控制流重连后,网关发送完整快照:
536
+
537
+ ```json
538
+ {
539
+ "kind": "session-queues",
540
+ "queues": {
541
+ "session-abc": [
542
+ {
543
+ "id": "message-1",
544
+ "placement": "queued",
545
+ "rpcId": "prompt-1",
546
+ "message": {
547
+ "id": "message-1",
548
+ "content": [{ "type": "text", "text": "稍后处理这件事" }]
549
+ }
550
+ }
551
+ ]
552
+ }
553
+ }
554
+ ```
555
+
556
+ 之后某个 Session 的队列发生变化时,网关发送该 Session 的完整替换值:
557
+
558
+ ```json
559
+ { "kind": "session-queue", "sessionId": "session-abc", "items": [] }
560
+ ```
561
+
562
+ - `session-queues.queues` 是全量快照,客户端必须整体替换所有 Session 的本地队列;快照里消失的 Session 也要清除。
563
+ - `session-queue.items` 是单个 Session 的全量队列,不能当作追加事件;空数组表示已无待处理消息。
564
+ - `id` 是后续编辑操作使用的 `itemId`。`placement` 为 `queued`(下一回合)、`steering`(当前回合最近的下一步)或 `context`(系统上下文)。
565
+ - `rpcId` 是 Host 的 Prompt 提交标识,网关会原样转发且它可能不存在。当前 `sent` 回执尚未把该值返回给移动端,因此“发送中本地回显与队列项的精确关联”仍是待实现能力。
566
+
567
+ 编辑排队文本:
568
+
569
+ ```json
570
+ { "type": "queue-update", "sessionId": "session-abc", "itemId": "message-1",
571
+ "action": "edit", "text": "修改后的消息" }
572
+ → { "kind": "queue-item-updated", "sessionId": "session-abc", "itemId": "message-1",
573
+ "action": "edit", "accepted": true }
574
+ ```
575
+
576
+ 删除排队项或将下一回合消息立即插入当前回答:
577
+
578
+ ```json
579
+ { "type": "queue-update", "sessionId": "session-abc", "itemId": "message-1", "action": "remove" }
580
+ { "type": "queue-update", "sessionId": "session-abc", "itemId": "message-1", "action": "steer" }
581
+ ```
582
+
583
+ `queue-item-updated` 只是 Host 已提交操作的回执,不是队列最终状态。App 应始终以最新的 `session-queue` 更新界面;该推送可能在回执之前或之后到达。编辑只接受非空文本;`steer` 只适用于仍处于 `queued` 且 Session 正在运行的消息。目标已经开始处理或消失时,Host 返回 `session/queue-item-not-found`;当前回合已不能插入时返回 `session/steer-unavailable`。
584
+
585
+ ### 停止当前生成与稍后继续
586
+
587
+ 停止当前 Session 正在执行的 Agent 回合:
588
+
589
+ ```json
590
+ { "type": "session-cancel", "sessionId": "session-abc" }
591
+ → { "kind": "session-cancelled", "sessionId": "session-abc", "accepted": true }
592
+ ```
593
+
594
+ - `accepted: true` 表示 Host 已接受取消请求;最终停止状态仍以该 Session 后续实时事件和 `sessions.running` 为准。
595
+ - 取消只停止当前回合,不会删除 Session 历史,也不会清空已经进入 Host inbox 的待处理消息。
596
+ - 稍后继续不需要专用恢复请求。客户端等待 Session 停止后,使用相同 `sessionId` 发送普通 `message` 即可继续已有上下文。
597
+ - 如果 Session 不存在、未附加或属于不能由普通 Session API 控制的子 Agent,服务端返回对应 Host 错误。
598
+ - `session-cancel` 与 `question-cancel` 不同:前者停止整个 Agent 回合,后者只取消一次 `ask_user_question`。
599
+
533
600
  ---
534
601
 
535
602
  ## 5. 会话与历史查询
@@ -537,6 +604,8 @@ let image = [
537
604
  | type | 参数 | 说明 |
538
605
  |---|---|---|
539
606
  | `sessions` | — | 会话列表(`updatedAt/running/blank/cwd/agentPreset`) |
607
+ | `session-archive` | `sessionId` | 将 Session 加入 Host 的完整归档集合(隐藏但不删除) |
608
+ | `session-rename` | `sessionId`, `title` | 写入用户指定的持久化 Session 名称 |
540
609
  | `history` | `sessionId`, `beforeSeq?`, `maxMessages?`, `maxBytes?`, `view?` | 历史事件页(见下) |
541
610
  | `attachment` | `sessionId`, `attachmentId` | 读取历史中属于该会话的图片字节 |
542
611
  | `file-list` | `sessionId`, `path?`, `requestId?` | 列出会话工作目录内的一层文件与文件夹 |
@@ -549,6 +618,41 @@ let image = [
549
618
  | `tasks` | `sessionId` | 当前任务列表(`todos` projection) |
550
619
  | `goal` | `sessionId` | 当前目标及其 CAS 版本(`goal` projection) |
551
620
 
621
+ ### Session 归档与重命名
622
+
623
+ 在 App 端归档 Session:
624
+
625
+ ```json
626
+ { "type": "session-archive", "sessionId": "session-abc" }
627
+ → { "kind": "session-archived", "sessionId": "session-abc",
628
+ "archivedSessionIds": ["session-abc", "session-old"] }
629
+ ```
630
+
631
+ 归档仅将 Session 从 Workspace 分组界面隐藏,不会删除历史。`archivedSessionIds` 始终是 Host 确认后的**完整归档集合**,客户端应使用它整体替换本地集合,而不是只追加本次 `sessionId`。
632
+
633
+ 在 App 端重命名 Session:
634
+
635
+ ```json
636
+ { "type": "session-rename", "sessionId": "session-abc", "title": "新的会话名称" }
637
+ → { "kind": "session-renamed", "sessionId": "session-abc",
638
+ "title": "新的会话名称", "seq": 128 }
639
+ ```
640
+
641
+ Host 会规范化并持久化名称;空白名称返回 `bad-request`,超过 Host 限制或规范化后无效的名称返回 Host 的 `session/title-invalid` 错误。
642
+
643
+ WebUI、App 或其他客户端造成的变化通过以下帧主动推送:
644
+
645
+ ```json
646
+ { "kind": "session-archives", "archivedSessionIds": ["session-abc"] }
647
+ { "kind": "session-title-changed", "sessionId": "session-abc",
648
+ "title": "WebUI 修改后的名称", "seq": 129, "time": 1786937352,
649
+ "source": { "kind": "user" } }
650
+ ```
651
+
652
+ - `session-archives` 在网关取得 opening baseline 后缓存;新设备连接时会收到当前完整集合,之后每次 WebUI 归档都会收到新的完整集合。
653
+ - `session-title-changed` 是 Session 列表级元数据通知,即使客户端正在 `subscribe` 另一个 Session 也会收到。
654
+ - 同一名称变化仍会作为带序号的普通 `event` 帧发给订阅该 Session 的客户端;客户端可按 `seq` 幂等处理。
655
+
552
656
  ### `history` 详细
553
657
  ```json
554
658
  { "type": "history", "sessionId": "session-abc", "maxMessages": 60, "maxBytes": 4194304, "view": "conversation" }
@@ -872,8 +976,11 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
872
976
  | kind | 触发时机 |
873
977
  |---|---|
874
978
  | `paired` | 首次配对成功;仅此一次返回长期设备 token |
875
- | `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images","commands","tasks","goals","file-downloads"], "authenticated":true, "port":3080, "clients":1 }` |
979
+ | `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["split-channels","images","commands","tasks","goals","session-cancel","queue-control","session-archive","session-rename","file-downloads"], "authenticated":true, "port":3080, "clients":1 }` |
876
980
  | `event` | 任意会话的 agent 输出(见下) |
981
+ | `session-queues` / `session-queue` | Host 队列完整快照 / 单个 Session 队列替换值 |
982
+ | `session-archives` | Host 的完整 Session 归档集合在连接初始化或 WebUI/App 归档后变化 |
983
+ | `session-title-changed` | 任意客户端写入新的持久化 Session 名称 |
877
984
  | `tasks-updated` / `goal-updated` | 当前会话的任务列表或目标 projection 发生变化 |
878
985
  | `question-requested` / `question-resolved` | Human-in-the-loop 问题请求与最终状态 |
879
986
  | `approval-requested` / `approval-resolved` | Human-in-the-loop 操作审批请求与最终状态 |
@@ -888,6 +995,7 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
888
995
  - `user/message` → `{text, source, images?: ImageAttachmentRef[]}`
889
996
  - `assistant/chunk` → `{turn, step, chunkType: text-delta|reasoning-delta|tool-call-delta|usage|finish, text?/tool?/usage?/finish?}`
890
997
  - `assistant/message` → `{turn, step, text, reasoning, toolCalls[]}`
998
+ - `session/title` → `{title, source?}`
891
999
  - `tool/call` → `{turn, step, callId, name, arguments}`
892
1000
  - `tool/result` → `{turn, step, callId, isError, preview(≤400字符)}`
893
1001
  - `turn/start|end` / `step/start|end` → `{turn, step, reason?}`
@@ -926,6 +1034,7 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
926
1034
 
927
1035
  | 版本 | 新增 |
928
1036
  |---|---|
1037
+ | v0.7.2 | 独立对话/控制连接;空 Session 创建;停止生成与稍后继续;排队消息同步及编辑/删除/Steer;App 归档/重命名 Session;WebUI 归档集合和名称变化实时同步到 App |
929
1038
  | v0.1.5 | workspace-create / directories / host |
930
1039
  | v0.1.6 | 修复消息分发器遗漏(host/directories/workspace-create 未路由) |
931
1040
  | v0.1.7 | models / select-model / permission-options / permission / context-usage |
@@ -949,7 +1058,42 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
949
1058
  | v0.6.8 | 会话工作目录受限的文件列表与分块下载:支持图片、文档、IPA、APK 等普通文件,含连接归属、路径越界防护、取消、超时和 SHA-256 完整性校验 |
950
1059
  | v0.6.9 | 服务端驱动的命令与技能目录:支持本地化 Hint、通用二级选项、专用命令执行,以及 command/compaction 生命周期事件;Host 命令不再作为用户 Prompt 发送 |
951
1060
  | v0.7.0 | 任务与 Goal 对齐:任务/Goal 基线查询、`todos`/`goal` 实时投影、Goal 改名、暂停、继续与删除 |
1061
+ | v0.7.1 | DSH v0.1.2-rc.1 兼容:内部迁移至 Remote Gateway 与 Host waterfall,移除 APIProxy 依赖;`dsh-mobile-v1` 保持不变 |
952
1062
 
953
1063
  ---
954
1064
 
955
1065
  *协议与插件源码同源维护:`dsh-plugin-mobile-gateway/lib/index.mjs` 顶部注释即协议摘要。*
1066
+
1067
+ ### 提前创建空会话
1068
+
1069
+ `hello.capabilities` 中的 `session-create` 表示支持发送首条消息前创建会话。
1070
+ 客户端发送 `{"type":"session-create","requestId":"unique-id","workspaceId":"w1"}`,
1071
+ 收到 `{"kind":"session-created","requestId":"unique-id","sessionId":"..."}` 后,
1072
+ 即可使用现有的命令目录、模型和权限接口;该操作不会调用 prompt 或启动 Agent。
1073
+ `workspaceId` 与 `cwd` 均可省略,同时提供时优先使用 `workspaceId`。
1074
+ 失败返回 `kind:error`、`requestType:session-create` 和原始 `requestId`。
1075
+ 客户端不应自动重试超时的创建请求,以免重复创建会话。
1076
+
1077
+
1078
+ ### Independent conversation and control connections
1079
+
1080
+ A gateway advertising `split-channels` in `hello.capabilities` accepts the optional
1081
+ `X-DSH-Channel` upgrade header (`control` or `conversation`). Without this header,
1082
+ the connection keeps the legacy behavior. Clients must wait for the control hello
1083
+ capability before opening a second connection. Pair only on control; reuse the
1084
+ returned device token and device ID for conversation, never reuse a pairing code.
1085
+
1086
+ - Conversation: `message`, `history`, `subscribe`, `unsubscribe`; receives their
1087
+ replies and subscribed session events. History and live events share this lane
1088
+ to preserve their ordering.
1089
+ - Control: all other requests, including file downloads, rename/archive, session
1090
+ cancellation, permissions, questions and approvals. Global metadata and pending
1091
+ interactions are delivered only to this connection.
1092
+ - `ping` is accepted on either connection. A request on the wrong lane receives
1093
+ `error` with code `wrong-channel` and `requestType`.
1094
+
1095
+ Each connection has its own WebSocket send queue. A slow conversation receiver
1096
+ does not put file responses behind conversation frames. This isolates socket
1097
+ queues; the underlying network link and Host resources remain shared. On connection
1098
+ failure clients reconnect and authenticate both lanes, then resubscribe and catch up
1099
+ history. Older gateways without the capability continue using one connection.
package/README.md CHANGED
@@ -4,9 +4,17 @@
4
4
 
5
5
  # dsh-plugin-mobile-gateway
6
6
 
7
- DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
7
+ DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排队消息同步及编辑/删除/Steer、Session 归档和重命名的双向同步、停止当前生成并稍后继续、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
8
8
 
9
- > v0.7.0:移动端现可与 WebUI 对齐展示任务列表和当前 Goal;支持 Goal 改名、暂停、继续和删除,并通过实时 projection 与 revision 校验处理多设备并发更新。
9
+ > v0.7.2:新增独立对话/控制连接、空 Session 创建、停止生成与稍后继续、排队消息同步及编辑/删除/Steer,以及 Session 归档和重命名的双向同步;兼容 DSH v0.1.2-rc.1 与 v0.1.3-alpha.1。
10
+
11
+ ## 协议与 DSH 兼容层
12
+
13
+ 移动端连接的是本项目维护的 `dsh-mobile-v1`,不是 DSH 的内部 Remote 协议。插件内部通过独立 Host Adapter 对接 DSH v0.1.2-rc.1 的 Remote Gateway;Session、Workspace、Settings、Commands、Goals 等 namespace 和严格参数只存在于该适配层。
14
+
15
+ 因此本次从旧 APIProxy 迁移到 Remote Gateway 不要求现有移动端修改:WebSocket 子协议、`hello.protocol = 3`、配对鉴权和已有请求/响应字段保持不变。以后 DSH 调整 Remote API 时,优先只更新插件适配层及契约测试;只有移动协议本身发生不可兼容变化时,才会新增并并行支持新的移动端协议版本。
16
+
17
+ 详细实施边界见 [Remote Gateway 重构实施计划](docs/remote-gateway-refactor-plan.md)。
10
18
 
11
19
  - WebSocket:`/ws/mobile`
12
20
  - 局域网:`ws://<局域网 IP>:3081/ws/mobile`
package/cordis.patch.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-plugin-mobile-gateway bundle patch.
2
2
  # Applied after dsh-base and dsh-web-app: inserts the persistent /ws/mobile
3
- # gateway row into the host plane (it consumes the host `webServer` service
4
- # and the host `session/event` feed, and publishes no service of its own).
3
+ # gateway row into the host plane (it consumes `webServer`, the Host Remote
4
+ # Gateway and the `session/event` feed, and publishes no service of its own).
5
5
  - insert:
6
6
  - id: mobile-gateway
7
7
  name: 'dsh-plugin-mobile-gateway'
@@ -379,7 +379,7 @@ DSH 用 Cordis 的依赖注入容器编排"服务行"。插件通过 `package.js
379
379
  const plugin = {
380
380
  name: 'mobile-gateway',
381
381
  Config,
382
- inject: ['webServer', 'apiProxy', 'typertGateway', 'agentDefaultModel'],
382
+ inject: ['webServer', 'typertGateway', 'agentDefaultModel'],
383
383
  apply(ctx, config) { /* ... */ },
384
384
  }
385
385
  ```
@@ -0,0 +1,52 @@
1
+ # Remote Gateway 重构实施计划
2
+
3
+ ## 目标
4
+
5
+ 在保持移动端 `dsh-mobile-v1`、配对流程和现有消息字段不变的前提下,移除对 DSH `APIProxy` 的依赖,改为只通过 Host 侧 `typertGateway` 访问 DSH v0.1.2-rc.1 Remote API。所有上游协议差异集中在插件内部适配层,后续 DSH 发生破坏性更新时不要求移动端同步升级。
6
+
7
+ ## 稳定边界
8
+
9
+ - 对移动端:继续协商 `dsh-mobile-v1`,`hello.protocol` 保持 `3`,已有请求和响应帧保持兼容。
10
+ - 对 DSH:仅 `lib/dsh-host-adapter.mjs` 知道 Remote namespace、method、严格参数名、stream opening frame 和错误形态。
11
+ - 对业务编排:`lib/index.mjs` 只调用语义化 Host Adapter,不拼装 Remote 描述符。
12
+ - 对错误:上游带 `code/message` 的错误映射为既有移动端错误帧;未知异常稳定映射为 `internal`。
13
+ - 对历史:适配层把 RC 版压缩的 `chunkrow/*` 记录还原为 v1 已支持的 `assistant/chunk` 事件。
14
+
15
+ ## 实施阶段
16
+
17
+ ### 1. Host Adapter 与普通 RPC
18
+
19
+ - 封装 `invoke()` / `stream()`,校验关键返回值。
20
+ - 迁移 Session、Workspace、Settings、Commands、Skills、Agent Presets、LLM、Goals。
21
+ - Session 历史通过 `session.follow` 获取快照游标和 projections;旧页通过 `session.page` 读取。
22
+ - Workspace 列表通过 `workspace.follow` 的 baseline 获取。
23
+ - Host 信息由插件本地能力和 Remote catalog 合成,不再依赖已删除的 `host.describe`。
24
+
25
+ ### 2. 实时与 Human-in-the-loop
26
+
27
+ - 普通实时会话事件继续使用 Host 内部 `session/event`,保持移动端事件帧不变。
28
+ - projection 更新从 `session.control` stream 转发,替代旧 `api.events.mux()` 中的投影帧。
29
+ - approval/question 改为 Cordis waterfall listener;生成插件自有不透明 `rpcId`,等待移动端回答。
30
+ - 只有存在匹配会话的已认证移动连接时才认领 waterfall;否则立即 `next()`,保留 WebUI 回退路径。
31
+ - 连接断开、请求 signal 取消或插件卸载时释放等待者并回退/取消,禁止悬挂 Promise。
32
+
33
+ ### 3. 兼容性测试与门禁
34
+
35
+ - 使用假的 `typertGateway`,按 RC 的精确 namespace/method/args 测试,不再模拟 APIProxy。
36
+ - 添加 Host Adapter 单元测试,覆盖 direct value、throw、stream baseline、历史 chunk 展开和严格参数形状。
37
+ - 保留现有 WebSocket v1 端到端用例,验证移动端请求/响应字段没有改变。
38
+ - 增加静态门禁:生产代码不得出现 `apiProxy`、旧 `{ rpcId, payload }` Remote 调用或 `api.events.mux()`。
39
+
40
+ ### 4. 配置与文档
41
+
42
+ - 删除 `apiProxy` 注入声明。
43
+ - 更新 README、PROTOCOL 和 bundle patch 注释,明确移动协议与 DSH Remote 的分层。
44
+ - 保留 `session-query-sqlite` 的按需搜索配置,但将其标记为可选能力,避免它成为插件启动前提。
45
+
46
+ ## 验收标准
47
+
48
+ 1. 插件可在 DSH v0.1.2-rc.1 组合中加载,且不请求 `apiProxy`。
49
+ 2. 当前移动端无需修改即可完成配对、会话列表、历史、发送消息、命令、模型、设置、任务、Goal、文件和交互审批。
50
+ 3. 全量测试通过;Remote 调用参数均由适配层测试锁定。
51
+ 4. 上游 namespace 或数据结构未来变化时,改动范围原则上限定于 Host Adapter 及对应契约测试。
52
+