dsh-connect-workbuddy 2.3.1 → 3.0.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 CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0 (2026-09-30)
4
+
5
+ > **主版本号升级的理由**:账号池是第一个会改变「哪个账号被计费」的功能。在此之前插件只有一条硬规则——**绝不为了找积分而重排账号**(`src/auth.ts` 的原注释:*This is NOT credit-seeking*)。账号池把这条规则变成**用户显式授权的例外**:打开「按积分自动轮换」后,被计费的账号可能不是你手动选的那个。这是语义级的变化,因此走主版本号。
6
+
7
+ ### Features
8
+
9
+ #### 账号池:多账号批量操作与自动轮换
10
+
11
+ - **一键签到所有账号**:逐个账号签到,**签到前先查当天状态**,已签到的直接标记、**不发写入请求**(幂等,不重复领取);单个账号失败只标记该行,**不中断整批**。
12
+ - **一键测试所有账号**:对每个成员发一次**真实体积**的探测(约 25k 输入 token,复用模型行的 `probeModel`),得出该账号此刻能否用这个模型。默认目标是**该区域倍率为 0 的免费模型**(国内版即 `deepseek-v4.1-flash`),也可指定具体模型;指定的模型若离开目录,手动动作**明确拒绝**(`target-model-stale`)而不是偷换一个。
13
+ - **按积分自动轮换**:在可用成员中按 **① 积分余额高者优先 → ② 快过期的积分优先用完 → ③ 平手时凭据更新鲜者优先** 选出计费账号。**与手动选择互斥**:开启后手动下拉立即置灰并就地给出原因与关闭按钮(否则会出现「下拉写着 A、实际计费 B」)。**轮换只在运行时决定用谁,绝不写回 `config.accounts[region]`**,关掉开关即原样恢复你上次的手动选择;每次换号都在卡片上留一条记录。
14
+ - **成员是显式勾选、默认空**:「本机有登录」不等于「要拿它去签到和花积分」。**空池不会退化成「全部账号」**——未勾选时两个批量按钮直接拒绝。
15
+ - **定时自动测试**:间隔 **5 ~ 1440 分钟(默认 30)**,定时器每 60 秒走一次。下限 5 分钟是刻意的:更密只会把整池打成限流。
16
+ - **每个区域各有独立的一池**:国内版与国际版的成员、目标模型、间隔与轮换开关互不影响。
17
+ - **不提供自动签到**(明确边界):签到是写操作、会真实领取奖励,只在你点按钮时执行;卡片上也不放一个永远点不动的开关。
18
+ - **成本如实标注**:测试是真实请求,指向免费模型(倍率 0)时几乎不花钱,但**不是零成本空转**。
19
+
20
+ ### Bug Fixes
21
+
22
+ - **卡片里的时间戳全部改为 24 小时制**:四个 `Intl.DateTimeFormat` 都漏了 `hourCycle`,于是跟随浏览器语言的默认偏好——en-US 渲染成 `09/30, 01:23 PM`。现统一为 `hourCycle: 'h23'`(不用 `hour12: false`:后者在部分引擎会落到 `h24`,把半夜显示成 `24:00`)。新增渲染回归测试钉住这一点,并用变异体确认该测试有判别力(去掉 `hourCycle` 即变红)。
23
+ - **陈旧的「目标模型」不再冻结整池的测量**:定时轮次原先在 `resolveTargetModel` 解不出 id 时直接返回,而「你指定的模型已离开目录」正是产出「无 id」的情形之一。于是一处显示偏好的错误会让整个区域的测量停摆:`被限流` 的成员靠时钟就能回池,而 `连不上上游` 的成员只能靠**新的探测**清除——探测不再写入,该成员便**永久挂起**。现在定时轮次会回落到该区域的**免费**模型继续刷新;没有免费模型时仍然整轮跳过(**绝不在定时器上烧收费模型**)。手动动作依旧拒绝陈旧目标。
24
+ - **一次刷新失败不再抹掉整个面板**:`refreshUsage` 的失败路径原先把整个区域的状态塌缩成 `{status:'error', message}`,而面板主体 gate 在 `signed-in` 上——于是**一次网络抖动就让正在编辑的账号池连草稿一起消失**。现在沿用项目既有的「快照 + 分区错误叠加」范式:**有数据就叠加 `refreshError`,没数据才塌缩**。
25
+ - **卡片不再永远渲染未登录占位**:`usageGuard` 的极性写反了(`begin()` 返回的探针报告的是**过期**),导致每一个未被更新的响应都被丢弃。已修正为 `const stale = …; if (stale()) return`,并由真实渲染测试(jsdom + react-dom)钉住。
26
+ - **间隔输入框可以正常输入**:逐键 clamp 会让 `120` 被记成 `520`、清空即跳回 `5`。现在只把**合法的整数**折进草稿,显示始终是原始输入,失焦/回车时**恰好收敛一次**。
27
+ - **保存失败不再丢弃草稿**:写入失败或「回读没有带来新 props」时保留草稿并说明原因;**草稿是用户编辑的唯一副本**。
28
+ - **批量动作后重新读取用量**:签到会领取积分、测试会写入探测结果,而面板数字来自用量路由——现在批量成功后**恰好重新读取一次**,且**失败时不重读**(避免掩盖错误)。
29
+ - **同一区域的用量请求只允许最新一次写入**:轮询 / 保存 / 批量三种刷新会并发,慢的响应原先可以覆盖新的快照。
30
+ - **成员勾选不再静默丢账号**:`editDraft` 的依赖数组缺了保存的成员列表,勾选会以**旧成员列表**为基准,保存即静默移出成员。
31
+ - **池路由的错误按原因本地化**:四个 503/500 位置现在都带结构化的 `reason`,中文用户不再只看到英文。
32
+ - **幽灵池(成员已消失)给出可执行的恢复指引**,而不是笼统失败。
33
+
34
+ ### Tests
35
+
36
+ - **引入真实渲染测试(jsdom + react-dom)**:第四轮独立对抗验证证明「读源码文本」的守卫与「住在行为里」的缺陷之间必然有缝——18 个变异体恢复了**用户可见**的缺陷而整套测试仍全绿。本轮把 `AccountPool.tsx` / `WorkBuddyCard.tsx` 真正挂载进 DOM 断言,从根上结束这一整类问题,并据此击毙了其中 16 个(余 2 个经逐字论证为**等价变异体**,不恢复任何缺陷)。
37
+ - 逐条收紧被证明可绕过的守卫:`stripLiterals()` 连**字符串字面量**一起去(原先只去注释,一个字符串就能顶替一次真实调用);「恰好一次」改为对**标识符**计数;A-8 增加「`committed` 只能被写一次且不在 `catch` 里」。
38
+ - 修掉一个会**伪造结论**的工装缺陷:调度器用例对负载敏感,固定轮数的排空在 CPU 争用下会读到 0,变异工装会把它读成「守卫抓住了变异体」,从而报告出并不存在的覆盖。现在改为**排空到条件成立**。
39
+
40
+ ### Docs
41
+
42
+ - README 两份均新增**账号池**章节(含产品截图):两个批量动作、成员为何必须显式勾选、自动轮换与手动选择的互斥、目标模型与间隔、四种探测结果与回池条件、以及为什么卡片上有两个「保存」。
43
+ - 新增 `docs/audit/` 审计与修复档案:A–I 九份独立报告 + MASTER 汇总 + E-fix-log 逐轮修复记录。全部 51 条审计条目经独立验证**100% 可复现**(0 条误报)。
44
+
3
45
  ## 2.3.1 (2026-09-29)
