dsh-plugin-mobile-gateway 0.7.1 → 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 +169 -4
- package/README.md +36 -3
- package/cordis.patch.yml +8 -0
- package/docs/multi-gateway-app-integration.md +124 -0
- package/docs/multi-gateway-phase1-acceptance.md +179 -0
- package/docs/multi-gateway-todo.md +137 -0
- package/docs/runtime-architecture.architecture.json +264 -0
- package/docs/runtime-architecture.html +15001 -0
- package/docs/runtime-architecture.visual-check.html +32 -0
- package/docs/runtime-architecture.visual-check.json +548 -0
- package/docs/typert-remote-gateway-feature-checklist.md +296 -0
- package/lib/client.js +30 -21
- package/lib/dsh-host-adapter.mjs +7 -0
- package/lib/gateway-state.mjs +57 -0
- package/lib/index.mjs +373 -56
- package/package.json +3 -3
package/PROTOCOL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh Mobile Gateway — WebSocket 协议参考
|
|
2
2
|
|
|
3
|
-
|
|
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)
|
|
@@ -46,7 +46,9 @@ WebUI 中有两个互相独立的开关。它们是本机管理设置,iOS 客
|
|
|
46
46
|
| 开启 | 开启(默认) | 必须使用一次性配对码或长期设备 token,否则返回 `401 Unauthorized` |
|
|
47
47
|
| 开启 | 关闭(仅 Debug) | 仅 DSH 本机监听允许无凭证连接,`hello.authenticated` 为 `false`;独立局域网监听仍返回 `401` |
|
|
48
48
|
|
|
49
|
-
-
|
|
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 进程中生效;独立局域网监听始终强制设备鉴权。
|
|
@@ -77,7 +79,7 @@ const pairingText = Buffer.from(JSON.stringify(payload), 'utf8').toString('base6
|
|
|
77
79
|
```json
|
|
78
80
|
{ "kind": "paired", "token": "<长期设备 token>",
|
|
79
81
|
"device": { "id": "...", "name": "iPhone", "createdAt": 1787111700000 } }
|
|
80
|
-
{ "kind": "hello", "protocol": 3, "capabilities": ["images", "commands", "tasks", "goals", "file-downloads"], "authenticated": true,
|
|
82
|
+
{ "kind": "hello", "protocol": 3, "capabilities": ["split-channels", "images", "commands", "tasks", "goals", "session-cancel", "queue-control", "session-archive", "session-rename", "file-downloads"], "authenticated": true,
|
|
81
83
|
"device": { "id": "...", "name": "iPhone" }, "port": 3080, "clients": 1 }
|
|
82
84
|
```
|
|
83
85
|
|
|
@@ -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`,并在过期前连接:
|
|
@@ -530,6 +552,73 @@ let image = [
|
|
|
530
552
|
→ { "kind": "sent", "sessionId": "session-abc", "mode": "queue", "command": { "kind": "success", "text": "..." } } // 斜杠命令时
|
|
531
553
|
```
|
|
532
554
|
|
|
555
|
+
### 排队消息同步与修改
|
|
556
|
+
|
|
557
|
+
`hello.capabilities` 包含 `queue-control` 时,控制连接会收到 Host 当前待处理消息。连接初始化或 Host 控制流重连后,网关发送完整快照:
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
{
|
|
561
|
+
"kind": "session-queues",
|
|
562
|
+
"queues": {
|
|
563
|
+
"session-abc": [
|
|
564
|
+
{
|
|
565
|
+
"id": "message-1",
|
|
566
|
+
"placement": "queued",
|
|
567
|
+
"rpcId": "prompt-1",
|
|
568
|
+
"message": {
|
|
569
|
+
"id": "message-1",
|
|
570
|
+
"content": [{ "type": "text", "text": "稍后处理这件事" }]
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
之后某个 Session 的队列发生变化时,网关发送该 Session 的完整替换值:
|
|
579
|
+
|
|
580
|
+
```json
|
|
581
|
+
{ "kind": "session-queue", "sessionId": "session-abc", "items": [] }
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
- `session-queues.queues` 是全量快照,客户端必须整体替换所有 Session 的本地队列;快照里消失的 Session 也要清除。
|
|
585
|
+
- `session-queue.items` 是单个 Session 的全量队列,不能当作追加事件;空数组表示已无待处理消息。
|
|
586
|
+
- `id` 是后续编辑操作使用的 `itemId`。`placement` 为 `queued`(下一回合)、`steering`(当前回合最近的下一步)或 `context`(系统上下文)。
|
|
587
|
+
- `rpcId` 是 Host 的 Prompt 提交标识,网关会原样转发且它可能不存在。当前 `sent` 回执尚未把该值返回给移动端,因此“发送中本地回显与队列项的精确关联”仍是待实现能力。
|
|
588
|
+
|
|
589
|
+
编辑排队文本:
|
|
590
|
+
|
|
591
|
+
```json
|
|
592
|
+
{ "type": "queue-update", "sessionId": "session-abc", "itemId": "message-1",
|
|
593
|
+
"action": "edit", "text": "修改后的消息" }
|
|
594
|
+
→ { "kind": "queue-item-updated", "sessionId": "session-abc", "itemId": "message-1",
|
|
595
|
+
"action": "edit", "accepted": true }
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
删除排队项或将下一回合消息立即插入当前回答:
|
|
599
|
+
|
|
600
|
+
```json
|
|
601
|
+
{ "type": "queue-update", "sessionId": "session-abc", "itemId": "message-1", "action": "remove" }
|
|
602
|
+
{ "type": "queue-update", "sessionId": "session-abc", "itemId": "message-1", "action": "steer" }
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
`queue-item-updated` 只是 Host 已提交操作的回执,不是队列最终状态。App 应始终以最新的 `session-queue` 更新界面;该推送可能在回执之前或之后到达。编辑只接受非空文本;`steer` 只适用于仍处于 `queued` 且 Session 正在运行的消息。目标已经开始处理或消失时,Host 返回 `session/queue-item-not-found`;当前回合已不能插入时返回 `session/steer-unavailable`。
|
|
606
|
+
|
|
607
|
+
### 停止当前生成与稍后继续
|
|
608
|
+
|
|
609
|
+
停止当前 Session 正在执行的 Agent 回合:
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{ "type": "session-cancel", "sessionId": "session-abc" }
|
|
613
|
+
→ { "kind": "session-cancelled", "sessionId": "session-abc", "accepted": true }
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
- `accepted: true` 表示 Host 已接受取消请求;最终停止状态仍以该 Session 后续实时事件和 `sessions.running` 为准。
|
|
617
|
+
- 取消只停止当前回合,不会删除 Session 历史,也不会清空已经进入 Host inbox 的待处理消息。
|
|
618
|
+
- 稍后继续不需要专用恢复请求。客户端等待 Session 停止后,使用相同 `sessionId` 发送普通 `message` 即可继续已有上下文。
|
|
619
|
+
- 如果 Session 不存在、未附加或属于不能由普通 Session API 控制的子 Agent,服务端返回对应 Host 错误。
|
|
620
|
+
- `session-cancel` 与 `question-cancel` 不同:前者停止整个 Agent 回合,后者只取消一次 `ask_user_question`。
|
|
621
|
+
|
|
533
622
|
---
|
|
534
623
|
|
|
535
624
|
## 5. 会话与历史查询
|
|
@@ -537,6 +626,8 @@ let image = [
|
|
|
537
626
|
| type | 参数 | 说明 |
|
|
538
627
|
|---|---|---|
|
|
539
628
|
| `sessions` | — | 会话列表(`updatedAt/running/blank/cwd/agentPreset`) |
|
|
629
|
+
| `session-archive` | `sessionId` | 将 Session 加入 Host 的完整归档集合(隐藏但不删除) |
|
|
630
|
+
| `session-rename` | `sessionId`, `title` | 写入用户指定的持久化 Session 名称 |
|
|
540
631
|
| `history` | `sessionId`, `beforeSeq?`, `maxMessages?`, `maxBytes?`, `view?` | 历史事件页(见下) |
|
|
541
632
|
| `attachment` | `sessionId`, `attachmentId` | 读取历史中属于该会话的图片字节 |
|
|
542
633
|
| `file-list` | `sessionId`, `path?`, `requestId?` | 列出会话工作目录内的一层文件与文件夹 |
|
|
@@ -549,6 +640,41 @@ let image = [
|
|
|
549
640
|
| `tasks` | `sessionId` | 当前任务列表(`todos` projection) |
|
|
550
641
|
| `goal` | `sessionId` | 当前目标及其 CAS 版本(`goal` projection) |
|
|
551
642
|
|
|
643
|
+
### Session 归档与重命名
|
|
644
|
+
|
|
645
|
+
在 App 端归档 Session:
|
|
646
|
+
|
|
647
|
+
```json
|
|
648
|
+
{ "type": "session-archive", "sessionId": "session-abc" }
|
|
649
|
+
→ { "kind": "session-archived", "sessionId": "session-abc",
|
|
650
|
+
"archivedSessionIds": ["session-abc", "session-old"] }
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
归档仅将 Session 从 Workspace 分组界面隐藏,不会删除历史。`archivedSessionIds` 始终是 Host 确认后的**完整归档集合**,客户端应使用它整体替换本地集合,而不是只追加本次 `sessionId`。
|
|
654
|
+
|
|
655
|
+
在 App 端重命名 Session:
|
|
656
|
+
|
|
657
|
+
```json
|
|
658
|
+
{ "type": "session-rename", "sessionId": "session-abc", "title": "新的会话名称" }
|
|
659
|
+
→ { "kind": "session-renamed", "sessionId": "session-abc",
|
|
660
|
+
"title": "新的会话名称", "seq": 128 }
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Host 会规范化并持久化名称;空白名称返回 `bad-request`,超过 Host 限制或规范化后无效的名称返回 Host 的 `session/title-invalid` 错误。
|
|
664
|
+
|
|
665
|
+
WebUI、App 或其他客户端造成的变化通过以下帧主动推送:
|
|
666
|
+
|
|
667
|
+
```json
|
|
668
|
+
{ "kind": "session-archives", "archivedSessionIds": ["session-abc"] }
|
|
669
|
+
{ "kind": "session-title-changed", "sessionId": "session-abc",
|
|
670
|
+
"title": "WebUI 修改后的名称", "seq": 129, "time": 1786937352,
|
|
671
|
+
"source": { "kind": "user" } }
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
- `session-archives` 在网关取得 opening baseline 后缓存;新设备连接时会收到当前完整集合,之后每次 WebUI 归档都会收到新的完整集合。
|
|
675
|
+
- `session-title-changed` 是 Session 列表级元数据通知,即使客户端正在 `subscribe` 另一个 Session 也会收到。
|
|
676
|
+
- 同一名称变化仍会作为带序号的普通 `event` 帧发给订阅该 Session 的客户端;客户端可按 `seq` 幂等处理。
|
|
677
|
+
|
|
552
678
|
### `history` 详细
|
|
553
679
|
```json
|
|
554
680
|
{ "type": "history", "sessionId": "session-abc", "maxMessages": 60, "maxBytes": 4194304, "view": "conversation" }
|
|
@@ -872,8 +998,11 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
|
|
|
872
998
|
| kind | 触发时机 |
|
|
873
999
|
|---|---|
|
|
874
1000
|
| `paired` | 首次配对成功;仅此一次返回长期设备 token |
|
|
875
|
-
| `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images","commands","tasks","goals","file-downloads"], "authenticated":true, "port":3080, "clients":1 }` |
|
|
1001
|
+
| `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
1002
|
| `event` | 任意会话的 agent 输出(见下) |
|
|
1003
|
+
| `session-queues` / `session-queue` | Host 队列完整快照 / 单个 Session 队列替换值 |
|
|
1004
|
+
| `session-archives` | Host 的完整 Session 归档集合在连接初始化或 WebUI/App 归档后变化 |
|
|
1005
|
+
| `session-title-changed` | 任意客户端写入新的持久化 Session 名称 |
|
|
877
1006
|
| `tasks-updated` / `goal-updated` | 当前会话的任务列表或目标 projection 发生变化 |
|
|
878
1007
|
| `question-requested` / `question-resolved` | Human-in-the-loop 问题请求与最终状态 |
|
|
879
1008
|
| `approval-requested` / `approval-resolved` | Human-in-the-loop 操作审批请求与最终状态 |
|
|
@@ -888,6 +1017,7 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
|
|
|
888
1017
|
- `user/message` → `{text, source, images?: ImageAttachmentRef[]}`
|
|
889
1018
|
- `assistant/chunk` → `{turn, step, chunkType: text-delta|reasoning-delta|tool-call-delta|usage|finish, text?/tool?/usage?/finish?}`
|
|
890
1019
|
- `assistant/message` → `{turn, step, text, reasoning, toolCalls[]}`
|
|
1020
|
+
- `session/title` → `{title, source?}`
|
|
891
1021
|
- `tool/call` → `{turn, step, callId, name, arguments}`
|
|
892
1022
|
- `tool/result` → `{turn, step, callId, isError, preview(≤400字符)}`
|
|
893
1023
|
- `turn/start|end` / `step/start|end` → `{turn, step, reason?}`
|
|
@@ -926,6 +1056,7 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
|
|
|
926
1056
|
|
|
927
1057
|
| 版本 | 新增 |
|
|
928
1058
|
|---|---|
|
|
1059
|
+
| v0.7.2 | 独立对话/控制连接;空 Session 创建;停止生成与稍后继续;排队消息同步及编辑/删除/Steer;App 归档/重命名 Session;WebUI 归档集合和名称变化实时同步到 App |
|
|
929
1060
|
| v0.1.5 | workspace-create / directories / host |
|
|
930
1061
|
| v0.1.6 | 修复消息分发器遗漏(host/directories/workspace-create 未路由) |
|
|
931
1062
|
| v0.1.7 | models / select-model / permission-options / permission / context-usage |
|
|
@@ -954,3 +1085,37 @@ WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos`
|
|
|
954
1085
|
---
|
|
955
1086
|
|
|
956
1087
|
*协议与插件源码同源维护:`dsh-plugin-mobile-gateway/lib/index.mjs` 顶部注释即协议摘要。*
|
|
1088
|
+
|
|
1089
|
+
### 提前创建空会话
|
|
1090
|
+
|
|
1091
|
+
`hello.capabilities` 中的 `session-create` 表示支持发送首条消息前创建会话。
|
|
1092
|
+
客户端发送 `{"type":"session-create","requestId":"unique-id","workspaceId":"w1"}`,
|
|
1093
|
+
收到 `{"kind":"session-created","requestId":"unique-id","sessionId":"..."}` 后,
|
|
1094
|
+
即可使用现有的命令目录、模型和权限接口;该操作不会调用 prompt 或启动 Agent。
|
|
1095
|
+
`workspaceId` 与 `cwd` 均可省略,同时提供时优先使用 `workspaceId`。
|
|
1096
|
+
失败返回 `kind:error`、`requestType:session-create` 和原始 `requestId`。
|
|
1097
|
+
客户端不应自动重试超时的创建请求,以免重复创建会话。
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
### Independent conversation and control connections
|
|
1101
|
+
|
|
1102
|
+
A gateway advertising `split-channels` in `hello.capabilities` accepts the optional
|
|
1103
|
+
`X-DSH-Channel` upgrade header (`control` or `conversation`). Without this header,
|
|
1104
|
+
the connection keeps the legacy behavior. Clients must wait for the control hello
|
|
1105
|
+
capability before opening a second connection. Pair only on control; reuse the
|
|
1106
|
+
returned device token and device ID for conversation, never reuse a pairing code.
|
|
1107
|
+
|
|
1108
|
+
- Conversation: `message`, `history`, `subscribe`, `unsubscribe`; receives their
|
|
1109
|
+
replies and subscribed session events. History and live events share this lane
|
|
1110
|
+
to preserve their ordering.
|
|
1111
|
+
- Control: all other requests, including file downloads, rename/archive, session
|
|
1112
|
+
cancellation, permissions, questions and approvals. Global metadata and pending
|
|
1113
|
+
interactions are delivered only to this connection.
|
|
1114
|
+
- `ping` is accepted on either connection. A request on the wrong lane receives
|
|
1115
|
+
`error` with code `wrong-channel` and `requestType`.
|
|
1116
|
+
|
|
1117
|
+
Each connection has its own WebSocket send queue. A slow conversation receiver
|
|
1118
|
+
does not put file responses behind conversation frames. This isolates socket
|
|
1119
|
+
queues; the underlying network link and Host resources remain shared. On connection
|
|
1120
|
+
failure clients reconnect and authenticate both lanes, then resubscribe and catch up
|
|
1121
|
+
history. Older gateways without the capability continue using one connection.
|
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
# dsh-plugin-mobile-gateway
|
|
6
6
|
|
|
7
|
-
DeepSeek Harness
|
|
7
|
+
DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排队消息同步及编辑/删除/Steer、Session 归档和重命名的双向同步、停止当前生成并稍后继续、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
|
|
8
8
|
|
|
9
|
-
> v0.7.
|
|
9
|
+
> v0.7.3:优化移动网关运行模式下拉框的箭头间距。
|
|
10
|
+
>
|
|
11
|
+
> v0.7.2:新增独立对话/控制连接、空 Session 创建、停止生成与稍后继续、排队消息同步及编辑/删除/Steer,以及 Session 归档和重命名的双向同步;兼容 DSH v0.1.2-rc.1 与 v0.1.3-alpha.1。
|
|
10
12
|
|
|
11
13
|
## 协议与 DSH 兼容层
|
|
12
14
|
|
|
@@ -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 发现、中心目录和网络中转不在本次交付范围。
|