dsh-plugin-mobile-gateway 0.6.5 → 0.6.9
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 +280 -12
- package/README.md +2 -2
- package/lib/index.mjs +934 -30
- package/package.json +2 -2
package/PROTOCOL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh Mobile Gateway — WebSocket 协议参考
|
|
2
2
|
|
|
3
|
-
移动端通过一个经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop
|
|
3
|
+
移动端通过一个经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop 提问与操作审批、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现(v0.6.9)。
|
|
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"], "authenticated": true,
|
|
80
|
+
{ "kind": "hello", "protocol": 3, "capabilities": ["images", "file-downloads"], "authenticated": true,
|
|
81
81
|
"device": { "id": "...", "name": "iPhone" }, "port": 3080, "clients": 1 }
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -138,7 +138,7 @@ func connectAuthenticated(publicURL: URL, token: String) -> URLSessionWebSocketT
|
|
|
138
138
|
| type | 参数 | 说明 |
|
|
139
139
|
|---|---|---|
|
|
140
140
|
| `ping` | — | 心跳;回复 `pong` |
|
|
141
|
-
| `subscribe` | `sessionId` | 事件流过滤:之后只收到该会话的 `event
|
|
141
|
+
| `subscribe` | `sessionId` | 事件流过滤:之后只收到该会话的 `event`,并重放该会话仍待处理的提问与审批(不订阅 = 接收所有会话) |
|
|
142
142
|
| `unsubscribe` | — | 取消过滤 |
|
|
143
143
|
|
|
144
144
|
```json
|
|
@@ -149,13 +149,26 @@ func connectAuthenticated(publicURL: URL, token: String) -> URLSessionWebSocketT
|
|
|
149
149
|
→ {"kind":"subscribed","sessionId":"session-abc"}
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
`subscribed` 之后,服务端会紧接着发送该 Session 尚未处理的
|
|
153
|
+
`question-requested` / `approval-requested`,并标记 `replay: true`。客户端必须按
|
|
154
|
+
`rpcId` 去重。这保证移动端在审批产生后才打开已有 Session 时仍能显示待处理卡片。
|
|
155
|
+
|
|
152
156
|
---
|
|
153
157
|
|
|
154
|
-
## 3. Human-in-the-loop
|
|
158
|
+
## 3. Human-in-the-loop
|
|
159
|
+
|
|
160
|
+
Human-in-the-loop 分为两条独立通道:
|
|
161
|
+
|
|
162
|
+
- **提问**:Agent 的 `ask_user_question` 工具向用户收集答案。
|
|
163
|
+
- **审批**:高风险工具操作(例如沙箱升权)请求一次性允许或拒绝。
|
|
164
|
+
|
|
165
|
+
二者都是 API Gateway 的临时请求,不属于持久化的 `session/event`,且都必须以其原始 `rpcId` 通过专用响应帧回答,不能作为普通 `message` 发送。
|
|
155
166
|
|
|
156
|
-
|
|
167
|
+
### 3.1 提问与回答
|
|
157
168
|
|
|
158
|
-
|
|
169
|
+
Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway 的 `events.mux()` 收到临时的待回答请求,并推送给移动端。
|
|
170
|
+
|
|
171
|
+
#### `question-requested` — 服务端推送问题
|
|
159
172
|
|
|
160
173
|
```json
|
|
161
174
|
{
|
|
@@ -187,7 +200,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
187
200
|
- `intent`:可选展示意图。目前可能为 `{ "kind":"plan-review", "approve":"批准选项标签" }`;未知 intent 应退化为普通选项列表。
|
|
188
201
|
- `replay: true`:可选。表示这是移动端连接后重放的仍待回答问题。iOS 必须按 `rpcId` 去重。
|
|
189
202
|
|
|
190
|
-
|
|
203
|
+
#### `question-answer` — 移动端提交整批答案
|
|
191
204
|
|
|
192
205
|
```json
|
|
193
206
|
{
|
|
@@ -236,7 +249,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
236
249
|
|
|
237
250
|
答案结构不合法时 `reason` 为 `bad-response`。这两种情况均不能重发为普通聊天消息。
|
|
238
251
|
|
|
239
|
-
|
|
252
|
+
#### `question-cancel` — 跳过/取消整批问题
|
|
240
253
|
|
|
241
254
|
```json
|
|
242
255
|
{ "type":"question-cancel", "rpcId":"5ce4f5d1-...", "sessionId":"session-abc" }
|
|
@@ -244,7 +257,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
244
257
|
|
|
245
258
|
回执仍为 `question-response`,其中 `action` 为 `cancel`。取消会让等待中的 `ask_user_question` 以 `ASK_CANCELLED` 结束,iOS 应在用户确认后再执行。
|
|
246
259
|
|
|
247
|
-
|
|
260
|
+
#### `question-resolved` — 服务端广播最终状态
|
|
248
261
|
|
|
249
262
|
```json
|
|
250
263
|
{ "kind":"question-resolved", "rpcId":"5ce4f5d1-...", "sessionId":"session-abc",
|
|
@@ -255,6 +268,72 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
255
268
|
|
|
256
269
|
---
|
|
257
270
|
|
|
271
|
+
### 3.2 操作审批
|
|
272
|
+
|
|
273
|
+
当 DSH 的工具管线要求人工授权时,插件会从 API Gateway 收到一次 `approval/requested`。这正是 Web UI 中“等待审批”卡片对应的事件:`reason` 是面向用户的审批说明,`toolName` 标识请求操作的工具,`callId` 可用于与实时工具调用轨迹关联。
|
|
274
|
+
|
|
275
|
+
#### `approval-requested` — 服务端推送待审批操作
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"kind": "approval-requested",
|
|
280
|
+
"rpcId": "approval-rpc-1",
|
|
281
|
+
"sessionId": "session-abc",
|
|
282
|
+
"approvalId": "approval-1",
|
|
283
|
+
"toolName": "bash",
|
|
284
|
+
"callId": "call-42",
|
|
285
|
+
"reason": "escalate sandbox to danger-full-access",
|
|
286
|
+
"replay": true
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- `rpcId`:本次可回答请求的稳定 RPC ID;提交决定时必须原样返回。
|
|
291
|
+
- `approvalId`:审批审计 ID;同样必须原样返回,并用于将最终状态关联到本地审批卡片。
|
|
292
|
+
- `toolName`:请求审批的工具名。
|
|
293
|
+
- `callId` / `reason`:可选。前者可关联工具调用,后者应直接显示为待审批原因。
|
|
294
|
+
- `replay: true`:表示当前仍未决定的审批在移动端连接或切换 Session 后重放。客户端应按 `rpcId` 去重。
|
|
295
|
+
|
|
296
|
+
审批请求不含工具完整参数;移动端应将 `reason` 与可见的工具调用轨迹作为展示依据,不应自行推断或构造命令。
|
|
297
|
+
|
|
298
|
+
#### `approval-response` — 移动端提交决定
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"type": "approval-response",
|
|
303
|
+
"rpcId": "approval-rpc-1",
|
|
304
|
+
"sessionId": "session-abc",
|
|
305
|
+
"approvalId": "approval-1",
|
|
306
|
+
"outcome": "allowed-once"
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`outcome` 只能是:
|
|
311
|
+
|
|
312
|
+
- `allowed-once`:仅允许这一次请求的操作。
|
|
313
|
+
- `rejected`:拒绝该操作。
|
|
314
|
+
|
|
315
|
+
这是一次性决定;协议不支持“始终允许”。`cancelled` 与 `unavailable` 是宿主侧状态,移动端不得提交。请求的 `sessionId`、`approvalId` 与 `rpcId` 必须匹配同一待审批项。
|
|
316
|
+
|
|
317
|
+
网关立即返回交付回执:
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
{ "kind":"approval-response", "rpcId":"approval-rpc-1", "sessionId":"session-abc",
|
|
321
|
+
"approvalId":"approval-1", "outcome":"allowed-once", "accepted":true }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
如果 Web UI 或另一台移动设备已经先作出决定,则回执为 `accepted:false`,并附带 `reason:"not-pending"`。收到错误帧或未被接受的回执时,客户端应保留当前状态,等待最终状态或重新打开事件流。
|
|
325
|
+
|
|
326
|
+
#### `approval-resolved` — 服务端广播最终状态
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{ "kind":"approval-resolved", "rpcId":"approval-rpc-1", "sessionId":"session-abc",
|
|
330
|
+
"approvalId":"approval-1", "outcome":"allowed-once" }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`outcome` 为 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`。所有移动连接都会收到最终状态并关闭对应审批卡片。移动端断线重连后,API Gateway 会重放仍待决定的审批;已决审批不会重放。
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
258
337
|
## 4. 消息(手机 → agent)
|
|
259
338
|
|
|
260
339
|
### `message` — 发送消息(会话不存在则创建)
|
|
@@ -264,10 +343,148 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
264
343
|
```
|
|
265
344
|
- `sessionId`:可选。省略时**自动创建新会话**(可用 `workspaceId` 或 `cwd` 指定归属工作区,至多一个,workspaceId 优先)
|
|
266
345
|
- `mode`:`"queue"`(排队,默认)/ `"steer"`(打断当前回合)
|
|
267
|
-
- `
|
|
346
|
+
- `message` 始终是用户 Prompt,Gateway 不会猜测或拦截其中的 `/...`。Host 命令必须使用下文的 `command-execute`;技能(如 `/android-cli 连接设备`)仍作为 `message` 发送,Host 会在 pre-step 阶段注入技能内容。
|
|
268
347
|
- `text` 与 `images` 至少提供一项;因此支持纯图片消息
|
|
269
348
|
- `clientTimeZone`:可选 IANA 时区,例如 `Asia/Shanghai`,宿主会校验后记录到这条用户消息
|
|
270
349
|
|
|
350
|
+
### 输入菜单目录(命令 + 技能)
|
|
351
|
+
|
|
352
|
+
目录按会话查询:Agent preset 会影响 Host 命令,会话工作目录会影响可用技能。客户端输入 `/` 后请求:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{ "type": "commands", "sessionId": "session-abc", "locale": "zh-CN" }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
→ {
|
|
360
|
+
"kind": "commands",
|
|
361
|
+
"sessionId": "session-abc",
|
|
362
|
+
"locale": "zh-CN",
|
|
363
|
+
"groups": [
|
|
364
|
+
{
|
|
365
|
+
"id": "commands",
|
|
366
|
+
"title": "命令",
|
|
367
|
+
"items": [
|
|
368
|
+
{
|
|
369
|
+
"id": "command:compact",
|
|
370
|
+
"name": "compact",
|
|
371
|
+
"description": "Compact older conversation history",
|
|
372
|
+
"source": "host",
|
|
373
|
+
"ui": {
|
|
374
|
+
"kind": "immediate",
|
|
375
|
+
"submitRequest": "command-execute",
|
|
376
|
+
"submitText": "/compact"
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"id": "command:permission",
|
|
381
|
+
"name": "permission",
|
|
382
|
+
"description": "Switch the permission preset",
|
|
383
|
+
"source": "host",
|
|
384
|
+
"ui": {
|
|
385
|
+
"kind": "select",
|
|
386
|
+
"insertText": "/permission",
|
|
387
|
+
"optionsRequest": "command-options",
|
|
388
|
+
"selectionRequest": "command-select"
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"id": "skills",
|
|
395
|
+
"title": "技能",
|
|
396
|
+
"items": [
|
|
397
|
+
{
|
|
398
|
+
"id": "skill:android-cli",
|
|
399
|
+
"name": "android-cli",
|
|
400
|
+
"description": "Provides instructions for installing and using the Android CLI",
|
|
401
|
+
"source": "skill",
|
|
402
|
+
"action": "insert",
|
|
403
|
+
"modelInvocable": true,
|
|
404
|
+
"ui": {
|
|
405
|
+
"kind": "input",
|
|
406
|
+
"insertText": "/android-cli ",
|
|
407
|
+
"images": true,
|
|
408
|
+
"submitRequest": "message"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
- `groups` 是客户端的权威渲染结构;分组标题、顺序、条目和交互参数全部由服务端下发。
|
|
418
|
+
- `locale` 可传 `zh-CN` 或英文 locale;服务端返回实际使用的 locale,并为已知命令下发 `ui.displayHint`。客户端优先显示 `displayHint`,缺失时回退到 Host 原始 `hint`。
|
|
419
|
+
- 客户端只解释 `ui`,不按条目名写分支:`immediate` 将 `submitText` 通过 `submitRequest` 发送;`input` 插入 `insertText`、高亮首个 Token 并使用可选的 `displayHint/hint/images`;`select` 插入 `insertText` 并打开通用二级菜单。
|
|
420
|
+
- `source: "host"` / `action: "execute"`:真实 DSH 斜杠命令,必须将 `/<name>` 或 `/<name> <args>` 通过 `command-execute.line` 提交,不得放入 `message.text`。
|
|
421
|
+
- `source: "skill"`:条目来自 DSH `skill.list({sessionId})`。选中时仅按 `ui.insertText` 写入草稿,发送后 Host 会在 pre-step 阶段加载技能内容,不需要专用执行接口。`modelInvocable: false` 的用户专用技能也会被列出,其显示描述由服务端加上“仅用户”标记。
|
|
422
|
+
- `model` 虽然是与官方 Web UI 一致的客户端命令,但选项加载与提交同样走下述通用接口,客户端不需要识别它的名字或模型协议。
|
|
423
|
+
- Host 命令和技能各自保留原始顺序,`model` 客户端命令追加在命令组末尾。若未来 Host 自己注册 `model`,gateway 不会重复追加。
|
|
424
|
+
- `hello.capabilities` 包含 `commands` 时表示服务端支持此目录接口。
|
|
425
|
+
|
|
426
|
+
#### Host 命令执行
|
|
427
|
+
|
|
428
|
+
`ui.submitRequest` 为 `command-execute` 时,客户端将完整命令行发送到专用接口:
|
|
429
|
+
|
|
430
|
+
```json
|
|
431
|
+
{ "type": "command-execute", "sessionId": "session-abc",
|
|
432
|
+
"line": "/compact", "images": [] }
|
|
433
|
+
→ {
|
|
434
|
+
"kind": "command-executed",
|
|
435
|
+
"sessionId": "session-abc",
|
|
436
|
+
"line": "/compact",
|
|
437
|
+
"commandId": "command-123",
|
|
438
|
+
"result": { "kind": "success", "text": "Compacted 24 history items (~7230 tokens)." }
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
带参数命令仍是 `command-execute`:
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{ "type": "command-execute", "sessionId": "session-abc",
|
|
446
|
+
"line": "/plan 帮我完成 Android 端适配", "images": [] }
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
- `line` 必须以 `/` 开头,参数作为同一字符串跟在命令后面。
|
|
450
|
+
- 只有目录中 `ui.images: true` 的命令可携带图片;图片结构与 `message.images` 相同。Gateway 会再次校验。
|
|
451
|
+
- `result.kind: "error"` 表示命令已进入 Host 但处理失败;客户端应保留当前草稿和图片供用户修改。
|
|
452
|
+
- 命令不会生成 `user/message`,也不会进入模型 Prompt。Host 会持久化 `command/run` / `command/done`;`compact` 还会产生 `compaction/start` / `compaction/summary` / `compaction/end`。Gateway 会把这些事件实时转发,客户端据此渲染“正在压缩…”和最终结果。
|
|
453
|
+
|
|
454
|
+
#### 通用二级菜单
|
|
455
|
+
|
|
456
|
+
当 `ui.kind` 为 `select` 时,客户端使用 `ui.optionsRequest` 指定的请求类型加载标准化选项:
|
|
457
|
+
|
|
458
|
+
```json
|
|
459
|
+
{ "type": "command-options", "sessionId": "session-abc", "command": "permission" }
|
|
460
|
+
→ {
|
|
461
|
+
"kind": "command-options",
|
|
462
|
+
"sessionId": "session-abc",
|
|
463
|
+
"command": "permission",
|
|
464
|
+
"options": [
|
|
465
|
+
{ "id": "ask", "label": "Ask", "description": "Ask before risky operations", "selected": true },
|
|
466
|
+
{ "id": "workspace-write", "label": "Workspace Write", "selected": false }
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
`id` 是服务端拥有的 opaque 值;客户端只负责原样回传。模型选项和权限选项使用完全相同的 `{id,label,detail?,description?,selected}` 结构。
|
|
472
|
+
|
|
473
|
+
选择后使用 `ui.selectionRequest` 指定的请求类型提交:
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
{ "type": "command-select", "sessionId": "session-abc",
|
|
477
|
+
"command": "permission", "optionId": "workspace-write" }
|
|
478
|
+
→ {
|
|
479
|
+
"kind": "command-selected",
|
|
480
|
+
"sessionId": "session-abc",
|
|
481
|
+
"command": "permission",
|
|
482
|
+
"selected": { "id": "workspace-write", "label": "Workspace Write", "selected": true }
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
客户端用 `selected.label/detail` 更新输入框状态栏。`model`、`permission` 的具体查询、校验和写入全部由服务端处理;现有 `models/select-model` 与 `permission-options/permission` 仅作为兼容接口保留。
|
|
487
|
+
|
|
271
488
|
### 发送图片
|
|
272
489
|
|
|
273
490
|
iOS 将本地图片原始文件数据编码成**标准 Base64**,不要包含 `data:image/...;base64,` 前缀:
|
|
@@ -322,6 +539,10 @@ let image = [
|
|
|
322
539
|
| `sessions` | — | 会话列表(`updatedAt/running/blank/cwd/agentPreset`) |
|
|
323
540
|
| `history` | `sessionId`, `beforeSeq?`, `maxMessages?`, `maxBytes?`, `view?` | 历史事件页(见下) |
|
|
324
541
|
| `attachment` | `sessionId`, `attachmentId` | 读取历史中属于该会话的图片字节 |
|
|
542
|
+
| `file-list` | `sessionId`, `path?`, `requestId?` | 列出会话工作目录内的一层文件与文件夹 |
|
|
543
|
+
| `file-download-open` | `sessionId`, `path`, `requestId` | 打开一个工作目录内的普通文件下载 |
|
|
544
|
+
| `file-download-read` | `transferId`, `offset` | 拉取下载的下一块字节 |
|
|
545
|
+
| `file-download-cancel` | `transferId` | 取消并关闭下载 |
|
|
325
546
|
| `search` | `query` | 会话全文搜索 |
|
|
326
547
|
| `session-stats` | `sessionId` | 执行统计投影(输入框统计条数据源) |
|
|
327
548
|
| `context-usage` | `sessionId` | token 用量 + 上下文占用投影 |
|
|
@@ -384,6 +605,46 @@ iOS 发现尚未缓存的 `attachmentId` 后发送:
|
|
|
384
605
|
|
|
385
606
|
iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建议以 `attachmentId` 为缓存键。不要把 Base64 长期保存在对话模型对象里。并发同步历史时可限制为 2~4 个附件请求,优先加载当前可见消息。
|
|
386
607
|
|
|
608
|
+
### 文件下载(图片、文档、IPA、APK 及其他普通文件)
|
|
609
|
+
|
|
610
|
+
文件下载是独立于历史图片 `attachment` 的二进制传输通道。它不按扩展名做授权白名单:图片、PDF/Office 文档、`.ipa`、`.apk` 和其他**普通文件**均可下载;服务端仅根据扩展名给出 `mediaType`,以便移动端决定打开方式。
|
|
611
|
+
|
|
612
|
+
所有 `path` 都是相对于该 `sessionId` 的 `cwd` 的相对路径,使用 `/` 分隔。例如先列出根目录:
|
|
613
|
+
|
|
614
|
+
```json
|
|
615
|
+
{ "type": "file-list", "requestId": "files-1", "sessionId": "session-abc" }
|
|
616
|
+
→ {
|
|
617
|
+
"kind": "file-list", "requestId": "files-1", "sessionId": "session-abc", "path": ".",
|
|
618
|
+
"entries": [
|
|
619
|
+
{ "name": "builds", "path": "builds", "kind": "directory" },
|
|
620
|
+
{ "name": "app.ipa", "path": "app.ipa", "kind": "file", "bytes": 123456,
|
|
621
|
+
"modifiedAt": 1787111700000, "mediaType": "application/octet-stream" }
|
|
622
|
+
]
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
打开并按需拉取每一块:
|
|
627
|
+
|
|
628
|
+
```json
|
|
629
|
+
{ "type": "file-download-open", "requestId": "download-1", "sessionId": "session-abc", "path": "builds/app-release.apk" }
|
|
630
|
+
→ { "kind": "file-download-opened", "requestId": "download-1", "transferId": "...",
|
|
631
|
+
"sessionId": "session-abc", "path": "builds/app-release.apk", "name": "app-release.apk",
|
|
632
|
+
"mediaType": "application/vnd.android.package-archive", "size": 2345678, "chunkBytes": 524288 }
|
|
633
|
+
|
|
634
|
+
{ "type": "file-download-read", "transferId": "...", "offset": 0 }
|
|
635
|
+
→ { "kind": "file-download-chunk", "transferId": "...", "offset": 0,
|
|
636
|
+
"data": "<标准 Base64>", "eof": false }
|
|
637
|
+
|
|
638
|
+
{ "type": "file-download-read", "transferId": "...", "offset": 524288 }
|
|
639
|
+
→ { "kind": "file-download-chunk", "transferId": "...", "offset": 524288,
|
|
640
|
+
"data": "<标准 Base64>", "eof": true, "sha256": "<64 位十六进制摘要>" }
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
- 客户端必须严格使用服务端返回块的 `offset + 已解码 data 字节数` 作为下一次 `offset`;当前版本不支持断线续传。请先写入临时文件,收到 `eof: true` 后校验整文件 SHA-256,再原子重命名为最终文件。
|
|
644
|
+
- 每个 `transferId` 仅归属创建它的 WebSocket 连接。连接关闭、`file-download-cancel`、空闲 2 分钟、传完最后一块或插件卸载都会关闭文件句柄;取消成功返回 `{ "kind":"file-download-cancelled", "transferId":"..." }`。
|
|
645
|
+
- 默认每块为 512 KiB、同时最多 4 个下载、单个文件最多 512 MiB。部署方可用 `fileDownloadChunkBytes`、`fileDownloadMaxTransfers`、`fileDownloadMaxBytes`、`fileDownloadIdleMs` 调整;`fileDownloadsEnabled: false` 会关闭该能力,且 `hello.capabilities` 不再包含 `file-downloads`。
|
|
646
|
+
- 绝对路径、空路径(`file-download-open`)、`..` 路径段、NUL 字符、工作目录外的符号链接、目录和其他非普通文件都会被拒绝。`file-list` 不返回符号链接,避免客户端误认为其可下载。
|
|
647
|
+
|
|
387
648
|
### `session-stats` 详细(输入框统计条)
|
|
388
649
|
```json
|
|
389
650
|
{ "type": "session-stats", "sessionId": "session-abc" }
|
|
@@ -523,9 +784,10 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
523
784
|
| kind | 触发时机 |
|
|
524
785
|
|---|---|
|
|
525
786
|
| `paired` | 首次配对成功;仅此一次返回长期设备 token |
|
|
526
|
-
| `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images"], "authenticated":true, "port":3080, "clients":1 }` |
|
|
787
|
+
| `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images","file-downloads"], "authenticated":true, "port":3080, "clients":1 }` |
|
|
527
788
|
| `event` | 任意会话的 agent 输出(见下) |
|
|
528
789
|
| `question-requested` / `question-resolved` | Human-in-the-loop 问题请求与最终状态 |
|
|
790
|
+
| `approval-requested` / `approval-resolved` | Human-in-the-loop 操作审批请求与最终状态 |
|
|
529
791
|
| `pong` / `subscribed` / `sent` | 对应请求的回复 |
|
|
530
792
|
|
|
531
793
|
### `event` 帧(agent 实时输出)
|
|
@@ -566,6 +828,8 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
566
828
|
- 长期 token 只保存在 iOS Keychain;服务端磁盘仅保存摘要
|
|
567
829
|
- `set-default` / `save-default-model` 是全局写操作,客户端 UI 应加确认
|
|
568
830
|
- `question-answer` / `question-cancel` 会直接恢复或终止等待中的 Agent 工具调用;只允许经过鉴权的可信设备提交,并按 `rpcId` 防止重复操作
|
|
831
|
+
- `approval-response` 会直接允许或拒绝等待中的高风险工具操作;只允许经过鉴权的可信设备提交,并按 `rpcId` 和 `approvalId` 防止串用或重复操作
|
|
832
|
+
- 文件下载只允许读取该会话 `cwd` 内的普通文件;移动端必须在写入完成后校验最终块给出的 SHA-256,且不得把 `transferId` 视为可跨连接复用的凭证
|
|
569
833
|
|
|
570
834
|
---
|
|
571
835
|
|
|
@@ -589,8 +853,12 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
589
853
|
| v0.3.0 | 默认设备鉴权;一次性二维码配对;摘要化凭证存储;WebUI 设备面板;在线状态和即时吊销 |
|
|
590
854
|
| v0.5.0 | Human-in-the-loop:转发 API Gateway question 请求、整批回答/取消、重连重放与多端状态收敛 |
|
|
591
855
|
| v0.6.0 | DSH 0.1.1 图片:WebSocket Base64 上传、实时图片引用、历史附件按会话安全读取 |
|
|
592
|
-
| v0.6.3 | macOS native picker 兼容:目录创建改用与目录浏览一致的宿主文件系统实现,并补齐路径、名称和错误码校验 |
|
|
593
856
|
| v0.6.2 | 目录创建:通过 API Gateway `host.createDirectory` 在工作区目录下创建子文件夹 |
|
|
857
|
+
| v0.6.3 | macOS native picker 兼容:目录创建改用与目录浏览一致的宿主文件系统实现,并补齐路径、名称和错误码校验 |
|
|
858
|
+
| v0.6.6 | Human-in-the-loop 操作审批:转发 API Gateway approval 请求、一次性允许/拒绝、重连重放与多端最终状态收敛 |
|
|
859
|
+
| v0.6.7 | 订阅已有 Session 时重放待处理 Human-in-the-loop 请求,并增加 Approval 端到端诊断日志与安装版本标记 |
|
|
860
|
+
| v0.6.8 | 会话工作目录受限的文件列表与分块下载:支持图片、文档、IPA、APK 等普通文件,含连接归属、路径越界防护、取消、超时和 SHA-256 完整性校验 |
|
|
861
|
+
| v0.6.9 | 服务端驱动的命令与技能目录:支持本地化 Hint、通用二级选项、专用命令执行,以及 command/compaction 生命周期事件;Host 命令不再作为用户 Prompt 发送 |
|
|
594
862
|
|
|
595
863
|
---
|
|
596
864
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# dsh-plugin-mobile-gateway
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
|
|
8
8
|
|
|
9
9
|
- WebSocket:`/ws/mobile`
|
|
10
10
|
- 局域网:`ws://<局域网 IP>:3081/ws/mobile`
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## 配套 iOS 客户端
|
|
15
15
|
|
|
16
|
-
[DeepSeek Harness Mobile](https://github.com/Clarklevis1995/dsh-mobile) 是本仓库的兄弟项目。它是面向 iOS 17+ 的 SwiftUI 原生客户端,支持工作区与会话、工作区内创建文件夹、历史和实时对话、图片、Agent 执行轨迹、Human-in-the-loop
|
|
16
|
+
[DeepSeek Harness Mobile](https://github.com/Clarklevis1995/dsh-mobile) 是本仓库的兄弟项目。它是面向 iOS 17+ 的 SwiftUI 原生客户端,支持工作区与会话、工作区内创建文件夹、历史和实时对话、图片、Agent 执行轨迹、Human-in-the-loop,以及由网关配置驱动的命令、技能、模型与权限菜单。
|
|
17
17
|
|
|
18
18
|
<table>
|
|
19
19
|
<tr>
|