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 f5e5557..0000000 Binary files a/Q&A.xlsx and /dev/null differ diff --git a/logs/docx_parser_and_template_fill.patch b/logs/docx_parser_and_template_fill.patch deleted file mode 100644 index 2195aba..0000000 --- a/logs/docx_parser_and_template_fill.patch +++ /dev/null @@ -1,854 +0,0 @@ -diff --git a/backend/app/api/endpoints/templates.py b/backend/app/api/endpoints/templates.py -index 572d56e..706f281 100644 ---- a/backend/app/api/endpoints/templates.py -+++ b/backend/app/api/endpoints/templates.py -@@ -13,7 +13,7 @@ import pandas as pd - from pydantic import BaseModel - - from app.services.template_fill_service import template_fill_service, TemplateField --from app.services.excel_storage_service import excel_storage_service -+from app.services.file_service import file_service - - logger = logging.getLogger(__name__) - -@@ -28,13 +28,15 @@ class TemplateFieldRequest(BaseModel): - name: str - field_type: str = "text" - required: bool = True -+ hint: str = "" - - - class FillRequest(BaseModel): - """填写请求""" - template_id: str - template_fields: List[TemplateFieldRequest] -- source_doc_ids: Optional[List[str]] = None -+ source_doc_ids: Optional[List[str]] = None # MongoDB 文档 ID 列表 -+ source_file_paths: Optional[List[str]] = None # 源文档文件路径列表 - user_hint: Optional[str] = None - - -@@ -71,7 +73,6 @@ async def upload_template( - - try: - # 保存文件 -- from app.services.file_service import file_service - content = await file.read() - saved_path = file_service.save_uploaded_file( - content, -@@ -87,7 +88,7 @@ async def upload_template( - - return { - "success": True, -- "template_id": saved_path, # 使用文件路径作为ID -+ "template_id": saved_path, - "filename": file.filename, - "file_type": file_ext, - "fields": [ -@@ -95,7 +96,8 @@ async def upload_template( - "cell": f.cell, - "name": f.name, - "field_type": f.field_type, -- "required": f.required -+ "required": f.required, -+ "hint": f.hint - } - for f in template_fields - ], -@@ -135,7 +137,8 @@ async def extract_template_fields( - "cell": f.cell, - "name": f.name, - "field_type": f.field_type, -- "required": f.required -+ "required": f.required, -+ "hint": f.hint - } - for f in fields - ] -@@ -153,7 +156,7 @@ async def fill_template( - """ - 执行表格填写 - -- 根据提供的字段定义,从已上传的文档中检索信息并填写 -+ 根据提供的字段定义,从源文档中检索信息并填写 - - Args: - request: 填写请求 -@@ -168,7 +171,8 @@ async def fill_template( - cell=f.cell, - name=f.name, - field_type=f.field_type, -- required=f.required -+ required=f.required, -+ hint=f.hint - ) - for f in request.template_fields - ] -@@ -177,6 +181,7 @@ async def fill_template( - result = await template_fill_service.fill_template( - template_fields=fields, - source_doc_ids=request.source_doc_ids, -+ source_file_paths=request.source_file_paths, - user_hint=request.user_hint - ) - -@@ -194,6 +199,8 @@ async def export_filled_template( - """ - 导出填写后的表格 - -+ 支持 Excel (.xlsx) 和 Word (.docx) 格式 -+ - Args: - request: 导出请求 - -@@ -201,25 +208,124 @@ async def export_filled_template( - 文件流 - """ - try: -- # 创建 DataFrame -- df = pd.DataFrame([request.filled_data]) -+ if request.format == "xlsx": -+ return await _export_to_excel(request.filled_data, request.template_id) -+ elif request.format == "docx": -+ return await _export_to_word(request.filled_data, request.template_id) -+ else: -+ raise HTTPException( -+ status_code=400, -+ detail=f"不支持的导出格式: {request.format},仅支持 xlsx/docx" -+ ) - -- # 导出为 Excel -- output = io.BytesIO() -- with pd.ExcelWriter(output, engine='openpyxl') as writer: -- df.to_excel(writer, index=False, sheet_name='填写结果') -+ except HTTPException: -+ raise -+ 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.{request.format}" -+async def _export_to_excel(filled_data: dict, template_id: str) -> 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 -```