From 3873928b66ab71779957e6c9ec69fec0357fd186 Mon Sep 17 00:00:00 2001 From: Mayorz Date: Wed, 4 Mar 2026 17:08:55 +0800 Subject: [PATCH] fix excel file message parsing and dingtalk fileType reply --- remote_sync/bot.py | 128 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/remote_sync/bot.py b/remote_sync/bot.py index 216d731..ff48474 100755 --- a/remote_sync/bot.py +++ b/remote_sync/bot.py @@ -1446,16 +1446,37 @@ class OpenClawDingTalkBot(ChatbotHandler): return codes def _normalize_file_name(self, raw_name: str) -> str: - name = str(raw_name or "").strip().replace("\\", "_").replace("/", "_") - name = re.sub(r"\s+", "_", name) - name = re.sub(r"[^0-9A-Za-z._-]+", "_", name).strip("._") - if not name: - return f"upload_{int(time.time())}.xlsx" - return name[:120] + raw = str(raw_name or "").strip().replace("\\", "_").replace("/", "_") + raw = re.sub(r"\s+", "_", raw) + stem, ext = os.path.splitext(raw) + safe_stem = re.sub(r"[^0-9A-Za-z._-]+", "_", stem).strip("._-") + safe_ext = re.sub(r"[^0-9A-Za-z.]+", "", ext).lower() + if safe_ext and not safe_ext.startswith("."): + safe_ext = f".{safe_ext}" + if not safe_stem: + safe_stem = f"upload_{int(time.time())}" + normalized = f"{safe_stem}{safe_ext}" if safe_ext else safe_stem + return normalized[:120] def _is_supported_excel_file(self, file_name: str) -> bool: return str(file_name or "").strip().lower().endswith(".xlsx") + def _is_supported_excel_payload(self, file_name: str, file_type: str) -> bool: + if self._is_supported_excel_file(file_name): + return True + normalized = str(file_type or "").strip().lower() + if not normalized: + return False + normalized = normalized.split(";")[0].strip() + if normalized in { + "xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }: + return True + if "spreadsheetml.sheet" in normalized: + return True + return False + def _is_excel_link_prompt(self, prompt: str) -> bool: raw = str(prompt or "").strip().lower() if not raw: @@ -1480,16 +1501,45 @@ class OpenClawDingTalkBot(ChatbotHandler): continue if isinstance(current, dict): lowered = {str(k).lower(): k for k in current.keys()} + code = "" if "downloadcode" in lowered: raw_code = current.get(lowered["downloadcode"]) code = str(raw_code or "").strip() - if code: - file_name = "" - for candidate in ("fileName", "filename", "file_name", "name", "title"): - if candidate in current and str(current.get(candidate) or "").strip(): - file_name = str(current.get(candidate)).strip() - break - return {"download_code": code, "file_name": file_name} + + download_url = "" + for key_name in ( + "downloadurl", + "download_url", + "privatedownloadurl", + "private_download_url", + "privateurl", + ): + if key_name in lowered: + raw_url = current.get(lowered[key_name]) + candidate_url = str(raw_url or "").strip() + if candidate_url.lower().startswith(("http://", "https://")): + download_url = candidate_url + break + + file_name = "" + for candidate in ("fileName", "filename", "file_name", "name", "title"): + if candidate in current and str(current.get(candidate) or "").strip(): + file_name = str(current.get(candidate)).strip() + break + + file_type = "" + for candidate in ("fileType", "filetype", "mimeType", "mimetype", "suffix", "ext", "extension"): + if candidate in current and str(current.get(candidate) or "").strip(): + file_type = str(current.get(candidate)).strip() + break + + if code or download_url: + return { + "download_code": code, + "download_url": download_url, + "file_name": file_name, + "file_type": file_type, + } for child in current.values(): stack.append(child) continue @@ -1509,7 +1559,16 @@ class OpenClawDingTalkBot(ChatbotHandler): payload = None if not payload: return {} + + download_code = str(payload.get("download_code") or "").strip() + download_url = str(payload.get("download_url") or "").strip() + if download_url and not download_url.lower().startswith(("http://", "https://")): + download_url = "" + if not download_code and not download_url: + return {} + file_name = payload.get("file_name") or "" + file_type = payload.get("file_type") or "" if not file_name and isinstance(callback_data, dict): for key in ("fileName", "filename", "name", "title"): raw = callback_data.get(key) @@ -1517,9 +1576,21 @@ class OpenClawDingTalkBot(ChatbotHandler): file_name = str(raw).strip() if file_name: break - payload["file_name"] = self._normalize_file_name(file_name or "upload.xlsx") - if not self._is_supported_excel_file(payload["file_name"]): + if not file_type and isinstance(callback_data, dict): + for key in ("fileType", "filetype", "mimeType", "mimetype", "suffix", "ext", "extension"): + raw = callback_data.get(key) + if raw: + file_type = str(raw).strip() + if file_type: + break + normalized_file_name = self._normalize_file_name(file_name or "upload.xlsx") + normalized_file_type = str(file_type or "").strip().lower() + if not self._is_supported_excel_payload(normalized_file_name, normalized_file_type): return {} + payload["download_code"] = download_code + payload["download_url"] = download_url + payload["file_name"] = normalized_file_name + payload["file_type"] = normalized_file_type return payload def _guess_image_ext(self, content_type: str, source_url: str) -> str: @@ -2200,9 +2271,11 @@ class OpenClawDingTalkBot(ChatbotHandler): raw = (proc.stdout or "").strip() return f"结果解析失败: {exc}.\n原始输出: {raw[:500]}" - def _reply_file_by_media_id(self, msg: ChatbotMessage, media_id: str) -> bool: + def _reply_file_by_media_id(self, msg: ChatbotMessage, media_id: str, file_name: str = "") -> bool: if not media_id: return False + safe_name = self._normalize_file_name(file_name or f"excel_{int(time.time())}.xlsx") + file_ext = os.path.splitext(safe_name)[1].lower().lstrip(".") or "file" request_headers = { "Content-Type": "application/json", "Accept": "*/*", @@ -2211,6 +2284,8 @@ class OpenClawDingTalkBot(ChatbotHandler): "msgtype": "file", "file": { "mediaId": media_id, + "fileName": safe_name, + "fileType": file_ext, }, "at": { "atUserIds": [msg.sender_staff_id] if msg.sender_staff_id else [], @@ -2337,15 +2412,17 @@ class OpenClawDingTalkBot(ChatbotHandler): return f"转换脚本不存在:{script_path}", False download_code = str(file_payload.get("download_code") or "").strip() + download_url = str(file_payload.get("download_url") or "").strip() file_name = self._normalize_file_name(file_payload.get("file_name") or "upload.xlsx") - if not download_code: - return "未获取到文件下载码,请重试上传。", False + if not download_code and not download_url: + return "未获取到文件下载地址,请重试上传。", False if not self._is_supported_excel_file(file_name): return "当前仅支持 .xlsx 文件,请上传Excel后重试。", False - download_url = self.get_image_download_url(download_code) if not download_url: - return "文件下载地址获取失败,请重试。", False + download_url = self.get_image_download_url(download_code) + if not download_url: + return "文件下载地址获取失败,请重试。", False with tempfile.TemporaryDirectory(prefix="dingtalk_excel_link_") as tmpdir: input_path = os.path.join(tmpdir, file_name) @@ -2437,7 +2514,7 @@ class OpenClawDingTalkBot(ChatbotHandler): except Exception as exc: self.logger.exception("upload converted excel failed") - sent = self._reply_file_by_media_id(msg, media_id) if media_id else False + sent = self._reply_file_by_media_id(msg, media_id, output_name) if media_id else False if not sent: s3_url = self._upload_excel_output_to_s3(output_path, output_name) if s3_url: @@ -2588,6 +2665,15 @@ class OpenClawDingTalkBot(ChatbotHandler): bool(incoming_image_codes), len(text or ""), ) + if str(getattr(msg, "message_type", "")).lower() == "file" and not has_excel_file: + callback_keys = [] + if isinstance(callback.data, dict): + callback_keys = list(callback.data.keys())[:15] + self.logger.warning( + "file message without excel payload: callback_keys=%s extensions_keys=%s", + callback_keys, + list((getattr(msg, "extensions", {}) or {}).keys())[:15], + ) if not text and not incoming_image_codes and not has_excel_file: return AckMessage.STATUS_OK, "ignored"