DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wzs39 /

wzs39/dsh-workbuddy-hardened

Verified

dsh-connect-workbuddy 5.13.1 加固分叉:版本同步、身份串中和、封号不连锁、6004 单模型限流同账号换模型重试。四个补丁可逐行审计,47 项行为验证

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: master@05de565e

dsh-workbuddy-hardened

一个 分叉加固版 的 dsh-connect-workbuddy。

不是重写,不是包装:vendor/ 里是上游 dsh-connect-workbuddy@5.13.1 的原样副本,lib/ 由 build.mjs 从 vendor/ 复制后应用四个补丁生成。所以「和上游的差异」就是 patches/ 里那四个文件,可以用 node build.mjs --diff 逐行审计。

为什么做这个

起因是「账号绑定无效 / API 密钥无效」这个症状。实测下来账号没坏:

  • node lib/bin.js status --json → signed-in,token 未过期、能解密、上游接受,CN 与 global 都还有余额。
  • 真实报错是三类:CN 区可用;global 区 11128 channel_reject(first message is not system prompt);以及 11140 account_restricted(封号态)。

API 密钥无效 也是宿主的一句固定文案(message.failure.auth),任何鉴权类失败都显示它,与密钥本身无关。

上游 5.13.1 已经修了 11128(ensureSystemHead + neutralizeChannelFingerprints),但有三个洞没堵:

  1. 身份字符串过期:插件把 UA 写死成 WorkBuddy/5.8.3 ... CLI/2.161.0,而应用已经是 5.9.3、CLI 是 2.165.0。每次读配置、计费、发消息都带着过期的身份。
  2. 封号会连锁:isFailoverWorthy() 没排除 account_restricted。一个号被封,账号池立刻换下一个号、重放同样的请求与同样的指纹,把剩下的号一个个烧掉。
  3. 单个模型的限流会被显示成欠费:业务码 6004 被归成 soft_rate → quota_exceeded → 宿主固定渲染「当前请求的额度已用尽」,而它的真实含义是「该模型用得太多,你可以切换其他模型继续用」。

四个补丁

01 · version-sync

host-heartbeat-BtMcdOIa.js 的 PRODUCT_UA 改成运行时解析:

  • 应用版本 ← %LOCALAPPDATA%\Programs\WorkBuddy\resources\install-manifest.json 的 appVersion
  • CLI 版本 ← ...\resources\app.asar.unpacked\cli\package.json 的 version(包名 @genie/agent-cli)

解析失败则回退到原常量,行为不变。APP_VERSION 从 PRODUCT_UA 正则派生,所以 X-IDE-Version 跟着一起正。

实测解析结果:WorkBuddy/5.9.3 WorkBuddy/5.9.3 CLI/2.165.0。

作用域要说清:PRODUCT_UA 是产品配置通道(/v3/config、计费)的 UA,不是 chat 的 UA。chat 走的是另一个常量 CLIENT_UA,见下一节。这个补丁影响的是配置/计费路径,以及 usageIdentity 打开后的 X-IDE-Version。

02 · identity-sanitizer

IDENTITY_CLAUSE 只认两种句式,都以 you are ... 开头。DSH 运行上下文里产品名是另一种句式:

The runtime context names the DeepSeek Harness implementation checkout.
Current DeepSeek Harness implementation checkout: ...
Current DeepSeek Harness Web GUI URL: ...

补丁把这三类 span 加进同一个正则,走上游同一条 breakIdentityClauses() 路径——在每个词边界插一个不可见断点(U+200B)。分词器读到的词与顺序完全不变,只是网关能匹配的那个连续子串被打断了。

范围克制:只加这三种 span,没有去打断每一处 DeepSeek Harness。改得越多、不可见字符越多,本身就越像异常。

诚实标注:you are ... 那两种句式的 11128 修复是上游用真实 DSH prompt 量过的,成立。这三种是同类字符串的预防性加固,不是已验证的修复。

03 · account-quarantine

isFailoverWorthy() 的排除列表加上 account_restricted。

结果:被封的号直接失败,不再自动换号。三个调用点(chat 重试、failoverShortfall、流式路径)都读这一个判定,所以一处改动全线生效。

保留的换号行为:policy_reject(内容拒绝,可能本就因号而异)、hard_credit、soft_rate、session_dead、not_found、server。

04 · model-detour

上游把业务码 6004 归类成 soft_rate,再经 KIND_HOST_CODE = { soft_rate: "quota_exceeded" } 翻成宿主的 QUOTA,宿主于是丢掉上游原话、固定显示「当前请求的额度已用尽」。而 6004 的真实含义是:

