用 AI Agent 自动发布华为云博客:从踩坑到成功的完整实战
本文记录了使用华为云码道(CodeArts)代码智能体,通过 Playwright 浏览器自动化 + API 逆向分析,实现博客自动发布的完整过程。包含 8 个关键问题的排查与修复,供开发者参考。
背景
华为云开发者成长中心提供了"发布文章"成长任务,完成可获得 +60 积分。但手动写博客、排版、发布流程繁琐。我们希望用 AI Agent 全自动完成:生成文章内容 → 打开编辑器 → 填写标题/正文/标签 → 点击发表。
听起来简单,实际踩坑无数。以下是完整实战记录。
整体流程
自动发布博客的流程分为以下步骤:
- 扫码登录:通过 Playwright 弹出浏览器,用户华为云 APP 扫码,Cookie 自动保存
- 打开写文章页面:访问博客编辑器 URL
- 填写标题:通过 Playwright
fillAPI 输入标题 - 填写正文:通过 TinyMCE 编辑器 API 设置 HTML 内容
- 选择标签:通过 tagator 插件交互选择标签
- 点击发表:调用博客发布 API
踩坑记录
坑1:写文章页面 URL 找不到
问题:尝试了多个 URL 均返回 404:
/blogs/post→ 404/community/writeblog→ 404/blogs/write→ 404
修复:通过搜索华为云社区页面,最终找到正确 URL:
https://bbs.huaweicloud.cn/blogs/article
坑2:TinyMCE 编辑器内容设置后不生效
问题:使用 Playwright 直接操作 iframe 内的 contenteditable 区域失败,因为 TinyMCE 编辑器有独立的事件系统。
修复:使用 TinyMCE 官方 API 设置内容,并触发事件通知编辑器内部状态更新:
tinymce.activeEditor.setContent(htmlContent);
tinymce.activeEditor.fire('change');
tinymce.activeEditor.fire('input');
tinymce.activeEditor.save();
坑3:标签选择器 tagator 插件选错标签
问题:通过 tagator 输入框输入搜索词"华为云码道"后,点击下拉选项,但实际选中的标签变成了"ABAP",而非目标标签。
原因:tagator 下拉选项的 DOM 元素可能包含 1000+ 个选项(全量标签),部分选项不可见但仍然存在于 DOM 中,导致点击了错误的元素。
修复:放弃通过 tagator UI 交互选择标签,改为直接设置 #blogLable input 的值,并重新初始化 tagator 插件:
const blogLabel = document.getElementById('blogLable');
blogLabel.value = '华为云码道(CodeArts)代码智能体';
blogLabel.dispatchEvent(new Event('change', { bubbles: true }));
$(blogLabel).tagator({ autocomplete: allInfoArray, useDimmer: true });
坑4:发表按钮始终禁用
问题:发表按钮 #submit-btn 始终带有 write-blog-publish-btn-no 禁用样式类,即使标题、内容、标签都已填写完毕。
排查过程:
- 检查按钮的
onclick属性 → 无 - 检查 jQuery 事件绑定 →
$._data(btn, "events")返回空 - 尝试调用
window.saveBlog()→ 报错Cannot read properties of undefined (reading 'isLock') - 手动移除禁用样式类并点击 → 无效,按钮点击没有触发任何请求
根本原因:编辑器内部验证逻辑无法感知通过 API 设置的内容和标签,导致按钮永远处于禁用状态。
修复:绕过前端 UI,直接调用后端 API 发布博客(见坑5)。
坑5:博客发布 API 端点和字段名
问题:不知道博客发布的 API 端点和请求体格式。
排查过程:
- 通过
saveBlog.toString()获取函数源码,发现发布 API 端点为:
/rest/developer/fwdu/rest/developer/user/hdblogservice/v1/blog/blogging
- 搜索页面 script 标签,找到
submitData的完整构造逻辑:
var blogData = {};
blogData.tagNameData = tagList.toString();
blogData.titleData = $("#blogTitle").val();
blogData.introData = blog_Summry;
blogData.contentData = editorContent.toString();
blogData.markdownContent = '...';
blogData.imageUrl = [];
关键发现:API 字段名不是 title/content/label,而是 titleData/contentData/tagNameData。第一次调用时使用了错误的字段名 label,虽然 API 返回成功,但博客页面显示"Render Error"。
坑6:博客页面显示"Render Error"
问题:使用 label 字段(而非 tagNameData)调用 API 后,API 返回成功,但打开博客页面显示"Render Error: "。
原因:API 调用缺少 introData(摘要)和 tagNameData(标签名)两个必填字段。
修复:补全所有必填字段后重新调用:
const submitData = {
titleData: title,
contentData: contentHtml,
introData: plainText.substring(0, 200), // 摘要
tagNameData: tagLabel, // 标签名
isLock: 0,
blogResource: 1, // 原创
isReship: 1, // 允许转载
disclaimerFlag: 1,
blogType: 0,
isOriginal: 1,
source: 1,
};
坑7:Markdown 转 HTML 格式错乱
问题:博客发布后,代码块显示为单反引号、列表没有格式化、表格显示为纯文本、标题未渲染。
原因:最初的 Markdown 转 HTML 使用简单的正则替换,无法正确处理代码块、列表、表格等复杂结构。
修复:重写 Markdown 转 HTML 解析器,逐行解析,支持完整的 Markdown 语法:
- 代码块(
`language)→ 无序列表(- item)→- 有序列表(
1. item)→- 表格(
| a | b |)→ - 引用(
> text)→ - 标题(
## text)→ - 行内格式(
bold、`code)→、` -
坑8:Windows 换行符导致标题不渲染问题:重写 Markdown 解析器后,标题仍然没有被转换为标签,而是显示为## 背景
。原因:Windows 环境下文件使用
\r\n换行符,split('\n')后每行末尾残留\r,导致正则表达式/^(#{1,6})\s+(.+)$/中的$无法正确匹配行尾。修复:在解析前统一处理换行符:
const lines = md.replace(/\r\n/g, '\n').replace(/\r/g, '').split('\n');最终方案
完整的自动发布博客流程如下:
- 通过 Playwright 打开写文章页面,注入登录 Cookie
- 使用 Playwright
fillAPI 填写标题 - 使用 TinyMCE API
setContent填写正文(Markdown 预先转为 HTML) - 触发编辑器
blur事件让系统自动提取标签,或手动设置标签值 - 获取 CSRF Token(通过
/api/get-ainfo接口) - 构造完整的
submitData,直接调用/blog/bloggingAPI 发布 - 解析返回的
blogId,验证发布结果
关键代码片段
Markdown 转 HTML 解析器核心逻辑
function mdToHtml(md) { const lines = md.replace(/\r\n/g, '\n').replace(/\r/g, '').split('\n'); let html = ''; let i = 0; while (i < lines.length) { const line = lines[i].trimEnd(); // 代码块 if (line.startsWith('```')) { /* ... */ } // 标题 const hMatch = line.match(/^(#{1,6})\s+(.+)$/); if (hMatch) { html += `<h${hMatch[1].length}>${inlineFormat(hMatch[2])}</h${hMatch[1].length}>`; i++; continue; } // 列表、表格、引用、段落... } return html; }直接调用发布 API
const resp = await fetch( devdataDomain + '/rest/developer/fwdu/rest/developer/user/hdblogservice/v1/blog/blogging', { method: 'POST', headers: { 'Content-Type': 'application/json', 'csrf': csrfToken }, body: JSON.stringify(submitData), credentials: 'include', } );经验总结
- 前端 UI 自动化有上限:当 UI 组件内部状态与 DOM 不同步时(如 TinyMCE、tagator),直接操作 DOM 往往无效。此时应转向 API 层面解决问题。
- 逆向分析 JS 源码是关键:通过
function.toString()获取函数源码,通过搜索
- 点赞
- 收藏
- 关注作者
评论(0)