给 AI Agent 开一个文章发布接口

给 AI Agent 开一个文章发布接口

一、缘起:让 Agent 自己把文章发出去

这个技术站点平时由我维护,写文章的人是我,发文章的人也是我。但每次发文的流程是这样的:整理好正文,手动登录后台,新建文章,粘贴 HTML,选栏目,填摘要关键词,传图,最后点发布。

如果只是偶尔发一篇,这个流程完全没问题。但当发文变成常态——尤其是当文章本身就是由 AI 生成的时候——「人肉搬运」就成了整条链路上最慢、最容易出错的一环。粘错栏目、漏填摘要、图片传了却没插进正文,都是真实发生过的事。

于是有了这个接口:让 Agent 直接通过 HTTP 把文章写进 CMS,去掉中间的人工环节。

不过这里有个前提:站点是基于 SIYUCMS(ThinkPHP)搭的现成 CMS,不是自己写的应用。所以接口是在既有 CMS 之上加的一层薄封装,而不是重写一套内容系统。

二、整体设计:四个接口,一条流水线

接口拆成了四步,每一步只做一件事:

文章发布接口的四步流程:查栏目、查字段、传图片、提交文章

① GET  /api/cms/cates                  → 拿可发布栏目列表,选定 cate_id
② GET  /api/cms/fields?cate_id=<id>    → 拿该栏目要填的字段清单与上传配置
③ POST /api/cms/upload                 → 上传头图/配图,拿到图片 URL
④ POST /api/cms/article                → 提交文章,拿到 id 与访问地址

之所以拆成四步而不是一个大而全的「一步发文」接口,核心原因是让调用方不依赖任何硬编码。后面会展开讲。

三、鉴权:一个静态 Token

鉴权用的是最朴素的方式:请求头带一个静态 Token。

X-Api-Token: <token>

# 也兼容标准写法
Authorization: Bearer <token>

选静态 Token 而不是 OAuth 或者签名机制,是基于实际场景的权衡:

  • 调用方是可信的——目前只有我这边的 Agent 在用,不存在多方授权、权限分级的需求
  • Token 泄了可以立刻换——服务端更换即让旧 Token 全部失效,不需要复杂的吊销流程
  • 实现成本低——一个请求头校验,不用维护令牌签发、刷新、过期整套机制

当然代价也很清楚:静态 Token 没有时效性,一旦泄露就是长期有效。所以对应的防护措施是——Token 不写进代码、不进版本库、不贴进聊天记录,统一放在服务端的环境变量文件里由脚本读取。文档里也只放占位符。

如果将来需要对外开放,这套鉴权就不够用了,届时该上的是签名 + 时效 + 权限分级,而不是继续加长 Token。

四、栏目与字段查询:不要硬编码

这是整个设计里最重要的一条原则。

CMS 的栏目是会变的——今天有「PHP」「Python」「前端」八个栏目,明天可能加一个「数据库」,或者把某个栏目改成单页模型(不再支持发文章)。如果调用方把栏目 id 和字段表写死在代码里,那么每次后台调整,调用方就得跟着改一次。

所以接口设计成调用方先问、再发

GET /api/cms/cates
→ [{"id": 16, "cate_name": "前端", "cate_folder": "FrontEnd",
     "model_name": "Article", "list_url": "/index/FrontEnd.html"}, ...]

GET /api/cms/fields?cate_id=16
→ {"fields": [
     {"field": "title",   "name": "标题", "type": "text",     "required": true},
     {"field": "content", "name": "内容", "type": "editor",   "required": false},
     ...
   ],
   "upload": {"url": "/api/cms/upload", "field_name": "file",
              "allow_ext": ["jpg","png","gif","jpeg","ico"]}}

这样带来两个好处:

  • 后台改了,调用方不用改——栏目新增、字段调整,接口返回的就是最新的
  • 必填项由服务端说了算——哪些字段必填、哪些可以留空,以接口返回为准,不会因为前端猜错而导致提交失败

顺带一提,fields 接口还会顺带返回上传配置(允许的图片后缀、大小上限)。这样调用方在传图之前就知道哪些格式会被拒,不用靠试错。

五、图片上传:必须先传到站点

图片走独立的上传接口,字段名单张用 file、多张用 file[]

POST /api/cms/upload
Content-Type: multipart/form-data

file=@cover.png

→ {"code": 1, "data": {
     "url": "/uploads/20260920/803ec9b5...png",
     "urls": ["/uploads/20260920/803ec9b5...png"]
   }}

返回的是相对路径(以 / 开头),可以直接原样写进 image 字段和正文的 <img src>,前台会自动拼上域名。

这里有两条必须遵守的规则:

  • 正文和头图只能引用本站地址——直接填外站图片链接可能不显示或被过滤,所以外链图要先下载到本地再上传
  • 服务端会校验真实图片内容——把非图片文件改个扩展名是过不了的,这个校验做在上传环节而不是仅靠后缀判断

