diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..ab1f416
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/doc-forge.iml b/.idea/doc-forge.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/doc-forge.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..53fab56
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..18b9e70
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..ed1e887
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,58 @@
+# AGENTS.md — doc-forge
+
+你正在参与一个外包项目,客户需要一套完整的 AI 文档模板生成系统。
+
+## 你的角色
+全栈开发 AI 助手,负责生成 Vue 3 + Ant Design Vue 前端代码和 Python FastAPI 后端代码。
+
+## 基础设施
+
+### MySQL
+- 数据库名:`doc_forge`,字符集 `utf8mb4`
+- 异步驱动:`asyncmy`
+- 连接池:pool_size=10, max_overflow=20
+- 本地开发:`docker-compose up mysql`
+- DDL 见 `init.sql`
+
+### MinIO(对象存储)
+- 三个 bucket:`doc-forge-templates`(模板)/ `doc-forge-uploads`(参考文件)/ `doc-forge-outputs`(导出文档)
+- 文件路径规则:`{bucket}/{YYYYMMDD}/{uuid}.{ext}`
+- 预签名 URL 用于前端下载,过期 1 小时
+- 本地开发:`docker-compose up minio`,Console http://localhost:9001
+- SDK:`from minio import Minio`,客户端在 `services/minio_client.py`
+
+### Docker
+- `docker-compose up -d mysql minio` 启动开发依赖
+- `docker-compose up backend web` 启动全栈
+
+## 通讯协议
+- 所有 API 响应格式:`{ code: 0, data: {...}, message: "ok" }`
+- 错误响应:`{ code: -1, message: "错误描述" }`
+- 分页响应:`{ code: 0, data: { items: [], total: N, page: 1, page_size: 20 } }`
+
+## 必须遵守的规则
+
+1. **AI 输出格式** — AI 必须返回 JSON,不得返回纯文本。前端解析 `content` 数组,按 type 分段渲染。
+2. **Word 导出** — 严禁重新生成文档。从 MinIO 拉取原始模板,只替换对应位置的文本节点。
+3. **段落边界** — 只认 Word 标题样式(Heading)。不要尝试用正则或关键词判断段落。
+4. **API Key 安全** — 所有 API Key 用 `cryptography.fernet.Fernet` 加密存储,前端只展示脱敏字符串。
+5. **并发控制** — 段落生成使用 `asyncio.gather` + `Semaphore`,单文档最大并发 5。
+6. **文件存储** — 所有用户文件存 MinIO,后端本地只做临时缓存。
+7. docs/规范与约束/开发规范.md
+8. docs/需求与设计/02-模板格式规范.md
+
+## 段落配置字段
+每个 paragraph 包含:
+- `edit_mode`: 'manual' | 'ai'
+- `model_id`: int | null(null 表示使用系统默认模型)
+- `need_prompt`: boolean + `prompt_text`: string
+- `need_file`: boolean + `file_note`: string(备注提示上传什么文件)
+- `output_format`: 'text' | 'table' | 'mixed' | 'chart'
+
+## 容易踩的坑
+- python-docx 中文字体名在 `run.fonts.eastAsia`,不是 `run.fonts.name`
+- Ant Design Vue 4.x 的 modal 使用 `v-model:open`,不是 `v-model:visible`
+- SSE 事件流要用 `sse-starlette` 的 `EventSourceResponse`
+- asyncio 中不能混用同步的 openpyxl,Excel 解析放在线程池执行 (`run_in_executor`)
+- MinIO SDK 是同步的,用 `run_in_executor` 包装,不要直接 in asyncio
+- asyncmy 连接 MySQL 需要 `charset=utf8mb4`,不然中文会乱码
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..d1c8a9f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,67 @@
+# doc-forge — AI 文档模板生成系统
+
+## 项目概述
+上传 Word 模板 → AI 自动解析段落(按标题样式切割)→ 用户标注段落配置 → 上传参考文件 → AI 多段落并行生成 → 预览编辑 → 导出 Word(保留原始样式)。
+
+## 技术栈
+| 层 | 技术 | 说明 |
+|----|------|------|
+| 前端 | Vue 3.4 + TypeScript + Ant Design Vue 4.x + Pinia + Vite 5 | web/ |
+| 后端 | Python 3.11+ + FastAPI + SQLAlchemy 2.0 async | backend/app/ |
+| 数据库 | MySQL 8.0(asyncmy 驱动) | docker-compose mysql |
+| 对象存储 | MinIO | 存模板文件、参考文件、生成文档 |
+| Word 处理 | python-docx | 解析/导出 |
+| Excel 处理 | openpyxl | 解析参考文件 |
+
+## 目录结构
+```
+doc-forge/
+├── web/ 前端
+│ └── src/
+│ ├── views/ TemplateList, TemplateEditor, ModelManage, GeneratePage, HistoryPage, PreviewEdit
+│ ├── components/ ParagraphList, ParagraphConfig, DocPreview, FileUploader, ModelModal, TestModal
+│ ├── api/ Axios(template, model, generate)
+│ ├── stores/ Pinia(template, model, document)
+│ └── router/ 6 条路由
+├── backend/
+│ ├── app/
+│ │ ├── models/ ORM(template, paragraph, ai_model, document, generation_log)
+│ │ ├── routers/ API(templates, models, generate, export)
+│ │ ├── schemas/ Pydantic 校验
+│ │ ├── services/ 业务逻辑(parser, ai_service, generator, exporter, minio_client)
+│ │ └── main.py
+│ ├── config.py 配置(MySQL + MinIO + AI)
+│ └── database.py 异步引擎
+├── docs/ 项目文档
+├── docker-compose.yml MySQL + MinIO + backend + web
+├── init.sql 数据库建表 DDL
+├── CLAUDE.md
+└── AGENTS.md
+```
+
+## 关键约定
+
+### 段落解析规则
+- 段落边界由 Word 标题样式(Heading 1~6)确定
+- 标题与下一标题之间的正文、表格归属到该标题段落
+- 表格独立存储为 `is_table=True`,归属于前一个标题
+
+### AI 输出格式
+AI 必须返回结构化 JSON:
+```json
+{"content": [{"type": "text", "text": "..."}, {"type": "table", "headers": [], "rows": []}]}
+```
+
+### 文件存储
+- 所有文件存 MinIO,不存本地磁盘
+- bucket 分三类:templates / uploads / outputs
+- MinIO 开发环境在 docker-compose 中启动
+
+### AI 模型调用
+- OpenAI 格式:GPT-4o, DeepSeek-V3, 通义千问
+- Anthropic 格式:Claude 3.5 Sonnet
+- 超时 60s,最多重试 3 次,单文档最大并发 5
+
+### Word 导出
+- 从 MinIO 拉取原始模板 → 在内存中修改 → 上传回 MinIO
+- 样式完全保留(字体/颜色/行距/页边距/页眉页脚)
diff --git a/README.md b/README.md
index 887871c..1b9dd5c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,80 @@
# doc-forge
-《文档锻造》
\ No newline at end of file
+上传 Word 模板 → AI 逐段落生成 → 导出保留原始样式的 Word 文档。
+
+## 技术栈
+
+- **前端**: Vue 3.4 + Vite 5 + TypeScript + Ant Design Vue 4.x + Pinia + Axios
+- **后端**: Python 3.11+ + FastAPI + SQLAlchemy 2.0 async
+- **数据库**: MySQL 8.0(asyncmy 驱动)
+- **对象存储**: MinIO(模板/参考文件/导出文档)
+- **文档处理**: python-docx、openpyxl
+
+## 快速开始
+
+```bash
+# 1. 启动基础设施(MySQL + MinIO)
+docker-compose up -d mysql minio
+
+# 2. 启动后端
+cd backend
+pip install -r requirements.txt
+python main.py
+# → http://localhost:8000/docs Swagger
+
+# 3. 启动前端
+cd web
+pnpm install
+pnpm dev
+# → http://localhost:5173
+```
+
+## 目录结构
+
+```
+doc-forge/
+├── web/ Vue 3 前端
+│ └── src/
+│ ├── views/ 6 个页面
+│ ├── components/ 6 个通用组件
+│ ├── api/ Axios 请求层
+│ ├── stores/ Pinia 状态管理
+│ └── router/ 路由配置
+├── backend/ Python FastAPI 后端
+│ ├── app/models/ ORM 数据模型
+│ ├── app/routers/ API 路由
+│ ├── app/schemas/ Pydantic 校验
+│ ├── app/services/ 业务逻辑层
+│ ├── config.py 配置
+│ └── database.py 异步数据库引擎
+├── docker-compose.yml MySQL + MinIO + 后端 + 前端
+├── init.sql 数据库建表 DDL
+└── docs/ 项目文档
+```
+
+## 核心流程
+
+```
+上传模板 → 解析段落 → 标注配置 → 保存模板
+ → 执行生成(上传文件 → AI 并行生成 → SSE 进度)
+ → 预览编辑 → 导出 Word(保留原始样式)
+```
+
+## 关键设计
+
+| 决策 | 选择 | 原因 |
+|------|------|------|
+| 段落边界 | Word 标题样式 Heading 1~6 | 稳定可靠,用户学习成本低 |
+| AI 输出 | 结构化 JSON | 支持文字+表格混合,后端可控解析 |
+| 导出策略 | 基于原模板替换内容 | 样式零损失,不限模板格式 |
+| 文件存储 | MinIO 对象存储 | 可扩展,不占用本地磁盘 |
+| 数据库 | MySQL 8.0 | 生产级可靠性 |
+| 生成方式 | 多段落并行 asyncio.gather | 大幅缩短等待时间 |
+
+## 环境要求
+
+- Python 3.11+
+- Node.js 18+
+- pnpm 8+
+- Docker + docker-compose(MySQL + MinIO)
+- LibreOffice(可选,用于 PDF 导出)
diff --git a/backend/config.py b/backend/config.py
new file mode 100644
index 0000000..b111198
--- /dev/null
+++ b/backend/config.py
@@ -0,0 +1,59 @@
+from pydantic_settings import BaseSettings
+from pathlib import Path
+import os
+
+
+class Settings(BaseSettings):
+ # 应用
+ APP_NAME: str = "AI 文档模板生成系统"
+ APP_VERSION: str = "1.0.0"
+ DEBUG: bool = True
+
+ # === 数据库 MySQL ===
+ DB_HOST: str = "localhost"
+ DB_PORT: int = 3306
+ DB_USER: str = "docforge"
+ DB_PASSWORD: str = "docforge123"
+ DB_NAME: str = "doc_forge"
+ @property
+ def DATABASE_URL(self) -> str:
+ return f"mysql+asyncmy://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset=utf8mb4"
+
+ # === MinIO 文件存储 ===
+ MINIO_ENDPOINT: str = "localhost:9000"
+ MINIO_ACCESS_KEY: str = "docforge"
+ MINIO_SECRET_KEY: str = "docforge123"
+ MINIO_BUCKET_TEMPLATES: str = "doc-forge-templates"
+ MINIO_BUCKET_UPLOADS: str = "doc-forge-uploads"
+ MINIO_BUCKET_OUTPUTS: str = "doc-forge-outputs"
+ MINIO_USE_SSL: bool = False
+
+ # 本地缓存目录(MinIO 文件的本地临时缓存)
+ LOCAL_CACHE_DIR: str = "local_cache"
+
+ # 文件上传限制
+ MAX_UPLOAD_SIZE: int = 50 * 1024 * 1024 # 50MB
+ ALLOWED_EXTENSIONS: list = [".docx", ".xlsx", ".xls", ".csv", ".pdf", ".txt", ".md"]
+
+ # 加密(用于 API Key 加密)
+ ENCRYPTION_KEY: str = "change-this-to-a-32-byte-key-in-production!!"
+
+ # AI 模型默认配置
+ AI_REQUEST_TIMEOUT: int = 60
+ AI_MAX_RETRIES: int = 3
+ AI_MAX_CONCURRENT: int = 5
+ AI_GLOBAL_CONCURRENT: int = 10
+
+ # 服务端口
+ HOST: str = "0.0.0.0"
+ PORT: int = 8000
+
+ class Config:
+ env_file = ".env"
+ env_file_encoding = "utf-8"
+
+
+settings = Settings()
+
+# 创建本地缓存目录
+os.makedirs(settings.LOCAL_CACHE_DIR, exist_ok=True)
diff --git a/backend/database.py b/backend/database.py
new file mode 100644
index 0000000..364d77c
--- /dev/null
+++ b/backend/database.py
@@ -0,0 +1,28 @@
+from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
+from sqlalchemy.orm import DeclarativeBase
+from config import settings
+
+engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG, pool_size=10, max_overflow=20)
+async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
+
+
+class Base(DeclarativeBase):
+ pass
+
+
+async def get_db():
+ async with async_session() as session:
+ try:
+ yield session
+ finally:
+ await session.close()
+
+
+async def init_db():
+ from models.template import Template
+ from models.paragraph import Paragraph
+ from models.ai_model import AiModel
+ from models.document import Document
+ from models.generation_log import GenerationLog
+ async with engine.begin() as conn:
+ await conn.run_sync(Base.metadata.create_all)
diff --git a/backend/main.py b/backend/main.py
new file mode 100644
index 0000000..a8e9186
--- /dev/null
+++ b/backend/main.py
@@ -0,0 +1,46 @@
+import uvicorn
+from contextlib import asynccontextmanager
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from database import init_db, engine
+from config import settings
+from routers import templates, models, generate, export
+from services.minio_client import init_buckets
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ await init_db()
+ await init_buckets() # 初始化 MinIO 存储桶
+ yield
+ await engine.dispose()
+
+
+app = FastAPI(title=settings.APP_NAME, version=settings.APP_VERSION, lifespan=lifespan)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.include_router(templates.router, prefix="/api/v1/templates", tags=["模板管理"])
+app.include_router(models.router, prefix="/api/v1/models", tags=["模型管理"])
+app.include_router(generate.router, prefix="/api/v1/generate", tags=["生成管理"])
+app.include_router(export.router, prefix="/api/v1/export", tags=["导出管理"])
+
+
+@app.get("/")
+async def root():
+ return {"message": "AI 文档模板生成系统 API", "version": settings.APP_VERSION}
+
+
+@app.get("/health")
+async def health():
+ return {"status": "ok"}
+
+
+if __name__ == "__main__":
+ uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)
diff --git a/backend/models/__init__.py b/backend/models/__init__.py
new file mode 100644
index 0000000..d79adf4
--- /dev/null
+++ b/backend/models/__init__.py
@@ -0,0 +1,5 @@
+from models.template import Template
+from models.paragraph import Paragraph
+from models.ai_model import AiModel
+from models.document import Document
+from models.generation_log import GenerationLog
diff --git a/backend/models/ai_model.py b/backend/models/ai_model.py
new file mode 100644
index 0000000..bd8bf0d
--- /dev/null
+++ b/backend/models/ai_model.py
@@ -0,0 +1,14 @@
+from sqlalchemy import Column, Integer, String, Text, DateTime, func
+from database import Base
+
+class AiModel(Base):
+ __tablename__ = "ai_models"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String(255), nullable=False, comment="模型名称")
+ provider = Column(String(100), default="", comment="供应厂商")
+ api_format = Column(String(20), default="openai", comment="anthropic/openai")
+ api_endpoint = Column(String(500), default="", comment="API接口地址")
+ api_key_encrypted = Column(Text, default="", comment="加密后的API Key")
+ status = Column(String(20), default="enabled", comment="enabled/disabled")
+ created_at = Column(DateTime, server_default=func.now())
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
diff --git a/backend/models/document.py b/backend/models/document.py
new file mode 100644
index 0000000..5d83381
--- /dev/null
+++ b/backend/models/document.py
@@ -0,0 +1,15 @@
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, func
+from database import Base
+
+class Document(Base):
+ __tablename__ = "documents"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ template_id = Column(Integer, ForeignKey("templates.id"), nullable=False)
+ name = Column(String(255), default="", comment="文档名称")
+ para_count_done = Column(Integer, default=0, comment="已完成段落数")
+ para_count_total = Column(Integer, default=0, comment="总段落数")
+ status = Column(String(20), default="pending", comment="pending/generating/completed/failed/cancelled")
+ file_path = Column(String(500), default="", comment="生成的文件路径")
+ error = Column(Text, default="", comment="错误信息")
+ created_at = Column(DateTime, server_default=func.now())
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
diff --git a/backend/models/generation_log.py b/backend/models/generation_log.py
new file mode 100644
index 0000000..c4f2848
--- /dev/null
+++ b/backend/models/generation_log.py
@@ -0,0 +1,14 @@
+from sqlalchemy import Column, Integer, String, Text, DateTime, Float, ForeignKey, func
+from database import Base
+
+class GenerationLog(Base):
+ __tablename__ = "generation_logs"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ document_id = Column(Integer, ForeignKey("documents.id"), nullable=False)
+ paragraph_id = Column(Integer, ForeignKey("paragraphs.id"), nullable=False)
+ model_id = Column(Integer, ForeignKey("ai_models.id"), nullable=True)
+ status = Column(String(20), default="pending", comment="pending/generating/success/failed")
+ content = Column(Text, default="", comment="生成的内容")
+ duration = Column(Float, default=0, comment="耗时秒数")
+ error_msg = Column(Text, default="", comment="错误信息")
+ created_at = Column(DateTime, server_default=func.now())
diff --git a/backend/models/paragraph.py b/backend/models/paragraph.py
new file mode 100644
index 0000000..2c98aec
--- /dev/null
+++ b/backend/models/paragraph.py
@@ -0,0 +1,22 @@
+from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, func
+from database import Base
+
+class Paragraph(Base):
+ __tablename__ = "paragraphs"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ template_id = Column(Integer, ForeignKey("templates.id"), nullable=False)
+ sort_index = Column(Integer, default=0, comment="排序")
+ title = Column(String(500), default="", comment="段落标题")
+ content = Column(Text, default="", comment="正文内容/上下文")
+ style_json = Column(Text, default="{}", comment="段落样式定义JSON")
+ is_table = Column(Boolean, default=False, comment="是否为表格")
+ table_json = Column(Text, default="{}", comment="表格结构JSON")
+ edit_mode = Column(String(20), default="ai", comment="manual/ai")
+ model_id = Column(Integer, ForeignKey("ai_models.id"), nullable=True, comment="指定模型")
+ need_prompt = Column(Boolean, default=True, comment="是否需要提示词")
+ prompt_text = Column(Text, default="", comment="预设提示词")
+ need_file = Column(Boolean, default=False, comment="是否需要上传参考文件")
+ file_note = Column(Text, default="", comment="备注说明(传什么文件)")
+ output_format = Column(String(20), default="text", comment="text/table/mixed/chart")
+ created_at = Column(DateTime, server_default=func.now())
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
diff --git a/backend/models/template.py b/backend/models/template.py
new file mode 100644
index 0000000..bdbb591
--- /dev/null
+++ b/backend/models/template.py
@@ -0,0 +1,13 @@
+from sqlalchemy import Column, Integer, String, Text, DateTime, func
+from database import Base
+
+class Template(Base):
+ __tablename__ = "templates"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String(255), nullable=False, comment="模板名称")
+ description = Column(Text, default="", comment="描述")
+ file_path = Column(String(500), nullable=False, comment="原始模板文件路径")
+ paragraph_count = Column(Integer, default=0, comment="段落数")
+ status = Column(String(20), default="draft", comment="draft/ready")
+ created_at = Column(DateTime, server_default=func.now())
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..066f63b
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,17 @@
+fastapi>=0.110.0
+uvicorn[standard]>=0.29.0
+sqlalchemy>=2.0.25
+asyncmy>=0.2.9 # MySQL async driver
+aiomysql>=0.2.0 # MySQL async fallback
+cryptography>=42.0.0
+python-docx>=1.1.0
+openpyxl>=3.1.0
+pandas>=2.1.0
+httpx>=0.27.0
+pydantic>=2.5.0
+pydantic-settings>=2.1.0
+python-multipart>=0.0.6
+aiofiles>=23.2.0
+sse-starlette>=2.0.0
+minio>=7.2.0 # MinIO 对象存储 SDK
+alembic>=1.13.0
diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/routers/export.py b/backend/routers/export.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/routers/generate.py b/backend/routers/generate.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/routers/models.py b/backend/routers/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/routers/templates.py b/backend/routers/templates.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/schemas/schemas.py b/backend/schemas/schemas.py
new file mode 100644
index 0000000..bd07cde
--- /dev/null
+++ b/backend/schemas/schemas.py
@@ -0,0 +1,86 @@
+from pydantic import BaseModel, Field
+from typing import Optional, Any
+from datetime import datetime
+
+class Response(BaseModel):
+ code: int = 0
+ data: Any = None
+ message: str = "ok"
+
+class PageData(BaseModel):
+ items: list = []
+ total: int = 0
+ page: int = 1
+ page_size: int = 20
+
+# 模板
+class TemplateCreate(BaseModel):
+ name: str
+ description: str = ""
+
+class TemplateOut(BaseModel):
+ id: int
+ name: str
+ description: str = ""
+ file_path: str = ""
+ paragraph_count: int = 0
+ status: str = "draft"
+ created_at: Optional[datetime] = None
+ updated_at: Optional[datetime] = None
+
+class ParagraphConfig(BaseModel):
+ id: int = 0
+ sort_index: int = 0
+ title: str = ""
+ edit_mode: str = "ai"
+ model_id: Optional[int] = None
+ need_prompt: bool = True
+ prompt_text: str = ""
+ need_file: bool = False
+ file_note: str = ""
+ output_format: str = "text"
+
+class TemplateSave(BaseModel):
+ paragraphs: list[ParagraphConfig] = []
+
+# 模型
+class AiModelCreate(BaseModel):
+ name: str
+ provider: str = ""
+ api_format: str = "openai"
+ api_endpoint: str = ""
+ api_key: str = ""
+ status: str = "enabled"
+
+class AiModelOut(BaseModel):
+ id: int
+ name: str
+ provider: str = ""
+ api_format: str = "openai"
+ api_endpoint: str = ""
+ api_key_preview: str = ""
+ status: str = "enabled"
+ created_at: Optional[datetime] = None
+
+# 生成
+class GenerateTestRequest(BaseModel):
+ paragraph_id: int
+ template_id: int
+ prompt_text: str = ""
+ model_id: int = 0
+ file_paths: list[str] = []
+
+class GenerateFullRequest(BaseModel):
+ template_id: int
+ file_map: dict[str, str] = {} # paragraph_id -> file_path
+
+class DocumentOut(BaseModel):
+ id: int
+ template_id: int
+ name: str
+ para_count_done: int = 0
+ para_count_total: int = 0
+ status: str = "pending"
+ file_path: str = ""
+ error: str = ""
+ created_at: Optional[datetime] = None
diff --git a/backend/services/__init__.py b/backend/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..fbb89ef
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,80 @@
+version: "3.8"
+
+services:
+ # === MySQL ===
+ mysql:
+ image: mysql:8.0
+ container_name: doc-forge-mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: root123
+ MYSQL_DATABASE: doc_forge
+ MYSQL_USER: docforge
+ MYSQL_PASSWORD: docforge123
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+ command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
+
+ # === MinIO(对象存储)===
+ minio:
+ image: minio/minio:latest
+ container_name: doc-forge-minio
+ restart: unless-stopped
+ command: server /data --console-address ":9001"
+ environment:
+ MINIO_ROOT_USER: docforge
+ MINIO_ROOT_PASSWORD: docforge123
+ ports:
+ - "9000:9000" # API
+ - "9001:9001" # Console
+ volumes:
+ - minio_data:/data
+ healthcheck:
+ test: ["CMD", "mc", "ready", "local"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+
+ # === 后端 ===
+ backend:
+ build: ./backend
+ container_name: doc-forge-backend
+ restart: unless-stopped
+ depends_on:
+ mysql:
+ condition: service_started
+ minio:
+ condition: service_healthy
+ environment:
+ DB_HOST: mysql
+ DB_PORT: 3306
+ DB_USER: docforge
+ DB_PASSWORD: docforge123
+ DB_NAME: doc_forge
+ MINIO_ENDPOINT: minio:9000
+ MINIO_ACCESS_KEY: docforge
+ MINIO_SECRET_KEY: docforge123
+ ENCRYPTION_KEY: "${ENCRYPTION_KEY}"
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./backend:/app
+ - local_cache:/app/local_cache
+
+ # === 前端 ===
+ web:
+ build: ./web
+ container_name: doc-forge-web
+ restart: unless-stopped
+ depends_on:
+ - backend
+ ports:
+ - "5173:80"
+
+volumes:
+ mysql_data:
+ minio_data:
+ local_cache:
diff --git a/docs/提示词库/提示词模板.md b/docs/提示词库/提示词模板.md
new file mode 100644
index 0000000..b586ce1
--- /dev/null
+++ b/docs/提示词库/提示词模板.md
@@ -0,0 +1,158 @@
+# AI 提示词库
+
+本文档包含所有 AI 调用时使用的提示词模板。提示词分为系统级和段落级两层。
+
+## 一、系统提示词
+
+### 1.1 默认系统提示词(通用)
+
+```
+你是一个专业的企业文档撰写助手。你的任务是按照给定的段落标题和参考内容,
+生成符合中文正式报告风格的段落内容。
+
+要求:
+1. 语言正式、客观、严谨,使用第三人称
+2. 逻辑清晰,层次分明
+3. 数据准确,引用上传文件中的实际数据
+4. 字数控制在 300-800 字之间
+5. 不要输出标题本身,只输出段落正文内容
+6. 如果正文需要分点描述,使用 1. 2. 3. 编号,不要使用无序列表
+
+输出格式必须为 JSON:
+{
+ "content": [
+ {"type": "text", "text": "正文内容..."}
+ ]
+}
+
+如果需要输出表格,使用:
+{
+ "content": [
+ {"type": "text", "text": "表格说明文字"},
+ {"type": "table", "headers": ["列1","列2","列3"], "rows": [["数据1","数据2","数据3"]]}
+ ]
+}
+```
+
+### 1.2 表格生成专用提示词
+
+```
+请根据上传的数据文件,生成以下表格内容:
+段落标题:{title}
+
+要求:
+1. 表格列名清晰,数据准确
+2. 只输出表格内容,不要文字说明
+3. 如果有多组数据,优先合并到一张表中
+
+输出格式:
+{
+ "content": [
+ {"type": "table", "headers": ["列名1","列名2","..."], "rows": [["值1","值2","..."]]}
+ ]
+}
+```
+
+### 1.3 报告摘要专用提示词
+
+```
+请根据以下参考内容,生成一段简洁的摘要。
+
+要求:
+1. 概括核心要点,不超过 200 字
+2. 突出关键数据和结论
+3. 使用总分结构
+
+输出格式:
+{
+ "content": [
+ {"type": "text", "text": "摘要内容..."}
+ ]
+}
+```
+
+## 二、段落预设提示词
+
+### 2.1 经营指标分析
+```
+请根据上传的财务数据,生成"{title}"章节内容。
+要求包括以下方面:
+1. 各核心指标的完成值
+2. 与上期/同期的同比变化
+3. 变化原因分析
+4. 存在的主要风险点
+
+参考上下文:
+{context}
+```
+
+### 2.2 成本费用分析
+```
+请根据上传的数据,生成成本费用分析内容。
+要求包括:
+1. 各项成本的构成及占比
+2. 同比变化情况及原因
+3. 成本管控措施及成效
+
+参考数据:
+{context}
+```
+
+### 2.3 问题总结
+```
+请基于以下数据和背景,分析当前存在的主要问题和风险。
+要求:
+1. 问题描述要具体,有数据支撑
+2. 分析问题产生的原因
+3. 指出风险等级和影响范围
+
+参考内容:
+{context}
+```
+
+### 2.4 工作措施
+```
+请针对上述问题,生成下一步工作措施。
+要求:
+1. 措施具体可执行
+2. 明确责任主体
+3. 设定完成时限或目标值
+4. 措施之间逻辑递进
+
+参考内容:
+{context}
+```
+
+## 三、提示词拼接规则
+
+### 3.1 最终 prompt 构成
+```
+[系统提示词]
+---
+段落标题:{paragraph.title}
+编辑方式:{paragraph.edit_mode}
+输出格式:{paragraph.output_format}
+---
+{paragraph.prompt_text}
+---
+参考文件摘要:
+{file_summary}
+---
+参考上下文:
+{paragraph.content}
+```
+
+### 3.2 参考文件摘要生成规则
+```
+读取上传的 Excel 文件:
+1. 提取列名 + 前 10 行数据作为样本
+2. 统计数值列的和/均值/最大最小值
+3. 生成文本摘要
+
+Excel 摘要示例:
+"文件:财务数据报表.xlsx
+包含 3 个工作表:
+- Sheet1(使用中):列 [月份, 营业收入, 利润总额, 净利润],共 12 行数据
+ 营业收入合计:148.2 亿元,月均 12.35 亿元
+ 利润总额合计:15.0 亿元,月均 1.25 亿元
+```
diff --git a/docs/规范与约束/开发规范.md b/docs/规范与约束/开发规范.md
new file mode 100644
index 0000000..ec86588
--- /dev/null
+++ b/docs/规范与约束/开发规范.md
@@ -0,0 +1,105 @@
+# AI 文档模板生成系统 · 开发规范
+
+## 一、代码规范
+
+### 1.1 Python 后端
+- Python 3.11+,使用类型注解
+- 文件命名:snake_case.py
+- 类命名:PascalCase
+- 函数/变量:snake_case
+- 数据库表:小写复数(templates, paragraphs)
+- 异步优先:async/await 贯穿全栈
+
+### 1.2 TypeScript 前端
+- TypeScript 5.x,strict 模式
+- 文件命名:PascalCase.vue(组件),camelCase.ts(工具/API)
+- 组件命名:多单词 PascalCase
+- 变量/函数:camelCase
+- 接口命名:I 开头或 PascalCase
+- 使用 `