diff --git a/.env.example b/.env.example index f551ac2..c25d49f 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,25 @@ POSTGRES_USER=docforge POSTGRES_PASSWORD=change-this-password POSTGRES_DB=docforge +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 -# Security (generate with: python -c "import secrets; print(secrets.token_urlsafe(32))") +# Redis +REDIS_URL=redis://localhost:6379/0 + +# Backend SECRET_KEY=change-me-to-a-random-secret-key +DEFAULT_MODEL_ID= + +# File Storage +STORAGE_ROOT=./storage + +# Celery +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/1 + +# CORS (JSON array format) +CORS_ORIGINS=["http://localhost:3000","http://localhost:5173"] # AI (optional, can be configured in the web UI) OPENAI_API_KEY= diff --git a/backend/app/api/models.py b/backend/app/api/models.py index 0fcf2f9..78a8216 100644 --- a/backend/app/api/models.py +++ b/backend/app/api/models.py @@ -24,7 +24,6 @@ async def create_model(data: AIModelCreate, db: AsyncSession = Depends(get_db)): db.add(model) await db.flush() await db.refresh(model) - model.api_key = "***" return model @@ -41,8 +40,6 @@ async def list_models( query = query.offset(skip).limit(limit).order_by(AIModel.created_at.desc()) result = await db.execute(query) models = result.scalars().all() - for m in models: - m.api_key = "***" return models @@ -52,7 +49,6 @@ async def get_model(model_id: str, db: AsyncSession = Depends(get_db)): model = result.scalar_one_or_none() if not model: raise HTTPException(status_code=404, detail="模型不存在") - model.api_key = "***" return model @@ -72,7 +68,6 @@ async def update_model(model_id: str, data: AIModelUpdate, db: AsyncSession = De await db.flush() await db.refresh(model) - model.api_key = "***" return model @@ -95,5 +90,4 @@ async def toggle_model(model_id: str, data: AIModelToggle, db: AsyncSession = De model.is_enabled = data.is_enabled await db.flush() await db.refresh(model) - model.api_key = "***" return model diff --git a/backend/app/api/templates.py b/backend/app/api/templates.py index 4f4431e..5343789 100644 --- a/backend/app/api/templates.py +++ b/backend/app/api/templates.py @@ -1,4 +1,5 @@ import uuid +import bleach from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession @@ -29,8 +30,6 @@ ALLOWED_ATTRS = { "table": ["style"], } -import bleach - router = APIRouter(prefix="/templates", tags=["模板管理"]) diff --git a/backend/app/schemas/ai_model.py b/backend/app/schemas/ai_model.py index 002f011..a9cbfa6 100644 --- a/backend/app/schemas/ai_model.py +++ b/backend/app/schemas/ai_model.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_serializer class AIModelCreate(BaseModel): @@ -32,7 +32,7 @@ class AIModelResponse(BaseModel): name: str provider: str endpoint: str - api_key: str = Field(default="***") + api_key: str extra_params: dict is_enabled: bool remark: str | None = None @@ -40,3 +40,7 @@ class AIModelResponse(BaseModel): updated_at: datetime model_config = {"from_attributes": True} + + @field_serializer("api_key") + def mask_api_key(self, value: str) -> str: + return "***" diff --git a/backend/app/services/ai_adapter.py b/backend/app/services/ai_adapter.py index 2e8cf38..92f175e 100644 --- a/backend/app/services/ai_adapter.py +++ b/backend/app/services/ai_adapter.py @@ -105,12 +105,16 @@ class CustomAdapter(AIAdapter): if ref_content: user_content = f"参考以下内容:\n{ref_content}\n\n任务:{prompt}" + messages = [{"role": "system", "content": "你是一个专业的文档内容生成助手。"}] + messages.append({"role": "user", "content": user_content}) + body = { - "prompt": user_content, + "model": extra.get("model", "gpt-3.5-turbo"), + "messages": messages, "max_tokens": extra.get("max_tokens", 2000), "temperature": extra.get("temperature", 0.7), } - body.update({k: v for k, v in extra.items() if k not in ("max_tokens", "temperature")}) + body.update({k: v for k, v in extra.items() if k not in ("model", "messages", "max_tokens", "temperature")}) return { "url": model_config["endpoint"], @@ -153,11 +157,25 @@ async def call_ai_model(model_config: dict, prompt: str, ref_content: str | None timeout = model_config.get("extra_params", {}).get("timeout", 120) - async with httpx.AsyncClient(timeout=timeout) as client: - response = await client.post( - request["url"], - headers=request["headers"], - json=request["json"], - ) - response.raise_for_status() - return adapter.parse_response(response.json()) + try: + async with httpx.AsyncClient( + timeout=timeout, + proxy=None, + trust_env=False, + ) as client: + response = await client.post( + request["url"], + headers=request["headers"], + json=request["json"], + ) + response.raise_for_status() + return adapter.parse_response(response.json()) + except httpx.HTTPStatusError as e: + detail = e.response.text[:500] if e.response else str(e) + raise RuntimeError(f"AI 服务返回错误 ({e.response.status_code}): {detail}") + except httpx.TimeoutException: + raise RuntimeError("AI 调用超时") + except httpx.ConnectError as e: + raise RuntimeError(f"无法连接 AI 服务: {e}") + except Exception as e: + raise RuntimeError(f"AI 调用异常: {str(e)}")