Repository navigation
Releases: XDTrees/dsh-xdpool
Release list
v2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数
这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 403;数据看板不再是一排零。
账号池:扫码渠道按渠道入池、逐模型可控
账号池页新增了扫码渠道卡。此前扫码登录页加的账号游离在账号池之外——账号池只认 WorkBuddy 桌面端登录的账号,扫码加进来的十几个渠道账号在池里没有一席之地,模型开不开、哪个开,全靠猜。现在每家扫码渠道在账号池页有一张自己的卡:账号列表(昵称、启停、到期、余额)、模型列表(逐个勾选启用、免费/失效标签、上下文大小选择)、每日签到与积分领取。改动即时生效,不必点保存。
配套的 RPC 通道一并补齐:前端与扫码渠道后端之间的账号、模型、积分、启停等请求全部走同一封装(channel-rpc.ts),新增 model.setDisabledMany、model.setContextBudget 等方法,保证卡片上看到的和后端真实状态一致。
重复账号自动清理:同一渠道反复扫码不再攒出一排半成品账号——按「可用性」保留(长期凭据优先于临期凭据),其余清掉,日志里写明留了谁删了谁。
停机补偿:自动领取
WorkBuddy 侧偶发的模型服务异常会按账号补偿积分(在网页「用量」页顶部领,过期作废)。现在自动任务多了一项「停机补偿」,默认每天下午检查一次,有就领、领完记账,账单里能看到「补偿 +N 积分」。
免费模型车道:跟上上游的客户端校验
免费模型上游在 10 月中旬收紧了两道校验,旧版几乎每次调用都撞 403:
- 会话 id 编码:上游要求会话 id 按规范的降序时间戳编码(低 12 位落在固定区间),旧实现用哈希摘要当前缀,合规概率只有约 1.5%。现在改为确定性派生规范 id——同一会话稳定映射到同一上游会话,配额不会被反复消耗。
- 客户端指纹:请求头、路径、工具声明的形状全部对齐当前 CLI 客户端(v2 形状),并用真实网关验证过:免费模型能正常返回内容,超限时明确报限流而不是 403 拒绝。
数据看板:不再是一排零
免密通道的调用此前从未接入用量记录器,看板上免费模型的调用数恒为 0、重启即清空。现在每次调用(无论成败)都会记录请求数、轮次、首字延迟、生成速度;Token 总量取决于上游是否回传用量帧,不回传时如实显示「未上报」而不是编一个数。
其他
- 聚合渠道页的模型按「免费优先、上下文窗口、识图能力」排序;能力未知的模型如实留空,不编造窗口数字。
- 渠道卡模型行在模型较多时提供搜索框;按钮配色统一为中性色,hover 有扫光动效。
- 重复的登录入口合并;「已添加 N 个账号」的计数修正为去重后的真实数量。
- 新增 5 个测试文件(免密车道闸门、扫码账号接入、RPC 方法清单、模型选择、客户端 bundle 纯净度),全套 68 文件 895 用例。
v2.2.1 — README 已知限制描述更正
2.2.1 — README 已知限制描述更正
纯文档修正:README「已知限制」里写着池「没法替你发起登录或扫码」——这是改名前的旧描述。现在的 XD Pool 有扫码登录页,可以为自己十三家渠道发起扫码登录;WorkBuddy 本体的 token 仍由桌面 App 的腾讯 SSO 铸造并绑定设备,这条限制保留但改成了准确的说法。代码无变化,发布这一版只为让 npm 页面的 README 跟上。
v2.2.0 — 账号池重启不再失忆,CLI 修好,快照真正回读
2.2.0 — 账号池重启不再失忆,CLI 修好,快照真正回读
群友报的三个问题一起收掉:重启 DSH 后账号池只剩当前登录账号;
dsh-xdpool命令行任何子命令都报ERR_MODULE_NOT_FOUND;import写进去的快照写了就忘,永远读不回来。
重启后账号还在:账号池持久化
现象:在桌面端来回切换登录,攒了一池账号;重启 DSH,池里只剩当前登录的那一个。
根因:账号池的账号列表是纯内存——scan() 只合并不落盘。而上游桌面端在磁盘上只保留当前登录的凭据文件(每次登录覆盖同一个文件),所以「重启 = 磁盘上有什么就是什么」,磁盘上永远只有一个。
修法:新增 account-store.ts,池的扫描从 ~/.dsh/.xdpool/accounts.json 播种、每次扫描后原子回写(先写 .tmp 再改名,崩溃不会留半个文件)。桌面端只记得最后一次登录,池子自己记账。生产入口(宿主和 CLI 共用的 createCore())默认开启;测试构造的池默认关闭,不会把 fixture 写进真实目录。
import 的快照终于有人读了
dsh-xdpool import 一直把 <md5前8>.json 快照写进 ~/.dsh/.xdpool/,但没有任何代码路径读回它——写了就忘。现在插件数据目录本身就是一个扫描源,快照里的每个凭据都会在下次扫描时收进池子;插件自己的状态文件(accounts.json、ignored.json、preferred.json)明确排除,绝不会被当成凭据解析出幻影账号。
CLI 能跑了
lib/bin.js 顶部带着四个打包时从 index.ts 一路带入的副作用 import(@earendil-works/pi-ai 等 optional peer),绑定里零引用——后果是所有 CLI 子命令直接 ERR_MODULE_NOT_FOUND。这四个包改为 external(源码里本来就没有绑定引用),bin.js 加载即净,只有命令真正走到 adapter 才会解析。
文档对齐
README 里「历史登录快照」的四处描述与实际行为不符,一并改写为如实口径:桌面端只保留当前登录,池子自己把攒到的账号持久保存到 ~/.dsh/.xdpool/accounts.json,重启不丢;import 快照同目录,每次扫描收进池。
v2.1.1 — 修掉两个用户看得见的 bug
一个让你在插件清单里看到一条失败的红条目,一个让你的账单上「使用端」是空的。都是 2.1.0 之前就存在的,这一版一起收掉。
插件清单里那条 failed 的 billion-context,没有了
现象:装了 billion-context 的用户会在插件清单里看到两条指向同一模块的 entry——bili-native(正常)和 billion-context-xdpool(fiber failed)。不影响功能,但那条 failed 是用户看得见的,而且没人能解释它为什么在那。
根因:2.0.0 想让没装压缩器的用户也能用 billion 页,于是插入了那条挂载行。但 billion-context 会在 cordis 里注册自己的服务,同一个服务注册第二次会被宿主直接拒绝——先到先得,后来那条的 fiber 就地死亡。2.0.0 的注释里写的是「宿主会禁用重复行」,实测不会。
修法:这条挂载行整个撤了(改成 disabled: true 留个可寻址的记号),billion-mount.ts 里的运行时 create() 也一并删除——两处只删一处,另一处还会打架。billion-context 同时从 dependencies 挪到 optionalDependencies,兑现注释里一直写着的承诺:装不装压缩器,是用户的决定,不是池子替用户做。
billion 页本身不变:没装显示「没装」,装了显示数据——三个状态本来就是分开的。
顺带修的两个判断错误(改判断逻辑时暴露的):
loader.entries()会返回被禁用的行,宿主只是不启动它们,不是丢弃。不排除的话,没装 billion-context 的用户会被我们自己那条 disabled 行误报成「已安装」- fiber 为空的 entry(正在导入)以前被当成「不存在」——那是在「我们负责挂载」的前提下才对;现在只读状态,它就是「已安装」
账单后台的「使用端」不再是空的
现象:从 1.8.4 起,插件发的聊天请求在账单后台的「使用端」一列是空的;同一个账号在 WorkBuddy 桌面端聊天就显示「WorkBuddy」。积分照扣,纯归因展示问题。
根因:网关按请求头里的 X-IDE-* 识别客户端。桌面端带,我们没有——而插件自己的积分自动化链路(openConversation)早就带了,这就是为什么只有主聊天链路归因不出来。
修法:把三个头(X-IDE-Name/Type/Version)提成一个常量,主聊天的两条路径(completeChat/chatStream)和 openConversation 共用同一份——以后不会再漂移。没有放进共享的 chatHeaders():那个函数还被积分自动化的任务接口共用,那些不是聊天接口,乱带身份头有归因风险。
其他
- README 换掉一张 1.8.x 时代的旧截图,补一张账号积分详情
- 账单里合计积分和各积分包的数字改成和「每日 +100 积分」一致的亮绿(以前是灰色,看起来像已花掉)
- 下载量徽章换成静态快照(shields 的实时 npm 徽章被上游限流,时好时坏)
v2.0.0 — 重大更新!!!
这是 XD Pool 从「一张设置卡片」变成「一个多页面的插件」的那个版本。账号池还是那个账号池,但它周围长出了六个新页面。
先说这一版为什么叫 2.0
1.9.0 之前,XD Pool 只有一个界面:一张卡片,从上到下把所有东西堆在一起。账号、积分、模型、签到、自动化,全挤在一屏里,越加越长。这一版把它拆成了七个页面:
| 页面 | 干什么 |
|---|---|
| 账号池 | 本机 WorkBuddy 账号、池健康、用量、积分自动化——原来那张卡片 |
| 扫码登录 | 十三家支持网页 / 扫码登录的厂商,登一次,它们的免费模型就进选择器 |
| 聚合渠道 | 76 家厂商的免费额度接进同一个模型池 |
| 数据看板 | 今天的用量:轮次、调用、失败、换号救回、token、缓存命中、速度 |
| billion-context | 只读观测本机 billion-context 压缩器的运行状态 |
| 路由 | 把多个模型串成一条链,按顺序或轮换使用 |
| 网关 | 把池子开成本地 OpenAI 兼容接口,可转发给局域网其它设备 |
顶部是品牌栏:头像、XD Pool、版本号,右边一个跳转到仓库的星标按钮。七个标签用一条会滑动的光带指示当前位置。
用量面板挪到了最上面
1.9.0 把它放在卡片中部,你得往下滚才看得见。现在它是账号池页的第一块内容——你打开这个页面十次有九次是想看它还活着没有、今天花了多少,那它就该在最上面。
扫码登录:十三家厂商,登一次就够
这些厂商各自有网页或扫码登录,登进去之后它们的免费模型就出现在模型选择器里。这一页是通道,不是账号池——两边管的是不同的东西:账号池管 WorkBuddy 自己的号,这一页管别家的免费额度。
十三家的名单来自一个被逐字节 vendored 进来的 channel-pack(和 dsh-our-free-model 用的是同一份,SHA256 相同)。这里有个已知情况要说明:如果那个插件也在跑,它已经注册了同一条 /api/channel-pack 路由,而宿主对重复路由是直接抛错的。我们没法预检(宿主只暴露 register,没有查询接口),所以做法是让位——检测到是路由所有权冲突就安静退出,页面显示「这些扫码登录通道已经由另一个插件接管了」,而不是报一个你根本看不懂的错。两边服务的是同样的十三家,功能上没有损失。
聚合渠道:76 家厂商,按分类筛
76 家厂商分成六类:API Key 60 家、有免费额度 10 家、本地 3 家、需授权 2 家、完全免费 1 家。
默认一家都不开。 这不是保守,是这一页唯一诚实的默认值:76 家全亮着,你唯一配好的那家和另外 75 个空壳长得一模一样,等于没有信息。判定规则是——显式开关优先;没有显式开关时,只有已经填了可用 key 的厂商算开(填 key 这个动作本身就是「我要用它」)。卡片上的开关和模型选择器读的是同一个谓词,所以两处不可能不一致。
每张卡片可以点开,里面是 key 输入框、endpoint、官网链接和这家能出的模型。填完保存后会保持展开,方便你接着填下一家。
数据看板
1.9.0 把用量账本做出来了,这一版给它一个正式页面:五个 KPI(token、轮次、速度、缓存命中、调用数)+ 明细表。
有两条口径写在这里,免得以后被当成 bug 报回来:累计数永远读 history 的总数,不是把今天的分片加起来——一个半夜归零的「累计」是假的;还有一轮提问 ≠ 一次调用,中间换过号的话后者会更大,两个数分开摆,谁也不冒充谁。
路由:把几个模型串成一条链
一条链可以按 fallback(从头试到尾,每轮都从头开始)或 round-robin(按顺序轮换,可以用 stickyLimit 指定一个成员连续服务几轮)来走。链里的成员可以来自池子,也可以来自别处。
指向已经失效成员的链不会被删掉,只是标出来。 你有意留着的那条链,比我们自作主张帮你清理更值钱。
网关:把池子开成一个本地接口
在 127.0.0.1:8326 上开一个 OpenAI 兼容的接口,可选监听 0.0.0.0 转发给局域网其它设备。带上转发跳数标记防止自己绕回自己,key 随机生成。
billion-context 页是只读的
这一页只观测,不配置、不启动、不内嵌压缩器。它读本机 billion-context 的运行状态,没装 / 没应答 / 就绪三种情况分开显示——「它没在跑」和「它跑了但我们问不到」是两件事。
其余
- 品牌名统一成 XD Pool(只改显示名;包名和 provider id 没动,动了等于换插件)。
- 顶部星标只是跳转链接,不解锁任何功能。文案里不会写「点星解锁」这种话。
- 猫猫旅行和积分自动化一行没动,还在原来的时间点跑。
- 宿主加载器的一处报错修好了:用
pathToFileURL构造 pack 入口,路径里有空格或中文也能起来。
v1.8.4 — 让模型自己说它什么时候免费
1.8.4 — 让模型自己说它什么时候免费
这次去把国内版和国际版的模型清单重新数了一遍,发现三个问题:内置的旧清单已经过时;上游其实在用一种我们完全看不懂的写法宣布活动(写了,然后被我们扔掉);还有一个更隐蔽的——国际版的离线清单里,混进了国内版的中文活动文案。
先把清单数对
之前那几张是「先有个大概,回头再说」的旧账。现在每一行都照着上游接口抄了一遍,连窗口大小都照抄。
- 国内版实测 17 个,比原来多一个叫
space-bunny的(x0.08,窗口 100 万)。 hy4-preview的窗口之前写 1 000 000,上游说 960 000。差的这 4 万不是小事:插件会拿窗口算「这条会话还能塞多少」,比真实值多报 4 万,就等于规划出一个上游转头拒收的请求。这正是 1.7.3 那次要治的病。kimi-k3这个 id 上游早就不用了,现在是kimi-k3-1。
另外你可能会问「视频号上说现在有 claude-sonnet-5.5 了」。我们也是这么想的——两个网关都没有 Claude 系。你听到的大概率是 gpt-5.5。所以这个不是缓存问题,重刷一百次也刷不出来。
上游其实在写「限时免费」,我们把它的字扔了
国内版的模型清单里,每个模型可以带一串标签。上游在其中写了活动的说明,长这样:
badge:限时免费:#FF0000 badge:夜间免费:#FF0000 badge:夜间折扣:#1E90FF
以前我们读这串标签,用的是一套自己发明的词汇表(free、limited-free、night-discount)——上游从来没发过这三种写法里的任何一种。所以每一次活动声明都被读了、然后丢掉,被打了活动标签的模型在界面上跟收费模型长得一模一样。
现在改成直接读上游写的那句话,一个字不改地显示出来。这么做还有个别的好处:上游哪天把「限时免费」改成别的说法,界面自己就跟着变,我们不用去猜它什么意思。
两个由此暴露出来的连带问题
一、「限时免费」被显示成了干巴巴的「免费」。 免费是免费的,但「限时」两个字恰恰是最要紧的那半截——它会过期。把这两半揉成「免费」,等于把有效期藏起来了。现在显示上游原文。
二、界面说你免费,排序却不认。 hy4-preview 上游挂着「限时免费」,价格却是 x0.29。以前排序只认「倍率为 0」,所以这一行顶着一个免费徽章,却排在底下收费模型的后面。徽章和排序自己打架。现在排序也读这个标签。
顺带修了个更危险的:glm-5.2 的标签是「夜间折扣」,x0.79——折扣,仍然要花钱。它该显示(上游写好的话没理由藏起来),但绝不能被算成免费。把「有标签」和「有免费标签」分开,就是为了让这种事不会再滑过去。给假免费比不给标记更糟:那会让人放心用,然后被扣积分。
国际版的离线清单,不该出现国内活动
两个网关的模型清单一直共用同一张内置表。这次上游的标签开始被解析之后,问题就藏不住了:国际版如果恰好拉不到清单、回落到内置表,那几行国内活动的字就会原样出现在国际版模型上——一个那个网关根本不提供的活动,摆在你面前。
现在国际版的内置表会把国内活动标签摘掉。其余内容两个版本仍然共用:国际版实测 25 个模型,我们只完整记下了倍率和窗口,凭记忆补齐其余字段等于往你面前塞编造的内容,不值得。
两件我们没做,并且想说明为什么的事
没有「免费到 X 月 X 日」。 你提过想看到这个。我们查过了:上游的标签只有一句话和一个颜色,没有日期。自己编一个日期看着很贴心,实际后果是——活动万一延期或者提前结束,你会以为还能白用,然后积分被扣。所以这一项我们没做,只显示上游写的那句话里已经有的信息。
没有「你的 14 天新人额度」。 上游的接口里没有任何字段能告诉我们「这个账号用过没有」。本地记你第一次调用 Hy4 preview 的时间也不行——那只能知道你用了插件之后的时间。曾经直接在官方客户端用过的人,本地记不出来,我们会给他显示一个假的「免费」。同上一条,假免费比不给标记更糟。
v1.8.3 — 快过期的积分先花掉,失效的登录别再刷屏
这一版有两件事。一件是新的第五种账号使用方式:让积分包最快过期的那个账号先上,别把积分放过期了。另一件是修一个看着很吓人的毛病——账号失效之后,卡片上会顶着一大段英文,把整个面板撑爆,而且每刷新一次就再撞一次墙。
新增:第五种模式「先花快到期的」
积分有两种:月度包每月自动刷新,永远不会过期;一次性包有明确截止日,到点没用完就归零。以前插件不知道这个区别,轮着用谁都是随机——结果就是某个账号的一次性积分包在角落里静静过期,而你还在用另一个账号刷。
现在多了一个选项:优先使用积分包最快过期的账号。
- 数据直接来自上游本来就返回的
nearestExpiryMs字段,没有为这个模式多发任何一次请求。 - 月度包不参与排序——它不会过期,算它「紧急」纯属瞎紧张。
- 没读过积分的账号排在最后。理由很简单:「没看过」不等于「有截止日」,拿未知去压真实截止日是错的。
- 积分花完或过期之后,那个截止日会被擦掉,不是一直记着。否则池子会死死钉在一个积分已经空了的账号上,跟这个模式的目的正好相反。
- 读不到任何到期信息时,行为退回「优先」模式。放心开。
默认仍然是「优先」,没有偷偷改任何人的设置。
模式选择换了排版:五个并排,说明放下面
第五个模式一加进来,原来那个「两列网格 + 最后一个跨整行」的排版就散了:五个格子掉成两行,最后一个还孤零零撑满一行,看着像掉出了网格。
现在改成一行可换行的芯片:
- 面板放得下的时候,五个模式一行排开,宽度自动均分。设置面板常见的 700px 宽度也还读作一行选择,而不是在 760px 处突然塌成两列。
- 真的放不下了才换行,换行后最后那个会自动撑满整行——这是 flex 的天然行为,不需要任何「最后一项跨行」的补丁规则,也就不会出现「明明第一行还装得下、却把第五个挤到第二行」的怪事。
- 每块芯片里只剩模式名。五句话塞进五个小方块会让选择器变成五段散文,所以说明改成行下方一行字,只讲当前选中的那个模式。
修:网关 401 不再打印一整段英文
有人的卡片上,三个失效账号每个都顶着这么一段:
the WorkBuddy gateway rejected this credential (http 401). This usually means
the account is using a stale sign-in the upstream no longer accepts: sign in
again in the WorkBuddy desktop app, then pick the account on the plugin card.
Run `dsh-workbuddy-xdpool doctor` to list every credential found.
话说得没错,但它出现在面板正文里,三个账号就是三段,把卡片撑到没法看。
现在:
- 这段文字折叠成一个红色短标记,完整原文进鼠标悬停提示。要看细节还在,不想看的时候不占地方。
- 更关键的是它现在真的会被当成「登录已失效」。以前只有「刷新 token 被拒」才会判死,而如果凭据文件里写的过期时间还在未来,刷新那一步根本不会跑——于是每次卡片刷新都老老实实再撞一次 401,撞完再显示一次红字。现在只要网关返回那段 HTML 拒绝页,就按失效处理:账号进冷却、退出轮换。
- 失效的账号不再多吃一次签到探测。以前每个死账号刷新时会被打两次(积分一次、签到一次),现在只打一次——签到那步直接跳过。
- 会自我纠正:只要某次积分读成功了,就说明登录其实是好的,死标立刻清掉。不用手动点任何按钮。
关于那三个账号
顺带说一句:本机实测那三个报 401 的账号是真的失效了,不是插件误判——强制刷新时上游明确回 session_dead (http 401): 12153:refresh token is invalid。要恢复它们,去 WorkBuddy 桌面端重新登录,再回卡片点「重新检测账号」。
v1.8.2 — 免费的那个被限流了,旁边那个收费的同款还能用
免费模型被限流的时候,插件以前只会说一句「所有账号都在冷却」。这句话没错,但它让用户以为自己只剩一条路:等。其实同一个模型往往还有一条收费的产品行,而且在限流器眼里它们是两个不同的模型——免费行被关小黑屋的时候,收费行通常毫发无伤。这个版本就是把这个「另一条路」写出来。
同一个模型,凭什么有两行
上游把一条产品线按多行卖,每行有自己的 id、自己的价格、自己的限流额度,但显示名字是一样的。实测两个网关:
| 网关 | 免费行 | 收费行 |
|---|---|---|
| 国际版 | deepseek-v4.1-flash(x0.00) |
deepseek-v4.1-flash-sg(x0.03) |
| 国内版 | hy3(x0.00) |
hy3-x(x0.05) |
注意 -sg 和 -x 跟免费行没有任何前缀关系,能把它们认成一家的只有那个相同的显示名。所以匹配是按名字来的,不是按 id 猜的。
现在会怎么提示
- 模型列表:免费行后面挂一个小灰标「付费同款 deepseek-v4.1-flash-sg(0.03x)」,鼠标悬停解释「免费档先被限流;同一个模型的付费行不受这个限制影响」。灰的、低调的——这一行现在仍然是免费的那个,标签只是告诉你退路在哪,不是催你花钱。
- 429 报错:以前是「every account is rate-limited for model deepseek-v4.1-flash (4/4 cooling, tried 0)」,现在后面会接一句「deepseek-v4.1-flash-sg is the same model on a paid row (x0.03 credits) and is rate-limited separately; switch to it to keep going」。
- 账号卡片:某个账号上「
hy4-previewcooling to 10:14」这种小标后面,也会跟上「→ 收费行的 id(倍率)还能用」。
刻意没做的事
- 收费行不标标签。 只有倍率等于 0 的行才找孪生兄弟。否则「GLM-5.3」这种本身收费、又恰好有个更便宜的闪速版的模型,也会被挂上一个标签,最后满屏都是提示,等于没提示。
- 没有收费同款就不说话。 独苗模型(整条线只有它自己)不会有任何多余文字,也不会留下一个孤零零的分隔符。
- 不把另一个免费行当退路。 两个免费行是一起被限流的,切过去纯属换个人排队,说了等于骗人。
- 倍率未知的行不算数。 上游没给价格的兄弟行不能被承诺成「那个收费的」——用户切过去以为知道价钱,结果未必。
顺带说清楚:国际版没有「每天领 30 积分」
上一版已经查过一遍,这次把话说完整。国际版(www.workbuddy.ai)没有签到活动,也没有 daily-checkin 这条路由:
POST /v2/billing/meter/checkin-activity-status返回 200,但active: false(字段在,活动没开);GET /v2/billing/meter/get-user-resource直接 404(国内版才有这个接口);/v2/billing/meter/checkin、/daily-checkin-activity、/checkin-activity全是 404。
国际版真正有的是另一套东西:成长任务板(5 条:完成一次对话、安装技能、绑定微信、召唤专家、使用模板)和 streak 连续打卡(7 天 / 14 天 / 28 天档位,分别给 0 / 50 / 150 积分)。但它的字段形状跟国内版不一样(没有 credit / energy / target / current),插件目前解析不了,所以没有为国际版开自动化——没有可领的东西,开了也只是每天准时去撞 404。
国内版的签到是正常的,多账号逐个领取的链路本来就有,不受本次改动影响。
v1.8.1 — 一个过期的登录状态,不再拖着整个池子陪跑
有人的卡片上,好几个账号下面顶着一大段红色英文,大意是「上游不认这个登录了,去桌面端重新登录」。话说得没错,可问题是:它只说了结论,没解决过程——那几个账号明明已经废了,每次请求还是要挨个撞一遍,撞得头破血流,旁边几个好端端的账号一次都轮不上。
事情是怎么变成这样的
上游吊销一个登录状态时,不会顺手把凭据文件里的过期时间改掉。于是本机留着一份「看起来还有效」的凭据:过期时间写着 11 月 4 日,今天才 10 月,一切正常。
插件就信了。它把这三个账号当成健康账号,按顺序一个一个试:
- 拿账号 A 去请求,上游回 401;
- 插件想「可能是 token 过期了」,于是刷新 token;
- 上游回答「这个 refresh token 早就没了」(
invalid_grant: Offline user session not found); - 插件刷新失败,但没有记仇,转头又拿 A 去试;
- 八次重试名额(
maxAttempts)就这么全喂给了同一个死账号,健康账号一次都没被碰过。
所以现象是「明明还有能用的账号,请求却一直失败」,外加卡片上一片红。
现在怎么处理
给账号加了第三种状态:判死(前两种是「正常」和「限流冷却」)。
- 只有上游亲口说死才算死。 判据是
invalid_grant/Offline user session not found这类明确答复。网络超时、DNS 抽风、5xx 一律不算——否则一次网络抖动就会把好账号关进小黑屋半小时,那比原来的 bug 还难查。 - 判死的账号立刻退出轮换,不再参与
acquire的选择,请求当场换健康账号继续,不用等到八次重试耗尽。 - 想复活有两条路:在 WorkBuddy 桌面端重新登录该账号(插件发现 refresh token 变了,自动放行),或者点卡片上的「清除所有冷却」手动再给它一次机会。
- 全池都死了的时候,返回的不再是含含糊糊的「你还没登录」,而是 401
session_dead:N/M WorkBuddy account(s) for this gateway were rejected by the upstream (their sign-in was revoked); sign in again in the WorkBuddy desktop app to restore them。「等待」和「重新登录」是两种完全不同的处置方式,不能混着报。
卡片上的红字也收拾了一下
- 原来那段整段英文报错直接铺在「积分包」面板里,把面板撑得老长,右边的「合计」都被挤出屏幕。现在折叠成一个红色小标「登录已被拒绝」,鼠标悬停才显示原文。
- 账号行上加了红色的「登录已被拒绝」标签,下面补一句「请在 WorkBuddy 桌面端重新登录该账号以恢复」——红色是故意的,用琥珀色会让人以为「等一会儿就好」,而这个状态等多久都不会好。
- 判死的账号不再参与卡片刷新时的积分 / 签到探测。以前每刷新一次卡片,就对每个死账号白打两个必定 401 的请求,刷新越勤越热闹。
已知边界
- 判死状态记在内存里,重启后重新判定。也就是说重启后的第一次请求还会再试一次那个账号,然后才把它请出去——这一次的代价是必要的,否则「重启一下就好了」的临时故障会被误判成永久失效。
- 国际版每日签到活动(
checkin-activity-status)实测当前是active: false,所以「多账号自动领 30 积分」目前没有可领的东西;国内版的活动是正常的,逐账号领取的链路本来就有。
v1.8.0 — 四种模式,终于排得像个样子了
上一版加了第四种模式「每对话固定」,功能是好的,排版是坏的。坏得很实在:格子还是按三个排的,第四个只好委屈地掉到下一行,占着半个身位,像个插班生。
问题出在哪
「账号使用方式」这一块本来写着 grid-template-columns: repeat(3, ...)——三列。模式从三个变成四个之后,没人去改这个数字。
于是:
- 第四张卡被挤到第二行,左边空出一大块,右边孤零零一张;
- 每张卡只剩大约 190 像素宽,中文说明被折成两三行,长短参差不齐,眼睛没法竖着扫;
- 更尴尬的是,测试全绿、构建全过——因为没有任何一个测试会去数 CSS 里的网格列数。这种错只能靠眼睛发现。
现在长什么样
- 两列排布,四张卡正好 2×2,方方正正;窄窗口下自动变成一列,每张卡独占一行,说明文字不再折行。
- 顶部多了一个「当前:每对话固定」的小徽章,不用在四张卡里自己找哪张亮着。
- 选中的那张卡明显抬高:底色提亮一层,左侧加一条竖色条,一眼就能认出来。
- 「推荐」不再塞进标题文字里(
每对话固定(推荐)这种写法会让标题长度不齐),改成一个独立的小绿标挂在卡片右上角。
配色上的一点坚持
这次参考了 dingminhua/dsh-connect-workbuddy 的界面,它的思路很清楚:给「当前项」一条竖色条 + 抬高的底色。这套手法我们借来了。
但颜色没借。它用的是宿主品牌蓝 #5686fe——那是 DSH 自己的颜色,不是这张卡片的。我们的卡片有自己的一套:底色用和上面区域切换按钮选中时同一个「抬升表面」色,竖色条用中性色,和宿主保持一致但不冒充宿主。
绿色只留给「推荐」徽章,因为卡片下方那个绿点已经在表示「账号健康」了,别让两种绿打架。
已知边界
- 这次只动了样式和排版,分发逻辑一行没改——四种模式的行为和 1.7.9 完全一样。
- 布局测试是读源码文本断言的(比如「网格必须是两列」「选中态不许出现 brand-primary」),能防住改错,但防不住视觉上「两列也不好看」。真正的判断还得靠眼睛。


