dsh-connect-workbuddy 5.9.0 → 5.11.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 CHANGED
@@ -1,5 +1,146 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.11.0 (2026-10-26)
4
+
5
+ > 特性版本:新增 `workbuddy_draw_video`(文生视频 / 图生视频,异步任务「提交 → 轮询 → 下载」,
6
+ > 逐区 opt-in、默认关闭),卡片「能力」栏新增视频开关与**按模式钉模型**;同期把生图 `size`
7
+ > 的描述补上实测的大图档位与 419 万像素上限(**执行层零改动**,改的是模型对「能画多大」的先验)。
8
+ > 视频工具落地后按现场报错修了三处:**时长校验形同虚设**、**参数错误也白等 60 秒冷却**、
9
+ > 以及**显式 `resolution` 在拿不到目录清单时被静默丢弃**。请求路径、provider 路由与凭据处理
10
+ > 一律未变:视频打的是官方 CLI `VideoService` 早已在用的两条 `/v2/videos/*` 路由,选模型仍只看
11
+ > 该区 `/v3/config` 的媒体标签。
12
+
13
+ ### Features
14
+
15
+ - **新增 `workbuddy_draw_video`:文生视频 / 图生视频,国内区与国际区都能注册,逐区 opt-in**
16
+ - **它不是「接一次 `submitVideo()`」**:视频是异步任务——`POST /v2/videos/generations` 只回一个 `taskId`,还要轮询 `POST /v2/videos/tasks` 到 `completed`(5 秒一次,上限 600 秒),再 `GET` 每条 `data[].url` 把 mp4 拉到本地。旧文档「加视频只是接一次 `submitVideo()`」少说了后两段,已在 `docs/VIDEO_GEN.md` 对账。
17
+ - **参数层与选模型层是复用的**:`MEDIA_TAGS`、`VIDEO_TAG_*`、`fetchMediaModels`、卡片媒体清单原本就认得两个视频标签,所以参数校验与选模型只补了一层 `src/video-gen.ts`;**执行层是全新异步代码**(`src/upstream.ts` 的 `submitVideo` / `pollVideoTask` / `downloadVideo`)。
18
+ - **花钱的三条红线照搬生图**:独占执行(不声明 `isConcurrencySafe`)、失败后 60 秒冷却、冷却**只看请求是否真的发出**(参数错、未登录、读不到输入图一律不进冷却);工具**逐区 opt-in**,不勾时根本不注册,模型看不到它。
19
+ - **产物落本地临时目录**:视频是二进制,不进对话体,工具只回 `file://` 路径 + 渲染成链接(`os.tmpdir()/workbuddy-video-gen/<model>-<时间戳>.mp4`)。
20
+ - **顺手改对一处描述**:`seconds` 原写「越界会被夹回默认值并提示」,而 `resolveVideoParams` 实际抛
21
+ `INVALID_ARGS`(不会夹取)——描述会教调用方传 30 秒再等一个 5 秒的结果,然后把报错当成 bug。
22
+ - **验证**:`tsc` 两套配置 0 error;`vitest run` 141 个文件 / **2445 条**通过(含新增
23
+ `tests/video-gen.spec.ts`、`tests/video-tool.spec.ts`、`tests/video-tool-optin.spec.ts`);
24
+ `tsdown` 构建通过。
25
+
26
+ - **实测单价与产物规格写进文档了(`docs/VIDEO_GEN.md`「实测单价与产物规格」)**
27
+ - **国内区实测单价**:图生视频 4 秒 `minimax-video-h3` 57.14 / `kling-video-v3` 68.57;
28
+ 文生视频 5 秒 `minimax-video-h3` 71.43 / `kling-video-v3` 128.57 积分。`minimax-video-h3`
29
+ 文生图生同价(14.29/s),`kling-video-v3` 图生比文生便宜(17.14/s 对 25.71/s)——
30
+ 「一条 5 秒多少积分」要连模式一起看。
31
+ - **产物实测**(解析 mp4 的 `moov` 盒,不靠上游自报):H.264 + AAC、**24fps**、
32
+ `1280x720` 或 `1344x768`(这两个尺寸是**默认档位**下的产物;见下方 Fixes 第三条——
33
+ 「没有 1080p」此前被记成上游行为,实则其中一部分是本插件把 `resolution` 丢掉了),
34
+ 视频码率 3.66–11.47 Mbps、体积 2.0–7.0 MB;**实际时长按帧数走**,请求 5 秒拿回
35
+ 5.09–5.17 秒,请求 4 秒拿回 4.04–4.46 秒。`enable_audio: true` 也不保证有音轨。
36
+ - **卡片上按模式钉住视频模型(软约束,与显式 `model` 分工明确)**
37
+ - **起因**:国内区每个视频标签组都有 2 个模型(`kling-video-v3` / `minimax-video-h3`),调用方多半省掉 `model`,于是永远拿到目录里的第一个;口头说一次不持久。
38
+ - **按区、按标签存**:`regions.<区>.preferredVideoModels` 是 `{ [tag]: modelId }`——两种模式可能由不同模型提供,且模型 id 不跨区(`minimax-video-h3` 在国际区不存在)。
39
+ - **软约束**:显式 `model` 是调用方为这一次写的,写错就报 `NO_VIDEO_MODEL`;卡片偏好是用户以前写的,钉住的模型已从目录里消失时**静默回落组内第一个**。两条方向相反的规则各有测试钉住。
40
+ - **清空 = 删 key**,不是写空串;卡片读值先问 Host(`statusByRegion`),浏览器配置快照只作回落。
41
+ - **写入走与开关同一条「写后回读校验」路径**(`writeRegionPreferredVideoModels`),不是裸 `scope.set`。
42
+
43
+ ### Fixes
44
+
45
+ - **视频工具:显式 `resolution` 在拿不到目录清单时被静默丢弃(视频同步找到的第三处)**
46
+ - **症状与上面那条同源**:`resolveVideoParams` 用 `new Set(config?.resolutions ?? [])` + `has()`
47
+ 判断,于是「模型不支持这个值」与「我们根本没有这份清单」两条路径合流——**任何**显式
48
+ `resolution` 在目录没下发 `videoGeneration.resolutions[]` 的账号上都被丢掉,`buildVideoBody`
49
+ 收到的已是 `undefined`,请求体里连 `resolution` 字段都没有,却还打印一句
50
+ 「resolution 1080P 不被模型 · 支持;改用 上游默认」,看起来像上游拒绝。
51
+ - **同一条判据修正**:只有真的拿到 `resolutions[]` 才做回落,拿不到就**原样透传**交给上游判
52
+ ——与 `seconds` 的「拿不到区间就不盲拒」完全一致。
53
+ - **顺带修掉大小写**:清单匹配改为大小写不敏感,命中后发**目录的拼写**。官方 CLI 的兜底档位是
54
+ `["720P","1080P"]`,而工具描述一直举 `720p`/`1080p` 的例子,精确匹配会把一个其实可用的值
55
+ 判成不支持。
56
+ - **工具描述与 `docs/VIDEO_GEN.md` 同步改正**(文档此前把这个 bug 的现象记成了上游的行为,
57
+ 并据此写了「`resolution: "1080p"` 对 `minimax-video-h3` 不成立」——那条结论在拿不到清单的
58
+ 账号上从未真正验证过)。
59
+ - **验证**:`tests/video-gen.spec.ts` 新增 2 条(无清单/空清单透传、大小写命中),全量
60
+ `vitest run` 141 个文件 / **2445 条**通过。
61
+
62
+ - **视频工具:`seconds: 1` 被上游 400 退回后还要白等 60 秒(两处真 bug)**
63
+ - **现场**:`Error: workbuddy video: 提交生视频任务失败(HTTP 400): code 14407: Create video failed with error: unsupported video duration: 1 seconds, supported range: 4-15 seconds`,紧接着第二次调用被挡成「请 38 秒后再试」。用户的判断是「上游返回这种错误,也要等半天」。
64
+ - **根因一(本地时长校验是死代码)**:`src/video-tool.ts` 调 `resolveVideoParams({...})` 时**没传第二个参数 `config`**;而 `src/video-gen.ts` 的 `minSeconds`/`maxSeconds` 校验只在 config 存在时才跑。更上游还有一处:`src/image-gen.ts` 的 `selectMediaModels` 只搬 `{id,name,tags}`,把条目上的 `videoGeneration` 丢了——所以即使传了也拿不到。
65
+ - **根因二(没花钱的失败也进冷却)**:`describeVideoFailure` 把提交阶段的一切非 401/403/404/429 都归成 `UPSTREAM`,而 `UPSTREAM` 在冷却集合里。可 400 是「建任务之前就被退回」,没有 task、没有扣积分。
66
+ - **修法**:`selectMediaModels`(video 与 image 两处)保留并透传 `videoGeneration`(`readVideoGeneration` 只搬已知字段,缺字段不补默认值),工具把它喂给 `resolveVideoParams`——越界时长现在**在本地就被拦下,一次付费请求都不发**;提交阶段其它 4xx 单独归类为 `REJECTED`(不进冷却,错误里明说「没有创建任务、没有扣积分」并保留上游原文)。
67
+ - **顺带学会区间**:目录不带 `videoGeneration` 的账号(实测部分国内区账号的 `/v3/config` 条目只有 `{id,name,tags}`)本地无从校验,只能让上游先拒一次;`parseSupportedSecondsRange` 从它的文案里读出 `supported range: 4-15`,按**区域 + 模型 id**记在本次进程里,第二次同样的越界值**零请求、零冷却**直接报「时长必须在 4 到 15 秒之间」。读不出区间(例如拒的是宽高比)就什么都不记——不猜。
68
+ - **验证**:`tsc` 两套配置 0 error;`vitest run` 141 个文件 / **2445 条**通过(`tests/video-gen.spec.ts`
69
+ 35→48、`tests/video-tool.spec.ts` 7→12;新增用例钉住「提交 400 之后下一次仍会真的发请求」
70
+ 与「学到区间后第二次不出进程」)。
71
+
72
+ - **`size` 的描述让人以为只能出 1024 级方图,而实测上限是约 419 万像素**
73
+ - **起因**:5.10.0 把 `size` 描述从单个 `1024x1024` 例子改成方 / 竖 / 横三个例子,解决了
74
+ 「只能出方图」那个错误先验;但三个例子全是 1024 级,模型据此认为「大图也就是这个量级」,
75
+ 用户要桌面壁纸、竖屏壁纸时只会得到一张 1024 宽的照片。
76
+ - **2026-10-25 逐档实测(国内区 `hunyuan-image-alpha`)**:`2048x2048`(4.19 MP)、
77
+ `2048x1536`(3.15 MP)、`2048x1152`(2.36 MP)、`2560x1440`(3.69 MP)、
78
+ `2560x1600`(4.10 MP)**全部成功,输出宽高与请求完全一致**——不是把大图静默压回原生
79
+ 尺寸再放大,画面里的刻度文字与纹理是独立线条。`2560x2560`(6.55 MP)、`3072x3072`
80
+ (9.44 MP)、`4096x4096`(16.8 MP)**全部被直接拒绝**,不会退回一个较小尺寸。
81
+ - **据此定上限**:约 **419 万总像素**,任意长宽比。这**推翻**了流传的判断「原生上限约
82
+ 1536 长边」——那个数字来自官方 CLI `.describe()` 的三个例子与 bundled OpenAI SDK 类型
83
+ 定义的取值范围,从来没有实测过;实测表明 2560 长边是真实可用的。
84
+ - **修法**:`src/image-tool.ts` 的 `size` 描述加入 `2048x2048` / `2048x1536` /
85
+ `2560x1440` / `2560x1600` 四个**实测通过**的档位、写明「约 419 万像素」上限、并点名
86
+ `2560x2560` / `3072x3072` / `4096x4096` 会被拒(防止模型往 4K 上撞)。
87
+ - **只写实测过的尺寸**:`1440x2560`(手机竖屏壁纸的常见答案)**没有单独跑过**,它与已通过
88
+ 的 `2560x1440` 像素数相同、只转 90°,按像素阈值推断可用,但描述里不写它——描述只给实测
89
+ 通过的宽高;`docs/IMAGE_GEN.md` 的「尺寸与比例」小节把 `1440x2560` 单列一行并标注
90
+ 「未单独实测」。
91
+ - **验证**:`tests/image-gen.spec.ts` 新增 1 条(描述同时含 `2048x2048` / `2560x1440` /
92
+ `419` / `4096x4096`,防止以后有人把上限改回「只给三个 1024 例子」);
93
+ `docs/IMAGE_GEN.md` 的「尺寸与比例」换成 11 行实测对照表 + 按用途选尺寸表;
94
+ README 中英各补一句。
95
+
96
+ ---
97
+
98
+ ## 5.10.0 (2026-10-25)
99
+
100
+ > 特性版本:`workbuddy_draw_image` 从「只在国内区注册、只能文生图」扩成「国内区与国际区都能用、文生图 + 改图(图生图)」,卡片「能力」栏新增该区媒体模型的只读清单。**请求路径、provider 路由、计费口径与凭据处理一律未变**:改图打的是官方 CLI `ImageService.editImage` 早就用的 `POST /v2/images/edits`,选模型仍然只看该区 `/v3/config` 的媒体标签。
101
+
102
+ ### Features
103
+
104
+ - **国际区账号也能用 `workbuddy_draw_image`(逐区授权,国内优先)**
105
+ - **起因**:此前文档与代码都写着「只有国内区有生图模型,国际区一条都没有」,工具因此硬编码成 `regionEnabled(config, 'cn')` 才注册。那句话是 **2026-10-21 的观测**,已经过期。
106
+ - **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 那条临时密钥链路在国际区整个不存在,与生图无关。
107
+ - **修法(区域选择集中到一处)**:`src/index.ts` 新增 `imageToolRegionOf(config)`——按 `REGION_KEYS`(`cn` 在前)遍历,跳过被关闭的供应商,返回第一个**既启用又打开了「允许模型自主生图」**的区域,都不满足返回 `undefined`。`syncImageTool()` 的注册判定改为 `imageToolRegionOf(value) !== undefined`;`resolveCredential` 改为每次调用时读当前配置、遍历被授权的区逐个 `resolve()`,某区未登录就试下一个区,全失败返回 `undefined`。**CN 优先是唯一向后兼容的顺序**:升级前这个工具只能走国内区,升级不能悄悄把老用户的支出挪到另一个网关。
108
+ - **文案层同步拆除「只有国内区」的前提**:`ImageToolDeps` 删掉 `regionLabel` 字段;工具描述改为「两个区都可用 + 各自的模型名 + 单价随区与模型不同 + 以返回的 `credit` 为准」;`NOT_LOGGED_IN` 改为「要么两个区都没登录,要么已登录的那个区没打开该开关」;`hint` 不再写死 5.71,有 `credit` 时印本次实扣;`describeImageFailure()` 的 404 文案与 `pickImageModel()` 的空目录文案不再断言「国际区没有生图模型」。
109
+ - **卡片**:`imageToolPreference` 不再包在 `activeRegion === 'cn' ? … : null` 里,两个 tab 都显示;勾选仍然只写**当前这一区**的 `imageToolEnabled`,两区都关着时工具根本不注册。
110
+ - **价格口径(README 中英各改一段)**:5.71(国内 `hunyuan-image-alpha`)与 0.03(国际 `hy-image-3.5`)是**两个模型 × 两个区域**同时变化的结果,不是同一张图的跨区差价;两区的积分池彼此独立,两个数字之间没有任何汇率含义。
111
+ - **改图(image-to-image):同一个工具,给了 `image` 就切模式**
112
+ - **起因**:`/v2/images/edits` 与 `hunyuan-image-alpha-edit` 的存在早在 4.13.2 取证时就写在 `docs/IMAGE_GEN.md` 的「没做的事」里——没做,是因为当时没跑通一次真实调用。这次跑通了,才把它接出来。
113
+ - **模式由「有没有输入图」唯一决定,不新增 `mode` 参数**:多一个参数就多一个「`mode=edit` 但没给图」的自相矛盾状态。`image` 存在 → `editImage` + `image-to-image` 组;不存在 → `generateImage` + `text-to-image` 组。
114
+ - **`model` 的校验跟着模式走**:`pickMediaModel(models, requested, tag)` 按当次标签组挑模型。拿文生图模型点名去改图会直接报「目录里没有图生图模型「xxx」。可用:…」,而不是把不匹配的 id 发上去换一个上游 404——那样报错只会是 `route config not found`,读者无从知道是名字写错了。`selectMediaModels` 同时接受单个标签与标签数组,目录一次读回四类媒体标签(`MEDIA_TAGS`),为将来的视频留位。
115
+ - **`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`)。
116
+ - **不做缩放、裁剪、格式转换**(`MAX_INPUT_IMAGE_BYTES = 8 MB` 只是上限):用户给什么就传什么。悄悄改输入图是比失败更糟的行为。
117
+ - **输入图的所有毛病都在「请求没发出去」之前**,所以统一 `INVALID_ARGS`、不进 30 秒冷却、不花积分:读不到文件、下载失败、data URL 格式不对、空文件、超 8 MB。紧接着再调一次仍能立刻执行——判据与既有规则一致(冷却只看「请求是否真的发出去了」)。
118
+ - **返回值与 `render` 区分两种模式**:output schema 新增必填 `mode`(`'generate'` | `'edit'`),`render` 显示「改图 · 模型:`xxx`」、markdown alt 由「WorkBuddy 生图」改为「WorkBuddy 文生图/改图」。
119
+ - **上游**:`src/upstream.ts` 新增 `editImage(credential, {model, prompt, size?, image, inputFidelity?})`,`src/image-gen.ts` 新增 `buildEditBody()`(在 `buildImageBody` 之上追加 `image` 数组,`input_fidelity` 仅在非空时写入)。
120
+ - **卡片的「能力」栏新增媒体模型只读清单**
121
+ - `POST /plugins/dsh-connect-workbuddy/media-models`(`WORKBUDDY_MEDIA_MODELS_PATH`):405 非 POST → 403 外部 Origin → 503 未接线 → `region` 查询参数(缺省 `cn`)→ 200 `{models:[{id,name,tags}]}`。空清单是**合法回答**(网关不发布媒体模型),不是错误——卡片为它单独写了一句话,而不是显示一个失败。
122
+ - **为什么单独一条端点**:`models/refresh` 返回的是**聊天**目录,投影时媒体条目被 `selectCliModels` 丢掉,刷新它永远看不到图像模型。
123
+ - 宿主侧 5 分钟缓存(复用 `MEDIA_MODEL_TTL_MS`),`refresh=1` 绕过——刚在上游启用了图像模型的用户不该等 TTL 到期才看见。读失败不写缓存。
124
+ - **只读,不做下拉**:两区今天每个标签组只有 1 个模型,长度 1 的下拉是假选择。切换能力的方式就是工具的 `model` 参数。清单**惰性读取**(只在能力面板打开时读、每区只读一次),失败只落在块内,绝不并进 `statusByRegion`——那会把图像模型写进 `lastCatalog`,adapter 就会拿它当聊天模型。
125
+ - **文案与文档**:`docs/IMAGE_GEN.md` 新增「改图」整节(四种形态、模式自动判定、每组模型、8 MB 上限、为什么不给 `mode` 参数),并把「图生图没做」那条从「没做的事」里删掉;README 中英各补一段。
126
+
127
+ ### Fixes
128
+
129
+ - **超限的 data URL 会绕过 8 MB 与空文件闸门,被原样发到上游,然后按「已计费失败」冷却 30 秒(本轮合并前审核发现)。**
130
+ - **症状**:`resolveImageInput` 的 `data:` 分支校验完格式就 `return text`,而空文件与 `MAX_INPUT_IMAGE_BYTES` 的检查都在 `encodeInputImage` 里——只有 `http(s)`、裸 base64、本地路径这三条路径会走到它。于是 `data:image/png;base64,<12 MB>`(解码后 9 MB)会被整个塞进请求体发出去;上游拒绝后 `sent` 已是 `true`,再吃一次 30 秒冷却。
131
+ - **与本 PR 自己的声明直接冲突**:工具描述、`docs/IMAGE_GEN.md` 的「输入图的失败不进冷却」小节,以及 `image-tool.ts` 里 `inputImageError()` 的注释都写着「输入图的一切毛病都发生在请求发出去之前,所以统一 `INVALID_ARGS`、不花积分、不进冷却」。这不是缺一条边界用例,是四条路径里有三条遵守、一条不遵守。
132
+ - **修法**:新增 `assertInputImageBytes(byteLength, source)` 作为唯一的体积/空文件闸门,`encodeInputImage` 与 `data:` 分支都调它——文案因此只有一份,四条路径不可能再各自漂移。
133
+ - **口径**:体积按**解码后**的字节数比上限,不按 base64 字符串长度。base64 比原始字节长约 4/3,拿字符串长度比会把一张合法的 6 MB 图(8 MB base64 字符)误判成超限,并在错误信息里报出一个虚高的 MB 数。
134
+ - **验证**:`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`,前两条与空内容那条中的两条立刻变红。
135
+
136
+ ### Tests
137
+
138
+ - `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` 现在分两种模式。
139
+ - `tests/image-tool-optin.spec.ts`:12 条。新增 `imageToolRegionOf` 四条(都没授权 → `undefined`、供应商关着时勾选不算授权、只开国际区 → `'global'`、两区都开 → `'cn'`),卡片源码断言改为「不再 CN-only」。
140
+ - `tests/web-status.spec.ts`:新增媒体端点 7 条(200 只回 id/name/tags 且 region 透传、空清单 200、`refresh=1` 透传且 `refresh=0`/缺省读作 false、GET 405、外部 Origin 403、未接线 503、上游抛错 500),以及路由顺序断言。
141
+ - `tests/card-media-models.spec.tsx`(新文件,6 条,jsdom 真渲染):只在能力面板打开后**每区读一次**、按钮才带 `refresh=1`;行里同时显示 name / 工具要用的 `id` / 翻译过的 tag;空清单读成一句话而不是失败;上游 500 只落在块内、同面板的其它开关照常可用;半形行被丢弃而不是抛异常(一个只读列表不值得把整张卡带下去)。
142
+ - 全量 `vitest run`:138 文件 / 2362 条通过(本功能相关的 image-gen 60 + image-tool-optin 12 + card-media-models 6 共 78 条);两个 tsconfig 的 `tsc` 均 0 错。
143
+
3
144
  ## 5.9.0 (2026-10-25)
