dsh-connect-workbuddy 5.13.0 → 5.13.1
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 +45 -0
- package/README.en.md +5 -4
- package/README.md +7 -4
- package/lib/bin.js +1 -1
- package/lib/client.js +3 -3
- package/lib/{host-heartbeat-DLVGPY4k.js → host-heartbeat-BtMcdOIa.js} +87 -1
- package/lib/index.d.ts +21 -11
- package/lib/index.js +70 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.13.1 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **按句改写网关固定指纹,落在 `neutralizeChannelFingerprints` 之前(#163,2026-10-29 实测)**
|
|
8
|
+
- **新测量**:CN 网关会在匹配前规范化 U+200B;system 开头插零宽仍 11128,`Note: ` 或真改词
|
|
9
|
+
才 200。`user` / `tool` 开头不扫(18:46 连测 3 次 200),所以不碰这两个角色。
|
|
10
|
+
- **第三处是整段组合指纹**:`@` 路径那 8 句逐句单独发 200,整段一起发 400。只改开头或整段
|
|
11
|
+
`replaceAll` 都不满足维护性要求;按句改才能既拆掉组合指纹,又覆盖宿主改一两个词的情况。
|
|
12
|
+
- **修法**:新增 `rewriteChannelFingerprintPhrases()` 与按句表(identity、coding agent、Web GUI、
|
|
13
|
+
`Agent Teams is available in this session`、`@` 路径整段逐句),在 `prepareChatBody()` 里
|
|
14
|
+
`keepPromptLast` 之后、`neutralizeChannelFingerprints` **之前**调用;只遍历 `system` /
|
|
15
|
+
`assistant` 的 string content 与 array text part,`user` / `tool`、工具调用、图片不动;无命中
|
|
16
|
+
逐字节不变,函数幂等。
|
|
17
|
+
- **测试**:新增 `tests/issue163-sentence-rewrite.spec.ts`(三处 span 消失、普通对话不变、
|
|
18
|
+
幂等、assistant 改写、array part、user/tool 不动、无命中 no-op);同步更新 `issue98*` /
|
|
19
|
+
`upstream` / `identity-clause-break` 里旧的「中性头 + 原文」断言。`pnpm run check` 通过。
|
|
20
|
+
|
|
21
|
+
- **自动「继续」降为最后兜底:默认 1 次,文案不再承诺有效(#163)**
|
|
22
|
+
- **证据**:固定 system 指纹每轮逐字节相同;同一 body 打上百次都是 400,只改三处才 200
|
|
23
|
+
(freedomx86 17:44);现场 3 次自动续发、Windpixy 反复继续/新老会话、QIYU 三级救援全跑完
|
|
24
|
+
仍 400;appulsexi-maker 19:12 的零历史新会话、只有 system 一条,仍 11128。新回合换不掉
|
|
25
|
+
固定 system 内容,自动续发对这一族无效。
|
|
26
|
+
- **修法**:`DEFAULT_MAX_CONSECUTIVE` 与 schema / 客户端默认 3 → **1**;卡片、配置描述、
|
|
27
|
+
README 与 `docs/PINNED-11128-RETRY.md` 改为「最后兜底,不是 11128 的修复;固定指纹由按句
|
|
28
|
+
改词处理,只有随轮次变化的内容才可能靠它碰过」。
|
|
29
|
+
- 仍然默认关闭;每次仍真实消耗一次请求。**行为面**未变(只认 `channel_reject`、不换号、
|
|
30
|
+
不碰子代理、成功/真人消息清零)。
|
|
31
|
+
|
|
32
|
+
- **换 provider 后,另一家的 thinking 不再被摊成 assistant 正文发给 WorkBuddy(#163 现场)**
|
|
33
|
+
- **现场**:`session-bb9396be` 在 02:03:43 从 `dsh-opencode-go/deepseek-v4.1-flash` 切到
|
|
34
|
+
`workbuddy/deepseek-v4.1-flash`,02:04:35 起连续 4 个回合 400/11128;错误里三条 assistant 的
|
|
35
|
+
14,410 / 4,225 / 5,138 字,逐字等于切换前 seq 98/109/114 的 reasoning 块。
|
|
36
|
+
- **机制**:宿主把另一家的 assistant 轮次重建为 `dsh-foreign`,pi-ai 把无签名的 thinking 块
|
|
37
|
+
改写成可见 `text`;WorkBuddy 网关扫描 assistant 消息,把它当成未授权渠道。`dropForeignThinking`
|
|
38
|
+
本来就是修这个的,但默认关闭,且卡片上没有开关。
|
|
39
|
+
- **修法**:`withLegacyContext` 在 `alignAssistantIdentities` 之后**无条件**丢弃**跨 provider** 的
|
|
40
|
+
foreign thinking(`api: dsh-foreign`,或 provider 不属于本 bundle 的两条路由);同一路由内换
|
|
41
|
+
模型(`hy3` → `deepseek-v4.1-flash`)仍由 `dropForeignThinking` 显式开关控制。跨 provider 的
|
|
42
|
+
思考块没有签名、本来就不可能作为 reasoning 重放,丢弃不改变语义;文本与工具调用不动。
|
|
43
|
+
- **诊断**:自动丢弃时写一条 warn(仅在开关关闭时),`latestForeignThinking` 仍照旧上报。
|
|
44
|
+
- **测试**:`tests/issue147-foreign-thinking.spec.ts` 新增三条(跨 provider 选中、同路由模型
|
|
45
|
+
切换不选、global 路由对齐后不误伤);`tests/issue147-model-leg-wiring.spec.ts` 新增两条(两个
|
|
46
|
+
generation × 两个入口的 `dsh-foreign` 回合在开关关闭时也被丢弃)。
|
|
47
|
+
|
|
3
48
|
## 5.13.0 (2026-10-28)
|
|
4
49
|
|
|
5
50
|
> 用户贴的那条失败:`本轮运行失败400: {"message":"workbuddy 上游失败:被网关的渠道校验拦截(不是账号、
|
package/README.en.md
CHANGED
|
@@ -52,12 +52,12 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
52
52
|
- **Optional: make the usage page record these calls like the official client's (5.8.0, issue #132, OFF by default)** — with `usageIdentity` on, chat requests carry `X-IDE-Name: WorkBuddy` (plus `X-IDE-Type: CLI` and a version taken from the existing product UA, not a second hard-coded number), so the usage page's client column is no longer blank, and the conversation-identity triple becomes the official **32-character lower-case hex UUIDv7** with `X-Request-ID` alongside it — the gateway echoes that id as the SSE `id` and writes it into the usage page, so hyphenated v4 ids made one account's rows look like two different clients — while "fresh per request, all distinct" has not been relaxed. **`User-Agent` is untouched under either setting**: billing and rate limits recognise the CLI channel. The image endpoint does not follow this switch; it keeps the shape measured on 2026-10-21. It is off by default because the shape fixes nothing on its own (the three `11128` doors read message openings, a missing conversation id, and whether the first message is a system prompt — no header moves any of them) and the only measurement is a reporter's on an older build, so a purely cosmetic alignment does not get to put every account at risk first. There is also an opt-in `keepPromptLast` (**off by default**): the usage page takes the request text from the LAST `user` message, and the host appends its time samples, runtime context and `<system-reminder>` blocks AFTER what you actually typed, so that column recorded injected text; when enabled it moves those injected blocks **above the real prompt** — order only, except that the recorded prompt's own leading `<acp …>mNNNNN</acp>` marker is stripped (leave it and that column still opens with a marker, i.e. nothing was fixed); earlier messages **keep** theirs, because the model cites history by those ids and the only id that can never matter is the message being answered now. It stays off by default because the order is something the upstream counts and a few leading characters are removed from the real prompt. The move matches only the host's own injected prefixes, and a conversation with no injected tail is untouched byte for byte — marker included — pinned by tests written for both.
|
|
53
53
|
- **Optional: move each new session's title request onto the hidden lightweight model (issue #133, OFF by default)** — the DSH host fires one title-generation request per new session through the same provider as the conversation, so it is billed at the main paid model's rate. With `sessionTitle.enabled` on, a request the plugin recognises as a title (its only user message opens with the host's template `Generate the session title from this JSON array of human messages:`) is re-pointed at `sessionTitle.model` (default `wb-lite`). The reporter measured 0 credits for those calls, with three caveats: it is still a real upstream request and counts toward request volume; the measurement is the reporter's account, not reproduced here; detection is fail-closed — if the host rewords the template the rewrite silently stops and billing reverts, and ordinary conversations are never touched. If the upstream rejects the model, only that session's title fails once; the conversation is unaffected. Like `usageIdentity` / `keepPromptLast` / `extraInjectedPrefixes` in the bullets above, `sessionTitle` is read once when the plugin mounts: **after editing `cordis.patch.yml`, reload the plugin or restart DSH for the change to take effect** — hot edits do not apply.
|
|
54
54
|
- **Benefits: a marker for what is unclaimed, plus a per-account Claim button** — the card marks, account by account, anything in the upstream's benefit campaigns (compensation / gifts) that **has not been claimed yet**. The data comes from the upstream's own two status endpoints; in the marking stage the plugin is **read-only** and never calls the two write endpoints. Three display rules all follow the upstream's state: `active && !claimed` shows the row, `claimed` removes it entirely, and an expired campaign (upstream flips `active` to `false`) disappears by itself — the plugin **never compares `end_time` against the local clock**, so a skewed machine cannot argue with the server; `end_time` only drives the "three days left" highlight. `active` alone is not enough to decide: an already-claimed account still reports `active: true`, and reading it alone would leave a claimed campaign hanging there forever. The read reuses the check-in pipeline (once per account, cached for 30 minutes, no request while the account pool is off), and a failing endpoint counts as "unread" rather than caching "nobody has benefits". Each row ends in a solid amber **Claim pill**, and one click claims **one campaign of one account**: there is no "claim everything", because a click spends one irrevocable real entitlement — and the write route **requires `accountId`**, so the request itself cannot batch. Before writing it **re-reads** the status and decides again; anything already claimed or expired returns 409 without touching upstream. After a successful claim it answers with the upstream's **fresh reading** and refreshes the card, with no optimistic update — even a silent upstream failure cannot leave a false "claimed" behind — and it also drops that account's cached benefit state, so the pill cannot linger on a stale status.
|
|
55
|
-
- **Optional:
|
|
55
|
+
- **Optional: a LAST-RESORT 「继续」 when the gateway refuses the request (OFF by default, 1 send by default)** — when the upstream rejects with `400` and business code `11128` the turn simply ends: the plugin does not retry inside that request (its three-rung ladder already ran), it does not swap accounts (the gate matches message contents, and every account sends the same ones), and the host does not retry. **This is not a fix for 11128.** Fixed system fingerprints (the identity / Web GUI / @-path paragraph from #163) are byte-identical every turn, so resending only pays for the same 400 again; since 5.13.1 `prepareChatBody` rewrites known phrases sentence by sentence. Only content that happens to change between turns can be helped by a new turn. Turning on the card's Capabilities switch makes the engine wait 300 ms and send `继续` through `agent.followup` (`source.kind` is `workbuddy-continue`, **not** `user`). **The cap counts CONSECUTIVE sends** (`channelRejectContinue.maxConsecutive`, default **1**; the old default of 3 is disproven — if one cannot help, three cannot): one successful turn, or one message you type yourself, resets it; the plugin's own message does not. Subagent sessions are never touched, and neither is a session already running or with something queued. **Every send spends a real request**, hence OFF by default; it is also the only upstream-shaped switch whose change **takes effect immediately**. It can be set directly in `cordis.patch.yml`:
|
|
56
56
|
|
|
57
57
|
```yaml
|
|
58
58
|
channelRejectContinue:
|
|
59
59
|
enabled: true
|
|
60
|
-
maxConsecutive:
|
|
60
|
+
maxConsecutive: 1
|
|
61
61
|
text: 继续
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -492,8 +492,9 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
492
492
|
- **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×.
|
|
493
493
|
- 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 — the downgrade itself cannot be removed, but **its becoming prose can**, see `dropForeignThinking` below; 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.
|
|
494
494
|
- **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.
|
|
495
|
-
-
|
|
496
|
-
-
|
|
495
|
+
- **Cross-provider foreign thinking is dropped unconditionally; the `dropForeignThinking` switch only covers a model switch inside one route (off by default)**: 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.
|
|
496
|
+
- **No switch for the cross-provider leg**: after a provider switch the host rebuilds the other provider's assistant turns as `dsh-foreign`, and pi-ai flattens their thinking into assistant text; the WorkBuddy gateway scans assistant messages, so the whole turn is refused with `11128` (issue #163's measured scene: 14,410 / 4,225 / 5,138 characters of reasoning replayed as prose, with all three in-place rescue rungs and three auto-`继续` attempts failing). Those blocks could never be replayed as reasoning anyway, so dropping them is a correction rather than a preference and does not wait for an opt-in. `dropForeignThinking` only widens the same repair to a model switch **inside one route** (`hy3` → `deepseek-v4.1-flash`, both served by this bundle and stamped `provider: workbuddy`); for that leg the off path is still **byte-for-byte** what earlier builds sent.
|
|
497
|
+
- An automatic drop writes a warn (only while the switch is off) and still reports through `latestForeignThinking`, so the log names which blocks that request removed.
|
|
497
498
|
- 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`.
|
|
498
499
|
- 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.
|
|
499
500
|
- **It also covers switching MODELS inside one region** (`hy3` → `deepseek-v4.1-flash`, both served by this plugin, both stamped `provider: workbuddy`). The provider and api legs are equal there, so only the `model` leg separates the turn from the request — and 5.12.0's predicate compared just the first two, so that switch detected **nothing** and the switch looked broken. The model id comes from the request entry points (`model.id` on `stream` / `streamSimple`); omitting it keeps 5.12.0's behaviour exactly, which is why the card's read-only path is unaffected (it reads `lastForeignThinking`, which the request path fills).
|
package/README.md
CHANGED
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
- **可选:让上游用量页把这笔记成官方客户端发的那笔(5.8.0,issue #132,默认关闭)** —— 开 `usageIdentity` 后 chat 请求带上 `X-IDE-Name: WorkBuddy`(连同 `X-IDE-Type: CLI` 与版本号;版本号取自现有的产品 UA,不留第二个硬编码数字),用量页的「使用端」由此不再空白;会话身份三件套同时改成官方客户端那种 **32 位十六进制 UUIDv7** 并补上 `X-Request-ID`——网关把这个 id 回显成 SSE 的 `id` 并写进用量页,此前带连字符的 v4 与官方记录摆在同一页上像两种客户端;**每请求现生成、互不相同**这条没有放松。**`User-Agent` 在两种取值下都一字未动**——账单与限流认的是 CLI 通道;生图路径也不跟着这个开关变,它保持自己 2026-10-21 实测过的形状。**默认关**的理由:形状本身不修任何东西(`11128` 的三道门读的是消息开头、会话 id 有没有、首条是不是 system,头改不动其中任何一条),而实测证据来自报告者的账号与旧版本、本插件未在真机复现——一次纯显示对齐不该让所有账号先承担风险。另有可选的 `keepPromptLast`(**默认关闭**):用量页的「请求」列取自 body 里**最后一条** `user` 消息,而宿主把时间采样、运行时上下文、`<system-reminder>` 追加在你真正输入的那句**之后**,那一列于是常年记的是注入块;开启后把这些注入块**搬到真实输入之前**(只变顺序;另外会剥掉**被记录那一条**真实输入前面的 `<acp …>mNNNNN</acp>` 标记,否则那一列仍然以标记开头而等于没修,更早的消息一律保留自己的标记,因为模型靠它们引用历史)。默认关的理由是它会改变上游会数到的消息顺序、并删掉真实输入的几个前导字符;搬移只匹配宿主自己的注入前缀,而普通对话在没有注入尾块时逐字节不变(连标记也不剥)——这两点各有专门测试钉住。
|
|
62
62
|
- **可选:把每个新会话的标题请求改到隐藏轻量模型(issue #133,默认关闭)** —— DSH 宿主每个新会话会发一次标题生成请求,走的是与对话相同的 provider,于是这笔按主付费模型的费率计费。开启 `sessionTitle.enabled` 后,插件把能按宿主标题模板(`Generate the session title from this JSON array of human messages:` 开头、整条 body 只有这一条 user 消息)识别出的标题请求改写到 `sessionTitle.model`(默认 `wb-lite`)。报告者实测该模型对这类请求 **0 积分**,但注意三点:这仍是**真实上游请求**,请求数照算;证据是报告者账号级的,本插件未复现;识别按 fail-closed 设计——宿主若改模板,改写静默停止、回到原计费,不会误伤普通对话。若上游拒绝该模型,只是该会话标题生成失败一次,不影响主对话。另外,`sessionTitle` 与本条及上一条的 `usageIdentity` / `keepPromptLast` / `extraInjectedPrefixes` 一样,都是插件挂载时读一次配置:**改完 `cordis.patch.yml` 后需要重载插件或重启 DSH 才生效**,热改不生效。
|
|
63
63
|
- **福利活动:待领取标记 + 单账号「领取」按钮** —— 卡片会逐账号标出上游活动(补偿 / 赠礼)里**还没领的那一份**,数据来自上游自己的两个状态接口,标记阶段**只读**、两个写接口一次都不调用。显示规则完全由上游状态决定:`active && !claimed` 才显示,`claimed` 整行消失,活动过期(`active` 翻 `false`)自动消失——**不用本地时钟去比 `end_time`**,本机时间偏了也不会与服务端打架;`end_time` 只用于 3 天内临期高亮。`active` 单独看不行:已领取的账号 `active` 仍为 `true`,只看它会让领过的账号永远挂着待领取。读取复用签到的管线(每账号一次、30 分钟缓存、账号池关闭时不发请求),任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。行尾是一个实心琥珀色的**胶囊按钮「领取」**,一次只领**一个账号的一个活动**:没有「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益,写路由也**强制要求 `accountId`**,请求本身无法批量领取。领取前**重新读取**状态再判定,已领取或已过期一律 409 且不调用上游;领取后用上游的**新读数**作答并刷新卡片,不做乐观更新——即使上游静默失败也不会留下「已领取」的假象,成功后同时清掉该账号的福利缓存,按钮不会顶着旧状态赖着不走。
|
|
64
|
-
-
|
|
64
|
+
- **可选:被网关渠道校验拦截后的最后兜底「继续」(默认关闭,默认只发 1 次)** —— 上游用 `400` + 业务码 `11128` 拒绝时,这一轮就此结束:插件不会在原请求里重试(三级救援已跑完),不会换号(网关匹配的是消息内容,每个账号发出去都一样),宿主也不重试。**它不是 11128 的修复**:固定 system 指纹(#163 的 identity / Web GUI / @ 路径整段)每一轮逐字节相同,重发只会再付一次同一个 400;5.13.1 起这些由 `prepareChatBody` 的按句改词处理。只有被拒内容刚好随轮次变化的窄情况,才可能靠新回合碰过去。开启卡片「能力」栏的开关后,插件检测到这一类失败、等 300ms、用 `agent.followup` 发一条 `继续`(`source.kind` 是 `workbuddy-continue`,**不是** `user`)。**上限数的是「连续」次数**(`channelRejectContinue.maxConsecutive`,默认 **1**;旧默认 3 已证伪——1 次救不了,3 次也救不了):成功一轮、或你自己发一条消息,计数清零;插件自己发的不清零。子代理会话一律不碰;会话正在跑、或收件箱已有排队内容时也不发。**每次都会真实消耗一次请求**,所以默认关闭;它也是本插件里**唯一一个改完即生效**的上游类开关。与其它上游开关一样,也能直接在 `cordis.patch.yml` 里写:
|
|
65
65
|
|
|
66
66
|
```yaml
|
|
67
67
|
channelRejectContinue:
|
|
68
68
|
enabled: true
|
|
69
|
-
maxConsecutive:
|
|
69
|
+
maxConsecutive: 1
|
|
70
70
|
text: 继续
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -321,6 +321,8 @@ dsh plugin --profile web exec dsh-connect-workbuddy status
|
|
|
321
321
|
|
|
322
322
|
**这条路到底绕开了什么,说清楚。** 提供方那条路要让请求看起来像 CodeBuddy 客户端才能过网关的渠道校验,4.15.x 起那条校验已经升级成「按提示词中段做精确子串匹配」,靠句读处插零宽空格躲开——这是军备竞赛,而且不再免费:issue #98 里已经出现**账号级**限制(`code 11140`,「账号存在异常行为,已被限制使用」)。
|
|
323
323
|
|
|
324
|
+
> **2026-10-29 的 #163 复测更新**:零宽这条路已经被网关堵死——CN 网关在匹配前会规范化 U+200B,system 开头每个词边界都插零宽仍然 11128。现在改为**按句改词**(`rewriteChannelFingerprintPhrases`,落在 `neutralizeChannelFingerprints` 之前),只改命中句、其余逐字节不动,也不再需要把提示词伪装成第三方 agent。细节见 CHANGELOG 的 Unreleased。
|
|
325
|
+
|
|
324
326
|
CLI 路线逃掉的是**客户端指纹层**,不是账号层。它用 `CODEBUDDY_AUTH_TOKEN` 把本插件 vault 里的同一个 WorkBuddy 账号交给 CLI,所以「跟网关说话的是真的 CodeBuddy CLI 进程、没有身份要伪造、因此账号不会被限制」这句话**不成立**:身份仍然是同一批账号,`11140` 是账号级的,同一批账号在 CLI 路线上一样受限。真正被绕开的只有「这条请求的客户端指纹像不像 CodeBuddy」——这一层是真实的收益(渠道校验不会再因为提示词里有句读而拒你),但它不等于「免封」。两条路是互补的容错,不是替代。
|
|
325
327
|
|
|
326
328
|
### 前提
|
|
@@ -507,8 +509,9 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
507
509
|
- **本插件已在 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」一节。
|
|
508
510
|
- 升级后仍可能看到计数变化的部分:**在同一区域内换模型**仍会降级(`model` 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的)——降级本身无法消除,但**它变成正文这件事可以**,见下面 `dropForeignThinking` 那一条;**升级之前就录在历史里的消息**带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
|
|
509
511
|
- **降级不只是计数问题:思考会变成正文,模型随后照抄这个格式(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)。
|
|
510
|
-
-
|
|
511
|
-
-
|
|
512
|
+
- **跨 provider 的 foreign thinking 无条件丢弃;开关 `dropForeignThinking` 只负责同一路由内换模型(默认关闭)**:本插件在请求发出前**删掉**那些「本来会被压成正文」的外来 thinking 块。之所以是删而不是保留:这些块由宿主以 `foreignAssistant()` 构造,**不带 `thinkingSignature`**,即使被当成自己的轮次也无法作为带签名的 reasoning 重放——它们对线上唯一的贡献就是被压成的那段正文。正文块与工具调用一律不动,每条消息的身份也不动。
|
|
513
|
+
- **跨 provider 不设开关**:换 provider 后宿主把另一家的 assistant 轮次重建成 `dsh-foreign`,pi-ai 会把它们的 thinking 摊成 assistant 正文;WorkBuddy 网关正好扫描 assistant 消息,于是整轮以 `11128` 被拒(issue #163 的现场:14,410 / 4,225 / 5,138 字的 reasoning 被当成正文重放,三次原地救援与三轮自动「继续」全失败)。这些块本来就不可能作为 reasoning 重放,丢弃是纠正而不是偏好,所以不等待用户 opt-in。`dropForeignThinking` 只把同样的修复扩展到**同一路由内换模型**(`hy3` → `deepseek-v4.1-flash`,两边都由本插件提供、都盖 `provider: workbuddy`);那一条的关闭路径仍**一个字节都不变**。
|
|
514
|
+
- 自动丢弃时写一条 warn(仅在开关关闭时),`latestForeignThinking` 仍照旧上报,日志里能看出这次请求删掉了哪些块。
|
|
512
515
|
- 顺序很关键:修复跑在 `alignAssistantIdentities` **之后**。国际区自己的轮次到达时也带 `api: 'dsh-foreign'`(宿主的重放降级路径),先诊断会把它误判成外来轮次并删掉它自己的推理——那正是 #102 的反向回归。`tests/issue147-foreign-thinking.spec.ts` 用真实 pi-ai 的 `transformMessages` 钉住了这个顺序。
|
|
513
516
|
- 已经录进历史的坏轮次**不会**被追溯修复:它们已经作为正文存在会话里,开关只管之后的请求。想彻底摆脱,切换 provider 后新开会话是最直接的办法。
|
|
514
517
|
- **它同时覆盖「同一个区域内换模型」**(`hy3` → `deepseek-v4.1-flash`,两者都由本插件提供、都盖 `provider: workbuddy`)。这时 provider 与 api 两条腿都相等,判据只差 `model` 一条腿——5.12.0 的判定只比前两条,因此这种切换**一个块也检不出**,开关看起来无效。判断所需的 model id 由请求入口(`stream` / `streamSimple` 的 `model.id`)传入;不传时行为与 5.12.0 完全一致,卡片那条只读路径因此不受影响(它读的本来就是请求路径填好的 `lastForeignThinking`)。
|
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, 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-
|
|
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-BtMcdOIa.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
|
@@ -852,7 +852,7 @@ window.__ModuleLoader__.load({
|
|
|
852
852
|
*/
|
|
853
853
|
const CHANNEL_CONTINUE_DEFAULTS = {
|
|
854
854
|
enabled: false,
|
|
855
|
-
maxConsecutive:
|
|
855
|
+
maxConsecutive: 1,
|
|
856
856
|
text: "继续"
|
|
857
857
|
};
|
|
858
858
|
/**
|
|
@@ -11151,7 +11151,7 @@ window.__ModuleLoader__.load({
|
|
|
11151
11151
|
"row.sessionTitleModel": "Target model:",
|
|
11152
11152
|
"row.extraPrefixesCount": "Custom injected prefixes in effect:",
|
|
11153
11153
|
"row.channelRejectContinueLabel": "Resend 「继续」 after the gateway refuses the request",
|
|
11154
|
-
"row.channelRejectContinueHint": "When the gateway answers 400 with code 11128 (「被网关的渠道校验拦截」), the turn just fails: the plugin never retries it in place and never swaps accounts, because the gate matches the message
|
|
11154
|
+
"row.channelRejectContinueHint": "LAST RESORT, not a fix for 11128. When the gateway answers 400 with code 11128 (「被网关的渠道校验拦截」), the turn just fails: the plugin never retries it in place and never swaps accounts, because the gate matches the message contents and every account sends the same ones. Fixed system fingerprints are byte-identical every turn, so resending only pays for the same 400 again; since 5.13.1 `prepareChatBody` rewrites known phrases instead. The narrow case this can help is content that changes between turns. It sends the text below as a NEW turn and spends a real request each time. Stops after N CONSECUTIVE sends (default 1); one successful turn, or one message you type yourself, resets the count. Subagent sessions are never touched. Off by default. Takes effect immediately, no reload.",
|
|
11155
11155
|
"row.channelRejectContinueLimit": "Max consecutive sends:",
|
|
11156
11156
|
"row.channelRejectContinueText": "Text sent:",
|
|
11157
11157
|
"row.cliBridgeLabel": "Let the local CodeBuddy CLI do the work",
|
|
@@ -11585,7 +11585,7 @@ window.__ModuleLoader__.load({
|
|
|
11585
11585
|
"row.sessionTitleModel": "目标模型:",
|
|
11586
11586
|
"row.extraPrefixesCount": "生效的自定义注入前缀:",
|
|
11587
11587
|
"row.channelRejectContinueLabel": "被网关渠道校验拦截后,自动再发一次「继续」",
|
|
11588
|
-
"row.channelRejectContinueHint": "
|
|
11588
|
+
"row.channelRejectContinueHint": "最后兜底,不是 11128 的修复。上游用 400 + code 11128(「被网关的渠道校验拦截」)拒绝时,这一轮就直接失败了:插件不会在原地重试,也不会换号——网关匹配的是消息内容,每个账号发出去都一样。固定 system 指纹每一轮逐字节相同,重发只会再付一次同一个 400;5.13.1 起由 `prepareChatBody` 按句改词处理。只有被拒内容刚好随轮次变化的窄情况才可能靠新回合碰过去。它会把下面这句作为新回合发出,每次真实消耗一次请求;默认只发 1 次、默认关闭;连续计数,成功一轮或你自己发一条消息即清零。子代理会话一律不碰,改完立即生效,不必重载插件。",
|
|
11589
11589
|
"row.channelRejectContinueLimit": "连续发送上限:",
|
|
11590
11590
|
"row.channelRejectContinueText": "发送的文本:",
|
|
11591
11591
|
"row.cliBridgeLabel": "允许调用本机 CodeBuddy CLI 干活",
|
|
@@ -3503,6 +3503,91 @@ function scansOpening(role) {
|
|
|
3503
3503
|
return name === "system" || name === "assistant";
|
|
3504
3504
|
}
|
|
3505
3505
|
/**
|
|
3506
|
+
* The gateway's current content fingerprints, and same-meaning rewrites for
|
|
3507
|
+
* them (issue #163).
|
|
3508
|
+
*
|
|
3509
|
+
* WHY REWRITE INSTEAD OF ZERO-WIDTH. On 2026-10-29 two independent probes
|
|
3510
|
+
* measured that the CN gateway normalizes invisible characters before matching:
|
|
3511
|
+
* the DSH identity with U+200B at every word boundary still answers 11128.
|
|
3512
|
+
* `breakIdentityClauses` and both perturbation rungs therefore cannot help this
|
|
3513
|
+
* family. A real wording change is what the same probes measured as 200.
|
|
3514
|
+
*
|
|
3515
|
+
* WHY SENTENCE-LEVEL, NOT ONE WHOLE-PARAGRAPH LITERAL. The third fingerprint is
|
|
3516
|
+
* an eight-sentence, roughly 800-character @-path paragraph. Two reporters
|
|
3517
|
+
* measured that each sentence alone answers 200 and the whole paragraph answers
|
|
3518
|
+
* 400 — it is a COMBINATION, not a single listed sentence. Replacing the
|
|
3519
|
+
* paragraph as one literal would stop matching the moment the host changes one
|
|
3520
|
+
* word; replacing every sentence breaks the combination and keeps working when
|
|
3521
|
+
* only some of the text moves. The rows below are the table proposed in issue
|
|
3522
|
+
* #163 (yicneggima-glitch, 18:56; QIYU-PLUS, 18:57), with the identity and Web
|
|
3523
|
+
* GUI rows both probes measured.
|
|
3524
|
+
*
|
|
3525
|
+
* `from` strings are matched ANYWHERE, not just at an opening, because the
|
|
3526
|
+
* measured rule is position-independent. Only a hit is replaced; every other
|
|
3527
|
+
* byte is left alone, so an ordinary conversation is unchanged.
|
|
3528
|
+
*/
|
|
3529
|
+
const CHANNEL_GATE_PHRASES = [
|
|
3530
|
+
["You are an AI agent powered by DeepSeek Harness", "You are an AI agent running inside DeepSeek Harness"],
|
|
3531
|
+
["You are a coding agent powered by the ", "You are a coding agent running on the "],
|
|
3532
|
+
["You are interacting with the user through the DeepSeek Harness Web GUI", "You are working with the user inside the DeepSeek Harness Web GUI"],
|
|
3533
|
+
["Agent Teams is available in this session", "Agent Teams is enabled for this session"],
|
|
3534
|
+
["Tokens prefixed with @ are paths the user explicitly referenced.", "Paths that begin with @ are the ones the user explicitly referenced."],
|
|
3535
|
+
["Relative paths resolve from the workspace root;", "Relative paths are resolved against the workspace root;"],
|
|
3536
|
+
["absolute paths identify files or directories on the host.", "absolute paths point at files or directories on the host."],
|
|
3537
|
+
["A trailing slash marks a directory:", "A trailing slash indicates a directory:"],
|
|
3538
|
+
["list it when its contents matter.", "list it whenever its contents matter."],
|
|
3539
|
+
["Anything else is a file:", "Everything else is a file:"],
|
|
3540
|
+
["use the read tool when its contents are needed,", "use the read tool whenever you need its contents,"],
|
|
3541
|
+
["and do not claim to have inspected it before reading.", "and never claim to have inspected it before reading it."],
|
|
3542
|
+
["@\"...\" quotes a path containing spaces.", "A path containing spaces is quoted with @\"...\"."]
|
|
3543
|
+
];
|
|
3544
|
+
/**
|
|
3545
|
+
* Rewrite the measured gate phrases in one text part.
|
|
3546
|
+
*
|
|
3547
|
+
* Exported for the tests and for callers that build a body themselves. It is
|
|
3548
|
+
* idempotent: every `to` string is different from its `from` string and no
|
|
3549
|
+
* rewrite recreates another `from` string, so a second pass is a no-op.
|
|
3550
|
+
*/
|
|
3551
|
+
function rewriteChannelFingerprintPhrases(text) {
|
|
3552
|
+
let out = text;
|
|
3553
|
+
for (const [from, to] of CHANNEL_GATE_PHRASES) if (out.includes(from)) out = out.replaceAll(from, to);
|
|
3554
|
+
return out;
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Apply {@link rewriteChannelFingerprintPhrases} to every scanned text part.
|
|
3558
|
+
*
|
|
3559
|
+
* `system` and `assistant` only (the roles the gate reads). `user` and `tool`
|
|
3560
|
+
* are deliberately untouched: QIYU-PLUS re-measured both on 2026-10-29 and
|
|
3561
|
+
* both answer 200 with the DSH identity at an opening, and rewriting a user's
|
|
3562
|
+
* own words would change the conversation for no gate.
|
|
3563
|
+
*/
|
|
3564
|
+
function rewriteChannelFingerprintPhrasesInBody(obj) {
|
|
3565
|
+
const messages = obj["messages"];
|
|
3566
|
+
if (!Array.isArray(messages)) return;
|
|
3567
|
+
for (const value of messages) {
|
|
3568
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
3569
|
+
const message = value;
|
|
3570
|
+
if (!scansOpening(message["role"])) continue;
|
|
3571
|
+
const content = message["content"];
|
|
3572
|
+
if (typeof content === "string") {
|
|
3573
|
+
const next = rewriteChannelFingerprintPhrases(content);
|
|
3574
|
+
if (next !== content) message["content"] = next;
|
|
3575
|
+
continue;
|
|
3576
|
+
}
|
|
3577
|
+
if (!Array.isArray(content)) continue;
|
|
3578
|
+
for (const part of content) {
|
|
3579
|
+
if (typeof part !== "object" || part === null || Array.isArray(part)) continue;
|
|
3580
|
+
const record = part;
|
|
3581
|
+
const type = record["type"];
|
|
3582
|
+
if (typeof type !== "string" || type.trim().toLowerCase() !== "text") continue;
|
|
3583
|
+
const text = record["text"];
|
|
3584
|
+
if (typeof text !== "string") continue;
|
|
3585
|
+
const next = rewriteChannelFingerprintPhrases(text);
|
|
3586
|
+
if (next !== text) record["text"] = next;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3506
3591
|
* Move every scanned message (`system` and `assistant`) off the content
|
|
3507
3592
|
* fingerprint above by prepending the placeholder head.
|
|
3508
3593
|
*
|
|
@@ -4153,6 +4238,7 @@ function prepareChatBody(source, options) {
|
|
|
4153
4238
|
}
|
|
4154
4239
|
ensureSystemHead(obj);
|
|
4155
4240
|
if (options?.keepPromptLast === true && Array.isArray(obj["messages"])) keepPromptLast(obj["messages"], injectedPrefixPattern(options.extraInjectedPrefixes));
|
|
4241
|
+
rewriteChannelFingerprintPhrasesInBody(obj);
|
|
4156
4242
|
neutralizeChannelFingerprints(obj);
|
|
4157
4243
|
normalizeToolChoice(obj);
|
|
4158
4244
|
dropRefusedReasoning(obj);
|
|
@@ -7711,7 +7797,7 @@ function describeRosterSeenPayload(raw) {
|
|
|
7711
7797
|
//#endregion
|
|
7712
7798
|
//#region src/version.ts
|
|
7713
7799
|
/** The npm package version this build was produced from. */
|
|
7714
|
-
const WORKBUDDY_CONNECT_VERSION = "5.13.
|
|
7800
|
+
const WORKBUDDY_CONNECT_VERSION = "5.13.1";
|
|
7715
7801
|
//#endregion
|
|
7716
7802
|
//#region src/host-heartbeat.ts
|
|
7717
7803
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -2334,12 +2334,21 @@ interface WorkBuddyAdapterOptions {
|
|
|
2334
2334
|
*/
|
|
2335
2335
|
piAiGeneration?: PiAiGeneration;
|
|
2336
2336
|
/**
|
|
2337
|
-
* Drop foreign thinking blocks pi-ai would publish as prose
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2337
|
+
* Drop SAME-ROUTE foreign thinking blocks pi-ai would publish as prose
|
|
2338
|
+
* (issue #147).
|
|
2339
|
+
*
|
|
2340
|
+
* CROSS-PROVIDER turns are dropped unconditionally: after a provider switch
|
|
2341
|
+
* the host rebuilds the other provider's assistant turns as `dsh-foreign`,
|
|
2342
|
+
* their unsigned thinking blocks can never be replayed, and pi-ai would
|
|
2343
|
+
* otherwise flatten them into assistant prose that the WorkBuddy gateway
|
|
2344
|
+
* refuses with 11128 (issue #163's measured scene). This switch only widens
|
|
2345
|
+
* the repair to a model switch INSIDE one of this bundle's own routes.
|
|
2346
|
+
*
|
|
2347
|
+
* OFF by default for that same-route leg, and read LIVE (a getter, not a
|
|
2348
|
+
* boolean) so the switch takes effect on the next request without reloading
|
|
2349
|
+
* the plugin: turning it on mid-session is the whole point — the user sees
|
|
2350
|
+
* the working-notes turn, flips the switch, and the next turn is answered
|
|
2351
|
+
* normally.
|
|
2343
2352
|
*/
|
|
2344
2353
|
dropForeignThinking?: () => boolean;
|
|
2345
2354
|
/**
|
|
@@ -5206,12 +5215,13 @@ interface Config {
|
|
|
5206
5215
|
*
|
|
5207
5216
|
* 这条失败**不换号也不重试**(`isFailoverWorthy` 刻意把它排除在外:网关匹配的
|
|
5208
5217
|
* 是消息开头,换账号只会每个账号白烧一次请求),所以这一轮就此结束,用户看到的
|
|
5209
|
-
* 是「本轮运行失败400: … 被网关的渠道校验拦截
|
|
5210
|
-
*
|
|
5211
|
-
*
|
|
5218
|
+
* 是「本轮运行失败400: … 被网关的渠道校验拦截 …」。固定指纹由 `prepareChatBody`
|
|
5219
|
+
* 的按句改词处理;这条开关只是**最后兜底**:只有被拒内容刚好随轮次变化的窄情况
|
|
5220
|
+
* 才可能靠新回合碰过去。固定 system 指纹每轮相同,重发也救不了(#163 实测)。
|
|
5212
5221
|
*
|
|
5213
5222
|
* 默认关闭的理由与 {@link Config.usageIdentity} 相同:它会在用户没有发消息的
|
|
5214
|
-
*
|
|
5223
|
+
* 情况下花掉真实请求,属于必须显式授权的开销。上限默认 1 次:1 次救不了,3 次也
|
|
5224
|
+
* 救不了,旧默认 3 只会把同一个 400 付三遍。
|
|
5215
5225
|
*/
|
|
5216
5226
|
channelRejectContinue?: {
|
|
5217
5227
|
/**
|
|
@@ -5219,7 +5229,7 @@ interface Config {
|
|
|
5219
5229
|
*/
|
|
5220
5230
|
enabled?: boolean;
|
|
5221
5231
|
/**
|
|
5222
|
-
* 连续自动续发的次数上限(默认
|
|
5232
|
+
* 连续自动续发的次数上限(默认 1,夹在 1..10)。
|
|
5223
5233
|
*
|
|
5224
5234
|
* 「连续」是关键:一次成功回合、或者用户自己发了一条消息,计数立刻清零。
|
|
5225
5235
|
* 所以上限拦的是「一直过不去就一直试」,而不是这个会话一生的总次数。
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as parseCreditMultiplier, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, qt as macosNestedAppBundles, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-
|
|
1
|
+
import { $ as parseCreditMultiplier, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, qt as macosNestedAppBundles, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-BtMcdOIa.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -1745,6 +1745,39 @@ function isForeignAssistant(message, identity, requestModelId) {
|
|
|
1745
1745
|
return typeof model === "string" && model !== requestModelId;
|
|
1746
1746
|
}
|
|
1747
1747
|
/**
|
|
1748
|
+
* Whether this assistant turn came from a DIFFERENT PROVIDER/route, rather than
|
|
1749
|
+
* from a different model on one of this bundle's own routes.
|
|
1750
|
+
*
|
|
1751
|
+
* WHY THIS DISTINCTION EXISTS (issue #163's live scene). A session switched
|
|
1752
|
+
* from `dsh-opencode-go` to a WorkBuddy route mid-turn. The host rebuilt the
|
|
1753
|
+
* other provider's assistant turns as `dsh-foreign`, and pi-ai rewrote their
|
|
1754
|
+
* unsigned `thinking` blocks into visible `text` — 14,410 / 4,225 / 5,138
|
|
1755
|
+
* characters in the measured scene. WorkBuddy's gateway scans every assistant
|
|
1756
|
+
* message and refused the turn with business code 11128; all three in-place
|
|
1757
|
+
* rescue rungs and three auto-`继续` attempts failed because the bytes never
|
|
1758
|
+
* changed.
|
|
1759
|
+
*
|
|
1760
|
+
* A cross-provider thinking block is different from a same-route model switch:
|
|
1761
|
+
* it carries no `thinkingSignature` (the host builds `foreignAssistant()`
|
|
1762
|
+
* without one), so it could never be replayed as reasoning. Its only possible
|
|
1763
|
+
* contribution to the wire is the prose pi-ai manufactures from it. Dropping it
|
|
1764
|
+
* is therefore a correctness repair, not a preference — and it must happen
|
|
1765
|
+
* even when the user never opted into {@link dropForeignThinking}.
|
|
1766
|
+
*
|
|
1767
|
+
* Same-route model switches (`hy3` → `deepseek-v4.1-flash`) stay behind the
|
|
1768
|
+
* explicit switch, because those turns WERE produced by this bundle and their
|
|
1769
|
+
* reasoning may be worth keeping in the transcript for the user to read.
|
|
1770
|
+
*/
|
|
1771
|
+
function isCrossProviderAssistant(message, identity, ownedRoutes) {
|
|
1772
|
+
if (typeof message !== "object" || message === null) return false;
|
|
1773
|
+
const record = message;
|
|
1774
|
+
if (record["role"] !== "assistant") return false;
|
|
1775
|
+
if (record["api"] === "dsh-foreign") return true;
|
|
1776
|
+
const provider = record["provider"];
|
|
1777
|
+
if (typeof provider !== "string") return true;
|
|
1778
|
+
return provider !== identity && !ownedRoutes.includes(provider);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1748
1781
|
* Every foreign thinking block pi-ai would turn into visible prose (issue #147).
|
|
1749
1782
|
*
|
|
1750
1783
|
* THE DEFECT. pi-ai keeps a historical `thinking` block only while
|
|
@@ -1801,38 +1834,26 @@ function foreignThinkingOf(context, identity = WORKBUDDY_MODEL_IDENTITY, request
|
|
|
1801
1834
|
return found;
|
|
1802
1835
|
}
|
|
1803
1836
|
/**
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
* WHY REMOVAL IS THE HONEST REPAIR. The alternatives are both worse:
|
|
1807
|
-
*
|
|
1808
|
-
* - LEAVE IT: pi-ai publishes the other provider's private working notes as
|
|
1809
|
-
* this turn's answer (the #147 defect), so the user reads reasoning instead
|
|
1810
|
-
* of a reply and the model imitates that shape forever after.
|
|
1811
|
-
* - KEEP IT AS THINKING: impossible from here. `isSameModel` is pi-ai's own
|
|
1812
|
-
* predicate over fields this plugin does not control (`model.provider` /
|
|
1813
|
-
* `model.api` / `model.id` are compared against the MESSAGE's), and the
|
|
1814
|
-
* only way to satisfy it is to claim the foreign turn as this model's own
|
|
1815
|
-
* output — which is exactly the provenance lie issue #111 removed. These
|
|
1816
|
-
* blocks carry no `thinkingSignature` (the host builds them that way), so
|
|
1817
|
-
* they could not be replayed as signed reasoning even if claimed.
|
|
1837
|
+
* The subset of {@link foreignThinkingOf} that is safe to repair without asking.
|
|
1818
1838
|
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1839
|
+
* This is the predicate the adapter uses when `dropForeignThinking` is OFF: a
|
|
1840
|
+
* turn from another provider/route is dropped unconditionally, because its
|
|
1841
|
+
* unsigned thinking block can never be replayed and its flattened prose is what
|
|
1842
|
+
* the WorkBuddy gateway refuses (issue #163's provider-switch scene). A turn
|
|
1843
|
+
* from this bundle under a different model id is NOT included — that one is
|
|
1844
|
+
* still the explicit switch's business.
|
|
1824
1845
|
*
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
* @param context - a pi-ai context.
|
|
1829
|
-
* @param identity - the identity this adapter stamps on its own models.
|
|
1830
|
-
* @param requestModelId - model id the request is addressed to (see
|
|
1831
|
-
* {@link isForeignAssistant}); omitted, only the provider/api legs are judged.
|
|
1832
|
-
* @returns the repaired context, or the input when no block qualified.
|
|
1846
|
+
* Runs after `alignAssistantIdentities`, so a global-region turn this bundle
|
|
1847
|
+
* owns has already had its `dsh-foreign` marker repaired away and is not
|
|
1848
|
+
* misclassified.
|
|
1833
1849
|
*/
|
|
1834
|
-
function
|
|
1835
|
-
const
|
|
1850
|
+
function crossProviderThinkingOf(context, identity = WORKBUDDY_MODEL_IDENTITY, ownedRoutes = [WORKBUDDY_PROVIDER, WORKBUDDY_GLOBAL_PROVIDER]) {
|
|
1851
|
+
const messages = context.messages;
|
|
1852
|
+
if (!Array.isArray(messages)) return [];
|
|
1853
|
+
return foreignThinkingOf(context, identity).filter((block) => isCrossProviderAssistant(messages[block.messageIndex], identity, ownedRoutes));
|
|
1854
|
+
}
|
|
1855
|
+
/** Remove exactly the listed blocks; the one place that edits a transcript. */
|
|
1856
|
+
function dropThinkingBlocks(context, affected) {
|
|
1836
1857
|
if (affected.length === 0) return context;
|
|
1837
1858
|
const messages = context.messages;
|
|
1838
1859
|
const byMessage = /* @__PURE__ */ new Map();
|
|
@@ -1860,13 +1881,12 @@ function withLegacyContext(api, generation, identity = WORKBUDDY_MODEL_IDENTITY,
|
|
|
1860
1881
|
/** One place so both branches and both entry points agree on the order. */
|
|
1861
1882
|
const prepare = (context, requestModelId) => {
|
|
1862
1883
|
const aligned = alignAssistantIdentities(generation === "modern" ? foldModernContext(context) : adaptLegacyPiAiContext(context), identity, ownedRoutes);
|
|
1863
|
-
|
|
1864
|
-
const affected = foreignThinkingOf(aligned, identity, requestModelId);
|
|
1884
|
+
const affected = repair?.dropForeignThinking === true ? foreignThinkingOf(aligned, identity, requestModelId) : crossProviderThinkingOf(aligned, identity, ownedRoutes);
|
|
1865
1885
|
if (affected.length === 0) return aligned;
|
|
1866
1886
|
try {
|
|
1867
|
-
repair
|
|
1887
|
+
repair?.onForeignThinking?.(affected);
|
|
1868
1888
|
} catch {}
|
|
1869
|
-
return
|
|
1889
|
+
return dropThinkingBlocks(aligned, affected);
|
|
1870
1890
|
};
|
|
1871
1891
|
if (generation === "modern") return {
|
|
1872
1892
|
...api,
|
|
@@ -2232,7 +2252,7 @@ async function foldOnce(deps, agent, measurement, window, target) {
|
|
|
2232
2252
|
* 宿主也不会替我们重发:`channel_reject` 走的是 400,`code` 又是它不认识的字符串,
|
|
2233
2253
|
* 不在 `DEFAULT_RETRYABLE_CODES` 里,于是这一轮直接结束,用户看到的正是上面那句。
|
|
2234
2254
|
*
|
|
2235
|
-
* ##
|
|
2255
|
+
* ## 为什么它过去看起来可能成功(只对随轮次变化的窄情况)
|
|
2236
2256
|
*
|
|
2237
2257
|
* `docs/PINNED-11128-RETRY.md` §2 记录了两条互相矛盾的事实:插件自己在
|
|
2238
2258
|
* `perturbChannelFingerprints` 旁的注释说「给定提示词被拒是确定性的」,而现场报告
|
|
@@ -2241,19 +2261,24 @@ async function foldOnce(deps, agent, measurement, window, target) {
|
|
|
2241
2261
|
* 可能变化、插件也可能因为学到偏好而从第一发就带扰动。所以「重发一次」不是把被拒
|
|
2242
2262
|
* 的字节原样再塞一遍(那才是真正的浪费),而是**开一个新回合**。
|
|
2243
2263
|
*
|
|
2264
|
+
* #163 的 2026-10-29 实测把这条解释收窄了:固定 system 指纹每轮逐字节相同,新回合
|
|
2265
|
+
* 换不掉它;只有被拒内容恰好落在随轮次变化的段上时才可能碰过。不要把它当成
|
|
2266
|
+
* 「11128 只要重发就能过」的承诺。
|
|
2267
|
+
*
|
|
2244
2268
|
* 本模块就是把这个「手动再发一次」自动化,并且**只做这件事**:
|
|
2245
2269
|
*
|
|
2246
2270
|
* - 只认 `channel_reject` 这一种失败。配额、限流、网络、内容审核都有各自的处置,
|
|
2247
2271
|
* 那些失败再发「继续」只会多烧一次请求(`NO_CONTINUE_CODES` 的同一判断)。
|
|
2248
|
-
* - 上限默认
|
|
2249
|
-
*
|
|
2272
|
+
* - 上限默认 **1** 次,**连续**计数:一次成功回合或一条真实用户消息立即清零。
|
|
2273
|
+
* 旧默认是 3;#163 的实测把「新回合会换掉被拒内容」证伪了(固定 system 指纹每轮
|
|
2274
|
+
* 相同,3 次只是把同一个 400 付三遍),所以一次足够:1 次救不了,3 次也救不了。
|
|
2250
2275
|
* - 不做换号、不换区域、不改请求内容、不重试同一份字节:换号对这类拒绝无效,而
|
|
2251
2276
|
* 篡改请求才是这条功能不该碰的东西。
|
|
2252
2277
|
* - 子代理会话一律不碰:子代理由父代理驱动,抢跑会打乱执行树。
|
|
2253
2278
|
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2279
|
+
* 它是**最后兜底**,不是 11128 的修复:固定指纹由 `prepareChatBody` 的按句改词处理;
|
|
2280
|
+
* 改词后仍 11128,多半是新的固定指纹,重发同样无效。只有「被拒内容刚好随轮次变化」
|
|
2281
|
+
* 的窄情况才可能靠它碰过去,所以默认关闭、默认只发 1 次。
|
|
2257
2282
|
*
|
|
2258
2283
|
* 参考:`dsh-client-auto-continue`(MIT,HsiangNianian/dsh-auto-continue)—
|
|
2259
2284
|
* 「连续失败上限」这套**语义**取自它:`state.consecutive` 在每次发出时自增、
|
|
@@ -2314,7 +2339,7 @@ const SESSION_EVICT = 50;
|
|
|
2314
2339
|
* @returns 归一化后的设置。
|
|
2315
2340
|
*/
|
|
2316
2341
|
function channelContinueSettingsOf(enabled, max, text) {
|
|
2317
|
-
const capped = Math.min(MAX_CONSECUTIVE_CEILING, Math.max(1, typeof max === "number" && Number.isFinite(max) ? Math.trunc(max) :
|
|
2342
|
+
const capped = Math.min(MAX_CONSECUTIVE_CEILING, Math.max(1, typeof max === "number" && Number.isFinite(max) ? Math.trunc(max) : 1));
|
|
2318
2343
|
const body = typeof text === "string" && text.trim() !== "" ? text.trim() : DEFAULT_CONTINUE_TEXT;
|
|
2319
2344
|
return {
|
|
2320
2345
|
enabled: enabled === true,
|
|
@@ -11305,16 +11330,16 @@ const Config = z.object({
|
|
|
11305
11330
|
usageIdentity: asVolatile(z.boolean().default(false).description("Make the upstream's usage page record these calls like the official client's: add the X-IDE-* identity headers (which fill the 使用端 column) and send the conversation ids as 32-hex UUIDv7. Off by default — it is a display alignment measured by a reporter, not by this plugin, and the chat channel identity (User-Agent) and request body never change.")),
|
|
11306
11331
|
keepPromptLast: asVolatile(z.boolean().default(false).description("Move the harness-injected runtime context above the real prompt, so the WorkBuddy usage page records what you actually typed instead of the injected block. Message count and text are preserved; the one edit is the recorded prompt's own leading <acp> marker. Off by default because it changes the message order the upstream counts.")),
|
|
11307
11332
|
extraInjectedPrefixes: asVolatile(z.array(z.string()).default([]).description("Extra literal prefixes (issue #133) treated like the harness-injected context markers when keepPromptLast is on — for blocks a companion plugin injects that the built-in list does not recognize. Each entry matches the start of a trailing user message; read only while keepPromptLast is enabled. Sampled when the plugin mounts: reload the plugin (or restart the host) after changing it.")),
|
|
11308
|
-
dropForeignThinking: asVolatile(z.boolean().default(false).description("Drop foreign thinking blocks that pi-ai would publish as prose (issue #147).
|
|
11333
|
+
dropForeignThinking: asVolatile(z.boolean().default(false).description("Drop foreign thinking blocks that pi-ai would publish as prose (issue #147). CROSS-PROVIDER turns are dropped automatically: after a provider switch the other provider's unsigned thinking blocks can never be replayed, and pi-ai would flatten them into assistant prose that the gateway refuses with 11128 (issue #163's measured scene). This switch only widens the repair to a model switch inside one of this bundle's own routes. On, those blocks are removed from the request; text and tool calls are untouched. Off by default for the same-route leg because it changes bytes the upstream counts.")),
|
|
11309
11334
|
sessionTitle: asVolatile(z.object({
|
|
11310
11335
|
enabled: z.boolean().default(false),
|
|
11311
11336
|
model: z.string().default("wb-lite")
|
|
11312
11337
|
}).default({}).description("Move the host session-title request onto a hidden lightweight model (issues #132/#133). The host fires one real upstream request per new session either way; enabled re-points it at `model` (default wb-lite, measured 0 credits by the reporter) instead of billing the main paid model. Request volume still counts, and the lightweight model is reporter-measured, not reproduced here. Sampled when the plugin mounts: reload the plugin (or restart the host) after changing it.")),
|
|
11313
11338
|
channelRejectContinue: asVolatile(z.object({
|
|
11314
11339
|
enabled: z.boolean().default(false),
|
|
11315
|
-
maxConsecutive: z.number().step(1).min(1).max(10).default(
|
|
11340
|
+
maxConsecutive: z.number().step(1).min(1).max(10).default(1),
|
|
11316
11341
|
text: z.string().default("继续")
|
|
11317
|
-
}).default({}).description("After the gateway refuses a turn with `channel_reject` (business code 11128), send 「继续」 once
|
|
11342
|
+
}).default({}).description("LAST RESORT, not a fix for 11128. After the gateway refuses a turn with `channel_reject` (business code 11128), send 「继续」 once automatically. Off by default: it spends a real request without the user typing anything. Fixed system fingerprints are handled by prepareChatBody's sentence rewrites; they are byte-identical every turn, so resending only pays for the same 400 again. The narrow case this can help is content that changes between turns. `maxConsecutive` (default 1) counts CONSECUTIVE auto-sends: one successful turn, or one real user message, resets it. Subagent sessions are never touched. Live: the engine reads this on every failure, so no reload is needed."))
|
|
11318
11343
|
});
|
|
11319
11344
|
/**
|
|
11320
11345
|
* One region's saved model state. A config written before the region split has
|
|
@@ -13086,6 +13111,7 @@ function apply(ctx, config) {
|
|
|
13086
13111
|
blocks,
|
|
13087
13112
|
atMs: Date.now()
|
|
13088
13113
|
};
|
|
13114
|
+
if (unwrapVolatile(current().dropForeignThinking) !== true) ctx.logger.warn(`dsh-connect-workbuddy: dropped ${blocks.length} cross-provider thinking block(s) before a WorkBuddy request (provider switch); see issue #163`);
|
|
13089
13115
|
}
|
|
13090
13116
|
});
|
|
13091
13117
|
invalidateCatalog = () => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
|
|
5
|
-
"version": "5.13.
|
|
5
|
+
"version": "5.13.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|