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 +21 -0
- package/README.en.md +239 -0
- package/README.md +188 -2
- package/cordis.patch.yml +32 -0
- package/dist/index.d.mts +293 -0
- package/dist/index.mjs +10532 -0
- package/docs/e2e-checklist.md +284 -0
- package/lib/client.js +1807 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +87 -4
- package/src/atomicFile.ts +74 -0
- package/src/connection.ts +1586 -0
- package/src/contentGate.ts +152 -0
- package/src/defaults.ts +139 -0
- package/src/deviceStore.ts +619 -0
- package/src/diagnostics.ts +839 -0
- package/src/httpApi.ts +1586 -0
- package/src/identity.ts +112 -0
- package/src/index.ts +815 -0
- package/src/pairing.ts +612 -0
- package/src/rpc/jsonRpcClient.ts +615 -0
- package/src/rpc/wsAdapter.ts +68 -0
- package/src/subscriptions.ts +976 -0
- package/src/toolScope.ts +1043 -0
- package/src/toolScopeHost.ts +1083 -0
- package/src/tools.ts +6415 -0
- package/src/urlHost.ts +15 -0
- package/static/logo.png +0 -0
- package/ui/index.html +18 -0
- package/ui/src/App.tsx +244 -0
- package/ui/src/DeviceCard.tsx +245 -0
- package/ui/src/PairingWizard.tsx +221 -0
- package/ui/src/ToolScopePanel.tsx +546 -0
- package/ui/src/api.ts +133 -0
- package/ui/src/dev.tsx +17 -0
- package/ui/src/i18n.ts +443 -0
- package/ui/src/main.tsx +352 -0
- package/ui/src/styles.css +303 -0
- package/ui/src/types.ts +299 -0
- package/ui/src/vite-env.d.ts +16 -0
- package/ui/vite.config.ts +173 -0
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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/cordis.patch.yml
ADDED
|
@@ -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
|