dsh-connect-workbuddy 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +67 -15
- package/lib/{host-heartbeat-CaS5Koaw.js → host-heartbeat-CPIHUxKL.js} +91 -10
- package/lib/index.d.ts +52 -4
- package/lib/index.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.4 (2026-09-21)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **修复「保存的账号在本机消失后,界面自相矛盾」**(issue #10 的缺陷 3;其症状属实,但其声称的机制与严重性不准确)。显式选择的账号若在本机不再存在(WorkBuddy 更换登录或清理备份文件——`current()` 的注释早已点名这个成因),同一个状态下四条读取路径给出两种说法:
|
|
8
|
+
|
|
9
|
+
| 路径 | 修复前 | 修复后 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `accounts()` | 靠对象同一性比较**仍标记一个账号为 selected** | 无人生效时不标记任何行 |
|
|
12
|
+
| `current()` | 返回 `undefined`(拒绝计费,刻意不回退) | 不变(账单安全语义保留) |
|
|
13
|
+
| `status()` | `signed-out` | 不变 |
|
|
14
|
+
| 卡片下拉框 | `value` 取 signed-in 分支才有的 `accountId` → `''` → **空白**(选项仍在) | 显示「当前没有生效的账号」占位项 |
|
|
15
|
+
|
|
16
|
+
- **根因**:`accounts()` 与 `current()` 对「id 失配」的口径不一致。卡片渲染前者、计费走后者,于是面板同时出现「下拉框选中 Alpha」「状态栏未登录」「提示去桌面端登录一次」——而用户明明已登录,那句提示还把人指向唯一无法修复该问题的操作。commit `1279234` 移除 `current()` 的回退(`?? this.preferred()`)以避免账单落到别的账号,本修复保留该决定,只消除显示层矛盾。
|
|
17
|
+
- **`accounts()` 不再报告幽灵选中**:显式选择失配时不再回退到对象同一性比较(原写法依赖 `preferred()` 返回同一数组元素,能工作但语义脆弱);未显式选择时仍如实标出正在跟随的默认账号。
|
|
18
|
+
- **新增 `selectionLost`**:usage 文档的 `signed-out` 分支新增该可选字段,区分「保存的选择已失效(凭据是好的,请重选或清除)」与「本机确实没有登录(去桌面端登录)」。仅前者为真时才显示新的说明文案,后者保留原文案。
|
|
19
|
+
- **卡片下拉框改从账号列表取选中值**(原先取 signed-in 文档的 `accountId`)。这是同一类问题的第二处:当保存的账号**仍然有效**、只是 token 过期且刷新失败时,`resolve()` 同样抛错,下拉框也会变空白——用户的完整选择看起来像消失了。现在由账号列表这一个来源决定,并以此区分「没有任何账号生效」与「已登录」。
|
|
20
|
+
- **新增「跟随 App 当前登录」按钮**:清除该区域的显式选择,回到文档承诺的默认行为。清除写入空字符串哨兵值,`selectAccount('')` 归一化为 `undefined`;此前空字符串会像任何失配 id 一样永久卡死(即「没有清除路径」)。
|
|
21
|
+
- **`rescanAccounts()` 不再写入选中的账号**:原先只要 `accounts()` 报告的选中项与保存值不同就回写设置,导致两点危害——无显式选择时把「跟随 App 登录」的默认行为固化成一个永久绑定(此后 App 换登录不再跟随,与 `README`/`DESIGN` 承诺不符),以及保存的 id 失配时静默把区域改绑到**另一个账号**(正是严格不回退规则要防的静默改选)。「重新检测」现在只重读,选择权归下拉框与清除按钮。
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
|
|
25
|
+
测试总数 131 → 142(新增 11 例):
|
|
26
|
+
|
|
27
|
+
- `auth.spec.ts` 新增 7 例:失配选择不标记任何账号、未显式选择仍标出默认账号、有效选择在刷新失败时保持可见、`selectionLost` 的三种取值(无选择/有效选择/失配)、无本机登录时不报 `selectionLost`、空字符串哨兵恢复默认、以及区域隔离(一个区域失配不影响另一区域)。
|
|
28
|
+
- `web-status.spec.ts` 新增 3 例:orphaned 选择标记 `selectionLost` 且保留账号列表、真正的未登录不标记、signed-in 文档不含该字段。
|
|
29
|
+
- `settings-integration.spec.ts` 新增 1 例:空字符串哨兵经真实的 `settings.update` 链路写入后仍为 `''`,且两个 provider 继续正常服务。
|
|
30
|
+
|
|
31
|
+
### Docs
|
|
32
|
+
|
|
33
|
+
- `README.md` / `README.en.md` 的「已知限制 / Known limitations」补充账号失效后的表现与恢复方式,并说明重新登录桌面端无法修复该状态。
|
|
34
|
+
|
|
3
35
|
## 2.0.2 (2026-09-15)
|
|
4
36
|
|
|
5
37
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -115,6 +115,7 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
115
115
|
|
|
116
116
|
- The plugin depends on WorkBuddy client endpoints (not an official public API), so a WorkBuddy update may require adjustments.
|
|
117
117
|
- **Account switching** relies on the historical auth files the WorkBuddy desktop app leaves behind (the app's own backups), not an official multi-account API. Following the app's current sign-in remains the default; switching is an explicit opt-in, and historical credentials can be invalidated by the app's cleanup or sign-out.
|
|
118
|
+
- If an explicitly selected account disappears locally (the app replaced its login or cleaned up its backups), the plugin does **not** silently switch to another account — that would bill a different account. The card says the saved account no longer exists; pick an account again, or use "Follow the app's sign-in" to clear the saved choice. **Signing in again in the desktop app will not fix this**: the credentials are fine, it is the saved account id that no longer matches.
|
|
118
119
|
- Windows and Linux credential paths are derived from platform conventions and are not verified on real hardware; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
|
|
119
120
|
|
|
120
121
|
## Disclaimer
|
package/README.md
CHANGED
|
@@ -123,6 +123,7 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
|
123
123
|
|
|
124
124
|
- 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。
|
|
125
125
|
- **账号切换**基于 WorkBuddy 桌面端留下的历史 auth 文件(App 自身的备份产物),并非官方多账号 API。默认行为仍是跟随 App 当前登录;切换账号是显式选项,且历史凭据可能因 App 清理或退出登录而失效。
|
|
126
|
+
- 显式选择的账号若在本机消失(App 更换登录或清理备份文件),插件**不会**静默改选其他账号——那样会让账单落到另一个账号上。卡片会明确说明「保存的账号已不存在」,此时重新选择一个账号,或点「跟随 App 当前登录」清除该选择即可恢复。**重新登录桌面端 App 无法修复这种状态**:凭据本身是好的,失效的只是保存下来的账号标识。
|
|
126
127
|
- Windows / Linux 的凭据默认路径为按平台约定推导,未经真机验证;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
|
|
127
128
|
|
|
128
129
|
## 免责声明
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { T as WorkBuddyUpstreamClient, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-
|
|
2
|
+
import { T as WorkBuddyUpstreamClient, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-CPIHUxKL.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
package/lib/client.js
CHANGED
|
@@ -356,6 +356,19 @@ window.__ModuleLoader__.load({
|
|
|
356
356
|
refreshUsage,
|
|
357
357
|
status.status
|
|
358
358
|
]);
|
|
359
|
+
/**
|
|
360
|
+
* Re-detect the local sign-ins and refresh the panel.
|
|
361
|
+
*
|
|
362
|
+
* This deliberately writes NO account selection. It used to persist whatever
|
|
363
|
+
* row the store reported as "selected" whenever that differed from the saved
|
|
364
|
+
* value, which had two harmful effects: with no explicit choice it turned the
|
|
365
|
+
* documented default (follow the app's current sign-in) into a permanent
|
|
366
|
+
* explicit binding — so a later sign-in in the app was no longer followed —
|
|
367
|
+
* and with an orphaned saved id it silently re-bound the region to a
|
|
368
|
+
* different account, which is the silent switch that the strict
|
|
369
|
+
* no-fallback rule exists to prevent. Re-detecting and re-picking are
|
|
370
|
+
* separate actions now; the picker and Clear button own the selection.
|
|
371
|
+
*/
|
|
359
372
|
const rescanAccounts = async () => {
|
|
360
373
|
setBusy(true);
|
|
361
374
|
try {
|
|
@@ -366,13 +379,6 @@ window.__ModuleLoader__.load({
|
|
|
366
379
|
});
|
|
367
380
|
const body = await response.json();
|
|
368
381
|
if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
|
|
369
|
-
const selected = body.accounts.find((account) => account.selected)?.id;
|
|
370
|
-
const configuredAccounts = configuredAccountsOf(settingsScope?.getSnapshot().value);
|
|
371
|
-
const configuredId = configuredAccounts[activeRegion];
|
|
372
|
-
if (selected !== void 0 && selected !== configuredId && settingsScope?.getSnapshot().writable === true) await settingsScope.set("accounts", {
|
|
373
|
-
...configuredAccounts,
|
|
374
|
-
[activeRegion]: selected
|
|
375
|
-
});
|
|
376
382
|
await refreshUsage(activeRegion);
|
|
377
383
|
} finally {
|
|
378
384
|
if (mounted.current) setBusy(false);
|
|
@@ -392,6 +398,26 @@ window.__ModuleLoader__.load({
|
|
|
392
398
|
if (mounted.current) setSwitchingAccount(false);
|
|
393
399
|
}
|
|
394
400
|
};
|
|
401
|
+
/**
|
|
402
|
+
* Drop the explicit choice so the region returns to its documented default:
|
|
403
|
+
* follow whatever the WorkBuddy app is currently signed in as. The empty
|
|
404
|
+
* string is the settings-level sentinel for "no explicit selection" (the
|
|
405
|
+
* store normalizes it away); the account id itself is never written here.
|
|
406
|
+
*/
|
|
407
|
+
const clearAccount = async () => {
|
|
408
|
+
if (settingsScope === void 0) return;
|
|
409
|
+
setSwitchingAccount(true);
|
|
410
|
+
try {
|
|
411
|
+
const configuredAccounts = configuredAccountsOf(settingsScope.getSnapshot().value);
|
|
412
|
+
await settingsScope.set("accounts", {
|
|
413
|
+
...configuredAccounts,
|
|
414
|
+
[activeRegion]: ""
|
|
415
|
+
});
|
|
416
|
+
await refreshUsage(activeRegion);
|
|
417
|
+
} finally {
|
|
418
|
+
if (mounted.current) setSwitchingAccount(false);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
395
421
|
const claimDailyCheckin = async () => {
|
|
396
422
|
setCheckingIn(true);
|
|
397
423
|
setCheckinActionError(void 0);
|
|
@@ -536,6 +562,8 @@ window.__ModuleLoader__.load({
|
|
|
536
562
|
};
|
|
537
563
|
const title = t("row.title");
|
|
538
564
|
const label = status.status === "signed-in" ? t("row.signedIn", { accountName: status.accountName }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
|
|
565
|
+
/** The saved choice no longer matches a local sign-in (tokens are fine). */
|
|
566
|
+
const selectionLost = status.status === "signed-out" && status.selectionLost === true;
|
|
539
567
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
540
568
|
className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
|
|
541
569
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -621,6 +649,10 @@ window.__ModuleLoader__.load({
|
|
|
621
649
|
status.status === "error" || status.status === "signed-in" && status.creditsError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
622
650
|
className: "dsm-workbuddy-usage-hint",
|
|
623
651
|
children: t("row.reloginHint")
|
|
652
|
+
}) : null,
|
|
653
|
+
selectionLost ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
654
|
+
className: "dsm-workbuddy-usage-hint",
|
|
655
|
+
children: t("row.selectionLostHint")
|
|
624
656
|
}) : null
|
|
625
657
|
]
|
|
626
658
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -633,24 +665,36 @@ window.__ModuleLoader__.load({
|
|
|
633
665
|
children: busy ? t("row.accountsScanning") : t("row.accountsRescan")
|
|
634
666
|
})]
|
|
635
667
|
}),
|
|
636
|
-
status.status !== "error" && status.accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
668
|
+
status.status !== "error" && status.accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
637
669
|
className: "dsm-workbuddy-account-picker",
|
|
638
670
|
"aria-label": t("row.accountsTitle"),
|
|
639
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
671
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
640
672
|
className: "dsm-workbuddy-usage-select-wrap",
|
|
641
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
673
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
642
674
|
className: "dsm-workbuddy-usage-select",
|
|
643
|
-
value: status.
|
|
675
|
+
value: status.accounts.find((account) => account.selected)?.id ?? "",
|
|
644
676
|
disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
|
|
645
677
|
onChange: (event) => {
|
|
646
678
|
switchAccount(event.currentTarget.value);
|
|
647
679
|
},
|
|
648
|
-
children: status.accounts.
|
|
680
|
+
children: [status.accounts.some((account) => account.selected) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
681
|
+
value: "",
|
|
682
|
+
disabled: true,
|
|
683
|
+
children: t("row.accountNoneInEffect")
|
|
684
|
+
}), status.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
649
685
|
value: account.id,
|
|
650
686
|
children: [account.accountName, account.domain === "" ? "" : ` · ${account.domain}`]
|
|
651
|
-
}, account.id))
|
|
687
|
+
}, account.id))]
|
|
652
688
|
})
|
|
653
|
-
})
|
|
689
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
690
|
+
type: "button",
|
|
691
|
+
className: "dsm-btn dsm-btn-outline",
|
|
692
|
+
disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
|
|
693
|
+
onClick: () => {
|
|
694
|
+
clearAccount();
|
|
695
|
+
},
|
|
696
|
+
children: t("row.accountsFollowApp")
|
|
697
|
+
})]
|
|
654
698
|
}) : null,
|
|
655
699
|
status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
656
700
|
status.credits === void 0 ? null : (() => {
|
|
@@ -876,7 +920,7 @@ window.__ModuleLoader__.load({
|
|
|
876
920
|
] }) : null,
|
|
877
921
|
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
878
922
|
className: "dsm-workbuddy-usage-text",
|
|
879
|
-
children: status.message ?? t("row.signedOutHint")
|
|
923
|
+
children: selectionLost ? t("row.selectionLostMessage") : status.message ?? t("row.signedOutHint")
|
|
880
924
|
}) : null,
|
|
881
925
|
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
882
926
|
className: "dsm-workbuddy-usage-error",
|
|
@@ -929,6 +973,10 @@ window.__ModuleLoader__.load({
|
|
|
929
973
|
"row.accountsHint": "Choose from locally detected sign-ins. Tokens are never shown or saved in DSH settings.",
|
|
930
974
|
"row.accountsRescan": "Detect accounts again",
|
|
931
975
|
"row.accountsScanning": "Detecting…",
|
|
976
|
+
"row.accountsFollowApp": "Follow the app's sign-in",
|
|
977
|
+
"row.accountNoneInEffect": "No account in effect",
|
|
978
|
+
"row.selectionLostHint": "The saved account no longer exists locally.",
|
|
979
|
+
"row.selectionLostMessage": "The account saved for this region is no longer among the local sign-ins (WorkBuddy replaced its login or cleaned up backups). Your other sign-ins are fine — pick one above, or choose \"Follow the app's sign-in\" to clear the saved choice. Signing in again in the desktop app will not fix this.",
|
|
932
980
|
"row.modelsTitle": "Models",
|
|
933
981
|
"row.modelsSummary": "{count} enabled",
|
|
934
982
|
"row.modelsRefresh": "Refresh from WorkBuddy",
|
|
@@ -977,6 +1025,10 @@ window.__ModuleLoader__.load({
|
|
|
977
1025
|
"row.accountsHint": "选择本机检测到的登录账号;Token 不会显示,也不会保存到 DSH 设置。",
|
|
978
1026
|
"row.accountsRescan": "重新检测账号",
|
|
979
1027
|
"row.accountsScanning": "正在检测…",
|
|
1028
|
+
"row.accountsFollowApp": "跟随 App 当前登录",
|
|
1029
|
+
"row.accountNoneInEffect": "当前没有生效的账号",
|
|
1030
|
+
"row.selectionLostHint": "已保存的账号在本机已不存在。",
|
|
1031
|
+
"row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方选择一个,或点「跟随 App 当前登录」清除已保存的选择。重新登录桌面端 App 无法修复此问题。",
|
|
980
1032
|
"row.modelsTitle": "模型",
|
|
981
1033
|
"row.modelsSummary": "已启用 {count} 个",
|
|
982
1034
|
"row.modelsRefresh": "从 WorkBuddy 刷新",
|
|
@@ -271,13 +271,56 @@ function parseCreditMultiplier(value) {
|
|
|
271
271
|
const parsed = Number(match[1]);
|
|
272
272
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
273
273
|
}
|
|
274
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* The effort vocabulary the upstream's plural-form payloads declare across both
|
|
276
|
+
* gateways (live union of every `supportedEfforts` list seen; `minimal` has
|
|
277
|
+
* never appeared). Both gateways also accept every level in it on
|
|
278
|
+
* singular-form models — medium/xhigh fold into high, low/max answer with
|
|
279
|
+
* their own budgets — so the singular `effort` value is a DEFAULT, never the
|
|
280
|
+
* model's only level.
|
|
281
|
+
*/
|
|
282
|
+
const SINGULAR_EFFORT_LADDER = [
|
|
283
|
+
"low",
|
|
284
|
+
"medium",
|
|
285
|
+
"high",
|
|
286
|
+
"xhigh",
|
|
287
|
+
"max"
|
|
288
|
+
];
|
|
289
|
+
/**
|
|
290
|
+
* The singular spelling of reasoning metadata: `effort` (plus the ignored
|
|
291
|
+
* `summary`) and none of the plural-form fields. Observed on CN
|
|
292
|
+
* `deepseek-v4.1-flash`/`kimi-k3-1`/`glm-5.2`… and global
|
|
293
|
+
* `deepseek-v4.1-flash`/`kimi-k3`/`gemini-3.5-flash`… (issue #7).
|
|
294
|
+
*/
|
|
295
|
+
function isSingularEffortForm(raw) {
|
|
296
|
+
return typeof raw["effort"] === "string" && !Array.isArray(raw["supportedEfforts"]) && typeof raw["defaultEffort"] !== "string" && typeof raw["canDisableThinking"] !== "boolean";
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Fold a singular-form `effort` into the plural shape the rest of the plugin
|
|
300
|
+
* already understands. Live probes on both gateways (issue #7) show these
|
|
301
|
+
* models answer with distinct `reasoning_content` across the whole ladder —
|
|
302
|
+
* and think NOT AT ALL when no `reasoning_effort` is sent — so the fold
|
|
303
|
+
* widens `supportedEfforts` and carries the declared value into
|
|
304
|
+
* `defaultEffort`. An unrecognized `effort` value passes through as the lone
|
|
305
|
+
* level, leaving its fate to the adapter's known-level filter.
|
|
306
|
+
*/
|
|
307
|
+
function singularEffortLadder(raw) {
|
|
308
|
+
const effort = typeof raw["effort"] === "string" ? raw["effort"] : void 0;
|
|
309
|
+
if (effort === void 0) return void 0;
|
|
310
|
+
return SINGULAR_EFFORT_LADDER.includes(effort) ? [...SINGULAR_EFFORT_LADDER] : [effort];
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse the upstream's `reasoning` object; unknown shapes degrade to `{}`.
|
|
314
|
+
* Both spellings normalize here: the plural form passes through as declared,
|
|
315
|
+
* and the singular `effort` form folds via {@link singularEffortLadder}.
|
|
316
|
+
*/
|
|
275
317
|
function parseReasoning(value) {
|
|
276
318
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
277
319
|
const raw = value;
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const
|
|
320
|
+
const effort = typeof raw["effort"] === "string" ? raw["effort"] : void 0;
|
|
321
|
+
const supportedEfforts = Array.isArray(raw["supportedEfforts"]) ? raw["supportedEfforts"].filter((entry) => typeof entry === "string") : singularEffortLadder(raw);
|
|
322
|
+
const defaultEffort = typeof raw["defaultEffort"] === "string" ? raw["defaultEffort"] : effort;
|
|
323
|
+
const canDisableThinking = typeof raw["canDisableThinking"] === "boolean" ? raw["canDisableThinking"] : isSingularEffortForm(raw) ? true : void 0;
|
|
281
324
|
if (supportedEfforts === void 0 && defaultEffort === void 0 && canDisableThinking === void 0) return;
|
|
282
325
|
return {
|
|
283
326
|
...supportedEfforts === void 0 || supportedEfforts.length === 0 ? {} : { supportedEfforts },
|
|
@@ -871,9 +914,17 @@ var WorkBuddyCredentialStore = class {
|
|
|
871
914
|
this.desktopPathOverride = path;
|
|
872
915
|
this.inflight = void 0;
|
|
873
916
|
}
|
|
874
|
-
/**
|
|
917
|
+
/**
|
|
918
|
+
* Select an account by id; tokens stay outside settings.
|
|
919
|
+
*
|
|
920
|
+
* The empty string is the settings-level sentinel for "no explicit
|
|
921
|
+
* selection" (the card's Clear action writes it), so it is normalized here
|
|
922
|
+
* rather than being kept as an id that can never match an account. Every
|
|
923
|
+
* caller therefore gets the documented default — follow the app's current
|
|
924
|
+
* sign-in — instead of a dead selection.
|
|
925
|
+
*/
|
|
875
926
|
selectAccount(accountId) {
|
|
876
|
-
this.accountId = accountId;
|
|
927
|
+
this.accountId = accountId === "" ? void 0 : accountId;
|
|
877
928
|
this.inflight = void 0;
|
|
878
929
|
}
|
|
879
930
|
/** Selected account id, for diagnostics and route assembly. */
|
|
@@ -971,11 +1022,25 @@ var WorkBuddyCredentialStore = class {
|
|
|
971
1022
|
if (credentials.length === 0) return void 0;
|
|
972
1023
|
return credentials.reduce((best, credential) => isFresher(credential, best) ? credential : best);
|
|
973
1024
|
}
|
|
974
|
-
/**
|
|
1025
|
+
/**
|
|
1026
|
+
* Token-free account list for the plugin card.
|
|
1027
|
+
*
|
|
1028
|
+
* `selected` answers one question only: which row is the account the plugin
|
|
1029
|
+
* is actually going to use? That is exactly what {@link current} decides, so
|
|
1030
|
+
* the two must never disagree — the card renders this list while the shim
|
|
1031
|
+
* bills through `current()`, and a row marked "selected" that `current()`
|
|
1032
|
+
* refuses to use is what made the dropdown look healthy while every request
|
|
1033
|
+
* failed with 401.
|
|
1034
|
+
*
|
|
1035
|
+
* So an explicit selection that matches NO local account marks nothing as
|
|
1036
|
+
* selected (no silent fallback to a different account — see {@link current}).
|
|
1037
|
+
* The implicit default is marked only when nothing was explicitly chosen.
|
|
1038
|
+
*/
|
|
975
1039
|
async accounts() {
|
|
976
1040
|
const credentials = await this.readAll();
|
|
977
1041
|
if (credentials.length === 0) return [];
|
|
978
|
-
const
|
|
1042
|
+
const hasExplicitSelection = this.accountId !== void 0;
|
|
1043
|
+
const selectedExists = hasExplicitSelection && credentials.some((credential) => workbuddyAccountId(credential) === this.accountId);
|
|
979
1044
|
const defaultSelected = this.preferred(credentials);
|
|
980
1045
|
return credentials.map((credential) => {
|
|
981
1046
|
const id = workbuddyAccountId(credential);
|
|
@@ -987,10 +1052,26 @@ var WorkBuddyCredentialStore = class {
|
|
|
987
1052
|
source: credential.source,
|
|
988
1053
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
989
1054
|
filePath: credential.filePath,
|
|
990
|
-
selected: selectedExists ? id === this.accountId : credential === defaultSelected
|
|
1055
|
+
selected: selectedExists ? id === this.accountId : !hasExplicitSelection && credential === defaultSelected
|
|
991
1056
|
};
|
|
992
1057
|
});
|
|
993
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Whether a persisted selection exists that matches no local account, while
|
|
1061
|
+
* other local sign-ins ARE available to choose from.
|
|
1062
|
+
*
|
|
1063
|
+
* The card uses this to explain the state honestly (the account is signed in,
|
|
1064
|
+
* but the SAVED choice is gone) instead of showing the generic "sign in
|
|
1065
|
+
* again" hint, which misdirects: the token is usually perfectly healthy and
|
|
1066
|
+
* signing in again does not repair an orphaned id. When no local sign-in is
|
|
1067
|
+
* available at all, that hint IS accurate and this returns false.
|
|
1068
|
+
*/
|
|
1069
|
+
async selectionLost() {
|
|
1070
|
+
if (this.accountId === void 0) return false;
|
|
1071
|
+
const credentials = await this.readAll();
|
|
1072
|
+
if (credentials.length === 0) return false;
|
|
1073
|
+
return !credentials.some((credential) => workbuddyAccountId(credential) === this.accountId);
|
|
1074
|
+
}
|
|
994
1075
|
/** The freshest stored credential for the current selection, no refresh. */
|
|
995
1076
|
async current() {
|
|
996
1077
|
const credentials = await this.readAll();
|
|
@@ -1370,7 +1451,7 @@ var WorkBuddyCatalog = class {
|
|
|
1370
1451
|
//#endregion
|
|
1371
1452
|
//#region src/version.ts
|
|
1372
1453
|
/** The npm package version this build was produced from. */
|
|
1373
|
-
const WORKBUDDY_CONNECT_VERSION = "2.0.
|
|
1454
|
+
const WORKBUDDY_CONNECT_VERSION = "2.0.4";
|
|
1374
1455
|
//#endregion
|
|
1375
1456
|
//#region src/host-heartbeat.ts
|
|
1376
1457
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -175,7 +175,15 @@ declare class WorkBuddyCredentialStore {
|
|
|
175
175
|
private ownCandidates;
|
|
176
176
|
/** Repoint the desktop file or directory; applies on the next read. */
|
|
177
177
|
setDesktopPath(path: string | undefined): void;
|
|
178
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Select an account by id; tokens stay outside settings.
|
|
180
|
+
*
|
|
181
|
+
* The empty string is the settings-level sentinel for "no explicit
|
|
182
|
+
* selection" (the card's Clear action writes it), so it is normalized here
|
|
183
|
+
* rather than being kept as an id that can never match an account. Every
|
|
184
|
+
* caller therefore gets the documented default — follow the app's current
|
|
185
|
+
* sign-in — instead of a dead selection.
|
|
186
|
+
*/
|
|
179
187
|
selectAccount(accountId: string | undefined): void;
|
|
180
188
|
/** Selected account id, for diagnostics and route assembly. */
|
|
181
189
|
selectedAccountId(): string | undefined;
|
|
@@ -215,8 +223,32 @@ declare class WorkBuddyCredentialStore {
|
|
|
215
223
|
* remaining credit.
|
|
216
224
|
*/
|
|
217
225
|
private preferred;
|
|
218
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Token-free account list for the plugin card.
|
|
228
|
+
*
|
|
229
|
+
* `selected` answers one question only: which row is the account the plugin
|
|
230
|
+
* is actually going to use? That is exactly what {@link current} decides, so
|
|
231
|
+
* the two must never disagree — the card renders this list while the shim
|
|
232
|
+
* bills through `current()`, and a row marked "selected" that `current()`
|
|
233
|
+
* refuses to use is what made the dropdown look healthy while every request
|
|
234
|
+
* failed with 401.
|
|
235
|
+
*
|
|
236
|
+
* So an explicit selection that matches NO local account marks nothing as
|
|
237
|
+
* selected (no silent fallback to a different account — see {@link current}).
|
|
238
|
+
* The implicit default is marked only when nothing was explicitly chosen.
|
|
239
|
+
*/
|
|
219
240
|
accounts(): Promise<WorkBuddyAccountChoice[]>;
|
|
241
|
+
/**
|
|
242
|
+
* Whether a persisted selection exists that matches no local account, while
|
|
243
|
+
* other local sign-ins ARE available to choose from.
|
|
244
|
+
*
|
|
245
|
+
* The card uses this to explain the state honestly (the account is signed in,
|
|
246
|
+
* but the SAVED choice is gone) instead of showing the generic "sign in
|
|
247
|
+
* again" hint, which misdirects: the token is usually perfectly healthy and
|
|
248
|
+
* signing in again does not repair an orphaned id. When no local sign-in is
|
|
249
|
+
* available at all, that hint IS accurate and this returns false.
|
|
250
|
+
*/
|
|
251
|
+
selectionLost(): Promise<boolean>;
|
|
220
252
|
/** The freshest stored credential for the current selection, no refresh. */
|
|
221
253
|
current(): Promise<WorkBuddyCredential | undefined>;
|
|
222
254
|
/** The credential to send upstream: {@link current}, refreshed on demand. */
|
|
@@ -360,7 +392,11 @@ declare function prepareChatBody(source: string): string;
|
|
|
360
392
|
* card simply omits the rate instead of displaying a fabricated one.
|
|
361
393
|
*/
|
|
362
394
|
declare function parseCreditMultiplier(value: unknown): number | undefined;
|
|
363
|
-
/**
|
|
395
|
+
/**
|
|
396
|
+
* Parse the upstream's `reasoning` object; unknown shapes degrade to `{}`.
|
|
397
|
+
* Both spellings normalize here: the plural form passes through as declared,
|
|
398
|
+
* and the singular `effort` form folds via {@link singularEffortLadder}.
|
|
399
|
+
*/
|
|
364
400
|
declare function parseReasoning(value: unknown): WorkBuddyReasoning | undefined;
|
|
365
401
|
/** Parse one catalog entry; entries without usable token limits are dropped. */
|
|
366
402
|
declare function parseUpstreamModel(value: unknown): WorkBuddyUpstreamModel | undefined;
|
|
@@ -721,10 +757,22 @@ type WorkBuddyWebPackage = WorkBuddyWebCreditPackage;
|
|
|
721
757
|
*/
|
|
722
758
|
type WorkBuddyWebRegion = 'cn' | 'global';
|
|
723
759
|
/** The JSON document the plugin card renders. */
|
|
724
|
-
type WorkBuddyWebUsage =
|
|
760
|
+
type WorkBuddyWebUsage =
|
|
761
|
+
/**
|
|
762
|
+
* Not usable right now. `accounts` is still populated so the picker can
|
|
763
|
+
* offer a way out, and `selectionLost` separates the two very different
|
|
764
|
+
* reasons this happens: local sign-ins exist but the SAVED choice no longer
|
|
765
|
+
* matches any of them (re-select it, or clear it to follow the app's current
|
|
766
|
+
* sign-in again), versus nothing usable was found at all (sign in in the
|
|
767
|
+
* desktop app). Only the latter makes the "sign in again" hint truthful —
|
|
768
|
+
* re-signing in does not repair an orphaned id.
|
|
769
|
+
*/
|
|
770
|
+
{
|
|
725
771
|
status: 'signed-out';
|
|
726
772
|
accounts: readonly WorkBuddyWebAccount[];
|
|
727
773
|
message?: string;
|
|
774
|
+
/** The persisted account id matches no local account. */
|
|
775
|
+
selectionLost?: boolean;
|
|
728
776
|
} | {
|
|
729
777
|
status: 'signed-in';
|
|
730
778
|
accountId: string;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as prepareChatBody, C as workbuddyAccountId, D as parseCreditMultiplier, E as classifyUpstreamError, O as parseReasoning, S as parseWorkBuddyAuth, T as WorkBuddyUpstreamClient, _ as authFileName, a as readHostHeartbeat, b as defaultDesktopAuthPath, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, i as processStartTimeMs, j as regionOf, k as parseUpstreamModel, l as FALLBACK_WORKBUDDY_MODELS, m as WORKBUDDY_AUTH_FILENAME, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-
|
|
1
|
+
import { A as prepareChatBody, C as workbuddyAccountId, D as parseCreditMultiplier, E as classifyUpstreamError, O as parseReasoning, S as parseWorkBuddyAuth, T as WorkBuddyUpstreamClient, _ as authFileName, a as readHostHeartbeat, b as defaultDesktopAuthPath, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WorkBuddyCredentialStore, h as WORKBUDDY_AUTH_FILE_ENV, i as processStartTimeMs, j as regionOf, k as parseUpstreamModel, l as FALLBACK_WORKBUDDY_MODELS, m as WORKBUDDY_AUTH_FILENAME, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as defaultDesktopAuthCandidates, w as workbuddyOwnAuthPath, x as legacyWorkbuddyOwnAuthPath, y as defaultDesktopAuthDirs } from "./host-heartbeat-CPIHUxKL.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -572,7 +572,8 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
572
572
|
return {
|
|
573
573
|
status: "signed-out",
|
|
574
574
|
accounts: accounts.map(toWebAccount),
|
|
575
|
-
message: safeMessage(error)
|
|
575
|
+
message: safeMessage(error),
|
|
576
|
+
...await store.selectionLost() ? { selectionLost: true } : {}
|
|
576
577
|
};
|
|
577
578
|
}
|
|
578
579
|
const account = {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
|
|
5
|
-
"version": "2.0.
|
|
5
|
+
"version": "2.0.4",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|