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 +4 -1
- package/README.md +4 -1
- package/lib/bin.js +10 -3
- package/lib/client.js +130 -30
- package/lib/index.d.ts +54 -1
- package/lib/index.js +49 -9
- package/lib/{variants-CExA7lJt.js → variants-CdmzOPIT.js} +138 -3
- package/package.json +1 -1
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
|

|
|
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
|
|
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
|

|
|
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
|

|
|
39
41
|
|
|
40
|
-
卡片展开后分为「状态 / 上下文 /
|
|
42
|
+
卡片展开后分为「状态 / 上下文 / 明细」三个标签:状态页展示账号、令牌有效期、合计积分、模型列表来源与推理档位检测;上下文页列出各模型的上下文窗口。国际版在上游声明了更大可选窗口时,可在这里切换「使用上游声明的最大上下文窗口」;该开关**默认开启**,DSH 会按上游声明的最大窗口安排上下文压缩;想改用上游的默认窗口就在这里关掉,偏好会持久化,重启后保持。明细页展示各套餐余量与模型优惠。国内版与国际版各有一张自己的卡片,各显示自己账号的信息。
|
|
41
43
|
|
|
42
44
|

|
|
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-
|
|
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)
|
|
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
|
|
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
|
-
/**
|
|
67
|
-
const
|
|
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
|
-
|
|
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: [
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
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("
|
|
498
|
+
children: t("useMaximumContextWindowHint")
|
|
449
499
|
})]
|
|
450
500
|
})]
|
|
451
|
-
},
|
|
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.
|
|
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
|
-
/**
|
|
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-
|
|
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
|
-
...
|
|
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
|
|
1314
|
-
const AI_SECTION = z.object({
|
|
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
|
-
/**
|
|
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) =>
|
|
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.
|
|
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.
|
|
5
|
+
"version": "0.5.3",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|