无尘阁日记

无尘阁日记

让 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">&quot;form&quot;</span>)<span class="op">.</span><span class="fu">attr</span>(<span class="st">&quot;action&quot;</span><span class="op">,</span><span class="st">&quot;cmd.php?act=verify&quot;</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">&quot;#password&quot;</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">&quot;#username&quot;</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">&lt;!-- /zb_system/admin/index.php 里的 meta 标签 --&gt;</span></span> <span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">meta</span><span class="ot"> name</span><span class="op">=</span><span class="st">&quot;csrfToken&quot;</span><span class="ot"> content</span><span class="op">=</span><span class="st">&quot;d73e305e9608258a48c9818d5b99b638&quot;</span><span class="dt">&gt;</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">&lt;</span><span class="kw">option</span><span class="ot"> value</span><span class="op">=</span><span class="st">&quot;0&quot;</span><span class="dt">&gt;</span>公开<span class="dt">&lt;/</span><span class="kw">option</span><span class="dt">&gt;</span></span> <span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">option</span><span class="ot"> value</span><span class="op">=</span><span class="st">&quot;1&quot;</span><span class="dt">&gt;</span>草稿<span class="dt">&lt;/</span><span class="kw">option</span><span class="dt">&gt;</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>&lt;h1&gt;</code>、<code>&lt;p&gt;&lt;strong&gt;</code>、<code>&lt;ul&gt;</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&amp;time=m # 下载验证码 POST /zb_system/cmd.php?act=verify 字段: csrfToken, username, password(MD5), verify, savedate 【发布】 1. get_categories() # 拉全量分类, 大模型选出正确分类 2. pandoc md-&gt;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&amp;id=X&amp;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>