实测
让 Codex 发文章和传图片:一次不依赖后台点击的实践
围绕一次真实的 GUI 文件选择限制,拆解项目内命令怎样处理内容输入、媒体导入、草稿 PR、审核、显式发布和撤稿。

这次内容工具的起点不是“做一个更复杂的后台”,而是一个很具体的阻碍:浏览器里的系统文件选择器不适合稳定地交给自动化流程控制,图片上传难以重复验证。
继续点击并不能解决可复现性。于是 Knowhy Lab 保留原来的 Git 内容架构,在项目里补了一套命令入口,让 Codex 可以用明确的文件输入创建文章、导入图片、生成草稿 PR,并在审核后显式发布。
先说清边界:这是一套项目内命令工具,不是已经对外提供的 HTTP API。它需要在取得仓库权限、安装项目依赖的运行环境中执行,也不会绕过 GitHub 和发布检查。
输入不是一段塞进命令行的长文字
每篇稿件拆成两个可审阅文件:
- JSON 保存标题、摘要、slug、语言、发布时间、标签、来源和 SEO 等结构化字段;
- Markdown 保存正文,便于正常写作和检查排版。
一个简化输入可以是:
{
"locale": "zh-CN",
"stableId": "一条稳定且唯一的 ID",
"title": "文章标题",
"excerpt": "读者会得到什么",
"type": "tutorial",
"slug": "article-slug",
"siteStatus": "online",
"publishedAt": "符合 ISO 8601 的时间",
"bodyFile": "./body.md",
"tags": ["示例"]
}
这里的 siteStatus: online 只表示内容在满足发布日期并进入生产构建后可以公开。它不会让仍在内容分支上的文件绕过 PR 直接出现在正式站。
stableId 用来识别同一篇文章;中英文版本如果以后成对出现,会复用同一个稳定 ID 和 slug。当前首批只写中文,不为了填充英文站自动生成翻译。
图片怎样进入站点
创建稿件时可以同时传入一张本地图片和准确的替代文本:
pnpm content -- draft \
--input article.json \
--media cover.png \
--alt "说明图片展示的信息" \
--dry-run
运行前提是:你位于项目仓库、依赖已经安装,输入文件和图片确实属于本次稿件。先加 --dry-run,预期结果是看到目标仓库、基线分支、内容分支、待新增文件和差异;不会创建分支、提交、推送或 PR。
媒体入口只接受经过文件签名检查的 PNG、JPEG、GIF 或 WebP,并限制单文件大小。导入时会根据图片内容计算摘要,生成类似 cover-a1b2c3d4e5f6.png 的文件名,再把文章封面引用写成 /uploads/...。
这样做有两个实际作用:同名但内容不同的图片不会悄悄覆盖;文章与媒体会进入同一个内容提交,审阅者能同时看到引用和文件。
替代文本不是文件名,也不是“配图”两个字。它应该描述图片传达的信息,例如“内容分支经过 Pull Request 合并到 main,再由 Pages 云端构建的流程图”。读者无法看到图片时,仍能理解它为什么出现在这里。
草稿如何变成可审核的 PR
预演通过后,加入 --push:
pnpm content -- draft \
--input article.json \
--media cover.png \
--alt "说明图片展示的信息" \
--push
命令会从 CMS 配置读取默认目标分支。正式站当前指向 main,工具会从远端最新基线创建独立的 content/* 分支,校验内容 schema 和 Git 差异,提交后推送并创建普通 GitHub Pull Request。
遇到本地与远端内容分支分叉、目标分支已经前进且无法安全继承,或 PR 不可合并时,工具会停止,而不是强推覆盖。这个保护很重要:自动化的价值不是“无论如何写进去”,而是把机械步骤做稳定,把冲突留在可见的位置解决。
为什么不直接合并
草稿命令不会隐式发布。审核时使用:
pnpm content -- review --pr 123
预期会看到 PR 的标题、状态、目标分支、来源分支,以及文章和媒体的完整差异。审阅重点包括:
- 标题是否真的对应正文解决的问题;
- slug、语言、发布时间和公开状态是否正确;
- Markdown、代码块、链接和图片引用是否可构建;
- 来源是否支撑关键事实;
- 是否混入测试地址、凭据或不该公开的内部细节。
只有差异和检查都合格,才执行显式发布:
pnpm content -- publish --pr 123 --confirm main
--confirm main 必须与 PR 的实际目标完全一致。发布命令会再次检查 PR 头提交、可合并状态和内容验证,然后请求 squash merge。合并后,Cloudflare Pages 的 Git 集成才会开始正式构建。
GitHub 官方文档把 Pull Request 描述为提议、讨论和合并变更的协作入口,也强调可以在合并前检查差异并运行自动检查。这里借用的正是这套状态边界,而不是把 GitHub 当作一个没有审核的文件上传盘。
它与 Sveltia CMS 是什么关系
两种入口共用 src/content/、媒体目录、内容 schema 和公开规则,没有第二份数据库。
但它们不是同一种草稿实现。Sveltia 的编辑工作流会使用自己的 cms/* 分支和工作流元数据;程序化命令创建的是普通 content/* PR。因此,普通内容 PR 不保证出现在 Sveltia 的“编辑工作流”草稿列表中。
正确的查看位置是 GitHub PR。等它合并到 main 后,文章会成为正式内容集合的一部分,之后仍可以在 Sveltia 中继续人工编辑。
为了避免冲突,同一篇文章不应该同时在 Sveltia 草稿和程序化内容分支里修改。先完成或关闭一个入口的工作,再从最新 main 开始另一个入口。
发布后还要验证什么
“合并成功”不是最后一步。对这套静态站,至少还要确认:
- Pages 成功部署对应合并后的提交;
- 正式文章 URL 能打开,封面和正文没有断链;
- 首页、文章列表和搜索能找到新稿;
- 中文 RSS 和 sitemap 已包含文章;
- 英文 Feed 没有混入只有中文版本的稿件。
这也是程序化路径仍然保留人工审阅的原因:命令可以保证流程一致,却不能替作者判断文章是否准确、图片是否有信息价值、手机阅读是否舒服。
撤稿也走非破坏性路径
如果文章需要下线,工具不会直接删除文件和媒体,而是创建一条把 siteStatus 改为 offline 的新 PR:
pnpm content -- offline --slug article-slug --locale zh-CN --push
审核并合并后,下一次构建会把文章从公开页面、搜索、RSS 和 sitemap 中排除。正文与 Git 历史仍然保留,媒体也不会自动删除,因为它可能还被其他内容引用。
这条路径解决的是“怎样安全地改变正式内容状态”,不是彻底删除互联网上已经被缓存或保存过的信息。
这次实践真正绕开的是什么
它绕开的是不稳定的文件选择器操作,不是审核、权限和发布确认。Codex 可以准备输入、导入媒体并把差异送进 PR;人仍然可以在合并前判断内容质量,系统也会在目标分支前进或校验失败时停下来。
如果你关心文章合并后为什么不需要电脑继续开着,可以接着看用 GitHub 存文章,发布博客还需要电脑一直开着吗?。