800 lines
30 KiB
Python
800 lines
30 KiB
Python
"""市場情報 Phase 1 骨架服務。
|
||
|
||
本階段只回報 feature flag 與 rollout 狀態,不啟動爬蟲、不寫資料庫。
|
||
"""
|
||
|
||
from dataclasses import asdict, dataclass
|
||
from datetime import datetime, timedelta, timezone
|
||
from uuid import uuid4
|
||
|
||
from config import (
|
||
MARKET_INTEL_CRAWLER_ENABLED,
|
||
MARKET_INTEL_ENABLED,
|
||
MARKET_INTEL_WRITE_ENABLED,
|
||
)
|
||
from services.market_intel.adapters import (
|
||
get_adapter,
|
||
get_adapter_registry,
|
||
get_adapter_summaries,
|
||
)
|
||
from services.market_intel.candidate_preview import build_candidate_preview_from_discovery
|
||
from services.market_intel.deployment_readiness import (
|
||
build_deployment_readiness_preview,
|
||
)
|
||
from services.market_intel.discovery_runner import ManualDiscoveryRunner
|
||
from services.market_intel.legacy_source_bridge import build_legacy_source_bridge_plan
|
||
from services.market_intel.live_db_inventory import build_live_db_inventory_preview
|
||
from services.market_intel.match_review_plan import build_match_review_plan_preview
|
||
from services.market_intel.mcp_activation_runbook import build_mcp_activation_runbook_preview
|
||
from services.market_intel.mcp_completion_audit import build_mcp_completion_audit_for_runtime
|
||
from services.market_intel.mcp_contract import build_mcp_tool_contract_preview
|
||
from services.market_intel.mcp_deploy_preflight import build_mcp_deploy_preflight_plan
|
||
from services.market_intel.mcp_fetch_gate import build_mcp_fetch_gate_preview
|
||
from services.market_intel.mcp_readiness import build_mcp_readiness_plan
|
||
from services.market_intel.manual_sample_plan import (
|
||
build_manual_sample_fetch_plan_preview,
|
||
)
|
||
from services.market_intel.manual_sample_acceptance import (
|
||
build_manual_sample_acceptance_preview,
|
||
)
|
||
from services.market_intel.manual_sample_candidate_queue import (
|
||
build_manual_sample_candidate_queue_approval_preview,
|
||
build_manual_sample_candidate_queue_draft_preview,
|
||
build_manual_sample_candidate_queue_transaction_preview,
|
||
)
|
||
from services.market_intel.manual_sample_review import (
|
||
build_manual_sample_candidate_handoff_preview,
|
||
build_manual_sample_review_evaluation_preview,
|
||
build_manual_sample_review_preview,
|
||
)
|
||
from services.market_intel.migration_blueprint import build_migration_blueprint
|
||
from services.market_intel.migration_catalog_review import (
|
||
build_migration_catalog_review_preview,
|
||
)
|
||
from services.market_intel.migration_drill import build_migration_apply_drill_preview
|
||
from services.market_intel.migration_live_smoke import (
|
||
build_migration_live_smoke_preview,
|
||
)
|
||
from services.market_intel.opportunity_alerts import (
|
||
build_opportunity_alert_plan_preview,
|
||
)
|
||
from services.market_intel.opportunity_evidence import (
|
||
build_opportunity_evidence_plan_preview,
|
||
)
|
||
from services.market_intel.opportunity_plan import build_opportunity_plan_preview
|
||
from services.market_intel.opportunity_scoring import (
|
||
build_opportunity_scoring_plan_preview,
|
||
)
|
||
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.phase import MARKET_INTEL_PHASE
|
||
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,
|
||
)
|
||
from services.market_intel.seed_writer_cli import build_seed_writer_cli_plan
|
||
from services.market_intel.schema_db_probe import build_schema_db_probe_plan
|
||
from services.market_intel.write_approval_runbook import build_write_approval_runbook
|
||
|
||
|
||
TAIPEI_TZ = timezone(timedelta(hours=8))
|
||
MARKET_INTEL_TABLES = (
|
||
"market_platforms",
|
||
"market_campaigns",
|
||
"market_campaign_snapshots",
|
||
"market_campaign_products",
|
||
"market_product_price_history",
|
||
"market_product_matches",
|
||
"market_crawler_runs",
|
||
"market_alert_review_queue",
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class MarketIntelRuntimeStatus:
|
||
"""市場情報模組目前的啟用狀態。"""
|
||
|
||
phase: str
|
||
enabled: bool
|
||
crawler_enabled: bool
|
||
write_enabled: bool
|
||
dry_run_only: bool
|
||
scheduler_attached: bool
|
||
database_write_allowed: bool
|
||
|
||
def to_dict(self):
|
||
return asdict(self)
|
||
|
||
|
||
class MarketIntelService:
|
||
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
|
||
|
||
phase = MARKET_INTEL_PHASE
|
||
|
||
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
|
||
return MarketIntelRuntimeStatus(
|
||
phase=self.phase,
|
||
enabled=MARKET_INTEL_ENABLED,
|
||
crawler_enabled=MARKET_INTEL_CRAWLER_ENABLED,
|
||
write_enabled=MARKET_INTEL_WRITE_ENABLED,
|
||
dry_run_only=not MARKET_INTEL_WRITE_ENABLED,
|
||
scheduler_attached=False,
|
||
database_write_allowed=(
|
||
MARKET_INTEL_ENABLED
|
||
and MARKET_INTEL_CRAWLER_ENABLED
|
||
and MARKET_INTEL_WRITE_ENABLED
|
||
),
|
||
)
|
||
|
||
def manual_fetch_allowed(self):
|
||
status = self.get_runtime_status()
|
||
return bool(status.enabled and status.crawler_enabled)
|
||
|
||
def get_schema_tables(self):
|
||
"""回傳 ADR-035 定義的 market_* schema 名稱。"""
|
||
return list(MARKET_INTEL_TABLES)
|
||
|
||
def get_adapter_summaries(self):
|
||
"""回傳目前已註冊 adapter,不觸發網路。"""
|
||
return get_adapter_summaries()
|
||
|
||
def build_dry_run_plan(self, platform_code="all"):
|
||
"""建立 dry-run 計畫,不執行爬蟲、不寫 DB。"""
|
||
status = self.get_runtime_status()
|
||
adapter_registry = get_adapter_registry()
|
||
return {
|
||
"batch_id": f"market-dry-run-{uuid4().hex[:12]}",
|
||
"platform_code": platform_code,
|
||
"created_at": datetime.now(TAIPEI_TZ).replace(tzinfo=None).isoformat(),
|
||
"phase": self.phase,
|
||
"would_discover_campaigns": bool(status.enabled and status.crawler_enabled),
|
||
"would_write_database": bool(status.database_write_allowed),
|
||
"scheduler_attached": status.scheduler_attached,
|
||
"manual_fetch_allowed": self.manual_fetch_allowed(),
|
||
"schema_tables": self.get_schema_tables(),
|
||
"adapter_count": len(adapter_registry),
|
||
"adapters": [adapter.summary() for adapter in adapter_registry.values()],
|
||
"status": status.to_dict(),
|
||
}
|
||
|
||
def build_discovery_plan(self, platform_code="all"):
|
||
"""建立平台 discovery dry-run plan,不發 request、不寫 DB。"""
|
||
if platform_code and platform_code != "all":
|
||
adapter = get_adapter(platform_code)
|
||
if not adapter:
|
||
return {
|
||
"platform_code": platform_code,
|
||
"found": False,
|
||
"plans": [],
|
||
"error": "未知平台 adapter",
|
||
}
|
||
return {
|
||
"platform_code": platform_code,
|
||
"found": True,
|
||
"plans": [adapter.build_discovery_plan()],
|
||
}
|
||
|
||
return {
|
||
"platform_code": "all",
|
||
"found": True,
|
||
"plans": [
|
||
adapter.build_discovery_plan()
|
||
for adapter in get_adapter_registry().values()
|
||
],
|
||
}
|
||
|
||
def run_manual_discovery(self, platform_code="all", *, fetch=False, http_get=None):
|
||
"""手動執行 discovery dry-run;預設不發 request,永遠不寫 DB。"""
|
||
registry = get_adapter_registry()
|
||
adapters = []
|
||
status = self.get_runtime_status()
|
||
mcp_fetch_gate = self.build_mcp_fetch_gate(
|
||
fetch_requested=fetch,
|
||
execute_readiness=bool(fetch and status.enabled and status.crawler_enabled),
|
||
)
|
||
|
||
if platform_code and platform_code != "all":
|
||
adapter = get_adapter(platform_code)
|
||
if not adapter:
|
||
return {
|
||
"platform_code": platform_code,
|
||
"found": False,
|
||
"runs": [],
|
||
"error": "未知平台 adapter",
|
||
}
|
||
adapters = [adapter]
|
||
else:
|
||
adapters = list(registry.values())
|
||
|
||
runner = ManualDiscoveryRunner(
|
||
runtime_status=status,
|
||
http_get=http_get,
|
||
network_allowed_override=mcp_fetch_gate["network_request_allowed"],
|
||
network_gate=mcp_fetch_gate,
|
||
)
|
||
return {
|
||
"platform_code": platform_code or "all",
|
||
"found": True,
|
||
"fetch_requested": bool(fetch),
|
||
"manual_fetch_allowed": self.manual_fetch_allowed(),
|
||
"mcp_fetch_gate": mcp_fetch_gate,
|
||
"runs": [
|
||
runner.run(adapter, fetch=fetch).to_dict()
|
||
for adapter in adapters
|
||
],
|
||
}
|
||
|
||
def build_candidate_preview(
|
||
self,
|
||
platform_code="all",
|
||
*,
|
||
fetch=False,
|
||
min_band="all",
|
||
limit=50,
|
||
http_get=None,
|
||
):
|
||
"""聚合候選連結 preview,只供人工審核,不寫 DB。"""
|
||
discovery_result = self.run_manual_discovery(
|
||
platform_code=platform_code,
|
||
fetch=fetch,
|
||
http_get=http_get,
|
||
)
|
||
preview = build_candidate_preview_from_discovery(
|
||
discovery_result,
|
||
min_band=min_band,
|
||
limit=limit,
|
||
)
|
||
preview["discovery_found"] = bool(discovery_result.get("found"))
|
||
preview["error"] = discovery_result.get("error")
|
||
return preview
|
||
|
||
def build_platform_seed_plan(self, platform_code="all"):
|
||
"""建立 market_platforms 初始資料計畫,不寫入 DB。"""
|
||
status = self.get_runtime_status()
|
||
seed_rows = build_platform_seed_rows(platform_code=platform_code)
|
||
found = bool(seed_rows) or platform_code in (None, "", "all")
|
||
return {
|
||
"platform_code": platform_code or "all",
|
||
"found": found,
|
||
"phase": self.phase,
|
||
"seed_count": len(seed_rows),
|
||
"seeds": seed_rows,
|
||
"would_write_database": False,
|
||
"database_write_allowed": bool(status.database_write_allowed),
|
||
"required_gates": {
|
||
"market_intel_enabled": bool(status.enabled),
|
||
"market_intel_write_enabled": bool(status.write_enabled),
|
||
"schema_smoke_required": True,
|
||
"migration_required": True,
|
||
"manual_operator_approval_required": True,
|
||
},
|
||
"status": status.to_dict(),
|
||
"error": None if found else "未知平台 adapter",
|
||
}
|
||
|
||
def build_platform_seed_write_guard(self, platform_code="all"):
|
||
"""回報 platform seed 寫入前置 gate;本方法不執行寫入。"""
|
||
status = self.get_runtime_status()
|
||
seed_plan = self.build_platform_seed_plan(platform_code=platform_code)
|
||
schema_smoke = build_schema_smoke(MARKET_INTEL_TABLES)
|
||
guard_checks = {
|
||
"seed_plan_found": bool(seed_plan["found"]),
|
||
"has_seed_rows": bool(seed_plan["seed_count"]),
|
||
"market_intel_enabled": bool(status.enabled),
|
||
"market_intel_write_enabled": bool(status.write_enabled),
|
||
"database_write_allowed": bool(status.database_write_allowed),
|
||
"migration_confirmed": False,
|
||
"schema_smoke_confirmed": bool(schema_smoke["passed"]),
|
||
"manual_operator_approval": False,
|
||
}
|
||
blocked_reasons = [
|
||
name for name, passed in guard_checks.items()
|
||
if not passed
|
||
]
|
||
return {
|
||
"platform_code": platform_code or "all",
|
||
"phase": self.phase,
|
||
"seed_count": seed_plan["seed_count"],
|
||
"ready_to_write": False,
|
||
"would_write_database": False,
|
||
"database_write_allowed": bool(status.database_write_allowed),
|
||
"guard_checks": guard_checks,
|
||
"blocked_reasons": blocked_reasons,
|
||
"write_action": "blocked_dry_run_only",
|
||
"schema_smoke": schema_smoke,
|
||
"seed_plan": seed_plan,
|
||
}
|
||
|
||
def build_schema_smoke(self):
|
||
"""回報 market_intel ORM metadata smoke 結果,不查詢 DB。"""
|
||
return {
|
||
"phase": self.phase,
|
||
"schema_smoke": build_schema_smoke(MARKET_INTEL_TABLES),
|
||
"expected_tables": self.get_schema_tables(),
|
||
}
|
||
|
||
def build_schema_db_probe(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""回報正式 DB schema 只讀探針;預設不連 DB。"""
|
||
probe = build_schema_db_probe_plan(
|
||
MARKET_INTEL_TABLES,
|
||
execute_requested=execute_requested,
|
||
engine=engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
probe["phase"] = self.phase
|
||
return probe
|
||
|
||
def build_platform_seed_db_diff(
|
||
self,
|
||
platform_code="all",
|
||
*,
|
||
execute_requested=False,
|
||
engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""回報 platform seed 與 DB 的只讀差異;預設不連 DB。"""
|
||
seed_plan = self.build_platform_seed_plan(platform_code=platform_code)
|
||
diff = build_platform_seed_db_diff_plan(
|
||
seed_plan,
|
||
execute_requested=execute_requested,
|
||
engine=engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
diff["phase"] = self.phase
|
||
diff["platform_code"] = platform_code or "all"
|
||
diff["seed_plan_found"] = bool(seed_plan["found"])
|
||
return diff
|
||
|
||
def build_legacy_source_bridge(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
sample_limit=5,
|
||
engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""回報既有 EDM/PChome 資料源橋接 preview;預設不連 DB。"""
|
||
bridge = build_legacy_source_bridge_plan(
|
||
execute_requested=execute_requested,
|
||
sample_limit=sample_limit,
|
||
engine=engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
bridge["phase"] = self.phase
|
||
return bridge
|
||
|
||
def build_mcp_readiness(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
timeout_sec=3,
|
||
engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
readiness = build_mcp_readiness_plan(
|
||
execute_requested=execute_requested,
|
||
timeout_sec=timeout_sec,
|
||
engine=engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
readiness["phase"] = self.phase
|
||
return readiness
|
||
|
||
def build_mcp_tool_contract(self):
|
||
contract = build_mcp_tool_contract_preview()
|
||
contract["phase"] = self.phase
|
||
return contract
|
||
|
||
def build_mcp_deploy_preflight(self):
|
||
preflight = build_mcp_deploy_preflight_plan()
|
||
preflight["phase"] = self.phase
|
||
return preflight
|
||
|
||
def build_mcp_activation_runbook(self):
|
||
preflight = self.build_mcp_deploy_preflight()
|
||
readiness = self.build_mcp_readiness()
|
||
runbook = build_mcp_activation_runbook_preview(
|
||
preflight=preflight,
|
||
readiness=readiness,
|
||
)
|
||
runbook["phase"] = self.phase
|
||
return runbook
|
||
|
||
def build_mcp_fetch_gate(self, *, fetch_requested=False, execute_readiness=False):
|
||
gate = build_mcp_fetch_gate_preview(
|
||
self.get_runtime_status(),
|
||
fetch_requested=fetch_requested,
|
||
execute_readiness=execute_readiness,
|
||
)
|
||
gate["phase"] = self.phase
|
||
return gate
|
||
|
||
def build_mcp_completion_audit(self):
|
||
return build_mcp_completion_audit_for_runtime(
|
||
runtime_status=self.get_runtime_status(),
|
||
phase=self.phase,
|
||
)
|
||
|
||
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_manual_sample_plan(self):
|
||
"""回報第一次人工 sample fetch 計畫;不抓外部頁、不寫 DB。"""
|
||
plan = build_manual_sample_fetch_plan_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
adapters=get_adapter_registry().values(),
|
||
mcp_fetch_gate=self.build_mcp_fetch_gate(),
|
||
live_db_inventory=self.build_live_db_inventory(),
|
||
)
|
||
plan["phase"] = self.phase
|
||
return plan
|
||
|
||
def build_manual_sample_acceptance(self):
|
||
"""回報人工 sample result 驗收契約;不載入外部結果、不寫 DB。"""
|
||
acceptance = build_manual_sample_acceptance_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
manual_sample_plan=self.build_manual_sample_plan(),
|
||
)
|
||
acceptance["phase"] = self.phase
|
||
return acceptance
|
||
|
||
def build_manual_sample_review(self, sample_result=None):
|
||
"""回報人工 sample result 審核預覽;預設不載入結果、不寫 DB。"""
|
||
review = build_manual_sample_review_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||
sample_result=sample_result,
|
||
)
|
||
review["phase"] = self.phase
|
||
return review
|
||
|
||
def build_manual_sample_review_evaluation(
|
||
self,
|
||
sample_result=None,
|
||
payload_error=None,
|
||
):
|
||
"""回報 POST sample result 即時審核;不保存 payload、不寫 DB。"""
|
||
review = build_manual_sample_review_evaluation_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||
sample_result=sample_result,
|
||
payload_error=payload_error,
|
||
)
|
||
review["phase"] = self.phase
|
||
return review
|
||
|
||
def build_manual_sample_candidate_handoff(
|
||
self,
|
||
sample_result=None,
|
||
payload_error=None,
|
||
limit=20,
|
||
):
|
||
"""回報人工樣本候選活動 handoff;只產生 preview,不寫 DB。"""
|
||
handoff = build_manual_sample_candidate_handoff_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||
sample_result=sample_result,
|
||
payload_error=payload_error,
|
||
limit=limit,
|
||
)
|
||
handoff["phase"] = self.phase
|
||
return handoff
|
||
|
||
def build_manual_sample_candidate_queue_draft(
|
||
self,
|
||
sample_result=None,
|
||
payload_error=None,
|
||
limit=20,
|
||
):
|
||
"""回報人工候選活動審核 queue 草案;只產生 preview,不寫 DB。"""
|
||
queue_draft = build_manual_sample_candidate_queue_draft_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||
sample_result=sample_result,
|
||
payload_error=payload_error,
|
||
limit=limit,
|
||
)
|
||
queue_draft["phase"] = self.phase
|
||
return queue_draft
|
||
|
||
def build_manual_sample_candidate_queue_approval(
|
||
self,
|
||
sample_result=None,
|
||
payload_error=None,
|
||
limit=20,
|
||
):
|
||
"""回報候選審核 queue 寫入前 gate;只產生 preview,不寫 DB。"""
|
||
approval = build_manual_sample_candidate_queue_approval_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||
sample_result=sample_result,
|
||
payload_error=payload_error,
|
||
limit=limit,
|
||
)
|
||
approval["phase"] = self.phase
|
||
return approval
|
||
|
||
def build_manual_sample_candidate_queue_transaction(self, sample_result=None, payload_error=None, limit=20):
|
||
"""回報候選審核 queue transaction preview;不開 transaction、不寫 DB。"""
|
||
transaction = build_manual_sample_candidate_queue_transaction_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||
sample_result=sample_result,
|
||
payload_error=payload_error,
|
||
limit=limit,
|
||
)
|
||
transaction["phase"] = self.phase
|
||
return transaction
|
||
|
||
def build_match_review_plan(self):
|
||
"""回報商品比對審核計畫;不建立 queue、不自動確認。"""
|
||
schema_smoke = self.build_schema_smoke()
|
||
plan = build_match_review_plan_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
mcp_tool_contract=self.build_mcp_tool_contract(),
|
||
legacy_source_bridge=self.build_legacy_source_bridge(),
|
||
schema_smoke={
|
||
**schema_smoke["schema_smoke"],
|
||
"expected_tables": schema_smoke["expected_tables"],
|
||
},
|
||
)
|
||
plan["phase"] = self.phase
|
||
return plan
|
||
|
||
def build_opportunity_plan(self):
|
||
"""回報市場機會與威脅判讀計畫;不派送、不產生 AI 報告。"""
|
||
plan = build_opportunity_plan_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
match_review_plan=self.build_match_review_plan(),
|
||
scheduler_plan=self.build_scheduler_plan(),
|
||
legacy_source_bridge=self.build_legacy_source_bridge(),
|
||
)
|
||
plan["phase"] = self.phase
|
||
return plan
|
||
|
||
def build_opportunity_scoring_plan(self):
|
||
"""回報機會與威脅分數模型計畫;不計分、不寫入。"""
|
||
schema_smoke = self.build_schema_smoke()
|
||
plan = build_opportunity_scoring_plan_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
opportunity_plan=self.build_opportunity_plan(),
|
||
match_review_plan=self.build_match_review_plan(),
|
||
scheduler_plan=self.build_scheduler_plan(),
|
||
schema_smoke=schema_smoke,
|
||
mcp_fetch_gate=self.build_mcp_fetch_gate(),
|
||
)
|
||
plan["phase"] = self.phase
|
||
return plan
|
||
|
||
def build_opportunity_evidence_plan(self):
|
||
"""回報機會與威脅 evidence bundle 計畫;不查詢、不造樣本。"""
|
||
schema_smoke = self.build_schema_smoke()
|
||
plan = build_opportunity_evidence_plan_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
opportunity_plan=self.build_opportunity_plan(),
|
||
scoring_plan=self.build_opportunity_scoring_plan(),
|
||
match_review_plan=self.build_match_review_plan(),
|
||
legacy_source_bridge=self.build_legacy_source_bridge(),
|
||
schema_smoke=schema_smoke,
|
||
)
|
||
plan["phase"] = self.phase
|
||
return plan
|
||
|
||
def build_opportunity_alert_plan(self):
|
||
"""回報機會與威脅告警候選計畫;不派送、不呼叫 LLM。"""
|
||
plan = build_opportunity_alert_plan_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
opportunity_plan=self.build_opportunity_plan(),
|
||
scoring_plan=self.build_opportunity_scoring_plan(),
|
||
evidence_plan=self.build_opportunity_evidence_plan(),
|
||
scheduler_plan=self.build_scheduler_plan(),
|
||
)
|
||
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)
|
||
write_guard = self.build_platform_seed_write_guard(platform_code=platform_code)
|
||
schema_smoke = write_guard["schema_smoke"]
|
||
writer_plan = build_platform_seed_writer_plan(
|
||
seed_plan=seed_plan,
|
||
write_guard=write_guard,
|
||
schema_smoke=schema_smoke,
|
||
)
|
||
writer_plan["phase"] = self.phase
|
||
writer_plan["platform_code"] = platform_code or "all"
|
||
writer_plan["seed_plan_found"] = bool(seed_plan["found"])
|
||
writer_plan["seed_count"] = seed_plan["seed_count"]
|
||
return writer_plan
|
||
|
||
def build_write_approval_runbook(self, platform_code="all"):
|
||
"""建立正式 seed writer 前的人工批准 runbook;本方法不執行寫入。"""
|
||
status = self.get_runtime_status()
|
||
seed_plan = self.build_platform_seed_plan(platform_code=platform_code)
|
||
write_guard = self.build_platform_seed_write_guard(platform_code=platform_code)
|
||
writer_plan = self.build_platform_seed_writer_plan(platform_code=platform_code)
|
||
return build_write_approval_runbook(
|
||
phase=self.phase,
|
||
status=status,
|
||
schema_smoke=write_guard["schema_smoke"],
|
||
seed_plan=seed_plan,
|
||
write_guard=write_guard,
|
||
writer_plan=writer_plan,
|
||
)
|
||
|
||
def build_migration_blueprint(self):
|
||
"""建立 market_intel migration 與 seed writer 命令草案;不執行 SQL。"""
|
||
blueprint = build_migration_blueprint(self.get_schema_tables())
|
||
blueprint["phase"] = self.phase
|
||
return blueprint
|
||
|
||
def build_migration_apply_drill(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
schema_engine=None,
|
||
seed_diff_engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""建立正式 migration 前的只讀演練;不執行 migration 或 rollback。"""
|
||
schema_db_probe = self.build_schema_db_probe(
|
||
execute_requested=execute_requested,
|
||
engine=schema_engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
platform_seed_db_diff = self.build_platform_seed_db_diff(
|
||
execute_requested=execute_requested,
|
||
engine=seed_diff_engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
drill = build_migration_apply_drill_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
migration_blueprint=self.build_migration_blueprint(),
|
||
schema_db_probe=schema_db_probe,
|
||
platform_seed_db_diff=platform_seed_db_diff,
|
||
)
|
||
drill["phase"] = self.phase
|
||
return drill
|
||
|
||
def build_migration_catalog_review(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
schema_engine=None,
|
||
seed_diff_engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""建立正式 DB catalog 判讀;不執行 migration、不寫 DB。"""
|
||
schema_db_probe = self.build_schema_db_probe(
|
||
execute_requested=execute_requested,
|
||
engine=schema_engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
platform_seed_db_diff = self.build_platform_seed_db_diff(
|
||
execute_requested=execute_requested,
|
||
engine=seed_diff_engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
review = build_migration_catalog_review_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
migration_blueprint=self.build_migration_blueprint(),
|
||
schema_db_probe=schema_db_probe,
|
||
platform_seed_db_diff=platform_seed_db_diff,
|
||
)
|
||
review["phase"] = self.phase
|
||
return review
|
||
|
||
def build_migration_live_smoke(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
schema_engine=None,
|
||
seed_diff_engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""建立正式 DB 只讀 smoke;不執行 migration、不寫 DB。"""
|
||
catalog_review = self.build_migration_catalog_review(
|
||
execute_requested=execute_requested,
|
||
schema_engine=schema_engine,
|
||
seed_diff_engine=seed_diff_engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
smoke = build_migration_live_smoke_preview(
|
||
runtime_status=self.get_runtime_status(),
|
||
catalog_review=catalog_review,
|
||
)
|
||
smoke["phase"] = self.phase
|
||
return smoke
|
||
|
||
def build_live_db_inventory(
|
||
self,
|
||
*,
|
||
execute_requested=False,
|
||
engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""建立 market_* 正式 DB 只讀庫存總覽;預設不連 DB。"""
|
||
inventory = build_live_db_inventory_preview(
|
||
self.get_schema_tables(),
|
||
execute_requested=execute_requested,
|
||
engine=engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
inventory["phase"] = self.phase
|
||
return inventory
|
||
|
||
def build_seed_writer_cli_status(
|
||
self,
|
||
platform_code="all",
|
||
*,
|
||
execute_requested=False,
|
||
approval_token=None,
|
||
approval_token_secret=None,
|
||
apply_real_write=False,
|
||
engine=None,
|
||
database_url=None,
|
||
database_type=None,
|
||
):
|
||
"""建立 seed writer CLI status;只有 CLI 明確批准時才寫入。"""
|
||
seed_plan = self.build_platform_seed_plan(platform_code=platform_code)
|
||
write_guard = self.build_platform_seed_write_guard(platform_code=platform_code)
|
||
writer_plan = self.build_platform_seed_writer_plan(platform_code=platform_code)
|
||
migration_blueprint = self.build_migration_blueprint()
|
||
status = build_seed_writer_cli_plan(
|
||
platform_code=platform_code or "all",
|
||
execute_requested=execute_requested,
|
||
approval_token=approval_token,
|
||
approval_token_secret=approval_token_secret,
|
||
apply_real_write=apply_real_write,
|
||
seed_plan=seed_plan,
|
||
write_guard=write_guard,
|
||
writer_plan=writer_plan,
|
||
migration_blueprint=migration_blueprint,
|
||
engine=engine,
|
||
database_url=database_url,
|
||
database_type=database_type,
|
||
)
|
||
status["phase"] = self.phase
|
||
return status
|
||
|
||
def build_deployment_readiness(self):
|
||
"""建立市場情報推版準備狀態;不執行 git、部署或遠端操作。"""
|
||
return build_deployment_readiness_preview(
|
||
service=self,
|
||
market_intel_tables=MARKET_INTEL_TABLES,
|
||
schema_smoke_builder=build_schema_smoke,
|
||
)
|