您的使用量已超出频率限制,将在 2026-10-30 18:46:19 UTC+8 重置,您也可以切换其他模型继续使用。

上限来自「账号×模型」而不是额度。实测同一个账号、同一秒:deepseek-v4.1-flash 429,而 hy3 / glm-5.3 / glm-5.2 / kimi-k2.7 / minimax-m3 / deepseek-v4-pro / deepseek-v3.2 全部 200。同一账号另一个号余额 1494 credits 仍在,6004 与钱无关。

所以补丁 04 做的是在同一个账号内换模型重试,不碰账号池:

  1. 只在 kind === "soft_rate" 且未被 abort 时进入。
  2. 候选来自 catalog.current()(本 region 真实模型表),跳过自己,最多 MODEL_DETOUR_LIMIT = 3 级。
  3. 原请求带图时,只留 nativeModalityOf(id) === "multimodal" 的目标。
  4. 改 body 用 JSON 往返替换 model,同时删掉 reasoning_effort 与 reasoning(新模型不一定接受原档位;这是上游 rewriteSessionTitleModel 已有的先例)。
  5. 阶梯上一级如果是非窗口类失败,立即停——继续换只会把真正的原因掩盖掉。
  6. 全阶梯失败则恢复原始 6004 结果,走原路径,行为与上游一致。
  7. 成功时在答案头部说明改用了哪个模型,措辞明确写出「6004、与积分/额度无关」,避免用户把限流误读成欠费。

插入点在任何账号轮换之前,所以成功时账号池完全不动,也不会给无辜账号写假的 rate-limited 测量值。

真实上游跑通的证据(tools/detour-live.mjs,用构建产物里的 classifyUpstreamError 与四个 detour 助手,非复述):

(账号 A)   deepseek-v4.1-flash → 429  kind=soft_rate
  classifyUpstreamError(429, <live body>) → "soft_rate"   → gate ENTERED
  ladder for a text request: hy3 > glm-5v-turbo > glm-5.3
  hy3 → 200  kind=(sse 200)  1138ms
RESULT: detoured deepseek-v4.1-flash → hy3 on the SAME account, real 200 SSE.

image request:
  bodyDeclaresImage → true    ladder: glm-5v-turbo > minimax-m3 > kimi-k3-1
  every rung multimodal: true    glm-5v-turbo → 200  kind=(sse 200)  2052ms

边界要说清:这是绕过单模型窗口,不是绕过限流本身。三个模型都进窗口时(多模型高频使用后会出现)仍然失败,此时唯一的办法是等重置或换账号。

附:两条身份通道的真相(为什么没有改 chat 的 UA)

分叉时我原本打算再加一个补丁,把 chat 的 User-Agent 也改成真实 app 的形状。查完证据后没有做,理由记录在这里,免得后人重走一遍。

从 app.asar 里挖出真实 app 的网关头部构造器:

t[`X-IDE-Type`]=`WorkBuddy`, t[`X-IDE-Name`]=`WorkBuddy`, t[`X-IDE-Version`]=n,
t[`X-Private-Data`]=`true`,
t[`User-Agent`]=[`WorkBuddy/${n}`,`WorkBuddy/${n}`,...r?[`CLI/${r}`]:[]].join(` `)

其中 n 是应用版本、r 是 cli/package.json 的版本。而插件 chat 实际发的是 User-Agent: CLI/2.63.2 CodeBuddy/2.63.2,形状完全不同。

看起来像 bug,其实是上游刻意的设计,源码注释写得很明确:

  • CLIENT_UA 注释:「chat 与配置是两个契约:会话身份三元组绑定在这个通道上,上游按 CLI agent 计费和限流。插件故意以 CLI agent 身份聊天」。
  • ideHeaders 注释:「X-IDE-Type 保持 CLI 是有意的。同时声称桌面 app 的 type 会比这个修复需要的身份声明更大」——而且这是 issue #132 报告者实测过的(加上 X-IDE-Name 只改变用量页的「使用端」列,HTTP 200、模型不变)。

所以「改成 app 身份」不是修 bug,是拿一个有实测依据的行为去换一个没验证过的猜测,还可能撞上按 CLI 通道做的计费/限流规则。不做。

真正值得做的是把已有的开关打开:usageIdentity。它默认关(z.boolean().default(false)),打开后补 X-IDE-* 与 X-Request-ID、会话 id 换成 32 位 UUIDv7。

实测(stub fetch 抓头 + 真实 chat):

off on
X-IDE-Type 缺 CLI
X-IDE-Name 缺 WorkBuddy
X-IDE-Version 缺 5.9.3(补丁 01 生效的证据)
X-Request-ID 缺 32 位 hex
会话 id 36 位 UUIDv4 32 位 UUIDv7
User-Agent 不变 不变
请求正文 不变 不变
cn 真实 chat 200 200,真实 SSE

