feat: builder 简历生成 + 轻度优化 + 简历导入交付副本

自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hyp
2026-08-05 11:08:30 +08:00
co-authored by Claude
commit ae2d9b128d
191 changed files with 27719 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
"""Skill-suggestion cards and selection handling for the Builder."""
from __future__ import annotations
from copy import deepcopy
from typing import Any
from ..fsm import FSMError, Transition, component
from ..models import Stage
from ..resume_skill_advisor import recommend_skill_candidates
from ..skill_groups import update_skill_groups
from .constants import u
from .state import _set_stream_phases
from .turns import _next_step_turn, recommended_section
def _skill_choice_card(candidates: list[dict[str, Any]]) -> dict[str, Any]:
return component(
"ChoiceChips",
module="builder_skill_select",
title=u("确认岗位技能"),
description=u("请选择你愿意确认加入简历的技能;未选择的候选不会写入。没有合适的也可以暂时跳过。"),
multiple=True,
skippable=True,
skip_label=u("暂不添加"),
options=[
{
"value": str(candidate["skill"]),
"label": f'{candidate["skill"]}{u("")}{candidate["category"]}{u("")}',
}
for candidate in candidates
],
)
def _builder_skill_candidates(
profile: dict[str, Any],
resume_content: dict[str, Any],
skill_suggester: Any | None,
) -> list[dict[str, Any]]:
if skill_suggester is None:
return []
existing = [
str(skill).strip()
for group in resume_content.get("skill_groups") or []
if isinstance(group, dict)
for skill in group.get("skills") or []
if str(skill).strip()
]
working_profile = deepcopy(profile)
working_profile["tags"] = {**dict(working_profile.get("tags") or {}), "skills": existing}
facts: list[dict[str, Any]] = []
for section in resume_content.get("sections") or []:
if not isinstance(section, dict):
continue
for entry in section.get("items") or []:
if isinstance(entry, dict):
facts.append(deepcopy(entry))
working_profile["experiences"] = facts
return recommend_skill_candidates(
working_profile,
existing,
u("根据我选择的目标岗位推荐可确认技能"),
skill_suggester,
)
def _process_skill_selection(
profile: dict[str, Any],
state: dict[str, Any],
action: str,
payload: dict[str, Any],
resume_content: dict[str, Any],
) -> Transition:
if action == "skip":
state["pending_skill_candidates"] = []
_set_stream_phases(profile, "suggesting_next")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
_next_step_turn(recommended_section(profile, resume_content), prefix=u("好的,先不添加技能。")),
lifecycle="dismissed",
)
if action != "select":
raise FSMError("invalid_builder_skill_selection", "Confirm or skip the skill suggestions", status_code=422)
selected = payload.get("values")
if not isinstance(selected, list):
selected = [payload.get("value")]
allowed = {
str(item.get("skill") or "").strip()
for item in state.get("pending_skill_candidates") or []
if isinstance(item, dict) and str(item.get("skill") or "").strip()
}
chosen = [str(value).strip() for value in selected if str(value or "").strip() in allowed]
if not chosen:
raise FSMError("invalid_builder_skill_selection", "Select at least one suggested skill or skip", status_code=422)
existing = [
str(skill).strip()
for group in resume_content.get("skill_groups") or []
if isinstance(group, dict)
for skill in group.get("skills") or []
if str(skill).strip()
]
preferred = {
str(item.get("skill") or "").strip(): str(item.get("category") or "").strip()
for item in state.get("pending_skill_candidates") or []
if isinstance(item, dict) and str(item.get("skill") or "").strip() and str(item.get("category") or "").strip()
}
content = update_skill_groups(resume_content, [*existing, *chosen], preferred_categories=preferred)
state["pending_skill_candidates"] = []
_set_stream_phases(profile, "saving", "suggesting_next")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
_next_step_turn(
recommended_section(profile, content),
prefix=u("已添加") + " " + u("").join(chosen) + u(""),
),
lifecycle="confirmed",
resume_content=content,
)