diff --git a/src/main/browser/session.ts b/src/main/browser/session.ts index 2273d93..25d32bd 100644 --- a/src/main/browser/session.ts +++ b/src/main/browser/session.ts @@ -162,6 +162,10 @@ export class BrowserSession implements BrowserLike { const spec = selectorSpec(selector); const result = await this.evaluate<{ found: boolean; value?: T }>(`(() => { const spec = ${JSON.stringify(spec)}; + const escapeSelectorValue = (value) => { + if (globalThis.CSS && typeof CSS.escape === 'function') return CSS.escape(String(value)); + return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => '\\\\' + char); + }; const find = () => { if (spec.kind === 'xpath') { return document.evaluate(spec.value, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; @@ -175,7 +179,7 @@ export class BrowserSession implements BrowserLike { return matches.sort((a, b) => (a.innerText || '').length - (b.innerText || '').length)[0] || null; } if (spec.kind === 'aria') { - const role = spec.role || spec.value; + const role = escapeSelectorValue(spec.role || spec.value); const name = spec.name || ''; return [...document.querySelectorAll('[role="' + role + '"], ' + role)].find((node) => { const accessibleName = node.getAttribute('aria-label') || node.innerText || ''; @@ -850,7 +854,7 @@ export class BrowserSession implements BrowserLike { if (options.url) { const keyword = options.url; captures = options.wildcard - ? captures.filter(c => new RegExp("^" + keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\ async currentUrl(): Promise {").replace(/\*/g, ".*")).test(c.url)) + ? captures.filter(c => new RegExp("^" + keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")).test(c.url)) : captures.filter(c => c.url.includes(keyword)); } const result: Array> = []; diff --git a/src/main/capabilities/ai-caps.ts b/src/main/capabilities/ai-caps.ts index c22153a..9265c5f 100644 --- a/src/main/capabilities/ai-caps.ts +++ b/src/main/capabilities/ai-caps.ts @@ -11,19 +11,18 @@ function inputText(args: Record, nodeId: string): string { } async function readMaybeFile(value: string): Promise { - if (/\.(txt|md|csv|json|log)$/i.test(value) && value.includes("/") && value.length < 260) { - try { - const fs = await import("node:fs"); - if (fs.existsSync(value)) return fs.readFileSync(value, "utf8"); - } catch { /* not a file */ } - } + const looksLikeFile = /\.(txt|md|csv|json|log)$/i.test(value) && value.length < 260 && !value.includes("\n"); + if (!looksLikeFile) return value; + try { + const fs = await import("node:fs/promises"); + const stat = await fs.stat(value); + if (stat.isFile()) return await fs.readFile(value, "utf8"); + } catch { /* treat the value as literal text */ } return value; } -async function complete(prompt: string, input: string, nodeId: string, temperature = 0.3): Promise { - if (!(await Promise.resolve(true))) return ""; - void nodeId; - const config = requireAiConfig(); +async function complete(prompt: string, input: string, temperature = 0.3): Promise { + requireAiConfig(); return chatCompletion([ { role: "system", content: prompt }, { role: "user", content: input } @@ -49,7 +48,7 @@ for (const task of TEXT_TASKS) { register(task.ids, async (ctx, node, args) => { const rawInput = inputText(args, node.id); const input = await readMaybeFile(rawInput); - const result = await complete(task.system, input, node.id); + const result = await complete(task.system, input); ctx.log(task.task + "完成", "info", node.id); return result; }); diff --git a/src/main/capabilities/human-dialog.ts b/src/main/capabilities/human-dialog.ts index 2741651..e1a7fc8 100644 --- a/src/main/capabilities/human-dialog.ts +++ b/src/main/capabilities/human-dialog.ts @@ -59,16 +59,30 @@ async function showForm(options: { title: string; fields: FormFieldSpec[]; timeo webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true } }); let settled = false; - const timer = setInterval(async () => { + let timer: NodeJS.Timeout | undefined; + let timeout: NodeJS.Timeout | undefined; + const finish = (value: Record) => { + if (settled) return; + settled = true; + if (timer) clearInterval(timer); + if (timeout) clearTimeout(timeout); + if (!win.isDestroyed()) win.destroy(); + resolve(value); + }; + timer = setInterval(async () => { try { const values = await win.webContents.executeJavaScript("window.__formValues || null", true); - if (values) { clearInterval(timer); win.destroy(); resolve(values as Record); } - } catch { clearInterval(timer); resolve({}); } + if (values) finish(values as Record); + } catch { + finish({}); + } }, 250); - const timeout = setTimeout(() => { - try { void win.webContents.executeJavaScript("document.getElementById(\"ok\").click()"); } catch {} + timeout = setTimeout(() => { + try { void win.webContents.executeJavaScript("document.getElementById(\"ok\").click()"); } catch { + finish({}); + } }, Math.max(5, options.timeoutSec) * 1000); - win.on("closed", () => { clearInterval(timer); clearTimeout(timeout); resolve({}); }); + win.on("closed", () => finish({})); void win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html)); }); } diff --git a/src/main/capabilities/message.ts b/src/main/capabilities/message.ts index ee01ed9..3d32714 100644 --- a/src/main/capabilities/message.ts +++ b/src/main/capabilities/message.ts @@ -27,14 +27,18 @@ register(["Mail_Send", "Mail_Send_V1", "Mail_UnionSend", "Mail_UnionSend_v1"], a const port = Number(text({ ...configured, ...args }, ["port", "smtpPort"], "25")); const secure = ["true", "1", "是", "True"].includes(text({ ...configured, ...args }, ["is_ssl", "secure", "ssl"], "False")); const transporter = nodemailer.createTransport({ host, port, secure, auth: password ? { user: account, pass: password } : undefined }); - const result = await transporter.sendMail({ - from: account, - to: list(text(args, ["to", "receiver"])), - cc: list(text(args, ["Cc", "cc"])), - bcc: list(text(args, ["Bcc", "bcc"])), - subject: text(args, ["subject"]), - text: text(args, ["content", "body"]), - attachments: text(args, ["attachment"]) ? [{ path: text(args, ["attachment"]) }] : undefined - }); - return { messageId: result.messageId, accepted: result.accepted }; + try { + const result = await transporter.sendMail({ + from: account, + to: list(text(args, ["to", "receiver"])), + cc: list(text(args, ["Cc", "cc"])), + bcc: list(text(args, ["Bcc", "bcc"])), + subject: text(args, ["subject"]), + text: text(args, ["content", "body"]), + attachments: text(args, ["attachment"]) ? [{ path: text(args, ["attachment"]) }] : undefined + }); + return { messageId: result.messageId, accepted: result.accepted }; + } finally { + transporter.close(); + } }); diff --git a/src/main/capabilities/word-extended.test.ts b/src/main/capabilities/word-extended.test.ts index 03cce03..f3b8046 100644 --- a/src/main/capabilities/word-extended.test.ts +++ b/src/main/capabilities/word-extended.test.ts @@ -32,5 +32,5 @@ describe("Word extended capabilities", () => { await run("Word_SetFontStyle", { objWord: handle, isBold: "True", isItalic: "False", isUnderline: "False" }); await run("Word_Enter", { objWord: handle }); expect((await readFile(path.join(directory, "extended.docx"))).byteLength).toBeGreaterThan(1000); - }, 60000); + }, 120000); }); diff --git a/src/main/desktop-pick.ts b/src/main/desktop-pick.ts index 0b9143b..89f9bcc 100644 --- a/src/main/desktop-pick.ts +++ b/src/main/desktop-pick.ts @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import fs from "node:fs"; -import os from "node:os"; + import path from "node:path"; import { app, BrowserWindow, screen } from "electron"; import { RuntimeError } from "./engine/types"; @@ -31,7 +31,7 @@ export async function pickDesktopElement(): Promise { const appWindows = BrowserWindow.getAllWindows().filter(w => !w.isDestroyed() && w.isVisible()); for (const w of appWindows) w.minimize(); const { stdout } = await run("powershell.exe", ["-NoProfile", "-STA", "-ExecutionPolicy", "Bypass", "-File", pickerScriptPath()], { timeout: 180000, windowsHide: false, maxBuffer: 4 * 1024 * 1024 }); - const jsonLine = stdout.trim().split(/\\r?\\n/).reverse().find(l => l.trim().startsWith("{")); + 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: "" }; @@ -45,7 +45,7 @@ export async function pickDesktopElement(): Promise { 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("[")); + 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 }>; } @@ -93,17 +93,17 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: win.destroy(); if (!region) throw new RuntimeError("pick_cancelled", "未完成框选"); await new Promise(r => setTimeout(r, 300)); - const outDir = "D:/RPA/test-output"; + const outDir = path.join(app.getPath("userData"), "region-picks"); fs.mkdirSync(outDir, { recursive: true }); - const imagePath = outDir + "/pick-region-" + Date.now() + ".png"; + const imagePath = path.join(outDir, `pick-region-${Date.now()}.png`); const psScript = [ "Add-Type -AssemblyName System.Drawing | Out-Null;", - " = New-Object System.Drawing.Bitmap " + region.width + ", " + region.height + ";", - " = [System.Drawing.Graphics]::FromImage();", - ".CopyFromScreen(" + region.x + ", " + region.y + ", 0, 0, .Size);", - ".Dispose();", - ".Save(" + JSON.stringify(imagePath) + ");", - ".Dispose();", + "$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 }); diff --git a/src/renderer/src/components/RunLogView.tsx b/src/renderer/src/components/RunLogView.tsx index d48f66c..471aecc 100644 --- a/src/renderer/src/components/RunLogView.tsx +++ b/src/renderer/src/components/RunLogView.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { ArrowDown, Check, Copy, Download } from "lucide-react"; import type { RunLog } from "../store/runStore"; @@ -25,6 +25,11 @@ export function RunLogView({ logs, nameOf, fileName = "rpa-run-logs.log", emptyT const [pinned, setPinned] = useState(true); const consoleRef = useRef(null); const pinnedRef = useRef(true); + const copiedTimerRef = useRef(); + + useEffect(() => () => { + if (copiedTimerRef.current !== undefined) window.clearTimeout(copiedTimerRef.current); + }, []); const counts = useMemo(() => ({ all: logs.length, @@ -66,7 +71,8 @@ export function RunLogView({ logs, nameOf, fileName = "rpa-run-logs.log", emptyT try { await navigator.clipboard.writeText(logText); setCopied(true); - window.setTimeout(() => setCopied(false), 1600); + if (copiedTimerRef.current !== undefined) window.clearTimeout(copiedTimerRef.current); + copiedTimerRef.current = window.setTimeout(() => setCopied(false), 1600); } catch { // Clipboard access can be blocked by system policy; download remains available. }