Skip to content

Releases: XDTrees/dsh-xdpool

v2.5.0 — 十张 issue 一起收

Choose a tag to compare

@XDTrees XDTrees released this 28 Oct 13:21

#68 到 #83 攒了十来张单子,这批一起收。其中三张是同一类毛病:代码把「没数据」和「数据是空」当成了同一件事。

最要紧的三条

症状 真实原因
一个失效账号吃掉整次请求的 8 次重试,最后报「API 密钥无效」 以「刷新拿到了新 token」判定账号仍可用。有一类账号 refresh 长期有效、chat 恒回 403,判死条件因此恒假
buddy 某一轮被中断后,之后每一轮都失败 中断留下的 assistant 消息只有 reasoning、没有正文,空字符串被原样发到 wire,国内网关 400/11151 且不在重试集合里
zcode 全部模型勾选后,每次重启又变回全禁用 用户全开时黑名单是空对象,与「从未选过」无法区分,启动补静音据此又把它们全关一遍

逐条

#68 失效账号耗尽重试预算 — 池车道以「刷新成功」当「账号可用」。改法:单次请求内只允许该账号换一次票(refreshedThisRequest),第二次仍 session_dead 就判死并轮到下一个。取「第二次」而不是把惩罚提到刷新之前,是为了仍给真正过期的 token 一次机会。压缩重试路径同步。

#70 侧栏入口开关 — 新增 sidebarRow 配置(默认开)。只控制左侧栏那一行:设置页的 XD Pool 卡片始终注册,模型池不受影响 —— 关掉入口不能把开关本身也锁在门外。

#72 订阅包余额虚高 30 倍 — fetchCredits 只把 CapacityType === 4 当月包,而 2 是周期订阅包(12 期 × 4000,CapacityRemain 是整期剩余、CycleCapacityRemain 才是当期剩余),落进「一次性」分支。三账号实测:官方 3,399.8 / 插件 102,396 → 修复后 3,366;另两个只有月包的账号(3,049 / 2,771)不变,所以长期没被发现。

#73 中断回合留下的空 assistant 消息 — serializeMessages2 出口加 isBlankWireMessage / dropBlankWireMessages,serializeMessages4 同步。三点取舍都用真实请求验证过:user 空消息不动;不伪造占位内容;有 tool_calls 的 assistant 消息绝不删(它是 tool 结果的配对锚点)。

#74 全开渠道每次重启被重新禁用 — setModelDisabled 打开模型时是删键而不是写 false,于是「用户全开」与「从未选择」在磁盘上是同一个空对象。新增持久标记 selectionTouched,四个用户入口都置位,启动补静音只在标记缺失时执行。影响面是「所有模型都打开」的渠道,目录越小越必然 —— 这解释了为什么只有 zcode 反复被报。

#75 保存模型选择后界面回滚 30 秒 — setDraftFor(region, undefined) 的语义是「退回池子最后报告的状态」,而那份状态靠 30 秒轮询刷新。改成先 refresh(region) 再清草稿(重读失败则保留草稿);settingScope.set 的返回值不再丢弃,revision 冲突时保留草稿、报「设置已在别处被修改」、不谎报成功。

#82 RPC 桥惰性解析 — 2.4.6 的 CHANGELOG 声称这条已修,但发布产物里 installPackRpc 仍只捕获一次。现已改为每次 callPack 前惰性重解析。

社区 PR

  • #81(@jmglsi) 第三条内容指纹:Relative paths resolve from the workspace root → are resolved against the workspace root,语义、长度、换行位置都不变,被网关字符串表收录的子串消失。
  • #84(@Refeain) CodeArts 输出上限:三条路径(listModels / resolveModel / wire)统一按 65536 封顶。实测 384000 → HTTP 200 + SSE 帧 InferHub.4001.200,65536 → 正常流式。

尚未定性

#69 CodeArts 提示密钥无效、#71 kimi k3.1 pro 没有响应 两张只有一句话。#69 至少有两条来源,其中 InferHub.4001.200 那条已随 #83 修掉;真正的 401/403 需要失败时的原始响应才能确认。#71 请补一下:是空回复、卡住不动、还是有报错文案。

