@@ -1069,7 +1069,7 @@ async def _resolve_forward_file_download_url(
10691069 action_candidates .append (("get_file" , {"file_id" : file_id }))
10701070
10711071 for action , params in action_candidates :
1072- response = await self ._call (action , params , timeout = 12.0 )
1072+ response = await self ._call (action , params , timeout = 12.0 , retries = 2 )
10731073 if not response or response .get ("status" ) != "ok" :
10741074 continue
10751075
@@ -1652,7 +1652,12 @@ async def _render_forward_segment(
16521652 f"NapCat [{ self .instance_id } ] rendering forward segment id={ forward_id } "
16531653 )
16541654
1655- response = await self ._call ("get_forward_msg" , {"id" : forward_id }, timeout = 30.0 )
1655+ response = await self ._call (
1656+ "get_forward_msg" ,
1657+ {"id" : forward_id },
1658+ timeout = 30.0 ,
1659+ retries = 2 ,
1660+ )
16561661 if not response or response .get ("status" ) != "ok" :
16571662 logger .warning (
16581663 f"NapCat [{ self .instance_id } ] get_forward_msg failed for id={ forward_id } : { response } "
@@ -1727,26 +1732,53 @@ def _resolve_send_mode(self, size: int) -> str:
17271732 return self .config .file_send_mode
17281733
17291734 async def _call (
1730- self , action : str , params : dict , timeout : float = 30.0
1735+ self ,
1736+ action : str ,
1737+ params : dict ,
1738+ timeout : float = 30.0 ,
1739+ retries : int = 0 ,
17311740 ) -> dict | None :
17321741 """Send a OneBot action and await its echo response."""
17331742 if self ._ws is None :
17341743 return None
1735- echo = str (uuid .uuid4 ())
1736- fut : asyncio .Future = asyncio .get_event_loop ().create_future ()
1737- self ._pending [echo ] = fut
1738- payload = {"action" : action , "params" : params , "echo" : echo }
1739- try :
1740- await self ._ws .send (json .dumps (payload , ensure_ascii = False ))
1741- return await asyncio .wait_for (fut , timeout = timeout )
1742- except TimeoutError :
1743- logger .warning (f"NapCat [{ self .instance_id } ] action '{ action } ' timed out" )
1744- self ._pending .pop (echo , None )
1745- return None
1746- except Exception as e :
1747- logger .error (f"NapCat [{ self .instance_id } ] action '{ action } ' error: { e } " )
1748- self ._pending .pop (echo , None )
1749- return None
1744+ max_attempts = max (1 , int (retries ) + 1 )
1745+
1746+ for attempt in range (1 , max_attempts + 1 ):
1747+ echo = str (uuid .uuid4 ())
1748+ fut : asyncio .Future = asyncio .get_event_loop ().create_future ()
1749+ self ._pending [echo ] = fut
1750+ payload = {"action" : action , "params" : params , "echo" : echo }
1751+ try :
1752+ await self ._ws .send (json .dumps (payload , ensure_ascii = False ))
1753+ return await asyncio .wait_for (fut , timeout = timeout )
1754+ except TimeoutError :
1755+ self ._pending .pop (echo , None )
1756+ if attempt >= max_attempts :
1757+ logger .warning (
1758+ f"NapCat [{ self .instance_id } ] action '{ action } ' timed out "
1759+ f"after { attempt } attempt(s)"
1760+ )
1761+ return None
1762+ logger .warning (
1763+ f"NapCat [{ self .instance_id } ] action '{ action } ' timed out, "
1764+ f"retrying ({ attempt } /{ max_attempts - 1 } )"
1765+ )
1766+ await asyncio .sleep (min (2.0 , 0.3 * (2 ** (attempt - 1 ))))
1767+ except Exception as e :
1768+ self ._pending .pop (echo , None )
1769+ if attempt >= max_attempts :
1770+ logger .error (
1771+ f"NapCat [{ self .instance_id } ] action '{ action } ' error "
1772+ f"after { attempt } attempt(s): { e } "
1773+ )
1774+ return None
1775+ logger .warning (
1776+ f"NapCat [{ self .instance_id } ] action '{ action } ' error, "
1777+ f"retrying ({ attempt } /{ max_attempts - 1 } ): { e } "
1778+ )
1779+ await asyncio .sleep (min (2.0 , 0.3 * (2 ** (attempt - 1 ))))
1780+
1781+ return None
17501782
17511783 async def _get_qid (self , user_id : str , group_id : str | None = None ) -> str :
17521784 """Get user's qid using NapCat API with caching."""
@@ -1757,7 +1789,10 @@ async def _get_qid(self, user_id: str, group_id: str | None = None) -> str:
17571789 try :
17581790 # Use get_stranger_info to get qid
17591791 result = await self ._call (
1760- "get_stranger_info" , {"user_id" : user_id }, timeout = 30.0
1792+ "get_stranger_info" ,
1793+ {"user_id" : user_id },
1794+ timeout = 30.0 ,
1795+ retries = 2 ,
17611796 )
17621797 # logger.debug(
17631798 # f"NapCat [{self.instance_id}] get_stranger_info result for {user_id}: {result}"
@@ -1880,10 +1915,34 @@ async def send(
18801915 segments .append ({"type" : "reply" , "data" : {"id" : str (reply_to_id )}})
18811916
18821917 rich_header = kwargs .get ("rich_header" )
1918+ has_non_image_attachments = any (
1919+ att .type != "image"
1920+ for att in (attachments or [])
1921+ if att .url or att .data is not None
1922+ )
1923+ send_header_separately = bool (
1924+ rich_header and has_non_image_attachments and not str (text or "" ).strip ()
1925+ )
18831926 if rich_header :
1884- t , c = rich_header .get ("title" , "" ), rich_header .get ("content" , "" )
1885- prefix = f"[{ t } " + (f" · { c } " if c else "" ) + "]"
1886- text = f"{ prefix } \n { text } " if text else prefix
1927+ if not send_header_separately :
1928+ t , c = rich_header .get ("title" , "" ), rich_header .get ("content" , "" )
1929+ prefix = f"[{ t } " + (f" · { c } " if c else "" ) + "]"
1930+ text = f"{ prefix } \n { text } " if text else prefix
1931+ else :
1932+ t , c = rich_header .get ("title" , "" ), rich_header .get ("content" , "" )
1933+ prefix = f"[{ t } " + (f" · { c } " if c else "" ) + "]"
1934+ header_resp = await self ._call (
1935+ "send_group_msg" ,
1936+ {
1937+ "group_id" : int (group_id ),
1938+ "message" : [{"type" : "text" , "data" : {"text" : prefix }}],
1939+ },
1940+ )
1941+ if not header_resp or header_resp .get ("status" ) != "ok" :
1942+ logger .warning (
1943+ f"NapCat [{ self .instance_id } ] failed to send standalone rich header "
1944+ f"before media message: { header_resp } "
1945+ )
18871946
18881947 # Process mentions: replace @Name with at segments
18891948 mentions = kwargs .get ("mentions" , [])
0 commit comments