Files
ewoooc/services/market_intel/opportunity_alerts.py
OoO 96533a1c20
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
feat(market-intel): add alert review queue contract
2026-05-18 19:41:35 +08:00

348 lines
11 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.
"""市場情報機會與威脅告警候選 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",
],
}