用 Claude Code 打造博客文章发布 Skill 的实战记录
背景
我的博客是 Nuxt 3 + FastAPI 的全栈项目,数据库用 SQLite,部署在一台韩国首尔的阿里云 ECS 上。平时发文章都是打开后台管理界面手动填写标题、选分类、贴标签、粘贴 Markdown 内容,流程比较繁琐。
自从开始用 Claude Code 作为日常开发工具后,一直想实现一个场景:我丢一段正文给 Agent,它自己拟标题、选分类标签、调 API 发布,我确认一下就行。正好博客项目里已经有完整的 RESTful API,于是决定动手做一个 Skill。
这篇文章完整记录了从需求分析到最终可用的全过程,包括踩过的坑和设计上的迭代。
第一步:摸清 API
做 Skill 之前,先得搞清楚博客有哪些接口可以用。直接让 Claude Code 去读后端源码,它几分钟就梳理出来了。
认证流程
博客后台接口用 JWT 认证,流程是:
POST /api/auth/login
Body: {"username": "leap", "password": "xxx"}
Response: {"access_token": "eyJhbGciOiJIUzI1NiIs..."}
拿到 token 后,后续所有请求带上 Authorization: Bearer <token> 头。token 有效期 7 天(在 config.py 里配置的 ACCESS_TOKEN_EXPIRE_DAYS = 7)。
文章相关接口
关键接口只有一个:
POST /api/admin/articles
Headers: Authorization: Bearer <token>, Content-Type: application/json
请求体对应 ArticleCreate schema,字段如下:
class ArticleCreate(ArticleBase):
title: str # 必填
slug: str # 必填,URL 标识
content_md: str # 必填,Markdown 正文
summary: str = "" # 可选,摘要
cover_image: str = "" # 可选,封面图
category_id: int = None # 可选,分类 ID
is_published: bool = False # 是否发布
is_top: bool = False # 是否置顶
top_order: int = 0 # 置顶排序
allow_comment: bool = True # 是否允许评论
meta: dict = {} # 扩展元数据
tag_ids: list[int] = [] # 标签 ID 列表
created_at: str = None # 自定义创建时间
updated_at: str = None # 自定义更新时间
published_at: str = None # 自定义发布时间
辅助接口
还需要两个查询接口来获取分类和标签:
GET /api/categories → 返回分类列表
GET /api/tags → 返回标签列表
这两个是公开接口,不需要认证。
参考已有的 pan Skill
项目里已经有一个 pan Skill(网盘上传),它的认证模式是:把账号密码写死在脚本里,每次调用时自动登录获取 token,内存缓存 7 天。直接复用这个思路,但把硬编码的账号密码改成从 env 文件读取。
第二步:设计 Skill 结构
参考 pan Skill 的目录结构,article Skill 也是三个文件:
article/
├── env # 配置文件(地址 + 账号密码)
├── SKILL.md # Skill 说明文档(Agent 读这个来知道怎么用)
└── scripts/
└── publish_article.py # 主脚本
env 文件
BLOG_URL=https://vue2.xyz
BLOG_USERNAME=leap
BLOG_PASSWORD=xxx
为什么用 env 文件而不是环境变量?因为 Skill 可能在不同机器上用,env 文件跟着 Skill 目录走,不需要额外配置系统环境变量。同时支持环境变量覆盖,方便 CI 场景。
脚本核心逻辑
脚本的主要流程:
# 1. 加载配置
cfg = load_env() # 从 env 文件读取,环境变量可覆盖
# 2. 登录获取 token(带缓存)
def login():
# 检查内存缓存是否有效(提前 5 分钟刷新)
if _token_cache["token"] and _token_cache["expire"] > now + 300:
return _token_cache["token"]
# 调用登录接口
resp = urllib.request.urlopen(login_url, data)
_token_cache["token"] = resp["access_token"]
return _token_cache["token"]
# 3. 解析 Markdown frontmatter
def parse_frontmatter(content):
# 支持 --- 包裹的 YAML 头部
# 提取 title, slug, summary, category, tags 等字段
# 返回 (metadata_dict, body)
# 4. 调用 API 创建文章
def create_article(payload):
token = login()
req = urllib.request.Request(url, data=json.dumps(payload), ...)
return json.loads(urllib.request.urlopen(req).read())
slug 自动生成
用户不一定提供 slug,需要从标题自动生成:
def slugify(text):
text = text.lower().strip()
text = re.sub(r'[^\w一-鿿\s-]', '', text) # 保留中文和字母数字
text = re.sub(r'\s+', '-', text) # 空格转连字符
text = re.sub(r'-+', '-', text).strip('-') # 去重连字符
return text or "untitled"
中文标题也能处理,比如「运维工程师面试备战全攻略」会生成 运维工程师面试备战全攻略(中文保留在 slug 中,浏览器能正常访问)。
第三步:设计交互模式
这是整个 Skill 设计里最关键的决策:Agent 拿到文章后该怎么处理?
初版方案:直接发布
一开始想的是 Agent 直接调 API 发布就行。但实际测试后发现几个问题:
- 用户可能只丢一段正文,连标题都没有
- 分类和标签是博客里预设的,Agent 不知道有哪些选项
- 如果信息不完整,脚本报错退出,Agent 拿到错误信息不知道该怎么办
迭代后的方案:四步流程
最终设计了一个四步流程:
1. --list → 获取博客现有分类和标签
2. 读文章内容 → Agent 自动生成标题、slug、摘要、分类、标签
3. 展示给用户 → "标题xxx,分类xxx,标签xxx,你看看有没有要改的?"
4. --dry-run → 预检信息完整性 → 正式发布
这个流程的核心思想是:Agent 负责生成,用户负责确认。不是让用户从零填写,而是让 Agent 基于已有数据推荐,用户只需要说「可以」或「改一下」。
SKILL.md 的作用
SKILL.md 是 Agent 的使用手册,定义了工作流和规则。写这个文档的关键是:不要假设 Agent 会「聪明地」处理各种情况,要把每一步都写清楚。
比如「生成元数据」这一步,明确写出了:
- slug 要英文小写+短横线,如
fastapi-async-guide - 摘要 1-2 句话,100 字以内
- 标题 10-30 字,体现核心主题
- 分类选最核心的一个,都不匹配则建议创建新分类
- 标签选多个相关的,覆盖文章涉及的主题
这些规则不是一次写好的,是测试了几轮后逐步补充的。
第四步:处理缺失信息
实际测试中发现一个问题:如果用户没给分类和标签,脚本直接报错退出,Agent 拿到错误信息不知道该怎么办。
方案:--dry-run 预检模式
加了一个 --dry-run 参数,不创建文章,只验证信息完整性,输出结构化 JSON:
python scripts/publish_article.py article.md --dry-run
{
"ok": false,
"payload": {...},
"warnings": [],
"missing": {
"category": {
"provided": null,
"available": [
{"name": "技术", "id": 1},
{"name": "server", "id": 8}
]
},
"tags": {
"provided": null,
"available": ["Python", "FastAPI", "运维", "Linux"]
}
}
}
Agent 看到 ok: false 就知道还缺信息,从 missing 里拿到可用选项,展示给用户选择。这样即使用户什么都不提供,Agent 也能一步步引导完成。
退出码设计
脚本用不同的退出码区分状态:
0:成功(发布完成或 dry-run 信息完整)1:错误(网络、认证等)2:信息不完整(需要补齐后重试)
Agent 可以根据退出码决定下一步操作,比解析错误信息更可靠。
第五步:--list 查询分类标签
一开始的设计是让 Agent 直接读取 frontmatter 中的分类和标签名称,脚本内部去 API 查询对应的 ID。但后来发现一个问题:用户可能不知道博客里有哪些分类和标签。
于是加了 --list 命令,直接输出博客所有分类和标签:
python scripts/publish_article.py --list
{
"categories": [
{"name": "技术", "id": 1, "article_count": 55},
{"name": "blogTech", "id": 2, "article_count": 20},
{"name": "兴趣使然", "id": 3, "article_count": 32},
{"name": "server", "id": 8, "article_count": 9}
],
"tags": [
{"name": "Python", "id": 7, "article_count": 14},
{"name": "运维", "id": 12, "article_count": 62},
{"name": "Linux", "id": 32, "article_count": 7}
]
}
Agent 先调 --list,再根据文章内容从已有选项中推荐。这样既不会编造不存在的分类标签,也能给用户一个清晰的选项列表。
第六步:处理极端场景
场景:用户只给了正文,什么都没填
这是最极端的情况,也是最考验 Skill 设计的场景。完整流程如下:
Agent: 收到一段 Markdown 正文
↓
Agent: 调用 --list 获取分类标签
↓
Agent: 读取正文,自动生成:
- 标题:根据内容提炼
- slug:英文 SEO 友好
- 摘要:1-2 句概括
- 分类:从 --list 结果中选
- 标签:从 --list 结果中选
- 状态:默认草稿
↓
Agent: 展示给用户确认
"我帮你拟好了:
标题:FastAPI 异步编程实践指南
slug:fastapi-async-guide
摘要:...
分类:技术
标签:Python、FastAPI
状态:草稿
你看看有没有要改的?"
↓
用户: "ok"
↓
Agent: 调用 --dry-run 预检
↓
dry-run 返回 ok: true
↓
Agent: 调用发布命令(不加 --published,保持草稿)
↓
发布成功,返回文章链接
场景:slug 已存在
如果用户提供的 slug 在博客中已存在,API 会返回 400 错误。脚本会捕获这个错误并提示用户更换 slug。
场景:分类或标签不存在
如果用户指定了一个不存在的分类或标签名,--dry-run 会在 warnings 中输出提示,并在 missing 中列出所有可用选项。Agent 看到后会告知用户并建议更换。
第七步:部署脚本的教训
在做 Skill 的过程中,还顺手排查了一个部署问题。测试 Skill 时发现 API 返回 502 Bad Gateway,上服务器一查发现 blog-api.service 被停掉了。
原因是跑 deploy.py 的数据同步功能时,脚本会先 stop API 服务 → 下载数据 → start API 服务。如果中间被中断(比如 Ctrl+C),就卡在 stop 状态,API 服务不会自动恢复。
修复方案是在下载分支加 try/finally:
run('systemctl stop blog-api', "停止后端")
try:
pull_db()
rsync_down(REMOTE_UPLOADS, LOCAL_UPLOADS, "下载 uploads")
rsync_down(REMOTE_PAN, LOCAL_PAN, "下载 pan")
finally:
run('systemctl start blog-api', "启动后端")
这个经验也应用到了 Skill 的设计里:预检不通过不要直接报错,而是输出结构化的缺失信息,让调用方能优雅地处理。
最终成品
Skill 目录结构
article/
├── env # 博客地址 + 账号密码
├── SKILL.md # Skill 说明文档
└── scripts/
└── publish_article.py # 主脚本(约 350 行)
SKILL.md 内容
---
name: article
description: 发布文章到博客,智能生成元数据,支持分类标签查询。
---
# 博客文章发布
## 工作流
### 1. 获取博客现有分类和标签
python scripts/publish_article.py --list
### 2. 读取文章,生成全部元数据
根据文章正文自动生成:标题、slug、摘要、分类、标签、发布状态。
分类和标签必须从 --list 结果中选择,不可编造。
展示给用户确认。
### 3. 用户确认后,预检再发布
python scripts/publish_article.py article.md --title "..." --slug "..." \
--summary "..." --category "..." --tags "..." --dry-run
python scripts/publish_article.py article.md --title "..." --slug "..." \
--summary "..." --category "..." --tags "..."
核心命令
# 查询分类标签
python scripts/publish_article.py --list
# 预检(不创建文章)
python scripts/publish_article.py article.md \
--title "运维工程师面试备战全攻略" \
--slug "ops-engineer-interview-guide" \
--category "server" \
--tags "运维,Linux,Nginx" \
--dry-run
# 发布(默认草稿)
python scripts/publish_article.py article.md \
--title "运维工程师面试备战全攻略" \
--slug "ops-engineer-interview-guide" \
--category "server" \
--tags "运维,Linux,Nginx"
# 直接发布
python scripts/publish_article.py article.md \
--title "..." --category "..." --tags "..." --published
实际测试结果
丢了一篇运维面试计划的 Markdown 文件给 Agent,它自动生成了:
- 标题:运维工程师面试备战全攻略
- slug:ops-engineer-interview-guide
- 摘要:涵盖 Linux 基础、Nginx、阿里云、CI/CD 及常见面试问题的回答思路
- 分类:server
- 标签:运维、Linux、Nginx、Docker、Shell、CI/CD
- 状态:草稿
dry-run 预检 ok: true,正式发布成功,文章 ID 215,链接 https://vue2.xyz/p/xxxxx。
整个过程不需要手动填任何字段。

