A8 多套交付包(packaging_task.py): - 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组 - 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容 - task74端到端验:3套各6图,独立agent7项交叉验证全过 M4 归档(tasks.py/exports.py/前端): - list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1) - 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM) - 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner - response.py加raise_param_error;独立agent验A1/A2/A9通过 R5 产品多图(product_images.py/020迁移/前端): - product_images表+5端点(上传/列/改场景/设主图/删图) - 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary - 前端ProductImageManager多图画廊 R6 账号config拆页(settings/): - 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin) - Key只显末4位不显余额(守红线) 核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
"""
|
||
app/core/config.py — 环境变量加载 + 启动校验
|
||
必填项缺失则启动失败,不静默。
|
||
"""
|
||
|
||
import os
|
||
from functools import lru_cache
|
||
|
||
from pydantic import field_validator
|
||
from pydantic_settings import BaseSettings
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
# ── 必填(缺失启动保护)───────────────────────────
|
||
FERNET_KEY: str
|
||
JWT_SECRET: str
|
||
DATABASE_URL: str
|
||
MONGO_URI: str
|
||
REDIS_URL: str
|
||
|
||
# ── AI 提供商(不写死默认,可配置)──────────────────
|
||
IMAGE_PROVIDER_PRIMARY: str = "gpt"
|
||
IMAGE_PROVIDER_FALLBACK: str = "gemini"
|
||
IMAGE_API_BASE: str = ""
|
||
IMAGE_MODEL: str = "gpt-image-2"
|
||
MODEL_IMAGE: str = "gpt-image-2" # 别名,与 IMAGE_MODEL 同义
|
||
MODEL_TEXT: str = "claude-opus-4-8"
|
||
MODEL_VISION: str = "" # 视觉模型(verify_real_image 脚本用)
|
||
CLAUDE_API_URL: str = ""
|
||
GEMINI_API_URL: str = ""
|
||
|
||
# ── 多 Provider Key(各人自录,测试/真实出图脚本用)──
|
||
APIPORTS_BASE_URL: str = ""
|
||
APIPORTS_KEY: str = ""
|
||
CODEPROXY_BASE_URL: str = ""
|
||
CODEPROXY_KEY: str = ""
|
||
|
||
# ── 应用配置 ──────────────────────────────────────
|
||
APP_ENV: str = "development"
|
||
JWT_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7天
|
||
CELERY_BROKER_URL: str = ""
|
||
CELERY_RESULT_BACKEND: str = ""
|
||
|
||
# ── 并发限制(可配置,不写死)─────────────────────
|
||
# 倩倩姐红线:每用户并发上限 5 个任务(客户侧实际跑几个再观察调整)。
|
||
MAX_CONCURRENT_TASKS_PER_USER: int = 5
|
||
|
||
# ── 文件存储路径 ──────────────────────────────────
|
||
UPLOAD_BASE_PATH: str = "uploads/packages"
|
||
# StaticFiles 实际服务的绝对目录(与 main.py app.mount("/uploads", .../uploads) 一致)。
|
||
# 容器内 main.py 在 /app/main.py,故 uploads 绝对根 = /app/uploads。
|
||
# 所有写盘/读盘统一锚定此根,避免 api(cwd=/app) 与 worker(cwd=/) 相对路径解析不一致。
|
||
UPLOAD_ABS_ROOT: str = "/app/uploads"
|
||
|
||
model_config = {"env_file": ".env", "case_sensitive": True, "extra": "ignore"}
|
||
|
||
@field_validator("FERNET_KEY")
|
||
@classmethod
|
||
def fernet_key_not_empty(cls, v: str) -> str:
|
||
if not v or len(v) < 32:
|
||
raise ValueError("FERNET_KEY must be set and at least 32 chars")
|
||
return v
|
||
|
||
@field_validator("JWT_SECRET")
|
||
@classmethod
|
||
def jwt_secret_not_empty(cls, v: str) -> str:
|
||
if not v or len(v) < 32:
|
||
raise ValueError("JWT_SECRET must be at least 32 characters")
|
||
return v
|
||
|
||
def celery_broker(self) -> str:
|
||
return self.CELERY_BROKER_URL or self.REDIS_URL
|
||
|
||
def celery_backend(self) -> str:
|
||
return self.CELERY_RESULT_BACKEND or self.REDIS_URL
|
||
|
||
|
||
@lru_cache()
|
||
def get_settings() -> Settings:
|
||
"""单例配置,启动时校验一次。"""
|
||
return Settings()
|