init
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user