| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879 |
- """配置以项目位置解析,开发密钥持久化才能保证重启后令牌仍可验证。"""
- from pathlib import Path
- import secrets
- from pydantic import Field, model_validator
- from pydantic_settings import BaseSettings, SettingsConfigDict
- ROOT = Path(__file__).resolve().parents[2]
- class Settings(BaseSettings):
- """关键参数有范围限制,错误配置在启动阶段暴露而非拖到审核过程中。"""
- model_config = SettingsConfigDict(env_file=ROOT / ".env", extra="ignore", case_sensitive=False)
- app_env: str = "development" # 生产环境启用额外密钥与演示数据校验。
- host: str = "127.0.0.1"
- port: int = Field(default=8000, ge=1, le=65535)
- api_prefix: str = "/api/v1"
- data_dir: str = "data"
- database_url: str = ""
- jwt_secret: str = ""
- access_minutes: int = Field(default=15, ge=1, le=1440)
- refresh_days: int = Field(default=7, ge=1, le=30)
- cors_origins: list[str] = ["http://localhost:5173", "http://127.0.0.1:5173"]
- business_timezone: str = "Asia/Shanghai"
- seed_demo: bool = True
- demo_password: str = "Demo@2026!"
- max_upload_mb: int = Field(default=10, ge=1, le=100)
- max_batch_files: int = Field(default=20, ge=1, le=100)
- max_pdf_pages: int = Field(default=5, ge=1, le=30)
- worker_enabled: bool = True
- worker_interval: float = Field(default=0.5, ge=0.1, le=30)
- outbox_attempts: int = Field(default=3, ge=1, le=10)
- approval_timeout_hours: int = Field(default=48, ge=1, le=720)
- timeout_scan_seconds: int = Field(default=60, ge=1, le=3600)
- rate_limit_per_minute: int = Field(default=120, ge=10, le=10000)
- llm_base_url: str = ""
- llm_model: str = ""
- vlm_base_url: str = ""
- vlm_model: str = ""
- ai_api_key: str = ""
- embedding_base_url: str = ""
- embedding_model: str = "bge-large-zh-v1.5"
- embedding_dimension: int = Field(default=1024, ge=1, le=8192)
- rerank_base_url: str = ""
- ai_timeout: float = Field(default=20, ge=1, le=120)
- ai_concurrency: int = Field(default=3, ge=1, le=10)
- ai_failure_threshold: int = Field(default=5, ge=1, le=100)
- ai_recovery_seconds: int = Field(default=60, ge=1, le=600)
- pdf_font_path: str = ""
- @model_validator(mode="after")
- def validate_environment(self):
- """限制生产弱配置;允许本地演示启动但不把它当作生产部署。"""
- if not self.api_prefix.startswith("/") or self.api_prefix.endswith("/"):
- raise ValueError("API_PREFIX 必须以 / 开头且末尾不能是 /")
- if self.app_env == "production" and (self.seed_demo or len(self.jwt_secret) < 32):
- raise ValueError("生产必须关闭 SEED_DEMO 并设置至少 32 字符 JWT_SECRET")
- return self
- @property
- def data_path(self) -> Path:
- """外部路径只由服务端配置提供,前端不能传入任意文件系统路径。"""
- return (ROOT / self.data_dir).resolve()
- @property
- def db_url(self) -> str:
- return self.database_url or "sqlite+aiosqlite:///" + (self.data_path / "audit.db").as_posix()
- def secret(self) -> str:
- """开发密钥仅写入运行数据目录,打包时排除以免传播实际凭据。"""
- if self.jwt_secret:
- return self.jwt_secret
- path = self.data_path / "jwt.secret"
- path.parent.mkdir(parents=True, exist_ok=True)
- if not path.exists():
- path.write_text(secrets.token_hex(32), encoding="utf-8")
- return path.read_text(encoding="utf-8").strip()
- settings = Settings()
|