dsh-connect-workbuddy 4.9.2 → 4.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +114 -20
- package/lib/{host-heartbeat-DSMmEhuD.js → host-heartbeat-BHaBzzOr.js} +1 -1
- package/lib/index.d.ts +50 -1
- package/lib/index.js +112 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.3 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **patch**:新增一个**可选**的查询参数(`detail=fast`)和一条**只读**的本地取数路径;不带参数的老请求逐字节不变,发往上游的请求体、路由 id、模型 id、凭据与配置项一律未改。但**它修的是一个用户可见的等待**——打开设置页后要等半分钟到一分钟才出现账号列表。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **issue #77:打开设置页要等 30–60 秒才出现各个账号**
|
|
10
|
+
- **原来错在哪**:卡片的账号列表来自**一份**用量文档,而这份文档的**末尾**要等一串上游读取——账号池每个成员一次 `fetchCredits`、每个账号一次 `fetchCheckinStatus`、当前账号自己再一次 `fetchCredits` + `fetchCheckinStatus`。每次上游调用超时是 `JSON_TIMEOUT_MS = 30_000`(`src/upstream.ts`)且**没有重试**,于是慢一点的上游把等待叠成 30–60 秒。更糟的是这段时间里卡片渲染的是 `signed-out` 占位(说是「未登录」并给出「重新检测账号」按钮)——**报告者截图里看到的正是这个占位**,一个已经登录、池里有一堆账号的用户被告知未登录。
|
|
11
|
+
- **为什么 4.6.7 显得快**:那版还没有账号池的逐账号读数和签到读数(`poolMembersOf` 的逐成员积分读、`checkedInToday` 的逐账号签到读都是更早的提交引入的,但**卡片等整份文档**这个形状把它们的代价直接变成了首屏代价);不是 4.9.x 新增了扇出,而是 4.9.x 的功能让这份文档变得更长。所以修法不是微调超时,而是**把首屏和计费读解耦**。
|
|
12
|
+
- **现在的形状**:
|
|
13
|
+
- **服务端**:用量路由接受 `detail=fast`(`src/status-paths.ts` 的 `withWorkBuddyRegionFastDetail` / `fastDetailOf`),快速文档**只读本地状态**——凭据用 `store.current()`(不触发 token 刷新),实时数字(`credits` / `checkin` / `credentialRejected` / `recovery`)整块省略,账号池成员取自新增的 `localMembers`(就是请求路径本来就在用的 `localPoolMembers`,**零上游调用**),签到状态取自本进程已有的测量缓存 `cachedCheckedInToday`。**不带参数的行为一字未变**:未知或拼错的 `detail` 值一律退回完整文档,绝不会变成一份更空的卡片。
|
|
14
|
+
- **客户端**:先取 `detail=fast` 把账号表格画出来,再照旧取完整文档补上积分与签到。快速文档**永远不会覆盖**已经拿到完整文档的区域(它更穷,覆盖会把已显示的列抹成「—」)。在这之前(两份都还没回来)渲染 `row.accountsLoading`「正在加载账号…」,**不再假称未登录**。
|
|
15
|
+
- **为什么快速文档仍然带账号池块**:`AccountPool` 在没有 `pool` 块时返回 `null`,账号行就来自那里——首屏要「出现各个账号」,这个块就是必须的。它现在由本地花名册生成,不再等积分读数。
|
|
16
|
+
- **测试**:`tests/issue77-fast-first-paint.spec.ts` 在**路由层**断言快速文档**一次上游调用都不发**(积分/签到客户端被换成会抛错的桩,快速文档仍然成功),以及 `detail` 的解析只认精确的 `fast`、其余值全部退回完整文档,并保留完整文档行为不变的用例。`tests/issue77-card-first-paint.spec.tsx` 在**渲染层**用「完整文档永不返回」的桩挂载真实卡片,断言账号行**照样出现**、且不出现「未登录」文案;同时断言全部未返回时显示加载行,以及迟到的快速文档不会覆盖已加载的完整文档。删掉快速路径或让卡片等完整文档,这两组都会失败——已实测确认判别力。
|
|
17
|
+
- **刻意没做的**:没有给上游调用加重试或缩短超时(那只是把 30 秒变成另一种 30 秒);没有把 `poolMembersOf` 从完整文档里拿掉(完整文档仍然需要真实积分);没有让快速文档在宿主不支持时触发那次昂贵读取(缺 `localMembers` 就省略账号池块,宁可少画一行也不拖慢首屏)。
|
|
18
|
+
|
|
3
19
|
## 4.9.2 (2026-10-19)
|
|
4
20
|
|
|
5
21
|
> 版本号是 **patch**:只改 `profiles` 里多登记一个注册表键,外加文档与注释;发往上游的请求体、路由 id、模型 id、模型选择器、目录与凭据一律未变。但**它修的是一个整区不可用的故障**——4.9.0/4.9.1 期间国际版(`workbuddy-global`)的每一个请求都在发出前失败。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-BHaBzzOr.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
|
@@ -123,6 +123,33 @@ window.__ModuleLoader__.load({
|
|
|
123
123
|
return `${path}?${WORKBUDDY_REGION_PARAM}=${region}`;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
+
* Query parameter selecting how much of a status document the Host assembles.
|
|
127
|
+
*
|
|
128
|
+
* The card's first paint only needs what local state already knows; live
|
|
129
|
+
* figures (credit, check-in, per-account readings) arrive through the SAME
|
|
130
|
+
* route once they have been measured. See {@link WORKBUDDY_DETAIL_FAST}.
|
|
131
|
+
*/
|
|
132
|
+
const WORKBUDDY_DETAIL_PARAM = "detail";
|
|
133
|
+
/**
|
|
134
|
+
* The `detail` value asking for the first-paint document.
|
|
135
|
+
*
|
|
136
|
+
* Absent means the full document, which is what every earlier caller asks for
|
|
137
|
+
* and what the route keeps answering — the parameter only ever ADDS a cheap
|
|
138
|
+
* mode, it never changes an existing one.
|
|
139
|
+
*/
|
|
140
|
+
const WORKBUDDY_DETAIL_FAST = "fast";
|
|
141
|
+
/**
|
|
142
|
+
* Address one region's status route for its first-paint document.
|
|
143
|
+
*
|
|
144
|
+
* The Host answers this from local state only (no upstream read, no token
|
|
145
|
+
* refresh), so a slow billing endpoint cannot decide when the account list
|
|
146
|
+
* appears. The card paints it first and then upgrades the same snapshot from
|
|
147
|
+
* the full document.
|
|
148
|
+
*/
|
|
149
|
+
function withWorkBuddyRegionFastDetail(path, region) {
|
|
150
|
+
return `${withWorkBuddyRegion(path, region)}&${WORKBUDDY_DETAIL_PARAM}=${WORKBUDDY_DETAIL_FAST}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
126
153
|
* Narrow a settings value to the `regions` map. Accepts EITHER the whole
|
|
127
154
|
* settings section (the Host's resolved `Config`) OR the `regions` map itself,
|
|
128
155
|
* and unwraps the former. This tolerance is deliberate: passing the whole
|
|
@@ -4612,23 +4639,16 @@ window.__ModuleLoader__.load({
|
|
|
4612
4639
|
/**
|
|
4613
4640
|
* Last-known usage per region, so tab dots survive tab switches.
|
|
4614
4641
|
*
|
|
4615
|
-
*
|
|
4616
|
-
*
|
|
4617
|
-
*
|
|
4618
|
-
*
|
|
4642
|
+
* EMPTY until a real document lands (issue #77). It used to be seeded with a
|
|
4643
|
+
* `signed-out` placeholder, which was then rendered: for as long as the usage
|
|
4644
|
+
* route took to answer — and that wait is exactly what #77 reports — a user
|
|
4645
|
+
* with several signed-in accounts was told they were not signed in, with a
|
|
4646
|
+
* "detect accounts" button as the only way out. An absent region now renders
|
|
4647
|
+
* the loading line instead, and the first paint itself arrives from the
|
|
4648
|
+
* `detail=fast` document (local state only, no upstream call), so the rows
|
|
4649
|
+
* appear before anything billing-side has answered.
|
|
4619
4650
|
*/
|
|
4620
|
-
const [statusByRegion, setStatusByRegion] = (0, react.useState)({
|
|
4621
|
-
cn: {
|
|
4622
|
-
status: "signed-out",
|
|
4623
|
-
accounts: [],
|
|
4624
|
-
selectionExplicit: false
|
|
4625
|
-
},
|
|
4626
|
-
global: {
|
|
4627
|
-
status: "signed-out",
|
|
4628
|
-
accounts: [],
|
|
4629
|
-
selectionExplicit: false
|
|
4630
|
-
}
|
|
4631
|
-
});
|
|
4651
|
+
const [statusByRegion, setStatusByRegion] = (0, react.useState)({});
|
|
4632
4652
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
4633
4653
|
const [settingsRevision, setSettingsRevision] = (0, react.useState)(0);
|
|
4634
4654
|
/** Per-region unsaved model edits; a draft on one tab is never dropped by
|
|
@@ -4763,19 +4783,86 @@ window.__ModuleLoader__.load({
|
|
|
4763
4783
|
return;
|
|
4764
4784
|
}
|
|
4765
4785
|
}, [t]);
|
|
4786
|
+
/**
|
|
4787
|
+
* FIRST PAINT (issue #77): the same usage document, built from LOCAL state
|
|
4788
|
+
* only — no billing request, no per-account check-in read.
|
|
4789
|
+
*
|
|
4790
|
+
* Why this exists. The card used to paint nothing until ONE monolithic
|
|
4791
|
+
* document landed, and that document's tail awaited an upstream call per
|
|
4792
|
+
* account (credits for every pool member, check-in for every account, plus
|
|
4793
|
+
* the selected account's own two). Each of those has a 30-second timeout and
|
|
4794
|
+
* no retry, so a slow or throttled upstream stacked the waits and the panel
|
|
4795
|
+
* stayed empty for the reported 30–60 seconds — showing a "not signed in"
|
|
4796
|
+
* placeholder the whole time, because that is what an absent document
|
|
4797
|
+
* rendered as.
|
|
4798
|
+
*
|
|
4799
|
+
* The Host answers `detail=fast` from the credential store, the local pool
|
|
4800
|
+
* roster and the measurement caches, so this is one local round trip. It
|
|
4801
|
+
* carries the whole account table (the row list lives in the `pool` block),
|
|
4802
|
+
* which is the part the user came for; credits, check-in and the selected
|
|
4803
|
+
* account's live figures arrive with the full document that follows.
|
|
4804
|
+
*
|
|
4805
|
+
* Deliberately a SEPARATE callback instead of a `detail` argument on
|
|
4806
|
+
* `refreshUsage`: the ordering guard inside that callback is pinned by a
|
|
4807
|
+
* source test that counts its writes and freshness checks, and a second
|
|
4808
|
+
* write path in there would either break the pin or hide a real regression
|
|
4809
|
+
* behind it. Here the rules are simple and independent — this response never
|
|
4810
|
+
* downgrades a region that has already answered in full.
|
|
4811
|
+
*/
|
|
4812
|
+
const refreshUsageFast = (0, react.useCallback)(async (region, signal) => {
|
|
4813
|
+
const stale = usageGuard.current.begin(region);
|
|
4814
|
+
try {
|
|
4815
|
+
const response = await fetch(withWorkBuddyRegionFastDetail(WORKBUDDY_USAGE_PATH, region), {
|
|
4816
|
+
headers: { accept: "application/json" },
|
|
4817
|
+
credentials: "same-origin",
|
|
4818
|
+
...signal === void 0 ? {} : { signal }
|
|
4819
|
+
});
|
|
4820
|
+
if (!response.ok) return;
|
|
4821
|
+
const usage = await response.json().catch(() => void 0);
|
|
4822
|
+
if (usage === void 0) return;
|
|
4823
|
+
if (stale()) return;
|
|
4824
|
+
if (!mounted.current || signal?.aborted === true) return;
|
|
4825
|
+
setStatusByRegion((prev) => {
|
|
4826
|
+
const previous = prev[region];
|
|
4827
|
+
if (previous !== void 0 && previous.status === "signed-in") return prev;
|
|
4828
|
+
return {
|
|
4829
|
+
...prev,
|
|
4830
|
+
[region]: usage
|
|
4831
|
+
};
|
|
4832
|
+
});
|
|
4833
|
+
} catch {}
|
|
4834
|
+
}, []);
|
|
4766
4835
|
(0, react.useEffect)(() => {
|
|
4767
4836
|
if (!open) return;
|
|
4768
4837
|
const controller = new AbortController();
|
|
4769
|
-
|
|
4838
|
+
(async () => {
|
|
4839
|
+
await Promise.all(WORKBUDDY_REGIONS.map(async (region) => {
|
|
4840
|
+
await refreshUsageFast(region, controller.signal);
|
|
4841
|
+
}));
|
|
4842
|
+
if (controller.signal.aborted) return;
|
|
4843
|
+
for (const region of WORKBUDDY_REGIONS) refreshUsage(region, controller.signal);
|
|
4844
|
+
})();
|
|
4770
4845
|
return () => {
|
|
4771
4846
|
controller.abort();
|
|
4772
4847
|
};
|
|
4773
4848
|
}, [
|
|
4774
4849
|
open,
|
|
4775
4850
|
activeRegion,
|
|
4776
|
-
refreshUsage
|
|
4851
|
+
refreshUsage,
|
|
4852
|
+
refreshUsageFast
|
|
4777
4853
|
]);
|
|
4778
|
-
|
|
4854
|
+
/**
|
|
4855
|
+
* The document this tab is rendering, or `undefined` before ANY has landed.
|
|
4856
|
+
*
|
|
4857
|
+
* The distinction is the point of issue #77: an absent document used to be
|
|
4858
|
+
* rendered as the `signed-out` placeholder below, so for the whole time the
|
|
4859
|
+
* usage route was out the card told a signed-in user they were not signed in
|
|
4860
|
+
* and offered "detect accounts" as the only remedy. `loading` now gates that
|
|
4861
|
+
* branch, and the placeholder is only a safe shape for the reads below it.
|
|
4862
|
+
*/
|
|
4863
|
+
const loaded = statusByRegion[activeRegion];
|
|
4864
|
+
const loading = loaded === void 0;
|
|
4865
|
+
const status = loaded ?? {
|
|
4779
4866
|
status: "signed-out",
|
|
4780
4867
|
accounts: [],
|
|
4781
4868
|
selectionExplicit: false
|
|
@@ -5795,7 +5882,12 @@ window.__ModuleLoader__.load({
|
|
|
5795
5882
|
})
|
|
5796
5883
|
})
|
|
5797
5884
|
] }) : null,
|
|
5798
|
-
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5885
|
+
status.status === "signed-out" && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5886
|
+
className: "dsm-workbuddy-usage-text",
|
|
5887
|
+
role: "status",
|
|
5888
|
+
children: t("row.accountsLoading")
|
|
5889
|
+
}) : null,
|
|
5890
|
+
status.status === "signed-out" && !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5799
5891
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5800
5892
|
className: "dsm-workbuddy-usage-text",
|
|
5801
5893
|
children: signedOutText(notice, t)
|
|
@@ -6854,6 +6946,7 @@ window.__ModuleLoader__.load({
|
|
|
6854
6946
|
"row.accountsHint": "Choose from locally detected sign-ins. Tokens are never shown or saved in DSH settings.",
|
|
6855
6947
|
"row.accountsRescan": "Detect accounts again",
|
|
6856
6948
|
"row.accountsScanning": "Detecting…",
|
|
6949
|
+
"row.accountsLoading": "Loading your accounts…",
|
|
6857
6950
|
"row.accountsWriteFailed": "The account change was not saved: {message} — the active profile's configuration 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 that file and try again (see docs/WINDOWS.md in the plugin repo for the exact path).",
|
|
6858
6951
|
"row.selectionLostHint": "The saved account no longer exists locally.",
|
|
6859
6952
|
"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 a current one from the list above. Signing in again in the desktop app will not fix this.",
|
|
@@ -7166,6 +7259,7 @@ window.__ModuleLoader__.load({
|
|
|
7166
7259
|
"row.accountsHint": "选择本机检测到的登录账号;Token 不会显示,也不会保存到 DSH 设置。",
|
|
7167
7260
|
"row.accountsRescan": "重新检测账号",
|
|
7168
7261
|
"row.accountsScanning": "正在检测…",
|
|
7262
|
+
"row.accountsLoading": "正在加载账号…",
|
|
7169
7263
|
"row.accountsWriteFailed": "账号改动未保存:{message} —— 当前 profile 的配置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用该文件的程序再重试(文件的确切路径见插件仓库的 docs/WINDOWS.md)。",
|
|
7170
7264
|
"row.selectionLostHint": "已保存的账号在本机已不存在。",
|
|
7171
7265
|
"row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方列表中重新选择一个当前账号。重新登录桌面端 App 无法修复此问题。",
|
|
@@ -3966,7 +3966,7 @@ function generationOf(version) {
|
|
|
3966
3966
|
//#endregion
|
|
3967
3967
|
//#region src/version.ts
|
|
3968
3968
|
/** The npm package version this build was produced from. */
|
|
3969
|
-
const WORKBUDDY_CONNECT_VERSION = "4.9.
|
|
3969
|
+
const WORKBUDDY_CONNECT_VERSION = "4.9.3";
|
|
3970
3970
|
//#endregion
|
|
3971
3971
|
//#region src/host-heartbeat.ts
|
|
3972
3972
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -3079,6 +3079,23 @@ interface WorkBuddyPoolDeps {
|
|
|
3079
3079
|
};
|
|
3080
3080
|
/** One region's CHECKED accounts, plus their credits and last measurements. */
|
|
3081
3081
|
members(region: WorkBuddyRegion): Promise<readonly WorkBuddyPoolMember[]>;
|
|
3082
|
+
/**
|
|
3083
|
+
* One region's CHECKED accounts from LOCAL state only — the first-paint
|
|
3084
|
+
* source (issue #77).
|
|
3085
|
+
*
|
|
3086
|
+
* Cheap by contract in the strongest sense: no upstream read at all. It
|
|
3087
|
+
* returns the saved members with whatever credit reading a previous full
|
|
3088
|
+
* document already recorded (a snapshot, possibly minutes old, possibly
|
|
3089
|
+
* absent), so the card can paint the account table before any billing
|
|
3090
|
+
* endpoint answers. `credits` being absent is normal here and renders as "—",
|
|
3091
|
+
* which is why {@link WorkBuddyWebPoolAccount} makes every figure optional.
|
|
3092
|
+
*
|
|
3093
|
+
* A Host built without it degrades to omitting the pool block from a
|
|
3094
|
+
* first-paint document (rather than paying {@link members} on that path):
|
|
3095
|
+
* the card then paints its account list from the full document, exactly as
|
|
3096
|
+
* before this contract existed.
|
|
3097
|
+
*/
|
|
3098
|
+
localMembers?(region: WorkBuddyRegion): Promise<readonly WorkBuddyPoolMember[]>;
|
|
3082
3099
|
/**
|
|
3083
3100
|
* The checked accounts that actually RESOLVE to a local sign-in.
|
|
3084
3101
|
*
|
|
@@ -3126,6 +3143,16 @@ interface WorkBuddyPoolDeps {
|
|
|
3126
3143
|
* Only the CN region has a check-in to report; see {@link checkinSupported}.
|
|
3127
3144
|
*/
|
|
3128
3145
|
checkedInToday?(region: WorkBuddyRegion): Promise<Readonly<Record<string, boolean>>>;
|
|
3146
|
+
/**
|
|
3147
|
+
* Today's check-in state, from the cells already measured this process.
|
|
3148
|
+
*
|
|
3149
|
+
* The first-paint counterpart of {@link checkedInToday}: reads the Host's
|
|
3150
|
+
* measurement cache and nothing else, so a first paint never waits on one
|
|
3151
|
+
* upstream request per account. An account it has not measured is simply
|
|
3152
|
+
* absent, which the card renders as unknown — the same meaning as a failed
|
|
3153
|
+
* per-account read in the full document.
|
|
3154
|
+
*/
|
|
3155
|
+
cachedCheckedInToday?(region: WorkBuddyRegion): Readonly<Record<string, boolean>>;
|
|
3129
3156
|
/**
|
|
3130
3157
|
* Every local sign-in the pool does NOT cover, so the card can offer them to
|
|
3131
3158
|
* check in. Absent means the card lists only pool members.
|
|
@@ -3172,14 +3199,36 @@ interface WorkBuddyPoolDeps {
|
|
|
3172
3199
|
/** The region's live catalog, used to resolve the free target model. */
|
|
3173
3200
|
catalog?(region: WorkBuddyRegion): readonly WorkBuddyModelInfo[];
|
|
3174
3201
|
}
|
|
3202
|
+
/**
|
|
3203
|
+
* How much live measurement a status document carries (issue #77).
|
|
3204
|
+
*
|
|
3205
|
+
* `full` is the original answer: the selected account's credit and check-in
|
|
3206
|
+
* plus every pool account's credit and check-in, all read from upstream now.
|
|
3207
|
+
* It is what the card's 60-second poll, the composer panel and every
|
|
3208
|
+
* measurement-driven control expect.
|
|
3209
|
+
*
|
|
3210
|
+
* `fast` is the FIRST PAINT. It answers from local state only — no token
|
|
3211
|
+
* refresh, no billing endpoint, no check-in endpoint — so the account list can
|
|
3212
|
+
* render in the first round trip and cannot be held up by a slow or
|
|
3213
|
+
* rate-limited upstream. Live figures are absent from it by construction; the
|
|
3214
|
+
* card then asks for the full document and upgrades the same snapshot.
|
|
3215
|
+
*/
|
|
3216
|
+
type WorkBuddyStatusDetail = 'full' | 'fast';
|
|
3175
3217
|
/**
|
|
3176
3218
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
3177
3219
|
* the region-scoped store already answers with only that region's accounts,
|
|
3178
3220
|
* so the document's model slots and account list are that region's by
|
|
3179
3221
|
* construction. Sign-in state is read-only; credit is a live billing answer
|
|
3180
3222
|
* whose failure degrades to `creditsError` rather than failing the document.
|
|
3223
|
+
*
|
|
3224
|
+
* `options.detail` selects the first-paint variant: it uses the credential
|
|
3225
|
+
* already known locally instead of resolving (resolving may refresh a token
|
|
3226
|
+
* over the network) and skips {@link liveFigures} entirely, so nothing about
|
|
3227
|
+
* the account list depends on how fast a billing endpoint answers.
|
|
3181
3228
|
*/
|
|
3182
|
-
declare function workBuddyWebStatus(deps: WorkBuddyStatusRouteOptions, region: WorkBuddyRegion
|
|
3229
|
+
declare function workBuddyWebStatus(deps: WorkBuddyStatusRouteOptions, region: WorkBuddyRegion, options?: {
|
|
3230
|
+
detail?: WorkBuddyStatusDetail;
|
|
3231
|
+
}): Promise<WorkBuddyWebUsage>;
|
|
3183
3232
|
/**
|
|
3184
3233
|
* Mount the read-only routes on a context where `webServer` is available.
|
|
3185
3234
|
* The caller uses `ctx.inject(['webServer'], ...)`, so Desktop startup order
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-BHaBzzOr.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -4829,6 +4829,34 @@ function withWorkBuddyRegion(path, region) {
|
|
|
4829
4829
|
return `${path}?${WORKBUDDY_REGION_PARAM}=${region}`;
|
|
4830
4830
|
}
|
|
4831
4831
|
/**
|
|
4832
|
+
* Query parameter selecting how much of a status document the Host assembles.
|
|
4833
|
+
*
|
|
4834
|
+
* The card's first paint only needs what local state already knows; live
|
|
4835
|
+
* figures (credit, check-in, per-account readings) arrive through the SAME
|
|
4836
|
+
* route once they have been measured. See {@link WORKBUDDY_DETAIL_FAST}.
|
|
4837
|
+
*/
|
|
4838
|
+
const WORKBUDDY_DETAIL_PARAM = "detail";
|
|
4839
|
+
/**
|
|
4840
|
+
* The `detail` value asking for the first-paint document.
|
|
4841
|
+
*
|
|
4842
|
+
* Absent means the full document, which is what every earlier caller asks for
|
|
4843
|
+
* and what the route keeps answering — the parameter only ever ADDS a cheap
|
|
4844
|
+
* mode, it never changes an existing one.
|
|
4845
|
+
*/
|
|
4846
|
+
const WORKBUDDY_DETAIL_FAST = "fast";
|
|
4847
|
+
/**
|
|
4848
|
+
* Whether a status-route URL asks for the first-paint document.
|
|
4849
|
+
*
|
|
4850
|
+
* Only the exact `fast` value is recognized: an unknown or misspelled value
|
|
4851
|
+
* degrades to the full document, which is what every earlier caller already
|
|
4852
|
+
* gets. A typo must never produce an emptier panel than no parameter at all.
|
|
4853
|
+
*/
|
|
4854
|
+
function fastDetailOf(url) {
|
|
4855
|
+
const at = url.indexOf("?");
|
|
4856
|
+
if (at === -1) return false;
|
|
4857
|
+
return new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_DETAIL_PARAM) === WORKBUDDY_DETAIL_FAST;
|
|
4858
|
+
}
|
|
4859
|
+
/**
|
|
4832
4860
|
* Read the region parameter off a status-route URL. Absent means the domestic
|
|
4833
4861
|
* tab (`cn`); a present-but-unknown value returns undefined so the route can
|
|
4834
4862
|
* answer 400 instead of guessing.
|
|
@@ -5156,13 +5184,42 @@ async function workBuddyAccountCredits(deps, region) {
|
|
|
5156
5184
|
}));
|
|
5157
5185
|
}
|
|
5158
5186
|
/**
|
|
5187
|
+
* Read the selected account's live figures, and classify a refusal.
|
|
5188
|
+
*
|
|
5189
|
+
* Extracted so the first-paint path can omit it as a unit: the two calls here
|
|
5190
|
+
* are the ones that used to gate the account list (each up to the upstream
|
|
5191
|
+
* client's 30-second ceiling, and the pool adds one pair per member on top).
|
|
5192
|
+
*/
|
|
5193
|
+
async function liveFigures(deps, region, credential, selected) {
|
|
5194
|
+
const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
|
|
5195
|
+
const rejected = [creditsResult, checkinResult].some((result) => result.status === "rejected" && isCredentialRejectedError(result.reason));
|
|
5196
|
+
const recovery = !rejected ? void 0 : await resolveCredentialRecovery({
|
|
5197
|
+
region,
|
|
5198
|
+
store: deps.store(region),
|
|
5199
|
+
...selected === void 0 ? {} : { rejectedAccountId: selected.id },
|
|
5200
|
+
probe: deps.accountUsable ?? (async () => false)
|
|
5201
|
+
});
|
|
5202
|
+
return {
|
|
5203
|
+
...creditsResult.status === "fulfilled" ? { credits: toCredits(creditsResult.value) } : { creditsError: safeMessage(creditsResult.reason) },
|
|
5204
|
+
...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) },
|
|
5205
|
+
...!rejected ? {} : { credentialRejected: true },
|
|
5206
|
+
...recovery === void 0 ? {} : { recovery }
|
|
5207
|
+
};
|
|
5208
|
+
}
|
|
5209
|
+
/**
|
|
5159
5210
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
5160
5211
|
* the region-scoped store already answers with only that region's accounts,
|
|
5161
5212
|
* so the document's model slots and account list are that region's by
|
|
5162
5213
|
* construction. Sign-in state is read-only; credit is a live billing answer
|
|
5163
5214
|
* whose failure degrades to `creditsError` rather than failing the document.
|
|
5215
|
+
*
|
|
5216
|
+
* `options.detail` selects the first-paint variant: it uses the credential
|
|
5217
|
+
* already known locally instead of resolving (resolving may refresh a token
|
|
5218
|
+
* over the network) and skips {@link liveFigures} entirely, so nothing about
|
|
5219
|
+
* the account list depends on how fast a billing endpoint answers.
|
|
5164
5220
|
*/
|
|
5165
|
-
async function workBuddyWebStatus(deps, region) {
|
|
5221
|
+
async function workBuddyWebStatus(deps, region, options = {}) {
|
|
5222
|
+
const fast = options.detail === "fast";
|
|
5166
5223
|
const store = deps.store(region);
|
|
5167
5224
|
const accounts = await store.accounts();
|
|
5168
5225
|
deps.regionUsable?.(region, accounts.length > 0);
|
|
@@ -5180,7 +5237,8 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
5180
5237
|
};
|
|
5181
5238
|
let credential;
|
|
5182
5239
|
try {
|
|
5183
|
-
credential = await store.resolve();
|
|
5240
|
+
credential = fast ? await store.current() : await store.resolve();
|
|
5241
|
+
if (credential === void 0) credential = await store.resolve();
|
|
5184
5242
|
} catch (error) {
|
|
5185
5243
|
const webAccounts = accounts.map(toWebAccount);
|
|
5186
5244
|
return {
|
|
@@ -5213,14 +5271,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
5213
5271
|
imageModelIds: [...deps.imageModelIds(region)],
|
|
5214
5272
|
offModelIds: [...deps.offModelIds(region)]
|
|
5215
5273
|
};
|
|
5216
|
-
const
|
|
5217
|
-
const rejected = [creditsResult, checkinResult].some((result) => result.status === "rejected" && isCredentialRejectedError(result.reason));
|
|
5218
|
-
const recovery = !rejected ? void 0 : await resolveCredentialRecovery({
|
|
5219
|
-
region,
|
|
5220
|
-
store: deps.store(region),
|
|
5221
|
-
...selected === void 0 ? {} : { rejectedAccountId: selected.id },
|
|
5222
|
-
probe: deps.accountUsable ?? (async () => false)
|
|
5223
|
-
});
|
|
5274
|
+
const figures = fast ? {} : await liveFigures(deps, region, credential, selected);
|
|
5224
5275
|
return {
|
|
5225
5276
|
status: "signed-in",
|
|
5226
5277
|
contextBudgets: Object.fromEntries(Object.entries(deps.contextBudgets(region)).filter(([, value]) => typeof value === "number")),
|
|
@@ -5230,18 +5281,22 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
5230
5281
|
authFile: volatileFlagOf("authFile")
|
|
5231
5282
|
},
|
|
5232
5283
|
...account,
|
|
5233
|
-
...
|
|
5234
|
-
...
|
|
5235
|
-
...!rejected ? {} : { credentialRejected: true },
|
|
5236
|
-
...recovery === void 0 ? {} : { recovery },
|
|
5237
|
-
...await poolSectionOf(deps, region)
|
|
5284
|
+
...figures,
|
|
5285
|
+
...await poolSectionOf(deps, region, fast)
|
|
5238
5286
|
};
|
|
5239
5287
|
}
|
|
5240
|
-
/**
|
|
5241
|
-
|
|
5288
|
+
/**
|
|
5289
|
+
* The pool block for a status document, or nothing when it cannot be built.
|
|
5290
|
+
*
|
|
5291
|
+
* `fast` asks for the first-paint variant: local state only, no upstream call.
|
|
5292
|
+
* A Host without a local member source then omits the block rather than paying
|
|
5293
|
+
* the full read, so a first-paint document never regresses into the expensive
|
|
5294
|
+
* path this mode exists to avoid.
|
|
5295
|
+
*/
|
|
5296
|
+
async function poolSectionOf(deps, region, fast) {
|
|
5242
5297
|
if (deps.pool === void 0) return {};
|
|
5243
5298
|
try {
|
|
5244
|
-
const pool = await workBuddyWebPool(deps, region);
|
|
5299
|
+
const pool = await workBuddyWebPool(deps, region, fast);
|
|
5245
5300
|
return pool === void 0 ? {} : { pool };
|
|
5246
5301
|
} catch {
|
|
5247
5302
|
return {};
|
|
@@ -5260,11 +5315,12 @@ async function poolSectionOf(deps, region) {
|
|
|
5260
5315
|
* SHOULD serve, `current` says who IS serving. Marking the winner as current
|
|
5261
5316
|
* would make the card claim a switch that may not have been applied yet.
|
|
5262
5317
|
*/
|
|
5263
|
-
async function workBuddyWebPool(deps, region) {
|
|
5318
|
+
async function workBuddyWebPool(deps, region, fast = false) {
|
|
5264
5319
|
const pool = deps.pool;
|
|
5265
5320
|
if (pool === void 0) return void 0;
|
|
5266
5321
|
const preferences = pool.preferences(region);
|
|
5267
|
-
const members = await pool.members(region);
|
|
5322
|
+
const members = fast ? await pool.localMembers?.(region) : await pool.members(region);
|
|
5323
|
+
if (members === void 0) return void 0;
|
|
5268
5324
|
const ranked = rankPool(members, Date.now(), preferences.orderAccountIds);
|
|
5269
5325
|
const catalog = pool.catalog?.(region) ?? [];
|
|
5270
5326
|
const target = resolveTargetModel(catalog, preferences.targetModelId);
|
|
@@ -5272,7 +5328,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
5272
5328
|
const byId = new Map(members.map((member) => [member.account.id, member]));
|
|
5273
5329
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
5274
5330
|
const others = await pool.otherAccounts?.(region) ?? [];
|
|
5275
|
-
const checkedIn = await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5331
|
+
const checkedIn = fast ? pool.cachedCheckedInToday?.(region) ?? {} : await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5276
5332
|
const rows = [...ranked.map((row) => ({
|
|
5277
5333
|
account: row.account,
|
|
5278
5334
|
member: true,
|
|
@@ -5379,8 +5435,9 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
5379
5435
|
}
|
|
5380
5436
|
const region = requestRegion(req, res);
|
|
5381
5437
|
if (region === void 0) return;
|
|
5438
|
+
const detail = fastDetailOf(req.url ?? "") ? "fast" : "full";
|
|
5382
5439
|
try {
|
|
5383
|
-
json(res, 200, await workBuddyWebStatus(deps, region));
|
|
5440
|
+
json(res, 200, await workBuddyWebStatus(deps, region, { detail }));
|
|
5384
5441
|
} catch (error) {
|
|
5385
5442
|
json(res, 500, { error: safeMessage(error) });
|
|
5386
5443
|
}
|
|
@@ -6451,6 +6508,19 @@ function apply(ctx, config) {
|
|
|
6451
6508
|
preferences: (region) => poolPreferencesOf(current(), region),
|
|
6452
6509
|
members: (region) => poolMembersOf(region),
|
|
6453
6510
|
/**
|
|
6511
|
+
* The first-paint member list (issue #77): local state only.
|
|
6512
|
+
*
|
|
6513
|
+
* This is `localPoolMembers`, i.e. the SAME list the request path already
|
|
6514
|
+
* consults — saved members, their last credit snapshot (if it is still
|
|
6515
|
+
* fresh), and their last probe result. No upstream call, so the card's
|
|
6516
|
+
* account table can paint before any billing endpoint answers; live
|
|
6517
|
+
* figures arrive with the full document a moment later.
|
|
6518
|
+
*
|
|
6519
|
+
* A member whose credit snapshot has expired simply carries no `credits`,
|
|
6520
|
+
* which the card renders as "—" rather than as zero.
|
|
6521
|
+
*/
|
|
6522
|
+
localMembers: (region) => localPoolMembers(region),
|
|
6523
|
+
/**
|
|
6454
6524
|
* The checked accounts that resolve to a local sign-in.
|
|
6455
6525
|
*
|
|
6456
6526
|
* A local scan only — no upstream calls — so the route's empty-pool guard
|
|
@@ -6527,6 +6597,25 @@ function apply(ctx, config) {
|
|
|
6527
6597
|
}));
|
|
6528
6598
|
return Object.fromEntries(entries.filter((entry) => entry !== void 0));
|
|
6529
6599
|
},
|
|
6600
|
+
/**
|
|
6601
|
+
* Today's check-in state from the cells already measured this process.
|
|
6602
|
+
*
|
|
6603
|
+
* The first-paint counterpart (issue #77) of `checkedInToday`: it reads
|
|
6604
|
+
* `checkinStateCache` and nothing else, so painting the table never waits
|
|
6605
|
+
* on one request per account. An unmeasured account is absent, which the
|
|
6606
|
+
* card already renders as unknown rather than as "not checked in".
|
|
6607
|
+
*/
|
|
6608
|
+
cachedCheckedInToday(region) {
|
|
6609
|
+
if (!poolPreferencesOf(current(), region).enabled) return {};
|
|
6610
|
+
const now = Date.now();
|
|
6611
|
+
const state = checkinStateCache[region] ?? {};
|
|
6612
|
+
const cached = {};
|
|
6613
|
+
for (const [accountId, entry] of Object.entries(state)) {
|
|
6614
|
+
if (now - entry.atMs >= CHECKIN_CACHE_MS) continue;
|
|
6615
|
+
cached[accountId] = entry.checkedIn;
|
|
6616
|
+
}
|
|
6617
|
+
return cached;
|
|
6618
|
+
},
|
|
6530
6619
|
async currentAccountId(region) {
|
|
6531
6620
|
await applyPoolSelection(region).catch(() => void 0);
|
|
6532
6621
|
const credential = await stacks[region].store.current().catch(() => void 0);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "4.9.
|
|
5
|
+
"version": "4.9.3",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|