From b79e5f1a1a4b8e4e3c495c7346346c0740c2df0d Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 26 Mar 2026 15:32:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Telegram=20HTML=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=8C=AF=E8=AA=A4=20+=20=E7=B0=BD=E6=A0=B8=E5=BE=8C=E5=85=A7?= =?UTF-8?q?=E5=AE=B9=E4=BF=9D=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修復: 1. telegram_gateway.py - HTML 轉義 (html.escape) 防止 "Can't parse entities" 2. openclaw-state-machine.tsx - 簽核後顯示結果 2 秒再導航 問題根因: - URL 和用戶輸入內容可能包含 <, >, & 破壞 HTML - 簽核後立即刷新列表,已簽核項目消失 Memory: feedback_approval_preserve_content.md Co-Authored-By: Claude Opus 4.5 --- apps/api/src/services/telegram_gateway.py | 27 +++-- .../components/ai/openclaw-state-machine.tsx | 108 ++++++++++++++---- 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 3ce2726d..c356cf5a 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -17,9 +17,13 @@ SOUL.md 鐵律 (4.1 Telegram 訊息壓縮原則): - 根因摘要: 100 字元 - 建議行動: 50 字元 - 總長度: 800 字元 (v7.0 擴展以容納 SignOz 區塊) + +修復紀錄: +- 2026-03-26 Claude Code: 修復 HTML 解析錯誤 (Can't parse entities) """ import asyncio +import html from dataclasses import dataclass from datetime import UTC, datetime @@ -156,31 +160,38 @@ class TelegramMessage: # 自動生成事件編號 incident_id = self.incident_id or f"INC-{self.approval_id[:8].upper()}" - # SignOz URL (優先使用動態 URL) + # SignOz URL (優先使用動態 URL) - 必須 HTML 轉義防止解析錯誤 service_name = self.resource_name.split("-")[0] if "-" in self.resource_name else self.resource_name - signoz_url = self.signoz_trace_url or f"http://192.168.0.188:3301/traces?service={service_name}" + raw_url = self.signoz_trace_url or f"http://192.168.0.188:3301/traces?service={service_name}" + signoz_url = html.escape(raw_url, quote=True) # SignOz 指標區塊 signoz_block = "" if self.signoz_metrics: signoz_block = f"━━━━━━━━━━━━━━━━━━━\n{self.signoz_metrics.format()}\n" + # HTML 轉義用戶輸入內容,防止 "Can't parse entities" 錯誤 + safe_resource = html.escape(self.resource_name[:35]) + safe_root_cause = html.escape(self.root_cause[:50]) + safe_action = html.escape(self.suggested_action[:35]) + safe_downtime = html.escape(self.estimated_downtime) + # 組裝訊息 message = ( f"═══════════════════════════\n" - f"{self.status_emoji} {self.risk_level} | {self.resource_name[:25]}\n" + f"{self.status_emoji} {html.escape(self.risk_level)} | {html.escape(self.resource_name[:25])}\n" f"═══════════════════════════\n" - f"📋 {incident_id}\n" - f"🎯 資源: {self.resource_name[:35]}\n" + f"📋 {html.escape(incident_id)}\n" + f"🎯 資源: {safe_resource}\n" f"━━━━━━━━━━━━━━━━━━━\n" f"🤖 AI 仲裁判定\n" f"👥 責任: {resp_display}\n" f"📊 信心: {conf_emoji} {confidence_pct}%\n" - f"💡 原因: {self.root_cause[:50]}\n" + f"💡 原因: {safe_root_cause}\n" f"{signoz_block}" f"━━━━━━━━━━━━━━━━━━━\n" - f"🔧 建議: {self.suggested_action[:35]}\n" - f"⏱️ 停機: {self.estimated_downtime}\n" + f"🔧 建議: {safe_action}\n" + f"⏱️ 停機: {safe_downtime}\n" f"🔍 查看 SignOz Trace (±5min)" ) diff --git a/apps/web/src/components/ai/openclaw-state-machine.tsx b/apps/web/src/components/ai/openclaw-state-machine.tsx index 9a89ff2b..7a73c1b2 100644 --- a/apps/web/src/components/ai/openclaw-state-machine.tsx +++ b/apps/web/src/components/ai/openclaw-state-machine.tsx @@ -107,6 +107,14 @@ export function OpenClawStateMachine({ const [selectedIndex, setSelectedIndex] = useState(null) const selectedApproval = selectedIndex !== null ? pendingApprovals[selectedIndex] : null + // 2026-03-26: 簽核後保留內容顯示 (feedback_approval_preserve_content.md) + // 儲存剛簽核/拒絕的項目,顯示結果後再清除 + const [processedApproval, setProcessedApproval] = useState<{ + approval: ApprovalRequest + action: 'approved' | 'rejected' + timestamp: Date + } | null>(null) + // Timer refs for cleanup const pollTimerRef = useRef(null) @@ -416,35 +424,95 @@ export function OpenClawStateMachine({ {/* Approval Modal - 全屏審核對話框 (Phase 17 UI/UX 修復) */} setSelectedIndex(null)} - title="審核詳情" - onPrev={handlePrevApproval} - onNext={handleNextApproval} - current={selectedIndex !== null ? selectedIndex + 1 : undefined} - total={pendingApprovals.length} + open={selectedIndex !== null || processedApproval !== null} + onClose={() => { + setSelectedIndex(null) + setProcessedApproval(null) + }} + title={processedApproval ? (processedApproval.action === 'approved' ? '✅ 已批准' : '❌ 已拒絕') : '審核詳情'} + onPrev={processedApproval ? undefined : handlePrevApproval} + onNext={processedApproval ? undefined : handleNextApproval} + current={selectedIndex !== null && !processedApproval ? selectedIndex + 1 : undefined} + total={!processedApproval ? pendingApprovals.length : undefined} > - {selectedApproval && ( + {/* 2026-03-26: 顯示剛處理的項目結果 */} + {processedApproval && ( +
+ {/* 結果覆蓋層 */} +
+
+
+ {processedApproval.action === 'approved' ? '✅' : '❌'} +
+
+ {processedApproval.action === 'approved' ? '已批准執行' : '已拒絕執行'} +
+
+ {processedApproval.timestamp.toLocaleTimeString()} +
+
+
+ {/* 原始內容 (保留顯示) */} + {}} + onReject={async () => {}} + holdDuration={1000} + fullHeight + /> +
+ )} + {/* 正常待審核項目 */} + {selectedApproval && !processedApproval && ( { + // 2026-03-26: 保留簽核後內容顯示 2 秒 + setProcessedApproval({ + approval: selectedApproval, + action: 'approved', + timestamp: new Date(), + }) + const newCount = await handleApprove(selectedApproval.id) ?? 0 - // 簽核後: 根據 API 返回的新數量決定導航 - if (newCount > 0 && selectedIndex !== null) { - // 還有待審核項目,保持 Modal 開啟 - // 如果當前 index 超出範圍,調整到最後一筆 - if (selectedIndex >= newCount) { - setSelectedIndex(newCount - 1) + + // 顯示結果 2 秒後導航 + setTimeout(() => { + setProcessedApproval(null) + if (newCount > 0 && selectedIndex !== null) { + if (selectedIndex >= newCount) { + setSelectedIndex(newCount - 1) + } + } else { + setSelectedIndex(null) } - // 否則保持 index 不變,列表刷新後自動顯示下一筆 - } else { - // 沒有待審核項目了,關閉 Modal - setSelectedIndex(null) - } + }, 2000) }} onReject={async () => { + // 2026-03-26: 保留拒絕後內容顯示 2 秒 + setProcessedApproval({ + approval: selectedApproval, + action: 'rejected', + timestamp: new Date(), + }) + await handleReject(selectedApproval.id) - setSelectedIndex(null) + + // 顯示結果 2 秒後關閉 + setTimeout(() => { + setProcessedApproval(null) + setSelectedIndex(null) + }, 2000) }} holdDuration={1000} fullHeight