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

390 lines
18 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";
// 平台标记单一源:从包内 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;
// A8 归一化:源值仅接受 win32|darwin|linux 白名单字符串,其余(含上游 shizai 的数字 1 脏值)一律重派生
const sourcePlatform = ["win32", "darwin", "linux"].includes(component.platformTag) ? component.platformTag : null;
const platformTag = sourcePlatform || (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);
});