dsh-connect-workbuddy 4.9.4 → 4.10.0
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 +60 -0
- package/README.en.md +4 -3
- package/README.md +3 -2
- package/lib/bin.js +1 -1
- package/lib/client.js +248 -10
- package/lib/{host-heartbeat-BpArQi1Q.js → host-heartbeat-BQHO4oiB.js} +98 -1
- package/lib/index.d.ts +159 -1
- package/lib/index.js +181 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.10.0 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **minor**:4.9.5 / 4.9.6 两个 patch 从未单独发布到 npm(npm 上仍是 4.9.4),本次一次性带上它们,并新增福利活动的完整链路。**发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。**
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **issue #79:福利活动自动发现 + 单账号领取(4.9.4 → 4.10.0 的主线)**
|
|
10
|
+
- **自动发现**:卡片逐账号标出上游活动(模型服务异常补偿 / 新人首月赠送)里**还没领的那一份**。数据来自上游两个只读状态接口 `/billing/meter/compensation-status` 与 `/billing/meter/check-gift-claimed`(注意**没有 `/v2` 前缀**,探 `/v2/...` 会 404)。
|
|
11
|
+
- **判定规则完全由上游状态决定**:`active && !claimed` 才显示;`claimed` 整行消失;活动过期(上游把 `active` 翻成 `false`)自动消失。**不拿本地时钟去比 `end_time`**,本机时间偏了也不会与服务端打架;`end_time` 只用于 3 天内临期高亮。
|
|
12
|
+
- `active` 单独看**不行**:已领取的账号 `active` 仍为 `true`,只看它会让领过的活动永远挂着待领取。
|
|
13
|
+
- 读取复用签到管线:每账号一次、30 分钟缓存、账号池关闭时不发请求;任一接口失败按「未读」处理,**不会把一次失败缓存成「全都没福利」**。
|
|
14
|
+
- **单账号「领取」按钮**:每个待领取项后一个实心琥珀色胶囊按钮,**一次只领一个账号的一个活动**。没有「全部领取」——一次点击花掉的是一份不可撤销的真实权益。
|
|
15
|
+
- 新路由 `POST /plugins/dsh-connect-workbuddy/benefit/claim?accountId=&kind=`,沿用写路由的 POST + loopback 守卫,并**强制要求 `accountId`**,请求本身无法批量领取。
|
|
16
|
+
- 领取前**重新读取**状态再判定;已领取/已过期一律 409 且不调用上游。领取后用上游**新读数**作答并刷新卡片,不做乐观更新。
|
|
17
|
+
- **领取成功后清掉该账号的福利缓存**:这是本次修掉的最关键 bug。缓存 TTL 30 分钟且此前从不因领取失效,于是「积分涨了、按钮还在」,用户会以为领取失败而重复点击,第二次被 409 拒绝。
|
|
18
|
+
- **每行按钮只联动自己那一行**:此前行的 busy 标志传的是「有任一行动作」,点一行会让五行同时变成「领取中…」,看起来像一次领了整池。
|
|
19
|
+
- 实测:真实账号 `19dea97d` 领取后 `claimed` 由 false 变 true,500 积分到账;同一批次 6 个账号 × 500 积分待领取,截止 10/22。
|
|
20
|
+
|
|
21
|
+
### Docs
|
|
22
|
+
|
|
23
|
+
- **描述与 README 同步到插件实际能力**:`package.json` 的 `description`、市场提交草稿的 `description.zh` / `description.en`、两个 README 的标题下 tagline 与开头段,统一改写为「国内版 + 国际版接入 / 每次刷新按上游目录自动读取每个模型的积分倍率 / 逐模型图片输入开关 / 多账号切换与账号池自动换号 / 每日签到与只读积分概览」。旧描述只写了「模型接入 + 只读积分概览 + 模型管理」,把 3.x~4.9 长出来的能力全落在了外面。
|
|
24
|
+
- **更正一处不成立的表述**:描述草稿里曾有「费率随错峰时段变化」的说法。全仓(`src/`、`tests/`、`docs/`、两个 README)与 WorkBuddy 桌面端 asar 里都没有任何峰谷/时段折扣逻辑,`git log -S` 也从无此词;插件真正做的是**从上游目录解析每个模型自带的积分倍率**(`x0.79` 这种,解析不出就不显示,绝不猜),且该倍率确实会被上游改动(`deepseek-v4.1-flash` 已从 0 变成 0.11)。会随「时段」变的那个是**免费模型的 token 定额**,不是价格,因此文案按实测口径写「随上游目录刷新更新」,不承诺峰谷计价。
|
|
25
|
+
- **README 补上「福利活动」条目(中英双语)**:4.9.4 的待领取标记与 4.9.6 的单账号「领取」按钮此前在两个 README 里**一次都没提过**。新增条目写清:数据只来自上游两个状态接口、标记阶段只读、三条显示规则由上游状态决定(不用本地时钟比 `end_time`)、一次只领一个账号的一个活动、写路由强制 `accountId` 所以无法批量、领取前重读状态、领取后用上游新读数刷新并清掉该账号的福利缓存。
|
|
26
|
+
- **卡片内说明文案(`src/client/locales.ts` 的 `row.desc`,中英同步)**同步改写,与上面的描述口径一致;两侧键仍严格 1:1。
|
|
27
|
+
- **修 EN README 的编号重复**:自动换号那一节有两个 `7.`,第二条(每次重试写一行 `warn`)改为 `8.`。
|
|
28
|
+
|
|
29
|
+
### Fixes
|
|
30
|
+
|
|
31
|
+
- **裁剪福利说明不再撑破积分列**:福利文案与按钮曾被挤成竖排「领取」二字并压住右侧目标模型列;胶囊改为 `white-space:nowrap` + 固定 19px 高 + `flex:none`,让列宽只能压缩文字、压不了按钮。
|
|
32
|
+
- **「领取」二字的视觉居中**:无头 Chrome 像素扫描测得 CJK 字形墨迹比行盒中心偏低约 1px,用 `padding-top:1px` 补偿(`transform` 与 `padding-bottom` 两个方向都试过,都会把墨迹推得更低,已弃用)。
|
|
33
|
+
- **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)的原样恢复标题。
|
|
34
|
+
|
|
35
|
+
## 4.9.6 (2026-10-19 · 未单独发布,并入 4.10.0)
|
|
36
|
+
|
|
37
|
+
> 版本号是 **patch**:新增一个写路由与行内按钮,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
- **issue #79:账号池卡片提供单账号「领取」按钮**
|
|
42
|
+
- 每个待领取的福利后面带一个按钮,**一次只领一个账号的一个活动**;没有任何「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益。
|
|
43
|
+
- 新路由 `POST /plugins/dsh-connect-workbuddy/benefit/claim?accountId=&kind=`,沿用写路由的 POST + loopback 守卫,并**强制要求 accountId**——请求本身无法批量领取。
|
|
44
|
+
- 领取前**重新读取**状态再判定(窗口内 && 未领取),所以过期的卡片不会触发无谓写入;已领取/已过期一律 409 且不调用上游。
|
|
45
|
+
- 领取后用上游的**新读数**作答并刷新卡片,不做乐观更新:即使上游静默失败,也不会留下「已领取」的假象。
|
|
46
|
+
- 实测:真实账号 `19dea97d` 领取后 `claimed` 由 false 变 true,500 积分到账。
|
|
47
|
+
|
|
48
|
+
## 4.9.5 (2026-10-19 · 未单独发布,并入 4.10.0)
|
|
49
|
+
|
|
50
|
+
> 版本号是 **patch**:只新增只读查询与界面标记,发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。
|
|
51
|
+
|
|
52
|
+
### Features
|
|
53
|
+
|
|
54
|
+
- **issue #79:账号池卡片标出「有福利还没领」**
|
|
55
|
+
- 数据来自上游自己的两个活动状态接口(`/billing/meter/compensation-status`、`/billing/meter/check-gift-claimed`),**只读**;`claim-compensation` / `claim-gift` 两个写接口从不调用。
|
|
56
|
+
- 三条显示规则都由上游状态直接决定,前端不维护状态:
|
|
57
|
+
- `active && !claimed` → 显示待领取。
|
|
58
|
+
- `claimed` → 整行不出现(领过的不再骚扰)。
|
|
59
|
+
- 活动过期 → `active` 翻 `false`,整行自动消失,**不用本地时钟比 `end_time`**(时钟偏了不会与服务端打架);`end_time` 只用于 3 天内临期高亮。
|
|
60
|
+
- `active` 不能单独判断:已领取的账号 `active` 仍为 `true`,只看它会让领过的账号永远挂着待领取。
|
|
61
|
+
- 读取复用 `checkedInToday` 的管线:每账号一次、TTL 30 分钟、池关闭不发请求;任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。
|
|
62
|
+
|
|
3
63
|
## 4.9.4 (2026-10-19)
|
|
4
64
|
|
|
5
65
|
> 版本号是 **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/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-BQHO4oiB.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": "被限流",
|
|
@@ -917,6 +917,19 @@ function clearAtRestKeyCache() {
|
|
|
917
917
|
*
|
|
918
918
|
* @module dsh-connect-workbuddy/upstream
|
|
919
919
|
*/
|
|
920
|
+
/**
|
|
921
|
+
* The write half of each campaign: the endpoint that actually grants the
|
|
922
|
+
* credits.
|
|
923
|
+
*
|
|
924
|
+
* Kept beside the status paths because they must move together: the pair is
|
|
925
|
+
* what the 「套餐与积分」 page itself uses (`xp`/`vp` in its bundle), and a
|
|
926
|
+
* campaign whose status is read from one path can only be claimed through its
|
|
927
|
+
* matching claim path. Note the same absence of a `/v2` prefix.
|
|
928
|
+
*/
|
|
929
|
+
const BENEFIT_CLAIM_PATHS = {
|
|
930
|
+
compensation: "/billing/meter/claim-compensation",
|
|
931
|
+
gift: "/billing/meter/claim-gift"
|
|
932
|
+
};
|
|
920
933
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
921
934
|
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
922
935
|
const GLOBAL_BASE = "https://www.workbuddy.ai";
|
|
@@ -1921,6 +1934,90 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1921
1934
|
...typeof data["claim_button_text"] === "string" && data["claim_button_text"] !== "" ? { claimButtonText: data["claim_button_text"] } : {}
|
|
1922
1935
|
};
|
|
1923
1936
|
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Read every promotional benefit this account can still see, WITHOUT claiming
|
|
1939
|
+
* anything.
|
|
1940
|
+
*
|
|
1941
|
+
* The campaign list is closed and small on purpose: the gateway exposes one
|
|
1942
|
+
* status endpoint per campaign, so "discover benefits" is a fixed fan-out, not
|
|
1943
|
+
* a search. A new campaign type means a new `/billing/meter/*-status` endpoint
|
|
1944
|
+
* added here — the plugin never guesses at undocumented ones.
|
|
1945
|
+
*
|
|
1946
|
+
* Every endpoint is read in parallel because they are independent, and a
|
|
1947
|
+
* failure is per-endpoint: one campaign answering 5xx must not blank out the
|
|
1948
|
+
* other. The caller gets only what was actually read.
|
|
1949
|
+
*
|
|
1950
|
+
* Note the paths carry NO `/v2` prefix — these are not the `/v2/billing/meter`
|
|
1951
|
+
* family the credit and check-in calls use. Probing `/v2/...` answers 404, which
|
|
1952
|
+
* is how this distinction was found.
|
|
1953
|
+
*/
|
|
1954
|
+
async fetchBenefits(credential) {
|
|
1955
|
+
const read = async (kind, path) => {
|
|
1956
|
+
try {
|
|
1957
|
+
return await this.readBenefitStatus(kind, credential, path);
|
|
1958
|
+
} catch {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
const [compensation, gift] = await Promise.all([read("compensation", "/billing/meter/compensation-status"), read("gift", "/billing/meter/check-gift-claimed")]);
|
|
1963
|
+
return [compensation, gift].filter((benefit) => benefit !== void 0);
|
|
1964
|
+
}
|
|
1965
|
+
/** One benefit status endpoint. Throws on anything that is not a clean `code: 0`. */
|
|
1966
|
+
async readBenefitStatus(kind, credential, path) {
|
|
1967
|
+
const response = await fetch(`${billingBase(credential)}${path}`, {
|
|
1968
|
+
method: "POST",
|
|
1969
|
+
headers: billingHeaders(credential),
|
|
1970
|
+
body: "{}",
|
|
1971
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
1972
|
+
});
|
|
1973
|
+
const envelope = await readEnvelope(response);
|
|
1974
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
1975
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
1976
|
+
const numberField = (key) => typeof data[key] === "number" ? data[key] : 0;
|
|
1977
|
+
/** The gateway states times as `YYYY-MM-DD HH:MM:SS` in local time. */
|
|
1978
|
+
const timeField = (key) => {
|
|
1979
|
+
const raw = data[key];
|
|
1980
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
1981
|
+
const parsed = Date.parse(raw.replace(" ", "T"));
|
|
1982
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1983
|
+
};
|
|
1984
|
+
const endsAtMs = timeField("end_time");
|
|
1985
|
+
const startsAtMs = timeField("start_time");
|
|
1986
|
+
const claimedAtMs = timeField("claimed_at");
|
|
1987
|
+
return {
|
|
1988
|
+
kind,
|
|
1989
|
+
active: data["active"] === true,
|
|
1990
|
+
claimed: data["claimed"] === true,
|
|
1991
|
+
creditNum: numberField("credit_num"),
|
|
1992
|
+
...endsAtMs === void 0 ? {} : { endsAtMs },
|
|
1993
|
+
...startsAtMs === void 0 ? {} : { startsAtMs },
|
|
1994
|
+
...claimedAtMs === void 0 ? {} : { claimedAtMs }
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Claim one benefit campaign on ONE account (issue #79).
|
|
1999
|
+
*
|
|
2000
|
+
* This is a real credit grant on a real account, so it takes no account
|
|
2001
|
+
* argument other than the credential it is handed: the caller decides which
|
|
2002
|
+
* single account, and there is deliberately no batch form of this method. The
|
|
2003
|
+
* browser route is the only caller, and it guards the mutation with the same
|
|
2004
|
+
* POST + loopback checks the check-in route carries.
|
|
2005
|
+
*
|
|
2006
|
+
* The response is not parsed: the documented envelope of these two endpoints
|
|
2007
|
+
* is an empty success, and the caller re-reads the status afterwards. That
|
|
2008
|
+
* re-read, not this answer, is what the user is shown — which is why a claim
|
|
2009
|
+
* that silently did nothing still cannot leave the card saying 待领取.
|
|
2010
|
+
*/
|
|
2011
|
+
async claimBenefit(credential, kind) {
|
|
2012
|
+
const response = await fetch(`${billingBase(credential)}${BENEFIT_CLAIM_PATHS[kind]}`, {
|
|
2013
|
+
method: "POST",
|
|
2014
|
+
headers: billingHeaders(credential),
|
|
2015
|
+
body: "{}",
|
|
2016
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
2017
|
+
});
|
|
2018
|
+
const envelope = await readEnvelope(response);
|
|
2019
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
2020
|
+
}
|
|
1924
2021
|
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
1925
2022
|
async claimDailyCheckin(credential) {
|
|
1926
2023
|
const response = await fetch(`${billingBase(credential)}/v2/billing/meter/daily-checkin`, {
|
|
@@ -3977,7 +4074,7 @@ function generationOf(version) {
|
|
|
3977
4074
|
//#endregion
|
|
3978
4075
|
//#region src/version.ts
|
|
3979
4076
|
/** The npm package version this build was produced from. */
|
|
3980
|
-
const WORKBUDDY_CONNECT_VERSION = "4.
|
|
4077
|
+
const WORKBUDDY_CONNECT_VERSION = "4.10.0";
|
|
3981
4078
|
//#endregion
|
|
3982
4079
|
//#region src/host-heartbeat.ts
|
|
3983
4080
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -706,6 +706,47 @@ interface WorkBuddyCheckinClaim {
|
|
|
706
706
|
streakDays: number;
|
|
707
707
|
isStreakDay: boolean;
|
|
708
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* One promotional benefit the account may or may not still be entitled to.
|
|
711
|
+
*
|
|
712
|
+
* Read from the billing gateway's OWN status endpoints — the same two the
|
|
713
|
+
* 「套餐与积分」 page calls before it decides whether to render a claim card
|
|
714
|
+
* (`/billing/meter/compensation-status`, `/billing/meter/check-gift-claimed`).
|
|
715
|
+
* Reading them is what makes the card honest: the plugin shows a benefit only
|
|
716
|
+
* while the gateway itself would still show one.
|
|
717
|
+
*
|
|
718
|
+
* The two flags answer DIFFERENT questions and neither can be derived from the
|
|
719
|
+
* other — this is the single most important fact about this shape, measured on
|
|
720
|
+
* a real account pool:
|
|
721
|
+
*
|
|
722
|
+
* - `active` is about the WINDOW, not about the reader. An account that has
|
|
723
|
+
* already claimed still reports `active: true` (observed: `claimed: true`
|
|
724
|
+
* together with `active: true` on a claimed gift), so `active` alone cannot
|
|
725
|
+
* mean "claimable".
|
|
726
|
+
* - `claimed` is about THIS account, and it survives the window: once the
|
|
727
|
+
* campaign lapses the endpoint keeps answering `claimed: false` with
|
|
728
|
+
* `active: false` (observed on every international account, whose campaign
|
|
729
|
+
* ended months ago).
|
|
730
|
+
*
|
|
731
|
+
* Hence a benefit is claimable exactly when {@link active} AND NOT
|
|
732
|
+
* {@link claimed}. See `benefitOf` in `src/benefits.ts`.
|
|
733
|
+
*/
|
|
734
|
+
interface WorkBuddyBenefit {
|
|
735
|
+
/** Which campaign this is; the plugin renders one row per kind. */
|
|
736
|
+
kind: 'compensation' | 'gift';
|
|
737
|
+
/** The campaign is inside its window (empty, see: already claimed ≠ inactive). */
|
|
738
|
+
active: boolean;
|
|
739
|
+
/** This account has already taken it — permanently, even after the window. */
|
|
740
|
+
claimed: boolean;
|
|
741
|
+
/** Credits on offer, as the gateway states them. */
|
|
742
|
+
creditNum: number;
|
|
743
|
+
/** Window end, epoch ms. Absent when the gateway sent none it could parse. */
|
|
744
|
+
endsAtMs?: number;
|
|
745
|
+
/** Window start, epoch ms. Compensation omits it. */
|
|
746
|
+
startsAtMs?: number;
|
|
747
|
+
/** When this account claimed it, epoch ms. Only meaningful with `claimed`. */
|
|
748
|
+
claimedAtMs?: number;
|
|
749
|
+
}
|
|
709
750
|
/** Token refresh answer; fields the upstream omits stay absent. */
|
|
710
751
|
interface WorkBuddyRefreshOutcome {
|
|
711
752
|
accessToken: string;
|
|
@@ -934,6 +975,41 @@ declare class WorkBuddyUpstreamClient {
|
|
|
934
975
|
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
935
976
|
/** Query today's check-in status without changing account state. */
|
|
936
977
|
fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
|
|
978
|
+
/**
|
|
979
|
+
* Read every promotional benefit this account can still see, WITHOUT claiming
|
|
980
|
+
* anything.
|
|
981
|
+
*
|
|
982
|
+
* The campaign list is closed and small on purpose: the gateway exposes one
|
|
983
|
+
* status endpoint per campaign, so "discover benefits" is a fixed fan-out, not
|
|
984
|
+
* a search. A new campaign type means a new `/billing/meter/*-status` endpoint
|
|
985
|
+
* added here — the plugin never guesses at undocumented ones.
|
|
986
|
+
*
|
|
987
|
+
* Every endpoint is read in parallel because they are independent, and a
|
|
988
|
+
* failure is per-endpoint: one campaign answering 5xx must not blank out the
|
|
989
|
+
* other. The caller gets only what was actually read.
|
|
990
|
+
*
|
|
991
|
+
* Note the paths carry NO `/v2` prefix — these are not the `/v2/billing/meter`
|
|
992
|
+
* family the credit and check-in calls use. Probing `/v2/...` answers 404, which
|
|
993
|
+
* is how this distinction was found.
|
|
994
|
+
*/
|
|
995
|
+
fetchBenefits(credential: WorkBuddyCredential): Promise<readonly WorkBuddyBenefit[]>;
|
|
996
|
+
/** One benefit status endpoint. Throws on anything that is not a clean `code: 0`. */
|
|
997
|
+
private readBenefitStatus;
|
|
998
|
+
/**
|
|
999
|
+
* Claim one benefit campaign on ONE account (issue #79).
|
|
1000
|
+
*
|
|
1001
|
+
* This is a real credit grant on a real account, so it takes no account
|
|
1002
|
+
* argument other than the credential it is handed: the caller decides which
|
|
1003
|
+
* single account, and there is deliberately no batch form of this method. The
|
|
1004
|
+
* browser route is the only caller, and it guards the mutation with the same
|
|
1005
|
+
* POST + loopback checks the check-in route carries.
|
|
1006
|
+
*
|
|
1007
|
+
* The response is not parsed: the documented envelope of these two endpoints
|
|
1008
|
+
* is an empty success, and the caller re-reads the status afterwards. That
|
|
1009
|
+
* re-read, not this answer, is what the user is shown — which is why a claim
|
|
1010
|
+
* that silently did nothing still cannot leave the card saying 待领取.
|
|
1011
|
+
*/
|
|
1012
|
+
claimBenefit(credential: WorkBuddyCredential, kind: WorkBuddyBenefit['kind']): Promise<void>;
|
|
937
1013
|
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
938
1014
|
claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
|
|
939
1015
|
/**
|
|
@@ -2011,6 +2087,29 @@ declare function probeModel(input: {
|
|
|
2011
2087
|
signal?: AbortSignal;
|
|
2012
2088
|
}): Promise<WorkBuddyProbeResult>;
|
|
2013
2089
|
//#endregion
|
|
2090
|
+
//#region src/benefits.d.ts
|
|
2091
|
+
/** What the account card shows for one campaign. */
|
|
2092
|
+
type WorkBuddyBenefitState =
|
|
2093
|
+
/** Offered, inside its window, not taken yet. The only state with a claim button. */
|
|
2094
|
+
'claimable' |
|
|
2095
|
+
/** This account already took it. Rendered as nothing at all. */
|
|
2096
|
+
'claimed' |
|
|
2097
|
+
/** The window is over and it was never taken — the offer is gone. */
|
|
2098
|
+
'expired';
|
|
2099
|
+
/** One campaign, resolved for display. */
|
|
2100
|
+
interface WorkBuddyBenefitView {
|
|
2101
|
+
kind: WorkBuddyBenefit['kind'];
|
|
2102
|
+
state: WorkBuddyBenefitState;
|
|
2103
|
+
/** Credits on offer, as the gateway stated them. */
|
|
2104
|
+
creditNum: number;
|
|
2105
|
+
/** Window end, epoch ms. */
|
|
2106
|
+
endsAtMs?: number;
|
|
2107
|
+
/** Whole days until the window closes, floored at 0. Absent without `endsAtMs`. */
|
|
2108
|
+
daysLeft?: number;
|
|
2109
|
+
/** True while the window closes within {@link URGENT_DAYS} days. */
|
|
2110
|
+
urgent: boolean;
|
|
2111
|
+
}
|
|
2112
|
+
//#endregion
|
|
2014
2113
|
//#region src/account-pool.d.ts
|
|
2015
2114
|
/**
|
|
2016
2115
|
* Which of the pool's writers produced a measurement.
|
|
@@ -2651,6 +2750,24 @@ type WorkBuddyWebPoolExclusion = 'unusable' | 'rate-limited' |
|
|
|
2651
2750
|
* does not say (issue #56).
|
|
2652
2751
|
*/
|
|
2653
2752
|
'model-busy' | 'out-of-credit' | 'credential-rejected' | 'not-found' | 'unavailable' | 'failed';
|
|
2753
|
+
/** One claimable benefit, as the card renders it (issue #79). */
|
|
2754
|
+
interface WorkBuddyWebBenefit {
|
|
2755
|
+
/** Which campaign: compensation (crash补偿) or gift (新人首月赠送). */
|
|
2756
|
+
kind: 'compensation' | 'gift';
|
|
2757
|
+
/** Display name for the campaign, resolved Host-side so both clients agree. */
|
|
2758
|
+
title: string;
|
|
2759
|
+
/** Credits on offer. */
|
|
2760
|
+
creditNum: number;
|
|
2761
|
+
/**
|
|
2762
|
+
* Whole days until the window closes, floored at 0.
|
|
2763
|
+
*
|
|
2764
|
+
* ABSENT when the gateway stated no end time — the card must then omit the
|
|
2765
|
+
* countdown rather than print a number nobody knows.
|
|
2766
|
+
*/
|
|
2767
|
+
daysLeft?: number;
|
|
2768
|
+
/** True when the window closes within 3 days, so the card can say 即将到期. */
|
|
2769
|
+
urgent: boolean;
|
|
2770
|
+
}
|
|
2654
2771
|
/**
|
|
2655
2772
|
* One account's row in the pool, as the card renders it.
|
|
2656
2773
|
*
|
|
@@ -2727,6 +2844,16 @@ interface WorkBuddyWebPoolAccount {
|
|
|
2727
2844
|
* result the user just watched succeed.
|
|
2728
2845
|
*/
|
|
2729
2846
|
checkedInToday?: boolean;
|
|
2847
|
+
/**
|
|
2848
|
+
* Promotional benefits this account can STILL claim (issue #79).
|
|
2849
|
+
*
|
|
2850
|
+
* Never carries a claimed or lapsed campaign: the issue's rule is that those
|
|
2851
|
+
* do not appear at all, so their absence here IS the "hide it" instruction.
|
|
2852
|
+
* ABSENT means not read (or nothing claimable), and the card renders no line
|
|
2853
|
+
* either way — a benefit line is only ever added to say "there is something
|
|
2854
|
+
* here for you", never to announce its own absence.
|
|
2855
|
+
*/
|
|
2856
|
+
benefits?: readonly WorkBuddyWebBenefit[];
|
|
2730
2857
|
/**
|
|
2731
2858
|
* The model a TEST of this row would use (issue #61).
|
|
2732
2859
|
*
|
|
@@ -2884,7 +3011,7 @@ declare const WORKBUDDY_SETTINGS_NS: "workbuddy";
|
|
|
2884
3011
|
interface WorkBuddyStatusRouteOptions {
|
|
2885
3012
|
/** The region-scoped credential store backing each region's requests. */
|
|
2886
3013
|
store(region: WorkBuddyRegion): WorkBuddyCredentialStore;
|
|
2887
|
-
client: Pick<WorkBuddyUpstreamClient, 'fetchCredits' | 'fetchCheckinStatus' | 'claimDailyCheckin'>;
|
|
3014
|
+
client: Pick<WorkBuddyUpstreamClient, 'fetchCredits' | 'fetchCheckinStatus' | 'claimDailyCheckin' | 'fetchBenefits' | 'claimBenefit'>;
|
|
2888
3015
|
/**
|
|
2889
3016
|
* The requested region's last-refreshed model directory (unfiltered) for
|
|
2890
3017
|
* card display. Region-scoped because the CN and international apps expose
|
|
@@ -3170,6 +3297,37 @@ interface WorkBuddyPoolDeps {
|
|
|
3170
3297
|
* per-account read in the full document.
|
|
3171
3298
|
*/
|
|
3172
3299
|
cachedCheckedInToday?(region: WorkBuddyRegion): Readonly<Record<string, boolean>>;
|
|
3300
|
+
/**
|
|
3301
|
+
* Claimable promotional benefits per account id (issue #79).
|
|
3302
|
+
*
|
|
3303
|
+
* Only campaigns the account can STILL claim are present; a claimed or lapsed
|
|
3304
|
+
* campaign is absent on purpose, so the card renders nothing for it. Absent
|
|
3305
|
+
* (or a missing entry) also means "not read yet", which the card treats the
|
|
3306
|
+
* same way — no line, rather than a guessed state.
|
|
3307
|
+
*/
|
|
3308
|
+
benefits?(region: WorkBuddyRegion): Promise<Readonly<Record<string, readonly WorkBuddyBenefitView[]>>>;
|
|
3309
|
+
/**
|
|
3310
|
+
* Claimable benefits, from the readings already taken this process.
|
|
3311
|
+
*
|
|
3312
|
+
* The first-paint counterpart of {@link benefits}: no request, so painting
|
|
3313
|
+
* never waits on two upstream reads per account.
|
|
3314
|
+
*/
|
|
3315
|
+
cachedBenefits?(region: WorkBuddyRegion): Readonly<Record<string, readonly WorkBuddyBenefitView[]>>;
|
|
3316
|
+
/**
|
|
3317
|
+
* Forget this account's cached benefit reading so the next status read is live.
|
|
3318
|
+
*
|
|
3319
|
+
* The claim route writes to upstream directly, so nothing else in the plugin
|
|
3320
|
+
* knows the campaign just changed. Without this the card kept showing 领取 for
|
|
3321
|
+
* up to `BENEFIT_CACHE_MS` (30 min) after a SUCCESSFUL claim: the credits went
|
|
3322
|
+
* up, the pill stayed, and the only remaining explanation was "the claim
|
|
3323
|
+
* silently failed" — which invited a second click the route then refused as
|
|
3324
|
+
* already-claimed. `checkin` has the same rule and invalidates its own cache;
|
|
3325
|
+
* this is the benefit counterpart.
|
|
3326
|
+
*
|
|
3327
|
+
* Absent is legal (a Host from before this hook) and means "the click refreshes
|
|
3328
|
+
* the card, but the host-side cache may still answer stale for a while".
|
|
3329
|
+
*/
|
|
3330
|
+
invalidateBenefit?(region: WorkBuddyRegion, accountId: string): void;
|
|
3173
3331
|
/**
|
|
3174
3332
|
* Every local sign-in the pool does NOT cover, so the card can offer them to
|
|
3175
3333
|
* check in. Absent means the card lists only pool members.
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-BQHO4oiB.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -78,6 +78,46 @@ async function resolveCredentialRecovery(options) {
|
|
|
78
78
|
};
|
|
79
79
|
return { reloginRequired: false };
|
|
80
80
|
}
|
|
81
|
+
/** How the display copy names each campaign. */
|
|
82
|
+
const WORKBUDDY_BENEFIT_TITLES = {
|
|
83
|
+
compensation: "模型服务异常补偿",
|
|
84
|
+
gift: "新人首月赠送"
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Resolve one campaign against the clock.
|
|
88
|
+
*
|
|
89
|
+
* `active` is the gateway's own window verdict and is trusted as-is; the clock is
|
|
90
|
+
* consulted only for the countdown, never to override `active`. An account whose
|
|
91
|
+
* clock is skewed therefore still renders the upstream's answer rather than a
|
|
92
|
+
* locally-invented one.
|
|
93
|
+
*/
|
|
94
|
+
function benefitOf(benefit, nowMs) {
|
|
95
|
+
const daysLeft = benefit.endsAtMs === void 0 ? void 0 : Math.max(0, Math.ceil((benefit.endsAtMs - nowMs) / 864e5));
|
|
96
|
+
const state = benefit.claimed ? "claimed" : benefit.active ? "claimable" : "expired";
|
|
97
|
+
return {
|
|
98
|
+
kind: benefit.kind,
|
|
99
|
+
state,
|
|
100
|
+
creditNum: benefit.creditNum,
|
|
101
|
+
...benefit.endsAtMs === void 0 ? {} : { endsAtMs: benefit.endsAtMs },
|
|
102
|
+
...daysLeft === void 0 ? {} : { daysLeft },
|
|
103
|
+
urgent: state === "claimable" && daysLeft !== void 0 && daysLeft <= 3
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The claimable campaigns of one account, urgent first.
|
|
108
|
+
*
|
|
109
|
+
* Ordered by `daysLeft` ascending so the soonest-expiring offer leads — the whole
|
|
110
|
+
* point of surfacing this is that a 3-day window otherwise lapses unnoticed. A
|
|
111
|
+
* campaign with no stated end sorts last, because "no deadline" is never more
|
|
112
|
+
* urgent than a deadline.
|
|
113
|
+
*
|
|
114
|
+
* `claimed` and `expired` campaigns are dropped here, not in the renderer: the
|
|
115
|
+
* user asked for them to be ABSENT, and encoding that in one tested place beats
|
|
116
|
+
* asking every caller to remember to filter.
|
|
117
|
+
*/
|
|
118
|
+
function claimableBenefits(benefits, nowMs) {
|
|
119
|
+
return benefits.map((benefit) => benefitOf(benefit, nowMs)).filter((view) => view.state === "claimable").sort((left, right) => (left.daysLeft ?? 1e6) - (right.daysLeft ?? 1e6));
|
|
120
|
+
}
|
|
81
121
|
//#endregion
|
|
82
122
|
//#region src/adapter.ts
|
|
83
123
|
/**
|
|
@@ -4772,6 +4812,15 @@ const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
|
4772
4812
|
* per-account figures.
|
|
4773
4813
|
*/
|
|
4774
4814
|
const WORKBUDDY_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-workbuddy/account-credits";
|
|
4815
|
+
/**
|
|
4816
|
+
* Plugin-owned benefit claim endpoint: POST + loopback, ONE account per call.
|
|
4817
|
+
*
|
|
4818
|
+
* A credit grant is a write on a real account, so it carries the same guards as
|
|
4819
|
+
* check-in, plus `?accountId=` because the pool can hold accounts this plugin
|
|
4820
|
+
* did not pick. There is deliberately no batch action here — see
|
|
4821
|
+
* `claimBenefit` and the route handler.
|
|
4822
|
+
*/
|
|
4823
|
+
const WORKBUDDY_BENEFIT_CLAIM_PATH = "/plugins/dsh-connect-workbuddy/benefit/claim";
|
|
4775
4824
|
/** Plugin-owned live model refresh endpoint. */
|
|
4776
4825
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
4777
4826
|
/** Plugin-owned local account rescan endpoint. */
|
|
@@ -5366,6 +5415,7 @@ async function workBuddyWebPool(deps, region, fast = false) {
|
|
|
5366
5415
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
5367
5416
|
const others = await pool.otherAccounts?.(region) ?? [];
|
|
5368
5417
|
const checkedIn = fast ? pool.cachedCheckedInToday?.(region) ?? {} : await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5418
|
+
const claimable = fast ? pool.cachedBenefits?.(region) ?? {} : await pool.benefits?.(region).catch(() => ({})) ?? {};
|
|
5369
5419
|
const rows = [...ranked.map((row) => ({
|
|
5370
5420
|
account: row.account,
|
|
5371
5421
|
member: true,
|
|
@@ -5413,7 +5463,18 @@ async function workBuddyWebPool(deps, region, fast = false) {
|
|
|
5413
5463
|
...row?.excludedBy === void 0 ? {} : { excludedBy: row.excludedBy },
|
|
5414
5464
|
current: account.id === currentAccountId,
|
|
5415
5465
|
member,
|
|
5416
|
-
...checkedIn[account.id] === void 0 ? {} : { checkedInToday: checkedIn[account.id] }
|
|
5466
|
+
...checkedIn[account.id] === void 0 ? {} : { checkedInToday: checkedIn[account.id] },
|
|
5467
|
+
...(() => {
|
|
5468
|
+
const views = claimable[account.id];
|
|
5469
|
+
if (views === void 0 || views.length === 0) return {};
|
|
5470
|
+
return { benefits: views.map((view) => ({
|
|
5471
|
+
kind: view.kind,
|
|
5472
|
+
title: WORKBUDDY_BENEFIT_TITLES[view.kind],
|
|
5473
|
+
creditNum: view.creditNum,
|
|
5474
|
+
...view.daysLeft === void 0 ? {} : { daysLeft: view.daysLeft },
|
|
5475
|
+
urgent: view.urgent
|
|
5476
|
+
})) };
|
|
5477
|
+
})()
|
|
5417
5478
|
};
|
|
5418
5479
|
});
|
|
5419
5480
|
return {
|
|
@@ -5543,6 +5604,43 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
5543
5604
|
}
|
|
5544
5605
|
}
|
|
5545
5606
|
});
|
|
5607
|
+
ctx.webServer.register({
|
|
5608
|
+
kind: "exact",
|
|
5609
|
+
path: WORKBUDDY_BENEFIT_CLAIM_PATH,
|
|
5610
|
+
handler: async (req, res) => {
|
|
5611
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5612
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5613
|
+
const region = requestRegion(req, res);
|
|
5614
|
+
if (region === void 0) return;
|
|
5615
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5616
|
+
const accountId = url.searchParams.get(WORKBUDDY_POOL_ACCOUNT_PARAM);
|
|
5617
|
+
const kind = url.searchParams.get("kind");
|
|
5618
|
+
if (accountId === null || accountId === "") return json(res, 400, { error: "accountId is required" });
|
|
5619
|
+
if (kind !== "compensation" && kind !== "gift") return json(res, 400, { error: "kind must be compensation or gift" });
|
|
5620
|
+
try {
|
|
5621
|
+
const credential = await deps.store(region).credentialFor(accountId);
|
|
5622
|
+
if (credential === void 0) return json(res, 404, { error: "no credential for this account" });
|
|
5623
|
+
const target = (await deps.client.fetchBenefits(credential)).find((benefit) => benefit.kind === kind);
|
|
5624
|
+
if (target === void 0) return json(res, 404, { error: "benefit not available on this account" });
|
|
5625
|
+
if (target.claimed) return json(res, 409, {
|
|
5626
|
+
error: "benefit already claimed",
|
|
5627
|
+
benefit: target
|
|
5628
|
+
});
|
|
5629
|
+
if (!target.active) return json(res, 409, {
|
|
5630
|
+
error: "benefit activity is not active",
|
|
5631
|
+
benefit: target
|
|
5632
|
+
});
|
|
5633
|
+
await deps.client.claimBenefit(credential, kind);
|
|
5634
|
+
deps.pool?.invalidateBenefit?.(region, accountId);
|
|
5635
|
+
json(res, 200, {
|
|
5636
|
+
claimed: true,
|
|
5637
|
+
benefit: (await deps.client.fetchBenefits(credential)).find((benefit) => benefit.kind === kind) ?? target
|
|
5638
|
+
});
|
|
5639
|
+
} catch (error) {
|
|
5640
|
+
json(res, 500, { error: safeMessage(error) });
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
});
|
|
5546
5644
|
const disposeRefresh = ctx.webServer.register({
|
|
5547
5645
|
kind: "exact",
|
|
5548
5646
|
path: WORKBUDDY_MODELS_REFRESH_PATH,
|
|
@@ -6268,6 +6366,16 @@ function apply(ctx, config) {
|
|
|
6268
6366
|
* immediately instead of waiting out the TTL.
|
|
6269
6367
|
*/
|
|
6270
6368
|
const checkinStateCache = {};
|
|
6369
|
+
/**
|
|
6370
|
+
* Per-region, per-account benefit readings (issue #79).
|
|
6371
|
+
*
|
|
6372
|
+
* Same shape and same reasoning as `checkinStateCache`: claim state changes
|
|
6373
|
+
* when a human acts on the web page or in the desktop app, not spontaneously,
|
|
6374
|
+
* so a per-poll read of two endpoints per account would be thousands of
|
|
6375
|
+
* requests a day to learn nothing. Coarser than the check-in TTL because a
|
|
6376
|
+
* benefit window is measured in days, not hours.
|
|
6377
|
+
*/
|
|
6378
|
+
const benefitStateCache = {};
|
|
6271
6379
|
const settingsNs = settingsNamespaceOf(ctx);
|
|
6272
6380
|
const stacks = {};
|
|
6273
6381
|
for (const region of REGION_KEYS) {
|
|
@@ -6653,6 +6761,77 @@ function apply(ctx, config) {
|
|
|
6653
6761
|
}
|
|
6654
6762
|
return cached;
|
|
6655
6763
|
},
|
|
6764
|
+
/**
|
|
6765
|
+
* Claimable promotional benefits per account (issue #79).
|
|
6766
|
+
*
|
|
6767
|
+
* Only campaigns the account can STILL claim are returned — a claimed or
|
|
6768
|
+
* lapsed one is absent, which is what the issue asked for. Absent means
|
|
6769
|
+
* the card shows no benefit line at all.
|
|
6770
|
+
*
|
|
6771
|
+
* Two status reads per account, so it is cached for `BENEFIT_CACHE_MS` and
|
|
6772
|
+
* skipped entirely when the pool is off (same reasoning as
|
|
6773
|
+
* `checkedInToday`: no table, no request).
|
|
6774
|
+
*/
|
|
6775
|
+
async benefits(region) {
|
|
6776
|
+
if (!poolPreferencesOf(current(), region).enabled) return {};
|
|
6777
|
+
const store = stacks[region].store;
|
|
6778
|
+
const accounts = await store.accounts();
|
|
6779
|
+
const now = Date.now();
|
|
6780
|
+
const entries = await Promise.all(accounts.map(async (account) => {
|
|
6781
|
+
const cached = benefitStateCache[region]?.[account.id];
|
|
6782
|
+
if (cached !== void 0 && now - cached.atMs < 18e5) return [account.id, cached.views];
|
|
6783
|
+
const credential = await store.credentialFor(account.id).catch(() => void 0);
|
|
6784
|
+
if (credential === void 0) return void 0;
|
|
6785
|
+
const read = await client.fetchBenefits(credential).catch(() => void 0);
|
|
6786
|
+
if (read === void 0) return void 0;
|
|
6787
|
+
const views = claimableBenefits(read, now);
|
|
6788
|
+
benefitStateCache[region] = {
|
|
6789
|
+
...benefitStateCache[region],
|
|
6790
|
+
[account.id]: {
|
|
6791
|
+
views,
|
|
6792
|
+
atMs: now
|
|
6793
|
+
}
|
|
6794
|
+
};
|
|
6795
|
+
return [account.id, views];
|
|
6796
|
+
}));
|
|
6797
|
+
return Object.fromEntries(entries.filter((entry) => entry !== void 0));
|
|
6798
|
+
},
|
|
6799
|
+
/**
|
|
6800
|
+
* Claimable benefits from the readings already taken this process.
|
|
6801
|
+
*
|
|
6802
|
+
* The first-paint counterpart of `benefits`, mirroring
|
|
6803
|
+
* `cachedCheckedInToday`: no request, so painting never blocks on two
|
|
6804
|
+
* calls per account. An unmeasured account is absent, not empty.
|
|
6805
|
+
*/
|
|
6806
|
+
cachedBenefits(region) {
|
|
6807
|
+
if (!poolPreferencesOf(current(), region).enabled) return {};
|
|
6808
|
+
const now = Date.now();
|
|
6809
|
+
const state = benefitStateCache[region] ?? {};
|
|
6810
|
+
const cached = {};
|
|
6811
|
+
for (const [accountId, entry] of Object.entries(state)) {
|
|
6812
|
+
if (now - entry.atMs >= 18e5) continue;
|
|
6813
|
+
cached[accountId] = entry.views;
|
|
6814
|
+
}
|
|
6815
|
+
return cached;
|
|
6816
|
+
},
|
|
6817
|
+
/**
|
|
6818
|
+
* Drop one account's cached benefit reading (issue #79).
|
|
6819
|
+
*
|
|
6820
|
+
* The claim route writes upstream itself, so this cache is the ONLY thing
|
|
6821
|
+
* that can keep answering "claimable" after a successful claim — and it was
|
|
6822
|
+
* never invalidated: the credits went up, the 领取 pill stayed for up to 30
|
|
6823
|
+
* minutes, and the claim looked like it had silently failed. That is the
|
|
6824
|
+
* exact symptom the user reported. `checkin` already deletes its entries
|
|
6825
|
+
* after a batch; this is the same rule for the claim.
|
|
6826
|
+
*
|
|
6827
|
+
* Deleting (rather than overwriting with the post-claim read) keeps the
|
|
6828
|
+
* ownership where it belongs: the route already made the authoritative
|
|
6829
|
+
* re-read, and the next `benefits()` refills this from live data.
|
|
6830
|
+
*/
|
|
6831
|
+
invalidateBenefit(region, accountId) {
|
|
6832
|
+
const cached = benefitStateCache[region];
|
|
6833
|
+
if (cached !== void 0) delete cached[accountId];
|
|
6834
|
+
},
|
|
6656
6835
|
async currentAccountId(region) {
|
|
6657
6836
|
await applyPoolSelection(region).catch(() => void 0);
|
|
6658
6837
|
const credential = await stacks[region].store.current().catch(() => void 0);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
|
-
"description": "把本机登录的 WorkBuddy
|
|
5
|
-
"version": "4.
|
|
4
|
+
"description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
|
|
5
|
+
"version": "4.10.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|