首页
/ Resume Matcher 后端 API 契约全解析:端点、数据格式与本地优先设计

Resume Matcher 后端 API 契约全解析:端点、数据格式与本地优先设计

2026-09-10 23:58:11作者:霍妲思

本文基于 docs/agent/apis/backend-requirements.md 展开,并结合 apps/backend/app 下的路由实现(routers)、Pydantic 数据模型(schemas)、配置与加密模块(config、crypto)逐一印证。Resume Matcher 是一个本地优先(local-first)的 AI 简历匹配应用:上传简历、解析为结构化数据、按职位描述(JD)进行 AI 定制化改写(Tailoring),并可选生成求职信(Cover Letter)、外联消息(Outreach Message)与面试准备(Interview Prep)。读者读完本文后,将掌握该后端完整的 HTTP API 契约——包括每个端点的请求/响应格式、状态码语义、错误约定,以及各端点背后的真实调用链,可直接用于前端对接、二次开发或自动化测试。

一、API 概览:基础 URL 与认证模型

Resume Matcher 后端是一个基于 FastAPI 的异步应用,入口为 apps/backend/app/main.py,所有业务路由都以 /api/v1 为前缀挂载(见 main.py)。

基础 URL

http://localhost:8000/api/v1

默认情况下后端监听 0.0.0.0:8000(由 apps/backend/app/config.pySettings.host/Settings.port 决定,默认值分别为 0.0.0.08000)。

认证

当前接口不包含任何认证机制。这是本地优先设计的核心取舍:Resume Matcher 面向单用户本地部署,数据与配置都存储在本地文件系统中(SQLite 数据库位于 data/resume_matcher.db,配置位于 data/config.json),因此 API 层没有 token、session 或 OAuth。从源码注释可以确认,部分破坏性操作(如清空 API Key、重置数据库)通过请求体/查询参数中的"确认令牌"来防止误操作,而不是真正的鉴权(见 apps/backend/app/routers/config.pyconfirm=CLEAR_ALL_KEYSconfirm=RESET_ALL_DATA)。这意味着不要把该 API 直接暴露到公网,多用户/生产场景需要在前面加一层网关或代理认证。

二、Health 与 Status:存活探针与降级状态报告

GET /health           → {healthy, provider, model, error?}
GET /status           → {status, llm_configured, llm_healthy, database_stats}

2.1 GET /health —— 纯存活探针

GET /health 是给 Docker HEALTHCHECK 或负载均衡器使用的轻量级存活探针。实现位于 apps/backend/app/routers/health.py,它不会调用 LLM,固定返回 {status: "healthy"}。文档中的 {healthy, provider, model, error?} 字段对应更早期的实现或设想;当前仓库中该端点仅返回 status 字段,健康检查的真实细节在 /status 中。

2.2 GET /status —— 系统状态总览

GET /status 返回应用整体状态,关键在于每个子系统检查相互隔离:LLM 健康探测失败或数据库统计查询失败,只会降级对应字段,而不会让整个端点返回 500(见 health.py)。这样状态页仍能展示"部分可用/降级"而非直接报错。

实际返回结构为:

{
  "status": "ready | setup_required",
  "llm_configured": true,
  "llm_healthy": true,
  "has_master_resume": true,
  "database_stats": {
    "total_resumes": 3,
    "total_jobs": 5,
    "total_improvements": 2,
    "has_master_resume": true
  }
}

字段语义(与 health.py 一致):

字段 含义
status ready 当且仅当 LLM 健康存在 master resume;否则为 setup_required
llm_configured 已配置 API Key,或 provider 属于 ollama/openai_compatible(这两个本地 provider 无需 Key)
llm_healthy 实际调用 check_llm_health() 的结果;失败只降级本字段
database_stats db.get_stats() 的汇总;统计查询失败时返回空统计(total_resumes: 0 等)而非报错

