dsh-workbuddy-connect 0.5.2 → 0.5.4

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/README.en.md CHANGED
@@ -22,13 +22,15 @@ Both the CN **WorkBuddy** and the international **WorkBuddy AI** apps are suppor
22
22
 
23
23
  - **Status and detection**: Settings → Plugins → the matching card shows the account, token validity, remaining credit, and model offers. It also lets you refresh the model list manually and shows whether the current list came from the upstream or from the built-in fallback, and provides manual reasoning-level detection for eligible models.
24
24
 
25
+ - **Enterprise credit**: on the CN product, enterprise accounts (non-empty `enterpriseId`) read their cycle quota from the enterprise billing endpoint, and the card shows an "enterprise quota" row with the cycle reset time.
26
+
25
27
  - **Rate**: every model name carries its credits multiplier (e.g. `GLM-5.2 · x0.79`, `Hy3 · x0.00`) in both the `/model` popup and the composer's model dropdown. The rate is display-only and never affects requests.
26
28
 
27
29
  - **Promo badges**: promo badges (`限时免费`, `夜间折扣`) ride the model name itself (e.g. `Hy4 preview · x0.00 · 限时免费`), visible wherever you pick a model; the status card also collects currently-discounted models. Per the WorkBuddy service data, synced each time DSH starts. The international version's promotions come from the service's `modelPromotions` (which carry an effective window). Once a promotion lapses its badge is withdrawn; because the service writes the discounted value into the model's own rate field, the original price cannot be reconstructed, so that model then reports "price unavailable — refresh to update" rather than repeating the discounted rate or claiming the model is free.
28
30
 
29
31
  ![Settings card showing the plugin](assets/2.png)
30
32
 
31
- The expanded card has three tabs: **Status** shows the account, token validity, total credit, catalog source, and reasoning-level detection; **Context** lists each model's context window (the international version distinguishes the default window from a larger selectable one); **Details** shows per-package credit and model offers. The CN and international versions each get their own card, showing their own account's information.
33
+ The expanded card has three tabs: **Status** shows the account, token validity, total credit, catalog source, and reasoning-level detection; **Context** lists each model's context window (where the international version offers a larger declared window, the "Use the largest declared context window" switch lives here — it is **on by default**, so DSH sizes context compression to the largest window the upstream declares; turn it off to follow the upstream default instead, and the preference persists across restarts); **Details** shows per-package credit and model offers. The CN and international versions each get their own card, showing their own account's information.
32
34
 
33
35
  ![Settings card showing account and remaining credit](assets/3.png)
34
36
 
@@ -110,6 +112,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
110
112
  - **The international version's model catalog comes from the app's own interface**: the service splits it by User-Agent, which is a private implementation detail that a server-side change can break. When that happens the plugin degrades to this account's last successful catalog and then to its built-in roster, showing the source (live / saved / built-in), the fetch time, and the failure reason on the card — but long-term compatibility is not guaranteed. The CN version's catalog uses the same interface as the official CLI and is unaffected.
111
113
  - **International-version environments not yet covered**: on Windows / WSL / Linux no reliable source for the international app's version has been located yet, so the saved value or the built-in default is used. On macOS, real-shim checks covered complete GPT-family replies, tool calls, and continued turns.
112
114
  - **Behaviour change with no credentials**: a version whose app was never signed in — and that left no plugin-owned copy — no longer shows a model group. The CN version used to display a built-in fallback list, but every model on it failed when selected.
115
+ - **The enterprise credit path currently covers the CN product only**: the international enterprise billing interface is unverified, so those accounts still read through the personal endpoint pending measurement. The enterprise branch could not be tested locally (the development machine holds a personal account); it was implemented from the official app's interface contract, and reports from enterprise users are welcome.
113
116
  - Relies on WorkBuddy client interfaces (not a public API); the plugin may need updates as WorkBuddy changes.
114
117
 
115
118
  ## Disclaimer
package/README.md CHANGED
@@ -30,6 +30,8 @@
30
30
 
31
31
  - **信息查看与检测**:设置 → 插件 → 对应卡片可查看账号、令牌有效期、剩余积分和模型优惠;也可以手动刷新模型列表,并在卡片上看到当前列表来自上游还是内置兜底。对于可检测模型,也可以在这里手动检测推理档位。
32
32
 
33
+ - **企业账号积分**:国内版企业账号(`enterpriseId` 非空)走企业专用计费接口读取周期额度,卡片显示「企业额度」与周期重置时间。
34
+
33
35
  - **费率比例**:模型选择列表里每个模型名后直接显示积分倍率(如 `GLM-5.2 · x0.79`、`Hy3 · x0.00`),`/model` 弹窗与输入框的模型下拉都能看到。倍率只是显示,不影响实际请求。
34
36
 
35
37
 
@@ -37,7 +39,7 @@
37
39
 
38
40
  ![设置卡片显示插件](assets/2.png)
39
41
 
40
- 卡片展开后分为「状态 / 上下文 / 明细」三个标签:状态页展示账号、令牌有效期、合计积分、模型列表来源与推理档位检测;上下文页列出各模型的上下文窗口(国际版会区分默认窗口与可选的更大窗口);明细页展示各套餐余量与模型优惠。国内版与国际版各有一张自己的卡片,各显示自己账号的信息。
42
+ 卡片展开后分为「状态 / 上下文 / 明细」三个标签:状态页展示账号、令牌有效期、合计积分、模型列表来源与推理档位检测;上下文页列出各模型的上下文窗口。国际版在上游声明了更大可选窗口时,可在这里切换「使用上游声明的最大上下文窗口」;该开关**默认开启**,DSH 会按上游声明的最大窗口安排上下文压缩;想改用上游的默认窗口就在这里关掉,偏好会持久化,重启后保持。明细页展示各套餐余量与模型优惠。国内版与国际版各有一张自己的卡片,各显示自己账号的信息。
41
43
 
42
44
  ![设置卡片显示账号与剩余积分](assets/3.png)
43
45
 
@@ -120,6 +122,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
120
122
  - **国际版的模型目录来自 App 界面接口**:服务端按 User-Agent 分流下发,属私有实现,上游改动可能使其失效。届时插件按「本账号上次成功目录 → 内置目录」降级,并在卡片上标明来源(实时 / 已保存 / 内置)、更新时间与失败原因,但不能保证长期兼容。国内版目录走官方 CLI 同款接口,不受此影响。
121
123
  - **国际版仍未覆盖的环境**:Windows / WSL / Linux 下国际版 App 的版本读取尚未找到可靠来源,会退回最近保存的版本或内置值。macOS 上已通过真实 shim 验证 GPT 系完整回复、工具调用与续轮。
122
124
  - **无凭据时的行为变化**:某版 App 从未登录、也没留下插件自留副本时,该版模型分组不再显示。此前国内版会显示一份内置兜底列表,但那些模型选了必然报错。
125
+ - **企业账号积分目前仅覆盖国内版**:国际版企业账号的计费接口尚未验证,仍按个人版接口读取;待有实测结论后再扩展。企业账号分支在本机无法自测(开发机为个人账号),依据官方 App 的接口契约实现,欢迎企业账号用户反馈实测结果。
123
126
  - 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。
124
127
 