4
145
 
5
146
  > 健壮性与诊断版本:活动徽标的时窗解析不再能因非字符串端点而抛错,上游改格式时徽标不再**静默**消失,配置区渲染失败不再退化成一片空白;同时修掉一个**徽标被冻结在保存时刻**的静默缺陷。**发往上游的请求体、provider 注册、凭据、计费、换号与路由门禁一律未变**——本轮不新增任何上游请求,也不改动入站路由。
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 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.
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. **Sizes are not limited to squares, and the ceiling is about 4.19 megapixels** (measured tier by tier on 2026-10-25: `1024x1536`, `2048x2048`, `2048x1536`, `2560x1440` and `2560x1600` all succeed and come back at exactly the requested width and height, not silently downscaled; `2560x2560` and anything larger is refused outright). So the usual answers are `2560x1440` for a 16:9 desktop, `2048x2048` for a large square and `2048x1536` for a wide one; for portrait, put the long edge in the same range — beyond that you need super-resolution, the parameter itself is maxed out. **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,9 @@
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`(国内版,默认关闭)** —— 把 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)。
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,**不缩放、不裁剪、不转格式**;读不到图、格式错、超限都在发请求之前报错,不算一次花钱的失败。**尺寸不限于方图,上限约 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)。
53
+ - **视频生成工具 `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)。
54
+
53
55
  - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
