Initial commit: Daily News App

企業內部新聞彙整與分析系統
- 自動新聞抓取 (Digitimes, 經濟日報, 工商時報)
- AI 智慧摘要 (OpenAI/Claude/Ollama)
- 群組管理與訂閱通知
- 已清理 Python 快取檔案

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
donald
2025-12-03 23:53:24 +08:00
commit db0f0bbfe7
50 changed files with 11883 additions and 0 deletions

137
app/core/config.py Normal file
View File

@@ -0,0 +1,137 @@
"""
應用程式設定模組
使用 Pydantic Settings 管理環境變數
"""
from functools import lru_cache
from typing import Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""應用程式設定"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False
)
# 應用程式
app_name: str = "每日報導APP"
app_env: Literal["development", "staging", "production"] = "development"
debug: bool = Field(
default=False, # 預設為 False更安全
description="除錯模式,僅開發環境使用"
)
secret_key: str = Field(
default="change-me-in-production",
description="應用程式密鑰,生產環境必須透過環境變數設定"
)
# 資料庫
db_host: str = "localhost"
db_port: int = 3306
db_name: str = "daily_news_app"
db_user: str = "root"
db_password: str = ""
@property
def database_url(self) -> str:
if self.db_host == "sqlite":
return f"sqlite:///{self.db_name}.db"
return f"mysql+pymysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}?charset=utf8mb4"
@property
def async_database_url(self) -> str:
if self.db_host == "sqlite":
return f"sqlite+aiosqlite:///{self.db_name}.db"
return f"mysql+aiomysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}?charset=utf8mb4"
# JWT
jwt_secret_key: str = Field(
default="change-me",
description="JWT 簽章密鑰,生產環境必須透過環境變數設定"
)
jwt_algorithm: str = "HS256"
jwt_access_token_expire_minutes: int = Field(
default=480, # 開發環境預設值
description="JWT Token 過期時間(分鐘),建議生產環境設為 60-120 分鐘"
)
# LDAP
ldap_server: str = ""
ldap_port: int = 389
ldap_base_dn: str = ""
ldap_bind_dn: str = ""
ldap_bind_password: str = ""
# LLM
llm_provider: Literal["gemini", "openai", "ollama"] = "gemini"
gemini_api_key: str = ""
gemini_model: str = "gemini-1.5-pro"
openai_api_key: str = ""
openai_model: str = "gpt-4o"
ollama_endpoint: str = "http://localhost:11434"
ollama_model: str = "llama3"
# SMTP
smtp_host: str = ""
smtp_port: int = 587
smtp_username: str = ""
smtp_password: str = ""
smtp_from_email: str = ""
smtp_from_name: str = "每日報導系統"
# 爬蟲
crawl_schedule_time: str = "08:00"
crawl_request_delay: int = 3
crawl_max_retries: int = 3
# Digitimes
digitimes_username: str = ""
digitimes_password: str = ""
# 資料保留
data_retention_days: int = 60
# PDF
pdf_logo_path: str = ""
pdf_header_text: str = ""
pdf_footer_text: str = "本報告僅供內部參考使用"
# CORS 設定
cors_origins: list[str] = Field(
default=["http://localhost:3000", "http://localhost:8000"],
description="允許的 CORS 來源列表,生產環境必須明確指定,不能使用 *"
)
# 管理員預設密碼
admin_password: str = Field(
default="admin123",
description="管理員預設密碼"
)
def validate_secrets():
"""驗證生產環境的密鑰設定"""
if settings.app_env == "production":
if settings.secret_key == "change-me-in-production":
raise ValueError("生產環境必須設定 SECRET_KEY 環境變數")
if settings.jwt_secret_key == "change-me":
raise ValueError("生產環境必須設定 JWT_SECRET_KEY 環境變數")
if len(settings.secret_key) < 32:
raise ValueError("SECRET_KEY 長度必須至少 32 字元")
if len(settings.jwt_secret_key) < 32:
raise ValueError("JWT_SECRET_KEY 長度必須至少 32 字元")
if settings.jwt_access_token_expire_minutes > 120:
import warnings
warnings.warn("生產環境 JWT Token 過期時間建議不超過 120 分鐘")
@lru_cache
def get_settings() -> Settings:
"""取得設定實例(快取)"""
return Settings()
settings = get_settings()