dsh-plugin-mobile-gateway 0.6.8 → 0.7.0
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 +243 -14
- package/README.md +4 -2
- package/lib/index.mjs +554 -15
- 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 提问与操作审批、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现(v0.
|
|
3
|
+
移动端通过一个经过设备鉴权的 WebSocket 连接与 dsh 通信:订阅 agent 实时输出、发送文字和图片、处理 Human-in-the-loop 提问与操作审批、查询会话/工作区/历史、调整会话配置。本协议由持久化插件 `dsh-plugin-mobile-gateway` 实现(v0.7.0)。
|
|
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", "file-downloads"], "authenticated": true,
|
|
80
|
+
{ "kind": "hello", "protocol": 3, "capabilities": ["images", "commands", "tasks", "goals", "file-downloads"], "authenticated": true,
|
|
81
81
|
"device": { "id": "...", "name": "iPhone" }, "port": 3080, "clients": 1 }
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -343,10 +343,148 @@ Agent 调用 DSH 的 `ask_user_question` 工具时,插件通过 API Gateway
|
|
|
343
343
|
```
|
|
344
344
|
- `sessionId`:可选。省略时**自动创建新会话**(可用 `workspaceId` 或 `cwd` 指定归属工作区,至多一个,workspaceId 优先)
|
|
345
345
|
- `mode`:`"queue"`(排队,默认)/ `"steer"`(打断当前回合)
|
|
346
|
-
- `
|
|
346
|
+
- `message` 始终是用户 Prompt,Gateway 不会猜测或拦截其中的 `/...`。Host 命令必须使用下文的 `command-execute`;技能(如 `/android-cli 连接设备`)仍作为 `message` 发送,Host 会在 pre-step 阶段注入技能内容。
|
|
347
347
|
- `text` 与 `images` 至少提供一项;因此支持纯图片消息
|
|
348
348
|
- `clientTimeZone`:可选 IANA 时区,例如 `Asia/Shanghai`,宿主会校验后记录到这条用户消息
|
|
349
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
|
+
|
|
350
488
|
### 发送图片
|
|
351
489
|
|
|
352
490
|
iOS 将本地图片原始文件数据编码成**标准 Base64**,不要包含 `data:image/...;base64,` 前缀:
|
|
@@ -408,6 +546,8 @@ let image = [
|
|
|
408
546
|
| `search` | `query` | 会话全文搜索 |
|
|
409
547
|
| `session-stats` | `sessionId` | 执行统计投影(输入框统计条数据源) |
|
|
410
548
|
| `context-usage` | `sessionId` | token 用量 + 上下文占用投影 |
|
|
549
|
+
| `tasks` | `sessionId` | 当前任务列表(`todos` projection) |
|
|
550
|
+
| `goal` | `sessionId` | 当前目标及其 CAS 版本(`goal` projection) |
|
|
411
551
|
|
|
412
552
|
### `history` 详细
|
|
413
553
|
```json
|
|
@@ -521,7 +661,93 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
521
661
|
|
|
522
662
|
---
|
|
523
663
|
|
|
524
|
-
## 6.
|
|
664
|
+
## 6. 任务列表与目标
|
|
665
|
+
|
|
666
|
+
WebUI 中的“任务”与“进行中的目标”分别对应 DSH 的 `todos` 和 `goal` session projection。移动端进入会话后应请求 `tasks` 与 `goal` 取得基线;随后以 `tasks-updated` / `goal-updated` 实时更新 UI。
|
|
667
|
+
|
|
668
|
+
### 任务列表
|
|
669
|
+
|
|
670
|
+
```json
|
|
671
|
+
{ "type": "tasks", "sessionId": "session-abc" }
|
|
672
|
+
→ {
|
|
673
|
+
"kind": "tasks",
|
|
674
|
+
"sessionId": "session-abc",
|
|
675
|
+
"asOfSeq": 42,
|
|
676
|
+
"todos": [
|
|
677
|
+
{ "content": "检查 Android SDK", "status": "completed" },
|
|
678
|
+
{ "content": "创建项目", "status": "in_progress" },
|
|
679
|
+
{ "content": "构建 APK", "status": "pending" }
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
- `todos: null` 表示该会话尚未写入过任务列表;客户端可隐藏任务卡片。
|
|
685
|
+
- 任务由 Agent 的 `todo_write` 更新;移动端只读展示,不能直接改写。
|
|
686
|
+
|
|
687
|
+
### 当前目标
|
|
688
|
+
|
|
689
|
+
```json
|
|
690
|
+
{ "type": "goal", "sessionId": "session-abc" }
|
|
691
|
+
→ {
|
|
692
|
+
"kind": "goal",
|
|
693
|
+
"sessionId": "session-abc",
|
|
694
|
+
"asOfSeq": 42,
|
|
695
|
+
"goal": {
|
|
696
|
+
"goal": {
|
|
697
|
+
"id": "goal-opaque-id",
|
|
698
|
+
"revision": 7,
|
|
699
|
+
"objective": "初始化一个 Android app",
|
|
700
|
+
"phase": "active",
|
|
701
|
+
"maxGoalRounds": 12
|
|
702
|
+
},
|
|
703
|
+
"roundsStarted": 3,
|
|
704
|
+
"createdAt": 1787111700000,
|
|
705
|
+
"updatedAt": 1787111800000
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`goal: null` 表示没有当前目标。所有目标写操作必须携带刚读取到的 `{id, revision}`。这是 DSH 的 compare-and-set 保护:当 WebUI 或另一台设备已经修改目标时,宿主拒绝陈旧 revision,移动端应重新请求 `goal` 后再提示用户重试。
|
|
711
|
+
|
|
712
|
+
| type | 参数 | 说明 |
|
|
713
|
+
|---|---|---|
|
|
714
|
+
| `goal-edit` | `sessionId`, `ref`, `objective?`, `maxGoalRounds?` | 修改目标名称(`objective`)或轮数上限,至少提供一项 |
|
|
715
|
+
| `goal-pause` | `sessionId`, `ref` | 暂停当前目标 |
|
|
716
|
+
| `goal-resume` | `sessionId`, `ref` | 继续已暂停/阻塞的目标 |
|
|
717
|
+
| `goal-clear` | `sessionId`, `ref` | 删除当前目标 |
|
|
718
|
+
|
|
719
|
+
更改目标名称:
|
|
720
|
+
|
|
721
|
+
```json
|
|
722
|
+
{ "type": "goal-edit", "sessionId": "session-abc",
|
|
723
|
+
"ref": { "id": "goal-opaque-id", "revision": 7 },
|
|
724
|
+
"objective": "完成 Android app 初始化" }
|
|
725
|
+
→ { "kind": "goal-edit", "sessionId": "session-abc",
|
|
726
|
+
"ref": { "id": "goal-opaque-id", "revision": 8 } }
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
暂停、继续和删除只替换 `type`:
|
|
730
|
+
|
|
731
|
+
```json
|
|
732
|
+
{ "type": "goal-pause", "sessionId": "session-abc", "ref": { "id": "goal-opaque-id", "revision": 8 } }
|
|
733
|
+
→ { "kind": "goal-pause", "sessionId": "session-abc", "ref": { "id": "goal-opaque-id", "revision": 9 } }
|
|
734
|
+
|
|
735
|
+
{ "type": "goal-clear", "sessionId": "session-abc", "ref": { "id": "goal-opaque-id", "revision": 9 } }
|
|
736
|
+
→ { "kind": "goal-clear", "sessionId": "session-abc", "cleared": true }
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### 实时更新
|
|
740
|
+
|
|
741
|
+
```json
|
|
742
|
+
{ "kind": "tasks-updated", "sessionId": "session-abc", "asOfSeq": 43, "todos": [ ... ] }
|
|
743
|
+
{ "kind": "goal-updated", "sessionId": "session-abc", "asOfSeq": 44, "goal": { ... } }
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
服务端只转发 `todos` 与 `goal` projection;客户端按 `asOfSeq` 做高序号覆盖,避免较早推送回写较新的查询结果。
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## 7. 工作区与目录
|
|
525
751
|
|
|
526
752
|
| type | 参数 | 说明 |
|
|
527
753
|
|---|---|---|
|
|
@@ -551,7 +777,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
551
777
|
|
|
552
778
|
---
|
|
553
779
|
|
|
554
|
-
##
|
|
780
|
+
## 8. 模型与思考等级
|
|
555
781
|
|
|
556
782
|
| type | 参数 | 说明 |
|
|
557
783
|
|---|---|---|
|
|
@@ -583,7 +809,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
583
809
|
|
|
584
810
|
---
|
|
585
811
|
|
|
586
|
-
##
|
|
812
|
+
## 9. 权限控制
|
|
587
813
|
|
|
588
814
|
| type | 参数 | 说明 |
|
|
589
815
|
|---|---|---|
|
|
@@ -598,7 +824,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
598
824
|
|
|
599
825
|
---
|
|
600
826
|
|
|
601
|
-
##
|
|
827
|
+
## 10. 新会话默认配置
|
|
602
828
|
|
|
603
829
|
| type | 参数 | 说明 |
|
|
604
830
|
|---|---|---|
|
|
@@ -616,7 +842,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
616
842
|
|
|
617
843
|
---
|
|
618
844
|
|
|
619
|
-
##
|
|
845
|
+
## 11. 分支(fork)
|
|
620
846
|
|
|
621
847
|
```json
|
|
622
848
|
{ "type": "fork", "sessionId": "session-abc", "atSeq": 42 }
|
|
@@ -627,7 +853,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
627
853
|
|
|
628
854
|
---
|
|
629
855
|
|
|
630
|
-
##
|
|
856
|
+
## 12. 宿主信息
|
|
631
857
|
|
|
632
858
|
| type | 返回 |
|
|
633
859
|
|---|---|
|
|
@@ -641,13 +867,14 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
641
867
|
|
|
642
868
|
---
|
|
643
869
|
|
|
644
|
-
##
|
|
870
|
+
## 13. 服务端主动推送
|
|
645
871
|
|
|
646
872
|
| kind | 触发时机 |
|
|
647
873
|
|---|---|
|
|
648
874
|
| `paired` | 首次配对成功;仅此一次返回长期设备 token |
|
|
649
|
-
| `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images","file-downloads"], "authenticated":true, "port":3080, "clients":1 }` |
|
|
875
|
+
| `hello` | 连接成功:`{ "kind":"hello", "protocol":3, "capabilities":["images","commands","tasks","goals","file-downloads"], "authenticated":true, "port":3080, "clients":1 }` |
|
|
650
876
|
| `event` | 任意会话的 agent 输出(见下) |
|
|
877
|
+
| `tasks-updated` / `goal-updated` | 当前会话的任务列表或目标 projection 发生变化 |
|
|
651
878
|
| `question-requested` / `question-resolved` | Human-in-the-loop 问题请求与最终状态 |
|
|
652
879
|
| `approval-requested` / `approval-resolved` | Human-in-the-loop 操作审批请求与最终状态 |
|
|
653
880
|
| `pong` / `subscribed` / `sent` | 对应请求的回复 |
|
|
@@ -667,7 +894,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
667
894
|
|
|
668
895
|
---
|
|
669
896
|
|
|
670
|
-
##
|
|
897
|
+
## 14. 端到端示例(Postman)
|
|
671
898
|
|
|
672
899
|
1. Connect → 收到 `hello`
|
|
673
900
|
2. `{"type":"sessions"}` → 挑 `sessionId`(或直接下一步自动建)
|
|
@@ -680,7 +907,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
680
907
|
|
|
681
908
|
---
|
|
682
909
|
|
|
683
|
-
##
|
|
910
|
+
## 15. 安全注意
|
|
684
911
|
|
|
685
912
|
- `/ws/mobile` 的移动网关默认关闭;本机 WebUI 手动开启后,若 5 分钟内没有设备成功连接会自动关闭
|
|
686
913
|
- 网关开启后仍要求已配对设备凭证;不要把 `requireAuth` 设为 `false` 后暴露到网络
|
|
@@ -695,7 +922,7 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
695
922
|
|
|
696
923
|
---
|
|
697
924
|
|
|
698
|
-
##
|
|
925
|
+
## 16. 版本历史(插件)
|
|
699
926
|
|
|
700
927
|
| 版本 | 新增 |
|
|
701
928
|
|---|---|
|
|
@@ -720,6 +947,8 @@ iOS 用 `Data(base64Encoded:)` 解码并按 `attachment.mediaType` 渲染,建
|
|
|
720
947
|
| v0.6.6 | Human-in-the-loop 操作审批:转发 API Gateway approval 请求、一次性允许/拒绝、重连重放与多端最终状态收敛 |
|
|
721
948
|
| v0.6.7 | 订阅已有 Session 时重放待处理 Human-in-the-loop 请求,并增加 Approval 端到端诊断日志与安装版本标记 |
|
|
722
949
|
| v0.6.8 | 会话工作目录受限的文件列表与分块下载:支持图片、文档、IPA、APK 等普通文件,含连接归属、路径越界防护、取消、超时和 SHA-256 完整性校验 |
|
|
950
|
+
| v0.6.9 | 服务端驱动的命令与技能目录:支持本地化 Hint、通用二级选项、专用命令执行,以及 command/compaction 生命周期事件;Host 命令不再作为用户 Prompt 发送 |
|
|
951
|
+
| v0.7.0 | 任务与 Goal 对齐:任务/Goal 基线查询、`todos`/`goal` 实时投影、Goal 改名、暂停、继续与删除 |
|
|
723
952
|
|
|
724
953
|
---
|
|
725
954
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
# dsh-plugin-mobile-gateway
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、任务列表和当前 Goal 同步及管理、服务端驱动的命令和技能菜单、Human-in-the-loop、图片及文件传输。安装后,Harness WebUI 左侧边栏会出现“移动设备”入口,可直接开启网关、生成配对二维码和管理可信设备。
|
|
8
|
+
|
|
9
|
+
> v0.7.0:移动端现可与 WebUI 对齐展示任务列表和当前 Goal;支持 Goal 改名、暂停、继续和删除,并通过实时 projection 与 revision 校验处理多设备并发更新。
|
|
8
10
|
|
|
9
11
|
- WebSocket:`/ws/mobile`
|
|
10
12
|
- 局域网:`ws://<局域网 IP>:3081/ws/mobile`
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
|
|
14
16
|
## 配套 iOS 客户端
|
|
15
17
|
|
|
16
|
-
[DeepSeek Harness Mobile](https://github.com/Clarklevis1995/dsh-mobile) 是本仓库的兄弟项目。它是面向 iOS 17+ 的 SwiftUI 原生客户端,支持工作区与会话、工作区内创建文件夹、历史和实时对话、图片、Agent 执行轨迹、Human-in-the-loop
|
|
18
|
+
[DeepSeek Harness Mobile](https://github.com/Clarklevis1995/dsh-mobile) 是本仓库的兄弟项目。它是面向 iOS 17+ 的 SwiftUI 原生客户端,支持工作区与会话、工作区内创建文件夹、历史和实时对话、图片、Agent 执行轨迹、Human-in-the-loop,以及由网关配置驱动的命令、技能、模型与权限菜单。
|
|
17
19
|
|
|
18
20
|
<table>
|
|
19
21
|
<tr>
|
package/lib/index.mjs
CHANGED
|
@@ -47,12 +47,20 @@
|
|
|
47
47
|
// { "type": "fork", "sessionId", "atSeq"? } -> branch a new session from a completed turn
|
|
48
48
|
// { "type": "models", "sessionId"? } -> per-session catalog (with sessionId) or global (without)
|
|
49
49
|
// { "type": "providers" } -> configurable provider list (live/dormant)
|
|
50
|
+
// { "type": "commands", "sessionId" } -> slash-command catalog for this session
|
|
51
|
+
// { "type": "command-execute", "sessionId", "line", "images"? } -> execute a Host command
|
|
52
|
+
// { "type": "command-options", "sessionId", "command" } -> normalized submenu options
|
|
53
|
+
// { "type": "command-select", "sessionId", "command", "optionId" } -> apply submenu option
|
|
50
54
|
// { "type": "select-model", "sessionId", "provider", "model", "reasoningEffort"? }
|
|
51
55
|
// { "type": "permission-options", "sessionId"? } -> permission presets (+ session knobs)
|
|
52
56
|
// { "type": "permission", "sessionId", "name" } -> switch preset via /permission command
|
|
53
57
|
// { "type": "context-usage", "sessionId" } -> tokenUsage + contextPressure projections
|
|
54
58
|
// { "type": "session-stats", "sessionId" } -> sessionStats + tokenUsage projections
|
|
55
59
|
// (the input-box stats strip source)
|
|
60
|
+
// { "type": "tasks", "sessionId" } -> current todo-list projection
|
|
61
|
+
// { "type": "goal", "sessionId" } -> current goal projection
|
|
62
|
+
// { "type": "goal-edit", "sessionId", "ref", "objective"?, "maxGoalRounds"? }
|
|
63
|
+
// { "type": "goal-pause"|"goal-resume"|"goal-clear", "sessionId", "ref" }
|
|
56
64
|
// { "type": "agent-presets" } -> preset roster (+ isDefault)
|
|
57
65
|
// { "type": "defaults" } -> default agent preset + default permission
|
|
58
66
|
// { "type": "set-default", "target": "agent-preset"|"permission", "value" }
|
|
@@ -60,12 +68,20 @@
|
|
|
60
68
|
// { "type": "question-cancel", "rpcId", "sessionId" }
|
|
61
69
|
// { "type": "approval-response", "rpcId", "sessionId", "approvalId",
|
|
62
70
|
// "outcome": "allowed-once"|"rejected" }
|
|
63
|
-
// server -> client: { "kind": "hello", "protocol": 3, "capabilities": ["images", "file-downloads"],
|
|
71
|
+
// server -> client: { "kind": "hello", "protocol": 3, "capabilities": ["images", "commands", "tasks", "goals", "file-downloads"],
|
|
64
72
|
// "authenticated", "device"?, "port", "clients" }
|
|
65
73
|
// { "kind": "pong", "at" }
|
|
66
74
|
// { "kind": "subscribed", "sessionId" }
|
|
67
75
|
// { "kind": "sent", "sessionId", "mode", "command"? }
|
|
68
76
|
// { "kind": "workspaces" | "sessions" | "history" | "search", ...data }
|
|
77
|
+
// { "kind": "commands", "sessionId", "groups": [
|
|
78
|
+
// { "id", "title", "items": [{ "id", "name", "description", "ui" }] }
|
|
79
|
+
// ] }
|
|
80
|
+
// { "kind": "command-options", "sessionId", "command", "options": [
|
|
81
|
+
// { "id", "label", "detail"?, "description"?, "selected" }
|
|
82
|
+
// ] }
|
|
83
|
+
// { "kind": "command-executed", "sessionId", "line", "commandId", "result" }
|
|
84
|
+
// { "kind": "command-selected", "sessionId", "command", "selected" }
|
|
69
85
|
// { "kind": "attachment", "sessionId", "attachment", "data" }
|
|
70
86
|
// { "kind": "file-list", "requestId"?, "sessionId", "path", "entries" }
|
|
71
87
|
// { "kind": "file-download-opened", "requestId", "transferId", "sessionId",
|
|
@@ -74,6 +90,12 @@
|
|
|
74
90
|
// { "kind": "file-download-cancelled", "transferId" }
|
|
75
91
|
// { "kind": "error", "code", "message", "requestType"?, "sessionId"? }
|
|
76
92
|
// { "kind": "event", "sessionId", "seq", "time", "event": { ... } }
|
|
93
|
+
// { "kind": "tasks", "sessionId", "asOfSeq", "todos" }
|
|
94
|
+
// { "kind": "goal", "sessionId", "asOfSeq", "goal" }
|
|
95
|
+
// { "kind": "goal-edit"|"goal-pause"|"goal-resume", "sessionId", "ref" }
|
|
96
|
+
// { "kind": "goal-clear", "sessionId", "cleared": true }
|
|
97
|
+
// { "kind": "tasks-updated", "sessionId", "asOfSeq", "todos" }
|
|
98
|
+
// { "kind": "goal-updated", "sessionId", "asOfSeq", "goal" }
|
|
77
99
|
// { "kind": "question-requested", "rpcId", "sessionId", "questions", "replay"? }
|
|
78
100
|
// { "kind": "question-response", "rpcId", "sessionId", "action", "accepted", "reason"? }
|
|
79
101
|
// { "kind": "question-resolved", "rpcId", "sessionId", "outcome" }
|
|
@@ -120,6 +142,42 @@ const INTERACTION_PROTOCOL_REVISION = 'question-approval-v2'
|
|
|
120
142
|
const DEFAULT_MAX_WS_PAYLOAD_BYTES = 144 * 1024 * 1024
|
|
121
143
|
const MAX_MANAGEMENT_BODY_BYTES = 16 * 1024
|
|
122
144
|
const IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
|
|
145
|
+
const COMMAND_UI_OVERRIDES = Object.freeze({
|
|
146
|
+
permission: Object.freeze({
|
|
147
|
+
kind: 'select',
|
|
148
|
+
optionsRequest: 'command-options',
|
|
149
|
+
selectionRequest: 'command-select',
|
|
150
|
+
}),
|
|
151
|
+
model: Object.freeze({
|
|
152
|
+
kind: 'select',
|
|
153
|
+
optionsRequest: 'command-options',
|
|
154
|
+
selectionRequest: 'command-select',
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
const COMMAND_CATALOG_COPY = Object.freeze({
|
|
158
|
+
zh: Object.freeze({
|
|
159
|
+
locale: 'zh-CN',
|
|
160
|
+
commandsTitle: '命令',
|
|
161
|
+
skillsTitle: '技能',
|
|
162
|
+
userOnly: '仅用户',
|
|
163
|
+
modelDescription: '选择本会话使用的模型',
|
|
164
|
+
hints: Object.freeze({
|
|
165
|
+
plan: '描述你的任务以生成计划',
|
|
166
|
+
goal: '输入目标,智能体将持续执行',
|
|
167
|
+
}),
|
|
168
|
+
}),
|
|
169
|
+
en: Object.freeze({
|
|
170
|
+
locale: 'en',
|
|
171
|
+
commandsTitle: 'Commands',
|
|
172
|
+
skillsTitle: 'Skills',
|
|
173
|
+
userOnly: 'user-only',
|
|
174
|
+
modelDescription: 'Select the model for this conversation',
|
|
175
|
+
hints: Object.freeze({
|
|
176
|
+
plan: 'describe your task to generate plan',
|
|
177
|
+
goal: 'describe the objective for a long-running task',
|
|
178
|
+
}),
|
|
179
|
+
}),
|
|
180
|
+
})
|
|
123
181
|
|
|
124
182
|
// Cordis validates this schema at plugin load and fills these defaults. Keep
|
|
125
183
|
// the defaults conservative: installing the bundle must never create an
|
|
@@ -248,6 +306,55 @@ function buildWireEvent(session, event) {
|
|
|
248
306
|
},
|
|
249
307
|
})
|
|
250
308
|
}
|
|
309
|
+
case 'command/run':
|
|
310
|
+
return Object.assign(base, {
|
|
311
|
+
event: {
|
|
312
|
+
type: 'command/run',
|
|
313
|
+
commandId: d.commandId,
|
|
314
|
+
name: d.name,
|
|
315
|
+
...(typeof d.args === 'string' ? { args: d.args } : {}),
|
|
316
|
+
...(d.source ? { source: d.source } : {}),
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
case 'command/done':
|
|
320
|
+
return Object.assign(base, {
|
|
321
|
+
event: {
|
|
322
|
+
type: 'command/done',
|
|
323
|
+
commandId: d.commandId,
|
|
324
|
+
outcome: d.kind,
|
|
325
|
+
...(typeof d.text === 'string' ? { text: d.text } : {}),
|
|
326
|
+
...(typeof d.sourceEventSeq === 'number' ? { sourceEventSeq: d.sourceEventSeq } : {}),
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
case 'compaction/start':
|
|
330
|
+
return Object.assign(base, {
|
|
331
|
+
event: {
|
|
332
|
+
type: 'compaction/start',
|
|
333
|
+
compactionId: d.compactionId,
|
|
334
|
+
...(d.sourceCommandId ? { sourceCommandId: d.sourceCommandId } : {}),
|
|
335
|
+
turn: d.turn ?? null,
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
case 'compaction/summary':
|
|
339
|
+
return Object.assign(base, {
|
|
340
|
+
event: {
|
|
341
|
+
type: 'compaction/summary',
|
|
342
|
+
compactionId: d.compactionId,
|
|
343
|
+
...(d.sourceCommandId ? { sourceCommandId: d.sourceCommandId } : {}),
|
|
344
|
+
shadowedItemCount: Array.isArray(d.shadowedSeqs) ? d.shadowedSeqs.length : null,
|
|
345
|
+
shadowedTokenCount: typeof d.shadowedTokenCount === 'number' ? d.shadowedTokenCount : null,
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
case 'compaction/end':
|
|
349
|
+
return Object.assign(base, {
|
|
350
|
+
event: {
|
|
351
|
+
type: 'compaction/end',
|
|
352
|
+
compactionId: d.compactionId,
|
|
353
|
+
...(d.sourceCommandId ? { sourceCommandId: d.sourceCommandId } : {}),
|
|
354
|
+
turn: d.turn ?? null,
|
|
355
|
+
...(typeof d.error === 'string' ? { error: d.error } : {}),
|
|
356
|
+
},
|
|
357
|
+
})
|
|
251
358
|
case 'turn/start':
|
|
252
359
|
case 'turn/end':
|
|
253
360
|
case 'step/start':
|
|
@@ -260,31 +367,28 @@ function buildWireEvent(session, event) {
|
|
|
260
367
|
}
|
|
261
368
|
}
|
|
262
369
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
async function admitMessage(api, msg) {
|
|
266
|
-
const text = typeof msg.text === 'string' ? msg.text.trim() : ''
|
|
267
|
-
const images = msg.images === undefined ? [] : msg.images
|
|
370
|
+
function parseWireImages(rawImages, requestType) {
|
|
371
|
+
const images = rawImages === undefined ? [] : rawImages
|
|
268
372
|
if (!Array.isArray(images)) {
|
|
269
|
-
return { kind: 'error', code: 'bad-request', message: 'images must be an array' }
|
|
373
|
+
return { error: { kind: 'error', code: 'bad-request', message: 'images must be an array', requestType } }
|
|
270
374
|
}
|
|
271
375
|
if (images.length > 20) {
|
|
272
|
-
return { kind: 'error', code: 'bad-request', message: 'a
|
|
376
|
+
return { error: { kind: 'error', code: 'bad-request', message: 'a request can contain at most 20 images', requestType } }
|
|
273
377
|
}
|
|
274
378
|
const imageParts = []
|
|
275
379
|
for (let index = 0; index < images.length; index++) {
|
|
276
380
|
const image = images[index]
|
|
277
381
|
if (!image || typeof image !== 'object') {
|
|
278
|
-
return { kind: 'error', code: 'bad-request', message: `images[${index}] must be an object
|
|
382
|
+
return { error: { kind: 'error', code: 'bad-request', message: `images[${index}] must be an object`, requestType } }
|
|
279
383
|
}
|
|
280
384
|
if (!IMAGE_MEDIA_TYPES.has(image.mediaType)) {
|
|
281
|
-
return { kind: 'error', code: 'bad-request', message: `images[${index}].mediaType is unsupported
|
|
385
|
+
return { error: { kind: 'error', code: 'bad-request', message: `images[${index}].mediaType is unsupported`, requestType } }
|
|
282
386
|
}
|
|
283
387
|
if (typeof image.data !== 'string' || image.data.length === 0) {
|
|
284
|
-
return { kind: 'error', code: 'bad-request', message: `images[${index}].data must be a non-empty base64 string
|
|
388
|
+
return { error: { kind: 'error', code: 'bad-request', message: `images[${index}].data must be a non-empty base64 string`, requestType } }
|
|
285
389
|
}
|
|
286
390
|
if (image.name !== undefined && (typeof image.name !== 'string' || image.name.length > 255)) {
|
|
287
|
-
return { kind: 'error', code: 'bad-request', message: `images[${index}].name must be a string of at most 255 characters
|
|
391
|
+
return { error: { kind: 'error', code: 'bad-request', message: `images[${index}].name must be a string of at most 255 characters`, requestType } }
|
|
288
392
|
}
|
|
289
393
|
imageParts.push({
|
|
290
394
|
type: 'image',
|
|
@@ -293,6 +397,88 @@ async function admitMessage(api, msg) {
|
|
|
293
397
|
...(image.name ? { name: image.name } : {}),
|
|
294
398
|
})
|
|
295
399
|
}
|
|
400
|
+
return { value: imageParts }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function commandNameOf(line) {
|
|
404
|
+
const trimmed = line.trim()
|
|
405
|
+
if (!trimmed.startsWith('/')) return null
|
|
406
|
+
const end = trimmed.search(/\s/)
|
|
407
|
+
return (end === -1 ? trimmed.slice(1) : trimmed.slice(1, end)) || null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function listHostCommands(typertGateway, sessionId) {
|
|
411
|
+
const listed = await typertGateway.invoke({
|
|
412
|
+
namespace: 'commands',
|
|
413
|
+
method: 'list',
|
|
414
|
+
args: { agentId: sessionId },
|
|
415
|
+
signal: new AbortController().signal,
|
|
416
|
+
})
|
|
417
|
+
if (!Array.isArray(listed)) throw new Error('commands/list returned an invalid catalog')
|
|
418
|
+
return listed
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function executeHostCommand(typertGateway, sessionId, line, images, requestType) {
|
|
422
|
+
try {
|
|
423
|
+
const execution = await typertGateway.invoke({
|
|
424
|
+
namespace: 'commands',
|
|
425
|
+
method: 'execute',
|
|
426
|
+
args: { agentId: sessionId, line, images },
|
|
427
|
+
signal: new AbortController().signal,
|
|
428
|
+
})
|
|
429
|
+
if (execution === undefined || execution === null) {
|
|
430
|
+
return { kind: 'error', code: 'unknown-command', message: `unknown or malformed command: ${line}`, requestType, sessionId }
|
|
431
|
+
}
|
|
432
|
+
log(`command executed: session=${sessionId} line=${JSON.stringify(line)} commandId=${execution.commandId} outcome=${execution.result.kind}`)
|
|
433
|
+
return {
|
|
434
|
+
kind: 'command-executed',
|
|
435
|
+
sessionId,
|
|
436
|
+
line,
|
|
437
|
+
commandId: execution.commandId,
|
|
438
|
+
result: execution.result,
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const code = error && error.code ? error.code : 'internal'
|
|
442
|
+
const message = error && error.message ? error.message : String(error)
|
|
443
|
+
log(`command rejected: session=${sessionId} ${code}: ${message}`)
|
|
444
|
+
return { kind: 'error', code, message, requestType, sessionId }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function admitCommand(typertGateway, msg) {
|
|
449
|
+
const sessionId = requireSessionId(msg)
|
|
450
|
+
if (sessionId.error) return sessionId.error
|
|
451
|
+
const line = typeof msg.line === 'string' ? msg.line.trim() : ''
|
|
452
|
+
const name = commandNameOf(line)
|
|
453
|
+
if (!name) {
|
|
454
|
+
return { kind: 'error', code: 'bad-request', message: 'command-execute requires a slash-prefixed line', requestType: 'command-execute', sessionId: sessionId.value }
|
|
455
|
+
}
|
|
456
|
+
const parsedImages = parseWireImages(msg.images, 'command-execute')
|
|
457
|
+
if (parsedImages.error) return { ...parsedImages.error, sessionId: sessionId.value }
|
|
458
|
+
try {
|
|
459
|
+
const listed = await listHostCommands(typertGateway, sessionId.value)
|
|
460
|
+
const descriptor = listed.find((command) => command && command.name === name)
|
|
461
|
+
if (!descriptor) {
|
|
462
|
+
return { kind: 'error', code: 'unknown-command', message: `command not found: /${name}`, requestType: 'command-execute', sessionId: sessionId.value }
|
|
463
|
+
}
|
|
464
|
+
if (parsedImages.value.length > 0 && descriptor.input?.images !== true) {
|
|
465
|
+
return { kind: 'error', code: 'bad-request', message: `/${name} does not accept image attachments`, requestType: 'command-execute', sessionId: sessionId.value }
|
|
466
|
+
}
|
|
467
|
+
return executeHostCommand(typertGateway, sessionId.value, line, parsedImages.value, 'command-execute')
|
|
468
|
+
} catch (error) {
|
|
469
|
+
const code = error && error.code ? error.code : 'internal'
|
|
470
|
+
const message = error && error.message ? error.message : String(error)
|
|
471
|
+
return { kind: 'error', code, message, requestType: 'command-execute', sessionId: sessionId.value }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Handle one mobile -> agent message through the official host API. Returns
|
|
476
|
+
// the wire frame to send back, or null when nothing should be sent.
|
|
477
|
+
async function admitMessage(api, msg) {
|
|
478
|
+
const text = typeof msg.text === 'string' ? msg.text.trim() : ''
|
|
479
|
+
const parsedImages = parseWireImages(msg.images, 'message')
|
|
480
|
+
if (parsedImages.error) return parsedImages.error
|
|
481
|
+
const imageParts = parsedImages.value
|
|
296
482
|
if (!text && imageParts.length === 0) {
|
|
297
483
|
return { kind: 'error', code: 'bad-request', message: 'message requires non-empty text or at least one image' }
|
|
298
484
|
}
|
|
@@ -782,6 +968,54 @@ function requireSessionId(msg) {
|
|
|
782
968
|
return { value: sessionId }
|
|
783
969
|
}
|
|
784
970
|
|
|
971
|
+
// Goal mutations use DSH's compare-and-set reference. Returning a stale
|
|
972
|
+
// revision is intentionally rejected by DSH instead of overwriting a newer
|
|
973
|
+
// WebUI/mobile update.
|
|
974
|
+
function requireGoalRef(msg, sessionId) {
|
|
975
|
+
const ref = msg.ref
|
|
976
|
+
const id = ref && typeof ref.id === 'string' && ref.id.trim() !== '' ? ref.id.trim() : null
|
|
977
|
+
const revision = ref && typeof ref.revision === 'number' && Number.isSafeInteger(ref.revision) && ref.revision > 0
|
|
978
|
+
? ref.revision
|
|
979
|
+
: null
|
|
980
|
+
if (!id || revision === null) {
|
|
981
|
+
return {
|
|
982
|
+
error: {
|
|
983
|
+
kind: 'error',
|
|
984
|
+
code: 'bad-request',
|
|
985
|
+
message: `${typeof msg.type === 'string' ? msg.type : 'goal mutation'} requires ref.id and a positive integer ref.revision`,
|
|
986
|
+
requestType: typeof msg.type === 'string' ? msg.type : 'goal',
|
|
987
|
+
sessionId,
|
|
988
|
+
},
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return { value: { id, revision } }
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function readSessionProjection(api, type, sessionId, key) {
|
|
995
|
+
const history = await proxyQuery(api, 'history', api.sessions.history.bind(api.sessions), { sessionId })
|
|
996
|
+
if (history.kind !== 'history') return history
|
|
997
|
+
const values = (history.projections && history.projections.values) || {}
|
|
998
|
+
return {
|
|
999
|
+
kind: type,
|
|
1000
|
+
sessionId,
|
|
1001
|
+
asOfSeq: history.projections ? history.projections.asOfSeq : undefined,
|
|
1002
|
+
[key]: Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null,
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function mutateGoal(api, type, method, payload) {
|
|
1007
|
+
if (!api.goals || typeof api.goals[method] !== 'function') {
|
|
1008
|
+
return {
|
|
1009
|
+
kind: 'error',
|
|
1010
|
+
code: 'unsupported',
|
|
1011
|
+
message: 'goal management is unavailable in this DSH host',
|
|
1012
|
+
requestType: type,
|
|
1013
|
+
sessionId: payload.sessionId,
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return proxyQuery(api, type, api.goals[method].bind(api.goals), payload)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
785
1019
|
// ---------------------------------------------------------------------------
|
|
786
1020
|
// History frame sizing: raw session logs can be huge (trajectory, tool output,
|
|
787
1021
|
// context), so a single history response must never blow the client's
|
|
@@ -856,6 +1090,171 @@ function capHistoryEvents(events, maxBytes, trim) {
|
|
|
856
1090
|
return { events: processed.slice(keptStart), bytes: total, dropped: keptStart }
|
|
857
1091
|
}
|
|
858
1092
|
|
|
1093
|
+
function resolveCommandCatalogCopy(locale) {
|
|
1094
|
+
return typeof locale === 'string' && !locale.toLowerCase().startsWith('zh')
|
|
1095
|
+
? COMMAND_CATALOG_COPY.en
|
|
1096
|
+
: COMMAND_CATALOG_COPY.zh
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function commandUiDescriptor(command, copy) {
|
|
1100
|
+
const override = COMMAND_UI_OVERRIDES[command.name]
|
|
1101
|
+
if (override) return { ...override, insertText: `/${command.name}` }
|
|
1102
|
+
if (command.input && typeof command.input.hint === 'string') {
|
|
1103
|
+
const displayHint = copy.hints[command.name]
|
|
1104
|
+
return {
|
|
1105
|
+
kind: 'input',
|
|
1106
|
+
insertText: `/${command.name} `,
|
|
1107
|
+
hint: command.input.hint,
|
|
1108
|
+
...(displayHint ? { displayHint } : {}),
|
|
1109
|
+
images: command.input.images === true,
|
|
1110
|
+
submitRequest: 'command-execute',
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return { kind: 'immediate', submitRequest: 'command-execute', submitText: `/${command.name}` }
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function skillUiDescriptor(skill) {
|
|
1117
|
+
return {
|
|
1118
|
+
kind: 'input',
|
|
1119
|
+
insertText: `/${skill.name} `,
|
|
1120
|
+
images: true,
|
|
1121
|
+
submitRequest: 'message',
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function modelCommandOptionId(provider, model) {
|
|
1126
|
+
return Buffer.from(JSON.stringify([provider, model]), 'utf8').toString('base64url')
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function decodeModelCommandOptionId(optionId) {
|
|
1130
|
+
try {
|
|
1131
|
+
const value = JSON.parse(Buffer.from(optionId, 'base64url').toString('utf8'))
|
|
1132
|
+
if (!Array.isArray(value) || value.length !== 2 || value.some((part) => typeof part !== 'string' || part === '')) return null
|
|
1133
|
+
return { provider: value[0], model: value[1] }
|
|
1134
|
+
} catch {
|
|
1135
|
+
return null
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async function loadCommandOptions(api, command, sessionId) {
|
|
1140
|
+
if (command === 'model') {
|
|
1141
|
+
const frame = await proxyQuery(api, 'models', api.sessions.models.bind(api.sessions), { sessionId })
|
|
1142
|
+
if (frame.kind !== 'models') return frame
|
|
1143
|
+
const options = []
|
|
1144
|
+
for (const group of frame.groups || []) {
|
|
1145
|
+
for (const model of group.models || []) {
|
|
1146
|
+
options.push({
|
|
1147
|
+
id: modelCommandOptionId(String(group.id), String(model.id)),
|
|
1148
|
+
label: String(model.name || model.id),
|
|
1149
|
+
detail: String(group.name || group.id),
|
|
1150
|
+
...(model.description ? { description: String(model.description) } : {}),
|
|
1151
|
+
selected: frame.current && frame.current.provider === group.id && frame.current.model === model.id,
|
|
1152
|
+
})
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return { kind: 'command-options', sessionId, command, options }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (command === 'permission') {
|
|
1159
|
+
const history = await proxyQuery(api, 'history', api.sessions.history.bind(api.sessions), { sessionId })
|
|
1160
|
+
if (history.kind !== 'history') return history
|
|
1161
|
+
const permissions = history.projections && history.projections.values && history.projections.values.permissions
|
|
1162
|
+
if (!permissions || !Array.isArray(permissions.options) || typeof permissions.currentValue !== 'string') {
|
|
1163
|
+
return {
|
|
1164
|
+
kind: 'error',
|
|
1165
|
+
code: 'command-options-unavailable',
|
|
1166
|
+
message: 'permission options are unavailable for this session',
|
|
1167
|
+
requestType: 'command-options',
|
|
1168
|
+
sessionId,
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
kind: 'command-options',
|
|
1173
|
+
sessionId,
|
|
1174
|
+
command,
|
|
1175
|
+
options: permissions.options.map((option) => ({
|
|
1176
|
+
id: String(option.value),
|
|
1177
|
+
label: String(option.name || option.value),
|
|
1178
|
+
...(option.description ? { description: String(option.description) } : {}),
|
|
1179
|
+
selected: option.value === permissions.currentValue,
|
|
1180
|
+
})),
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return {
|
|
1185
|
+
kind: 'error',
|
|
1186
|
+
code: 'bad-request',
|
|
1187
|
+
message: `command does not provide selectable options: ${command}`,
|
|
1188
|
+
requestType: 'command-options',
|
|
1189
|
+
sessionId,
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function selectCommandOption(api, typertGateway, command, sessionId, optionId) {
|
|
1194
|
+
const catalog = await loadCommandOptions(api, command, sessionId)
|
|
1195
|
+
if (catalog.kind !== 'command-options') return catalog
|
|
1196
|
+
const option = catalog.options.find((candidate) => candidate.id === optionId)
|
|
1197
|
+
if (!option) {
|
|
1198
|
+
return {
|
|
1199
|
+
kind: 'error',
|
|
1200
|
+
code: 'bad-request',
|
|
1201
|
+
message: `unknown option for ${command}: ${optionId}`,
|
|
1202
|
+
requestType: 'command-select',
|
|
1203
|
+
sessionId,
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (command === 'permission') {
|
|
1208
|
+
try {
|
|
1209
|
+
const execution = await typertGateway.invoke({
|
|
1210
|
+
namespace: 'commands',
|
|
1211
|
+
method: 'execute',
|
|
1212
|
+
args: { agentId: sessionId, line: '/permission ' + optionId, images: [] },
|
|
1213
|
+
signal: new AbortController().signal,
|
|
1214
|
+
})
|
|
1215
|
+
if (!execution || !execution.result) throw new Error('permission command returned an invalid result')
|
|
1216
|
+
if (execution.result.kind === 'error') {
|
|
1217
|
+
return { kind: 'error', code: 'command-error', message: execution.result.text, requestType: 'command-select', sessionId }
|
|
1218
|
+
}
|
|
1219
|
+
return { kind: 'command-selected', sessionId, command, selected: { ...option, selected: true } }
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
const code = error && error.code ? error.code : 'internal'
|
|
1222
|
+
const message = error && error.message ? error.message : String(error)
|
|
1223
|
+
return { kind: 'error', code, message, requestType: 'command-select', sessionId }
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (command === 'model') {
|
|
1228
|
+
const selection = decodeModelCommandOptionId(optionId)
|
|
1229
|
+
if (!selection) {
|
|
1230
|
+
return { kind: 'error', code: 'bad-request', message: 'invalid model option id', requestType: 'command-select', sessionId }
|
|
1231
|
+
}
|
|
1232
|
+
const models = await proxyQuery(api, 'models', api.sessions.models.bind(api.sessions), { sessionId })
|
|
1233
|
+
if (models.kind !== 'models') return models
|
|
1234
|
+
const group = (models.groups || []).find((candidate) => candidate.id === selection.provider)
|
|
1235
|
+
const model = group && (group.models || []).find((candidate) => candidate.id === selection.model)
|
|
1236
|
+
if (!model) {
|
|
1237
|
+
return { kind: 'error', code: 'bad-request', message: 'model option is no longer available', requestType: 'command-select', sessionId }
|
|
1238
|
+
}
|
|
1239
|
+
const payload = { sessionId, ...selection }
|
|
1240
|
+
const reasoningEffort = models.current && models.current.provider === selection.provider && models.current.model === selection.model
|
|
1241
|
+
? models.current.reasoningEffort
|
|
1242
|
+
: model.reasoning && model.reasoning.defaultEffort
|
|
1243
|
+
if (reasoningEffort) payload.reasoningEffort = reasoningEffort
|
|
1244
|
+
const selected = await proxyQuery(api, 'select-model', api.sessions.selectModel.bind(api.sessions), payload)
|
|
1245
|
+
if (selected.kind !== 'select-model') return selected
|
|
1246
|
+
return {
|
|
1247
|
+
kind: 'command-selected',
|
|
1248
|
+
sessionId,
|
|
1249
|
+
command,
|
|
1250
|
+
selected: { ...option, selected: true },
|
|
1251
|
+
value: selected.selected,
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return { kind: 'error', code: 'bad-request', message: `command is not selectable: ${command}`, requestType: 'command-select', sessionId }
|
|
1256
|
+
}
|
|
1257
|
+
|
|
859
1258
|
// Dispatch one mobile query frame; returns the wire frame to send back.
|
|
860
1259
|
async function handleQuery(api, typertGateway, agentDefaultModel, msg) {
|
|
861
1260
|
if (msg.type === 'workspaces') {
|
|
@@ -986,6 +1385,99 @@ async function handleQuery(api, typertGateway, agentDefaultModel, msg) {
|
|
|
986
1385
|
if (msg.type === 'providers') {
|
|
987
1386
|
return proxyQuery(api, 'providers', api.llm.providers.bind(api.llm), {})
|
|
988
1387
|
}
|
|
1388
|
+
if (msg.type === 'command-execute') {
|
|
1389
|
+
return admitCommand(typertGateway, msg)
|
|
1390
|
+
}
|
|
1391
|
+
if (msg.type === 'commands') {
|
|
1392
|
+
const sessionId = requireSessionId(msg)
|
|
1393
|
+
if (sessionId.error) return sessionId.error
|
|
1394
|
+
const copy = resolveCommandCatalogCopy(msg.locale)
|
|
1395
|
+
try {
|
|
1396
|
+
// This is the same session-scoped catalog used by the official Web UI.
|
|
1397
|
+
// Copy only the stable descriptor fields so no live Typert object crosses
|
|
1398
|
+
// the mobile wire. The Web UI also contributes /model client-side; expose
|
|
1399
|
+
// the equivalent action here so mobile clients can render the same menu.
|
|
1400
|
+
const [listed, skillFrame] = await Promise.all([
|
|
1401
|
+
listHostCommands(typertGateway, sessionId.value),
|
|
1402
|
+
api.skills && typeof api.skills.list === 'function'
|
|
1403
|
+
? proxyQuery(api, 'skills', api.skills.list.bind(api.skills), { sessionId: sessionId.value })
|
|
1404
|
+
: Promise.resolve({ kind: 'error', code: 'unsupported', message: 'skill catalog is unavailable', requestType: 'skills' }),
|
|
1405
|
+
])
|
|
1406
|
+
if (!Array.isArray(listed)) throw new Error('commands/list returned an invalid catalog')
|
|
1407
|
+
const commands = listed
|
|
1408
|
+
.filter((command) => command && typeof command.name === 'string' && typeof command.description === 'string')
|
|
1409
|
+
.map((command) => ({
|
|
1410
|
+
id: `command:${command.name}`,
|
|
1411
|
+
name: command.name,
|
|
1412
|
+
description: command.description,
|
|
1413
|
+
source: 'host',
|
|
1414
|
+
action: 'execute',
|
|
1415
|
+
ui: commandUiDescriptor(command, copy),
|
|
1416
|
+
...(command.input && typeof command.input.hint === 'string'
|
|
1417
|
+
? { input: { hint: command.input.hint, ...(command.input.images === true ? { images: true } : {}) } }
|
|
1418
|
+
: {}),
|
|
1419
|
+
}))
|
|
1420
|
+
if (!commands.some((command) => command.name === 'model')) {
|
|
1421
|
+
commands.push({
|
|
1422
|
+
id: 'command:model',
|
|
1423
|
+
name: 'model',
|
|
1424
|
+
description: copy.modelDescription,
|
|
1425
|
+
source: 'client',
|
|
1426
|
+
action: 'select-model',
|
|
1427
|
+
ui: commandUiDescriptor({ name: 'model' }, copy),
|
|
1428
|
+
})
|
|
1429
|
+
}
|
|
1430
|
+
const skills = skillFrame.kind === 'skills' && Array.isArray(skillFrame.skills)
|
|
1431
|
+
? skillFrame.skills
|
|
1432
|
+
.filter((skill) => skill && typeof skill.name === 'string' && typeof skill.description === 'string')
|
|
1433
|
+
.map((skill) => ({
|
|
1434
|
+
id: `skill:${skill.name}`,
|
|
1435
|
+
name: skill.name,
|
|
1436
|
+
description: skill.modelInvocable === true ? skill.description : `${copy.userOnly} · ${skill.description}`,
|
|
1437
|
+
source: 'skill',
|
|
1438
|
+
action: 'insert',
|
|
1439
|
+
modelInvocable: skill.modelInvocable === true,
|
|
1440
|
+
...(typeof skill.whenToUse === 'string' && skill.whenToUse !== '' ? { whenToUse: skill.whenToUse } : {}),
|
|
1441
|
+
ui: skillUiDescriptor(skill),
|
|
1442
|
+
}))
|
|
1443
|
+
: []
|
|
1444
|
+
const groups = [
|
|
1445
|
+
{ id: 'commands', title: copy.commandsTitle, items: commands },
|
|
1446
|
+
...(skills.length > 0 ? [{ id: 'skills', title: copy.skillsTitle, items: skills }] : []),
|
|
1447
|
+
]
|
|
1448
|
+
const warnings = skillFrame.kind === 'error'
|
|
1449
|
+
? [{ source: 'skills', code: skillFrame.code, message: skillFrame.message }]
|
|
1450
|
+
: []
|
|
1451
|
+
log(`command catalog queried: session=${sessionId.value} commands=${commands.length} skills=${skills.length}`)
|
|
1452
|
+
return {
|
|
1453
|
+
kind: 'commands',
|
|
1454
|
+
sessionId: sessionId.value,
|
|
1455
|
+
locale: copy.locale,
|
|
1456
|
+
groups,
|
|
1457
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
1458
|
+
}
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
const code = error && error.code ? error.code : 'internal'
|
|
1461
|
+
const message = error && error.message ? error.message : String(error)
|
|
1462
|
+
log(`command catalog failed: session=${sessionId.value} ${code}: ${message}`)
|
|
1463
|
+
return { kind: 'error', code, message, requestType: 'commands', sessionId: sessionId.value }
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (msg.type === 'command-options') {
|
|
1467
|
+
const sessionId = requireSessionId(msg)
|
|
1468
|
+
if (sessionId.error) return sessionId.error
|
|
1469
|
+
const command = typeof msg.command === 'string' && msg.command.trim() !== '' ? msg.command.trim() : null
|
|
1470
|
+
if (!command) return { kind: 'error', code: 'bad-request', message: 'command-options requires a command', requestType: 'command-options', sessionId: sessionId.value }
|
|
1471
|
+
return loadCommandOptions(api, command, sessionId.value)
|
|
1472
|
+
}
|
|
1473
|
+
if (msg.type === 'command-select') {
|
|
1474
|
+
const sessionId = requireSessionId(msg)
|
|
1475
|
+
if (sessionId.error) return sessionId.error
|
|
1476
|
+
const command = typeof msg.command === 'string' && msg.command.trim() !== '' ? msg.command.trim() : null
|
|
1477
|
+
const optionId = typeof msg.optionId === 'string' && msg.optionId !== '' ? msg.optionId : null
|
|
1478
|
+
if (!command || !optionId) return { kind: 'error', code: 'bad-request', message: 'command-select requires command and optionId', requestType: 'command-select', sessionId: sessionId.value }
|
|
1479
|
+
return selectCommandOption(api, typertGateway, command, sessionId.value, optionId)
|
|
1480
|
+
}
|
|
989
1481
|
if (msg.type === 'select-model') {
|
|
990
1482
|
const sessionId = requireSessionId(msg)
|
|
991
1483
|
if (sessionId.error) return sessionId.error
|
|
@@ -1081,6 +1573,39 @@ async function handleQuery(api, typertGateway, agentDefaultModel, msg) {
|
|
|
1081
1573
|
contextPressure: values.contextPressure || null,
|
|
1082
1574
|
}
|
|
1083
1575
|
}
|
|
1576
|
+
if (msg.type === 'tasks') {
|
|
1577
|
+
const sessionId = requireSessionId(msg)
|
|
1578
|
+
if (sessionId.error) return sessionId.error
|
|
1579
|
+
return readSessionProjection(api, 'tasks', sessionId.value, 'todos')
|
|
1580
|
+
}
|
|
1581
|
+
if (msg.type === 'goal') {
|
|
1582
|
+
const sessionId = requireSessionId(msg)
|
|
1583
|
+
if (sessionId.error) return sessionId.error
|
|
1584
|
+
return readSessionProjection(api, 'goal', sessionId.value, 'goal')
|
|
1585
|
+
}
|
|
1586
|
+
if (msg.type === 'goal-edit') {
|
|
1587
|
+
const sessionId = requireSessionId(msg)
|
|
1588
|
+
if (sessionId.error) return sessionId.error
|
|
1589
|
+
const ref = requireGoalRef(msg, sessionId.value)
|
|
1590
|
+
if (ref.error) return ref.error
|
|
1591
|
+
const payload = { sessionId: sessionId.value, ref: ref.value }
|
|
1592
|
+
if (typeof msg.objective === 'string' && msg.objective.trim() !== '') payload.objective = msg.objective.trim()
|
|
1593
|
+
if (typeof msg.maxGoalRounds === 'number' && Number.isSafeInteger(msg.maxGoalRounds) && msg.maxGoalRounds > 0) {
|
|
1594
|
+
payload.maxGoalRounds = msg.maxGoalRounds
|
|
1595
|
+
}
|
|
1596
|
+
if (payload.objective === undefined && payload.maxGoalRounds === undefined) {
|
|
1597
|
+
return { kind: 'error', code: 'bad-request', message: 'goal-edit requires a non-empty objective or a positive maxGoalRounds', requestType: 'goal-edit', sessionId: sessionId.value }
|
|
1598
|
+
}
|
|
1599
|
+
return mutateGoal(api, 'goal-edit', 'edit', payload)
|
|
1600
|
+
}
|
|
1601
|
+
if (msg.type === 'goal-pause' || msg.type === 'goal-resume' || msg.type === 'goal-clear') {
|
|
1602
|
+
const sessionId = requireSessionId(msg)
|
|
1603
|
+
if (sessionId.error) return sessionId.error
|
|
1604
|
+
const ref = requireGoalRef(msg, sessionId.value)
|
|
1605
|
+
if (ref.error) return ref.error
|
|
1606
|
+
const method = msg.type === 'goal-pause' ? 'pause' : msg.type === 'goal-resume' ? 'resume' : 'clear'
|
|
1607
|
+
return mutateGoal(api, msg.type, method, { sessionId: sessionId.value, ref: ref.value })
|
|
1608
|
+
}
|
|
1084
1609
|
if (msg.type === 'agent-presets') {
|
|
1085
1610
|
return proxyQuery(api, 'agent-presets', api.agentPresets.list.bind(api.agentPresets), {})
|
|
1086
1611
|
}
|
|
@@ -1824,11 +2349,14 @@ const plugin = {
|
|
|
1824
2349
|
})
|
|
1825
2350
|
} else if (msg.type === 'workspaces' || msg.type === 'sessions' || msg.type === 'history' || msg.type === 'attachment' ||
|
|
1826
2351
|
msg.type === 'search' || msg.type === 'host' || msg.type === 'directories' || msg.type === 'directory-create' ||
|
|
1827
|
-
msg.type === 'workspace-create' || msg.type === 'models' || msg.type === '
|
|
2352
|
+
msg.type === 'workspace-create' || msg.type === 'models' || msg.type === 'commands' || msg.type === 'command-execute' ||
|
|
2353
|
+
msg.type === 'command-options' || msg.type === 'command-select' || msg.type === 'select-model' ||
|
|
1828
2354
|
msg.type === 'permission-options' || msg.type === 'permission' || msg.type === 'context-usage' ||
|
|
1829
2355
|
msg.type === 'agent-presets' || msg.type === 'defaults' || msg.type === 'set-default' ||
|
|
1830
2356
|
msg.type === 'session-stats' || msg.type === 'default-model' ||
|
|
1831
|
-
msg.type === 'save-default-model' || msg.type === 'fork' || msg.type === 'providers'
|
|
2357
|
+
msg.type === 'save-default-model' || msg.type === 'fork' || msg.type === 'providers' ||
|
|
2358
|
+
msg.type === 'tasks' || msg.type === 'goal' || msg.type === 'goal-edit' ||
|
|
2359
|
+
msg.type === 'goal-pause' || msg.type === 'goal-resume' || msg.type === 'goal-clear') {
|
|
1832
2360
|
handleQuery(api, typertGateway, agentDefaultModel, msg).then((frame) => {
|
|
1833
2361
|
if (frame && ws.readyState === 1) ws.send(JSON.stringify(frame))
|
|
1834
2362
|
})
|
|
@@ -1857,7 +2385,7 @@ const plugin = {
|
|
|
1857
2385
|
ws.send(JSON.stringify({
|
|
1858
2386
|
kind: 'hello',
|
|
1859
2387
|
protocol: 3,
|
|
1860
|
-
capabilities: ['images', ...(options.fileDownloadsEnabled ? ['file-downloads'] : [])],
|
|
2388
|
+
capabilities: ['images', 'commands', 'tasks', 'goals', ...(options.fileDownloadsEnabled ? ['file-downloads'] : [])],
|
|
1861
2389
|
port: transport.port || webServer.port,
|
|
1862
2390
|
clients: clients.size,
|
|
1863
2391
|
authenticated: !!device,
|
|
@@ -1989,6 +2517,17 @@ const plugin = {
|
|
|
1989
2517
|
outcome: payload.outcome,
|
|
1990
2518
|
})
|
|
1991
2519
|
log(`approval resolved: rpcId=${rpcId} approvalId=${approvalId} session=${sessionId} outcome=${payload.outcome}`)
|
|
2520
|
+
} else if (payload.type === 'session/projection' && (payload.key === 'todos' || payload.key === 'goal')) {
|
|
2521
|
+
const sessionId = String(payload.sessionId)
|
|
2522
|
+
const kind = payload.key === 'todos' ? 'tasks-updated' : 'goal-updated'
|
|
2523
|
+
const valueKey = payload.key === 'todos' ? 'todos' : 'goal'
|
|
2524
|
+
broadcastInteractionFrame({
|
|
2525
|
+
kind,
|
|
2526
|
+
sessionId,
|
|
2527
|
+
asOfSeq: payload.seq,
|
|
2528
|
+
[valueKey]: payload.value,
|
|
2529
|
+
})
|
|
2530
|
+
log(`projection forwarded: key=${payload.key} session=${sessionId} seq=${payload.seq}`)
|
|
1992
2531
|
} else {
|
|
1993
2532
|
log(`interaction ignored: type=${payload.type}`)
|
|
1994
2533
|
}
|
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.7.0",
|
|
4
|
+
"description": "Secure mobile gateway for DeepSeek Harness with realtime sessions, tasks, goals, commands, approvals, and file transfers",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin",
|