Files
MailUI4Agents/scripts/verify-mail-sessions-readable.mjs
JianFeeeee e0f7f2477c 修复: 验证脚本在 dsh 0.2.0 下直接崩掉(ERR_MODULE_NOT_FOUND),且漏认 v4 会话文件
升级到 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。
2026-10-02 04:05:04 +08:00

106 lines
4.9 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-
* 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);