dsh-connect-workbuddy 3.4.2 → 3.4.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 CHANGED
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.4 (2026-10-03)
4
+
5
+ ### Features
6
+
7
+ - **测试请求体积可选(1 万 / 2 万 / 3 万 / 5 万 / 10 万 token),不再固定 2.5 万。**
8
+ - **背景**:上游的频率限制(6004)按**请求体积**触发——实测阈值落在 2 万~3 万 token 之间,**单次请求就足以触顶**(不是累计)。而账号池的「测试」探针此前固定发 25,000 token,于是真实对话体积在 2.5 万~3 万之间时会出现「测试测出可用、一发聊天立刻被限」。
9
+ - **为什么不是把固定值调大**:探针只测一个点,而阈值是一个区间、真实对话体积是连续的——把 2.5 万改成 3 万只是把缝隙上移(3 万以上漏判),且成本线性上升(2.5 万 ≈ 0.72 积分,5 万 ≈ 1.4+)。**体积是用户流量的属性,因此交给用户选。**
10
+ - 新增池子设置「测试请求体积」,默认仍是实测标定的 25K(`0` = 默认);改动立即生效,无需重载。
11
+ - 相应用户可见的说明写在设置项旁:解释 2 万~3 万阈值与「测出可用、一发就被限」的成因。
12
+
13
+ - **换号时在回复正文里说明原因,不再只有日志。**
14
+ - **背景**:池子的 failover 会给同一个请求依次试多个账号,此前只写一行 `logger.warn`——用户完全看不到。于是一次切换过的回复,和「你选的那个账号正常回答」**在界面上完全一样**,而且事后无法从记录区分「刚才真有请求失败」和「状态莫名其妙退回了旧值」。
15
+ - 现在回答开头会附一行(仅当真的发生了切换、且确实有内容送达时):
16
+
17
+ > [dsh-connect-workbuddy] 「13046625578」was rate limited — this answer came from another account
18
+
19
+ - 只报**第一个**被放弃的账号与原因(连续试 4 个账号时不刷屏),原因按用户能采取的行动措辞(`rate limited` / `signed out` / `rejected by policy` / …),而不是内部 kind 名。
20
+
21
+ - **池子记录标注来源与时间,让「结果变了」可解释。**
22
+ - 每条测量现在记录它是**谁写的**:`测得`(用户点「测试」的批测)或 `聊天撞上`(实时聊天失败被记录),以及**多久前**更新。
23
+ - 卡片上分两行显示:主行是结果/恢复时间/原因,次行是来源胶囊 + 相对时间。此前只能靠时间戳分布**猜**来源(批测写同一时间戳、单账号写入时间戳各不同)。
24
+ - 「来源未知」用于本次升级之前写入、没有来源字段的旧记录——不猜,如实标注。
25
+
26
+ ### Fixes
27
+
28
+ - **「来源未知」胶囊太淡看不清。** 旧样式同时做了四处削弱(`background:transparent`、`1px dashed`、`label-dimmed #9aa0a6`、`font-weight:400`);实测对比度发现**字色不是主因**(`#9aa0a6` 5.28:1,反而比正常胶囊的 `#999` 4.89:1 更亮),主因是 10px 小字配 400 字重。已改为实底 + 实线边框 + `label-secondary`(8.41:1),并去掉字重覆盖以继承基类的 600。
29
+
30
+ ## 3.4.3 (2026-10-03)
31
+
32
+ ### Features
33
+
34
+ - **装在非默认目录的 Windows 安装也能找到了:注册表回退(issue #30)。**
35
+ - **现象**:WorkBuddy 桌面端装在 `E:\workbuddy`、`F:\AGENTS\WorkBuddy` 这类非默认位置时,插件报「未登录」——而用户明明已经登录。
36
+ - **根因**:`workbuddyAppExecutableCandidates()` 只探四个**默认布局**路径(`%LOCALAPPDATA%\Programs\WorkBuddy`、`%LOCALAPPDATA%\WorkBuddy`、`%ProgramFiles%\WorkBuddy`、`%ProgramFiles(x86)%\WorkBuddy`)。装在别处时四个候选全落空,于是拿不到加密字段的密钥,**已登录的账号被报成未登录**。README 的答案是手工设 `WORKBUDDY_APP_EXECUTABLE`——而安装器其实早就把答案记在注册表里了。
37
+ - **修法**:四个候选全落空后,在 `win32` 上查询卸载注册表(`HKCU` / `HKLM` / `HKLM\WOW6432Node` 三处),从 `DisplayIcon` 优先、`InstallLocation` 兜底推出可执行文件路径。路径必须 basename 为 `WorkBuddy.exe` 且 `existsSync` 通过才会被采用(该路径最终会传给 `execFile`);注册表只是**回退**,默认安装零开销。`queryRegistry` 作为可注入 seam,测试不依赖主机状态。
38
+ - `doctor` 现在同时报告**来源**(`via env` / `default-layout` / `registry`):只看路径的话,「默认布局命中」和「回退救了你」输出的字符串一模一样,而这正是用户排查时唯一有用的区别。
39
+
40
+ ### Fixes
41
+
42
+ - **Windows 路径不再用宿主平台的分隔符构造(非 Windows 上会静默失效)。**
43
+ - **现象**:在 Linux/macOS 上构建/运行时,Windows 专用路径会带上 `/` 分隔符,永远不可能命中真实安装。
44
+ - **根因**:`workbuddyAppExecutableCandidates()` 的四个 Windows 候选、以及注册表回退的 `normalizeRegistryExecutable()` / `registryInstallLocationFromQuery()`,都用了 `path.join` / `path.basename`——它们跟随**运行平台**,不是数据的平台。在 POSIX 宿主上 `basename('E:\workbuddy\WorkBuddy.exe')` 返回整串,于是**每一个合法路径都被 basename 白名单拒绝**,注册表回退等于从未生效。
45
+ - **修法**:凡处理 Windows 数据一律改用 `path.win32.join` / `path.win32.basename`——**分隔符由数据决定,不由宿主决定**。
46
+ - **测试同样是错的**:期望值也用 `join()` 构造,生产与测试一起漂移,所以本地绿、CI(ubuntu)绿,而 Windows 上根本不命中。已改为 `win32.*` 并补一条**形状守卫**(断言每个 Windows 候选都不含 `/`、末段为 `WorkBuddy.exe`),变异验证(把 `win32.join` 换回 `join`)必红。
47
+ - 顺带修正一条与环境耦合的断言:原测试断言 `darwin` 返回 `undefined`,但本机装了 WorkBuddy 时 macOS 分支会**合法**命中,于是「开发者机器红、CI 绿」。该用例真正要断言的是注册表 seam 未被调用,已按此重写。
48
+
49
+ ### Fixes
50
+
51
+ - **403「request illegal」(11140)不再伪装成登录故障。**
52
+
53
+ 国际网关的真实失败(2026-09-29 抓包):同一凭据下 `GET /v3/config` 返回 200,聊天 `POST /v2/chat/completions` 却被 403 拒绝,响应体逐字为:
54
+
55
+ ```json
56
+ {"code":11140,"msg":"request illegal","requestId":"3498bf50-98a9-4746-962e-c14016b8c578","displayMsg":{"en":"The content did not pass the safety review. Please adjust and retry.","zh":"内容未通过安全审核,请调整后重试。"},"actions":["SUBMIT_FEEDBACK","COPY_ERROR","EDIT_INPUT"]}
57
+ ```
58
+
59
+ - **根因**:`classifyUpstreamError` 只认积分、会话失效、限流三类标记,11140 落进兜底的 `client`;shim 把 `client` 映射成 400,DSH 侧进一步归为 AUTH。用户看到的是「登录坏了」——实际是服务端内容策略拒绝,重新登录、刷新 token 都不会有效。
60
+ - **改动**:
61
+ - `upstream.ts` 新增 `policy_reject` 分类(标记 `request illegal` / `"code":11140`,在通用 4xx 之前判定)与 `parseUpstreamErrorDetail()`(从 JSON 失败体提取 `code` / `requestId` / `displayMsg`,非 JSON 返回 `undefined`,不猜);chat 失败结果附带 `detail`。
62
+ - `shim.ts` 把 `policy_reject` 映射回 403,错误消息从解析字段合成:官方文案(zh 优先,回落 en、`msg`)+ `code 11140` + `requestId` +「重新登录不会解决;可在桌面端用同一账号验证,或切换区域/账号后重试」;不再把整段原始 JSON 嵌进 message。
63
+ - **写回宿主的 `code` 是 `policy_reject`,不是上游的数值码 11140**(2026-10-03 适配 3.4.1 的 `hostErrorCode`)。宿主按稳定的机器可读类别路由,`11140` 不是它认识的类别;而 3.4.1 建立的规则是「只有 429/6004 需要翻译成宿主认得的词」。`policy_reject` 在 403 下本就落在重试集合之外,**不需要**翻译;更要紧的是**不能**被翻译进集合——内容策略拒绝是确定性的,原地重试只会把预算烧在一次必然再被拒的请求上,跨供应商故障转移也治不了策略判定。
64
+ - 探针 `outcomeOfFailure` 在 401/403 一刀切**之前**先判 `policy_reject`,卡片新增「服务端按内容策略拒绝」文案(中英)。探针与真实请求复用同一分类函数,对同一响应的判定不会分歧。
65
+ - **账号池不再因一次策略拒绝把健康账号永久踢出**(适配 3.4.2 时发现的连带问题):`policy-rejected` 是新 outcome,在 `exclusionOf` 里落进了 `default` 分支被标成 `unusable`——而 `unusable` 刻意**不带冷却**,于是账号会被**永久**移出池子,且卡片会显示「不可用」。这是错的:策略拒绝针对的是**那一条请求的内容**,不是这个账号,同一个账号换一条消息照样能服务。现按 `ok` 处理(测量照常记录,卡片能解释发生了什么,但不影响它继续被计费),并补守卫 + 变异验证。
66
+ - **守卫(先红后绿)**:`upstream.spec` / `shim.spec` / `probe.spec` 新增 8 例——分类(完整/最小 403 体 → `policy_reject`)、字段解析(zh 优先、缺 `displayMsg` 回落 `msg`、非 JSON 为 `undefined`)、shim 端到端(重放上面那段真实 403 体,断言 403 + `policy_reject` + **`code` 为 `policy_reject`** + 官方文案 + requestId,而不是 400)、探针(11140 → `policy-rejected`)。7 例先在未修复代码上跑红、实现后转绿;1 例良性对照(良性 403 / HTML 403 仍判 `client` / `credential-rejected`)双向保持绿——防止修复把无辜的 403 一起吞掉。
67
+
3
68
  ## 3.4.2 (2026-10-03)
4
69
 
5
70
  ### Fixes
@@ -62,7 +127,6 @@
62
127
 
63
128
  - README 的「账号为什么出池」表按新规则改写:区分「限流/欠费/瞬时故障会自动回池」与「凭据被拒/模型不存在只能手动重测」,并删掉已不存在的「定时器」表述。
64
129
  - 清理死代码:`POOL_TICK_MS`、`POOL_MIN_INTERVAL_MINUTES`、`poolDueAt`、`duePoolRegions`、`PoolScheduleInput` / `PoolScheduleResult` 及其单元测试全部删除。它们自 3.2.0 起已无任何生产调用(构建时已被 tree-shake),却仍被测试覆盖——于是「测试全绿」对「账号会不会回来」这个问题什么也没说明。
65
-
66
130
  ## 3.3.0 (2026-10-01)
67
131
 
68
132
  ### Features
package/README.en.md CHANGED
@@ -249,9 +249,10 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
249
249
  - 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.
250
250
  - **On Windows, an account change may fail to save while the profile's `cordis.patch.yml` 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.
251
251
  - Windows and Linux credential paths are derived from platform conventions. On a real Windows 11 machine, `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` and `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` **did resolve**, but **the remaining candidates and the Linux paths are still not verified on real hardware**; set `WORKBUDDY_AUTH_FILE` if yours lives elsewhere.
252
- - **Encrypted credentials require the desktop app to be present.** Newer desktop builds (**from 5.6.0, on macOS as well as Windows**) encrypt the token fields in the auth file, and the plugin needs that app's own native binding to obtain the field key. If the app is not installed, was uninstalled, or lives outside the probed paths (set `WORKBUDDY_APP_EXECUTABLE`), an encrypted file cannot be read — the account then shows as signed out, and the plugin **never** degrades into emitting a truncated token. `doctor` reports whether this capability is available. The key is a build-time constant that may rotate with app releases; the plugin fetches it from the local app each run and caches nothing on disk, so an app upgrade needs no plugin upgrade.
252
+ - **Encrypted credentials require the desktop app to be present.** Newer desktop builds (**from 5.6.0, on macOS as well as Windows**) encrypt the token fields in the auth file, and the plugin needs that app's own native binding to obtain the field key. If the app is not installed, was uninstalled, or lives outside the probed paths (on Windows the install registration is consulted automatically, so a non-default install is found without configuration; set `WORKBUDDY_APP_EXECUTABLE` if that misses too), an encrypted file cannot be read — the account then shows as signed out, and the plugin **never** degrades into emitting a truncated token. `doctor` reports whether this capability is available. The key is a build-time constant that may rotate with app releases; the plugin fetches it from the local app each run and caches nothing on disk, so an app upgrade needs no plugin upgrade.
253
253
  - **The executable is located from the bundle's own declaration, not guessed from the app name.** The WorkBuddy macOS bundles declare `CFBundleExecutable` as `Electron` (not `WorkBuddy`), so the plugin reads each bundle's `Info.plist` to learn the binary's name. Both the domestic `WorkBuddy.app` and the international `WorkBuddy AI.app` are candidates, and an app filed into a subdirectory of an applications folder (e.g. `/Applications/IDE/WorkBuddy.app`) is found by a one-level scan that confirms each candidate's bundle identifier before use — so a different Electron app can never be launched by mistake.
254
254
  - **"Signed out" and "your sign-in cannot be read" are reported separately.** When an encrypted file cannot be read, the advice is to install the desktop app or point `WORKBUDDY_APP_EXECUTABLE` at it — **not** to sign in again, which cannot help: the credential is fine, the key is what is missing.
255
+ - **A 403 "request illegal" (code 11140) is a server-side policy refusal, not a sign-in problem.** The chat gateway sometimes refuses a request outright under its content policy; the body reads `{"code":11140,"msg":"request illegal","requestId":"…","displayMsg":{"en":"The content did not pass the safety review. Please adjust and retry."}}`. The plugin used to file this under the generic `client` error (HTTP 400), which DSH then displayed as an AUTH failure — it looked like a broken sign-in, but **signing in again cannot fix it**: the credentials are fine; it is this one request that was refused. The refusal now passes through as 403 with the official message, `code 11140`, and the `requestId` (give that to official support). The `error.code` written back to the host is the stable class `policy_reject` rather than the numeric `11140`: the host decides in-place retrial from that class, and a policy refusal is deterministic — retrying only burns the budget, and `11140` is not a word the host knows, and the per-model "test" reports "refused by the server's content policy" instead of "token refused". Two ways out: send the same prompt from the WorkBuddy desktop app with the same account (if the desktop app is refused too, the limit is on the account/content side), or switch region/account and retry.
255
256
 
256
257
  ## Disclaimer
257
258
 
package/README.md CHANGED
@@ -259,9 +259,10 @@ dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
259
259
  - 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 `accountId`(迁移来源)。它只作用于其所属区域,且**该区域被显式清除(`accounts.<区域> = ""`)后即不再生效**——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
260
260
  - **Windows:profile 的配置文件 `cordis.patch.yml` 被占用时,账号改动可能无法保存**。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败**不会**让 `set()` 报错。插件因此会在写入后回读校验:**确认落盘才显示「已清除」**,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。
261
261
  - Windows / Linux 的凭据默认路径按平台约定推导。已在真实 Windows 11 上确认 `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` 与 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe` 命中,**其余候选与 Linux 路径仍未经真机验证**;必要时可通过环境变量 `WORKBUDDY_AUTH_FILE` 指定实际位置。
262
- - **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
262
+ - **加密凭据依赖桌面 App 在场**:新版桌面端(**5.6.0 起,macOS 与 Windows 同样如此**)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(**Windows 上会自动回退查安装注册表**,仍找不到时用 `WORKBUDDY_APP_EXECUTABLE` 指定),加密文件就读不出来——此时账号会显示为未登录,而**不会**退化成读出一个残缺的 token。`doctor` 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。
263
263
  - **App 可执行文件按 bundle 自身声明定位,不按 App 名猜**:WorkBuddy 的 macOS bundle 里 `CFBundleExecutable` 是 `Electron`(不是 `WorkBuddy`),因此插件读取 bundle 的 `Info.plist` 来决定二进制名。国内版 `WorkBuddy.app` 与国际版 `WorkBuddy AI.app` 都在候选内;App 被归入 `/Applications` 子目录(如 `/Applications/IDE/WorkBuddy.app`)时,插件还会向下扫一层并用 bundle id 确认身份后才使用——**不会**因为同名的其他 Electron 应用而误启动它。
264
264
  - **「未登录」与「登录信息读不出来」是两件事**:加密文件读不出时,插件给出的建议是「安装桌面 App / 用 `WORKBUDDY_APP_EXECUTABLE` 指定其位置」,而**不是**「重新登录一次」——后者对这种情况无效(凭据本身是好的,缺的是密钥)。
265
+ - **403「request illegal」(code 11140)是服务端策略拒绝,不是登录问题**:聊天网关有时会按内容策略直接拒绝一次请求,响应体形如 `{"code":11140,"msg":"request illegal","requestId":"…","displayMsg":{"zh":"内容未通过安全审核,请调整后重试。"}}`。此前插件把它归为通用 `client` 错误返回 400,DSH 侧又显示成 AUTH 异常——看起来像「登录坏了」,但**重新登录解决不了它**:凭据本身是好的,被拒的是这一次请求。现在这类拒绝按 403 原样透传,消息里带官方文案、`code 11140` 和 `requestId`(联系官方支持时提供)。写回宿主的 `error.code` 是 `policy_reject` 这个稳定类别(而非数值 `11140`):宿主按类别决定要不要原地重试,而策略拒绝是确定性的——重试只会白烧预算,`11140` 也不是宿主认得的词;模型行的「测试」也显示「服务端按内容策略拒绝」而非「token 被拒」。遇到时的两个出口:在 WorkBuddy 桌面端用同一账号发同样的内容验证(桌面端同样被拒,说明限制在账号/内容侧);或切换区域/账号后重试。
265
266
 
266
267
  ## 免责声明
267
268
 
package/docs/WINDOWS.md CHANGED
@@ -30,6 +30,12 @@ Windows 在本项目里不是「顺带能跑」的平台,而是**有专属分
30
30
 
31
31
  **通用注意**:路径一律经 `path.join` 构造,**不得写死 POSIX 分隔符**。历史上正因此挂过 CI——`2.0.11` 新增的 darwin 候选断言写死了 `/Applications/WorkBuddy.app/...`,生产代码没问题(用 `join`),错的是测试:它断言的是「跑测试的机器是 POSIX」而非「候选列表对不对」,于是 Windows 必挂、macOS 恒绿。现有回归守卫:候选路径不得**混用**分隔符。
32
32
 
33
+ **更隐蔽的反向形态**:处理 **Windows 专用**数据(注册表值、`%LOCALAPPDATA%` 等 Windows 环境变量)时,`path.join` / `path.basename` **不是**正确答案——它们跟随**运行平台**。在 Linux/macOS 上跑,`basename('E:\workbuddy\WorkBuddy.exe')` 返回整串(`\` 在 POSIX 下不是分隔符),`join('E:\workbuddy', 'x.exe')` 产出 `E:\workbuddy/x.exe`,两者在 Windows 上都不可能命中。正确写法是 `path.win32.join` / `path.win32.basename`:**分隔符由数据决定,而不是由宿主决定**。
34
+
35
+ - 生产代码里曾有两处踩到:`workbuddyAppExecutableCandidates()` 的四个 Windows 候选(`DIRTY` 形态的分隔符 → 非 Windows 宿主上构造出的路径永远不存在,且**它的测试用 `join()` 构造期望值,以同样的方式被改坏,于是双双通过**),以及注册表回退的 `normalizeRegistryExecutable()` / `registryInstallLocationFromQuery()`。
36
+ - 因此**测试的期望值也必须用 `win32.*` 构造**:用 `join()` 拼 Windows 期望值时,生产与测试会一起漂移,CI 在 ubuntu 上照样绿——这正是上一条「断言的是宿主而非逻辑」的同一种错,只是方向相反。
37
+ - 判断标准很简单:**这个字符串里出现 `\` 或盘符了吗?**有 → `win32.*`;是 POSIX 绝对路径或拼接本机路径 → 用 `join`。
38
+
33
39
  ## 2. 证据分级(写结论时必须用对档位)
34
40
 
35
41
  | 档位 | 含义 | 实例 |
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, x as defaultDesktopAuthCandidates } from "./host-heartbeat-Sq5JwluI.js";
2
+ import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, J as findWorkbuddyAppExecutableWithSource, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, x as defaultDesktopAuthCandidates } from "./host-heartbeat-DTuwptkM.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -63,7 +63,8 @@ async function doctor(jsonOutput) {
63
63
  const desktopPresent = await anyStore.desktopFilePresent();
64
64
  const heartbeat = await readHostHeartbeat();
65
65
  const hostAlive = heartbeat !== void 0 && isHeartbeatProcessAlive(heartbeat);
66
- const appExecutable = findWorkbuddyAppExecutable();
66
+ const discoveredApp = findWorkbuddyAppExecutableWithSource();
67
+ const appExecutable = discoveredApp?.executable;
67
68
  const regionLists = await Promise.all(REGIONS.map(async (region) => ({
68
69
  region,
69
70
  accounts: await makeStore(region).accounts()
@@ -106,6 +107,13 @@ async function doctor(jsonOutput) {
106
107
  */
107
108
  atRestDecryption: {
108
109
  appExecutable: appExecutable ?? `(not found; set WORKBUDDY_APP_EXECUTABLE)`,
110
+ /**
111
+ * Where the path came from. `registry` means the app is installed outside
112
+ * the default layout and the Windows uninstall registration found it; the
113
+ * absence of this field (older reports) is why the hint below can now name
114
+ * the mechanism instead of telling every user to set the env var.
115
+ */
116
+ appExecutableSource: discoveredApp?.source,
109
117
  available: appExecutable !== void 0
110
118
  },
111
119
  ownAuthFiles: {
@@ -137,7 +145,7 @@ async function doctor(jsonOutput) {
137
145
  hints: [
138
146
  ...anySignedIn ? [] : ["Sign in once in the WorkBuddy desktop app (either region), then run status again."],
139
147
  ...desktopPresent ? [] : [`No WorkBuddy desktop auth file at the expected path; set ${WORKBUDDY_AUTH_FILE_ENV} if it lives elsewhere.`],
140
- ...appExecutable === void 0 ? [`The WorkBuddy desktop app was not found, so encrypted credential fields cannot be opened; set ${WORKBUDDY_APP_EXECUTABLE_ENV} to its executable if it is installed elsewhere.`] : [],
148
+ ...appExecutable === void 0 ? [`The WorkBuddy desktop app was not found, so encrypted credential fields cannot be opened. On Windows the install registration was already checked, so the app is either not installed or its uninstall entry is missing; set ${WORKBUDDY_APP_EXECUTABLE_ENV} to its executable if it is installed elsewhere.`] : [],
141
149
  ...hostAlive ? [] : ["Host bundle not running in this DSH profile (or the process exited). The browser card and providers are unavailable until DSH starts the plugin."],
142
150
  ...piAi.generation === "legacy" && piAi.resolvedLocally ? [`This plugin resolves pi-ai ${piAi.version ?? "(unreadable version)"} (the 0.85 context shape) while DSH 0.2.0+ hosts normalize with 0.87; the adapter bridges that gap, but if tools still misbehave, remove any nested pi-ai copy so the plugin resolves the host's.`] : []
143
151
  ]