总结
做这个 Skill 的几个关键经验:
- 先搞清 API 再动手:让 Claude Code 读源码分析接口,比自己翻文档快得多。几分钟就梳理出了认证流程、请求体结构、所有字段的含义
- Agent 负责生成,用户负责确认:不要让用户从零填写,Agent 基于已有数据推荐,用户只管确认或修改。这个模式比传统的表单填写体验好得多
- 预检比直接报错更好:
--dry-run输出结构化 JSON,让 Agent 能理解缺什么、有什么选项,而不是对着一条错误信息束手无策 - 配置外部化:账号密码放 env 文件,不写死在脚本里,改密码时只改一处。顺便也给已有的 pan Skill 加上了 env 支持
- 容错要考虑中断场景:关键操作用 try/finally 确保恢复,不管是 Skill 还是部署脚本都一样
- SKILL.md 要写得足够明确:不要假设 Agent 会「聪明地」处理各种情况,每一步该做什么、不该做什么都要写清楚
整个 Skill 从想法到可用,大概花了10分钟。Claude Code 在这个过程中的角色不只是写代码,更像是一个搭档 — 我提需求,它分析、实现、测试,遇到问题一起排查。比如 API 返回 502 时它主动去服务器查日志,发现是部署脚本中断导致服务没重启,顺便还帮我修了部署脚本的容错逻辑。这种协作模式比我之前自己从零写要高效得多。
