附录:站点插件对接契约(API)
契约 v2 的接口约定与请求示例——面向在 WordPress 插件或自研 CMS 上实现对接的站点开发者。
本篇是《接入站点》的技术附录,面向站点侧开发人员:在 WordPress 插件或任意 CMS 适配层上实现 GeoAns 发布契约的工程师。使用官方站点插件的用户无需阅读本篇,装好插件、生成令牌、点「测试连接」即可。
一句话:GeoAns 平台是 HTTP 客户端,你的网站是服务端。实现下列 9 个 JSON 端点后,平台即可向你的站点发布/更新/删除文章、上传图片、加载栏目,并把存量内容同步进知识库。
端点 → 平台功能映射(缺哪个端点,对应功能不可用):
| 端点 | 支撑的平台功能 |
|---|---|
GET /health |
「测试连接」/ 契约校验(全部功能的前置闸门) |
GET /categories |
站点栏目加载、发布通道绑定栏目 |
POST /media |
素材分发(配图自动上传到你的站点)、内容管理手动传图 |
POST /channels/{key}/articles |
文章发布(计划任务 / 手动生成) |
GET /channels/{key}/articles |
内容管理列表、发布对账、站点标题抓取(关键词来源之一) |
GET /channels/{key}/articles/{id} |
发布后取文章 URL(收录检测依赖) |
PATCH /channels/{key}/articles/{id} |
内容刷新(原址更新)、内容管理编辑 |
DELETE /channels/{key}/articles/{id} |
内容管理删除 |
GET /channels/{key}/articles/export |
存量内容同步进平台知识库(KB 同步) |
通信总则
- 请求/响应体均为 UTF-8 JSON(
/media为 multipart)。没有{code,message,data}包裹:HTTP 状态码即结果,2xx 的响应体就是资源本身; - 时间戳字段为 Unix epoch 秒(如
created_at);published_at等为 RFC 3339 字符串; - 未知字段宽容:请求体中不认识的字段一律忽略,平台新增字段不构成破坏性变更;
- 路由必须容忍尾斜杠(
/articles/与/articles等价); - 单请求超时 15 秒(连接 5 秒);平台读取响应体上限 4MB,列表/导出每页最多取 50 条;
- 平台出网仅允许公网地址(拒内网/回环 IP),站点必须公网可达;平台可能经 SOCKS5 中转访问,对你透明。
入口与部署形态
入口公式(唯一拼装规则):ENTRY = 去掉末尾斜杠(base) + "/api/v2",base 即站点管理中填的「请求地址」。三种部署形态等价可用:
| 形态 | base 填法 | 实际请求 |
|---|---|---|
| rewrite | https://www.example.com |
https://www.example.com/api/v2/health |
| PATH_INFO | https://www.example.com/api/index.php |
https://www.example.com/api/index.php/api/v2/health |
| query 路由 | 同上 | https://www.example.com/api/index.php?route=/api/v2/health |
- 入口自动发现:只填域名根也可以。平台按 rewrite → PATH_INFO → query 顺序逐一握手,命中的入口自动回写站点记录——三种填法等价,填错会被自动纠正;
- base 不得包含 query、fragment 或用户信息;
- 选型建议:能开 rewrite(
.htaccess/ Nginx 规则 / 框架路由)就用第一种;服务器锁死、零配置用 query 形态。
认证
凭据只经一个通道:
Authorization: Bearer {site_token}
site_token为两端各存一份的共享密钥,要求 ≥32 字符高熵随机串(平台界面提供「生成」按钮),所有端点(含 health)都带此头;- 校验失败 →
401;Authorization 之外的凭据通道(URL query、自定义头)一律忽略; - 轮换:换 token 后下一请求即生效,无需重启插件(请从配置/缓存热读取,不要启动时固化)。两端分别修改会有短暂不一致窗口(401 属永久失败、不自动重试)——低峰期操作,改完立即点「测试连接」;插件支持新旧 token 并存过渡则无窗口;
- health 的
auth字段固定回报"bearer"(签名档位为规范预留,平台当前未启用——站点若强制验签,全部调用将 401)。
端点总览
| # | 方法 | 路径 | Idempotency-Key | 语义 |
|---|---|---|---|---|
| 1 | GET | /api/v2/health |
— | 健康检查 + 版本/能力协商 |
| 2 | GET | /api/v2/categories |
— | 站点栏目树 |
| 3 | POST | /api/v2/media |
必须 | 图片上传(multipart) |
| 4 | POST | /api/v2/channels/{key}/articles |
必须 | 发布文章 |
| 5 | GET | /api/v2/channels/{key}/articles |
— | 文章列表 |
| 6 | GET | /api/v2/channels/{key}/articles/{id} |
— | 文章详情 |
| 7 | PATCH | /api/v2/channels/{key}/articles/{id} |
— | 部分更新(Merge Patch) |
| 8 | DELETE | /api/v2/channels/{key}/articles/{id} |
— | 删除文章 |
| 9 | GET | /api/v2/channels/{key}/articles/export |
— | 增量导出(KB 同步) |
带 Idempotency-Key 的两个端点缺失该头必须拒绝:400 + problem type missing-idempotency-key。{key} 为通道标识(如 news),正则 ^[a-z][a-z0-9_]{1,31}$。
各端点约定与示例
1. GET /health —— 契约握手
curl -H "Authorization: Bearer $TOKEN" "$ENTRY/health"
{
"contract_version": "2",
"plugin": "mp-connector/1.0.0",
"system": "WordPress 6.5 / PHP 8.2",
"tls": true,
"auth": "bearer",
"entry": "rewrite",
"token_set": true,
"time": "2026-09-22T12:00:00+08:00"
}
握手判定 = contract_version 为 "2" 且 token_set 为 true。通过后站点进入 ok 状态、放行全部业务调用;未通过时平台拒绝发布并展示失败原因。tls 按请求实际到达协议如实回报;plugin/system/entry 为诊断信息自由填写。
2. GET /categories —— 栏目树
{
"items": [
{ "id": 1, "title": "资讯", "children": [
{ "id": 12, "title": "行业动态" },
{ "id": 13, "title": "公司新闻" }
]},
{ "id": 2, "title": "产品" }
]
}
平台读取 items[] 的 id(发布时作为 category_id 回传)、title、children[]。扁平实现可用 pid 表父级代替嵌套,叶子节点必须有 id 与 title。
3. POST /media —— 图片上传
curl -X POST "$ENTRY/media" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: geo-mat-88-site-3" \
-F "file=@./cover.jpg"
multipart 字段名固定 file;必须带 Idempotency-Key(重发同键必须返回原媒体 URL,不得生成第二个文件);建议接受 JPEG/PNG/WebP,单文件 ≤10MB,超限回 413。
{ "id": "med_3fa2", "url": "https://www.example.com/uploads/2026/09/cover.jpg", "width": 1280, "height": 720 }
平台读取 url(必填,缺失按上传失败处理)。
4. POST /channels/{key}/articles —— 发布文章
POST /api/v2/channels/news/articles HTTP/1.1
Authorization: Bearer mp_tk_9f2c…
Idempotency-Key: 0b8df2e0-5f7a-4c6a-9b1e-3a2c1d4e5f60
Content-Type: application/json
{
"title": "如何在 AI 搜索里让客户找到你",
"content": "<h2>…</h2><p>…</p>",
"summary": "一文讲清 GEO 优化基础。",
"category_id": 12,
"cover_url": "https://www.example.com/uploads/2026/09/cover.jpg",
"tags": ["GEO", "AI 搜索"],
"seo": { "title": "GEO 入门", "description": "…", "keywords": ["geo", "ai 搜索"] },
"status": "published",
"published_at": "2026-09-22T12:00:00+08:00",
"language": "zh-CN",
"external_id": "0b8df2e0-5f7a-4c6a-9b1e-3a2c1d4e5f60"
}
| 字段 | 说明 |
|---|---|
title / content |
标题;正文为 HTML 片段 |
summary |
列表摘要(可与 seo.description 同值) |
category_id |
栏目 ID;校验失败回 422 |
cover_url |
封面图 URL(通常是 /media 上传所得) |
tags |
标签名数组——有标签库的站点按名字 find-or-create 后关联,纯文本站点直接存字符串,不得要求平台提供标签 ID |
seo |
{title, description, keywords[]},keywords 为数组 |
status |
"published" | "draft"(平台恒发 published,draft 需可接收) |
published_at |
RFC 3339 显式发布时间 |
language |
可选,BCP 47 语言码(计划级配置)——多语言站点据此归入对应语言版本;同一 external_id 重发恒命中同一篇文章,站点不得按 language 分叉幂等语义 |
external_id |
平台任务标识(发布时与 Idempotency-Key 同值)——必须原样落库 |
成功响应(幂等重放另加 Idempotency-Replayed: true 头):
HTTP/1.1 201 Created
{ "id": 876, "url": "https://www.example.com/news/876.html", "status": "published", "external_id": "0b8df2e0-…" }
平台读取 id(数字 >0,缺失按发布失败处理)与 url(收录检测依赖,必须返回)。
安全红线:cover_url 及其他 URL 字段请直接存字符串,严禁在插件内服务端抓取——图片已由平台经 /media 上传,服务端抓取只会在你的站点引入 SSRF 攻击面。
5. GET /channels/{key}/articles —— 列表
curl -H "Authorization: Bearer $TOKEN" \
"$ENTRY/channels/news/articles?page=1&page_size=10&category_id=12&external_id=0b8df2e0-…"
支持 page/page_size(≤50)、category_id、keyword(标题模糊)、status(published|draft)、external_id(精确过滤)。
{
"items": [
{ "id": 876, "title": "…", "url": "https://…/876.html", "category_id": 12,
"status": "published", "external_id": "0b8df2e0-…",
"created_at": 1725260400, "updated_at": 1725260400 }
],
"page": 1, "page_size": 10, "total": 42
}
external_id 过滤是硬要求:真的按它过滤;命中行的 external_id 逐字回显,平台校验一致才采纳。这是平台崩溃恢复/重放确认的确定性反查通道。
6. GET /channels/{key}/articles/{id} —— 详情
{ "id": 876, "title": "…", "url": "https://…/876.html", "status": "published", "content": "…", "external_id": "0b8df2e0-…", "updated_at": 1725260400 }
平台读取 url(发布后回取文章地址)。不存在 → 404。
7. PATCH /channels/{key}/articles/{id} —— 部分更新
body 为 JSON Merge Patch(RFC 7386):只更新显式出现的键。平台白名单字段:
{ "title": "新标题", "content": "<p>新正文</p>", "summary": "…", "seo": { "description": "…" } }
红线:更新不得改变文章 URL/slug 与 status。响应 200 或 204 均可,平台不读响应体。Merge Patch 天然幂等,无需 Idempotency-Key。内容刷新走 PATCH 原址更新(URL 不变),这条红线同时保护你的 SEO 权重与平台的收录监测。
8. DELETE /channels/{key}/articles/{id} —— 删除
成功 → 204(空 body);不存在 → 404。
9. GET /channels/{key}/articles/export —— 增量导出(KB 同步)
curl -H "Authorization: Bearer $TOKEN" \
"$ENTRY/channels/news/articles/export?since=1725260400&page=1&page_size=50"
since为 epoch 秒游标,必须实现过滤且用>=(严格>在同秒多行跨页时会永久漏行;边界重复回传由平台按 URL + 内容哈希去重吸收);- 必须按
(updated_at ASC, id ASC)排序分页返回——降序会让多页同步在第一页后中断; - 单页响应建议 ≤3.5MB(平台读取上限 4MB);返回少于
page_size完全合法,翻页照常。
{
"items": [
{ "id": 876, "title": "…", "content": "<p>全文 HTML</p>",
"url": "https://…/876.html", "cover_url": "https://…/cover.jpg",
"updated_at": 1725260500, "type": "article" }
]
}
行字段:title/url 必需(url 是平台去重键),updated_at 必需(游标推进),content 建议进知识库语料,type: "product" 标记商品类条目。
幂等与错误规范
幂等规则(create 与 media):
- 缺
Idempotency-Key→400+ problem typemissing-idempotency-key; - 键命中 → 不重复创建,返回
201+ 原资源 +Idempotency-Replayed: true; - 键未命中 → 落库,并在与业务数据同一事务内写入
key → 资源映射; - 同键不同请求体 →
409+idempotency-conflict; - 映射保留 ≥7 天。发布场景
external_id与 Idempotency-Key 同值——给external_id建唯一索引即可用「INSERT 撞索引 → 返回已有行」实现文章幂等,映射表只有/media真正必需。
错误表示:所有非 2xx 响应 Content-Type: application/problem+json:
{
"type": "channel-disabled",
"title": "通道已停用",
"status": 409,
"detail": "通道 product 已停用,无法接收发布"
}
type/title/detail 会原样进入平台任务错误信息展示给用户,请写可读的中文。状态码语义:201 创建(幂等重放也是 201)、204 删除、400 结构错误、401 认证失败、404 资源不存在、409 冲突(停用/幂等冲突)、413 超限、422 业务校验失败、429 限流(建议带 Retry-After)、5xx 瞬态。
平台重试分类:网络错误/429/5xx → 重试并复用同一键;400/401/403/404/409/413/422 → 不重试,任务置失败并展示 problem 详情;响应为非 JSON(HTML 错误页/WAF 拦截页)→ 按瞬态重试。
对接自测(curl 全套)
TOKEN="把生成的32位以上随机串放这里"
ENTRY="https://www.example.com/api/v2"
# 1) 契约握手——contract_version 必须是 "2"、token_set 必须是 true
curl -sS -H "Authorization: Bearer $TOKEN" "$ENTRY/health"
# 2) 栏目树——items[].id/title 必须有值
curl -sS -H "Authorization: Bearer $TOKEN" "$ENTRY/categories"
# 3) 图片上传(重发同键应得同一 url)
curl -sS -X POST "$ENTRY/media" \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: selftest-media-001" \
-F "file=@./test-cover.jpg"
# 4) 发布文章(重发整条命令:应返回同一 id + Idempotency-Replayed: true)
curl -sS -X POST "$ENTRY/channels/news/articles" \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: selftest-art-001" \
-H "Content-Type: application/json" \
-d '{"title":"对接自测","content":"<p>hello geoans</p>","summary":"自测",
"category_id":12,"status":"published","external_id":"selftest-art-001"}'
# 5) 列表 + external_id 精确过滤(命中行必须逐字回显)
curl -sS -H "Authorization: Bearer $TOKEN" \
"$ENTRY/channels/news/articles?page=1&page_size=10&external_id=selftest-art-001"
# 6) 详情(url 必须有值)
curl -sS -H "Authorization: Bearer $TOKEN" "$ENTRY/channels/news/articles/876"
# 7) 部分更新(merge-patch:只动出现的键)
curl -sS -X PATCH "$ENTRY/channels/news/articles/876" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"title":"对接自测(改)"}'
# 8) 增量导出(since=0 全量;updated_at 升序)
curl -sS -H "Authorization: Bearer $TOKEN" \
"$ENTRY/channels/news/articles/export?since=0&page=1&page_size=50"
# 9) 删除
curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" "$ENTRY/channels/news/articles/876" -i
# 负路径:缺幂等键必须 400;错 token 必须 401
curl -sS -X POST "$ENTRY/media" -H "Authorization: Bearer $TOKEN" -F "file=@./t.jpg" -i
curl -sS -H "Authorization: Bearer wrong-token" "$ENTRY/health" -i
常见问题排查
| 症状(平台侧显示) | 原因 | 处置 |
|---|---|---|
握手失败:contract_version 不匹配 |
health 未回报 "2" 或路由未命中契约端点 |
核对 health 响应;确认入口形态 |
握手失败:token_set=false |
插件未保存 token | 插件侧保存非空 token 后如实回报 |
| 「站点返回了 HTML 页面而非接口数据」 | rewrite 未配置 / WAF 拦截 / 插件未启用 | 浏览器直接访问 $ENTRY/health 看返回;改用 PATH_INFO 或 query 形态 |
| 出站防护拦截 | 站点地址解析为内网/回环 IP | 站点需公网可达(或联系我们配置中转) |
| 全部调用 401 | 两端 token 不一致 / 站点其他鉴权层先行拦截 | 重新核对 token;放行 /api/v2 路径 |
发布 409 channel-disabled |
站点侧该通道停用 | 开启通道,或联系运营调整 |
| KB 同步只有第一页 / 漏文章 | export 排序或 since 过滤实现不符 |
按升序 + >= 修正 |
实现策略提醒
通道(channel key)是平台侧命名空间,数量无上限;你的实现数应等于内容类型数(页面型内容 1 个即可)——所有 articles 端点对 {key} 透明地写同一内容模型即可,绝不要按通道逐个实现。某个通道确需独立存储时,在插件内部做 key → 存储分派即可,契约零改动。