@@ -2230,6 +2230,45 @@ class MediaResolveMiddleware(InboundMiddleware):
22302230
22312231 name = "media-resolve"
22322232
2233+ # --- Resource download cache (keyed by resourceId) ---
2234+ # Avoids redundant downloads of the same resource within the TTL window.
2235+ # The same resourceId can be referenced multiple times in a session (own
2236+ # attachment, then quoted again, then observed in a group backfill); each
2237+ # reference otherwise triggers a fresh token exchange + download.
2238+ _resource_cache : ClassVar [Dict [str , Tuple [str , str , float ]]] = {} # rid -> (local_path, mime, ts)
2239+ _RESOURCE_CACHE_TTL_S : ClassVar [int ] = 24 * 60 * 60 # 24 hours
2240+ _RESOURCE_CACHE_MAX_SIZE : ClassVar [int ] = 256
2241+
2242+ @classmethod
2243+ def _get_cached_resource (cls , resource_id : str ) -> Optional [Tuple [str , str ]]:
2244+ """Return cached ``(local_path, mime)`` if still valid and file exists, else None."""
2245+ if not resource_id :
2246+ return None
2247+ entry = cls ._resource_cache .get (resource_id )
2248+ if entry is None :
2249+ return None
2250+ local_path , mime , ts = entry
2251+ if time .time () - ts > cls ._RESOURCE_CACHE_TTL_S :
2252+ cls ._resource_cache .pop (resource_id , None )
2253+ return None
2254+ # Verify the cached file still exists on disk (cache dir may be swept).
2255+ if not os .path .isfile (local_path ):
2256+ cls ._resource_cache .pop (resource_id , None )
2257+ return None
2258+ return local_path , mime
2259+
2260+ @classmethod
2261+ def _put_cached_resource (cls , resource_id : str , local_path : str , mime : str ) -> None :
2262+ """Store download result in cache. Evicts oldest entries when over capacity."""
2263+ if not resource_id :
2264+ return
2265+ if len (cls ._resource_cache ) >= cls ._RESOURCE_CACHE_MAX_SIZE :
2266+ # Drop the oldest 25% of entries by timestamp.
2267+ sorted_keys = sorted (cls ._resource_cache , key = lambda k : cls ._resource_cache [k ][2 ])
2268+ for k in sorted_keys [: cls ._RESOURCE_CACHE_MAX_SIZE // 4 ]:
2269+ cls ._resource_cache .pop (k , None )
2270+ cls ._resource_cache [resource_id ] = (local_path , mime , time .time ())
2271+
22332272 @staticmethod
22342273 def _guess_image_ext_from_url (url : str ) -> str :
22352274 """Guess image extension from URL path."""
@@ -2327,8 +2366,23 @@ async def _resolve_download_url(adapter, url: str) -> str:
23272366 async def _download_and_cache (
23282367 cls , adapter , * , fetch_url : str , kind : str ,
23292368 file_name : Optional [str ] = None , log_tag : str = "" ,
2369+ resource_id : str = "" ,
23302370 ) -> Optional [Tuple [str , str ]]:
2331- """Download a Yuanbao resource and cache locally. Returns ``(local_path, mime)`` or ``None``."""
2371+ """Download a Yuanbao resource and cache locally. Returns ``(local_path, mime)`` or ``None``.
2372+
2373+ When *resource_id* is provided, an in-memory cache keyed by resourceId
2374+ is consulted first to skip redundant downloads of the same resource
2375+ within the TTL window.
2376+ """
2377+ if resource_id :
2378+ hit = cls ._get_cached_resource (resource_id )
2379+ if hit is not None :
2380+ logger .debug (
2381+ "[%s] resource cache hit: rid=%s path=%s" ,
2382+ adapter .name , resource_id , hit [0 ],
2383+ )
2384+ return hit
2385+
23322386 try :
23332387 file_bytes , content_type = await media_download_url (
23342388 fetch_url , max_size_mb = adapter .MEDIA_MAX_SIZE_MB ,
@@ -2353,6 +2407,7 @@ async def _download_and_cache(
23532407 mime = guess_mime_type (f"image{ ext } " )
23542408 if not mime .startswith ("image/" ):
23552409 mime = content_type if content_type .startswith ("image/" ) else "image/jpeg"
2410+ cls ._put_cached_resource (resource_id , local_path , mime )
23562411 return local_path , mime
23572412
23582413 # kind == "file"
@@ -2368,6 +2423,7 @@ async def _download_and_cache(
23682423 )
23692424 return None
23702425 mime = guess_mime_type (file_name ) or content_type or "application/octet-stream"
2426+ cls ._put_cached_resource (resource_id , local_path , mime )
23712427 return local_path , mime
23722428
23732429 @classmethod
@@ -2393,6 +2449,9 @@ async def _resolve_media_urls(
23932449 if kind not in _RESOLVABLE_MEDIA_KINDS or not url :
23942450 continue
23952451
2452+ # Extract resourceId from the placeholder URL for cache dedup.
2453+ rid = ExtractContentMiddleware ._parse_resource_id (url )
2454+
23962455 try :
23972456 fetch_url = await cls ._resolve_download_url (adapter , url )
23982457 except Exception as exc :
@@ -2408,6 +2467,7 @@ async def _resolve_media_urls(
24082467 kind = kind ,
24092468 file_name = str (ref .get ("name" ) or "" ).strip () or None ,
24102469 log_tag = f"placeholder_url={ url [:80 ]} " ,
2470+ resource_id = rid ,
24112471 )
24122472 if cached is None :
24132473 continue
@@ -2480,6 +2540,7 @@ async def _collect_observed_media(
24802540 kind = kind ,
24812541 file_name = filename or None ,
24822542 log_tag = f"rid={ rid } " ,
2543+ resource_id = rid ,
24832544 )
24842545 if cached is None :
24852546 continue
@@ -2563,6 +2624,7 @@ async def _dispatch_inbound_event() -> None:
25632624 kind = kind ,
25642625 file_name = filename or None ,
25652626 log_tag = f"quote rid={ rid } " ,
2627+ resource_id = rid ,
25662628 )
25672629 if cached is None :
25682630 continue
0 commit comments