构建 Halo Publish Tool:用 pi Agent 让 Markdown 一键发布到博客
背景
在上一篇《在 WSL2 中连接宿主机 Windows Chrome 调试端口》中,我们折腾了一大圈——从 Chrome CDP 调试端口、WSL2 网络代理、Python 中继、到 Halo API 的 POST/GET/PUT 三轮调用——最终验证了通过浏览器登录态发布文章的完整链路。
但那个流程太长了。每次都要:
- 把 Markdown 转成 HTML
- 判断内容大小决定分不分块
- 生成 base64
- 分多次
bb_browser_eval执行 - 祈祷编码没出错
这显然不是「方便以后发布个人博客」该有的体验。
所以我把整个链路做成了 pi Agent 的工具包——一个 Skill + 发布脚本 + 自定义 Tool 扩展。现在只需要说一句:
「发布这篇 Markdown」
剩下的全部自动完成。
工具包架构
halo-publish/
├── SKILL.md ← pi 自动发现的 Skill(提供工作流文档)
├── scripts/
│ └── halo-publish.mjs ← 核心发布脚本(Node.js)
└── (extension)
~/.pi/agent/extensions/
└── halo-publish.ts ← 自定义 Tool 扩展(注册 halo_publish 命令)
三个组件各司其职
组件
语言
职责
halo-publish.mjs
Node.js
解析 Markdown → 转 HTML → 生成 Halo API 调用 JS
SKILL.md
Markdown
告诉 pi 如何执行流程、何时分块、注意事项
halo-publish.ts
TypeScript
注册 halo_publish 工具,一键串联全部步骤
核心流程
用户说 "发布这篇 Markdown"
↓
pi 发现 halo-publish Skill + Tool
↓
halo-publish.mjs 解析 Markdown
├─ 提取 frontmatter(title, slug, tags)
├─ 用 Python markdown 库转 HTML
└─ 判断内容大小
↓
├─ 小文章 (<11KB) → 生成单个 JS
│ sessionStorage + API 调用都在一个 eval 里
│
└─ 大文章 (>11KB) → 自动分块
├─ chunk 0.js: 存储内容前半段
├─ chunk 1.js: 存储内容后半段(如果需要)
├─ assemble.js: 拼回完整内容
└─ publish.js: 创建帖子 + 快照 + 发布
↓
bb_browser_eval 在 Halo 标签页执行 JS
└─ 浏览器 Cookie 自动携带认证
↓
Halo API 三轮调用:
1. POST /posts → 创建帖子
2. POST /snapshots → 写入内容
3. GET + PUT /posts/{name} → 发布
关键技术决策
1. 为什么不直接用 Node.js fetch 调 Halo API?
因为 Halo 的认证是 session cookie。直接从 WSL 发请求不带 cookie,需要额外处理认证。而通过浏览器执行 fetch(),cookie 自动携带——这是之前折腾好的基础设施红利。
2. 为什么还要分块?
bb_browser_eval 工具的实际参数长度限制在 ~11KB 左右。对于一篇几千字的文章,Markdown 转成 HTML 后动辄 15-20KB。所以脚本会自动检测:
- 如果生成的 JS < 11KB → 单步搞定
- 如果 > 11KB → 拆成 store(分块) + assemble + publish 三步
用户完全无感知。
3. 为什么用 Python 转 HTML 而不是 Node.js 的 markdown-it?
因为系统里已经有 Python 的 markdown 库(pip install markdown),零依赖。Node.js 那边再装一个 markdown 库需要 npm install,多一个步骤。kill 一个 dead simple 的方案:python3 -c "import markdown; print(markdown.markdown(md))"。
4. 自定义 Tool vs 只用 bash 脚本?
两者都可以。但自定义 Tool 注册为 pi 的 halo_publish 后:
- 参数有类型检查和描述(TypeBox schema)
- 返回结构化结果(不是纯文本)
- 更深的 pi 生态集成
如果不想用扩展,直接执行脚本也完全 OK——Skill 文档里两种方式都写了。
核心代码片段
自动分块逻辑
const MAX_CODE = 11000; // 安全上限
if (htmlStr.length + publishJS.length < MAX_CODE) {
// 单步:store + publish 合并在一个 eval 里
const allJS = storeJS + ';' + publishJS;
} else {
// 分块:根据 MAX_CODE 自动计算每个 chunk 能容纳多少字符
const chunkSize = MAX_CODE - 150; // 留出 setItem 包裹代码的空间
let chunks = [];
for (let i = 0; i < html.length; i += chunkSize) {
chunks.push(html.slice(i, i + chunkSize));
}
// 每个 chunk 生成一个 store JS 文件
// 最后生成 assemble + publish JS 文件
}
Halo API 三轮调用(在浏览器中执行)
// 1. 创建帖子
let post = await fetch('/apis/api.console.halo.run/v1alpha1/posts', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ post: { spec: { title, slug, tags, ... } } })
}).then(r => r.json());
// 2. 创建快照(写入内容)
let snap = await fetch('/apis/content.halo.run/v1alpha1/snapshots', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
spec: { rawType:'HTML', rawPatch: html, contentPatch: html, ... }
})
}).then(r => r.json());
// 3. 获取当前状态 + 修改为已发布
let current = await fetch('/apis/content.halo.run/v1alpha1/posts/' + postName)
.then(r => r.json());
current.spec.headSnapshot = snapName;
current.spec.baseSnapshot = snapName;
current.spec.releaseSnapshot = snapName;
current.spec.publish = true;
await fetch('/apis/content.halo.run/v1alpha1/posts/' + postName, {
method:'PUT', headers:{'Content-Type':'application/json'},
body: JSON.stringify(current)
}).then(r => r.json());
使用演示
小文章:一句话搞定
用户:发布这篇 hello.md
AI (through halo_publish tool):
✅ 解析 Markdown frontmatter(title: Hello World)
✅ 转为 HTML(312 bytes)
✅ 单步执行(JS 2.3KB)
✅ SUCCESS: Published!
大文章:自动分两步,用户无感知
用户:发布这篇 long-article.md
AI:
✅ 内容 16KB,自动分 2 块
Step 1: store chunk 0 (10KB) ✓
Step 2: store chunk 1 (6KB) ✓
Step 3: assemble (212 bytes) ✓
Step 4: publish (2.5KB) ✓
✅ SUCCESS: Published!
相对上一篇的改进
方面
上一篇(手动)
现在(自动化)
编码
base64 手工分块
自动转 HTML + 自动分块
步骤
6-7 步
1 步或自动
大小判断
人工
脚本自动判断
错误处理
靠肉眼看
脚本返回明确状态
复用性
临时脚本
Skill + Tool 可复用
frontmatter
手动提取
自动解析 title/slug/tags
总结
这个工具包的价值不在于技术难度——Halo API 的流程之前已经摸清了。而在于把一次性的复杂操作封装成可复用的能力。
pi Agent 的 Skill + Extension 机制非常适合做这种事:
- Skill 提供文档上下文,让 AI 知道该怎么做
- Script 做脏活累活(Markdown 转换、JS 生成、分块计算)
- Extension 注册为工具,让 AI 能一键调用
现在,发博客的流程变成了:
写 Markdown → 告诉 pi "发布这篇" → done
完整的代码和文档在:
- Skill: ~/.pi/agent/skills/halo-publish/SKILL.md
- 脚本: ~/.pi/agent/skills/halo-publish/scripts/halo-publish.mjs
- 扩展: ~/.pi/agent/extensions/halo-publish.ts