dsh-connect-workbuddy 5.8.5 → 5.10.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 +83 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +940 -597
- package/lib/{host-heartbeat-4Ao_1fKu.js → host-heartbeat-_EeM2YDF.js} +227 -30
- package/lib/index.d.ts +65 -5
- package/lib/index.js +249 -34
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.10.0 (2026-10-25)
|
|
4
|
+
|
|
5
|
+
> 特性版本:`workbuddy_draw_image` 从「只在国内区注册、只能文生图」扩成「国内区与国际区都能用、文生图 + 改图(图生图)」,卡片「能力」栏新增该区媒体模型的只读清单。**请求路径、provider 路由、计费口径与凭据处理一律未变**:改图打的是官方 CLI `ImageService.editImage` 早就用的 `POST /v2/images/edits`,选模型仍然只看该区 `/v3/config` 的媒体标签。
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **国际区账号也能用 `workbuddy_draw_image`(逐区授权,国内优先)**
|
|
10
|
+
- **起因**:此前文档与代码都写着「只有国内区有生图模型,国际区一条都没有」,工具因此硬编码成 `regionEnabled(config, 'cn')` 才注册。那句话是 **2026-10-21 的观测**,已经过期。
|
|
11
|
+
- **2026-10-25 跨区实测**:国际区 `GET /v3/config` 29 条模型里 `hy-image-3.5` **同时**带 `text-to-image` 与 `image-to-image`;同一份凭据 `POST /v2/images/generations` + `hy-image-3.5` → HTTP 200、`usage.credit = 0.03`、产物是 `ap-singapore` 的 COS 签名 URL。`buildImageBody()` 的「其它」分支(带 `response_format: 'b64_json'`)被原样接受,**协议层一行都不用改**。同一轮探针确认国际区 `/v2/api-keys` 回 503、`/agenttool/v1/tempkey` 回 500(而国内区后者回 200)——即 3D 那条临时密钥链路在国际区整个不存在,与生图无关。
|
|
12
|
+
- **修法(区域选择集中到一处)**:`src/index.ts` 新增 `imageToolRegionOf(config)`——按 `REGION_KEYS`(`cn` 在前)遍历,跳过被关闭的供应商,返回第一个**既启用又打开了「允许模型自主生图」**的区域,都不满足返回 `undefined`。`syncImageTool()` 的注册判定改为 `imageToolRegionOf(value) !== undefined`;`resolveCredential` 改为每次调用时读当前配置、遍历被授权的区逐个 `resolve()`,某区未登录就试下一个区,全失败返回 `undefined`。**CN 优先是唯一向后兼容的顺序**:升级前这个工具只能走国内区,升级不能悄悄把老用户的支出挪到另一个网关。
|
|
13
|
+
- **文案层同步拆除「只有国内区」的前提**:`ImageToolDeps` 删掉 `regionLabel` 字段;工具描述改为「两个区都可用 + 各自的模型名 + 单价随区与模型不同 + 以返回的 `credit` 为准」;`NOT_LOGGED_IN` 改为「要么两个区都没登录,要么已登录的那个区没打开该开关」;`hint` 不再写死 5.71,有 `credit` 时印本次实扣;`describeImageFailure()` 的 404 文案与 `pickImageModel()` 的空目录文案不再断言「国际区没有生图模型」。
|
|
14
|
+
- **卡片**:`imageToolPreference` 不再包在 `activeRegion === 'cn' ? … : null` 里,两个 tab 都显示;勾选仍然只写**当前这一区**的 `imageToolEnabled`,两区都关着时工具根本不注册。
|
|
15
|
+
- **价格口径(README 中英各改一段)**:5.71(国内 `hunyuan-image-alpha`)与 0.03(国际 `hy-image-3.5`)是**两个模型 × 两个区域**同时变化的结果,不是同一张图的跨区差价;两区的积分池彼此独立,两个数字之间没有任何汇率含义。
|
|
16
|
+
- **改图(image-to-image):同一个工具,给了 `image` 就切模式**
|
|
17
|
+
- **起因**:`/v2/images/edits` 与 `hunyuan-image-alpha-edit` 的存在早在 4.13.2 取证时就写在 `docs/IMAGE_GEN.md` 的「没做的事」里——没做,是因为当时没跑通一次真实调用。这次跑通了,才把它接出来。
|
|
18
|
+
- **模式由「有没有输入图」唯一决定,不新增 `mode` 参数**:多一个参数就多一个「`mode=edit` 但没给图」的自相矛盾状态。`image` 存在 → `editImage` + `image-to-image` 组;不存在 → `generateImage` + `text-to-image` 组。
|
|
19
|
+
- **`model` 的校验跟着模式走**:`pickMediaModel(models, requested, tag)` 按当次标签组挑模型。拿文生图模型点名去改图会直接报「目录里没有图生图模型「xxx」。可用:…」,而不是把不匹配的 id 发上去换一个上游 404——那样报错只会是 `route config not found`,读者无从知道是名字写错了。`selectMediaModels` 同时接受单个标签与标签数组,目录一次读回四类媒体标签(`MEDIA_TAGS`),为将来的视频留位。
|
|
20
|
+
- **`image` 收官方 CLI `processImageInput` 的全部四种形态**:本地绝对路径、`file://`、`http(s)`(宿主侧下载、30 秒超时)、data URL,另加裸 base64(长度 ≥ 100、无路径分隔符、字符集合法才认,否则当路径)。**一律先归一成 `data:<mime>;base64,…`** —— 上游唯一接受的形态。mime 判定顺序:HTTP `Content-Type` 的 `image/*` → 按扩展名 → 按魔数嗅探(PNG / JPEG / GIF / WEBP / BMP,兜底 `image/png`)。
|
|
21
|
+
- **不做缩放、裁剪、格式转换**(`MAX_INPUT_IMAGE_BYTES = 8 MB` 只是上限):用户给什么就传什么。悄悄改输入图是比失败更糟的行为。
|
|
22
|
+
- **输入图的所有毛病都在「请求没发出去」之前**,所以统一 `INVALID_ARGS`、不进 30 秒冷却、不花积分:读不到文件、下载失败、data URL 格式不对、空文件、超 8 MB。紧接着再调一次仍能立刻执行——判据与既有规则一致(冷却只看「请求是否真的发出去了」)。
|
|
23
|
+
- **返回值与 `render` 区分两种模式**:output schema 新增必填 `mode`(`'generate'` | `'edit'`),`render` 显示「改图 · 模型:`xxx`」、markdown alt 由「WorkBuddy 生图」改为「WorkBuddy 文生图/改图」。
|
|
24
|
+
- **上游**:`src/upstream.ts` 新增 `editImage(credential, {model, prompt, size?, image, inputFidelity?})`,`src/image-gen.ts` 新增 `buildEditBody()`(在 `buildImageBody` 之上追加 `image` 数组,`input_fidelity` 仅在非空时写入)。
|
|
25
|
+
- **卡片的「能力」栏新增媒体模型只读清单**
|
|
26
|
+
- `POST /plugins/dsh-connect-workbuddy/media-models`(`WORKBUDDY_MEDIA_MODELS_PATH`):405 非 POST → 403 外部 Origin → 503 未接线 → `region` 查询参数(缺省 `cn`)→ 200 `{models:[{id,name,tags}]}`。空清单是**合法回答**(网关不发布媒体模型),不是错误——卡片为它单独写了一句话,而不是显示一个失败。
|
|
27
|
+
- **为什么单独一条端点**:`models/refresh` 返回的是**聊天**目录,投影时媒体条目被 `selectCliModels` 丢掉,刷新它永远看不到图像模型。
|
|
28
|
+
- 宿主侧 5 分钟缓存(复用 `MEDIA_MODEL_TTL_MS`),`refresh=1` 绕过——刚在上游启用了图像模型的用户不该等 TTL 到期才看见。读失败不写缓存。
|
|
29
|
+
- **只读,不做下拉**:两区今天每个标签组只有 1 个模型,长度 1 的下拉是假选择。切换能力的方式就是工具的 `model` 参数。清单**惰性读取**(只在能力面板打开时读、每区只读一次),失败只落在块内,绝不并进 `statusByRegion`——那会把图像模型写进 `lastCatalog`,adapter 就会拿它当聊天模型。
|
|
30
|
+
- **文案与文档**:`docs/IMAGE_GEN.md` 新增「改图」整节(四种形态、模式自动判定、每组模型、8 MB 上限、为什么不给 `mode` 参数),并把「图生图没做」那条从「没做的事」里删掉;README 中英各补一段。
|
|
31
|
+
|
|
32
|
+
### Fixes
|
|
33
|
+
|
|
34
|
+
- **超限的 data URL 会绕过 8 MB 与空文件闸门,被原样发到上游,然后按「已计费失败」冷却 30 秒(本轮合并前审核发现)。**
|
|
35
|
+
- **症状**:`resolveImageInput` 的 `data:` 分支校验完格式就 `return text`,而空文件与 `MAX_INPUT_IMAGE_BYTES` 的检查都在 `encodeInputImage` 里——只有 `http(s)`、裸 base64、本地路径这三条路径会走到它。于是 `data:image/png;base64,<12 MB>`(解码后 9 MB)会被整个塞进请求体发出去;上游拒绝后 `sent` 已是 `true`,再吃一次 30 秒冷却。
|
|
36
|
+
- **与本 PR 自己的声明直接冲突**:工具描述、`docs/IMAGE_GEN.md` 的「输入图的失败不进冷却」小节,以及 `image-tool.ts` 里 `inputImageError()` 的注释都写着「输入图的一切毛病都发生在请求发出去之前,所以统一 `INVALID_ARGS`、不花积分、不进冷却」。这不是缺一条边界用例,是四条路径里有三条遵守、一条不遵守。
|
|
37
|
+
- **修法**:新增 `assertInputImageBytes(byteLength, source)` 作为唯一的体积/空文件闸门,`encodeInputImage` 与 `data:` 分支都调它——文案因此只有一份,四条路径不可能再各自漂移。
|
|
38
|
+
- **口径**:体积按**解码后**的字节数比上限,不按 base64 字符串长度。base64 比原始字节长约 4/3,拿字符串长度比会把一张合法的 6 MB 图(8 MB base64 字符)误判成超限,并在错误信息里报出一个虚高的 MB 数。
|
|
39
|
+
- **验证**:`tests/image-gen.spec.ts` 新增 3 条——超限 data URL 报 `INVALID_ARGS` 且 `editImage` 没被调用、紧接着再调一次**不进冷却**;6 MB 输入(8 MB base64 字符)仍被接受;`data:image/png;base64,====`(形似 data URL 但解码为空)报 `INVALID_ARGS`。变异验证:把 `data:` 分支改回直接 `return`,前两条与空内容那条中的两条立刻变红。
|
|
40
|
+
|
|
41
|
+
### Tests
|
|
42
|
+
|
|
43
|
+
- `tests/image-gen.spec.ts`:38 → **60 条**。新增 `pickMediaModel` 按组校验(含国际区 `hy-image-3.5` 两组都能选、拿文生图模型去改图的报错文案)、`selectMediaModels` 的多标签形态、`buildEditBody`(字段顺序 / `n:1` / 空 `input_fidelity` 不写入 / `image` 是拷贝)、输入图归一(扩展名大小写、五种魔数 + 兜底、data URL 只认 `data:image/*;base64,`、裸 base64 的字符集判定、8 MB 常量),以及改图路径的 9 条端到端(走 `editImage` 且 `mode:'edit'`、不给图走 `generateImage`、读不到图 `INVALID_ARGS` 且**不进冷却**、`data:text/plain` 被拒、**超限 data URL 被拒且不进冷却**、**上限按解码后字节数算**、**空内容 data URL 被拒**、点名错组模型不发请求、`render` 文案)。国际区夹具换成真实形状(`hy-image-3.5` 双标签 + `kling-video-v3`),新增「国际区目录同样能选出默认模型」「工具描述同时交代两个区」「`render` 原样印出 0.03(不能被抹成 0)」;404 与未登录两条用例从「断言点名国内区」翻转为「断言不再点名」。一条旧断言从「WorkBuddy 生图」改为「WorkBuddy 文生图」——`render` 现在分两种模式。
|
|
44
|
+
- `tests/image-tool-optin.spec.ts`:12 条。新增 `imageToolRegionOf` 四条(都没授权 → `undefined`、供应商关着时勾选不算授权、只开国际区 → `'global'`、两区都开 → `'cn'`),卡片源码断言改为「不再 CN-only」。
|
|
45
|
+
- `tests/web-status.spec.ts`:新增媒体端点 7 条(200 只回 id/name/tags 且 region 透传、空清单 200、`refresh=1` 透传且 `refresh=0`/缺省读作 false、GET 405、外部 Origin 403、未接线 503、上游抛错 500),以及路由顺序断言。
|
|
46
|
+
- `tests/card-media-models.spec.tsx`(新文件,6 条,jsdom 真渲染):只在能力面板打开后**每区读一次**、按钮才带 `refresh=1`;行里同时显示 name / 工具要用的 `id` / 翻译过的 tag;空清单读成一句话而不是失败;上游 500 只落在块内、同面板的其它开关照常可用;半形行被丢弃而不是抛异常(一个只读列表不值得把整张卡带下去)。
|
|
47
|
+
- 全量 `vitest run`:138 文件 / 2362 条通过(本功能相关的 image-gen 60 + image-tool-optin 12 + card-media-models 6 共 78 条);两个 tsconfig 的 `tsc` 均 0 错。
|
|
48
|
+
|
|
49
|
+
## 5.9.0 (2026-10-25)
|
|
50
|
+
|
|
51
|
+
> 健壮性与诊断版本:活动徽标的时窗解析不再能因非字符串端点而抛错,上游改格式时徽标不再**静默**消失,配置区渲染失败不再退化成一片空白;同时修掉一个**徽标被冻结在保存时刻**的静默缺陷。**发往上游的请求体、provider 注册、凭据、计费、换号与路由门禁一律未变**——本轮不新增任何上游请求,也不改动入站路由。
|
|
52
|
+
|
|
53
|
+
### Fixes
|
|
54
|
+
|
|
55
|
+
- **`minutesOfClock` 缺运行时类型守卫,非字符串端点会在渲染期抛 `value.trim is not a function`(issue #139)。**
|
|
56
|
+
- **症状(机制属实,但 issue 的根因链不成立)**:函数签名是 `(value: string)`,只靠调用方 `parseDaily` 的 `text()` 维持不变式。一旦 `{ start: 1380, end: 470 }` 这样的数字端点到达 `withinDailyWindows`(`src/promo.ts:530`),渲染期抛 `TypeError`,宿主 `SlotErrorBoundary` 把它吞成一个空的 `<div data-slot-error>`——页面上表现为「插件像没装上」:卡片只剩头部,配置区整块消失。
|
|
57
|
+
- **issue 的根因不成立,已核**:客户端 bundle 里 `modelPromotions` / `parseDaily` 的命中数是 **0**(徽标的 `promos` 由宿主解析后经 `status-paths` → `web-status` 投影下来),且宿主 `parseDaily` 对数字端点是**丢弃而非抛错**(`text()` 先挡)。实测国内区三台主机 × 四种 UA,`schedule.daily` 端点全部是字符串(`"23:00"` / `"7:50"` / `"0:00"` / `"23:59"`),无一个数字。另外 `TypeError` 与时刻无关,issue 描述的「只在夜间崩」自相矛盾(13:00 与 23:30 抛的是同一个错)。
|
|
58
|
+
- **但那条路径并非不可达**:`toPersistedWorkBuddyModel` 保留 `promos`,settings 的 `modelConfig` schema 不剥离未知键,`resolveRoster` 原样携带——所以 `lastCatalog` 是一条**真实的第二入口**,且客户端对它**零运行时校验**。这就是补守卫而不是「纯纵深防御」的理由。
|
|
59
|
+
- **修法**:签名改 `(value: unknown)`,首行 `if (typeof value !== 'string') return undefined`。**没有**采纳 issue 建议的「数字按当日分钟数解释」:`1380` 确实等于 23:00,但没有任何上游发布过这种形状,为一条上游从未描述过的活动凭空造出时段,正是本模块要防的「说多」。
|
|
60
|
+
- **验证**:`tests/promo.spec.ts` 新增 2 条——数字端点 `selectPromoBadge` / `selectPromoMultiplier` 均不抛错且返回 `undefined`;`parseWorkBuddyPromos` 对数字端点产出 `dailyUnusable`(「永不运行」)而非「无时段 = 全天」。变异验证:删掉守卫 → 该条变红。
|
|
61
|
+
|
|
62
|
+
- **上游的促销变更被冻结在用户保存目录的那一刻(本轮审核发现,影响面大于 #139)。**
|
|
63
|
+
- **症状**:用户只要保存过一次模型目录,`promos` 就被写进 `lastCatalog`,此后**上游的促销变更再也不生效**——徽标与折后价用的都是保存那一刻的快照。两个方向都实测过:saved 无 `promos` + live 有 → 徽标**完全不出现**;saved 有旧 `promos` + live 有新 → **旧的赢**。极端情形:一条无 `schedule` 的常驻徽标(如「订阅优先」)在上游撤下后仍会**永远**挂在模型行上,因为它没有绝对窗口能让它自己过期。
|
|
64
|
+
- **根因**:`promos` 不在 `LIVE_OWNED_FIELDS`(`src/catalog-sync.ts:77`),而 `overlayLiveCapabilities` 返回 `{ ...saved, ...overlay }`——saved 的 `promos` 原样留下,live 的被整个丢掉。这与 `src/promo.ts` 文件头「徽标是时钟的函数,不是抓取的函数」直接矛盾:它冻在了**保存**时。
|
|
65
|
+
- **修法**:`promos` 改为 live-owned,但**不能**复用 `LIVE_OWNED_FIELDS` 那个循环(它只写 live 有值的字段,无法表达「清除」)。改为在 `overlayLiveCapabilities` 里单独处理:live 有 `promos` 就整体覆盖;live 没有就**按 key 删除** saved 的那份(不能用 `undefined`——显式 `undefined` 会存活 `structuredClone`,并被设置写入路径的严格 JSON codec 整单拒绝,与 `busyLevel` 的处理同理)。
|
|
66
|
+
- **验证**:`tests/catalog-sync.spec.ts` 新增 5 条覆盖四个方向 + 直接调用 `overlayLiveCapabilities`。变异验证:让 `promosDiffer` 恒为 false → 4 条变红。
|
|
67
|
+
- **已知未改**:`catalogFingerprint` 仍不含 `promos`,`lastCatalog` 里那份 `promos` 现在是纯冗余。持久化形状的调整会牵动严格 JSON codec 的历史教训,留待另议。
|
|
68
|
+
|
|
69
|
+
- **配置区渲染失败时页面一片空白,无法与「插件没装上」区分(issue #139 的报告者正是被这一点拖住)。**
|
|
70
|
+
- **症状**:宿主 `SlotErrorBoundary` 的失败面是**空** `<div data-slot-error>`,只留一行 `console.error`。issue 贴的 `slot entry crashed in 'plugins.bundle.config': value.trim is not a function` 与该边界逐字吻合。
|
|
71
|
+
- **修法**:在卡片 **body** 层包一层 `CardBodyBoundary`(**不含 header**——配置区崩了也要能折叠/展开,且 header 是纯静态标记,不可能是抛错源)。失败面显示「配置区渲染失败」+ **错误摘要原文** + 一句「模型服务本身未受影响」,并照旧 `console.error`(含 `componentStack`),因此**不遮蔽**任何本该暴露的错误。
|
|
72
|
+
- **边界条件**:`SlotAssemblyError`(宿主装配故障)**原样重抛**,不降级——把响亮的装配错误变成安静的「配置失败」是错的。
|
|
73
|
+
- **范围声明**:React 边界只捕获 render / 生命周期 / 构造期错误,**不捕获**事件处理器、定时器与 promise 回调里的抛错;那些路径仍按原样呈现。本次崩溃发生在 render 期(栈顶 `promo.ts:263`),落点正确。
|
|
74
|
+
- **验证**:直接渲染抛错的子组件,确认失败面出现、摘要为 `value.trim is not a function`、且 `console.error` 被调用。
|
|
75
|
+
|
|
76
|
+
- **上游发布了活动时段却一条都读不懂时,徽标静默消失,界面上没有任何解释。**
|
|
77
|
+
- **症状**:`dailyUnusable` 让该活动按「永不运行」处理(正确),但用户看到的是徽标不见了——与「活动已结束」无法区分。**这才是上游将来改格式时用户真正会遇到的症状**(不崩,静默消失),而 issue 猜的是崩溃。
|
|
78
|
+
- **修法**:诊断栏的 `row.diagFailures` 行计入 `promoUnreadable`,显示「N 条活动时段读不懂 —— 徽标已隐藏」。`dailyUnusable` 本来就已传到客户端,只是从未显示过。
|
|
79
|
+
- **验证**:`tests/promo-card.spec.tsx` 新增 2 条(有 `dailyUnusable` 时诊断行出现且不画徽标;全部可解析时该行不出现)。变异验证:删掉该行 → 变红。
|
|
80
|
+
|
|
81
|
+
- **`parseDaily` 丢弃窗口时完全静默,上游改格式在日志里也留不下痕迹。**
|
|
82
|
+
- **修法**:丢弃时打一行 `console.warn`,带活动 id 与端点的 `typeof`(`number` 单独命名,因为它正是未来格式变更可能的样子)。
|
|
83
|
+
- **位置是刻意的**:日志打在**宿主侧** `parseDaily`(每个上游文档跑一次),**不是**客户端 `withinDailyWindows`(卡片每 60s 轮询、每个模型每轮都重算,打在那里会变成每分钟几十行)。
|
|
84
|
+
- **不泄露上游内容**:只报 `typeof`,不回显原始值。
|
|
85
|
+
|
|
3
86
|
## 5.8.5 (2026-10-24)
|
|
4
87
|
|
|
5
88
|
> 安全与健壮性版本:给设置卡片的 13 条路由(10 条 POST + 3 条 GET)补上缺失的 `Host` 校验(DNS-rebinding),并让测速探针不再发送一个自造的档位值。**发往上游的请求体、provider 注册、凭据、计费与换号规则一律未变**——唯一的上游侧行为变化是测速请求少了一个字段;入站侧(本插件对浏览器请求的接受范围)**确有变化**,见下文两条:Host/Origin 判据的扩展与空 Origin 的统一。
|
package/README.en.md
CHANGED
|
@@ -40,7 +40,7 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
40
40
|
- **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.
|
|
41
41
|
- **Tabbed settings card (accounts / models / capabilities / 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, Capabilities, holds the **grants**: let the plugin drive the local CodeBuddy CLI, let the model buy an image on its own, where the credit readout is drawn. Each takes effect on the click and never passes through Save, and each is expected to grow sub-options of its own (a working directory for the CLI route, a size or price cap for the image tool) — which is why they no longer share one thin strip above the account table. The fourth 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.
|
|
42
42
|
- **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. **A success never has a cooldown**: this gateway also attaches `Retry-After` to a 200, and its value is the account's monthly quota reset (measured: `2026-11-01 00:00 +08:00`) — not a wait. So a successful row shows “✓ Usable” and never appends “usable again after”, and records written by older builds with that stray time are healed on read. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
|
|
43
|
-
- **Image generation tool `workbuddy_draw_image` (CN
|
|
43
|
+
- **Image generation tool `workbuddy_draw_image` (CN and international, OFF by default, granted per region)** — 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 on the CN gateway and about 0.03 on the international one, and the host offers no per-call confirmation. Tick 「Let the model draw images」 on that region's Capabilities 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. **Both gateways work, but through two different models**: CN `hunyuan-image-alpha` and international `hy-image-3.5` (the latter measured on 2026-10-25). The two credit pools are independent, so **5.71 and 0.03 are not a cross-region price difference for the same image** — they are two different models on two different gateways, and neither number converts into the other. The grant is **per region**: ticking the box on one tab authorises only that gateway's accounts, and with both ticked the CN gateway wins (the only order that keeps the old behaviour). **Endpoint and request body are copied from the official CLI's ImageService** (`POST /v2/images/generations`), and the product is a 24-hour COS signed URL the plugin tells you to download in time. **The same tool edits images**: pass `image` (a local path, `file://`, `http(s)`, a data URL, or bare base64, up to 8 MB) and it switches to `POST /v2/images/edits` with the model carrying the `image-to-image` tag (CN `hunyuan-image-alpha-edit`, international `hy-image-3.5`); omit it and you get text-to-image — **the mode is decided solely by whether there is an input image, there is no `mode` parameter**. Input images are only normalised into the data URL the gateway accepts: **no rescaling, cropping or format conversion**. An unreadable, malformed or oversized image fails before any request is sent, so it is not a paid failure. **It really costs money**: about **5.71 credits per image** on CN and **0.03** on the international gateway (both measured), so the tool description gives both magnitudes and says to trust the `credit` each response returns, 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.
|
|
44
44
|
|
|
45
45
|
**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).
|
|
46
46
|
- **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.
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
- **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 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`),它不是「你在此之前不能用」——所以成功的行只显示「✓ 可用」,绝不附「之后可再用」;旧版本已经写进文件的那类记录会在读取时自动治愈。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
|
|
50
50
|
|
|
51
51
|
**两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的**原生窗口**向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限(可选 200K / 500K / 模型原生窗口)。
|
|
52
|
-
- **生图工具 `workbuddy_draw_image
|
|
52
|
+
- **生图工具 `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,**不缩放、不裁剪、不转格式**;读不到图、格式错、超限都在发请求之前报错,不算一次花钱的失败。**它真的花钱**:单价随区域与模型不同,所以工具描述里同时写了两个区的量级,并要求以返回的 `credit` 为准;失败后有 30 秒冷却(但参数写错、没登录、模型不存在这三类一分钱没花,不进冷却),也不提供「一次画四张」这类不可控支出的参数。工具定义是手写的 raw JSON Schema,**没有引入任何新依赖**。取证、协议与联调记录见 [docs/IMAGE_GEN.md](docs/IMAGE_GEN.md)。
|
|
53
53
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
54
54
|
- **对话输入框积分读数(国内版 / 国际版自动切换)** —— 对话输入框左侧工具栏常驻一行可点击的读数(如 `WB CN · 1,072`),**按本会话当前选中的模型自动切换区域**:选中 WB CN 的模型就显示国内版余额,选中 WB AI 的模型就显示国际版余额;用其它 provider 的模型时该读数完全不出现。每 5 分钟自动刷新,也可点开手动刷新。点击读数弹出面板:**按账号列出的余额表格**,点某一行即可切换到该账号(与卡片上的账号选择是同一个设置);**当前不能用的账号会在行内标出原因**(如「被限流」「积分耗尽」),标注口径与账号池表格一致。面板里的余额只在打开时才去查,因此不会让 5 分钟的后台刷新按账号数翻倍。要关掉某一边,就在卡片对应区域的 tab 下取消勾选「在输入框工具栏显示积分读数」;该区域的供应商被关闭时,读数不显示、开关也置灰。同一个开关下面还有「显示位置」这一行:默认在输入框工具栏(与模型选择同一行),改成「输入框下方」后读数落到轮次/步数/token 统计那一行——装了别的插件把工具栏座位占掉时用得上。
|
|
55
55
|
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as WorkBuddyCredentialStore,
|
|
2
|
+
import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, Gt as hostPeersInfo, I as workbuddyOwnAuthPath, Lt as findWorkbuddyAppExecutableWithSource, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, V as WorkBuddyUpstreamClient, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, jt as WORKBUDDY_APP_EXECUTABLE_ENV, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-_EeM2YDF.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|