@@ -147,7 +155,7 @@ async function doctor(jsonOutput) {
147
155
  `WorkBuddy Connect ${WORKBUDDY_CONNECT_VERSION} on ${process.version}`,
148
156
  ...piAi.resolvedLocally ? [`pi-ai runtime: ${piAi.generation}${piAi.version === void 0 ? "" : ` ${piAi.version}`} (${piAi.resolvedFrom}) — ${report.piAiRuntime.adapterBehaviour}`] : [`pi-ai runtime: ${piAi.generation}, host-provided (no local copy; DSH 0.2.0+ ships 0.87.1) — passes provider contexts through unchanged`],
149
157
  `Desktop auth file: ${report.desktopAuthFile.present ? "present" : "missing"} (${report.desktopAuthFile.path})`,
150
- `Encrypted-credential support: ${report.atRestDecryption.available ? "available" : "unavailable"} (${report.atRestDecryption.appExecutable})`,
158
+ `Encrypted-credential support: ${report.atRestDecryption.available ? "available" : "unavailable"} (${report.atRestDecryption.appExecutable}${report.atRestDecryption.appExecutableSource === void 0 ? "" : `, via ${report.atRestDecryption.appExecutableSource}`})`,
151
159
  `Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : heartbeat !== void 0 ? "stale heartbeat (process exited)" : "not started"}`,
152
160
  ...regionLists.flatMap(({ region, accounts }) => [`${REGION_LABELS[region]} accounts: ${accounts.length} (own copy ${workbuddyOwnAuthPath(region)})`, ...accounts.map((account) => ` - ${account.accountName === "" ? "(unnamed)" : account.accountName} (${account.id})${account.selected ? " [selected]" : ""} expires ${new Date(account.tokenExpiresAtMs).toISOString()}`)]),
153
161
  `Static fallback models: CN ${report.fallbackModels.cn}, Global ${report.fallbackModels.global}`,
package/lib/client.js CHANGED
@@ -513,11 +513,19 @@ window.__ModuleLoader__.load({
513
513
  * @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
514
514
  */
515
515
  async function writePoolPreferences(scope, region, preferences) {
516
- const next = nextRegionPool(scope.getSnapshot().value, region, preferences)[region];
516
+ const slot = {
517
+ enabled: preferences.enabled,
518
+ targetModelId: preferences.targetModelId,
519
+ memberAccountIds: preferences.memberAccountIds
520
+ };
521
+ if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
522
+ const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
517
523
  await writeField(scope, "regions", region, next, (readBack) => {
518
524
  const pool = readBack?.pool;
519
525
  if (pool === void 0 || pool === null) return false;
520
526
  if (pool["enabled"] !== preferences.enabled || pool["targetModelId"] !== preferences.targetModelId) return false;
527
+ const wantedTokens = preferences.probeInputTokens ?? 0;
528
+ if ((pool["probeInputTokens"] ?? 0) !== wantedTokens) return false;
521
529
  const stored = pool["memberAccountIds"];
522
530
  if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
523
531
  const wanted = new Set(preferences.memberAccountIds);
@@ -619,6 +627,53 @@ window.__ModuleLoader__.load({
619
627
  return message.replace(/^transport error:\s*/u, "");
620
628
  }
621
629
  //#endregion
630
+ //#region src/client/probe-age.ts
631
+ const MINUTE_MS$1 = 6e4;
632
+ const HOUR_MS$1 = 60 * MINUTE_MS$1;
633
+ const DAY_MS$1 = 24 * HOUR_MS$1;
634
+ /**
635
+ * How long ago a measurement was taken, coarsely: "刚刚", "N 分钟前",
636
+ * "N 小时前", "N 天前".
637
+ *
638
+ * Coarse on purpose, for the same reason `remainingText` rounds: a figure that
639
+ * changes every second reads as live telemetry rather than as "how stale is
640
+ * this", which is the only question being asked here.
641
+ *
642
+ * A clock skew that puts the measurement in the FUTURE (a stored `atMs` ahead
643
+ * of `nowMs`, from a machine whose clock moved backwards) is reported as
644
+ * "刚刚" rather than as a negative age: the record is real, and the card must
645
+ * not print "-3 分钟前".
646
+ */
647
+ function probeAgeText(t, atMs, nowMs) {
648
+ const age = nowMs - atMs;
649
+ if (age < MINUTE_MS$1) return t("row.poolProbeJustNow");
650
+ if (age < HOUR_MS$1) return t("row.poolProbeMinutesAgo", { count: Math.round(age / MINUTE_MS$1) });
651
+ if (age < 2 * DAY_MS$1) return t("row.poolProbeHoursAgo", { count: Math.round(age / HOUR_MS$1) });
652
+ return t("row.poolProbeDaysAgo", { count: Math.round(age / DAY_MS$1) });
653
+ }
654
+ //#endregion
655
+ //#region src/probe.ts
656
+ /**
657
+ * The probe sizes the card offers, in tokens.
658
+ *
659
+ * A closed set, each step a meaningful position relative to the measured 20k~30k
660
+ * threshold: below it (10k/20k) proves an account can serve SHORT requests and
661
+ * is the cheap choice; at it (30k) is the size that finally covers the whole
662
+ * measured window; above it (50k/100k) is for conversations that routinely run
663
+ * long, where a smaller probe would report "usable" for an account that refuses
664
+ * the very next message.
665
+ *
666
+ * Exported so the schema's description, the card's dropdown, and the coercion
667
+ * below cannot drift apart into three different menus.
668
+ */
669
+ const PROBE_INPUT_TOKEN_CHOICES = [
670
+ 1e4,
671
+ 2e4,
672
+ 3e4,
673
+ 5e4,
674
+ 1e5
675
+ ];
676
+ //#endregion
622
677
  //#region src/client/remaining.ts
623
678
  const MINUTE_MS = 6e4;
624
679
  const HOUR_MS = 60 * MINUTE_MS;
@@ -989,10 +1044,11 @@ window.__ModuleLoader__.load({
989
1044
  const saved = {
990
1045
  enabled: pool?.enabled ?? false,
991
1046
  targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
1047
+ probeInputTokens: pool?.probeInputTokens ?? 0,
992
1048
  memberAccountIds: pool?.memberAccountIds ?? []
993
1049
  };
994
1050
  const active = draftRegion === region && draft !== null ? draft : saved;
995
- const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
1051
+ const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
996
1052
  /**
997
1053
  * Membership that actually resolves to a local sign-in — what a batch runs on.
998
1054
  *
@@ -1376,10 +1432,24 @@ window.__ModuleLoader__.load({
1376
1432
  const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
1377
1433
  const excluded = exclusionText(t, account);
1378
1434
  const probe = account.probe;
1435
+ const outcomeLine = probe === void 0 ? t("row.poolNeverTested") : probe.retryAtMs !== void 0 ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryAt", { at: formatShort(probe.retryAtMs) })}` : probe.outcome === "rate-limited" || probe.outcome === "out-of-credit" ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}` : outcomeText(t, probe.outcome);
1436
+ const remaining = probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now());
1437
+ const reason = probe === void 0 ? void 0 : inlineProbeReason(probe.outcome, probe.message);
1438
+ const age = probe === void 0 ? void 0 : probeAgeText(t, probe.atMs, Date.now());
1439
+ const probeLabel = probe === void 0 ? void 0 : probe.source === void 0 ? {
1440
+ text: t("row.poolProbeSourceUnknown"),
1441
+ className: "dsm-workbuddy-pool-prov-tag dsm-workbuddy-pool-prov-unknown"
1442
+ } : probe.source === "live-request" ? {
1443
+ text: t("row.poolProbeSourceLive"),
1444
+ className: "dsm-workbuddy-pool-prov-tag dsm-workbuddy-pool-prov-live"
1445
+ } : {
1446
+ text: t("row.poolProbeSourceTest"),
1447
+ className: "dsm-workbuddy-pool-prov-tag"
1448
+ };
1379
1449
  const probeLine = [
1380
- probe === void 0 ? t("row.poolNeverTested") : probe.retryAtMs !== void 0 ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryAt", { at: formatShort(probe.retryAtMs) })}` : probe.outcome === "rate-limited" || probe.outcome === "out-of-credit" ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}` : outcomeText(t, probe.outcome),
1381
- probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now()),
1382
- probe === void 0 ? void 0 : inlineProbeReason(probe.outcome, probe.message)
1450
+ outcomeLine,
1451
+ remaining,
1452
+ reason
1383
1453
  ].filter((part) => part !== void 0 && part !== "").join(" · ");
1384
1454
  return (0, react.createElement)("div", {
1385
1455
  className: `dsm-workbuddy-pool-row${account.current ? " dsm-workbuddy-pool-row-current" : ""}`,
@@ -1396,7 +1466,7 @@ window.__ModuleLoader__.load({
1396
1466
  }), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name), account.current ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-tag" }, t("row.poolCurrentBadge")) : null, (0, react.createElement)("span", { className: checked ? "dsm-workbuddy-pool-member" : "dsm-workbuddy-pool-not-member" }, checked ? t("row.poolMember") : t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShort(probe.atMs) })) : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-excluded" }, excluded))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-credits" }, account.credits === void 0 ? (0, react.createElement)("span", null, "—") : (0, react.createElement)("b", null, formatNumber$1(account.credits)), account.expiringSoon !== void 0 && account.expiringSoon > 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon" }, t("row.poolCreditSoon", { count: formatNumber$1(account.expiringSoon) })) : account.nearestExpiryMs === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon-plain" }, t("row.poolCreditNearest", { at: formatShort(account.nearestExpiryMs) }))), (0, react.createElement)("span", {
1397
1467
  className: "dsm-workbuddy-pool-probe",
1398
1468
  ...probe?.message === void 0 ? {} : { title: probe.message }
1399
- }, probeLine), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
1469
+ }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
1400
1470
  className: "dsm-workbuddy-pool-unknown",
1401
1471
  title: t("row.poolNeverTested")
1402
1472
  }, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null);
@@ -1431,7 +1501,22 @@ window.__ModuleLoader__.load({
1431
1501
  }, t("row.poolTargetStaleOption", { model: active.targetModelId })) : null, ...(pool.catalog ?? []).map((model) => (0, react.createElement)("option", {
1432
1502
  value: model.id,
1433
1503
  key: model.id
1434
- }, model.creditMultiplier === void 0 ? model.name : `${model.name} (x${model.creditMultiplier.toFixed(2)})`)))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-save-bar" }, (0, react.createElement)("a", {
1504
+ }, model.creditMultiplier === void 0 ? model.name : `${model.name} (x${model.creditMultiplier.toFixed(2)})`))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolProbeSizeLabel")), (0, react.createElement)("span", null, t("row.poolProbeSizeHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
1505
+ className: "dsm-workbuddy-pool-select",
1506
+ value: String(active.probeInputTokens),
1507
+ disabled: !canEdit,
1508
+ onChange: (event) => {
1509
+ const parsed = Number.parseInt(event.currentTarget.value, 10);
1510
+ const next = Number.isFinite(parsed) ? parsed : 0;
1511
+ onEdit((current) => ({
1512
+ ...current,
1513
+ probeInputTokens: next
1514
+ }));
1515
+ }
1516
+ }, (0, react.createElement)("option", { value: "0" }, t("row.poolProbeSizeDefault")), ...PROBE_INPUT_TOKEN_CHOICES.map((tokens) => (0, react.createElement)("option", {
1517
+ value: String(tokens),
1518
+ key: String(tokens)
1519
+ }, t("row.poolProbeSizeOption", { tokens: String(tokens / 1e3) }))))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-save-bar" }, (0, react.createElement)("a", {
1435
1520
  className: "dsm-workbuddy-usage-cheer",
1436
1521
  href: WORKBUDDY_GITHUB_URL,
1437
1522
  target: "_blank",
@@ -1705,6 +1790,14 @@ window.__ModuleLoader__.load({
1705
1790
  .dsm-workbuddy-pool-soon{font-size:11px;line-height:16px;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
1706
1791
  .dsm-workbuddy-pool-soon-plain{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#999)}
1707
1792
  .dsm-workbuddy-pool-probe,.dsm-workbuddy-pool-checkin{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#999);min-width:0;overflow-wrap:anywhere}
1793
+ .dsm-workbuddy-pool-probe{display:flex;flex-direction:column;gap:4px}
1794
+ .dsm-workbuddy-pool-probe-main{color:var(--dsw-alias-label-secondary,#c6c9d0)}
1795
+ .dsm-workbuddy-pool-probe-prov{display:flex;align-items:center;gap:5px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary,#999);min-width:0;flex-wrap:wrap}
1796
+ .dsm-workbuddy-pool-prov-tag{padding:1px 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-tertiary,#999);font-size:10px;font-weight:600;line-height:15px;white-space:nowrap}
1797
+ .dsm-workbuddy-pool-prov-live{background:rgba(245,158,11,.16);color:var(--dsw-alias-state-warn-primary,#f59e0b)}
1798
+ .dsm-workbuddy-pool-prov-unknown{background:var(--dsw-alias-bg-layer-3,#2a2c33);border:1px solid var(--dsw-alias-border-l1,#2c2d31);color:var(--dsw-alias-label-secondary,#c6c9d0)}
1799
+ .dsm-workbuddy-pool-prov-sep{color:var(--dsw-alias-label-dimmed,#9aa0a6);opacity:.7}
1800
+ .dsm-workbuddy-pool-prov-age{color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap}
1708
1801
  .dsm-workbuddy-pool-members{display:flex;flex-direction:column;gap:10px}
1709
1802
  .dsm-workbuddy-pool-members-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
1710
1803
  .dsm-workbuddy-pool-members-title{margin:0 0 3px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
@@ -2073,6 +2166,10 @@ window.__ModuleLoader__.load({
2073
2166
  text: t("row.probeCredentialRejected"),
2074
2167
  tone: "bad"
2075
2168
  };
2169
+ case "policy-rejected": return {
2170
+ text: t("row.probePolicyRejected"),
2171
+ tone: "bad"
2172
+ };
2076
2173
  case "not-found": return {
2077
2174
  text: t("row.probeNotFound"),
2078
2175
  tone: "bad"
@@ -3024,6 +3121,7 @@ window.__ModuleLoader__.load({
3024
3121
  "row.probeOutOfCreditUnknown": "✗ Not enough credits · the upstream gave no time for when it frees up",
3025
3122
  "row.probeOutOfCreditAt": "✗ Not enough credits · quota refreshes {at}",
3026
3123
  "row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
3124
+ "row.probePolicyRejected": "✗ WorkBuddy's server refused this request under its content policy — re-signing in won't help; try the desktop app or another region/account",
3027
3125
  "row.probeNotFound": "✗ The upstream does not know this model",
3028
3126
  "row.probeUnavailable": "✗ The upstream could not be reached — no usable answer came back",
3029
3127
  "row.probeFailed": "✗ Test failed",
@@ -3072,6 +3170,13 @@ window.__ModuleLoader__.load({
3072
3170
  "row.remainingMinutes": "in about {count} min",
3073
3171
  "row.remainingHours": "in about {count} h",
3074
3172
  "row.remainingDays": "in about {count} d",
3173
+ "row.poolProbeJustNow": "just now",
3174
+ "row.poolProbeMinutesAgo": "{count} min ago",
3175
+ "row.poolProbeHoursAgo": "{count} h ago",
3176
+ "row.poolProbeDaysAgo": "{count} d ago",
3177
+ "row.poolProbeSourceTest": "tested",
3178
+ "row.poolProbeSourceLive": "hit by a live request",
3179
+ "row.poolProbeSourceUnknown": "source unknown",
3075
3180
  "row.poolSettingsTitle": "Account pool settings",
3076
3181
  "row.poolSettingsHint": "These are preferences: they take effect when you save (same as the model list). Test results are written automatically and are not affected by this button.",
3077
3182
  "row.poolEnabled": "Enable the account pool",
@@ -3082,6 +3187,10 @@ window.__ModuleLoader__.load({
3082
3187
  "row.poolTargetLabel": "Target test model",
3083
3188
  "row.poolTargetHint": "Auto (default) picks a zero-multiplier model from this region's catalog, preferring the largest context window. The two regions can pick different models. A manual choice always wins.",
3084
3189
  "row.poolTargetAutoOption": "Auto — pick a free model",
3190
+ "row.poolProbeSizeLabel": "Test request size",
3191
+ "row.poolProbeSizeHint": "How much text a test sends. The upstream refuses a request once it grows past roughly 20K–30K tokens, so a test smaller than your real conversations answers \"usable\" for an account that would refuse them. Bigger tests are more truthful but cost more credit each.",
3192
+ "row.poolProbeSizeDefault": "Default (25K)",
3193
+ "row.poolProbeSizeOption": "{tokens}K tokens",
3085
3194
  "row.poolSaved": "Save",
3086
3195
  "row.poolSaving": "Saving…",
3087
3196
  "row.poolSaveFailed": "Could not save: {message}",
@@ -3200,6 +3309,7 @@ window.__ModuleLoader__.load({
3200
3309
  "row.probeOutOfCreditUnknown": "✗ 积分不足 · 上游未给出何时恢复",
3201
3310
  "row.probeOutOfCreditAt": "✗ 积分不足 · 积分包 {at} 刷新",
3202
3311
  "row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
3312
+ "row.probePolicyRejected": "✗ WorkBuddy 服务端按内容策略拒绝了这次请求——重新登录无效;可在桌面端验证或切换区域/账号",
3203
3313
  "row.probeNotFound": "✗ 上游不认识这个模型",
3204
3314
  "row.probeUnavailable": "✗ 连不上上游——上游没有给出可用答复",
3205
3315
  "row.probeFailed": "✗ 测试失败",
@@ -3248,6 +3358,13 @@ window.__ModuleLoader__.load({
3248
3358
  "row.remainingMinutes": "约 {count} 分钟后",
3249
3359
  "row.remainingHours": "约 {count} 小时后",
3250
3360
  "row.remainingDays": "约 {count} 天后",
3361
+ "row.poolProbeJustNow": "刚刚",
3362
+ "row.poolProbeMinutesAgo": "{count} 分钟前",
3363
+ "row.poolProbeHoursAgo": "{count} 小时前",
3364
+ "row.poolProbeDaysAgo": "{count} 天前",
3365
+ "row.poolProbeSourceTest": "测得",
3366
+ "row.poolProbeSourceLive": "聊天撞上",
3367
+ "row.poolProbeSourceUnknown": "来源未知",
3251
3368
  "row.poolSettingsTitle": "账号池设置",
3252
3369
  "row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
3253
3370
  "row.poolEnabled": "启用账号池",
@@ -3258,6 +3375,10 @@ window.__ModuleLoader__.load({
3258
3375
  "row.poolTargetLabel": "目标测试模型",
3259
3376
  "row.poolTargetHint": "「自动」(默认)会从本区域目录里挑一个倍率为 0 的免费模型,多个候选取上下文窗口最大的。国内版与国际版可能挑到不同的模型。手动指定优先于自动。",
3260
3377
  "row.poolTargetAutoOption": "自动 —— 挑一个免费模型",
3378
+ "row.poolProbeSizeLabel": "测试请求体积",
3379
+ "row.poolProbeSizeHint": "测试时发送多少文本。上游在请求大约超过 2 万~3 万 token 时会开始拒绝,所以测试体积若小于你真实对话的体积,就会出现「测出可用、实际一发就被限」。体积越大测得越准,但每次测试消耗的积分也越多。",
3380
+ "row.poolProbeSizeDefault": "默认(2.5 万)",
3381
+ "row.poolProbeSizeOption": "{tokens} 万 token",
3261
3382
  "row.poolSaved": "保存",
3262
3383
  "row.poolSaving": "保存中…",
3263
3384
  "row.poolSaveFailed": "保存失败:{message}",