11from __future__ import annotations
22
3- import base64
43import enum
54import json
65from dataclasses import dataclass , field
2120from astrbot .core .agent .tool import ToolSet
2221from astrbot .core .db .po import Conversation
2322from astrbot .core .message .message_event_result import MessageChain
23+ from astrbot .core .utils .image_utils import encode_image_to_base64_url
2424from astrbot .core .utils .io import download_image_by_url
2525
2626
@@ -189,12 +189,13 @@ async def assemble_context(self) -> dict:
189189 for image_url in self .image_urls :
190190 if image_url .startswith ("http" ):
191191 image_path = await download_image_by_url (image_url )
192- image_data = await self ._encode_image_bs64 (image_path )
192+ # 统一通过公共工具函数编码,自动检测 MIME 类型
193+ image_data = await encode_image_to_base64_url (image_path )
193194 elif image_url .startswith ("file:///" ):
194195 image_path = image_url .replace ("file:///" , "" )
195- image_data = await self . _encode_image_bs64 (image_path )
196+ image_data = await encode_image_to_base64_url (image_path )
196197 else :
197- image_data = await self . _encode_image_bs64 (image_url )
198+ image_data = await encode_image_to_base64_url (image_url )
198199 if not image_data :
199200 logger .warning (f"图片 { image_url } 得到的结果为空,将忽略。" )
200201 continue
@@ -214,87 +215,6 @@ async def assemble_context(self) -> dict:
214215 # 否则返回多模态格式
215216 return {"role" : "user" , "content" : content_blocks }
216217
217- @staticmethod
218- def _detect_mime_type (header_bytes : bytes ) -> str :
219- """根据文件头魔术字节(magic bytes)检测图片的实际 MIME 类型。
220-
221- 依次匹配常见图片格式的文件头特征,均不匹配时回退到 image/jpeg
222- 以保持向后兼容。支持的格式:JPEG、PNG、GIF、WebP、BMP、TIFF、
223- ICO、SVG、AVIF、HEIF/HEIC。
224-
225- Args:
226- header_bytes: 文件头原始字节,建议至少传入 16 字节。
227-
228- Returns:
229- 对应的 MIME 类型字符串,例如 "image/png"。
230- """
231- if len (header_bytes ) >= 3 and header_bytes [:3 ] == b'\xff \xd8 \xff ' :
232- return "image/jpeg"
233- if len (header_bytes ) >= 8 and header_bytes [:8 ] == b'\x89 PNG\r \n \x1a \n ' :
234- return "image/png"
235- if len (header_bytes ) >= 4 and header_bytes [:4 ] == b'GIF8' :
236- return "image/gif"
237- # WebP: RIFF????WEBP
238- if len (header_bytes ) >= 12 and header_bytes [:4 ] == b'RIFF' and header_bytes [8 :12 ] == b'WEBP' :
239- return "image/webp"
240- if len (header_bytes ) >= 2 and header_bytes [:2 ] == b'BM' :
241- return "image/bmp"
242- # TIFF: 小端 (II) 或大端 (MM)
243- if len (header_bytes ) >= 4 and header_bytes [:4 ] in (b'II\x2a \x00 ' , b'MM\x00 \x2a ' ):
244- return "image/tiff"
245- if len (header_bytes ) >= 4 and header_bytes [:4 ] == b'\x00 \x00 \x01 \x00 ' :
246- return "image/x-icon"
247- # SVG 为文本格式,检测头部是否含有 <svg 标签
248- if b'<svg' in header_bytes [:256 ].lower ():
249- return "image/svg+xml"
250- # AVIF: ftyp box brand = avif
251- if len (header_bytes ) >= 12 and header_bytes [4 :12 ] == b'ftypavif' :
252- return "image/avif"
253- # HEIF/HEIC: ftyp box,brand 为 heic/heix/hevc/hevx/mif1
254- if len (header_bytes ) >= 12 and header_bytes [4 :8 ] == b'ftyp' :
255- brand = header_bytes [8 :12 ]
256- if brand in (b'heic' , b'heix' , b'hevc' , b'hevx' , b'mif1' ):
257- return "image/heif"
258- # 无法识别,回退到 image/jpeg
259- return "image/jpeg"
260-
261- async def _encode_image_bs64 (self , image_url : str ) -> str :
262- """将图片转换为 base64 Data URL,自动检测实际 MIME 类型。
263-
264- 原实现硬编码 image/jpeg,会导致 PNG 等格式在严格校验的接口上报错。
265- 现通过读取文件头魔术字节来推断正确的 MIME 类型。
266-
267- Args:
268- image_url: 本地文件路径,或以 "base64://" 开头的 base64 字符串。
269-
270- Returns:
271- 形如 "data:image/png;base64,..." 的 Data URL 字符串。
272- """
273- if image_url .startswith ("base64://" ):
274- raw_b64 = image_url [len ("base64://" ):]
275- # 从 base64 数据中解码少量字节以检测实际格式,
276- # 取前 32 个 base64 字符可解码出约 24 字节的原始数据,足以覆盖所有魔术字节
277- try :
278- sample = raw_b64 [:32 ]
279- # 确保 base64 填充正确,避免解码报错
280- missing_padding = len (sample ) % 4
281- if missing_padding :
282- sample += '=' * (4 - missing_padding )
283- header_bytes = base64 .b64decode (sample )
284- mime_type = self ._detect_mime_type (header_bytes )
285- except Exception :
286- # 解码失败时安全回退
287- mime_type = "image/jpeg"
288- return f"data:{ mime_type } ;base64,{ raw_b64 } "
289-
290- with open (image_url , "rb" ) as f :
291- # 先读取文件头用于格式检测,再 seek 回起点读取完整内容
292- header_bytes = f .read (16 )
293- mime_type = self ._detect_mime_type (header_bytes )
294- f .seek (0 )
295- image_bs64 = base64 .b64encode (f .read ()).decode ("utf-8" )
296- return f"data:{ mime_type } ;base64,{ image_bs64 } "
297-
298218
299219@dataclass
300220class TokenUsage :
0 commit comments