File_File_Move 目标为已存在目录时自动移入该目录

此前 pathDest 被固定当作完整目标文件路径, 目标填分类目录时
rename(文件->目录)失败, fallback cp 报 ENOTDIR(cannot overwrite
directory with non-directory), 文件整理类流程无法用目录作目标。

现在目标存在且为目录时自动拼接源文件名(移入语义), 目标不存在
仍按完整文件路径处理。

附: 晨间助手全家桶测试流程生成脚本(30节点, 文件整理+记事本
键鼠+Excel加工+OCR/AI)
This commit is contained in:
yu 2026-11-05 16:05:07 +08:00
parent b59d906161
commit 902ee0cec4
2 changed files with 202 additions and 0 deletions

View File

@ -92,6 +92,16 @@ register(["File_File_Move", "File_Move"], async (_ctx, node, args) => {
const newName = truthy(args.rename) ? pick(args, ["name"]) : "";
target = requirePath(newName ? path.join(destDir, newName) : destDir, "目的地路径", node.id, node.componentId);
}
let targetStat: import("node:fs").Stats | null = null;
try {
targetStat = await fs.stat(target);
} catch {
// 目标不存在,按完整文件路径处理
}
if (targetStat?.isDirectory()) {
// 目标是已存在的目录:语义为“移入该目录”,自动拼上源文件名
target = path.join(target, path.basename(source));
}
await fs.mkdir(path.dirname(target), { recursive: true });
try {
await fs.rename(source, target);

192
scripts/gen-morning-flow.py Normal file
View File

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
"""生成「全家桶 · 晨间自动化助手」流程 JSON。"""
import json
import os
BASE = r"C:\Users\Administrator\Desktop\RPA整理测试"
EXCEL = "C:/Users/Administrator/Desktop/淘宝手机比价.xlsx"
OUT_XLSX = "C:/Users/Administrator/Desktop/低价手机清单.xlsx"
SHOT = BASE + "\\整理完成截图.png"
nodes = []
edges = []
def node(id, componentId, label, values, output_var=None, timeout=30000, parent=None, slot=None, pos=None, options_extra=None):
options = {"timeoutMs": timeout}
if output_var:
options["outputVar"] = output_var
if options_extra:
options.update(options_extra)
n = {
"id": id,
"componentId": componentId,
"label": label,
"values": values,
"options": options,
}
if parent:
n["parentId"] = parent
n["slot"] = slot
n["position"] = {"x": 18, "y": 0}
else:
n["position"] = pos
nodes.append(n)
return n
def link(a, b):
edges.append({"id": f"edge_{a}_{b}", "source": a, "target": b})
# ============ 主链 ============
node("node_start", "Trigger.Manual", "立即执行", {"entryName": "立即执行"}, pos={"x": 240, "y": 70})
# ---- 板块 A:文件整理 ----
node("node_scan", "File_GetFileList_V1", "扫描测试文件夹", {
"folder_path": BASE, "key_word": "", "ignore": "",
"recursive": "0", "is_full_path": "1", "sort_type": "1",
}, output_var="dlFiles", pos={"x": 240, "y": 210})
node("node_mkdir_zip", "File_MakeDir", "建分类文件夹:压缩包", {"path": BASE + "\\压缩包", "name": "", "overwrite": "False"}, pos={"x": 240, "y": 340})
node("node_mkdir_img", "File_MakeDir", "建分类文件夹:图片", {"path": BASE + "\\图片", "name": "", "overwrite": "False"}, pos={"x": 240, "y": 460})
node("node_mkdir_doc", "File_MakeDir", "建分类文件夹:文档", {"path": BASE + "\\文档", "name": "", "overwrite": "False"}, pos={"x": 240, "y": 580})
node("node_mkdir_other", "File_MakeDir", "建分类文件夹:其他", {"path": BASE + "\\其他", "name": "", "overwrite": "False"}, pos={"x": 240, "y": 700})
node("node_loop", "logic_control_array_for_v3", "遍历文件逐个分类", {
"array_name": "dlFiles", "reverse": "0", "start_index": "0", "end_index": "-1",
}, pos={"x": 240, "y": 840})
# 循环体内:三层 IF 嵌套分类
node("node_if_zip", "logic_control_if_else", "是压缩包?", {
"condition": '${contains(lower(item), ".zip") || contains(lower(item), ".rar") || contains(lower(item), ".7z")}',
}, parent="node_loop", slot="array_for_fn")
node("node_move_zip", "File_File_Move", "移入「压缩包」", {
"file_type": "file", "file_path": "${item}", "target_path": BASE + "\\压缩包", "overwrite": "True",
}, parent="node_if_zip", slot="statement_if")
node("node_if_img", "logic_control_if_else", "是图片?", {
"condition": '${contains(lower(item), ".png") || contains(lower(item), ".jpg") || contains(lower(item), ".jpeg") || contains(lower(item), ".gif") || contains(lower(item), ".bmp")}',
}, parent="node_if_zip", slot="statement_else")
node("node_move_img", "File_File_Move", "移入「图片」", {
"file_type": "file", "file_path": "${item}", "target_path": BASE + "\\图片", "overwrite": "True",
}, parent="node_if_img", slot="statement_if")
node("node_if_doc", "logic_control_if_else", "是文档?", {
"condition": '${contains(lower(item), ".doc") || contains(lower(item), ".pdf") || contains(lower(item), ".txt") || contains(lower(item), ".xlsx") || contains(lower(item), ".ppt")}',
}, parent="node_if_img", slot="statement_else")
node("node_move_doc", "File_File_Move", "移入「文档」", {
"file_type": "file", "file_path": "${item}", "target_path": BASE + "\\文档", "overwrite": "True",
}, parent="node_if_doc", slot="statement_if")
node("node_move_other", "File_File_Move", "移入「其他」", {
"file_type": "file", "file_path": "${item}", "target_path": BASE + "\\其他", "overwrite": "True",
}, parent="node_if_doc", slot="statement_else")
# ---- 板块 B:Excel 数据加工 ----
node("node_read_excel", "Bivartable_DoOpenExcel", "读取淘宝比价 Excel", {
"filePath": EXCEL, "sheetName": "Sheet1", "indexx": "False",
}, output_var="priceTable", pos={"x": 240, "y": 990})
node("node_sort", "Bivartable_SortDf", "按价格升序排序", {
"df": "${priceTable}", "name": "价格", "sort": "True", "showTable": "0",
}, output_var="sortedTable", pos={"x": 240, "y": 1110})
node("node_filter", "Bivartable_FilterDf", "筛选 3000 元以下", {
"df": "${sortedTable}", "filter_mode": "ordinary", "col_name": "价格",
"choice": "4", "criteria": "3000", "rule": '["价格"] <= 3000', "showTable": "0",
}, output_var="filteredTable", pos={"x": 240, "y": 1230})
node("node_min", "Bivartable_GetMinValue", "算最低价", {
"df": "${priceTable}", "groupby": "", "minvaluecols": "价格",
}, output_var="minPrice", pos={"x": 240, "y": 1350})
node("node_max", "Bivartable_GetMaxValue", "算最高价", {
"df": "${priceTable}", "groupby": "", "maxvaluecols": "价格",
}, output_var="maxPrice", pos={"x": 240, "y": 1470})
node("node_avg", "Bivartable_GetAvgValue", "算平均价", {
"df": "${priceTable}", "groupby": "", "avgvaluecols": "价格",
}, output_var="avgPrice", pos={"x": 240, "y": 1590})
node("node_write_cheap", "Bivartable_DfWriteToFile", "保存低价清单 Excel", {
"df": "${filteredTable}", "suffix": "xlsx", "filePath": OUT_XLSX, "folderPath": "C:/Users/Administrator/Desktop", "sheet": "Sheet1",
}, pos={"x": 240, "y": 1710})
# ---- 板块 C:记事本键鼠 + 截图 ----
node("node_notepad_run", "App_Run", "启动记事本", {
"executeable": "notepad.exe", "params": "", "cwd": "", "is_admin": "0", "create_new_console": "0",
}, output_var="notepadPid", pos={"x": 240, "y": 1830})
node("node_delay_notepad", "Basic_Delay_V2", "等记事本就绪", {
"is_random": "0", "second": "1", "min_second": "1", "max_second": "5",
}, pos={"x": 240, "y": 1950})
summary_expr = (
'${"RPA 晨间助手报告 " + today() + ":已整理测试文件 " + len(dlFiles) + " 个;'
'比价表筛出 3000 元以下手机 " + (len(filteredTable) - 1) + " 款;'
'最低 " + minPrice + " 元,最高 " + maxPrice + " 元,平均 " + round(avgPrice, 1) + " 元。"}'
)
node("node_notepad_type", "Keyboard_Send_V1", "记事本输入摘要(键盘模拟)", {
"words": summary_expr, "interval": "10", "fuctype": "0", "window_title": "记事本",
}, pos={"x": 240, "y": 2070})
node("node_shot", "Image_Screenshot", "截屏留证", {
"pictureSource": "1", "shotRange": "1", "image_path": SHOT, "position": "", "outImageName": "",
}, pos={"x": 240, "y": 2190})
node("node_notepad_kill", "App_Kill", "关闭记事本", {
"pid": "${notepadPid}",
}, pos={"x": 240, "y": 2310})
# ---- 板块 D:OCR + AI + 收尾 ----
node("node_ocr", "OcrOffline_GetOcrTextNew", "OCR 识别截图", {
"image_path": SHOT,
}, output_var="ocrText", timeout=120000, pos={"x": 240, "y": 2430})
ai_prompt = (
"这是一次 RPA 自动化的收尾总结。屏幕 OCR 识别到的内容:${ocrText}。"
"数据统计:整理测试文件 ${len(dlFiles)} 个;低价手机 ${len(filteredTable) - 1} 款;"
"最低价 ${minPrice} 元、最高价 ${maxPrice} 元、平均 ${round(avgPrice, 1)} 元。"
"请用中文生成一段 100 字左右的晨间摘要,说明本次自动化完成了哪些事、结果如何,语气轻松一点。"
)
node("node_ai", "DataService_LargeModel", "AI 生成晨间摘要", {
"hint_word": ai_prompt,
}, output_var="aiSummary", timeout=200000, pos={"x": 240, "y": 2550})
node("node_clipboard", "Clipboard_SetText", "摘要复制到剪贴板", {
"text": "${aiSummary}",
}, pos={"x": 240, "y": 2670})
node("node_notice", "Dialog_Notice_V2", "弹窗通知完成", {
"text": "晨间摘要已生成并复制到剪贴板:${aiSummary}", "title": "RPA 晨间助手", "time_out": "20",
}, timeout=60000, pos={"x": 240, "y": 2790})
# ---- 主链连线 ----
main_chain = [
"node_start", "node_scan",
"node_mkdir_zip", "node_mkdir_img", "node_mkdir_doc", "node_mkdir_other",
"node_loop",
"node_read_excel", "node_sort", "node_filter",
"node_min", "node_max", "node_avg", "node_write_cheap",
"node_notepad_run", "node_delay_notepad", "node_notepad_type", "node_shot", "node_notepad_kill",
"node_ocr", "node_ai", "node_clipboard", "node_notice",
]
for a, b in zip(main_chain, main_chain[1:]):
link(a, b)
flow = {
"schemaVersion": 1,
"id": "flow_morning_assistant",
"name": "全家桶 · 晨间自动化助手",
"description": "测试长流程:文件整理(IF分支+遍历+文件操作) → Excel数据加工(读取/排序/筛选/统计/落盘) → 记事本键鼠模拟+截屏 → OCR识别+AI摘要+剪贴板+弹窗通知。",
"enabled": True,
"trigger": {"label": "手动运行", "type": "manual"},
"variables": [],
"nodes": nodes,
"edges": edges,
"meta": {
"createdAt": "2026-11-05T07:25:00.000Z",
"updatedAt": "2026-11-05T07:25:00.000Z",
"appVersion": "0.1.0",
},
}
out = os.path.join(os.environ["APPDATA"], "rpa-studio", "flows", "flow_morning_assistant.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(flow, f, ensure_ascii=False, indent=2)
print("written:", out, "| nodes:", len(nodes), "| edges:", len(edges))