diff --git a/.kiro/steering/项目结构说明.md b/.kiro/steering/项目结构说明.md index 38d0df4..60ffd02 100644 --- a/.kiro/steering/项目结构说明.md +++ b/.kiro/steering/项目结构说明.md @@ -79,7 +79,7 @@ offerpie_python_ai/ │ └─ user_job_customize_resume.py # 用户岗位定制简历表(bg_user_job_customize_resume) │ ├─ tool/ # **工具层**(无状态、无业务依赖的通用工具) - │ ├─ file_parser.py # 文件解析工具(PDF/Word/TXT → 纯文本,parse_to_text 入口方法) + │ ├─ file_parser.py # 文件解析工具(PDF/Word(.docx)/TXT/Markdown → 文本单元数组,parse_to_segments 入口方法) │ ├─ json_helper.py # AI 输出 JSON 解析工具(自动去除 markdown 代码块包裹 + json_repair 容错,parse_llm_json 入口方法) │ └─ snowflake.py # 雪花 ID 生成工具(next_id) │ @@ -108,7 +108,7 @@ offerpie_python_ai/ | **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举(models.py)、`model_config.py`(场景模型配置)、`resume_extractor/`(简历并行提取)、`resume_polisher/`(简历段落润色)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化)、`nova_chat/`(Nova 对话助手,纯对话) | | **api** | REST API 路由定义 | `health.py`(健康检查)、`resume.py`(简历上传解析 + 段落润色)、`resume_diagnose.py`(简历诊断)、`skill_gap.py`(技能差距分析 + 生成定制简历 + AI对话编辑)、`customize_resume.py`(定制简历查询/修改/回滚)、`job_agent_chat.py`(求职助手对话 + 岗位简历优化)、`nova_chat.py`(Nova 对话助手) | | **models** | SQLAlchemy ORM 模型,与 Java 端共享同一数据库 | `FuncPermission`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`UserResume`、`UserResumeEducation`/`Work`/`Internship`/`Project`/`Competition`、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`Job`(只读)、`JobAgentConfig`、`UserJobCustomizeResume` | -| **tool** | 无状态通用工具,不依赖数据库/Redis/用户上下文 | `file_parser.py`(PDF/Word/TXT 文件解析为纯文本)、`json_helper.py`(AI 输出 JSON 解析,去 markdown 代码块 + json_repair 容错)、`snowflake.py`(雪花ID生成) | +| **tool** | 无状态通用工具,不依赖数据库/Redis/用户上下文 | `file_parser.py`(PDF/Word(.docx)/TXT/Markdown 文件解析为文本单元数组)、`json_helper.py`(AI 输出 JSON 解析,去 markdown 代码块 + json_repair 容错)、`snowflake.py`(雪花ID生成) | | **services** | 业务逻辑实现 | `FuncPermissionService`(功能权限校验、扣减、回退)、`ResumeService`(简历文件解析→AI结构化→入库 + 段落润色)、`ResumeDiagnoseService`(简历诊断→AI并行分析→评级→入库)、`SkillGapService`(技能差距分析→定制简历生成→AI对话编辑)、`resume_loader`(简历统一查询,返回ResumeDetail)、`customize_resume_store`(定制简历数据库存取+数据构建,按用户+岗位维度,Redis回滚备份)、`JobAgentChatService`(求职助手对话+岗位简历优化)、`NovaChatService`(Nova对话助手,查简历+查岗位→调AI) | ## 3️⃣ 技术栈 diff --git a/app/tool/file_parser.py b/app/tool/file_parser.py index ebf3489..3c76660 100644 --- a/app/tool/file_parser.py +++ b/app/tool/file_parser.py @@ -1,14 +1,10 @@ """文件解析工具 -将上传的简历文件(PDF / Word / TXT)转换为「文本单元数组」或纯文本字符串。 +将上传的简历文件解析为「文本单元数组」,供 AI 按行号定位 description/summary,避免照抄长文本。 - PDF:使用 LiteParse(JSON 模式)按文本块的阅读顺序提取,每个文本块作为一个单元。 关闭 OCR —— 文本型简历无需 OCR,且 OCR 默认会联网下载字库导致严重阻塞。 -- Word:按段落(paragraph)切分,表格行用 \t 拼成一个单元。 -- TXT:按换行切分。 -单元数组用于 AI 提取时按行号定位 description/summary,避免 AI 照抄长文本。 - -另提供 parse_to_markdown:基于 LiteParse 的 Markdown 结构化输出(保留标题/列表/加粗等 -语义层级),可用于需要整篇结构化文本的 AI 场景。 +- Word(.docx):按段落(paragraph)切分,表格行用 \t 拼成一个单元。 +- TXT / Markdown(.md):自动检测编码,按换行切分。 """ import io @@ -20,8 +16,7 @@ from liteparse import LiteParse from app.core.logger import log # LiteParse 解析器复用实例(关闭 OCR、静默日志)。线程安全由调用方的 to_thread 串行保证。 -_PDF_SEGMENT_PARSER = LiteParse(output_format="json", ocr_enabled=False, quiet=True) -_PDF_MARKDOWN_PARSER = LiteParse(output_format="markdown", ocr_enabled=False, quiet=True) +_PDF_PARSER = LiteParse(output_format="json", ocr_enabled=False, quiet=True) def _drop_repeated(segments: list[str], threshold: int = 3) -> list[str]: @@ -35,7 +30,7 @@ def _pdf_segments(content: bytes) -> list[str]: 不在预处理阶段合并行——是否合并/分段/排除噪声全部交给 AI 的行号区间决定。 """ - result = _PDF_SEGMENT_PARSER.parse(content) + result = _PDF_PARSER.parse(content) segments: list[str] = [] for page in result.pages: for item in page.text_items: @@ -50,7 +45,7 @@ def _docx_segments(content: bytes) -> list[str]: try: doc = Document(io.BytesIO(content)) except Exception: - raise ValueError("无法解析该 Word 文件,如果是旧版 .doc 格式,请另存为 .docx 后重试") + raise ValueError("无法解析该 Word 文件,请确认为有效的 .docx 格式") segments: list[str] = [] for para in doc.paragraphs: text = para.text.strip() @@ -65,7 +60,7 @@ def _docx_segments(content: bytes) -> list[str]: def _txt_segments(content: bytes) -> list[str]: - """解析 TXT,自动检测编码,按换行切分为单元""" + """解析纯文本 (.txt / .md),自动检测编码,按换行切分为单元""" text = None for encoding in ("utf-8", "gbk", "gb2312", "latin-1"): try: @@ -78,38 +73,20 @@ def _txt_segments(content: bytes) -> list[str]: return [line.strip() for line in text.splitlines() if line.strip()] +# 后缀 → 解析函数。新增格式只需在此登记一行。 +_PARSERS = { + ".pdf": _pdf_segments, + ".docx": _docx_segments, + ".txt": _txt_segments, + ".md": _txt_segments, +} + + def parse_to_segments(filename: str, content: bytes) -> list[str]: - """根据文件名后缀自动选择解析方法,返回「文本单元数组」""" + """根据文件名后缀选择解析器,返回去重后的「文本单元数组」""" suffix = filename[filename.rfind("."):].lower() if "." in filename else "" log.info(f"解析文件: {filename},类型: {suffix}") - if suffix == ".pdf": - segments = _pdf_segments(content) - elif suffix in (".docx", ".doc"): - segments = _docx_segments(content) - elif suffix == ".txt": - segments = _txt_segments(content) - else: - raise ValueError(f"不支持的文件类型: {suffix},支持: .pdf, .docx, .doc, .txt") - return _drop_repeated(segments) - - -def parse_to_text(filename: str, content: bytes) -> str: - """根据文件名后缀自动选择解析方法,返回纯文本(单元用换行拼接)""" - return "\n".join(parse_to_segments(filename, content)) - - -def parse_to_markdown(filename: str, content: bytes) -> str: - """将简历解析为 Markdown 结构化文本(保留标题/列表/加粗等语义层级)。 - - PDF 使用 LiteParse 的 Markdown 输出;Word/TXT 无版式信息,退化为换行拼接的纯文本。 - 适用于需要整篇结构化文本喂给 LLM 的场景。 - """ - suffix = filename[filename.rfind("."):].lower() if "." in filename else "" - log.info(f"解析文件(markdown): {filename},类型: {suffix}") - if suffix == ".pdf": - return _PDF_MARKDOWN_PARSER.parse(content).text - if suffix in (".docx", ".doc"): - return "\n".join(_drop_repeated(_docx_segments(content))) - if suffix == ".txt": - return "\n".join(_drop_repeated(_txt_segments(content))) - raise ValueError(f"不支持的文件类型: {suffix},支持: .pdf, .docx, .doc, .txt") + parser = _PARSERS.get(suffix) + if parser is None: + raise ValueError(f"不支持的文件类型: {suffix},支持: {', '.join(_PARSERS)}") + return _drop_repeated(parser(content))