From 6701df613b0c5701a4be9e1066153b77a7afd107 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Tue, 14 Apr 2026 21:16:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E5=A1=AB=E5=86=99=E5=92=8C=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 Word 文档表格模板解析功能,支持从 .docx 文件中提取字段定义 - 新增源文档字段提示词(hint)功能,提升数据提取准确性 - 支持多种方式指定源文档:MongoDB 文档 ID 列表或文件路径列表 - 增强模板字段类型推断机制,支持从提示词和示例值自动识别 - 实现 Excel 和 Word 格式导出功能,提供多种导出选项 - 重构模板填写服务,优化上下文构建和文档加载逻辑 - 更新前端 API 接口,支持传递源文档参数和字段提示词 - --- .gitignore | 24 +- Q&A.xlsx | Bin 18694 -> 0 bytes logs/docx_parser_and_template_fill.patch | 854 ----------------------- logs/frontend_template_fill.patch | 53 -- logs/planning_doc.patch | 221 ------ logs/rag_disable_note.txt | 59 -- logs/template_fill_feature_changes.md | 144 ---- 比赛备赛规划.md | 558 --------------- 8 files changed, 17 insertions(+), 1896 deletions(-) delete mode 100644 Q&A.xlsx delete mode 100644 logs/docx_parser_and_template_fill.patch delete mode 100644 logs/frontend_template_fill.patch delete mode 100644 logs/planning_doc.patch delete mode 100644 logs/rag_disable_note.txt delete mode 100644 logs/template_fill_feature_changes.md delete mode 100644 比赛备赛规划.md diff --git a/.gitignore b/.gitignore index 4c224b9..3255644 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.git/ +/.gitignore /.idea/ /.vscode/ /backend/venv/ @@ -18,11 +19,7 @@ /frontend/.idea/ /frontend/.env /frontend/*.log -/技术路线.md -/开发路径.md -/开发日志_2026-03-16.md -/frontendTest/ -/docs/ + /frontend/src/api/ /frontend/src/api/index.js /frontend/src/api/index.ts @@ -30,9 +27,22 @@ /frontend/src/api/index.py /frontend/src/api/index.go /frontend/src/api/index.java + +/frontend - 副本/ + /docs/ -/frontend - 副本/* +/frontendTest/ /supabase.txt -**/__pycache__/* +# 取消跟踪的文件 / Untracked files +比赛备赛规划.md +Q&A.xlsx +package.json +技术路线.md +开发路径.md +开发日志_2026-03-16.md +/logs/ + +# Python cache +**/__pycache__/** **.pyc diff --git a/Q&A.xlsx b/Q&A.xlsx deleted file mode 100644 index f5e555730fd7e6e5e2cbe6efa1ee5ab140bb41a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18694 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�eIza7FZo!FYqU=vx z_m*-iOB7PF-;aNa>hpoUd)g@%hym(Ron>I-={74~h1V`2oNqk?2oxCcFCzf>rDluE z^9h8B5dJ8S*P+0lzyBdZ7{h|-T%SM?)qBT^G^H)Ve0m6U2DP9P;(yn)-qhRw!||1%LpZMR)`_L-a!=vly3{7a2V3 zhR0%sfihC_?LE`w{psWbcW3K7iK!;K8RT@bEoYvN;~7Z#8my#^6Hw7)O~bC4!&UNf zr1T*v1r<91lU^q%v+`#F@XLcc?=b-*o9B!`JDDNvI|jTLT`9(D9gC%Qd9i4aD?3~M z7!LUKYW(TJA-j@*{0r9t?{x=((9vL;qRtNiekOoZ{{(1pW}9RI$Y6|k0GIqR{zvV+ zC##Iw2uTBkjQn=+wH@N+sO40kFTCN)qfoWt*|c&8Ipe1*%Ndb?(kQVO@I=UtzqFEE zYKOZvWJSg#`xN~23|}E@^`|SqN=P2_fmL>1q<@+lY;%0PE0*J3LiFNgizSLRhtnPX z@KyJDl~LKp`G$L^_#{4N>$-``1Q5VYoMR@mipG7TlFzCzRDRgElR(EH`X}*qGM|~12b2?jGbaz6we+5 za&Bd?zRAZ+m|kMyU3uat54;mCJ(R3nas*YxIV~Q640PV z)&)qI(H51-uz{L=4)We~K&?W4 z9rij#IlSEygz*hMEv^hndoNod7~yW?Be3Ur2v&SlhSG}~vMo;W+|g6(V0i6ur9sZE z7r~`qAfSoFQ?D(+=WjpkzaH;Hcm^|H@113^`vzm#Cg<;Qh+eqRxW3KM!Kl z|A*e?-fPERk8lLoCZs`FP%$4YzjfEq8I zgtT0CB`T_IgrrI+mN8E;FCkSbXC833W!RlewzQ?kAXUm>r;s z>=_fu2)SVHs*O(*-OtD7Yo8k!|FGw&wdM~d_SrjzpGr6QPhF6nrLL5XrIjtUuBDCs zAN!u4^3X5;D+dAqaEsA2^`$}T+W>okiE$LyF;(TKeBslq@7BV?aD^N)$0(7GF`KvS zC@Qb9JEeU{d$k!S8oE~6U1v5T5RJ!2huihu&W|!l%xM@K*2JAqDgYi41^I5$nWgXG z8hc5$pC5ptu8>s@yu%tpM_VCN+fW}u%j5h+{93-ac`UjmI4(l~Y4{zo5H4ah}b zmpg+E&AdIm+mj`B^g6WnUNO09cenrOQZF38pHly zYt0P^z3#TgZ3$R)7WtKuKS@HmO!i?tztR;DPIfNWg7=$@cv1AYr_GyGX` zJaBkQD)ZY>o9`IeX(>N2OVSMrK+UC-q2tIx!{tkw& z-gj_(cZh@*w%x}IREHOt%AkzDtRU^DdbeO7-cUqbcVIZLNKeX34h=e0Yx|NHAE(5O zEQ~5}A|GcFOhB1!+WccjLDK@XrW&Q=w=besbtPp}UyVhcdJZ>pscD|-=d?gBLVXUA zvn+kO(#1e73U#vM{cY(I{Hx)CJ--K`6&V=Y>hs3s^czE?y9?z!iJM~7>=uShZt9gO zbdd)7&DGiqW2}(rL`lLvc;Z-QUc7@#xG0+Tc$jiAW$xvfXKglC(H^o0u@I}F*TPkV zG<|)9xy*xIrL$|q8qWdll+L{1UKZJj%#Wz$l5Zi|b``i?Yt+1IR(Xc>uhe|BmZlK- zhFge%1!F@Rgm(60v46K!&I0PENkXRCt%CS*#Pq+ZDje|bF8ib2 z(DjqmpC@DdEvVYt+F6?aFRA}8r2zoaqDCZoXy61MgkF93+H?mK+Y=7ZLBP_6aB?d=NE zt`?lXBm`1pUQj$8QYCShqx15k$qpDmRrj~v{8=0rMYKeyU#kI2TlemD#zIErI$}sE z-WwOCI(h(CE_ehQi+JIe0`#($yoqW_8Vd%5xN0KXTd z{RJ?E`zOF(C2GH${$988m+2AVzf6CxV)-56_x$-Ugk1LD``{n5>E8kWGu`|P4gjE! z0|4N^B%i;V|7WW3cXJ``znT9#aroW(Kg06ht StreamingResponse: -+ """导出为 Excel 格式""" -+ # 将字典转换为单行 DataFrame -+ df = pd.DataFrame([filled_data]) - -- return StreamingResponse( -- io.BytesIO(output.getvalue()), -- media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", -- headers={"Content-Disposition": f"attachment; filename={filename}"} -- ) -+ output = io.BytesIO() -+ with pd.ExcelWriter(output, engine='openpyxl') as writer: -+ df.to_excel(writer, index=False, sheet_name='填写结果') - -- except Exception as e: -- logger.error(f"导出失败: {str(e)}") -- raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}") -+ output.seek(0) -+ -+ filename = f"filled_template.xlsx" -+ -+ return StreamingResponse( -+ io.BytesIO(output.getvalue()), -+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", -+ headers={"Content-Disposition": f"attachment; filename={filename}"} -+ ) -+ -+ -+async def _export_to_word(filled_data: dict, template_id: str) -> StreamingResponse: -+ """导出为 Word 格式""" -+ from docx import Document -+ from docx.shared import Pt, RGBColor -+ from docx.enum.text import WD_ALIGN_PARAGRAPH -+ -+ doc = Document() -+ -+ # 添加标题 -+ title = doc.add_heading('填写结果', level=1) -+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER -+ -+ # 添加填写时间和模板信息 -+ from datetime import datetime -+ info_para = doc.add_paragraph() -+ info_para.add_run(f"模板ID: {template_id}\n").bold = True -+ info_para.add_run(f"导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") -+ -+ doc.add_paragraph() # 空行 -+ -+ # 添加字段表格 -+ table = doc.add_table(rows=1, cols=3) -+ table.style = 'Light Grid Accent 1' -+ -+ # 表头 -+ header_cells = table.rows[0].cells -+ header_cells[0].text = '字段名' -+ header_cells[1].text = '填写值' -+ header_cells[2].text = '状态' -+ -+ for field_name, field_value in filled_data.items(): -+ row_cells = table.add_row().cells -+ row_cells[0].text = field_name -+ row_cells[1].text = str(field_value) if field_value else '' -+ row_cells[2].text = '已填写' if field_value else '为空' -+ -+ # 保存到 BytesIO -+ output = io.BytesIO() -+ doc.save(output) -+ output.seek(0) -+ -+ filename = f"filled_template.docx" -+ -+ return StreamingResponse( -+ io.BytesIO(output.getvalue()), -+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", -+ headers={"Content-Disposition": f"attachment; filename={filename}"} -+ ) -+ -+ -+@router.post("/export/excel") -+async def export_to_excel( -+ filled_data: dict, -+ template_id: str = Query(..., description="模板ID") -+): -+ """ -+ 专门导出为 Excel 格式 -+ -+ Args: -+ filled_data: 填写数据 -+ template_id: 模板ID -+ -+ Returns: -+ Excel 文件流 -+ """ -+ return await _export_to_excel(filled_data, template_id) -+ -+ -+@router.post("/export/word") -+async def export_to_word( -+ filled_data: dict, -+ template_id: str = Query(..., description="模板ID") -+): -+ """ -+ 专门导出为 Word 格式 -+ -+ Args: -+ filled_data: 填写数据 -+ template_id: 模板ID -+ -+ Returns: -+ Word 文件流 -+ """ -+ return await _export_to_word(filled_data, template_id) -diff --git a/backend/app/core/document_parser/docx_parser.py b/backend/app/core/document_parser/docx_parser.py -index 75e79da..03c341d 100644 ---- a/backend/app/core/document_parser/docx_parser.py -+++ b/backend/app/core/document_parser/docx_parser.py -@@ -161,3 +161,133 @@ class DocxParser(BaseParser): - fields[field_name] = match.group(1) - - return fields -+ -+ def parse_tables_for_template( -+ self, -+ file_path: str -+ ) -> Dict[str, Any]: -+ """ -+ 解析 Word 文档中的表格,提取模板字段 -+ -+ 专门用于比赛场景:解析表格模板,识别需要填写的字段 -+ -+ Args: -+ file_path: Word 文件路径 -+ -+ Returns: -+ 包含表格字段信息的字典 -+ """ -+ from docx import Document -+ from docx.table import Table -+ from docx.oxml.ns import qn -+ -+ doc = Document(file_path) -+ -+ template_info = { -+ "tables": [], -+ "fields": [], -+ "field_count": 0 -+ } -+ -+ for table_idx, table in enumerate(doc.tables): -+ table_info = { -+ "table_index": table_idx, -+ "rows": [], -+ "headers": [], -+ "data_rows": [], -+ "field_hints": {} # 字段名称 -> 提示词/描述 -+ } -+ -+ # 提取表头(第一行) -+ if table.rows: -+ header_cells = [cell.text.strip() for cell in table.rows[0].cells] -+ table_info["headers"] = header_cells -+ -+ # 提取数据行 -+ for row_idx, row in enumerate(table.rows[1:], 1): -+ row_data = [cell.text.strip() for cell in row.cells] -+ table_info["data_rows"].append(row_data) -+ table_info["rows"].append({ -+ "row_index": row_idx, -+ "cells": row_data -+ }) -+ -+ # 尝试从第二列/第三列提取提示词 -+ # 比赛模板通常格式为:字段名 | 提示词 | 填写值 -+ if len(table.rows[0].cells) >= 2: -+ for row_idx, row in enumerate(table.rows[1:], 1): -+ cells = [cell.text.strip() for cell in row.cells] -+ if len(cells) >= 2 and cells[0]: -+ # 第一列是字段名 -+ field_name = cells[0] -+ # 第二列可能是提示词或描述 -+ hint = cells[1] if len(cells) > 1 else "" -+ table_info["field_hints"][field_name] = hint -+ -+ template_info["fields"].append({ -+ "table_index": table_idx, -+ "row_index": row_idx, -+ "field_name": field_name, -+ "hint": hint, -+ "expected_value": cells[2] if len(cells) > 2 else "" -+ }) -+ -+ template_info["tables"].append(table_info) -+ -+ template_info["field_count"] = len(template_info["fields"]) -+ return template_info -+ -+ def extract_template_fields_from_docx( -+ self, -+ file_path: str -+ ) -> List[Dict[str, Any]]: -+ """ -+ 从 Word 文档中提取模板字段定义 -+ -+ 适用于比赛评分表格:表格第一列是字段名,第二列是提示词/填写示例 -+ -+ Args: -+ file_path: Word 文件路径 -+ -+ Returns: -+ 字段定义列表 -+ """ -+ template_info = self.parse_tables_for_template(file_path) -+ -+ fields = [] -+ for field in template_info["fields"]: -+ fields.append({ -+ "cell": f"T{field['table_index']}R{field['row_index']}", # TableXRowY 格式 -+ "name": field["field_name"], -+ "hint": field["hint"], -+ "table_index": field["table_index"], -+ "row_index": field["row_index"], -+ "field_type": self._infer_field_type_from_hint(field["hint"]), -+ "required": True -+ }) -+ -+ return fields -+ -+ def _infer_field_type_from_hint(self, hint: str) -> str: -+ """ -+ 从提示词推断字段类型 -+ -+ Args: -+ hint: 字段提示词 -+ -+ Returns: -+ 字段类型 (text/number/date) -+ """ -+ hint_lower = hint.lower() -+ -+ # 日期关键词 -+ date_keywords = ["年", "月", "日", "日期", "时间", "出生"] -+ if any(kw in hint for kw in date_keywords): -+ return "date" -+ -+ # 数字关键词 -+ number_keywords = ["数量", "金额", "人数", "面积", "增长", "比率", "%", "率"] -+ if any(kw in hint_lower for kw in number_keywords): -+ return "number" -+ -+ return "text" -diff --git a/backend/app/services/template_fill_service.py b/backend/app/services/template_fill_service.py -index 2612354..94930fb 100644 ---- a/backend/app/services/template_fill_service.py -+++ b/backend/app/services/template_fill_service.py -@@ -4,13 +4,12 @@ - 从非结构化文档中检索信息并填写到表格模板 - """ - import logging --from dataclasses import dataclass -+from dataclasses import dataclass, field - from typing import Any, Dict, List, Optional - - from app.core.database import mongodb --from app.services.rag_service import rag_service - from app.services.llm_service import llm_service --from app.services.excel_storage_service import excel_storage_service -+from app.core.document_parser import ParserFactory - - logger = logging.getLogger(__name__) - -@@ -22,6 +21,17 @@ class TemplateField: - name: str # 字段名称 - field_type: str = "text" # 字段类型: text/number/date - required: bool = True -+ hint: str = "" # 字段提示词 -+ -+ -+@dataclass -+class SourceDocument: -+ """源文档""" -+ doc_id: str -+ filename: str -+ doc_type: str -+ content: str = "" -+ structured_data: Dict[str, Any] = field(default_factory=dict) - - - @dataclass -@@ -38,12 +48,12 @@ class TemplateFillService: - - def __init__(self): - self.llm = llm_service -- self.rag = rag_service - - async def fill_template( - self, - template_fields: List[TemplateField], - source_doc_ids: Optional[List[str]] = None, -+ source_file_paths: Optional[List[str]] = None, - user_hint: Optional[str] = None - ) -> Dict[str, Any]: - """ -@@ -51,7 +61,8 @@ class TemplateFillService: - - Args: - template_fields: 模板字段列表 -- source_doc_ids: 源文档ID列表,不指定则从所有文档检索 -+ source_doc_ids: 源文档 MongoDB ID 列表 -+ source_file_paths: 源文档文件路径列表 - user_hint: 用户提示(如"请从合同文档中提取") - - Returns: -@@ -60,28 +71,23 @@ class TemplateFillService: - filled_data = {} - fill_details = [] - -+ # 1. 加载源文档内容 -+ source_docs = await self._load_source_documents(source_doc_ids, source_file_paths) -+ -+ if not source_docs: -+ logger.warning("没有找到源文档,填表结果将全部为空") -+ -+ # 2. 对每个字段进行提取 - for field in template_fields: - try: -- # 1. 从 RAG 检索相关上下文 -- rag_results = await self._retrieve_context(field.name, user_hint) -- -- if not rag_results: -- # 如果没有检索到结果,尝试直接询问 LLM -- result = FillResult( -- field=field.name, -- value="", -- source="未找到相关数据", -- confidence=0.0 -- ) -- else: -- # 2. 构建 Prompt 让 LLM 提取信息 -- result = await self._extract_field_value( -- field=field, -- rag_context=rag_results, -- user_hint=user_hint -- ) -- -- # 3. 存储结果 -+ # 从源文档中提取字段值 -+ result = await self._extract_field_value( -+ field=field, -+ source_docs=source_docs, -+ user_hint=user_hint -+ ) -+ -+ # 存储结果 - filled_data[field.name] = result.value - fill_details.append({ - "field": field.name, -@@ -107,75 +113,113 @@ class TemplateFillService: - return { - "success": True, - "filled_data": filled_data, -- "fill_details": fill_details -+ "fill_details": fill_details, -+ "source_doc_count": len(source_docs) - } - -- async def _retrieve_context( -+ async def _load_source_documents( - self, -- field_name: str, -- user_hint: Optional[str] = None -- ) -> List[Dict[str, Any]]: -+ source_doc_ids: Optional[List[str]] = None, -+ source_file_paths: Optional[List[str]] = None -+ ) -> List[SourceDocument]: - """ -- 从 RAG 检索相关上下文 -+ 加载源文档内容 - - Args: -- field_name: 字段名称 -- user_hint: 用户提示 -+ source_doc_ids: MongoDB 文档 ID 列表 -+ source_file_paths: 源文档文件路径列表 - - Returns: -- 检索结果列表 -+ 源文档列表 - """ -- # 构建查询文本 -- query = field_name -- if user_hint: -- query = f"{user_hint} {field_name}" -- -- # 检索相关文档片段 -- results = self.rag.retrieve(query=query, top_k=5) -- -- return results -+ source_docs = [] -+ -+ # 1. 从 MongoDB 加载文档 -+ if source_doc_ids: -+ for doc_id in source_doc_ids: -+ try: -+ doc = await mongodb.get_document(doc_id) -+ if doc: -+ source_docs.append(SourceDocument( -+ doc_id=doc_id, -+ filename=doc.get("metadata", {}).get("original_filename", "unknown"), -+ doc_type=doc.get("doc_type", "unknown"), -+ content=doc.get("content", ""), -+ structured_data=doc.get("structured_data", {}) -+ )) -+ logger.info(f"从MongoDB加载文档: {doc_id}") -+ except Exception as e: -+ logger.error(f"从MongoDB加载文档失败 {doc_id}: {str(e)}") -+ -+ # 2. 从文件路径加载文档 -+ if source_file_paths: -+ for file_path in source_file_paths: -+ try: -+ parser = ParserFactory.get_parser(file_path) -+ result = parser.parse(file_path) -+ if result.success: -+ source_docs.append(SourceDocument( -+ doc_id=file_path, -+ filename=result.metadata.get("filename", file_path.split("/")[-1]), -+ doc_type=result.metadata.get("extension", "unknown").replace(".", ""), -+ content=result.data.get("content", ""), -+ structured_data=result.data.get("structured_data", {}) -+ )) -+ logger.info(f"从文件加载文档: {file_path}") -+ except Exception as e: -+ logger.error(f"从文件加载文档失败 {file_path}: {str(e)}") -+ -+ return source_docs - - async def _extract_field_value( - self, - field: TemplateField, -- rag_context: List[Dict[str, Any]], -+ source_docs: List[SourceDocument], - user_hint: Optional[str] = None - ) -> FillResult: - """ -- 使用 LLM 从上下文中提取字段值 -+ 使用 LLM 从源文档中提取字段值 - - Args: - field: 字段定义 -- rag_context: RAG 检索到的上下文 -+ source_docs: 源文档列表 - user_hint: 用户提示 - - Returns: - 提取结果 - """ -+ if not source_docs: -+ return FillResult( -+ field=field.name, -+ value="", -+ source="无源文档", -+ confidence=0.0 -+ ) -+ - # 构建上下文文本 -- context_text = "\n\n".join([ -- f"【文档 {i+1}】\n{doc['content']}" -- for i, doc in enumerate(rag_context) -- ]) -+ context_text = self._build_context_text(source_docs, max_length=8000) -+ -+ # 构建提示词 -+ hint_text = field.hint if field.hint else f"请提取{field.name}的信息" -+ if user_hint: -+ hint_text = f"{user_hint}。{hint_text}" - -- # 构建 Prompt -- prompt = f"""你是一个数据提取专家。请根据以下文档内容,提取指定字段的信息。 -+ prompt = f"""你是一个专业的数据提取专家。请根据以下文档内容,提取指定字段的信息。 - - 需要提取的字段: - - 字段名称:{field.name} - - 字段类型:{field.field_type} -+- 填写提示:{hint_text} - - 是否必填:{'是' if field.required else '否'} - --{'用户提示:' + user_hint if user_hint else ''} -- - 参考文档内容: - {context_text} - - 请严格按照以下 JSON 格式输出,不要添加任何解释: - {{ - "value": "提取到的值,如果没有找到则填写空字符串", -- "source": "数据来源的文档描述", -- "confidence": 0.0到1.0之间的置信度 -+ "source": "数据来源的文档描述(如:来自xxx文档)", -+ "confidence": 0.0到1.0之间的置信度,表示对提取结果的信心程度" - }} - """ - -@@ -226,6 +270,54 @@ class TemplateFillService: - confidence=0.0 - ) - -+ def _build_context_text(self, source_docs: List[SourceDocument], max_length: int = 8000) -> str: -+ """ -+ 构建上下文文本 -+ -+ Args: -+ source_docs: 源文档列表 -+ max_length: 最大字符数 -+ -+ Returns: -+ 上下文文本 -+ """ -+ contexts = [] -+ total_length = 0 -+ -+ for doc in source_docs: -+ # 优先使用结构化数据(表格),其次使用文本内容 -+ doc_content = "" -+ -+ if doc.structured_data and doc.structured_data.get("tables"): -+ # 如果有表格数据,优先使用 -+ tables = doc.structured_data.get("tables", []) -+ for table in tables: -+ if isinstance(table, dict): -+ rows = table.get("rows", []) -+ if rows: -+ doc_content += f"\n【文档: {doc.filename} 表格数据】\n" -+ for row in rows[:20]: # 限制每表最多20行 -+ if isinstance(row, list): -+ doc_content += " | ".join(str(cell) for cell in row) + "\n" -+ elif isinstance(row, dict): -+ doc_content += " | ".join(str(v) for v in row.values()) + "\n" -+ elif doc.content: -+ doc_content = doc.content[:5000] # 限制文本长度 -+ -+ if doc_content: -+ doc_context = f"【文档: {doc.filename} ({doc.doc_type})】\n{doc_content}" -+ if total_length + len(doc_context) <= max_length: -+ contexts.append(doc_context) -+ total_length += len(doc_context) -+ else: -+ # 如果超出长度,截断 -+ remaining = max_length - total_length -+ if remaining > 100: -+ contexts.append(doc_context[:remaining]) -+ break -+ -+ return "\n\n".join(contexts) if contexts else "(源文档内容为空)" -+ - async def get_template_fields_from_file( - self, - file_path: str, -@@ -236,7 +328,7 @@ class TemplateFillService: - - Args: - file_path: 模板文件路径 -- file_type: 文件类型 -+ file_type: 文件类型 (xlsx/xls/docx) - - Returns: - 字段列表 -@@ -245,43 +337,108 @@ class TemplateFillService: - - try: - if file_type in ["xlsx", "xls"]: -- # 从 Excel 读取表头 -- import pandas as pd -- df = pd.read_excel(file_path, nrows=5) -+ fields = await self._get_template_fields_from_excel(file_path) -+ elif file_type == "docx": -+ fields = await self._get_template_fields_from_docx(file_path) - -- for idx, col in enumerate(df.columns): -- # 获取单元格位置 (A, B, C, ...) -- cell = self._column_to_cell(idx) -+ except Exception as e: -+ logger.error(f"提取模板字段失败: {str(e)}") - -- fields.append(TemplateField( -- cell=cell, -- name=str(col), -- field_type=self._infer_field_type(df[col]), -- required=True -- )) -+ return fields - -- elif file_type == "docx": -- # 从 Word 表格读取 -- from docx import Document -- doc = Document(file_path) -- -- for table_idx, table in enumerate(doc.tables): -- for row_idx, row in enumerate(table.rows): -- for col_idx, cell in enumerate(row.cells): -- cell_text = cell.text.strip() -- if cell_text: -- fields.append(TemplateField( -- cell=self._column_to_cell(col_idx), -- name=cell_text, -- field_type="text", -- required=True -- )) -+ async def _get_template_fields_from_excel(self, file_path: str) -> List[TemplateField]: -+ """从 Excel 模板提取字段""" -+ fields = [] -+ -+ try: -+ import pandas as pd -+ df = pd.read_excel(file_path, nrows=5) -+ -+ for idx, col in enumerate(df.columns): -+ cell = self._column_to_cell(idx) -+ col_str = str(col) -+ -+ fields.append(TemplateField( -+ cell=cell, -+ name=col_str, -+ field_type=self._infer_field_type_from_value(df[col].iloc[0] if len(df) > 0 else ""), -+ required=True, -+ hint="" -+ )) - - except Exception as e: -- logger.error(f"提取模板字段失败: {str(e)}") -+ logger.error(f"从Excel提取字段失败: {str(e)}") - - return fields - -+ async def _get_template_fields_from_docx(self, file_path: str) -> List[TemplateField]: -+ """从 Word 模板提取字段""" -+ fields = [] -+ -+ try: -+ from docx import Document -+ -+ doc = Document(file_path) -+ -+ for table_idx, table in enumerate(doc.tables): -+ for row_idx, row in enumerate(table.rows): -+ cells = [cell.text.strip() for cell in row.cells] -+ -+ # 假设第一列是字段名 -+ if cells and cells[0]: -+ field_name = cells[0] -+ hint = cells[1] if len(cells) > 1 else "" -+ -+ # 跳过空行或标题行 -+ if field_name and field_name not in ["", "字段名", "名称", "项目"]: -+ fields.append(TemplateField( -+ cell=f"T{table_idx}R{row_idx}", -+ name=field_name, -+ field_type=self._infer_field_type_from_hint(hint), -+ required=True, -+ hint=hint -+ )) -+ -+ except Exception as e: -+ logger.error(f"从Word提取字段失败: {str(e)}") -+ -+ return fields -+ -+ def _infer_field_type_from_hint(self, hint: str) -> str: -+ """从提示词推断字段类型""" -+ hint_lower = hint.lower() -+ -+ date_keywords = ["年", "月", "日", "日期", "时间", "出生"] -+ if any(kw in hint for kw in date_keywords): -+ return "date" -+ -+ number_keywords = ["数量", "金额", "人数", "面积", "增长", "比率", "%", "率", "总计", "合计"] -+ if any(kw in hint_lower for kw in number_keywords): -+ return "number" -+ -+ return "text" -+ -+ def _infer_field_type_from_value(self, value: Any) -> str: -+ """从示例值推断字段类型""" -+ if value is None or value == "": -+ return "text" -+ -+ value_str = str(value) -+ -+ # 检查日期模式 -+ import re -+ if re.search(r'\d{4}[年/-]\d{1,2}[月/-]\d{1,2}', value_str): -+ return "date" -+ -+ # 检查数值 -+ try: -+ float(value_str.replace(',', '').replace('%', '')) -+ return "number" -+ except ValueError: -+ pass -+ -+ return "text" -+ - def _column_to_cell(self, col_idx: int) -> str: - """将列索引转换为单元格列名 (0 -> A, 1 -> B, ...)""" - result = "" -@@ -290,17 +447,6 @@ class TemplateFillService: - col_idx = col_idx // 26 - 1 - return result - -- def _infer_field_type(self, series) -> str: -- """推断字段类型""" -- import pandas as pd -- -- if pd.api.types.is_numeric_dtype(series): -- return "number" -- elif pd.api.types.is_datetime64_any_dtype(series): -- return "date" -- else: -- return "text" -- - - # ==================== 全局单例 ==================== - diff --git a/logs/frontend_template_fill.patch b/logs/frontend_template_fill.patch deleted file mode 100644 index 378b6b5..0000000 --- a/logs/frontend_template_fill.patch +++ /dev/null @@ -1,53 +0,0 @@ -diff --git a/frontend/src/db/backend-api.ts b/frontend/src/db/backend-api.ts -index 8944353..94ac852 100644 ---- a/frontend/src/db/backend-api.ts -+++ b/frontend/src/db/backend-api.ts -@@ -92,6 +92,7 @@ export interface TemplateField { - name: string; - field_type: string; - required: boolean; -+ hint?: string; - } - - // 表格填写结果 -@@ -625,7 +626,10 @@ export const backendApi = { - */ - async fillTemplate( - templateId: string, -- templateFields: TemplateField[] -+ templateFields: TemplateField[], -+ sourceDocIds?: string[], -+ sourceFilePaths?: string[], -+ userHint?: string - ): Promise { - const url = `${BACKEND_BASE_URL}/templates/fill`; - -@@ -636,6 +640,9 @@ export const backendApi = { - body: JSON.stringify({ - template_id: templateId, - template_fields: templateFields, -+ source_doc_ids: sourceDocIds || [], -+ source_file_paths: sourceFilePaths || [], -+ user_hint: userHint || null, - }), - }); - -diff --git a/frontend/src/pages/TemplateFill.tsx b/frontend/src/pages/TemplateFill.tsx -index 8c330a9..f9a4a39 100644 ---- a/frontend/src/pages/TemplateFill.tsx -+++ b/frontend/src/pages/TemplateFill.tsx -@@ -128,8 +128,12 @@ const TemplateFill: React.FC = () => { - setStep('filling'); - - try { -- // 调用后端填表接口 -- const result = await backendApi.fillTemplate('temp-template-id', templateFields); -+ // 调用后端填表接口,传递选中的文档ID -+ const result = await backendApi.fillTemplate( -+ 'temp-template-id', -+ templateFields, -+ selectedDocs // 传递源文档ID列表 -+ ); - setFilledResult(result); - setStep('preview'); - toast.success('表格填写完成'); diff --git a/logs/planning_doc.patch b/logs/planning_doc.patch deleted file mode 100644 index 3f62b75..0000000 --- a/logs/planning_doc.patch +++ /dev/null @@ -1,221 +0,0 @@ -diff --git "a/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md" "b/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md" -index bcb48fd..440a12d 100644 ---- "a/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md" -+++ "b/\346\257\224\350\265\233\345\244\207\350\265\233\350\247\204\345\210\222.md" -@@ -50,7 +50,7 @@ - | `prompt_service.py` | ✅ 已完成 | Prompt 模板管理 | - | `text_analysis_service.py` | ✅ 已完成 | 文本分析 | - | `chart_generator_service.py` | ✅ 已完成 | 图表生成服务 | --| `template_fill_service.py` | ❌ 未完成 | 模板填写服务 | -+| `template_fill_service.py` | ✅ 已完成 | 模板填写服务,支持直接读取源文档进行填表 | - - ### 2.2 API 接口 (`backend/app/api/endpoints/`) - -@@ -61,7 +61,7 @@ - | `ai_analyze.py` | `/api/v1/analyze/*` | ✅ AI 分析(Excel、Markdown、流式) | - | `rag.py` | `/api/v1/rag/*` | ⚠️ RAG 检索(当前返回空) | - | `tasks.py` | `/api/v1/tasks/*` | ✅ 异步任务状态查询 | --| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理 | -+| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理 (含 Word 导出) | - | `visualization.py` | `/api/v1/visualization/*` | ✅ 可视化图表 | - | `health.py` | `/api/v1/health` | ✅ 健康检查 | - -@@ -78,8 +78,8 @@ - |------|----------|------| - | Excel (.xlsx/.xls) | ✅ 已完成 | pandas + XML 回退解析 | - | Markdown (.md) | ✅ 已完成 | 正则 + AI 分章节 | --| Word (.docx) | ❌ 未完成 | 尚未实现 | --| Text (.txt) | ❌ 未完成 | 尚未实现 | -+| Word (.docx) | ✅ 已完成 | python-docx 解析,支持表格提取和字段识别 | -+| Text (.txt) | ✅ 已完成 | chardet 编码检测,支持文本清洗和结构化提取 | - - --- - -@@ -87,7 +87,7 @@ - - ### 3.1 模板填写模块(最优先) - --**这是比赛的核心评测功能,必须完成。** -+**当前状态**:✅ 已完成 - - ``` - 用户上传模板表格(Word/Excel) -@@ -103,30 +103,34 @@ AI 根据字段提示词从源数据中提取信息 - 返回填写完成的表格 - ``` - --**需要实现**: --- [ ] `template_fill_service.py` - 模板填写核心服务 --- [ ] Word 模板解析 (`docx_parser.py` 需新建) --- [ ] Text 模板解析 (`txt_parser.py` 需新建) --- [ ] 模板字段识别与提示词提取 --- [ ] 多文档数据聚合与冲突处理 --- [ ] 结果导出为 Word/Excel -+**已完成实现**: -+- [x] `template_fill_service.py` - 模板填写核心服务 -+- [x] Word 模板解析 (`docx_parser.py` - parse_tables_for_template, extract_template_fields_from_docx) -+- [x] Text 模板解析 (`txt_parser.py` - 已完成) -+- [x] 模板字段识别与提示词提取 -+- [x] 多文档数据聚合与冲突处理 -+- [x] 结果导出为 Word/Excel - - ### 3.2 Word 文档解析 - --**当前状态**:仅有框架,尚未实现具体解析逻辑 -+**当前状态**:✅ 已完成 - --**需要实现**: --- [ ] `docx_parser.py` - Word 文档解析器 --- [ ] 提取段落文本 --- [ ] 提取表格内容 --- [ ] 提取关键信息(标题、列表等) -+**已实现功能**: -+- [x] `docx_parser.py` - Word 文档解析器 -+- [x] 提取段落文本 -+- [x] 提取表格内容 -+- [x] 提取关键信息(标题、列表等) -+- [x] 表格模板字段提取 (`parse_tables_for_template`, `extract_template_fields_from_docx`) -+- [x] 字段类型推断 (`_infer_field_type_from_hint`) - - ### 3.3 Text 文档解析 - --**需要实现**: --- [ ] `txt_parser.py` - 文本文件解析器 --- [ ] 编码自动检测 --- [ ] 文本清洗 -+**当前状态**:✅ 已完成 -+ -+**已实现功能**: -+- [x] `txt_parser.py` - 文本文件解析器 -+- [x] 编码自动检测 (chardet) -+- [x] 文本清洗 - - ### 3.4 文档模板匹配(已有框架) - -@@ -215,5 +219,122 @@ docs/test/ - - --- - --*文档版本: v1.0* --*最后更新: 2026-04-08* -\ No newline at end of file -+*文档版本: v1.1* -+*最后更新: 2026-04-08* -+ -+--- -+ -+## 八、技术实现细节 -+ -+### 8.1 模板填表流程(已实现) -+ -+#### 流程图 -+``` -+┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -+│ 上传模板 │ ──► │ 选择数据源 │ ──► │ AI 智能填表 │ -+└─────────────┘ └─────────────┘ └─────────────┘ -+ │ -+ ▼ -+ ┌─────────────┐ -+ │ 导出结果 │ -+ └─────────────┘ -+``` -+ -+#### 核心组件 -+ -+| 组件 | 文件 | 说明 | -+|------|------|------| -+| 模板上传 | `templates.py` `/templates/upload` | 接收模板文件,提取字段 | -+| 字段提取 | `template_fill_service.py` | 从 Word/Excel 表格提取字段定义 | -+| 文档解析 | `docx_parser.py`, `xlsx_parser.py`, `txt_parser.py` | 解析源文档内容 | -+| 智能填表 | `template_fill_service.py` `fill_template()` | 使用 LLM 从源文档提取信息 | -+| 结果导出 | `templates.py` `/templates/export` | 导出为 Excel 或 Word | -+ -+### 8.2 源文档加载方式 -+ -+模板填表服务支持两种方式加载源文档: -+ -+1. **通过 MongoDB 文档 ID**:`source_doc_ids` -+ - 文档已上传并存入 MongoDB -+ - 服务直接查询 MongoDB 获取文档内容 -+ -+2. **通过文件路径**:`source_file_paths` -+ - 直接读取本地文件 -+ - 使用对应的解析器解析内容 -+ -+### 8.3 Word 表格模板解析 -+ -+比赛评分表格通常是 Word 格式,`docx_parser.py` 提供了专门的解析方法: -+ -+```python -+# 提取表格模板字段 -+fields = docx_parser.extract_template_fields_from_docx(file_path) -+ -+# 返回格式 -+# [ -+# { -+# "cell": "T0R1", # 表格0,行1 -+# "name": "字段名", -+# "hint": "提示词", -+# "field_type": "text/number/date", -+# "required": True -+# }, -+# ... -+# ] -+``` -+ -+### 8.4 字段类型推断 -+ -+系统支持从提示词自动推断字段类型: -+ -+| 关键词 | 推断类型 | 示例 | -+|--------|----------|------| -+| 年、月、日、日期、时间、出生 | date | 出生日期 | -+| 数量、金额、比率、%、率、合计 | number | 增长比率 | -+| 其他 | text | 姓名、地址 | -+ -+### 8.5 API 接口 -+ -+#### POST `/api/v1/templates/fill` -+ -+填写请求: -+```json -+{ -+ "template_id": "模板ID", -+ "template_fields": [ -+ {"cell": "A1", "name": "姓名", "field_type": "text", "required": true, "hint": "提取人员姓名"} -+ ], -+ "source_doc_ids": ["mongodb_doc_id_1", "mongodb_doc_id_2"], -+ "source_file_paths": [], -+ "user_hint": "请从合同文档中提取" -+} -+``` -+ -+响应: -+```json -+{ -+ "success": true, -+ "filled_data": {"姓名": "张三"}, -+ "fill_details": [ -+ { -+ "field": "姓名", -+ "cell": "A1", -+ "value": "张三", -+ "source": "来自:合同文档.docx", -+ "confidence": 0.95 -+ } -+ ], -+ "source_doc_count": 2 -+} -+``` -+ -+#### POST `/api/v1/templates/export` -+ -+导出请求: -+```json -+{ -+ "template_id": "模板ID", -+ "filled_data": {"姓名": "张三", "金额": "10000"}, -+ "format": "xlsx" // 或 "docx" -+} -+``` -\ No newline at end of file diff --git a/logs/rag_disable_note.txt b/logs/rag_disable_note.txt deleted file mode 100644 index cf75308..0000000 --- a/logs/rag_disable_note.txt +++ /dev/null @@ -1,59 +0,0 @@ -RAG 服务临时禁用说明 -======================== -日期: 2026-04-08 - -修改内容: ----------- -应需求,RAG 向量检索功能已临时禁用,具体如下: - -1. 修改文件: backend/app/services/rag_service.py - -2. 关键变更: - - 在 RAGService.__init__ 中添加 self._disabled = True 标志 - - index_field() - 添加 _disabled 检查,跳过实际索引操作并记录日志 - - index_document_content() - 添加 _disabled 检查,跳过实际索引操作并记录日志 - - retrieve() - 添加 _disabled 检查,返回空列表并记录日志 - - get_vector_count() - 添加 _disabled 检查,返回 0 并记录日志 - - clear() - 添加 _disabled 检查,跳过实际清空操作并记录日志 - -3. 行为变更: - - 所有 RAG 索引构建操作会被记录到日志 ([RAG DISABLED] 前缀) - - 所有 RAG 检索操作返回空结果 - - 向量计数始终返回 0 - - 实际向量数据库操作被跳过 - -4. 恢复方式: - - 将 RAGService.__init__ 中的 self._disabled = True 改为 self._disabled = False - - 重新启动服务即可恢复 RAG 功能 - -目的: ------- -保留 RAG 索引构建功能的前端界面和代码结构,暂不实际调用向量数据库 API, -待后续需要时再启用。 - -影响范围: ---------- -- /api/v1/rag/search - RAG 搜索接口 (返回空结果) -- /api/v1/rag/status - RAG 状态接口 (返回 vector_count=0) -- /api/v1/rag/rebuild - RAG 重建接口 (仅记录日志) -- Excel/文档上传时的 RAG 索引构建 (仅记录日志) - -======================== -后续补充 (2026-04-08): -======================== -修改文件: backend/app/services/table_rag_service.py - -关键变更: -- 在 TableRAGService.__init__ 中添加 self._disabled = True 标志 -- build_table_rag_index() - RAG 索引部分被跳过,仅记录日志 -- index_document_table() - RAG 索引部分被跳过,仅记录日志 - -行为变更: -- Excel 上传时,MySQL 存储仍然正常进行 -- AI 字段描述仍然正常生成(调用 LLM) -- 只有向量数据库索引操作被跳过 - -恢复方式: -- 将 TableRAGService.__init__ 中的 self._disabled = True 改为 self._disabled = False -- 或将 rag_service.py 中的 self._disabled = True 改为 self._disabled = False -- 两者需同时改为 False 才能完全恢复 RAG 功能 diff --git a/logs/template_fill_feature_changes.md b/logs/template_fill_feature_changes.md deleted file mode 100644 index 78ade15..0000000 --- a/logs/template_fill_feature_changes.md +++ /dev/null @@ -1,144 +0,0 @@ -# 模板填表功能变更日志 - -**变更日期**: 2026-04-08 -**变更类型**: 功能完善 -**变更内容**: Word 表格解析和模板填表功能 - ---- - -## 变更概述 - -本次变更完善了 Word 表格解析、表格模板构建和填写功能,实现了从源文档(MongoDB/文件)读取数据并智能填表的核心流程。 - -### 涉及文件 - -| 文件 | 变更行数 | 说明 | -|------|----------|------| -| backend/app/api/endpoints/templates.py | +156 | API 端点完善,添加 Word 导出 | -| backend/app/core/document_parser/docx_parser.py | +130 | Word 表格解析增强 | -| backend/app/services/template_fill_service.py | +340 | 核心填表服务重写 | -| frontend/src/db/backend-api.ts | +9 | 前端 API 更新 | -| frontend/src/pages/TemplateFill.tsx | +8 | 前端页面更新 | -| 比赛备赛规划.md | +169 | 文档更新 | - ---- - -## 详细变更 - -### 1. backend/app/core/document_parser/docx_parser.py - -**新增方法**: - -- `parse_tables_for_template(file_path)` - 解析 Word 文档中的表格,提取模板字段 -- `extract_template_fields_from_docx(file_path)` - 从 Word 文档提取模板字段定义 -- `_infer_field_type_from_hint(hint)` - 从提示词推断字段类型 - -**功能说明**: -- 专门用于比赛场景:解析表格模板,识别需要填写的字段 -- 支持从表格第一列提取字段名,第二列提取提示词/描述 -- 自动推断字段类型(text/number/date) - -### 2. backend/app/services/template_fill_service.py - -**重构内容**: - -- 不再依赖 RAG 服务,直接从 MongoDB 或文件读取源文档 -- 新增 `SourceDocument` 数据类 -- 完善 `fill_template()` 方法,支持 `source_doc_ids` 和 `source_file_paths` -- 新增 `_load_source_documents()` - 加载源文档内容 -- 新增 `_extract_field_value()` - 使用 LLM 提取字段值 -- 新增 `_build_context_text()` - 构建上下文(优先使用表格数据) -- 完善 `_get_template_fields_from_docx()` - Word 模板字段提取 - -**核心流程**: -``` -1. 加载源文档(MongoDB 或文件) -2. 对每个字段调用 LLM 提取值 -3. 返回填写结果 -``` - -### 3. backend/app/api/endpoints/templates.py - -**新增内容**: - -- `FillRequest` 添加 `source_doc_ids`, `source_file_paths`, `user_hint` 字段 -- `ExportRequest` 添加 `format` 字段 -- `_export_to_word()` - 导出为 Word 格式 -- `/templates/export/excel` - 专门导出 Excel -- `/templates/export/word` - 专门导出 Word - -### 4. frontend/src/db/backend-api.ts - -**更新内容**: - -- `TemplateField` 接口添加 `hint` 字段 -- `fillTemplate()` 方法添加 `sourceDocIds`, `sourceFilePaths`, `userHint` 参数 - -### 5. frontend/src/pages/TemplateFill.tsx - -**更新内容**: - -- `handleFillTemplate()` 传递 `selectedDocs` 作为 `sourceDocIds` 参数 - ---- - -## API 接口变更 - -### POST /api/v1/templates/fill - -**请求体**: -```json -{ - "template_id": "模板ID", - "template_fields": [ - { - "cell": "A1", - "name": "姓名", - "field_type": "text", - "required": true, - "hint": "提取人员姓名" - } - ], - "source_doc_ids": ["mongodb_doc_id"], - "source_file_paths": [], - "user_hint": "请从xxx文档中提取" -} -``` - -**响应**: -```json -{ - "success": true, - "filled_data": {"姓名": "张三"}, - "fill_details": [...], - "source_doc_count": 1 -} -``` - -### POST /api/v1/templates/export - -**新增支持 format=dicx**,可导出为 Word 格式 - ---- - -## 技术细节 - -### 字段类型推断 - -| 关键词 | 推断类型 | -|--------|----------| -| 年、月、日、日期、时间、出生 | date | -| 数量、金额、比率、%、率、合计 | number | -| 其他 | text | - -### 上下文构建 - -源文档内容构建优先级: -1. 结构化数据(表格数据) -2. 原始文本内容(限制 5000 字符) - ---- - -## 相关文档 - -- [比赛备赛规划.md](../比赛备赛规划.md) - 已更新功能状态和技术实现细节 diff --git a/比赛备赛规划.md b/比赛备赛规划.md deleted file mode 100644 index a81ca47..0000000 --- a/比赛备赛规划.md +++ /dev/null @@ -1,558 +0,0 @@ -# 比赛备赛规划文档 - -## 一、赛题核心理解 - -### 1.1 赛题名称 -**A23 - 基于大语言模型的文档理解与多源数据融合** -参赛院校:金陵科技学院 - -### 1.2 核心任务 -1. **文档解析**:解析 docx/md/xlsx/txt 四种格式的源数据文档 -2. **模板填写**:根据模板表格要求,从源文档中提取数据填写到 Word/Excel 模板 -3. **准确率与速度**:准确率优先,速度作为辅助评分因素 - -### 1.3 评分规则 -| 要素 | 说明 | -|------|------| -| 准确率 | 填写结果与样例表格对比的正确率 | -| 响应时间 | 从导入文档到得到结果的时间 ≤ 90s × 文档数量 | -| 评测方式 | 赛方提供空表格模板 + 样例表格(人工填写),系统自动填写后对比 | - -### 1.4 关键Q&A摘录 - -| 问题 | 解答要点 | -|------|----------| -| Q2: 模板与文档的关系 | 前2个表格只涉及1份文档;第3-4个涉及多份文档;第5个涉及大部分文档(从易到难) | -| Q5: 响应时间定义 | 从导入文档到最终得到结果的时间 ≤ 90s × 文档数量 | -| Q7: 需要读取哪些文件 | 每个模板只读取指定的数据文件,不需要读取全部 | -| Q10: 部署方式 | 不要求部署到服务器,本地部署即可 | -| Q14: 模板匹配 | 模板已指定数据文件,不需要算法匹配 | -| Q16: 数据库存储 | 可跳过,不强制要求 | -| Q20: 创新点 | 不用管,随意发挥 | -| Q21: 填写依据 | 按照测试表格模板给的提示词进行填写 | - ---- - -## 二、已完成功能清单 - -### 2.1 后端服务 (`backend/app/services/`) - -| 服务文件 | 功能状态 | 说明 | -|----------|----------|------| -| `file_service.py` | ✅ 已完成 | 文件上传、保存、类型识别 | -| `excel_storage_service.py` | ✅ 已完成 | Excel 存储到 MySQL,支持 XML 回退解析 | -| `table_rag_service.py` | ⚠️ 已禁用 | RAG 索引构建(当前禁用,仅记录日志) | -| `llm_service.py` | ✅ 已完成 | LLM 调用、流式输出、多模型支持 | -| `markdown_ai_service.py` | ✅ 已完成 | Markdown AI 分析、分章节提取、流式输出、图表生成 | -| `excel_ai_service.py` | ✅ 已完成 | Excel AI 分析 | -| `visualization_service.py` | ✅ 已完成 | 图表生成(matplotlib) | -| `rag_service.py` | ⚠️ 已禁用 | FAISS 向量检索(当前禁用) | -| `prompt_service.py` | ✅ 已完成 | Prompt 模板管理 | -| `text_analysis_service.py` | ✅ 已完成 | 文本分析 | -| `chart_generator_service.py` | ✅ 已完成 | 图表生成服务 | -| `template_fill_service.py` | ✅ 已完成 | 模板填写服务,支持多行提取、直接从结构化数据提取、JSON容错、Word文档表格处理 | - -### 2.2 API 接口 (`backend/app/api/endpoints/`) - -| 接口文件 | 路由 | 功能状态 | -|----------|------|----------| -| `upload.py` | `/api/v1/upload/document` | ✅ 文档上传与解析 | -| `documents.py` | `/api/v1/documents/*` | ✅ 文档管理(列表、删除、搜索) | -| `ai_analyze.py` | `/api/v1/analyze/*` | ✅ AI 分析(Excel、Markdown、流式) | -| `rag.py` | `/api/v1/rag/*` | ⚠️ RAG 检索(当前返回空) | -| `tasks.py` | `/api/v1/tasks/*` | ✅ 异步任务状态查询 | -| `templates.py` | `/api/v1/templates/*` | ✅ 模板管理(含多行导出、Word导出、Word结构化字段解析) | -| `visualization.py` | `/api/v1/visualization/*` | ✅ 可视化图表 | -| `health.py` | `/api/v1/health` | ✅ 健康检查 | - -### 2.3 前端页面 (`frontend/src/pages/`) - -| 页面文件 | 功能 | 状态 | -|----------|------|------| -| `Documents.tsx` | 主文档管理页面 | ✅ 已完成 | -| `TemplateFill.tsx` | 智能填表页面 | ✅ 已完成 | -| `ExcelParse.tsx` | Excel 解析页面 | ✅ 已完成 | - -### 2.4 文档解析能力 - -| 格式 | 解析状态 | 说明 | -|------|----------|------| -| Excel (.xlsx/.xls) | ✅ 已完成 | pandas + XML 回退解析,支持多sheet | -| Markdown (.md) | ✅ 已完成 | 正则 + AI 分章节 | -| Word (.docx) | ✅ 已完成 | python-docx 解析,支持表格提取和字段识别 | -| Text (.txt) | ✅ 已完成 | chardet 编码检测,支持文本清洗和结构化提取 | - ---- - -## 三、核心功能实现详情 - -### 3.1 模板填写模块(✅ 已完成) - -**核心流程**: -``` -上传模板表格(Word/Excel) - ↓ -解析模板,提取需要填写的字段和提示词 - ↓ -根据源文档ID列表读取源数据(MongoDB或文件) - ↓ -优先从结构化数据直接提取(Excel rows) - ↓ -无法直接提取时使用 LLM 从文本中提取 - ↓ -将提取的数据填入原始模板对应位置(保持模板格式) - ↓ -导出填写完成的表格(Excel/Word) -``` - -**关键特性**: -- **原始模板填充**:直接打开原始模板文件,填充数据到原表格/单元格 -- **多行数据支持**:每个字段可提取多个值,导出时自动扩展行数 -- **结构化数据优先**:直接从 Excel rows 提取,无需 LLM -- **JSON 容错**:支持 LLM 返回的损坏/截断 JSON -- **Markdown 清理**:自动清理 LLM 返回的 markdown 格式 - -### 3.2 Word 文档解析(✅ 已完成) - -**已实现功能**: -- `docx_parser.py` - Word 文档解析器 -- 提取段落文本 -- 提取表格内容(支持比赛表格格式:字段名 | 提示词 | 填写值) -- `parse_tables_for_template()` - 解析表格模板,提取字段 -- `extract_template_fields_from_docx()` - 提取模板字段定义 -- `_infer_field_type_from_hint()` - 从提示词推断字段类型 -- **API 端点**:`/api/v1/templates/parse-word-structure` - 上传 Word 文档,提取结构化字段并存入 MongoDB -- **API 端点**:`/api/v1/templates/word-fields/{doc_id}` - 获取已存文档的模板字段信息 - -### 3.3 Text 文档解析(✅ 已完成) - -**已实现功能**: -- `txt_parser.py` - 文本文件解析器 -- 编码自动检测 (chardet) -- 文本清洗(去除控制字符、规范化空白) -- 结构化数据提取(邮箱、URL、电话、日期、金额) - ---- - -## 四、参赛材料准备 - -### 4.1 必交材料 - -| 材料 | 要求 | 当前状态 | 行动项 | -|------|------|----------|--------| -| 项目概要介绍 | PPT 格式 | ❌ 待制作 | 制作 PPT | -| 项目简介 PPT | - | ❌ 待制作 | 制作 PPT | -| 项目详细方案 | 文档 | ⚠️ 部分完成 | 完善文档 | -| 项目演示视频 | - | ❌ 待制作 | 录制演示视频 | -| 训练素材说明 | 来源说明 | ⚠️ 已有素材 | 整理素材文档 | -| 关键模块设计文档 | 概要设计 | ⚠️ 已有部分 | 完善文档 | -| 可运行 Demo | 核心代码 | ✅ 已完成 | 打包可运行版本 | - -### 4.2 Demo 提交要求 - -根据 Q&A: -- 可以只提交核心代码,不需要完整运行环境 -- 现场答辩可使用自带笔记本电脑 -- 需要提供部署和运行说明(README) - ---- - -## 五、测试验证计划 - -### 5.1 使用现有测试数据 - -``` -docs/test/ -├── 2023年文化和旅游发展统计公报.md -├── 2024年卫生健康事业发展统计公报.md -├── 第三次全国工业普查主要数据公报.md -``` - -### 5.2 模板填写测试流程 - -1. 准备一个 Word/Excel 模板表格 -2. 指定源数据文档 -3. 上传模板和文档 -4. 执行模板填写 -5. 检查填写结果准确率 -6. 记录响应时间 - -### 5.3 性能目标 - -| 指标 | 目标 | 当前状态 | -|------|------|----------| -| 信息提取准确率 | ≥80% | 需测试验证 | -| 单次响应时间 | ≤90s × 文档数 | 需测试验证 | - ---- - -## 六、工作计划(建议) - -### 第一优先级:端到端测试 -- 使用真实测试数据进行准确率测试 -- 验证多行数据导出是否正确 -- 测试 Word 模板解析是否正常 - -### 第二优先级:Demo 打包与文档 -- 制作项目演示 PPT -- 录制演示视频 -- 完善 README 部署文档 - -### 第三优先级:优化 -- 优化响应时间 -- 完善错误处理 -- 增加更多测试用例 - ---- - -## 七、注意事项 - -1. **创新点**:根据 Q&A,不必纠结创新点数量限制 -2. **数据库**:不强制要求数据库存储,可跳过 -3. **部署**:本地部署即可,不需要公网服务器 -4. **评测数据**:初赛仅使用目前提供的数据 -5. **RAG 功能**:当前已临时禁用,不影响核心评测功能(因为使用直接文件读取) - ---- - -*文档版本: v1.5* -*最后更新: 2026-04-09* - ---- - -## 八、技术实现细节 - -### 8.1 模板填表流程 - -#### 流程图 -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ 上传模板 │ ──► │ 选择数据源 │ ──► │ 智能填表 │ -└─────────────┘ └─────────────┘ └─────────────┘ - │ - ┌─────────────────────────┼─────────────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ - │ 结构化数据提取 │ │ LLM 提取 │ │ 导出结果 │ - │ (直接读rows) │ │ (文本理解) │ │ (Excel/Word) │ - └───────────────┘ └───────────────┘ └───────────────┘ -``` - -#### 核心组件 - -| 组件 | 文件 | 说明 | -|------|------|------| -| 模板上传 | `templates.py` `/templates/upload` | 接收模板文件,提取字段 | -| 字段提取 | `template_fill_service.py` | 从 Word/Excel 表格提取字段定义 | -| 文档解析 | `docx_parser.py`, `xlsx_parser.py`, `txt_parser.py` | 解析源文档内容 | -| 智能填表 | `template_fill_service.py` `fill_template()` | 结构化提取 + LLM 提取 | -| 多行支持 | `template_fill_service.py` `FillResult` | values 数组支持 | -| JSON 容错 | `template_fill_service.py` `_fix_json()` | 修复损坏的 JSON | -| 结果导出 | `templates.py` `/templates/export` | 多行 Excel + Word 导出 | - -### 8.2 源文档加载方式 - -模板填表服务支持两种方式加载源文档: - -1. **通过 MongoDB 文档 ID**:`source_doc_ids` - - 文档已上传并存入 MongoDB - - 服务直接查询 MongoDB 获取文档内容 - -2. **通过文件路径**:`source_file_paths` - - 直接读取本地文件 - - 使用对应的解析器解析内容 - -### 8.3 Word 表格模板解析 - -比赛评分表格通常是 Word 格式,`docx_parser.py` 提供了专门的解析方法: - -```python -# 提取表格模板字段 -from docx_parser import DocxParser -parser = DocxParser() -fields = parser.extract_template_fields_from_docx(file_path) - -# 返回格式 -# [ -# { -# "cell": "T0R1", # 表格0,行1 -# "name": "字段名", -# "hint": "提示词", -# "field_type": "text/number/date", -# "required": True -# }, -# ... -# ] -``` - -### 8.4 字段类型推断 - -系统支持从提示词自动推断字段类型: - -| 关键词 | 推断类型 | 示例 | -|--------|----------|------| -| 年、月、日、日期、时间、出生 | date | 出生日期 | -| 数量、金额、比率、%、率、合计 | number | 增长比率 | -| 其他 | text | 姓名、地址 | - -### 8.5 API 接口 - -#### POST `/api/v1/templates/upload` - -上传模板文件,提取字段定义。 - -**响应**: -```json -{ - "success": true, - "template_id": "/path/to/saved/template.docx", - "filename": "模板.docx", - "file_type": "docx", - "fields": [ - {"cell": "A1", "name": "姓名", "field_type": "text", "required": true, "hint": "提取人员姓名"} - ], - "field_count": 1 -} -``` - -#### POST `/api/v1/templates/fill` - -填写请求: -```json -{ - "template_id": "模板ID", - "template_fields": [ - {"cell": "A1", "name": "姓名", "field_type": "text", "required": true, "hint": "提取人员姓名"} - ], - "source_doc_ids": ["mongodb_doc_id_1", "mongodb_doc_id_2"], - "source_file_paths": [], - "user_hint": "请从xxx文档中提取" -} -``` - -**响应(含多行支持)**: -```json -{ - "success": true, - "filled_data": { - "姓名": ["张三", "李四", "王五"], - "年龄": ["25", "30", "28"] - }, - "fill_details": [ - { - "field": "姓名", - "cell": "A1", - "values": ["张三", "李四", "王五"], - "value": "张三", - "source": "结构化数据直接提取", - "confidence": 1.0 - } - ], - "source_doc_count": 2, - "max_rows": 3 -} -``` - -#### POST `/api/v1/templates/export` - -导出请求(创建新文件): -```json -{ - "template_id": "模板ID", - "filled_data": {"姓名": ["张三", "李四"], "金额": ["10000", "20000"]}, - "format": "xlsx" -} -``` - -#### POST `/api/v1/templates/fill-and-export` - -**填充原始模板并导出**(推荐用于比赛) - -直接打开原始模板文件,将数据填入模板的表格/单元格中,然后导出。**保持原始模板格式不变**。 - -**请求**: -```json -{ - "template_path": "/path/to/original/template.docx", - "filled_data": { - "姓名": ["张三", "李四", "王五"], - "年龄": ["25", "30", "28"] - }, - "format": "docx" -} -``` - -**响应**:填充后的 Word/Excel 文件(文件流) - -**特点**: -- 打开原始模板文件 -- 根据表头行匹配字段名到列索引 -- 将数据填入对应列的单元格 -- 多行数据自动扩展表格行数 -- 保持原始模板格式和样式 - -#### POST `/api/v1/templates/parse-word-structure` - -**上传 Word 文档并提取结构化字段**(比赛专用) - -上传 Word 文档,从表格模板中提取字段定义(字段名、提示词、字段类型)并存入 MongoDB。 - -**请求**:multipart/form-data -- file: Word 文件 - -**响应**: -```json -{ - "success": true, - "doc_id": "mongodb_doc_id", - "filename": "模板.docx", - "file_path": "/path/to/saved/template.docx", - "field_count": 5, - "fields": [ - { - "cell": "T0R1", - "name": "字段名", - "hint": "提示词", - "field_type": "text", - "required": true - } - ], - "tables": [...], - "metadata": { - "paragraph_count": 10, - "table_count": 1, - "word_count": 500, - "has_tables": true - } -} -``` - -#### GET `/api/v1/templates/word-fields/{doc_id}` - -**获取 Word 文档模板字段信息** - -根据 doc_id 获取已上传的 Word 文档的模板字段信息。 - -**响应**: -```json -{ - "success": true, - "doc_id": "mongodb_doc_id", - "filename": "模板.docx", - "fields": [...], - "tables": [...], - "field_count": 5, - "metadata": {...} -} -``` - -### 8.6 多行数据处理 - -**FillResult 数据结构**: -```python -@dataclass -class FillResult: - field: str - values: List[Any] = None # 支持多个值(数组) - value: Any = "" # 保留兼容(第一个值) - source: str = "" # 来源文档 - confidence: float = 1.0 # 置信度 -``` - -**导出逻辑**: -- 计算所有字段的最大行数 -- 遍历每一行,取对应索引的值 -- 不足的行填空字符串 - -### 8.7 JSON 容错处理 - -当 LLM 返回的 JSON 损坏或被截断时,系统会: - -1. 清理 markdown 代码块标记(```json, ```) -2. 尝试配对括号找到完整的 JSON -3. 移除末尾多余的逗号 -4. 使用正则表达式提取 values 数组 -5. 备选方案:直接提取所有引号内的字符串 - -### 8.8 结构化数据优先提取 - -对于 Excel 等有 `rows` 结构的文档,系统会: - -1. 直接从 `structured_data.rows` 中查找匹配列 -2. 使用模糊匹配(字段名包含或被包含) -3. 提取该列的所有行值 -4. 无需调用 LLM,速度更快,准确率更高 - -```python -# 内部逻辑 -if structured.get("rows"): - columns = structured.get("columns", []) - values = _extract_column_values(rows, columns, field_name) -``` - ---- - -## 九、依赖说明 - -### Python 依赖 - -``` -# requirements.txt 中需要包含 -fastapi>=0.104.0 -uvicorn>=0.24.0 -motor>=3.3.0 # MongoDB 异步驱动 -sqlalchemy>=2.0.0 # MySQL ORM -pandas>=2.0.0 # Excel 处理 -openpyxl>=3.1.0 # Excel 写入 -python-docx>=0.8.0 # Word 处理 -chardet>=4.0.0 # 编码检测 -httpx>=0.25.0 # HTTP 客户端 -``` - -### 前端依赖 - -``` -# package.json 中需要包含 -react>=18.0.0 -react-dropzone>=14.0.0 -lucide-react>=0.300.0 -sonner>=1.0.0 # toast 通知 -``` - ---- - -## 十、启动说明 - -### 后端启动 - -```bash -cd backend -.\venv\Scripts\Activate.ps1 # 或 Activate.bat -pip install -r requirements.txt # 确保依赖完整 -.\venv\Scripts\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload -``` - -### 前端启动 - -```bash -cd frontend -npm install -npm run dev -``` - -### 环境变量 - -在 `backend/.env` 中配置: -``` -MONGODB_URL=mongodb://localhost:27017 -MONGODB_DB_NAME=document_system -MYSQL_HOST=localhost -MYSQL_PORT=3306 -MYSQL_USER=root -MYSQL_PASSWORD=your_password -MYSQL_DATABASE=document_system -LLM_API_KEY=your_api_key -LLM_BASE_URL=https://api.minimax.chat -LLM_MODEL_NAME=MiniMax-Text-01 -```