@moonquake2004/dsh-doctor 0.4.19 → 0.4.20
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 +142 -1
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -2001,6 +2001,111 @@ 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
|
+
|
|
2004
2109
|
async function runBootCheck(profileDir) {
|
|
2005
2110
|
const entries = collectBootEntries(profileDir);
|
|
2006
2111
|
const targets = entries.filter((e) => e.name);
|
|
@@ -2052,6 +2157,25 @@ async function run() {
|
|
|
2052
2157
|
const bootCheckArg = process.argv.includes('--boot-check');
|
|
2053
2158
|
const quarantineArg = flagValue('--quarantine');
|
|
2054
2159
|
const unquarantineArg = flagValue('--unquarantine');
|
|
2160
|
+
const safeAddArg = flagValue('--safe-add');
|
|
2161
|
+
if (safeAddArg) {
|
|
2162
|
+
// 装完立即预检、坏了自动回滚——"装了个不兼容插件导致 dsh 起不来"的预防手段
|
|
2163
|
+
const profDir = resolveProfile(profileArg || 'web');
|
|
2164
|
+
const r = safeAdd(profileArg || 'web', safeAddArg);
|
|
2165
|
+
if (jsonOut) console.log(JSON.stringify(r, null, 2));
|
|
2166
|
+
else if (r.ok && r.stage === 'verified') console.log(`✓ 已安装并预检通过(${r.checked} 条 entry 均可导入)——重启 dsh 即可`);
|
|
2167
|
+
else if (r.ok && r.stage === 'quarantined') {
|
|
2168
|
+
console.log(`⚠ 已安装,但该插件的 entry 导入失败,已自动隔离以避免 dsh 起不来:`);
|
|
2169
|
+
for (const f of r.failures || []) console.log(` ${f}`);
|
|
2170
|
+
console.log(` 隔离项:${r.quarantined.join(', ')}(用 --unquarantine <包名> 放回,修好版本后再重试)`);
|
|
2171
|
+
console.log(` dsh 现在可以正常启动。`);
|
|
2172
|
+
} else if (r.stage === 'install') console.log(`✗ 安装命令本身失败(exit ${r.exit}),已还原 manifest`);
|
|
2173
|
+
else {
|
|
2174
|
+
console.log(`✗ 安装后无法启动,且隔离也救不回来 → 已整体回滚到安装前状态`);
|
|
2175
|
+
for (const f of r.failures || []) console.log(` ${f}`);
|
|
2176
|
+
}
|
|
2177
|
+
process.exit(r.ok ? 0 : 2);
|
|
2178
|
+
}
|
|
2055
2179
|
if (bootCheckArg || quarantineArg || unquarantineArg) {
|
|
2056
2180
|
const profDir = resolveProfile(profileArg || 'web');
|
|
2057
2181
|
try {
|
|
@@ -2063,8 +2187,25 @@ async function run() {
|
|
|
2063
2187
|
}
|
|
2064
2188
|
const results = await runBootCheck(profDir);
|
|
2065
2189
|
const failed = results.filter((r) => r.status === 'failed');
|
|
2190
|
+
const prevSnap = readSnapshot(profDir);
|
|
2191
|
+
const curSnap = bootSnapshot(profDir);
|
|
2192
|
+
const drift = diffSnapshot(prevSnap, curSnap);
|
|
2193
|
+
if (drift && !jsonOut) {
|
|
2194
|
+
const lines = [];
|
|
2195
|
+
if (drift.addedBundles.length) lines.push(`新增 bundle: ${drift.addedBundles.join(', ')}`);
|
|
2196
|
+
if (drift.versionChanged.length) lines.push(`版本变化: ${drift.versionChanged.join('; ')}`);
|
|
2197
|
+
if (drift.removedBundles.length) lines.push(`移除 bundle: ${drift.removedBundles.join(', ')}`);
|
|
2198
|
+
if (drift.addedEntries.length) lines.push(`新增/变更 entry: ${drift.addedEntries.join('; ')}`);
|
|
2199
|
+
if (drift.removedEntries.length) lines.push(`移除 entry: ${drift.removedEntries.join(', ')}`);
|
|
2200
|
+
if (lines.length) {
|
|
2201
|
+
console.log(`自上次预检通过以来(${String(prevSnap.at).slice(0, 16)}):`);
|
|
2202
|
+
for (const l of lines) console.log(` · ${l}`);
|
|
2203
|
+
console.log(' ——若本次启动失败,上面这些就是首要嫌疑。');
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (!failed.length) writeSnapshot(profDir, curSnap); // 只有通过时才更新"已知良好"快照
|
|
2066
2207
|
if (jsonOut) {
|
|
2067
|
-
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, results }, null, 2));
|
|
2208
|
+
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, drift, results }, null, 2));
|
|
2068
2209
|
} else {
|
|
2069
2210
|
console.log(`装载模拟(${profDir}):检查 ${results.length} 条 entry,失败 ${failed.length} 条`);
|
|
2070
2211
|
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.20",
|
|
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": [
|