dsh-connect-workbuddy
把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness —— 每次刷新自动读取每个模型的积分倍率,逐模型开关图片输入,多账号可切换,并带每日签到与只读积分概览(卡片与输入框里的胶囊读数)。
English ·
安装 ·
工作原理 ·
账号池 ·
更新日志 ·
问题反馈
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 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 的工作目录、生图的尺寸或价格上限),所以不挤在账号表格上方那一小条里。第四栏「诊断」是只读的事实表:当前区域、上游域名、真正在服务的账号名、登录有效期、目录里共几个模型、启用了几个、账号池开关,以及上游最近一次错误的**原文**(原样引用,不翻译、不概括)——这一栏存在的唯一理由就是能被整段贴进问题反馈,所以里面没有任何会改设置的控件。
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按核验表判定**:刷新时只有核验表收录为原生多模态的模型才自动勾选,写明纯文本的不勾,**没有资料的 id 一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。核验表分两档证据——厂商文档(13 条)与**上游目录自述**(2 条:`space-bunny`、`minimax-m3.1-flash`,各自的描述里写着「原生多模态」)——但新增 id 仍要求厂商来源,详见 [docs/DESIGN.md](docs/DESIGN.md) 的「图片输入」一节。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于当前不需要该档位的模型;**名单内那 9 个模型不可勾选**(行上标「拒绝 off」,悬停有说明):该档位被勾上后不只是多一个选项,pi-ai 会把它当作**所有未指定档位请求的默认值**,压缩摘要请求正属于这一类,勾一次等于让该模型的每一次 `/compact` 都 400 / 11133(issue #83)。名单外的模型仍完全由你的勾选与自身声明决定;首次保存即写入你的勾选,之后不再重算。
- **模型行上的活动徽标** —— 上游的模型目录里带着活动信息(免费试用、夜间折扣、限时免费、订阅优先等),插件**不新增任何请求**(复用刷新模型时本来就会拉的那份目录配置),直接在模型行上画一枚胶囊徽标:颜色与文案都由上游给出,悬停能看到上游的活动说明、活动的起止日期与每日生效时段(例如「夜间 23:00–7:50」)。**徽标是「此刻」的函数**:活动只在它自己声明的时间窗内显示,窗口一关徽标就自己消失;跨零点的时段按上游声明的时区判断;同一个模型命中多条活动时只画优先级最高的那条。上游没给结束时间的权益标记(如「订阅优先」)则常驻,直到上游把它下架。**徽标只是提示,不改变模型行为**——它标注的是上游此刻给出的活动与折扣,不替你做选择。**同一枚徽标也出现在 DSH 的模型选择器里**:菜单每一行都在模型名之后、队列图标之前画同样的胶囊 —— 菜单行用上游的短词(「夜间」「限免」),输入框旁那个触发器用完整词(「夜间折扣」「独家限免」);判定用的是同一份数据、同一个时钟,因为选模型的那一屏正好是最该看到「现在免费吗」的地方。(两个区域的上游目录都下发活动字段:国内版按 App 身份读 `/v3/config` 时带 8 条促销 + 1 条等级标记,国际版带 5 条 + 1 条;取证与判定规则见 [docs/PROMO_BADGE.md](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 / 模型原生窗口)。
- **生图工具 `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](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](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` 消息,而宿主把时间采样、运行时上下文、`` 追加在你真正输入的那句**之后**,那一列于是常年记的是注入块;开启后把这些注入块**搬到真实输入之前**(只变顺序;另外会剥掉**被记录那一条**真实输入前面的 `mNNNNN` 标记,否则那一列仍然以标记开头而等于没修,更早的消息一律保留自己的标记,因为模型靠它们引用历史)。默认关的理由是它会改变上游会数到的消息顺序、并删掉真实输入的几个前导字符;搬移只匹配宿主自己的注入前缀,而普通对话在没有注入尾块时逐字节不变(连标记也不剥)——这两点各有专门测试钉住。
- **可选:把每个新会话的标题请求改到隐藏轻量模型(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` 里写:
```yaml
channelRejectContinue:
enabled: true
maxConsecutive: 1
text: 继续
```
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
- **扫码添加账号、批量导入导出、逐账号测试/删除** —— 不必先装 WorkBuddy 桌面 App:卡片里的「扫码登录」按钮(国际版为「网页登录」——那边是浏览器登录页,没有二维码;这一行就在「更多设置」上方,点一下立即生效,不需要保存)点开即弹出对话框,并在浏览器里**自动打开**授权页;被浏览器拦截时对话框内提供链接、复制与「重新打开页面」兜底。所有凭据统一落到插件自己的凭据库(`$DSH_HOME/workbuddy-vault//.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 路线](#cli-路线可选让-codebuddy-直接干活)。
## 工作原理
```text
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/ WorkBuddy/ 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` 指定其可执行文件。
## 账号池
一台机器上往往登录过多个 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](https://github.com/dingminhua/dsh-connect-workbuddy/issues/164))。停在旧版的人会反复撞上这个已经修好的 400,而表现是「每次必现、重启也没用」。
>
> 两个会让人升级不到的原因:**① 插件市场的不同索引在同一时刻可能给出三个「最新」**([#164](https://github.com/dingminhua/dsh-connect-workbuddy/issues/164) 实测到 5.12.3 / 5.12.4 / 5.13.1 并存),在旧索引上点「检查更新」看不到新版;**② pnpm 的 24 小时新版本等待期**(`minimumReleaseAge`)会把浮动范围**静默压回上一个版本**。需要立刻修复时请写**精确版本号**:`dsh plugin --profile desktop add dsh-connect-workbuddy@5.14.0`(原因见下方「刚发布的版本请写精确版本号」那条)。
### 受支持的宿主范围
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*` 命名空间,且 rc 宿主上本插件按 `link:` / 手工安装照常加载。
> 这**不是**靠改范围能解决的问题——任何静态范围都覆盖不了「任意 `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](CHANGELOG.md) 的 2.0.6 / 2.0.11 / 2.0.12 各节)。见下方[平台支持](#平台支持)一节了解各平台的具体差异与已知限制。
前置:已安装并登录 WorkBuddy 桌面 App(插件复用 App 的登录状态)。
推荐使用 DSH 插件命令安装 npm 已发布版本:
```sh
dsh plugin --profile desktop add dsh-connect-workbuddy
```
或直接通过 npm 安装:
```sh
npm install dsh-connect-workbuddy
```
安装、更新或卸载 bundle 后,需要重启对应的 DSH 进程。
> ⚠️ **刚发布的版本请写「精确版本号」,不要依赖浮动范围。** pnpm 有**新版本等待期**(`minimumReleaseAge`,默认 24 小时),所以一个版本刚发布不久时,浮动范围会被**静默压回上一个版本**:声明 `^4.4.2` 时它仍解析到 `4.4.2`,而写精确的 `4.4.3` 则立即拿到 `4.4.3`。由于**两种写法都「安装成功」**,表现是「升级命令跑完了、版本却没变」,很容易被误判成发布失败。已实测(隔离目录、pnpm 12.3.4、`--lockfile-only`,未触碰用户环境):
>
> | 目标写法 | 解析到的版本 |
> | --- | --- |
> | 浮动 `^4.4.2` | **4.4.2** ⚠️ |
> | 精确 `4.4.3` | **4.4.3** |
> | 浮动 + `--config.minimum-release-age=0` | **4.4.3** |
>
> 想立刻用上新版,请写精确版本号:`dsh plugin --profile desktop add dsh-connect-workbuddy@4.4.4`。过了等待期之后,浮动范围会自然跟上。
插件在 **Web**、**Desktop**、**TUI** 三种界面下均可运行,按你使用的 profile 选择对应命令:
```sh
dsh plugin --profile web add dsh-connect-workbuddy # Web
dsh plugin --profile dsh-tui add dsh-connect-workbuddy # TUI
```
## 平台支持
三个平台都是受支持目标;下列差异是**实现事实**,不是「尚未支持」的委婉说法。要改这一节,请同时改 [`docs/WINDOWS.md`](docs/WINDOWS.md)——那份文件是平台相关约束的登记处。
| 方面 | Windows | macOS | Linux |
| --- | --- | --- | --- |
| 凭据默认目录 | `%LOCALAPPDATA%` / `%APPDATA%` 优先,未设时回落 `\AppData`(重定向配置文件仍可解析) | `~/Library/Application Support/CodeBuddyExtension/Data/Public/auth` | `$XDG_CONFIG_HOME` 优先,未设时回落 `~/.config` |
| 桌面 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 |
| 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` 指定。
### 在服务器上使用:搬运已登录的凭据
在无桌面环境的服务器(或任何不想安装 WorkBuddy 桌面 App 的机器)上,插件读不到本机凭据。办法是**从一台已经登录过 WorkBuddy 的机器上,把凭据目录整体拷过来**——插件只读这份文件,不要求 App 在场(**前提是凭据本身未加密**,见下):
```sh
# 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 上没有 `~/.local/share` 这条路。** 插件的候选目录由 `src/auth.ts` 的 `defaultDesktopAuthDirs()` 决定,**Linux 只读 `$XDG_CONFIG_HOME`(缺省 `~/.config`)**:
>
> | 平台 | 插件实际读取的凭据目录 |
> | --- | --- |
> | macOS | `~/Library/Application Support/CodeBuddyExtension/Data/Public/auth` |
> | Linux | `${XDG_CONFIG_HOME:-~/.config}/CodeBuddyExtension/Data/Public/auth` |
> | Windows | `%LOCALAPPDATA%` 与 `%APPDATA%` 下的 `CodeBuddyExtension\Data\Public\auth` |
>
> 所以**只把凭据解到 `~/.local/share` 是不会被读到的**——要么解到上面列出的目录,要么用 `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//.json`)。也就是说,**你为了用这个插件登录过的账号,CLI 路线直接复用,不需要再登录一次。**(真机验证:CLI 的 `getAuthenticationAttributesSync` 把 `CODEBUDDY_AUTH_TOKEN` 排在自己的登录文件之前。)
- 区域**优先于新鲜度**:国内账号的令牌对 `copilot.tencent.com` 有效,国际版的令牌在那边会 401(真机实测:`401 Authentication required ... (target: https://copilot.tencent.com)`)。所以默认顺序是 `cn,global`,某个区域失败才换下一个。
### 打开
```jsonc
{
"cliBridge": {
"enabled": true
}
}
```
其余键都有默认值,不写就是默认。完整的键见下表。改这些键不需要重启宿主:`cliBridge` 是 volatile 字段,改动只重建这一条路由,不会重载两个 provider 栈。
### 两个入口
| 入口 | 谁触发 | 工作目录 | 花谁的额度 |
| --- | --- | --- | --- |
| `codebuddy_ask` 工具 | 模型自己决定调用 | 会话工作区;或调用方传的 `working_dir`;或 `cliBridge.workingDir` | CodeBuddy 的配额 + 那一次回注唤醒的 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` 的子目录)。
### 配置
| 键 | 默认 | 说明 |
| --- | --- | --- |
| `enabled` | `false` | 唯一的开关。`false` 时这个路由什么都不注册。 |
| `binary` | `''` | CLI 可执行文件的绝对路径。**留空时只按已知安装位置探测,找不到就拒绝运行**——不会回退到 PATH 上的 `codebuddy`(那个二进制会拿到 vault 令牌,不能随便认)。 |
| `model` | `''` | 传给 `--model`;留空用 CLI 自己的默认模型。 |
| `permissionMode` | `dontAsk` | `dontAsk`(默认,只读)/ `plan` / `default` / `auto` / `acceptEdits` / `bypassPermissions`。后两个有写权限。 |
| `effort` | `''` | `--effort`:`minimal` / `low` / `medium` / `high` / `xhigh` / `max`;留空用 CLI 默认。 |
| `maxTurns` | `10` | 单次任务的 CLI 轮数上限(硬上限 100)。 |
| `timeoutSecs` | `900` | 单次任务的墙钟超时,秒(硬上限 1800)。 |
| `regionOrder` | `cn,global` | 凭据区域的优先顺序。 |
| `workingDir` | `''` | 工具入口的默认工作目录,同时也是允许根之一。 |
| `allowedDirs` | `''` | 额外允许的根,逗号分隔的绝对路径。 |
| `appendSystemPrompt` | `''` | 追加到 CLI 系统提示末尾;留空不追加。 |
| `allowUnattended` | `false` | 允许在**没有任何工作目录**时退回一个临时目录。默认关:CLI 有读写权限,不该在没人指定的目录里跑。 |
| `maxReplyChars` | `60000` | 交回给会话模型的最大字符数,超出截断并注明(硬上限 400000)。 |
| `injectResult` | `true` | 跑完后是否把结论回注给会话模型。设为 `false` 才完全不碰模型。 |
| `sessionContinuity` | `auto` | 同一个 DSH 会话里,下一次调用是否续接上一次的 CodeBuddy 对话。`auto` = 自动续接(走 `--session-id`),`off` = 每次都是独立的新对话。显式传 `session_id` 参数时以参数为准。 |
| `maxTurnsPerSessionHour` | `6` | 每个会话每小时最多几次。`0` = 不限。 |
| `maxTurnsPerHour` | `20` | 全部会话每小时合计最多几次。`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 宿主才生效。
## 命令行
```sh
dsh plugin --profile exec dsh-connect-workbuddy status # 登录状态与剩余积分
dsh plugin --profile exec dsh-connect-workbuddy doctor # 凭据路径与宿主诊断
dsh plugin --profile exec dsh-connect-workbuddy logout # 清理插件持有的凭据副本
```
`status` 与 `doctor` 支持 `--json` 输出机器可读格式。
> **为什么这三条命令能在「什么都没装」的机器上跑(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` 的依赖图里再出现裸包名就变红。
## 开发
```sh
pnpm install
pnpm run check # typecheck + test + build
```
本地开发用 `link:` 安装到 desktop profile(改码后重启 DSH Desktop 生效):
```sh
dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
```
### 写脚本调用 `chatStream()` 时注意:它返回的是**信封**,不是可迭代流
```ts
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](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 注册表同步条目;仓库内的条目镜像位于 [`awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml`](awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml)。**条目已于 PR [#3812](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/3812) 合并收录(2026-08-30)**。
> ⚠️ **条目合并之后不会自己更新。** #3812 收录时插件还在 2.0.15,注册表上挂的描述因此一直停在当时的能力:没提国内版/国际版双供应商、没提每次刷新读取积分倍率、没提账号池自动换号,也没说明签到是**手动**的(签到会真实领取奖励,只在用户点按钮时执行)。**插件每次有大的能力变化,条目也要另开一次 PR 跟上**——注册表评审明确「描述必须属实、会对着代码核」。当前那条更新是 PR [#10281](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/10281)。
**收录目录规范**(依据 [contributing.md](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/contributing.md)):
- 每个插件一个 YAML:`data/plugins/__.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](screenshots.json),含使用界面截图),注册表 `data/screenshots.json` 是旧约定下的回退,**不再添加新键**(本项目已在提交前撤销该文件的改动)。
- **市场图标**在注册表条目里用可选的 `icon:` 字段声明(本项目指向 `icons/dsh-connect-workbuddy-128.png`):**必须是 GitHub 托管的位图**(png/jpg/jpeg/webp),SVG 与第三方图床会被拒;不写则回退到作者头像。它并不是 npm `package.json` 的通用字段,也不是 README 徽章。
**插件卡上那句标题与简介不是本插件渲染的**——那是 DSH 插件管理器读的**包元数据**:它按 `locale/.json` 的 `meta.title` / `meta.description` 取词(`locale/en.json` 是锚点,缺了它 `zh.json` 永远不会被读;`exports` 里缺 `"./locale/*.json"` 通配则整个目录被**静默**跳过),英文回退到 `package.json` 的 `description`。因此同一句话在本仓库写在三处,并由 [`tests/locale-meta.spec.ts`](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`)。它转写了宿主的行为契约(锁文件命名 `.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 组成报告,它只记计数、不记正文。**注意它会持续追加**:每个区一份 `.jsonl`,超过 32 MB 自动滚成 `.1`(只保留一份旧档);排查完请关闭该环境变量并删除报告文件,不要开着它长期跑。
## 免责声明
**请完整读完后自行决定是否安装和使用。**(本文开头另有一条[风险提示](#风险提示),两条说的是同一件事。)
- **第三方接入存在账号风险。** 官方服务条款对未经授权的第三方接入设有限制。使用本项目可能导致账号或功能受限、**账号封禁**、服务暂停或终止。**能够正常调用、正常扣费,不代表获得官方授权或不存在账号风险。**
- 本项目**仅供个人学习和研究使用**,仅驱动使用者自己的 WorkBuddy/WorkBuddy AI 账号在本机调用,**请勿用于商业用途或超出个人合理使用的场景**。
- 使用本项目**不保证服务持续可用、账号不受限制,或积分展示始终准确**。开发者无法控制平台的限制、处罚或服务调整;上游一旦收紧限制,插件可能失效。
- **请充分了解上述风险,并自行决定是否安装和使用。** 因使用本项目产生的账号限制、额度损失、服务中断等后果,**由使用者自行承担**;如不能接受,请使用**官方 WorkBuddy 客户端**——那始终是最安全的选择。
- 本项目作者**不对任何因使用或滥用本项目产生的直接或间接损失负责**。
- 本项目与**腾讯、WorkBuddy、DeepSeek 均无关联,未获其授权或认可**;文中出现的名称仅用于描述兼容关系,**商标权利归各自所有**。
## 致谢
本项目的实现建立在他人已公开的工作之上。以下内容如实标注来源与许可证,我们对此保持充分尊重:
### 连接内核的参照
- [corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)(MIT,Copyright (c) 2026 Corrine Hu)— **本项目的主要参照**。WorkBuddy 接入 DeepSeek Harness 的完整可行方案由该项目首先验证:桌面端凭据的发现与刷新机制、上游协议与请求头约定、loopback shim 的入站加固、pi-ai provider 的装配方式、以及状态诊断 CLI,均以其为参照。本项目在保留这些已验证能力的基础上重写,着重改善使用体验。
- [Sliverkiss/workbuddy2api](https://github.com/Sliverkiss/workbuddy2api)(MIT)— WorkBuddy 上游协议(`copilot.tencent.com` 的 wire behavior)的参照实现,经 `dsh-workbuddy-connect` 转引。
- [hawklithm/workbuddy2api](https://github.com/hawklithm/workbuddy2api)(MIT,Copyright (c) 2026 Mayer)— **本项目的致谢名单里唯一一项「代码移植」而非「思路参照」**:其 `src/codebuddy_proxy/dsml_parser.py` 是一份代理同一 WorkBuddy/CodeBuddy 上游的 DSML 工具调用解析器,本项目把它移植为 `src/dsml-recovery.ts`(标签状态机、忽略区域判定、缺包裹修复、参数递归解析、流式缓冲),并在其上补了它没有的两道闸(工具名必须在本次请求声明的工具里、无声明工具完全不做恢复)。三处有意偏离逐条写在该文件头部,义务声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
### 自动续跑的语义参照
- [HsiangNianian/dsh-auto-continue](https://github.com/HsiangNianian/dsh-auto-continue)(MIT)— 「连续失败上限」这套语义的来源:发出时计数自增、达到上限停止、成功回合清零、跳过子代理会话。本项目的 `channelRejectContinue` 只取这层语义,未照搬其引擎。
- [YuJunZhiXue/dsh-purge](https://github.com/YuJunZhiXue/dsh-purge)(MIT)— 三处具体做法:哪些失败码不该续(配额/限流/传输)、收件箱有排队就不插队、以及用消息的 `source.kind` 区分真人介入与插件自己的消息。
### 插件呈现与结构的基线
- [dingminhua/dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae)(MIT,Copyright (c) 2026 LaoDing)— 插件的呈现方式与其保持一致:设置卡片的结构与交互、模型管理(刷新 → 勾选 → 保存)与账号选择模型、只读的 host↔client 路由形态,以及 npm 发布工程。
- [dingminhua/dsh-subagent-default-model](https://github.com/dingminhua/dsh-subagent-default-model)(MIT,Copyright (c) 2026 LaoDing)— `dsm-*` 卡片风格体系、`row.*` 双语文案键约定与品牌图标的来源,经 `dsh-connect-trae` 转引。
- [franksong2702/dsh-codex-connect](https://github.com/franksong2702/dsh-codex-connect)(Apache-2.0)— DSH 插件结构与 provider 注册的参照,经 `dsh-workbuddy-connect` 转引;其 Apache-2.0 义务在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中单独履行。
### 说明
以上项目的版权归各自作者所有。本项目采用**借鉴设计思路 + 独立实现**的方式,未整体复制任何参考项目的源码;关键模块均为独立编写,并在源文件头部注释中标注了所参考的具体项目与模式。若你发现本项目的标注有遗漏或不当之处,请提交 issue,我们会立即更正。
## 第三方开源依赖
本项目参考的开源项目、其许可证与合规说明,完整记录见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。引入新的与 WorkBuddy 接入相关的外部依赖或复用其他项目代码时,请同步更新该文件并遵守对应许可证要求。
## 许可证
本项目采用 [MIT](LICENSE) 许可证,版权归属:**Copyright (c) 2026 LaoDing**。