@moonquake2004/dsh-doctor 0.4.0 → 0.4.2
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 +116 -33
- package/lib/index.js +1 -1
- package/package.json +2 -2
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);
|
|
@@ -335,6 +336,10 @@ function checkProfile(name) {
|
|
|
335
336
|
for (const b of bundles) {
|
|
336
337
|
const dir2 = findPkg(b);
|
|
337
338
|
if (!dir2) {
|
|
339
|
+
// installAnchor 为 null(web GUI/无 dsh 锚点)时,宿主侧 bundle 无法验证——
|
|
340
|
+
// 跳过而非误报(#917 core bundles dsh-base/dsh-web-app 由宿主直接提供,
|
|
341
|
+
// 不在 profile node_modules 里,installAnchor 缺失时 findPkg 找不到是正常的)
|
|
342
|
+
if (!installAnchor) continue;
|
|
338
343
|
report('profile', 'P1', false, `bundle 条目 ${b} 无法在安装目录或 profile node_modules 解析(#917/#1377/#880)`, `dsh plugin --profile ${name} add ${b} 或从 dsh.profile.bundles 移除`);
|
|
339
344
|
} else {
|
|
340
345
|
const pkg = JSON.parse(readFileSync(join(dir2, 'package.json'), 'utf8'));
|
|
@@ -343,19 +348,28 @@ function checkProfile(name) {
|
|
|
343
348
|
}
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
|
-
// P2 id
|
|
347
|
-
const
|
|
351
|
+
// P2 id 冲突(#1404 bundle↔user + #2315 bundle↔bundle)
|
|
352
|
+
const bundleIdSources = new Map(); // id → Set<bundle name>
|
|
348
353
|
for (const b of bundles) {
|
|
349
354
|
const dir2 = findPkg(b);
|
|
350
355
|
if (!dir2) continue;
|
|
351
356
|
const pkg = JSON.parse(readFileSync(join(dir2, 'package.json'), 'utf8'));
|
|
352
357
|
const rel = pkg.dsh?.bundle?.patch;
|
|
353
358
|
if (!rel) continue;
|
|
354
|
-
for (const id of readInsertIds(join(dir2, rel)))
|
|
359
|
+
for (const id of readInsertIds(join(dir2, rel))) {
|
|
360
|
+
if (!bundleIdSources.has(id)) bundleIdSources.set(id, new Set());
|
|
361
|
+
bundleIdSources.get(id).add(b);
|
|
362
|
+
}
|
|
355
363
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
364
|
+
// 跨 bundle 冲突:多个 bundle 注册同一 entry id(#2315 dsh-tui↔dsh-web-app agent-presets)
|
|
365
|
+
const crossBundleDup = [...bundleIdSources].filter(([, s]) => s.size > 1).map(([id, s]) => `${id}(${[...s].join(' + ')})`);
|
|
366
|
+
// bundle vs 用户 patch 冲突(#1404)
|
|
367
|
+
const userBundleDup = [...bundleIdSources.keys()].filter((id) => userIds.has(id));
|
|
368
|
+
const p2Issues = [];
|
|
369
|
+
if (crossBundleDup.length) p2Issues.push(`多个 bundle 注册相同 entry id(启动必崩 duplicate loader entry id,#2315): ${crossBundleDup.join('; ')}`);
|
|
370
|
+
if (userBundleDup.length) p2Issues.push(`bundle 与用户 patch 的 id 冲突(启动必崩 duplicate loader entry id,#1404): ${userBundleDup.join(', ')}`);
|
|
371
|
+
if (p2Issues.length) {
|
|
372
|
+
report('profile', 'P2', false, p2Issues.join(' | '), crossBundleDup.length ? '移除冲突 bundle 中的一个(如不兼容的 TUI/standalone 插件误装入 profile),或让上游协商唯一 entry id' : `备份后从 ${patchPath} 删除这些 insert(或运行 check-dsh-profile.mjs 查看详情)`);
|
|
359
373
|
} else {
|
|
360
374
|
report('profile', 'P2', true, '无 bundle/用户 patch id 冲突', undefined);
|
|
361
375
|
}
|
|
@@ -395,7 +409,8 @@ function checkProfile(name) {
|
|
|
395
409
|
const fp = join(topDir, p);
|
|
396
410
|
let st;
|
|
397
411
|
try { st = lstatSync(fp); } catch { continue; }
|
|
398
|
-
if (st.isSymbolicLink()
|
|
412
|
+
if (st.isSymbolicLink()) {
|
|
413
|
+
if (!hostScope) continue; // installAnchor 缺失时无法验证 symlink 指向宿主——跳过(#1697 workaround 已知安全形态)
|
|
399
414
|
try {
|
|
400
415
|
const real = realpathSync(fp);
|
|
401
416
|
const hostPkg = join(hostScope, p);
|
|
@@ -1248,45 +1263,109 @@ async function run() {
|
|
|
1248
1263
|
process.exit(1);
|
|
1249
1264
|
}
|
|
1250
1265
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
+
}
|
|
1256
1274
|
|
|
1257
|
-
// 远程检查目录(层 A):内置检查之后追加执行;--no-catalog
|
|
1275
|
+
// 远程检查目录(层 A):内置检查之后追加执行;--no-catalog 只走内置副本;--security-only 跳过
|
|
1258
1276
|
let catalogMeta = { source: 'none', checks: 0 };
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
+
}
|
|
1267
1287
|
}
|
|
1268
1288
|
|
|
1269
|
-
// 层 B:版本检查与更新(--no-catalog 同时禁用网络检查;--update 手动更新;DSH_DOCTOR_AUTO_UPDATE=1
|
|
1289
|
+
// 层 B:版本检查与更新(--no-catalog 同时禁用网络检查;--update 手动更新;DSH_DOCTOR_AUTO_UPDATE=1 自动;--security-only 跳过)
|
|
1270
1290
|
const noRemote = process.argv.includes('--no-catalog');
|
|
1271
1291
|
let updateInfo = { current: localVersion(), latest: null, available: false };
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
+
}
|
|
1276
1301
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1302
|
+
|
|
1303
|
+
// 安全检查(--security):导入 dsh-security 运行安全检查,合并到 results
|
|
1304
|
+
const securityEnabled = process.argv.includes('--security');
|
|
1305
|
+
let securityMeta = { enabled: false, summary: {} };
|
|
1306
|
+
if (securityEnabled) {
|
|
1307
|
+
try {
|
|
1308
|
+
// 尝试从 profile node_modules 或全局安装导入 dsh-security
|
|
1309
|
+
let secMod;
|
|
1310
|
+
const profileDir = (() => { try { return resolveProfile(profileArg); } catch { return null; } })();
|
|
1311
|
+
const secCandidates = [
|
|
1312
|
+
profileDir ? join(profileDir, 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs') : null,
|
|
1313
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'node_modules', '@moonquake2004', 'dsh-security', 'src', 'index.mjs'),
|
|
1314
|
+
'/Users/waterfly/dsh工作区/dsh-security/src/index.mjs',
|
|
1315
|
+
].filter(Boolean);
|
|
1316
|
+
for (const candidate of secCandidates) {
|
|
1317
|
+
if (existsSync(candidate)) { secMod = await import(candidate); break; }
|
|
1318
|
+
}
|
|
1319
|
+
if (secMod) {
|
|
1320
|
+
const registry = await secMod.createDefaultRegistry();
|
|
1321
|
+
// 获取最新会话文件(供 SR*/SS* 检查使用)
|
|
1322
|
+
const findLatestSession = () => {
|
|
1323
|
+
if (sessionArg) return sessionArg;
|
|
1324
|
+
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;
|
|
1328
|
+
} catch { return null; }
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const { results: secResults, exitCode: secExit, summary: secSummary } = await registry.runAll(
|
|
1332
|
+
(check) => {
|
|
1333
|
+
// 静态检查用 profile 目录
|
|
1334
|
+
if (check.phase === 'pre-install' || check.phase === 'lifecycle') {
|
|
1335
|
+
return profileDir || profileArg;
|
|
1336
|
+
}
|
|
1337
|
+
// SR*/SS* 检查用会话文件
|
|
1338
|
+
if (check.id && (check.id.startsWith('SR') || check.id.startsWith('SS'))) {
|
|
1339
|
+
return findLatestSession() || null;
|
|
1340
|
+
}
|
|
1341
|
+
// 默认用 profile 目录
|
|
1342
|
+
return profileDir || profileArg;
|
|
1343
|
+
}
|
|
1344
|
+
);
|
|
1345
|
+
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 });
|
|
1347
|
+
}
|
|
1348
|
+
securityMeta = { enabled: true, summary: secSummary, exitCode: secExit };
|
|
1349
|
+
} else {
|
|
1350
|
+
securityMeta = { enabled: true, error: 'dsh-security not found', summary: {} };
|
|
1351
|
+
}
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
securityMeta = { enabled: true, error: e.message.slice(0, 80), summary: {} };
|
|
1354
|
+
}
|
|
1279
1355
|
}
|
|
1280
1356
|
|
|
1281
1357
|
// 退出码只计内置失败 + catalog 中 severity=error 的失败;warn 失败提示但不改退出码
|
|
1358
|
+
// 安全检查退出码:CRITICAL→2, HIGH→1(取 max)
|
|
1282
1359
|
const bad = results.filter((r) => !r.ok && catalogSeverity.get(r.id) !== 'warn');
|
|
1360
|
+
const secExit = securityMeta.exitCode ?? 0;
|
|
1283
1361
|
if (jsonOut && process.argv.includes('--envelope')) {
|
|
1284
1362
|
// v1 契约信封(dsh doctor 规格,zoahdev/doctor 对齐):status 小写 + 退出码 0/1/2
|
|
1285
1363
|
const st = (r) => (r.skip ? 'skip' : (!r.ok ? (catalogSeverity.get(r.id) === 'warn' ? 'warn' : 'fail') : 'pass'));
|
|
1286
1364
|
const summary = { pass: 0, warn: 0, fail: 0, skip: 0 }; // skip 常驻(v1 词汇表 r5:#1719),r5 后 P12 会在未装 bundle 时实际触发
|
|
1287
|
-
const checks = results.map((r) => { summary[st(r)]++; return { name: r.id, status: st(r), detail: r.detail }; });
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1365
|
+
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;
|
|
1367
|
+
const exitCode = Math.max(baseExit, secExit);
|
|
1368
|
+
const out = {
|
|
1290
1369
|
schema: 'dsh-doctor/v1',
|
|
1291
1370
|
tool: 'dsh-doctor',
|
|
1292
1371
|
generatedAt: new Date().toISOString(),
|
|
@@ -1295,10 +1374,14 @@ async function run() {
|
|
|
1295
1374
|
summary,
|
|
1296
1375
|
ok: exitCode === 0,
|
|
1297
1376
|
checks,
|
|
1298
|
-
}
|
|
1377
|
+
};
|
|
1378
|
+
if (securityMeta.enabled) {
|
|
1379
|
+
out.security = { enabled: true, summary: securityMeta.summary, ...(securityMeta.error ? { error: securityMeta.error } : {}) };
|
|
1380
|
+
}
|
|
1381
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1299
1382
|
process.exit(exitCode);
|
|
1300
1383
|
} else if (jsonOut) {
|
|
1301
|
-
console.log(JSON.stringify({ ok: bad.length === 0, checks: results, catalog: catalogMeta, update: updateInfo }, null, 2));
|
|
1384
|
+
console.log(JSON.stringify({ ok: bad.length === 0 && secExit === 0, checks: results, catalog: catalogMeta, update: updateInfo, ...(securityMeta.enabled ? { security: securityMeta } : {}) }, null, 2));
|
|
1302
1385
|
} else {
|
|
1303
1386
|
const sectionOrder = { env: 0, profile: 1, session: 2, catalog: 3 };
|
|
1304
1387
|
const ordered = [...results].sort((a, b) => (sectionOrder[a.section] ?? 9) - (sectionOrder[b.section] ?? 9));
|
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.2",
|
|
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": [
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"bin": {
|
|
49
49
|
"dsh-doctor": "./dsh-doctor.mjs"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|