125
128
  ## 免责声明
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { E as WorkBuddyUpstreamClient, J as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-CExA7lJt.js";
2
+ import { E as WorkBuddyUpstreamClient, J as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-XUStsapF.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -118,7 +118,13 @@ async function status(jsonOutput, variant) {
118
118
  let credits;
119
119
  try {
120
120
  const credential = await store.current();
121
- if (credential !== void 0) credits = { total: (await client.fetchCredits(credential)).total };
121
+ if (credential !== void 0) {
122
+ const fetched = await client.fetchCredits(credential);
123
+ credits = {
124
+ total: fetched.total,
125
+ ...fetched.unlimited === true ? { unlimited: true } : {}
126
+ };
127
+ }
122
128
  } catch (error) {
123
129
  credits = {
124
130
  total: 0,
@@ -138,6 +144,7 @@ async function status(jsonOutput, variant) {
138
144
  ...authStatus.domain === void 0 || authStatus.domain === "" ? {} : { domain: authStatus.domain },
139
145
  source: authStatus.source,
140
146
  credits: credits?.total,
147
+ ...credits?.unlimited === true ? { creditsUnlimited: true } : {},
141
148
  ...credits?.error === void 0 ? {} : { creditsError: credits.error },
142
149
  hostBundle: hostState
143
150
  });
@@ -146,7 +153,7 @@ async function status(jsonOutput, variant) {
146
153
  process.stdout.write([
147
154
  `${variant.displayName} Connect: signed in${authStatus.nickname === void 0 ? "" : ` as ${authStatus.nickname}`}`,
148
155
  ...expiresAt === void 0 ? [] : [`Access token expires ${expiresAt} (refresh is automatic)`],
149
- credits?.error === void 0 ? `Remaining credit: ${credits?.total ?? "unknown"}` : `Remaining credit: unavailable (${credits.error})`,
156
+ credits?.error !== void 0 ? `Remaining credit: unavailable (${credits.error})` : credits?.unlimited === true ? "Remaining credit: unlimited" : `Remaining credit: ${credits?.total ?? "unknown"}`,
150
157
  `Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : hostState === "stale" ? "stale heartbeat (DSH process exited)" : "not started in this profile"}`,
151
158
  "Client card: load failures are logged to the browser console only; the host provider is unaffected.",
152
159
  ""
package/lib/client.js CHANGED
@@ -63,15 +63,17 @@ window.__ModuleLoader__.load({
63
63
  statusPath: WORKBUDDY_STATUS_PATH,
64
64
  probePath: WORKBUDDY_PROBE_PATH
65
65
  };
66
- /** Both cards, in display order. */
67
- const CARD_VARIANTS = [CN_CARD_VARIANT, {
66
+ /** International WorkBuddy AI. */
67
+ const AI_CARD_VARIANT = {
68
68
  id: "workbuddy-ai",
69
69
  titleKey: "titleAI",
70
70
  introKey: "introAI",
71
71
  signedOutKey: "signedOutHintAI",
72
72
  statusPath: WORKBUDDY_AI_STATUS_PATH,
73
73
  probePath: WORKBUDDY_AI_PROBE_PATH
74
- }];
74
+ };
75
+ /** Both cards, in display order. */
76
+ const CARD_VARIANTS = [CN_CARD_VARIANT, AI_CARD_VARIANT];
75
77
  const POLL_INTERVAL_MS = 6e4;
76
78
  const cardStyle = {
77
79
  overflow: "hidden",
@@ -199,6 +201,22 @@ window.__ModuleLoader__.load({
199
201
  lineHeight: "18px",
200
202
  color: "var(--dsw-alias-label-tertiary)"
201
203
  };
204
+ const contextPreferenceStyle = {
205
+ display: "flex",
206
+ alignItems: "flex-start",
207
+ gap: 9,
208
+ padding: "10px 12px",
209
+ border: "1px solid var(--dsw-alias-border-l2)",
210
+ borderRadius: 8,
211
+ color: "var(--dsw-alias-label-primary)",
212
+ fontSize: 13,
213
+ lineHeight: "20px"
214
+ };
215
+ const contextPreferenceCopyStyle = {
216
+ display: "flex",
217
+ flexDirection: "column",
218
+ gap: 2
219
+ };
202
220
  const modelBadgeChipStyle = {
203
221
  padding: "1px 8px",
204
222
  borderRadius: 999,
@@ -336,6 +354,11 @@ window.__ModuleLoader__.load({
336
354
  timeStyle: "short"
337
355
  }).format(new Date(ms));
338
356
  }
357
+ function formatCycleReset(time) {
358
+ const parsed = Date.parse(time);
359
+ if (!Number.isNaN(parsed)) return formatTime(parsed);
360
+ return time;
361
+ }
339
362
  /**
340
363
  * One billing package as a labeled progress bar.
341
364
  *
@@ -345,7 +368,29 @@ window.__ModuleLoader__.load({
345
368
  * honest "remaining N" line printed below it. Unknown size therefore renders the
346
369
  * percent slot as unknown copy and an unfilled, indeterminate track.
347
370
  */
348
- function CreditBar({ label, remain, size, t }) {
371
+ function CreditBar({ label, remain, size, unlimited, t }) {
372
+ if (unlimited === true) {
373
+ const quotaText = t("unlimitedQuota");
374
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
375
+ style: quotaGroupStyle,
376
+ children: [
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
378
+ style: quotaLabelStyle,
379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: quotaText })]
380
+ }),
381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
382
+ style: progressTrackStyle,
383
+ role: "progressbar",
384
+ "aria-label": label,
385
+ "aria-valuetext": quotaText
386
+ }),
387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
388
+ style: bodyStyle,
389
+ children: quotaText
390
+ })
391
+ ]
392
+ });
393
+ }
349
394
  const sizeKnown = size > 0;
350
395
  const detail = sizeKnown ? t("exactRemaining", {
351
396
  remain: formatNumber(remain),
@@ -425,31 +470,56 @@ window.__ModuleLoader__.load({
425
470
  * the default is the budget actually requested, while the larger value is a
426
471
  * ceiling the upstream would accept.
427
472
  */
428
- function ContextTable({ models, t }) {
473
+ function ContextTable({ models, t, useMaximumContextWindow, disabled, onUseMaximumContextWindow }) {
429
474
  const known = (models ?? []).filter((model) => model.contextWindow !== void 0).sort((a, b) => b.contextWindow - a.contextWindow);
430
- if (known.length === 0) return null;
475
+ const canSelectMaximum = known.some((model) => model.maxContextWindow !== void 0 && model.maxContextWindow > (model.defaultContextWindow ?? model.contextWindow ?? 0));
476
+ const showPreference = onUseMaximumContextWindow !== void 0 && (canSelectMaximum || useMaximumContextWindow === true);
477
+ if (known.length === 0 && !showPreference) return null;
431
478
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
432
479
  style: quotaListStyle,
433
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
434
- style: quotaTitleStyle,
435
- children: t("contextHeading")
436
- }), known.map((model) => {
437
- const capacity = model.contextWindow;
438
- const alternative = model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
439
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
440
- style: quotaLabelStyle,
441
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
442
- style: modelOfferStyle,
443
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
444
- style: { textAlign: "right" },
445
- children: formatTokens(capacity)
446
- }), alternative === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
480
+ children: [
481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
482
+ style: quotaTitleStyle,
483
+ children: t("contextHeading")
484
+ }),
485
+ showPreference && onUseMaximumContextWindow !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
486
+ style: contextPreferenceStyle,
487
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
488
+ type: "checkbox",
489
+ checked: useMaximumContextWindow === true,
490
+ disabled,
491
+ onChange: (event) => {
492
+ onUseMaximumContextWindow(event.currentTarget.checked);
493
+ }
494
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
495
+ style: contextPreferenceCopyStyle,
496
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("useMaximumContextWindow") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
447
497
  style: modelRateStyle,
448
- children: t("contextUpTo", { size: formatTokens(alternative) })
498
+ children: t("useMaximumContextWindowHint")
449
499
  })]
450
500
  })]