4
46
 
5
47
  ### Features
package/README.en.md CHANGED
@@ -29,6 +29,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
29
29
 
30
30
  **Two things to know.** ① **It really does spend credits** — one measured probe reported `credit` 0.02 (a refused one 429s and costs almost nothing). So testing is **manual and per-model only**: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, **accepting a single model only** and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises up to 200K to DSH, so DSH compacts very late — hitting the throttle in a long session follows directly from that.
31
31
  - **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
32
+ - **Account pool: batch actions across several accounts, plus credit-aware rotation** — 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 — when you want it — **rotation by credits**, letting the plugin pick which pool member gets billed. 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". Rotation and manual selection are **mutually exclusive** and the card says why, in place; rotation only decides who serves at runtime and **never writes back your manual choice**, so switching it off restores that choice verbatim. 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).
32
33
  - **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.
33
34
  - **Read-only credits overview** — remaining credit aggregated per package, plus each model's credit multiplier. Queries consume no credits.
34
35
  - **Multi-candidate credential paths** — probes the platform defaults for macOS / Windows / Linux in turn, overridable by environment variable or directly in the card. **When nothing is found, the card lists which paths were probed and why each one failed**: five distinct reasons (absent / unreadable / no usable token / encrypted with no key available / belongs to the other region), so the easiest case to misdiagnose — signed in, but the desktop app is not present — says the app must be there instead of telling you to sign in again. The **belongs to the other region** reason is different in kind: that sign-in is real and usable, it is simply filed under the other tab, so the card states the fix outright — switch tabs, no need to sign in again. The list says it once: the paragraph states the conclusion, the collapsed list supplies the per-path detail, and the two never repeat each other.
