Files
MailUI4Agents/scripts/verify-mail-sessions-readable.mjs
JianFeeeee fe0cfe626c 验收口径: --prefix 严格前缀 —— 修正 --only mail- 误匹配 agentmail- 造成的 3 个假失败
第一版验收用 --only mail- 得到「47 可读 / 3 不可读」,但那 3 个根本不是邮件会话:
目录名是普通 UUID,只是父目录 --home-program-agentmail-- 里含子串 mail-。
它们的错因是另一个独立缺陷(subagent/descriptor v2)。

换严格前缀后的真实数字:真 mail-* 会话 43/43 可读,0 不可读。
脚本现在同时提供 --only(子串)与 --prefix(目录名严格前缀)。
2026-09-19 12:04:43 +08:00

88 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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-
*
* 退出码: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>/ 目录,返回 {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);