dsh-deepseek-web-login 0.7.1 → 0.7.2

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,94 @@
2
2
 
3
3
  本项目大致遵循语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.7.2 — 2026-10-10
6
+
7
+ **修好外部审查报出的 8 条缺陷**(P1×3 / P2×4 / P3×1)。审查给了行号、复现脚本与置信度标注,
8
+ 7 个复现脚本全部实跑复现、无一条夸大。**其中两处修法不能照抄**,详见下文。
9
+
10
+ ### P1-1「到期前自动重登」开关无法落盘 ⇒ 功能一次都不会跑
11
+
12
+ `gate.ts` 的 `settings()` 返回对象与 `configure()` 都漏了 `autoRelogin`,而保存链是
13
+ `configure(patch)` → `applied = settings()` → `writeGateSettings(applied)`,
14
+ 后者是**整对象覆盖写、不合并** ⇒ 面板开关存不下去,定时检查永不触发。
15
+ 改动其它设置也会顺手把它抹掉。
16
+
17
+ ⚠️ **审查给的修法不能照抄**:它在 `configure()` 里写 `autoRelogin = next.autoRelogin`,
18
+ 但**没有对应的模块级变量**(`autoRelogin` 原本是无状态字段,`index.ts:680` 每轮
19
+ `readGateSettings()` 重读文件)⇒ 照抄会 ReferenceError。实测踩过。
20
+ 正解是给它内存态,并让 `settings()` **真的返回一个键**。
21
+
22
+ ### P1-2 启动扫尾误删「已排队但尚未确认删除」的记录
23
+
24
+ `session-journal.ts` 的 `writeJournal(plan.kept)` 漏了 `plan.toDelete`,与模块自身文档
25
+ ("记录要留到确认删掉")直接冲突。触发条件是组合式的:同一批里**同时**有孤儿记录
26
+ (`dropped` 非空)与已排队记录 ⇒ 后者被一并抹掉;若进程在删除回执到达前又被强杀,
27
+ 就**永远补删不到**。修法:`[...plan.kept, ...plan.toDelete]`。
28
+
29
+ ### P1-3 `salvageXmlToolCalls` 的 `bodyStart` 偏移算错
30
+
31
+ `attrs` 是捕获组 `([^>]*)`,**不含** `TAG_OPEN_PREFIX` 与结尾的 `>`,用
32
+ `index + attrs.length` 会从开标签内部开始切 ⇒ 参数变成
33
+ `{"_raw":"=\"read\">{...}"}`。改用 `invokeStartRe.lastIndex`。
34
+
35
+ ### P2-4 内部请求抛错时同一会话被丢弃两次
36
+
37
+ `catch` 与 `finally` 两处做完全相同的事(同一判据 / 同一集合 / 同一动作),而 JS 语义下
38
+ `catch` 里 `throw` 之后 `finally` **仍会执行** ⇒ 两个可避免的 DELETE(与"降低请求密度"
39
+ 的目标冲突)+ `scaffolding-discorders.jsonl` 多写一条假告警。删掉 `catch` 那份。
40
+
41
+ 🔴 连带修了一条**过时的守卫**:`check-bundle` 原判据是"扫 `catch` 块里那段清理",
42
+ **实现修好了它反而会红**(判据断的是源码位置,不是行为)。已改成守 `finally` 那层,
43
+ 并新增一条反向守卫(catch 区间里**不许**再有那段)。变异实测:放回 catch 立刻红。
44
+
45
+ ### P2-5 `sawCallFence` 单向闩锁 ⇒ 吃掉回答结尾的代码块闭栏
46
+
47
+ `grep sawCallFence\s*=` 只有 3 处、全是 `= true`,**没有任何复位点** ⇒ 调用之后用户回答里的
48
+ 任意尾随闭栏都被 `CALL_FENCE_TAIL_RE` 剥掉(回答以代码块结尾时闭栏消失)。
49
+
50
+ ⚠️ **光复位不够**:`CALL_FENCE_TAIL_RE`(`protocol.ts:967`)匹配的是**任意**尾部闭栏
51
+ (含 ` ```python ` 这种带语言名的)。"闭栏一定是配对的另一半"这个论断**只对紧邻调用块的
52
+ 那一次**成立。所以除了复位,还加了内容判据:pending 里若还有未闭合的用户代码块开栏,
53
+ 这个闭栏就不是残留,放它出去。
54
+
55
+ ### P2-6 空 `response/fragments` 帧清掉已建立的 thinking 通道
56
+
57
+ `appendFragments` 在**没追加任何 fragment** 时也改写 `sink`,把
58
+ `response/thinking_content` 建立的 `sink='thinking'` 清成 `null` ⇒ 紧接着的裸续段被当成
59
+ 正文上屏(同文件的 `fragments/-1/content` 分支已有守卫,漏了这道)。已对齐。
60
+
61
+ ### P2-7 `unwrapDsmlArguments` 字符串分支缺多键保护
62
+
63
+ 函数头注释声明"只剥只有一个 `arguments` 键的情况",对象分支实现了,字符串分支
64
+ 无条件 `return reparsed` ⇒ 静默丢参数。把 `Object.keys(args).length === 1` 提到两条分支共用。
65
+
66
+ ### P3 `credentials.json` 落盘权限比账号文件更松(仅 POSIX)
67
+
68
+ 装的是**邮箱 + 明文密码**(敏感度高于 token 文件),却用裸 `writeFileSync`(按 umask 落地,
69
+ 通常 0644)。复用 `accounts.ts` 的 `writeJsonAtomic`(创建即 0600 + rename 失败清理)。
70
+
71
+ ### 三处"假绿灯"也已堵上
72
+
73
+ 新增 `tests/check-external-review-fixes.mjs`(8 条**行为**断言,不是 grep 源码):
74
+
75
+ | 原问题 | 处置 |
76
+ |---|---|
77
+ | `check-relogin.mjs:164` 用正则断言"这行代码在" | 改成走真实配置链(`configure` → `settings` → `writeGateSettings` → `readGateSettings`) |
78
+ | `check-session-journal.mjs` 两条各覆盖一半,**交集**没测 | 新增 dropped 与 toDelete 同时非空的场景 |
79
+ | `ToolCallStreamFilter` 没有"围栏调用 + 回答含代码块"的端到端文本断言 | 新增围栏计数断言 |
80
+
81
+ ⚠️ **变异验证**(这是本项目的既有纪律):8 条里抽 5 条做变异,把修复还原 ⇒ 对应用例立刻红。
82
+ 其中 P2-6/P2-7 一次变异命中两条。
83
+
84
+ ### 验证
85
+
86
+ `tsc --noEmit` / `build` / `check-bundle` / `check-test-isolation` / 逻辑测试 81 /
87
+ 新守卫 8 / **67/67** 全过。
88
+
89
+ ⚠️ 一个测试方法上的教训(记在这里以免再犯):本机 `shell` 里写文件再 `cp` 还原的
90
+ "变异 → 跑守卫"会出现**假阴性**(守卫其实抓得住,但那次跑出来是绿的)。
91
+ 可靠做法是**在同一个进程里做「变异 → execFileSync 跑守卫 → 还原」**,已实测如此。
92
+
5
93
  ## 0.7.1 — 2026-10-09
6
94
 
7
95
  **修好 CI:从 2026-10-02 起一直红的发布链路**(根因是探针里硬编码了本机绝对路径)。
package/lib/index.js CHANGED
@@ -1275,6 +1275,7 @@ function createRequestGate(options = {}) {
1275
1275
  let contextWindow = clampContextWindow(options.contextWindow ?? 1048576);
1276
1276
  let autoSwitchMinutes = clampAutoSwitchMinutes(options.autoSwitchMinutes ?? 0);
1277
1277
  let serialToolCalls = options.serialToolCalls !== false;
1278
+ let autoRelogin = readGateSettings()?.autoRelogin === true;
1278
1279
  let freshSessionOnRestart = options.freshSessionOnRestart === true;
1279
1280
  let cleanupMode = options.sessionCleanup;
1280
1281
  let cleanupBatch = normalizeCleanupRange(options.cleanupBatch, CLEANUP_BATCH_BOUNDS);
@@ -1296,7 +1297,8 @@ function createRequestGate(options = {}) {
1296
1297
  contextWindow,
1297
1298
  autoSwitchMinutes,
1298
1299
  serialToolCalls,
1299
- freshSessionOnRestart
1300
+ freshSessionOnRestart,
1301
+ autoRelogin
1300
1302
  };
1301
1303
  }
1302
1304
  function configure(next) {
@@ -1310,6 +1312,7 @@ function createRequestGate(options = {}) {
1310
1312
  if (next.autoSwitchMinutes !== void 0) autoSwitchMinutes = clampAutoSwitchMinutes(Number(next.autoSwitchMinutes));
1311
1313
  if (typeof next.serialToolCalls === "boolean") serialToolCalls = next.serialToolCalls;
1312
1314
  if (typeof next.freshSessionOnRestart === "boolean") freshSessionOnRestart = next.freshSessionOnRestart;
1315
+ if (typeof next.autoRelogin === "boolean") autoRelogin = next.autoRelogin;
1313
1316
  if (next.cleanupBatch !== void 0) {
1314
1317
  const value = normalizeCleanupRange(next.cleanupBatch, CLEANUP_BATCH_BOUNDS);
1315
1318
  if (value) cleanupBatch = value;
@@ -2524,7 +2527,9 @@ function unwrapDsmlArguments(args) {
2524
2527
  if (wrapped === void 0) return args;
2525
2528
  if (typeof wrapped === "string") {
2526
2529
  const reparsed = parseJsonLenient(wrapped);
2527
- if (reparsed && typeof reparsed === "object" && reparsed !== null && !Array.isArray(reparsed)) return reparsed;
2530
+ if (reparsed && typeof reparsed === "object" && reparsed !== null && !Array.isArray(reparsed)) {
2531
+ if (Object.keys(args).length === 1) return reparsed;
2532
+ }
2528
2533
  return args;
2529
2534
  }
2530
2535
  if (wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)) {
@@ -2613,6 +2618,7 @@ function salvageXmlToolCalls(text) {
2613
2618
  let match;
2614
2619
  while ((match = invokeStartRe.exec(text)) !== null) starts.push({
2615
2620
  index: match.index,
2621
+ end: invokeStartRe.lastIndex,
2616
2622
  attrs: match[1]
2617
2623
  });
2618
2624
  if (starts.length === 0) return null;
@@ -2620,7 +2626,7 @@ function salvageXmlToolCalls(text) {
2620
2626
  for (let i = 0; i < starts.length; i++) {
2621
2627
  const name = readAttr(starts[i].attrs, "name");
2622
2628
  if (!name) continue;
2623
- const bodyStart = starts[i].index + starts[i].attrs.length;
2629
+ const bodyStart = starts[i].end;
2624
2630
  const nextStart = starts[i + 1]?.index ?? text.length;
2625
2631
  const body = text.slice(bodyStart, nextStart);
2626
2632
  calls.push({
@@ -2841,7 +2847,10 @@ var ToolCallStreamFilter = class {
2841
2847
  this.capture = null;
2842
2848
  }
2843
2849
  const tailText = stripStrayToolMarkup(this.pending);
2844
- out.text += this.sawCallFence ? tailText.replace(CALL_FENCE_TAIL_RE, "") : tailText;
2850
+ if (this.sawCallFence) {
2851
+ out.text += tailText.replace(CALL_FENCE_TAIL_RE, "");
2852
+ this.sawCallFence = false;
2853
+ } else out.text += tailText;
2845
2854
  this.pending = "";
2846
2855
  if (this.abandoned) out.rejected = this.abandoned;
2847
2856
  return out;
@@ -2951,10 +2960,15 @@ var ToolCallStreamFilter = class {
2951
2960
  this.pending = this.pending.slice(this.pending.length - hold);
2952
2961
  return;
2953
2962
  }
2954
- if (this.sawCallFence && this.pending.includes("```")) {
2955
- out.text += this.pending.replace(CALL_FENCE_TAIL_RE, "");
2956
- this.pending = "";
2957
- return;
2963
+ if (this.sawCallFence) {
2964
+ const beforeTail = this.pending.replace(CALL_FENCE_TAIL_RE, "");
2965
+ if (!/```[ \t]*(?!dsh-tool\b)[a-zA-Z0-9_-]+[ \t]*\n/.test(beforeTail)) {
2966
+ out.text += this.pending.replace(CALL_FENCE_TAIL_RE, "");
2967
+ this.pending = "";
2968
+ this.sawCallFence = false;
2969
+ return;
2970
+ }
2971
+ this.sawCallFence = false;
2958
2972
  }
2959
2973
  out.text += this.pending;
2960
2974
  this.pending = "";
@@ -6267,7 +6281,7 @@ function createSseState(options = {}) {
6267
6281
  emitText(out, fragment.content);
6268
6282
  }
6269
6283
  }
6270
- sink = fragments.length > 0 ? "fragments" : null;
6284
+ if (!(fragments.length === 0 && (sink === "thinking" || sink === "content"))) sink = fragments.length > 0 ? "fragments" : null;
6271
6285
  };
6272
6286
  /** 增量:续写最后一个 fragment。 */
6273
6287
  const appendToLastFragment = (text, out) => {
@@ -7241,16 +7255,6 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
7241
7255
  yield item.value;
7242
7256
  }
7243
7257
  } catch (error) {
7244
- if (params.promptParts === void 0) {
7245
- const scaffolding = new Set(owned);
7246
- if (sessionId) scaffolding.add(sessionId);
7247
- for (const id of scaffolding) {
7248
- retireSession(id);
7249
- try {
7250
- params.onDiscardSession?.(id);
7251
- } catch {}
7252
- }
7253
- }
7254
7258
  if (params.signal?.aborted) throw new AdapterLlmError("请求已取消", "ABORTED", { cause: error });
7255
7259
  if (controller.signal.aborted && controller.signal.reason instanceof AdapterLlmError) throw controller.signal.reason;
7256
7260
  if (error instanceof AdapterLlmError) throw error;
@@ -10907,7 +10911,7 @@ async function checkForUpdate(current, fetchImpl) {
10907
10911
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
10908
10912
  */
10909
10913
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
10910
- const FALLBACK_VERSION = "0.7.1";
10914
+ const FALLBACK_VERSION = "0.7.2";
10911
10915
  let cached;
10912
10916
  /** 本插件版本(如 `0.1.26`)。 */
10913
10917
  function pluginVersion() {
@@ -11075,7 +11079,7 @@ function runStartupSweep(options) {
11075
11079
  } catch {
11076
11080
  plan.kept.push(entry);
11077
11081
  }
11078
- if (plan.dropped.length > 0) writeJournal(plan.kept, file);
11082
+ if (plan.dropped.length > 0) writeJournal([...plan.kept, ...plan.toDelete], file);
11079
11083
  if (scheduled > 0) options.log?.(`deepseek-web: 上次退出遗留了 ${scheduled} 个临时会话,已交给清理器补删`);
11080
11084
  if (plan.dropped.length > 0) options.log?.(`deepseek-web: ${plan.dropped.length} 个遗留会话的账号已不在账号库里,无法回收(已从记录移除)`);
11081
11085
  return {