1. 为什么我要让 AI 自己去社区发稿
先说结论:让 AI 独立完成一次"打开浏览器、登录、写稿、排版、发布"的全流程,远比想象中难。我这次实测跑下来,总共触发了227 次浏览器操作,纯执行耗时4.3 小时,中间踩了9 个坑。这不是一个"跑通就完事"的玩具项目,而是一次把浏览器自动化、AI Agent 决策、页面状态判断揉在一起的硬仗。
事情的起因很朴素。我平时在腾讯云社区写技术稿,流程固定:登录、进创作中心、填标题、贴正文、选分类标签、上传封面、点发布。这套动作我重复了几十次,每次大概 20 分钟。某天我盯着屏幕想,这活儿能不能交给 AI 自己干?不是那种"生成一段文字让我复制粘贴"的半自动,而是真正意义上的端到端——我只给一个主题,剩下的它自己搞定。
关键词里出现了AI、腾讯云、浏览器自动化、Playwright、Chrome,这几个词基本勾勒出了整个技术栈的轮廓。核心思路是:用Playwright驱动Chrome浏览器,把"人操作网页"的每一步翻译成代码指令;再套一层AI Agent做决策,让它根据页面当前状态判断下一步该点哪里、填什么。听起来很顺,但真正跑起来,页面会给你无数个"意外"。
这篇文章适合三类人看:一是想入门浏览器自动化但不知道真实项目长什么样的开发者;二是已经在用 Playwright 做测试、想扩展到"自动化操作真实业务系统"的工程师;三是对 AI Agent 落地感兴趣、想知道"让 AI 自己干活"到底卡在哪的人。我会把 227 次操作背后的决策逻辑、9 个坑的完整排查链路、以及我最后沉淀下来的稳定方案,全部摊开讲。
需要提前说明的是,本文所有操作都基于公开可访问的网页界面,讨论的是自动化技术本身,不涉及任何绕过平台规则的手段。发布行为本身遵守社区的内容规范,AI 生成的内容也经过人工审核后才提交。
2. 技术选型:为什么是 Playwright 而不是别的
2.1 Playwright 对比 Selenium 和 Puppeteer 的真实差异
做浏览器自动化,绕不开三个名字:Selenium、Puppeteer、Playwright。我三个都用过,这次选 Playwright 不是跟风,是有具体理由的。
Selenium 是最老牌的,生态最全,但它的硬伤在于等待机制。Selenium 的显式等待要自己写WebDriverWait配合expected_conditions,页面稍微复杂一点,等待逻辑就写得又臭又长。而且 Selenium 和浏览器之间隔着一层 WebDriver 协议,通信开销大,操作延迟明显。我早期用它做过一个表单填写脚本,光是处理"元素还没渲染出来"就写了一百多行等待代码。
Puppeteer 是 Chrome 团队出的,只支持 Chromium 系,API 简洁,速度快。但它的定位偏"爬虫和截图",多浏览器支持弱,而且社区版更新节奏受 Chrome 版本影响大。关键词里出现了chrome 109、chrome 144这些版本号,说明浏览器版本兼容是个真实痛点,Puppeteer 在这方面绑得比较死。
Playwright 是微软出的,最大的优势是自动等待(auto-waiting)。它内置了"元素可交互"的判断逻辑——点击前会自动等元素可见、稳定、可接收事件,输入前会等元素可编辑。这一条直接砍掉了我 80% 的等待代码。另外它原生支持 Chromium、Firefox、WebKit 三套内核,API 统一,切换浏览器只改一个参数。
| 维度 | Selenium | Puppeteer | Playwright |
|---|---|---|---|
| 自动等待 | 需手写 | 部分支持 | 内置完善 |
| 多浏览器 | 全支持但配置繁琐 | 仅 Chromium 系 | 三内核统一 API |
| 通信方式 | WebDriver 协议 | CDP | CDP + 自有协议 |
| 操作延迟 | 较高 | 低 | 低 |
| 调试工具 | 一般 | 一般 | Trace Viewer 强 |
| 网络拦截 | 弱 | 强 | 强 |
我最终选 Playwright,核心就是那个Trace Viewer。它能把整个执行过程录下来,每一步操作前后的 DOM 快照、网络请求、控制台日志全都有。这次 227 次操作里,有 6 个坑是靠 Trace Viewer 回放才定位到的。没有这个工具,排查时间至少翻倍。
2.2 为什么不用纯 API 调用而要走浏览器
有人会问:腾讯云社区有没有开放 API?直接调 API 发稿不是更稳吗?
这个问题我认真评估过。现实是,大部分内容平台的"发布"接口都是内部接口,没有公开文档,参数结构随时可能变,而且往往带一堆签名校验。你去逆向这些接口,维护成本极高,平台一改你就得重来。相比之下,浏览器自动化操作的是"用户可见的界面",只要界面不变,脚本就不用改。界面是平台对用户的承诺,稳定性远高于内部接口。
另一个原因是,走浏览器能完整复现"真人操作路径",包括登录态维持、页面跳转、表单校验触发。这些在 API 层面要么被跳过,要么需要额外模拟。对于"让 AI 自己发稿"这个目标,走界面更贴近真实场景。
代价当然也有:慢。API 调用可能 200 毫秒完成的事,浏览器操作要 2 秒。227 次操作累积 4.3 小时,平均每次操作 68 秒——这里面大部分时间花在等待页面加载和 AI 决策上。但考虑到这是"无人值守"的任务,慢一点可以接受。
2.3 AI Agent 在链路里扮演什么角色
如果只是固定流程,其实不需要 AI,写死脚本就行。但发稿这件事有个特点:页面状态是不确定的。登录可能遇到验证码,编辑器可能弹出引导蒙层,图片上传可能失败需要重试,分类标签的选项可能因为账号权限不同而不同。这些分支用 if-else 写,能写出上千行,而且覆盖不全。
所以我引入了一层 AI Agent 做决策。它的职责不是"生成文章内容"(那是另一个模型干的),而是根据当前页面截图和 DOM 摘要,判断下一步该做什么。比如截图显示有个弹窗,Agent 就判断"需要先关闭弹窗";DOM 显示标题输入框为空,Agent 就判断"需要填入标题"。
这层设计的关键在于:把"操作"和"决策"解耦。Playwright 负责执行具体动作(点击、输入、滚动),AI 负责决定动作序列。这样即使页面改版,只要 Agent 能看懂新界面,脚本主体不用大改。
3. 227 次操作拆解:一次发稿到底经历了什么
3.1 操作次数的统计口径
先明确"227 次操作"是怎么算的。我统计的是 Playwright 层面实际执行的原子动作,包括:goto(页面跳转)、click(点击)、fill(填充)、type(逐字输入)、press(按键)、wait_for_selector(等待元素)、screenshot(截图,供 AI 判断)、scroll(滚动)。每一次调用算一次。
不包含 AI 模型的推理调用(那是另一个维度的开销),也不包含内部的重试。227 次是"净操作数",实际因为重试触发的操作更多,但那些算在坑的排查里。
3.2 各阶段操作分布
把 227 次操作按阶段拆开,能看出时间花在哪:
| 阶段 | 操作次数 | 占比 | 主要动作 |
|---|---|---|---|
| 启动与环境准备 | 12 | 5.3% | 启动浏览器、设置视口、注入登录态 |
| 登录与状态确认 | 28 | 12.3% | 跳转、检测登录态、处理可能的验证 |
| 进入创作中心 | 19 | 8.4% | 导航、等待编辑器加载、关闭引导 |
| 标题与正文填充 | 41 | 18.1% | 填标题、粘贴正文、处理格式 |
| 分类与标签选择 | 34 | 15.0% | 展开下拉、搜索标签、多选 |
| 封面上传 | 26 | 11.5% | 触发上传、等待处理、裁剪确认 |
| 预览与校验 | 22 | 9.7% | 点预览、检查渲染、返回编辑 |
| 发布与结果确认 | 31 | 13.7% | 点发布、处理二次确认、验证成功 |
| 异常重试与兜底 | 14 | 6.2% | 各类重试、回退、重新定位 |
正文填充只占 18%,说明"写内容"根本不是瓶颈,跟界面交互才是。这也印证了我前面的判断:难点在页面状态处理,不在文本生成。
3.3 4.3 小时的时间都去哪了
4.3 小时 = 258 分钟。平均每次操作 68 秒,这个数字乍看很慢,拆开就合理了:
- 页面加载等待:约 90 分钟。每次跳转、每次编辑器初始化,都要等网络和渲染。
- AI 决策推理:约 70 分钟。每次截图后要调模型判断,单次 3-8 秒,累计起来很可观。
- 元素等待与重试:约 55 分钟。有些元素加载慢,Playwright 的自动等待会耗时间。
- 实际操作执行:约 30 分钟。点击、输入这些动作本身很快。
- 异常处理:约 13 分钟。9 个坑的排查和重试。
提示:如果你的场景对时间敏感,优化重点应该放在"减少 AI 决策次数"上——把高频、确定的操作写成固定脚本,只把不确定的分支交给 AI。我后来就是这么优化的,决策次数砍了一半。
4. 九个坑的完整排查链路
这部分是全文的核心。我不直接给答案,而是把每个坑的现象、排查过程、根因、修复完整还原,你可以照着这个思路复现。
4.1 坑一:登录态在无头模式下失效
现象:有头模式(能看到浏览器窗口)跑得好好的,一换成无头模式(headless),登录态就没了,页面直接跳登录页。
排查:先怀疑是 cookie 没带上。检查context.storage_state()导出的文件,cookie 是在的。再对比有头和无头的请求头,发现无头模式下User-Agent里带了HeadlessChrome字样。有些站点的风控会识别这个特征,判定为非正常环境,直接让登录态失效。
根因:无头模式的默认 UA 暴露了自动化特征。
修复:启动时手动覆盖 UA,去掉 Headless 标识:
context = browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" )改完登录态就稳了。这个坑的教训是:无头模式不是"隐身模式",它有自己的指纹特征,该伪装的地方要伪装。
4.2 坑二:编辑器是 iframe 套 iframe
现象:正文填充一直失败,fill报错说找不到元素,但截图里明明能看到编辑区。
排查:用page.content()打印 DOM,发现编辑区根本不在主文档里。顺着frame标签一层层找,发现是iframe 套 iframe——外层 iframe 是编辑器容器,内层 iframe 才是真正的富文本编辑区。Playwright 的page.fill默认只在主 frame 找元素,自然找不到。
根因:富文本编辑器普遍用 iframe 隔离样式,多层嵌套很常见。
修复:逐层定位 frame:
outer = page.frame_locator("#editor-container iframe") inner = outer.frame_locator("iframe.editor-body") inner.locator("body").fill(content)关键词里出现了scrapy playwright 动态 iframe,说明这是动态加载的 iframe,还得先等外层 frame 出现再进内层。我加了个wait_for_selector才稳定。
4.3 坑三:富文本粘贴变成了纯文本
现象:正文填进去了,但所有格式(标题、加粗、代码块)全丢了,变成一坨纯文本。
排查:fill方法的行为是"清空后设置 value",对富文本编辑器来说,它只认纯文本。富文本编辑器监听的是input事件和剪贴板事件,fill触发的方式不对。
根因:fill适合普通 input,富文本要用剪贴板或键盘模拟。
修复:改用剪贴板注入。先把内容写入系统剪贴板,再模拟Ctrl+V:
import pyperclip pyperclip.copy(markdown_content) inner.locator("body").click() page.keyboard.press("Control+V")但这里有个新问题:Markdown 粘贴进去不会自动转格式,得看编辑器支不支持 Markdown 快捷输入。我实测发现该编辑器支持,粘贴后自动渲染了标题和代码块。如果不支持,就得逐段用快捷键设置格式,那操作次数会翻倍。
4.4 坑四:分类下拉框点不开
现象:点分类选择框没反应,或者点开了但选项列表是空的。
排查:截图看,下拉框确实被点了,但列表没展开。用page.evaluate检查元素,发现这个下拉框是自定义组件,不是原生<select>。它监听的是mousedown而不是click,而且展开动画有 300 毫秒延迟。
根因:现代前端框架的自定义组件,事件绑定和原生控件不同。
修复:改用dispatch_event触发mousedown,并加等待:
dropdown.dispatch_event("mousedown") page.wait_for_timeout(400) page.locator(".option-item").first.click()这个坑让我意识到:不能假设网页控件是原生的。遇到点不动的元素,先看它监听什么事件。
4.5 坑五:标签搜索的防抖导致选错
现象:想选"云原生"标签,输入后立刻点第一个选项,结果选成了"云计算"。
排查:标签选择框有搜索功能,输入关键词后会异步请求候选列表。我输入"云原生"后马上点,此时列表还是上一次"云"的搜索结果,第一个是"云计算"。
根因:搜索有防抖(debounce),输入后要等请求返回,列表才更新。
修复:输入后等待目标选项文本出现,再点击:
search_input.fill("云原生") page.wait_for_selector(".option-item:has-text('云原生')") page.locator(".option-item:has-text('云原生')").click()用has-text精确匹配,避免选错。这个坑的通用教训是:任何带搜索的异步列表,都要等目标项出现再操作。
4.6 坑六:封面上传的 input 是隐藏的
现象:找不到上传按钮,或者找到了但click无效。
排查:DOM 里确实有个<input type="file">,但它被display: none隐藏了,界面上显示的是个美化过的按钮。Playwright 默认不会操作不可见元素。
根因:文件上传控件普遍被隐藏,用自定义按钮触发。
修复:直接用set_input_files操作隐藏的 input,绕过点击:
page.locator("input[type='file']").set_input_files("cover.png")set_input_files不需要元素可见,直接设置文件路径即可。这是处理上传的标准姿势。
4.7 坑七:图片处理是异步的,点快了报错
现象:上传封面后立刻点"确定",提示"图片处理中,请稍候"。
排查:上传后服务端要做压缩、裁剪、生成缩略图,这需要时间。界面上有个进度条,但进度条消失不代表处理完成,还有个"确认裁剪"的步骤。
根因:异步处理,需要等明确的完成信号。
修复:等"确认"按钮从 disabled 变 enabled,或者等某个成功提示出现:
page.locator("button.confirm-crop").wait_for(state="visible") page.locator("button.confirm-crop").click() page.wait_for_selector(".upload-success", timeout=30000)超时设 30 秒,给足处理时间。这个坑的教训是:异步操作要等状态信号,不能靠固定 sleep。
4.8 坑八:发布按钮被遮挡
现象:点发布没反应,控制台报"element is not clickable at point"。
排查:截图看,发布按钮在页面底部,但有个浮动的"帮助"按钮正好压在它上面。Playwright 点击时会检查点击位置是否被遮挡,被遮挡就报错。
根因:浮动元素遮挡。
修复:先滚动到底部,再关掉浮动帮助按钮,或者用force=True强制点击:
page.locator(".help-float").click() # 关掉帮助 page.locator("button.publish").scroll_into_view_if_needed() page.locator("button.publish").click()我倾向先关浮动元素,因为force=True可能点到错误的位置。这个坑说明:页面上的浮动组件是自动化的天敌。
4.9 坑九:发布后的成功判断不可靠
现象:点了发布,页面跳转了,但不确定是真成功还是跳到了错误页。
排查:一开始我用"URL 变化"判断成功,结果有次跳到了登录页(登录态过期),也被判成成功。后来改用"检测成功提示元素",但提示是 toast,几秒就消失,检测时机不好把握。
根因:成功信号有多种,单一判断不可靠。
修复:组合判断——URL 包含成功路径 + 成功提示元素出现 + 文章列表页能查到新文章:
page.wait_for_url("**/article/**", timeout=15000) page.wait_for_selector(".publish-success-toast", timeout=5000) # 最后去列表页验证 page.goto("https://cloud.tencent.com/developer/articles") assert page.locator(f"text={title}").count() > 0三重校验,任何一环失败都算失败。这个坑的教训是:关键操作的成败判断,永远不要只依赖单一信号。
5. AI 决策层怎么设计才不添乱
5.1 决策粒度:太细会慢,太粗会错
AI Agent 的决策粒度是个关键设计点。我一开始让 AI 决定"每一个原子操作",结果就是 227 次操作里 AI 参与了 200 多次,光推理就花了 70 分钟,而且经常做出莫名其妙的决定——比如在标题框里输入了正文内容。
后来我改成分层决策:确定性的操作(跳转、填标题、点发布)写死脚本,不确定性的分支(弹窗处理、异常恢复、元素定位失败)才交给 AI。这样 AI 决策次数降到 40 次左右,时间省了一半,准确率反而高了。
判断标准很简单:这个操作在不同运行中会不会不一样?会,就交给 AI;不会,就写死。
5.2 给 AI 的输入:截图 + DOM 摘要
AI 要判断页面状态,得给它"看"页面的方式。纯截图信息量大但模型理解成本高,纯 DOM 又太冗长。我的做法是截图 + 精简 DOM 摘要:
- 截图:整页截图,缩放到 1280 宽,控制 token 消耗。
- DOM 摘要:只提取可交互元素(button、input、a、select),每个元素给 tag、文本、位置、可见性。
给 AI 的 prompt 大概是:"当前页面截图和可交互元素列表如下,目标是完成发稿,当前处于 XX 阶段,请判断下一步应该操作哪个元素,输出元素索引和操作类型。"
这样 AI 的输出是结构化的(元素索引 + 动作),Playwright 直接执行,不用解析自然语言。
5.3 决策失败的兜底
AI 也会犯错。我的兜底策略是三次失败就回退到固定脚本。比如 AI 连续三次没找到"发布"按钮,就执行预设的"滚动到底部 + 查找发布按钮"的固定逻辑。这样即使 AI 抽风,流程也不会卡死。
另外,每次 AI 决策后我都会记录"决策 + 结果",跑完复盘时能看出哪些决策是低效的,针对性优化 prompt。
6. 稳定性优化的几个关键手段
6.1 用 storage_state 复用登录态
每次跑都重新登录太慢,而且容易触发风控。Playwright 的storage_state能把 cookie 和 localStorage 导出成 JSON,下次直接加载:
context = browser.new_context(storage_state="auth.json")第一次手动登录后导出,后续自动加载。注意这个文件有时效,登录态过期了要重新导出。我加了个检测:如果加载后跳到了登录页,就提示需要重新登录。
6.2 关键步骤加截图存档
每次关键操作(进入编辑器、填充完成、发布前、发布后)都截图存档。出问题时不用重跑,直接看截图就知道卡在哪。这次 9 个坑里有 4 个是靠截图对比发现的。
截图命名带上时间戳和阶段名,方便回溯:
page.screenshot(path=f"shots/{stage}_{timestamp}.png", full_page=True)6.3 重试要带退避,不要死循环
网络抖动、元素加载慢是常态,重试是必须的。但重试要带指数退避,不能死循环:
for attempt in range(3): try: do_something() break except Exception: page.wait_for_timeout(1000 * (2 ** attempt))第一次等 1 秒,第二次 2 秒,第三次 4 秒。超过三次就抛异常,交给上层处理。这样既给了页面恢复时间,又不会无限卡住。
6.4 把"等待"写对,比什么都重要
Playwright 的自动等待能解决大部分问题,但有些场景要手动等。我的原则是:等状态,不等时间。等元素可见、等文本出现、等 URL 变化,而不是wait_for_timeout(5000)。固定 sleep 要么不够要么浪费,状态等待才是正解。
7. 这套方案还能怎么扩展
跑通发稿之后,我把这套框架抽象成了一个通用的"浏览器自动化 + AI 决策"模板。核心是三个模块:执行器(Playwright 封装)、决策器(AI Agent)、状态机(管理流程阶段)。换一个目标网站,只需要改状态机的阶段定义和决策器的 prompt,执行器基本不用动。
实际扩展方向有几个。一是多平台分发,同一篇文章自动发到多个社区,每个平台一套状态机。二是定时任务,配合调度器每天自动发稿。三是监控告警,发布失败时推送到消息渠道。
但我要泼盆冷水:这套东西的维护成本不低。平台界面一改,状态机就得跟着改。所以它适合"高频、重复、流程固定"的场景,低频操作还是手动划算。我现在的用法是:批量发稿时用自动化,单篇精修时手动。
最后分享一个我踩坑后才明白的道理:浏览器自动化的难点从来不是"怎么点按钮",而是"怎么知道现在该点哪个按钮"。Playwright 把前者解决得很好,后者才是 AI 真正能发挥价值的地方。把这两件事分清楚,你的自动化项目就成功了一半。