验证

  • 全套 80 文件 / 1033 用例(串行 60.7s 全绿)
  • pack.js 每次改动后 node --check 通过
  • server tsc 0 错;client tsc 的 6 条是改前就有的(accounts.ts / context-budget.ts / free-upstream.ts 的 Node 类型不匹配)

v2.4.6 — 扫码账号每次打开都空白

Choose a tag to compare

@XDTrees XDTrees released this 23 Oct 12:05

2.4.4/2.4.5 之后侧栏能进、能滚了,但扫码过的渠道在账号池里经常整片空白,重开也一样。磁盘上账号是齐的(state.json 里 15 条),所以问题全在读取链。

16 秒的事实抓取把首屏挡掉了

load() 里先 await fetchChannelModels(...) 再 setEntries(...)。那次采样测到 16.5 秒——它要把每个模型逐个交给宿主适配器解析上下文窗口,冷启动时很慢。卡片区在这十几秒里是空的,看起来就像「没有账号」。

改成两阶段:provider.status 与 account.list 先回来(几十毫秒),卡片立刻渲染;事实(窗口/识图)随后合并进去。

失败被渲染成了「空」

provider.status 是「哪些渠道有账号」的唯一判据。它一旦整段返回 401,scanProviders 就是空的,entries 也是空的,组件走到 if (entries.length === 0 && error === undefined) return <></> —— 静默渲染空白,一个字都不说。读者无法区分「还没扫码」和「读取失败」。

现在保留原始信封判 ok:真的读失败就把错误显示出来。

RPC 桥只捕获一次

