网页重复性操作流程封装
一句话定位
把「人肉重复点一遍网页」变成「一条命令跑完,且出问题能被诊断」。
本 skill 是方法论 + 可复用资产,不绑定任何具体站点;落地案例见 docx-to-help-center-upload。
0. 存放位置 / 前置条件 / 调用方式
存放位置:~/.workbuddy/skills/web-flow-automation/(用户级 → 所有项目可用)。
无需注册:技能加载器按目录扫描,目录名必须与 frontmatter 的 name 完全一致(kebab-case)。
若要团队共享,再复制一份到 <项目根>/.workbuddy/skills/(项目级)。
运行环境:
| 资产 | 依赖 | 说明 |
|---|---|---|
scripts/probe_page.py |
Python 3.9+ 且已装 playwright |
★ 本 skill 唯一有外部依赖的资产。缺它报 ModuleNotFoundError: playwright,别误判成 skill 损坏 |
scripts/scan_required_fields.js |
无 | 是一段待注入页面的 JS 源码,在 DevTools 控制台或 page.evaluate 里跑,不需要 Node 环境 |
references/*.md |
无 | 纯文档 |
调用方式:
SK="$HOME/.workbuddy/skills/web-flow-automation"
# 侦察:接管已开的调试浏览器(推荐,不抢窗口控制权)
python "$SK/scripts/probe_page.py" --attach --port 9222 --url "<目标URL>"
python "$SK/scripts/probe_page.py" --attach --dump-form # 附导出表单字段 + 必填标记
python "$SK/scripts/probe_page.py" --attach --json out.json
# 必填扫描 JS:两条路都行
# a) 浏览器 DevTools 控制台直接粘贴执行
# b) Playwright:page.evaluate(Path("scan_required_fields.js").read_text())
作为 skill 被调用:正常描述场景即可(如"每次都要手工点一遍这个表单"),
我会按 description 命中并加载;也可显式指定 @skill:web-flow-automation。
1. 适用场景
该用:
- 同一条网页流程要反复走(每周/每天/每份文档都要来一遍)
- 流程有"操作量大但规则固定"的部分(几十段文字 + 几十张图逐张插入)
- 目标站点没有开放 API,或拿不到写接口的授权
- 需要保留人工环节(验证码、业务判断、最终提交),但把体力活去掉
不该用(先想清楚,否则白做):
- 流程只跑一次 → 直接手工,写脚本的收益是负的
- 站点有官方 API / 批量导入 → 优先走接口,比点网页稳一个数量级
- 流程的"判断"部分占大头(不是"操作量大")→ 自动化的收益有限
判定口诀:"操作量大 + 规则固定 + 要反复跑"三条同时成立才值得封装。
2. 核心步骤
七步,顺序不能换。第 0 步跳过是本类任务翻车的第一大原因。
步骤 0 — 先侦察,再写选择器(别猜)
对着页面凭肉眼/凭想象写选择器,是这类任务最主要的返工来源。
必须先跑侦察脚本,把候选元素的实际 DOM 结构、contentEditable 状态、匹配数量打出来。
- 资产:
scripts/probe_page.py(通用侦察骨架)、references/probe-recipes.md(配方) - 两条铁律:
1. 候选选择器必须核对匹配数量。document.querySelector('.ql-editor')在类名复用的页面上
会命中外壳(contenteditable=false,只包 toolbar),写进去看起来"能输入"其实全错。
同一类名在页面上出现 2 次以上,就要用父子限定(.ql-container .ql-editor)收窄。
2. 富文本/可输入元素要同时看contentEditable和子元素结构,不能因为"能点进去"就认为选对了。
步骤 1 — 分层:站点细节全部外置
代码里不许出现任何站点特有的字符串(URL、选择器、字段名)。
全部落进一份配置(config.yaml 或等价物),代码只读配置。
- 收益:换站点时改配置不改代码;配置能进版本管理、能被非开发者改
- 配套:写一个
discover子命令,探测后台选择器并回填配置(只填空字段,不覆盖用户已填内容 —— 回写前先备份) - 配置至少要能表达:目标页 URL、编辑器根选择器、图片插入方式、保存按钮、成功提示、登录探测页
步骤 2 — 浏览器生命周期:默认交还用户,且必须保活
这一段是"内容丢不丢"的生死线。 四种模式:
| 模式 | 谁开窗口 | 脚本结束时 | 用途 |
|---|---|---|---|
auto(出厂值) |
探测调试端口(默认 9222)有活的就接管,没有才自己拉起 | 只断开,不关窗口 | 日常 |
attach |
只接管,绝不开;找不到直接报错(报错里带开启命令原文) | 什么都不做 | 用户要完全掌控 |
cdp |
程序自己拉起浏览器 | 只断开,不关窗口 | 明确要程序开 |
managed |
Playwright 托管 | 会关窗口 | 仅无头测试 |
必须记住的因果链(真实事故):
launch_persistent_context 托管时,context 就是 browser
↓
context.close() ≠ "断开连接但保留进程",而是关掉整个窗口
↓
而"填完不点保存"是常态(其他必填字段还没填,点了也被拦)
↓
内容只存在 DOM 里 → 窗口一关 → 内容全丢
三条硬约束:
stop()里keep_browser_open=true时只pw.stop(),绝不调ctx.close()/browser.close();
attach模式什么都不做(第一条判断就 return)- 程序自己拉起浏览器时,
subprocess.Popen必须带
CREATE_BREAKAWAY_FROM_JOB | DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
—— 否则企业机/沙箱的 Job Object 会在父进程退出时连带杀掉浏览器 - 启动参数必须有
--remote-allow-origins=*(Chromium 111+ 的 DevTools Origin 校验,
不带则connect_over_cdp握手失败)
验证手段:必须跨进程数浏览器进程数(同进程内看不到真相)。
脚本收尾后进程数不成 0 才算保活成功。
换浏览器只改配置不改脚本(runtime.browser: edge/chrome/auto,另留 browser_path 最高优先)。
指定了引擎但本机没装 → 大声告警并退回 auto,绝不静默换成另一个浏览器(静默替换会让后续排查全被带偏)。
候选路径要按引擎分组:Edge 在企业机上常装在 Program Files (x86)。
步骤 3 — 人机分工 + 唯一等待点
不要试图全自动。 把"程序填不了的"显式交给人工,且只留一个等待点:
开浏览器 → 停在目标页 → 【提示用户:登录 + 填人工字段 + 填完回控制台按回车】 → 接管灌入 → 守在页面上等用户点保存
- 多个等待点会互相打架,用户不知道现在该按哪个回车
- 人工负责:验证码、业务判断、其他必填字段、最终提交
- 程序负责:体力活(逐段文字 + 逐张图片,保序)
- 取消路径必须走独立返回码(如
CANCELLED = 130),
绝不能 return 0 —— 返回 0 会被上层当成"干完了、内容已写进页面",就是谎报成功
步骤 4 — 写入 + 同步外层框架模型
内容写进 DOM ≠ 外层框架(Vue/React)的表单模型收到变更。
某些组件只在 input/change/blur 时才同步内部状态 → 出现「肉眼看到内容在、点保存却被拦」。
- 写完做一次「敲一个空格 + 退格」,再 dispatch
input/change/blur - 判定"写入成功"的通用信号:编辑器内
<img>数量 +1(对各家富文本都通用) - 图片走"点按钮弹系统文件对话框"型编辑器时,用
page.expect_file_chooser()接住再set_files(),
比塞input[type=file]可靠 - 确定性验证:读回编辑器块序列与源内容逐块比对;再把焦点移出编辑器再读一次
(未同步内部模型的编辑器会在 blur 时丢内容,这是"能不能安全保存"的关键判据)
步骤 5 — 判定"保存成功":三态 + 可见性
两个常见错误:
- 按"元素存在"判成功 —— 很多后台把提示元素预置在 DOM 里平时
display:none,
按querySelector判存在会一上来就误判成已保存。
→ 必须用getComputedStyle+getBoundingClientRect确认可见且有尺寸。 - 用一个布尔值表示结果 —— 把"没等到"和"确实失败"混成一个
False,报告就没法诚实。
正确:三态 confirmed / unknown(没等到,超时或被打断)/ not_waited。
报告里如实写「未确认保存」。
另一个坑:这类提示(Element UI message 等)通常 3 秒自动消失,1 秒轮询可能整段错过。
→ 页面里装 MutationObserver,出现过就永久记标记,轮询改为读标记。
步骤 6 — 兜底自证:把"为什么存不上"提前讲清楚
用户报「点保存没反应」时,先确认点击前的先决条件,再看有没有静默拦截。
不要一上来就怀疑自己的自动化逻辑(更不要先怀疑浏览器)。
真实事故链条:程序其实灌入成功了,但其他必填字段没填 →
前端校验根本不发保存请求,只在页面顶部闪一条提示,3 秒后消失 →
用户只看到「没反应」→ 结论成了「这个界面存不了」。
所以必须做三件事:
a. 灌入后立刻做必填项体检,把「还差哪几项」列出来。
★ 识别必填不能只认 .el-form-item.is-required —— 真实站点(Element UI)的必填星号
是 CSS ::before { content:"*" } 画出来的,innerText 里没有 *,
is-required 类也不一定加。只认这个类会一项都认不出来、整个体检静默失效。
→ 四条线索取或(完整实现见 scripts/scan_required_fields.js):
| 线索 | 取法 |
|---|---|
is-required 类 |
el.classList.contains('is-required') |
标签文字含 * |
label 的 textContent |
| CSS 画的星号 | getComputedStyle(labelEl,'::before').content 非空且含 *(::after 同样查) |
控件 required 属性 |
el.required |
三条配套硬约束:
- 不许静默兜底:一个必填标记都没认出来但确实有空字段 → 列出所有空字段并明说"没识别出必填标记"。宁可列多,不能一声不吭。
- 标红不能自污染:角标挂在 form-item 上(不要塞 label);读标签前先剥掉自己打的角标(克隆节点取
textContent);每次扫描前先清理旧标记。(第一版塞进 label → 二次扫描把标签读成「问题必填未填」→ 标红一个都不中) - 判定"空"按类型分支:
input/textarea看value.trim();contenteditable看文本 + 有无img;级联/多选看已选标签数。富文本只认ql-blank会漏(有图无字时不算空)。
b. 保存诊断:页面里挂钩子记 ① 提示文案 ② 点过的按钮 ③ fetch/XHR 请求与响应(滤掉静态资源)。
只取最后一次点保存之后的记录下结论:
| 观察到 | 结论 |
|---|---|
| 有请求 + 响应成功 | 保存成功 |
| 有请求 + 响应报错 | 直接打印服务端返回 |
| 一条业务请求都没有 | 被前端校验拦下 → 必填没填全或格式不合规 |
落盘 logs/save_trace_*.json 供事后翻。
c. 页面标红 + 闭环:缺的必填项在页面上框红 + 顶部弹一份清单(用户不用回头翻日志,红在哪补哪);
保存成功后自动清除。并做闭环:新提示命中 参数为空/请填写/不能为空 等词
→ 当场重新体检 + 标红。
★ 取最后一条命中的提示(hit[-1])—— 页面上常有上一轮遗留的旧提示,取第一条会把上轮的原因当成本次的。
步骤 7 — 留档、复位、断点续传
- 每次灌入后把内容落盘(
.json+.html)到草稿目录 —— 页面万一被关掉/刷新,内容还在磁盘上 - 目标页已经打开时不要重新
goto:重新加载等于刷新,会冲掉用户已手填的字段和上一轮内容。
用"路由归一化比较"(忽略 query 与末尾斜杠)判断"是否已在该页" - 进度落盘,重跑时自动跳过已完成项;提供
status(查进度)/reset(清进度)/--force(强制重发)
3. 输入与输出
| 内容 | |
|---|---|
| 输入 | ① 目标流程的可访问 URL + 一个能登录的账号(验证码等人工环节保留)<br>② 要灌的源数据(本地文档 / Excel / 结构化清单)<br>③ 站点配置(跑一次 discover --write 生成,或手填选择器) |
| 中间产物 | work/ 草稿与图片缓存 · logs/ 运行日志 + save_trace_*.json · 进度文件 · 浏览器用户目录(登录态可复用,属于敏感数据) |
| 输出 | ① 页面上填好的内容(最终由用户点保存落库)<br>② 可读的运行报告:文字块数 / 图片成功数 / 耗时 / 保存三态 / 还差哪几项必填<br>③ 草稿留档(.json + .html)<br>④ 诊断产物(必填清单 + 保存请求追踪) |
收尾报告必须诚实:内容没落库就写"未确认保存",不要因为有草稿就说成功。
4. 复用方式
4.1 换站点只要改三处
- 跑一次
discover --write重新探测选择器(必要时手调editor.kind/image.strategy) - 改目标页 URL / 保存按钮 / 成功提示选择器
- 如果新站点的"必填项"不是 Element UI 风格 → 检查
scan_required_fields.js的 form-item 选择器组
代码不动。若发现要改代码,说明分层没做干净,先把站点细节推回配置。
4.2 复用本 skill 的哪些资产
| 资产 | 什么时候用 |
|---|---|
scripts/probe_page.py |
步骤 0:新站点侦察,输出候选元素的结构/可编辑性/匹配数 |
scripts/scan_required_fields.js |
步骤 6a:任何 Element UI / 类 Element 表单的必填项体检,可直接注入 |
references/probe-recipes.md |
侦察阶段的通用配方(登录态探测、iframe、动态渲染等待) |
references/pitfalls.md |
20 个已知坑 + 排障对照表,写代码前先扫一遍,能省掉大半返工 |
4.3 完整实战案例
docx-to-help-center-upload 是本 skill 在「某 B2B 电商采购商端 · 新建问题页」上的完整落地:
可直接复用它的站点档案、配置、命令清单与 20 条坑的现场记录。
本 skill 是它抽象出来的通用层 —— 换站点时参考本 skill 的步骤,具体实现抄那个。
5. 反模式速查
| 反模式 | 后果 | 正解 |
|---|---|---|
| 凭想象写选择器 | 命中外壳元素,看似能输入实则全错 | 步骤 0 侦察 + 核对匹配数量与 contentEditable |
| 用托管模式跑生产 | 窗口被关,未保存内容全丢 | auto/attach/cdp 三模式 + 跨进程验保活 |
| 用布尔值表示保存结果 | "没等到"被当成"失败"或谎报成功 | 三态 confirmed/unknown/not_waited |
| 按"元素存在"判成功 | 预置在 DOM 里的隐藏提示导致误判 | 可见性判定 + MutationObserver 记标记 |
只认 is-required 类 |
体检静默失效,一行日志都没有 | 四条线索取或 + 全空字段兜底 |
| 标红时污染标签文本 | 二次扫描标签对不上,一个都标不中 | 角标挂 form-item + 读标签前剥角标 |
取消却 return 0 |
用户被告知"已写进页面",实际什么都没做 | 独立返回码 + 上层单独分支 |
| 演练页跟真实站点不像 | 测试全绿但线上照样翻车 | 照着真实站点改造演练页(星号用 CSS 画、后端对空值返回真实错误码) |
| 只验证"里面的程序" | 包装器/入口脚本的 bug 完全漏过 | 验证任何"壳"都必须真的执行它 |
6. 验证纪律
- 端到端要有本地演练页(模拟目标站点),跑得起来、可断网复现
- 演练页必须照着真实站点改造 —— 星号用 CSS
::before画、后端对空字段回真实错误码。
演练页跟真实站点不像,测试就是自嗨(真实事故:必填识别静默失效的 bug 就是这么漏过去的) - 每次改行为,回头搜一遍有没有测试在"保护"旧语义
(真实事故:取消路径改成非零返回码后,老断言还写rc == 0,是跟着错的行为写的) - 体检脚本要能独立跑、能落盘、能回答"这台电脑能不能跑"这个问题
7. 排障入口
| 现象 | 先做什么 |
|---|---|
| 点保存没反应 | 先看页面上有没有红框/顶部清单 → 看体检那句「还差必填 N 项」→ 翻 save_trace_*.json 有没有发出业务请求 |
| 灌完窗口自己关了、内容没了 | 查 launch_mode 是不是 managed(ctx.close() 会关窗口)→ 改 auto/attach → 内容从草稿目录取回 |
日志一行 [必填检查] 都没有 |
必填标记识别失败(CSS 画的星号)→ 换四条线索取或的实现 |
--attach 报找不到已开浏览器 |
调试端口上确实没有活的 → 让用户用带参数的入口开一次;报错文案里要带命令原文 |
connect_over_cdp 握手失败 |
启动参数缺 --remote-allow-origins=* |
| 调试端口起不来 | 用户目录被同引擎另一个实例占用(singleton 机制让新实例静默退出)→ 关掉那些窗口 |
| 判定未登录但实际已登录 | SPA 的登录探测要先打开业务页(首屏可能是 chrome://new-tab-page/,不是 about:blank) |
| 怀疑是"浏览器兼容性"想换浏览器 | 先跑兼容性体检;同为 Chromium 内核通常无差异 → 换浏览器不是万能药,先按第一条排查 |
完整 20 条坑与现场记录见 references/pitfalls.md。