dsh-deepseek-web-login 0.6.30 → 0.6.31

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 CHANGED
@@ -2,6 +2,55 @@
2
2
 
3
3
  本项目大致遵循语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.31 — 2026-10-02
6
+
7
+ **修复:新建一个窗口、只发一句话,网页端会多出一个会话(那个是"生成标题"用的脚手架)。**
8
+
9
+ 现场(用户原话):「刚在 dsh 中新建一个窗口聊天,就发了一句话,网页版直接俩窗口」。
10
+ `feed-decisions.jsonl` 里两条对得整整齐齐:
11
+
12
+ ```
13
+ 12:55:54 new-session sess=04c63135 ← 对话那条
14
+ 12:56:00 no-parts sess=de6a8d4f ← 网页端多出来的那个
15
+ ```
16
+
17
+ `de6a8d4f` 正是用户在网页端看到的那条(标题请求的 prompt 直接显示在里面)。
18
+
19
+ **根因**:`session-title` 这类内部请求**必须有一条自己的网页端会话**才能调 completion
20
+ (0.6.26 为了修「`n/n` 分叉」特意把它们和对话分开,见 `requestSlotKey` 的注释)。
21
+ 当时的注释写的是「代价是内部请求自己占一个会话,**它不出现在对话里**」——
22
+ **判断漏了一半**:它确实不在*对话*里,但它**出现在侧边栏里**。
23
+
24
+ 而它**从来不会被删**:用户设的是 `sessionCleanup: keep`,而清理器的 `schedule()` 在 keep 下
25
+ 第一行就 `return` —— 于是这条会话**连 ledger 都没有**(从没安排过删除),永远留在网页端。
26
+
27
+ **改法**:把「内部请求的会话」与「用户的对话会话」在**收尾时彻底分开**。
28
+
29
+ - 收尾时按 `promptParts` 有没有传来分(与 `requestSlotKey`、adapter 的 `chatLike` 同一处判据):
30
+ **不带** = 内部请求 ⇒ 会话是**脚手架**,一律走新的 `onDiscardSession` 通道丢掉,
31
+ **不进用户的清理队列**。
32
+ - ⚠️ **复用来的会话也要丢** —— 它不在收尾逻辑的 `owned` 集合里(那一轮没调 `createSession`),
33
+ 这正是最容易漏掉的一半。
34
+ - 清理器新增 `discard(auth, sessionId)`:**不受 `keep` / `manualOnly` 影响** ——
35
+ 那两道开关的语义是「别删**我的对话**」,而脚手架会话不是用户的对话。
36
+ - ⚠️ `discard` **只删那一个**,**不 drain 队列**:`manualOnly`(链式模式)下队列里攒的是
37
+ **用户的**会话,借这次机会顺手删掉就变成"用户没点按钮却被删了"(0.6.1x 修过的那个 bug)。
38
+ - ⚠️ 仍然受 `deleteWebSessions === false`("一个都不许删"总闸)约束 —— 那个开关不能破。
39
+ 所以**关掉总闸时这类会话仍会留在网页端**,这是已知代价。
40
+
41
+ **顺带把一条"注意事项"升级成硬约束:测试 fixture 模拟真实 chat 时必须传 `promptParts`。**
42
+ 改了收尾判据之后,4 个用例文件成批变红 —— 全是拿裸 `params`(不带 `promptParts`)去测
43
+ "chat 会话的轮换 / 回收 / 归属"的。这不是 bug,是夹具没按生产链路的形态构造
44
+ (adapter 只给 `chatLike` 传 `promptParts`)。已给 6 处夹具补上,并在文件里写清为什么。
45
+
46
+ **新增 6 条用例**(`check-internal-session-discard.mjs`):内部请求走「丢掉」通道且不进用户队列 /
47
+ 复用的会话也要丢 / 对话请求**不**走丢弃通道(防过度清理)/ `keep` 与 `manualOnly` 下都必须删 /
48
+ `discard` 不许顺手清空用户的队列。**三处变异确认变红**:关掉内部请求分支(2 红)、
49
+ 把 `discard` 改成 drain 队列(2 红)、以及重建后的产物断言。
50
+
51
+ > ⚠️ 又记一条**产物断言的坑**:打包器除了会去掉单语句的花括号,还会把 `undefined` 改写成 `void 0`
52
+ > —— 断言里写 `=== undefined` 必然假红(这次踩了)。
53
+
5
54
  ## 0.6.30 — 2026-10-02
6
55
 
7
56
  **补齐请求头:把浏览器**自动加**的那批指纹头收回来,并修掉一个过时的写死版本号。**
package/lib/index.js CHANGED
@@ -4501,6 +4501,30 @@ function createSessionCleaner(options = {}) {
4501
4501
  sessionId: item.sessionId
4502
4502
  });
4503
4503
  }
