dsh-connect-workbuddy 5.10.0 → 5.12.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 +119 -0
- package/README.en.md +6 -1
- package/README.md +8 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +242 -10
- package/lib/{host-heartbeat-_EeM2YDF.js → host-heartbeat-xEJRdYaC.js} +445 -16
- package/lib/index.d.ts +281 -1
- package/lib/index.js +669 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.12.0 (2026-10-26)
|
|
4
|
+
|
|
5
|
+
> 特性版本:新增 `dropForeignThinking`(**默认关闭**),修 issue #147——跨 provider 切换后
|
|
6
|
+
> pi-ai 把外来轮次的 thinking 改写成正文,模型随后照抄这个格式,整轮回答变成英文工作笔记。
|
|
7
|
+
> 关闭时请求字节与 5.11.0 完全一致;打开后只删「本来会被压成正文、且不可能作为带签名 reasoning
|
|
8
|
+
> 重放」的外来 thinking 块,正文块、工具调用与每条消息的身份都不动。请求路径、provider 路由、
|
|
9
|
+
> 计费与凭据处理一律未变。
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- **思考变正文:跨 provider 切换后模型把工作笔记写进回答(#147)**
|
|
14
|
+
- **病因在 pi-ai,不在本插件。** `transform-messages.js:68` 用 `isSameModel`(provider **且** api **且** model id)决定一条历史 thinking 能否按 reasoning 重放;不成立时它**不丢弃**该块,而是改写成 `{type:'text', text: thinking}`(`:87–90`)——也就是答案通道。会话一旦跨 provider 切换过,另一个模型的内部工作笔记就以「本轮回答」的身份出现,模型读到「我过去的思考全是正文」这个形状后持续照抄。
|
|
15
|
+
- **实测复现(本机 pi-ai 0.85.1 + 真实 `transformMessages`)**:`api: 'dsh-foreign'` 的外来轮次 → `[text]`(200/300 字符全部进正文);本插件自己的轮次 → `[thinking]`;把该块丢掉 → `[]`(不产生任何正文)。报告者的一轮真实会话里,80 条 assistant 消息中 56 条带正文而 `reasoning` ≤ 1 字符,20 条英文工作笔记合计 58,960 字符,CJK 占比跌到 1.37%;会话从 696,966 涨到 758,005 total token。
|
|
16
|
+
- **为什么是「删除」而不是「保留为 thinking」**:`isSameModel` 只比对消息自身字段,唯一的满足方式是把这个外来轮次宣称为本模型的输出——那正是 #111 刚删掉的来源谎言。而这些块由宿主 `foreignAssistant()` 构造、**不带 `thinkingSignature`**,即便被宣称也无法作为带签名 reasoning 重放,它们对线上唯一的贡献就是被压成的那段正文。
|
|
17
|
+
- **修法**:新增 `src/adapter.ts` 的 `foreignThinkingOf()`(只读诊断)与 `dropForeignThinking()`(实施删除),在 `withLegacyContext` 的 `prepare` 里、**`alignAssistantIdentities` 之后**执行。顺序是承重的:国际区自己的轮次到达时同样带 `api: 'dsh-foreign'`,先诊断会把它误判成外来轮次并删掉它自己的推理(#102 的反向回归)。
|
|
18
|
+
- **开关**:`dropForeignThinking`(volatile,默认 `false`)。用 getter 读活值,拨动后**下一个请求**即生效,不需要重载插件——用户正是在看着那条工作笔记时打开它。
|
|
19
|
+
- **诊断可见**:被删的块数与位置写入 status 端点的 `lastForeignThinking`(进程内、last-wins、重启即忘),`dropForeignThinking` 开关本身也随 status 下发,供卡片渲染提示。
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- 新增 `tests/issue147-foreign-thinking.spec.ts`(15 条),全部经过**真实 pi-ai 的 `transformMessages`**,而不是桩:缺陷本身可复现、开启后不产生正文、关闭时原对象返回(零改动)、本插件国内外两区轮次都保留 thinking、正文与工具调用不被触碰、redacted 与空块不误报、错误轮次报 `dropped` 变体、顺序必须在 alignment 之后、幂等。
|
|
24
|
+
- 变异验证:把 `dropForeignThinking` 改成空操作 → 4 条变红;把判断放宽到所有 assistant 轮次 → 7 条变红。
|
|
25
|
+
- 全量:**142 文件 / 2458 通过(2 条按平台跳过)**;两个 tsconfig 均 exit 0。
|
|
26
|
+
|
|
27
|
+
## 5.11.0 (2026-10-26)
|
|
28
|
+
|
|
29
|
+
> 特性版本:新增 `workbuddy_draw_video`(文生视频 / 图生视频,异步任务「提交 → 轮询 → 下载」,
|
|
30
|
+
> 逐区 opt-in、默认关闭),卡片「能力」栏新增视频开关与**按模式钉模型**;同期把生图 `size`
|
|
31
|
+
> 的描述补上实测的大图档位与 419 万像素上限(**执行层零改动**,改的是模型对「能画多大」的先验)。
|
|
32
|
+
> 视频工具落地后按现场报错修了三处:**时长校验形同虚设**、**参数错误也白等 60 秒冷却**、
|
|
33
|
+
> 以及**显式 `resolution` 在拿不到目录清单时被静默丢弃**。请求路径、provider 路由与凭据处理
|
|
34
|
+
> 一律未变:视频打的是官方 CLI `VideoService` 早已在用的两条 `/v2/videos/*` 路由,选模型仍只看
|
|
35
|
+
> 该区 `/v3/config` 的媒体标签。
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
- **新增 `workbuddy_draw_video`:文生视频 / 图生视频,国内区与国际区都能注册,逐区 opt-in**
|
|
40
|
+
- **它不是「接一次 `submitVideo()`」**:视频是异步任务——`POST /v2/videos/generations` 只回一个 `taskId`,还要轮询 `POST /v2/videos/tasks` 到 `completed`(5 秒一次,上限 600 秒),再 `GET` 每条 `data[].url` 把 mp4 拉到本地。旧文档「加视频只是接一次 `submitVideo()`」少说了后两段,已在 `docs/VIDEO_GEN.md` 对账。
|
|
41
|
+
- **参数层与选模型层是复用的**:`MEDIA_TAGS`、`VIDEO_TAG_*`、`fetchMediaModels`、卡片媒体清单原本就认得两个视频标签,所以参数校验与选模型只补了一层 `src/video-gen.ts`;**执行层是全新异步代码**(`src/upstream.ts` 的 `submitVideo` / `pollVideoTask` / `downloadVideo`)。
|
|
42
|
+
- **花钱的三条红线照搬生图**:独占执行(不声明 `isConcurrencySafe`)、失败后 60 秒冷却、冷却**只看请求是否真的发出**(参数错、未登录、读不到输入图一律不进冷却);工具**逐区 opt-in**,不勾时根本不注册,模型看不到它。
|
|
43
|
+
- **产物落本地临时目录**:视频是二进制,不进对话体,工具只回 `file://` 路径 + 渲染成链接(`os.tmpdir()/workbuddy-video-gen/<model>-<时间戳>.mp4`)。
|
|
44
|
+
- **顺手改对一处描述**:`seconds` 原写「越界会被夹回默认值并提示」,而 `resolveVideoParams` 实际抛
|
|
45
|
+
`INVALID_ARGS`(不会夹取)——描述会教调用方传 30 秒再等一个 5 秒的结果,然后把报错当成 bug。
|
|
46
|
+
- **验证**:`tsc` 两套配置 0 error;`vitest run` 141 个文件 / **2445 条**通过(含新增
|
|
47
|
+
`tests/video-gen.spec.ts`、`tests/video-tool.spec.ts`、`tests/video-tool-optin.spec.ts`);
|
|
48
|
+
`tsdown` 构建通过。
|
|
49
|
+
|
|
50
|
+
- **实测单价与产物规格写进文档了(`docs/VIDEO_GEN.md`「实测单价与产物规格」)**
|
|
51
|
+
- **国内区实测单价**:图生视频 4 秒 `minimax-video-h3` 57.14 / `kling-video-v3` 68.57;
|
|
52
|
+
文生视频 5 秒 `minimax-video-h3` 71.43 / `kling-video-v3` 128.57 积分。`minimax-video-h3`
|
|
53
|
+
文生图生同价(14.29/s),`kling-video-v3` 图生比文生便宜(17.14/s 对 25.71/s)——
|
|
54
|
+
「一条 5 秒多少积分」要连模式一起看。
|
|
55
|
+
- **产物实测**(解析 mp4 的 `moov` 盒,不靠上游自报):H.264 + AAC、**24fps**、
|
|
56
|
+
`1280x720` 或 `1344x768`(这两个尺寸是**默认档位**下的产物;见下方 Fixes 第三条——
|
|
57
|
+
「没有 1080p」此前被记成上游行为,实则其中一部分是本插件把 `resolution` 丢掉了),
|
|
58
|
+
视频码率 3.66–11.47 Mbps、体积 2.0–7.0 MB;**实际时长按帧数走**,请求 5 秒拿回
|
|
59
|
+
5.09–5.17 秒,请求 4 秒拿回 4.04–4.46 秒。`enable_audio: true` 也不保证有音轨。
|
|
60
|
+
- **卡片上按模式钉住视频模型(软约束,与显式 `model` 分工明确)**
|
|
61
|
+
- **起因**:国内区每个视频标签组都有 2 个模型(`kling-video-v3` / `minimax-video-h3`),调用方多半省掉 `model`,于是永远拿到目录里的第一个;口头说一次不持久。
|
|
62
|
+
- **按区、按标签存**:`regions.<区>.preferredVideoModels` 是 `{ [tag]: modelId }`——两种模式可能由不同模型提供,且模型 id 不跨区(`minimax-video-h3` 在国际区不存在)。
|
|
63
|
+
- **软约束**:显式 `model` 是调用方为这一次写的,写错就报 `NO_VIDEO_MODEL`;卡片偏好是用户以前写的,钉住的模型已从目录里消失时**静默回落组内第一个**。两条方向相反的规则各有测试钉住。
|
|
64
|
+
- **清空 = 删 key**,不是写空串;卡片读值先问 Host(`statusByRegion`),浏览器配置快照只作回落。
|
|
65
|
+
- **写入走与开关同一条「写后回读校验」路径**(`writeRegionPreferredVideoModels`),不是裸 `scope.set`。
|
|
66
|
+
|
|
67
|
+
### Fixes
|
|
68
|
+
|
|
69
|
+
- **视频工具:显式 `resolution` 在拿不到目录清单时被静默丢弃(视频同步找到的第三处)**
|
|
70
|
+
- **症状与上面那条同源**:`resolveVideoParams` 用 `new Set(config?.resolutions ?? [])` + `has()`
|
|
71
|
+
判断,于是「模型不支持这个值」与「我们根本没有这份清单」两条路径合流——**任何**显式
|
|
72
|
+
`resolution` 在目录没下发 `videoGeneration.resolutions[]` 的账号上都被丢掉,`buildVideoBody`
|
|
73
|
+
收到的已是 `undefined`,请求体里连 `resolution` 字段都没有,却还打印一句
|
|
74
|
+
「resolution 1080P 不被模型 · 支持;改用 上游默认」,看起来像上游拒绝。
|
|
75
|
+
- **同一条判据修正**:只有真的拿到 `resolutions[]` 才做回落,拿不到就**原样透传**交给上游判
|
|
76
|
+
——与 `seconds` 的「拿不到区间就不盲拒」完全一致。
|
|
77
|
+
- **顺带修掉大小写**:清单匹配改为大小写不敏感,命中后发**目录的拼写**。官方 CLI 的兜底档位是
|
|
78
|
+
`["720P","1080P"]`,而工具描述一直举 `720p`/`1080p` 的例子,精确匹配会把一个其实可用的值
|
|
79
|
+
判成不支持。
|
|
80
|
+
- **工具描述与 `docs/VIDEO_GEN.md` 同步改正**(文档此前把这个 bug 的现象记成了上游的行为,
|
|
81
|
+
并据此写了「`resolution: "1080p"` 对 `minimax-video-h3` 不成立」——那条结论在拿不到清单的
|
|
82
|
+
账号上从未真正验证过)。
|
|
83
|
+
- **验证**:`tests/video-gen.spec.ts` 新增 2 条(无清单/空清单透传、大小写命中),全量
|
|
84
|
+
`vitest run` 141 个文件 / **2445 条**通过。
|
|
85
|
+
|
|
86
|
+
- **视频工具:`seconds: 1` 被上游 400 退回后还要白等 60 秒(两处真 bug)**
|
|
87
|
+
- **现场**:`Error: workbuddy video: 提交生视频任务失败(HTTP 400): code 14407: Create video failed with error: unsupported video duration: 1 seconds, supported range: 4-15 seconds`,紧接着第二次调用被挡成「请 38 秒后再试」。用户的判断是「上游返回这种错误,也要等半天」。
|
|
88
|
+
- **根因一(本地时长校验是死代码)**:`src/video-tool.ts` 调 `resolveVideoParams({...})` 时**没传第二个参数 `config`**;而 `src/video-gen.ts` 的 `minSeconds`/`maxSeconds` 校验只在 config 存在时才跑。更上游还有一处:`src/image-gen.ts` 的 `selectMediaModels` 只搬 `{id,name,tags}`,把条目上的 `videoGeneration` 丢了——所以即使传了也拿不到。
|
|
89
|
+
- **根因二(没花钱的失败也进冷却)**:`describeVideoFailure` 把提交阶段的一切非 401/403/404/429 都归成 `UPSTREAM`,而 `UPSTREAM` 在冷却集合里。可 400 是「建任务之前就被退回」,没有 task、没有扣积分。
|
|
90
|
+
- **修法**:`selectMediaModels`(video 与 image 两处)保留并透传 `videoGeneration`(`readVideoGeneration` 只搬已知字段,缺字段不补默认值),工具把它喂给 `resolveVideoParams`——越界时长现在**在本地就被拦下,一次付费请求都不发**;提交阶段其它 4xx 单独归类为 `REJECTED`(不进冷却,错误里明说「没有创建任务、没有扣积分」并保留上游原文)。
|
|
91
|
+
- **顺带学会区间**:目录不带 `videoGeneration` 的账号(实测部分国内区账号的 `/v3/config` 条目只有 `{id,name,tags}`)本地无从校验,只能让上游先拒一次;`parseSupportedSecondsRange` 从它的文案里读出 `supported range: 4-15`,按**区域 + 模型 id**记在本次进程里,第二次同样的越界值**零请求、零冷却**直接报「时长必须在 4 到 15 秒之间」。读不出区间(例如拒的是宽高比)就什么都不记——不猜。
|
|
92
|
+
- **验证**:`tsc` 两套配置 0 error;`vitest run` 141 个文件 / **2445 条**通过(`tests/video-gen.spec.ts`
|
|
93
|
+
35→48、`tests/video-tool.spec.ts` 7→12;新增用例钉住「提交 400 之后下一次仍会真的发请求」
|
|
94
|
+
与「学到区间后第二次不出进程」)。
|
|
95
|
+
|
|
96
|
+
- **`size` 的描述让人以为只能出 1024 级方图,而实测上限是约 419 万像素**
|
|
97
|
+
- **起因**:5.10.0 把 `size` 描述从单个 `1024x1024` 例子改成方 / 竖 / 横三个例子,解决了
|
|
98
|
+
「只能出方图」那个错误先验;但三个例子全是 1024 级,模型据此认为「大图也就是这个量级」,
|
|
99
|
+
用户要桌面壁纸、竖屏壁纸时只会得到一张 1024 宽的照片。
|
|
100
|
+
- **2026-10-25 逐档实测(国内区 `hunyuan-image-alpha`)**:`2048x2048`(4.19 MP)、
|
|
101
|
+
`2048x1536`(3.15 MP)、`2048x1152`(2.36 MP)、`2560x1440`(3.69 MP)、
|
|
102
|
+
`2560x1600`(4.10 MP)**全部成功,输出宽高与请求完全一致**——不是把大图静默压回原生
|
|
103
|
+
尺寸再放大,画面里的刻度文字与纹理是独立线条。`2560x2560`(6.55 MP)、`3072x3072`
|
|
104
|
+
(9.44 MP)、`4096x4096`(16.8 MP)**全部被直接拒绝**,不会退回一个较小尺寸。
|
|
105
|
+
- **据此定上限**:约 **419 万总像素**,任意长宽比。这**推翻**了流传的判断「原生上限约
|
|
106
|
+
1536 长边」——那个数字来自官方 CLI `.describe()` 的三个例子与 bundled OpenAI SDK 类型
|
|
107
|
+
定义的取值范围,从来没有实测过;实测表明 2560 长边是真实可用的。
|
|
108
|
+
- **修法**:`src/image-tool.ts` 的 `size` 描述加入 `2048x2048` / `2048x1536` /
|
|
109
|
+
`2560x1440` / `2560x1600` 四个**实测通过**的档位、写明「约 419 万像素」上限、并点名
|
|
110
|
+
`2560x2560` / `3072x3072` / `4096x4096` 会被拒(防止模型往 4K 上撞)。
|
|
111
|
+
- **只写实测过的尺寸**:`1440x2560`(手机竖屏壁纸的常见答案)**没有单独跑过**,它与已通过
|
|
112
|
+
的 `2560x1440` 像素数相同、只转 90°,按像素阈值推断可用,但描述里不写它——描述只给实测
|
|
113
|
+
通过的宽高;`docs/IMAGE_GEN.md` 的「尺寸与比例」小节把 `1440x2560` 单列一行并标注
|
|
114
|
+
「未单独实测」。
|
|
115
|
+
- **验证**:`tests/image-gen.spec.ts` 新增 1 条(描述同时含 `2048x2048` / `2560x1440` /
|
|
116
|
+
`419` / `4096x4096`,防止以后有人把上限改回「只给三个 1024 例子」);
|
|
117
|
+
`docs/IMAGE_GEN.md` 的「尺寸与比例」换成 11 行实测对照表 + 按用途选尺寸表;
|
|
118
|
+
README 中英各补一句。
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
3
122
|
## 5.10.0 (2026-10-25)
|
|
4
123
|
|
|
5
124
|
> 特性版本:`workbuddy_draw_image` 从「只在国内区注册、只能文生图」扩成「国内区与国际区都能用、文生图 + 改图(图生图)」,卡片「能力」栏新增该区媒体模型的只读清单。**请求路径、provider 路由、计费口径与凭据处理一律未变**:改图打的是官方 CLI `ImageService.editImage` 早就用的 `POST /v2/images/edits`,选模型仍然只看该区 `/v3/config` 的媒体标签。
|
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 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.
|
|
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.
|
|
@@ -482,6 +482,11 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
482
482
|
|
|
483
483
|
- **Switching region makes the context "grow", and can hit `code 11115` immediately — the gateway is not over-charging you.** The same history is counted under different buckets in the two regions, and the root cause is how the host's bundled `@earendil-works/pi-ai` replays history. It evaluates `isSameModel` for every assistant message (`transform-messages.js:68`, keyed on provider **and** api **and** model id all being equal): for a message that fails, its thinking is **downgraded to ordinary content** (`:88–96` — not dropped), and content is what the gateways count, while thinking goes out in a different field. This plugin registers the domestic and international editions as **two provider ids** (`workbuddy` / `workbuddy-global`, see [How it works](#how-it-works)) that carry the **same model id** — so one region switch moves **all the other side's thinking into the content bucket at once**. That is the cause of issue #59's "the same conversation history is charged 1.374M by the international gateway and 708K by the domestic one (1.94×)"; switching back is the same formula read the other way, 0.696×.
|
|
484
484
|
- Three consequences you can see: a region switch can move the token count by ±30%–94%, and **how far depends only on the other side's thinking character count, not on total history length**; **switching models inside one region** triggers the same downgrade (the model leg of `isSameModel`), so the count can move without a region switch at all; and that jump lands on top of the host's current compaction threshold (`min(0.8 × window, window − 128000 − 65536)`), so a session that was comfortably under the limit can hit `code 11115 prompt is too long` the moment you switch region.
|
|
485
|
+
- **The downgrade is not only a counting problem: the reasoning becomes the answer, and the model then copies that shape (issue #147).** The bullet above covers what the downgrade does to BILLING; this one covers what it does to CONTENT. For a message where `isSameModel` fails, pi-ai does not drop its thinking — it rewrites each block to `{type:'text'}` (`transform-messages.js:87–90`), i.e. the answer channel. So after a provider switch, **another model's internal working notes are presented as this turn's reply**, and a model that reads "all of my past reasoning was plain text" keeps writing its plan into the text channel from then on. The reporter measured one real turn: 56 of 80 assistant messages carried an answer while their own `reasoning` was ≤ 1 character, 20 of those held English working notes totalling 58,960 characters, and CJK fell to 1.37% of the 60,984 visible characters where the previous six turns had been entirely Chinese. It also pushed the session from 696,966 to 758,005 total tokens against an 800,000 compaction threshold.
|
|
486
|
+
- **`dropForeignThinking` (off by default)**: when on, this plugin **removes** those foreign thinking blocks before the request goes out, instead of letting them be flattened into prose. Removal rather than preservation, because these blocks are built by the host's `foreignAssistant()` and carry **no `thinkingSignature`** — even claimed as our own turn they could never be replayed as signed reasoning, so their only possible contribution to the wire was the prose they became. Text blocks and tool calls are never touched, and neither is any message's identity.
|
|
487
|
+
- With the switch off the request is **byte-for-byte** what earlier builds sent: it changes bytes the upstream counts, so it is an explicit opt-in on the same rule as `usageIdentity`.
|
|
488
|
+
- The ORDER is load-bearing: the repair runs **after** `alignAssistantIdentities`. A global-region turn of this plugin's own arrives carrying `api: 'dsh-foreign'` too (the host's replay-degrade path), so diagnosing first would classify it as foreign and delete its reasoning — exactly issue #102 in reverse. `tests/issue147-foreign-thinking.spec.ts` pins the order through pi-ai's real `transformMessages`.
|
|
489
|
+
- Turns already recorded in history are **not** retroactively repaired: they are already stored as prose, and the switch governs later requests only. Starting a new session after the provider switch is the direct way out.
|
|
485
490
|
- **This is a mismatch between the host and pi-ai, not a bug in this plugin**: not replaying thinking across providers/models is right in itself (another model's train of thought is not context this model should be fed), and the mistake is treating `provider` as a necessary condition for "the same model" when the two provider ids here are **two access regions of one model**. The fix belongs upstream (same api + same model id, providers as an equivalence group); on the plugin side the backstop is `src/context-rescue.ts` since 4.8.0 — **once the upstream confirms an overflow, it folds history in chunks and resends**, instead of replaying the entire range the host meant to compact (which is exactly what makes a compaction request overflow itself and deadlock). To see where it happened: set `WORKBUDDY_BODY_REPORT` to capture a body-composition report — it records counts only, never text. **It appends continuously**: one `<region>.jsonl` per region, rolled to `.1` past 32 MB (one old copy kept); unset the variable and delete the files when you are done — do not leave it on for long sessions.
|
|
486
491
|
|
|
487
492
|
## Disclaimer
|
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 积分一张、国际区约 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
|
|
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
|
|
|
@@ -495,6 +497,11 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
495
497
|
- **这是宿主与 pi-ai 之间的一处错配,不是本插件的 bug**:不跨 provider/模型重放 thinking 这件事本身是对的(别家模型的思考过程对当前模型没有上下文价值),错在把 `provider` 当作「是不是同一个模型」的必要条件,而这里两个 provider 字符串指的是**同一个模型的两个接入区域**。
|
|
496
498
|
- **本插件已在 4.9.0 起从插件侧绕开它**:两条路由注册的 **model 身份统一为 `workbuddy`**(`src/adapter.ts` 的 `WORKBUDDY_MODEL_IDENTITY`),也就是模型自己声明的 `provider` 字段两边相同,于是 `isSameModel` 成立、换区不再把 thinking 降级成正文。**路由 id 没有合并**——它仍是 `workbuddy` / `workbuddy-global`,各自指向自己的 shim、目录与凭据,所以一侧故障不会带走另一侧,模型选择器也仍是两组。**4.9.0/4.9.1 的这版修复漏了一半,国际版整区不可用(issue #75)**:上面的说法只对 `getModel(provider, id)` 这一条按**参数**查表的路径成立;真正跑聊天的 `models.streamSimple(model, …)` 第一步是 `requireProvider(model)` = `providers.get(model.provider)`(`models.js:351–355`),查的是**模型上盖的那个字段**。于是国内版(路由 `workbuddy`、盖 `workbuddy`)自洽,国际版(路由 `workbuddy-global`、盖 `workbuddy`)在只注册了 `workbuddy-global` 的注册表里查不到,**每一个请求都在发出前就抛 `Unknown provider: workbuddy`**——一次也会失败,且报错里不带任何 WorkBuddy 字样,看起来像宿主坏了。**4.9.2 起改为:每条路由把自己的注册表同时登记上「路由 id」和「共享身份」两个键**(`src/adapter.ts` 的 `modelIdentityKeysOf`),模型上盖的身份仍然两边相同(#59 不回退),而 `requireProvider` 在国际版自己的注册表里也能查到、并拿到**国际版自己的** provider 对象(含它自己的 shim 地址与凭据)。pi-ai 的注册表本来就是 `Map`(`models.js:37` 的 `providers.set(provider.id, provider)`,键取的是 **provider 对象的 `id`**,不是 profiles 映射的键),所以这是注册表原生支持的事;**路由集合、模型选择器、目录与凭据都没有变化**,多出来的那个键不出现在任何 UI 里。回归测试 `tests/issue75-global-provider-dispatch.spec.ts` 走的是宿主的真实构造(`createModels` + `setProvider` + `streamSimple`),并保留一个反证用例固定住旧的失败文案。详见 `docs/audit/ISSUE59-OPTIONS.md` 的「#75 CORRECTION」一节。
|
|
497
499
|
- 升级后仍可能看到计数变化的部分:**在同一区域内换模型**仍会降级(`model` 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的);**升级之前就录在历史里的消息**带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
|
|
500
|
+
- **降级不只是计数问题:思考会变成正文,模型随后照抄这个格式(issue #147)。** 上一条说的是「thinking 被降级」对计费的影响,这条说的是它对**内容**的影响。pi-ai 对 `isSameModel` 不成立的消息**不是丢弃** thinking,而是把它改写成 `{type:'text'}`(`transform-messages.js:87–90`)——也就是答案通道。于是跨 provider 切换后,**另一个模型的内部工作笔记会作为本轮回答呈现**,模型读到「我过去的思考都是正文」这个形状后,接下来的每一轮都继续把计划写进正文。报告者在一轮真实会话里量到:80 条 assistant 消息中 56 条带正文而自身 `reasoning` ≤ 1 字符,其中 20 条是英文工作笔记、合计 58,960 字符;整轮 60,984 字符正文里 CJK 只剩 1.37%,而此前 6 轮的可见回复全是中文。它同时把会话从 696,966 推到 758,005 total token(压缩阈值 800,000)。
|
|
501
|
+
- **开关 `dropForeignThinking`(默认关闭)**:打开后,本插件在请求发出前**删掉**那些「本来会被压成正文」的外来 thinking 块。之所以是删而不是保留:这些块由宿主以 `foreignAssistant()` 构造,**不带 `thinkingSignature`**,即使被当成自己的轮次也无法作为带签名的 reasoning 重放——它们对线上唯一的贡献就是被压成的那段正文。正文块与工具调用一律不动,每条消息的身份也不动。
|
|
502
|
+
- 关闭时**一个字节都不变**,与之前版本完全一致——它改的是上游会数到的字节,所以按 `usageIdentity` 的同一条规矩做成显式 opt-in。
|
|
503
|
+
- 顺序很关键:修复跑在 `alignAssistantIdentities` **之后**。国际区自己的轮次到达时也带 `api: 'dsh-foreign'`(宿主的重放降级路径),先诊断会把它误判成外来轮次并删掉它自己的推理——那正是 #102 的反向回归。`tests/issue147-foreign-thinking.spec.ts` 用真实 pi-ai 的 `transformMessages` 钉住了这个顺序。
|
|
504
|
+
- 已经录进历史的坏轮次**不会**被追溯修复:它们已经作为正文存在会话里,开关只管之后的请求。想彻底摆脱,切换 provider 后新开会话是最直接的办法。
|
|
498
505
|
- 真正的修法仍在上游(让 `isSameModel` 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 `src/context-rescue.ts`——**一旦上游确认超限,就按块折叠历史后重发**,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 `WORKBUDDY_BODY_REPORT` 抓一次 body 组成报告,它只记计数、不记正文。**注意它会持续追加**:每个区一份 `<region>.jsonl`,超过 32 MB 自动滚成 `.1`(只保留一份旧档);排查完请关闭该环境变量并删除报告文件,不要开着它长期跑。
|
|
499
506
|
|
|
500
507
|
## 免责声明
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates,
|
|
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-xEJRdYaC.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
|
@@ -478,6 +478,44 @@ window.__ModuleLoader__.load({
|
|
|
478
478
|
function regionImageToolEnabledOf(value, region) {
|
|
479
479
|
return regionSlotOf(value, region)["imageToolEnabled"] === true;
|
|
480
480
|
}
|
|
481
|
+
function regionVideoToolEnabledOf(value, region) {
|
|
482
|
+
return regionSlotOf(value, region)["videoToolEnabled"] === true;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The video model pinned per tag in one region, or an empty record.
|
|
486
|
+
*
|
|
487
|
+
* A PIN, not an authorisation: reading it grants nothing, and an absent entry
|
|
488
|
+
* merely means the tool takes that tag group's first model. Values are model ids
|
|
489
|
+
* from the same region's catalog. A stale id is tolerated on purpose — a catalog
|
|
490
|
+
* can drop a model the user had pinned, and that must not turn into a failed
|
|
491
|
+
* generation (the tool falls back to the group's first model).
|
|
492
|
+
*/
|
|
493
|
+
function regionPreferredVideoModelsOf(value, region) {
|
|
494
|
+
const raw = regionSlotOf(value, region)["preferredVideoModels"];
|
|
495
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
496
|
+
const out = {};
|
|
497
|
+
for (const [tag, id] of Object.entries(raw)) if (typeof id === "string" && id !== "") out[tag] = id;
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Build the next `regions` settings value for one pinned video model.
|
|
502
|
+
*
|
|
503
|
+
* `modelId` of `undefined` or `''` CLEARS that tag's pin, which is why this is
|
|
504
|
+
* a three-state write rather than a boolean: the card's picker has a "no pin"
|
|
505
|
+
* row, and deleting the key is what makes the absence mean "nothing pinned"
|
|
506
|
+
* again (an empty string would be a value the tool would have to special-case).
|
|
507
|
+
*
|
|
508
|
+
* Same merge rule as the switches above: only this region's field changes.
|
|
509
|
+
*/
|
|
510
|
+
function nextRegionPreferredVideoModels(value, region, tag, modelId) {
|
|
511
|
+
const pinned = { ...regionPreferredVideoModelsOf(value, region) };
|
|
512
|
+
if (modelId === void 0 || modelId === "") delete pinned[tag];
|
|
513
|
+
else pinned[tag] = modelId;
|
|
514
|
+
return nextRegionSlots(regionsMapOf(value), region, {
|
|
515
|
+
...regionSlotOf(value, region),
|
|
516
|
+
preferredVideoModels: pinned
|
|
517
|
+
});
|
|
518
|
+
}
|
|
481
519
|
/** The seat a region's readout uses, defaulting to the composer row. */
|
|
482
520
|
function regionCreditPositionOf(value, region) {
|
|
483
521
|
return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
|
|
@@ -520,6 +558,12 @@ window.__ModuleLoader__.load({
|
|
|
520
558
|
imageToolEnabled: enabled
|
|
521
559
|
});
|
|
522
560
|
}
|
|
561
|
+
function nextRegionVideoToolEnabled(value, region, enabled) {
|
|
562
|
+
return nextRegionSlots(regionsMapOf(value), region, {
|
|
563
|
+
...regionSlotOf(value, region),
|
|
564
|
+
videoToolEnabled: enabled
|
|
565
|
+
});
|
|
566
|
+
}
|
|
523
567
|
/**
|
|
524
568
|
* Project one card row into its persisted `lastCatalog` shape: the native
|
|
525
569
|
* context window becomes the stored `contextWindow`, and the card-only
|
|
@@ -1162,6 +1206,33 @@ window.__ModuleLoader__.load({
|
|
|
1162
1206
|
return stored !== null && stored !== void 0 && stored["imageToolEnabled"] === enabled;
|
|
1163
1207
|
});
|
|
1164
1208
|
}
|
|
1209
|
+
async function writeRegionVideoToolEnabled(scope, region, enabled, slot) {
|
|
1210
|
+
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
1211
|
+
const stored = readBack;
|
|
1212
|
+
return stored !== null && stored !== void 0 && stored["videoToolEnabled"] === enabled;
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Pin (or clear) one region's video model for ONE mode tag, verifying it landed.
|
|
1217
|
+
*
|
|
1218
|
+
* `modelId` of `undefined`/`''` clears the pin, so the landed check accepts an
|
|
1219
|
+
* ABSENT key as success for that case — writing an empty string instead would
|
|
1220
|
+
* leave a value the tool has to keep special-casing forever.
|
|
1221
|
+
*
|
|
1222
|
+
* Routed through `writeField` like every other settings write (Host endpoint
|
|
1223
|
+
* first, then the landed read-back): on the affected 0.1.7 deployments a direct
|
|
1224
|
+
* `scope.set()` settles without storing anything, and a picker that silently
|
|
1225
|
+
* reverted would be worse than no picker at all.
|
|
1226
|
+
*/
|
|
1227
|
+
async function writeRegionPreferredVideoModels(scope, region, tag, modelId, slot) {
|
|
1228
|
+
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
1229
|
+
const stored = readBack;
|
|
1230
|
+
if (stored === null || stored === void 0) return false;
|
|
1231
|
+
const pinned = stored["preferredVideoModels"];
|
|
1232
|
+
const current = typeof pinned === "object" && pinned !== null ? pinned[tag] : void 0;
|
|
1233
|
+
return modelId === void 0 || modelId === "" ? current === void 0 || current === "" : current === modelId;
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1165
1236
|
async function writeRegionCreditsShown(scope, region, shown, slot) {
|
|
1166
1237
|
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
1167
1238
|
const stored = readBack;
|
|
@@ -4970,7 +5041,7 @@ window.__ModuleLoader__.load({
|
|
|
4970
5041
|
CONTRAST colour, so a ticked box painted itself as a heavy black pill — the
|
|
4971
5042
|
loudest object in a section made of plain 13px text. */
|
|
4972
5043
|
.dsm-workbuddy-pref input{flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
4973
|
-
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-cli-bridge-check,.dsm-workbuddy-upstream-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
5044
|
+
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-video-tool-check,.dsm-workbuddy-cli-bridge-check,.dsm-workbuddy-upstream-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
4974
5045
|
/* The upstream-identity switches are their own group, so they get a divider
|
|
4975
5046
|
above them instead of relying on spacing alone: they change the request the
|
|
4976
5047
|
whole plugin sends, not a per-region display choice, and reading them as the
|
|
@@ -5011,6 +5082,10 @@ window.__ModuleLoader__.load({
|
|
|
5011
5082
|
.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
5083
|
.dsm-workbuddy-media-tags{display:inline-flex;flex-wrap:wrap;gap:4px}
|
|
5013
5084
|
.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)}
|
|
5085
|
+
/* The per-mode video-model picker. It reuses .dsm-workbuddy-media-row so the
|
|
5086
|
+
tag chip and the select sit on one baseline, like a roster row does. */
|
|
5087
|
+
.dsm-workbuddy-video-model-select{font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary,#b8bcc4);background:var(--dsw-alias-bg-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:1px 6px;max-width:100%;min-width:0}
|
|
5088
|
+
.dsm-workbuddy-video-model-select:disabled{opacity:.6}
|
|
5014
5089
|
.dsm-workbuddy-media-empty,.dsm-workbuddy-media-error{font-size:12px;line-height:17px}
|
|
5015
5090
|
.dsm-workbuddy-media-empty{color:var(--dsw-alias-label-tertiary,#8b8f98)}
|
|
5016
5091
|
.dsm-workbuddy-media-error{color:var(--dsw-alias-label-error,#e5675f)}
|
|
@@ -6533,6 +6608,15 @@ window.__ModuleLoader__.load({
|
|
|
6533
6608
|
*/
|
|
6534
6609
|
const [togglingCredits, setTogglingCredits] = (0, react.useState)(void 0);
|
|
6535
6610
|
/**
|
|
6611
|
+
* Which video-model pin is being written right now: the tag string, or
|
|
6612
|
+
* undefined.
|
|
6613
|
+
*
|
|
6614
|
+
* A SEPARATE state from {@link togglingCredits} for the same reason those two
|
|
6615
|
+
* switches keep separate busy flags: the pin lives in a different field of the
|
|
6616
|
+
* same region slot, so one in-flight write must not disable the other control.
|
|
6617
|
+
*/
|
|
6618
|
+
const [pinningVideoModel, setPinningVideoModel] = (0, react.useState)(void 0);
|
|
6619
|
+
/**
|
|
6536
6620
|
* The OPTIONAL CodeBuddy CLI route switch (global: one switch, one budget).
|
|
6537
6621
|
*
|
|
6538
6622
|
* `undefined` until the first Host answer arrives, so the box renders the
|
|
@@ -6914,6 +6998,35 @@ window.__ModuleLoader__.load({
|
|
|
6914
6998
|
return regionImageToolEnabledOf(settingsScope?.getSnapshot().value, item);
|
|
6915
6999
|
};
|
|
6916
7000
|
/**
|
|
7001
|
+
* Whether the model may call the video tool in this region. Mirrors
|
|
7002
|
+
* imageToolOn: read off the Host answer first, so a stale mirror cannot
|
|
7003
|
+
* leave the spending capability on. Absent is NOT consent.
|
|
7004
|
+
*/
|
|
7005
|
+
const videoToolOn = (item) => {
|
|
7006
|
+
const usage = statusByRegion[item];
|
|
7007
|
+
if (typeof usage?.videoToolEnabled === "boolean") return usage.videoToolEnabled;
|
|
7008
|
+
return regionVideoToolEnabledOf(settingsScope?.getSnapshot().value, item);
|
|
7009
|
+
};
|
|
7010
|
+
/**
|
|
7011
|
+
* The video model pinned for one mode tag in this region, or `''` for "no pin".
|
|
7012
|
+
*
|
|
7013
|
+
* Same read order as {@link videoToolOn} and for the same measured reason: the
|
|
7014
|
+
* Host's answer first, because this namespace's browser mirror is stale on the
|
|
7015
|
+
* affected 0.1.7 deployments — a picker reading the mirror alone would keep
|
|
7016
|
+
* showing a pin the tool no longer uses.
|
|
7017
|
+
*
|
|
7018
|
+
* `''` rather than `undefined` because that is what the `<select>` needs for its
|
|
7019
|
+
* "default" row; the write path turns it back into "remove the key".
|
|
7020
|
+
*/
|
|
7021
|
+
const preferredVideoModelOf = (item, tag) => {
|
|
7022
|
+
const fromHost = statusByRegion[item]?.preferredVideoModels;
|
|
7023
|
+
if (typeof fromHost === "object" && fromHost !== null) {
|
|
7024
|
+
const pinned = fromHost[tag];
|
|
7025
|
+
if (typeof pinned === "string") return pinned;
|
|
7026
|
+
}
|
|
7027
|
+
return regionPreferredVideoModelsOf(settingsScope?.getSnapshot().value, item)[tag] ?? "";
|
|
7028
|
+
};
|
|
7029
|
+
/**
|
|
6917
7030
|
* Switch one region's provider off or on. The write carries the region's
|
|
6918
7031
|
* whole slot through untouched — only `enabled` changes — so the user's
|
|
6919
7032
|
* directory, model picks, image opt-ins and budgets survive a round trip.
|
|
@@ -7183,6 +7296,41 @@ window.__ModuleLoader__.load({
|
|
|
7183
7296
|
if (mounted.current) setTogglingCredits(void 0);
|
|
7184
7297
|
}
|
|
7185
7298
|
};
|
|
7299
|
+
const toggleVideoTool = async (item, enabled) => {
|
|
7300
|
+
if (settingsScope === void 0) return;
|
|
7301
|
+
setTogglingCredits(item);
|
|
7302
|
+
try {
|
|
7303
|
+
const slot = nextRegionVideoToolEnabled(settingsScope.getSnapshot().value, item, enabled)[item];
|
|
7304
|
+
await writeRegionVideoToolEnabled(settingsScope, item, enabled, typeof slot === "object" && slot !== null ? slot : {});
|
|
7305
|
+
await refreshUsage(item);
|
|
7306
|
+
} catch (error) {
|
|
7307
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
7308
|
+
} finally {
|
|
7309
|
+
if (mounted.current) setTogglingCredits(void 0);
|
|
7310
|
+
}
|
|
7311
|
+
};
|
|
7312
|
+
/**
|
|
7313
|
+
* Pin (or clear) one region's video model for one mode tag.
|
|
7314
|
+
*
|
|
7315
|
+
* `undefined` means "no pin": the picker's default row, which the tool reads as
|
|
7316
|
+
* "take the first model of that group". Both directions go through the same
|
|
7317
|
+
* landed-checked write as every other settings control, then re-read the Host —
|
|
7318
|
+
* a pin that reverted silently would leave the user believing the tool would
|
|
7319
|
+
* use a model it never picks.
|
|
7320
|
+
*/
|
|
7321
|
+
const pinVideoModel = async (item, tag, modelId) => {
|
|
7322
|
+
if (settingsScope === void 0) return;
|
|
7323
|
+
setPinningVideoModel(tag);
|
|
7324
|
+
try {
|
|
7325
|
+
const slot = nextRegionPreferredVideoModels(settingsScope.getSnapshot().value, item, tag, modelId)[item];
|
|
7326
|
+
await writeRegionPreferredVideoModels(settingsScope, item, tag, modelId, typeof slot === "object" && slot !== null ? slot : {});
|
|
7327
|
+
await refreshUsage(item);
|
|
7328
|
+
} catch (error) {
|
|
7329
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
7330
|
+
} finally {
|
|
7331
|
+
if (mounted.current) setPinningVideoModel(void 0);
|
|
7332
|
+
}
|
|
7333
|
+
};
|
|
7186
7334
|
/**
|
|
7187
7335
|
* Read the active region's media roster. Read-only and free (the same GET the
|
|
7188
7336
|
* image tool performs), so it is safe to run whenever the capability panel is
|
|
@@ -7768,14 +7916,43 @@ window.__ModuleLoader__.load({
|
|
|
7768
7916
|
})]
|
|
7769
7917
|
});
|
|
7770
7918
|
/**
|
|
7771
|
-
*
|
|
7919
|
+
* The video-tool opt-in. Same shape and same stakes as imageToolPreference:
|
|
7920
|
+
* per-region, off means the tool is NOT registered at all (not "registered
|
|
7921
|
+
* but refusing"), so a tick here only ever authorizes spending on THIS tab.
|
|
7922
|
+
* The tool is an async task that runs tens of seconds to minutes.
|
|
7923
|
+
*/
|
|
7924
|
+
const videoToolPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7925
|
+
className: "dsm-workbuddy-pref",
|
|
7926
|
+
title: t("row.videoToolHint"),
|
|
7927
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7928
|
+
type: "checkbox",
|
|
7929
|
+
className: "dsm-workbuddy-video-tool-check",
|
|
7930
|
+
checked: activeRegionOn ? videoToolOn(activeRegion) : false,
|
|
7931
|
+
disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
|
|
7932
|
+
onChange: (event) => {
|
|
7933
|
+
toggleVideoTool(activeRegion, event.target.checked);
|
|
7934
|
+
}
|
|
7935
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7936
|
+
className: "dsm-workbuddy-pref-copy",
|
|
7937
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7938
|
+
className: "dsm-workbuddy-pref-label",
|
|
7939
|
+
children: t("row.videoToolLabel")
|
|
7940
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7941
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7942
|
+
children: t("row.videoToolHint")
|
|
7943
|
+
})]
|
|
7944
|
+
})]
|
|
7945
|
+
});
|
|
7946
|
+
/**
|
|
7947
|
+
* What this region can actually draw. The IMAGE rows stay read-only; the
|
|
7948
|
+
* VIDEO rows are pinnable in `videoModelPreference` below.
|
|
7772
7949
|
*
|
|
7773
|
-
*
|
|
7774
|
-
* model in both regions
|
|
7775
|
-
* choice of one
|
|
7776
|
-
*
|
|
7777
|
-
*
|
|
7778
|
-
*
|
|
7950
|
+
* The roster used to be read-only because every tag group held exactly one
|
|
7951
|
+
* model in both regions (measured 2026-10-25), so a dropdown would have been a
|
|
7952
|
+
* choice of one. That stopped being true for VIDEO: CN publishes two models
|
|
7953
|
+
* per video tag (`kling-video-v3` and `minimax-video-h3`) — the very
|
|
7954
|
+
* "两个模型能不能挑" question. The pin therefore lives in its own block below,
|
|
7955
|
+
* and the roster keeps answering "can this tab draw at all, and with what".
|
|
7779
7956
|
*
|
|
7780
7957
|
* Indented like the position picker, because it is a property of the switch
|
|
7781
7958
|
* above it rather than a fourth independent setting.
|
|
@@ -7787,6 +7964,49 @@ window.__ModuleLoader__.load({
|
|
|
7787
7964
|
"image-to-video": t("row.mediaTagImageToVideo")
|
|
7788
7965
|
};
|
|
7789
7966
|
const activeMedia = mediaByRegion[activeRegion];
|
|
7967
|
+
const videoModelPreference = !(activeRegionOn && videoToolOn(activeRegion)) || activeMedia === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7968
|
+
className: "dsm-workbuddy-pref dsm-workbuddy-media",
|
|
7969
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7970
|
+
className: "dsm-workbuddy-pref-copy",
|
|
7971
|
+
children: [
|
|
7972
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7973
|
+
className: "dsm-workbuddy-pref-label",
|
|
7974
|
+
children: t("row.videoModelLabel")
|
|
7975
|
+
}),
|
|
7976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7977
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7978
|
+
children: t("row.videoModelHint")
|
|
7979
|
+
}),
|
|
7980
|
+
["text-to-video", "image-to-video"].map((tag) => {
|
|
7981
|
+
const group = activeMedia.filter((model) => model.tags.includes(tag));
|
|
7982
|
+
if (group.length === 0) return null;
|
|
7983
|
+
const pinned = preferredVideoModelOf(activeRegion, tag);
|
|
7984
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7985
|
+
className: "dsm-workbuddy-media-row",
|
|
7986
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7987
|
+
className: "dsm-workbuddy-media-tag",
|
|
7988
|
+
children: mediaTagLabels[tag] ?? tag
|
|
7989
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
7990
|
+
className: "dsm-workbuddy-video-model-select",
|
|
7991
|
+
value: group.some((model) => model.id === pinned) ? pinned : "",
|
|
7992
|
+
disabled: !canWrite || saving || poolBusy || pinningVideoModel !== void 0,
|
|
7993
|
+
onChange: (event) => {
|
|
7994
|
+
const next = event.target.value;
|
|
7995
|
+
pinVideoModel(activeRegion, tag, next === "" ? void 0 : next);
|
|
7996
|
+
},
|
|
7997
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
7998
|
+
value: "",
|
|
7999
|
+
children: t("row.videoModelDefault")
|
|
8000
|
+
}), group.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
8001
|
+
value: model.id,
|
|
8002
|
+
children: model.name
|
|
8003
|
+
}, model.id))]
|
|
8004
|
+
})]
|
|
8005
|
+
}, tag);
|
|
8006
|
+
})
|
|
8007
|
+
]
|
|
8008
|
+
})
|
|
8009
|
+
});
|
|
7790
8010
|
const mediaModelsPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7791
8011
|
className: "dsm-workbuddy-pref dsm-workbuddy-media",
|
|
7792
8012
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -8088,6 +8308,8 @@ window.__ModuleLoader__.load({
|
|
|
8088
8308
|
cliBridgePreference,
|
|
8089
8309
|
cliBridgeSwitchedOn() ? cliBridgePickers : null,
|
|
8090
8310
|
imageToolPreference,
|
|
8311
|
+
videoToolPreference,
|
|
8312
|
+
videoModelPreference,
|
|
8091
8313
|
mediaModelsPreference,
|
|
8092
8314
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8093
8315
|
className: "dsm-workbuddy-pref",
|
|
@@ -10836,8 +11058,13 @@ window.__ModuleLoader__.load({
|
|
|
10836
11058
|
"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.",
|
|
10837
11059
|
"row.imageToolLabel": "Let the model draw images",
|
|
10838
11060
|
"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.",
|
|
11061
|
+
"row.videoToolLabel": "Let the model generate videos",
|
|
11062
|
+
"row.videoToolHint": "Adds the workbuddy_draw_video tool, which the MODEL may call on its own — a video is an async task (submit, poll, then download) that costs WorkBuddy credits and runs tens of seconds to minutes, with no per-call confirmation. Ticking it here only authorizes THIS tab's region. Off everywhere means the tool is not registered at all.",
|
|
11063
|
+
"row.videoModelLabel": "Video model per mode",
|
|
11064
|
+
"row.videoModelHint": "Pinned separately for text-to-video and image-to-video, from THIS region's own catalog — a model id does not carry across regions. A call that names no model uses the pin; with nothing pinned the tool takes the first model of that group. A pin the catalog no longer carries quietly falls back to the first model instead of failing.",
|
|
11065
|
+
"row.videoModelDefault": "Default (first in the group)",
|
|
10839
11066
|
"row.mediaModelsLabel": "Image models in this region",
|
|
10840
|
-
"row.mediaModelsHint": "Read from this account's product configuration. The draw
|
|
11067
|
+
"row.mediaModelsHint": "Read from this account's product configuration. The draw tools pick their models from here: a prompt without an image uses a text-to-image model, one WITH an image uses an image-to-image model. Image rows are read-only (pass a model to the tool to change it); the VIDEO model is pinnable in the picker above.",
|
|
10841
11068
|
"row.mediaModelsEmpty": "This region publishes no image or video model.",
|
|
10842
11069
|
"row.mediaModelsError": "Could not read the media model list: {message}",
|
|
10843
11070
|
"row.mediaModelsRefresh": "Re-read",
|
|
@@ -11261,8 +11488,13 @@ window.__ModuleLoader__.load({
|
|
|
11261
11488
|
"row.cliBridgePickersHint": "三个选择都写入全局 cliBridge 块;模型/档位的\"CLI 默认\"是显式的空值而非未设置,届时 CLI 使用自己的默认;区域选择写入 cliBridge.regionOrder。",
|
|
11262
11489
|
"row.imageToolLabel": "允许模型自主生图",
|
|
11263
11490
|
"row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——国内区约 5.71、国际区约 0.03 积分/张,没有逐次确认。在这里勾选只授权当前这一区的账号。两区都关着时该工具根本不会注册。",
|
|
11491
|
+
"row.videoToolLabel": "允许模型自主生视频",
|
|
11492
|
+
"row.videoToolHint": "开启后模型可以自己调用 workbuddy_draw_video 生成短视频——视频是异步任务(提交、轮询到完成、再下载到本地),要几十秒到几分钟,没有逐次确认。在这里勾选只授权当前这一区的账号。两区都关着时该工具根本不会注册。",
|
|
11493
|
+
"row.videoModelLabel": "按模式指定视频模型",
|
|
11494
|
+
"row.videoModelHint": "文生视频与图生视频各钉一个,选项只来自当前这一区自己的目录——模型 id 不跨区。调用时不填 model 就用这里钉的;什么都没钉就用该组第一个模型。钉住的模型如果目录里已经没有了,会安静地回落到第一个,而不是让这次生成失败。",
|
|
11495
|
+
"row.videoModelDefault": "默认(组内第一个)",
|
|
11264
11496
|
"row.mediaModelsLabel": "本区可用的图像模型",
|
|
11265
|
-
"row.mediaModelsHint": "
|
|
11497
|
+
"row.mediaModelsHint": "读自当前账号的产品配置。生图工具就按这里挑模型:不填图片走「文生图」那一组,填了图片走「图生图」那一组。「图像」那几行是只读的(想换就在调用工具时指定模型);「视频」模型可以在上面的选择器里钉住。",
|
|
11266
11498
|
"row.mediaModelsEmpty": "这一区的产品配置里没有图像或视频模型。",
|
|
11267
11499
|
"row.mediaModelsError": "读取媒体模型清单失败:{message}",
|
|
11268
11500
|
"row.mediaModelsRefresh": "重新读取",
|