升级到 0.2.0-rc.2 后 `verify-mail-sessions-readable.mjs` **一条都验不了**:
1) 依赖不再嵌在 `<dsh>/node_modules/@deepseek-ai/`,而是平铺到
`/usr/lib/node_modules/@deepseek-ai/` ⇒ import 直接 ERR_MODULE_NOT_FOUND;
2) 会话文件新增 **v4**(`session.v4.jsonl.zstd`),walk 只认 v0+v3
⇒ 本条线索的 `mail-f8f9a840`(v3+v4 共存、**无 v0**)会被**整目录漏掉**。
★ 关键点:这两种失败都长得像"会话不可读",但**都不是**。
1 是脚本自己崩了(连候选数都出不来);2 是**漏扫**(少算而不是算错)。
—— "工具报错"与"数据坏"必须分开,否则会把脚本的年龄当成磁盘的病情。
## 实测(0.2.0-rc.2 修好后)
--prefix mail- : 候选 55 可读 55 不可读 0 <- 邮件通道全绿
mail-f8f9a840(.new 线索): 可读,1623 events
mail-d042cc4c(老线索) : 可读,56012 events
全盘 /root/.dsh/sessions : 候选 148 可读 119 不可读 29
那 29 个不可读**全部**是既有的独立缺陷
(`subagent/descriptor ... unsupported descriptor version 2`,去重后仅此一种),
**没有一个是 mail-*** ⇒ 与邮件通道无关,仍不建议混进同一个 repair。
修法:先探两个候选根再 import(不靠报错"感觉"哪个对),
并把 v4 加进 SESSION_FILES。
106 lines
4.9 KiB
JavaScript
106 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 用**生产真读路径**逐个验证 dsh 邮件会话能否读出来。
|
||
*
|
||
* 为什么单独写一个验证脚本:`repair-legacy-spliced-ids.mjs` 的自检走的是
|
||
* `sessionFormatCatalog.createRestore`(迁移链),那是"解码器口径";本脚本走的是
|
||
* `JsonlSessionPersistence.open(id, "read")` —— dsh 桥实际读盘用的那条路。
|
||
* 两者曾经在实测中给出不同结论,所以修复后的验收必须以本脚本为准。
|
||
*
|
||
* 用法:
|
||
* node scripts/verify-mail-sessions-readable.mjs # 验 mail-*
|
||
* node scripts/verify-mail-sessions-readable.mjs --root <dir> --only mail-
|
||
* node scripts/verify-mail-sessions-readable.mjs --prefix mail- # 目录名严格前缀
|
||
*
|
||
* 退出码:0 = 全部可读;1 = 存在不可读(打印 id 与逐字错误)。
|
||
*
|
||
* ★ 0.2.0 的安装布局变了(2026-09-19/20 实测):依赖**不再嵌在**
|
||
* `<dsh>/node_modules/@deepseek-ai/` 下,而是**平铺**到
|
||
* `/usr/lib/node_modules/@deepseek-ai/`;会话文件多出
|
||
* `session.v3.jsonl.zstd` 与 **v4**(`session.v4.jsonl.zstd`)。
|
||
* ⇒ 下面**先探两个位置**再 import,且 walk 认 v4 —— 否则脚本会以
|
||
* `ERR_MODULE_NOT_FOUND` 直接崩掉,而**那不是会话不可读的证据**。
|
||
*/
|
||
|
||
import { readdirSync, statSync, existsSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
import { createRequire } from "node:module";
|
||
|
||
const dsRoot = process.env.DSH_INSTALL ?? "/usr/lib/node_modules/@deepseek-ai/dsh";
|
||
// 0.1.5 是嵌套布局,0.2.0 是平铺布局;两个都试,且不靠报错来"感觉"哪个对。
|
||
const CANDIDATE_ROOTS = [
|
||
join(dsRoot, "node_modules/@deepseek-ai"),
|
||
"/usr/lib/node_modules/@deepseek-ai",
|
||
];
|
||
const NM = CANDIDATE_ROOTS.find((r) => existsSync(join(r, "dsh-session-persistence-jsonl/lib/index.js"))) ?? CANDIDATE_ROOTS[0];
|
||
const require_ = createRequire(join(NM, "x.js"));
|
||
const { default: JsonlSessionPersistence } = await import(join(NM, "dsh-session-persistence-jsonl/lib/index.js"));
|
||
|
||
const argv = process.argv.slice(2);
|
||
const argOf = (flag, dflt) => {
|
||
const i = argv.indexOf(flag);
|
||
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
||
};
|
||
const ROOT = argOf("--root", "/root/.dsh/sessions");
|
||
// `--only` 是**子串**匹配(保留原语义,便于按路径片段过滤)。
|
||
// 但它会把 `--only mail-` 误匹配到 `--home-program-agentmail--/` 下的普通会话
|
||
// (`agentmail-` 里含 `mail-`),从而把「其他缺陷」报成「邮件会话仍不可读」。
|
||
// 需要「只统计真 mail-* 会话」时用 `--prefix mail-`(目录名严格前缀)。
|
||
const ONLY = argOf("--only", "");
|
||
const PREFIX = argOf("--prefix", "");
|
||
|
||
const { Context } = await import(join(NM, "cordis/lib/index.js"));
|
||
// 桥建实例时注入的最小 ctx:Persistence 继承 cordis Service,
|
||
// 构造时会用 ctx.reflect.provide(...),所以不能拿普通字面量顶,
|
||
// 必须给一个真的 Context 实例。
|
||
const ctx = new Context();
|
||
ctx.logger = { warn() {}, info() {}, error() {}, debug() {} };
|
||
|
||
const persistence = new JsonlSessionPersistence(ctx, { root: ROOT });
|
||
|
||
// 0.2.0 起会话文件可能是 v0 / v3 / **v4**;只认 v0+v3 会漏掉 v4-only 的新会话
|
||
// (本条线索的 `mail-f8f9a840` 就是 v3+v4 共存、且**没有** v0 的那类)。
|
||
const SESSION_FILES = new Set(["session.jsonl.zstd", "session.v3.jsonl.zstd", "session.v4.jsonl.zstd"]);
|
||
|
||
/** 找出所有 <store>/<id>/ 目录,返回 {store, id}。 */
|
||
function* walk(dir) {
|
||
for (const name of readdirSync(dir)) {
|
||
const p = join(dir, name);
|
||
if (statSync(p).isDirectory()) yield* walk(p);
|
||
else if (SESSION_FILES.has(name)) yield p;
|
||
}
|
||
}
|
||
|
||
const targets = [];
|
||
for (const p of walk(ROOT)) {
|
||
if (ONLY && !p.includes(ONLY)) continue;
|
||
const id = p.split("/").at(-2);
|
||
if (PREFIX && !id.startsWith(PREFIX)) continue;
|
||
targets.push({ id, path: p });
|
||
}
|
||
|
||
let ok = 0;
|
||
const bad = [];
|
||
for (const { id, path } of targets) {
|
||
try {
|
||
const handle = await persistence.open(id, "read");
|
||
// 真正把日志读出来才算“可读”:只看 open 不够,events 必须用 handle.read()
|
||
// (handle 本身不是 async iterable,早先版本写 for await 会得 0 events)。
|
||
const slice = await handle.read(0);
|
||
const events = Array.isArray(slice) ? slice.length : (slice?.events?.length ?? slice?.length ?? 0);
|
||
if (typeof handle.close === "function") await handle.close();
|
||
ok++;
|
||
console.log(` READ OK ${id} (${events} events)`);
|
||
} catch (err) {
|
||
bad.push([id, path, String(err?.message ?? err)]);
|
||
console.log(` READ FAIL ${id}: ${String(err?.message ?? err).slice(0, 150)}`);
|
||
}
|
||
}
|
||
|
||
console.log(`\n=== 汇总 ===`);
|
||
console.log(` 候选: ${targets.length}`);
|
||
console.log(` 可读: ${ok}`);
|
||
console.log(` 不可读: ${bad.length}`);
|
||
for (const [id, , msg] of bad.slice(0, 20)) console.log(` - ${id}: ${msg.slice(0, 140)}`);
|
||
process.exit(bad.length === 0 ? 0 : 1);
|