From 0fe264a8840202a998d8b22e63f2f3b104330ab5 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 15:04:53 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E6=B8=85=E7=A9=BA=E5=8C=BA=E5=9F=9F?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20cellPos=20=E4=B8=8E=20startRow/startColumn?= =?UTF-8?q?=20=E9=94=AE=E5=B9=B6=E6=94=AF=E6=8C=81=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/capabilities/office-aliases.test.ts | 22 ++++++++++++++++++- src/main/capabilities/office-file-extended.ts | 22 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/capabilities/office-aliases.test.ts b/src/main/capabilities/office-aliases.test.ts index 2f5228d..249abda 100644 --- a/src/main/capabilities/office-aliases.test.ts +++ b/src/main/capabilities/office-aliases.test.ts @@ -1,6 +1,7 @@ import { readFile, mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types"; import type { RunContext } from "../engine/types"; @@ -19,7 +20,7 @@ afterEach(async () => Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursiv describe("common Excel aliases", () => { it("registers only real catalog aliases", async () => { - const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog; + const catalog = JSON.parse(await readFile(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../resources/catalog.json"), "utf8")) as Catalog; const ids = new Set(catalog.components.map((item) => item.id)); expect(EXCEL_COMMON_ALIASES.length).toBeGreaterThanOrEqual(25); for (const id of EXCEL_COMMON_ALIASES) { expect(ids.has(id), id).toBe(true); expect(getHandler(id), id).toBeTruthy(); } @@ -82,4 +83,23 @@ describe("common Excel aliases", () => { const savedAs = await run("Excel_SaveAs", { objExcel: created, filename: "rel-target.xlsx", isAbsolutePath: "False" }) as { path: string }; expect(savedAs.path).toBe(path.resolve(directory, "rel-target.xlsx")); }); + + it("clears a range through the Excel_ClearRange cellPos key", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-clear-")); dirs.push(directory); + const created = await run("Excel_NewExcel", { filePath: directory, name: "clear.xlsx" }) as { path: string }; + await run("Excel_WriteRange", { objExcel: created, sheet: "Sheet1", cellPos: "A1", data: [["甲", "乙"], ["丙", "丁"]] }); + await run("Excel_ClearRange", { objExcel: created, sheet: "Sheet1", cellPos: "A1:B2", isFormat: "False", isSave: "True" }); + const reopened = await run("Excel_OpenExcel", { filePath: created.path }) as { id: string }; + await expect(run("Excel_Cs_ReadContentV2", { workbook: reopened, sheet: "Sheet1", excelRng: "A1:B2" })).resolves.toEqual([["", ""], ["", ""]]); + }); + + it("clears through the Excel_Cs_ClearRange startRow/startColumn keys", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-clear-cs-")); dirs.push(directory); + const created = await run("Excel_NewExcel", { filePath: directory, name: "clear-cs.xlsx" }) as { path: string }; + await run("Excel_WriteRange", { objExcel: created, sheet: "Sheet1", cellPos: "A1", data: [["1", "2"], ["3", "4"]] }); + await run("Excel_Cs_ClearRange", { objExcel: created, sheet: "Sheet1", startRow: "2", startColumn: "A", lastRow: "2", lastColumn: "B", mode: "内容", save: "True" }); + const reopened = await run("Excel_OpenExcel", { filePath: created.path }) as { id: string }; + await expect(run("Excel_Cs_ReadContentV2", { workbook: reopened, sheet: "Sheet1", excelRng: "A2:B2" })).resolves.toEqual([["", ""]]); + await expect(run("Excel_Cs_ReadContentV2", { workbook: reopened, sheet: "Sheet1", excelRng: "A1:B1" })).resolves.toEqual([["1", "2"]]); + }); }); diff --git a/src/main/capabilities/office-file-extended.ts b/src/main/capabilities/office-file-extended.ts index d6dcec6..789beb3 100644 --- a/src/main/capabilities/office-file-extended.ts +++ b/src/main/capabilities/office-file-extended.ts @@ -164,12 +164,30 @@ register("Excel_Cs_DeleteColumn", async (_ctx, node, args) => { register("Excel_Cs_ClearRangeV4", async (_ctx, node, args) => { const source = await loadWorkbook(args, node.id, node.componentId); const sheet = worksheetOf(source.workbook, args); - const range = text(args, ["excelRng", "range"]); + // 兼容各别名版本的键:V4=excelRng、V2/V3=range、Legacy Excel_ClearRange=cellPos、V1=startRow/startColumn/lastRow/lastColumn + let range = text(args, ["excelRng", "range", "cellPos"]); + if (!range) { + const startRow = text(args, ["startRow"]); + const startColumn = text(args, ["startColumn"]); + if (startRow && startColumn) { + const lastRow = text(args, ["lastRow"], startRow); + const lastColumn = text(args, ["lastColumn"], startColumn); + range = `${startColumn}${startRow}:${lastColumn}${lastRow}`; + } + } if (!range) throw new RuntimeError("validation_error", "Range is empty", { nodeId: node.id, componentId: node.componentId }); const [start, end = start] = range.split(":"); const first = sheet.getCell(start); const last = sheet.getCell(end); - for (let row = first.row; row <= last.row; row += 1) for (let column = first.col; column <= last.col; column += 1) sheet.getCell(row, column).value = null; + // 清除模式:全部=内容+格式,格式=仅格式,其余(内容/默认)=仅内容;Legacy isFormat=True 同样清格式 + const mode = text(args, ["clearType", "mode"], "内容"); + const clearFormat = mode.includes("全部") || mode.includes("格式") || truthy(value(args, ["isFormat"])); + const clearValue = !mode.includes("格式"); + for (let row = first.row; row <= last.row; row += 1) for (let column = first.col; column <= last.col; column += 1) { + const cell = sheet.getCell(row, column); + if (clearValue) cell.value = null; + if (clearFormat) cell.style = {}; + } await saveWhenRequested(source, args); return range; }); From 7f3aaac2ff3b6278cc28ec2ca01b2d15ff9cc216 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 15:59:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20Excel=5FCs=5FSaveAs=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20path(=E7=9B=AE=E5=BD=95)+name(=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=90=8D)=20=E7=BB=84=E5=90=88=E5=8F=A6=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/capabilities/office-aliases.test.ts | 12 ++++++++++++ src/main/capabilities/office.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/capabilities/office-aliases.test.ts b/src/main/capabilities/office-aliases.test.ts index 249abda..f89e59a 100644 --- a/src/main/capabilities/office-aliases.test.ts +++ b/src/main/capabilities/office-aliases.test.ts @@ -1,6 +1,7 @@ import { readFile, mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import ExcelJS from "exceljs"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types"; @@ -84,6 +85,17 @@ describe("common Excel aliases", () => { expect(savedAs.path).toBe(path.resolve(directory, "rel-target.xlsx")); }); + it("combines the Excel_Cs_SaveAs directory path with the name field", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-saveas-cs-")); dirs.push(directory); + const created = await run("Excel_NewExcel", { filePath: directory, name: "cs-origin.xlsx" }) as { path: string }; + const targetDir = path.join(directory, "nested"); + const savedAs = await run("Excel_Cs_SaveAs", { workbook: created, path: targetDir, name: "cs-target.xlsx" }) as { path: string }; + expect(savedAs.path).toBe(path.resolve(targetDir, "cs-target.xlsx")); + const reopened = new ExcelJS.Workbook(); + await reopened.xlsx.readFile(savedAs.path); + expect(reopened.worksheets.length).toBeGreaterThan(0); + }); + it("clears a range through the Excel_ClearRange cellPos key", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-clear-")); dirs.push(directory); const created = await run("Excel_NewExcel", { filePath: directory, name: "clear.xlsx" }) as { path: string }; diff --git a/src/main/capabilities/office.ts b/src/main/capabilities/office.ts index 134b06d..9786cb2 100644 --- a/src/main/capabilities/office.ts +++ b/src/main/capabilities/office.ts @@ -178,9 +178,13 @@ const saveExcel: CapabilityHandler = async (_ctx, node, args) => { const saveAsExcel: CapabilityHandler = async (_ctx, node, args) => { const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId); - const raw = required(value(args, ["path", "file", "savePath", "filename", "saveFilePath"]), "另存为路径", node.id, node.componentId); + // CS 版(Excel_Cs_SaveAs)用 path=目录 + name=文件名;Legacy(Excel_SaveAs)用 filename=完整路径 + const rawName = text(args, ["name"]); + const raw = rawName + ? path.resolve(text(args, ["path", "file", "savePath"], ""), rawName) + : required(value(args, ["path", "file", "savePath", "filename", "saveFilePath"]), "另存为路径", node.id, node.componentId); // Legacy Excel_SaveAs 用 isAbsolutePath 标记 filename 是否为绝对路径,相对路径基于原文件目录 - const absolute = bool(value(args, ["isAbsolutePath"]), true); + const absolute = rawName ? true : bool(value(args, ["isAbsolutePath"]), true); const previousPath = handle.path; handle.path = absolute ? path.resolve(raw) : path.resolve(path.dirname(previousPath), raw); await fs.mkdir(path.dirname(handle.path), { recursive: true }); From 3a1c176f4dad26993a54956d0b1e989949c3698a Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 16:04:44 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E5=88=87=E6=8D=A2=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=97=B6=E6=B8=85=E7=A9=BA=E8=BF=90=E8=A1=8C=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=EF=BC=8C=E9=81=BF=E5=85=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=B8=8A=E4=B8=80=E6=B5=81=E7=A8=8B=E7=9A=84=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/store/editorStore.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/store/editorStore.ts b/src/renderer/src/store/editorStore.ts index 7fc31d4..1c2bf21 100644 --- a/src/renderer/src/store/editorStore.ts +++ b/src/renderer/src/store/editorStore.ts @@ -8,6 +8,7 @@ import type { FlowNodeOptions, FlowVariable } from "@shared/types"; +import { useRunStore } from "./runStore"; const clone = (value: T): T => structuredClone(value); @@ -111,7 +112,12 @@ export const useEditorStore = create((set, get) => { setCatalog: (catalog) => set({ catalog }), - loadFlow: (flow) => set({ flow, selectedNodeId: null, dirty: false, past: [], future: [] }), + loadFlow: (flow) => { + // 切换流程时清掉上一次的运行面板状态,避免显示旧流程的运行结果 + const runState = useRunStore.getState(); + if (!runState.running) runState.clear(); + set({ flow, selectedNodeId: null, dirty: false, past: [], future: [] }); + }, selectNode: (id) => set({ selectedNodeId: id }), From 37f2c8977bd9fe7d1eccbc660c3fbd97d72d3809 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 16:37:25 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=96=B0=E5=BB=BA=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E9=A6=96=E6=AC=A1=E8=BF=9B=E5=85=A5=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=BC=95=E5=AF=BC=E6=8F=90=E7=A4=BA=EF=BC=8C5=20=E7=A7=92?= =?UTF-8?q?=E5=90=8E=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/pages/EditorPage.tsx | 29 ++++++++++++- src/renderer/src/styles/app.css | 60 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/pages/EditorPage.tsx b/src/renderer/src/pages/EditorPage.tsx index b687909..8b43417 100644 --- a/src/renderer/src/pages/EditorPage.tsx +++ b/src/renderer/src/pages/EditorPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { ArrowLeft, Check, CircleAlert, CirclePlay, Loader2, Redo2, Save, Square, Undo2, X } from "lucide-react"; +import { ArrowLeft, Check, CircleAlert, CirclePlay, Lightbulb, Loader2, Redo2, Save, Square, Undo2, X } from "lucide-react"; import type { Catalog } from "@shared/types"; import { useEditorStore } from "../store/editorStore"; import { useRunStore } from "../store/runStore"; @@ -37,6 +37,23 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) { const [saveConflict, setSaveConflict] = useState(false); const [savedAt, setSavedAt] = useState(null); const [validationIssues, setValidationIssues] = useState(null); + // 新建流程首访引导:仅第一次进入空流程页面时展示,5 秒后自动关闭 + const [showGuide, setShowGuide] = useState(false); + + useEffect(() => { + if (!flow || flow.nodes.length > 1) return; + const seen = window.localStorage.getItem("rpa.editor.guide.seen"); + if (seen) return; + setShowGuide(true); + window.localStorage.setItem("rpa.editor.guide.seen", "1"); + const timer = window.setTimeout(() => setShowGuide(false), 5000); + return () => window.clearTimeout(timer); + }, [flow?.id]); + + useEffect(() => { + // 引导展示期间用户已拖入节点(不再是空流程)则立即收起 + if (showGuide && flow && flow.nodes.length > 1) setShowGuide(false); + }, [flow?.nodes.length, showGuide]); const save = useCallback(async (opts?: { interactive?: boolean }) => { const current = useEditorStore.getState().flow; @@ -213,6 +230,16 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) { addNode(componentId, position)} /> + {showGuide && ( +
+ +
+ 从左侧「组件库」拖入能力节点,开始搭建流程 + 拖拽到画布任意位置,或单击组件快速追加;支持按名称 / ID / 标签搜索。 +
+
+ )} +
可执行 {catalog.components.filter((item) => item.implemented).length} / {catalog.counts.total} 项 diff --git a/src/renderer/src/styles/app.css b/src/renderer/src/styles/app.css index cee8628..8edf309 100644 --- a/src/renderer/src/styles/app.css +++ b/src/renderer/src/styles/app.css @@ -2257,6 +2257,66 @@ textarea:focus-visible { margin-top: 2px; } +/* 新建流程首访引导:顶部浮出,5 秒后自动淡出,不接收鼠标事件 */ +.editor-guide { + position: absolute; + left: 50%; + top: 52px; + transform: translateX(-50%); + z-index: 5; + max-width: 460px; + padding: 11px 16px; + display: flex; + gap: 11px; + align-items: center; + border: 1px solid var(--line); + border-radius: 13px; + background: rgba(255, 255, 255, 0.94); + box-shadow: var(--shadow-panel); + backdrop-filter: blur(8px); + pointer-events: none; + animation: guide-life 5s ease forwards; +} + +.editor-guide-icon { + width: 30px; + height: 30px; + flex: none; + display: grid; + place-items: center; + border-radius: 9px; + background: var(--blue-soft); + color: var(--blue-dark); +} + +.editor-guide-icon .icon { + width: 16px; + height: 16px; +} + +.editor-guide-text strong { + display: block; + font-size: 12.5px; + font-weight: 700; + color: var(--ink); + line-height: 18px; +} + +.editor-guide-text span { + display: block; + font-size: 11px; + color: var(--muted); + line-height: 16px; + margin-top: 2px; +} + +@keyframes guide-life { + 0% { opacity: 0; transform: translateX(-50%) translateY(-10px); } + 6% { opacity: 1; transform: translateX(-50%) translateY(0); } + 90% { opacity: 1; } + 100% { opacity: 0; transform: translateX(-50%) translateY(-6px); } +} + .mono { font-family: var(--mono); } From 2b372e2d7f455f12b2ca0317ddfde25276ed8479 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 16:42:19 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E4=B8=AD=E5=A4=AE=E5=B8=B8=E9=A9=BB=E7=A9=BA=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=8F=90=E7=A4=BA=EF=BC=8C=E5=BC=95=E5=AF=BC=E8=81=8C?= =?UTF-8?q?=E8=B4=A3=E7=94=B1=E9=A6=96=E8=AE=BF=E6=A8=AA=E5=B9=85=E6=89=BF?= =?UTF-8?q?=E6=8B=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/canvas/FlowCanvas.tsx | 11 ------- src/renderer/src/styles/app.css | 40 -------------------------- 2 files changed, 51 deletions(-) diff --git a/src/renderer/src/canvas/FlowCanvas.tsx b/src/renderer/src/canvas/FlowCanvas.tsx index 1d2ed1f..9e52c4b 100644 --- a/src/renderer/src/canvas/FlowCanvas.tsx +++ b/src/renderer/src/canvas/FlowCanvas.tsx @@ -19,7 +19,6 @@ import { useEditorStore } from "../store/editorStore"; import { useRunStore } from "../store/runStore"; import { RpaNode, type RpaNodeData } from "./RpaNode"; import { ContainerNode, containerSlots, type ContainerNodeData } from "./ContainerNode"; -import { MousePointerClick } from "lucide-react"; const nodeTypes = { rpa: RpaNode, container: ContainerNode }; @@ -256,16 +255,6 @@ function CanvasInner({ catalog, onAddAt }: { catalog: Catalog; onAddAt: (compone maskColor="rgba(242,245,248,.72)" /> - - {flow.nodes.length <= 1 && ( -
- -
- 从左侧拖入能力,开始搭建流程 - 支持搜索 1016 项目录;单击组件也可追加。 -
-
- )}
); } diff --git a/src/renderer/src/styles/app.css b/src/renderer/src/styles/app.css index 8edf309..466ce26 100644 --- a/src/renderer/src/styles/app.css +++ b/src/renderer/src/styles/app.css @@ -2213,49 +2213,9 @@ textarea:focus-visible { margin-top: 2px; } -.canvas-hint { - position: absolute; - left: 50%; - top: 44%; - transform: translate(-50%, -50%); - z-index: 4; - min-width: 300px; - max-width: 420px; - padding: 13px 15px; - display: flex; - gap: 10px; - align-items: flex-start; - border: 1px solid var(--line); - border-radius: 13px; - background: rgba(255, 255, 255, 0.92); - box-shadow: var(--shadow-panel); - backdrop-filter: blur(8px); - pointer-events: none; -} -.canvas-hint .icon { - width: 18px; - height: 18px; - color: var(--blue); - flex: none; - margin-top: 1px; -} -.canvas-hint strong { - display: block; - font-size: 11.5px; - font-weight: 700; - color: var(--ink); - line-height: 17px; -} -.canvas-hint span { - display: block; - font-size: 10.5px; - color: var(--muted); - line-height: 15px; - margin-top: 2px; -} /* 新建流程首访引导:顶部浮出,5 秒后自动淡出,不接收鼠标事件 */ .editor-guide {