├── .DS_Store ├── .env ├── .gitignore ├── README.md ├── backend ├── Dockerfile ├── Dockerfile.local ├── app │ ├── api │ │ ├── dependencies.py │ │ └── routes.py │ ├── controllers │ │ ├── base_controller.py │ │ └── document_controller.py │ ├── core │ │ ├── config.py │ │ └── database.py │ ├── main.py │ ├── models │ │ └── schemas.py │ ├── providers │ │ ├── ai │ │ │ └── ai_provider.py │ │ ├── base_provider.py │ │ ├── cache_provider.py │ │ ├── mongodb_cache_provider.py │ │ ├── provider_factory.py │ │ └── storage │ │ │ ├── local_storage_provider.py │ │ │ └── storage_provider.py │ └── services │ │ ├── advanced_content_generator.py │ │ ├── ai_client.py │ │ ├── ai_service_factory.py │ │ ├── ai_service_interface.py │ │ ├── cache_service.py │ │ ├── content_generator.py │ │ ├── deepseek_client.py │ │ ├── deepseek_service.py │ │ ├── openai_client.py │ │ ├── outline_generator.py │ │ ├── pdf_generator.py │ │ ├── ppt_generator.py │ │ └── word_generator.py ├── package-lock.json └── requirements.txt ├── docker-compose.yml ├── frontend ├── .env.development ├── .env.production ├── .eslintrc.js ├── Dockerfile ├── nginx │ └── default.conf ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── components │ │ ├── AdvancedDocumentForm.vue │ │ ├── DocumentForm.vue │ │ ├── Footer.vue │ │ ├── Header.vue │ │ └── ProgressIndicator.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── services │ │ └── api.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ └── documents.js │ └── views │ │ ├── Generate.vue │ │ ├── History.vue │ │ ├── Home.vue │ │ └── Results.vue └── vue.config.js ├── images ├── advanced.png ├── document_form.png ├── homepage.png ├── result.png ├── simple.png ├── 强化学习_document.docx └── 量化投资:寻找阿尔法因子_presentation.pptx └── nginx ├── Dockerfile └── nginx.conf /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/.DS_Store -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | AI_API_KEY= 2 | AI_API_ENDPOINT=https://api.deepseek.com/v1/chat/completions 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 依赖项 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | .pnpm-debug.log 7 | 8 | # 前端构建输出 9 | /frontend/dist/ 10 | /frontend/.cache/ 11 | 12 | # Python 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | *.so 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | venv/ 34 | .venv/ 35 | ENV/ 36 | 37 | # 生成的文档 38 | generated_docs/ 39 | 40 | # 环境变量文件(包含敏感信息) 41 | .env 42 | .env.local 43 | .env.development.local 44 | .env.test.local 45 | .env.production.local 46 | 47 | # 编辑器和 IDE 文件 48 | .idea/ 49 | .vscode/ 50 | *.swp 51 | *.swo 52 | *~ 53 | 54 | # 操作系统文件 55 | .DS_Store 56 | Thumbs.db 57 | 58 | # 日志文件 59 | *.log 60 | logs/ 61 | log/ 62 | 63 | # 测试覆盖率报告 64 | coverage/ 65 | .coverage 66 | htmlcov/ 67 | 68 | # 其他 69 | .pytest_cache/ 70 | .mypy_cache/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI 文档生成器 2 | 3 | 一个基于 AI 的文档生成系统,可以根据用户提供的主题自动生成 PPT 和 Word 文档。 4 | 5 | 6 | ![项目首页](images/homepage.png) 7 | 8 | ## 功能特点 9 | 10 | - 根据主题自动生成文档大纲 11 | - 支持 PPT 和 Word 文档格式 12 | - 实时显示生成进度 13 | - 支持文档下载和预览 14 | - 使用 Server-Sent Events (SSE) 实现实时进度更新 15 | - **新增:高级文档创建功能,可自定义页数限制和具体内容** 16 | 17 | ## 效果展示 18 | 19 | ### 创建文档界面 20 | 21 | ![创建文档界面](images/simple.png) 22 | 23 | ![创建高级文档界面](images/advanced.png) 24 | 25 | ### 文档生成界面 26 | 27 | ![文档生成界面](images/document_form.png) 28 | 29 | ### 生成结果 30 | 31 | ![生成结果](images/result.png) 32 | 33 | ### 样本文档 34 | 35 | 我们提供了一些由系统生成的样本文档,您可以查看以了解系统的能力: 36 | 37 | - [PPT 样本:量化投资:寻找阿尔法因子](images/量化投资:寻找阿尔法因子_presentation.pptx) 38 | - [Word 样本:强化学习](images/强化学习_document.docx) 39 | 40 | ## 技术栈 41 | 42 | ### 前端 43 | - Vue.js 44 | - Vuex 45 | - Vue Router 46 | - Axios 47 | 48 | ### 后端 49 | - FastAPI 50 | - Python-docx (Word 文档生成) 51 | - Python-pptx (PPT 生成) 52 | - DeepSeek API (AI 内容生成) 53 | 54 | 55 | ## 环境变量 56 | 57 | 创建一个 `.env` 文件在后端目录中: 58 | 59 | ``` 60 | AI_API_KEY=your_deepseek_api_key 61 | AI_API_ENDPOINT=https://api.deepseek.com/v1/chat/completions 62 | ``` 63 | 64 | ## 安裝与运行 65 | 66 | ```bash 67 | docker pull ubuntu:20.04 68 | docker compose build 69 | docker compose up -d 70 | ``` 71 | 浏览器登陆: localhost:3000 72 | 73 | ## 使用流程 74 | 75 | 1. 在首页点击"创建新文档"按钮 76 | 2. 选择基础模式或高级模式: 77 | - **基础模式**:只需输入主题和文档类型 78 | - **高级模式**:可以设置页数限制和自定义每页/章节内容 79 | 3. 填写文档主题(如"人工智能"、"区块链"等) 80 | 4. 选择文档类型(PPT 或 Word) 81 | 5. 可选:添加额外信息或选择模板 82 | 6. 点击"生成文档"按钮 83 | 7. 等待文档生成完成(您可以实时查看进度) 84 | 8. 下载或预览生成的文档 85 | 86 | 87 | ## 更新日志 88 | 89 | ### 2025-03-22 90 | - 新增高级文档创建功能,允许用户限制页数和自定义具体内容, 优化ppt内容生成的格式 91 | 92 | ### 2025-03-21 93 | - 更新了安装和运行改为docker部署, 对代码进行重构 94 | ### 2025-03-07 95 | - 这周比较忙,继续更新项目,让AI能够生成更好的ppt内容,目前只使用了deepseek的api。 96 | 97 | ## 许可证 98 | [MIT](LICENSE) 99 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/python:3.9-slim as build 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | FROM docker.io/python:3.9-slim 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=build /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages 14 | COPY . . 15 | 16 | # 创建非root用户运行应用 17 | RUN useradd -m appuser 18 | USER appuser 19 | 20 | EXPOSE 8001 21 | 22 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] -------------------------------------------------------------------------------- /backend/Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | WORKDIR /app 4 | 5 | # 安装Python和pip 6 | RUN apt-get update && \ 7 | apt-get install -y python3 python3-pip && \ 8 | apt-get clean && \ 9 | rm -rf /var/lib/apt/lists/* 10 | 11 | # 创建Python软链接 12 | RUN ln -sf /usr/bin/python3 /usr/bin/python 13 | 14 | # 不复制文件,因为docker-compose中会挂载卷 15 | # 但是安装依赖 16 | COPY requirements.txt . 17 | RUN pip3 install --no-cache-dir -r requirements.txt 18 | 19 | # 创建必要的目录并设置权限 20 | RUN mkdir -p /app/generated_docs && \ 21 | chmod 777 /app/generated_docs 22 | 23 | EXPOSE 8001 24 | 25 | # 添加健康检查 26 | HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ 27 | CMD curl -f http://localhost:8001/ || exit 1 28 | 29 | CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] -------------------------------------------------------------------------------- /backend/app/api/dependencies.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | from typing import Optional 3 | 4 | from ..providers.provider_factory import ProviderFactory 5 | from ..providers.ai.ai_provider import AIProvider 6 | from ..providers.storage.storage_provider import StorageProvider 7 | from ..providers.cache_provider import CacheProvider 8 | from ..controllers.document_controller import DocumentController 9 | 10 | # 缓存Provider单例 11 | _cache_provider = None 12 | 13 | # 存储Provider单例 14 | _storage_provider = None 15 | 16 | # 文档Controller单例 17 | _document_controller = None 18 | 19 | def get_ai_provider(provider_type: str = "deepseek") -> AIProvider: 20 | """获取AI Provider依赖""" 21 | return ProviderFactory.get_ai_provider(provider_type) 22 | 23 | def get_storage_provider() -> StorageProvider: 24 | """获取存储Provider依赖(单例)""" 25 | global _storage_provider 26 | if _storage_provider is None: 27 | _storage_provider = ProviderFactory.get_storage_provider("local") 28 | return _storage_provider 29 | 30 | def get_cache_provider() -> Optional[CacheProvider]: 31 | """获取缓存Provider依赖(单例)""" 32 | global _cache_provider 33 | if _cache_provider is None: 34 | _cache_provider = ProviderFactory.get_cache_provider("memory") 35 | return _cache_provider 36 | 37 | def get_document_controller( 38 | ai_provider: AIProvider = Depends(get_ai_provider), 39 | storage_provider: StorageProvider = Depends(get_storage_provider), 40 | cache_provider: Optional[CacheProvider] = Depends(get_cache_provider) 41 | ) -> DocumentController: 42 | """获取文档Controller依赖(单例)""" 43 | global _document_controller 44 | if _document_controller is None: 45 | _document_controller = DocumentController( 46 | ai_provider=ai_provider, 47 | storage_provider=storage_provider, 48 | cache_provider=cache_provider 49 | ) 50 | return _document_controller -------------------------------------------------------------------------------- /backend/app/controllers/base_controller.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, Generic, List, Optional, TypeVar 3 | from fastapi import BackgroundTasks, Depends, HTTPException 4 | 5 | from ..providers.base_provider import BaseProvider 6 | 7 | T = TypeVar('T') 8 | 9 | class BaseController(ABC, Generic[T]): 10 | """ 11 | 基础Controller类,定义了Controller的基本方法 12 | 13 | 泛型参数T定义了Controller操作的资源类型 14 | """ 15 | 16 | def __init__(self, provider: BaseProvider[T]): 17 | """ 18 | 初始化Controller 19 | 20 | Args: 21 | provider: 资源提供者 22 | """ 23 | self.provider = provider 24 | 25 | async def get(self, resource_id: str) -> T: 26 | """ 27 | 获取指定ID的资源 28 | 29 | Args: 30 | resource_id: 资源ID 31 | 32 | Returns: 33 | 资源对象 34 | 35 | Raises: 36 | HTTPException: 如果资源不存在 37 | """ 38 | resource = await self.provider.get(resource_id) 39 | if not resource: 40 | raise HTTPException(status_code=404, detail=f"资源不存在: {resource_id}") 41 | return resource 42 | 43 | async def create(self, data: Dict[str, Any], background_tasks: Optional[BackgroundTasks] = None) -> T: 44 | """ 45 | 创建新资源 46 | 47 | Args: 48 | data: 资源数据 49 | background_tasks: 后台任务队列(可选) 50 | 51 | Returns: 52 | 新创建的资源对象 53 | """ 54 | return await self.provider.create(data) 55 | 56 | async def update(self, resource_id: str, data: Dict[str, Any]) -> T: 57 | """ 58 | 更新资源 59 | 60 | Args: 61 | resource_id: 资源ID 62 | data: 更新数据 63 | 64 | Returns: 65 | 更新后的资源对象 66 | 67 | Raises: 68 | HTTPException: 如果资源不存在 69 | """ 70 | resource = await self.provider.update(resource_id, data) 71 | if not resource: 72 | raise HTTPException(status_code=404, detail=f"资源不存在: {resource_id}") 73 | return resource 74 | 75 | async def delete(self, resource_id: str) -> Dict[str, bool]: 76 | """ 77 | 删除资源 78 | 79 | Args: 80 | resource_id: 资源ID 81 | 82 | Returns: 83 | 包含success字段的对象 84 | 85 | Raises: 86 | HTTPException: 如果资源不存在或删除失败 87 | """ 88 | success = await self.provider.delete(resource_id) 89 | if not success: 90 | raise HTTPException(status_code=404, detail=f"资源不存在或删除失败: {resource_id}") 91 | return {"success": True} 92 | 93 | async def list(self, filters: Optional[Dict[str, Any]] = None) -> List[T]: 94 | """ 95 | 列出符合条件的资源 96 | 97 | Args: 98 | filters: 过滤条件 99 | 100 | Returns: 101 | 资源列表 102 | """ 103 | return await self.provider.list(filters) -------------------------------------------------------------------------------- /backend/app/controllers/document_controller.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional 2 | from fastapi import BackgroundTasks, Depends, HTTPException 3 | import uuid 4 | from datetime import datetime 5 | import logging 6 | 7 | from ..models.schemas import DocumentRequest, DocumentResponse, AdvancedDocumentRequest, GenerationStatus 8 | from ..providers.ai.ai_provider import AIProvider 9 | from ..providers.storage.storage_provider import StorageProvider 10 | from ..providers.cache_provider import CacheProvider 11 | from .base_controller import BaseController 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | class DocumentController: 16 | """文档控制器,处理文档生成相关逻辑""" 17 | 18 | def __init__( 19 | self, 20 | ai_provider: AIProvider, 21 | storage_provider: StorageProvider, 22 | cache_provider: Optional[CacheProvider] = None 23 | ): 24 | """ 25 | 初始化文档控制器 26 | 27 | Args: 28 | ai_provider: AI服务提供者 29 | storage_provider: 存储服务提供者 30 | cache_provider: 缓存服务提供者(可选) 31 | """ 32 | self.ai_provider = ai_provider 33 | self.storage_provider = storage_provider 34 | self.cache_provider = cache_provider 35 | self.generation_tasks = {} # 存储生成任务的状态 36 | 37 | async def create_document( 38 | self, 39 | request: DocumentRequest, 40 | background_tasks: BackgroundTasks 41 | ) -> DocumentResponse: 42 | """ 43 | 创建文档生成任务 44 | 45 | Args: 46 | request: 文档请求 47 | background_tasks: 后台任务队列 48 | 49 | Returns: 50 | 文档响应 51 | """ 52 | # 确保文档类型有效 53 | if request.doc_type not in ["ppt", "word", "pdf"]: 54 | raise HTTPException(status_code=400, detail="无效的文档类型") 55 | 56 | # 生成唯一ID 57 | doc_id = str(uuid.uuid4()) 58 | 59 | # 创建初始响应 60 | response = DocumentResponse( 61 | id=doc_id, 62 | topic=request.topic, 63 | doc_type=request.doc_type, 64 | status="queued", 65 | created_at=datetime.now().isoformat() 66 | ) 67 | 68 | # 存储任务状态 69 | self.generation_tasks[doc_id] = { 70 | "status": "queued", 71 | "progress": 0.0, 72 | "message": "任务已加入队列", 73 | "topic": request.topic, 74 | "doc_type": request.doc_type 75 | } 76 | 77 | # 添加到后台任务 78 | background_tasks.add_task( 79 | self.generate_document_background, 80 | doc_id=doc_id, 81 | topic=request.topic, 82 | doc_type=request.doc_type, 83 | additional_info=request.additional_info, 84 | template_id=request.template_id, 85 | ai_service_type=request.ai_service_type 86 | ) 87 | 88 | return response 89 | 90 | async def create_advanced_document( 91 | self, 92 | request: AdvancedDocumentRequest, 93 | background_tasks: BackgroundTasks 94 | ) -> DocumentResponse: 95 | """ 96 | 创建高级文档生成任务 97 | 98 | Args: 99 | request: 高级文档请求 100 | background_tasks: 后台任务队列 101 | 102 | Returns: 103 | 文档响应 104 | """ 105 | # 确保文档类型有效 106 | if request.doc_type not in ["ppt", "word", "pdf"]: 107 | raise HTTPException(status_code=400, detail="无效的文档类型") 108 | 109 | # 生成唯一ID 110 | doc_id = str(uuid.uuid4()) 111 | 112 | # 创建初始响应 113 | response = DocumentResponse( 114 | id=doc_id, 115 | topic=request.topic, 116 | doc_type=request.doc_type, 117 | status="queued", 118 | created_at=datetime.now().isoformat() 119 | ) 120 | 121 | # 存储任务状态 122 | self.generation_tasks[doc_id] = { 123 | "status": "queued", 124 | "progress": 0.0, 125 | "message": "高级文档生成任务已加入队列", 126 | "topic": request.topic, 127 | "doc_type": request.doc_type 128 | } 129 | 130 | # 添加到后台任务 131 | background_tasks.add_task( 132 | self.generate_advanced_document_background, 133 | doc_id=doc_id, 134 | topic=request.topic, 135 | doc_type=request.doc_type, 136 | additional_info=request.additional_info, 137 | template_id=request.template_id, 138 | ai_service_type=request.ai_service_type, 139 | max_pages=request.max_pages, 140 | detailed_content=request.detailed_content 141 | ) 142 | 143 | return response 144 | 145 | async def get_document_status(self, doc_id: str) -> GenerationStatus: 146 | """ 147 | 获取文档生成任务的状态 148 | 149 | Args: 150 | doc_id: 文档ID 151 | 152 | Returns: 153 | 生成状态 154 | 155 | Raises: 156 | HTTPException: 如果任务不存在 157 | """ 158 | if doc_id not in self.generation_tasks: 159 | raise HTTPException(status_code=404, detail="任务不存在") 160 | 161 | task_info = self.generation_tasks[doc_id] 162 | return GenerationStatus( 163 | id=doc_id, 164 | status=task_info["status"], 165 | progress=task_info["progress"], 166 | message=task_info.get("message") 167 | ) 168 | 169 | async def get_document(self, document_id: str) -> DocumentResponse: 170 | """ 171 | 获取生成的文档信息 172 | 173 | Args: 174 | document_id: 文档ID 175 | 176 | Returns: 177 | 文档响应 178 | 179 | Raises: 180 | HTTPException: 如果文档不存在 181 | """ 182 | if document_id not in self.generation_tasks: 183 | raise HTTPException( 184 | status_code=404, 185 | detail=f"文档不存在,可用的文档ID: {list(self.generation_tasks.keys())}" 186 | ) 187 | 188 | task_info = self.generation_tasks[document_id] 189 | 190 | if task_info["status"] != "completed": 191 | return DocumentResponse( 192 | id=document_id, 193 | topic=task_info.get("topic", "未知"), 194 | doc_type=task_info.get("doc_type", "ppt"), 195 | status=task_info["status"], 196 | created_at=datetime.now().isoformat() 197 | ) 198 | 199 | return DocumentResponse( 200 | id=document_id, 201 | topic=task_info.get("topic", "未知"), 202 | doc_type=task_info.get("doc_type", "ppt"), 203 | status=task_info["status"], 204 | download_url=task_info.get("download_url"), 205 | preview_url=task_info.get("preview_url"), 206 | created_at=task_info.get("created_at", datetime.now().isoformat()) 207 | ) 208 | 209 | async def generate_document_background( 210 | self, 211 | doc_id: str, 212 | topic: str, 213 | doc_type: str, 214 | additional_info: Optional[str] = None, 215 | template_id: Optional[str] = None, 216 | ai_service_type: str = "deepseek" 217 | ): 218 | """ 219 | 后台生成文档 220 | 221 | Args: 222 | doc_id: 文档ID 223 | topic: 主题 224 | doc_type: 文档类型 225 | additional_info: 额外信息 226 | template_id: 模板ID 227 | ai_service_type: AI服务类型 228 | """ 229 | # 处理文档生成的实际逻辑 230 | # 具体实现将在子类中提供 231 | pass 232 | 233 | async def generate_advanced_document_background( 234 | self, 235 | doc_id: str, 236 | topic: str, 237 | doc_type: str, 238 | additional_info: Optional[str] = None, 239 | template_id: Optional[str] = None, 240 | ai_service_type: str = "deepseek", 241 | max_pages: Optional[int] = None, 242 | detailed_content: Optional[List[Dict[str, Any]]] = None 243 | ): 244 | """ 245 | 后台生成高级文档 246 | 247 | Args: 248 | doc_id: 文档ID 249 | topic: 主题 250 | doc_type: 文档类型 251 | additional_info: 额外信息 252 | template_id: 模板ID 253 | ai_service_type: AI服务类型 254 | max_pages: 最大页数 255 | detailed_content: 详细内容 256 | """ 257 | # 处理高级文档生成的实际逻辑 258 | # 具体实现将在子类中提供 259 | pass -------------------------------------------------------------------------------- /backend/app/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | from typing import Optional 3 | import os 4 | from dotenv import load_dotenv 5 | 6 | # 加载.env文件中的环境变量 7 | load_dotenv() 8 | 9 | class Settings(BaseSettings): 10 | API_V1_STR: str = "/api/v1" 11 | PROJECT_NAME: str = "AI Doc Platform" 12 | 13 | # DeepSeek API配置 14 | AI_API_KEY: str = os.getenv("AI_API_KEY", "") 15 | AI_API_ENDPOINT: str = os.getenv("AI_API_ENDPOINT", "https://api.deepseek.com/v1/chat/completions") 16 | 17 | # 安全配置 18 | SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-for-jwt") 19 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days 20 | 21 | # 数据库配置 22 | MONGODB_URL: str = os.getenv("MONGODB_URL", "mongodb://mongo:27017") 23 | MONGODB_DB_NAME: str = os.getenv("MONGODB_DB_NAME", "ai_doc_platform") 24 | 25 | # 多种AI服务设置 26 | OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "") 27 | OPENAI_API_ENDPOINT: str = os.getenv("OPENAI_API_ENDPOINT", "https://api.openai.com/v1/chat/completions") 28 | CLAUDE_API_KEY: str = os.getenv("CLAUDE_API_KEY", "") 29 | CLAUDE_API_ENDPOINT: str = os.getenv("CLAUDE_API_ENDPOINT", "https://api.anthropic.com/v1/messages") 30 | 31 | class Config: 32 | env_file = ".env" 33 | case_sensitive = True 34 | 35 | settings = Settings() -------------------------------------------------------------------------------- /backend/app/core/database.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, Optional 3 | import json 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | class InMemoryDatabase: 8 | """内存数据库实现""" 9 | 10 | def __init__(self): 11 | self.collections: Dict[str, Dict[str, Any]] = {} 12 | logger.info("使用内存数据库") 13 | 14 | def __getitem__(self, collection_name: str): 15 | if collection_name not in self.collections: 16 | self.collections[collection_name] = {} 17 | logger.info(f"创建内存集合: {collection_name}") 18 | return InMemoryCollection(self.collections[collection_name]) 19 | 20 | class InMemoryCollection: 21 | """内存集合实现""" 22 | 23 | def __init__(self, data: Dict[str, Any]): 24 | self.data = data 25 | 26 | async def find_one(self, query: Dict[str, Any]) -> Optional[Dict[str, Any]]: 27 | """查找单个文档""" 28 | for doc_id, doc in self.data.items(): 29 | matches = True 30 | for key, value in query.items(): 31 | if key not in doc or doc[key] != value: 32 | matches = False 33 | break 34 | if matches: 35 | return dict(doc) 36 | return None 37 | 38 | async def insert_one(self, document: Dict[str, Any]) -> Any: 39 | """插入单个文档""" 40 | # 创建简单的自增ID 41 | doc_id = str(len(self.data) + 1) 42 | document["_id"] = doc_id 43 | self.data[doc_id] = document 44 | logger.debug(f"插入文档: {document}") 45 | return type('InsertOneResult', (), {'inserted_id': doc_id}) 46 | 47 | async def update_one(self, query: Dict[str, Any], update: Dict[str, Any], upsert: bool = False) -> Any: 48 | """更新单个文档""" 49 | doc = await self.find_one(query) 50 | 51 | if doc: 52 | doc_id = doc["_id"] 53 | if "$set" in update: 54 | for key, value in update["$set"].items(): 55 | self.data[doc_id][key] = value 56 | logger.debug(f"更新文档 {doc_id}: {update}") 57 | return type('UpdateResult', (), {'modified_count': 1, 'matched_count': 1}) 58 | elif upsert: 59 | # 创建新文档 60 | new_doc = {} 61 | for key, value in query.items(): 62 | new_doc[key] = value 63 | 64 | if "$set" in update: 65 | for key, value in update["$set"].items(): 66 | new_doc[key] = value 67 | 68 | result = await self.insert_one(new_doc) 69 | return type('UpdateResult', (), {'modified_count': 0, 'matched_count': 0, 'upserted_id': result.inserted_id}) 70 | else: 71 | return type('UpdateResult', (), {'modified_count': 0, 'matched_count': 0}) 72 | 73 | async def delete_one(self, query: Dict[str, Any]) -> Any: 74 | """删除单个文档""" 75 | doc = await self.find_one(query) 76 | if doc: 77 | doc_id = doc["_id"] 78 | del self.data[doc_id] 79 | logger.debug(f"删除文档: {doc_id}") 80 | return type('DeleteResult', (), {'deleted_count': 1}) 81 | return type('DeleteResult', (), {'deleted_count': 0}) 82 | 83 | async def delete_many(self, query: Dict[str, Any]) -> Any: 84 | """删除多个文档""" 85 | to_delete = [] 86 | for doc_id, doc in self.data.items(): 87 | matches = True 88 | for key, value in query.items(): 89 | if key not in doc or doc[key] != value: 90 | matches = False 91 | break 92 | if matches: 93 | to_delete.append(doc_id) 94 | 95 | for doc_id in to_delete: 96 | del self.data[doc_id] 97 | 98 | logger.debug(f"批量删除 {len(to_delete)} 个文档") 99 | return type('DeleteResult', (), {'deleted_count': len(to_delete)}) 100 | 101 | async def create_index(self, key: str, **kwargs): 102 | """创建索引(模拟)""" 103 | logger.debug(f"创建内存索引: {key} {kwargs}") 104 | # 内存实现不需要索引 105 | pass 106 | 107 | class Database: 108 | db = None 109 | 110 | db = Database() 111 | 112 | async def connect_to_mongodb(): 113 | """初始化数据库连接""" 114 | logger.info("初始化内存数据库...") 115 | db.db = InMemoryDatabase() 116 | logger.info("内存数据库初始化完成") 117 | 118 | async def close_mongodb_connection(): 119 | """关闭数据库连接""" 120 | logger.info("关闭数据库连接...") 121 | # 内存数据库无需关闭连接 122 | logger.info("数据库连接已关闭") 123 | 124 | def get_database(): 125 | """获取数据库对象""" 126 | return db.db -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from fastapi.staticfiles import StaticFiles 4 | import os 5 | import logging 6 | 7 | from .api.routes import router as api_router 8 | from .core.config import settings 9 | from .core.database import connect_to_mongodb, close_mongodb_connection 10 | 11 | # 配置日志 12 | logging.basicConfig( 13 | level=logging.INFO, 14 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 15 | ) 16 | logger = logging.getLogger(__name__) 17 | 18 | app = FastAPI(title=settings.PROJECT_NAME) 19 | 20 | # 配置CORS 21 | app.add_middleware( 22 | CORSMiddleware, 23 | allow_origins=["*", "http://localhost:3000", "http://localhost:8080"], # 明确包含前端域名 24 | allow_credentials=True, 25 | allow_methods=["*"], 26 | allow_headers=["*"], 27 | expose_headers=["*"] 28 | ) 29 | 30 | # 挂载API路由 31 | app.include_router(api_router, prefix=settings.API_V1_STR) 32 | 33 | # 创建下载和预览目录 34 | docs_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "generated_docs")) 35 | logger.info(f"文档生成路径: {docs_path}") 36 | os.makedirs(docs_path, exist_ok=True) 37 | 38 | # 确保目录具有正确的权限 39 | os.chmod(docs_path, 0o777) 40 | 41 | # 挂载静态文件服务 42 | app.mount("/downloads", StaticFiles(directory=docs_path), name="downloads") 43 | app.mount("/previews", StaticFiles(directory=docs_path), name="previews") 44 | 45 | @app.get("/") 46 | def read_root(): 47 | return {"message": "Welcome to AI Doc Platform API"} 48 | 49 | @app.get("/api/health") 50 | def health_check(): 51 | """健康检查接口""" 52 | return {"status": "healthy"} 53 | 54 | # 启动事件 55 | @app.on_event("startup") 56 | async def startup_db_client(): 57 | """启动时连接数据库""" 58 | await connect_to_mongodb() 59 | 60 | # 关闭事件 61 | @app.on_event("shutdown") 62 | async def shutdown_db_client(): 63 | """关闭时断开数据库连接""" 64 | await close_mongodb_connection() 65 | 66 | if __name__ == "__main__": 67 | import uvicorn 68 | uvicorn.run("app.main:app", host="0.0.0.0", port=8001, reload=True) -------------------------------------------------------------------------------- /backend/app/models/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import List, Optional, Dict, Any 3 | from enum import Enum 4 | from datetime import datetime 5 | 6 | class DocumentType(str, Enum): 7 | PPT = "ppt" 8 | WORD = "word" 9 | PDF = "pdf" 10 | 11 | class DocumentRequest(BaseModel): 12 | topic: str = Field(..., description="文档的主题") 13 | doc_type: str = Field(..., description="文档类型") 14 | additional_info: Optional[str] = Field(None, description="额外的信息或要求") 15 | template_id: Optional[str] = Field(None, description="模板ID,如果使用预定义模板") 16 | ai_service_type: Optional[str] = Field("deepseek", description="AI服务类型,如deepseek、openai等") 17 | 18 | # 新增高级文档请求模型 19 | class PageChapterContent(BaseModel): 20 | """页面或章节的具体内容定义""" 21 | title: str = Field(..., description="页面或章节标题") 22 | content: Optional[str] = Field(None, description="页面或章节的具体内容") 23 | position: int = Field(..., description="页面/章节的位置顺序") 24 | 25 | class AdvancedDocumentRequest(DocumentRequest): 26 | """高级文档请求,支持限制页数和定义具体页面/章节内容""" 27 | max_pages: Optional[int] = Field(None, description="限制生成的最大页数/章节数") 28 | detailed_content: Optional[List[PageChapterContent]] = Field(None, description="用户定义的页面/章节内容") 29 | is_advanced_mode: bool = Field(True, description="标记为高级模式") 30 | 31 | class DocumentResponse(BaseModel): 32 | id: str = Field(..., description="文档ID") 33 | topic: str = Field(..., description="文档的主题") 34 | doc_type: DocumentType = Field(..., description="文档类型") 35 | status: str = Field(..., description="生成状态") 36 | download_url: Optional[str] = Field(None, description="下载URL") 37 | preview_url: Optional[str] = Field(None, description="预览URL") 38 | created_at: datetime = Field(..., description="创建时间") 39 | 40 | class DocumentOutline(BaseModel): 41 | title: str = Field(..., description="文档标题") 42 | sections: List[Dict[str, Any]] = Field(..., description="文档章节") 43 | 44 | class GenerationStatus(BaseModel): 45 | id: str = Field(..., description="任务ID") 46 | status: str = Field(..., description="当前状态") 47 | progress: float = Field(..., description="完成百分比") 48 | message: Optional[str] = Field(None, description="状态消息") -------------------------------------------------------------------------------- /backend/app/providers/ai/ai_provider.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, List, Optional 3 | 4 | class AIProvider(ABC): 5 | """ 6 | AI服务提供者接口,定义了所有AI服务必须实现的方法 7 | """ 8 | 9 | @abstractmethod 10 | async def generate_completion(self, messages: List[Dict[str, str]], 11 | temperature: float = 0.7, 12 | max_tokens: int = 2000) -> Optional[str]: 13 | """ 14 | 生成文本完成 15 | 16 | Args: 17 | messages: 消息列表,格式为[{"role": "user", "content": "你好"}] 18 | temperature: 温度参数,控制随机性 19 | max_tokens: 生成的最大token数 20 | 21 | Returns: 22 | 生成的文本,如果请求失败则返回None 23 | """ 24 | pass 25 | 26 | @abstractmethod 27 | async def generate_document_outline(self, topic: str, doc_type: str, 28 | additional_info: Optional[str] = None) -> Optional[List[Dict[str, Any]]]: 29 | """ 30 | 生成文档大纲 31 | 32 | Args: 33 | topic: 文档主题 34 | doc_type: 文档类型 (ppt, word, pdf) 35 | additional_info: 额外的信息或要求 36 | 37 | Returns: 38 | 文档大纲,如果生成失败则返回None 39 | """ 40 | pass 41 | 42 | @abstractmethod 43 | async def generate_section_content(self, topic: str, section_title: str, 44 | doc_type: str, 45 | additional_info: Optional[str] = None) -> Optional[str]: 46 | """ 47 | 生成文档章节内容 48 | 49 | Args: 50 | topic: 文档主题 51 | section_title: 章节标题 52 | doc_type: 文档类型 (ppt, word, pdf) 53 | additional_info: 额外的信息或要求 54 | 55 | Returns: 56 | 章节内容,如果生成失败则返回None 57 | """ 58 | pass -------------------------------------------------------------------------------- /backend/app/providers/base_provider.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, Generic, List, Optional, TypeVar 3 | 4 | T = TypeVar('T') 5 | 6 | class BaseProvider(ABC, Generic[T]): 7 | """ 8 | 基础Provider接口类,定义了所有Provider必须实现的方法 9 | 10 | 泛型参数T定义了Provider操作的资源类型 11 | """ 12 | 13 | @abstractmethod 14 | async def get(self, resource_id: str) -> Optional[T]: 15 | """ 16 | 获取指定ID的资源 17 | 18 | Args: 19 | resource_id: 资源ID 20 | 21 | Returns: 22 | 资源对象,如果不存在则返回None 23 | """ 24 | pass 25 | 26 | @abstractmethod 27 | async def create(self, data: Dict[str, Any]) -> T: 28 | """ 29 | 创建新资源 30 | 31 | Args: 32 | data: 资源数据 33 | 34 | Returns: 35 | 新创建的资源对象 36 | """ 37 | pass 38 | 39 | @abstractmethod 40 | async def update(self, resource_id: str, data: Dict[str, Any]) -> Optional[T]: 41 | """ 42 | 更新资源 43 | 44 | Args: 45 | resource_id: 资源ID 46 | data: 更新数据 47 | 48 | Returns: 49 | 更新后的资源对象,如果资源不存在则返回None 50 | """ 51 | pass 52 | 53 | @abstractmethod 54 | async def delete(self, resource_id: str) -> bool: 55 | """ 56 | 删除资源 57 | 58 | Args: 59 | resource_id: 资源ID 60 | 61 | Returns: 62 | 是否成功删除 63 | """ 64 | pass 65 | 66 | @abstractmethod 67 | async def list(self, filters: Optional[Dict[str, Any]] = None) -> List[T]: 68 | """ 69 | 列出符合条件的资源 70 | 71 | Args: 72 | filters: 过滤条件 73 | 74 | Returns: 75 | 资源列表 76 | """ 77 | pass -------------------------------------------------------------------------------- /backend/app/providers/cache_provider.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, List, Optional, TypeVar, Generic 3 | 4 | T = TypeVar('T') 5 | 6 | class CacheProvider(ABC, Generic[T]): 7 | """ 8 | 缓存服务提供者接口,定义了所有缓存服务必须实现的方法 9 | 10 | 泛型参数T定义了缓存存储的数据类型 11 | """ 12 | 13 | @abstractmethod 14 | async def get(self, key: str) -> Optional[T]: 15 | """ 16 | 获取缓存数据 17 | 18 | Args: 19 | key: 缓存键 20 | 21 | Returns: 22 | 缓存的数据,如果不存在则返回None 23 | """ 24 | pass 25 | 26 | @abstractmethod 27 | async def set(self, key: str, value: T, ttl_seconds: Optional[int] = None) -> bool: 28 | """ 29 | 设置缓存数据 30 | 31 | Args: 32 | key: 缓存键 33 | value: 缓存值 34 | ttl_seconds: 过期时间(秒),如果为None则不过期 35 | 36 | Returns: 37 | 是否成功设置 38 | """ 39 | pass 40 | 41 | @abstractmethod 42 | async def delete(self, key: str) -> bool: 43 | """ 44 | 删除缓存数据 45 | 46 | Args: 47 | key: 缓存键 48 | 49 | Returns: 50 | 是否成功删除 51 | """ 52 | pass 53 | 54 | @abstractmethod 55 | async def clear(self) -> bool: 56 | """ 57 | 清空所有缓存 58 | 59 | Returns: 60 | 是否成功清空 61 | """ 62 | pass 63 | 64 | @abstractmethod 65 | async def ttl(self, key: str) -> Optional[int]: 66 | """ 67 | 获取缓存过期时间 68 | 69 | Args: 70 | key: 缓存键 71 | 72 | Returns: 73 | 剩余过期时间(秒),如果不存在或已过期则返回None 74 | """ 75 | pass -------------------------------------------------------------------------------- /backend/app/providers/mongodb_cache_provider.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict, List, Optional, TypeVar, Generic 3 | from datetime import datetime, timedelta 4 | 5 | from .cache_provider import CacheProvider 6 | 7 | T = TypeVar('T') 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class MemoryCacheProvider(CacheProvider[T]): 12 | """内存缓存Provider实现""" 13 | 14 | def __init__(self, collection_name: str, ttl_seconds: int = 604800): # 默认7天 15 | """ 16 | 初始化内存缓存Provider 17 | 18 | Args: 19 | collection_name: 集合名称(仅用于日志) 20 | ttl_seconds: 默认缓存过期时间(秒) 21 | """ 22 | self.collection_name = collection_name 23 | self.default_ttl = ttl_seconds 24 | self.cache: Dict[str, Dict[str, Any]] = {} 25 | logger.info(f"初始化内存缓存: {collection_name}, TTL: {ttl_seconds}秒") 26 | 27 | async def get(self, key: str) -> Optional[T]: 28 | """ 29 | 获取缓存数据 30 | 31 | Args: 32 | key: 缓存键 33 | 34 | Returns: 35 | 缓存的数据,如果不存在则返回None 36 | """ 37 | try: 38 | if key not in self.cache: 39 | logger.debug(f"缓存未命中: {key}") 40 | return None 41 | 42 | item = self.cache[key] 43 | 44 | # 检查是否过期 45 | if "expires_at" in item and item["expires_at"] < datetime.now(): 46 | logger.debug(f"缓存已过期: {key}") 47 | await self.delete(key) 48 | return None 49 | 50 | logger.debug(f"缓存命中: {key}") 51 | return item.get("value") 52 | except Exception as e: 53 | logger.error(f"获取缓存出错: {str(e)}") 54 | return None 55 | 56 | async def set(self, key: str, value: T, ttl_seconds: Optional[int] = None) -> bool: 57 | """ 58 | 设置缓存数据 59 | 60 | Args: 61 | key: 缓存键 62 | value: 缓存值 63 | ttl_seconds: 过期时间(秒),如果为None则使用默认值 64 | 65 | Returns: 66 | 是否成功设置 67 | """ 68 | try: 69 | ttl = ttl_seconds if ttl_seconds is not None else self.default_ttl 70 | expires_at = datetime.now() + timedelta(seconds=ttl) if ttl > 0 else None 71 | 72 | # 构造缓存项 73 | cache_item = { 74 | "key": key, 75 | "value": value, 76 | "created_at": datetime.now(), 77 | "updated_at": datetime.now() 78 | } 79 | 80 | if expires_at: 81 | cache_item["expires_at"] = expires_at 82 | 83 | # 更新缓存 84 | self.cache[key] = cache_item 85 | 86 | logger.debug(f"缓存已设置: {key}, TTL: {ttl}秒") 87 | return True 88 | except Exception as e: 89 | logger.error(f"设置缓存出错: {str(e)}") 90 | return False 91 | 92 | async def delete(self, key: str) -> bool: 93 | """ 94 | 删除缓存数据 95 | 96 | Args: 97 | key: 缓存键 98 | 99 | Returns: 100 | 是否成功删除 101 | """ 102 | try: 103 | if key in self.cache: 104 | del self.cache[key] 105 | logger.debug(f"已删除缓存: {key}") 106 | return True 107 | return False 108 | except Exception as e: 109 | logger.error(f"删除缓存出错: {str(e)}") 110 | return False 111 | 112 | async def clear(self) -> bool: 113 | """ 114 | 清空所有缓存 115 | 116 | Returns: 117 | 是否成功清空 118 | """ 119 | try: 120 | count = len(self.cache) 121 | self.cache.clear() 122 | logger.info(f"已清空缓存, 删除数量: {count}") 123 | return True 124 | except Exception as e: 125 | logger.error(f"清空缓存出错: {str(e)}") 126 | return False 127 | 128 | async def ttl(self, key: str) -> Optional[int]: 129 | """ 130 | 获取缓存过期时间 131 | 132 | Args: 133 | key: 缓存键 134 | 135 | Returns: 136 | 剩余过期时间(秒),如果不存在或已过期则返回None 137 | """ 138 | try: 139 | if key not in self.cache or "expires_at" not in self.cache[key]: 140 | return None 141 | 142 | # 计算剩余时间 143 | remaining = (self.cache[key]["expires_at"] - datetime.now()).total_seconds() 144 | return max(0, int(remaining)) if remaining > 0 else None 145 | except Exception as e: 146 | logger.error(f"获取TTL出错: {str(e)}") 147 | return None -------------------------------------------------------------------------------- /backend/app/providers/provider_factory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, Optional, Type 3 | 4 | from .ai.ai_provider import AIProvider 5 | from .storage.storage_provider import StorageProvider 6 | from .cache_provider import CacheProvider 7 | from .mongodb_cache_provider import MemoryCacheProvider 8 | from .storage.local_storage_provider import LocalStorageProvider 9 | 10 | # 导入所有AI Provider实现 11 | from ..services.deepseek_service import DeepSeekService 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | class ProviderFactory: 16 | """Provider工厂,用于创建和获取各类Provider""" 17 | 18 | # 支持的AI服务提供者映射 19 | AI_PROVIDERS = { 20 | "deepseek": DeepSeekService, 21 | # 可以添加其他AI Provider,如OpenAI、Claude等 22 | } 23 | 24 | # 支持的存储提供者映射 25 | STORAGE_PROVIDERS = { 26 | "local": LocalStorageProvider, 27 | # 可以添加其他存储Provider,如S3、Azure Blob等 28 | } 29 | 30 | # 支持的缓存提供者映射 31 | CACHE_PROVIDERS = { 32 | "memory": MemoryCacheProvider, 33 | # 可以添加其他缓存Provider,如Redis等 34 | } 35 | 36 | @classmethod 37 | def get_ai_provider(cls, provider_type: str = "deepseek", **kwargs) -> AIProvider: 38 | """ 39 | 获取AI Provider 40 | 41 | Args: 42 | provider_type: AI Provider类型 43 | **kwargs: 传递给Provider构造函数的参数 44 | 45 | Returns: 46 | AI Provider实例 47 | 48 | Raises: 49 | ValueError: 如果Provider类型不支持 50 | """ 51 | if provider_type not in cls.AI_PROVIDERS: 52 | supported = ", ".join(cls.AI_PROVIDERS.keys()) 53 | error_msg = f"不支持的AI Provider类型: {provider_type},支持的类型: {supported}" 54 | logger.error(error_msg) 55 | raise ValueError(error_msg) 56 | 57 | provider_class = cls.AI_PROVIDERS[provider_type] 58 | logger.info(f"创建AI Provider: {provider_type}") 59 | return provider_class(**kwargs) 60 | 61 | @classmethod 62 | def get_storage_provider(cls, provider_type: str = "local", **kwargs) -> StorageProvider: 63 | """ 64 | 获取存储Provider 65 | 66 | Args: 67 | provider_type: 存储Provider类型 68 | **kwargs: 传递给Provider构造函数的参数 69 | 70 | Returns: 71 | 存储Provider实例 72 | 73 | Raises: 74 | ValueError: 如果Provider类型不支持 75 | """ 76 | if provider_type not in cls.STORAGE_PROVIDERS: 77 | supported = ", ".join(cls.STORAGE_PROVIDERS.keys()) 78 | error_msg = f"不支持的存储Provider类型: {provider_type},支持的类型: {supported}" 79 | logger.error(error_msg) 80 | raise ValueError(error_msg) 81 | 82 | provider_class = cls.STORAGE_PROVIDERS[provider_type] 83 | logger.info(f"创建存储Provider: {provider_type}") 84 | return provider_class(**kwargs) 85 | 86 | @classmethod 87 | def get_cache_provider(cls, provider_type: str = "memory", collection_name: str = "document_cache", **kwargs) -> Optional[CacheProvider]: 88 | """ 89 | 获取缓存Provider 90 | 91 | Args: 92 | provider_type: 缓存Provider类型 93 | collection_name: 集合名称 94 | **kwargs: 传递给Provider构造函数的参数 95 | 96 | Returns: 97 | 缓存Provider实例,如果类型不支持则返回None 98 | """ 99 | if provider_type not in cls.CACHE_PROVIDERS: 100 | logger.warning(f"不支持的缓存Provider类型: {provider_type},将不使用缓存") 101 | return None 102 | 103 | provider_class = cls.CACHE_PROVIDERS[provider_type] 104 | logger.info(f"创建缓存Provider: {provider_type}") 105 | return provider_class(collection_name=collection_name, **kwargs) -------------------------------------------------------------------------------- /backend/app/providers/storage/local_storage_provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from typing import List, Optional, BinaryIO 4 | from pathlib import Path 5 | 6 | from ...core.config import settings 7 | from .storage_provider import StorageProvider 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class LocalStorageProvider(StorageProvider): 12 | """本地文件存储Provider实现""" 13 | 14 | def __init__(self, base_dir: Optional[str] = None, base_url: Optional[str] = None): 15 | """ 16 | 初始化本地存储Provider 17 | 18 | Args: 19 | base_dir: 基础目录,默认为generated_docs 20 | base_url: 基础URL,默认为/downloads 21 | """ 22 | self.base_dir = base_dir or os.path.abspath(os.path.join( 23 | os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), 24 | "generated_docs" 25 | )) 26 | self.base_url = base_url or "/downloads" 27 | 28 | # 确保目录存在 29 | os.makedirs(self.base_dir, exist_ok=True) 30 | 31 | logger.info(f"本地存储Provider初始化,基础目录: {self.base_dir}") 32 | 33 | async def save_file(self, file_path: str, content: BinaryIO) -> bool: 34 | """ 35 | 保存文件 36 | 37 | Args: 38 | file_path: 文件路径(相对于基础目录) 39 | content: 文件内容 40 | 41 | Returns: 42 | 是否成功保存 43 | """ 44 | try: 45 | # 获取完整路径 46 | full_path = os.path.join(self.base_dir, file_path) 47 | 48 | # 确保父目录存在 49 | os.makedirs(os.path.dirname(full_path), exist_ok=True) 50 | 51 | # 写入文件 52 | with open(full_path, 'wb') as f: 53 | f.write(content.read()) 54 | 55 | logger.info(f"文件已保存: {full_path}") 56 | return True 57 | except Exception as e: 58 | logger.error(f"保存文件失败: {str(e)}") 59 | return False 60 | 61 | async def get_file(self, file_path: str) -> Optional[bytes]: 62 | """ 63 | 获取文件内容 64 | 65 | Args: 66 | file_path: 文件路径(相对于基础目录) 67 | 68 | Returns: 69 | 文件内容,如果文件不存在则返回None 70 | """ 71 | try: 72 | # 获取完整路径 73 | full_path = os.path.join(self.base_dir, file_path) 74 | 75 | # 检查文件是否存在 76 | if not os.path.exists(full_path): 77 | logger.warning(f"文件不存在: {full_path}") 78 | return None 79 | 80 | # 读取文件 81 | with open(full_path, 'rb') as f: 82 | return f.read() 83 | except Exception as e: 84 | logger.error(f"获取文件失败: {str(e)}") 85 | return None 86 | 87 | async def delete_file(self, file_path: str) -> bool: 88 | """ 89 | 删除文件 90 | 91 | Args: 92 | file_path: 文件路径(相对于基础目录) 93 | 94 | Returns: 95 | 是否成功删除 96 | """ 97 | try: 98 | # 获取完整路径 99 | full_path = os.path.join(self.base_dir, file_path) 100 | 101 | # 检查文件是否存在 102 | if not os.path.exists(full_path): 103 | logger.warning(f"文件不存在: {full_path}") 104 | return False 105 | 106 | # 删除文件 107 | os.remove(full_path) 108 | logger.info(f"文件已删除: {full_path}") 109 | return True 110 | except Exception as e: 111 | logger.error(f"删除文件失败: {str(e)}") 112 | return False 113 | 114 | async def list_files(self, directory: str = "") -> List[str]: 115 | """ 116 | 列出目录中的文件 117 | 118 | Args: 119 | directory: 目录路径(相对于基础目录),默认为根目录 120 | 121 | Returns: 122 | 文件路径列表(相对于基础目录) 123 | """ 124 | try: 125 | # 获取完整路径 126 | full_path = os.path.join(self.base_dir, directory) 127 | 128 | # 检查目录是否存在 129 | if not os.path.exists(full_path) or not os.path.isdir(full_path): 130 | logger.warning(f"目录不存在: {full_path}") 131 | return [] 132 | 133 | # 列出文件 134 | files = [] 135 | for root, _, filenames in os.walk(full_path): 136 | for filename in filenames: 137 | # 获取相对路径 138 | rel_path = os.path.relpath(os.path.join(root, filename), self.base_dir) 139 | files.append(rel_path) 140 | 141 | return files 142 | except Exception as e: 143 | logger.error(f"列出文件失败: {str(e)}") 144 | return [] 145 | 146 | async def get_file_url(self, file_path: str) -> str: 147 | """ 148 | 获取文件URL 149 | 150 | Args: 151 | file_path: 文件路径(相对于基础目录) 152 | 153 | Returns: 154 | 文件URL 155 | """ 156 | # 拼接URL 157 | return f"{self.base_url}/{file_path}" -------------------------------------------------------------------------------- /backend/app/providers/storage/storage_provider.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, List, Optional, BinaryIO 3 | from pathlib import Path 4 | 5 | class StorageProvider(ABC): 6 | """ 7 | 存储服务提供者接口,定义了所有存储服务必须实现的方法 8 | """ 9 | 10 | @abstractmethod 11 | async def save_file(self, file_path: str, content: BinaryIO) -> bool: 12 | """ 13 | 保存文件 14 | 15 | Args: 16 | file_path: 文件路径 17 | content: 文件内容 18 | 19 | Returns: 20 | 是否成功保存 21 | """ 22 | pass 23 | 24 | @abstractmethod 25 | async def get_file(self, file_path: str) -> Optional[bytes]: 26 | """ 27 | 获取文件内容 28 | 29 | Args: 30 | file_path: 文件路径 31 | 32 | Returns: 33 | 文件内容,如果文件不存在则返回None 34 | """ 35 | pass 36 | 37 | @abstractmethod 38 | async def delete_file(self, file_path: str) -> bool: 39 | """ 40 | 删除文件 41 | 42 | Args: 43 | file_path: 文件路径 44 | 45 | Returns: 46 | 是否成功删除 47 | """ 48 | pass 49 | 50 | @abstractmethod 51 | async def list_files(self, directory: str) -> List[str]: 52 | """ 53 | 列出目录中的文件 54 | 55 | Args: 56 | directory: 目录路径 57 | 58 | Returns: 59 | 文件路径列表 60 | """ 61 | pass 62 | 63 | @abstractmethod 64 | async def get_file_url(self, file_path: str) -> str: 65 | """ 66 | 获取文件URL 67 | 68 | Args: 69 | file_path: 文件路径 70 | 71 | Returns: 72 | 文件URL 73 | """ 74 | pass -------------------------------------------------------------------------------- /backend/app/services/advanced_content_generator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, List, Optional, Tuple, Callable 3 | import json 4 | from pydantic import ValidationError 5 | import re 6 | 7 | from .ai_service_factory import AIServiceFactory 8 | from .outline_generator import OutlineGenerator 9 | from ..models.schemas import PageChapterContent 10 | 11 | # 配置日志 12 | logging.basicConfig(level=logging.INFO) 13 | logger = logging.getLogger(__name__) 14 | 15 | class AdvancedContentGenerator: 16 | def __init__(self, ai_service_type: str = "deepseek"): 17 | self.ai_service = AIServiceFactory.create_service(ai_service_type) 18 | self.outline_generator = OutlineGenerator(self.ai_service) 19 | logger.info(f"高级内容生成器已初始化,使用 {ai_service_type} 服务") 20 | 21 | async def generate_with_constraints( 22 | self, 23 | topic: str, 24 | doc_type: str, 25 | additional_info: Optional[str] = None, 26 | max_pages: Optional[int] = None, 27 | detailed_content: Optional[List[PageChapterContent]] = None, 28 | progress_callback: Optional[Callable[[float, str], None]] = None 29 | ) -> Dict[str, Any]: 30 | """ 31 | 根据用户提供的约束生成内容 32 | 33 | Args: 34 | topic: 文档主题 35 | doc_type: 文档类型 (ppt 或 word) 36 | additional_info: 附加信息 37 | max_pages: 限制最大页数/章节数 38 | detailed_content: 用户定义的具体页面/章节内容 39 | progress_callback: 进度回调函数 40 | 41 | Returns: 42 | 包含大纲和详细内容的字典 43 | """ 44 | try: 45 | logger.info(f"开始高级模式内容生成: 主题='{topic}', 类型={doc_type}") 46 | if max_pages: 47 | logger.info(f"应用页数/章节限制: {max_pages}") 48 | if detailed_content: 49 | logger.info(f"用户提供了 {len(detailed_content)} 个自定义页面/章节") 50 | 51 | # 步骤1: 生成基本大纲 52 | if progress_callback: 53 | progress_callback(0.1, "正在生成基本大纲...") 54 | 55 | # 如果用户提供了详细内容,我们构建基于这些内容的大纲 56 | if detailed_content and len(detailed_content) > 0: 57 | outline = self._build_outline_from_user_content( 58 | topic, 59 | doc_type, 60 | detailed_content, 61 | max_pages 62 | ) 63 | logger.info("基于用户提供的内容创建了大纲") 64 | else: 65 | # 否则通过AI生成大纲 66 | outline_constraints = "" 67 | if max_pages: 68 | outline_constraints = f"大纲必须限制在最多{max_pages}个{'页面' if doc_type == 'ppt' else '章节'}内。" 69 | 70 | outline = await self._generate_ai_outline( 71 | topic, 72 | doc_type, 73 | f"{additional_info or ''} {outline_constraints}".strip() 74 | ) 75 | logger.info("通过AI生成了大纲") 76 | 77 | if progress_callback: 78 | progress_callback(0.3, "大纲生成完成,开始生成详细内容...") 79 | 80 | # 步骤2: 填充详细内容 81 | full_content = await self._generate_detailed_content( 82 | topic, 83 | doc_type, 84 | outline, 85 | detailed_content, 86 | progress_callback 87 | ) 88 | 89 | if progress_callback: 90 | progress_callback(0.95, "内容生成完成,准备导出...") 91 | 92 | logger.info(f"高级内容生成完成,主题: {topic}") 93 | return full_content 94 | 95 | except Exception as e: 96 | logger.error(f"高级内容生成错误: {str(e)}") 97 | raise 98 | 99 | def _build_outline_from_user_content( 100 | self, 101 | topic: str, 102 | doc_type: str, 103 | user_content: List[PageChapterContent], 104 | max_pages: Optional[int] = None 105 | ) -> Dict[str, Any]: 106 | """根据用户提供的内容构建大纲""" 107 | # 按位置排序用户内容 108 | sorted_content = sorted(user_content, key=lambda x: x.position) 109 | 110 | # 如果有最大页数限制,确保不超过 111 | if max_pages and len(sorted_content) > max_pages: 112 | logger.info(f"用户内容超过最大限制({max_pages}),将截断至{max_pages}个条目") 113 | sorted_content = sorted_content[:max_pages] 114 | 115 | if doc_type == "ppt": 116 | # 创建PPT大纲 117 | sections = [] 118 | current_section = { 119 | "title": "主要内容", 120 | "slides": [] 121 | } 122 | 123 | # 确保幻灯片数量不超过max_pages 124 | total_slides = 0 125 | for item in sorted_content: 126 | # 计数标题和结束幻灯片 127 | base_slides = 2 # 标题幻灯片和结束幻灯片 128 | if max_pages and total_slides + len(current_section["slides"]) + base_slides >= max_pages: 129 | # 如果已经达到限制,不再添加更多幻灯片 130 | logger.info(f"已达到幻灯片数量限制({max_pages}),停止添加更多内容") 131 | break 132 | 133 | current_section["slides"].append({ 134 | "title": item.title, 135 | "content": item.content or f"关于{item.title}的内容", 136 | "type": "content" 137 | }) 138 | 139 | sections.append(current_section) 140 | 141 | # 记录最终大纲信息 142 | total_slides = base_slides + sum(len(section.get("slides", [])) for section in sections) 143 | logger.info(f"构建的PPT大纲包含 {total_slides} 张幻灯片,最大限制为 {max_pages or '无限制'}") 144 | 145 | return { 146 | "title": topic, 147 | "sections": sections 148 | } 149 | else: 150 | # 创建Word大纲 151 | sections = [] 152 | for item in sorted_content: 153 | # 如果已达到章节数限制,停止添加 154 | if max_pages and len(sections) >= max_pages: 155 | logger.info(f"已达到章节数量限制({max_pages}),停止添加更多内容") 156 | break 157 | 158 | sections.append({ 159 | "title": item.title, 160 | "content": item.content or f"关于{item.title}的内容", 161 | "subsections": [] 162 | }) 163 | 164 | logger.info(f"构建的Word大纲包含 {len(sections)} 章节,最大限制为 {max_pages or '无限制'}") 165 | 166 | return { 167 | "title": topic, 168 | "sections": sections 169 | } 170 | 171 | async def _generate_detailed_content( 172 | self, 173 | topic: str, 174 | doc_type: str, 175 | outline: Dict[str, Any], 176 | user_content: Optional[List[PageChapterContent]] = None, 177 | progress_callback: Optional[Callable[[float, str], None]] = None 178 | ) -> Dict[str, Any]: 179 | """根据大纲填充详细内容""" 180 | # 创建用户内容的映射表,以便快速查找 181 | user_content_map = {} 182 | if user_content: 183 | for item in user_content: 184 | user_content_map[item.title] = item.content 185 | 186 | # 检查大纲中的章节/幻灯片数量 187 | if doc_type == "ppt": 188 | total_slides = 2 # 标题和结束幻灯片 189 | for section in outline.get("sections", []): 190 | total_slides += 1 # 章节标题幻灯片 191 | total_slides += len(section.get("slides", [])) 192 | logger.info(f"生成详细内容: PPT共计 {total_slides} 张幻灯片") 193 | 194 | # 处理PPT内容 195 | result = {"title": outline["title"], "sections": []} 196 | 197 | for section_idx, section in enumerate(outline["sections"]): 198 | # 计算总体进度 199 | if progress_callback: 200 | progress_percent = 0.3 + 0.65 * (section_idx / len(outline["sections"])) 201 | progress_callback(progress_percent, f"正在生成第 {section_idx+1}/{len(outline['sections'])} 章节...") 202 | 203 | # 复制章节基本信息 204 | result_section = { 205 | "title": section["title"], 206 | "slides": [] 207 | } 208 | 209 | # 处理每个幻灯片 210 | for slide in section.get("slides", []): 211 | # 如果用户提供了这个标题的内容,使用用户内容 212 | if slide["title"] in user_content_map and user_content_map[slide["title"]]: 213 | # 可以根据需要转换用户内容格式 214 | logger.info(f"使用用户提供的内容: {slide['title']}") 215 | slide["content"] = user_content_map[slide["title"]] 216 | 217 | result_section["slides"].append(slide) 218 | 219 | result["sections"].append(result_section) 220 | 221 | # 最终记录生成的内容大小 222 | final_total_slides = 2 # 标题和结束幻灯片 223 | for section in result["sections"]: 224 | final_total_slides += 1 # 章节标题幻灯片 225 | final_total_slides += len(section.get("slides", [])) 226 | logger.info(f"完成PPT内容生成: 共计 {final_total_slides} 张幻灯片") 227 | 228 | return result 229 | else: 230 | # 处理Word内容 231 | total_sections = len(outline.get("sections", [])) 232 | logger.info(f"生成详细内容: Word文档共计 {total_sections} 个章节") 233 | 234 | result = {"title": outline["title"], "sections": []} 235 | 236 | for section_idx, section in enumerate(outline["sections"]): 237 | # 计算总体进度 238 | if progress_callback: 239 | progress_percent = 0.3 + 0.65 * (section_idx / total_sections) 240 | progress_callback(progress_percent, f"正在生成第 {section_idx+1}/{total_sections} 章节...") 241 | 242 | # 如果用户提供了这个标题的内容,使用用户内容 243 | if section["title"] in user_content_map and user_content_map[section["title"]]: 244 | section["content"] = user_content_map[section["title"]] 245 | logger.info(f"使用用户提供的内容: {section['title']}") 246 | # 否则进行内容填充 247 | elif not section.get("content"): 248 | # 这里可以添加AI内容生成 249 | # 为了简化,这里只使用占位符 250 | section["content"] = f"关于{section['title']}的详细内容" 251 | 252 | result["sections"].append(section) 253 | 254 | # 最终记录生成的内容大小 255 | logger.info(f"完成Word内容生成: 共计 {len(result['sections'])} 个章节") 256 | 257 | return result 258 | 259 | async def _generate_ai_outline( 260 | self, 261 | topic: str, 262 | doc_type: str, 263 | additional_info: str = "" 264 | ) -> Dict[str, Any]: 265 | """ 266 | 使用AI生成文档大纲 267 | 268 | Args: 269 | topic: 文档主题 270 | doc_type: 文档类型 (ppt 或 word) 271 | additional_info: 附加信息 272 | 273 | Returns: 274 | 大纲字典 275 | """ 276 | try: 277 | logger.info(f"使用AI生成大纲: 主题={topic}, 类型={doc_type}") 278 | 279 | # 从additional_info中提取max_pages约束 280 | max_pages = None 281 | if "大纲必须限制在最多" in additional_info and "个" in additional_info: 282 | try: 283 | # 尝试提取数字 284 | match = re.search(r'大纲必须限制在最多(\d+)个', additional_info) 285 | if match: 286 | max_pages = int(match.group(1)) 287 | logger.info(f"从附加信息中提取到页数限制: {max_pages}") 288 | except ValueError: 289 | logger.warning("无法从附加信息中提取页数限制") 290 | 291 | # 使用AI服务生成大纲 292 | sections = self.outline_generator.generate_document_outline(topic, doc_type) 293 | 294 | if not sections: 295 | # 如果AI生成失败,创建一个基本大纲 296 | logger.warning(f"AI生成大纲失败,创建基本大纲: {topic}") 297 | 298 | if doc_type == "ppt": 299 | # 为PPT创建基本大纲 300 | basic_outline = { 301 | "title": topic, 302 | "sections": [ 303 | { 304 | "title": "概述", 305 | "slides": [ 306 | {"title": f"{topic}简介", "type": "content"}, 307 | {"title": "主要内容", "type": "content"}, 308 | {"title": "关键点", "type": "content"}, 309 | ] 310 | }, 311 | { 312 | "title": "详细内容", 313 | "slides": [ 314 | {"title": f"{topic}的基本概念", "type": "content"}, 315 | {"title": "重要特性", "type": "content"}, 316 | {"title": "应用场景", "type": "content"}, 317 | ] 318 | }, 319 | { 320 | "title": "总结", 321 | "slides": [ 322 | {"title": "主要收获", "type": "content"}, 323 | {"title": "未来展望", "type": "content"}, 324 | ] 325 | } 326 | ] 327 | } 328 | 329 | # 应用最大页数限制 330 | if max_pages: 331 | # 计算当前幻灯片总数 332 | total_slides = 2 # 标题幻灯片和结束幻灯片 333 | for section in basic_outline["sections"]: 334 | total_slides += 1 # 每个章节有一张章节标题幻灯片 335 | total_slides += len(section.get("slides", [])) 336 | 337 | # 如果超出限制,逐步减少内容 338 | if total_slides > max_pages: 339 | logger.info(f"基本大纲超过页数限制({max_pages}),进行裁剪") 340 | 341 | # 保留概述和总结章节,删除/缩减中间章节 342 | if len(basic_outline["sections"]) > 2: 343 | # 保留第一个和最后一个章节 344 | basic_outline["sections"] = [basic_outline["sections"][0], basic_outline["sections"][-1]] 345 | 346 | # 如果还是太多,继续减少幻灯片 347 | total_slides = 2 # 重新计算 348 | for section in basic_outline["sections"]: 349 | total_slides += 1 # 章节标题幻灯片 350 | total_slides += len(section.get("slides", [])) 351 | 352 | if total_slides > max_pages: 353 | # 每个章节最多保留一张幻灯片 354 | for section in basic_outline["sections"]: 355 | if len(section.get("slides", [])) > 1: 356 | section["slides"] = section["slides"][:1] 357 | 358 | return basic_outline 359 | else: 360 | # 为Word文档创建基本大纲 361 | basic_outline = { 362 | "title": topic, 363 | "sections": [ 364 | { 365 | "title": "引言", 366 | "subsections": [ 367 | {"title": f"{topic}背景"}, 368 | {"title": "研究意义"} 369 | ] 370 | }, 371 | { 372 | "title": "理论基础", 373 | "subsections": [ 374 | {"title": "基本概念"}, 375 | {"title": "核心原理"} 376 | ] 377 | }, 378 | { 379 | "title": "应用与实践", 380 | "subsections": [ 381 | {"title": "典型应用"}, 382 | {"title": "案例分析"} 383 | ] 384 | }, 385 | { 386 | "title": "总结与展望", 387 | "subsections": [ 388 | {"title": "主要成果"}, 389 | {"title": "未来研究方向"} 390 | ] 391 | } 392 | ] 393 | } 394 | 395 | # 应用最大章节数限制 396 | if max_pages and len(basic_outline["sections"]) > max_pages: 397 | logger.info(f"基本大纲超过章节限制({max_pages}),进行裁剪") 398 | # 保留必要的章节,如引言和总结 399 | if max_pages >= 2: 400 | basic_outline["sections"] = [basic_outline["sections"][0]] + basic_outline["sections"][-(max_pages-1):] 401 | else: 402 | basic_outline["sections"] = basic_outline["sections"][:max_pages] 403 | 404 | return basic_outline 405 | 406 | # 如果AI生成成功,构建完整大纲并应用页数限制 407 | outline = { 408 | "title": topic, 409 | "sections": sections 410 | } 411 | 412 | # 应用最大页数限制 413 | if max_pages: 414 | if doc_type == "ppt": 415 | # 计算当前幻灯片总数 416 | total_slides = 2 # 标题幻灯片和结束幻灯片 417 | for section in outline["sections"]: 418 | total_slides += 1 # 每个章节有一张章节标题幻灯片 419 | total_slides += len(section.get("slides", [])) 420 | 421 | # 如果超出限制,逐步减少内容 422 | if total_slides > max_pages: 423 | logger.info(f"AI生成的大纲超过页数限制({max_pages}),当前页数{total_slides},进行裁剪") 424 | 425 | # 从最后一个章节开始减少内容 426 | removed_slides = 0 427 | for i in range(len(outline["sections"])-1, -1, -1): 428 | section = outline["sections"][i] 429 | while len(section.get("slides", [])) > 0 and total_slides > max_pages: 430 | section["slides"].pop() 431 | total_slides -= 1 432 | removed_slides += 1 433 | 434 | # 如果章节内容为空,考虑移除整个章节 435 | if len(section.get("slides", [])) == 0 and len(outline["sections"]) > 1: 436 | outline["sections"].pop(i) 437 | total_slides -= 1 # 减去章节标题幻灯片 438 | removed_slides += 1 439 | 440 | if total_slides <= max_pages: 441 | break 442 | 443 | logger.info(f"裁剪完成,移除了{removed_slides}个内容,调整后页数为{total_slides}") 444 | else: 445 | # 对Word文档应用章节数限制 446 | if len(outline["sections"]) > max_pages: 447 | logger.info(f"AI生成的大纲超过章节限制({max_pages}),进行裁剪") 448 | # 保留有意义的章节(如开头和结尾) 449 | if max_pages >= 2: 450 | outline["sections"] = outline["sections"][:max_pages-1] + [outline["sections"][-1]] 451 | else: 452 | outline["sections"] = outline["sections"][:max_pages] 453 | 454 | return outline 455 | 456 | except Exception as e: 457 | logger.error(f"生成大纲时出错: {str(e)}") 458 | # 发生错误时返回一个最小大纲 459 | return { 460 | "title": topic, 461 | "sections": [{"title": "主要内容", "slides" if doc_type == "ppt" else "subsections": []}] 462 | } -------------------------------------------------------------------------------- /backend/app/services/ai_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | import logging 5 | from typing import List, Dict, Any, Optional 6 | from tenacity import retry, stop_after_attempt, wait_exponential 7 | from abc import ABC, abstractmethod 8 | 9 | # 配置日志 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | class AIClient(ABC): 14 | """ 15 | AI客户端基类,处理与AI API的通信 16 | """ 17 | 18 | def __init__(self, api_key: Optional[str] = None, api_endpoint: Optional[str] = None): 19 | """ 20 | 初始化AI客户端 21 | 22 | Args: 23 | api_key: API密钥,如果为None则从环境变量获取 24 | api_endpoint: API端点,如果为None则从环境变量获取 25 | """ 26 | self.api_key = api_key or os.getenv("AI_API_KEY", "") 27 | self.api_endpoint = api_endpoint or os.getenv("AI_API_ENDPOINT", "") 28 | 29 | if not self.api_key: 30 | logger.error("API密钥未设置") 31 | raise ValueError("API密钥未设置") 32 | 33 | logger.info(f"AI客户端初始化完成,API端点: {self.api_endpoint}") 34 | 35 | self.headers = { 36 | "Content-Type": "application/json", 37 | "Authorization": f"Bearer {self.api_key}" 38 | } 39 | 40 | @retry( 41 | stop=stop_after_attempt(3), 42 | wait=wait_exponential(multiplier=1, min=4, max=10) 43 | ) 44 | def call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 2000) -> Optional[Dict[str, Any]]: 45 | """ 46 | 调用AI API 47 | 48 | Args: 49 | messages: 消息列表 50 | temperature: 温度参数 51 | max_tokens: 最大token数 52 | 53 | Returns: 54 | API响应,如果请求失败则返回None 55 | """ 56 | try: 57 | payload = self._prepare_payload(messages, temperature, max_tokens) 58 | 59 | # 记录请求信息 60 | logger.info(f"发送API请求: endpoint={self.api_endpoint}, model={payload.get('model', 'unknown')}") 61 | logger.info(f"请求参数: temperature={temperature}, max_tokens={max_tokens}") 62 | logger.info(f"请求消息: {messages[-1]['content'][:100]}..." if messages else "无消息") 63 | 64 | # 增加超时时间到120秒 65 | response = requests.post( 66 | self.api_endpoint, 67 | headers=self.headers, 68 | json=payload, 69 | timeout=120 70 | ) 71 | 72 | logger.info(f"API响应状态码: {response.status_code}") 73 | 74 | if response.status_code != 200: 75 | logger.error(f"API请求失败: {response.status_code} - {response.text}") 76 | return None 77 | 78 | response_json = response.json() 79 | 80 | # 记录响应内容摘要 81 | if response_json and "choices" in response_json and response_json["choices"]: 82 | content = response_json["choices"][0].get("message", {}).get("content", "") 83 | logger.info(f"API响应内容摘要: {content[:150]}..." if content else "无内容") 84 | 85 | return response_json 86 | 87 | except requests.exceptions.Timeout: 88 | logger.error("API请求超时,可能需要更长的处理时间") 89 | return None 90 | except requests.exceptions.RequestException as e: 91 | logger.error(f"API请求异常: {str(e)}") 92 | return None 93 | except Exception as e: 94 | logger.error(f"API调用出错: {str(e)}") 95 | return None 96 | 97 | @abstractmethod 98 | def _prepare_payload(self, messages: List[Dict[str, str]], temperature: float, max_tokens: int) -> Dict[str, Any]: 99 | """ 100 | 准备API请求的payload 101 | 102 | Args: 103 | messages: 消息列表 104 | temperature: 温度参数 105 | max_tokens: 最大token数 106 | 107 | Returns: 108 | 准备好的payload 109 | """ 110 | pass 111 | 112 | @abstractmethod 113 | def extract_response_content(self, response: Dict[str, Any]) -> Optional[str]: 114 | """ 115 | 从API响应中提取内容 116 | 117 | Args: 118 | response: API响应 119 | 120 | Returns: 121 | 提取的内容,如果提取失败则返回None 122 | """ 123 | pass -------------------------------------------------------------------------------- /backend/app/services/ai_service_factory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, Optional 3 | 4 | from .ai_service_interface import AIServiceInterface 5 | from .deepseek_service import DeepSeekService 6 | 7 | # 配置日志 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | class AIServiceFactory: 12 | """ 13 | AI服务工厂,用于创建不同的AI服务实例 14 | """ 15 | 16 | # 支持的AI服务类型 17 | SUPPORTED_SERVICES = { 18 | "deepseek": DeepSeekService, 19 | # 未来可以添加更多服务,如: 20 | # "openai": OpenAIService, 21 | # "anthropic": AnthropicService, 22 | } 23 | 24 | @classmethod 25 | def create_service(cls, service_type: str = "deepseek", **kwargs) -> AIServiceInterface: 26 | """ 27 | 创建AI服务实例 28 | 29 | Args: 30 | service_type: 服务类型,如"deepseek"、"openai"等 31 | **kwargs: 传递给服务构造函数的参数 32 | 33 | Returns: 34 | AI服务实例 35 | 36 | Raises: 37 | ValueError: 如果服务类型不支持 38 | """ 39 | if service_type not in cls.SUPPORTED_SERVICES: 40 | supported = ", ".join(cls.SUPPORTED_SERVICES.keys()) 41 | logger.error(f"不支持的AI服务类型: {service_type},支持的类型: {supported}") 42 | raise ValueError(f"不支持的AI服务类型: {service_type},支持的类型: {supported}") 43 | 44 | service_class = cls.SUPPORTED_SERVICES[service_type] 45 | logger.info(f"创建AI服务: {service_type}") 46 | return service_class(**kwargs) 47 | 48 | @classmethod 49 | def get_default_service(cls) -> AIServiceInterface: 50 | """ 51 | 获取默认的AI服务实例 52 | 53 | Returns: 54 | 默认的AI服务实例 55 | """ 56 | return cls.create_service("deepseek") -------------------------------------------------------------------------------- /backend/app/services/ai_service_interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Dict, Any, Optional 3 | 4 | class AIServiceInterface(ABC): 5 | """ 6 | AI服务接口,定义了所有AI服务必须实现的方法 7 | """ 8 | 9 | @abstractmethod 10 | def generate_completion(self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 2000) -> Optional[str]: 11 | """ 12 | 生成文本完成 13 | 14 | Args: 15 | messages: 消息列表,格式为[{"role": "user", "content": "你好"}] 16 | temperature: 温度参数,控制随机性 17 | max_tokens: 生成的最大token数 18 | 19 | Returns: 20 | 生成的文本,如果请求失败则返回None 21 | """ 22 | pass 23 | 24 | @abstractmethod 25 | def generate_document_outline(self, topic: str, doc_type: str) -> Optional[List[Dict[str, Any]]]: 26 | """ 27 | 生成文档大纲 28 | 29 | Args: 30 | topic: 文档主题 31 | doc_type: 文档类型 (ppt, word, pdf) 32 | 33 | Returns: 34 | 文档大纲,如果生成失败则返回None 35 | """ 36 | pass 37 | 38 | @abstractmethod 39 | def generate_section_content(self, topic: str, section_title: str, doc_type: str) -> str: 40 | """ 41 | 生成文档章节内容 42 | 43 | Args: 44 | topic: 文档主题 45 | section_title: 章节标题 46 | doc_type: 文档类型 (ppt, word, pdf) 47 | 48 | Returns: 49 | 章节内容 50 | """ 51 | pass -------------------------------------------------------------------------------- /backend/app/services/cache_service.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import logging 4 | from datetime import datetime, timedelta 5 | from typing import Optional, Dict, Any 6 | from ..core.database import get_database 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class CacheService: 11 | """文档内容缓存服务,减少重复生成""" 12 | 13 | def __init__(self): 14 | self.db = get_database() 15 | self.collection = self.db.document_cache 16 | self.cache_ttl = timedelta(days=7) # 缓存有效期为7天 17 | 18 | @staticmethod 19 | def generate_cache_key(topic: str, doc_type: str, additional_info: Optional[str] = None) -> str: 20 | """生成缓存键""" 21 | # 规范化输入 22 | topic = topic.lower().strip() 23 | doc_type = doc_type.lower().strip() 24 | additional_info = (additional_info or "").lower().strip() 25 | 26 | # 创建合并字符串 27 | cache_string = f"{topic}:{doc_type}:{additional_info}" 28 | 29 | # 生成哈希值作为缓存键 30 | return hashlib.md5(cache_string.encode('utf-8')).hexdigest() 31 | 32 | async def get_cached_content(self, topic: str, doc_type: str, additional_info: Optional[str] = None) -> Optional[Dict[str, Any]]: 33 | """获取缓存的文档内容""" 34 | cache_key = self.generate_cache_key(topic, doc_type, additional_info) 35 | 36 | # 查询缓存 37 | cached_item = await self.collection.find_one({"cache_key": cache_key}) 38 | 39 | if not cached_item: 40 | logger.info(f"缓存未命中: {cache_key}") 41 | return None 42 | 43 | # 检查缓存是否过期 44 | created_at = cached_item.get("created_at") 45 | if created_at and datetime.fromisoformat(created_at) + self.cache_ttl < datetime.now(): 46 | logger.info(f"缓存已过期: {cache_key}") 47 | await self.collection.delete_one({"cache_key": cache_key}) 48 | return None 49 | 50 | logger.info(f"缓存命中: {cache_key}") 51 | return cached_item.get("content") 52 | 53 | async def cache_content(self, topic: str, doc_type: str, content: Dict[str, Any], additional_info: Optional[str] = None) -> bool: 54 | """缓存文档内容""" 55 | cache_key = self.generate_cache_key(topic, doc_type, additional_info) 56 | 57 | try: 58 | # 存储缓存项 59 | await self.collection.update_one( 60 | {"cache_key": cache_key}, 61 | {"$set": { 62 | "cache_key": cache_key, 63 | "topic": topic, 64 | "doc_type": doc_type, 65 | "additional_info": additional_info, 66 | "content": content, 67 | "created_at": datetime.now().isoformat() 68 | }}, 69 | upsert=True 70 | ) 71 | logger.info(f"内容已缓存: {cache_key}") 72 | return True 73 | except Exception as e: 74 | logger.error(f"缓存内容失败: {str(e)}") 75 | return False 76 | 77 | async def clear_expired_cache(self) -> int: 78 | """清理过期缓存""" 79 | expiry_date = (datetime.now() - self.cache_ttl).isoformat() 80 | result = await self.collection.delete_many({"created_at": {"$lt": expiry_date}}) 81 | deleted_count = result.deleted_count 82 | logger.info(f"已清理 {deleted_count} 条过期缓存") 83 | return deleted_count -------------------------------------------------------------------------------- /backend/app/services/content_generator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, Optional, List 3 | 4 | from .ai_client import AIClient 5 | 6 | # 配置日志 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | class ContentGenerator: 11 | """ 12 | 文档内容生成器 13 | """ 14 | 15 | def __init__(self, ai_client: AIClient): 16 | """ 17 | 初始化内容生成器 18 | 19 | Args: 20 | ai_client: AI客户端实例 21 | """ 22 | self.ai_client = ai_client 23 | logger.info("内容生成器初始化完成") 24 | 25 | def generate_section_content(self, topic: str, section_title: str, doc_type: str) -> str: 26 | """ 27 | 生成文档章节内容 28 | 29 | Args: 30 | topic: 文档主题 31 | section_title: 章节标题 32 | doc_type: 文档类型 (ppt, word, pdf) 33 | 34 | Returns: 35 | 章节内容 36 | """ 37 | try: 38 | logger.info(f"开始为章节 '{section_title}' 生成内容 (主题: {topic}, 类型: {doc_type})") 39 | 40 | # 构建提示 41 | prompt = self._build_section_prompt(topic, section_title, doc_type) 42 | 43 | # 调用AI客户端 44 | messages = [ 45 | {"role": "system", "content": "你是一个专业的文档内容生成助手,擅长创建高质量、信息丰富的文档内容。"}, 46 | {"role": "user", "content": prompt} 47 | ] 48 | 49 | response = self.ai_client.call_api(messages) 50 | 51 | if not response: 52 | logger.warning(f"生成章节 '{section_title}' 内容失败,使用模拟内容") 53 | return self._get_mock_section_content(topic, section_title) 54 | 55 | # 提取内容 56 | content = self.ai_client.extract_response_content(response) 57 | 58 | if not content: 59 | logger.warning(f"从响应中提取章节 '{section_title}' 内容失败,使用模拟内容") 60 | return self._get_mock_section_content(topic, section_title) 61 | 62 | # 记录生成的内容摘要 63 | content_preview = content.replace('\n', ' ')[:100] + "..." if len(content) > 100 else content 64 | logger.info(f"成功生成章节 '{section_title}' 内容: {content_preview}") 65 | 66 | return content 67 | 68 | except Exception as e: 69 | logger.error(f"生成章节内容时出错: {str(e)}") 70 | return self._get_mock_section_content(topic, section_title) 71 | 72 | def generate_slide_content(self, topic: str, section_title: str, slide_title: str, slide_type: str) -> Dict[str, Any]: 73 | """ 74 | 生成幻灯片内容 75 | 76 | Args: 77 | topic: 文档主题 78 | section_title: 章节标题 79 | slide_title: 幻灯片标题 80 | slide_type: 幻灯片类型 (content, two_column, image_content) 81 | 82 | Returns: 83 | 幻灯片内容 84 | """ 85 | try: 86 | logger.info(f"开始为幻灯片 '{slide_title}' 生成内容 (章节: {section_title}, 类型: {slide_type})") 87 | 88 | # 构建提示 89 | prompt = self._build_slide_prompt(topic, section_title, slide_title, slide_type) 90 | 91 | # 调用AI客户端 92 | messages = [ 93 | {"role": "system", "content": "你是一个专业的PPT内容生成助手,擅长创建简洁、有力的幻灯片内容。"}, 94 | {"role": "user", "content": prompt} 95 | ] 96 | 97 | response = self.ai_client.call_api(messages) 98 | 99 | if not response: 100 | logger.warning(f"生成幻灯片 '{slide_title}' 内容失败,使用模拟内容") 101 | return self._get_mock_slide_content(slide_title, slide_type) 102 | 103 | # 提取内容 104 | content = self.ai_client.extract_response_content(response) 105 | 106 | if not content: 107 | logger.warning(f"从响应中提取幻灯片 '{slide_title}' 内容失败,使用模拟内容") 108 | return self._get_mock_slide_content(slide_title, slide_type) 109 | 110 | # 解析内容 111 | slide_content = self._parse_slide_content(content, slide_title, slide_type) 112 | 113 | # 记录生成的内容摘要 114 | if slide_type == "content": 115 | points = slide_content.get("points", []) 116 | logger.info(f"成功生成幻灯片 '{slide_title}' 内容: {len(points)} 个要点") 117 | for i, point in enumerate(points[:2]): # 只记录前2个要点 118 | logger.info(f" 要点 {i+1}: {point.get('main', '未知')}") 119 | if len(points) > 2: 120 | logger.info(f" ... 还有 {len(points) - 2} 个要点") 121 | elif slide_type == "two_column": 122 | left_points = slide_content.get("left_points", []) 123 | right_points = slide_content.get("right_points", []) 124 | logger.info(f"成功生成幻灯片 '{slide_title}' 内容: 左侧 {len(left_points)} 个要点, 右侧 {len(right_points)} 个要点") 125 | elif slide_type == "image_content": 126 | points = slide_content.get("points", []) 127 | image_desc = slide_content.get("image_description", "") 128 | logger.info(f"成功生成幻灯片 '{slide_title}' 内容: {len(points)} 个要点, 图片描述: {image_desc[:50]}...") 129 | 130 | return slide_content 131 | 132 | except Exception as e: 133 | logger.error(f"生成幻灯片内容时出错: {str(e)}") 134 | return self._get_mock_slide_content(slide_title, slide_type) 135 | 136 | def _build_section_prompt(self, topic: str, section_title: str, doc_type: str) -> str: 137 | """ 138 | 构建生成章节内容的提示 139 | 140 | Args: 141 | topic: 文档主题 142 | section_title: 章节标题 143 | doc_type: 文档类型 144 | 145 | Returns: 146 | 提示文本 147 | """ 148 | if doc_type == "ppt": 149 | return f""" 150 | 请为主题"{topic}"中的章节"{section_title}"生成详细的内容。 151 | 152 | 要求: 153 | 1. 内容应该全面、准确、专业 154 | 2. 包含该章节的关键概念、原理和应用 155 | 3. 使用清晰的结构和逻辑 156 | 4. 适合PPT演示的简洁表达 157 | 5. 内容长度适中,约500-800字 158 | 159 | 请直接返回内容,不需要额外的格式或标记。 160 | """ 161 | else: 162 | return f""" 163 | 请为主题"{topic}"中的章节"{section_title}"生成详细的内容。 164 | 165 | 要求: 166 | 1. 内容应该全面、准确、专业 167 | 2. 包含该章节的关键概念、原理和应用 168 | 3. 使用清晰的结构和逻辑 169 | 4. 适合学术或专业文档的正式表达 170 | 5. 内容长度适中,约1000-1500字 171 | 172 | 请直接返回内容,不需要额外的格式或标记。 173 | """ 174 | 175 | def _build_slide_prompt(self, topic: str, section_title: str, slide_title: str, slide_type: str) -> str: 176 | """ 177 | 构建生成幻灯片内容的提示 178 | 179 | Args: 180 | topic: 文档主题 181 | section_title: 章节标题 182 | slide_title: 幻灯片标题 183 | slide_type: 幻灯片类型 184 | 185 | Returns: 186 | 提示文本 187 | """ 188 | base_prompt = f""" 189 | 请为主题"{topic}"中章节"{section_title}"的幻灯片"{slide_title}"生成内容。 190 | 191 | 幻灯片类型: {slide_type} 192 | """ 193 | 194 | if slide_type == "content": 195 | return base_prompt + """ 196 | 要求: 197 | 1. 创建3-5个简洁的要点 198 | 2. 每个要点包含一个主要观点和1-2个支持细节 199 | 3. 内容应该简洁明了,适合PPT展示 200 | 201 | 请以JSON格式返回,格式如下: 202 | { 203 | "points": [ 204 | { 205 | "main": "主要要点1", 206 | "details": ["细节1", "细节2"] 207 | }, 208 | ... 209 | ] 210 | } 211 | """ 212 | elif slide_type == "two_column": 213 | return base_prompt + """ 214 | 要求: 215 | 1. 创建左右两列内容 216 | 2. 每列包含2-3个要点 217 | 3. 每个要点包含一个主要观点和1-2个支持细节 218 | 219 | 请以JSON格式返回,格式如下: 220 | { 221 | "left_points": [ 222 | { 223 | "main": "左侧要点1", 224 | "details": ["细节1", "细节2"] 225 | }, 226 | ... 227 | ], 228 | "right_points": [ 229 | { 230 | "main": "右侧要点1", 231 | "details": ["细节1", "细节2"] 232 | }, 233 | ... 234 | ] 235 | } 236 | """ 237 | else: # image_content 238 | return base_prompt + """ 239 | 要求: 240 | 1. 创建3-4个要点,描述与图片相关的内容 241 | 2. 每个要点包含一个主要观点和1-2个支持细节 242 | 3. 添加一个图片描述,说明应该使用什么样的图片 243 | 244 | 请以JSON格式返回,格式如下: 245 | { 246 | "points": [ 247 | { 248 | "main": "主要要点1", 249 | "details": ["细节1", "细节2"] 250 | }, 251 | ... 252 | ], 253 | "image_description": "图片应该展示..." 254 | } 255 | """ 256 | 257 | def _parse_slide_content(self, content: str, slide_title: str, slide_type: str) -> Dict[str, Any]: 258 | """ 259 | 解析幻灯片内容 260 | 261 | Args: 262 | content: 生成的内容 263 | slide_title: 幻灯片标题 264 | slide_type: 幻灯片类型 265 | 266 | Returns: 267 | 解析后的幻灯片内容 268 | """ 269 | try: 270 | import json 271 | import re 272 | 273 | # 尝试提取JSON部分 274 | json_match = re.search(r'\{.*\}', content, re.DOTALL) 275 | if json_match: 276 | try: 277 | slide_content = json.loads(json_match.group(0)) 278 | 279 | # 验证内容格式 280 | if slide_type == "content" and "points" in slide_content: 281 | return slide_content 282 | elif slide_type == "two_column" and "left_points" in slide_content and "right_points" in slide_content: 283 | return slide_content 284 | elif slide_type == "image_content" and "points" in slide_content: 285 | if "image_description" not in slide_content: 286 | slide_content["image_description"] = f"关于{slide_title}的图示" 287 | return slide_content 288 | except json.JSONDecodeError: 289 | pass 290 | 291 | # 如果JSON解析失败,尝试从文本中提取内容 292 | if slide_type == "content": 293 | points = self._extract_points_from_text(content) 294 | return {"points": points} 295 | elif slide_type == "two_column": 296 | # 尝试分割内容为左右两部分 297 | parts = content.split("右侧", 1) 298 | if len(parts) == 2: 299 | left_points = self._extract_points_from_text(parts[0]) 300 | right_points = self._extract_points_from_text("右侧" + parts[1]) 301 | else: 302 | # 如果无法分割,则平均分配要点 303 | all_points = self._extract_points_from_text(content) 304 | mid = len(all_points) // 2 305 | left_points = all_points[:mid] 306 | right_points = all_points[mid:] 307 | 308 | return { 309 | "left_points": left_points, 310 | "right_points": right_points 311 | } 312 | else: # image_content 313 | points = self._extract_points_from_text(content) 314 | 315 | # 尝试提取图片描述 316 | image_desc = "" 317 | for line in content.split('\n'): 318 | if "图片" in line and ("描述" in line or "说明" in line): 319 | image_desc = line.split(":", 1)[-1].strip() 320 | break 321 | 322 | if not image_desc: 323 | image_desc = f"关于{slide_title}的图示" 324 | 325 | return { 326 | "points": points, 327 | "image_description": image_desc 328 | } 329 | 330 | except Exception as e: 331 | logger.error(f"解析幻灯片内容时出错: {str(e)}") 332 | return self._get_mock_slide_content(slide_title, slide_type) 333 | 334 | def _extract_points_from_text(self, text: str) -> List[Dict[str, Any]]: 335 | """ 336 | 从文本中提取要点 337 | 338 | Args: 339 | text: 文本内容 340 | 341 | Returns: 342 | 要点列表 343 | """ 344 | points = [] 345 | current_main = None 346 | current_details = [] 347 | 348 | for line in text.split('\n'): 349 | line = line.strip() 350 | if not line: 351 | continue 352 | 353 | # 检查是否是主要要点(通常以数字、项目符号或关键词开头) 354 | if re.match(r'^(\d+\.|\-|\*|\•|\○|\◆|要点|关键点|主要|首先|其次|再次|最后)', line): 355 | # 如果已有要点,保存它 356 | if current_main: 357 | points.append({ 358 | "main": current_main, 359 | "details": current_details 360 | }) 361 | 362 | # 开始新的要点 363 | current_main = re.sub(r'^(\d+\.|\-|\*|\•|\○|\◆|要点|关键点|主要|首先|其次|再次|最后)\s*', '', line) 364 | current_details = [] 365 | elif current_main and line.startswith((' ', '\t')): 366 | # 这是一个细节(缩进的行) 367 | current_details.append(line.strip()) 368 | elif current_main: 369 | # 如果不是明显的细节但已有主要要点,也视为细节 370 | current_details.append(line) 371 | 372 | # 添加最后一个要点 373 | if current_main: 374 | points.append({ 375 | "main": current_main, 376 | "details": current_details 377 | }) 378 | 379 | # 如果没有提取到要点,创建一个默认要点 380 | if not points: 381 | points = [{"main": "重要信息", "details": [text[:100] + "..."]}] 382 | 383 | return points 384 | 385 | def _get_mock_section_content(self, topic: str, section_title: str) -> str: 386 | """ 387 | 获取模拟的章节内容 388 | 389 | Args: 390 | topic: 文档主题 391 | section_title: 章节标题 392 | 393 | Returns: 394 | 模拟的章节内容 395 | """ 396 | return f""" 397 | {section_title}是{topic}的重要组成部分。本章节将详细介绍其关键概念、应用场景和发展趋势。 398 | 399 | 首先,{section_title}的基本概念建立在多年的研究和实践基础上。它包含多个核心要素,这些要素相互关联,共同构成了完整的理论体系。理解这些基本概念对于掌握整个主题至关重要。 400 | 401 | 其次,{section_title}在多个领域有广泛的应用场景。无论是在教育、商业还是技术创新方面,都能找到其成功应用的案例。这些应用不仅验证了理论的有效性,也为未来的发展提供了宝贵的经验。 402 | 403 | 最后,随着新技术和新方法的出现,{section_title}正在不断发展。未来研究将进一步探索其潜力和应用前景,我们有理由相信,它将在{topic}的发展中发挥更加重要的作用。 404 | """ 405 | 406 | def _get_mock_slide_content(self, slide_title: str, slide_type: str) -> Dict[str, Any]: 407 | """ 408 | 获取模拟的幻灯片内容 409 | 410 | Args: 411 | slide_title: 幻灯片标题 412 | slide_type: 幻灯片类型 413 | 414 | Returns: 415 | 模拟的幻灯片内容 416 | """ 417 | if slide_type == "content": 418 | return { 419 | "points": [ 420 | { 421 | "main": f"{slide_title}的核心要素", 422 | "details": ["包含多个关键组成部分", "这些组成部分相互关联"] 423 | }, 424 | { 425 | "main": "应用场景广泛", 426 | "details": ["适用于多个领域", "有丰富的实践案例"] 427 | }, 428 | { 429 | "main": "未来发展趋势", 430 | "details": ["将继续创新和完善", "有望解决更多实际问题"] 431 | } 432 | ] 433 | } 434 | elif slide_type == "two_column": 435 | return { 436 | "left_points": [ 437 | { 438 | "main": "理论基础", 439 | "details": ["建立在坚实的研究基础上", "有完整的理论体系"] 440 | }, 441 | { 442 | "main": "核心优势", 443 | "details": ["高效、可靠", "易于实施和推广"] 444 | } 445 | ], 446 | "right_points": [ 447 | { 448 | "main": "实际应用", 449 | "details": ["已在多个领域成功应用", "取得了显著成效"] 450 | }, 451 | { 452 | "main": "未来展望", 453 | "details": ["将继续发展和完善", "有更广阔的应用前景"] 454 | } 455 | ] 456 | } 457 | else: # image_content 458 | return { 459 | "points": [ 460 | { 461 | "main": f"{slide_title}的图示说明", 462 | "details": ["直观展示关键概念", "帮助理解复杂关系"] 463 | }, 464 | { 465 | "main": "实际案例", 466 | "details": ["展示真实应用场景", "验证理论的有效性"] 467 | }, 468 | { 469 | "main": "对比分析", 470 | "details": ["与其他方法的对比", "突出独特优势"] 471 | } 472 | ], 473 | "image_description": f"关于{slide_title}的图示,展示其核心概念和关系" 474 | } -------------------------------------------------------------------------------- /backend/app/services/deepseek_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from typing import List, Dict, Any, Optional 4 | 5 | from .ai_client import AIClient 6 | 7 | # 配置日志 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | class DeepSeekClient(AIClient): 12 | """ 13 | DeepSeek API客户端实现 14 | """ 15 | 16 | def __init__(self, api_key: Optional[str] = None, api_endpoint: Optional[str] = None): 17 | """ 18 | 初始化DeepSeek客户端 19 | 20 | Args: 21 | api_key: API密钥,如果为None则从环境变量获取 22 | api_endpoint: API端点,如果为None则从环境变量获取 23 | """ 24 | # 如果未提供API端点,使用DeepSeek默认端点 25 | default_endpoint = "https://api.deepseek.com/v1/chat/completions" 26 | super().__init__( 27 | api_key=api_key, 28 | api_endpoint=api_endpoint or os.getenv("AI_API_ENDPOINT", default_endpoint) 29 | ) 30 | logger.info("DeepSeek客户端初始化完成") 31 | 32 | def _prepare_payload(self, messages: List[Dict[str, str]], temperature: float, max_tokens: int) -> Dict[str, Any]: 33 | """ 34 | 准备DeepSeek API请求的payload 35 | 36 | Args: 37 | messages: 消息列表 38 | temperature: 温度参数 39 | max_tokens: 最大token数 40 | 41 | Returns: 42 | 准备好的payload 43 | """ 44 | return { 45 | "model": "deepseek-chat", # 或其他可用模型 46 | "messages": messages, 47 | "temperature": temperature, 48 | "max_tokens": max_tokens 49 | } 50 | 51 | def extract_response_content(self, response: Dict[str, Any]) -> Optional[str]: 52 | """ 53 | 从DeepSeek API响应中提取内容 54 | 55 | Args: 56 | response: API响应 57 | 58 | Returns: 59 | 提取的内容,如果提取失败则返回None 60 | """ 61 | if not response or "choices" not in response: 62 | return None 63 | 64 | try: 65 | return response["choices"][0]["message"]["content"] 66 | except (KeyError, IndexError) as e: 67 | logger.error(f"从响应中提取内容时出错: {str(e)}") 68 | return None -------------------------------------------------------------------------------- /backend/app/services/deepseek_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List, Dict, Any, Optional 3 | 4 | from .ai_service_interface import AIServiceInterface 5 | from .deepseek_client import DeepSeekClient 6 | from .outline_generator import OutlineGenerator 7 | from .content_generator import ContentGenerator 8 | 9 | # 配置日志 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | class DeepSeekService(AIServiceInterface): 14 | """ 15 | DeepSeek服务,实现AI服务接口 16 | 这是一个门面类,整合了各个组件的功能 17 | """ 18 | 19 | def __init__(self): 20 | """ 21 | 初始化DeepSeek服务 22 | """ 23 | # 初始化AI客户端 24 | self.client = DeepSeekClient() 25 | 26 | # 初始化大纲生成器 27 | self.outline_generator = OutlineGenerator(self.client) 28 | 29 | # 初始化内容生成器 30 | self.content_generator = ContentGenerator(self.client) 31 | 32 | logger.info("DeepSeek服务初始化完成") 33 | 34 | def generate_completion(self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 2000) -> Optional[str]: 35 | """ 36 | 生成文本完成 37 | 38 | Args: 39 | messages: 消息列表,格式为[{"role": "user", "content": "你好"}] 40 | temperature: 温度参数,控制随机性 41 | max_tokens: 生成的最大token数 42 | 43 | Returns: 44 | 生成的文本,如果请求失败则返回None 45 | """ 46 | try: 47 | # 调用AI客户端 48 | response = self.client.call_api(messages, temperature, max_tokens) 49 | if not response: 50 | return None 51 | 52 | # 提取内容 53 | return self.client.extract_response_content(response) 54 | 55 | except Exception as e: 56 | logger.error(f"生成文本完成时出错: {str(e)}") 57 | return None 58 | 59 | def call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 2000) -> Optional[Dict[str, Any]]: 60 | """ 61 | 调用AI API 62 | 63 | Args: 64 | messages: 消息列表,格式为[{"role": "user", "content": "你好"}] 65 | temperature: 温度参数,控制随机性 66 | max_tokens: 生成的最大token数 67 | 68 | Returns: 69 | API响应,如果请求失败则返回None 70 | """ 71 | try: 72 | return self.client.call_api(messages, temperature, max_tokens) 73 | except Exception as e: 74 | logger.error(f"调用API时出错: {str(e)}") 75 | return None 76 | 77 | def generate_document_outline(self, topic: str, doc_type: str) -> Optional[List[Dict[str, Any]]]: 78 | """ 79 | 生成文档大纲 80 | 81 | Args: 82 | topic: 文档主题 83 | doc_type: 文档类型 (ppt, word, pdf) 84 | 85 | Returns: 86 | 文档大纲,如果生成失败则返回None 87 | """ 88 | return self.outline_generator.generate_document_outline(topic, doc_type) 89 | 90 | def generate_section_content(self, topic: str, section_title: str, doc_type: str) -> str: 91 | """ 92 | 生成文档章节内容 93 | 94 | Args: 95 | topic: 文档主题 96 | section_title: 章节标题 97 | doc_type: 文档类型 (ppt, word, pdf) 98 | 99 | Returns: 100 | 章节内容 101 | """ 102 | return self.content_generator.generate_section_content(topic, section_title, doc_type) 103 | 104 | def generate_slide_content(self, topic: str, section_title: str, slide_title: str, slide_type: str) -> Dict[str, Any]: 105 | """ 106 | 生成幻灯片内容 107 | 108 | Args: 109 | topic: 文档主题 110 | section_title: 章节标题 111 | slide_title: 幻灯片标题 112 | slide_type: 幻灯片类型 (content, two_column, image_content) 113 | 114 | Returns: 115 | 幻灯片内容 116 | """ 117 | return self.content_generator.generate_slide_content(topic, section_title, slide_title, slide_type) -------------------------------------------------------------------------------- /backend/app/services/openai_client.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import json 3 | import logging 4 | from typing import Dict, Any, List, Optional 5 | 6 | from ..core.config import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class OpenAIClient: 11 | """OpenAI API客户端""" 12 | 13 | def __init__(self, api_key: Optional[str] = None, api_endpoint: Optional[str] = None): 14 | """ 15 | 初始化OpenAI客户端 16 | 17 | Args: 18 | api_key: API密钥,默认从配置中获取 19 | api_endpoint: API端点,默认从配置中获取 20 | """ 21 | self.api_key = api_key or settings.OPENAI_API_KEY 22 | self.api_endpoint = api_endpoint or settings.OPENAI_API_ENDPOINT 23 | 24 | if not self.api_key: 25 | logger.warning("OpenAI API密钥未配置") 26 | 27 | async def chat_completion(self, 28 | messages: List[Dict[str, str]], 29 | temperature: float = 0.7, 30 | max_tokens: int = 2000, 31 | model: str = "gpt-4") -> Optional[Dict[str, Any]]: 32 | """ 33 | 调用OpenAI聊天完成API 34 | 35 | Args: 36 | messages: 消息列表,格式为[{"role": "user", "content": "你好"}] 37 | temperature: 温度参数,控制随机性 38 | max_tokens: 生成的最大token数 39 | model: 使用的模型,默认为"gpt-4" 40 | 41 | Returns: 42 | API响应,如果请求失败则返回None 43 | """ 44 | if not self.api_key: 45 | logger.error("无法调用OpenAI API: 未配置API密钥") 46 | return None 47 | 48 | payload = { 49 | "model": model, 50 | "messages": messages, 51 | "temperature": temperature, 52 | "max_tokens": max_tokens 53 | } 54 | 55 | headers = { 56 | "Content-Type": "application/json", 57 | "Authorization": f"Bearer {self.api_key}" 58 | } 59 | 60 | try: 61 | async with aiohttp.ClientSession() as session: 62 | async with session.post(self.api_endpoint, 63 | json=payload, 64 | headers=headers) as response: 65 | if response.status != 200: 66 | error_text = await response.text() 67 | logger.error(f"OpenAI API请求失败: {response.status}, {error_text}") 68 | return None 69 | 70 | result = await response.json() 71 | return result 72 | except Exception as e: 73 | logger.error(f"OpenAI API请求异常: {str(e)}") 74 | return None -------------------------------------------------------------------------------- /backend/app/services/outline_generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import re 4 | from typing import List, Dict, Any, Optional 5 | 6 | from .ai_client import AIClient 7 | 8 | # 配置日志 9 | logging.basicConfig(level=logging.INFO) 10 | logger = logging.getLogger(__name__) 11 | 12 | class OutlineGenerator: 13 | """ 14 | 文档大纲生成器 15 | """ 16 | 17 | def __init__(self, ai_client: AIClient): 18 | """ 19 | 初始化大纲生成器 20 | 21 | Args: 22 | ai_client: AI客户端实例 23 | """ 24 | self.ai_client = ai_client 25 | logger.info("大纲生成器初始化完成") 26 | 27 | def generate_document_outline(self, topic: str, doc_type: str) -> Optional[List[Dict[str, Any]]]: 28 | """ 29 | 生成文档大纲 30 | 31 | Args: 32 | topic: 文档主题 33 | doc_type: 文档类型 (ppt, word, pdf) 34 | 35 | Returns: 36 | 文档大纲,如果生成失败则返回None 37 | """ 38 | try: 39 | logger.info(f"开始为主题 '{topic}' 生成 {doc_type} 类型的文档大纲") 40 | 41 | # 1. 首先生成主要章节 42 | main_sections = self._generate_main_sections(topic, doc_type) 43 | if not main_sections: 44 | logger.error(f"生成主要章节失败: 主题={topic}, 类型={doc_type}") 45 | return None 46 | 47 | logger.info(f"成功生成主要章节: {len(main_sections)} 个章节") 48 | for i, section in enumerate(main_sections): 49 | logger.info(f" 章节 {i+1}: {section.get('title', '未知标题')}") 50 | 51 | # 2. 然后为每个章节生成子章节 52 | outline = [] 53 | for section in main_sections: 54 | section_title = section.get("title", "") 55 | logger.info(f"为章节 '{section_title}' 生成详细内容") 56 | 57 | section_detail = self._generate_section_detail(topic, section, doc_type) 58 | if section_detail: 59 | outline.append(section_detail) 60 | 61 | # 记录子章节或幻灯片信息 62 | if doc_type == "ppt" and "slides" in section_detail: 63 | slides = section_detail.get("slides", []) 64 | logger.info(f" 生成了 {len(slides)} 张幻灯片") 65 | for i, slide in enumerate(slides[:3]): # 只记录前3张幻灯片 66 | logger.info(f" 幻灯片 {i+1}: {slide.get('title', '未知标题')} ({slide.get('type', 'content')})") 67 | if len(slides) > 3: 68 | logger.info(f" ... 还有 {len(slides) - 3} 张幻灯片") 69 | elif "subsections" in section_detail: 70 | subsections = section_detail.get("subsections", []) 71 | logger.info(f" 生成了 {len(subsections)} 个子章节") 72 | for i, subsection in enumerate(subsections[:3]): # 只记录前3个子章节 73 | logger.info(f" 子章节 {i+1}: {subsection.get('title', '未知标题')}") 74 | if len(subsections) > 3: 75 | logger.info(f" ... 还有 {len(subsections) - 3} 个子章节") 76 | 77 | logger.info(f"文档大纲生成完成: 共 {len(outline)} 个章节") 78 | return outline 79 | 80 | except Exception as e: 81 | logger.error(f"生成大纲时出错: {str(e)}") 82 | return None 83 | 84 | def _generate_main_sections(self, topic: str, doc_type: str) -> Optional[List[Dict[str, Any]]]: 85 | """ 86 | 生成文档的主要章节 87 | 88 | Args: 89 | topic: 文档主题 90 | doc_type: 文档类型 91 | 92 | Returns: 93 | 主要章节列表,如果生成失败则返回None 94 | """ 95 | try: 96 | # 构建提示 97 | prompt = self._build_main_sections_prompt(topic, doc_type) 98 | 99 | # 调用AI客户端 100 | messages = [ 101 | {"role": "system", "content": "你是一个专业的文档生成助手,擅长创建结构化的文档大纲。"}, 102 | {"role": "user", "content": prompt} 103 | ] 104 | 105 | response = self.ai_client.call_api(messages) 106 | if not response: 107 | return self._get_mock_main_sections(topic, doc_type) 108 | 109 | # 提取内容 110 | content = self.ai_client.extract_response_content(response) 111 | if not content: 112 | return self._get_mock_main_sections(topic, doc_type) 113 | 114 | # 解析内容 115 | sections = self._extract_sections_from_text(content) 116 | if not sections: 117 | return self._get_mock_main_sections(topic, doc_type) 118 | 119 | return sections 120 | 121 | except Exception as e: 122 | logger.error(f"生成主要章节时出错: {str(e)}") 123 | return self._get_mock_main_sections(topic, doc_type) 124 | 125 | def _generate_section_detail(self, topic: str, section: Dict[str, Any], doc_type: str) -> Optional[Dict[str, Any]]: 126 | """ 127 | 为章节生成详细内容 128 | 129 | Args: 130 | topic: 文档主题 131 | section: 章节信息 132 | doc_type: 文档类型 133 | 134 | Returns: 135 | 详细的章节信息,如果生成失败则返回None 136 | """ 137 | try: 138 | section_title = section.get("title", "") 139 | if not section_title: 140 | return None 141 | 142 | # 构建提示 143 | prompt = self._build_section_detail_prompt(topic, section_title, doc_type) 144 | 145 | # 调用AI客户端 146 | messages = [ 147 | {"role": "system", "content": "你是一个专业的文档生成助手,擅长创建结构化的文档大纲。"}, 148 | {"role": "user", "content": prompt} 149 | ] 150 | 151 | response = self.ai_client.call_api(messages) 152 | if not response: 153 | return self._get_mock_section_detail(section, doc_type) 154 | 155 | # 提取内容 156 | content = self.ai_client.extract_response_content(response) 157 | if not content: 158 | return self._get_mock_section_detail(section, doc_type) 159 | 160 | # 解析内容 161 | if doc_type == "ppt": 162 | subsections = self._extract_slides_from_text(content, section_title) 163 | return { 164 | "title": section_title, 165 | "slides": subsections 166 | } 167 | else: 168 | subsections = self._extract_subsections_from_text(content, section_title) 169 | return { 170 | "title": section_title, 171 | "subsections": subsections 172 | } 173 | 174 | except Exception as e: 175 | logger.error(f"生成章节详情时出错: {str(e)}") 176 | return self._get_mock_section_detail(section, doc_type) 177 | 178 | def _build_main_sections_prompt(self, topic: str, doc_type: str) -> str: 179 | """ 180 | 构建生成主要章节的提示 181 | 182 | Args: 183 | topic: 文档主题 184 | doc_type: 文档类型 185 | 186 | Returns: 187 | 提示文本 188 | """ 189 | if doc_type == "ppt": 190 | return f""" 191 | 请为主题"{topic}"创建一个PPT演示文稿的主要章节列表。 192 | 193 | 要求: 194 | 1. 创建5-7个主要章节 195 | 2. 每个章节应该是主题的一个重要方面 196 | 3. 章节应该有逻辑顺序,从介绍到结论 197 | 198 | 请以JSON格式返回,格式如下: 199 | [ 200 | {{"title": "章节1标题"}}, 201 | {{"title": "章节2标题"}}, 202 | ... 203 | ] 204 | 205 | 确保JSON格式正确,可以直接解析。 206 | """ 207 | else: 208 | return f""" 209 | 请为主题"{topic}"创建一个文档的主要章节列表。 210 | 211 | 要求: 212 | 1. 创建5-7个主要章节 213 | 2. 每个章节应该是主题的一个重要方面 214 | 3. 章节应该有逻辑顺序,从介绍到结论 215 | 216 | 请以JSON格式返回,格式如下: 217 | [ 218 | {{"title": "章节1标题"}}, 219 | {{"title": "章节2标题"}}, 220 | ... 221 | ] 222 | 223 | 确保JSON格式正确,可以直接解析。 224 | """ 225 | 226 | def _build_section_detail_prompt(self, topic: str, section_title: str, doc_type: str) -> str: 227 | """ 228 | 构建生成章节详情的提示 229 | 230 | Args: 231 | topic: 文档主题 232 | section_title: 章节标题 233 | doc_type: 文档类型 234 | 235 | Returns: 236 | 提示文本 237 | """ 238 | if doc_type == "ppt": 239 | return f""" 240 | 请为主题"{topic}"中的章节"{section_title}"创建详细的PPT幻灯片内容。 241 | 242 | 要求: 243 | 1. 创建3-5个幻灯片 244 | 2. 每个幻灯片应该有一个标题和类型 245 | 3. 类型可以是"content"(普通内容)、"two_column"(两列内容)或"image_content"(带图片的内容) 246 | 247 | 请以JSON格式返回,格式如下: 248 | [ 249 | {{ 250 | "title": "幻灯片1标题", 251 | "type": "content" 252 | }}, 253 | {{ 254 | "title": "幻灯片2标题", 255 | "type": "two_column" 256 | }}, 257 | ... 258 | ] 259 | 260 | 确保JSON格式正确,可以直接解析。 261 | """ 262 | else: 263 | return f""" 264 | 请为主题"{topic}"中的章节"{section_title}"创建详细的子章节列表。 265 | 266 | 要求: 267 | 1. 创建3-5个子章节 268 | 2. 每个子章节应该是章节的一个重要方面 269 | 3. 子章节应该有逻辑顺序 270 | 271 | 请以JSON格式返回,格式如下: 272 | [ 273 | {{ 274 | "title": "子章节1标题" 275 | }}, 276 | {{ 277 | "title": "子章节2标题" 278 | }}, 279 | ... 280 | ] 281 | 282 | 确保JSON格式正确,可以直接解析。 283 | """ 284 | 285 | def _extract_sections_from_text(self, text: str) -> List[Dict[str, str]]: 286 | """ 287 | 从文本中提取章节信息 288 | 289 | Args: 290 | text: 文本内容 291 | 292 | Returns: 293 | 章节列表 294 | """ 295 | try: 296 | # 尝试直接解析JSON 297 | sections = json.loads(text) 298 | if isinstance(sections, list) and all(isinstance(s, dict) and "title" in s for s in sections): 299 | return sections 300 | except json.JSONDecodeError: 301 | pass 302 | 303 | # 如果直接解析失败,尝试从文本中提取JSON部分 304 | json_match = re.search(r'\[\s*\{.*\}\s*\]', text, re.DOTALL) 305 | if json_match: 306 | try: 307 | sections = json.loads(json_match.group(0)) 308 | if isinstance(sections, list) and all(isinstance(s, dict) and "title" in s for s in sections): 309 | return sections 310 | except json.JSONDecodeError: 311 | pass 312 | 313 | # 如果仍然失败,尝试从文本中提取章节标题 314 | sections = [] 315 | for line in text.split('\n'): 316 | # 查找类似 "1. 章节标题" 或 "第一章:章节标题" 的模式 317 | match = re.search(r'(?:\d+\.\s*|\w+章[::]\s*)(.+)', line) 318 | if match: 319 | sections.append({"title": match.group(1).strip()}) 320 | 321 | return sections if sections else self._get_mock_main_sections("未知主题", "word") 322 | 323 | def _extract_subsections_from_text(self, text: str, section_title: str) -> List[Dict[str, Any]]: 324 | """ 325 | 从文本中提取子章节信息 326 | 327 | Args: 328 | text: 文本内容 329 | section_title: 章节标题 330 | 331 | Returns: 332 | 子章节列表 333 | """ 334 | try: 335 | # 尝试直接解析JSON 336 | subsections = json.loads(text) 337 | if isinstance(subsections, list) and all(isinstance(s, dict) and "title" in s for s in subsections): 338 | return subsections 339 | except json.JSONDecodeError: 340 | pass 341 | 342 | # 如果直接解析失败,尝试从文本中提取JSON部分 343 | json_match = re.search(r'\[\s*\{.*\}\s*\]', text, re.DOTALL) 344 | if json_match: 345 | try: 346 | subsections = json.loads(json_match.group(0)) 347 | if isinstance(subsections, list) and all(isinstance(s, dict) and "title" in s for s in subsections): 348 | return subsections 349 | except json.JSONDecodeError: 350 | pass 351 | 352 | # 如果仍然失败,尝试从文本中提取子章节标题 353 | subsections = [] 354 | for line in text.split('\n'): 355 | # 查找类似 "1.1 子章节标题" 或 "- 子章节标题" 的模式 356 | match = re.search(r'(?:\d+\.\d+\s*|\-\s*)(.+)', line) 357 | if match: 358 | subsections.append({"title": match.group(1).strip()}) 359 | 360 | return subsections if subsections else self._get_mock_subsections(section_title) 361 | 362 | def _extract_slides_from_text(self, text: str, section_title: str) -> List[Dict[str, Any]]: 363 | """ 364 | 从文本中提取幻灯片信息 365 | 366 | Args: 367 | text: 文本内容 368 | section_title: 章节标题 369 | 370 | Returns: 371 | 幻灯片列表 372 | """ 373 | try: 374 | # 尝试直接解析JSON 375 | slides = json.loads(text) 376 | if isinstance(slides, list) and all(isinstance(s, dict) and "title" in s for s in slides): 377 | return slides 378 | except json.JSONDecodeError: 379 | pass 380 | 381 | # 如果直接解析失败,尝试从文本中提取JSON部分 382 | json_match = re.search(r'\[\s*\{.*\}\s*\]', text, re.DOTALL) 383 | if json_match: 384 | try: 385 | slides = json.loads(json_match.group(0)) 386 | if isinstance(slides, list) and all(isinstance(s, dict) and "title" in s for s in slides): 387 | return slides 388 | except json.JSONDecodeError: 389 | pass 390 | 391 | # 如果仍然失败,尝试从文本中提取幻灯片标题和类型 392 | slides = [] 393 | current_title = None 394 | current_type = "content" # 默认类型 395 | 396 | for line in text.split('\n'): 397 | # 查找幻灯片标题 398 | title_match = re.search(r'(?:幻灯片\s*\d+\s*[::]\s*|标题\s*[::]\s*)(.+)', line) 399 | if title_match: 400 | if current_title: # 如果已经有标题,保存当前幻灯片 401 | slides.append({"title": current_title, "type": current_type}) 402 | current_title = title_match.group(1).strip() 403 | current_type = "content" # 重置类型 404 | continue 405 | 406 | # 查找幻灯片类型 407 | type_match = re.search(r'(?:类型\s*[::]\s*)(\w+)', line) 408 | if type_match and current_title: 409 | type_text = type_match.group(1).lower() 410 | if "两列" in type_text or "two" in type_text: 411 | current_type = "two_column" 412 | elif "图片" in type_text or "image" in type_text: 413 | current_type = "image_content" 414 | else: 415 | current_type = "content" 416 | 417 | # 添加最后一个幻灯片 418 | if current_title: 419 | slides.append({"title": current_title, "type": current_type}) 420 | 421 | return slides if slides else self._get_mock_slides(section_title) 422 | 423 | def _get_mock_main_sections(self, topic: str, doc_type: str) -> List[Dict[str, Any]]: 424 | """ 425 | 获取模拟的主要章节 426 | 427 | Args: 428 | topic: 文档主题 429 | doc_type: 文档类型 430 | 431 | Returns: 432 | 模拟的章节列表 433 | """ 434 | return [ 435 | {"title": "引言"}, 436 | {"title": f"{topic}的基本概念"}, 437 | {"title": f"{topic}的主要特点"}, 438 | {"title": f"{topic}的应用场景"}, 439 | {"title": f"{topic}的发展趋势"}, 440 | {"title": "总结与展望"} 441 | ] 442 | 443 | def _get_mock_section_detail(self, section: Dict[str, Any], doc_type: str) -> Dict[str, Any]: 444 | """ 445 | 获取模拟的章节详情 446 | 447 | Args: 448 | section: 章节信息 449 | doc_type: 文档类型 450 | 451 | Returns: 452 | 模拟的章节详情 453 | """ 454 | section_title = section.get("title", "未知章节") 455 | 456 | if doc_type == "ppt": 457 | return { 458 | "title": section_title, 459 | "slides": self._get_mock_slides(section_title) 460 | } 461 | else: 462 | return { 463 | "title": section_title, 464 | "subsections": self._get_mock_subsections(section_title) 465 | } 466 | 467 | def _get_mock_slides(self, section_title: str) -> List[Dict[str, Any]]: 468 | """ 469 | 获取模拟的幻灯片 470 | 471 | Args: 472 | section_title: 章节标题 473 | 474 | Returns: 475 | 模拟的幻灯片列表 476 | """ 477 | return [ 478 | {"title": f"{section_title}概述", "type": "content"}, 479 | {"title": f"{section_title}的关键要素", "type": "two_column"}, 480 | {"title": f"{section_title}的应用示例", "type": "image_content"}, 481 | {"title": f"{section_title}的最佳实践", "type": "content"} 482 | ] 483 | 484 | def _get_mock_subsections(self, section_title: str) -> List[Dict[str, Any]]: 485 | """ 486 | 获取模拟的子章节 487 | 488 | Args: 489 | section_title: 章节标题 490 | 491 | Returns: 492 | 模拟的子章节列表 493 | """ 494 | return [ 495 | {"title": f"{section_title}概述"}, 496 | {"title": f"{section_title}的关键要素"}, 497 | {"title": f"{section_title}的应用示例"}, 498 | {"title": f"{section_title}的最佳实践"} 499 | ] -------------------------------------------------------------------------------- /backend/app/services/pdf_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Dict, Any, Optional 3 | import logging 4 | from reportlab.lib.pagesizes import letter 5 | from reportlab.lib import colors 6 | from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle 7 | from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak 8 | from reportlab.lib.units import inch 9 | from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY 10 | import datetime 11 | 12 | from .ai_service_factory import AIServiceFactory 13 | 14 | # 配置日志 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger(__name__) 17 | 18 | class PDFGenerator: 19 | """ 20 | 生成PDF文档 21 | """ 22 | def __init__(self, ai_service_type: str = "deepseek"): 23 | # 修正路径:生成文档目录和app是同级的 24 | self.output_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "generated_docs")) 25 | logger.info(f"PDF生成器输出目录: {self.output_dir}") 26 | os.makedirs(self.output_dir, exist_ok=True) 27 | 28 | # 确保目录权限正确 29 | try: 30 | os.chmod(self.output_dir, 0o777) 31 | except Exception as e: 32 | logger.warning(f"无法修改目录权限: {e}") 33 | 34 | self.ai_service = AIServiceFactory.create_service(ai_service_type) 35 | 36 | def generate(self, topic: str, outline: List[Dict[str, Any]], template_id: Optional[str] = None) -> Optional[str]: 37 | """ 38 | 根据大纲生成PDF文档 39 | 40 | Args: 41 | topic: 文档主题 42 | outline: 文档大纲 43 | template_id: 可选的模板ID 44 | 45 | Returns: 46 | 生成的PDF文件路径,如果失败则返回None 47 | """ 48 | try: 49 | logger.info(f"开始生成PDF文档: 主题='{topic}', 章节数={len(outline)}") 50 | if template_id: 51 | logger.info(f"使用模板: {template_id}") 52 | 53 | # 创建文件名和文档 54 | file_name = f"{topic.replace(' ', '_')}_document.pdf" 55 | file_path = os.path.join(self.output_dir, file_name) 56 | 57 | # 创建文档对象 58 | doc = SimpleDocTemplate( 59 | file_path, 60 | pagesize=letter, 61 | rightMargin=72, 62 | leftMargin=72, 63 | topMargin=72, 64 | bottomMargin=72 65 | ) 66 | logger.info(f"创建新的PDF文档对象,页面大小: {letter}") 67 | 68 | # 获取样式 69 | styles = getSampleStyleSheet() 70 | styles.add(ParagraphStyle( 71 | name='Title', 72 | parent=styles['Heading1'], 73 | fontSize=24, 74 | alignment=TA_CENTER, 75 | spaceAfter=36 76 | )) 77 | styles.add(ParagraphStyle( 78 | name='Normal_Justified', 79 | parent=styles['Normal'], 80 | alignment=TA_JUSTIFY, 81 | fontSize=12, 82 | leading=14, 83 | spaceAfter=12 84 | )) 85 | 86 | # 创建内容元素列表 87 | elements = [] 88 | 89 | # 添加标题页 90 | logger.info("添加标题页") 91 | elements.append(Paragraph(topic, styles['Title'])) 92 | elements.append(Paragraph("AI自动生成的文档", styles['Heading2'])) 93 | elements.append(Spacer(1, 0.5*inch)) 94 | elements.append(Paragraph(datetime.datetime.now().strftime("%Y年%m月%d日"), styles['Normal'])) 95 | elements.append(PageBreak()) 96 | 97 | # 添加目录 98 | logger.info("添加目录") 99 | elements.append(Paragraph("目录", styles['Heading1'])) 100 | elements.append(Spacer(1, 0.25*inch)) 101 | 102 | for i, section in enumerate(outline): 103 | section_title = section.get("title", "未知章节") 104 | elements.append(Paragraph(f"{i+1}. {section_title}", styles['Normal'])) 105 | 106 | # 添加子章节到目录 107 | subsections = section.get("subsections", []) 108 | for j, subsection in enumerate(subsections): 109 | subsection_title = subsection.get("title", "未知子章节") 110 | elements.append(Paragraph(f" {i+1}.{j+1} {subsection_title}", styles['Normal'])) 111 | 112 | elements.append(PageBreak()) 113 | 114 | # 添加章节内容 115 | for section_index, section in enumerate(outline): 116 | section_title = section.get("title", "未知章节") 117 | logger.info(f"处理章节 {section_index+1}/{len(outline)}: '{section_title}'") 118 | 119 | # 添加章节标题 120 | elements.append(Paragraph(f"{section_index+1}. {section_title}", styles['Heading1'])) 121 | 122 | # 生成章节内容 123 | content = self.ai_service.generate_section_content(topic, section_title, "pdf") 124 | paragraphs = content.strip().split('\n\n') 125 | for p_text in paragraphs: 126 | if p_text: 127 | elements.append(Paragraph(p_text.strip(), styles['Normal_Justified'])) 128 | 129 | # 处理子章节 130 | subsections = section.get("subsections", []) 131 | if subsections: 132 | logger.info(f" 章节 '{section_title}' 包含 {len(subsections)} 个子章节") 133 | 134 | for subsection_index, subsection in enumerate(subsections): 135 | subsection_title = subsection.get("title", "未知子章节") 136 | logger.info(f" 处理子章节 {subsection_index+1}/{len(subsections)}: '{subsection_title}'") 137 | 138 | # 添加子章节标题 139 | elements.append(Paragraph( 140 | f"{section_index+1}.{subsection_index+1} {subsection_title}", 141 | styles['Heading2'] 142 | )) 143 | 144 | # 生成子章节内容 145 | subcontent = self.ai_service.generate_section_content(topic, subsection_title, "pdf") 146 | subparagraphs = subcontent.strip().split('\n\n') 147 | for p_text in subparagraphs: 148 | if p_text: 149 | elements.append(Paragraph(p_text.strip(), styles['Normal_Justified'])) 150 | 151 | # 添加参考文献 152 | logger.info("添加参考文献") 153 | elements.append(PageBreak()) 154 | elements.append(Paragraph("参考文献", styles['Heading1'])) 155 | elements.append(Spacer(1, 0.25*inch)) 156 | 157 | # 生成一些模拟的参考文献 158 | references = [ 159 | f"[1] AI文档生成系统. (2023). {topic}研究综述.", 160 | f"[2] 智能文档分析小组. (2023). {topic}的最新进展.", 161 | f"[3] 文档自动化协会. (2022). {topic}标准与实践." 162 | ] 163 | 164 | for ref in references: 165 | elements.append(Paragraph(ref, styles['Normal'])) 166 | elements.append(Spacer(1, 0.1*inch)) 167 | 168 | # 构建文档 169 | logger.info("构建PDF文档") 170 | doc.build(elements) 171 | 172 | # 估算页数 173 | page_count = len(elements) // 20 # 假设每页约20个元素 174 | logger.info(f"PDF文档生成成功: 约 {page_count} 页, 保存至: {file_path}") 175 | return file_path 176 | 177 | except Exception as e: 178 | logger.error(f"生成PDF文档时出错: {str(e)}") 179 | return None -------------------------------------------------------------------------------- /backend/app/services/ppt_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional, List 2 | import os 3 | from pptx import Presentation 4 | from pptx.util import Inches, Pt 5 | from pptx.dml.color import RGBColor 6 | from pptx.enum.text import PP_ALIGN 7 | from pptx.slide import Slide 8 | from pptx.text.text import TextFrame 9 | from pptx.enum.shapes import MSO_SHAPE 10 | import logging 11 | 12 | from .ai_service_factory import AIServiceFactory 13 | 14 | # 配置日志 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger(__name__) 17 | 18 | class PPTGenerator: 19 | def __init__(self, ai_service_type: str = "deepseek"): 20 | self.templates_dir = os.path.join(os.path.dirname(__file__), "../templates/ppt_templates") 21 | # 修正路径:生成文档目录和app是同级的 22 | self.output_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "generated_docs")) 23 | logger.info(f"PPT生成器输出目录: {self.output_dir}") 24 | os.makedirs(self.output_dir, exist_ok=True) 25 | 26 | # 确保目录权限正确 27 | try: 28 | os.chmod(self.output_dir, 0o777) 29 | except Exception as e: 30 | logger.warning(f"无法修改目录权限: {e}") 31 | 32 | # 定义配色方案 33 | self.COLORS = { 34 | 'primary': RGBColor(41, 128, 185), # 主色调:蓝色 35 | 'secondary': RGBColor(44, 62, 80), # 次要色:深灰 36 | 'accent': RGBColor(231, 76, 60), # 强调色:红色 37 | 'light': RGBColor(236, 240, 241), # 浅色:近白 38 | 'dark': RGBColor(52, 73, 94) # 深色:深灰蓝 39 | } 40 | 41 | self.prs = None 42 | self.ai_service = AIServiceFactory.create_service(ai_service_type) 43 | 44 | def generate(self, topic: str, outline: List[Dict[str, Any]], template_id: Optional[str] = None) -> Optional[str]: 45 | """ 46 | 生成PPT文档 47 | 48 | Args: 49 | topic: 主题 50 | outline: 大纲内容 51 | template_id: 模板ID(现在不使用,保留参数以保持接口兼容) 52 | """ 53 | try: 54 | logger.info(f"开始生成PPT: 主题='{topic}', 章节数={len(outline)}") 55 | if template_id: 56 | logger.info(f"使用模板: {template_id}") 57 | 58 | # 创建新的演示文稿 59 | self.prs = Presentation() 60 | self.prs.slide_width = Inches(16) 61 | self.prs.slide_height = Inches(9) 62 | logger.info(f"创建新的演示文稿: 宽度={self.prs.slide_width}, 高度={self.prs.slide_height}") 63 | 64 | # 添加标题幻灯片 65 | logger.info("添加标题幻灯片") 66 | self._add_title_slide(topic) 67 | 68 | # 添加目录幻灯片 69 | logger.info("添加目录幻灯片") 70 | self._add_toc_slide(outline) 71 | 72 | # 处理每个章节 73 | total_slides = 2 # 已添加标题和目录幻灯片 74 | for section_index, section in enumerate(outline): 75 | section_title = section["title"] 76 | logger.info(f"处理章节 {section_index+1}/{len(outline)}: '{section_title}'") 77 | 78 | # 添加章节标题幻灯片 79 | self._add_section_title_slide(section_title) 80 | total_slides += 1 81 | 82 | # 添加章节内容幻灯片 83 | slides = section.get("slides", []) 84 | logger.info(f" 章节 '{section_title}' 包含 {len(slides)} 张幻灯片") 85 | 86 | for slide_index, slide_content in enumerate(slides): 87 | slide_title = slide_content.get("title", "未知标题") 88 | slide_type = slide_content.get("type", "content") 89 | logger.info(f" 添加幻灯片 {slide_index+1}/{len(slides)}: '{slide_title}' (类型: {slide_type})") 90 | self._add_content_slide(slide_content, topic, section_title) 91 | total_slides += 1 92 | 93 | # 添加结束幻灯片 94 | logger.info("添加结束幻灯片") 95 | self._add_ending_slide(topic) 96 | total_slides += 1 97 | 98 | # 保存文件 99 | output_path = os.path.join(self.output_dir, f"{topic}_presentation.pptx") 100 | self.prs.save(output_path) 101 | 102 | logger.info(f"PPT生成完成: 共 {total_slides} 张幻灯片, 保存至: {output_path}") 103 | return output_path 104 | 105 | except Exception as e: 106 | logger.error(f"生成PPT时出错: {str(e)}") 107 | return None 108 | finally: 109 | self.prs = None 110 | 111 | def _add_points(self, text_frame: TextFrame, points: List[Dict[str, Any]]) -> None: 112 | """添加要点和详细说明,自动调整字体大小和布局""" 113 | # 要点数量的阈值,超过此数量则减小字体大小 114 | points_threshold = 3 115 | details_threshold = 2 116 | 117 | # 根据要点数量调整字体大小 118 | main_point_size = Pt(28) 119 | detail_point_size = Pt(20) 120 | 121 | if len(points) > points_threshold: 122 | # 要点较多,减小字体 123 | main_point_size = Pt(24) 124 | detail_point_size = Pt(18) 125 | 126 | for i, point in enumerate(points): 127 | # 添加主要要点 128 | p = text_frame.add_paragraph() 129 | p.text = "▪ " + point.get("main", "") # 添加项目符号 130 | p.level = 0 131 | p.font.name = '微软雅黑' 132 | p.font.size = main_point_size 133 | p.font.bold = True 134 | p.font.color.rgb = self.COLORS['primary'] 135 | p.space_after = Pt(8) if len(points) > points_threshold else Pt(12) # 根据内容量调整间距 136 | 137 | # 获取详细说明 138 | details = point.get("details", []) 139 | 140 | # 如果详细说明太多,减小字体大小和间距 141 | detail_font_size = detail_point_size 142 | if len(details) > details_threshold: 143 | detail_font_size = Pt(16) 144 | 145 | # 添加详细说明 146 | for detail in details: 147 | p = text_frame.add_paragraph() 148 | p.text = "• " + detail 149 | p.level = 1 150 | p.font.name = '微软雅黑' 151 | p.font.size = detail_font_size 152 | p.font.color.rgb = self.COLORS['secondary'] 153 | p.space_before = Pt(4) if len(details) > details_threshold else Pt(6) # 根据内容量调整间距 154 | p.space_after = Pt(4) if len(details) > details_threshold else Pt(6) # 根据内容量调整间距 155 | 156 | def _add_content_slide(self, content: Dict[str, Any], topic: str, section_title: str) -> None: 157 | """添加内容幻灯片,根据内容量自动调整布局""" 158 | slide = self.prs.slides.add_slide(self.prs.slide_layouts[6]) 159 | 160 | # 添加标题和装饰线条 161 | title = slide.shapes.add_textbox( 162 | Inches(1), Inches(0.5), Inches(14), Inches(1) 163 | ) 164 | title.text_frame.text = content.get("title", "") 165 | p = title.text_frame.paragraphs[0] 166 | p.font.name = '微软雅黑' 167 | p.font.size = Pt(36) 168 | p.font.bold = True 169 | p.font.color.rgb = self.COLORS['dark'] 170 | p.alignment = PP_ALIGN.LEFT 171 | 172 | # 添加装饰线条(使用矩形作为线条) 173 | line = slide.shapes.add_shape( 174 | MSO_SHAPE.RECTANGLE, 175 | Inches(1), Inches(1.3), Inches(14), Inches(0.03) 176 | ) 177 | line.fill.solid() 178 | line.fill.fore_color.rgb = self.COLORS['primary'] 179 | 180 | # 获取幻灯片内容 181 | slide_title = content.get("title", "") 182 | slide_type = content.get("type", "content") 183 | 184 | # 使用AI服务生成幻灯片内容 185 | slide_content = self.ai_service.generate_slide_content(topic, section_title, slide_title, slide_type) 186 | 187 | # 获取要点数量,判断是否需要双列布局 188 | points = slide_content.get("points", []) 189 | total_points = len(points) 190 | total_details = sum(len(point.get("details", [])) for point in points) 191 | 192 | # 自动判断布局方式 193 | use_two_column = total_points > 3 or total_details > 6 194 | 195 | if slide_type == "two_column" or use_two_column: 196 | # 如果指定双列或内容较多,使用两栏布局 197 | if slide_type == "two_column" and "left_points" in slide_content and "right_points" in slide_content: 198 | # 使用预定义的左右栏内容 199 | left_content = slide.shapes.add_textbox( 200 | Inches(1), Inches(1.5), Inches(6.5), Inches(6) 201 | ) 202 | right_content = slide.shapes.add_textbox( 203 | Inches(8), Inches(1.5), Inches(6.5), Inches(6) 204 | ) 205 | self._add_points(left_content.text_frame, slide_content.get("left_points", [])) 206 | self._add_points(right_content.text_frame, slide_content.get("right_points", [])) 207 | else: 208 | # 自动将内容分为左右两栏 209 | left_points = points[:len(points)//2 + len(points)%2] # 左侧放置一半+余数 210 | right_points = points[len(points)//2 + len(points)%2:] # 右侧放置剩余部分 211 | 212 | left_content = slide.shapes.add_textbox( 213 | Inches(1), Inches(1.5), Inches(6.5), Inches(6) 214 | ) 215 | right_content = slide.shapes.add_textbox( 216 | Inches(8), Inches(1.5), Inches(6.5), Inches(6) 217 | ) 218 | self._add_points(left_content.text_frame, left_points) 219 | self._add_points(right_content.text_frame, right_points) 220 | else: 221 | # 普通单列布局 222 | text_content = slide.shapes.add_textbox( 223 | Inches(1), Inches(1.5), 224 | Inches(14 if slide_type == "content" else 8), 225 | Inches(6.5) # 增加高度 226 | ) 227 | self._add_points(text_content.text_frame, points) 228 | 229 | if slide_type == "image_content": 230 | # 在这里我们只添加图片描述,实际项目中可以根据描述生成或选择图片 231 | image_desc = slide_content.get("image_description", "") 232 | if image_desc: 233 | image_note = slide.shapes.add_textbox( 234 | Inches(9.5), Inches(3.5), Inches(5.5), Inches(1) 235 | ) 236 | image_note.text_frame.text = f"[图片: {image_desc}]" 237 | p = image_note.text_frame.paragraphs[0] 238 | p.font.italic = True 239 | p.font.color.rgb = self.COLORS['secondary'] 240 | 241 | # 添加注释 242 | if notes := content.get("notes"): 243 | slide.notes_slide.notes_text_frame.text = notes 244 | 245 | def _add_title_slide(self, topic: str) -> None: 246 | """添加标题幻灯片""" 247 | slide_layout = self.prs.slide_layouts[0] # 使用标题布局 248 | slide = self.prs.slides.add_slide(slide_layout) 249 | 250 | title = slide.shapes.title 251 | subtitle = slide.placeholders[1] 252 | 253 | title.text = topic 254 | subtitle.text = "AI自动生成的演示文稿" 255 | 256 | # 设置标题样式 257 | for paragraph in title.text_frame.paragraphs: 258 | paragraph.alignment = PP_ALIGN.CENTER 259 | for run in paragraph.runs: 260 | run.font.size = Pt(44) 261 | run.font.bold = True 262 | run.font.color.rgb = self.COLORS['primary'] 263 | 264 | # 设置副标题样式 265 | for paragraph in subtitle.text_frame.paragraphs: 266 | paragraph.alignment = PP_ALIGN.CENTER 267 | for run in paragraph.runs: 268 | run.font.size = Pt(24) 269 | run.font.italic = True 270 | run.font.color.rgb = self.COLORS['secondary'] 271 | 272 | def _add_toc_slide(self, outline: List[Dict[str, Any]]) -> None: 273 | """添加目录幻灯片""" 274 | slide = self.prs.slides.add_slide(self.prs.slide_layouts[6]) # 使用空白布局 275 | 276 | # 添加标题 277 | title = slide.shapes.add_textbox( 278 | Inches(1), Inches(0.5), Inches(14), Inches(1) 279 | ) 280 | title.text_frame.text = "目录" 281 | title_p = title.text_frame.paragraphs[0] 282 | title_p.alignment = PP_ALIGN.CENTER 283 | title_p.font.size = Pt(40) 284 | title_p.font.bold = True 285 | title_p.font.color.rgb = self.COLORS['primary'] 286 | 287 | # 添加装饰线条 288 | line = slide.shapes.add_shape( 289 | MSO_SHAPE.RECTANGLE, # 修改这里 290 | Inches(2), Inches(1.8), Inches(12), Inches(0.05) 291 | ) 292 | line.fill.solid() 293 | line.fill.fore_color.rgb = self.COLORS['primary'] 294 | 295 | # 创建两列布局 296 | left_content = slide.shapes.add_textbox( 297 | Inches(2), Inches(2.2), Inches(5.5), Inches(5) 298 | ) 299 | right_content = slide.shapes.add_textbox( 300 | Inches(8.5), Inches(2.2), Inches(5.5), Inches(5) 301 | ) 302 | 303 | # 分配目录项到两列 304 | mid_point = len(outline) // 2 + len(outline) % 2 305 | 306 | # 添加左列目录项 307 | tf_left = left_content.text_frame 308 | for i, section in enumerate(outline[:mid_point], 1): 309 | p = tf_left.add_paragraph() 310 | p.text = f"{i}. {section['title']}" 311 | p.font.size = Pt(24) 312 | p.font.bold = True 313 | p.font.color.rgb = self.COLORS['secondary'] 314 | p.space_after = Pt(20) # 增加段落间距 315 | 316 | # 添加右列目录项 317 | tf_right = right_content.text_frame 318 | for i, section in enumerate(outline[mid_point:], mid_point + 1): 319 | p = tf_right.add_paragraph() 320 | p.text = f"{i}. {section['title']}" 321 | p.font.size = Pt(24) 322 | p.font.bold = True 323 | p.font.color.rgb = self.COLORS['secondary'] 324 | p.space_after = Pt(20) # 增加段落间距 325 | 326 | def _add_section_title_slide(self, section_title: str) -> None: 327 | """添加章节标题幻灯片""" 328 | slide_layout = self.prs.slide_layouts[2] # 使用章节标题布局 329 | slide = self.prs.slides.add_slide(slide_layout) 330 | 331 | title = slide.shapes.title 332 | title.text = section_title 333 | 334 | # 设置标题样式 335 | for paragraph in title.text_frame.paragraphs: 336 | paragraph.alignment = PP_ALIGN.CENTER 337 | for run in paragraph.runs: 338 | run.font.size = Pt(40) 339 | run.font.bold = True 340 | run.font.color.rgb = self.COLORS['primary'] 341 | 342 | def _add_ending_slide(self, topic: str) -> None: 343 | """添加结束幻灯片""" 344 | slide_layout = self.prs.slide_layouts[1] # 使用标题和内容布局 345 | slide = self.prs.slides.add_slide(slide_layout) 346 | 347 | title = slide.shapes.title 348 | content = slide.placeholders[1] 349 | 350 | title.text = "总结与问答" 351 | 352 | # 设置标题样式 353 | for paragraph in title.text_frame.paragraphs: 354 | paragraph.alignment = PP_ALIGN.CENTER 355 | for run in paragraph.runs: 356 | run.font.size = Pt(40) 357 | run.font.bold = True 358 | run.font.color.rgb = self.COLORS['primary'] 359 | 360 | # 添加总结内容 361 | tf = content.text_frame 362 | 363 | summary_points = [ 364 | f"我们探讨了{topic}的多个关键方面", 365 | f"理解这些概念对把握{topic}的本质至关重要", 366 | f"未来发展将带来更多机遇和挑战", 367 | f"感谢您的关注!有任何问题欢迎提问" 368 | ] 369 | 370 | for point in summary_points: 371 | p = tf.add_paragraph() 372 | p.text = point 373 | p.level = 0 374 | 375 | # 设置段落样式 376 | p.alignment = PP_ALIGN.CENTER 377 | for run in p.runs: 378 | run.font.size = Pt(28) 379 | run.font.color.rgb = self.COLORS['secondary'] -------------------------------------------------------------------------------- /backend/app/services/word_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Dict, Any, Optional 3 | import logging 4 | from docx import Document 5 | from docx.shared import Pt, Inches, RGBColor 6 | from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING 7 | from docx.enum.style import WD_STYLE_TYPE 8 | from docx.oxml.ns import qn 9 | from ..core.config import settings 10 | 11 | from .ai_service_factory import AIServiceFactory 12 | 13 | # 配置日志 14 | logging.basicConfig(level=logging.INFO) 15 | logger = logging.getLogger(__name__) 16 | 17 | class WordGenerator: 18 | """ 19 | 生成Word文档 20 | """ 21 | def __init__(self, ai_service_type: str = "deepseek"): 22 | self.templates_dir = os.path.join(os.path.dirname(__file__), "../templates/word_templates") 23 | self.output_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "generated_docs")) 24 | logger.info(f"Word生成器输出目录: {self.output_dir}") 25 | os.makedirs(self.output_dir, exist_ok=True) 26 | 27 | # 确保目录权限正确 28 | try: 29 | os.chmod(self.output_dir, 0o777) 30 | except Exception as e: 31 | logger.warning(f"无法修改目录权限: {e}") 32 | 33 | self.ai_service = AIServiceFactory.create_service(ai_service_type) 34 | 35 | def generate(self, topic: str, outline: List[Dict[str, Any]], template_id: Optional[str] = None) -> Optional[str]: 36 | """ 37 | 根据大纲生成Word文档 38 | 39 | Args: 40 | topic: 文档主题 41 | outline: 文档大纲 42 | template_id: 可选的模板ID 43 | 44 | Returns: 45 | 生成的Word文件路径,如果失败则返回None 46 | """ 47 | try: 48 | logger.info(f"开始生成Word文档: 主题='{topic}', 章节数={len(outline)}") 49 | if template_id: 50 | logger.info(f"使用模板: {template_id}") 51 | 52 | # 创建文档 53 | doc = self._create_document(template_id) 54 | logger.info(f"创建新的Word文档" + (f" (使用模板: {template_id})" if template_id else "")) 55 | 56 | # 添加标题页 57 | logger.info("添加标题页") 58 | self._add_title_page(doc, topic) 59 | 60 | # 添加目录 61 | logger.info("添加目录") 62 | self._add_toc(doc) 63 | 64 | # 添加章节内容 65 | for section_index, section in enumerate(outline): 66 | section_title = section["title"] 67 | logger.info(f"处理章节 {section_index+1}/{len(outline)}: '{section_title}'") 68 | self._add_section(doc, section, topic) 69 | 70 | # 记录子章节信息 71 | subsections = section.get("subsections", []) 72 | if subsections: 73 | logger.info(f" 章节 '{section_title}' 包含 {len(subsections)} 个子章节") 74 | for i, subsection in enumerate(subsections): 75 | subsection_title = subsection.get("title", "未知子章节") 76 | logger.info(f" 子章节 {i+1}: '{subsection_title}'") 77 | 78 | # 添加参考文献 79 | logger.info("添加参考文献") 80 | self._add_references(doc, topic) 81 | 82 | # 保存文件 83 | file_name = f"{topic.replace(' ', '_')}_document.docx" 84 | file_path = os.path.join(self.output_dir, file_name) 85 | doc.save(file_path) 86 | 87 | # 计算页数(近似值) 88 | page_count = len(doc.paragraphs) // 20 # 假设每页约20个段落 89 | logger.info(f"Word文档生成成功: 约 {page_count} 页, 保存至: {file_path}") 90 | return file_path 91 | 92 | except Exception as e: 93 | logger.error(f"生成Word文档时出错: {str(e)}") 94 | return None 95 | 96 | def _create_document(self, template_id: Optional[str]) -> Document: 97 | """ 98 | 创建文档对象,可选择使用模板 99 | """ 100 | # 如果提供了模板ID,尝试加载模板 101 | if template_id and template_id != "default": 102 | template_path = f"templates/word/{template_id}.docx" 103 | if os.path.exists(template_path): 104 | return Document(template_path) 105 | 106 | # 默认创建空白文档 107 | doc = Document() 108 | 109 | # 设置默认样式 110 | styles = doc.styles 111 | 112 | # 设置正文样式 113 | style_normal = styles['Normal'] 114 | font = style_normal.font 115 | font.name = 'Times New Roman' 116 | font.size = Pt(12) 117 | 118 | # 设置标题样式 119 | for i in range(1, 4): 120 | style_name = f'Heading {i}' 121 | if style_name in styles: 122 | style = styles[style_name] 123 | font = style.font 124 | font.name = 'Arial' 125 | font.size = Pt(16 - (i-1)*2) 126 | font.bold = True 127 | 128 | return doc 129 | 130 | def _add_title_page(self, doc: Document, topic: str) -> None: 131 | """ 132 | 添加标题页 133 | """ 134 | # 添加标题 135 | doc.add_paragraph().add_run().add_break() # 添加空行 136 | title = doc.add_paragraph(topic) 137 | title.alignment = WD_ALIGN_PARAGRAPH.CENTER 138 | title_run = title.runs[0] 139 | title_run.font.size = Pt(24) 140 | title_run.font.bold = True 141 | 142 | # 添加副标题 143 | doc.add_paragraph().add_run().add_break() # 添加空行 144 | subtitle = doc.add_paragraph("AI自动生成的文档") 145 | subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER 146 | subtitle_run = subtitle.runs[0] 147 | subtitle_run.font.size = Pt(16) 148 | subtitle_run.font.italic = True 149 | 150 | # 添加日期 151 | doc.add_paragraph().add_run().add_break() # 添加空行 152 | date = doc.add_paragraph() 153 | date.alignment = WD_ALIGN_PARAGRAPH.CENTER 154 | import datetime 155 | date.add_run(datetime.datetime.now().strftime("%Y年%m月%d日")) 156 | 157 | # 添加分页符 158 | doc.add_page_break() 159 | 160 | def _add_toc(self, doc: Document) -> None: 161 | """ 162 | 添加目录 163 | """ 164 | doc.add_paragraph("目录", style='Heading 1') 165 | 166 | # 添加目录域代码 167 | paragraph = doc.add_paragraph() 168 | run = paragraph.add_run() 169 | 170 | # 使用域代码添加目录 171 | # 这只是一个占位符,实际的目录需要在Word中更新 172 | run.add_text("目录将在Word中显示。请右键点击并选择'更新域'来显示目录。") 173 | 174 | # 添加分页符 175 | doc.add_page_break() 176 | 177 | def _add_section(self, doc: Document, section: Dict[str, Any], topic: str) -> None: 178 | """ 179 | 添加章节 180 | """ 181 | # 添加章节标题 182 | section_title = section["title"] 183 | doc.add_paragraph(section_title, style='Heading 1') 184 | 185 | # 使用AI服务生成章节内容 186 | content = self.ai_service.generate_section_content(topic, section_title, "word") 187 | 188 | # 添加章节介绍 189 | intro = doc.add_paragraph() 190 | intro.add_run(content) 191 | 192 | # 添加子章节 193 | subsections = section.get("subsections", []) 194 | if not subsections: 195 | # 如果没有子章节,添加一些默认内容 196 | self._add_default_content(doc, section_title, topic) 197 | return 198 | 199 | for subsection in subsections: 200 | self._add_subsection(doc, subsection, section_title, topic) 201 | 202 | def _add_subsection(self, doc: Document, subsection: Dict[str, Any], section_title: str, topic: str) -> None: 203 | """ 204 | 添加子章节 205 | """ 206 | # 添加子章节标题 207 | subsection_title = subsection["title"] 208 | doc.add_paragraph(subsection_title, style='Heading 2') 209 | 210 | # 使用AI服务生成子章节内容 211 | content = self.ai_service.generate_section_content(topic, subsection_title, "word") 212 | 213 | # 将内容分段添加 214 | paragraphs = content.strip().split('\n\n') 215 | for p_text in paragraphs: 216 | if p_text: 217 | p = doc.add_paragraph() 218 | p.add_run(p_text.strip()) 219 | p.paragraph_format.line_spacing = 1.5 220 | 221 | def _add_default_content(self, doc: Document, section_title: str, topic: str) -> None: 222 | """ 223 | 添加默认内容 224 | """ 225 | content = f""" 226 | {section_title}是{topic}的核心组成部分,它包含多个关键要素和特性。 227 | 228 | 深入理解{section_title}对掌握整个主题至关重要。通过系统分析其基本原理、应用场景和发展趋势,我们可以更全面地把握{topic}的本质。 229 | 230 | {section_title}的理论基础建立在多年的研究和实践之上。众多学者和专家通过实证研究和理论探索,不断丰富和完善相关知识体系。 231 | 232 | 在实际应用中,{section_title}展现出强大的适应性和实用价值。无论是在教育、商业还是技术创新领域,都能找到其成功应用的案例。 233 | 234 | 未来研究将进一步探索{section_title}的潜力和应用前景。随着新技术和新方法的出现,我们有理由相信,{section_title}将在{topic}的发展中发挥更加重要的作用。 235 | """ 236 | 237 | paragraphs = content.strip().split('\n\n') 238 | for p_text in paragraphs: 239 | if p_text: 240 | p = doc.add_paragraph() 241 | p.add_run(p_text.strip()) 242 | p.paragraph_format.line_spacing = 1.5 243 | 244 | def _add_references(self, doc: Document, topic: str) -> None: 245 | """ 246 | 添加参考文献 247 | """ 248 | doc.add_page_break() 249 | doc.add_paragraph("参考文献", style='Heading 1') 250 | 251 | # 添加一些模拟的参考文献 252 | references = [ 253 | f"Smith, J. (2022). 理解{topic}的基本原理. 学术期刊, 45(2), 112-128.", 254 | f"Johnson, A., & Williams, B. (2021). {topic}的应用与实践. 科技出版社.", 255 | f"Chen, L., Wang, H., & Zhang, Y. (2023). {topic}的最新进展. 研究评论, 10(3), 78-95.", 256 | f"Taylor, M. (2020). {topic}在教育领域的应用. 教育研究, 33(1), 45-62.", 257 | f"Brown, R., & Davis, S. (2022). {topic}的未来发展趋势. 未来研究, 15(4), 201-215." 258 | ] 259 | 260 | for ref in references: 261 | p = doc.add_paragraph() 262 | p.add_run(ref) 263 | p.paragraph_format.first_line_indent = Inches(0.5) 264 | p.paragraph_format.line_spacing = 1.5 -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.95.0 2 | uvicorn>=0.22.0 3 | python-dotenv>=1.0.0 4 | pydantic>=2.0.0 5 | pydantic-settings>=2.0.0 6 | requests>=2.28.0 7 | python-multipart>=0.0.6 8 | python-pptx>=0.6.21 9 | python-docx>=0.8.11 10 | jinja2>=3.1.2 11 | aiofiles>=23.1.0 12 | tenacity>=8.0.0 13 | reportlab>=3.6.12 14 | aiohttp>=3.9.1 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | backend: 5 | image: ai_doc_platform-backend 6 | build: 7 | context: ./backend 8 | dockerfile: Dockerfile.local 9 | container_name: ai-doc-platform-backend 10 | restart: unless-stopped 11 | volumes: 12 | - ./backend:/app 13 | - generated_docs:/app/generated_docs 14 | env_file: 15 | - .env 16 | environment: 17 | - AI_API_KEY=${AI_API_KEY} 18 | - AI_API_ENDPOINT=${AI_API_ENDPOINT} 19 | ports: 20 | - "8001:8001" 21 | deploy: 22 | resources: 23 | limits: 24 | cpus: '0.5' 25 | memory: 512M 26 | healthcheck: 27 | test: ["CMD", "curl", "-f", "http://localhost:8001/api/health"] 28 | interval: 30s 29 | timeout: 10s 30 | retries: 3 31 | start_period: 15s 32 | networks: 33 | - app-network 34 | 35 | frontend: 36 | image: ai_doc_platform-frontend 37 | build: ./frontend 38 | container_name: ai-doc-platform-frontend 39 | restart: unless-stopped 40 | environment: 41 | - VUE_APP_API_URL=http://localhost:8080/api/v1 42 | ports: 43 | - "3000:80" 44 | deploy: 45 | resources: 46 | limits: 47 | cpus: '0.3' 48 | memory: 256M 49 | depends_on: 50 | - backend 51 | networks: 52 | - app-network 53 | 54 | nginx: 55 | image: ai_doc_platform-nginx 56 | build: ./nginx 57 | container_name: ai-doc-platform-nginx 58 | restart: unless-stopped 59 | ports: 60 | - "8080:80" 61 | depends_on: 62 | - backend 63 | - frontend 64 | volumes: 65 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf 66 | - generated_docs:/usr/share/nginx/html/downloads 67 | deploy: 68 | resources: 69 | limits: 70 | cpus: '0.2' 71 | memory: 128M 72 | healthcheck: 73 | test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:80"] 74 | interval: 30s 75 | timeout: 5s 76 | retries: 3 77 | networks: 78 | - app-network 79 | 80 | volumes: 81 | generated_docs: 82 | driver: local 83 | 84 | networks: 85 | app-network: 86 | driver: bridge -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=http://localhost:8001/api/v1 -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=/api/v1 -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | // 添加更宽松的规则,避免开发中的烦恼 17 | 'vue/no-unused-components': 'warn', 18 | 'no-unused-vars': 'warn' 19 | } 20 | } -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 as build-stage 2 | 3 | WORKDIR /app 4 | 5 | # 安装Node.js和npm 6 | RUN apt-get update && \ 7 | apt-get install -y curl && \ 8 | curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \ 9 | apt-get install -y nodejs && \ 10 | apt-get clean && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | # 利用Docker的缓存机制 14 | COPY package*.json ./ 15 | RUN npm install 16 | 17 | # 复制必要的源代码文件 18 | COPY src ./src 19 | COPY vue.config.js ./ 20 | COPY .env.* ./ 21 | COPY .eslintrc.js ./ 22 | 23 | # 构建应用 24 | RUN npm run build 25 | 26 | # 生产环境使用nginx 27 | FROM ubuntu:20.04 as production-stage 28 | 29 | # 安装nginx 30 | RUN apt-get update && \ 31 | apt-get install -y nginx && \ 32 | apt-get clean && \ 33 | rm -rf /var/lib/apt/lists/* 34 | 35 | # 从构建阶段复制构建产物 36 | COPY --from=build-stage /app/dist /var/www/html 37 | 38 | # 创建默认nginx配置 39 | RUN echo 'server { \n\ 40 | listen 80; \n\ 41 | server_name localhost; \n\ 42 | \n\ 43 | location / { \n\ 44 | root /var/www/html; \n\ 45 | index index.html index.htm; \n\ 46 | try_files $uri $uri/ /index.html; \n\ 47 | } \n\ 48 | }' > /etc/nginx/conf.d/default.conf 49 | 50 | EXPOSE 80 51 | 52 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /frontend/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | # 添加CORS支持 6 | add_header 'Access-Control-Allow-Origin' '*' always; 7 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always; 8 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; 9 | 10 | # 静态文件 11 | location / { 12 | root /usr/share/nginx/html; 13 | index index.html index.htm; 14 | try_files $uri $uri/ /index.html; 15 | } 16 | 17 | # 处理OPTIONS请求 18 | if ($request_method = 'OPTIONS') { 19 | add_header 'Access-Control-Allow-Origin' '*' always; 20 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always; 21 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; 22 | add_header 'Access-Control-Max-Age' 1728000; 23 | add_header 'Content-Type' 'text/plain charset=UTF-8'; 24 | add_header 'Content-Length' 0; 25 | return 204; 26 | } 27 | 28 | # 代理API请求到后端 29 | location /api/ { 30 | proxy_pass http://backend:8001/api/; 31 | proxy_http_version 1.1; 32 | proxy_set_header Host $host; 33 | proxy_set_header X-Real-IP $remote_addr; 34 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 35 | proxy_set_header X-Forwarded-Proto $scheme; 36 | 37 | # 禁用缓存 38 | proxy_no_cache 1; 39 | proxy_cache_bypass 1; 40 | } 41 | 42 | # 代理下载和预览请求 43 | location /downloads/ { 44 | proxy_pass http://backend:8001/downloads/; 45 | } 46 | 47 | location /previews/ { 48 | proxy_pass http://backend:8001/previews/; 49 | } 50 | 51 | error_page 500 502 503 504 /50x.html; 52 | location = /50x.html { 53 | root /usr/share/nginx/html; 54 | } 55 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-doc-platform-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.4", 12 | "core-js": "^3.6.5", 13 | "vue": "^2.6.11", 14 | "vue-router": "^3.2.0", 15 | "vuex": "^3.4.0" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "~4.5.0", 19 | "@vue/cli-plugin-eslint": "~4.5.0", 20 | "@vue/cli-plugin-router": "~4.5.0", 21 | "@vue/cli-plugin-vuex": "~4.5.0", 22 | "@vue/cli-service": "~4.5.0", 23 | "babel-eslint": "^10.1.0", 24 | "eslint": "^6.7.2", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "vue-template-compiler": "^2.6.11" 27 | } 28 | } -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/AdvancedDocumentForm.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 190 | 191 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentForm.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 114 | 115 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/ProgressIndicator.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 61 | 62 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | 6 | Vue.config.productionTip = false 7 | 8 | new Vue({ 9 | router, 10 | store, 11 | render: h => h(App) 12 | }).$mount('#app') -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import Home from '../views/Home.vue'; 4 | import Generate from '../views/Generate.vue'; 5 | import Results from '../views/Results.vue'; 6 | import History from '../views/History.vue'; 7 | 8 | Vue.use(VueRouter); 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'Home', 14 | component: Home 15 | }, 16 | { 17 | path: '/generate', 18 | name: 'Generate', 19 | component: Generate 20 | }, 21 | { 22 | path: '/results/:id', 23 | name: 'Results', 24 | component: Results 25 | }, 26 | { 27 | path: '/history', 28 | name: 'History', 29 | component: History 30 | } 31 | ]; 32 | 33 | const router = new VueRouter({ 34 | mode: 'history', 35 | base: process.env.BASE_URL, 36 | routes 37 | }); 38 | 39 | export default router; -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // 根据当前URL选择合适的API路径 4 | const currentPort = window.location.port; 5 | const API_URL = currentPort === '3000' 6 | ? 'http://localhost:8001/api/v1' // 前端在3000端口时直接访问后端 7 | : '/api/v1'; // 其他情况使用相对路径 8 | 9 | console.log('使用API URL:', API_URL, '当前端口:', currentPort); 10 | 11 | const apiClient = axios.create({ 12 | baseURL: API_URL, 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Accept': 'application/json' 16 | } 17 | }); 18 | 19 | // 添加响应拦截器,用于调试 20 | apiClient.interceptors.response.use( 21 | response => { 22 | console.log('API 响应成功:', response); 23 | return response; 24 | }, 25 | error => { 26 | console.error('API Error:', error.response || error.message); 27 | return Promise.reject(error); 28 | } 29 | ); 30 | 31 | // 添加请求拦截器,用于调试 32 | apiClient.interceptors.request.use( 33 | config => { 34 | console.log('API 请求:', config.method.toUpperCase(), config.url, config.data || {}); 35 | return config; 36 | }, 37 | error => { 38 | console.error('请求配置错误:', error); 39 | return Promise.reject(error); 40 | } 41 | ); 42 | 43 | export default { 44 | // 创建新文档 45 | createDocument(documentData) { 46 | return apiClient.post('/documents/', documentData); 47 | }, 48 | 49 | // 创建高级文档 50 | createAdvancedDocument(documentData) { 51 | return apiClient.post('/advanced-documents/', documentData); 52 | }, 53 | 54 | // 获取文档状态 55 | getDocumentStatus(documentId) { 56 | return apiClient.get(`/documents/${documentId}/status`); 57 | }, 58 | 59 | // 获取文档详情 60 | getDocument(documentId) { 61 | return apiClient.get(`/documents/${documentId}`); 62 | }, 63 | 64 | // 获取文档列表 65 | getDocuments() { 66 | return apiClient.get('/documents/'); 67 | } 68 | }; -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import documentsModule from './modules/documents' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | }, 10 | mutations: { 11 | }, 12 | actions: { 13 | }, 14 | modules: { 15 | documents: documentsModule 16 | } 17 | }) -------------------------------------------------------------------------------- /frontend/src/store/modules/documents.js: -------------------------------------------------------------------------------- 1 | import api from '@/services/api' 2 | 3 | export default { 4 | namespaced: true, 5 | 6 | state: { 7 | documents: [], 8 | loading: false, 9 | error: null 10 | }, 11 | 12 | mutations: { 13 | SET_DOCUMENTS(state, documents) { 14 | state.documents = documents 15 | }, 16 | SET_LOADING(state, loading) { 17 | state.loading = loading 18 | }, 19 | SET_ERROR(state, error) { 20 | state.error = error 21 | }, 22 | ADD_DOCUMENT(state, document) { 23 | state.documents.unshift(document) 24 | }, 25 | UPDATE_DOCUMENT(state, updatedDocument) { 26 | const index = state.documents.findIndex(doc => doc.id === updatedDocument.id) 27 | if (index !== -1) { 28 | state.documents.splice(index, 1, updatedDocument) 29 | } 30 | } 31 | }, 32 | 33 | actions: { 34 | async fetchDocuments({ commit }) { 35 | commit('SET_LOADING', true) 36 | try { 37 | const response = await api.getDocuments() 38 | commit('SET_DOCUMENTS', response.data) 39 | commit('SET_ERROR', null) 40 | } catch (error) { 41 | commit('SET_ERROR', '获取文档列表失败') 42 | console.error(error) 43 | } finally { 44 | commit('SET_LOADING', false) 45 | } 46 | }, 47 | 48 | async createDocument({ commit }, documentData) { 49 | try { 50 | const response = await api.createDocument(documentData) 51 | return response.data 52 | } catch (error) { 53 | console.error(error) 54 | throw error 55 | } 56 | }, 57 | 58 | async createAdvancedDocument({ commit }, documentData) { 59 | try { 60 | const response = await api.createAdvancedDocument(documentData) 61 | return response.data 62 | } catch (error) { 63 | console.error(error) 64 | throw error 65 | } 66 | }, 67 | 68 | async getDocument({ commit }, documentId) { 69 | try { 70 | const response = await api.getDocument(documentId) 71 | return response.data 72 | } catch (error) { 73 | console.error(error) 74 | throw error 75 | } 76 | }, 77 | 78 | async getDocumentStatus({ commit }, documentId) { 79 | try { 80 | const response = await api.getDocumentStatus(documentId) 81 | return response.data 82 | } catch (error) { 83 | console.error(error) 84 | throw error 85 | } 86 | } 87 | }, 88 | 89 | getters: { 90 | getDocumentById: (state) => (id) => { 91 | return state.documents.find(doc => doc.id === id) 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /frontend/src/views/Generate.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/views/History.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 94 | 95 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 70 | 71 | -------------------------------------------------------------------------------- /frontend/src/views/Results.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 256 | 257 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false 3 | } -------------------------------------------------------------------------------- /images/advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/advanced.png -------------------------------------------------------------------------------- /images/document_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/document_form.png -------------------------------------------------------------------------------- /images/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/homepage.png -------------------------------------------------------------------------------- /images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/result.png -------------------------------------------------------------------------------- /images/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/simple.png -------------------------------------------------------------------------------- /images/强化学习_document.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/强化学习_document.docx -------------------------------------------------------------------------------- /images/量化投资:寻找阿尔法因子_presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chongliujia/AI_doc_platform/afb5b10fbc21236ba4ca0bb9f68ab00e0108eee1/images/量化投资:寻找阿尔法因子_presentation.pptx -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | # 安装nginx 4 | RUN apt-get update && \ 5 | apt-get install -y nginx && \ 6 | apt-get clean && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | # 复制nginx配置 10 | COPY nginx.conf /etc/nginx/nginx.conf 11 | 12 | # 创建下载目录 13 | RUN mkdir -p /usr/share/nginx/html/downloads 14 | 15 | # 设置适当的权限 16 | RUN chown -R www-data:www-data /usr/share/nginx/html/ 17 | 18 | EXPOSE 80 19 | 20 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | #tcp_nopush on; 23 | 24 | keepalive_timeout 65; 25 | 26 | #gzip on; 27 | 28 | # 简化的CORS处理 - 使用map来检测OPTIONS请求 29 | map $request_method $cors_method { 30 | OPTIONS 'true'; 31 | default ''; 32 | } 33 | 34 | server { 35 | listen 80; 36 | server_name localhost; 37 | 38 | # 简单的CORS响应头 39 | add_header Access-Control-Allow-Origin '*'; 40 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE'; 41 | add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; 42 | 43 | # 前端静态文件 44 | location / { 45 | root /usr/share/nginx/html; 46 | index index.html index.htm; 47 | try_files $uri $uri/ /index.html; 48 | } 49 | 50 | # 处理OPTIONS请求的简化方法 51 | location /api/ { 52 | if ($cors_method) { 53 | add_header Content-Type 'text/plain charset=UTF-8'; 54 | add_header Content-Length 0; 55 | add_header Access-Control-Allow-Origin '*'; 56 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE'; 57 | add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; 58 | add_header Access-Control-Max-Age 1728000; 59 | return 204; 60 | } 61 | proxy_pass http://backend:8001/api/; 62 | proxy_http_version 1.1; 63 | proxy_set_header Host $host; 64 | proxy_set_header X-Real-IP $remote_addr; 65 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 66 | } 67 | 68 | location /downloads/ { 69 | proxy_pass http://backend:8001/downloads/; 70 | proxy_http_version 1.1; 71 | proxy_set_header Host $host; 72 | proxy_set_header X-Real-IP $remote_addr; 73 | } 74 | 75 | location /previews/ { 76 | proxy_pass http://backend:8001/previews/; 77 | proxy_http_version 1.1; 78 | proxy_set_header Host $host; 79 | proxy_set_header X-Real-IP $remote_addr; 80 | } 81 | 82 | error_page 500 502 503 504 /50x.html; 83 | location = /50x.html { 84 | root /usr/share/nginx/html; 85 | } 86 | } 87 | } --------------------------------------------------------------------------------