config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. from __future__ import annotations
  2. from functools import lru_cache
  3. from pathlib import Path
  4. from urllib.parse import quote_plus
  5. from pydantic import Field, SecretStr, model_validator
  6. from pydantic_settings import BaseSettings, SettingsConfigDict
  7. # supply_infra/config.py -> project root; avoid depending on process cwd
  8. _PROJECT_ROOT = Path(__file__).resolve().parent.parent
  9. _ENV_FILE = _PROJECT_ROOT / ".env"
  10. class InfraSettings(BaseSettings):
  11. """Shared infrastructure settings."""
  12. model_config = SettingsConfigDict(
  13. env_file=str(_ENV_FILE),
  14. env_file_encoding="utf-8",
  15. extra="ignore",
  16. )
  17. # MySQL
  18. mysql_host: str = Field(default="127.0.0.1", alias="MYSQL_HOST")
  19. mysql_port: int = Field(default=3306, alias="MYSQL_PORT")
  20. mysql_user: str = Field(default="root", alias="MYSQL_USER")
  21. mysql_password: str = Field(default="", alias="MYSQL_PASSWORD")
  22. mysql_database: str = Field(default="supply_agent", alias="MYSQL_DATABASE")
  23. mysql_pool_size: int = Field(default=1, ge=1, alias="MYSQL_POOL_SIZE")
  24. mysql_connection_budget: int = Field(
  25. default=40,
  26. ge=30,
  27. le=50,
  28. alias="MYSQL_CONNECTION_BUDGET",
  29. )
  30. mysql_pool_size_api: int = Field(default=6, ge=1, alias="MYSQL_POOL_SIZE_API")
  31. mysql_pool_size_control: int = Field(
  32. default=2,
  33. ge=1,
  34. alias="MYSQL_POOL_SIZE_CONTROL",
  35. )
  36. mysql_max_overflow: int = Field(default=0, ge=0, alias="MYSQL_MAX_OVERFLOW")
  37. mysql_pool_timeout_seconds: int = Field(
  38. default=10,
  39. ge=1,
  40. alias="MYSQL_POOL_TIMEOUT_SECONDS",
  41. )
  42. mysql_pool_recycle_seconds: int = Field(
  43. default=1800,
  44. ge=60,
  45. alias="MYSQL_POOL_RECYCLE_SECONDS",
  46. )
  47. mysql_connect_timeout_seconds: int = Field(
  48. default=10,
  49. ge=1,
  50. alias="MYSQL_CONNECT_TIMEOUT_SECONDS",
  51. )
  52. mysql_read_timeout_seconds: int = Field(
  53. default=30,
  54. ge=1,
  55. alias="MYSQL_READ_TIMEOUT_SECONDS",
  56. )
  57. mysql_write_timeout_seconds: int = Field(
  58. default=30,
  59. ge=1,
  60. alias="MYSQL_WRITE_TIMEOUT_SECONDS",
  61. )
  62. mysql_operational_reserve: int = Field(
  63. default=4,
  64. ge=2,
  65. alias="MYSQL_OPERATIONAL_RESERVE",
  66. )
  67. mysql_echo: bool = Field(default=False, alias="MYSQL_ECHO")
  68. # Read-only AIGC article visualization database
  69. aigc_readonly_mysql_host: str = Field(
  70. default="",
  71. alias="AIGC_READONLY_MYSQL_HOST",
  72. )
  73. aigc_readonly_mysql_port: int = Field(
  74. default=3306,
  75. alias="AIGC_READONLY_MYSQL_PORT",
  76. )
  77. aigc_readonly_mysql_user: str = Field(
  78. default="",
  79. alias="AIGC_READONLY_MYSQL_USER",
  80. )
  81. aigc_readonly_mysql_password: SecretStr = Field(
  82. default=SecretStr(""),
  83. alias="AIGC_READONLY_MYSQL_PASSWORD",
  84. )
  85. aigc_readonly_mysql_database: str = Field(
  86. default="",
  87. alias="AIGC_READONLY_MYSQL_DATABASE",
  88. )
  89. # Local web authentication
  90. auth_session_hours: int = Field(default=12, ge=1, le=168, alias="AUTH_SESSION_HOURS")
  91. auth_cookie_secure: bool = Field(default=False, alias="AUTH_COOKIE_SECURE")
  92. auth_bootstrap_admin_username: str = Field(
  93. default="",
  94. alias="AUTH_BOOTSTRAP_ADMIN_USERNAME",
  95. )
  96. auth_bootstrap_admin_password: SecretStr = Field(
  97. default=SecretStr(""),
  98. alias="AUTH_BOOTSTRAP_ADMIN_PASSWORD",
  99. )
  100. auth_bootstrap_admin_display_name: str = Field(
  101. default="系统管理员",
  102. alias="AUTH_BOOTSTRAP_ADMIN_DISPLAY_NAME",
  103. )
  104. # ODPS (MaxCompute)
  105. odps_access_id: str = Field(default="", alias="ODPS_ACCESS_ID")
  106. odps_access_key: str = Field(default="", alias="ODPS_ACCESS_KEY")
  107. odps_project: str = Field(default="", alias="ODPS_PROJECT")
  108. odps_endpoint: str = Field(
  109. default="https://service.cn.maxcompute.aliyun.com/api",
  110. alias="ODPS_ENDPOINT",
  111. )
  112. # Scheduler
  113. scheduler_timezone: str = Field(default="Asia/Shanghai", alias="SCHEDULER_TIMEZONE")
  114. scheduler_enabled: bool = Field(default=False, alias="SCHEDULER_ENABLED")
  115. scheduler_cron_hour: int = Field(default=15, ge=0, le=23, alias="SCHEDULER_CRON_HOUR")
  116. scheduler_cron_minute: int = Field(
  117. default=0,
  118. ge=0,
  119. le=59,
  120. alias="SCHEDULER_CRON_MINUTE",
  121. )
  122. scheduler_aigc_publish_interval_minutes: int = Field(
  123. default=10,
  124. ge=1,
  125. le=1440,
  126. alias="SCHEDULER_AIGC_PUBLISH_INTERVAL_MINUTES",
  127. )
  128. # find_agent quality gates
  129. find_agent_rule_version: str = Field(
  130. default="find-agent-gate-v1",
  131. alias="FIND_AGENT_RULE_VERSION",
  132. )
  133. find_agent_min_duration_seconds: int = Field(
  134. default=30,
  135. ge=30,
  136. alias="FIND_AGENT_MIN_DURATION_SECONDS",
  137. )
  138. find_agent_min_share_count: int = Field(
  139. default=1000,
  140. ge=0,
  141. alias="FIND_AGENT_MIN_SHARE_COUNT",
  142. )
  143. find_agent_min_content_50_plus_ratio: float = Field(
  144. default=0.20,
  145. ge=0,
  146. le=1,
  147. alias="FIND_AGENT_MIN_CONTENT_50_PLUS_RATIO",
  148. )
  149. find_agent_min_account_50_plus_ratio: float = Field(
  150. default=0.20,
  151. ge=0,
  152. le=1,
  153. alias="FIND_AGENT_MIN_ACCOUNT_50_PLUS_RATIO",
  154. )
  155. find_agent_allow_missing_elder_portrait: bool = Field(
  156. default=False,
  157. alias="FIND_AGENT_ALLOW_MISSING_ELDER_PORTRAIT",
  158. )
  159. find_agent_festival_lead_days: int = Field(
  160. default=7,
  161. ge=0,
  162. le=30,
  163. alias="FIND_AGENT_FESTIVAL_LEAD_DAYS",
  164. )
  165. find_agent_event_max_age_days: int = Field(
  166. default=7,
  167. ge=1,
  168. le=180,
  169. alias="FIND_AGENT_EVENT_MAX_AGE_DAYS",
  170. )
  171. find_agent_seasonal_max_age_days: int = Field(
  172. default=180,
  173. ge=1,
  174. le=366,
  175. alias="FIND_AGENT_SEASONAL_MAX_AGE_DAYS",
  176. )
  177. # Pipeline process/control plane
  178. process_role: str = Field(default="api", alias="PROCESS_ROLE")
  179. database_auto_create: bool = Field(default=False, alias="DATABASE_AUTO_CREATE")
  180. pipeline_worker_processes: int = Field(
  181. default=4,
  182. ge=1,
  183. le=20,
  184. alias="PIPELINE_WORKER_PROCESSES",
  185. )
  186. pipeline_max_active_steps: int = Field(
  187. default=4,
  188. ge=1,
  189. le=20,
  190. alias="PIPELINE_MAX_ACTIVE_STEPS",
  191. )
  192. pipeline_worker_poll_seconds: float = Field(
  193. default=2.0,
  194. gt=0,
  195. alias="PIPELINE_WORKER_POLL_SECONDS",
  196. )
  197. pipeline_lease_seconds: int = Field(
  198. default=120,
  199. ge=30,
  200. alias="PIPELINE_LEASE_SECONDS",
  201. )
  202. pipeline_heartbeat_seconds: int = Field(
  203. default=30,
  204. ge=5,
  205. alias="PIPELINE_HEARTBEAT_SECONDS",
  206. )
  207. pipeline_reconcile_seconds: int = Field(
  208. default=60,
  209. ge=10,
  210. alias="PIPELINE_RECONCILE_SECONDS",
  211. )
  212. pipeline_missed_run_grace_seconds: int = Field(
  213. default=600,
  214. ge=60,
  215. alias="PIPELINE_MISSED_RUN_GRACE_SECONDS",
  216. )
  217. pipeline_shutdown_grace_seconds: int = Field(
  218. default=300,
  219. ge=10,
  220. alias="PIPELINE_SHUTDOWN_GRACE_SECONDS",
  221. )
  222. pipeline_warn_after_hours: int = Field(
  223. default=12,
  224. ge=1,
  225. alias="PIPELINE_WARN_AFTER_HOURS",
  226. )
  227. pipeline_critical_before_next_minutes: int = Field(
  228. default=120,
  229. ge=1,
  230. alias="PIPELINE_CRITICAL_BEFORE_NEXT_MINUTES",
  231. )
  232. pipeline_final_warn_before_next_minutes: int = Field(
  233. default=30,
  234. ge=1,
  235. alias="PIPELINE_FINAL_WARN_BEFORE_NEXT_MINUTES",
  236. )
  237. pipeline_log_dir: str = Field(default="logs/pipeline", alias="PIPELINE_LOG_DIR")
  238. # Aliyun OSS (agent run log publishing)
  239. aliyun_oss_access_key_id: str = Field(default="", alias="ALIYUN_OSS_ACCESS_KEY_ID")
  240. aliyun_oss_access_key_secret: str = Field(default="", alias="ALIYUN_OSS_ACCESS_KEY_SECRET")
  241. aliyun_oss_region: str = Field(default="cn-hangzhou", alias="ALIYUN_OSS_REGION")
  242. aliyun_oss_bucket: str = Field(default="art-pubbucket", alias="ALIYUN_OSS_BUCKET")
  243. aliyun_oss_root_prefix: str = Field(default="supply_agent", alias="ALIYUN_OSS_ROOT_PREFIX")
  244. aliyun_oss_manual_log_prefix: str = Field(
  245. default="supply_agent/manual_logs",
  246. alias="ALIYUN_OSS_MANUAL_LOG_PREFIX",
  247. )
  248. aliyun_oss_public_base_url: str = Field(
  249. default="http://rescdn.yishihui.com",
  250. alias="ALIYUN_OSS_PUBLIC_BASE_URL",
  251. )
  252. aliyun_oss_connect_timeout_seconds: int = Field(
  253. default=30,
  254. ge=5,
  255. le=300,
  256. alias="ALIYUN_OSS_CONNECT_TIMEOUT_SECONDS",
  257. )
  258. log_oss_upload_enabled: bool = Field(default=True, alias="LOG_OSS_UPLOAD_ENABLED")
  259. # AIGC platform
  260. aigc_api_token: str = Field(default="", alias="AIGC_API_TOKEN")
  261. @model_validator(mode="after")
  262. def validate_pipeline_runtime(self) -> "InfraSettings":
  263. roles = {"api", "scheduler", "worker", "reconciler", "step", "cli"}
  264. if self.process_role not in roles:
  265. raise ValueError(
  266. f"PROCESS_ROLE must be one of {sorted(roles)}, got {self.process_role!r}"
  267. )
  268. if self.pipeline_heartbeat_seconds >= self.pipeline_lease_seconds:
  269. raise ValueError(
  270. "PIPELINE_HEARTBEAT_SECONDS must be smaller than PIPELINE_LEASE_SECONDS"
  271. )
  272. if self.pipeline_max_active_steps > self.pipeline_worker_processes:
  273. raise ValueError("PIPELINE_MAX_ACTIVE_STEPS cannot exceed PIPELINE_WORKER_PROCESSES")
  274. if self.mysql_max_overflow != 0:
  275. raise ValueError("MYSQL_MAX_OVERFLOW must remain 0 for connection budgeting")
  276. required = self.pipeline_required_connections
  277. if required > self.mysql_connection_budget:
  278. raise ValueError(
  279. "Pipeline connection budget exceeded: "
  280. f"required={required} budget={self.mysql_connection_budget}"
  281. )
  282. return self
  283. @property
  284. def mysql_url(self) -> str:
  285. user = quote_plus(self.mysql_user)
  286. password = quote_plus(self.mysql_password)
  287. return (
  288. f"mysql+pymysql://{user}:{password}"
  289. f"@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
  290. f"?charset=utf8mb4"
  291. )
  292. @property
  293. def selected_mysql_pool_size(self) -> int:
  294. if self.process_role == "api":
  295. return self.mysql_pool_size_api
  296. if self.process_role in {"scheduler", "worker", "reconciler", "step"}:
  297. return self.mysql_pool_size_control
  298. return self.mysql_pool_size
  299. @property
  300. def aigc_readonly_mysql_url(self) -> str:
  301. user = quote_plus(self.aigc_readonly_mysql_user)
  302. password = quote_plus(self.aigc_readonly_mysql_password.get_secret_value())
  303. return (
  304. f"mysql+pymysql://{user}:{password}"
  305. f"@{self.aigc_readonly_mysql_host}:{self.aigc_readonly_mysql_port}"
  306. f"/{self.aigc_readonly_mysql_database}?charset=utf8mb4"
  307. )
  308. @property
  309. def pipeline_required_connections(self) -> int:
  310. control_processes = 2 # scheduler + reconciler
  311. return (
  312. self.mysql_pool_size_api
  313. + control_processes * self.mysql_pool_size_control
  314. + self.pipeline_worker_processes * self.mysql_pool_size_control
  315. + self.pipeline_max_active_steps * self.mysql_pool_size_control
  316. + self.mysql_operational_reserve
  317. )
  318. @lru_cache
  319. def get_infra_settings() -> InfraSettings:
  320. return InfraSettings()