PRA-only-demo/scripts/build-catalog.mjs

366 lines
16 KiB
JavaScript
Raw Permalink 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 normalizeOptions(value) {
if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === "string") return { label: item, value: item };
if (typeof item !== "object" || item === null) return null;
const label = String(item.label ?? item.text ?? item.value ?? "");
const rawValue = item.value ?? item.label ?? "";
return { label, value: cleanDefault(rawValue) };
}).filter((item) => item.label || item.value);
}
if (Array.isArray(value?.options)) return normalizeOptions(value.options);
return [];
}
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";
// 任务 3.4(决策点 7):平台标记只填 catalog 数据,渲染层置灰延后。
// Windows 专用组:desktop(user32/键鼠)、app(COM/进程)、office(Excel/Word COM)、vision(PowerShell 截屏);
// data/file/message 纯 Node,web 走 CDP,均视为跨平台(null)。
const WIN32_GROUPS = new Set(["desktop", "app", "office", "vision"]);
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 || (WIN32_GROUPS.has(group) ? "win32" : 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();