dsh-connect-workbuddy 4.9.2 → 4.9.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 +40 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +114 -20
- package/lib/{host-heartbeat-DSMmEhuD.js → host-heartbeat-BpArQi1Q.js} +21 -10
- package/lib/index.d.ts +67 -1
- package/lib/index.js +154 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.4 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **patch**:只改**错误文案的取材方式**,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变(换号规则的变化只有一处「不再多问一次」的减少,见下)。但它修的是一个**会把人送去查错地方**的故障——一个普通的 400 被渲染成「账号池空了」,而上游唯一那句可执行建议被截断丢掉。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **issue #78:400/11133 被渲染成「池中其余账号本次请求都已尝试过」,且 `displayTips` 被 400 字截断丢弃**
|
|
10
|
+
- **原来错在哪**:三个缺陷叠在同一行错误里。
|
|
11
|
+
1. **描述了一次从未发生的账号遍历**。`isFailoverWorthy()` 明确排除 `client`(同一个请求体换哪个账号都是 400),换号循环因此一次都没跑;但通用分支判断「要不要问池子要解释」用的条件是**回调是否存在**,而真实宿主两个回调都给了。于是 `explainPoolShortfall()` 被问了一次,它看到池里每个成员都健康(400 不记账号故障)、`excluded.length === 0`,命中那条分支返回「池中其余账号本次请求都已尝试过」——**池子其实没被问过任何候选**。句中再加上尾部固定的「池中没有其他可用账号」,整句读起来就是「账号用完了/额度耗尽了」,而真实原因是请求参数。
|
|
12
|
+
2. **`displayTips` 从未被解析**。`parseUpstreamErrorDetail()` 只取 `code`/`requestId`/`displayMsg`;通用分支把 `result.message.slice(0, 400)` 直接拼进用户可见文本,而这类报文 637–777 字符,前 400 字符全是 `code`/`requestId`/`extError` 的骨架(`param: ""`、`Request: null`),那句「请新建任务、换个模型或稍后重试」必然落在截断之外。报告者看到的回复停在 `Pleas` 中间。
|
|
13
|
+
3. **状态码印两次**。`failoverReasonText()` 的 `default` 回「不可用(http 400)」,而调用方自己又拼了一次「(http 400)」。
|
|
14
|
+
- **为什么测试没抓住**:既有测试全都用**不带 `displayTips`** 的报文(#56 的 14003、#51 的策略拒绝),所以「可执行建议被截断」这件事在夹具里根本不存在;而「问了没问池子」只被间接覆盖——`shortfall` 回调存在的用例恰好都是有换号的用例,于是「有没有问」和「该不该问」在测试里始终是同一个问题。状态码重复更无从断言:**没有任何测试读过用户看到的那一行**,只断言过 `detail` 字段本身。
|
|
15
|
+
- **现在的形状**:
|
|
16
|
+
- `UpstreamErrorDetail` 增加 `tips?: string`,`parseUpstreamErrorDetail()` 用同一个 `localizedText()` 读 `displayTips` 与 `displayMsg`(字符串与 `{ zh, en }` 两种形态都接受)。两个字段分开存:`displayMsg` 说「错在哪」,`displayTips` 说「该怎么办」——合并会让其中一个失去自己的意思。
|
|
17
|
+
- 通用分支的正文改成 `detail.tips ?? detail.displayMsg ?? clippedUpstreamText(message, 400)`,`code`/`requestId` 收进紧凑后缀「(code 11133,requestId 33221fe5-…)」(support 要的就是这两个,此前只能从被切碎的 JSON 里自己找)。`clippedUpstreamText()` 压平空白、超长时优先在句末断开并补 `…`——它**只**接住解析不出来的报文(非 JSON 网关页),有 `detail` 的报文永远走不到它。
|
|
18
|
+
- `failoverReasonText()` 增加 `case 'client': return '请求被上游拒绝'`,`default` 改为 `'不可用'`(不再自带状态码)。另两个分支(`policy_reject` / `model_busy`)里同样的 `slice(0, 200)` 也换成 `clippedUpstreamText()`,形状统一。
|
|
19
|
+
- **问池子的条件改为「真的搜过」**:`failoverShortfall !== undefined && failoverAccount !== undefined && isFailoverWorthy(result.kind)`。`client` 与 `model_busy` 两类不再触发那次排序(它们本来就不换号,问出来的答案必然是假的);`client` 另给一句正确的建议——「该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果」。
|
|
20
|
+
- **同一份报文现在渲染为**(177 字符,原来是 490):
|
|
21
|
+
```
|
|
22
|
+
workbuddy 上游失败:请求被上游拒绝(http 400)(code 11133,requestId 33221fe5-f6d5-4ea1-8882-669411e07a5b)。这次对话的内容这个模型不接受,与网络无关。请新建任务、换个模型或稍后重试。该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试
|
|
23
|
+
```
|
|
24
|
+
- **测试**:新增 `tests/upstream-error-copy.spec.ts`(12 例)。它**不测 helper 而测渲染出来的那句话**——helper 只值它换来的用户可见结果,且 `detail` 由真实的 `parseUpstreamErrorDetail()` 产出,所以解析与渲染是一起被覆盖的(手搓 `detail` 的夹具可以在解析坏掉时仍然是绿的)。用例含:报告者那份 11133 报文逐字节渲染出上游建议、`message` 里**没有** `extError`/`"requestId"` 等骨架、长度 < 300、`(http 400)` 恰好出现**一次**(数出来的,因为「含有 http 400」对坏文案也成立)、`client` 失败时 shortfall 回调**调用次数为 0**且不出现两句池子术语、429 这类**会**换号的失败仍然既问池子又打印池子的结论并保留上游的重置时间、超长非 JSON 页补 `…` 且长度有界、短的非 JSON 页原样显示且不加 `…`、以及 `policy_reject`/`model_busy` 两个分支的文案不回归。删掉 `tips` 的解析、或把「问池子」的条件改回看回调是否存在,这组里都有用例会失败——已实测确认判别力。
|
|
25
|
+
- **刻意没做的**:没有改 `explainPoolShortfall()` 本身(它那句「池中其余账号本次请求都已尝试过」在**真的**搜过池子之后是诚实的,问题在于以前没搜过也会被问);没有把 `triedAccountIds` 传进 shortfall 回调(`tests/pool-e2e.spec.ts` 有源码文本守卫固定 `explainPoolShortfall(region)` 这个调用形状,且现有签名「不从 shim 接收参数」是 `src/index.ts:837-844` 写明的设计);没有改 400 的换号策略本身——它本来就是对的,错的是文案在描述一次没发生的换号。
|
|
26
|
+
|
|
27
|
+
## 4.9.3 (2026-10-19)
|
|
28
|
+
|
|
29
|
+
> 版本号是 **patch**:新增一个**可选**的查询参数(`detail=fast`)和一条**只读**的本地取数路径;不带参数的老请求逐字节不变,发往上游的请求体、路由 id、模型 id、凭据与配置项一律未改。但**它修的是一个用户可见的等待**——打开设置页后要等半分钟到一分钟才出现账号列表。
|
|
30
|
+
|
|
31
|
+
### Fixes
|
|
32
|
+
|
|
33
|
+
- **issue #77:打开设置页要等 30–60 秒才出现各个账号**
|
|
34
|
+
- **原来错在哪**:卡片的账号列表来自**一份**用量文档,而这份文档的**末尾**要等一串上游读取——账号池每个成员一次 `fetchCredits`、每个账号一次 `fetchCheckinStatus`、当前账号自己再一次 `fetchCredits` + `fetchCheckinStatus`。每次上游调用超时是 `JSON_TIMEOUT_MS = 30_000`(`src/upstream.ts`)且**没有重试**,于是慢一点的上游把等待叠成 30–60 秒。更糟的是这段时间里卡片渲染的是 `signed-out` 占位(说是「未登录」并给出「重新检测账号」按钮)——**报告者截图里看到的正是这个占位**,一个已经登录、池里有一堆账号的用户被告知未登录。
|
|
35
|
+
- **为什么 4.6.7 显得快**:那版还没有账号池的逐账号读数和签到读数(`poolMembersOf` 的逐成员积分读、`checkedInToday` 的逐账号签到读都是更早的提交引入的,但**卡片等整份文档**这个形状把它们的代价直接变成了首屏代价);不是 4.9.x 新增了扇出,而是 4.9.x 的功能让这份文档变得更长。所以修法不是微调超时,而是**把首屏和计费读解耦**。
|
|
36
|
+
- **现在的形状**:
|
|
37
|
+
- **服务端**:用量路由接受 `detail=fast`(`src/status-paths.ts` 的 `withWorkBuddyRegionFastDetail` / `fastDetailOf`),快速文档**只读本地状态**——凭据用 `store.current()`(不触发 token 刷新),实时数字(`credits` / `checkin` / `credentialRejected` / `recovery`)整块省略,账号池成员取自新增的 `localMembers`(就是请求路径本来就在用的 `localPoolMembers`,**零上游调用**),签到状态取自本进程已有的测量缓存 `cachedCheckedInToday`。**不带参数的行为一字未变**:未知或拼错的 `detail` 值一律退回完整文档,绝不会变成一份更空的卡片。
|
|
38
|
+
- **客户端**:先取 `detail=fast` 把账号表格画出来,再照旧取完整文档补上积分与签到。快速文档**永远不会覆盖**已经拿到完整文档的区域(它更穷,覆盖会把已显示的列抹成「—」)。在这之前(两份都还没回来)渲染 `row.accountsLoading`「正在加载账号…」,**不再假称未登录**。
|
|
39
|
+
- **为什么快速文档仍然带账号池块**:`AccountPool` 在没有 `pool` 块时返回 `null`,账号行就来自那里——首屏要「出现各个账号」,这个块就是必须的。它现在由本地花名册生成,不再等积分读数。
|
|
40
|
+
- **测试**:`tests/issue77-fast-first-paint.spec.ts` 在**路由层**断言快速文档**一次上游调用都不发**(积分/签到客户端被换成会抛错的桩,快速文档仍然成功),以及 `detail` 的解析只认精确的 `fast`、其余值全部退回完整文档,并保留完整文档行为不变的用例。`tests/issue77-card-first-paint.spec.tsx` 在**渲染层**用「完整文档永不返回」的桩挂载真实卡片,断言账号行**照样出现**、且不出现「未登录」文案;同时断言全部未返回时显示加载行,以及迟到的快速文档不会覆盖已加载的完整文档。删掉快速路径或让卡片等完整文档,这两组都会失败——已实测确认判别力。
|
|
41
|
+
- **刻意没做的**:没有给上游调用加重试或缩短超时(那只是把 30 秒变成另一种 30 秒);没有把 `poolMembersOf` 从完整文档里拿掉(完整文档仍然需要真实积分);没有让快速文档在宿主不支持时触发那次昂贵读取(缺 `localMembers` 就省略账号池块,宁可少画一行也不拖慢首屏)。
|
|
42
|
+
|
|
3
43
|
## 4.9.2 (2026-10-19)
|
|
4
44
|
|
|
5
45
|
> 版本号是 **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-BpArQi1Q.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 无法修复此问题。",
|
|
@@ -1095,6 +1095,23 @@ function classifyUpstreamError(status, body) {
|
|
|
1095
1095
|
return "client";
|
|
1096
1096
|
}
|
|
1097
1097
|
/**
|
|
1098
|
+
* Read one of the upstream's localized fields, in either shape it ships: a plain
|
|
1099
|
+
* string, or a `{ zh, en }` object.
|
|
1100
|
+
*
|
|
1101
|
+
* One reader for every such field on purpose. `displayMsg` and `displayTips`
|
|
1102
|
+
* arrive in the same two shapes from the same envelope, so parsing them with two
|
|
1103
|
+
* separate hand-rolled branches is how one of them ends up only handling the
|
|
1104
|
+
* object form (which is exactly what happened to `displayTips`: it was never
|
|
1105
|
+
* read at all, issue #78).
|
|
1106
|
+
*/
|
|
1107
|
+
function localizedText(value) {
|
|
1108
|
+
if (typeof value === "string") return value;
|
|
1109
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1110
|
+
const localized = value;
|
|
1111
|
+
const pick = localized.zh ?? localized.en;
|
|
1112
|
+
return typeof pick === "string" ? pick : void 0;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1098
1115
|
* Parse the known-good fields out of an upstream failure body. Non-JSON
|
|
1099
1116
|
* bodies (HTML error pages, empty strings) yield undefined rather than a guess.
|
|
1100
1117
|
*/
|
|
@@ -1107,19 +1124,13 @@ function parseUpstreamErrorDetail(body) {
|
|
|
1107
1124
|
}
|
|
1108
1125
|
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1109
1126
|
const raw = parsed;
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
if (typeof display === "string") displayMsg = display;
|
|
1113
|
-
else if (typeof display === "object" && display !== null) {
|
|
1114
|
-
const localized = display;
|
|
1115
|
-
const pick = localized.zh ?? localized.en;
|
|
1116
|
-
if (typeof pick === "string") displayMsg = pick;
|
|
1117
|
-
}
|
|
1118
|
-
if (displayMsg === void 0 && typeof raw.msg === "string") displayMsg = raw.msg;
|
|
1127
|
+
const displayMsg = localizedText(raw.displayMsg) ?? (typeof raw.msg === "string" ? raw.msg : void 0);
|
|
1128
|
+
const tips = localizedText(raw.displayTips);
|
|
1119
1129
|
const detail = {};
|
|
1120
1130
|
if (typeof raw.code === "number") detail.upstreamCode = raw.code;
|
|
1121
1131
|
if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
|
|
1122
1132
|
if (displayMsg !== void 0) detail.displayMsg = displayMsg;
|
|
1133
|
+
if (tips !== void 0) detail.tips = tips;
|
|
1123
1134
|
return detail;
|
|
1124
1135
|
}
|
|
1125
1136
|
/**
|
|
@@ -3966,7 +3977,7 @@ function generationOf(version) {
|
|
|
3966
3977
|
//#endregion
|
|
3967
3978
|
//#region src/version.ts
|
|
3968
3979
|
/** The npm package version this build was produced from. */
|
|
3969
|
-
const WORKBUDDY_CONNECT_VERSION = "4.9.
|
|
3980
|
+
const WORKBUDDY_CONNECT_VERSION = "4.9.4";
|
|
3970
3981
|
//#endregion
|
|
3971
3982
|
//#region src/host-heartbeat.ts
|
|
3972
3983
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -754,6 +754,23 @@ interface UpstreamErrorDetail {
|
|
|
754
754
|
requestId?: string;
|
|
755
755
|
/** Best human-facing message: `displayMsg.zh`, falling back to `.en`, then `msg`. */
|
|
756
756
|
displayMsg?: string;
|
|
757
|
+
/**
|
|
758
|
+
* What the upstream says to DO about it: `displayTips.zh`, falling back to
|
|
759
|
+
* `.en` (issue #78).
|
|
760
|
+
*
|
|
761
|
+
* Kept separate from {@link displayMsg} because the two answer different
|
|
762
|
+
* questions and both are needed: `displayMsg` names what was rejected
|
|
763
|
+
* ("模型无法处理此请求"), while `displayTips` names the REMEDY ("请新建任务、换个
|
|
764
|
+
* 模型或稍后重试"). The generic failure path used to discard it and render the
|
|
765
|
+
* raw body instead, whose first 400 characters are the `code`/`requestId`/
|
|
766
|
+
* `extError` skeleton — so the one actionable sentence was always cut off, and
|
|
767
|
+
* a plain parameter rejection read like an empty account pool.
|
|
768
|
+
*
|
|
769
|
+
* Recorded as data rather than folded into `displayMsg` so a caller that wants
|
|
770
|
+
* to tell the user what to change can do so without the "what went wrong"
|
|
771
|
+
* clause losing its own meaning.
|
|
772
|
+
*/
|
|
773
|
+
tips?: string;
|
|
757
774
|
}
|
|
758
775
|
/**
|
|
759
776
|
* Region for a login domain; an empty domain means CN (matching upstream tooling).
|
|
@@ -3079,6 +3096,23 @@ interface WorkBuddyPoolDeps {
|
|
|
3079
3096
|
};
|
|
3080
3097
|
/** One region's CHECKED accounts, plus their credits and last measurements. */
|
|
3081
3098
|
members(region: WorkBuddyRegion): Promise<readonly WorkBuddyPoolMember[]>;
|
|
3099
|
+
/**
|
|
3100
|
+
* One region's CHECKED accounts from LOCAL state only — the first-paint
|
|
3101
|
+
* source (issue #77).
|
|
3102
|
+
*
|
|
3103
|
+
* Cheap by contract in the strongest sense: no upstream read at all. It
|
|
3104
|
+
* returns the saved members with whatever credit reading a previous full
|
|
3105
|
+
* document already recorded (a snapshot, possibly minutes old, possibly
|
|
3106
|
+
* absent), so the card can paint the account table before any billing
|
|
3107
|
+
* endpoint answers. `credits` being absent is normal here and renders as "—",
|
|
3108
|
+
* which is why {@link WorkBuddyWebPoolAccount} makes every figure optional.
|
|
3109
|
+
*
|
|
3110
|
+
* A Host built without it degrades to omitting the pool block from a
|
|
3111
|
+
* first-paint document (rather than paying {@link members} on that path):
|
|
3112
|
+
* the card then paints its account list from the full document, exactly as
|
|
3113
|
+
* before this contract existed.
|
|
3114
|
+
*/
|
|
3115
|
+
localMembers?(region: WorkBuddyRegion): Promise<readonly WorkBuddyPoolMember[]>;
|
|
3082
3116
|
/**
|
|
3083
3117
|
* The checked accounts that actually RESOLVE to a local sign-in.
|
|
3084
3118
|
*
|
|
@@ -3126,6 +3160,16 @@ interface WorkBuddyPoolDeps {
|
|
|
3126
3160
|
* Only the CN region has a check-in to report; see {@link checkinSupported}.
|
|
3127
3161
|
*/
|
|
3128
3162
|
checkedInToday?(region: WorkBuddyRegion): Promise<Readonly<Record<string, boolean>>>;
|
|
3163
|
+
/**
|
|
3164
|
+
* Today's check-in state, from the cells already measured this process.
|
|
3165
|
+
*
|
|
3166
|
+
* The first-paint counterpart of {@link checkedInToday}: reads the Host's
|
|
3167
|
+
* measurement cache and nothing else, so a first paint never waits on one
|
|
3168
|
+
* upstream request per account. An account it has not measured is simply
|
|
3169
|
+
* absent, which the card renders as unknown — the same meaning as a failed
|
|
3170
|
+
* per-account read in the full document.
|
|
3171
|
+
*/
|
|
3172
|
+
cachedCheckedInToday?(region: WorkBuddyRegion): Readonly<Record<string, boolean>>;
|
|
3129
3173
|
/**
|
|
3130
3174
|
* Every local sign-in the pool does NOT cover, so the card can offer them to
|
|
3131
3175
|
* check in. Absent means the card lists only pool members.
|
|
@@ -3172,14 +3216,36 @@ interface WorkBuddyPoolDeps {
|
|
|
3172
3216
|
/** The region's live catalog, used to resolve the free target model. */
|
|
3173
3217
|
catalog?(region: WorkBuddyRegion): readonly WorkBuddyModelInfo[];
|
|
3174
3218
|
}
|
|
3219
|
+
/**
|
|
3220
|
+
* How much live measurement a status document carries (issue #77).
|
|
3221
|
+
*
|
|
3222
|
+
* `full` is the original answer: the selected account's credit and check-in
|
|
3223
|
+
* plus every pool account's credit and check-in, all read from upstream now.
|
|
3224
|
+
* It is what the card's 60-second poll, the composer panel and every
|
|
3225
|
+
* measurement-driven control expect.
|
|
3226
|
+
*
|
|
3227
|
+
* `fast` is the FIRST PAINT. It answers from local state only — no token
|
|
3228
|
+
* refresh, no billing endpoint, no check-in endpoint — so the account list can
|
|
3229
|
+
* render in the first round trip and cannot be held up by a slow or
|
|
3230
|
+
* rate-limited upstream. Live figures are absent from it by construction; the
|
|
3231
|
+
* card then asks for the full document and upgrades the same snapshot.
|
|
3232
|
+
*/
|
|
3233
|
+
type WorkBuddyStatusDetail = 'full' | 'fast';
|
|
3175
3234
|
/**
|
|
3176
3235
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
3177
3236
|
* the region-scoped store already answers with only that region's accounts,
|
|
3178
3237
|
* so the document's model slots and account list are that region's by
|
|
3179
3238
|
* construction. Sign-in state is read-only; credit is a live billing answer
|
|
3180
3239
|
* whose failure degrades to `creditsError` rather than failing the document.
|
|
3240
|
+
*
|
|
3241
|
+
* `options.detail` selects the first-paint variant: it uses the credential
|
|
3242
|
+
* already known locally instead of resolving (resolving may refresh a token
|
|
3243
|
+
* over the network) and skips {@link liveFigures} entirely, so nothing about
|
|
3244
|
+
* the account list depends on how fast a billing endpoint answers.
|
|
3181
3245
|
*/
|
|
3182
|
-
declare function workBuddyWebStatus(deps: WorkBuddyStatusRouteOptions, region: WorkBuddyRegion
|
|
3246
|
+
declare function workBuddyWebStatus(deps: WorkBuddyStatusRouteOptions, region: WorkBuddyRegion, options?: {
|
|
3247
|
+
detail?: WorkBuddyStatusDetail;
|
|
3248
|
+
}): Promise<WorkBuddyWebUsage>;
|
|
3183
3249
|
/**
|
|
3184
3250
|
* Mount the read-only routes on a context where `webServer` is available.
|
|
3185
3251
|
* 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-BpArQi1Q.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";
|
|
@@ -2691,10 +2691,39 @@ function failoverReasonText(failure) {
|
|
|
2691
2691
|
case "policy_reject": return "被服务端策略拒绝";
|
|
2692
2692
|
case "not_found": return "模型不可用";
|
|
2693
2693
|
case "server": return "上游异常";
|
|
2694
|
-
|
|
2694
|
+
case "client": return "请求被上游拒绝";
|
|
2695
|
+
default: return "不可用";
|
|
2695
2696
|
}
|
|
2696
2697
|
}
|
|
2697
2698
|
/**
|
|
2699
|
+
* One upstream sentence, made safe to paste into a single-line error message.
|
|
2700
|
+
*
|
|
2701
|
+
* `result.message` carries the WHOLE upstream body — for a classified failure
|
|
2702
|
+
* that is a JSON envelope 637–777 characters long whose first 400 characters are
|
|
2703
|
+
* the `code`/`requestId`/`extError` skeleton (`param: ""`, `Request: null`). A
|
|
2704
|
+
* plain `slice(0, 400)` therefore cut the one actionable sentence in half: the
|
|
2705
|
+
* reply ended mid-word at `Pleas` and the reader lost `displayTips` entirely
|
|
2706
|
+
* (issue #78).
|
|
2707
|
+
*
|
|
2708
|
+
* Two rules keep that from happening again:
|
|
2709
|
+
* - whitespace collapses to single spaces, because the body is JSON/HTML and its
|
|
2710
|
+
* newlines would otherwise render as a ragged block inside one paragraph;
|
|
2711
|
+
* - the cut prefers a sentence boundary once one exists past the halfway point,
|
|
2712
|
+
* so a clipped sentence reads as "truncated" instead of as broken prose, and
|
|
2713
|
+
* the `…` always says the text continues.
|
|
2714
|
+
*
|
|
2715
|
+
* This is the LAST resort: a body that parsed into a `detail` with `displayMsg`
|
|
2716
|
+
* or `tips` never reaches it, and those are short by construction.
|
|
2717
|
+
*/
|
|
2718
|
+
function clippedUpstreamText(text, limit) {
|
|
2719
|
+
const collapsed = text.replace(/\s+/gu, " ").trim();
|
|
2720
|
+
if (collapsed.length <= limit) return collapsed;
|
|
2721
|
+
const cut = collapsed.slice(0, limit);
|
|
2722
|
+
const stop = Math.max(cut.lastIndexOf("。"), cut.lastIndexOf("!"), cut.lastIndexOf("?"), cut.lastIndexOf(". "), cut.lastIndexOf("! "), cut.lastIndexOf("? "));
|
|
2723
|
+
if (stop >= Math.floor(limit / 2)) return `${cut.slice(0, stop + 1).trimEnd()}…`;
|
|
2724
|
+
return `${cut.trimEnd()}…`;
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2698
2727
|
* The line prepended to an answer that came from a failover account.
|
|
2699
2728
|
*
|
|
2700
2729
|
* Block-quoted and bracketed so it reads as machinery rather than as the model's
|
|
@@ -3094,11 +3123,11 @@ function createWorkBuddyShim(options) {
|
|
|
3094
3123
|
if (!result.ok) {
|
|
3095
3124
|
const attempts = triedAccountIds.length;
|
|
3096
3125
|
const note = attempts > 1 ? `(已尝试 ${attempts} 个账号)` : "";
|
|
3097
|
-
const shortfall = failoverShortfall
|
|
3126
|
+
const shortfall = failoverShortfall !== void 0 && failoverAccount !== void 0 && isFailoverWorthy(result.kind) ? await failoverShortfall().catch(() => void 0) : void 0;
|
|
3098
3127
|
const why = shortfall === void 0 ? "" : `:${shortfall}`;
|
|
3099
3128
|
if (result.kind === "policy_reject") {
|
|
3100
3129
|
const detail = result.detail;
|
|
3101
|
-
const lead = detail?.displayMsg ?? result.message
|
|
3130
|
+
const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
|
|
3102
3131
|
const meta = ["服务端策略拒绝"];
|
|
3103
3132
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3104
3133
|
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
@@ -3108,7 +3137,7 @@ function createWorkBuddyShim(options) {
|
|
|
3108
3137
|
}
|
|
3109
3138
|
if (result.kind === "model_busy") {
|
|
3110
3139
|
const detail = result.detail;
|
|
3111
|
-
const lead = detail?.displayMsg ?? result.message
|
|
3140
|
+
const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
|
|
3112
3141
|
const meta = ["模型当前负载较高"];
|
|
3113
3142
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3114
3143
|
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
@@ -3116,7 +3145,15 @@ function createWorkBuddyShim(options) {
|
|
|
3116
3145
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
3117
3146
|
return;
|
|
3118
3147
|
}
|
|
3119
|
-
|
|
3148
|
+
const detail = result.detail;
|
|
3149
|
+
const meta = [];
|
|
3150
|
+
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3151
|
+
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
3152
|
+
const stamp = meta.length === 0 ? "" : `(${meta.join(",")})`;
|
|
3153
|
+
const receipt = detail?.tips ?? detail?.displayMsg ?? clippedUpstreamText(result.message, 400);
|
|
3154
|
+
const advice = result.kind === "client" ? "该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试" : shortfall === void 0 ? "" : "建议换一个模型继续,或稍后等待该账号的额度恢复";
|
|
3155
|
+
const body = advice === "" ? receipt : `${receipt.replace(/[。.]$/u, "")}。${advice}`;
|
|
3156
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${why}${stamp}。${body}`, hostErrorCode(result.kind));
|
|
3120
3157
|
return;
|
|
3121
3158
|
}
|
|
3122
3159
|
res.writeHead(200, {
|
|
@@ -4829,6 +4866,34 @@ function withWorkBuddyRegion(path, region) {
|
|
|
4829
4866
|
return `${path}?${WORKBUDDY_REGION_PARAM}=${region}`;
|
|
4830
4867
|
}
|
|
4831
4868
|
/**
|
|
4869
|
+
* Query parameter selecting how much of a status document the Host assembles.
|
|
4870
|
+
*
|
|
4871
|
+
* The card's first paint only needs what local state already knows; live
|
|
4872
|
+
* figures (credit, check-in, per-account readings) arrive through the SAME
|
|
4873
|
+
* route once they have been measured. See {@link WORKBUDDY_DETAIL_FAST}.
|
|
4874
|
+
*/
|
|
4875
|
+
const WORKBUDDY_DETAIL_PARAM = "detail";
|
|
4876
|
+
/**
|
|
4877
|
+
* The `detail` value asking for the first-paint document.
|
|
4878
|
+
*
|
|
4879
|
+
* Absent means the full document, which is what every earlier caller asks for
|
|
4880
|
+
* and what the route keeps answering — the parameter only ever ADDS a cheap
|
|
4881
|
+
* mode, it never changes an existing one.
|
|
4882
|
+
*/
|
|
4883
|
+
const WORKBUDDY_DETAIL_FAST = "fast";
|
|
4884
|
+
/**
|
|
4885
|
+
* Whether a status-route URL asks for the first-paint document.
|
|
4886
|
+
*
|
|
4887
|
+
* Only the exact `fast` value is recognized: an unknown or misspelled value
|
|
4888
|
+
* degrades to the full document, which is what every earlier caller already
|
|
4889
|
+
* gets. A typo must never produce an emptier panel than no parameter at all.
|
|
4890
|
+
*/
|
|
4891
|
+
function fastDetailOf(url) {
|
|
4892
|
+
const at = url.indexOf("?");
|
|
4893
|
+
if (at === -1) return false;
|
|
4894
|
+
return new URLSearchParams(url.slice(at + 1)).get(WORKBUDDY_DETAIL_PARAM) === WORKBUDDY_DETAIL_FAST;
|
|
4895
|
+
}
|
|
4896
|
+
/**
|
|
4832
4897
|
* Read the region parameter off a status-route URL. Absent means the domestic
|
|
4833
4898
|
* tab (`cn`); a present-but-unknown value returns undefined so the route can
|
|
4834
4899
|
* answer 400 instead of guessing.
|
|
@@ -5156,13 +5221,42 @@ async function workBuddyAccountCredits(deps, region) {
|
|
|
5156
5221
|
}));
|
|
5157
5222
|
}
|
|
5158
5223
|
/**
|
|
5224
|
+
* Read the selected account's live figures, and classify a refusal.
|
|
5225
|
+
*
|
|
5226
|
+
* Extracted so the first-paint path can omit it as a unit: the two calls here
|
|
5227
|
+
* are the ones that used to gate the account list (each up to the upstream
|
|
5228
|
+
* client's 30-second ceiling, and the pool adds one pair per member on top).
|
|
5229
|
+
*/
|
|
5230
|
+
async function liveFigures(deps, region, credential, selected) {
|
|
5231
|
+
const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
|
|
5232
|
+
const rejected = [creditsResult, checkinResult].some((result) => result.status === "rejected" && isCredentialRejectedError(result.reason));
|
|
5233
|
+
const recovery = !rejected ? void 0 : await resolveCredentialRecovery({
|
|
5234
|
+
region,
|
|
5235
|
+
store: deps.store(region),
|
|
5236
|
+
...selected === void 0 ? {} : { rejectedAccountId: selected.id },
|
|
5237
|
+
probe: deps.accountUsable ?? (async () => false)
|
|
5238
|
+
});
|
|
5239
|
+
return {
|
|
5240
|
+
...creditsResult.status === "fulfilled" ? { credits: toCredits(creditsResult.value) } : { creditsError: safeMessage(creditsResult.reason) },
|
|
5241
|
+
...checkinResult.status === "fulfilled" ? { checkin: checkinResult.value } : { checkinError: safeMessage(checkinResult.reason) },
|
|
5242
|
+
...!rejected ? {} : { credentialRejected: true },
|
|
5243
|
+
...recovery === void 0 ? {} : { recovery }
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5246
|
+
/**
|
|
5159
5247
|
* Assemble one region's card document. `region` is the tab the card is on;
|
|
5160
5248
|
* the region-scoped store already answers with only that region's accounts,
|
|
5161
5249
|
* so the document's model slots and account list are that region's by
|
|
5162
5250
|
* construction. Sign-in state is read-only; credit is a live billing answer
|
|
5163
5251
|
* whose failure degrades to `creditsError` rather than failing the document.
|
|
5252
|
+
*
|
|
5253
|
+
* `options.detail` selects the first-paint variant: it uses the credential
|
|
5254
|
+
* already known locally instead of resolving (resolving may refresh a token
|
|
5255
|
+
* over the network) and skips {@link liveFigures} entirely, so nothing about
|
|
5256
|
+
* the account list depends on how fast a billing endpoint answers.
|
|
5164
5257
|
*/
|
|
5165
|
-
async function workBuddyWebStatus(deps, region) {
|
|
5258
|
+
async function workBuddyWebStatus(deps, region, options = {}) {
|
|
5259
|
+
const fast = options.detail === "fast";
|
|
5166
5260
|
const store = deps.store(region);
|
|
5167
5261
|
const accounts = await store.accounts();
|
|
5168
5262
|
deps.regionUsable?.(region, accounts.length > 0);
|
|
@@ -5180,7 +5274,8 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
5180
5274
|
};
|
|
5181
5275
|
let credential;
|
|
5182
5276
|
try {
|
|
5183
|
-
credential = await store.resolve();
|
|
5277
|
+
credential = fast ? await store.current() : await store.resolve();
|
|
5278
|
+
if (credential === void 0) credential = await store.resolve();
|
|
5184
5279
|
} catch (error) {
|
|
5185
5280
|
const webAccounts = accounts.map(toWebAccount);
|
|
5186
5281
|
return {
|
|
@@ -5213,14 +5308,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
5213
5308
|
imageModelIds: [...deps.imageModelIds(region)],
|
|
5214
5309
|
offModelIds: [...deps.offModelIds(region)]
|
|
5215
5310
|
};
|
|
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
|
-
});
|
|
5311
|
+
const figures = fast ? {} : await liveFigures(deps, region, credential, selected);
|
|
5224
5312
|
return {
|
|
5225
5313
|
status: "signed-in",
|
|
5226
5314
|
contextBudgets: Object.fromEntries(Object.entries(deps.contextBudgets(region)).filter(([, value]) => typeof value === "number")),
|
|
@@ -5230,18 +5318,22 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
5230
5318
|
authFile: volatileFlagOf("authFile")
|
|
5231
5319
|
},
|
|
5232
5320
|
...account,
|
|
5233
|
-
...
|
|
5234
|
-
...
|
|
5235
|
-
...!rejected ? {} : { credentialRejected: true },
|
|
5236
|
-
...recovery === void 0 ? {} : { recovery },
|
|
5237
|
-
...await poolSectionOf(deps, region)
|
|
5321
|
+
...figures,
|
|
5322
|
+
...await poolSectionOf(deps, region, fast)
|
|
5238
5323
|
};
|
|
5239
5324
|
}
|
|
5240
|
-
/**
|
|
5241
|
-
|
|
5325
|
+
/**
|
|
5326
|
+
* The pool block for a status document, or nothing when it cannot be built.
|
|
5327
|
+
*
|
|
5328
|
+
* `fast` asks for the first-paint variant: local state only, no upstream call.
|
|
5329
|
+
* A Host without a local member source then omits the block rather than paying
|
|
5330
|
+
* the full read, so a first-paint document never regresses into the expensive
|
|
5331
|
+
* path this mode exists to avoid.
|
|
5332
|
+
*/
|
|
5333
|
+
async function poolSectionOf(deps, region, fast) {
|
|
5242
5334
|
if (deps.pool === void 0) return {};
|
|
5243
5335
|
try {
|
|
5244
|
-
const pool = await workBuddyWebPool(deps, region);
|
|
5336
|
+
const pool = await workBuddyWebPool(deps, region, fast);
|
|
5245
5337
|
return pool === void 0 ? {} : { pool };
|
|
5246
5338
|
} catch {
|
|
5247
5339
|
return {};
|
|
@@ -5260,11 +5352,12 @@ async function poolSectionOf(deps, region) {
|
|
|
5260
5352
|
* SHOULD serve, `current` says who IS serving. Marking the winner as current
|
|
5261
5353
|
* would make the card claim a switch that may not have been applied yet.
|
|
5262
5354
|
*/
|
|
5263
|
-
async function workBuddyWebPool(deps, region) {
|
|
5355
|
+
async function workBuddyWebPool(deps, region, fast = false) {
|
|
5264
5356
|
const pool = deps.pool;
|
|
5265
5357
|
if (pool === void 0) return void 0;
|
|
5266
5358
|
const preferences = pool.preferences(region);
|
|
5267
|
-
const members = await pool.members(region);
|
|
5359
|
+
const members = fast ? await pool.localMembers?.(region) : await pool.members(region);
|
|
5360
|
+
if (members === void 0) return void 0;
|
|
5268
5361
|
const ranked = rankPool(members, Date.now(), preferences.orderAccountIds);
|
|
5269
5362
|
const catalog = pool.catalog?.(region) ?? [];
|
|
5270
5363
|
const target = resolveTargetModel(catalog, preferences.targetModelId);
|
|
@@ -5272,7 +5365,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
5272
5365
|
const byId = new Map(members.map((member) => [member.account.id, member]));
|
|
5273
5366
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
5274
5367
|
const others = await pool.otherAccounts?.(region) ?? [];
|
|
5275
|
-
const checkedIn = await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5368
|
+
const checkedIn = fast ? pool.cachedCheckedInToday?.(region) ?? {} : await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5276
5369
|
const rows = [...ranked.map((row) => ({
|
|
5277
5370
|
account: row.account,
|
|
5278
5371
|
member: true,
|
|
@@ -5379,8 +5472,9 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
5379
5472
|
}
|
|
5380
5473
|
const region = requestRegion(req, res);
|
|
5381
5474
|
if (region === void 0) return;
|
|
5475
|
+
const detail = fastDetailOf(req.url ?? "") ? "fast" : "full";
|
|
5382
5476
|
try {
|
|
5383
|
-
json(res, 200, await workBuddyWebStatus(deps, region));
|
|
5477
|
+
json(res, 200, await workBuddyWebStatus(deps, region, { detail }));
|
|
5384
5478
|
} catch (error) {
|
|
5385
5479
|
json(res, 500, { error: safeMessage(error) });
|
|
5386
5480
|
}
|
|
@@ -6451,6 +6545,19 @@ function apply(ctx, config) {
|
|
|
6451
6545
|
preferences: (region) => poolPreferencesOf(current(), region),
|
|
6452
6546
|
members: (region) => poolMembersOf(region),
|
|
6453
6547
|
/**
|
|
6548
|
+
* The first-paint member list (issue #77): local state only.
|
|
6549
|
+
*
|
|
6550
|
+
* This is `localPoolMembers`, i.e. the SAME list the request path already
|
|
6551
|
+
* consults — saved members, their last credit snapshot (if it is still
|
|
6552
|
+
* fresh), and their last probe result. No upstream call, so the card's
|
|
6553
|
+
* account table can paint before any billing endpoint answers; live
|
|
6554
|
+
* figures arrive with the full document a moment later.
|
|
6555
|
+
*
|
|
6556
|
+
* A member whose credit snapshot has expired simply carries no `credits`,
|
|
6557
|
+
* which the card renders as "—" rather than as zero.
|
|
6558
|
+
*/
|
|
6559
|
+
localMembers: (region) => localPoolMembers(region),
|
|
6560
|
+
/**
|
|
6454
6561
|
* The checked accounts that resolve to a local sign-in.
|
|
6455
6562
|
*
|
|
6456
6563
|
* A local scan only — no upstream calls — so the route's empty-pool guard
|
|
@@ -6527,6 +6634,25 @@ function apply(ctx, config) {
|
|
|
6527
6634
|
}));
|
|
6528
6635
|
return Object.fromEntries(entries.filter((entry) => entry !== void 0));
|
|
6529
6636
|
},
|
|
6637
|
+
/**
|
|
6638
|
+
* Today's check-in state from the cells already measured this process.
|
|
6639
|
+
*
|
|
6640
|
+
* The first-paint counterpart (issue #77) of `checkedInToday`: it reads
|
|
6641
|
+
* `checkinStateCache` and nothing else, so painting the table never waits
|
|
6642
|
+
* on one request per account. An unmeasured account is absent, which the
|
|
6643
|
+
* card already renders as unknown rather than as "not checked in".
|
|
6644
|
+
*/
|
|
6645
|
+
cachedCheckedInToday(region) {
|
|
6646
|
+
if (!poolPreferencesOf(current(), region).enabled) return {};
|
|
6647
|
+
const now = Date.now();
|
|
6648
|
+
const state = checkinStateCache[region] ?? {};
|
|
6649
|
+
const cached = {};
|
|
6650
|
+
for (const [accountId, entry] of Object.entries(state)) {
|
|
6651
|
+
if (now - entry.atMs >= CHECKIN_CACHE_MS) continue;
|
|
6652
|
+
cached[accountId] = entry.checkedIn;
|
|
6653
|
+
}
|
|
6654
|
+
return cached;
|
|
6655
|
+
},
|
|
6530
6656
|
async currentAccountId(region) {
|
|
6531
6657
|
await applyPoolSelection(region).catch(() => void 0);
|
|
6532
6658
|
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.4",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|