본문으로 건너뛰기
D

dsh-connect-workbuddy

dingminhua/dsh-connect-workbuddy

중국 내외의 로컬 로그인 WorkBuddy 모델을 DSH에 연결합니다. 새로고침할 때마다 상위 카탈로그에서 모델별 크레딧 배율을 읽고, 모델별 이미지 입력 전환, 자동 장애 조치 계정 풀, 수동 일일 체크인, 카드와 작성창의 읽기 전용 크레딧 요약을 제공합니다.

설치

dsh plugin --profile web add github:dingminhua/dsh-connect-workbuddy

README

dsh-connect-workbuddy 设置卡:模型目录与倍率、图片与可关闭思考开关、活动标签(账号名已打码)

dsh-connect-workbuddy

把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness —— 每次刷新自动读取每个模型的积分倍率,逐模型开关图片输入,多账号可切换,并带每日签到与只读积分概览(卡片与输入框里的胶囊读数)。

English · 安装 · 工作原理 · 账号池 · 更新日志 · 问题反馈

npm version npm downloads test status MIT license GitHub stars dshfind plugin

一个独立的 DeepSeek Harness (DSH) bundle 插件。它把本机已登录的 WorkBuddy 账号(国内版与国际版 WorkBuddy AI 均支持)接到 DSH 的模型选择器,同时提供账号池、可勾选的模型管理,以及一套围绕积分的只读能力:每次刷新按上游目录自动读取每个模型的积分倍率(x0.79 这种,倍率完全由上游给出,插件不写死也不猜)、逐模型开关图片输入、多账号切换与账号池自动换号、每日签到、以及卡片与输入框里胶囊式的只读积分读数。国内版与国际版是两个并行的供应商(workbuddy / workbuddy-global),可同时使用;插件设置卡片以 tab 区分两者,方便统一管理。只需本机 WorkBuddy 的登录态,或在浏览器里登录一次即可。

风险提示

WorkBuddy 已开始限制非官方客户端请求。本项目不保证相关接口长期可用,上游一旦收紧限制,插件可能失效。请先阅读文末「免责声明」,自行评估账号风险。使用官方 WorkBuddy 客户端始终是最安全的选择。

需要说明的是,这个插件的目的很普通:省掉在 WorkBuddy 客户端和 DSH 之间来回切换的麻烦。你已经在自己的机器上登录了 WorkBuddy,插件只是把那份已有的登录态接到 DSH 的模型选择器上,让你在写代码的地方直接用到它——不提供账号、不共享凭据、不绕过计费。每一次调用都消耗你自己账号的额度,按上游给出的倍率真实扣费,插件从不修改账号状态,也不附带任何提权或刷额度的能力。

它依赖的是 WorkBuddy 客户端自身的接口(非官方开放 API),所以上游调整时插件可能需要跟着调整——这是这类工具的常态,也是这条提示存在的原因。请自行评估账号风险后再决定是否安装。