@@ -55,6 +56,64 @@ Credentials are read (read-only) from the WorkBuddy desktop app's own auth file.
55
56
 
56
57
  **Encrypted credential fields**: newer desktop builds (Windows first) replace the auth file's `accessToken` / `refreshToken` strings with an AES-256-GCM envelope (`{"$wbEncrypted":1,"envelope":"…"}`), and the account's **display name (`nickname`) is encrypted the same way** — left unopened it degrades to a bare uin number. The envelope is sealed with a field key **compiled into the app itself**, not a user secret — so the plugin ships no key copy. When it meets an encrypted document it runs the installed app with `ELECTRON_RUN_AS_NODE` and calls that app's own native binding (`electron_browser_workbuddy_storage.loggerGet()`) to fetch the same payload and derive the key locally: it asks the very build that wrote the file. The key is cached in process memory only — never on disk, never in logs. Plain-string documents (macOS, older Windows builds) take the original path and never spawn a child process. `account.phoneNumber` is encrypted there too, and the plugin deliberately **neither reads nor displays it**. If the app lives somewhere unusual, point `WORKBUDDY_APP_EXECUTABLE` at its executable.
57
58
 
59
+ ## Account pool
60
+
61
+ <p align="center">
62
+ <img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy account pool: check-in all, test all, membership selection and credit-aware rotation" />
63
+ </p>
64
+
65
+ A machine usually carries more than one WorkBuddy sign-in. The **account pool** turns that into a single click: tick the accounts you want into the pool, then press **Check in all accounts** or **Test all accounts**; open **rotation by credits** when you want the plugin to choose the billed account for you.
66
+
67
+ **Each region (domestic / international) has its own independent pool** — membership, target model, interval, and the rotation switch never affect the other side, because the two sides' accounts belong to different upstream stacks.
68
+
69
+ ### The two batch actions
70
+
71
+ - **Check in all accounts** — signs in account by account. It **reads today's status first**, so an account that is already checked in is marked as such and receives **no write request at all** (idempotent — no duplicate claims); a single account failing marks only that row and **never aborts the batch**.
72
+ - **Test all accounts** — sends one **real-volume** probe per member (about 25k input tokens; the same `probeModel` the model rows use, so it inherits both measured facts: throttling is triggered by request **size**, and the reset time is written into the response **body** rather than a header), reporting whether that account can serve that model right now.
73
+
74
+ **Which model is tested:** by default the **zero-multiplier free model in that region's catalog** (`deepseek-v4.1-flash` on the domestic side), or a specific model you pick in the dropdown. If the model you named has left the catalog, the **manual action refuses outright** (`target-model-stale`) instead of quietly substituting another one — you asked to test *that* model, and swapping it answers a question you did not ask.
75
+
76
+ **Cost and boundaries:** testing is a real request. Pointed at the free model (multiplier 0) it costs virtually nothing, but it is **not a free no-op**; check-in claims that day's real reward. Both actions run **only when you press the button** — the plugin **does not offer automatic check-in**, and the card does not show a switch that can never be flipped.
77
+
78
+ ### Membership is an explicit opt-in, empty by default
79
+
80
+ "signed in on this machine" is not "spend this account's credits and claim its rewards". Membership starts **empty** and must be ticked one by one (or with **Select all**); **an empty pool never degrades into "all accounts"** — with nothing ticked, both batch actions are refused rather than running every account on your behalf.
81
+
82
+ Ticking boxes is a **draft edit**: it lands on **Save**, the save button carries a dirty marker until then, and the draft survives switching tabs or closing the card.
83
+
84
+ ### Rotation by credits: turning "pick an account by balance" into an explicit grant
85
+
86
+ This plugin's standing rule is that it **never reorders accounts to seek credits** — which account gets billed has to be predictable. Rotation is the exception **you switch on yourself**:
87
+
88
+ 1. Once on, the plugin ranks the usable members by **① highest credit balance first → ② credits expiring soonest first → ③ freshest credential as the tie-break** and bills the winner.
89
+ 2. **It is mutually exclusive with manual selection**: turning it on **greys out the account dropdown immediately** and explains why in place, with a switch-off button right there. Otherwise you get the worst possible state — **the dropdown reads account A while account B is actually billed**.
90
+ 3. **Rotation only decides who serves at runtime and never writes back your manual choice** (it does not touch `config.accounts[region]`). Switch it off and your last manual pick returns untouched.
91
+
92
+ **Every rotation leaves a line in the card** (when, and from whom to whom) rather than happening silently.
93
+
94
+ ### The automatic test interval
95
+
96
+ A timer ticks every 60 seconds and runs a "test all accounts" pass on your interval (**5 to 1440 minutes, default 30**). The 5-minute floor is deliberate: anything denser only throttles the whole pool.
97
+
98
+ **One easily missed edge:** if the model you named has left the catalog, the scheduled pass **does not stop** — it falls back to the region's free model and keeps the measurements fresh. It has to: a member comes back into the pool through a **new probe result** (below), and a stale display preference must not freeze the whole pool's measurements. The manual action still refuses, for the reason given above.
99
+
100
+ ### Why an account leaves the pool, and when it comes back
101
+
102
+ | Probe outcome | What it means for rotation |
103
+ | --- | --- |
104
+ | Usable | Eligible |
105
+ | Rate limited | Out of the pool **temporarily**, returning on the reset time the upstream stated (and when it states none, the card says so rather than **inventing a countdown**) |
106
+ | Out of credit | Out of the pool, returning at the monthly package's refresh time |
107
+ | Credential rejected / model not found / upstream unreachable / failed | Out of the pool until a later probe refreshes the result |
108
+
109
+ The key line: **"rate limited" is not "unusable"**. It is precisely the state most worth waiting on — treating it as permanently dead throws away the accounts that would have served you best.
110
+
111
+ ### Saving and drafts: why the card has two Save buttons
112
+
113
+ One for model management, one for the pool — **deliberately kept apart** rather than merged: the two draft domains differ, and merging them means one failed write drags the other down (a catalog that will not save would stop you saving pool preferences too); a shared Discard would also throw away both drafts on a single misclick. Both sides use a **verified write** — **a write that did not land never discards the draft**, or your edits are lost for good.
114
+
115
+ One class of state in the pool **does not go through a draft**: probe results, cooldowns, and the account rotation currently picked. Those are **observations**, written frequently by the plugin itself. The reason is practical — drafts are overwrite-based, so if observations lived in one, a single manual **Save** could roll the result a timer had just written **back to a several-minute-old value**.
116
+
58
117
  ## Install
