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 +88 -0
- package/lib/index.js +25 -21
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
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))
|
|
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].
|
|
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
|
-
|
|
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
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
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.
|
|
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 {
|