54
56
  - **对话输入框积分读数(国内版 / 国际版自动切换)** —— 对话输入框左侧工具栏常驻一行可点击的读数(如 `WB CN · 1,072`),**按本会话当前选中的模型自动切换区域**:选中 WB CN 的模型就显示国内版余额,选中 WB AI 的模型就显示国际版余额;用其它 provider 的模型时该读数完全不出现。每 5 分钟自动刷新,也可点开手动刷新。点击读数弹出面板:**按账号列出的余额表格**,点某一行即可切换到该账号(与卡片上的账号选择是同一个设置);**当前不能用的账号会在行内标出原因**(如「被限流」「积分耗尽」),标注口径与账号池表格一致。面板里的余额只在打开时才去查,因此不会让 5 分钟的后台刷新按账号数翻倍。要关掉某一边,就在卡片对应区域的 tab 下取消勾选「在输入框工具栏显示积分读数」;该区域的供应商被关闭时,读数不显示、开关也置灰。同一个开关下面还有「显示位置」这一行:默认在输入框工具栏(与模型选择同一行),改成「输入框下方」后读数落到轮次/步数/token 统计那一行——装了别的插件把工具栏座位占掉时用得上。
55
57
 
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as WorkBuddyCredentialStore, Ct as WORKBUDDY_APP_EXECUTABLE_ENV, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as hostPeersInfo, 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, kt as findWorkbuddyAppExecutableWithSource, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-C1tVU2IN.js";
2
+ import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, Ut as findWorkbuddyAppExecutableWithSource, V as WorkBuddyUpstreamClient, Zt as hostPeersInfo, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-BYdr-pVq.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";