值得注意的细节:llm_configured 的判断与 check_llm_health 保持一致——Ollama 和 OpenAI 兼容端点可以在不配置 API Key 的情况下运行,这是本地模型优先设计的一个体现。

三、Configuration 配置端点:LLM、功能开关与 API Key

GET /config/llm-api-key    → {provider, model, api_key(masked), api_base?}
PUT /config/llm-api-key    ← {provider, model, api_key, api_base?}
POST /config/llm-test      → {healthy, provider, model}
GET /config/features       → {enable_cover_letter, enable_outreach_message, enable_interview_prep}
PUT /config/features       ← {enable_cover_letter?, enable_outreach_message?, enable_interview_prep?}

配置路由全部位于 apps/backend/app/routers/config.py

3.1 LLM 配置:GET/PUT /config/llm-api-key

  • GET 返回当前生效的 provider、model、api_base 以及被掩码的 API Key(_mask_api_key 保留前 4 位与后 4 位,其余以 * 填充,见 config.py),此外还包含 reasoning_effort 字段。
  • PUT 采用"只更新传入字段"的合并策略,providermodelreasoning_effort 非空即覆盖;api_base 需要区分"未传"(保持不变)与"传了空值"(显式清除),以避免历史遗留的 stale 覆盖问题。
  • 一个关键设计:PUT 不再持久化 API Keyrequest.api_key 在 schema 中保留仅为兼容与掩码展示,密钥实际通过 PUT /config/api-keys 写入加密存储(Fernet 加密后存入 SQLite 的 api_keys 表)。这样做的直接原因是旧的单 Key 槽位会导致多 provider 互相覆盖、并遮蔽按 provider 管理的密钥表。配置保存失败不会硬失败:PUT /config/llm-api-key 会先保存配置,再通过 BackgroundTasks 做一次"尽力而为"的健康检查并写日志,不阻塞响应(见 config.py)。连通性验证请走 /config/llm-test 或状态页。

3.2 连接测试:POST /config/llm-test

可选传入 LLMConfigRequest(用于保存前预测试);不传则使用当前已保存配置。实现用固定的 test_prompt = "Hi" 调用 check_llm_health(config, include_details=True),返回 {healthy, provider, model} 及更多细节。这是对接新 provider / 代理聚合器时最直接的排障入口。

3.3 功能开关:GET/PUT /config/features

三个布尔开关控制 AI 附加内容的生成:enable_cover_letterenable_outreach_messageenable_interview_prep,默认均为 False(见 config.py)。在 PUT 时同样只更新传入字段。这些开关在简历改进流程(/resumes/improve/confirm)中会被读取:只有对应开关打开时,才会触发求职信 / 外联 / 面试准备的 LLM 生成(见 apps/backend/app/routers/resumes.py)。

3.4 API Key 管理:按 provider 的加密密钥库

文档提到的配置端点之外,仓库当前还提供了完整的按 provider 密钥管理(config.py):

GET    /config/api-keys            → {providers: [{provider, configured, masked_key}]}
POST   /config/api-keys            ← {provider: key, ...}   # Fernet 加密后 upsert
DELETE /config/api-keys/{provider}                           # 删除单个 provider 的密钥
DELETE /config/api-keys?confirm=CLEAR_ALL_KEYS               # 清空全部密钥

支持 8 个 provider:openaianthropicgoogleopenrouterdeepseekgroqopenai_compatibleollama(见 config.py)。底层加密实现在 apps/backend/app/crypto.py:使用 Fernet 对称加密,密钥自动生成并保存在 data/.secret_key(0600 权限、gitignored),明文只在校验时存在于内存。解密失败的密钥会被当作"未配置"处理而不是崩溃,用户重输即可。应用启动时还会执行幂等的 migrate_legacy_keys(),把旧的明文 Key 迁入加密库后从 config.json 中剥离(见 apps/backend/app/main.py)。