451
- }, model.id);
452
- })]
501
+ }) : null,
502
+ known.map((model) => {
503
+ const capacity = model.contextWindow;
504
+ const alternative = model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
505
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
506
+ style: quotaLabelStyle,
507
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
508
+ style: modelOfferStyle,
509
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
510
+ style: { textAlign: "right" },
511
+ children: formatTokens(capacity)
512
+ }), alternative !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
513
+ style: modelRateStyle,
514
+ children: t("contextUpTo", { size: formatTokens(alternative) })
515
+ }) : model.defaultContextWindow !== void 0 && model.defaultContextWindow < capacity ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
516
+ style: modelRateStyle,
517
+ children: t("contextDefault", { size: formatTokens(model.defaultContextWindow) })
518
+ }) : null]
519
+ })]
520
+ }, model.id);
521
+ })
522
+ ]
453
523
  });
454
524
  }
455
525
  /**
@@ -791,6 +861,10 @@ window.__ModuleLoader__.load({
791
861
  const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
792
862
  throw new Error(message);
793
863
  }
864
+ if (action.action === "set-maximum-context-window" && (typeof value !== "object" || value === null || value["state"] !== "updated")) {
865
+ const reason = typeof value === "object" && value !== null && "reason" in value ? String(value["reason"]) : t("requestFailed");
866
+ throw new Error(reason);
867
+ }
794
868
  await refresh(controller.signal);
795
869
  } catch (error) {
796
870
  if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
@@ -923,18 +997,21 @@ window.__ModuleLoader__.load({
923
997
  tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
924
998
  style: tabPanelStyle,
925
999
  children: [
926
- status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1000
+ status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
927
1001
  style: quotaListStyle,
928
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1002
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
929
1003
  style: rowStyle,
930
1004
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
931
1005
  style: quotaTitleStyle,
932
1006
  children: t("creditsHeading")
933
1007
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
934
1008
  style: bodyStyle,
935
- children: t("creditsTotal", { total: formatNumber(status.credits.total) })
1009
+ children: status.credits.unlimited === true ? t("creditsTotalUnlimited") : t("creditsTotal", { total: formatNumber(status.credits.total) })
936
1010
  })]
937
- })
1011
+ }), status.credits.cycleResetTime === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1012
+ style: descriptionStyle,
1013
+ children: t("cycleResetAt", { time: formatCycleReset(status.credits.cycleResetTime) })
1014
+ })]
938
1015
  }),
939
1016
  status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
940
1017
  style: errorStyle,
@@ -954,7 +1031,15 @@ window.__ModuleLoader__.load({
954
1031
  style: tabPanelStyle,
955
1032
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
956
1033
  models: status.models,
957
- t
1034
+ t,
1035
+ disabled: busy,
1036
+ ...status.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: status.useMaximumContextWindow },
1037
+ ...variant.id === AI_CARD_VARIANT.id ? { onUseMaximumContextWindow: (enabled) => {
1038
+ control({
1039
+ action: "set-maximum-context-window",
1040
+ enabled
1041
+ });
1042
+ } } : {}
958
1043
  })
959
1044
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
960
1045
  style: tabPanelStyle,
@@ -963,10 +1048,11 @@ window.__ModuleLoader__.load({
963
1048
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
964
1049
  style: quotaTitleStyle,
965
1050
  children: t("creditsDetailHeading")
966
- }), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
967
- label: account.packageName,
1051
+ }), status.credits.accounts.filter((account) => account.packageName === "enterprise" || account.remain > 0 || account.unlimited === true).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
1052
+ label: account.packageName === "enterprise" ? t("packageEnterprise") : account.packageName,
968
1053
  remain: account.remain,
969
1054
  size: account.size,
1055
+ unlimited: account.unlimited,
970
1056
  t
971
1057
  }, `${account.packageName}-${String(index)}`))]
972
1058
  }), status.models === void 0 || status.models.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1445,6 +1531,10 @@ window.__ModuleLoader__.load({
1445
1531
  tabDetails: "Credit details",
1446
1532
  creditsDetailHeading: "By package",
1447
1533
  creditsTotal: "Total: {total}",
1534
+ creditsTotalUnlimited: "Total: Unlimited",
1535
+ unlimitedQuota: "Unlimited",
1536
+ packageEnterprise: "Enterprise quota",
1537
+ cycleResetAt: "Resets {time}",
1448
1538
  percentRemaining: "{percent}% remaining",
1449
1539
  percentUnknown: "Remaining share unknown",
1450
1540
  exactRemaining: "{remain} / {size} remaining",
@@ -1466,6 +1556,9 @@ window.__ModuleLoader__.load({
1466
1556
  modelsHeading: "Model offers",
1467
1557
  contextHeading: "Context window",
1468
1558
  contextUpTo: "up to {size}",
1559
+ contextDefault: "default {size}",
1560
+ useMaximumContextWindow: "Use the largest declared context window",
1561
+ useMaximumContextWindowHint: "Applies to WorkBuddy AI models that offer a larger window.",
1469
1562
  freeModel: "Free",
1470
1563
  badgeLimitedFree: "Limited-time free",
1471
1564
  badgeNightDiscount: "Night discount",
@@ -1521,6 +1614,10 @@ window.__ModuleLoader__.load({
1521
1614
  tabDetails: "积分详情",
1522
1615
  creditsDetailHeading: "按套餐",
1523
1616
  creditsTotal: "合计:{total}",
1617
+ creditsTotalUnlimited: "合计:不限额",
1618
+ unlimitedQuota: "不限额",
1619
+ packageEnterprise: "企业额度",
1620
+ cycleResetAt: "重置时间:{time}",
1524
1621
  percentRemaining: "剩余 {percent}%",
1525
1622
  percentUnknown: "剩余占比未知",
1526
1623
  exactRemaining: "剩余 {remain} / {size}",
@@ -1542,6 +1639,9 @@ window.__ModuleLoader__.load({
1542
1639
  modelsHeading: "模型优惠",
1543
1640
  contextHeading: "上下文窗口",
1544
1641
  contextUpTo: "最高 {size}",
1642
+ contextDefault: "默认 {size}",
1643
+ useMaximumContextWindow: "使用上游声明的最大上下文窗口",
1644
+ useMaximumContextWindowHint: "仅作用于 WorkBuddy AI 中声明了更大窗口的模型。",
1545
1645
  freeModel: "免费",
1546
1646
  badgeLimitedFree: "限时免费",
1547
1647
  badgeNightDiscount: "夜间折扣",
package/lib/index.d.ts CHANGED
@@ -225,6 +225,8 @@ interface WorkBuddyUpstreamModel {
225
225
  id: string;
226
226
  name: string;
227
227
  contextWindow: number;
228
+ /** The upstream's preferred window before an optional maximum is selected. */
229
+ defaultContextWindow?: number;
228
230
  maxInputTokens?: number;
229
231
  supportedContextWindows?: readonly number[];
230
232
  promotions?: readonly WorkBuddyPromotion[];
@@ -295,11 +297,24 @@ interface WorkBuddyCreditAccount {
295
297
  packageName: string;
296
298
  remain: number;
297
299
  size: number;
300
+ unlimited?: true;
298
301
  }
299
302
  /** Aggregated credit answer for one credential. */
