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