@moonquake2004/dsh-doctor 0.4.2 → 0.4.4
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/dsh-doctor.mjs +90 -23
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -707,6 +707,36 @@ function checkProfile(name) {
|
|
|
707
707
|
} catch (e) {
|
|
708
708
|
report('profile', 'installed_bundle', false, `bundle 版本对比异常: ${e.message.slice(0, 60)}`, undefined);
|
|
709
709
|
}
|
|
710
|
+
|
|
711
|
+
// P15:关键文件 BOM 检测(#5176:package.json 被意外加 BOM 头导致 JSON 解析失败)
|
|
712
|
+
// UTF-8 BOM = EF BB BF = '\uFEFF',pnpm/node 解析 JSON 时不认识 BOM → 报错
|
|
713
|
+
const bomTargets = [
|
|
714
|
+
join(dir, 'package.json'),
|
|
715
|
+
join(dir, 'cordis.patch.yml'),
|
|
716
|
+
join(dir, 'settings.yaml'),
|
|
717
|
+
];
|
|
718
|
+
// 加上 config/*.json
|
|
719
|
+
const configDir = join(dir, 'config');
|
|
720
|
+
if (existsSync(configDir)) {
|
|
721
|
+
try {
|
|
722
|
+
for (const f of readdirSync(configDir)) {
|
|
723
|
+
if (f.endsWith('.json')) bomTargets.push(join(configDir, f));
|
|
724
|
+
}
|
|
725
|
+
} catch { /* skip */ }
|
|
726
|
+
}
|
|
727
|
+
const bomFiles = [];
|
|
728
|
+
for (const f of bomTargets) {
|
|
729
|
+
if (!existsSync(f)) continue;
|
|
730
|
+
try {
|
|
731
|
+
const head = readFileSync(f, 'utf8').slice(0, 1);
|
|
732
|
+
if (head === '\uFEFF') bomFiles.push(f.replace(dir + '/', ''));
|
|
733
|
+
} catch { /* skip */ }
|
|
734
|
+
}
|
|
735
|
+
if (bomFiles.length > 0) {
|
|
736
|
+
report('profile', 'P15', false, `检测到 BOM 头(#5176:JSON/YAML 解析将失败): ${bomFiles.join(', ')}`, '用文本编辑器打开文件,删除首字符(BOM/U+FEFF)后保存;或运行: sed -i "" "1s/^\xEF\xBB\xBF//" <file>');
|
|
737
|
+
} else {
|
|
738
|
+
report('profile', 'P15', true, '关键文件无 BOM 头', undefined);
|
|
739
|
+
}
|
|
710
740
|
}
|
|
711
741
|
|
|
712
742
|
/* ================= session ================= */
|
|
@@ -945,6 +975,7 @@ catalogSeverity.set('E3-node', 'warn');
|
|
|
945
975
|
catalogSeverity.set('installed_bundle', 'warn');
|
|
946
976
|
catalogSeverity.set('P13', 'warn');
|
|
947
977
|
catalogSeverity.set('P14', 'warn');
|
|
978
|
+
catalogSeverity.set('P15', 'error');
|
|
948
979
|
|
|
949
980
|
function bundledCatalog() {
|
|
950
981
|
const p = new URL('./checks.json', import.meta.url);
|
|
@@ -1301,49 +1332,71 @@ async function run() {
|
|
|
1301
1332
|
}
|
|
1302
1333
|
|
|
1303
1334
|
// 安全检查(--security):导入 dsh-security 运行安全检查,合并到 results
|
|
1304
|
-
|
|
1335
|
+
// --security-only 隐含启用安全检查(复审修复:此前单独使用 = 静默空跑)
|
|
1336
|
+
const securityEnabled = process.argv.includes('--security') || securityOnly;
|
|
1305
1337
|
let securityMeta = { enabled: false, summary: {} };
|
|
1306
1338
|
if (securityEnabled) {
|
|
1307
1339
|
try {
|
|
1308
|
-
// 尝试从 profile node_modules 或全局安装导入 dsh-security
|
|
1340
|
+
// 尝试从 profile node_modules 或全局安装导入 dsh-security;
|
|
1341
|
+
// 开发调试可用 DSH_SECURITY_SRC 指向工作区源码(复审修复:移除硬编码个人路径)
|
|
1309
1342
|
let secMod;
|
|
1310
1343
|
const profileDir = (() => { try { return resolveProfile(profileArg); } catch { return null; } })();
|
|
1311
1344
|
const secCandidates = [
|
|
1345
|
+
process.env.DSH_SECURITY_SRC,
|
|
1312
1346
|
profileDir ? join(profileDir, 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs') : null,
|
|
1313
1347
|
join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs'),
|
|
1314
|
-
'/Users/waterfly/dsh工作区/dsh-security/src/index.mjs',
|
|
1315
1348
|
].filter(Boolean);
|
|
1316
1349
|
for (const candidate of secCandidates) {
|
|
1317
1350
|
if (existsSync(candidate)) { secMod = await import(candidate); break; }
|
|
1318
1351
|
}
|
|
1319
1352
|
if (secMod) {
|
|
1320
1353
|
const registry = await secMod.createDefaultRegistry();
|
|
1354
|
+
// 注入 ~/.dsh/security.json 配置(旧版 dsh-security 无此能力时静默跳过)
|
|
1355
|
+
try {
|
|
1356
|
+
if (secMod.loadConfig && typeof registry.setConfig === 'function') {
|
|
1357
|
+
registry.setConfig(secMod.loadConfig(HOME));
|
|
1358
|
+
}
|
|
1359
|
+
} catch { /* 配置损坏不影响检查 */ }
|
|
1321
1360
|
// 获取最新会话文件(供 SR*/SS* 检查使用)
|
|
1361
|
+
// 复审修复:对齐 S11 的两层布局 sessions/<user>/<session>/session.jsonl[.zstd],
|
|
1362
|
+
// 按 mtime 取最新;旧实现只扫顶层 *.jsonl,真实部署下永远返回 null → 运行时层全跳过
|
|
1322
1363
|
const findLatestSession = () => {
|
|
1323
1364
|
if (sessionArg) return sessionArg;
|
|
1324
1365
|
try {
|
|
1325
|
-
const
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1366
|
+
const root = join(HOME, 'sessions');
|
|
1367
|
+
const candidates = [];
|
|
1368
|
+
for (const u of readdirSync(root)) {
|
|
1369
|
+
const sd = join(root, u);
|
|
1370
|
+
let subs = [];
|
|
1371
|
+
try { subs = readdirSync(sd); } catch { continue; }
|
|
1372
|
+
for (const s of subs) {
|
|
1373
|
+
const zstdPath = join(sd, s, 'session.jsonl.zstd');
|
|
1374
|
+
const plainPath = join(sd, s, 'session.jsonl');
|
|
1375
|
+
const f = existsSync(zstdPath) ? zstdPath : (existsSync(plainPath) ? plainPath : null);
|
|
1376
|
+
if (!f) continue;
|
|
1377
|
+
try { candidates.push({ f, m: statSync(f).mtimeMs }); } catch { /* race */ }
|
|
1378
|
+
}
|
|
1379
|
+
// 兼容直接放在用户目录下的散文件
|
|
1380
|
+
const loose = join(sd, 'session.jsonl');
|
|
1381
|
+
if (existsSync(loose)) { try { candidates.push({ f: loose, m: statSync(loose).mtimeMs }); } catch { /* race */ } }
|
|
1382
|
+
}
|
|
1383
|
+
candidates.sort((a, b) => b.m - a.m);
|
|
1384
|
+
return candidates.length ? candidates[0].f : null;
|
|
1328
1385
|
} catch { return null; }
|
|
1329
1386
|
};
|
|
1330
1387
|
|
|
1331
1388
|
const { results: secResults, exitCode: secExit, summary: secSummary } = await registry.runAll(
|
|
1332
1389
|
(check) => {
|
|
1333
|
-
//
|
|
1334
|
-
if (check.phase === 'pre-install' || check.phase === 'lifecycle') {
|
|
1335
|
-
return profileDir || profileArg;
|
|
1336
|
-
}
|
|
1337
|
-
// SR*/SS* 检查用会话文件
|
|
1390
|
+
// SR*/SS* 运行时会话检查用会话文件
|
|
1338
1391
|
if (check.id && (check.id.startsWith('SR') || check.id.startsWith('SS'))) {
|
|
1339
|
-
return findLatestSession()
|
|
1392
|
+
return findLatestSession();
|
|
1340
1393
|
}
|
|
1341
|
-
//
|
|
1394
|
+
// 其余(SP*/SL*/EXT-*)用 profile 目录
|
|
1342
1395
|
return profileDir || profileArg;
|
|
1343
1396
|
}
|
|
1344
1397
|
);
|
|
1345
1398
|
for (const r of secResults) {
|
|
1346
|
-
results.push({ section: 'security', id: r.id, ok: r.ok, detail: r.detail, fix: r.fix, severity: r.severity, skip:
|
|
1399
|
+
results.push({ section: 'security', id: r.id, ok: r.ok, detail: r.detail, fix: r.fix, severity: r.severity, skip: !!r.skipped });
|
|
1347
1400
|
}
|
|
1348
1401
|
securityMeta = { enabled: true, summary: secSummary, exitCode: secExit };
|
|
1349
1402
|
} else {
|
|
@@ -1355,15 +1408,25 @@ async function run() {
|
|
|
1355
1408
|
}
|
|
1356
1409
|
|
|
1357
1410
|
// 退出码只计内置失败 + catalog 中 severity=error 的失败;warn 失败提示但不改退出码
|
|
1358
|
-
//
|
|
1359
|
-
|
|
1411
|
+
// 安全检查走独立通道:secExit 由 dsh-security 按 severity 计算(CRITICAL→2, HIGH→1, 其余 0),
|
|
1412
|
+
// 不参与 bad[] 与信封 baseExit——复审修复:此前任何安全失败(哪怕 LOW 级关注点)都会把退出码抬到 1/2,
|
|
1413
|
+
// 违反 dsh-security 契约「MEDIUM 及以下不影响退出码」与本函数上方注释的声明。
|
|
1414
|
+
const bad = results.filter((r) => r.section !== 'security' && !r.ok && catalogSeverity.get(r.id) !== 'warn');
|
|
1360
1415
|
const secExit = securityMeta.exitCode ?? 0;
|
|
1361
1416
|
if (jsonOut && process.argv.includes('--envelope')) {
|
|
1362
1417
|
// v1 契约信封(dsh doctor 规格,zoahdev/doctor 对齐):status 小写 + 退出码 0/1/2
|
|
1363
|
-
const st = (r) => (r.skip ? 'skip' : (!r.ok ? (catalogSeverity.get(r.id) === 'warn' ? 'warn' : 'fail') : 'pass'));
|
|
1418
|
+
const st = (r) => (r.skip ? 'skip' : (!r.ok ? (((r.section === 'security') ? r.severity !== 'critical' : catalogSeverity.get(r.id) === 'warn') ? 'warn' : 'fail') : 'pass'));
|
|
1364
1419
|
const summary = { pass: 0, warn: 0, fail: 0, skip: 0 }; // skip 常驻(v1 词汇表 r5:#1719),r5 后 P12 会在未装 bundle 时实际触发
|
|
1365
1420
|
const checks = results.map((r) => { summary[st(r)]++; return { name: r.id, status: st(r), detail: r.detail, ...(r.severity ? { severity: r.severity } : {}), ...(r.section === 'security' ? { section: 'security' } : {}) }; });
|
|
1366
|
-
|
|
1421
|
+
// baseExit 只统计非安全项;安全项对退出码的贡献由 secExit 独立承载
|
|
1422
|
+
let baseFail = 0; let baseWarn = 0;
|
|
1423
|
+
for (const r of results) {
|
|
1424
|
+
if (r.section === 'security') continue;
|
|
1425
|
+
const s = st(r);
|
|
1426
|
+
if (s === 'fail') baseFail++;
|
|
1427
|
+
else if (s === 'warn') baseWarn++;
|
|
1428
|
+
}
|
|
1429
|
+
const baseExit = baseFail > 0 ? 2 : baseWarn > 0 ? 1 : 0;
|
|
1367
1430
|
const exitCode = Math.max(baseExit, secExit);
|
|
1368
1431
|
const out = {
|
|
1369
1432
|
schema: 'dsh-doctor/v1',
|
|
@@ -1387,10 +1450,13 @@ async function run() {
|
|
|
1387
1450
|
const ordered = [...results].sort((a, b) => (sectionOrder[a.section] ?? 9) - (sectionOrder[b.section] ?? 9));
|
|
1388
1451
|
let lastSection = '';
|
|
1389
1452
|
for (const r of ordered) {
|
|
1390
|
-
if (r.section !== lastSection) { console.log(`\n== ${r.section.toUpperCase()} ==`); lastSection = r.section; }
|
|
1453
|
+
if (r.section !== lastSection) { console.log(`\n== ${r.section === 'security' ? '🔒 安全' : r.section.toUpperCase()} ==`); lastSection = r.section; }
|
|
1391
1454
|
const sev = catalogSeverity.get(r.id);
|
|
1392
|
-
|
|
1393
|
-
|
|
1455
|
+
// 安全检查:skip 显示 ⊖;critical/high 失败 ✗;medium 及以下失败 ⚠(不影响退出码)
|
|
1456
|
+
const mark = r.skip ? '⊖'
|
|
1457
|
+
: (!r.ok ? (((r.section === 'security' && r.severity !== 'critical' && r.severity !== 'high') || sev === 'warn') ? '⚠' : '✗')
|
|
1458
|
+
: '✓');
|
|
1459
|
+
console.log(` ${mark} [${r.id}]${r.severity ? `(${r.severity}${r.skip ? '/skip' : ''})` : ''} ${r.detail}${r.src === 'catalog' ? ' [目录]' : ''}`);
|
|
1394
1460
|
if (!r.ok && r.fix) console.log(` ↳ 修复: ${r.fix}`);
|
|
1395
1461
|
}
|
|
1396
1462
|
if (updateInfo.available && !updateInfo.applied) {
|
|
@@ -1398,9 +1464,10 @@ async function run() {
|
|
|
1398
1464
|
} else if (updateInfo.applied) {
|
|
1399
1465
|
console.log(`\n✓ ${updateInfo.applied}`);
|
|
1400
1466
|
}
|
|
1401
|
-
console.log(`\n${bad.length === 0 ? '✓ 全部通过' : `✗ ${bad.length}
|
|
1467
|
+
console.log(`\n${(bad.length === 0 && secExit === 0) ? '✓ 全部通过' : `✗ ${bad.length} 个内置问题${secExit > 0 ? ` + 安全 ${secExit === 2 ? 'CRITICAL' : 'HIGH'} 级失败` : ''}`}(profile=${profileArg},目录=${catalogMeta.source},${catalogMeta.checks} 条)`);
|
|
1402
1468
|
}
|
|
1403
|
-
|
|
1469
|
+
// 最终退出码:内置失败 → 1;安全 HIGH → 1、CRITICAL → 2(取 max)
|
|
1470
|
+
process.exit(Math.max(bad.length > 0 ? 1 : 0, secExit));
|
|
1404
1471
|
}
|
|
1405
1472
|
|
|
1406
1473
|
// 直接执行(CLI:根目录薄封装、plugin 本体、npm bin 均可);被 import(测试/宿主)时不自动运行
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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": [
|