Resume Matcher 后端 API 契约全解析:端点、数据格式与本地优先设计
本文基于 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.py 中 Settings.host/Settings.port 决定,默认值分别为 0.0.0.0 与 8000)。
认证
当前接口不包含任何认证机制。这是本地优先设计的核心取舍:Resume Matcher 面向单用户本地部署,数据与配置都存储在本地文件系统中(SQLite 数据库位于 data/resume_matcher.db,配置位于 data/config.json),因此 API 层没有 token、session 或 OAuth。从源码注释可以确认,部分破坏性操作(如清空 API Key、重置数据库)通过请求体/查询参数中的"确认令牌"来防止误操作,而不是真正的鉴权(见 apps/backend/app/routers/config.py 的 confirm=CLEAR_ALL_KEYS 与 confirm=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 采用"只更新传入字段"的合并策略,
provider、model、reasoning_effort非空即覆盖;api_base需要区分"未传"(保持不变)与"传了空值"(显式清除),以避免历史遗留的 stale 覆盖问题。 - 一个关键设计:PUT 不再持久化 API Key。
request.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_letter、enable_outreach_message、enable_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:openai、anthropic、google、openrouter、deepseek、groq、openai_compatible、ollama(见 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_language与content_language,支持en、es、zh、ja、pt、fr六种语言,非法值返回 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:
- 类型校验:仅允许
application/pdf、application/msword(.doc)、application/vnd.openxmlformats-officedocument.wordprocessingml.document(.docx),否则 400。 - 大小校验:超过
MAX_FILE_SIZE = 4MB返回 413。 - 空文件校验:空内容返回 400。
- 文档解析:调用
parse_document()将 PDF/DOCX 转为 Markdown,解析失败返回 422;提取不到文本(如图片型/扫描 PDF)同样返回 422。 - 落库:先以
processing_status="processing"原子创建记录(create_resume_atomic_master保证 master 分配的原子性),原始 Markdown 作为original_markdown永久保留。 - LLM 结构化解析:调用
parse_resume_to_json()把 Markdown 转为结构化 JSON(processed_data);成功则状态置为ready,失败则置为failed。该步骤可选——LLM 未配置时上传依然成功,只是解析状态为failed。
响应体(ResumeUploadResponse)包含 message、request_id、resume_id、processing_status、is_master,其中 processing_status 如实反映解析结果(ready 或 failed),避免客户端误判。对于解析失败或卡在 processing 的记录,还可调用 POST /resumes/{id}/retry-processing 重试(仅允许 failed/processing 状态,见 resumes.py)。
4.2 读取与列表:GET /resumes 与 GET /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_id、filename、is_master、parent_id、processing_status、created_at、updated_at、title。
4.3 改进(Tailoring):POST /resumes/improve 与 preview/confirm 流程
文档列出的 POST /resumes/improve 在当前仓库中是遗留端点,主流程已演进为两步式的 preview(预览)/ confirm(确认) 模式:
POST /resumes/improve/preview:不落库地生成定制化简历。请求体包含resume_id、job_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_id为null,同时携带markdownOriginal、markdownImproved、diff_summary、detailed_changes、refinement_stats、ats_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 覆盖content、processed_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。该端点接受大量模板与排版参数(均有范围约束):template(swiss-single、swiss-two-column、modern、modern-two-column、latex、clean、vivid)、pageSize(A4/LETTER)、页边距marginTop/Bottom/Left/Right(5–25mm)、sectionSpacing/itemSpacing/lineHeight/fontSize/headerScale(1–5)、headerFont/bodyFont(serif/sans-serif/mono)、compactMode、showContactIcons、accentColor(blue/green/orange/red)、lang(xx或xx-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_id与processed_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_id、content、created_at,还会包含由改进流程写入的job_keywords、job_keywords_hash、company、role、preview_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"
}
实际返回时,该对象被封装为 ResumeFetchResponse:data 下含 resume_id、raw_resume(原始 Markdown + content_type + 时间戳 + 处理状态)、processed_resume(由 ResumeData 校验的结构化数据)、cover_letter、outreach_message、interview_prep、parent_id、title,另附顶层 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(恒为第一且不可排序)、text、itemList、stringList。 - 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 契约提供了充分的集成测试,可作为对接时的"活文档":
- apps/backend/tests/integration/test_resume_api.py:上传、读取、更新、删除、PDF、改进与面试准备等简历端点。
- apps/backend/tests/integration/test_config_api.py:LLM 配置、功能开关、API Key 管理。
- apps/backend/tests/integration/test_jobs_api.py 与 test_applications_api.py:JD 上传与 tracker 操作。
- apps/backend/tests/integration/test_health_api.py:健康与状态端点。
- apps/backend/tests/integration/test_regenerate_endpoints.py:求职信/外联/面试准备的按需生成与再生成。
实战要点总结:
- 本地优先,无鉴权:任何端点都可直接调用,但切勿暴露公网;破坏性操作需确认令牌。
- 上传与解析分离:
POST /resumes/upload返回的processing_status可能为failed(LLM 未配置或解析失败),可配合POST /resumes/{id}/retry-processing重试。 - 定制化走 preview → confirm:先
improve/preview拿到哈希与 diff,再improve/confirm落库;绕过 preview 直接 confirm 会因哈希缺失返回 400。 - 附加内容按开关生成:求职信/外联/面试准备默认关闭,需先
PUT /config/features打开;也可事后用按需生成端点补生成。 - 超时按层配置:本地 LLM(Ollama 等)耗时较长,需同步调大后端
REQUEST_TIMEOUT_SECONDS(上限 1800)与前端NEXT_PUBLIC_REQUEST_TIMEOUT_MS,否则按最短层 abort。 - PDF 依赖无头浏览器:确保环境能启动 Chrome/Chromium/Edge(apps/backend/app/pdf.py 会自动探测常见安装路径);渲染失败返回 503。
完整的端点清单与调用关系还可参考 docs/agent/apis/api-flow-maps.md,其中给出了简历上传、改进、PDF 生成、配置更新、API Key 加密存储、求职追踪等关键流程的分步调用链,与本文的契约说明互为补充。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python230
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java281
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java200
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript180
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python300