@moonquake2004/dsh-doctor 0.4.18 → 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 +270 -0
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -1950,7 +1950,277 @@ 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
|
+
|
|
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
|
+
async function runBootCheck(profileDir) {
|
|
2110
|
+
const entries = collectBootEntries(profileDir);
|
|
2111
|
+
const targets = entries.filter((e) => e.name);
|
|
2112
|
+
const results = [];
|
|
2113
|
+
for (const e of targets) {
|
|
2114
|
+
const spec = e.name;
|
|
2115
|
+
// 宿主内置与相对路径不做 import 探测(前者由宿主提供,后者依赖运行上下文)
|
|
2116
|
+
if (spec.startsWith('cordis:') || spec.startsWith('.') || spec.startsWith('/')) {
|
|
2117
|
+
results.push({ id: e.id, bundle: e.bundle, spec, status: 'skipped', reason: '宿主内置或相对路径,不在本探测范围' });
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
const r = spawnSync(process.execPath, ['--input-type=module', '-e', `await import(${JSON.stringify(spec)});`],
|
|
2121
|
+
{ cwd: profileDir, encoding: 'utf8', timeout: 20000, maxBuffer: 8 * 1024 * 1024 });
|
|
2122
|
+
if (r.status === 0) { results.push({ id: e.id, bundle: e.bundle, spec, status: 'loadable' }); continue; }
|
|
2123
|
+
const raw = String((r.stderr || '') + (r.stdout || '')).trim();
|
|
2124
|
+
const first = raw.split('\n').find((l) => /Error|error/.test(l)) || raw.split('\n')[0] || '(无输出)';
|
|
2125
|
+
const cls = classifyImportError(raw);
|
|
2126
|
+
results.push({ id: e.id, bundle: e.bundle, spec, status: 'failed', kind: cls.kind, hint: cls.hint, error: first.slice(0, 220) });
|
|
2127
|
+
}
|
|
2128
|
+
return results;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function quarantineBundle(profileDir, name, undo = false) {
|
|
2132
|
+
const manifestPath = join(profileDir, 'package.json');
|
|
2133
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
2134
|
+
const manifest = JSON.parse(raw);
|
|
2135
|
+
manifest.dsh = manifest.dsh ?? {};
|
|
2136
|
+
manifest.dsh.profile = manifest.dsh.profile ?? {};
|
|
2137
|
+
const prof = manifest.dsh.profile;
|
|
2138
|
+
prof.bundles = prof.bundles ?? [];
|
|
2139
|
+
prof._quarantined = prof._quarantined ?? [];
|
|
2140
|
+
if (undo) {
|
|
2141
|
+
const hit = prof._quarantined.find((q) => q.name === name);
|
|
2142
|
+
if (!hit) return { ok: false, error: `隔离列表里没有 ${name}` };
|
|
2143
|
+
prof._quarantined = prof._quarantined.filter((q) => q.name !== name);
|
|
2144
|
+
if (!prof.bundles.includes(name)) prof.bundles.push(name);
|
|
2145
|
+
} else {
|
|
2146
|
+
if (!prof.bundles.includes(name)) return { ok: false, error: `${name} 不在 dsh.profile.bundles 里(当前 ${prof.bundles.length} 项)` };
|
|
2147
|
+
writeFileSync(`${manifestPath}.bak.${Date.now()}`, raw); // 备份原始 manifest
|
|
2148
|
+
prof.bundles = prof.bundles.filter((b) => b !== name);
|
|
2149
|
+
prof._quarantined.push({ name, at: new Date().toISOString(), by: 'dsh-doctor --quarantine' });
|
|
2150
|
+
}
|
|
2151
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
2152
|
+
return { ok: true, bundles: prof.bundles.length, quarantined: prof._quarantined.map((q) => q.name) };
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1953
2155
|
async function run() {
|
|
2156
|
+
// 启动失败自救子命令(不需要 dsh 能启动)
|
|
2157
|
+
const bootCheckArg = process.argv.includes('--boot-check');
|
|
2158
|
+
const quarantineArg = flagValue('--quarantine');
|
|
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
|
+
}
|
|
2179
|
+
if (bootCheckArg || quarantineArg || unquarantineArg) {
|
|
2180
|
+
const profDir = resolveProfile(profileArg || 'web');
|
|
2181
|
+
try {
|
|
2182
|
+
if (quarantineArg || unquarantineArg) {
|
|
2183
|
+
const r = quarantineBundle(profDir, quarantineArg || unquarantineArg, !!unquarantineArg);
|
|
2184
|
+
if (!r.ok) { console.error(`✗ ${r.error}`); process.exit(1); }
|
|
2185
|
+
console.log(JSON.stringify({ ok: true, action: unquarantineArg ? 'unquarantine' : 'quarantine', ...r, next: '重启 dsh;随后用 --boot-check 复查,或用 --unquarantine 撤销' }, null, 2));
|
|
2186
|
+
process.exit(0);
|
|
2187
|
+
}
|
|
2188
|
+
const results = await runBootCheck(profDir);
|
|
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); // 只有通过时才更新"已知良好"快照
|
|
2207
|
+
if (jsonOut) {
|
|
2208
|
+
console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, drift, results }, null, 2));
|
|
2209
|
+
} else {
|
|
2210
|
+
console.log(`装载模拟(${profDir}):检查 ${results.length} 条 entry,失败 ${failed.length} 条`);
|
|
2211
|
+
for (const r of results) {
|
|
2212
|
+
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}`);
|
|
2213
|
+
else if (r.status === 'skipped') console.log(` ⊖ ${r.id}(${r.reason})`);
|
|
2214
|
+
}
|
|
2215
|
+
if (!failed.length) console.log(' ✓ 所有可探测 entry 均可导入——启动失败若仍发生,问题多在配置合并或原生环境,请贴 --json 输出');
|
|
2216
|
+
}
|
|
2217
|
+
process.exit(failed.length ? 2 : 0);
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
console.error(`启动模拟失败: ${e.message}`);
|
|
2220
|
+
process.exit(1);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
1954
2224
|
// 层 C 观察者(--observe / --observe-apply):独立子命令,跑完即退出,不执行常规检查
|
|
1955
2225
|
const observeArg = flagValue('--observe');
|
|
1956
2226
|
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.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": [
|