dsh-plugin-mobile-gateway 0.5.0 → 0.6.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 +92 -5
- package/README.md +86 -199
- package/lib/index.mjs +96 -13
- package/package.json +2 -2
package/PROTOCOL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# dsh Mobile Gateway — WebSocket 协议参考
|
|
2
2
|
|
|
3
|
-
移动端通过一个经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent
|
|
3
|
+
移动端通过一个经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop 提问、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现(v0.6.0)。
|
|
4
4
|
|
|
5
5
|
- **本机端点**:`ws://127.0.0.1:3080/ws/mobile`(与 dsh web GUI 同端口)
|
|
6
6
|
- **局域网端点**:`ws://<电脑的私有局域网 IP>:3081/ws/mobile`(插件独立监听,只提供经过鉴权的 WebSocket)
|
|
7
7
|
- **公网端点**:必须由 TLS 反向代理提供 `wss://<域名>/ws/mobile`
|
|
8
|
-
- **帧格式**:全部为 JSON 文本帧(UTF-8
|
|
8
|
+
- **帧格式**:全部为 JSON 文本帧(UTF-8);图片字节使用标准 Base64
|
|
9
9
|
- **连接即推送**:连上后服务端立刻发送一条 `hello`,之后 agent 输出以 `event` 帧实时推送
|
|
10
10
|
|
|
11
11
|
---
|
|
@@ -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":
|
|
80
|
+
{ "kind": "hello", "protocol": 3, "capabilities": ["images"], "authenticated": true,
|
|
81
81
|
"device": { "id": "...", "name": "iPhone" }, "port": 3080, "clients": 1 }
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -265,6 +265,48 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
265
265
|
- `sessionId`:可选。省略时**自动创建新会话**(可用 `workspaceId` 或 `cwd` 指定归属工作区,至多一个,workspaceId 优先)
|
|
266
266
|
- `mode`:`"queue"`(排队,默认)/ `"steer"`(打断当前回合)
|
|
267
267
|
- `text` 以 `/` 开头会被当作**斜杠命令**(如 `/permission ask`),宿主直接执行、**绝不发给模型**
|
|
268
|
+
- `text` 与 `images` 至少提供一项;因此支持纯图片消息
|
|
269
|
+
- `clientTimeZone`:可选 IANA 时区,例如 `Asia/Shanghai`,宿主会校验后记录到这条用户消息
|
|
270
|
+
|
|
271
|
+
### 发送图片
|
|
272
|
+
|
|
273
|
+
iOS 将本地图片原始文件数据编码成**标准 Base64**,不要包含 `data:image/...;base64,` 前缀:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"type": "message",
|
|
278
|
+
"sessionId": "session-abc",
|
|
279
|
+
"text": "请描述这两张图片",
|
|
280
|
+
"clientTimeZone": "Asia/Shanghai",
|
|
281
|
+
"images": [
|
|
282
|
+
{
|
|
283
|
+
"mediaType": "image/jpeg",
|
|
284
|
+
"data": "/9j/4AAQSkZJRgABAQ...",
|
|
285
|
+
"name": "IMG_1024.JPG"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"mediaType": "image/png",
|
|
289
|
+
"data": "iVBORw0KGgoAAA...",
|
|
290
|
+
"name": "diagram.png"
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
支持的 `mediaType`:`image/png`、`image/jpeg`、`image/webp`、`image/gif`。宿主会验证 Base64、文件签名、格式、尺寸、像素数、单图大小、图片数量和总大小;声明 MIME 与真实字节不一致会拒绝整条消息,且不会产生部分附件。
|
|
297
|
+
|
|
298
|
+
当前 DSH 默认最多 20 张图片、单图约 3.5 MiB、单条消息图片总计 100 MiB,实际值以最近一次 `history.projections.values.imageLimits` 为准。WebSocket 单帧上限默认 144 MiB,用于容纳 100 MiB 图片经 Base64 后的 JSON 请求;反向代理也必须允许相应大小的 WebSocket 帧。
|
|
299
|
+
|
|
300
|
+
Swift 编码示例:
|
|
301
|
+
|
|
302
|
+
```swift
|
|
303
|
+
let data = try Data(contentsOf: imageURL)
|
|
304
|
+
let image = [
|
|
305
|
+
"mediaType": "image/jpeg",
|
|
306
|
+
"data": data.base64EncodedString(),
|
|
307
|
+
"name": imageURL.lastPathComponent
|
|
308
|
+
]
|
|
309
|
+
```
|
|
268
310
|
|
|
269
311
|
```json
|
|
270
312
|
→ { "kind": "sent", "sessionId": "session-abc", "mode": "queue" }
|
|
@@ -279,6 +321,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
279
321
|
|---|---|---|
|
|
280
322
|
| `sessions` | — | 会话列表(`updatedAt/running/blank/cwd/agentPreset`) |
|
|
281
323
|
| `history` | `sessionId`, `beforeSeq?`, `maxMessages?`, `maxBytes?`, `view?` | 历史事件页(见下) |
|
|
324
|
+
| `attachment` | `sessionId`, `attachmentId` | 读取历史中属于该会话的图片字节 |
|
|
282
325
|
| `search` | `query` | 会话全文搜索 |
|
|
283
326
|
| `session-stats` | `sessionId` | 执行统计投影(输入框统计条数据源) |
|
|
284
327
|
| `context-usage` | `sessionId` | token 用量 + 上下文占用投影 |
|
|
@@ -288,6 +331,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
288
331
|
{ "type": "history", "sessionId": "session-abc", "maxMessages": 60, "maxBytes": 4194304, "view": "conversation" }
|
|
289
332
|
```
|
|
290
333
|
- 返回**原始 SessionEvent**(`{type, seq, time, data}`,方案A),可选裁剪
|
|
334
|
+
- 图片不会内联进历史页。`user/message.data.content[]` 中的图片块为 `{ "type":"image", "attachment": ImageAttachmentRef }`;iOS 使用其中的 `attachmentId` 请求图片数据
|
|
291
335
|
- `maxBytes`:单帧字节预算,默认 **4 MiB**;超预算保留最新部分并给出 `nextBeforeSeq` 续页(客户端 16 MiB 上限的安全余量)
|
|
292
336
|
- `view: "conversation"`:**对话裁剪模式**——丢弃 `assistant/chunk`(token 回放)与 `request/header`(system prompt),`tool/result` 嵌套文本截断到 2000 字符
|
|
293
337
|
- 分页:`hasMore` 为真时用 `beforeSeq: nextBeforeSeq` 请求更早一页
|
|
@@ -298,6 +342,48 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
298
342
|
"projections": { "asOfSeq": 127, "values": { "tokenUsage": {...}, "contextPressure": {...}, "permissions": {...}, "sessionStats": {...} } } }
|
|
299
343
|
```
|
|
300
344
|
|
|
345
|
+
图片引用结构:
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"type": "image",
|
|
350
|
+
"attachment": {
|
|
351
|
+
"attachmentId": "sha256-opaque-id",
|
|
352
|
+
"mediaType": "image/jpeg",
|
|
353
|
+
"bytes": 184320,
|
|
354
|
+
"width": 1200,
|
|
355
|
+
"height": 900,
|
|
356
|
+
"name": "IMG_1024.JPG"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
iOS 发现尚未缓存的 `attachmentId` 后发送:
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{ "type":"attachment", "sessionId":"session-abc", "attachmentId":"sha256-opaque-id" }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
服务端在确认该会话历史确实引用了这张图片后返回:
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"kind": "attachment",
|
|
372
|
+
"sessionId": "session-abc",
|
|
373
|
+
"attachment": {
|
|
374
|
+
"attachmentId": "sha256-opaque-id",
|
|
375
|
+
"mediaType": "image/jpeg",
|
|
376
|
+
"bytes": 184320,
|
|
377
|
+
"width": 1200,
|
|
378
|
+
"height": 900,
|
|
379
|
+
"name": "IMG_1024.JPG"
|
|
380
|
+
},
|
|
381
|
+
"data": "/9j/4AAQSkZJRgABAQ..."
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建议以 `attachmentId` 为缓存键。不要把 Base64 长期保存在对话模型对象里。并发同步历史时可限制为 2~4 个附件请求,优先加载当前可见消息。
|
|
386
|
+
|
|
301
387
|
### `session-stats` 详细(输入框统计条)
|
|
302
388
|
```json
|
|
303
389
|
{ "type": "session-stats", "sessionId": "session-abc" }
|
|
@@ -423,7 +509,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
423
509
|
| kind | 触发时机 |
|
|
424
510
|
|---|---|
|
|
425
511
|
| `paired` | 首次配对成功;仅此一次返回长期设备 token |
|
|
426
|
-
| `hello` | 连接成功:`{ "kind":"hello", "protocol":
|
|
512
|
+
| `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images"], "authenticated":true, "port":3080, "clients":1 }` |
|
|
427
513
|
| `event` | 任意会话的 agent 输出(见下) |
|
|
428
514
|
| `question-requested` / `question-resolved` | Human-in-the-loop 问题请求与最终状态 |
|
|
429
515
|
| `pong` / `subscribed` / `sent` | 对应请求的回复 |
|
|
@@ -434,7 +520,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
434
520
|
"event": { "type": "assistant/chunk", "turn": 1, "step": 0, "chunkType": "text-delta", "text": "正在" } }
|
|
435
521
|
```
|
|
436
522
|
`event.type` 覆盖(精炼字段):
|
|
437
|
-
- `user/message` → `{text, source}`
|
|
523
|
+
- `user/message` → `{text, source, images?: ImageAttachmentRef[]}`
|
|
438
524
|
- `assistant/chunk` → `{turn, step, chunkType: text-delta|reasoning-delta|tool-call-delta|usage|finish, text?/tool?/usage?/finish?}`
|
|
439
525
|
- `assistant/message` → `{turn, step, text, reasoning, toolCalls[]}`
|
|
440
526
|
- `tool/call` → `{turn, step, callId, name, arguments}`
|
|
@@ -487,6 +573,7 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
487
573
|
| v0.1.17 | models 支持无 sessionId 全局目录;新增 providers |
|
|
488
574
|
| v0.3.0 | 默认设备鉴权;一次性二维码配对;摘要化凭证存储;WebUI 设备面板;在线状态和即时吊销 |
|
|
489
575
|
| v0.5.0 | Human-in-the-loop:转发 API Gateway question 请求、整批回答/取消、重连重放与多端状态收敛 |
|
|
576
|
+
| v0.6.0 | DSH 0.1.1 图片:WebSocket Base64 上传、实时图片引用、历史附件按会话安全读取 |
|
|
490
577
|
|
|
491
578
|
---
|
|
492
579
|
|
package/README.md
CHANGED
|
@@ -1,276 +1,163 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/whale-girl-ios-app-promo-16x9.png" alt="
|
|
2
|
+
<img src="docs/assets/whale-girl-ios-app-promo-16x9.png" alt="DeepSeek Harness Mobile 与移动网关" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# dsh-plugin-mobile-gateway
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
让 iPhone 通过经过设备鉴权的 WebSocket 连接 DeepSeek Harness。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- WebSocket:`/ws/mobile`
|
|
10
|
+
- 局域网:`ws://<局域网 IP>:3081/ws/mobile`
|
|
11
|
+
- 公网:`wss://<公网 IP>/ws/mobile`
|
|
12
|
+
- 协议文档:[PROTOCOL.md](PROTOCOL.md)
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
- 管理入口:Harness WebUI 左侧边栏底部的“移动设备”
|
|
13
|
-
- 默认安全策略:网关默认关闭、设备鉴权默认开启、管理接口仅允许本机访问
|
|
14
|
-
- 完整通信格式:[`PROTOCOL.md`](PROTOCOL.md)
|
|
14
|
+
## 配套 iOS 客户端
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
[DeepSeek Harness Mobile](https://github.com/Clarklevis1995/dsh-mobile) 是本仓库的兄弟项目。它是面向 iOS 17+ 的 SwiftUI 原生客户端,支持工作区与会话、历史和实时对话、图片、Agent 执行轨迹、Human-in-the-loop、模型与权限设置。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
<table>
|
|
19
|
+
<tr>
|
|
20
|
+
<td width="33.33%" align="center"><img src="https://raw.githubusercontent.com/Clarklevis1995/dsh-mobile/main/Docs/images/screenshots/home.png" alt="iOS 工作区首页" width="100%"></td>
|
|
21
|
+
<td width="33.33%" align="center"><img src="https://raw.githubusercontent.com/Clarklevis1995/dsh-mobile/main/Docs/images/screenshots/conversation-dark.png" alt="iOS 深色对话界面" width="100%"></td>
|
|
22
|
+
<td width="33.33%" align="center"><img src="https://raw.githubusercontent.com/Clarklevis1995/dsh-mobile/main/Docs/images/screenshots/pairing-dark.png" alt="iOS 设备配对界面" width="100%"></td>
|
|
23
|
+
</tr>
|
|
24
|
+
<tr>
|
|
25
|
+
<td align="center"><strong>工作区首页</strong></td>
|
|
26
|
+
<td align="center"><strong>实时对话</strong></td>
|
|
27
|
+
<td align="center"><strong>设备配对</strong></td>
|
|
28
|
+
</tr>
|
|
29
|
+
</table>
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
## 安装插件
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
dsh plugin --profile web add dsh-plugin-mobile-gateway
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
这条命令会直接从 npm 获取插件、安装依赖,并将插件加入 `web` profile 的 bundle 列表。用户不需要 clone 仓库,也不需要运行 `pnpm install`。
|
|
27
|
-
|
|
28
|
-
如果希望固定版本,可以在包名后指定版本号:
|
|
33
|
+
前提:已经安装 `dsh` CLI,并能正常启动 `dsh web`。
|
|
29
34
|
|
|
30
35
|
```bash
|
|
31
|
-
dsh plugin --profile web add dsh-plugin-mobile-gateway@
|
|
36
|
+
dsh plugin --profile web add dsh-plugin-mobile-gateway@latest
|
|
32
37
|
```
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
安装后停止并重新启动 WebUI:
|
|
35
40
|
|
|
36
41
|
```bash
|
|
37
|
-
dsh
|
|
42
|
+
dsh web
|
|
38
43
|
```
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
打开 WebUI,确认左侧边栏底部出现“移动设备”。
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
## 局域网配对
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
dsh --profile web --dump-config | grep -A3 mobile-gateway
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
启动 WebUI 后,左侧边栏底部应出现“移动设备”入口。
|
|
51
|
-
|
|
52
|
-
### 3. 局域网直接连接(无需域名、证书或反向代理)
|
|
53
|
-
|
|
54
|
-
插件安装并重启 `dsh web` 后,会额外创建一个仅用于移动端的局域网监听:
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
ws://<运行 DSH 的电脑局域网 IP>:3081/ws/mobile
|
|
58
|
-
```
|
|
49
|
+
适用于 DSH 电脑和 iPhone 位于同一个可互访的局域网。
|
|
59
50
|
|
|
60
|
-
打开 WebUI
|
|
51
|
+
1. 打开 WebUI 的“移动设备”。
|
|
52
|
+
2. 开启“允许移动设备连接”。
|
|
53
|
+
3. 保持“设备鉴权”开启。
|
|
54
|
+
4. 确认面板显示 `ws://<电脑局域网 IP>:3081/ws/mobile`。
|
|
55
|
+
5. 填写设备名称并点击“生成配对二维码”。
|
|
56
|
+
6. 在 iOS 客户端打开“设备认证”,扫描二维码。
|
|
57
|
+
7. WebUI 的可信设备显示“在线”后即完成。
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
如果系统防火墙拦截连接,只允许私有网络访问 TCP `3081`。不要把 3081 开放到公网。
|
|
63
60
|
|
|
64
|
-
|
|
61
|
+
## 公网 IP 配对
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
适用于带固定公网 IPv4 的 Ubuntu/Debian 服务器(其他发行版本可自行尝试)。Node.js 通过 nvm 安装时,也使用下面的命令。
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
sudo npx --yes dsh-plugin-mobile-gateway setup
|
|
70
|
-
```
|
|
65
|
+
### 1. 配置端口
|
|
71
66
|
|
|
72
|
-
|
|
67
|
+
从云厂商控制台复制服务器的公网 IPv4,并在安全组中放行入站 TCP `80` 和 `443`。
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- 对普通 HTTP、WebUI、`/mgw/*` 管理接口和其他路径返回 `404`
|
|
78
|
-
- 创建每天两次运行的 systemd 证书续期任务
|
|
79
|
-
- 把公网地址写入 `/etc/dsh-mobile-gateway/public-url`,插件启动时自动读取
|
|
69
|
+
不要开放 TCP `3080` 和 `3081`!!!
|
|
70
|
+
不要开放 TCP `3080` 和 `3081`!!!
|
|
71
|
+
不要开放 TCP `3080` 和 `3081`!!!
|
|
80
72
|
|
|
81
|
-
|
|
73
|
+
### 2. 配置公网入口
|
|
82
74
|
|
|
83
|
-
|
|
75
|
+
把示例 IP 替换为云厂商控制台中的实际公网 IPv4:
|
|
84
76
|
|
|
85
77
|
```bash
|
|
86
|
-
sudo npx --yes dsh-plugin-mobile-gateway setup \
|
|
78
|
+
sudo env "PATH=$PATH" npx --yes dsh-plugin-mobile-gateway@latest setup \
|
|
87
79
|
--ip 203.0.113.10 \
|
|
88
|
-
--port 3080
|
|
89
|
-
--email you@example.com
|
|
80
|
+
--port 3080
|
|
90
81
|
```
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
查看状态或移除安装器生成的公网入口:
|
|
83
|
+
安装器完成后,重新启动:
|
|
95
84
|
|
|
96
85
|
```bash
|
|
97
|
-
|
|
98
|
-
sudo npx --yes dsh-plugin-mobile-gateway remove
|
|
86
|
+
dsh web
|
|
99
87
|
```
|
|
100
88
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#### 已有域名或反向代理
|
|
89
|
+
### 3. 打开远程 WebUI(使用端口转发,VSCode等IDE自带)
|
|
104
90
|
|
|
105
|
-
|
|
91
|
+
如果 WebUI 运行在远程服务器,在自己的电脑执行:
|
|
106
92
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
3. 在“WebSocket 地址”中填写手机可以访问的地址。
|
|
110
|
-
|
|
111
|
-
本机浏览器调试可以使用:
|
|
112
|
-
|
|
113
|
-
```text
|
|
114
|
-
ws://127.0.0.1:3080/ws/mobile
|
|
93
|
+
```bash
|
|
94
|
+
ssh -N -L 3080:127.0.0.1:3080 <服务器用户名>@<服务器公网 IP>
|
|
115
95
|
```
|
|
116
96
|
|
|
117
|
-
|
|
97
|
+
然后在本地浏览器打开:
|
|
118
98
|
|
|
119
99
|
```text
|
|
120
|
-
|
|
100
|
+
http://127.0.0.1:3080
|
|
121
101
|
```
|
|
122
102
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
### 5. 配对 iOS 客户端
|
|
126
|
-
|
|
127
|
-
1. 在 WebUI 中填写设备名称,例如 `iPhone`。
|
|
128
|
-
2. 点击“生成配对二维码”。
|
|
129
|
-
3. 在 iOS 客户端首页点击认证按钮:
|
|
130
|
-
- 选择“扫描二维码”,扫码后自动连接;或
|
|
131
|
-
- 选择“手动输入配对信息”,粘贴 WebUI 中复制的 Base64URL 配对 Token,再点击连接。
|
|
132
|
-
4. iOS 首次连接成功后会把长期设备凭证保存到 Keychain。配对二维码只能使用一次,并会在 5 分钟后过期。
|
|
133
|
-
5. WebUI 的可信设备列表显示“在线”,iOS 首页连接状态变为绿色,即表示连接完成。
|
|
134
|
-
|
|
135
|
-
后续启动 iOS 客户端时会使用 Keychain 中的长期凭证重新连接,不需要再次扫码。重新配对同一套 iOS 安装也会复用其设备身份,不会重复创建可信设备。
|
|
136
|
-
|
|
137
|
-
## 日常使用
|
|
138
|
-
|
|
139
|
-
连接成功后,移动客户端可以:
|
|
103
|
+
### 4. 使用 WebUI 配对
|
|
140
104
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
105
|
+
1. 打开“移动设备”。
|
|
106
|
+
2. 开启“允许移动设备连接”。
|
|
107
|
+
3. 保持“设备鉴权”开启。
|
|
108
|
+
4. 确认 WebSocket 地址为 `wss://<公网 IP>/ws/mobile`。
|
|
109
|
+
5. 填写设备名称并点击“生成配对二维码”。
|
|
110
|
+
6. iPhone 打开“设备认证”,扫描二维码;也可以复制 Base64URL 配对字符串手动连接。
|
|
111
|
+
7. WebUI 的可信设备显示“在线”后即完成。
|
|
148
112
|
|
|
149
|
-
|
|
113
|
+
二维码只能使用一次,并会在 5 分钟后过期;超时后在 WebUI 重新生成即可。
|
|
150
114
|
|
|
151
|
-
##
|
|
115
|
+
## 公网入口管理
|
|
152
116
|
|
|
153
|
-
|
|
117
|
+
查看状态:
|
|
154
118
|
|
|
155
|
-
```
|
|
156
|
-
|
|
119
|
+
```bash
|
|
120
|
+
sudo env "PATH=$PATH" npx --yes dsh-plugin-mobile-gateway@latest status
|
|
157
121
|
```
|
|
158
122
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
- 从可信设备列表删除该设备
|
|
162
|
-
- 立即关闭该设备现有的 WebSocket 连接
|
|
163
|
-
- 使该设备保存的长期凭证永久失效
|
|
164
|
-
|
|
165
|
-
被吊销的设备再次连接会收到 `401 Unauthorized`,需要重新配对。
|
|
166
|
-
|
|
167
|
-
## 更新插件(无需源码)
|
|
168
|
-
|
|
169
|
-
插件会被安装到 profile 中,不会自动更新。升级时重新安装最新 npm 版本并重启 WebUI:
|
|
123
|
+
移除公网入口:
|
|
170
124
|
|
|
171
125
|
```bash
|
|
172
|
-
|
|
173
|
-
dsh plugin --profile web add dsh-plugin-mobile-gateway
|
|
126
|
+
sudo env "PATH=$PATH" npx --yes dsh-plugin-mobile-gateway@latest remove
|
|
174
127
|
```
|
|
175
128
|
|
|
176
|
-
|
|
129
|
+
## 更新插件
|
|
177
130
|
|
|
178
|
-
|
|
131
|
+
重新安装 npm 最新版本:
|
|
179
132
|
|
|
180
133
|
```bash
|
|
181
|
-
dsh plugin --profile web
|
|
134
|
+
dsh plugin --profile web add dsh-plugin-mobile-gateway@latest
|
|
182
135
|
```
|
|
183
136
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
卸载插件不会自动删除 `~/.dsh/mobile-gateway-devices.json`。
|
|
187
|
-
|
|
188
|
-
## 手动公网部署
|
|
137
|
+
随后停止并重新启动 `dsh web`。
|
|
189
138
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
Nginx 核心配置示例:
|
|
193
|
-
|
|
194
|
-
```nginx
|
|
195
|
-
location = /ws/mobile {
|
|
196
|
-
proxy_pass http://127.0.0.1:3080;
|
|
197
|
-
proxy_http_version 1.1;
|
|
198
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
199
|
-
proxy_set_header Connection "upgrade";
|
|
200
|
-
}
|
|
201
|
-
```
|
|
139
|
+
## 卸载插件
|
|
202
140
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
如需把公网地址写入 profile,可在 `cordis.patch.yml` 的 `mobile-gateway` 配置段覆盖:
|
|
206
|
-
|
|
207
|
-
```yaml
|
|
208
|
-
- id: mobile-gateway
|
|
209
|
-
config:
|
|
210
|
-
path: /ws/mobile
|
|
211
|
-
gatewayEnabled: false
|
|
212
|
-
gatewayWaitTimeoutMs: 300000
|
|
213
|
-
requireAuth: true
|
|
214
|
-
adminLoopbackOnly: true
|
|
215
|
-
lanEnabled: true
|
|
216
|
-
lanHost: 0.0.0.0
|
|
217
|
-
lanPort: 3081
|
|
218
|
-
lanAdvertiseHost: ''
|
|
219
|
-
publicUrl: wss://gateway.example.com/ws/mobile
|
|
220
|
-
publicUrlFile: /etc/dsh-mobile-gateway/public-url
|
|
221
|
-
pairingTtlMs: 300000
|
|
222
|
-
allowQueryToken: false
|
|
141
|
+
```bash
|
|
142
|
+
dsh plugin --profile web remove dsh-plugin-mobile-gateway
|
|
223
143
|
```
|
|
224
144
|
|
|
225
|
-
后置 patch 会替换整个配置段,因此覆盖时应保留所有需要的字段。
|
|
226
|
-
|
|
227
145
|
## 常见问题
|
|
228
146
|
|
|
229
|
-
| 现象 |
|
|
147
|
+
| 现象 | 处理方式 |
|
|
230
148
|
|---|---|
|
|
231
|
-
| WebUI
|
|
232
|
-
| iOS
|
|
233
|
-
| iOS
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
| Certbot 申请或续期失败 | 确认腾讯云安全组和服务器防火墙都允许入站 TCP 80/443,并确认公网 IP 没有变化 |
|
|
238
|
-
| 想检查自动续期 | 执行 `sudo systemctl status dsh-mobile-gateway-cert-renew.timer` 和 `sudo npx --yes dsh-plugin-mobile-gateway status` |
|
|
239
|
-
| 二维码无法再次使用 | 配对码设计为一次性且 5 分钟过期,重新生成即可 |
|
|
240
|
-
| 修改配置后没有生效 | 插件与 profile composition 在启动时加载,需要重启 WebUI 进程 |
|
|
241
|
-
| 需要排查服务端原因 | 查看 `/tmp/mobile-gateway.log`,其中包含连接、鉴权、查询与错误记录 |
|
|
242
|
-
|
|
243
|
-
## 安全说明
|
|
244
|
-
|
|
245
|
-
- 不建议关闭设备鉴权。该开关仅用于本机 Debug,重启后会恢复安全默认值。
|
|
246
|
-
- `/mgw/*` 默认只接受 loopback 请求,不应通过公网反向代理暴露。
|
|
247
|
-
- 长期 token 默认禁止通过 URL query 传输,避免进入代理日志、浏览器历史或监控系统。
|
|
248
|
-
- 配对二维码和长期 token 不会明文写入服务端磁盘。
|
|
149
|
+
| WebUI 没有“移动设备” | 确认安装在 `web` profile,并完整重启 `dsh web` |
|
|
150
|
+
| iOS 收到 `503` | 回到 WebUI 开启“允许移动设备连接” |
|
|
151
|
+
| iOS 收到 `401` | 在 WebUI 重新生成二维码并配对 |
|
|
152
|
+
| 公网连接超时 | 检查云安全组、服务器防火墙和 TCP `80/443` |
|
|
153
|
+
| 公网地址没有自动显示 | 确认已执行 `setup --ip <公网 IP>`,然后重启 `dsh web` |
|
|
154
|
+
| 需要查看服务端日志 | 执行 `tail -f /tmp/mobile-gateway.log` |
|
|
249
155
|
|
|
250
156
|
## 源码开发
|
|
251
157
|
|
|
252
|
-
只有需要修改插件本身时才需要源码安装:
|
|
253
|
-
|
|
254
158
|
```bash
|
|
255
159
|
dsh plugin --profile web add file:/absolute/path/to/dsh-plugin-mobile-gateway
|
|
160
|
+
npm test
|
|
256
161
|
```
|
|
257
162
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
测试命令:
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
# 鉴权、配对与吊销
|
|
264
|
-
NODE_PATH=/path/to/dsh/node_modules node test/auth.test.mjs
|
|
265
|
-
|
|
266
|
-
# mock Harness + 真实 WebSocket 客户端的完整协议链路
|
|
267
|
-
NODE_PATH=/path/to/dsh/node_modules node test/gateway.test.mjs
|
|
268
|
-
|
|
269
|
-
# 公网 IP 安装器的参数、Nginx 隔离与续期配置
|
|
270
|
-
node test/setup-ip.test.mjs
|
|
271
|
-
|
|
272
|
-
# 独立局域网监听、强制鉴权与管理接口隔离
|
|
273
|
-
node test/lan.test.mjs
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
版本历史与全部 WebSocket 消息类型见 [`PROTOCOL.md`](PROTOCOL.md)。
|
|
163
|
+
源码修改后需要重新安装插件并重启 `dsh web`。
|
package/lib/index.mjs
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
// client -> server: { "type": "ping" }
|
|
19
19
|
// { "type": "subscribe", "sessionId": "..." } (optional filter)
|
|
20
20
|
// { "type": "unsubscribe" }
|
|
21
|
-
// { "type": "message", "sessionId"?, "text"
|
|
22
|
-
// "
|
|
21
|
+
// { "type": "message", "sessionId"?, "text"?, "images"?:
|
|
22
|
+
// [{ "mediaType", "data", "name"? }], "mode"?: "queue"|"steer",
|
|
23
|
+
// "workspaceId"?, "cwd"?, "clientTimeZone"? }
|
|
23
24
|
// sessionId omitted -> a new session is created first; the
|
|
24
25
|
// new session can be placed in a workspace via workspaceId
|
|
25
26
|
// (or cwd; at most one, workspaceId wins)
|
|
@@ -30,6 +31,8 @@
|
|
|
30
31
|
// -> raw SessionEvent page (scheme A), capped at maxBytes
|
|
31
32
|
// (default 4 MiB); hasMore + nextBeforeSeq page backward;
|
|
32
33
|
// view:"conversation" trims chunk/header events and tool output
|
|
34
|
+
// { "type": "attachment", "sessionId", "attachmentId" }
|
|
35
|
+
// -> verified image metadata + canonical base64 bytes
|
|
33
36
|
// { "type": "search", "query" } -> session search
|
|
34
37
|
// { "type": "host" } -> host.describe snapshot (incl. default provider/model)
|
|
35
38
|
// { "type": "default-model" } -> agentDefaultModel.currentSelection()
|
|
@@ -50,11 +53,13 @@
|
|
|
50
53
|
// { "type": "set-default", "target": "agent-preset"|"permission", "value" }
|
|
51
54
|
// { "type": "question-answer", "rpcId", "sessionId", "answers": [...] }
|
|
52
55
|
// { "type": "question-cancel", "rpcId", "sessionId" }
|
|
53
|
-
// server -> client: { "kind": "hello", "protocol":
|
|
56
|
+
// server -> client: { "kind": "hello", "protocol": 3, "capabilities": ["images"],
|
|
57
|
+
// "authenticated", "device"?, "port", "clients" }
|
|
54
58
|
// { "kind": "pong", "at" }
|
|
55
59
|
// { "kind": "subscribed", "sessionId" }
|
|
56
60
|
// { "kind": "sent", "sessionId", "mode", "command"? }
|
|
57
61
|
// { "kind": "workspaces" | "sessions" | "history" | "search", ...data }
|
|
62
|
+
// { "kind": "attachment", "sessionId", "attachment", "data" }
|
|
58
63
|
// { "kind": "error", "code", "message", "requestType"?, "sessionId"? }
|
|
59
64
|
// { "kind": "event", "sessionId", "seq", "time", "event": { ... } }
|
|
60
65
|
// { "kind": "question-requested", "rpcId", "sessionId", "questions", "replay"? }
|
|
@@ -85,7 +90,12 @@ const LOG_FILE = '/tmp/mobile-gateway.log'
|
|
|
85
90
|
const DEFAULT_WS_PATH = '/ws/mobile'
|
|
86
91
|
const DEFAULT_PAIRING_TTL_MS = 5 * 60 * 1000
|
|
87
92
|
const DEFAULT_GATEWAY_WAIT_TIMEOUT_MS = 5 * 60 * 1000
|
|
93
|
+
// DSH 0.1.1 allows up to 100 MiB of decoded images in one prompt. Base64 plus
|
|
94
|
+
// the JSON envelope needs roughly 4/3 of that on the wire. Keep this separately
|
|
95
|
+
// configurable so deployments may choose a lower transport ceiling.
|
|
96
|
+
const DEFAULT_MAX_WS_PAYLOAD_BYTES = 144 * 1024 * 1024
|
|
88
97
|
const MAX_MANAGEMENT_BODY_BYTES = 16 * 1024
|
|
98
|
+
const IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
|
|
89
99
|
|
|
90
100
|
// Cordis validates this schema at plugin load and fills these defaults. Keep
|
|
91
101
|
// the defaults conservative: installing the bundle must never create an
|
|
@@ -95,6 +105,7 @@ const Config = Schema.object({
|
|
|
95
105
|
requireAuth: Schema.boolean().default(true),
|
|
96
106
|
gatewayEnabled: Schema.boolean().default(false),
|
|
97
107
|
gatewayWaitTimeoutMs: Schema.natural().min(30_000).max(30 * 60 * 1000).default(DEFAULT_GATEWAY_WAIT_TIMEOUT_MS),
|
|
108
|
+
maxPayloadBytes: Schema.natural().min(1024 * 1024).max(160 * 1024 * 1024).default(DEFAULT_MAX_WS_PAYLOAD_BYTES),
|
|
98
109
|
adminLoopbackOnly: Schema.boolean().default(true),
|
|
99
110
|
publicUrl: Schema.string().default(''),
|
|
100
111
|
deviceFile: Schema.string().default(''),
|
|
@@ -126,20 +137,40 @@ function textOf(blocks) {
|
|
|
126
137
|
return text
|
|
127
138
|
}
|
|
128
139
|
|
|
140
|
+
function imagesOf(blocks) {
|
|
141
|
+
const images = []
|
|
142
|
+
for (const block of blocks) {
|
|
143
|
+
if (!block || block.type !== 'image' || !block.attachment) continue
|
|
144
|
+
const attachment = block.attachment
|
|
145
|
+
images.push({
|
|
146
|
+
attachmentId: attachment.attachmentId,
|
|
147
|
+
mediaType: attachment.mediaType,
|
|
148
|
+
bytes: attachment.bytes,
|
|
149
|
+
width: attachment.width,
|
|
150
|
+
height: attachment.height,
|
|
151
|
+
...(attachment.name ? { name: attachment.name } : {}),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
return images
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
// Build the small, owned JSON wire record for one session event. Reads only
|
|
130
158
|
// leaf fields of the live SessionEvent — never serializes live objects.
|
|
131
159
|
function buildWireEvent(session, event) {
|
|
132
|
-
const base = { sessionId: String(session.id), seq: event.seq, time: event.time }
|
|
160
|
+
const base = { kind: 'event', sessionId: String(session.id), seq: event.seq, time: event.time }
|
|
133
161
|
const d = event.data || {}
|
|
134
162
|
switch (event.type) {
|
|
135
|
-
case 'user/message':
|
|
163
|
+
case 'user/message': {
|
|
164
|
+
const images = imagesOf(d.content || [])
|
|
136
165
|
return Object.assign(base, {
|
|
137
166
|
event: {
|
|
138
167
|
type: 'user/message',
|
|
139
168
|
text: textOf(d.content || []),
|
|
140
169
|
source: d.source && d.source.kind,
|
|
170
|
+
...(images.length ? { images } : {}),
|
|
141
171
|
},
|
|
142
172
|
})
|
|
173
|
+
}
|
|
143
174
|
case 'assistant/chunk': {
|
|
144
175
|
const chunk = d.chunk || {}
|
|
145
176
|
const ev = { type: 'assistant/chunk', turn: d.turn, step: d.step, chunkType: chunk.type }
|
|
@@ -154,6 +185,7 @@ function buildWireEvent(session, event) {
|
|
|
154
185
|
let text = ''
|
|
155
186
|
let reasoning = ''
|
|
156
187
|
const toolCalls = []
|
|
188
|
+
const images = imagesOf(blocks)
|
|
157
189
|
for (const block of blocks) {
|
|
158
190
|
if (!block) continue
|
|
159
191
|
if (block.type === 'text' && typeof block.text === 'string') text += block.text
|
|
@@ -161,7 +193,7 @@ function buildWireEvent(session, event) {
|
|
|
161
193
|
else if (block.type === 'tool-call') toolCalls.push({ id: block.id, name: block.name, arguments: block.arguments })
|
|
162
194
|
}
|
|
163
195
|
return Object.assign(base, {
|
|
164
|
-
event: { type: 'assistant/message', turn: d.turn, step: d.step, text, reasoning, toolCalls },
|
|
196
|
+
event: { type: 'assistant/message', turn: d.turn, step: d.step, text, reasoning, toolCalls, ...(images.length ? { images } : {}) },
|
|
165
197
|
})
|
|
166
198
|
}
|
|
167
199
|
case 'tool/call':
|
|
@@ -203,8 +235,37 @@ function buildWireEvent(session, event) {
|
|
|
203
235
|
// the wire frame to send back, or null when nothing should be sent.
|
|
204
236
|
async function admitMessage(api, msg) {
|
|
205
237
|
const text = typeof msg.text === 'string' ? msg.text.trim() : ''
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
const images = msg.images === undefined ? [] : msg.images
|
|
239
|
+
if (!Array.isArray(images)) {
|
|
240
|
+
return { kind: 'error', code: 'bad-request', message: 'images must be an array' }
|
|
241
|
+
}
|
|
242
|
+
if (images.length > 20) {
|
|
243
|
+
return { kind: 'error', code: 'bad-request', message: 'a message can contain at most 20 images' }
|
|
244
|
+
}
|
|
245
|
+
const imageParts = []
|
|
246
|
+
for (let index = 0; index < images.length; index++) {
|
|
247
|
+
const image = images[index]
|
|
248
|
+
if (!image || typeof image !== 'object') {
|
|
249
|
+
return { kind: 'error', code: 'bad-request', message: `images[${index}] must be an object` }
|
|
250
|
+
}
|
|
251
|
+
if (!IMAGE_MEDIA_TYPES.has(image.mediaType)) {
|
|
252
|
+
return { kind: 'error', code: 'bad-request', message: `images[${index}].mediaType is unsupported` }
|
|
253
|
+
}
|
|
254
|
+
if (typeof image.data !== 'string' || image.data.length === 0) {
|
|
255
|
+
return { kind: 'error', code: 'bad-request', message: `images[${index}].data must be a non-empty base64 string` }
|
|
256
|
+
}
|
|
257
|
+
if (image.name !== undefined && (typeof image.name !== 'string' || image.name.length > 255)) {
|
|
258
|
+
return { kind: 'error', code: 'bad-request', message: `images[${index}].name must be a string of at most 255 characters` }
|
|
259
|
+
}
|
|
260
|
+
imageParts.push({
|
|
261
|
+
type: 'image',
|
|
262
|
+
mediaType: image.mediaType,
|
|
263
|
+
data: image.data,
|
|
264
|
+
...(image.name ? { name: image.name } : {}),
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
if (!text && imageParts.length === 0) {
|
|
268
|
+
return { kind: 'error', code: 'bad-request', message: 'message requires non-empty text or at least one image' }
|
|
208
269
|
}
|
|
209
270
|
const mode = msg.mode === 'steer' ? 'steer' : 'queue'
|
|
210
271
|
|
|
@@ -231,10 +292,16 @@ async function admitMessage(api, msg) {
|
|
|
231
292
|
|
|
232
293
|
const resp = await api.sessions.prompt({
|
|
233
294
|
rpcId: crypto.randomUUID(),
|
|
234
|
-
payload: {
|
|
295
|
+
payload: {
|
|
296
|
+
sessionId,
|
|
297
|
+
mode,
|
|
298
|
+
// Match the official WebUI ordering: images first, optional text last.
|
|
299
|
+
content: [...imageParts, ...(text ? [{ type: 'text', text }] : [])],
|
|
300
|
+
...(typeof msg.clientTimeZone === 'string' && msg.clientTimeZone.trim() ? { clientTimeZone: msg.clientTimeZone.trim() } : {}),
|
|
301
|
+
},
|
|
235
302
|
})
|
|
236
303
|
if (resp.result.ok) {
|
|
237
|
-
log(`mobile message accepted: session=${sessionId} mode=${mode} text="${text.slice(0, 60)}"`)
|
|
304
|
+
log(`mobile message accepted: session=${sessionId} mode=${mode} images=${imageParts.length} text="${text.slice(0, 60)}"`)
|
|
238
305
|
return {
|
|
239
306
|
kind: 'sent',
|
|
240
307
|
sessionId,
|
|
@@ -438,6 +505,20 @@ async function handleQuery(api, typertGateway, agentDefaultModel, msg) {
|
|
|
438
505
|
}
|
|
439
506
|
return frame
|
|
440
507
|
}
|
|
508
|
+
if (msg.type === 'attachment') {
|
|
509
|
+
const sessionId = requireSessionId(msg)
|
|
510
|
+
if (sessionId.error) return sessionId.error
|
|
511
|
+
const attachmentId = typeof msg.attachmentId === 'string' && msg.attachmentId.trim() !== '' ? msg.attachmentId.trim() : null
|
|
512
|
+
if (!attachmentId) {
|
|
513
|
+
return { kind: 'error', code: 'bad-request', message: 'attachment requires an attachmentId', requestType: 'attachment', sessionId: sessionId.value }
|
|
514
|
+
}
|
|
515
|
+
const frame = await proxyQuery(api, 'attachment', api.sessions.attachment.bind(api.sessions), {
|
|
516
|
+
sessionId: sessionId.value,
|
|
517
|
+
attachmentId,
|
|
518
|
+
})
|
|
519
|
+
if (frame.kind === 'attachment') frame.sessionId = sessionId.value
|
|
520
|
+
return frame
|
|
521
|
+
}
|
|
441
522
|
if (msg.type === 'search') {
|
|
442
523
|
const query = typeof msg.query === 'string' ? msg.query.trim() : ''
|
|
443
524
|
if (!query) {
|
|
@@ -827,6 +908,7 @@ const plugin = {
|
|
|
827
908
|
requireAuth: true,
|
|
828
909
|
gatewayEnabled: false,
|
|
829
910
|
gatewayWaitTimeoutMs: DEFAULT_GATEWAY_WAIT_TIMEOUT_MS,
|
|
911
|
+
maxPayloadBytes: DEFAULT_MAX_WS_PAYLOAD_BYTES,
|
|
830
912
|
adminLoopbackOnly: true,
|
|
831
913
|
publicUrl: '',
|
|
832
914
|
deviceFile: '',
|
|
@@ -855,7 +937,7 @@ const plugin = {
|
|
|
855
937
|
const registry = createRegistry(deviceFile, { pairingTtlMs: options.pairingTtlMs })
|
|
856
938
|
const wss = new WebSocketServer({
|
|
857
939
|
noServer: true,
|
|
858
|
-
maxPayload:
|
|
940
|
+
maxPayload: options.maxPayloadBytes,
|
|
859
941
|
// Authentication data may ride in requested subprotocols, but a secret
|
|
860
942
|
// must never be echoed as the negotiated protocol.
|
|
861
943
|
handleProtocols(protocols) {
|
|
@@ -1186,7 +1268,7 @@ const plugin = {
|
|
|
1186
1268
|
admitMessage(api, msg).then((frame) => {
|
|
1187
1269
|
if (frame && ws.readyState === 1) ws.send(JSON.stringify(frame))
|
|
1188
1270
|
})
|
|
1189
|
-
} else if (msg.type === 'workspaces' || msg.type === 'sessions' || msg.type === 'history' ||
|
|
1271
|
+
} else if (msg.type === 'workspaces' || msg.type === 'sessions' || msg.type === 'history' || msg.type === 'attachment' ||
|
|
1190
1272
|
msg.type === 'search' || msg.type === 'host' || msg.type === 'directories' ||
|
|
1191
1273
|
msg.type === 'workspace-create' || msg.type === 'models' || msg.type === 'select-model' ||
|
|
1192
1274
|
msg.type === 'permission-options' || msg.type === 'permission' || msg.type === 'context-usage' ||
|
|
@@ -1217,7 +1299,8 @@ const plugin = {
|
|
|
1217
1299
|
}
|
|
1218
1300
|
ws.send(JSON.stringify({
|
|
1219
1301
|
kind: 'hello',
|
|
1220
|
-
protocol:
|
|
1302
|
+
protocol: 3,
|
|
1303
|
+
capabilities: ['images'],
|
|
1221
1304
|
port: transport.port || webServer.port,
|
|
1222
1305
|
clients: clients.size,
|
|
1223
1306
|
authenticated: !!device,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-mobile-gateway",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "update readme",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin",
|