From 46192fbd2abe922390be1961819a86dc75c74321 Mon Sep 17 00:00:00 2001 From: coldxiangyu Date: Thu, 5 Mar 2026 20:18:13 +0800 Subject: [PATCH 1/2] fix(context): detect image MIME type from magic bytes instead of file extension Feishu downloads images with incorrect extensions (e.g. .jpg for PNG files). mimetypes.guess_type() relies on the file extension, causing a MIME mismatch that Anthropic rejects with 'image was specified using image/jpeg but appears to be image/png'. Fix: read the first bytes of the image data and detect the real MIME type via magic bytes (PNG: 0x89PNG, JPEG: 0xFFD8FF, GIF: GIF87a/GIF89a, WEBP: RIFF+WEBP). Fall back to mimetypes.guess_type() only when magic bytes are inconclusive. --- nanobot/agent/context.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index df4825f..7ead317 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -12,6 +12,19 @@ from nanobot.agent.memory import MemoryStore from nanobot.agent.skills import SkillsLoader +def _detect_image_mime(data: bytes) -> str | None: + """Detect image MIME type from magic bytes, ignoring file extension.""" + if data[:8] == b"\x89PNG\r\n\x1a\n": + return "image/png" + if data[:3] == b"\xff\xd8\xff": + return "image/jpeg" + if data[:6] in (b"GIF87a", b"GIF89a"): + return "image/gif" + if data[:4] == b"RIFF" and data[8:12] == b"WEBP": + return "image/webp" + return None + + class ContextBuilder: """Builds the context (system prompt + messages) for the agent.""" @@ -136,10 +149,14 @@ Reply directly with text for conversations. Only use the 'message' tool to send images = [] for path in media: p = Path(path) - mime, _ = mimetypes.guess_type(path) - if not p.is_file() or not mime or not mime.startswith("image/"): + if not p.is_file(): continue - b64 = base64.b64encode(p.read_bytes()).decode() + raw = p.read_bytes() + # Detect real MIME type from magic bytes; fallback to filename guess + mime = _detect_image_mime(raw) or mimetypes.guess_type(path)[0] + if not mime or not mime.startswith("image/"): + continue + b64 = base64.b64encode(raw).decode() images.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}) if not images: From 3a01fe536a37c8424fc196b1b0aad3535a50af93 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Fri, 6 Mar 2026 06:49:09 +0000 Subject: [PATCH 2/2] refactor: move detect_image_mime to utils/helpers for reuse --- nanobot/agent/context.py | 16 ++-------------- nanobot/utils/helpers.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index 7ead317..27511fa 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -10,19 +10,7 @@ from typing import Any from nanobot.agent.memory import MemoryStore from nanobot.agent.skills import SkillsLoader - - -def _detect_image_mime(data: bytes) -> str | None: - """Detect image MIME type from magic bytes, ignoring file extension.""" - if data[:8] == b"\x89PNG\r\n\x1a\n": - return "image/png" - if data[:3] == b"\xff\xd8\xff": - return "image/jpeg" - if data[:6] in (b"GIF87a", b"GIF89a"): - return "image/gif" - if data[:4] == b"RIFF" and data[8:12] == b"WEBP": - return "image/webp" - return None +from nanobot.utils.helpers import detect_image_mime class ContextBuilder: @@ -153,7 +141,7 @@ Reply directly with text for conversations. Only use the 'message' tool to send continue raw = p.read_bytes() # Detect real MIME type from magic bytes; fallback to filename guess - mime = _detect_image_mime(raw) or mimetypes.guess_type(path)[0] + mime = detect_image_mime(raw) or mimetypes.guess_type(path)[0] if not mime or not mime.startswith("image/"): continue b64 = base64.b64encode(raw).decode() diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index 3a8c802..b543174 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -5,6 +5,19 @@ from datetime import datetime from pathlib import Path +def detect_image_mime(data: bytes) -> str | None: + """Detect image MIME type from magic bytes, ignoring file extension.""" + if data[:8] == b"\x89PNG\r\n\x1a\n": + return "image/png" + if data[:3] == b"\xff\xd8\xff": + return "image/jpeg" + if data[:6] in (b"GIF87a", b"GIF89a"): + return "image/gif" + if data[:4] == b"RIFF" and data[8:12] == b"WEBP": + return "image/webp" + return None + + def ensure_dir(path: Path) -> Path: """Ensure directory exists, return it.""" path.mkdir(parents=True, exist_ok=True)