dsh-deepseek-web-login 0.6.27 → 0.6.29
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 +62 -0
- package/lib/index.js +122 -15
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
本项目大致遵循语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.29 — 2026-10-02
|
|
6
|
+
|
|
7
|
+
**修复:登录态过期(或账号被限流)之后,重新登录会"又新建一个网页端会话"、整段历史全量重发。**
|
|
8
|
+
|
|
9
|
+
现场:用户在旧窗口发了一条消息,网页端凭空多出一个会话;DSH 日志里那一轮是
|
|
10
|
+
`API 密钥无效`(AUTH)→ 重试 1795ms。他随后重新登录了**同一个账号**,但会话没能接回去。
|
|
11
|
+
`feed-decisions.jsonl` 对上了:那次是 `new-session`,而 `account` 前后**完全一样** ——
|
|
12
|
+
账号没变,是**会话槽被退役了**。
|
|
13
|
+
|
|
14
|
+
**根因**:`retireSession()` 被当成"失败即弃"用在了三处失败分支(`webapi.ts`):
|
|
15
|
+
建连抛错、`!resp.ok`、业务错误信封。401/403 说的是**令牌**过期、429 说的是**账号级**限流
|
|
16
|
+
—— 这些跟"这个会话"毫无关系,而且**换一个新会话账号照样被限**:退役它没有任何好处,
|
|
17
|
+
唯一的效果是把用户整段对话丢掉(重登后只能新建会话 + 把历史全量重发一遍)。
|
|
18
|
+
|
|
19
|
+
**改法**:只有**服务端明说"这个会话废了"**(业务码 `invalid chat session id`)才退役;
|
|
20
|
+
认证与限流这类**账号级**失败改为记进 `keepIds`,由收尾逻辑保住。
|
|
21
|
+
|
|
22
|
+
- 判定发生在 `openCompletion`(只有它看得见错误码/业务码),消费发生在
|
|
23
|
+
`streamWebCompletion` 的 finally —— 因为建连阶段失败时外层 `sessionId` **还是 undefined**
|
|
24
|
+
(它在 `await` 之后才赋值),`id === sessionId` 恒假,靠错误码在外层判会连"本轮真正在用的
|
|
25
|
+
那个会话"一起退役。
|
|
26
|
+
- ⚠️ 5xx / 网络失败 / 取消 / 空响应**不在**保留之列:那些情况下服务端可能已经开始生成、
|
|
27
|
+
会话停在半路,照旧退役(N04 的意图不削弱)。原「失败即弃」用例改用 500 并改名为
|
|
28
|
+
「服务端 5xx(会话可能停在半路)⇒ 不复用那个会话」,意图不变、范围收窄。
|
|
29
|
+
|
|
30
|
+
**新增 5 条用例**:401 / 429 / `user is muted` / `throttled` 四种账号级失败后必须**接回原会话**、
|
|
31
|
+
且不得把会话交给宿主去删;外加"新窗口第一轮就 401"(走的是收尾逻辑那条路径 ——
|
|
32
|
+
复用来的会话不在 `owned` 里,两条路径必须各守一条)。
|
|
33
|
+
四处变异全部确认变红:内联跳过、信封分支、收尾闸门,以及重建后的产物断言。
|
|
34
|
+
|
|
35
|
+
## 0.6.28 — 2026-10-02
|
|
36
|
+
|
|
37
|
+
**修复:思考通道的垃圾(DSML 等工具调用标记)会显示在网页端、并且一直留在转写里。**
|
|
38
|
+
|
|
39
|
+
起因是用户拿另一个项目(cuckoo-code)对照,说"用它从来没在网页端见过 DSML,一个项目就是一个会话"。
|
|
40
|
+
读数之后确认真凶不在会话管理(0.6.22~0.6.26 已经修好,实机 `feed-decisions.jsonl` 里连续 6 轮
|
|
41
|
+
`chained / tailSame=true` 为证),而在**思考通道**:
|
|
42
|
+
|
|
43
|
+
- **思考通道此前一道网都没有**。正文通道有四道网(工具调用捕获 / 残片剥离 / 伪系统标记 /
|
|
44
|
+
免责声明 / 回声守卫),而 `adapter.ts` 里思考分支一句 `continue` 把网全跳过了。
|
|
45
|
+
模型在思考里起草工具调用(JSON 或 XML/DSML)或伪造 `<ds_system>` 块时,标记会:
|
|
46
|
+
① 原样显示在网页端的思考区;② 进 DSH 历史 ⇒ 下一轮被当增量**重发** ⇒
|
|
47
|
+
变成网页端可见的正文垃圾,并且**一直留着**,每轮重复。
|
|
48
|
+
- **新增 `ReasoningSanitizer`**(protocol.ts):复用 `ToolCallStreamFilter` 那套久经考验的捕获逻辑,
|
|
49
|
+
但**不提取调用、不报拒绝** —— 思考里出现标记时模型还在推理,执行它是语义错误;报拒绝则会
|
|
50
|
+
把一次本来正常的续写打断。只把标记本身抹掉,思考的其余部分一个字都不许少。
|
|
51
|
+
轮末必须 `flush()`,否则被跨包 hold 住的尾巴会静默少一截。
|
|
52
|
+
|
|
53
|
+
**顺带修一个把诊断数据搞脏的缺陷:**
|
|
54
|
+
|
|
55
|
+
- 跑一次 `npm run test` 会往**用户真实的** `~/.dsh/web-login/feed-decisions.jsonl` 灌 695 条
|
|
56
|
+
测试噪声(`sess-1..7`、同一毫秒)。诊断数据被污染比没有数据更糟 —— 排查真实问题前得先手工
|
|
57
|
+
滤掉噪声。两层都漏:① 4 个用例(fetch-injection / session-lifecycle / session-reuse /
|
|
58
|
+
session-journal)没设 `DSH_HOME`;② 跑批脚本 `test-offline.mjs` 没有兜底。
|
|
59
|
+
现在跑批给每个子进程注入临时 `DSH_HOME`,另加 `check-test-isolation.mjs` 守"会写状态的用例
|
|
60
|
+
必须自己隔离",新用例自动纳入。
|
|
61
|
+
- `feedDecisionLogPath()` 改为走 `webLoginDir()`(原先自己拼了一遍 `DSH_HOME || ~/.dsh`,
|
|
62
|
+
是路径的第二份来源);`dumpSinkPath()` 同理改走 `resolveDshHome()`。
|
|
63
|
+
|
|
64
|
+
**新增:** `dev/scan-dsml.mjs`(扫会话日志里有没有工具调用标记,含多帧 zstd 解压)、
|
|
65
|
+
`dev/scan-session-images.mjs`、`dev/dump-image-messages.mjs`、`dev/trace-session-images.mjs`。
|
|
66
|
+
|
|
5
67
|
## 0.6.27 — 2026-10-02
|
|
6
68
|
|
|
7
69
|
**修复:早先发过的图片被反复重挂在后面的每条消息上("我这轮没发图,网页端却又有图")。**
|
package/lib/index.js
CHANGED
|
@@ -2769,6 +2769,53 @@ var ToolCallStreamFilter = class {
|
|
|
2769
2769
|
}
|
|
2770
2770
|
};
|
|
2771
2771
|
/**
|
|
2772
|
+
* 思考通道净化器(0.6.28)。
|
|
2773
|
+
*
|
|
2774
|
+
* ## 为什么需要它
|
|
2775
|
+
*
|
|
2776
|
+
* 正文通道有四道网(工具调用捕获、`stripStrayToolMarkup`、伪系统标记、
|
|
2777
|
+
* 免责声明、回声守卫),而思考通道此前是**原样直通**的 —— `adapter.ts` 里
|
|
2778
|
+
* `if (event.kind === 'thinking') { … ; continue }` 一句 `continue` 跳过了全部过滤。
|
|
2779
|
+
*
|
|
2780
|
+
* 后果是双份的,而且第二份更严重:
|
|
2781
|
+
* 1. 标记原样显示在**网页端的思考区**(用户 2026-10-02:「我从没见过 cuckoo 有 DSML」);
|
|
2782
|
+
* 2. 它进了 **DSH 的历史** ⇒ 下一轮被我们当增量**重发** ⇒ 网页端看到的就是正文里的垃圾,
|
|
2783
|
+
* 并且这团垃圾会**一直留在转写里**,每轮重复(这才是真正让对话看起来不像人的地方)。
|
|
2784
|
+
*
|
|
2785
|
+
* ## 与 `ToolCallStreamFilter` 的关键差别
|
|
2786
|
+
*
|
|
2787
|
+
* 复用它的**捕获**逻辑(跨包、配平、宽容抢救都是久经考验的),但三处不同:
|
|
2788
|
+
* - **不提取调用**:`out.calls` 直接丢掉。思考里出现调用标记时模型还在推理、
|
|
2789
|
+
* 并没有决定要调 —— 从思考里执行工具是语义错误。
|
|
2790
|
+
* - **不报拒绝**:不设 `rejected`。思考里有杂音是常态,报拒绝会触发整步重试,
|
|
2791
|
+
* 把一次本来正常的续写打断(正文通道必须报,因为丢掉的是真调用)。
|
|
2792
|
+
* - **剥离后照常透出**:只把标记本身抹掉,思考的其余部分一个字都不许少。
|
|
2793
|
+
*/
|
|
2794
|
+
var ReasoningSanitizer = class {
|
|
2795
|
+
filter = new ToolCallStreamFilter();
|
|
2796
|
+
didStrip = false;
|
|
2797
|
+
/** 喂一段思考增量,返回**可以上屏**的部分(可能为空串)。 */
|
|
2798
|
+
push(text) {
|
|
2799
|
+
const out = this.filter.push(text);
|
|
2800
|
+
if (out.calls.length > 0) this.didStrip = true;
|
|
2801
|
+
const body = stripStrayToolMarkup(out.text);
|
|
2802
|
+
if (body !== out.text) this.didStrip = true;
|
|
2803
|
+
return body;
|
|
2804
|
+
}
|
|
2805
|
+
/** 流结束:把扣住没吐的尾巴交出来。 */
|
|
2806
|
+
flush() {
|
|
2807
|
+
const out = this.filter.flush();
|
|
2808
|
+
if (out.calls.length > 0 || out.rejected) this.didStrip = true;
|
|
2809
|
+
const body = stripStrayToolMarkup(out.text);
|
|
2810
|
+
if (body !== out.text) this.didStrip = true;
|
|
2811
|
+
return body;
|
|
2812
|
+
}
|
|
2813
|
+
/** 是否剥掉过东西(供日志与用例判定,不影响输出)。 */
|
|
2814
|
+
get stripped() {
|
|
2815
|
+
return this.didStrip;
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
/**
|
|
2772
2819
|
* 剥离模型模仿的「系统标记」(`<ds_system>…</ds_system>` / `<system>…</system>`)。
|
|
2773
2820
|
*
|
|
2774
2821
|
* 实测(deepseek-web):模型会在正文里吐出成串的伪系统标记,**模仿它见过的协议格式**。
|
|
@@ -4675,7 +4722,7 @@ const THINKING_WRAPPER_RE = /<\s*\/?\s*(analysis|summary|thinking|scratchpad|tho
|
|
|
4675
4722
|
function dumpSinkPath() {
|
|
4676
4723
|
if (process.env.DSH_WEB_LOGIN_DUMP_SSE !== "1") return null;
|
|
4677
4724
|
try {
|
|
4678
|
-
const dir = join(
|
|
4725
|
+
const dir = join(resolveDshHome(), "deepseek-web", "frames");
|
|
4679
4726
|
mkdirSync(dir, { recursive: true });
|
|
4680
4727
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4681
4728
|
return join(dir, `${stamp}-${Math.random().toString(36).slice(2, 8)}.sse`);
|
|
@@ -5143,9 +5190,15 @@ function dropSentRefIds(sessionId) {
|
|
|
5143
5190
|
* 同时"哪一轮开始退回全量、为什么"又一定能看见。
|
|
5144
5191
|
*/
|
|
5145
5192
|
let lastFeedReason;
|
|
5146
|
-
/**
|
|
5193
|
+
/**
|
|
5194
|
+
* 投喂决策留痕的路径(与 gate.json 同目录)。
|
|
5195
|
+
* ⚠️ 必须走 `webLoginDir()` —— 曾经这里自己拼了一遍 `DSH_HOME || ~/.dsh`,
|
|
5196
|
+
* 于是"测试隔离"只对走 webLoginDir 的模块生效,这一条照旧写进用户的真实目录
|
|
5197
|
+
* (2026-10-02:一次 `npm run test` 往用户的 `feed-decisions.jsonl` 灌了 695 条测试噪声,
|
|
5198
|
+
* 手工过滤才看得清真实数据)。路径只能有一个来源。
|
|
5199
|
+
*/
|
|
5147
5200
|
function feedDecisionLogPath() {
|
|
5148
|
-
return join(
|
|
5201
|
+
return join(webLoginDir(), "feed-decisions.jsonl");
|
|
5149
5202
|
}
|
|
5150
5203
|
/** 超过这个字节数就裁剪一次(`statSync` 很便宜,不必每次读全文件)。 */
|
|
5151
5204
|
const FEED_DECISION_MAX_BYTES = 262144;
|
|
@@ -5342,7 +5395,7 @@ function makeCanFailover(params) {
|
|
|
5342
5395
|
}
|
|
5343
5396
|
};
|
|
5344
5397
|
}
|
|
5345
|
-
async function openCompletion(auth, params, signal, transport) {
|
|
5398
|
+
async function openCompletion(auth, params, signal, transport, keepIds) {
|
|
5346
5399
|
let lastFailure;
|
|
5347
5400
|
const canFailover = makeCanFailover(params);
|
|
5348
5401
|
const slotKey = requestSlotKey(auth, params);
|
|
@@ -5424,8 +5477,11 @@ async function openCompletion(auth, params, signal, transport) {
|
|
|
5424
5477
|
signal
|
|
5425
5478
|
});
|
|
5426
5479
|
} catch (error) {
|
|
5427
|
-
|
|
5428
|
-
|
|
5480
|
+
if (error instanceof AdapterLlmError && (error.code === "AUTH" || error.code === "RATE_LIMIT")) keepIds?.add(sessionId);
|
|
5481
|
+
else {
|
|
5482
|
+
retireSession(sessionId);
|
|
5483
|
+
params.onDeleteSession?.(sessionId);
|
|
5484
|
+
}
|
|
5429
5485
|
if (error instanceof AdapterLlmError) throw error;
|
|
5430
5486
|
if (params.signal?.aborted) throw new AdapterLlmError("DeepSeek web request aborted by caller", "ABORTED", { cause: error });
|
|
5431
5487
|
throw new AdapterLlmError(`DeepSeek web request failed: ${error?.message ?? error}`, "TRANSPORT", { cause: error });
|
|
@@ -5435,8 +5491,11 @@ async function openCompletion(auth, params, signal, transport) {
|
|
|
5435
5491
|
const code = httpErrorCode(resp.status);
|
|
5436
5492
|
const retryAfter = parseRetryAfterMs(resp.headers.get("retry-after"));
|
|
5437
5493
|
const hint = code === "AUTH" ? " —— 网页登录态可能已过期,请到「设置 → DeepSeek 网页登录」重新登录" : code === "RATE_LIMIT" ? " —— 网页版限流" : "";
|
|
5438
|
-
|
|
5439
|
-
|
|
5494
|
+
if (code === "AUTH" || code === "RATE_LIMIT") keepIds?.add(sessionId);
|
|
5495
|
+
else {
|
|
5496
|
+
retireSession(sessionId);
|
|
5497
|
+
params.onDeleteSession?.(sessionId);
|
|
5498
|
+
}
|
|
5440
5499
|
const authRetryAfterMs = code === "AUTH" ? canFailover("auth") ? AUTH_FAILOVER_RETRY_MS : AUTH_GIVEUP_RETRY_MS : void 0;
|
|
5441
5500
|
throw new AdapterLlmError(`DeepSeek web completion failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 200)}` : ""}${hint}`, code, {
|
|
5442
5501
|
status: resp.status,
|
|
@@ -5481,9 +5540,14 @@ async function openCompletion(auth, params, signal, transport) {
|
|
|
5481
5540
|
providerRetryAfterMs: canFailover("throttled") ? FAILOVER_RETRY_MS : THROTTLE_RETRY_MS
|
|
5482
5541
|
} : {}
|
|
5483
5542
|
}) : new AdapterLlmError(`DeepSeek 网页端返回了非流式响应(content-type: ${contentType || "unknown"}):${text.slice(0, 200)}`, "MALFORMED_RESPONSE", { status: resp.status });
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
if (
|
|
5543
|
+
const accountLevel = !!biz && (muted || busy || throttled || failureCode === "AUTH");
|
|
5544
|
+
const ruined = !!biz && isInvalidSessionError(biz);
|
|
5545
|
+
if (accountLevel) keepIds?.add(sessionId);
|
|
5546
|
+
else {
|
|
5547
|
+
retireSession(sessionId);
|
|
5548
|
+
params.onDeleteSession?.(sessionId);
|
|
5549
|
+
}
|
|
5550
|
+
if (attempt === 0 && ruined) {
|
|
5487
5551
|
lastFailure = failure;
|
|
5488
5552
|
continue;
|
|
5489
5553
|
}
|
|
@@ -5576,6 +5640,26 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
|
|
|
5576
5640
|
* 所以必须在这里兜住它后续才返回的那些会话。
|
|
5577
5641
|
*/
|
|
5578
5642
|
const owned = /* @__PURE__ */ new Set();
|
|
5643
|
+
/**
|
|
5644
|
+
* 本轮**账号级失败**(认证/限流)时不该被退役的会话(0.6.29)。
|
|
5645
|
+
*
|
|
5646
|
+
* 为什么不能放在 `finally` 里靠错误码判:建连阶段失败时外层 `sessionId` **还是 undefined**
|
|
5647
|
+
* (它在 `await openCompletion(...)` 之后才赋值),`id === sessionId` 恒假 ⇒ 会连
|
|
5648
|
+
* "本轮真正在用的那个会话"一起退役。必须先把它记下来 —— 只有 `openCompletion` 知道。
|
|
5649
|
+
*
|
|
5650
|
+
* 判据(在那里判,这里只消费):`AUTH`(HTTP 401/403、信封 40003/40001)与
|
|
5651
|
+
* `RATE_LIMIT`(HTTP 429、`user is muted`、`busy generating`、`throttled` 文案族)
|
|
5652
|
+
* 说的都是**账号级**状态 —— 令牌过期、账号被限到某时刻、账号同时在生成。
|
|
5653
|
+
* 换一个新网页端会话一点用都没有(账号照样被限),唯一的效果是把用户的对话丢掉:
|
|
5654
|
+
* 他重新登录后只能新建会话、把整段历史全量重发一遍。
|
|
5655
|
+
* 2026-10-02 现场:登录态过期 ⇒ 401 ⇒ 会话被弃 ⇒ `feed-decisions` 里同账号冒出
|
|
5656
|
+
* `new-session`、网页端"又新建一个窗口"、29 条历史全量重发。
|
|
5657
|
+
*
|
|
5658
|
+
* ⚠️ 5xx / 网络失败 / 取消 / 空响应 **不在此列** —— 那些情况下服务端可能已经开始生成、
|
|
5659
|
+
* 会话停在半路,照旧退役(N04 的意图不能削弱);会话真的废了则由业务码
|
|
5660
|
+
* `invalid chat session id` 显式告知。
|
|
5661
|
+
*/
|
|
5662
|
+
const keepIds = /* @__PURE__ */ new Set();
|
|
5579
5663
|
let finalized = false;
|
|
5580
5664
|
const tracked = {
|
|
5581
5665
|
...transport,
|
|
@@ -5604,7 +5688,7 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
|
|
|
5604
5688
|
...params,
|
|
5605
5689
|
sessionReuseTurns: limit,
|
|
5606
5690
|
onDeleteSession: cleanup
|
|
5607
|
-
}, signal, tracked));
|
|
5691
|
+
}, signal, tracked, keepIds));
|
|
5608
5692
|
sessionId = opened.sessionId;
|
|
5609
5693
|
sentFeed = opened.feed;
|
|
5610
5694
|
sentChainKey = slotKey;
|
|
@@ -5645,6 +5729,7 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
|
|
|
5645
5729
|
const roundOk = complete || sawTerminal;
|
|
5646
5730
|
lastRetireReason = void 0;
|
|
5647
5731
|
for (const id of owned) {
|
|
5732
|
+
if (limit > 0 && keepIds.has(id) && !poisoned) continue;
|
|
5648
5733
|
if (id === sessionId && roundOk && !poisoned && limit > 0) continue;
|
|
5649
5734
|
lastRetireReason = id !== sessionId ? "extra-session" : poisoned ? "poisoned" : !roundOk ? "not-finished" : "rotated";
|
|
5650
5735
|
retireSession(id);
|
|
@@ -6695,6 +6780,7 @@ function createAdapter(deps) {
|
|
|
6695
6780
|
const prompt = promptParts.full;
|
|
6696
6781
|
const knownNames = new Set((options?.tools ?? []).map((tool) => String(tool?.name ?? "")));
|
|
6697
6782
|
let filter = new ToolCallStreamFilter(knownNames);
|
|
6783
|
+
let reasoningSanitizer = new ReasoningSanitizer();
|
|
6698
6784
|
let echoGuard = new TranscriptEchoGuard();
|
|
6699
6785
|
let systemMarkerFilter = new SystemMarkerStreamFilter();
|
|
6700
6786
|
let boilerplate = new BoilerplateFilter();
|
|
@@ -6817,6 +6903,8 @@ function createAdapter(deps) {
|
|
|
6817
6903
|
})) {
|
|
6818
6904
|
if (event.kind === "thinking" || event.kind === "text") roundUsage.outputChars += event.text.length;
|
|
6819
6905
|
if (event.kind === "thinking") {
|
|
6906
|
+
const clean = reasoningSanitizer.push(event.text);
|
|
6907
|
+
if (!clean) continue;
|
|
6820
6908
|
const block = openReasoning();
|
|
6821
6909
|
if (!reasoningStarted) {
|
|
6822
6910
|
reasoningStarted = true;
|
|
@@ -6826,11 +6914,11 @@ function createAdapter(deps) {
|
|
|
6826
6914
|
blockType: "reasoning"
|
|
6827
6915
|
};
|
|
6828
6916
|
}
|
|
6829
|
-
block.text +=
|
|
6917
|
+
block.text += clean;
|
|
6830
6918
|
yield {
|
|
6831
6919
|
type: "reasoning-delta",
|
|
6832
6920
|
index: block.index,
|
|
6833
|
-
text:
|
|
6921
|
+
text: clean
|
|
6834
6922
|
};
|
|
6835
6923
|
continue;
|
|
6836
6924
|
}
|
|
@@ -6888,6 +6976,24 @@ function createAdapter(deps) {
|
|
|
6888
6976
|
logger?.warn?.(`deepseek-web: 自动续写第 ${rounds} 轮失败,保留已输出部分:${roundError.message}`);
|
|
6889
6977
|
} else throw error;
|
|
6890
6978
|
}
|
|
6979
|
+
const reasoningTail = reasoningSanitizer.flush();
|
|
6980
|
+
if (reasoningTail) {
|
|
6981
|
+
const block = openReasoning();
|
|
6982
|
+
if (!reasoningStarted) {
|
|
6983
|
+
reasoningStarted = true;
|
|
6984
|
+
yield {
|
|
6985
|
+
type: "block-start",
|
|
6986
|
+
index: block.index,
|
|
6987
|
+
blockType: "reasoning"
|
|
6988
|
+
};
|
|
6989
|
+
}
|
|
6990
|
+
block.text += reasoningTail;
|
|
6991
|
+
yield {
|
|
6992
|
+
type: "reasoning-delta",
|
|
6993
|
+
index: block.index,
|
|
6994
|
+
text: reasoningTail
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6891
6997
|
const drained = drainTextPipeline(filter, boilerplate, echoGuard, false);
|
|
6892
6998
|
if (drained.echoed) echoedTranscript = true;
|
|
6893
6999
|
if (drained.disclaimers > 0) disclaimerStripped = true;
|
|
@@ -6975,6 +7081,7 @@ function createAdapter(deps) {
|
|
|
6975
7081
|
});
|
|
6976
7082
|
currentPrompt = promptParts.full;
|
|
6977
7083
|
filter = new ToolCallStreamFilter(knownNames);
|
|
7084
|
+
reasoningSanitizer = new ReasoningSanitizer();
|
|
6978
7085
|
echoGuard = new TranscriptEchoGuard();
|
|
6979
7086
|
systemMarkerFilter = new SystemMarkerStreamFilter();
|
|
6980
7087
|
boilerplate = new BoilerplateFilter();
|
|
@@ -10152,7 +10259,7 @@ async function checkForUpdate(current, fetchImpl) {
|
|
|
10152
10259
|
* 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
|
|
10153
10260
|
*/
|
|
10154
10261
|
/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
|
|
10155
|
-
const FALLBACK_VERSION = "0.6.
|
|
10262
|
+
const FALLBACK_VERSION = "0.6.29";
|
|
10156
10263
|
let cached;
|
|
10157
10264
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
10158
10265
|
function pluginVersion() {
|