@moonquake2004/dsh-doctor 0.4.1 → 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 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,109 @@ async function run() {
1262
1263
  process.exit(1);
1263
1264
  }
1264
1265
  }
1265
- try { checkEnv(); } catch (e) { report('env', 'E0', false, `env 检查异常: ${e.message.slice(0, 80)}`); }
1266
- try { await checkPort3080(); } catch (e) { report('env', 'E10-port-3080', false, `端口检查异常: ${e.message.slice(0, 60)}`); }
1267
- try { checkProfile(profileArg); } catch (e) { report('profile', 'P0', false, `profile 检查异常: ${e.message.slice(0, 100)}`); }
1268
- try { checkSession(sessionArg); } catch (e) { report('session', 'S0', false, `session 检查异常: ${e.message.slice(0, 100)}`); }
1269
- try { scanAllSessions(); } catch (e) { report('session', 'S11', false, `全会话扫描异常: ${e.message.slice(0, 100)}`); }
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
- try {
1274
- const catalog = await loadCatalog({ noRemote: process.argv.includes('--no-catalog'), fetchImpl: typeof fetch === 'function' ? fetch : undefined });
1275
- catalogMeta = { source: catalog.source, checks: catalog.checks.length };
1276
- const profileDir = (() => { try { return resolveProfile(profileArg); } catch { return null; } })();
1277
- if (catalog.checks.length && profileDir) checkCatalog({ home: HOME, profile: profileArg, profileDir }, catalog);
1278
- else if (catalog.checks.length) report('catalog', 'C0', true, `profile 无效(${profileArg}),目录检查跳过(${catalog.source})`, undefined, 'catalog');
1279
- } catch (e) {
1280
- catalogMeta = { source: 'error', checks: 0, error: e.message.slice(0, 80) };
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
- try {
1287
- updateInfo = await checkForUpdate({ noRemote, fetchImpl: typeof fetch === 'function' ? fetch : undefined });
1288
- } catch (e) {
1289
- updateInfo = { current: localVersion(), latest: null, available: false, error: e.message.slice(0, 60) };
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
- if (process.argv.includes('--update') || (process.env.DSH_DOCTOR_AUTO_UPDATE === '1' && updateInfo.available)) {
1292
- updateInfo.applied = runUpdate();
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
+ }
1293
1355
  }
1294
1356
 
1295
1357
  // 退出码只计内置失败 + catalog 中 severity=error 的失败;warn 失败提示但不改退出码
1358
+ // 安全检查退出码:CRITICAL→2, HIGH→1(取 max)
1296
1359
  const bad = results.filter((r) => !r.ok && catalogSeverity.get(r.id) !== 'warn');
1360
+ const secExit = securityMeta.exitCode ?? 0;
1297
1361
  if (jsonOut && process.argv.includes('--envelope')) {
1298
1362
  // v1 契约信封(dsh doctor 规格,zoahdev/doctor 对齐):status 小写 + 退出码 0/1/2
1299
1363
  const st = (r) => (r.skip ? 'skip' : (!r.ok ? (catalogSeverity.get(r.id) === 'warn' ? 'warn' : 'fail') : 'pass'));
1300
1364
  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
- const exitCode = summary.fail > 0 ? 2 : summary.warn > 0 ? 1 : 0;
1303
- console.log(JSON.stringify({
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 = {
1304
1369
  schema: 'dsh-doctor/v1',
1305
1370
  tool: 'dsh-doctor',
1306
1371
  generatedAt: new Date().toISOString(),
@@ -1309,10 +1374,14 @@ async function run() {
1309
1374
  summary,
1310
1375
  ok: exitCode === 0,
1311
1376
  checks,
1312
- }, null, 2));
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));
1313
1382
  process.exit(exitCode);
1314
1383
  } else if (jsonOut) {
1315
- 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));
1316
1385
  } else {
1317
1386
  const sectionOrder = { env: 0, profile: 1, session: 2, catalog: 3 };
1318
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.1",
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": [