源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const cdpUrl = args.cdpUrl ?? process.env.CDP_URL ?? "http://127.0.0.1:9825";
|
||||
const mode = args.mode ?? "preflight";
|
||||
const text = args.text ?? "Open extension settings.";
|
||||
const expectRoute = args.expectRoute ?? "";
|
||||
const requireAudioPermission = args.requireAudioPermission === true;
|
||||
|
||||
async function main() {
|
||||
const target = await pickTarget(cdpUrl);
|
||||
const client = await connectCdp(target.webSocketDebuggerUrl);
|
||||
|
||||
try {
|
||||
await waitFor(client, "Boolean(window.__openworkControl)", 15000);
|
||||
const preflight = await runPreflight(client);
|
||||
if (mode === "preflight") {
|
||||
console.log(JSON.stringify(preflight, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureVoicePanel(client);
|
||||
|
||||
if (mode === "transcript" || mode === "full") {
|
||||
const transcript = await executeControl(client, "voice.inject_transcript", { text });
|
||||
console.log(JSON.stringify({ step: "transcript", result: transcript }, null, 2));
|
||||
}
|
||||
|
||||
if (mode === "audio" || mode === "full") {
|
||||
const pcm16Base64 = await synthesizePcm16Base64(text);
|
||||
const audio = await executeControl(client, "voice.inject_audio", { pcm16Base64 });
|
||||
console.log(JSON.stringify({ step: "audio", result: audio }, null, 2));
|
||||
const proof = await collectProof(client, expectRoute);
|
||||
console.log(JSON.stringify({ step: "proof", result: proof }, null, 2));
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(values) {
|
||||
const parsed = {};
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const value = values[index];
|
||||
if (value === "--mode") parsed.mode = values[++index];
|
||||
else if (value === "--text") parsed.text = values[++index];
|
||||
else if (value === "--cdp-url") parsed.cdpUrl = values[++index];
|
||||
else if (value === "--expect-route") parsed.expectRoute = values[++index];
|
||||
else if (value === "--require-audio-permission") parsed.requireAudioPermission = true;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function runPreflight(client) {
|
||||
const userAgent = await evaluate(client, "navigator.userAgent");
|
||||
const controlReady = await evaluate(client, "Boolean(window.__openworkControl)");
|
||||
const actions = controlReady
|
||||
? await evaluate(client, "window.__openworkControl.listActions().map((action) => action.id)")
|
||||
: [];
|
||||
const media = await evaluate(client, `(${mediaPreflight.toString()})()`, true);
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
electron: typeof userAgent === "string" && userAgent.includes("Electron/"),
|
||||
userAgent,
|
||||
controlReady,
|
||||
voiceActions: actions.filter((id) => id.startsWith("voice.")),
|
||||
media,
|
||||
};
|
||||
|
||||
if (!result.electron) throw new Error("Target is not Electron.");
|
||||
if (!controlReady) throw new Error("OpenWork control API is not available.");
|
||||
if (requireAudioPermission && !media.audio.ok) {
|
||||
throw new Error(`Audio getUserMedia failed: ${media.audio.name} ${media.audio.message}`);
|
||||
}
|
||||
if (media.video.ok) throw new Error("Video getUserMedia unexpectedly succeeded; audio-only permission guard may be broken.");
|
||||
return result;
|
||||
}
|
||||
|
||||
async function mediaPreflight() {
|
||||
async function request(constraints) {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false, name: error?.name ?? "Error", message: error?.message ?? String(error) };
|
||||
}
|
||||
}
|
||||
return {
|
||||
audio: await request({ audio: true }),
|
||||
video: await request({ video: true }),
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureVoicePanel(client) {
|
||||
await evaluate(client, "window.__openworkControl.setEnabled(true)");
|
||||
await evaluate(client, "window.localStorage.setItem('openwork.extension.enabled.openwork-voice', '1'); window.dispatchEvent(new CustomEvent('openwork:extension-state-changed', { detail: { id: 'openwork-voice', enabled: true } }))");
|
||||
let actions = await evaluate(client, "window.__openworkControl.listActions().map((action) => action.id)");
|
||||
if (actions.includes("voice.inject_audio")) return;
|
||||
if (actions.includes("voice.panel.open")) {
|
||||
await executeControl(client, "voice.panel.open");
|
||||
await waitFor(client, "window.__openworkControl.listActions().some((action) => action.id === 'voice.inject_audio')", 8000);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Voice panel actions are not registered. Open a session and enable Voice Mode first. Voice actions: ${actions.filter((id) => id.startsWith("voice.")).join(", ")}`);
|
||||
}
|
||||
|
||||
async function collectProof(client, expectedRoute) {
|
||||
const started = Date.now();
|
||||
let proof = null;
|
||||
while (true) {
|
||||
proof = await readProof(client);
|
||||
const routeMatched = expectedRoute && proof.href.includes(expectedRoute);
|
||||
const acted = proof.narration.includes("Done:") || proof.narration.includes("Running");
|
||||
if (routeMatched || (!expectedRoute && acted)) return { ...proof, elapsedMs: Date.now() - started };
|
||||
if (Date.now() - started >= 60000) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
return { ...proof, elapsedMs: Date.now() - started, timedOut: true };
|
||||
}
|
||||
|
||||
async function readProof(client) {
|
||||
return evaluate(client, `({
|
||||
href: location.href,
|
||||
narration: window.__openworkControl?.snapshot?.().narration ?? "",
|
||||
route: window.__openworkControl?.snapshot?.().route ?? "",
|
||||
bodyText: document.body.innerText.slice(-2400),
|
||||
})`);
|
||||
}
|
||||
|
||||
async function executeControl(client, actionId, actionArgs = undefined) {
|
||||
const expression = `window.__openworkControl.execute(${JSON.stringify(actionId)}, ${JSON.stringify(actionArgs)})`;
|
||||
const result = await evaluate(client, expression, true);
|
||||
if (!result?.ok) throw new Error(`Control action failed: ${actionId}: ${result?.error ?? "unknown error"}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function synthesizePcm16Base64(input) {
|
||||
const ffmpeg = await requireCommand("ffmpeg", "ffmpeg is required for generated voice audio. On Daytona/Linux install it with: apt-get update && apt-get install -y ffmpeg espeak-ng");
|
||||
const tts = await findTtsCommand();
|
||||
const dir = await mkdtemp(join(tmpdir(), "openwork-voice-cdp-"));
|
||||
const source = join(dir, "speech.wav");
|
||||
const pcm = join(dir, "speech.pcm");
|
||||
|
||||
try {
|
||||
if (tts.kind === "say") {
|
||||
const aiff = join(dir, "speech.aiff");
|
||||
await execFileAsync(tts.command, ["-v", "Samantha", "-o", aiff, input]);
|
||||
await execFileAsync(ffmpeg, ["-y", "-i", aiff, "-ac", "1", "-ar", "24000", "-f", "s16le", pcm]);
|
||||
} else {
|
||||
await execFileAsync(tts.command, ["-w", source, input]);
|
||||
await execFileAsync(ffmpeg, ["-y", "-i", source, "-ac", "1", "-ar", "24000", "-f", "s16le", pcm]);
|
||||
}
|
||||
return (await readFile(pcm)).toString("base64");
|
||||
} finally {
|
||||
await rm(dir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function findTtsCommand() {
|
||||
const say = await commandPath("say");
|
||||
if (say) return { kind: "say", command: say };
|
||||
const espeakNg = await commandPath("espeak-ng");
|
||||
if (espeakNg) return { kind: "espeak", command: espeakNg };
|
||||
const espeak = await commandPath("espeak");
|
||||
if (espeak) return { kind: "espeak", command: espeak };
|
||||
throw new Error("No TTS command found. On Daytona/Linux install one with: apt-get update && apt-get install -y espeak-ng ffmpeg");
|
||||
}
|
||||
|
||||
async function requireCommand(command, message) {
|
||||
const found = await commandPath(command);
|
||||
if (!found) throw new Error(message);
|
||||
return found;
|
||||
}
|
||||
|
||||
async function commandPath(command) {
|
||||
try {
|
||||
const { stdout } = await execFileAsync("which", [command]);
|
||||
return stdout.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function pickTarget(baseUrl) {
|
||||
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/json/list`);
|
||||
if (!response.ok) throw new Error(`Could not list CDP targets: ${response.status}`);
|
||||
const targets = await response.json();
|
||||
const pages = targets.filter((target) => target.type === "page" && target.webSocketDebuggerUrl);
|
||||
const target = pages.find((page) => page.title === "OpenWork") ??
|
||||
pages.find((page) => page.url.includes("localhost") || page.url.includes("127.0.0.1") || page.url.includes("[::1]")) ??
|
||||
pages[0];
|
||||
if (!target) throw new Error("No CDP page target found.");
|
||||
return target;
|
||||
}
|
||||
|
||||
function connectCdp(webSocketDebuggerUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(webSocketDebuggerUrl);
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
let opened = false;
|
||||
|
||||
const rejectPending = (error) => {
|
||||
for (const callbacks of pending.values()) callbacks.reject(error);
|
||||
pending.clear();
|
||||
};
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
opened = true;
|
||||
resolve({
|
||||
close: () => socket.close(),
|
||||
send(method, params = {}) {
|
||||
const id = nextId++;
|
||||
return new Promise((innerResolve, innerReject) => {
|
||||
pending.set(id, { resolve: innerResolve, reject: innerReject });
|
||||
try {
|
||||
socket.send(JSON.stringify({ id, method, params }));
|
||||
} catch (error) {
|
||||
pending.delete(id);
|
||||
innerReject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(String(event.data));
|
||||
if (!message.id) return;
|
||||
const callbacks = pending.get(message.id);
|
||||
if (!callbacks) return;
|
||||
pending.delete(message.id);
|
||||
if (message.error) callbacks.reject(new Error(message.error.message));
|
||||
else callbacks.resolve(message.result);
|
||||
});
|
||||
socket.addEventListener("error", () => {
|
||||
const error = new Error("CDP websocket failed.");
|
||||
rejectPending(error);
|
||||
if (!opened) reject(error);
|
||||
});
|
||||
socket.addEventListener("close", () => {
|
||||
const error = new Error("CDP websocket closed.");
|
||||
rejectPending(error);
|
||||
if (!opened) reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function evaluate(client, expression, awaitPromise = false) {
|
||||
const result = await client.send("Runtime.evaluate", { expression, awaitPromise, returnByValue: true });
|
||||
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "Evaluation failed.");
|
||||
return result.result?.value;
|
||||
}
|
||||
|
||||
async function waitFor(client, expression, timeoutMs) {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
if (await evaluate(client, expression)) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${expression}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user