Files
ewoooc/services/market_intel/mcp_manual_fetch_handoff.py
OoO 768127ac52
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
V10.405 add market intel MCP manual fetch handoff gate
2026-05-24 13:15:14 +08:00

231 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""市場情報 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,
}