Halo 博客 API 自动化完全指南:从零到发布的实战教程
Halo 是一款功能强大的开源博客系统,提供了丰富的 API 接口。本文将详细介绍如何通过 API 实现文章的自动化创建、更新和发布,包含完整的代码示例和踩坑记录。
目录
环境准备
| 项目 | 说明 |
|---|---|
| Halo 版本 | 2.22+(推荐 2.22.14) |
| 认证方式 | PAT(Personal Access Token)或 Basic Auth |
| 开发语言 | Python 3.10+ |
| 依赖库 | requests |
注意: Halo 2.20+ 默认关闭了 Basic Auth,强烈推荐使用 PAT 进行认证。
认证方式选择
PAT 认证(推荐)
在 Halo 后台 → 个人中心 → 个人令牌 中创建 PAT,权限选择「全部」。
import os
def read_env_var(name):
"""从 .env 文件逐行读取变量,避免 shell 截断长 token"""
env_path = os.path.expanduser("~/.hermes/.env")
with open(env_path, "r") as f:
for line in f:
line = line.strip()
if line.startswith(f"{name}=") and not line.startswith("#"):
return line.split("=", 1)[1].strip()
raise ValueError(f"{name} not found in .env")
HALO_TOKEN = read_env_var("HALO_API_TOKEN")
HALO_BASE = read_env_var("HALO_BASE_URL").rstrip("/")
HEADERS = {
"Authorization": f"Bearer {HALO_TOKEN}",
"Content-Type": "application/json",
}
Basic Auth(备选)
如果不想用 PAT,也可以用 Basic Auth,但需要在 Halo 配置中手动开启:
import base64
credentials = base64.b64encode(b"username:password").decode()
HEADERS = {
"Authorization": f"Basic {credentials}",
"Content-Type": "application/json",
}
⚠️ 关键陷阱
PAT token 通常非常长(2000+ 字符)。不要用 os.environ.get() 或 grep 来读取——shell 会截断长 token 导致认证失败。必须用 Python 逐行读取 .env 文件。
核心 API 端点一览
Halo 2.22+ 使用 User-center(UC)API,所有操作都以 /apis/uc.api.content.halo.run/v1alpha1/posts 为基础路径。
| 操作 | 方法 | 路径 |
|---|---|---|
| 获取文章列表 | GET |
/apis/uc.api.content.halo.run/v1alpha1/posts |
| 获取单篇文章 | GET |
/apis/uc.api.content.halo.run/v1alpha1/posts/{name} |
| 创建文章 | POST |
/apis/uc.api.content.halo.run/v1alpha1/posts |
| 获取草稿内容 | GET |
/posts/{name}/draft?patched=true |
| 更新草稿 | PUT |
/posts/{name}/draft |
| 发布文章 | PUT |
/posts/{name}/publish |
| 取消发布 | PUT |
/posts/{name}/unpublish |
| 删除文章 | PUT |
/posts/{name}/recycle |
完整工作流详解
正确的文章创建流程分为四步:创建 → 获取草稿 → 更新内容 → 发布。
第一步:创建文章(POST)
创建文章时必须包含完整的 spec 字段和 content-json annotations:
import json
import hashlib
from datetime import datetime
def generate_slug(title):
"""根据标题生成 URL 友好的 slug"""
ts = datetime.now().strftime("%Y%m%d%H%M")
h = hashlib.md5(title.encode()).hexdigest()[:6]
en_parts = re.findall(r'[a-zA-Z0-9]+', title)
if en_parts:
slug = "-".join(en_parts[:5]).lower()
if len(slug) > 5:
return f"{slug}-{h[:4]}"
return f"post-{ts}-{h[:6]}"
def create_post(title, markdown, categories=None, tags=None):
"""创建文章草稿"""
slug = generate_slug(title)
content_json = json.dumps({
"content": "",
"raw": markdown,
"rawType": "markdown"
}, ensure_ascii=False)
payload = {
"apiVersion": "content.halo.run/v1alpha1",
"kind": "Post",
"metadata": {
"name": slug,
"annotations": {
"content.halo.run/preferred-editor": "default",
"content.halo.run/content-json": content_json,
}
},
"spec": {
"title": title,
"slug": slug,
"excerpt": {"autoGenerate": True, "raw": ""},
"cover": "",
"pinned": False,
"allowComment": True,
"publish": False,
"visible": "PUBLIC",
"categories": categories or [],
"tags": tags or [],
"owner": "",
},
}
resp = requests.post(
f"{HALO_BASE}/apis/uc.api.content.halo.run/v1alpha1/posts",
headers=HEADERS, json=payload, timeout=30
)
resp.raise_for_status()
return resp.json()
第二步:获取草稿内容
创建成功后,需要获取草稿以拿到完整的元数据:
def get_draft(post_name):
"""获取文章草稿"""
resp = requests.get(
f"{HALO_BASE}/posts/{post_name}/draft?patched=true",
headers=HEADERS, timeout=30
)
resp.raise_for_status()
return resp.json()
第三步:更新草稿内容
这是最关键的一步! 必须同时更新三个字段,否则前台不会显示新内容:
def update_draft(post_name, draft, markdown):
"""更新草稿内容 —— 必须同时更新三个字段!"""
content_json = json.dumps({
"content": "",
"raw": markdown,
"rawType": "markdown"
}, ensure_ascii=False)
# 修改 annotations 中的 content-json
draft["metadata"]["annotations"]["content.halo.run/content-json"] = content_json
# 修改 spec 中的 raw 和 content
draft["spec"]["raw"] = markdown
draft["spec"]["content"] = markdown # 这个也要更新!
resp = requests.put(
f"{HALO_BASE}/posts/{post_name}/draft",
headers=HEADERS, json=draft, timeout=30
)
resp.raise_for_status()
return resp.json()
第四步:发布文章
def publish_post(post_name):
"""发布文章"""
resp = requests.put(
f"{HALO_BASE}/apis/uc.api.content.halo.run/v1alpha1/posts/{post_name}/publish",
headers=HEADERS, timeout=30
)
resp.raise_for_status()
return resp.json()
常见问题与排坑
问题 1:POST 返回 500 错误
错误信息: java.lang.NullPointerException: content is null
原因: 缺少 content-json annotations 或 spec 字段不完整。
解决: 确保创建时包含完整的 spec 和 content.halo.run/content-json annotation。
问题 2:更新后前台不显示新内容
原因: 只更新了 content-json,没有同步更新 patched-content 和 patched-raw。
解决: 更新草稿时必须同时修改三个位置:
- annotations.content.halo.run/content-json
- spec.raw
- spec.content
问题 3:删除文章返回 403
原因: /recycle 接口需要管理员权限,PAT 可能权限不足。
解决: 使用软删除方式,设置 spec.deleted = true,或在后台手动删除。
问题 4:创建文章时报 slug 冲突
原因: 同名 slug 已存在。
解决: 在 slug 中加入时间戳或 hash 后缀,确保唯一性。
实战代码示例
将上面的函数组合成一个完整的发布脚本:
#!/usr/bin/env python3
"""Halo 博客一键发布脚本"""
import requests
import json
import re
import hashlib
import os
from datetime import datetime
# ...(上面的函数定义)
def main():
title = "我的第一篇自动化文章"
markdown = """
## Hello World
这是一篇通过 API 自动发布的文章。
```python
print("Hello from Halo API!")
自动化发布让博客管理更轻松。 """
# 1. 创建草稿
post = create_post(title, markdown, categories=["halo-blog"])
post_name = post["metadata"]["name"]
print(f"Draft created: {post_name}")
# 2. 获取草稿
draft = get_draft(post_name)
# 3. 更新内容
update_draft(post_name, draft, markdown)
print("Content updated")
# 4. 发布
publish_post(post_name)
permalink = post.get("status", {}).get("permalink", "")
print(f"Published: {permalink}")
if __name__ == "__main__": main() ```
参考资源
- Halo 官方文档 — 最权威的 API 参考
- Halo API 规范 — 完整的 API Schema
- obsidian-halo 插件源码 — 学习 API 调用的最佳范例
通过本文的介绍,你应该已经掌握了 Halo 博客 API 自动化的核心流程。关键在于理解四步工作流(创建 → 获取 → 更新 → 发布)以及更新时必须同步三个字段的注意事项。祝你的博客自动化之路顺利!