Reactive Resume 6.0.8 API 使用详解

Reactive Resume v6.0.8 REST API 实测手册:三套认证方式、简历/求职信/API Key 等核心域的请求与真实响应、错误码与限流行为,curl 逐条照抄执行。

Reactive ResumeReactive ResumeDocker简历PostgreSQL2026/10/30

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

二、前置条件

  1. 一个可用的 v6.0.8 实例(健康端点返回 healthy);
  2. curl 或任意 HTTP 客户端;
  3. 一组注册凭据(下文 4.1 现场注册演示账号);
  4. 明确两个前缀的分工:认证走 /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"]}
bearerAuthAuthorization: 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"}
codestatus触发场景(实测)
UNAUTHORIZED401不带凭据、API Key 错误
NOT_FOUND404资源 ID 不存在
BAD_REQUEST400请求体校验失败(data.issues 数组逐字段给出 zod 问题)或 JSON Patch 路径不可解析(code: OPERATION_PATH_UNRESOLVABLE)
FAILED_TO_CREATE_USER422注册缺 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 三套认证通道的边界(实测)

调用CookieX-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 星标数的缓存值)。

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

以简历列表为代表的集合接口约定一致:

  1. 分页参数:limit(1–100)与 offset(从 0 起);两个都不传 = 返回全量(兼容旧用法);只传 offset 时 limit 默认 20。
  2. 响应形态不统一:简历列表返回裸数组,求职信列表返回 {"items":[...]} 包装——对接脚本时按各自域实测确认(本文均给出实测回显)。
  3. 列表瘦身:集合接口不返回 data(简历正文/求职信正文),单份内容一律按 ID 查询,避免大对象拖垮列表。
  4. 过滤与排序:简历列表支持 tags 按标签过滤,排序按最近更新/创建时间/名称(规范定义);本域接口均为账号内隔离,无需传用户参数。
  5. 字段写法: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,不要等自然过期。

八、参考链接