"""RESUME_ENRICHING 模块队列引擎(V1 全链路收集)。 组件事件分发见 enrichment_collectors.py;fsm.py 通过函数内 import 调用,避免循环依赖。 """ from __future__ import annotations from typing import Any from .enrichment_modules import ( ENRICHMENT_MODULES, ENRICHMENT_QUEUES, PICKER_OPTIONS, RECORD_KEYS, TAG_TITLES, ModuleSpec, skill_suggestions, ) from .fsm import Transition, assistant_turn, component from .models import ComposerMode, JobType, Stage from .record_card import record_card def ensure_enrichment_state(profile: dict[str, Any]) -> dict[str, Any]: """惰性创建 records/tags/enrichment;旧会话现场建队列。""" records = profile.setdefault("records", {}) for key in RECORD_KEYS: records.setdefault(key, []) profile.setdefault("tags", {"skills": [], "certificates": []}) enrichment = profile.setdefault("enrichment", {}) if "queue" not in enrichment: try: job_type = JobType(str(profile.get("job_type"))) except ValueError as exc: raise ValueError("profile contains an unsupported job_type") from exc enrichment["queue"] = list(ENRICHMENT_QUEUES[job_type]) enrichment.setdefault("index", 0) enrichment.setdefault("current", None) enrichment.setdefault("skipped", []) enrichment.setdefault("completed", []) enrichment.setdefault("module_draft", {}) enrichment.setdefault("custom_mode", False) return profile def current_module(profile: dict[str, Any]) -> ModuleSpec | None: name = (profile.get("enrichment") or {}).get("current") return ENRICHMENT_MODULES.get(name) if name else None def enrichment_progress(profile: dict[str, Any]) -> dict[str, Any]: enrichment = profile.get("enrichment") or {} total = len(enrichment.get("queue") or []) completed = len(enrichment.get("completed") or []) # total 固定为队列长度;skip 只切换到下一模块,不推进度(PR 评审结论) ratio = completed / total if total > 0 else 1.0 return { "completed": completed, "skipped": len(enrichment.get("skipped") or []), "total": total, "ratio": ratio, } def begin_enrichment(profile: dict[str, Any]) -> Transition: ensure_enrichment_state(profile) enrichment = profile["enrichment"] queue = enrichment["queue"] if enrichment["index"] >= len(queue): from .enrichment_custom import custom_card_picker_transition return custom_card_picker_transition(profile) profile["enrichment_finished"] = False spec = ENRICHMENT_MODULES[queue[enrichment["index"]]] enrichment["current"] = spec.name enrichment["module_draft"] = {} return begin_module(profile, spec) def progress_block(profile: dict[str, Any], spec: ModuleSpec) -> dict[str, Any]: progress = enrichment_progress(profile) return component( "ProgressCard", module=spec.name, completed=progress["completed"], skipped=progress["skipped"], total=progress["total"], percent=round(progress["ratio"] * 100), actions=["defer"], ) def begin_module(profile: dict[str, Any], spec: ModuleSpec) -> Transition: blocks = [progress_block(profile, spec)] if spec.name in PICKER_OPTIONS and not profile["enrichment"]["module_draft"].get("record_type"): options = [{"value": v, "label": l} for v, l in PICKER_OPTIONS[spec.name]] blocks.append( component("ChoiceChips", module=spec.name, field="record_type", title=spec.prompt, options=options, skippable=True) ) elif spec.kind in {"record_fields", "anchor_note"}: blocks.append(record_card(profile, spec)) elif spec.kind == "record_form": blocks.append(component("CompetitionFields", module=spec.name, title=spec.prompt)) elif spec.kind == "tags": field = spec.optional_fields[0] profile["enrichment"]["module_draft"] = {"field": field} data = { "module": spec.name, "field": field, "title": TAG_TITLES[field], "description": spec.prompt, } if field == "skills": data["suggestions"] = skill_suggestions(profile.get("target_position"), profile) blocks.append(component("TagsInput", **data)) transition = Transition( Stage.RESUME_ENRICHING, profile, assistant_turn(spec.prompt, blocks, mode=ComposerMode.UI_ONLY), ) if spec.kind == "tags" and spec.optional_fields[0] == "skills": transition.suggest_skills = True return transition def advance_or_finish(profile: dict[str, Any], *, refresh: bool = False) -> Transition: enrichment = profile["enrichment"] enrichment["index"] += 1 enrichment["module_draft"] = {} if enrichment["index"] >= len(enrichment["queue"]): from .enrichment_custom import custom_card_picker_transition transition = custom_card_picker_transition(profile) else: spec = ENRICHMENT_MODULES[enrichment["queue"][enrichment["index"]]] enrichment["current"] = spec.name transition = begin_module(profile, spec) transition.refresh_resume = refresh return transition def defer_enrichment(profile: dict[str, Any]) -> Transition: profile["enrichment"]["current"] = None profile["enrichment"]["module_draft"] = {} profile["enrichment_finished"] = True return Transition( Stage.CONTENT_READY, profile, assistant_turn( "好的,随时可以回来继续完善。", [component("ContentReadyCard", can_continue=True)], mode=ComposerMode.UI_ONLY, ), lifecycle="confirmed", ) def skip_module(profile: dict[str, Any], spec: ModuleSpec) -> Transition: if profile["enrichment"].get("custom_mode"): from .enrichment_custom import custom_card_picker_transition return custom_card_picker_transition(profile) skipped = profile["enrichment"]["skipped"] if spec.name not in skipped: skipped.append(spec.name) return advance_or_finish(profile) def mark_completed(profile: dict[str, Any], name: str) -> None: completed = profile["enrichment"]["completed"] if name not in completed: completed.append(name) def add_another_transition(profile: dict[str, Any], spec: ModuleSpec, *, refresh: bool = False) -> Transition: """multi 模块确认一段后的"再添加/下一项"卡片。""" transition = Transition( Stage.RESUME_ENRICHING, profile, assistant_turn( "已写入简历。", [ progress_block(profile, spec), component( "AddAnother", module=spec.name, title="还要再添加一段吗?", options=[ {"value": "again", "label": "再添加一段"}, {"value": "next", "label": "进入下一项"}, ], ), ], mode=ComposerMode.UI_ONLY, ), lifecycle="confirmed", ) transition.refresh_resume = refresh return transition