59
118
 
60
119
  > ⚠️ **Version requirement: DSH 0.1.7-rc.1 or newer (current window: `0.1.7-rc.1` – `0.2.x`).** Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15. Since v2.1.2 the upper bound is **`<0.3.0-0`**: the **whole 0.2.0 line, GA included, is verified**; the 0.3.0 line is explicitly refused. See [Supported host range](#supported-host-range) below.
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  <a href="README.en.md">English</a> ·
11
11
  <a href="#安装">安装</a> ·
12
12
  <a href="#工作原理">工作原理</a> ·
13
+ <a href="#账号池">账号池</a> ·
13
14
  <a href="CHANGELOG.md">更新日志</a> ·
14
15
  <a href="https://github.com/dingminhua/dsh-connect-workbuddy/issues">问题反馈</a>
15
16
  </p>
@@ -35,6 +36,7 @@
35
36
 
36
37
  **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件向 DSH 宣称的窗口最高 200K,所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。
37
38
  - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
39
+ - **账号池:多账号批量操作与自动轮换** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型),需要时再打开**按积分自动轮换**让插件在池内自行挑一个计费账号。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);自动轮换与手动选择**互斥**并就地说明原因;轮换只决定运行时用谁,**永不写回你的手动选择**,关掉开关即原样恢复。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
38
40
  - **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
