@moonquake2004/dsh-doctor 0.4.31 → 0.5.0
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 +72 -0
- package/dsh-doctor.mjs +131 -19
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本文件记录面向使用者的变更。检查清单与判定语义属于公开契约的一部分,故**新增检查、子命令、判定翻转都算 MINOR**。
|
|
4
|
+
|
|
5
|
+
## 发布规则 / Release policy
|
|
6
|
+
|
|
7
|
+
| 变更类型 | 版本位 | 例 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| 新增检查、新增子命令/开关 | **MINOR** | 新增 `--boot-check`、新增 S13 |
|
|
10
|
+
| 判定或语义变化(同一输入给出不同结论)、默认行为变化、输出结构新增字段 | **MINOR**(并在下方 *Changed* 里逐条写明) | 默认会话目标改为跳过活跃会话;P21 定为 error |
|
|
11
|
+
| 纯修复(错判、崩溃、文案出处错误、CI/依赖问题) | PATCH | E4 无安装树时改 skip |
|
|
12
|
+
| 新增**远程目录检查**(数据驱动,`checks.json`) | **不动版本** | 6 小时内自动生效,无需发版 |
|
|
13
|
+
|
|
14
|
+
> 0.x 期间 MINOR 位是"特性/行为"轴。此前的实践曾把特性当成 PATCH 连发(2026-09-15 一天 19 个 PATCH),
|
|
15
|
+
> 那会让 `^0.4.x` 的消费者静默收到判定翻转——本文件与 `0.5.0` 就是为纠正这一点。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [0.5.0] — 2026-09-15
|
|
20
|
+
|
|
21
|
+
累积自 `0.4.6` 的全部变更(中间以 0.4.14–0.4.32 的 PATCH 形式先行发布过,此处归并到 MINOR)。
|
|
22
|
+
|
|
23
|
+
### Added — 新检查
|
|
24
|
+
|
|
25
|
+
- **S13 会话头完整性**(#6651):首行必须是 `{"type":"session"}` 头,且**第一个 zstd 帧恰好只有一行**——判据取自 harness 自身的读取路径(`zlib.zstdDecompressSync` 只解首帧)。此类损坏会让 `dsh web` **整体启动失败**。
|
|
26
|
+
- **S14 投影安全性预检**(#6686):迁移**成功但打开抛错**的会话——缺 `data.message` 的 `assistant/message`/`tool/result`/`system/message`,以及 replace 形态 `surfaceOp` 引用解析不到。对需要迁移的日志**走真实迁移链取"迁移后视图"**再判定,并在输出里注明判定视图。
|
|
27
|
+
- **E12 运行时 zstd 稳定性**(#6651):会话持久化直接依赖 `node:zlib` 的 zstd;用子进程探测是否仍标为实验性。
|
|
28
|
+
- **E13 CLI 静默失效签名**(#6341 / #6692):`dsh` 在 PATH 中但 `--version` **零输出且退出码 0** —— `import.meta.main` 门控(该 API 仅 Node ≥22.18.0 / ≥24.2.0 存在)。
|
|
29
|
+
- **P18 profile manifest 的 version**(#6667):有 `name` 无 `version` 时,解析游离本地模块会抛 `must declare non-empty name and version`。
|
|
30
|
+
- **P19 host peer 范围 vs 实际提供版本**(#6678 / #6683,社区 @ciceroyang 提案):只取 `@deepseek-ai/*`、跟随软链接、作用域名两段、三态(未知既不算兼容也不算不兼容);另按规范规则 1 计数"有 bundle 却未声明 host 范围"的包。
|
|
31
|
+
- **P20 client 产物加载格式**(#6693 规则 3):必须是 `__ModuleLoader__.load()` 的 CJS 工厂;写成裸 ESM 会在浏览器抛 `Unexpected token 'export'` 而**服务端日志零痕迹**(`node --check` 抓不到,因为那是合法 ESM 语法)。
|
|
32
|
+
- **P21 沙箱专属符号**(#6693 规则 2/4,**error**):host 侧裸 `harness`、client 侧 `styles` / `ctx.get('host')` —— 从 `node_modules` 加载的普通插件里一律不存在;host 侧在 `apply()` 内同步抛出会**中断整棵装载链**。
|
|
33
|
+
- E10 增加"端口绑着但 **HTTP 无应答**"判定(#6693):端口占用 ≠ 服务健康(崩溃重启循环下两者同时成立)。
|
|
34
|
+
|
|
35
|
+
### Added — 子命令(不依赖 dsh 能启动)
|
|
36
|
+
|
|
37
|
+
- **`--boot-check`**:离线模拟插件树装载,逐条 entry 真去 import,归类失败(缺导出/未安装/原生 ABI/模块格式/卡死)并给出修复方向与隔离命令。
|
|
38
|
+
- **`--quarantine <包>` / `--unquarantine <包>`**:把 bundle 移出启动列表(**先写 `package.json.bak.<ts>`**,只改列表、不动包文件,可撤销)。
|
|
39
|
+
- **`--safe-add <包>`**:安装 → 立即验证 → 通过则写"已知良好"快照;失败自动隔离;隔离不足则**整体回滚**到安装前 manifest。
|
|
40
|
+
- **`--pre-upgrade` / `--post-upgrade`**:升级前记基线(含核心版本),升级后自动对比 + 复检(`--auto-quarantine` 可一步隔离)。
|
|
41
|
+
|
|
42
|
+
### Changed — ⚠️ 行为变更
|
|
43
|
+
|
|
44
|
+
- **运行时检查默认跳过"正在写入的活跃会话"**:原先扫"最新会话"通常就是当前会话,会把操作者自己的操作报成安全发现。现取 mtime 早于活跃窗口的最新会话;窗口用 `DSH_DOCTOR_LIVE_WINDOW_MS` 调整(默认 120000,设 `0` 关闭保护)。`--session` 显式指定始终尊重。
|
|
45
|
+
- **`--json` 每条检查新增 `status` 字段**(`pass`/`warn`/`fail`/`skip`,与 `--envelope` 同一词汇表)。纯新增,但消费者此前自行推导的状态应与之一致。
|
|
46
|
+
- **P21 按 error 定级** → 某些环境会**新出现 exit 2**;P20 按 warn(判据较粗,不阻断)。
|
|
47
|
+
- **P19 的版本判定按外部参考实现对齐**(#6683):`>=0.1.0-rc.5 <0.2.0` 面对 `0.1.5-rc.2` 由"未知"变为**兼容**;纯 release 区间面对数值满足的预发布版本记**未知**。此前按这一条报出的结论会消失。
|
|
48
|
+
- **E3 的支持范围可溯源**:默认标注"内置常量(出处:仓库根 package.json engines.node,核对日期)";设置 `DSH_DOCTOR_HARNESS_ROOT` 时**真读**该检出。
|
|
49
|
+
- `--quarantine` / `--unquarantine` 默认输出人读形态(`--json` 时保留机器形态)。
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **P3:ESM-only 包被误判为不可解析**(#1719 社区指出的 `require.resolve` 陷阱,实测命中我们)——改为存在性判据(跟随软链接、作用域名两段、排除 `cordis:`/相对路径)。
|
|
54
|
+
- **E4:无安装树可查时 skip 而非 fail**——原先在合成 HOME/干净容器里报"未找到 node-pty",连带 E2/E5/envelope 三个用例红;套件由 3 红转全绿。
|
|
55
|
+
- **E7 / E13:无 DSH 环境时 skip**,探针判断"被告知的环境"而非进程全局 HOME。
|
|
56
|
+
- **S13 覆盖 harness 精确判据**:从"首行是不是头"升级为"第一个 zstd 帧恰好一行";改用与 harness 相同的解压器。
|
|
57
|
+
- **S11 不再把首行非会话头的日志报成"健康"**(#6651 类的假阴性)。
|
|
58
|
+
- **S14 改判迁移后视图**(#6686):初版只看磁盘行,会漏掉迁移过程中产生的形态。
|
|
59
|
+
- **P20/P21 两轮误报治理**:注释(`//#region styles`)与字符串(`"deepseek-harness"`)曾被当成符号引用——真实 profile 上 9 处命中全是假的;现要求符号处于代码位置且未被本地声明。
|
|
60
|
+
- **E3 出处引用两次更正**:先写"root package.json engines"、后改成"不是来自 manifest",两次都不准——准确说法是"范围确实声明在仓库根的**私有** workspace(`@deepseek-ai/dsh-root`)上,没有任何发布物继承它"。
|
|
61
|
+
- `--boot-check` 会执行插件顶层代码(这正是"启动"的语义),故为显式开关;相关说明已写入 README。
|
|
62
|
+
|
|
63
|
+
### Infra
|
|
64
|
+
|
|
65
|
+
- **CI 覆盖 ubuntu + windows × Node 22/24**。加入 Windows 后立即抓出 `dsh-security` SP14 的真实平台缺陷(路径分隔符),并暴露出 7 处测试的平台假设。
|
|
66
|
+
- README 新增「dsh 起不来怎么办(不依赖其它工具)」与 `--safe-add`/漂移对比两节。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## [0.4.6] 及更早
|
|
71
|
+
|
|
72
|
+
早期版本请见 git 历史与 `docs/`。要点:`dsh-doctor/v1` 信封与 catalog 检查、S11/S12 全会话扫描与迁移拒载预检、P11–P17 系列、Layer C 观察者。
|
package/dsh-doctor.mjs
CHANGED
|
@@ -690,6 +690,110 @@ function checkProfile(name) {
|
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
+
// P20 / P21:第三方插件把整棵插件树拖垮的两种**静态可判**形态(社区 #6693 实测)
|
|
694
|
+
// 报告者在 host 侧 `apply()` 里连续撞了两个独立致命错、client 侧撞了两个静默白屏错;
|
|
695
|
+
// 其核心抱怨是"唯一的诊断入口被故障本身摧毁"。这两条都不需要执行插件代码即可判定。
|
|
696
|
+
{
|
|
697
|
+
const pkgDirs19 = listProfilePackages(dir);
|
|
698
|
+
// 收集 client 产物(三种真实布局,与 dsh-security SP7 的收集口径一致)
|
|
699
|
+
const clientFiles = [];
|
|
700
|
+
const hostFiles = [];
|
|
701
|
+
const MAX_PER_PKG = 8;
|
|
702
|
+
for (const { dir: pkgDir } of pkgDirs19) {
|
|
703
|
+
const push = (arr, f) => { if (arr.length < MAX_PER_PKG && existsSync(f) && /\.(js|mjs|cjs)$/.test(f)) arr.push(f); };
|
|
704
|
+
const clientDir = join(pkgDir, 'client');
|
|
705
|
+
if (existsSync(clientDir)) {
|
|
706
|
+
try { for (const e of readdirSync(clientDir, { withFileTypes: true })) if (e.isFile()) push(clientFiles, join(clientDir, e.name)); } catch { /* 不可读 */ }
|
|
707
|
+
}
|
|
708
|
+
push(clientFiles, join(pkgDir, 'lib', 'client.js'));
|
|
709
|
+
push(clientFiles, join(pkgDir, 'client.js'));
|
|
710
|
+
let mf = null;
|
|
711
|
+
try { mf = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')); } catch { /* 跳过 */ }
|
|
712
|
+
const mainRel = typeof mf?.main === 'string' ? mf.main : (typeof mf?.exports === 'string' ? mf.exports : null);
|
|
713
|
+
if (mainRel) push(hostFiles, join(pkgDir, mainRel));
|
|
714
|
+
push(hostFiles, join(pkgDir, 'lib', 'index.js'));
|
|
715
|
+
push(hostFiles, join(pkgDir, 'index.js'));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// P20:client 产物必须是 `__ModuleLoader__.load({ id, factory })` 的 CJS 工厂。
|
|
719
|
+
// 写成裸 ESM(顶层 export/import)会在浏览器抛 `Unexpected token 'export'` 而**服务端零痕迹**——
|
|
720
|
+
// 注意:`node --check` 抓不到它,因为那是**合法 ESM 语法**(dsh-security SP7 用的是 --check)。
|
|
721
|
+
const p20 = [];
|
|
722
|
+
let p20scanned = 0;
|
|
723
|
+
for (const f of clientFiles) {
|
|
724
|
+
let text; try { text = readFileSync(f, 'utf8'); } catch { continue; }
|
|
725
|
+
p20scanned++;
|
|
726
|
+
const hasFactory = /__ModuleLoader__\s*\.\s*load\s*\(/.test(text);
|
|
727
|
+
const topLevelEsm = /^\s*(export\s|import\s)/m.test(text);
|
|
728
|
+
if (hasFactory) continue;
|
|
729
|
+
if (topLevelEsm) p20.push(`${f.split('/node_modules/')[1] || f}(未见 __ModuleLoader__.load,且有顶层 export/import)`);
|
|
730
|
+
}
|
|
731
|
+
if (!p20scanned) reportSkip('profile', 'P20', '未发现 client 产物,跳过加载格式检查');
|
|
732
|
+
else if (p20.length) {
|
|
733
|
+
report('profile', 'P20', false,
|
|
734
|
+
`client 产物不是 CJS 工厂(${p20.length}/${p20scanned} 个):\n ${p20.slice(0, 5).join('\n ')}\n`
|
|
735
|
+
+ ` —— 浏览器会抛 \`Uncaught SyntaxError: Unexpected token 'export'\`,**服务端日志里出现 0 次**:`
|
|
736
|
+
+ `systemd 健康、端口在听、页面永远空白(#6693 实测:整份 journal 里该错误计数为 0)`,
|
|
737
|
+
'把 client 半边改成 `__ModuleLoader__.load({ id, factory })` 的 CJS 工厂形态(参照 dsh-better-sidebar / dsh-dream-skin)');
|
|
738
|
+
} else {
|
|
739
|
+
report('profile', 'P20', true, `client 产物均为 CJS 工厂形态(${p20scanned} 个)`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// P21:**沙箱专属符号在普通插件里不存在**(host 与 client 两侧一律如此,同一根因两个面)
|
|
743
|
+
// host 侧:裸 `harness` → ReferenceError: harness is not defined(#6693 规则 2,出现 55 次)
|
|
744
|
+
// client 侧:`styles` / `ctx.get('host')` 等同样拿不到(#6693 规则 4)
|
|
745
|
+
// 判据保守:文件里**完全没有** `typeof <symbol>` 守卫才报(有守卫则只提示——词法上无法证明守卫顺序,
|
|
746
|
+
// 而报告者特别指出 `harness.handle && …` 这种写法挡不住:裸标识符在 `&&` 短路之前就被解析)。
|
|
747
|
+
const p21 = [];
|
|
748
|
+
const notes21 = [];
|
|
749
|
+
// 匹配前的净化(两轮实测教训,都是真实 profile 上的误报):
|
|
750
|
+
// 1) `//#region styles` 这类**注释**区域标记会被当成裸引用(6 处全是它);
|
|
751
|
+
// 2) `"deepseek-harness"` / `/api/harness/connector/stream` 这类**字符串**同理(3 处全是它)。
|
|
752
|
+
// 故:剥注释 + 剥字符串字面量(近似,不处理转义/嵌套模板)+ 要求符号处于**代码位置**
|
|
753
|
+
// (后跟 `.` / `[` / `(` —— 真正使用一个全局必然如此)+ 若文件本地声明了同名变量则不算。
|
|
754
|
+
const stripAll = (t) => t
|
|
755
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
756
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, '$1 ')
|
|
757
|
+
.replace(/'(?:[^'\\\n]|\\.)*'/g, ' ')
|
|
758
|
+
.replace(/"(?:[^"\\\n]|\\.)*"/g, ' ')
|
|
759
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, ' ');
|
|
760
|
+
const scan = (files, symbols, side) => {
|
|
761
|
+
for (const f of files) {
|
|
762
|
+
let text; try { text = stripAll(readFileSync(f, 'utf8')); } catch { continue; }
|
|
763
|
+
const rel = f.split('/node_modules/')[1] || f;
|
|
764
|
+
for (const sym of symbols) {
|
|
765
|
+
const useRe = new RegExp(`(^|[^\\w.$])\\b${sym}\\b\\s*[.[(]`, 'm');
|
|
766
|
+
if (!useRe.test(text)) continue;
|
|
767
|
+
const declared = new RegExp(`\\b(?:const|let|var|function|class)\\s+${sym}\\b|[{,]\\s*${sym}\\s*[,}]`).test(text);
|
|
768
|
+
if (declared) continue; // 本地声明同名变量 → 不是沙箱符号
|
|
769
|
+
const guarded = new RegExp(`typeof\\s+${sym}\\b`).test(text);
|
|
770
|
+
if (guarded) { notes21.push(`${rel}: 引用 ${sym}(同文件有 typeof 守卫,但词法上无法证明守卫顺序)`); continue; }
|
|
771
|
+
p21.push(`${rel}(${side}): 裸引用沙箱专属符号 \`${sym}\``);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
scan(hostFiles, ['harness'], 'host');
|
|
776
|
+
scan(clientFiles, ['harness', 'styles'], 'client');
|
|
777
|
+
// client 侧还常通过 ctx.get('host'/'styles') 取沙箱服务
|
|
778
|
+
for (const f of clientFiles) {
|
|
779
|
+
let text; try { text = stripAll(readFileSync(f, 'utf8')); } catch { continue; }
|
|
780
|
+
const m = /ctx\.get\(\s*['"](host|styles)['"]/.exec(text);
|
|
781
|
+
if (m) p21.push(`${f.split('/node_modules/')[1] || f}(client): ctx.get('${m[1]}') —— 沙箱宿主专属服务,常驻 bundle 里拿不到`);
|
|
782
|
+
}
|
|
783
|
+
if (p21.length) {
|
|
784
|
+
report('profile', 'P21', false,
|
|
785
|
+
`插件的 host/client 半边引用了**沙箱专属符号**(${p21.length} 处)——这类符号在从 node_modules 加载的普通插件里一律不存在:\n `
|
|
786
|
+
+ p21.slice(0, 6).join('\n ')
|
|
787
|
+
+ (p21.length > 6 ? `\n …另有 ${p21.length - 6} 处` : '')
|
|
788
|
+
+ `\n host 侧表现为 \`ReferenceError: harness is not defined\`(在 \`apply()\` 内同步抛出 → cordis 中断装载链 → Web UI 整体不可用)`,
|
|
789
|
+
'移除这些引用或用宿主提供的等价能力;若确需守卫,必须写成 `typeof X !== \"undefined\"` **放在最前**——`X.foo && …` 挡不住(裸标识符先被解析)');
|
|
790
|
+
} else if (notes21.length) {
|
|
791
|
+
report('profile', 'P21', true, `未见裸引用沙箱专属符号(${notes21.length} 处有 typeof 守卫,未计入)`);
|
|
792
|
+
} else {
|
|
793
|
+
report('profile', 'P21', true, `扫描 ${hostFiles.length} 个 host 入口与 ${clientFiles.length} 个 client 产物,未见沙箱专属符号引用`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
693
797
|
// P19:插件声明的 host peer 范围 vs 实际提供的 host 版本(社区 #6678 @ciceroyang 提案)
|
|
694
798
|
// 这是"升级后起不来"的常见原因之一:插件声明只支持某段 core 版本,而实际装的核心已在区间外,
|
|
695
799
|
// 安装时却没有任何警告(#6680 的机制:profile 的 pnpm 配置 autoInstallPeers:false 且 host 由
|
|
@@ -698,24 +802,7 @@ function checkProfile(name) {
|
|
|
698
802
|
const findings19 = [];
|
|
699
803
|
let unknown19 = 0;
|
|
700
804
|
let checked19 = 0;
|
|
701
|
-
|
|
702
|
-
// 只看 isDirectory 会漏掉它们,这正是社区提醒的坑)
|
|
703
|
-
const pkgDirs19 = [];
|
|
704
|
-
try {
|
|
705
|
-
for (const e of readdirSync(join(dir, 'node_modules'), { withFileTypes: true })) {
|
|
706
|
-
if (!e.name || e.name.startsWith('.') || e.name === '.bin') continue;
|
|
707
|
-
if (e.name.startsWith('@')) {
|
|
708
|
-
try {
|
|
709
|
-
for (const sub of readdirSync(join(dir, 'node_modules', e.name), { withFileTypes: true })) {
|
|
710
|
-
if (!sub.name || sub.name.startsWith('.')) continue;
|
|
711
|
-
if (sub.isDirectory() || sub.isSymbolicLink()) pkgDirs19.push({ name: `${e.name}/${sub.name}`, dir: join(dir, 'node_modules', e.name, sub.name) });
|
|
712
|
-
}
|
|
713
|
-
} catch { /* 跳过不可读 scope */ }
|
|
714
|
-
} else if (e.isDirectory() || e.isSymbolicLink()) {
|
|
715
|
-
pkgDirs19.push({ name: e.name, dir: join(dir, 'node_modules', e.name) });
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
} catch { /* 无 node_modules */ }
|
|
805
|
+
const pkgDirs19 = listProfilePackages(dir);
|
|
719
806
|
for (const { name: pkgName, dir: pkgDir } of pkgDirs19) {
|
|
720
807
|
let mf;
|
|
721
808
|
try { mf = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')); } catch { continue; }
|
|
@@ -1875,7 +1962,10 @@ catalogSeverity.set('P15', 'error');
|
|
|
1875
1962
|
catalogSeverity.set('P16', 'warn');
|
|
1876
1963
|
catalogSeverity.set('P17', 'warn');
|
|
1877
1964
|
catalogSeverity.set('P18', 'warn');
|
|
1878
|
-
catalogSeverity.set('P19', 'warn');
|
|
1965
|
+
catalogSeverity.set('P19', 'warn');
|
|
1966
|
+
catalogSeverity.set('P20', 'warn'); // #6693:client 格式问题在浏览器才炸,服务端零痕迹——高价值提示但不阻断 CI
|
|
1967
|
+
// P21 不设 warn:它是在 apply() 内**同步抛出**、直接中断整棵装载链的致命类(#6693 实测 195+55 次),
|
|
1968
|
+
// 且我们两轮去误报(注释/字符串)后在真实 profile 的 8 个 host 入口 + 8 个 client 产物上零误报,故按 error 处理。 // #6678:声明不匹配是风险信号而非确定失败,提示但不阻断 // #6667:条件性风险(需游离本地模块才触发),提示但不翻退出码
|
|
1879
1969
|
|
|
1880
1970
|
function bundledCatalog() {
|
|
1881
1971
|
const p = new URL('./checks.json', import.meta.url);
|
|
@@ -2332,6 +2422,28 @@ function resolveHostVersion(profileDir, name) {
|
|
|
2332
2422
|
* 注意:--boot-check 会**执行插件顶层代码**(这正是"启动"本身的语义),故为显式开关、非默认行为。
|
|
2333
2423
|
*/
|
|
2334
2424
|
|
|
2425
|
+
/** 枚举 profile node_modules 里的包(含 scoped)。**跟随软链接**——pnpm 与 file:/dev 安装都是软链接,
|
|
2426
|
+
* 只看 isDirectory() 会漏掉它们(社区 #6678 提醒的坑)。P19/P20/P21 共用。 */
|
|
2427
|
+
function listProfilePackages(profileDir) {
|
|
2428
|
+
const out = [];
|
|
2429
|
+
try {
|
|
2430
|
+
for (const e of readdirSync(join(profileDir, 'node_modules'), { withFileTypes: true })) {
|
|
2431
|
+
if (!e.name || e.name.startsWith('.') || e.name === '.bin') continue;
|
|
2432
|
+
if (e.name.startsWith('@')) {
|
|
2433
|
+
try {
|
|
2434
|
+
for (const sub of readdirSync(join(profileDir, 'node_modules', e.name), { withFileTypes: true })) {
|
|
2435
|
+
if (!sub.name || sub.name.startsWith('.')) continue;
|
|
2436
|
+
if (sub.isDirectory() || sub.isSymbolicLink()) out.push({ name: `${e.name}/${sub.name}`, dir: join(profileDir, 'node_modules', e.name, sub.name) });
|
|
2437
|
+
}
|
|
2438
|
+
} catch { /* 跳过不可读 scope */ }
|
|
2439
|
+
} else if (e.isDirectory() || e.isSymbolicLink()) {
|
|
2440
|
+
out.push({ name: e.name, dir: join(profileDir, 'node_modules', e.name) });
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
} catch { /* 无 node_modules */ }
|
|
2444
|
+
return out;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2335
2447
|
/** 解析 profile 的启动列表与各 bundle 的 entry(含用户 patch 的 insert),跳过 disabled 与已隔离项。 */
|
|
2336
2448
|
function collectBootEntries(profileDir) {
|
|
2337
2449
|
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": [
|
|
7
|
+
"CHANGELOG.md",
|
|
7
8
|
"README.md",
|
|
8
9
|
"README.zh.md",
|
|
9
10
|
"checks.json",
|