Expose plain-text API documentation with agent_mode
studio CI and deployment / verify-and-deploy (push) Successful in 3m17s
wiki CI and deployment / verify-and-deploy (push) Successful in 16m44s

This commit is contained in:
2026-10-26 14:59:54 +08:00
parent b0a60ea498
commit f81aed8ce8
20 changed files with 228 additions and 5 deletions
+101
View File
@@ -0,0 +1,101 @@
import type {NextApiRequest,NextApiResponse} from 'next';
// Read documentation before business validation or storage access. Mutations never enter this branch.
export const apiDocuments:Record<string,readonly string[]>={
"/api/explore-feed": [
"统一内容流",
"GET",
"查询参数:q(可选,最多 200 字符);category(可选,标签名称或命名空间);cursor(可选,上一页 nextCursor)。",
"JSON:entries 数组与 nextCursor;没有更多结果时 nextCursor 为 null。静态内容仅出现在第一页。",
"GET /api/explore-feed?q=排版",
"503:素材库暂时无法连接。"
],
"/api/feed": [
"素材列表",
"GET",
"查询参数:section(images / videos / audio / articles / motion,可选);path(可选,标签 path);q(可选,最多 200 字符);cursor(可选,上一页 nextCursor)。",
"JSON:posts、tags、contributions、nextCursor;每页最多 24 个 post。",
"GET /api/feed?section=images",
"400:未知 section、标签或无效游标。"
],
"/api/notes/{id}": [
"读取与编辑笔记",
"GET, PUT",
"路径参数:id,笔记稳定 ID。PUT 使用 application/json,所有字段必填:title(1–200 字符)、description(最多 1000 字符)、markdown(最多 100000 字符)、tags(最多 50 个标签 UUID)、revision(GET 返回的 entry.revision,64 位十六进制)。",
"GET 返回 {entry,tags};PUT 返回更新后的 {entry}。先 GET 获取 revision,再 PUT;冲突后重新读取。",
"GET /api/notes/{id}\nPUT /api/notes/{id}\n{\"title\":\"标题\",\"description\":\"简介\",\"markdown\":\"正文\",\"tags\":[],\"revision\":\"<GET 返回的 entry.revision>\"}",
"400:输入错误;404:笔记不存在;409:版本冲突。"
],
"/api/posts": [
"创建笔记或素材包",
"POST",
"application/json:kind(必填,image / video / audio / article / motion)、title(必填,1–200 字符);slug(可选,最多 120 字符,以小写字母或数字开头,仅含小写字母、数字、连字符);description(可选,最多 1000 字符);text(可选,最多 100000 字符);tags(可选,最多 50 个 UUID);coverAssetId(可选,图片 UUID);assets(可选,最多 100 项,{id: UUID,role: content | caption},role 默认 content)。媒体类型必须引用至少一个对应类型的 content 素材。",
"201 JSON:{id,slug}。引用已有素材;不会在此接口上传二进制文件。",
"POST /api/posts\n{\"kind\":\"article\",\"title\":\"新笔记\",\"text\":\"正文\",\"tags\":[]}",
"400:输入或素材引用不合法;409:slug 已存在。"
],
"/api/tags": [
"标签列表与创建",
"GET, POST",
"POST 使用 application/json:name(必填,去掉开头 # 并 trim 后 1–60 字符,不含 #、空白、英文或中文逗号);slug(可选,最多 60 字符,以小写字母或数字开头,仅含小写字母、数字、连字符)。",
"GET 返回标签数组;POST 新建返回 201,名称已存在时复用并返回 200。标签含 id、name、slug、path。",
"POST /api/tags\n{\"name\":\"排版\"}",
"400:输入错误;409:slug 冲突。"
],
"/api/upload": [
"上传媒体并创建素材包",
"POST",
"multipart/form-data:kind(必填,image / video / audio);title(必填,1–200 字符);slug(可选,小写字母或数字组成的非空段,以连字符分隔,最多 120 字符);description(可选,最多 100000 字符);tags(可选,UUID 数组的 JSON 字符串,最多 50 项);文件字段名任意,至少一个文件。最多 16 个文件,单个不超过 250 MB,总计不超过 1 GB。图片支持 JPG、PNG、WebP、GIF、AVIF;音频必须为独立声音文件。",
"201 JSON:{id,slug,url,assets},assets 为素材 UUID 数组。视频会自动进入后台加工队列。",
"POST /api/upload\nContent-Type: multipart/form-data\nkind=image\ntitle=示例图片\ntags=[]\nfile=<图片文件>",
"400:输入或媒体不合法;409:slug 冲突;413:上传超过限制。"
],
"/api/media/{id}": [
"读取媒体原件或衍生文件",
"GET, HEAD",
"路径参数:id,素材 UUID。查询参数:variant(可选,poster / silent_video / audio_track,读取以 id 为来源的衍生素材)。支持 Range 请求头。",
"二进制媒体,Content-Type 随文件类型变化;200 为完整文件,206 为范围响应,HEAD 无正文。",
"GET /api/media/{id}?variant=poster",
"404:素材不存在或无效 variant;416:请求范围无效。"
],
"/api/preview": [
"轻量图片预览",
"GET, HEAD",
"查询参数:source(必填,本站静态栅格图片路径,或 /api/media/<UUID>,可带 ?variant=poster;作为查询值时必须 URL 编码);width(可选,240 / 480 / 960 / 1280,默认 480)。不接受任意远程图片地址。支持 If-None-Match。",
"WebP 图片;包含 ETag;缓存命中可返回 304;HEAD 无正文。",
"GET /api/preview?source=%2Fapi%2Fmedia%2F<UUID>&width=480",
"400:参数不合法;404:图片不存在;503:预览暂不可用。"
],
"/api/skills/street-people": [
"透明人物规范与上传",
"GET, POST",
"GET 默认返回 JSON 规范,format=md 返回创作指导 Markdown。POST 使用 multipart/form-data,沿用 /api/upload 字段与大小限制;kind 固定为 image,title 默认“街头小人 · 新生成”。图片必须为带 alpha 的 PNG,含 alpha=0 的透明像素及 alpha≥250 的像素。",
"GET 返回生成规范;POST 返回 201 {id,slug,url,assets}。",
"GET /api/skills/street-people\nPOST /api/skills/street-people\nContent-Type: multipart/form-data\nfile=<透明 PNG>",
"400:图片或字段不合法;413:超过上传限制。"
],
"/api/health": [
"应用健康检查",
"GET, HEAD",
"无业务参数。",
"GET 返回 JSON:status、service、release;HEAD 仅返回状态和响应头。",
"GET /api/health",
"405:不支持的方法。"
],
"/api/ready": [
"依赖就绪检查",
"GET",
"无业务参数。检查 PostgreSQL 和对象存储桶。",
"200 JSON:{\"status\":\"ok\",\"database\":\"ready\",\"objects\":\"ready\"}。",
"GET /api/ready",
"503 JSON:{\"status\":\"unavailable\"}。"
]
};
export function serveApiDocumentation(req:NextApiRequest,res:NextApiResponse,path:string){
if(!['GET','HEAD'].includes(req.method||'')||typeof req.query.agent_mode!=='string'||req.query.agent_mode.toLowerCase()!=='true')return false;
const doc=apiDocuments[path];if(!doc)return false;
const [title,methods,parameters,response,example,errors]=doc;
const text=`# ${title}\n\n地址:${path}\n方法:${methods}\n\n## 参数\n${parameters}\n\n## 响应\n${response}\n\n## 调用示例\n${example}\n\n## 错误\n${errors}\n\n## 读取说明\n使用 GET 或 HEAD,在原地址加 ?agent_mode=True;已有查询参数时加 &agent_mode=True。值不区分大小写。动态路径可使用实际 ID,也可保留占位符读取本接口说明。\n业务调用请移除 agent_mode。POST / PUT 不会被转换为说明请求。写入接口会拒绝 Origin 与 Host 不一致的请求;当前没有 API Key 或登录鉴权。\n\nAPI 目录:/api?agent_mode=True\n`;
res.setHeader('Content-Type','text/plain; charset=utf-8');res.setHeader('Cache-Control','no-store');
res.status(200);if(req.method==='HEAD')res.end();else res.send(text);return true;
}