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 { 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; 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> { 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 = [ "", "
拖拽框选区域,松开鼠标完成;Esc 取消
", "" ]; 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 }; }