Note Mark REST API 使用详解(基于 1.1.1 实测)

Note Mark 1.1.1 REST API 全量实测:Bearer 令牌与会话 Cookie 双轨认证、用户管理、空间/笔记层级 slug 体系、frontmatter 分享控制、附件上传、ETag 条件请求与回收站,全部接口附实测回显。

Note Mark自托管笔记Markdown知识库2026/10/30

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 配置参数详解》)。

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/usersPOST注册新用户:{"username":"tester","password":"t5w6e7r8","name":"测试账号"},实测 HTTP 201 并回显完整用户对象
/api/users/{username}PUT修改自己的 name
/api/users/{username}/passwordPUT修改自己的密码:{"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 实测确认的三条关键语义

  1. publicRead 只对匿名访客生效。实测用另一个登录用户 tester 请求 admin 的 publicRead 笔记,返回 404 Not Found——登录用户要走 users 映射,这个语义从字段名看不出来,务必留意。
  2. 分享可见性需要沿路径逐级开启。实测仅给子笔记设 publicRead 而父空间未设时,匿名请求用户树中看不到该空间(整棵子树被过滤);父子都开启后匿名访问树与内容均返回 200。
  3. 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
登录用户读公开笔记 404publicRead 仅匿名生效,登录用户需 users 映射
重命名后笔记"消失"新 slug 被当作相对用户根的完整路径,单段名会移到顶层,用 GET 树确认后改回完整路径即可
删除回收站深层节点 500已实测复现的服务端问题,重试无效后改用 CLI clean trash
匿名树看不到已分享笔记沿路径逐级开启 publicRead

九、延伸阅读