dsh-connect-workbuddy 2.0.4 → 2.0.5
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 +46 -0
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +187 -54
- package/lib/{host-heartbeat-CPIHUxKL.js → host-heartbeat-BOrNj3pY.js} +14 -1
- package/lib/index.d.ts +62 -1
- package/lib/index.js +67 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.5 (2026-09-22)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **「跟随 App 当前登录」清除后不再被旧版 `accountId` 顶掉,且清除结果在卡片上可见**(issue #11 的缺陷 1;其症状属实,但所声称的机制与严重性不准确——见下方「核查」)。
|
|
8
|
+
|
|
9
|
+
- **空字符串哨兵现在显式终止旧值回退**。`selectAccountFor()`(由 `applySelection` 内联逻辑提升为导出纯函数)按顺序判定:`accounts.<region> === ""` → 无选择(跟随 App);具体 id → 该区域的显式选择;键不存在 → 回退到所属区域的旧版 `accountId`。此前 `""` 走的是「值已存在」分支并被 `selectAccount('')` 归一化为 `undefined`,结果碰巧正确,但语义靠下游兜底;任何一处偏离(下游不再归一化、或增加新的读取方)都会让旧账号复活。现在哨兵在源头终止回退,并有测试锁定。
|
|
10
|
+
- **启动时的旧值归属(attribution)跳过被显式清除的区域**(`legacyAttributionRegion()`)。该 pass 在启动后异步运行:跳过被清除的区域,既避免把用户刚丢弃的选择重新绑回,也保证**另一个区域仍有机会认领同一个 id**(用 `continue` 而非 `break`,否则国际版的迁移会被国内版的清除顺手打断)。
|
|
11
|
+
- **卡片新增区域状态行**:显示该区域当前是「使用保存的账号」还是「跟随 App 当前登录」,清除后先显示确认文案。这是 issue #11 里真正属于产品缺陷的那一半——清除后回退到的默认账号**通常正是刚被清掉的那个账号**(用户当初就是在 App 登录状态下选它的),因此清除成功与清除无效在界面上完全同形。此前 `usage` 文档也没有任何字段能区分二者。
|
|
12
|
+
- **设置写入改为「写入后回读校验」,失败时明确报错**(Windows 上尤其重要,见下)。`settingsScope.set()` 的 promise resolve **并不代表值已落盘**:设置文档 `settings.yaml` 靠「写临时文件 + rename 覆盖」替换,而 `@deepseek-ai/dsh-atomic-write` 对 `EPERM`/`EBUSY`/`EACCES` 的重试**只在 `win32` 生效**——Windows 上杀毒软件、OneDrive 等同步盘或编辑器会短暂锁住该文件。重试耗尽后,失败以「不成功的响应」回到客户端,而设置 scope 的 `mutate()` 对此**只是重新加载 Host 状态然后正常返回**,所以调用方的 `await` 会成功、文档却没变。新增 `writeAccountSlot()` 与 `writeRegionModels()`:写入后把值读回,读不回即抛 `WorkBuddySettingsWriteError`。对三处写入的影响:
|
|
13
|
+
- **清除账号**:只有确认落盘才显示「已清除」,失败则显示错误并说明原选择仍生效——否则会给出一个被旧选择默默反驳的假确认,比不提示更糟;
|
|
14
|
+
- **切换账号**:同样校验。静默丢弃会让下拉框显示一个账号、实际计费另一个;
|
|
15
|
+
- **保存模型**:尤其要紧——保存成功会**丢弃草稿**,若写入没落盘就丢,用户唯一的副本就没了,却还被告知「已保存」。校验比对回读的 `lastCatalog` id 序列(不能只看「槽位存在」,截断/残缺的写入同样要检出)。
|
|
16
|
+
|
|
17
|
+
**核查**:报告的根因链在 HEAD 上不成立,逐条复现如下。**注意措辞更正**:2.0.5 初稿曾断言「清除本来就生效」,该结论只在 Host 侧成立;在 Windows 上写入可能被静默丢弃,因此报告的「点了没反应」在那里是真实存在的,只是机制与报告所述不同(不是 `accountId` 顶掉,而是写入没落盘)。回读校验修复的正是后者。
|
|
18
|
+
|
|
19
|
+
| 报告的主张 | 实测结果 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `clearAccount()` 只写区域槽位,旧 `accountId` 会持续顶掉清除结果 | 清除后该区域立即跟随 App 默认;三种变体(旧账号≠当前登录、旧账号=当前登录、启动归属与清除竞争)均通过 |
|
|
22
|
+
| `effectiveAccountFor()` 在哨兵值上不提前返回,因此旧值继续生效 | 该分支返回 `""`,经 `selectAccount('')` 归一化为 `undefined`,**不会**回退到 `accountId`——返回值虽然「不提前」,但结果正确 |
|
|
23
|
+
| 启动迁移块会把区域重新绑回旧账号 | 迁移块只在启动时运行一次,且 `applySelection` 走同一个 `effectiveAccountFor`,同样被哨兵挡住 |
|
|
24
|
+
| `""` 与「未设置」在 schema 层不可区分 | `""` 与键缺失**是可区分的**:`z.object({cn: z.string()})` 对 `{cn: ''}` 保留空串、对 `{}` 保留缺键;`Object.hasOwn` 亦为真。不可区分的只有**语义**,不是 schema |
|
|
25
|
+
| (报告未提及)写入可能根本不生效 | **成立,且与平台相关**:见上一条。空串本身在 YAML 里能正确往返(写成 `cn: ""`),所以哨兵语义没问题;丢的是**整次写入** |
|
|
26
|
+
|
|
27
|
+
因此本版本不按报告的建议去「清除旧字段」——旧字段是**两个区域共享的**迁移来源,清掉它会让另一侧失去迁移依据;正确的做法是让哨兵赢(已实现),并把无法观测的状态暴露给界面(已实现),再加上对「写入没生效」的显式检测。
|
|
28
|
+
|
|
29
|
+
### Tests
|
|
30
|
+
|
|
31
|
+
测试总数 142 → 173(新增 31 例,**全套 173 通过**)。
|
|
32
|
+
|
|
33
|
+
> 关于 `host-heartbeat.spec.ts` 的 `processStartTimeMs`:在受限沙箱下该例会失败
|
|
34
|
+
> (`processStartTimeMs` 需要执行 `ps`,被沙箱以 `EPERM` 拒绝 → 返回 `undefined`)。
|
|
35
|
+
> 放开文件策略后 `ps` 可用,**5/5 通过**。该例与本次改动无关——两次运行用同一份
|
|
36
|
+
> 源码,差异只来自沙箱能否执行 `ps`。
|
|
37
|
+
|
|
38
|
+
- `settings-integration.spec.ts` 新增 15 例:`selectAccountFor` 的 6 例(显式选择优先、旧值只归属本区域、哨兵终止回退、清除与未配置可区分、两区域同时清除、清除一侧不影响另一侧);`legacyAttributionRegion` 的 6 例(归属、跳过已清除区域、跳过但继续扫描另一区域、账号已消失、无旧字段、跨区域共享 id);另有 3 例走真实 settings 链路 —— 清除后旧字段仍在但选不中、**重启后**已清除区域不被重新归属、以及清除前后经 usage 路由可见 `selectionExplicit` 翻转(账号名不变,这正是报告里「看起来没反应」的场景)。
|
|
39
|
+
- `client-account-selection.spec.ts` 新增 13 例(新文件):`configuredAccountsOf` 的容错读取;`writeAccountSlot` 的保存与「保留另一区域」、哨兵写成 `""` 而非删键、**锁文件导致的静默未落盘必须抛错**、错误携带字段名、**不把「键缺失」误判为清除成功**、正常落盘不误报、回读与卡片读取同源(scope 的 flush 默认 `sync`,落盘后同步可见,故不会误失败);`writeRegionModels` 的保存与区域保留、**静默未落盘必须抛错**、目录被截断也要检出(不能只看「槽位存在」)、错误字段名、正常落盘不误报。
|
|
40
|
+
- `auth.spec.ts` 新增 1 例:`hasExplicitSelection()` 在未选择/已选择/选择失效/哨兵清除四种状态下的取值。
|
|
41
|
+
- `web-status.spec.ts` 新增 2 例:signed-in 与两个 signed-out 分支都携带 `selectionExplicit`。
|
|
42
|
+
|
|
43
|
+
每条新增守卫都用「删掉对应实现即失败」逐一验证过:移除哨兵判断会挂 5 例,移除归属跳过会挂 2 例,把 `selectionExplicit` 写死为 `true` 会挂 3 例,把账号回读校验换成「相信 resolve」会挂 3 例,去掉模型保存的回读校验会挂 3 例。
|
|
44
|
+
|
|
45
|
+
### Docs
|
|
46
|
+
|
|
47
|
+
- `README.md` / `README.en.md` 的「已知限制 / Known limitations」补充:旧版 `accountId` 只作用于所属区域、且该区域被显式清除后不再生效;卡片状态行用于分辨清除是否生效;**Windows 上设置文件被占用会导致账号改动无法保存**,此时卡片报错且原选择保持生效。
|
|
48
|
+
|
|
3
49
|
## 2.0.4 (2026-09-21)
|
|
4
50
|
|
|
5
51
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -116,6 +116,8 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
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
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.
|
|
119
|
+
- Users upgrading from before 2.0.0 may still carry the old top-level `accountId` (the migration source). It applies only to its own region and **stops applying once that region is explicitly cleared (`accounts.<region> = ""`)** — otherwise the account you cleared would be silently restored at the next startup. The card shows whether a region is "using the saved account" or "following the app's current sign-in": the restored default is usually the very account you just cleared, so without that line there is no way to tell a successful clear from a no-op.
|
|
120
|
+
- **On Windows, an account change may fail to save while `settings.yaml` is held open.** DSH updates that file by writing a temp file and replacing the target; an antivirus scanner, a sync client (OneDrive), or an open editor can lock it briefly, and once the replacement retries are exhausted the failed write does **not** make `set()` reject. The plugin therefore reads the value back after writing: it shows "cleared" **only once the write is confirmed on disk**, and on failure it reports the error and keeps your previous choice (never a false confirmation that the old selection silently contradicts). Close whatever holds the file and retry.
|
|
119
121
|
- 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.
|
|
120
122
|
|
|
121
123
|
## Disclaimer
|
package/README.md
CHANGED
|
@@ -124,6 +124,8 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
|
124
124
|
- 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。
|
|
125
125
|
- **账号切换**基于 WorkBuddy 桌面端留下的历史 auth 文件(App 自身的备份产物),并非官方多账号 API。默认行为仍是跟随 App 当前登录;切换账号是显式选项,且历史凭据可能因 App 清理或退出登录而失效。
|
|
126
126
|
- 显式选择的账号若在本机消失(App 更换登录或清理备份文件),插件**不会**静默改选其他账号——那样会让账单落到另一个账号上。卡片会明确说明「保存的账号已不存在」,此时重新选择一个账号,或点「跟随 App 当前登录」清除该选择即可恢复。**重新登录桌面端 App 无法修复这种状态**:凭据本身是好的,失效的只是保存下来的账号标识。
|
|
127
|
+
- 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
|
|
128
|
+
- **Windows:设置文件 `settings.yaml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
|
|
127
129
|
- Windows / Linux 的凭据默认路径为按平台约定推导,未经真机验证;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
|
|
128
130
|
|
|
129
131
|
## 免责声明
|
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-BOrNj3pY.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
|
@@ -57,6 +57,92 @@ window.__ModuleLoader__.load({
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
//#endregion
|
|
60
|
+
//#region src/client/account-selection.ts
|
|
61
|
+
/**
|
|
62
|
+
* A settings write that did not take effect.
|
|
63
|
+
*
|
|
64
|
+
* Distinct from a rejected `set()`: this is thrown when the write reported
|
|
65
|
+
* success and the value is nonetheless absent from the document.
|
|
66
|
+
*/
|
|
67
|
+
var WorkBuddySettingsWriteError = class extends Error {
|
|
68
|
+
/** The settings field that did not land. */
|
|
69
|
+
field;
|
|
70
|
+
constructor(field) {
|
|
71
|
+
super(`workbuddy: settings field "${field}" was not persisted by the settings write`);
|
|
72
|
+
this.name = "WorkBuddySettingsWriteError";
|
|
73
|
+
this.field = field;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Read the per-region account selections out of the settings snapshot. */
|
|
77
|
+
function configuredAccountsOf(configured) {
|
|
78
|
+
const accounts = configured?.accounts;
|
|
79
|
+
return typeof accounts === "object" && accounts !== null ? accounts : {};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Write one region's account slot, then confirm the value actually landed.
|
|
83
|
+
*
|
|
84
|
+
* `settingsScope.set()` resolving is NOT proof that anything was stored. The
|
|
85
|
+
* Host's document is `settings.yaml`, replaced by writing a temp file and
|
|
86
|
+
* renaming it over the target; on Windows an antivirus scanner, a sync client
|
|
87
|
+
* (OneDrive, Dropbox), or an open editor can hold the file briefly.
|
|
88
|
+
* `@deepseek-ai/dsh-atomic-write` retries `EPERM` / `EBUSY` / `EACCES` a
|
|
89
|
+
* bounded number of times and those retries exist only for `win32` — and once
|
|
90
|
+
* they are exhausted the failure reaches the client as an unsuccessful
|
|
91
|
+
* response, which the settings scope handles by reloading Host state and
|
|
92
|
+
* merely RETURNING. The caller's `await` therefore succeeds while the document
|
|
93
|
+
* is unchanged.
|
|
94
|
+
*
|
|
95
|
+
* Reading the field back is the only reliable check, and it is what the
|
|
96
|
+
* official plugin cards do (`CardForm.store()` compares the user layer against
|
|
97
|
+
* the value it wrote). Failing loudly matters most on Clear: a false success
|
|
98
|
+
* would flip the card to "following the app's sign-in" while the old selection
|
|
99
|
+
* keeps running — the exact confusion that state line exists to remove.
|
|
100
|
+
*
|
|
101
|
+
* @param scope - the bound settings scope for this plugin's namespace.
|
|
102
|
+
* @param region - the region whose slot is written; the other is preserved.
|
|
103
|
+
* @param value - the account id to save, or `''` to clear the region.
|
|
104
|
+
* @throws {WorkBuddySettingsWriteError} when the value is readable back as
|
|
105
|
+
* something else — i.e. the write silently did not persist.
|
|
106
|
+
*/
|
|
107
|
+
async function writeAccountSlot(scope, region, value) {
|
|
108
|
+
const accounts = configuredAccountsOf(scope.getSnapshot().value);
|
|
109
|
+
await scope.set("accounts", {
|
|
110
|
+
...accounts,
|
|
111
|
+
[region]: value
|
|
112
|
+
});
|
|
113
|
+
if (configuredAccountsOf(scope.getSnapshot().value)[region] !== value) throw new WorkBuddySettingsWriteError("accounts");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Write one region's saved model state, then confirm it actually landed.
|
|
117
|
+
*
|
|
118
|
+
* The same silent-failure mode as {@link writeAccountSlot} applies to EVERY
|
|
119
|
+
* settings write, not just the account slot. It matters here for a different
|
|
120
|
+
* reason: a successful save discards the user's draft, so a write that did not
|
|
121
|
+
* persist makes the card throw away unsaved edits while claiming they were
|
|
122
|
+
* saved — unrecoverable, since the drafts are the only copy.
|
|
123
|
+
*
|
|
124
|
+
* Verification compares the round-tripped `lastCatalog` ids, which is the part
|
|
125
|
+
* of the payload that is both user-visible and cheap to compare; the selection
|
|
126
|
+
* fields ride along in the same object and cannot land separately.
|
|
127
|
+
*
|
|
128
|
+
* @param scope - the bound settings scope for this plugin's namespace.
|
|
129
|
+
* @param region - the region whose model slot is written.
|
|
130
|
+
* @param payload - the complete next state for that region's slot.
|
|
131
|
+
* @throws {WorkBuddySettingsWriteError} when the catalog does not read back.
|
|
132
|
+
*/
|
|
133
|
+
async function writeRegionModels(scope, region, payload) {
|
|
134
|
+
const configured = scope.getSnapshot().value?.regions;
|
|
135
|
+
const regions = typeof configured === "object" && configured !== null ? configured : {};
|
|
136
|
+
await scope.set("regions", {
|
|
137
|
+
...regions,
|
|
138
|
+
[region]: payload
|
|
139
|
+
});
|
|
140
|
+
const landed = scope.getSnapshot().value?.regions?.[region];
|
|
141
|
+
const written = payload.lastCatalog.map((model) => model.id);
|
|
142
|
+
const stored = landed?.lastCatalog?.map((model) => model.id);
|
|
143
|
+
if (!(stored !== void 0 && stored.length === written.length && stored.every((id, index) => id === written[index]))) throw new WorkBuddySettingsWriteError("regions");
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
60
146
|
//#region src/client/icon.ts
|
|
61
147
|
/**
|
|
62
148
|
* Plugin icon (LD brand logo, 64px) shared across the LaoDing plugin family.
|
|
@@ -117,7 +203,10 @@ window.__ModuleLoader__.load({
|
|
|
117
203
|
.dsm-workbuddy-usage-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
118
204
|
.dsm-workbuddy-usage-expiry{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
|
|
119
205
|
.dsm-workbuddy-usage-hint{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
|
|
120
|
-
.dsm-workbuddy-account-picker{display:
|
|
206
|
+
.dsm-workbuddy-account-picker{display:flex;flex-direction:column;gap:8px}
|
|
207
|
+
.dsm-workbuddy-account-state{color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
|
|
208
|
+
.dsm-workbuddy-account-state:empty{display:none}
|
|
209
|
+
.dsm-workbuddy-account-error{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:12px;line-height:18px;white-space:pre-line}
|
|
121
210
|
.dsm-workbuddy-usage-select-wrap{position:relative}
|
|
122
211
|
.dsm-workbuddy-usage-select{appearance:none;width:100%;font:inherit;padding:10px 34px 10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-3,#2a2c33);cursor:pointer;transition:border-color .15s,box-shadow .15s}
|
|
123
212
|
.dsm-workbuddy-usage-select:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary,#5686fe);box-shadow:0 0 0 3px rgba(86,134,254,.22)}
|
|
@@ -255,26 +344,30 @@ window.__ModuleLoader__.load({
|
|
|
255
344
|
function dotStyle(status) {
|
|
256
345
|
return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
|
|
257
346
|
}
|
|
258
|
-
/** Read the per-region account selections out of the settings snapshot. */
|
|
259
|
-
function configuredAccountsOf(configured) {
|
|
260
|
-
const accounts = configured?.accounts;
|
|
261
|
-
return typeof accounts === "object" && accounts !== null ? accounts : {};
|
|
262
|
-
}
|
|
263
347
|
/** Render WorkBuddy sign-in state, credits, and model selection as one card. */
|
|
264
348
|
function WorkBuddyCard({ t, settingsScope }) {
|
|
265
349
|
if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
|
|
266
350
|
const [open, setOpen] = (0, react.useState)(false);
|
|
267
351
|
/** The region whose tab is on screen; each tab is its own provider stack. */
|
|
268
352
|
const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
|
|
269
|
-
/**
|
|
353
|
+
/**
|
|
354
|
+
* Last-known usage per region, so tab dots survive tab switches.
|
|
355
|
+
*
|
|
356
|
+
* The placeholder carries `selectionExplicit: false` because that is the
|
|
357
|
+
* state the plugin documents as the default before any choice is saved. It
|
|
358
|
+
* is never rendered: the account picker and its state line only appear once
|
|
359
|
+
* a real fetch has populated `accounts`.
|
|
360
|
+
*/
|
|
270
361
|
const [statusByRegion, setStatusByRegion] = (0, react.useState)({
|
|
271
362
|
cn: {
|
|
272
363
|
status: "signed-out",
|
|
273
|
-
accounts: []
|
|
364
|
+
accounts: [],
|
|
365
|
+
selectionExplicit: false
|
|
274
366
|
},
|
|
275
367
|
global: {
|
|
276
368
|
status: "signed-out",
|
|
277
|
-
accounts: []
|
|
369
|
+
accounts: [],
|
|
370
|
+
selectionExplicit: false
|
|
278
371
|
}
|
|
279
372
|
});
|
|
280
373
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
@@ -286,6 +379,10 @@ window.__ModuleLoader__.load({
|
|
|
286
379
|
/** Save failure surfaced next to the buttons; cleared by the next attempt. */
|
|
287
380
|
const [saveError, setSaveError] = (0, react.useState)(void 0);
|
|
288
381
|
const [switchingAccount, setSwitchingAccount] = (0, react.useState)(false);
|
|
382
|
+
/** Set right after a CONFIRMED Clear so the dropped choice is stated. */
|
|
383
|
+
const [accountNote, setAccountNote] = (0, react.useState)(void 0);
|
|
384
|
+
/** A refused account write (silently unpersisted settings on a locked file). */
|
|
385
|
+
const [accountError, setAccountError] = (0, react.useState)(void 0);
|
|
289
386
|
const [checkingIn, setCheckingIn] = (0, react.useState)(false);
|
|
290
387
|
const [checkinActionError, setCheckinActionError] = (0, react.useState)(void 0);
|
|
291
388
|
const mounted = (0, react.useRef)(true);
|
|
@@ -338,7 +435,8 @@ window.__ModuleLoader__.load({
|
|
|
338
435
|
]);
|
|
339
436
|
const status = statusByRegion[activeRegion] ?? {
|
|
340
437
|
status: "signed-out",
|
|
341
|
-
accounts: []
|
|
438
|
+
accounts: [],
|
|
439
|
+
selectionExplicit: false
|
|
342
440
|
};
|
|
343
441
|
(0, react.useEffect)(() => {
|
|
344
442
|
if (!open || status.status !== "signed-in") return;
|
|
@@ -387,13 +485,13 @@ window.__ModuleLoader__.load({
|
|
|
387
485
|
const switchAccount = async (accountId) => {
|
|
388
486
|
if (settingsScope === void 0) return;
|
|
389
487
|
setSwitchingAccount(true);
|
|
488
|
+
setAccountNote(void 0);
|
|
489
|
+
setAccountError(void 0);
|
|
390
490
|
try {
|
|
391
|
-
|
|
392
|
-
await settingsScope.set("accounts", {
|
|
393
|
-
...configuredAccounts,
|
|
394
|
-
[activeRegion]: accountId
|
|
395
|
-
});
|
|
491
|
+
await writeAccountSlot(settingsScope, activeRegion, accountId);
|
|
396
492
|
await refreshUsage(activeRegion);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
397
495
|
} finally {
|
|
398
496
|
if (mounted.current) setSwitchingAccount(false);
|
|
399
497
|
}
|
|
@@ -402,18 +500,29 @@ window.__ModuleLoader__.load({
|
|
|
402
500
|
* Drop the explicit choice so the region returns to its documented default:
|
|
403
501
|
* follow whatever the WorkBuddy app is currently signed in as. The empty
|
|
404
502
|
* string is the settings-level sentinel for "no explicit selection" (the
|
|
405
|
-
* store normalizes it away
|
|
503
|
+
* store normalizes it away, and the host's legacy attribution treats it as a
|
|
504
|
+
* deliberate clear rather than an unset key); the account id itself is never
|
|
505
|
+
* written here.
|
|
506
|
+
*
|
|
507
|
+
* The confirmation is only shown once the write is CONFIRMED. That matters
|
|
508
|
+
* twice over: the restored default is usually the very account that was
|
|
509
|
+
* saved, so the state line is the only feedback the user gets — and on
|
|
510
|
+
* Windows the atomic replace of `settings.yaml` can fail outright (locked by
|
|
511
|
+
* an antivirus scanner or a sync client), in which case claiming "cleared"
|
|
512
|
+
* would be a lie that the old selection silently contradicts.
|
|
406
513
|
*/
|
|
407
514
|
const clearAccount = async () => {
|
|
408
515
|
if (settingsScope === void 0) return;
|
|
409
516
|
setSwitchingAccount(true);
|
|
517
|
+
setAccountError(void 0);
|
|
410
518
|
try {
|
|
411
|
-
|
|
412
|
-
await settingsScope.set("accounts", {
|
|
413
|
-
...configuredAccounts,
|
|
414
|
-
[activeRegion]: ""
|
|
415
|
-
});
|
|
519
|
+
await writeAccountSlot(settingsScope, activeRegion, "");
|
|
416
520
|
await refreshUsage(activeRegion);
|
|
521
|
+
if (mounted.current) setAccountNote("cleared");
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (!mounted.current) return;
|
|
524
|
+
setAccountNote(void 0);
|
|
525
|
+
setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
417
526
|
} finally {
|
|
418
527
|
if (mounted.current) setSwitchingAccount(false);
|
|
419
528
|
}
|
|
@@ -542,15 +651,11 @@ window.__ModuleLoader__.load({
|
|
|
542
651
|
setSaving(true);
|
|
543
652
|
setSaveError(void 0);
|
|
544
653
|
try {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
[
|
|
549
|
-
|
|
550
|
-
enabledModelIds: [...activeEnabledIds],
|
|
551
|
-
imageModelIds: [...activeImageIds],
|
|
552
|
-
contextBudgets: activeContextBudgets
|
|
553
|
-
}
|
|
654
|
+
await writeRegionModels(settingsScope, status.region, {
|
|
655
|
+
lastCatalog: visibleModels.map(toPersistedWorkBuddyModel),
|
|
656
|
+
enabledModelIds: [...activeEnabledIds],
|
|
657
|
+
imageModelIds: [...activeImageIds],
|
|
658
|
+
contextBudgets: activeContextBudgets
|
|
554
659
|
});
|
|
555
660
|
discardModels();
|
|
556
661
|
await refreshUsage(activeRegion);
|
|
@@ -564,6 +669,11 @@ window.__ModuleLoader__.load({
|
|
|
564
669
|
const label = status.status === "signed-in" ? t("row.signedIn", { accountName: status.accountName }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
|
|
565
670
|
/** The saved choice no longer matches a local sign-in (tokens are fine). */
|
|
566
671
|
const selectionLost = status.status === "signed-out" && status.selectionLost === true;
|
|
672
|
+
/**
|
|
673
|
+
* Whether this region runs a saved choice, per the Host. `undefined` only on
|
|
674
|
+
* the error branch, which renders no picker and therefore no state line.
|
|
675
|
+
*/
|
|
676
|
+
const selectionExplicit = status.status === "error" ? void 0 : status.selectionExplicit;
|
|
567
677
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
568
678
|
className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
|
|
569
679
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -615,6 +725,8 @@ window.__ModuleLoader__.load({
|
|
|
615
725
|
className: `dsm-workbuddy-tab${region === activeRegion ? " dsm-workbuddy-tab-active" : ""}`,
|
|
616
726
|
onClick: () => {
|
|
617
727
|
setActiveRegion(region);
|
|
728
|
+
setAccountNote(void 0);
|
|
729
|
+
setAccountError(void 0);
|
|
618
730
|
},
|
|
619
731
|
children: [regionStatus === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
620
732
|
"aria-hidden": "true",
|
|
@@ -668,33 +780,46 @@ window.__ModuleLoader__.load({
|
|
|
668
780
|
status.status !== "error" && status.accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
669
781
|
className: "dsm-workbuddy-account-picker",
|
|
670
782
|
"aria-label": t("row.accountsTitle"),
|
|
671
|
-
children: [
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
783
|
+
children: [
|
|
784
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
785
|
+
className: "dsm-workbuddy-usage-select-wrap",
|
|
786
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
787
|
+
className: "dsm-workbuddy-usage-select",
|
|
788
|
+
value: status.accounts.find((account) => account.selected)?.id ?? "",
|
|
789
|
+
disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
|
|
790
|
+
onChange: (event) => {
|
|
791
|
+
switchAccount(event.currentTarget.value);
|
|
792
|
+
},
|
|
793
|
+
children: [status.accounts.some((account) => account.selected) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
794
|
+
value: "",
|
|
795
|
+
disabled: true,
|
|
796
|
+
children: t("row.accountNoneInEffect")
|
|
797
|
+
}), status.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
798
|
+
value: account.id,
|
|
799
|
+
children: [account.accountName, account.domain === "" ? "" : ` · ${account.domain}`]
|
|
800
|
+
}, account.id))]
|
|
801
|
+
})
|
|
802
|
+
}),
|
|
803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
804
|
+
type: "button",
|
|
805
|
+
className: "dsm-btn dsm-btn-outline",
|
|
676
806
|
disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
|
|
677
|
-
|
|
678
|
-
|
|
807
|
+
onClick: () => {
|
|
808
|
+
clearAccount();
|
|
679
809
|
},
|
|
680
|
-
children:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
810
|
+
children: t("row.accountsFollowApp")
|
|
811
|
+
}),
|
|
812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
813
|
+
className: "dsm-workbuddy-account-state",
|
|
814
|
+
role: "status",
|
|
815
|
+
children: accountNote === "cleared" ? t("row.accountsCleared") : selectionExplicit === false ? t("row.accountsFollowingApp") : selectionExplicit === true ? t("row.accountsSavedChoice") : ""
|
|
816
|
+
}),
|
|
817
|
+
accountError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
818
|
+
className: "dsm-workbuddy-account-error",
|
|
819
|
+
role: "alert",
|
|
820
|
+
children: t("row.accountsWriteFailed", { message: accountError })
|
|
688
821
|
})
|
|
689
|
-
|
|
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
|
-
})]
|
|
822
|
+
]
|
|
698
823
|
}) : null,
|
|
699
824
|
status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
700
825
|
status.credits === void 0 ? null : (() => {
|
|
@@ -975,6 +1100,10 @@ window.__ModuleLoader__.load({
|
|
|
975
1100
|
"row.accountsScanning": "Detecting…",
|
|
976
1101
|
"row.accountsFollowApp": "Follow the app's sign-in",
|
|
977
1102
|
"row.accountNoneInEffect": "No account in effect",
|
|
1103
|
+
"row.accountsSavedChoice": "Using the account saved for this region.",
|
|
1104
|
+
"row.accountsFollowingApp": "Following the app's current sign-in — no account is saved for this region.",
|
|
1105
|
+
"row.accountsCleared": "Saved choice cleared. This region follows the app's current sign-in again.",
|
|
1106
|
+
"row.accountsWriteFailed": "The account change was not saved: {message} — the settings file could not be updated (on Windows it may be locked by an antivirus scanner or a sync client). Your previous choice is still in effect; close whatever holds settings.yaml and try again.",
|
|
978
1107
|
"row.selectionLostHint": "The saved account no longer exists locally.",
|
|
979
1108
|
"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.",
|
|
980
1109
|
"row.modelsTitle": "Models",
|
|
@@ -1027,6 +1156,10 @@ window.__ModuleLoader__.load({
|
|
|
1027
1156
|
"row.accountsScanning": "正在检测…",
|
|
1028
1157
|
"row.accountsFollowApp": "跟随 App 当前登录",
|
|
1029
1158
|
"row.accountNoneInEffect": "当前没有生效的账号",
|
|
1159
|
+
"row.accountsSavedChoice": "正在使用该区域保存的账号。",
|
|
1160
|
+
"row.accountsFollowingApp": "正在跟随 App 当前登录——该区域没有保存账号。",
|
|
1161
|
+
"row.accountsCleared": "已清除保存的选择,该区域重新跟随 App 当前登录。",
|
|
1162
|
+
"row.accountsWriteFailed": "账号改动未保存:{message} —— 设置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用 settings.yaml 的程序再重试。",
|
|
1030
1163
|
"row.selectionLostHint": "已保存的账号在本机已不存在。",
|
|
1031
1164
|
"row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方选择一个,或点「跟随 App 当前登录」清除已保存的选择。重新登录桌面端 App 无法修复此问题。",
|
|
1032
1165
|
"row.modelsTitle": "模型",
|
|
@@ -931,6 +931,19 @@ var WorkBuddyCredentialStore = class {
|
|
|
931
931
|
selectedAccountId() {
|
|
932
932
|
return this.accountId;
|
|
933
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Whether the region runs a SAVED choice rather than the documented default.
|
|
936
|
+
*
|
|
937
|
+
* `false` means "follow the app's current sign-in", which is also what the
|
|
938
|
+
* card's Clear action restores. The two states can resolve to the very same
|
|
939
|
+
* account — an upgraded user whose pre-split `accountId` happens to be the
|
|
940
|
+
* app's current sign-in clears the choice and sees no change at all — so the
|
|
941
|
+
* card cannot infer this from the account list alone. It reports the state
|
|
942
|
+
* here so clearing is observable instead of looking like a dead button.
|
|
943
|
+
*/
|
|
944
|
+
hasExplicitSelection() {
|
|
945
|
+
return this.accountId !== void 0;
|
|
946
|
+
}
|
|
934
947
|
/** The auth-file path candidates, in probe order. */
|
|
935
948
|
resolveDesktopCandidates() {
|
|
936
949
|
const fromEnv = process.env[WORKBUDDY_AUTH_FILE_ENV];
|
|
@@ -1451,7 +1464,7 @@ var WorkBuddyCatalog = class {
|
|
|
1451
1464
|
//#endregion
|
|
1452
1465
|
//#region src/version.ts
|
|
1453
1466
|
/** The npm package version this build was produced from. */
|
|
1454
|
-
const WORKBUDDY_CONNECT_VERSION = "2.0.
|
|
1467
|
+
const WORKBUDDY_CONNECT_VERSION = "2.0.5";
|
|
1455
1468
|
//#endregion
|
|
1456
1469
|
//#region src/host-heartbeat.ts
|
|
1457
1470
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -187,6 +187,17 @@ declare class WorkBuddyCredentialStore {
|
|
|
187
187
|
selectAccount(accountId: string | undefined): void;
|
|
188
188
|
/** Selected account id, for diagnostics and route assembly. */
|
|
189
189
|
selectedAccountId(): string | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Whether the region runs a SAVED choice rather than the documented default.
|
|
192
|
+
*
|
|
193
|
+
* `false` means "follow the app's current sign-in", which is also what the
|
|
194
|
+
* card's Clear action restores. The two states can resolve to the very same
|
|
195
|
+
* account — an upgraded user whose pre-split `accountId` happens to be the
|
|
196
|
+
* app's current sign-in clears the choice and sees no change at all — so the
|
|
197
|
+
* card cannot infer this from the account list alone. It reports the state
|
|
198
|
+
* here so clearing is observable instead of looking like a dead button.
|
|
199
|
+
*/
|
|
200
|
+
hasExplicitSelection(): boolean;
|
|
190
201
|
/** The auth-file path candidates, in probe order. */
|
|
191
202
|
private resolveDesktopCandidates;
|
|
192
203
|
/** The resolved desktop auth-file path, for diagnostics. */
|
|
@@ -766,6 +777,11 @@ type WorkBuddyWebUsage =
|
|
|
766
777
|
* sign-in again), versus nothing usable was found at all (sign in in the
|
|
767
778
|
* desktop app). Only the latter makes the "sign in again" hint truthful —
|
|
768
779
|
* re-signing in does not repair an orphaned id.
|
|
780
|
+
*
|
|
781
|
+
* `selectionExplicit` answers a question the account list cannot: whether
|
|
782
|
+
* the region runs a saved choice at all. Clearing restores the default, and
|
|
783
|
+
* when the default resolves to the same account the card would otherwise
|
|
784
|
+
* look unchanged — the "clear did nothing" report.
|
|
769
785
|
*/
|
|
770
786
|
{
|
|
771
787
|
status: 'signed-out';
|
|
@@ -773,6 +789,8 @@ type WorkBuddyWebUsage =
|
|
|
773
789
|
message?: string;
|
|
774
790
|
/** The persisted account id matches no local account. */
|
|
775
791
|
selectionLost?: boolean;
|
|
792
|
+
/** A saved per-region choice is in effect (false = following the app). */
|
|
793
|
+
selectionExplicit: boolean;
|
|
776
794
|
} | {
|
|
777
795
|
status: 'signed-in';
|
|
778
796
|
accountId: string;
|
|
@@ -783,6 +801,8 @@ type WorkBuddyWebUsage =
|
|
|
783
801
|
region: WorkBuddyWebRegion;
|
|
784
802
|
source?: 'desktop' | 'dsh';
|
|
785
803
|
tokenExpiresAtMs: number;
|
|
804
|
+
/** A saved per-region choice is in effect (false = following the app). */
|
|
805
|
+
selectionExplicit: boolean;
|
|
786
806
|
accounts: readonly WorkBuddyWebAccount[];
|
|
787
807
|
models: readonly WorkBuddyWebModel[];
|
|
788
808
|
enabledModelIds: readonly string[];
|
|
@@ -898,6 +918,47 @@ declare const Config: z<Config>;
|
|
|
898
918
|
* picks.
|
|
899
919
|
*/
|
|
900
920
|
declare function regionStateOf(config: Config, region: WorkBuddyRegion): WorkBuddyRegionState;
|
|
921
|
+
/**
|
|
922
|
+
* The account id a region should select, or `undefined` for the documented
|
|
923
|
+
* default: follow whatever the WorkBuddy app is currently signed in as.
|
|
924
|
+
*
|
|
925
|
+
* Three inputs decide it, and the ORDER is the whole point:
|
|
926
|
+
*
|
|
927
|
+
* 1. `accounts[region] === ''` — the Clear sentinel. It means "the user
|
|
928
|
+
* dropped this region's choice", which is the opposite of the key being
|
|
929
|
+
* absent ("never configured"). It therefore terminates the lookup: falling
|
|
930
|
+
* through to the legacy `accountId` here would re-bind the very account the
|
|
931
|
+
* user just dropped, and would do it after clearing appeared to succeed.
|
|
932
|
+
* 2. `accounts[region]` set to a real id — an explicit per-region choice.
|
|
933
|
+
* 3. absent key, with the pre-split `accountId` belonging to this region —
|
|
934
|
+
* the legacy migration, attributed once at startup from the local scan.
|
|
935
|
+
*
|
|
936
|
+
* `legacyAccountRegion` is resolved asynchronously after the first
|
|
937
|
+
* `applySelection` pass, so this is a pure function of it rather than a
|
|
938
|
+
* closure over the stores: the same call answers both the early pass (no
|
|
939
|
+
* attribution yet) and every later one.
|
|
940
|
+
*/
|
|
941
|
+
declare function selectAccountFor(region: WorkBuddyRegion, value: Config, legacyAccountRegion: WorkBuddyRegion | undefined): string | undefined;
|
|
942
|
+
/** Whether a region carries the Clear sentinel rather than a saved choice. */
|
|
943
|
+
declare function regionCleared(value: Config, region: WorkBuddyRegion): boolean;
|
|
944
|
+
/**
|
|
945
|
+
* Which region owns the pre-split `accountId`, or `undefined` when it cannot
|
|
946
|
+
* be attributed (the field is absent, or the account is gone from every local
|
|
947
|
+
* sign-in list).
|
|
948
|
+
*
|
|
949
|
+
* Regions the user explicitly cleared are skipped, even when the saved account
|
|
950
|
+
* IS among their local sign-ins. `selectAccountFor` already refuses to fall
|
|
951
|
+
* back for a cleared region, so this cannot change what runs — but it keeps the
|
|
952
|
+
* attribution honest about its own decision: the region that owns this id is
|
|
953
|
+
* not the one the user just told the plugin to stop pinning, so the other
|
|
954
|
+
* region must get the chance to claim it.
|
|
955
|
+
*
|
|
956
|
+
* `accountsFor` is injected so the sequential, stop-at-first-match scan is
|
|
957
|
+
* testable without a filesystem.
|
|
958
|
+
*/
|
|
959
|
+
declare function legacyAttributionRegion(value: Config, accountsFor: (region: WorkBuddyRegion) => Promise<readonly {
|
|
960
|
+
id: string;
|
|
961
|
+
}[]>): Promise<WorkBuddyRegion | undefined>;
|
|
901
962
|
/**
|
|
902
963
|
* Start both regions' loopback endpoints, register the `workbuddy` (CN) and
|
|
903
964
|
* `workbuddy-global` (international) providers, and refresh each region's
|
|
@@ -907,4 +968,4 @@ declare function regionStateOf(config: Config, region: WorkBuddyRegion): WorkBud
|
|
|
907
968
|
*/
|
|
908
969
|
declare function apply(ctx: Context, config: Config): void;
|
|
909
970
|
//#endregion
|
|
910
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyReasoning, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebRegion, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
971
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyReasoning, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyWebAccount, type WorkBuddyWebCheckin, type WorkBuddyWebCredits, type WorkBuddyWebModel, type WorkBuddyWebPackage, type WorkBuddyWebRegion, type WorkBuddyWebUsage, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionCleared, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
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-BOrNj3pY.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";
|
|
@@ -561,9 +561,12 @@ function toWebAccount(account) {
|
|
|
561
561
|
async function workBuddyWebStatus(deps, region) {
|
|
562
562
|
const store = deps.store(region);
|
|
563
563
|
const accounts = await store.accounts();
|
|
564
|
-
|
|
564
|
+
const authStatus = await store.status();
|
|
565
|
+
const selectionExplicit = store.hasExplicitSelection();
|
|
566
|
+
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
565
567
|
status: "signed-out",
|
|
566
|
-
accounts: []
|
|
568
|
+
accounts: [],
|
|
569
|
+
selectionExplicit
|
|
567
570
|
};
|
|
568
571
|
let credential;
|
|
569
572
|
try {
|
|
@@ -573,6 +576,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
573
576
|
status: "signed-out",
|
|
574
577
|
accounts: accounts.map(toWebAccount),
|
|
575
578
|
message: safeMessage(error),
|
|
579
|
+
selectionExplicit,
|
|
576
580
|
...await store.selectionLost() ? { selectionLost: true } : {}
|
|
577
581
|
};
|
|
578
582
|
}
|
|
@@ -584,6 +588,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
584
588
|
region,
|
|
585
589
|
source: credential.source,
|
|
586
590
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
591
|
+
selectionExplicit,
|
|
587
592
|
accounts: accounts.map(toWebAccount),
|
|
588
593
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
589
594
|
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
@@ -759,6 +764,59 @@ function regionStateOf(config, region) {
|
|
|
759
764
|
/** Every region, in card tab order. */
|
|
760
765
|
const REGION_KEYS = ["cn", "global"];
|
|
761
766
|
/**
|
|
767
|
+
* The account id a region should select, or `undefined` for the documented
|
|
768
|
+
* default: follow whatever the WorkBuddy app is currently signed in as.
|
|
769
|
+
*
|
|
770
|
+
* Three inputs decide it, and the ORDER is the whole point:
|
|
771
|
+
*
|
|
772
|
+
* 1. `accounts[region] === ''` — the Clear sentinel. It means "the user
|
|
773
|
+
* dropped this region's choice", which is the opposite of the key being
|
|
774
|
+
* absent ("never configured"). It therefore terminates the lookup: falling
|
|
775
|
+
* through to the legacy `accountId` here would re-bind the very account the
|
|
776
|
+
* user just dropped, and would do it after clearing appeared to succeed.
|
|
777
|
+
* 2. `accounts[region]` set to a real id — an explicit per-region choice.
|
|
778
|
+
* 3. absent key, with the pre-split `accountId` belonging to this region —
|
|
779
|
+
* the legacy migration, attributed once at startup from the local scan.
|
|
780
|
+
*
|
|
781
|
+
* `legacyAccountRegion` is resolved asynchronously after the first
|
|
782
|
+
* `applySelection` pass, so this is a pure function of it rather than a
|
|
783
|
+
* closure over the stores: the same call answers both the early pass (no
|
|
784
|
+
* attribution yet) and every later one.
|
|
785
|
+
*/
|
|
786
|
+
function selectAccountFor(region, value, legacyAccountRegion) {
|
|
787
|
+
const configured = value.accounts?.[region];
|
|
788
|
+
if (configured === "") return void 0;
|
|
789
|
+
if (configured !== void 0) return configured;
|
|
790
|
+
return legacyAccountRegion === region ? value.accountId : void 0;
|
|
791
|
+
}
|
|
792
|
+
/** Whether a region carries the Clear sentinel rather than a saved choice. */
|
|
793
|
+
function regionCleared(value, region) {
|
|
794
|
+
return value.accounts?.[region] === "";
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Which region owns the pre-split `accountId`, or `undefined` when it cannot
|
|
798
|
+
* be attributed (the field is absent, or the account is gone from every local
|
|
799
|
+
* sign-in list).
|
|
800
|
+
*
|
|
801
|
+
* Regions the user explicitly cleared are skipped, even when the saved account
|
|
802
|
+
* IS among their local sign-ins. `selectAccountFor` already refuses to fall
|
|
803
|
+
* back for a cleared region, so this cannot change what runs — but it keeps the
|
|
804
|
+
* attribution honest about its own decision: the region that owns this id is
|
|
805
|
+
* not the one the user just told the plugin to stop pinning, so the other
|
|
806
|
+
* region must get the chance to claim it.
|
|
807
|
+
*
|
|
808
|
+
* `accountsFor` is injected so the sequential, stop-at-first-match scan is
|
|
809
|
+
* testable without a filesystem.
|
|
810
|
+
*/
|
|
811
|
+
async function legacyAttributionRegion(value, accountsFor) {
|
|
812
|
+
const id = value.accountId;
|
|
813
|
+
if (id === void 0) return void 0;
|
|
814
|
+
for (const region of REGION_KEYS) {
|
|
815
|
+
if (regionCleared(value, region)) continue;
|
|
816
|
+
if ((await accountsFor(region)).some((account) => account.id === id)) return region;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
762
820
|
* Start both regions' loopback endpoints, register the `workbuddy` (CN) and
|
|
763
821
|
* `workbuddy-global` (international) providers, and refresh each region's
|
|
764
822
|
* model catalog from the upstream once that region's credentials allow it.
|
|
@@ -808,11 +866,7 @@ function apply(ctx, config) {
|
|
|
808
866
|
* a selection that belongs to the other side of the split.
|
|
809
867
|
*/
|
|
810
868
|
let legacyAccountRegion;
|
|
811
|
-
const effectiveAccountFor = (region, value) =>
|
|
812
|
-
const explicit = value.accounts?.[region];
|
|
813
|
-
if (explicit !== void 0) return explicit;
|
|
814
|
-
return legacyAccountRegion === region ? value.accountId : void 0;
|
|
815
|
-
};
|
|
869
|
+
const effectiveAccountFor = (region, value) => selectAccountFor(region, value, legacyAccountRegion);
|
|
816
870
|
const discoverModels = async (region, signal) => {
|
|
817
871
|
const credential = await stacks[region].store.resolve();
|
|
818
872
|
return client.fetchModels(credential, signal);
|
|
@@ -845,14 +899,11 @@ function apply(ctx, config) {
|
|
|
845
899
|
});
|
|
846
900
|
applySelection(config);
|
|
847
901
|
(async () => {
|
|
848
|
-
const id = current().accountId;
|
|
849
|
-
if (id === void 0) return;
|
|
850
902
|
try {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
903
|
+
const region = await legacyAttributionRegion(current(), async (candidate) => stacks[candidate].store.accounts());
|
|
904
|
+
if (region === void 0) return;
|
|
905
|
+
legacyAccountRegion = region;
|
|
906
|
+
applySelection(current());
|
|
856
907
|
} catch {}
|
|
857
908
|
})();
|
|
858
909
|
let stopped = false;
|
|
@@ -950,4 +1001,4 @@ function apply(ctx, config) {
|
|
|
950
1001
|
});
|
|
951
1002
|
}
|
|
952
1003
|
//#endregion
|
|
953
|
-
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
1004
|
+
export { Config, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialStore, WorkBuddyUpstreamClient, apply, authFileName, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveCatalog, fallbackModelsFor, inject, isHeartbeatProcessAlive, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, name, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionCleared, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, selectAccountFor, toPersistedWorkBuddyModel, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
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.5",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|