#!/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 text = String(objective || ""); const fullUrl = text.match(/https?:\/\/[^\s,。;、))]+/i)?.[0]; if (fullUrl) { return fullUrl; } const bareDomain = text.match( /(?:访问|打开|进入|看一下|visit|open)?\s*((?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s,。;、))]*)?)/i, )?.[1]; return bareDomain ? `https://${bareDomain}` : undefined; } function cleanupSearchQuery(value) { return String(value || "") .replace(/打开\s*(?:youtube|油管)/gi, " ") .replace(/(?:youtube|油管)/gi, " ") .replace(/用浏览器打开/gi, " ") .replace(/打开浏览器/gi, " ") .replace(/找一个|找一下|搜索|搜一下|搜|播放/gi, " ") .replace(/的\s*mv/gi, " MV") .replace(/mv/gi, " MV") .replace(/[,。;;、]/g, " ") .replace(/\s+/g, " ") .trim(); } function deriveYouTubeSearchUrl(objective) { const text = String(objective || "").trim(); if (!/(youtube|油管)/i.test(text)) { return undefined; } const queryPatterns = [ /(?:找一个|找一下|搜索|搜一下|搜|播放)\s*([^,。;;]+)/i, /(?:youtube|油管)\s*([^,。;;]+)/i, ]; for (const pattern of queryPatterns) { const query = cleanupSearchQuery(text.match(pattern)?.[1]); if (query) { return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`; } } const fallbackQuery = cleanupSearchQuery(text); return fallbackQuery ? `https://www.youtube.com/results?search_query=${encodeURIComponent(fallbackQuery)}` : "https://www.youtube.com"; } 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 detectRequestedBrowserApp(objective) { const text = String(objective || "").toLowerCase(); if (text.includes("chrome") || text.includes("谷歌浏览器")) { return "Google Chrome"; } if (text.includes("safari")) { return "Safari"; } return undefined; } function resolveBrowserOpenArgs(command, objective) { const configured = parseArgsJson(process.env.BOSS_BROWSER_OPEN_ARGS_JSON) ?? parseArgs(process.env.BOSS_BROWSER_OPEN_ARGS); if (configured.length > 0) { return configured; } const requestedApp = detectRequestedBrowserApp(objective); const commandName = path.basename(command || "").toLowerCase(); return requestedApp && commandName === "open" ? ["-a", requestedApp] : []; } function escapeAppleScriptString(value) { return String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } async function openTargetUrl(targetUrl, objective) { const commandOverride = String(process.env.BOSS_BROWSER_OPEN_COMMAND || "").trim(); const command = commandOverride || "open"; const requestedApp = detectRequestedBrowserApp(objective); if (!commandOverride && process.platform === "darwin" && requestedApp) { const app = escapeAppleScriptString(requestedApp); const url = escapeAppleScriptString(targetUrl); const script = [ `tell application "${app}" to activate`, `tell application "${app}" to open location "${url}"`, ].join("\n"); await runCommand("osascript", ["-e", script]); return "osascript_open_url_executed"; } const prefixArgs = resolveBrowserOpenArgs(command, objective); await runCommand(command, [...prefixArgs, targetUrl]); return "open_url_executed"; } 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 shouldOpenVisibleBrowserAfterAutomation() { const raw = String(process.env.BOSS_BROWSER_VISIBLE_OPEN_AFTER_AUTOMATION || "").trim().toLowerCase(); return !["0", "false", "off", "no"].includes(raw); } 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(/]*>([\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) || deriveYouTubeSearchUrl(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; let automationError; let automatedTitle; if (targetUrl && !dryRun && automationMode === "playwright") { try { automatedTitle = await runBrowserAutomation(targetUrl, currentRequestId); } catch (error) { automationError = error instanceof Error ? error.message : String(error); } } const pageTitle = automatedTitle || (automationMode !== "off" ? await inspectPageTitle(targetUrl) : undefined); if (targetUrl && !dryRun) { if (automationMode === "playwright" && automatedTitle) { action = "browser_automation_executed"; if (shouldOpenVisibleBrowserAfterAutomation()) { const visibleAction = await openTargetUrl(targetUrl, objective); action = `${action}+${visibleAction}`; } } else { action = await openTargetUrl(targetUrl, objective); } } 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}${automationError ? ", automationFallback=true" : ""})` : `${action} completed (risk=${riskLevel}${automationError ? ", automationFallback=true" : ""})`, targetUrl, artifacts, });