dsh-deepseek-web-login 0.6.30 → 0.6.32
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 +93 -0
- package/lib/index.js +75 -16
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,99 @@
|
|
|
2
2
|
|
|
3
3
|
本项目大致遵循语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.32 — 2026-10-02
|
|
6
|
+
|
|
7
|
+
**修复:链式投喂"退回全量重发"时会重发整份固定头(约 6.35 万字符)—— 那一段是纯重复。**
|
|
8
|
+
|
|
9
|
+
起因是用户对比了 cuckoo 与自己的分享链接:同一段 `Tool Calling Protocol` 在网页端**出现了两次**。
|
|
10
|
+
|
|
11
|
+
**先看数据,不猜**(`feed-decisions.jsonl`,25 条真实记录):
|
|
12
|
+
|
|
13
|
+
| reason | 次数 | 含义 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `chained`(增量) | 14(56%) | 正常 |
|
|
16
|
+
| `new-session` | 6(24%) | **新窗口的第一句** —— 新会话必须全量,天经地义 |
|
|
17
|
+
| `no-parts` | 4(16%) | 内部标题请求 |
|
|
18
|
+
| `not-appended`(链断) | **1(4%)** | 只有这一次真的退了全量 |
|
|
19
|
+
|
|
20
|
+
所以"每次调用工具就重发"**与日志不符**(1/25)。但那次重发里有一件**确实该改**的事:
|
|
21
|
+
|
|
22
|
+
**根因**:`replay`("链还在但不能只发增量")一律发整份 `full` = 固定头 + 历史。
|
|
23
|
+
而走到 `replay` 就说明**链还在**(同一个网页端会话、同一个账号)——
|
|
24
|
+
**那条会话的首条消息里已经把固定头给过了**,重发它是纯重复。
|
|
25
|
+
固定头 = system + 协议指令 + 工具目录,实测约 **6.35 万字符**,占一轮的大头;
|
|
26
|
+
网页端看到"同一大段又出现一次"就是它。
|
|
27
|
+
|
|
28
|
+
**改法**:`PromptParts` 新增 `transcript`(`full` 里**属于历史的那一段**,超预算时是截断后那份),
|
|
29
|
+
`replay` 在**头没变**时只发 `transcript`:
|
|
30
|
+
|
|
31
|
+
- `not-appended`(头已确认相同)⇒ **省掉固定头**,省下的就是那 6.35 万字符。
|
|
32
|
+
- `head-changed` ⇒ **必须整份重发**(新头从没发过,省了模型手里就是旧头)。
|
|
33
|
+
- 没传 `transcript` ⇒ 退回旧行为。所以这是一条**纯优化**:漏传只少省一点,不会错。
|
|
34
|
+
- ⚠️ `transcript` 交出去的必须是**截断后**那份 —— 否则"只发历史"会比原来的全量还长,把优化做成事故。有用例钉死。
|
|
35
|
+
|
|
36
|
+
**顺带补上体量诊断**:`feed-decisions.jsonl` 此前只有"**为什么**走全量",没有"**发了多大**"。
|
|
37
|
+
正是这个缺口让我今天只能去读分享页**估**字符数 —— 估出来的数字不能用来下任何结论。
|
|
38
|
+
现在每轮多记 `promptChars`(实发字符数)与 `headChars`(固定头字符数),体量从此可测。
|
|
39
|
+
|
|
40
|
+
**新增 6 条用例**:省头路径(两种触发都要省)/ `head-changed` 必须重发头 / 没传 `transcript` 退回旧行为 /
|
|
41
|
+
`transcript` 恒等于 `full` 里属于历史的那一段 / **截断时 `transcript` 也必须是截断后那份**。
|
|
42
|
+
**三处变异确认变红**:把 `replay` 改回总是发 `full`(2 红)、把 `transcript` 交成未截断那份(1 红)、
|
|
43
|
+
以及重建后的产物断言。
|
|
44
|
+
|
|
45
|
+
> ⚠️ 顺带修掉一条**绑语法形态的产物断言**:它写死 `const replay = (reason)` + 120 字窗口,
|
|
46
|
+
> 于是这次**无害的形态改动**(箭头+对象字面量 → 块体)把它打红了。已放宽窗口并注明理由 ——
|
|
47
|
+
> 产物断言要守**意图**,别守签名长什么样。
|
|
48
|
+
|
|
49
|
+
## 0.6.31 — 2026-10-02
|
|
50
|
+
|
|
51
|
+
**修复:新建一个窗口、只发一句话,网页端会多出一个会话(那个是"生成标题"用的脚手架)。**
|
|
52
|
+
|
|
53
|
+
现场(用户原话):「刚在 dsh 中新建一个窗口聊天,就发了一句话,网页版直接俩窗口」。
|
|
54
|
+
`feed-decisions.jsonl` 里两条对得整整齐齐:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
12:55:54 new-session sess=04c63135 ← 对话那条
|
|
58
|
+
12:56:00 no-parts sess=de6a8d4f ← 网页端多出来的那个
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`de6a8d4f` 正是用户在网页端看到的那条(标题请求的 prompt 直接显示在里面)。
|
|
62
|
+
|
|
63
|
+
**根因**:`session-title` 这类内部请求**必须有一条自己的网页端会话**才能调 completion
|
|
64
|
+
(0.6.26 为了修「`n/n` 分叉」特意把它们和对话分开,见 `requestSlotKey` 的注释)。
|
|
65
|
+
当时的注释写的是「代价是内部请求自己占一个会话,**它不出现在对话里**」——
|
|
66
|
+
**判断漏了一半**:它确实不在*对话*里,但它**出现在侧边栏里**。
|
|
67
|
+
|
|
68
|
+
而它**从来不会被删**:用户设的是 `sessionCleanup: keep`,而清理器的 `schedule()` 在 keep 下
|
|
69
|
+
第一行就 `return` —— 于是这条会话**连 ledger 都没有**(从没安排过删除),永远留在网页端。
|
|
70
|
+
|
|
71
|
+
**改法**:把「内部请求的会话」与「用户的对话会话」在**收尾时彻底分开**。
|
|
72
|
+
|
|
73
|
+
- 收尾时按 `promptParts` 有没有传来分(与 `requestSlotKey`、adapter 的 `chatLike` 同一处判据):
|
|
74
|
+
**不带** = 内部请求 ⇒ 会话是**脚手架**,一律走新的 `onDiscardSession` 通道丢掉,
|
|
75
|
+
**不进用户的清理队列**。
|
|
76
|
+
- ⚠️ **复用来的会话也要丢** —— 它不在收尾逻辑的 `owned` 集合里(那一轮没调 `createSession`),
|
|
77
|
+
这正是最容易漏掉的一半。
|
|
78
|
+
- 清理器新增 `discard(auth, sessionId)`:**不受 `keep` / `manualOnly` 影响** ——
|
|
79
|
+
那两道开关的语义是「别删**我的对话**」,而脚手架会话不是用户的对话。
|
|
80
|
+
- ⚠️ `discard` **只删那一个**,**不 drain 队列**:`manualOnly`(链式模式)下队列里攒的是
|
|
81
|
+
**用户的**会话,借这次机会顺手删掉就变成"用户没点按钮却被删了"(0.6.1x 修过的那个 bug)。
|
|
82
|
+
- ⚠️ 仍然受 `deleteWebSessions === false`("一个都不许删"总闸)约束 —— 那个开关不能破。
|
|
83
|
+
所以**关掉总闸时这类会话仍会留在网页端**,这是已知代价。
|
|
84
|
+
|
|
85
|
+
**顺带把一条"注意事项"升级成硬约束:测试 fixture 模拟真实 chat 时必须传 `promptParts`。**
|
|
86
|
+
改了收尾判据之后,4 个用例文件成批变红 —— 全是拿裸 `params`(不带 `promptParts`)去测
|
|
87
|
+
"chat 会话的轮换 / 回收 / 归属"的。这不是 bug,是夹具没按生产链路的形态构造
|
|
88
|
+
(adapter 只给 `chatLike` 传 `promptParts`)。已给 6 处夹具补上,并在文件里写清为什么。
|
|
89
|
+
|
|
90
|
+
**新增 6 条用例**(`check-internal-session-discard.mjs`):内部请求走「丢掉」通道且不进用户队列 /
|
|
91
|
+
复用的会话也要丢 / 对话请求**不**走丢弃通道(防过度清理)/ `keep` 与 `manualOnly` 下都必须删 /
|
|
92
|
+
`discard` 不许顺手清空用户的队列。**三处变异确认变红**:关掉内部请求分支(2 红)、
|
|
93
|
+
把 `discard` 改成 drain 队列(2 红)、以及重建后的产物断言。
|
|
94
|
+
|
|
95
|
+
> ⚠️ 又记一条**产物断言的坑**:打包器除了会去掉单语句的花括号,还会把 `undefined` 改写成 `void 0`
|
|
96
|
+
> —— 断言里写 `=== undefined` 必然假红(这次踩了)。
|
|
97
|
+
|
|
5
98
|
## 0.6.30 — 2026-10-02
|
|
6
99
|
|
|
7
100
|
**补齐请求头:把浏览器**自动加**的那批指纹头收回来,并修掉一个过时的写死版本号。**
|
package/lib/index.js
CHANGED
|
@@ -1927,15 +1927,18 @@ function serializePromptParts(options) {
|
|
|
1927
1927
|
if (merged.length <= maxChars) return {
|
|
1928
1928
|
head,
|
|
1929
1929
|
entries: lines,
|
|
1930
|
-
full: merged
|
|
1930
|
+
full: merged,
|
|
1931
|
+
transcript
|
|
1931
1932
|
};
|
|
1932
1933
|
const separator = transcript ? "\n\n---\n\n" : "";
|
|
1933
1934
|
const budget = maxChars - head.length - separator.length;
|
|
1934
1935
|
if (budget < 128) throw new AdapterLlmError("系统/工具定义超出 prompt 预算,请减少固定输入或扩大上限", "CONTEXT_WINDOW_EXCEEDED");
|
|
1936
|
+
const sentTranscript = truncateMiddle(transcript, budget, .7);
|
|
1935
1937
|
return {
|
|
1936
1938
|
head,
|
|
1937
1939
|
entries: lines,
|
|
1938
|
-
full: head + separator +
|
|
1940
|
+
full: head + separator + sentTranscript,
|
|
1941
|
+
transcript: sentTranscript
|
|
1939
1942
|
};
|
|
1940
1943
|
}
|
|
1941
1944
|
/** 完整 JSON 调用标记:{"tool_calls": 或 {"tool_call": (允许空白)。 */
|
|
@@ -3555,25 +3558,34 @@ function decideFeed(input) {
|
|
|
3555
3558
|
* (「修改 / 重新生成」+ `n / n` 翻页),用户看到的就是"聊着聊着分叉了"。
|
|
3556
3559
|
* 挂在链尾的代价只是服务端上下文里多一段重复历史(多花一点 token),
|
|
3557
3560
|
* 而分叉是**结构性**的坏:它会永久破坏"一个窗口一条对话线"这个形态。
|
|
3561
|
+
*
|
|
3562
|
+
* 🔴 2026-10-02 补:**头没变时不再重发固定头**。
|
|
3563
|
+
* 走到这里说明链还在(同一个网页端会话、同一个账号)⇒ 那条会话的**首条消息**里
|
|
3564
|
+
* 已经把固定头给过了,重发它是纯粹的重复。而它就是"重发一大段"里的**那一大段**
|
|
3565
|
+
* (system + 协议指令 + 工具目录,实测约 **6.35 万字符**)。
|
|
3566
|
+
* 用户在网页端看到的现象(同一段 Tool Calling Protocol 出现两次)就是它。
|
|
3567
|
+
* ⚠️ 只有 `head-changed` 必须重发头(新头没给过),所以那条路径**不**走这个优化。
|
|
3558
3568
|
*/
|
|
3559
|
-
const replay = (reason) =>
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3569
|
+
const replay = (reason, headUnchanged = false) => {
|
|
3570
|
+
return {
|
|
3571
|
+
prompt: headUnchanged && typeof input.transcript === "string" ? input.transcript : full,
|
|
3572
|
+
parentMessageId: chain.parentId,
|
|
3573
|
+
next: {
|
|
3574
|
+
head,
|
|
3575
|
+
entries: entries.slice(),
|
|
3576
|
+
sessionId: input.sessionId,
|
|
3577
|
+
accountKey: input.accountKey
|
|
3578
|
+
},
|
|
3579
|
+
reason
|
|
3580
|
+
};
|
|
3581
|
+
};
|
|
3570
3582
|
if (!input.reused) return detach("new-session");
|
|
3571
3583
|
const chain = input.chain;
|
|
3572
3584
|
if (!chain) return detach("no-chain");
|
|
3573
3585
|
if (chain.sessionId !== input.sessionId) return detach("session-changed");
|
|
3574
3586
|
if (chain.accountKey !== input.accountKey) return detach("account-changed");
|
|
3575
3587
|
if (chain.head !== head) return replay("head-changed");
|
|
3576
|
-
if (!canExtendChain(chain.entries, entries)) return replay("not-appended");
|
|
3588
|
+
if (!canExtendChain(chain.entries, entries)) return replay("not-appended", true);
|
|
3577
3589
|
const appended = entries.slice(chain.entries.length);
|
|
3578
3590
|
const meaningful = appended.some((line) => isContinuationCue(line)) ? appended : appended.filter((line) => !isAssistantTranscriptEntry(line));
|
|
3579
3591
|
const echoDropped = meaningful.length > 0 ? appended.length - meaningful.length : 0;
|
|
@@ -4501,6 +4513,30 @@ function createSessionCleaner(options = {}) {
|
|
|
4501
4513
|
sessionId: item.sessionId
|
|
4502
4514
|
});
|
|
4503
4515
|
}
|
|
4516
|
+
/**
|
|
4517
|
+
* 立刻丢掉**一个我们自己的脚手架会话**(内部请求用的那个),**不受清理策略影响**。
|
|
4518
|
+
*
|
|
4519
|
+
* 为什么要单独一条路:`keep`(用户选的「不删」)与 `manualOnly`(链式模式下攒着等手动清)
|
|
4520
|
+
* 都让 `schedule()` 变成空操作 —— 那对**用户的对话**是对的,对**我们自己建的会话**是错的。
|
|
4521
|
+
* 内部请求(`session-title` / 压缩之类)必须有一条会话才能调 completion,而那条会话
|
|
4522
|
+
* 既不是用户建的、用户也不需要它 —— 留在网页端就是**凭空多出来的一个会话**。
|
|
4523
|
+
*
|
|
4524
|
+
* 🔴 2026-10-02 用户现场:「刚在 dsh 中新建一个窗口聊天,就发了一句话,网页版直接俩窗口」。
|
|
4525
|
+
* `feed-decisions.jsonl` 里对上了:12:55:54 `new-session`(对话,`04c63135`)+
|
|
4526
|
+
* 12:56:00 `no-parts`(内部请求,`de6a8d4f`)—— 后者正是网页端多出来的那一个,
|
|
4527
|
+
* 而且它**连 ledger 都没有**(keep 模式下 `schedule()` 直接 return,从没安排过删除)。
|
|
4528
|
+
*
|
|
4529
|
+
* ⚠️ 只删**这一个**,**不去 drain 队列**:`manualOnly` 下队列里攒的是用户的会话,
|
|
4530
|
+
* 借这次机会顺手把它们删掉,就变成"用户没点按钮却被删了"(0.6.1x 修过的那个 bug)。
|
|
4531
|
+
* ⚠️ 仍然受 `deleteWebSessions === false`(总闸)约束 —— 那个开关的语义是"一个都不许删",
|
|
4532
|
+
* 接线在 adapter.ts。所以**关掉总闸时这类会话仍会留在网页端**,这是已知代价。
|
|
4533
|
+
*/
|
|
4534
|
+
function discard(auth, sessionId) {
|
|
4535
|
+
return deleteChunk([{
|
|
4536
|
+
auth,
|
|
4537
|
+
sessionId
|
|
4538
|
+
}]);
|
|
4539
|
+
}
|
|
4504
4540
|
function configure(next) {
|
|
4505
4541
|
const modeChanged = next.mode !== void 0 && next.mode !== policy.mode;
|
|
4506
4542
|
if (next.mode !== void 0) policy.mode = next.mode;
|
|
@@ -4525,6 +4561,7 @@ function createSessionCleaner(options = {}) {
|
|
|
4525
4561
|
}
|
|
4526
4562
|
return {
|
|
4527
4563
|
schedule,
|
|
4564
|
+
discard,
|
|
4528
4565
|
flush,
|
|
4529
4566
|
pendingCount: () => queue.length,
|
|
4530
4567
|
policy: () => ({ ...policy }),
|
|
@@ -4549,6 +4586,13 @@ function scheduleDeleteSession(auth, sessionId) {
|
|
|
4549
4586
|
defaultCleaner.schedule(auth, sessionId);
|
|
4550
4587
|
}
|
|
4551
4588
|
/**
|
|
4589
|
+
* 丢掉一个**脚手架会话**(内部请求用的那个)—— 没有注入清理器时的兜底路径。
|
|
4590
|
+
* 语义与 `SessionCleaner.discard` 完全一致(不受 keep 影响)。
|
|
4591
|
+
*/
|
|
4592
|
+
function discardSession(auth, sessionId) {
|
|
4593
|
+
defaultCleaner.discard(auth, sessionId);
|
|
4594
|
+
}
|
|
4595
|
+
/**
|
|
4552
4596
|
* 「立即清理」用:把**当前在用的**网页端会话连同投喂链一起退掉,返回被退的会话 id。
|
|
4553
4597
|
*
|
|
4554
4598
|
* 为什么要有它:链式模式下不自动清理(会话就是链的载体),但用户总得有个"我现在就要
|
|
@@ -5425,6 +5469,7 @@ async function openCompletion(auth, params, signal, transport, keepIds) {
|
|
|
5425
5469
|
...params.promptParts ? {
|
|
5426
5470
|
head: params.promptParts.head,
|
|
5427
5471
|
entries: params.promptParts.entries,
|
|
5472
|
+
...typeof params.promptParts.transcript === "string" ? { transcript: params.promptParts.transcript } : {},
|
|
5428
5473
|
...params.promptParts.maxChars !== void 0 ? { maxChars: params.promptParts.maxChars } : {}
|
|
5429
5474
|
} : {},
|
|
5430
5475
|
full: params.prompt,
|
|
@@ -5454,6 +5499,8 @@ async function openCompletion(auth, params, signal, transport, keepIds) {
|
|
|
5454
5499
|
account: accountKey(auth),
|
|
5455
5500
|
chainLen: chainEntries ? chainEntries.length : null,
|
|
5456
5501
|
entriesLen: currentEntries ? currentEntries.length : null,
|
|
5502
|
+
promptChars: feed.prompt.length,
|
|
5503
|
+
headChars: params.promptParts ? String(params.promptParts.head ?? "").length : null,
|
|
5457
5504
|
tailSame: !chainEntries || !currentEntries ? null : currentEntries.length <= chainEntries.length ? false : currentEntries[chainEntries.length - 1] === chainEntries[chainEntries.length - 1]
|
|
5458
5505
|
});
|
|
5459
5506
|
}
|
|
@@ -5747,7 +5794,14 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
|
|
|
5747
5794
|
finalized = true;
|
|
5748
5795
|
const roundOk = complete || sawTerminal;
|
|
5749
5796
|
lastRetireReason = void 0;
|
|
5750
|
-
|
|
5797
|
+
if (params.promptParts === void 0) {
|
|
5798
|
+
const scaffolding = new Set(owned);
|
|
5799
|
+
if (sessionId) scaffolding.add(sessionId);
|
|
5800
|
+
for (const id of scaffolding) {
|
|
5801
|
+
retireSession(id);
|
|
5802
|
+
params.onDiscardSession?.(id);
|
|
5803
|
+
}
|
|
5804
|
+
} else for (const id of owned) {
|
|
5751
5805
|
if (limit > 0 && keepIds.has(id) && !poisoned) continue;
|
|
5752
5806
|
if (id === sessionId && roundOk && !poisoned && limit > 0) continue;
|
|
5753
5807
|
lastRetireReason = id !== sessionId ? "extra-session" : poisoned ? "poisoned" : !roundOk ? "not-finished" : "rotated";
|
|
@@ -6902,6 +6956,7 @@ function createAdapter(deps) {
|
|
|
6902
6956
|
...chatLike ? { promptParts: {
|
|
6903
6957
|
head: promptParts.head,
|
|
6904
6958
|
entries: promptParts.entries,
|
|
6959
|
+
transcript: promptParts.transcript,
|
|
6905
6960
|
maxChars: deps.config.maxPromptChars ?? 4e5
|
|
6906
6961
|
} } : {},
|
|
6907
6962
|
onContextFeed: (report) => {
|
|
@@ -6918,6 +6973,10 @@ function createAdapter(deps) {
|
|
|
6918
6973
|
onDeleteSession: deps.config.deleteWebSessions === false ? void 0 : (sessionId) => {
|
|
6919
6974
|
if (deps.sessionCleaner) deps.sessionCleaner.schedule(auth, sessionId);
|
|
6920
6975
|
else scheduleDeleteSession(auth, sessionId);
|
|
6976
|
+
},
|
|
6977
|
+
onDiscardSession: deps.config.deleteWebSessions === false ? void 0 : (sessionId) => {
|
|
6978
|
+
if (deps.sessionCleaner) deps.sessionCleaner.discard(auth, sessionId);
|
|
6979
|
+
else discardSession(auth, sessionId);
|
|
6921
6980
|
}
|
|
6922
6981
|
})) {
|
|
6923
6982
|
if (event.kind === "thinking" || event.kind === "text") roundUsage.outputChars += event.text.length;
|
|
@@ -10312,7 +10371,7 @@ async function checkForUpdate(current, fetchImpl) {
|
|
|
10312
10371
|
* 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
|
|
10313
10372
|
*/
|
|
10314
10373
|
/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
|
|
10315
|
-
const FALLBACK_VERSION = "0.6.
|
|
10374
|
+
const FALLBACK_VERSION = "0.6.32";
|
|
10316
10375
|
let cached;
|
|
10317
10376
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
10318
10377
|
function pluginVersion() {
|