Cleep 0.4.4 API 使用详解
Cleep 0.4.4 全接口 API 详解:认证与会话、笔记 CRUD 与视图查询、密码锁定与回收站、标签、附件上传下载、链接预览 SSRF 防护、导出与 Keep 导入、管理端用户与设置,附全端点总表、状态码错误体速查与全部实测回显。
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标签,与源码 tagv0.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/auth | GET /registration-status | 公开 | auth(20 次/分) |
/api/auth | POST /register | 公开(受注册开关控制) | auth |
/api/auth | POST /login | 公开 | auth |
/api/auth | POST /logout | 无需登录 | auth |
/api/auth | GET /me | 公开(未登录返回 {"user":null}) | auth |
/api/auth | PATCH /me | 登录 | auth |
/api/auth | POST /password | 登录 | auth |
/api/admin | GET /users | 管理员 | — |
/api/admin | POST /users | 管理员 | — |
/api/admin | PATCH /users/:id | 管理员 | — |
/api/admin | POST /users/:id/password | 管理员 | — |
/api/admin | DELETE /users/:id | 管理员 | — |
/api/admin | GET /settings、PATCH /settings | 管理员 | — |
/api/notes/:noteId/attachments | GET / | 登录 | — |
/api/notes/:noteId/attachments | POST /(multipart 上传) | 登录 | upload(60 次/分) |
/api/notes/:noteId/attachments | DELETE /:attachmentId | 登录 | — |
/api/notes | GET / | 登录 | — |
/api/notes | POST / | 登录 | — |
/api/notes | PATCH /:id | 登录 | — |
/api/notes | POST /:id/lock、/:id/unlock、/:id/unlock-attachment/:attachmentId、/:id/remove-lock | 登录 | note-lock(20 次/分) |
/api/notes | POST /:id/trash、/:id/restore | 登录 | — |
/api/notes | DELETE /:id | 登录 | — |
/api/notes | PUT /:id/labels/:labelId、DELETE /:id/labels/:labelId | 登录 | — |
/api/link-preview | GET /?url=…、GET /image?url=… | 登录 | link-preview(300 次/分) |
/api/labels | GET /、POST /、PATCH /:id、DELETE /:id | 登录 | — |
/api/attachments | GET /:id/file | 登录 | — |
/api | GET /export | 登录 | upload |
/api | POST /import/keep(multipart 上传) | 登录 | upload |
三条全局规则先记住:
- 鉴权靠 Cookie 会话,不是 API Key。除「公开」标注的接口外,全部接口要求携带登录后下发的
sessionCookie;未登录访问受保护接口统一返回401 {"error":"Authentication required."}(实测)。 - 四组限流互相独立(auth/upload/note-lock/link-preview),数值与计数机制见《Cleep 0.4.4 配置参数详解》第五章;其中 auth 组挂在
/api/auth整个前缀上,连查注册开关都计数。 - 未知 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
单接口承担全部视图,靠查询参数区分:
| 参数 | 取值 | 说明 |
|---|---|---|
view | notes(默认)/ archive / trash / recordings | 不传等价 notes:非回收、非归档、非录音的笔记;archive 只看归档;trash 只看回收站;recordings 只看语音备忘 |
label | 标签 id,可重复出现(多值) | 按标签过滤;传入不属于本人的 id 会被静默忽略 |
labelMatch | any(默认)/ 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):
- 用密码 + 随机盐派生 AES-GCM 密钥,把
blocks加密写入encryptedBlocks,同时清空明文列(blocks/content/items全部置空)——锁定后数据库里不存在正文明文; - 同步加密该笔记全部附件的文件字节(落盘文件原地变密文,
iv/authTag记入附件行); - 可选带
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."}。
八、链接预览
元数据:GET /api/link-preview?url=…
为笔记里的外部链接抓取标题/配图(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 即无有效预览」处理,别把它当故障。
图片代理:GET /api/link-preview/image?url=…
把外部图片/图标经本服务中转返回: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 表达失败。
十二、使用注意事项
- 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 天),不是权限问题。 SameSite=Strict的联动:Cookie 只在同站请求中携带。前后端如果分属不同站点域名(跨站调用 API),浏览器根本不会带上会话——这不是 CORS 配置能解决的(Cleep 也未开放 CORS)。自托管场景让前端与 API 同站访问即可;跨站集成只能改在服务端发起请求(服务端不受 SameSite 约束)。- 业务错误大量走 200:登录错密码、标签重名、注册字段校验失败等都是 HTTP 200 +
null对象 +error文案。轮询与自动化务必解析 JSON 内容,不能只看状态码。 - 无 OpenAPI/官方文档:本篇即按源码与实测整理的 v0.4.4 全量接口参考;升级版本后请对照源码复核差异(尤其字段与错误文案)。
- 限流是单进程内存态:四组独立计数、按 IP 分桶、重启清零、多实例不共享,参数可用环境变量调整——机制与踩坑详见《Cleep 0.4.4 配置参数详解》第五章;脚本压测前先算清配额(auth 组 20 次/分钟连打开页面都算)。
- JSON 请求体 1MB 上限:所有
application/json请求(含 blocks 正文)超 1MB 直接 413;大内容拆笔记。附件走 multipart,上限 100MB,互不影响。 - 锁定即加密、忘密无解:锁定笔记的正文与附件在服务端全为密文,管理员与数据库都救不回;
/api/export也不会带出锁定附件。给「锁定」设密码请按「永久丢失」的心态管理。 - 数据自留出口:定期
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 的三条途径处理(改库/重建引导)。重置会强制目标账号全设备下线。
十四、参考链接
- Cleep 官方仓库(源码均出自 tag
v0.4.4):https://github.com/blindpassasjer/cleep (查询日期 2026-10-25) - 路由挂载与全局兜底:https://github.com/blindpassasjer/cleep/blob/main/server/app.ts
- 认证路由:https://github.com/blindpassasjer/cleep/blob/main/server/routes/auth.ts
- 笔记路由(含锁定/回收站):https://github.com/blindpassasjer/cleep/blob/main/server/routes/notes.ts
- 标签路由:https://github.com/blindpassasjer/cleep/blob/main/server/routes/labels.ts
- 附件路由:https://github.com/blindpassasjer/cleep/blob/main/server/routes/noteAttachments.ts 、 https://github.com/blindpassasjer/cleep/blob/main/server/routes/attachmentFiles.ts
- 附件类型/大小限制实现:https://github.com/blindpassasjer/cleep/blob/main/server/lib/attachments.ts
- 链接预览与 SSRF 防护:https://github.com/blindpassasjer/cleep/blob/main/server/routes/linkPreview.ts 、 https://github.com/blindpassasjer/cleep/blob/main/server/lib/ssrf.ts
- 导出与 Keep 导入:https://github.com/blindpassasjer/cleep/blob/main/server/routes/dataTransfer.ts
- 管理端路由:https://github.com/blindpassasjer/cleep/blob/main/server/routes/admin.ts
- 会话与限流中间件:https://github.com/blindpassasjer/cleep/blob/main/server/middleware/session.ts 、 https://github.com/blindpassasjer/cleep/blob/main/server/middleware/rateLimit.ts
- 本专栏系列:《Cleep 0.4.4 介绍与选型指南》、《Cleep 0.4.4 Docker Compose 部署指南》、《Cleep 0.4.4 配置参数详解》