@moonquake2004/dsh-doctor 0.4.19 → 0.4.21
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 +246 -1
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -2001,6 +2001,154 @@ function classifyImportError(msg) {
|
|
|
2001
2001
|
return { kind: 'other', hint: '按上面的原始报错定位;确认后可先隔离该 bundle 让 dsh 起来' };
|
|
2002
2002
|
}
|
|
2003
2003
|
|
|
2004
|
+
|
|
2005
|
+
/* ---- 预检增强(2026-09):快照对比 + 安全安装 ---- */
|
|
2006
|
+
|
|
2007
|
+
const SNAPSHOT_FILE = '.dsh-doctor-snapshot.json';
|
|
2008
|
+
|
|
2009
|
+
/** 采集"启动关键状态":每个 bundle 的解析版本 + 每条 entry 的 spec。 */
|
|
2010
|
+
function bootSnapshot(profileDir) {
|
|
2011
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
2012
|
+
const prof = manifest.dsh?.profile ?? {};
|
|
2013
|
+
const bundles = {};
|
|
2014
|
+
for (const b of prof.bundles ?? []) {
|
|
2015
|
+
let v = null;
|
|
2016
|
+
try { v = JSON.parse(readFileSync(join(profileDir, 'node_modules', b, 'package.json'), 'utf8')).version ?? null; } catch { /* 未装/不可读 */ }
|
|
2017
|
+
bundles[b] = v;
|
|
2018
|
+
}
|
|
2019
|
+
const entries = {};
|
|
2020
|
+
for (const e of collectBootEntries(profileDir)) if (e.name) entries[e.id] = `${e.bundle} → ${e.name}`;
|
|
2021
|
+
return { at: new Date().toISOString(), bundles, entries };
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/** 与上次"已知良好"快照对比——这是"到底改了什么"的直接答案(dshmarket 自升级、手动 pnpm add 都算)。 */
|
|
2025
|
+
function diffSnapshot(prev, cur) {
|
|
2026
|
+
if (!prev) return null;
|
|
2027
|
+
const out = { addedBundles: [], removedBundles: [], versionChanged: [], addedEntries: [], removedEntries: [] };
|
|
2028
|
+
for (const [k, v] of Object.entries(cur.bundles)) {
|
|
2029
|
+
if (!(k in prev.bundles)) out.addedBundles.push(k);
|
|
2030
|
+
else if (prev.bundles[k] !== v) out.versionChanged.push(`${k}: ${prev.bundles[k]} → ${v}`);
|
|
2031
|
+
}
|
|
2032
|
+
for (const k of Object.keys(prev.bundles)) if (!(k in cur.bundles)) out.removedBundles.push(k);
|
|
2033
|
+
for (const [k, v] of Object.entries(cur.entries)) {
|
|
2034
|
+
if (!(k in prev.entries)) out.addedEntries.push(`${k} (${v})`);
|
|
2035
|
+
else if (prev.entries[k] !== v) out.addedEntries.push(`${k}: ${prev.entries[k]} → ${v}`);
|
|
2036
|
+
}
|
|
2037
|
+
for (const k of Object.keys(prev.entries)) if (!(k in cur.entries)) out.removedEntries.push(k);
|
|
2038
|
+
return out;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function readSnapshot(profileDir) {
|
|
2042
|
+
try { return JSON.parse(readFileSync(join(profileDir, SNAPSHOT_FILE), 'utf8')); } catch { return null; }
|
|
2043
|
+
}
|
|
2044
|
+
function writeSnapshot(profileDir, snap) {
|
|
2045
|
+
try { writeFileSync(join(profileDir, SNAPSHOT_FILE), JSON.stringify(snap, null, 2) + '\n'); } catch { /* 写不了不致命 */ }
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* --safe-add <pkg>:装完立即预检,坏了自动回滚。
|
|
2050
|
+
* 顺序:先备份 manifest → 调 `dsh plugin --profile <name> add <pkg>` → boot-check →
|
|
2051
|
+
* 通过:写"已知良好"快照;
|
|
2052
|
+
* 失败:先自动隔离出问题的那几个 bundle(若能就此恢复可启动,则保留安装并如实报告),
|
|
2053
|
+
* 若隔离后仍不可启动 → **整体回滚**到安装前的 manifest。
|
|
2054
|
+
* 这样"装了个不兼容的插件导致 dsh 起不来"在最坏情况下也只是一次无害的失败尝试。
|
|
2055
|
+
*/
|
|
2056
|
+
function safeAdd(profileArg, pkg) {
|
|
2057
|
+
const profDir = resolveProfile(profileArg || 'web');
|
|
2058
|
+
const manifestPath = join(profDir, 'package.json');
|
|
2059
|
+
const before = readFileSync(manifestPath, 'utf8');
|
|
2060
|
+
writeFileSync(`${manifestPath}.dsh-doctor.pre-add.${Date.now()}`, before);
|
|
2061
|
+
const install = spawnSync(process.platform === 'win32' ? 'dsh.cmd' : 'dsh',
|
|
2062
|
+
['plugin', '--profile', profileArg || 'web', 'add', pkg],
|
|
2063
|
+
{ cwd: profDir, encoding: 'utf8', stdio: 'inherit', timeout: 15 * 60 * 1000 });
|
|
2064
|
+
if (install.status !== 0) {
|
|
2065
|
+
writeFileSync(manifestPath, before); // 安装本身失败:还原
|
|
2066
|
+
return { ok: false, stage: 'install', restored: true, exit: install.status };
|
|
2067
|
+
}
|
|
2068
|
+
const results = runBootCheckSync(profDir);
|
|
2069
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
2070
|
+
if (!failed.length) {
|
|
2071
|
+
writeSnapshot(profDir, bootSnapshot(profDir));
|
|
2072
|
+
return { ok: true, stage: 'verified', checked: results.length, failed: 0 };
|
|
2073
|
+
}
|
|
2074
|
+
// 自动隔离出问题的 bundle
|
|
2075
|
+
const quarantined = [];
|
|
2076
|
+
for (const name of new Set(failed.map((f) => f.bundle))) {
|
|
2077
|
+
if (name === '(user patch)') continue;
|
|
2078
|
+
try { const r = quarantineBundle(profDir, name, false); if (r.ok) quarantined.push(name); } catch { /* 忽略 */ }
|
|
2079
|
+
}
|
|
2080
|
+
const after = runBootCheckSync(profDir);
|
|
2081
|
+
const stillFailed = after.filter((r) => r.status === 'failed');
|
|
2082
|
+
if (stillFailed.length) {
|
|
2083
|
+
writeFileSync(manifestPath, before); // 隔离都救不回来 → 整体回滚
|
|
2084
|
+
return { ok: false, stage: 'verify', quarantined, restored: true, failures: failed.map((f) => `${f.bundle}/${f.id}: ${f.kind}`) };
|
|
2085
|
+
}
|
|
2086
|
+
writeSnapshot(profDir, bootSnapshot(profDir));
|
|
2087
|
+
return { ok: true, stage: 'quarantined', quarantined, failures: failed.map((f) => `${f.bundle}/${f.id}: ${f.kind} ${f.error}`) };
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/** 同步版装载模拟(--safe-add 内部用;与 --boot-check 同一逻辑) */
|
|
2091
|
+
function runBootCheckSync(profileDir) {
|
|
2092
|
+
const out = [];
|
|
2093
|
+
for (const e of collectBootEntries(profileDir)) {
|
|
2094
|
+
if (!e.name) continue;
|
|
2095
|
+
const spec = e.name;
|
|
2096
|
+
if (spec.startsWith('cordis:') || spec.startsWith('.') || spec.startsWith('/')) { out.push({ id: e.id, bundle: e.bundle, spec, status: 'skipped' }); continue; }
|
|
2097
|
+
const r = spawnSync(process.execPath, ['--input-type=module', '-e', `await import(${JSON.stringify(spec)});`],
|
|
2098
|
+
{ cwd: profileDir, encoding: 'utf8', timeout: 20000, maxBuffer: 8 * 1024 * 1024 });
|
|
2099
|
+
if (r.status === 0) out.push({ id: e.id, bundle: e.bundle, spec, status: 'loadable' });
|
|
2100
|
+
else {
|
|
2101
|
+
const raw = String((r.stderr || '') + (r.stdout || ''));
|
|
2102
|
+
const cls = classifyImportError(raw);
|
|
2103
|
+
out.push({ id: e.id, bundle: e.bundle, spec, status: 'failed', kind: cls.kind, hint: cls.hint, error: (raw.split('\n').find((l) => /Error/.test(l)) || raw.slice(0, 160)).slice(0, 220) });
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return out;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
/* ---- 升级前后基线(2026-09):升级 dsh 前记一次,升级后自动复检 ---- */
|
|
2111
|
+
|
|
2112
|
+
const PRE_UPGRADE_FILE = '.dsh-doctor-pre-upgrade.json';
|
|
2113
|
+
|
|
2114
|
+
/** 取当前 dsh 核心版本:先问 CLI,再从 PATH 里 dsh 的安装树读 package.json。 */
|
|
2115
|
+
function coreVersion() {
|
|
2116
|
+
try {
|
|
2117
|
+
const bin = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
|
|
2118
|
+
const r = spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: 10000, shell: process.platform === 'win32' });
|
|
2119
|
+
const m = String(r.stdout || '').trim().match(/\d+\.\d+\.\d+[-\w.]*/);
|
|
2120
|
+
if (m) return m[0];
|
|
2121
|
+
} catch { /* 落到安装树 */ }
|
|
2122
|
+
for (const p of (process.env.PATH || '').split(PATH_DELIM)) {
|
|
2123
|
+
if (p.endsWith('node_modules/.bin') && existsSync(join(p, 'dsh'))) {
|
|
2124
|
+
try {
|
|
2125
|
+
const mf = join(dirname(p), '@deepseek-ai', 'dsh', 'package.json');
|
|
2126
|
+
return JSON.parse(readFileSync(mf, 'utf8')).version ?? null;
|
|
2127
|
+
} catch { /* 继续找 */ }
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/** 升级前基线:核心版本 + bundle 版本 + entry 列表 + manifest 原文(用于必要时人工比对)。 */
|
|
2134
|
+
function writePreUpgrade(profileDir) {
|
|
2135
|
+
const snap = bootSnapshot(profileDir);
|
|
2136
|
+
const payload = {
|
|
2137
|
+
kind: 'pre-upgrade',
|
|
2138
|
+
at: snap.at,
|
|
2139
|
+
coreVersion: coreVersion(),
|
|
2140
|
+
bundles: snap.bundles,
|
|
2141
|
+
entries: snap.entries,
|
|
2142
|
+
bundleCount: Object.keys(snap.bundles).length,
|
|
2143
|
+
};
|
|
2144
|
+
writeFileSync(join(profileDir, PRE_UPGRADE_FILE), JSON.stringify(payload, null, 2) + '\n');
|
|
2145
|
+
return payload;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function readPreUpgrade(profileDir) {
|
|
2149
|
+
try { return JSON.parse(readFileSync(join(profileDir, PRE_UPGRADE_FILE), 'utf8')); } catch { return null; }
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2004
2152
|
async function runBootCheck(profileDir) {
|
|
2005
2153
|
const entries = collectBootEntries(profileDir);
|
|
2006
2154
|
const targets = entries.filter((e) => e.name);
|
|
@@ -2052,6 +2200,86 @@ async function run() {
|
|
|
2052
2200
|
const bootCheckArg = process.argv.includes('--boot-check');
|
|
2053
2201
|
const quarantineArg = flagValue('--quarantine');
|
|
2054
2202
|
const unquarantineArg = flagValue('--unquarantine');
|
|
2203
|
+
const safeAddArg = flagValue('--safe-add');
|
|
2204
|
+
const preUpgradeArg = process.argv.includes('--pre-upgrade');
|
|
2205
|
+
const postUpgradeArg = process.argv.includes('--post-upgrade');
|
|
2206
|
+
if (preUpgradeArg || postUpgradeArg) {
|
|
2207
|
+
const profDir = resolveProfile(profileArg || 'web');
|
|
2208
|
+
if (preUpgradeArg) {
|
|
2209
|
+
const base = writePreUpgrade(profDir);
|
|
2210
|
+
if (jsonOut) console.log(JSON.stringify({ ok: true, ...base }, null, 2));
|
|
2211
|
+
else {
|
|
2212
|
+
console.log(`✓ 已记录升级前基线(${profDir})`);
|
|
2213
|
+
console.log(` 核心版本: ${base.coreVersion ?? '(未能确定)'} | bundle ${base.bundleCount} 个 | entry ${Object.keys(base.entries).length} 条`);
|
|
2214
|
+
console.log(' 下一步:升级 dsh,然后运行');
|
|
2215
|
+
console.log(` npx @moonquake2004/dsh-doctor --post-upgrade --profile ${profileArg || 'web'}`);
|
|
2216
|
+
console.log(' (--post-upgrade 会自动对比核心与插件变化、跑装载模拟,并给出修复/隔离命令)');
|
|
2217
|
+
}
|
|
2218
|
+
process.exit(0);
|
|
2219
|
+
}
|
|
2220
|
+
const base = readPreUpgrade(profDir);
|
|
2221
|
+
if (!base) {
|
|
2222
|
+
console.error(`✗ 没找到升级前基线(${join(profDir, PRE_UPGRADE_FILE)})——请先在升级前运行 --pre-upgrade`);
|
|
2223
|
+
process.exit(1);
|
|
2224
|
+
}
|
|
2225
|
+
const nowCore = coreVersion();
|
|
2226
|
+
const nowSnap = bootSnapshot(profDir);
|
|
2227
|
+
const drift = diffSnapshot({ bundles: base.bundles, entries: base.entries }, nowSnap) ?? {};
|
|
2228
|
+
const results = runBootCheckSync(profDir);
|
|
2229
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
2230
|
+
const coreChanged = base.coreVersion !== nowCore;
|
|
2231
|
+
const lines = [];
|
|
2232
|
+
lines.push(`核心版本: ${base.coreVersion ?? '?'} → ${nowCore ?? '?'}${coreChanged ? '(已变化)' : '(未变化)'}`);
|
|
2233
|
+
if ((drift.addedBundles ?? []).length) lines.push(`新增 bundle: ${drift.addedBundles.join(', ')}`);
|
|
2234
|
+
if ((drift.removedBundles ?? []).length) lines.push(`移除 bundle: ${drift.removedBundles.join(', ')}`);
|
|
2235
|
+
if ((drift.versionChanged ?? []).length) lines.push(`插件版本变化: ${drift.versionChanged.join('; ')}`);
|
|
2236
|
+
if ((drift.addedEntries ?? []).length) lines.push(`新增/变更 entry: ${drift.addedEntries.join('; ')}`);
|
|
2237
|
+
if (jsonOut) {
|
|
2238
|
+
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, baselineAt: base.at, coreChanged, from: base.coreVersion, to: nowCore, drift, checked: results.length, failed: failed.length, results }, null, 2));
|
|
2239
|
+
process.exit(failed.length ? 2 : 0);
|
|
2240
|
+
}
|
|
2241
|
+
console.log(`升级后复检(基线取自 ${String(base.at).slice(0, 16)}):`);
|
|
2242
|
+
for (const l of lines) console.log(` · ${l}`);
|
|
2243
|
+
if (!failed.length) {
|
|
2244
|
+
console.log(' ✓ 装载模拟通过——升级未破坏任何可探测 entry');
|
|
2245
|
+
writeSnapshot(profDir, nowSnap);
|
|
2246
|
+
} else {
|
|
2247
|
+
console.log(` ✗ 装载模拟失败 ${failed.length} 条(这就是"升级后起不来"的直接原因):`);
|
|
2248
|
+
for (const f of failed) {
|
|
2249
|
+
console.log(` [${f.bundle}] ${f.id} → ${f.spec}`);
|
|
2250
|
+
console.log(` ${f.kind}: ${f.error}`);
|
|
2251
|
+
console.log(` 修复方向:${f.hint}`);
|
|
2252
|
+
console.log(` 先起来:npx @moonquake2004/dsh-doctor --quarantine ${f.bundle} --profile ${profileArg || 'web'}`);
|
|
2253
|
+
}
|
|
2254
|
+
if (process.argv.includes('--auto-quarantine')) {
|
|
2255
|
+
const q = [];
|
|
2256
|
+
for (const name of new Set(failed.map((f) => f.bundle))) {
|
|
2257
|
+
if (name === '(user patch)') continue;
|
|
2258
|
+
try { const r = quarantineBundle(profDir, name, false); if (r.ok) q.push(name); } catch { /* 忽略 */ }
|
|
2259
|
+
}
|
|
2260
|
+
if (q.length) console.log(` → 已自动隔离: ${q.join(', ')}(重启 dsh 即可;用 --unquarantine 放回)`);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
process.exit(failed.length ? 2 : 0);
|
|
2264
|
+
}
|
|
2265
|
+
if (safeAddArg) {
|
|
2266
|
+
// 装完立即预检、坏了自动回滚——"装了个不兼容插件导致 dsh 起不来"的预防手段
|
|
2267
|
+
const profDir = resolveProfile(profileArg || 'web');
|
|
2268
|
+
const r = safeAdd(profileArg || 'web', safeAddArg);
|
|
2269
|
+
if (jsonOut) console.log(JSON.stringify(r, null, 2));
|
|
2270
|
+
else if (r.ok && r.stage === 'verified') console.log(`✓ 已安装并预检通过(${r.checked} 条 entry 均可导入)——重启 dsh 即可`);
|
|
2271
|
+
else if (r.ok && r.stage === 'quarantined') {
|
|
2272
|
+
console.log(`⚠ 已安装,但该插件的 entry 导入失败,已自动隔离以避免 dsh 起不来:`);
|
|
2273
|
+
for (const f of r.failures || []) console.log(` ${f}`);
|
|
2274
|
+
console.log(` 隔离项:${r.quarantined.join(', ')}(用 --unquarantine <包名> 放回,修好版本后再重试)`);
|
|
2275
|
+
console.log(` dsh 现在可以正常启动。`);
|
|
2276
|
+
} else if (r.stage === 'install') console.log(`✗ 安装命令本身失败(exit ${r.exit}),已还原 manifest`);
|
|
2277
|
+
else {
|
|
2278
|
+
console.log(`✗ 安装后无法启动,且隔离也救不回来 → 已整体回滚到安装前状态`);
|
|
2279
|
+
for (const f of r.failures || []) console.log(` ${f}`);
|
|
2280
|
+
}
|
|
2281
|
+
process.exit(r.ok ? 0 : 2);
|
|
2282
|
+
}
|
|
2055
2283
|
if (bootCheckArg || quarantineArg || unquarantineArg) {
|
|
2056
2284
|
const profDir = resolveProfile(profileArg || 'web');
|
|
2057
2285
|
try {
|
|
@@ -2063,8 +2291,25 @@ async function run() {
|
|
|
2063
2291
|
}
|
|
2064
2292
|
const results = await runBootCheck(profDir);
|
|
2065
2293
|
const failed = results.filter((r) => r.status === 'failed');
|
|
2294
|
+
const prevSnap = readSnapshot(profDir);
|
|
2295
|
+
const curSnap = bootSnapshot(profDir);
|
|
2296
|
+
const drift = diffSnapshot(prevSnap, curSnap);
|
|
2297
|
+
if (drift && !jsonOut) {
|
|
2298
|
+
const lines = [];
|
|
2299
|
+
if (drift.addedBundles.length) lines.push(`新增 bundle: ${drift.addedBundles.join(', ')}`);
|
|
2300
|
+
if (drift.versionChanged.length) lines.push(`版本变化: ${drift.versionChanged.join('; ')}`);
|
|
2301
|
+
if (drift.removedBundles.length) lines.push(`移除 bundle: ${drift.removedBundles.join(', ')}`);
|
|
2302
|
+
if (drift.addedEntries.length) lines.push(`新增/变更 entry: ${drift.addedEntries.join('; ')}`);
|
|
2303
|
+
if (drift.removedEntries.length) lines.push(`移除 entry: ${drift.removedEntries.join(', ')}`);
|
|
2304
|
+
if (lines.length) {
|
|
2305
|
+
console.log(`自上次预检通过以来(${String(prevSnap.at).slice(0, 16)}):`);
|
|
2306
|
+
for (const l of lines) console.log(` · ${l}`);
|
|
2307
|
+
console.log(' ——若本次启动失败,上面这些就是首要嫌疑。');
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (!failed.length) writeSnapshot(profDir, curSnap); // 只有通过时才更新"已知良好"快照
|
|
2066
2311
|
if (jsonOut) {
|
|
2067
|
-
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, results }, null, 2));
|
|
2312
|
+
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, drift, results }, null, 2));
|
|
2068
2313
|
} else {
|
|
2069
2314
|
console.log(`装载模拟(${profDir}):检查 ${results.length} 条 entry,失败 ${failed.length} 条`);
|
|
2070
2315
|
for (const r of results) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.21",
|
|
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": [
|