PRA-only-demo/src/main/capabilities/remaining-core.ts

262 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
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" } }] }) });
});
// Image_Exists / Image_Location / Image_Wait / Image_Move 统一由 vision.ts 注册到新查找管线
// (虚拟桌面多屏拼接 + 相似度语义 + JPG 模板)。此处旧实现用 GDI CopyFromScreen 只截主屏
// 且 accuracy 语义颠倒, 已整体迁出删除。
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()`);
}
// 图片Base64编解码: 统一组件按 mode 分流(encode=图片→Base64, decode=Base64→图片)。
// Image_Base64Decode 已隐藏进该组件,但保留注册以防旧流程直接引用旧 id。
register("Image_Base64Encode", async (_ctx, node, args) => {
const mode = text(args, ["mode"], "encode");
if (mode === "decode") {
const value = text(args, ["val", "value"]).replace(/^data:[^;]+;base64,/, "");
if (!value.trim()) throw new RuntimeError("validation_error", "Base64编码不能为空", { nodeId: node.id, componentId: node.componentId, recommendation: "填写 base64 字符串,或用 ${变量} 引用上游编码输出" });
const custom = text(args, ["output"]).trim();
const file = custom ? path.resolve(custom) : path.resolve(process.cwd(), `image-${Date.now()}.png`);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, Buffer.from(value, "base64"));
return file;
}
const imageArg = text(args, ["path", "image"]);
if (!imageArg.trim()) throw new RuntimeError("validation_error", "图片对象不能为空", { nodeId: node.id, componentId: node.componentId, recommendation: "填写源图片路径,或用 ${变量} 引用上游截图输出" });
const source = path.resolve(imageArg);
if (!fs.existsSync(source)) throw new RuntimeError("validation_error", `找不到图片文件:${source}`, { nodeId: node.id, componentId: node.componentId, recommendation: "确认路径正确,或先用「屏幕截图」组件生成图片" });
return fs.readFileSync(source).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 rawInput = text(args, ["file_path", "imagePath", "path"]);
if (!rawInput.trim()) throw new RuntimeError("validation_error", "图片地址不能为空", { nodeId: node.id, componentId: node.componentId, recommendation: "填写要裁剪的源图片完整路径(如 D:\\images\\shot.png),该组件是从已有图片文件里裁剪区域,不是截屏" });
const input = path.resolve(rawInput);
if (!fs.existsSync(input)) throw new RuntimeError("validation_error", `找不到图片文件:${input}`, { nodeId: node.id, componentId: node.componentId, recommendation: "确认路径正确;若要截取屏幕内容,请改用「截图图像 (Image_Screenshot)」组件" });
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;
if (width <= 0 || height <= 0) throw new RuntimeError("validation_error", `裁剪区域无效(宽 ${width} × 高 ${height})`, { nodeId: node.id, componentId: node.componentId, recommendation: "终点坐标需要大于起点坐标(单位是图片上的像素)" });
fs.mkdirSync(path.dirname(output), { recursive: true });
// 注意: .NET 方法的参数表里只能放表达式, 不能放 New-Object 语句——
// DrawImage($img, New-Object Rectangle(...), ...) 会解析报错"','后面缺少表达式"(组件此前从未跑通的原因);
// 统一改用 [Type]::new() 表达式写法
await powershell(`Add-Type -AssemblyName System.Drawing;$image=[System.Drawing.Image]::FromFile(${psString(input)});$rect=[System.Drawing.Rectangle]::new(${x},${y},${width},${height});$bitmap=[System.Drawing.Bitmap]::new($rect.Width,$rect.Height);$graphics=[System.Drawing.Graphics]::FromImage($bitmap);$graphics.DrawImage($image,[System.Drawing.Rectangle]::new(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"]));
if (!source.trim() || !template.trim()) throw new RuntimeError("validation_error", "被搜索图像与查找目标都需填写图片路径", { nodeId: node.id, componentId: node.componentId });
if (!fs.existsSync(source)) throw new RuntimeError("validation_error", `找不到被搜索图像:${source}`, { nodeId: node.id, componentId: node.componentId });
if (!fs.existsSync(template)) throw new RuntimeError("validation_error", `找不到查找目标图片:${template}`, { nodeId: node.id, componentId: node.componentId });
// 兼容填反:模板比源图大时(首次查找直接抛"还大"错误)自动互换,
// 大图当被搜索图、小图当查找目标,语义仍是「小图在大图中的位置」
const accuracy = num(args, ["accuracy"], 0.2);
let hit: Awaited<ReturnType<typeof findImage>> = null;
let firstError: unknown = null;
try {
hit = await findImage(template, source, accuracy);
} catch (error) {
firstError = error;
try {
hit = await findImage(source, template, accuracy);
if (hit) ctx.log("两张图疑似填反,已自动按「小图在大图中」查找并命中", "warn", node.id);
} catch {
// 互换也失败则回落到原始方向的错误信息
}
}
if (!hit) {
if (firstError) throw firstError;
throw new RuntimeError("not_found", "查找目标未出现在被搜索图像中", { nodeId: node.id, componentId: node.componentId, recommendation: "「查找目标」应是被搜索图里出现的小图案(按钮/图标/文字块);两张图互不包含时无法匹配" });
}
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 };
});
// CV_GetText / CV_SetText / CV_MouseHover / CV_MouseClick 统一由 cv-vision.ts 注册:
// 此处曾重复注册并覆盖 cv-vision 的实现,其中内联 here-string(@'...'@ 单行)是
// 非法 PowerShell 语法,导致 CV_MouseClick/CV_MouseHover 必然解析失败(报 CLIXML 错误)。
// 如需调整 CV 行为请修改 cv-vision.ts,不要在此重复注册。
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;
});