39
41
  - **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
40
42
  - **凭据路径多候选** —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。**未登录时卡片会列出「探测过哪些路径、各自为什么没成」**:五档原因(不存在 / 读不了 / 没有可用 token / 字段已加密且拿不到密钥 / 属于另一个地区)分开呈现,因此「已经登录了、但桌面 App 不在场」这种最容易误判的情况会明说需要 App 在场,而不是叫你重新登录一次。**属于另一个地区**那一档尤其不同:登录信息是真实可用的,只是挂在另一个标签页下,卡片会直接给出结论「切换标签页即可,不用重新登录」。列表只讲一次——提示段落说结论,折叠列表给路径细节,两处不重复同一件事。
@@ -61,6 +63,64 @@ DSH PiAiAdapter(每个 provider 一套)
61
63
 
62
64
  **加密凭据字段**:新版桌面端(Windows 先行)把 auth 文件里的 `accessToken` / `refreshToken` 从纯字符串改为 `{"$wbEncrypted":1,"envelope":"…"}` 的 AES-256-GCM 信封,**账号昵称 `nickname` 同样被加密**(不解密就会退回成一串 uin 数字)。信封用的是 App **自身构建期内置**的字段密钥,并非用户密钥,因此插件不内置任何密钥副本,而是在需要时用 `ELECTRON_RUN_AS_NODE` 调用已安装 App 的原生绑定(`electron_browser_workbuddy_storage.loggerGet()`)取回同一份载荷并就地派生密钥——也就是「向写下这个文件的那个构建本身询问」。密钥只在本进程内存中缓存,不落盘、不进日志;纯字符串文件(macOS 与旧版 Windows)走原路径,一次子进程都不会启动。其中的 `account.phoneNumber` 虽同样加密,插件**不读取也不显示**。若 App 安装在非常规位置,用 `WORKBUDDY_APP_EXECUTABLE` 指定其可执行文件。
63
65
 
