Files
boss/scripts/browser-control-smoke.mjs

254 lines
7.3 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
import { mkdir, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { spawn } from "node:child_process";
import path from "node:path";
function writeJson(payload) {
process.stdout.write(`${JSON.stringify(payload)}\n`);
}
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
}
return chunks.join("").trim();
}
function normalizePayload(raw) {
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return {
ok: false,
error: "INVALID_BROWSER_CONTROL_PAYLOAD: expected object",
};
}
return {
ok: true,
payload: parsed,
};
} catch {
return {
ok: false,
error: "INVALID_BROWSER_CONTROL_PAYLOAD: invalid json",
};
}
}
function extractTargetUrl(objective) {
const match = String(objective || "").match(/https?:\/\/[^\s、)]+/i);
return match?.[0] || undefined;
}
async function writeArtifact(payload) {
const artifactDir = String(process.env.BOSS_CONTROL_ARTIFACT_DIR || "").trim();
if (!artifactDir) {
return [];
}
await mkdir(artifactDir, { recursive: true });
const requestId =
typeof payload.requestId === "string" && payload.requestId.trim()
? payload.requestId.trim()
: `browser-${Date.now()}`;
const artifactPath = path.join(artifactDir, `${requestId}.json`);
await writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
return [
{
kind: "json",
path: artifactPath,
},
];
}
function parseArgs(value) {
return String(value || "")
.trim()
.split(/\s+/)
.filter(Boolean);
}
function parseArgsJson(value) {
const raw = String(value || "").trim();
if (!raw) {
return undefined;
}
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.map((item) => String(item)).filter(Boolean) : undefined;
} catch {
return undefined;
}
}
function getBrowserAutomationMode() {
const raw = String(process.env.BOSS_BROWSER_AUTOMATION_MODE || "").trim().toLowerCase();
if (raw === "off" || raw === "fetch" || raw === "playwright" || raw === "auto") {
return raw;
}
return "auto";
}
function resolveCodexHome() {
return String(process.env.CODEX_HOME || "").trim() || path.join(process.env.HOME || "", ".codex");
}
function resolveBundledPlaywrightCommand() {
const wrapper = path.join(resolveCodexHome(), "skills", "playwright", "scripts", "playwright_cli.sh");
return existsSync(wrapper) ? wrapper : undefined;
}
function resolveBrowserAutomationArgs(command, commandArgs, targetUrl, requestId) {
const args = [...commandArgs];
const session =
String(process.env.BOSS_BROWSER_AUTOMATION_SESSION || "").trim() ||
String(requestId || "").trim();
if (session) {
args.push("--session", session);
}
args.push(command, targetUrl);
return args;
}
async function runCommand(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", reject);
child.on("close", (code) => {
if (code !== 0) {
reject(new Error(stderr.trim() || `browser open exit code ${code}`));
return;
}
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
});
});
}
async function runBrowserAutomation(targetUrl, requestId) {
const command =
String(process.env.BOSS_BROWSER_AUTOMATION_COMMAND || "").trim() || resolveBundledPlaywrightCommand();
if (!command || !targetUrl) {
return undefined;
}
const prefixArgs =
parseArgsJson(process.env.BOSS_BROWSER_AUTOMATION_ARGS_JSON) ??
parseArgs(process.env.BOSS_BROWSER_AUTOMATION_ARGS);
await runCommand(command, resolveBrowserAutomationArgs("open", prefixArgs, targetUrl, requestId));
const title = await runCommand(
command,
resolveBrowserAutomationArgs("eval", prefixArgs, "document.title", requestId),
);
return title.stdout || undefined;
}
async function inspectPageTitle(targetUrl) {
if (!targetUrl) {
return undefined;
}
try {
const response = await fetch(targetUrl, {
redirect: "follow",
signal: AbortSignal.timeout(8000),
});
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("text/html")) {
return undefined;
}
const html = await response.text();
const title = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]?.replace(/\s+/g, " ").trim();
return title || undefined;
} catch {
return undefined;
}
}
const raw = await readStdin();
const normalized = normalizePayload(raw);
if (!normalized.ok) {
writeJson({
status: "failed",
error: normalized.error,
});
process.exit(0);
}
const payload = normalized.payload;
const currentRequestId = typeof payload.requestId === "string" ? payload.requestId.trim() : "";
const objective =
typeof payload.objective === "string" && payload.objective.trim()
? payload.objective.trim()
: "浏览器控制 smoke 链路正常";
const targetUrl = extractTargetUrl(objective);
const riskLevel =
typeof payload.context?.riskLevel === "string" && payload.context.riskLevel.trim()
? payload.context.riskLevel.trim()
: "unknown";
const dryRun = payload.context?.dryRun === true;
let action = targetUrl ? "open_url" : "browser_smoke";
const configuredAutomationMode = getBrowserAutomationMode();
const automationMode =
configuredAutomationMode === "auto"
? String(process.env.BOSS_BROWSER_AUTOMATION_COMMAND || "").trim() || resolveBundledPlaywrightCommand()
? "playwright"
: "fetch"
: configuredAutomationMode;
const automatedTitle =
targetUrl && !dryRun && automationMode === "playwright"
? await runBrowserAutomation(targetUrl, currentRequestId)
: undefined;
const pageTitle =
automatedTitle ||
(automationMode !== "off" ? await inspectPageTitle(targetUrl) : undefined);
if (targetUrl && !dryRun) {
if (automationMode === "playwright" && automatedTitle) {
action = "browser_automation_executed";
} else {
const command = String(process.env.BOSS_BROWSER_OPEN_COMMAND || "").trim() || "open";
const prefixArgs =
parseArgsJson(process.env.BOSS_BROWSER_OPEN_ARGS_JSON) ??
parseArgs(process.env.BOSS_BROWSER_OPEN_ARGS);
await runCommand(command, [...prefixArgs, targetUrl]);
action = "open_url_executed";
}
}
const artifacts = await writeArtifact({
requestKind: payload.requestKind,
requestId: payload.requestId,
action,
objective,
targetUrl,
dryRun,
riskLevel,
capturedAt: new Date().toISOString(),
});
writeJson({
status: "completed",
requestId: typeof payload.requestId === "string" ? payload.requestId : undefined,
replyBody: pageTitle
? `浏览器控制已完成:${objective}。页面标题:${pageTitle}`
: `浏览器控制已完成:${objective}`,
executionSummary: pageTitle
? `${action} completed (risk=${riskLevel}, title=${pageTitle})`
: `${action} completed (risk=${riskLevel})`,
targetUrl,
artifacts,
});