在 cfapi.aog.cc 开发你的应用插件
这是一份按 2026-11-05 线上源码核对的开发文档。先根据账号层级选路径,能避免绝大多数“按文档做了却 403 / 上传失败 / 审核后没有运行”的问题。
| 账号层级 | 私有开发(自建自用) | 市场投稿 | 内部能力 |
|---|---|---|---|
external 外部开发者(默认) | 不能创建、上传或启用私有插件 | 可以:上传包 → 提交审核,批准后进入市场 | 不可用 |
internal 普通内部 | 不能写:仅只读 plugin.view | 可投稿 | 只读查看与安全调用 |
internal_advanced 高级内部 | 可以管理归属于自己的插件 | 可投稿 | 含沙箱等受控写能力 |
层级由后台管理员设置,登录令牌里的 tier 只是展示;服务端实时读库,降级后最迟约 20 秒生效。自己签发的 API 密钥不能把层级抬高到超过账号本身。
文档集合、Schema、带 TTL 的键值
插件私有桶、受控文件读写
独立用户目录;当前登录不签发 API 令牌
统一模型调用与用量查询
Hook、UI Slot、RPC、队列、调度
平台托管静态前端 / 平台反代你的后端
按目标选路线
| 目标 | 正确路线 | 不要做什么 |
|---|---|---|
| 我想把插件发布给所有人 | 外部账号登录 → 准备 local 或 remote 包 → 市场上传 → 提交审核 → 管理员批准 → 由具备 plugin.manage 的实例管理员从市场安装 | 不要申请 plugin.manage 企图直接上传安装;外部账号没有这个权限,市场投稿也不需要它 |
| 我在平台内部,自建自用 | internal_advanced 账号 → 在「我的插件」注册/上传 → 启用 → 调试 | 不要用未归属于自己的插件 code,覆盖同名插件会被拒绝 |
| 我要调用已上架插件 | 先确认目标插件已 approved 且启用;调用者仍需具备对应的 plugin.view(读)或 plugin.manage(写) | 不要假设“市场可见”等于“无需授权即可调用”;仅 external 层级本身不会自动带来权限码 |
local;业务代码跑在你自己的服务器 → remote。两种包都以根目录(或唯一顶层目录)下的 plugin.json 为清单。remote 通过审核后,原提交者可取得本插件的专属验签密钥,用于验证平台签名;不需要也不应该索取平台全局密钥。
给 AI 编码助手的直接入口
把下面地址提供给 AI,比让它猜页面文字可靠。全部为公开、机器可读资料:
/docs/devcenter/llms.txt/docs/devcenter/ai-context.json/docs/devcenter/schemas/plugin-manifest.schema.json/api/openapi.json文档版本与变更记录见 CHANGELOG。机器合同的请求体覆盖边界也在 AI 索引中明确标注,不要跳过。
开发指南
零、第一步:拿到平台账号
开发者中心本身不创建账号,也没有「用第三方登录直接成为开发者」的通道。按你的身份选一条:
| 你的情况 | 怎么拿账号 | 拿到之后的身份 |
|---|---|---|
| 外部开发者 | POST /api/app/register,body 至少含 username、password;可选 nickname / email / phone | 强制 external 层级,不能自提权;随后 POST /api/auth/login 取 data.token |
| 平台内部人员 | 由管理员在后台创建(POST /api/admin/users,需管理权限),不走公开注册 | 按管理员授予的层级与 RBAC 权限码 |
# 1) 注册(用户名 3-32 位字母/数字/下划线/连字符;密码 8-64 位且含字母和数字)
curl -sS -X POST https://cfapi.aog.cc/api/app/register \
-H 'Content-Type: application/json' \
-d '{"username":"dev_user","password":"Abc12345","nickname":"开发者"}'
# 2) 登录,token 在 data.token
curl -sS -X POST https://cfapi.aog.cc/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"dev_user","password":"Abc12345"}'
注册与登录都按 IP 限流(默认注册 5 次/分钟、登录 10 次/分钟),超限返回 429/42900。若注册返回 403「当前已关闭开放注册」,说明平台关闭了自助注册,只能由管理员开通——这不是你请求写错了。
一、你能开发哪些形态
| 想做的形态 | 登记类型 | 真实运行方式 |
|---|---|---|
| 网页 / H5 / 单页应用 | local | ZIP 解压到插件目录,平台同源静态托管;默认入口是 index.html |
| 前后端分离的网站 | remote | 只登记 base_url;平台把 /api/plugins/{code}/proxy/** 转发到你的服务器 |
| 微信 / 支付宝小程序、APP、桌面软件 | remote | 客户端界面在各自平台;业务后端由你托管,经平台代理或直接调 REST |
| 纯后端能力 / 定时任务 | remote | 进程在你自己的服务器,平台不托管其运行 |
| 受控脚本 / WASM | wasm 或 sandbox | 登记类型只是标签。必须另建沙箱配置并处于 active 才可执行;当前是内部受控能力,不是外部开发者自助运行环境 |
wasm/sandbox 不会因为注册类型就自动创建沙箱、更不会自动执行;市场上架批准也不会自动把插件放进沙箱。当前真实可自助发布的交付形态是 local 与 remote,其余属于内部运维能力。二、接入前必须知道的
base_url三、跨域(最容易踩的坑)
平台跨域是白名单制,默认只放行本站。若你的前端部署在自己域名、想直接 fetch 平台接口,浏览器会拦截。
解决(二选一):① 让平台方把你的域名加入后台「设置 → cors_allowed」;② 用 remote 类型,通过平台反向代理 /api/plugins/{code}/proxy/...(服务端到服务端,不受跨域限制,最省心)。
提示:本开发者中心与你同域,调接口无跨域问题,可直接测试。
四、身份、权限与三套凭据
先记结论:登录令牌不等于权限。所有需要权限码的接口,都会先校验登录身份,再校验权限码;API 密钥还多一道 scope 闸门。身份合法但缺权限,返回 403,不是重新登录就能解决。
| 凭据 | 取得方式 | 用途 | 不能做什么 |
|---|---|---|---|
| 用户登录 JWT | POST /api/auth/login,取 data.token | 管理端页面、插件市场投稿、需要用户身份的调用 | 不会自动获得插件管理权限;不能绕过层级 |
| API Key | 后台或个人中心签发,形如 ak_... 的服务端密钥 | 服务端到服务端调用 | scope 只是前置闸门;仍受账号层级与 RBAC 约束,不能提权 |
| 插件作用域令牌 | POST /api/plugins/{code}/token,使用平台下发的插件密钥做 HMAC 签名 | 插件后端在最小 scope 内调平台能力 | 只能申领管理员已授权且未过期的 scope;只对所属插件生效 |
前台账号的开发者层级:external(默认)、internal(只获 plugin.view)、internal_advanced(获 plugin.view、plugin.manage、dev.manage)。治理权限 plugin.govern 不在任何开发者层级里,只由后台 RBAC/超管持有。
插件自有用户账号与平台账号完全隔离:终端用户注册 POST /api/plugins/{code}/users/register,登录 POST /api/plugins/{code}/users/login。当前登录接口只校验凭据并返回 user_id、display_name、logged_in_at,不会签发访问令牌;不要把它当平台 API 的授权凭据。平台 API 的身份仍来自平台 JWT/API Key 或插件作用域令牌。
不要把平台登录令牌交给第三方插件页面,也不要把插件密钥、API Key 发到浏览器。数据岛归属由平台调用者身份决定,不由插件自有用户的 username 决定。
local 插件的 HTML/JS 对访问者可读。任何 API Key、插件密钥、plugin.manage 令牌都不得写进前端包、localStorage 或公开仓库。浏览器端只持有终端用户自己的登录态;高权限调用必须放在受信任服务端。五、统一响应规范
{ "code": 0, "message": "ok", "data": { } }
| HTTP | code | 含义 | 处理 |
|---|---|---|---|
| 2xx | 0 | 成功 | 读 data |
| 400 | 40000 | 参数错误 | 检查请求体/路径 |
| 401 | 40100 | 未认证或令牌过期 | 重新登录换 token,不要重试同一枚过期令牌 |
| 403 | 40300 | 无权限:缺权限码、层级不足或不是归属人 | 核对权限码与层级,换密钥无效 |
| 404 | 40400 | 资源不存在 | 检查 code / ID,或目标插件未启用 |
| 409 | 40900 | 冲突:重复提交、用户名已存在、状态不允许 | 先查当前状态再决定是否重试 |
| 429 | 42900 | 触发限流(按 IP 或账号的滑动窗口) | 退避后重试,减少并发;不要换密钥硬刷 |
| 500 | 50000 | 服务端内部错误 | 带时间点与请求路径报障 |
| 503 | 50300 | 服务暂不可用(升级/保护/依赖不可达) | 稍后重试,不要当成权限问题 |
限流是 429/42900,不是 403。403 只表示权限与归属问题;把 429 当 403 去换密钥或改权限,只会继续被限流。失败响应固定为 { "code": 非0, "message": "...", "data": null },程序分支请依据 code 与 HTTP 状态码,不要解析 message 文案。
分页与列表约定
列表接口统一用 page + page_size,不是 offset/limit 混用:
| 参数/字段 | 含义 | 默认与边界 |
|---|---|---|
page | 页码,从 1 开始 | 默认 1 |
page_size | 每页条数 | 默认 20;服务端夹到 1–100,传 1000 也只返回 100 |
data.items | 当前页数据 | 空结果时是 [],不是 null |
data.total | 符合条件的总条数 | 用于算总页数:ceil(total / page_size) |
data.page / data.page_size | 服务端实际采用的页码与页大小 | 以返回值回显为准,不要假设等于入参 |
插件数据集合、插件用户列表、后台列表均遵循这套约定;少数按游标或固定上限的接口会在该接口的 OpenAPI 描述里单独说明。翻页期间数据可能变化,需要强一致时按 id 递增拉取并去重。
幂等与重试
- 可安全重试:
GET、以及语义幂等的PUT(改状态、改配置、设置归属);网络超时后重发不会产生重复对象。 - 不要盲目重试:
POST创建类(市场投稿、上传、插件数据新增、发起扣费)。超时只说明你没收到响应,不说明服务端没执行;应先查询当前状态(如GET /api/market/submissions)再决定是否重发。 - 撤回是删除:
POST /api/market/submissions/{code}/withdraw会移除这条 pending 提交,不会保留草稿;撤回后要重新走POST /api/market/submit。 - 重试节奏:只对 429/42900、500/50000、503/50300 与网络错误退避重试;400/401/403/404/409 重试多少次结果都一样。建议指数退避并加随机抖动,单请求最多重试 3 次。
六、5 种插件类型(登记标签)
| type | 含义 | 实际执行/托管 |
|---|---|---|
local | 静态资源包,解压到插件目录 | 平台静态服务托管;不执行服务端代码 |
remote | 远程服务,登记 base_url | 平台仅反向代理,进程与数据责任在你 |
wasm | WASM 运行时标签 | 需另建沙箱配置并处于 active;默认 wasmi,不是挂上类型就能跑 |
sandbox | 进程沙箱标签 | 需另建配置并受内部策略控制;隔离强度取决于实际配置与宿主能力 |
disabled | 停用状态 | 不对外提供页面或代理 |
七、发布与安装是两条不同的链
| 链路 | 接口 | 身份/权限 | 结果 |
|---|---|---|---|
| 内部私有安装 | POST /api/plugins/upload?code={code},body 是 ZIP 原始字节 | 需 plugin.manage;实际只有 internal_advanced 等具备 | 文件落地并注册,可自行启用 |
| 市场投稿上传 | POST /api/market/upload?plugin_code={code},multipart/form-data,字段 file | 任意有效登录身份;不需要 plugin.manage;校验归属 | 返回本地 download_url,尚未上架 |
| 提交审核 | POST /api/market/submit | 有效登录身份 + 验证码;每用户每分钟最多 3 次 | 状态 pending |
| 管理员批准 | POST /api/market/submissions/{id}/approve | plugin.govern | 写入市场版本;不会自动安装、启用或启动沙箱 |
| 实例管理员安装 | GET /api/plugins/market → POST /api/plugins/market/install | 浏览需 plugin.view;安装需 plugin.manage | local 拉包落地;remote 只登记,归属平台 |
清单统一使用 plugin.json。必备字段是 code,且必须与上传 query / 市场 plugin_code 完全一致;支持 name、version、type、base_url、description,以及 entry、dependencies、requirements、permissions。ZIP 可含唯一顶层目录,服务端会自动上提;符号链接会被清除。
entry 不会改变 local 页面入口。静态托管固定以目录下 index.html 作为入口;请把你希望访问的首页命名为 index.html。plugin.toml 不是本平台的上传清单。可下载 plugin.json JSON Schema 做本地校验。内部管理接口(启用/停用/卸载)分别为 POST /api/plugins/{code}/enable、POST /api/plugins/{code}/disable、DELETE /api/plugins/{code},都需要 plugin.manage 与归属校验。
八、平台能力与访问边界
插件能力接口位于 /api/plugins/{code}/...;它们不是“有 external 密钥就能全调”。能否调用取决于账号层级 + 权限码 + 插件 scope 授权 + 目标插件状态。
| 能力 | 典型端点 | 开发边界 |
|---|---|---|
| 文档集合 / Schema / KV | /data/{collection}、/kv/{key} | 需插件身份与权限;按插件 + 用户数据岛隔离 |
| 对象存储 / 文件 | /storage/objects、/files | 文件上传 multipart,不是 JSON |
| 插件用户 | /users/register、/users/login | 独立用户目录;登录不签发 token,不用于平台 API 鉴权 |
| AI / 支付 / 推送 | /ai/chat、/payments、/push | 受插件 scope、账号层级与平台配额共同约束 |
| 沙箱执行 | /sandbox/exec | 仅具备权限且已配置 active 沙箱时可执行;默认网络 deny、文件系统 readonly |
已核对的插件端点和参数以 OpenAPI 3.1 为准;页面「接口文档」按路径搜索,交互式调试用 Swagger。OpenAPI 中标记 internal-admin-generic 的内部运维接口不构成对外开发承诺。
八·补、调用其他插件的功能(组装式开发)
你开发自己的插件时,不需要自己实现所有能力——可以直接调用平台上已上架、已开放外部调用的插件。 这就是「组装式开发」:别人做好的图片库、表单生成、监控面板等,你一行请求就能用。
① 发现可调用插件
打开本页顶部「可调用插件」标签,它实时拉取 GET /api/plugins/callable,
列出所有 approval_status=approved 且已启用的插件,每张卡片都带可复制的调用示例。
② 调用约定
只有 remote 且配置了 base_url 的插件可经代理调用。local 插件没有后端,访问其 /proxy 会返回“未配置后端地址”,不要把它当 API 服务。
GET /api/plugins/<对方code>/proxy/<对方暴露的路径> # 需要权限码 plugin.view
POST /api/plugins/<对方code>/proxy/<对方暴露的路径> # 需要权限码 plugin.manage
Authorization: Bearer <有对应权限的登录令牌或 API Key>
Content-Type: application/json
{ "arg1": "value1" }
代理仅接受 GET / HEAD / OPTIONS / POST / PUT / DELETE;请求 query 会原样透传,响应上限 16 MB,默认 15 秒超时。
③ 权限与配额(最容易误判的一段)
- “市场可见”不等于“无需授权”。读调用仍要求调用者具备
plugin.view,写调用要求plugin.manage。插件处于approved只说明它可被列为目录项,不会替你补齐权限码。 - 仅有
external层级不会自动获得任何权限码;API Key 的 scope 只是第一道闸门,仍要经过层级/RBAC 判定。若调用返回403,先核对权限码,不要反复换密钥。 - 目标插件必须已注册且启用(
status=1),否则代理返回 404/403,与网络无关。 - 代理会剥离你的
Authorization、Cookie、x-api-key、x-auth-token,所以对方后端不可能代你操作平台账号。
④ 完整示例(服务端调用可公开的 remote 插件)
// 服务端执行。API Key 必须同时具备对应权限码,不能只写 scope 就假设能调。
const resp = await fetch(
'https://cfapi.aog.cc/api/plugins/<remote_code>/proxy/list',
{
method: 'GET', // 读接口;写接口改成 POST 并确保有 plugin.manage
headers: {
'Authorization': 'Bearer ' + process.env.CFAPI_API_KEY,
'Accept': 'application/json'
}
}
);
if (!resp.ok) throw new Error('HTTP ' + resp.status + ': ' + await resp.text());
const result = await resp.json();
// 平台统一响应 { code, message, data };目标插件自定义业务结果在代理响应体中
if (result.code !== 0 && result.code != null) throw new Error(result.message || '插件调用失败');
⑤ remote 后端如何验证平台签名(必须实现)
平台转发时注入以下头。新接入只应验证按插件派生的 X-Platform-Signature,不要依赖旧的全局签名头:
| 请求头 | 含义 |
|---|---|
X-Platform-Signature | 推荐校验:HMAC-SHA256(插件专属密钥, {plugin_code}{timestamp}{tenant}) |
X-Platform-Caller-Signature | 推荐校验:HMAC-SHA256(插件专属密钥, {plugin_code}{timestamp}{tenant}{sub}) |
X-Platform-Signature-Version | 当前为 v1,用于未来平滑升级 |
X-Platform-Timestamp | 秒级时间戳;建议拒绝 ±300 秒之外的请求 |
X-Platform-Plugin | 目标插件 code,必须与自身一致 |
X-Platform-Tenant | 调用者租户 ID,供多租户隔离 |
X-Platform-Caller-Sub | 调用者用户 ID(纯数字),用于用户级数据归属 |
X-Platform-Token / Caller-Token | 旧兼容头:使用平台全局密钥签名,外部插件无法验证也不应依赖;新代码改用 Signature 头 |
① 市场已批准插件:原始提交者用登录 JWT 调
GET /api/market/submissions/{code}/signing-secret;② 私有插件:归属人用具备
plugin.manage 的令牌调 GET /api/plugins/{code}/signing-secret。两者都返回 按 plugin_code 派生的密钥,而不是平台全局密钥。它只能验证你这一个插件;放入服务端环境变量,禁止写前端或提交到仓库。平台轮换全局密钥后派生密钥会变化,需同步更新。
平台签名校验参考代码(必须实现)
常量时间比较 + 时间戳窗口 + plugin code 一致性,三者缺一不可。
Node / Express
const crypto = require('crypto');
function safeEqual(a, b) {
const x = Buffer.from(String(a || ''), 'utf8');
const y = Buffer.from(String(b || ''), 'utf8');
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
function verifyPlatform(secret, pluginCode, headers) {
const sig = headers['x-platform-signature'];
const ts = headers['x-platform-timestamp'];
const tenant = headers['x-platform-tenant'];
const plugin = headers['x-platform-plugin'];
if (!sig || !ts || tenant == null || plugin !== pluginCode) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(pluginCode + ts + tenant)
.digest('hex');
return safeEqual(sig, expected);
}
// 服务端路由中:
// if (!verifyPlatform(process.env.PLUGIN_SIGNING_SECRET, 'my_remote_plugin', req.headers)) {
// return res.status(403).json({ code: 40300, message: 'invalid platform signature' });
// }
Python(Flask / FastAPI)
import hmac, hashlib, time
def verify_platform(secret, plugin_code, headers):
sig = headers.get('X-Platform-Signature')
ts = headers.get('X-Platform-Timestamp')
tenant = headers.get('X-Platform-Tenant')
plugin = headers.get('X-Platform-Plugin')
if not sig or not ts or tenant is None or plugin != plugin_code:
return False
if abs(int(time.time()) - int(ts)) > 300:
return False
msg = f'{plugin_code}{ts}{tenant}'.encode()
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)
⑤-b 调用者身份与用户级数据岛
平台同时注入 X-Platform-Caller-Sub 与 X-Platform-Caller-Signature,用于证明本次请求代表哪个终端用户:
Caller-Signature = HMAC-SHA256(
plugin_signing_secret,
plugin_code + timestamp + tenant + caller_sub
)
校验方式与平台签名相同,只是消息多了末尾的 caller_sub。只有签名有效时才信任该用户号。
import hmac, hashlib, time
def verify_caller(secret, plugin_code, headers):
sub = headers.get('X-Platform-Caller-Sub')
sig = headers.get('X-Platform-Caller-Signature')
ts = headers.get('X-Platform-Timestamp')
tenant = headers.get('X-Platform-Tenant')
if not (sub and sig and ts and tenant is not None):
return None
if not sub.isdigit() or abs(int(time.time()) - int(ts)) > 300:
return None
msg = f'{plugin_code}{ts}{tenant}{sub}'.encode()
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return sub if hmac.compare_digest(sig, expected) else None
用户级数据岛
- 每个平台用户的文档、KV、文件与对象各归各的数据岛,A 不能读、删或覆盖 B 的同名数据。
- 插件后端代表终端用户回调平台时,才能用该用户的数据岛;不能借调用者凭据跨到其他插件。
- 匿名公共收件箱落入具名岛
public,只有超管可显式读写;普通用户与外部密钥会被拒绝。
注意:平台代理绝不会把调用者本人的凭据(Authorization / Cookie / x-api-key)转发给你的后端。你的后端若需回调平台,应使用自己的服务端凭据,而不是借用用户令牌。
⑥ Python 版完整调用示例
import os, requests
resp = requests.get(
'https://cfapi.aog.cc/api/plugins/<remote_code>/proxy/list',
headers={'Authorization': 'Bearer ' + os.environ['CFAPI_API_KEY']},
timeout=20,
)
resp.raise_for_status() # 403 = 权限码不足;404 = 插件/路径不存在;504 = 对方超时
data = resp.json()
if data.get('code') not in (None, 0):
raise RuntimeError(data.get('message', 'plugin error'))
注意:被调用的插件具体暴露了哪些路径、要什么入参, 由该插件作者定义,请以其文档为准(卡片有「查看文档」按钮时优先看它)。平台只保证调用通道与鉴权可用。
九、最小可运行示例
plugin.manage 的高级内部账号。A. 私有 remote 插件(仅 internal_advanced)
# 1. 注册(全部写接口都要带同一枚 Bearer 令牌)
curl -sS -X POST https://cfapi.aog.cc/api/plugins \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"code":"demo_remote","name":"演示","version":"1.0.0","type":"remote","base_url":"https://your-server.example.com"}'
# 2. 启用(不能漏 Authorization,否则 401)
curl -sS -X POST https://cfapi.aog.cc/api/plugins/demo_remote/enable \
-H "Authorization: Bearer $TOKEN"
# 3. 读调用(调用者需 plugin.view)
curl -sS https://cfapi.aog.cc/api/plugins/demo_remote/proxy/health \
-H "Authorization: Bearer $TOKEN"
# 4. 写调用(调用者需 plugin.manage)
curl -sS -X POST https://cfapi.aog.cc/api/plugins/demo_remote/proxy/items \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"demo"}'
B. 私有 local 插件(仅 internal_advanced)
# 根目录(或唯一顶层目录)必须含 plugin.json,且 code 与 query 一致
my_local_plugin/
├── plugin.json # {"code":"my_local_plugin","name":"演示","version":"1.0.0","type":"local"}
├── index.html # 固定入口,不要改名
└── app.js
# 原始 ZIP 字节上传,不是 multipart
curl -sS -X POST "https://cfapi.aog.cc/api/plugins/upload?code=my_local_plugin" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @my_local_plugin.zip
# 静态页面(无需 Bearer):https://cfapi.aog.cc/plugins/my_local_plugin/
C. 外部开发者发布到市场(正式路径)
完整命令见下一章「安全 & 发布」。核心区别:先登录 → 市场 multipart 上传拿到 download_url → 验证码 → /api/market/submit → 等待审批。不需要 plugin.manage,也不会在本地直接运行。
接口文档
下方只加载唯一权威合同 /api/openapi.json。openapi_extra.json 已废弃并清空,不再参与合并。权限码标签是接口要求,不代表当前账号已具备。
加载中…
交互式调试(Swagger UI)
Swagger 与上方列表读取同一份主规范。调试前先点 Authorize 填 Bearer 令牌;403 请检查权限码与账号层级。
安全 & 发布
本章是外部开发者最该逐行读的部分:市场投稿、审核、安装、升级、回滚是四件不同的事,任何一步都不会“顺便”替你完成下一步。
一、外部开发者发布到市场(完整可执行流程)
plugin.manage,也不需要私有安装。登录,拿用户 JWT
POST /api/auth/login(JSON:username、password);后续请求统一带Authorization: Bearer <data.token>。没有登录态会 401。打包并自检
ZIP 根目录或唯一顶层目录下必须有
plugin.json;code只允许小写字母/数字/下划线,1–64 位,必须与上传参数完全一致。入口固定为index.html;plugin.toml不会被识别。上传包(multipart,不是原始字节)
POST /api/market/upload?plugin_code={code},Content-Type: multipart/form-data,文件字段名必须是file,上限 500 MB。成功返回data.download_url,但此时还没有提交审核。获取一次性验证码
GET /api/market/captcha返回token与算术题question。答案只用于紧接着的一次提交,过期/复用会失败。提交审核
POST /api/market/submit,JSON 必填plugin_code、name、download_url,再带version(严格x.y.z)、description、author、email、category、tags、captcha_token、captcha_answer。成功进入pending。查看状态 / 撤回
GET /api/market/submissions?status=pending只能看到自己的提交;POST /api/market/submissions/{code}/withdraw仅能撤回自己处于pending的记录。撤回是删除动作:该提交记录会被移除,不保留草稿;已进入审核结果(approved / rejected)的提交不能撤回,要重新提交只能再走一次POST /api/market/submit。管理员审核
POST /api/market/submissions/{id}/approve需plugin.govern;静态扫描命中危险项时默认拒绝,只有显式force=1才能强制批准。批准后发生了什么
只更新市场提交状态并新增/切换当前版本记录。不会安装到任何人的插件目录、不会自动启用、不会自动创建或启动沙箱。安装由具备
plugin.manage的实例管理员发起,不是任意终端用户能自助完成的动作。remote:取插件专属验签密钥
批准后,原始提交者用登录 JWT 调
GET /api/market/submissions/{code}/signing-secret,把返回的plugin_secret放服务端环境变量,用于验证X-Platform-Signature/X-Platform-Caller-Signature。该密钥按插件派生,不是平台全局密钥;禁止写进前端或仓库。
可直接复制的最小命令链
API=https://cfapi.aog.cc
# 1) 登录(把用户名、密码换成你的)
TOKEN=$(curl -sS -X POST "$API/api/auth/login" \
-H 'Content-Type: application/json' \
-d '{"username":"YOUR_USER","password":"YOUR_PASSWORD"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["token"])')
# 2) 市场上传:multipart + 字段名 file
curl -sS -X POST "$API/api/market/upload?plugin_code=my_plugin" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@my_plugin.zip;type=application/zip"
# → {"code":0,...,"data":{"download_url":"/api/market/uploads/my_plugin.zip",...}}
# 3) 取验证码(返回 token + question,人工/程序算出答案)
curl -sS "$API/api/market/captcha" -H "Authorization: Bearer $TOKEN"
# 4) 提交审核(答案填上一步)
curl -sS -X POST "$API/api/market/submit" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"plugin_code":"my_plugin","name":"我的插件","version":"1.0.0",
"download_url":"/api/market/uploads/my_plugin.zip",
"description":"功能说明","category":"productivity",
"captcha_token":"<token>","captcha_answer":"<answer>"}'
# 5) 查看我的提交
curl -sS "$API/api/market/submissions" -H "Authorization: Bearer $TOKEN"
file;plugin_code 带连字符或与 plugin.json 不一致;版本不是三段数字;版本不高于该插件历史最高上架版本;验证码被复用;重复使用别人已占用的插件标识。二、升级、回滚、下架(语义不要猜)
| 操作 | 接口 | 实际效果 |
|---|---|---|
| 发布新版本 | 再次走 upload + submit | 版本必须严格高于历史最高上架版本;重新进入 pending,再次审核 |
| 查看版本历史 | GET /api/market/versions/{plugin_code} | 仅提交者本人或超管可看 |
| 切换当前版本 | POST /api/market/versions/{id}/set-current | 需 plugin.govern。只切换市场“当前版本”指针与目录展示;不会替换已部署文件、不会重装、不会重启 remote 服务 |
| 下架 | POST /api/market/plugins/{code}/unpublish | 需 plugin.govern + 归属校验;市场目录与安装入口同时移除 |
| 管理员更新已安装插件 | 再次 POST /api/plugins/market/install | 需 plugin.manage;local 才会重新拉包,remote 只更新登记 |
set-current。对已经安装的 local 插件,要重新安装目标版本;对 remote 服务,发布与回滚由你自己服务器完成。平台无“一键替换运行中第三方服务”的能力。三、local / remote 的真实安全边界
| 形态 | 平台提供 | 平台不提供 |
|---|---|---|
local | 同源静态托管、路径越界防护、HTML 禁缓存、其余资源 7 天缓存、符号链接清除、ZIP 静态扫描 | 服务端代码隔离、密钥保管、浏览器端权限控制。页面里的任何密钥都可被访客读取 |
remote | 鉴权反向代理、按插件隔离的签名密钥与签名头、剥离用户凭据、SSRF 校验、响应 16 MB 上限、默认 15 秒超时 | 你的服务器补丁、数据备份、进程隔离与可用性。你的后端必须自行校验 X-Platform-Signature / X-Platform-Caller-Signature |
wasm/sandbox | 受控执行接口、配置化网络/文件/资源限制、运行记录 | “注册类型即自动沙箱”;外部开发者自助运行;无限制的系统调用能力 |
静态扫描只能降低明显风险,不等于代码审计。审核通过也不意味着平台为插件行为背书;插件造成的业务、数据与合规责任仍由发布者承担。
四、沙箱真实配置与资源上限
沙箱必须在插件之外另有配置记录且生命周期为 active;运行时只接受 wasm / process,路径为 POST /api/plugins/{code}/sandbox/exec,需要 plugin.manage,且受内部层级策略约束。
| 项目 | 默认 | 硬限制 / 说明 |
|---|---|---|
| 执行超时 | 5000 ms | 最小 500 ms,最大 120000 ms |
| 内存 | 256 MB | 最小 16 MB,最大 2048 MB |
| CPU | 1000 ms | 硬上限 120000 ms |
| 并发 | 8 | 全平台同时运行沙箱数上限 |
| 单次日志落库 | 64 KB | 超出截断,避免撑爆数据库 |
| 网络 | deny | 可在受控配置中改为 allow;不是所有执行都天然断网 |
| 文件系统 | readonly | 可选 allow/readonly/deny;隔离强度取决于宿主能力与实际策略 |
| WASM 运行时 | wasmi | wasmtime 仅在编译 feature 开启时可用;未开启会明确报错 |
| 入口文件 | — | 必须是相对路径,禁止绝对路径与目录穿越;文件缺失即拒绝 |
进程沙箱依赖 unshare/chroot/prlimit 等宿主能力;缺少必要隔离条件时会拒绝需要隔离的执行,而不是静默降级。
五、排错与上架前检查清单
| 现象 | 先检查 | 正确动作 |
|---|---|---|
| 401 缺少登录凭证 / 登录已过期 | 是否漏了 Authorization;令牌是否复制完整 | 重新登录;每个写请求都带上 Bearer,不要只给第一个请求加 |
| 403 缺少权限 / 层级不允许 | 权限码、账号层级、目标插件层级 | 市场投稿不需要 plugin.manage;私有管理才需要。不要用换密钥掩盖缺权限 |
| 上传返回“缺少 plugin.json” | ZIP 是否只有一层目录;清单是否真的叫这个名字 | 把清单放在根或唯一顶层目录;Windows 下确认没有多套一层根目录 |
| 上传返回“code 与上传路径不一致” | plugin.json.code 与 query 参数 | 两者改成完全相同的小写标识 |
| 市场提交返回版本错误 | 是否是 x.y.z、是否高于历史最高版本 | 递增 PATCH,不能原地覆盖同版本 |
| 代理 502 / 504 | base_url 是否可访问、是否被 SSRF 规则拒绝、对方是否超时 | 先用服务端 curl 自测;确认公网可达、HTTPS、响应在 16 MB 内 |
| 批准后市场仍不能安装 | 插件是否被下架、用户是否具备安装权限 | 审核批准 ≠ 自动安装;安装是独立动作 |
index.html 可直接打开;无密钥、无私钥、无真实用户数据;所有请求有超时与错误分支;remote 服务校验平台签名;版本已递增;验证码与下载地址来自本次上传;README 已按真实端点编写。插件示例下载
plugin.json.code 与你要上传的 plugin_code 一致 → 市场投稿(multipart)或内部高级账号私有安装(raw ZIP)。不要把模板当“批准后就会自动运行”的示例;市场批准只完成上架,安装与启用是后续动作。
可调用插件
这里列出已上架且启用的插件元数据。只有 remote 且配置了 base_url 的插件可以经 /api/plugins/{code}/proxy/ 调用;local 插件没有可代理后端。
plugin.view,写调用需要 plugin.manage。仅有 external 身份不会自动获得权限码;API Key 的 scope 也不是完整授权。若下方试调返回 403,请与平台管理员确认权限码,而不是反复更换密钥。
我的工作台
plugin.manage,也不会在本站直接运行插件;高级内部开发者才会看到并使用「私有插件」。
① 登录态 / 开发令牌
从用户中心已登录状态自动同步;也可以手动粘贴。令牌只保存在本页内存,不上传第三方。市场投稿允许普通登录身份,私有插件管理才需要相应权限。
② 私有插件(仅 internal_advanced)
plugin.manage 且只能管理归属于自己的插件。普通外部与普通内部账号无法使用;外部开发者请直接看下方「市场投稿」。创建 remote 私有插件
上传 local 私有插件(ZIP 原始字节)
③ 市场投稿(所有已登录开发者)
上传得到 download_url → 获取验证码 → 提交审核。批准后进入市场;由具备 plugin.manage 的实例管理员安装是另一个动作。
提交审核信息
④ 我的市场提交
点上方按钮查看。
我拥有的私有插件
点「刷新状态」加载。
内部能力(仅内部开发者)
external(默认,无自动权限)、internal(仅 plugin.view)、
internal_advanced(plugin.view、plugin.manage、dev.manage)。层级变更最迟约 20 秒生效。
治理权不等于“内部高级自带”:审批、下架、切换当前版本、scope 授权等需要
plugin.govern;该权限由后台 RBAC/超管持有,并仍有插件归属校验。任何开发者层级都不会自动获得它。
内部专属能力组(是否可用由层级策略与权限码共同决定)
事件、Hook、UI Slot、配置与内核扩展
WASM / 进程受控执行
定时任务
插件文件与受控文件接口
异步任务队列
如何获得内部文档与能力
内部版铁律与使用说明书不在公开静态目录提供,必须用有效内部凭证从受鉴权入口读取;开发者中心的「平台文档」会在检测到内部层级后通过带 Authorization 的请求加载,不把内部副本暴露成公开 URL。
若需要更高层级,请由后台管理员在「用户管理」调整;不要尝试用令牌文本或前端显隐伪造层级。外部账号即使修改页面元素,服务端仍按真实账号层级判定并返回 403。
内部层级不改变插件归属:普通内部只读;高级内部可管理自己的插件;平台插件仅由具备治理权的管理员维护。越权调用会写入审计。
高级内部开发者:能力边界
- 可创建、上传、启停、卸载归属于自己的插件。
- 可在已有 active 沙箱配置且层级策略允许时执行沙箱;默认网络
deny、文件系统readonly。 - 可写入自身插件的配置、数据与受控文件能力。
- 不可管理平台插件或他人插件;不可审批自己的市场投稿;不可授予 scope 或修改层级策略。
不要把“高级内部”理解成“小型超管”。生产变更会进入审计;治理与归属校验不会因层级高而跳过。
常见问题
Q:我的前端在自己域名,调接口报跨域?
把域名加入后台 cors_allowed,或改用 remote 插件 + /proxy/ 反向代理。
Q:插件数据存哪?能直连数据库吗?
数据通过 REST 接口(文档集合 / 键值 / 对象存储)读写,禁止直连数据库(对插件是黑盒)。
Q:平台自己用什么数据库?能换吗?
三种后端:SQLite 3.46(默认,库文件 app/var/data/cfapi.db)、MariaDB 11.8、PostgreSQL 18.6。后台「数据库管理」可一键互斥切换,或命令行 scripts/db_port/cutover.sh sqlite|mysql|postgres;切换自动完成「停写 → 快照 → 搬运 → 行级校验(差异必须为 0)→ 重启 → 健康检查」,任一步失败自动回滚。
Q:换数据库会影响插件吗?
不会。插件看不到后端类型,也不允许直连;切换期间会短暂停写,避开高峰即可。当前后端可查 GET /api/db-backend/status(需管理凭证)。
Q:更新了插件前端,用户看不到新版?
静态资源有 7 天缓存,让用户强刷(Ctrl+Shift+R)或给资源加版本号参数。
Q:想做付费功能?
用 /api/plugins/{code}/payments 发起扣费,无需自己接支付通道。
Q:想接 AI?
用 /api/plugins/{code}/ai/chat,平台网关已对接多家大模型。
Q:高并发被限流?
先按 429/42900 做退避重试,减少并发与重复请求。额度由平台管理员在后台调整,外部开发者不能在客户端自行扩容;换密钥不会绕过限流。
开发工具
点击标签在下方内置窗口打开;刷新与新窗口按钮在右侧。
SDK 与接口集合
由公开契约自动生成,覆盖 138 个公开/插件操作;内部管理接口不在其中,也不承诺兼容。生成源见 openapi.public.json。
SDK 用法:client.setConfig({ baseUrl: 'https://cfapi.aog.cc' }),登录后把 data.token 放进 Authorization: Bearer。Postman 集合已内置 baseUrl 与 token 两个变量。
接入协议与选型指引
平台对外提供 20 类接入方式。先看「怎么选」,再看每种的实际限制,避免选错协议白做工。以下能力均已在线验证。
其中 gRPC 仅提供 gRPC-Web(JSON) 兼容形态,原生二进制 gRPC 不支持,详见下表。
一、四句话选型
| 你要做的事 | 用什么 |
|---|---|
| 插件内部调用平台能力 | REST + 插件作用域令牌;事件回调用 Webhook |
| 自建后端 / 外部系统集成 | API Key 或 OAuth2 / OIDC,配合 OpenAPI 生成 SDK |
| AI / Agent 工具接入 | MCP(首选)或 JSON-RPC 2.0 |
| 需要强类型 RPC 客户端 | Connect(官方 Connect 客户端原生支持) |
二、逐项能力与限制
| 协议 | 入口 | 适用场景 | 限制 / 注意 |
|---|---|---|---|
| HTTP / REST | /api/plugins/{code}/... | 插件开发主通道:数据、KV、存储、文件、配置、状态、AI、支付 | 统一响应 {code,message,data},code=0 为成功 |
| JSON-RPC 2.0 | POST /api/rpc | 轻量脚本、批量调用 | 8 个方法:health / version / openapi / plugins.list / plugins.get / market.list / db.tables / http.request;其中 http.request 可转发任意 REST 端点(万能兜底),支持批量数组 |
| MCP | POST /api/mcp | AI / Agent 工具发现与调用 | 8 个工具;标准流程 initialize → tools/list → tools/call |
| ConnectRPC(Connect) | POST /connect/cfapi.v1.{Service}/{Method} | 强类型 RPC | 3 个服务 5 个方法:SystemService.Health / Version / Watch,PluginService.List / Get,GatewayService.Call;支持 json 与 proto 两种 codec,Watch 为服务端流 |
| gRPC(原生) | — | — | 不支持:无二进制帧与 trailers,请用 Connect 或下面的兼容形态 |
| gRPC-Web(JSON) | POST /api/grpc/{service}/{method} | 调试兼容 | JSON 封装而非二进制;方法名用点分形式,如 /api/grpc/system/health |
| GraphQL | POST /api/graphql | 一次请求取多类数据 | 12 个字段(me / plugins / users / roles / 角色增删改 / 插件注册 / 上下架 / 市场安装 / 超管改状态),偏管理面 |
| WebSocket | GET /ws?token=<JWT> | 实时系统事件 | 必须 JWT;订阅全站事件仅限内部层级,外部令牌会被拒 |
| SSE | GET /api/events | 轻量事件流 | 只推 system_event 与 error,不是插件业务推送通道 |
| Webhook | /api/plugins/{code}/hooks | 插件事件回调首选 | 平台按事件投递到你的 URL,带投递记录、重试与测试触发 |
| OpenAPI | GET /api/openapi.json | 生成各语言 SDK、导入 Postman | OpenAPI 3.1.0 |
| OAuth2 | POST /api/oauth/token | 终端用户授权码流程 | 面向你的应用用户,不是插件间互调 |
| OIDC | GET /api/oauth/.well-known/openid-configuration | 标准发现 + JWKS | 可据此自行验签用户令牌,无需回源 |
| JWT | Authorization: Bearer <jwt> | 用户身份、离线验签 | EdDSA(Ed25519);公钥可从 JWKS 获取 |
| API Key(APIKey) | Authorization: Bearer <key> | 服务端到服务端调用 | 分内部 / 外部两级;外部默认最小权限(见第四节) |
| AdminAPI | /api/admin/* | 平台管理 | 需管理员权限,不是插件接口 |
| Metrics(MetricsAPI) | GET /metrics | Prometheus 抓取 | 运维面,插件不应依赖 |
| HealthCheck | GET /api/health | 探活 | 运维面 |
| CLI | 平台内 scripts/cfapi-cli.sh | 运维脚本 | 不是插件开发接口 |
三、三条身份通道(别混用)
| 通道 | 怎么拿 | 用在哪 |
|---|---|---|
| 插件作用域令牌 | POST /api/plugins/{code}/token(用插件专属验签密钥与 X-Platform-* 头换取),POST /api/plugins/{code}/token/verify 校验 | 插件后端调平台:数据 / KV / 存储 / 文件 / 状态 / AI;scope 必须先由管理员授权 |
| API Key | 后台创建,内部 / 外部两级 | 你的服务器、脚本、CI 调平台 REST |
| 用户 JWT / OAuth2 + OIDC | 用户登录后签发;或走 OAuth2 授权码 | 代表终端用户操作、前端直连 |
三者权限范围不同:插件令牌只在所属插件的命名空间内有效;API Key 按层级与授权范围收敛;用户 JWT 只能做该用户被允许的事。
四、外部开发者的默认权限边界
| 能力分组 | 外部层级默认 | 说明 |
|---|---|---|
datastore / jobqueue / storage / proxy / cdn | 开放 | 数据、任务队列、对象存储、反向代理、图片/字体库 |
kernel / sandbox / scheduler / files / shell | 关闭 | 内核扩展、WASM 沙箱、调度器、文件系统、命令执行;需管理员在后台显式授权 |
五、插件作用域端点清单
| 能力 | 端点(前缀 /api/plugins/{code}) |
|---|---|
| 文档数据 | /data/{collection}、/data/{collection}/{doc_id}、/data/{collection}/schema、/schemas |
| 键值(带 TTL) | /kv/{key} |
| 对象存储 | /storage/objects、/storage/objects/{key} |
| 文件 | /files、/files/upload、/files/download/{path} |
| 配置与状态 | /config、/config/schema、/store、/store/{key} |
| 插件间 RPC | /rpc、/grants、/grants/{scope} |
| 界面插槽 | /ui-slots、/ui-slots/{id} |
| 事件回调 | /hooks、/hooks/{id} |
| 访问令牌 | /token、/token/verify |
| AI | /ai/chat、/ai/usage |
| 支付 | /payments |
| 沙箱执行 | /sandbox/exec(需该分组被授权) |
| 健康 | /health、/health/ping |
六、最小可用示例
# 1) 插件后端读自己的数据(插件作用域令牌)
curl -H "Authorization: Bearer $PLUGIN_TOKEN" \
"https://cfapi.aog.cc/api/plugins/{code}/data/{collection}?page=1&page_size=20"
# 2) 外部系统用 API Key 调 REST
curl -H "Authorization: Bearer $API_KEY" \
"https://cfapi.aog.cc/api/plugins"
# 3) MCP:先发现工具,再调用
curl -X POST https://cfapi.aog.cc/api/mcp \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 4) Connect(官方客户端等价于这个 POST)
curl -X POST https://cfapi.aog.cc/connect/cfapi.v1.SystemService/Version \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
完整接口清单可在「接口文档」页按关键词搜索;需要 SDK 就直接用 OpenAPI 规范生成。
AI 开发资料
给编码助手一套确定的事实源,避免它从页面文案里猜能力。建议按「索引 → 完整上下文 → API 合同 → 清单 Schema」顺序提供。
可直接复制的开发提示词
机器合同的诚实边界
- OpenAPI 覆盖已挂载路由;核心插件端点和市场流程已给出请求体。
- 少数内部运维接口仍可能只有泛化请求体,AI 不应靠猜;先发最小请求并读错误。
openapi_extra.json已废弃并清空,不要再作为补充接口来源。- “字段存在”不等于“当前账号有权限”。权限码、层级、插件归属、启用状态都要同时判断。
平台文档
开发铁律 v0.5.17。公开版直接显示;内部版经鉴权接口读取。插件接口以 OpenAPI 与 AI 开发资料为准。