# cfapi.aog.cc 插件开发完整上下文(AI 用) 文档版本:2026.11.05-r1 核对时间:2026-11-05 目标读者:需要在 cfapi.aog.cc 上开发、调试、投稿或维护插件的 AI 编码助手与开发者。 本文件描述源码与线上行为,不描述愿望。遇到“文档说可以、实际返回 403/404”时,以本文件、OpenAPI 和实际接口响应为准,不要自行放宽平台权限。 ================================================================ 0. 先选路线,避免整条路走错 ================================================================ 平台有两种完全不同的插件流转: A. 外部开发者 → 市场投稿 → 管理员审核 → 具备 plugin.manage 的实例管理员安装 - 账号层级:external(默认)即可。 - 可以投稿,不能私有创建/上传/启用。 - 投稿不需要 plugin.manage。 - 审核批准不会自动安装、启用、部署或运行插件。 - 市场批准的 local 包不会给你一个“审核前预览实例”。 B. 内部高级开发者 → 私有插件 → 自建自用 - 账号层级:internal_advanced。 - 可创建 remote、上传 local、启用、停用、卸载。 - 只能管理归属于自己的插件;平台插件与别人的插件一律不能覆盖。 - internal(普通内部)只有 plugin.view,不能写。 如果你只有 external 账号,不要生成“调用 POST /api/plugins/upload 完成安装”的方案;它会因缺少 plugin.manage 失败。 ================================================================ 1. 权威资源 ================================================================ - 人读文档:https://cfapi.aog.cc/docs/devcenter/ - AI 索引:https://cfapi.aog.cc/docs/devcenter/llms.txt - 本文件:https://cfapi.aog.cc/docs/devcenter/llms-full.txt - 结构化上下文:https://cfapi.aog.cc/docs/devcenter/ai-context.json - 清单 JSON Schema:https://cfapi.aog.cc/docs/devcenter/schemas/plugin-manifest.schema.json - API 合同:https://cfapi.aog.cc/api/openapi.json - 协议清单:https://cfapi.aog.cc/api/protocols - 健康检查:https://cfapi.aog.cc/api/health - 版本:https://cfapi.aog.cc/api/version API 双前缀: - /api/... - /api/v1/... 两者挂载同一批路由,选一个并保持一致。不要为两个前缀写两套客户端。 统一响应: { "code": 0, "message": "ok", "data": ... } 平台错误码: 0 成功 40000 参数错误 40100 未认证或令牌无效 40300 无权限(权限码/层级/归属不符) 40400 不存在 40900 冲突(重复提交、用户名已存在、状态不允许) 42900 触发限流(HTTP 429) 50000 内部错误 50300 服务暂不可用 注意: - HTTP 状态码与业务 code 都可能出现;客户端必须同时检查。 - 代理 remote 插件时,响应体由目标插件决定,不保证符合平台统一响应。 - 401 不是“权限不足”,403 才是权限/层级不足;不要用换 token 掩盖 403。 - 限流是 429/42900,不是 403/40300。把 429 当权限问题去换密钥或改权限,只会继续被限流。 列表分页约定: - page(从 1 开始,默认 1)+ page_size(默认 20,服务端夹到 1–100)。 - 返回 data.items(空为 [])、data.total、data.page、data.page_size。 - 不要传 offset/limit 混用;需要强一致时按 id 递增拉取并去重。 幂等与重试: - GET 与语义幂等的 PUT 可安全重试。 - POST 创建类(市场投稿/上传/插件数据新增/发起扣费)不可盲重试;超时先查状态再决定。 - 只对 429/500/503 与网络错误退避重试;400/401/403/404/409 重试无意义。 - POST /api/market/submissions/{code}/withdraw 是删除动作,不移除已审核记录,也不保留草稿。 账号来源: - 外部开发者:POST /api/app/register(username 3–32 位字母数字下划线连字符;password 8–64 位且含字母和数字),成功后强制 external 层级,再 POST /api/auth/login 取 data.token。 - 平台内部账号由管理员 POST /api/admin/users 创建,没有公开自助注册。 - 注册/登录按 IP 限流(默认 5/分钟、10/分钟),超限为 429/42900。 ================================================================ 2. 账号层级与权限(源码口径) ================================================================ 开发者层级由后台管理员设置,服务端实时读库(约 20 秒缓存)。 external - 自动权限:无。 - 可:登录、市场投稿、查看目录(需接口自身允许)。 - 不可:私有插件管理;调用 proxy 读接口(需要 plugin.view,但默认不授予)。 internal - 自动权限:plugin.view。 - 可:只读查看与具备 view 的调用。 - 不可:插件写操作、沙箱执行、私有上传。 internal_advanced - 自动权限:plugin.view、plugin.manage、dev.manage。 - 可:管理自己拥有的插件、私有上传、沙箱执行等受控能力。 - 不可:治理操作。 plugin.govern - 审批、下架、切换当前版本、scope 授权、层级策略等治理动作。 - 只来自后台 RBAC/超管;任何开发者层级都不会自动获得。 - 高级内部也不能自审自批。 权限校验顺序: 1. API Key 必须有匹配 scope(scope 只是第一道闸门)。 2. 执行 ABAC 策略。 3. 超管短路。 4. 开发者层级授权。 5. 后台 RBAC。 因此: - API Key scopes 写 plugin.manage 不代表账号真的能管理插件。 - API Key 的层级不能高于归属账号。 - 不要建议用户“生成 internal 密钥”来绕过外部限制;前台开发者签不出高于自身层级的密钥。 三套凭据: 1. 用户登录 JWT:POST /api/auth/login → data.token。 2. API Key:服务端使用,按 scope + 账号权限收敛。 3. 插件作用域令牌:POST /api/plugins/{code}/token,只有插件专属签名密钥与管理员 scope 授权都具备时才能申领。 按插件隔离的验签密钥: - 平台不再要求外部插件持有全局 PLUGIN_SECRET。 - 市场插件审核通过后:GET /api/market/submissions/{code}/signing-secret(仅原始提交者或超管,状态必须 approved)。 - 私有插件:GET /api/plugins/{code}/signing-secret(需要 plugin.manage + 归属)。 - 返回的 plugin_secret 是按 plugin_code 派生的 HMAC-SHA256 密钥,只对单个插件有效。 - 该密钥用于验证 X-Platform-Signature 与 X-Platform-Caller-Signature,放在服务端环境变量。 - 平台轮换全局 PLUGIN_SECRET 后该派生密钥会变化,需要重新获取并更新插件后端。 - 旧 X-Platform-Token / X-Platform-Caller-Token 仍可能随请求下发,但使用平台全局密钥签名,只为兼容旧插件;新代码不得依赖。 关于插件作用域令牌: - scope 必须先由持 plugin.govern 的管理员授权。 - 插件用插件专属密钥对 `{code}{timestamp}` 做 HMAC-SHA256,带 X-Platform-Signature / X-Platform-Timestamp / X-Platform-Plugin / X-Platform-Signature-Version: v1 发起 POST /api/plugins/{code}/token,请求已授权的 scope。注意:申领令牌的签名串只有 code+timestamp,不带 tenant;代理回调签名才带 tenant。 - 即使有验签密钥,也不能申领未授权或已过期的 scope。 - 当前对外部开发者稳定可用的主路径是:自己的服务端(remote)持有 API Key 或插件作用域令牌;浏览器只持有终端用户登录态。 ================================================================ 3. 插件包与清单 ================================================================ 清单文件名:plugin.json 不是:plugin.toml 不是:package.json 作为清单(Node 包可以有,但平台只读 plugin.json) ZIP 结构: my_plugin.zip ├── plugin.json ├── index.html ├── app.js └── assets/... 也支持: my_plugin.zip └── my_plugin/ ├── plugin.json ├── index.html └── ... 若根目录没有 plugin.json 且只有唯一顶层目录,服务端会把该目录内容上提。存在多个顶层目录时不会猜。 plugin.json 关键字段: - code:必填;^[a-z0-9_]{1,64}$;必须与上传参数一致。 - name:展示名。 - version:建议/市场要求 x.y.z。 - type:local | remote | wasm | sandbox | disabled;默认 local。 - base_url:remote 必填,http/https,外部提交禁止内网/回环/元数据地址。 - description:说明。 - entry:相对入口;禁止绝对路径与 ..。对 local 静态托管无效果。 - permissions:数组。可用平台权限码(如 plugin.view)或 resource:action。上传/安装路径不会强制要求冒号;/api/manifest/scan 的解析器更严格(resource:action 或 *)。声明不等于授权。 - dependencies:数组;每项包含 plugin,version 默认 *,optional 默认 false。 - requirements:memory_mb / cpu_shares / disk_mb / features;默认 64 / 100 / 10 / []。 local 入口固定: - 访问 /plugins/{code}/ 或空路径 → index.html。 - 不读取 entry 来选入口。 - index.html 不存在就无页面可访问。 - HTML no-store;其他静态资源通常 7 天缓存,因此更新 JS/CSS 要带版本参数。 安全: - ZIP 内符号链接会被清除。 - 解压路径有防穿越处理。 - 上传大小:私有安装默认 50 MB;市场 500 MB。 - 静态扫描会检测危险扩展名/结构性危险内容,但不是完整代码审计。 完整 schema 使用: https://cfapi.aog.cc/docs/devcenter/schemas/plugin-manifest.schema.json ================================================================ 4. local 插件 ================================================================ 定义:静态前端由平台同源托管在 /plugins/{code}/。 不执行服务端代码。不是沙箱。 开发建议: - 纯静态 HTML/CSS/JS,或构建后的 SPA。 - 使用相对路径或 /plugins/{code}/ 下的资源路径。 - API 根地址用 https://cfapi.aog.cc/api(与页面同源时可用 /api)。 - 不要把 API Key、plugin_secret、plugin.manage 令牌写进代码或 localStorage。 - 需要高权限的调用放服务端;前端只调用面向终端用户的安全接口。 浏览器存储: - 用户中心用 localStorage 的 aog_token / aog_user。 - 不要在插件前端复制平台登录令牌到第三方站点。 - 第三方 cookies 受浏览器策略和平台 CORS 白名单限制。 CORS: - 平台 CORS 默认只允许 https://cfapi.aog.cc 等配置源,不是 *。 - 若前端部署在别的域名,必须让平台把该域名加入允许列表,或改用 remote 反向代理。 - 不要生成带 credentials 的跨域请求并假设会通过。 测试: - 审核前平台不提供 local 预览实例。使用本地静态服务器测试页面,用 curl 测试接口。 - 上传私有 local 仅 internal_advanced 可用;external 只能投稿并等待审核。 ================================================================ 5. remote 插件 ================================================================ 定义:只登记 base_url,平台反向代理。平台不托管你的进程,也不自动部署你的代码。 代理路径: ANY /api/plugins/{code}/proxy/{tail} 实际注册方法:GET、POST、PUT、DELETE(HEAD/OPTIONS 会走 actix 语义,但不要依赖未列出的方法)。 转发目标: {base_url}/{tail}?{原始 query} 读方法(GET/HEAD/OPTIONS): - 需要 plugin.view。 写方法(POST/PUT/DELETE 等): - 需要 plugin.manage。 额外的目标条件: - 插件必须存在。 - 插件必须 status=1(已启用)。 - 插件必须有非空 base_url。 - 只有 remote 类型有 base_url;local 的 /proxy 会返回“未配置后端地址”。 平台注入请求头: X-Platform-Signature HMAC(按插件派生密钥, code + timestamp + tenant) ← 推荐验这个 X-Platform-Caller-Signature HMAC(按插件派生密钥, code + timestamp + tenant + sub) X-Platform-Signature-Version 当前 v1 X-Platform-Timestamp 秒级时间戳 X-Platform-Plugin 目标插件 code X-Platform-Tenant 调用者租户 ID X-Platform-Caller-Sub 调用者用户 ID(纯数字) X-Platform-Token 旧兼容头,使用平台全局密钥签名;外部插件不得依赖 X-Platform-Caller-Token 旧兼容头,使用平台全局密钥签名;外部插件不得依赖 平台删除的入站凭据: Authorization Cookie x-api-key x-auth-token 因此: - 第三方插件后端永远拿不到调用者的平台凭据。 - 插件后端要回调平台,不能借用原用户 Authorization,应使用自己的服务端凭据。 - 插件后端必须校验 X-Platform-Signature 和 X-Platform-Caller-Signature(若使用调用者身份),使用插件专属密钥。 - 常量时间比较,不要用普通字符串 ==。 - 时间戳窗口建议 ±300 秒。 - 校验 X-Platform-Plugin 与自身 code 一致。 Node 参考: const crypto = require('crypto'); function safeEqualHex(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, code, headers }) { const signature = headers['x-platform-signature']; const ts = headers['x-platform-timestamp']; const tenant = headers['x-platform-tenant']; const plugin = headers['x-platform-plugin']; if (!signature || !ts || !tenant || plugin !== code) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false; const expected = crypto.createHmac('sha256', secret) .update(String(code) + String(ts) + String(tenant)) .digest('hex'); return safeEqualHex(signature, expected); } function verifyCaller({ secret, code, headers }) { const signature = headers['x-platform-caller-signature']; const sub = headers['x-platform-caller-sub']; const ts = headers['x-platform-timestamp']; const tenant = headers['x-platform-tenant']; if (!signature || !sub || !ts || !tenant) return null; if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return null; if (!/^[0-9]+$/.test(String(sub))) return null; const expected = crypto.createHmac('sha256', secret) .update(String(code) + String(ts) + String(tenant) + String(sub)) .digest('hex'); return safeEqualHex(signature, expected) ? String(sub) : null; } 注意:这里的 secret 是插件专属验签密钥,通过前面两个 signing-secret 端点获取,不是平台全局密钥。不要使用旧的 X-Platform-Token 做外部验签,也不要把这个密钥放前端。 Python 参考: import hmac, hashlib, time def verify_platform(secret: str, code: str, headers) -> bool: signature = 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 signature or not ts or tenant is None or plugin != code: return False if abs(int(time.time()) - int(ts)) > 300: return False expected = hmac.new(secret.encode(), f'{code}{ts}{tenant}'.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(signature, expected) 平台入站调用示例(调用方需要正确权限码): GET https://cfapi.aog.cc/api/plugins/{target}/proxy/health Authorization: Bearer Accept: application/json 写调用: POST https://cfapi.aog.cc/api/plugins/{target}/proxy/items Authorization: Bearer Content-Type: application/json {"name":"demo"} 错误映射: 401:没有 Authorization/令牌无效。 403:缺 plugin.view/plugin.manage,或层级不允许。 404:插件不存在、路径不存在、local 无后端。 502:目标不可达。 504/平台内部提示:目标超时(默认 15 秒)。 16 MB 响应上限:超过会被拒绝或截断,按平台实际错误处理。 ================================================================ 6. 市场投稿完整流程 ================================================================ 6.1 登录 POST /api/auth/login Content-Type: application/json {"username":"YOUR_USER","password":"YOUR_PASSWORD"} 响应: {"code":0,"message":"ok","data":{"token":"eyJ...","user":{...},"permissions":[...]}} 后续:Authorization: Bearer 6.2 打包 local: - plugin.json - index.html - 其他静态资源 remote: - plugin.json 必含 code/name/version/type=remote/base_url - base_url 必须是公网可访问的 HTTP(S),外部提交会拦截内网/回环。 - 包仍可有 README、server 源码,但平台不会执行它们;它们只是分发材料。 6.3 市场上传 POST /api/market/upload?plugin_code={code} Content-Type: multipart/form-data 字段:file 最大:500 MB 认证:任意有效登录用户 不需要:plugin.manage curl: curl -X POST "https://cfapi.aog.cc/api/market/upload?plugin_code=my_plugin" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@my_plugin.zip;type=application/zip" 成功: {"code":0,...,"data":{"plugin_code":"my_plugin","download_url":"/api/market/uploads/my_plugin.zip","file_size":12345,...}} 限制: - plugin_code 只能小写字母/数字/下划线。 - 包必须是有效 ZIP(PK 魔数)。 - 已归属他人或平台(owner_sub=0)时,非超管不能覆盖。 - 同一插件标识已有他人提交记录时不能覆盖。 - 上传有 IP 限流。 6.4 验证码 GET /api/market/captcha Authorization: Bearer $TOKEN(匿名也可签发,但提交必须登录) 返回一个一次性 token 与算术题 question。提交时: captcha_token = token captcha_answer = 算出的答案 不要缓存验证码复用。 6.5 提交审核 POST /api/market/submit Authorization: Bearer $TOKEN Content-Type: application/json { "plugin_code": "my_plugin", "name": "我的插件", "version": "1.0.0", "description": "功能说明", "author": "作者", "email": "dev@example.com", "download_url": "/api/market/uploads/my_plugin.zip", "documentation_url": "https://example.com/docs", "category": "productivity", "tags": "tag1,tag2", "captcha_token": "...", "captcha_answer": "..." } 必填:plugin_code、name、download_url、有效 captcha。 版本规则: - 必须 x.y.z,三段非负整数。 - 若已有上架/历史版本,必须严格大于历史最高版本。 - 不允许用同版本反复覆盖。 提交频率:每用户每分钟最多 3 次;另有 IP 限流。 归属:只能提交自己名下插件,平台插件只有超管可提交。 扫描:会对 ZIP 做静态扫描,危险内容记录在 scan_result;批准时默认阻断危险项。 6.6 查询和撤回 查看自己的提交: GET /api/market/submissions GET /api/market/submissions?status=pending 管理员看全部;普通用户只能看到 submitted_by=自己。 撤回: POST /api/market/submissions/{plugin_code}/withdraw 仅 pending、仅本人(或超管)。撤回会删除该 pending 记录,不是“下架已上架插件”。 6.7 审核 管理员: POST /api/market/submissions/{id}/approve POST /api/market/submissions/{id}/reject 需要 plugin.govern。 批准效果: - plugin_submissions 状态变为 approved。 - plugin_market_versions 新增/切换当前版本。 - 不会改 plugins.approval_status,不会安装,不会启用,不会启动沙箱。 6.8 安装 安装(不是任意终端用户可自助): GET /api/plugins/market(浏览目录,需 plugin.view) POST /api/plugins/market/install(安装,需 plugin.manage) 通常由平台/实例管理员执行;external 普通用户没有这些权限。 安装结果: - remote:登记 base_url,无文件落地;归属平台 owner_sub=0。 - local:从市场 download_url 拉包、落地到插件目录。 - 新安装的市场插件归平台,只有超管能改;开发者不能把平台插件当成自己的私有实例随意覆盖。 ================================================================ 7. 升级、版本、回滚、下架 ================================================================ 发布新版本: - 重新 upload,使用同一 plugin_code,上传新 ZIP。 - 提交 version 必须严格高于历史最高版本。 - 再次 pending,再次审核。 - 批准后历史保留,当前版本指针切换。 查看历史: GET /api/market/versions/{plugin_code} - 提交者本人或超管。 - 返回版本、download_url、is_current、created_at。 切换当前版本: POST /api/market/versions/{id}/set-current - 需要 plugin.govern,且 ensure_plugin_owner。 - 只切换 plugin_market_versions.is_current 与 plugin_submissions 的 version/download_url/semver。 - 不会重装已部署的 local 文件。 - 不会重启或回滚你服务器上的 remote 服务。 - 若要真正回滚运行中的 local 实例,需重新 market/install 目标版本。 - remote 的运行版本由你自己的服务器负责。 下架: POST /api/market/plugins/{code}/unpublish - 需要 plugin.govern + 归属。 - 同一事务把当前版本与市场载体置为 unpublished。 - 目录与安装入口一并消失。 不要承诺: - 灰度发布 - 自动回滚运行中服务 - 市场批准自动部署 - 审核通过即代表安全认证 ================================================================ 8. 插件运行时能力(仅列可确认的稳定面) ================================================================ 所有路径以 /api/plugins/{code} 为前缀;统一要求已注册、启用(写操作)、层级匹配、权限码。具体权限码以每个接口为准;不要只看路径。 数据: POST /data/{collection} 创建文档(body 为文档对象) GET /data/{collection} 列表(分页参数按 OpenAPI) GET /data/{collection}/{doc_id} PUT /data/{collection}/{doc_id} DELETE /data/{collection}/{doc_id} PUT /data/{collection}/schema {"fields":[...]},整体覆盖 GET /data/{collection}/schema KV: PUT /kv/{key} {"value": <任意 JSON>, "ttl_secs": 秒};ttl 缺省/0 表示不过期 GET /kv/{key} DELETE /kv/{key} 用户: POST /plugins/{code}/users/register {"username":"u","password":"p","email":"optional","display_name":"optional"} 公开端点,有 IP 限流;插件必须存在、启用。 POST /plugins/{code}/users/login {"username":"u","password":"p"} 公开端点,有 IP 限流;只校验插件自有用户,返回 user_id/display_name/logged_in_at,不签发 bearer token。 不要把它当平台 API 授权凭据;平台身份用 JWT/API Key/插件作用域令牌。 GET /plugins/{code}/users 管理面,需要 plugin.manage + 层级匹配 AI: POST /plugins/{code}/ai/chat {"provider":"optional","model":"optional","messages":[...],"max_tokens":123} 需要 plugin.manage,插件须启用,受平台 AI 配额/上游密钥限制。 GET /plugins/{code}/ai/usage 支付: POST /plugins/{code}/payments {"user_id":123,"amount_micros":1000000,"currency":"CNY","description":"..."} 需要 plugin.manage;金额必须 >0;user_id 必须是该插件自己的用户。 GET /plugins/{code}/payments 推送/房间: POST /plugins/{code}/push {"room":"optional","event":"name","data":{...}} POST /plugins/{code}/rooms {"room":"name","description":"optional"} GET /plugins/{code}/rooms Hook: POST /plugins/{code}/hooks 创建回调,需要 plugin.manage GET/PUT/DELETE /plugins/{code}/hooks/{id} POST /plugins/{code}/hooks/{id}/test 配置/状态: GET/PUT /plugins/{code}/config GET/PUT /plugins/{code}/config/schema GET/PUT/DELETE /plugins/{code}/store/{key} 对象存储: POST /plugins/{code}/storage/objects GET/PUT/DELETE /plugins/{code}/storage/objects/{key} GET /plugins/{code}/storage/objects 文件: POST /plugins/{code}/files/upload(multipart) GET /plugins/{code}/files GET /plugins/{code}/files/download/{path} 调度/队列: POST /plugins/{code}/scheduler PUT/DELETE /plugins/{code}/scheduler/{id} POST /plugins/{code}/jobs GET /plugins/{code}/jobs UI 插槽: POST/GET /plugins/{code}/ui-slots PUT/DELETE /plugins/{code}/ui-slots/{id} 沙箱: POST /plugins/{code}/sandbox/exec {"payload": <任意 JSON>} 需要 plugin.manage、已有 active 沙箱配置、runtime wasm/process。 默认 timeout 5000ms、memory 256MB、cpu 1000ms、network deny、filesystem readonly、并发 8。 Webhook 入站: POST /api/webhook/inbox/{code} {"source":"...","event_type":"...","payload":{...},"secret":"optional"} 公开端点但有 IP 限流;不是插件代理路径。 健康: GET /plugins/{code}/health POST /plugins/{code}/health/ping 注意: - 上表是基于源码路由与结构体的稳定面;OpenAPI 对部分管理接口的 request body 仍是泛化描述。 - 参数不确定时先用明确的最小请求测试,读取 400 错误信息,不要凭空补字段。 - 平台不会把“路由存在”等同于“external 默认可用”。每个接口都可能还有层级、权限与归属判断。 ================================================================ 9. 插件互调 ================================================================ 存在两条通道,但都不是“external 只带个密钥随便调”: 1. 代理通道:/api/plugins/{to}/proxy/{path} - 只支持 remote + base_url。 - 读需要 plugin.view,写需要 plugin.manage。 - 没有自动授权。 2. 显式 RPC 通道: POST /api/plugins/{from}/call/{to}/{path} - 需要 plugin.manage。 - 需要在 plugin_rpc_grants 中显式授权 from → to,method 匹配或 *。 - 目标必须是 remote、启用、base_url 合规。 - 这不是外部开发者自助可配置的通道。 不要生成“external 用户直接调用 call”的示例;它会被权限与授权表拦住。 如果需要复用他人能力,优先让平台管理员确认授权与权限,不要把绕过校验当成方案。 ================================================================ 10. 内部运维接口(不要写给外部开发者) ================================================================ 以下接口存在,但不是插件开发者的自助面: - /api/plugin-sandbox/* - /api/plugin-events/* - /api/plugin-data/* - /api/plugin-storage/* - /api/job-queue/* - /api/plugins/scan - /api/tier-policy/* - /api/market/submissions/{id}/approve|reject - /api/market/versions/{id}/set-current - /api/market/plugins/{code}/unpublish 不要因为 OpenAPI 列出了路径就告诉用户“任何人都能调用”。先看权限码与层级。 ================================================================ 11. 协议接入 ================================================================ REST:主通道。 JSON-RPC 2.0:POST /api/rpc,固定方法集,http.request 可兜底 REST。 MCP:POST /api/mcp,固定 8 个工具,不自动展开全部 REST。 Connect:POST /connect/cfapi.v1.{Service}/{Method},支持 JSON/proto codec;Watch 为服务端流。 gRPC:原生二进制 gRPC 不支持(无 trailers);只有 gRPC-Web JSON 兼容。 GraphQL:POST /api/graphql,偏管理面。 WebSocket:GET /ws?token=JWT;外部令牌不能订阅全站内部事件。 SSE:GET /api/events,只推系统事件与错误,不是插件业务推送。 Webhook:/api/plugins/{code}/hooks 配置回调;入站收件是 /api/webhook/inbox/{code}。 OpenAPI:/api/openapi.json,OpenAPI 3.1(完整清单,含内部操作)。 公开契约:/docs/devcenter/openapi.public.json(105 路径 / 138 操作,只含公开与插件接口,全部 JSON 请求体都有 schema)。 SDK:/docs/devcenter/downloads/cfapi-sdk-ts.zip(TypeScript,由公开契约生成)。 Postman:/docs/devcenter/downloads/cfapi-postman.json(138 个请求,内置 baseUrl 与 token 变量)。 OAuth2/OIDC:用户授权与发现,不是插件间互调。 Metrics/Health:运维面,插件不应依赖。 选型结论: - 插件后端调平台:REST + 服务端 API Key(当前外部稳定路径)。 - 前后端分离:remote + 平台代理。 - AI Agent 工具:MCP 或 JSON-RPC。 - 强类型 RPC:Connect。 - 不要用 GraphQL 做插件公开 API;它偏管理面且权限面不同。 ================================================================ 12. 调试与错误处理模板 ================================================================ 每个 HTTP 调用必须有: - 连接/读取超时。 - 非 2xx 分支。 - JSON 解析失败分支。 - 对 401/403/404/429/5xx 分别提示。 - 日志不打印 token、Cookie、secret、完整个人信息。 Node: async function callPlatform(path, { method = 'GET', body, token, timeoutMs = 20000 } = {}) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), timeoutMs); try { const res = await fetch('https://cfapi.aog.cc' + path, { method, headers: { ...(token ? { Authorization: 'Bearer ' + token } : {}), ...(body ? { 'Content-Type': 'application/json' } : {}), Accept: 'application/json' }, body: body ? JSON.stringify(body) : undefined, signal: ctrl.signal }); const text = await res.text(); let data; try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; } if (!res.ok) { const hint = res.status === 401 ? '检查令牌' : res.status === 403 ? '检查权限码与账号层级' : res.status === 404 ? '检查插件 code 与路径' : res.status === 429 ? '降低频率或稍后重试' : '平台或上游错误'; throw new Error(`HTTP ${res.status}: ${hint}; ${data && data.message ? data.message : ''}`); } if (data && typeof data.code === 'number' && data.code !== 0) { throw new Error(`业务错误 ${data.code}: ${data.message || ''}`); } return data; } finally { clearTimeout(timer); } } Python: import requests def call_platform(path, token, method='GET', body=None, timeout=20): r = requests.request( method, 'https://cfapi.aog.cc' + path, headers={'Authorization': 'Bearer ' + token, 'Accept': 'application/json'}, json=body, timeout=(5, timeout), ) try: data = r.json() except ValueError: data = {'raw': r.text} if r.status_code >= 400: raise RuntimeError(f'HTTP {r.status_code}: {data}') if isinstance(data, dict) and data.get('code') not in (None, 0): raise RuntimeError(f"业务错误 {data.get('code')}: {data.get('message')}") return data ================================================================ 13. 安全不变量(生成代码时必须遵守) ================================================================ 1. 不把 API Key、plugin_secret、管理令牌放进前端、localStorage、URL。 2. 不关闭 TLS 校验,不把 HTTPS 改为 HTTP。 3. 不记录完整 Authorization/Cookie。 4. remote 后端必须验证平台签名头,不能只看来源 IP。 5. 验证用常量时间比较,时间戳有窗口,plugin code 匹配。 6. 不用用户 Authorization 调其他插件;平台也会剥离它。 7. 不把“审核通过”当作安全背书。 8. 不把“路由存在”当作“当前账号有权限”。 9. 不生成绕过层级、关闭限流、关闭看门狗的方案。 10. 不把用户数据写进公开示例、测试文件或 Git。 ================================================================ 14. 上架前检查清单 ================================================================ 包结构 [ ] plugin.json 存在且合法 JSON [ ] code 与上传参数一致、只含小写字母/数字/下划线 [ ] local 有 index.html 且入口可直接打开 [ ] remote 有公网 HTTPS base_url [ ] 清单不含密钥、内网地址、真实用户数据 功能 [ ] 所有 fetch 有超时与错误分支 [ ] 401/403/404/429/5xx 有明确提示 [ ] 前端没有高权限凭据 [ ] remote 后端用插件专属密钥校验 X-Platform-Signature 与 X-Platform-Caller-Signature [ ] 分页、空数据、重复提交、断网均有处理 [ ] 不会因为重复点击创建重复订单/文档 发布 [ ] 版本 x.y.z 且高于历史最高 [ ] 验证码与 download_url 属于本次上传 [ ] README 只写真实存在的端点 [ ] 上传后检查返回的 download_url [ ] 提交后查 /api/market/submissions 确认 pending [ ] 知道批准不等于安装 运维 [ ] 日志不输出秘密 [ ] 有更新/降级计划 [ ] 了解 set-current 不会回滚已部署文件 [ ] 了解下架会同时移除目录与安装入口 ================================================================ 15. 不实声明清单(禁止继续传播) ================================================================ 以下说法在 2026-11-05 源码核对中不成立或不准确: - “上传 plugin.toml 即可” → 错误,平台清单是 plugin.json。 - “注册 wasm/sandbox 类型就自动隔离运行” → 错误,需独立配置且 active。 - “市场批准后插件在沙箱隔离中执行” → 错误,批准只改市场状态/版本。 - “set-current 一键回滚运行中插件” → 错误,只切市场版本指针。 - “所有插件类型都能经 proxy 调用” → 错误,只有 remote + base_url。 - “external 密钥可直接 POST 调所有插件” → 错误,需对应权限码。 - “API Key scope 写了 plugin.manage 就一定有权限” → 错误,仍受账号层级/RBAC 限制。 - “普通 internal 可以创建并运行插件” → 错误,只能读。 - “上传 zip 自动进入沙箱” → 错误,local 是静态托管。 - “默认资源 128MB/5 秒/10MB 文件” → 错误,当前默认 256MB/1000ms CPU/5000ms 超时、单次日志 64KB。 - “平台全站有统一 CSRF Token” → 没有证据,不要声称。 - “seccomp/capability 全插件生效” → 没有对应通用实现证据,不要声称。 - “审核通过 = 安全认证” → 平台未做此承诺。 - “openapi_extra.json 是补充合同” → 已废弃,只看 /api/openapi.json。 ================================================================ 16. 仍存在的边界(如实告知用户) ================================================================ - 外部开发者无法在平台内创建私有 local 预览实例;本地测试后投稿,批准后由有 plugin.manage 的实例管理员安装。 - 市场批准后,原始提交者可用 `GET /api/market/submissions/{code}/signing-secret` 取得插件专属验签密钥;状态未批准时会拒绝。 - OpenAPI 覆盖已核对并纳入合同的公开与插件路由,但不是对每条内部运维路由的完整性承诺。内部管理操作标记 `internal-admin-generic`:其中 73 个已从 Rust 结构体导出字段级 schema(`x-schema-source: derived-from-struct`,未逐项运行时校验),其余 33 个是无类型 JSON 或非 JSON 请求体,不对外承诺。 - 需要稳定集成时请用公开契约 `/docs/devcenter/openapi.public.json`:它排除了全部内部操作,且所有 JSON 请求体都有 schema,可直接生成客户端。 - 平台没有“任意插件自动运行”的通用能力;local 是静态,remote 是代理,wasm/process 是受控沙箱。 - 市场目录中的插件可调用性还受类型与权限码限制。 这些边界不是建议绕过的障碍,而是部署安全模型的一部分。 ## 插件层级完整规格(v1.10.130+) ### 1. 数据模型 | 列 | 含义 | |---|---| | `parent_id` | 父节点 `plugins.id`;`0` = 直属 cfapi(总插件平台层) | | `tree_depth` | 1=总插件平台,2=一级子插件平台,3=二级,4=三级……(cfapi 自身为 0) | | `tree_path` | 物化路径 `/42/94/95/`(含自身)。祖先判定 = 前缀比较,避免递归 CTE | | `is_platform` | 1 = 具备插件平台能力(可挂子节点);0 = 纯插件 | | `manage_mode` | `inherit`(上级代管)/ `delegated`(自治子树,上级仍有越级权) | | `node_kind` | `native` / `api` / `code` / `db` / `wasm` / `sandbox` | | `protocols` | 该节点**原生**支持的协议声明(JSON);未声明的由 CFAPI 网关桥接 | 深度上限 16(`MAX_DEPTH`),超出返回 400 —— 这是防误操作,不是业务限制。 ### 2. 三种管理身份 | 身份 | 判据 | 权限范围 | |---|---|---| | 平台超管 | `is_super=true` | 全部节点 | | 归属开发者 | `owner_sub == sub` | 自己名下节点及其整棵子树(含越级) | | 平台作用域令牌 | `pscope > 0` | 该节点及其整棵子树,**恒非超管** | 判定顺序:超管 → pscope 子树 → 名下祖先(`SELECT COUNT(*) FROM plugins WHERE owner_sub=? AND ? LIKE CONCAT(tree_path,'%')`)。 任一路径命中即通过,因此「上级越级」不需要为每一层单独授权。 ### 3. 端点 ``` GET /api/plugins/tree?root=&max_depth= 整棵树 / 以某节点为根的子树 GET /api/plugins/{code}/children 直接子节点 POST /api/plugins/{code}/children 新建子节点(父需 is_platform=1) GET /api/plugins/{code}/descendants?max_depth=n 全部后代(越级视角) GET /api/plugins/{code}/lineage cfapi → 自身的祖先链 PUT /api/plugins/{code}/parent 改挂(含整棵子树,防环 + 深度校验) POST /api/plugins/{code}/platform 开关平台能力 / 设 manage_mode GET /api/plugins/{code}/protocols 该节点 20 种协议入口 POST /api/plugins/{code}/tokens 签发 pscope 子树管理令牌 ``` ### 4. 改挂的原子性 `PUT /parent` 会先在内存里算出整棵子树的新 `tree_path` 与 `tree_depth`,再用单个事务批量写; 任一行失败整体回滚,不会留下「父路径变了、子路径没变」的半截状态。 拒绝两种输入:① 目标父节点是自己的子孙(成环);② 改挂后子树最深超过 16 层。 ### 5. 协议面(每层都一样) `GET /api/plugins/{code}/protocols` 返回 20 项:HTTP、REST、JSON-RPC 2.0、MCP、Connect、gRPC-Web(JSON)、GraphQL、 WebSocket、SSE、Webhook、CLI、OpenAPI、OAuth2、OIDC、JWT、APIKey、AdminAPI、Metrics、HealthCheck、SAML2。 每项都带 `endpoint` 与 `status`: - `native` —— 该协议在该节点上有真实专用入口(如 `{base}/proxy/*`、`{base}/hooks`、`{base}/tokens`); - `gateway` —— 全局唯一入口 + 节点身份 `code`(如 `POST /api/rpc` + `params.plugin`)。 **为什么不是每个节点一套服务器**:协议层是翻译器,业务层复用同一套实现。复制服务器会让「新增一个节点」 变成「新增一套要独立升级、独立修 bug 的进程」,且各层行为必然漂移。当前设计保证:新增节点即刻拥有全部协议, 且所有层级的协议行为完全一致。 ### 6. 与既有模型的关系 - 层级不替代 `owner_sub` 归属隔离:归属决定「谁能管」,层级决定「管到哪一层」。 - 层级不影响 `api_tier`(internal/external 运行期隔离):那是安全闸门,穿过它会击穿整套隔离。 - `data_type`(interop/isolation)与层级正交:同一棵子树里可以既有互通插件也有隔离插件。 ### 7. 不要做的事 - 不要为每一层建独立表或独立服务。 - 不要用「逐层授权」实现越级管理:那是 O(深度) 的授权维护成本,且极易漏配。 - 不要把 `pscope` 令牌当超管令牌用:它恒 `is_super=false`,且服务端每次按 `tree_path` 前缀校验。 - 不要指望关闭平台能力时子节点会自动提升层级:有子节点时关闭会被 400 拒绝。 ================================================================ 13. 本地插件实测踩坑清单(2026-11-07 实测补充) ================================================================ 这一节全部来自「按文档从零写一个 local 插件并上线」的实测,不是推测。 1) 上传参数名 - 私有安装:POST /api/plugins/upload?code=,Body 是 raw ZIP(application/zip)。 - 市场投稿:POST /api/market/upload?plugin_code=,multipart 字段名 file。 两者不一致;把 plugin_code 用在私有上传上会返回 400 missing field `code`。 2) 重复上传与顶层目录(1.10.131 已修) 1.10.131 之前,若 ZIP 含唯一顶层目录(inknote/…)且目标目录已存在 plugin.json, 新文件会被解到 /plugins/{code}/{code}/,根目录仍是旧文件,接口却返回「插件包已安装并注册」。 判断是否踩中:对比 /plugins/{code}/app.js 与包内文件大小;或看目录里是否出现同名嵌套目录。 1.10.131 起改为「临时舞台解压 + land_plugin 落盘」,并删除新包中已不存在的旧文件。 3) 静态缓存 静态资源响应 `cache-control: public, max-age=604800`,缓存键是完整 URL。 `style.css?v=1` 与 `style.css?v=999` 会被当成两个资源;只改文件、不改 `?v=` 时, 线上会继续返回旧内容。每次发布都要递增 `index.html` 里的版本参数。 4) 数据接口的能力边界 - 列表固定 `ORDER BY doc_id DESC`,没有 sort 参数;doc_id 由服务端生成,不等同于更新时间。 - 过滤只支持单字段等值(`?field=&value=`),没有全文检索、没有范围查询。 - 单文档默认 256KB、单值 KV 默认 64KB,均可在后台调整。 - PUT 是全量覆盖且没有 If-Match/版本号,最后写入者获胜;需要冲突检测只能先 GET 比对 updated_at。 5) 时间 接口返回的时间是 UTC 且无时区后缀。`2026-11-06T23:18:45` 是 UTC, 本地(+08:00)应为 2026-11-07 07:18:45。直接 `new Date(str)` 会按本地时区解释,产生 8 小时偏差。 6) approval_status 私有安装的插件默认 `approval_status=pending`(即使 `status=1` 已启用)。 这不影响启用、访问页面与数据读写,但不会出现在 `/api/plugins/callable` 与模板列表中; 需要插件间调用时,先走审批把状态置为 approved。 ================================================================ 17. 运行时数据库后端(三种,可一键切换) ================================================================ 这一节描述**平台自身**的存储层。对插件开发者只有一句话需要记住: 插件仍然禁止直连数据库,数据只能走 REST(文档集合 / 键值 / 对象存储)。 以下内容用于排障、写运维脚本、或判断「某个现象是不是后端差异」。 17.1 三种后端 - SQLite 3.46.0 —— 默认。库文件 app/var/data/cfapi.db,WAL 模式; 结构由编译期内嵌 DDL 自举(CREATE ... IF NOT EXISTS),不需要手工建库。 - MariaDB 11.8.6 —— 备用。 - PostgreSQL 18.6 —— 备用。注意端口是 5433(5432 属于同机其它项目)。 17.2 配置形态 .env 里三个槽位常驻,切换时只改「当前生效」两项: DATABASE_SQLITE_URL=sqlite:/abs/path/app/var/data/cfapi.db DATABASE_MYSQL_URL=mysql://user:pass@127.0.0.1:3306/db DATABASE_POSTGRES_URL=postgres://user:pass@127.0.0.1:5433/db DATABASE_URL=<当前生效的连接串> DATABASE_TYPE=sqlite | mysql | postgres # 显式声明,避免靠 scheme 猜 17.3 切换怎么做 后台:/qq123008/ → 数据库管理 → 目标后端 → 切换。 命令行(等价):bash scripts/db_port/cutover.sh sqlite|mysql|postgres 一次切换会自动完成:停写 → 给目标库打快照 → 全量搬运 → 行级摘要校验 (mismatch 必须为 0)→ 改 .env → 重启 → 健康检查;任一步失败自动回滚到原后端, 旧服务继续跑。搬运耗时(实测,128 表约 4800 行): mysql→sqlite 0.6s | sqlite→mysql 1.4s | sqlite→postgres 30s | postgres→sqlite 49.9s 17.4 查询当前后端 GET /api/db-backend/status(需管理凭证)返回 dialect / server_version / table_count / production_ready / backends 数组(含 sqlite,可据此做前端下拉)。 17.5 已知的后端差异(写 SQL 时必须注意) 1) 整数宽度:MySQL int(11) 在 PG 是 int4、tinyint 在 PG 是 int2。 解码类型必须对齐(bigint↔i64 / integer↔i32 / smallint↔i16), 否则 MySQL 会隐式放宽、PG 直接拒绝解码,表现为「功能静默失效」而不是报错。 部署前的质量门会自动做这项一致性审计。 2) MySQL 专属语法在 SQLite 下会被改写:INSERT IGNORE→INSERT OR IGNORE、 IF()→iif()、CONCAT()→||、DATE_FORMAT()→strftime、LIMIT a,b→LIMIT b OFFSET a、 ON DUPLICATE KEY UPDATE→ON CONFLICT ... DO UPDATE、COLLATE utf8mb4_*→剥离。 3) PG 的 upsert 里自引用列必须带表名(`t.calls = t.calls + 1`), 否则报 column reference "calls" is ambiguous。 4) SQLite 索引名是**库内全局唯一**(不是表内唯一),同名索引会被 IF NOT EXISTS 静默跳过。 5) 三种后端返回的时间都是 UTC,无时区后缀,与其它接口一致。 17.6 备份 scripts/db_port/backup_db.sh 对三种后端都可用(SQLite 走 .backup + integrity_check, MySQL 走 mariadb-dump,PG 走 pg_dump),crontab 每天 03:20 执行,产物在 app/tmp/backup/db/,按天数轮转。 17.7 什么情况下不要切 正在跑数据库管理里的长 SQL、或有大事务未提交时不要切;切换会停写。 插件侧无需任何改动——插件看不到后端类型。