dsh-workbuddy-connect 0.5.2 → 0.5.3

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-CdmzOPIT.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
  }
@@ -1126,6 +1177,8 @@ interface Config {
1126
1177
  * until the user explicitly agrees.
1127
1178
  */
1128
1179
  probeConsent?: boolean;
1180
+ /** Use the largest context window the international catalog explicitly offers. */
1181
+ useMaximumContextWindow?: boolean;
1129
1182
  }
1130
1183
  declare const Config: z<Config>;
1131
1184
  /**
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-CdmzOPIT.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";
@@ -1001,6 +1001,7 @@ async function workBuddyWebStatus(deps) {
1001
1001
  const rate = normalizeCredits(model.billing?.credits);
1002
1002
  const supported = model.supportedContextWindows ?? [];
1003
1003
  const maxContextWindow = supported.length > 0 ? Math.max(...supported) : void 0;
1004
+ const defaultContextWindow = model.defaultContextWindow ?? model.contextWindow;
1004
1005
  return {
1005
1006
  id: model.id,
1006
1007
  name: model.name,
@@ -1009,7 +1010,8 @@ async function workBuddyWebStatus(deps) {
1009
1010
  ...rate === void 0 ? {} : { credits: rate },
1010
1011
  ...model.billing?.rateUnknown === true ? { rateUnknown: true } : {},
1011
1012
  ...typeof model.contextWindow === "number" && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {},
1012
- ...maxContextWindow === void 0 || maxContextWindow === model.contextWindow ? {} : { maxContextWindow },
1013
+ ...typeof defaultContextWindow === "number" && defaultContextWindow > 0 && defaultContextWindow < model.contextWindow ? { defaultContextWindow } : {},
1014
+ ...maxContextWindow === void 0 || maxContextWindow <= defaultContextWindow ? {} : { maxContextWindow },
1013
1015
  ...typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}
1014
1016
  };
1015
1017
  });
@@ -1025,7 +1027,8 @@ async function workBuddyWebStatus(deps) {
1025
1027
  const probed = deps.probe === void 0 ? statusWithModels : {
1026
1028
  ...statusWithModels,
1027
1029
  probe: deps.probe(),
1028
- ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey }
1030
+ ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey },
1031
+ ...deps.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: deps.useMaximumContextWindow() }
1029
1032
  };
1030
1033
  try {
1031
1034
  const credential = await deps.store.current();
@@ -1145,6 +1148,10 @@ function parseAction(text) {
1145
1148
  const action = wrapped["action"];
1146
1149
  if (action === "clear") return { action: "clear" };
1147
1150
  if (action === "refresh") return { action: "refresh" };
1151
+ if (action === "set-maximum-context-window") return typeof wrapped["enabled"] === "boolean" ? {
1152
+ action: "set-maximum-context-window",
1153
+ enabled: wrapped["enabled"]
1154
+ } : void 0;
1148
1155
  if (action === "probe") {
1149
1156
  const model = wrapped["model"];
1150
1157
  if (typeof model !== "string" || model.trim() === "") return void 0;
@@ -1196,6 +1203,14 @@ function workBuddyProbeHandler(deps, key) {
1196
1203
  json(res, 200, await deps.refresh());
1197
1204
  return;
1198
1205
  }
1206
+ if (action.action === "set-maximum-context-window") {
1207
+ if (deps.setMaximumContextWindow === void 0) {
1208
+ json(res, 404, { error: "context-window-setting-not-supported" });
1209
+ return;
1210
+ }
1211
+ json(res, 200, await deps.setMaximumContextWindow(action.enabled === true));
1212
+ return;
1213
+ }
1199
1214
  json(res, 200, await deps.probe(action.model));
1200
1215
  } catch (error) {
1201
1216
  json(res, 500, { error: error instanceof Error ? error.message : String(error) });
@@ -1292,10 +1307,12 @@ const AUTH_FILE_FIELD = z.string().description("WorkBuddy desktop auth file (def
1292
1307
  const AUTH_FILE_AI_FIELD = z.string().description("WorkBuddy AI desktop auth file (defaults to the app's own location)");
1293
1308
  /** Probe authorization (shared by the plugin schema and the CN section). */
1294
1309
  const PROBE_CONSENT_FIELD = z.boolean().default(false).description("Authorize reasoning-effort probes (each probe sends real requests that may consume credit)");
1310
+ 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
1311
  const Config = z.object({
1296
1312
  authFile: AUTH_FILE_FIELD,
1297
1313
  authFileAI: AUTH_FILE_AI_FIELD,
1298
- probeConsent: PROBE_CONSENT_FIELD
1314
+ probeConsent: PROBE_CONSENT_FIELD,
1315
+ useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1299
1316
  });
