Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

555 lines
18 KiB
JavaScript

#!/usr/bin/env bun
/**
* #9581 — Windows non-disruptive mouse/keyboard *effect* screen RECORDING.
*
* The capture harness (`capture-windows-desktop-evidence.mjs`) proves the input
* lands by reading the typed marker back; this companion produces the moving
* picture the issue asks for — a real screen recording of CUA input taking
* effect on a controlled Windows text-input window.
*
* gdigrab is blocked in this RDP session (BitBlt error 5), but the computeruse
* capture path (WinRT/.NET) works, so we capture a dense frame burst through it
* WHILE driving mouse_move → click → paste (progressive, chunked) → save →
* select-all, verify the marker via a saved file + clipboard/UIA read-back,
* then ffmpeg the frames into an mp4 + gif. Non-disruptive: it drives a
* freshly-launched controlled window and kills it by window id, never the
* user's apps.
*
* Run: bun scripts/record-windows-cua-input.mjs
*/
import { execFileSync } from "node:child_process";
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readClipboard, writeClipboard } from "../src/platform/clipboard.ts";
import { ComputerUseService } from "../src/services/computer-use-service.ts";
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "../../..");
const outDir = path.join(
repoRoot,
"test-results/evidence/9581-windows-cua/input-recording",
);
const framesDir = path.join(outDir, "frames");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function createRuntime(settings = {}) {
return {
character: {},
getSetting: (k) => settings[k],
getService: () => null,
};
}
function displayForPoint(displays, x, y) {
return (
displays.find((d) => {
const [dx, dy, w, h] = d.bounds;
return x >= dx && x < dx + w && y >= dy && y < dy + h;
}) ??
displays.find((d) => d.primary) ??
displays[0]
);
}
function windowHaystack(w) {
return `${w?.app ?? ""} ${w?.title ?? ""}`.toLowerCase();
}
function looksLikeTargetWindow(w, targetTitle) {
return windowHaystack(w).includes(targetTitle.toLowerCase());
}
function psSingleQuote(value) {
return `'${String(value).replace(/'/g, "''")}'`;
}
function createInputTargetScript(title, savePath) {
const savedTitle = `${title} (saved)`;
return `
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$savePath = ${psSingleQuote(savePath)}
$form = New-Object System.Windows.Forms.Form
$form.Text = ${psSingleQuote(title)}
$form.Width = 920
$form.Height = 680
$form.StartPosition = 'CenterScreen'
$form.KeyPreview = $true
$box = New-Object System.Windows.Forms.TextBox
$box.Multiline = $true
$box.AcceptsReturn = $true
$box.AcceptsTab = $true
$box.ScrollBars = 'Both'
$box.WordWrap = $false
$box.Dock = 'Fill'
$box.Font = New-Object System.Drawing.Font('Consolas', 16)
$form.Controls.Add($box)
$save = {
[System.IO.File]::WriteAllText($savePath, $box.Text)
$form.Text = ${psSingleQuote(savedTitle)}
}
$box.Add_KeyDown({
if ($_.Control -and $_.KeyCode -eq [System.Windows.Forms.Keys]::S) {
& $save
$_.SuppressKeyPress = $true
}
})
$form.Add_KeyDown({
if ($_.Control -and $_.KeyCode -eq [System.Windows.Forms.Keys]::S) {
& $save
$_.SuppressKeyPress = $true
}
})
$form.Add_Shown({ $box.Focus() })
[void]$form.ShowDialog()
`;
}
function readWindowTextViaUia(windowId) {
const pid = Number.parseInt(String(windowId), 10);
if (!Number.isFinite(pid) || pid <= 0) return "";
const ps = `
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$proc = Get-Process -Id ${pid} -ErrorAction Stop
$root = [System.Windows.Automation.AutomationElement]::FromHandle($proc.MainWindowHandle)
if ($null -eq $root) { exit 0 }
$nodes = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
$out = New-Object System.Collections.Generic.List[string]
for ($i = 0; $i -lt $nodes.Count; $i++) {
$el = $nodes.Item($i)
try {
$pattern = $null
if ($el.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref]$pattern)) {
$text = $pattern.DocumentRange.GetText(-1)
if ($text) { [void]$out.Add($text) }
}
} catch {}
try {
$pattern = $null
if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
$value = $pattern.Current.Value
if ($value) { [void]$out.Add($value) }
}
} catch {}
try {
$name = $el.Current.Name
if ($name) { [void]$out.Add($name) }
} catch {}
}
$out -join "\`n"
`;
try {
return execFileSync("powershell", ["-NoProfile", "-Command", ps], {
encoding: "utf8",
timeout: 15_000,
stdio: ["ignore", "pipe", "ignore"],
});
} catch {
return "";
}
}
let frameIndex = 0;
async function captureFrame(service, label, frames) {
const shot = await service.executeCommand("screenshot");
if (!shot.success || !shot.screenshot) {
throw new Error(`screenshot failed: ${shot.error ?? "no payload"}`);
}
const buf = Buffer.from(shot.screenshot, "base64");
const name = `frame-${String(frameIndex).padStart(3, "0")}.png`;
await writeFile(path.join(framesDir, name), buf);
frames.push({ index: frameIndex, name, label, bytes: buf.length });
frameIndex++;
return buf.length;
}
async function main() {
await rm(outDir, { recursive: true, force: true }).catch(() => {});
await mkdir(framesDir, { recursive: true });
const token = `eliza-win-cua-${Date.now()}`;
const phrase = token;
const chunks = phrase.match(/.{1,12}/g) ?? [phrase];
const targetTitle = `elizaOS CUA Input Target ${process.pid}`;
const targetTextFile = path.join(outDir, "windows-cua-input-target.txt");
const targetScript = path.join(outDir, "windows-cua-input-target.ps1");
await writeFile(targetTextFile, "", "utf8");
await writeFile(
targetScript,
createInputTargetScript(targetTitle, targetTextFile),
"utf8",
);
const service = await ComputerUseService.start(
createRuntime({
COMPUTER_USE_APPROVAL_MODE: "full_control",
// Explicit captures only — no implicit post-action screenshots (faster,
// and we control exactly which frames make the recording).
COMPUTER_USE_SCREENSHOT_AFTER_ACTION: "false",
COMPUTER_USE_BROWSER_HEADLESS: "true",
}),
);
const frames = [];
const originalClipboard = await readClipboard().catch(() => "");
let targetWindow = null;
let verified = false;
let verificationMethod = "none";
let verificationReadback = "";
try {
const displays = service.getDisplays();
const launched = await service.executeCommand("launch", {
app: "cmd.exe",
appArgs: [
"/c",
"start",
"",
"powershell.exe",
"-NoProfile",
"-STA",
"-ExecutionPolicy",
"Bypass",
"-File",
targetScript,
],
});
if (!launched.success) {
throw new Error(
`launch input target failed: ${launched.error ?? "unknown"}`,
);
}
// Resolve the real target window by the generated title. That keeps this
// run isolated from whatever user apps or restored editor sessions are open.
for (let attempt = 0; attempt < 20; attempt++) {
const found = await service.executeWindowAction({ action: "list" });
targetWindow =
(found?.windows ?? []).find((window) =>
looksLikeTargetWindow(window, targetTitle),
) ?? null;
if (targetWindow?.id) break;
await sleep(250);
}
if (!targetWindow?.id) {
throw new Error(
`could not resolve controlled input window titled ${targetTitle}`,
);
}
await service
.executeWindowAction({ action: "focus", windowId: targetWindow.id })
.catch(() => {});
await sleep(500);
await captureFrame(
service,
"controlled text target launched (empty)",
frames,
);
// Maximize the target, then read its actual bounds. Windows can preserve a
// snapped/tiled layout even after a maximize request in this RDP session, so
// display-center clicks can land in a neighboring Edge window.
await service
.executeWindowAction({ action: "maximize", windowId: targetWindow.id })
.catch(() => {});
await sleep(700);
await captureFrame(
service,
"controlled text target maximized (empty)",
frames,
);
const bounds = await service.executeWindowAction({
action: "get_window_position",
windowId: targetWindow.id,
});
if (!bounds.success || !bounds.bounds) {
throw new Error(
`could not read target bounds: ${bounds.error ?? "unknown"}`,
);
}
const b = bounds.bounds;
// Aim at the center of the multiline text box.
const globalX = Math.round(b.x + b.width / 2);
const globalY = Math.round(b.y + b.height / 2);
const display = displayForPoint(displays, globalX, globalY);
if (!display) {
throw new Error("no display available for input target");
}
const [displayX, displayY] = display.bounds;
const coordinate = [globalX - displayX, globalY - displayY];
// Click to focus + place the caret; Ctrl+End re-homes the caret so re-clicks
// between chunks never split the text.
const focusControlledTarget = async () => {
if (!targetWindow?.id) {
throw new Error("Input target window is not resolved");
}
await service
.executeWindowAction({ action: "focus", windowId: targetWindow.id })
.catch(() => {});
await sleep(200);
};
const clickFocus = async () => {
await focusControlledTarget();
await service.executeCommand("click", {
coordinate,
displayId: display.id,
});
await sleep(250);
};
const keyComboInTarget = async (key) => {
await clickFocus();
const result = await service.executeCommand("key_combo", { key });
if (!result.success) {
throw new Error(`key_combo ${key} failed: ${result.error}`);
}
await sleep(250);
};
await service
.executeCommand("mouse_move", { coordinate, displayId: display.id })
.catch(() => {});
await clickFocus();
await captureFrame(
service,
"clicked into the text area (caret active)",
frames,
);
// Progressive, chunked typing so the recording shows text appearing.
let typedSoFar = "";
for (const chunk of chunks) {
await keyComboInTarget("ctrl+End");
await writeClipboard(chunk);
await keyComboInTarget("ctrl+v");
typedSoFar += chunk;
await captureFrame(
service,
`pasted ${typedSoFar.length}/${phrase.length} chars via ctrl+v`,
frames,
);
}
await keyComboInTarget("ctrl+s");
await sleep(700);
await captureFrame(service, "ctrl+s saved the typed text file", frames);
// Ask for select-all (visible when the legacy SendKeys driver manages to
// keep focus), then verify by clipboard. On this Windows/Edge split-screen
// session SendKeys can leave focus on the text area without a visible
// selection, so fall back to UI Automation text readback from the controlled
// target window. These methods read the real app/window, not a mock.
await keyComboInTarget("ctrl+a");
await captureFrame(service, "ctrl+a requested for the typed text", frames);
await sleep(300);
await keyComboInTarget("ctrl+a");
await keyComboInTarget("ctrl+c");
await sleep(350);
const fileReadBack = await readFile(targetTextFile, "utf8").catch(() => "");
const readBack = await readClipboard().catch(() => "");
const uiaReadBack = readWindowTextViaUia(targetWindow.id);
const freshWindows = await service.executeCommand("list_windows");
const titleReadBack = Array.isArray(freshWindows.windows)
? freshWindows.windows
.filter((window) => looksLikeTargetWindow(window, targetTitle))
.map((window) => `${window.title ?? ""} ${window.app ?? ""}`.trim())
.join("\n")
: "";
if (fileReadBack.includes(token)) {
verificationReadback = fileReadBack;
verificationMethod = "saved-file";
} else if (readBack.includes(token)) {
verificationReadback = readBack;
verificationMethod = "clipboard";
} else if (uiaReadBack.includes(token)) {
verificationReadback = uiaReadBack;
verificationMethod = "uia";
} else {
verificationReadback = titleReadBack;
verificationMethod = "window-title";
}
verified = verificationReadback.includes(token);
await captureFrame(
service,
verified
? `verified: marker read back from target via ${verificationMethod}`
: "read-back did NOT contain the marker",
frames,
);
if (!verified) {
throw new Error(
`read-back missing marker; clipboard=${JSON.stringify(
readBack.slice(0, 80),
)}; savedFile=${JSON.stringify(
fileReadBack.slice(0, 120),
)}; uia=${JSON.stringify(
uiaReadBack.slice(0, 120),
)}; title=${JSON.stringify(titleReadBack.slice(0, 120))}`,
);
}
} finally {
if (targetWindow?.id) {
const killed = await service
.executeCommand("kill_app", { target: String(targetWindow.id) })
.catch(() => ({ success: false }));
if (!killed.success) {
await service
.executeCommand("close_window", { windowId: targetWindow.id })
.catch(() => {});
}
}
await writeClipboard(originalClipboard).catch(() => {});
await service.stop().catch(() => {});
}
// Assemble the frame burst into an mp4 + gif (ffmpeg reads PNG files, not the
// blocked gdigrab device). ~2.5 fps reads as a clear step-by-step recording.
const fps = "5/2";
const pattern = path.join(framesDir, "frame-%03d.png");
const mp4 = path.join(outDir, "windows-cua-input.mp4");
const gif = path.join(outDir, "windows-cua-input.gif");
const palette = path.join(outDir, "palette.png");
execFileSync(
"ffmpeg",
[
"-y",
"-framerate",
fps,
"-i",
pattern,
"-vf",
"scale=1100:-2:flags=lanczos,format=yuv420p",
"-r",
"12",
mp4,
],
{ stdio: "pipe" },
);
execFileSync(
"ffmpeg",
[
"-y",
"-framerate",
fps,
"-i",
pattern,
"-vf",
"scale=1000:-1:flags=lanczos,palettegen",
palette,
],
{ stdio: "pipe" },
);
execFileSync(
"ffmpeg",
[
"-y",
"-framerate",
fps,
"-i",
pattern,
"-i",
palette,
"-lavfi",
"scale=1000:-1:flags=lanczos[x];[x][1:v]paletteuse",
"-r",
"6",
gif,
],
{ stdio: "pipe" },
);
await rm(palette, { force: true }).catch(() => {});
const initialFrame = frames[1] ?? frames[0];
const finalFrame = frames[frames.length - 1];
const initialStill = path.join(outDir, "initial-empty-input-window.png");
const finalStill = path.join(outDir, "final-typed-selected.png");
if (initialFrame) {
await copyFile(path.join(framesDir, initialFrame.name), initialStill);
}
if (finalFrame) {
await copyFile(path.join(framesDir, finalFrame.name), finalStill);
}
const readme = `# #9581 — Windows non-disruptive mouse/keyboard effect screen recording
The capture harness proves Windows CUA input lands by reading the typed marker
back. This is the moving-picture companion: a real screen recording of CUA mouse
and keyboard input taking effect on a controlled Windows text-input window.
Captured on a real Windows 11 Pro host (QEMU), 1728x1052, via
\`plugins/plugin-computeruse/scripts/record-windows-cua-input.mjs\`:
1. launch a generated Windows Forms text target through computeruse
2. click inside the controlled text box bounds
3. progressively paste a marker with \`Ctrl+V\` while capturing frames
4. save with \`Ctrl+S\` and verify the marker from the real saved file/window
Frames are captured through the computeruse WinRT/.NET capture path and
assembled into MP4/GIF with ffmpeg.
| File | What it is |
|------|------------|
| \`windows-cua-input.gif\` | Inline recording of the run. |
| \`windows-cua-input.mp4\` | Same recording, H.264. |
| \`final-typed-selected.png\` | Final frame after verification; the typed marker is visible in the target. |
| \`initial-empty-input-window.png\` | Early frame before typing. |
| \`windows-cua-input-target.txt\` | The real text file saved by the \`Ctrl+S\` step. |
| \`windows-cua-input-target.ps1\` | The generated controlled Windows Forms target used for the run. |
| \`recording-summary.json\` | Run metadata, including \`verified: true\` and verification method. |
Verification method for this run: \`${verificationMethod}\`.
`;
await writeFile(path.join(outDir, "README.md"), readme);
const summary = {
issue: 9581,
capturedAt: new Date().toISOString(),
host: "Windows 11 Pro (QEMU)",
verified,
verificationMethod,
verificationReadbackPreview: verificationReadback.slice(0, 240),
marker: token,
phrase,
frameCount: frames.length,
frames,
artifacts: {
mp4: path.relative(repoRoot, mp4),
gif: path.relative(repoRoot, gif),
initialStill: path.relative(repoRoot, initialStill),
finalStill: path.relative(repoRoot, finalStill),
savedTextFile: path.relative(repoRoot, targetTextFile),
targetScript: path.relative(repoRoot, targetScript),
readme: path.relative(repoRoot, path.join(outDir, "README.md")),
framesDir: path.relative(repoRoot, framesDir),
},
};
await writeFile(
path.join(outDir, "recording-summary.json"),
`${JSON.stringify(summary, null, 2)}\n`,
);
console.log(
JSON.stringify(
{
status: verified ? "passed" : "failed",
...summary.artifacts,
frameCount: frames.length,
},
null,
2,
),
);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});