让 AI 帮我发博客:一条只有踩过坑才写得出来的封装实录
2026-08-04
<blockquote>
<p>需求很简单:以后我可以在本机任意一个 agent 会话里说一句”把这个发到博客上”,文章就自动发到 jiangtian.top。 但真做起来,从”能登上后台”到”稳定发一篇格式正确、分类正确的文章”,藏着的坑远比想象多。这是一篇不含滤镜的真实排障记录。</p>
</blockquote>
<h2 id="一先说结论">一、先说结论</h2>
<p>我用 OpenClaw 做了一个叫 <code>blog-publish</code> 的 skill,放在本机 <code>~/.agents/skills/blog-publish/</code> 下。</p>
<p>以后无论哪个 agent 会话,只要用户说”发文章到博客”,它就会自动: 1. 登录后台(自动识别图形验证码,失败自动重试) 2. <strong>自动把 Markdown 转成 HTML</strong>(因为 ZBlog 编辑器是”HTML / 可视化”双模切换,传裸 Markdown 会原样显示成 <code># 标题</code> 这种源码) 3. <strong>拉取全量分类,让大模型判断内容归属,选正确分类</strong> 4. 返回文章链接</p>
<p>我发了好几篇<strong>草稿</strong>反复验证格式和分类,确认无误后才敢上线。这篇就是用它自己发出来的。</p>
<h2 id="二技术栈侦察先搞清楚对手是谁">二、技术栈侦察:先搞清楚对手是谁</h2>
<p>博客 <code>jiangtian.top</code> 用的是 <strong>Z-BlogPHP 1.7.3</strong>(nginx 服务器)。第一步不是写代码,而是摸清它的登录和发布机制。用 <code>curl</code> 一通探测:</p>
<ul>
<li>后台登录页:<code>/zb_system/login.php</code>,正常</li>
<li>发布入口:<code>/zb_system/cmd.php</code>,未登录时返回 500</li>
<li><code>xmlrpc.php</code>:返回的竟然是首页 HTML —— <strong>说明这个 ZBlog 没启用 XML-RPC 扩展</strong>,想走 WordPress 兼容 API 的路直接堵死</li>
</ul>
<p>所以只能走<strong>原生 cmd.php 接口</strong>,这就牵扯出好几个硬骨头。</p>
<h2 id="三踩坑实录这才是干货">三、踩坑实录(这才是干货)</h2>
<h3 id="坑-1登录-post-到哪密码还要-md5">坑 1:登录 POST 到哪?密码还要 MD5</h3>
<p>一开始按直觉 POST 到 <code>login.php</code>,一直 500。抓了登录页的 JS 才看清:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">$</span>(<span class="st">"form"</span>)<span class="op">.</span><span class="fu">attr</span>(<span class="st">"action"</span><span class="op">,</span><span class="st">"cmd.php?act=verify"</span>)<span class="op">;</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="fu">$</span>(<span class="st">"#password"</span>)<span class="op">.</span><span class="fu">val</span>(<span class="fu">MD5</span>(strPassWord))<span class="op">;</span> <span class="co">// 密码要 MD5</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="fu">$</span>(<span class="st">"#username"</span>)<span class="op">.</span><span class="fu">val</span>(strUserName)<span class="op">;</span></span></code></pre></div>
<p>真相: - 登录不是提交给 <code>login.php</code>,而是 <code>cmd.php?act=verify</code> - 密码<strong>要先 MD5 加密</strong>,填到隐藏域 <code>password</code> - 用户名填到隐藏域 <code>username</code></p>
<h3 id="坑-2csrftoken-有两个用错一个就非法访问">坑 2:csrfToken 有两个,用错一个就”非法访问”</h3>
<p>这是最大的坑。登录页会给你一个 <code>csrfToken</code>(藏在隐藏 input 里)。但<strong>登录之后</strong>,你再想发布,用回这个 token 就报”非法访问”。</p>
<p>正确的 token 在<strong>后台首页</strong>里:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co"><!-- /zb_system/admin/index.php 里的 meta 标签 --></span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="dt"><</span><span class="kw">meta</span><span class="ot"> name</span><span class="op">=</span><span class="st">"csrfToken"</span><span class="ot"> content</span><span class="op">=</span><span class="st">"d73e305e9608258a48c9818d5b99b638"</span><span class="dt">></span></span></code></pre></div>
<p>注意它是 <code>content=</code> 而不是 <code>value=</code>,正则要先匹配对。我的做法:<strong>登录成功后,从后台首页 meta 标签重新取 token</strong>,用它来发布。这一步修完,发布立刻从”非法访问”变成”成功跳转”。</p>
<h3 id="坑-3zblog-的-status-语义和你想的相反">坑 3:ZBlog 的 Status 语义和你想的相反</h3>
<p>发布要传 <code>Status</code>。我的第一反应是 <code>1=发布, 0=草稿</code>。结果用 <code>status="draft"</code> 传 <code>Status:0</code> 发出去的竟是<strong>公开</strong>文章!</p>
<p>去后台文章管理页看了一眼下拉框才明白:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="dt"><</span><span class="kw">option</span><span class="ot"> value</span><span class="op">=</span><span class="st">"0"</span><span class="dt">></span>公开<span class="dt"></</span><span class="kw">option</span><span class="dt">></span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="dt"><</span><span class="kw">option</span><span class="ot"> value</span><span class="op">=</span><span class="st">"1"</span><span class="dt">></span>草稿<span class="dt"></</span><span class="kw">option</span><span class="dt">></span></span></code></pre></div>
<p><strong>ZBlog 的 Status:0=公开(已发布),1=草稿</strong>,正好反了。这个不发现,以后每次”存草稿”都会误发成公开文章。</p>
<h3 id="坑-4正文必须是-html不是-markdown">坑 4:正文必须是 HTML,不是 Markdown</h3>
<p>ZBlog 后台编辑器的正文是一个 <strong>“HTML / 可视化”双模切换的富文本区域</strong>——它存的是 HTML,可视化模式是拿这段 HTML 渲染出来的。</p>
<p>如果你把 Markdown 源码(<code># 标题</code>、<code>**加粗**</code>)直接塞进 Content 字段: - 可视化模式下会看到一堆 <code>#</code>、<code>**</code> 星号,格式全乱 - “HTML 模式”里倒是能看到源码,但那不是人看的</p>
<p><strong>解决办法</strong>:发布前用 pandoc 把 Markdown 转成标准 HTML:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">pandoc</span> <span class="at">-f</span> markdown <span class="at">-t</span> html <span class="at">--wrap</span><span class="op">=</span>none input.md</span></code></pre></div>
<p>得到 <code><h1></code>、<code><p><strong></code>、<code><ul></code> 这种规范结构,ZBlog 就能正确渲染成带格式的文章。这一步我一开始也漏了,导致第一版发出去的文章在可视化模式下是乱的。</p>
<h3 id="坑-5图形验证码纯-ocr-不够得多引擎投票-重试">坑 5:图形验证码,纯 OCR 不够,得”多引擎投票 + 重试”</h3>
<p>ZBlog 登录要图形验证码:5 位字符(大小写字母+数字混合),背景/字符颜色随机,还有斜向干扰线,JPEG 压缩还严重。</p>
<p>先测各种 OCR: - tesseract:对这张图”顾此失彼”,不同参数下时而准时而空 - macOS Vision:部分图完美,部分图空或串符号</p>
<p><strong>结论:没有任何单一引擎能稳定全对。</strong></p>
<p>最终方案是<strong>多引擎投票 + 自动重试循环</strong>: - 同时跑 tesseract(多个分段模式)和 Vision,收集候选 - 取出现次数最多的候选 - 验证码错了就<strong>换一张新的、重新识别、重新登录</strong>,直到成功</p>
<p>实测登录通常 1~3 次尝试搞定,最多 10 来次内必成。ZBlog 这里<strong>没有防爆破次数限制</strong>(连试 7 次错误都不锁),所以重试策略完全可行。</p>
<h3 id="坑-6分类不能硬编码要拉全量让大模型选">坑 6:分类不能硬编码,要拉全量让大模型选</h3>
<p>第一版我图省事,把”技术笔记”硬编码成 <code>category=3</code> 传进去。这有两个问题: 1. 分类 ID 是站点相关的,换个博客就全对不上 2. 内容归属应该由理解语义的模型来判断,而不是写死</p>
<p><strong>正确做法</strong>:发布前先调用 <code>get_categories()</code> 把全量分类(我这边有 60 多个)拉下来,然后让大模型读文章内容、判断它最该进哪个分类,再传对应 ID。这样”这篇技术文章进技术笔记、那篇读书心得进读书笔记”,才是真智能。</p>
<h2 id="四最终发布的协议存个档">四、最终发布的协议(存个档)</h2>
<p>理顺之后,完整调用协议如下:</p>
<pre><code>【登录】
GET /zb_system/login.php # 取 csrfToken 和会话 cookie
GET /zb_system/script/c_validcode.php?id=login&time=m # 下载验证码
POST /zb_system/cmd.php?act=verify
字段: csrfToken, username, password(MD5), verify, savedate
【发布】
1. get_categories() # 拉全量分类, 大模型选出正确分类
2. pandoc md->html # 正文转 HTML
3. GET /zb_system/admin/index.php # 重新取后台 csrfToken (meta标签)
4. POST /zb_system/cmd.php?act=ArticlePst
字段: ID=0(新建), Title, Content(HTML), Cate_ID, Tag, Status(0公开/1草稿),
Alias, Intro, csrfToken
【删除】
GET /zb_system/cmd.php?act=ArticleDel&id=X&csrfToken=Y</code></pre>
<h2 id="五几个值得记住的通用经验">五、几个值得记住的通用经验</h2>
<ol type="1">
<li><strong>动手前先”侦察”</strong>:用 curl 看响应头、抓 JS、看表单字段,搞清楚生态再写代码。你以为的”标准接口”往往是错的。</li>
<li><strong>500 不一定是服务器挂了</strong>:ZBlog 用”HTTP 500 + 错误页”来优雅表达”验证码错误”这类业务失败。别被状态码吓到,去看响应体。</li>
<li><strong>登录成功 ≠ 能发文章</strong>:cookie 有了、后台进了,但发布用错 csrfToken 照样”非法访问”。每种操作的鉴权上下文要分开测。</li>
<li><strong>富文本编辑器一律收 HTML</strong>:凡是”HTML/可视化”切换的编辑器,Content 字段只认 HTML,别图省事塞 Markdown。</li>
<li><strong>字段语义务必实测</strong>:Status 的 0/1 我完全想反了。<strong>一定要发一篇草稿实测</strong>,别轻信直觉。</li>
<li><strong>分类让模型选,别硬编码</strong>:拉全量分类 → 大模型判断归属 → 提交,可迁移、更智能。</li>
<li><strong>测试垃圾随手清理</strong>:我发的那几篇测试文章最后都删掉了(404 确认),没给博客留脏数据。</li>
</ol>
<h2 id="六成果">六、成果</h2>
<p>以后我从任何 agent 会话里说一句”把这篇发到技术笔记”,文章就会自动以正确的 HTML 格式、正确的分类落到 jiangtian.top。这篇就是用它自己发出来的第一篇文章。</p>
<p>(注:中途所有带”测试”的草稿均已删除,无残留。)</p>
<hr />
<p><em>如果你也在折腾 ZBlog / 自建博客的自动化,希望这些坑能帮你少走弯路。</em></p>
发表评论: