11import asyncio
22import inspect
33import json
4+ import os
45import traceback
56import typing as T
67import uuid
3637from astrbot .core .provider .entites import ProviderRequest
3738from astrbot .core .provider .register import llm_tools
3839from astrbot .core .utils .history_saver import persist_agent_history
40+ from astrbot .core .utils .string_utils import normalize_and_dedupe_strings
3941
4042
4143class FunctionToolExecutor (BaseFunctionToolExecutor [AstrAgentContext ]):
44+ _ALLOWED_IMAGE_EXTENSIONS = {
45+ ".png" ,
46+ ".jpg" ,
47+ ".jpeg" ,
48+ ".gif" ,
49+ ".webp" ,
50+ ".bmp" ,
51+ ".tif" ,
52+ ".tiff" ,
53+ ".svg" ,
54+ ".heic" ,
55+ }
56+
57+ @classmethod
58+ def _is_supported_image_ref (cls , image_ref : str ) -> bool :
59+ if not image_ref :
60+ return False
61+ lowered = image_ref .lower ()
62+ if lowered .startswith (("http://" , "https://" , "base64://" )):
63+ return True
64+ file_path = image_ref [8 :] if lowered .startswith ("file:///" ) else image_ref
65+ ext = os .path .splitext (file_path )[1 ].lower ()
66+ return ext in cls ._ALLOWED_IMAGE_EXTENSIONS
67+
68+ @classmethod
69+ async def _prepare_handoff_image_urls (
70+ cls ,
71+ run_context : ContextWrapper [AstrAgentContext ],
72+ tool_args : dict [str , T .Any ],
73+ ) -> list [str ]:
74+ image_urls = tool_args .get ("image_urls" )
75+ if image_urls is None :
76+ candidates : list [T .Any ] = []
77+ elif isinstance (image_urls , str ):
78+ candidates = [image_urls ]
79+ else :
80+ try :
81+ candidates = list (image_urls )
82+ except (TypeError , ValueError ):
83+ candidates = [image_urls ]
84+
85+ normalized = normalize_and_dedupe_strings (candidates )
86+ sanitized = [item for item in normalized if cls ._is_supported_image_ref (item )]
87+ dropped_count = len (normalized ) - len (sanitized )
88+ if dropped_count > 0 :
89+ logger .warning (
90+ "Dropped %d invalid image_urls entries in handoff tool args." ,
91+ dropped_count ,
92+ )
93+
94+ # Merge current event image attachments so sub-agent behavior matches main-agent flow.
95+ event = getattr (run_context .context , "event" , None )
96+ message_obj = getattr (event , "message_obj" , None )
97+ message = getattr (message_obj , "message" , None )
98+ if message :
99+ for idx , component in enumerate (message ):
100+ if not isinstance (component , Image ):
101+ continue
102+ try :
103+ path = await component .convert_to_file_path ()
104+ if (
105+ path
106+ and cls ._is_supported_image_ref (path )
107+ and path not in sanitized
108+ ):
109+ sanitized .append (path )
110+ except Exception as e :
111+ logger .error (
112+ "Failed to convert handoff image component at index %d: %s" ,
113+ idx ,
114+ e ,
115+ exc_info = True ,
116+ )
117+
118+ tool_args ["image_urls" ] = sanitized
119+ return sanitized
120+
42121 @classmethod
43122 async def execute (cls , tool , run_context , ** tool_args ):
44123 """执行函数调用。
@@ -165,29 +244,7 @@ async def _execute_handoff(
165244 ** tool_args ,
166245 ):
167246 input_ = tool_args .get ("input" )
168- image_urls = tool_args .get ("image_urls" )
169- if image_urls is None :
170- image_urls = []
171- elif isinstance (image_urls , str ):
172- image_urls = [image_urls ]
173- else :
174- try :
175- image_urls = list (image_urls )
176- except (TypeError , ValueError ):
177- image_urls = [image_urls ]
178-
179- # 获取当前事件中的图片
180- event = run_context .context .event
181- if event .message_obj and event .message_obj .message :
182- for component in event .message_obj .message :
183- if isinstance (component , Image ):
184- try :
185- # 调用组件的 convert_to_file_path 异步方法
186- path = await component .convert_to_file_path ()
187- if path and path not in image_urls :
188- image_urls .append (path )
189- except Exception as e :
190- logger .error (f"转换图片失败: { e } " )
247+ image_urls = await cls ._prepare_handoff_image_urls (run_context , tool_args )
191248
192249 # Build handoff toolset from registered tools plus runtime computer tools.
193250 toolset = cls ._build_handoff_toolset (run_context , tool .agent .tools )
@@ -286,8 +343,12 @@ async def _do_handoff_background(
286343 ) -> None :
287344 """Run the subagent handoff and, on completion, wake the main agent."""
288345 result_text = ""
346+ prepared_tool_args = dict (tool_args )
289347 try :
290- async for r in cls ._execute_handoff (tool , run_context , ** tool_args ):
348+ await cls ._prepare_handoff_image_urls (run_context , prepared_tool_args )
349+ async for r in cls ._execute_handoff (
350+ tool , run_context , ** prepared_tool_args
351+ ):
291352 if isinstance (r , mcp .types .CallToolResult ):
292353 for content in r .content :
293354 if isinstance (content , mcp .types .TextContent ):
@@ -304,7 +365,7 @@ async def _do_handoff_background(
304365 task_id = task_id ,
305366 tool_name = tool .name ,
306367 result_text = result_text ,
307- tool_args = tool_args ,
368+ tool_args = prepared_tool_args ,
308369 note = (
309370 event .get_extra ("background_note" )
310371 or f"Background task for subagent '{ tool .agent .name } ' finished."
0 commit comments