d8f8b5da1e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
// Phase 3 多进程执行:worker 进程【入口】。
|
||
//
|
||
// node worker.js <runId> (或 tsx worker.ts <runId>)
|
||
//
|
||
// daemon 在 spawn 前写好 runs/<runId>/job.json,本进程读它即可执行,【完全不碰 DB】。
|
||
// 一切进度/结果只经 protocol 的文件协议汇报:
|
||
// - 起手 emit started(pid/worktree/branch/model)
|
||
// - 周期 touchHeartbeat(daemon 据 mtime 判活)
|
||
// - runPipeline 内部 emit failed|result + done(终态)
|
||
// - 意外异常 / 收到 SIGTERM → 兜底 emit failed + done,然后退出
|
||
//
|
||
// 顶层【不 import 任何 store/db】——worker 与 DB 完全隔离。
|
||
|
||
import {
|
||
readJobSpec,
|
||
appendOutbox,
|
||
touchHeartbeat,
|
||
HEARTBEAT_INTERVAL_MS,
|
||
type OutboxPayload,
|
||
} from './protocol.js';
|
||
import { pickModel } from './models.js';
|
||
import { runPipeline, realDeps } from './pipeline.js';
|
||
|
||
async function main(): Promise<void> {
|
||
const runId = process.argv[2];
|
||
if (!runId) {
|
||
// 没 runId 没法定位 job.json / outbox,无处汇报——只能直接退出(非 0 让父进程可感知)。
|
||
process.stderr.write('worker: 缺少 runId 参数(用法:worker <runId>)\n');
|
||
process.exit(2);
|
||
}
|
||
|
||
const emit = (p: OutboxPayload): void => { appendOutbox(runId, p); };
|
||
const job = readJobSpec(runId);
|
||
|
||
// 起手汇报:pid / worktree / branch / 实际执行模型
|
||
emit({
|
||
type: 'started',
|
||
pid: process.pid,
|
||
worktree: job.worktreeDir,
|
||
branch: job.branch,
|
||
model: pickModel(job.task, job.project, 'executor'),
|
||
});
|
||
|
||
// 心跳:立即一次 + 周期刷(结束时清掉)
|
||
touchHeartbeat(runId);
|
||
const hb = setInterval(() => touchHeartbeat(runId), HEARTBEAT_INTERVAL_MS);
|
||
// 心跳定时器不应拖住事件循环退出(正常路径我们显式 exit,这里只是兜底)
|
||
hb.unref?.();
|
||
|
||
// SIGTERM=daemon 取消/超时:兜底报失败终态后干净退出(exit 0:已自报终态,不算崩溃)
|
||
let signalled = false;
|
||
process.on('SIGTERM', () => {
|
||
if (signalled) return;
|
||
signalled = true;
|
||
clearInterval(hb);
|
||
try {
|
||
emit({ type: 'failed', error: 'worker 收到 SIGTERM(取消/超时)', transcriptRef: null, sessionId: null });
|
||
emit({ type: 'done' });
|
||
} catch { /* 汇报失败也要退出,别卡死 */ }
|
||
process.exit(0);
|
||
});
|
||
|
||
try {
|
||
// runPipeline 内部已 emit failed|result + done;正常路径这里不再补发终态
|
||
await runPipeline(job, realDeps, emit);
|
||
} catch (e) {
|
||
// 意外异常(pipeline 之外或 emit 抛错等):兜底补一条 failed + done
|
||
if (!signalled) {
|
||
try {
|
||
emit({ type: 'failed', error: (e as Error).message, transcriptRef: null, sessionId: null });
|
||
emit({ type: 'done' });
|
||
} catch { /* 已尽力汇报 */ }
|
||
}
|
||
} finally {
|
||
clearInterval(hb);
|
||
}
|
||
|
||
if (!signalled) process.exit(0);
|
||
}
|
||
|
||
main().catch((e) => {
|
||
// main 自身(如 readJobSpec 抛错)兜底:尽量写一条 failed,但若连 runId 都没有就只能干退。
|
||
const runId = process.argv[2];
|
||
if (runId) {
|
||
try {
|
||
appendOutbox(runId, { type: 'failed', error: (e as Error).message, transcriptRef: null, sessionId: null });
|
||
appendOutbox(runId, { type: 'done' });
|
||
} catch { /* 无处可报 */ }
|
||
} else {
|
||
process.stderr.write(`worker 致命错误:${(e as Error).message}\n`);
|
||
}
|
||
process.exit(1);
|
||
});
|