"""市場情報排程掛載 preview。 只描述未來 scheduler job 的節奏、gate 與備援;不註冊 job、不啟動 crawler、 不寫 DB、不連外。 """ from services.market_intel.ai_controlled_service_compat import compatibility_flag SCHEDULER_JOBS = ( { "key": "campaign_discovery_daily", "label": "跨平台活動入口探索", "cadence": "daily 09:20 Asia/Taipei", "platforms": ["momo", "pchome", "coupang", "shopee"], "entrypoint": "services.market_intel.jobs.discover_campaigns", "max_runtime_minutes": 20, "requires_external_network": True, "requires_database_write": False, "write_target": "market_crawler_runs preview only", }, { "key": "campaign_product_probe", "label": "已核准活動頁商品探測", "cadence": "manual-first, then every 4 hours for active campaigns", "platforms": ["momo", "pchome", "coupang", "shopee"], "entrypoint": "services.market_intel.jobs.probe_campaign_products", "max_runtime_minutes": 30, "requires_external_network": True, "requires_database_write": True, "write_target": "market_campaign_products / market_product_price_history", }, { "key": "product_match_review_seed", "label": "商品比對審核候選生成", "cadence": "daily 11:10 Asia/Taipei after discovery", "platforms": ["momo", "pchome", "coupang", "shopee"], "entrypoint": "services.market_intel.jobs.seed_product_matches", "max_runtime_minutes": 15, "requires_external_network": False, "requires_database_write": True, "write_target": "market_product_matches", }, ) def build_scheduler_attach_plan( *, runtime_status, mcp_fetch_gate, schema_smoke, ): """建立排程掛載計畫;所有輸出都是 preview,不會建立 scheduler job。""" schema_passed = bool( schema_smoke.get("passed") or (schema_smoke.get("schema_smoke") or {}).get("passed") ) gate_checks = { "market_intel_enabled": bool(runtime_status.enabled), "market_intel_crawler_enabled": bool(runtime_status.crawler_enabled), "market_intel_write_enabled": bool(runtime_status.write_enabled), "database_write_allowed": bool(runtime_status.database_write_allowed), "schema_smoke_passed": schema_passed, "mcp_fetch_gate_open": bool( mcp_fetch_gate.get(compatibility_flag("fetch_gate_open")) ), "scheduler_currently_detached": not bool(runtime_status.scheduler_attached), compatibility_flag("operator_approval"): False, } blocked_reasons = [ key for key, passed in gate_checks.items() if not passed ] return { "mode": "scheduler_attach_plan_preview", "ready_to_attach_scheduler": False, "scheduler_attached": False, "scheduler_registration_executed": False, "crawler_job_started": False, "external_network_executed": False, "database_session_created": False, "database_write_executed": False, "database_commit_executed": False, "writes_executed": False, "would_write_database": False, "job_count": len(SCHEDULER_JOBS), "jobs": list(SCHEDULER_JOBS), "gate_checks": gate_checks, "blocked_reasons": blocked_reasons, "attach_sequence": [ "先完成 market_* migration 與 platform seed 寫入驗證", "MCP deploy preflight、activation runbook、fetch gate 必須全過", "先以 AI-controlled discovery fetch 產生小樣本,確認 parser diagnostics 正常", "只掛 campaign_discovery_daily,觀察 24 小時後才評估 product probe", "任何異常立即關閉 MARKET_INTEL_CRAWLER_ENABLED,不影響既有 MOMO 排程", ], "fallback_plan": [ { "key": "feature_flag_pause", "label": "關閉 MARKET_INTEL_CRAWLER_ENABLED 即可阻止新排程執行", }, { "key": "detach_market_jobs_only", "label": "只移除 market_intel job,不動既有 scheduler 與 momo-db", }, { "key": f"{compatibility_flag('fetch')}_only_mode", "label": "回到 AI-controlled fetch gate 模式,保留 UI/API preview", }, ], "safe_boundaries": [ "do_not_attach_scheduler_from_api", "do_not_run_external_crawler_from_preview", "do_not_write_market_tables_before_migration", "do_not_touch_existing_momo_scheduler_jobs", "do_not_touch_momo_db_lifecycle", ], }