PRA-only-demo/scripts/build-catalog.mjs
liuwenxin 5f9e28715c fix(应用与系统)!: 修复组件执行与参数校验
修正 PID 空结果、目标类型判断与转换、毫秒等待。
让关闭程序和命令行按所选模式显示并校验参数。
统一 PowerShell 输出编码,修正 BAT 路径引号处理。
明确屏保和屏幕唤醒的实际能力,补充缺陷回归测试。

验证:类型检查、构建与组件回归检查通过。

BREAKING CHANGE: 判断数据类型改为返回布尔值。
需要类型名称的旧流程请改用 Basic_Type。
屏保 V1 移除未实现参数,旧自定义密码节点需重新添加。
2026-11-02 09:05:48 +08:00

348 lines
15 KiB
JavaScript
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.

/**
* 从 Agent-Employee 的 1016 项目录源数据生成产品目录。
*
* 输入:
* - shizai-catalog-data.js:997 厂商组件 + 16 原生控制节点
* - componentsNew.json:厂商原始定义(补 vType / validatorRules / tip / displayParam)
* 输出:
* - resources/catalog.json:1016 全量 + 归一化字段 / 分组 / 控件类型
*
* 说明:这里只派生元数据,不包含厂商运行时、二进制或业务数据。
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { applyCatalogOverrides } from "./catalog-overrides.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const SRC_CATALOG = process.env.RPA_CATALOG_SOURCE ||
"D:/code/Agent-Employee/code/frontend/apps/desktop/src/renderer/product-preview/pages/workflows/shizai-catalog-data.js";
const SRC_COMPONENTS_NEW = process.env.RPA_COMPONENTS_NEW ||
"D:/code/Agent-Employee/项目资料/Rea-AgentShizai-cankao/code/rpa-engine/componentDicts/componentsNew.json";
const OUT_FILE = path.join(ROOT, "resources", "catalog.json");
const GROUPS = [
{ id: "control", label: "编排控制", icon: "branch" },
{ id: "trigger", label: "流程触发", icon: "trigger" },
{ id: "app", label: "应用与系统", icon: "app" },
{ id: "web", label: "网页自动化", icon: "web" },
{ id: "office", label: "Office 与文档", icon: "table" },
{ id: "data", label: "数据处理", icon: "database" },
{ id: "file", label: "文件与传输", icon: "file" },
{ id: "desktop", label: "桌面与键鼠", icon: "mouse" },
{ id: "vision", label: "图像与 OCR", icon: "scan" },
{ id: "message", label: "网络与消息", icon: "send" },
{ id: "mobile", label: "移动端与设备", icon: "mobile" }
];
const PACKAGE_GROUPS = {
app: new Set(["App", "System", "Basic", "CustomConfig", "ScreenControl"]),
web: new Set(["WebBrowser", "Element"]),
office: new Set(["Excel", "Word", "PDF", "GoogleTable", "FileTypeConversion"]),
data: new Set(["Array", "Bivartable", "Collection", "CSV", "Database", "Databook", "Dict", "Ini", "JSON", "List", "Math", "Regex", "StrUtil", "Time"]),
file: new Set(["File", "FTP"]),
desktop: new Set(["Clipboard", "Dialog", "Keyboard", "Mouse", "Window", "SafeControlInput"]),
vision: new Set(["CV", "Image", "OCR", "OCR_BaiDu", "OcrOffline", "Mark"]),
message: new Set(["DataService", "Http", "Mail", "MessageNotification", "NLP", "NLP_XunFei", "Scenario"]),
mobile: new Set(["Mobile", "peripherals"])
};
const CONTROL_LABELS = {
condition: "条件表达式", var_name: "变量名称", var_value: "变量值", count_index: "计数变量",
count_start_value: "开始值", count_end_value: "结束值", count_step: "步长", if_for_condition: "循环条件",
return_value: "返回值", module_name: "子流程", call_type: "调用方式", fun_name: "方法", args: "输入映射",
fun_version: "版本", is_async: "是否异步", array_name: "列表变量", reverse: "是否倒序", start_index: "开始索引",
end_index: "结束索引", dictionary_name: "字典变量", event_type: "事件类型", uiSelector: "元素选择器",
designated_info: "指定信息", designated_obj: "指定对象", browser_type: "浏览器类型", is_exits: "存在条件",
monitor_type: "监控类型", monitor_frequency: "监控频率", input_data: "屏幕判断要求", log_process: "记录过程"
};
const TRIGGERS = [
{
id: "Trigger.Manual", name: "人工发起", description: "由用户在运行页手动发起流程。",
fields: [{ key: "entryName", label: "入口名称", displayType: "text-expression", defaultValue: "立即执行", required: true }]
},
{
id: "Trigger.Schedule", name: "定时触发", description: "按工作日或时间计划创建运行请求。",
fields: [
{ key: "mode", label: "计划方式", displayType: "dropDownSelect", defaultValue: "工作日", required: true, options: ["工作日", "每天", "每周", "Cron 表达式"] },
{ key: "time", label: "执行时间", displayType: "text-expression", defaultValue: "08:30", required: true },
{ key: "timezone", label: "时区", displayType: "text-expression", defaultValue: "Asia/Shanghai", required: true }
]
},
{
id: "Trigger.FileArrival", name: "文件到达触发", description: "监听指定目录的新文件事件。",
fields: [
{ key: "folder", label: "监听目录", displayType: "selectFile", defaultValue: "D:\\业务文件\\待处理", required: true, widget: "folder" },
{ key: "pattern", label: "文件匹配", displayType: "text-expression", defaultValue: "*.xlsx", required: true },
{ key: "stableSeconds", label: "文件稳定等待", displayType: "number", defaultValue: "3", required: true, unit: "秒" }
]
}
];
function cleanDefault(value) {
if (value === null || value === undefined) return "";
if (typeof value === "object") return "";
const text = String(value);
if (text === "None" || text === "null") return "";
if (/^"(?:[^"\\]|\\.)*"$/.test(text)) {
try { return JSON.parse(text); } catch { return text; }
}
return text;
}
function widgetFor(displayType, hint) {
if (hint) return hint;
const t = String(displayType || "");
if (/^hidden$/i.test(t)) return "hidden";
if (/excelRange|chooseRegion|captureWindow|openWindowSelect|mouseRecord|screenShot|getScreenShot|screenshotSelectUi|clickPosition|collectData|drawDataTable|makeOcrTableSelector|cellRule|dateRange|searchRange/i.test(t)) return "region";
if (/^code|jsonEditor|codeInput/i.test(t)) return "code";
if (/encryption|designPassword/i.test(t)) return "secret";
if (/comboSelector|selectUi|uiwindowselector|phoneSelector|verifyElement|newVerifyElement|cvselectUi|uiselectorOcr/i.test(t)) return "element";
if (/selectFile|multiSelectFileOrDir|saveAsDialog|selectControllerFile/i.test(t)) return "file";
if (/selectControllerFolder/i.test(t)) return "folder";
if (/textArea|multipleTextArea|quillInput|logInput|multiInput|inputItemArray|inputCompose/i.test(t)) return "textarea";
if (/checkbox|switchControl|multifileSwitch|placementCheckbox|introViewCheckbox|multifile$/i.test(t)) return "boolean";
if (/dropDownSelect|dynamicSelect|browserTypeInput|databaseType|autoComplete|phoneSelector|remoteOptionSelect/i.test(t)) return "select";
if (/radioGroup|radioLeft|radio$|buttonGroup|coloredRadio|radioInputNumber/i.test(t)) return "radio";
if (/number|sliderBar|rangeInput|unitInput|inputInterval|movingSpeed|touchCoord|pageScroll|windowSize/i.test(t)) return "number";
return "text";
}
function fieldTypeFor(widget) {
if (widget === "number") return "number";
if (widget === "boolean") return "boolean";
if (widget === "region" || widget === "element" || widget === "file" || widget === "folder") return "object";
return "string";
}
function packageName(component) {
return String(component.baseType || component.id).split("_")[0];
}
function groupFor(component) {
const pkg = packageName(component);
for (const [group, packages] of Object.entries(PACKAGE_GROUPS)) if (packages.has(pkg)) return group;
const cats = new Set(component.categories || []);
if ([...cats].some((x) => /web|browser|sap/i.test(x))) return "web";
if ([...cats].some((x) => /excel|word|pdf/i.test(x))) return "office";
if ([...cats].some((x) => /ocr|image|vision/i.test(x))) return "vision";
if ([...cats].some((x) => /mobile|peripheral/i.test(x))) return "mobile";
if ([...cats].some((x) => /file|ftp/i.test(x))) return "file";
if ([...cats].some((x) => /mail|network|notification|nlp/i.test(x))) return "message";
if ([...cats].some((x) => /window|cursor|input|controller|element/i.test(x))) return "desktop";
return "data";
}
function normalizeVendorField(field) {
const widget = widgetFor(field.displayType, field.widget);
const valueTypes = Array.isArray(field.valueTypes) ? [...field.valueTypes] : [];
const result = {
key: field.key,
label: field.label || field.key,
widget,
type: fieldTypeFor(widget),
displayType: field.displayType || "",
required: Boolean(field.required),
defaultValue: cleanDefault(field.defaultValue),
placeholder: field.placeholder || "",
description: field.description || "",
advanced: Boolean(field.advanced),
valueTypes
};
if (Array.isArray(field.options) && field.options.length) result.options = field.options;
if (field.unit) result.unit = field.unit;
return result;
}
function loadCatalogSource() {
const raw = fs.readFileSync(SRC_CATALOG, "utf8");
const start = raw.indexOf("=");
if (start < 0) throw new Error("catalog source has no assignment");
let body = raw.slice(start + 1).trim();
if (body.endsWith(";")) body = body.slice(0, -1);
return JSON.parse(body);
}
function loadComponentsNew() {
if (!fs.existsSync(SRC_COMPONENTS_NEW)) return new Map();
const list = JSON.parse(fs.readFileSync(SRC_COMPONENTS_NEW, "utf8"));
const map = new Map();
for (const item of list) map.set(item.type || item.baseType || item.id, item);
return map;
}
function enrichFromVendor(field, vendorComponent) {
if (!vendorComponent) return field;
const args = vendorComponent.definition?.args0 || [];
const arg = args.find((a) => a.name === field.key);
if (!arg) return field;
const out = { ...field };
if (!out.label && arg.label) out.label = arg.label;
if (!out.description && arg.tip) out.description = arg.tip;
if (arg.displayParam?.text && !out.label) out.label = arg.displayParam.text;
if (arg.displayType && !out.displayType) out.displayType = arg.displayType;
if (arg.isAdvanced && !out.advanced) out.advanced = true;
const vTypes = Array.isArray(arg.vType) ? arg.vType.flatMap((v) => v.types || []) : [];
if (vTypes.length) out.valueTypes = [...new Set([...out.valueTypes, ...vTypes])];
const rules = arg.validatorRules?.compAttrValue;
if (Array.isArray(rules) && rules.some((r) => Number(r.required) === 1)) out.required = true;
if (Array.isArray(arg.displayParam?.properties) && arg.displayParam.properties.includes("openDirectory")) out.widget = "folder";
return out;
}
function outputOf(component, vendorComponent) {
const first = component.outputFields?.[0];
const name = cleanDefault(first?.defaultValue);
const typeNames = first?.valueTypes || [];
return {
label: first?.label || "",
key: first?.key || "",
suggestedName: /^[A-Za-z_\u4e00-\u9fa5][\w\u4e00-\u9fa5]*$/.test(name) ? name : "",
valueTypes: typeNames,
fields: (component.outputFields || []).map((f) => ({
key: f.key, label: f.label || f.key, defaultValue: cleanDefault(f.defaultValue), valueTypes: f.valueTypes || []
}))
};
}
function normalizeVendorComponent(component, vendorMap) {
const group = groupFor(component);
const vendor = vendorMap.get(component.id) || vendorMap.get(component.baseType);
const fields = (component.inputFields || []).map((f) => enrichFromVendor(normalizeVendorField(f), vendor));
const output = outputOf(component, vendor);
const risk = ["web", "app", "desktop", "message", "mobile"].includes(group) ? "high" : "low";
return {
id: component.id,
name: component.name,
kind: "vendor",
group,
categories: component.categories || [],
categorySignature: component.categorySignature || "",
description: component.description || component.messageTemplate || "",
messageTemplate: component.messageTemplate || "",
baseType: component.baseType || component.id,
functionName: component.functions?.[0]?.function || "",
functionCalls: (component.functions || []).map((f) => ({ function: f.function, args: f.arguments || [], assignTo: f.assignTo || "" })),
version: component.version || "",
tags: component.tags || [],
platformTag: component.platformTag || null,
icon: group,
risk,
fields,
output,
slots: []
};
}
function normalizeNativeNode(node) {
return {
id: node.id,
name: node.name,
kind: "native",
group: "control",
categories: node.categories || [],
categorySignature: (node.categories || []).join("/"),
description: node.description || "",
messageTemplate: "",
baseType: node.id,
functionName: node.id,
functionCalls: [],
version: "",
tags: [...(node.categories || []), node.kind || ""],
platformTag: null,
icon: "branch",
risk: node.id.includes("ai_vision") ? "high" : "low",
fields: (node.inputFields || []).map((key) => {
const booleanField = ["reverse", "is_async", "is_exits", "log_process"].includes(key);
return {
key,
label: CONTROL_LABELS[key] || key,
widget: booleanField ? "boolean" : "text",
type: booleanField ? "boolean" : "string",
displayType: booleanField ? "checkbox" : "text-expression",
required: key !== "log_process",
defaultValue: "",
placeholder: "",
description: "",
advanced: false,
valueTypes: []
};
}),
output: { label: "", key: "", suggestedName: node.outputFields?.[0] || "", valueTypes: [], fields: [] },
slots: node.childSlots || [],
nodeKind: node.kind || "statement"
};
}
function normalizeTrigger(trigger) {
return {
id: trigger.id,
name: trigger.name,
kind: "trigger",
group: "trigger",
categories: ["trigger"],
categorySignature: "trigger",
description: trigger.description,
messageTemplate: "",
baseType: "Trigger",
functionName: trigger.id,
functionCalls: [],
version: "",
tags: ["触发", trigger.name],
platformTag: null,
icon: "trigger",
risk: "low",
fields: trigger.fields.map((f) => normalizeVendorField(f)),
output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
slots: []
};
}
function main() {
const source = loadCatalogSource();
const vendorMap = loadComponentsNew();
const vendorComponents = (source.components || []).map((c) => normalizeVendorComponent(c, vendorMap));
const nativeNodes = (source.nativeControlNodes || []).map(normalizeNativeNode);
const triggers = TRIGGERS.map(normalizeTrigger);
const components = [...triggers, ...nativeNodes, ...vendorComponents];
const groupMap = new Map(GROUPS.map((g) => [g.id, { ...g, count: 0 }]));
for (const c of components) {
const g = groupMap.get(c.group) || groupMap.get("data");
g.count += 1;
}
const counts = {
total: components.length,
vendor: vendorComponents.length,
native: nativeNodes.length,
triggers: triggers.length,
fields: components.reduce((sum, c) => sum + c.fields.length, 0)
};
const catalog = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
source: {
catalog: path.basename(SRC_CATALOG),
componentsNew: fs.existsSync(SRC_COMPONENTS_NEW) ? path.basename(SRC_COMPONENTS_NEW) : null
},
counts,
groups: [...groupMap.values()],
components
};
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
fs.writeFileSync(OUT_FILE, JSON.stringify(applyCatalogOverrides(catalog)), "utf8");
const size = fs.statSync(OUT_FILE).size;
console.log(`catalog: ${counts.total} components (${counts.vendor} vendor + ${counts.native} native + ${counts.triggers} triggers)`);
console.log(`fields: ${counts.fields}; groups: ${catalog.groups.map((g) => `${g.id}:${g.count}`).join(",")}`);
console.log(`output: ${OUT_FILE} (${(size / 1024 / 1024).toFixed(2)} MB)`);
}
main();