dsh-connect-workbuddy 2.0.3 → 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-CYeVuoJE.js → host-heartbeat-CPIHUxKL.js} +44 -6
- package/lib/index.d.ts +47 -3
- 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 刷新",
|
|
@@ -914,9 +914,17 @@ var WorkBuddyCredentialStore = class {
|
|
|
914
914
|
this.desktopPathOverride = path;
|
|
915
915
|
this.inflight = void 0;
|
|
916
916
|
}
|
|
917
|
-
/**
|
|
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
|
+
*/
|
|
918
926
|
selectAccount(accountId) {
|
|
919
|
-
this.accountId = accountId;
|
|
927
|
+
this.accountId = accountId === "" ? void 0 : accountId;
|
|
920
928
|
this.inflight = void 0;
|
|
921
929
|
}
|
|
922
930
|
/** Selected account id, for diagnostics and route assembly. */
|
|
@@ -1014,11 +1022,25 @@ var WorkBuddyCredentialStore = class {
|
|
|
1014
1022
|
if (credentials.length === 0) return void 0;
|
|
1015
1023
|
return credentials.reduce((best, credential) => isFresher(credential, best) ? credential : best);
|
|
1016
1024
|
}
|
|
1017
|
-
/**
|
|
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
|
+
*/
|
|
1018
1039
|
async accounts() {
|
|
1019
1040
|
const credentials = await this.readAll();
|
|
1020
1041
|
if (credentials.length === 0) return [];
|
|
1021
|
-
const
|
|
1042
|
+
const hasExplicitSelection = this.accountId !== void 0;
|
|
1043
|
+
const selectedExists = hasExplicitSelection && credentials.some((credential) => workbuddyAccountId(credential) === this.accountId);
|
|
1022
1044
|
const defaultSelected = this.preferred(credentials);
|
|
1023
1045
|
return credentials.map((credential) => {
|
|
1024
1046
|
const id = workbuddyAccountId(credential);
|
|
@@ -1030,10 +1052,26 @@ var WorkBuddyCredentialStore = class {
|
|
|
1030
1052
|
source: credential.source,
|
|
1031
1053
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
1032
1054
|
filePath: credential.filePath,
|
|
1033
|
-
selected: selectedExists ? id === this.accountId : credential === defaultSelected
|
|
1055
|
+
selected: selectedExists ? id === this.accountId : !hasExplicitSelection && credential === defaultSelected
|
|
1034
1056
|
};
|
|
1035
1057
|
});
|
|
1036
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
|
+
}
|
|
1037
1075
|
/** The freshest stored credential for the current selection, no refresh. */
|
|
1038
1076
|
async current() {
|
|
1039
1077
|
const credentials = await this.readAll();
|
|
@@ -1413,7 +1451,7 @@ var WorkBuddyCatalog = class {
|
|
|
1413
1451
|
//#endregion
|
|
1414
1452
|
//#region src/version.ts
|
|
1415
1453
|
/** The npm package version this build was produced from. */
|
|
1416
|
-
const WORKBUDDY_CONNECT_VERSION = "2.0.
|
|
1454
|
+
const WORKBUDDY_CONNECT_VERSION = "2.0.4";
|
|
1417
1455
|
//#endregion
|
|
1418
1456
|
//#region src/host-heartbeat.ts
|
|
1419
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. */
|
|
@@ -725,10 +757,22 @@ type WorkBuddyWebPackage = WorkBuddyWebCreditPackage;
|
|
|
725
757
|
*/
|
|
726
758
|
type WorkBuddyWebRegion = 'cn' | 'global';
|
|
727
759
|
/** The JSON document the plugin card renders. */
|
|
728
|
-
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
|
+
{
|
|
729
771
|
status: 'signed-out';
|
|
730
772
|
accounts: readonly WorkBuddyWebAccount[];
|
|
731
773
|
message?: string;
|
|
774
|
+
/** The persisted account id matches no local account. */
|
|
775
|
+
selectionLost?: boolean;
|
|
732
776
|
} | {
|
|
733
777
|
status: 'signed-in';
|
|
734
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",
|