|
|
import fs from "node:fs";
|
|
|
import path from "node:path";
|
|
|
import readline from "node:readline";
|
|
|
import { stdin as input, stdout as output } from "node:process";
|
|
|
import { PHASE0_DIR } from "./paths";
|
|
|
|
|
|
export const TRIGGER_CHECKPOINT_A = path.join(PHASE0_DIR, "trigger-checkpoint-a");
|
|
|
export const TRIGGER_CHECKPOINT_B = path.join(PHASE0_DIR, "trigger-checkpoint-b");
|
|
|
export const RECORDING_STATUS = path.join(PHASE0_DIR, "recording.status.json");
|
|
|
|
|
|
const POLL_MS = 400;
|
|
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
}
|
|
|
|
|
|
function consumeTrigger(triggerPath: string): void {
|
|
|
if (fs.existsSync(triggerPath)) {
|
|
|
fs.rmSync(triggerPath, { force: true });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function waitForTriggerFile(
|
|
|
triggerPath: string,
|
|
|
label: string,
|
|
|
): Promise<void> {
|
|
|
consumeTrigger(triggerPath);
|
|
|
const fileName = path.basename(triggerPath);
|
|
|
console.log(`
|
|
|
[phase0] ${label} — 文件触发模式
|
|
|
在项目根目录另开终端执行:
|
|
|
PowerShell: New-Item -ItemType File -Force -Path ".rpa\\phase0\\${fileName}"
|
|
|
CMD: type nul > .rpa\\phase0\\${fileName}
|
|
|
等待文件: ${triggerPath}
|
|
|
`);
|
|
|
|
|
|
while (!fs.existsSync(triggerPath)) {
|
|
|
await sleep(POLL_MS);
|
|
|
}
|
|
|
consumeTrigger(triggerPath);
|
|
|
console.log(`[phase0] ${label} 已触发 (${fileName})`);
|
|
|
}
|
|
|
|
|
|
export function writeRecordingStatus(payload: Record<string, unknown>): void {
|
|
|
fs.mkdirSync(PHASE0_DIR, { recursive: true });
|
|
|
fs.writeFileSync(
|
|
|
RECORDING_STATUS,
|
|
|
`${JSON.stringify({ updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`,
|
|
|
"utf8",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 等待人工确认。优先 TTY Enter;非 TTY 或 PHASE0_USE_FILE_TRIGGER=1 时用触发文件。
|
|
|
*/
|
|
|
export async function waitForCheckpoint(options: {
|
|
|
label: string;
|
|
|
message: string;
|
|
|
triggerPath: string;
|
|
|
rl?: readline.Interface | null;
|
|
|
}): Promise<void> {
|
|
|
const useFileTrigger =
|
|
|
process.env.PHASE0_USE_FILE_TRIGGER === "1" || !process.stdin.isTTY;
|
|
|
|
|
|
console.log(`\n[phase0] ── ${options.label} ──`);
|
|
|
console.log(options.message);
|
|
|
|
|
|
if (useFileTrigger) {
|
|
|
await waitForTriggerFile(options.triggerPath, options.label);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const rl =
|
|
|
options.rl ??
|
|
|
readline.createInterface({ input, output, terminal: true });
|
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
rl.question("\n按 Enter 继续… ", () => {
|
|
|
resolve();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
if (!options.rl) {
|
|
|
rl.close();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export function createSharedReadline(): readline.Interface {
|
|
|
return readline.createInterface({ input, output, terminal: true });
|
|
|
}
|