Note Mark REST API 使用详解(基于 1.1.1 实测)
Note Mark 1.1.1 REST API 全量实测:Bearer 令牌与会话 Cookie 双轨认证、用户管理、空间/笔记层级 slug 体系、frontmatter 分享控制、附件上传、ETag 条件请求与回收站,全部接口附实测回显。
Note Mark REST API 使用详解(基于 1.1.1 实测)
Note Mark 内置完整 REST API,官方前端与第三方客户端走同一套接口。本文全部请求与回显均在 1.1.1 实测服务器逐条执行抄录,示例账号 admin / q1w2e3r4。接口行为与部署方式无关,部署前置见《Note Mark Docker Compose 部署指南》。
一、API 总览与发现
服务自带 OpenAPI 规范与交互文档:
- OpenAPI 规范:
GET /api/openapi - 交互式文档:浏览器打开
/api/docs - 服务器信息:
GET /api/info - 心跳:
GET /heartbeat(返回.,HTTP 200)
/api/info 实测回显:
{"minSupportedVersion":"1.0.0","allowInternalSignup":true,"allowInternalLogin":true,"enableAnonymousUserSearch":true}
三个布尔值分别对应 ENABLE_INTERNAL_SIGNUP、ENABLE_INTERNAL_LOGIN、ENABLE_ANONYMOUS_USER_SEARCH 配置项(配置语义见《Note Mark 配置参数详解》)。
二、认证:Bearer 令牌与会话 Cookie 双轨制
Note Mark 同时支持两种认证方式,可按客户端形态选择:
2.1 访问令牌(Bearer,推荐脚本使用)
curl http://192.168.1.200:8136/api/auth/o/token -X POST \
-H 'Content-Type: application/json' \
-d '{"grant_type":"password","username":"admin","password":"q1w2e3r4"}'
实测回显:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMWExOGJiOC03MzY0LTc5YjgtYjY5YS0xYTQwYzA3OTIxNzIiLCJleHAiOjE3OTM2MDYyOTh9.6NZ8BQeQlkwvZxzNEfVobpXDK9K46inX50BmY5km8Zo","token_type":"Bearer","expires_in":259200}
后续请求携带 Authorization: Bearer <access_token>。注意 grant_type 必填且值为 password——实测漏掉该字段返回 422 校验错误(见排障章节)。
令牌有效期 expires_in: 259200(3 天),由 AUTH_TOKEN__EXPIRY 控制。
2.2 会话 Cookie(浏览器形态)
curl http://192.168.1.200:8136/api/auth/s/start -X POST \
-H 'Content-Type: application/json' \
-d '{"grant_type":"password","username":"admin","password":"q1w2e3r4"}' \
-c cookies.txt
实测返回 HTTP 204 并下发 Cookie:
Set-Cookie: Auth-Session-Token=eyJ...; Path=/; Max-Age=259200; HttpOnly; SameSite=Strict
检查与登出:
curl -b cookies.txt http://192.168.1.200:8136/api/auth/s/has-session # 实测 true
curl -b cookies.txt -X DELETE http://192.168.1.200:8136/api/auth/s/end # 实测 HTTP 204
curl -b cookies.txt http://192.168.1.200:8136/api/auth/s/has-session # 实测 false
2.3 身份自证与鉴权失败
curl http://192.168.1.200:8136/api/auth/o/userinfo -H "Authorization: Bearer $TOKEN"
实测回显:
{"sub":"01a18bb8-7364-79b8-b69a-1a40c0792172","preferred_username":"admin"}
不带令牌请求受保护接口实测返回:
{"title":"Unauthorized","status":401,"detail":"authentication is required but none was provided"}
三、用户管理
| 接口 | 方法 | 说明 |
|---|---|---|
/api/users/search?username=<前缀> | GET | 用户名前缀搜索,查询参数名是 username。实测 ?username=adm 返回 ["admin"] |
/api/users/{username} | GET | 查询用户公开资料。实测返回 {"createdAt":"...","uid":"...","username":"admin","name":null} |
/api/users | POST | 注册新用户:{"username":"tester","password":"t5w6e7r8","name":"测试账号"},实测 HTTP 201 并回显完整用户对象 |
/api/users/{username} | PUT | 修改自己的 name |
/api/users/{username}/password | PUT | 修改自己的密码:{"existingPassword":"...","newPassword":"..."} |
注册校验实测规则:用户名 3~30 位纯字母数字;密码至少 8 位。修改类接口只能操作自己——用 admin 的令牌修改 tester 资料实测返回:
{"title":"Forbidden","status":403,"detail":"you do not have permission to update another users account"}
管理员重置他人密码走服务端 CLI(user set-password),API 层无此能力。
四、空间与笔记:层级 slug 体系
这是 Note Mark API 的核心模型。没有独立的"空间接口",一切皆树节点:
用户根
├── my-space/ ← 空间(slug 无扩展名,作为父节点)
│ ├── meeting.md ← 笔记(同样无扩展名)
│ └── img.png ← 附件(有扩展名 = asset 节点)
└── my-space.md ← 空间首页内容
slug 规则实测:段间以 / 分隔,段内允许字母、数字、短横线、下划线、空格;无扩展名为笔记类节点,带扩展名为附件类节点;附件不能位于用户根(必须有父空间)。
4.1 读取整棵树
curl http://192.168.1.200:8136/api/tree/u/admin -H "Authorization: Bearer $TOKEN"
实测回显(节选):
{
"my-space": {
"slug": "my-space", "type": "note",
"modTime": "2026-10-30T07:58:53.449343583Z",
"frontmatter": {"title": "我的空间"},
"children": {
"meeting-notes": {"slug": "meeting-notes", "type": "note", "frontmatter": {"title": "会议纪要"}, "children": {}}
}
}
}
4.2 创建与更新(PUT 即 Upsert)
创建空间首页与子笔记都是对 content 接口 PUT 原始 Markdown:
# 创建空间(空间首页)
curl -X PUT http://192.168.1.200:8136/api/tree/content/u/admin/my-space \
-H "Authorization: Bearer $TOKEN" \
--data-binary $'---\ntitle: 我的空间\n---\n# 我的空间\n\n这是空间首页内容。\n'
# 实测 HTTP 204
# 创建空间下笔记
curl -X PUT http://192.168.1.200:8136/api/tree/content/u/admin/my-space/meeting-notes \
-H "Authorization: Bearer $TOKEN" \
--data-binary $'---\ntitle: 会议纪要\n---\n# 会议纪要\n\n- 讨论 API 发布\n'
# 实测 HTTP 204
重复 PUT 同一 slug 即更新内容。读取时原样返回含 frontmatter 的 Markdown 文本(HTTP 200)。
4.3 只改元数据(frontmatter)
curl -X PUT http://192.168.1.200:8136/api/tree/frontmatter/u/admin/my-space/meeting-notes \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"会议纪要","accessControl":{"publicRead":true}}'
# 实测 HTTP 204
服务会把 title/accessControl 合并写回笔记文件的 YAML 头部,正文不动。
4.4 重命名(注意:新 slug 是完整路径)
curl -X POST http://192.168.1.200:8136/api/tree/rename/u/admin/my-space/meeting-notes \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '"my-space/weekly-meeting"'
# 实测 HTTP 204
实测踩坑记录:若新 slug 只传 "weekly-meeting",节点会被移动到用户根目录(重命名语义是"移动到新完整路径"),而非原地改名。跨空间移动笔记正是靠这个语义实现的。
4.5 附件(asset)上传与下载
curl -X PUT http://192.168.1.200:8136/api/tree/content/u/admin/my-space/test-img.png \
-H "Authorization: Bearer $TOKEN" --data-binary @test-img.png
# 实测 HTTP 204
下载回显实测 Content-Type: application/octet-stream——服务不按扩展名猜测 MIME,统一按二进制流返回,前端以 WASM 渲染器自行处理。
4.6 回收站
# 移入回收站(返回新位置)
curl -X POST http://192.168.1.200:8136/api/tree/move-to-trash/u/admin/my-space/weekly-meeting \
-H "Authorization: Bearer $TOKEN"
# 实测回显:".trash/20261030T080143-848Z/my-space/weekly-meeting"
# 永久删除(URL 含 .trash 段)
curl -X DELETE http://192.168.1.200:8136/api/tree/u/admin/.trash/20261030T080143-848Z/my-space/weekly-meeting \
-H "Authorization: Bearer $TOKEN"
回收站按时间戳目录隔离、保留原层级。实测踩坑记录:对回收站深层路径的 DELETE 请求偶发 500 unknown error occurred,重试仍复现时用 CLI 兜底:clean trash。
五、分享与协作控制
分享能力全部通过 frontmatter 的 accessControl 表达,两种粒度:
---
title: 会议纪要
accessControl:
publicRead: true # 匿名可读
users:
tester: write # 指定用户可写(read/write)
---
5.1 实测确认的三条关键语义
publicRead只对匿名访客生效。实测用另一个登录用户 tester 请求 admin 的publicRead笔记,返回404 Not Found——登录用户要走users映射,这个语义从字段名看不出来,务必留意。- 分享可见性需要沿路径逐级开启。实测仅给子笔记设
publicRead而父空间未设时,匿名请求用户树中看不到该空间(整棵子树被过滤);父子都开启后匿名访问树与内容均返回 200。 - 403 与 404 的语义区分。匿名访问未分享节点返回 404(不暴露存在性);登录用户对存在但无权限的节点实测返回明确的
403 Forbidden(detail: "you don't have permission")。
5.2 协作写入实测
admin 给 tester 开启空间与笔记两级 write 后,tester 直接 PUT 更新内容:
curl -X PUT http://192.168.1.200:8136/api/tree/content/u/admin/my-space/html-note \
-H "Authorization: Bearer $TESTER_TOKEN" --data-binary '更新后的正文'
# 实测 HTTP 204
5.3 ETag 条件请求
树接口响应带个性化 ETag(同一棵树对不同用户摘要不同),实测配合 If-None-Match 命中缓存:
curl -D - -o /dev/null http://192.168.1.200:8136/api/tree/u/admin \
-H "Authorization: Bearer $TOKEN" -H 'If-None-Match: "6c9bc57e...abc4b3"'
# 实测 HTTP 304
六、自定义 CSS 与静态资源
GET /api/static/custom.css:配置了 CUSTOM_CSS_PATH 时返回该文件内容(实测原样返回 body{font-family:sans-serif});未配置时返回 HTTP 200 空响应体。前端界面自动加载该样式,可用于品牌定制。
七、脚本化全流程示例
#!/bin/bash
BASE=http://192.168.1.200:8136
TOKEN=$(curl -s $BASE/api/auth/o/token -X POST -H 'Content-Type: application/json' \
-d '{"grant_type":"password","username":"admin","password":"q1w2e3r4"}' | \
python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
# 1. 建空间与笔记
curl -s -o /dev/null -w '%{http_code}\n' -X PUT $BASE/api/tree/content/u/admin/demo-space \
-H "Authorization: Bearer $TOKEN" --data-binary $'---\ntitle: 演示空间\n---\n# 演示空间\n'
# 2. 开启匿名分享
curl -s -o /dev/null -w '%{http_code}\n' -X PUT $BASE/api/tree/frontmatter/u/admin/demo-space \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"演示空间","accessControl":{"publicRead":true}}'
# 3. 匿名验证(无需令牌)
curl -s $BASE/api/tree/content/u/admin/demo-space
八、排障速查
| 现象 | 根因与处置 |
|---|---|
| 换令牌返回 422 validation failed | 请求体缺 grant_type 字段,固定传 "password" |
| 用户搜索 422 | 查询参数名是 username,不是 q |
| 登录用户读公开笔记 404 | publicRead 仅匿名生效,登录用户需 users 映射 |
| 重命名后笔记"消失" | 新 slug 被当作相对用户根的完整路径,单段名会移到顶层,用 GET 树确认后改回完整路径即可 |
| 删除回收站深层节点 500 | 已实测复现的服务端问题,重试无效后改用 CLI clean trash |
| 匿名树看不到已分享笔记 | 沿路径逐级开启 publicRead |
九、延伸阅读
- 组件定位与技术栈背景:《Note Mark 介绍与选型指南:轻量级自托管 Markdown 笔记与分享服务》
- 从零部署与运维排障:《Note Mark Docker Compose 部署指南》
- 环境变量逐项说明:《Note Mark 配置参数详解》