Resume Matcher API 请求/响应流程全解析:从简历上传、AI 润色到求职追踪的端点调用链
Resume Matcher API 请求/响应流程全解析从简历上传、AI 润色到求职追踪的端点调用链【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher本文以 docs/agent/apis/api-flow-maps.md 为骨架逐条拆解 Resume Matcher 后端全部核心 API 端点的请求/响应流向并结合 apps/backend/app/routers 下各路由的真实实现与 apps/backend/app/services 中的服务层源码讲清楚每个端点进来什么、内部经过哪几步、最终返回什么。读完你将掌握简历上传与 LLM 结构化解析的状态机、AI 润色的 preview/confirm 两段式流程、面试准备与 PDF 生成的服务端调用链、健康检查与系统状态的设计哲学、加密 API Key 存储机制以及求职追踪Application Tracker的列分组与自动建卡逻辑——可直接用于二次开发、联调排错与 Agent 自动化接入。一、概览所有路由如何挂载在深入单个端点前先看清路由的整体挂载方式。入口文件 apps/backend/app/main.py 将所有路由统一挂在/api/v1前缀下health_router→/api/v1/health、/api/v1/statusconfig_router→/api/v1/config/*resumes_router→/api/v1/resumes/*jobs_router→/api/v1/jobs/*enrichment_router→/api/v1/enrichment/*applications_router→/api/v1/applicationsresume_wizard_router→/api/v1/resume-wizard/*因此下文所有端点路径都以/api/v1开头。应用启动时lifespan还会自动执行两件幂等操作把旧 TinyDB 数据迁移进 SQLite以及将旧版明文 API Key 折叠进加密存储并从config.json中剥离见 main.py这对理解下文API Keys 不再写入 config.json的设计有直接关联。二、Resume Upload上传 → 解析 → 结构化三步流水线原文档给出的流程为POST /api/v1/resumes/upload ├── Validate file (PDF/DOCX, ≤4MB) ├── parse_document() → Markdown ├── db.create_resume(statusprocessing) ├── parse_resume_to_json() → LLM │ ├── Success: statusready │ └── Failure: statusfailed └── Return {resume_id}对照实现 apps/backend/app/routers/resumes.py#L630-L713可将每一步细化如下1. 文件校验同步、无 LLM白名单 MIME 类型application/pdf、application/msword、application/vnd.openxmlformats-officedocument.wordprocessingml.document即 PDF、DOC、DOCX。不在白名单内直接返回400。大小上限MAX_FILE_SIZE 4 * 1024 * 10244MB超限返回413空文件返回400。2.parse_document()→ Markdown由 apps/backend/app/services/parser.py#L119-L141 实现将上传字节写入临时文件交给markitdown库转成 Markdown 文本。如果转换失败或提取出的文本为空例如扫描件/纯图片 PDF返回422并提示用户上传含可选文本的文件。3. 落库为 processing 状态调用db.create_resume_atomic_master()以content_typemd、processing_statusprocessing先写入记录并把original_markdown永久保留——即使之后 builder 保存用 JSON 覆盖content原始 Markdown 仍可供日期恢复等后处理使用。这是先落库、后解析的关键设计保证解析失败时用户仍能看到上传记录。4.parse_resume_to_json()→ LLM 结构化见 parser.py#L144-L176用PARSE_RESUME_PROMPT拼出提示词调用complete_json()带 3 次重试随后做两项后处理restore_dates_from_markdown()LLM 常把 Jun 2020 - Aug 2021 压缩成 2020 - 2021此函数从原始 Markdown 提取含月份的日期区间并按年份回填用ResumeDataPydantic 模型校验保证产出符合统一 schema。5. 状态收敛与返回成功db.update_resume(..., processing_statusready)返回resume_id与processing_statusready失败状态置为failed但上传记录仍然保留返回resume_id与processing_statusfailed并附带is_master标记第一份上传的简历会成为 master resume。值得补充的是 resumes.py#L1674-L1724 的POST /resumes/{id}/retry-processing当状态卡在failed或processing时可对已存 Markdown 重跑parse_resume_to_json()无需重新上传。三、Resume Improvementpreview/confirm 两段式 AI 润色原文档给出的流程POST /api/v1/resumes/improve ├── Fetch resume job from DB ├── extract_job_keywords() → LLM ├── improve_resume() → LLM ├── [If enabled] generate_cover_letter() → LLM ├── [If enabled] generate_outreach_message() → LLM ├── [If enabled] generate_interview_prep() → LLM ├── db.create_resume(improved) ├── db.create_improvement() └── Return {data, cover_letter, outreach_message, interview_prep}当前实现已将旧版单端点拆分为preview预览不落库 confirm确认后持久化两段式同时保留旧版POST /improve作为兼容路径。三者共享同一套内部管线理解_improve_preview_flowresumes.py#L848-L1112即可掌握核心1. 加载简历与 JD读取特征开关从请求中取resume_id、job_id_load_config()读取三个布尔开关enable_cover_letter/enable_outreach_message/enable_interview_prep以及get_content_language()决定输出语言。2. 关键词提取带内容哈希缓存extract_job_keywords()对 JD 做一次 LLM 调用。结果按job_keywords_hash sha256(job.content)缓存在 job 记录上哈希一致则直接复用避免重复付费调用。同时把 LLM 返回的company/role提升到 job 顶层字段带类型守卫后再.strip()供后续 tracker 自动建卡零额外调用读取。3. 差异化润色diff-based improvement若原始简历有结构化数据original_resume_data走 diff 管线generate_skill_target_plan()verify_skill_target_plan()先让 LLM 产出技能改写目标再用规则校验哪些可接受不支持的技能目标会被拒绝并计入 warningsgenerate_resume_diffs()→apply_diffs()LLM 只产出变更集由代码应用被拒的变更计入 warningsverify_diff_result()应用后逐条核对保证没有越界改写。 若无结构化数据则回退到improve_resume()全量输出模式。4. 四层安全网defense in depth_preserve_personal_info()个人联系方式永远以原稿为准改动会被拒绝见 resumes.py#L435-L459_restore_original_dates()restore_dates_from_markdown()LLM 截断的月份日期被还原resumes.py#L234-L308_preserve_original_skills()技术技能/证书/语言/奖项中任何被 LLM 丢弃的条目都会被追回resumes.py#L311-L362_protect_custom_sections()自定义 section 的条目数不增不减虚构的 description 被回滚resumes.py#L365-L432。5. 多轮精炼refinement拿到 master resume 数据后调用refine_resume()做多轮处理注入可注入关键词、移除 AI 腔短语、校验与原稿的对齐一致性并产出RefinementStatspasses 数、注入关键词数、移除 AI 短语数、修复的 critical 对齐违规数、初终匹配率。6. 差异摘要 ATS 评分 辅助内容_calculate_diff_from_resume()生成diff_summary与detailed_changes原结构数据缺失时给出original_data_missing之类的错误原因_build_ats_score()计算 ATS 总分与子分、缺失关键词、可注入关键词、建议_generate_auxiliary_messages()resumes.py#L556-L617用asyncio.gather并行生成标题始终开启以及按开关启用的 cover letter、outreach、interview prep单个失败只记 warning不阻断主流程。7. preview 与 confirm 的分野POST /improve/previewresumes.py#L796-L845不落库resume_id返回null但会把preview_hash写回 job 记录且整体包在asyncio.wait_for里受settings.request_timeout_seconds超时保护超时返回504并提示调整REQUEST_TIMEOUT_SECONDS/NEXT_PUBLIC_REQUEST_TIMEOUT_MS。POST /improve/confirmresumes.py#L1115-L1260先校验personalInfo未被改动再用_hash_improved_data()经ResumeData规范化后做 SHA-256见 resumes.py#L154-L177与 job 上缓存的 preview hash 比对防篡改通过后db.create_resume(content_typejson, parent_id原简历)、db.create_improvement()并触发 tracker 自动建卡见第七节。随后并行生成 cover letter / outreach / interview prep 并随响应返回。旧版POST /improveresumes.py#L1263-L1522行为与 confirm 一致只是不做 preview-hash 校验属于兼容入口。四、Interview Prep Generation按需生成面试准备原文档流程POST /api/v1/resumes/{id}/generate-interview-prep ├── Require tailored resume (parent_id) ├── Fetch improvement record and associated job description ├── Require processed resume data ├── generate_interview_prep() → LLM JSON ├── Validate InterviewPrepData ├── Save resumes.interview_prep as serialized JSON TEXT └── Return {interview_prep, message}实现位于 resumes.py#L1908-L1972前置校验链① 简历必须存在404② 必须是润色后的简历有parent_id否则 400 Interview preparation can only be generated for tailored resumes③ 通过db.get_improvement_by_tailored_resume()找到关联 JDimprovements 表是润色简历 → JD的桥④processed_data必须存在。LLM 调用apps/backend/app/services/interview_prep.py#L101-L128用INTERVIEW_PREP_PROMPT拼装提示词并对输入做了显式截断保护——JD 超 12,000 字符、简历 JSON 超 30,000 字符时逐级降采样字符串限长/列表限条数并附_prompt_truncation_notice提示 LLM只依据可见证据、不得臆造。max_tokens用get_safe_max_tokens(model, requested8192)求安全上限结果经InterviewPrepData.model_validate()强校验。存储方式存入resumes.interview_prep字段的是序列化 JSON 文本_serialize_interview_prep()json.dumps(model_dump(...))。读取时由_parse_interview_prep()resumes.py#L135-L151反向反序列化并再次校验解析失败只记日志返回None不会 500。这一点是联调时最容易踩的坑该字段是 TEXT 而非嵌套 JSON 列。五、PDF GenerationPlaywright 无头渲染原文档流程GET /api/v1/resumes/{id}/pdf ├── Fetch resume from DB ├── Build URL: {frontend}/print/resumes/{id}?{params} ├── Playwright render (wait for .resume-print) └── Return PDF bytes端点签名位于 resumes.py#L1582-L1662支持大量模板与排版参数均带Query校验参数默认值取值范围/说明templateswiss-singleswiss-single / swiss-two-column / modern / modern-two-column / latex / clean / vividpageSizeA4A4/LETTERmarginTop/Bottom/Left/Right10页边距mm5-25sectionSpacing/itemSpacing3/2区块/条目间距1-5lineHeight/fontSize/headerScale3行高/字号/页眉缩放1-5headerFont/bodyFontserif/sans-serifserif/sans-serif/monocompactModefalse紧凑模式showContactIconsfalse联系方式图标accentColorblueblue/green/orange/redlang无形如en、zh-CN的 locale实现要点拼接{settings.frontend_base_url}/print/resumes/{resume_id}?{params}后交给 apps/backend/app/pdf.py#L279-L337 的render_resume_pdf()渲染核心pdf.py#L138-L163有意识地不使用networkidleNext.js dev server 的 HMR/RSC 流式响应会让 idle 永不到来而挂死而是按确定性条件等待document load→ 目标选择器.resume-print出现 →document.fonts.ready全程受_NAV_TIMEOUT_MS 60_000显式超时约束浏览器启动策略优先复用常驻实例带asyncio.Lock防并发初始化竞态无内置 Playwright 浏览器时回退到系统 Chrome/Chromium/Edge跨 Windows/macOS/Linux 探测路径Windows 下若事件循环不支持子进程则退化为线程内新事件循环渲染错误语义化缺浏览器可执行文件 → 提示安装 Playwright chromiumnet::ERR_CONNECTION_REFUSED→ 提示检查FRONTEND_BASE_URL环境变量与前端是否在运行这正是联调 PDF 时最常见的两类失败。封面信 PDF 走同一管线GET /resumes/{id}/cover-letter/pdfresumes.py#L2017-L2055选择器换为.cover-letter-print渲染失败统一转 503。六、Health Check 与 System Status健康探针的隔离哲学原文档将两个端点并列语义区分非常明确GET /api/v1/health └── Return {status: healthy} # pure liveness — does NOT call the LLM GET /api/v1/status # each check isolated → 200 (partial/degraded), never 500 ├── try: get_llm_config() │ ├── llm_configured api_key set OR provider ∈ {ollama, openai_compatible} │ └── check_llm_health() → llm_healthy # failure here degrades only this field ├── try: db.get_stats() # failure → empty stats, still 200 └── Return {status, llm_configured, llm_healthy, has_master_resume, database_stats}源码位于 apps/backend/app/routers/health.pyGET /healthhealth.py#L25-L31纯存活探针用于 Docker HEALTHCHECK永不调用 LLM固定返回{status: healthy}不会因外部服务抖动而误报容器不健康。GET /statushealth.py#L34-L68深度状态页核心设计是逐项隔离任何单项失败只降级该字段整体恒为 200LLM 配置判定llm_configured bool(api_key) or provider in (ollama, openai_compatible)—— 注意本地 Ollama 与 OpenAI 兼容端点无需 Key 也算已配置check_llm_health()失败只把llm_healthy置 falsedb.get_stats()失败时回落到_EMPTY_DB_STATS空统计total_resumes/jobs/improvements 全 0、has_master_resumeFalse同样不影响 200顶层status由llm_healthy and has_master_resume收敛为ready或setup_required。这套设计让前端状态面板永远能渲染部分降级/待配置视图而不是整页报错是 Agent 做环境自检时的首选端点。七、Configuration 与加密 API Keys密钥从 config.json 迁出的安全演进原文档对配置更新与 API Keys 分了两组端点源码在 apps/backend/app/routers/config.py。7.1 非密钥配置更新PUT /api/v1/config/llm-api-key ├── _load_config() ├── Merge new NON-SECRET values (provider/model/base/...) ├── (no longer persists any key — keys go through /config/api-keys) ├── _save_config() └── Return masked config实现见 config.py#L111-L174。要点只更新请求中显式出现的字段provider、model、api_base、reasoning_effortapi_key字段在该端点被有意忽略、不再持久化——注释明确说明过去把单 Key 写进api_key槽位正是各 provider 互相覆盖、并遮蔽resolve_api_key()中 per-provider 映射的根因issue #760 同类问题;api_base做了空值即显式清除的语义区分请求中出现api_base: null/会清理旧值并归一化为None避免空字符串被当成伪端点传给 LiteLLM保存永远成功不因健康检查失败而硬失败用户配置代理/聚合器或临时不可达端点时仍可落盘随后在BackgroundTasks里做 best-effort 健康检查仅用于服务端日志连通性验证请走POST /config/llm-test。7.2 每 Provider 加密 Key 存储GET /api/v1/config/api-keys POST /api/v1/config/api-keys DELETE /api/v1/config/api-keys/{provider} DELETE /api/v1/config/api-keys?confirm...支持 provider 集合config.py#L435-L444 常量openai、anthropic、google、openrouter、deepseek、groq、openai_compatible、ollama。行为细节GET返回每个 provider 的{provider, configured, masked_key}掩码策略是保留最后 4 位如...abcd任何场景都不回显明文POST支持一次更新多个 provider只动请求里出现的 provider其余保持不动空字符串等于清除该 provider 的 Key写入时先 Fernet 加密再 upsert 进 SQLiteapi_keys表两个 DELETE 是破坏性操作单个 provider 删除直接执行清空全部需要 query 参数confirmCLEAR_ALL_KEYS否则 400。POST /config/reset同理需要confirmRESET_ALL_DATA才会清库应用启动时migrate_legacy_keys()main.py会把 config.json 里的旧明文 Key 幂等迁移进加密存储并剥离明文。7.3 功能开关、语言、提示词配置GET/PUT /config/features管理enable_cover_letter/enable_outreach_message/enable_interview_prep三个布尔开关默认全关config.py#L220-L252GET/PUT /config/languageui_language与content_language分离支持集合为[en, es, zh, ja, pt, fr]并兼容旧版单一language字段迁移config.py#L256-L309GET/PUT /config/prompts管理润色默认提示词default_prompt_id非法值回落内置默认config.py#L312-L357GET/PUT /config/feature-prompts自定义 cover letter / outreach 提示词非空时校验必须包含{job_description}、{resume_data}、{output_language}三个占位符缺失返回 422 且 detail 里列出具体缺失项空串表示清除覆盖、回退内置默认config.py#L360-L429。八、Job Upload批量 JD 入库原文档流程极简实现位于 apps/backend/app/routers/jobs.py#L11-L39POST /api/v1/jobs/upload ├── For each description: │ └── db.create_job() └── Return {job_id[]}细节请求体{job_descriptions: [...], resume_id}空列表 400逐条校验非空后db.create_job(contentjd, resume_id...)返回job_id数组与输入顺序一一对应。GET /jobs/{job_id}用于按 ID 取回 JD 原文404 兜底。JD 只在润色时才按需做关键词提取并缓存见第三节上传本身不触发任何 LLM 调用。九、Resume OperationsCRUD 速查原文档用表格概括了四个端点实现位置与补充语义如下端点实现位置关键行为GET /resumes?idresumes.py#L716-L767返回raw_resumeprocessed_resume经normalize_resume_data()惰性迁移旧记录的 section 元数据同时带出cover_letter、outreach_message、interview_prep反序列化、parent_id、titleGET /resumes/list?include_masterresumes.py#L770-L793默认排除 master resume按updated_at倒序返回摘要列表含is_master、parent_id、processing_statusPATCH /resumes/{id}resumes.py#L1525-L1579请求体为完整ResumeData落库时把content同步为 JSON 文本、content_typejson、processing_statusreadyDELETE /resumes/{id}resumes.py#L1665-L1671删除不存在时 404其余衍生端点同属此模块PATCH /resumes/{id}/cover-letter、/outreach-message、/title标题截断 80 字符POST /resumes/{id}/generate-cover-letter与/generate-outreach均要求parent_id且能从 improvements 表找到 JDGET /resumes/{id}/job-description回取润色所用 JD。十、Application Tracker七列看板与自动建卡原文档用一段树状图 两张表格描述了求职追踪的全部端点核心数据模型见 apps/backend/app/schemas/applications.py#L9-L22class ApplicationStatus(str, Enum): saved saved applied applied no_response no_response response response interview interview accepted accepted rejected rejected APPLICATION_STATUS_ORDER [s.value for s in ApplicationStatus]七个状态键即七列看板顺序由APPLICATION_STATUS_ORDER固定且与 i18n 文案解耦只存 enum 值文案由前端映射。10.1 列表与详情GET /applicationsdb.list_applications()后由_group_by_status()applications.py#L27-L41按七列分组七列键永远齐全遇到未知状态的行直接跳过并记日志而不是让整个看板 500。GET /applications/{id}一次往返内把卡片与关联job_content、投递所用简历一起返回简历被删时resume字段为null弹窗渲染简历不可用而非 500。10.2 手动添加贴 JD 建卡POST /applicationsapplications.py#L55-L97POST /api/v1/applications # manual add from a pasted JD ├── db.create_job(jd) ├── [If company/role missing] extract_job_keywords() → LLM # one best-effort call ├── db.create_application(status default applied) # dedupes on (job_id, resume_id) └── Return Application要点公司/角色缺失时只做一次 best-effort 的extract_job_keywords()复用关键词提取不新增提示词路径失败则回退为空值可手动编辑LLM 抖动绝不阻塞建卡若db.create_application()失败会顺带清理刚创建的孤儿 job 记录避免重试漂移。db.create_application层对(job_id, resume_id)去重。10.3 更新、批量操作与删除端点语义PATCH /applications/{id}部分更新 status/position/notes/company/role/applied_atstatus 由 enum 归一化为稳定字符串position由服务端重新编号PATCH /applications/bulk{application_ids, status}批量移动卡片到同一列返回受影响数DELETE /applications/{id}单卡删除POST /applications/bulk-delete{application_ids}批量删除10.4 自动建卡Auto-create原文档特别标注POST /resumes/improve/confirm及旧版POST /resumes/improve在持久化润色简历后会创建一个applied卡片。实现是 resumes.py#L73-L98 的_auto_create_tracker_application()公司/角色直接复用第三节中已缓存在 job 上的 keyword 提取结果job.get(company)、title or job.get(role)零额外 LLM 调用整个调用包在try/except里tracker 失败只记logger.warning永远不会反过来破坏润色主流程——这是best-effort 副作用的典型实现Agent 接入时应把该卡片视为可重建的派生数据。十一、调试与联调指引把流程图变成可执行验证结合上述源码事实给出三条可落地的联调路径全链路自检先GET /api/v1/status确认llm_configured/llm_healthy/has_master_resume再POST /config/llm-test做连通性实测LLM 未就绪时上传简历会得到processing_statusfailed不是报错此时用POST /resumes/{id}/retry-processing重试。润色联调按POST /jobs/upload→POST /resumes/improve/preview拿到preview_hash与 diff 预览→ 修改确认后POST /resumes/improve/confirm的顺序走若 confirm 报 400 Invalid improved resume data. Please retry preview.多半是前端把非 schema 完整字段发回导致 hash 不一致服务端已用ResumeData规范化缓解。PDF 排障GET /resumes/{id}/pdf返回 503 时对照 pdf.py 的错误分类检查前端是否运行、FRONTEND_BASE_URL是否与前端地址一致、Playwright 浏览器是否已python -m playwright install chromium。权限与安全边界所有配置与重置端点/config/reset、/config/api-keys的 DELETE在源码注释中被明确标注为本地单用户部署设计多用户生产环境需自行加认证——接入前务必评估。结语本文以 docs/agent/apis/api-flow-maps.md 的流程图为纲逐一落到 apps/backend/app/routers 与 apps/backend/app/services 的源码实现上。可以看到Resume Matcher 的 API 层在设计上高度一致——LLM 调用全部收敛在服务层、可缓存结果全部哈希化复用、副作用如 tracker 自动建卡一律 best-effort 隔离、敏感字段API Key走加密独立存储并全程掩码返回。这些模式不仅适用于本项目的二次开发也值得在自建 LLM 工具链时直接借鉴。如需继续深入可对照 apps/backend/tests/integration 下的集成测试如test_regenerate_endpoints.py、test_tracker_autocreate.py、test_resume_api.py逐端点验证本文描述的行为。【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考