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

388 lines
17 KiB
JavaScript
Raw Normal View History

/**
* 从 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";
// 平台标记单一源:从包内 PLATFORM_GROUPS 逐域派生(npm run catalog 用 tsx 运行)
import { buildDomainPlatformMap, PLATFORM_GROUPS } from "../packages/rpa-core/src/capabilities/register-all";
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() {
// 优先环境变量/对面仓库路径;对面重构删除该文件后,回退到 temp/catalog-source/ 的历史恢复版
// (自 Agent-Employee commit 7bc627c8 恢复;temp/ 不入库,换机器需重新恢复)
const candidates = [SRC_CATALOG, path.join(ROOT, "temp", "catalog-source", "shizai-catalog-data.js")];
const found = candidates.find((p) => fs.existsSync(p));
if (!found) {
throw new Error(
`找不到目录源数据 shizai-catalog-data.js(尝试:${candidates.join(" ; ")})。` +
`可从 Agent-Employee 历史 commit 7bc627c8 恢复到 temp/catalog-source/`
);
}
const raw = fs.readFileSync(found, "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)+ 五件小事 #1:platformTag 从包内 PLATFORM_GROUPS 单一源派生
// (能力 id 命中包内注册 → win32;未实现的能力与宿主原生节点 → null 跨平台占位)。
const platforms = domainPlatformMap.get(component.id) || component.platforms;
const platformTag = component.platformTag || (platforms?.includes("win32") ? "win32" : null);
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,
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: []
};
}
async function main() {
// 平台单一源:逐域加载包内能力,得到「能力 id → 平台数组」映射(与运行时同一张 PLATFORM_GROUPS 表)
domainPlatformMap = await buildDomainPlatformMap();
console.log(`platform map: ${domainPlatformMap.size} capability ids from ${Object.keys(PLATFORM_GROUPS).length} domains`);
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)`);
}
/** 能力 id → 平台数组(buildDomainPlatformMap 的结果,main 内填充) */
let domainPlatformMap = new Map();
main().catch((err) => {
console.error(err);
process.exit(1);
});