callPack 必须走 ctx.connection.rpc(裸 fetch('/api/channel-pack') 被宿主 401,因为 /api/* 只允许经 admission 的客户端)。但 installPackRpc(ctx) 在 apply() 里只跑一次,而提供 connection 的是另一个 fiber——那一刻没就绪,桥就永久为空,此后所有 callPack 都回落裸 fetch 吃 401,正好触发上一条的静默空白。

改为保存 ctx、每次调用前惰性解析,就绪后自动恢复。每一步仍各自 try:宿主给客户端 ctx 套的 Proxy 对未声明服务的属性访问会抛异常(并上报运行失败),?. 挡不住抛异常的 getter,只有 ctx.get() 豁免。

其他

  • 全套 79 文件 1023 用例。

v2.4.5 — 侧栏面板能滚动了

Choose a tag to compare

@XDTrees XDTrees released this 23 Oct 10:58

2.4.4 把账号池搬进了侧栏那行,但页面在折叠处被切掉、翻不到下半页。原因不在功能,在容器:@deepseek-ai/dsh-client-ui-layout 把中央列样式成 display:flex; flex-direction:column; overflow:hidden,设置对话框自己带滚动容器,所以我原样搬过去的组件在主区域只能被裁掉。

现在页面外面包了一层滚动容器(height:100%; min-height:0; overflow:auto),并补上设置对话框原本提供的内边距——否则卡片会贴着窗口边缘。设置页那条路径不受影响:那里宿主已经在滚,再加一层会双份。

侧栏那行标签保持 XD Pool,中英文都不译。

npm i dsh-xdpool@2.4.5

装完重启 DSH。

v2.4.4 — 扫码凭据不再输给旧快照、渠道包按身份去重、侧栏入口

Choose a tag to compare

@XDTrees XDTrees released this 23 Oct 10:15

扫了码,账号池还是说「登录已被拒绝」

你看到的:某两个账号卡片一直显示「登录已被拒绝 / 上游已不再接受这个登录状态」,提示你去 WorkBuddy 桌面端重新登录。可你的登录途径是这个插件的扫码登录,而且刚刚扫过——扫完还是那句。

真实原因(两处,都在本插件自己的代码里,不在上游):

① 新鲜度排序把扫码凭据排在最后。 同一个账号可能同时存在两份凭据:桌面 App 留在 auth 目录里的快照文件,和你扫码后由本插件铸造的凭据。compareFreshness 原先只认三档——桌面 live 文件、lastRefreshAtMs、expiresAtMs。扫码凭据的 sourcePath 是 channel-pack:<provider>:<ref>,既不是 live 文件名、也永远没有 lastRefreshAtMs,于是无条件输给任何残留的旧快照。它输一次,凭据就不被替换;不替换,就走不到「清除死亡标记」那一行——卡片上那句提示于是永远不会消失,反复扫码也没反应。

现在改为按来源分三级:桌面 live 文件 > 本插件扫码凭据 > 其它残留快照,同级才继续比签发时间和有效期。

② 渠道包的去重按昵称分组,会删掉刚扫进来的凭据。 本插件对渠道包做过本地补丁:同渠道同昵称视为重复条目,只保留一条。问题有两个——重扫时新凭据遇到未过期的旧条目会被当作重复删掉(连凭据一起删);而同一个人的两个账号如果昵称相同,也会被当成同一个人删掉一个。

现在改用凭据里的上游身份 user_id 判重,并且:认不出身份的一律保留(宁可留一条重复,也不能删掉别人的账号);登录半成品(昵称还是生成 id、没有有效期)在比较中让位给已完成登录的条目;都可用时按 createdAt 留新的。

左侧栏可以直接进账号池了

账号池此前只能从「设置 → XD Pool」进。现在侧栏底部(设置按钮附近)多了一个图标,点开就是同一个账号池卡片,Esc 或点面板外关闭。

按钮与面板是两个互不相邻的宿主插槽(sidebar.footer.action 与 shell.overlay),没有共同祖先,因此开合状态放在模块级而不是组件状态里;面板直接渲染同一个 PoolCard,不复制一份界面。宿主只给侧栏注册者 { wide },没有「打开设置」的接口——这也是同类插件都用自己的浮层而不是跳转的原因。老宿主缺少 shell.overlay 时只损失这个入口,不影响设置页。

测试

账号发现新增 4 例:扫码凭据胜过旧快照、桌面 live 文件仍胜过扫码凭据、凭据被替换后死亡标记清除,以及钉住「同昵称的两个人不会被合并」。全套 79 文件 1023 用例。


完整更新说明:CHANGELOG.md

v2.4.3 — 渠道包补上 11128、粘性绑定跨重启、网关白名单、目录 TTL,外加 CodeArts 重签开关

Choose a tag to compare

@XDTrees XDTrees released this 22 Oct 08:18

2.4.1 与 2.4.2 修的是池车道(src/upstream.ts);这一版把同一套 11128 规则补进渠道车道自己那份 channel pack,并顺带解决三件独立的事:会话粘性绑定不该止于进程边界、网关页面的勾选该是真开关、远端模型目录该会过期。四个 PR 来自社区(@GIGIGIGIbaby),复核与发布由维护者完成。另附一个默认关闭的 CodeArts 重签开关(见末节)。

渠道账号也吃 400 / 11128(PR #63)

workbuddy 渠道在 channels.ts 的 POOL_OWNED_CHANNELS 列表里,但它不走 src/upstream.ts —— 它由 channel pack 自己的 WorkBuddy 适配器应答。2.4.1 的两道内容指纹、2.4.2 的 role 转换与三个会话身份头都只加在了池车道,于是渠道账号继续吃到 2.4.2 已经替别人修好的那个 400 / 11128:

项 渠道适配器改前 改后
首条消息 role developer system(就地转换)
powered by DeepSeek Harness 原样发出 running on DeepSeek Harness
DeepSeek Harness Web GUI 原样发出 DeepSeek Harness web interface
X-Conversation-ID / -Request-ID / -Message-ID 一个都不带 三个都带

三处编辑各自标了 [xdpool-local],配套的 tests/chat-identity-headers.test.ts 直接对已发布的 bundle 做源断言(pack.js 是外部构建产物、无法 import,所以只能在字节层 pin 住)—— 下次 pack 刷新若把这三处冲掉,测试会变红,而不是静默回退。

会话粘性绑定跨过进程边界(PR #64)

卡片承诺「一个会话一个账号」,但这个承诺原先止于进程边界:绑定存在一个 Map 里,每次重启都重新挑选,于是同一个会话一天里可能被三个账号各服务一遍,每次都按完整 prompt 计费。现在这张表写进插件状态目录(只存哈希到账号 id,不含任何消息内容),构造时加载,写入合并成一次原子 rename,并沿用与内存版相同的 LRU 上限。

测试特意把会话绑到一个「默认挑选不会选中」的账号 —— 新建的空池本来就会同意未绑定会话的选择,所以「两个池答案一致」在禁用加载时也能通过;实测把加载去掉会让其中三个用例变红。

网关页面的勾选不再是纯展示(PR #65)

Gateway 页的模型勾选框与 LAN 共享列表此前只是展示过滤器:/v1/models 会隐藏没勾的模型,但请求路径照单全收。一台还留着旧模型 id 的手机、或照文档猜一个 id 的第二编辑器,就能继续拿卡片上写着「关」的模型花用户的钱。

现在强制发生在调用方实际发出的那个 id 上,且在任何车道剥前缀或改写 body 之前,覆盖每条车道:pool、aggregate、free、combo,以及登录渠道。列表与闸门读同一份集合——这正是让卡片上的勾选有意义的原因。

远端模型目录会过期(PR #66)

bundle 里每个适配器都把 remoteModels 缓存到进程结束,于是厂商改名或调整倍率要等重启才反映到卡片上——而改名通常就是改价,因为倍率是名字的一部分。现在由一道 TTL 闸门决定缓存能否应答,时长十分钟,可用 DSH_XDPOOL_CATALOG_TTL_MS 覆盖以便测试。

闸门刻意不清缓存:加载器只在成功时覆盖,所以刷新失败会继续沿用最后一份已知良好的目录,而不是退化成一堆裸 id;网关侧 30s 的冷却又限住了重试速率。同样无需重建 lib —— channels.ts 在运行期按路径加载 pack.js。

可选:CodeArts 请求带重签头(默认关闭,[xdpool-local])

在 CodeArts 的 chat 请求上追加两个头:

x-bili-resign: sdk-hmac-sha256
x-bili-resign-credential: base64({"ak":"…","sk":"…","token":"…"})

用途是让 billion-context 用这对凭证重签一条它接管的 SDK-HMAC-SHA256 请求:该方案的签名覆盖请求体,压缩改写会让签名失效,而 bili 拿不到重签凭证时会按 compress-or-refuse 契约本地拒发(bili_resign_unavailable,表现为 403,再被误读成「密钥无效」)。带上这对头,bili 就能重算签名,压缩能力得以保留(billion-context 的 #1884 逃生口)。

为什么默认关闭:billion-context 只在 optionalDependencies 里,未安装时这两个头会直达上游,而它们携带的是 AK/SK;另外 bili 的进程内 native client 自己会构造这两个头(resignExtra),xdpool 加不加都不影响它。开关留给「xdpool 直接对接 bili 代理路径」或将来需要显式携带凭证的场合 —— 启用方式是在 pool 构造时传 codeartsResignHeaders: true(当前没有对应的用户级配置项)。

配套 tests/codearts-resign-headers.test.ts 用源断言 pin 住四件事:两个头名齐全、字段名与 bili decodeApigCredential 读取的 ak/sk/token 一致、插入位置在 signRequestHuawei 之后、以及默认关闭。

其他

  • 新增测试:渠道包 11128 规则源断言、粘性绑定跨重启(含「去掉加载即变红」的对照)、网关白名单与列表一致性、目录 TTL。

v2.4.2 — 第二道内容指纹:Web GUI 那句,以及 assistant 历史

Choose a tag to compare

@XDTrees XDTrees released this 21 Oct 11:47

v2.4.2 — 第二道内容指纹:Web GUI 那句,以及 assistant 历史

2.4.1 之后仍有用户撞上 400 / 11128 Illegal API invocation from an unapproved channel。这一版补上第二道内容指纹,并把改写范围扩到 assistant 历史。

这次修复来自社区 PR,谢谢 @jmglsi —— PR #62 自己把成因、边界和最小改法都查清写明了,还带了覆盖「被扫角色」的测试;维护者这边独立用真实网关复核后合并发布。同时也谢谢 @GIGIGIGIbaby 的两个修复(PR #49、PR #50)。

同一条提示词里还有第二道指纹

2.4.1 只改写了 You are an AI agent powered by DeepSeek Harness 那一句。改掉之后请求仍然 400 / 11128,因为网关还在看提示词的另一段——介绍 Web GUI 的那一对句子:

You are interacting with the user through the DeepSeek Harness Web GUI
whose URL the runtime context names. When the user refers to "this page",
"this GUI", or "this app" without naming another target, they mean this GUI.

这两句要合起来才算:单独发任一句都 200,两句凑齐就 400,在这对句子里做任何小改动都放行。所以这是第二道内容指纹,不是请求长度限制,也不是别的头。

修法是把产品名后的 Web GUI 改称 web interface——保住原意的最小改动。

被扫的是 system 和 assistant,user/tool 不扫

同一句话放进 assistant 消息同样 400,改写后 200;放进 user 或 tool 则从来不管。这一条很要紧:只要历史里有一条助手回复带着这段文字,此后这个会话的每一次请求都会失败。所以改写范围从「只改 system」扩到 system + assistant。

实测(真实国内网关)

请求体 修复前 修复后
主 agent 提示词(含 Web GUI 那对句子) 400 / 11128 200
同一对句子放在 assistant 历史里 400 / 11128 200
两句合一的提示词(两道指纹同时在) 400 / 11128 200
埋进数百字长提示词中间 400 / 11128 200
只有无关填充的长提示词(基线) 200 200

复核时还确认了一件事:DSH 当前版本真正发出的那几句(The runtime context names the DeepSeek Harness implementation checkout…、… the DeepSeek Harness Web GUI at <url>.、Current DeepSeek Harness Web GUI URL: …)原样发送就是 200,含产品名但不是模板命中的句式。也就是说命中的是句式,不是「出现产品名」——这也是为什么没有采用「把 DeepSeek Harness 全局换成 DSH」那种更宽的改法:它会连 D:\DeepSeek Harness\resources\app\dsh\ 这类要求模型逐字使用的路径一起改掉,却换不来任何额外放行。

同时并入的两个社区修复

  • PR #49(@GIGIGIGIbaby):模型连通性探测的 content 改用内容块数组。宿主按 content.some(...) 校验每条出站消息,裸字符串没有 .some,探测在进程内就炸了,卡片上显示成 content.some is not a function——一个形状错误伪装成模型坏了。
  • PR #50(@GIGIGIGIbaby):禁用按钮划过时会「消失」。.dsm-btn:disabled:hover 里的 background:transparent 优先级 (0,3,0) 压过 .dsm-btn-primary 的填充 (0,1,0);主按钮文字是深色、为浅底设计的,底一变透明文字就看不见了。现在这条规则只保留 transform。

升级

pnpm add dsh-xdpool@latest

装完重启 DSH 即可。完整改动见 CHANGELOG。

v2.4.1 — 11128 的真正成因:system 内容指纹

Choose a tag to compare

@XDTrees XDTrees released this 21 Oct 04:16

这一版修的是「为什么还是 11128」

2.4.0 把 400 / 11128 Illegal API invocation from an unapproved channel 归因于缺少会话身份头。这个归因是错的——实测证明国内网关根本不检查那三个头,不带它们照样放行。

真正的触发条件是 system 消息里的自报句:网关按内容指纹判断「这是不是官方客户端发出来的」,命中串是 You are an AI agent powered by DeepSeek Harness。这句话由宿主的第一方插件 @deepseek-ai/dsh-system-prompt 固定注入,任何未改装的安装都会 100% 命中,与账号、密钥、渠道、UA 全部无关。这也解释了为什么它的表现是「第一个账号失败后不再重试」——每个账号都会失败,重试没有意义。

修法是在发送前把这句话改写成网关放行的同义句(powered by → running on),只动命中的文本块,图片与缓存块的引用原样透传;没命中时返回同一个引用,不为「什么都没改」付深拷贝代价。改写清单做成了数组,将来网关加句子只是多一行。

实测(真实国内网关,同一账号三组对照)

请求体 结果
经本插件改写 429 / 6004(通过渠道校验,仅受频率限制)
原样含该句(对照) 400 / 11128
system 干净(基线) 429 / 6004

另外两条只在特定网关成立的规则

11128 是多路复用的错误码——同一个码在不同网关指向完全不同的成因,所以这次也一并处理了另外两个。

国际网关要求首条消息是 system

www.workbuddy.ai 对一个以 user 开头的请求体回 400 / 11128 "first message is not system prompt",而同一个请求体在国内网关是被接受的。现在国际版请求若首条不是 system,会在最前面补一条占位 system,你自己的消息位置和措辞一个字符都不动。

思考档位「关闭」会被两区同时拒绝

适配层把 thinkingLevelMap.off 钉成字面量 'off',而 pi-ai 对「没有显式指定档位」的请求发的就是它——所以选「默认」和选「关闭」产生了同一个请求体。两区都不接受这个值:国际版 GPT 族回 400 / 11133(extError.param === 'reasoning.effort'),国内版在声明了 onlyReasoning 的模型上报错。现在发送前把它删掉,让上游走自己的默认档。

省略是唯一在各个区、各个模型上都实测通过的写法;换成字面量 'none' 不是安全替代(GPT-5.6 族接受,别的模型拒绝)。

实测(真实双网关,同一账号前后对照)

请求 修复前 修复后
国际版,首条为 user 400 / 11128 first message is not system prompt 200
国际版,reasoning_effort: "off" 400 / 11133 extError.param=reasoning.effort 200

免费模型限流提示说清了配额口径

免费车道的拒绝原文是「the provider asked to wait 74558s」——二十小时,读起来像整个免费车道被封了。实际配额按上游会话计,换一个会话(或换一个模型)就另有一份额度。现在限流错误的文案补上了这一句;只有限流类错误带这句,凭据失效、模型不存在之类加上去只会引导用户去做无用重试。


升级

dsh plugin add dsh-xdpool@2.4.1

升级后请重启 DSH 让新版本生效。

测试

全套 70 文件 / 931 用例。本版新增:

  • system 内容指纹改写 8 例
  • 区域专属请求体规则 12 例(含反向验证:把两条规则去掉会有 5 个用例转红)
  • 免费车道会话配额提示 1 例

v2.4.0 — 会话身份头、模型清单解冻、中文路由可存

Choose a tag to compare

@XDTrees XDTrees released this 21 Oct 03:10

这一版收的是「用着用着就报错」的那一类问题:调用被网关判成非法渠道、国际版模型清单停在开机那一刻、中文名字的路由点了保存却没存下、上下文一选就弹回第一项。都不新增界面,全是把已经该work的事情修回来。

先看这一版修掉了什么

九个问题,按你遇到它的顺序排:

你看到的 真实原因
聊天突然报 400 / 11128 Illegal API invocation from an unapproved channel,看着像密钥失效 网关要一组「这一轮对话属于谁」的身份头,缺一个就整轮拒绝。不是密钥的问题
国际版的模型清单永远停在启动那一刻,「重新获取模型」点了没反应 模型数组在创建时被快照,刷新时复用了同一个对象,宿主按对象身份记忆化,新清单从没送出去
中文名字的路由,点了保存,列表里没有 链名 slug 只认 a-z0-9,中文名被压成空 id,规范化时当作无效条目静默丢弃
上下文大小选了「完整」,选中态立刻弹回第一项 「从未保存过预算」被当成「已收窄到 200K」显示,清空后该行又算回 200K
国际版 Deepseek-V4.1-Flash 只能选 High 一档 兜底表把这批模型的思考档位写成了单档
同一个人,账号池里显示成两个,各带一部分模型 桌面凭据带 uin+uid、扫码渠道只带 uid,池 id 按 uin 优先哈希
「重新检测账号」永远没结果,扫码账号也进不了池 listAllAccounts 漏了 await,拿到 Promise 后 .filter 抛错并被 catch 吞掉

会话身份头:11128 的真身

网关对「这一轮对话属于谁」有一组身份头,缺一个就整轮拒绝,报 11128 Illegal API invocation from an unapproved channel。这个文案会让人以为密钥失效,其实是新会话没自报身份。按官方客户端的契约补齐:

  • 会话稳定三头(同一会话内不变):X-Conversation-ID、X-Conversation-Request-ID、acp-connection-id
  • 每请求两头(逐次随机):X-Conversation-Message-ID、X-Request-ID
  • 另附 X-Agent-Intent、X-Agent-Purpose、x-codebuddy-request、X-Private-Data、X-Product 与 W3C/B3 追踪族

实测口径:三个稳定头缺任一个即 400 11128,齐全即 200;值本身不校验(非 UUID 字符串也过)。池车道(WorkBuddy 国内/国际)与扫码渠道两条路径都接上了。

顺带修掉同源的一处:role: "developer" 的消息在国内网关同样触发 11128,现统一上送为 system。

国际版模型清单不再冻结

createProvider 的 models 是创建 provider 时快照下来的数组,而原来的 invalidate() 只重排了 profile、复用了同一个 provider 对象。宿主按对象身份记忆化 current(),于是新拿到的清单永远送不到选择器。

现在每次 invalidate() 重建 provider 与 profile 两个对象,并通过 adapter handle 的 replace() 重新登记路由,由宿主自己发出 llm/adapters-updated。

顺便核查了一处怀疑:国际版本来就有独立的 modelSelectionGlobal 键,与国内白名单分开读写,这一处无需改动。

中文路由名现在存得下

链名 slug 原来是 [^a-z0-9]+ → '-',中文名整串被压成空 id。改为 Unicode 感知的 slug(\p{L} / \p{N}),拉丁名形态完全不变(comboId('Write Then Fallback') 仍是 combo:write-then-fallback),已存的路由 id 继续可解析。

上下文大小:选中态不再一选就弹回

渠道卡把「从未保存过预算」当成了「已收窄到 200K」,用户点「完整」清空预算后,该行又算回 200K,选中态立刻跳回第一项。

现在未保存过预算就显示完整窗口;清空预算时同时保留 nativeContextWindow,radio 组不会在指针底下消失。

思考档位:不再只认一档

pack 兜底表把十几个模型的 reasoningEfforts 写成了单档(["medium"] / ["high"]),于是国际版 Deepseek-V4.1-Flash 只能选 High。

逐模型实测(两个网关、五档全跑)后按真实能力补齐:凡是能调通的模型,五档都接受,且各档 reasoning_content 长度不同。两个例外如实留在表里:

  • gpt-5.3-codex 五档皆 200,但 reasoning_content 恒为空——档位合法而不可观测,已在表内注明
  • gemini-3.5-flash 与 primary-model 五档全 400,model [X] is not allowed for user type [personal]——这是账号套餐权限,与档位无关,未动

同一个人不再显示成两个

桌面端凭据同时带数字 uin 与 UUID uid,扫码渠道只带 uid,而账号 id 是按 uin 优先哈希的。同一个人走两条登录路径就整出两个池条目,各自带一部分模型——这正是长期反馈的「重复账号」。

现在按 uid 建次级索引合并到同一个池条目。刻意不改哈希:冷却、失效标记、忽略名单、持久化快照全部按旧 id 归档,改了一次性作废。也与凭据到达的先后顺序无关。

「重新检测账号」不再静默失败

listAllAccounts 漏了 await,拿到的是一个 Promise,随后的 .filter 抛 is not a function 并被外层 try/catch 吞掉。表现是扫码账号全部并不进池、重新检测永远没结果。

其余

  • 测试:新增会话身份头与中文路由名的回归用例,全套 68 文件 / 910 用例通过。
  • 版本:2.3.0 → 2.4.0(POOL_VERSION 同步,有测试守着它不许漂移)。

升级

dsh plugin add dsh-xdpool@2.4.0

装完重启一次 DSH:身份头与模型清单的修复都在后端侧,热重载拿不到。


完整改动:2def4d0...933a5c3 · 逐条说明:CHANGELOG.md

v2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数

Choose a tag to compare

@XDTrees XDTrees released this 17 Oct 14:37

这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 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 已知限制描述更正

Choose a tag to compare

@XDTrees XDTrees released this 16 Oct 04:21

2.2.1 — README 已知限制描述更正

纯文档修正:README「已知限制」里写着池「没法替你发起登录或扫码」——这是改名前的旧描述。现在的 XD Pool 有扫码登录页,可以为自己十三家渠道发起扫码登录;WorkBuddy 本体的 token 仍由桌面 App 的腾讯 SSO 铸造并绑定设备,这条限制保留但改成了准确的说法。代码无变化,发布这一版只为让 npm 页面的 README 跟上。