diff --git a/backend/ai_system/core_agents/main_agent.py b/backend/ai_system/core_agents/main_agent.py index a92c6c7..bcd054f 100644 --- a/backend/ai_system/core_agents/main_agent.py +++ b/backend/ai_system/core_agents/main_agent.py @@ -238,11 +238,29 @@ def _setup_tools(self): }, } + # rename_section_title工具 + rename_section_title_tool = { + "type": "function", + "function": { + "name": "rename_section_title", + "description": "修改paper.md文件中指定章节的标题,保持标题层级不变", + "parameters": { + "type": "object", + "properties": { + "old_title": {"type": "string", "description": "原章节标题(支持模糊匹配)"}, + "new_title": {"type": "string", "description": "新章节标题"} + }, + "required": ["old_title", "new_title"], + }, + }, + } + tools.extend([ analyze_template_tool, get_section_content_tool, update_section_content_tool, - add_section_tool + add_section_tool, + rename_section_title_tool ]) self.tools = tools @@ -270,7 +288,8 @@ def _register_tool_functions(self): "analyze_template": template_tool.analyze_template, "get_section_content": template_tool.get_section_content, "update_section_content": template_tool.update_section_content, - "add_section": template_tool.add_section + "add_section": template_tool.add_section, + "rename_section_title": template_tool.rename_section_title }) async def _execute_code_agent_wrapper(self, task_prompt: str) -> str: diff --git a/backend/ai_system/core_tools/template_tools.py b/backend/ai_system/core_tools/template_tools.py index 7787a6a..d7c937c 100644 --- a/backend/ai_system/core_tools/template_tools.py +++ b/backend/ai_system/core_tools/template_tools.py @@ -236,4 +236,58 @@ async def add_section(self, section_title: str, content: str = "") -> str: except Exception as e: logger.error(f"添加章节失败: {e}") - return f"❌ 添加章节失败: {str(e)}" \ No newline at end of file + return f"❌ 添加章节失败: {str(e)}" + + async def rename_section_title(self, old_title: str, new_title: str) -> str: + """修改paper.md文件中指定章节的标题""" + template_content = self._read_paper_md() + if not template_content: + return "错误:当前工作目录中没有找到paper.md文件" + + try: + import re + lines = template_content.split('\n') + result_lines = [] + title_found = False + original_title = "" + i = 0 + + while i < len(lines): + line = lines[i] + stripped_line = line.strip() + + # 检查是否是目标标题 + if (stripped_line.startswith('#') and + old_title.lower() in stripped_line.lower()): + + # 使用正则表达式提取标题信息 + header_match = re.match(r'^(#{1,6})\s+(.+)$', stripped_line) + if header_match: + level = header_match.group(1) # 保持原标题层级 + original_title = header_match.group(2).strip() + + # 创建新的标题行 + new_line = f"{level} {new_title}" + result_lines.append(new_line) + title_found = True + i += 1 + continue + + result_lines.append(line) + i += 1 + + if not title_found: + return f"❌ 未找到匹配的标题: {old_title}" + + # 保存修改后的内容 + updated_content = '\n'.join(result_lines) + save_result = self._save_paper_md(updated_content) + + if "✅" in save_result: + return f"✅ 标题修改成功:\"{original_title}\" → \"{new_title}\"" + else: + return f"❌ 标题修改失败: {save_result}" + + except Exception as e: + logger.error(f"修改标题失败: {e}") + return f"❌ 修改标题失败: {str(e)}" \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 0f429bf..d0c4207 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -25,4 +25,6 @@ dependencies = [ "pandas>=2.3.1", "seaborn>=0.13.2", "alembic>=1.16.5", + "python-docx>=1.1.2", + "markdown>=3.6.0", ] diff --git a/backend/services/file_services/workspace_files.py b/backend/services/file_services/workspace_files.py index d1ec748..4ee7a40 100644 --- a/backend/services/file_services/workspace_files.py +++ b/backend/services/file_services/workspace_files.py @@ -3,12 +3,25 @@ import shutil import zipfile import tempfile +import logging from pathlib import Path from fastapi import HTTPException, status, UploadFile from typing import List, Dict, Any, Optional from .file_helper import FileHelper from ..data_services.utils import handle_service_errors +logger = logging.getLogger(__name__) + +try: + from docx import Document + from docx.shared import Pt, Inches + from docx.enum.text import WD_PARAGRAPH_ALIGNMENT + from docx.enum.style import WD_STYLE_TYPE + DOCX_AVAILABLE = True +except ImportError: + logger.warning("python-docx not available. DOCX export will be disabled.") + DOCX_AVAILABLE = False + class WorkspaceFileService: def __init__(self): self.base_path = Path("../pa_data/workspaces") @@ -341,6 +354,18 @@ def export_workspace(self, work_id: str) -> str: arcname = os.path.relpath(file_path, workspace_path) zip_file.write(file_path, arcname) + # 生成并添加docx文件 + try: + docx_content = self._generate_docx_from_paper(work_id) + if docx_content: + zip_file.writestr("paper.docx", docx_content) + logger.info(f"Successfully added paper.docx to workspace {work_id} export") + else: + logger.info(f"No paper.md found or docx generation failed for workspace {work_id}") + except Exception as e: + logger.error(f"Failed to generate docx for workspace {work_id}: {str(e)}") + # 继续导出其他文件,不因docx生成失败而中断整个导出过程 + # 如果工作空间为空,添加一个空的README文件 if not os.listdir(workspace_path): zip_file.writestr("README.txt", "This workspace is empty.") @@ -355,5 +380,90 @@ def export_workspace(self, work_id: str) -> str: detail=f"Failed to export workspace: {str(e)}" ) + def _generate_docx_from_paper(self, work_id: str) -> Optional[bytes]: + """生成paper.md对应的docx文件内容""" + if not DOCX_AVAILABLE: + logger.warning("python-docx not available, skipping docx generation") + return None + + try: + workspace_path = self.ensure_workspace_exists(work_id) + paper_md_path = workspace_path / "paper.md" + + if not paper_md_path.exists(): + logger.info(f"paper.md not found in workspace {work_id}, skipping docx generation") + return None + + # 读取paper.md内容 + with open(paper_md_path, 'r', encoding='utf-8') as f: + markdown_content = f.read() + + # 转换为docx + docx_content = self._convert_markdown_to_docx(markdown_content) + return docx_content + + except Exception as e: + logger.error(f"Failed to generate docx for workspace {work_id}: {str(e)}") + return None + + def _convert_markdown_to_docx(self, markdown_content: str) -> bytes: + """将Markdown内容转换为docx格式""" + try: + # 创建Word文档 + doc = Document() + + # 设置默认字体 + style = doc.styles['Normal'] + font = style.font + font.name = 'Times New Roman' + font.size = Pt(12) + + # 分割Markdown内容为行 + lines = markdown_content.split('\n') + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # 处理标题 + if line.startswith('# '): + heading = doc.add_heading(line[2:], level=1) + heading.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + elif line.startswith('## '): + heading = doc.add_heading(line[3:], level=2) + elif line.startswith('### '): + heading = doc.add_heading(line[4:], level=3) + elif line.startswith('#### '): + heading = doc.add_heading(line[5:], level=4) + elif line.startswith('##### '): + heading = doc.add_heading(line[6:], level=5) + # 处理空行 + elif line == '': + doc.add_paragraph() + # 处理普通段落 + else: + # 简单的Markdown格式处理 + processed_line = self._process_markdown_line(line) + paragraph = doc.add_paragraph(processed_line) + + i += 1 + + # 保存到内存中的字节流 + from io import BytesIO + doc_stream = BytesIO() + doc.save(doc_stream) + return doc_stream.getvalue() + + except Exception as e: + logger.error(f"Failed to convert markdown to docx: {str(e)}") + raise + + def _process_markdown_line(self, line: str) -> str: + """处理单行Markdown格式,转换为纯文本""" + # 简单处理,去除一些常见的Markdown标记 + line = line.replace('**', '').replace('*', '').replace('`', '') + line = line.replace('[', '').replace(']', '').replace('(', '').replace(')', '') + return line + # 创建全局实例 workspace_file_service = WorkspaceFileService() \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4217010..70cc3f6 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,4 +15,9 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, + server: { + host: '0.0.0.0', // 允许局域网访问 + port: 5173, // 明确指定端口 + strictPort: true, // 如果端口被占用则失败而不是尝试下一个端口 + }, })