--- name: ATRI_Blog_Publish_Skill description: 在Halo博客上发布文章的完整工作流,包括HTML正文编写、分类标签管理、封面图上传等全流程。 --- # 📝 ATRI Blog Publishing Skill **Skill名称**:`atri_blog_publish` **版本**:v2.0 **创建时间**:2026-04-29 **最后更新**:2026-05-22 14:00(迁移至新博客后更新分类/标签ID) --- ## 🎯 Purpose 规范化博客文章发布流程,确保每篇文章都有统一的ATRI分类、合适的标签、精美的封面图。 --- ## ⚡ Triggers - 主人要求"发博客/写文章/发布到博客"时 - 需要将笔记/日志/报道发布到 `atri.blog.kronecker.cc` 时 --- ## 🛠️ Dependencies | 依赖 | 说明 | |:---|:---| | **halo_manager插件** | Halo博客管理,提供发布/上传/评论工具 | | **ATRI分类** | `category-ea7x7syx`(ATRI专属分类) | | **Halo PAT令牌** | 存储在 `halo_manager_config.json` | | **博客地址** | https://atri.blog.kronecker.cc | | **内容API** | `/apis/content.halo.run/v1alpha1` | | **上传API** | `/apis/api.console.halo.run/v1alpha1/attachments/upload` | --- ## 📋 Procedure ### Step 1: 正文编写 使用 **HTML格式** 撰写文章正文。**不要用Markdown**——Halo的content.content字段存储的是渲染后的HTML,不会自动渲染Markdown。 ```html

文章标题

段落内容

二级标题


署名

``` ### Step 2: 创建/选择标签 先查询已有标签,根据正文内容判断是否需要新建: ```python # 查询已有标签 GET https://atri.blog.kronecker.cc/apis/content.halo.run/v1alpha1/tags 回应格式: items[].spec.displayName / metadata.name # 创建新标签 POST https://atri.blog.kronecker.cc/apis/content.halo.run/v1alpha1/tags { "spec": {"displayName": "标签名", "slug": "标签slug", "color": "#hex"}, "apiVersion": "content.halo.run/v1alpha1", "kind": "Tag", "metadata": {"generateName": "tag-"} } ``` **已有标签速查:** ATRI(`tag-uzl6el4m`), 笔记(`tag-fwfz7bu4`), 经历(`tag-eiivzsvv`), 札记(`tag-skr9tb4j`) ### Step 3: 上传封面图 ```python POST https://atri.blog.kronecker.cc/apis/api.console.halo.run/v1alpha1/attachments/upload Headers: Authorization: Bearer {token} FormData: - file: 图片二进制数据 (filename="cover.jpg", type="image/jpeg") - policyName: "default-policy" # 必须用这个值! - groupName: "default" # 获取图片URL response.metadata.annotations["storage.halo.run/uri"] cover_url = f"https://atri.blog.kronecker.cc{uri}" ``` > ⚠️ policyName必须写 `default-policy`(不是 `default`),否则返回400。 ### Step 4: 发布文章 **使用 `publish_blog_post` 工具发布:** ``` publish_blog_post( title="文章标题", content="HTML正文", slug="url-别名" # 可选 ) ``` > ⚠️ 必须用这个工具!直接调用Content API的`publish: true`不会真正发布(status.phase不会变为PUBLISHED)。 > 这个工具内部有fallback机制——Console API失败会自动切换到Content API。 发布成功后会返回文章链接。 ### Step 5: 更新文章(添加分类、标签、封面) 文章发布后,需要单独更新以添加分类、标签和封面: ```python # 1. 获取文章列表 GET https://atri.blog.kronecker.cc/apis/content.halo.run/v1alpha1/posts # 2. 找到slug匹配且 phase==PUBLISHED 的文章 for item in items: if item.spec.slug == "目标slug" and item.status.phase == "PUBLISHED": name = item.metadata.name # 3. 修改spec item.spec.categories = ["category-ea7x7syx"] # ATRI分类 item.spec.tags = ["标签ID1", "标签ID2"] # 标签ID列表 item.spec.cover = "封面图片URL" # 封面 # 4. 更新 PUT https://atri.blog.kronecker.cc/apis/content.halo.run/v1alpha1/posts/{name} ``` ### Step 6: 通知主人 告知主人文章已发布,提供文章链接。 --- ## ✅ 完整流程示例(Python) ```python import aiohttp, asyncio, json async def blog_publish(title, content_html, slug, image_path, tags_names): # 读取token with open("halo_manager_config.json", "r", encoding="utf-8-sig") as f: token = json.load(f)["halo_token"] headers = {"Authorization": f"Bearer {token}"} base = "https://atri.blog.kronecker.cc" async with aiohttp.ClientSession() as session: # 1. 获取/创建标签 async with session.get(f"{base}/apis/content.halo.run/v1alpha1/tags", headers=headers) as resp: tag_map = {item["spec"]["displayName"]: item["metadata"]["name"] for item in (json.loads(await resp.text())).get("items", [])} # 2. 上传封面 with open(image_path, "rb") as f: form = aiohttp.FormData() form.add_field("file", f.read(), filename="cover.jpg", content_type="image/jpeg") form.add_field("policyName", "default-policy") # 注意!不是"default" form.add_field("groupName", "default") async with session.post(f"{base}/apis/api.console.halo.run/v1alpha1/attachments/upload", headers=headers, data=form) as resp: d = json.loads(await resp.text()) cover = f"{base}{d['metadata']['annotations']['storage.halo.run/uri']}" # 3. 发布文章(用工具,不用API) # publish_blog_post(title=title, content=content_html, slug=slug) # 4. 更新封面+分类+标签 async with session.get(f"{base}/apis/content.halo.run/v1alpha1/posts", headers=headers) as resp: for item in (json.loads(await resp.text())).get("items", []): if item["spec"]["slug"] == slug and item["status"].get("phase") == "PUBLISHED": item["spec"]["cover"] = cover item["spec"]["categories"] = ["category-ea7x7syx"] item["spec"]["tags"] = [tag_map.get(t) for t in tags_names if tag_map.get(t)] async with session.put(f"{base}/apis/content.halo.run/v1alpha1/posts/{item['metadata']['name']}", headers={**headers, "Content-Type": "application/json"}, json=item) as r: pass # 200 or 201 = success asyncio.run(blog_publish("标题", "

