dsh-connect-workbuddy 4.14.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +183 -0
- package/README.en.md +22 -4
- package/README.md +22 -4
- package/lib/bin.js +1 -1
- package/lib/client.js +463 -109
- package/lib/{host-heartbeat-D1WnqQY2.js → host-heartbeat-D313ENPV.js} +952 -53
- package/lib/index.d.ts +182 -1
- package/lib/index.js +434 -10
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,188 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.0 (2026-10-21)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **major**:接口与配置项没有破坏性变更(顶层配置字段一个不少,也没有导出被删),但这一版把「花不花钱」的决定权**第一次交给了模型**。在此之前,本插件每一次花钱都跟一个人工动作绑在一起:一条聊天请求是你发的,模型行的「测试」按钮是你点的。5.0.0 新增的 `workbuddy_draw_image` 是模型自己决定调的——约 5.71 积分一张,而宿主没有「调用前先确认」的机制(`needsApproval` / `toolApproval` 在 `app.asar` 里都不存在)。
|
|
6
|
+
>
|
|
7
|
+
> 按这个仓库自己的判准,这属于**语义级**变更:3.0.0 升 major 是因为账号池让「哪个账号被计费」可以变(同样是零 API 破坏),4.0.0 升 major 是因为一个用户可见的位置消失(同样是零 API 破坏)。一个能自己花钱的能力,比这两者都更需要一个新的大版本号来说清楚。
|
|
8
|
+
>
|
|
9
|
+
> 正因为如此,它以**默认关闭**的形态出现:`regions.<region>.imageToolEnabled` 默认 `false`,关着的时候这个工具**根本不注册**——模型看不到它,也就不会去调用一个注定失败的入口。升级本身不会赋予任何人花钱的能力。
|
|
10
|
+
|
|
11
|
+
### ⚠️ Breaking Change(语义,非接口)
|
|
12
|
+
|
|
13
|
+
- **生图工具默认关闭**。旧版本升级上来的人不会自动拥有 `workbuddy_draw_image`;要发一句话让模型画图,先在卡片「账号」栏勾上「允许模型自主生图」。这是刻意的:这项能力能让模型在你没有逐次确认的情况下花掉积分,所以它必须是你打开的,而不是升级附赠的。
|
|
14
|
+
- **没勾选时工具不存在**,不是「存在但拒绝」。一个注册了却永远拒绝的工具会出现在模型的工具列表里,诱使它发起注定失败的调用;不注册则完全不可见。
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- **设置卡片按「账号 / 模型 / 诊断」分栏** —— 这张卡片原来是一条 3538 像素的长页,把两类提交语义完全不同的事情混排在一根竖线上:账号改动点一下立刻生效,目录改动是草稿、必须按「保存」,而用户在第 1 屏勾完模型后要滚三屏才能找到那个按钮。
|
|
19
|
+
- **三栏**:**账号**(地区说明、积分开关、凭据类错误、账号池的切换/签到/导入导出/日志)、**模型**(模型表、保存/放弃)、**诊断**(只读事实表:区域、上游域名、当前服务的账号名、登录有效期、目录模型数、已启用数、上游最近一次错误的原文)。地区的「国内版 / 国际版」两个 tab 原样保留在最上面,它是这三栏共同的作用域。
|
|
20
|
+
- **切栏不重新拉、不丢草稿**:非活动栏用 `hidden` 留在页面里而不是卸载,所以改了几个勾、切去看一眼余额、再切回来的改动还在,账号池的日志也不重建。`hidden` 同时把非活动栏移出无障碍树,`role="tabpanel"` 的语义成立(用 CSS `display:none` 反而会被后续样式覆盖)。
|
|
21
|
+
- **模型表自己内滚**(`max-height: 520px`),栏高不再由模型条数决定。
|
|
22
|
+
- **签出态不画分栏**:没有登录时这张卡片就是一屏说明,套三个空 tab 只会把唯一的说明藏起来。
|
|
23
|
+
- **诊断栏没有任何控件**,它存在的唯一理由是能被整段贴进问题反馈:账号显示**名字**而不是 id(测试里专门断言不出现 id),上游错误是原文引用、不翻译不概括。
|
|
24
|
+
|
|
25
|
+
- **模型行显示上游的繁忙程度** —— 上游把「这个模型现在多忙」放在一条独立的只读探针里(`POST /v2/chat/busy`,一次最多 50 个 id,只读、免费、不动账号状态),不在模型目录文档中。插件在每次刷新目录时顺带问一次,把等级拼到模型名右侧:`Hy4 preview · x0.29 · 极度拥堵`。
|
|
26
|
+
- **只画 `busy` / `congested` / `unavailable`**:`idle` 有词(空闲)但不画——实测 20 个模型里 7 个空闲,每行都挂徽标就成了装饰而不是警告;`normal` / `unknown` 一律不画。
|
|
27
|
+
- **`unknown` 不等于 `normal`**:探针没回答的模型是未知,不替它猜「一切正常」。
|
|
28
|
+
- **等级不落盘**:目录是能活过重启的事实,负载只是那一分钟的事实。等级只盖在运行时目录上,随进程消失;探针失败只 warn,绝不挡住目录刷新。
|
|
29
|
+
- **实测**:`POST /v2/chat/busy` 在 CLI 与 App 两种身份下答案逐字节相同(所以不需要产品身份);50 个 id 的上限真实存在(62 个 → HTTP 400 `too many models: 62 (max 50)`);19 个模型 186 ms 返回,不拖累刷新。
|
|
30
|
+
|
|
31
|
+
- **补上市场卡面文案(`locale/{en,zh}.json`)** —— 插件卡上的标题与简介**不是本插件渲染的**,而是 DSH 插件管理器读的包元数据:`readPluginMeta()` 先解析 `<包名>/locale/en.json`,再**扫描这个文件所在目录**取出其余语言文件,逐个读 `meta.title` / `meta.description`。此前本仓库只提供了后一半(`package.json` 的 `description`),于是卡面标题落回**裸包名**、英文界面里的简介是一句中文。
|
|
32
|
+
- **`locale/en.json` 是锚点**:读取端遍历的是「英文资源所在目录」,没有它 `zh.json` 永远不会被读——而且不报错。
|
|
33
|
+
- **`exports` 缺 `"./locale/*.json"` 通配 = 静默失败**:解析抛 `ERR_PACKAGE_PATH_NOT_EXPORTED`,读取端把它连同其它失败一起吞掉。
|
|
34
|
+
- **`files` 必须带裸目录 `locale`**,否则发布出去的 tarball 里没有它自己那份文案。
|
|
35
|
+
- 现在中文界面显示「WorkBuddy 模型接入」+中文简介,英文界面显示「WorkBuddy Model Access」+英文简介。`package.json` 的 `description` **保持中文**:它同时是 npm 包页面的简介、GitHub 的仓库简介,和「万一 locale 读不到」的最后兜底——英文文案住在 `locale/en.json` 里,那才是能覆盖兜底的那一份。
|
|
36
|
+
|
|
37
|
+
- **新增生图工具 `workbuddy_draw_image`(国内区,默认关闭,需显式授权)** —— 把 WorkBuddy 官方 CLI 的文生图能力暴露成一个 DSH agent 工具,模型可以按用户的描述直接画一张图。
|
|
38
|
+
- **为什么它是 opt-in,而其他功能不是**:这是本插件唯一「由模型决定花钱」的能力。聊天请求是你发的,「测试」按钮是你点的,而工具调用是模型自己发起的——约 5.71 积分一张,宿主又没有逐次确认的机制。所以开关归你:`regions.cn.imageToolEnabled`,默认 `false`,关着时工具不注册。这与 3.0.0 给账号池的处理是同一个道理(能改变账单落点的功能必须显式打开)。
|
|
39
|
+
- **读侧刻意与同槽其他开关相反**:`regionImageToolEnabledOf` 用 `=== true` 而不是 `!== false`。卡片上的积分读数与供应商开关都是 opt-out(缺省即开,因为藏一行或撤一个 provider 都可以再点回来),而这一项是**授权消费**,缺省必须是关——升级、配置读取失败、手写了个 `"yes"` 都不能被当成同意。
|
|
40
|
+
- **为什么是工具而不是又一个按钮**:生图是「一句话 → 一张图」的能力,用户的自然表达是「帮我画一只在窗台上晒太阳的猫」,不是「打开某个面板、填三个字段、点提交」。做成 agent 工具,模型自己就能决定什么时候画、画什么。
|
|
41
|
+
- **端点与协议照抄官方 CLI 的 ImageService**:`POST {chatBase}/v2/images/generations`,请求体只有 `model` / `prompt` / `size`(`hunyuan-*` 额外带 `n: 1`,`gemini-*` 什么都不加,其它模型带 `response_format: 'b64_json'`)。国内区 `chatBase` 为 `https://copilot.tencent.com`。响应是 `{code, msg, requestId, data:{created, data:[{url}], usage:{credit}}}`。
|
|
42
|
+
- **产物是 24 小时有效的 COS 签名 URL**:插件把 URL 原样返回,并在 `hint` 里写明时效与「要长期保留请及时下载」;上游若返回内联 base64(非 hunyuan 分支),插件落盘到系统临时目录下的 `workbuddy-image-gen/`(`os.tmpdir()`,三个平台各自的位置)再返回 `file://` URL 与本地路径,不让 2 MB 的 base64 灌进对话。
|
|
43
|
+
- **模型目录会多读一次同一份文档**:生图要 `data.models` 里的媒体标签,而聊天目录投影时会把它们丢掉,所以工具**另发一次只读 GET** `/v3/config`(国内区同样走 `/v3` → `/v2` 降级),结果缓存 5 分钟。国内区实测 62 条模型里只有 `hunyuan-image-alpha` 带 `text-to-image`;**国际区一条都没有**——所以工具只在国内区注册(国际区注册出来只会是每次调用都报「没有生图模型」的按钮)。这只读、不花积分,但**确实是一次额外请求**,不记成「零新增请求」。
|
|
44
|
+
- **按「花钱的工具」对待**:缺省即独占执行(不声明 `isConcurrencySafe`),单次超时 180 秒(实测一张 12.9–38.7 秒),失败后 **30 秒冷却**——冷却只看**这一次有没有真的把请求发出去**,由调用点显式置位而不是从异常类型反推:`AUTH` / `RATE_LIMIT` / `NO_ROUTE` / `UPSTREAM` / `EMPTY_RESULT` 这些发生在请求已发出之后的失败才冷却;参数写错、未登录、目录里没有模型,以及**读目录时的一次网络抖动**,一分钱没花,都不进冷却。工具描述里直接写明单价(实测约 5.71 积分/张),让模型自己知道不要为了试错连续调用。
|
|
45
|
+
- **点名的模型不在目录里就直接报错**并列出可用模型,而不是把不存在的 id 发上去换一个上游 404——那样报错只会是 `route config not found`,读者无从知道是名字写错了。
|
|
46
|
+
- **注册跟随国内区开关的活值**:`regions` 是 volatile 字段,用户拨动开关时 loader 只广播、不重新 apply。在 apply 期判一次的话,启动时关着的用户之后打开永远看不到工具,而启动时开着的用户关掉后台工具仍挂着、调用时报「未登录」而不是「区域已关闭」。
|
|
47
|
+
- **没有引入新依赖**:工具定义是手写的 raw JSON Schema `ToolDefinition`,不 import `@deepseek-ai/dsh-tools`(宿主 `Tools.register` 只校验 `output.render` 是函数与 `output.schema` 是受支持的 JSON Schema,`defineTool` 多出来的只是包装)。这样 `lib/bin.js` 的静态可达图里不会多出裸 peer 说明符,`tests/host-peers.spec.ts` 的断言原样成立。
|
|
48
|
+
- **凭据不出宿主进程**:token 只用于这一次 `fetch`,不进对话上下文、不进页面。
|
|
49
|
+
|
|
50
|
+
### Fixes
|
|
51
|
+
|
|
52
|
+
- **撞上排队时不再先把整个账号池试一遍** —— 上游用三个业务码表达排队(`6020` 等待、`6021` 队列满、`6022` 已达上限)。它们在错误分类里此前**没有任何分支**,于是按承载它的 HTTP 状态落成 `server`(500) 或 `soft_rate`(429)——两者都是换号worthy。实测:`HTTP 500` + `code 6020` → `kind=server` → `failover=true`。结果是一次排队会先把账号池挨个试一遍、每个账号都烧一次真实请求,最后才告诉用户「你排在第 8777 位」;而队列跟随的是模型和平台级队伍,换个账号拿到的还是同一个位次。
|
|
53
|
+
- **`model_queue` 成为自己的错误类别**,分类放在状态分支**之前**(排队是业务码,承载它的状态是偶然的:实测同一条件在 200/429/500 三种信封下都出现过)。
|
|
54
|
+
- **`isFailoverWorthy` 排除它**,与 `channel_reject` / `model_busy` 同理。
|
|
55
|
+
- 渲染改由 kind 选择,不再二次解析 body 决定;body 解析只用来取位次与队列长度。
|
|
56
|
+
|
|
57
|
+
- **生图工具的冷却不再把「没花钱的失败」算成花过钱** —— 原实现从异常类型反推:凡不是 `WorkBuddyImageError` 的一律算「花过钱」。方向错了两头:读目录(一次只读 GET)失败抛的是上游的 `envelopeError` 或网络错误,一分钱没花却把工具冷却 30 秒;而真正免费的 `INVALID_ARGS` 全靠码表才被放行。现在由调用点在发起生图前显式置位「已发出」,冷却只看它。
|
|
58
|
+
|
|
59
|
+
- **`fetchMediaModels` 补上 `/v3` → `/v2` 降级** —— 聊天目录早就会在 `/v3` 不答时降级,生图这里没有:上游一旦收起 `/v3`,聊天照常工作而点生图会得到一个原始异常。国际区不降级(它没有对应的历史文档,给过去只会是 CN 形状的数据)。
|
|
60
|
+
|
|
61
|
+
### Tests
|
|
62
|
+
|
|
63
|
+
- **`tests/image-tool-optin.spec.ts`(8 条,新增)**:这一版唯一新增的**授权**契约。读侧把「没说开」与「说了开」分开:`{}`、`{regions:{}}`、`{regions:{cn:{}}}`、显式 `false`,以及手写配置里的 `"yes"` / `1` 全部读成 **关**;只有真正布尔 `true` 读成开;两个区域互不串(CN 开了不意味着国际版也开)。写侧 `nextRegionImageToolEnabled` 只动目标槽的那一个字段,同槽的 `enabled` / `enabledModelIds` 与兄弟区域都保住。另有两组直接钉住升级路径:空配置不是同意、`enabled: true` 也不是同意——**供应商开着和允许模型花钱是两道独立的闸**,把它们混为一谈就等于给所有存量用户发了一个花钱能力。最后一条钉住渲染位置与类名(只在 CN tab、用独立的 `.dsm-workbuddy-image-tool-check`,不与积分开关共用 `.dsm-workbuddy-pref-check`——那是「恰好一个开关」那条测试的选择器契约)。
|
|
64
|
+
- **`tests/image-gen.spec.ts`(35 条)**,夹具是 2026-10-21 国内区 `/v3/config` 的真实片段:模型筛选与去重、`pickImageModel` 的点名/回退/空目录、`buildImageBody` 三分支、`parseImageResponse`(url / b64 / `code !== 0` / 空结果)、`describeImageFailure`(401/403/404/429/500-JSON/非 JSON)、工具的 schema 形状与**返回值键严格落在 `output.schema` 里**、`INVALID_ARGS` 不冷却、`NOT_LOGGED_IN`、`NO_IMAGE_MODEL`、上游失败进冷却、目录 TTL 只读一次、base64 落盘、`render` 的 markdown;以及本轮新增的**两条**:读目录失败不进冷却(同一实例紧接着调用可成功)、读目录失败不被缓存(两次调用都真的到达读取器)。
|
|
65
|
+
- **`tests/busy.spec.ts`(14 条)**:枚举外一律 `unknown`、分批去重(62 → 50+12)、空闲有词但不进名字、等级只盖在匹配的条目上、不修改传入的目录条目;`tests/promo-card` 之外另有 `tests/card-panels.spec.tsx`(6 条,jsdom)钉住分栏:`hidden` 而非卸载、非活动栏仍挂载、诊断栏显示账号名而不显示 id、签出态不画分栏、恰好一个 tab 被标记选中。
|
|
66
|
+
- **`tests/queue.spec.ts`(14 条)**:三个业务码的识别、位置/队列长度/`retry_after` 的读取、嵌套与 `_meta` 信封、14003/402/500 不被误判为队列、永不打印上游的 `queue.*` i18n key、文案;以及本轮新增的**三条**:三种业务码在 200/400/429/500 四种状态下一律 `model_queue`、无关失败不被吞、**两种曾导致换号的承载状态(500 与 429)逐个钉住**。
|
|
67
|
+
- **`tests/upstream.spec.ts`(+2 条)**:`fetchMediaModels` 在 `/v3` 失败时降级到 `/v2` 并上报降级原因;国际区不降级。
|
|
68
|
+
- **`tests/locale-meta.spec.ts`(9 条)**:`locale/en.json` 是锚点、每个 locale 文件名必须是语言 id、`exports` 的通配与 `files` 的裸目录都在、中英两份各自是自己的语言、市场草稿与卡面逐字一致;以及「`package.json` 的 `description` 保持中文」——它记着 `8f3a96e` 把这行从英文改成中文是为了对齐 README 首屏,将来想改回英文必须显式说明。
|
|
69
|
+
- 全量 `npx vitest run` = **107 文件 / 1751 测试全绿**;`tsc` 两个 project 均 exit 0;`npm run build` 成功。
|
|
70
|
+
- **真实联调**(已花 5.71 积分):同一凭据出图成功,HTTP 200、12.9 秒、`credit: 5.71`、产物 2 065 707 字节 PNG、1024×1024、画面与提示词一致。取证与协议细节见 [docs/IMAGE_GEN.md](docs/IMAGE_GEN.md)。
|
|
71
|
+
|
|
72
|
+
### Notes
|
|
73
|
+
|
|
74
|
+
- **README 顶部新增「风险提示」,文末「免责声明」同步重写**(中英双份)。提示本身说两层:① 上游已开始限制非官方客户端请求,本项目**不保证接口长期可用**、上游收紧时可能失效,请自行评估账号风险,官方客户端始终最安全;② 这个插件的目的很普通——**省掉在 WorkBuddy 客户端和 DSH 之间来回切换的麻烦**,接的是你自己机器上已有的登录态,不提供账号、不共享凭据、不绕过计费,每次调用都按上游倍率真实扣费、从不修改账号状态,也不带任何提权或刷额度的能力。它依赖客户端自身的接口(非官方开放 API),所以上游调整时需要跟着调整——这是这类工具的常态,也是这条提示存在的理由。文末的免责声明补齐了第三方接入的账号风险口径(受限/封禁/暂停/终止)、个人使用与商用边界、与腾讯及 DeepSeek 无关联的声明,以及后果自负。
|
|
75
|
+
- **图生图(`/v2/images/edits`)与视频(`/v2/videos/generations`)没有暴露成工具**:两条端点的存在与模型名(`hunyuan-image-alpha-edit`、`minimax-video-h3`、`kling-video-v3`)已由探针确认,但**没有跑通一次真实调用**,所以不给用户一个「看起来能用」的按钮。
|
|
76
|
+
- **「排队后自动重发」没有实现**:识别并把位次说清楚是今天能诚实交付的那一半;重发循环需要排队答案在 OpenAI 兼容通道上的真实信封,没有测到就写等于写一段永不执行的代码。为此准备的三个常量(轮询节奏、12 分钟上限、位次分档)随实现一起撤下,而不是留成只被自己测试调用的导出。
|
|
77
|
+
|
|
78
|
+
## 4.15.1 (2026-10-21)
|
|
79
|
+
|
|
80
|
+
> 版本号是 **patch**:没有接口与配置变更。发往上游的请求在**没有被网关拒绝时仍然逐字节不变**;改动的只有「被渠道校验拒绝之后」那一段——多一级重试。路由、模型、凭据、计费与换号规则未变。
|
|
81
|
+
|
|
82
|
+
### Fixes
|
|
83
|
+
|
|
84
|
+
- **4.15.0 上线后仍有 11128:网关还会按「提示词里的一段长文本」匹配,不只看开头**
|
|
85
|
+
- **真实报文**(2026-10-21 19:1x,国内区、插件 4.15.0、requestId `06e7950a…`):回显「system:You are an AI agent powered by DeepSeek Harness.」(插件已加中性头)——**每条被扫消息的开头都已经在中性头之后**,网关仍然 400。这直接证伪了「只匹配开头」这个 4.13.0 时代的结论。
|
|
86
|
+
- **实测**(同账号 / 同模型 / 同请求头,只改 body;把宿主的真实失败请求逐段二分):
|
|
87
|
+
- 触发点是 system 提示词里 **221 个字符**的一段(DSH 的 Web GUI 段落前两句:「You are interacting with the user through the DeepSeek Harness Web GUI … they mean this GUI.」);
|
|
88
|
+
- 这一段**单独**发就 400;**任何一处**变化就 200(改写一个词、替换引号里的某个词、甚至插入一个零宽空格都行);
|
|
89
|
+
- 在这段外面加前缀或后缀**不影响**结果 ⇒ 精确子串匹配,不是开头规则;
|
|
90
|
+
- 段落里的**单句**各自发都是 200 —— 所以按「开头」设计的归一化永远看不到它。
|
|
91
|
+
- **为什么之前的两级都救不了**:前缀归一化与强制中性头都只动「开头」,而这次命中的是提示词**中间**的一段。
|
|
92
|
+
- **改法**:新增 `perturbChannelFingerprints()` —— 在被扫消息(`system` / `assistant`,含每个 text part)的**句读处**插入 U+200B 零宽空格。对读者不可见、对模型无意义(词一个都没改),但任何跨句的指纹都会被截断。**只在网关已经拒绝之后**使用,正常请求逐字节不变。
|
|
93
|
+
- **重试阶梯**(同一个账号,`channel_reject` 仍不换号):首次失败 → ① `forceNeutralHead`(无处可改就跳过)→ 仍失败 → ② `perturbChannelFingerprints`(无处可改就跳过)。回复里会写明用了哪几级(「并已用强制中性头与一次不可见扰动重试过」)。
|
|
94
|
+
- **代价**:被拒的那一轮最多多两个请求;账号池不记这两次失败(拒的是请求,不是账号)。
|
|
95
|
+
|
|
96
|
+
- **`perturbChannelFingerprints()` 的最终形态**:在被扫消息(`system` / `assistant`,含每个 text part)的**句末标点后**与**分句逗号/分号后**插入 U+200B;对已含 U+200B 的内容幂等跳过;`undefined` 表示「改了也没用」,调用方据此不发请求。`prepareChatBody` 与它的关系是「前者管已知开头,后者管未知的中段」,两者互不覆盖。
|
|
97
|
+
|
|
98
|
+
- **回复会写明用了几级重试**:「并已用强制中性头重试过」「并已用一次不可见扰动重试过」「并已用强制中性头与一次不可见扰动重试过」。这一行只在这两级真的跑过时出现。
|
|
99
|
+
|
|
100
|
+
### Tests
|
|
101
|
+
|
|
102
|
+
- `tests/issue98-channel-gate.spec.ts` 新增 5 条 `perturbChannelFingerprints` 用例:句读处插入、`system`/`assistant` 两种角色都插而 `user`/`tool` 不动、parts 数组与中日文标点、幂等且无改动时返回 `undefined`、畸形 body 不抛错。
|
|
103
|
+
- `tests/issue98-shim.spec.ts` 新增 2 条:开头级无救时**仍会**走扰动级(第二份 body 只多了不可见字符);扰动级把被拒请求救成 200。
|
|
104
|
+
- 既有断言按新阶梯更新:重试全失败时的文案改为同时写出两级。
|
|
105
|
+
|
|
106
|
+
### 真实上游核验
|
|
107
|
+
|
|
108
|
+
- 用**宿主的真实失败请求**(`/tmp/failing-body.json`:10,956 字符 system + 5 条 user + 44 个 tools)在真实国内网关跑完整阶梯:`prepareChatBody` → **400**;`forceNeutralHead` → `undefined`(开头已在中性头之后,证实 4.15.0 那一级在这里是空操作);`perturbChannelFingerprints` → **200**。
|
|
109
|
+
- 同一份 body 上把开头换成 `You are CodeBuddy…` / `You are WorkBuddy…` 仍然 **400**——这是「换成腾讯身份」不能作为解法的直接证据。
|
|
110
|
+
|
|
111
|
+
## 4.15.0 (2026-10-21)
|
|
112
|
+
|
|
113
|
+
> 版本号是 **minor**:有两处用户可见的行为变更 ——(1)`assistant` 历史消息也会被归一化(此前只处理 `system`),(2)渠道校验失败后插件会用「强制中性头」在**同一个账号**上重试一次。**没有被网关拒绝的请求逐字节不变**;被拒的那一次多花一个请求。路由 id、模型 id、凭据、计费与换号规则一律未变。
|
|
114
|
+
|
|
115
|
+
### Fixes
|
|
116
|
+
|
|
117
|
+
- **issue #98:网关的渠道校验也扫 `assistant` 历史消息,而插件只归一到 `system` —— 一个被污染过的会话从此每一步都失败**
|
|
118
|
+
- **实测**(2026-10-21,国内网关 `copilot.tencent.com`,同账号 / 模型 `deepseek-v4.1-flash` / 请求头,**只改 body**;`max_tokens: 8`):
|
|
119
|
+
|
|
120
|
+
| body 里唯一的变化 | 结果 |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `system` 开头 = `You are an AI agent powered by DeepSeek Harness.` | 400 / 11128 |
|
|
123
|
+
| **`assistant` 历史消息开头** = 同一句 | **400 / 11128** |
|
|
124
|
+
| 同一句放在 `assistant` 消息内容**中间** | 200 |
|
|
125
|
+
| 给那条 `assistant` 消息开头补一行 `You are a helpful assistant.` | 200 |
|
|
126
|
+
| `user` 消息开头 = 同一句 | 200(不扫 user) |
|
|
127
|
+
| `tool` 结果开头 = 同一句 | 200(不扫 tool) |
|
|
128
|
+
| `system` 的 content 是 parts 数组、命中在**第 2 个** text part | 400 |
|
|
129
|
+
| 同上,但每个 text part 都补了中性头 | 200 |
|
|
130
|
+
| `system` 开头是当前宿主的 `You are a coding agent powered by …` | 200(不在名单里) |
|
|
131
|
+
|
|
132
|
+
- **它解释了 issue #98 的每一条症状**:只要会话里有一条 `assistant` 消息**开头**是被拦的身份(模型把系统提示词原样复述出来、会话恢复/重放、折叠摘要留下的痕迹),**这个会话之后的每一轮**都会被拒——「单轮跑到 7 分钟多才第一次报错」「继续仍报同一个错」「同一账号另一个会话正常」「重启后 1 分 22 秒失败」都是这一条的形状。换账号、换区域、重装官方客户端都不能改变它,因为被拒的是**请求里的那一行历史**。
|
|
133
|
+
- **改了什么**:`neutralizeChannelFingerprints()` 现在对 `system` 与 `assistant` **两种角色**做同一件事(字符串 content 或 parts 数组里的**每个** text part,命中已知指纹就在最前面补一行中性头)。此前只处理 `system`,所以 `assistant` 里那一行**原样发出**。
|
|
134
|
+
- **为什么可以改历史消息**:这是唯一能救活会话的做法——那一行在**宿主的历史里**,插件只能改它发出去的那一份。改动只有一行前缀,`user` / `tool` / `tool_calls` 结构与工具配对一律不动;宿主自己的会话记录也不改(插件只重写请求体)。
|
|
135
|
+
|
|
136
|
+
- **`system` 的 content 是 parts 数组时,4.13.0 的归一化一个字节都没生效**
|
|
137
|
+
- 归一化只认 `typeof content === 'string'`,于是数组形态的 system 消息(多段文本、图文混排)整个跳过。实测:命中放在**第 2 个** text part 照样 400;每个 part 都补中性头才 200。
|
|
138
|
+
- 现在逐个 text part 判断并前置;非 text part(图片等)与未命中的 part **逐字节不动**,畸形 part 只跳过、不抛错。
|
|
139
|
+
|
|
140
|
+
- **渠道校验失败后,用「强制中性头」在同一个账号上重试一次(结构性兜底)**
|
|
141
|
+
- **不能靠「把名单列全」**:那份名单是网关自己的数据,插件只持有**实测快照**(`UNAPPROVED_CHANNEL_OPENINGS`)。网关加一条,插件就漏一条,而漏掉的那条会让**整个会话**死掉。猜下一条的名字不是修法。
|
|
142
|
+
- **所以改成不看名单**:首次被渠道校验拒绝后,`forceNeutralHead()` 给**每一条** `system` 与 `assistant` 消息(含每个 text part)的最前面补中性头,再发一次。实测:任何被拦的开头,只要被挪到内容开头之外就 200——网关读的是**开头**,不是内容。
|
|
143
|
+
- **同一个账号**:闸读的是请求,不是账号,所以 `channel_reject` 本来就不换号;这一次重试也不换(换号只会烧真实额度,再拿同一个 11128)。
|
|
144
|
+
- **只在需要时**:如果每条开头都已经是中性头(`forceNeutralHead` 返回 `undefined`),不重试——那只会把同样的字节再发一遍。
|
|
145
|
+
- **不记假账**:第一次失败是提示词的问题,不是账号的问题,所以它不进账号池的失败统计;重试若也失败,按最后一次的结果记账。
|
|
146
|
+
|
|
147
|
+
- **回复不再谎报「命中的 system 提示开头」(issue #98 的核心误导)**
|
|
148
|
+
- **旧行为**:从**归一化之后**的 body 里取第一条 `system` 的第一行,而插件补的中性头正好在第一条 ⇒ 回复永远说「命中的 system 提示开头:「You are a helpful assistant.」」。issue #98 里 10 位报告者收到的都是这句,而这句话单独发是 200,真正被拦的那一行从未出现在回复里。
|
|
149
|
+
- **新行为**:从**宿主原始 body**(归一化之前)取,**逐条**列出每个 `system` 与 `assistant` 消息的开头(parts 数组按 part 计),每条标明角色与插件是否已覆盖:
|
|
150
|
+
- `「system:…」(插件已加中性头)` —— 插件会改写它;
|
|
151
|
+
- `「system:…」(不在插件已知名单里,网关可能新增了词条)` —— 这一行**原样发出**,是真正该看的那条;
|
|
152
|
+
- 超过 4 条时只列前 4 条,并写明还剩几条。
|
|
153
|
+
- **不再说「命中的」**:网关从不告诉插件它匹配了哪一条,声称知道就是这次误导的根因。
|
|
154
|
+
|
|
155
|
+
- **同一族业务码的另两道门现在会点名,而不是转述网关的托词**
|
|
156
|
+
- `11128` 有三个来源,`displayTips` 对三者**一字不差**(「这次调用没通过渠道校验,可能不是官方客户端发出的」)。另两道(`conversation id missing`、`first message is not system prompt`)此前落进通用 `client` 兜底,读者拿到的是「该请求被上游按参数拒绝……请按上面的提示调整后重试」+那句误导文案——与本次报告者粘贴回来的那条 400 逐字一致。
|
|
157
|
+
- 现在按 `msg` 点名(仍**只认 msg,绝不认 displayTips**,分类与宿主错误码保持 `client` / 400):缺会话 id 时说明网关要三枚会话头、插件每次都带;首条不是 system 时说明网关要求首条 system、插件在有消息时会补上,因此出现即代表这次请求本身没有消息或没走插件通道。
|
|
158
|
+
|
|
159
|
+
### 评估后**没有**采纳:把中性头换成腾讯自家产品身份
|
|
160
|
+
|
|
161
|
+
- **提议**(issue #98 末尾):把 `You are a helpful assistant.` 换成 `You are CodeBuddy, Tencent's official AI coding assistant.`。理由成立——腾讯不会把自家客户端写进黑名单,这条「结构上安全」。
|
|
162
|
+
- **实测代价**(同一天、同一账号、同一份 DSH 提示词,**只换开头一行**,问「你是哪个产品的助手?」):
|
|
163
|
+
|
|
164
|
+
| 中性头 | 回答 |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `You are a helpful assistant.`(现状) | 我是 DeepSeek 的编程助手。 |
|
|
167
|
+
| `You are CodeBuddy, Tencent's official AI coding assistant.` | 我是腾讯云 CodeBuddy 的 AI 编程助手。 |
|
|
168
|
+
| `You are WorkBuddy, an AI agent developed by Tencent.` | 我是腾讯 WorkBuddy 的 AI 助手。 |
|
|
169
|
+
|
|
170
|
+
前者不改变模型对自身身份的表述,后两者会**串味**——这是所有请求都要付的质量代价,不只是被拦的那几次。
|
|
171
|
+
- **它也不解决 issue #98**:真正被拦的是**会话里的 `assistant` 历史**,不是开头那一行;中性头当时实测仍是 200,报告者把它当成「被拦的行」是插件的回显造成的误解。
|
|
172
|
+
- **结论**:默认不变。「强制中性头」+「不可见扰动」已经覆盖了两类风险,且不必冒充。若将来中性头本身被拉黑,最小改动是把中性头**做成可配置**(默认仍为中性),而不是默认冒充——这留待届时有证据再评估。
|
|
173
|
+
|
|
174
|
+
### Tests
|
|
175
|
+
|
|
176
|
+
- `tests/issue98-channel-gate.spec.ts`(新增):把上表的边界逐条钉住——parts 数组的第 1/第 2 个 part 都归一化、非 text part 与非命中 part 逐字节不动、畸形 part 不抛错、`assistant` 角色同样归一化、`isUnapprovedChannelOpening` 只匹配开头且大小写不敏感、`forceNeutralHead` 覆盖两种角色且空内容不碰、`openingEvidenceOfBody` 按角色与 part 上报并区分「插件已加中性头」与「不在名单里」、`conversation id missing` / `first message is not system prompt` 两道门的判据(code 与措辞两半都要,请求 id 里恰好含数字不算)。
|
|
177
|
+
- `tests/issue98-shim.spec.ts`(新增):挂载**真实的 shim**,只替换上游应答——重试一次即成功(且发出去的第二份 body 在 `system` 与 `assistant` 两种消息前都补了中性头)、重试不换号且第一次失败不记账号账、无法改变时不重试、重试也失败时回复点名的是**宿主的**开头(含 `assistant:`)而不是插件自己的占位行、回复里不再出现「命中的 system 提示开头」与旧兜底句、两道旁门各自点名且不转述误导文案、宿主错误码仍是 `client` / 400。
|
|
178
|
+
- `tests/issue90-channel-reject.spec.ts` 改写:旧的三条按「从归一化后的 body 回显」写的用例方向相反(那正是本次修掉的误导),改为按新契约断言;分类与「不浪费换号」等护栏原样保留。
|
|
179
|
+
|
|
180
|
+
### 真实上游核验
|
|
181
|
+
|
|
182
|
+
- 2026-10-21 共 **200 余次只读对照**(每次 `max_tokens: 8`,未改动任何账号设置):国内网关覆盖开头/中间/parts/角色/多 system/缺头/内容指纹定位(逐段二分 + 前缀增长 + 单点改写的双向验证)等 180 余组,国际网关 20 余组(国际不拦 DSH 身份,只拦 Claude Code;`user` 首条仍是 `first message is not system prompt`)。
|
|
183
|
+
- **内容指纹那一组的判定是「同一份 body 只改一处」**:原样 400、任意一处改动 200、外面加前后缀仍 400,三种同时成立才敢下「精确子串」的结论。
|
|
184
|
+
- 关键结论都经过**双向**确认:既能复现 400,也能用同一份 body 只改开头一行拿到 200(否则无法排除账号、区域或时间窗的影响)。
|
|
185
|
+
|
|
3
186
|
## 4.14.1 (2026-10-21)
|
|
4
187
|
|
|
5
188
|
> 版本号是 **patch**:没有接口、配置项或请求结构的变更,发往上游的请求体**一个字节都没变**。本版只改「哪些上游条目算作模型」这一处判断,以及它带出的一个目录退化路径。
|
package/README.en.md
CHANGED
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
|
|
20
20
|
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in WorkBuddy models to the DSH model picker — **both the CN app and the international WorkBuddy AI app are supported** — with an account pool, selectable model management, and a set of read-only capabilities built around credits: **every catalog refresh reads each model's credit multiplier from upstream** (the `x0.79` figure comes entirely from the upstream catalog; the plugin neither hard-codes it nor guesses), image input is toggled per model, multiple accounts can be switched or fail over automatically through the pool, and daily check-in plus read-only credit overviews shown as compact pills on the card and in the composer. **The CN and international sides are two parallel providers (`workbuddy` / `workbuddy-global`) that can be used at the same time**; the settings card separates them with tabs for convenient management. All it needs is a WorkBuddy sign-in on this machine — or **one sign-in from the browser**.
|
|
21
21
|
|
|
22
|
+
## Risk notice
|
|
23
|
+
|
|
24
|
+
> **WorkBuddy has begun restricting requests from unofficial clients. This project does not promise that these endpoints will keep working; the day upstream tightens its rules, the plugin may stop working. Read the [Disclaimer](#disclaimer) at the end of this file and judge the account risk for yourself. Using the official WorkBuddy client is always the safest choice.**
|
|
25
|
+
|
|
26
|
+
It is worth stating what this plugin is actually for: **saving you the trip back and forth between the WorkBuddy client and DSH.** You are already signed in to WorkBuddy on your own machine; the plugin simply connects that existing sign-in to DSH's model picker so you can use it where you write code — it does not supply accounts, share credentials, or bypass billing. Every call spends **your own account's** quota at the multiplier upstream reports, and the plugin never modifies account state; it carries no privilege escalation or credit-farming capability of any kind.
|
|
27
|
+
|
|
28
|
+
It depends on the WorkBuddy client's own endpoints (not a public API), so the plugin may need to follow along when upstream changes them — that is normal for a tool of this kind, and it is the reason this notice exists. **Judge the account risk yourself before deciding to install.**
|
|
29
|
+
|
|
22
30
|
## Features
|
|
23
31
|
|
|
24
32
|
- **Dual parallel providers (CN + international)** — the CN side registers as the `workbuddy` provider (`GLM-5.3`, `DeepSeek-V4-Pro`, `Kimi-K3`, `MiniMax-M3`, `Hy3`, etc.), the international side as `workbuddy-global` (`GPT-5.6`, `Gemini-3.5-Flash`, `GLM-5.3`, `Kimi-K3`, etc.). **A region's models appear in the DSH model picker when that region has an account**: with accounts on both sides, both rosters are selectable at once, and different sessions can each pick a side without interfering. **A region with no local sign-in lists no models** — it cannot serve a single request, so listing them would offer nothing but failures. Sign in once for that region and its models come back on "detect accounts again" or after a restart; the provider itself stays registered either way, because its settings card is how you sign in (re-detection lives in the pool block). Model names carry the upstream credit multiplier (e.g. `GLM-5.3 · x0.79`), matching WorkBuddy's own model menu.
|
|
@@ -26,7 +34,11 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
26
34
|
- **Switch either provider off on its own** — each tab carries an on/off checkbox (both checked by default). Unchecking one **withdraws that provider from DSH's model picker entirely** (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report `NO_ADAPTER` on its next call; the card states this plainly on the switched-off tab instead of failing silently.
|
|
27
35
|
- **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; **image input is decided by a vendor-verified table**: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's `supportsImages` is a *platform* declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally. **Every model row also carries a “Can disable” (`off`) checkbox**: it decides whether that model gets the “turn thinking off entirely” level. The default comes from a reviewed built-in table (the 9 models that claim the capability but answer HTTP 400 for it start unticked), and you can change it per model — **untick** for a model you do not need the level on. **The 9 tabled models cannot be ticked at all** (their row is marked "refuses off" with an explanatory tooltip): the level is not merely an extra option — pi-ai uses it as the DEFAULT for every request that names no tier, and a compaction summary is exactly such a request, so one tick made every `/compact` for that model fail with HTTP 400 / code 11133 (issue #83). Models outside the table are still decided entirely by your selection and their own declaration; your ticks are saved on first save and recomputed no further.
|
|
28
36
|
- **Campaign badges on model rows** — the upstream model catalog carries campaign information (free trials, night discounts, limited-time free use, subscription-only tiers, and the like). The plugin adds **no extra request** (it reuses the catalog configuration it already fetches when refreshing models) and draws a pill badge right on the model row: the upstream supplies both the colour and the wording, and hovering shows the upstream's campaign text, its start/end dates, and its daily window (e.g. “night 23:00–7:50”). **A badge is a function of the current moment**: it shows only inside the window the upstream declares and disappears on its own when that window closes; a window that crosses midnight is judged in the timezone the upstream declared; when several campaigns hit the same model, only the highest-priority one is drawn. A benefit marker without an end date (such as “subscription only”) stays until upstream withdraws it. **A badge is a hint, not behaviour** — it reports the campaign and discount upstream currently advertises, and makes no choice for you. (Both regions' catalogs ship the campaign fields: read as the app, the CN `/v3/config` carries 8 promotions + 1 tier badge and the global one carries 5 + 1; the evidence and the decision rules are written up in [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md).)
|
|
37
|
+
- **Model load on model rows** — the upstream answers "how busy is this model right now" from a separate read-only probe (`POST /v2/chat/busy`, up to 50 ids per call, free, no account state touched). The plugin asks it once per catalog refresh and appends the level after the multiplier: `Hy4 preview · x0.29 · 极度拥堵`. **Only `busy` / `congested` / `unavailable` are drawn**: `idle` has a word but stays off the row — 7 of 20 models were idle when measured, so a badge on every row would be decoration rather than a warning; `normal` and the probe's own `unknown` draw nothing, and **"we were told nothing" is never reported as "all good"**. **The level is not persisted**: the catalog is a fact that survives a restart, a load level is a fact about that minute, so it is stamped onto the runtime catalog only and dies with the process; a failed probe logs one warning and never blocks the refresh. Measured: the probe answers byte-identically under the CLI and the app identity, the 50-id ceiling is real (62 ids → HTTP 400), and 19 models come back in 186 ms.
|
|
38
|
+
- **Queueing tells you your position** — upstream expresses a queue with three business codes (`6020` waiting, `6021` queue full, `6022` this user already at the maximum). Instead of passing along a raw upstream error, the plugin says "模型排队中:你排在第 8777 位,队列共 9021 个请求;约 7 秒后自动重试". **A queue does not trigger failover**: the line is shared platform-wide and bound to the model, so another account is handed the same position — no account-pool rotation, no extra requests burned. (The upstream's `/v2/chat/queue/status` returns an i18n KEY rather than a sentence, so the plugin never relays its `message` and always uses its own wording.) **The "resend once it is your turn" half is not implemented** — that needs the queue answer's real envelope on the OpenAI-compatible channel, and writing it unmeasured would mean code that never runs.
|
|
39
|
+
- **Tabbed settings card (accounts / models / diagnostics)** — the card used to be a single 3,500-pixel column that mixed two things with completely different commit semantics: **operating accounts** (who is serving, how much is left, switch order, check-in log) and **editing the catalog** (ticking models, context budgets, reasoning tiers, turning thinking off). An account change takes effect on the click; a catalog change is a draft that needs Save — so the Save button often sat three screens away from the thing it saves. Now each tab does one thing, and **switching tabs neither refetches nor drops an unsaved draft** (inactive panels stay mounted and are merely `hidden`, so they also leave the accessibility tree). The third tab, Diagnostics, is a read-only fact table: current region, upstream host, the account actually serving (by NAME, never by id), sign-in validity, how many models the catalog holds, how many are enabled, the pool switch, and the **raw text** of the upstream's most recent error (quoted, never translated or summarised) — its only reason to exist is to be pasted into a bug report, so it contains no control that changes a setting.
|
|
29
40
|
- **Model availability testing, including cooldown recovery times** — working out when a limited model frees up used to mean guessing through repeated failures. Now every model row has a **Test** button that gives you a direct answer for whether the model works right now and, if not, when it comes back. **It sends a real-volume request (about 25k input tokens)**, rather than "ping it and see if it answers" — because the upstream throttles on request **SIZE** (measured: about 20k tokens pass, about 30k are refused), so a small request looks fine while every request in a long conversation may be refused. That is exactly where "the test passes but I am limited" came from. **Where the recovery time comes from**: the upstream's 429 carries **no** `Retry-After` header (and no `X-RateLimit-*`), but it writes the reset time into the response **body** — `{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`. The plugin parses that time and converts it using the UTC offset the message states, so the result does not depend on your machine's timezone; the card then shows “✗ Rate limited · usable again after 2026/9/30 02:30:30”. When none of the sources states a time, the plugin **never invents a countdown** and says plainly that no time was given. A rate limit deliberately does **not** borrow the monthly refresh time, which would tell you to wait weeks for a limit that clears in seconds. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
|
|
41
|
+
- **Image generation tool `workbuddy_draw_image` (CN only, OFF by default)** — hands WorkBuddy's text-to-image capability to the agent: say "draw me a cat sunning itself on a windowsill" in the conversation and the model calls the tool itself, then posts the image back. **It is off by default**, because it is the one capability here where the MODEL decides to spend: about 5.71 credits an image, with no per-call confirmation available in the host. Tick 「Let the model draw images」 under the card's Accounts tab to enable it; while it is off the tool is **not registered at all**, so the model never sees it and cannot attempt a call that would fail. **It is CN-only** — the international catalog lists no image model at all (not unimplemented; upstream has none). **Endpoint and request body are copied from the official CLI's ImageService** (`POST /v2/images/generations`, model `hunyuan-image-alpha`), and the product is a 24-hour COS signed URL the plugin tells you to download in time. **It really costs money**: about **5.71 credits per image** measured, so the tool description states the price outright, a failure carries a 30-second cooldown (while a bad parameter, a missing sign-in or an unknown model costs nothing and does not cool down), and there is no "draw four at once" parameter that could spend uncontrollably. The definition is hand-written raw JSON Schema, **adding no dependency**. See [docs/IMAGE_GEN.md](docs/IMAGE_GEN.md) for the evidence, protocol and live run.
|
|
30
42
|
|
|
31
43
|
**Two things to know.** ① **It really does spend credits** — one measured probe reported `credit` 0.02 (a refused one 429s and costs almost nothing). So testing is **manual and per-model only**: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, **accepting a single model only** and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises each model's **native** window to DSH (no longer clamped to 200K as of 3.5.0), so DSH compacts very late — hitting the throttle in a long session follows directly from that. To make DSH compact sooner, set a “DSH context budget” cap on the model's row (200K / 500K / the model's native window).
|
|
32
44
|
- **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
|
|
@@ -344,6 +356,8 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
344
356
|
- **Marketplace screenshots** follow the current convention and are declared in the plugin's own root [`screenshots.json`](screenshots.json) (this project declares its usage-card screenshot there). The registry's `data/screenshots.json` is a legacy fallback and **no longer accepts new keys** (this project removed its edit before submitting).
|
|
345
357
|
- **Marketplace icons/placeholders** follow each marketplace's own display rules; they are not a generic npm `package.json` field nor a README badge.
|
|
346
358
|
|
|
359
|
+
**The title and description on the plugin card are not rendered by this plugin** — they are **package metadata** the DSH plugin manager reads: it takes its words from `meta.title` / `meta.description` in `locale/<lang>.json` (`locale/en.json` is the anchor, so without it `zh.json` is never read; without the `"./locale/*.json"` wildcard in `exports` the whole directory is skipped **silently**), and falls back to `package.json`'s `description` for English. The same sentence therefore lives in three places in this repository, and [`tests/locale-meta.spec.ts`](tests/locale-meta.spec.ts) pins them together: the card dictionary (`locale/en.json` / `locale/zh.json`), the English fallback (`package.json.description`) and the listing draft (`description.en` / `description.zh` in `awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml`). **Edit one, edit all three.**
|
|
360
|
+
|
|
347
361
|
## Known limitations
|
|
348
362
|
|
|
349
363
|
- The plugin depends on WorkBuddy client endpoints (not an official public API), so a WorkBuddy update may require adjustments.
|
|
@@ -368,10 +382,14 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
368
382
|
|
|
369
383
|
## Disclaimer
|
|
370
384
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
- The
|
|
374
|
-
- This project is
|
|
385
|
+
**Read all of it, then decide whether to install.** (There is also a [risk notice](#risk-notice) near the top; the two say the same thing.)
|
|
386
|
+
|
|
387
|
+
- **Third-party access carries account risk.** The official terms of service restrict unauthorised third-party access. Using this project may lead to account or feature restrictions, **account bans**, or suspension and termination of service. **Working normally and being billed normally do not mean you are authorized, and do not mean there is no account risk.**
|
|
388
|
+
- This project is **for personal study and research only**. It drives only your own WorkBuddy / WorkBuddy AI account locally; **do not use it commercially or beyond reasonable personal use**.
|
|
389
|
+
- Using this project **does not guarantee continuous availability, an unrestricted account, or always-accurate credit figures**. The author cannot control the platform's limits, penalties or service changes; the day upstream tightens its rules, the plugin may stop working.
|
|
390
|
+
- **Understand the risk and decide for yourself whether to install.** Account restrictions, lost credits and service interruptions caused by using this project are **your own responsibility**; if you cannot accept that, use the **official WorkBuddy client**, which is always the safest choice.
|
|
391
|
+
- The author is **not liable for any direct or indirect loss** arising from the use or misuse of this project.
|
|
392
|
+
- This project is **unaffiliated with and unendorsed by Tencent, WorkBuddy, or DeepSeek**. Names appear solely to describe compatibility; **their trademarks belong to their respective owners**.
|
|
375
393
|
|
|
376
394
|
## Acknowledgements
|
|
377
395
|
|
package/README.md
CHANGED
|
@@ -26,16 +26,28 @@
|
|
|
26
26
|
|
|
27
27
|
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 WorkBuddy 账号(**国内版与国际版 WorkBuddy AI 均支持**)接到 DSH 的模型选择器,同时提供账号池、可勾选的模型管理,以及一套围绕积分的只读能力:**每次刷新按上游目录自动读取每个模型的积分倍率**(`x0.79` 这种,倍率完全由上游给出,插件不写死也不猜)、逐模型开关图片输入、多账号切换与账号池自动换号、每日签到、以及卡片与输入框里胶囊式的只读积分读数。**国内版与国际版是两个并行的供应商(`workbuddy` / `workbuddy-global`),可同时使用**;插件设置卡片以 tab 区分两者,方便统一管理。只需本机 WorkBuddy 的登录态,**或在浏览器里登录一次**即可。
|
|
28
28
|
|
|
29
|
+
## 风险提示
|
|
30
|
+
|
|
31
|
+
> **WorkBuddy 已开始限制非官方客户端请求。本项目不保证相关接口长期可用,上游一旦收紧限制,插件可能失效。请先阅读文末[「免责声明」](#免责声明),自行评估账号风险。使用官方 WorkBuddy 客户端始终是最安全的选择。**
|
|
32
|
+
|
|
33
|
+
需要说明的是,这个插件的目的很普通:**省掉在 WorkBuddy 客户端和 DSH 之间来回切换的麻烦**。你已经在自己的机器上登录了 WorkBuddy,插件只是把那份已有的登录态接到 DSH 的模型选择器上,让你在写代码的地方直接用到它——不提供账号、不共享凭据、不绕过计费。每一次调用都消耗**你自己账号**的额度,按上游给出的倍率真实扣费,插件从不修改账号状态,也不附带任何提权或刷额度的能力。
|
|
34
|
+
|
|
35
|
+
它依赖的是 WorkBuddy 客户端自身的接口(非官方开放 API),所以上游调整时插件可能需要跟着调整——这是这类工具的常态,也是这条提示存在的原因。**请自行评估账号风险后再决定是否安装。**
|
|
36
|
+
|
|
29
37
|
## 功能特性
|
|
30
38
|
|
|
31
39
|
- **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `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 自身模型菜单一致。
|
|
32
40
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号池与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
|
|
33
41
|
- **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
|
|
42
|
+
- **设置卡片分三栏:账号 / 模型 / 诊断** —— 原来这张卡片是一列 3000 多像素的长页,把两类互不相干的事情混排在一起:**运维账号**(谁在服务、还剩多少、切换顺序、签到日志)与**编辑目录**(勾模型、上下文预算、推理档、关闭思考)。它们连提交语义都不同——账号改动点一下即生效,目录改动是草稿、必须点保存——所以「保存」按钮常常离它要保存的东西隔着三屏。现在分三栏,一栏只做一件事;**切栏既不会重新拉取,也不会丢掉未保存的目录草稿**(非活动栏留在页面里,只是通过 `hidden` 不可见,因此也不进入无障碍树)。第三栏「诊断」是只读的事实表:当前区域、上游域名、真正在服务的账号名、登录有效期、目录里共几个模型、启用了几个、账号池开关,以及上游最近一次错误的**原文**(原样引用,不翻译、不概括)——这一栏存在的唯一理由就是能被整段贴进问题反馈,所以里面没有任何会改设置的控件。
|
|
34
43
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于当前不需要该档位的模型;**名单内那 9 个模型不可勾选**(行上标「拒绝 off」,悬停有说明):该档位被勾上后不只是多一个选项,pi-ai 会把它当作**所有未指定档位请求的默认值**,压缩摘要请求正属于这一类,勾一次等于让该模型的每一次 `/compact` 都 400 / 11133(issue #83)。名单外的模型仍完全由你的勾选与自身声明决定;首次保存即写入你的勾选,之后不再重算。
|
|
35
44
|
- **模型行上的活动徽标** —— 上游的模型目录里带着活动信息(免费试用、夜间折扣、限时免费、订阅优先等),插件**不新增任何请求**(复用刷新模型时本来就会拉的那份目录配置),直接在模型行上画一枚胶囊徽标:颜色与文案都由上游给出,悬停能看到上游的活动说明、活动的起止日期与每日生效时段(例如「夜间 23:00–7:50」)。**徽标是「此刻」的函数**:活动只在它自己声明的时间窗内显示,窗口一关徽标就自己消失;跨零点的时段按上游声明的时区判断;同一个模型命中多条活动时只画优先级最高的那条。上游没给结束时间的权益标记(如「订阅优先」)则常驻,直到上游把它下架。**徽标只是提示,不改变模型行为**——它标注的是上游此刻给出的活动与折扣,不替你做选择。(两个区域的上游目录都下发活动字段:国内版按 App 身份读 `/v3/config` 时带 8 条促销 + 1 条等级标记,国际版带 5 条 + 1 条;取证与判定规则见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。)
|
|
45
|
+
- **模型行上的繁忙程度** —— 上游另有一条只读探针(`POST /v2/chat/busy`,一次最多 50 个 id,免费、不动账号状态)回答「这个模型此刻多忙」,插件在每次刷新目录时顺带问一次,把等级拼在倍率后面:`Hy4 preview · x0.29 · 极度拥堵`。**只画 `busy` / `congested` / `unavailable`**(轻微拥堵 / 极度拥堵 / 不可用):`idle` 有词但不画——实测 20 个模型里 7 个空闲,每行都挂就成了装饰而不是警告;`normal` 与探针没回答的 `unknown` 一律不画,**「没被告知」不会被当成「一切正常」**。**等级不落盘**:目录是能活过重启的事实,负载只是那一分钟的事实,所以等级只盖在运行时目录上、随进程消失,探针失败只记一条 warn,绝不挡住目录刷新。实测这条探针在 CLI 与 App 两种身份下答案逐字节相同,50 个 id 的上限也真实存在(62 个 → HTTP 400),19 个模型 186 ms 返回。
|
|
46
|
+
- **排队时说清楚你排在第几位** —— 上游用三个业务码表达排队(`6020` 等待、`6021` 队列满、`6022` 已达上限)。撞上它们时,插件不再给一句上游报错,而是「模型排队中:你排在第 8777 位,队列共 9021 个请求;约 7 秒后自动重试」。**排队不会触发换号**:队列跟随的是模型与平台级的队伍,换个账号拿到的还是同一个位次,所以这里既不走账号池也不烧额外请求(上游 `/v2/chat/queue/status` 回的 `message` 是 i18n key 而不是人话,插件因此从不转述它,只用自己的措辞)。**「排到之后自动重发」这一半没有实现**——它需要排队答案在 OpenAI 兼容通道上的真实信封,没测到就不写。
|
|
36
47
|
- **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 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 之后可再用」。三处来源都没说时,插件**不编造倒计时**,如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
|
|
37
48
|
|
|
38
49
|
**两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的**原生窗口**向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限(可选 200K / 500K / 模型原生窗口)。
|
|
50
|
+
- **生图工具 `workbuddy_draw_image`(国内版,默认关闭)** —— 把 WorkBuddy 的文生图能力交给 agent:你在对话里说「帮我画一只在窗台上晒太阳的猫」,模型自己就会调用这个工具,画完把图贴回来。**默认不开启**,因为这是本插件里唯一一项「由模型自己决定花钱」的能力:约 5.71 积分一张,而宿主没有「调用前先确认」的机制。想用就去卡片「账号」栏勾上「允许模型自主生图」;不勾的时候这个工具**根本不会注册**,模型看不到它,也就不会发起注定失败的调用。**它只在国内版可用**——国际版的模型目录里一条生图模型都没有(不是没做,是上游没有)。**端点与请求体照抄官方 CLI 的 ImageService**(`POST /v2/images/generations`,`hunyuan-image-alpha` 模型),产物是 24 小时有效的 COS 签名 URL,插件会提醒你及时下载。**它真的花钱**:实测约 **5.71 积分/张**,所以工具描述里直接写了单价,失败后有 30 秒冷却(但参数写错、没登录、模型不存在这三类一分钱没花,不进冷却),也不提供「一次画四张」这类不可控支出的参数。工具定义是手写的 raw JSON Schema,**没有引入任何新依赖**。取证、协议与联调记录见 [docs/IMAGE_GEN.md](docs/IMAGE_GEN.md)。
|
|
39
51
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
40
52
|
- **对话输入框积分读数(国内版 / 国际版自动切换)** —— 对话输入框左侧工具栏常驻一行可点击的读数(如 `WB CN · 1,072`),**按本会话当前选中的模型自动切换区域**:选中 WB CN 的模型就显示国内版余额,选中 WB AI 的模型就显示国际版余额;用其它 provider 的模型时该读数完全不出现。每 5 分钟自动刷新,也可点开手动刷新。点击读数弹出面板:**按账号列出的余额表格**,点某一行即可切换到该账号(与卡片上的账号选择是同一个设置);**当前不能用的账号会在行内标出原因**(如「被限流」「积分耗尽」),标注口径与账号池表格一致。面板里的余额只在打开时才去查,因此不会让 5 分钟的后台刷新按账号数翻倍。要关掉某一边,就在卡片对应区域的 tab 下取消勾选「在输入框工具栏显示积分读数」;该区域的供应商被关闭时,读数不显示、开关也置灰。同一个开关下面还有「显示位置」这一行:默认在输入框工具栏(与模型选择同一行),改成「输入框下方」后读数落到轮次/步数/token 统计那一行——装了别的插件把工具栏座位占掉时用得上。
|
|
41
53
|
|
|
@@ -354,6 +366,8 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
354
366
|
- **市场截图**按当前约定由**插件自己仓库根目录的 `screenshots.json`** 声明(本项目声明了 [screenshots.json](screenshots.json),含使用界面截图),注册表 `data/screenshots.json` 是旧约定下的回退,**不再添加新键**(本项目已在提交前撤销该文件的改动)。
|
|
355
367
|
- **市场图标/占位图**由具体市场的展示规则决定,并不是 npm `package.json` 的通用字段,也不是 README 徽章。
|
|
356
368
|
|
|
369
|
+
**插件卡上那句标题与简介不是本插件渲染的**——那是 DSH 插件管理器读的**包元数据**:它按 `locale/<lang>.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`)。**改一处就要同改三处。**
|
|
370
|
+
|
|
357
371
|
## 已知限制
|
|
358
372
|
|
|
359
373
|
- 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。
|
|
@@ -380,10 +394,14 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
380
394
|
|
|
381
395
|
## 免责声明
|
|
382
396
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
-
|
|
386
|
-
-
|
|
397
|
+
**请完整读完后自行决定是否安装和使用。**(本文开头另有一条[风险提示](#风险提示),两条说的是同一件事。)
|
|
398
|
+
|
|
399
|
+
- **第三方接入存在账号风险。** 官方服务条款对未经授权的第三方接入设有限制。使用本项目可能导致账号或功能受限、**账号封禁**、服务暂停或终止。**能够正常调用、正常扣费,不代表获得官方授权或不存在账号风险。**
|
|
400
|
+
- 本项目**仅供个人学习和研究使用**,仅驱动使用者自己的 WorkBuddy/WorkBuddy AI 账号在本机调用,**请勿用于商业用途或超出个人合理使用的场景**。
|
|
401
|
+
- 使用本项目**不保证服务持续可用、账号不受限制,或积分展示始终准确**。开发者无法控制平台的限制、处罚或服务调整;上游一旦收紧限制,插件可能失效。
|
|
402
|
+
- **请充分了解上述风险,并自行决定是否安装和使用。** 因使用本项目产生的账号限制、额度损失、服务中断等后果,**由使用者自行承担**;如不能接受,请使用**官方 WorkBuddy 客户端**——那始终是最安全的选择。
|
|
403
|
+
- 本项目作者**不对任何因使用或滥用本项目产生的直接或间接损失负责**。
|
|
404
|
+
- 本项目与**腾讯、WorkBuddy、DeepSeek 均无关联,未获其授权或认可**;文中出现的名称仅用于描述兼容关系,**商标权利归各自所有**。
|
|
387
405
|
|
|
388
406
|
## 致谢
|
|
389
407
|
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, O as legacyWorkbuddyOwnAuthPath,
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, O as legacyWorkbuddyOwnAuthPath, Ot as hostPeersInfo, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, bt as findWorkbuddyAppExecutableWithSource, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, mt as WORKBUDDY_APP_EXECUTABLE_ENV, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-D313ENPV.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|