Cleep 0.4.4 API 使用详解

Cleep 0.4.4 全接口 API 详解:认证与会话、笔记 CRUD 与视图查询、密码锁定与回收站、标签、附件上传下载、链接预览 SSRF 防护、导出与 Keep 导入、管理端用户与设置,附全端点总表、状态码错误体速查与全部实测回显。

CleepCleep自托管NASDocker Compose2026/10/25

Cleep 0.4.4 API 使用详解

目标:Cleep 没有官方 API 文档,也没有 OpenAPI 规范文件,所有接口语义只能从源码与真实行为还原。本篇基于官方仓库源码(server/app.ts、server/routes/*、server/middleware/*、server/lib/*,2026-10-25 实查 v0.4.4)与真实部署环境逐接口实测成文:全部 38 个端点均来自 app.ts 的路由挂载,文中命令与回显一律出自 2026-10-25 在 8140 端口实测的取证记录,覆盖认证、笔记增删改查、密码锁定、回收站、标签、附件、链接预览、导出与 Keep 导入、管理端与全部错误语义。部署方法见《Cleep 0.4.4 Docker Compose 部署指南》,配置参数与限流机制见《Cleep 0.4.4 配置参数详解》。

一、环境信息

本篇接口实测与取证环境如下,读者操作时请替换为自己的取值:

  • 操作系统:Ubuntu 24.04 LTS,x86_64
  • Cleep 版本:0.4.4(官方 GHCR latest 标签,与源码 tag v0.4.4 一致)
  • 数据库:PostgreSQL 16.15
  • 服务端口:应用服务内部监听 6169,对外发布 8140,本篇所有示例访问 http://localhost:8140
  • 演示账号:管理员 admin@quzuo.net / q1w2e3r4(首次启动引导创建),另有一个普通用户用于权限对照测试
  • 调用工具:curl,会话用 cookie jar 文件(-c 写入、-b 携带)管理

取证方式说明:「端点清单、请求/响应结构、默认值」逐条对照 v0.4.4 源码(本地已克隆 blindpassasjer/cleep 仓库);「实际行为」全部通过 curl 实测,共 30 余段回显原文,证据留存于本地 tmp/cleep/api_evidence.txt 等文件,文中代码块均未修饰、照实摘录。测试产生的笔记、标签、附件与用户账号均已在测试后删除。

二、API 总览

Cleep 的 API 与前端页面共用同一个服务进程,全部接口都挂在 /api 前缀下,其余路径由前端页面路由接管。以下为 v0.4.4 全部端点(来源:server/app.ts 挂载顺序 + 各路由文件逐一核对,2026-10-25):

前缀方法与路径权限限流组
/api/authGET /registration-status公开auth(20 次/分)
/api/authPOST /register公开(受注册开关控制)auth
/api/authPOST /login公开auth
/api/authPOST /logout无需登录auth
/api/authGET /me公开(未登录返回 {"user":null})auth
/api/authPATCH /me登录auth
/api/authPOST /password登录auth
/api/adminGET /users管理员—
/api/adminPOST /users管理员—
/api/adminPATCH /users/:id管理员—
/api/adminPOST /users/:id/password管理员—
/api/adminDELETE /users/:id管理员—
/api/adminGET /settings、PATCH /settings管理员—
/api/notes/:noteId/attachmentsGET /登录—
/api/notes/:noteId/attachmentsPOST /(multipart 上传)登录upload(60 次/分)
/api/notes/:noteId/attachmentsDELETE /:attachmentId登录—
/api/notesGET /登录—
/api/notesPOST /登录—
/api/notesPATCH /:id登录—
/api/notesPOST /:id/lock、/:id/unlock、/:id/unlock-attachment/:attachmentId、/:id/remove-lock登录note-lock(20 次/分)
/api/notesPOST /:id/trash、/:id/restore登录—
/api/notesDELETE /:id登录—
/api/notesPUT /:id/labels/:labelId、DELETE /:id/labels/:labelId登录—
/api/link-previewGET /?url=…、GET /image?url=…登录link-preview(300 次/分)
/api/labelsGET /、POST /、PATCH /:id、DELETE /:id登录—
/api/attachmentsGET /:id/file登录—
/apiGET /export登录upload
/apiPOST /import/keep(multipart 上传)登录upload

三条全局规则先记住:

  1. 鉴权靠 Cookie 会话,不是 API Key。除「公开」标注的接口外,全部接口要求携带登录后下发的 session Cookie;未登录访问受保护接口统一返回 401 {"error":"Authentication required."}(实测)。
  2. 四组限流互相独立(auth/upload/note-lock/link-preview),数值与计数机制见《Cleep 0.4.4 配置参数详解》第五章;其中 auth 组挂在 /api/auth 整个前缀上,连查注册开关都计数。
  3. 未知 API 路径返回 404:访问任何不存在的 /api/* 路径(包括把 GET 用成 POST 之外的方法组合)都会命中兜底路由,返回 404 {"error":"Not found."}(实测)。

⚠️ Cleep 未提供任何 API 令牌/密钥机制。做脚本或第三方集成只能模拟登录拿 Cookie,见第十二章注意事项。

三、认证与会话

登录:POST /api/auth/login

请求体为 JSON:{"email": "...", "password": "...", "rememberMe": true},其中 rememberMe 可省略(省略或非 false 都按「保持登录」处理,Cookie 带 30 天 Expires;显式传 false 则下发会话 Cookie,关浏览器即失效)。

登录成功的完整实测回显(含响应头,2026-10-25):

Set-Cookie: session=s%3Aceae4c72044042fa8ddca23ce27d454747f2ccaf98fa4d6b991145b7ce5664f4.2pawY4YfjWXvJE%2B%2BlZtUYZv2KH1XLHflth0uY7Iy6J4; Path=/; Expires=Tue, 24 Nov 2026 05:41:08 GMT; HttpOnly; SameSite=Strict
{"user":{"id":"aba48719-7d4b-44b6-82c7-963a2b0fa777","email":"admin@quzuo.net","username":"admin","role":"admin"},"error":null}

Cookie 属性(HttpOnly、SameSite=Strict、Secure 与 COOKIE_SECURE 的联动)已在配置篇第四章逐项解读,此处不赘。user 对象只暴露四个公开字段(id/email/username/role),密码哈希等敏感列不会出现在任何响应里。

最容易踩的语义坑:错误密码也返回 HTTP 200。实测用错误密码登录:

{"user":null,"error":"Invalid email or password."}
HTTP 200

Cleep 用 error 字段表达业务失败,而不是用 401 区分「密码错」与「未认证」。写自动化判断时必须检查 user 是否为 null,只看状态码会把失败登录当成功。邮箱不存在与密码错误返回同一条文案,不暴露账号是否存在。字段类型不对(如漏传 password)返回 200 {"user":null,"error":"Email and password are required."}。

注册开关查询与注册

$ curl -s http://localhost:8140/api/auth/registration-status
{"open":false}            # HTTP 200

open 即管理端 registrationOpen 运行期开关(配置篇第八章有完整切换实测)。注册接口 POST /api/auth/register 请求体 {email, username, password}:

  • 注册开关关闭时:403 {"user":null,"error":"Registration is currently closed."}(实测)——这是注册接口唯一一处 403;
  • 字段校验(邮箱须含 @、用户名 ≥2 字符、密码 ≥8 字符)失败:HTTP 200 + {"user":null,"error":"..."};
  • 邮箱已存在:200 {"user":null,"error":"An account with this email already exists."};
  • 成功:201 {"user":{...},"error":null},并且直接登录(响应同样带 Set-Cookie)。

自助注册创建的都是 role: "user" 普通账号;家庭/小团队部署建议保持注册关闭,账号由管理员逐个创建(第十章)。

身份、资料与改密

  • GET /api/auth/me:未登录也是 200,返回 {"user":null}(实测);已登录返回 {"user":{...}}。前端靠它判断登录态,做探活监控不要拿它的 200 当「已登录」。
  • PATCH /api/auth/me:改自己的 email/username,字段校验失败为 200 + 错误体,邮箱或用户名撞唯一约束返回 200 {"user":null,"error":"An account with this email or username already exists."};成功 200 {"user":{...},"error":null}。
  • POST /api/auth/password:{"currentPassword":"...","newPassword":"..."},新密码 ≥8 字符;当前密码错返回 200 {"error":"Current password is incorrect."};成功返回 {"error":null}。成功后服务端删除该账号除当前会话外的全部 session——其他已登录设备全部下线,防止旧 Cookie 存活(源码 routes/auth.ts 明确注释此设计)。
  • POST /api/auth/logout:删除服务端 session 行并清 Cookie,返回 {}。登出后再查 me 返回 {"user":null}(实测),旧 Cookie 彻底失效。

四、笔记 CRUD 与视图查询

查询:GET /api/notes

单接口承担全部视图,靠查询参数区分:

参数取值说明
viewnotes(默认)/ archive / trash / recordings不传等价 notes:非回收、非归档、非录音的笔记;archive 只看归档;trash 只看回收站;recordings 只看语音备忘
label标签 id,可重复出现(多值)按标签过滤;传入不属于本人的 id 会被静默忽略
labelMatchany(默认)/ all多标签时「任一命中」或「全部命中」
limit正整数,上限 200超过 200 按 200 截断(源码 MAX_PAGE_SIZE = 200);非法值(负数、非整数)整个参数被忽略,返回全量
offset正整数,默认 0翻页偏移

排序固定为 pinned 降序 → position 降序 → updatedAt 降序(源码 orderBy(desc(notes.pinned), desc(notes.position), desc(notes.updatedAt))):置顶永远在最前,新建笔记的 position 取创建时刻的毫秒时间戳,因此「新建的在前」是排序的副产品。两个细节:

  • 带 label 过滤时强制附加「非回收、非归档」条件(源码显式写死),所以即使某笔记在回收站里带标签,view 不传时也查不到;想按标签清点回收站是做不到的,只能全量拉 view=trash 自行过滤。
  • 锁定笔记在列表中 attachments 一律为空数组(源码注释:锁定笔记的附件文件名/缩略图与正文同等敏感,索性整表隐藏),blocks 正常返回但内容是空数组,正文密文在 encryptedBlocks 字段。

响应为一个 JSON 对象 {"notes":[…]},每条笔记字段(实测创建响应原文,字段全集以此为准):

{"note":{"id":"6465e447-c70f-4b58-959a-1eb9706d14a4","userId":"aba48719-7d4b-44b6-82c7-963a2b0fa777","title":"S8 D篇接口验证","content":"第一段正文","isChecklist":false,"items":[{"id":"i1","text":"待办一","checked":true}],"blocks":[{"id":"b1","html":"第一段正文","type":"text"},{"id":"b2","type":"checklist","items":[{"id":"i1","text":"待办一","checked":true}]}],"color":"teal","pinned":false,"archived":false,"isRecording":false,"trashedAt":null,"position":1792906868337,"createdAt":"2026-10-25T05:41:08.338Z","updatedAt":"2026-10-25T05:41:08.338Z","locked":false,"lockSalt":null,"lockVerifier":null,"lockIv":null,"lockAuthTag":null,"encryptedBlocks":null,"labelIds":[],"attachments":[]}}

实测按标签过滤(?label=<标签id>)返回 {"count": 1, "labelIds": ["326d3da0-…"]}(节选),limit=2 时返回 {"count": 2, "first": {"title": "S8 D篇接口验证", "blocks": 2, "labelIds": []}}(节选)。

创建:POST /api/notes

请求体核心是 blocks 数组——v0.4.4 的规范字段,块类型两种:text({id, type:"text", html:"..."},正文放在 html)与 checklist({id, type:"checklist", items:[{id, text, checked}]})。服务端会把 blocks 反解析出等价的 content/items/isChecklist 冗余列一起写库(「影子写」,供回滚兼容),所以两种形态在响应里同时出现。上传实测(HTTP 201)即上一节的回显:一个 text 块 + 一个含「待办一(已勾选)」的 checklist 块。

长度限制(超出部分静默截断,不报错):title ≤300 字符、每条 text 的 html/content ≤20000、checklist 条目 ≤300 条、每条 text ≤500 字符。color 只接受预定义色名(default/red/orange/yellow/green/teal/blue/purple/pink/brown/gray),非法值静默落为 default。isRecording: true 可创建语音备忘条目(配合附件上传的波形数据使用)。

更新:PATCH /api/notes/:id

可更新字段:title、color、pinned、archived、position、blocks(以及旧版客户端兼容的 content/items/isChecklist,自建脚本统一用 blocks 即可)。字段部分更新、只传要改的即可,实测同时改标题并置顶(HTTP 200,节选):

{"title": "S8 D篇接口验证(已改名)", "pinned": true, "updatedAt": "2026-10-25T05:41:08.424Z"}

注意 archived 也在 PATCH 里:归档/取消归档不是独立接口。笔记不存在或不属于当前用户一律 404 {"error":"Note not found."}——不区分「不存在」与「无权访问」,避免暴露他人笔记的存在性。锁定笔记的正文更新有额外密码校验,见第五章。

五、笔记锁定与回收站

锁定:POST /api/notes/:id/lock

请求体 {"password":"..."},密码最少 4 字符,不足返回 400 {"error":"Choose a password with at least 4 characters."}。锁定动作做了三件事(源码 routes/notes.ts):

  1. 用密码 + 随机盐派生 AES-GCM 密钥,把 blocks 加密写入 encryptedBlocks,同时清空明文列(blocks/content/items 全部置空)——锁定后数据库里不存在正文明文;
  2. 同步加密该笔记全部附件的文件字节(落盘文件原地变密文,iv/authTag 记入附件行);
  3. 可选带 blocks/title 一步到位:锁定时顺带保存编辑器里未提交的内容,避免「先保存再锁定」的竞态。

对已锁定笔记重复锁定返回 400 {"error":"This note is already locked."}。实测锁定成功(HTTP 200,节选):{"locked": true, "blocks": [], "encryptedBlocks_len": 188}。

锁定后的读写规则

  • PATCH 编辑锁定笔记的正文必须重带密码:服务端没有任何「已解锁」会话状态,每次保存都要验密并重新加密。不带 password 时实测:

    {"error":"This note is locked. Enter its password to save changes."}
    HTTP 401
    

    密码错误:401 {"error":"Incorrect password."}。只改 pinned/archived/title 等元数据不需要密码。

  • unlock 是只读的:POST /api/notes/:id/unlock 带正确密码后仅在本次响应里返回解密后的 blocks 与附件元数据,不改变 locked 状态、不落明文——下次读正文还要再解锁。实测错误密码 401 {"error":"Incorrect password."},正确密码(HTTP 200,节选){"blocks": ["text", "checklist"], "attachments": 0}。对未锁定笔记调用返回 400 {"error":"This note is not locked."}。

  • 取锁定附件的字节:POST /api/notes/:id/unlock-attachment/:attachmentId,带密码验证后直接返回文件本体(Content-Type 为原 MIME,响应带 Cache-Control: no-store 防缓存泄漏)。普通下载地址 GET /api/attachments/:id/file 对锁定笔记一律 403 {"error":"This note is locked."}(源码:密文文件只从这条带密码的通道出去)。

  • 解除锁定:POST /api/notes/:id/remove-lock 带密码,解密正文与全部附件并恢复明文落库,实测 {"locked": false, "blocks": 2}(节选,HTTP 200)。

锁定/解锁/解锁附件/移除锁四个接口同属 note-lock 限流组(默认 20 次/分钟/IP),本质是密码试错防护面;忘密码没有任何找回手段——密钥由用户密码派生,管理员与系统都无法解密,只能删除笔记重建。

回收站:trash / restore / 彻底删除

  • POST /api/notes/:id/trash:软删除,写入 trashedAt,实测(节选){"trashedAt": "2026-10-25T06:09:46.114Z"}(HTTP 200);回收站视图 GET /api/notes?view=trash 实测 {"count": 1}(节选)。
  • POST /api/notes/:id/restore:恢复,trashedAt 复位为 null。
  • DELETE /api/notes/:id:物理删除,且在删除数据库行之前先把附件文件从磁盘清掉(源码注释明确:附件行随笔记级联删除,但磁盘文件不会自动消失)。实测 {"ok":true};对已删除的笔记再次调用返回 404 {"error":"Note not found."}。回收站有后台定时清理任务(配置篇/源码 db/purgeTrash.ts),过期条目会被自动物理删除。

六、标签

四个自管理接口加两个「挂到笔记上」的接口:

  • GET /api/labels:返回 {"labels":[…]},按名称排序,仅本人标签。

  • POST /api/labels:{"name":"...","color":"blue"},名称去首尾空格后须 1–50 字符,超界返回 HTTP 200 {"label":null,"error":"Label name must be 1-50 characters."};同名重复返回的也是 200(实测):

    {"label":null,"error":"A label with this name already exists."}
    HTTP 200
    

    唯一同名判断在数据库层(用户维度唯一索引),并发创建也拦得住。成功 201 {"label":{"id":"326d3da0-…","userId":"aba48719-…","name":"S8验证标签","color":"blue"}}(实测)。

  • PATCH /api/labels/:id:改名/改色;不存在或不属于本人 404 {"error":"Label not found."}。

  • DELETE /api/labels/:id:删除标签并自动解除全部笔记上的挂载(关系表级联),返回 {"ok":true}(实测)。

  • PUT /api/notes/:id/labels/:labelId:给笔记打标签,返回 {"ok":true}(实测);重复打同一标签幂等(onConflictDoNothing)。DELETE /api/notes/:id/labels/:labelId:摘标签,同样 {"ok":true}(实测)。笔记或标签只要有一个不存在/不属于本人,统一 404 {"error":"Note not found."}。

打完标签即可回第四章的 ?label= 过滤查询,实测闭环 {"count": 1}(节选)。

七、附件

上传:POST /api/notes/:noteId/attachments

multipart/form-data,文件字段名固定为 file,可选附加 waveformPeaks 字段(JSON 数组,录音波形,最多 200 个 0~1 数值,仅音频存储)。限制(源码 lib/attachments.ts):

  • 单文件 ≤100MB,超出 400 {"error":"File is too large. Maximum size is 100MB."}(内存缓冲,不留临时文件);
  • 只接受 image/video/audio 三类 MIME,并且明确拒绝 SVG(image/svg+xml 直接判非法)——源码注释:附件以 Content-Disposition: inline 回传浏览器,收 SVG 等于允许在应用源内执行脚本;错误统一 400 {"error":"Only image, video, and audio files are accepted."};
  • 上传属 upload 限流组(默认 60 次/分钟/IP);
  • 父笔记已锁定:403 {"error":"Unlock this note before adding attachments."};笔记不存在:404;没带文件:400 {"error":"No file provided."}。

实测上传一张 PNG(HTTP 201):

{"attachment":{"id":"013e2aa9-5888-49b1-bd7e-cac013929d87","noteId":"6465e447-c70f-4b58-959a-1eb9706d14a4","kind":"image","name":"t.png","mimeType":"image/png","sizeBytes":69,"waveformPeaks":null,"url":"/api/attachments/013e2aa9-5888-49b1-bd7e-cac013929d87/file","createdAt":"2026-10-25T06:09:44.052Z"}}

url 字段即下载地址。GET /api/notes/:noteId/attachments 列出笔记全部附件(实测与上面同一对象);锁定笔记返回 {"attachments":[]},与列表接口的隐藏行为一致。

下载:GET /api/attachments/:id/file

按附件 id 直取文件字节,鉴权只查「附件属于当前用户」。实测响应头原文(2026-10-25):

HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' https: http: blob: data:; media-src 'self' blob:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Permissions-Policy: camera=(self), microphone=(self), geolocation=(), payment=(), usb=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
X-Permitted-Cross-Domain-Policies: none
Content-Type: image/png
Content-Disposition: inline; filename="t.png"
Content-Length: 69

要点:Content-Disposition: inline(浏览器内直接渲染而非下载);Cache-Control: private, max-age=31536000, immutable(附件 id 不可变,浏览器缓存一年,公共代理不缓存)。inline + 全局安全头组合说明:任何文件直接开 URL 都在自己源内渲染,这正是拒 SVG 的原因(第九章安全头逐项解读见配置篇)。父笔记锁定时 403 {"error":"This note is locked."},改走第五章 unlock-attachment 通道。

删除附件:DELETE /api/notes/:noteId/attachments/:attachmentId,删数据库行并删磁盘文件,返回 {"ok":true}(实测);不存在 404 {"error":"Attachment not found."}。

八、链接预览

为笔记里的外部链接抓取标题/配图(og:title、og:image、twitter:image,缺位时退化到 <title> 和正文 <img> 打分选图),结果缓存进 link_previews 表:抓取成功缓存 30 天,失败缓存 1 天(次日自动重试)。响应里 image/favicon 都被改写为同源代理地址 /api/link-preview/image?url=…——浏览器只从自己源加载预览图,反代强 CSP 也能显示,外部站点也拿不到读者 IP。实测公网链接(HTTP 200):

{"preview":{"url":"https://example.com/","title":"Example Domain","image":null,"siteName":"example.com","favicon":"/api/link-preview/image?url=https%3A%2F%2Fexample.com%2Ffavicon.ico"}}

SSRF 防护:内网地址不报错,返回降级卡片

链接预览是全服务唯一会向外部发起请求的接口,v0.4.4 为此内置了完整 SSRF 防护(源码 lib/ssrf.ts):仅允许 http/https 且 80/443 端口;localhost、回环、私网(10/8、172.16/12、192.168/16、127/8)、链路本地(169.254/16,云元数据)、组播与保留段全在黑名单;IPv4-mapped IPv6、NAT64 等变体写法用地址块匹配防绕过;域名解析出的每一个地址都必须公网,且连接钉死在校验过的那个 IP 上(防 DNS rebinding);重定向每一跳重新校验。

实测对内网地址(服务自身)取预览:

{"preview":{"url":"http://127.0.0.1:8140/api/auth/me","title":null,"image":null,"siteName":"127.0.0.1","favicon":"/api/link-preview/image?url=http%3A%2F%2F127.0.0.1%3A8140%2Ffavicon.ico"}}
HTTP 200

行为要点:不返回 4xx/5xx 错误,而是一个降级卡片——title/image 为 null、siteName 落为域名。这是「拒绝取回」而非「取回失败」的永久判定(源码注释:a rejected target is a permanent verdict),失败缓存 1 天。自建集成解析预览响应时按「title 为 null 即无有效预览」处理,别把它当故障。

把外部图片/图标经本服务中转返回:10 秒超时、≤5MB、只转发浏览器可渲染的图片类型(png/jpeg/gif/webp/avif/svg/bmp/x-icon)、缓存 7 天;目标不合法(SSRF 拒绝)返回空 400,抓取失败或类型不符返回空 502(无 JSON 错误体,客户端按图片加载失败处理即可)。整组接口属 link-preview 限流组(默认 300 次/分钟/IP,上限最宽——一条多链接笔记展开会扇出成倍的元数据与图片请求)。

九、数据导出与 Keep 导入

导出:GET /api/export

一键导出当前用户的全部数据,返回 zip 附件(文件名 cleep-export-<日期>.zip),属 upload 限流组。实测下载后解包清单(2026-10-25):

Archive:  /opt/data/quzuo/tmp/cleep/s8-export.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       79  2026-10-25 06:09   manifest.json
     2686  2026-10-25 06:09   notes.json
      319  2026-10-25 06:09   labels.json
       69  2026-10-25 06:09   attachments/cb983725-d38e-4ad5-b24f-e6332021dcdc/494c14fd-7e75-48a1-9b2b-56db9c302f29-t.png
       69  2026-10-25 06:09   attachments/6465e447-c70f-4b58-959a-1eb9706d14a4/013e2aa9-5888-49b1-bd7e-cac013929d87-t.png
---------                     -------
     3222                     5 files

结构固定四类:manifest.json({format: 1, app: "cleep", exportedAt: …})、notes.json(全部笔记,含回收站条目与 labelIds 关联)、labels.json、attachments/<笔记id>/<附件id>-<文件名>/。锁定笔记的附件不进包(源码注释:磁盘上是密文,按原名导出会得到一个静默损坏的文件,宁可跳过),笔记本身仍导出(locked 标记为 true)——想完整备份先解锁再导出。附件文件本身另有目录备份方案(部署篇/配置篇第七章),导出 zip 用于迁移到其他笔记软件或长期归档。

Keep 导入:POST /api/import/keep

multipart/form-data,文件字段名 file,直接吃 Google Takeout 的 Keep 导出包。限制:zip ≤200MB(超限 400 {"error":"File is too large. Maximum size is 200MB."})、笔记条数 ≤5000(400 {"error":"Archive contains more than 5000 notes."})、无 Keep 笔记时 400 {"error":"No Google Keep notes found in that archive (expected Takeout/Keep/*.json)."}、非 zip 400 {"error":"That does not look like a valid .zip file."}。

导入行为:只认 Takeout/Keep/*.json 路径的笔记文件;isTrashed 条目跳过并计数;标签按名称去重自动创建(颜色统一 default);Keep 色号映射到 Cleep 色板;附件按文件名匹配包内媒体并同样过「仅图/视/音」检查,缺失或不支持的记入 errors。实测回显(HTTP 200):

{"imported":3,"skippedTrashed":1,"errors":[]}

errors 数组最多返回 50 条(如 Missing attachment "…" for note "…"),部分失败不影响其余条目——批量导入适合断点重跑。

十、管理端接口

/api/admin 前缀全部接口要求「已登录 + role=admin」,路由级统一拦截:未登录 401 {"error":"Authentication required."}(配置篇实测),普通用户访问实测 403 {"error":"Admin access required."}。Cleep 的管理员无独立入口,就是 role 为 admin 的普通账号多开一组接口。

用户管理

  • GET /api/admin/users:全量用户列表 + 每人笔记数(实测,2026-10-25):

    {"users":[{"id":"aba48719-7d4b-44b6-82c7-963a2b0fa777","email":"admin@quzuo.net","username":"admin","role":"admin","createdAt":"2026-10-25T04:21:57.011Z","noteCount":5},{"id":"48a78a16-65db-4ed7-9cd8-849aa56ddd82","email":"user2@quzuo.net","username":"user2","role":"user","createdAt":"2026-10-25T04:25:58.493Z","noteCount":0}]}
    
  • POST /api/admin/users:{email, username, password, role?},校验规则与自助注册一致(邮箱含 @、用户名 ≥2、密码 ≥8),role 只认 "admin",其余一律落为 "user";邮箱重复 200 {"user":null,"error":"An account with this email already exists."};成功 201 返回带 noteCount 的用户对象(实测创建的测试账号见下)。注意它不返回 Cookie——管理员代建账号后把口令单独交给用户,与自助注册(直接登录)不同。

  • PATCH /api/admin/users/:id:改 email/username,实测改名回显(HTTP 200):

    {"user":{"id":"501bd487-7aeb-4f9a-8182-8d69a8802694","email":"s8viewer@quzuo.net","username":"s8viewer2","role":"user","createdAt":"2026-10-25T06:09:45.887Z","noteCount":0},"error":null}
    

    邮箱撞唯一约束此处返回 409(管理端与自助改资料的错误码不同,源码如此)。

  • POST /api/admin/users/:id/password:重置密码。请求体 {newPassword} 可省略——省略时服务端生成 12 字节随机临时密码并在响应 tempPassword 字段里返回;指定则用指定值。实测(指定新密码,HTTP 200):{"ok":true,"error":null}。重置即删除该用户全部 session,强制所有设备下线重新登录(实测语义,源码注释:a stolen/leaked cookie shouldn't survive a password change);新密码 ≠8 字符返回 200 {"ok":false,"error":"Password must be at least 8 characters."}。对不存在的用户 404 {"error":"User not found."}。

  • DELETE /api/admin/users/:id:注销账号(笔记、标签、会话、附件文件全部随之清理)。两条保护:不能删自己(400 {"error":"You can't delete your own account here. Sign in as a different admin."})、不能删最后一个管理员(400 {"error":"Cannot delete the last admin account."})。实测删除测试账号返回 {"ok":true}。

运行期设置

GET /api/admin/settings 返回 {"registrationOpen":false};PATCH /api/admin/settings 传非布尔值 200 {"error":"registrationOpen must be a boolean."},成功 {"registrationOpen":true,"error":null} 并立即生效(完整切换闭环实测见《Cleep 0.4.4 配置参数详解》第八章)。

十一、状态码与错误体速查表

全部错误体为单字段 JSON {"error":"..."}(认证类接口多带 user:null),文案固定如下,均为 2026-10-25 实测或源码核对原文:

状态码实测/源码错误体原文触发场景
200{"user":null,"error":"Invalid email or password."}登录失败(业务错误走 200,同类的还有标签重名、注册字段校验失败、改密密码错等)
200{"user":null}登出后/未登录查 me(不是错误,登录态为空)
201—(返回创建对象)创建笔记/标签/附件/用户、自助注册成功
400{"error":"Choose a password with at least 4 characters."}锁定密码太短
400{"error":"Only image, video, and audio files are accepted."}附件类型非法(含 SVG)
400{"error":"File is too large. Maximum size is 100MB."}(导入包为 200MB 文案)附件/导入包超限
400{"error":"No Google Keep notes found in that archive (expected Takeout/Keep/*.json)."}导入包结构不对
400{"error":"You can't delete your own account here. Sign in as a different admin."}管理员删自己
401{"error":"Authentication required."}未登录访问受保护接口(实测:未登录 GET /api/notes 即此回显)
401{"error":"This note is locked. Enter its password to save changes."}编辑锁定笔记未带密码
401{"error":"Incorrect password."}锁定相关接口密码错误
403{"user":null,"error":"Registration is currently closed."}注册开关关闭时注册(实测)
403{"error":"Admin access required."}普通用户访问管理端(实测)
403{"error":"This note is locked."}下载锁定笔记的附件
403{"error":"Unlock this note before adding attachments."}给锁定笔记传附件
404{"error":"Not found."}未知 API 路径(/api/* 兜底,实测)
404{"error":"Note not found."}笔记不存在/不属于本人(含跨用户访问、标签挂载时笔记或标签失配,实测)
404{"error":"Label not found."} / {"error":"Attachment not found."} / {"error":"User not found."}对应资源不存在或不属于本人
413—(无 JSON 错误体,Express 框架默认页)JSON 请求体超 1MB(附件走 multipart 不受限)
429{"error":"Too many requests. Please try again shortly."}限流触发(无 Retry-After,四组独立计数,详见配置篇)
500{"error":"Something went wrong. Please try again."}未预期异常(服务端仅记日志,细节不外泄)

三条判读规则:① 4xx 不一定都带 {"error":...} 固定结构之外的字段,判断成败永远先看状态码、再补看 error/user;② 404 有两套语义——「路径不存在」(Not found.)与「资源不存在/不可见」(Note not found. 等),排查 URL 拼写错误时注意区分;③ 200 不代表业务成功,登录/注册/标签类接口用 user:null 或 label:null 表达失败。

十二、使用注意事项

  1. Cookie 会话认证,无 API Key:程序化调用的标准姿势是「登录存 Cookie → 后续请求带 Cookie」,例如 curl -c jar.txt -X POST …/api/auth/login -d '{"email":"…","password":"…"}' -H 'Content-Type: application/json' 之后统一 -b jar.txt。401 大多是没带 Cookie 或会话过期(30 天),不是权限问题。
  2. SameSite=Strict 的联动:Cookie 只在同站请求中携带。前后端如果分属不同站点域名(跨站调用 API),浏览器根本不会带上会话——这不是 CORS 配置能解决的(Cleep 也未开放 CORS)。自托管场景让前端与 API 同站访问即可;跨站集成只能改在服务端发起请求(服务端不受 SameSite 约束)。
  3. 业务错误大量走 200:登录错密码、标签重名、注册字段校验失败等都是 HTTP 200 + null 对象 + error 文案。轮询与自动化务必解析 JSON 内容,不能只看状态码。
  4. 无 OpenAPI/官方文档:本篇即按源码与实测整理的 v0.4.4 全量接口参考;升级版本后请对照源码复核差异(尤其字段与错误文案)。
  5. 限流是单进程内存态:四组独立计数、按 IP 分桶、重启清零、多实例不共享,参数可用环境变量调整——机制与踩坑详见《Cleep 0.4.4 配置参数详解》第五章;脚本压测前先算清配额(auth 组 20 次/分钟连打开页面都算)。
  6. JSON 请求体 1MB 上限:所有 application/json 请求(含 blocks 正文)超 1MB 直接 413;大内容拆笔记。附件走 multipart,上限 100MB,互不影响。
  7. 锁定即加密、忘密无解:锁定笔记的正文与附件在服务端全为密文,管理员与数据库都救不回;/api/export 也不会带出锁定附件。给「锁定」设密码请按「永久丢失」的心态管理。
  8. 数据自留出口:定期 GET /api/export 拉 zip 即可整体备份用户数据(锁定附件除外);服务端另提供磁盘附件目录 + 数据库的完整备份路径(部署篇已验证的 scripts/backup.sh)。

十三、FAQ

Q1:脚本调用始终 401 Authentication required.,怎么破? 没带 Cookie。先 POST /api/auth/login 用 -c 存下 Cookie,后续每个请求 -b 带上;会话 30 天过期后重新登录。若在浏览器里测试,确认访问的是同一站点(SameSite=Strict,跨站不带 Cookie)。

Q2:登录返回 200 却拿不到 user? 看响应体 user 字段——业务失败(密码错/邮箱不存在)统一 200 + {"user":null,"error":"Invalid email or password."},与 HTTP 401 无关(第三章)。

Q3:锁定笔记的附件列表是空的,文件丢了吗? 没有。锁定笔记在所有列表/详情响应里都隐藏附件元数据(安全设计);带密码调 POST /api/notes/:id/unlock 可拿到附件清单,字节走 POST /api/notes/:id/unlock-attachment/:attachmentId(第五章)。

Q4:导出的 zip 里某笔记的 attachments 是空的? 该笔记处于锁定状态,密文附件被有意跳过(第九章)。解锁后重新导出即可。

Q5:链接预览对内网/localhost 地址返回 200 但卡片全空,是故障吗? 不是。SSRF 防护拒绝向私有地址发起请求,返回的是降级卡片(title/image 为 null,仅域名),并缓存 1 天(第八章)。公网链接正常出标题配图。

Q6:注册接口 403 Registration is currently closed.? 注册开关默认关闭。管理员调 PATCH /api/admin/settings 传 {"registrationOpen":true} 立即放行,用完记得关回去(第十章)。

Q7:能批量导入自己的旧笔记吗(非 Google Keep)? 官方通道只认 Google Takeout 的 Keep zip。其他来源可按 notes.json 的结构自查转换,或直接走 POST /api/notes 逐条创建(blocks 结构见第四章);5000 条/200MB 是单次导入硬上限。

Q8:管理员密码忘了,能调 API 重置吗? 需要有另一个管理员账号才能调 POST /api/admin/users/:id/password;没有则按配置篇 FAQ Q4 的三条途径处理(改库/重建引导)。重置会强制目标账号全设备下线。

十四、参考链接