diff --git a/remote_sync/bot.py b/remote_sync/bot.py index 51224ef..216d731 100755 --- a/remote_sync/bot.py +++ b/remote_sync/bot.py @@ -14,6 +14,7 @@ import re import subprocess import sys import tempfile +import threading import time import urllib.parse import uuid @@ -328,6 +329,16 @@ class OpenClawDingTalkBot(ChatbotHandler): 10, min(int(os.getenv("EXCEL_TEXT_PROMPT_COOLDOWN_SECONDS", "120")), 600), ) + self.excel_alert_log_enabled = bool_env("EXCEL_ALERT_LOG_ENABLED", True) + self.excel_alert_log_path = ( + os.getenv( + "EXCEL_ALERT_LOG_PATH", + "/opt/openclaw-dingtalk-bridge/logs/excel_converter_alerts.log", + ).strip() + or "/opt/openclaw-dingtalk-bridge/logs/excel_converter_alerts.log" + ) + self.excel_alert_detail_limit = max(200, min(int(os.getenv("EXCEL_ALERT_DETAIL_LIMIT", "1600")), 10000)) + self._excel_alert_log_lock = threading.Lock() self._recent_excel_success_ts = {} def _should_handle(self, msg: ChatbotMessage, text: str) -> bool: @@ -2253,6 +2264,71 @@ class OpenClawDingTalkBot(ChatbotHandler): self.logger.exception("upload excel output to s3 failed: %s", exc) return "" + def _excel_error_summary(self, detail: str) -> str: + raw = str(detail or "").strip() + if not raw: + return "unknown_error" + lines = [line.strip() for line in raw.splitlines() if line.strip()] + if not lines: + return raw[:300] + for line in reversed(lines): + lower_line = line.lower() + if lower_line.startswith("traceback"): + continue + if lower_line.startswith("file "): + continue + if lower_line in {"^^^^^^", "^^^^^"}: + continue + return line[:300] + return lines[-1][:300] + + def _record_excel_failure_alert( + self, + msg: ChatbotMessage, + file_name: str, + stage: str, + detail: str, + return_code: int | None = None, + stdout_text: str = "", + stderr_text: str = "", + ) -> None: + normalized_detail = str(detail or "").strip() or "unknown_error" + summary = self._excel_error_summary(normalized_detail) + digest = hashlib.sha1(normalized_detail.encode("utf-8", errors="ignore")).hexdigest()[:12] + log_record = { + "ts": dt.datetime.now().isoformat(timespec="seconds"), + "event": "excel_convert_failed", + "stage": stage, + "digest": digest, + "summary": summary, + "return_code": return_code, + "file_name": str(file_name or ""), + "conversation_id": str(getattr(msg, "conversation_id", "") or ""), + "session_webhook": str(getattr(msg, "session_webhook", "") or ""), + "sender_staff_id": str(getattr(msg, "sender_staff_id", "") or ""), + "detail": normalized_detail[: self.excel_alert_detail_limit], + "stderr": str(stderr_text or "")[: self.excel_alert_detail_limit], + "stdout": str(stdout_text or "")[: self.excel_alert_detail_limit], + } + self.logger.warning( + "excel failure alert stage=%s digest=%s file=%s summary=%s", + stage, + digest, + file_name, + summary, + ) + if not self.excel_alert_log_enabled: + return + try: + parent_dir = os.path.dirname(self.excel_alert_log_path) or "." + os.makedirs(parent_dir, exist_ok=True) + line = json.dumps(log_record, ensure_ascii=False) + with self._excel_alert_log_lock: + with open(self.excel_alert_log_path, "a", encoding="utf-8") as file_obj: + file_obj.write(line + "\n") + except Exception: + self.logger.exception("write excel alert log failed") + def _run_excel_image_link_job(self, msg: ChatbotMessage, file_payload: dict) -> tuple[str, bool]: if not self.excel_link_feature_enabled: return "Excel 图片链接转图片功能未启用。", False @@ -2282,6 +2358,12 @@ class OpenClawDingTalkBot(ChatbotHandler): fh.write(response.content) except Exception as exc: self.logger.exception("download incoming excel failed") + self._record_excel_failure_alert( + msg, + file_name, + stage="download_source_excel", + detail=f"{type(exc).__name__}: {exc}", + ) return f"下载源文件失败:{exc}", False cmd = [ @@ -2312,6 +2394,12 @@ class OpenClawDingTalkBot(ChatbotHandler): timeout=self.excel_converter_timeout, ) except subprocess.TimeoutExpired: + self._record_excel_failure_alert( + msg, + file_name, + stage="excel_converter_timeout", + detail=f"timeout>{self.excel_converter_timeout}s", + ) return f"Excel 转换超时(>{self.excel_converter_timeout}秒),请稍后重试。", False stdout_text = (proc.stdout or "").strip() @@ -2319,6 +2407,15 @@ class OpenClawDingTalkBot(ChatbotHandler): if proc.returncode != 0 or not os.path.exists(output_path): detail = stderr_text or stdout_text or "未知错误" self.logger.error("excel converter failed code=%s detail=%s", proc.returncode, detail[:2500]) + self._record_excel_failure_alert( + msg, + file_name, + stage="excel_converter_failed", + detail=detail, + return_code=proc.returncode, + stdout_text=stdout_text, + stderr_text=stderr_text, + ) return f"Excel 转换失败:{detail[:1200]}", False summary_line = ""