231 lines
8.4 KiB
Python
231 lines
8.4 KiB
Python
"""市場情報 MCP manual fetch handoff preview。
|
||
|
||
本模組只審核 runtime promotion 與操作員安全確認是否足以進入人工
|
||
fetch gate review;不打開 fetch、不連外、不開 DB、不保存 payload、不掛排程。
|
||
"""
|
||
|
||
from services.market_intel.mcp_fetch_gate import build_mcp_fetch_gate_preview
|
||
from services.market_intel.mcp_runtime_promotion import (
|
||
build_mcp_runtime_promotion_preview,
|
||
)
|
||
|
||
|
||
ACKNOWLEDGEMENT_LABELS = {
|
||
"public_pages_only": "只允許公開頁面與公開結構化資料",
|
||
"respect_rate_limit": "每平台需遵守節流與 timeout / retry ceiling",
|
||
"no_login_no_antibot": "不得登入、不得處理會員資料、不得繞反爬或帳號池",
|
||
"no_database_write": "本階段不得寫 DB,正式入庫需另走 writer gate",
|
||
"no_scheduler_attach": "不得掛上正式 scheduler,排程掛載另行批准",
|
||
"manual_fetch_requires_separate_execution": "manual fetch 需另由人工命令或後續 gate 執行",
|
||
}
|
||
|
||
_BLOCKED_SIDE_EFFECT_KEYS = (
|
||
"api_executes_health_check",
|
||
"api_executes_docker",
|
||
"api_executes_ssh",
|
||
"api_opens_database_connection",
|
||
"api_writes_database",
|
||
"api_uses_external_network",
|
||
"database_session_created",
|
||
"database_write_executed",
|
||
"database_commit_executed",
|
||
"external_network_executed",
|
||
"scheduler_attached",
|
||
"writes_executed",
|
||
"would_write_database",
|
||
)
|
||
|
||
|
||
def _default_runtime_status(phase):
|
||
return {
|
||
"phase": phase,
|
||
"enabled": False,
|
||
"crawler_enabled": False,
|
||
"write_enabled": False,
|
||
"dry_run_only": True,
|
||
"scheduler_attached": False,
|
||
"database_write_allowed": False,
|
||
}
|
||
|
||
|
||
def _sample_handoff_package(promotion_review):
|
||
return {
|
||
"promotion_package": promotion_review["sample_promotion_package"],
|
||
"operator_acknowledgements": {
|
||
key: True for key in ACKNOWLEDGEMENT_LABELS
|
||
},
|
||
}
|
||
|
||
|
||
def _promotion_review_from_inputs(promotion_package, promotion_review, phase):
|
||
if isinstance(promotion_review, dict) and promotion_review:
|
||
return promotion_review
|
||
|
||
promotion_package = promotion_package or {}
|
||
return build_mcp_runtime_promotion_preview(
|
||
activation_evidence=promotion_package.get("activation_evidence", {}),
|
||
runtime_receipt=promotion_package.get("runtime_receipt", {}),
|
||
phase=phase,
|
||
)
|
||
|
||
|
||
def _side_effect_free(payload):
|
||
return all(not payload.get(key) for key in _BLOCKED_SIDE_EFFECT_KEYS)
|
||
|
||
|
||
def build_mcp_manual_fetch_handoff_preview(
|
||
*,
|
||
promotion_package=None,
|
||
promotion_review=None,
|
||
operator_acknowledgements=None,
|
||
runtime_status=None,
|
||
phase=None,
|
||
):
|
||
"""建立 manual fetch handoff review;只輸出審核結果,不執行 fetch。"""
|
||
promotion_package = promotion_package or {}
|
||
operator_acknowledgements = operator_acknowledgements or {}
|
||
review = _promotion_review_from_inputs(
|
||
promotion_package,
|
||
promotion_review,
|
||
phase,
|
||
)
|
||
fetch_gate = build_mcp_fetch_gate_preview(
|
||
runtime_status or _default_runtime_status(phase),
|
||
fetch_requested=False,
|
||
execute_readiness=False,
|
||
)
|
||
fetch_gate["phase"] = phase
|
||
|
||
handoff_payload_received = bool(
|
||
promotion_package or promotion_review or operator_acknowledgements
|
||
)
|
||
acknowledgement_status = {
|
||
key: {
|
||
"label": label,
|
||
"acknowledged": bool(operator_acknowledgements.get(key)),
|
||
}
|
||
for key, label in ACKNOWLEDGEMENT_LABELS.items()
|
||
}
|
||
acknowledgement_complete = all(
|
||
item["acknowledged"] for item in acknowledgement_status.values()
|
||
)
|
||
promotion_accepted = bool(review.get("runtime_promotion_accepted"))
|
||
fetch_gate_closed = bool(
|
||
not fetch_gate.get("manual_fetch_gate_open")
|
||
and not fetch_gate.get("network_request_allowed")
|
||
and not fetch_gate.get("external_network_executed")
|
||
)
|
||
side_effect_free = bool(
|
||
_side_effect_free(review)
|
||
and _side_effect_free(fetch_gate)
|
||
)
|
||
|
||
gates = [
|
||
{
|
||
"key": "runtime_promotion_payload_or_review_received",
|
||
"passed": bool(promotion_package or promotion_review),
|
||
"label": "已提供 runtime promotion package 或已審核結果",
|
||
},
|
||
{
|
||
"key": "runtime_promotion_accepted",
|
||
"passed": promotion_accepted,
|
||
"label": "runtime promotion 已通過,可進下一段人工交接",
|
||
},
|
||
{
|
||
"key": "operator_acknowledgements_complete",
|
||
"passed": acknowledgement_complete,
|
||
"label": "操作員已確認公開頁面、節流、無登入、無 DB、無排程邊界",
|
||
},
|
||
{
|
||
"key": "fetch_gate_preview_present",
|
||
"passed": bool(fetch_gate.get("mode") == "mcp_fetch_gate_planned"),
|
||
"label": "已產生 fetch gate planned preview",
|
||
},
|
||
{
|
||
"key": "fetch_gate_remains_closed_by_api",
|
||
"passed": fetch_gate_closed,
|
||
"label": "本 API 不打開 fetch、不允許 network request",
|
||
},
|
||
{
|
||
"key": "handoff_side_effect_free",
|
||
"passed": side_effect_free,
|
||
"label": "handoff 審核不打 health、不開 DB、不連外、不掛 scheduler",
|
||
},
|
||
]
|
||
gates.extend(
|
||
{
|
||
"key": f"ack_{key}",
|
||
"passed": item["acknowledged"],
|
||
"label": item["label"],
|
||
}
|
||
for key, item in acknowledgement_status.items()
|
||
)
|
||
blocked_reasons = [
|
||
gate["key"] for gate in gates
|
||
if not gate["passed"]
|
||
]
|
||
accepted = bool(handoff_payload_received and not blocked_reasons)
|
||
|
||
return {
|
||
"mode": (
|
||
"mcp_manual_fetch_handoff_review"
|
||
if handoff_payload_received
|
||
else "mcp_manual_fetch_handoff_preview"
|
||
),
|
||
"phase": phase,
|
||
"handoff_payload_received": handoff_payload_received,
|
||
"promotion_payload_received": bool(promotion_package or promotion_review),
|
||
"operator_acknowledgements_received": bool(operator_acknowledgements),
|
||
"runtime_promotion_accepted": promotion_accepted,
|
||
"operator_acknowledgements_complete": acknowledgement_complete,
|
||
"manual_fetch_handoff_accepted": accepted,
|
||
"ready_for_manual_fetch_gate_operator_review": accepted,
|
||
"manual_fetch_gate_opened_by_api": False,
|
||
"network_request_allowed": False,
|
||
"fetch_executed": False,
|
||
"gate_count": len(gates),
|
||
"passed_gate_count": sum(1 for gate in gates if gate["passed"]),
|
||
"blocked_reasons": blocked_reasons,
|
||
"gates": gates,
|
||
"operator_acknowledgement_status": acknowledgement_status,
|
||
"promotion_summary": {
|
||
"mode": review.get("mode"),
|
||
"accepted": promotion_accepted,
|
||
"passed_gate_count": review.get("passed_gate_count", 0),
|
||
"gate_count": review.get("gate_count", 0),
|
||
"blocked_reasons": review.get("blocked_reasons", []),
|
||
"ready_for_manual_fetch_gate_review": bool(
|
||
review.get("ready_for_manual_fetch_gate_review")
|
||
),
|
||
},
|
||
"fetch_gate_summary": {
|
||
"mode": fetch_gate.get("mode"),
|
||
"manual_fetch_gate_open": bool(fetch_gate.get("manual_fetch_gate_open")),
|
||
"network_request_allowed": bool(fetch_gate.get("network_request_allowed")),
|
||
"blocked_reasons": fetch_gate.get("blocked_reasons", []),
|
||
},
|
||
"mcp_runtime_promotion": review,
|
||
"mcp_fetch_gate": fetch_gate,
|
||
"sample_handoff_package": _sample_handoff_package(review),
|
||
"next_operator_steps": [
|
||
"若 handoff 通過,只代表可進入人工 fetch gate 審核,不代表 API 可直接抓取",
|
||
"操作員仍需確認公開頁面範圍、限速策略、紀錄保存與回退方式",
|
||
"正式 DB write、scheduler attach、Telegram/AI 摘要仍需各自獨立 gate",
|
||
],
|
||
"payload_persisted": False,
|
||
"handoff_persisted": False,
|
||
"api_executes_health_check": False,
|
||
"api_executes_docker": False,
|
||
"api_executes_ssh": False,
|
||
"api_opens_database_connection": False,
|
||
"api_writes_database": False,
|
||
"api_uses_external_network": False,
|
||
"database_session_created": False,
|
||
"database_write_executed": False,
|
||
"database_commit_executed": False,
|
||
"external_network_executed": False,
|
||
"scheduler_attached": False,
|
||
"writes_executed": False,
|
||
"would_write_database": False,
|
||
}
|