3.5 其他配置端点(仓库当前实现)

  • GET/PUT /config/language:读写 ui_languagecontent_language,支持 eneszhjaptfr 六种语言,非法值返回 400。
  • GET/PUT /config/prompts:查看/设置 default_prompt_id,非法 ID 返回 400。
  • GET/PUT /config/feature-prompts:自定义求职信/外联提示词;非空提示词必须包含 {job_description}{resume_data}{output_language} 三个占位符,缺失时返回结构化 422。
  • POST /config/reset:清空数据库与上传文件,需请求体 confirm=RESET_ALL_DATA

四、Resumes 简历端点:上传、读取、改进与 PDF

POST /resumes/upload       ← multipart/form-data {file}  → {resume_id}
GET /resumes?resume_id=    → Resume object
GET /resumes/list          → [{resume_id, filename, is_master, created_at}]
PATCH /resumes/{id}        ← ResumeData
DELETE /resumes/{id}       → {message}
GET /resumes/{id}/pdf      → application/pdf
POST /resumes/improve      ← {resume_id, job_id} → {data, cover_letter?, outreach_message?, interview_prep?}
POST /resumes/{id}/generate-interview-prep → {interview_prep, message}

路由定义于 apps/backend/app/routers/resumes.py,以下按功能拆解。

4.1 上传与解析:POST /resumes/upload

接收 multipart/form-data 中的 file 字段,完整校验流程见 resumes.py

  1. 类型校验:仅允许 application/pdfapplication/msword(.doc)、application/vnd.openxmlformats-officedocument.wordprocessingml.document(.docx),否则 400。
  2. 大小校验:超过 MAX_FILE_SIZE = 4MB 返回 413
  3. 空文件校验:空内容返回 400。
  4. 文档解析:调用 parse_document() 将 PDF/DOCX 转为 Markdown,解析失败返回 422;提取不到文本(如图片型/扫描 PDF)同样返回 422。
  5. 落库:先以 processing_status="processing" 原子创建记录(create_resume_atomic_master 保证 master 分配的原子性),原始 Markdown 作为 original_markdown 永久保留。
  6. LLM 结构化解析:调用 parse_resume_to_json() 把 Markdown 转为结构化 JSON(processed_data);成功则状态置为 ready,失败则置为 failed该步骤可选——LLM 未配置时上传依然成功,只是解析状态为 failed

响应体(ResumeUploadResponse)包含 messagerequest_idresume_idprocessing_statusis_master,其中 processing_status 如实反映解析结果(readyfailed),避免客户端误判。对于解析失败或卡在 processing 的记录,还可调用 POST /resumes/{id}/retry-processing 重试(仅允许 failed/processing 状态,见 resumes.py)。

4.2 读取与列表:GET /resumesGET /resumes/list

  • GET /resumes?resume_id=xxx 返回单个简历,包含原始 Markdown(raw_resume)、结构化数据(processed_resume,经 ResumeData 校验)、求职信、外联消息、面试准备(JSON 字符串反序列化为 InterviewPrepData)、parent_id(指向 master 简历)与 title。查询参数缺失或记录不存在均返回 404。响应还会做惰性迁移:旧简历的 processed_data 通过 normalize_resume_data() 补齐 section 元数据。
  • GET /resumes/list?include_master=false 返回简历摘要列表,按 updated_at 降序排列。include_master 默认为 false(默认不返回 master 简历),每个摘要含 resume_idfilenameis_masterparent_idprocessing_statuscreated_atupdated_attitle

4.3 改进(Tailoring):POST /resumes/improve 与 preview/confirm 流程

