390 lines
18 KiB
JavaScript
390 lines
18 KiB
JavaScript
/**
|
||
* 从 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);
|
||
});
|