@moonquake2004/dsh-doctor 0.4.6 → 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/dsh-doctor.mjs +76 -58
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -148,7 +148,7 @@ function pickNewestSessionLib(libs) {
|
|
|
148
148
|
function sessionTableFrom(lib) {
|
|
149
149
|
try {
|
|
150
150
|
const s = readFileSync(lib, 'utf8');
|
|
151
|
-
const m = /const KNOWN_SESSION_EVENT_TYPES = new Set\(\[(
|
|
151
|
+
const m = /const KNOWN_SESSION_EVENT_TYPES = new Set\(\[([\s\S]*?)\]\);/.exec(s);
|
|
152
152
|
if (!m) return null;
|
|
153
153
|
const items = [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]);
|
|
154
154
|
return items.length ? new Set(items) : null;
|
|
@@ -1026,25 +1026,77 @@ function packageNamedExports(pkgDir) {
|
|
|
1026
1026
|
}
|
|
1027
1027
|
}
|
|
1028
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;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1029
1096
|
/* ================= session ================= */
|
|
1030
1097
|
function checkSession(targetPath) {
|
|
1031
1098
|
if (!wants('session')) return;
|
|
1032
|
-
const target = targetPath || (
|
|
1033
|
-
let best = null, bestM = -1;
|
|
1034
|
-
const root = join(HOME, 'sessions');
|
|
1035
|
-
if (!existsSync(root)) return null;
|
|
1036
|
-
for (const u of readdirSync(root)) {
|
|
1037
|
-
const sd = join(root, u);
|
|
1038
|
-
if (!existsSync(sd)) continue;
|
|
1039
|
-
for (const s of readdirSync(sd)) {
|
|
1040
|
-
const f = existsSync(join(sd, s, 'session.jsonl.zstd')) ? join(sd, s, 'session.jsonl.zstd') : join(sd, s, 'session.jsonl');
|
|
1041
|
-
if (!existsSync(f)) continue;
|
|
1042
|
-
const m = statSync(f).mtimeMs;
|
|
1043
|
-
if (m > bestM) { bestM = m; best = f; }
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
return best;
|
|
1047
|
-
})();
|
|
1099
|
+
const target = targetPath || latestSessionLog();
|
|
1048
1100
|
if (!target || !existsSync(target)) { report('session', 'S0', true, '无会话日志,跳过单会话检查(可用 --session <path> 指定)', undefined); return; }
|
|
1049
1101
|
let text;
|
|
1050
1102
|
try {
|
|
@@ -1322,15 +1374,8 @@ function scanMigrationRefusals() {
|
|
|
1322
1374
|
}
|
|
1323
1375
|
const root = join(HOME, 'sessions');
|
|
1324
1376
|
if (!existsSync(root)) { reportSkip('session', 'S12', '无会话目录,迁移拒载预检不适用', undefined); return; }
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const sd = join(root, u);
|
|
1328
|
-
if (!existsSync(sd)) continue;
|
|
1329
|
-
for (const s of readdirSync(sd)) {
|
|
1330
|
-
const f = existsSync(join(sd, s, 'session.jsonl.zstd')) ? join(sd, s, 'session.jsonl.zstd') : join(sd, s, 'session.jsonl');
|
|
1331
|
-
if (existsSync(f)) files.push(f);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1377
|
+
// 世代感知:每个会话目录只取权威世代(v3 优先于 v0),与 store 的会话列表对齐
|
|
1378
|
+
const files = listSessionLogs(root).map((x) => x.f);
|
|
1334
1379
|
if (files.length === 0) { reportSkip('session', 'S12', '未发现会话日志', undefined); return; }
|
|
1335
1380
|
const refused = [];
|
|
1336
1381
|
let viaChain = 0;
|
|
@@ -1365,15 +1410,8 @@ function scanAllSessions() {
|
|
|
1365
1410
|
if (!wants('session')) return;
|
|
1366
1411
|
const root = join(HOME, 'sessions');
|
|
1367
1412
|
if (!existsSync(root)) { report('session', 'S11', true, '无会话目录,跳过全会话扫描', undefined); return; }
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const sd = join(root, u);
|
|
1371
|
-
if (!existsSync(sd)) continue;
|
|
1372
|
-
for (const s of readdirSync(sd)) {
|
|
1373
|
-
const f = existsSync(join(sd, s, 'session.jsonl.zstd')) ? join(sd, s, 'session.jsonl.zstd') : join(sd, s, 'session.jsonl');
|
|
1374
|
-
if (existsSync(f)) files.push(f);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1413
|
+
// 世代感知:每个会话目录只取权威世代(v3 优先于 v0),与 store 的会话列表对齐
|
|
1414
|
+
const files = listSessionLogs(root).map((x) => x.f);
|
|
1377
1415
|
if (files.length === 0) { report('session', 'S11', true, '未发现会话日志', undefined); return; }
|
|
1378
1416
|
const corrupt = []; const oversized = []; const clean = [];
|
|
1379
1417
|
let totalDS = 0; let totalEvents = 0;
|
|
@@ -1863,31 +1901,11 @@ async function run() {
|
|
|
1863
1901
|
}
|
|
1864
1902
|
} catch { /* 配置损坏不影响检查 */ }
|
|
1865
1903
|
// 获取最新会话文件(供 SR*/SS* 检查使用)
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1904
|
+
// 世代感知定位(与 S11 同一规则):三层 sessions/<user>/<session>/session*.jsonl*,
|
|
1905
|
+
// 目录内取最高世代(v0/v3 并存时取 v3),跨目录按 mtime 取最新,忽略 session.lock
|
|
1868
1906
|
const findLatestSession = () => {
|
|
1869
1907
|
if (sessionArg) return sessionArg;
|
|
1870
|
-
try {
|
|
1871
|
-
const root = join(HOME, 'sessions');
|
|
1872
|
-
const candidates = [];
|
|
1873
|
-
for (const u of readdirSync(root)) {
|
|
1874
|
-
const sd = join(root, u);
|
|
1875
|
-
let subs = [];
|
|
1876
|
-
try { subs = readdirSync(sd); } catch { continue; }
|
|
1877
|
-
for (const s of subs) {
|
|
1878
|
-
const zstdPath = join(sd, s, 'session.jsonl.zstd');
|
|
1879
|
-
const plainPath = join(sd, s, 'session.jsonl');
|
|
1880
|
-
const f = existsSync(zstdPath) ? zstdPath : (existsSync(plainPath) ? plainPath : null);
|
|
1881
|
-
if (!f) continue;
|
|
1882
|
-
try { candidates.push({ f, m: statSync(f).mtimeMs }); } catch { /* race */ }
|
|
1883
|
-
}
|
|
1884
|
-
// 兼容直接放在用户目录下的散文件
|
|
1885
|
-
const loose = join(sd, 'session.jsonl');
|
|
1886
|
-
if (existsSync(loose)) { try { candidates.push({ f: loose, m: statSync(loose).mtimeMs }); } catch { /* race */ } }
|
|
1887
|
-
}
|
|
1888
|
-
candidates.sort((a, b) => b.m - a.m);
|
|
1889
|
-
return candidates.length ? candidates[0].f : null;
|
|
1890
|
-
} catch { return null; }
|
|
1908
|
+
try { return latestSessionLog(); } catch { return null; }
|
|
1891
1909
|
};
|
|
1892
1910
|
|
|
1893
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": [
|