dsh-cherry-bridge 0.0.0-stage → 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SAXEM1997
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,239 @@
1
+ <div align="center">
2
+
3
+ **English** | [简体中文](README.md)
4
+
5
+ </div>
6
+
7
+ <div align="center">
8
+
9
+ <img src="static/logo.png" alt="dsh-cherry-bridge" width="880">
10
+
11
+ </div>
12
+
13
+ # dsh-cherry-bridge
14
+
15
+ **Bridge DeepSeek Harness to Cherry Studio desktops** — a DSH plugin
16
+
17
+ Drive Cherry Studio agents on your LAN straight from a DSH session: pair, list
18
+ devices/sessions, read history, send messages and wait for a terminal state,
19
+ answer approval and question cards, cancel executions — plus a same-origin
20
+ settings page for device and tool-scope management.
21
+
22
+ ```mermaid
23
+ flowchart TB
24
+ T["DSH sessions<br/>14 cherry_* tools"] --> P
25
+ W["Settings page<br/>devices · pairing · scope"] --> P
26
+ P["dsh-cherry-bridge<br/>pool · subscriptions · scope"]
27
+ P <-->|"ws:// · Noise XX"| C["Cherry Studio desktop<br/>QR pairing · approvals"]
28
+ ```
29
+
30
+ - Package: `dsh-cherry-bridge`, plugin id: `cherry-remote`
31
+ - Transport: `ws://` + Noise XX (encryption lives in the Noise record layer; no TLS termination required)
32
+ - Tool face: 14 `cherry_*` tools, enabled **per workspace**, and by default **nobody has them** (see “Scope”)
33
+ - Settings page and HTTP face: the same-origin prefix `/cherry-remote/api` on the host web server
34
+
35
+ ## Install
36
+
37
+ > **DSH version requirement**: `>=0.2.0-rc.2 <0.3.0-0` (declared via `engines.dsh` in `package.json`; when unmet the bundle is skipped and its settings-page row disabled — `dsh plugin allow-version` grants an explicit exemption; 0.3.0 needs a fresh compatibility pass before the range widens).
38
+
39
+ ```bash
40
+ # From npm (recommended, pinned)
41
+ npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge@0.1.6
42
+
43
+ # Install the latest npm release
44
+ npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge
45
+
46
+ # From GitHub source (same commit as the npm release)
47
+ npx @deepseek-ai/dsh plugin --profile web add github:SAXEM1997/dsh-cherry-bridge#v0.1.6
48
+ ```
49
+
50
+ The package ships `cordis.patch.yml` (`package.json`'s `dsh.bundle.patch` points
51
+ at it); dsh-plugin-manager inserts the plugin line from it. The manual profile
52
+ line is equivalent to:
53
+
54
+ ```yaml
55
+ - id: cherry-remote
56
+ name: 'dsh-cherry-bridge'
57
+ config:
58
+ perSessionCacheBytes: 2097152
59
+ rateBudgetPerSecond: 32
60
+ ```
61
+
62
+ ### First pairing
63
+
64
+ 1. On the Cherry Studio desktop open **remote access → add device**: that
65
+ screen shows a **QR code**; the pairing information only appears on the page
66
+ the code leads to.
67
+ 2. **Scan the QR code** (WeChat, any browser, …) and copy the pairing
68
+ information the scanned page displays.
69
+ 3. In DSH call `cherry_pair` (the settings-page wizard works too) and paste it.
70
+ 4. **Cross-check the 6-digit verification code both ways** against the
71
+ desktop's number before approving on the desktop side.
72
+ 5. After approval the device archive is written to the data directory
73
+ (`devices.json`, `0600`) with the `desktopIdentity` **pinned**; every later
74
+ connection verifies the Noise peer static key against the pin, so a hijacked
75
+ QR code cannot be traded for a man-in-the-middle substitute.
76
+
77
+ ## Quick start
78
+
79
+ ```text
80
+ cherry_pair → paste the pairing info from the scanned page, check the 6-digit code
81
+ cherry_devices → see devices and connection state (a prefix selects one, e.g. "dev-A")
82
+ cherry_agents → list the device's agents, take an agentId
83
+ cherry_sessions → list an agent's sessions, take a sessionId
84
+ cherry_read → read history (defaults to the newest 100, newest-first)
85
+ cherry_send → send a message and wait for the terminal state; returns the assistant text
86
+ cherry_interactions → see pending question/approval cards; cherry_respond answers them
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ | Key | Type | Default | Notes |
92
+ | --- | --- | --- | --- |
93
+ | `dataDir` | string | `$DSH_HOME/cherry-remote` (fallback `~/.dsh/cherry-remote`) | Plugin data directory holding `identity.key` and `devices.json` (both `0600`). |
94
+ | `perSessionCacheBytes` | number | `2097152` (2 MiB) | Per-session cap on the **local** subscription event cache; beyond it the oldest events are dropped LRU-style with a `warn`. |
95
+ | `rateBudgetPerSecond` | number | `32` | Per-device smoothed request budget (req/s). The desktop's hard limit is 64 req/s with no negotiation channel; exceeding it terminates the session with close `1011` — hence the conservative value. |
96
+
97
+ > ⚠️ `cordis.patch.yml`'s `config` is **whole-replace**, not merged with the
98
+ > defaults. Removing a key gets you the plugin default; a key that exists but is
99
+ > unusable (`0`, negative, a string) falls back with a named `warn`.
100
+
101
+ ## Tools
102
+
103
+ | Tool | What it does |
104
+ | --- | --- |
105
+ | `cherry_devices` | List every paired device and its connection state (`connected`/`connecting`/`offline`/`disabled`/`identity-mismatch`). |
106
+ | `cherry_agents` | List the agents available on a device (paginated). A truncated answer always carries `truncated` + `truncationReason` (`limit` or `safety-bound`, the latter the 1000-item internal ceiling), plus a `resumeCursor`: replay it as `cursor` with the same device and filters to continue from that stop point (`limit` then bounds the next answer). A token from another tool, device or filter set is refused **locally**, never forwarded. |
107
+ | `cherry_workspaces` | List an agent's **registered** workspaces, and whether a system workspace exists. A truncated answer always carries `truncated` + `truncationReason` and a `resumeCursor` — replay it as `cursor` with the same filters to continue. |
108
+ | `cherry_sessions` | List an agent's sessions, optionally filtered by workspace. A truncated answer always carries `truncated` + `truncationReason` (`limit` or `safety-bound`) plus a `resumeCursor`: replay it as `cursor` with the same device and **identical filters** to continue (`limit` then bounds the next answer); a token from another tool, device or filter set is refused **locally**. |
109
+ | `cherry_read` | Read a session's message history snapshot, **newest-first** (`limit` drops the **oldest** messages). Attachment parts come back as `name`/`mediaType`/`byteLength` with an empty `content` — the bytes are never fetched. A truncated walk always says so (`truncated` + `truncationReason`): `limit` means your own ceiling was reached and raising `limit` helps; `aggregate-bytes` means the answer hit its **output size limit** — independent of `limit` (a larger `limit` only costs more round trips and stops at the same budget). **The older messages are reached by the resume token**: pass the `messagesResumeCursor` a truncated answer returned as this tool's own `messagesResumeCursor` argument to continue from that stop point (`limit` then bounds how many messages come back **from there**), walking a long history in successive answers. Two token forms: `cursor:<peer position>` (the peer page position after a whole window was consumed) and `after:<messageId>` (the walk stopped inside its window; the resume scans from the newest end to that message and returns the older ones). A token this tool did not emit is **refused locally** (never forwarded — the desktop silently falls back to the newest page for one), and an `after:` anchor that is no longer in the history is an **error**, not an empty result. |
110
+ | `cherry_events` | Read the session's buffered events newer than `cursor`, acking them after delivery. The answer is `{events, nextCursor}`; pass that `nextCursor` back as `cursor` to continue from it. |
111
+ | `cherry_create` | Create a session in an agent workspace (workspace selection is **required**; identity is read back). |
112
+ | `cherry_send` | Send a message, by default waiting for a terminal state and returning the assistant text; `wait: false` returns the command receipt immediately. `messagesTruncated`/`messagesTruncationReason` report a **message**-level cut (older messages dropped); `deadline` means the **answer's own assembly budget** ran out (see below), with the content read so far still returned. |
113
+ | `cherry_wait` | Wait only for the session's in-flight execution to settle (sends nothing). |
114
+ | `cherry_interactions` | List the session's **pending** interaction cards (questions / approvals). A truncated answer always carries `truncated` + `truncationReason` and a `resumeCursor` — replay it as `cursor` to continue (`limit` counts the peer's pre-filter rows). |
115
+ | `cherry_respond` | Answer an interaction card: questions must be answered in full; approvals take `decision`. |
116
+ | `cherry_cancel` | Cancel the session's in-flight execution (`expectedExecutionId` required). |
117
+ | `cherry_pair` | Pair with a desktop using the pairing information shown by the scanned page. |
118
+ | `cherry_unpair` | Unpair: tear the connection down and delete the device archive entry. |
119
+
120
+ The `device` argument accepts an **exact `deviceId`**, or a **prefix of a
121
+ `deviceId` or of a display name**; several matches return `ambiguous-device` +
122
+ `candidates`, and no match returns `not-found` + `known`.
123
+
124
+ `cherry_send(wait: true)` / `cherry_wait` results carry one of **8 `status`
125
+ values**: the four protocol terminal states
126
+ (`completed`/`failed`/`cancelled`/`interrupted`), plus `awaiting-interaction`
127
+ (a pending card appeared while the run is still alive), `timeout` (the window ran
128
+ out, **not a failure** — partial content plus a stop-point cursor come back),
129
+ `idle` (`cherry_wait` met a session with nothing in flight and returned the
130
+ snapshot immediately), and `subscription-lost` (the subscription was lost and
131
+ could not be rebuilt, so the outcome is **unknowable**; repair guidance ships
132
+ with it).
133
+
134
+ ## Errors and limits
135
+
136
+ - Read methods are replayed **once** after a reconnect; **commands are never
137
+ replayed** — a write that loses its reply is recovered by retrying with the
138
+ **original `commandId`**, and the desktop's command ledger replays the receipt.
139
+ - One externalized content is capped at 32 MiB; a larger part is marked
140
+ `unavailable` rather than returning unverifiable partial content.
141
+ - **Externalized-content assembly is gated process-wide**: at most 4 `{ref}`
142
+ contents are assembled at once (the official client's read-concurrency
143
+ policy). One ref's assembly peaks at ~3× its size, so the **assembly** peak is
144
+ at most 4 × 3 × 32 MiB = 384 MiB worst case, and in ordinary traffic
145
+ 4 × 3 × (declared size). It bounds what is being ASSEMBLED, not everything
146
+ resident: content an answer has already assembled is retained by its caller
147
+ outside this product (up to one whole message per concurrent read, since the
148
+ output budget is checked only between messages), and the parts cache adds its
149
+ own budget on top. Waiting for a slot is part of a wait answer's time
150
+ budget: at its deadline the caller gives up and marks that one part
151
+ `unavailable`.
152
+ - A truncated history walk always says so (`truncationReason`; `limit`,
153
+ `aggregate-bytes` and `deadline` mean different things, see above) and,
154
+ whenever a stop point can be named honestly, carries a `messagesResumeCursor`
155
+ resume token (absent when the walk reached the peer's last page); a message
156
+ whose own parts could not be fully read carries `incomplete: true` (inside a
157
+ wait answer, a message cut by the deadline carries
158
+ `partsTruncated: "deadline"`).
159
+ - **A wait answer has an assembly budget of its own.** After the window ends,
160
+ `cherry_send(wait: true)` / `cherry_wait` still read some history (the message
161
+ page, one part list per message, one content walk per externalized `{ref}`),
162
+ and that step is now **time-bounded**: it may run until "your window plus
163
+ another one, floored at 1 s and capped at 15 s" (a 1 s window → at most one
164
+ more second; a 20 s window → at most 15 more). A flapping link can no longer
165
+ turn each request's 5 s reconnect wait into minutes of extra latency; when the
166
+ budget is spent the walk stops, the content already read is returned with
167
+ `messagesTruncationReason: "deadline"`, and older history is reached with
168
+ `cherry_read`. The same budget covers the session snapshot the **terminal and
169
+ timeout answers** read: when it cannot be read the answer degrades
170
+ (`aggregateError` + `guidance`) instead of losing the run's verdict and its
171
+ identifiers. The snapshot `cherry_wait` takes to decide whether anything is in
172
+ flight is a *precondition* — there is no answer yet to degrade — and it is
173
+ bounded too, by an assembly deadline anchored at tool entry (your window plus
174
+ the tail). When that budget is spent the call **fails with guidance**: nothing
175
+ is known about the run, so a retry is the recovery — and a 500 ms window can no
176
+ longer turn into 5–10 s of reconnect-plus-replay (#51). After a successful
177
+ snapshot the idle answer's history walk starts its own budget, so a slow but
178
+ successful snapshot does not squeeze the walk to nothing. The cost is that the
179
+ two phases are counted separately: a pathological call can spend both.
180
+ - **Known limitations**: `cherry_respond` lets a DSH agent approve Cherry-side
181
+ approval cards on the user's behalf (the MVP adds no interception layer and
182
+ trusts DSH's own tool-call approvals); only the `agent` capability is
183
+ requested per device; `ws://` only (Noise encryption), no direct TLS.
184
+
185
+ > Engineering detail — replay rules, the 32 MiB/≈3× memory contract, empty-page
186
+ > and cursor tolerance, integrity semantics — lives in [`AGENTS.md`](AGENTS.md).
187
+
188
+ ## Settings page and scope
189
+
190
+ The plugin is also a **web client bundle** (`package.json`'s `dsh.client`,
191
+ exporting `./client`). Through the `settings.section` slot it registers a
192
+ **`cherry-remote`** page (nav label “Cherry Studio Bridge”): device cards
193
+ (enable/disable/remove), a pairing wizard (paste → 6-digit code → verdict), a
194
+ scope panel, and live state pushed over the same-origin SSE feed
195
+ (`/cherry-remote/api/events`).
196
+
197
+ The 14 tools appear **per workspace**, by **hiding, not refusing** (in an
198
+ out-of-scope session they never enter the tool table, saving context):
199
+
200
+ - **Default is nobody**: a missing, corrupt or unreadable scope document is
201
+ treated as `selected` + an empty list (fail-closed).
202
+ - **`Selected workspaces only`**: only sessions inside ticked workspaces can see
203
+ the tools; sessions with no workspace never do.
204
+ - **`Global`**: visible everywhere; the ticks play no part.
205
+ - **Saves take effect immediately**: the restriction layer is re-installed or
206
+ released for every live agent, effective from the **next turn** — no restart,
207
+ no new session.
208
+ - **Scope governs tools only**: the settings page and `/cherry-remote/api` are
209
+ never scope-gated, so an excluded workspace can rescue itself.
210
+ - **No pretending**: on `pending` / `unsupported` host capability the panel says
211
+ so and the tools stay **unregistered** (fail-closed).
212
+
213
+ For development run `npm run dev:ui` — a DSH host must already be running at
214
+ `http://127.0.0.1:23023`; override with
215
+ `CHERRY_DEV_HOST=http://127.0.0.1:<port> npm run dev:ui`. The scope document is
216
+ `<dataDir>/scope.json` (`0600`, atomic, monotonic `revision`); endpoints and
217
+ error codes are documented in [`AGENTS.md`](AGENTS.md).
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ npm test # full vitest run (incl. tests/contract/)
223
+ npm run typecheck # server + UI projects
224
+ npm run build # tsdown → dist/index.mjs + dist/index.d.mts
225
+ npm run build:client # vite → lib/client.js
226
+ npm run dev:ui # settings-page dev server (host must run at 127.0.0.1:23023)
227
+ npm run prepublishOnly # pre-publish gate: typecheck + test + build + build:client + npm pack --dry-run
228
+ ```
229
+
230
+ `dist/` and `lib/` are **committed** artifacts: after changing `src/` or `ui/`
231
+ re-run `npm run build && npm run build:client` before committing. Working
232
+ conventions, build facts, the protocol basis and the design decisions live in
233
+ [`AGENTS.md`](AGENTS.md); before a release, walk
234
+ [`docs/e2e-checklist.md`](docs/e2e-checklist.md) (pairing / send / interaction
235
+ answers / disconnect recovery / multi-device coexistence need a real device).
236
+
237
+ ## License
238
+
239
+ MIT
package/README.md CHANGED
@@ -1,3 +1,189 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [English](README.en.md) | **简体中文**
4
+
5
+ </div>
6
+
7
+ <div align="center">
8
+
9
+ <img src="static/logo.png" alt="dsh-cherry-bridge" width="880">
10
+
11
+ </div>
12
+
13
+ # dsh-cherry-bridge
14
+
15
+ **把 DeepSeek Harness 桥接到 Cherry Studio 桌面端** — DSH 插件
16
+
17
+ 在 DSH 会话里直接驱动局域网内 Cherry Studio 的 agent:配对、列设备/会话、读历史、
18
+ 发消息并等待终态、应答审批与提问卡、取消执行;附一个同源设置页管理设备与工具启用范围。
19
+
20
+ ```mermaid
21
+ flowchart TB
22
+ T["DSH 会话<br/>14 个 cherry_* 工具"] --> P
23
+ W["设置页<br/>设备 · 配对 · 启用范围"] --> P
24
+ P["dsh-cherry-bridge<br/>连接池 · 订阅缓存 · 作用域"]
25
+ P <-->|"ws:// · Noise XX"| C["Cherry Studio 桌面端<br/>二维码配对 · 审批"]
26
+ ```
27
+
28
+ - 包名:`dsh-cherry-bridge`,插件 id:`cherry-remote`
29
+ - 传输:`ws://` + Noise XX(加密由 Noise 记录层提供,不依赖 TLS 终止)
30
+ - 工具面:14 个 `cherry_*` 工具,**按工作区**启用,默认**谁都没有**(见「启用范围」)
31
+ - 设置页与 HTTP 面:宿主 Web 服务器上的同源前缀 `/cherry-remote/api`
32
+
33
+ ## 安装
34
+
35
+ > **DSH 版本需求**:`>=0.2.0-rc.2 <0.3.0-0`(由 `package.json` 的 `engines.dsh` 声明;不满足时 bundle 被跳过、设置页里该插件的行被禁用,确需在区间外使用可用 `dsh plugin allow-version` 精确豁免。0.3.0 需重新评估兼容性后再放开)。
36
+
37
+ ```bash
38
+ # 从 npm 安装(推荐,钉住版本)
39
+ npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge@0.1.6
40
+
41
+ # 安装 npm 最新发布版
42
+ npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge
43
+
44
+ # 从 GitHub 源码安装(与 npm 发布同一提交)
45
+ npx @deepseek-ai/dsh plugin --profile web add github:SAXEM1997/dsh-cherry-bridge#v0.1.6
46
+ ```
47
+
48
+ 本包自带 `cordis.patch.yml`(`package.json` 的 `dsh.bundle.patch` 指向它),
49
+ dsh-plugin-manager 据此自动插入插件行。手工加行等价于:
50
+
51
+ ```yaml
52
+ - id: cherry-remote
53
+ name: 'dsh-cherry-bridge'
54
+ config:
55
+ perSessionCacheBytes: 2097152
56
+ rateBudgetPerSecond: 32
57
+ ```
58
+
59
+ ### 首次配对
60
+
61
+ 1. 在 Cherry Studio 桌面端打开 **remote access → 添加设备**:该界面显示**二维码**,
62
+ 配对信息要点开扫码后的页面才看得到。
63
+ 2. **扫这个二维码**(微信、浏览器等均可),复制页面上显示的配对信息。
64
+ 3. 在 DSH 里调用 `cherry_pair`(设置页向导亦可),粘贴这段配对信息。
65
+ 4. **双向核对 6 位验证码**:与桌面端显示的数字人工比对一致后再在桌面端批准。
66
+ 5. 批准后设备档案写入数据目录(`devices.json`,`0600`),其 `desktopIdentity` 被 pin 住,
67
+ 此后每次连接都校验 Noise 对端静态公钥与 pin 一致,防二维码被劫持后的中间人替换。
68
+
69
+ ## 快速上手
70
+
71
+ ```text
72
+ cherry_pair → 粘贴扫码页的配对信息,核对 6 位验证码
73
+ cherry_devices → 看设备与连接状态(前缀即可选中,如 "dev-A")
74
+ cherry_agents → 列设备上的 agent,拿 agentId
75
+ cherry_sessions → 列某 agent 的会话,拿 sessionId
76
+ cherry_read → 读会话历史(默认取最新 100 条,从新到旧)
77
+ cherry_send → 发消息并等待终态,返回助手文本
78
+ cherry_interactions → 看待答的提问/审批卡;cherry_respond 作答
79
+ ```
80
+
81
+ ## 配置
82
+
83
+ | 键 | 类型 | 默认 | 说明 |
84
+ | --- | --- | --- | --- |
85
+ | `dataDir` | string | `$DSH_HOME/cherry-remote`(回退 `~/.dsh/cherry-remote`) | 插件数据目录,存放 `identity.key` 与 `devices.json`(均 `0600`)。 |
86
+ | `perSessionCacheBytes` | number | `2097152`(2 MiB) | 每个会话的**本地**订阅事件缓存上限,超出按 LRU 丢弃最旧事件并 `warn`。 |
87
+ | `rateBudgetPerSecond` | number | `32` | 每设备平滑请求预算(请求/秒)。桌面端硬上限是 64 req/s 且无协商通道,超限会以 close `1011` 终止会话,故取保守值。 |
88
+
89
+ > ⚠️ `cordis.patch.yml` 的 `config` 是**整体替换**,不与默认值合并。删掉一个键得到的是
90
+ > 插件默认值;键存在但值非法(`0`、负数、字符串)会按名 `warn` 后回落默认值。
91
+
92
+ ## 工具
93
+
94
+ | 工具 | 作用 |
95
+ | --- | --- |
96
+ | `cherry_devices` | 列出已配对设备及连接状态(`connected`/`connecting`/`offline`/`disabled`/`identity-mismatch`)。 |
97
+ | `cherry_agents` | 列出某设备上可用的 agent(分页)。被截断时必带 `truncated`+`truncationReason`(`limit` 或 `safety-bound`,后者是 1000 条内部上限),并给出 `resumeCursor`——用同一 device 与过滤条件把它回传为 `cursor` 即可从停点继续(此时 `limit` 约束的是下一次回答);跨工具/设备/过滤条件的令牌会被**本地拒绝**,不会转发给对端。 |
98
+ | `cherry_workspaces` | 列出某 agent 的**已注册**工作区,以及是否存在系统工作区。被截断时必带 `truncated`+`truncationReason`,并给出 `resumeCursor`——用相同过滤条件回传为 `cursor` 即可续读。 |
99
+ | `cherry_sessions` | 列出某 agent 的会话,可按工作区过滤。被截断时必带 `truncated`+`truncationReason`(`limit` 或 `safety-bound`),并给出 `resumeCursor`——用同一 device 与**相同过滤条件**把它回传为 `cursor` 即可从停点继续(此时 `limit` 约束的是下一次回答);跨工具/设备/过滤条件的令牌会被**本地拒绝**。 |
100
+ | `cherry_read` | 读会话消息历史快照,**从新到旧**返回(`limit` 丢掉的是**最旧**的消息)。附件 part 只回 `name`/`mediaType`/`byteLength`,`content` 为空——字节不取。被截断时 `truncated` + `truncationReason` 一定说明:`limit` 表示你设的条数到了,提高 `limit` 即可;`aggregate-bytes` 表示单次回答达到了**输出大小上限**——与 `limit` 无关,提高 `limit` 只会多付往返、仍停在同一预算。**被挡下的更旧消息用 `messagesResumeCursor` 续读**:把本次返回的 `messagesResumeCursor` 原样传给下一次调用的 `messagesResumeCursor` 参数即可从停点继续(`limit` 此时约束的是**从停点起**本次最多返回多少条),长历史分几次回答读完。两种令牌:`cursor:<peer position>`(整窗读完后的 peer 分页位置)与 `after:<messageId>`(停在窗内,续读从最新端扫到该消息、返回它更旧的那些)。不是本工具发出的令牌会被**本地拒绝**(不转发给桌面端——桌面端会静默回退到最新页);`after:` 锚点在历史里找不到则**报错**,不会返回空结果。 |
101
+ | `cherry_events` | 读取该会话缓冲区内比 `cursor` 更新的事件,并在投递后 ack。返回 `{events, nextCursor}`;把 `nextCursor` 作为下次的 `cursor` 传回即可续读。 |
102
+ | `cherry_create` | 在 agent 工作区创建会话(工作区选择**必填**;创建后回读身份)。 |
103
+ | `cherry_send` | 向会话发消息,默认等待运行到终态并返回助手文本;`wait: false` 立即返回命令回执。内容被截断时 `messagesTruncated`/`messagesTruncationReason` 说的是**消息**被截断(丢更旧的);`deadline` 表示**回答自身的装配时限**到了(见下),已读到的内容照常返回。 |
104
+ | `cherry_wait` | 只等待该会话在途执行收敛(不发送新消息)。 |
105
+ | `cherry_interactions` | 列出该会话**待答**的交互卡(提问 / 审批)。被截断时必带 `truncated`+`truncationReason`,并给出 `resumeCursor`——用 `sessionId` 相同的调用回传为 `cursor` 即可续读(`limit` 计的是过滤**前**的对端条目)。 |
106
+ | `cherry_respond` | 应答交互卡:提问必须全量作答,审批走 `decision`。 |
107
+ | `cherry_cancel` | 取消会话在途执行(`expectedExecutionId` 必填)。 |
108
+ | `cherry_pair` | 用「扫码后页面显示的配对信息」建立与桌面端的配对。 |
109
+ | `cherry_unpair` | 解除配对:断开连接并从设备档案删除。 |
110
+
111
+ `device` 参数接受**精确 `deviceId`**,或 **`deviceId` 前缀 / 显示名前缀**;多项命中返回
112
+ `ambiguous-device` + `candidates`,一个都没命中返回 `not-found` + `known`。
113
+
114
+ `cherry_send(wait: true)` / `cherry_wait` 的 `status` 有 **8 个取值**:四个协议终态
115
+ (`completed`/`failed`/`cancelled`/`interrupted`),加上 `awaiting-interaction`(出现待答
116
+ 交互卡,运行仍活着)、`timeout`(窗口用尽,**不是失败**,随附部分内容与停点游标)、
117
+ `idle`(`cherry_wait` 遇到没有在途执行的会话,立即返回快照)、`subscription-lost`
118
+ (事件订阅丢失且重建失败,运行结局**不可知**,随附修复指引)。
119
+
120
+ ## 错误与限制
121
+
122
+ - 读方法在重连后**单次**重放;**命令永不重放**——写操作丢失应答时,携**原 `commandId`**
123
+ 重试即可,桌面端的命令账本会回放原回执。
124
+ - 单个外置内容上限 32 MiB,超过的 part 标 `unavailable`,不回无法校验的部分内容。
125
+ - **外置内容装配是进程级限流的**:同时最多 4 个 `{ref}` 在装配(官方客户端的读并发策略)。
126
+ 单个 ref 的装配峰值约 3× 它的大小,所以**装配峰值**上界是 4×3×32 MiB = 384 MiB,日常远低于此
127
+ (4×3×声明大小)。它约束的是**正在装配**的内容,不是全部驻留:已经装配完、被各调用自己持有的内容
128
+ 不在这个乘积里(每次并发读最多再带一整条消息,因为输出预算只在消息之间检查),parts 缓存另计。
129
+ 等待槽位也计入等待类回答的时限:排到时限就放弃并把这一 part 标 `unavailable`。
130
+ - 历史走查被截断时一定说明(`truncationReason`,`limit` / `aggregate-bytes` / `deadline` 三种含义不同,见上),
131
+ 并在能诚实标出停点时给出 `messagesResumeCursor` 续读令牌(走到对端末页时没有可标的位置,就不给);
132
+ 某条消息自身 parts 没读全时带 `incomplete: true`(wait 类回答里被时限截断的那条消息会带
133
+ `partsTruncated: "deadline"`)。
134
+ - **等待类回答有自己的装配时限**:`cherry_send(wait: true)` / `cherry_wait` 在窗口结束后仍要读一段历史
135
+ (消息页 + 每条消息的 parts + 每个外置 `{ref}` 内容),这一步现在是**有时限的**——截至「你的窗口 +
136
+ 同样长度的时间」,下限 1 秒、上限 15 秒(即窗口 1 秒 → 最多再等 1 秒;窗口 20 秒 → 最多再等 15 秒)。
137
+ 链路抖动时不会再有「每个请求各等 5 秒重连」把回答拖到几分钟之后;时限用尽就停走查,
138
+ 已读到的内容照常返回并标 `messagesTruncationReason: "deadline"`,更旧的历史用 `cherry_read` 续读。
139
+ 同一条时限也覆盖 **终态与超时回答**所读的会话快照:快照读不到时
140
+ 回执降级(`aggregateError` + `guidance`),不会把已经确定的运行结局(`status` 与各 id)一起丢掉。
141
+ `cherry_wait` 判断「会话是否还有在途执行」的那次**前置快照**同样有时限(从工具入口起算「窗口 + 尾」):
142
+ 它在预算内读不到时直接**失败并给出修复指引**——此时还没有回答可降级,运行状态**不可知**,重试是恢复路径;
143
+ 「500 毫秒窗口却先等 5–10 秒重连与重放」的旧形态因此消失(#51)。前置快照成功之后,
144
+ idle 回答的历史走查仍从那一刻起算自己的预算(慢但成功的快照不会把走查挤成零);
145
+ 代价是两段预算**分开计**,病态调用可能把两段都花掉。
146
+ - **已知限制**:`cherry_respond` 让 DSH agent 可代替用户批准 Cherry 端审批卡(MVP 不加拦截层,
147
+ 信任 DSH 自身的工具审批);每设备仅申请 `agent` capability;仅 `ws://`(Noise 加密),不做 TLS 直连。
148
+
149
+ > 传输重放、32 MiB/≈3× 内存、空页与游标容忍、完整性语义等工程细节见 [`AGENTS.md`](AGENTS.md)。
150
+
151
+ ## 设置页与启用范围
152
+
153
+ 插件同时是 **web client bundle**(`package.json` 的 `dsh.client`,导出 `./client`),通过
154
+ `settings.section` 槽位注册 **`cherry-remote`**(导航标签「Cherry Studio 互联」)页面:
155
+ 设备卡片(启用/停用/删除)、配对向导(粘贴 → 6 位验证码 → 终态)、启用范围面板,以及经
156
+ `/cherry-remote/api/events` SSE 推送的实时状态。
157
+
158
+ 14 个工具**按工作区**决定是否出现,机制是**隐藏而非拒绝**(不在范围的会话里不进工具表,省上下文):
159
+
160
+ - **默认谁都没有**:作用域文档不存在、损坏或读不出来,都按 `selected` + 空列表处理(fail-closed)。
161
+ - **`仅选定工作区`**:只有勾选的工作区里的会话才可能出现这些工具;没有工作区的会话始终没有。
162
+ - **`全局`**:所有工作区都可见,面板勾选不参与判定。
163
+ - **保存即时生效**:对所有 live agent 重装/释放限制层,**下一轮**即生效,无需重启或新建会话。
164
+ - **范围只管工具**:设置页与 `/cherry-remote/api` 不受范围影响,被排除的工作区也能自救。
165
+ - **不假装**:宿主能力 `pending` / `unsupported` 时面板如实说明,这些工具都**不注册**(fail-closed)。
166
+
167
+ 开发时可单独跑 `npm run dev:ui`,前提是宿主已在 `http://127.0.0.1:23023` 运行;
168
+ `CHERRY_DEV_HOST=http://127.0.0.1:<port>` 可改端口。作用域文档在 `<dataDir>/scope.json`
169
+ (`0600`、原子写入、`revision` 单调),HTTP 端点与错误码见 [`AGENTS.md`](AGENTS.md)。
170
+
171
+ ## 开发
172
+
173
+ ```bash
174
+ npm test # vitest 全量(含 tests/contract/)
175
+ npm run typecheck # 服务端 + UI 两侧
176
+ npm run build # tsdown → dist/index.mjs + dist/index.d.mts
177
+ npm run build:client # vite → lib/client.js
178
+ npm run dev:ui # 设置页 dev server(需宿主在 127.0.0.1:23023 运行)
179
+ npm run prepublishOnly # 发布前闸门:typecheck + test + build + build:client + npm pack --dry-run
180
+ ```
181
+
182
+ `dist/` 与 `lib/` 是**随仓库提交**的产物:改完 `src/` 或 `ui/` 必须重跑
183
+ `npm run build && npm run build:client` 再提交。开发约定、构建事实、协议依据与设计决策见
184
+ [`AGENTS.md`](AGENTS.md);发布前请走完 [`docs/e2e-checklist.md`](docs/e2e-checklist.md)
185
+ (配对 / 发送 / 交互应答 / 断连恢复 / 多设备并存需真机人工核验)。
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,32 @@
1
+ # dsh-cherry-bridge 插件(DSH Bundle patch)
2
+ #
3
+ # 作为 bundle 被合入 profile 时,自动插入本插件行。
4
+ # 插件把本机 DSH 桥接到局域网内的 Cherry Studio 桌面端:设备连接池、`cherry_*`
5
+ # 工具面,以及同源设置页 UI(`dsh.client`)。
6
+ #
7
+ # ⚠️ patch 的 `config` 是**整体替换**而非与 bundle 默认值合并:装配层必须对缺键
8
+ # 回落默认值(`src/index.ts` 的 `resolveBridgeConfig`,Task 10 落地)。因此这里
9
+ # 列出的键就是该 profile 下的**完整**配置面;删掉一个键不等于"沿用默认",
10
+ # 而是让装配层用自己的默认值(二者当前一致,见下)。
11
+ #
12
+ # 键值与 spec §8 一致:
13
+ # - perSessionCacheBytes:每个会话的本地订阅事件缓存上限(字节)。2 MiB。
14
+ # - rateBudgetPerSecond:每设备二级平滑请求预算(请求/秒),与
15
+ # `DeviceConnection` 默认值一致,远低于桌面端实现的 64 req/s 硬上限
16
+ # (该上限无协商通道,超限会以 close 1011 终止整条会话)。
17
+ # - dataDir 故意不写:留空时装配层按 $DSH_HOME/cherry-remote 解析,
18
+ # 写死会让所有 profile 共用同一份设备档案。
19
+ #
20
+ # ⚠️ 行的 `id` / `name` 不是自由文本——宿主 `applyEntryPatches` 只追加顶层 insert 行,
21
+ # 再由 `declaredRows` / bundle-in-use 判定「本插件已装配」,两处都要求
22
+ # `row.id == entry.options.id` **且** `row.name == entry.options.name`;缺 id 或
23
+ # 缺 name 的行会被**静默跳过**(不报错、不装配)。因此:
24
+ # `id` = 组合条目 id,必须严格等于 `src/index.ts` 导出的插件 `name`;
25
+ # `name` = npm 包名(package.json `name`)。
26
+ # 这条不变式由 `tests/contract/contract.test.ts` 断言锁死。
27
+ - insert:
28
+ - id: cherry-remote
29
+ name: 'dsh-cherry-bridge'
30
+ config:
31
+ perSessionCacheBytes: 2097152
32
+ rateBudgetPerSecond: 32