1. 这不是RPA教程,而是一份三个月实战后的“选型避坑手记”
RPA这个词,这两年在自动化圈子里火得有点烫手。但凡聊起流程自动化,十个人里有八个会脱口而出“用RPA试试”,剩下两个在查RPA和低代码的区别。可真正从零开始搭一个能跑通、能维护、能交付的RPA系统,光靠厂商宣传页上的“三步上线”“零代码拖拽”是远远不够的——尤其当你手头没预算买商业 license,又不想被云平台绑定,更不愿把核心业务逻辑交到第三方服务器上时。
我就是那个在去年夏天咬牙决定自己动手的人。项目标题里说的“三个月”,不是夸张修辞:前两周在环境里反复踩坑,中间六周集中开发五个真实业务流(财务对账、电商订单抓取、内部报表生成、客户信息同步、工单状态回填),最后四周全在做稳定性压测、异常日志归因和交接文档沉淀。整个过程没用任何商业RPA平台,全程基于开源技术栈搭建,核心组件就三个:FreeRPA(风影 RPA)作为主控引擎,SQLite 作为本地状态中枢,Deno 作为轻量级脚本胶水层。这三者组合起来,意外地形成了一套极简但极其可控的自动化底座。
为什么选这三个?不是因为它们最热门,而是因为它们解决了我在选型阶段最揪心的几个问题:第一,能不能离线运行——财务数据不能出内网;第二,状态能不能持久化且可审计——每次执行完,谁改了什么、改成了啥,必须一查就清;第三,逻辑能不能快速迭代不依赖UI重录——业务规则一个月变三次,重录脚本等于推倒重来;第四,出错了能不能像调试普通程序一样定位——不是弹个“操作失败,请重试”的模糊提示;第五,部署能不能做到“复制即用”——新同事拿到U盘,插上就能跑,不需要装一堆运行时。
这五件事,恰恰是多数RPA工具在宣传中刻意弱化、甚至回避的硬伤。而FreeRPA+SQLite+Deno这套组合,用三个月实打实的生产验证,把它们一一拆解、落地、固化成了可复用的方法论。接下来的内容,不会教你如何点击“新建流程”,也不会罗列每个组件的API文档——我会直接带你走进那五个真实业务流的代码片段、日志截图、数据库结构变更记录,以及我写在便签纸上、贴在显示器边框上的那些血泪备注:“这里必须加事务锁”“SQLite日期格式别用datetime()函数”“Deno fetch超时设成8秒刚好卡在页面加载临界点”。这才是一个真实RPA工程师每天面对的东西。
2. 为什么是 FreeRPA + SQLite + Deno?一套组合拳背后的底层逻辑
2.1 FreeRPA:不是“影刀平替”,而是“可控性优先”的必然选择
市面上提到RPA,大家第一反应是影刀、UiPath、金智维这些名字。它们确实强大,可视化编辑器流畅,组件库丰富,企业级管理后台完善。但当我把“能否离线”“能否审计”“能否深度定制”这几个硬指标列出来时,这些平台的短板立刻暴露:影刀RPA的本地版虽支持离线,但所有流程包编译后是加密二进制,无法查看或修改底层逻辑;UiPath Community Edition强制要求联网激活,且日志默认只存本地临时目录,清理后不可恢复;金智维则完全闭源,连基础组件的源码都看不到。
FreeRPA(风影 RPA)的出现,恰恰填补了这个空白。它本质是一个基于Electron的桌面应用,但所有核心逻辑都以TypeScript源码形式开放。这意味着什么?意味着我可以直接修改它的“元素识别引擎”——比如把默认的OCR识别换成自己训练的轻量模型;意味着我能重写它的“等待机制”,让它不再死等某个按钮出现,而是结合页面DOM变化+网络请求状态双校验;更重要的是,它的“流程执行器”是模块化的,我可以把原本耦合在UI层的业务逻辑,抽离成独立的Deno脚本,再通过标准IPC协议调用。
提示:FreeRPA的“自定义组件”功能不是噱头。它允许你用任意语言(Python、Go、Deno)写一个命令行程序,只要接收JSON输入、输出JSON结果,就能无缝接入流程图。这直接绕开了RPA工具最常被诟病的“逻辑黑盒”问题——你的业务规则,永远是你自己写的代码,而不是拖拽出来的神秘节点。
我选FreeRPA,根本原因就一条:它把RPA从“配置工具”拉回到了“开发框架”的层面。它不承诺“零代码”,反而坦诚告诉你:“你需要写代码,但我们会让你写得更少、更稳、更可追踪。”
2.2 SQLite:不是“凑合用”,而是“状态可信”的唯一解
RPA最怕什么?不是流程跑不通,而是跑通了却不知道它到底干了什么。比如一个电商订单抓取流程,它成功下载了100条数据,但其中第47条因为价格字段为空被跳过——这个“跳过”动作,是静默发生的,还是记录在案的?如果第二天运营发现少了5单,你拿什么去证明“不是我的锅”?
商业RPA平台通常用两种方式处理状态:一种是把所有日志打到云端,你得登录后台看;另一种是存本地文件,但格式混乱,没有索引,查一条记录得grep半天。而我选择SQLite,是因为它用一个单文件,同时解决了结构化、可查询、可事务、可嵌入四大痛点。
- 结构化:我为每个业务流建一张表,比如
order_sync_log,字段包括id,timestamp,source_order_id,status(success/failed/skipped),error_message,raw_data_json。所有执行结果,不是一行行文本,而是标准的INSERT语句。 - 可查询:当运营质疑“为什么没同步XX订单”时,我打开DB Browser for SQLite,敲一句
SELECT * FROM order_sync_log WHERE source_order_id = 'ORD20231001',3秒内给出完整执行链路,包括原始数据快照和错误堆栈。 - 可事务:财务对账流程涉及“读取银行流水→匹配内部账单→生成差异报告→邮件通知”四步。任何一步失败,必须回滚前面所有操作。SQLite的
BEGIN TRANSACTION和ROLLBACK让我能精确控制原子性,避免“部分成功”带来的数据污染。 - 可嵌入:FreeRPA的Node.js运行时天然支持
sqlite3包,Deno也通过deno-sqlite提供原生驱动。整个状态中枢,不需要额外安装服务、配置端口、管理用户权限——就是一个.db文件,随项目一起打包。
注意:SQLite不是万能的。它不适合高并发写入(比如每秒上千次INSERT),也不适合PB级数据。但对RPA场景——单机、低频、强一致性要求——它几乎是完美的。我见过太多团队用MySQL存RPA日志,结果因为连接池配置不当,导致流程执行一半卡死,最后发现是数据库连接耗尽。
2.3 Deno:不是“尝鲜”,而是“脚本可靠性”的终极保障
RPA流程里,90%的复杂逻辑不在UI操作,而在数据清洗、API对接、条件判断、异常分支。传统方案是用RPA工具自带的表达式编辑器,或者调用外部Python脚本。前者功能有限,后者则带来环境管理噩梦:Python版本冲突、pip依赖打架、Windows路径分隔符报错……我曾为一个Excel处理脚本,在三台测试机上花两天配环境。
Deno的出现,彻底终结了这种混乱。它用TypeScript作为默认语言,内置HTTP客户端、文件系统API、JSON解析器,所有标准能力开箱即用。最关键的是,它没有node_modules。所有依赖通过URL导入,版本锁定在import语句里:
import { parse } from "https://deno.land/std@0.208.0/csv/parse.ts"; import { serve } from "https://deno.land/std@0.208.0/http/server.ts";这意味着什么?意味着我把一个Deno脚本发给同事,他只需要deno run script.ts,Deno会自动下载并缓存指定版本的依赖,整个过程干净、可重现、无污染。更妙的是,Deno的权限模型(--allow-read,--allow-write,--allow-env)强制你声明脚本需要什么能力,杜绝了“某个RPA脚本偷偷读取了C盘所有Excel文件”这类安全盲区。
在实际项目中,我用Deno做了三类事:一是作为FreeRPA的“外挂逻辑处理器”,比如把抓取的HTML表格转成结构化JSON;二是独立运行的定时任务,比如每天凌晨3点自动检查SQLite里标记为pending的工单,调用企业微信API推送提醒;三是构建简易Web界面,让非技术人员也能通过浏览器查看RPA执行统计——就用Deno的serve函数搭个静态服务,前端用纯HTML+JS,连Webpack都不用。
这套组合的底层共识很清晰:FreeRPA负责“与世界交互”(点击、输入、截图),SQLite负责“记住发生了什么”,Deno负责“决定下一步做什么”。三者边界分明,各司其职,没有冗余耦合,也没有隐藏黑箱。
3. 核心细节拆解:从“能跑”到“稳跑”的五个关键实操点
3.1 元素识别稳定性:告别“今天能点,明天报错”的玄学
RPA最让人崩溃的,莫过于昨天还好好运行的流程,今天一启动就卡在登录按钮上,报错“元素未找到”。根源往往不是XPath写错了,而是页面加载节奏、动态ID生成、CSS动画延迟这些前端细节在作祟。
我的解法是:放弃纯XPath,转向“多特征融合识别”。FreeRPA支持自定义识别器,我写了一个TS模块,它不只看id="login-btn",而是同时校验四个维度:
- 可见性:
element.offsetParent !== null - 可点击性:
element.getAttribute('disabled') === null && getComputedStyle(element).cursor === 'pointer' - 文本内容:
element.textContent.trim() === '登 录'(注意空格和全角字符) - 父容器状态:检查其最近的
form元素是否style.display !== 'none'
这个识别器被封装成smartClick.ts,在FreeRPA的“自定义动作”里调用:
// smartClick.ts export async function smartClick(selector: string): Promise<void> { const el = document.querySelector(selector); if (!el) throw new Error(`Element not found: ${selector}`); // 四重校验 if (!el.offsetParent) throw new Error("Element is not visible"); if (el.getAttribute('disabled') || getComputedStyle(el).cursor !== 'pointer') throw new Error("Element is disabled or not clickable"); if (el.textContent?.trim() !== '登 录') throw new Error("Text content mismatch"); if (el.closest('form')?.style.display === 'none') throw new Error("Parent form is hidden"); el.click(); }实操心得:这个方案让登录成功率从82%提升到99.7%。关键在于,它把“等待元素出现”的被动逻辑,变成了“主动验证元素可用性”的主动逻辑。FreeRPA的“等待元素”动作只是在DOM里轮询,而我的
smartClick是在元素出现后,再做一次业务语义级的健康检查。
3.2 SQLite状态写入:如何避免“日志写了,但流程崩了”的数据撕裂
RPA流程常包含“读→处理→写”三步。如果写入SQLite发生在流程末尾,而中间某步崩溃,就会导致日志缺失,无法追溯失败原因。更糟的是,如果写入本身失败(比如磁盘满),整个流程可能卡死。
我的方案是:“先写日志,再执行,最后更新状态”。以客户信息同步为例:
- 流程启动,立即向
sync_log表插入一条记录,status='init',raw_data_json存原始API返回; - 执行数据清洗、去重、格式转换;
- 调用CRM API提交数据;
- 根据API返回,更新该记录的
status为success或failed,并写入error_message。
这样设计,即使第3步崩溃,数据库里也有一条init记录,时间戳、原始数据都在,排查时直接查这条记录就行。
-- sync_log 表结构 CREATE TABLE sync_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, source_system TEXT NOT NULL, record_id TEXT NOT NULL, status TEXT CHECK(status IN ('init', 'success', 'failed', 'skipped')) NOT NULL, raw_data_json TEXT, error_message TEXT, processed_at DATETIME );注意:SQLite的
CURRENT_TIMESTAMP在Windows下有时区问题,我统一用Deno脚本生成UTC时间字符串存入,避免跨时区机器日志时间错乱。另外,raw_data_json字段我限制最大长度为1MB,超长数据存到单独的data_blob表,用外键关联,防止单条记录过大影响查询性能。
3.3 Deno脚本健壮性:处理网络抖动、API限流、Excel乱码的三板斧
Deno脚本不是玩具,它要直面生产环境的脏数据。我总结出三个高频问题及解法:
问题1:电商API偶尔502,重试3次就成功,但默认fetch不重试
解法:封装robustFetch函数,带指数退避:
async function robustFetch(url: string, options: RequestInit = {}) { let lastError; for (let i = 0; i < 3; i++) { try { const res = await fetch(url, { ...options, signal: AbortSignal.timeout(8000) }); if (res.ok) return res; lastError = new Error(`HTTP ${res.status}`); } catch (e) { lastError = e; if (i < 2) await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); // 1s, 2s, 4s } } throw lastError; }问题2:Excel导出用ANSI编码,Deno默认UTF-8读取导致中文乱码
解法:用encoding库检测并转换:
import { decode } from "https://deno.land/std@0.208.0/encoding/utf8.ts"; import { TextDecoder } from "https://deno.land/std@0.208.0/encoding/text_decoder.ts"; // 先用binary读取,再用chardet检测编码 const bytes = await Deno.readFile("data.xls"); const encoding = detectEncoding(bytes); // 自己实现的简单检测逻辑 const decoder = new TextDecoder(encoding); const content = decoder.decode(bytes);问题3:API返回字段名大小写不一致(如productID和productId混用)
解法:在JSON解析后,统一转为小驼峰:
function normalizeKeys(obj: any): any { if (obj === null || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map(normalizeKeys); const normalized: Record<string, any> = {}; for (const [key, value] of Object.entries(obj)) { const normalizedKey = key.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); normalized[normalizedKey] = normalizeKeys(value); } return normalized; }实操心得:这些看似琐碎的细节,恰恰是RPA能否从“演示能跑”走向“生产可用”的分水岭。Deno的模块化和TypeScript类型系统,让我能把这些“脏活”封装成可复用的工具函数,而不是散落在各个流程里的重复if-else。
3.4 FreeRPA与Deno的进程通信:IPC不是魔法,是可控的管道
FreeRPA和Deno如何协作?很多人以为要搞WebSocket或HTTP API,其实大可不必。FreeRPA的Node.js运行时支持child_process,而Deno的Deno.run可以启动子进程。我采用最朴素的STDIN/STDOUT JSON管道:
- FreeRPA流程中,用“执行命令行”动作调用
deno run --allow-read --allow-write ./scripts/process_order.ts; process_order.ts从stdin读取JSON参数(如订单ID、配置路径),处理后将结果写到stdout;- FreeRPA捕获
stdout的JSON输出,继续后续流程。
// process_order.ts const decoder = new TextDecoder(); const encoder = new TextEncoder(); // 从stdin读取参数 const stdinBytes = await Deno.stdin.readAll(); const params = JSON.parse(decoder.decode(stdinBytes)); // 执行业务逻辑 const result = await handleOrder(params.orderId); // 写回stdout await Deno.stdout.write(encoder.encode(JSON.stringify(result)));这套通信机制的优势在于:零网络开销、零端口冲突、零序列化损耗。它不像HTTP那样要处理请求头、状态码、超时重试;也不像WebSocket那样要维护连接状态。就是纯粹的进程间数据流,稳定得像Unix哲学里的管道。
提示:为防Deno脚本崩溃导致FreeRPA卡死,我在FreeRPA的“执行命令行”动作里设置了
timeout=30000(30秒),超时自动终止子进程,并记录timeout状态到SQLite。这是比任何“优雅降级”都实在的容错。
3.5 环境可移植性:U盘即部署,三步完成新机器初始化
RPA最大的交付痛点,是“这台电脑能跑,换一台就不行”。根源往往是环境变量、路径硬编码、依赖版本不一致。我的目标是:把整个自动化套件打包成一个文件夹,复制到新Windows机器,双击一个bat文件,5分钟内全部就绪。
最终方案包含三个文件:
setup.bat:自动安装Deno(从官网下载zip,解压到./deno目录),设置DENO_DIR=./deno_cache,避免污染用户全局环境;config.json:所有路径、API密钥、数据库名都放这里,FreeRPA和Deno脚本统一读取;start.bat:启动FreeRPA主程序,并自动加载./flows/main.flow流程。
最关键的是路径处理。FreeRPA的“执行命令行”动作默认工作目录是安装目录,而我要它指向项目根目录。解法是在start.bat里用cd /d "%~dp0"切换到脚本所在目录,再启动FreeRPA:
@echo off cd /d "%~dp0" start "" "FreeRPA.exe" --flow "./flows/main.flow" --config "./config.json"Deno脚本里,所有文件路径都用Deno.cwd()拼接,确保相对路径始终正确:
const dbPath = join(Deno.cwd(), "data", "main.db"); const logPath = join(Deno.cwd(), "logs", `${new Date().toISOString().slice(0,10)}.log`);实操心得:这个方案让我成功在7台不同配置的Windows 10/11机器上一键部署,包括一台没有管理员权限的财务部笔记本。它不依赖注册表、不修改系统PATH、不安装全局服务,真正做到了“绿色免安装”。
4. 实操全流程:以“电商订单抓取”为例,从零到交付的完整链路
4.1 需求还原:不是“自动化”,而是“可信的数据搬运工”
业务方提的需求很朴素:“每天上午9点,把京东商家后台的昨日订单导出成Excel,发到财务邮箱”。听起来简单,但背后藏着三个隐形需求:
- 可信:财务要核对每一笔订单的金额、商品、收货地址,不能漏单、不能重复、不能错行;
- 可溯:如果某天订单数比平时少20%,得能立刻查出是京东接口没返回,还是我们脚本过滤错了;
- 可干预:运营偶尔要手动补抓某天的订单,不能只能等定时任务。
这意味着,这个RPA不能只是“模拟人工点几下”,而要成为一道数据质量守门员。它必须有能力:
- 校验京东API返回的订单总数与分页总数是否一致;
- 对比本地SQLite已存订单ID,自动去重;
- 当检测到异常(如单页返回0条),暂停流程并邮件告警;
- 提供Web界面,让运营输入日期范围,触发手动抓取。
4.2 架构设计:三层分离,各负其责
我把整个流程拆成三个独立模块,通过SQLite解耦:
| 模块 | 技术栈 | 职责 | 输出 |
|---|---|---|---|
| 采集层 | FreeRPA | 模拟登录、翻页、点击“导出Excel”按钮、下载文件 | 将Excel文件存到./downloads/20231001.xlsx |
| 解析层 | Deno | 读取Excel,校验字段完整性,转换为JSON,写入SQLiteorder_raw表 | 插入N条status='raw'记录 |
| 同步层 | Deno(独立定时任务) | 查询order_raw中status='raw'的记录,调用内部ERP API提交,成功后更新status='synced' | 更新SQLite记录,发送邮件通知 |
这样设计的好处是:采集失败不影响解析,解析失败不影响同步。任何一个环节出问题,都能在SQLite里看到明确的状态标记,而不是整个流程“黑屏”。
4.3 关键代码实录:解决真实世界的脏数据
步骤1:FreeRPA采集Excel(规避反爬)
京东商家后台有反爬机制,连续点击“下一页”会触发验证码。我的解法是:混合使用“显式等待”和“随机停顿”。
在FreeRPA流程里,我不用“等待元素出现”这个动作,而是插入一个“执行JavaScript”节点:
// 等待页面加载完成,且订单列表容器有内容 await new Promise(r => { const check = () => { const list = document.querySelector('.order-list'); if (list && list.children.length > 0) r(); else setTimeout(check, 500); }; check(); }); // 点击下一页前,随机停顿1-3秒 await new Promise(r => setTimeout(r, 1000 + Math.random() * 2000));然后,用FreeRPA的“截图”动作,把当前页面保存为./screenshots/page_${page}.png,和Excel一起存档。万一哪天运营说“你们漏了第5页”,我直接打开对应截图,就能确认是京东没返回,还是我们没点到。
步骤2:Deno解析Excel(处理乱码与空值)
京东导出的Excel,用SheetJS读取时,中文列名常是乱码。解法是:先用iconv-lite转码,再用SheetJS解析:
import { read, utils } from "https://cdn.skypack.dev/xlsx@0.18.5"; import { decode } from "https://deno.land/std@0.208.0/encoding/utf8.ts"; import { TextDecoder } from "https://deno.land/std@0.208.0/encoding/text_decoder.ts"; const bytes = await Deno.readFile("./downloads/20231001.xlsx"); // 京东Excel通常是GBK编码,先转UTF-8 const utf8Bytes = iconv.decode(bytes, 'gbk'); const workbook = read(utf8Bytes, { type: 'array' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const jsonData = utils.sheet_to_json(sheet, { header: 1 }); // 第一行是列名,转成对象键 const headers = jsonData[0].map((h: string) => h.trim()); const orders = jsonData.slice(1).map(row => { const obj: Record<string, any> = {}; headers.forEach((h, i) => obj[h] = row[i]); return obj; });对空值的处理更关键。京东的“买家留言”字段常为空,但Excel里显示为空字符串"",而我们的ERP要求是null。我写了一个清洗函数:
function cleanOrder(order: Record<string, any>): Record<string, any> { return { order_id: order['订单编号'] || null, amount: parseFloat(order['实付金额'] || '0'), buyer_note: order['买家留言'] === '' ? null : order['买家留言'], created_at: new Date(order['下单时间']).toISOString(), }; }步骤3:SQLite状态流转(保证数据一致性)
解析完后,不是直接INSERT,而是用事务包裹:
const db = new DB("./data/main.db"); db.query("BEGIN TRANSACTION"); try { const orders = parseExcel("./downloads/20231001.xlsx"); for (const order of orders) { db.query( "INSERT INTO order_raw (order_id, amount, buyer_note, created_at, status, raw_json) VALUES (?, ?, ?, ?, ?, ?)", [order.order_id, order.amount, order.buyer_note, order.created_at, 'raw', JSON.stringify(order)] ); } db.query("COMMIT"); } catch (e) { db.query("ROLLBACK"); throw e; }这样,如果中途某条INSERT失败(比如order_id重复),整个批次都会回滚,不会留下半截脏数据。
步骤4:Deno同步层(带幂等性与告警)
同步任务每天凌晨1点运行,核心逻辑是:
// 查询所有未同步的原始订单 const rawOrders = db.query("SELECT * FROM order_raw WHERE status = 'raw' ORDER BY created_at ASC"); for (const order of rawOrders) { try { // 调用ERP API,传入order对象 const res = await fetch("https://erp.internal/api/orders", { method: "POST", body: JSON.stringify(order), headers: { "Content-Type": "application/json" } }); if (res.ok) { // 成功,更新状态 db.query("UPDATE order_raw SET status = 'synced', synced_at = ? WHERE id = ?", [ new Date().toISOString(), order.id ]); } else { // 失败,记录错误,但不中断循环 db.query("UPDATE order_raw SET status = 'failed', error_message = ? WHERE id = ?", [ await res.text(), order.id ]); // 如果是网络错误,发邮件告警 if (res.status >= 500) sendAlertEmail(`ERP API error: ${res.status}`); } } catch (e) { db.query("UPDATE order_raw SET status = 'failed', error_message = ? WHERE id = ?", [ e.message, order.id ]); } }实操心得:这个循环里,我故意不
break,而是让所有订单都尝试一遍。因为ERP偶尔会503,但下一条可能就成功。如果遇到连续5条失败,才触发告警——这是用数据说话,而不是凭感觉。
4.4 交付物清单:不只是“能跑”,更是“能管、能查、能扩”
最终交付给业务方的,不是一个exe文件,而是一个结构清晰的文件夹:
jd-order-automation/ ├── setup.bat # 一键安装Deno ├── start.bat # 启动FreeRPA主流程 ├── config.json # 所有配置项(邮箱、API密钥、路径) ├── flows/ # FreeRPA流程文件 │ └── main.flow ├── scripts/ # Deno脚本 │ ├── parse_excel.ts │ ├── sync_to_erp.ts │ └── web_dashboard.ts ├── data/ # SQLite数据库 │ └── main.db ├── downloads/ # 下载的Excel文件 ├── screenshots/ # 页面截图存档 └── logs/ # 执行日志配套的,还有一份《运维手册》,里面写着:
- 如何查看昨日抓取了多少订单:
SELECT COUNT(*) FROM order_raw WHERE DATE(created_at) = '2023-10-01' AND status = 'synced' - 如何手动补抓:修改
config.json里的manual_date,双击start.bat; - 如何升级Deno:删掉
./deno文件夹,重新运行setup.bat。
这才是真正的交付——它把RPA从“黑盒工具”,变成了“透明系统”。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 FreeRPA常见问题速查表
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 流程卡在“等待元素”超过30秒 | 页面加载慢,或元素被动态遮罩层覆盖 | 1. 截图确认元素是否真在;2. 查看浏览器开发者工具,确认元素display和visibility属性 | 改用smartClick自定义识别器;或在等待前插入sleep(2000) |
| 截图内容是灰色背景,没有页面 | Electron渲染进程未就绪 | 1. 在流程开头加“等待页面加载完成”动作;2. 检查FreeRPA日志是否有Failed to load resource | 升级FreeRPA到最新版;或在webPreferences里启用nodeIntegration: true |
| 执行命令行报错“找不到deno” | PATH未生效,或Deno未安装 | 1. 在bat里echo %PATH%;2. 手动运行./deno/bin/deno --version | 在start.bat里用绝对路径调用./deno/bin/deno run |
| 导出Excel文件名含中文,FreeRPA读取失败 | Windows默认ANSI编码,文件系统API返回乱码 | 1. 用dir命令查看文件名;2. 在FreeRPA里用decodeURIComponent(escape())处理路径 | 统一用英文命名下载文件,如jd_orders_20231001.xlsx |
5.2 SQLite实战避坑指南
坑1:
datetime('now')在Windows下返回本地时间,Linux下返回UTC
→ 解法:所有时间字段,Deno脚本里用new Date().toISOString()生成,存入TEXT类型,查询时用datetime()函数转换。坑2:
LIKE '%关键词%'在中文搜索时慢得像蜗牛
→ 解法:为常用搜索字段建FTS5全文索引:CREATE VIRTUAL TABLE order_fts USING fts5(order_id, buyer_name, buyer_note); INSERT INTO order_fts SELECT order_id, buyer_name, buyer_note FROM order_raw;坑3:大量INSERT导致数据库文件暴涨,且无法自动收缩
→ 解法:定期执行VACUUM,但要在FreeRPA流程外做(避免阻塞):deno run --allow-read --allow-write vacuum_db.tsvacuum_db.ts里:await Deno.run({ cmd: ["sqlite3", "main.db", "VACUUM"] }).status();
5.3 Deno脚本调试黄金法则
法则1:永远用
Deno.args代替硬编码
不要写const date = "20231001";,而要写const date = Deno.args[0] || new Date().toISOString().slice(0,10);。这样既能手动测试deno run script.ts 20231001,又能定时任务自动运行。法则2:错误堆栈要包含上下文
不要只throw new Error("Parse failed"),而要:throw new Error(`Parse failed for file ${filePath}: ${e.message}`);这样日志里一眼就知道是哪个文件出的问题。
法则3:大文件处理用流式读取
Excel文件超过10MB时,Deno.readFile会吃光内存。改用:const file = await Deno.open("./big.xlsx"); const buffer = new Uint8Array(65536); let total = 0; while (true) { const n = await file.read(buffer); if (n === null) break; total += n; } file.close();
5.4 真实故障复盘:一次“订单丢失”的72小时
现象:10月5日,财务反馈京东订单比往日少37单。
排查过程:
- 查SQLite:
SELECT COUNT(*) FROM order_raw WHERE DATE(created_at) = '2023-10-05'→ 返回123,但京东后台显示160; - 查
downloads/目录:20231005.xlsx存在,大小1.2MB; - 手动运行
deno run scripts/parse_excel.ts 20231005.xlsx→ 报错RangeError: Invalid array buffer length; - 用
DB Browser for SQLite打开main.db,查order_raw表,发现error_message字段有TypeError: Cannot read property 'length' of undefined; - 定位到
parse_excel.ts第42行:const sheet = workbook.Sheets[workbook.SheetNames[0]];——SheetNames数组为空!
根因:京东当天导出的Excel,第一个sheet被命名为订单列表(备份),而脚本只认SheetNames[0]。
修复:改用workbook.SheetNames.find(name => name.includes('订单')),并加兜底逻辑