dsh-remote-bridge 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/api/asyncapi.yaml +591 -1
- package/docs/api/openapi.yaml +2 -2
- package/kotlin-sdk/src/main/kotlin/ai/deepseek/dsh/workspace/MobileGatewayClient.kt +437 -0
- package/lib/index.d.ts +100 -3
- package/lib/index.js +296 -19
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img alt="Version" src="https://img.shields.io/badge/version-v2.8.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.8.2-087f8c">
|
|
13
13
|
<img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
|
|
14
14
|
<img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
|
|
@@ -402,7 +402,7 @@ inside the zero-change set, and 4 slots retained with 0 removed.
|
|
|
402
402
|
|
|
403
403
|
## Project
|
|
404
404
|
|
|
405
|
-
- Version: `v2.8.
|
|
405
|
+
- Version: `v2.8.2`
|
|
406
406
|
- **v2.7.1 is a defect-fix release**: the kernel's `sessionController.resolveAgent()` returns an
|
|
407
407
|
`{ agent }` / `{ error }` **envelope**, not the Agent itself (`dsh-api-session-controller/lib/index.js:365-368`
|
|
408
408
|
`liveAgent()` and `:221-246` `resolve()` both have that shape). The bridge used to hand the envelope
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img alt="Version" src="https://img.shields.io/badge/version-v2.8.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.8.2-087f8c">
|
|
13
13
|
<img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
|
|
14
14
|
<img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
|
|
@@ -433,7 +433,7 @@ DSH **0.2.1-rc.3** 隔离实例上**实跑验过**,不是静态推断:插件
|
|
|
433
433
|
|
|
434
434
|
## 项目信息
|
|
435
435
|
|
|
436
|
-
- 当前版本:`v2.8.
|
|
436
|
+
- 当前版本:`v2.8.2`
|
|
437
437
|
- **v2.7.1 是缺陷修复版**:内核 `sessionController.resolveAgent()` 返回的是 `{ agent }` / `{ error }`
|
|
438
438
|
**包装对象**而非 Agent 本体(`dsh-api-session-controller/lib/index.js:365-368` 的 `liveAgent()` 与
|
|
439
439
|
`:221-246` 的 `resolve()` 都是这个形状),桥此前把包装对象直接当 Agent 传给 `fileUploads.upload()`,
|
package/docs/api/asyncapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
asyncapi: 3.0.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile Events
|
|
4
|
-
version: 2.8.
|
|
4
|
+
version: 2.8.2
|
|
5
5
|
description: |
|
|
6
6
|
Live file and authorized DSH chat events. Reconnect clients must refresh
|
|
7
7
|
REST state; unbounded replay is not provided.
|
|
@@ -21,6 +21,13 @@ info:
|
|
|
21
21
|
(message too big).
|
|
22
22
|
- A server ping is sent every 30 seconds; a connection that misses the
|
|
23
23
|
pong for two consecutive intervals is terminated.
|
|
24
|
+
- `/ws/mobile` gives the same three guarantees with its own counters: a
|
|
25
|
+
32-connection limit (checked **before** the upgrade, so the 33rd gets
|
|
26
|
+
HTTP `429` with `Retry-After: 5` rather than a WebSocket close code), a
|
|
27
|
+
30-second ping with two-interval tolerance, and a 512-frame send queue
|
|
28
|
+
whose overflow is reported as `connection-resync-required` (the
|
|
29
|
+
`/ws/mobile` spelling of the `connection.resyncRequired` signal; same
|
|
30
|
+
payload, see `ConnectionResyncRequired`).
|
|
24
31
|
- Each connection has a 512-frame send queue. Under backpressure the
|
|
25
32
|
server first drops rebuildable `chat.message.delta` frames, then other
|
|
26
33
|
rebuildable frames queued ahead, and as a last resort the oldest state
|
|
@@ -35,18 +42,190 @@ servers:
|
|
|
35
42
|
description: Default listener; the bind IP and port are configurable from the local DSH WebUI
|
|
36
43
|
protocol: ws
|
|
37
44
|
pathname: /api/v1/events
|
|
45
|
+
mobile:
|
|
46
|
+
host: 127.0.0.1:3090
|
|
47
|
+
description: |
|
|
48
|
+
Same listener as `local`, different path. `/ws/mobile` is a **bidirectional**
|
|
49
|
+
channel (request/response frames plus server push), unlike the one-way
|
|
50
|
+
`/api/v1/events` broadcast. Port default is 3090; the bind IP/port are
|
|
51
|
+
configurable from the local DSH WebUI.
|
|
52
|
+
protocol: ws
|
|
53
|
+
pathname: /ws/mobile
|
|
38
54
|
channels:
|
|
39
55
|
events:
|
|
40
56
|
address: /api/v1/events
|
|
41
57
|
messages:
|
|
42
58
|
workspaceEvent:
|
|
43
59
|
$ref: '#/components/messages/WorkspaceEvent'
|
|
60
|
+
mobile:
|
|
61
|
+
address: /ws/mobile
|
|
62
|
+
title: Mobile gateway (bidirectional frames)
|
|
63
|
+
description: |
|
|
64
|
+
Mobile-facing gateway added in v2.8: the client sends `{"type": ...}` frames,
|
|
65
|
+
the server answers `{"kind": ...}` frames and may push unsolicited frames
|
|
66
|
+
(`event`, `question-requested`, `approval-requested`, `speech-state`, ...).
|
|
67
|
+
|
|
68
|
+
**WS first, REST fallback.** Every business operation here has a REST
|
|
69
|
+
equivalent under `/api/v1` (same device token, same scope/root/session
|
|
70
|
+
authorization). A client should fall back to REST when the socket cannot be
|
|
71
|
+
established, or when a frame answers `error` with code `NOT_IMPLEMENTED`
|
|
72
|
+
(capability temporarily missing) — not treat it as "the feature does not
|
|
73
|
+
exist". `UNKNOWN_TYPE` means the client sent a frame this server does not
|
|
74
|
+
know at all (a client bug, not a capability gap).
|
|
75
|
+
|
|
76
|
+
## Handshake and authentication
|
|
77
|
+
|
|
78
|
+
- Path: `ws://HOST:PORT/ws/mobile` (upgrade on the main HTTP server).
|
|
79
|
+
- The first requested subprotocol **must** be `dsh-mobile-v1`. A connection
|
|
80
|
+
whose negotiated subprotocol differs is closed with code `4001`.
|
|
81
|
+
- Three credential carriers, all resolved through the same `principal()`
|
|
82
|
+
predicate as `/api/v1` (device token / scopes / roots / revocation):
|
|
83
|
+
1. **First pairing** — subprotocols `dsh-mobile-v1, dsh-pair.<code>` plus
|
|
84
|
+
header `X-DSH-Device-ID` (required; installation-stable id, validated
|
|
85
|
+
only, never persisted) and optional `X-DSH-Device-Name`
|
|
86
|
+
(`Base64(UTF-8)` display name; missing or invalid base64 falls back to
|
|
87
|
+
the device id). On success the server sends `paired`
|
|
88
|
+
(`{token, device}`) **before** `hello`; the pairing code is one-shot.
|
|
89
|
+
2. **Later connections** — subprotocols `dsh-mobile-v1, dsh-auth.<token>`.
|
|
90
|
+
3. **Later connections (header form)** — `Authorization: Bearer <token>`.
|
|
91
|
+
- Credentials in the URL query string are **rejected** (`4001`): a token in
|
|
92
|
+
a URL ends up in access logs.
|
|
93
|
+
- Connection cap is 32 authenticated sockets (configurable). Authentication
|
|
94
|
+
happens **before** counting; the 33rd authenticated upgrade is refused at
|
|
95
|
+
the HTTP Upgrade stage with `429` + `Retry-After: 5`.
|
|
96
|
+
- Every accepted connection receives `hello` as its first frame (after
|
|
97
|
+
`paired` on the pairing flow). `protocol` is currently `3`;
|
|
98
|
+
`capabilities` shares its source with `connection.ready`.
|
|
99
|
+
|
|
100
|
+
## Frame shapes
|
|
101
|
+
|
|
102
|
+
Client → server frames carry `type` (the `MobileClientFrame` schema
|
|
103
|
+
enumerates the full set); server → client frames carry `kind` (the
|
|
104
|
+
`MobileServerFrame` schema enumerates the full set). Unknown `type` answers
|
|
105
|
+
`error` with code `UNKNOWN_TYPE`.
|
|
106
|
+
|
|
107
|
+
## requestId correlation (request/response)
|
|
108
|
+
|
|
109
|
+
`requestId` is an **opaque client-chosen string**. Correlation is
|
|
110
|
+
**uniform**: every frame that answers a client request carries the
|
|
111
|
+
request's `requestId` back verbatim on `data.requestId`.
|
|
112
|
+
|
|
113
|
+
The echo is implemented once, centrally, in
|
|
114
|
+
`MobileGatewayWsHandler.sendFrame` — handlers do not copy the id
|
|
115
|
+
themselves any more (a few still do; the central injection never
|
|
116
|
+
overwrites a value already present in `data`). The id travels through
|
|
117
|
+
`AsyncLocalStorage`, not through instance state, because frame dispatch is
|
|
118
|
+
**concurrent**: `webSocket.on('message')` calls `void
|
|
119
|
+
this.handleMessage(...)` without awaiting, so several requests can be in
|
|
120
|
+
flight on one connection. A shared slot would cross-talk; a per-async-chain
|
|
121
|
+
context cannot.
|
|
122
|
+
|
|
123
|
+
**Correlated (response) frames** — every `kind` that answers a request,
|
|
124
|
+
including `error` and `sent`:
|
|
125
|
+
|
|
126
|
+
`sessions`, `history`, `workspaces`, `workspace-create`, `attachment`,
|
|
127
|
+
`message` (answers with `sent`), `models`, `providers`, `commands`,
|
|
128
|
+
`command-options`, `command-executed`, `command-selected`, `permission`,
|
|
129
|
+
`permission-options`, `agent-presets`, `host`, `default-model`,
|
|
130
|
+
`session-created`, `session-renamed`, `session-archived`,
|
|
131
|
+
`session-cancelled`, `session-stats`, `context-usage`, `tasks`, `goal*`,
|
|
132
|
+
`agent-team`, `subagents`, `schedule-*`, `speech-*` (except the pushes
|
|
133
|
+
below), `file-list`, `directory-create`, `file-resolve`,
|
|
134
|
+
`file-download-open`/`-cancelled`, `file-upload-open`/`-completed`/`-cancelled`,
|
|
135
|
+
`unsubscribe` (answers with `sent`), and **`error`**.
|
|
136
|
+
|
|
137
|
+
**Uncorrelated (unsolicited / streaming) frames** — these are never an
|
|
138
|
+
answer to a specific request, so they never carry `requestId`. The set is
|
|
139
|
+
declared in code as `REQUEST_ID_ECHO_EXCLUDED_KINDS`
|
|
140
|
+
(`src/host/ws-protocol.ts`):
|
|
141
|
+
|
|
142
|
+
- handshake & heartbeat: `hello`, `paired`, `pong`
|
|
143
|
+
- subscription & event stream: `subscribed`, `event`
|
|
144
|
+
- HITL pushes: `question-requested`, `approval-requested`,
|
|
145
|
+
`approval-resolved`, `question-resolved`
|
|
146
|
+
- speech pushes: `speech-state`, `speech-transcript`, `speech-preparing`
|
|
147
|
+
- projection / queue pushes: `tasks-updated`, `goal-updated`,
|
|
148
|
+
`session-title-changed`, `queue-item-updated`
|
|
149
|
+
- chunked transfer pushes: `file-download-chunk`,
|
|
150
|
+
`file-upload-chunk-accepted`
|
|
151
|
+
|
|
152
|
+
> Adding a **push** kind is a two-step change: append it to
|
|
153
|
+
> `REQUEST_ID_ECHO_EXCLUDED_KINDS`, and add a line here. A push kind that
|
|
154
|
+
> is missing from the set will start echoing a stale `requestId` from
|
|
155
|
+
> whatever request happened to be in flight when it fired.
|
|
156
|
+
|
|
157
|
+
Rules:
|
|
158
|
+
|
|
159
|
+
- Absent or empty string ⇒ **no** `requestId` key in the response at all
|
|
160
|
+
(never `null`, never `""`), so a client must not match on falsy values.
|
|
161
|
+
- A non-object `data` (omitted, `null`, or an array) never gets the key.
|
|
162
|
+
- `file-resolve`, `file-download-open`, `file-upload-open` **require**
|
|
163
|
+
`requestId` and answer `error` code `bad-request` without it.
|
|
164
|
+
- Correlation is per-connection; two connections may reuse the same id.
|
|
165
|
+
- Clients must time out unmatched requests themselves — the server does not
|
|
166
|
+
send a "request expired" frame.
|
|
167
|
+
- `error` frames carry **both** `data.requestId` and
|
|
168
|
+
`data.requestType`. `requestId` pins the failure to one in-flight
|
|
169
|
+
request; `requestType` says which request kind it was. Concurrent
|
|
170
|
+
requests of the same kind are distinguishable only by `requestId`.
|
|
171
|
+
|
|
172
|
+
## Backpressure and close codes
|
|
173
|
+
|
|
174
|
+
Identical parameters to `/api/v1/events` (see `MobileConnectionLimits`):
|
|
175
|
+
a 1 MiB per-frame cap, a 512-frame per-connection send queue that drops the
|
|
176
|
+
oldest frame when full, and a heartbeat every 30 s (two missed intervals ⇒
|
|
177
|
+
terminate). Device revocation or an authorization change closes the socket
|
|
178
|
+
with `4001`. `connection.resyncRequired` (delivered as an `event` frame)
|
|
179
|
+
tells the client to re-fetch its baseline over REST; dropped frames are
|
|
180
|
+
never replayed.
|
|
181
|
+
|
|
182
|
+
## Declared but not backed by this kernel build
|
|
183
|
+
|
|
184
|
+
`search`, `directories`, `defaults`, `set-default`, `queue-update`,
|
|
185
|
+
`plugin-catalog`, `plugin-inspect`, `plugin-install`, `plugin-settings`
|
|
186
|
+
answer `error` with code `NOT_IMPLEMENTED` and a human-readable `message`
|
|
187
|
+
pointing at the REST fallback (`requestType` echoes the frame type). They
|
|
188
|
+
are part of the vocabulary precisely so a client can tell "capability
|
|
189
|
+
missing" from "unknown frame".
|
|
190
|
+
messages:
|
|
191
|
+
clientFrame:
|
|
192
|
+
$ref: '#/components/messages/MobileClientFrame'
|
|
193
|
+
serverFrame:
|
|
194
|
+
$ref: '#/components/messages/MobileServerFrame'
|
|
44
195
|
operations:
|
|
45
196
|
receiveEvents:
|
|
46
197
|
action: receive
|
|
47
198
|
channel:
|
|
48
199
|
$ref: '#/channels/events'
|
|
49
200
|
summary: Receive authorized file and chat events after Bearer authentication during upgrade.
|
|
201
|
+
sendMobileClientFrame:
|
|
202
|
+
action: send
|
|
203
|
+
channel:
|
|
204
|
+
$ref: '#/channels/mobile'
|
|
205
|
+
summary: "Send a `{type: ...}` request frame; supply a `requestId` if you need to correlate the response."
|
|
206
|
+
description: |
|
|
207
|
+
The client owns this direction. `subscribe`/`unsubscribe` narrow the pushed
|
|
208
|
+
`event` stream to a session whitelist; `ping` is answered with `pong`;
|
|
209
|
+
everything else maps to a REST-equivalent operation (see `docs/project/API.md`).
|
|
210
|
+
|
|
211
|
+
Every answer frame echoes a non-empty `requestId` verbatim on
|
|
212
|
+
`data.requestId` — see "requestId correlation" on the `mobile` channel.
|
|
213
|
+
receiveMobileServerFrame:
|
|
214
|
+
action: receive
|
|
215
|
+
channel:
|
|
216
|
+
$ref: '#/channels/mobile'
|
|
217
|
+
summary: "Receive `{kind: ...}` response frames plus unsolicited pushes (event, HITL, speech state)."
|
|
218
|
+
description: |
|
|
219
|
+
Every response frame echoes the request's `requestId` on
|
|
220
|
+
`data.requestId` when the request supplied a non-empty one; `error` frames
|
|
221
|
+
do too, so a failure is always attributable to a single in-flight request.
|
|
222
|
+
Unsolicited frames (`hello`, `paired`, `pong`, `subscribed`, `event`,
|
|
223
|
+
`question-requested`, `approval-requested`, `approval-resolved`,
|
|
224
|
+
`question-resolved`, `speech-state`, `speech-transcript`,
|
|
225
|
+
`speech-preparing`, `tasks-updated`, `goal-updated`,
|
|
226
|
+
`session-title-changed`, `queue-item-updated`, `file-download-chunk`,
|
|
227
|
+
`file-upload-chunk-accepted`, `connection-resync-required`) may arrive at
|
|
228
|
+
any time, never carry `requestId`, and must not be mistaken for responses.
|
|
50
229
|
components:
|
|
51
230
|
messages:
|
|
52
231
|
WorkspaceEvent:
|
|
@@ -73,6 +252,16 @@ components:
|
|
|
73
252
|
- $ref: '#/components/schemas/ChatDeliverablesPresented'
|
|
74
253
|
- type: object
|
|
75
254
|
additionalProperties: true
|
|
255
|
+
MobileClientFrame:
|
|
256
|
+
name: MobileClientFrame
|
|
257
|
+
title: "Client → server frame (`{type: ...}`)"
|
|
258
|
+
payload:
|
|
259
|
+
$ref: '#/components/schemas/MobileClientFrame'
|
|
260
|
+
MobileServerFrame:
|
|
261
|
+
name: MobileServerFrame
|
|
262
|
+
title: "Server → client frame (`{kind: ...}`)"
|
|
263
|
+
payload:
|
|
264
|
+
$ref: '#/components/schemas/MobileServerFrame'
|
|
76
265
|
schemas:
|
|
77
266
|
ChatMessageDelta:
|
|
78
267
|
type: object
|
|
@@ -271,6 +460,11 @@ components:
|
|
|
271
460
|
do not recognize this event type ignore it safely. Example:
|
|
272
461
|
`{"id":"<uuid>","type":"connection.resyncRequired","time":1761100000000,
|
|
273
462
|
"data":{"dropped":12,"reason":"backpressure"}}`.
|
|
463
|
+
This payload is shared by both channels: `/api/v1/events` spells the
|
|
464
|
+
frame type `connection.resyncRequired` (dotted, matching its other event
|
|
465
|
+
types), while `/ws/mobile` spells the kind `connection-resync-required`
|
|
466
|
+
(kebab, matching every other kind there). Both are unsolicited and never
|
|
467
|
+
carry `requestId`.
|
|
274
468
|
required: [dropped, reason]
|
|
275
469
|
properties:
|
|
276
470
|
dropped:
|
|
@@ -280,3 +474,399 @@ components:
|
|
|
280
474
|
reason:
|
|
281
475
|
type: string
|
|
282
476
|
const: backpressure
|
|
477
|
+
|
|
478
|
+
# ───────────────────────── /ws/mobile ─────────────────────────
|
|
479
|
+
#
|
|
480
|
+
# 帧形状的事实来源:src/host/ws-protocol.ts(ClientFrameType / ServerFrameKind)、
|
|
481
|
+
# src/host/ws-handler.ts(基础帧 + 域处理器委派)。改一处必须同步另一处,
|
|
482
|
+
# tests/mobile-frame-contract.spec.ts 会做双向比对(联合成员 ⊆ 本文件 enum)。
|
|
483
|
+
|
|
484
|
+
MobileClientFrame:
|
|
485
|
+
type: object
|
|
486
|
+
title: Client → server frame
|
|
487
|
+
description: |
|
|
488
|
+
One text frame from the client, JSON encoded. `type` selects the handler;
|
|
489
|
+
every other field is handler-specific (see `docs/project/API.md` §
|
|
490
|
+
「客户端→服务端帧」 for the per-type field table).
|
|
491
|
+
|
|
492
|
+
`requestId` is optional on most frames and **required** on `file-resolve`,
|
|
493
|
+
`file-download-open` and `file-upload-open`. When present and non-empty it
|
|
494
|
+
is echoed verbatim on `data.requestId` of **whatever frame answers this
|
|
495
|
+
request** — including `error` frames. See "requestId correlation" on the
|
|
496
|
+
`mobile` channel for the full rule set.
|
|
497
|
+
required: [type]
|
|
498
|
+
properties:
|
|
499
|
+
type:
|
|
500
|
+
type: string
|
|
501
|
+
description: |
|
|
502
|
+
The frame type. The enum is the **complete** vocabulary this server
|
|
503
|
+
recognises: members that the current kernel cannot back are answered
|
|
504
|
+
with `error` code `NOT_IMPLEMENTED` (see the channel description),
|
|
505
|
+
and anything outside the enum answers `error` code `UNKNOWN_TYPE`.
|
|
506
|
+
enum:
|
|
507
|
+
# 基础帧(ws-handler.ts 直接处理)
|
|
508
|
+
- ping
|
|
509
|
+
- subscribe
|
|
510
|
+
- unsubscribe
|
|
511
|
+
- message
|
|
512
|
+
- workspaces
|
|
513
|
+
- sessions
|
|
514
|
+
- history
|
|
515
|
+
- attachment
|
|
516
|
+
# 会话生命周期(ws-sessions.ts)
|
|
517
|
+
- session-create
|
|
518
|
+
- session-cancel
|
|
519
|
+
- session-archive
|
|
520
|
+
- session-rename
|
|
521
|
+
- session-agent-preset
|
|
522
|
+
- fork
|
|
523
|
+
- workspace-create
|
|
524
|
+
# 文件浏览 / 目录(ws-files.ts)
|
|
525
|
+
- file-list
|
|
526
|
+
- directory-create
|
|
527
|
+
# 分块传输(ws-file-transfer.ts)
|
|
528
|
+
- file-resolve
|
|
529
|
+
- file-download-open
|
|
530
|
+
- file-download-read
|
|
531
|
+
- file-download-cancel
|
|
532
|
+
- file-upload-open
|
|
533
|
+
- file-upload-chunk
|
|
534
|
+
- file-upload-finish
|
|
535
|
+
- file-upload-cancel
|
|
536
|
+
# 模型 / 设置(ws-models.ts, ws-settings.ts)
|
|
537
|
+
- models
|
|
538
|
+
- select-model
|
|
539
|
+
- default-model
|
|
540
|
+
- save-default-model
|
|
541
|
+
- providers
|
|
542
|
+
- permission-options
|
|
543
|
+
- permission
|
|
544
|
+
# 命令与二级菜单(ws-commands.ts)
|
|
545
|
+
- commands
|
|
546
|
+
- command-execute
|
|
547
|
+
- command-options
|
|
548
|
+
- command-select
|
|
549
|
+
# 预设(ws-presets.ts)
|
|
550
|
+
- agent-presets
|
|
551
|
+
- select-agent-preset
|
|
552
|
+
# 会话投影(ws-goals.ts, ws-team.ts)
|
|
553
|
+
- context-usage
|
|
554
|
+
- session-stats
|
|
555
|
+
- tasks
|
|
556
|
+
- goal
|
|
557
|
+
- goal-edit
|
|
558
|
+
- goal-pause
|
|
559
|
+
- goal-resume
|
|
560
|
+
- goal-clear
|
|
561
|
+
- agent-team
|
|
562
|
+
- subagents
|
|
563
|
+
# HITL(ws-hitl.ts)
|
|
564
|
+
- question-answer
|
|
565
|
+
- question-cancel
|
|
566
|
+
- approval-response
|
|
567
|
+
# 主机快照(ws-host.ts)
|
|
568
|
+
- host
|
|
569
|
+
# 语音(ws-speech.ts)
|
|
570
|
+
- speech-catalog
|
|
571
|
+
- speech-subscribe
|
|
572
|
+
- speech-unsubscribe
|
|
573
|
+
- speech-configure
|
|
574
|
+
- speech-prepare
|
|
575
|
+
- speech-cancel-preparation
|
|
576
|
+
- speech-transcribe
|
|
577
|
+
- speech-cancel
|
|
578
|
+
# 定时任务(ws-schedule.ts)
|
|
579
|
+
- schedule-catalog
|
|
580
|
+
- schedule-list
|
|
581
|
+
- schedule-history
|
|
582
|
+
- schedule-update
|
|
583
|
+
- schedule-delete
|
|
584
|
+
# 声明但本内核无对应 RPC → NOT_IMPLEMENTED + REST 兜底
|
|
585
|
+
- search
|
|
586
|
+
- directories
|
|
587
|
+
- defaults
|
|
588
|
+
- set-default
|
|
589
|
+
- queue-update
|
|
590
|
+
- plugin-catalog
|
|
591
|
+
- plugin-inspect
|
|
592
|
+
- plugin-install
|
|
593
|
+
- plugin-settings
|
|
594
|
+
requestId:
|
|
595
|
+
type: string
|
|
596
|
+
minLength: 1
|
|
597
|
+
description: |
|
|
598
|
+
Opaque client-chosen correlation id. Echoed verbatim on the answer
|
|
599
|
+
frame's `data.requestId` when non-empty; absent (or `""`) ⇒ the answer
|
|
600
|
+
has no `requestId` key at all.
|
|
601
|
+
sessionId:
|
|
602
|
+
type: string
|
|
603
|
+
description: Target session for session-scoped frames; `subscribe` requires it non-empty.
|
|
604
|
+
additionalProperties: true
|
|
605
|
+
|
|
606
|
+
MobileServerFrame:
|
|
607
|
+
type: object
|
|
608
|
+
title: Server → client frame
|
|
609
|
+
description: |
|
|
610
|
+
One text frame from the server, JSON encoded. `kind` selects the shape of
|
|
611
|
+
`data`. Every answer to a client request carries `data.requestId` when
|
|
612
|
+
the request supplied a non-empty one — `error` frames included. Frames
|
|
613
|
+
that are not an answer to a specific request never carry it: `hello`,
|
|
614
|
+
`paired`, `pong`, `subscribed`, `event`, the HITL pushes
|
|
615
|
+
(`question-requested`, `approval-requested`, `approval-resolved`,
|
|
616
|
+
`question-resolved`), the speech pushes (`speech-state`,
|
|
617
|
+
`speech-transcript`, `speech-preparing`), the projection/queue pushes
|
|
618
|
+
(`tasks-updated`, `goal-updated`, `session-title-changed`,
|
|
619
|
+
`queue-item-updated`) and the chunked-transfer pushes
|
|
620
|
+
(`file-download-chunk`, `file-upload-chunk-accepted`). They may arrive at
|
|
621
|
+
any time. The exclusion set lives in `REQUEST_ID_ECHO_EXCLUDED_KINDS`
|
|
622
|
+
(`src/host/ws-protocol.ts`) — see "requestId correlation" on the `mobile`
|
|
623
|
+
channel.
|
|
624
|
+
|
|
625
|
+
Note: `id`/`time` envelope fields are **not** stamped on `/ws/mobile`
|
|
626
|
+
frames (that envelope belongs to `/api/v1/events`); a client must key on
|
|
627
|
+
`kind` plus its own `requestId`.
|
|
628
|
+
required: [kind]
|
|
629
|
+
properties:
|
|
630
|
+
kind:
|
|
631
|
+
type: string
|
|
632
|
+
description: |
|
|
633
|
+
The frame kind. The enum is the complete set this server can emit.
|
|
634
|
+
`hello` is always the first frame on an authenticated connection
|
|
635
|
+
(`paired` precedes it on the pairing flow).
|
|
636
|
+
enum:
|
|
637
|
+
# 握手
|
|
638
|
+
- hello
|
|
639
|
+
- pong
|
|
640
|
+
- paired
|
|
641
|
+
# 基础帧应答
|
|
642
|
+
- subscribed
|
|
643
|
+
- sent
|
|
644
|
+
- error
|
|
645
|
+
- event
|
|
646
|
+
# 查询应答
|
|
647
|
+
- workspaces
|
|
648
|
+
- sessions
|
|
649
|
+
- history
|
|
650
|
+
- search
|
|
651
|
+
- attachment
|
|
652
|
+
# 文件
|
|
653
|
+
- file-list
|
|
654
|
+
- directory-create
|
|
655
|
+
- file-resolved
|
|
656
|
+
- file-download-opened
|
|
657
|
+
- file-download-chunk
|
|
658
|
+
- file-download-cancelled
|
|
659
|
+
- file-upload-opened
|
|
660
|
+
- file-upload-chunk-accepted
|
|
661
|
+
- file-upload-completed
|
|
662
|
+
- file-upload-cancelled
|
|
663
|
+
# 会话生命周期
|
|
664
|
+
- session-created
|
|
665
|
+
- session-cancelled
|
|
666
|
+
- session-archived
|
|
667
|
+
- session-renamed
|
|
668
|
+
- session-archives
|
|
669
|
+
- session-title-changed
|
|
670
|
+
- fork
|
|
671
|
+
- workspace-create
|
|
672
|
+
- queue-item-updated
|
|
673
|
+
- session-queues
|
|
674
|
+
- session-queue
|
|
675
|
+
# 模型 / 设置
|
|
676
|
+
- models
|
|
677
|
+
- default-model
|
|
678
|
+
- providers
|
|
679
|
+
- permission
|
|
680
|
+
- permission-options
|
|
681
|
+
- agent-presets
|
|
682
|
+
# 命令
|
|
683
|
+
- commands
|
|
684
|
+
- command-options
|
|
685
|
+
- command-executed
|
|
686
|
+
- command-selected
|
|
687
|
+
# 投影
|
|
688
|
+
- tasks
|
|
689
|
+
- goal
|
|
690
|
+
- goal-edit
|
|
691
|
+
- goal-pause
|
|
692
|
+
- goal-resume
|
|
693
|
+
- goal-clear
|
|
694
|
+
- tasks-updated
|
|
695
|
+
- goal-updated
|
|
696
|
+
- session-stats
|
|
697
|
+
- context-usage
|
|
698
|
+
- agent-team
|
|
699
|
+
- subagents
|
|
700
|
+
- host
|
|
701
|
+
# HITL
|
|
702
|
+
- question-requested
|
|
703
|
+
- question-response
|
|
704
|
+
- question-resolved
|
|
705
|
+
- approval-requested
|
|
706
|
+
- approval-response
|
|
707
|
+
- approval-resolved
|
|
708
|
+
# 语音
|
|
709
|
+
- speech-catalog
|
|
710
|
+
- speech-subscribed
|
|
711
|
+
- speech-unsubscribed
|
|
712
|
+
- speech-state
|
|
713
|
+
- speech-configured
|
|
714
|
+
- speech-preparing
|
|
715
|
+
- speech-preparation-cancelled
|
|
716
|
+
- speech-transcript
|
|
717
|
+
- speech-cancelled
|
|
718
|
+
# 定时任务
|
|
719
|
+
- schedule-catalog
|
|
720
|
+
- schedule-list
|
|
721
|
+
- schedule-history
|
|
722
|
+
- schedule-update
|
|
723
|
+
- schedule-delete
|
|
724
|
+
# 连接与反压
|
|
725
|
+
- connection-resync-required
|
|
726
|
+
data:
|
|
727
|
+
description: |
|
|
728
|
+
Kind-specific payload. `hello` uses `MobileHello`, `paired` uses
|
|
729
|
+
`MobilePaired`, `error` uses `MobileError`, `event` reuses
|
|
730
|
+
`WorkspaceEvent`. Other kinds are documented field-by-field in
|
|
731
|
+
`docs/project/API.md` and typed in the Kotlin SDK.
|
|
732
|
+
additionalProperties: true
|
|
733
|
+
|
|
734
|
+
MobileHello:
|
|
735
|
+
type: object
|
|
736
|
+
title: hello (first frame on every authenticated connection)
|
|
737
|
+
description: |
|
|
738
|
+
Sent immediately after the upgrade (and after `paired` on the pairing
|
|
739
|
+
flow). `capabilities` shares its source with `connection.ready`
|
|
740
|
+
(`src/host/capabilities.ts`), names are `<feature>-v<major>` and are never
|
|
741
|
+
silently removed — use them for feature detection instead of comparing
|
|
742
|
+
versions. `port` is the local port this connection arrived on; `clients`
|
|
743
|
+
is the current authenticated connection count. `device` is omitted for
|
|
744
|
+
loopback-admin principals (they are not devices).
|
|
745
|
+
required: [protocol, capabilities, authenticated]
|
|
746
|
+
properties:
|
|
747
|
+
protocol:
|
|
748
|
+
type: integer
|
|
749
|
+
const: 3
|
|
750
|
+
description: Mobile gateway protocol revision.
|
|
751
|
+
capabilities:
|
|
752
|
+
type: array
|
|
753
|
+
items: { type: string }
|
|
754
|
+
authenticated:
|
|
755
|
+
type: boolean
|
|
756
|
+
const: true
|
|
757
|
+
device:
|
|
758
|
+
type: object
|
|
759
|
+
required: [id, name, scopes, rootIds]
|
|
760
|
+
properties:
|
|
761
|
+
id: { type: string }
|
|
762
|
+
name: { type: string }
|
|
763
|
+
scopes:
|
|
764
|
+
type: array
|
|
765
|
+
items:
|
|
766
|
+
type: string
|
|
767
|
+
enum: [chat.read, chat.write, files.read, files.write, files.delete, settings.read, settings.write]
|
|
768
|
+
rootIds:
|
|
769
|
+
type: array
|
|
770
|
+
items: { type: string }
|
|
771
|
+
port: { type: integer }
|
|
772
|
+
clients: { type: integer, minimum: 1 }
|
|
773
|
+
|
|
774
|
+
MobilePaired:
|
|
775
|
+
type: object
|
|
776
|
+
title: paired (pairing flow only, before hello)
|
|
777
|
+
description: |
|
|
778
|
+
Carries the freshly minted device token. It is shown **once** — the
|
|
779
|
+
pairing code itself is one-shot, and reusing it closes the next
|
|
780
|
+
connection with `4001`.
|
|
781
|
+
required: [token, device]
|
|
782
|
+
properties:
|
|
783
|
+
token: { type: string }
|
|
784
|
+
device:
|
|
785
|
+
type: object
|
|
786
|
+
required: [id, name]
|
|
787
|
+
properties:
|
|
788
|
+
id: { type: string }
|
|
789
|
+
name: { type: string }
|
|
790
|
+
|
|
791
|
+
MobileError:
|
|
792
|
+
type: object
|
|
793
|
+
title: error
|
|
794
|
+
description: |
|
|
795
|
+
`code` distinguishes three very different situations: `NOT_IMPLEMENTED`
|
|
796
|
+
(this kernel build cannot back the frame — fall back to REST),
|
|
797
|
+
`UNKNOWN_TYPE` (the client sent a frame outside the vocabulary — a client
|
|
798
|
+
bug), and everything else (`MISSING_PARAMS`, `FORBIDDEN_SCOPE`,
|
|
799
|
+
`bad-request`, `PATH_OUTSIDE_ROOTS`, `INTERNAL_ERROR`, upstream codes…).
|
|
800
|
+
`message` never contains absolute host paths.
|
|
801
|
+
required: [code, message]
|
|
802
|
+
properties:
|
|
803
|
+
code: { type: string }
|
|
804
|
+
message: { type: string }
|
|
805
|
+
requestType:
|
|
806
|
+
type: string
|
|
807
|
+
description: The offending frame's `type` (or `kind` for a rejected response).
|
|
808
|
+
requestId:
|
|
809
|
+
type: string
|
|
810
|
+
description: |
|
|
811
|
+
The offending request's `requestId`, echoed so the failure can be
|
|
812
|
+
pinned to one in-flight request. Absent when the request carried none.
|
|
813
|
+
statusCode:
|
|
814
|
+
type: integer
|
|
815
|
+
description: Present when the failure originated from an HTTP-status-shaped upstream error.
|
|
816
|
+
|
|
817
|
+
MobileConnectionLimits:
|
|
818
|
+
type: object
|
|
819
|
+
title: Backpressure and connection limits
|
|
820
|
+
description: |
|
|
821
|
+
Constants shared with `/api/v1/events` (source of truth
|
|
822
|
+
`src/host/ws-protocol.ts` / `src/host/server.ts`). Clients should treat
|
|
823
|
+
these as documented defaults, not hard-coded contracts: they are
|
|
824
|
+
server-configurable, and the observable behaviour (`1013` close,
|
|
825
|
+
`resyncRequired`) is the real contract.
|
|
826
|
+
properties:
|
|
827
|
+
maxFrameBytes:
|
|
828
|
+
type: integer
|
|
829
|
+
const: 1048576
|
|
830
|
+
description: 1 MiB per frame; larger frames are refused with close `1009`.
|
|
831
|
+
maxWebSocketPayloadBytes:
|
|
832
|
+
type: integer
|
|
833
|
+
const: 150994944
|
|
834
|
+
description: 144 MiB total buffered payload per connection (`DEFAULT_MAX_WS_PAYLOAD_BYTES`).
|
|
835
|
+
sendQueueFrames:
|
|
836
|
+
type: integer
|
|
837
|
+
const: 512
|
|
838
|
+
description: Per-connection send queue; on overflow the oldest frame is dropped and a `resyncRequired` is queued.
|
|
839
|
+
heartbeatIntervalMs:
|
|
840
|
+
type: integer
|
|
841
|
+
const: 30000
|
|
842
|
+
description: Server ping cadence; two missed pongs terminate the connection.
|
|
843
|
+
maxConnections:
|
|
844
|
+
type: integer
|
|
845
|
+
const: 32
|
|
846
|
+
description: "Authenticated connections; the next upgrade gets HTTP 429 + `Retry-After: 5`."
|
|
847
|
+
pairingTtlMs:
|
|
848
|
+
type: integer
|
|
849
|
+
const: 600000
|
|
850
|
+
description: Pairing code lifetime (10 minutes, one-shot).
|
|
851
|
+
closeDeviceRevoked:
|
|
852
|
+
type: integer
|
|
853
|
+
const: 4001
|
|
854
|
+
description: Close code for revoked devices, failed authentication, or a wrong subprotocol.
|
|
855
|
+
fileDownload:
|
|
856
|
+
type: object
|
|
857
|
+
properties:
|
|
858
|
+
maxBytes: { type: integer, const: 536870912 }
|
|
859
|
+
chunkBytes: { type: integer, const: 524288 }
|
|
860
|
+
idleMs: { type: integer, const: 120000 }
|
|
861
|
+
maxTransfers: { type: integer, const: 4 }
|
|
862
|
+
fileUpload:
|
|
863
|
+
type: object
|
|
864
|
+
properties:
|
|
865
|
+
maxBytes: { type: integer, const: 536870912 }
|
|
866
|
+
chunkBytes: { type: integer, const: 262144 }
|
|
867
|
+
idleMs: { type: integer, const: 120000 }
|
|
868
|
+
maxTransfers: { type: integer, const: 4 }
|
|
869
|
+
interactionProtocolRevision:
|
|
870
|
+
type: string
|
|
871
|
+
const: question-approval-v2
|
|
872
|
+
description: HITL (question/approval) frame revision.
|