66
+ ## 账号池
67
+
68
+ <p align="center">
69
+ <img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy 账号池区块:一键签到所有账号、一键测试所有账号、成员勾选与自动轮换" />
70
+ </p>
71
+
72
+ 一台机器上往往登录过多个 WorkBuddy 账号。**账号池**把这件事收成一次点击:把要用的账号勾进池子,然后「一键签到所有账号」或「一键测试所有账号」;需要时再打开**按积分自动轮换**,让插件在池内自行挑一个计费账号。
73
+
74
+ **每个区域(国内版 / 国际版)各有独立的一池**:成员、目标模型、间隔与轮换开关互不影响——两边的账号本来就属于不同的上游栈。
75
+
76
+ ### 两个批量动作
77
+
78
+ - **一键签到所有账号** —— 逐个账号签到。**签到前先查当天状态**,已签到的直接标记为「已签到」而**不发写入请求**(幂等,不重复领取);任何单个账号失败只标记该行,**不中断整批**。
79
+ - **一键测试所有账号** —— 对每个成员发一次**真实体积**的探测请求(约 25k 输入 token;与模型行的「测试」是同一套 `probeModel`,因而同样继承两个实测结论:限流**按请求体积**触发、恢复时间写在**响应体**里而不是响应头),得出该账号此刻能否用这个模型。
80
+
81
+ **测试目标是哪个模型**:默认是**该区域目录里倍率为 0 的免费模型**(国内版即 `deepseek-v4.1-flash`),也可以在下拉里指定一个具体模型。指定的模型若已离开目录,**手动动作会明确拒绝**(`target-model-stale`),而不是偷偷换一个——你点的是「测这个模型」,换掉就是答非所问。
82
+
83
+ **成本与边界**:测试是真实请求,指向免费模型时(倍率 0)几乎不花钱,但它**不是零成本的空转**;签到则会真实领取当日奖励。两个动作都**只在你点按钮时执行**——插件**不提供自动签到**,卡片上也不放一个永远点不动的开关。
84
+
85
+ ### 池成员是显式勾选,默认一个都不含
86
+
87
+ 「本机有登录」不等于「要拿它去签到和花积分」。成员默认**空**,必须逐个勾选(或点「全选」);**空池不会退化成「全部账号」**——没勾选时两个批量动作直接拒绝,而不是替你把所有账号跑一遍。
88
+
89
+ 勾选是一次**草稿编辑**:点「保存」才落盘,未保存时保存按钮带 dirty 指示;切走或关掉卡片,草稿仍在。
90
+
91
+ ### 自动轮换:把「按积分选账号」变成一次显式授权
92
+
93
+ 插件此前的原则是**绝不为了找积分而重排账号**——账单落在谁身上必须可预期。自动轮换是**你显式打开的**例外:
94
+
95
+ 1. 打开后,插件在可用的成员里按 **① 积分余额高者优先 → ② 快过期的积分优先用完 → ③ 平手时凭据更新鲜者优先** 排序,把得分最高的那个作为当前计费账号。
96
+ 2. **它与手动选择互斥**:自动轮换开启时,账号下拉**立即置灰**,并就地给出原因与一个「关闭自动轮换」的按钮。否则你会看到最坏的一种状态——**下拉里写着账号 A,实际计费的却是账号 B**。
97
+ 3. **轮换只在运行时决定用谁,绝不写回你的手动选择**(不写 `config.accounts[region]`)。所以关掉开关后,你上次手动选的账号原封不动地回来。
98
+
99
+ **每次轮换都在卡片上留下一条记录**(何时、从谁到谁),而不是悄悄发生。
100
+
101
+ ### 自动测试的时间间隔
102
+
103
+ 定时器每 60 秒走一次,按你设定的间隔(**5 ~ 1440 分钟,默认 30**)执行一轮「测试所有账号」。下限是 5 分钟而非更短,是刻意的:更密只会把整池打成限流。
104
+
105
+ **一个容易忽略的边界**:若你指定的模型已离开目录,定时轮次**不会就此停摆**——它会回落到该区域的免费模型继续刷新测量。这必须如此:成员能否回池靠的是**新的探测结果**(见下),一个陈旧的显示偏好不该把整池的测量冻住。手动动作仍然拒绝,理由同上。
106
+
107
+ ### 账号为什么出池,以及什么时候回来
108
+
109
+ | 探测结果 | 对轮换的意义 |
110
+ | --- | --- |
111
+ | 可用 | 可入选 |
112
+ | 被限流 | **暂时**出池,按上游给出的重置时间自动回池(上游没说时间就如实说没说,**不编造倒计时**) |
113
+ | 积分耗尽 | 出池,按月周期包的刷新时间回池 |
114
+ | 凭据被拒 / 模型不存在 / 连不上上游 / 失败 | 出池,直到下一次探测刷新结果 |
115
+
116
+ 关键一条:**「被限流」不等于不可用**。它恰恰是最值得等的那一类状态——把它当成永久失效剔除,正好丢掉池子里本来最能用的账号。
117
+
118
+ ### 保存与草稿:为什么卡片上有两个「保存」
119
+
120
+ 模型管理一个、账号池一个,这是**刻意保留**而非漏合并:两者的草稿域不同,合并后一次写入失败会连累另一边(模型目录写不进去,就连池偏好也存不了);一个共享的「放弃」还会在一次误点里同时丢掉两份草稿。两边都是**验证写入**——**写不成功不丢弃草稿**,否则用户的编辑就永久丢了。
121
+
122
+ 池里还有一类状态**不走草稿**:探测结果、冷却时间、当前轮换到的账号。它们是**观测事实**,由插件自己高频写入。原因很实际——草稿是覆盖式的,若观测结果也进草稿,你手工点一次「保存」就可能把定时器刚写进去的结果**回退成几分钟前的旧值**。
123
+
64
124
  ## 安装
65
125
 
66
126
  > ⚠️ **版本要求:DSH 0.1.7-rc.1 及以上(当前窗口:`0.1.7-rc.1` ~ `0.2.x`)。** 自 v2.1.0 起本插件只支持 DSH 0.1.7-rc.1 及以上的宿主(旧版宿主无法通过依赖解析安装本版);0.1.5 及更早的宿主请停留在 v2.0.15。自 v2.1.2 起上界放宽为 **`<0.3.0-0`**——**0.2.0 全线(含正式版)已实测支持**,0.3.0 线则被明确拒绝。详见下方[受支持的宿主范围](#受支持的宿主范围)。