Reactive Resume 6.0.8 API 使用详解
Reactive Resume v6.0.8 REST API 实测手册:三套认证方式、简历/求职信/API Key 等核心域的请求与真实响应、错误码与限流行为,curl 逐条照抄执行。
Reactive Resume 6.0.8 API 使用详解
目标:把 Reactive Resume v6.0.8 的 REST API 讲到可以照抄执行——三套认证方式如何拿到凭据、简历/求职信/API Key 等核心域的请求与真实响应、错误码与限流行为。本文全部请求均在 Ubuntu 22.04 + Reactive Resume 6.0.8(应用监听 8150 端口,部署见《Reactive Resume 6.0.8 部署指南》)实测实例上用 curl 逐条执行,响应 JSON 照抄(长响应标注节选),演示账号
admin/q1w2e3r4。
一、环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04(x86_64) |
| 服务端版本 | Reactive Resume 6.0.8(/api/health 返回 version: 6.0.8) |
| API 基地址 | http://localhost:8150(读者按自己的 APP_URL 替换) |
| API 前缀 | Better Auth 认证端点挂 /api/auth/*;业务 REST API 挂 /api/openapi/*;OpenAPI 规范文档在 /openapi.json |
| 客户端工具 | curl 8.18.0(实测 curl --version 首行:curl 8.18.0 (x86_64-pc-linux-gnu)) |
| 测试数据 | 账号 admin / q1w2e3r4,简历 Backend Engineer Resume(slug backend-engineer-resume) |
v6 的服务端在 3000 端口同时承载 Web 站点、认证与 REST API,没有独立的 API 端口。全部 161 个业务路径的结构化规范可从本机实例直接取得:
curl -s http://localhost:8150/openapi.json -o openapi.json
python3 -c "import json; d=json.load(open('openapi.json')); print(d['info']['title'], d['info']['version'], len(d['paths']), 'paths')"
Reactive Resume 6.0.8 161 paths
二、前置条件
- 一个可用的 v6.0.8 实例(健康端点返回
healthy); curl或任意 HTTP 客户端;- 一组注册凭据(下文 4.1 现场注册演示账号);
- 明确两个前缀的分工:认证走
/api/auth/*(Better Auth 协议),业务资源走/api/openapi/*(OpenAPI 规范里的全部路径都相对该前缀)——把业务路径直接拼到/api下会得到 404,这是 v6 API 最容易踩的第一个坑。
三、API 总览
协议与版本:HTTP + JSON;业务 API 为 REST 风格,规范版本与应用同版(6.0.8)。接口清单、参数与响应 schema 以实例自带的 /openapi.json 为准(161 个路径,tags 分组:Resumes、Applications、Cover Letters、Documents、Authentication、AI、Agent、Career 等)。
认证方式(规范 components.securitySchemes 定义三套,本文全部实测):
| 方式 | 载体 | 实测结论 |
|---|---|---|
cookieAuth | 会话 Cookie better-auth.session_token | 登录后自动下发,浏览器与 curl 均可用 |
apiKey | 请求头 X-API-Key: <key> | 面向脚本与集成的首选;Key 经 API 创建,全权限 Key 权限集为 {"api":["read","write","delete"]} |
bearerAuth | Authorization: Bearer <token> | 面向 API/MCP 客户端的 OAuth 访问令牌(实例作为 OAuth 资源服务器签发) |
限流约定(生产模式默认开启):认证端点限流严格——实测对 /api/auth/sign-in/email 连续提交错误密码,前 5 次返回 401,第 6 次起返回 429,响应体为 {"message":"Too many requests. Please try again later."};携带 API Key 的业务读接口实测 100 连发全部 200,限流阈值远高于常规脚本流量。内网压测等特殊场景可用 FLAG_DISABLE_API_RATE_LIMIT=true 显式关闭(变量说明见《Reactive Resume 6.0.8 配置参数详解》)。
错误结构:业务 API(oRPC 层)返回统一结构化错误;认证端点(Better Auth 层)返回 {"message","code"} 简结构。业务错误实测样例:
{"defined":false,"code":"UNAUTHORIZED","status":401,"message":"Unauthorized"}
| code | status | 触发场景(实测) |
|---|---|---|
UNAUTHORIZED | 401 | 不带凭据、API Key 错误 |
NOT_FOUND | 404 | 资源 ID 不存在 |
BAD_REQUEST | 400 | 请求体校验失败(data.issues 数组逐字段给出 zod 问题)或 JSON Patch 路径不可解析(code: OPERATION_PATH_UNRESOLVABLE) |
FAILED_TO_CREATE_USER | 422 | 注册缺 username 字段(见 4.1 排障实录) |
四、认证与首次调用
4.1 注册账号(实测排障:必须带 username)
POST /api/auth/sign-up/email,请求体为 JSON:
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:8150/api/auth/sign-up/email \
-H 'Content-Type: application/json' \
-d '{"name":"admin","username":"admin","email":"admin@example.com","password":"q1w2e3r4"}' \
-c cookies.txt
HTTP 200
响应 200 即注册成功并登录(会话 Cookie 写入 cookies.txt)。⚠️ 排障实录:请求体漏掉 username 时返回 422:
{"message":"Failed to create user","code":"FAILED_TO_CREATE_USER"}
服务端日志同步出现 PostgreSQL 非空约束违规 null value in column "username" of relation "user" violates not-null constraint——用户名是数据库必填列,网页注册表单含用户名输入框,API 调用必须显式携带。密码策略为至少 8 字符,q1w2e3r4 恰好满足。
4.2 登录与获取会话
已注册用户用 POST /api/auth/sign-in/email 登录(同样以 Cookie 承载会话);GET /api/auth/get-session 回读当前会话与用户信息:
curl -s -b cookies.txt http://localhost:8150/api/auth/get-session
{
"session": {
"expiresAt": "2026-11-05T14:10:02.862Z",
"token": "vDmNUqS7T08PT3jiPPjQlIg5GzPWxOg2",
"createdAt": "2026-10-29T14:10:02.862Z",
"updatedAt": "2026-10-29T14:10:02.862Z",
"ipAddress": "10.89.74.1",
"userAgent": "curl/8.18.0",
"userId": "01a187ff-11e7-722e-bc96-0689405693a6",
"id": "01a187ff-11ef-777f-b6cd-76a6abe9855f"
},
"user": {
"name": "admin",
"email": "admin@example.com",
"emailVerified": false,
"role": "user",
"twoFactorEnabled": false,
"username": "admin"
}
}
(输出节选:完整响应另含 image、createdAt、banReason 等字段。)/api/auth/ok 是免认证的存活探针,实测返回 {"ok":true}。
4.3 创建 API Key(脚本集成推荐)
API Key 用浏览器会话创建(规范注明「Requires a browser session; keys and OAuth tokens cannot create credentials」),POST /api/openapi/auth/api-keys:
curl -s -b cookies.txt -X POST http://localhost:8150/api/openapi/auth/api-keys \
-H 'Content-Type: application/json' \
-d '{"name":"docs-key","expiresIn":null,"access":"full"}'
{"key":"XxUMCAwCNrappJRsgYeWsQIOCtCtOYnSNIzdbKMdulzmPrfruxssOEbzxrkfzuaM"}
access 二选一:read(只读)或 full(全权);expiresIn 为过期秒数,null 表示永不过期。明文 Key 只在创建响应里出现一次,服务端只留前 6 位(start: "XxUMCA")用于列表展示。
4.4 用 API Key 首次调用
KEY="XxUMCAwCNrappJRsgYeWsQIOCtCtOYnSNIzdbKMdulzmPrfruxssOEbzxrkfzuaM"
curl -s -H "X-API-Key: $KEY" http://localhost:8150/api/openapi/resumes
[]
空数组表示当前账号还没有简历。同一个列表接口用 Cookie 会话调用结果一致——两种凭据可互换(见 4.5 节实测)。不带凭据调用返回 401:
{"defined":false,"code":"UNAUTHORIZED","status":401,"message":"Unauthorized"}
4.5 三套认证通道的边界(实测)
| 调用 | Cookie | X-API-Key |
|---|---|---|
GET /api/openapi/resumes(业务资源) | 200 [] | 200 [] |
GET /api/openapi/auth/api-keys(Key 列表) | 200(列表) | 200(列表,实测两者均可读) |
POST /api/openapi/auth/api-keys(创建 Key) | 200(创建成功) | 仅浏览器会话可创建(规范约束) |
POST /api/auth/sign-in/email(认证端点) | 免认证调用 | 不适用 |
Key 列表实测回显(注意 start 只露前 6 位,无法从列表恢复明文 Key):
{"apiKeys":[{"id":"01a187ff-505e-71be-bda0-8a533404f006","name":"docs-key","start":"XxUMCA","prefix":null,"enabled":true,"createdAt":"2026-10-29T14:10:18.845Z","updatedAt":"2026-10-29T14:12:52.835Z","expiresAt":null,"lastRequest":"2026-10-29T14:12:52.834Z","permissions":{"api":["read","write","delete"]}}],"total":1}
五、各 API 域详解
5.1 简历域(Resumes)
创建简历 POST /api/openapi/resumes,必填 name 与 tags(数组),可选 withSampleData(填充一套完整示例数据)、slug(省略时按名称生成并去重)、autoName:
curl -s -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-X POST http://localhost:8150/api/openapi/resumes \
-d '{"name":"Backend Engineer Resume","tags":["tech"],"withSampleData":true}'
"01a187ff-8791-75e3-b768-75fbd7651667"
注意:成功响应是纯 ID 字符串(JSON string),不是对象。
查询单份 GET /api/openapi/resumes/{id}:
RID="01a187ff-8791-75e3-b768-75fbd7651667"
curl -s -H "X-API-Key: $KEY" http://localhost:8150/api/openapi/resumes/$RID -o resume_get.json -w 'HTTP %{http_code}\n'
python3 -c "
import json; r=json.load(open('resume_get.json'))
print('top keys:', sorted(r.keys()))
print('data keys:', sorted(r['data'].keys()))
print('headline:', r['data']['basics']['headline'])
"
HTTP 200
top keys: ['applicationId', 'createdAt', 'data', 'hasPassword', 'hideContactDetails', 'id', 'isLocked', 'isPrimary', 'isPublic', 'name', 'showDownloadButtons', 'showInSearchEngines', 'slug', 'tags', 'updatedAt']
data keys: ['basics', 'customSections', 'metadata', 'picture', 'sections', 'summary']
headline: Game Developer | Unity & Unreal Engine Specialist
(示例数据的人物质感来自 withSampleData 预填。)顶层是元数据(isPublic/isPrimary/hasPassword 等),简历内容整体在 data 字段里:basics(姓名/头衔/联系方式)、summary、sections(经历/教育/技能/项目等标准节)、customSections、metadata(模板 azurill、分页布局、配色、字体等排版数据)。
列表(分页) GET /api/openapi/resumes?limit=20&offset=0,列表项不含 data(官方注明为性能考虑,单份内容走查询接口):
[
{
"id": "01a187ff-8791-75e3-b768-75fbd7651667",
"name": "Backend Engineer Resume",
"slug": "backend-engineer-resume",
"tags": ["tech"],
"isPublic": false,
"showDownloadButtons": true,
"showInSearchEngines": false,
"hideContactDetails": false,
"isPrimary": false,
"isLocked": false,
"createdAt": "2026-10-29T14:10:33.019Z",
"updatedAt": "2026-10-29T14:10:33.019Z"
}
]
局部更新(JSON Patch,RFC 6902) PATCH /api/openapi/resumes/{id}。这是 v6 API 最有个性的一处:请求体是 {"operations": [...]},每个操作含 op(add/remove/replace/move/copy/test)与 path,path 相对简历的 data 字段解析。实测把头衔改为 Senior Backend Engineer:
curl -s -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-X PATCH http://localhost:8150/api/openapi/resumes/$RID \
-d '{"operations":[{"op":"replace","path":"/basics/headline","value":"Senior Backend Engineer"}]}' \
-w '\nHTTP %{http_code}\n' -o /dev/null
HTTP 200
200,响应体是更新后的完整简历对象(data.basics.headline 已变,updatedAt 前进为 2026-10-29T14:11:11.913Z)。两个实测错误分支:
operations缺失 → 400,"message":"Input validation failed",data.issues指出Invalid input: expected array, received undefined;- 路径写成绝对形式
/data/basics/headline→ 400,"code":"OPERATION_PATH_UNRESOLVABLE","message":"Cannot perform the operation at a path that does not exist."——路径从data的下一级开始写(/basics/headline,而非/data/basics/headline)。
可选字段 expectedUpdatedAt(乐观锁)提供原 updatedAt 时,仅当简历未被并发修改才应用补丁。
元数据更新 PATCH /api/openapi/resumes/{id}/metadata,改发布状态等元数据字段(实测公开发布):
curl -s -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-X PATCH http://localhost:8150/api/openapi/resumes/$RID/metadata \
-d '{"isPublic":true}' -o /dev/null -w 'HTTP %{http_code}\n'
HTTP 200
slug 可用性检查 GET /api/openapi/resumes/{resumeId}/slug-availability?slug=<候选值>(注意查询参数名是 slug,实测传错参数名返回 400 且 data.issues 点名 path:["slug"]):
{"status":"current"} ← 传本简历当前 slug
{"status":"available"} ← 传一个未被占用的 slug
导出 PDF GET /api/openapi/resumes/{id}/pdf(服务端 Forme 引擎渲染):
curl -s -H "X-API-Key: $KEY" http://localhost:8150/api/openapi/resumes/$RID/pdf \
-o resume.pdf -w 'HTTP %{http_code} | %{content_type} | %{size_download} bytes\n'
file resume.pdf
HTTP 200 | application/pdf | 145915 bytes
resume.pdf: PDF document, version 1.7, 3 page(s)
导出 DOCX GET /api/openapi/resumes/{id}/exports/docx:
HTTP 200 | application/vnd.openxmlformats-officedocument.wordprocessingml.document | 14608 bytes
删除(软删除进回收站) DELETE /api/openapi/resumes/{id}:规范写明这是 Move a resume to Trash——公开链接立即失效,数据保留 30 天后随文件一起清除,期内可经 Documents 域的 documents.restore 恢复或 documents.purge 立即清除;锁定(isLocked)的简历需先解锁才能移动。实测:删除后列表不再出现该简历(GET /resumes 只剩 1 份),但按 ID 直查仍可读到(回收站语义)。
5.2 公开访问域(免认证)
已发布(isPublic: true)的简历可免凭据访问:GET /api/openapi/resumes/{username}/{slug}:
curl -s http://localhost:8150/api/openapi/resumes/admin/backend-engineer-resume | python3 -c "
import json,sys; r=json.load(sys.stdin)
print('name:', r.get('name'), '| slug:', r.get('slug'), '| keys:', sorted(r.keys()))
"
name: Resume | slug: backend-engineer-resume | keys: ['data', 'hideContactDetails', 'id', 'isLocked', 'isPrimary', 'isPublic', 'name', 'showDownloadButtons', 'showInSearchEngines', 'slug', 'tags']
响应不含 username 字段,name 显示为通用 Resume(公开面脱敏),简历正文在 data 中。同一份简历的访问统计(需凭据)随之更新——GET /api/openapi/resumes/{id}/statistics 实测:
{"isPublic":true,"views":1,"downloads":0,"lastViewedAt":"2026-10-29T14:11:39.870Z","lastDownloadedAt":null}
上面的 views: 1 正是前一步公开访问被计数的结果,构成一条完整的「发布 → 访问 → 统计」链路。
5.3 求职信域(Cover Letters)
创建 POST /api/openapi/cover-letters(必填 name),响应是完整对象(与简历域的纯 ID 字符串不同):
{"name":"Cover Letter for ACME","recipient":"","content":"","greeting":null,"signOff":null,"style":{"basics":{...},"metadata":{"template":"onyx","page":{"format":"a4",...}}},"layout":"structured","letterDate":"2026-10-29","id":"01a18800-d04a-71c0-b355-673872da2e31","revision":1,"createdAt":"2026-10-29T14:11:57.126Z","updatedAt":"2026-10-29T14:11:57.126Z"}
(输出节选:完整对象含整套默认排版样式。)列表 GET /api/openapi/cover-letters 为分页包装结构 {"items":[...]}(注意与简历列表的裸数组不同)。求职信同样支持 GET /cover-letters/{id}/export 与版本管理(/versions)。
5.4 API Key 管理域
| 操作 | 请求 | 实测要点 |
|---|---|---|
| 创建 | POST /api/openapi/auth/api-keys {"name","expiresIn","access"} | 明文 Key 只出现一次;access 取 read/full |
| 列表 | GET /api/openapi/auth/api-keys | 回显 start 前 6 位、lastRequest 最近使用时间、permissions |
| 删除 | DELETE /api/openapi/auth/api-keys/{keyId} | 按 Key 的 id 删除,即刻失效 |
lastRequest 字段实测会随每次使用更新(创建时为空,首次调用后变为 2026-10-29T14:12:52.834Z),可用于审计 Key 是否仍在被使用。
5.5 站点统计域
curl -s -H "X-API-Key: $KEY" http://localhost:8150/api/openapi/statistics
{"users":1,"resumes":1,"cachedAt":1793283116332}
GET /api/openapi/statistics/github/stars 免认证,实测返回 45051(实例服务端代理查询 GitHub 星标数的缓存值)。
六、分页/过滤/排序通用约定
以简历列表为代表的集合接口约定一致:
- 分页参数:
limit(1–100)与offset(从 0 起);两个都不传 = 返回全量(兼容旧用法);只传offset时limit默认 20。 - 响应形态不统一:简历列表返回裸数组,求职信列表返回
{"items":[...]}包装——对接脚本时按各自域实测确认(本文均给出实测回显)。 - 列表瘦身:集合接口不返回
data(简历正文/求职信正文),单份内容一律按 ID 查询,避免大对象拖垮列表。 - 过滤与排序:简历列表支持
tags按标签过滤,排序按最近更新/创建时间/名称(规范定义);本域接口均为账号内隔离,无需传用户参数。 - 字段写法:PATCH 类接口一律 JSON 语义(简历为 RFC 6902
operations数组、元数据为普通 JSON 对象),两种形态不要混用。
七、常见问题(FAQ)
Q1:请求业务路径返回 404?
检查前缀:业务资源挂 /api/openapi/*(如 /api/openapi/resumes),不是 /api/resumes,也不是 /api/v1/resumes(实测两者均为 404)。OpenAPI 规范文档则在站点根 /openapi.json。
Q2:注册返回 422 FAILED_TO_CREATE_USER?
请求体缺 username。该字段是数据库非空列(实测服务端日志出现 violates not-null constraint),补上 username 即可(4.1 节排障实录)。
Q3:PATCH 简历报 OPERATION_PATH_UNRESOLVABLE?
JSON Patch 的 path 相对 data 字段解析:写 /basics/headline,不要写 /data/basics/headline(实测对照见 5.1 节)。目标字段不存在时同样报此错误,先用查询接口确认字段路径。
Q4:创建的 API Key 第二次就找不回明文?
设计如此:明文只在创建响应出现一次,列表只回显前 6 位(start)。丢失就删除旧 Key 重建;建议在 name 里写清用途,配合 lastRequest 判断废弃。
Q5:登录接口连续失败后一直 429? 生产模式认证限流生效(实测错误 5 次后触发,见第三章)。等待限流窗口重置后重试;自动化脚本应改用 API Key 而不是反复走登录。
Q6:删除的简历还能恢复吗?
能,30 天内。DELETE 是进回收站(Trash),期间可调 Documents 域的 documents.restore 恢复、documents.purge 立即清除;到期自动连同文件删除。需要彻底删除时走 purge,不要等自然过期。
八、参考链接
- 实例自带 OpenAPI 规范:
http://<你的实例地址>/openapi.json(本篇路径与 schema 口径来源) - 官方仓库:https://github.com/reactive-resume/reactive-resume
- 相关篇目:《Reactive Resume 介绍与选型指南》 · 《Reactive Resume 6.0.8 部署指南》 · 《Reactive Resume 6.0.8 配置参数详解》