4504
+ /**
4505
+ * 立刻丢掉**一个我们自己的脚手架会话**(内部请求用的那个),**不受清理策略影响**。
4506
+ *
4507
+ * 为什么要单独一条路:`keep`(用户选的「不删」)与 `manualOnly`(链式模式下攒着等手动清)
4508
+ * 都让 `schedule()` 变成空操作 —— 那对**用户的对话**是对的,对**我们自己建的会话**是错的。
4509
+ * 内部请求(`session-title` / 压缩之类)必须有一条会话才能调 completion,而那条会话
4510
+ * 既不是用户建的、用户也不需要它 —— 留在网页端就是**凭空多出来的一个会话**。
4511
+ *
4512
+ * 🔴 2026-10-02 用户现场:「刚在 dsh 中新建一个窗口聊天,就发了一句话,网页版直接俩窗口」。
4513
+ * `feed-decisions.jsonl` 里对上了:12:55:54 `new-session`(对话,`04c63135`)+
4514
+ * 12:56:00 `no-parts`(内部请求,`de6a8d4f`)—— 后者正是网页端多出来的那一个,
4515
+ * 而且它**连 ledger 都没有**(keep 模式下 `schedule()` 直接 return,从没安排过删除)。
4516
+ *
4517
+ * ⚠️ 只删**这一个**,**不去 drain 队列**:`manualOnly` 下队列里攒的是用户的会话,
4518
+ * 借这次机会顺手把它们删掉,就变成"用户没点按钮却被删了"(0.6.1x 修过的那个 bug)。
4519
+ * ⚠️ 仍然受 `deleteWebSessions === false`(总闸)约束 —— 那个开关的语义是"一个都不许删",
4520
+ * 接线在 adapter.ts。所以**关掉总闸时这类会话仍会留在网页端**,这是已知代价。
4521
+ */
4522
+ function discard(auth, sessionId) {
4523
+ return deleteChunk([{
4524
+ auth,
4525
+ sessionId
4526
+ }]);
4527
+ }
4504
4528
  function configure(next) {
4505
4529
  const modeChanged = next.mode !== void 0 && next.mode !== policy.mode;
4506
4530
  if (next.mode !== void 0) policy.mode = next.mode;
@@ -4525,6 +4549,7 @@ function createSessionCleaner(options = {}) {
4525
4549
  }
4526
4550
  return {
4527
4551
  schedule,
4552
+ discard,
4528
4553
  flush,
4529
4554
  pendingCount: () => queue.length,
4530
4555
  policy: () => ({ ...policy }),
@@ -4549,6 +4574,13 @@ function scheduleDeleteSession(auth, sessionId) {
4549
4574
  defaultCleaner.schedule(auth, sessionId);
4550
4575
  }
4551
4576
  /**
4577
+ * 丢掉一个**脚手架会话**(内部请求用的那个)—— 没有注入清理器时的兜底路径。
4578
+ * 语义与 `SessionCleaner.discard` 完全一致(不受 keep 影响)。
4579
+ */
4580
+ function discardSession(auth, sessionId) {
4581
+ defaultCleaner.discard(auth, sessionId);
4582
+ }
4583
+ /**
4552
4584
  * 「立即清理」用:把**当前在用的**网页端会话连同投喂链一起退掉,返回被退的会话 id。
4553
4585
  *
4554
4586
  * 为什么要有它:链式模式下不自动清理(会话就是链的载体),但用户总得有个"我现在就要
@@ -5747,7 +5779,14 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
5747
5779
  finalized = true;
5748
5780
  const roundOk = complete || sawTerminal;
5749
5781
  lastRetireReason = void 0;
5750
- for (const id of owned) {
5782
+ if (params.promptParts === void 0) {
5783
+ const scaffolding = new Set(owned);
5784
+ if (sessionId) scaffolding.add(sessionId);
5785
+ for (const id of scaffolding) {
5786
+ retireSession(id);
5787
+ params.onDiscardSession?.(id);
5788
+ }
5789
+ } else for (const id of owned) {
5751
5790
  if (limit > 0 && keepIds.has(id) && !poisoned) continue;
5752
5791
  if (id === sessionId && roundOk && !poisoned && limit > 0) continue;
5753
5792
  lastRetireReason = id !== sessionId ? "extra-session" : poisoned ? "poisoned" : !roundOk ? "not-finished" : "rotated";
@@ -6918,6 +6957,10 @@ function createAdapter(deps) {
6918
6957
  onDeleteSession: deps.config.deleteWebSessions === false ? void 0 : (sessionId) => {
6919
6958
  if (deps.sessionCleaner) deps.sessionCleaner.schedule(auth, sessionId);
6920
6959
  else scheduleDeleteSession(auth, sessionId);
6960
+ },
6961
+ onDiscardSession: deps.config.deleteWebSessions === false ? void 0 : (sessionId) => {
6962
+ if (deps.sessionCleaner) deps.sessionCleaner.discard(auth, sessionId);
6963
+ else discardSession(auth, sessionId);
6921
6964
  }
6922
6965
  })) {
6923
6966
  if (event.kind === "thinking" || event.kind === "text") roundUsage.outputChars += event.text.length;
@@ -10312,7 +10355,7 @@ async function checkForUpdate(current, fetchImpl) {
10312
10355
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
10313
10356
  */
10314
10357
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
10315
- const FALLBACK_VERSION = "0.6.30";
10358
+ const FALLBACK_VERSION = "0.6.31";
10316
10359
  let cached;
10317
10360
  /** 本插件版本(如 `0.1.26`)。 */
10318
10361
  function pluginVersion() {