@moonquake2004/dsh-doctor 0.4.5 → 0.4.7
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/README.md +1 -0
- package/README.zh.md +1 -0
- package/dsh-doctor.mjs +175 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ With `--envelope` (doctor-contract mode): `0` = all pass · `1` = any WARN · `2
|
|
|
53
53
|
| P13 | client-half `provide` service name clashes with core client services (`chatFileMentions` etc. from `@deepseek-ai/dsh-client-*`, warn) or cross-bundle same-name grabs (browser-side "service already registered" → UI white screen, server logs see nothing) | [#2752](https://github.com/deepseek-ai/deepseek-harness/discussions/2752) |
|
|
54
54
|
| P14 | declared `bin` executability (target file present + shebang required for text `bin`; exec-bit alone does not identify the interpreter → ENOEXEC on direct run, #1846) | [#1846](https://github.com/deepseek-ai/deepseek-harness/discussions/1846) |
|
|
55
55
|
| P16 | plugin imports a named export the installed package does not provide (warn; boot fails hard on one bad entry) | [#5864](https://github.com/deepseek-ai/deepseek-harness/discussions/5864) |
|
|
56
|
+
| P17 | client-side `require()` of a specifier the host module table cannot serve (platform seeds ∪ installed graph rows ∪ declared externals) — warn-only; catches the browser `Failed to load plugins` white screen | [#5719](https://github.com/deepseek-ai/deepseek-harness/discussions/5719) |
|
|
56
57
|
|
|
57
58
|
### session
|
|
58
59
|
| ID | Checks | Discussion |
|
package/README.zh.md
CHANGED
|
@@ -48,6 +48,7 @@ node dsh-doctor.mjs --no-catalog # 不拉远程目录(只用内置副
|
|
|
48
48
|
| P13 | client 半 `provide` 服务名抢注核心客户端服务(`chatFileMentions` 等 `@deepseek-ai/dsh-client-*`,warn)或跨 bundle 同名(浏览器端 service already registered → UI 白屏、服务端日志无感知) | [#2752](https://github.com/deepseek-ai/deepseek-harness/discussions/2752) |
|
|
49
49
|
| P14 | 声明 `bin` 可执行性(目标文件在位 + 文本 bin 必须带 shebang;仅可执行位不识别解释器 → 直接执行 ENOEXEC,#1846) | [#1846](https://github.com/deepseek-ai/deepseek-harness/discussions/1846) |
|
|
50
50
|
| P16 | 插件导入了已装包未提供的命名导出(warn;单条坏 entry 即 boot 硬失败) | [#5864](https://github.com/deepseek-ai/deepseek-harness/discussions/5864) |
|
|
51
|
+
| P17 | client 端 `require()` 的模块宿主模块表无法服务(平台种子 ∪ 已装图行 ∪ 声明的 external)——warn 级;抓浏览器 `Failed to load plugins` 白屏 | [#5719](https://github.com/deepseek-ai/deepseek-harness/discussions/5719) |
|
|
51
52
|
|
|
52
53
|
### session
|
|
53
54
|
| ID | 检查 | 对应讨论 |
|
package/dsh-doctor.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* P10 inject 引用客户端专属服务(#1947:@deepseek-ai/dsh-client-* 服务端永不提供 → Fiber 永久 PENDING → web boot 失败)
|
|
16
16
|
* P11 已装 bundle 的 main 入口产物缺失(#1965:市场装未构建源码树 → ERR_MODULE_NOT_FOUND → boot 崩)
|
|
17
17
|
* P13 client 端 provide 服务名抢注核心客户端服务 / 跨 bundle 同名(#2752:浏览器端 service already registered → UI 白屏,服务端日志无感知)
|
|
18
|
+
* P17 client 端 require 不在宿主模块表(#5719:warn 级,种子表自省自 web-frontend 产物)
|
|
18
19
|
* P16 命名导入的导出缺失(#5864:warn 级,静态自省已装包导出面)
|
|
19
20
|
* P14 declared bin 可执行性(#1846:打包成功但 bin 缺 shebang/产物 → 直接执行 ENOEXEC;与 P11 互补)
|
|
20
21
|
* P12 `installed_bundle`(#1719 v1.1 词汇:profile 内 bundle 版本 vs 运行 CLI 版本——web 面板/API 跑的是 profile 里装的 bundle,可与独立 CLI 版本不一致)
|
|
@@ -147,7 +148,7 @@ function pickNewestSessionLib(libs) {
|
|
|
147
148
|
function sessionTableFrom(lib) {
|
|
148
149
|
try {
|
|
149
150
|
const s = readFileSync(lib, 'utf8');
|
|
150
|
-
const m = /const KNOWN_SESSION_EVENT_TYPES = new Set\(\[(
|
|
151
|
+
const m = /const KNOWN_SESSION_EVENT_TYPES = new Set\(\[([\s\S]*?)\]\);/.exec(s);
|
|
151
152
|
if (!m) return null;
|
|
152
153
|
const items = [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]);
|
|
153
154
|
return items.length ? new Set(items) : null;
|
|
@@ -957,27 +958,145 @@ function packageNamedExports(pkgDir) {
|
|
|
957
958
|
} else {
|
|
958
959
|
report('profile', 'P16', true, '插件命名导入均在已装包的导出里(静态可判定的部分)', undefined);
|
|
959
960
|
}
|
|
961
|
+
|
|
962
|
+
/* P17:client 端 require 的 specifier 不在宿主模块表(#5719:dsh-client-modules 的 makeRequire 硬 throw
|
|
963
|
+
* → 浏览器端 Failed to load plugins / 白屏,服务端 HTTP 200 且日志零感知)。
|
|
964
|
+
* 可服务 ⟺ 平台种子 ∪ 图行(已装包同时有 dsh.client 与 exports["./client"])∪ 该包声明的 external/inject。
|
|
965
|
+
* 防误报(#5719 实测得出):必须先剥注释(JSDoc 里的 require("picomatch") 示例会误报)、只认双引号形态、
|
|
966
|
+
* 排除模板插值/相对路径/Node 内置、归一 /client 后缀、跳过自引用。warn 级(静态近似,宁可漏报不误报)。 */
|
|
967
|
+
const p17Issues = [];
|
|
968
|
+
const composedRows = new Set();
|
|
969
|
+
{
|
|
970
|
+
const roots = [];
|
|
971
|
+
try { roots.push(join(resolveProfile(profileArg), 'node_modules')); } catch { /* 无 profile */ }
|
|
972
|
+
for (const lib of SESSION_LIBS) roots.push(lib.slice(0, lib.indexOf(join('@deepseek-ai', 'dsh-session'))));
|
|
973
|
+
for (const nm of roots) {
|
|
974
|
+
if (!existsSync(nm)) continue;
|
|
975
|
+
const pkgs = [];
|
|
976
|
+
for (const d of readdirSync(nm)) {
|
|
977
|
+
if (d.startsWith('.')) continue;
|
|
978
|
+
if (d.startsWith('@')) { // scoped:@scope/name 两层
|
|
979
|
+
const scopeDir = join(nm, d);
|
|
980
|
+
try { for (const n of readdirSync(scopeDir)) pkgs.push(join(scopeDir, n)); } catch { /* 忽略 */ }
|
|
981
|
+
} else pkgs.push(join(nm, d));
|
|
982
|
+
}
|
|
983
|
+
for (const dir of pkgs) {
|
|
984
|
+
const pj = join(dir, 'package.json');
|
|
985
|
+
if (!existsSync(pj)) continue;
|
|
986
|
+
try {
|
|
987
|
+
const pkg = JSON.parse(readFileSync(pj, 'utf8'));
|
|
988
|
+
if (pkg.name && pkg.dsh?.client && pkg.exports?.['./client']) { composedRows.add(pkg.name); composedRows.add(`${pkg.name}/client`); }
|
|
989
|
+
} catch { /* 忽略 */ }
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const stripClientSuffix = (s) => s.replace(/\/client$/, '');
|
|
994
|
+
for (const [b, d] of bundleDirs) {
|
|
995
|
+
const declared = new Set();
|
|
996
|
+
try {
|
|
997
|
+
const pkg = JSON.parse(readFileSync(join(d, 'package.json'), 'utf8'));
|
|
998
|
+
for (const k of ['external', 'inject']) {
|
|
999
|
+
const v = pkg.dsh?.client?.[k];
|
|
1000
|
+
if (Array.isArray(v)) for (const x of v) if (typeof x === 'string') declared.add(x);
|
|
1001
|
+
}
|
|
1002
|
+
} catch { /* 无 manifest */ }
|
|
1003
|
+
const misses = new Map();
|
|
1004
|
+
for (const f of collectClientJsFiles(d)) {
|
|
1005
|
+
let code = readJs(f);
|
|
1006
|
+
code = code.replace(/\/\*[\s\S]*?\*\//g, ''); // 块注释(JSDoc 示例会误报)
|
|
1007
|
+
code = code.replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行注释(避开 https://)
|
|
1008
|
+
for (const m of code.matchAll(/require\(\s*"([^"]+)"\s*\)/g)) { // 打包产物用双引号;单引号多为文档示例
|
|
1009
|
+
const spec = m[1];
|
|
1010
|
+
if (!spec || spec.includes('${') || spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('node:')) continue;
|
|
1011
|
+
if (NODE_BUILTINS.has(spec)) continue;
|
|
1012
|
+
const id = stripClientSuffix(spec);
|
|
1013
|
+
if (CLIENT_SEEDS.has(spec) || CLIENT_SEEDS.has(id)) continue;
|
|
1014
|
+
if (composedRows.has(spec) || composedRows.has(id)) continue;
|
|
1015
|
+
if (declared.has(spec) || declared.has(id)) continue;
|
|
1016
|
+
if (id === b || spec === b) continue; // 自引用 → 打包内联
|
|
1017
|
+
misses.set(spec, relative(d, f));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (misses.size) p17Issues.push(`${b}(${[...misses].map(([s, f]) => `require("${s}") [${f}]`).join('; ')})`);
|
|
1021
|
+
}
|
|
1022
|
+
if (p17Issues.length) {
|
|
1023
|
+
report('profile', 'P17', false, `client 端 require 的模块不在宿主模块表(#5719:makeRequire 硬 throw → 浏览器白屏且服务端无感知): ${p17Issues.join('; ')}`, `改用宿主提供的模块名;若确由宿主提供,在本包 package.json 的 dsh.client.external/inject 里声明;平台种子当前 ${CLIENT_SEEDS.size} 项、已装图行 ${composedRows.size} 项`);
|
|
1024
|
+
} else {
|
|
1025
|
+
report('profile', 'P17', true, `client 端 require 的 specifier 均可服务(平台种子 ${CLIENT_SEEDS.size} 项 + 已装图行 ${composedRows.size} 项)`, undefined);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/* ---- 会话日志定位(世代感知,规范见 dsh-security/docs/session-shape-v3.md §1) ----
|
|
1030
|
+
* 命名:^session(?:\.v([1-9][0-9]*))?\.jsonl(\.zstd)?$(与 dsh-session-format 的
|
|
1031
|
+
* CANONICAL_LOG_FILENAME 一致);v0 = 无 .vN 的旧名,vN = 当前世代(本机为 v3)。
|
|
1032
|
+
* 规则:① 同一会话目录内取**最高世代**;② 跨目录按 **mtime** 取最新;
|
|
1033
|
+
* ③ 忽略 session.lock(不匹配正则);④ 目录名任意(含 _no-cwd / 无 session- 前缀);
|
|
1034
|
+
* ⑤ 同世代同时存在 .zstd 与裸 .jsonl 时**优先 .zstd**(与旧实现
|
|
1035
|
+
* `existsSync(zstd) ? zstd : plain` 一致;真实 store 遇到两种编码并存会抛
|
|
1036
|
+
* encodingMismatch,这里只是让诊断仍能看到日志)。
|
|
1037
|
+
* 旧实现只认 session.jsonl[.zstd],v3 上线后每次 S 检查都在分析 2 天前的旧世代日志。 */
|
|
1038
|
+
const SESSION_LOG_RE = /^session(?:\.v([1-9][0-9]*))?\.jsonl(\.zstd)?$/;
|
|
1039
|
+
|
|
1040
|
+
/** 解析一个文件名 → { gen, zstd };非会话日志(含 session.lock)→ null。 */
|
|
1041
|
+
function parseSessionLogName(name) {
|
|
1042
|
+
const m = SESSION_LOG_RE.exec(name);
|
|
1043
|
+
if (!m) return null;
|
|
1044
|
+
return { gen: m[1] === undefined ? 0 : Number(m[1]), zstd: m[2] === '.zstd' };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** 单个会话目录 → 该目录的权威日志(最高世代;同世代 .zstd 优先);无 → null。 */
|
|
1048
|
+
function pickSessionLogIn(dir) {
|
|
1049
|
+
let names;
|
|
1050
|
+
try { names = readdirSync(dir); } catch { return null; }
|
|
1051
|
+
let best = null;
|
|
1052
|
+
for (const n of names) {
|
|
1053
|
+
const g = parseSessionLogName(n);
|
|
1054
|
+
if (!g) continue;
|
|
1055
|
+
if (!best || g.gen > best.gen || (g.gen === best.gen && g.zstd && !best.zstd)) best = { ...g, f: join(dir, n) };
|
|
1056
|
+
}
|
|
1057
|
+
return best ? { f: best.f, gen: best.gen } : null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** 会话库全量定位:三层 sessions/<project>/<session-id>/session*.jsonl*。
|
|
1061
|
+
* loose=true 时额外接受两层散文件 sessions/<project>/session*.jsonl*——这是
|
|
1062
|
+
* "取最新会话"(S 检查默认目标 / 安全层 SR/SS)旧有的兼容行为;S11/S12 的全库扫描
|
|
1063
|
+
* 旧实现只走三层,保持 loose=false,避免把散文件误当会话(那会引入误报)。
|
|
1064
|
+
* 返回 [{ f, gen, m }](m = mtimeMs,按 mtime 降序)。 */
|
|
1065
|
+
function listSessionLogs(root, { loose = false } = {}) {
|
|
1066
|
+
const out = [];
|
|
1067
|
+
let users;
|
|
1068
|
+
try { users = readdirSync(root, { withFileTypes: true }); } catch { return out; }
|
|
1069
|
+
const add = (hit) => {
|
|
1070
|
+
try { out.push({ f: hit.f, gen: hit.gen, m: statSync(hit.f).mtimeMs }); } catch { /* race */ }
|
|
1071
|
+
};
|
|
1072
|
+
for (const u of users) {
|
|
1073
|
+
if (!u.isDirectory()) continue; // root 下的散文件不在旧实现语义内,不扩权
|
|
1074
|
+
const sd = join(root, u.name);
|
|
1075
|
+
// 两层散文件(sessions/<user>/session.jsonl[.zstd])
|
|
1076
|
+
if (loose) { const l = pickSessionLogIn(sd); if (l) add(l); }
|
|
1077
|
+
// 三层:sessions/<user>/<session-id>/session*.jsonl*
|
|
1078
|
+
let subs;
|
|
1079
|
+
try { subs = readdirSync(sd, { withFileTypes: true }); } catch { continue; }
|
|
1080
|
+
for (const s of subs) {
|
|
1081
|
+
if (!s.isDirectory()) continue;
|
|
1082
|
+
const hit = pickSessionLogIn(join(sd, s.name));
|
|
1083
|
+
if (hit) add(hit);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
out.sort((a, b) => b.m - a.m);
|
|
1087
|
+
return out;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** 最新会话日志路径(世代感知;含两层散文件兼容;无 → null)。 */
|
|
1091
|
+
function latestSessionLog(root = join(HOME, 'sessions')) {
|
|
1092
|
+
const logs = listSessionLogs(root, { loose: true });
|
|
1093
|
+
return logs.length ? logs[0].f : null;
|
|
960
1094
|
}
|
|
961
1095
|
|
|
962
1096
|
/* ================= session ================= */
|
|
963
1097
|
function checkSession(targetPath) {
|
|
964
1098
|
if (!wants('session')) return;
|
|
965
|
-
const target = targetPath || (
|
|
966
|
-
let best = null, bestM = -1;
|
|
967
|
-
const root = join(HOME, 'sessions');
|
|
968
|
-
if (!existsSync(root)) return null;
|
|
969
|
-
for (const u of readdirSync(root)) {
|
|
970
|
-
const sd = join(root, u);
|
|
971
|
-
if (!existsSync(sd)) continue;
|
|
972
|
-
for (const s of readdirSync(sd)) {
|
|
973
|
-
const f = existsSync(join(sd, s, 'session.jsonl.zstd')) ? join(sd, s, 'session.jsonl.zstd') : join(sd, s, 'session.jsonl');
|
|
974
|
-
if (!existsSync(f)) continue;
|
|
975
|
-
const m = statSync(f).mtimeMs;
|
|
976
|
-
if (m > bestM) { bestM = m; best = f; }
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return best;
|
|
980
|
-
})();
|
|
1099
|
+
const target = targetPath || latestSessionLog();
|
|
981
1100
|
if (!target || !existsSync(target)) { report('session', 'S0', true, '无会话日志,跳过单会话检查(可用 --session <path> 指定)', undefined); return; }
|
|
982
1101
|
let text;
|
|
983
1102
|
try {
|
|
@@ -1174,6 +1293,37 @@ function loadFormatCatalog() {
|
|
|
1174
1293
|
}
|
|
1175
1294
|
const FORMAT_CATALOG = loadFormatCatalog();
|
|
1176
1295
|
|
|
1296
|
+
/* P17:平台种子表 + Node 内置模块(#5719)
|
|
1297
|
+
* 种子 = 浏览器端 require 的"平台种子词",由宿主 web-frontend 构建产物决定;自省失败回退常量。 */
|
|
1298
|
+
const CLIENT_PLATFORM_SEEDS_FALLBACK = new Set([
|
|
1299
|
+
'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis',
|
|
1300
|
+
'@deepseek-ai/dsh-client-store', '@deepseek-ai/dsh-client-ui-slots',
|
|
1301
|
+
'@deepseek-ai/dsh-client-ui-primitives', '@deepseek-ai/dsh-client-ui-dockkit',
|
|
1302
|
+
]);
|
|
1303
|
+
function clientPlatformSeeds() {
|
|
1304
|
+
for (const lib of SESSION_LIBS) {
|
|
1305
|
+
const nm = lib.slice(0, lib.indexOf(join('@deepseek-ai', 'dsh-session')));
|
|
1306
|
+
const assets = join(nm, '@deepseek-ai', 'dsh-web-frontend', 'dist', 'assets');
|
|
1307
|
+
if (!existsSync(assets)) continue;
|
|
1308
|
+
for (const f of readdirSync(assets)) {
|
|
1309
|
+
if (!/^index-.*\.js$/.test(f) && !/\.js$/.test(f)) continue;
|
|
1310
|
+
try {
|
|
1311
|
+
const s = readFileSync(join(assets, f), 'utf8');
|
|
1312
|
+
const i = s.indexOf('dsh-client-ui-dockkit');
|
|
1313
|
+
if (i < 0) continue;
|
|
1314
|
+
const win = s.slice(Math.max(0, i - 1400), i + 60);
|
|
1315
|
+
const keys = [...win.matchAll(/["']?([A-Za-z@][^"':,{}]*)["']?\s*:\s*[A-Za-z_$][\w$]*/g)]
|
|
1316
|
+
.map((m) => m[1].trim()).filter((k) => /^(react|react-dom|@deepseek-ai\/)/.test(k));
|
|
1317
|
+
if (keys.length >= 8) return new Set(keys);
|
|
1318
|
+
} catch { /* 试下一个资产 */ }
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return CLIENT_PLATFORM_SEEDS_FALLBACK;
|
|
1322
|
+
}
|
|
1323
|
+
const CLIENT_SEEDS = clientPlatformSeeds();
|
|
1324
|
+
const NODE_BUILTINS = new Set(['url','path','fs','util','events','stream','buffer','crypto','os','zlib','assert','worker_threads','perf_hooks','querystring','string_decoder','timers','tty','net','http','https','child_process','process','module','v8','vm','tls','dns','readline','repl','cluster','constants','domain','punycode','sys','timers/promises','fs/promises','stream/web','stream/promises','util/types','dns/promises']);
|
|
1325
|
+
|
|
1326
|
+
|
|
1177
1327
|
/** 用**真实迁移链**在内存里跑一遍:返回 null = 可恢复,否则返回拒载原因(截断)。
|
|
1178
1328
|
* 比复刻规则更准(上游每条 fail-closed 规则都覆盖,且自动跟随版本变化)。 */
|
|
1179
1329
|
function realChainRefusal(text) {
|
|
@@ -1224,15 +1374,8 @@ function scanMigrationRefusals() {
|
|
|
1224
1374
|
}
|
|
1225
1375
|
const root = join(HOME, 'sessions');
|
|
1226
1376
|
if (!existsSync(root)) { reportSkip('session', 'S12', '无会话目录,迁移拒载预检不适用', undefined); return; }
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const sd = join(root, u);
|
|
1230
|
-
if (!existsSync(sd)) continue;
|
|
1231
|
-
for (const s of readdirSync(sd)) {
|
|
1232
|
-
const f = existsSync(join(sd, s, 'session.jsonl.zstd')) ? join(sd, s, 'session.jsonl.zstd') : join(sd, s, 'session.jsonl');
|
|
1233
|
-
if (existsSync(f)) files.push(f);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1377
|
+
// 世代感知:每个会话目录只取权威世代(v3 优先于 v0),与 store 的会话列表对齐
|
|
1378
|
+
const files = listSessionLogs(root).map((x) => x.f);
|
|
1236
1379
|
if (files.length === 0) { reportSkip('session', 'S12', '未发现会话日志', undefined); return; }
|
|
1237
1380
|
const refused = [];
|
|
1238
1381
|
let viaChain = 0;
|
|
@@ -1267,15 +1410,8 @@ function scanAllSessions() {
|
|
|
1267
1410
|
if (!wants('session')) return;
|
|
1268
1411
|
const root = join(HOME, 'sessions');
|
|
1269
1412
|
if (!existsSync(root)) { report('session', 'S11', true, '无会话目录,跳过全会话扫描', undefined); return; }
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const sd = join(root, u);
|
|
1273
|
-
if (!existsSync(sd)) continue;
|
|
1274
|
-
for (const s of readdirSync(sd)) {
|
|
1275
|
-
const f = existsSync(join(sd, s, 'session.jsonl.zstd')) ? join(sd, s, 'session.jsonl.zstd') : join(sd, s, 'session.jsonl');
|
|
1276
|
-
if (existsSync(f)) files.push(f);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1413
|
+
// 世代感知:每个会话目录只取权威世代(v3 优先于 v0),与 store 的会话列表对齐
|
|
1414
|
+
const files = listSessionLogs(root).map((x) => x.f);
|
|
1279
1415
|
if (files.length === 0) { report('session', 'S11', true, '未发现会话日志', undefined); return; }
|
|
1280
1416
|
const corrupt = []; const oversized = []; const clean = [];
|
|
1281
1417
|
let totalDS = 0; let totalEvents = 0;
|
|
@@ -1361,6 +1497,7 @@ catalogSeverity.set('P13', 'warn');
|
|
|
1361
1497
|
catalogSeverity.set('P14', 'warn');
|
|
1362
1498
|
catalogSeverity.set('P15', 'error');
|
|
1363
1499
|
catalogSeverity.set('P16', 'warn');
|
|
1500
|
+
catalogSeverity.set('P17', 'warn');
|
|
1364
1501
|
|
|
1365
1502
|
function bundledCatalog() {
|
|
1366
1503
|
const p = new URL('./checks.json', import.meta.url);
|
|
@@ -1764,31 +1901,11 @@ async function run() {
|
|
|
1764
1901
|
}
|
|
1765
1902
|
} catch { /* 配置损坏不影响检查 */ }
|
|
1766
1903
|
// 获取最新会话文件(供 SR*/SS* 检查使用)
|
|
1767
|
-
//
|
|
1768
|
-
//
|
|
1904
|
+
// 世代感知定位(与 S11 同一规则):三层 sessions/<user>/<session>/session*.jsonl*,
|
|
1905
|
+
// 目录内取最高世代(v0/v3 并存时取 v3),跨目录按 mtime 取最新,忽略 session.lock
|
|
1769
1906
|
const findLatestSession = () => {
|
|
1770
1907
|
if (sessionArg) return sessionArg;
|
|
1771
|
-
try {
|
|
1772
|
-
const root = join(HOME, 'sessions');
|
|
1773
|
-
const candidates = [];
|
|
1774
|
-
for (const u of readdirSync(root)) {
|
|
1775
|
-
const sd = join(root, u);
|
|
1776
|
-
let subs = [];
|
|
1777
|
-
try { subs = readdirSync(sd); } catch { continue; }
|
|
1778
|
-
for (const s of subs) {
|
|
1779
|
-
const zstdPath = join(sd, s, 'session.jsonl.zstd');
|
|
1780
|
-
const plainPath = join(sd, s, 'session.jsonl');
|
|
1781
|
-
const f = existsSync(zstdPath) ? zstdPath : (existsSync(plainPath) ? plainPath : null);
|
|
1782
|
-
if (!f) continue;
|
|
1783
|
-
try { candidates.push({ f, m: statSync(f).mtimeMs }); } catch { /* race */ }
|
|
1784
|
-
}
|
|
1785
|
-
// 兼容直接放在用户目录下的散文件
|
|
1786
|
-
const loose = join(sd, 'session.jsonl');
|
|
1787
|
-
if (existsSync(loose)) { try { candidates.push({ f: loose, m: statSync(loose).mtimeMs }); } catch { /* race */ } }
|
|
1788
|
-
}
|
|
1789
|
-
candidates.sort((a, b) => b.m - a.m);
|
|
1790
|
-
return candidates.length ? candidates[0].f : null;
|
|
1791
|
-
} catch { return null; }
|
|
1908
|
+
try { return latestSessionLog(); } catch { return null; }
|
|
1792
1909
|
};
|
|
1793
1910
|
|
|
1794
1911
|
const { results: secResults, exitCode: secExit, summary: secSummary } = await registry.runAll(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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": [
|