diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 888c495..23198a2 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.460 收斂 daily/growth 圖表空白誤判與 ElephantAlpha 告警信封:`page-daily-sales.js`、`page-growth.js` 的 chart 判斷改為至少有一個非零資料點才繪製 Chart.js,避免全 0 序列只畫座標軸;`resource_optimization` / `ea_escalation` 改輸出 deterministic `decision_envelope`,只使用 action_plans、CPU 實測與 hygiene evidence,不再輸出空泛「48 小時效益」敘事。 - V10.459 強化 PChome `protected_existing_match` 決策封包:解析 `existing_match_conflict` 的既有候選、新候選與雙方 score,寫入 `decision_envelope.evidence` / `expected_impact` / `guardrails`,並把下一步明確標成「比較既有正式候選與新候選」;仍保持 `can_auto_execute=false`,避免新候選分數較高時繞過人工覆核自動覆蓋正式價差。 - V10.458 將 OpenClaw / 競品 PPT 接上 PChome 覆核 `decision_envelope` 摘要:`competitor_intel_repository.summarize_review_decision_envelopes()` 成為共用 formatter,OpenClaw 週報/日報/月報與競品簡報 data_summary / KPI slide 都讀同一份信封文字,避免策略報告與 PPT 各自翻譯覆核狀態或遺失 HITL guardrails。 - V10.457 將 PChome 覆核 `decision_envelope` 連到人工操作面:Dashboard 覆核卡新增決策等級、資料品質、HITL/trace 信封摘要;`/api/export/excel/pchome-review` 匯出同步增加決策信封 ID、決策類型、建議代碼、責任人、資料品質、自動執行允許與證據摘要,讓線上操作與下載檔都保留同一份 guardrails。 diff --git a/config.py b/config.py index d0da8b2..4bac58d 100644 --- a/config.py +++ b/config.py @@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.459" +SYSTEM_VERSION = "V10.460" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index d172050..8afae9b 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-24 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 備援預設關閉 -> **適用版本**: V10.459 +> **適用版本**: V10.460 --- @@ -50,6 +50,7 @@ - PChome 覆核隊列本身也必須輸出 `decision_envelope`:`fetch_competitor_review_queue()`、`fetch_competitor_review_queue_page()` 與 `/api/pchome-review/queue` 的每筆候選需帶相同的 `subject`、`evidence`、`recommended_action`、`expected_impact` 與 `guardrails`,供 Dashboard、Agent、Telegram 與 PPT 共用;任何下游不得另寫一套比價狀態翻譯或繞過 HITL guardrails。 - Dashboard 覆核卡與 `/api/export/excel/pchome-review` 也必須顯示/匯出 `decision_envelope` 的等級、資料品質、建議代碼、HITL、trace 與 `can_auto_execute=false` 邊界;操作員離開系統畫面或下載 Excel 後,仍要看得到「不可自動寫正式價差」的 guardrails。 - OpenClaw 週報/日報/月報與 competitor PPT 不得再各自重算或翻譯 PChome 覆核狀態;必須透過 `competitor_intel_repository.summarize_review_decision_envelopes()` 讀取同一份 `decision_envelope` 摘要,並在 prompt / data_summary / KPI slide 保留 HITL 與 `can_auto_execute=false` 邊界。 +- ElephantAlpha 的 `resource_optimization` 與低信心 `ea_escalation` 也必須輸出 `decision_envelope`:資源壓力信封只能使用 `action_plans`、CPU 實測、hygiene 結果與 insight/action trace,不得加入 LLM 預測效益;`triaged_alert()` 對 `ea_escalation` 亦需渲染信封並以 `decision_id` 作為 callback 追蹤 ID。 ## 一、四 AI Agent 路由架構 @@ -148,6 +149,7 @@ SQL漏斗(~300筆) - ElephantAlpha 價格類 trigger 的 HITL / 決策 prefetch 必須先使用觸發 SQL 與 `competitor_prices` / `price_records` 的 DB 實證生成 SKU、MOMO / PChome 價差與建議 action lines;完整 Hermes LLM prefetch 預設關閉(`ELEPHANT_ALPHA_HERMES_LLM_PREFETCH_ENABLED=false`),避免 5s timeout 後落入無實證摘要或雲端備援。若無 DB 實證,只記錄 suppressed telemetry / cooldown,不發 Telegram 空告警。 - ElephantAlpha 協調器收到非純 JSON、fenced JSON 或混文字 JSON 時,必須先做容錯抽取;仍無法解析時,只能使用 DB/Hermes 實證生成保守 HITL fallback。fallback 不得放入 OpenClaw `generate_*` 類舊策略步驟,也不得暗示已自動調價。 - `resource_optimization` 不再交給 LLM 生成「預期效益 / 已執行」敘事,顯示名稱統一為「資源壓力治理」。此 trigger 必須先由程式量測 `action_plans` backlog、P1/P2 數、pending_review、逾時項目與 CPU load;只有 CPU 達門檻、P1/P2 積壓或逾時積壓才發 Telegram「資源壓力告警」。單純 queue 大但 CPU 正常只記錄 telemetry,不派發 Hermes/NemoTron、不宣稱 48 小時效益;Telegram 段落使用「系統處置紀錄」而非泛稱「已執行」,避免暗示 AI 已完成未經驗證的外部動作。 +- `resource_optimization` 的 Telegram 必須包含 `decision_envelope` 區塊,標明 `source_agent=elephant_alpha`、資料品質、量測證據、`can_auto_execute=false` 與 deterministic trace;此路徑不呼叫 Gemini、不呼叫 Hermes/NemoTron,也不得把 queue backlog 翻譯成主機資源耗盡。 - `resource_optimization` 會先執行 `ActionPlanHygieneService` 清理過期噪音:只關閉超過 72 小時的 `code_review_fix` / `openclaw_recommendation` 類 advisory action_plans,以及 NemoTron `direct_response/reply_simple` 舊聊天回覆計畫;將狀態改為 `auto_disabled` 或 `rejected` 並寫入 `metadata_json.hygiene_history`。不刪資料,也不碰 NemoTron human_review / pricing / tool action 類業務行動。 - `momo-scheduler` 每 6 小時固定執行 `run_action_plan_hygiene_task()`,讓過期 advisory action_plans 的關閉不再依賴 `resource_optimization` 告警觸發;排程失敗會經 EventRouter 發送 `action_plan_hygiene_failure`。 - `action_plans` 產生端必須防重:Code Review 同一檔案已有 active `code_review_fix` 時不重建;OpenClaw recommendation 會寫入文字 fingerprint 並跳過同一建議;AIOrchestrator 不再把 NemoTron `direct_response/reply_simple` 聊天回覆存成 action plan,真正需工具、審核或執行的 NemoTron action 才能進 queue。 diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index ae7af01..da11098 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -49,6 +49,7 @@ - 2026-05-24 23:15 CST 起,Dashboard 覆核卡與 PChome 覆核 Excel 匯出也顯示/輸出信封摘要、資料品質、HITL、trace、自動執行阻擋原因與證據摘要;下載檔不得丟失 guardrails。 - 2026-05-24 23:25 CST 起,OpenClaw 週報/日報/月報與 competitor PPT 使用 `summarize_review_decision_envelopes()` 的同一份 HITL 信封摘要,不再手寫 attempt_status 統計或自行翻譯覆核狀態。 - 2026-05-24 23:40 CST 起,`compare_existing_identity` 成為 `protected_existing_match` 的明確建議動作;Agent 只能提示「比較既有正式候選與新候選」,不得因新候選分數較高自動寫正式價差。 +- 2026-05-24 23:55 CST 起,ElephantAlpha `resource_optimization` / `ea_escalation` 都必須帶 deterministic `decision_envelope`;Telegram 按鈕 callback 使用 `decision_id`,證據只允許 action_plans、CPU 實測、hygiene 與 trigger trace。 - 告警不得再輸出空泛「預期效益」;必須帶資料品質、證據來源、HITL 邊界與 trace id。 - Agent 建議只能輔助排序與分析,不得繞過 matcher / feeder / review service 寫正式價格。 @@ -58,6 +59,7 @@ - `/daily_sales`、`/growth_analysis` 圖表不得空白;需保留原本圖表並升級成更專業的呈現。 - 圖表需通過 runtime nonblank canvas 檢查與手機版 responsive。 - daily/growth/PPT 必須共用 `competitor_intel_repository` 的比價資料出口,避免價差方向或統計口徑分裂。 +- 2026-05-24 23:55 CST 起,daily/growth chart 判斷不再只看 series 長度;若序列全 0,顯示 chart-empty 狀態而不是畫只有座標軸的假圖。正式 smoke 需跑 `scripts/check_sales_charts_runtime.js` 確認主要 canvas 非空。 ## 5. PPT 視覺 QA 與自動簡報產線 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index e065e10..24de9b3 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -13,6 +13,7 @@ ## 📅 詳細更新日誌 (考古存檔) ### 2026-05-24:PChome 近門檻身份回收第二輪 +- **V10.460 ElephantAlpha 告警決策信封**: `resource_optimization` 會為資源壓力告警產生 deterministic `decision_envelope`,證據只來自 `action_plans`、CPU 實測與 hygiene 結果,Telegram 同時顯示決策信封、量測指標、判讀、系統處置與下一步;`ea_escalation` 模板也會渲染信封並使用 `decision_id` 作為 `momo:eig:*` callback,避免低信心升級告警只剩空泛文字或不可追蹤按鈕。 - **V10.459 protected_existing_match 決策封包**: PChome 覆核信封開始解析 `existing_match_conflict`,把既有正式候選、新候選、雙方 matcher score 與 score delta 寫入 evidence / expected_impact / guardrails;新候選即使分數較高也維持 `can_auto_execute=false`,但 OpenClaw、PPT、Dashboard 與人工覆核可清楚看見該比較哪兩個候選。 - **V10.458 OpenClaw / PPT 決策信封摘要**: 新增 `summarize_review_decision_envelopes()` 作為 PChome 覆核信封共用摘要 formatter;OpenClaw 週報/日報/月報、OpenClaw Bot competitor PPT data_summary 與 PPT KPI slide 都使用同一份 HITL / 資料品質 / action / trace 摘要,不再各自手寫 attempt_status 翻譯。 - **V10.457 Dashboard / Excel 決策信封連動**: 商品看板 PChome 覆核卡顯示 `decision_envelope` 的決策等級、資料品質、HITL 與 trace;`/api/export/excel/pchome-review` 匯出新增決策信封 ID、建議代碼、責任人、資料品質、自動執行允許、阻擋原因與證據摘要,讓下載檔仍保留不可自動寫正式價差的 guardrails。 diff --git a/services/elephant_alpha_autonomous_engine.py b/services/elephant_alpha_autonomous_engine.py index 5336626..300496b 100644 --- a/services/elephant_alpha_autonomous_engine.py +++ b/services/elephant_alpha_autonomous_engine.py @@ -1107,12 +1107,114 @@ class ElephantAlphaAutonomousEngine: actions.append("確認 action_plans 來源是否持續產生重複建議;若是報表型建議,應改為摘要消化而非逐筆告警。") return actions + @staticmethod + def _resource_pressure_severity(metrics: Dict[str, Any]) -> str: + level = str((metrics or {}).get("pressure_level") or "unknown") + if level == "critical": + return "P1" + if level == "warning": + return "P2" + if level == "backlog_only": + return "P3" + return "P4" + + @staticmethod + def _build_resource_pressure_decision_envelope( + metrics: Dict[str, Any], + insight_id: Optional[int], + previous_limit: int, + new_limit: int, + ) -> Dict[str, Any]: + metrics = dict(metrics or {}) + load_pct = float(metrics.get("system_load_pct") or 0.0) + hygiene = metrics.get("hygiene_result") if isinstance(metrics.get("hygiene_result"), dict) else {} + hygiene_count = int((hygiene or {}).get("updated_count") or 0) + data_quality = "partial" if metrics.get("query_error") else "complete" + deadline = (datetime.now() + timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S") + + evidence = [ + { + "type": "queue", + "metric": "action_queue_size", + "value": f"{int(metrics.get('action_queue_size') or 0)}/{int(metrics.get('queue_threshold') or RESOURCE_QUEUE_THRESHOLD)}", + "basis": "action_plans pending / auto_pending / pending_review", + "confidence": 1.0 if data_quality == "complete" else 0.7, + }, + { + "type": "priority", + "metric": "p1_p2_pending", + "value": f"{int(metrics.get('high_priority_count') or 0)}/{int(metrics.get('high_priority_threshold') or RESOURCE_HIGH_PRIORITY_THRESHOLD)}", + "basis": "priority <= 2", + "confidence": 1.0 if data_quality == "complete" else 0.7, + }, + { + "type": "resource", + "metric": "cpu_load_pct", + "value": f"{load_pct:.1f}%/{float(metrics.get('load_threshold_pct') or RESOURCE_LOAD_THRESHOLD_PCT):.0f}%", + "basis": "local system load measurement", + "confidence": 0.9, + }, + ] + if int(metrics.get("stale_count") or 0) > 0: + evidence.append({ + "type": "stale", + "metric": "stale_action_plans", + "value": f"{int(metrics.get('stale_count') or 0)}/{int(metrics.get('stale_threshold') or RESOURCE_STALE_THRESHOLD)}", + "basis": f"age >= {int(metrics.get('stale_hours') or RESOURCE_STALE_HOURS)}h", + "confidence": 1.0 if data_quality == "complete" else 0.7, + }) + + return { + "decision_id": f"ea_resource_pressure_{insight_id or int(datetime.now().timestamp())}", + "decision_type": "resource_optimization", + "source_agent": "elephant_alpha", + "severity": ElephantAlphaAutonomousEngine._resource_pressure_severity(metrics), + "confidence": float(metrics.get("confidence") or 0.86), + "analysis": ( + "由 action_plans 與 CPU 實測值判定資源壓力;" + "未使用 LLM 生成效益預測,也未啟動價格分析。" + ), + "subject": { + "sku": "action_plans", + "name": "Elephant Alpha 資源壓力治理", + }, + "evidence": evidence, + "recommended_action": { + "action": "human_review_backlog_triage", + "owner": "ops", + "deadline": deadline, + "requires_hitl": True, + }, + "expected_impact": { + "risk_reduction": ( + "reduce stale high-priority backlog" + if metrics.get("should_alert") or hygiene_count > 0 + else "observe only" + ), + }, + "guardrails": { + "can_auto_execute": False, + "blocked_reason": ( + "resource_optimization 只允許清理過期 advisory action_plans;" + "外部修復、價格分析與策略派發需人工覆核" + ), + "data_quality": data_quality, + "llm_used": False, + }, + "trace": { + "insight_id": insight_id, + "model": "deterministic_metrics", + "provider": "action_plans_cpu_probe", + }, + } + @staticmethod def _build_resource_pressure_telegram_message( metrics: Dict[str, Any], insight_id: Optional[int], previous_limit: int, new_limit: int, + decision_envelope: Optional[Dict[str, Any]] = None, ) -> str: level = str(metrics.get("pressure_level", "unknown")) level_label = { @@ -1156,6 +1258,13 @@ class ElephantAlphaAutonomousEngine: "", "量測指標", ] + if decision_envelope: + try: + from services.telegram_templates import _format_decision_envelope + + lines[4:4] = ["", *_format_decision_envelope(decision_envelope)] + except Exception: + pass if pre_hygiene: lines.append( "• 清理前 Action queue:" @@ -1217,6 +1326,12 @@ class ElephantAlphaAutonomousEngine: insight_id, previous_limit, new_limit, + decision_envelope=self._build_resource_pressure_decision_envelope( + metrics, + insight_id, + previous_limit, + new_limit, + ), ) await self._run_with_timeout(_send_telegram_raw, msg, timeout=10) self._log.info("Resource pressure Telegram sent: level=%s", metrics.get("pressure_level")) @@ -1699,6 +1814,72 @@ class ElephantAlphaAutonomousEngine: exc_info=True, ) + @staticmethod + def _build_human_escalation_decision_envelope( + decision: StrategicDecision, + trigger: AutonomousTrigger, + *, + insight_id: Optional[int] = None, + concrete_actions: Optional[List[str]] = None, + ) -> Dict[str, Any]: + concrete_actions = [str(item).strip() for item in (concrete_actions or []) if str(item).strip()] + data_quality = "complete" if concrete_actions else "partial" + trigger_label = _zh_trigger(trigger.trigger_type) + evidence = [ + { + "type": "confidence", + "metric": "decision_confidence", + "value": f"{float(decision.confidence or 0):.2f}", + "basis": "ElephantAlpha confidence below autonomous threshold", + "confidence": float(decision.confidence or 0), + }, + { + "type": "trigger", + "metric": "trigger_type", + "value": trigger.trigger_type, + "basis": trigger_label, + }, + ] + for idx, action in enumerate(concrete_actions[:3], start=1): + evidence.append({ + "type": "action_candidate", + "metric": f"candidate_{idx}", + "value": action[:120], + "basis": "DB/Hermes concrete evidence" if trigger.trigger_type in _PRICE_RELATED_TRIGGERS else "trigger evidence", + }) + + return { + "decision_id": f"ea_review_{insight_id or int(datetime.now().timestamp())}", + "decision_type": "ea_escalation", + "source_agent": "elephant_alpha", + "severity": "P2" if data_quality == "complete" else "P3", + "confidence": float(decision.confidence or 0), + "analysis": "低信心自主決策已轉人工覆核;未批准前不執行外部副作用。", + "subject": { + "sku": trigger.trigger_type, + "name": f"Elephant Alpha · {trigger_label}", + }, + "evidence": evidence, + "recommended_action": { + "action": "human_review", + "owner": "ops", + "requires_hitl": True, + }, + "expected_impact": { + "risk_reduction": "prevent low-confidence autonomous execution", + }, + "guardrails": { + "can_auto_execute": False, + "blocked_reason": "L3 HITL required; no automatic execution before approval", + "data_quality": data_quality, + }, + "trace": { + "insight_id": insight_id, + "model": "deterministic_escalation_gate", + "provider": "elephant_alpha", + }, + } + async def _escalate_to_human(self, decision: StrategicDecision, trigger: AutonomousTrigger) -> None: self._log.warning("Escalating to human: %s", trigger.trigger_type) concrete_actions = ( @@ -1717,6 +1898,7 @@ class ElephantAlphaAutonomousEngine: self._record_suppressed_escalation(decision, trigger, "no_concrete_evidence") return + insight_id = None session = get_session() try: row = session.execute( @@ -1744,6 +1926,7 @@ class ElephantAlphaAutonomousEngine: ).fetchone() session.commit() if row: + insight_id = row[0] try: from services.openclaw_learning_service import enqueue_insight_embedding enqueue_insight_embedding( @@ -1803,6 +1986,12 @@ class ElephantAlphaAutonomousEngine: ) try: + decision_envelope = self._build_human_escalation_decision_envelope( + decision, + trigger, + insight_id=insight_id, + concrete_actions=concrete_actions, + ) msg, keyboard = triaged_alert( base_event={ "event_type": "ea_escalation", @@ -1811,7 +2000,8 @@ class ElephantAlphaAutonomousEngine: f"自主決策信心度 {decision.confidence:.2f} 低於門檻,需人工批准" + ("" if concrete_actions else "(⚠️ 無實證數據)") ), - "id": f"ea_review_{int(datetime.now().timestamp())}", + "id": decision_envelope.get("decision_id"), + "decision_envelope": decision_envelope, }, tier_label="🐘 Elephant Alpha · L3 HITL", ai_summary=ai_summary_text, diff --git a/services/telegram_templates.py b/services/telegram_templates.py index 9940647..4c98974 100644 --- a/services/telegram_templates.py +++ b/services/telegram_templates.py @@ -648,6 +648,7 @@ def _format_ea_escalation_alert( generic_actions = [item for item in parsed_actions if not _is_ea_sku_action(item)] shown_actions = sku_actions[:5] hidden_count = max(0, len(sku_actions) - len(shown_actions)) + decision_envelope = base_event.get("decision_envelope") or base_event.get("decision") lines = [ f"⚡ {escape(str(tier_label))}", @@ -661,6 +662,9 @@ def _format_ea_escalation_alert( for part in cause_parts[:3]: lines.append(f"• {part}") + if isinstance(decision_envelope, dict) and decision_envelope: + lines += ["", *_format_decision_envelope(decision_envelope)] + if ai_summary: lines += [ "", @@ -824,6 +828,9 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str, title = escape(str(base_event.get("title", ""))) summary = escape(str(base_event.get("summary", ""))) event_id = base_event.get("id") + decision_envelope = base_event.get("decision_envelope") or base_event.get("decision") + if not event_id and isinstance(decision_envelope, dict): + event_id = decision_envelope.get("decision_id") safe_ai_summary = escape(str(ai_summary or "")) safe_ai_cause = escape(str(ai_cause or "")) if ai_cause else None safe_actions = [escape(str(a)) for a in (ai_actions or [])] @@ -849,7 +856,6 @@ def triaged_alert(base_event: Dict[str, Any], tier_label: str, lines += [f"🧠 AI 摘要:{safe_ai_summary[:400]}", ""] if safe_ai_cause: lines += [f"💡 可能原因:{safe_ai_cause}", ""] - decision_envelope = base_event.get("decision_envelope") or base_event.get("decision") if isinstance(decision_envelope, dict): lines += _format_decision_envelope(decision_envelope) if not event_id: diff --git a/tests/test_chart_fallback_contract.py b/tests/test_chart_fallback_contract.py index 97f97d3..4ee07d0 100644 --- a/tests/test_chart_fallback_contract.py +++ b/tests/test_chart_fallback_contract.py @@ -14,6 +14,7 @@ def test_daily_sales_canvas_is_primary_and_fallback_is_opt_in(): assert ".chart-container:not(.chart-fallback-active) .chart-fallback-list" in css assert "node.content && node.content.textContent" in script assert "chart-empty-active" in script + assert "Math.abs(Number(value)) > 1e-9" in script render_body = script.split("function renderAllCharts()", 1)[1].split("function bootCharts()", 1)[0] assert "renderHtmlChartFallbacks();" not in render_body assert "catch(error =>" in script @@ -30,6 +31,7 @@ def test_growth_analysis_canvas_is_primary_and_fallback_is_opt_in(): assert ".ga-chart-card__body:not(.chart-fallback-active) .ga-chart-snapshot" in css assert "node.content && node.content.textContent" in script assert "chart-empty-active" in script + assert "Math.abs(Number(value)) > 1e-9" in script render_body = script.split("function renderCharts()", 1)[1].split("function bootCharts()", 1)[0] assert "renderHtmlChartFallbacks();" not in render_body assert "catch(error =>" in script diff --git a/tests/test_elephant_alpha_engine.py b/tests/test_elephant_alpha_engine.py index 25d97b1..5c12ced 100644 --- a/tests/test_elephant_alpha_engine.py +++ b/tests/test_elephant_alpha_engine.py @@ -373,6 +373,49 @@ def test_resource_pressure_message_is_measurement_based_not_llm_theatre(): assert "48 小時效益預測" in msg +def test_resource_pressure_decision_envelope_is_measurement_based(): + from services.elephant_alpha_autonomous_engine import ElephantAlphaAutonomousEngine + + metrics = ElephantAlphaAutonomousEngine._classify_resource_pressure({ + "action_queue_size": 34, + "high_priority_count": 8, + "human_review_count": 6, + "stale_count": 5, + "system_load_pct": 22.0, + "queue_threshold": 10, + "load_threshold_pct": 80, + "high_priority_threshold": 5, + "stale_threshold": 5, + "stale_hours": 24, + }) + + envelope = ElephantAlphaAutonomousEngine._build_resource_pressure_decision_envelope( + metrics, + insight_id=1942, + previous_limit=10, + new_limit=8, + ) + msg = ElephantAlphaAutonomousEngine._build_resource_pressure_telegram_message( + metrics, + insight_id=1942, + previous_limit=10, + new_limit=8, + decision_envelope=envelope, + ) + + assert envelope["decision_id"] == "ea_resource_pressure_1942" + assert envelope["source_agent"] == "elephant_alpha" + assert envelope["decision_type"] == "resource_optimization" + assert envelope["severity"] == "P2" + assert envelope["guardrails"]["can_auto_execute"] is False + assert envelope["guardrails"]["llm_used"] is False + assert envelope["trace"]["provider"] == "action_plans_cpu_probe" + assert "🧭 決策信封" in msg + assert "資料品質:complete 自動執行:不允許" in msg + assert "不採用 LLM 生成的 48 小時效益預測" in msg + assert "Gemini" not in msg + + def test_resource_optimization_bypasses_llm_orchestrator(monkeypatch): import services.elephant_alpha_autonomous_engine as engine_module from services.elephant_alpha_autonomous_engine import ( @@ -495,6 +538,44 @@ def test_resource_optimization_cannot_use_legacy_autonomous_execution_template(m assert sent == [] +def test_human_escalation_decision_envelope_blocks_auto_execution(): + from services.elephant_alpha_autonomous_engine import ( + AutonomousTrigger, + ElephantAlphaAutonomousEngine, + ) + from services.elephant_alpha_orchestrator import StrategicDecision + + decision = StrategicDecision( + priority="medium", + agents_required=["elephant_alpha"], + reasoning="需要人工判讀。", + expected_outcome="不自動執行。", + confidence=0.62, + execution_plan=[], + resource_requirements={}, + ) + trigger = AutonomousTrigger( + trigger_type="code_exception", + conditions={"scan_containers": ["momo-pro-system"]}, + threshold=1.0, + enabled=True, + ) + + envelope = ElephantAlphaAutonomousEngine._build_human_escalation_decision_envelope( + decision, + trigger, + insight_id=77, + concrete_actions=[], + ) + + assert envelope["decision_id"] == "ea_review_77" + assert envelope["decision_type"] == "ea_escalation" + assert envelope["guardrails"]["can_auto_execute"] is False + assert envelope["guardrails"]["data_quality"] == "partial" + assert envelope["recommended_action"]["requires_hitl"] is True + assert envelope["trace"]["provider"] == "elephant_alpha" + + def test_elephant_alpha_openclaw_registry_is_ollama_first(): from services.elephant_alpha_orchestrator import ElephantAlphaOrchestrator diff --git a/tests/test_telegram_triaged_alert_format.py b/tests/test_telegram_triaged_alert_format.py index 1224d9f..55ca288 100644 --- a/tests/test_telegram_triaged_alert_format.py +++ b/tests/test_telegram_triaged_alert_format.py @@ -126,6 +126,57 @@ def test_ea_escalation_generic_actions_do_not_render_as_sku_cards(): assert "未取得實證前,不執行自動調價、修復或策略派發" in msg +def test_ea_escalation_renders_decision_envelope_and_uses_decision_id_callback(): + msg, keyboard = triaged_alert( + base_event={ + "event_type": "ea_escalation", + "title": "🐘 EA 升級審核 · 資源壓力治理", + "summary": "自主決策信心度 0.82 低於門檻,需人工批准", + "decision_envelope": { + "decision_id": "ea_resource_pressure_1942", + "decision_type": "resource_optimization", + "source_agent": "elephant_alpha", + "severity": "P2", + "confidence": 0.86, + "subject": { + "sku": "action_plans", + "name": "Elephant Alpha 資源壓力治理", + }, + "evidence": [ + { + "type": "queue", + "metric": "action_queue_size", + "value": "7/10", + "basis": "action_plans pending / pending_review", + "confidence": 1.0, + }, + ], + "recommended_action": { + "action": "human_review_backlog_triage", + "owner": "ops", + "requires_hitl": True, + }, + "guardrails": { + "can_auto_execute": False, + "blocked_reason": "resource_optimization requires HITL", + "data_quality": "complete", + }, + }, + }, + tier_label="🐘 Elephant Alpha · L3 HITL", + ai_summary="只採用 action_plans 與 CPU 實測值。", + ai_cause="觸發類型:資源壓力治理 | 信心度:0.86", + ai_actions=["檢查 priority <= 2 的 action_plans"], + ) + + assert "🧭 決策信封" in msg + assert "類型:resource_optimization" in msg + assert "資料品質:complete 自動執行:不允許" in msg + assert "SKU:action_plans" in msg + assert "human_review_backlog_triage" in msg + assert keyboard["inline_keyboard"][0][0]["callback_data"] == "momo:eig:ea_resource_pressure_1942" + + def test_triaged_alert_renders_decision_envelope_contract(): msg, keyboard = triaged_alert( base_event={ diff --git a/web/static/js/page-daily-sales.js b/web/static/js/page-daily-sales.js index ecf1564..56ea9fa 100644 --- a/web/static/js/page-daily-sales.js +++ b/web/static/js/page-daily-sales.js @@ -219,7 +219,10 @@ function hasSeriesData(labels, ...seriesList) { return Array.isArray(labels) && labels.length > 0 && - seriesList.some(series => Array.isArray(series) && series.length > 0); + seriesList.some(series => + Array.isArray(series) && + series.some(value => Number.isFinite(Number(value)) && Math.abs(Number(value)) > 1e-9) + ); } function renderChartEmpty(canvasId, message) { diff --git a/web/static/js/page-growth.js b/web/static/js/page-growth.js index 58b673f..bef17fb 100644 --- a/web/static/js/page-growth.js +++ b/web/static/js/page-growth.js @@ -135,7 +135,10 @@ function hasSeriesData(labels, ...seriesList) { return Array.isArray(labels) && labels.length > 0 && - seriesList.some(series => Array.isArray(series) && series.length > 0); + seriesList.some(series => + Array.isArray(series) && + series.some(value => Number.isFinite(Number(value)) && Math.abs(Number(value)) > 1e-9) + ); } function renderChartEmpty(canvasId, message) {