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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as defaultDesktopAuthCandidates, D as workbuddyAccountId, E as parseWorkBuddyAuth, O as workbuddyOwnAuthPath, S as authFileName, T as defaultDesktopAuthPath, _ as WorkBuddyCatalog, a as readHostHeartbeat, b as WORKBUDDY_AUTH_FILE_ENV, c as WORKBUDDY_CONNECT_VERSION, d as parseCreditMultiplier, f as parseReasoning, g as FALLBACK_WORKBUDDY_MODELS, h as regionOf, i as processStartTimeMs, 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 deriveCatalog, w as defaultDesktopAuthDirs, x as WorkBuddyCredentialStore, y as WORKBUDDY_AUTH_FILENAME } from "./host-heartbeat-UlZjd09_.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",
@@ -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("Selected local WorkBuddy account id (never a token)"),
655
- lastCatalog: z.array(modelConfig).description("Last refreshed WorkBuddy model directory shown by the plugin card"),
656
- enabledModelIds: z.array(z.string()).default([]).description("WorkBuddy model ids the user enabled"),
657
- imageModelIds: z.array(z.string()).default([]).description("WorkBuddy model ids the user opted into image input"),
658
- contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Local DSH context budget per WorkBuddy model")
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
- * Start the loopback endpoint, register the `workbuddy` provider, and
662
- * refresh the model catalog from the upstream once credentials allow it.
663
- * The static fallback catalog serves from the first moment, so an offline
664
- * upstream never leaves the provider empty.
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 store = new WorkBuddyCredentialStore({
669
- ...config.authFile === void 0 ? {} : { desktopPath: config.authFile },
670
- refresh: (credential) => client.refreshToken(credential)
671
- });
672
- if (config.accountId !== void 0) store.selectAccount(config.accountId);
673
- const catalog = new WorkBuddyCatalog();
674
- const shim = createWorkBuddyShim({
675
- store,
676
- client,
677
- catalog,
678
- logger: ctx.logger
679
- });
680
- const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
681
- const imageSet = (value) => new Set(value.imageModelIds ?? []);
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) => withImageSelection(deriveCatalog(value.lastCatalog?.length ? value.lastCatalog : FALLBACK_WORKBUDDY_MODELS, enabledSet(value), value.contextBudgets ?? {}), imageSet(value));
687
- const displayModels = (value) => value.lastCatalog?.length ? value.lastCatalog : FALLBACK_WORKBUDDY_MODELS;
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
- const discoverModels = async (signal) => {
691
- const credential = await store.resolve();
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
- const next = current();
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
- let invalidate;
865
+ const adapters = {};
724
866
  try {
725
- const workbuddy = createWorkBuddyAdapter({
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
- workbuddy.invalidate();
876
+ for (const region of REGION_KEYS) adapters[region].invalidate();
734
877
  };
735
- let releaseAdapter;
878
+ let releaseAdapterCn;
879
+ let releaseAdapterGlobal;
736
880
  let releaseDirectory;
737
881
  try {
738
- 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);
739
884
  releaseDirectory = ctx.llm.registerConfigurableProviders([{
740
885
  provider: WORKBUDDY_PROVIDER,
741
- 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,
742
893
  settingsNs: WORKBUDDY_SETTINGS_NS,
743
894
  settingsPath: [],
744
895
  declared: false
745
896
  }]);
746
897
  } finally {
747
- if (releaseAdapter === void 0 || releaseDirectory === void 0) {
748
- releaseAdapter?.();
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
- releaseAdapter?.();
906
+ releaseAdapterCn?.();
907
+ releaseAdapterGlobal?.();
755
908
  releaseDirectory?.();
756
909
  });
757
910
  } catch {
758
- releaseAdapter?.();
911
+ releaseAdapterCn?.();
912
+ releaseAdapterGlobal?.();
759
913
  releaseDirectory?.();
760
914
  }
761
915
  ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
762
- if (request.provider !== "workbuddy") return [];
763
- return withImageSelection(deriveCatalog(await discoverModels(signal), enabledSet(current()), current().contextBudgets ?? {}), imageSet(current())).map((model) => ({
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
- catalog.set(withImageSelection(deriveCatalog(models, enabledSet(current()), current().contextBudgets ?? {}), imageSet(current())));
784
- 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();
785
943
  } catch (error) {
786
- 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);
787
945
  }
788
946
  })();
789
947
  }).catch((error) => {
790
- 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);
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": "1.3.0",
5
+ "version": "2.0.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",