feat: 增加可安装的RPA示例任务
This commit is contained in:
parent
76f2160950
commit
5de47ee420
@ -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/邮件属于用户显式配置的外部系统。
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<string, unknown> = {}, 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[] = [];
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -12,6 +12,7 @@ const api = {
|
||||
list: (): Promise<FlowSummary[]> => ipcRenderer.invoke("flows:list"),
|
||||
create: (name?: string): Promise<FlowDefinition> => ipcRenderer.invoke("flows:create", name),
|
||||
createDemo: (): Promise<FlowDefinition> => ipcRenderer.invoke("flows:create-demo"),
|
||||
createExamples: (): Promise<FlowSummary[]> => ipcRenderer.invoke("flows:create-examples"),
|
||||
get: (id: string): Promise<FlowDefinition> => ipcRenderer.invoke("flows:get", id),
|
||||
save: (flow: FlowDefinition): Promise<FlowDefinition> => ipcRenderer.invoke("flows:save", flow),
|
||||
delete: (id: string): Promise<boolean> => ipcRenderer.invoke("flows:delete", id)
|
||||
|
||||
@ -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() {
|
||||
<TasksPage
|
||||
flows={flows}
|
||||
onCreate={() => void createTask()}
|
||||
onCreateExamples={() => void createExamples()}
|
||||
onOpenEditor={(id) => void openEditor(id)}
|
||||
onRun={(id) => void runTask(id)}
|
||||
onDelete={(id) => void deleteTask(id)}
|
||||
|
||||
@ -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<string | null>(flows[0]?.id || null);
|
||||
|
||||
@ -77,6 +79,9 @@ export function TasksPage({ flows, onCreate, onOpenEditor, onRun, onDelete, onRe
|
||||
<button type="button" className="icon-btn" title="刷新任务列表" onClick={onRefresh}>
|
||||
<RefreshCw className="icon" />
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={onCreateExamples}>
|
||||
<PackagePlus className="icon" /> 安装示例任务
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={onCreate}>
|
||||
<Plus className="icon" /> 新建任务
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user