另外 alt 属性建议必填。除了无障碍阅读,它对 SEO 也有实际作用——图片的语义信息搜索引擎只能从这里读。

六、文章提交:字段与 HTML

提交接口接收 JSON,字段与后台表单一一对应:

字段说明
cate_id栏目 id(必填)
title标题,≤255 字符(必填)
content正文 HTML(图片嵌在这里)
summary摘要,留空会自动截正文前 120 字
image头图 URL
tags / keywords标签与 SEO 关键词
status1=发布,0=草稿
sort排序,越大越靠前

正文是原样渲染的 HTML 片段,这一点需要调用方特别注意。写的时候要注意:

  • 正文最高层级用 <h2>——页面大标题已经占位,正文里再用 <h1> 会出现两个一级标题
  • 不要内联 style 属性——会被站点样式覆盖或破坏排版,样式统一由模板控制
  • 不要提交完整 HTML 文档——只交正文片段,<html><head> 这些不需要

响应里会返回新文章的 idurl,方便调用方直接给出文章链接。

七、为什么只开放新增

这个接口刻意不提供修改和删除能力

接口只开放新增,不开放修改与删除

看起来是个限制,实际上是基于风险的主动设计:

  • 删除是不可逆的——文章一旦被误删,连带评论、收录、外链全部失效。把删除能力交给一个可能误判的自动化流程,风险远大于收益
  • 修改的语义很复杂——是覆盖全文还是增量更新?草稿和已发布要不要区分?这些语义一旦定义不好,很容易出现「改了一半」的状态。与其设计一个半吊子的更新接口,不如先不做
  • 新增是幂等性可控的——重复提交只会多一篇文章,不会破坏已有数据。最坏的情况是清理一条多余记录,而不是丢失历史内容

这个取舍带来一个必须遵守的纪律:接口返回成功后绝不重试。因为接口不支持去重,网络抖动时如果盲目重发,站点上就会多出一条重复文章。

需要修改文章时怎么办?目前的方案是去后台手动改,或者重新发一条替代。对于自动化流程来说,「只能往前加」比「可以随意改」更容易做出可靠的行为——这跟数据库里的 append-only 日志是同一个道理。

八、踩过的坑

开发和使用过程中遇到几个值得记下来的问题:

8.1 判成功要看响应体,不能看 HTTP 状态码

接口的统一响应结构是 {"code": 1, "msg": "OK", "data": {}},其中 code 为 1 才算成功。关键在于——失败时 HTTP 状态码可能仍然是 200

只判断 response.status_code == 200 的调用方,会把「栏目不存在」这种业务失败当成成功。正确做法是解析响应体检查 code 字段。

8.2 404 不一定是 Token 问题

接口刚上线时访问返回的是 ThinkPHP 的「系统发生错误」HTML 页面,而不是 JSON。当时第一反应是 Token 写错了,但仔细看返回的是 404 且根本不是 JSON——说明请求压根没匹配到路由,跟鉴权无关。

排查方向应该在路由注册、控制器文件是否上传、Nginx 伪静态是否覆盖 /api 路径,而不是反复检查 Token。经验是:返回格式不对,先怀疑路由;返回格式对但 code=0,才怀疑业务

8.3 不要用测试文章验证接口

因为接口不能删除,用一篇「测试标题」的文章来验证链路,这篇测试数据就永久留在站点上了。

正确做法是用一条真实内容走完整流程——反正都要发,不如第一篇就发真的。

8.4 头图别为了凑比例裁出断字

用官方新闻图做头图时,常遇到原图宽高比不标准的问题。为了凑成 16:10 硬裁,很容易把图片上的文字切成 …ase III Trial of 这样的断头词——比比例不标准难看得多

结论是:宁可保留完整文字、接受非标准比例,也不要出现残字。如果确实需要裁,别靠肉眼估边界,用像素分析定位文字区域更靠谱。

九、小结

这个接口本身不复杂——四个端点、一个静态 Token、一层薄薄的 CMS 封装。但它在「AI 生成内容」这件事上补上了一块关键拼图:让内容的产出和发布之间不再需要人工中转

回头看,几个设计决策值得保留下来:

  • 接口分步而非大而全——调用方先问再发,避免硬编码栏目与字段
  • 鉴权按实际威胁模型选型——可信调用方场景下,静态 Token 够用,不必过度设计
  • 能力边界要主动收窄——只开放新增,把不可逆操作挡在自动化之外
  • 成功判断以业务字段为准——HTTP 状态码只表示传输层结果

给自动化流程开放写权限时,限制它能做什么,比教它别做错更可靠。接口层面不给删除能力,比在提示词里反复叮嘱「不要删文章」要稳得多。