@moonquake2004/dsh-doctor 0.5.1 → 0.6.1

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
@@ -16,6 +16,34 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## [0.6.1] — 2026-09-15
20
+
21
+ ### Fixed — `--boot-check` 在"bundle 缺 `dsh.bundle`"这一类上给假绿灯(社区 #6788)
22
+
23
+ #6788 报告:0.1.6-alpha.1 起 loader **严格要求** `dsh.profile.bundles` 里每个包声明 `dsh.bundle.patch`,
24
+ 而 `@deepseek-ai/dsh-computer-use` 等包发布时**漏了这个字段** → 照文档 `dsh plugin add` 之后 profile **立刻起不来**。
25
+
26
+ 这一类的特点是:**没有任何 entry 可探**(包没有 patch,自然没有 insert 条目),于是只做 import 的
27
+ `--boot-check` 会输出"✓ 所有可探测 entry 均可导入"——而用户恰恰是按我们的建议先跑它的。**假绿灯出现在
28
+ 最该给出结论的时刻**,比不报更糟。
29
+
30
+ 现在 `--boot-check`(及其同步版,供 `--safe-add` 使用)在探测 entry 之前先判 **bundle 级前置条件**:
31
+ 列出的包若解析得到、却没有 `dsh.bundle.patch` → 直接判失败,给出 `missing-bundle-manifest`、
32
+ 修复方向与隔离命令。宿主核心包(`dsh-base`/`dsh-web-app`,由 CLI 提供)照旧跳过。
33
+
34
+ (`P1` 早已覆盖同一类:`bundle 条目 X 存在但未声明 dsh.bundle`——本次修的是**我们最推荐的那条自救路径**
35
+ 没有覆盖它。)
36
+
37
+ ## [0.6.0] — 2026-09-15
38
+
39
+ ### Added
40
+
41
+ - **P22 profile manifest 带 UTF-8 BOM**(社区 #6758,**error**):DSH 直接 `JSON.parse` 该文件,带 BOM 时抛 `Unexpected token '…' is not valid JSON` 并**启动硬失败**;而 GBK 控制台会把 BOM 三个字节渲染成乱码,用户看不出是编码问题。判据零误报,补的正是"报错不指向病因"缺的那一句,并给出无 BOM 保存的具体做法(PowerShell 5.1 的 `Set-Content -Encoding UTF8` 默认会写 BOM)。
42
+
43
+ ### Fixed — 我们自己在同一输入上的错判
44
+
45
+ - 此前直接 `JSON.parse(readFileSync(profile/package.json))`:**带 BOM 时解析失败 → 置 null → P18 报"未找到 profile manifest"**,是**错误结论**;整个 profile 段更会直接报"检查异常"。现在所有 manifest 读取走**带 BOM 感知**的读取:剥离 BOM 后继续工作,`hadBom` 交给 P22 单独报出。**诊断工具不能被它要诊断的那份输入打败**——#6758 让我们发现了自己在同一个输入上的同类问题。
46
+
19
47
  ## [0.5.1] — 2026-09-15
20
48
 
21
49
  ### Fixed — 把"读取间歇性失败"错判成"文件损坏"(社区 #6739)
package/dsh-doctor.mjs CHANGED
@@ -549,7 +549,14 @@ function checkProfile(name) {
549
549
  try { dir = resolveProfile(name); } catch (e) { report('profile', 'P0', false, e.message); return; }
550
550
  const manifestPath = join(dir, 'package.json');
551
551
  if (!existsSync(manifestPath)) { report('profile', 'P0', false, `profile 不存在: ${dir}`); return; }
552
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
552
+ // 用**带 BOM 感知**的读取:带 BOM 的 manifest 会让 DSH 启动硬失败(#6758),
553
+ // 而诊断工具更不能被它要诊断的那份输入打败——剥离 BOM 后继续工作,并由 P22 单独报出病因。
554
+ const mainRead = readJsonReportingBom(manifestPath);
555
+ const manifest = mainRead.data;
556
+ if (manifest === null) {
557
+ report('profile', 'P0', false, `profile manifest 无法解析为 JSON${mainRead.hadBom ? '(且带 UTF-8 BOM —— 见 P22,这正是 DSH 启动硬失败的原因 #6758)' : ''}`);
558
+ return;
559
+ }
553
560
  const bundles = manifest.dsh?.profile?.bundles ?? [];
554
561
  const deps = manifest.dependencies ?? {};
555
562
 
@@ -678,7 +685,10 @@ function checkProfile(name) {
678
685
  // DeepSeek 请求以 REQUEST_EXTENSION 失败(#6667 报告的最小复现)。故这里按"条件性风险"报 warn,不报 fail。
679
686
  {
680
687
  let profManifest = null;
681
- try { profManifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); } catch { profManifest = null; }
688
+ let manifestHadBom = false;
689
+ const profRead = readJsonReportingBom(join(dir, 'package.json'));
690
+ profManifest = profRead.data;
691
+ manifestHadBom = profRead.hadBom;
682
692
  if (!profManifest || !(profManifest.dsh && profManifest.dsh.profile)) {
683
693
  reportSkip('profile', 'P18', '未找到 profile manifest(无 dsh.profile),跳过 version 检查');
684
694
  } else if (typeof profManifest.version === 'string' && profManifest.version.length > 0) {
@@ -794,6 +804,27 @@ function checkProfile(name) {
794
804
  }
795
805
  }
796
806
 
807
+ // P22:profile manifest 带 UTF-8 BOM(#6758)—— **硬失败且报错不指向病因**
808
+ // DSH 直接 `JSON.parse` 该文件:带 BOM 时抛 `Unexpected token '...' is not valid JSON` 并**起不来**;
809
+ // 而 GBK 控制台会把 BOM 三个字节渲染成乱码,用户完全看不出是编码问题。
810
+ // 这条判据零误报,且正是"报错不指向病因"要补的那一句:**是 BOM,不是 JSON 语法**。
811
+ {
812
+ const profRead22 = readJsonReportingBom(join(dir, 'package.json'));
813
+ if (!profRead22.exists) {
814
+ reportSkip('profile', 'P22', '无 profile manifest,跳过 BOM 检查');
815
+ } else if (profRead22.hadBom) {
816
+ report('profile', 'P22', false,
817
+ `profile manifest 带 **UTF-8 BOM**(EF BB BF)—— DSH 会直接 \`JSON.parse\` 该文件:`
818
+ + `BOM 会让它抛 \`SyntaxError: Unexpected token '…' is not valid JSON\`(#6758)并**启动硬失败**;`
819
+ + `而在 GBK 控制台上那三个字节会显示成乱码,报错里看不出是编码问题`,
820
+ '去掉 BOM(保留 UTF-8 无 BOM):PowerShell 5.1 的 `Set-Content -Encoding UTF8` 默认会写 BOM,改用 '
821
+ + '`[IO.File]::WriteAllText($p, (Get-Content $p -Raw), (New-Object Text.UTF8Encoding $false))`,'
822
+ + '或任何「UTF-8(无 BOM)」保存方式;本工具的其余检查已忽略 BOM 继续工作');
823
+ } else {
824
+ report('profile', 'P22', true, 'profile manifest 无 UTF-8 BOM(不会触发 #6758 的启动硬失败)');
825
+ }
826
+ }
827
+
797
828
  // P19:插件声明的 host peer 范围 vs 实际提供的 host 版本(社区 #6678 @ciceroyang 提案)
798
829
  // 这是"升级后起不来"的常见原因之一:插件声明只支持某段 core 版本,而实际装的核心已在区间外,
799
830
  // 安装时却没有任何警告(#6680 的机制:profile 的 pnpm 配置 autoInstallPeers:false 且 host 由
@@ -1979,7 +2010,8 @@ catalogSeverity.set('P16', 'warn');
1979
2010
  catalogSeverity.set('P17', 'warn');
1980
2011
  catalogSeverity.set('P18', 'warn');
1981
2012
  catalogSeverity.set('P19', 'warn');
1982
- catalogSeverity.set('P20', 'warn'); // #6693:client 格式问题在浏览器才炸,服务端零痕迹——高价值提示但不阻断 CI
2013
+ catalogSeverity.set('P20', 'warn');
2014
+ catalogSeverity.set('P22', 'error'); // #6758:BOM 会让启动硬失败,零误报 → 按 error // #6693:client 格式问题在浏览器才炸,服务端零痕迹——高价值提示但不阻断 CI
1983
2015
  // P21 不设 warn:它是在 apply() 内**同步抛出**、直接中断整棵装载链的致命类(#6693 实测 195+55 次),
1984
2016
  // 且我们两轮去误报(注释/字符串)后在真实 profile 的 8 个 host 入口 + 8 个 client 产物上零误报,故按 error 处理。 // #6678:声明不匹配是风险信号而非确定失败,提示但不阻断 // #6667:条件性风险(需游离本地模块才触发),提示但不翻退出码
1985
2017
 
@@ -2460,6 +2492,30 @@ function listProfilePackages(profileDir) {
2460
2492
  return out;
2461
2493
  }
2462
2494
 
2495
+
2496
+ /**
2497
+ * 列出 `dsh.profile.bundles` 里**能解析到、但没有 `dsh.bundle`** 的包。
2498
+ * 这类会让 loader 拒绝整个 profile(#6788/#1378),且**没有任何 entry 可探**——
2499
+ * 所以必须由 bundle 级前置条件来判,否则装载模拟会给出假绿灯。
2500
+ * 宿主核心包(如 @deepseek-ai/dsh-base / dsh-web-app)由 CLI 提供、不在 profile node_modules 里,跳过。
2501
+ */
2502
+ function bundlesMissingManifest(profileDir) {
2503
+ const out = [];
2504
+ try {
2505
+ const read = readJsonReportingBom(join(profileDir, 'package.json'));
2506
+ const bundles = read.data?.dsh?.profile?.bundles ?? [];
2507
+ for (const b of bundles) {
2508
+ if (String(b).startsWith('@deepseek-ai/dsh-base') || String(b).startsWith('@deepseek-ai/dsh-web-app')) continue;
2509
+ const mf = join(profileDir, 'node_modules', String(b), 'package.json');
2510
+ if (!existsSync(mf)) continue; // 解析不到 → 由 P1 报(且可能是宿主提供的核心包)
2511
+ const pkg = readJsonReportingBom(mf).data;
2512
+ if (!pkg) continue;
2513
+ if (!pkg.dsh?.bundle?.patch) out.push({ id: String(b), bundle: String(b) });
2514
+ }
2515
+ } catch { /* 读不了就不判 */ }
2516
+ return out;
2517
+ }
2518
+
2463
2519
  /** 解析 profile 的启动列表与各 bundle 的 entry(含用户 patch 的 insert),跳过 disabled 与已隔离项。 */
2464
2520
  function collectBootEntries(profileDir) {
2465
2521
  const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
@@ -2538,6 +2594,26 @@ function classifyReadAttempts(attemptLog) {
2538
2594
  return 'intermittent';
2539
2595
  }
2540
2596
 
2597
+
2598
+ /**
2599
+ * 读 JSON 并**报告是否带 UTF-8 BOM**。
2600
+ *
2601
+ * 社区 #6758:profile 清单只要带 BOM(PowerShell 5.1 的 `Set-Content -Encoding UTF8` 默认会写),
2602
+ * `JSON.parse` 就抛 `Unexpected token '...'`,DSH **硬失败起不来**;而 GBK 控制台会把 BOM 三个字节
2603
+ * 渲染成乱码,报错里完全看不出是编码问题——"报错不指向病因"。
2604
+ *
2605
+ * 我们自己也踩同一个坑:此前直接 `JSON.parse(readFileSync(...))` 失败后置 null,于是 P18 会报
2606
+ * "未找到 profile manifest"——**错误结论**。诊断工具在它要诊断的输入上给出错判,正是最该避免的。
2607
+ * 所以:**剥离 BOM 后再解析**(让其余检查继续工作),并把 hadBom 单独报出来(P22)。
2608
+ */
2609
+ function readJsonReportingBom(file) {
2610
+ let raw;
2611
+ try { raw = readFileSync(file); } catch { return { data: null, hadBom: false, exists: false }; }
2612
+ const hadBom = raw.length >= 3 && raw[0] === 0xEF && raw[1] === 0xBB && raw[2] === 0xBF;
2613
+ const text = hadBom ? raw.subarray(3).toString('utf8') : raw.toString('utf8');
2614
+ try { return { data: JSON.parse(text), hadBom, exists: true }; } catch { return { data: null, hadBom, exists: true }; }
2615
+ }
2616
+
2541
2617
  /* ---- 预检增强(2026-09):快照对比 + 安全安装 ---- */
2542
2618
 
2543
2619
  const SNAPSHOT_FILE = '.dsh-doctor-snapshot.json';
@@ -2626,6 +2702,10 @@ function safeAdd(profileArg, pkg) {
2626
2702
  /** 同步版装载模拟(--safe-add 内部用;与 --boot-check 同一逻辑) */
2627
2703
  function runBootCheckSync(profileDir) {
2628
2704
  const out = [];
2705
+ // **bundle 级前置条件**(社区 #6788):loader 要求 `dsh.profile.bundles` 里每个包都声明
2706
+ // `dsh.bundle.patch`,否则**整场启动硬失败**。这类失败**没有任何 entry 可探**,于是只做 import 的
2707
+ // 探针会给出"全部可导入 ✓"的**假绿灯**——而用户是按我们的建议先跑 `--boot-check` 的,被误导的代价最大。
2708
+ for (const miss of bundlesMissingManifest(profileDir)) out.push({ id: miss.id, bundle: miss.bundle, spec: '(bundle 清单)', status: 'failed', kind: 'missing-bundle-manifest', hint: '该包未声明 dsh.bundle(loader 会拒绝整个 profile);升级/更换该包,或从 dsh.profile.bundles 移除', error: `bundle 条目 ${miss.bundle} 存在但未声明 dsh.bundle.patch(#1378/#6788)` });
2629
2709
  for (const e of collectBootEntries(profileDir)) {
2630
2710
  if (!e.name) continue;
2631
2711
  const spec = e.name;
@@ -2689,6 +2769,10 @@ async function runBootCheck(profileDir) {
2689
2769
  const entries = collectBootEntries(profileDir);
2690
2770
  const targets = entries.filter((e) => e.name);
2691
2771
  const results = [];
2772
+ // 同 runBootCheckSync:bundle 级前置条件必须一并判,否则这一类会得到假绿灯(#6788)
2773
+ for (const miss of bundlesMissingManifest(profileDir)) {
2774
+ results.push({ id: miss.id, bundle: miss.bundle, spec: '(bundle 清单)', status: 'failed', kind: 'missing-bundle-manifest', hint: '该包未声明 dsh.bundle(loader 会拒绝整个 profile);升级/更换该包,或从 dsh.profile.bundles 移除', error: `bundle 条目 ${miss.bundle} 存在但未声明 dsh.bundle.patch(#1378/#6788)` });
2775
+ }
2692
2776
  for (const e of targets) {
2693
2777
  const spec = e.name;
2694
2778
  // 宿主内置与相对路径不做 import 探测(前者由宿主提供,后者依赖运行上下文)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
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": [