@moonquake2004/dsh-doctor 0.2.0 → 0.2.3
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/checks.json +3 -2
- package/dsh-doctor.mjs +182 -7
- package/package.json +2 -2
package/checks.json
CHANGED
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
"probe": {
|
|
42
42
|
"type": "text-not-contains",
|
|
43
43
|
"path": "{profile}/cordis.patch.yml",
|
|
44
|
-
"pattern": "^\\s
|
|
45
|
-
"flags": "m"
|
|
44
|
+
"pattern": "^\\s*name:\\s*['\"]?[^'\"\\s]+[^'\"\\n]*\\s[^'\"\\n]+",
|
|
45
|
+
"flags": "m",
|
|
46
|
+
"required": false
|
|
46
47
|
},
|
|
47
48
|
"detailOk": "用户 patch 的 insert name 均无空格",
|
|
48
49
|
"detailFail": "用户 patch 存在含空格的 insert name(Windows 下 spawn 参数解析会断,#1420 待实现 lint 的目录版)",
|
package/dsh-doctor.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* P3 用户 patch 的 insert name 从 profile 锚点不可解析(#1197/#880)
|
|
10
10
|
* P4 file: 依赖指向不存在的目录(#1197:悬空 file: 链接)
|
|
11
11
|
* P5 profile 顶层 @deepseek-ai/* 与框架重复(#1486:双模块实例 → Symbol 不匹配)
|
|
12
|
+
* P7 cordis.patch.yml 结构 lint(#1724:~ insert: 是 YAML null → parsePatchList 崩溃 → UI 打不开;tab 缩进/缺冒号同族)
|
|
12
13
|
* [session]
|
|
13
14
|
* S1 孤儿 tool_call(#1363:assistant tool_calls 无对应 tool 结果 → INVALID_REQUEST)
|
|
14
15
|
* S2 未闭合 turn(#466/#1265:turn/start 无 turn/end → 会话永久"运行中")
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
* E4 node-pty 原生模块完整性(#1219:pty.node 缺失 → dsh web 启动失败)
|
|
26
27
|
* E5 存储 JSON 文件合法性(#1357:并发写 workspace.json 乱码 → 工作区列表消失)
|
|
27
28
|
* E6 锚点元检查(tripwire:S6 的 expandRow seq0+k、S7 的 session/end-seed、S10 的 sourceEventSeqs 是否仍在安装的 dsh-session 中)
|
|
29
|
+
* E10 3080 Web 端口可用性(#1719:启动 dsh web 前检查;dsh web 自身占用=正常,其他程序占用=FAIL;DSH_DOCTOR_PORT 可覆盖)
|
|
28
30
|
* (P6 Windows 空格参数 lint,#1420 —— 待实现)
|
|
29
31
|
*
|
|
30
32
|
* 用法:
|
|
@@ -42,11 +44,12 @@
|
|
|
42
44
|
* 退出码:0 = 全部通过;1 = 发现可修复问题(内置 + catalog severity=error);warn 级失败不改退出码。
|
|
43
45
|
*/
|
|
44
46
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
45
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
47
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
46
48
|
import { createRequire } from 'node:module';
|
|
49
|
+
import net from 'node:net';
|
|
47
50
|
import { basename, delimiter as PATH_DELIM, dirname, join } from 'node:path';
|
|
48
51
|
import { homedir } from 'node:os';
|
|
49
|
-
import { pathToFileURL } from 'node:url';
|
|
52
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
50
53
|
|
|
51
54
|
const HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
52
55
|
const results = []; // { section, id, ok, detail, fix? }
|
|
@@ -185,6 +188,55 @@ function checkEnv() {
|
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
/* ================= E10:Web 端口可用性(#1719 提案;启动 dsh web 前检查,避免 address in use) =================
|
|
192
|
+
* 本地 socket bind 探测(离线兼容):端口空闲 → PASS;被 dsh web 实例占用 → PASS+提示
|
|
193
|
+
* (宿主自身或另一实例,正常);被其他程序占用 → FAIL。
|
|
194
|
+
* 默认 3080,可用 DSH_DOCTOR_PORT 覆盖(测试/换端口)。
|
|
195
|
+
*/
|
|
196
|
+
function portOccupierInfo(port) {
|
|
197
|
+
try {
|
|
198
|
+
if (process.platform === 'win32') {
|
|
199
|
+
const r = execFileSync('netstat', ['-ano'], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
200
|
+
const m = new RegExp(`TCP\\s+[^\\s]+:${port}\\s+.*?LISTENING\\s+(\\d+)`).exec(r);
|
|
201
|
+
if (!m) return null;
|
|
202
|
+
return { pid: m[1], cmd: '', dsh: false };
|
|
203
|
+
}
|
|
204
|
+
const r = execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 });
|
|
205
|
+
const lines = r.trim().split('\n').slice(1).filter(Boolean);
|
|
206
|
+
if (!lines.length) return null;
|
|
207
|
+
const parts = lines[0].trim().split(/\s+/);
|
|
208
|
+
const cmd = parts[0] || '';
|
|
209
|
+
const pid = parts[1] || '';
|
|
210
|
+
let dsh = /dsh|deepseek/.test(cmd);
|
|
211
|
+
if (!dsh && pid) {
|
|
212
|
+
try { dsh = /dsh web|deepseek-ai/.test(execFileSync('ps', ['-p', pid, '-o', 'command='], { encoding: 'utf8' })); } catch { /* 无法识别则按非 dsh 处理 */ }
|
|
213
|
+
}
|
|
214
|
+
return { pid, cmd, dsh };
|
|
215
|
+
} catch { return null; }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function checkPort3080() {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
if (!wants('env')) { resolve(); return; }
|
|
221
|
+
const port = Number(process.env.DSH_DOCTOR_PORT || 3080);
|
|
222
|
+
const srv = net.createServer();
|
|
223
|
+
srv.unref();
|
|
224
|
+
let done = false;
|
|
225
|
+
const finish = (fn) => (...args) => { if (done) return; done = true; try { fn(...args); } catch { } resolve(); };
|
|
226
|
+
srv.on('error', finish((e) => {
|
|
227
|
+
if (e.code === 'EADDRINUSE') {
|
|
228
|
+
const info = portOccupierInfo(port);
|
|
229
|
+
if (info && info.dsh) report('env', 'E10-port-3080', true, `端口 ${port} 被 dsh web 实例占用(PID ${info.pid})——宿主自身或另一实例,正常`, undefined);
|
|
230
|
+
else if (info) report('env', 'E10-port-3080', false, `端口 ${port} 被其他程序占用(PID ${info.pid}: ${info.cmd}),dsh web 启动会 address in use(#1719)`, `关掉占用进程,或让 dsh web 用别的端口`);
|
|
231
|
+
else report('env', 'E10-port-3080', true, `⚠ 端口 ${port} 被占用但无法识别占用者`, undefined);
|
|
232
|
+
} else {
|
|
233
|
+
report('env', 'E10-port-3080', false, `端口 ${port} 探测异常: ${e.message.slice(0, 60)}`, undefined);
|
|
234
|
+
}
|
|
235
|
+
}));
|
|
236
|
+
srv.listen(port, '127.0.0.1', finish(() => { srv.close(); report('env', 'E10-port-3080', true, `端口 ${port} 空闲`, undefined); }));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
188
240
|
/* ================= profile ================= */
|
|
189
241
|
function checkProfile(name) {
|
|
190
242
|
if (!wants('profile')) return;
|
|
@@ -235,7 +287,8 @@ function checkProfile(name) {
|
|
|
235
287
|
const out = new Set();
|
|
236
288
|
if (!existsSync(patchPath)) return out;
|
|
237
289
|
const text = readFileSync(patchPath, 'utf8');
|
|
238
|
-
|
|
290
|
+
// 真实格式:`- id:` 下缩进的 `name:` 行(无破折号)——2026-08-15 fixtures 发现旧正则从未匹配
|
|
291
|
+
for (const m of text.matchAll(/^\s*name:\s*['"]?([^'"\s]+)/gm)) out.add(m[1]);
|
|
239
292
|
return out;
|
|
240
293
|
})();
|
|
241
294
|
|
|
@@ -293,17 +346,54 @@ function checkProfile(name) {
|
|
|
293
346
|
const dangling = Object.entries(deps).filter(([, spec]) => spec.startsWith('file:')).filter(([, spec]) => !existsSync(resolveFileSpec(spec)));
|
|
294
347
|
if (dangling.length) report('profile', 'P4', false, `悬空 file: 依赖(#1197): ${dangling.map(([n, s]) => `${n} (${s})`).join(', ')}`, '恢复目录或移除依赖');
|
|
295
348
|
else report('profile', 'P4', true, 'file: 依赖完整', undefined);
|
|
296
|
-
// P5 顶层 @deepseek-ai/*
|
|
349
|
+
// P5 顶层 @deepseek-ai/* 重复(#1486/#1697:hoisted 布局下同版本双实例 → 模块级 Symbol 不匹配)
|
|
350
|
+
// symlink 指向宿主同一份(#1697 的 link: workaround / pnpm file: 正常形态)= 单实例,放行
|
|
297
351
|
const topDup = [];
|
|
298
352
|
const topDir = join(dir, 'node_modules', '@deepseek-ai');
|
|
353
|
+
const hostScope = installAnchor ? join(installAnchor, '@deepseek-ai') : null;
|
|
299
354
|
if (existsSync(topDir)) {
|
|
300
355
|
for (const p of readdirSync(topDir)) {
|
|
301
356
|
const fp = join(topDir, p);
|
|
357
|
+
let st;
|
|
358
|
+
try { st = lstatSync(fp); } catch { continue; }
|
|
359
|
+
if (st.isSymbolicLink() && hostScope) {
|
|
360
|
+
try {
|
|
361
|
+
const real = realpathSync(fp);
|
|
362
|
+
const hostPkg = join(hostScope, p);
|
|
363
|
+
if (existsSync(hostPkg) && realpathSync(hostPkg) === real) continue; // 宿主同一份
|
|
364
|
+
} catch { /* 无法解析 → 按独立副本处理 */ }
|
|
365
|
+
}
|
|
302
366
|
if (existsSync(join(fp, 'package.json'))) topDup.push(p);
|
|
303
367
|
}
|
|
304
368
|
}
|
|
305
|
-
if (topDup.length) report('profile', 'P5', false, `profile 顶层存在 @deepseek-ai/* 重复(#1486
|
|
369
|
+
if (topDup.length) report('profile', 'P5', false, `profile 顶层存在 @deepseek-ai/* 重复(#1486/#1697 双实例风险,hoisted 布局会让同版本工具包互相遮蔽导致 Symbol 不匹配): ${topDup.join(', ')}`, '清理 profile 顶层 node_modules/@deepseek-ai 中与宿主同名的独立副本(真实目录);指向宿主的 link: symlink 是安全的(#1697 workaround)');
|
|
306
370
|
else report('profile', 'P5', true, '无顶层 @deepseek-ai 重复', undefined);
|
|
371
|
+
|
|
372
|
+
// P7 patch YAML 结构 lint(#1724:~ insert: / 顶层映射+序列混排 / tab / 缺冒号 → parsePatchList 崩 → UI 打不开)
|
|
373
|
+
// 离线、零依赖的保守检查,覆盖已实测的崩溃机制:
|
|
374
|
+
// 1) ~ / null 等非法 insert 标记(~ 是 YAML null)
|
|
375
|
+
// 2) 顶层映射(key: value)与顶层序列(- xxx)混排 → js-yaml "document separator expected"
|
|
376
|
+
// 3) tab 缩进(YAML 硬错误);4) insert 缺冒号
|
|
377
|
+
const yamlProblems = [];
|
|
378
|
+
const patchText = existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : '';
|
|
379
|
+
if (patchText) {
|
|
380
|
+
const topLines = patchText.split('\n');
|
|
381
|
+
let hasTopMapping = false, hasTopSeq = false;
|
|
382
|
+
topLines.forEach((line, i) => {
|
|
383
|
+
if (!line.trim() || line.trim().startsWith('#')) return;
|
|
384
|
+
if (line.includes('\t')) yamlProblems.push(`第 ${i + 1} 行含制表符缩进(YAML 禁止 tab)`);
|
|
385
|
+
if (/^\s*(~|null|Null|NULL)\s*insert\s*:/.test(line)) yamlProblems.push(`第 ${i + 1} 行 "${line.trim()}" —— ~ 是 YAML null 字面量,应为 "- insert:"(#1724)`);
|
|
386
|
+
else if (/^\s*-\s*insert(\s|$)/.test(line) && !/^\s*-\s*insert\s*:/.test(line)) yamlProblems.push(`第 ${i + 1} 行 "${line.trim()}" —— "- insert" 缺冒号`);
|
|
387
|
+
// 顶层混排检测:col 0 的映射键 vs col 0 的序列项
|
|
388
|
+
if (!/^\s/.test(line)) {
|
|
389
|
+
if (/^[^\s#-][^:]*:\s/.test(line)) hasTopMapping = true;
|
|
390
|
+
if (/^-\s/.test(line)) hasTopSeq = true;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
if (hasTopMapping && hasTopSeq) yamlProblems.push('顶层同时存在 key: value 映射与 - xxx 序列(js-yaml 报 "stream or a document separator is expected",#1724 实测)');
|
|
394
|
+
}
|
|
395
|
+
if (yamlProblems.length) report('profile', 'P7', false, `cordis.patch.yml 结构错误(boot 会崩,UI 打不开 #1724): ${yamlProblems.join('; ')}`, 'patch 必须是顶层纯列表(只有 - insert: / - id: 条目):删掉顶层 key: value 行;~ 是 YAML null;缩进用空格不用 tab');
|
|
396
|
+
else report('profile', 'P7', true, 'cordis.patch.yml 结构正常(无 tab / 无 ~ insert / 无映射-序列混排)', undefined);
|
|
307
397
|
}
|
|
308
398
|
|
|
309
399
|
/* ================= session ================= */
|
|
@@ -707,10 +797,11 @@ export function runCatalogCheck(check, ctx) {
|
|
|
707
797
|
}
|
|
708
798
|
}
|
|
709
799
|
|
|
710
|
-
/** 逐条执行目录检查,汇入统一 results 管线(src='catalog'
|
|
800
|
+
/** 逐条执行目录检查,汇入统一 results 管线(src='catalog')。尊重 --profile/--env/--session 收窄。 */
|
|
711
801
|
function checkCatalog(ctx, catalog) {
|
|
712
802
|
const platform = process.platform;
|
|
713
803
|
for (const check of catalog.checks ?? []) {
|
|
804
|
+
if (!wants(check.section)) continue; // 与内置检查一致的 section 收窄
|
|
714
805
|
const when = check.when ?? {};
|
|
715
806
|
if (Array.isArray(when.os) && !when.os.includes(platform)) continue;
|
|
716
807
|
if (check.section === 'profile' && !ctx.profileDir) continue; // profile 无效时跳过 profile 段
|
|
@@ -723,12 +814,79 @@ function checkCatalog(ctx, catalog) {
|
|
|
723
814
|
}
|
|
724
815
|
}
|
|
725
816
|
|
|
817
|
+
/* ================= 层 B:版本检查与更新(v0.2.1) =================
|
|
818
|
+
* 检查 npm dist-tags.latest 是否比本地版本新(TTL 6h 缓存 + 离线回退 last-known-good)。
|
|
819
|
+
* 默认只提示;--update 手动执行更新;DSH_DOCTOR_AUTO_UPDATE=1 可用时自动更新。
|
|
820
|
+
* 诚实边界:cordis 启动时加载插件,更新后需重启 dsh web 才生效。
|
|
821
|
+
*/
|
|
822
|
+
const UPDATE_URL = 'https://registry.npmjs.org/@moonquake2004%2Fdsh-doctor';
|
|
823
|
+
const UPDATE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
824
|
+
|
|
825
|
+
export function localVersion() {
|
|
826
|
+
try {
|
|
827
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
828
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
829
|
+
} catch { return '0.0.0'; }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** 返回 { current, latest, available };latest=null 表示无法确认(离线且无缓存)。 */
|
|
833
|
+
export async function checkForUpdate({ noRemote = false, fetchImpl, home = HOME } = {}) {
|
|
834
|
+
const current = localVersion();
|
|
835
|
+
if (noRemote || typeof fetchImpl !== 'function') return { current, latest: null, available: false };
|
|
836
|
+
const cachePath = join(home, '.cache', 'dsh-doctor', 'update.json');
|
|
837
|
+
const readCache = () => { try { const d = JSON.parse(readFileSync(cachePath, 'utf8')); return d && typeof d.latest === 'string' ? d : null; } catch { return null; } };
|
|
838
|
+
try {
|
|
839
|
+
const c = readCache();
|
|
840
|
+
if (c && Date.now() - statSync(cachePath).mtimeMs < UPDATE_TTL_MS) return { current, latest: c.latest, available: c.latest !== current };
|
|
841
|
+
} catch { /* 回退 */ }
|
|
842
|
+
try {
|
|
843
|
+
const ac = new AbortController();
|
|
844
|
+
const timer = setTimeout(() => ac.abort(), 3000);
|
|
845
|
+
const res = await fetchImpl(UPDATE_URL, { signal: ac.signal });
|
|
846
|
+
clearTimeout(timer);
|
|
847
|
+
if (res && res.ok) {
|
|
848
|
+
const data = await res.json();
|
|
849
|
+
const latest = data?.['dist-tags']?.latest;
|
|
850
|
+
if (typeof latest === 'string') {
|
|
851
|
+
try { mkdirSync(dirname(cachePath), { recursive: true }); writeFileSync(cachePath, JSON.stringify({ latest, checkedAt: new Date().toISOString() })); } catch { /* 缓存失败不影响 */ }
|
|
852
|
+
return { current, latest, available: latest !== current };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
} catch { /* 离线/超时 → last-known-good */ }
|
|
856
|
+
const stale = readCache();
|
|
857
|
+
if (stale) return { current, latest: stale.latest, available: stale.latest !== current };
|
|
858
|
+
return { current, latest: null, available: false };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** 判断本模块是否安装在某个 profile 的 node_modules 下;返回 profile 目录或 null。 */
|
|
862
|
+
export function profileDirOfModule() {
|
|
863
|
+
const here = fileURLToPath(new URL('.', import.meta.url));
|
|
864
|
+
const m = here.match(/(\/\.dsh\/profiles\/[^/]+)\/node_modules\//);
|
|
865
|
+
return m ? m[1] : null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** 执行更新(profile 内 pnpm install 刷新 file:/npm 依赖;可用 DSH_DOCTOR_UPDATE_CMD 覆盖命令)。 */
|
|
869
|
+
export function runUpdate() {
|
|
870
|
+
const override = process.env.DSH_DOCTOR_UPDATE_CMD;
|
|
871
|
+
if (override) {
|
|
872
|
+
const r = spawnSync(override, { shell: true, stdio: 'inherit' });
|
|
873
|
+
return r.status === 0 ? '更新命令执行完成,请重启 dsh web 生效' : `更新命令失败(exit ${r.status ?? r.error?.message})`;
|
|
874
|
+
}
|
|
875
|
+
const profileDir = profileDirOfModule();
|
|
876
|
+
if (profileDir) {
|
|
877
|
+
const r = spawnSync('pnpm', ['install'], { cwd: profileDir, stdio: 'inherit' });
|
|
878
|
+
return r.status === 0 ? `已更新 ${profileDir},请重启 dsh web 使新版本生效` : `pnpm install 失败(exit ${r.status ?? r.error?.message})`;
|
|
879
|
+
}
|
|
880
|
+
return '仓库 checkout 模式:请 git pull 后重新安装插件(file: 依赖指向仓库)';
|
|
881
|
+
}
|
|
882
|
+
|
|
726
883
|
/* ================= main ================= */
|
|
727
884
|
const profileArg = (() => { const i = process.argv.indexOf('--profile'); return i >= 0 ? process.argv[i + 1] : 'web'; })();
|
|
728
885
|
const sessionArg = (() => { const i = process.argv.indexOf('--session'); return i >= 0 ? process.argv[i + 1] : undefined; })();
|
|
729
886
|
|
|
730
887
|
async function run() {
|
|
731
888
|
try { checkEnv(); } catch (e) { report('env', 'E0', false, `env 检查异常: ${e.message.slice(0, 80)}`); }
|
|
889
|
+
try { await checkPort3080(); } catch (e) { report('env', 'E10-port-3080', false, `端口检查异常: ${e.message.slice(0, 60)}`); }
|
|
732
890
|
try { checkProfile(profileArg); } catch (e) { report('profile', 'P0', false, `profile 检查异常: ${e.message.slice(0, 100)}`); }
|
|
733
891
|
try { checkSession(sessionArg); } catch (e) { report('session', 'S0', false, `session 检查异常: ${e.message.slice(0, 100)}`); }
|
|
734
892
|
try { scanAllSessions(); } catch (e) { report('session', 'S11', false, `全会话扫描异常: ${e.message.slice(0, 100)}`); }
|
|
@@ -745,10 +903,22 @@ async function run() {
|
|
|
745
903
|
catalogMeta = { source: 'error', checks: 0, error: e.message.slice(0, 80) };
|
|
746
904
|
}
|
|
747
905
|
|
|
906
|
+
// 层 B:版本检查与更新(--no-catalog 同时禁用网络检查;--update 手动更新;DSH_DOCTOR_AUTO_UPDATE=1 自动)
|
|
907
|
+
const noRemote = process.argv.includes('--no-catalog');
|
|
908
|
+
let updateInfo = { current: localVersion(), latest: null, available: false };
|
|
909
|
+
try {
|
|
910
|
+
updateInfo = await checkForUpdate({ noRemote, fetchImpl: typeof fetch === 'function' ? fetch : undefined });
|
|
911
|
+
} catch (e) {
|
|
912
|
+
updateInfo = { current: localVersion(), latest: null, available: false, error: e.message.slice(0, 60) };
|
|
913
|
+
}
|
|
914
|
+
if (process.argv.includes('--update') || (process.env.DSH_DOCTOR_AUTO_UPDATE === '1' && updateInfo.available)) {
|
|
915
|
+
updateInfo.applied = runUpdate();
|
|
916
|
+
}
|
|
917
|
+
|
|
748
918
|
// 退出码只计内置失败 + catalog 中 severity=error 的失败;warn 失败提示但不改退出码
|
|
749
919
|
const bad = results.filter((r) => !r.ok && catalogSeverity.get(r.id) !== 'warn');
|
|
750
920
|
if (jsonOut) {
|
|
751
|
-
console.log(JSON.stringify({ ok: bad.length === 0, checks: results, catalog: catalogMeta }, null, 2));
|
|
921
|
+
console.log(JSON.stringify({ ok: bad.length === 0, checks: results, catalog: catalogMeta, update: updateInfo }, null, 2));
|
|
752
922
|
} else {
|
|
753
923
|
const sectionOrder = { env: 0, profile: 1, session: 2, catalog: 3 };
|
|
754
924
|
const ordered = [...results].sort((a, b) => (sectionOrder[a.section] ?? 9) - (sectionOrder[b.section] ?? 9));
|
|
@@ -760,6 +930,11 @@ async function run() {
|
|
|
760
930
|
console.log(` ${mark} [${r.id}] ${r.detail}${r.src === 'catalog' ? ' [目录]' : ''}`);
|
|
761
931
|
if (!r.ok && r.fix) console.log(` ↳ 修复: ${r.fix}`);
|
|
762
932
|
}
|
|
933
|
+
if (updateInfo.available && !updateInfo.applied) {
|
|
934
|
+
console.log(`\n⚠ 新版本 ${updateInfo.latest} 可用(当前 ${updateInfo.current})→ 运行 \`dsh-doctor --update\` 或 \`dsh plugin update\``);
|
|
935
|
+
} else if (updateInfo.applied) {
|
|
936
|
+
console.log(`\n✓ ${updateInfo.applied}`);
|
|
937
|
+
}
|
|
763
938
|
console.log(`\n${bad.length === 0 ? '✓ 全部通过' : `✗ ${bad.length} 个问题`}(profile=${profileArg},目录=${catalogMeta.source},${catalogMeta.checks} 条)`);
|
|
764
939
|
}
|
|
765
940
|
process.exit(bad.length === 0 ? 0 : 1);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Offline diagnostic for DeepSeek Harness — 19 built-in checks
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Offline diagnostic for DeepSeek Harness — 19 built-in checks + self-updating catalog (Layer A) + self-update check (Layer B); 'Doctor' panel in web UI settings.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"lib",
|