补齐模型管理与基础生成预览链路
This commit is contained in:
@@ -1,3 +1,20 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{document_id}/docx")
|
||||
async def export_docx(document_id: int):
|
||||
return PlainTextResponse(
|
||||
f"文档 {document_id} 的 Word 导出功能正在开发中,当前版本请先使用预览页查看结果。",
|
||||
media_type="text/plain; charset=utf-8",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{document_id}/pdf")
|
||||
async def export_pdf(document_id: int):
|
||||
return PlainTextResponse(
|
||||
f"文档 {document_id} 的 PDF 导出功能正在开发中,当前版本请先使用预览页查看结果。",
|
||||
media_type="text/plain; charset=utf-8",
|
||||
)
|
||||
|
||||
+216
-1
@@ -1,3 +1,218 @@
|
||||
from fastapi import APIRouter
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import get_db
|
||||
from models.document import Document
|
||||
from models.generation_log import GenerationLog
|
||||
from models.paragraph import Paragraph
|
||||
from models.template import Template
|
||||
from schemas.schemas import GenerateFullRequest, GenerateTestRequest, Response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _serialize_document(document: Document) -> dict:
|
||||
return {
|
||||
"id": document.id,
|
||||
"template_id": document.template_id,
|
||||
"name": document.name,
|
||||
"para_count_done": document.para_count_done,
|
||||
"para_count_total": document.para_count_total,
|
||||
"status": document.status,
|
||||
"file_path": document.file_path,
|
||||
"error": document.error,
|
||||
"created_at": document.created_at,
|
||||
"updated_at": document.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def _build_mock_content(paragraph: Paragraph) -> dict:
|
||||
if paragraph.output_format == "table":
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "table",
|
||||
"title": paragraph.title,
|
||||
"headers": ["字段", "内容"],
|
||||
"rows": [
|
||||
["段落标题", paragraph.title],
|
||||
["生成说明", paragraph.prompt_text or "根据模板内容生成"],
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
blocks = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"这是“{paragraph.title}”的示例生成内容,可用于前端联调与流程验证。"
|
||||
}
|
||||
]
|
||||
if paragraph.content:
|
||||
blocks.append({"type": "text", "text": f"模板上下文:{paragraph.content[:200]}"})
|
||||
if paragraph.need_prompt and paragraph.prompt_text:
|
||||
blocks.append({"type": "text", "text": f"预设提示词:{paragraph.prompt_text[:200]}"})
|
||||
return {"content": blocks}
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
async def generate_test(body: GenerateTestRequest, db: AsyncSession = Depends(get_db)):
|
||||
paragraph = await db.get(Paragraph, body.paragraph_id)
|
||||
if paragraph is None or paragraph.template_id != body.template_id:
|
||||
raise HTTPException(status_code=404, detail="段落不存在")
|
||||
|
||||
content = _build_mock_content(paragraph)
|
||||
return Response(
|
||||
data={
|
||||
"paragraph_id": paragraph.id,
|
||||
"content": content,
|
||||
"message": "当前返回本地模拟生成结果,便于前端联调。",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/full")
|
||||
async def generate_full(body: GenerateFullRequest, db: AsyncSession = Depends(get_db)):
|
||||
template = await db.get(Template, body.template_id)
|
||||
if template is None:
|
||||
raise HTTPException(status_code=404, detail="模板不存在")
|
||||
|
||||
result = await db.execute(
|
||||
select(Paragraph)
|
||||
.where(Paragraph.template_id == body.template_id)
|
||||
.order_by(Paragraph.sort_index.asc(), Paragraph.id.asc())
|
||||
)
|
||||
paragraphs = result.scalars().all()
|
||||
if not paragraphs:
|
||||
raise HTTPException(status_code=400, detail="模板下暂无可生成段落")
|
||||
|
||||
document = Document(
|
||||
template_id=template.id,
|
||||
name=f"{template.name}-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
para_count_done=0,
|
||||
para_count_total=len(paragraphs),
|
||||
status="generating",
|
||||
file_path="",
|
||||
error="",
|
||||
)
|
||||
db.add(document)
|
||||
await db.flush()
|
||||
|
||||
done_count = 0
|
||||
for paragraph in paragraphs:
|
||||
if paragraph.edit_mode == "manual":
|
||||
content = {"content": [{"type": "text", "text": paragraph.content or "该段落为人工编辑模式。"}]}
|
||||
else:
|
||||
start = time.perf_counter()
|
||||
content = _build_mock_content(paragraph)
|
||||
duration = round(time.perf_counter() - start, 4)
|
||||
log = GenerationLog(
|
||||
document_id=document.id,
|
||||
paragraph_id=paragraph.id,
|
||||
model_id=paragraph.model_id,
|
||||
status="success",
|
||||
content=json.dumps(content, ensure_ascii=False),
|
||||
duration=duration,
|
||||
error_msg="",
|
||||
)
|
||||
db.add(log)
|
||||
done_count += 1
|
||||
continue
|
||||
|
||||
log = GenerationLog(
|
||||
document_id=document.id,
|
||||
paragraph_id=paragraph.id,
|
||||
model_id=paragraph.model_id,
|
||||
status="success",
|
||||
content=json.dumps(content, ensure_ascii=False),
|
||||
duration=0,
|
||||
error_msg="",
|
||||
)
|
||||
db.add(log)
|
||||
done_count += 1
|
||||
|
||||
document.para_count_done = done_count
|
||||
document.status = "completed"
|
||||
document.file_path = f"mock://document/{document.id}"
|
||||
await db.commit()
|
||||
await db.refresh(document)
|
||||
return Response(data=_serialize_document(document))
|
||||
|
||||
|
||||
@router.get("/documents")
|
||||
async def list_documents(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
total = (await db.execute(select(func.count(Document.id)))).scalar_one()
|
||||
result = await db.execute(
|
||||
select(Document)
|
||||
.order_by(Document.id.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
items = [_serialize_document(item) for item in result.scalars().all()]
|
||||
return Response(data={"items": items, "total": total, "page": page, "page_size": page_size})
|
||||
|
||||
|
||||
@router.get("/documents/{document_id}")
|
||||
async def get_document(document_id: int, db: AsyncSession = Depends(get_db)):
|
||||
document = await db.get(Document, document_id)
|
||||
if document is None:
|
||||
raise HTTPException(status_code=404, detail="生成记录不存在")
|
||||
|
||||
log_result = await db.execute(
|
||||
select(GenerationLog, Paragraph)
|
||||
.join(Paragraph, Paragraph.id == GenerationLog.paragraph_id)
|
||||
.where(GenerationLog.document_id == document_id)
|
||||
.order_by(Paragraph.sort_index.asc(), Paragraph.id.asc())
|
||||
)
|
||||
items = []
|
||||
for log, paragraph in log_result.all():
|
||||
items.append(
|
||||
{
|
||||
"id": log.id,
|
||||
"paragraph_id": paragraph.id,
|
||||
"title": paragraph.title,
|
||||
"sort_index": paragraph.sort_index,
|
||||
"status": log.status,
|
||||
"content": json.loads(log.content) if log.content else {"content": []},
|
||||
}
|
||||
)
|
||||
|
||||
payload = _serialize_document(document)
|
||||
payload["logs"] = items
|
||||
return Response(data=payload)
|
||||
|
||||
|
||||
@router.post("/cancel/{document_id}")
|
||||
async def cancel_document(document_id: int, db: AsyncSession = Depends(get_db)):
|
||||
document = await db.get(Document, document_id)
|
||||
if document is None:
|
||||
raise HTTPException(status_code=404, detail="生成记录不存在")
|
||||
|
||||
document.status = "cancelled"
|
||||
await db.commit()
|
||||
await db.refresh(document)
|
||||
return Response(data=_serialize_document(document))
|
||||
|
||||
|
||||
@router.delete("/documents/{document_id}")
|
||||
async def delete_document(document_id: int, db: AsyncSession = Depends(get_db)):
|
||||
document = await db.get(Document, document_id)
|
||||
if document is None:
|
||||
raise HTTPException(status_code=404, detail="生成记录不存在")
|
||||
|
||||
result = await db.execute(select(GenerationLog).where(GenerationLog.document_id == document_id))
|
||||
for log in result.scalars().all():
|
||||
await db.delete(log)
|
||||
|
||||
await db.delete(document)
|
||||
await db.commit()
|
||||
return Response(data={"id": document_id})
|
||||
|
||||
@@ -1,3 +1,97 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import get_db
|
||||
from models.ai_model import AiModel
|
||||
from schemas.schemas import AiModelCreate, AiModelUpdate, Response
|
||||
from services.security import decrypt_text, encrypt_text, mask_secret
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _serialize_model(model: AiModel) -> dict:
|
||||
api_key = decrypt_text(model.api_key_encrypted)
|
||||
return {
|
||||
"id": model.id,
|
||||
"name": model.name,
|
||||
"provider": model.provider,
|
||||
"api_format": model.api_format,
|
||||
"api_endpoint": model.api_endpoint,
|
||||
"api_key_preview": mask_secret(api_key),
|
||||
"status": model.status,
|
||||
"created_at": model.created_at,
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_models(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(AiModel).order_by(AiModel.id.desc()))
|
||||
items = [_serialize_model(item) for item in result.scalars().all()]
|
||||
return Response(data=items)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_model(body: AiModelCreate, db: AsyncSession = Depends(get_db)):
|
||||
model = AiModel(
|
||||
name=body.name,
|
||||
provider=body.provider,
|
||||
api_format=body.api_format,
|
||||
api_endpoint=body.api_endpoint,
|
||||
api_key_encrypted=encrypt_text(body.api_key),
|
||||
status=body.status,
|
||||
)
|
||||
db.add(model)
|
||||
await db.commit()
|
||||
await db.refresh(model)
|
||||
return Response(data=_serialize_model(model))
|
||||
|
||||
|
||||
@router.put("/{model_id}")
|
||||
async def update_model(model_id: int, body: AiModelUpdate, db: AsyncSession = Depends(get_db)):
|
||||
model = await db.get(AiModel, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
if body.name is not None:
|
||||
model.name = body.name
|
||||
if body.provider is not None:
|
||||
model.provider = body.provider
|
||||
if body.api_format is not None:
|
||||
model.api_format = body.api_format
|
||||
if body.api_endpoint is not None:
|
||||
model.api_endpoint = body.api_endpoint
|
||||
if body.status is not None:
|
||||
model.status = body.status
|
||||
if body.api_key:
|
||||
model.api_key_encrypted = encrypt_text(body.api_key)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(model)
|
||||
return Response(data=_serialize_model(model))
|
||||
|
||||
|
||||
@router.delete("/{model_id}")
|
||||
async def delete_model(model_id: int, db: AsyncSession = Depends(get_db)):
|
||||
model = await db.get(AiModel, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
await db.delete(model)
|
||||
await db.commit()
|
||||
return Response(data={"id": model_id})
|
||||
|
||||
|
||||
@router.post("/{model_id}/test")
|
||||
async def test_model(model_id: int, db: AsyncSession = Depends(get_db)):
|
||||
model = await db.get(AiModel, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
return Response(
|
||||
data={
|
||||
"id": model.id,
|
||||
"success": True,
|
||||
"message": f"模型 {model.name} 配置校验通过(当前为本地模拟测试)",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -52,6 +52,15 @@ class AiModelCreate(BaseModel):
|
||||
api_key: str = ""
|
||||
status: str = "enabled"
|
||||
|
||||
|
||||
class AiModelUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
provider: Optional[str] = None
|
||||
api_format: Optional[str] = None
|
||||
api_endpoint: Optional[str] = None
|
||||
api_key: str = ""
|
||||
status: Optional[str] = None
|
||||
|
||||
class AiModelOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
def _build_fernet() -> Fernet:
|
||||
raw_key = settings.ENCRYPTION_KEY.encode("utf-8")
|
||||
digest = hashlib.sha256(raw_key).digest()
|
||||
return Fernet(base64.urlsafe_b64encode(digest))
|
||||
|
||||
|
||||
def encrypt_text(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
return _build_fernet().encrypt(value.encode("utf-8")).decode("utf-8")
|
||||
|
||||
|
||||
def decrypt_text(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
try:
|
||||
return _build_fernet().decrypt(value.encode("utf-8")).decode("utf-8")
|
||||
except InvalidToken:
|
||||
return ""
|
||||
|
||||
|
||||
def mask_secret(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
if len(value) <= 7:
|
||||
return "*" * len(value)
|
||||
return f"{value[:3]}****{value[-4:]}"
|
||||
Reference in New Issue
Block a user