@moonquake2004/dsh-doctor 0.4.14 → 0.4.16

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.
Files changed (2) hide show
  1. package/dsh-doctor.mjs +67 -4
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -54,11 +54,11 @@
54
54
  * 退出码:0 = 全部通过;1 = 发现可修复问题(内置 + catalog severity=error);warn 级失败不改退出码。
55
55
  */
56
56
  import { execFileSync, spawnSync } from 'node:child_process';
57
- import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
57
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
58
58
  import { createRequire } from 'node:module';
59
59
  import net from 'node:net';
60
60
  import { basename, delimiter as PATH_DELIM, dirname, join, relative, resolve } from 'node:path';
61
- import { homedir } from 'node:os';
61
+ import { homedir, tmpdir } from 'node:os';
62
62
  import { pathToFileURL, fileURLToPath } from 'node:url';
63
63
 
64
64
  const HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
@@ -268,6 +268,38 @@ function checkEnv() {
268
268
  supported ? undefined : '升级 node 到 ^22.19.0 或 >=24.0.0(root package.json engines,见 #2259)');
269
269
  }
270
270
 
271
+ // E12:运行时 zstd 稳定性(#6651 的运行时线索)
272
+ // 锚点已核实:dsh-session-persistence-jsonl:15 直接 `import { zstdCompress, zstdDecompress,
273
+ // zstdDecompressSync } from "node:zlib"`(用于 :1263 / :1287 / :1368)——会话日志的读写**依赖 Node 内置 zstd**。
274
+ // 该 API 在部分 Node 版本上仍是实验性的;社区报告 #6651(Linux + Node 22.22.3)出现首帧损坏让
275
+ // `dsh web` 无法启动,换到 Node 26 后正常,怀疑写入侧不稳。故这里如实报告运行时状态,不做因果断言。
276
+ if (!hasDshEnvironment(HOME)) {
277
+ reportSkip('env', 'E12', '未发现 DSH 环境,跳过运行时 zstd 稳定性检查');
278
+ } else {
279
+ const zstdFn = (() => { try { return createRequire(import.meta.url)('node:zlib').zstdDecompressSync; } catch { return null; } })();
280
+ if (typeof zstdFn !== 'function') {
281
+ report('env', 'E12', false,
282
+ `当前 Node(${process.version})没有 zlib.zstdDecompressSync——而会话持久化直接依赖它读写 .zstd 日志(#6651 同族风险)`,
283
+ '升级 Node 到内置 zstd 的版本(22.15+/23.8+),否则该运行时的 .zstd 会话日志无法读写');
284
+ } else {
285
+ // 用子进程观察 Node 是否对该 API 发出实验性警告(版本无关的经验判据,避免硬编码版本表)
286
+ let experimental = false; let probed = false;
287
+ try {
288
+ const r = spawnSync(process.execPath, ['-e', 'require("node:zlib").zstdDecompressSync(require("node:zlib").zstdCompressSync(Buffer.from("x")))'], { encoding: 'utf8', timeout: 10000 });
289
+ probed = true;
290
+ experimental = /ExperimentalWarning/.test(String(r.stderr || ''));
291
+ } catch { /* 探测失败 → 下面按"未探明"处理 */ }
292
+ if (!probed) reportSkip('env', 'E12', '运行时 zstd 实验性探测未能执行,跳过(不外推为通过)');
293
+ else if (experimental) {
294
+ report('env', 'E12', false,
295
+ `当前 Node(${process.version})的 zlib zstd 仍标记为**实验性**——会话持久化直接依赖它读写日志;社区报告 #6651 在同类运行时上出现首帧损坏并导致 dsh web 无法启动(换新版 Node 后正常,因果未定)`,
296
+ '升级到 zstd 已稳定的 Node(本工具在 24.x 上实测无实验性警告)后再观察;同时建议先用 dsh-doctor 扫一遍会话库有无 S13 类损坏');
297
+ } else {
298
+ report('env', 'E12', true, `运行时 zstd 非实验性(Node ${process.version})`, undefined);
299
+ }
300
+ }
301
+ }
302
+
271
303
  // E4:node-pty 原生模块完整性(#1219:pty.node 缺失 → dsh web 启动失败)
272
304
  const ptyDirs = [];
273
305
  for (const p of (process.env.PATH || '').split(PATH_DELIM)) {
@@ -1162,8 +1194,39 @@ function checkSession(targetPath) {
1162
1194
  `日志首行不是会话头(type=${header && header.type ? JSON.stringify(header.type) : '缺失/无法解析'})——#6651:此类损坏会让 \`dsh web\` **整体启动失败**(corrupt Zstandard session log: first frame is not exactly …),会话列表一并损坏`,
1163
1195
  '优先从备份恢复该日志;无备份时把整个会话目录**移出** sessions/(隔离,勿删)后重启 dsh,再用本工具复查');
1164
1196
  } else {
1165
- report('session', 'S13', true,
1166
- `会话头完整(type=session, version=${header.version ?? '?'}, id=${String(header.id ?? '').slice(0, 12)})`);
1197
+ // 更精确的判据:harness 的读取路径是 **Node 的 `zlib.zstdDecompressSync`**,它**只解第一个帧**,
1198
+ // 然后要求该帧明文恰好是一行(dsh-session-persistence-jsonl:1891/2185 的
1199
+ // `plaintext.indexOf(10) !== plaintext.length - 1`)。所以最忠实的做法不是自己扫 magic 猜帧边界,
1200
+ // 而是**用同一个解压器**复现同一个条件(2026-09 与 #6651 的运行时线索一致后再校准)。
1201
+ let frameNote = '';
1202
+ let frameBad = false;
1203
+ if (target.endsWith('.zstd')) {
1204
+ const zstdSync = (() => { try { return createRequire(import.meta.url)('node:zlib').zstdDecompressSync; } catch { return null; } })();
1205
+ if (typeof zstdSync !== 'function') {
1206
+ frameNote = '(本机 Node 无 zlib.zstdDecompressSync,未据此判定)';
1207
+ } else {
1208
+ try {
1209
+ const first = zstdSync(readFileSync(target)).toString('utf8');
1210
+ const exactlyOneLine = first.length > 0 && first.indexOf('\n') === first.length - 1;
1211
+ if (!exactlyOneLine) {
1212
+ frameBad = true;
1213
+ const n = first.split('\n').filter((l) => l.trim()).length;
1214
+ frameNote = `首帧明文含 ${n} 行(harness 要求恰好 1 行头)——帧边界错位/单帧容器都会触发 corrupt Zstandard session log`;
1215
+ }
1216
+ } catch (e) {
1217
+ frameBad = true;
1218
+ frameNote = `首帧无法解压(${String(e.code || e.message).slice(0, 40)})——与 harness 读取路径一致,启动会被拒绝`;
1219
+ }
1220
+ }
1221
+ }
1222
+ if (frameBad) {
1223
+ report('session', 'S13', false,
1224
+ `会话头存在但**首帧边界不合规**:${frameNote}`,
1225
+ '优先从备份恢复;无备份时把整个会话目录移出 sessions/(隔离,勿删)后重启 dsh,再用本工具复查');
1226
+ } else {
1227
+ report('session', 'S13', true,
1228
+ `会话头完整(type=session, version=${header.version ?? '?'}, id=${String(header.id ?? '').slice(0, 12)})${frameNote}`);
1229
+ }
1167
1230
  }
1168
1231
  }
1169
1232
  const calls = new Map(); const results2 = new Set(); let maxSeq = -1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
5
5
  "main": "lib/index.js",
6
6
  "files": [