300
303
  interface WorkBuddyCredits {
301
304
  total: number;
302
305
  accounts: readonly WorkBuddyCreditAccount[];
306
+ /**
307
+ * The account's cycle quota is uncapped (`limitNum === -1` on the CN
308
+ * enterprise endpoint).
309
+ *
310
+ * A separate flag rather than a `-1`/`0` sentinel in {@link total}: the two
311
+ * mean opposite things to a reader ("no limit" vs "nothing left"), and the
312
+ * existing negative-clamp in the personal branch would turn a sentinel into
313
+ * a plausible-looking zero. Every renderer must therefore test this flag
314
+ * first and not fall back to `total` when it is set.
315
+ */
316
+ unlimited?: true;
317
+ cycleResetTime?: string;
303
318
  }
304
319
  /** Token refresh answer; fields the upstream omits stay absent. */
305
320
  interface WorkBuddyRefreshOutcome {
@@ -409,8 +424,41 @@ declare class WorkBuddyUpstreamClient {
409
424
  * such rather than as a generic catalog failure.
410
425
  */
411
426
  fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
412
- /** POST the billing endpoint for the aggregated remaining credit. */
427
+ /**
428
+ * POST the billing endpoint for the aggregated remaining credit.
429
+ *
430
+ * Two upstream shapes, chosen by account type:
431
+ *
432
+ * - **CN enterprise** (`regionOf === 'cn'` and `enterpriseId` non-empty) asks
433
+ * `/v2/billing/meter/get-enterprise-user-usage`, which answers with a single
434
+ * cycle quota. The personal endpoint serves these accounts an empty
435
+ * `Accounts` list, which the card then renders as "0 credit" — a wrong
436
+ * number rather than a visible failure (issue #31).
437
+ * - **Everyone else** keeps the personal endpoint unchanged.
438
+ *
439
+ * The region gate is load-bearing: the enterprise endpoint is unverified for
440
+ * the global region, so an international credential that happens to carry an
441
+ * `enterpriseId` must stay on the measured personal path instead of being
442
+ * moved onto an unmeasured one.
443
+ */
413
444
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
445
+ /**
446
+ * CN enterprise credit read: a single cycle quota instead of a package list.
447
+ *
448
+ * Verified against the WorkBuddy desktop app (`app.asar`,
449
+ * `BackendProvider.getEnterpriseUsage` and `CloudAccountRepo.billing`): the
450
+ * body is an empty object and the account identity travels only in the
451
+ * headers. The two official call sites disagree on the field spelling
452
+ * (`limitNum`/`credit` vs `limit_num`/`used_num`), so both are accepted.
453
+ *
454
+ * A body carrying no recognisable quota field is a hard error rather than a
455
+ * zero. Rendering `0` for "we did not understand the answer" is exactly how
456
+ * issue #31 stayed invisible while users saw a plausible wrong number.
457
+ *
458
+ * The error names fields and types only: it reaches the browser, and the
459
+ * response body may describe the account's usage.
460
+ */
461
+ private fetchEnterpriseCredits;
414
462
  /**
415
463
  * One probe request: a real streaming chat call carrying the effort under
416
464
  * test.
@@ -704,6 +752,7 @@ declare const FALLBACK_WORKBUDDY_AI_MODELS: readonly WorkBuddyModelInfo[];
704
752
  declare class WorkBuddyCatalog {
705
753
  private models;
706
754
  private visible;
755
+ private useMaximumContextWindow;
707
756
  constructor(initial?: readonly WorkBuddyModelInfo[]);
708
757
  /** Current entries; empty while the variant has no usable credential. */
709
758
  current(): readonly WorkBuddyModelInfo[];
@@ -716,6 +765,8 @@ declare class WorkBuddyCatalog {
716
765
  * caller can skip an invalidation that would re-render an identical list.
717
766
  */
718
767
  setVisible(visible: boolean): boolean;
768
+ /** Select the largest declared international window where the upstream offers one. */
769
+ setUseMaximumContextWindow(useMaximum: boolean): boolean;
719
770
  /** Models to fall back to when the upstream fetch fails; ignores visibility. */
720
771
  fallback(): readonly WorkBuddyModelInfo[];
721
772
  }
@@ -750,12 +801,12 @@ interface WorkBuddyProbeRecord {
750
801
  *
751
802
  * An effort set is a fact about one account's entitlement as much as about
752
803
  * the model: the same model id can accept different levels under a different
753
- * subscription. Without this a record outlived the account that produced it,
754
- * so signing out and in as someone else inherited the previous account's
755
- * detected levels. Records written before this field existed carry no
756
- * identity and are therefore never reused.
804
+ * subscription. Records are stored under this identity and only ever served
805
+ * back to it, so one account never inherits another's detected levels — and
806
+ * because the store nests by this identity, switching back finds this
807
+ * account's own records intact rather than re-probing from scratch.
757
808
  */
758
- account?: string;
809
+ account: string;
759
810
  }
760
811
  /**
761
812
  * Plugin-owned probe record path inside the Harness home.
@@ -788,8 +839,9 @@ interface WorkBuddyProbeStoreOptions {
788
839
  now?: () => number;
789
840
  }
790
841
  /**
791
- * The plugin's probe records: read once, written atomically, never trusted
792
- * across a fingerprint change or past the TTL.
842
+ * The plugin's probe records: read once, written atomically, keyed by the
843
+ * account that produced each observation, and never trusted across a
844
+ * fingerprint change or past the TTL.
793
845
  */
794
846
  declare class WorkBuddyProbeStore {
795
847
  private readonly path;
@@ -802,24 +854,25 @@ declare class WorkBuddyProbeStore {
802
854
  filePath(): string;
803
855
  private load;
804
856
  /**
805
- * The usable record for a model, or `undefined` when there is none, it is
806
- * expired, it was taken against a different catalog row, or it belongs to a
807
- * different account.
857
+ * The usable record for one account and model, or `undefined` when there is
858
+ * none, it is expired, it was taken against a different catalog row, or it
859
+ * belongs to a different account.
808
860
  *
809
861
  * @param account - the account in effect, as `uid:enterpriseId`. Records are
810
862
  * only returned for the account that produced them.
811
863
  */
812
864
  get(modelId: string, fingerprint: string, account: string): WorkBuddyProbeRecord | undefined;
813
865
  /**
814
- * Store one observation. Only a decisive answer (`validating` /
815
- * `non-validating`) replaces an existing decisive record: a transient
816
- * `unknown` must not erase knowledge the user already paid for.
866
+ * Store one observation under the account stamped on it. Only a decisive
867
+ * answer (`validating` / `non-validating`) replaces an existing decisive
868
+ * record *of the same account*: a transient `unknown` must not erase
869
+ * knowledge the user already paid for.
817
870
  */
818
871
  set(modelId: string, record: WorkBuddyProbeRecord): void;
819
- /** Drop every record; used by the card's explicit "clear" action. */
872
+ /** Drop every record of every account; used by the card's explicit "clear" action. */
820
873
  clear(): void;
821
- /** Every record currently held, for status display. */
822
- all(): Readonly<Record<string, WorkBuddyProbeRecord>>;
874
+ /** Every record currently held, grouped by account, for status display. */
875
+ all(): Readonly<Record<string, Readonly<Record<string, WorkBuddyProbeRecord>>>>;
823
876
  /** Build a record stamped with this store's clock, version, and account. */
824
877
  record(fingerprint: string, validation: WorkBuddyProbeValidation, efforts: readonly WorkBuddyEffort[], account: string): WorkBuddyProbeRecord;
825
878
  /**
@@ -981,8 +1034,8 @@ interface WorkBuddyProbeServiceOptions {
981
1034
  * Records are read and written against this identity, and it is re-checked
982
1035
  * after the sweep finishes: an observation produced under account A must not
983
1036
  * be stored once account B is in effect, however long the probe took. The
984
- * caller's `clear()` on an account switch is not sufficient on its own,
985
- * because an in-flight probe completes *after* that clear.
1037
+ * store's per-account keying alone cannot catch that, because an in-flight
1038
+ * probe completes *after* the switch has already happened.
986
1039
  */
987
1040
  account: () => string | undefined;
988
1041
  sentinel?: SentinelFactory;
@@ -1126,6 +1179,8 @@ interface Config {
1126
1179
  * until the user explicitly agrees.
1127
1180
  */
1128
1181
  probeConsent?: boolean;
1182
+ /** Use the largest context window the international catalog explicitly offers. */
1183
+ useMaximumContextWindow?: boolean;
1129
1184
  }
1130
1185
  declare const Config: z<Config>;
1131
1186
  /**
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as parseModelCatalog, B as fallbackChatIdentity, C as desktopAuthCandidatesFor, D as classifyUpstreamError, E as WorkBuddyUpstreamClient, F as probeModel, G as appUserAgent, H as resolveChatIdentity, I as randomSentinel, J as resolveAppVersion, K as installedAppVersion, L as CN_APP_VERSION_FILENAME, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as FALLBACK_CN_APP_VERSION, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as validCliVersion, V as readCliVersion, W as WORKBUDDY_APP_VERSION_FILENAME, Y as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as prepareChatBody, k as normalizeCredits, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as readBundleVersion, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as chatUserAgent } from "./variants-CExA7lJt.js";
1
+ import { A as parseModelCatalog, B as fallbackChatIdentity, C as desktopAuthCandidatesFor, D as classifyUpstreamError, E as WorkBuddyUpstreamClient, F as probeModel, G as appUserAgent, H as resolveChatIdentity, I as randomSentinel, J as resolveAppVersion, K as installedAppVersion, L as CN_APP_VERSION_FILENAME, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as FALLBACK_CN_APP_VERSION, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as validCliVersion, V as readCliVersion, W as WORKBUDDY_APP_VERSION_FILENAME, Y as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as prepareChatBody, k as normalizeCredits, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as readBundleVersion, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as chatUserAgent } from "./variants-XUStsapF.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
@@ -656,8 +656,17 @@ function createWorkBuddyShim(options) {
656
656
  */
657
657
  /** Basename of the probe record inside the Harness home. */
658
658
  const WORKBUDDY_PROBE_FILENAME = ".workbuddy-probe.json";
659
- /** On-disk format this reader accepts; other versions are discarded. */
660
- const PROBE_FORMAT_VERSION = 1;
659
+ /**
660
+ * On-disk format this reader accepts; other versions are discarded.
661
+ *
662
+ * Version 2 nested the records under the account that produced them
663
+ * (`records[account][modelId]`), so two accounts no longer overwrite each
664
+ * other's observations for the same model. Version 1 files (flat, one record
665
+ * per model) are deliberately not migrated: they read as empty and the
666
+ * affected models are re-probed on demand, which keeps the reader free of
667
+ * half-understood compatibility paths.
668
+ */
669
+ const PROBE_FORMAT_VERSION = 2;
661
670
  /**
662
671
  * How long an observation stays usable. Conservative on purpose: the plan's
663
672
  * whole argument is that upstream metadata moves fast, so a result that has
@@ -718,13 +727,15 @@ function isRecord(value) {
718
727
  if (typeof wrapped["fingerprint"] !== "string") return false;
719
728
  if (typeof wrapped["probedAtMs"] !== "number" || !Number.isFinite(wrapped["probedAtMs"])) return false;
720
729
  if (typeof wrapped["pluginVersion"] !== "string") return false;
730
+ if (typeof wrapped["account"] !== "string" || wrapped["account"] === "") return false;
721
731
  const efforts = wrapped["efforts"];
722
732
  if (!Array.isArray(efforts) || efforts.some((effort) => typeof effort !== "string")) return false;
723
733
  return true;
724
734
  }
725
735
  /**
726
- * The plugin's probe records: read once, written atomically, never trusted
727
- * across a fingerprint change or past the TTL.
736
+ * The plugin's probe records: read once, written atomically, keyed by the
737
+ * account that produced each observation, and never trusted across a
738
+ * fingerprint change or past the TTL.
728
739
  */
729
740
  var WorkBuddyProbeStore = class {
730
741
  path;
@@ -750,21 +761,26 @@ var WorkBuddyProbeStore = class {
750
761
  if (this.records === void 0) {
751
762
  const document = readDocument(this.path);
752
763
  const records = {};
753
- for (const [id, record] of Object.entries(document?.records ?? {})) if (isRecord(record)) records[id] = record;
764
+ for (const [account, bucket] of Object.entries(document?.records ?? {})) {
765
+ if (typeof bucket !== "object" || bucket === null || Array.isArray(bucket)) continue;
766
+ const parsed = {};
767
+ for (const [modelId, record] of Object.entries(bucket)) if (isRecord(record)) parsed[modelId] = record;
768
+ records[account] = parsed;
769
+ }
754
770
  this.records = records;
755
771
  }
756
772
  return this.records;
757
773
  }
758
774
  /**
759
- * The usable record for a model, or `undefined` when there is none, it is
760
- * expired, it was taken against a different catalog row, or it belongs to a
761
- * different account.
775
+ * The usable record for one account and model, or `undefined` when there is
776
+ * none, it is expired, it was taken against a different catalog row, or it
777
+ * belongs to a different account.
762
778
  *
763
779
  * @param account - the account in effect, as `uid:enterpriseId`. Records are
764
780
  * only returned for the account that produced them.
765
781
  */
766
782
  get(modelId, fingerprint, account) {
767
- const record = this.load()[modelId];
783
+ const record = this.load()[account]?.[modelId];
768
784
  if (record === void 0) return void 0;
769
785
  if (record.fingerprint !== fingerprint) return void 0;
770
786
  if (record.account !== account) return void 0;
@@ -772,25 +788,28 @@ var WorkBuddyProbeStore = class {
772
788
  return record;
773
789
  }
774
790
  /**
775
- * Store one observation. Only a decisive answer (`validating` /
776
- * `non-validating`) replaces an existing decisive record: a transient
777
- * `unknown` must not erase knowledge the user already paid for.
791
+ * Store one observation under the account stamped on it. Only a decisive
792
+ * answer (`validating` / `non-validating`) replaces an existing decisive
793
+ * record *of the same account*: a transient `unknown` must not erase
794
+ * knowledge the user already paid for.
778
795
  */
779
796
  set(modelId, record) {
780
797
  const records = this.load();
781
- const existing = records[modelId];
798
+ const bucket = records[record.account] ?? (records[record.account] = {});
799
+ const existing = bucket[modelId];
782
800
  if (record.validation === "unknown" && existing !== void 0 && existing.fingerprint === record.fingerprint && existing.validation !== "unknown") return;
783
- records[modelId] = record;
801
+ bucket[modelId] = record;
784
802
  this.persist();
785
803
  }
786
- /** Drop every record; used by the card's explicit "clear" action. */
804
+ /** Drop every record of every account; used by the card's explicit "clear" action. */
787
805
  clear() {
788
806
  this.records = {};
789
807
  this.persist();
790
808
  }
791
- /** Every record currently held, for status display. */
809
+ /** Every record currently held, grouped by account, for status display. */
792
810
  all() {
793
- return { ...this.load() };
811
+ const records = this.load();
812
+ return Object.fromEntries(Object.entries(records).map(([account, bucket]) => [account, { ...bucket }]));
794
813
  }
795
814
  /** Build a record stamped with this store's clock, version, and account. */
796
815
  record(fingerprint, validation, efforts, account) {
@@ -1001,6 +1020,7 @@ async function workBuddyWebStatus(deps) {
1001
1020
  const rate = normalizeCredits(model.billing?.credits);
1002
1021
  const supported = model.supportedContextWindows ?? [];
1003
1022
  const maxContextWindow = supported.length > 0 ? Math.max(...supported) : void 0;
1023
+ const defaultContextWindow = model.defaultContextWindow ?? model.contextWindow;
1004
1024
  return {
1005
1025
  id: model.id,
1006
1026
  name: model.name,
@@ -1009,7 +1029,8 @@ async function workBuddyWebStatus(deps) {
1009
1029
  ...rate === void 0 ? {} : { credits: rate },
1010
1030
  ...model.billing?.rateUnknown === true ? { rateUnknown: true } : {},
1011
1031
  ...typeof model.contextWindow === "number" && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {},
1012
- ...maxContextWindow === void 0 || maxContextWindow === model.contextWindow ? {} : { maxContextWindow },
1032
+ ...typeof defaultContextWindow === "number" && defaultContextWindow > 0 && defaultContextWindow < model.contextWindow ? { defaultContextWindow } : {},
1033
+ ...maxContextWindow === void 0 || maxContextWindow <= defaultContextWindow ? {} : { maxContextWindow },
1013
1034
  ...typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}
1014
1035
  };
1015
1036
  });
@@ -1025,7 +1046,8 @@ async function workBuddyWebStatus(deps) {
1025
1046
  const probed = deps.probe === void 0 ? statusWithModels : {
1026
1047
  ...statusWithModels,
1027
1048
  probe: deps.probe(),
1028
- ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey }
1049
+ ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey },
1050
+ ...deps.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: deps.useMaximumContextWindow() }
1029
1051
  };
1030
1052
  try {
1031
1053
  const credential = await deps.store.current();
@@ -1145,6 +1167,10 @@ function parseAction(text) {
1145
1167
  const action = wrapped["action"];
1146
1168
  if (action === "clear") return { action: "clear" };
1147
1169
  if (action === "refresh") return { action: "refresh" };
1170
+ if (action === "set-maximum-context-window") return typeof wrapped["enabled"] === "boolean" ? {
1171
+ action: "set-maximum-context-window",
1172
+ enabled: wrapped["enabled"]
1173
+ } : void 0;
1148
1174
  if (action === "probe") {
1149
1175
  const model = wrapped["model"];
1150
1176
  if (typeof model !== "string" || model.trim() === "") return void 0;
@@ -1196,6 +1222,14 @@ function workBuddyProbeHandler(deps, key) {
1196
1222
  json(res, 200, await deps.refresh());
1197
1223
  return;
1198
1224
  }
1225
+ if (action.action === "set-maximum-context-window") {
1226
+ if (deps.setMaximumContextWindow === void 0) {
1227
+ json(res, 404, { error: "context-window-setting-not-supported" });
1228
+ return;
1229
+ }
1230
+ json(res, 200, await deps.setMaximumContextWindow(action.enabled === true));
1231
+ return;
1232
+ }
1199
1233
  json(res, 200, await deps.probe(action.model));
1200
1234
  } catch (error) {
1201
1235
  json(res, 500, { error: error instanceof Error ? error.message : String(error) });
@@ -1292,10 +1326,12 @@ const AUTH_FILE_FIELD = z.string().description("WorkBuddy desktop auth file (def
1292
1326
  const AUTH_FILE_AI_FIELD = z.string().description("WorkBuddy AI desktop auth file (defaults to the app's own location)");
1293
1327
  /** Probe authorization (shared by the plugin schema and the CN section). */
1294
1328
  const PROBE_CONSENT_FIELD = z.boolean().default(false).description("Authorize reasoning-effort probes (each probe sends real requests that may consume credit)");
1329
+ const MAXIMUM_CONTEXT_WINDOW_FIELD = z.boolean().default(true).description("Use the largest context window declared by WorkBuddy AI when alternatives are available (on by default)");
1295
1330
  const Config = z.object({
1296
1331
  authFile: AUTH_FILE_FIELD,
1297
1332
  authFileAI: AUTH_FILE_AI_FIELD,
1298
- probeConsent: PROBE_CONSENT_FIELD
1333
+ probeConsent: PROBE_CONSENT_FIELD,
1334
+ useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1299
1335
  });
1300
1336
  /**
1301
1337
  * The CN card's settings section: only the fields that card edits.
@@ -1310,8 +1346,11 @@ const CN_SECTION = z.object({
1310
1346
  authFile: AUTH_FILE_FIELD,
1311
1347
  probeConsent: PROBE_CONSENT_FIELD
1312
1348
  });
1313
- /** The international card's settings section: only its own auth-file path. */
1314
- const AI_SECTION = z.object({ authFileAI: AUTH_FILE_AI_FIELD });
1349
+ /** The international card's settings section and its context-window preference. */
1350
+ const AI_SECTION = z.object({
1351
+ authFileAI: AUTH_FILE_AI_FIELD,
1352
+ useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1353
+ });
1315
1354
  /** Stable identity key used by credentials, probe records, and catalog entries. */
1316
1355
  function credentialIdentity(credential) {
1317
1356
  return `${credential.uid}:${credential.enterpriseId ?? ""}`;
@@ -1345,6 +1384,7 @@ function createVariantRuntime(config, variant, current, identityOf) {
1345
1384
  });
1346
1385
  const fallback = fallbackFor(variant);
1347
1386
  const catalog = new WorkBuddyCatalog(fallback);
1387
+ if (variant.id !== CN_VARIANT.id) catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
1348
1388
  catalog.setVisible(false);
1349
1389
  const probeStore = new WorkBuddyProbeStore({
1350
1390
  pluginVersion: WORKBUDDY_CONNECT_VERSION,
@@ -1520,6 +1560,7 @@ function apply(ctx, config) {
1520
1560
  const lastIdentities = /* @__PURE__ */ new Map();
1521
1561
  const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config, variant, () => current(), (id) => lastIdentities.get(id)));
1522
1562
  const probeKey = createProbeKey();
1563
+ let setMaximumContextWindow;
1523
1564
  /**
1524
1565
  * Point a variant at an account identity, invalidating whatever the previous
1525
1566
  * one left behind.
@@ -1530,13 +1571,12 @@ function apply(ctx, config) {
1530
1571
  * transition bumps {@link VariantRuntime.catalogGeneration}, which is what
1531
1572
  * makes an in-flight request from before the change refuse to write back.
1532
1573
  *
1533
- * Probe observations are dropped whenever the account actually changes —
1534
- * including sign-out, and including the "signed out, then in as someone else"
1535
- * sequence that used to look like a first sighting and let the new account
1536
- * inherit the old one's detected levels. They are deliberately NOT cleared on
1537
- * a first sign-in: no previous account's data could leak there, and clearing
1538
- * would delete records this very account owns (written before a restart, or
1539
- * seeded while all of this is running).
1574
+ * Probe observations are kept across an account change: the store nests them
1575
+ * per account, so the departing account's records simply stop being served
1576
+ * (every read is account-scoped) and are found intact if that account
1577
+ * returns. The "signed out, then in as someone else" sequence that used to
1578
+ * look like a first sighting is still safe — a record only ever answers for
1579
+ * the account stamped on it, so the new account inherits nothing.
1540
1580
  *
1541
1581
  * @param identity - the account now in effect, or `undefined` when signed out.
1542
1582
  */
@@ -1550,10 +1590,7 @@ function apply(ctx, config) {
1550
1590
  runtime.catalogGeneration += 1;
1551
1591
  runtime.inflightFetch?.controller.abort();
1552
1592
  runtime.inflightFetch = void 0;
1553
- if (hadCredential && known !== identity) {
1554
- runtime.probeStore.clear();
1555
- runtime.invalidate();
1556
- }
1593
+ if (hadCredential && known !== identity) runtime.invalidate();
1557
1594
  if (identity === void 0) {
1558
1595
  if (known !== void 0) runtime.savedCatalogs.delete(known);
1559
1596
  runtime.catalog.set(runtime.fallback);
@@ -1586,7 +1623,8 @@ function apply(ctx, config) {
1586
1623
  models: () => runtime.catalog.current(),
1587
1624
  catalog: () => catalogSection(runtime),
1588
1625
  probe: () => probeSection(runtime, current().probeConsent === true),
1589
- probeKey
1626
+ probeKey,
1627
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { useMaximumContextWindow: () => current().useMaximumContextWindow === true }
1590
1628
  });
1591
1629
  registerWorkBuddyProbeRoute(webCtx, {
1592
1630
  path: runtime.variant.probePath,
@@ -1627,7 +1665,14 @@ function apply(ctx, config) {
1627
1665
  state: "failed",
1628
1666
  reason: runtime.catalogError
1629
1667
  };
1630
- }
1668
+ },
1669
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { setMaximumContextWindow: async (enabled) => {
1670
+ if (setMaximumContextWindow === void 0) return {
1671
+ state: "failed",
1672
+ reason: "settings are unavailable"
1673
+ };
1674
+ return setMaximumContextWindow(enabled);
1675
+ } }
1631
1676
  }, probeKey);
1632
1677
  }
1633
1678
  });
@@ -1641,10 +1686,16 @@ function apply(ctx, config) {
1641
1686
  const merged = () => ({
1642
1687
  ...sources.cn().authFile === void 0 ? {} : { authFile: sources.cn().authFile },
1643
1688
  ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
1644
- ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI }
1689
+ ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI },
1690
+ ...sources.ai().useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: sources.ai().useMaximumContextWindow }
1645
1691
  });
1692
+ const applyMaximumContextWindow = (next) => {
1693
+ const runtime = runtimes.find((candidate) => candidate.variant.id !== CN_VARIANT.id);
1694
+ if (runtime?.catalog.setUseMaximumContextWindow(next.useMaximumContextWindow === true)) runtime.invalidate();
1695
+ };
1646
1696
  const repointStores = () => {
1647
1697
  const next = merged();
1698
+ applyMaximumContextWindow(next);
1648
1699
  for (const runtime of runtimes) runtime.store.setDesktopPath(configuredAuthFile(next, runtime.variant));
1649
1700
  };
1650
1701
  settingsCtx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, CN_SECTION, config, {
@@ -1661,6 +1712,10 @@ function apply(ctx, config) {
1661
1712
  },
1662
1713
  onChange: repointStores
1663
1714
  });
1715
+ setMaximumContextWindow = async (enabled) => {
1716
+ await settingsCtx.settings.update(WORKBUDDY_AI_SETTINGS_NS, { useMaximumContextWindow: enabled });
1717
+ return { state: "updated" };
1718
+ };
1664
1719
  });
