dsh-workbuddy-bridge 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -14
- package/README_en.md +22 -19
- package/lib/bin.js +8 -7
- package/lib/client.js +669 -408
- package/lib/{variants-3SmUZ0e_.js → heartbeat-CP6YM98x.js} +137 -104
- package/lib/index.d.ts +12 -3
- package/lib/index.js +90 -10
- package/package.json +14 -13
- package/locale/README.md +0 -10
|
@@ -2342,6 +2342,107 @@ function discoveryIncomplete(detail) {
|
|
|
2342
2342
|
const HELPER_SCRIPT = "process.stdout.write(String(process._linkedBinding(\"electron_browser_workbuddy_storage\").loggerGet()))";
|
|
2343
2343
|
const HELPER_SCRIPT_ARGUMENT_FLAG = "-e";
|
|
2344
2344
|
//#endregion
|
|
2345
|
+
//#region src/shared/paths.ts
|
|
2346
|
+
/** Node-free constants and types shared by the Host and browser halves. */
|
|
2347
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
2348
|
+
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/status";
|
|
2349
|
+
/**
|
|
2350
|
+
* Plugin-owned probe control endpoint.
|
|
2351
|
+
*
|
|
2352
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
2353
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
2354
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
2355
|
+
* also requires the in-memory key the browser half receives with the status
|
|
2356
|
+
* document.
|
|
2357
|
+
*/
|
|
2358
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/probe";
|
|
2359
|
+
/**
|
|
2360
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
2361
|
+
*
|
|
2362
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
2363
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
2364
|
+
* independently, and a shared expression is one build-config drift away from
|
|
2365
|
+
* the desk asking a route the host never mounted.
|
|
2366
|
+
*/
|
|
2367
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/ai/status";
|
|
2368
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/ai/probe";
|
|
2369
|
+
/**
|
|
2370
|
+
* Plugin-owned per-message credit route.
|
|
2371
|
+
*
|
|
2372
|
+
* A read-only GET taking `?sessionId=…`, answering `messageId → credit` for one
|
|
2373
|
+
* Session. It is its own route rather than a field of the status document
|
|
2374
|
+
* because the two have different lifetimes: the status document is per
|
|
2375
|
+
* *account* and is re-read when the account changes, while credit accounting
|
|
2376
|
+
* is per *Session* and grows as messages are sent.
|
|
2377
|
+
*
|
|
2378
|
+
* **One route for both variants, on purpose.** The accounting is attributed to
|
|
2379
|
+
* a DSH `messageId` before it is stored, and a message id belongs to one
|
|
2380
|
+
* Session regardless of which provider served it — a conversation can switch
|
|
2381
|
+
* between the two WorkBuddy products (or away from both) mid-thread. A route
|
|
2382
|
+
* per variant would force the message row to know which provider answered a
|
|
2383
|
+
* message *it is merely labelling*, and would strand every message whose turn
|
|
2384
|
+
* ran under the other variant.
|
|
2385
|
+
*
|
|
2386
|
+
* No key is required: the route only reads what this plugin already observed on
|
|
2387
|
+
* its own loopback stream, carries no credential material, and answers loopback
|
|
2388
|
+
* requests through the same Host/Origin guard as the status route.
|
|
2389
|
+
*/
|
|
2390
|
+
const WORKBUDDY_CREDIT_PATH = "/plugins/dsh-workbuddy-bridge/credit";
|
|
2391
|
+
//#endregion
|
|
2392
|
+
//#region src/variants.ts
|
|
2393
|
+
/**
|
|
2394
|
+
* The two WorkBuddy desktop apps this one plugin serves.
|
|
2395
|
+
*
|
|
2396
|
+
* Both products are the same client framework in different regions, and both
|
|
2397
|
+
* write their sign-in into the *same* shared `CodeBuddyExtension` auth
|
|
2398
|
+
* directory — they differ by file basename, base URL, catalog endpoint, and
|
|
2399
|
+
* display identity. Everything that varies between them is collected here as
|
|
2400
|
+
* one descriptor, so no module has to carry its own `if (international)`
|
|
2401
|
+
* branch and a third variant would be a data change rather than a refactor.
|
|
2402
|
+
*
|
|
2403
|
+
* This module is host-side (it names files and env vars). The browser half
|
|
2404
|
+
* takes the same ids and routes from the Node-free `status-paths.ts`, which
|
|
2405
|
+
* stays the single source shared by both halves.
|
|
2406
|
+
*
|
|
2407
|
+
* @module dsh-workbuddy-bridge/variants
|
|
2408
|
+
*/
|
|
2409
|
+
/** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
|
|
2410
|
+
const WORKBUDDY_VARIANTS = [{
|
|
2411
|
+
id: "workbuddy",
|
|
2412
|
+
displayName: "WorkBuddy",
|
|
2413
|
+
appName: "WorkBuddy",
|
|
2414
|
+
region: "cn",
|
|
2415
|
+
env: "WORKBUDDY_AUTH_FILE",
|
|
2416
|
+
desktopFilename: "workbuddy-desktop.info",
|
|
2417
|
+
ownFilename: ".workbuddy-auth.json",
|
|
2418
|
+
probeFilename: ".workbuddy-probe.json",
|
|
2419
|
+
catalogFilename: ".workbuddy-catalog.json",
|
|
2420
|
+
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
2421
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
2422
|
+
probePath: WORKBUDDY_PROBE_PATH
|
|
2423
|
+
}, {
|
|
2424
|
+
id: "workbuddy-ai",
|
|
2425
|
+
displayName: "WorkBuddy AI",
|
|
2426
|
+
appName: "WorkBuddy AI",
|
|
2427
|
+
region: "global",
|
|
2428
|
+
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
2429
|
+
desktopFilename: "workbuddy-desktop-ai.info",
|
|
2430
|
+
ownFilename: ".workbuddy-ai-auth.json",
|
|
2431
|
+
probeFilename: ".workbuddy-ai-probe.json",
|
|
2432
|
+
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
2433
|
+
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
2434
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
2435
|
+
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
2436
|
+
}];
|
|
2437
|
+
/** The CN variant; the plugin's long-standing default and compatibility anchor. */
|
|
2438
|
+
const CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
2439
|
+
/** The international variant. */
|
|
2440
|
+
const AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
2441
|
+
/** Look up a variant by provider id. */
|
|
2442
|
+
function variantFor(id) {
|
|
2443
|
+
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
2444
|
+
}
|
|
2445
|
+
//#endregion
|
|
2345
2446
|
//#region src/credential/store.ts
|
|
2346
2447
|
/**
|
|
2347
2448
|
* WorkBuddy credential resolution. The primary source is the WorkBuddy
|
|
@@ -2528,6 +2629,38 @@ function isENOENT(error) {
|
|
|
2528
2629
|
return error?.code === "ENOENT";
|
|
2529
2630
|
}
|
|
2530
2631
|
/**
|
|
2632
|
+
* Which automatic at-rest key discovery this store may run, by variant.
|
|
2633
|
+
*
|
|
2634
|
+
* **One home for a rule that had three.** The choice used to be made at every
|
|
2635
|
+
* call site — `src/index.ts` and `src/cli/bin.ts` each spelled out
|
|
2636
|
+
* `discovery: variant.id === CN_VARIANT.id ? cnAppDiscovery() : 'none'` — and the
|
|
2637
|
+
* store's own default was a bare `new WorkBuddyAtRestKeyProvider()`, whose
|
|
2638
|
+
* discovery is `'none'`. So a caller that did neither got a provider that could
|
|
2639
|
+
* never open an encrypted credential: `scripts/live-e2e.mjs` was exactly that
|
|
2640
|
+
* caller, and it failed with `electron-binary-unavailable` on every platform
|
|
2641
|
+
* (2026-10-03, found by the release checklist). The store knows its variant, so
|
|
2642
|
+
* the choice belongs here.
|
|
2643
|
+
*
|
|
2644
|
+
* Why the difference is deliberate:
|
|
2645
|
+
*
|
|
2646
|
+
* - Only the CN WorkBuddy install has been verified to hold the key its envelopes
|
|
2647
|
+
* name, and only its macOS/Windows layout is known, so CN may look for the app.
|
|
2648
|
+
* - A Global (WorkBuddy AI) encrypted credential has never been seen live, so that
|
|
2649
|
+
* variant runs at `'none'` — no default path and no app search. A Global unlock
|
|
2650
|
+
* must not silently execute the *CN* app's Electron. An explicit
|
|
2651
|
+
* `WORKBUDDY_ELECTRON_BIN` still works for Global.
|
|
2652
|
+
*
|
|
2653
|
+
* **No variant** means the CN product's own default file —
|
|
2654
|
+
* `defaultDesktopAuthCandidates()` is the `workbuddy-desktop.info` path, never the
|
|
2655
|
+
* AI one — so it gets the same discovery CN does.
|
|
2656
|
+
*
|
|
2657
|
+
* A keyId mismatch is still reported as a diagnosis rather than a wrong open, and
|
|
2658
|
+
* the helper only runs if an encrypted credential is actually read.
|
|
2659
|
+
*/
|
|
2660
|
+
function atRestDiscoveryFor(variant) {
|
|
2661
|
+
return variant === void 0 || variant.id === CN_VARIANT.id ? cnAppDiscovery() : "none";
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2531
2664
|
* Read-only credential store with demand-driven refresh.
|
|
2532
2665
|
*
|
|
2533
2666
|
* Refresh policy: refresh only when the access token is inside the margin
|
|
@@ -2549,7 +2682,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
2549
2682
|
this.refresh = options.refresh;
|
|
2550
2683
|
this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
|
|
2551
2684
|
this.ownPath = options.ownPath ?? (options.variant ? join(resolveDshHome(), options.variant.ownFilename) : workbuddyOwnAuthPath());
|
|
2552
|
-
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider();
|
|
2685
|
+
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider({ discovery: atRestDiscoveryFor(options.variant) });
|
|
2553
2686
|
this.desktopPathOverride = options.desktopPath;
|
|
2554
2687
|
}
|
|
2555
2688
|
/**
|
|
@@ -2624,6 +2757,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
2624
2757
|
expiresAtMs: credential.expiresAtMs,
|
|
2625
2758
|
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
|
|
2626
2759
|
...credential.nickname === void 0 ? {} : { nickname: credential.nickname },
|
|
2760
|
+
...credential.uid === "" ? {} : { uid: credential.uid },
|
|
2627
2761
|
...credential.domain === "" ? {} : { domain: credential.domain },
|
|
2628
2762
|
source: credential.source
|
|
2629
2763
|
};
|
|
@@ -3553,55 +3687,8 @@ var WorkBuddyCatalog = class {
|
|
|
3553
3687
|
}
|
|
3554
3688
|
};
|
|
3555
3689
|
//#endregion
|
|
3556
|
-
//#region src/shared/paths.ts
|
|
3557
|
-
/** Node-free constants and types shared by the Host and browser halves. */
|
|
3558
|
-
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
3559
|
-
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/status";
|
|
3560
|
-
/**
|
|
3561
|
-
* Plugin-owned probe control endpoint.
|
|
3562
|
-
*
|
|
3563
|
-
* Separate from the status route because it accepts writes: the status route's
|
|
3564
|
-
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
3565
|
-
* not the same as authorizing a state-changing action. This route therefore
|
|
3566
|
-
* also requires the in-memory key the browser half receives with the status
|
|
3567
|
-
* document.
|
|
3568
|
-
*/
|
|
3569
|
-
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/probe";
|
|
3570
|
-
/**
|
|
3571
|
-
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
3572
|
-
*
|
|
3573
|
-
* Kept as separate constants rather than a computed suffix so both halves
|
|
3574
|
-
* reference literal strings: the browser bundle and the host bundle are built
|
|
3575
|
-
* independently, and a shared expression is one build-config drift away from
|
|
3576
|
-
* the desk asking a route the host never mounted.
|
|
3577
|
-
*/
|
|
3578
|
-
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/ai/status";
|
|
3579
|
-
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/ai/probe";
|
|
3580
|
-
/**
|
|
3581
|
-
* Plugin-owned per-message credit route.
|
|
3582
|
-
*
|
|
3583
|
-
* A read-only GET taking `?sessionId=…`, answering `messageId → credit` for one
|
|
3584
|
-
* Session. It is its own route rather than a field of the status document
|
|
3585
|
-
* because the two have different lifetimes: the status document is per
|
|
3586
|
-
* *account* and is re-read when the account changes, while credit accounting
|
|
3587
|
-
* is per *Session* and grows as messages are sent.
|
|
3588
|
-
*
|
|
3589
|
-
* **One route for both variants, on purpose.** The accounting is attributed to
|
|
3590
|
-
* a DSH `messageId` before it is stored, and a message id belongs to one
|
|
3591
|
-
* Session regardless of which provider served it — a conversation can switch
|
|
3592
|
-
* between the two WorkBuddy products (or away from both) mid-thread. A route
|
|
3593
|
-
* per variant would force the message row to know which provider answered a
|
|
3594
|
-
* message *it is merely labelling*, and would strand every message whose turn
|
|
3595
|
-
* ran under the other variant.
|
|
3596
|
-
*
|
|
3597
|
-
* No key is required: the route only reads what this plugin already observed on
|
|
3598
|
-
* its own loopback stream, carries no credential material, and answers loopback
|
|
3599
|
-
* requests through the same Host/Origin guard as the status route.
|
|
3600
|
-
*/
|
|
3601
|
-
const WORKBUDDY_CREDIT_PATH = "/plugins/dsh-workbuddy-bridge/credit";
|
|
3602
|
-
//#endregion
|
|
3603
3690
|
//#region src/version.ts
|
|
3604
|
-
const WORKBUDDY_BRIDGE_VERSION = "0.
|
|
3691
|
+
const WORKBUDDY_BRIDGE_VERSION = "0.4.0";
|
|
3605
3692
|
//#endregion
|
|
3606
3693
|
//#region src/web/heartbeat.ts
|
|
3607
3694
|
/**
|
|
@@ -3772,58 +3859,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
3772
3859
|
return startAtMs <= heartbeat.registeredAt;
|
|
3773
3860
|
}
|
|
3774
3861
|
//#endregion
|
|
3775
|
-
|
|
3776
|
-
/**
|
|
3777
|
-
* The two WorkBuddy desktop apps this one plugin serves.
|
|
3778
|
-
*
|
|
3779
|
-
* Both products are the same client framework in different regions, and both
|
|
3780
|
-
* write their sign-in into the *same* shared `CodeBuddyExtension` auth
|
|
3781
|
-
* directory — they differ by file basename, base URL, catalog endpoint, and
|
|
3782
|
-
* display identity. Everything that varies between them is collected here as
|
|
3783
|
-
* one descriptor, so no module has to carry its own `if (international)`
|
|
3784
|
-
* branch and a third variant would be a data change rather than a refactor.
|
|
3785
|
-
*
|
|
3786
|
-
* This module is host-side (it names files and env vars). The browser half
|
|
3787
|
-
* takes the same ids and routes from the Node-free `status-paths.ts`, which
|
|
3788
|
-
* stays the single source shared by both halves.
|
|
3789
|
-
*
|
|
3790
|
-
* @module dsh-workbuddy-bridge/variants
|
|
3791
|
-
*/
|
|
3792
|
-
/** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
|
|
3793
|
-
const WORKBUDDY_VARIANTS = [{
|
|
3794
|
-
id: "workbuddy",
|
|
3795
|
-
displayName: "WorkBuddy",
|
|
3796
|
-
appName: "WorkBuddy",
|
|
3797
|
-
region: "cn",
|
|
3798
|
-
env: "WORKBUDDY_AUTH_FILE",
|
|
3799
|
-
desktopFilename: "workbuddy-desktop.info",
|
|
3800
|
-
ownFilename: ".workbuddy-auth.json",
|
|
3801
|
-
probeFilename: ".workbuddy-probe.json",
|
|
3802
|
-
catalogFilename: ".workbuddy-catalog.json",
|
|
3803
|
-
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
3804
|
-
statusPath: WORKBUDDY_STATUS_PATH,
|
|
3805
|
-
probePath: WORKBUDDY_PROBE_PATH
|
|
3806
|
-
}, {
|
|
3807
|
-
id: "workbuddy-ai",
|
|
3808
|
-
displayName: "WorkBuddy AI",
|
|
3809
|
-
appName: "WorkBuddy AI",
|
|
3810
|
-
region: "global",
|
|
3811
|
-
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
3812
|
-
desktopFilename: "workbuddy-desktop-ai.info",
|
|
3813
|
-
ownFilename: ".workbuddy-ai-auth.json",
|
|
3814
|
-
probeFilename: ".workbuddy-ai-probe.json",
|
|
3815
|
-
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
3816
|
-
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
3817
|
-
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
3818
|
-
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
3819
|
-
}];
|
|
3820
|
-
/** The CN variant; the plugin's long-standing default and compatibility anchor. */
|
|
3821
|
-
const CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
3822
|
-
/** The international variant. */
|
|
3823
|
-
const AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
3824
|
-
/** Look up a variant by provider id. */
|
|
3825
|
-
function variantFor(id) {
|
|
3826
|
-
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
3827
|
-
}
|
|
3828
|
-
//#endregion
|
|
3829
|
-
export { modelWithCurrentPromotion as A, CN_APP_VERSION_FILENAME as B, defaultDesktopAuthPath as C, WorkBuddyAtRestKeyProvider as D, workbuddyOwnAuthPath as E, regionOf as F, resolveChatIdentity as G, chatUserAgent as H, PROBE_EFFORT_CANDIDATES as I, appUserAgent as J, validCliVersion as K, probeModel as L, parseModelCatalog as M, prepareChatBody as N, cnAppDiscovery as O, prepareInternationalChatBody as P, validAppVersion as Q, randomSentinel as R, defaultDesktopAuthCandidates as S, parseWorkBuddyAuth as T, fallbackChatIdentity as U, FALLBACK_CN_APP_VERSION as V, readCliVersion as W, readBundleVersion as X, installedAppVersion as Y, resolveAppVersion as Z, FALLBACK_WORKBUDDY_MODELS as _, WORKBUDDY_HOST_HEARTBEAT_FILENAME as a, WORKBUDDY_AUTH_FILE_ENV as b, processStartTimeMs as c, writeHostHeartbeat as d, WORKBUDDY_BRIDGE_VERSION as f, FALLBACK_WORKBUDDY_AI_MODELS as g, WORKBUDDY_STATUS_PATH as h, variantFor as i, normalizeCredits as j, WorkBuddyUpstreamClient as k, readHostHeartbeat as l, WORKBUDDY_PROBE_PATH as m, CN_VARIANT as n, clearHostHeartbeat as o, WORKBUDDY_CREDIT_PATH as p, WORKBUDDY_APP_VERSION_FILENAME as q, WORKBUDDY_VARIANTS as r, isHeartbeatProcessAlive as s, AI_VARIANT as t, workbuddyHostHeartbeatPath as u, WorkBuddyCatalog as v, desktopAuthCandidatesFor as w, WorkBuddyCredentialStore as x, WORKBUDDY_AUTH_FILENAME as y, classifyUpstreamError as z };
|
|
3862
|
+
export { parseModelCatalog as A, chatUserAgent as B, variantFor as C, WorkBuddyUpstreamClient as D, WORKBUDDY_STATUS_PATH as E, probeModel as F, WORKBUDDY_APP_VERSION_FILENAME as G, readCliVersion as H, randomSentinel as I, readBundleVersion as J, appUserAgent as K, classifyUpstreamError as L, prepareInternationalChatBody as M, regionOf as N, modelWithCurrentPromotion as O, PROBE_EFFORT_CANDIDATES as P, CN_APP_VERSION_FILENAME as R, WORKBUDDY_VARIANTS as S, WORKBUDDY_PROBE_PATH as T, resolveChatIdentity as U, fallbackChatIdentity as V, validCliVersion as W, validAppVersion as X, resolveAppVersion as Y, desktopAuthCandidatesFor as _, readHostHeartbeat as a, AI_VARIANT as b, WORKBUDDY_BRIDGE_VERSION as c, WorkBuddyCatalog as d, WORKBUDDY_AUTH_FILENAME as f, defaultDesktopAuthPath as g, defaultDesktopAuthCandidates as h, processStartTimeMs as i, prepareChatBody as j, normalizeCredits as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, installedAppVersion as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, parseWorkBuddyAuth as v, WORKBUDDY_CREDIT_PATH as w, CN_VARIANT as x, workbuddyOwnAuthPath as y, FALLBACK_CN_APP_VERSION as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -906,6 +906,14 @@ interface WorkBuddyAuthStatus {
|
|
|
906
906
|
expiresAtMs?: number;
|
|
907
907
|
refreshExpiresAtMs?: number;
|
|
908
908
|
nickname?: string;
|
|
909
|
+
/**
|
|
910
|
+
* The account's own id (`account.uid`), when the credential carries one.
|
|
911
|
+
*
|
|
912
|
+
* The fallback display name: the desktop document is the only source of a
|
|
913
|
+
* nickname, and it does not always have one. Non-secret — the same identity
|
|
914
|
+
* the per-account state is keyed by, sent upstream as `X-User-Id`.
|
|
915
|
+
*/
|
|
916
|
+
uid?: string;
|
|
909
917
|
domain?: string;
|
|
910
918
|
source?: 'desktop' | 'dsh';
|
|
911
919
|
/**
|
|
@@ -933,9 +941,10 @@ interface WorkBuddyStoreOptions {
|
|
|
933
941
|
refreshMarginMs?: number;
|
|
934
942
|
/**
|
|
935
943
|
* Resolver for WorkBuddy 5.6's at-rest protector key, needed when the
|
|
936
|
-
* desktop file stores encrypted token fields. Defaults to the real
|
|
937
|
-
*
|
|
938
|
-
*
|
|
944
|
+
* desktop file stores encrypted token fields. Defaults to the real provider
|
|
945
|
+
* with the discovery this store's variant may run — see
|
|
946
|
+
* {@link atRestDiscoveryFor}. Tests stand in a stub; the shape is structural
|
|
947
|
+
* so a store never depends on how the key is reached.
|
|
939
948
|
*/
|
|
940
949
|
keyProvider?: Pick<WorkBuddyAtRestKeyProvider, 'protectorKeyFor' | 'helperPath'>;
|
|
941
950
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as parseModelCatalog, B as chatUserAgent, C as variantFor, D as WorkBuddyUpstreamClient, F as probeModel, G as WORKBUDDY_APP_VERSION_FILENAME, H as readCliVersion, I as randomSentinel, J as readBundleVersion, K as appUserAgent, L as classifyUpstreamError, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as CN_APP_VERSION_FILENAME, S as WORKBUDDY_VARIANTS, U as resolveChatIdentity, V as fallbackChatIdentity, W as validCliVersion, X as validAppVersion, Y as resolveAppVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as AI_VARIANT, c as WORKBUDDY_BRIDGE_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as prepareChatBody, k as normalizeCredits, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as installedAppVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as WORKBUDDY_CREDIT_PATH, x as CN_VARIANT, y as workbuddyOwnAuthPath, z as FALLBACK_CN_APP_VERSION } from "./heartbeat-CP6YM98x.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -6,7 +6,7 @@ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
8
8
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
9
|
-
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
9
|
+
import { ReasoningEffortId, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
10
10
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
12
|
import { Readable } from "node:stream";
|
|
@@ -303,6 +303,39 @@ function resolveEfforts(info, observed) {
|
|
|
303
303
|
canDisable: false
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* The level a switchable model defaults to, or undefined when it has no switch.
|
|
308
|
+
*
|
|
309
|
+
* **Why this is not simply the upstream's `defaultEffort`.** The host's model
|
|
310
|
+
* picker adds a "Default" item — "follow whatever the provider does" — exactly
|
|
311
|
+
* when a model reports no default; reporting one is what removes that item. The
|
|
312
|
+
* upstream's own answer is used whenever it names a level the offered set
|
|
313
|
+
* contains, because that is the only defensible source: it is what the provider
|
|
314
|
+
* itself would do with no `reasoning_effort` on the wire.
|
|
315
|
+
*
|
|
316
|
+
* `high` backs it up otherwise, and the asymmetry is deliberate. A row can
|
|
317
|
+
* declare a set while naming no default, and a stale `defaultEffort` can name a
|
|
318
|
+
* level a narrower declared set no longer offers (`defaultEffort: 'low'` beside
|
|
319
|
+
* `supportedEfforts: ['high']`). Both leave the level unpinned, and the chosen
|
|
320
|
+
* behaviour is to pin it anyway: a wrong-but-explicit level is a one-click fix
|
|
321
|
+
* in the picker, while a silent "follow the provider" is not even visible.
|
|
322
|
+
*
|
|
323
|
+
* The fallback therefore never strays outside the offered set. A model whose set
|
|
324
|
+
* excludes `high` falls back to its first offered level — naming a level the
|
|
325
|
+
* model cannot accept would make every request that omits an effort fail
|
|
326
|
+
* `UNSUPPORTED_REASONING_EFFORT`, which is the failure the `reasoning-merge`
|
|
327
|
+
* specs exist to prevent.
|
|
328
|
+
*
|
|
329
|
+
* @param info - the catalog row, whose `reasoning.defaultEffort` may name one.
|
|
330
|
+
* @param offered - the offered levels, in order; the answer is always a member.
|
|
331
|
+
* @returns a member of `offered`, or undefined when nothing is offered.
|
|
332
|
+
*/
|
|
333
|
+
function resolveDefaultEffort(info, offered) {
|
|
334
|
+
if (offered.length === 0) return void 0;
|
|
335
|
+
const declared = info.reasoning?.defaultEffort;
|
|
336
|
+
if (declared !== void 0 && offered.includes(declared)) return declared;
|
|
337
|
+
return offered.includes("high") ? "high" : offered[0];
|
|
338
|
+
}
|
|
306
339
|
//#endregion
|
|
307
340
|
//#region src/llm/adapter.ts
|
|
308
341
|
/**
|
|
@@ -498,7 +531,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
498
531
|
};
|
|
499
532
|
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
500
533
|
return {
|
|
501
|
-
adapter: new WorkBuddyPiAiAdapter(catalog, hidden ?? (() => []), {
|
|
534
|
+
adapter: new WorkBuddyPiAiAdapter(catalog, hidden ?? (() => []), observe ?? (() => void 0), {
|
|
502
535
|
profiles: () => profiles,
|
|
503
536
|
auth: INERT_AUTH,
|
|
504
537
|
resolveApiKey: async () => shim.token(),
|
|
@@ -520,6 +553,14 @@ function createWorkBuddyAdapter(options) {
|
|
|
520
553
|
* capability resolution, and effort mapping stay exactly as `dsh-llm-pi-ai`
|
|
521
554
|
* implements them.
|
|
522
555
|
*
|
|
556
|
+
* `resolveModel()` and `prepareCall()` additionally answer the *default*
|
|
557
|
+
* thinking level. `dsh-llm-pi-ai` can only report a default from a whole-route
|
|
558
|
+
* profile field (`profile.reasoning`), which cannot express one level per
|
|
559
|
+
* model, so the per-model answer has to be attached here. Two call sites are
|
|
560
|
+
* required rather than one: the picker reads `resolveModel()`, while the
|
|
561
|
+
* request path reads `prepareCall()`, and a default present on only one of them
|
|
562
|
+
* would show the user a level the wire does not actually send.
|
|
563
|
+
*
|
|
523
564
|
* A model missing from the catalog (an id the shim would serve but the last
|
|
524
565
|
* upstream refresh did not list) falls through with its name untouched rather
|
|
525
566
|
* than being dropped: catalog membership is advisory, and the seam tolerates
|
|
@@ -528,15 +569,48 @@ function createWorkBuddyAdapter(options) {
|
|
|
528
569
|
var WorkBuddyPiAiAdapter = class extends PiAiAdapter {
|
|
529
570
|
catalog;
|
|
530
571
|
hidden;
|
|
531
|
-
|
|
572
|
+
observe;
|
|
573
|
+
constructor(catalog, hidden, observe, options) {
|
|
532
574
|
super(options);
|
|
533
575
|
this.catalog = catalog;
|
|
534
576
|
this.hidden = hidden;
|
|
577
|
+
this.observe = observe;
|
|
535
578
|
}
|
|
536
579
|
/** Catalog entry for one model id, or undefined when the catalog omits it. */
|
|
537
580
|
infoFor(model) {
|
|
538
581
|
return this.catalog.current().find((entry) => entry.id === model);
|
|
539
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* The default level for one model, or undefined when it has no switch.
|
|
585
|
+
*
|
|
586
|
+
* Derived from the *same* switch the picker is built from — `resolveEfforts`
|
|
587
|
+
* with the same observation the adapter passes to `reasoningFields` — so the
|
|
588
|
+
* default can never name a level this model does not offer.
|
|
589
|
+
*/
|
|
590
|
+
defaultEffortFor(model) {
|
|
591
|
+
const info = this.infoFor(model);
|
|
592
|
+
if (info === void 0) return void 0;
|
|
593
|
+
const effort = resolveDefaultEffort(info, resolveEfforts(info, this.observe(model)).efforts);
|
|
594
|
+
return effort === void 0 ? void 0 : ReasoningEffortId(effort);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Attach the per-model default to an already-resolved model answer.
|
|
598
|
+
*
|
|
599
|
+
* Written as a rewrite of `reasoning` rather than a spread of the whole
|
|
600
|
+
* answer: `reasoning.defaultEffort` must stay absent — not `undefined` — on a
|
|
601
|
+
* model with no switch, because the host reads absence as "offer Default".
|
|
602
|
+
*/
|
|
603
|
+
withDefaultEffort(resolved) {
|
|
604
|
+
const effort = this.defaultEffortFor(resolved.id);
|
|
605
|
+
if (effort === void 0 || resolved.reasoning === void 0) return resolved;
|
|
606
|
+
return {
|
|
607
|
+
...resolved,
|
|
608
|
+
reasoning: {
|
|
609
|
+
...resolved.reasoning,
|
|
610
|
+
defaultEffort: effort
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
540
614
|
async listModels(provider) {
|
|
541
615
|
const models = await super.listModels(provider);
|
|
542
616
|
const hidden = new Set(this.hidden());
|
|
@@ -553,11 +627,18 @@ var WorkBuddyPiAiAdapter = class extends PiAiAdapter {
|
|
|
553
627
|
async resolveModel(provider, model, signal) {
|
|
554
628
|
const resolved = await super.resolveModel(provider, model, signal);
|
|
555
629
|
const info = this.infoFor(model);
|
|
556
|
-
|
|
557
|
-
return {
|
|
630
|
+
const named = info === void 0 ? resolved : {
|
|
558
631
|
...resolved,
|
|
559
632
|
name: withCatalogDisplay(resolved.name, info)
|
|
560
633
|
};
|
|
634
|
+
return this.withDefaultEffort(named);
|
|
635
|
+
}
|
|
636
|
+
async prepareCall(provider, model, signal) {
|
|
637
|
+
const prepared = await super.prepareCall(provider, model, signal);
|
|
638
|
+
return {
|
|
639
|
+
...prepared,
|
|
640
|
+
model: this.withDefaultEffort(prepared.model)
|
|
641
|
+
};
|
|
561
642
|
}
|
|
562
643
|
};
|
|
563
644
|
//#endregion
|
|
@@ -1467,6 +1548,7 @@ async function workBuddyWebStatus(deps) {
|
|
|
1467
1548
|
const status = {
|
|
1468
1549
|
status: "signed-in",
|
|
1469
1550
|
...authStatus.nickname === void 0 ? {} : { nickname: authStatus.nickname },
|
|
1551
|
+
...authStatus.uid === void 0 ? {} : { uid: authStatus.uid },
|
|
1470
1552
|
...authStatus.domain === void 0 || authStatus.domain === "" ? {} : { domain: authStatus.domain },
|
|
1471
1553
|
...authStatus.source === void 0 ? {} : { source: authStatus.source },
|
|
1472
1554
|
...authStatus.expiresAtMs === void 0 ? {} : { expiresAt: authStatus.expiresAtMs }
|
|
@@ -1913,13 +1995,12 @@ function fallbackFor(variant) {
|
|
|
1913
1995
|
return variant.id === CN_VARIANT.id ? FALLBACK_WORKBUDDY_MODELS : FALLBACK_WORKBUDDY_AI_MODELS;
|
|
1914
1996
|
}
|
|
1915
1997
|
/** Build one variant's stores and probe state. */
|
|
1916
|
-
function createVariantRuntime(config, variant, identityOf, accountOf,
|
|
1998
|
+
function createVariantRuntime(config, variant, identityOf, accountOf, creditIndex) {
|
|
1917
1999
|
const client = new WorkBuddyUpstreamClient();
|
|
1918
2000
|
const configured = configuredAuthFile(config, variant);
|
|
1919
2001
|
const store = new WorkBuddyCredentialStore({
|
|
1920
2002
|
variant,
|
|
1921
2003
|
...configured === void 0 ? {} : { desktopPath: configured },
|
|
1922
|
-
...keyProvider === void 0 ? {} : { keyProvider },
|
|
1923
2004
|
refresh: (credential) => client.refreshToken(credential)
|
|
1924
2005
|
});
|
|
1925
2006
|
const fallback = fallbackFor(variant);
|
|
@@ -2121,9 +2202,8 @@ function apply(ctx, refs) {
|
|
|
2121
2202
|
* case that must not fall back to a shared preference bucket.
|
|
2122
2203
|
*/
|
|
2123
2204
|
const lastAccounts = /* @__PURE__ */ new Map();
|
|
2124
|
-
const atRestKeysFor = (variant) => new WorkBuddyAtRestKeyProvider({ discovery: variant.id === CN_VARIANT.id ? cnAppDiscovery() : "none" });
|
|
2125
2205
|
const creditIndex = new WorkBuddySessionCreditIndex();
|
|
2126
|
-
const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config(), variant, (id) => lastIdentities.get(id), (id) => lastAccounts.get(id),
|
|
2206
|
+
const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config(), variant, (id) => lastIdentities.get(id), (id) => lastAccounts.get(id), creditIndex));
|
|
2127
2207
|
const probeKey = createProbeKey();
|
|
2128
2208
|
/**
|
|
2129
2209
|
* Point a variant at an account identity, invalidating whatever the previous
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-workbuddy-bridge",
|
|
3
3
|
"displayName": "DSH WorkBuddy Bridge",
|
|
4
4
|
"description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"author": "zlZayn",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh",
|
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"dsh-workbuddy-bridge": "lib/bin.js"
|
|
26
26
|
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepack": "node node_modules/tsdown/dist/run.mjs",
|
|
29
|
+
"build": "tsdown",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"format": "prettier --write .",
|
|
33
|
+
"format:check": "prettier --check .",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
35
|
+
"check:release": "node scripts/check-release.mjs",
|
|
36
|
+
"check": "pnpm run typecheck && pnpm run build && pnpm run test"
|
|
37
|
+
},
|
|
27
38
|
"exports": {
|
|
28
39
|
".": {
|
|
29
40
|
"types": "./lib/index.d.ts",
|
|
@@ -135,7 +146,7 @@
|
|
|
135
146
|
"eslint-config-prettier": "^10.1.8",
|
|
136
147
|
"globals": "^17.12.0",
|
|
137
148
|
"immer": "^11.1.18",
|
|
138
|
-
"katex": "^0.
|
|
149
|
+
"katex": "^0.18.2",
|
|
139
150
|
"lightningcss": "^1.33.0",
|
|
140
151
|
"mdast-util-from-markdown": "^2.0.3",
|
|
141
152
|
"mdast-util-gfm": "^3.1.0",
|
|
@@ -160,15 +171,5 @@
|
|
|
160
171
|
"unrun": "^0.3.1",
|
|
161
172
|
"vitest": "^4.1.11",
|
|
162
173
|
"zustand": "^5.0.15"
|
|
163
|
-
},
|
|
164
|
-
"scripts": {
|
|
165
|
-
"build": "tsdown",
|
|
166
|
-
"test": "vitest run",
|
|
167
|
-
"lint": "eslint .",
|
|
168
|
-
"format": "prettier --write .",
|
|
169
|
-
"format:check": "prettier --check .",
|
|
170
|
-
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
171
|
-
"check:release": "node scripts/check-release.mjs",
|
|
172
|
-
"check": "pnpm run typecheck && pnpm run build && pnpm run test"
|
|
173
174
|
}
|
|
174
|
-
}
|
|
175
|
+
}
|
package/locale/README.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# locale/ — 包展示元数据
|
|
2
|
-
|
|
3
|
-
- 职责:宿主直读的**包展示元数据**(插件页上的标题与一句话说明)。
|
|
4
|
-
不是插件界面文案 —— 那在 [../src/client/locales.ts](../src/client/locales.ts),运行时经 `ctx.locale` 注册。
|
|
5
|
-
- `en.json`:`meta.title` + `meta.description` 的英文。宿主先解析这一份,它不在就不会读其他语言文件。
|
|
6
|
-
- `zh.json`:同一键集的中文。
|
|
7
|
-
- 生效条件:随包发布(`package.json` 的 `files` 收了 `locale/*.json`),宿主从 `<包名>/locale/<语言>.json` 读;
|
|
8
|
-
不进 `lib/`,本仓代码一个字节都不读它。
|
|
9
|
-
- 被谁依赖:宿主插件页;发布面断言在 [../scripts/check-release.mjs](../scripts/check-release.mjs)。
|
|
10
|
-
- 回落链、字段形状、改动波及面(规则层)→ 见 [AGENTS.md](AGENTS.md)
|