dsh-connect-workbuddy 4.9.4 → 4.10.1
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 +80 -0
- package/README.en.md +4 -3
- package/README.md +3 -2
- package/docs/WINDOWS.md +5 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +248 -10
- package/lib/{host-heartbeat-BpArQi1Q.js → host-heartbeat-Cq4_pJ_l.js} +211 -21
- package/lib/index.d.ts +184 -10
- package/lib/index.js +181 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.10.1 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **patch**:没有接口、配置项或请求结构的变更,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。**但它是 issue #77 的正解**——4.9.3 起的 `detail=fast` 只去掉了上游调用,真正吃掉 60 秒的本地开销一直没动;本版把每次打开设置页的 `reg query` 子进程从约 **135 个**降到 **3 个以内**。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **issue #77(第二轮):字段密钥的探测按「失败也记住」缓存,一次打开页面不再逐文件重跑注册表探测**
|
|
10
|
+
- **报告与归因**:4.9.3 加了 `detail=fast` 之后,用户在 4.9.4、4.10 上仍然等 60 s。其 CPU profile 指出:19.1 s 的请求里 **15.3 s 在 `spawnSync`**,来自约 **135 个同步 `reg query` 子进程**——`findWorkbuddyAppExecutable()` 每次请求被调用 45 次,每次查三个卸载 hive。
|
|
11
|
+
- **根因**:`readAtRestKey()` 只缓存**成功**。当凭据字段是 `$wbEncrypted` 而桌面端又不在候选目录里时,每次探测都会重跑整套发现;`scanAll()` 是**逐文件**调用的,一次打开页面又要读多次,两个乘数叠在一起。
|
|
12
|
+
- **第二个乘数**:`SCAN_SHARE_WINDOW_MS` 从扫描的**开始**时刻计窗——比窗口更慢的那次扫描**在完成时就已经过期**,永远无法服务它之后的读取。现在从**完成**时刻计窗,并单独保留 `invalidateScan()` 供写入路径强制刷新(写完再读不会看到旧列表)。
|
|
13
|
+
- **两处顺手的去重**:`findWorkbuddyAppExecutableWithSource()` 会在默认布局 → 注册表 → `findWorkbuddyAppExecutable()` 的回落链里对同一个答案问两遍注册表,现在只问一遍;失败结果带上过期时间缓存,所以「先不装、后装 App」的用户**不需要重启**就能被看见。
|
|
14
|
+
- **代价**:第一次真正的读取仍付一次探测的钱(3 次 `reg`),此后每一轮读取都是 0。
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
|
|
18
|
+
- **`tests/issue77-at-rest-key-cache.spec.ts`:用「子进程次数」钉住,不靠计时措辞**
|
|
19
|
+
- `process.platform` 影子成 `win32` + `execFileSync` 在模块边界打桩,于是**任何宿主(含 CI 的 macOS/Linux)**都会真的走进 Windows 分支并把 `reg` 调用数出来:一次探测恰好 3 次、5 个加密凭据文件的一轮读取(`accounts` → `status` → `current` → `accounts`)仍是 3 次、失败结果被记住、过期后重新探测。
|
|
20
|
+
- **变异验证**:把修复回退后这两条断言分别报 **15 ≠ 3** 与 **9 ≠ 3**;三个用例在修复后全绿。写入后再读的用例在前修复/后修复两种状态下都绿——它守的是「共享窗口不得变成 TTL」,是回归护栏而非修复证据。
|
|
21
|
+
- 文件里的凭据夹具用**真实的加密字段形状** `{$wbEncrypted: 1, envelope: "<base64>"}`;写成嵌套对象会被 `isEncryptedFieldWrapper()` 判为「未加密」,让整条用例静默走错分支(写测试时踩到过,已在夹具注释里记下)。
|
|
22
|
+
|
|
23
|
+
## 4.10.0 (2026-10-19)
|
|
24
|
+
|
|
25
|
+
> 版本号是 **minor**:4.9.5 / 4.9.6 两个 patch 从未单独发布到 npm(npm 上仍是 4.9.4),本次一次性带上它们,并新增福利活动的完整链路。**发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。**
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
- **issue #79:福利活动自动发现 + 单账号领取(4.9.4 → 4.10.0 的主线)**
|
|
30
|
+
- **自动发现**:卡片逐账号标出上游活动(模型服务异常补偿 / 新人首月赠送)里**还没领的那一份**。数据来自上游两个只读状态接口 `/billing/meter/compensation-status` 与 `/billing/meter/check-gift-claimed`(注意**没有 `/v2` 前缀**,探 `/v2/...` 会 404)。
|
|
31
|
+
- **判定规则完全由上游状态决定**:`active && !claimed` 才显示;`claimed` 整行消失;活动过期(上游把 `active` 翻成 `false`)自动消失。**不拿本地时钟去比 `end_time`**,本机时间偏了也不会与服务端打架;`end_time` 只用于 3 天内临期高亮。
|
|
32
|
+
- `active` 单独看**不行**:已领取的账号 `active` 仍为 `true`,只看它会让领过的活动永远挂着待领取。
|
|
33
|
+
- 读取复用签到管线:每账号一次、30 分钟缓存、账号池关闭时不发请求;任一接口失败按「未读」处理,**不会把一次失败缓存成「全都没福利」**。
|
|
34
|
+
- **单账号「领取」按钮**:每个待领取项后一个实心琥珀色胶囊按钮,**一次只领一个账号的一个活动**。没有「全部领取」——一次点击花掉的是一份不可撤销的真实权益。
|
|
35
|
+
- 新路由 `POST /plugins/dsh-connect-workbuddy/benefit/claim?accountId=&kind=`,沿用写路由的 POST + loopback 守卫,并**强制要求 `accountId`**,请求本身无法批量领取。
|
|
36
|
+
- 领取前**重新读取**状态再判定;已领取/已过期一律 409 且不调用上游。领取后用上游**新读数**作答并刷新卡片,不做乐观更新。
|
|
37
|
+
- **领取成功后清掉该账号的福利缓存**:这是本次修掉的最关键 bug。缓存 TTL 30 分钟且此前从不因领取失效,于是「积分涨了、按钮还在」,用户会以为领取失败而重复点击,第二次被 409 拒绝。
|
|
38
|
+
- **每行按钮只联动自己那一行**:此前行的 busy 标志传的是「有任一行动作」,点一行会让五行同时变成「领取中…」,看起来像一次领了整池。
|
|
39
|
+
- 实测:真实账号 `19dea97d` 领取后 `claimed` 由 false 变 true,500 积分到账;同一批次 6 个账号 × 500 积分待领取,截止 10/22。
|
|
40
|
+
|
|
41
|
+
### Docs
|
|
42
|
+
|
|
43
|
+
- **描述与 README 同步到插件实际能力**:`package.json` 的 `description`、市场提交草稿的 `description.zh` / `description.en`、两个 README 的标题下 tagline 与开头段,统一改写为「国内版 + 国际版接入 / 每次刷新按上游目录自动读取每个模型的积分倍率 / 逐模型图片输入开关 / 多账号切换与账号池自动换号 / 每日签到与只读积分概览」。旧描述只写了「模型接入 + 只读积分概览 + 模型管理」,把 3.x~4.9 长出来的能力全落在了外面。
|
|
44
|
+
- **更正一处不成立的表述**:描述草稿里曾有「费率随错峰时段变化」的说法。全仓(`src/`、`tests/`、`docs/`、两个 README)与 WorkBuddy 桌面端 asar 里都没有任何峰谷/时段折扣逻辑,`git log -S` 也从无此词;插件真正做的是**从上游目录解析每个模型自带的积分倍率**(`x0.79` 这种,解析不出就不显示,绝不猜),且该倍率确实会被上游改动(`deepseek-v4.1-flash` 已从 0 变成 0.11)。会随「时段」变的那个是**免费模型的 token 定额**,不是价格,因此文案按实测口径写「随上游目录刷新更新」,不承诺峰谷计价。
|
|
45
|
+
- **README 补上「福利活动」条目(中英双语)**:4.9.4 的待领取标记与 4.9.6 的单账号「领取」按钮此前在两个 README 里**一次都没提过**。新增条目写清:数据只来自上游两个状态接口、标记阶段只读、三条显示规则由上游状态决定(不用本地时钟比 `end_time`)、一次只领一个账号的一个活动、写路由强制 `accountId` 所以无法批量、领取前重读状态、领取后用上游新读数刷新并清掉该账号的福利缓存。
|
|
46
|
+
- **卡片内说明文案(`src/client/locales.ts` 的 `row.desc`,中英同步)**同步改写,与上面的描述口径一致;两侧键仍严格 1:1。
|
|
47
|
+
- **修 EN README 的编号重复**:自动换号那一节有两个 `7.`,第二条(每次重试写一行 `warn`)改为 `8.`。
|
|
48
|
+
|
|
49
|
+
### Fixes
|
|
50
|
+
|
|
51
|
+
- **裁剪福利说明不再撑破积分列**:福利文案与按钮曾被挤成竖排「领取」二字并压住右侧目标模型列;胶囊改为 `white-space:nowrap` + 固定 19px 高 + `flex:none`,让列宽只能压缩文字、压不了按钮。
|
|
52
|
+
- **「领取」二字的视觉居中**:无头 Chrome 像素扫描测得 CJK 字形墨迹比行盒中心偏低约 1px,用 `padding-top:1px` 补偿(`transform` 与 `padding-bottom` 两个方向都试过,都会把墨迹推得更低,已弃用)。
|
|
53
|
+
- **CHANGELOG 结构:补回丢失的 `## 4.9.5 (2026-10-19 · 未单独发布,并入 4.10.0)` 标题**。`6bf825a`(feat: 单账号领取福利按钮)把顶部标题改写成 4.9.6 时,把 4.9.5 那一节的正文留在了下面却没有版本标题——于是 4.9.5 的 Features 看起来像是 4.9.6 的一部分,而 `git tag` 与 npm 上它确实是一个独立版本。现按 `cad9ec3`(chore(release): 4.9.5)的原样恢复标题。
|
|
54
|
+
|
|
55
|
+
## 4.9.6 (2026-10-19 · 未单独发布,并入 4.10.0)
|
|
56
|
+
|
|
57
|
+
> 版本号是 **patch**:新增一个写路由与行内按钮,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。
|
|
58
|
+
|
|
59
|
+
### Features
|
|
60
|
+
|
|
61
|
+
- **issue #79:账号池卡片提供单账号「领取」按钮**
|
|
62
|
+
- 每个待领取的福利后面带一个按钮,**一次只领一个账号的一个活动**;没有任何「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益。
|
|
63
|
+
- 新路由 `POST /plugins/dsh-connect-workbuddy/benefit/claim?accountId=&kind=`,沿用写路由的 POST + loopback 守卫,并**强制要求 accountId**——请求本身无法批量领取。
|
|
64
|
+
- 领取前**重新读取**状态再判定(窗口内 && 未领取),所以过期的卡片不会触发无谓写入;已领取/已过期一律 409 且不调用上游。
|
|
65
|
+
- 领取后用上游的**新读数**作答并刷新卡片,不做乐观更新:即使上游静默失败,也不会留下「已领取」的假象。
|
|
66
|
+
- 实测:真实账号 `19dea97d` 领取后 `claimed` 由 false 变 true,500 积分到账。
|
|
67
|
+
|
|
68
|
+
## 4.9.5 (2026-10-19 · 未单独发布,并入 4.10.0)
|
|
69
|
+
|
|
70
|
+
> 版本号是 **patch**:只新增只读查询与界面标记,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。
|
|
71
|
+
|
|
72
|
+
### Features
|
|
73
|
+
|
|
74
|
+
- **issue #79:账号池卡片标出「有福利还没领」**
|
|
75
|
+
- 数据来自上游自己的两个活动状态接口(`/billing/meter/compensation-status`、`/billing/meter/check-gift-claimed`),**只读**;`claim-compensation` / `claim-gift` 两个写接口从不调用。
|
|
76
|
+
- 三条显示规则都由上游状态直接决定,前端不维护状态:
|
|
77
|
+
- `active && !claimed` → 显示待领取。
|
|
78
|
+
- `claimed` → 整行不出现(领过的不再骚扰)。
|
|
79
|
+
- 活动过期 → `active` 翻 `false`,整行自动消失,**不用本地时钟比 `end_time`**(时钟偏了不会与服务端打架);`end_time` 只用于 3 天内临期高亮。
|
|
80
|
+
- `active` 不能单独判断:已领取的账号 `active` 仍为 `true`,只看它会让领过的账号永远挂着待领取。
|
|
81
|
+
- 读取复用 `checkedInToday` 的管线:每账号一次、TTL 30 分钟、池关闭不发请求;任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。
|
|
82
|
+
|
|
3
83
|
## 4.9.4 (2026-10-19)
|
|
4
84
|
|
|
5
85
|
> 版本号是 **patch**:只改**错误文案的取材方式**,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变(换号规则的变化只有一处「不再多问一次」的减少,见下)。但它修的是一个**会把人送去查错地方**的故障——一个普通的 400 被渲染成「账号池空了」,而上游唯一那句可执行建议被截断丢掉。
|
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-workbuddy</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><b>Connect locally signed-in WorkBuddy models to DeepSeek Harness, with
|
|
7
|
+
<p align="center"><b>Connect locally signed-in WorkBuddy models — CN and international — to DeepSeek Harness: read each model's credit multiplier automatically, toggle image input per model, switch between multiple accounts, and keep daily check-in with a read-only credits overview in capsule readouts on the card and in the composer.</b></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/dsh-connect-workbuddy"><img src="https://img.shields.io/npm/v/dsh-connect-workbuddy?style=flat-square&label=npm&color=cb3837" alt="npm version"></a>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
[English](README.en.md) | 中文
|
|
19
19
|
|
|
20
|
-
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — **both the CN app and the international WorkBuddy AI app are supported** — with an account pool and
|
|
20
|
+
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — **both the CN app and the international WorkBuddy AI app are supported** — with an account pool, selectable model management, and a set of read-only capabilities built around credits: **every catalog refresh reads each model's credit multiplier from upstream** (the `x0.79` figure comes entirely from the upstream catalog; the plugin neither hard-codes it nor guesses), image input is toggled per model, multiple accounts can be switched or fail over automatically through the pool, and daily check-in plus read-only credit overviews shown as compact pills on the card and in the composer. **The CN and international sides are two parallel providers (`workbuddy` / `workbuddy-global`) that can be used at the same time**; the settings card separates them with tabs for convenient management. All it needs is a WorkBuddy sign-in on this machine — or **one sign-in from the browser**.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
@@ -33,6 +33,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
33
33
|
|
|
34
34
|
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="Composer credit panel: a per-account balance table for the domestic region, with a refresh button and a last-refreshed time" />
|
|
35
35
|
- **Account pool: batch actions across several accounts, plus automatic failover** — tick the accounts you want into a pool and you get **one-click check-in for every account**, **one-click testing of every account against a model you choose** (defaulting to the region's zero-multiplier free model), and with **Enable the account pool** switched on, **automatic failover**: a request that fails upstream is retried against the pool's other usable accounts, in order, until one serves it or none is left. Membership starts **empty and is an explicit opt-in**: "signed in on this machine" is not the same as "spend this account's credits and claim its rewards". The pool's **ranking decides who serves every request** (usable first, then expiring-credits-first, higher credits, freshest credential), and on a failure the next usable member is tried. It **never writes back your saved selection** — switch the pool off and your own account applies again immediately. **The switch only decides whether the PLUGIN chooses** — with it off, the plugin stops picking and switching accounts for you, but check-in and testing stay available (they are manual actions). Testing reuses the model rows' real-volume probe, so it **does cost real credits**; **automatic check-in is deliberately not offered** — a check-in claims real rewards, so it only ever runs when you press the button. See [Account pool](#account-pool).
|
|
36
|
+
- **Benefits: a marker for what is unclaimed, plus a per-account Claim button** — the card marks, account by account, anything in the upstream's benefit campaigns (compensation / gifts) that **has not been claimed yet**. The data comes from the upstream's own two status endpoints; in the marking stage the plugin is **read-only** and never calls the two write endpoints. Three display rules all follow the upstream's state: `active && !claimed` shows the row, `claimed` removes it entirely, and an expired campaign (upstream flips `active` to `false`) disappears by itself — the plugin **never compares `end_time` against the local clock**, so a skewed machine cannot argue with the server; `end_time` only drives the "three days left" highlight. `active` alone is not enough to decide: an already-claimed account still reports `active: true`, and reading it alone would leave a claimed campaign hanging there forever. The read reuses the check-in pipeline (once per account, cached for 30 minutes, no request while the account pool is off), and a failing endpoint counts as "unread" rather than caching "nobody has benefits". Each row ends in a solid amber **Claim pill**, and one click claims **one campaign of one account**: there is no "claim everything", because a click spends one irrevocable real entitlement — and the write route **requires `accountId`**, so the request itself cannot batch. Before writing it **re-reads** the status and decides again; anything already claimed or expired returns 409 without touching upstream. After a successful claim it answers with the upstream's **fresh reading** and refreshes the card, with no optimistic update — even a silent upstream failure cannot leave a false "claimed" behind — and it also drops that account's cached benefit state, so the pill cannot linger on a stale status.
|
|
36
37
|
- **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
|
|
37
38
|
- **Add accounts by QR sign-in, import/export them in bulk, test or delete them one by one** — the WorkBuddy desktop app is no longer a prerequisite: the card's **Add account by QR code** (labelled **Sign in on the web** for the international build — that region uses a browser login page, there is no QR) opens a dialog and takes you **straight to the authorization page** in your browser; if the browser blocks that, the dialog's link, copy and **Open page again** are the fallback. Every credential lands in the plugin's own vault (`$DSH_HOME/workbuddy-vault/<region>/<accountId>.json`, one file per account), and the desktop app's auth files stay **read-only and are never modified**. Each row of the account table gains **Test** and **Remove**: a row test reuses the same probe as the batch run, so the two are comparable; removing deletes only the copy the plugin stored and **hides** the account: it disappears at once and the automatic poll cannot bring it back, while **Detect accounts again** restores it (signing in or importing the same account clears the hide too). **The desktop app's sign-in is never touched** — removing means "this plugin no longer lists it", not "sign out of the app". **Export / import accounts** moves credentials to another machine: pick accounts to export a JSON file → choose a file and read a **redacted preview** (field names and whether a token is present; the token itself never enters the preview) → tick what to import. An import overwrites per account rather than duplicating, and **only a record positively identified as belonging to that region is stored** — a domain the plugin cannot classify is skipped and counted as skipped. See [Account pool](#account-pool).
|
|
38
39
|
- **Credits live in the account pool** — the member table shows each account's remaining credits and nearest expiry, and credits are the ranking's second key (more credits serves first). The queries are **read-only** and consume no credits.
|
|
@@ -96,7 +97,7 @@ This plugin's standing rule is that it **never rewrites who pays** — your save
|
|
|
96
97
|
5. **Which do not:** a malformed request (HTTP 400). Every account answers the same 400, so walking the pool would only multiply the wait before the user sees an error they must act on anyway. A stream that dies **mid-flight** is not retried either — bytes are already on the wire, and replaying would splice two answers together.
|
|
97
98
|
6. **Members already known to be unusable are skipped** — a rate-limited account still inside its stated cooldown, or one whose credential the upstream rejected. **An account that was never tested counts as usable**: a freshly discovered sign-in must not be invisible just because nobody has measured it yet.
|
|
98
99
|
7. **A live failure is RECORDED as a measurement** (with the reset time the upstream stated), so the NEXT request starts from an available account instead of re-hitting the account that just refused it. Before this, only the manual batch test wrote measurements — so a just-limited account still read as untested and kept being picked first. HTTP 400 is the exception: that is the REQUEST being rejected, which says nothing about the account, and recording it would sideline a good one. Accounts not checked into the pool are not measured either, since the tick is what grants "you may spend this account's credits".
|
|
99
|
-
|
|
100
|
+
8. Every retry leaves a `warn` line in the host log naming the failure and the account that follows.
|
|
100
101
|
|
|
101
102
|
### What the switch actually controls
|
|
102
103
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-workbuddy</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><b>把本机登录的 WorkBuddy
|
|
7
|
+
<p align="center"><b>把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness —— 每次刷新自动读取每个模型的积分倍率,逐模型开关图片输入,多账号可切换,并带每日签到与只读积分概览(卡片与输入框里的胶囊读数)。</b></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="README.en.md">English</a> ·
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<a href="https://dshfind.com/plugins/dingminhua/dsh-connect-workbuddy"><img src="https://dshfind.com/api/badge/dingminhua/dsh-connect-workbuddy" alt="dshfind plugin"></a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
|
-
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH
|
|
27
|
+
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH 的模型选择器,同时提供账号池、可勾选的模型管理,以及一套围绕积分的只读能力:**每次刷新按上游目录自动读取每个模型的积分倍率**(`x0.79` 这种,倍率完全由上游给出,插件不写死也不猜)、逐模型开关图片输入、多账号切换与账号池自动换号、每日签到、以及卡片与输入框里胶囊式的只读积分读数。**国内版与国际版是两个并行的供应商(`workbuddy` / `workbuddy-global`),可同时使用**;插件设置卡片以 tab 区分两者,方便统一管理。只需本机 WorkBuddy 的登录态,**或在浏览器里登录一次**即可。
|
|
28
28
|
|
|
29
29
|
## 功能特性
|
|
30
30
|
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="对话输入框积分面板:国内版账号余额表格、刷新积分按钮与最近刷新时间" />
|
|
42
42
|
- **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型);开启**启用账号池**即同时打开**自动换号**——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);**开启后由池子的排序决定每一次请求用谁**(可用优先,其次快过期先花、积分高、凭据更新的);失败时依次用池内其他可用账号重试。排序**从不写回你保存的选择**——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。**这个开关只管「插件要不要自己决定」**:关掉它,插件不再替你选账号、也不再替你换号,但**签到与测试仍然可用**(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
|
|
43
|
+
- **福利活动:待领取标记 + 单账号「领取」按钮** —— 卡片会逐账号标出上游活动(补偿 / 赠礼)里**还没领的那一份**,数据来自上游自己的两个状态接口,标记阶段**只读**、两个写接口一次都不调用。显示规则完全由上游状态决定:`active && !claimed` 才显示,`claimed` 整行消失,活动过期(`active` 翻 `false`)自动消失——**不用本地时钟去比 `end_time`**,本机时间偏了也不会与服务端打架;`end_time` 只用于 3 天内临期高亮。`active` 单独看不行:已领取的账号 `active` 仍为 `true`,只看它会让领过的账号永远挂着待领取。读取复用签到的管线(每账号一次、30 分钟缓存、账号池关闭时不发请求),任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。行尾是一个实心琥珀色的**胶囊按钮「领取」**,一次只领**一个账号的一个活动**:没有「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益,写路由也**强制要求 `accountId`**,请求本身无法批量领取。领取前**重新读取**状态再判定,已领取或已过期一律 409 且不调用上游;领取后用上游的**新读数**作答并刷新卡片,不做乐观更新——即使上游静默失败也不会留下「已领取」的假象,成功后同时清掉该账号的福利缓存,按钮不会顶着旧状态赖着不走。
|
|
43
44
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
44
45
|
- **扫码添加账号、批量导入导出、逐账号测试/删除** —— 不必先装 WorkBuddy 桌面 App:卡片里的「扫码添加账号」(国际版为「网页登录」——那边是浏览器登录页,没有二维码)点开即弹出对话框,并在浏览器里**自动打开**授权页;被浏览器拦截时对话框内提供链接、复制与「重新打开页面」兜底。所有凭据统一落到插件自己的凭据库(`$DSH_HOME/workbuddy-vault/<region>/<accountId>.json`,一账号一文件),**桌面 App 的认证文件始终只读、永不被改动**。账号表格每一行右侧有「测试」与「删除」:测试与批量测试走同一套探测,结果可比;删除只删插件自己存的那份,并把这个账号**暂时隐藏**:删掉后立刻消失,也不会被自动轮询带回来;点「重新检测账号」即可找回(重新登录或导入同一账号也会清除隐藏记录)。**桌面 App 的登录始终不动**——删除是「本插件不再列出它」,不是退出 App 登录。「导出 / 导入账号」可把凭据搬到另一台机器:多选导出 JSON → 选文件后先看**脱敏预览**(只显示字段与是否有 token,明文不进预览)→ 勾选导入。导入按账号去重覆盖,**只有确认属于该区域**的记录才入库,认不出的域名会被跳过并如实计入「已跳过」。详见[账号池](#账号池)。
|
|
45
46
|
- **积分信息在账号池里** —— 成员表逐账号显示剩余积分与最近到期时间,排序的第二键就是积分(谁多谁优先)。查询是**只读**的、不消耗积分。
|
package/docs/WINDOWS.md
CHANGED
|
@@ -25,6 +25,9 @@ Windows 在本项目里不是「顺带能跑」的平台,而是**有专属分
|
|
|
25
25
|
| 3 | `src/host-heartbeat.ts` `processStartTimeMs()` → `processStartProbe()` | 走 PowerShell `Get-Process` 的 `StartTime`,以 UTC ISO 8601 输出供 `Date.parse` 解析 | PID 复用判定失准 → 心跳误判宿主存活 |
|
|
26
26
|
| 4 | 设置写入回读校验(`src/client/account-selection.ts`) | 写入后**回读校验**:profile 的 `cordis.patch.yml` 被占用会让 rename 覆盖失败,而 `set()` 的 promise 仍 resolve | 用户看到假确认(显示「已清除」但没落盘)→ issue #11 |
|
|
27
27
|
| 5 | 子进程启动选项:`src/host-heartbeat.ts` `PROCESS_PROBE_OPTIONS` 与 `src/at-rest.ts` `fetchAtRestKeyPayload()` | 一律带 `windowsHide: true`(POSIX 忽略该选项) | **宿主本身没有控制台**(Electron GUI 进程,实测 `MainWindowHandle = 0`),Windows 会给子进程**新建一个可见的控制台窗口** → 每次进程存活判定都在屏幕上闪一次黑框 |
|
|
28
|
+
| 6 | `src/at-rest.ts` `readAtRestKey()` → `findWorkbuddyAppExecutableWithSource()` | 字段密钥**不带参数**调用时才走注册表回退(`reg query` × 3 个卸载 hive);这个探测曾被**只按成功结果**缓存 | 桌面端不在候选目录、凭据又是加密的机器上,`scanAll()` 会**逐文件**重跑整套探测。issue #77 实测:19.1 s 的请求里 15.3 s 花在 `spawnSync`,约 **135 个 `reg query` 子进程**(`findWorkbuddyAppExecutable()` 被调用 45 次)→ 打开页面白等 60 s。POSIX 上这一支根本不跑,**在 macOS 开发机上永远看不到** |
|
|
29
|
+
|
|
30
|
+
**第 6 条为什么单列**:这是**第 5 条之外的第二个**「只在 Windows 才付费」形态——不是正确性错误,而是**成本随文件数 × 读取次数放大**的性能缺陷,且两个乘数都在 Windows 特有的分支里。`tests/issue77-at-rest-key-cache.spec.ts` 用 `process.platform` 影子 + `execFileSync` 打桩,在**任何宿主**上都能数出这三件事(失败结果被记住、一次探测 3 次 `reg`、5 个加密文件的一轮读取仍是 3 次)。
|
|
28
31
|
|
|
29
32
|
**第 5 条为什么单列**:它不是路径或逻辑错误,而是**只在「父进程无控制台」时才出现**的一类缺陷——而这恰是 GUI 宿主的常态、终端里的例外。在终端手工跑插件时父进程有控制台,子进程只是继承,**永远看不到这个现象**;它因此逃过了所有本地验证,直到在真机上以「父进程无控制台」的形态复现(§2 A 档)。
|
|
30
33
|
|
|
@@ -118,6 +121,7 @@ WMI 服务(`Win32_Process.Create`)创建的进程与 Electron 宿主同为
|
|
|
118
121
|
| 2.1.0(本次) | 宿主内每次进程存活判定都闪一次黑色控制台窗口 | 心跳探测未设 `windowsHide`,而宿主是无控制台的 GUI 进程(§1-5、§2 A 档取证) |
|
|
119
122
|
| 未发布(PR #44) | 「重新检测账号」没能把删掉的账号找回来,而界面什么都不说 | 隐藏名单删除被拒时异常被吞掉(`rm` 的 `force` 不忽略 `EPERM`)——详见下节(B 档) |
|
|
120
123
|
| 未发布(PR #44 追加) | `tests/windows-file-lock.spec.ts` 在真机上恒红,而 CI 恒绿 | **测试自身的清场竞态**(不是产品缺陷):`kill()` 只投递信号,内核关闭句柄在其后数毫秒,此窗口内 `rm` 必得 `EBUSY`——详见下节 |
|
|
124
|
+
| 4.10.1(issue #77 第二轮) | 打开设置页要等 60 s;CPU profile 里 19.1 s 的请求有 15.3 s 在 `spawnSync`(约 135 个 `reg query`) | 字段密钥的探测**只缓存成功**,且共享扫描窗口从**开始**时刻计窗——两个乘数叠在 `scanAll()` 的逐文件探测上(§1-6)。**根因跨平台**,Windows 只是把重复探测换成了同步子进程,才变成 60 秒 |
|
|
121
125
|
|
|
122
126
|
### 「重新检测账号」的静默失败(PR #44,**B 档 CI 实测**)
|
|
123
127
|
|
|
@@ -181,6 +185,7 @@ WMI 服务(`Win32_Process.Create`)创建的进程与 Electron 宿主同为
|
|
|
181
185
|
| 可执行文件候选与测试 | `src/at-rest.ts` `workbuddyAppExecutableCandidates()`;`tests/at-rest.spec.ts` |
|
|
182
186
|
| 心跳时间戳与测试 | `src/host-heartbeat.ts` `processStartTimeMs()` / `processStartProbe()`;`tests/host-heartbeat.spec.ts` |
|
|
183
187
|
| 子进程启动选项(`windowsHide`)与测试 | `src/host-heartbeat.ts` `PROCESS_PROBE_OPTIONS`、`src/at-rest.ts` `fetchAtRestKeyPayload()`;`tests/platform-standing.spec.ts`、`tests/host-heartbeat.spec.ts` |
|
|
188
|
+
| 字段密钥探测的缓存与注册表子进程计数 | `src/at-rest.ts` `readAtRestKey()` / `clearAtRestKeyCache()`、`SCAN_SHARE_WINDOW_MS`(`src/auth.ts`);`tests/issue77-at-rest-key-cache.spec.ts` |
|
|
184
189
|
| 真机文件占用取证与测试自身清场 | `tests/windows-file-lock.spec.ts`(`kill()` 后必须 `await` `exit` 再 `rm`,见 §3 末) |
|
|
185
190
|
| 写入回读校验与测试 | `src/client/account-selection.ts`;`tests/client-account-selection.spec.ts` |
|
|
186
191
|
| 设置隔离(供测试) | `vitest.config.ts`(`LOCALAPPDATA` / `APPDATA` / `HOME` / `USERPROFILE` / `XDG_CONFIG_HOME`) |
|
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-Cq4_pJ_l.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
|
@@ -44,6 +44,23 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
* costs nothing on the Host side either.
|
|
45
45
|
*/
|
|
46
46
|
const WORKBUDDY_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-workbuddy";
|
|
47
|
+
/**
|
|
48
|
+
* The page where a human claims a benefit: 「套餐与积分」.
|
|
49
|
+
*
|
|
50
|
+
* Named here rather than inline in the card because it is the page the benefit
|
|
51
|
+
* line points at when the plugin itself declines to claim (an unknown campaign
|
|
52
|
+
* kind). One constant keeps that pointer honest if the page moves.
|
|
53
|
+
*/
|
|
54
|
+
const WORKBUDDY_PLAN_URL = "https://www.workbuddy.cn/profile/plans-usage";
|
|
55
|
+
/**
|
|
56
|
+
* Plugin-owned benefit claim endpoint: POST + loopback, ONE account per call.
|
|
57
|
+
*
|
|
58
|
+
* A credit grant is a write on a real account, so it carries the same guards as
|
|
59
|
+
* check-in, plus `?accountId=` because the pool can hold accounts this plugin
|
|
60
|
+
* did not pick. There is deliberately no batch action here — see
|
|
61
|
+
* `claimBenefit` and the route handler.
|
|
62
|
+
*/
|
|
63
|
+
const WORKBUDDY_BENEFIT_CLAIM_PATH = "/plugins/dsh-connect-workbuddy/benefit/claim";
|
|
47
64
|
/** Plugin-owned live model refresh endpoint. */
|
|
48
65
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
49
66
|
/** Plugin-owned local account rescan endpoint. */
|
|
@@ -2243,6 +2260,65 @@ window.__ModuleLoader__.load({
|
|
|
2243
2260
|
t
|
|
2244
2261
|
]);
|
|
2245
2262
|
/**
|
|
2263
|
+
* Claim ONE benefit on ONE account, from its row (issue #79).
|
|
2264
|
+
*
|
|
2265
|
+
* Always a single account and a single campaign: there is no batch entry
|
|
2266
|
+
* point, because a claim spends a real entitlement that cannot be undone. The
|
|
2267
|
+
* row is the unit of consent, so the row is the only way to ask for it.
|
|
2268
|
+
*
|
|
2269
|
+
* The Host re-reads the campaign before AND after the write, so the message
|
|
2270
|
+
* here reports what the gateway says now rather than what was requested. The
|
|
2271
|
+
* refresh afterwards is what actually removes the 待领取 line: the card has no
|
|
2272
|
+
* optimistic update, so a claim that silently did nothing cannot leave a stale
|
|
2273
|
+
* line claiming credits were taken.
|
|
2274
|
+
*/
|
|
2275
|
+
const runRowClaim = (0, react.useCallback)(async (accountId, accountName, kind, title, creditNum) => {
|
|
2276
|
+
const name = accountName === "" ? t("row.accountUnnamed") : accountName;
|
|
2277
|
+
setRowBusyAccountId(accountId);
|
|
2278
|
+
setRowBusyAction("claim");
|
|
2279
|
+
setActionError(void 0);
|
|
2280
|
+
const url = withWorkBuddyRegion(WORKBUDDY_BENEFIT_CLAIM_PATH, region) + "&accountId=" + encodeURIComponent(accountId) + "&kind=" + encodeURIComponent(kind);
|
|
2281
|
+
try {
|
|
2282
|
+
const response = await fetch(url, {
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
headers: { accept: "application/json" },
|
|
2285
|
+
credentials: "same-origin"
|
|
2286
|
+
});
|
|
2287
|
+
const body = await response.json().catch(() => void 0);
|
|
2288
|
+
if (!response.ok) throw new Error(poolFailureText(t, {
|
|
2289
|
+
status: response.status,
|
|
2290
|
+
reason: body?.reason,
|
|
2291
|
+
error: body?.error
|
|
2292
|
+
}));
|
|
2293
|
+
if (!mounted.current) return;
|
|
2294
|
+
appendLog(t("row.poolLogBenefitClaimed", {
|
|
2295
|
+
accountName: name,
|
|
2296
|
+
title,
|
|
2297
|
+
count: formatNumber$1(creditNum)
|
|
2298
|
+
}), "ok");
|
|
2299
|
+
await onRefresh?.();
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
const message = error instanceof Error ? error.message : t("row.requestFailed");
|
|
2302
|
+
if (mounted.current) {
|
|
2303
|
+
setActionError(message);
|
|
2304
|
+
appendLog(t("row.poolLogBenefitFailed", {
|
|
2305
|
+
accountName: name,
|
|
2306
|
+
message
|
|
2307
|
+
}), "error");
|
|
2308
|
+
}
|
|
2309
|
+
} finally {
|
|
2310
|
+
if (mounted.current) {
|
|
2311
|
+
setRowBusyAccountId(void 0);
|
|
2312
|
+
setRowBusyAction(void 0);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}, [
|
|
2316
|
+
appendLog,
|
|
2317
|
+
onRefresh,
|
|
2318
|
+
region,
|
|
2319
|
+
t
|
|
2320
|
+
]);
|
|
2321
|
+
/**
|
|
2246
2322
|
* Remove ONE account from its row, via the Host.
|
|
2247
2323
|
*
|
|
2248
2324
|
* The Host dismisses the account (and deletes its vault entry when it has
|
|
@@ -2393,7 +2469,11 @@ window.__ModuleLoader__.load({
|
|
|
2393
2469
|
canEdit: canEditPool,
|
|
2394
2470
|
checkinSupported: pool.checkinSupported,
|
|
2395
2471
|
onToggle: (next) => toggleMember(account.accountId, next),
|
|
2396
|
-
busy: rowBusyAccountId
|
|
2472
|
+
busy: rowBusyAccountId === account.accountId,
|
|
2473
|
+
rowBusy: rowBusyAccountId !== void 0,
|
|
2474
|
+
rowBusyAction,
|
|
2475
|
+
/** The pill also needs to know WHICH row is busy, not just that one is. */
|
|
2476
|
+
claimBusy: rowBusyAccountId === account.accountId,
|
|
2397
2477
|
testDisabled: pool.targetModelSource === "none" || pool.targetModelSource === "stale",
|
|
2398
2478
|
targetHint: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
|
|
2399
2479
|
onTest: () => {
|
|
@@ -2402,6 +2482,9 @@ window.__ModuleLoader__.load({
|
|
|
2402
2482
|
onRemove: () => {
|
|
2403
2483
|
runRowRemove(account.accountId, account.accountName);
|
|
2404
2484
|
},
|
|
2485
|
+
onClaim: (kind, title, creditNum) => {
|
|
2486
|
+
runRowClaim(account.accountId, account.accountName, kind, title, creditNum);
|
|
2487
|
+
},
|
|
2405
2488
|
orderIndex: isMember && orderIsManual ? left + 1 : void 0,
|
|
2406
2489
|
orderIsManual,
|
|
2407
2490
|
moveEnabled: pool.enabled === true && isMember && canEditPool,
|
|
@@ -2481,7 +2564,7 @@ window.__ModuleLoader__.load({
|
|
|
2481
2564
|
}
|
|
2482
2565
|
/** One account's row. */
|
|
2483
2566
|
function renderAccountRow(input) {
|
|
2484
|
-
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove, orderIndex, orderIsManual, moveEnabled, onMoveUp, onMoveDown, moveUpDisabled, moveDownDisabled } = input;
|
|
2567
|
+
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, rowBusy, testDisabled, targetHint, onTest, onRemove, onClaim, rowBusyAction, claimBusy, orderIndex, orderIsManual, moveEnabled, onMoveUp, onMoveDown, moveUpDisabled, moveDownDisabled } = input;
|
|
2485
2568
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
2486
2569
|
const excluded = exclusionText(t, account.excludedBy);
|
|
2487
2570
|
const probe = account.probe;
|
|
@@ -2531,7 +2614,30 @@ window.__ModuleLoader__.load({
|
|
|
2531
2614
|
"aria-label": t("row.poolMoveDownAria", { accountName: name }),
|
|
2532
2615
|
disabled: moveDownDisabled,
|
|
2533
2616
|
onClick: onMoveDown
|
|
2534
|
-
}, "↓")) : null) : null, 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: formatShortClock(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: formatShortClock(account.nearestExpiryMs) }))), (0, react.createElement)("span", {
|
|
2617
|
+
}, "↓")) : null) : null, 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: formatShortClock(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: formatShortClock(account.nearestExpiryMs) })), account.benefits === void 0 || account.benefits.length === 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-benefits" }, account.benefits.map((benefit, index) => (0, react.createElement)("span", {
|
|
2618
|
+
key: benefit.kind,
|
|
2619
|
+
className: benefit.urgent ? "dsm-workbuddy-pool-benefit dsm-workbuddy-pool-benefit-urgent" : "dsm-workbuddy-pool-benefit"
|
|
2620
|
+
}, index === 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-benefit-tag" }, benefit.urgent ? benefit.daysLeft === void 0 || benefit.daysLeft === 0 ? t("row.poolBenefitDueToday", {
|
|
2621
|
+
title: benefit.title,
|
|
2622
|
+
count: formatNumber$1(benefit.creditNum)
|
|
2623
|
+
}) : t("row.poolBenefitUrgent", {
|
|
2624
|
+
title: benefit.title,
|
|
2625
|
+
count: formatNumber$1(benefit.creditNum),
|
|
2626
|
+
days: formatNumber$1(benefit.daysLeft)
|
|
2627
|
+
}) : t("row.poolBenefitClaimable", {
|
|
2628
|
+
title: benefit.title,
|
|
2629
|
+
count: formatNumber$1(benefit.creditNum)
|
|
2630
|
+
})), (0, react.createElement)("button", {
|
|
2631
|
+
type: "button",
|
|
2632
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn dsm-workbuddy-pool-benefit-btn",
|
|
2633
|
+
disabled: rowBusy,
|
|
2634
|
+
title: t("row.poolBenefitHint", { url: WORKBUDDY_PLAN_URL }),
|
|
2635
|
+
"aria-label": t("row.poolBenefitClaimAria", {
|
|
2636
|
+
title: benefit.title,
|
|
2637
|
+
count: formatNumber$1(benefit.creditNum)
|
|
2638
|
+
}),
|
|
2639
|
+
onClick: () => onClaim(benefit.kind, benefit.title, benefit.creditNum)
|
|
2640
|
+
}, (0, react.createElement)("span", null, claimBusy && rowBusyAction === "claim" ? t("row.poolBenefitClaiming") : t("row.poolBenefitClaim"))))))), (0, react.createElement)("span", {
|
|
2535
2641
|
className: "dsm-workbuddy-pool-probe",
|
|
2536
2642
|
...probe?.message === void 0 ? {} : { title: probe.message }
|
|
2537
2643
|
}, (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), probe === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-model" }, probe.modelId === void 0 ? t("row.poolProbeModelUnknown") : t("row.poolProbeModel", { model: probe.modelId })))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
@@ -2540,13 +2646,13 @@ window.__ModuleLoader__.load({
|
|
|
2540
2646
|
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("button", {
|
|
2541
2647
|
type: "button",
|
|
2542
2648
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
2543
|
-
disabled:
|
|
2649
|
+
disabled: rowBusy || testDisabled,
|
|
2544
2650
|
title: testDisabled ? targetHint : void 0,
|
|
2545
2651
|
onClick: onTest
|
|
2546
2652
|
}, t("row.poolRowTest")), (0, react.createElement)("button", {
|
|
2547
2653
|
type: "button",
|
|
2548
2654
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
2549
|
-
disabled:
|
|
2655
|
+
disabled: rowBusy,
|
|
2550
2656
|
title: t("row.poolRowRemoveHint"),
|
|
2551
2657
|
"aria-label": t("row.poolRowRemoveAria", { accountName: name }),
|
|
2552
2658
|
onClick: onRemove
|
|
@@ -3566,11 +3672,11 @@ window.__ModuleLoader__.load({
|
|
|
3566
3672
|
* Client styles for the WorkBuddy plugin card.
|
|
3567
3673
|
*
|
|
3568
3674
|
* 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
|
|
3569
|
-
* — 整套
|
|
3675
|
+
* — 整套 dsm-* 卡片样式系统(卡片外壳、按钮原语、--dsw-alias-*
|
|
3570
3676
|
* 主题变量与十六进制回退值)逐字沿用自该项目,其又复制自
|
|
3571
3677
|
* dingminhua/dsh-subagent-default-model(MIT)的 SETTINGS_CSS。
|
|
3572
3678
|
* 沿用目的是让两个插件共享同一套外部表现语言。
|
|
3573
|
-
* 改动:类名由
|
|
3679
|
+
* 改动:类名由 dsm-trae-* 改为 dsm-workbuddy-*;
|
|
3574
3680
|
* 移除 trae 特有的 1M 变体样式,新增按套餐聚合的积分行样式;
|
|
3575
3681
|
* 双 provider 化后新增国内版/国际版 tab 栏样式。
|
|
3576
3682
|
*
|
|
@@ -3960,6 +4066,114 @@ window.__ModuleLoader__.load({
|
|
|
3960
4066
|
.dsm-workbuddy-pool-credits b{color:var(--dsw-alias-label-primary,#e6e6e6);font-weight:600}
|
|
3961
4067
|
.dsm-workbuddy-pool-soon{font-size:11px;line-height:16px;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
3962
4068
|
.dsm-workbuddy-pool-soon-plain{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
4069
|
+
/* Claimable benefits (issue #79). A quiet line by default — it is an opportunity, not a fault — and loud only when the window is about to close. */
|
|
4070
|
+
.dsm-workbuddy-pool-benefits{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#999);min-width:0}
|
|
4071
|
+
/* One entry = text + pill, and the text wraps rather than being cut.
|
|
4072
|
+
The tag used to be nowrap + ellipsis, which truncated the label to
|
|
4073
|
+
「模型服务异常补偿…」 — dropping the amount, i.e. the only part the user is
|
|
4074
|
+
deciding on. Measured in headless Chrome at the card's real width, the entry
|
|
4075
|
+
line has room for two lines, so the tag clamps to two and shows the whole
|
|
4076
|
+
label; the pill wraps to the line below and never shrinks.
|
|
4077
|
+
line-clamp, not plain wrapping: an unexpectedly long upstream title must not
|
|
4078
|
+
push the row taller than the pill can travel. */
|
|
4079
|
+
.dsm-workbuddy-pool-benefit-tag{min-width:0;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
4080
|
+
/* min-width:0 IS the whole fix (issue #79).
|
|
4081
|
+
The pill made this entry's min-content 229px (full line + button) inside a
|
|
4082
|
+
148px column, so it painted over the probe column beside it.
|
|
4083
|
+
A flex ITEM defaults to min-width:auto and refuses to shrink below its
|
|
4084
|
+
content, which is what pinned it at 229; min-width:0 lets the tag ellipsize
|
|
4085
|
+
and the pill keep its size.
|
|
4086
|
+
|
|
4087
|
+
Measured in headless Chrome against the real rendered row, as an A/B:
|
|
4088
|
+
min-width:0 -> 0 overflowing children, entry 148px
|
|
4089
|
+
without it -> 3 overflowing children, entry 229px
|
|
4090
|
+
Changing the row's grid tracks to minmax(0,...) was tried too and made NO
|
|
4091
|
+
difference (the entry overflows its CELL, so the CELL was never the
|
|
4092
|
+
constraint) — so the tracks are left as they were. */
|
|
4093
|
+
.dsm-workbuddy-pool-benefit{display:flex;flex-wrap:wrap;align-items:center;gap:2px 5px;min-width:0;max-width:100%}
|
|
4094
|
+
.dsm-workbuddy-pool-benefit-urgent{color:var(--dsw-alias-state-warn-primary,#f59e0b);font-weight:600}
|
|
4095
|
+
/* The claim pill: a solid amber call to action.
|
|
4096
|
+
Orange, not the brand blue, because this is the ONE control in the pool that
|
|
4097
|
+
hands the user something (500 credits) instead of spending their attention on
|
|
4098
|
+
a read-only figure. It should read as an opportunity, and amber is the hue
|
|
4099
|
+
that says "take this" without shouting.
|
|
4100
|
+
|
|
4101
|
+
Solid fill rather than a tinted outline: at 19px tall and 46px wide, a tinted
|
|
4102
|
+
outline sat next to the 1,294 credit figure above it and read as DISABLED.
|
|
4103
|
+
The solid fill also survives the row's grey text without competing for the
|
|
4104
|
+
eye. The card's own 已签到 green is the only other saturated element here.
|
|
4105
|
+
|
|
4106
|
+
Load-bearing details, each measured in headless Chrome:
|
|
4107
|
+
- white-space:nowrap: without it the flex row squeezed 领取 into 领/取 on
|
|
4108
|
+
two lines and the pill grew into a 32px-tall blob;
|
|
4109
|
+
- a fixed 19px height, so it matches the 11px text beside it instead of
|
|
4110
|
+
inheriting .dsm-btn's padding;
|
|
4111
|
+
- flex:none, so the shrinking column eats the TEXT and never the pill. */
|
|
4112
|
+
/* Order matters: this rule must come AFTER .dsm-btn-outline (line ~36) so its
|
|
4113
|
+
amber background wins. Two-class specificity turned out to be the wrong lever
|
|
4114
|
+
and was REMOVED after measuring: pairing the classes also beat the
|
|
4115
|
+
dsm-workbuddy-pool-small-btn padding, and without dsm-btn-outline on the
|
|
4116
|
+
element the button fell back to dsm-btn's default background — rendered white
|
|
4117
|
+
on a dark row. Source order is what was already working; keep it. */
|
|
4118
|
+
.dsm-workbuddy-pool-benefit-btn{
|
|
4119
|
+
/* Flex centring plus a 1px nudge is what actually centres 领取.
|
|
4120
|
+
Measured on the rendered pill in headless Chrome (OCR-free pixel scan of
|
|
4121
|
+
the glyph mask inside the 46x19 box):
|
|
4122
|
+
display:inline-flex; align-items:center -> above 3.5px, below 4.5px,
|
|
4123
|
+
glyph sat 1.0px low
|
|
4124
|
+
plus translateY(-0.5px) below -> above 4.0px, below 4.0px
|
|
4125
|
+
Flex centring centres the LINE BOX, and a CJK glyph's ink sits below the
|
|
4126
|
+
centre of its em box, so centring the box leaves the ink low. The nudge
|
|
4127
|
+
compensates for the ink, not for the box. It is half a pixel because the
|
|
4128
|
+
label renders at 2x on a retina display and 1px would overshoot there. */
|
|
4129
|
+
display:inline-flex;
|
|
4130
|
+
align-items:center;
|
|
4131
|
+
justify-content:center;
|
|
4132
|
+
flex:none;
|
|
4133
|
+
padding:0 9px;
|
|
4134
|
+
height:19px;
|
|
4135
|
+
font-size:11px;
|
|
4136
|
+
line-height:1;
|
|
4137
|
+
font-weight:600;
|
|
4138
|
+
border-radius:999px;
|
|
4139
|
+
white-space:nowrap;
|
|
4140
|
+
letter-spacing:.01em;
|
|
4141
|
+
border-color:var(--dsw-alias-state-warn-primary,#f59e0b);
|
|
4142
|
+
color:#2a1d05;
|
|
4143
|
+
background:var(--dsw-alias-state-warn-primary,#f59e0b);
|
|
4144
|
+
/* The only shadow in the pool table: it lifts the pill off the row so the
|
|
4145
|
+
control reads as clickable rather than as part of the label. */
|
|
4146
|
+
box-shadow:0 1px 2px rgba(0,0,0,.28);
|
|
4147
|
+
transition:filter .12s ease;
|
|
4148
|
+
}
|
|
4149
|
+
/* Optical centring for the label (issue #79).
|
|
4150
|
+
Flex centring centres the LINE BOX, but a CJK glyph's ink sits below the
|
|
4151
|
+
centre of that box, so 领取 measured 3.5px above / 4.5px below — visibly low.
|
|
4152
|
+
padding-top pushes the ink down by shifting the line box within the centred
|
|
4153
|
+
flex item. Measured on the rendered pill in headless Chrome, as a sweep:
|
|
4154
|
+
(none) -> above 3.5 / below 4.5 (+1.0 low)
|
|
4155
|
+
padding-top:1px -> above 4.0 / below 4.0 (centred)
|
|
4156
|
+
padding-top:2px -> above 4.5 / below 3.5 (-1.0 high)
|
|
4157
|
+
transform and padding-bottom were both tried first and moved the ink the
|
|
4158
|
+
WRONG way, because both grow the flex item, which re-centres the line box
|
|
4159
|
+
downward. Padding-top is the one that shifts the ink up. */
|
|
4160
|
+
.dsm-workbuddy-pool-benefit-btn > span{display:block;padding-top:1px}
|
|
4161
|
+
.dsm-workbuddy-pool-benefit-btn:hover:not(:disabled){
|
|
4162
|
+
/* Brighten, do not invert: an inverted pill loses its "take this" reading,
|
|
4163
|
+
and the dark-on-amber swap is the part that looked cheap in the draft. */
|
|
4164
|
+
filter:brightness(1.08);
|
|
4165
|
+
border-color:var(--dsw-alias-state-warn-primary,#f59e0b);
|
|
4166
|
+
color:#2a1d05;
|
|
4167
|
+
background:var(--dsw-alias-state-warn-primary,#f59e0b);
|
|
4168
|
+
}
|
|
4169
|
+
.dsm-workbuddy-pool-benefit-btn:active:not(:disabled){filter:brightness(.94)}
|
|
4170
|
+
/* A row action is in flight: this row, or another one. The pill goes flat so it
|
|
4171
|
+
cannot be clicked twice while the first claim is still landing. */
|
|
4172
|
+
.dsm-workbuddy-pool-benefit-btn:disabled{
|
|
4173
|
+
opacity:.45;
|
|
4174
|
+
cursor:default;
|
|
4175
|
+
box-shadow:none;
|
|
4176
|
+
}
|
|
3963
4177
|
.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}
|
|
3964
4178
|
.dsm-workbuddy-pool-probe{display:flex;flex-direction:column;gap:4px}
|
|
3965
4179
|
.dsm-workbuddy-pool-probe-main{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
@@ -3977,6 +4191,12 @@ window.__ModuleLoader__.load({
|
|
|
3977
4191
|
.dsm-workbuddy-pool-members-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
3978
4192
|
.dsm-workbuddy-pool-members-title{margin:0 0 3px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3979
4193
|
.dsm-workbuddy-pool-members-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
4194
|
+
/* The 测试/删除 and 全选/全不选 buttons. Shared with the benefit pill, which
|
|
4195
|
+
* OVERRIDES both properties at higher specificity (see its rule above): a pill is
|
|
4196
|
+
* a capsule whose height is fixed at 19px, so inheriting this 3px/10px rhythm
|
|
4197
|
+
* pushed its own padding/line-height out and changed how the label sat.
|
|
4198
|
+
* Kept as-is rather than narrowed to the parents that use it, so the row-action
|
|
4199
|
+
* and select-all sizing stays in one place. */
|
|
3980
4200
|
.dsm-workbuddy-pool-small-btn{padding:3px 10px;font-size:12px}
|
|
3981
4201
|
.dsm-workbuddy-pool-check{display:inline-flex;align-items:center;flex:none;cursor:pointer}
|
|
3982
4202
|
.dsm-workbuddy-pool-check input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
@@ -4137,7 +4357,7 @@ window.__ModuleLoader__.load({
|
|
|
4137
4357
|
* Composer credit readout styles, injected by the readout component itself.
|
|
4138
4358
|
*
|
|
4139
4359
|
* 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
|
|
4140
|
-
* — 该项目的
|
|
4360
|
+
* — 该项目的 COMPOSER_POINTS_CSS,类名改为 dsm-workbuddy-*,
|
|
4141
4361
|
* 其余(尺寸、节奏、主题变量与回退值、表格对齐)逐条沿用。
|
|
4142
4362
|
*
|
|
4143
4363
|
* Separate from the card bundle because the readout lives in the session's
|
|
@@ -6898,7 +7118,7 @@ window.__ModuleLoader__.load({
|
|
|
6898
7118
|
*/
|
|
6899
7119
|
const en = {
|
|
6900
7120
|
"row.title": "WorkBuddy credits & models (dsh-connect-workbuddy)",
|
|
6901
|
-
"row.desc": "Use WorkBuddy models in DSH and
|
|
7121
|
+
"row.desc": "Use WorkBuddy models in DSH: each refresh reads every model's credit multiplier from the upstream catalog, image input is toggled per model, several accounts can be switched or failed over automatically, and daily check-in plus your remaining credits are one click away. The domestic and international sides are two independent providers, each with its own account — use both at the same time.",
|
|
6902
7122
|
"row.expand": "Expand",
|
|
6903
7123
|
"row.collapse": "Collapse",
|
|
6904
7124
|
"row.tabCn": "Domestic",
|
|
@@ -7068,6 +7288,15 @@ window.__ModuleLoader__.load({
|
|
|
7068
7288
|
"row.poolCheckedIn": "Checked in",
|
|
7069
7289
|
"row.poolNotCheckedIn": "Not checked in",
|
|
7070
7290
|
"row.poolCheckinUnknown": "—",
|
|
7291
|
+
"row.poolBenefitClaimable": "{title}: {count} credits",
|
|
7292
|
+
"row.poolBenefitUrgent": "{title}: {count} credits · {days}d left",
|
|
7293
|
+
"row.poolBenefitDueToday": "{title}: {count} credits · last day",
|
|
7294
|
+
"row.poolBenefitClaim": "Claim",
|
|
7295
|
+
"row.poolBenefitClaiming": "Claiming…",
|
|
7296
|
+
"row.poolBenefitClaimAria": "Claim {title} ({count} credits) for this account",
|
|
7297
|
+
"row.poolLogBenefitClaimed": "{accountName} claimed {title}: {count} credits",
|
|
7298
|
+
"row.poolLogBenefitFailed": "{accountName} claim failed: {message}",
|
|
7299
|
+
"row.poolBenefitHint": "Claim it on the plans page: {url}",
|
|
7071
7300
|
"row.poolCreditSoon": "{count} expiring within 3 days",
|
|
7072
7301
|
"row.poolCreditNearest": "Nearest package expires {at}",
|
|
7073
7302
|
"row.poolExcludedRateLimited": "Rate limited",
|
|
@@ -7211,7 +7440,7 @@ window.__ModuleLoader__.load({
|
|
|
7211
7440
|
};
|
|
7212
7441
|
const zh = {
|
|
7213
7442
|
"row.title": "接入使用 WorkBuddy 积分与模型(dsh-connect-workbuddy)",
|
|
7214
|
-
"row.desc": "在 DSH 中使用 WorkBuddy
|
|
7443
|
+
"row.desc": "在 DSH 中使用 WorkBuddy 模型:每次刷新都按上游目录读取每个模型的积分倍率,图片输入可逐模型开关,多个账号可切换或由账号池自动换号,每日签到与剩余积分一键可查。国内版与国际版是两个独立供应商,各有自己的账号,可同时使用。",
|
|
7215
7444
|
"row.expand": "展开",
|
|
7216
7445
|
"row.collapse": "收起",
|
|
7217
7446
|
"row.tabCn": "国内版",
|
|
@@ -7381,6 +7610,15 @@ window.__ModuleLoader__.load({
|
|
|
7381
7610
|
"row.poolCheckedIn": "已签到",
|
|
7382
7611
|
"row.poolNotCheckedIn": "未签到",
|
|
7383
7612
|
"row.poolCheckinUnknown": "—",
|
|
7613
|
+
"row.poolBenefitClaimable": "{title}:{count} 积分",
|
|
7614
|
+
"row.poolBenefitUrgent": "{title}:{count} 积分 · 剩 {days} 天",
|
|
7615
|
+
"row.poolBenefitDueToday": "{title}:{count} 积分 · 今天截止",
|
|
7616
|
+
"row.poolBenefitClaim": "领取",
|
|
7617
|
+
"row.poolBenefitClaiming": "领取中…",
|
|
7618
|
+
"row.poolBenefitClaimAria": "为该账号领取{title}({count} 积分)",
|
|
7619
|
+
"row.poolLogBenefitClaimed": "{accountName} 已领取{title}:{count} 积分",
|
|
7620
|
+
"row.poolLogBenefitFailed": "{accountName} 领取失败:{message}",
|
|
7621
|
+
"row.poolBenefitHint": "去套餐与积分页领取:{url}",
|
|
7384
7622
|
"row.poolCreditSoon": "{count} 积分 3 天内过期",
|
|
7385
7623
|
"row.poolCreditNearest": "最近积分包 {at} 过期",
|
|
7386
7624
|
"row.poolExcludedRateLimited": "被限流",
|