bewCloud v4.21.0 API 使用详解

bewCloud v4.21.0 全部程序化接口实测详解:会话认证与首次调用、files/dashboard/expenses/calendar/contacts/auth 六个 JSON API 域端点参数与真实响应、WebDAV 与 CalDAV/CardDAV 端点 curl 调用、上传会话标签算法、错误形态速查与分页过滤约定,全部请求回显 2026-10-30 实测。

bewCloudbewCloudCalDAVWebDAV 服务器2026/10/30

bewCloud v4.21.0 API 使用详解

目标:把 bewCloud v4.21.0 对外暴露的全部程序化接口讲清并逐条实测——会话认证与首次调用、files/dashboard/expenses/calendar/contacts/auth 六个 JSON API 域的端点、参数与真实响应,以及 WebDAV 文件端点与 CalDAV/CardDAV 协议端点的 curl 调用方法。所有请求与响应均为 2026-10-30 在实测环境(Linux x86_64、bewCloud v4.21.0)逐条执行的真实回显,不转述官方文档示例。

本文是 bewCloud 系列的第四篇。部署过程见 《bewCloud v4.21.0 Docker Compose 部署指南》,接口行为背后的配置项见 《bewCloud v4.21.0 配置参数详解》。

bewCloud 没有独立的管理 CLI,也没有单独的 API 网关——它对外只有一个入口端口,Web 界面、JSON API、WebDAV、CalDAV/CardDAV 四类协议面共用。这意味着「程序化使用 bewCloud」= 用 HTTP 客户端(curl、脚本、DAV 客户端)直接对话这一个端口,本篇逐域演示怎么做。

一、环境信息

项目值
组件版本bewCloud v4.21.0
服务端点http://<服务器IP>:8155(Web / JSON API / WebDAV / CalDAV / CardDAV 共用单端口,应用内 8000)
实测账号admin@example.com / q1w2e3r4(首用户管理员,注册过程见 4.1)
配置要点skipCookieDomainSecurity: true(HTTP+IP 访问)、allowPublicSharing: true、allowDirectoryDownloads: true(本篇分享与打包下载实测依赖,取值依据见配置篇)
客户端工具curl 8.5.0(2.3 实测)
实测日期2026-10-30

二、前置条件(端点/认证/客户端工具)

2.1 服务端点

bewCloud 的全部接口收敛在一个端口(本篇实测映射为宿主 8155)。四类协议面按路径前缀划分:

