dsh-connect-workbuddy 5.9.0 → 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 CHANGED
@@ -1,5 +1,51 @@
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
+
3
49
  ## 5.9.0 (2026-10-25)
4
50
 
5
51
  > 健壮性与诊断版本:活动徽标的时窗解析不再能因非字符串端点而抛错,上游改格式时徽标不再**静默**消失,配置区渲染失败不再退化成一片空白;同时修掉一个**徽标被冻结在保存时刻**的静默缺陷。**发往上游的请求体、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. **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`(国内版,默认关闭)** —— 把 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,**不缩放、不裁剪、不转格式**;读不到图、格式错、超限都在发请求之前报错,不算一次花钱的失败。**它真的花钱**:单价随区域与模型不同,所以工具描述里同时写了两个区的量级,并要求以返回的 `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, 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, 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";
package/lib/client.js CHANGED
@@ -50,6 +50,18 @@ window.__ModuleLoader__.load({
50
50
  const WORKBUDDY_BENEFIT_CLAIM_PATH = "/plugins/dsh-connect-workbuddy/benefit/claim";
51
51
  /** Plugin-owned live model refresh endpoint. */
52
52
  const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
53
+ /**
54
+ * Plugin-owned MEDIA model list endpoint (image + video, read from the region's
55
+ * `/v3/config`).
56
+ *
57
+ * Separate from {@link WORKBUDDY_MODELS_REFRESH_PATH} because it answers a
58
+ * different question from a different document projection: the refresh route
59
+ * returns the CHAT roster, from which `selectCliModels` drops every media
60
+ * entry, while this one keeps the `text-to-image`/`image-to-image`/
61
+ * `text-to-video`/`image-to-video` rows the card lists. Read-only and free —
62
+ * it is the same GET the image tool already performs.
63
+ */
64
+ const WORKBUDDY_MEDIA_MODELS_PATH = "/plugins/dsh-connect-workbuddy/media-models";
53
65
  /** Plugin-owned local account rescan endpoint. */
54
66
  const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
55
67
  /**
@@ -4981,6 +4993,27 @@ window.__ModuleLoader__.load({
4981
4993
  .dsm-workbuddy-pref-lead{display:flex;align-items:center;justify-content:flex-start;gap:10px;min-width:0}
4982
4994
  .dsm-workbuddy-pref-lead .dsm-workbuddy-pref-label{flex:none}
4983
4995
  .dsm-workbuddy-pref-hint{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:17px}
4996
+ /* The media roster: a READ-ONLY list, so it is drawn as quiet rows rather than
4997
+ as a table — it answers "what can this tab draw with" in passing, and must
4998
+ not compete with the switches above it for attention. The id is monospace
4999
+ and dimmer than the name because it is the string a tool call has to pass,
5000
+ not the thing a human reads; the tag chips carry the only colour, one hue
5001
+ per direction (text→image vs image→image), so the grouping is visible
5002
+ without a legend. */
5003
+ /* Aligned with the indented rows above it, but under its OWN class: the
5004
+ credit position picker owns .dsm-workbuddy-pref-indent, and the card suite
5005
+ resolves that selector expecting the picker. This block is a block (lead
5006
+ row + hint + list), not a single indented line. */
5007
+ .dsm-workbuddy-media{padding-left:25px}
5008
+ .dsm-workbuddy-media-list{margin:2px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
5009
+ .dsm-workbuddy-media-row{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px;min-width:0}
5010
+ .dsm-workbuddy-media-name{color:var(--dsw-alias-label-secondary,#b8bcc4);font-size:12px}
5011
+ .dsm-workbuddy-media-id{color:var(--dsw-alias-label-tertiary,#8b8f98);font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere}
5012
+ .dsm-workbuddy-media-tags{display:inline-flex;flex-wrap:wrap;gap:4px}
5013
+ .dsm-workbuddy-media-tag{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:0 6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8b8f98)}
5014
+ .dsm-workbuddy-media-empty,.dsm-workbuddy-media-error{font-size:12px;line-height:17px}
5015
+ .dsm-workbuddy-media-empty{color:var(--dsw-alias-label-tertiary,#8b8f98)}
5016
+ .dsm-workbuddy-media-error{color:var(--dsw-alias-label-error,#e5675f)}
4984
5017
  /* The supplement to the host's fixed quota sentence (issue #63). The plugin no
4985
5018
  longer claims the host's shell.quota-notice chain seat, so this no longer
4986
5019
  paints into shell.overlay — that seat is the reason the previous build's
@@ -6062,6 +6095,32 @@ window.__ModuleLoader__.load({
6062
6095
  * text-only model and an unverified one look identically broken next to their
6063
6096
  * checked siblings.
6064
6097
  */
6098
+ /**
6099
+ * The media roster from the Host's JSON, tolerantly.
6100
+ *
6101
+ * Anything that is not a row with a string id and a name is DROPPED, never
6102
+ * rendered: this is a read-only list, and a half-shaped entry is not worth an
6103
+ * exception that would take down the whole card. A Host older than the route
6104
+ * (or any other answer without `models`) yields an empty list, which the block
6105
+ * shows as "this tab publishes none" instead of as a failure.
6106
+ */
6107
+ function readMediaRoster(value) {
6108
+ const models = value?.models;
6109
+ if (!Array.isArray(models)) return void 0;
6110
+ const rows = [];
6111
+ for (const entry of models) {
6112
+ const record = entry;
6113
+ if (record === null || typeof record !== "object") continue;
6114
+ if (typeof record.id !== "string" || record.id === "") continue;
6115
+ const tags = Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : [];
6116
+ rows.push({
6117
+ id: record.id,
6118
+ name: typeof record.name === "string" ? record.name : record.id,
6119
+ tags
6120
+ });
6121
+ }
6122
+ return rows;
6123
+ }
6065
6124
  function imageCheckboxHint(model, t) {
6066
6125
  switch (nativeModalityOf(model.id)) {
6067
6126
  case "multimodal": return t("row.modelImage");
@@ -6443,6 +6502,18 @@ window.__ModuleLoader__.load({
6443
6502
  const [saveError, setSaveError] = (0, react.useState)(void 0);
6444
6503
  const [switchingAccount, setSwitchingAccount] = (0, react.useState)(false);
6445
6504
  /**
6505
+ * The region's MEDIA model roster (read-only), keyed by region.
6506
+ *
6507
+ * NOT part of `statusByRegion`: the usage document is the editable model
6508
+ * directory, and folding media rows into it would put them in the saved
6509
+ * `lastCatalog` — where the adapter would offer an image model as a chat
6510
+ * model. It is a separate, cheap, read-only read that only the capability
6511
+ * panel needs.
6512
+ */
6513
+ const [mediaByRegion, setMediaByRegion] = (0, react.useState)({});
6514
+ const [mediaError, setMediaError] = (0, react.useState)(void 0);
6515
+ const [mediaBusy, setMediaBusy] = (0, react.useState)(false);
6516
+ /**
6446
6517
  * Whether the account pool's save is in flight. Both sections write into one
6447
6518
  * region slot and the Host merges per-region, so two concurrent saves can
6448
6519
  * interleave on a stale base and revert each other while both report success
@@ -7112,6 +7183,72 @@ window.__ModuleLoader__.load({
7112
7183
  if (mounted.current) setTogglingCredits(void 0);
7113
7184
  }
7114
7185
  };
7186
+ /**
7187
+ * Read the active region's media roster. Read-only and free (the same GET the
7188
+ * image tool performs), so it is safe to run whenever the capability panel is
7189
+ * shown; `refresh` bypasses the Host's 5-minute cache for the explicit
7190
+ * button, because a user who just enabled an image model upstream should not
7191
+ * have to wait out a TTL to see it.
7192
+ *
7193
+ * A failure lands in `mediaError` INSIDE the block and nowhere else: it must
7194
+ * not touch `statusByRegion`, or one unreachable roster would replace the
7195
+ * whole panel with an error screen.
7196
+ */
7197
+ const loadMediaModels = (0, react.useCallback)(async (region, refresh, signal) => {
7198
+ try {
7199
+ const url = refresh ? `${withWorkBuddyRegion(WORKBUDDY_MEDIA_MODELS_PATH, region)}&refresh=1` : withWorkBuddyRegion(WORKBUDDY_MEDIA_MODELS_PATH, region);
7200
+ const response = await fetch(url, {
7201
+ method: "POST",
7202
+ headers: { accept: "application/json" },
7203
+ credentials: "same-origin",
7204
+ ...signal === void 0 ? {} : { signal }
7205
+ });
7206
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
7207
+ const models = readMediaRoster(await response.json().catch(() => void 0));
7208
+ if (models === void 0) throw new Error(`HTTP ${response.status}`);
7209
+ if (!mounted.current || signal?.aborted === true) return;
7210
+ setMediaByRegion((prev) => ({
7211
+ ...prev,
7212
+ [region]: models
7213
+ }));
7214
+ setMediaError(void 0);
7215
+ } catch (error) {
7216
+ if (!mounted.current || signal?.aborted === true) return;
7217
+ setMediaError(error instanceof Error ? error.message : t("row.requestFailed"));
7218
+ }
7219
+ }, [t]);
7220
+ /**
7221
+ * Regions whose media roster has already been asked for, so showing the
7222
+ * capability panel does not re-read the roster on every render — and so a
7223
+ * FAILED read is not retried in a loop. The explicit "re-read" button is the
7224
+ * only thing that asks twice, because a user who just enabled a model
7225
+ * upstream is the one case where a fresh answer is the point.
7226
+ */
7227
+ const mediaRequested = (0, react.useRef)(/* @__PURE__ */ new Set());
7228
+ /**
7229
+ * Read the media roster once per region, when the capability panel is opened.
7230
+ *
7231
+ * Lazy rather than alongside the usage fetch: the roster is only visible in
7232
+ * this one panel, and the usage fetch runs for BOTH regions on every open. If
7233
+ * the secret store happens to hold a credential for the region the user is not
7234
+ * looking at, an eager read would spend a request nobody asked for.
7235
+ */
7236
+ (0, react.useEffect)(() => {
7237
+ if (!open || panel !== "capabilities" || !hasAccounts) return;
7238
+ if (mediaRequested.current.has(activeRegion)) return;
7239
+ mediaRequested.current.add(activeRegion);
7240
+ const controller = new AbortController();
7241
+ loadMediaModels(activeRegion, false, controller.signal);
7242
+ return () => {
7243
+ controller.abort();
7244
+ };
7245
+ }, [
7246
+ open,
7247
+ panel,
7248
+ activeRegion,
7249
+ hasAccounts,
7250
+ loadMediaModels
7251
+ ]);
7115
7252
  const refreshModels = async () => {
7116
7253
  setBusy(true);
7117
7254
  try {
@@ -7601,12 +7738,14 @@ window.__ModuleLoader__.load({
7601
7738
  *
7602
7739
  * It sits first because it is the only preference here that authorizes SPEND
7603
7740
  * rather than display: the credit line is a readout, this one lets the model
7604
- * buy an image (about 5.71 credits) without asking again. CN-only, because the
7605
- * upstream publishes a `text-to-image` model on the CN gateway and none at all
7606
- * internationally — showing the box on the global tab would offer a control
7607
- * that can never do anything.
7741
+ * buy an image without asking again. Shown on BOTH tabs: each gateway now
7742
+ * publishes its own `text-to-image` model (CN `hunyuan-image-alpha`,
7743
+ * international `hy-image-3.5`, measured 2026-10-25), so the global box is no
7744
+ * longer a control that can never do anything. The flag is per-region and the
7745
+ * tool uses whichever region is BOTH enabled and switched on (CN first), so
7746
+ * ticking it here only ever authorizes spending on THIS tab.
7608
7747
  */
7609
- const imageToolPreference = activeRegion === "cn" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7748
+ const imageToolPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7610
7749
  className: "dsm-workbuddy-pref",
7611
7750
  title: t("row.imageToolHint"),
7612
7751
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -7627,7 +7766,86 @@ window.__ModuleLoader__.load({
7627
7766
  children: t("row.imageToolHint")
7628
7767
  })]
7629
7768
  })]
7630
- }) : null;
7769
+ });
7770
+ /**
7771
+ * What this region can actually draw, READ-ONLY.
7772
+ *
7773
+ * There is deliberately no picker here. Each tag group holds exactly one
7774
+ * model in both regions today (measured 2026-10-25), so a dropdown would be a
7775
+ * choice of one — the card would be inviting a decision that does not exist.
7776
+ * What the user DOES need is the answer to "can this tab draw at all, and
7777
+ * with what" — which is also what makes the image-tool switch above
7778
+ * meaningful on the international tab.
7779
+ *
7780
+ * Indented like the position picker, because it is a property of the switch
7781
+ * above it rather than a fourth independent setting.
7782
+ */
7783
+ const mediaTagLabels = {
7784
+ "text-to-image": t("row.mediaTagTextToImage"),
7785
+ "image-to-image": t("row.mediaTagImageToImage"),
7786
+ "text-to-video": t("row.mediaTagTextToVideo"),
7787
+ "image-to-video": t("row.mediaTagImageToVideo")
7788
+ };
7789
+ const activeMedia = mediaByRegion[activeRegion];
7790
+ const mediaModelsPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7791
+ className: "dsm-workbuddy-pref dsm-workbuddy-media",
7792
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7793
+ className: "dsm-workbuddy-pref-copy",
7794
+ children: [
7795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7796
+ className: "dsm-workbuddy-pref-lead",
7797
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7798
+ className: "dsm-workbuddy-pref-label",
7799
+ children: t("row.mediaModelsLabel")
7800
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7801
+ type: "button",
7802
+ className: "dsm-btn dsm-btn-outline",
7803
+ disabled: mediaBusy || !activeRegionOn,
7804
+ onClick: () => {
7805
+ setMediaBusy(true);
7806
+ loadMediaModels(activeRegion, true).finally(() => {
7807
+ if (mounted.current) setMediaBusy(false);
7808
+ });
7809
+ },
7810
+ children: mediaBusy ? t("row.mediaModelsRefreshing") : t("row.mediaModelsRefresh")
7811
+ })]
7812
+ }),
7813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7814
+ className: "dsm-workbuddy-pref-hint",
7815
+ children: t("row.mediaModelsHint")
7816
+ }),
7817
+ mediaError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7818
+ className: "dsm-workbuddy-media-error",
7819
+ children: t("row.mediaModelsError", { message: mediaError })
7820
+ }) : activeMedia === void 0 ? null : activeMedia.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7821
+ className: "dsm-workbuddy-media-empty",
7822
+ children: t("row.mediaModelsEmpty")
7823
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
7824
+ className: "dsm-workbuddy-media-list",
7825
+ children: activeMedia.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
7826
+ className: "dsm-workbuddy-media-row",
7827
+ children: [
7828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7829
+ className: "dsm-workbuddy-media-name",
7830
+ children: model.name
7831
+ }),
7832
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7833
+ className: "dsm-workbuddy-media-id",
7834
+ children: model.id
7835
+ }),
7836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7837
+ className: "dsm-workbuddy-media-tags",
7838
+ children: model.tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7839
+ className: "dsm-workbuddy-media-tag",
7840
+ children: mediaTagLabels[tag] ?? tag
7841
+ }, tag))
7842
+ })
7843
+ ]
7844
+ }, model.id))
7845
+ })
7846
+ ]
7847
+ })
7848
+ });
7631
7849
  /**
7632
7850
  * The CLI-route switch.
7633
7851
  *
@@ -7870,6 +8088,7 @@ window.__ModuleLoader__.load({
7870
8088
  cliBridgePreference,
7871
8089
  cliBridgeSwitchedOn() ? cliBridgePickers : null,
7872
8090
  imageToolPreference,
8091
+ mediaModelsPreference,
7873
8092
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7874
8093
  className: "dsm-workbuddy-pref",
7875
8094
  title: activeRegion === "cn" ? t("row.showCreditsCnHint") : t("row.showCreditsGlobalHint"),
@@ -10616,7 +10835,17 @@ window.__ModuleLoader__.load({
10616
10835
  "row.cliBridgeRegionOrderGlobalOnly": "Global only",
10617
10836
  "row.cliBridgePickersHint": "All three pickers write the global cliBridge block; \"CLI default\" on model and effort is an explicit empty value, not \"unset\" — the CLI then uses its own defaults; the region picker writes cliBridge.regionOrder.",
10618
10837
  "row.imageToolLabel": "Let the model draw images",
10619
- "row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image, with no per-call confirmation. Off means the tool is not registered at all.",
10838
+ "row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image on the CN gateway and about 0.03 on the international one, with no per-call confirmation. Ticking it here only authorizes THIS tab's region. Off everywhere means the tool is not registered at all.",
10839
+ "row.mediaModelsLabel": "Image models in this region",
10840
+ "row.mediaModelsHint": "Read from this account's product configuration. The draw tool picks its model from here: a prompt without an image uses a text-to-image model, one WITH an image uses an image-to-image model. Read-only — to use a different model, pass it to the tool.",
10841
+ "row.mediaModelsEmpty": "This region publishes no image or video model.",
10842
+ "row.mediaModelsError": "Could not read the media model list: {message}",
10843
+ "row.mediaModelsRefresh": "Re-read",
10844
+ "row.mediaModelsRefreshing": "Reading…",
10845
+ "row.mediaTagTextToImage": "text → image",
10846
+ "row.mediaTagImageToImage": "image → image",
10847
+ "row.mediaTagTextToVideo": "text → video",
10848
+ "row.mediaTagImageToVideo": "image → video",
10620
10849
  "row.showCreditsCn": "Show credits in the composer toolbar",
10621
10850
  "row.showCreditsGlobal": "Show credits in the composer toolbar",
10622
10851
  "row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a one-click test of the checked accounts (the test spends real credits). The readout refreshes by itself every 5 minutes; the per-account table every 60 seconds while it is open.",
@@ -11031,7 +11260,17 @@ window.__ModuleLoader__.load({
11031
11260
  "row.cliBridgeRegionOrderGlobalOnly": "仅国际",
11032
11261
  "row.cliBridgePickersHint": "三个选择都写入全局 cliBridge 块;模型/档位的\"CLI 默认\"是显式的空值而非未设置,届时 CLI 使用自己的默认;区域选择写入 cliBridge.regionOrder。",
11033
11262
  "row.imageToolLabel": "允许模型自主生图",
11034
- "row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——约 5.71 积分/张,没有逐次确认。关闭时该工具根本不会注册。",
11263
+ "row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——国内区约 5.71、国际区约 0.03 积分/张,没有逐次确认。在这里勾选只授权当前这一区的账号。两区都关着时该工具根本不会注册。",
11264
+ "row.mediaModelsLabel": "本区可用的图像模型",
11265
+ "row.mediaModelsHint": "读自当前账号的产品配置。生图工具就按这里挑模型:不填图片走「文生图」那一组,填了图片走「图生图」那一组。这里是只读的——想换模型请在调用工具时指定。",
11266
+ "row.mediaModelsEmpty": "这一区的产品配置里没有图像或视频模型。",
11267
+ "row.mediaModelsError": "读取媒体模型清单失败:{message}",
11268
+ "row.mediaModelsRefresh": "重新读取",
11269
+ "row.mediaModelsRefreshing": "正在读取…",
11270
+ "row.mediaTagTextToImage": "文生图",
11271
+ "row.mediaTagImageToImage": "图生图",
11272
+ "row.mediaTagTextToVideo": "文生视频",
11273
+ "row.mediaTagImageToVideo": "图生视频",
11035
11274
  "row.showCreditsCn": "在输入框工具栏显示积分读数",
11036
11275
  "row.showCreditsGlobal": "在输入框工具栏显示积分读数",
11037
11276
  "row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新。",