作者:亿公子(税务合规 / 大额居间费方向自媒体博主) 适用场景:浏览器自动化、RPA 批处理、财务票据管理
我是做税务自媒体的,公司名下记账卡每个月都有几十笔高速通行费要开票。手动登录票根、逐月点选、勾行程、填邮箱……一个月一次,又碎又烦,还容易漏月。
后来我用 WorkBuddy 的浏览器自动化能力(agent-browser + Playwright)把整条开票链路做成了可复跑的脚本:一键接管已登录的浏览器,逐月扫描"可开票"行程,自动勾选、填邮箱、提交,最后只差"确认发布"式的一个真人点按。
这篇把核心思路和可直接抄的代码拆给你,重点讲三个真实踩坑:Vue 单页应用的路由坑、Element Plus 月份选择器、以及最要命的"月份切换异步竞态"。
agent-browser 开可见浏览器(用户扫码登录)
↓
Playwright 用 CDP 接管同一浏览器会话
↓
菜单「我要开票」→ 点卡片 → 进入月份表格
↓
逐月设置查询月份 → 读取"可开票"行程 → 勾选
↓
底部「确认」→ 核对页填邮箱 → 「确认开票」
↓
成功页「继续开票」→ 循环下一个月核心原则:让 AI 跑机械循环,人只在登录和最终提交时点一下。
# 1. 安装浏览器自动化 CLI(全局)
npm install -g agent-browser
# 2. 下载 Chromium(较大,建议后台跑)
agent-browser install
# 3. 本机准备 Python 环境(Playwright 接管用)
python -m venv venv
venv\Scripts\pip install playwright
playwright install chromiumWindows 关键:起浏览器必须带
--no-sandbox和--headed,否则要么连不上,要么弹不出可见窗口让你扫码。agent-browser open "https://mag.96533.com:8243/personService/index.html" --headed --args "--no-sandbox"
agent-browser 本质也是 Chromium,我们可以用 Chrome DevTools Protocol(CDP)让 Playwright 直接连上同一个会话,这样用户在窗口里扫的码、登的录,脚本都能接着用。
先找到浏览器调试端口:
# Windows 查监听端口与进程
powershell -Command "Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -gt 9000 } | Select-Object LocalPort, OwningProcess"连上它(端口号以你实际为准,常见 49xxx):
import asyncio
from playwright.async_api import async_playwright
CDP = "http://127.0.0.1:49283" # 换成你查到的端口
async def main():
async with async_playwright() as p:
b = await p.chromium.connect_over_cdp(CDP)
pg = b.contexts[0].pages[0] # 复用已登录的标签页
await pg.bring_to_front()
print("当前页:", pg.url)
# 后面所有操作都基于 pg
await b.close()
asyncio.run(main())粤通卡的"电子发票"菜单其实内嵌了票根子系统,而且 CheckBillingInfo 这个原始路由在我们自动化环境下会报 "undefined" is not valid JSON,直接挂掉。
正确入口是:菜单「我要开票」→ 点卡片 .etcItem → InvoiceDetail 页(带月份选择器)。如果路由 push 不灵,就用菜单点击:
# 进「我要开票」并点卡片
await pg.evaluate("""() => {
const el=[...document.querySelectorAll('*')].find(e=>(e.innerText||'').trim()==='我要开票');
if(el) el.click();
}""")
await pg.wait_for_timeout(1500)
await pg.evaluate("""() => {
const card=[...document.querySelectorAll('.etcItem')].find(e=>e.innerText.includes('粤B')); // 用你的车牌关键字
if(card) card.click();
}""")
await pg.wait_for_timeout(2000)Element Plus 的月份选择器面板里,月份是中文数字(一月…十二月),年份用箭头按钮切。更重要:切月后表格是异步刷新的,Playwright 有时读到的是上一月的旧数据——直接开票就会开错月。
解法:每次切月后,校验每一行的"交易日期"是否真的落在所选年月,不符就等一会儿重试。
import asyncio
MONTH_CN = ["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]
async def set_month(pg, year, month):
await pg.click('.el-date-editor--month', timeout=5000)
await pg.wait_for_timeout(400)
# 切到目标年份
for _ in range(6):
yr = await read_year_label(pg) # 读 .el-date-picker__header-label 里的 4 位数字
if yr == year: break
arrow = '前一年' if yr > year else '后一年'
await pg.click(f'button[aria-label="{arrow}"]')
await pg.wait_for_timeout(300)
await pg.locator('.el-month-table .cell', has_text=MONTH_CN[month-1]).first.click()
await pg.wait_for_timeout(1200)
async def read_rows_validated(pg, year, month):
rows = await pg.evaluate("""() => [...document.querySelectorAll('.el-table__body-wrapper tbody tr')]
.map(tr => [...tr.querySelectorAll('td')].map(td => td.innerText.replace(/\\s+/g,' ').trim()))""")
valid = [r for r in rows if len(r) >= 6 and r[3].startswith(f'{year}-{month:02d}')]
return valid # 只返回日期真正匹配的行的,避免开错月经验值:切月后等 1.2 秒 + 日期校验,基本能 100% 避开竞态。
勾选行 → 底部「确认」→ 核对页填邮箱 → 「确认开票」→ 成功页「继续开票」。
async def issue_month(pg, year, month):
await set_month(pg, year, month)
rows = await read_rows_validated(pg, year, month)
if not rows:
return f"{year}-{month:02d}: 无未开票行程"
# 逐行勾选(点 label.el-checkbox 才触发 Vue)
for _ in range(3):
boxes = await pg.locator('.el-table__body-wrapper tbody tr label.el-checkbox').all()
for bx in boxes: await bx.click()
await pg.wait_for_timeout(400)
break
# 底部确认
await pg.evaluate("""() => {
const b=[...document.querySelectorAll('div.confirmBtn')].find(e=>e.innerText.includes('确认'));
if(b) b.click();
}""")
await pg.wait_for_timeout(1500)
# 核对页填邮箱(必填!否则提交无反应)
await pg.fill('input[placeholder="请输入接收发票的电子邮箱"]', 'you@example.com')
await pg.evaluate("""() => {
const b=[...document.querySelectorAll('div.confirmBtn')].find(e=>e.innerText.includes('确认开票'));
if(b) b.click();
}""")
await pg.wait_for_timeout(2000)
return f"{year}-{month:02d}: 已提交 {len(rows)} 笔"主循环就是遍历你想开的月份列表,逐月调用 issue_month,把结果写进 JSON 留档。
坑 | 现象 | 解决 |
|---|---|---|
浏览器起不来 | 连接失败 os error |
|
登录态丢失 | 票根子系统刷新后"请重新登录" | 票根是独立子系统,需重新短信验证码登录 |
路由报错 |
| 改用「我要开票」→ |
开错月份 | 表格显示上一月数据 | 切月后按行日期校验(第六节) |
提交无反应 | 填完点「确认开票」没动静 | 邮箱框是必填,先填再提交 |
程序化点击无效 |
| 最终"确认"步骤留给真人点 |
ETC 发票这类"每月一次、步骤固定、界面是 Vue 老 SPA"的活儿,特别适合用 WorkBuddy 的浏览器自动化来承包。整套打法可以复用到任何类似的票据/报表后台:
div 的自定义点击,认准文案去 querySelectorAll 找。把循环脚本化后,我每个月只要点一次"确认开票",剩下的 WorkBuddy 全包了。欢迎在评论区聊聊你用 WorkBuddy 自动化的财务场景 👍
声明:文中卡号、手机号、邮箱均为占位示例,实操请替换为你的真实信息;敏感操作请确保本人授权。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。