/** * 从 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();