网页后台上传 / 发布 / 填表自动化
这个技能解决什么
有些平台只给网页入口:没有 CLI、没有可用的 API(或 API 没 token 直接 401), 还必须带着登录态才能走完。典型如 SkillHub 发布页、各类云控制台、内容后台的上传表单。
本技能的做法:不装任何浏览器,直接遥控用户机器上已有的 Chrome / Edge
(用 Chrome DevTools Protocol)。对比 agent-browser / Playwright 方案:
| 本技能 | agent-browser / Playwright | |
|---|---|---|
| 下载体积 | 0(只依赖 websocket-client,几十 KB) |
Chromium 约 500MB |
| 登录态 | 复用用户日常浏览器的登录态 | 全新 profile,通常要重新登录 |
| 适用 | 有头、需登录的后台操作 | 无头采集、批量自动化 |
依赖:pip install websocket-client(多数环境已具备)。
流程
1. 探测浏览器
python scripts/cdp.py chrome-path
按 Windows / macOS / Linux 依次探测 Chrome、Edge、Chromium,打印可执行文件路径;找不到输出 NOT_FOUND。
2. 带调试端口启动 —— 必须用「常驻后台任务」
这一步最容易卡住:浏览器会随启动它的 shell 一起被回收。
| 启动方式 | 结果 |
|---|---|
普通 shell 调用 + & |
❌ shell 退出即死,调试端口立刻不通 |
cmd //c start(Git Bash) |
❌ MSYS 路径转换会把 C:\Program Files\... 拆坏,报 'hrome' not recognized |
Start-Process(PowerShell) |
❌ 进程起来了但端口不监听 |
后台常驻任务(Bash 工具 run_in_background=true) |
✅ 稳定常驻 |
"<上一步探测到的路径>" --remote-debugging-port=9222 \
--user-data-dir="<临时目录,建议新建,别污染日常 profile>" \
--no-first-run --no-default-browser-check "https://目标页"
想长期省事,可在启动参数里加 --remote-allow-origins=*,这样第 3 步就不必抑制 Origin。
3. 连通性自检(两条都要做)
unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy ALL_PROXY all_proxy # 本机代理会拦 localhost
curl -s --noproxy '*' http://127.0.0.1:9222/json/version # 能返回即通
连 WebSocket 时(脚本已处理):Chrome 新版校验握手来源,需
websocket.create_connection(ws_url, suppress_origin=True),否则 Handshake status 403 Forbidden。
4. 读页面 → 判断状态
python scripts/cdp.py eval "document.body.innerText.slice(0,1500)"
用正文判断「是否已登录 / 列表里有没有这条 / 当前什么状态」,比截图快得多。
需要换页面时用 CDP_MATCH 环境变量指定 URL 片段;留空则取第一个页面标签。
5. 点击
先按 textContent.trim() 精确匹配元素再 el.click():
var spans=[].slice.call(document.querySelectorAll('span,button,a'));
for (var i=0;i<spans.length;i++){
var s=spans[i];
if((s.textContent||'').trim()!=='更新') continue;
var box=s, hit=null;
for(var k=0;k<5 && box;k++){ box=box.parentElement;
if(box && (box.textContent||'').indexOf('slug: 唯一标识')>=0){ hit=box; break; } }
if(hit){ s.click(); return 'clicked'; }
}
⚠️ 别用 closest('div') 直接找卡片——按钮常在更外层的兄弟容器里,
要向上逐层找包含唯一标识文本的那一层再定位。
6. 填表(React / Vue 受控组件)
直接改 .value 不生效,必须走原生 setter:
var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, v);
el.dispatchEvent(new Event('input', {bubbles: true}));
⚠️ 但带异步后端校验的字段(如「slug 是否可用」)光这样还不够:
合成事件常常不触发它的防抖校验,界面会一直挂着上一次的报错(如 Slug 不可用、
输入框 aria-invalid="true"),改了值也不重算,于是提交必然失败。
必须用真实输入事件再补一次 blur:
c.call('DOM.focus', nodeId=nid)
c.eval("var e=document.querySelector('#skill-slug'); e.focus(); e.setSelectionRange(0,e.value.length);")
c.call('Input.insertText', text='目标值')
c.eval("var e=document.querySelector('#skill-slug'); e.blur();")
# 等 5~6 秒后检查 aria-invalid 是否为 false,再提交
判据:提交前确认目标输入框的 aria-invalid 是 false、页面里没有残留报错文本。
「改完值但报错不消失」≠ 值真的不可用——先怀疑校验没被触发(真被占用时
重新输入后仍会报,可据此区分)。
7. 上传文件
python scripts/cdp.py file 'input[accept=".zip,application/zip"]' /abs/path/file.zip
然后补发一次 change 事件让前端解析:
inp.dispatchEvent(new Event('change', {bubbles:true}));
解析成功的标志是界面出现「已选择 N 个文件,总大小 X KB」——以这条为准。
8. 提交与复核
点提交按钮后对话框通常自动关闭。复核要 location.reload() 再读列表状态
(如「安全审核中 / V 1.0.1」)。最后连 /json/version 的 webSocketDebuggerUrl
发 Browser.close 收尾,别留僵尸浏览器。
必坑清单
- 常驻后台启动,否则端口永远不通(见第 2 步)。
suppress_origin=True,否则 WS 403。- localhost 也要 bypass 代理,否则 502。
- 隐藏 file input 常不止一个:文件夹那个带
webkitdirectory,压缩包那个带accept=".zip,application/zip"——按 accept 挑,别按序号猜。 - 弹窗会重渲染:注入文件后原 input 常被移除,
querySelector返回 null 不算失败,看界面文本确认。 - 提交后对话框关闭通常即成功;不确定就刷新页面看状态字段。
- 若遇到站点有反爬/WAF,优先复用用户真实 profile(登录态 + 指纹),这是本方案相对无头浏览器的最大优势。
脚本接口
| 子命令 | 作用 |
|---|---|
chrome-path |
探测本机浏览器可执行文件 |
eval "<js>" |
页面上下文求值 |
shot <out.png> |
截图 |
file <选择器> <路径> |
给 <input type=file> 塞文件 |
环境变量:CDP_PORT(默认 9222)、CDP_MATCH(默认取第一个页面标签)。