文档列出的 POST /resumes/improve 在当前仓库中是遗留端点,主流程已演进为两步式的 preview(预览)/ confirm(确认) 模式:

  • POST /resumes/improve/preview:不落库地生成定制化简历。请求体包含 resume_idjob_id、可选的 prompt_id。内部调用链为:extract_job_keywords()(JD 关键词提取,带内容哈希缓存)→ generate_skill_target_plan() + verify_skill_target_plan()(技能目标规划与校验,拒绝不支持的技能目标)→ generate_resume_diffs() + apply_diffs()基于 diff 的定向修改,而非全量重写)→ verify_diff_result()(差异验证)→ 多层安全网(_preserve_personal_info 保留个人信息、_restore_original_dates 恢复被 LLM 截断的日期、_preserve_original_skills 补回被丢的技能、_protect_custom_sections 抵御自定义 section 幻觉)→ refine_resume() 多轮精修(关键词注入、AI 味短语清除、对齐校验)。整个流程被 asyncio.wait_for(..., timeout=settings.request_timeout_seconds) 包裹,默认 240 秒、可在 30, 1800] 秒内调节([apps/backend/app/config.py),超时返回 504 并给出调优建议(本地 LLM 需调大 REQUEST_TIMEOUT_SECONDS 与前端 NEXT_PUBLIC_REQUEST_TIMEOUT_MS)。预览响应中 resume_idnull,同时携带 markdownOriginalmarkdownImproveddiff_summarydetailed_changesrefinement_statsats_score(ATS 评分与缺失关键词)、improvements(建议列表)与 warnings
  • POST /resumes/improve/confirm:将预览结果落库。请求体携带 improved_data(结构化简历)、improvements 等。校验层包括:personalInfo 不允许被改动(_validate_confirm_payload)、preview 哈希校验_hash_improved_data 先通过 ResumeData 规范化再计算 SHA-256,防止 schema 不完整的旧数据导致误拒;哈希不在允许集合内返回 400)。确认成功后:创建定制化简历记录(content_type="json"parent_id 指向原简历)、写入 improvement 记录、按功能开关并行生成求职信/外联/面试准备(asyncio.gather,单项失败仅记 warning),并自动创建 tracker 卡片(best-effort,_auto_create_tracker_application 复用已缓存的 company/role,不额外调用 LLM;tracker 失败绝不影响定制化主流程)。
  • POST /resumes/improve(遗留):一次调用完成"生成+落库",逻辑与 confirm 类似,但不做 preview 哈希校验,直接持久化并返回非空 resume_id

无论哪种路径,未配置结构化数据(processed_data)的简历会回退到 improve_resume() 全量输出模式,保证兼容性。

4.4 更新、删除与 PDF 生成

  • PATCH /resumes/{id}:请求体为 ResumeData(即文档中的结构化 ResumeData),服务端将其序列化为 JSON 覆盖 contentprocessed_data,并把 processing_status 置为 ready。返回与 GET /resumes 相同的结构。
  • DELETE /resumes/{id}:删除记录,不存在返回 404,成功返回 {message: "Resume deleted successfully"}
  • GET /resumes/{id}/pdf:基于 Playwright 无头浏览器渲染 PDF。实现流程(见 resumes.py):拼接前端打印页 URL {frontend_base_url}/print/resumes/{resume_id}?{params}render_resume_pdf() 等待 .resume-print 选择器出现后打印为 PDF。该端点接受大量模板与排版参数(均有范围约束):templateswiss-singleswiss-two-columnmodernmodern-two-columnlatexcleanvivid)、pageSizeA4/LETTER)、页边距 marginTop/Bottom/Left/Right(5–25mm)、sectionSpacing/itemSpacing/lineHeight/fontSize/headerScale(1–5)、headerFont/bodyFontserif/sans-serif/mono)、compactModeshowContactIconsaccentColorblue/green/orange/red)、langxxxx-XX)。渲染失败返回 503。底层渲染器见 apps/backend/app/pdf.py,支持跨平台自动发现 Chrome/Chromium/Edge 可执行文件。
  • GET /resumes/{id}/cover-letter/pdf:类似地渲染求职信 PDF,等待 .cover-letter-print 选择器;该简历必须已存在求职信,否则 404。