1665
1720
  ctx.effect(() => () => {
1666
1721
  stopped = true;
@@ -485,6 +485,33 @@ async function probeModel(options) {
485
485
  const CN_CHAT_BASE = "https://copilot.tencent.com";
486
486
  const CN_BILLING_BASE = "https://www.codebuddy.cn";
487
487
  const GLOBAL_BASE = "https://www.workbuddy.ai";
488
+ /**
489
+ * Display name for the single synthetic row the enterprise endpoint produces.
490
+ *
491
+ * The endpoint reports one cycle quota, not the personal endpoint's list of
492
+ * named packages, so the card's "by package" table has exactly one row.
493
+ */
494
+ const enterprisePackageName = "enterprise";
495
+ /**
496
+ * Field names and value types of a response document, for diagnostics.
497
+ *
498
+ * Names and `typeof` only. This string ends up in the status route and then in
499
+ * the browser, and the response describes the account's own usage; the values
500
+ * themselves must never travel. Only the document and its `data` member are
501
+ * described, so the output stays small.
502
+ */
503
+ function describeShape(document) {
504
+ if (typeof document !== "object" || document === null || Array.isArray(document)) return typeof document;
505
+ const record = document;
506
+ const at = (source) => {
507
+ const keys = Object.keys(source).slice(0, 24);
508
+ return keys.length === 0 ? "(empty)" : keys.map((key) => `${key}:${typeof source[key]}`).join(", ");
509
+ };
510
+ const top = `top-level { ${at(record)} }`;
511
+ const data = record["data"];
512
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return top;
513
+ return `${top}; data { ${at(data)} }`;
514
+ }
488
515
  /** Shared CLI-form User-Agent for refresh and the CN catalog; chat and probe present the desktop identity (client-identity.ts). */
489
516
  const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
490
517
  const JSON_TIMEOUT_MS = 3e4;
@@ -882,8 +909,25 @@ var WorkBuddyUpstreamClient = class {
882
909
  };
883
910
  return models;
884
911
  }
885
- /** POST the billing endpoint for the aggregated remaining credit. */
912
+ /**
913
+ * POST the billing endpoint for the aggregated remaining credit.
914
+ *
915
+ * Two upstream shapes, chosen by account type:
916
+ *
917
+ * - **CN enterprise** (`regionOf === 'cn'` and `enterpriseId` non-empty) asks
918
+ * `/v2/billing/meter/get-enterprise-user-usage`, which answers with a single
919
+ * cycle quota. The personal endpoint serves these accounts an empty
920
+ * `Accounts` list, which the card then renders as "0 credit" — a wrong
921
+ * number rather than a visible failure (issue #31).
922
+ * - **Everyone else** keeps the personal endpoint unchanged.
923
+ *
924
+ * The region gate is load-bearing: the enterprise endpoint is unverified for
925
+ * the global region, so an international credential that happens to carry an
926
+ * `enterpriseId` must stay on the measured personal path instead of being
927
+ * moved onto an unmeasured one.
928
+ */
886
929
  async fetchCredits(credential) {
930
+ if (regionOf(credential.domain) === "cn" && credential.enterpriseId !== void 0 && credential.enterpriseId !== "") return await this.fetchEnterpriseCredits(credential);
887
931
  const now = /* @__PURE__ */ new Date();
888
932
  const format = (date) => [
889
933
  date.getFullYear().toString().padStart(4, "0"),
@@ -941,6 +985,75 @@ var WorkBuddyUpstreamClient = class {
941
985
  };
942
986
  }
943
987
  /**
988
+ * CN enterprise credit read: a single cycle quota instead of a package list.
989
+ *
990
+ * Verified against the WorkBuddy desktop app (`app.asar`,
991
+ * `BackendProvider.getEnterpriseUsage` and `CloudAccountRepo.billing`): the
992
+ * body is an empty object and the account identity travels only in the
993
+ * headers. The two official call sites disagree on the field spelling
994
+ * (`limitNum`/`credit` vs `limit_num`/`used_num`), so both are accepted.
995
+ *
996
+ * A body carrying no recognisable quota field is a hard error rather than a
997
+ * zero. Rendering `0` for "we did not understand the answer" is exactly how
998
+ * issue #31 stayed invisible while users saw a plausible wrong number.
999
+ *
1000
+ * The error names fields and types only: it reaches the browser, and the
1001
+ * response body may describe the account's usage.
1002
+ */
1003
+ async fetchEnterpriseCredits(credential) {
1004
+ const response = await fetch(`${CN_BILLING_BASE}/v2/billing/meter/get-enterprise-user-usage`, {
1005
+ method: "POST",
1006
+ headers: billingHeaders(credential),
1007
+ body: JSON.stringify({}),
1008
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
1009
+ });
1010
+ const envelope = await readEnvelope(response);
1011
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
1012
+ const sources = [];
1013
+ for (const candidate of [envelope.data, envelope.document]) {
1014
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) continue;
1015
+ const record = candidate;
1016
+ if (typeof record["data"] === "object" && record["data"] !== null && !Array.isArray(record["data"])) sources.push(record["data"]);
1017
+ sources.push(record);
1018
+ }
1019
+ const numberAt = (source, key) => typeof source[key] === "number" ? source[key] : void 0;
1020
+ let limit;
1021
+ let used;
1022
+ let resetTime;
1023
+ for (const source of sources) {
1024
+ const candidate = numberAt(source, "limitNum") ?? numberAt(source, "limit_num");
1025
+ if (candidate === void 0) continue;
1026
+ limit = candidate;
1027
+ used = numberAt(source, "credit") ?? numberAt(source, "used_num");
1028
+ if (typeof source["cycleResetTime"] === "string" && source["cycleResetTime"] !== "") resetTime = source["cycleResetTime"];
1029
+ break;
1030
+ }
1031
+ if (limit === void 0) throw new Error(`workbuddy enterprise billing response carried no recognised quota field (expected limitNum/limit_num + credit/used_num; received ${describeShape(envelope.document)})`);
1032
+ if (limit === -1) return {
1033
+ total: 0,
1034
+ accounts: [{
1035
+ packageName: enterprisePackageName,
1036
+ remain: 0,
1037
+ size: 0,
1038
+ unlimited: true
1039
+ }],
1040
+ unlimited: true,
1041
+ ...resetTime === void 0 ? {} : { cycleResetTime: resetTime }
1042
+ };
1043
+ if (used === void 0) throw new Error(`workbuddy enterprise billing response carried a quota limit but no recognised usage field (expected credit/used_num alongside limitNum/limit_num; received ${describeShape(envelope.document)})`);
1044
+ let remain = limit - used;
1045
+ if (remain < 0) remain = 0;
1046
+ return {
1047
+ total: remain,
1048
+ accounts: [{
1049
+ packageName: enterprisePackageName,
1050
+ remain,
1051
+ size: limit
1052
+ }],
1053
+ ...resetTime === void 0 ? {} : { cycleResetTime: resetTime }
1054
+ };
1055
+ }
1056
+ /**
944
1057
  * One probe request: a real streaming chat call carrying the effort under
945
1058
  * test.
946
1059
  *
@@ -1092,6 +1205,7 @@ function parseModelCatalog(data, international = false) {
1092
1205
  name: typeof wrapped["name"] === "string" && wrapped["name"] !== "" ? wrapped["name"] : id,
1093
1206
  contextWindow: international && isObject(wrapped["contextWindow"]) && positive(wrapped["contextWindow"]["defaultLength"]) ? wrapped["contextWindow"]["defaultLength"] : input,
1094
1207
  ...international ? {
1208
+ ...isObject(wrapped["contextWindow"]) && positive(wrapped["contextWindow"]["defaultLength"]) ? { defaultContextWindow: wrapped["contextWindow"]["defaultLength"] } : {},
1095
1209
  maxInputTokens: input,
1096
1210
  supportedContextWindows: isObject(wrapped["contextWindow"]) && Array.isArray(wrapped["contextWindow"]["supportedLengths"]) ? wrapped["contextWindow"]["supportedLengths"].filter(positive) : [],
1097
1211
  promotions: parsePromotions(data["modelPromotions"], id)
@@ -1971,6 +2085,8 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
1971
2085
  id: "hy4-preview-f",
1972
2086
  name: "Hy4 preview",
1973
2087
  contextWindow: 3e5,
2088
+ defaultContextWindow: 3e5,
2089
+ supportedContextWindows: [3e5, 1e6],
1974
2090
  maxTokens: 64e3,
1975
2091
  supportsImages: true,
1976
2092
  reasoning: {
@@ -2007,6 +2123,8 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
2007
2123
  id: "deepseek-v4.1-flash",
2008
2124
  name: "Deepseek-V4.1-Flash",
2009
2125
  contextWindow: 3e5,
2126
+ defaultContextWindow: 3e5,
2127
+ supportedContextWindows: [3e5, 1e6],
2010
2128
  maxTokens: 128e3,
2011
2129
  supportsImages: true,
2012
2130
  reasoning: {
@@ -2024,6 +2142,8 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
2024
2142
  id: "gpt-6-astra",
2025
2143
  name: "GPT-6-Astra",
2026
2144
  contextWindow: 4e5,
2145
+ defaultContextWindow: 4e5,
2146
+ supportedContextWindows: [4e5, 1e6],
2027
2147
  maxTokens: 128e3,
2028
2148
  supportsImages: true,
2029
2149
  reasoning: {
@@ -2287,13 +2407,22 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
2287
2407
  var WorkBuddyCatalog = class {
2288
2408
  models;
2289
2409
  visible = true;
2410
+ useMaximumContextWindow = false;
2290
2411
  constructor(initial = FALLBACK_WORKBUDDY_MODELS) {
2291
2412
  this.models = initial;
2292
2413
  }
2293
2414
  /** Current entries; empty while the variant has no usable credential. */
2294
2415
  current() {
2295
2416
  if (!this.visible) return [];
2296
- return this.models.map((model) => modelWithCurrentPromotion(model));
2417
+ return this.models.map((model) => {
2418
+ const current = modelWithCurrentPromotion(model);
2419
+ const maximum = current.supportedContextWindows === void 0 ? void 0 : Math.max(...current.supportedContextWindows);
2420
+ return this.useMaximumContextWindow && maximum !== void 0 && maximum > current.contextWindow ? {
2421
+ ...current,
2422
+ defaultContextWindow: current.defaultContextWindow ?? current.contextWindow,
2423
+ contextWindow: maximum
2424
+ } : current;
2425
+ });
2297
2426
  }
2298
2427
  /** Replace the list; callers invalidate their adapter snapshot after this. */
2299
2428
  set(models) {
@@ -2312,6 +2441,12 @@ var WorkBuddyCatalog = class {
2312
2441
  this.visible = visible;
2313
2442
  return true;
2314
2443
  }
2444
+ /** Select the largest declared international window where the upstream offers one. */
2445
+ setUseMaximumContextWindow(useMaximum) {
2446
+ if (this.useMaximumContextWindow === useMaximum) return false;
2447
+ this.useMaximumContextWindow = useMaximum;
2448
+ return true;
2449
+ }
2315
2450
  /** Models to fall back to when the upstream fetch fails; ignores visibility. */
2316
2451
  fallback() {
2317
2452
  return this.models;
@@ -2344,7 +2479,7 @@ const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
2344
2479
  const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
2345
2480
  //#endregion
2346
2481
  //#region src/version.ts
2347
- const WORKBUDDY_CONNECT_VERSION = "0.5.2";
2482
+ const WORKBUDDY_CONNECT_VERSION = "0.5.4";
2348
2483
  //#endregion
2349
2484
  //#region src/host-heartbeat.ts
2350
2485
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-connect",
3
3
  "displayName": "DSH WorkBuddy Connect",
4
4
  "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.5.2",
5
+ "version": "0.5.4",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",