262 lines
24 KiB
TypeScript
262 lines
24 KiB
TypeScript
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;
|
||
}); |