dsh-cherry-bridge 0.1.8 → 0.1.9
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 +3 -3
- package/README.md +3 -3
- package/dist/index.mjs +90 -23
- package/docs/e2e-checklist.md +1 -1
- package/lib/client.js +2 -2
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
- package/src/connection.ts +61 -0
- package/src/defaults.ts +18 -0
- package/src/tools.ts +64 -27
- package/ui/index.html +1 -1
- package/ui/src/i18n.ts +2 -2
package/README.en.md
CHANGED
|
@@ -38,13 +38,13 @@ flowchart TB
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
# From npm (recommended, pinned)
|
|
41
|
-
npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge@0.1.
|
|
41
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge@0.1.9
|
|
42
42
|
|
|
43
43
|
# Install the latest npm release
|
|
44
44
|
npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge
|
|
45
45
|
|
|
46
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.
|
|
47
|
+
npx @deepseek-ai/dsh plugin --profile web add github:SAXEM1997/dsh-cherry-bridge#v0.1.9
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
The package ships `cordis.patch.yml` (`package.json`'s `dsh.bundle.patch` points
|
|
@@ -203,7 +203,7 @@ subscription was lost and could not be rebuilt, so the outcome is
|
|
|
203
203
|
|
|
204
204
|
The plugin is also a **web client bundle** (`package.json`'s `dsh.client`,
|
|
205
205
|
exporting `./client`). Through the `settings.section` slot it registers a
|
|
206
|
-
**`cherry-remote`** page (nav label “Cherry
|
|
206
|
+
**`cherry-remote`** page (nav label “Cherry Bridge”): device cards
|
|
207
207
|
(enable/disable/remove), a pairing wizard (paste → 6-digit code → verdict), a
|
|
208
208
|
scope panel, and live state pushed over the same-origin SSE feed
|
|
209
209
|
(`/cherry-remote/api/events`).
|
package/README.md
CHANGED
|
@@ -36,13 +36,13 @@ flowchart TB
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
# 从 npm 安装(推荐,钉住版本)
|
|
39
|
-
npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge@0.1.
|
|
39
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge@0.1.9
|
|
40
40
|
|
|
41
41
|
# 安装 npm 最新发布版
|
|
42
42
|
npx @deepseek-ai/dsh plugin --profile web add dsh-cherry-bridge
|
|
43
43
|
|
|
44
44
|
# 从 GitHub 源码安装(与 npm 发布同一提交)
|
|
45
|
-
npx @deepseek-ai/dsh plugin --profile web add github:SAXEM1997/dsh-cherry-bridge#v0.1.
|
|
45
|
+
npx @deepseek-ai/dsh plugin --profile web add github:SAXEM1997/dsh-cherry-bridge#v0.1.9
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
本包自带 `cordis.patch.yml`(`package.json` 的 `dsh.bundle.patch` 指向它),
|
|
@@ -159,7 +159,7 @@ cherry_interactions → 看待答的提问/审批卡;cherry_respond 作答
|
|
|
159
159
|
## 设置页与启用范围
|
|
160
160
|
|
|
161
161
|
插件同时是 **web client bundle**(`package.json` 的 `dsh.client`,导出 `./client`),通过
|
|
162
|
-
`settings.section` 槽位注册 **`cherry-remote`**(导航标签「Cherry
|
|
162
|
+
`settings.section` 槽位注册 **`cherry-remote`**(导航标签「Cherry 互联」)页面:
|
|
163
163
|
设备卡片(启用/停用/删除)、配对向导(粘贴 → 6 位验证码 → 终态)、启用范围面板,以及经
|
|
164
164
|
`/cherry-remote/api/events` SSE 推送的实时状态。
|
|
165
165
|
|
package/dist/index.mjs
CHANGED
|
@@ -2695,6 +2695,13 @@ var DeviceConnection = class {
|
|
|
2695
2695
|
#transportRetryWaitMs;
|
|
2696
2696
|
#stateListeners = /* @__PURE__ */ new Set();
|
|
2697
2697
|
/**
|
|
2698
|
+
* Callers parked in {@link waitForSettled}. Separate from the state listeners
|
|
2699
|
+
* because their lifetime is ONE wait: a listener stays for the lifecycle, a
|
|
2700
|
+
* settle waiter removes itself the moment it is answered (or times out), and
|
|
2701
|
+
* mixing them would leave every answered wait in the multicast set.
|
|
2702
|
+
*/
|
|
2703
|
+
#settleWaiters = /* @__PURE__ */ new Set();
|
|
2704
|
+
/**
|
|
2698
2705
|
* Waits parked inside `#awaitReconnect`. close() drains them: a shutdown has
|
|
2699
2706
|
* no reconnect to wait for, and — the case that matters — a close() followed
|
|
2700
2707
|
* by a fresh connect() must not let an OLD lifecycle's replay ride the new
|
|
@@ -2819,6 +2826,46 @@ var DeviceConnection = class {
|
|
|
2819
2826
|
this.#stateListeners.add(cb);
|
|
2820
2827
|
}
|
|
2821
2828
|
/**
|
|
2829
|
+
* Waits for an attempt that is ALREADY IN FLIGHT to settle, bounded by
|
|
2830
|
+
* `timeoutMs`, and answers with the state it ends on.
|
|
2831
|
+
*
|
|
2832
|
+
* The only state this waits on is `connecting`: an attempt is running, and
|
|
2833
|
+
* its outcome is what the caller asked about. `connected` is answered at
|
|
2834
|
+
* once, and so are `offline` (parked in backoff), `disabled` and
|
|
2835
|
+
* `identity-mismatch` — no wait can change those, and waiting on them would
|
|
2836
|
+
* merely delay the caller's answer by the whole bound. `offline` is the
|
|
2837
|
+
* common case for a genuinely powered-off desktop, so it is the one that
|
|
2838
|
+
* matters: the tools stay fail-fast there.
|
|
2839
|
+
*
|
|
2840
|
+
* This NEVER starts an attempt — it observes one the pool already started.
|
|
2841
|
+
* That is the line the tool face has always drawn (a tool must not dial); the
|
|
2842
|
+
* wait only stops the tool from calling a live dial a failure.
|
|
2843
|
+
*
|
|
2844
|
+
* The bound expiring is not an error: the current state (`connecting`) is
|
|
2845
|
+
* returned, and the caller judges it exactly as it would have without the
|
|
2846
|
+
* wait.
|
|
2847
|
+
*/
|
|
2848
|
+
async waitForSettled(timeoutMs) {
|
|
2849
|
+
if (this.#state !== "connecting" || timeoutMs <= 0) return this.#state;
|
|
2850
|
+
return new Promise((resolve) => {
|
|
2851
|
+
let settled = false;
|
|
2852
|
+
let timer;
|
|
2853
|
+
const finish = (state) => {
|
|
2854
|
+
if (settled) return;
|
|
2855
|
+
settled = true;
|
|
2856
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2857
|
+
this.#settleWaiters.delete(onState);
|
|
2858
|
+
resolve(state);
|
|
2859
|
+
};
|
|
2860
|
+
const onState = (state) => {
|
|
2861
|
+
if (state !== "connecting") finish(state);
|
|
2862
|
+
};
|
|
2863
|
+
timer = setTimeout(() => finish(this.#state), timeoutMs);
|
|
2864
|
+
this.#settleWaiters.add(onState);
|
|
2865
|
+
if (this.#state !== "connecting") finish(this.#state);
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2822
2869
|
* Starts (or joins) the supervised lifecycle. Resolves once this cycle
|
|
2823
2870
|
* reaches `connected`; rejects with the cycle's failure. Transient
|
|
2824
2871
|
* failures keep being retried in the background by the backoff loop even
|
|
@@ -3308,6 +3355,11 @@ var DeviceConnection = class {
|
|
|
3308
3355
|
} catch (error) {
|
|
3309
3356
|
console.warn(`[cherry-remote] state listener threw for ${this.record.deviceId}`, error);
|
|
3310
3357
|
}
|
|
3358
|
+
for (const cb of [...this.#settleWaiters]) try {
|
|
3359
|
+
cb(next);
|
|
3360
|
+
} catch (error) {
|
|
3361
|
+
console.warn(`[cherry-remote] settle waiter threw for ${this.record.deviceId}`, error);
|
|
3362
|
+
}
|
|
3311
3363
|
}
|
|
3312
3364
|
};
|
|
3313
3365
|
//#endregion
|
|
@@ -5200,8 +5252,12 @@ var CherryToolEngine = class {
|
|
|
5200
5252
|
* ⑤): the candidate list is the answer. A MISS is a verdict too — see
|
|
5201
5253
|
* {@link CherryDeviceNotFound} — and it carries the ids the caller could have
|
|
5202
5254
|
* meant so the answer stays actionable.
|
|
5255
|
+
*
|
|
5256
|
+
* ASYNC because of the `connecting` wait below: the pool dials on first touch
|
|
5257
|
+
* and on every reconnect, so a tool called in that window used to be refused
|
|
5258
|
+
* for a connection that comes up a moment later (issue #99).
|
|
5203
5259
|
*/
|
|
5204
|
-
#resolve(device) {
|
|
5260
|
+
async #resolve(device) {
|
|
5205
5261
|
const resolution = this.#deps.store.resolve(device);
|
|
5206
5262
|
if ("ambiguous" in resolution) return {
|
|
5207
5263
|
error: "ambiguous-device",
|
|
@@ -5211,10 +5267,11 @@ var CherryToolEngine = class {
|
|
|
5211
5267
|
if ("notFound" in resolution) return this.#notFoundVerdict(device);
|
|
5212
5268
|
const connection = this.#deps.connect(resolution.deviceId);
|
|
5213
5269
|
this.#watchConnection(resolution.deviceId, connection);
|
|
5214
|
-
|
|
5270
|
+
const state = connection.state === "connecting" ? await connection.waitForSettled(this.#deps.deviceSettleWaitMs ?? 5e3) : connection.state;
|
|
5271
|
+
if (state !== "connected") return {
|
|
5215
5272
|
error: "device-unavailable",
|
|
5216
5273
|
device: resolution.deviceId,
|
|
5217
|
-
state
|
|
5274
|
+
state
|
|
5218
5275
|
};
|
|
5219
5276
|
return {
|
|
5220
5277
|
record: resolution,
|
|
@@ -5250,6 +5307,10 @@ var CherryToolEngine = class {
|
|
|
5250
5307
|
/**
|
|
5251
5308
|
* Resolve-or-verdict: every non-`cherry_devices` tool needs a live link.
|
|
5252
5309
|
*
|
|
5310
|
+
* `connecting` is waited out inside {@link #resolve} first, so the
|
|
5311
|
+
* `DeviceUnavailableError` below is only ever raised for a state the wait
|
|
5312
|
+
* could not improve on (bounded by `deviceSettleWaitMs`).
|
|
5313
|
+
*
|
|
5253
5314
|
* The failure faces are deliberately DIFFERENT (controller ruling ⑤): an
|
|
5254
5315
|
* ambiguous selector and a MISS are ANSWERS — the candidate list, or the
|
|
5255
5316
|
* known ids, returned in band, because the caller can act on them — while a
|
|
@@ -5262,8 +5323,8 @@ var CherryToolEngine = class {
|
|
|
5262
5323
|
* `cherry_devices` does not come through this method — it goes through
|
|
5263
5324
|
* {@link #selectRecords}, which returns the verdict in band.
|
|
5264
5325
|
*/
|
|
5265
|
-
#requireConnection(device) {
|
|
5266
|
-
const resolved = this.#resolve(device);
|
|
5326
|
+
async #requireConnection(device) {
|
|
5327
|
+
const resolved = await this.#resolve(device);
|
|
5267
5328
|
if (!this.#isVerdict(resolved)) return resolved;
|
|
5268
5329
|
if (resolved.error === "ambiguous-device") return resolved;
|
|
5269
5330
|
if (resolved.error === "not-found") {
|
|
@@ -5282,10 +5343,10 @@ var CherryToolEngine = class {
|
|
|
5282
5343
|
* replace it with a weaker, wrong one — a miss under a READ-ONLY archive
|
|
5283
5344
|
* would describe a store that still holds a valid token as "no such device".
|
|
5284
5345
|
*/
|
|
5285
|
-
#linkOr(device) {
|
|
5346
|
+
async #linkOr(device) {
|
|
5286
5347
|
const quarantined = this.#quarantineVerdict();
|
|
5287
5348
|
if (quarantined !== void 0) return { verdict: quarantined };
|
|
5288
|
-
const resolved = this.#requireConnection(device);
|
|
5349
|
+
const resolved = await this.#requireConnection(device);
|
|
5289
5350
|
if ("error" in resolved) return { verdict: resolved };
|
|
5290
5351
|
return { link: resolved };
|
|
5291
5352
|
}
|
|
@@ -5507,10 +5568,13 @@ var CherryToolEngine = class {
|
|
|
5507
5568
|
const selector = optionalString(args["device"]);
|
|
5508
5569
|
const quarantined = this.#quarantineVerdict();
|
|
5509
5570
|
if (quarantined !== void 0) return quarantined;
|
|
5510
|
-
if (selector === void 0)
|
|
5571
|
+
if (selector === void 0) {
|
|
5572
|
+
const records = this.#deps.store.list();
|
|
5573
|
+
return { devices: await Promise.all(records.map((record) => this.#deviceView(record))) };
|
|
5574
|
+
}
|
|
5511
5575
|
const selected = this.#selectRecords(selector);
|
|
5512
5576
|
if (!Array.isArray(selected)) return selected;
|
|
5513
|
-
return { devices: selected.map((record) => this.#deviceView(record)) };
|
|
5577
|
+
return { devices: await Promise.all(selected.map((record) => this.#deviceView(record))) };
|
|
5514
5578
|
}
|
|
5515
5579
|
});
|
|
5516
5580
|
}
|
|
@@ -5540,7 +5604,9 @@ var CherryToolEngine = class {
|
|
|
5540
5604
|
return [resolution];
|
|
5541
5605
|
}
|
|
5542
5606
|
/**
|
|
5543
|
-
* One device's view WITHOUT dialing
|
|
5607
|
+
* One device's view WITHOUT dialing — but after waiting out an attempt that is
|
|
5608
|
+
* already in flight, exactly as {@link #resolve} does, so a cold-start row
|
|
5609
|
+
* does not present a transient `connecting` as the device's answer.
|
|
5544
5610
|
*
|
|
5545
5611
|
* A disabled record is authoritative: the pooled connection is constructed
|
|
5546
5612
|
* with its own `record.enabled` snapshot and reports `disabled` only once
|
|
@@ -5548,10 +5614,11 @@ var CherryToolEngine = class {
|
|
|
5548
5614
|
* for a device whose connection was never started. Everything else falls back
|
|
5549
5615
|
* to the connection's own CLOSED state set.
|
|
5550
5616
|
*/
|
|
5551
|
-
#deviceView(record) {
|
|
5617
|
+
async #deviceView(record) {
|
|
5552
5618
|
let state = record.enabled ? "offline" : "disabled";
|
|
5553
5619
|
if (record.enabled) try {
|
|
5554
|
-
|
|
5620
|
+
const connection = this.#deps.connect(record.deviceId);
|
|
5621
|
+
state = connection.state === "connecting" ? await connection.waitForSettled(this.#deps.deviceSettleWaitMs ?? 5e3) : connection.state;
|
|
5555
5622
|
} catch (error) {
|
|
5556
5623
|
console.warn(`[cherry-remote] cherry_devices could not read the connection for ${record.deviceId}`, error);
|
|
5557
5624
|
}
|
|
@@ -5587,7 +5654,7 @@ var CherryToolEngine = class {
|
|
|
5587
5654
|
additionalProperties: false
|
|
5588
5655
|
},
|
|
5589
5656
|
execute: async (args) => {
|
|
5590
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
5657
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
5591
5658
|
if (resolved.link === void 0) return resolved.verdict;
|
|
5592
5659
|
const { connection, record } = resolved.link;
|
|
5593
5660
|
const binding = {
|
|
@@ -5634,7 +5701,7 @@ var CherryToolEngine = class {
|
|
|
5634
5701
|
additionalProperties: false
|
|
5635
5702
|
},
|
|
5636
5703
|
execute: async (args) => {
|
|
5637
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
5704
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
5638
5705
|
if (resolved.link === void 0) return resolved.verdict;
|
|
5639
5706
|
const { connection, record } = resolved.link;
|
|
5640
5707
|
const params = { agentId: requiredString(args, "agentId") };
|
|
@@ -5693,7 +5760,7 @@ var CherryToolEngine = class {
|
|
|
5693
5760
|
additionalProperties: false
|
|
5694
5761
|
},
|
|
5695
5762
|
execute: async (args) => {
|
|
5696
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
5763
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
5697
5764
|
if (resolved.link === void 0) return resolved.verdict;
|
|
5698
5765
|
const { connection, record } = resolved.link;
|
|
5699
5766
|
const agentId = optionalString(args["agentId"]);
|
|
@@ -5751,7 +5818,7 @@ var CherryToolEngine = class {
|
|
|
5751
5818
|
},
|
|
5752
5819
|
execute: async (args) => {
|
|
5753
5820
|
const resume = parseMessageResume(args["messagesResumeCursor"]);
|
|
5754
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
5821
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
5755
5822
|
if (resolved.link === void 0) return resolved.verdict;
|
|
5756
5823
|
const { connection, record } = resolved.link;
|
|
5757
5824
|
const sessionId = requiredString(args, "sessionId");
|
|
@@ -6171,7 +6238,7 @@ var CherryToolEngine = class {
|
|
|
6171
6238
|
additionalProperties: false
|
|
6172
6239
|
},
|
|
6173
6240
|
execute: async (args) => {
|
|
6174
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6241
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6175
6242
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6176
6243
|
const { record } = resolved.link;
|
|
6177
6244
|
const sessionId = requiredString(args, "sessionId");
|
|
@@ -6234,7 +6301,7 @@ var CherryToolEngine = class {
|
|
|
6234
6301
|
additionalProperties: false
|
|
6235
6302
|
},
|
|
6236
6303
|
execute: async (args) => {
|
|
6237
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6304
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6238
6305
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6239
6306
|
const { connection, record } = resolved.link;
|
|
6240
6307
|
const agentId = requiredString(args, "agentId");
|
|
@@ -6319,7 +6386,7 @@ var CherryToolEngine = class {
|
|
|
6319
6386
|
additionalProperties: false
|
|
6320
6387
|
},
|
|
6321
6388
|
execute: async (args) => {
|
|
6322
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6389
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6323
6390
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6324
6391
|
const { connection, record } = resolved.link;
|
|
6325
6392
|
const sessionId = requiredString(args, "sessionId");
|
|
@@ -6443,7 +6510,7 @@ var CherryToolEngine = class {
|
|
|
6443
6510
|
additionalProperties: false
|
|
6444
6511
|
},
|
|
6445
6512
|
execute: async (args) => {
|
|
6446
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6513
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6447
6514
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6448
6515
|
const { connection, record } = resolved.link;
|
|
6449
6516
|
const sessionId = requiredString(args, "sessionId");
|
|
@@ -6525,7 +6592,7 @@ var CherryToolEngine = class {
|
|
|
6525
6592
|
additionalProperties: false
|
|
6526
6593
|
},
|
|
6527
6594
|
execute: async (args) => {
|
|
6528
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6595
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6529
6596
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6530
6597
|
const { connection, record } = resolved.link;
|
|
6531
6598
|
const sessionId = requiredString(args, "sessionId");
|
|
@@ -6625,7 +6692,7 @@ var CherryToolEngine = class {
|
|
|
6625
6692
|
additionalProperties: false
|
|
6626
6693
|
},
|
|
6627
6694
|
execute: async (args) => {
|
|
6628
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6695
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6629
6696
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6630
6697
|
const { connection, record } = resolved.link;
|
|
6631
6698
|
const sessionId = requiredString(args, "sessionId");
|
|
@@ -6702,7 +6769,7 @@ var CherryToolEngine = class {
|
|
|
6702
6769
|
additionalProperties: false
|
|
6703
6770
|
},
|
|
6704
6771
|
execute: async (args) => {
|
|
6705
|
-
const resolved = this.#linkOr(requiredString(args, "device"));
|
|
6772
|
+
const resolved = await this.#linkOr(requiredString(args, "device"));
|
|
6706
6773
|
if (resolved.link === void 0) return resolved.verdict;
|
|
6707
6774
|
const { connection, record } = resolved.link;
|
|
6708
6775
|
const sessionId = requiredString(args, "sessionId");
|
package/docs/e2e-checklist.md
CHANGED
|
@@ -175,7 +175,7 @@ schema 已允许 `interrupted`,所以客户端**必须**处理,但当前服
|
|
|
175
175
|
**W1**(步骤 6 会真的把它删掉),其中有一个**保持开着**的会话;③ 一个不勾选的工作区
|
|
176
176
|
**W2**(含一个会话);④ 一个**没有工作区**的会话——`cwd` 不属于任何已注册工作区,最省事的造法是:
|
|
177
177
|
注册一个临时工作区 W0 → 在其中建一个会话 → 删掉 W0(该会话转入「未分组」,D7 的判定对象就是它)。
|
|
178
|
-
面板入口:设置页 →「**Cherry
|
|
178
|
+
面板入口:设置页 →「**Cherry 互联**」→「**启用范围**」。`curl -s
|
|
179
179
|
http://127.0.0.1:23023/cherry-remote/api/scope`(宿主换过端口就换端口;curl 不带 `Origin`,走
|
|
180
180
|
信任 fence 的放行分支)应返回 `host.state: "ready"`、`host.capable: true`、`host.degraded: []`。
|
|
181
181
|
|
package/lib/client.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
*/
|
|
14
14
|
var dictionaries = {
|
|
15
15
|
zh: {
|
|
16
|
-
"app.title": "Cherry
|
|
16
|
+
"app.title": "Cherry 互联",
|
|
17
17
|
"app.subtitle": "通过远程传输把本机 DeepSeek Harness 连接到 Cherry Studio 桌面端;设备档案保存在插件数据目录,页面不接触任何凭据。",
|
|
18
18
|
"app.eventSourceUnavailable": "此环境不支持 EventSource",
|
|
19
19
|
"app.devices.heading": "已配对设备",
|
|
@@ -113,7 +113,7 @@ window.__ModuleLoader__.load({
|
|
|
113
113
|
"list.separator": "、"
|
|
114
114
|
},
|
|
115
115
|
en: {
|
|
116
|
-
"app.title": "Cherry
|
|
116
|
+
"app.title": "Cherry Bridge",
|
|
117
117
|
"app.subtitle": "Connect this machine’s DeepSeek Harness to a Cherry Studio desktop over the remote transport. Device records live in the plugin data directory; this page never touches a credential.",
|
|
118
118
|
"app.eventSourceUnavailable": "EventSource is unavailable in this environment",
|
|
119
119
|
"app.devices.heading": "Paired devices",
|
package/locale/en.json
CHANGED
package/locale/zh.json
CHANGED
package/package.json
CHANGED
package/src/connection.ts
CHANGED
|
@@ -627,6 +627,13 @@ export class DeviceConnection {
|
|
|
627
627
|
readonly #helloProtocolVersions: readonly number[]
|
|
628
628
|
readonly #transportRetryWaitMs: number
|
|
629
629
|
readonly #stateListeners = new Set<(state: DeviceState) => void>()
|
|
630
|
+
/**
|
|
631
|
+
* Callers parked in {@link waitForSettled}. Separate from the state listeners
|
|
632
|
+
* because their lifetime is ONE wait: a listener stays for the lifecycle, a
|
|
633
|
+
* settle waiter removes itself the moment it is answered (or times out), and
|
|
634
|
+
* mixing them would leave every answered wait in the multicast set.
|
|
635
|
+
*/
|
|
636
|
+
readonly #settleWaiters = new Set<(state: DeviceState) => void>()
|
|
630
637
|
/**
|
|
631
638
|
* Waits parked inside `#awaitReconnect`. close() drains them: a shutdown has
|
|
632
639
|
* no reconnect to wait for, and — the case that matters — a close() followed
|
|
@@ -783,6 +790,53 @@ export class DeviceConnection {
|
|
|
783
790
|
this.#stateListeners.add(cb)
|
|
784
791
|
}
|
|
785
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Waits for an attempt that is ALREADY IN FLIGHT to settle, bounded by
|
|
795
|
+
* `timeoutMs`, and answers with the state it ends on.
|
|
796
|
+
*
|
|
797
|
+
* The only state this waits on is `connecting`: an attempt is running, and
|
|
798
|
+
* its outcome is what the caller asked about. `connected` is answered at
|
|
799
|
+
* once, and so are `offline` (parked in backoff), `disabled` and
|
|
800
|
+
* `identity-mismatch` — no wait can change those, and waiting on them would
|
|
801
|
+
* merely delay the caller's answer by the whole bound. `offline` is the
|
|
802
|
+
* common case for a genuinely powered-off desktop, so it is the one that
|
|
803
|
+
* matters: the tools stay fail-fast there.
|
|
804
|
+
*
|
|
805
|
+
* This NEVER starts an attempt — it observes one the pool already started.
|
|
806
|
+
* That is the line the tool face has always drawn (a tool must not dial); the
|
|
807
|
+
* wait only stops the tool from calling a live dial a failure.
|
|
808
|
+
*
|
|
809
|
+
* The bound expiring is not an error: the current state (`connecting`) is
|
|
810
|
+
* returned, and the caller judges it exactly as it would have without the
|
|
811
|
+
* wait.
|
|
812
|
+
*/
|
|
813
|
+
async waitForSettled(timeoutMs: number): Promise<DeviceState> {
|
|
814
|
+
if (this.#state !== 'connecting' || timeoutMs <= 0) return this.#state
|
|
815
|
+
return new Promise<DeviceState>((resolve) => {
|
|
816
|
+
let settled = false
|
|
817
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
818
|
+
const finish = (state: DeviceState): void => {
|
|
819
|
+
if (settled) return
|
|
820
|
+
settled = true
|
|
821
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
822
|
+
this.#settleWaiters.delete(onState)
|
|
823
|
+
resolve(state)
|
|
824
|
+
}
|
|
825
|
+
// The waiter's own filter: a transition to `offline` IS the attempt
|
|
826
|
+
// settling (failed); only a transition INTO `connecting` is not an
|
|
827
|
+
// answer, and `#setState` never reports a state it is already in.
|
|
828
|
+
const onState = (state: DeviceState): void => {
|
|
829
|
+
if (state !== 'connecting') finish(state)
|
|
830
|
+
}
|
|
831
|
+
timer = setTimeout(() => finish(this.#state), timeoutMs)
|
|
832
|
+
this.#settleWaiters.add(onState)
|
|
833
|
+
// The attempt may have settled between the guard above and the
|
|
834
|
+
// registration; that transition is already gone from the multicast, so
|
|
835
|
+
// it has to be observed by hand.
|
|
836
|
+
if (this.#state !== 'connecting') finish(this.#state)
|
|
837
|
+
})
|
|
838
|
+
}
|
|
839
|
+
|
|
786
840
|
/**
|
|
787
841
|
* Starts (or joins) the supervised lifecycle. Resolves once this cycle
|
|
788
842
|
* reaches `connected`; rejects with the cycle's failure. Transient
|
|
@@ -1582,5 +1636,12 @@ export class DeviceConnection {
|
|
|
1582
1636
|
console.warn(`[cherry-remote] state listener threw for ${this.record.deviceId}`, error)
|
|
1583
1637
|
}
|
|
1584
1638
|
}
|
|
1639
|
+
for (const cb of [...this.#settleWaiters]) {
|
|
1640
|
+
try {
|
|
1641
|
+
cb(next)
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
console.warn(`[cherry-remote] settle waiter threw for ${this.record.deviceId}`, error)
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1585
1646
|
}
|
|
1586
1647
|
}
|
package/src/defaults.ts
CHANGED
|
@@ -81,6 +81,24 @@ export const DEFAULT_CONTENT_READ_BUDGET_PER_SECOND = 256
|
|
|
81
81
|
*/
|
|
82
82
|
export const DEFAULT_TRANSPORT_RETRY_WAIT_MS = 5_000
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* How long a DEVICE TOOL waits for an establishment attempt that is already in
|
|
86
|
+
* flight (`state === 'connecting'`) before judging the device.
|
|
87
|
+
*
|
|
88
|
+
* WHY THIS EXISTS: the connection pool builds a `DeviceConnection` on FIRST
|
|
89
|
+
* TOUCH and that constructor dials immediately, so every tool called in the
|
|
90
|
+
* same tick as the first touch — and every tool called during a reconnect —
|
|
91
|
+
* observed `connecting` and answered `device-unavailable` about a connection
|
|
92
|
+
* that comes up a moment later. The state is a transient, not a verdict.
|
|
93
|
+
*
|
|
94
|
+
* It is deliberately NOT a reconnect budget: an `offline` device is parked in
|
|
95
|
+
* its backoff, where waiting cannot change anything, so the tools answer at
|
|
96
|
+
* once. Five seconds is ~15x a LAN dial plus handshake; a bound longer than
|
|
97
|
+
* that only delays the unavoidable answer, and the tool call is the caller's
|
|
98
|
+
* unit of patience.
|
|
99
|
+
*/
|
|
100
|
+
export const DEVICE_SETTLE_WAIT_MS = 5_000
|
|
101
|
+
|
|
84
102
|
/** 2 MiB of event text cached per session (spec §3.5). */
|
|
85
103
|
export const DEFAULT_PER_SESSION_CACHE_BYTES = 2 * 1024 * 1024
|
|
86
104
|
|
package/src/tools.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { DeviceConnection, DeviceState } from './connection.js'
|
|
|
4
4
|
import type { DeviceRecord, DeviceResolution, DeviceStore } from './deviceStore.js'
|
|
5
5
|
import { QUARANTINE_RECOVERY, QuarantinedArchiveError } from './deviceStore.js'
|
|
6
6
|
import {
|
|
7
|
+
DEVICE_SETTLE_WAIT_MS,
|
|
7
8
|
INTERACTIONS_LIST_PAGE_CLAMP,
|
|
8
9
|
LIST_PAGE_CLAMP,
|
|
9
10
|
MAX_RESUME_TOKEN_CHARS,
|
|
@@ -1030,6 +1031,14 @@ export interface CherryToolsDeps {
|
|
|
1030
1031
|
* this one subscribes the engine to ONE device's transitions.
|
|
1031
1032
|
*/
|
|
1032
1033
|
watchDeviceState?: (deviceId: string, cb: (state: DeviceState) => void) => void
|
|
1034
|
+
/**
|
|
1035
|
+
* How long a device tool waits for an establishment attempt that is ALREADY
|
|
1036
|
+
* IN FLIGHT (`state === 'connecting'`) before judging the device (issue #99).
|
|
1037
|
+
*
|
|
1038
|
+
* OPTIONAL: production omits it and takes {@link DEVICE_SETTLE_WAIT_MS}; test
|
|
1039
|
+
* rigs drive it down so the bounded case needs no real five seconds.
|
|
1040
|
+
*/
|
|
1041
|
+
deviceSettleWaitMs?: number
|
|
1033
1042
|
}
|
|
1034
1043
|
|
|
1035
1044
|
// ---------------------------------------------------------------------------
|
|
@@ -1617,10 +1626,14 @@ class CherryToolEngine {
|
|
|
1617
1626
|
* ⑤): the candidate list is the answer. A MISS is a verdict too — see
|
|
1618
1627
|
* {@link CherryDeviceNotFound} — and it carries the ids the caller could have
|
|
1619
1628
|
* meant so the answer stays actionable.
|
|
1629
|
+
*
|
|
1630
|
+
* ASYNC because of the `connecting` wait below: the pool dials on first touch
|
|
1631
|
+
* and on every reconnect, so a tool called in that window used to be refused
|
|
1632
|
+
* for a connection that comes up a moment later (issue #99).
|
|
1620
1633
|
*/
|
|
1621
|
-
#resolve(
|
|
1634
|
+
async #resolve(
|
|
1622
1635
|
device: string,
|
|
1623
|
-
): ResolvedDevice | CherryAmbiguousDevice | CherryDeviceNotFound | CherryDeviceUnavailable {
|
|
1636
|
+
): Promise<ResolvedDevice | CherryAmbiguousDevice | CherryDeviceNotFound | CherryDeviceUnavailable> {
|
|
1624
1637
|
const resolution: DeviceResolution = this.#deps.store.resolve(device)
|
|
1625
1638
|
if ('ambiguous' in resolution) {
|
|
1626
1639
|
return { error: 'ambiguous-device', device, candidates: resolution.ambiguous }
|
|
@@ -1634,10 +1647,21 @@ class CherryToolEngine {
|
|
|
1634
1647
|
// transition this exists for can land while the device is already offline,
|
|
1635
1648
|
// and the watch must be in place from the first call either way.
|
|
1636
1649
|
this.#watchConnection(resolution.deviceId, connection)
|
|
1637
|
-
|
|
1650
|
+
// `connecting` is an attempt IN FLIGHT, not a verdict (issue #99): the pool
|
|
1651
|
+
// dials on first touch and on every reconnect, so refusing here used to
|
|
1652
|
+
// report a failure about a connection that comes up a moment later. Wait for
|
|
1653
|
+
// that attempt — bounded, and never starting one — then judge the state it
|
|
1654
|
+
// settled on. Every other non-connected state is answered at once: no wait
|
|
1655
|
+
// can change `offline` (parked in backoff), `disabled` or
|
|
1656
|
+
// `identity-mismatch`, and waiting would only delay the answer.
|
|
1657
|
+
const state =
|
|
1658
|
+
connection.state === 'connecting'
|
|
1659
|
+
? await connection.waitForSettled(this.#deps.deviceSettleWaitMs ?? DEVICE_SETTLE_WAIT_MS)
|
|
1660
|
+
: connection.state
|
|
1661
|
+
if (state !== 'connected') {
|
|
1638
1662
|
// The established Task 5 shape: the tool never dials implicitly, and the
|
|
1639
1663
|
// caller gets the same `{device, state}` verdict the RPC layer throws.
|
|
1640
|
-
return { error: 'device-unavailable', device: resolution.deviceId, state
|
|
1664
|
+
return { error: 'device-unavailable', device: resolution.deviceId, state }
|
|
1641
1665
|
}
|
|
1642
1666
|
return { record: resolution, connection }
|
|
1643
1667
|
}
|
|
@@ -1675,6 +1699,10 @@ class CherryToolEngine {
|
|
|
1675
1699
|
/**
|
|
1676
1700
|
* Resolve-or-verdict: every non-`cherry_devices` tool needs a live link.
|
|
1677
1701
|
*
|
|
1702
|
+
* `connecting` is waited out inside {@link #resolve} first, so the
|
|
1703
|
+
* `DeviceUnavailableError` below is only ever raised for a state the wait
|
|
1704
|
+
* could not improve on (bounded by `deviceSettleWaitMs`).
|
|
1705
|
+
*
|
|
1678
1706
|
* The failure faces are deliberately DIFFERENT (controller ruling ⑤): an
|
|
1679
1707
|
* ambiguous selector and a MISS are ANSWERS — the candidate list, or the
|
|
1680
1708
|
* known ids, returned in band, because the caller can act on them — while a
|
|
@@ -1687,8 +1715,8 @@ class CherryToolEngine {
|
|
|
1687
1715
|
* `cherry_devices` does not come through this method — it goes through
|
|
1688
1716
|
* {@link #selectRecords}, which returns the verdict in band.
|
|
1689
1717
|
*/
|
|
1690
|
-
#requireConnection(device: string): ResolvedDevice | CherryAmbiguousDevice {
|
|
1691
|
-
const resolved = this.#resolve(device)
|
|
1718
|
+
async #requireConnection(device: string): Promise<ResolvedDevice | CherryAmbiguousDevice> {
|
|
1719
|
+
const resolved = await this.#resolve(device)
|
|
1692
1720
|
if (!this.#isVerdict(resolved)) return resolved
|
|
1693
1721
|
if (resolved.error === 'ambiguous-device') return resolved
|
|
1694
1722
|
if (resolved.error === 'not-found') {
|
|
@@ -1710,14 +1738,13 @@ class CherryToolEngine {
|
|
|
1710
1738
|
* replace it with a weaker, wrong one — a miss under a READ-ONLY archive
|
|
1711
1739
|
* would describe a store that still holds a valid token as "no such device".
|
|
1712
1740
|
*/
|
|
1713
|
-
#linkOr(
|
|
1714
|
-
device: string,
|
|
1715
|
-
):
|
|
1741
|
+
async #linkOr(device: string): Promise<
|
|
1716
1742
|
| { link: ResolvedDevice; verdict?: undefined }
|
|
1717
|
-
| { link?: undefined; verdict: CherryAmbiguousDevice | CherryArchiveQuarantined }
|
|
1743
|
+
| { link?: undefined; verdict: CherryAmbiguousDevice | CherryArchiveQuarantined }
|
|
1744
|
+
> {
|
|
1718
1745
|
const quarantined = this.#quarantineVerdict()
|
|
1719
1746
|
if (quarantined !== undefined) return { verdict: quarantined }
|
|
1720
|
-
const resolved = this.#requireConnection(device)
|
|
1747
|
+
const resolved = await this.#requireConnection(device)
|
|
1721
1748
|
if ('error' in resolved) return { verdict: resolved }
|
|
1722
1749
|
return { link: resolved }
|
|
1723
1750
|
}
|
|
@@ -2172,11 +2199,12 @@ class CherryToolEngine {
|
|
|
2172
2199
|
const quarantined = this.#quarantineVerdict()
|
|
2173
2200
|
if (quarantined !== undefined) return quarantined
|
|
2174
2201
|
if (selector === undefined) {
|
|
2175
|
-
|
|
2202
|
+
const records = this.#deps.store.list()
|
|
2203
|
+
return { devices: await Promise.all(records.map((record) => this.#deviceView(record))) }
|
|
2176
2204
|
}
|
|
2177
2205
|
const selected = this.#selectRecords(selector)
|
|
2178
2206
|
if (!Array.isArray(selected)) return selected
|
|
2179
|
-
return { devices: selected.map((record) => this.#deviceView(record)) }
|
|
2207
|
+
return { devices: await Promise.all(selected.map((record) => this.#deviceView(record))) }
|
|
2180
2208
|
},
|
|
2181
2209
|
})
|
|
2182
2210
|
}
|
|
@@ -2208,7 +2236,9 @@ class CherryToolEngine {
|
|
|
2208
2236
|
}
|
|
2209
2237
|
|
|
2210
2238
|
/**
|
|
2211
|
-
* One device's view WITHOUT dialing
|
|
2239
|
+
* One device's view WITHOUT dialing — but after waiting out an attempt that is
|
|
2240
|
+
* already in flight, exactly as {@link #resolve} does, so a cold-start row
|
|
2241
|
+
* does not present a transient `connecting` as the device's answer.
|
|
2212
2242
|
*
|
|
2213
2243
|
* A disabled record is authoritative: the pooled connection is constructed
|
|
2214
2244
|
* with its own `record.enabled` snapshot and reports `disabled` only once
|
|
@@ -2216,11 +2246,18 @@ class CherryToolEngine {
|
|
|
2216
2246
|
* for a device whose connection was never started. Everything else falls back
|
|
2217
2247
|
* to the connection's own CLOSED state set.
|
|
2218
2248
|
*/
|
|
2219
|
-
#deviceView(record: DeviceRecord): CherryDeviceView {
|
|
2249
|
+
async #deviceView(record: DeviceRecord): Promise<CherryDeviceView> {
|
|
2220
2250
|
let state: DeviceState = record.enabled ? 'offline' : 'disabled'
|
|
2221
2251
|
if (record.enabled) {
|
|
2222
2252
|
try {
|
|
2223
|
-
|
|
2253
|
+
const connection = this.#deps.connect(record.deviceId)
|
|
2254
|
+
// Same transient as {@link #resolve} (issue #99): a row rendered while
|
|
2255
|
+
// the first dial is in flight must not present `connecting` as the
|
|
2256
|
+
// device's answer. Bounded, and only for an attempt already running.
|
|
2257
|
+
state =
|
|
2258
|
+
connection.state === 'connecting'
|
|
2259
|
+
? await connection.waitForSettled(this.#deps.deviceSettleWaitMs ?? DEVICE_SETTLE_WAIT_MS)
|
|
2260
|
+
: connection.state
|
|
2224
2261
|
} catch (error) {
|
|
2225
2262
|
// A device that cannot even be resolved to a connection still deserves a
|
|
2226
2263
|
// truthful row; `offline` is the fallback verdict.
|
|
@@ -2268,7 +2305,7 @@ class CherryToolEngine {
|
|
|
2268
2305
|
additionalProperties: false,
|
|
2269
2306
|
},
|
|
2270
2307
|
execute: async (args) => {
|
|
2271
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
2308
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
2272
2309
|
if (resolved.link === undefined) return resolved.verdict
|
|
2273
2310
|
const { connection, record } = resolved.link
|
|
2274
2311
|
const binding: ListResumeBinding = { method: 'agent.agents.list', deviceId: record.deviceId, params: {} }
|
|
@@ -2312,7 +2349,7 @@ class CherryToolEngine {
|
|
|
2312
2349
|
additionalProperties: false,
|
|
2313
2350
|
},
|
|
2314
2351
|
execute: async (args) => {
|
|
2315
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
2352
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
2316
2353
|
if (resolved.link === undefined) return resolved.verdict
|
|
2317
2354
|
const { connection, record } = resolved.link
|
|
2318
2355
|
const agentId = requiredString(args, 'agentId')
|
|
@@ -2367,7 +2404,7 @@ class CherryToolEngine {
|
|
|
2367
2404
|
additionalProperties: false,
|
|
2368
2405
|
},
|
|
2369
2406
|
execute: async (args) => {
|
|
2370
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
2407
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
2371
2408
|
if (resolved.link === undefined) return resolved.verdict
|
|
2372
2409
|
const { connection, record } = resolved.link
|
|
2373
2410
|
const agentId = optionalString(args['agentId'])
|
|
@@ -2466,7 +2503,7 @@ class CherryToolEngine {
|
|
|
2466
2503
|
// a malformed cursor, which would present the newest page again as
|
|
2467
2504
|
// "continued history" — the refusal has to be ours.
|
|
2468
2505
|
const resume = parseMessageResume(args['messagesResumeCursor'])
|
|
2469
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
2506
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
2470
2507
|
if (resolved.link === undefined) return resolved.verdict
|
|
2471
2508
|
const { connection, record } = resolved.link
|
|
2472
2509
|
const sessionId = requiredString(args, 'sessionId')
|
|
@@ -3201,7 +3238,7 @@ class CherryToolEngine {
|
|
|
3201
3238
|
additionalProperties: false,
|
|
3202
3239
|
},
|
|
3203
3240
|
execute: async (args) => {
|
|
3204
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
3241
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
3205
3242
|
if (resolved.link === undefined) return resolved.verdict
|
|
3206
3243
|
const { record } = resolved.link
|
|
3207
3244
|
const sessionId = requiredString(args, 'sessionId')
|
|
@@ -3319,7 +3356,7 @@ class CherryToolEngine {
|
|
|
3319
3356
|
additionalProperties: false,
|
|
3320
3357
|
},
|
|
3321
3358
|
execute: async (args) => {
|
|
3322
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
3359
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
3323
3360
|
if (resolved.link === undefined) return resolved.verdict
|
|
3324
3361
|
const { connection, record } = resolved.link
|
|
3325
3362
|
|
|
@@ -3440,7 +3477,7 @@ class CherryToolEngine {
|
|
|
3440
3477
|
additionalProperties: false,
|
|
3441
3478
|
},
|
|
3442
3479
|
execute: async (args) => {
|
|
3443
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
3480
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
3444
3481
|
if (resolved.link === undefined) return resolved.verdict
|
|
3445
3482
|
const { connection, record } = resolved.link
|
|
3446
3483
|
const sessionId = requiredString(args, 'sessionId')
|
|
@@ -3671,7 +3708,7 @@ class CherryToolEngine {
|
|
|
3671
3708
|
additionalProperties: false,
|
|
3672
3709
|
},
|
|
3673
3710
|
execute: async (args) => {
|
|
3674
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
3711
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
3675
3712
|
if (resolved.link === undefined) return resolved.verdict
|
|
3676
3713
|
const { connection, record } = resolved.link
|
|
3677
3714
|
const sessionId = requiredString(args, 'sessionId')
|
|
@@ -3835,7 +3872,7 @@ class CherryToolEngine {
|
|
|
3835
3872
|
additionalProperties: false,
|
|
3836
3873
|
},
|
|
3837
3874
|
execute: async (args) => {
|
|
3838
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
3875
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
3839
3876
|
if (resolved.link === undefined) return resolved.verdict
|
|
3840
3877
|
const { connection, record } = resolved.link
|
|
3841
3878
|
const sessionId = requiredString(args, 'sessionId')
|
|
@@ -3945,7 +3982,7 @@ class CherryToolEngine {
|
|
|
3945
3982
|
additionalProperties: false,
|
|
3946
3983
|
},
|
|
3947
3984
|
execute: async (args) => {
|
|
3948
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
3985
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
3949
3986
|
if (resolved.link === undefined) return resolved.verdict
|
|
3950
3987
|
const { connection, record } = resolved.link
|
|
3951
3988
|
const sessionId = requiredString(args, 'sessionId')
|
|
@@ -4051,7 +4088,7 @@ class CherryToolEngine {
|
|
|
4051
4088
|
additionalProperties: false,
|
|
4052
4089
|
},
|
|
4053
4090
|
execute: async (args) => {
|
|
4054
|
-
const resolved = this.#linkOr(requiredString(args, 'device'))
|
|
4091
|
+
const resolved = await this.#linkOr(requiredString(args, 'device'))
|
|
4055
4092
|
if (resolved.link === undefined) return resolved.verdict
|
|
4056
4093
|
const { connection, record } = resolved.link
|
|
4057
4094
|
const sessionId = requiredString(args, 'sessionId')
|
package/ui/index.html
CHANGED
package/ui/src/i18n.ts
CHANGED
|
@@ -57,7 +57,7 @@ export const LOCALE_IDS: readonly LocaleId[] = ['zh', 'en']
|
|
|
57
57
|
*/
|
|
58
58
|
const zh: Dict = {
|
|
59
59
|
// -- app shell ------------------------------------------------------------
|
|
60
|
-
'app.title': 'Cherry
|
|
60
|
+
'app.title': 'Cherry 互联',
|
|
61
61
|
'app.subtitle':
|
|
62
62
|
'通过远程传输把本机 DeepSeek Harness 连接到 Cherry Studio 桌面端;设备档案保存在插件数据目录,页面不接触任何凭据。',
|
|
63
63
|
// The one string that used to reach a Chinese page in English (App.tsx's
|
|
@@ -210,7 +210,7 @@ const zh: Dict = {
|
|
|
210
210
|
*/
|
|
211
211
|
const en: Dict = {
|
|
212
212
|
// -- app shell ------------------------------------------------------------
|
|
213
|
-
'app.title': 'Cherry
|
|
213
|
+
'app.title': 'Cherry Bridge',
|
|
214
214
|
'app.subtitle':
|
|
215
215
|
'Connect this machine’s DeepSeek Harness to a Cherry Studio desktop over the remote transport. Device records live in the plugin data directory; this page never touches a credential.',
|
|
216
216
|
'app.eventSourceUnavailable': 'EventSource is unavailable in this environment',
|