@moonquake2004/dsh-doctor 0.2.4 → 0.2.6
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 +59 -6
- package/dsh-doctor.mjs +91 -1
- package/package.json +1 -1
package/checks.json
CHANGED
|
@@ -8,8 +8,17 @@
|
|
|
8
8
|
"severity": "error",
|
|
9
9
|
"title": "dsh 可执行文件在 PATH 中",
|
|
10
10
|
"discussion": "#1270",
|
|
11
|
-
"when": {
|
|
12
|
-
|
|
11
|
+
"when": {
|
|
12
|
+
"os": [
|
|
13
|
+
"darwin",
|
|
14
|
+
"linux",
|
|
15
|
+
"win32"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"probe": {
|
|
19
|
+
"type": "command-exists",
|
|
20
|
+
"cmd": "dsh"
|
|
21
|
+
},
|
|
13
22
|
"detailOk": "dsh 在 PATH 中可用",
|
|
14
23
|
"detailFail": "dsh 不在 PATH(插件/CLI 安装链路不可用,#1270 同族)",
|
|
15
24
|
"fix": "安装 dsh 并将 node_modules/.bin 加入 PATH"
|
|
@@ -20,7 +29,13 @@
|
|
|
20
29
|
"severity": "warn",
|
|
21
30
|
"title": "workspace-root profile 的 .npmrc 安装开关",
|
|
22
31
|
"discussion": "#20",
|
|
23
|
-
"when": {
|
|
32
|
+
"when": {
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin",
|
|
35
|
+
"linux",
|
|
36
|
+
"win32"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
24
39
|
"probe": {
|
|
25
40
|
"type": "text-contains",
|
|
26
41
|
"path": "{profile}/.npmrc",
|
|
@@ -37,7 +52,13 @@
|
|
|
37
52
|
"severity": "error",
|
|
38
53
|
"title": "patch insert name 含空格(Windows 参数 lint)",
|
|
39
54
|
"discussion": "#1420",
|
|
40
|
-
"when": {
|
|
55
|
+
"when": {
|
|
56
|
+
"os": [
|
|
57
|
+
"darwin",
|
|
58
|
+
"linux",
|
|
59
|
+
"win32"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
41
62
|
"probe": {
|
|
42
63
|
"type": "text-not-contains",
|
|
43
64
|
"path": "{profile}/cordis.patch.yml",
|
|
@@ -55,11 +76,43 @@
|
|
|
55
76
|
"severity": "error",
|
|
56
77
|
"title": "config 目录 JSON 文件合法性",
|
|
57
78
|
"discussion": "#1357",
|
|
58
|
-
"when": {
|
|
59
|
-
|
|
79
|
+
"when": {
|
|
80
|
+
"os": [
|
|
81
|
+
"darwin",
|
|
82
|
+
"linux",
|
|
83
|
+
"win32"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"probe": {
|
|
87
|
+
"type": "json-valid",
|
|
88
|
+
"path": "{home}/config/workspace.json",
|
|
89
|
+
"required": false
|
|
90
|
+
},
|
|
60
91
|
"detailOk": "config/workspace.json 为合法 JSON",
|
|
61
92
|
"detailFail": "config/workspace.json 不是合法 JSON(#1357 并发写乱码类,工作区列表会消失)",
|
|
62
93
|
"fix": "排查多实例并发写;修复或删除损坏文件"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "E11-settings-writable",
|
|
97
|
+
"section": "env",
|
|
98
|
+
"severity": "error",
|
|
99
|
+
"title": "settings.yaml 可写性(sudo 属主问题)",
|
|
100
|
+
"discussion": "#1719",
|
|
101
|
+
"when": {
|
|
102
|
+
"os": [
|
|
103
|
+
"darwin",
|
|
104
|
+
"linux",
|
|
105
|
+
"win32"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"probe": {
|
|
109
|
+
"type": "file-writable",
|
|
110
|
+
"path": "{home}/settings.yaml",
|
|
111
|
+
"required": false
|
|
112
|
+
},
|
|
113
|
+
"detailOk": "settings.yaml 可写",
|
|
114
|
+
"detailFail": "settings.yaml 不可写(sudo 安装遗留的属主/权限问题),dsh 无法持久化设置",
|
|
115
|
+
"fix": "把属主改回当前用户:sudo chown <当前用户> ~/.dsh/settings.yaml(或删掉让 dsh 重建)"
|
|
63
116
|
}
|
|
64
117
|
]
|
|
65
118
|
}
|
package/dsh-doctor.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* P4 file: 依赖指向不存在的目录(#1197:悬空 file: 链接)
|
|
11
11
|
* P5 profile 顶层 @deepseek-ai/* 与框架重复(#1486:双模块实例 → Symbol 不匹配)
|
|
12
12
|
* P7 cordis.patch.yml 结构 lint(#1724:~ insert: 是 YAML null → parsePatchList 崩溃 → UI 打不开;tab 缩进/缺冒号同族)
|
|
13
|
+
* P8 adapter provider 注册冲突(#1904②:两 bundle 抢注同一 provider → boot 时 DUPLICATE_ADAPTER 崩溃)
|
|
14
|
+
* P9 ctx.settings 未声明 inject: ['settings'](#1904⑤:先于 settings 就绪激活 → namespace not registered)
|
|
13
15
|
* [session]
|
|
14
16
|
* S1 孤儿 tool_call(#1363:assistant tool_calls 无对应 tool 结果 → INVALID_REQUEST)
|
|
15
17
|
* S2 未闭合 turn(#466/#1265:turn/start 无 turn/end → 会话永久"运行中")
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
* 退出码:0 = 全部通过;1 = 发现可修复问题(内置 + catalog severity=error);warn 级失败不改退出码。
|
|
45
47
|
*/
|
|
46
48
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
47
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
49
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
48
50
|
import { createRequire } from 'node:module';
|
|
49
51
|
import net from 'node:net';
|
|
50
52
|
import { basename, delimiter as PATH_DELIM, dirname, join } from 'node:path';
|
|
@@ -398,6 +400,84 @@ function checkProfile(name) {
|
|
|
398
400
|
}
|
|
399
401
|
if (yamlProblems.length) report('profile', 'P7', false, `cordis.patch.yml 结构错误(boot 会崩,UI 打不开 #1724): ${yamlProblems.join('; ')}`, 'patch 必须是顶层纯列表(只有 - insert: / - id: 条目):删掉顶层 key: value 行;~ 是 YAML null;缩进用空格不用 tab');
|
|
400
402
|
else report('profile', 'P7', true, 'cordis.patch.yml 结构正常(无 tab / 无 ~ insert / 无映射-序列混排)', undefined);
|
|
403
|
+
|
|
404
|
+
// P8/P9 需要扫描 bundle 构建产物:收集目录下有限深度的 .js 文件(lib/dist/根 + main 入口,跳过 node_modules)
|
|
405
|
+
const bundleDirs = new Map(); // bundle 名 → 目录(可解析的)
|
|
406
|
+
for (const b of bundles) {
|
|
407
|
+
const d = findPkg(b);
|
|
408
|
+
if (d) bundleDirs.set(b, d);
|
|
409
|
+
}
|
|
410
|
+
const collectJsFiles = (root, maxDepth = 3) => {
|
|
411
|
+
const out = [];
|
|
412
|
+
const walk = (dir, depth) => {
|
|
413
|
+
if (depth > maxDepth) return;
|
|
414
|
+
let entries;
|
|
415
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
416
|
+
for (const e of entries) {
|
|
417
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
418
|
+
// client/web 是浏览器端产物,不在宿主进程运行(避免 ctx.settings 误报)
|
|
419
|
+
if (e.isDirectory() && (e.name === 'client' || e.name === 'web')) continue;
|
|
420
|
+
const fp = join(dir, e.name);
|
|
421
|
+
if (e.isDirectory()) walk(fp, depth + 1);
|
|
422
|
+
else if (e.name.endsWith('.js') && e.name !== 'cordis.patch.yml') out.push(fp);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
walk(root, 0);
|
|
426
|
+
// 主入口(main 指向的 .js)单独兜底
|
|
427
|
+
try {
|
|
428
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
429
|
+
if (typeof pkg.main === 'string' && pkg.main.endsWith('.js')) {
|
|
430
|
+
const mp = join(root, pkg.main);
|
|
431
|
+
if (existsSync(mp) && !out.includes(mp)) out.push(mp);
|
|
432
|
+
}
|
|
433
|
+
} catch { /* 无 manifest */ }
|
|
434
|
+
return out;
|
|
435
|
+
};
|
|
436
|
+
const readJs = (fp) => { try { return readFileSync(fp, 'utf8'); } catch { return ''; } };
|
|
437
|
+
|
|
438
|
+
// P8:adapter provider 注册冲突(#1904②:两个 bundle 抢注同一 provider → boot 时 DUPLICATE_ADAPTER 崩溃)
|
|
439
|
+
const providerRegs = new Map(); // provider → Set(bundle)
|
|
440
|
+
for (const [b, d] of bundleDirs) {
|
|
441
|
+
for (const f of collectJsFiles(d)) {
|
|
442
|
+
const src = readJs(f);
|
|
443
|
+
for (const m of src.matchAll(/registerAdapter\s*\(\s*\[([^\]]*)\]/g)) {
|
|
444
|
+
for (const pm of m[1].matchAll(/['"]([^'"]+)['"]/g)) {
|
|
445
|
+
if (!providerRegs.has(pm[1])) providerRegs.set(pm[1], new Set());
|
|
446
|
+
providerRegs.get(pm[1]).add(b);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const adapterConflicts = [...providerRegs].filter(([, v]) => v.size > 1);
|
|
452
|
+
if (adapterConflicts.length) {
|
|
453
|
+
report('profile', 'P8', false, `adapter provider 注册冲突(#1904②:boot 时 DUPLICATE_ADAPTER 崩溃): ${adapterConflicts.map(([p, v]) => `${p}(${[...v].join(' ↔ ')}})`).join('; ')}`, '冲突 provider 只能注册一次:让第三方路由插件用 registerConfigurableProviders 或只注册新路由,移除抢注一方');
|
|
454
|
+
} else {
|
|
455
|
+
report('profile', 'P8', true, '无 adapter provider 注册冲突', undefined);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// P9:ctx.settings/ctx.get('settings') 未声明 settings 依赖(#1904⑤:先于 settings 就绪激活 → namespace not registered)
|
|
459
|
+
// 注意边界:sctx.settings 不算(sctx 是别的变量);ctx.inject(["settings"], cb) 运行时声明算满足
|
|
460
|
+
const injectIssues = [];
|
|
461
|
+
for (const [b, d] of bundleDirs) {
|
|
462
|
+
const files = collectJsFiles(d);
|
|
463
|
+
const all = files.map(readJs).join('\n');
|
|
464
|
+
const usesSettings = /(?<![A-Za-z0-9_$])ctx\.(?:get\(\s*['"]settings['"]\s*\)|settings\b)/.test(all);
|
|
465
|
+
if (!usesSettings) continue;
|
|
466
|
+
const declared = [];
|
|
467
|
+
// 收集全部 inject 声明(bundle 里可能混有内部模块的 inject;任一含 settings 即满足)
|
|
468
|
+
for (const m of all.matchAll(/inject\s*=\s*\[([^\]]*)\]/gs)) {
|
|
469
|
+
declared.push(...[...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]));
|
|
470
|
+
}
|
|
471
|
+
for (const m of all.matchAll(/ctx\.inject\s*\(\s*\[([^\]]*)\]/g)) {
|
|
472
|
+
declared.push(...[...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]));
|
|
473
|
+
}
|
|
474
|
+
const uniq = declared.filter((v, i) => declared.indexOf(v) === i);
|
|
475
|
+
if (!uniq.includes('settings')) {
|
|
476
|
+
injectIssues.push(`${b}(用 ctx.settings 但 settings 依赖未声明${uniq.length ? `,全部 inject: [${uniq.join(', ')}]` : ',未找到任何 inject 声明'})`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (injectIssues.length) report('profile', 'P9', false, `插件用 ctx.settings 但未声明 settings 依赖(#1904⑤:激活时 settings 可能未就绪 → namespace not registered): ${injectIssues.join('; ')}`, '在插件代码加 export const inject = ["settings"](或对可选服务做 undefined 处理)');
|
|
480
|
+
else report('profile', 'P9', true, 'bundle 的 ctx.settings 用法均声明了 settings 依赖(模块 inject 或 ctx.inject)', undefined);
|
|
401
481
|
}
|
|
402
482
|
|
|
403
483
|
/* ================= session ================= */
|
|
@@ -796,6 +876,16 @@ export function runCatalogCheck(check, ctx) {
|
|
|
796
876
|
if (count > max) return { ok: false, detail: check.detailFail ?? `${probe.pattern} 匹配 ${count} 个(> ${max})` };
|
|
797
877
|
return { ok: true, detail: check.detailOk ?? `${probe.pattern} 匹配 ${count} 个(${min}..${max})` };
|
|
798
878
|
}
|
|
879
|
+
case 'file-writable': {
|
|
880
|
+
const fp = p(probe.path);
|
|
881
|
+
if (!existsSync(fp)) return probe.required === false
|
|
882
|
+
? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
|
|
883
|
+
: { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
|
|
884
|
+
let writable = false;
|
|
885
|
+
try { const fd = openSync(fp, 'a'); closeSync(fd); writable = true; } catch { /* 只读/属主问题 */ }
|
|
886
|
+
return writable ? { ok: true, detail: check.detailOk ?? `${fp} 可写` }
|
|
887
|
+
: { ok: false, detail: check.detailFail ?? `${fp} 不可写(sudo 属主或只读权限,#1719)` };
|
|
888
|
+
}
|
|
799
889
|
default:
|
|
800
890
|
return { ok: true, skipped: true, detail: `探测原语 ${probe.type} 本引擎不支持,已跳过(需更新插件)` };
|
|
801
891
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
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": [
|