#!/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
--only mail-
*
* 退出码:0 = 全部可读;1 = 存在不可读(打印 id 与逐字错误)。
*/
import { readdirSync, statSync } from "node:fs";
import { join } from "node:path";
const dsRoot = process.env.DSH_INSTALL ?? "/usr/lib/node_modules/@deepseek-ai/dsh";
const NM = join(dsRoot, "node_modules/@deepseek-ai");
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 });
/** 找出所有 // 目录,返回 {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 (name === "session.jsonl.zstd" || name === "session.v3.jsonl.zstd") 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);