@moonquake2004/dsh-doctor 0.4.1 → 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.
- package/client/client.js +3 -1
- package/dsh-doctor.mjs +138 -33
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -23,6 +23,7 @@ const zh = {
|
|
|
23
23
|
sectionEnv: '环境',
|
|
24
24
|
sectionProfile: 'Profile',
|
|
25
25
|
sectionSession: '会话',
|
|
26
|
+
sectionSecurity: '🔒 安全',
|
|
26
27
|
fix: '修复',
|
|
27
28
|
quarantineHint: '隔离建议(手动执行,勿自动)',
|
|
28
29
|
error: '诊断失败:{msg}',
|
|
@@ -37,6 +38,7 @@ const en = {
|
|
|
37
38
|
sectionEnv: 'Environment',
|
|
38
39
|
sectionProfile: 'Profile',
|
|
39
40
|
sectionSession: 'Session',
|
|
41
|
+
sectionSecurity: '🔒 Security',
|
|
40
42
|
fix: 'Fix',
|
|
41
43
|
quarantineHint: 'Quarantine suggestion (run manually, never auto)',
|
|
42
44
|
error: 'Doctor failed: {msg}',
|
|
@@ -86,7 +88,7 @@ function DoctorSection(props) {
|
|
|
86
88
|
finally { setRunning(false) }
|
|
87
89
|
}, [])
|
|
88
90
|
useEffect(() => { injectStyles(); run() }, [run])
|
|
89
|
-
const sectionName = (s) => ({ env: L.sectionEnv, profile: L.sectionProfile, session: L.sectionSession }[s] || s)
|
|
91
|
+
const sectionName = (s) => ({ env: L.sectionEnv, profile: L.sectionProfile, session: L.sectionSession, security: L.sectionSecurity }[s] || s)
|
|
90
92
|
return h('div', { className: 'dshd-wrap' },
|
|
91
93
|
h('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } },
|
|
92
94
|
h('button', { className: 'dshd-btn', onClick: run, disabled: running }, running ? L.running : L.run)),
|
package/dsh-doctor.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
|
61
61
|
const HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
62
62
|
const results = []; // { section, id, ok, detail, fix? }
|
|
63
63
|
const jsonOut = process.argv.includes('--json');
|
|
64
|
+
const securityOnly = process.argv.includes('--security-only');
|
|
64
65
|
const only = process.argv
|
|
65
66
|
.filter((a) => a.startsWith('--profile') || a.startsWith('--session') || a === '--env')
|
|
66
67
|
.map((a) => a.startsWith('--') ? a.slice(2) : a);
|
|
@@ -1262,45 +1263,141 @@ async function run() {
|
|
|
1262
1263
|
process.exit(1);
|
|
1263
1264
|
}
|
|
1264
1265
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1266
|
+
// --security-only 跳过非安全检查
|
|
1267
|
+
if (!securityOnly) {
|
|
1268
|
+
try { checkEnv(); } catch (e) { report('env', 'E0', false, `env 检查异常: ${e.message.slice(0, 80)}`); }
|
|
1269
|
+
try { await checkPort3080(); } catch (e) { report('env', 'E10-port-3080', false, `端口检查异常: ${e.message.slice(0, 60)}`); }
|
|
1270
|
+
try { checkProfile(profileArg); } catch (e) { report('profile', 'P0', false, `profile 检查异常: ${e.message.slice(0, 100)}`); }
|
|
1271
|
+
try { checkSession(sessionArg); } catch (e) { report('session', 'S0', false, `session 检查异常: ${e.message.slice(0, 100)}`); }
|
|
1272
|
+
try { scanAllSessions(); } catch (e) { report('session', 'S11', false, `全会话扫描异常: ${e.message.slice(0, 100)}`); }
|
|
1273
|
+
}
|
|
1270
1274
|
|
|
1271
|
-
// 远程检查目录(层 A):内置检查之后追加执行;--no-catalog
|
|
1275
|
+
// 远程检查目录(层 A):内置检查之后追加执行;--no-catalog 只走内置副本;--security-only 跳过
|
|
1272
1276
|
let catalogMeta = { source: 'none', checks: 0 };
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1277
|
+
if (!securityOnly) {
|
|
1278
|
+
try {
|
|
1279
|
+
const catalog = await loadCatalog({ noRemote: process.argv.includes('--no-catalog'), fetchImpl: typeof fetch === 'function' ? fetch : undefined });
|
|
1280
|
+
catalogMeta = { source: catalog.source, checks: catalog.checks.length };
|
|
1281
|
+
const profileDir = (() => { try { return resolveProfile(profileArg); } catch { return null; } })();
|
|
1282
|
+
if (catalog.checks.length && profileDir) checkCatalog({ home: HOME, profile: profileArg, profileDir }, catalog);
|
|
1283
|
+
else if (catalog.checks.length) report('catalog', 'C0', true, `profile 无效(${profileArg}),目录检查跳过(${catalog.source})`, undefined, 'catalog');
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
catalogMeta = { source: 'error', checks: 0, error: e.message.slice(0, 80) };
|
|
1286
|
+
}
|
|
1281
1287
|
}
|
|
1282
1288
|
|
|
1283
|
-
// 层 B:版本检查与更新(--no-catalog 同时禁用网络检查;--update 手动更新;DSH_DOCTOR_AUTO_UPDATE=1
|
|
1289
|
+
// 层 B:版本检查与更新(--no-catalog 同时禁用网络检查;--update 手动更新;DSH_DOCTOR_AUTO_UPDATE=1 自动;--security-only 跳过)
|
|
1284
1290
|
const noRemote = process.argv.includes('--no-catalog');
|
|
1285
1291
|
let updateInfo = { current: localVersion(), latest: null, available: false };
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1292
|
+
if (!securityOnly) {
|
|
1293
|
+
try {
|
|
1294
|
+
updateInfo = await checkForUpdate({ noRemote, fetchImpl: typeof fetch === 'function' ? fetch : undefined });
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
updateInfo = { current: localVersion(), latest: null, available: false, error: e.message.slice(0, 60) };
|
|
1297
|
+
}
|
|
1298
|
+
if (process.argv.includes('--update') || (process.env.DSH_DOCTOR_AUTO_UPDATE === '1' && updateInfo.available)) {
|
|
1299
|
+
updateInfo.applied = runUpdate();
|
|
1300
|
+
}
|
|
1290
1301
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1302
|
+
|
|
1303
|
+
// 安全检查(--security):导入 dsh-security 运行安全检查,合并到 results
|
|
1304
|
+
// --security-only 隐含启用安全检查(复审修复:此前单独使用 = 静默空跑)
|
|
1305
|
+
const securityEnabled = process.argv.includes('--security') || securityOnly;
|
|
1306
|
+
let securityMeta = { enabled: false, summary: {} };
|
|
1307
|
+
if (securityEnabled) {
|
|
1308
|
+
try {
|
|
1309
|
+
// 尝试从 profile node_modules 或全局安装导入 dsh-security;
|
|
1310
|
+
// 开发调试可用 DSH_SECURITY_SRC 指向工作区源码(复审修复:移除硬编码个人路径)
|
|
1311
|
+
let secMod;
|
|
1312
|
+
const profileDir = (() => { try { return resolveProfile(profileArg); } catch { return null; } })();
|
|
1313
|
+
const secCandidates = [
|
|
1314
|
+
process.env.DSH_SECURITY_SRC,
|
|
1315
|
+
profileDir ? join(profileDir, 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs') : null,
|
|
1316
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs'),
|
|
1317
|
+
].filter(Boolean);
|
|
1318
|
+
for (const candidate of secCandidates) {
|
|
1319
|
+
if (existsSync(candidate)) { secMod = await import(candidate); break; }
|
|
1320
|
+
}
|
|
1321
|
+
if (secMod) {
|
|
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 { /* 配置损坏不影响检查 */ }
|
|
1329
|
+
// 获取最新会话文件(供 SR*/SS* 检查使用)
|
|
1330
|
+
// 复审修复:对齐 S11 的两层布局 sessions/<user>/<session>/session.jsonl[.zstd],
|
|
1331
|
+
// 按 mtime 取最新;旧实现只扫顶层 *.jsonl,真实部署下永远返回 null → 运行时层全跳过
|
|
1332
|
+
const findLatestSession = () => {
|
|
1333
|
+
if (sessionArg) return sessionArg;
|
|
1334
|
+
try {
|
|
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;
|
|
1354
|
+
} catch { return null; }
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const { results: secResults, exitCode: secExit, summary: secSummary } = await registry.runAll(
|
|
1358
|
+
(check) => {
|
|
1359
|
+
// SR*/SS* 运行时会话检查用会话文件
|
|
1360
|
+
if (check.id && (check.id.startsWith('SR') || check.id.startsWith('SS'))) {
|
|
1361
|
+
return findLatestSession();
|
|
1362
|
+
}
|
|
1363
|
+
// 其余(SP*/SL*/EXT-*)用 profile 目录
|
|
1364
|
+
return profileDir || profileArg;
|
|
1365
|
+
}
|
|
1366
|
+
);
|
|
1367
|
+
for (const r of secResults) {
|
|
1368
|
+
results.push({ section: 'security', id: r.id, ok: r.ok, detail: r.detail, fix: r.fix, severity: r.severity, skip: !!r.skipped });
|
|
1369
|
+
}
|
|
1370
|
+
securityMeta = { enabled: true, summary: secSummary, exitCode: secExit };
|
|
1371
|
+
} else {
|
|
1372
|
+
securityMeta = { enabled: true, error: 'dsh-security not found', summary: {} };
|
|
1373
|
+
}
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
securityMeta = { enabled: true, error: e.message.slice(0, 80), summary: {} };
|
|
1376
|
+
}
|
|
1293
1377
|
}
|
|
1294
1378
|
|
|
1295
1379
|
// 退出码只计内置失败 + catalog 中 severity=error 的失败;warn 失败提示但不改退出码
|
|
1296
|
-
|
|
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');
|
|
1384
|
+
const secExit = securityMeta.exitCode ?? 0;
|
|
1297
1385
|
if (jsonOut && process.argv.includes('--envelope')) {
|
|
1298
1386
|
// v1 契约信封(dsh doctor 规格,zoahdev/doctor 对齐):status 小写 + 退出码 0/1/2
|
|
1299
|
-
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'));
|
|
1300
1388
|
const summary = { pass: 0, warn: 0, fail: 0, skip: 0 }; // skip 常驻(v1 词汇表 r5:#1719),r5 后 P12 会在未装 bundle 时实际触发
|
|
1301
|
-
const checks = results.map((r) => { summary[st(r)]++; return { name: r.id, status: st(r), detail: r.detail }; });
|
|
1302
|
-
|
|
1303
|
-
|
|
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' } : {}) }; });
|
|
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;
|
|
1399
|
+
const exitCode = Math.max(baseExit, secExit);
|
|
1400
|
+
const out = {
|
|
1304
1401
|
schema: 'dsh-doctor/v1',
|
|
1305
1402
|
tool: 'dsh-doctor',
|
|
1306
1403
|
generatedAt: new Date().toISOString(),
|
|
@@ -1309,19 +1406,26 @@ async function run() {
|
|
|
1309
1406
|
summary,
|
|
1310
1407
|
ok: exitCode === 0,
|
|
1311
1408
|
checks,
|
|
1312
|
-
}
|
|
1409
|
+
};
|
|
1410
|
+
if (securityMeta.enabled) {
|
|
1411
|
+
out.security = { enabled: true, summary: securityMeta.summary, ...(securityMeta.error ? { error: securityMeta.error } : {}) };
|
|
1412
|
+
}
|
|
1413
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1313
1414
|
process.exit(exitCode);
|
|
1314
1415
|
} else if (jsonOut) {
|
|
1315
|
-
console.log(JSON.stringify({ ok: bad.length === 0, checks: results, catalog: catalogMeta, update: updateInfo }, null, 2));
|
|
1416
|
+
console.log(JSON.stringify({ ok: bad.length === 0 && secExit === 0, checks: results, catalog: catalogMeta, update: updateInfo, ...(securityMeta.enabled ? { security: securityMeta } : {}) }, null, 2));
|
|
1316
1417
|
} else {
|
|
1317
1418
|
const sectionOrder = { env: 0, profile: 1, session: 2, catalog: 3 };
|
|
1318
1419
|
const ordered = [...results].sort((a, b) => (sectionOrder[a.section] ?? 9) - (sectionOrder[b.section] ?? 9));
|
|
1319
1420
|
let lastSection = '';
|
|
1320
1421
|
for (const r of ordered) {
|
|
1321
|
-
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; }
|
|
1322
1423
|
const sev = catalogSeverity.get(r.id);
|
|
1323
|
-
|
|
1324
|
-
|
|
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' ? ' [目录]' : ''}`);
|
|
1325
1429
|
if (!r.ok && r.fix) console.log(` ↳ 修复: ${r.fix}`);
|
|
1326
1430
|
}
|
|
1327
1431
|
if (updateInfo.available && !updateInfo.applied) {
|
|
@@ -1329,9 +1433,10 @@ async function run() {
|
|
|
1329
1433
|
} else if (updateInfo.applied) {
|
|
1330
1434
|
console.log(`\n✓ ${updateInfo.applied}`);
|
|
1331
1435
|
}
|
|
1332
|
-
console.log(`\n${bad.length === 0 ? '✓ 全部通过' : `✗ ${bad.length}
|
|
1436
|
+
console.log(`\n${(bad.length === 0 && secExit === 0) ? '✓ 全部通过' : `✗ ${bad.length} 个内置问题${secExit > 0 ? ` + 安全 ${secExit === 2 ? 'CRITICAL' : 'HIGH'} 级失败` : ''}`}(profile=${profileArg},目录=${catalogMeta.source},${catalogMeta.checks} 条)`);
|
|
1333
1437
|
}
|
|
1334
|
-
|
|
1438
|
+
// 最终退出码:内置失败 → 1;安全 HIGH → 1、CRITICAL → 2(取 max)
|
|
1439
|
+
process.exit(Math.max(bad.length > 0 ? 1 : 0, secExit));
|
|
1335
1440
|
}
|
|
1336
1441
|
|
|
1337
1442
|
// 直接执行(CLI:根目录薄封装、plugin 本体、npm bin 均可);被 import(测试/宿主)时不自动运行
|
package/lib/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export function apply(ctx, config) {
|
|
|
35
35
|
*/
|
|
36
36
|
function runChecks(profile, sessionPath) {
|
|
37
37
|
return new Promise((resolvePromise) => {
|
|
38
|
-
const args = ['--json'];
|
|
38
|
+
const args = ['--json', '--security'];
|
|
39
39
|
if (profile) args.push('--profile', profile);
|
|
40
40
|
if (sessionPath) args.push('--session', sessionPath);
|
|
41
41
|
const child = spawn(process.execPath, [SCRIPT, ...args], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.4.
|
|
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": [
|