dsh-deepseek-web-login 0.6.22 → 0.6.23
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 +43 -0
- package/lib/index.js +80 -22
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
本项目大致遵循语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.23 — 2026-10-01
|
|
6
|
+
|
|
7
|
+
**一个窗口 = 一条对话线:不再断链、不再分叉、不再删会话。**
|
|
8
|
+
|
|
9
|
+
0.6.22 只堵住了"每轮换新会话",但**链仍然每轮断**——`parent_message_id` 一变成 `null`,
|
|
10
|
+
网页端就把那条渲染成**同层消息**(「修改 / 重新生成」+ `n / n` 翻页)。这次治的是断链本身。
|
|
11
|
+
|
|
12
|
+
### ① 续链判据:从"严格前缀"改成"容得下运行时注入被原地替换"
|
|
13
|
+
|
|
14
|
+
旧判据要求 `prev[i] === next[i]` 对全部 i 成立;而 DSH **每一轮**都会重写它自己注入的那条
|
|
15
|
+
运行时快照(原话:`Current runtime context. This snapshot supersedes earlier runtime-context
|
|
16
|
+
snapshots.`)——**替换式**,位置不变、内容每轮变。于是链几乎每轮都作废。
|
|
17
|
+
|
|
18
|
+
新判据 `canExtendChain` 两条:
|
|
19
|
+
1. 本轮条目必须**真的变长**(没变长说不清是重发还是历史被截断,保守退回);
|
|
20
|
+
2. **链尾仍在原位**(`next[prev.length-1] === prev[prev.length-1]`)——同时挡住
|
|
21
|
+
"整体重写"和"中途插入/删除"(那会让增量切错位置)。
|
|
22
|
+
满足时中间差异只可能是原位替换,`entries.slice(prev.length)` 切出来的正好是真正的新内容。
|
|
23
|
+
|
|
24
|
+
### ② 退回全量时**不再发根消息**(这是"分叉"的直接来源)
|
|
25
|
+
|
|
26
|
+
`decideFeed` 里把收尾拆成两条路:
|
|
27
|
+
- `detach`(`parent: null`)——**只有真的没有链**时才走:新会话 / 换了 DSH 会话 / 换了账号;
|
|
28
|
+
- `replay`(`parent:` 链尾)——头部变了、历史被改写、增量空/超预算等,**重发全量但仍挂链尾**。
|
|
29
|
+
|
|
30
|
+
代价只是服务端上下文里多一段重复历史,而分叉是**结构性**的坏。
|
|
31
|
+
|
|
32
|
+
### ③ 槽位淘汰不再删网页端会话
|
|
33
|
+
|
|
34
|
+
超限时旧实现 `retireSession + cleanup`(= 排队 DELETE)。现在只清内存槽与链:
|
|
35
|
+
会话留着,回到那个窗口还能接着用;而且"删会话"本身是最强的机器特征之一。
|
|
36
|
+
|
|
37
|
+
### ④ 顺带修正:投喂回执里的 `chained` 改按 `reason` 判
|
|
38
|
+
|
|
39
|
+
旧写法 `chained: feed.parentMessageId !== null`,而 ② 之后"重发全量"也挂在链尾 ⇒
|
|
40
|
+
会把"这一轮其实重发了全量"报成 `chained=true`,日志直接说谎。
|
|
41
|
+
|
|
42
|
+
### 验证
|
|
43
|
+
|
|
44
|
+
- `tsc --noEmit`、`npm run test`(60/60)、`check-smoke`、`check-bundle` 全通过
|
|
45
|
+
- 新增 4 条用例(2 条 ★ 在 `decideFeed` 层、1 条 ★ 端到端、1 条 ★ 槽位淘汰)
|
|
46
|
+
- 三条修复各自**变异反向验证**:改回旧实现 ⇒ 对应用例变红、`rc=1`
|
|
47
|
+
|
|
5
48
|
## 0.6.22 — 2026-10-01
|
|
6
49
|
|
|
7
50
|
**修复:一个窗口聊两句、网页端多出三个会话;以及选「不删」反而触发一次批量删除。**
|
package/lib/index.js
CHANGED
|
@@ -3426,17 +3426,43 @@ function effectiveReuseLimit(maxTurns, mode) {
|
|
|
3426
3426
|
if (!(maxTurns > 0)) return maxTurns;
|
|
3427
3427
|
return mode === "chained" ? Number.POSITIVE_INFINITY : maxTurns;
|
|
3428
3428
|
}
|
|
3429
|
-
/**
|
|
3430
|
-
|
|
3429
|
+
/**
|
|
3430
|
+
* 本轮能不能接着链往下发(替代旧的"严格前缀"判据,2026-10-01)。
|
|
3431
|
+
*
|
|
3432
|
+
* ## 旧判据为什么会毁掉整个链式投喂
|
|
3433
|
+
*
|
|
3434
|
+
* 旧实现要求**严格前缀**:`prev[i] === next[i]` 对全部 i 成立。只要有**一条**被改写,
|
|
3435
|
+
* 整条链就作废 ⇒ `parent=null` ⇒ 网页端出现同层根消息(「修改 / 重新生成」+ `n / n`),
|
|
3436
|
+
* 或者(0.6.10~0.6.21)直接换一个新会话。
|
|
3437
|
+
*
|
|
3438
|
+
* 而实测 DSH **每一轮**都会重写它自己注入的那条运行时快照,原话就在会话日志里:
|
|
3439
|
+
* `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.`
|
|
3440
|
+
* —— 它是**替换式**的(位置不变、内容每轮变),所以"严格前缀"在 DSH 下几乎每轮都失败。
|
|
3441
|
+
*
|
|
3442
|
+
* ## 新判据(够严,但不至于被一条注入打穿)
|
|
3443
|
+
*
|
|
3444
|
+
* 1. **必须真的变长** —— 长度没增长时说不清是"重发"还是"历史被截断/压缩",保守退回;
|
|
3445
|
+
* 2. **链尾仍在原位**(`next[prev.length-1] === prev[prev.length-1]`)——
|
|
3446
|
+
* 这条同时挡住了两种真正危险的情况:整体重写、以及在中途**插入/删除**条目
|
|
3447
|
+
* (那会让增量切错位置、漏掉内容)。
|
|
3448
|
+
*
|
|
3449
|
+
* 满足这两条时,中间的差异只可能是**原位替换**(就是那条运行时快照),
|
|
3450
|
+
* 而增量按 `next.slice(prev.length)` 切出来的正好是**真正的新内容** —— 长度对齐,
|
|
3451
|
+
* 被替换的那条不会挤进增量里。
|
|
3452
|
+
*/
|
|
3453
|
+
function canExtendChain(prev, next) {
|
|
3431
3454
|
if (next.length <= prev.length) return false;
|
|
3432
|
-
|
|
3433
|
-
return true;
|
|
3455
|
+
return next[prev.length - 1] === prev[prev.length - 1];
|
|
3434
3456
|
}
|
|
3435
3457
|
/**
|
|
3436
3458
|
* 决定本轮发什么。**纯函数**:不读文件、不看时间、不改全局状态。
|
|
3437
3459
|
*
|
|
3438
|
-
*
|
|
3439
|
-
*
|
|
3460
|
+
* 判据宁可保守:只要能续链就发增量,任何一处不确定就**重发全量**。
|
|
3461
|
+
*
|
|
3462
|
+
* ⚠️ 但"重发全量"**不等于**发根消息(0.6.23):只要链还在(同一个网页端会话、同一个账号),
|
|
3463
|
+
* 重发的那条仍然挂在**链尾**(见 `replay`)—— 发 `parent=null` 会让网页端把它渲染成
|
|
3464
|
+
* 同层的另一条消息(「修改 / 重新生成」+ `n / n`),也就是"聊着聊着分叉了"。
|
|
3465
|
+
* 只有**真的没有链**时(新会话 / 换了会话 / 换了账号)才发根消息(见 `detach`)。
|
|
3440
3466
|
*/
|
|
3441
3467
|
function decideFeed(input) {
|
|
3442
3468
|
const full = input.full;
|
|
@@ -3454,7 +3480,8 @@ function decideFeed(input) {
|
|
|
3454
3480
|
next: void 0,
|
|
3455
3481
|
reason: "no-parts"
|
|
3456
3482
|
};
|
|
3457
|
-
|
|
3483
|
+
/** 没有可用的链(= 这个网页端会话的第一条消息)⇒ 只能发根消息,这是唯一合法的情况。 */
|
|
3484
|
+
const detach = (reason) => ({
|
|
3458
3485
|
prompt: full,
|
|
3459
3486
|
parentMessageId: null,
|
|
3460
3487
|
next: {
|
|
@@ -3465,20 +3492,40 @@ function decideFeed(input) {
|
|
|
3465
3492
|
},
|
|
3466
3493
|
reason
|
|
3467
3494
|
});
|
|
3468
|
-
|
|
3495
|
+
/**
|
|
3496
|
+
* 「链还在,但这一轮不能只发增量」⇒ 重发全量,但仍**挂在链尾**。
|
|
3497
|
+
*
|
|
3498
|
+
* 🔴 这是 2026-10-01 用户明确要求的取舍:**宁可多发一次,也绝不发根消息**。
|
|
3499
|
+
* 旧实现这里一律 `parentMessageId: null` —— 网页端会把这条渲染成**同层的另一条消息**
|
|
3500
|
+
* (「修改 / 重新生成」+ `n / n` 翻页),用户看到的就是"聊着聊着分叉了"。
|
|
3501
|
+
* 挂在链尾的代价只是服务端上下文里多一段重复历史(多花一点 token),
|
|
3502
|
+
* 而分叉是**结构性**的坏:它会永久破坏"一个窗口一条对话线"这个形态。
|
|
3503
|
+
*/
|
|
3504
|
+
const replay = (reason) => ({
|
|
3505
|
+
prompt: full,
|
|
3506
|
+
parentMessageId: chain.parentId,
|
|
3507
|
+
next: {
|
|
3508
|
+
head,
|
|
3509
|
+
entries: entries.slice(),
|
|
3510
|
+
sessionId: input.sessionId,
|
|
3511
|
+
accountKey: input.accountKey
|
|
3512
|
+
},
|
|
3513
|
+
reason
|
|
3514
|
+
});
|
|
3515
|
+
if (!input.reused) return detach("new-session");
|
|
3469
3516
|
const chain = input.chain;
|
|
3470
|
-
if (!chain) return
|
|
3471
|
-
if (chain.sessionId !== input.sessionId) return
|
|
3472
|
-
if (chain.accountKey !== input.accountKey) return
|
|
3473
|
-
if (chain.head !== head) return
|
|
3474
|
-
if (!
|
|
3517
|
+
if (!chain) return detach("no-chain");
|
|
3518
|
+
if (chain.sessionId !== input.sessionId) return detach("session-changed");
|
|
3519
|
+
if (chain.accountKey !== input.accountKey) return detach("account-changed");
|
|
3520
|
+
if (chain.head !== head) return replay("head-changed");
|
|
3521
|
+
if (!canExtendChain(chain.entries, entries)) return replay("not-appended");
|
|
3475
3522
|
const appended = entries.slice(chain.entries.length);
|
|
3476
3523
|
const meaningful = appended.some((line) => isContinuationCue(line)) ? appended : appended.filter((line) => !isAssistantTranscriptEntry(line));
|
|
3477
3524
|
const echoDropped = meaningful.length > 0 ? appended.length - meaningful.length : 0;
|
|
3478
3525
|
const delta = (meaningful.length > 0 ? meaningful : appended).join("\n\n");
|
|
3479
|
-
if (delta.trim().length === 0) return
|
|
3526
|
+
if (delta.trim().length === 0) return replay("empty-delta");
|
|
3480
3527
|
const cap = input.maxChars;
|
|
3481
|
-
if (typeof cap === "number" && Number.isFinite(cap) && cap > 0 && delta.length > cap) return
|
|
3528
|
+
if (typeof cap === "number" && Number.isFinite(cap) && cap > 0 && delta.length > cap) return replay("delta-too-long");
|
|
3482
3529
|
return {
|
|
3483
3530
|
prompt: delta,
|
|
3484
3531
|
parentMessageId: chain.parentId,
|
|
@@ -5143,16 +5190,27 @@ async function leaseSession(auth, signal, transport, maxTurns, cleanup, forceNew
|
|
|
5143
5190
|
reused: false
|
|
5144
5191
|
};
|
|
5145
5192
|
}
|
|
5146
|
-
/**
|
|
5193
|
+
/**
|
|
5194
|
+
* 槽数超过上限时,把**最久没用**的那些从内存里淘汰掉。
|
|
5195
|
+
*
|
|
5196
|
+
* 🔴 0.6.23:这里**不再删网页端会话**(旧实现做 `retireSession` + `cleanup`,也就是排队 DELETE)。
|
|
5197
|
+
*
|
|
5198
|
+
* 两个理由,都是用户明确提的:
|
|
5199
|
+
* 1. **"回到原来的窗口还要能接着聊"** —— 把会话删了,等于把他那个窗口的上下文扔了;
|
|
5200
|
+
* 他只能得到一个空白的新会话。槽是内存态、淘汰本来就不该有"销毁用户数据"的副作用。
|
|
5201
|
+
* 2. **"删"本身是最强的机器特征之一**:真人不会每开一个新窗口就顺手删掉一个旧对话。
|
|
5202
|
+
* 网页端留着的会话由用户自己管理(面板有「立即清理」,设置里也有清理策略)。
|
|
5203
|
+
*
|
|
5204
|
+
* 链一并清掉:槽没了的窗口下一轮会拿到**新会话**(`reused=false` ⇒ `detach('new-session')`),
|
|
5205
|
+
* 旧链再留着只会指向一个这个窗口已经不用的会话。
|
|
5206
|
+
*/
|
|
5147
5207
|
function evictIdleSlots() {
|
|
5148
5208
|
if (reuseSlots.size <= 6) return;
|
|
5149
5209
|
const idle = [...reuseSlots.values()].sort((a, b) => a.at - b.at);
|
|
5150
5210
|
for (const slot of idle.slice(0, reuseSlots.size - 6)) {
|
|
5151
5211
|
reuseSlots.delete(slot.key);
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
slot.cleanup?.(slot.sessionId);
|
|
5155
|
-
} catch {}
|
|
5212
|
+
contextChains.delete(slot.key);
|
|
5213
|
+
if (lastChainKey === slot.key) lastChainKey = void 0;
|
|
5156
5214
|
}
|
|
5157
5215
|
}
|
|
5158
5216
|
/** 把某个会话从复用槽里摘掉(会话失效 / 请求失败时调用,下次会新建)。 */
|
|
@@ -5254,7 +5312,7 @@ async function openCompletion(auth, params, signal, transport) {
|
|
|
5254
5312
|
lastFeedReason = feed.reason;
|
|
5255
5313
|
params.onContextFeed?.({
|
|
5256
5314
|
reason: feed.reason,
|
|
5257
|
-
chained: feed.
|
|
5315
|
+
chained: feed.reason === "chained",
|
|
5258
5316
|
promptChars: feed.prompt.length,
|
|
5259
5317
|
echoDropped: feed.echoDropped ?? 0
|
|
5260
5318
|
});
|
|
@@ -9952,7 +10010,7 @@ async function checkForUpdate(current, fetchImpl) {
|
|
|
9952
10010
|
* 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
|
|
9953
10011
|
*/
|
|
9954
10012
|
/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
|
|
9955
|
-
const FALLBACK_VERSION = "0.6.
|
|
10013
|
+
const FALLBACK_VERSION = "0.6.23";
|
|
9956
10014
|
let cached;
|
|
9957
10015
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
9958
10016
|
function pluginVersion() {
|