@moonquake2004/dsh-doctor 0.4.2 → 0.4.3

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 +59 -23
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -1301,49 +1301,71 @@ async function run() {
1301
1301
  }
1302
1302
 
1303
1303
  // 安全检查(--security):导入 dsh-security 运行安全检查,合并到 results
1304
- const securityEnabled = process.argv.includes('--security');
1304
+ // --security-only 隐含启用安全检查(复审修复:此前单独使用 = 静默空跑)
1305
+ const securityEnabled = process.argv.includes('--security') || securityOnly;
1305
1306
  let securityMeta = { enabled: false, summary: {} };
1306
1307
  if (securityEnabled) {
1307
1308
  try {
1308
- // 尝试从 profile node_modules 或全局安装导入 dsh-security
1309
+ // 尝试从 profile node_modules 或全局安装导入 dsh-security
1310
+ // 开发调试可用 DSH_SECURITY_SRC 指向工作区源码(复审修复:移除硬编码个人路径)
1309
1311
  let secMod;
1310
1312
  const profileDir = (() => { try { return resolveProfile(profileArg); } catch { return null; } })();
1311
1313
  const secCandidates = [
1314
+ process.env.DSH_SECURITY_SRC,
1312
1315
  profileDir ? join(profileDir, 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs') : null,
1313
1316
  join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs'),
1314
- '/Users/waterfly/dsh工作区/dsh-security/src/index.mjs',
1315
1317
  ].filter(Boolean);
1316
1318
  for (const candidate of secCandidates) {
1317
1319
  if (existsSync(candidate)) { secMod = await import(candidate); break; }
1318
1320
  }
1319
1321
  if (secMod) {
1320
1322
  const registry = await secMod.createDefaultRegistry();
1323
+ // 注入 ~/.dsh/security.json 配置(旧版 dsh-security 无此能力时静默跳过)
1324
+ try {
1325
+ if (secMod.loadConfig && typeof registry.setConfig === 'function') {
1326
+ registry.setConfig(secMod.loadConfig(HOME));
1327
+ }
1328
+ } catch { /* 配置损坏不影响检查 */ }
1321
1329
  // 获取最新会话文件(供 SR*/SS* 检查使用)
1330
+ // 复审修复:对齐 S11 的两层布局 sessions/<user>/<session>/session.jsonl[.zstd],
1331
+ // 按 mtime 取最新;旧实现只扫顶层 *.jsonl,真实部署下永远返回 null → 运行时层全跳过
1322
1332
  const findLatestSession = () => {
1323
1333
  if (sessionArg) return sessionArg;
1324
1334
  try {
1325
- const sDir = join(HOME, 'sessions');
1326
- const latest = readdirSync(sDir).filter(f => f.endsWith('.jsonl')).sort().pop();
1327
- return latest ? join(sDir, latest) : null;
1335
+ const root = join(HOME, 'sessions');
1336
+ const candidates = [];
1337
+ for (const u of readdirSync(root)) {
1338
+ const sd = join(root, u);
1339
+ let subs = [];
1340
+ try { subs = readdirSync(sd); } catch { continue; }
1341
+ for (const s of subs) {
1342
+ const zstdPath = join(sd, s, 'session.jsonl.zstd');
1343
+ const plainPath = join(sd, s, 'session.jsonl');
1344
+ const f = existsSync(zstdPath) ? zstdPath : (existsSync(plainPath) ? plainPath : null);
1345
+ if (!f) continue;
1346
+ try { candidates.push({ f, m: statSync(f).mtimeMs }); } catch { /* race */ }
1347
+ }
1348
+ // 兼容直接放在用户目录下的散文件
1349
+ const loose = join(sd, 'session.jsonl');
1350
+ if (existsSync(loose)) { try { candidates.push({ f: loose, m: statSync(loose).mtimeMs }); } catch { /* race */ } }
1351
+ }
1352
+ candidates.sort((a, b) => b.m - a.m);
1353
+ return candidates.length ? candidates[0].f : null;
1328
1354
  } catch { return null; }
1329
1355
  };
1330
1356
 
1331
1357
  const { results: secResults, exitCode: secExit, summary: secSummary } = await registry.runAll(
1332
1358
  (check) => {
1333
- // 静态检查用 profile 目录
1334
- if (check.phase === 'pre-install' || check.phase === 'lifecycle') {
1335
- return profileDir || profileArg;
1336
- }
1337
- // SR*/SS* 检查用会话文件
1359
+ // SR*/SS* 运行时会话检查用会话文件
1338
1360
  if (check.id && (check.id.startsWith('SR') || check.id.startsWith('SS'))) {
1339
- return findLatestSession() || null;
1361
+ return findLatestSession();
1340
1362
  }
1341
- // 默认用 profile 目录
1363
+ // 其余(SP*/SL*/EXT-*)用 profile 目录
1342
1364
  return profileDir || profileArg;
1343
1365
  }
1344
1366
  );
1345
1367
  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: false });
1368
+ results.push({ section: 'security', id: r.id, ok: r.ok, detail: r.detail, fix: r.fix, severity: r.severity, skip: !!r.skipped });
1347
1369
  }
1348
1370
  securityMeta = { enabled: true, summary: secSummary, exitCode: secExit };
1349
1371
  } else {
@@ -1355,15 +1377,25 @@ async function run() {
1355
1377
  }
1356
1378
 
1357
1379
  // 退出码只计内置失败 + catalog 中 severity=error 的失败;warn 失败提示但不改退出码
1358
- // 安全检查退出码:CRITICAL→2, HIGH→1(取 max)
1359
- const bad = results.filter((r) => !r.ok && catalogSeverity.get(r.id) !== 'warn');
1380
+ // 安全检查走独立通道:secExit 由 dsh-security 按 severity 计算(CRITICAL→2, HIGH→1, 其余 0),
1381
+ // 不参与 bad[] 与信封 baseExit——复审修复:此前任何安全失败(哪怕 LOW 级关注点)都会把退出码抬到 1/2,
1382
+ // 违反 dsh-security 契约「MEDIUM 及以下不影响退出码」与本函数上方注释的声明。
1383
+ const bad = results.filter((r) => r.section !== 'security' && !r.ok && catalogSeverity.get(r.id) !== 'warn');
1360
1384
  const secExit = securityMeta.exitCode ?? 0;
1361
1385
  if (jsonOut && process.argv.includes('--envelope')) {
1362
1386
  // 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'));
1387
+ const st = (r) => (r.skip ? 'skip' : (!r.ok ? (((r.section === 'security') ? r.severity !== 'critical' : catalogSeverity.get(r.id) === 'warn') ? 'warn' : 'fail') : 'pass'));
1364
1388
  const summary = { pass: 0, warn: 0, fail: 0, skip: 0 }; // skip 常驻(v1 词汇表 r5:#1719),r5 后 P12 会在未装 bundle 时实际触发
1365
1389
  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
- const baseExit = summary.fail > 0 ? 2 : summary.warn > 0 ? 1 : 0;
1390
+ // baseExit 只统计非安全项;安全项对退出码的贡献由 secExit 独立承载
1391
+ let baseFail = 0; let baseWarn = 0;
1392
+ for (const r of results) {
1393
+ if (r.section === 'security') continue;
1394
+ const s = st(r);
1395
+ if (s === 'fail') baseFail++;
1396
+ else if (s === 'warn') baseWarn++;
1397
+ }
1398
+ const baseExit = baseFail > 0 ? 2 : baseWarn > 0 ? 1 : 0;
1367
1399
  const exitCode = Math.max(baseExit, secExit);
1368
1400
  const out = {
1369
1401
  schema: 'dsh-doctor/v1',
@@ -1387,10 +1419,13 @@ async function run() {
1387
1419
  const ordered = [...results].sort((a, b) => (sectionOrder[a.section] ?? 9) - (sectionOrder[b.section] ?? 9));
1388
1420
  let lastSection = '';
1389
1421
  for (const r of ordered) {
1390
- if (r.section !== lastSection) { console.log(`\n== ${r.section.toUpperCase()} ==`); lastSection = r.section; }
1422
+ if (r.section !== lastSection) { console.log(`\n== ${r.section === 'security' ? '🔒 安全' : r.section.toUpperCase()} ==`); lastSection = r.section; }
1391
1423
  const sev = catalogSeverity.get(r.id);
1392
- const mark = !r.ok && sev === 'warn' ? '⚠' : (r.ok ? '✓' : '✗');
1393
- console.log(` ${mark} [${r.id}] ${r.detail}${r.src === 'catalog' ? ' [目录]' : ''}`);
1424
+ // 安全检查:skip 显示 ⊖;critical/high 失败 ✗;medium 及以下失败 ⚠(不影响退出码)
1425
+ const mark = r.skip ? ''
1426
+ : (!r.ok ? (((r.section === 'security' && r.severity !== 'critical' && r.severity !== 'high') || sev === 'warn') ? '⚠' : '✗')
1427
+ : '✓');
1428
+ console.log(` ${mark} [${r.id}]${r.severity ? `(${r.severity}${r.skip ? '/skip' : ''})` : ''} ${r.detail}${r.src === 'catalog' ? ' [目录]' : ''}`);
1394
1429
  if (!r.ok && r.fix) console.log(` ↳ 修复: ${r.fix}`);
1395
1430
  }
1396
1431
  if (updateInfo.available && !updateInfo.applied) {
@@ -1398,9 +1433,10 @@ async function run() {
1398
1433
  } else if (updateInfo.applied) {
1399
1434
  console.log(`\n✓ ${updateInfo.applied}`);
1400
1435
  }
1401
- console.log(`\n${bad.length === 0 ? '✓ 全部通过' : `✗ ${bad.length} 个问题`}(profile=${profileArg},目录=${catalogMeta.source},${catalogMeta.checks} 条)`);
1436
+ console.log(`\n${(bad.length === 0 && secExit === 0) ? '✓ 全部通过' : `✗ ${bad.length} 个内置问题${secExit > 0 ? ` + 安全 ${secExit === 2 ? 'CRITICAL' : 'HIGH'} 级失败` : ''}`}(profile=${profileArg},目录=${catalogMeta.source},${catalogMeta.checks} 条)`);
1402
1437
  }
1403
- process.exit(bad.length === 0 ? 0 : 1);
1438
+ // 最终退出码:内置失败 1;安全 HIGH 1、CRITICAL → 2(取 max)
1439
+ process.exit(Math.max(bad.length > 0 ? 1 : 0, secExit));
1404
1440
  }
1405
1441
 
1406
1442
  // 直接执行(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.2",
3
+ "version": "0.4.3",
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": [