PRA-only-demo/src/main/desktop-pick.ts

117 lines
6.9 KiB
TypeScript
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.

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import fs from "node:fs";
import path from "node:path";
import { app, BrowserWindow, screen } from "electron";
import { RuntimeError } from "./engine/types";
const run = promisify(execFile);
function pickerScriptPath(): string {
const candidates = [
path.join(process.resourcesPath || "", "desktop-picker.ps1"),
path.join(app.getAppPath(), "resources", "desktop-picker.ps1")
].filter(Boolean);
const found = candidates.find(p => fs.existsSync(p));
if (!found) throw new RuntimeError("picker_unavailable", "桌面拾取脚本缺失", { recommendation: "重新安装应用" });
return found;
}
export interface DesktopPickResult {
cancelled?: boolean;
window?: string;
name?: string;
automationId?: string;
controlType?: string;
className?: string;
selector: string;
}
export async function pickDesktopElement(): Promise<DesktopPickResult> {
const appWindows = BrowserWindow.getAllWindows().filter(w => !w.isDestroyed());
for (const w of appWindows) w.minimize();
try {
const { stdout } = await run("powershell.exe", ["-NoProfile", "-STA", "-ExecutionPolicy", "Bypass", "-File", pickerScriptPath()], { timeout: 180000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 });
const jsonLine = stdout.trim().split(/\r?\n/).reverse().find(l => l.trim().startsWith("{"));
if (!jsonLine) throw new RuntimeError("picker_failed", "拾取未返回结果");
const parsed = JSON.parse(jsonLine) as Omit<DesktopPickResult, "selector">;
if (parsed.cancelled) return { ...parsed, selector: "" };
const parts: string[] = [];
if (parsed.window) parts.push("window=" + parsed.window);
if (parsed.name) parts.push("name=" + parsed.name);
if (parsed.controlType) parts.push("type=" + parsed.controlType);
if (parsed.automationId) parts.push("id=" + parsed.automationId);
return { ...parsed, selector: parts.join(";") };
} finally {
for (const w of appWindows) {
if (!w.isDestroyed()) w.restore();
}
}
}
export async function listVisibleWindows(): Promise<Array<{ title: string; process: string }>> {
const { stdout } = await run("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", pickerScriptPath(), "-Mode", "windows"], { timeout: 30000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 });
const jsonLine = stdout.trim().split(/\r?\n/).reverse().find(l => l.trim().startsWith("["));
if (!jsonLine) return [];
return JSON.parse(jsonLine) as Array<{ title: string; process: string }>;
}
export async function pickScreenRegion(): Promise<{ x: number; y: number; width: number; height: number; imagePath: string }> {
const appWindows = BrowserWindow.getAllWindows().filter(w => !w.isDestroyed());
for (const w of appWindows) w.minimize();
const display = screen.getPrimaryDisplay();
const sx = display.bounds.x, sy = display.bounds.y, sw = display.bounds.width, sh = display.bounds.height;
const win = new BrowserWindow({
x: sx, y: sy, width: sw, height: sh,
frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, resizable: false, movable: false,
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true }
});
const htmlParts = [
"<!DOCTYPE html><html><head><meta charset='utf-8'><style>",
"html,body{margin:0;height:100%;background:rgba(0,30,80,0.25);cursor:crosshair;overflow:hidden}",
"#sel{position:absolute;border:2px solid #2563eb;background:rgba(37,99,235,0.15);display:none}",
"#tip{position:fixed;top:12px;left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,0.65);padding:8px 18px;border-radius:6px;font-family:Microsoft YaHei;font-size:14px}",
"</style></head><body>",
"<div id=sel></div><div id=tip>拖拽框选区域,松开鼠标完成;Esc 取消</div>",
"<script>",
"var sel = document.getElementById('sel'); var start = null;",
"document.addEventListener('mousedown', function(e) { start = [e.clientX, e.clientY]; sel.style.display = 'block'; sel.style.left = e.clientX + 'px'; sel.style.top = e.clientY + 'px'; sel.style.width = '0px'; sel.style.height = '0px'; });",
"document.addEventListener('mousemove', function(e) { if (!start) return; sel.style.left = Math.min(start[0], e.clientX) + 'px'; sel.style.top = Math.min(start[1], e.clientY) + 'px'; sel.style.width = Math.abs(e.clientX - start[0]) + 'px'; sel.style.height = Math.abs(e.clientY - start[1]) + 'px'; });",
"document.addEventListener('mouseup', function(e) { if (!start) return; window.__region = { x: Math.min(start[0], e.clientX), y: Math.min(start[1], e.clientY), width: Math.abs(e.clientX - start[0]), height: Math.abs(e.clientY - start[1]) }; window.__done = true; });",
"document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { window.__cancelled = true; window.__done = true; } });",
"</script></body></html>"
];
void win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(htmlParts.join("")));
const deadline = Date.now() + 60000;
let region: { x: number; y: number; width: number; height: number } | null = null;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 250));
try {
const state = await win.webContents.executeJavaScript("JSON.stringify({ done: window.__done === true, cancelled: window.__cancelled === true, region: window.__region || null })", true);
const parsed = JSON.parse(state) as { done: boolean; cancelled: boolean; region: { x: number; y: number; width: number; height: number } | null };
if (parsed.cancelled) { win.destroy(); throw new RuntimeError("pick_cancelled", "已取消框选"); }
if (parsed.done && parsed.region && parsed.region.width > 2 && parsed.region.height > 2) { region = parsed.region; break; }
if (parsed.done) break;
} catch (error) {
if ((error as Error).message.includes("已取消框选")) throw error;
}
}
for (const w of appWindows) { if (!w.isDestroyed()) { w.show(); w.restore(); w.focus(); } }
win.destroy();
if (!region) throw new RuntimeError("pick_cancelled", "未完成框选");
await new Promise(r => setTimeout(r, 300));
const outDir = path.join(app.getPath("userData"), "region-picks");
fs.mkdirSync(outDir, { recursive: true });
const imagePath = path.join(outDir, `pick-region-${Date.now()}.png`);
const psScript = [
"Add-Type -AssemblyName System.Drawing | Out-Null;",
"$bitmap = New-Object System.Drawing.Bitmap " + region.width + ", " + region.height + ";",
"$graphics = [System.Drawing.Graphics]::FromImage();",
"$graphics.CopyFromScreen(" + region.x + ", " + region.y + ", 0, 0, $bitmap.Size);",
"$graphics.Dispose();",
"$bitmap.Save(" + JSON.stringify(imagePath) + ");",
"$bitmap.Dispose();",
"Write-Output ok"
].join("");
await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psScript], { timeout: 30000, windowsHide: true });
return { ...region, imagePath };
}