fix: 修复质量检查发现的选择器与资源问题
修复浏览器通配符过滤和 ARIA 选择器转义,收敛表单窗口生命周期,释放邮件连接,清理日志复制定时器,并校准 Word COM 测试超时。
This commit is contained in:
parent
745da55e4e
commit
cc28d49ab5
@ -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<string> {").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<Record<string, unknown>> = [];
|
||||
|
||||
@ -11,19 +11,18 @@ function inputText(args: Record<string, unknown>, nodeId: string): string {
|
||||
}
|
||||
|
||||
async function readMaybeFile(value: string): Promise<string> {
|
||||
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<string> {
|
||||
if (!(await Promise.resolve(true))) return "";
|
||||
void nodeId;
|
||||
const config = requireAiConfig();
|
||||
async function complete(prompt: string, input: string, temperature = 0.3): Promise<string> {
|
||||
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;
|
||||
});
|
||||
|
||||
@ -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<string, string>) => {
|
||||
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<string, string>); }
|
||||
} catch { clearInterval(timer); resolve({}); }
|
||||
if (values) finish(values as Record<string, string>);
|
||||
} 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));
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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<DesktopPickResult> {
|
||||
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<DesktopPickResult, "selector">;
|
||||
if (parsed.cancelled) return { ...parsed, selector: "" };
|
||||
@ -45,7 +45,7 @@ export async function pickDesktopElement(): Promise<DesktopPickResult> {
|
||||
|
||||
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("["));
|
||||
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 });
|
||||
|
||||
@ -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<HTMLDivElement>(null);
|
||||
const pinnedRef = useRef(true);
|
||||
const copiedTimerRef = useRef<number>();
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user