This commit is contained in:
zwt13703
2026-07-02 14:48:05 +08:00
parent a1a314cf99
commit e558733f05
64 changed files with 3258 additions and 1 deletions
+59
View File
@@ -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)
+28
View File
@@ -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)
+46
View File
@@ -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)
+5
View File
@@ -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
+14
View File
@@ -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())
+15
View File
@@ -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())
+14
View File
@@ -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())
+22
View File
@@ -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())
+13
View File
@@ -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())
+17
View File
@@ -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
View File
View File
View File
View File
View File
View File
+86
View File
@@ -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
View File