348 lines
11 KiB
Python
348 lines
11 KiB
Python
"""市場情報機會與威脅告警候選 preview。
|
||
|
||
只定義未來 alert candidate、Telegram 與 AI 摘要的 gate、去重與節流;
|
||
不建立 queue、不寫 DB、不派送訊息、不呼叫 LLM。
|
||
"""
|
||
|
||
|
||
ALERT_CHANNELS = (
|
||
{
|
||
"key": "operator_review",
|
||
"label": "人工審核清單",
|
||
"minimum_level": "watch",
|
||
"requires_operator_approval": False,
|
||
"dispatch_target": "market_intel_review_queue",
|
||
},
|
||
{
|
||
"key": "daily_digest",
|
||
"label": "每日市場情報摘要",
|
||
"minimum_level": "medium",
|
||
"requires_operator_approval": True,
|
||
"dispatch_target": "openclaw_daily_digest",
|
||
},
|
||
{
|
||
"key": "telegram_candidate",
|
||
"label": "Telegram 候選告警",
|
||
"minimum_level": "high",
|
||
"requires_operator_approval": True,
|
||
"dispatch_target": "telegram_market_intel_channel",
|
||
},
|
||
{
|
||
"key": "ai_briefing_candidate",
|
||
"label": "AI 摘要候選",
|
||
"minimum_level": "medium",
|
||
"requires_operator_approval": True,
|
||
"dispatch_target": "hermes_openclaw_briefing",
|
||
},
|
||
)
|
||
|
||
ALERT_GATES = (
|
||
{
|
||
"key": "evidence_bundle_ready",
|
||
"label": "必須有完整 evidence bundle",
|
||
"required_for": ["watch", "medium", "high", "critical"],
|
||
},
|
||
{
|
||
"key": "score_threshold_met",
|
||
"label": "分數需達 channel 最低門檻",
|
||
"required_for": ["medium", "high", "critical"],
|
||
},
|
||
{
|
||
"key": "dedupe_key_clear",
|
||
"label": "同日同商品同活動不得重複派送",
|
||
"required_for": ["medium", "high", "critical"],
|
||
},
|
||
{
|
||
"key": "freshness_window_valid",
|
||
"label": "價格與活動快照需在 freshness window 內",
|
||
"required_for": ["medium", "high", "critical"],
|
||
},
|
||
{
|
||
"key": "operator_approval",
|
||
"label": "人工批准後才可進 Telegram 或 AI 摘要候選",
|
||
"required_for": ["high", "critical"],
|
||
},
|
||
)
|
||
|
||
THROTTLE_POLICY = {
|
||
"dedupe_window_hours": 24,
|
||
"max_telegram_candidates_per_day": 12,
|
||
"max_critical_candidates_per_hour": 3,
|
||
"max_ai_briefing_items_per_digest": 20,
|
||
}
|
||
|
||
REVIEW_STATES = (
|
||
{
|
||
"key": "draft",
|
||
"label": "候選草案",
|
||
"dispatch_allowed": False,
|
||
"description": "由 scoring / evidence 形成候選,但尚未進人工審核。",
|
||
},
|
||
{
|
||
"key": "needs_review",
|
||
"label": "待人工審核",
|
||
"dispatch_allowed": False,
|
||
"description": "資料完整但尚未批准,不得推送或產生 AI 摘要。",
|
||
},
|
||
{
|
||
"key": "approved_for_digest",
|
||
"label": "批准進每日摘要",
|
||
"dispatch_allowed": False,
|
||
"description": "只可進每日摘要候選,仍不得直接推 Telegram。",
|
||
},
|
||
{
|
||
"key": "approved_for_telegram",
|
||
"label": "批准進 Telegram 候選",
|
||
"dispatch_allowed": True,
|
||
"description": "仍需經 dispatch gate 與 throttle,再進下一階段。",
|
||
},
|
||
{
|
||
"key": "rejected",
|
||
"label": "駁回",
|
||
"dispatch_allowed": False,
|
||
"description": "保留審核理由,不再升級。",
|
||
},
|
||
{
|
||
"key": "deferred",
|
||
"label": "延後觀察",
|
||
"dispatch_allowed": False,
|
||
"description": "等待新價格快照或活動狀態更新。",
|
||
},
|
||
)
|
||
|
||
REVIEW_ACTIONS = (
|
||
{
|
||
"key": "approve_digest",
|
||
"from": ["needs_review"],
|
||
"to": "approved_for_digest",
|
||
"requires_reason": True,
|
||
},
|
||
{
|
||
"key": "approve_telegram",
|
||
"from": ["needs_review", "approved_for_digest"],
|
||
"to": "approved_for_telegram",
|
||
"requires_reason": True,
|
||
},
|
||
{
|
||
"key": "reject",
|
||
"from": ["draft", "needs_review", "approved_for_digest"],
|
||
"to": "rejected",
|
||
"requires_reason": True,
|
||
},
|
||
{
|
||
"key": "defer",
|
||
"from": ["needs_review", "approved_for_digest"],
|
||
"to": "deferred",
|
||
"requires_reason": True,
|
||
},
|
||
)
|
||
|
||
REVIEW_QUEUE_CONTRACT = {
|
||
"table_name": "market_alert_review_queue",
|
||
"primary_key": "id",
|
||
"required_fields": [
|
||
"alert_candidate_id",
|
||
"review_state",
|
||
"priority_lane",
|
||
"threshold_level",
|
||
"total_score",
|
||
"evidence_bundle_id",
|
||
"dedupe_key",
|
||
"source_batch_id",
|
||
"created_at",
|
||
"updated_at",
|
||
],
|
||
"audit_fields": [
|
||
"reviewer_identity",
|
||
"review_action",
|
||
"review_reason",
|
||
"reviewed_at",
|
||
"previous_state",
|
||
"next_state",
|
||
],
|
||
"forbidden_fields": [
|
||
"customer_personal_data",
|
||
"login_required_price",
|
||
"cart_or_order_data",
|
||
"payment_or_shipping_data",
|
||
],
|
||
}
|
||
|
||
REVIEW_QUEUE_INDEXES = (
|
||
{
|
||
"key": "idx_market_alert_review_queue_state_priority",
|
||
"columns": ["review_state", "priority_lane", "created_at"],
|
||
"purpose": "審核台依狀態與優先級快速排序。",
|
||
},
|
||
{
|
||
"key": "ux_market_alert_review_queue_dedupe",
|
||
"columns": ["dedupe_key"],
|
||
"purpose": "避免同商品同活動同日重複進入審核佇列。",
|
||
},
|
||
{
|
||
"key": "idx_market_alert_review_queue_bundle",
|
||
"columns": ["evidence_bundle_id", "source_batch_id"],
|
||
"purpose": "回查 evidence bundle 與來源批次。",
|
||
},
|
||
)
|
||
|
||
REVIEW_PRIORITY_LANES = (
|
||
{
|
||
"key": "critical",
|
||
"label": "立即審核",
|
||
"max_age_hours": 2,
|
||
"minimum_threshold": "critical",
|
||
},
|
||
{
|
||
"key": "high",
|
||
"label": "今日優先",
|
||
"max_age_hours": 8,
|
||
"minimum_threshold": "high",
|
||
},
|
||
{
|
||
"key": "medium",
|
||
"label": "每日摘要候選",
|
||
"max_age_hours": 24,
|
||
"minimum_threshold": "medium",
|
||
},
|
||
{
|
||
"key": "watch",
|
||
"label": "觀察池",
|
||
"max_age_hours": 72,
|
||
"minimum_threshold": "watch",
|
||
},
|
||
)
|
||
|
||
|
||
def build_opportunity_alert_plan_preview(
|
||
*,
|
||
runtime_status,
|
||
opportunity_plan,
|
||
scoring_plan,
|
||
evidence_plan,
|
||
scheduler_plan,
|
||
):
|
||
"""建立告警候選與派送 gate 計畫;不建立或派送任何告警。"""
|
||
opportunity_preview_safe = (
|
||
opportunity_plan.get("mode") == "opportunity_plan_preview"
|
||
and not opportunity_plan.get("opportunity_queue_created")
|
||
and not opportunity_plan.get("threat_alert_dispatched")
|
||
and not opportunity_plan.get("database_write_executed")
|
||
)
|
||
scoring_preview_safe = (
|
||
scoring_plan.get("mode") == "opportunity_scoring_plan_preview"
|
||
and not scoring_plan.get("score_calculation_executed")
|
||
and not scoring_plan.get("scoring_job_created")
|
||
and not scoring_plan.get("database_write_executed")
|
||
)
|
||
evidence_preview_safe = (
|
||
evidence_plan.get("mode") == "opportunity_evidence_plan_preview"
|
||
and not evidence_plan.get("evidence_bundle_created")
|
||
and not evidence_plan.get("evidence_query_executed")
|
||
and not evidence_plan.get("database_write_executed")
|
||
)
|
||
scheduler_preview_safe = (
|
||
scheduler_plan.get("mode") == "scheduler_attach_plan_preview"
|
||
and not scheduler_plan.get("scheduler_registration_executed")
|
||
and not scheduler_plan.get("crawler_job_started")
|
||
)
|
||
gate_checks = {
|
||
"opportunity_plan_preview_safe": opportunity_preview_safe,
|
||
"scoring_plan_preview_safe": scoring_preview_safe,
|
||
"evidence_plan_preview_safe": evidence_preview_safe,
|
||
"scheduler_plan_preview_safe": scheduler_preview_safe,
|
||
"evidence_bundle_ready": bool(
|
||
evidence_plan.get("ready_for_evidence_bundle")
|
||
),
|
||
"scoring_job_ready": bool(scoring_plan.get("ready_for_scoring_job")),
|
||
"opportunity_queue_ready": bool(
|
||
opportunity_plan.get("ready_for_opportunity_queue")
|
||
),
|
||
"database_write_still_blocked": not bool(
|
||
runtime_status.database_write_allowed
|
||
),
|
||
"operator_approval": False,
|
||
}
|
||
blocked_reasons = [
|
||
key for key, passed in gate_checks.items()
|
||
if not passed
|
||
]
|
||
|
||
return {
|
||
"mode": "opportunity_alert_plan_preview",
|
||
"ready_for_alert_candidates": False,
|
||
"alert_candidate_created": False,
|
||
"alert_queue_created": False,
|
||
"review_queue_created": False,
|
||
"review_queue_contract_defined": True,
|
||
"review_queue_contract_written": False,
|
||
"review_queue_table_created": False,
|
||
"review_action_executed": False,
|
||
"approval_record_written": False,
|
||
"telegram_dispatched": False,
|
||
"ai_summary_generated": False,
|
||
"llm_call_executed": False,
|
||
"notification_sent": 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,
|
||
"channel_count": len(ALERT_CHANNELS),
|
||
"channels": list(ALERT_CHANNELS),
|
||
"alert_gates": list(ALERT_GATES),
|
||
"review_state_count": len(REVIEW_STATES),
|
||
"review_states": list(REVIEW_STATES),
|
||
"review_actions": list(REVIEW_ACTIONS),
|
||
"review_queue_contract": REVIEW_QUEUE_CONTRACT,
|
||
"review_queue_indexes": list(REVIEW_QUEUE_INDEXES),
|
||
"review_priority_lanes": list(REVIEW_PRIORITY_LANES),
|
||
"throttle_policy": THROTTLE_POLICY,
|
||
"gate_checks": gate_checks,
|
||
"blocked_reasons": blocked_reasons,
|
||
"approval_policy": {
|
||
"manual_approval_required_for_dispatch": True,
|
||
"approval_reason_required": True,
|
||
"reviewer_identity_required": True,
|
||
"approved_candidate_still_requires_dispatch_gate": True,
|
||
"queue_item_requires_evidence_bundle": True,
|
||
"queue_item_requires_dedupe_key": True,
|
||
},
|
||
"payload_contract": {
|
||
"required_fields": [
|
||
"alert_candidate_id",
|
||
"rule_key",
|
||
"threshold_level",
|
||
"total_score",
|
||
"bundle_id",
|
||
"source_batch_id",
|
||
"dedupe_key",
|
||
"created_at",
|
||
],
|
||
"forbidden_fields": [
|
||
"customer_personal_data",
|
||
"login_required_price",
|
||
"cart_or_order_data",
|
||
],
|
||
},
|
||
"operator_sequence": [
|
||
"先由 evidence bundle 產生 alert candidate 草案",
|
||
"依 throttle_policy 去重與節流,不直接推播",
|
||
"medium 以上進每日摘要候選,high 以上才可進 Telegram 候選",
|
||
"人工批准後才允許 AI 摘要或 Telegram 候選進入下一階段",
|
||
"所有派送內容必須附 bundle id、source batch id 與 dedupe key",
|
||
],
|
||
"safe_boundaries": [
|
||
"do_not_create_alert_queue_from_preview",
|
||
"do_not_create_review_queue_table_from_preview",
|
||
"do_not_write_review_queue_contract_from_preview",
|
||
"do_not_dispatch_telegram_from_alert_preview",
|
||
"do_not_call_llm_from_alert_preview",
|
||
"do_not_notify_without_evidence_bundle",
|
||
"do_not_send_duplicate_alerts",
|
||
"do_not_include_personal_or_login_required_data",
|
||
],
|
||
}
|