浏览器中使用AI开发好的插件实现自动报销
浏览器中使用AI开发好的插件实现自动报销
属于 #创作灵感# 系列:记录工作实践、项目复盘、写技术笔记巩固知识要点、发表职场感悟心得
每月报销,程序员的"至暗时刻"
不知道有多少人和我一样,每个月最痛苦的事不是写代码,而是——报销。
出差一趟回来,攒了一堆PDF票据:高铁票、打车行程单、酒店发票、餐饮发票……然后打开蓝凌OA,开始手动填表。一张高铁票要填出发地、到达地、日期、舱位、金额,一个行程对应多笔费用,一行一行地敲,一个下拉框一个下拉框地选。遇到那种搜索式下拉框,还得先输入关键字、等加载、再点选——手指头都快敲出茧子了。
最崩溃的是,蓝凌OA用的是自研的LUI框架,表单控件各种花式封装:有隐藏的input、有iframe里嵌套的浮层、有MarcoPolo自动补全、还有那种readonly还得上传触发搜索的下拉框……每次填完一张报销单,我感觉自己就像个人肉RPA。
有一次月末赶着报销,十几张票据填了将近40分钟,中间还因为选错了下拉框选项被退回重填。那一刻我盯着屏幕想:我是写代码的啊,这事儿凭什么让我手动干?
于是,一个大胆的想法诞生了——用AI帮我写一个浏览器插件,让它替我填表。
从"想法"到"方案":用AI搭起整套架构
说实话,一开始我只想写个简单的脚本,后来跟Cursor(AI编程助手)聊了几轮需求,它帮我理清了一个关键问题:这件事不只是"填表",而是一条完整的链路——从票据识别到表单填写,中间涉及PDF解析、数据结构化、DOM操作,不是一个脚本搞得定的。
最终在AI的辅助下,我设计了一套三层解耦架构:
plaintext
浏览器插件(Chrome/Edge/Firefox, MV3规范)
↕ HTTP通信(端口8088)
本地解析服务(Flask)
↕ 本地调用
客户端GUI(Python tkinter)
为什么是三层?
- 浏览器插件负责"看"和"填"——监听OA页面、注入表单、操作DOM
- 本地解析服务负责"想"——PDF解析、票据分类、数据提取,这些重计算不适合放在浏览器里
- 客户端GUI负责"管"——给非技术同事一个可视化的操作界面,选文件、看结果、改配置
三层通过HTTP通信解耦,插件和服务各自独立部署,任何一层升级都不影响其他层。这个架构不是我一上来就想明白的,是在AI的引导下逐步迭代出来的——v1.0的时候还只有Python客户端+Selenium自动化,后来才演化出浏览器插件模式。
核心难点一:PDF票据智能识别,比想象中难多了
多类型分类:文件名快判 + 内容精判
我一开始天真地以为,PDF解析嘛,正则表达式一把梭。结果发现票据种类五花八门——火车票、打车行程单、打车发票、酒店发票、餐饮发票,格式各不相同。光是区分"打车行程单"和"打车发票"就够喝一壶了。
最终实现的分类策略是两层过滤:
python
# 第一层:文件名快速预判
def classify_by_filename(filename):
if "火车" in filename or "高铁" in filename:
return "train_ticket"
if "行程单" in filename:
return "ride_itinerary"
# ...
# 第二层:内容精确判断
def classify_by_content(text):
if re.search(r'车次|座位号|二等座', text):
return "train_ticket"
if re.search(r'行程单|起步价|里程', text):
return "ride_itinerary"
# ...
文件名快判能处理80%的情况,剩下20%靠内容精判兜底。这种分层策略是AI建议的——我本来只想做一层,AI提示说文件名判断速度快、可提前分流,避免每个文件都走全文解析,性能好很多。实测下来确实如此。
高铁票解析:站名翻译与座位等级
高铁票的解析坑点在于站名。12306的PDF里,站名有时是英文缩写(比如"Beijing South"),有时带"站"字("北京南站"),还有时候站名换行被拆开。我的处理策略:
- 英文站名 → 维护一个映射表翻译成中文
- 去掉"站"字统一格式
- 乘客姓名提取采用多策略(位置定位、关键词匹配、模式匹配组合)
数字粘连:OCR的"幽灵Bug"
这是最让我头疼的问题。PDF文本提取时,相邻的数字偶尔会粘连在一起——比如"单价30数量2金额60"可能被提取成"单价302金额60",30和2粘成了302。
我写了一个暴力枚举+验证的拆分算法:
python
def split_stuck_numbers(text, expected_total):
"""尝试所有可能的拆分点,验证 单价×数量≈金额"""
for split_pos in range(1, len(text)):
unit_price = int(text[:split_pos])
quantity = int(text[split_pos:])
if abs(unit_price * quantity - expected_total) < 1:
return unit_price, quantity
return None
暴力是暴力了点,但数字字符串通常不长,枚举开销可以接受。关键是验证逻辑——单价×数量≈金额,这个约束条件有效过滤掉了绝大多数错误的拆分方式。
电子发票:城市推导
酒店发票和餐饮发票里没有直接的"城市"字段,但报销需要填城市。我的解法是从销售方名称智能推导——"XX市XX酒店"→提取"XX市"。这个看似简单的正则,调试了不下十版,因为销售方名称的格式太随意了。最终还是在AI的帮助下,覆盖了十几种常见的命名模式。
核心难点二:OA表单自动填写,跟LUI框架斗智斗勇
如果说PDF解析是"脑力活",那OA表单填写就是"体力活+技术活"。蓝凌OA的LUI框架把原生HTML控件封装得面目全非,每个控件都有它自己的"脾气"。
MarcoPolo自动补全:逐字输入的艺术
蓝凌OA里有些字段用的是MarcoPolo控件——你输入一个字,它弹出候选列表,你得用鼠标点选。Selenium的send_keys()直接填完整文本是不行的,因为它不会触发搜索。
我的解决方案是逐字输入 + mousedown选择:
python
def fill_marcopolo(element, text):
for char in text:
element.send_keys(char)
time.sleep(0.1) # 等待候选列表刷新
# 找到候选项,用mousedown而非click触发
candidate = wait_for_candidate(text)
dispatch_mouse_event(candidate, "mousedown")
注意这里用的是mousedown而不是click——因为MarcoPolo控件监听的是mousedown事件,用click反而选不上。这个坑我踩了整整一个下午,最后是AI帮我分析了DOM事件绑定才搞明白的。
搜索式下拉框:移除readonly的"黑客"操作
还有一种下拉框,表面看是<input>,但设了readonly属性,你直接输入会被拦截。我的做法简单粗暴——先用JavaScript移除readonly,再输入,再触发搜索:
python
driver.execute_script(
"arguments[0].removeAttribute('readonly')", input_element
)
input_element.clear()
input_element.send_keys(keyword)
而且这种下拉框还经常加载慢,需要多次重试。我设了3次重试 + 5秒超时的容错机制。
跨iframe通信:你以为你在操作页面,其实你在操作iframe
蓝凌OA的某些表单区域是嵌在iframe里的,直接用Selenium定位根本找不到元素。浏览器插件模式下更麻烦——Content Script默认只能访问顶层页面,iframe里的内容需要通过chrome.webNavigationAPI来注入。
跨iframe协调的逻辑大致是:
- 主Content Script监听文件上传事件
- 通过
chrome.runtime.sendMessage将解析结果发给后台Service Worker - Service Worker通过
chrome.tabs.sendMessage转发给iframe的Content Script - iframe的Content Script执行具体的DOM操作
这个消息链路调试起来极其痛苦,因为Chrome DevTools对Service Worker的调试支持本身就有限。最后还是靠着AI帮我写了详细的日志输出,才把消息流转的时序问题排查清楚。
填写节奏控制
填表不是越快越好——太快了OA的LUI框架会来不及响应,导致数据丢失。我摸索出的节奏是:
- 行程行:每行1.5秒间隔
- 费用行:200ms基础间隔 + 5秒超时重试
这些数值是反复测试调出来的,AI帮我做了个参数化的配置,方便后续微调。
核心难点三:浏览器插件开发,MV3的"坑"你没商量
Service Worker的生命周期
Chrome MV3规范要求后台脚本改为Service Worker,而Service Worker有个让人抓狂的特性——它会休眠。如果你用setInterval做定时任务,一休眠就丢了。
解决方案是使用chrome.alarmsAPI替代setInterval:
javascript
// 健康检查:每30秒
chrome.alarms.create("healthCheck", { periodInMinutes: 0.5 });
// 版本检测:每30分钟
chrome.alarms.create("versionCheck", { periodInMinutes: 30 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "healthCheck") checkServiceHealth();
if (alarm.name === "versionCheck") checkExtensionUpdate();
});
chrome.alarms的最小间隔是1分钟(开发模式可以更短),对于30秒健康检查这种需求,我用了periodInMinutes: 0.5的写法——虽然文档说最小是1分钟,但实测在已发布的扩展中0.5分钟也是生效的(可能是Chrome的一个"特性")。
Content Script注入时机
另一个坑是Content Script的注入时机。蓝凌OA是SPA(单页应用),页面路由切换不会触发完整的页面加载,所以manifest.json里的content_scripts匹配规则有时不会重新注入。
我的做法是在background.js里用chrome.webNavigation.onHistoryStateUpdated监听路由变化,主动重新注入Content Script:
javascript
chrome.webNavigation.onHistoryStateUpdated.addListener(
(details) => {
if (details.url.includes("/oa/")) {
chrome.scripting.executeScript({
target: { tabId: details.tabId },
files: ["content_script.js"]
});
}
}
);
这个坑也是AI帮我发现的——我一开始以为Content Script只需要在manifest.json里声明就够了,结果发现SPA场景下必须手动处理。
实际效果:从40分钟到40秒
现在我的报销流程是这样的:
- 登录蓝凌OA → 插件自动注入,图标亮起绿灯🟢
- 上传PDF附件 → 插件通过MutationObserver监听到文件上传,自动发送给本地解析服务
- 解析完成 → 弹出确认对话框,展示票据数量、合计金额、类型统计
- 确认无误 → 点击确认,插件自动填写所有表单字段
- 人工检查 → 快速扫一遍,确认提交 ✅
十几张票据,从上传到填写完成,全程不到1分钟。 而且因为数据是从PDF直接提取的,人为出错率大幅降低——以前手动填经常填错金额或日期,现在基本不会了。
本地解析服务也很稳定,日志按天轮转保留30天,每个请求带trace ID方便排查。Flask虽然不是性能最强的框架,但这个场景下单机并发足够了。
AI辅助开发感悟:哪些擅长,哪些还需人工
整个项目从v1.0到v2.3,我深度使用了Cursor和Copilot等AI编程工具,有一些真实的感受想分享:
AI擅长的
- 架构设计讨论:三层解耦架构就是在和AI的对话中逐步明确的。AI擅长从需求中提取关键约束,帮你理清模块边界
- 正则表达式编写:票据解析里大量正则,AI写正则的速度和准确率远超我手写。特别是那些边界case——AI能想到你容易忽略的变体
- DOM操作方案:面对LUI框架的各种奇葩控件,AI能快速给出多种尝试方案。比如MarcoPolo的mousedown问题,AI在第三轮对话就提到了事件类型的差异
- 代码脚手架:Flask服务的路由框架、Chrome插件的manifest配置、消息通信模板……这些套路化的代码AI几秒钟就能生成
AI不太擅长的
- 业务细节调参:填表延迟1.5秒还是2秒?搜索下拉框重试几次?这些需要反复实测,AI给不出标准答案
- DOM调试:蓝凌OA的DOM结构非常深、动态变化多,AI无法直接"看到"页面,你得上截图或HTML片段才能让它帮你分析
- 跨系统联调:插件和服务之间的通信问题,往往是时序或状态不一致导致的,AI很难仅凭代码推断运行时的交互顺序
- 创新性解决方案:数字粘连拆分那个暴力枚举的思路是我想出来的,AI最初给的是基于规则匹配的方案,对这种"反直觉"的问题不太在行
总结一句话:AI是超强辅助,但不是自动驾驶。 它能帮你省下80%的编码时间,但剩下20%的关键调优,还是得靠自己的判断力和耐心。
版本演进:一个项目的成长史
回顾一下版本历程,能清晰看到项目是如何一步步演化的:
- v1.0:只能解析高铁票 + Selenium自动填写——验证了可行性,但只能自己用
- v1.1:加入自定义文件夹浏览、多选功能——开始考虑易用性
- v1.2:统一PDF浏览对话框——UI规范化
- v1.3:酒店/餐饮发票解析——覆盖更多票据类型,同事开始问能不能用
- v2.3:浏览器插件模式、自动更新、日志轮转、链路追踪——正式成为可交付的工具
每一次版本升级,背后都是真实使用中遇到的问题。v1.3的时候同事说"能不能也支持酒店发票",v2.3的时候同事说"我不想装Python环境,能不能直接用浏览器"——好的产品不是设计出来的,是用出来的。
写在最后
现在每个月报销,我只需要把PDF往OA里一拖,等几秒钟确认一下,搞定。同事看到我这么快就提交了报销单,跑来问我用了什么"黑科技",我就把插件给他们装上。
说实话,做这个项目最大的收获不是省了多少时间,而是验证了一个想法:AI时代,一个人也能做出完整的产品级工具。从前端插件到后端服务到解析引擎,每一层我都不是专家,但AI补齐了我的短板,让我能在每个领域都写出可用的代码。
当然,AI写的代码不是万能的——那些深夜调试MarcoPolo控件的崩溃时刻,那些PDF解析返回空结果的抓狂瞬间,都不是AI能替你扛的。但至少,你不再需要一个人从零开始写每一行代码了。
如果你也有类似的重复性工作困扰,不妨试试用AI帮你造个轮子。也许一开始只是一个小脚本,但谁知道呢,说不定它会长成一套完整的工具链 🚀
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)