dsh-connect-workbuddy 2.0.5 → 2.0.10
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 +172 -1
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/lib/bin.js +15 -2
- package/lib/client.js +416 -66
- package/lib/{host-heartbeat-BOrNj3pY.js → host-heartbeat-DPbL9oi2.js} +578 -28
- package/lib/index.d.ts +481 -7
- package/lib/index.js +298 -29
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as isCredentialRejectedError, B as fetchAtRestKeyPayload, C as parseWorkBuddyAuth, D as WorkBuddyCredentialRejectedError, E as CREDENTIAL_REJECTED_CODE, F as regionOf, G as workbuddyAppExecutableCandidates, H as isEncryptedFieldWrapper, I as WORKBUDDY_APP_EXECUTABLE_ENV, L as clearAtRestKeyCache, M as parseReasoning, N as parseUpstreamModel, O as WorkBuddyUpstreamClient, P as prepareChatBody, R as deriveAtRestKey, S as legacyWorkbuddyOwnAuthPath, T as workbuddyOwnAuthPath, U as openEncryptedField, V as findWorkbuddyAppExecutable, W as readAtRestKey, _ 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 parseCreditMultiplier, k as classifyUpstreamError, 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 workbuddyAccountId, x as hasEncryptedCredentialFields, y as defaultDesktopAuthDirs, z as deriveAtRestKeyId } from "./host-heartbeat-DPbL9oi2.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";
|
|
@@ -7,6 +7,76 @@ import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
|
7
7
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
import { Readable } from "node:stream";
|
|
10
|
+
//#region src/credential-recovery.ts
|
|
11
|
+
/** How long a verdict stays fresh by default. */
|
|
12
|
+
const DEFAULT_PROBE_TTL_MS = 6e4;
|
|
13
|
+
/**
|
|
14
|
+
* Cached "is this account still accepted upstream?" probe.
|
|
15
|
+
*
|
|
16
|
+
* The cache key includes the credential's own `lastRefreshAtMs`, so a fresh
|
|
17
|
+
* sign-in (which rewrites the file's issuance time) invalidates a stale verdict
|
|
18
|
+
* automatically — a rejection recorded before the user signed in again cannot
|
|
19
|
+
* keep labelling the account unusable afterwards, and no explicit invalidation
|
|
20
|
+
* is needed.
|
|
21
|
+
*/
|
|
22
|
+
function createAccountUsabilityProbe(options) {
|
|
23
|
+
const ttlMs = options.ttlMs ?? DEFAULT_PROBE_TTL_MS;
|
|
24
|
+
const now = options.now ?? Date.now;
|
|
25
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
26
|
+
return async (region, account) => {
|
|
27
|
+
let credential;
|
|
28
|
+
try {
|
|
29
|
+
credential = await options.store(region).credentialFor(account.id);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (credential === void 0) return false;
|
|
34
|
+
const key = `${region}\0${account.id}\0${credential.lastRefreshAtMs ?? credential.expiresAtMs}`;
|
|
35
|
+
const cached = verdicts.get(key);
|
|
36
|
+
if (cached !== void 0 && now() - cached.at < ttlMs) return cached.usable;
|
|
37
|
+
let usable = false;
|
|
38
|
+
try {
|
|
39
|
+
await options.client.fetchCheckinStatus(credential);
|
|
40
|
+
usable = true;
|
|
41
|
+
} catch {
|
|
42
|
+
usable = false;
|
|
43
|
+
}
|
|
44
|
+
verdicts.set(key, {
|
|
45
|
+
at: now(),
|
|
46
|
+
usable
|
|
47
|
+
});
|
|
48
|
+
return usable;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decide what to tell the user after the upstream refused the selected
|
|
53
|
+
* credential: switch to a verified-usable account, or sign in again.
|
|
54
|
+
*
|
|
55
|
+
* The two answers are mutually exclusive by construction. `reloginRequired` is
|
|
56
|
+
* set ONLY when there is no other local account to switch to — never as a
|
|
57
|
+
* fallback for "the probe did not verify anything", because that would repeat
|
|
58
|
+
* the misdirection this module exists to remove. When other accounts exist but
|
|
59
|
+
* none was verified, both fields stay empty and the card says so honestly.
|
|
60
|
+
*/
|
|
61
|
+
async function resolveCredentialRecovery(options) {
|
|
62
|
+
let accounts;
|
|
63
|
+
try {
|
|
64
|
+
accounts = await options.store.accounts();
|
|
65
|
+
} catch {
|
|
66
|
+
return { reloginRequired: false };
|
|
67
|
+
}
|
|
68
|
+
const others = accounts.filter((account) => account.id !== options.rejectedAccountId);
|
|
69
|
+
if (others.length === 0) return { reloginRequired: true };
|
|
70
|
+
for (const account of others) if (await options.probe(options.region, account)) return {
|
|
71
|
+
usableAccount: {
|
|
72
|
+
accountId: account.id,
|
|
73
|
+
accountName: account.accountName
|
|
74
|
+
},
|
|
75
|
+
reloginRequired: false
|
|
76
|
+
};
|
|
77
|
+
return { reloginRequired: false };
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
10
80
|
//#region src/adapter.ts
|
|
11
81
|
/**
|
|
12
82
|
* The WorkBuddy pi-ai providers: loopback-backed adapters registered
|
|
@@ -467,6 +537,69 @@ function regionOfStatusUrl(url) {
|
|
|
467
537
|
return WORKBUDDY_REGIONS.includes(value) ? value : void 0;
|
|
468
538
|
}
|
|
469
539
|
/**
|
|
540
|
+
* Narrow a settings value to the `regions` map. Accepts EITHER the whole
|
|
541
|
+
* settings section (the Host's resolved `Config`) OR the `regions` map itself,
|
|
542
|
+
* and unwraps the former. This tolerance is deliberate: passing the whole
|
|
543
|
+
* section where the map was expected was a real shipped bug in the sibling
|
|
544
|
+
* project — the lookup then read `section['cn']` (absent), so the card's
|
|
545
|
+
* checkbox reported `true` forever and clicking it appeared to do nothing even
|
|
546
|
+
* though the write succeeded.
|
|
547
|
+
*/
|
|
548
|
+
function regionsMapOf(value) {
|
|
549
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
550
|
+
const record = value;
|
|
551
|
+
const nested = record["regions"];
|
|
552
|
+
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
|
|
553
|
+
return record;
|
|
554
|
+
}
|
|
555
|
+
/** One region's stored slot as a plain object; any other shape reads as empty. */
|
|
556
|
+
function regionSlotOf(value, region) {
|
|
557
|
+
const slot = regionsMapOf(value)[region];
|
|
558
|
+
return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Whether one region's provider is switched on. Opt-out semantics: only an
|
|
562
|
+
* explicit `false` disables it, so a config written before this switch existed
|
|
563
|
+
* (and the pre-region-split flat fields, which never carry `enabled`) keep both
|
|
564
|
+
* providers running exactly as before. The Host reads the same rule through
|
|
565
|
+
* `regionStateOf`, so card and Host can never disagree about a region's state.
|
|
566
|
+
*
|
|
567
|
+
* `value` may be the whole settings section or the `regions` map (see
|
|
568
|
+
* {@link regionsMapOf}).
|
|
569
|
+
*/
|
|
570
|
+
function regionEnabledOf(value, region) {
|
|
571
|
+
return regionSlotOf(value, region)["enabled"] !== false;
|
|
572
|
+
}
|
|
573
|
+
/** Build the next `regions` settings value for a signed-in tab's save. */
|
|
574
|
+
function nextRegionSlots(regions, region, slot) {
|
|
575
|
+
return {
|
|
576
|
+
...typeof regions === "object" && regions !== null && !Array.isArray(regions) ? regions : {},
|
|
577
|
+
[region]: slot
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Build the next `regions` settings value for a provider on/off toggle. ONLY
|
|
582
|
+
* the target region's `enabled` flag changes: every other field of that slot
|
|
583
|
+
* (its directory, selection, image opt-ins, context budgets) and every other
|
|
584
|
+
* region's slot are carried over verbatim, so switching a provider off never
|
|
585
|
+
* discards the user's model picks and switching it back on restores them.
|
|
586
|
+
*
|
|
587
|
+
* This is deliberately separate from {@link nextRegionSlots}: that helper
|
|
588
|
+
* writes a whole slot from a signed-in tab's draft, while this one must work
|
|
589
|
+
* for a region that is signed OUT — which is precisely the region a user wants
|
|
590
|
+
* to switch off (no international install, no international account).
|
|
591
|
+
*
|
|
592
|
+
* `value` may be the whole settings section or the `regions` map; the RETURN
|
|
593
|
+
* value is always the `regions` map, i.e. exactly what `settingsScope.set(
|
|
594
|
+
* 'regions', ...)` needs.
|
|
595
|
+
*/
|
|
596
|
+
function nextRegionEnabled(value, region, enabled) {
|
|
597
|
+
return nextRegionSlots(regionsMapOf(value), region, {
|
|
598
|
+
...regionSlotOf(value, region),
|
|
599
|
+
enabled
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
470
603
|
* Project one card row into its persisted `lastCatalog` shape: the native
|
|
471
604
|
* context window becomes the stored `contextWindow`, and the card-only
|
|
472
605
|
* presentation fields (`nativeContextWindow`, `multimodal`) are removed BY
|
|
@@ -539,12 +672,18 @@ function toWebModel(model, budgets) {
|
|
|
539
672
|
} }
|
|
540
673
|
};
|
|
541
674
|
}
|
|
542
|
-
/**
|
|
675
|
+
/**
|
|
676
|
+
* Project a store account into the card's token-free account row.
|
|
677
|
+
*
|
|
678
|
+
* `uin` is deliberately NOT forwarded: the card has never rendered it, so
|
|
679
|
+
* sending it was pure exposure of an account identifier for no feature. The
|
|
680
|
+
* name carries whatever the desktop app recorded, and `''` means the card
|
|
681
|
+
* should show its own placeholder rather than an identifier.
|
|
682
|
+
*/
|
|
543
683
|
function toWebAccount(account) {
|
|
544
684
|
return {
|
|
545
685
|
id: account.id,
|
|
546
686
|
accountName: account.accountName,
|
|
547
|
-
...account.uin === void 0 ? {} : { uin: account.uin },
|
|
548
687
|
domain: account.domain,
|
|
549
688
|
source: account.source,
|
|
550
689
|
tokenExpiresAtMs: account.tokenExpiresAtMs,
|
|
@@ -552,6 +691,31 @@ function toWebAccount(account) {
|
|
|
552
691
|
};
|
|
553
692
|
}
|
|
554
693
|
/**
|
|
694
|
+
* The probed-path list for a signed-out region, or nothing when the store
|
|
695
|
+
* cannot produce one.
|
|
696
|
+
*
|
|
697
|
+
* Diagnostics must never turn a page into an error: `diagnose()` re-reads the
|
|
698
|
+
* filesystem, and a store built without it (or one whose probe throws on an
|
|
699
|
+
* exotic filesystem) degrades to the plain "not signed in" hint the card showed
|
|
700
|
+
* before this existed. Failure reasons are `safeMessage`d because a raw
|
|
701
|
+
* filesystem error can embed an absolute path or a fragment of file content.
|
|
702
|
+
*/
|
|
703
|
+
async function searchedPaths(store) {
|
|
704
|
+
if (typeof store.diagnose !== "function") return {};
|
|
705
|
+
try {
|
|
706
|
+
const { failures } = await store.diagnose();
|
|
707
|
+
if (failures.length === 0) return {};
|
|
708
|
+
return { searched: failures.map((failure) => ({
|
|
709
|
+
path: failure.path,
|
|
710
|
+
source: failure.source,
|
|
711
|
+
reason: failure.reason,
|
|
712
|
+
...failure.message === void 0 ? {} : { message: safeMessage(failure.message) }
|
|
713
|
+
})) };
|
|
714
|
+
} catch {
|
|
715
|
+
return {};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
555
719
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
556
720
|
* the region-scoped store already answers with only that region's accounts,
|
|
557
721
|
* so the document's model slots and account list are that region's by
|
|
@@ -561,45 +725,61 @@ function toWebAccount(account) {
|
|
|
561
725
|
async function workBuddyWebStatus(deps, region) {
|
|
562
726
|
const store = deps.store(region);
|
|
563
727
|
const accounts = await store.accounts();
|
|
728
|
+
deps.regionUsable?.(region, accounts.length > 0);
|
|
729
|
+
const enabled = deps.regionEnabled(region);
|
|
564
730
|
const authStatus = await store.status();
|
|
565
731
|
const selectionExplicit = store.hasExplicitSelection();
|
|
566
732
|
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
567
733
|
status: "signed-out",
|
|
568
734
|
accounts: [],
|
|
569
|
-
selectionExplicit
|
|
735
|
+
selectionExplicit,
|
|
736
|
+
enabled
|
|
570
737
|
};
|
|
571
738
|
let credential;
|
|
572
739
|
try {
|
|
573
740
|
credential = await store.resolve();
|
|
574
741
|
} catch (error) {
|
|
742
|
+
const webAccounts = accounts.map(toWebAccount);
|
|
575
743
|
return {
|
|
576
744
|
status: "signed-out",
|
|
577
|
-
accounts:
|
|
745
|
+
accounts: webAccounts,
|
|
578
746
|
message: safeMessage(error),
|
|
579
747
|
selectionExplicit,
|
|
580
|
-
|
|
748
|
+
enabled,
|
|
749
|
+
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
750
|
+
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
581
751
|
};
|
|
582
752
|
}
|
|
753
|
+
const selected = accounts.find((account) => account.selected);
|
|
583
754
|
const account = {
|
|
584
|
-
accountId:
|
|
585
|
-
accountName: credential.nickname ??
|
|
586
|
-
...credential.uin === void 0 ? {} : { uin: credential.uin },
|
|
755
|
+
accountId: selected?.id ?? "",
|
|
756
|
+
accountName: credential.nickname ?? "",
|
|
587
757
|
...credential.domain === "" ? {} : { domain: credential.domain },
|
|
588
758
|
region,
|
|
589
759
|
source: credential.source,
|
|
590
760
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
591
761
|
selectionExplicit,
|
|
762
|
+
enabled,
|
|
592
763
|
accounts: accounts.map(toWebAccount),
|
|
593
764
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
594
765
|
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
595
766
|
imageModelIds: [...deps.imageModelIds(region)]
|
|
596
767
|
};
|
|
597
768
|
const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
|
|
769
|
+
const rejected = [creditsResult, checkinResult].some((result) => result.status === "rejected" && isCredentialRejectedError(result.reason));
|
|
770
|
+
const recovery = !rejected ? void 0 : await resolveCredentialRecovery({
|
|
771
|
+
region,
|
|
772
|
+
store: deps.store(region),
|
|
773
|
+
...selected === void 0 ? {} : { rejectedAccountId: selected.id },
|
|
774
|
+
probe: deps.accountUsable ?? (async () => false)
|
|
775
|
+
});
|
|
598
776
|
return {
|
|
599
777
|
status: "signed-in",
|
|
600
778
|
...account,
|
|
601
779
|
...creditsResult.status === "fulfilled" ? { credits: toCredits(creditsResult.value) } : { creditsError: safeMessage(creditsResult.reason) },
|
|
602
|
-
...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) }
|
|
780
|
+
...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) },
|
|
781
|
+
...!rejected ? {} : { credentialRejected: true },
|
|
782
|
+
...recovery === void 0 ? {} : { recovery }
|
|
603
783
|
};
|
|
604
784
|
}
|
|
605
785
|
/**
|
|
@@ -651,7 +831,9 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
651
831
|
const region = requestRegion(req, res);
|
|
652
832
|
if (region === void 0) return;
|
|
653
833
|
try {
|
|
654
|
-
|
|
834
|
+
const accounts = await deps.store(region).accounts();
|
|
835
|
+
deps.regionUsable?.(region, accounts.length > 0);
|
|
836
|
+
json(res, 200, { accounts: accounts.map(toWebAccount) });
|
|
655
837
|
} catch (error) {
|
|
656
838
|
json(res, 500, { error: safeMessage(error) });
|
|
657
839
|
}
|
|
@@ -722,6 +904,7 @@ const modelConfig = z.object({
|
|
|
722
904
|
maxTokens: z.number().step(1).min(1)
|
|
723
905
|
});
|
|
724
906
|
const regionStateConfig = z.object({
|
|
907
|
+
enabled: z.boolean().default(true).description("Whether this region's provider is offered to DSH (opt-out; false withdraws it entirely)"),
|
|
725
908
|
lastCatalog: z.array(modelConfig).default([]),
|
|
726
909
|
enabledModelIds: z.array(z.string()).default([]),
|
|
727
910
|
imageModelIds: z.array(z.string()).default([]),
|
|
@@ -761,6 +944,17 @@ function regionStateOf(config, region) {
|
|
|
761
944
|
...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
|
|
762
945
|
};
|
|
763
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Whether one region's provider is switched on. Opt-out semantics: only an
|
|
949
|
+
* explicit `false` disables it, so every config written before this switch
|
|
950
|
+
* existed — including the pre-region-split flat fields, which never carry
|
|
951
|
+
* `enabled` — keeps both providers running exactly as before. The card reads the
|
|
952
|
+
* same rule through `regionEnabledOf`, so the two halves can never disagree
|
|
953
|
+
* about a region's state.
|
|
954
|
+
*/
|
|
955
|
+
function regionEnabled(config, region) {
|
|
956
|
+
return regionStateOf(config, region).enabled !== false;
|
|
957
|
+
}
|
|
764
958
|
/** Every region, in card tab order. */
|
|
765
959
|
const REGION_KEYS = ["cn", "global"];
|
|
766
960
|
/**
|
|
@@ -844,6 +1038,10 @@ function apply(ctx, config) {
|
|
|
844
1038
|
})
|
|
845
1039
|
};
|
|
846
1040
|
}
|
|
1041
|
+
const accountUsabilityProbe = createAccountUsabilityProbe({
|
|
1042
|
+
store: (region) => stacks[region].store,
|
|
1043
|
+
client
|
|
1044
|
+
});
|
|
847
1045
|
const withImageSelection = (models, images) => models.map((model) => ({
|
|
848
1046
|
...model,
|
|
849
1047
|
...images.has(model.id) ? { multimodal: true } : { multimodal: false }
|
|
@@ -879,15 +1077,81 @@ function apply(ctx, config) {
|
|
|
879
1077
|
stacks[region].catalog.set(configuredModels(value, region));
|
|
880
1078
|
}
|
|
881
1079
|
invalidateCatalog();
|
|
1080
|
+
syncRegionRegistration(value);
|
|
1081
|
+
refreshRegionUsability();
|
|
1082
|
+
};
|
|
1083
|
+
/**
|
|
1084
|
+
* Tell each catalog whether its region has ANY local sign-in, so a region the
|
|
1085
|
+
* user has no account for advertises nothing instead of a roster that can
|
|
1086
|
+
* only 401 (issue #12).
|
|
1087
|
+
*
|
|
1088
|
+
* `accounts()` is the right source rather than the SELECTED credential: an
|
|
1089
|
+
* orphaned saved id must not blank a region that still has other sign-ins to
|
|
1090
|
+
* fall back on, and a region the user deliberately cleared must come back to
|
|
1091
|
+
* life the moment its first account appears.
|
|
1092
|
+
*
|
|
1093
|
+
* Deliberately fire-and-forget and idempotent — it runs on every settings
|
|
1094
|
+
* change, and `setRegionUsable` reports whether anything moved so a
|
|
1095
|
+
* no-change pass costs nothing beyond the scan. A failed scan leaves the
|
|
1096
|
+
* previous answer alone (the catalog starts permissive), so a transient
|
|
1097
|
+
* filesystem error never blanks a working region.
|
|
1098
|
+
*/
|
|
1099
|
+
const refreshRegionUsability = async () => {
|
|
1100
|
+
for (const region of REGION_KEYS) {
|
|
1101
|
+
let accounts;
|
|
1102
|
+
try {
|
|
1103
|
+
accounts = await stacks[region].store.accounts();
|
|
1104
|
+
} catch {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (stacks[region].catalog.setRegionUsable(accounts.length > 0)) invalidateCatalog();
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
/**
|
|
1111
|
+
* Live registration handles per region, filled once the shim is listening.
|
|
1112
|
+
* A disabled region holds ZERO routes while staying registered: DSH allows
|
|
1113
|
+
* `replace([])` for exactly this case ("a settings section that emptied holds
|
|
1114
|
+
* zero routes while staying registered"), which is what makes the on/off
|
|
1115
|
+
* switch reversible without a restart. Withdrawing the adapter route is what
|
|
1116
|
+
* actually removes the region's models from DSH's model picker — hiding the
|
|
1117
|
+
* card tab alone would leave every model selectable.
|
|
1118
|
+
*/
|
|
1119
|
+
const registration = {
|
|
1120
|
+
cn: {},
|
|
1121
|
+
global: {}
|
|
1122
|
+
};
|
|
1123
|
+
/**
|
|
1124
|
+
* Publish each region's on/off state to the harness (issue #11-style region
|
|
1125
|
+
* switch). Both swaps are single synchronous sections, so no request can
|
|
1126
|
+
* observe a half-applied state, and `replace` announces itself through
|
|
1127
|
+
* `llm/adapters-updated`, which is what makes third-party consumers drop the
|
|
1128
|
+
* region too. No-op until the shim has registered; `applySelection` runs again
|
|
1129
|
+
* on every card write (and once more after registration completes), so a
|
|
1130
|
+
* toggle lands immediately.
|
|
1131
|
+
*/
|
|
1132
|
+
const syncRegionRegistration = (value) => {
|
|
1133
|
+
for (const region of REGION_KEYS) registration[region].adapter?.replace(regionEnabled(value, region) ? [WORKBUDDY_PROVIDERS[region]] : []);
|
|
1134
|
+
registration.cn.directory?.replace(REGION_KEYS.filter((region) => regionEnabled(value, region)).map((region) => ({
|
|
1135
|
+
provider: WORKBUDDY_PROVIDERS[region],
|
|
1136
|
+
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
|
|
1137
|
+
settingsNs: WORKBUDDY_SETTINGS_NS,
|
|
1138
|
+
settingsPath: [],
|
|
1139
|
+
declared: false
|
|
1140
|
+
})));
|
|
882
1141
|
};
|
|
883
1142
|
ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
|
|
884
1143
|
store: (region) => stacks[region].store,
|
|
885
1144
|
client,
|
|
1145
|
+
accountUsable: accountUsabilityProbe,
|
|
886
1146
|
displayModels: (region) => displayModels(current(), region),
|
|
887
1147
|
enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
|
|
888
1148
|
imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
|
|
889
1149
|
contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
|
|
890
|
-
discoverModels
|
|
1150
|
+
discoverModels,
|
|
1151
|
+
regionEnabled: (region) => regionEnabled(current(), region),
|
|
1152
|
+
regionUsable(region, usable) {
|
|
1153
|
+
if (stacks[region].catalog.setRegionUsable(usable)) invalidateCatalog();
|
|
1154
|
+
}
|
|
891
1155
|
}));
|
|
892
1156
|
ctx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, Config, config, {
|
|
893
1157
|
setSource(source) {
|
|
@@ -931,9 +1195,9 @@ function apply(ctx, config) {
|
|
|
931
1195
|
let releaseAdapterGlobal;
|
|
932
1196
|
let releaseDirectory;
|
|
933
1197
|
try {
|
|
934
|
-
releaseAdapterCn = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], adapters.cn.adapter);
|
|
935
|
-
releaseAdapterGlobal = ctx.llm.registerAdapter([WORKBUDDY_GLOBAL_PROVIDER], adapters.global.adapter);
|
|
936
|
-
releaseDirectory = ctx.llm.registerConfigurableProviders([{
|
|
1198
|
+
releaseAdapterCn = registration.cn.adapter = ctx.llm.registerAdapter([WORKBUDDY_PROVIDER], adapters.cn.adapter);
|
|
1199
|
+
releaseAdapterGlobal = registration.global.adapter = ctx.llm.registerAdapter([WORKBUDDY_GLOBAL_PROVIDER], adapters.global.adapter);
|
|
1200
|
+
releaseDirectory = registration.cn.directory = ctx.llm.registerConfigurableProviders([{
|
|
937
1201
|
provider: WORKBUDDY_PROVIDER,
|
|
938
1202
|
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES.cn,
|
|
939
1203
|
settingsNs: WORKBUDDY_SETTINGS_NS,
|
|
@@ -964,9 +1228,11 @@ function apply(ctx, config) {
|
|
|
964
1228
|
releaseAdapterGlobal?.();
|
|
965
1229
|
releaseDirectory?.();
|
|
966
1230
|
}
|
|
1231
|
+
syncRegionRegistration(current());
|
|
967
1232
|
ctx.llm.registerModelDiscovery(WORKBUDDY_SETTINGS_NS, async (request, signal) => {
|
|
968
1233
|
const region = regionOfProvider(request.provider ?? "");
|
|
969
1234
|
if (region === void 0) return [];
|
|
1235
|
+
if (!regionEnabled(current(), region)) return [];
|
|
970
1236
|
const discovered = await discoverModels(region, signal);
|
|
971
1237
|
const state = regionStateOf(current(), region);
|
|
972
1238
|
return withImageSelection(deriveCatalog(discovered, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])).map((model) => ({
|
|
@@ -983,22 +1249,25 @@ function apply(ctx, config) {
|
|
|
983
1249
|
return;
|
|
984
1250
|
}
|
|
985
1251
|
if (stopped) return;
|
|
986
|
-
for (const region of REGION_KEYS)
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1252
|
+
for (const region of REGION_KEYS) {
|
|
1253
|
+
if (!regionEnabled(current(), region)) continue;
|
|
1254
|
+
(async () => {
|
|
1255
|
+
try {
|
|
1256
|
+
const credential = await stacks[region].store.resolve();
|
|
1257
|
+
if (stopped) return;
|
|
1258
|
+
const models = await client.fetchModels(credential);
|
|
1259
|
+
if (stopped) return;
|
|
1260
|
+
const state = regionStateOf(current(), region);
|
|
1261
|
+
stacks[region].catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
|
|
1262
|
+
adapters[region].invalidate();
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
ctx.logger.warn(`dsh-connect-workbuddy: dynamic ${region} model catalog unavailable; serving the static fallback list`, error);
|
|
1265
|
+
}
|
|
1266
|
+
})();
|
|
1267
|
+
}
|
|
999
1268
|
}).catch((error) => {
|
|
1000
1269
|
ctx.logger.error("dsh-connect-workbuddy: loopback endpoint failed to start; providers not registered", error);
|
|
1001
1270
|
});
|
|
1002
1271
|
}
|
|
1003
1272
|
//#endregion
|
|
1004
|
-
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, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionCleared, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
1273
|
+
export { CREDENTIAL_REJECTED_CODE, Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, 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, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, 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": "2.0.
|
|
5
|
+
"version": "2.0.10",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|