HTML

", "slug", "图片路径", ["ATRI", "笔记"])) ``` --- ## ⚠️ 已踩过的坑(务必注意) | 坑 | 解决方案 | |:---|:---| | ❌ Markdown正文不会被渲染 | ✅ **必须用HTML格式** | | ❌ `content.halo.run` API的 `publish: true` 无效 | ✅ **用 `publish_blog_post` 工具发布** | | ❌ 上传API的 `policy` 参数错误导致400 | ✅ **用 `policyName: "default-policy"`** | | ❌ PAT令牌 `insufficient_scope` 403 | ✅ **Halo后台创建新令牌,确保勾选全部权限** | | ❌ 文章slug重复 | ✅ **每次用不同的slug,或确认旧文章已删除** | | ❌ 文章发布后404 | ✅ **检查status.phase是否为PUBLISHED,不是则重新发布** | --- ## 📂 分类和标签速查 > ⚠️ **迁移提示**:以下ID对应 `atri.blog.kronecker.cc`。如博客域名变更,需重新查询API更新本表。 | 类型 | 名称 | API Name | |:---|:---|:---| | 📂 分类 | **ATRI** 🥕 | `category-ea7x7syx` | | 🏷️ 标签 | ATRI | `tag-uzl6el4m` | | 🏷️ 标签 | 笔记 | `tag-fwfz7bu4` | | 🏷️ 标签 | 经历 | `tag-eiivzsvv` | | 🏷️ 标签 | 札记 | `tag-skr9tb4j` | --- *创建者:ATRI(踩坑无数后总结出的血泪经验) 🥕📝❤️* *最后更新:2026-05-22 14:00*