1300
1317
  /**
1301
1318
  * The CN card's settings section: only the fields that card edits.
@@ -1310,8 +1327,11 @@ const CN_SECTION = z.object({
1310
1327
  authFile: AUTH_FILE_FIELD,
1311
1328
  probeConsent: PROBE_CONSENT_FIELD
1312
1329
  });
1313
- /** The international card's settings section: only its own auth-file path. */
1314
- const AI_SECTION = z.object({ authFileAI: AUTH_FILE_AI_FIELD });
1330
+ /** The international card's settings section and its context-window preference. */
1331
+ const AI_SECTION = z.object({
1332
+ authFileAI: AUTH_FILE_AI_FIELD,
1333
+ useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1334
+ });
1315
1335
  /** Stable identity key used by credentials, probe records, and catalog entries. */
1316
1336
  function credentialIdentity(credential) {
1317
1337
  return `${credential.uid}:${credential.enterpriseId ?? ""}`;
@@ -1345,6 +1365,7 @@ function createVariantRuntime(config, variant, current, identityOf) {
1345
1365
  });
1346
1366
  const fallback = fallbackFor(variant);
1347
1367
  const catalog = new WorkBuddyCatalog(fallback);
1368
+ if (variant.id !== CN_VARIANT.id) catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
1348
1369
  catalog.setVisible(false);
1349
1370
  const probeStore = new WorkBuddyProbeStore({
1350
1371
  pluginVersion: WORKBUDDY_CONNECT_VERSION,
@@ -1520,6 +1541,7 @@ function apply(ctx, config) {
1520
1541
  const lastIdentities = /* @__PURE__ */ new Map();
1521
1542
  const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config, variant, () => current(), (id) => lastIdentities.get(id)));
1522
1543
  const probeKey = createProbeKey();
1544
+ let setMaximumContextWindow;
1523
1545
  /**
1524
1546
  * Point a variant at an account identity, invalidating whatever the previous
1525
1547
  * one left behind.
@@ -1586,7 +1608,8 @@ function apply(ctx, config) {
1586
1608
  models: () => runtime.catalog.current(),
1587
1609
  catalog: () => catalogSection(runtime),
1588
1610
  probe: () => probeSection(runtime, current().probeConsent === true),
1589
- probeKey
1611
+ probeKey,
1612
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { useMaximumContextWindow: () => current().useMaximumContextWindow === true }
1590
1613
  });
1591
1614
  registerWorkBuddyProbeRoute(webCtx, {
1592
1615
  path: runtime.variant.probePath,
@@ -1627,7 +1650,14 @@ function apply(ctx, config) {
1627
1650
  state: "failed",
1628
1651
  reason: runtime.catalogError
1629
1652
  };
1630
- }
1653
+ },
1654
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { setMaximumContextWindow: async (enabled) => {
1655
+ if (setMaximumContextWindow === void 0) return {
1656
+ state: "failed",
1657
+ reason: "settings are unavailable"
1658
+ };
1659
+ return setMaximumContextWindow(enabled);
1660
+ } }
1631
1661
  }, probeKey);
1632
1662
  }
1633
1663
  });
@@ -1641,10 +1671,16 @@ function apply(ctx, config) {
1641
1671
  const merged = () => ({
1642
1672
  ...sources.cn().authFile === void 0 ? {} : { authFile: sources.cn().authFile },
1643
1673
  ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
1644
- ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI }
1674
+ ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI },
1675
+ ...sources.ai().useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: sources.ai().useMaximumContextWindow }
1645
1676
  });
1677
+ const applyMaximumContextWindow = (next) => {
1678
+ const runtime = runtimes.find((candidate) => candidate.variant.id !== CN_VARIANT.id);
1679
+ if (runtime?.catalog.setUseMaximumContextWindow(next.useMaximumContextWindow === true)) runtime.invalidate();
1680
+ };
1646
1681
  const repointStores = () => {
1647
1682
  const next = merged();
1683
+ applyMaximumContextWindow(next);
1648
1684
  for (const runtime of runtimes) runtime.store.setDesktopPath(configuredAuthFile(next, runtime.variant));
1649
1685
  };
1650
1686
  settingsCtx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, CN_SECTION, config, {
@@ -1661,6 +1697,10 @@ function apply(ctx, config) {
1661
1697
  },
1662
1698
  onChange: repointStores
1663
1699
  });
1700
+ setMaximumContextWindow = async (enabled) => {
1701
+ await settingsCtx.settings.update(WORKBUDDY_AI_SETTINGS_NS, { useMaximumContextWindow: enabled });
1702
+ return { state: "updated" };
1703
+ };
1664
1704
  });
1665
1705
  ctx.effect(() => () => {
1666
1706
  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.3";
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.3",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",