@moonquake2004/dsh-doctor 0.4.18 → 0.4.19
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 +129 -0
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -1950,7 +1950,136 @@ const flagValue = (name) => { const i = process.argv.indexOf(name); return i >=
|
|
|
1950
1950
|
const profileArg = (() => { const i = process.argv.indexOf('--profile'); return i >= 0 ? process.argv[i + 1] : 'web'; })();
|
|
1951
1951
|
const sessionArg = (() => { const i = process.argv.indexOf('--session'); return i >= 0 ? process.argv[i + 1] : undefined; })();
|
|
1952
1952
|
|
|
1953
|
+
|
|
1954
|
+
/* ================= 启动失败自救:装载模拟 + 隔离 =================
|
|
1955
|
+
* 场景(用户高频反馈):装了个不兼容的插件、或 dsh 升级后与旧插件不兼容 → **dsh 根本起不来**,
|
|
1956
|
+
* 于是没法用 dsh 自己来诊断,只能借别的工具。本工具是独立 Node CLI(`npx @moonquake2004/dsh-doctor`),
|
|
1957
|
+
* **不需要 dsh 能启动**,所以这两个子命令正是为此设计:
|
|
1958
|
+
* --boot-check 离线模拟插件树装载:逐条 entry 真去 import,报出到底哪一条炸、炸在哪
|
|
1959
|
+
* --quarantine <包名> 把某个 bundle 从启动列表里摘掉(先备份),让 dsh 能先起来
|
|
1960
|
+
* --unquarantine <包名> 撤销隔离
|
|
1961
|
+
* 注意:--boot-check 会**执行插件顶层代码**(这正是"启动"本身的语义),故为显式开关、非默认行为。
|
|
1962
|
+
*/
|
|
1963
|
+
|
|
1964
|
+
/** 解析 profile 的启动列表与各 bundle 的 entry(含用户 patch 的 insert),跳过 disabled 与已隔离项。 */
|
|
1965
|
+
function collectBootEntries(profileDir) {
|
|
1966
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
1967
|
+
const prof = manifest.dsh?.profile ?? {};
|
|
1968
|
+
const quarantined = new Set((prof._quarantined ?? []).map((q) => q.name));
|
|
1969
|
+
const bundles = (prof.bundles ?? []).filter((b) => !quarantined.has(b));
|
|
1970
|
+
const out = [];
|
|
1971
|
+
const parsePatch = (text, source) => {
|
|
1972
|
+
let cur = null;
|
|
1973
|
+
const flush = () => { if (cur) out.push(cur); cur = null; };
|
|
1974
|
+
for (const line of text.split('\n')) {
|
|
1975
|
+
const idM = /^\s*-?\s*id:\s*['"]?([\w@/.\-]+)['"]?\s*$/.exec(line);
|
|
1976
|
+
if (idM) { flush(); cur = { id: idM[1], bundle: source, name: null, disabled: false }; continue; }
|
|
1977
|
+
if (!cur) continue;
|
|
1978
|
+
const nameM = /^\s*name:\s*['"]?([^'"\s]+)['"]?\s*$/.exec(line);
|
|
1979
|
+
if (nameM) { cur.name = nameM[1]; continue; }
|
|
1980
|
+
if (/^\s*disabled:\s*true\s*$/.test(line)) cur.disabled = true;
|
|
1981
|
+
}
|
|
1982
|
+
flush();
|
|
1983
|
+
};
|
|
1984
|
+
for (const b of bundles) {
|
|
1985
|
+
const patchPath = join(profileDir, 'node_modules', b, 'cordis.patch.yml');
|
|
1986
|
+
if (!existsSync(patchPath)) continue;
|
|
1987
|
+
try { parsePatch(readFileSync(patchPath, 'utf8'), b); } catch { /* 读不了由 P1/P7 报 */ }
|
|
1988
|
+
}
|
|
1989
|
+
const userPatch = join(profileDir, 'cordis.patch.yml');
|
|
1990
|
+
if (existsSync(userPatch)) { try { parsePatch(readFileSync(userPatch, 'utf8'), '(user patch)'); } catch { /* 同上 */ } }
|
|
1991
|
+
return out.filter((e) => !e.disabled);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/** 归类 import 失败,给出人能照做的判断。 */
|
|
1995
|
+
function classifyImportError(msg) {
|
|
1996
|
+
if (/does not provide an export named/.test(msg)) return { kind: 'missing-export', hint: '插件比所装的 @deepseek-ai/* 旧/新:把该插件升级到匹配版本(或降级核心)' };
|
|
1997
|
+
if (/ERR_MODULE_NOT_FOUND|Cannot find module/.test(msg)) return { kind: 'not-installed', hint: '依赖或包本身没装全:在该 profile 里重装该插件' };
|
|
1998
|
+
if (/NODE_MODULE_VERSION|compiled against a different Node\.js version|invalid ELF header|not a valid Win32/.test(msg)) return { kind: 'native-abi', hint: '原生模块与当前 Node ABI 不匹配:重装该插件(npm rebuild / 重新 pnpm add)' };
|
|
1999
|
+
if (/ERR_PACKAGE_PATH_NOT_EXPORTED|ERR_REQUIRE_ESM|require\(\) of ES Module/.test(msg)) return { kind: 'module-format', hint: '包导出/模块格式问题:升级该插件(多为上游未适配)' };
|
|
2000
|
+
if (/was killed|ETIMEDOUT|timed out/.test(msg)) return { kind: 'hang', hint: '顶层代码卡住:该插件在加载期做了阻塞操作,需上游修复' };
|
|
2001
|
+
return { kind: 'other', hint: '按上面的原始报错定位;确认后可先隔离该 bundle 让 dsh 起来' };
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
async function runBootCheck(profileDir) {
|
|
2005
|
+
const entries = collectBootEntries(profileDir);
|
|
2006
|
+
const targets = entries.filter((e) => e.name);
|
|
2007
|
+
const results = [];
|
|
2008
|
+
for (const e of targets) {
|
|
2009
|
+
const spec = e.name;
|
|
2010
|
+
// 宿主内置与相对路径不做 import 探测(前者由宿主提供,后者依赖运行上下文)
|
|
2011
|
+
if (spec.startsWith('cordis:') || spec.startsWith('.') || spec.startsWith('/')) {
|
|
2012
|
+
results.push({ id: e.id, bundle: e.bundle, spec, status: 'skipped', reason: '宿主内置或相对路径,不在本探测范围' });
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
const r = spawnSync(process.execPath, ['--input-type=module', '-e', `await import(${JSON.stringify(spec)});`],
|
|
2016
|
+
{ cwd: profileDir, encoding: 'utf8', timeout: 20000, maxBuffer: 8 * 1024 * 1024 });
|
|
2017
|
+
if (r.status === 0) { results.push({ id: e.id, bundle: e.bundle, spec, status: 'loadable' }); continue; }
|
|
2018
|
+
const raw = String((r.stderr || '') + (r.stdout || '')).trim();
|
|
2019
|
+
const first = raw.split('\n').find((l) => /Error|error/.test(l)) || raw.split('\n')[0] || '(无输出)';
|
|
2020
|
+
const cls = classifyImportError(raw);
|
|
2021
|
+
results.push({ id: e.id, bundle: e.bundle, spec, status: 'failed', kind: cls.kind, hint: cls.hint, error: first.slice(0, 220) });
|
|
2022
|
+
}
|
|
2023
|
+
return results;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function quarantineBundle(profileDir, name, undo = false) {
|
|
2027
|
+
const manifestPath = join(profileDir, 'package.json');
|
|
2028
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
2029
|
+
const manifest = JSON.parse(raw);
|
|
2030
|
+
manifest.dsh = manifest.dsh ?? {};
|
|
2031
|
+
manifest.dsh.profile = manifest.dsh.profile ?? {};
|
|
2032
|
+
const prof = manifest.dsh.profile;
|
|
2033
|
+
prof.bundles = prof.bundles ?? [];
|
|
2034
|
+
prof._quarantined = prof._quarantined ?? [];
|
|
2035
|
+
if (undo) {
|
|
2036
|
+
const hit = prof._quarantined.find((q) => q.name === name);
|
|
2037
|
+
if (!hit) return { ok: false, error: `隔离列表里没有 ${name}` };
|
|
2038
|
+
prof._quarantined = prof._quarantined.filter((q) => q.name !== name);
|
|
2039
|
+
if (!prof.bundles.includes(name)) prof.bundles.push(name);
|
|
2040
|
+
} else {
|
|
2041
|
+
if (!prof.bundles.includes(name)) return { ok: false, error: `${name} 不在 dsh.profile.bundles 里(当前 ${prof.bundles.length} 项)` };
|
|
2042
|
+
writeFileSync(`${manifestPath}.bak.${Date.now()}`, raw); // 备份原始 manifest
|
|
2043
|
+
prof.bundles = prof.bundles.filter((b) => b !== name);
|
|
2044
|
+
prof._quarantined.push({ name, at: new Date().toISOString(), by: 'dsh-doctor --quarantine' });
|
|
2045
|
+
}
|
|
2046
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
2047
|
+
return { ok: true, bundles: prof.bundles.length, quarantined: prof._quarantined.map((q) => q.name) };
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1953
2050
|
async function run() {
|
|
2051
|
+
// 启动失败自救子命令(不需要 dsh 能启动)
|
|
2052
|
+
const bootCheckArg = process.argv.includes('--boot-check');
|
|
2053
|
+
const quarantineArg = flagValue('--quarantine');
|
|
2054
|
+
const unquarantineArg = flagValue('--unquarantine');
|
|
2055
|
+
if (bootCheckArg || quarantineArg || unquarantineArg) {
|
|
2056
|
+
const profDir = resolveProfile(profileArg || 'web');
|
|
2057
|
+
try {
|
|
2058
|
+
if (quarantineArg || unquarantineArg) {
|
|
2059
|
+
const r = quarantineBundle(profDir, quarantineArg || unquarantineArg, !!unquarantineArg);
|
|
2060
|
+
if (!r.ok) { console.error(`✗ ${r.error}`); process.exit(1); }
|
|
2061
|
+
console.log(JSON.stringify({ ok: true, action: unquarantineArg ? 'unquarantine' : 'quarantine', ...r, next: '重启 dsh;随后用 --boot-check 复查,或用 --unquarantine 撤销' }, null, 2));
|
|
2062
|
+
process.exit(0);
|
|
2063
|
+
}
|
|
2064
|
+
const results = await runBootCheck(profDir);
|
|
2065
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
2066
|
+
if (jsonOut) {
|
|
2067
|
+
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, results }, null, 2));
|
|
2068
|
+
} else {
|
|
2069
|
+
console.log(`装载模拟(${profDir}):检查 ${results.length} 条 entry,失败 ${failed.length} 条`);
|
|
2070
|
+
for (const r of results) {
|
|
2071
|
+
if (r.status === 'failed') console.log(` ✗ [${r.bundle}] ${r.id} → ${r.spec}\n ${r.kind}: ${r.error}\n 修复方向:${r.hint}\n 先起来:npx @moonquake2004/dsh-doctor --quarantine ${r.bundle}`);
|
|
2072
|
+
else if (r.status === 'skipped') console.log(` ⊖ ${r.id}(${r.reason})`);
|
|
2073
|
+
}
|
|
2074
|
+
if (!failed.length) console.log(' ✓ 所有可探测 entry 均可导入——启动失败若仍发生,问题多在配置合并或原生环境,请贴 --json 输出');
|
|
2075
|
+
}
|
|
2076
|
+
process.exit(failed.length ? 2 : 0);
|
|
2077
|
+
} catch (e) {
|
|
2078
|
+
console.error(`启动模拟失败: ${e.message}`);
|
|
2079
|
+
process.exit(1);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1954
2083
|
// 层 C 观察者(--observe / --observe-apply):独立子命令,跑完即退出,不执行常规检查
|
|
1955
2084
|
const observeArg = flagValue('--observe');
|
|
1956
2085
|
const observeApplyArg = flagValue('--observe-apply');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
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": [
|