fix excel file message parsing and dingtalk fileType reply

This commit is contained in:
Mayorz 2026-03-04 17:08:55 +08:00
parent fa631100ba
commit 3873928b66

View File

@ -1446,16 +1446,37 @@ class OpenClawDingTalkBot(ChatbotHandler):
return codes return codes
def _normalize_file_name(self, raw_name: str) -> str: def _normalize_file_name(self, raw_name: str) -> str:
name = str(raw_name or "").strip().replace("\\", "_").replace("/", "_") raw = str(raw_name or "").strip().replace("\\", "_").replace("/", "_")
name = re.sub(r"\s+", "_", name) raw = re.sub(r"\s+", "_", raw)
name = re.sub(r"[^0-9A-Za-z._-]+", "_", name).strip("._") stem, ext = os.path.splitext(raw)
if not name: safe_stem = re.sub(r"[^0-9A-Za-z._-]+", "_", stem).strip("._-")
return f"upload_{int(time.time())}.xlsx" safe_ext = re.sub(r"[^0-9A-Za-z.]+", "", ext).lower()
return name[:120] 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: def _is_supported_excel_file(self, file_name: str) -> bool:
return str(file_name or "").strip().lower().endswith(".xlsx") 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: def _is_excel_link_prompt(self, prompt: str) -> bool:
raw = str(prompt or "").strip().lower() raw = str(prompt or "").strip().lower()
if not raw: if not raw:
@ -1480,16 +1501,45 @@ class OpenClawDingTalkBot(ChatbotHandler):
continue continue
if isinstance(current, dict): if isinstance(current, dict):
lowered = {str(k).lower(): k for k in current.keys()} lowered = {str(k).lower(): k for k in current.keys()}
code = ""
if "downloadcode" in lowered: if "downloadcode" in lowered:
raw_code = current.get(lowered["downloadcode"]) raw_code = current.get(lowered["downloadcode"])
code = str(raw_code or "").strip() code = str(raw_code or "").strip()
if code:
file_name = "" download_url = ""
for candidate in ("fileName", "filename", "file_name", "name", "title"): for key_name in (
if candidate in current and str(current.get(candidate) or "").strip(): "downloadurl",
file_name = str(current.get(candidate)).strip() "download_url",
break "privatedownloadurl",
return {"download_code": code, "file_name": file_name} "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(): for child in current.values():
stack.append(child) stack.append(child)
continue continue
@ -1509,7 +1559,16 @@ class OpenClawDingTalkBot(ChatbotHandler):
payload = None payload = None
if not payload: if not payload:
return {} 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_name = payload.get("file_name") or ""
file_type = payload.get("file_type") or ""
if not file_name and isinstance(callback_data, dict): if not file_name and isinstance(callback_data, dict):
for key in ("fileName", "filename", "name", "title"): for key in ("fileName", "filename", "name", "title"):
raw = callback_data.get(key) raw = callback_data.get(key)
@ -1517,9 +1576,21 @@ class OpenClawDingTalkBot(ChatbotHandler):
file_name = str(raw).strip() file_name = str(raw).strip()
if file_name: if file_name:
break break
payload["file_name"] = self._normalize_file_name(file_name or "upload.xlsx") if not file_type and isinstance(callback_data, dict):
if not self._is_supported_excel_file(payload["file_name"]): 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 {} 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 return payload
def _guess_image_ext(self, content_type: str, source_url: str) -> str: def _guess_image_ext(self, content_type: str, source_url: str) -> str:
@ -2200,9 +2271,11 @@ class OpenClawDingTalkBot(ChatbotHandler):
raw = (proc.stdout or "").strip() raw = (proc.stdout or "").strip()
return f"结果解析失败: {exc}.\n原始输出: {raw[:500]}" 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: if not media_id:
return False 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 = { request_headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "*/*", "Accept": "*/*",
@ -2211,6 +2284,8 @@ class OpenClawDingTalkBot(ChatbotHandler):
"msgtype": "file", "msgtype": "file",
"file": { "file": {
"mediaId": media_id, "mediaId": media_id,
"fileName": safe_name,
"fileType": file_ext,
}, },
"at": { "at": {
"atUserIds": [msg.sender_staff_id] if msg.sender_staff_id else [], "atUserIds": [msg.sender_staff_id] if msg.sender_staff_id else [],
@ -2337,15 +2412,17 @@ class OpenClawDingTalkBot(ChatbotHandler):
return f"转换脚本不存在:{script_path}", False return f"转换脚本不存在:{script_path}", False
download_code = str(file_payload.get("download_code") or "").strip() 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") file_name = self._normalize_file_name(file_payload.get("file_name") or "upload.xlsx")
if not download_code: if not download_code and not download_url:
return "未获取到文件下载码,请重试上传。", False return "未获取到文件下载地址,请重试上传。", False
if not self._is_supported_excel_file(file_name): if not self._is_supported_excel_file(file_name):
return "当前仅支持 .xlsx 文件,请上传Excel后重试。", False return "当前仅支持 .xlsx 文件,请上传Excel后重试。", False
download_url = self.get_image_download_url(download_code)
if not download_url: 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: with tempfile.TemporaryDirectory(prefix="dingtalk_excel_link_") as tmpdir:
input_path = os.path.join(tmpdir, file_name) input_path = os.path.join(tmpdir, file_name)
@ -2437,7 +2514,7 @@ class OpenClawDingTalkBot(ChatbotHandler):
except Exception as exc: except Exception as exc:
self.logger.exception("upload converted excel failed") 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: if not sent:
s3_url = self._upload_excel_output_to_s3(output_path, output_name) s3_url = self._upload_excel_output_to_s3(output_path, output_name)
if s3_url: if s3_url:
@ -2588,6 +2665,15 @@ class OpenClawDingTalkBot(ChatbotHandler):
bool(incoming_image_codes), bool(incoming_image_codes),
len(text or ""), 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: if not text and not incoming_image_codes and not has_excel_file:
return AckMessage.STATUS_OK, "ignored" return AckMessage.STATUS_OK, "ignored"