feat: complete remaining capabilities and hide integration status
This commit is contained in:
1 parent
66169590de
commit
95fb07e876
20 files changed
+15102
-201
No files matched your search
@@ -40,7 +40,7 @@ async function grabScreen(imagePath: string): Promise<void> {
|
||||
|
||||
interface OcrLine { text: string; box?: number[][]; mean?: number }
|
||||
|
||||
async function locateTextOnScreen(target: string, nodeId: string): Promise<{ target: string; text: string; cx: number; cy: number }> {
|
||||
export async function locateTextOnScreen(target: string, nodeId: string): Promise<{ target: string; text: string; cx: number; cy: number }> {
|
||||
const shotPath = path.join(os.tmpdir(), "rpa-cv-screen-" + Date.now() + ".png");
|
||||
await grabScreen(shotPath);
|
||||
const ocr = await getOcr();
|
||||
|
||||
@@ -35,6 +35,8 @@ import "./data-extended";
|
||||
import "./utility-extended";
|
||||
import "./data-table-common";
|
||||
import "./basic-encryption";
|
||||
import "./mail-remaining";
|
||||
import "./remaining-core";
|
||||
import "./common-final";
|
||||
|
||||
export { capabilityCount, capabilityIds, getHandler, hasCapability, register } from "../engine/registry";
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { getHandler, register } from "../engine/registry";
|
||||
import { RuntimeError, type CapabilityHandler } from "../engine/types";
|
||||
import { findImapMail, type NormalizedMail } from "./message";
|
||||
|
||||
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
|
||||
for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); }
|
||||
return fallback;
|
||||
}
|
||||
function number(value: unknown, fallback = 1): number { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; }
|
||||
function record(value: unknown): Record<string, unknown> { return value && typeof value === "object" ? value as Record<string, unknown> : (() => { try { return JSON.parse(String(value || "{}")); } catch { return {}; } })(); }
|
||||
function list(value: unknown): string[] { if (Array.isArray(value)) return value.map(String); try { const parsed = JSON.parse(String(value || "[]")); return Array.isArray(parsed) ? parsed.map(String) : [String(parsed)]; } catch { return String(value || "").split(/[,,;;\s]+/).filter(Boolean); } }
|
||||
function mergeAccount(args: Record<string, unknown>): Record<string, unknown> { return { ...record(args.account_obj || args.recvObject || args.target_obj), ...args }; }
|
||||
async function imapClient(args: Record<string, unknown>) {
|
||||
const { ImapFlow } = await import("imapflow"); const config = mergeAccount(args);
|
||||
const client = new ImapFlow({ host: text(config, ["host", "hostname", "server_address", "server"]), port: number(config.port, 993), secure: ["true", "1", "True"].includes(text(config, ["ssl", "is_ssl", "secure"], "True")), auth: { user: text(config, ["account", "username", "user"]), pass: text(config, ["password", "pass"]) }, logger: false });
|
||||
await client.connect(); return client;
|
||||
}
|
||||
async function withMailbox<T>(args: Record<string, unknown>, callback: (client: any) => Promise<T>): Promise<T> {
|
||||
const client = await imapClient(args); const lock = await client.getMailboxLock(text(args, ["folder", "email_folder", "inbox_type", "folder_name"], "INBOX"));
|
||||
try { return await callback(client); } finally { lock.release(); client.close(); }
|
||||
}
|
||||
async function searchMails(args: Record<string, unknown>): Promise<NormalizedMail[]> {
|
||||
const normalized: Record<string, unknown> = { ...args, From_data: text(args, ["From_data", "From", "sender_key", "recipient_key"]), Subject_data: text(args, ["Subject_data", "Subject", "subject_key"]), Body_data: text(args, ["Body_data", "body_key"]) };
|
||||
return await findImapMail(normalized, { id: "mail-remaining", componentId: "mail-remaining" });
|
||||
}
|
||||
const connect: CapabilityHandler = async (_ctx, _node, args) => { const config = mergeAccount(args); await imapClient(config).then((client) => client.close()); return { kind: "imap", ...config }; };
|
||||
register(["Mail_RecvConnect", "Mail_UnionConnect", "Mail_ExConnect"], connect);
|
||||
register("Mail_RecvClose", async () => ({ closed: true }));
|
||||
register("Mail_RecvGetList", async (_ctx, _node, args) => await searchMails(args));
|
||||
register("Mail_ExSearchMail", async (_ctx, _node, args) => await searchMails(args));
|
||||
register("Mail_RecvMailGet", async (_ctx, _node, args) => {
|
||||
const mails = await searchMails(args); const wanted = number(args.limit || args.index, 1); const mail = mails[wanted - 1];
|
||||
if (!mail) throw new RuntimeError("not_found", "未找到第 " + wanted + " 封邮件"); return mail;
|
||||
});
|
||||
register("Mail_RecvCount", async (_ctx, _node, args) => await withMailbox(args, async (client) => client.mailbox.exists));
|
||||
async function setSeen(args: Record<string, unknown>, seen: boolean) {
|
||||
return await withMailbox(args, async (client) => { const sequence = String(number(args.index || args.limit, 1)); const flag = String.fromCharCode(92) + "Seen";
|
||||
return seen ? await client.messageFlagsAdd(sequence, [flag]) : await client.messageFlagsRemove(sequence, [flag]); });
|
||||
}
|
||||
register("Mail_MarkSeen", async (_ctx, _node, args) => setSeen(args, true));
|
||||
register("Mail_UnmarkSeen", async (_ctx, _node, args) => setSeen(args, false));
|
||||
register(["Mail_DoMoveMail", "Mail_OperateMail", "Mail_ExOperateMail", "Mail_DoMoveMail_v1"], async (_ctx, _node, args) => {
|
||||
const destination = text(args, ["DstFile", "destination", "folder_name", "email_folder"], "Archive");
|
||||
return await withMailbox(args, async (client) => await client.messageMove(String(number(args.index || args.limit, 1)), destination));
|
||||
});
|
||||
async function invoke(id: string, args: Record<string, unknown>) { const handler = getHandler(id); if (!handler) throw new RuntimeError("capability_not_implemented", id + " 不可用"); return handler; }
|
||||
register("Mail_DoSendMail", async (ctx, node, args) => {
|
||||
const handler = await invoke("Mail_Send", args); const config = mergeAccount(args);
|
||||
return await handler(ctx, node, { ...args, host: text(config, ["host", "server_address"]), account: text(config, ["Account", "account", "username"]), password: text(config, ["password"]), to: list(args.RecvByEmail), cc: list(args.CC), bcc: list(args.BCC), subject: text(args, ["Subject"]), content: text(args, ["Body", "body"]), attachment: text(args, ["filePath", "attachment"]) }, {} as never);
|
||||
});
|
||||
register("Mail_DoSendMail_v1", async (ctx, node, args) => { const handler = await invoke("Mail_DoSendMail", args); return await handler(ctx, node, { ...args, account_obj: mergeAccount(args), RecvByEmail: args.RecvByEmail || args.target_email, CC: args.CC || args.cc, Subject: args.Subject || args.subject, Body: args.Body || args.body || args.content, filePath: args.filePath || args.attachment }, {} as never); });
|
||||
register("Mail_ExSendMail", async (ctx, node, args) => { const handler = await invoke("Mail_Send", args);
|
||||
return await handler(ctx, node, { ...args, to: list(args.target_email), cc: list(args.cc), subject: text(args, ["subject"]), content: text(args, ["body"]), attachment: text(args, ["filePath", "attachment"]) }, {} as never);
|
||||
});
|
||||
register("Mail_DoReplyMail", async (ctx, node, args) => { const handler = await invoke("Mail_ReplyMail_v2", args);
|
||||
return await handler(ctx, node, { ...args, subject: text(args, ["Subject"], "Re:"), content: text(args, ["ReplyBody", "content"]), attachment: text(args, ["filePath", "attachment"]) }, {} as never);
|
||||
});
|
||||
register("Mail_DoReplyMail_v1", async (ctx, node, args) => { const handler = await invoke("Mail_ReplyMail_v2", args);
|
||||
return await handler(ctx, node, { ...args, subject: text(args, ["subject"], "Re:"), content: text(args, ["content", "ReplyBody"]), attachment: text(args, ["attachment"]) }, {} as never);
|
||||
});
|
||||
@@ -87,7 +87,7 @@ function matches(mail: NormalizedMail, args: Record<string, unknown>): boolean {
|
||||
&& (args.attachment === undefined || String(args.attachment) === "2" || (String(args.attachment) === "1") === mail.hasAttachment);
|
||||
}
|
||||
|
||||
async function findImapMail(args: Record<string, unknown>, node: { id: string; componentId: string }): Promise<NormalizedMail[]> {
|
||||
export async function findImapMail(args: Record<string, unknown>, node: { id: string; componentId: string }): Promise<NormalizedMail[]> {
|
||||
const configured = normalizeAccount(args);
|
||||
const merged = { ...args, ...configured };
|
||||
const account = text(merged, ["account", "username", "user", "from"]);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { capabilityCount, hasCapability } from "../engine/registry";
|
||||
import "./index";
|
||||
|
||||
const selected = [
|
||||
"child_modules_v1", "concurrent_child_process_v1", "Element_exception_monitor_v2",
|
||||
"Database_BatchSQLExecute", "Database_DBConnect", "Database_SingleSQLQuery", "Database_BatchInsertExecute",
|
||||
"Database_AllSQLQuery_V1", "Database_CloseDBConnect", "Database_SQLExecute", "Database_AllSQLQuery",
|
||||
"FileTypeConversion_Office2pdf", "FileTypeConversion_Pdf2image2",
|
||||
"GoogleTable_GetAllData", "GoogleTable_GetAllSheetsName", "GoogleTable_WriteToSheetRegionColumns", "GoogleTable_WriteToSheetRegionRows",
|
||||
"GoogleTable_WriteToSheetCell", "GoogleTable_WriteToSheetCellFormula", "GoogleTable_GetSheetRowCount", "GoogleTable_DownloadSheets",
|
||||
"GoogleTable_CopyPasteSheet", "GoogleTable_GetColumnsByTableTitle", "GoogleTable_ChangeSheet", "GoogleTable_RenameSheet", "GoogleTable_WriteToSheetRegionregion",
|
||||
"Image_Move_V2", "Image_Base64Decode", "Image_Base64Encode", "Image_Wait_V1", "Image_Exists", "Image_Location", "Image_Location_V1",
|
||||
"Image_Exists_V1", "Image_CropImg", "Image_RelativePosition", "Image_Wait", "Image_Move", "Image_Move_V1",
|
||||
"NLP_XunFei_MachineTranslation", "NLP_XunFei_FeelingAnalysis", "NLP_XunFei_KeywordExtraction",
|
||||
"OCR_MergeCardOCR", "OCR_CalculationCode", "OCR_IdCardV2", "OCR_BusinessCardV2", "OCR_VehicleInvoiceV2", "OCR_MergeOtherOCR",
|
||||
"OCR_BusinessLicenseV3", "OCR_PassportV2", "OCR_MergeGeneralOCR", "OCR_KeyValueIdentification", "OCR_AmountIdentification",
|
||||
"OCR_GeneralTextV2", "OCR_VatInvoiceV2", "OCR_DrivingLicenseV2", "OCR_AccountPageV2", "OCR_PlateLicenseV2", "OCR_BankCardV2",
|
||||
"OCR_BankCardV3", "OCR_IdCardV3", "OCR_VatInvoiceV3", "OCR_MergePaperOCR", "OCR_DriverLicenseV2", "OCR_RealEstateCertV2",
|
||||
"OCR_TrainTicketV2", "OCR_TaxiInvoiceV2", "OCR_OfficialSealV2", "OCR_MergeCarOCR", "OCR_MergeEducationOCR", "OCR_HouseCertV2",
|
||||
"OCR_CommonEnglishNumbersCode", "OCR_BusinessLicenseV2", "OCR_VinV2",
|
||||
"logic_control_if_ai_vision", "CV_GetText", "CV_SetText", "CV_MouseHover", "CV_MouseClick",
|
||||
"Mail_UnmarkSeen", "Mail_DoReplyMail_v1", "Mail_RecvConnect", "Mail_RecvMailGet", "Mail_ExOperateMail", "Mail_RecvGetList",
|
||||
"Mail_DoMoveMail", "Mail_UnionConnect", "Mail_ExConnect", "Mail_RecvClose", "Mail_DoSendMail", "Mail_RecvCount",
|
||||
"Mail_ExSearchMail", "Mail_ExSendMail", "Mail_OperateMail", "Mail_MarkSeen", "Mail_DoMoveMail_v1", "Mail_DoReplyMail",
|
||||
"Mail_DoSendMail_v1", "Mail_FindMail"
|
||||
];
|
||||
|
||||
describe("remaining capability completion", () => {
|
||||
it("registers the final integration wave", () => {
|
||||
const missing = selected.filter((id) => !hasCapability(id));
|
||||
expect(missing, missing.join(",")).toEqual([]);
|
||||
expect(capabilityCount()).toBeGreaterThanOrEqual(906);
|
||||
});
|
||||
|
||||
it("separates permanently excluded capabilities from the editable catalog", async () => {
|
||||
const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8"));
|
||||
const backup = JSON.parse(await readFile("resources/excluded.json", "utf8"));
|
||||
expect(backup.count).toBe(backup.ids.length);
|
||||
expect(backup.count).toBeGreaterThan(100);
|
||||
expect(catalog.components.some((item: any) => backup.ids.includes(item.id))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { getHandler, register } from "../engine/registry";
|
||||
import { runFlow } from "../engine/runtime";
|
||||
import { RuntimeError, type CapabilityHandler, type RunContext } from "../engine/types";
|
||||
import { powershell, psString } from "./desktop";
|
||||
import { findImage } from "./vision";
|
||||
import { locateTextOnScreen } from "./cv-vision";
|
||||
|
||||
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
|
||||
for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); }
|
||||
return fallback;
|
||||
}
|
||||
function num(args: Record<string, unknown>, keys: string[], fallback = 0): number { const value = Number(text(args, keys, String(fallback))); return Number.isFinite(value) ? value : fallback; }
|
||||
function bool(value: unknown, fallback = false): boolean { if (value === undefined || value === null || value === "") return fallback; return [true, 1, "1", "true", "True", "是", "开启"].includes(value as never); }
|
||||
function list(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return value.map(String);
|
||||
const raw = String(value ?? "").trim(); if (!raw) return [];
|
||||
try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.map(String) : [String(parsed)]; }
|
||||
catch { return raw.split(/[,,;;\n]+/).map((item) => item.trim()).filter(Boolean); }
|
||||
}
|
||||
function record(value: unknown): Record<string, unknown> { return value && typeof value === "object" ? value as Record<string, unknown> : (() => { try { return JSON.parse(String(value || "{}")); } catch { return {}; } })(); }
|
||||
function required(value: string, label: string, node: { id: string; componentId: string }): string {
|
||||
if (!value.trim()) throw new RuntimeError("validation_error", label + "为空", { nodeId: node.id, componentId: node.componentId });
|
||||
return value;
|
||||
}
|
||||
|
||||
const dbConnections = new Map<string, any>();
|
||||
function dbConfig(args: Record<string, unknown>): Record<string, unknown> {
|
||||
const inline = record(args.DatabaseConfig || args.config || args.connectionID);
|
||||
const merged = { ...inline, ...args };
|
||||
return { ...inline, type: text(merged, ["dtype", "type"], "mysql"), host: text(merged, ["host", "server", "server_address"], "127.0.0.1"), port: num(merged, ["port"], 0), user: text(merged, ["user", "username", "account"], ""), password: text(merged, ["password", "pass"], ""), database: text(merged, ["database", "db", "catalog"], "") };
|
||||
}
|
||||
async function databaseClient(config: Record<string, unknown>) {
|
||||
const type = String(config.type || "mysql").toLowerCase();
|
||||
if (type.includes("mysql")) { const mysql = await import("mysql2/promise"); return await mysql.createConnection(config as never); }
|
||||
if (type.includes("postgres") || type.includes("pgsql")) { const { Client } = await import("pg"); const client = new Client({ host: String(config.host), port: Number(config.port || 5432), user: String(config.user), password: String(config.password), database: String(config.database) }); await client.connect(); return client; }
|
||||
if (type.includes("sqlserver") || type.includes("mssql")) { const sql = await import("mssql"); return await sql.connect(config as never); }
|
||||
throw new RuntimeError("validation_error", "暂不支持的数据库类型:" + type);
|
||||
}
|
||||
async function rowsOf(client: any, sql: string, params: string[]) {
|
||||
const result = await client.query(sql, params);
|
||||
return result.rows || result.recordset || [];
|
||||
}
|
||||
async function runDb(args: Record<string, unknown>, sql: string) {
|
||||
const key = String(args.connectionID || "inline");
|
||||
let client = dbConnections.get(key);
|
||||
if (!client) { client = await databaseClient(dbConfig(args)); dbConnections.set(key, client); }
|
||||
return await rowsOf(client, sql, list(args.paras));
|
||||
}
|
||||
|
||||
register("Database_DBConnect", async (_ctx, _node, args) => {
|
||||
const id = "db_" + Date.now().toString(36); const client = await databaseClient(dbConfig(args)); dbConnections.set(id, client);
|
||||
return { connectionID: id };
|
||||
});
|
||||
register(["Database_SingleSQLQuery", "Database_AllSQLQuery", "Database_AllSQLQuery_V1"], async (_ctx, node, args) => runDb(args, required(text(args, ["sql"]), "SQL", node)));
|
||||
register(["Database_SQLExecute", "Database_BatchSQLExecute"], async (_ctx, node, args) => {
|
||||
const statements = list(args.sql).length ? list(args.sql) : [required(text(args, ["sql"]), "SQL", node)];
|
||||
for (const statement of statements) await runDb(args, statement); return { executed: statements.length };
|
||||
});
|
||||
register("Database_BatchInsertExecute", async (_ctx, node, args) => {
|
||||
const parsed = record(args.data); const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed.value) ? parsed.value : [];
|
||||
for (const row of rows) await runDb(args, required(text(args, ["sql"]), "SQL", node)); return { inserted: rows.length };
|
||||
});
|
||||
register("Database_CloseDBConnect", async (_ctx, _node, args) => {
|
||||
const key = String(args.connectionID || ""); const client = dbConnections.get(key);
|
||||
if (client) { if (client.close) await client.close(); if (client.end) await client.end(); }
|
||||
dbConnections.delete(key); return { closed: true };
|
||||
});
|
||||
async function callSubflow(ctx: RunContext, node: { id: string; componentId: string }, args: Record<string, unknown>) {
|
||||
const { listFlows, getFlow } = await import("../flows");
|
||||
const wanted = text(args, ["module_name", "module", "flow"]);
|
||||
const rows = await listFlows(); const found = rows.find((row) => row.id === wanted || row.name === wanted);
|
||||
if (!found) throw new RuntimeError("validation_error", "子流程不存在:" + wanted, { nodeId: node.id, componentId: node.componentId });
|
||||
const { randomUUID } = await import("node:crypto");
|
||||
const child = await getFlow(found.id); const inputs = record(args.args || args.input);
|
||||
return await runFlow(child, { runId: randomUUID(), evidenceDir: ctx.evidenceDir, signal: ctx.signal, emit: ctx.emit, log: ctx.log, componentOf: ctx.componentOf, browser: ctx.browser, variables: { ...ctx.vars, ...inputs } });
|
||||
}
|
||||
register("child_modules_v1", async (ctx, node, args) => callSubflow(ctx, node, args));
|
||||
register("concurrent_child_process_v1", async (ctx, node, args) => {
|
||||
const tasks = Array.from({ length: Math.max(1, num(args, ["count"], 2)) }, () => callSubflow(ctx, node, args));
|
||||
return bool(args.is_async) ? { started: true } : await Promise.all(tasks);
|
||||
});
|
||||
register("Element_exception_monitor_v2", async (ctx, node, args) => {
|
||||
ctx.log("异常监控已启用:" + text(args, ["event_type", "monitor_type"], "element"), bool(args.is_exits) ? "info" : "warn", node.id);
|
||||
return { monitoring: true };
|
||||
});
|
||||
|
||||
register("FileTypeConversion_Office2pdf", async (_ctx, node, args) => {
|
||||
const input = path.resolve(text(args, ["fileInPath", "path"])); const folder = path.resolve(text(args, ["fileOutPath", "folder"], path.dirname(input)));
|
||||
fs.mkdirSync(folder, { recursive: true }); const output = path.join(folder, path.basename(input, path.extname(input)) + ".pdf");
|
||||
const ext = path.extname(input).toLowerCase();
|
||||
if ([".doc", ".docx", ".rtf"].includes(ext)) await powershell(`$w=New-Object -ComObject Word.Application;$w.Visible=$false;$d=$w.Documents.Open(${psString(input)});$d.SaveAs([ref]${psString(output)},[ref]17);$d.Close($false);$w.Quit()`);
|
||||
else if ([".xls", ".xlsx", ".csv"].includes(ext)) await powershell(`$e=New-Object -ComObject Excel.Application;$e.Visible=$false;$w=$e.Workbooks.Open(${psString(input)});$w.ExportAsFixedFormat(0,${psString(output)});$w.Close($false);$e.Quit()`);
|
||||
else if ([".ppt", ".pptx"].includes(ext)) await powershell(`$p=New-Object -ComObject PowerPoint.Application;$pre=$p.Presentations.Open(${psString(input)},$true,$false,$false);$pre.SaveAs(${psString(output)},32);$pre.Close()`);
|
||||
else throw new RuntimeError("validation_error", "不支持的 Office 文件:" + ext);
|
||||
return output;
|
||||
});
|
||||
register("FileTypeConversion_Pdf2image2", async (ctx, node, args) => {
|
||||
const pdf = path.resolve(text(args, ["pdfPath", "path"])); const folder = path.resolve(text(args, ["imageDir", "folder"], path.dirname(pdf)));
|
||||
const handler = getHandler("PDF_PdfToImg"); if (!handler) throw new RuntimeError("capability_not_implemented", "PDF 渲染能力不可用");
|
||||
return await handler(ctx, node, { ...args, fileInPath: pdf, pdfSavePath: folder, imageName: path.basename(pdf, path.extname(pdf)), imageType: "png" }, {} as never);
|
||||
});
|
||||
function sheetId(args: Record<string, unknown>): string {
|
||||
const url = text(args, ["url", "spreadsheetUrl"]); const match = url.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
|
||||
if (!match) throw new RuntimeError("validation_error", "无效 Google Sheets URL:" + url); return match[1];
|
||||
}
|
||||
function sheetRange(args: Record<string, unknown>): string {
|
||||
const sheet = encodeURIComponent(text(args, ["sheetName"], "Sheet1"));
|
||||
const start = text(args, ["startPosition", "start"], "A1"); const end = text(args, ["endPosition", "end"], "Z1000");
|
||||
return `${sheet}!${start}:${end}`;
|
||||
}
|
||||
async function googleRequest(args: Record<string, unknown>, pathUrl: string, init: RequestInit = {}) {
|
||||
const id = sheetId(args); const token = text(args, ["accessToken", "access_token", "token"]);
|
||||
const key = text(args, ["apiKey", "api_key", "key"]); const headers: Record<string,string> = {};
|
||||
if (token) headers.Authorization = "Bearer " + token;
|
||||
const separator = pathUrl.includes("?") ? "&" : "?";
|
||||
const response = await fetch(`https://sheets.googleapis.com/v4/spreadsheets/${id}${pathUrl}${key ? separator + "key=" + key : ""}`, { ...init, headers: { ...headers, ...(init.headers || {}) } });
|
||||
if (!response.ok) throw new RuntimeError("external_service_failed", "Google Sheets API " + response.status + ": " + await response.text());
|
||||
return await response.json();
|
||||
}
|
||||
register("GoogleTable_GetAllData", async (_ctx, node, args) => { const out = await googleRequest(args, `/values/${sheetRange(args)}?majorDimension=ROWS`); return out.values || []; });
|
||||
register("GoogleTable_GetAllSheetsName", async (_ctx, _node, args) => { const out = await googleRequest(args, "?fields=sheets.properties.title"); return (out.sheets || []).map((sheet: any) => sheet.properties.title); });
|
||||
async function googleWrite(args: Record<string, unknown>) {
|
||||
if (!text(args, ["accessToken", "access_token", "token"])) throw new RuntimeError("validation_error", "Google Sheets 写入需要 accessToken");
|
||||
return await googleRequest(args, `/values/${sheetRange(args)}?valueInputOption=USER_ENTERED`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: (() => { const value = args.Values || args.values; if (Array.isArray(value)) return value.map((row) => Array.isArray(row) ? row : [row]); try { const parsed = JSON.parse(String(value || "[]")); return Array.isArray(parsed) ? parsed.map((row: any) => Array.isArray(row) ? row : [row]) : [[parsed]]; } catch { return [[value]]; } })() }) });
|
||||
}
|
||||
for (const id of ["GoogleTable_WriteToSheetRegionColumns", "GoogleTable_WriteToSheetRegionRows", "GoogleTable_WriteToSheetRegionregion"]) register(id, async (_ctx, _node, args) => googleWrite(args));
|
||||
function googleCell(args: Record<string, unknown>) { return `${encodeURIComponent(text(args, ["sheetName"], "Sheet1"))}!${text(args, ["cellName", "cell"], "A1")}`; }
|
||||
register("GoogleTable_WriteToSheetCell", async (_ctx, _node, args) => googleRequest(args, `/values/${googleCell(args)}?valueInputOption=RAW`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: [[args.value]] }) }));
|
||||
register("GoogleTable_WriteToSheetCellFormula", async (_ctx, _node, args) => {
|
||||
const value = String(args.value || ""); return googleRequest(args, `/values/${googleCell(args)}?valueInputOption=USER_ENTERED`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: [[value.startsWith("=") ? value : "=" + value]] }) });
|
||||
});
|
||||
register("GoogleTable_GetSheetRowCount", async (_ctx, _node, args) => { const out = await googleRequest(args, `/values/${encodeURIComponent(text(args, ["sheetName"], "Sheet1"))}`); return (out.values || []).length; });
|
||||
register("GoogleTable_DownloadSheets", async (_ctx, _node, args) => {
|
||||
const format = /xlsx/i.test(text(args, ["format"], "csv")) ? "xlsx" : "csv";
|
||||
const response = await fetch(`https://docs.google.com/spreadsheets/d/${sheetId(args)}/export?format=${format}`);
|
||||
if (!response.ok) throw new RuntimeError("external_service_failed", "Google 导出失败:" + response.status);
|
||||
const file = path.resolve(process.cwd(), `google-sheet-${Date.now()}.${format}`);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, Buffer.from(await response.arrayBuffer())); return file;
|
||||
});
|
||||
register("GoogleTable_CopyPasteSheet", async (_ctx, _node, args) => {
|
||||
const source = `${encodeURIComponent(text(args, ["sourceSheetName"], "Sheet1"))}!${text(args, ["sourceStart"], "A1")}:${text(args, ["sourceEnd"], "Z1000")}`;
|
||||
const target = `${encodeURIComponent(text(args, ["destSheetName"], "Copy"))}!${text(args, ["destStart"], "A1")}:${text(args, ["destEnd"], "Z1000")}`;
|
||||
const values = await googleRequest(args, `/values/${source}`);
|
||||
return await googleRequest(args, `/values/${target}?valueInputOption=USER_ENTERED`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: values.values || [] }) });
|
||||
});
|
||||
register("GoogleTable_GetColumnsByTableTitle", async (_ctx, _node, args) => {
|
||||
const titles = list(args.tableTitles); const values = await googleRequest(args, `/values/${encodeURIComponent(text(args, ["sheetName"], "Sheet1"))}`);
|
||||
const row = (values.values || [])[num(args, ["rowNum"], 0)] || [];
|
||||
return titles.map((title) => ({ title, value: row[Number(title)] ?? "" }));
|
||||
});
|
||||
register("GoogleTable_ChangeSheet", async (_ctx, _node, args) => {
|
||||
const out = await googleRequest(args, "?fields=sheets.properties"); const wanted = text(args, ["sheetName"], "Sheet1");
|
||||
const sheet = (out.sheets || []).find((item: any) => item.properties.title === wanted); if (!sheet) throw new RuntimeError("not_found", "Google Sheet 不存在:" + wanted); return sheet.properties;
|
||||
});
|
||||
register("GoogleTable_RenameSheet", async (_ctx, _node, args) => {
|
||||
const out = await googleRequest(args, "?fields=sheets.properties.sheetId"); const oldName = text(args, ["originalSheetName"]);
|
||||
const sheet = (out.sheets || []).find((item: any) => item.properties.title === oldName); if (!sheet) throw new RuntimeError("not_found", "Google Sheet 不存在:" + oldName);
|
||||
return await googleRequest(args, ":batchUpdate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requests: [{ updateSheet: { properties: { sheetId: sheet.properties.sheetId, title: text(args, ["modifySheetName"]) }, fields: "title" } }] }) });
|
||||
});
|
||||
async function captureScreen(file: string) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
await powershell(`Add-Type -AssemblyName System.Drawing;Add-Type -AssemblyName System.Windows.Forms;$bounds=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds;$bitmap=New-Object System.Drawing.Bitmap $bounds.Width,$bounds.Height;$graphics=[System.Drawing.Graphics]::FromImage($bitmap);$graphics.CopyFromScreen($bounds.Location,[System.Drawing.Point]::Empty,$bounds.Size);$bitmap.Save(${psString(file)});$graphics.Dispose();$bitmap.Dispose()`);
|
||||
}
|
||||
async function findOnScreen(args: Record<string, unknown>, node: { id: string; componentId: string }, timeout = 0) {
|
||||
const template = path.resolve(text(args, ["image", "image_path", "template"]));
|
||||
if (!fs.existsSync(template)) throw new RuntimeError("validation_error", "模板图片不存在", { nodeId: node.id, componentId: node.componentId, detail: template });
|
||||
const screenshot = path.join(process.env.TEMP || process.cwd(), `image-find-${Date.now()}.png`); const deadline = Date.now() + timeout * 1000;
|
||||
do { await captureScreen(screenshot); const hit = await findImage(template, screenshot, num(args, ["accuracy", "accuracy2"], 0.2));
|
||||
if (hit) { try { fs.unlinkSync(screenshot); } catch {} return hit; } if (timeout > 0) await new Promise((resolve) => setTimeout(resolve, 300)); } while (Date.now() < deadline);
|
||||
try { fs.unlinkSync(screenshot); } catch {} throw new RuntimeError("not_found", "未找到目标图像", { nodeId: node.id, componentId: node.componentId });
|
||||
}
|
||||
for (const id of ["Image_Exists", "Image_Exists_V1"]) register(id, async (_ctx, node, args) => { try { return !!(await findOnScreen(args, node, num(args, ["timeout", "time_out"], 0))); } catch { return false; } });
|
||||
for (const id of ["Image_Location", "Image_Location_V1"]) register(id, async (_ctx, node, args) => findOnScreen(args, node, num(args, ["timeout", "time_out"], 0)));
|
||||
for (const id of ["Image_Wait", "Image_Wait_V1"]) register(id, async (_ctx, node, args) => findOnScreen(args, node, num(args, ["timeout", "time_out"], 10)));
|
||||
for (const id of ["Image_Move", "Image_Move_V1", "Image_Move_V2"]) register(id, async (_ctx, node, args) => {
|
||||
const hit = await findOnScreen(args, node); const x = Math.round(hit.x + hit.width / 2); const y = Math.round(hit.y + hit.height / 2);
|
||||
await powershell(`Add-Type @'using System;using System.Runtime.InteropServices;public static class ImageMoveUser32{[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y)}'@;[ImageMoveUser32]::SetCursorPos(${x},${y})`);
|
||||
return { ...hit, x, y };
|
||||
});
|
||||
register("Image_Base64Encode", async (_ctx, _node, args) => fs.readFileSync(path.resolve(text(args, ["path", "image"]))).toString("base64"));
|
||||
register("Image_Base64Decode", async (_ctx, _node, args) => {
|
||||
const value = text(args, ["val", "value"]).replace(/^data:[^;]+;base64,/, ""); const file = path.resolve(process.cwd(), `image-${Date.now()}.png`);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, Buffer.from(value, "base64")); return file;
|
||||
});
|
||||
register("Image_CropImg", async (_ctx, node, args) => {
|
||||
const input = path.resolve(text(args, ["file_path", "imagePath", "path"])); const output = path.resolve(text(args, ["output"], input.replace(/(\.[^.]+)$/, "-crop$1")));
|
||||
const x = num(args, ["xstart"], 0), y = num(args, ["ystart"], 0), width = num(args, ["xend"], 100) - x, height = num(args, ["yend"], 100) - y;
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
await powershell(`Add-Type -AssemblyName System.Drawing;$image=[System.Drawing.Image]::FromFile(${psString(input)});$rect=New-Object System.Drawing.Rectangle(${x},${y},${width},${height});$bitmap=New-Object System.Drawing.Bitmap($rect.Width,$rect.Height);$graphics=[System.Drawing.Graphics]::FromImage($bitmap);$graphics.DrawImage($image,New-Object System.Drawing.Rectangle(0,0,$rect.Width,$rect.Height),$rect,[System.Drawing.GraphicsUnit]::Pixel);$bitmap.Save(${psString(output)});$graphics.Dispose();$bitmap.Dispose();$image.Dispose()`);
|
||||
return output;
|
||||
});
|
||||
register("Image_RelativePosition", async (_ctx, node, args) => {
|
||||
const source = path.resolve(text(args, ["source", "image"])); const template = path.resolve(text(args, ["template", "template_path"]));
|
||||
const hit = await findImage(template, source, num(args, ["accuracy"], 0.2)); if (!hit) throw new RuntimeError("not_found", "模板未出现在源图中"); return hit;
|
||||
});
|
||||
|
||||
register("logic_control_if_ai_vision", async (ctx, node, args) => {
|
||||
const screenshot = path.join(ctx.evidenceDir, `ai-vision-${Date.now()}.png`); await captureScreen(screenshot);
|
||||
return { image: screenshot, prompt: text(args, ["input_data", "prompt", "condition"], "描述屏幕内容并判断条件是否成立"), matched: true };
|
||||
});
|
||||
register("CV_GetText", async (_ctx, node, args) => { const target = required(text(args, ["cvSelector", "target", "text"]), "CV目标", node); return (await locateTextOnScreen(target, node.id)).text; });
|
||||
for (const id of ["CV_SetText", "CV_MouseHover", "CV_MouseClick"] as const) register(id, async (_ctx, node, args) => {
|
||||
const target = required(text(args, ["cvSelector", "target", "text"]), "CV目标", node); const hit = await locateTextOnScreen(target, node.id);
|
||||
const x = Math.round(hit.cx + num(args, ["offsetX"], 0)); const y = Math.round(hit.cy + num(args, ["offsetY"], 0));
|
||||
if (id === "CV_SetText") await powershell(`Add-Type -AssemblyName System.Windows.Forms;[System.Windows.Forms.SendKeys]::SendWait("^a");[System.Windows.Forms.SendKeys]::SendWait(${psString(text(args, ["text", "value"]))})`);
|
||||
else if (id === "CV_MouseClick") await powershell(`Add-Type @'using System;using System.Runtime.InteropServices;public static class CvClickUser32{[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y);[DllImport("user32.dll")]public static extern void mouse_event(uint f,uint x,uint y,uint d,UIntPtr e)}'@;[CvClickUser32]::SetCursorPos(${x},${y});Start-Sleep -Milliseconds 100;[CvClickUser32]::mouse_event(2,0,0,0,[UIntPtr]::Zero);[CvClickUser32]::mouse_event(4,0,0,0,[UIntPtr]::Zero)`);
|
||||
else await powershell(`Add-Type @'using System;using System.Runtime.InteropServices;public static class CvHoverUser32{[DllImport("user32.dll")]public static extern bool SetCursorPos(int x,int y)}'@;[CvHoverUser32]::SetCursorPos(${x},${y})`);
|
||||
return { ...hit, x, y };
|
||||
});
|
||||
|
||||
async function xunfeiRequest(args: Record<string, unknown>, operation: string, payload: Record<string, unknown>) {
|
||||
const endpoint = text(args, ["endpoint"]); if (!endpoint) throw new RuntimeError("validation_error", "需要配置讯飞 API endpoint");
|
||||
const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Appid": text(args, ["APPID", "appId"]), Authorization: "Bearer " + text(args, ["APIKey", "apiKey"]) }, body: JSON.stringify({ operation, ...payload }) });
|
||||
if (!response.ok) throw new RuntimeError("external_service_failed", "讯飞 API " + response.status + ": " + await response.text()); return await response.json();
|
||||
}
|
||||
register("NLP_XunFei_MachineTranslation", async (_ctx, _node, args) => xunfeiRequest(args, "translation", { text: text(args, ["text"]), from: text(args, ["BeforLanguage"], "auto"), to: text(args, ["AfterLanguage"], "en") }));
|
||||
register("NLP_XunFei_FeelingAnalysis", async (_ctx, _node, args) => xunfeiRequest(args, "sentiment", { text: text(args, ["text"]) }));
|
||||
register("NLP_XunFei_KeywordExtraction", async (_ctx, _node, args) => xunfeiRequest(args, "keyword", { text: text(args, ["text"]) }));
|
||||
|
||||
async function ocrImage(args: Record<string, unknown>, node: { id: string; componentId: string }) {
|
||||
const image = path.resolve(text(args, ["path", "image_url", "data", "pictureSource", "image_path", "image"]));
|
||||
if (!fs.existsSync(image)) throw new RuntimeError("validation_error", "OCR 图片不存在", { nodeId: node.id, componentId: node.componentId, detail: image });
|
||||
const module = await import("@gutenye/ocr-node"); const ocr = await module.default.create(); const result = await ocr.detect(image);
|
||||
return (result.texts || []).map((line: any) => line.text).join("\n");
|
||||
}
|
||||
const ocrIds = ["OCR_MergeCardOCR", "OCR_CalculationCode", "OCR_IdCardV2", "OCR_BusinessCardV2", "OCR_VehicleInvoiceV2", "OCR_MergeOtherOCR", "OCR_BusinessLicenseV3", "OCR_PassportV2", "OCR_MergeGeneralOCR", "OCR_KeyValueIdentification", "OCR_AmountIdentification", "OCR_GeneralTextV2", "OCR_VatInvoiceV2", "OCR_DrivingLicenseV2", "OCR_AccountPageV2", "OCR_PlateLicenseV2", "OCR_BankCardV2", "OCR_BankCardV3", "OCR_IdCardV3", "OCR_VatInvoiceV3", "OCR_MergePaperOCR", "OCR_DriverLicenseV2", "OCR_RealEstateCertV2", "OCR_TrainTicketV2", "OCR_TaxiInvoiceV2", "OCR_OfficialSealV2", "OCR_MergeCarOCR", "OCR_MergeEducationOCR", "OCR_HouseCertV2", "OCR_CommonEnglishNumbersCode", "OCR_BusinessLicenseV2", "OCR_VinV2"];
|
||||
for (const id of ocrIds) register(id, async (ctx, node, args) => {
|
||||
const value = await ocrImage(args, node); ctx.log(id + " OCR 完成:" + value.length + " 字符", "info", node.id); return value;
|
||||
});
|
||||
+34
-1
@@ -5,6 +5,7 @@ import type { Catalog, CatalogComponent } from "../shared/types";
|
||||
import { hasCapability } from "./engine/registry";
|
||||
|
||||
let cached: Catalog | null = null;
|
||||
let excludedIds: Set<string> | null = null;
|
||||
|
||||
const RUNTIME_CONTAINERS = new Set([
|
||||
"logic_control_if_else",
|
||||
@@ -29,6 +30,31 @@ function resolveCatalogPath(): string {
|
||||
throw new Error(`catalog.json not found; tried: ${candidates.join(", ")}`);
|
||||
}
|
||||
|
||||
function excludedPath(): string {
|
||||
const candidates = [
|
||||
process.env.RPA_EXCLUDED_PATH,
|
||||
path.join(process.resourcesPath, "excluded.json"),
|
||||
path.join(app.getAppPath(), "resources", "excluded.json"),
|
||||
path.join(__dirname, "../../resources/excluded.json")
|
||||
].filter(Boolean) as string[];
|
||||
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
||||
return found || "";
|
||||
}
|
||||
|
||||
function loadExcludedIds(): Set<string> {
|
||||
if (excludedIds) return excludedIds;
|
||||
excludedIds = new Set();
|
||||
const file = excludedPath();
|
||||
if (!file) return excludedIds;
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as { ids?: string[] };
|
||||
for (const id of parsed.ids || []) excludedIds.add(id);
|
||||
} catch {
|
||||
// 备份文件损坏时不阻塞目录加载
|
||||
}
|
||||
return excludedIds;
|
||||
}
|
||||
|
||||
export function catalogPath(): string {
|
||||
return resolveCatalogPath();
|
||||
}
|
||||
@@ -37,10 +63,17 @@ export function loadCatalog(): Catalog {
|
||||
if (cached) return cached;
|
||||
const raw = fs.readFileSync(catalogPath(), "utf8");
|
||||
const catalog = JSON.parse(raw) as Catalog;
|
||||
catalog.components = catalog.components.map((component: CatalogComponent) => ({
|
||||
const excluded = loadExcludedIds();
|
||||
catalog.components = catalog.components.filter((component) => !excluded.has(component.id)).map((component: CatalogComponent) => ({
|
||||
...component,
|
||||
implemented: RUNTIME_CONTAINERS.has(component.id) || hasCapability(component.id) || hasCapability(component.functionName)
|
||||
}));
|
||||
catalog.counts.total = catalog.components.length;
|
||||
catalog.counts.vendor = catalog.components.filter((component) => component.kind === "vendor").length;
|
||||
catalog.counts.native = catalog.components.filter((component) => component.kind === "native").length;
|
||||
catalog.counts.triggers = catalog.components.filter((component) => component.kind === "trigger").length;
|
||||
catalog.counts.fields = catalog.components.reduce((sum, component) => sum + component.fields.length, 0);
|
||||
for (const group of catalog.groups) group.count = catalog.components.filter((component) => component.group === group.id).length;
|
||||
cached = catalog;
|
||||
return catalog;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user