4.5 按需生成:求职信、外联、面试准备

针对已经定制化过的简历(必须存在 parent_id),提供三个按需生成端点,全部要求能从 improvements 表找到对应 JD:

  • POST /resumes/{id}/generate-cover-letter:按需生成求职信并保存,返回 {content, message}
  • POST /resumes/{id}/generate-outreach:按需生成外联消息,返回 {content, message}
  • POST /resumes/{id}/generate-interview-prep:按需生成面试准备。要求简历必须有 parent_idprocessed_data;生成的 InterviewPrepData 经 Pydantic 校验后序列化为 JSON 文本存入 resumes.interview_prep 列(_serialize_interview_prep),响应返回 {interview_prep, message}。读取时再经 _parse_interview_prep 反序列化并容错(非法 JSON 返回 null 而非报错)。
  • GET /resumes/{id}/job-description:返回用于定制化该简历的原始 JD({job_id, content}),仅对定制化简历可用。

五、Jobs 职位描述端点

POST /jobs/upload          ← {job_descriptions: [], resume_id?} → {job_id: []}
GET /jobs/{id}             → {job_id, content, created_at}

实现在 apps/backend/app/routers/jobs.py

  • POST /jobs/upload:请求体为 {job_descriptions: [...], resume_id?}。空数组或空字符串返回 400;逐条调用 db.create_job(),响应 {message, job_id: [...]}(数组与输入一一对应)。
  • GET /jobs/{id}:返回该 JD 的完整记录。实际记录除了 job_idcontentcreated_at,还会包含由改进流程写入的 job_keywordsjob_keywords_hashcompanyrolepreview_hash 等缓存字段(关键词提取结果按内容哈希缓存,见 resumes.py)。

六、Request/Response 数据格式

6.1 Resume Object

文档给出的核心响应结构:

{
  "resume_id": "uuid",
  "content": "markdown or json",
  "processed_data": {
    "personalInfo": {},
    "summary": "",
    "workExperience": [],
    "education": [],
    "additional": {}
  },
  "is_master": true,
  "processing_status": "ready|processing|failed",
  "cover_letter": "text or null",
  "outreach_message": "text or null",
  "interview_prep": "structured object or null"
}

实际返回时,该对象被封装为 ResumeFetchResponsedata 下含 resume_idraw_resume(原始 Markdown + content_type + 时间戳 + 处理状态)、processed_resume(由 ResumeData 校验的结构化数据)、cover_letteroutreach_messageinterview_prepparent_idtitle,另附顶层 request_id

6.2 ResumeData 结构化模型

PATCH /resumes/{id} 接受与 processed_data 同构的结构化负载。仓库中的 Pydantic 定义位于 apps/backend/app/schemas/models.py,核心字段为:

{
  "personalInfo": {
    "name": "", "title": "", "email": "", "phone": "",
    "location": "", "website": null, "linkedin": null, "github": null
  },
  "summary": "",
  "workExperience": [{"id": 0, "title": "", "company": "", "location": null, "years": "", "description": []}],
  "education": [{"id": 0, "institution": "", "degree": "", "years": "", "description": null}],
  "personalProjects": [{"id": 0, "name": "", "role": "", "years": "", "github": null, "website": null, "description": []}],
  "additional": {
    "technicalSkills": [],
    "languages": [],
    "certificationsTraining": [],
    "awards": []
  },
  "customSections": {},
  "sectionMeta": []
}

几个值得注意的模型行为(均在 models.py 中实现):

  • 容错型字段规范化description 等字段支持从嵌套结构/带项目符号的多行文本自动规范为干净的字符串数组(_coerce_string_list 去掉 -*1. 等前缀)。
  • Section 类型枚举personalInfo(恒为第一且不可排序)、textitemListstringList
  • master 与定制化简历的关联:定制化简历通过 parent_id 指向其 master 简历,is_master 标记区分二者。

