1893 年马塔贝莱战争,700 名英军凭 4 挺马克沁机枪,击溃了 5000 名马塔贝莱战士的冲锋,对方伤亡超 2000 人,英军仅阵亡 4 人。这不是勇气的胜负,是技术代差的绝对碾压。
当我第一次用 Claude Code 开发 BaikePublisher(百度百科自动发布系统)时,我脑海里浮现的正是这幅画面。
那些曾经让开发者抓狂的研发任务——Playwright 选择器调试、Spring Boot 循环依赖、Hibernate 类型校验、多线程 Cookie 并发——就像潮水一般涌来的马塔贝莱战士,凶悍、密集、且不断变化阵型。
而 Claude Code,就是那挺马克沁机枪。
一、战场:BaikePublisher 是什么
在正式开打之前,先交代清楚战场地形。
BaikePublisher 是一个 Spring Boot 3.3 + Playwright 1.44 的全栈浏览器自动化系统。它的核心使命是:接收一个词条名称和内容,然后全程无人值守地完成百度百科的发布流程——从登录、搜索、创建词条,到填写摘要、提交审核,一气呵成。
最终实现的自动化流程共 8个原子步骤:
登录(Cookie优先/QR码降级)
↓
搜索词条(判断新建 or 编辑)
↓
进入创建/编辑页面
↓
选择词条类型(如"科技/软件")
↓
关闭编辑器新手引导弹窗
↓
填写义项名
↓
填写概述(至少10字)
↓
填写正文 → 提交审核 → 选择"词条未收录"原因 → 确认
每一步都有截图存档,每一步的耗时和状态写入 task_log 表,任意步骤失败后支持按 task_no 重跑。
系统交付物一览:
|
类型 |
数量 |
|
Java 核心类 |
17 个 |
|
REST API 端点 |
8 个 |
|
Playwright 页面动作方法 |
14 个 |
|
数据表 |
3 张(账号、任务、日志) |
|
参考 HTML 文件 |
14 个 |
这是一场需要同时精通 Spring 生态、Playwright 自动化、百度前端逆向、并发安全设计、数据库迁移的多线战役。
放在以往,这样的系统从零到可运行,少说 3~5 天,每个技术坑都可能卡你大半天。
Claude Code 用了不到 4 小时。
二、第一轮齐射:编译错误与循环依赖
战役刚开始,敌军就冲来了第一波——编译无法通过。
Spring 经典的循环依赖:
TaskService ← BaikeService ← TaskService
这种问题传统解法要么加 @Lazy,要么费心重新拆分依赖关系,往往需要你从头梳理调用链,光是理清楚谁依赖谁就要花不少时间。
我只对 Claude Code 说了一句话:
"修复 TaskService 和 BaikeService 之间的循环依赖"
它扫描两个类的完整依赖链,精准识别根因:TaskService.submit() 原本返回 String,TaskController 里同时注入了 TaskService 和 BaikeService,后者又回调前者。解法干净利落:
- TaskService.submit() 改为返回 PublishTask 实体,让 Controller 拿到 ID
- baikeService.execute(task.getId()) 调用迁移到 TaskController,彻底切断环
- BaikeService 只依赖 TaskService 的状态更新方法(updateRunning/updateSuccess/updateFailed)
修改 3 个文件,0 误改,一次编译通过。
紧接着第二波:Hibernate 类型校验异常轰炸上来:
Error creating bean 'entityManagerFactory'
found [text (Types#LONGVARCHAR)], but expecting [tinytext (Types#CLOB)]
这是 Hibernate 6 的一个典型陷阱——@Lob String 在新版本映射为 TINYTEXT,而数据库建的是 TEXT,类型对不上就直接拒绝启动。Claude Code 不需要你解释这个历史包袱,它直接给出正解:
// 删除 @Lob,用 columnDefinition 明确指定
@Column(columnDefinition = "MEDIUMTEXT") private String content;
@Column(columnDefinition = "TEXT") private String errorMsg;
@Column(columnDefinition = "TEXT") private String message; // TaskLog
不需要翻 Hibernate 6 迁移文档,不需要 Stack Overflow。机枪就是这么扫的。
三、第二轮:Playwright 选择器攻坚战——敌军最凶猛的一波
这才是真正的马塔贝莱战士冲锋。凶猛、密集,而且每次换一个阵型。
百度的前端是 React SPA,元素动态渲染,TANGRAM ID 随机生成,class 名带 hash 后缀。靠猜是没有出路的。我采用的战术是:每次报错,把那个页面的 HTML 另存下来,连同报错栈一起喂给 Claude Code。
第一刀:QR 码登录页选择器
TimeoutError: waiting for '#TANGRAM__PSP_11__qrcodeImg' to be visible
原来 TANGRAM ID 不是 PSP_11,查 HTML 发现是 PSP_3,而且这个 ID 下次还会变。Claude Code 分析 登录百度账号.html 后,提出根本解法——彻底放弃 TANGRAM ID,改用稳定的语义 class:
private static final String QRCODE_SECTION = ".tang-pass-qrcode";
private static final String QRCODE_WRAPPER = ".tang-pass-qrcode-imgWrapper";
private static final String QRCODE_IMG = ".tang-pass-qrcode-img";
private static final String QRCODE_REFRESH_BTN = ".Qrcode-refresh-btn";
同时,QR 码图片 wrapper 初始是 display:none,刷新后变 block,不能用 :visible 等待。Claude Code 给出:
page.waitForFunction(
"() => { const el = document.querySelector('.tang-pass-qrcode-imgWrapper'); " +
"return el && getComputedStyle(el).display !== 'none'; }",
null, new Page.WaitForFunctionOptions().setTimeout(15000));
第二刀:创建词条选择器解析崩溃
PlaywrightException: Unexpected token "=" while parsing selector
"a[href*='create'], .create-lemma-btn, text=创建词条"
CSS 属性选择器里的单引号与 text= 引擎不能混写在同一个逗号列表里,这是 Playwright 的解析规则。初学者很容易掉进这个坑。Claude Code 直接给出正确拆分方式:
page.locator("a[href*=\"create\"], .create-lemma-btn")
.or(page.getByText("创建词条"))
.first().click();
第三刀:进入编辑器后点了企业词条入口
百度的创建词条页有两个按钮:普通词条 #create-lemma-btn 和企业词条 #enterprise-create-btn。原来通过搜索结果页跳转,a[href*="create"] 会同时匹配到企业词条链接,导致走错流程。
Claude Code 分析 创建词条_百度百科.html 后,提出直接导航到创建页面,绕开搜索结果的歧义:
private static final String CREATE_INDEX_URL = "https://baike.baidu.com/page/createindex";
// clickCreate() 直接导航,不再从搜索结果点击
page.navigate(CREATE_INDEX_URL);
page.locator("#create-lemma-ipt").fill(entryName);
page.locator("#create-lemma-btn").first().click(); // 精确点普通词条按钮
第四刀:React 编辑器拒绝 fill()
在 contenteditable 的富文本区域调用 Locator.fill() 没有任何反应——React 编辑器根本不是 <input>,也不监听 Playwright 的原生填充事件。Claude Code 给出双层降级方案,先 JS 注入,失败再键盘模拟:
// 第一层:JavaScript 直接写入,触发 React 的 input 事件
Boolean filled = (Boolean) page.evaluate("""
(content) => {
const el = document.querySelector('.eeditor__editor--context');
if (el) {
el.focus();
el.innerText = content;
el.dispatchEvent(new Event('input', {bubbles: true}));
return true;
}
return false;
}""", content);
// 第二层:键盘模拟兜底
if (Boolean.FALSE.equals(filled)) {
Locator editor = page.locator(".eeditor__editor--context").first();
editor.waitFor();
editor.click();
page.keyboard().press("Control+A");
page.keyboard().type(content);
}
第五刀:提交后 URL 永远超时
// 原来的判断条件
page.waitForURL(url -> url.contains("/item/") || url.contains("/view/"),
new Page.WaitForURLOptions().setTimeout(timeout));
词条提交后页面一直等,等了 30 秒超时。把 百度百科——版本提交成功.html 拖进目录,Claude Code 秒查出真相:词条提交后进入审核队列,页面跳转到 submitSuccessful,根本不跳 /item/。修复:
page.waitForURL(
url -> url.contains("submitSuccessful")
|| url.contains("/item/")
|| url.contains("/view/"),
new Page.WaitForURLOptions().setTimeout(timeout));
这五把刀,每一个放在过去都能让人卡半天。Claude Code 平均每个不超过 3 分钟。
四、Claude Code 的核心战术:HTML 作为弹药库
在这次开发中,我摸索出了一个关键战术,也是整个项目效率最高的方法论:
把实际页面的 HTML 下载到项目目录,作为 Claude Code 的"弹药补给"。
每次 Playwright 超时或报错,我就把那个百度页面在浏览器里"另存为完整网页",丢进 src/main/resources/baidu/,然后告诉 Claude Code:
"根据 baidu 文件夹下的 html 文件修改这个 bug"
Claude Code 会用 grep 把所有关键选择器、class 名、data 属性挖出来,精准定位问题,给出有实际 HTML 作为依据的修复方案,而不是凭空猜测。
整个项目累计下载了 14 个页面:
|
HTML 文件 |
解决的问题 |
|
登录百度账号.html |
QR 码登录 class 选择器 |
|
创建词条_百度百科.html |
普通/企业词条按钮区分 |
|
创建引导页_百度百科.html |
引导页跳过按钮 [data-option="ignore"] |
|
词条类型.html |
类型选择对话框 .select-type-dialog 结构 |
|
编辑器新手引导.html |
引导弹窗跳过按钮 .guide-wv-btn-cancel |
|
词条创建原因.html |
提交原因弹窗结构 .bke__modal-save |
|
百度百科——版本提交成功.html |
提交后真实跳转 URL submitSuccessful |
|
… |
… |
这就是马克沁机枪的弹药补给机制:给 AI 真实的战场情报,它就能精准射击,而不是朝着雾中的影子乱打。
没有这些 HTML,Claude Code 给出的选择器全是猜测;有了它们,每一个选择器都有据可查,都能在实际 DOM 中找到对应节点。
五、第三轮:功能扩展的连续火力
核心流程跑通之后,需求开始像追加弹药一样不断涌来。Claude Code 展现出了不间断的持久火力,每一波需求都被快速压制。
需求 1:词条类型选择没有被触发
用户反馈:"selectType 没有被调用"。
根因一查即明:task.getLemmaType() 为 null,条件判断 != null && contains("/") 直接跳过了。修复只需两行,并顺手设置默认值:
String lemmaType = (task.getLemmaType() != null && task.getLemmaType().contains("/"))
? task.getLemmaType() : "科技/软件";
String[] typeParts = lemmaType.split("/", 2);
step(page, taskId, "SELECT_TYPE", start, () ->
baikePageAction.selectType(page, typeParts[0].trim(), typeParts[1].trim()));
不再有条件判断,无论如何都会执行,默认值兜底。
需求 2:义项名必须填写
一句话需求,Claude Code 完成了完整的全链路改动:
PublishRequest 新增 lemmaDesc 字段(@NotBlank 校验)
→ PublishTask 实体新增 lemmaDesc 列
→ V3__add_lemma_desc.sql 数据库迁移
→ BaikePageAction.fillDesc() 方法(定位 #J-lemma-desc input)
→ BaikeService 插入 FILL_DESC 步骤
→ 编译通过
从 DTO 到数据库迁移到 Playwright 动作,一条链路没有遗漏。
需求 3:概述至少 10 字 + 提交弹窗选择原因
两个需求同时来。Claude Code 分析 词条创建原因.html 找到弹窗结构:
<div class="bke__modal-save">
<button class="reason-item active">
<span class="reason-item__content">词条未收录</span>
</button>
<button class="bke__modal-save__footer__btn active">提交</button>
</div>
精准写出提交流程:
// 1. 点击编辑器提交
page.locator(".feat-save__action__btn--active").first().click();
// 2. 等待原因选择弹窗
page.waitForSelector(".bke__modal-save");
// 3. 选择"词条未收录"
page.locator(".reason-item")
.filter(new Locator.FilterOptions().setHasText("词条未收录"))
.first().click();
// 4. 点击弹窗内的提交按钮
page.locator(".bke__modal-save__footer__btn.active").first().click();
需求 4:概述填写前先要处理引导弹窗顺序问题
用户反馈:"先选词条类型,确定后才弹出新手引导"。当时 DISMISS_GUIDE 步骤放在了 SELECT_TYPE 之前,顺序错误。
// 调整前(错误顺序)
DISMISS_GUIDE → SELECT_TYPE → FILL_DESC
// 调整后(正确顺序)
SELECT_TYPE → DISMISS_GUIDE → FILL_DESC
一行注释,两行代码移位,重新编译,问题消灭。
需求 5:按 task_no 直接运行已有任务
新增一个 REST endpoint,5行代码:
@PostMapping("/{taskNo}/run")
public ResponseEntity<Map<String, String>> run(@PathVariable String taskNo) {
PublishTask task = taskService.rerun(taskNo); // 重置为 PENDING
baikeService.execute(task.getId()); // 异步触发执行
return ResponseEntity.accepted()
.body(Map.of("taskNo", task.getTaskNo(), "status", "PENDING"));
}
taskService.rerun() 方法之前已经写好只是没暴露出来,Claude Code 直接复用,不重复造轮子。
六、代码清洁度:马克沁不扫平民
有一点值得专门说:Claude Code 不是无差别轰炸,它有工程师的自律。
整个开发过程中,它始终遵守几条不成文的规矩:
只改必要的代码。修 Playwright 选择器,它不会顺手重构 Service 层;加一个字段,它不会把整个 DTO 翻新一遍。改动范围精准到行级别。
每次改完自动验证。每一处修改后,Claude Code 都会主动跑 mvn compile,把编译结果作为下一轮的输入。从不假设"应该能编译过"。
选择器加注释,标注来源。每个 CSS 选择器旁边都有注释,说明它来自哪个 HTML 文件、对应什么 DOM 元素:
/** 编辑器顶部工具栏,出现即表示 React 编辑器已渲染完毕
* (来源:蓝师傅AI智能客服_百度百科.html id=J-editor-top-bar)*/
private static final String EDITOR_TOOLBAR_SELECTOR = "#J-editor-top-bar";
/** 词条创建原因弹窗确认按钮
* (来源:词条创建原因.html class=bke__modal-save__footer__btn active)*/
数据库迁移保持整洁。开发过程中产生了 V2、V3、V4 三个增量迁移文件,Claude Code 在收尾时主动将它们合并回 V1,整个建表语句一目了然,没有零散补丁:
CREATE TABLE publish_task (
...
lemma_type VARCHAR(64) COMMENT '词条类型,如"科技/软件"',
lemma_desc VARCHAR(255) COMMENT '义项名',
lemma_abstract TEXT COMMENT '概述内容,至少10个字',
...
)
这是精准火力,不是地毯式轰炸。
七、最终战果复盘
战役结束,清点战场:
|
维度 |
数据 |
|
核心 Java 类 |
17 个 |
|
REST API 端点 |
8 个 |
|
Playwright 页面动作方法 |
14 个 |
|
自动化步骤(每次任务) |
8 步 |
|
数据库迁移文件 |
1 个(含全部字段) |
|
参考 HTML 文件 |
14 个 |
|
消灭的 Bug 类型 |
循环依赖、Lob 类型校验、选择器解析、企业词条误点、contenteditable 填写、QR 码超时、URL 匹配、弹窗顺序 |
|
预计人工耗时 |
3~5 天 |
|
实际耗时 |
< 4 小时 |
八、给下一场战役的战术手册
从这次开发中提炼出 5 条可复用的实战原则:
原则 1:把错误原文喂给它,不要自己翻译
完整的异常栈 + 原始报错信息,比你"用自己的话描述问题"准确 10 倍。Playwright 的 TimeoutError 直接粘贴,Spring 的启动异常完整复制,不要删减,不要改写。AI 读机器语言比读人类描述更准。
原则 2:把真实 HTML 放进项目目录
前端自动化最大的陷阱是"我以为页面长这样"。真实 HTML 是弹药。把要自动化操作的每一个页面存下来,放进项目,让 AI 自己去 grep 选择器。猜测出来的选择器,十有八九是错的。
原则 3:说结果,不说方案
"提交后页面一直在等,超时了"比"把 waitForURL 的条件改成包含 submitSuccessful"更好。你是指挥官,描述目标和问题现象;AI 是机枪手,它自己知道怎么扫。
原则 4:一次一个清晰任务
Claude Code 的精准来自清晰的任务定义。把多个问题拆开一个一个来,而不是一次性抛出"帮我把所有 bug 都修了"。每次任务的边界越清晰,火力越集中,误伤越少。
原则 5:用编译器做闭环,不要靠感觉
"应该对了吧"是最危险的想法。每次修改后看编译结果,报错继续喂给 Claude Code,形成"修改 → mvn compile → 再修改"的快速闭环。只有编译器通过才算这一波结束,然后继续下一波。
尾声:骑兵时代的终结
1893年之后,没有任何一支骑兵部队愿意正面冲击马克沁阵地。不是因为马塔贝莱战士不够勇猛,更不是因为他们胆小——他们是当时世界上最精锐的骑兵之一。而是因为时代变了,勇猛本身不再是决定胜负的变量。
AI 辅助编程的到来,不是说开发者不再重要。指挥官永远不可或缺。 你需要理解架构,你需要看懂 AI 给出的代码,你需要判断什么时候它的方案是错的,你需要知道整场战役往哪里打。
但那些曾经让人痛苦的"消耗战"——反复调试 CSS 选择器、追循环依赖调用链、翻 Hibernate 迁移文档、写样板式的 CRUD 代码——正在被 Claude Code 这挺马克沁机枪以惊人的速度和精度扫平。
你可以把节省出来的时间,用在真正值得思考的事情上:系统如何扩展到多账号并发?如何接入 AI 生成词条内容?如何监控每日发布成功率?这些才是指挥官该操心的战略问题。
战场还在,但打法已经彻底变了。
本文基于 BaikePublisher 项目的真实开发过程撰写。该项目使用 Spring Boot 3.3 + Playwright 1.44 实现百度百科词条的全自动发布,包含浏览器自动化、Cookie 管理、并发控制、步骤截图存档与完整 REST API。文中所有代码均来自实际生产代码,所有 Bug 均为开发过程中真实遭遇。
:Claude Code 如何用 AI 碾压研发战场&spm=1001.2101.3001.5002&articleId=159583132&d=1&t=3&u=8283ed1fe3dd4d898ca922990ddd106f)
278

被折叠的 条评论
为什么被折叠?



