dsh-connect-workbuddy 1.4.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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as workbuddyOwnAuthPath, C as WorkBuddyCredentialStore, D as defaultDesktopAuthPath, E as defaultDesktopAuthDirs, O as parseWorkBuddyAuth, S as WORKBUDDY_AUTH_FILE_ENV, T as defaultDesktopAuthCandidates, _ as FALLBACK_WORKBUDDY_MODELS_GLOBAL, a as readHostHeartbeat, b as fallbackModelsFor, c as WORKBUDDY_CONNECT_VERSION, d as parseCreditMultiplier, f as parseReasoning, g as FALLBACK_WORKBUDDY_MODELS, h as regionOf, i as processStartTimeMs, k as workbuddyAccountId, l as WorkBuddyUpstreamClient, m as prepareChatBody, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as parseUpstreamModel, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as classifyUpstreamError, v as WorkBuddyCatalog, w as authFileName, x as WORKBUDDY_AUTH_FILENAME, y as deriveCatalog } from "./host-heartbeat-DQQwiUtm.js";
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 `workbuddy` pi-ai provider: one loopback-backed adapter registered
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: WORKBUDDY_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: WORKBUDDY_PROVIDER,
137
- name: "WorkBuddy",
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: WORKBUDDY_PROVIDER,
155
- displayName: "WorkBuddy",
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([[WORKBUDDY_PROVIDER, profile]]);
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([[WORKBUDDY_PROVIDER, profile]]);
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 the card's usage document. Sign-in state is read-only; credit is a
514
- * live billing answer whose failure degrades to `creditsError` rather than
515
- * failing the whole document.
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 accounts = await deps.store.accounts();
519
- if ((await deps.store.status()).state !== "signed-out" && accounts.length === 0) return {
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 deps.store.resolve();
570
+ credential = await store.resolve();
526
571
  } catch (error) {
527
572
  return {
528
573
  status: "signed-out",
@@ -530,10 +575,8 @@ async function workBuddyWebStatus(deps) {
530
575
  message: safeMessage(error)
531
576
  };
532
577
  }
533
- const selected = accounts.find((account) => account.selected);
534
- const region = regionOf(credential.domain);
535
578
  const account = {
536
- accountId: selected?.id ?? "",
579
+ accountId: accounts.find((account) => account.selected)?.id ?? "",
537
580
  accountName: credential.nickname ?? credential.uin ?? credential.uid,
538
581
  ...credential.uin === void 0 ? {} : { uin: credential.uin },
539
582
  ...credential.domain === "" ? {} : { domain: credential.domain },
@@ -554,6 +597,18 @@ async function workBuddyWebStatus(deps) {
554
597
  };
555
598
  }
556
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
+ /**
557
612
  * Mount the read-only routes on a context where `webServer` is available.
558
613
  * The caller uses `ctx.inject(['webServer'], ...)`, so Desktop startup order
559
614
  * cannot make this registration disappear.
@@ -572,8 +627,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
572
627
  json(res, 403, { error: "origin-not-trusted" });
573
628
  return;
574
629
  }
630
+ const region = requestRegion(req, res);
631
+ if (region === void 0) return;
575
632
  try {
576
- json(res, 200, await workBuddyWebStatus(deps));
633
+ json(res, 200, await workBuddyWebStatus(deps, region));
577
634
  } catch (error) {
578
635
  json(res, 500, { error: safeMessage(error) });
579
636
  }
@@ -585,8 +642,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
585
642
  handler: async (req, res) => {
586
643
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
587
644
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
645
+ const region = requestRegion(req, res);
646
+ if (region === void 0) return;
588
647
  try {
589
- json(res, 200, { accounts: (await deps.store.accounts()).map(toWebAccount) });
648
+ json(res, 200, { accounts: (await deps.store(region).accounts()).map(toWebAccount) });
590
649
  } catch (error) {
591
650
  json(res, 500, { error: safeMessage(error) });
592
651
  }
@@ -598,8 +657,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
598
657
  handler: async (req, res) => {
599
658
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
600
659
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
660
+ const region = requestRegion(req, res);
661
+ if (region === void 0) return;
601
662
  try {
602
- const credential = await deps.store.resolve();
663
+ const credential = await deps.store(region).resolve();
603
664
  const current = await deps.client.fetchCheckinStatus(credential);
604
665
  if (!current.active) return json(res, 409, { error: "check-in activity is not active" });
605
666
  if (current.todayCheckedIn) return json(res, 200, {
@@ -623,9 +684,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
623
684
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
624
685
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
625
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;
626
689
  try {
627
- const region = regionOf((await deps.store.resolve()).domain);
628
- json(res, 200, { models: (await deps.discoverModels()).map((model) => toWebModel(model, deps.contextBudgets(region))) });
690
+ json(res, 200, { models: (await deps.discoverModels(region)).map((model) => toWebModel(model, deps.contextBudgets(region))) });
629
691
  } catch (error) {
630
692
  json(res, 500, { error: safeMessage(error) });
631
693
  }
@@ -659,9 +721,14 @@ const regionStateConfig = z.object({
659
721
  imageModelIds: z.array(z.string()).default([]),
660
722
  contextBudgets: z.dict(z.number().step(1).min(1)).default({})
661
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
+ });
662
728
  const Config = z.object({
663
729
  authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)"),
664
- accountId: z.string().description("Selected local WorkBuddy account id (never a token)"),
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"),
665
732
  regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | global"),
666
733
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
667
734
  enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
@@ -688,26 +755,36 @@ function regionStateOf(config, region) {
688
755
  ...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
689
756
  };
690
757
  }
758
+ /** Every region, in card tab order. */
759
+ const REGION_KEYS = ["cn", "global"];
691
760
  /**
692
- * Start the loopback endpoint, register the `workbuddy` provider, and
693
- * refresh the model catalog from the upstream once credentials allow it.
694
- * The static fallback catalog serves from the first moment, so an offline
695
- * upstream never leaves the provider empty.
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.
696
766
  */
697
767
  function apply(ctx, config) {
698
768
  const client = new WorkBuddyUpstreamClient();
699
- const store = new WorkBuddyCredentialStore({
700
- ...config.authFile === void 0 ? {} : { desktopPath: config.authFile },
701
- refresh: (credential) => client.refreshToken(credential)
702
- });
703
- if (config.accountId !== void 0) store.selectAccount(config.accountId);
704
- const catalog = new WorkBuddyCatalog();
705
- const shim = createWorkBuddyShim({
706
- store,
707
- client,
708
- catalog,
709
- logger: ctx.logger
710
- });
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
+ }
711
788
  const withImageSelection = (models, images) => models.map((model) => ({
712
789
  ...model,
713
790
  ...images.has(model.id) ? { multimodal: true } : { multimodal: false }
@@ -723,25 +800,33 @@ function apply(ctx, config) {
723
800
  let current = () => config;
724
801
  let invalidateCatalog = () => {};
725
802
  /**
726
- * Region of the selected account, tracked so a settings change can recompute
727
- * the runtime catalog without re-resolving the credential synchronously.
728
- * Every path that reads the credential (startup seed, discovery, card route)
729
- * refreshes it, so it converges on the real region.
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.
730
808
  */
731
- let currentRegion = "cn";
732
- const regionOfCredential = async () => {
733
- try {
734
- currentRegion = regionOf((await store.resolve()).domain);
735
- } catch {}
736
- return currentRegion;
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;
737
814
  };
738
- const discoverModels = async (signal) => {
739
- const credential = await store.resolve();
740
- currentRegion = regionOf(credential.domain);
815
+ const discoverModels = async (region, signal) => {
816
+ const credential = await stacks[region].store.resolve();
741
817
  return client.fetchModels(credential, signal);
742
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
+ };
743
828
  ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
744
- store,
829
+ store: (region) => stacks[region].store,
745
830
  client,
746
831
  displayModels: (region) => displayModels(current(), region),
747
832
  enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
@@ -754,67 +839,83 @@ function apply(ctx, config) {
754
839
  current = source;
755
840
  },
756
841
  onChange() {
757
- const next = current();
758
- store.setDesktopPath(next.authFile);
759
- store.selectAccount(next.accountId);
760
- catalog.set(configuredModels(next, currentRegion));
761
- invalidateCatalog();
762
- regionOfCredential().then((region) => {
763
- catalog.set(configuredModels(current(), region));
764
- invalidateCatalog();
765
- });
842
+ applySelection(current());
766
843
  }
767
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
+ })();
768
857
  let stopped = false;
769
858
  ctx.effect(() => () => {
770
859
  stopped = true;
771
- shim.close();
860
+ for (const region of REGION_KEYS) stacks[region].shim.close();
772
861
  clearHostHeartbeat();
773
862
  });
774
- shim.ready.then(async () => {
863
+ Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
775
864
  if (stopped) return;
776
- let invalidate;
865
+ const adapters = {};
777
866
  try {
778
- const workbuddy = createWorkBuddyAdapter({
779
- shim,
780
- store,
781
- 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],
782
873
  resolveAttachments: () => ctx.get("attachments")
783
874
  });
784
- invalidate = workbuddy.invalidate;
785
875
  invalidateCatalog = () => {
786
- workbuddy.invalidate();
876
+ for (const region of REGION_KEYS) adapters[region].invalidate();
787
877
  };
788
- let releaseAdapter;
878
+ let releaseAdapterCn;
879
+ let releaseAdapterGlobal;
789
880
  let releaseDirectory;
790
881
  try {
791
- releaseAdapter = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], workbuddy.adapter);
882
+ releaseAdapterCn = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], adapters.cn.adapter);
883
+ releaseAdapterGlobal = ctx.llm.registerAdapter([WORKBUDDY_GLOBAL_PROVIDER], adapters.global.adapter);
792
884
  releaseDirectory = ctx.llm.registerConfigurableProviders([{
793
885
  provider: WORKBUDDY_PROVIDER,
794
- displayName: "WorkBuddy",
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,
795
893
  settingsNs: WORKBUDDY_SETTINGS_NS,
796
894
  settingsPath: [],
797
895
  declared: false
798
896
  }]);
799
897
  } finally {
800
- if (releaseAdapter === void 0 || releaseDirectory === void 0) {
801
- releaseAdapter?.();
898
+ if (releaseAdapterCn === void 0 || releaseAdapterGlobal === void 0 || releaseDirectory === void 0) {
899
+ releaseAdapterCn?.();
900
+ releaseAdapterGlobal?.();
802
901
  releaseDirectory?.();
803
902
  }
804
903
  }
805
904
  try {
806
905
  ctx.effect(() => () => {
807
- releaseAdapter?.();
906
+ releaseAdapterCn?.();
907
+ releaseAdapterGlobal?.();
808
908
  releaseDirectory?.();
809
909
  });
810
910
  } catch {
811
- releaseAdapter?.();
911
+ releaseAdapterCn?.();
912
+ releaseAdapterGlobal?.();
812
913
  releaseDirectory?.();
813
914
  }
814
915
  ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
815
- if (request.provider !== "workbuddy") return [];
816
- const discovered = await discoverModels(signal);
817
- const region = currentRegion;
916
+ const region = regionOfProvider(request.provider ?? "");
917
+ if (region === void 0) return [];
918
+ const discovered = await discoverModels(region, signal);
818
919
  const state = regionStateOf(current(), region);
819
920
  return withImageSelection(deriveCatalog(discovered, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])).map((model) => ({
820
921
  id: model.id,
@@ -830,23 +931,22 @@ function apply(ctx, config) {
830
931
  return;
831
932
  }
832
933
  if (stopped) return;
833
- (async () => {
934
+ for (const region of REGION_KEYS) (async () => {
834
935
  try {
835
- const credential = await store.resolve();
936
+ const credential = await stacks[region].store.resolve();
836
937
  if (stopped) return;
837
- currentRegion = regionOf(credential.domain);
838
938
  const models = await client.fetchModels(credential);
839
939
  if (stopped) return;
840
- const state = regionStateOf(current(), currentRegion);
841
- catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
842
- invalidate?.();
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();
843
943
  } catch (error) {
844
- ctx.logger.warn("dsh-connect-workbuddy: dynamic model catalog unavailable; serving the static fallback list", error);
944
+ ctx.logger.warn(`dsh-connect-workbuddy: dynamic ${region} model catalog unavailable; serving the static fallback list`, error);
845
945
  }
846
946
  })();
847
947
  }).catch((error) => {
848
- ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start; provider not registered", error);
948
+ ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start; providers not registered", error);
849
949
  });
850
950
  }
851
951
  //#endregion
852
- 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_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, fallbackModelsFor, inject, isHeartbeatProcessAlive, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, regionStateOf, 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": "1.4.0",
5
+ "version": "2.0.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",