From 5de47ee4203c3adc8fd92fda9d00e4d436100a28 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 27 Oct 2026 16:37:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=8F=AF=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E7=9A=84RPA=E7=A4=BA=E4=BE=8B=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + src/main/flows.test.ts | 10 +++- src/main/flows.ts | 79 +++++++++++++++++++++++++++- src/main/index.ts | 8 ++- src/preload/index.ts | 1 + src/renderer/src/App.tsx | 10 ++++ src/renderer/src/pages/TasksPage.tsx | 7 ++- 7 files changed, 112 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b370ac1..6413053 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ docs 现状分析、实施方案 - 1016 项目录中当前有 619 项通过能力注册表或运行时容器标记为可执行,覆盖控制/触发、除数据库外的数据处理、文件、网页元素层级/表格/下拉框/浏览器控制/Cookie/下载及 DOM 高级能力、HTTP、Excel 常用读写/格式/筛选/历史版本、系统路径与环境变量、进程管理、桌面键鼠/窗口对象/剪贴板图片/对话框、PDF、Word 文档编辑、SMTP 与群机器人 Webhook、文本差异、工作日判断、屏幕截图、离线图像查找与点击;其余灰标「待接入」。运行前检查会提前定位待接入组件、必填参数、多出口与断链节点。 - 内置本地演示站默认监听 `127.0.0.1:17863`;新用户首次启动会获得“本地网页订单演示”任务,可离线验证网页输入、点击和文本读取。 +- 编排首页提供“安装示例任务”,可安装网页订单、文件文本、Excel读写、剪贴板、Word文档和异常分支 6 条固定示例;重复安装会更新同名示例,不会无限复制。 - 浏览器可信输入与定位机制参考 `C:\rpa-demo`:CDP Input、唯一定位、遮挡检查和输入回读校验;不使用 `el.click()` 伪造点击。 - 不包含厂商代码、二进制、模型或前端资产;目录是从厂商元数据派生的静态定义。 - 运行期不访问云服务;HTTP/邮件属于用户显式配置的外部系统。 diff --git a/src/main/flows.test.ts b/src/main/flows.test.ts index 8c31b85..323e4bb 100644 --- a/src/main/flows.test.ts +++ b/src/main/flows.test.ts @@ -11,7 +11,7 @@ vi.mock("electron", () => ({ } })); -import { createDemoFlow, createFlow, listFlows, saveFlow } from "./flows"; +import { createDemoFlow, createExampleFlows, createFlow, listFlows, saveFlow } from "./flows"; describe("flow summaries", () => { beforeEach(async () => { @@ -55,4 +55,12 @@ describe("flow summaries", () => { expect(flow.edges).toHaveLength(5); expect(flow.nodes.find((node) => node.id === "node_result")?.options.outputVar).toBe("订单结果"); }); + + it("creates six stable example flows without duplicate ids", () => { + const examples = createExampleFlows("http://127.0.0.1:17863/"); + expect(examples).toHaveLength(6); + expect(new Set(examples.map((flow) => flow.id)).size).toBe(6); + expect(examples.every((flow) => flow.nodes.length > 2 && flow.edges.length > 1)).toBe(true); + expect(examples.find((flow) => flow.id === "example_error_route")?.edges.some((edge) => edge.sourceHandle === "error")).toBe(true); + }); }); diff --git a/src/main/flows.ts b/src/main/flows.ts index 992d864..09790bf 100644 --- a/src/main/flows.ts +++ b/src/main/flows.ts @@ -1,7 +1,7 @@ import { app } from "electron"; import fs from "node:fs"; import path from "node:path"; -import type { FlowDefinition, FlowSummary } from "../shared/types"; +import type { FlowDefinition, FlowEdge, FlowNode, FlowSummary } from "../shared/types"; const ID_PATTERN = /^[A-Za-z0-9_-]{3,80}$/; @@ -92,6 +92,83 @@ export function createDemoFlow(url: string): FlowDefinition { return flow; } +function exampleFlow(id: string, name: string, description: string): FlowDefinition { + const flow = createFlow(name); + flow.id = id; + flow.description = description; + flow.nodes = []; + flow.edges = []; + return flow; +} + +function exampleNode(id: string, componentId: string, y: number, values: Record = {}, outputVar?: string): FlowNode { + return { id, componentId, position: { x: 220, y }, values, options: { timeoutMs: 30000, retryCount: 0, retryIntervalMs: 1000, skipOnError: false, logException: true, outputVar }, enabled: true }; +} + +function chain(nodes: FlowNode[]): FlowEdge[] { + return nodes.slice(0, -1).map((node, index) => ({ id: `edge_${node.id}_${nodes[index + 1].id}`, source: node.id, target: nodes[index + 1].id })); +} + +export function createExampleFlows(url: string): FlowDefinition[] { + const browser = createDemoFlow(url); + browser.id = "example_browser_order"; + browser.name = "示例 1 · 本地网页订单"; + + const file = exampleFlow("example_file_text", "示例 2 · 文件与文本处理", "在临时目录写入文本、读取、转换为大写并写入结果文件。"); + file.nodes = [ + exampleNode("file_start", "Trigger.Manual", 80, { entryName: "立即执行" }), + exampleNode("file_temp", "System_GetTempPath", 210, {}, "tempPath"), + exampleNode("file_write", "File_Write", 340, { path: "${tempPath}\\RPA-Studio-Demo\\input.txt", content: "hello rpa studio", encode: "utf-8", append: "False" }), + exampleNode("file_read", "File_Read", 470, { path: "${tempPath}\\RPA-Studio-Demo\\input.txt", encode: "utf-8" }, "sourceText"), + exampleNode("file_upper", "StrUtil_UpperCase", 600, { string: "${sourceText}" }, "upperText"), + exampleNode("file_result", "File_Write", 730, { path: "${tempPath}\\RPA-Studio-Demo\\result.txt", content: "${upperText}", encode: "utf-8", append: "False" }), + exampleNode("file_log", "Basic_DebugOutput", 860, { msg: "处理完成:${upperText}" }) + ]; file.edges = chain(file.nodes); + + const excel = exampleFlow("example_excel_orders", "示例 3 · Excel 订单读写", "在临时目录创建 Excel,写入订单数据并读取验证。"); + excel.nodes = [ + exampleNode("excel_start", "Trigger.Manual", 80, { entryName: "立即执行" }), + exampleNode("excel_temp", "System_GetTempPath", 210, {}, "tempPath"), + exampleNode("excel_new", "Excel_Cs_NewV5", 340, { path: "${tempPath}\\RPA-Studio-Demo", name: "orders.xlsx", override: "1", show: "0" }, "workbook"), + exampleNode("excel_write", "Excel_Cs_WriteRange", 470, { workbook: "${workbook}", sheet: "Sheet1", row: "1", column: "A", contents: '[["客户","金额","状态"],["张三",128,"已创建"],["李四",256,"已创建"]]', save: "1" }), + exampleNode("excel_read", "Excel_Cs_ReadRangeV2", 600, { workbook: "${workbook}", sheet: "Sheet1", range: "A1:C3", decimal: "0" }, "excelData"), + exampleNode("excel_log", "Basic_DebugOutput", 730, { msg: "Excel内容:${excelData}" }) + ]; excel.edges = chain(excel.nodes); + + const clipboard = exampleFlow("example_clipboard", "示例 4 · 剪贴板往返", "写入并读取 Windows 剪贴板文本。"); + clipboard.nodes = [ + exampleNode("clip_start", "Trigger.Manual", 80, { entryName: "立即执行" }), + exampleNode("clip_set", "Clipboard_SetText", 230, { text: "RPA Studio 剪贴板测试" }), + exampleNode("clip_get", "Clipboard_GetText", 380, {}, "clipboardText"), + exampleNode("clip_log", "Basic_DebugOutput", 530, { msg: "剪贴板内容:${clipboardText}" }) + ]; clipboard.edges = chain(clipboard.nodes); + + const word = exampleFlow("example_word_document", "示例 5 · Word 文档生成", "在临时目录创建 Word 文档、写入文本并读取验证,需要本机安装 Word。"); + word.nodes = [ + exampleNode("word_start", "Trigger.Manual", 80, { entryName: "立即执行" }), + exampleNode("word_temp", "System_GetTempPath", 200, {}, "tempPath"), + exampleNode("word_folder", "File_MakeDir", 320, { folder_path: "${tempPath}\\RPA-Studio-Demo" }), + exampleNode("word_new", "Word_New_V1", 440, { filePath: "${tempPath}\\RPA-Studio-Demo", name: "demo.docx", visible: "False" }, "wordDoc"), + exampleNode("word_write", "Word_Write", 560, { objWord: "${wordDoc}", text: "RPA Studio Word 自动化测试" }), + exampleNode("word_read", "Word_ReadAll", 680, { objWord: "${wordDoc}" }, "wordText"), + exampleNode("word_log", "Basic_DebugOutput", 800, { msg: "Word内容:${wordText}" }) + ]; word.edges = chain(word.nodes); + + const error = exampleFlow("example_error_route", "示例 6 · 异常分支", "故意读取不存在文件,验证右侧异常出口和错误变量。"); + error.nodes = [ + exampleNode("error_start", "Trigger.Manual", 80, { entryName: "立即执行" }), + { ...exampleNode("error_fail", "File_Read", 230, { path: "Z:\\RPA-DEMO-NOT-FOUND.txt", encode: "utf-8" }), options: { timeoutMs: 5000, retryCount: 0, retryIntervalMs: 1000, skipOnError: false, logException: true, errorRouteEnabled: true } }, + exampleNode("error_normal", "Basic_DebugOutput", 390, { msg: "不应执行到正常出口" }), + { ...exampleNode("error_recover", "Basic_DebugOutput", 230, { msg: "异常已捕获:${error.reason}" }), position: { x: 560, y: 230 } } + ]; error.edges = [ + { id: "edge_error_start", source: "error_start", target: "error_fail" }, + { id: "edge_error_normal", source: "error_fail", target: "error_normal" }, + { id: "edge_error_recover", source: "error_fail", sourceHandle: "error", target: "error_recover" } + ]; + + return [browser, file, excel, clipboard, word, error]; +} + export function listFlows(): FlowSummary[] { const base = dir(); const rows: FlowSummary[] = []; diff --git a/src/main/index.ts b/src/main/index.ts index f416fd0..f80fb10 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,7 +3,7 @@ import path from "node:path"; import type { AppInfo, FlowDefinition } from "../shared/types"; import "./capabilities"; import { catalogPath, loadCatalog } from "./catalog"; -import { createDemoFlow, createFlow, deleteFlow, getFlow, listFlows, saveFlow } from "./flows"; +import { createDemoFlow, createExampleFlows, createFlow, deleteFlow, getFlow, listFlows, saveFlow } from "./flows"; import { capabilityCount, capabilityIds } from "./engine/registry"; import { closeBrowserSession, startRun, stopRun } from "./run/manager"; import { fixtureUrl, startFixtureServer, stopFixtureServer } from "./fixture/server"; @@ -76,6 +76,12 @@ function registerIpc(): void { const url = fixtureUrl(); return saveFlow(url ? createDemoFlow(url) : createFlow("未命名任务")); }); + ipcMain.handle("flows:create-examples", () => { + const url = fixtureUrl(); + if (!url) throw new Error("本地演示站未启动"); + for (const flow of createExampleFlows(url)) saveFlow(flow); + return listFlows(); + }); ipcMain.handle("flows:get", (_event, id: string) => getFlow(id)); ipcMain.handle("flows:save", (_event, flow: FlowDefinition) => saveFlow(flow)); ipcMain.handle("flows:delete", (_event, id: string) => { diff --git a/src/preload/index.ts b/src/preload/index.ts index 71f139a..648452c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -12,6 +12,7 @@ const api = { list: (): Promise => ipcRenderer.invoke("flows:list"), create: (name?: string): Promise => ipcRenderer.invoke("flows:create", name), createDemo: (): Promise => ipcRenderer.invoke("flows:create-demo"), + createExamples: (): Promise => ipcRenderer.invoke("flows:create-examples"), get: (id: string): Promise => ipcRenderer.invoke("flows:get", id), save: (flow: FlowDefinition): Promise => ipcRenderer.invoke("flows:save", flow), delete: (id: string): Promise => ipcRenderer.invoke("flows:delete", id) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3653588..9ee81fc 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -149,6 +149,15 @@ export function App() { } }; + const createExamples = async () => { + try { + setFlows(await window.rpa.flows.createExamples()); + showToast("已安装 6 条示例任务"); + } catch (exampleError) { + showToast(exampleError instanceof Error ? exampleError.message : String(exampleError), "error"); + } + }; + const deleteTask = async (id: string) => { if (flows.length <= 1) { showToast("至少保留一个任务", "error"); @@ -245,6 +254,7 @@ export function App() { void createTask()} + onCreateExamples={() => void createExamples()} onOpenEditor={(id) => void openEditor(id)} onRun={(id) => void runTask(id)} onDelete={(id) => void deleteTask(id)} diff --git a/src/renderer/src/pages/TasksPage.tsx b/src/renderer/src/pages/TasksPage.tsx index 5275932..06793a8 100644 --- a/src/renderer/src/pages/TasksPage.tsx +++ b/src/renderer/src/pages/TasksPage.tsx @@ -8,6 +8,7 @@ import { ListChecks, Pencil, Plus, + PackagePlus, RefreshCw, Search, Trash2, @@ -18,6 +19,7 @@ import type { FlowSummary } from "@shared/types"; interface TasksPageProps { flows: FlowSummary[]; onCreate: () => void; + onCreateExamples: () => void; onOpenEditor: (id: string) => void; onRun: (id: string) => void; onDelete: (id: string) => void; @@ -43,7 +45,7 @@ function formatDuration(value?: number): string { const RUN_LABEL = { succeeded: "成功", failed: "失败", cancelled: "已取消", never: "未运行" } as const; -export function TasksPage({ flows, onCreate, onOpenEditor, onRun, onDelete, onRefresh }: TasksPageProps) { +export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onRun, onDelete, onRefresh }: TasksPageProps) { const [query, setQuery] = useState(""); const [selectedId, setSelectedId] = useState(flows[0]?.id || null); @@ -77,6 +79,9 @@ export function TasksPage({ flows, onCreate, onOpenEditor, onRun, onDelete, onRe +