Tighten chart guards and EA envelopes
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-24 23:56:17 +08:00
parent 916ddc010b
commit 0f7ad3e036
12 changed files with 348 additions and 6 deletions

View File

@@ -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()` 成為共用 formatterOpenClaw 週報/日報/月報與競品簡報 data_summary / KPI slide 都讀同一份信封文字,避免策略報告與 PPT 各自翻譯覆核狀態或遺失 HITL guardrails。
- V10.457 將 PChome 覆核 `decision_envelope` 連到人工操作面Dashboard 覆核卡新增決策等級、資料品質、HITL/trace 信封摘要;`/api/export/excel/pchome-review` 匯出同步增加決策信封 ID、決策類型、建議代碼、責任人、資料品質、自動執行允許與證據摘要讓線上操作與下載檔都保留同一份 guardrails。

View File

@@ -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 # 用於模板顯示

View File

@@ -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。

View File

@@ -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 與自動簡報產線

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **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 覆核信封共用摘要 formatterOpenClaw 週報/日報/月報、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。

View File

@@ -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:
"",
"<b>量測指標</b>",
]
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,

View File

@@ -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"⚡ <b>{escape(str(tier_label))}</b>",
@@ -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"🧠 <b>AI 摘要:</b>{safe_ai_summary[:400]}", ""]
if safe_ai_cause:
lines += [f"💡 <b>可能原因:</b>{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:

View File

@@ -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

View File

@@ -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 "🧭 <b>決策信封</b>" in msg
assert "資料品質:<code>complete</code> 自動執行:<b>不允許</b>" 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

View File

@@ -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 "🧭 <b>決策信封</b>" in msg
assert "類型:<code>resource_optimization</code>" in msg
assert "資料品質:<code>complete</code> 自動執行:<b>不允許</b>" in msg
assert "SKU<code>action_plans</code>" 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={

View File

@@ -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) {

View File

@@ -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) {