1. 项目缘起与整体设计思路
1.1 为什么想让 AI 自己去发一篇稿
这个项目的起点其实特别朴素:我手头有一堆需要分发到不同技术社区的内容,每次手动复制粘贴、调整格式、上传封面、选分类标签,一套流程走下来少说二十分钟,多的时候一个小时都打不住。重复劳动做多了人就容易烦躁,烦躁就会出错,出错就得返工,返工又浪费时间——这个循环我实在受够了。
于是我就想,能不能让 AI 自己把这件事干了?不是那种"帮我生成一段文字我自己去贴"的半自动,而是从打开浏览器、登录账号、进入编辑器、粘贴正文、设置封面、选择分类、点击发布,整条链路全部交给 AI 去跑。我只需要在最后确认一下结果就行。
这个想法听起来有点疯狂,但拆开来看其实并不复杂。核心就是三件事:浏览器自动化操作、AI 对页面状态的判断与决策、异常情况的处理与重试。把这三件事串起来,就是一个能自己发稿的 AI Agent。
我给它起了个名字叫 WorkBuddy,定位就是一个"能帮你干脏活累活的 AI 工作搭子"。这次实战的目标很明确:让 WorkBuddy 自己去腾讯云社区发一篇技术稿,全程记录操作次数、耗时、踩到的坑,做一次完整的复盘。
1.2 整体架构是怎么搭的
整个方案的架构我反复调整过好几版,最终定下来的结构是这样的:
- 控制层:一个 Python 脚本作为主控,负责调度整个流程,维护状态机,记录日志。
- 浏览器层:用 Playwright 驱动 Chromium,负责所有页面操作。选 Playwright 而不是 Selenium,主要是因为它对现代前端框架的兼容性更好,自动等待机制更省心,而且支持拦截网络请求,方便我做调试。
- 决策层:接入大模型 API,把当前页面的 DOM 摘要和截图传给模型,让模型判断"现在处于哪个阶段""下一步该点哪里""有没有异常弹窗"。
- 状态层:用一个 JSON 文件记录当前进度,比如"已登录""已进入编辑器""正文已粘贴""封面已上传",这样即使中途崩了也能从断点恢复,不用从头再来。
为什么要把决策层单独拆出来?因为纯靠选择器硬编码的自动化脚本太脆了。腾讯云社区的页面结构时不时会微调,今天能用的 CSS 选择器明天可能就失效了。让 AI 来看页面、做判断,虽然慢一点,但鲁棒性高很多。实测下来,同样的流程,纯选择器方案在页面改版后基本全废,而 AI 决策方案只需要调整提示词就能继续跑。
1.3 预期目标与验收标准
我给这次实战定了几个硬指标:
| 指标项 | 目标值 | 实际值 |
|---|---|---|
| 总操作次数 | 控制在 300 次以内 | 227 次 |
| 总耗时 | 6 小时以内 | 4.3 小时 |
| 人工干预次数 | 不超过 3 次 | 2 次 |
| 最终发布成功 | 是 | 是 |
| 踩坑数量 | 记录完整 | 9 个 |
227 次操作是什么概念?平均下来每分钟大概 0.88 次操作。这个节奏其实不算快,因为每次操作之后都要等页面响应、等 AI 判断、等网络请求完成。但胜在稳定,没有出现那种"疯狂点击导致页面卡死"的情况。
4.3 小时的耗时里,真正花在"操作"上的时间大概只占三分之一,剩下三分之二都花在等待和重试上。这个比例后面我会详细拆解,因为搞清楚时间花在哪里,才能知道优化空间在哪里。
2. 核心细节解析与实操要点
2.1 浏览器自动化的选型与配置
浏览器自动化这块,我前后试过三种方案:
第一种是直接用 requests 库发 HTTP 请求模拟表单提交。这个方案最快,但问题是腾讯云社区的发布接口有签名校验和 CSRF token,逆向成本太高,而且一旦接口变了就得重新逆向,维护成本爆炸。我试了两个小时就放弃了。
第二种是 Selenium。经典方案,资料多,但坑也多。最大的问题是它的等待机制不够智能,经常出现"元素还没渲染出来就去点"的情况,导致大量 NoSuchElementException。而且 Selenium 对 Shadow DOM 的支持一直不太好,腾讯云社区的部分组件用了 Shadow DOM,操作起来很别扭。
第三种就是最终采用的 Playwright。选它的理由很实在:
- 自动等待:Playwright 的
click()方法自带等待机制,会等元素可交互了再点,省去了大量sleep()。 - 网络拦截:可以监听所有 XHR 请求,方便判断"发布请求有没有发出去""返回了什么"。
- 多浏览器支持:虽然这次只用 Chromium,但以后要扩展到 Firefox 或 WebKit 也不用改代码。
- 截图和录屏:内置的截图功能对调试太重要了,每次操作前后各截一张,出问题了一看便知。
安装配置很简单:
pip install playwright playwright install chromium然后在代码里这样初始化:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch( headless=False, # 调试阶段开有头模式,方便观察 args=['--disable-blink-features=AutomationControlled'] ) context = browser.new_context( viewport={'width': 1920, 'height': 1080}, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ) page = context.new_page()注意:
--disable-blink-features=AutomationControlled这个参数很重要,不加的话很多网站会检测到你是自动化工具,直接给你弹验证码或者限制操作。
2.2 AI 决策层的提示词设计
AI 决策层是整个方案里最"玄学"的部分,提示词写得好不好,直接决定了 AI 能不能准确判断页面状态。我前后改了十几版提示词,最终稳定下来的版本大概是这样的结构:
你是一个浏览器操作助手。当前页面的 DOM 摘要如下: {dom_summary} 当前页面的截图已附上。 请判断: 1. 当前处于哪个阶段?(登录页/首页/编辑器/发布确认页/其他) 2. 下一步应该执行什么操作?(点击某个元素/输入文字/滚动页面/等待/报告异常) 3. 如果点击,目标元素的描述是什么? 请以 JSON 格式返回: { "stage": "阶段名称", "action": "操作类型", "target": "目标元素描述", "reason": "判断理由" }这里有几个关键设计:
DOM 摘要而不是完整 DOM。完整 DOM 动辄几万行,传给模型既慢又贵,而且模型容易被无关信息干扰。我的做法是只提取可见元素、按钮、输入框、链接,并且只保留文本内容和关键属性(id、class、role),把 DOM 压缩到 2000 字以内。
截图辅助判断。有些状态光看 DOM 看不出来,比如"页面是不是在加载中""有没有弹窗遮挡",这时候截图就派上用场了。我用的是多模态模型,能同时理解文本和图像。
强制 JSON 输出。这样解析起来方便,不会出现模型返回一段自然语言然后我还要用正则去提取的情况。
reason 字段。这个字段看起来多余,但实际调试时特别有用。AI 判断错了,我看一眼 reason 就知道它为什么错,是 DOM 摘要没提取好,还是提示词有歧义。
2.3 状态机与断点恢复机制
整个发布流程我拆成了 12 个状态:
INIT:初始化,打开浏览器LOGIN_CHECK:检查是否已登录LOGIN:执行登录操作NAV_TO_EDITOR:导航到编辑器页面TITLE_INPUT:输入标题CONTENT_INPUT:输入正文COVER_UPLOAD:上传封面TAG_SELECT:选择分类标签PREVIEW_CHECK:预览检查PUBLISH_CLICK:点击发布PUBLISH_CONFIRM:确认发布DONE:完成
每个状态执行完就写一次 JSON 文件:
import json def save_state(state, extra=None): data = { 'current_state': state, 'timestamp': time.time(), 'extra': extra or {} } with open('workbuddy_state.json', 'w') as f: json.dump(data, f, ensure_ascii=False, indent=2)这样如果脚本在第 7 步崩了,重启之后直接从第 7 步继续,不用重新登录、重新输入标题正文。实测下来,这个机制至少帮我省了 1.5 小时。
实操心得:状态文件一定要加时间戳,并且设置过期时间。我有一次隔了一天再跑,状态文件还停留在"已登录",但实际上 cookie 早就失效了,结果脚本一直在那儿点发布按钮,点了二十多次都没反应。后来加了判断:如果状态文件超过 2 小时,强制从
LOGIN_CHECK重新开始。
3. 实操过程与核心环节实现
3.1 登录环节:最耗时但也最关键的 40 分钟
登录是整个流程里最麻烦的一步,没有之一。腾讯云社区的登录支持多种方式:账号密码、手机验证码、扫码登录。我一开始想用账号密码,但发现它有滑块验证,Playwright 模拟的鼠标轨迹太"完美"了,一眼就被识别出来。
后来改用扫码登录。思路是这样的:脚本打开登录页,切换到扫码模式,然后把二维码截图保存到本地,我用手机扫一下,脚本检测到登录成功后继续。
# 切换到扫码登录 page.click('text=扫码登录') page.wait_for_selector('.qrcode-img', timeout=10000) # 截图二维码 qrcode_element = page.query_selector('.qrcode-img') qrcode_element.screenshot(path='qrcode.png') print('请扫描 qrcode.png 完成登录') # 轮询检测登录状态 for i in range(60): # 最多等 2 分钟 if page.query_selector('.user-avatar'): print('登录成功') break time.sleep(2) else: raise Exception('登录超时')这个环节花了 40 分钟,主要时间都耗在调试上。踩的坑包括:二维码元素被遮挡截不到、登录成功后页面跳转导致选择器失效、轮询间隔太短导致请求过多被限流。
注意:轮询检测登录状态时,间隔不要低于 2 秒。我一开始设的 0.5 秒,结果触发了风控,账号被临时限制登录 10 分钟。这个坑后面还会提到。
3.2 正文输入:从"逐字打字"到"一次性粘贴"
正文输入这块我走了弯路。最开始用的是 Playwright 的type()方法,模拟真人逐字输入:
page.type('.editor-content', article_content, delay=50)delay=50表示每个字符间隔 50 毫秒。一篇 3000 字的文章,光输入就要 150 秒,而且中间只要有一个字符输入失败,整段就乱了。更麻烦的是,腾讯云的编辑器是富文本编辑器,type()方法对它的兼容性很差,经常出现文字跑到错误的位置、格式丢失的情况。
后来改成用剪贴板方案:
# 把内容写入剪贴板 import pyperclip pyperclip.copy(article_content) # 聚焦编辑器,然后粘贴 page.click('.editor-content') page.keyboard.press('Control+V')这个方案快得多,3000 字瞬间粘贴完成。但新的问题来了:富文本编辑器对粘贴的内容会做格式清洗,我原本的 Markdown 格式全丢了,变成了一坨纯文本。
最终的解决方案是:先用剪贴板粘贴纯文本,然后通过编辑器的 API 或者模拟快捷键来重新应用格式。腾讯云的编辑器支持 Markdown 快捷输入,比如输入##然后空格就会变成二级标题。我写了一个小函数,把 Markdown 内容拆成块,逐块处理:
def paste_markdown(page, markdown_text): blocks = parse_markdown_blocks(markdown_text) for block in blocks: if block['type'] == 'heading': page.keyboard.type('#' * block['level'] + ' ') page.keyboard.type(block['content']) page.keyboard.press('Enter') elif block['type'] == 'paragraph': page.keyboard.type(block['content']) page.keyboard.press('Enter') elif block['type'] == 'code': page.keyboard.type('```') page.keyboard.press('Enter') page.keyboard.type(block['content']) page.keyboard.press('Enter') page.keyboard.type('```') page.keyboard.press('Enter')这个环节花了大概 50 分钟,其中 30 分钟都在调试格式问题。
3.3 封面与标签:看似简单实则暗坑无数
封面和标签这两个环节,我原本以为是最简单的,结果加起来花了将近 1 小时。
封面问题:腾讯云社区的封面上传支持拖拽和点击两种方式。我一开始用拖拽,Playwright 的拖拽 API 对文件拖拽支持不太好,试了好几次都失败。后来改用点击上传:
# 点击上传按钮 with page.expect_file_chooser() as fc_info: page.click('.cover-upload-btn') file_chooser = fc_info.value file_chooser.set_files('cover.png')这里有个坑:expect_file_chooser必须和click配合使用,而且 click 必须触发文件选择框。如果点击之后弹的是自定义的上传弹窗而不是系统文件选择框,这个方法就失效了。腾讯云社区恰好就是自定义弹窗,所以我又得换方案:
# 直接设置 input[type=file] 的值 page.set_input_files('input[type=file]', 'cover.png')这个方案的前提是页面上有input[type=file]元素。腾讯云社区的上传组件底层确实用了这个,只是被隐藏了。用set_input_files可以直接绕过 UI 操作,稳定得多。
标签问题:标签选择看起来就是点几个按钮的事,但腾讯云的标签是动态加载的,而且有数量限制(最多选 5 个)。我一开始没注意限制,脚本疯狂点击,结果第 6 个标签点下去之后,前面选的标签全被清空了。后来加了计数逻辑:
selected_tags = [] for tag in target_tags: if len(selected_tags) >= 5: break page.click(f'text={tag}') selected_tags.append(tag) time.sleep(0.5) # 等标签选中状态更新实操心得:标签选择一定要加延时。腾讯云的标签选中状态是异步更新的,点太快会出现"点了但没选上"的情况。我实测下来,0.5 秒的间隔比较稳妥,低于 0.3 秒就容易出问题。
3.4 发布确认:最后一步的惊魂 20 分钟
点击发布按钮之后,腾讯云社区会弹一个确认框,让你确认标题、分类、原创声明等信息。这个确认框的"确认发布"按钮位置是固定的,但问题是——它有时候会加载很慢。
我第一次跑的时候,脚本点击发布按钮之后立刻去找确认按钮,结果没找到,报错退出。第二次加了 3 秒等待,还是偶尔失败。第三次改成轮询等待:
page.click('.publish-btn') # 轮询等待确认框出现 for i in range(30): confirm_btn = page.query_selector('.confirm-publish-btn') if confirm_btn and confirm_btn.is_visible(): confirm_btn.click() break time.sleep(1) else: raise Exception('确认框未出现')这个环节花了 20 分钟,主要是在处理各种边界情况:确认框没出现、确认框出现了但按钮不可点、点击之后页面没反应、点击之后报错说"内容不能为空"(实际上是正文加载延迟导致的)。
最终发布成功的那一刻,我看了一下计时器:4 小时 18 分钟。加上前面准备和调试的时间,总共 4.3 小时。
4. 九个坑的完整记录与排查技巧
4.1 坑一:登录轮询触发风控
现象:轮询检测登录状态时,间隔设得太短(0.5 秒),跑了大概 2 分钟之后,页面突然跳转到"操作过于频繁,请稍后再试"。
排查:一开始以为是 IP 被 ban 了,换了网络环境还是不行。后来看请求日志,发现 2 分钟内发了 240 次检测请求,平均每秒 2 次。这个频率对任何网站来说都算异常。
解决:轮询间隔改成 2 秒,并且加了随机抖动(1.8 到 2.2 秒之间随机)。另外,检测逻辑从"每次都查 DOM"改成"先查 cookie 再查 DOM",减少不必要的页面查询。
避坑技巧:任何轮询操作,间隔不要低于 1 秒。如果必须高频检测,用 WebSocket 或者长轮询,不要用短轮询硬怼。
4.2 坑二:富文本编辑器粘贴格式丢失
现象:用剪贴板粘贴 Markdown 内容后,所有格式(标题、加粗、代码块)全部丢失,变成纯文本。
排查:一开始以为是剪贴板的问题,换了pyperclip和clipboard两个库,结果一样。后来发现是腾讯云编辑器的粘贴处理逻辑:它默认只接受纯文本,富文本格式需要走特定的粘贴事件。
解决:改成逐块输入 + Markdown 快捷语法触发。虽然慢一点,但格式准确率从 0% 提升到了 95% 以上。
4.3 坑三:封面上传的两种失败模式
现象:用拖拽上传失败,用expect_file_chooser也失败。
排查:拖拽失败是因为 Playwright 的拖拽 API 对文件拖拽支持不完善;expect_file_chooser失败是因为腾讯云用的是自定义上传弹窗,不是系统文件选择框。
解决:直接用set_input_files操作隐藏的input[type=file]元素。这个方案最稳定,但前提是你能找到那个 input 元素。找的方法是在页面上搜索input[type=file],或者用开发者工具看上传组件的实现。
4.4 坑四:标签选择数量超限导致清空
现象:选了 6 个标签之后,前面选的 5 个全没了。
排查:看腾讯云的提示文案,发现最多只能选 5 个。脚本没有做数量控制,点第 6 个的时候触发了"重置"逻辑。
解决:加计数逻辑,超过 5 个就停止。另外,每次选择之后加 0.5 秒延时,确保选中状态更新完成。
4.5 坑五:确认框加载延迟导致点击失败
现象:点击发布按钮后,脚本立刻去找确认按钮,找不到就报错。
排查:确认框是异步加载的,有时候快有时候慢,取决于网络状况。
解决:改成轮询等待,最多等 30 秒。同时加了"如果确认框没出现,截图保存现场"的逻辑,方便事后排查。
4.6 坑六:状态文件过期导致无效操作
现象:隔天再跑脚本,状态文件显示"已登录",但实际上 cookie 已失效,脚本一直在点发布按钮,点了二十多次。
排查:状态文件没有过期机制,脚本无条件信任文件里的状态。
解决:状态文件加时间戳,超过 2 小时强制从LOGIN_CHECK重新开始。另外,每次操作前先检查关键元素是否存在,不存在就触发状态回退。
4.7 坑七:DOM 摘要提取不完整导致 AI 误判
现象:AI 把"发布确认页"误判为"编辑器页",导致重复输入内容。
排查:DOM 摘要提取时,只提取了可见元素,但确认页的某些关键元素被判定为"不可见"(因为它们在折叠区域里)。
解决:调整提取逻辑,把display:none但visibility:visible的元素也纳入提取范围。同时,在提示词里加了"如果页面同时存在编辑器和确认框元素,优先判断为确认页"的规则。
4.8 坑八:网络请求超时导致操作中断
现象:上传封面时,请求卡住,脚本等了 60 秒后超时退出。
排查:封面图片有 2MB,上传时网络波动导致超时。
解决:给所有网络操作加了重试机制,最多重试 3 次,每次间隔 5 秒。另外,把封面图片压缩到 500KB 以内,减少上传时间。
4.9 坑九:发布成功但页面没跳转
现象:点击确认发布后,接口返回成功,但页面没有跳转到文章详情页,脚本一直等在那儿。
排查:腾讯云社区发布成功后是局部刷新,不是整页跳转。脚本在等 URL 变化,但 URL 根本没变。
解决:改成检测页面上的"发布成功"提示元素,而不是等 URL 变化。同时,加了"如果 30 秒内没检测到成功提示,但接口返回了成功,也判定为成功"的兜底逻辑。
4.10 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 登录轮询被限制 | 轮询频率过高 | 看请求日志频率 | 间隔改 2 秒 + 随机抖动 |
| 粘贴格式丢失 | 编辑器只接受纯文本 | 手动粘贴测试 | 逐块输入 + Markdown 语法 |
| 封面上传失败 | 拖拽 API 不兼容 | 看上传组件实现 | 用 set_input_files |
| 标签被清空 | 超过数量限制 | 看页面提示文案 | 加计数逻辑 |
| 确认框找不到 | 异步加载延迟 | 截图看现场 | 轮询等待 + 超时截图 |
| 状态文件过期 | 无过期机制 | 看时间戳 | 加 2 小时过期判断 |
| AI 误判页面 | DOM 摘要不完整 | 看 AI 返回的 reason | 调整提取逻辑 + 提示词 |
| 网络请求超时 | 图片太大/网络波动 | 看请求耗时 | 重试机制 + 压缩图片 |
| 发布成功没跳转 | 局部刷新 | 看接口返回 | 检测提示元素而非 URL |
5. 效率分析与优化空间
5.1 227 次操作的时间分布
我把 227 次操作按类型做了统计:
| 操作类型 | 次数 | 占比 | 总耗时 | 平均耗时 |
|---|---|---|---|---|
| 页面导航 | 18 | 7.9% | 12 分钟 | 40 秒 |
| 元素点击 | 89 | 39.2% | 45 分钟 | 30 秒 |
| 文本输入 | 34 | 15.0% | 38 分钟 | 67 秒 |
| 等待/轮询 | 52 | 22.9% | 95 分钟 | 110 秒 |
| 截图/日志 | 21 | 9.3% | 8 分钟 | 23 秒 |
| 异常重试 | 13 | 5.7% | 20 分钟 | 92 秒 |
从这张表可以看出来,等待和轮询占了将近一半的时间(95 分钟 / 258 分钟 ≈ 37%),加上异常重试的 20 分钟,超过 44% 的时间花在"非操作"上。这是自动化流程的常态,但也说明优化空间很大。
5.2 三个可落地的优化方向
方向一:减少不必要的等待。目前很多等待是固定延时(比如time.sleep(2)),改成条件等待(等某个元素出现就继续)可以省不少时间。我粗略估算,如果全部改成条件等待,等待时间能从 95 分钟压缩到 60 分钟左右。
方向二:并行化。封面压缩、标签准备、正文格式化这些操作可以在浏览器操作之前就并行完成,不用串行等待。这部分能省大概 10 分钟。
方向三:缓存登录态。每次跑都重新登录太浪费时间。可以把 cookie 持久化到本地,下次直接加载,跳过登录环节。这个能省 40 分钟。
三个方向加起来,理论上能把总耗时从 4.3 小时压缩到 2.5 小时左右。当然,实际优化效果还得看具体场景。
5.3 成本核算
这次实战的成本主要包括三块:
- API 调用费用:AI 决策层调用了大概 180 次模型,每次平均消耗 1500 token(输入+输出),按当前价格算下来不到 5 块钱。
- 时间成本:4.3 小时,如果按人工时薪折算,这是最大的一块。
- 调试成本:前期调试花了大概 6 小时,这部分是一次性投入,后续复用不用再花。
所以如果只跑一次,这个方案其实不划算。但如果要发 10 篇、20 篇,边际成本就降下来了。我的判断是:发布量超过 5 篇,这个方案就开始有优势了。
6. 后续扩展与个人体会
这套方案跑通之后,我陆续把它扩展到了其他场景:自动回复社区评论、自动同步文章到多个平台、自动生成周报并提交。核心逻辑都是一样的:浏览器自动化 + AI 决策 + 状态机 + 异常重试。
有几个个人体会比较深:
第一,不要追求一步到位。我一开始想做一个"全自动、零干预"的方案,结果处处碰壁。后来改成"半自动、关键节点人工确认",反而跑得更顺。比如登录环节,扫码登录就是人工介入,但只花 10 秒,比硬怼滑块验证划算得多。
第二,日志和截图比什么都重要。自动化脚本出问题是常态,关键是出问题之后能不能快速定位。我现在的习惯是:每个关键操作前后各截一张图,日志里记录操作类型、目标元素、耗时、结果。出问题了一看日志和截图,基本能定位到具体哪一步。
第三,AI 决策层不是万能的。AI 擅长处理"模糊判断",比如"这个页面是什么状态""这个按钮能不能点",但不擅长处理"精确操作",比如"点击坐标 (523, 287) 的位置"。所以我的方案是:AI 负责判断,Playwright 负责执行,各司其职。
第四,状态机是稳定性的基石。没有状态机的自动化脚本,就像没有存档的游戏,一崩就得从头来。有了状态机,崩了也能从断点恢复,心理压力小很多,调试效率也高很多。
最后分享一个小技巧:如果你也在做类似的浏览器自动化项目,建议先用有头模式(headless=False)跑通全流程,确认没问题了再切无头模式。有头模式下你能看到页面在干什么,调试效率比看日志高十倍。我这次全程用的有头模式,虽然占屏幕,但省下来的调试时间远远超过那点不便。