first commit

This commit is contained in:
baozaotumao2025
2026-07-18 21:00:26 +08:00
commit 14722be770
105 changed files with 25004 additions and 0 deletions
View File
+107
View File
@@ -0,0 +1,107 @@
from functools import lru_cache
from typing import Literal
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
app_name: str = "AI模型内部安全测试平台"
app_env: Literal["test", "production"] = "test"
app_host: str = "0.0.0.0"
app_port: int = 8000
app_debug: bool = True
app_api_prefix: str = "/api/v1"
log_level_test: str = "INFO"
log_level_production: str = "WARNING"
log_json: bool = False
database_url: str = "sqlite:///./data/platform.db"
cors_allow_origins: str = "http://localhost:3000"
cors_allow_credentials: bool = True
cors_allow_methods: str = "*"
cors_allow_headers: str = "*"
target_api_type: str = "openai"
target_base_url: str
target_chat_path: str = "/chat/completions"
target_models_path: str = "/models"
target_model: str = ""
target_auth_type: str = "none"
target_api_key: str = ""
target_auth_header: str = "Authorization"
target_auth_prefix: str = "Bearer "
judge_api_type: str = "openai"
judge_base_url: str
judge_chat_path: str = "/chat/completions"
judge_models_path: str = "/models"
judge_model: str = ""
judge_auth_type: str = "bearer"
judge_api_key: str = ""
judge_auth_header: str = "Authorization"
judge_auth_prefix: str = "Bearer "
request_timeout_seconds: int = Field(default=180, gt=0)
request_retry_count: int = Field(default=5, ge=0, le=10)
request_retry_delay_seconds: int = Field(default=5, gt=0)
request_retry_max_delay_seconds: int = Field(default=60, ge=5, le=300)
request_verify_ssl: bool = True
auth_token_secret: str = ""
auth_token_ttl_seconds: int = Field(default=3600, ge=300, le=86400)
auth_token_refresh_window_seconds: int = Field(default=1800, ge=60, le=43200)
auth_refresh_token_ttl_seconds: int = Field(default=28800, ge=3600, le=31536000)
dataset_path: str = "data/dataset.json"
output_dir: str = "outputs"
@model_validator(mode="after")
def validate_auth_timing(self):
if self.auth_token_refresh_window_seconds >= self.auth_token_ttl_seconds:
raise ValueError(
"AUTH_TOKEN_REFRESH_WINDOW_SECONDS must be less than AUTH_TOKEN_TTL_SECONDS"
)
if self.auth_refresh_token_ttl_seconds < self.auth_token_ttl_seconds:
raise ValueError(
"AUTH_REFRESH_TOKEN_TTL_SECONDS must not be shorter than AUTH_TOKEN_TTL_SECONDS"
)
return self
@property
def log_level(self) -> str:
return self.log_level_production if self.app_env == "production" else self.log_level_test
@property
def cors_origins(self) -> list[str]:
return [x.strip() for x in self.cors_allow_origins.split(",") if x.strip()]
@property
def cors_methods(self) -> list[str]:
return (
["*"]
if self.cors_allow_methods.strip() == "*"
else [x.strip() for x in self.cors_allow_methods.split(",") if x.strip()]
)
@property
def cors_headers(self) -> list[str]:
return (
["*"]
if self.cors_allow_headers.strip() == "*"
else [x.strip() for x in self.cors_allow_headers.split(",") if x.strip()]
)
@lru_cache
def get_settings() -> Settings:
return Settings()
+43
View File
@@ -0,0 +1,43 @@
class AppError(Exception):
status_code = 400
code = "APP_ERROR"
def __init__(self, message: str, *, details: dict | None = None):
super().__init__(message)
self.message = message
self.details = details or {}
class NotFoundError(AppError):
status_code = 404
code = "NOT_FOUND"
class ConfigurationError(AppError):
status_code = 500
code = "CONFIGURATION_ERROR"
class ProviderError(AppError):
status_code = 502
code = "PROVIDER_ERROR"
class ConflictError(AppError):
status_code = 409
code = "CONFLICT"
class RefreshNotDueError(AppError):
status_code = 409
code = "REFRESH_NOT_DUE"
class UnauthorizedError(AppError):
status_code = 401
code = "UNAUTHORIZED"
class ForbiddenError(AppError):
status_code = 403
code = "FORBIDDEN"
+41
View File
@@ -0,0 +1,41 @@
import logging
from uuid import uuid4
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.core.exceptions import AppError
logger = logging.getLogger(__name__)
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(AppError)
async def handle_app_error(request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"details": exc.details,
"request_id": getattr(request.state, "request_id", None),
}
},
)
@app.exception_handler(Exception)
async def handle_unexpected_error(request: Request, exc: Exception) -> JSONResponse:
error_id = str(uuid4())
logger.exception("Unhandled exception error_id=%s", error_id)
return JSONResponse(
status_code=500,
content={
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "服务器内部错误",
"error_id": error_id,
"request_id": getattr(request.state, "request_id", None),
}
},
)
+34
View File
@@ -0,0 +1,34 @@
import json
import logging
import sys
from datetime import datetime, timezone
from app.core.config import get_settings
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if hasattr(record, "request_id"):
payload["request_id"] = record.request_id
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=False)
def configure_logging() -> None:
settings = get_settings()
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(
JsonFormatter() if settings.log_json
else logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
)
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(settings.log_level.upper())