意义:关着的时候,这个账号在上游用量页上「使用端」一列是空白——一个官方客户端会显示 WorkBuddy 的账号,却有一批请求显示为未识别客户端。空白本身就是个不一致信号。打开后至少是自洽的。

代价为零、可随时关、不影响 chat 契约。所以在 profile 配置里打开了(不是补丁):

usageIdentity: true

安装

先把本仓库放到你放插件的地方,然后让 profile 的 package.json 用 file: 指向它:

"dsh-connect-workbuddy": "file:../../plugins/dsh-workbuddy-hardened"
cd <your-dsh-home>\profiles\desktop
pnpm install --no-frozen-lockfile

包名刻意保持不变(dsh-connect-workbuddy)。lib/*.js 里硬编码了这个名字 159 次(settings key、logger 前缀等),改名会让已有配置失联。cordis.patch.yml 里的 id 也保持 dsh-connect-workbuddy,所以 profile cordis.patch.yml 里已有的 accounts / regions 配置块自动继承,不用重新配账号。

重启 dsh 生效。

重建

cd <this-repo>
node build.mjs          # 生成 lib/
node build.mjs --diff    # 生成 + 打印每个补丁的增删行
node verify.mjs          # 行为验证,47 项

需要重建的时机:上游发新版(先替换 vendor/)、改了补丁、或应用更新导致 install-manifest.json / cli/package.json 的形状变化(版本号本身是运行时读的,不需要重建)。

lib/ 是生成物,不要手改——下次 build.mjs 会覆盖。

重建后必须重跑 pnpm install:本 profile 用 nodeLinker: hoisted,file: 依赖是被拷贝进 node_modules 的(不是软链,Get-Item -Force 看 LinkType 为空)。所以改了 lib/ 不重装,跑的还是旧副本。

回滚

# 1. 依赖改回注册表版本
cd <your-dsh-home>\profiles\desktop
# 把 package.json 里改成 "dsh-connect-workbuddy": "5.13.1"
pnpm install --no-frozen-lockfile

或者只回滚单个补丁:把 patches/ 里对应文件移走,重新 node build.mjs。

已验证 / 未验证

node verify.mjs 覆盖 47 项,全部通过:

  • 版本解析真的读到 5.9.3 / 2.165.0,chunk 能干净 import(108 个导出)
  • 目标 span 确实被打断(17 个 U+200B);无关文本逐字节不变;去掉 U+200B 能完整还原原文;二次处理不叠加;上游 you are ... 句式仍被中和
  • account_restricted 不再换号;其余 kind 行为与上游一致
  • 补丁 04 的四个助手从产物里提取后真跑:受限模型不会成为自己的替代品、阶梯封顶 3 级、带图请求每一级都是 multimodal、正文里只是引用 "image_url" 字面量不被误判成多模态、withModelId 同时删两种 reasoning 拼写并保留其余字段、非 JSON 返回 undefined、通知文案点名两个模型且不含换号措辞;位置断言 detour 在账号轮换之前、原始失败在账号循环读它之前已恢复

没有验证的:这套加固能否真的降低封号概率。封号判定在上游,不可观测。这里能做的是把「已知的、可测的」几个放大器(过期身份、换号连锁、单模型 6004 就直接失败)去掉,而不是承诺不被封。补丁 02 的三类 span 也仍是预防性加固,不是用真实 prompt 量过的修复。

复现补丁 04 的实测

tools/detour-live.mjs 用构建产物里的分类器与 detour 助手,对你自己的账号打一次真实请求,看 6004 是否真被跨过去:

node tools/detour-live.mjs
# 或指定 vault / 只测某个账号
node tools/detour-live.mjs --vault <dir> --account <accountId>

它需要你的 vault(插件自己写的 <dsh-home>/workbuddy-vault),只会发「hi」这种一词请求。如果当前没有账号处在 6004 窗口里,它会直接告诉你「nothing to detour from」并退出 0——那是成功状态,不是失败。

上游

vendor/ 来自 dsh-connect-workbuddy@5.13.1(作者 dingminhua,MIT,https://github.com/dingminhua/dsh-connect-workbuddy)。LICENSE 与 THIRD_PARTY_NOTICES.md 原样保留。

本仓库是非官方分叉,与上游作者无关。四个补丁都建立在上游已有的机制上(breakIdentityClauses、isFailoverWorthy、prepareChatBody 等),没有一行是从别处抄来的。

—/ 5

No ratings yet

Verified DSH bundle

Commit 05de565e2be7

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout