dsh-connect-workbuddy 1.3.0 → 2.0.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/CHANGELOG.md +72 -0
- package/README.en.md +14 -10
- package/README.md +14 -10
- package/lib/bin.d.ts +3 -1
- package/lib/bin.js +103 -69
- package/lib/client.js +199 -69
- package/lib/{host-heartbeat-UlZjd09_.js → host-heartbeat-sqjxgXkW.js} +1192 -884
- package/lib/index.d.ts +209 -37
- package/lib/index.js +248 -90
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as prepareChatBody, C as workbuddyAccountId, D as parseCreditMultiplier, E as classifyUpstreamError, O as parseReasoning, S as parseWorkBuddyAuth, T as WorkBuddyUpstreamClient, _ as authFileName, a as readHostHeartbeat, b as defaultDesktopAuthPath, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, i as processStartTimeMs, j as regionOf, k as parseUpstreamModel, l as FALLBACK_WORKBUDDY_MODELS, m as WORKBUDDY_AUTH_FILENAME, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-sqjxgXkW.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -9,9 +9,11 @@ import { createServer } from "node:http";
|
|
|
9
9
|
import { Readable } from "node:stream";
|
|
10
10
|
//#region src/adapter.ts
|
|
11
11
|
/**
|
|
12
|
-
* The
|
|
12
|
+
* The WorkBuddy pi-ai providers: loopback-backed adapters registered
|
|
13
13
|
* into the Harness LLM seam, assembled from public `dsh-llm-pi-ai`
|
|
14
|
-
* extension points.
|
|
14
|
+
* extension points. One instance per region — `workbuddy` for the domestic
|
|
15
|
+
* gateway, `workbuddy-global` for the international one — each pointing at
|
|
16
|
+
* its own shim and catalog so the two regions serve simultaneously.
|
|
15
17
|
*
|
|
16
18
|
* 参考:corrinehu/dsh-workbuddy-connect(MIT,Copyright (c) 2026 Corrine Hu)
|
|
17
19
|
* — pi-ai provider 的装配方式(createProvider + openAICompletionsApi +
|
|
@@ -19,12 +21,28 @@ import { Readable } from "node:stream";
|
|
|
19
21
|
* DSH 插件结构与 provider 注册的思路参照
|
|
20
22
|
* franksong2702/dsh-codex-connect(Apache-2.0),经其转引。
|
|
21
23
|
* 改动:模型描述符补上 upstream 给出的多模态与推理档位信息(若有),
|
|
22
|
-
* 供 DSH
|
|
24
|
+
* 供 DSH 的能力判断使用;工厂参数化 provider id,支持双区域实例。
|
|
23
25
|
*
|
|
24
26
|
* @module dsh-connect-workbuddy/adapter
|
|
25
27
|
*/
|
|
26
|
-
/** Provider route this bundle owns. */
|
|
28
|
+
/** Provider route this bundle owns for the domestic (CN) gateway. */
|
|
27
29
|
const WORKBUDDY_PROVIDER = "workbuddy";
|
|
30
|
+
/** Provider route this bundle owns for the international gateway. */
|
|
31
|
+
const WORKBUDDY_GLOBAL_PROVIDER = "workbuddy-global";
|
|
32
|
+
/** The provider id each region registers as. */
|
|
33
|
+
const WORKBUDDY_PROVIDERS = {
|
|
34
|
+
cn: WORKBUDDY_PROVIDER,
|
|
35
|
+
global: WORKBUDDY_GLOBAL_PROVIDER
|
|
36
|
+
};
|
|
37
|
+
/** Region a provider route id belongs to. */
|
|
38
|
+
function regionOfProvider(provider) {
|
|
39
|
+
for (const [region, id] of Object.entries(WORKBUDDY_PROVIDERS)) if (id === provider) return region;
|
|
40
|
+
}
|
|
41
|
+
/** Human-readable provider name, shown in the DSH model picker. */
|
|
42
|
+
const WORKBUDDY_PROVIDER_DISPLAY_NAMES = {
|
|
43
|
+
cn: "WorkBuddy",
|
|
44
|
+
global: "WorkBuddy Global"
|
|
45
|
+
};
|
|
28
46
|
/** Provider idle ceiling while one stream read is outstanding. */
|
|
29
47
|
const WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
30
48
|
/**
|
|
@@ -103,13 +121,13 @@ function workBuddyThinkingLevelMap(info) {
|
|
|
103
121
|
return map;
|
|
104
122
|
}
|
|
105
123
|
/** Build one pi-ai model descriptor pointing at the loopback shim. */
|
|
106
|
-
function toPiModel(info, baseUrl) {
|
|
124
|
+
function toPiModel(info, baseUrl, providerId) {
|
|
107
125
|
const thinkingLevelMap = workBuddyThinkingLevelMap(info);
|
|
108
126
|
return {
|
|
109
127
|
id: info.id,
|
|
110
128
|
name: workBuddyDisplayName(info),
|
|
111
129
|
api: "openai-completions",
|
|
112
|
-
provider:
|
|
130
|
+
provider: providerId,
|
|
113
131
|
baseUrl,
|
|
114
132
|
input: workBuddyModelInput(info),
|
|
115
133
|
cost: NO_COST,
|
|
@@ -127,14 +145,16 @@ function toPiModel(info, baseUrl) {
|
|
|
127
145
|
*/
|
|
128
146
|
function createWorkBuddyAdapter(options) {
|
|
129
147
|
const { shim, store, catalog, resolveAttachments } = options;
|
|
148
|
+
const providerId = options.provider ?? "workbuddy";
|
|
149
|
+
const providerName = options.displayName ?? "WorkBuddy";
|
|
130
150
|
const buildModels = () => {
|
|
131
151
|
const baseUrl = `${shim.baseUrl()}/v1`;
|
|
132
|
-
return catalog.current().map((info) => toPiModel(info, baseUrl));
|
|
152
|
+
return catalog.current().map((info) => toPiModel(info, baseUrl, providerId));
|
|
133
153
|
};
|
|
134
154
|
const provider = {
|
|
135
155
|
...createProvider({
|
|
136
|
-
id:
|
|
137
|
-
name:
|
|
156
|
+
id: providerId,
|
|
157
|
+
name: providerName,
|
|
138
158
|
auth: { apiKey: {
|
|
139
159
|
name: "WorkBuddy OAuth bearer token",
|
|
140
160
|
async resolve({ credential }) {
|
|
@@ -151,8 +171,8 @@ function createWorkBuddyAdapter(options) {
|
|
|
151
171
|
getModels: () => buildModels()
|
|
152
172
|
};
|
|
153
173
|
const profile = {
|
|
154
|
-
provider:
|
|
155
|
-
displayName:
|
|
174
|
+
provider: providerId,
|
|
175
|
+
displayName: providerName,
|
|
156
176
|
streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
|
|
157
177
|
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-workbuddy retryPolicy"),
|
|
158
178
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
@@ -160,7 +180,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
160
180
|
...REQUEST_IMAGE_BUDGETS,
|
|
161
181
|
piProvider: provider
|
|
162
182
|
};
|
|
163
|
-
let profiles = /* @__PURE__ */ new Map([[
|
|
183
|
+
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
164
184
|
return {
|
|
165
185
|
adapter: new PiAiAdapter({
|
|
166
186
|
profiles: () => profiles,
|
|
@@ -169,7 +189,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
169
189
|
...resolveAttachments === void 0 ? {} : { resolveAttachments }
|
|
170
190
|
}),
|
|
171
191
|
invalidate: () => {
|
|
172
|
-
profiles = /* @__PURE__ */ new Map([[
|
|
192
|
+
profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
173
193
|
}
|
|
174
194
|
};
|
|
175
195
|
}
|
|
@@ -424,6 +444,28 @@ const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/ref
|
|
|
424
444
|
const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
|
|
425
445
|
/** Plugin-owned daily check-in action endpoint. */
|
|
426
446
|
const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
447
|
+
/** Query parameter naming the region a card request addresses. */
|
|
448
|
+
const WORKBUDDY_REGION_PARAM = "region";
|
|
449
|
+
/** Every region, in card tab order. */
|
|
450
|
+
const WORKBUDDY_REGIONS = ["cn", "global"];
|
|
451
|
+
/**
|
|
452
|
+
* Address one region's status route. The two regions are separate provider
|
|
453
|
+
* stacks; every card request carries the region whose tab the user is on.
|
|
454
|
+
*/
|
|
455
|
+
function withWorkBuddyRegion(path, region) {
|
|
456
|
+
return `${path}?${WORKBUDDY_REGION_PARAM}=${region}`;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Read the region parameter off a status-route URL. Absent means the domestic
|
|
460
|
+
* tab (`cn`); a present-but-unknown value returns undefined so the route can
|
|
461
|
+
* answer 400 instead of guessing.
|
|
462
|
+
*/
|
|
463
|
+
function regionOfStatusUrl(url) {
|
|
464
|
+
const at = url.indexOf("?");
|
|
465
|
+
const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_REGION_PARAM);
|
|
466
|
+
if (value === null || value === "") return "cn";
|
|
467
|
+
return WORKBUDDY_REGIONS.includes(value) ? value : void 0;
|
|
468
|
+
}
|
|
427
469
|
/**
|
|
428
470
|
* Project one card row into its persisted `lastCatalog` shape: the native
|
|
429
471
|
* context window becomes the stored `contextWindow`, and the card-only
|
|
@@ -510,19 +552,22 @@ function toWebAccount(account) {
|
|
|
510
552
|
};
|
|
511
553
|
}
|
|
512
554
|
/**
|
|
513
|
-
* Assemble
|
|
514
|
-
*
|
|
515
|
-
*
|
|
555
|
+
* Assemble one region's card document. `region` is the tab the card is on;
|
|
556
|
+
* the region-scoped store already answers with only that region's accounts,
|
|
557
|
+
* so the document's model slots and account list are that region's by
|
|
558
|
+
* construction. Sign-in state is read-only; credit is a live billing answer
|
|
559
|
+
* whose failure degrades to `creditsError` rather than failing the document.
|
|
516
560
|
*/
|
|
517
|
-
async function workBuddyWebStatus(deps) {
|
|
518
|
-
const
|
|
519
|
-
|
|
561
|
+
async function workBuddyWebStatus(deps, region) {
|
|
562
|
+
const store = deps.store(region);
|
|
563
|
+
const accounts = await store.accounts();
|
|
564
|
+
if ((await store.status()).state !== "signed-out" && accounts.length === 0) return {
|
|
520
565
|
status: "signed-out",
|
|
521
566
|
accounts: []
|
|
522
567
|
};
|
|
523
568
|
let credential;
|
|
524
569
|
try {
|
|
525
|
-
credential = await
|
|
570
|
+
credential = await store.resolve();
|
|
526
571
|
} catch (error) {
|
|
527
572
|
return {
|
|
528
573
|
status: "signed-out",
|
|
@@ -535,12 +580,13 @@ async function workBuddyWebStatus(deps) {
|
|
|
535
580
|
accountName: credential.nickname ?? credential.uin ?? credential.uid,
|
|
536
581
|
...credential.uin === void 0 ? {} : { uin: credential.uin },
|
|
537
582
|
...credential.domain === "" ? {} : { domain: credential.domain },
|
|
583
|
+
region,
|
|
538
584
|
source: credential.source,
|
|
539
585
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
540
586
|
accounts: accounts.map(toWebAccount),
|
|
541
|
-
models: deps.displayModels().map((model) => toWebModel(model, deps.contextBudgets())),
|
|
542
|
-
enabledModelIds: [...deps.enabledModelIds()],
|
|
543
|
-
imageModelIds: [...deps.imageModelIds()]
|
|
587
|
+
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
588
|
+
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
589
|
+
imageModelIds: [...deps.imageModelIds(region)]
|
|
544
590
|
};
|
|
545
591
|
const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
|
|
546
592
|
return {
|
|
@@ -551,6 +597,18 @@ async function workBuddyWebStatus(deps) {
|
|
|
551
597
|
};
|
|
552
598
|
}
|
|
553
599
|
/**
|
|
600
|
+
* The region a request addresses, or a 400 answer. Absent parameter means the
|
|
601
|
+
* domestic tab; an unknown value is refused rather than guessed.
|
|
602
|
+
*/
|
|
603
|
+
function requestRegion(req, res) {
|
|
604
|
+
const region = regionOfStatusUrl(req.url ?? "/");
|
|
605
|
+
if (region === void 0) {
|
|
606
|
+
json(res, 400, { error: "unknown region" });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
return region;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
554
612
|
* Mount the read-only routes on a context where `webServer` is available.
|
|
555
613
|
* The caller uses `ctx.inject(['webServer'], ...)`, so Desktop startup order
|
|
556
614
|
* cannot make this registration disappear.
|
|
@@ -569,8 +627,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
569
627
|
json(res, 403, { error: "origin-not-trusted" });
|
|
570
628
|
return;
|
|
571
629
|
}
|
|
630
|
+
const region = requestRegion(req, res);
|
|
631
|
+
if (region === void 0) return;
|
|
572
632
|
try {
|
|
573
|
-
json(res, 200, await workBuddyWebStatus(deps));
|
|
633
|
+
json(res, 200, await workBuddyWebStatus(deps, region));
|
|
574
634
|
} catch (error) {
|
|
575
635
|
json(res, 500, { error: safeMessage(error) });
|
|
576
636
|
}
|
|
@@ -582,8 +642,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
582
642
|
handler: async (req, res) => {
|
|
583
643
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
584
644
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
645
|
+
const region = requestRegion(req, res);
|
|
646
|
+
if (region === void 0) return;
|
|
585
647
|
try {
|
|
586
|
-
json(res, 200, { accounts: (await deps.store.accounts()).map(toWebAccount) });
|
|
648
|
+
json(res, 200, { accounts: (await deps.store(region).accounts()).map(toWebAccount) });
|
|
587
649
|
} catch (error) {
|
|
588
650
|
json(res, 500, { error: safeMessage(error) });
|
|
589
651
|
}
|
|
@@ -595,8 +657,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
595
657
|
handler: async (req, res) => {
|
|
596
658
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
597
659
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
660
|
+
const region = requestRegion(req, res);
|
|
661
|
+
if (region === void 0) return;
|
|
598
662
|
try {
|
|
599
|
-
const credential = await deps.store.resolve();
|
|
663
|
+
const credential = await deps.store(region).resolve();
|
|
600
664
|
const current = await deps.client.fetchCheckinStatus(credential);
|
|
601
665
|
if (!current.active) return json(res, 409, { error: "check-in activity is not active" });
|
|
602
666
|
if (current.todayCheckedIn) return json(res, 200, {
|
|
@@ -620,8 +684,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
620
684
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
621
685
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
622
686
|
if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
|
|
687
|
+
const region = requestRegion(req, res);
|
|
688
|
+
if (region === void 0) return;
|
|
623
689
|
try {
|
|
624
|
-
json(res, 200, { models: (await deps.discoverModels()).map((model) => toWebModel(model, deps.contextBudgets())) });
|
|
690
|
+
json(res, 200, { models: (await deps.discoverModels(region)).map((model) => toWebModel(model, deps.contextBudgets(region))) });
|
|
625
691
|
} catch (error) {
|
|
626
692
|
json(res, 500, { error: safeMessage(error) });
|
|
627
693
|
}
|
|
@@ -649,55 +715,123 @@ const modelConfig = z.object({
|
|
|
649
715
|
contextWindow: z.number().step(1).min(1),
|
|
650
716
|
maxTokens: z.number().step(1).min(1)
|
|
651
717
|
});
|
|
718
|
+
const regionStateConfig = z.object({
|
|
719
|
+
lastCatalog: z.array(modelConfig).default([]),
|
|
720
|
+
enabledModelIds: z.array(z.string()).default([]),
|
|
721
|
+
imageModelIds: z.array(z.string()).default([]),
|
|
722
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({})
|
|
723
|
+
});
|
|
724
|
+
const accountSelectionConfig = z.object({
|
|
725
|
+
cn: z.string().description("Selected domestic (CN) account id (never a token)"),
|
|
726
|
+
global: z.string().description("Selected international account id (never a token)")
|
|
727
|
+
});
|
|
652
728
|
const Config = z.object({
|
|
653
729
|
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)"),
|
|
654
|
-
accountId: z.string().description("
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
730
|
+
accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
|
|
731
|
+
accounts: accountSelectionConfig.description("Per-region account selections, keyed cn | global"),
|
|
732
|
+
regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | global"),
|
|
733
|
+
lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
|
|
734
|
+
enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
|
|
735
|
+
imageModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN image opt-in"),
|
|
736
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets")
|
|
659
737
|
});
|
|
660
738
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
739
|
+
* One region's saved model state. A config written before the region split has
|
|
740
|
+
* only the flat fields: those were always captured from the CN endpoint (the
|
|
741
|
+
* plugin had no international support), so they are read as the CN state and
|
|
742
|
+
* only when no explicit CN slot exists. The global region never inherits them —
|
|
743
|
+
* that inheritance is exactly the bug where a stale CN directory was
|
|
744
|
+
* intersected with the international catalog and silently dropped the user's
|
|
745
|
+
* picks.
|
|
746
|
+
*/
|
|
747
|
+
function regionStateOf(config, region) {
|
|
748
|
+
const stored = config.regions?.[region];
|
|
749
|
+
if (stored !== void 0) return stored;
|
|
750
|
+
if (region !== "cn") return {};
|
|
751
|
+
return {
|
|
752
|
+
...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
|
|
753
|
+
...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
|
|
754
|
+
...config.imageModelIds === void 0 ? {} : { imageModelIds: config.imageModelIds },
|
|
755
|
+
...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/** Every region, in card tab order. */
|
|
759
|
+
const REGION_KEYS = ["cn", "global"];
|
|
760
|
+
/**
|
|
761
|
+
* Start both regions' loopback endpoints, register the `workbuddy` (CN) and
|
|
762
|
+
* `workbuddy-global` (international) providers, and refresh each region's
|
|
763
|
+
* model catalog from the upstream once that region's credentials allow it.
|
|
764
|
+
* The static fallback catalogs serve from the first moment, so an offline
|
|
765
|
+
* upstream never leaves a provider empty.
|
|
665
766
|
*/
|
|
666
767
|
function apply(ctx, config) {
|
|
667
768
|
const client = new WorkBuddyUpstreamClient();
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
769
|
+
const stacks = {};
|
|
770
|
+
for (const region of REGION_KEYS) {
|
|
771
|
+
const store = new WorkBuddyCredentialStore({
|
|
772
|
+
region,
|
|
773
|
+
...config.authFile === void 0 ? {} : { desktopPath: config.authFile },
|
|
774
|
+
refresh: (credential) => client.refreshToken(credential)
|
|
775
|
+
});
|
|
776
|
+
const catalog = new WorkBuddyCatalog(region);
|
|
777
|
+
stacks[region] = {
|
|
778
|
+
store,
|
|
779
|
+
catalog,
|
|
780
|
+
shim: createWorkBuddyShim({
|
|
781
|
+
store,
|
|
782
|
+
client,
|
|
783
|
+
catalog,
|
|
784
|
+
logger: ctx.logger
|
|
785
|
+
})
|
|
786
|
+
};
|
|
787
|
+
}
|
|
682
788
|
const withImageSelection = (models, images) => models.map((model) => ({
|
|
683
789
|
...model,
|
|
684
790
|
...images.has(model.id) ? { multimodal: true } : { multimodal: false }
|
|
685
791
|
}));
|
|
686
|
-
const configuredModels = (value
|
|
687
|
-
|
|
792
|
+
const configuredModels = (value, region) => {
|
|
793
|
+
const state = regionStateOf(value, region);
|
|
794
|
+
return withImageSelection(deriveCatalog(state.lastCatalog?.length ? state.lastCatalog : fallbackModelsFor(region), new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? []));
|
|
795
|
+
};
|
|
796
|
+
const displayModels = (value, region) => {
|
|
797
|
+
const state = regionStateOf(value, region);
|
|
798
|
+
return state.lastCatalog?.length ? state.lastCatalog : fallbackModelsFor(region);
|
|
799
|
+
};
|
|
688
800
|
let current = () => config;
|
|
689
801
|
let invalidateCatalog = () => {};
|
|
690
|
-
|
|
691
|
-
|
|
802
|
+
/**
|
|
803
|
+
* Legacy migration for the pre-split single `accountId`: its region is
|
|
804
|
+
* resolved once from the local account scan and the selection is then
|
|
805
|
+
* attributed to that region ONLY — the other region keeps its documented
|
|
806
|
+
* default (follow the app's current sign-in) instead of silently inheriting
|
|
807
|
+
* a selection that belongs to the other side of the split.
|
|
808
|
+
*/
|
|
809
|
+
let legacyAccountRegion;
|
|
810
|
+
const effectiveAccountFor = (region, value) => {
|
|
811
|
+
const explicit = value.accounts?.[region];
|
|
812
|
+
if (explicit !== void 0) return explicit;
|
|
813
|
+
return legacyAccountRegion === region ? value.accountId : void 0;
|
|
814
|
+
};
|
|
815
|
+
const discoverModels = async (region, signal) => {
|
|
816
|
+
const credential = await stacks[region].store.resolve();
|
|
692
817
|
return client.fetchModels(credential, signal);
|
|
693
818
|
};
|
|
819
|
+
/** Push the current config into every region's store selection and catalog. */
|
|
820
|
+
const applySelection = (value) => {
|
|
821
|
+
for (const region of REGION_KEYS) {
|
|
822
|
+
stacks[region].store.setDesktopPath(value.authFile);
|
|
823
|
+
stacks[region].store.selectAccount(effectiveAccountFor(region, value));
|
|
824
|
+
stacks[region].catalog.set(configuredModels(value, region));
|
|
825
|
+
}
|
|
826
|
+
invalidateCatalog();
|
|
827
|
+
};
|
|
694
828
|
ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
|
|
695
|
-
store,
|
|
829
|
+
store: (region) => stacks[region].store,
|
|
696
830
|
client,
|
|
697
|
-
displayModels: () => displayModels(current()),
|
|
698
|
-
enabledModelIds: () => current().enabledModelIds ?? [],
|
|
699
|
-
imageModelIds: () => current().imageModelIds ?? [],
|
|
700
|
-
contextBudgets: () => current().contextBudgets ?? {},
|
|
831
|
+
displayModels: (region) => displayModels(current(), region),
|
|
832
|
+
enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
|
|
833
|
+
imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
|
|
834
|
+
contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
|
|
701
835
|
discoverModels
|
|
702
836
|
}));
|
|
703
837
|
ctx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
|
|
@@ -705,62 +839,85 @@ function apply(ctx, config) {
|
|
|
705
839
|
current = source;
|
|
706
840
|
},
|
|
707
841
|
onChange() {
|
|
708
|
-
|
|
709
|
-
store.setDesktopPath(next.authFile);
|
|
710
|
-
store.selectAccount(next.accountId);
|
|
711
|
-
catalog.set(configuredModels(next));
|
|
712
|
-
invalidateCatalog();
|
|
842
|
+
applySelection(current());
|
|
713
843
|
}
|
|
714
844
|
});
|
|
845
|
+
applySelection(config);
|
|
846
|
+
(async () => {
|
|
847
|
+
const id = current().accountId;
|
|
848
|
+
if (id === void 0) return;
|
|
849
|
+
try {
|
|
850
|
+
for (const region of REGION_KEYS) if ((await stacks[region].store.accounts()).some((account) => account.id === id)) {
|
|
851
|
+
legacyAccountRegion = region;
|
|
852
|
+
applySelection(current());
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
} catch {}
|
|
856
|
+
})();
|
|
715
857
|
let stopped = false;
|
|
716
858
|
ctx.effect(() => () => {
|
|
717
859
|
stopped = true;
|
|
718
|
-
shim.close();
|
|
860
|
+
for (const region of REGION_KEYS) stacks[region].shim.close();
|
|
719
861
|
clearHostHeartbeat();
|
|
720
862
|
});
|
|
721
|
-
shim.ready.then(async () => {
|
|
863
|
+
Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
|
|
722
864
|
if (stopped) return;
|
|
723
|
-
|
|
865
|
+
const adapters = {};
|
|
724
866
|
try {
|
|
725
|
-
const
|
|
726
|
-
shim,
|
|
727
|
-
store,
|
|
728
|
-
catalog,
|
|
867
|
+
for (const region of REGION_KEYS) adapters[region] = createWorkBuddyAdapter({
|
|
868
|
+
shim: stacks[region].shim,
|
|
869
|
+
store: stacks[region].store,
|
|
870
|
+
catalog: stacks[region].catalog,
|
|
871
|
+
provider: WORKBUDDY_PROVIDERS[region],
|
|
872
|
+
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
|
|
729
873
|
resolveAttachments: () => ctx.get("attachments")
|
|
730
874
|
});
|
|
731
|
-
invalidate = workbuddy.invalidate;
|
|
732
875
|
invalidateCatalog = () => {
|
|
733
|
-
|
|
876
|
+
for (const region of REGION_KEYS) adapters[region].invalidate();
|
|
734
877
|
};
|
|
735
|
-
let
|
|
878
|
+
let releaseAdapterCn;
|
|
879
|
+
let releaseAdapterGlobal;
|
|
736
880
|
let releaseDirectory;
|
|
737
881
|
try {
|
|
738
|
-
|
|
882
|
+
releaseAdapterCn = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], adapters.cn.adapter);
|
|
883
|
+
releaseAdapterGlobal = ctx.llm.registerAdapter([WORKBUDDY_GLOBAL_PROVIDER], adapters.global.adapter);
|
|
739
884
|
releaseDirectory = ctx.llm.registerConfigurableProviders([{
|
|
740
885
|
provider: WORKBUDDY_PROVIDER,
|
|
741
|
-
displayName:
|
|
886
|
+
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES.cn,
|
|
887
|
+
settingsNs: WORKBUDDY_SETTINGS_NS,
|
|
888
|
+
settingsPath: [],
|
|
889
|
+
declared: false
|
|
890
|
+
}, {
|
|
891
|
+
provider: WORKBUDDY_GLOBAL_PROVIDER,
|
|
892
|
+
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES.global,
|
|
742
893
|
settingsNs: WORKBUDDY_SETTINGS_NS,
|
|
743
894
|
settingsPath: [],
|
|
744
895
|
declared: false
|
|
745
896
|
}]);
|
|
746
897
|
} finally {
|
|
747
|
-
if (
|
|
748
|
-
|
|
898
|
+
if (releaseAdapterCn === void 0 || releaseAdapterGlobal === void 0 || releaseDirectory === void 0) {
|
|
899
|
+
releaseAdapterCn?.();
|
|
900
|
+
releaseAdapterGlobal?.();
|
|
749
901
|
releaseDirectory?.();
|
|
750
902
|
}
|
|
751
903
|
}
|
|
752
904
|
try {
|
|
753
905
|
ctx.effect(() => () => {
|
|
754
|
-
|
|
906
|
+
releaseAdapterCn?.();
|
|
907
|
+
releaseAdapterGlobal?.();
|
|
755
908
|
releaseDirectory?.();
|
|
756
909
|
});
|
|
757
910
|
} catch {
|
|
758
|
-
|
|
911
|
+
releaseAdapterCn?.();
|
|
912
|
+
releaseAdapterGlobal?.();
|
|
759
913
|
releaseDirectory?.();
|
|
760
914
|
}
|
|
761
915
|
ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
|
|
762
|
-
|
|
763
|
-
|
|
916
|
+
const region = regionOfProvider(request.provider ?? "");
|
|
917
|
+
if (region === void 0) return [];
|
|
918
|
+
const discovered = await discoverModels(region, signal);
|
|
919
|
+
const state = regionStateOf(current(), region);
|
|
920
|
+
return withImageSelection(deriveCatalog(discovered, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])).map((model) => ({
|
|
764
921
|
id: model.id,
|
|
765
922
|
name: workBuddyDisplayName(model),
|
|
766
923
|
contextWindow: model.contextWindow,
|
|
@@ -774,21 +931,22 @@ function apply(ctx, config) {
|
|
|
774
931
|
return;
|
|
775
932
|
}
|
|
776
933
|
if (stopped) return;
|
|
777
|
-
(async () => {
|
|
934
|
+
for (const region of REGION_KEYS) (async () => {
|
|
778
935
|
try {
|
|
779
|
-
const credential = await store.resolve();
|
|
936
|
+
const credential = await stacks[region].store.resolve();
|
|
780
937
|
if (stopped) return;
|
|
781
938
|
const models = await client.fetchModels(credential);
|
|
782
939
|
if (stopped) return;
|
|
783
|
-
|
|
784
|
-
|
|
940
|
+
const state = regionStateOf(current(), region);
|
|
941
|
+
stacks[region].catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
|
|
942
|
+
adapters[region].invalidate();
|
|
785
943
|
} catch (error) {
|
|
786
|
-
ctx.logger.warn(
|
|
944
|
+
ctx.logger.warn(`dsh-connect-workbuddy: dynamic ${region} model catalog unavailable; serving the static fallback list`, error);
|
|
787
945
|
}
|
|
788
946
|
})();
|
|
789
947
|
}).catch((error) => {
|
|
790
|
-
ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start;
|
|
948
|
+
ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start; providers not registered", error);
|
|
791
949
|
});
|
|
792
950
|
}
|
|
793
951
|
//#endregion
|
|
794
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
952
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2.0.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|