@moonquake2004/dsh-doctor 0.2.2 → 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/dsh-doctor.mjs +79 -0
- package/package.json +1 -1
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
|
* 用法:
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
45
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
52
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
@@ -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;
|
|
@@ -316,6 +368,32 @@ function checkProfile(name) {
|
|
|
316
368
|
}
|
|
317
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)');
|
|
318
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);
|
|
319
397
|
}
|
|
320
398
|
|
|
321
399
|
/* ================= session ================= */
|
|
@@ -808,6 +886,7 @@ const sessionArg = (() => { const i = process.argv.indexOf('--session'); return
|
|
|
808
886
|
|
|
809
887
|
async function run() {
|
|
810
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)}`); }
|
|
811
890
|
try { checkProfile(profileArg); } catch (e) { report('profile', 'P0', false, `profile 检查异常: ${e.message.slice(0, 100)}`); }
|
|
812
891
|
try { checkSession(sessionArg); } catch (e) { report('session', 'S0', false, `session 检查异常: ${e.message.slice(0, 100)}`); }
|
|
813
892
|
try { scanAllSessions(); } catch (e) { report('session', 'S11', false, `全会话扫描异常: ${e.message.slice(0, 100)}`); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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": [
|