dsh-connect-workbuddy 3.5.0 → 4.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 +53 -0
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/docs/assets/dsh-connect-workbuddy-composer-credits.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +959 -35
- package/lib/{host-heartbeat-C6G_goTD.js → host-heartbeat-vvVjdY_q.js} +78 -29
- package/lib/index.d.ts +86 -8
- package/lib/index.js +113 -4
- package/package.json +1 -1
- package/screenshots.json +2 -1
|
@@ -583,16 +583,34 @@ const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
|
583
583
|
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
584
584
|
const GLOBAL_BASE = "https://www.workbuddy.ai";
|
|
585
585
|
/**
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
586
|
+
* Legacy model-catalog path, kept only as the CN region's FALLBACK.
|
|
587
|
+
*
|
|
588
|
+
* This is the document the WorkBuddy *plugin* used to read, and it is NOT the
|
|
589
|
+
* document the WorkBuddy *app* reads. The gateway answers it with the CLI
|
|
590
|
+
* channel's roster, whose second slot is the paid `hy4-preview`
|
|
591
|
+
* (`credits: 'x0.29 credits'`), while the app's own config lists the free
|
|
592
|
+
* `hy4-preview-f` (`credits: 'x0.00 credits'`) in that slot under the SAME
|
|
593
|
+
* display name "Hy4 preview". Reading this path is therefore what makes the
|
|
594
|
+
* plugin's card disagree with the app about a model's price — both are
|
|
595
|
+
* correctly displaying a real record, just different ones.
|
|
590
596
|
*/
|
|
591
597
|
const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
|
|
592
598
|
/**
|
|
593
|
-
* Remote product-config path
|
|
594
|
-
*
|
|
595
|
-
*
|
|
599
|
+
* Remote product-config path; the CN region's PRIMARY catalog source and the
|
|
600
|
+
* global region's only one.
|
|
601
|
+
*
|
|
602
|
+
* It is field-compatible with {@link MODELS_CATALOG_PATH} for every key
|
|
603
|
+
* {@link parseUpstreamModel} reads, so one parser still serves both. On CN this
|
|
604
|
+
* path is what the app itself consumes, so the plugin's roster and the app's
|
|
605
|
+
* agree — that parity is the whole point of preferring it.
|
|
606
|
+
*
|
|
607
|
+
* The CLI user agent deliberately stays {@link CLIENT_UA} here. `/v3/config`
|
|
608
|
+
* serves a DIFFERENT roster per client channel: the desktop token yields a
|
|
609
|
+
* roster without the free `hy4-preview-f`, and the CLI token yields one with
|
|
610
|
+
* it. Measured 2026-10-04 — CN + `/v3/config`: CLI token = 17 models incl.
|
|
611
|
+
* `hy4-preview-f`(x0.00); desktop token = 29 models, no `hy4-preview-f` and no
|
|
612
|
+
* cheap free tier. That is why the global branch below pairs this path with
|
|
613
|
+
* {@link DESKTOP_UA} and the CN branch must NOT.
|
|
596
614
|
*/
|
|
597
615
|
const GLOBAL_CONFIG_PATH = "/v3/config";
|
|
598
616
|
const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
|
|
@@ -1152,6 +1170,19 @@ function selectCliModels(rawModels, agents) {
|
|
|
1152
1170
|
* the credential explicitly so token refreshes apply on the next call.
|
|
1153
1171
|
*/
|
|
1154
1172
|
var WorkBuddyUpstreamClient = class {
|
|
1173
|
+
onFallback;
|
|
1174
|
+
/**
|
|
1175
|
+
* Reports a fallback the catalog reader had to take.
|
|
1176
|
+
*
|
|
1177
|
+
* Injected rather than logged here because this module holds no logger on
|
|
1178
|
+
* purpose — it is pure transport + parsing, so it stays importable from
|
|
1179
|
+
* tests and the CLI without a cordis context. The host wires its
|
|
1180
|
+
* `ctx.logger.warn` in; without a host the event is dropped, which is the
|
|
1181
|
+
* same silence the previous single-source reader had.
|
|
1182
|
+
*/
|
|
1183
|
+
constructor(onFallback) {
|
|
1184
|
+
this.onFallback = onFallback;
|
|
1185
|
+
}
|
|
1155
1186
|
/** POST the chat endpoint; a successful answer is the raw SSE response. */
|
|
1156
1187
|
async chatStream(credential, bodyJson, signal) {
|
|
1157
1188
|
let response;
|
|
@@ -1274,13 +1305,23 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1274
1305
|
/**
|
|
1275
1306
|
* Read the model directory for the credential's region.
|
|
1276
1307
|
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
* channel (see {@link DESKTOP_UA})
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1308
|
+
* Both regions prefer `/v3/config`, the document the WorkBuddy app itself
|
|
1309
|
+
* consumes, so the plugin's roster and the app's agree. They differ in the
|
|
1310
|
+
* user agent that requests it, and that difference is load-bearing: `/v3`
|
|
1311
|
+
* serves a different roster per client channel. Global asks as the desktop
|
|
1312
|
+
* channel (see {@link DESKTOP_UA}); CN asks as the CLI channel
|
|
1313
|
+
* ({@link CLIENT_UA}), which is the only CN channel whose `/v3` answer
|
|
1314
|
+
* contains the free `hy4-preview-f` (see {@link GLOBAL_CONFIG_PATH}).
|
|
1315
|
+
*
|
|
1316
|
+
* CN falls back to the legacy {@link MODELS_CATALOG_PATH} when `/v3` fails,
|
|
1317
|
+
* returns a non-zero envelope, or resolves to no usable model, so a gateway
|
|
1318
|
+
* that stops serving the modern document degrades to the previous roster
|
|
1319
|
+
* instead of leaving the region empty. The fallback is silent when unused and
|
|
1320
|
+
* reported when it fires, because a roster that silently differs from the
|
|
1321
|
+
* app's is exactly the bug this ordering exists to fix.
|
|
1322
|
+
*
|
|
1323
|
+
* No user-side toggle is involved: the region comes from the credential's
|
|
1324
|
+
* `domain`.
|
|
1284
1325
|
*/
|
|
1285
1326
|
async fetchModels(credential, signal) {
|
|
1286
1327
|
const timeout = signal ?? AbortSignal.timeout(JSON_TIMEOUT_MS);
|
|
@@ -1303,20 +1344,28 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1303
1344
|
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
1304
1345
|
return selectCliModels(data["models"], data["agents"]);
|
|
1305
1346
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1347
|
+
const readCatalog = async (path) => {
|
|
1348
|
+
const response = await fetch(`${chatBase(credential)}${path}`, {
|
|
1349
|
+
headers: {
|
|
1350
|
+
"Authorization": `Bearer ${credential.accessToken}`,
|
|
1351
|
+
"Accept": "application/json",
|
|
1352
|
+
"Origin": originReferer(credential),
|
|
1353
|
+
"Referer": `${originReferer(credential)}/`,
|
|
1354
|
+
"User-Agent": CLIENT_UA
|
|
1355
|
+
},
|
|
1356
|
+
signal: timeout
|
|
1357
|
+
});
|
|
1358
|
+
const envelope = await readEnvelope(response);
|
|
1359
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
1360
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
1361
|
+
return selectCliModels(data["models"], data["agents"]);
|
|
1362
|
+
};
|
|
1363
|
+
try {
|
|
1364
|
+
return await readCatalog(GLOBAL_CONFIG_PATH);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
this.onFallback?.(`CN model catalog ${GLOBAL_CONFIG_PATH} failed; falling back to ${MODELS_CATALOG_PATH}: ` + (error instanceof Error ? error.message : String(error)));
|
|
1367
|
+
return await readCatalog(MODELS_CATALOG_PATH);
|
|
1368
|
+
}
|
|
1320
1369
|
}
|
|
1321
1370
|
/** Query today's check-in status without changing account state. */
|
|
1322
1371
|
async fetchCheckinStatus(credential) {
|
|
@@ -2806,7 +2855,7 @@ function generationOf(version) {
|
|
|
2806
2855
|
//#endregion
|
|
2807
2856
|
//#region src/version.ts
|
|
2808
2857
|
/** The npm package version this build was produced from. */
|
|
2809
|
-
const WORKBUDDY_CONNECT_VERSION = "
|
|
2858
|
+
const WORKBUDDY_CONNECT_VERSION = "4.0.0";
|
|
2810
2859
|
//#endregion
|
|
2811
2860
|
//#region src/host-heartbeat.ts
|
|
2812
2861
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -628,6 +628,17 @@ declare function parseUpstreamModel(value: unknown): WorkBuddyUpstreamModel | un
|
|
|
628
628
|
* the credential explicitly so token refreshes apply on the next call.
|
|
629
629
|
*/
|
|
630
630
|
declare class WorkBuddyUpstreamClient {
|
|
631
|
+
private readonly onFallback?;
|
|
632
|
+
/**
|
|
633
|
+
* Reports a fallback the catalog reader had to take.
|
|
634
|
+
*
|
|
635
|
+
* Injected rather than logged here because this module holds no logger on
|
|
636
|
+
* purpose — it is pure transport + parsing, so it stays importable from
|
|
637
|
+
* tests and the CLI without a cordis context. The host wires its
|
|
638
|
+
* `ctx.logger.warn` in; without a host the event is dropped, which is the
|
|
639
|
+
* same silence the previous single-source reader had.
|
|
640
|
+
*/
|
|
641
|
+
constructor(onFallback?: ((message: string) => void) | undefined);
|
|
631
642
|
/** POST the chat endpoint; a successful answer is the raw SSE response. */
|
|
632
643
|
chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
|
|
633
644
|
/**
|
|
@@ -662,13 +673,23 @@ declare class WorkBuddyUpstreamClient {
|
|
|
662
673
|
/**
|
|
663
674
|
* Read the model directory for the credential's region.
|
|
664
675
|
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* channel (see {@link DESKTOP_UA})
|
|
670
|
-
*
|
|
671
|
-
*
|
|
676
|
+
* Both regions prefer `/v3/config`, the document the WorkBuddy app itself
|
|
677
|
+
* consumes, so the plugin's roster and the app's agree. They differ in the
|
|
678
|
+
* user agent that requests it, and that difference is load-bearing: `/v3`
|
|
679
|
+
* serves a different roster per client channel. Global asks as the desktop
|
|
680
|
+
* channel (see {@link DESKTOP_UA}); CN asks as the CLI channel
|
|
681
|
+
* ({@link CLIENT_UA}), which is the only CN channel whose `/v3` answer
|
|
682
|
+
* contains the free `hy4-preview-f` (see {@link GLOBAL_CONFIG_PATH}).
|
|
683
|
+
*
|
|
684
|
+
* CN falls back to the legacy {@link MODELS_CATALOG_PATH} when `/v3` fails,
|
|
685
|
+
* returns a non-zero envelope, or resolves to no usable model, so a gateway
|
|
686
|
+
* that stops serving the modern document degrades to the previous roster
|
|
687
|
+
* instead of leaving the region empty. The fallback is silent when unused and
|
|
688
|
+
* reported when it fires, because a roster that silently differs from the
|
|
689
|
+
* app's is exactly the bug this ordering exists to fix.
|
|
690
|
+
*
|
|
691
|
+
* No user-side toggle is involved: the region comes from the credential's
|
|
692
|
+
* `domain`.
|
|
672
693
|
*/
|
|
673
694
|
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
674
695
|
/** Query today's check-in status without changing account state. */
|
|
@@ -1850,7 +1871,13 @@ interface WorkBuddyWebModel {
|
|
|
1850
1871
|
name: string;
|
|
1851
1872
|
/** Effective DSH context after applying the saved local budget. */
|
|
1852
1873
|
contextWindow: number;
|
|
1853
|
-
/**
|
|
1874
|
+
/**
|
|
1875
|
+
* Native maximum advertised by WorkBuddy, never clamped by a default.
|
|
1876
|
+
*
|
|
1877
|
+
* Models above 200K expose the tier selector (200K / 500K / max); whether the
|
|
1878
|
+
* window is actually reduced is the user's stored `contextBudgets` decision
|
|
1879
|
+
* and nothing else — see issue #33.
|
|
1880
|
+
*/
|
|
1854
1881
|
nativeContextWindow: number;
|
|
1855
1882
|
maxTokens: number;
|
|
1856
1883
|
creditMultiplier?: number;
|
|
@@ -1956,6 +1983,17 @@ type WorkBuddyWebUsage =
|
|
|
1956
1983
|
accounts: readonly WorkBuddyWebAccount[];
|
|
1957
1984
|
/** Whether this region's provider is currently offered to DSH. */
|
|
1958
1985
|
enabled?: boolean;
|
|
1986
|
+
/**
|
|
1987
|
+
* Whether this region's sidebar credit line is shown.
|
|
1988
|
+
*
|
|
1989
|
+
* Carried on the Host's answer rather than read from the browser settings
|
|
1990
|
+
* mirror, for the same measured reason as `contextBudgets`: on the affected
|
|
1991
|
+
* 0.1.7 deployment the mirror never picks up this plugin's writes (a save
|
|
1992
|
+
* through the Host endpoint leaves it stale), so a control reading it kept
|
|
1993
|
+
* showing the old value and looked like it could not be turned off — the
|
|
1994
|
+
* "关闭不了" report.
|
|
1995
|
+
*/
|
|
1996
|
+
showCreditsInMainUi?: boolean;
|
|
1959
1997
|
message?: string;
|
|
1960
1998
|
/** The persisted account id matches no local account. */
|
|
1961
1999
|
selectionLost?: boolean;
|
|
@@ -1984,6 +2022,12 @@ type WorkBuddyWebUsage =
|
|
|
1984
2022
|
region: WorkBuddyWebRegion;
|
|
1985
2023
|
/** Whether this region's provider is currently offered to DSH. */
|
|
1986
2024
|
enabled?: boolean;
|
|
2025
|
+
/**
|
|
2026
|
+
* Whether this region's sidebar credit line is shown. See the signed-out
|
|
2027
|
+
* variant for why this travels on the Host's answer instead of the browser
|
|
2028
|
+
* settings mirror.
|
|
2029
|
+
*/
|
|
2030
|
+
showCreditsInMainUi?: boolean;
|
|
1987
2031
|
source?: 'desktop' | 'dsh';
|
|
1988
2032
|
tokenExpiresAtMs: number;
|
|
1989
2033
|
/** A saved per-region choice is in effect (false = following the app). */
|
|
@@ -2290,6 +2334,19 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
2290
2334
|
* committed settings value rather than a local guess.
|
|
2291
2335
|
*/
|
|
2292
2336
|
regionEnabled(region: WorkBuddyRegion): boolean;
|
|
2337
|
+
/**
|
|
2338
|
+
* Whether the region's sidebar credit line is shown.
|
|
2339
|
+
*
|
|
2340
|
+
* Read from the COMMITTED settings value here rather than in the browser, for
|
|
2341
|
+
* the same reason as `regionEnabled` — and additionally because the browser's
|
|
2342
|
+
* settings mirror is documented as stale for this namespace on the affected
|
|
2343
|
+
* 0.1.7 deployments, which made the checkbox appear unable to turn the line
|
|
2344
|
+
* off ("关闭不了"). The card reads this field off the Host's answer.
|
|
2345
|
+
*
|
|
2346
|
+
* Optional so a Host that predates the setting still satisfies the interface;
|
|
2347
|
+
* absent reads as ON, matching the schema's `default(true)`.
|
|
2348
|
+
*/
|
|
2349
|
+
showCreditsInMainUi?(region: WorkBuddyRegion): boolean;
|
|
2293
2350
|
/**
|
|
2294
2351
|
* Send one real-volume request to each named model of a region and report what
|
|
2295
2352
|
* came back.
|
|
@@ -2332,6 +2389,18 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
2332
2389
|
* probe.
|
|
2333
2390
|
*/
|
|
2334
2391
|
accountUsable?(region: WorkBuddyRegion, account: WorkBuddyRecoveryCandidate): Promise<boolean>;
|
|
2392
|
+
/**
|
|
2393
|
+
* Why each of the region's accounts cannot serve right now, keyed by account
|
|
2394
|
+
* id. Absent key (or an absent dep) means "no reason known" — the composer
|
|
2395
|
+
* panel then marks nothing, which is the honest answer for an account that has
|
|
2396
|
+
* never been measured.
|
|
2397
|
+
*
|
|
2398
|
+
* The rule is the SAME one the pool ranks by (`exclusionOf`), including its
|
|
2399
|
+
* cooldown-expiry handling, so an account the pool calls usable and one the
|
|
2400
|
+
* panel calls rate-limited can never be the same account at the same moment.
|
|
2401
|
+
* Reimplementing the expiry check here would let the two drift.
|
|
2402
|
+
*/
|
|
2403
|
+
accountExclusions?(region: WorkBuddyRegion): Promise<Readonly<Record<string, WorkBuddyWebPoolExclusion>>>;
|
|
2335
2404
|
/**
|
|
2336
2405
|
* The region's account pool, assembled on the Host.
|
|
2337
2406
|
*
|
|
@@ -2481,6 +2550,15 @@ interface WorkBuddyRegionState {
|
|
|
2481
2550
|
* row (see `syncRegionRegistration`).
|
|
2482
2551
|
*/
|
|
2483
2552
|
enabled?: boolean;
|
|
2553
|
+
/**
|
|
2554
|
+
* Whether this region's credit balance shows at the DSH sidebar foot.
|
|
2555
|
+
*
|
|
2556
|
+
* Per-region, independently switched: WB CN and WB AI are separate accounts
|
|
2557
|
+
* on separate gateways, so a user holding only one of them must be able to
|
|
2558
|
+
* show exactly that one. Opt-out — an absent value reads as ON, so a config
|
|
2559
|
+
* written before this switch existed keeps whatever the default was.
|
|
2560
|
+
*/
|
|
2561
|
+
showCreditsInMainUi?: boolean;
|
|
2484
2562
|
/** The last-refreshed directory for this region; what the card displays. */
|
|
2485
2563
|
lastCatalog?: WorkBuddyModelInfo[];
|
|
2486
2564
|
/** The user's selection in this region, as model ids. */
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as macosNestedAppBundles, A as workbuddyOwnAuthPath, B as parseUpstreamModel, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, E as isEncryptedCredentialError, F as classifyUpstreamError, G as deriveAtRestKey, H as regionOf, I as declaredTools, J as findWorkbuddyAppExecutable, K as deriveAtRestKeyId, L as isCredentialRejectedError, M as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, N as WorkBuddyCredentialRejectedError, O as parseWorkBuddyAuth, P as WorkBuddyUpstreamClient, Q as macosBundleExecutable, R as parseCreditMultiplier, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as WORKBUDDY_APP_EXECUTABLE_ENV, V as prepareChatBody, W as clearAtRestKeyCache, X as isEncryptedFieldWrapper, Z as isWorkbuddyBundle, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as openEncryptedField, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, j as CREDENTIAL_REJECTED_CODE, k as workbuddyAccountId, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as workbuddyAppExecutableCandidates, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as fetchAtRestKeyPayload, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as readAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as parseReasoning } from "./host-heartbeat-
|
|
1
|
+
import { $ as macosNestedAppBundles, A as workbuddyOwnAuthPath, B as parseUpstreamModel, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, E as isEncryptedCredentialError, F as classifyUpstreamError, G as deriveAtRestKey, H as regionOf, I as declaredTools, J as findWorkbuddyAppExecutable, K as deriveAtRestKeyId, L as isCredentialRejectedError, M as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, N as WorkBuddyCredentialRejectedError, O as parseWorkBuddyAuth, P as WorkBuddyUpstreamClient, Q as macosBundleExecutable, R as parseCreditMultiplier, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as WORKBUDDY_APP_EXECUTABLE_ENV, V as prepareChatBody, W as clearAtRestKeyCache, X as isEncryptedFieldWrapper, Z as isWorkbuddyBundle, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as openEncryptedField, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, j as CREDENTIAL_REJECTED_CODE, k as workbuddyAccountId, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as workbuddyAppExecutableCandidates, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as fetchAtRestKeyPayload, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as readAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as parseReasoning } from "./host-heartbeat-vvVjdY_q.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -2832,6 +2832,18 @@ async function writePoolProbes(region, updates) {
|
|
|
2832
2832
|
*/
|
|
2833
2833
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
2834
2834
|
const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
2835
|
+
/**
|
|
2836
|
+
* Per-account credit balances for one region, read ONLY while the composer
|
|
2837
|
+
* panel is open.
|
|
2838
|
+
*
|
|
2839
|
+
* Separate from {@link WORKBUDDY_USAGE_PATH} because its cost is different in
|
|
2840
|
+
* kind: the usage document is one upstream read for the selected account, while
|
|
2841
|
+
* this is one read PER account in the region. Folding it into the usage route
|
|
2842
|
+
* would make every 5-minute refresh pay for a table nobody is looking at, so it
|
|
2843
|
+
* is fetched on demand instead — the same principle the pool applies to its
|
|
2844
|
+
* per-account figures.
|
|
2845
|
+
*/
|
|
2846
|
+
const WORKBUDDY_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-workbuddy/account-credits";
|
|
2835
2847
|
/** Plugin-owned live model refresh endpoint. */
|
|
2836
2848
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
2837
2849
|
/** Plugin-owned local account rescan endpoint. */
|
|
@@ -3158,6 +3170,39 @@ function volatileFlagOf(field) {
|
|
|
3158
3170
|
return Config.dict?.[field]?.meta?.volatile === true;
|
|
3159
3171
|
}
|
|
3160
3172
|
/**
|
|
3173
|
+
* Per-account credit balances for one region, for the composer panel's table.
|
|
3174
|
+
*
|
|
3175
|
+
* ONE upstream read per account, which is exactly why this is its own route:
|
|
3176
|
+
* the panel calls it only while it is open, so the 5-minute readout never
|
|
3177
|
+
* multiplies its cost by the account count.
|
|
3178
|
+
*
|
|
3179
|
+
* Every account is listed even when its balance cannot be read — a credential
|
|
3180
|
+
* that no longer resolves, or an upstream that refused — because the panel's
|
|
3181
|
+
* job is to show which accounts exist and let the user switch. Such a row
|
|
3182
|
+
* carries NO `credits` field rather than `0`, so the panel renders "—" instead
|
|
3183
|
+
* of telling the user they are out of credits.
|
|
3184
|
+
*
|
|
3185
|
+
* Failures are contained per account: one dead credential must not blank the
|
|
3186
|
+
* table for the accounts that are fine.
|
|
3187
|
+
*/
|
|
3188
|
+
async function workBuddyAccountCredits(deps, region) {
|
|
3189
|
+
const store = deps.store(region);
|
|
3190
|
+
const accounts = await store.accounts();
|
|
3191
|
+
const exclusions = await deps.accountExclusions?.(region).catch(() => void 0) ?? {};
|
|
3192
|
+
return Promise.all(accounts.map(async (account) => {
|
|
3193
|
+
const credential = await store.credentialFor(account.id).catch(() => void 0);
|
|
3194
|
+
const credits = credential === void 0 ? void 0 : await deps.client.fetchCredits(credential).then((answer) => answer.total).catch(() => void 0);
|
|
3195
|
+
const excludedBy = exclusions[account.id];
|
|
3196
|
+
return {
|
|
3197
|
+
id: account.id,
|
|
3198
|
+
accountName: account.accountName,
|
|
3199
|
+
selected: account.selected,
|
|
3200
|
+
...credits === void 0 ? {} : { credits },
|
|
3201
|
+
...excludedBy === void 0 ? {} : { excludedBy }
|
|
3202
|
+
};
|
|
3203
|
+
}));
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3161
3206
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
3162
3207
|
* the region-scoped store already answers with only that region's accounts,
|
|
3163
3208
|
* so the document's model slots and account list are that region's by
|
|
@@ -3169,13 +3214,15 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3169
3214
|
const accounts = await store.accounts();
|
|
3170
3215
|
deps.regionUsable?.(region, accounts.length > 0);
|
|
3171
3216
|
const enabled = deps.regionEnabled(region);
|
|
3217
|
+
const showCreditsInMainUi = deps.showCreditsInMainUi?.(region) ?? true;
|
|
3172
3218
|
const authStatus = await store.status();
|
|
3173
3219
|
const selectionExplicit = store.hasExplicitSelection();
|
|
3174
3220
|
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
3175
3221
|
status: "signed-out",
|
|
3176
3222
|
accounts: [],
|
|
3177
3223
|
selectionExplicit,
|
|
3178
|
-
enabled
|
|
3224
|
+
enabled,
|
|
3225
|
+
showCreditsInMainUi
|
|
3179
3226
|
};
|
|
3180
3227
|
let credential;
|
|
3181
3228
|
try {
|
|
@@ -3188,6 +3235,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3188
3235
|
message: safeMessage(error),
|
|
3189
3236
|
selectionExplicit,
|
|
3190
3237
|
enabled,
|
|
3238
|
+
showCreditsInMainUi,
|
|
3191
3239
|
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
3192
3240
|
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
3193
3241
|
};
|
|
@@ -3202,6 +3250,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3202
3250
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
3203
3251
|
selectionExplicit,
|
|
3204
3252
|
enabled,
|
|
3253
|
+
showCreditsInMainUi,
|
|
3205
3254
|
accounts: accounts.map(toWebAccount),
|
|
3206
3255
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
3207
3256
|
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
@@ -3354,6 +3403,21 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
3354
3403
|
}
|
|
3355
3404
|
}
|
|
3356
3405
|
});
|
|
3406
|
+
const disposeAccountCredits = ctx.webServer.register({
|
|
3407
|
+
kind: "exact",
|
|
3408
|
+
path: WORKBUDDY_ACCOUNT_CREDITS_PATH,
|
|
3409
|
+
handler: async (req, res) => {
|
|
3410
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
3411
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
3412
|
+
const region = requestRegion(req, res);
|
|
3413
|
+
if (region === void 0) return;
|
|
3414
|
+
try {
|
|
3415
|
+
json(res, 200, { accounts: await workBuddyAccountCredits(deps, region) });
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
json(res, 500, { error: safeMessage(error) });
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
});
|
|
3357
3421
|
const disposeAccounts = ctx.webServer.register({
|
|
3358
3422
|
kind: "exact",
|
|
3359
3423
|
path: WORKBUDDY_ACCOUNTS_REFRESH_PATH,
|
|
@@ -3586,6 +3650,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
3586
3650
|
disposeCheckin();
|
|
3587
3651
|
disposeAccounts();
|
|
3588
3652
|
disposeUsage();
|
|
3653
|
+
disposeAccountCredits();
|
|
3589
3654
|
disposeProbe();
|
|
3590
3655
|
disposePool();
|
|
3591
3656
|
disposeDiagWrite();
|
|
@@ -3785,6 +3850,22 @@ const poolConfig = z.object({
|
|
|
3785
3850
|
});
|
|
3786
3851
|
const regionStateConfig = z.object({
|
|
3787
3852
|
enabled: z.boolean().default(true).description("Whether this region's provider is offered to DSH (opt-out; false withdraws it entirely)"),
|
|
3853
|
+
/**
|
|
3854
|
+
* Whether this region's credit line shows at the DSH sidebar foot.
|
|
3855
|
+
*
|
|
3856
|
+
* Per-REGION rather than one global switch, because the two sides are
|
|
3857
|
+
* independent providers the user may hold only one account on: a CN-only user
|
|
3858
|
+
* has nothing to read from the international line and vice versa, and a
|
|
3859
|
+
* single shared switch would force them to either show a permanently empty
|
|
3860
|
+
* row or lose the one that works.
|
|
3861
|
+
*
|
|
3862
|
+
* ON by default (opt-out). The plugin card is not reachable in every host —
|
|
3863
|
+
* a market plugin can own the Plugins tab and never render
|
|
3864
|
+
* `plugins.bundle.config` — so a feature that is off until discovered would be
|
|
3865
|
+
* off forever for those users. The line carries its own `×` in the sidebar,
|
|
3866
|
+
* which is reachable exactly where the feature renders.
|
|
3867
|
+
*/
|
|
3868
|
+
showCreditsInMainUi: z.boolean().default(true).description("Show this region's remaining credits at the DSH sidebar foot (opt-out; the line carries its own hide button)"),
|
|
3788
3869
|
lastCatalog: z.array(modelConfig).default([]),
|
|
3789
3870
|
enabledModelIds: z.array(z.string()).default([]),
|
|
3790
3871
|
imageModelIds: z.array(z.string()).default([]),
|
|
@@ -3871,7 +3952,7 @@ const Config = z.object({
|
|
|
3871
3952
|
*/
|
|
3872
3953
|
function regionStateOf(config, region) {
|
|
3873
3954
|
const stored = unwrapVolatile(config.regions)?.[region];
|
|
3874
|
-
if (stored !== void 0) return stored;
|
|
3955
|
+
if (stored !== void 0 && stored !== null) return stored;
|
|
3875
3956
|
if (region !== "cn") return {};
|
|
3876
3957
|
const lastCatalog = unwrapVolatile(config.lastCatalog);
|
|
3877
3958
|
const enabledModelIds = unwrapVolatile(config.enabledModelIds);
|
|
@@ -3959,7 +4040,9 @@ async function legacyAttributionRegion(value, accountsFor) {
|
|
|
3959
4040
|
* upstream never leaves a provider empty.
|
|
3960
4041
|
*/
|
|
3961
4042
|
function apply(ctx, config) {
|
|
3962
|
-
const client = new WorkBuddyUpstreamClient()
|
|
4043
|
+
const client = new WorkBuddyUpstreamClient((message) => {
|
|
4044
|
+
ctx.logger.warn(`dsh-connect-workbuddy: ${message}`);
|
|
4045
|
+
});
|
|
3963
4046
|
/**
|
|
3964
4047
|
* How long a check-in reading is reused before asking the upstream again.
|
|
3965
4048
|
*
|
|
@@ -4138,6 +4221,25 @@ function apply(ctx, config) {
|
|
|
4138
4221
|
store: (region) => stacks[region].store,
|
|
4139
4222
|
client,
|
|
4140
4223
|
accountUsable: accountUsabilityProbe,
|
|
4224
|
+
/**
|
|
4225
|
+
* Why each of the region's accounts cannot serve right now, for the composer
|
|
4226
|
+
* panel's table.
|
|
4227
|
+
*
|
|
4228
|
+
* Runs the SAME `exclusionOf` the pool ranks by, against every account the
|
|
4229
|
+
* region knows — not only pool members. Deriving it any other way (a second
|
|
4230
|
+
* expiry comparison, or `probe.outcome` read raw) would let the panel call an
|
|
4231
|
+
* account rate-limited while the pool has already moved past its cooldown.
|
|
4232
|
+
*/
|
|
4233
|
+
accountExclusions: async (region) => {
|
|
4234
|
+
const probes = await readPoolProbes(region);
|
|
4235
|
+
const nowMs = Date.now();
|
|
4236
|
+
const out = {};
|
|
4237
|
+
for (const [accountId, probe] of Object.entries(probes)) {
|
|
4238
|
+
const excludedBy = exclusionOf(probe, nowMs);
|
|
4239
|
+
if (excludedBy !== void 0) out[accountId] = excludedBy;
|
|
4240
|
+
}
|
|
4241
|
+
return out;
|
|
4242
|
+
},
|
|
4141
4243
|
displayModels: (region) => displayModels(current(), region),
|
|
4142
4244
|
enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
|
|
4143
4245
|
imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
|
|
@@ -4146,6 +4248,13 @@ function apply(ctx, config) {
|
|
|
4146
4248
|
discoverModels,
|
|
4147
4249
|
regionEnabled: (region) => regionEnabled(current(), region),
|
|
4148
4250
|
/**
|
|
4251
|
+
* The region's committed sidebar-credit flag, read from the same config the
|
|
4252
|
+
* card writes. Exposed on the Host's answer because the browser settings
|
|
4253
|
+
* mirror goes stale for this namespace on the affected 0.1.7 deployments,
|
|
4254
|
+
* which made the switch look unable to turn the line off ("关闭不了").
|
|
4255
|
+
*/
|
|
4256
|
+
showCreditsInMainUi: (region) => regionStateOf(current(), region).showCreditsInMainUi !== false,
|
|
4257
|
+
/**
|
|
4149
4258
|
* One minimal request per named model, through the region's own credential.
|
|
4150
4259
|
*
|
|
4151
4260
|
* Resolved here rather than in the route so the browser half never touches a
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "4.0.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
package/screenshots.json
CHANGED