协议面路径前缀认证载体典型用途
JSON API/api/*会话 Cookie脚本化操作文件、记账、日历、仪表盘
WebDAV/dav/会话 Cookie桌面挂载网络磁盘、脚本上传下载
CalDAV/CardDAV/caldav/、/carddav/、/.well-known/*会话 Cookie 或 Basic 认证(5.8 实测两者均通过)手机日历、通讯录直连同步
页面路由/login、/signup、/dashboard、/file-share/<id> 等Cookie / 公开会话建立、公开分享落地

页面路由与 JSON API 同源:登录态由同一枚 Cookie 承载,页面里前端小岛调用的正是 /api/* 这套接口——用 curl 模拟浏览器行为即可完全复现界面上的操作。

2.2 认证方式

实测结论先给出,细节与回显见第四节与 5.9:

  • 没有 API Key/Token 机制:bewCloud v4.21.0 不提供长期 API 令牌,程序化调用一律以登录会话 Cookie 模拟浏览器会话——脚本先 POST /login 拿 Cookie,后续请求全部携带;
  • 会话 Cookie:登录后服务端签发名为 bewcloud-app-v1 的 HttpOnly Cookie,值是一个 JWT(内含 user_id 与 session_id),有效期长达一年(4.2 实测 Cookie 过期时间戳);
  • DAV 端点双认证:/caldav/、/carddav/ 既接受会话 Cookie,也接受邮箱+口令的 Basic 认证(5.8 两种方式实测均返回 207)——后者正是手机日历/通讯录客户端使用的形态;
  • 上传会话标签:/api/files/upload 要求额外提交 upload_session_tag(会话 ID 与口令盐的 SHA-256 哈希,见 5.1),这是官方针对「上传队列跨会话存活」设计的防护,纯 Cookie 调用也绕不开。

2.3 客户端工具

本篇全部示例用 curl 完成,实测环境版本:

curl --version | head -1

实测回显:

curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.2.11 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 (+libidn2/2.3.7) libssh/0.10.6/openssl/zlib nghttp2/1.59.0 librtmp/2.3 OpenLDAP/2.6.10

后续示例中的 Cookie 统一保存在 ~/bewcloud-cookie.txt(-c 写入、-b 携带),路径可按需替换。

三、API 总览

3.1 JSON API 域清单

v4.21.0 的 JSON API 按应用域划分(端点清单来源:官方仓库 routes.ts 与实例容器内 pages/api/ 路由目录实查,2026-10-30;方法与参数行为以下各节实测为准):

域端点说明
files/api/files/upload、upload-chunk、upload-abort、get、get-directories、search、create-directory、delete-directory、rename-directory、move-directory、rename、move、delete、download-directory、create-share、get-share、update-share、delete-share、update-sort文件上传(直传+分片)、目录/文件管理、公共分享;另有 /dav/ WebDAV 通道(5.7)
dashboard/api/dashboard/save-links、save-notes仪表盘快捷链接与速记保存
expenses/api/expenses/add-budget、add-expense、update-budget、update-expense、delete-budget、delete-expense、auto-complete、export-expenses、import-expenses记账与预算管理
calendar/api/calendar/add、add-event、update、delete、delete-event、search-events、export-events、import日历与事件管理;另有 /caldav/ 协议通道
contacts/api/contacts/add-addressbook、get-addressbooks、add、get、delete-addressbook、delete、import通讯录与地址簿;另有 /carddav/ 协议通道
auth/api/auth/multi-factor/email/setup、passkey/{begin,setup-begin,setup-complete,verify}、totp/setup、enable、disableMFA/Passkey 管理(enableMultiFactor 开启后可用)

清单里的端点绝大多数是 POST;download-directory 是 GET(查询参数传目录路径,5.1 实测踩过 POST 返回 501 的坑)。本篇逐域实测最常用的一条主线,其余端点的请求结构与同域端点同构(同样的 success 包裹与字段命名风格)。

3.2 认证与限额约定

  • 认证:全部 /api/* 与 /dav/、/caldav/、/carddav/ 端点要求已登录会话;未认证请求的实际形态见 4.4 实测;
  • 请求体:JSON 域接口为 JSON 请求体(Content-Type: application/json),文件上传为 multipart 表单——均以各节实测回显为准;
  • 响应包裹:业务响应统一为 JSON 对象,第一字段是 success 布尔值,失败时附 error 字符串(5.6 的 403 实测可见);
  • 限额:一般请求体上限 core.maxRequestSizeInMegabytes(默认 12MB),文件上传走独立限额 files.maxUploadSizeInMegabytes(默认 100MB),配置依据见配置篇 5.2/5.3;
  • 无分页:列表类响应一次性返回全量条目(5.1/5.4 实测均无游标或页码字段),详见第六节。

四、认证与首次调用(curl 实测)

4.1 注册第一个用户

新实例没有任何用户时,第一个注册请求会被放行并自动成为管理员(allowSignups=false 也放行,行为依据见配置篇 5.1)。先确认注册页与表单字段名:

curl -s http://<服务器IP>:8155/signup | grep -oE 'name="[^"]+"|type="email"|type="password"' | sort | uniq -c

实测回显:

      1 name="author"
      1 name="description"
      1 name="email"
      1 name="password"
      1 name="viewport"
      1 type="email"
      1 type="password"

(author/description/viewport 是页面 meta 标签的同名属性,注册表单字段只有 email 和 password 两个。)按表单字段提交注册:

curl -s -X POST http://<服务器IP>:8155/signup \
  -d 'email=admin@example.com&password=q1w2e3r4' \
  -o /dev/null -w '%{http_code} %{redirect_url}\n'

实测回显:

303 http://192.168.146.128:8155/login?success=signup&email=admin%40example.com

303 跳转到登录页并携带 success=signup 参数——首用户注册成功,该账号即实例管理员。注册后如果用同一账号再次提交,注册页会重渲染并提示账号已存在。

curl -s -c ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/login \
  -d 'email=admin@example.com&password=q1w2e3r4' \
  -o /dev/null -w '%{http_code} %{redirect_url}\n'

实测回显:

303 http://192.168.146.128:8155/

登录成功,303 跳转首页,会话 Cookie 已写入 ~/bewcloud-cookie.txt。查看拿到的 Cookie 内容:

cat ~/bewcloud-cookie.txt

实测回显:

# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_192.168.146.128	FALSE	/	FALSE	1796016700	bewcloud-app-v1	eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOiI5ZDVlYmFkNC1mNmE3LTQ1ODgtODg0ZS1mOGM4YzhlZGU2MzIiLCJzZXNzaW9uX2lkIjoiYTg5MWM3ZTQtZDQ5MC00NmRiLTg1MTYtOTExYjY5ZTMzMGI2In19.sRkPQF0J-PS5ZIw13UJC3v25SX5X8hbNYd4pvqIq24w

关键信息三处:Cookie 名 bewcloud-app-v1;标记 #HttpOnly_ 前缀说明服务端下了 HttpOnly 属性(浏览器脚本读不到,curl 不受影响);过期时间戳 1796016700 换算约为 2027 年——会话有效期一年。JWT 载荷(中间段 base64 解码)含 user_id 与 session_id,5.1 节计算上传标签要用到 session_id。

curl -s -b ~/bewcloud-cookie.txt -o /dev/null -w '%{http_code}\n' http://<服务器IP>:8155/dashboard

实测回显:

200

4.4 未认证请求的实际形态

不带 Cookie 分别打页面与 API 端点,确认未认证形态(脚本里可据此判断会话失效):

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://<服务器IP>:8155/dashboard
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://<服务器IP>:8155/api/dashboard/save-notes -X POST -H 'Content-Type: application/json' -d '{}'

实测回显:

302 http://192.168.146.128:8155/login?redirectTo=%2Fdashboard
302 http://192.168.146.128:8155/login?redirectTo=%2Fapi%2Fdashboard%2Fsave-notes

两类端点行为一致:302 重定向到登录页并带回跳地址,而不是 REST 风格的 401 JSON。这是「API 为页面服务」的同构设计——脚本侧判断会话失效的依据是 302 而非 401。

五、各 API 域详解

以下各域请求均在 2026-10-30 实测执行,回显逐条照抄(长响应按「输出节选(实测首尾)」标注截取)。除特别注明外,请求都携带 4.2 拿到的 Cookie(-b ~/bewcloud-cookie.txt)。实测环境用户 ID 为 9d5ebad4-f6a7-4588-884e-f8c8c8ede632,默认日历 ID 为 03f9ab87-3c19-49a0-95ed-cd31b40b5795(取自 /calendar 页面数据),读者实测时会得到自己的 ID。

5.1 files 域:文件与目录

files 域是最常被脚本化的域。主线六步:建目录 → 上传 → 列目录 → 建分享 → 公开访问 → 打包下载,最后清理。

创建目录(JSON 请求体 {parentPath, name}):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/files/create-directory \
  -H 'Content-Type: application/json' -d '{"parentPath":"/","name":"docs"}'

实测回显(输出节选):

{"success":true,"newDirectories":[{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","directory_name":".Trash",…},{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","directory_name":"docs","has_write_access":true,"size_in_bytes":4096,"file_share_id":null,"updated_at":"2026-10-30T05:36:36.986Z","created_at":"2026-10-30T05:36:36.986Z"}]}

首次创建目录时,应用自动建立了 .Trash 回收站目录;响应返回变更后的完整目录列表(newDirectories),这个「变更 + 新列表」的响应模式贯穿 files 域。

上传文件(multipart 表单,字段:path_in_view、parent_path、name、contents、overwrite、upload_session_tag)。upload_session_tag 的算法(源码 lib/auth.ts 实查):SHA-256("<session_id>:<PASSWORD_SALT>"),其中 session_id 在 4.2 的 JWT 载荷里、PASSWORD_SALT 是部署时 .env 里的值。实测环境先算标签再上传:

SID=a891c7e4-d490-46db-8516-911b69e330b6
TAG=$(printf '%s:bewcloud-demo-salt-4d8b1f6a' "$SID" | sha256sum | cut -d' ' -f1)
echo 'hello bewCloud' > /tmp/hello.txt
curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/files/upload \
  -F 'path_in_view=/' -F 'parent_path=/' -F 'name=hello.txt' \
  -F "upload_session_tag=$TAG" -F 'overwrite=true' -F 'contents=@/tmp/hello.txt'

实测回显(输出节选):

{"success":true,"newFiles":[{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","file_name":"hello.txt","has_write_access":true,"size_in_bytes":15,"file_share_id":null,"updated_at":"2026-10-30T05:36:37.166Z","created_at":"2026-10-30T05:36:37.166Z"}],"newDirectories":[{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","directory_name":".chunk-uploads",…},…]}

上传成功返回新文件条目(15 字节);首次上传还会自动创建 .chunk-uploads 分片暂存目录(配合 /api/files/upload-chunk 大文件分片上传)。缺 upload_session_tag 或标签算错,上传返回 403 Forbidden——这是纯 Cookie 调用也绕不开的防护(详见第七节 FAQ)。

列出目录(JSON 请求体 {parentPath},只返回目录;文件列表由页面渲染):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/files/get-directories \
  -H 'Content-Type: application/json' -d '{"parentPath":"/"}'

实测回显(输出节选):

{"success":true,"directories":[{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","directory_name":".chunk-uploads",…},{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","directory_name":".Trash",…},{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","directory_name":"docs","has_write_access":true,"size_in_bytes":4096,"file_share_id":null,"updated_at":"2026-10-30T05:36:36.986Z","created_at":"2026-10-30T05:36:36.986Z"}]}

创建公共分享(JSON 请求体 {pathInView, filePath, password},password 可空):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/files/create-share \
  -H 'Content-Type: application/json' -d '{"pathInView":"/","filePath":"/hello.txt","password":""}'

实测回显(输出节选):

{"success":true,"newFiles":[{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","file_name":"hello.txt","has_write_access":true,"size_in_bytes":15,"file_share_id":"55d3ac8e-5d0c-4ffb-9af0-c3080b3e70c4",…}],"newDirectories":[…],"createdFileShareId":"55d3ac8e-5d0c-4ffb-9af0-c3080b3e70c4"}

响应尾部的 createdFileShareId 就是分享 ID,公开链接为 /file-share/<id>。

公开访问分享链接(不带 Cookie 访问):

curl -s -o /dev/null -w '%{http_code}\n' http://<服务器IP>:8155/file-share/55d3ac8e-5d0c-4ffb-9af0-c3080b3e70c4
curl -s http://<服务器IP>:8155/file-share/55d3ac8e-5d0c-4ffb-9af0-c3080b3e70c4 | grep -o '<title>[^<]*</title>'

实测回显:

200
<title>File Share - bewCloud</title>

分享落地页无需登录即可打开,页内提供文件下载。若 allowPublicSharing=false(默认值),create-share 接口整体不可用(403)。

目录打包下载(注意是 GET + 查询参数,不是 POST):

curl -s -b ~/bewcloud-cookie.txt \
  'http://<服务器IP>:8155/api/files/download-directory?parentPath=/&name=docs' \
  -o /tmp/docs.zip -w '%{http_code} %{content_type}\n'
unzip -l /tmp/docs.zip | tail -2

实测回显(目录中已放入一个 12 字节的 dav-note.txt,见 5.7):

200 application/zip
---------                     -------
       12                     1 file

allowDirectoryDownloads=false(默认值)时该端点返回 403;用 POST 打它则返回 501 Not Implemented(5.9 实测踩坑记录)。

删除文件与分享(两个端点):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/files/delete \
  -H 'Content-Type: application/json' -d '{"parentPath":"/","name":"hello.txt"}'
curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/files/delete-share \
  -H 'Content-Type: application/json' -d '{"pathInView":"/","fileShareId":"aa80bdcf-fa9a-4501-808d-595ebeb57012"}'
curl -s -o /dev/null -w '%{http_code}\n' http://<服务器IP>:8155/file-share/aa80bdcf-fa9a-4501-808d-595ebeb57012

实测回显(输出节选):

{"success":true,"newFiles":[]}
{"success":true,"newFiles":[{"user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","parent_path":"/","file_name":"share.txt",…}],"newDirectories":[…]}
404

delete 删除文件(newFiles 变空);delete-share 撤销分享——注意它要求同时传 pathInView 与 fileShareId,只传 ID 实测返回 400 Bad Request;分享撤销后原公开链接立即 404。

5.2 dashboard 域:速记与快捷链接

dashboard 域只有两个保存端点(读取随页面渲染返回)。实测速记保存:

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/dashboard/save-notes \
  -H 'Content-Type: application/json' -d '{"notes":"API 实测速记 2026-10-30"}'

实测回显:

{"success":true}

save-links 同构,请求体为 links 数组(每项含名称与 URL),保存后仪表盘显示自定义快捷链接。两个接口都是「全量覆盖」语义:每次保存的是完整列表,不是增量追加。

5.3 expenses 域:记账

记账域主线四步:建预算 → 记支出 → 自动补全 → 导出。

建预算({name, month, value, currentMonth}):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/expenses/add-budget \
  -H 'Content-Type: application/json' -d '{"name":"餐饮","month":"2026-10","value":800,"currentMonth":"2026-10"}'

实测回显:

{"success":true,"newBudgets":[{"id":"a04deb98-d75f-4785-8c2e-16db42c40641","user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","name":"餐饮","month":"2026-10","value":800,"extra":{"usedValue":0,"availableValue":800},"created_at":"2026-10-30T05:37:46.736Z"}]}

记支出({cost, description, budget, date, is_recurring, month}):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/expenses/add-expense \
  -H 'Content-Type: application/json' \
  -d '{"cost":35.5,"description":"午餐","budget":"餐饮","date":"2026-10-30","is_recurring":false,"month":"2026-10"}'

实测回显(输出节选):

{"success":true,"newExpenses":[{"id":"3bc6e879-fe73-44a5-b5ad-d2c37da1cf0b","user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","cost":35.5,"description":"午餐","budget":"餐饮","date":"2026-10-30","is_recurring":false,"extra":{},"created_at":"2026-10-30T05:37:47.078Z"}],"newBudgets":[{"id":"a04deb98-d75f-4785-8c2e-16db42c40641","user_id":"9d5ebad4-f6a7-4588-884e-f8c8c8ede632","name":"餐饮","mon

响应同时带回支出与预算(预算的 extra 内含用量,界面据此渲染进度)。描述自动补全(输入至少 2 个字符):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/expenses/auto-complete \
  -H 'Content-Type: application/json' -d '{"name":"午餐"}'

实测回显:

{"success":true,"suggestions":["午餐"]}

导出(注意是 POST;GET 打它返回 501):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/expenses/export-expenses

实测回显:

{"success":true,"jsonContents":{"expenses":[{"description":"午餐","budget":"餐饮","date":"2026-10-30","is_recurring":false,"cost":35.5,"extra":{}}],"budgets":[{"name":"餐饮","month":"2026-10","value":800,"extra":{}}]}}

导出为 JSON(expenses + budgets 两组数组),配合同域的 import-expenses 可完成实例间迁移。CSV 形态的导入导出在网页界面里完成。

5.4 calendar 域:日历与事件

calendar 域的写操作实际落点在 Radicale(事件以 ICS 文件存储)。日历 ID 可从 /calendar 页面数据中获得,实测环境默认日历为 03f9ab87-3c19-49a0-95ed-cd31b40b5795。

新建日历({name}):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/calendar/add \
  -H 'Content-Type: application/json' -d '{"name":"家庭"}'

实测回显(输出节选):

{"success":true,"newCalendars":[{"description":"","timezone":"","url":"http://radicale:5232/9d5ebad4-f6a7-4588-884e-f8c8c8ede632/82b804ca-fec4-434f-b528-6bd9d33c0862/","ctag":"\"c12f179192d17dee9485f544d09cf75c31aecc38adeb3c5783304a3ef42f31fb\"","calendarColor":"#BC133D","displayName":"家庭",…

响应里的 url 直指 Radicale 内部集合地址——印证日历数据真实落在 Radicale 上。

新建事件(calendarIds 是"当前视图显示哪些日历"、calendarId 是事件归属日历,两者都是数组/单值并用):

CAL=03f9ab87-3c19-49a0-95ed-cd31b40b5795
curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/calendar/add-event \
  -H 'Content-Type: application/json' \
  -d "{\"calendarIds\":[\"$CAL\"],\"calendarView\":\"month\",\"calendarStartDate\":\"2026-10-01\",\"calendarId\":\"$CAL\",\"title\":\"API 实测事件\",\"startDate\":\"2026-10-30 15:00\",\"endDate\":\"2026-10-30 16:00\",\"isAllDay\":false}"

实测回显(输出节选):

{"success":true,"newCalendarEvents":[{"url":"http://radicale:5232/9d5ebad4-f6a7-4588-884e-f8c8c8ede632/03f9ab87-3c19-49a0-95ed-cd31b40b5795/d339694b-964f-4fbb-8950-f41affe429b7.ics","etag":"\"ed7f6f6d69ade5e0e4c8783fa7744303b6bc751591dee784e1fd443edf523e1c\"","data":"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN\r\nBEGIN:VEVENT\r\nUID:d33969

搜索事件({calendarIds, searchTerm}):

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/calendar/search-events \
  -H 'Content-Type: application/json' \
  -d '{"calendarIds":["03f9ab87-3c19-49a0-95ed-cd31b40b5795"],"searchTerm":"API"}'

实测回显(输出节选):

{"success":true,"matchingCalendarEvents":[{"url":"http://radicale:5232/9d5ebad4-f6a7-4588-884e-f8c8c8ede632/03f9ab87-3c19-49a0-95ed-cd31b40b5795/d339694b-964f-4fbb-8950-f41affe429b7.ics",…,"data":"BEGIN:VCALENDAR\r\n…(节选)\r\nSUMMARY:API 实测事件\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR","uid":"d339694b-964f-4fbb-8950-f41affe429b7","startDate":"2026-10-30T15:00:00.000Z","endDate":"2026-10-30T16:00:00.000Z","title":"API 实测事件","transparency":"opaque","calendarId":"03f9ab87-3c19-49a0-95ed-cd31b40b5795"}]}

搜索命中了刚建的事件,响应携带完整 ICS 原文与起止时间。export-events 同域可用,导出格式即 ICS。

5.5 contacts 域:通讯录与地址簿

通讯录数据同样落在 Radicale,接口按「地址簿 → 条目」两级组织。实测地址簿列表与新建:

curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/contacts/get-addressbooks \
  -H 'Content-Type: application/json' -d '{}'
curl -s -b ~/bewcloud-cookie.txt -X POST http://<服务器IP>:8155/api/contacts/add-addressbook \
  -H 'Content-Type: application/json' -d '{"name":"同事"}'

实测回显(输出节选):

{"success":true,"addressBooks":[]}
{"success":true,"addressBooks":[{"url":"http://radicale:5232/9d5ebad4-f6a7-4588-884e-f8c8c8ede632/4b876b58-ff33-4458-a37c-ffb88bb5f139/","ctag":"\"c3820dfd7f46fadc3e804f138fb5a6c6cea2dfc53ac3d52a806160dcba5f831d\"","displayName":"%E5%90%8C%E4%BA%8B","resourcetype":["addressbook","collection"],"syncToken":"http://radicale.org/ns/sync/e3b0c442…

新实例地址簿为空,新建后返回的 displayName 是 URL 编码后的名称(%E5%90%8C%E4%BA%8B 即「同事」)。条目级增删(add/delete/import)同域可用,vCard 数据结构与日历域同构。

5.6 auth 域:MFA 与 Passkey

auth 域端点在 enableMultiFactor=false(默认)时的行为——返回 403 与明确的 JSON 错误说明(这是标准错误形态的最佳样本):

curl -s -w '\nHTTP %{http_code}\n' -b ~/bewcloud-cookie.txt -X POST \
  http://<服务器IP>:8155/api/auth/multi-factor/totp/setup -H 'Content-Type: application/json' -d '{}'
curl -s -w '\nHTTP %{http_code}\n' -b ~/bewcloud-cookie.txt -X POST \
  http://<服务器IP>:8155/api/auth/multi-factor/passkey/begin -H 'Content-Type: application/json' -d '{}'

实测回显:

{"success":false,"error":"Multi-factor authentication is not enabled on this server"}
HTTP 403
{"success":false,"error":"Passwordless passkey login requires multi-factor authentication to be enabled."}
HTTP 403

把配置 auth.enableMultiFactor 改为 true 并重启应用后(配置方法见配置篇 5.1,.env 需补 MFA_KEY/MFA_SALT),这组端点即按 TOTP/passkey 注册流程工作。

5.7 WebDAV 文件端点(/dav/)

/dav/ 是文件域的另一扇门:桌面 WebDAV 客户端挂载与脚本批量传文件都走这里,路径与 /api/files 操作的是同一份 data-files 存储(5.1 上传的 hello.txt 与本节 PUT 的文件在 PROPFIND 结果中同现)。

探测能力(OPTIONS):

curl -s -b ~/bewcloud-cookie.txt -X OPTIONS -D - -o /dev/null http://<服务器IP>:8155/dav/ | grep -iE 'HTTP/|allow|dav'

实测回显:

HTTP/1.1 200 OK
allow: OPTIONS, GET, HEAD, PUT, DELETE, COPY, MOVE, MKCOL, PROPFIND, LOCK, UNLOCK
dav: 1, 2
ms-author-via: DAV

Class 1 + Class 2(dav: 1, 2)意味着完整 WebDAV 能力含锁——主流桌面客户端(Windows 资源管理器、macOS Finder、Linux 的 davfs2/GVFS)均可挂载。

上传与下载(PUT/GET):

printf 'from webdav\n' > /tmp/dav-note.txt
curl -s -b ~/bewcloud-cookie.txt -X PUT --data-binary @/tmp/dav-note.txt \
  -o /dev/null -w '%{http_code}\n' http://<服务器IP>:8155/dav/docs/dav-note.txt
curl -s -b ~/bewcloud-cookie.txt http://<服务器IP>:8155/dav/docs/dav-note.txt

实测回显:

201
from webdav

PUT 到 /dav/docs/(5.1 建的目录)返回 201 Created,GET 回读内容一致。

列目录(PROPFIND):

curl -s -b ~/bewcloud-cookie.txt -X PROPFIND -H 'Depth: 1' \
  http://<服务器IP>:8155/dav/ -o /tmp/propfind.xml -w '%{http_code}\n'
grep -oE '<D:href>[^<]*</D:href>' /tmp/propfind.xml | head -5

实测回显:

207
<D:href>/dav/</D:href>
<D:href>/dav/docs/</D:href>
<D:href>/dav/hello.txt</D:href>
<D:href>/dav/.chunk-uploads/</D:href>
<D:href>/dav/.Trash/</D:href>

207 Multi-Status 是 WebDAV 列目录的标准响应;Depth: 1 返回根目录及其直接子项,与 5.1 API 视角看到的目录树一致。

5.8 CalDAV/CardDAV 协议端点

日历与通讯录的协议端点由应用反代到 Radicale(架构见介绍篇第三节),认证由应用完成。well-known 发现端点(DAV 客户端配置时只需给域名,客户端靠它自动发现协议路径):

curl -s -o /dev/null -D - http://<服务器IP>:8155/.well-known/caldav | grep -iE 'HTTP/|location'
curl -s -o /dev/null -D - http://<服务器IP>:8155/.well-known/carddav | grep -iE 'HTTP/|location'

实测回显:

HTTP/1.1 301 Moved Permanently
Location: /caldav

HTTP/1.1 301 Moved Permanently
Location: /carddav

CalDAV PROPFIND(先带 Cookie,再用 Basic 认证验证手机客户端形态):

curl -s -b ~/bewcloud-cookie.txt -X PROPFIND -H 'Depth: 0' \
  http://<服务器IP>:8155/caldav/ -o /tmp/caldav.xml -w '%{http_code}\n'
head -c 260 /tmp/caldav.xml
curl -s -u 'admin@example.com:q1w2e3r4' -X PROPFIND -H 'Depth: 0' \
  http://<服务器IP>:8155/caldav/ -o /dev/null -w '%{http_code}\n'

实测回显:

207
<?xml version='1.0' encoding='utf-8'?>
<multistatus xmlns="DAV:" xmlns:RADICALE="http://radicale.org/ns/"><response><href>/caldav/</href><propstat><prop><principal-collection-set><href>/caldav/</href></principal-collection-set><RADICALE:version>3.7.6</RADICALE…
207

CardDAV PROPFIND:

curl -s -b ~/bewcloud-cookie.txt -X PROPFIND -H 'Depth: 0' \
  http://<服务器IP>:8155/carddav/ -o /tmp/carddav.xml -w '%{http_code}\n'
head -c 200 /tmp/carddav.xml

实测回显:

207
<?xml version='1.0' encoding='utf-8'?>
<multistatus xmlns="DAV:" xmlns:RADICALE="http://radicale.org/ns/"><response><href>/carddav/</href><propstat><prop><principal-collection-set><href>/carddav/</hre…

两个协议端点的 PROPFIND 都返回 207,响应 XML 里的 RADICALE:version 3.7.6 印证了后端版本;Cookie 与 Basic 认证实测均通过——手机日历/通讯录 App(iPhone「日历/通讯录」账户、Davx⁵ 等)用 http://<服务器IP>:8155 加邮箱口令即可直连。5.4/5.5 经 API 建的日历与地址簿,正是从这些协议端点可同步到的集合。

5.9 错误码形态速查

本篇实测触达的全部错误形态汇总(脚本侧可直接按此做分支判断):

场景实测响应判断依据
未认证访问页面或 API302 → /login?redirectTo=…Location 头(4.4)
会话 Cookie 失效同上,302 回登录页同上
访问不存在的路径404(HTML 404 页,<title>404 - Page not found - bewCloud</title>)状态码
口令错误的登录200(重渲染登录页并显示错误)响应体含错误提示
JSON API 用错 HTTP 方法(如 GET 打 POST-only 端点)501 Not Implemented(text/plain)状态码
请求体参数不合法(如 auto-complete 的 name 少于 2 字符)400 Bad Request(text/plain)状态码
业务性拒绝(如 MFA 未启用时调 MFA 端点)403 + JSON {"success":false,"error":"…"}JSON error 字段
上传缺 upload_session_tag 或标签不匹配403 Forbidden状态码
分享链接被撤销后访问404状态码

形态规律:页面同构错误走重定向/HTML,JSON API 的业务错误走 {"success":false,"error":…} JSON;501/400 这类框架级错误则是裸文本。错误信息字面量(如上表引号内文本)均为实测原文。

六、分页/过滤/排序通用约定

v4.21.0 的 JSON API 没有统一的分页机制:本篇实测的所有列表响应(newDirectories/directories/newExpenses/matchingCalendarEvents/addressBooks)都是一次性全量返回,无页码、游标或 limit/offset 参数——这是单用户个人云数据规模下的设计取舍,数据量大时靠应用侧过滤。

过滤与排序按域各自约定,实测依据如下:

  • 目录过滤:get-directories 支持 directoryPathToExclude 参数(排除指定目录,源码 pages/api/files/get-directories.ts RequestBody 实查),移动/复制对话框用它排除自身;
  • 文件排序:文件页的排序状态经 /api/files/update-sort 持久化,请求体 {sortBy, sortOrder}(实例前端 JS 实查),下次登录保持;
  • 文件搜索:/api/files/search 按名称过滤文件与目录;
  • 事件过滤:search-events 的 searchTerm 按事件标题匹配(5.4 实测命中);
  • 描述补全:auto-complete 按历史描述前缀联想,输入至少 2 字符(不足实测返回 400)。

七、常见问题

Q1:为什么没有 API Token?脚本每次都要先登录吗? 是。bewCloud 的 API 面为自家前端服务,会话即凭证。脚本的标准姿势是 4.2 的一步登录(Cookie 有效期一年,4.2 实测),之后重复使用 Cookie 文件即可;JWT_SECRET 轮换会使所有会话失效(配置篇 5.8),脚本需具备重新登录能力。

Q2:/api/files/upload 明明带了 Cookie 还是 403? 缺 upload_session_tag 或算错。该标签 = SHA-256("<session_id>:<PASSWORD_SALT>"),session_id 从登录 Cookie 的 JWT 载荷解码,PASSWORD_SALT 是 .env 里的值(5.1 实测计算与上传全过程)。标签随会话变化,重登后要重算。只做文件搬运的脚本建议直接走 /dav/ PUT——没有标签要求,且能力完整(5.7)。

Q3:明明路径正确,为什么返回 501? 方法用错了。JSON API 几乎全是 POST,唯 download-directory 等极少数是 GET;用 GET 打 POST-only 端点实测返回 501 Not Implemented(5.3/5.9)。先对照 3.1 清单确认方法。

Q4:DAV 客户端(手机日历/通讯录、桌面网盘挂载)分别怎么配? 三类入口:手机日历/通讯录用 CalDAV/CardDAV——服务器填 http://<服务器IP>:8155,用户名邮箱、口令即登录口令(Basic 认证实测通过,5.8),客户端会自动走 /.well-known 发现;桌面文件挂载用 WebDAV 地址 http://<服务器IP>:8155/dav;纯脚本用 5.1 的 files API。HTTP 明文场景下这些凭据会明文传输,公网部署务必套 HTTPS 反代。

Q5:API 建的日历/地址簿,换一个 CalDAV 客户端能看到吗? 能看到。API 写操作落在 Radicale 集合(5.4/5.5 响应里的 url 字段即 Radicale 地址),任何 CalDAV/CardDAV 客户端经 5.8 的协议端点连入后看到的是同一份数据——「API 面」与「协议面」只是同一存储的两个入口。

Q6:脚本怎么判断会话失效? 任何请求返回 302 且 Location 指向 /login?redirectTo=… 即会话失效(4.4 实测形态),重新执行登录步骤刷新 Cookie 文件即可。

八、参考链接