feat(market-intel): add scheduler attach plan preview
Some checks failed
CD Pipeline / deploy (push) Failing after 45s
Some checks failed
CD Pipeline / deploy (push) Failing after 45s
This commit is contained in:
115
services/market_intel/scheduler_plan.py
Normal file
115
services/market_intel/scheduler_plan.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""市場情報排程掛載 preview。
|
||||
|
||||
只描述未來 scheduler job 的節奏、gate 與備援;不註冊 job、不啟動 crawler、
|
||||
不寫 DB、不連外。
|
||||
"""
|
||||
|
||||
|
||||
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("manual_fetch_gate_open")),
|
||||
"scheduler_currently_detached": not bool(runtime_status.scheduler_attached),
|
||||
"manual_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 必須全過",
|
||||
"先以 manual 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": "manual_fetch_only_mode",
|
||||
"label": "回到人工 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",
|
||||
],
|
||||
}
|
||||
@@ -28,6 +28,7 @@ from services.market_intel.mcp_readiness import build_mcp_readiness_plan
|
||||
from services.market_intel.migration_blueprint import build_migration_blueprint
|
||||
from services.market_intel.platform_seed import build_platform_seed_rows
|
||||
from services.market_intel.platform_seed_db_diff import build_platform_seed_db_diff_plan
|
||||
from services.market_intel.scheduler_plan import build_scheduler_attach_plan
|
||||
from services.market_intel.platform_seed_writer import (
|
||||
build_platform_seed_writer_plan,
|
||||
build_schema_smoke,
|
||||
@@ -68,7 +69,7 @@ class MarketIntelRuntimeStatus:
|
||||
class MarketIntelService:
|
||||
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
|
||||
|
||||
phase = "phase_32_mcp_fetch_gate_preview"
|
||||
phase = "phase_33_scheduler_plan_preview"
|
||||
|
||||
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
|
||||
return MarketIntelRuntimeStatus(
|
||||
@@ -366,6 +367,16 @@ class MarketIntelService:
|
||||
gate["phase"] = self.phase
|
||||
return gate
|
||||
|
||||
def build_scheduler_plan(self):
|
||||
"""回報市場情報排程掛載計畫;不註冊 job、不啟動 crawler。"""
|
||||
plan = build_scheduler_attach_plan(
|
||||
runtime_status=self.get_runtime_status(),
|
||||
mcp_fetch_gate=self.build_mcp_fetch_gate(),
|
||||
schema_smoke=build_schema_smoke(MARKET_INTEL_TABLES),
|
||||
)
|
||||
plan["phase"] = self.phase
|
||||
return plan
|
||||
|
||||
def build_platform_seed_writer_plan(self, platform_code="all"):
|
||||
"""建立 platform seed writer dry-run plan,不建立 DB session。"""
|
||||
seed_plan = self.build_platform_seed_plan(platform_code=platform_code)
|
||||
@@ -445,6 +456,7 @@ class MarketIntelService:
|
||||
mcp_deploy_preflight = self.build_mcp_deploy_preflight()
|
||||
mcp_activation_runbook = self.build_mcp_activation_runbook()
|
||||
mcp_fetch_gate = self.build_mcp_fetch_gate()
|
||||
scheduler_plan = self.build_scheduler_plan()
|
||||
checks = {
|
||||
"schema_smoke_passed": bool(schema_smoke["passed"]),
|
||||
"feature_flags_default_safe": bool(
|
||||
@@ -489,6 +501,12 @@ class MarketIntelService:
|
||||
and not mcp_fetch_gate["network_request_allowed"]
|
||||
and not mcp_fetch_gate["external_network_executed"]
|
||||
),
|
||||
"scheduler_plan_preview_safe": bool(
|
||||
scheduler_plan["mode"] == "scheduler_attach_plan_preview"
|
||||
and not scheduler_plan["scheduler_registration_executed"]
|
||||
and not scheduler_plan["crawler_job_started"]
|
||||
and not scheduler_plan["database_write_executed"]
|
||||
),
|
||||
}
|
||||
ready_for_production_deploy = all(checks.values())
|
||||
blocked_reasons = [
|
||||
@@ -617,6 +635,7 @@ class MarketIntelService:
|
||||
"/api/market_intel/mcp_deploy_preflight",
|
||||
"/api/market_intel/mcp_activation_runbook",
|
||||
"/api/market_intel/mcp_fetch_gate",
|
||||
"/api/market_intel/scheduler_plan",
|
||||
],
|
||||
"status": status.to_dict(),
|
||||
"schema_smoke": schema_smoke,
|
||||
@@ -636,4 +655,5 @@ class MarketIntelService:
|
||||
"mcp_deploy_preflight": mcp_deploy_preflight,
|
||||
"mcp_activation_runbook": mcp_activation_runbook,
|
||||
"mcp_fetch_gate": mcp_fetch_gate,
|
||||
"scheduler_plan": scheduler_plan,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user