From ebffbc8615e0651b0d07e0d67aacf5fb3138a03d Mon Sep 17 00:00:00 2001 From: zk Date: Tue, 14 Apr 2026 12:12:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E5=B2=97=E4=BD=8D?= =?UTF-8?q?=E7=BC=96=E8=BE=91agent=20=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kiro/specs/skill-gap-analysis.md | 136 +++++++++++++------ app/ai/skill_gap_analyzer/analyzer.py | 55 ++++++-- app/ai/skill_gap_analyzer/prompts.py | 75 +++++++---- app/services/skill_gap_service.py | 180 ++++++++++++++++++++------ 4 files changed, 326 insertions(+), 120 deletions(-) diff --git a/.kiro/specs/skill-gap-analysis.md b/.kiro/specs/skill-gap-analysis.md index 592d926..972e5fd 100644 --- a/.kiro/specs/skill-gap-analysis.md +++ b/.kiro/specs/skill-gap-analysis.md @@ -282,14 +282,16 @@ ### 处理流程(两步走) +核心思路:将用户操作原子化,把模块级并发拆成记录级并发,减少单次 AI 输出量,提升响应速度。 + #### 第一步:准备数据 1. 从 Redis 取当前定制简历(不存在则报错) -2. 查 `bg_job` 拿 title、description +2. 查 `bg_job` 拿 title、description、requirement -#### 第二步:规划 AI(意图识别) +#### 第二步:规划 AI(意图识别 + 操作原子化) -输入:用户指令 + 对话历史 + 当前完整简历内容 + 岗位 title +输入:用户指令 + 对话历史 + 当前完整简历内容 + 岗位信息(title + description + requirement) 输出两种结果: @@ -299,18 +301,31 @@ ``` → 直接返回 `{ "type": "message", "message": "..." }`,结束。 -**修改计划(指令明确)**: +**操作计划(指令明确)**: ```json { "action": "modify", - "modules": [ - { "module": "resume", "instruction": "在 summary 中融入数据分析技能,skills 添加 Python" }, - { "module": "work", "instruction": "精简第一段工作经历,突出量化成果" } - ], - "updatedModulesLabel": "个人简介、工作经验" + "operations": [ + { "type": "delete", "module": "work", "id": "abc12345" }, + { "type": "update", "module": "work", "id": "def67890", "instruction": "精简描述,突出量化成果" }, + { "type": "update", "module": "resume", "instruction": "在summary中融入数据分析关键词" }, + { "type": "add", "module": "internship", "instruction": "新增一段数据分析实习经历" } + ] } ``` +操作类型: +- **delete**:删除记录,需要 module + id +- **update**:修改记录,需要 module + id + instruction(resume 主表不需要 id) +- **add**:新增记录,需要 module + instruction + +规则: +- 每个操作对应一条原子操作,一个用户指令可拆出多条操作 +- `instruction` 限 50 字以内 +- `delete` 和 `update`(非 resume)必须带 `id`,从当前简历中匹配 +- `update` module 为 `resume` 时不需要 `id`(主表是单对象) +- `add` 不需要 `id` + 模块划分(按表结构,共 6 个): | 模块名 | 对应表 | 可修改字段 | @@ -322,25 +337,27 @@ | `project` | `bg_user_resume_project` | 全部字段 | | `competition` | `bg_user_resume_competition` | 全部字段 | -#### 第三步:按模块并发执行修改 +#### 第三步:按操作类型并发执行 -根据修改计划,对每个模块并发调 AI(asyncio.gather): -- 每个模块传入该模块的完整数据(如 work 传 `[{工作1}, {工作2}]` 整体) -- AI 返回修改后的该模块完整数据 -- 所有模块统一走 AI,包括 skills 等简单操作(因为需要 AI 理解用户自然语言指令) +先处理 delete(零 AI 开销),再并发执行所有 update 和 add(asyncio.gather): + +- **delete**:后端直接按 module + id 从数组移除,不调 AI +- **update(含 resume 主表和子表记录)**:按 id 取出单条记录 + instruction + schema + 岗位信息(title + description + requirement)+ 最近 10 条对话历史,调 AI 修改,只输出单条记录。resume 主表和子表记录复用同一个 prompt +- **add**:instruction + schema + 岗位信息(title + description + requirement)+ 最近 10 条对话历史,调 AI 生成一条新记录 #### 第四步:合并 -- AI 调用失败的模块保持原数据不动 -- 成功的模块直接用 AI 返回结果替换 - -把所有模块结果合并回完整简历。 +- delete 已直接处理 +- update 按 module + id 替换回原数据(resume 主表直接整体替换) +- add 按 module 追加到对应数组 +- AI 调用失败的操作跳过,不影响其他操作 #### 第五步:保存 + 返回 1. 当前简历存为回滚数据(key:`customize:resume:rollback:{userId}`,过期 30 分钟) 2. 新简历覆盖 Redis(key:`customize:resume:{userId}`),刷新过期时间 12 小时 -3. 返回 `type: updated` + 消息(前端通过 GET 接口查询新简历,通过回滚接口恢复) +3. `updatedModulesLabel` 由后端根据 operations 中的 module 去重映射中文名拼接 +4. 返回 `type: updated` + 消息(前端通过 GET 接口查询新简历,通过回滚接口恢复) ### description 字段处理 @@ -427,10 +444,11 @@ ### 4. Agent - 规划 Prompt ``` -你是一个简历编辑助手。分析用户的指令,决定需要修改简历的哪些模块。 +你是一个简历编辑助手。分析用户的指令,将其拆解为原子操作。 【目标岗位】 {job_title} +{job_description} 【当前简历】 {resume_json} @@ -441,47 +459,81 @@ 【用户指令】 {instruction} -如果用户指令明确,返回修改计划 JSON: -{"action": "modify", "modules": [{"module": "模块名", "instruction": "具体修改要求"}], "updatedModulesLabel": "中文模块名列表"} - -如果用户指令不明确或需要澄清,返回对话 JSON: +如果用户指令不明确或需要澄清,返回: {"action": "chat", "message": "你的追问内容"} -模块名可选: -- resume:主表(个人信息,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl) -- education:教育经历 -- work:工作经历 -- internship:实习经历 -- project:项目经历 -- competition:竞赛经历 -只返回 JSON,不要其他内容。 +如果用户指令明确,将其拆解为原子操作列表,返回: +{"action": "modify", "operations": [...]} + +操作类型: +1. 删除记录:{"type": "delete", "module": "模块名", "id": "记录id"} +2. 修改记录:{"type": "update", "module": "模块名", "id": "记录id", "instruction": "修改说明(50字内)"} +3. 修改主表:{"type": "update", "module": "resume", "instruction": "修改说明(50字内)"} +4. 新增记录:{"type": "add", "module": "模块名", "instruction": "新增说明(50字内)"} + +模块名可选:resume(主表,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)、education(教育)、work(工作)、internship(实习)、project(项目)、competition(竞赛) + +规则: +1. 每条操作对应一个最小粒度的修改,一个用户指令可拆出多条操作 +2. delete 和 update(非resume)必须带 id,从当前简历中匹配 +3. instruction 不超过50字,简明扼要 +4. 只返回 JSON,不要其他内容 ``` -### 5. Agent - 模块修改 Prompt +### 5. Agent - 单条记录修改 Prompt ``` -你是一个简历编辑助手。根据修改要求,修改简历的指定模块。 +你是一个简历编辑助手。根据修改要求,修改简历中的一条记录。 【目标岗位】 {job_title} +{job_description} 【修改要求】 -{module_instruction} +{instruction} + +【最近对话】 +{chat_history} 【模块数据结构】 {module_schema} -【当前模块数据】 -{module_data} +【当前记录数据】 +{record_data} 规则: -1. 严格按照修改要求操作,可以增删改 -2. 未要求修改的记录保持不变 +1. 严格按照修改要求操作 +2. 未要求修改的字段保持不变 3. 不要编造用户简历中不存在的内容 4. 保持原文格式和结构 -5. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除 -6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串 -7. 返回修改后的完整模块数据(JSON 格式,与输入格式一致) +5. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 +6. 返回修改后的完整记录数据(JSON 格式,与输入格式一致) +``` + +### 6. Agent - 新增记录 Prompt + +``` +你是一个简历编辑助手。根据要求,生成一条新的简历记录。 + +【目标岗位】 +{job_title} +{job_description} + +【新增要求】 +{instruction} + +【最近对话】 +{chat_history} + +【模块数据结构】 +{module_schema} + +规则: +1. 按照模块数据结构生成完整字段 +2. id 使用随机8位字符串 +3. description 中每个段落的 id 也使用随机8位字符串 +4. 内容要合理真实,贴合目标岗位方向 +5. 返回一条完整记录的 JSON,与模块数据结构一致 ``` ### 各模块数据结构定义(传入 prompt 的 module_schema) diff --git a/app/ai/skill_gap_analyzer/analyzer.py b/app/ai/skill_gap_analyzer/analyzer.py index 85337c3..d9c5070 100644 --- a/app/ai/skill_gap_analyzer/analyzer.py +++ b/app/ai/skill_gap_analyzer/analyzer.py @@ -14,7 +14,7 @@ from langchain_core.prompts import ChatPromptTemplate from app.ai.models import LLM from app.ai.skill_gap_analyzer.prompts import ( SKILL_GAP_PROMPT, SUMMARY_OPTIMIZE_PROMPT, EXPERIENCE_OPTIMIZE_PROMPT, - AGENT_PLAN_PROMPT, AGENT_MODULE_EDIT_PROMPT, MODULE_SCHEMAS, + AGENT_PLAN_PROMPT, AGENT_MODULE_EDIT_PROMPT, AGENT_MODULE_ADD_PROMPT, MODULE_SCHEMAS, ) from app.core.logger import log @@ -100,11 +100,13 @@ _plan_chain = ( ) -async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruction: str) -> dict | None: - """Agent 规划:分析用户指令,返回修改计划或对话回复""" +async def plan_edit(job_title: str, job_description: str, resume_json: str, + chat_history: str, instruction: str) -> dict | None: + """Agent 规划:分析用户指令,返回原子操作列表或对话回复""" try: raw = await _plan_chain.ainvoke({ - "job_title": job_title, "resume_json": resume_json, + "job_title": job_title, "job_description": job_description, + "resume_json": resume_json, "chat_history": chat_history, "instruction": instruction, }) result = _parse_json(raw) @@ -114,24 +116,51 @@ async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruc return None -# ===== Agent - 模块修改 ===== +# ===== Agent - 单条记录修改 ===== -_module_edit_chain = ( +_record_edit_chain = ( ChatPromptTemplate.from_messages([("system", AGENT_MODULE_EDIT_PROMPT), ("human", "请执行修改。")]) | LLM.GPT_4O.create(temperature=0.3) | StrOutputParser() ) -async def execute_module_edit(job_title: str, module_instruction: str, - module_schema: str, module_data: str) -> dict | list | None: - """Agent 模块修改:根据指令修改指定模块数据""" +async def execute_record_edit(job_title: str, job_description: str, instruction: str, + chat_history: str, module_schema: str, + record_data: str) -> dict | list | None: + """修改单条记录:根据指令修改指定记录数据""" try: - raw = await _module_edit_chain.ainvoke({ - "job_title": job_title, "module_instruction": module_instruction, - "module_schema": module_schema, "module_data": module_data, + raw = await _record_edit_chain.ainvoke({ + "job_title": job_title, "job_description": job_description, + "instruction": instruction, "chat_history": chat_history, + "module_schema": module_schema, "record_data": record_data, }) return _parse_json(raw) except Exception as e: - log.warning(f"AI模块修改失败: {e}") + log.warning(f"AI单条记录修改失败: {e}") + return None + + +# ===== Agent - 新增记录 ===== + +_record_add_chain = ( + ChatPromptTemplate.from_messages([("system", AGENT_MODULE_ADD_PROMPT), ("human", "请生成新记录。")]) + | LLM.GPT_4O.create(temperature=0.3) + | StrOutputParser() +) + + +async def execute_record_add(job_title: str, job_description: str, instruction: str, + chat_history: str, module_schema: str) -> dict | None: + """新增一条记录:根据指令生成新记录""" + try: + raw = await _record_add_chain.ainvoke({ + "job_title": job_title, "job_description": job_description, + "instruction": instruction, "chat_history": chat_history, + "module_schema": module_schema, + }) + result = _parse_json(raw) + return result if isinstance(result, dict) else None + except Exception as e: + log.warning(f"AI新增记录失败: {e}") return None diff --git a/app/ai/skill_gap_analyzer/prompts.py b/app/ai/skill_gap_analyzer/prompts.py index 8fe36a6..ea63b1d 100644 --- a/app/ai/skill_gap_analyzer/prompts.py +++ b/app/ai/skill_gap_analyzer/prompts.py @@ -52,10 +52,11 @@ EXPERIENCE_OPTIMIZE_PROMPT = """你是一个简历优化助手。根据目标岗 4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)""" -AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,决定需要修改简历的哪些模块。 +AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,将其拆解为原子操作。 【目标岗位】 {job_title} +{job_description} 【当前简历】 {resume_json} @@ -66,43 +67,73 @@ AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令, 【用户指令】 {instruction} -如果用户指令明确,返回修改计划 JSON: -{{"action": "modify", "modules": [{{"module": "模块名", "instruction": "具体修改要求"}}], "updatedModulesLabel": "中文模块名列表"}} - -如果用户指令不明确或需要澄清,返回对话 JSON: +如果用户指令不明确或需要澄清,返回: {{"action": "chat", "message": "你的追问内容"}} -模块名可选: -- resume:主表(个人信息,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl) -- education:教育经历 -- work:工作经历 -- internship:实习经历 -- project:项目经历 -- competition:竞赛经历 -只返回 JSON,不要其他内容。""" +如果用户指令明确,将其拆解为原子操作列表,返回: +{{"action": "modify", "operations": [...]}} -AGENT_MODULE_EDIT_PROMPT = """你是一个简历编辑助手。根据修改要求,修改简历的指定模块。 +操作类型: +1. 删除记录:{{"type": "delete", "module": "模块名", "id": "记录id"}} +2. 修改记录:{{"type": "update", "module": "模块名", "id": "记录id", "instruction": "修改说明(50字内)"}} +3. 修改主表:{{"type": "update", "module": "resume", "instruction": "修改说明(50字内)"}} +4. 新增记录:{{"type": "add", "module": "模块名", "instruction": "新增说明(50字内)"}} + +模块名可选:resume(主表,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)、education(教育)、work(工作)、internship(实习)、project(项目)、competition(竞赛) + +规则: +1. 每条操作对应一个最小粒度的修改,一个用户指令可拆出多条操作 +2. delete 和 update(非resume)必须带 id,从当前简历中匹配 +3. instruction 不超过50字,简明扼要 +4. 只返回 JSON,不要其他内容""" + +AGENT_MODULE_EDIT_PROMPT = """你是一个简历编辑助手。根据修改要求,修改简历中的一条记录。 【目标岗位】 {job_title} +{job_description} 【修改要求】 -{module_instruction} +{instruction} + +【最近对话】 +{chat_history} 【模块数据结构】 {module_schema} -【当前模块数据】 -{module_data} +【当前记录数据】 +{record_data} 规则: -1. 严格按照修改要求操作,可以增删改 -2. 未要求修改的记录保持不变 +1. 严格按照修改要求操作 +2. 未要求修改的字段保持不变 3. 不要编造用户简历中不存在的内容 4. 保持原文格式和结构 -5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除 -6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串 -7. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)""" +5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 +6. 返回修改后的完整记录数据(JSON 格式,与输入格式一致)""" + +AGENT_MODULE_ADD_PROMPT = """你是一个简历编辑助手。根据要求,生成一条新的简历记录。 + +【目标岗位】 +{job_title} +{job_description} + +【新增要求】 +{instruction} + +【最近对话】 +{chat_history} + +【模块数据结构】 +{module_schema} + +规则: +1. 按照模块数据结构生成完整字段 +2. id 使用随机8位字符串 +3. description 中每个段落的 id 也使用随机8位字符串 +4. 内容要合理真实,贴合目标岗位方向 +5. 返回一条完整记录的 JSON,与模块数据结构一致""" # 各模块数据结构定义(传入 prompt 的 module_schema) MODULE_SCHEMAS: dict[str, str] = { diff --git a/app/services/skill_gap_service.py b/app/services/skill_gap_service.py index be5bafc..6400dfe 100644 --- a/app/services/skill_gap_service.py +++ b/app/services/skill_gap_service.py @@ -16,7 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.ai.skill_gap_analyzer.analyzer import ( analyze_skill_gap, optimize_summary, optimize_module, - plan_edit, execute_module_edit, + plan_edit, execute_record_edit, execute_record_add, ) from app.ai.skill_gap_analyzer.prompts import MODULE_SCHEMAS from app.core.logger import log @@ -40,6 +40,16 @@ CUSTOMIZE_RESUME_ROLLBACK_EXPIRE = 30 * 60 # 30分钟 _CHARS = string.ascii_letters + string.digits +# 模块名 → 中文标签映射 +_MODULE_LABELS = { + "resume": "个人简介", + "education": "教育经历", + "work": "工作经历", + "internship": "实习经历", + "project": "项目经历", + "competition": "竞赛经历", +} + def _rand_id() -> str: """生成随机8位字符串标识""" @@ -201,7 +211,7 @@ class SkillGapService: async def ai_edit_customize_resume(self, user_id: int, job_id: int, instruction: str, chat_history: list) -> dict: - """AI 对话式编辑定制简历""" + """AI 对话式编辑定制简历(原子化操作版)""" # 1. 取当前定制简历 key = f"{CUSTOMIZE_RESUME_KEY_PREFIX}{user_id}" raw = await RedisManager.client.get(key) @@ -211,69 +221,153 @@ class SkillGapService: resume_json = cr.model_dump_json(by_alias=True) # 2. 查岗位 job = await self._get_job(job_id) - # 3. 规划 AI + job_desc = f"{job.description or ''}\n{job.requirement or ''}" + # 3. 规划 AI(意图识别 + 操作原子化) history_str = json.dumps(chat_history, ensure_ascii=False) if chat_history else "无" - plan = await plan_edit(job.title or "", resume_json, history_str, instruction) + plan = await plan_edit(job.title or "", job_desc, resume_json, history_str, instruction) if not plan: return {"type": "message", "message": "抱歉,我没有理解你的意思,请再描述一下。"} if plan.get("action") == "chat": return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")} - # 4. 按模块并发执行修改 - modules = plan.get("modules", []) - if not modules: + # 4. 解析操作列表 + operations = plan.get("operations", []) + if not operations: return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")} - edit_tasks = [] - for m in modules: - mod_name = m.get("module", "") - mod_instr = m.get("instruction", "") + # 截取最近10条对话历史 + recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history + recent_history_str = json.dumps(recent_history, ensure_ascii=False) if recent_history else "无" + # 5. 按操作类型分发执行 + # 先处理 delete(零 AI 开销) + for op in operations: + if op.get("type") == "delete": + self._apply_delete(cr, op.get("module", ""), op.get("id", "")) + # 并发执行 update 和 add + ai_tasks = [] + for op in operations: + op_type = op.get("type", "") + mod_name = op.get("module", "") + op_instruction = op.get("instruction", "") schema = MODULE_SCHEMAS.get(mod_name, "") - mod_data = self._get_module_data(cr, mod_name) - edit_tasks.append((mod_name, execute_module_edit(job.title or "", mod_instr, schema, mod_data))) - keys = [t[0] for t in edit_tasks] - results = await asyncio.gather(*[t[1] for t in edit_tasks], return_exceptions=True) - # 5. 合并结果 - for mod_key, result in zip(keys, results): - if isinstance(result, Exception): - log.warning(f"AI编辑模块[{mod_key}]失败: {result}") - continue - if result is None: - continue - self._apply_edit_result(cr, mod_key, result) + if op_type == "update": + record_data = self._get_record_data(cr, mod_name, op.get("id")) + if record_data is not None: + ai_tasks.append(( + "update", mod_name, op.get("id"), + execute_record_edit( + job.title or "", job_desc, op_instruction, + recent_history_str, schema, record_data, + ), + )) + elif op_type == "add": + ai_tasks.append(( + "add", mod_name, None, + execute_record_add( + job.title or "", job_desc, op_instruction, + recent_history_str, schema, + ), + )) + # 并发执行 + if ai_tasks: + coros = [t[3] for t in ai_tasks] + results = await asyncio.gather(*coros, return_exceptions=True) + for (op_type, mod_name, record_id, _), result in zip(ai_tasks, results): + if isinstance(result, Exception): + log.warning(f"AI编辑[{op_type}/{mod_name}/{record_id}]失败: {result}") + continue + if result is None: + continue + if op_type == "update": + self._apply_record_update(cr, mod_name, record_id, result) + elif op_type == "add": + self._apply_record_add(cr, mod_name, result) # 6. 保存回滚 + 新版本 rollback_key = f"{CUSTOMIZE_RESUME_ROLLBACK_KEY_PREFIX}{user_id}" await RedisManager.client.set(rollback_key, raw, ex=CUSTOMIZE_RESUME_ROLLBACK_EXPIRE) await self._save_customize_resume(user_id, cr) - label = plan.get("updatedModulesLabel", "简历内容") - return {"type": "updated", "message": f"完成!已更新:{label}"} + # 拼接更新模块标签 + updated_modules = list(dict.fromkeys(op.get("module", "") for op in operations)) + label = "、".join(_MODULE_LABELS.get(m, m) for m in updated_modules if m) + return {"type": "updated", "message": f"完成!已更新:{label or '简历内容'}"} @staticmethod - def _get_module_data(cr: CustomizeResume, mod_name: str) -> str: - """获取指定模块的 JSON 数据""" + def _get_record_data(cr: CustomizeResume, mod_name: str, record_id: str | None) -> str | None: + """获取单条记录的 JSON 数据,resume 主表返回整个对象""" if mod_name == "resume": return cr.resume.model_dump_json(by_alias=True) - mapping = {"education": cr.education, "work": cr.work, "internship": cr.internship, - "project": cr.project, "competition": cr.competition} + mapping = { + "education": cr.education, "work": cr.work, "internship": cr.internship, + "project": cr.project, "competition": cr.competition, + } items = mapping.get(mod_name, []) - return json.dumps([item.model_dump(by_alias=True) for item in items], ensure_ascii=False) + if not record_id: + return None + for item in items: + if item.id == record_id: + return item.model_dump_json(by_alias=True) + log.warning(f"未找到记录[{mod_name}/{record_id}]") + return None @staticmethod - def _apply_edit_result(cr: CustomizeResume, mod_name: str, result) -> None: - """将 AI 编辑结果应用到定制简历""" + def _apply_delete(cr: CustomizeResume, mod_name: str, record_id: str) -> None: + """删除指定模块中的一条记录""" + if not record_id or mod_name == "resume": + return + mapping = { + "education": cr.education, "work": cr.work, "internship": cr.internship, + "project": cr.project, "competition": cr.competition, + } + items = mapping.get(mod_name) + if items is not None: + for i, item in enumerate(items): + if item.id == record_id: + items.pop(i) + break + + @staticmethod + def _apply_record_update(cr: CustomizeResume, mod_name: str, record_id: str | None, result) -> None: + """将 AI 修改结果替换回对应记录""" try: if mod_name == "resume" and isinstance(result, dict): cr.resume = ResumeProfile.model_validate(result) - elif mod_name == "education" and isinstance(result, list): - cr.education = [Education.model_validate(item) for item in result] - elif mod_name == "work" and isinstance(result, list): - cr.work = [Work.model_validate(item) for item in result] - elif mod_name == "internship" and isinstance(result, list): - cr.internship = [Internship.model_validate(item) for item in result] - elif mod_name == "project" and isinstance(result, list): - cr.project = [Project.model_validate(item) for item in result] - elif mod_name == "competition" and isinstance(result, list): - cr.competition = [Competition.model_validate(item) for item in result] + return + model_map = { + "education": Education, "work": Work, "internship": Internship, + "project": Project, "competition": Competition, + } + model_cls = model_map.get(mod_name) + if not model_cls or not isinstance(result, dict) or not record_id: + return + list_map = { + "education": cr.education, "work": cr.work, "internship": cr.internship, + "project": cr.project, "competition": cr.competition, + } + items = list_map.get(mod_name, []) + new_item = model_cls.model_validate(result) + for i, item in enumerate(items): + if item.id == record_id: + items[i] = new_item + break except Exception as e: - log.warning(f"应用AI编辑结果[{mod_name}]失败: {e}") + log.warning(f"应用AI编辑结果[{mod_name}/{record_id}]失败: {e}") + + @staticmethod + def _apply_record_add(cr: CustomizeResume, mod_name: str, result) -> None: + """将 AI 新增的记录追加到对应模块""" + try: + model_map = { + "education": (Education, cr.education), + "work": (Work, cr.work), + "internship": (Internship, cr.internship), + "project": (Project, cr.project), + "competition": (Competition, cr.competition), + } + entry = model_map.get(mod_name) + if not entry or not isinstance(result, dict): + return + model_cls, items = entry + items.append(model_cls.model_validate(result)) + except Exception as e: + log.warning(f"应用AI新增记录[{mod_name}]失败: {e}") # ===== 内部工具方法 =====