a42349717c
任务书 13 第一部分,用户 2026-10-03 同意。 - config:studyId 限定小写字母、数字和 ._-;task、promptDir 必须以 / 开头。 - eval:research.json 路径改绝对,writeConfig 不再转换;quality.ts 第 2、6 问每对 A、B 与 B、A 各问一次取平均,两次原始概率都存。 - tests:大写编号、相对路径各加测试。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.9 KiB
TypeScript
78 lines
3.9 KiB
TypeScript
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
|
||
// 检验脚本共用的起研究工具:检验目录放在仓库外,配置照用户的写法写一份,研究在子进程里跑(要能被 SIGKILL)。
|
||
|
||
/** 检验输出一律放在仓库外(任务书 12 第四部分第 4 条),不提交。 */
|
||
export const EVAL_ROOT = "/home/iomgaa/inforsi-eval";
|
||
|
||
/** 新建一个检验目录 <日期-名字>;已经有了就报错,不把两次检验的产物混在一起。 */
|
||
export function newEvalDir(name: string): string {
|
||
const dir = join(EVAL_ROOT, `${new Date().toISOString().slice(0, 10)}-${name}`);
|
||
if (existsSync(dir)) throw new Error(`检验目录 ${dir} 已经有了,换个名字或先挪走`);
|
||
mkdirSync(dir, { recursive: true });
|
||
return dir;
|
||
}
|
||
|
||
/** 照一份底稿写这次检验用的 research.json:换研究编号(容器名、镜像标签都带它),需要时换各角色模型。 */
|
||
export function writeConfig(base: string, path: string, studyId: string, models: unknown): string {
|
||
const config = JSON.parse(readFileSync(base, "utf8")) as Record<string, unknown>;
|
||
config["studyId"] = studyId;
|
||
if (models !== null) config["models"] = models;
|
||
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
||
return path;
|
||
}
|
||
|
||
export interface StudyProcess {
|
||
child: ChildProcess;
|
||
/** 子进程退出:退出码、信号,以及它打印的结局(被杀掉时没有,是 null)。 */
|
||
done: Promise<{ code: number | null; signal: NodeJS.Signals | null; outcome: unknown }>;
|
||
}
|
||
|
||
/** 在子进程里起 eval/lib/cli.ts。config 为 null 是接着跑。子进程的输出原样转到本进程,并记进 <研究目录>.log。 */
|
||
export function launch(config: string | null, studyDir: string, env: Record<string, string>): StudyProcess {
|
||
const args = ["eval/lib/cli.ts", ...(config === null ? ["--resume"] : ["--config", config]), "--dir", studyDir];
|
||
const child = spawn(process.execPath, args, { env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] });
|
||
const log: string[] = [];
|
||
let stdout = "";
|
||
child.stdout!.on("data", (chunk: Buffer) => {
|
||
stdout += chunk.toString();
|
||
log.push(chunk.toString());
|
||
process.stdout.write(chunk);
|
||
});
|
||
child.stderr!.on("data", (chunk: Buffer) => {
|
||
log.push(chunk.toString());
|
||
process.stderr.write(chunk);
|
||
});
|
||
const done = new Promise<{ code: number | null; signal: NodeJS.Signals | null; outcome: unknown }>((ok) =>
|
||
child.on("close", (code, signal) => {
|
||
writeFileSync(`${studyDir}.log`, log.join(""), { flag: "a" });
|
||
const line = stdout.split("\n").find((l) => l.startsWith("RESULT "));
|
||
ok({ code, signal, outcome: line === undefined ? null : JSON.parse(line.slice("RESULT ".length)) });
|
||
}),
|
||
);
|
||
return { child, done };
|
||
}
|
||
|
||
export function sleep(ms: number): Promise<void> {
|
||
return new Promise((ok) => setTimeout(ok, ms));
|
||
}
|
||
|
||
/** 本机名字以 prefix 开头的容器(含已停止的)。检验直接问 Docker,不经正式代码。 */
|
||
export function containers(prefix: string): string[] {
|
||
const names = execFileSync("docker", ["ps", "-a", "--format", "{{.Names}}"], { encoding: "utf8" });
|
||
return names.split("\n").filter((n) => n.startsWith(prefix));
|
||
}
|
||
|
||
/**
|
||
* 等 flow 把账本的表建好,检验才第一次打开账本:Ledger.open 遇到空文件会自己建表,和 flow 同时建会冲突。
|
||
* 信号是研究目录里出现 repo.git:flow 先 Ledger.open(建表),冻结配置时才建裸仓库(src/flow/loop.ts、study.ts 的 freezeConfig)。
|
||
*/
|
||
export async function waitForLedger(studyDir: string, child: ChildProcess): Promise<void> {
|
||
while (!existsSync(join(studyDir, "repo.git"))) {
|
||
if (child.exitCode !== null) throw new Error(`研究进程没建出 repo.git 就退出了,退出码 ${child.exitCode}`);
|
||
await sleep(50);
|
||
}
|
||
}
|