功能特性

  • 双供应商并行接入(国内版 + 国际版) —— 国内版注册为 workbuddy provider(GLM-5.3、DeepSeek-V4-Pro、Kimi-K3、MiniMax-M3、Hy3 等),国际版注册为 workbuddy-global provider(GPT-5.6、Gemini-3.5-Flash、GLM-5.3、Kimi-K3 等)。有账号的区域其模型会出现在 DSH 模型选择器里:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。本机没有账号的区域不列出任何模型(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider 本身始终注册着,因为它的设置卡片正是登录的入口(重新检测在账号池区块里)。模型名内嵌上游积分倍率(如 GLM-5.3 · x0.79),与 WorkBuddy 自身模型菜单一致;活动时段显示的是折后价——「夜间折扣」开着的时候,卡片与选择器上的数字一起变成 x0.06,活动一过自动回到原价,不需要重启(#120)。

  • 插件卡片 tab 切换 —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号池与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。

  • 可单独关闭任一版本供应商 —— 每个 tab 右侧有一个勾选框(默认都勾选)。取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选全部保留,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 NO_ADAPTER,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。

  • 设置卡片分四栏:账号 / 模型 / 能力 / 诊断 —— 原来这张卡片是一列 3000 多像素的长页,把两类互不相干的事情混排在一起:运维账号(谁在服务、还剩多少、切换顺序、签到日志)与编辑目录(勾模型、上下文预算、推理档、关闭思考)。它们连提交语义都不同——账号改动点一下即生效,目录改动是草稿、必须点保存——所以「保存」按钮常常离它要保存的东西隔着三屏。现在分栏,一栏只做一件事;切栏既不会重新拉取,也不会丢掉未保存的目录草稿(非活动栏留在页面里,只是通过 hidden 不可见,因此也不进入无障碍树)。第三栏「能力」放的是授权类开关——允许调用本机 CodeBuddy CLI、允许模型自主生图、积分读数显示在哪——它们点一下即生效、从不经过保存,而且每一个都留着长自己子选项的位置(CLI 的工作目录、生图的尺寸或价格上限),所以不挤在账号表格上方那一小条里。第四栏「诊断」是只读的事实表:当前区域、上游域名、真正在服务的账号名、登录有效期、目录里共几个模型、启用了几个、账号池开关,以及上游最近一次错误的原文(原样引用,不翻译、不概括)——这一栏存在的唯一理由就是能被整段贴进问题反馈,所以里面没有任何会改设置的控件。

  • 可选:侧边栏入口(sidebarPanel,默认关闭) —— 在 DSH 侧边栏加一行 WorkBuddy,点开就是这张卡片的整页版(左上有「返回会话」)。卡片以四栏挤在设置页里时,账号表格、模型目录与诊断原文都只有半屏宽;整页版把同一批东西摊到整栏。默认关闭:这一行占的是所有已安装插件共用的全局侧边栏座位,而「插件」页本来就能点开同一张卡片,一次升级不该替用户占这个位置。开关在「能力」栏,勾上即出现、取消即消失,不走保存——与这一栏其它授权类开关一致。

    • 关掉时是把注册注销,不是渲染空组件:侧边栏导航行是用槽位条目元数据(label + 图标)拼的,组件返回 null 只会得到一颗空白但仍可点的按钮。所以决策点在 apply(src/client/panel-switch.ts 是卡片与入口之间的接缝),关掉即 dispose。
    • 首屏状态问宿主,不问浏览器里的设置镜像:那份镜像在部分 0.1.7 上是陈的,且只在命名空间边缘触发、不在值变化时触发——一份陈的 false 会让这一行整个会话都不出现,且没有任何事件来纠正它。所以种子读宿主自己的答案(usage 文档的 sidebarPanelEnabled)。
    • 开着面板时关掉,中间栏自己回对话(ui-layout 丢掉已不存在的 activePanelId),不需要插件去导航。macOS 实测记录见 docs/MAC-VERIFY-PR188-REPORT.md,验证清单见 docs/MAC-VERIFY-PR188.md。
  • 模型管理 —— 从上游刷新完整模型目录,逐项勾选启用或禁用;勾选是草稿,点保存才生效(issue #214)。目录与价格不等人:刷新会把这次读到的目录直接记下来并立刻生效,因为价格不属于草稿——上游倍率随时会变,把新价锁在草稿里意味着卡片继续显示旧价、/usage 与刷新按钮各说一套(报告者实测:刷新得 glm-5.3 0.69,卡片却仍显示 0.79)。所以目录与倍率随刷新即刻落盘、即刻生效,你的勾选、上下文预算、图片勾选与默认推理档仍然只在点保存时提交;刷新不会替你把没保存的改动一起写下去。上游同时给出积分倍率、上下文/输出上限与推理档位;每个模型行上有一个「默认推理强度」选择器(issue #179):请求未指定档位时该模型用哪一档——新对话、压缩摘要、会话标题都属于这类请求。选项就是该模型自己声明的那些档位(名册里 deepseek/glm-5.3 是 low/high/max,glm-5.2 是 high/xhigh,space-bunny 有五个,所以只能逐模型选、不能全局一个值),第一项会写明真正会用的那一档——括号里先是上游为这个模型声明的档位,上游没声明时则是该模型自己的中档(不是最弱的那一档,免得默认把思考调低)。这一行同时也是原来那行只读「推理强度:…」的替代——选择器已经列全了档位,不必再多说一遍。「关闭思考」不在这里,它仍是下面那个 off 勾选框的职责。图片输入按核验表判定:刷新时只有核验表收录为原生多模态的模型才自动勾选,写明纯文本的不勾,没有资料的 id 一律按「未核实」不猜(上游的 supportsImages 是平台侧声明,不等于模型能看图,不参与判定)。核验表分两档证据——厂商文档(13 条)与上游目录自述(2 条:space-bunny、minimax-m3.1-flash,各自的描述里写着「原生多模态」)——但新增 id 仍要求厂商来源,详见 docs/DESIGN.md 的「图片输入」一节。用户仍可随时手动勾选,勾选状态保存在本地。每个模型行还有「可关闭思考」(off)勾选框:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——取消用于当前不需要该档位的模型;名单内那 9 个模型不可勾选(行上标「拒绝 off」,悬停有说明):该档位被勾上后不只是多一个选项,pi-ai 会把它当作所有未指定档位请求的默认值,压缩摘要请求正属于这一类,勾一次等于让该模型的每一次 /compact 都 400 / 11133(issue #83)。名单外的模型仍完全由你的勾选与自身声明决定;首次保存即写入你的勾选,之后不再重算。

    DSH 模型选择器:同一枚活动徽标(夜间折扣 / Free now)画在模型名之后(无账号信息)
  • 模型行上的活动徽标 —— 上游的模型目录里带着活动信息(免费试用、夜间折扣、限时免费、订阅优先等),插件不新增任何请求(复用刷新模型时本来就会拉的那份目录配置),直接在模型行上画一枚胶囊徽标:颜色与文案都由上游给出,悬停能看到上游的活动说明、活动的起止日期与每日生效时段(例如「夜间 23:00–7:50」)。徽标是「此刻」的函数:活动只在它自己声明的时间窗内显示,窗口一关徽标就自己消失;跨零点的时段按上游声明的时区判断;同一个模型命中多条活动时只画优先级最高的那条。上游没给结束时间的权益标记(如「订阅优先」)则常驻,直到上游把它下架。徽标只是提示,不改变模型行为——它标注的是上游此刻给出的活动与折扣,不替你做选择。同一枚徽标也出现在 DSH 的模型选择器里:菜单每一行都在模型名之后、队列图标之前画同样的胶囊 —— 菜单行用上游的短词(「夜间」「限免」),输入框旁那个触发器用完整词(「夜间折扣」「独家限免」);判定用的是同一份数据、同一个时钟,因为选模型的那一屏正好是最该看到「现在免费吗」的地方。(两个区域的上游目录都下发活动字段:国内版按 App 身份读 /v3/config 时带 8 条促销 + 1 条等级标记,国际版带 5 条 + 1 条;取证与判定规则见 docs/PROMO_BADGE.md。)

  • 模型行上的繁忙程度(仅国内版) —— 上游另有一条只读探针(POST /v2/chat/busy,一次最多 50 个 id,免费、不动账号状态)回答「这个模型此刻多忙」,插件在每次刷新目录时顺带问一次,把等级画成模型名旁边的一枚小人图标:排队人数 = 小人个数(正常 1 个黄人 / 轻微拥堵 2 个橙人 / 极度拥堵 3 个红人),空闲则是一枚绿色小火箭(没人排队 = 可以全速跑)——空闲不画成「绿色的 1 个人」,因为 12px 下「没人等」与「有 1 个人在等」必须一眼分开;不可用是一个划掉的小人。探针没回答的 unknown 一律不画,「没被告知」不会被当成「一切正常」;颜色跟随主题,title 悬停文字(「极度拥堵」四个字)仍在。等级不落盘:目录是能活过重启的事实,负载只是那一分钟的事实,所以等级只盖在运行时目录上、随进程消失,探针失败只记一条 warn,绝不挡住目录刷新。国内版实测这条探针在 CLI 与 App 两种身份下答案逐字节相同,50 个 id 的上限也真实存在(62 个 → HTTP 400),19 个模型 186 ms 返回。国际版没有这个徽标,因为国际网关根本没有这条接口 —— 实测 www.workbuddy.ai 对 /v2/chat/busy 回 404(/v3/chat/busy、/v2/models/busy 等同族路径也全是 404),而同一份凭据读它的 /v3/config 正常返回 28 个模型、没有任何负载/排队字段。这不是本插件没实现,也不是配置或权限问题:上游国际版从未提供这个数据。所以国际版只显示模型名与活动徽标,探针那条请求失败后安静退化成「没有徽标」,不会挡住模型列表,也不会用延迟或积分去猜一个「忙不忙」。

  • 排队时说清楚你排在第几位 —— 上游用三个业务码表达排队(6020 等待、6021 队列满、6022 已达上限)。撞上它们时,插件不再给一句上游报错,而是「模型排队中:你排在第 8777 位,队列共 9021 个请求。请稍后重新发送这条消息,或换一个模型」。排队不会触发换号:队列跟随的是模型与平台级的队伍,换个账号拿到的还是同一个位次,所以这里既不走账号池也不额外烧请求(上游 /v2/chat/queue/status 回的 message 是 i18n key 而不是人话,插件因此从不转述它,只用自己的措辞)。排队不会自动重发,也不会被自动重试——插件没有排队轮询循环(它需要排队答案在 OpenAI 兼容通道上的真实信封,没测到就不写),并且明确告诉宿主不要重试(x-should-retry: false):一次排队在几千位之后,重试十几次也轮不到,只会把真实请求白白花掉。位次报出来,由你决定是重发还是换模型。

  • 模型可用性测试(含冷却恢复时间) —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。它发的是一条真实体积的请求(约 25k 输入 token),而不是「发个 ping 看通不通」——因为上游的限流按请求体积触发(实测约 20k 能过、约 30k 被拒),小请求看着正常、长对话里却可能每次都被拒,那正是「测试通过、实际被限」的来源。恢复时间的来源:上游的 429 不带 Retry-After 响应头(也没有 X-RateLimit-*),但它把重置时间写在响应体里——{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}。插件从响应体解析该时间(并按其声明的 UTC 偏移换算,不受你本机时区影响),因此卡片能直接显示「✗ 被限流 · 2026/9/30 02:30:30 之后可再用」。三处来源都没说时,插件不编造倒计时,如实显示「上游未给出何时恢复」。限流不会去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。成功即无冷却:这个网关在 200 响应上也会带 Retry-After,值是账号月度积分包的刷新时刻(实测 2026-11-01 00:00 +08:00),它不是「你在此之前不能用」——所以成功的行只显示「✓ 可用」,绝不附「之后可再用」;旧版本已经写进文件的那类记录会在读取时自动治愈。探测在后台发起,凭据不会传到页面;结果不写入设置,也不改变你已保存的勾选。

    两点需要知道:① 它真的花钱——实测一次报 credit 0.02(被限流时直接 429,几乎不花)。因此只能按单个模型手工点,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此只接受单个模型,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的原生窗口向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限(可选 200K / 500K / 模型原生窗口)。这三档里哪一个是「当前」由后端算出的实际窗口决定(issue #215):没设过预算时选中的就是模型原生窗口,因为 DSH 拿到的正是它;「模型原生窗口」这一档对应的是没有上限,选中它会把这个模型的预算条目删掉,而不是写一个恰好等于窗口的数字——这样上游日后调大该模型的窗口时,这一档会跟着变大。

  • 生图工具 workbuddy_draw_image(国内版 / 国际版,默认关闭,逐区授权) —— 把 WorkBuddy 的文生图能力交给 agent:你在对话里说「帮我画一只在窗台上晒太阳的猫」,模型自己就会调用这个工具,画完把图贴回来。默认不开启,因为这是本插件里唯一一项「由模型自己决定花钱」的能力:国内区约 5.71 积分一张、国际区约 0.03 积分一张,而宿主没有「调用前先确认」的机制。想用就去卡片「能力」栏那个区勾上「允许模型自主生图」;不勾的时候这个工具根本不会注册,模型看不到它,也就不会发起注定失败的调用。两个区都能用,但用的是两个不同的模型:国内区 hunyuan-image-alpha(5.71 积分/张),国际区 hy-image-3.5(0.03 积分/张,2026-10-25 实测)。两者的积分池彼此独立,5.71 与 0.03 不是同一张图的跨区差价,只是一个读数,别拿它做汇率换算。勾选是逐区的:在一个 tab 里勾上只授权那一区的账号;两区都勾着时优先国内区(与升级前的行为一致)。端点与请求体照抄官方 CLI 的 ImageService(POST /v2/images/generations),产物是 24 小时有效的 COS 签名 URL,插件会提醒你及时下载。同一个工具也能改图:给 image 参数(本地路径 / file:// / http(s) / data URL / 裸 base64,上限 8 MB)就自动切到 POST /v2/images/edits 并用带 image-to-image 标签的模型(国内 hunyuan-image-alpha-edit、国际 hy-image-3.5),不给就是文生图——模式由「有没有图」唯一决定,没有 mode 参数。输入图只归一成上游要的 data URL,不缩放、不裁剪、不转格式;读不到图、格式错、超限都在发请求之前报错,不算一次花钱的失败。尺寸不限于方图,上限约 419 万像素(2026-10-25 逐档实测:1024x1536 / 2048x2048 / 2048x1536 / 2560x1440 / 2560x1600 都成功且输出宽高与请求一致;2560x2560 起被直接拒绝,不会悄悄降级),常见回答是「桌面 16:9 用 2560x1440、方形大图 2048x2048、横图 2048x1536,竖版把长边放到同一档」;再大只能走超分,单纯调参数已经到顶。它真的花钱:单价随区域与模型不同,所以工具描述里同时写了两个区的量级,并要求以返回的 credit 为准;失败后有 30 秒冷却(但参数写错、没登录、模型不存在这三类一分钱没花,不进冷却),也不提供「一次画四张」这类不可控支出的参数。工具定义是手写的 raw JSON Schema,没有引入任何新依赖。取证、协议与联调记录见 docs/IMAGE_GEN.md。

  • 视频生成工具 workbuddy_draw_video(国内版 / 国际版,默认关闭,逐区授权) —— 把 WorkBuddy 的视频能力交给 agent:「帮我把这段描述做成 5 秒视频」,模型自己调用工具,跑完把 mp4 的本地路径贴回来(视频是二进制,只回 file:// 路径并渲染成链接,不塞进对话体)。它和生图不是一个形状:视频是异步任务,POST /v2/videos/generations 只回一个 taskId,插件再轮询 POST /v2/videos/tasks 直到 completed(5 秒一次、上限 10 分钟),最后把产物下载到 os.tmpdir()/workbuddy-video-gen/。同一个工具也做图生视频:给 image 参数(与生图同一套输入形态)就切到 image-to-video 标签组并按输入图决定比例,不给就是文生视频——模式由「有没有图」唯一决定。其余参数:seconds(默认 5,须在该模型 [min,max] 内)、resolution(不在模型 resolutions 内会回落默认值并给出告警)、aspect_ratio(仅文生视频,16:9/9:16/1:1)、negative_prompt、enable_audio。默认不开启、逐区勾选「允许模型自主生视频」,不勾时工具根本不注册;失败后 60 秒冷却,但参数错、没登录、读不到输入图、以及上游在建任务之前就退回的 4xx(它明说没扣积分)这类一分钱没花的失败不进冷却——改完参数可以立刻重试,不用白等一分钟;上游若在拒绝文案里报了时长区间(supported range: 4-15 seconds),插件会记住它,下次同样的越界值直接在本地拦下。模型可按模式钉住:卡片「能力」栏在视频开关下方按文生 / 图生各给一行选择框,写进 preferredVideoModels(按区、按标签存);它是软约束——显式 model 写错照旧报错,钉住的模型若已从目录里消失则静默回落到组内第一个。两个区的模型目录不同(国内区 kling-video-v3 / minimax-video-h3,国际区 kling-video-v3),id 不跨区。实测单价(国内区):图生视频 4 秒 57.14(minimax-video-h3)/ 68.57(kling-video-v3),文生视频 5 秒 71.43 / 128.57 积分;产物实测是 H.264 + AAC、24fps 的 1280x720 或 1344x768(没有 1080p,resolution: "1080p" 会被拒并回落默认),视频码率 3.7–11.5 Mbps,秒数以 24fps 帧数为准(请求 5 秒得 5.09–5.17 秒)。取证、协议与联调记录见 docs/VIDEO_GEN.md。

  • 本机账号切换 —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。

  • 对话输入框积分读数(国内版 / 国际版自动切换) —— 对话输入框左侧工具栏常驻一行可点击的读数(如 WB CN · 1,072),按本会话当前选中的模型自动切换区域:选中 WB CN 的模型就显示国内版余额,选中 WB AI 的模型就显示国际版余额;用其它 provider 的模型时该读数完全不出现。每 5 分钟自动刷新,也可点开手动刷新。点击读数弹出面板:按账号列出的余额表格,点某一行即可切换到该账号(与卡片上的账号选择是同一个设置);当前不能用的账号会在行内标出原因(如「被限流」「积分耗尽」),标注口径与账号池表格一致。面板里的余额只在打开时才去查,因此不会让 5 分钟的后台刷新按账号数翻倍。要关掉某一边,就在卡片对应区域的 tab 下取消勾选「在输入框工具栏显示积分读数」;该区域的供应商被关闭时,读数不显示、开关也置灰。同一个开关下面还有「显示位置」这一行:默认在输入框工具栏(与模型选择同一行,位于模型选择器左侧、与权限切换器隔开),改成「输入框下方」后读数落到轮次/步数/token 统计那一行——装了别的插件把工具栏座位占掉时用得上。

    对话输入框积分面板:国内版账号余额表格、一键测试按钮与最近刷新时间(账号名已打码)
  • 账号池:多账号批量操作与自动换号 —— 把要用的账号勾进池子,即可签到勾选的账号、测试勾选的账号 × 你指定的模型(默认挑该区域倍率为 0 的免费模型);开启启用账号池即同时打开自动换号——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员默认一个都不含、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);开启后由池子的排序决定每一次请求用谁(可用优先,其次快过期先花、积分高、凭据更新的);失败时依次用池内其他可用账号重试。排序从不写回你保存的选择——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。这个开关只管「插件要不要自己决定」:关掉它,插件不再替你选账号、也不再替你换号,但签到与测试仍然可用(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此有真实成本;不提供自动签到——签到会真实领取奖励,只在你点按钮时执行。详见账号池。

  • 企业版 / 教育版账号:签到与「领取」不适用 —— 带 enterpriseId 的席位(企业版或教育版)在池子里不显示签到列、不显示福利活动,也不会发出领取请求(5.8.0,issue #129):上游对这类成员的个人权益写接口一律拒绝(实测 10001),而它的读接口照样把活动列出来、标着「未领」——所以「读到了」绝不能当成「可以领」。积分也在另一本账上(企业用量 = 上限 − 已用),个人积分表对这种账号返回空列表。这个判断贯穿池卡片、输入框胶囊与 Web 状态页,并且在账号进入池子的那一刻就带上标记,而不是等用户点了按钮才发现不支持。

  • 可选:让上游用量页把这笔记成官方客户端发的那笔(5.8.0,issue #132,默认关闭) —— 开 usageIdentity 后 chat 请求带上 X-IDE-Name: WorkBuddy(连同 X-IDE-Type: CLI 与版本号;版本号取自现有的产品 UA,不留第二个硬编码数字),用量页的「使用端」由此不再空白;会话身份三件套同时改成官方客户端那种 32 位十六进制 UUIDv7 并补上 X-Request-ID——网关把这个 id 回显成 SSE 的 id 并写进用量页,此前带连字符的 v4 与官方记录摆在同一页上像两种客户端;每请求现生成、互不相同这条没有放松。User-Agent 在两种取值下都一字未动——账单与限流认的是 CLI 通道;生图路径也不跟着这个开关变,它保持自己 2026-10-21 实测过的形状。默认关的理由:形状本身不修任何东西(11128 的三道门读的是消息开头、会话 id 有没有、首条是不是 system,头改不动其中任何一条),而实测证据来自报告者的账号与旧版本、本插件未在真机复现——一次纯显示对齐不该让所有账号先承担风险。另有可选的 keepPromptLast(默认关闭):用量页的「请求」列取自 body 里最后一条 user 消息,而宿主把时间采样、运行时上下文、<system-reminder> 追加在你真正输入的那句之后,那一列于是常年记的是注入块;开启后把这些注入块搬到真实输入之前(只变顺序;另外会剥掉被记录那一条真实输入前面的 <acp …>mNNNNN</acp> 标记,否则那一列仍然以标记开头而等于没修,更早的消息一律保留自己的标记,因为模型靠它们引用历史)。默认关的理由是它会改变上游会数到的消息顺序、并删掉真实输入的几个前导字符;搬移只匹配宿主自己的注入前缀,而普通对话在没有注入尾块时逐字节不变(连标记也不剥)——这两点各有专门测试钉住。

  • 可选:把每个新会话的标题请求改到隐藏轻量模型(issue #133,默认关闭) —— DSH 宿主每个新会话会发一次标题生成请求,走的是与对话相同的 provider,于是这笔按主付费模型的费率计费。开启 sessionTitle.enabled 后,插件把能按宿主标题模板(Generate the session title from this JSON array of human messages: 开头、整条 body 只有这一条 user 消息)识别出的标题请求改写到 sessionTitle.model(默认 wb-lite)。报告者实测该模型对这类请求 0 积分,但注意三点:这仍是真实上游请求,请求数照算;证据是报告者账号级的,本插件未复现;识别按 fail-closed 设计——宿主若改模板,改写静默停止、回到原计费,不会误伤普通对话。若上游拒绝该模型,只是该会话标题生成失败一次,不影响主对话。另外,sessionTitle 与本条及上一条的 usageIdentity / keepPromptLast / extraInjectedPrefixes 一样,都是插件挂载时读一次配置:改完 cordis.patch.yml 后需要重载插件或重启 DSH 才生效,热改不生效。

  • 福利活动:待领取标记 + 单账号「领取」按钮 —— 卡片会逐账号标出上游活动(补偿 / 赠礼)里还没领的那一份,数据来自上游自己的两个状态接口,标记阶段只读、两个写接口一次都不调用。显示规则完全由上游状态决定:active && !claimed 才显示,claimed 整行消失,活动过期(active 翻 false)自动消失——不用本地时钟去比 end_time,本机时间偏了也不会与服务端打架;end_time 只用于 3 天内临期高亮。active 单独看不行:已领取的账号 active 仍为 true,只看它会让领过的账号永远挂着待领取。读取复用签到的管线(每账号一次、30 分钟缓存、账号池关闭时不发请求),任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。行尾是一个实心琥珀色的胶囊按钮「领取」,一次只领一个账号的一个活动:没有「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益,写路由也强制要求 accountId,请求本身无法批量领取。领取前重新读取状态再判定,已领取或已过期一律 409 且不调用上游;领取后用上游的新读数作答并刷新卡片,不做乐观更新——即使上游静默失败也不会留下「已领取」的假象,成功后同时清掉该账号的福利缓存,按钮不会顶着旧状态赖着不走。

  • 可选:被网关渠道校验拦截后的最后兜底「继续」(默认关闭,默认只发 1 次) —— 上游用 400 + 业务码 11128 拒绝时,这一轮就此结束:插件不会在原请求里重试(三级救援已跑完),不会换号(网关匹配的是消息内容,每个账号发出去都一样),宿主也不重试。它不是 11128 的修复:固定 system 指纹(#163 的 identity / Web GUI / @ 路径整段)每一轮逐字节相同,重发只会再付一次同一个 400;5.13.1 起这些由 prepareChatBody 的按句改词处理(5.14.0 又补上 Web GUI 段落的第二句,见 #167)。只有被拒内容刚好随轮次变化的窄情况,才可能靠新回合碰过去。开启卡片「能力」栏的开关后,插件检测到这一类失败、等 300ms、用 agent.followup 发一条 继续(source.kind 是 workbuddy-continue,不是 user)。上限数的是「连续」次数(channelRejectContinue.maxConsecutive,默认 1;旧默认 3 已证伪——1 次救不了,3 次也救不了):成功一轮、或你自己发一条消息,计数清零;插件自己发的不清零。子代理会话一律不碰;会话正在跑、或收件箱已有排队内容时也不发。每次都会真实消耗一次请求,所以默认关闭;它也是本插件里唯一一个改完即生效的上游类开关。与其它上游开关一样,也能直接在 cordis.patch.yml 里写:

    channelRejectContinue:
      enabled: true
      maxConsecutive: 1
      text: 继续
    
  • 凭据失效时给出可执行的建议 —— 上游拒绝当前账号的 token 时,插件会实际探测本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。

  • 扫码添加账号、批量导入导出、逐账号测试/删除 —— 不必先装 WorkBuddy 桌面 App:卡片里的「扫码登录」按钮(国际版为「网页登录」——那边是浏览器登录页,没有二维码;这一行就在「更多设置」上方,点一下立即生效,不需要保存)点开即弹出对话框,并在浏览器里自动打开授权页;被浏览器拦截时对话框内提供链接、复制与「重新打开页面」兜底。所有凭据统一落到插件自己的凭据库($DSH_HOME/workbuddy-vault/<region>/<accountId>.json,一账号一文件),桌面 App 的认证文件始终只读、永不被改动。账号表格每一行右侧有「测试」与「删除」:测试与批量测试走同一套探测,结果可比;删除只删插件自己存的那份,并把这个账号暂时隐藏:删掉后立刻消失,也不会被自动轮询带回来;点「重新检测账号」即可找回(重新登录或导入同一账号也会清除隐藏记录)。桌面 App 的登录始终不动——删除是「本插件不再列出它」,不是退出 App 登录。「导出 / 导入账号」可把凭据搬到另一台机器:多选导出 JSON → 选文件后先看脱敏预览(只显示字段与是否有 token,明文不进预览)→ 勾选导入。导入按账号去重覆盖,只有确认属于该区域的记录才入库,认不出的域名会被跳过并如实计入「已跳过」。详见账号池。

  • 积分信息在账号池里 —— 成员表逐账号显示剩余积分与最近到期时间,排序的第二键就是积分(谁多谁优先)。查询是只读的、不消耗积分。

  • 凭据路径多候选 —— macOS / Windows / Linux 逐一探测默认位置,也支持环境变量与卡片内直接指定。未登录时卡片会列出「探测过哪些路径、各自为什么没成」:五档原因(不存在 / 读不了 / 没有可用 token / 字段已加密且拿不到密钥 / 属于另一个地区)分开呈现,因此「已经登录了、但桌面 App 不在场」这种最容易误判的情况会明说需要 App 在场,而不是叫你重新登录一次。属于另一个地区那一档尤其不同:登录信息是真实可用的,只是挂在另一个标签页下,卡片会直接给出结论「切换标签页即可,不用重新登录」。列表只讲一次——提示段落说结论,折叠列表给路径细节,两处不重复同一件事。

  • 安全 loopback shim —— 每区域一个随机端口 + 进程内随机 secret,真实 WorkBuddy token 不交给 pi-ai。

  • 命令行诊断 —— status / doctor / logout,按区域报告登录与积分,无需浏览器即可确认宿主状态。

  • CLI 路线(可选,默认关闭) —— 伪装成 CodeBuddy 客户端过渠道校验是一条军备竞赛的路,而且已经出现账号级限制(issue #98 的 code 11140)。另有一条路不做这层伪装:让本机已登录的 CodeBuddy CLI 自己跑任务,由它去读文件、跑 git、改代码。打开 cliBridge.enabled 后插件多出 codebuddy_ask 工具和 /codebuddy 指令,跑完把结论回注给会话模型。关闭时插件行为与之前完全一致。它绕开的是客户端指纹层,不是账号层:用的仍是本插件 vault 里的同一批账号,所以不该把它当成「封号之后的免死金牌」。详见 CLI 路线。

工作原理

DSH PiAiAdapter(每个 provider 一套)
  -> 安全 loopback shim(每区域一个随机端口 + 进程内随机 secret)
  -> WorkBuddyUpstreamClient
  -> 国内版 https://copilot.tencent.com/v2/chat/completions
  -> 国际版 https://www.workbuddy.ai/v2/chat/completions
  -> WorkBuddy SSE
  -> DSH 本地执行工具并回传结果

国内版与国际版各持一套完整的运行时栈——凭据 store、模型 catalog、回环 shim、adapter——按凭据域名(workbuddy.ai / codebuddy.ai → 国际版,其余 → 国内版)隔离可见账号,所以两个区域的账号可以同时在线、同时被不同会话使用。国际版有两个品牌域名:桌面端登录在 workbuddy.ai,CodeBuddy CLI 登录在 codebuddy.ai,两者都是国际版。

模型目录按区域取自各自的正确来源,两者都是 App 自己读的那份 /v3/config,且都以 App 自己的客户端身份去读(WorkBuddy/<app> WorkBuddy/<app> CLI/<cli>,即桌面端 buildVlmGatewayHeaders 拼出的那个头)。这一步是必需的:/v3/config 按客户端通道下发不同的产品配置,CLI 通道的答复是 App 通道的严格子集,既不发布 modelPromotions 也不发布 modelTiers——用 CLI 身份读,国内版的活动徽标就会一个都画不出来。聊天与探针仍以 CLI 身份发送(会话身份三元组绑在聊天通道上),只有目录读取换成 App 身份。国内版若 /v3/config 取不到,会退回旧的 /v2/enterprises/personal/models 并记一条日志,所以只在降级时使用。积分概览走 https://www.codebuddy.cn/v2/billing/meter/get-user-resource(国际版账号自动路由到其凭据所属的国际网关,workbuddy.ai 或 codebuddy.ai——两个品牌域名的凭据互不通用),均为只读接口。

凭据读取自 WorkBuddy 桌面 App 自身的 auth 文件(只读);刷新得到的 token 按区域存放在 $DSH_HOME/.workbuddy-auth.cn.json 与 $DSH_HOME/.workbuddy-auth.global.json(双账号同时在线互不覆盖;旧的单文件 .workbuddy-auth.json 作为迁移来源保留读取),桌面端文件永不被写入。

加密凭据字段:新版桌面端(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 指定其可执行文件。

账号池

dsh-connect-workbuddy 账号池:签到勾选的账号、测试勾选的账号、成员排序与签到状态(账号名已打码)

一台机器上往往登录过多个 WorkBuddy 账号。账号池把这件事收成一次点击:把要用的账号勾进池子,然后「签到勾选的账号」或「测试勾选的账号」;再打开启用账号池,插件就会在请求失败时自动换号接着跑。

每个区域(国内版 / 国际版)各有独立的一池:成员与目标模型互不影响——两边的账号本来就属于不同的上游栈。签到只有国内版有:海外区域不显示签到按钮,也没有那一列。

自动换号(开启账号池即生效)

  • 谁来服务由排序决定:开启账号池后,每一次请求都发给池子里排序最靠前的可用账号——所以「当前使用」显示的是排序的结果,不一定是你手动选的那个。排序规则见下一节。
  • 失败时继续往下试:某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止;全部失败才把错误报出来(并在错误里注明尝试过几个账号)。
  • 哪些失败会换号:429 限流、402 无积分、401 凭据失效、502 网关/网络——这些都是某个账号的状态,换一个账号很可能就过了。
  • 哪些不会:请求本身不合法(HTTP 400)。同一个请求体换哪个账号都是同样的 400,重试只是让用户多等几轮才看到同一个错误。流中途断开也不会重试——字节已经发出去了,重放会让客户端看到两段拼接的回答。
  • 已知不可用的账号会被跳过:正在限流冷却期内、或凭据已被上游拒绝的成员不会参与(这也是「一键测试」的意义:测试结果直接决定谁能被选中)。从没测试过的账号按「默认可用」参与——刚发现的登录不该因为「还没测」就被排除。
  • 手动选择不会被改写:池子通过运行时覆盖决定「谁服务」,从不写回你保存的选择;关闭账号池,你选定的账号立刻完全恢复生效。这也是为什么卡片要显示「当前使用」——它可能不是你在上面选的那个,而你应该看得出来。
  • 失败会被记下来:某个请求撞上失败时,插件把这次失败写成该账号的测量(含上游报文里给出的重置时间),所以下一个请求直接从可用账号开始,不会每次都先撞一遍同一个限流账号。只有需要你手动「一键测试」时才不写测量这件事,从此不成立了。HTTP 400 例外——那是请求不合法,与账号无关,记下来会白白闲置一个好账号;未勾选进池的账号也不记(勾选才是「可以花它的积分」的同意)。
  • 每次换号都会在宿主日志里留一行 warn,写明因何失败、换到了下一个账号。

这个开关到底控制什么

它只控制「插件要不要自己决定」,不控制「你能不能操作」。

账号池 开启账号池 关闭
谁服务由排序自动选出(可用优先 → 快过期先花 → 积分高 → 凭据新)你在账号池里手动选「使用哪个账号」
请求失败自动换下一个可用账号重试不换号,如实报错
一键签到 / 一键测试可用仍然可用
目标测试模型可改仍然可改

最后两行是刻意的:签到与测试是手动动作,它们作用于你勾选的成员,与「插件是否自动路由」是两件事。之前把按钮也挂在开关上,导致关掉账号池就再也没法签到或测试——而「关闭」本该是「我手动来」,不是「什么都不许做」。

手动选择账号在哪里:账号池区块里。关闭账号池时,该区块会显示一行「使用哪个账号」的下拉,选定即生效(写回本区域的账号槽,与凭据失效时的「切换到可用账号」走同一条已验证的写入路径);开启账号池时这一行不显示——那时由排序决定,一个改了不生效的下拉比没有更糟。账号的发现(「重新检测账号」)、勾选、签到与测试全部集中在这个区块里,所以账号管理只有一处。

唯一的硬约束:池里一个成员都没勾选时,批量动作一律拒绝。空的池绝不退化成「那就对全部账号执行」——那是唯一会花掉你没授权过的积分的解读。

排序规则(开启账号池后决定谁服务)

  1. 可用优先 —— 被测量判定为不可用(限流未到恢复时间、凭据被拒、连通性失败等)的排到最后,不参与选择;
  2. 快过期的积分先花 —— 3 天内到期的积分用不掉就作废,所以有临期积分的账号排在前面,先到期的先排;两个都在临期时,临期积分多的优先;
  3. 积分高者优先 —— 在没有临期积分的账号里,选积分最多的。积分只在有新鲜读数时参与(卡片轮询、批量测试或保存时读到的那份,10 分钟内有效):请求路径不会为了排序去打上游,否则一页对话就会变成 N 个额外请求;
  4. 凭据更晚过期的优先;
  5. 最后按账号 id 收尾,保证顺序是全序、可复现,不会因为账号表顺序变化而抖动。

第 2 条为什么排到第 3 条前面(issue #70):这两条此前是严格字典序——先比积分总量,只有总量完全相等才比到期时间。而总量几乎从不恰好相等,于是「快过期优先」实际上从未生效过:一条「不花就作废」的规则被另一条完全遮蔽,等于死代码。真实病例里,某账号有 196.4 分当天作废,只因为比另两个账号少约 1,200 分就永远排在最后,那 196 分必然烂掉。

代价也要说清:这条规则会让临期 1 分的账号排到余额 5 万的账号前面。这不是缺陷——积分是通用的,不花那 1 分它确定会损失;而余额 5 万的账号下一轮还在。唯一的真实代价是临期账号可能不够跑完一个请求,这时由既有的换号逻辑接手。

两个批量动作

  • 签到勾选的账号 —— 逐个账号签到。签到前先查当天状态,已签到的直接标记为「已签到」而不发写入请求(幂等,不重复领取);任何单个账号失败只标记该行,不中断整批。
  • 测试勾选的账号 —— 对每个成员发一次真实体积的探测请求(约 25k 输入 token;与模型行的「测试」是同一套 probeModel,因而同样继承两个实测结论:限流按请求体积触发、恢复时间写在响应体里而不是响应头),得出该账号此刻能否用这个模型。

测试目标是哪个模型:默认是该区域目录里倍率为 0 的免费模型(实测国内版当前只有 hy3 一个;不写死名字,因为上游会改倍率——deepseek-v4.1-flash 就已从 0 变成 0.11),也可以在下拉里指定一个具体模型。指定的模型若已离开目录,手动动作会明确拒绝(target-model-stale),而不是偷偷换一个——你点的是「测这个模型」,换掉就是答非所问。

成本与边界:测试是真实请求,指向免费模型时(倍率 0)几乎不花钱,但它不是零成本的空转;签到则会真实领取当日奖励。两个动作都只在你点按钮时执行——插件不提供自动签到,卡片上也不放一个永远点不动的开关。

池成员是显式勾选,默认一个都不含

「本机有登录」不等于「要拿它去签到和花积分」。成员默认空,必须逐个勾选(或点「全选」);空池不会退化成「全部账号」——没勾选时两个批量动作直接拒绝,而不是替你把所有账号跑一遍。

勾选是一次草稿编辑:点「保存」才落盘,未保存时保存按钮带 dirty 指示;切走或关掉卡片,草稿仍在。

手动选择:卡片上没有账号下拉了

账号一律在账号池里管:成员勾选就是现在的账号选择面,「重新检测账号」也在那一块。原先那个「账号选择」下拉在有账号池之后是自相矛盾的——池开启时由排序决定谁服务,它改什么都不生效,却看起来像权威;顶部状态行还可能显示成 A、下拉显示成 B。

所以区域状态行也跟着改:池开启时不再报出某一个账号名(多个账号同时登录是常态,用单数说「已登录:A」本身就是错的),只陈述本区域可用、由池决定用谁;池关闭时才报出那个账号——那时它确实是唯一答案。状态行旁边的圆点与 token 过期时间讲的是「这个区域能不能用」,和「谁在计费」是两件事,后者由账号池区块负责显示。

账号为什么出池,以及什么时候回来

探测结果对轮换的意义
可用可入选
被限流暂时出池,按上游给出的重置时间自动回池(上游没说时间就按 30 分钟兜底,不编造倒计时)
积分耗尽出池,按月周期包的刷新时间回池
连不上上游 / 失败暂时出池,30 分钟后自动回池——这类结果说的往往不是账号,而是一次网络抖动或上游 5xx
凭据被拒 / 模型不存在出池,只能靠手动「测试」回池:一个需要重新登录,一个是本区域不再提供该模型,等待都修不好
账号被上游限制使用停权,等待修不好(5.8.0)。上游的话是「账号存在异常行为,已被限制使用」——它点名的是这个账号,所以自动换号会绕开它去用下一个。这与「渠道校验失败」正好相反:那条说的是这个请求,换号只会白烧一次额度,因此不换号。回池的实际出路只有手动「测试」:账号 id 是 uin/uid 的哈希,重新登录后仍是同一个 id,插件也不会因为重登清掉这条测量(「重新授权就回池」不成立);上游解除限制后测一次通过,它才会重新成为候选。插件不给它编一个倒计时,但停权只作用于轮换候选资格——全池没有可用账号时,请求仍会落到你保存的那个账号上

关键一条:「被限流」「连不上上游」都不等于不可用。它们恰恰是最值得等的那两类状态——当成永久失效剔除,正好丢掉池子里本来最能用的账号,而且当池内每个成员都被这样剔除时,池子就没有任何候选,自动换号会无处可去,你只会看到上游原始报错。

为什么只有限流/欠费/瞬时故障会自己回池:这三类要么由上游写明了恢复时间,要么由插件按 30 分钟兜底;而「凭据被拒」「模型不存在」等待都无意义,只能由你手动重测或重新登录来刷新。插件不再有自动复测定时器(见下),所以后两类的唯一出路就是手动动作。

保存与草稿:为什么卡片上有两个「保存」

模型管理一个、账号池一个,这是刻意保留而非漏合并:两者的草稿域不同,合并后一次写入失败会连累另一边(模型目录写不进去,就连池偏好也存不了);一个共享的「放弃」还会在一次误点里同时丢掉两份草稿。两边都是验证写入——写不成功不丢弃草稿,否则用户的编辑就永久丢了。「保存中」那个转圈只覆盖这次已校验的写入(5.8.0,issue #131):写入一落地就解除忙碌、丢弃草稿,随后的用量刷新只是显示——刷新慢或失败都不会把已经保存成功的设置说成「没保存上」,只会另附一行「已保存,但面板未能重新读取。设置已写入;下次刷新后会显示。」(role="status",与「保存失败」的 role="alert" 是两个独立节点)。此前两者共用同一个 loading,于是一次慢刷新能让按钮永远转下去,而数据其实早就已经落盘。

池里还有一类状态不走草稿:探测结果、冷却时间、当前轮换到的账号。它们是观测事实,由插件自己高频写入。原因很实际——草稿是覆盖式的,若观测结果也进草稿,你手工点一次「保存」就可能把插件刚写进去的结果回退成几分钟前的旧值。

安装

⚠️ 版本要求: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.3.0 线被明确拒绝。详见下方受支持的宿主范围。

⚠️ 国内区的「渠道校验」400(code 11128)请确保插件 ≥ 5.14.0。 5.13.1 起会在发出请求之前按句改写已知的固定指纹,5.14.0 又补上了 Web GUI 段落的第二句(#167,5.13.1 上仍然必现的那一支);5.12.x 及更早只有「中性头 + 零宽扰动」三级救援,在同一份 body 上实测全部 400(对照矩阵见 #164)。停在旧版的人会反复撞上这个已经修好的 400,而表现是「每次必现、重启也没用」。

⚠️ 别把插件市场里那个「最新」当日程表——它读的是一份会落后的索引。 社区市场走定期构建的快照(按包各自收录),2026-10-30 15:00 那次观测里本插件还停在 5.13.1,而当时 npm latest 已是 5.14.1;桌面宿主另有「24 小时内发布的版本先标成「新版等待中」」这条规则。以 npm view dsh-connect-workbuddy version 与 Releases 为准,按下方「更新」一节做(写精确版本号),或者把 docs/PINNED-UPGRADE-LAG.md §3 那段话丢给 AI 让它替你跑。

另一个会让人升级不到的原因:pnpm 的 24 小时新版本等待期(minimumReleaseAge)会把浮动范围静默压回上一个版本。需要立刻修复时请写精确版本号:dsh plugin --profile desktop add dsh-connect-workbuddy@5.15.1(原因见下方「刚发布的版本请写精确版本号」那条)。

受支持的宿主范围

DSH 的随包门禁(packages/boot/app-boot/src/plugin-compatibility.ts 的 evaluatePluginCompatibility)会逐条核对本插件声明的 dsh-* peer 范围;只要有一条不满足,整个组合包会被跳过(进入 skippedBundles,仅打印到 stderr)——provider 不注册、卡片不出现、模型列表清空,而页面上没有任何报错。

宿主版本判定
0.1.7-rc.1 ~ 0.1.7-rc.2✅ 支持
0.2.x 正式版(0.2.0、0.2.1 …)✅ 支持(2.1.2 起实测)
0.2.x 预发布(0.2.0-rc.1、0.2.1-rc.1 …)⚠️ 不落在声明范围内,见下方说明
0.3.0-0 及以上❌ 明确拒绝(该线尚未验证)
0.1.6 及更早❌ 明确拒绝(0.1.5 线请停留在 v2.0.15)

关于 0.2.x 预发布(v4.7.0 更正):此前本表把 0.2.0-rc.1 写作「✅ 支持」,那是错的。 npm semver 规定:预发布版本只有当一个比较器在同一条 major.minor.patch 元组上带预发布标签时才被放行。 现声明 >=0.1.7-rc.1 <0.3.0-0 只对 0.1.7-x 开口,<0.3.0-0 又排除了 0.3.0 元组,因此 0.2.1-rc.1 这类版本两条比较器都不满足(satisfies('0.2.1-rc.1', …) === false)。 实际影响很小:宿主门禁只校验 @deepseek-ai/dsh* 命名空间(其余 peer 不参与),而它对这批 peer 的范围求值用的是 semver.satisfies(rt, range, { includePrerelease: true }),所以 rc 宿主照旧 放行——闸门确实校验了,只是用 includePrerelease 放过(归因订正见 docs/DSH-0.2.1-RC3-FOLLOWUP.md §3)。 这不是靠改范围能解决的问题——任何静态范围都覆盖不了「任意 0.2.x 预发布」,已逐一实算否证五种写法 (见 CHANGELOG 4.7.0)。等宿主发 0.2.1 正式版即自然成立。

上界写作 <0.3.0-0 而非 <0.3.0:DSH 至今全部 tag 都是预发布(-alpha.N / -rc.N),只写 <0.3.0 会放行整条 0.3.0-alpha.1 / 0.3.0-rc.1 这条从未验证过的线,围栏形同虚设——这正是 2.1.2 修掉的缺陷形状。该口径由 tests/dsh-line.spec.ts 守卫(含预发布悬崖的用例)。

🪟 平台支持:Windows、macOS、Linux 三者并列支持。 Windows 是一等目标平台,不是「顺带能跑」:CI 在 windows-latest 上跑完整套件,凭据路径、进程心跳、App 可执行文件定位、设置写入四处都有 Windows 专属分支与测试,并且修复只对 Windows 生效的缺陷时,以 Windows 用户回报的现场与真机取证的加密向量为准(见 CHANGELOG 的 2.0.6 / 2.0.11 / 2.0.12 各节)。见下方平台支持一节了解各平台的具体差异与已知限制。

前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态)。

推荐使用 DSH 插件命令安装 npm 已发布版本:

dsh plugin --profile desktop add dsh-connect-workbuddy

或直接通过 npm 安装:

npm install dsh-connect-workbuddy

安装、更新或卸载 bundle 后,需要重启对应的 DSH 进程。

⚠️ 刚发布的版本请写「精确版本号」,不要依赖浮动范围。 pnpm 有新版本等待期(minimumReleaseAge,默认 24 小时),所以一个版本刚发布不久时,浮动范围会被静默压回上一个版本:声明 ^4.4.2 时它仍解析到 4.4.2,而写精确的 4.4.3 则立即拿到 4.4.3。由于两种写法都「安装成功」,表现是「升级命令跑完了、版本却没变」,很容易被误判成发布失败。已实测两轮(隔离目录、--lockfile-only,未触碰用户环境):

第一轮 pnpm 12.3.4(4.4.x 当时):

目标写法解析到的版本
浮动 ^4.4.24.4.2 ⚠️
精确 4.4.34.4.3
浮动 + --config.minimum-release-age=04.4.3

第二轮 pnpm 10.34.5(2026-10-30,5.14.1 发布 11 小时,显式设 minimumReleaseAge=1440):

目标写法结果
浮动 ^5.13.1解析到 5.13.1,不报错 ⚠️
精确 5.14.1ERR_PNPM_NO_MATURE_MATCHING_VERSION,明确拒绝
精确 5.14.1 + profile pnpm-workspace.yaml 里 minimumReleaseAgeExclude: [dsh-connect-workbuddy]5.14.1 ✅
不设该项(pnpm 默认)浮动 ^5.13.1 → 5.14.1 ✅

想立刻用上新版,请写精确版本号:dsh plugin --profile desktop add dsh-connect-workbuddy@5.15.1;若被等待期拒绝,见 docs/PINNED-UPGRADE-LAG.md §2 的两条路(等窗口过去,或加那行 exclude)。过了等待期之后,浮动范围会自然跟上。

插件在 Web、Desktop、TUI 三种界面下均可运行,按你使用的 profile 选择对应命令:

dsh plugin --profile web add dsh-connect-workbuddy     # Web
dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI

安装报 ERR_PNPM_IGNORED_BUILDS?先看这条

[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: <某个包>
dsh: plugin command failed; diagnostics: …/operation-xxxx/pnpm.log

这个错误几乎总是别的插件的,不是本插件的。 本插件的 dependencies 只有 @deepseek-ai/schemastery 一个,没有任何 preinstall / install / postinstall 脚本,也没有 optionalDependencies 或原生模块(native/turing-sdk/worker.cjs 是 纯文本 JS)。所以它不可能产生「被拦下的构建脚本」。

那为什么装本插件时会报?因为 pnpm 每次 add 都会重新校验整个 profile 的依赖树。 如果树里已经有某个包声明了构建脚本、而 pnpm 没有拿到批准,strictDepBuilds (pnpm 11 起的默认值 true)就会让这一整次安装失败——装的哪个包都一样。实测 在同一个 profile 里装 dsh-better-sidebar、dsh-better-reasoning-effort 也会报 同一句,而干净的 profile 装本插件是 1.5 秒成功。

怎么定位是哪个包。 看报错里 Ignored build scripts: 冒号后面那个名字,那是 别人的包。例如:

[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: dsh-purge@file:../../vendor/dsh-purge

@file:… 说明它不是从 npm 装的,而是一个本地目录。上面这个路径是相对 profile 目录写的:<DSH Home>/profiles/<profile>/../../vendor/dsh-purge 就是 <DSH Home>/vendor/dsh-purge。也就是说这个包被放在了 DSH Home 下的 vendor/ 里, 再由 profile 以 file 协议引用。这类包最容易出问题:它的 postinstall 指向的文件 可能没被打进那个目录,于是即使放行,执行时还会报 Cannot find module …。

(路径按报错原文推算,用来定位是哪个目录;具体是谁、为什么把包装在那里,得看那个 包自己的安装说明。)

三条修法,按推荐顺序:

  1. 卸掉那个包(最干净)——如果它不是你正在用的插件:
    dsh plugin --profile <你的profile> remove <那个包名>
    
  2. 让 pnpm 不再因为被拒的构建脚本整体失败——编辑 profile 的 pnpm-workspace.yaml(位置:<DSH Home>/profiles/<profile>/pnpm-workspace.yaml, Windows 上是 C:\Users\<你>\.dsh\profiles\<profile>\pnpm-workspace.yaml),加一行:
    strictDepBuilds: false
    
    这不会执行任何脚本,只是把「有脚本没被批准」从错误降级为警告。
  3. 显式放行那个包——只有在你确实需要它的构建脚本时:
    allowBuilds:
      "dsh-purge@file:vendor/dsh-purge": true
    
    ⚠️ 两件事:key 必须是完整标识——写 "dsh-purge@file:vendor/dsh-purge", 只写包名 dsh-purge 在 pnpm 11 里对 file: 依赖不生效(实测仍报同一句); 而且放行后会真的去执行那个脚本,如果它本身是坏的,你会从 IGNORED_BUILDS 变成 Cannot find module——那就回到第 1 条,卸掉它。

这三条与 issue #204 对应。那条 issue 里的自动摘要把原因写成「本插件依赖了原生模块、pnpm 默认拦了它的 构建脚本」,这个归因是错的——那个 dsh-purge 不是本插件的依赖。

那个自动摘要来自插件市场(dsh-plugin-hub)安装器,已在其 #583 里修正:从 2.2.8 起, 当 Ignored build scripts: 里的包全是 file: / link: 本地依赖时,它不再 归因给被装的那个插件(已发布的插件不可能依赖用户 profile 里的路径),改为提示 用户自己去 profile 的 package.json 里找那些 file: / link: 条目。混合列表 (本地 + registry)仍归给作者,因为其中可能真有插件的传递依赖。

更新

市场里显示几就是几,那是快照;npm view dsh-connect-workbuddy version 与 Releases 才是准的。 2026-10-30 15:00 实测:社区市场索引里本插件还只列到 5.13.1,而 npm latest 当时已是 5.14.1(现在是 5.15.1)。滞后是按包各自发生的——同一份索引里 @changfenhuang/dsh-genui 0.15.0、dsh-client-auto-continue 0.17.0、billion-context 0.1.209 都是新的,所以「别人那个包有新版」不能证明「你这个包也该有」。

手动更新(一条命令):

dsh plugin --profile desktop add dsh-connect-workbuddy@5.15.1
dsh plugin --profile desktop exec dsh-connect-workbuddy doctor   # 第一行就是版本

让 AI 更新:把下面这段整段发给 DSH 里的 AI,它会把版本号、安装与核对都跑完,只在需要改 pnpm 配置时回来问你:

把 dsh-connect-workbuddy 更新到 npm 上的最新版,按下面的步骤来,不要跳步:
1) 先跑 `npm view dsh-connect-workbuddy version` 确认当前 latest 是多少;
2) 用精确版本号安装:`dsh plugin --profile desktop add dsh-connect-workbuddy@<上一步得到的版本>`;
3) 如果报 ERR_PNPM_NO_MATURE_MATCHING_VERSION,不要改用浮动范围硬装(那会静默装回旧版),
   告诉我这个版本还要等多久,或者问我是否要在 profile 的 pnpm-workspace.yaml 里加
   minimumReleaseAgeExclude: dsh-connect-workbuddy;
4) 装完跑 `dsh plugin --profile desktop exec dsh-connect-workbuddy doctor`,
   把第一行的版本号念给我听;
5) 提醒我重启/重载 DSH(bundle 的 host 半边在启动时加载)。
我的 profile 是 desktop。

别让 AI「反复重试到成功」:等待期内重试只会拿到同一个拒绝,pnpm 没有一次性的绕过开关。

怎么确认真的升上去了:doctor 的第一行(WorkBuddy Connect 5.15.1 on …),或 status --json 顶层的 version。不要找 /workbuddy status(本插件注册的斜杠命令是 /codebuddy,用于转发本机 CodeBuddy CLI),也不要在卡片「诊断」栏找版本号(那一栏只有区域 / 域名 / 服务账号 / 登录有效期 / 目录模型数 / 已启用数 / 账号池 / 最近一次失败八行,没有版本行)。装完重启或重载再判断。

细节(市场索引与 pnpm 等待期分别怎么拦住你、两条绕过路径)见 docs/PINNED-UPGRADE-LAG.md,同一份正文也贴成了长期置顶 issue。

平台支持

三个平台都是受支持目标;下列差异是实现事实,不是「尚未支持」的委婉说法。要改这一节,请同时改 docs/WINDOWS.md——那份文件是平台相关约束的登记处。

方面WindowsmacOSLinux
凭据默认目录%LOCALAPPDATA% / %APPDATA% 优先,未设时回落 <home>\AppData(重定向配置文件仍可解析)~/Library/Application Support/CodeBuddyExtension/Data/Public/auth$XDG_CONFIG_HOME(缺省 ~/.config)先,其后追加 $XDG_DATA_HOME(缺省 ~/.local/share)(issue #190)
桌面 App 可执行文件%LOCALAPPDATA%\Programs\WorkBuddy、%LOCALAPPDATA%\WorkBuddy、两个 Program Files(按 WorkBuddy.exe 定位)读 bundle 的 Info.plist 取 CFBundleExecutable(真实值 Electron,不是按 App 名猜);含国际版 WorkBuddy AI.app 与 /Applications 下的子目录不适用(加密凭据场景下 Linux 无对应 App;用 WORKBUDDY_AUTH_FILE 指定凭据)
进程心跳的时间戳来源PowerShell Get-Process 的 StartTime(POSIX 无 ps -lstart 等价物)ps -o lstart=ps -o lstart=
子进程的窗口必须 windowsHide:宿主本身没有控制台(GUI 进程),漏设会让每次探测都在屏幕上闪一个可见的黑框不适用(该选项仅 Windows 生效)不适用
设置写入的瞬时占用会踩到:profile 的配置文件 cordis.patch.yml 被杀毒软件 / OneDrive / 编辑器短暂锁定会拒绝 rename 覆盖,@deepseek-ai/dsh-atomic-write 的重试只在 win32 生效不踩:POSIX 的 rename 直接替换,同一段代码不进重试分支同 macOS
凭据库(vault)的文件权限mode: 0o600 不生效,权限由继承的 DACL 决定 → 写入前后用 icacls 显式加固(只允许 所有者 / SYSTEM / Administrators)。doctor 会回读并报告 protected / exposed / unverified由 mode 0600 生效由 mode 0600 生效
CI 覆盖windows-latest 跑完整套件开发者本机(macOS)ubuntu-latest 跑完整套件

Windows 上需要知道的四个具体点:

  1. 账号改动可能保存失败(profile 的配置文件 cordis.patch.yml 被占用)。插件会在写入后回读校验:确认落盘才显示「已清除」,失败则明确报错并保留原选择。遇到提示时关闭占用该文件的程序后重试。详见已知限制。
  2. 凭据路径已部分经真机验证。候选列表按平台约定推导并有单元测试钉住(含 LOCALAPPDATA / APPDATA 缺省与空白串两种回落)。已在真实 Windows 11 上确认:%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth 与 %LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe 命中,加密凭据的 keyId 与登记向量逐字一致。其余候选仍未经真机逐项验证——若你的凭据或 App 不在默认位置,用 WORKBUDDY_AUTH_FILE / WORKBUDDY_APP_EXECUTABLE 指定,并欢迎回报现场。
  3. 加密凭据(5.6.0 起)需要桌面 App 在场,Windows 与 macOS 同样如此;App 装在自定义目录(如 E:\WorkBuddy\WorkBuddy.exe)时用 WORKBUDDY_APP_EXECUTABLE 指定。
  4. 凭据库里存的是明文 token(issue #183)。插件的 vault 目录 <DSH home>/workbuddy-vault/<region>/<accountId>.json 保存解密后的 accessToken / refreshToken,未加密。Windows 上从 5.16.0 起会在写入前后用 icacls 把该目录限制为所有者 / SYSTEM / Administrators 可读,并用 doctor 的 Credential vault ACL 一行回读确认;但权限收紧不等于加密——管理员、SYSTEM 服务,或已取得你用户令牌的攻击者仍可读取。若你需要磁盘加密,请把 DSH home 放在 BitLocker / FileVault 卷上。POSIX 平台依靠 mode 0600。

在服务器上使用:搬运已登录的凭据

在无桌面环境的服务器(或任何不想安装 WorkBuddy 桌面 App 的机器)上,插件读不到本机凭据。办法是从一台已经登录过 WorkBuddy 的机器上,把凭据目录整体拷过来——插件只读这份文件,不要求 App 在场(前提是凭据本身未加密,见下):

# 1) 在「已登录 WorkBuddy 的机器」上,找到并打包凭据目录
#    macOS:  ~/Library/Application Support/CodeBuddyExtension
#    Linux:  ${XDG_CONFIG_HOME:-~/.config}/CodeBuddyExtension
#    Windows:%LOCALAPPDATA%\CodeBuddyExtension  以及  %APPDATA%\CodeBuddyExtension
tar czf codebuddy-cred.tgz -C <上面的父目录> CodeBuddyExtension

# 2) 把 codebuddy-cred.tgz 传到服务器,解到「插件会读的那个目录」
#    Linux:
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}"
tar xzf codebuddy-cred.tgz -C "${XDG_CONFIG_HOME:-$HOME/.config}"

# 3) 确认插件读到了
dsh plugin --profile web exec dsh-connect-workbuddy status

Linux 会依次看两套 XDG 目录(issue #190 修正了这里原先的说法)。插件的候选目录由 src/auth.ts 的 defaultDesktopAuthDirs() 决定:

平台插件实际读取的凭据目录(按探测顺序)
macOS~/Library/Application Support/CodeBuddyExtension/Data/Public/auth
Linux① ${XDG_CONFIG_HOME:-~/.config}/CodeBuddyExtension/Data/Public/auth ② ${XDG_DATA_HOME:-~/.local/share}/CodeBuddyExtension/Data/Public/auth
Windows%LOCALAPPDATA% 与 %APPDATA% 下的 CodeBuddyExtension\Data\Public\auth
HarmonyOS${WORKBUDDY_USER_HOME:-/storage/Users/currentUser}/.local/share/CodeBuddyExtension/Data/Public/auth

这里原先写着"Linux 上没有 ~/.local/share 这条路"——那是当时的实现,不是平台事实。Data/Public/auth 是数据,按 XDG 惯例本就在 $XDG_DATA_HOME 下,issue #190 的报告机器实测也在那里。

⚠️ 但 ~/.config 仍然排在前面。 顺序决定"不设 WORKBUDDY_AUTH_FILE 时用哪一个",而 ~/.config 是上线以来的行为;Linux 候选从未经真机逐项验证,把新目录提到前面会改变老用户的解析结果而没有证据支撑。这次只做加法。

仍然可以完全不依赖这些默认值:用 WORKBUDDY_AUTH_FILE 直接指向那份 auth 文件(显式指定时该目录下的同级备份文件仍会一并扫描,账号切换照常可用)。

🔐 加密凭据(WorkBuddy 5.6.0 起)不能靠搬运解决。 新版桌面端把 auth 文件里的 token 字段加密了,密钥由 App 的原生绑定在运行时现取、不落盘。因此一台没有 App 的服务器上,即使把加密后的 auth 文件拷过去也读不出来(插件会如实报告未登录,而不会退化成读出一个残缺 token)。可行的做法是从一台仍在运行 5.6.0 之前版本、或凭据未加密的机器上搬运,或者在该服务器上安装 App 以取得密钥。

判断当前处于哪种情况,看 doctor 的两行输出即可:

Desktop auth file: present (…/CodeBuddyExtension/Data/Public/auth/.workbuddy-auth.json)
Encrypted-credential support: unavailable (…)

Desktop auth file: present 说明文件找到了;此时若 status 仍报未登录、而 Encrypted-credential support 为 unavailable(服务器上没有 App),那就是加密凭据 + 缺密钥的场景——搬运救不了它。

🔒 凭据等同于账号:这份文件里是可直接使用的登录态。传输时请走加密通道(scp / 私网),落到服务器后用 chmod 600 收紧权限,别放进任何会被打包或提交的目录。

CLI 路线(可选):让 CodeBuddy 直接干活

默认关闭(cliBridge.enabled: false)。关闭时不注册工具、不注册指令,provider 那条路一行都没变。唯一的例外是下面那颗实时胶囊:它的只读路由与客户端组件无条件注册(客户端要靠它区分「没有回合」和「没有这条路由」),所以关着时每个打开的页面仍会每 5 秒问一次那条本地路由——不碰上游、不花额度,返回一个空数组。

这条路不把 CodeBuddy 当模型提供方,而是当工具:插件提供一个 codebuddy_ask 工具和一条 /codebuddy 指令,由本机已登录的 CodeBuddy CLI 在自己的进程里跑一轮真实任务——读文件、跑 git、改代码——再把结论交回来。

这条路到底绕开了什么,说清楚。 提供方那条路要让请求看起来像 CodeBuddy 客户端才能过网关的渠道校验,4.15.x 起那条校验已经升级成「按提示词中段做精确子串匹配」,靠句读处插零宽空格躲开——这是军备竞赛,而且不再免费:issue #98 里已经出现账号级限制(code 11140,「账号存在异常行为,已被限制使用」)。

2026-10-29 的 #163 复测更新:零宽这条路已经被网关堵死——CN 网关在匹配前会规范化 U+200B,system 开头每个词边界都插零宽仍然 11128。现在改为按句改词(rewriteChannelFingerprintPhrases,落在 neutralizeChannelFingerprints 之前),只改命中句、其余逐字节不动,也不再需要把提示词伪装成第三方 agent。细节见 CHANGELOG 的 Unreleased。

CLI 路线逃掉的是客户端指纹层,不是账号层。它用 CODEBUDDY_AUTH_TOKEN 把本插件 vault 里的同一个 WorkBuddy 账号交给 CLI,所以「跟网关说话的是真的 CodeBuddy CLI 进程、没有身份要伪造、因此账号不会被限制」这句话不成立:身份仍然是同一批账号,11140 是账号级的,同一批账号在 CLI 路线上一样受限。真正被绕开的只有「这条请求的客户端指纹像不像 CodeBuddy」——这一层是真实的收益(渠道校验不会再因为提示词里有句读而拒你),但它不等于「免封」。两条路是互补的容错,不是替代。

前提

  • 本机装了 CodeBuddy CLI(npm 包 @tencent-ai/codebuddy-code,codebuddy 与 cbc 是同一个入口)。装在非标准位置就用 cliBridge.binary 指绝对路径。
  • 它已登录。插件按这个顺序找凭据:环境变量 CODEBUDDY_AUTH_TOKEN → 本插件已经维护的那份 vault($DSH_HOME/workbuddy-vault/<region>/<account>.json)。也就是说,你为了用这个插件登录过的账号,CLI 路线直接复用,不需要再登录一次。(真机验证:CLI 的 getAuthenticationAttributesSync 把 CODEBUDDY_AUTH_TOKEN 排在自己的登录文件之前。)
  • 区域优先于新鲜度:国内账号的令牌对 copilot.tencent.com 有效,国际版的令牌在那边会 401(真机实测:401 Authentication required ... (target: https://copilot.tencent.com))。所以默认顺序是 cn,global,某个区域失败才换下一个。

打开

{
  "cliBridge": {
    "enabled": true
  }
}

其余键都有默认值,不写就是默认。完整的键见下表。改这些键不需要重启宿主:cliBridge 是 volatile 字段,改动只重建这一条路由,不会重载两个 provider 栈。

两个入口

入口谁触发工作目录花谁的额度
codebuddy_ask 工具模型自己决定调用会话工作区;或调用方传的 working_dir;或 cliBridge.workingDirCodeBuddy 的配额 + 那一次回注唤醒的 DSH token(injectResult 默认开)
/codebuddy 指令用户敲自动取当前会话的工作区同上——指令本身不经过模型,但默认会把结论回注给模型

/codebuddy 后面留空 = 让 CodeBuddy 评审当前工作区里未提交的改动(这种调用强制只读的 dontAsk 权限模式,并且要求它自己跑 git diff、只报确实看见的问题);写了字 = 原样把这句话发过去;以 --edit 开头 = 这一轮允许它改文件(acceptEdits,只影响这一次,不改变默认值,例如 /codebuddy --edit 修一下这个空指针;--edit 单独用 = 评审并顺手修掉找到的问题)。

跑完之后结论会回注给会话模型(cliBridge.injectResult,默认开):会话空闲时直接唤醒它起一轮,正在跑就排进它的下一步。这一步真的花 DSH token——把 injectResult 设成 false 才是「完全不碰模型」,那时 /codebuddy 的结论只显示给你看。回注的文本以一行 [dsh-connect-workbuddy · 由 /codebuddy 自动回注,不是用户输入] 开头,免得你(或者模型)把它当成自己打的字。

跑的时候看得见:输入框下方那颗胶囊

工具调用在 DSH 里是一次性的:execute() 什么时候返回,界面才知道发生了什么。中间那几分钟(读文件、跑 git、反复试)本来是一片空白,用户分不清「还在干活」和「已经卡死」。

所以插件在 conversation.composer.dock(输入框下方那一行,和额度胶囊同排)挂了一颗实时胶囊:CodeBuddy · 7 tools · 42s · last: Bash。它由插件自己的只读路由 /plugins/dsh-connect-workbuddy/cli-live 供数,那个路由把宿主侧的 live board(src/cli-bridge/live.ts)序列化出去。

  • 只在真有回合在跑时出现。 没有回合时组件渲染 null,那一行一个像素都不变——这条路由是无条件注册的(开关关着也在),客户端才能把「没有回合」和「没有这条路由」区分开,而不用知道开关状态。
  • 只描述本会话的回合。 live board 是宿主级的(一条路由回答进程里所有会话),所以座位 conversation.composer.dock 声明成 scope: 'session'、inject 第一个参数就是 sessionId,胶囊只留下这一行的行。不这么过滤,A 会话输入框下面就会描述 B 会话(甚至另一个标签页)的回合——一条关于你没在看的目录的字,计数器还在动。宿主没给 sessionId 时(老宿主,或直接挂载组件的测试)不过滤:少一颗诊断读数比假装没有回合更糟;宿主拿不到 agent.id 而标成 sessionId: '' 的行也照样保留,那同样是「真有人在干活」的证据。
  • 不按供应商门控。 额度胶囊只在会话的模型是 WorkBuddy 供应商时出现,因为它报的是那家供应商的额度;这颗胶囊报的是一个本地进程,而 CLI 路线刻意与模型无关——从 DeepSeek 会话里发起也应当看得见。cliBridge.enabled 也不需要单独判断:关着时不会有回合开始,板子是空的,胶囊自然什么都不渲染。
  • 秒数在本地走,不额外发请求。 5 秒轮询一次文档,秒数由 1 秒的本地 tick 从 startedAt 推出来。多个会话共享同一个在途请求(模块级缓存 + inflight 去重),N 个标签页不会变成 N 倍轮询。
  • 时钟偏差按宿主时间校正。 startedAt 是宿主时钟;浏览器时钟慢一分钟就会算出负数。文档里带 serverTime,客户端据此算偏移——而且只在拿到新文档时重算(缓存的文档带的是旧 serverTime,每次都重算会把已经过去的时间从偏移里再减一遍,计数器就会冻在原地)。
  • 工具次数是近似值。 从 CLI 的 NDJSON 里数 tool_use 帧得来,phase 是 starting / running / finishing 三态;lastTool 是最近一次工具名。这些是给人看的进度信号,不是计费依据。
  • 它不会撒谎,也不会赖着不走。 文档里 serverTime 不是有限数字时整份作废(否则标签会印出 NaNs);路由连续三个周期(15s)失败就清空,不会在输入框上留一颗描述着已结束回合的幽灵。同一个会话里多条回合同时跑时它显示最新的那条,+N 表示还有几条,目录只进 tooltip——因为这条 dock 只有一行。

这颗胶囊走的是插件的客户端入口(dsh.client),所以改它需要重新构建 lib/client.js 并重启宿主,和宿主侧代码一样。

权限:默认只读

permissionMode 默认 dontAsk:只读工具(Read/Glob/Grep)自动放行,写文件与执行命令被拒,且不卡住——CLI 会自己结束这一轮并告诉你哪些工具被拒了。

为什么不是 plan(plan 是更保守的候选,也是本次评审建议的默认值):在 -p(headless)下 plan 会发一条 control_request 等 stdin 回授权,而无人值守的调用没有人在终端上点「同意」。不处理这条请求,CLI 会一直等到 60 秒的协议超时,插件则会一路等到 timeoutSecs(默认 900 秒)才失败——实测两次都是这样挂死的。所以默认取 dontAsk,同时在 runner 里把每一条 control_request 明确拒掉。拒绝时要按工具区分:只有 ExitPlanMode 带 interrupt: true(不带则 plan 不结束,实测挂过 70 秒);其它工具(Bash、Write…)不能带——带了会让整轮直接中止(实测 acceptEdits 下拒绝 Bash:is_error、空回复、4.2 秒收场),不带则 CLI 记下这次拒绝、换一个工具继续,整轮正常出结论(实测改用 PowerShell 后 12 秒成功)。想让它真的能改文件,显式设成 acceptEdits。

工作目录:四级阶梯 + 允许根

codebuddy_ask 的工作目录按顺序取:调用方传的 working_dir → 本会话的工作区(session.header.cwd)→ cliBridge.workingDir → (只有 allow_unattended 打开时才)临时目录 → 以上都没有就拒绝并说明三条出路。

不管来源是哪一级,选中的目录都要过三关:必须是绝对路径、必须存在且是一个目录、必须落在允许根之内(本会话工作区、cliBridge.workingDir、cliBridge.allowedDirs)。allowedDirs 是逗号分隔的绝对路径列表,用来放行工作区之外的目录。这样「模型自己编一个 / 或 ~/.ssh 传进来」不会被执行,.. 也走不出去(比较的是 realpath 解析后的路径,并按路径分隔符对齐边界,/repo-evil 不算 /repo 的子目录)。

配置

键默认说明
enabledfalse唯一的开关。false 时这个路由什么都不注册。
binary''CLI 可执行文件的绝对路径。留空时只按已知安装位置探测,找不到就拒绝运行——不会回退到 PATH 上的 codebuddy(那个二进制会拿到 vault 令牌,不能随便认)。
model''传给 --model;留空用 CLI 自己的默认模型。
permissionModedontAskdontAsk(默认,只读)/ plan / default / auto / acceptEdits / bypassPermissions。后两个有写权限。
effort''--effort:minimal / low / medium / high / xhigh / max;留空用 CLI 默认。
maxTurns10单次任务的 CLI 轮数上限(硬上限 100)。
timeoutSecs900单次任务的墙钟超时,秒(硬上限 1800)。
regionOrdercn,global凭据区域的优先顺序。
workingDir''工具入口的默认工作目录,同时也是允许根之一。
allowedDirs''额外允许的根,逗号分隔的绝对路径。
appendSystemPrompt''追加到 CLI 系统提示末尾;留空不追加。
allowUnattendedfalse允许在没有任何工作目录时退回一个临时目录。默认关:CLI 有读写权限,不该在没人指定的目录里跑。
maxReplyChars60000交回给会话模型的最大字符数,超出截断并注明(硬上限 400000)。
injectResulttrue跑完后是否把结论回注给会话模型。设为 false 才完全不碰模型。
sessionContinuityauto同一个 DSH 会话里,下一次调用是否续接上一次的 CodeBuddy 对话。auto = 自动续接(走 --session-id),off = 每次都是独立的新对话。显式传 session_id 参数时以参数为准。
maxTurnsPerSessionHour6每个会话每小时最多几次。0 = 不限。
maxTurnsPerHour20全部会话每小时合计最多几次。0 = 不限。

边界

  • 它看不到这段对话,但看得到自己的上一轮。 CodeBuddy 是另一个 agent,DSH 这边的对话历史它拿不到。不过从 4.14 起,同一个会话里连续两次调用默认是同一条 CodeBuddy 对话(sessionContinuity=auto):第二次知道第一次说了什么、读了哪些文件,不用从零重读仓库。所以「接着刚才那个思路改」现在可行,但它依据的是 CodeBuddy 自己的上下文,不是你在这里说的话——真要它知道你说过什么,还是得写进指令。想回到「每次都独立」就设 sessionContinuity=off。
  • 它有写权限的门槛,没有沙箱。 默认 dontAsk 只读;调成 acceptEdits 之后它真的会改工作区里的文件。允许根只约束它在哪个目录里跑,不约束它在这个目录里做什么。
  • 两套账。 CodeBuddy CLI 的配额和 DSH 的模型额度是两回事。指令本身不花 DSH 模型 token,但默认的回注会让会话模型多跑一轮,那一次是真的 DSH token。描述里写了成本信号,也设了每小时上限,防止「回注 → 模型再问 → 再回注」这种循环。
  • 失败就停,不换着账号试。 只有明确识别出凭据被拒(401 / Authentication required / /login)才会换下一个账号;超时、崩溃、找不到 CLI 都直接报错——十几个 vault 账号逐个重试一遍 900 秒,比失败本身更糟。换号也不再看模型说的话:早期版本把 CLI 的正文也扫了一遍,导致「评审一段包含 HTTP 401 的代码」被误判成凭据失效。
  • 超时会连子进程一起杀。 CLI 会派生 git / npm,只 kill 直接子进程会留下孤儿。POSIX 上按进程组发信号,Windows 上走 taskkill /T。
  • 要重启。 新增的 bundle 与 JS 改动都要重启 DSH 宿主才生效。

命令行

dsh plugin --profile <web|desktop|dsh-tui> exec dsh-connect-workbuddy status   # 登录状态与剩余积分
dsh plugin --profile <web|desktop|dsh-tui> exec dsh-connect-workbuddy doctor   # 凭据路径与宿主诊断
dsh plugin --profile <web|desktop|dsh-tui> exec dsh-connect-workbuddy logout   # 清理插件持有的凭据副本

status 与 doctor 支持 --json 输出机器可读格式。

设备身份相关的环境变量

出站身份档位(wireProfile)与设备头档位(deviceTier)都是卡片配置项,不是环境变量。下面这些是给"配置项够不着的场景"留的旁路:

变量默认说明
WORKBUDDY_DEVICE_TOKEN_FILE未设「虚拟值」档外部 X-Device-Token 的文件源:指向别的工具维护的 token 文件(裸值或 {"token":…} 信封都吃),5 分钟重读、4 KB 上限;token 字节不进日志与 doctor。与卡片配置 deviceToken.file 同义,配置为空时读它
WORKBUDDY_HEADER_TRACE未设设 1 后 doctor 多打一节:每条请求路径将要发出的头名清单(只有名字,永远没有值)——排查"这一档到底发了什么"用
WORKBUDDY_TURING_SDK_DIR未设「真实值」档的 Turing Shield SDK 目录。不设置时插件按桌面端安装形状自动发现(/Applications、%LOCALAPPDATA%、卸载注册表等);装了桌面端却仍报「未找到」时,用它显式指路。卡片「SDK」分区也能直接填(不用改环境变量、保存即生效),本变量优先级更高。取不到真实设备时的行为见下一行
WORKBUDDY_TURING_CHANNEL_ID109144SDK 的渠道号,与桌面端 product.json 一致
WORKBUDDY_TURING_PRODUCT_NAMEWorkBuddySDK 初始化自称的产品名
WORKBUDDY_TURING_PRODUCT_VERSION插件版本SDK 初始化自称的产品版本
WORKBUDDY_TURING_KEYCHAIN_STORAGE未设(=关)设 1 允许 SDK 启用其自身的设备缓存存储;默认关闭——插件不把自己的设备状态写进用户机器的钥匙串
WORKBUDDY_DEVICE_SDK_ENABLED未设(=关)SDK 总开关的旁路,供 doctor 这种读不到插件配置的独立进程使用:1 / true = 开。卡片里的同名开关(deviceSdkEnabled,默认关)是常规入口;两者同义,插件永远显式传值,所以不会打架。关着时不发现 SDK、不起子进程、不预热。排障时想看「这个 SDK 到底能不能用」,用 WORKBUDDY_DEVICE_SDK_ENABLED=1 node lib/bin.js doctor 临时打开
WORKBUDDY_USER_HOME未设(=/storage/Users/currentUser)仅 HarmonyOS:用户可见的家目录根。鸿蒙上 os.homedir() 返回的是应用沙箱(/data/storage/el2/base/haps/entry/files),不能用来拼凭据路径,所以由这条变量或系统固定挂载点决定(issue #190)

为什么这三条命令能在「什么都没装」的机器上跑(issue #47)

该插件把 @deepseek-ai/dsh-atomic-write、@deepseek-ai/dsh-home-paths 等一整套 DSH 包声明为 peer:由宿主提供,插件不自己带一份。这在已启动的 DSH 进程内工作正常(宿主有运行期拦截层负责按宿主安装位置解析这些名字),但 dsh plugin … exec 不是那种环境——它就是在 profile 目录里跑一次 pnpm exec,子进程用普通 Node 解析规则,而 profile 是按 autoInstallPeers: false 建的,peer 根本不会装进去。于是这两个包解析不到。

4.3.1 及更早版本会因此在加载期直接崩溃(ERR_MODULE_NOT_FOUND),且报错文件看起来和命令无关:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@deepseek-ai/dsh-atomic-write'
imported from …/profiles/dsh-tui/node_modules/dsh-connect-workbuddy/lib/host-heartbeat-*.js

原因是打包把 auth / host-heartbeat / at-rest / pi-ai-runtime 合并进了同一个 chunk,lib/bin.js 一 import 它,auth.ts 里那句顶层 import 就被求值了——连只读的 status 也一样。4.3.2 起这两个包改为「宿主优先、按需加载」:宿主在场时仍用宿主的那份(保持与宿主共享同一套跨进程写协议),不在场时回落到插件自带的小实现。所以现在 exec 在干净机器上可用,doctor 也会把当前用的是哪一份打印出来:

Atomic writes: built-in copy (host peer not resolvable here; normal outside a booted DSH)

在仓库里跑(peer 齐全)则显示 host-provided (@deepseek-ai/dsh-atomic-write)。这条守卫写在 tests/host-peers.spec.ts:它直接扫描构建产物,一旦 lib/bin.js 的依赖图里再出现裸包名就变红。

开发

pnpm install
pnpm run check   # typecheck + test + build

本地开发用 link: 安装到 desktop profile(改码后重启 DSH Desktop 生效):

dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy

lib/ 是构建产物且被 gitignore,而 link: 安装不执行包的生命周期脚本(prepare 不会跑)。 于是从一份没构建过的源码检出安装时,package.json 里声明的 main / exports 指向的文件并不存在, DSH 的插件市场据此判定:

声明的入口产物缺失(源码检出或构建被拦),下次启动会失败

这个判定来自 dshmarket 的 entryArtifactExists()——它只读安装目录里的 main 与 exports, 文件在不在就是全部依据。所以:

pnpm install        # prepare 会构建一次(lib/ 出现)
# 或只构建
pnpm run build

改完源码也要再 pnpm run build 一次(或重跑 pnpm install),然后重启 DSH Desktop。 pnpm install 之后仍报同样的话,先跑这一条自查——它打印每个被声明的产物在不在, 并区分「从没构建过」和「manifest 与构建不一致」两种原因:

pnpm run verify:entries                                   # 当前仓库
node scripts/verify-entry-artifacts.mjs <已安装的目录>      # 具体某个安装副本

从 npm 安装(dsh plugin add dsh-connect-workbuddy)不需要这一步:发布包里 lib/ 已经在, prepack 也只负责把它重新生成一遍。

写脚本调用 chatStream() 时注意:它返回的是信封,不是可迭代流

const envelope = await client.chatStream(credential, bodyJson)
if (!envelope.ok) { /* 失败:envelope.status / .kind / .message / .detail */ }
else { await envelope.response.text() /* 或者按 SSE 消费 */ }

先查 envelope.ok 再消费 —— 它返回 { ok: true, response } 或 { ok: false, status, kind, message, detail },而不是一个可以直接 for await 的流。跳过这一步的典型后果是把失败全报成成功(response 是 undefined,读它会抛错或被吞掉,于是「每个模型都成功」)。这条来自 issue #56 报告者的实测提醒,写在这里是为了让下一个人少踩一次同样的坑。

失败信封里的 kind 就是本文提到的失败分类(soft_rate / model_busy / hard_credit / session_dead / policy_reject / not_found / server / client),detail 携带上游的 upstreamCode / requestId / displayMsg。

市场收录与展示

插件已包含可安装的 dsh.bundle manifest,并发布到 npm。社区市场通常从 Awesome DSH Plugin 注册表同步条目;仓库内的条目镜像位于 awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml。条目已于 PR #3812 合并收录(2026-08-30)。

⚠️ 条目合并之后不会自己更新。 #3812 收录时插件还在 2.0.15,注册表上挂的描述因此一直停在当时的能力:没提国内版/国际版双供应商、没提每次刷新读取积分倍率、没提账号池自动换号,也没说明签到是手动的(签到会真实领取奖励,只在用户点按钮时执行)。插件每次有大的能力变化,条目也要另开一次 PR 跟上——注册表评审明确「描述必须属实、会对着代码核」。当前那条更新是 PR #10281。

收录目录规范(依据 contributing.md):

  • 每个插件一个 YAML:data/plugins/<owner>__<repo>.yml,字段为 url(与仓库地址完全一致)、name(owner/repo)、category(有效值之一,本项目为 model)、description.en(必填,以句号结尾)/ description.zh(可选);描述中含 : 必须加引号。
  • 仓库必须声明 dsh.bundle manifest(本项目 dsh.bundle.patch: ./cordis.patch.yml),且被 dsh-plugin topic 标记;只声明 dsh.client 是最常见的被拒原因。仓库创建满 1 天由 CI 自动检查(没有提交数门槛,那条已废除)。
  • 两个 README(README.md / README.zh.md)由脚本生成,不得手工编辑;修改 YAML 后执行 node scripts/generate-readme.mjs 重新生成。
  • 一个 PR 最多收录 3 条;只改自己的条目,不动其他插件。

市场卡片中的截图与 GitHub README 徽章是两套机制:

  • GitHub 徽章由本 README 顶部的 Shields/dshfind 图片链接生成。
  • 市场截图按当前约定由插件自己仓库根目录的 screenshots.json 声明(本项目声明了 screenshots.json,含使用界面截图),注册表 data/screenshots.json 是旧约定下的回退,不再添加新键(本项目已在提交前撤销该文件的改动)。
  • 市场图标在注册表条目里用可选的 icon: 字段声明(本项目指向 icons/dsh-connect-workbuddy-128.png):必须是 GitHub 托管的位图(png/jpg/jpeg/webp),SVG 与第三方图床会被拒;不写则回退到作者头像。它并不是 npm package.json 的通用字段,也不是 README 徽章。

插件卡上那句标题与简介不是本插件渲染的——那是 DSH 插件管理器读的包元数据:它按 locale/<lang>.json 的 meta.title / meta.description 取词(locale/en.json 是锚点,缺了它 zh.json 永远不会被读;exports 里缺 "./locale/*.json" 通配则整个目录被静默跳过),英文回退到 package.json 的 description。因此同一句话在本仓库写在三处,并由 tests/locale-meta.spec.ts 钉死:卡面字典(locale/en.json / locale/zh.json)、英文回退(package.json.description)、市场条目草稿(awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml 的 description.en / description.zh)。改一处就要同改三处。

已知限制

  • 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。

  • 账号切换基于 WorkBuddy 桌面端留下的历史 auth 文件(App 自身的备份产物),并非官方多账号 API。默认行为仍是跟随 App 当前登录;切换账号是显式选项,且历史凭据可能因 App 清理或退出登录而失效。

    • 显式选择的账号若在本机消失(App 更换登录或清理备份文件),插件不会静默改选其他账号——那样会让账单落到另一个账号上。卡片会明确说明「保存的账号已不存在」,此时重新选择一个账号,或点「跟随 App 当前登录」清除该选择即可恢复。重新登录桌面端 App 无法修复这种状态:凭据本身是好的,失效的只是保存下来的账号标识。
    • 从 2.0.0 之前的版本升级而来的用户,设置里可能仍留有旧版的顶层 accountId(迁移来源)。它只作用于其所属区域,且该区域被显式清除(accounts.<区域> = "")后即不再生效——否则清除掉的账号会在下次启动时被静默恢复。卡片会显示每个区域当前是「使用保存的账号」还是「跟随 App 当前登录」:清除后回到的默认账号往往与刚清掉的账号是同一个,没有这行提示就无法分辨清除是否生效。
  • Windows:profile 的配置文件 cordis.patch.yml 被占用时,账号改动可能无法保存。DSH 用「写临时文件 + 覆盖替换」的方式更新该文件,而杀毒软件、OneDrive 等同步盘或正在打开该文件的编辑器会短暂锁住它;重试耗尽后,写入失败不会让 set() 报错。插件因此会在写入后回读校验:确认落盘才显示「已清除」,失败则给出明确错误并保留原来的选择(不会显示一个旧选择默默反驳的假确认)。遇到该提示时,关闭占用该文件的程序后重试即可。

  • Windows / Linux 的凭据默认路径按平台约定推导。已在真实 Windows 11 上确认 %LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth 与 %LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe 命中,其余候选与 Linux 路径仍未经真机验证;必要时可通过环境变量 WORKBUDDY_AUTH_FILE 指定实际位置。

  • 独立 CLI(dsh plugin … exec)不带宿主提供的 peer 包,因此插件在宿主之外运行时会回落到自带的原子写实现(src/host-peers.ts)。它转写了宿主的行为契约(锁文件命名 <file>.lock、Windows 的 EACCES/EBUSY/EPERM 重试与节奏),并且只在这两个包解析不到时才启用——宿主内、以及仓库检出里都仍用宿主/依赖里的那份。doctor 会报告当前用的是哪一份。详见命令行一节的 issue #47 说明。

  • 加密凭据依赖桌面 App 在场:新版桌面端(5.6.0 起,macOS 与 Windows 同样如此)加密了 auth 文件里的 token 字段,插件需要调用该 App 自身的原生绑定才能取回字段密钥。因此 App 若未安装、被卸载,或安装在探测路径之外(Windows 上会自动回退查安装注册表,仍找不到时用 WORKBUDDY_APP_EXECUTABLE 指定),加密文件就读不出来——此时账号会显示为未登录,而不会退化成读出一个残缺的 token。doctor 会报告这项能力是否可用。该密钥是构建期常量,随 App 版本可能轮换;插件每次向本机 App 现取,不缓存到磁盘,所以 App 升级后无需升级插件。

    • App 可执行文件按 bundle 自身声明定位,不按 App 名猜:WorkBuddy 的 macOS bundle 里 CFBundleExecutable 是 Electron(不是 WorkBuddy),因此插件读取 bundle 的 Info.plist 来决定二进制名。国内版 WorkBuddy.app 与国际版 WorkBuddy AI.app 都在候选内;App 被归入 /Applications 子目录(如 /Applications/IDE/WorkBuddy.app)时,插件还会向下扫一层并用 bundle id 确认身份后才使用——不会因为同名的其他 Electron 应用而误启动它。
    • 「未登录」与「登录信息读不出来」是两件事:加密文件读不出时,插件给出的建议是「安装桌面 App / 用 WORKBUDDY_APP_EXECUTABLE 指定其位置」,而不是「重新登录一次」——后者对这种情况无效(凭据本身是好的,缺的是密钥)。
  • 403「request illegal」(code 11140)是服务端策略拒绝,不是登录问题:聊天网关有时会按内容策略直接拒绝一次请求,响应体形如 {"code":11140,"msg":"request illegal","requestId":"…","displayMsg":{"zh":"内容未通过安全审核,请调整后重试。"}}。此前插件把它归为通用 client 错误返回 400,DSH 侧又显示成 AUTH 异常——看起来像「登录坏了」,但重新登录解决不了它:凭据本身是好的,被拒的是这一次请求。现在这类拒绝按 403 原样透传,消息里带官方文案、code 11140 和 requestId(联系官方支持时提供)。写回宿主的 error.code 是 policy_reject 这个稳定类别(而非数值 11140):宿主按类别决定要不要原地重试,而策略拒绝是确定性的——重试只会白烧预算,11140 也不是宿主认得的词;模型行的「测试」也显示「服务端按内容策略拒绝」而非「token 被拒」。遇到时的两个出口:在 WorkBuddy 桌面端用同一账号发同样的内容验证(桌面端同样被拒,说明限制在账号/内容侧);或切换区域/账号后重试。

  • 「模型繁忙」不是「额度已用尽」——但国际版有两个同名模型,选错那个才是真的额度不够:14003(model_busy)是模型级、暂时、与账号和积分无关的限流,换一个模型即可绕开;它以前被本插件写回宿主时译成了 quota_exceeded,而宿主按错误文本做正则分类,恰好命中 @deepseek-ai/dsh-llm 的 isQuotaExceededError → 类别 QUOTA,于是聊天客户端用它自己写死的「当前请求的额度已用尽」替换掉插件的话(那句话从来不是插件写的)。4.12.0 起不再翻译 model_busy,宿主读到的是 RATE_LIMIT:文案保留插件自己那句「这不是账号被限流,也与积分、额度无关」,并因此进入宿主的原地重试集合,由 dsh-llm-retry 按默认的 500 ms→10 s 退避重试几次(有界,非无限)。soft_rate(6004)仍然照旧写成 quota_exceeded:它的重置时刻在几小时后,重试预算换不出答案,必须继续挡在重试集合之外。

    • 但请注意另一件事:国际版有两个「Deepseek-V4.1-Flash」。/v3/config 的 28 个模型里,deepseek-v4.1-flash(x0.00,免费)与 deepseek-v4.1-flash-sg(x0.03,付费)的名称、说明、上下文与能力字段逐字相同,选择器里只有名字后面那个 · x0.00 / · x0.03 能区分。零积分账号选中付费那一个时,网关返回 429 / code 14018「Credits exhausted」——这一次是真的额度不足,宿主的「额度已用尽」是正确的,重试也救不了。所以看到「额度已用尽」时先看模型名的乘数后缀:把国际版的那一个 deepseek 换成带 x0.00 的,或改用 step-5-preview(x0.00,没有付费孪生)。国际版上的免费模型只有 deepseek-v4.1-flash、hy3、step-5-preview 三个。
    • 宿主那套「按文本正则分类」的判定顺序也是这个坑的一部分:401/403 数字优先于一切(会被判成 AUTH),quota 措辞优先于 429(会被判成 QUOTA 并触发上面那段替换),400 数字会被判成 INVALID_REQUEST。写回宿主的错误体因此不能随意带上这些字面量——tests/issue84-model-busy-not-quota.spec.ts 用真实的 pi-ai 客户端打真实的 shim,再把产出的文本交给宿主自己的 isQuotaExceededError() 来钉住这一层。
  • 切换区域会让上下文「变大」,甚至一换区就 code 11115 超限——不是网关多算你的钱:同一份历史在两个区域的计费口径不同,根子在宿主内置的 @earendil-works/pi-ai 重放历史的方式。它给每条 assistant 消息算一次 isSameModel(transform-messages.js:68,判据是 provider 且 api 且 model id 三者全等):不等的消息,其 thinking 会被降级成普通正文(:88–96,并不是丢弃),而正文是网关会数的,thinking 走的是另一个字段(字段名两边都是 reasoning_content)。这一点已由 issue #59 报告者的同区增量实测证实:reasoning 不计费,且同一侧自己的工具结果块按本插件实测的 455.5 tok/1k 就吃掉了它全部计费量的 97.7%。本插件把国内版与国际版注册成两条 provider 路由(workbuddy / workbuddy-global,见工作原理),而它们是同一个 model id——于是换一次区,另一侧产生的全部 thinking 就整批从 thinking 桶挪进正文桶。这就是 issue #59 里「同一份对话历史,国际网关算 1.374M、国内网关算 708K(1.94×)」的成因;反方向切回来则是同一公式的 0.696×。

    • 由此带来三个用户可见的后果:换区后 token 计数可能跳 ±30%~94%,跳多少只取决于另一侧 thinking 的字符数,与历史总长无关;在同一个区域内换模型也会触发同样的降级(isSameModel 的 model 那条腿),所以不换区也可能看到计数变化;跳变叠加在宿主当前的压缩阈值(min(0.8×窗口, 窗口−128000−65536))上,就会让本来不超限的会话一换区立刻撞上 code 11115 prompt is too long。
    • 这是宿主与 pi-ai 之间的一处错配,不是本插件的 bug:不跨 provider/模型重放 thinking 这件事本身是对的(别家模型的思考过程对当前模型没有上下文价值),错在把 provider 当作「是不是同一个模型」的必要条件,而这里两个 provider 字符串指的是同一个模型的两个接入区域。
    • 本插件已在 4.9.0 起从插件侧绕开它:两条路由注册的 model 身份统一为 workbuddy(src/adapter.ts 的 WORKBUDDY_MODEL_IDENTITY),也就是模型自己声明的 provider 字段两边相同,于是 isSameModel 成立、换区不再把 thinking 降级成正文。路由 id 没有合并——它仍是 workbuddy / workbuddy-global,各自指向自己的 shim、目录与凭据,所以一侧故障不会带走另一侧,模型选择器也仍是两组。4.9.0/4.9.1 的这版修复漏了一半,国际版整区不可用(issue #75):上面的说法只对 getModel(provider, id) 这一条按参数查表的路径成立;真正跑聊天的 models.streamSimple(model, …) 第一步是 requireProvider(model) = providers.get(model.provider)(models.js:351–355),查的是模型上盖的那个字段。于是国内版(路由 workbuddy、盖 workbuddy)自洽,国际版(路由 workbuddy-global、盖 workbuddy)在只注册了 workbuddy-global 的注册表里查不到,每一个请求都在发出前就抛 Unknown provider: workbuddy——一次也会失败,且报错里不带任何 WorkBuddy 字样,看起来像宿主坏了。4.9.2 起改为:每条路由把自己的注册表同时登记上「路由 id」和「共享身份」两个键(src/adapter.ts 的 modelIdentityKeysOf),模型上盖的身份仍然两边相同(#59 不回退),而 requireProvider 在国际版自己的注册表里也能查到、并拿到国际版自己的 provider 对象(含它自己的 shim 地址与凭据)。pi-ai 的注册表本来就是 Map(models.js:37 的 providers.set(provider.id, provider),键取的是 provider 对象的 id,不是 profiles 映射的键),所以这是注册表原生支持的事;路由集合、模型选择器、目录与凭据都没有变化,多出来的那个键不出现在任何 UI 里。回归测试 tests/issue75-global-provider-dispatch.spec.ts 走的是宿主的真实构造(createModels + setProvider + streamSimple),并保留一个反证用例固定住旧的失败文案。详见 docs/audit/ISSUE59-OPTIONS.md 的「#75 CORRECTION」一节。
    • 升级后仍可能看到计数变化的部分:在同一区域内换模型仍会降级(model 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的)——降级本身无法消除,但它变成正文这件事可以,见下面 dropForeignThinking 那一条;升级之前就录在历史里的消息带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
    • 降级不只是计数问题:思考会变成正文,模型随后照抄这个格式(issue #147)。 上一条说的是「thinking 被降级」对计费的影响,这条说的是它对内容的影响。pi-ai 对 isSameModel 不成立的消息不是丢弃 thinking,而是把它改写成 {type:'text'}(transform-messages.js:87–90)——也就是答案通道。于是跨 provider 切换后,另一个模型的内部工作笔记会作为本轮回答呈现,模型读到「我过去的思考都是正文」这个形状后,接下来的每一轮都继续把计划写进正文。报告者在一轮真实会话里量到:80 条 assistant 消息中 56 条带正文而自身 reasoning ≤ 1 字符,其中 20 条是英文工作笔记、合计 58,960 字符;整轮 60,984 字符正文里 CJK 只剩 1.37%,而此前 6 轮的可见回复全是中文。它同时把会话从 696,966 推到 758,005 total token(压缩阈值 800,000)。
      • 跨 provider 的 foreign thinking 无条件丢弃;开关 dropForeignThinking 只负责同一路由内换模型(默认关闭):本插件在请求发出前删掉那些「本来会被压成正文」的外来 thinking 块。之所以是删而不是保留:这些块由宿主以 foreignAssistant() 构造,不带 thinkingSignature,即使被当成自己的轮次也无法作为带签名的 reasoning 重放——它们对线上唯一的贡献就是被压成的那段正文。正文块与工具调用一律不动,每条消息的身份也不动。
      • 跨 provider 不设开关:换 provider 后宿主把另一家的 assistant 轮次重建成 dsh-foreign,pi-ai 会把它们的 thinking 摊成 assistant 正文;WorkBuddy 网关正好扫描 assistant 消息,于是整轮以 11128 被拒(issue #163 的现场:14,410 / 4,225 / 5,138 字的 reasoning 被当成正文重放,三次原地救援与三轮自动「继续」全失败)。这些块本来就不可能作为 reasoning 重放,丢弃是纠正而不是偏好,所以不等待用户 opt-in。dropForeignThinking 只把同样的修复扩展到同一路由内换模型(hy3 → deepseek-v4.1-flash,两边都由本插件提供、都盖 provider: workbuddy);那一条的关闭路径仍一个字节都不变。
      • 自动丢弃时写一条 warn(仅在开关关闭时),latestForeignThinking 仍照旧上报,日志里能看出这次请求删掉了哪些块。
      • 顺序很关键:修复跑在 alignAssistantIdentities 之后。国际区自己的轮次到达时也带 api: 'dsh-foreign'(宿主的重放降级路径),先诊断会把它误判成外来轮次并删掉它自己的推理——那正是 #102 的反向回归。tests/issue147-foreign-thinking.spec.ts 用真实 pi-ai 的 transformMessages 钉住了这个顺序。
      • 已经录进历史的坏轮次不会被追溯修复:它们已经作为正文存在会话里,开关只管之后的请求。想彻底摆脱,切换 provider 后新开会话是最直接的办法。
      • 它同时覆盖「同一个区域内换模型」(hy3 → deepseek-v4.1-flash,两者都由本插件提供、都盖 provider: workbuddy)。这时 provider 与 api 两条腿都相等,判据只差 model 一条腿——5.12.0 的判定只比前两条,因此这种切换一个块也检不出,开关看起来无效。判断所需的 model id 由请求入口(stream / streamSimple 的 model.id)传入;不传时行为与 5.12.0 完全一致,卡片那条只读路径因此不受影响(它读的本来就是请求路径填好的 lastForeignThinking)。
    • 真正的修法仍在上游(让 isSameModel 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 src/context-rescue.ts——一旦上游确认超限,就按块折叠历史后重发,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 WORKBUDDY_BODY_REPORT 抓一次 body 组成报告,它只记计数、不记正文。注意它会持续追加:每个区一份 <region>.jsonl,超过 32 MB 自动滚成 .1(只保留一份旧档);排查完请关闭该环境变量并删除报告文件,不要开着它长期跑。

免责声明

请完整读完后自行决定是否安装和使用。(本文开头另有一条风险提示,两条说的是同一件事。)

  • 第三方接入存在账号风险。 官方服务条款对未经授权的第三方接入设有限制。使用本项目可能导致账号或功能受限、账号封禁、服务暂停或终止。能够正常调用、正常扣费,不代表获得官方授权或不存在账号风险。
  • 本项目仅供个人学习和研究使用,仅驱动使用者自己的 WorkBuddy/WorkBuddy AI 账号在本机调用,请勿用于商业用途或超出个人合理使用的场景。
  • 使用本项目不保证服务持续可用、账号不受限制,或积分展示始终准确。开发者无法控制平台的限制、处罚或服务调整;上游一旦收紧限制,插件可能失效。
  • 请充分了解上述风险,并自行决定是否安装和使用。 因使用本项目产生的账号限制、额度损失、服务中断等后果,由使用者自行承担;如不能接受,请使用官方 WorkBuddy 客户端——那始终是最安全的选择。
  • 本项目作者不对任何因使用或滥用本项目产生的直接或间接损失负责。
  • 本项目与腾讯、WorkBuddy、DeepSeek 均无关联,未获其授权或认可;文中出现的名称仅用于描述兼容关系,商标权利归各自所有。

致谢

本项目的实现建立在他人已公开的工作之上。以下内容如实标注来源与许可证,我们对此保持充分尊重:

连接内核的参照

  • corrinehu/dsh-workbuddy-connect(MIT,Copyright (c) 2026 Corrine Hu)— 本项目的主要参照。WorkBuddy 接入 DeepSeek Harness 的完整可行方案由该项目首先验证:桌面端凭据的发现与刷新机制、上游协议与请求头约定、loopback shim 的入站加固、pi-ai provider 的装配方式、以及状态诊断 CLI,均以其为参照。本项目在保留这些已验证能力的基础上重写,着重改善使用体验。
  • Sliverkiss/workbuddy2api(MIT)— WorkBuddy 上游协议(copilot.tencent.com 的 wire behavior)的参照实现,经 dsh-workbuddy-connect 转引。
  • hawklithm/workbuddy2api(MIT,Copyright (c) 2026 Mayer)— 本项目的致谢名单里唯一一项「代码移植」而非「思路参照」:其 src/codebuddy_proxy/dsml_parser.py 是一份代理同一 WorkBuddy/CodeBuddy 上游的 DSML 工具调用解析器,本项目把它移植为 src/dsml-recovery.ts(标签状态机、忽略区域判定、缺包裹修复、参数递归解析、流式缓冲),并在其上补了它没有的两道闸(工具名必须在本次请求声明的工具里、无声明工具完全不做恢复)。三处有意偏离逐条写在该文件头部,义务声明见 THIRD_PARTY_NOTICES.md。

自动续跑的语义参照

  • HsiangNianian/dsh-auto-continue(MIT)— 「连续失败上限」这套语义的来源:发出时计数自增、达到上限停止、成功回合清零、跳过子代理会话。本项目的 channelRejectContinue 只取这层语义,未照搬其引擎。
  • YuJunZhiXue/dsh-purge(MIT)— 三处具体做法:哪些失败码不该续(配额/限流/传输)、收件箱有排队就不插队、以及用消息的 source.kind 区分真人介入与插件自己的消息。

插件呈现与结构的基线

  • dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)— 插件的呈现方式与其保持一致:设置卡片的结构与交互、模型管理(刷新 → 勾选 → 保存)与账号选择模型、只读的 host↔client 路由形态,以及 npm 发布工程。
  • dingminhua/dsh-subagent-default-model(MIT,Copyright (c) 2026 LaoDing)— dsm-* 卡片风格体系、row.* 双语文案键约定与品牌图标的来源,经 dsh-connect-trae 转引。
  • franksong2702/dsh-codex-connect(Apache-2.0)— DSH 插件结构与 provider 注册的参照,经 dsh-workbuddy-connect 转引;其 Apache-2.0 义务在 THIRD_PARTY_NOTICES.md 中单独履行。

说明

以上项目的版权归各自作者所有。本项目采用借鉴设计思路 + 独立实现的方式,未整体复制任何参考项目的源码;关键模块均为独立编写,并在源文件头部注释中标注了所参考的具体项目与模式。若你发现本项目的标注有遗漏或不当之处,请提交 issue,我们会立即更正。

第三方开源依赖

本项目参考的开源项目、其许可证与合规说明,完整记录见 THIRD_PARTY_NOTICES.md。引入新的与 WorkBuddy 接入相关的外部依赖或复用其他项目代码时,请同步更新该文件并遵守对应许可证要求。

许可证

本项目采用 MIT 许可证,版权归属:Copyright (c) 2026 LaoDing。

관련 플러그인