Files
boss/scripts/ssh-computer-use-smoke.mjs

203 lines
5.6 KiB
JavaScript
Executable File
Raw Permalink 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 { spawn } from "node:child_process";
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 parsePayload(raw) {
try {
const parsed = JSON.parse(raw || "{}");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("expected object");
}
return parsed;
} catch {
return null;
}
}
function envString(name) {
return String(process.env[name] || "").trim();
}
function envBoolean(name) {
return envString(name).toLowerCase() === "true";
}
function detectTargetApp(objective) {
const text = String(objective || "").toLowerCase();
const candidates = [
["Chrome", ["chrome", "谷歌浏览器"]],
["Safari", ["safari"]],
["Finder", ["finder", "访达"]],
["System Settings", ["system settings", "系统设置", "设置"]],
["QQ", ["qq"]],
["WeChat", ["wechat", "微信"]],
["Telegram", ["telegram"]],
];
for (const [name, aliases] of candidates) {
if (aliases.some((alias) => text.includes(alias.toLowerCase()))) {
return name;
}
}
return "Finder";
}
function extractQuotedText(objective) {
const text = String(objective || "");
const patterns = [
/[“"]([^“”"]+)[”"]/,
/[「『]([^」』]+)[」』]/,
/输入[:]\s*([^\n。;]+)/,
/打字[:]\s*([^\n。;]+)/,
];
for (const pattern of patterns) {
const match = text.match(pattern);
const value = match?.[1]?.trim();
if (value) return value;
}
return undefined;
}
function shouldSubmitAfterTyping(objective) {
const text = String(objective || "").toLowerCase();
return text.includes("发送") || text.includes("提交") || text.includes("回车") || text.includes("enter");
}
function escapeAppleScriptString(value) {
return String(value || "").replaceAll("\\", "\\\\").replaceAll('"', '\\"');
}
function buildAppleScript(targetApp, objective) {
const lines = [
`tell application "${escapeAppleScriptString(targetApp)}"`,
"activate",
"end tell",
];
const typedText = extractQuotedText(objective);
if (typedText) {
lines.push("delay 0.2");
lines.push('tell application "System Events"');
lines.push(`keystroke "${escapeAppleScriptString(typedText)}"`);
if (shouldSubmitAfterTyping(objective)) {
lines.push("key code 36");
}
lines.push("end tell");
}
return lines.join("\n");
}
function runCommand(command, args, env = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
env: {
...process.env,
...env,
},
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() || `ssh computer use exited with ${code}`));
return;
}
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
});
});
}
async function runRemoteAppleScript(script) {
const host = envString("BOSS_SSH_CONTROL_HOST");
const user = envString("BOSS_SSH_CONTROL_USER");
const port = envString("BOSS_SSH_CONTROL_PORT") || "22";
const password = envString("BOSS_SSH_CONTROL_PASSWORD");
if (!host) throw new Error("SSH_CONTROL_HOST_REQUIRED");
if (!user) throw new Error("SSH_CONTROL_USER_REQUIRED");
const sshTarget = `${user}@${host}`;
const encodedScript = Buffer.from(script, "utf8").toString("base64");
const remoteCommand = `printf '%s' '${encodedScript}' | base64 -D | osascript`;
const sshArgs = [
"-o",
"StrictHostKeyChecking=no",
"-o",
"ConnectTimeout=8",
"-o",
"PreferredAuthentications=password",
"-o",
"PubkeyAuthentication=no",
"-o",
"NumberOfPasswordPrompts=1",
"-p",
port,
sshTarget,
remoteCommand,
];
if (password) {
await runCommand("sshpass", ["-e", "ssh", ...sshArgs], { SSHPASS: password });
return;
}
await runCommand("ssh", sshArgs);
}
const payload = parsePayload(await readStdin());
if (!payload) {
writeJson({ status: "failed", error: "INVALID_SSH_COMPUTER_USE_PAYLOAD" });
process.exit(0);
}
const requestId = typeof payload.requestId === "string" ? payload.requestId : undefined;
const objective = typeof payload.objective === "string" && payload.objective.trim()
? payload.objective.trim()
: "远程桌面控制 smoke 链路测试";
const targetApp = detectTargetApp(objective);
const typedText = extractQuotedText(objective);
try {
if (!envString("BOSS_SSH_CONTROL_HOST")) {
throw new Error("SSH_CONTROL_HOST_REQUIRED");
}
if (!envString("BOSS_SSH_CONTROL_USER")) {
throw new Error("SSH_CONTROL_USER_REQUIRED");
}
const appleScript = buildAppleScript(targetApp, objective);
if (!envBoolean("BOSS_SSH_CONTROL_DRY_RUN")) {
await runRemoteAppleScript(appleScript);
}
writeJson({
status: "completed",
requestId,
replyBody: `SSH 桌面控制已完成:${objective}`,
executionSummary: `ssh osascript ${envBoolean("BOSS_SSH_CONTROL_DRY_RUN") ? "dry-run" : "executed"} (${targetApp})`,
targetApp,
typedText,
});
} catch (error) {
writeJson({
status: "failed",
requestId,
error: error instanceof Error ? error.message : "SSH_COMPUTER_USE_FAILED",
});
}