config.py 3.5 KB

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