6.3 Error Response 与状态码

所有错误统一返回 FastAPI 风格的 JSON:

{
  "detail": "Error message"
}

状态码语义如下(与源码一一对应):

Code Meaning 触发场景(源码依据)
400 Bad request 文件类型/空文件非法、非法 provider、确认令牌错误、preview 哈希不匹配等
404 Not found resume/job/application 不存在、求职信缺失
413 File too large (>4MB) MAX_FILE_SIZE 超限(resumes.py
422 Parsing failed parse_document 失败、扫描版 PDF 无文本、自定义提示词缺占位符
500 Server error 改进/生成/更新等内部异常
503 PDF rendering failed PDFRenderError 抛出时(resumes.py

另外还有两个文档未列出的状态码:504(改进流程超时,settings.request_timeout_seconds 控制,见 resumes.py)与改进流程中 404/400 的前置校验。

七、Application Tracker 求职追踪端点(文档之外的补充)

虽然 backend-requirements.md 未涉及,但仓库当前版本已内置完整的 Kanban 求职追踪模块,实现在 apps/backend/app/routers/applications.py,与改进流程深度联动,这里一并给出契约要点:

GET    /applications          → {columns: {saved: [], applied: [], no_response: [], response: [], interview: [], accepted: [], rejected: []}}
POST   /applications          ← {resume_id, job_description, company?, role?, status?, notes?}
GET    /applications/{id}     → Application + job_content + resume(resume 可为 null)
PATCH  /applications/{id}     ← {status?, position?, notes?, company?, role?, applied_at?}
PATCH  /applications/bulk     ← {application_ids: [], status}
DELETE /applications/{id}
POST   /applications/bulk-delete ← {application_ids: []}

七个状态列由枚举 ApplicationStatus 定义(apps/backend/app/schemas/applications.py),响应中的 columns 七个 key 恒存在(未知状态的行会被跳过并记日志而非 500)。POST /applications 手动添加时:先创建 job,若未提供 company/role 则做一次 best-effort 的关键词提取(复用 extract_job_keywords,失败回退为空可编辑);若应用创建失败会清理刚创建的孤儿 job。自动创建逻辑贯穿改进流程:improve/confirm(及遗留 improve)在持久化定制化简历后自动落一张 applied 卡片,复用缓存的 company/role、零额外 LLM 调用,且失败不阻断主流程(resumes.py)。

八、测试验证与实战建议

仓库为上述 API 契约提供了充分的集成测试,可作为对接时的"活文档":

实战要点总结:

  1. 本地优先,无鉴权:任何端点都可直接调用,但切勿暴露公网;破坏性操作需确认令牌。
  2. 上传与解析分离POST /resumes/upload 返回的 processing_status 可能为 failed(LLM 未配置或解析失败),可配合 POST /resumes/{id}/retry-processing 重试。
  3. 定制化走 preview → confirm:先 improve/preview 拿到哈希与 diff,再 improve/confirm 落库;绕过 preview 直接 confirm 会因哈希缺失返回 400。
  4. 附加内容按开关生成:求职信/外联/面试准备默认关闭,需先 PUT /config/features 打开;也可事后用按需生成端点补生成。
  5. 超时按层配置:本地 LLM(Ollama 等)耗时较长,需同步调大后端 REQUEST_TIMEOUT_SECONDS(上限 1800)与前端 NEXT_PUBLIC_REQUEST_TIMEOUT_MS,否则按最短层 abort。
  6. PDF 依赖无头浏览器:确保环境能启动 Chrome/Chromium/Edge(apps/backend/app/pdf.py 会自动探测常见安装路径);渲染失败返回 503。

完整的端点清单与调用关系还可参考 docs/agent/apis/api-flow-maps.md,其中给出了简历上传、改进、PDF 生成、配置更新、API Key 加密存储、求职追踪等关键流程的分步调用链,与本文的契约说明互为补充。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
901
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
604
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23