
    i                   V   d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlmZmZ ddlmZ ddlmZmZmZmZmZmZmZ ddlmZ 	 ddlZdZn# e$ r d	ZdZY nw xY w	 ddlZdZn# e$ r d	ZdZY nw xY wdd
lmZm Z  ddl!m"Z"m#Z#m$Z$m%Z%m&Z&m'Z'm(Z( ddl)m*Z*  ej+        e,          Z- G d de.          Z/ddl0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZEmFZF ddlGmHZImJZJ ddlKmLZLmMZMmNZN ddlOmPZPmQZQmRZRmSZSmTZTmUZUmVZVmWZWmXZX ddZYddZZ G d de"          Z[dS )uY  
QQ Bot platform adapter using the Official QQ Bot API (v2).

Connects to the QQ Bot WebSocket Gateway for inbound events and uses the
REST API (``api.sgroup.qq.com``) for outbound messages and media uploads.

Configuration in config.yaml:
    platforms:
      qq:
        enabled: true
        extra:
          app_id: "your-app-id"            # or QQ_APP_ID env var
          client_secret: "your-secret"     # or QQ_CLIENT_SECRET env var
          markdown_support: true           # enable QQ markdown (msg_type 2)
          dm_policy: "open"                # open | allowlist | disabled
          allow_from: ["openid_1"]
          group_policy: "open"             # open | allowlist | disabled
          group_allow_from: ["group_openid_1"]
          stt:                             # Voice-to-text config (optional)
            provider: "zai"                # zai (GLM-ASR), openai (Whisper), etc.
            baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4"
            apiKey: "your-stt-api-key"     # or set QQ_STT_API_KEY env var
            model: "glm-asr"               # glm-asr, whisper-1, etc.

    Voice transcription priority:
      1. QQ's built-in ``asr_refer_text`` (Tencent ASR — free, always tried first)
      2. Configured STT provider via ``stt`` config or ``QQ_STT_*`` env vars

Reference: https://bot.q.qq.com/wiki/develop/api-v2/
    )annotationsN)datetimetimezone)Path)Any	AwaitableCallableDictListOptionalTuple)urlparseTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResult_ssrf_redirect_guardcache_document_from_bytescache_image_from_bytes)strip_markdownc                  $     e Zd ZdZd fd	Z xZS )QQCloseErrorzRaised when QQ WebSocket closes with a specific code.

    Carries the close code and reason for proper handling in the reconnect loop.
     c                    |rt          |          nd | _        |rt          |          nd| _        t	                                          d| j         d| j         d           d S )Nr   zWebSocket closed (code=z	, reason=))intcodestrreasonsuper__init__)selfr   r!   	__class__s      D/home/piyush/.hermes/hermes-agent/gateway/platforms/qqbot/adapter.pyr#   zQQCloseError.__init__T   se    !%/CIII4	%+3c&kkkU49UUt{UUUVVVVV    r   )__name__
__module____qualname____doc__r#   __classcell__r%   s   @r&   r   r   N   sQ         
W W W W W W W W W Wr'   r   )API_BASE	TOKEN_URLGATEWAY_URL_PATHDEFAULT_API_TIMEOUTFILE_UPLOAD_TIMEOUTCONNECT_TIMEOUT_SECONDSRECONNECT_BACKOFFMAX_RECONNECT_ATTEMPTSRATE_LIMIT_DELAYQUICK_DISCONNECT_THRESHOLDMAX_QUICK_DISCONNECT_COUNTMAX_MESSAGE_LENGTHDEDUP_WINDOW_SECONDSDEDUP_MAX_SIZEMSG_TYPE_TEXTMSG_TYPE_MARKDOWNMSG_TYPE_MEDIAMSG_TYPE_INPUT_NOTIFYMEDIA_TYPE_IMAGEMEDIA_TYPE_VIDEOMEDIA_TYPE_VOICEMEDIA_TYPE_FILE)coerce_listbuild_user_agent)ChunkedUploaderUploadDailyLimitExceededErrorUploadFileTooLargeError)	ApprovalRequestApprovalSenderInlineKeyboardInteractionEventbuild_approval_keyboardbuild_update_prompt_keyboardparse_approval_button_dataparse_interaction_eventparse_update_prompt_button_datareturnboolc                     t           ot          S )z/Check if QQ runtime dependencies are available.)AIOHTTP_AVAILABLEHTTPX_AVAILABLE r'   r&   check_qq_requirementsrY      s    00r'   valuer   	List[str]c                     t          |           S )z0Coerce config values into a trimmed string list.)_coerce_list_impl)rZ   s    r&   _coerce_listr^      s    U###r'   c                  F    e Zd ZdZdZeZdZdZedd            Z	dd
Z
dĈ fdZedd            ZddZddZddZddZddZddZddZddZddZddZddZddZed             Zdd"Zdd%Zedd(            Zedd*            Zd͈ fd-Zdd/Z dd2Z!dd3Z"	 ddd7Z#d8d9d:d;Z$dd=Z%edddA            Z&ddEZ'ddFZ(ddGZ)ddHZ*ddIZ+eddL            Z,eddP            Z-ddRZ.ddVZ/eddX            Z0ddZZ1d[d[d\dd_Z2ddbZ3eddd            Z4edde            Z5ddhZ6ddiZ7ddjZ8ddlZ9ddmZ:ddoZ;d[e<fdduZ=	 	 	 	 ddd|Z>d}Z?d~Z@ddZA	 	 dddZB	 dddZC	 	 dddZD	 	 dddZE	 dddZF	 dddZG	 dddZH	 	 dddZIdZJ	 	 	 dddZK	 dddZL	 	 	 dddZM	 	 dddZN	 	 dddZO	 	 dddZP	 	 	 dddZQ	 	 	 dddZRddZS	 dddZTdd dZUddZVddZWedd            ZXddZYedd            ZZddZ[ddZ\edd            Z]ddZ^d	dZ_ xZ`S (
  	QQAdapterzJQQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API.F<   2   rS   r    c                6    t          | dd          }|rd| S dS )z>Log prefix including app_id for multi-instance disambiguation._app_idNzQQBot:QQBot)getattr)r$   app_ids     r&   _log_tagzQQAdapter._log_tag   s0     y$// 	%$F$$$wr'   r!   Nonec                    | j                                         D ]8}|                                s"|                    t	          |                     9| j                                          dS )z"Fail all pending response futures.N)_pending_responsesvaluesdoneset_exceptionRuntimeErrorclear)r$   r!   futs      r&   _fail_pendingzQQAdapter._fail_pending   sh    *1133 	8 	8C88:: 8!!,v"6"6777%%'''''r'   configr   c                P   t                                          |t          j                   |j        pi }t          |                    d          pt          j        dd                    	                                | _
        t          |                    d          pt          j        dd                    	                                | _        t          |                    dd                    | _        t          |                    dd	                    	                                                                | _        t!          |                    d
          p|                    d                    | _        t          |                    dd	                    	                                                                | _        t!          |                    d          p|                    d                    | _        d | _        d | _        d | _        d | _        d | _        d| _        d | _        d | _        i | _        i | _        i | _        i | _        i | _         d | _!        d| _"        tG          j$                    | _%        i | _&        d | _'        | j(        | _'        d S )Nrg   	QQ_APP_IDr   client_secretQQ_CLIENT_SECRETmarkdown_supportT	dm_policyopen
allow_from	allowFromgroup_policygroup_allow_fromgroupAllowFrom      >@        ))r"   r#   r   QQBOTextrar    getosgetenvstriprd   _client_secretrT   _markdown_supportlower
_dm_policyr^   _allow_from_group_policy_group_allow_from_session_ws_http_client_listen_task_heartbeat_task_heartbeat_interval_session_id	_last_seq_chat_type_maprk   _seen_messages_last_msg_id_typing_sent_at_access_token_token_expires_atasyncioLock_token_lock_upload_cache_interaction_callback_default_interaction_dispatch)r$   rs   r   r%   s      r&   r#   zQQAdapter.__init__   sN   000"599X..L")K2L2LMMSSUU!IIo&&K")4F*K*K
 

%'' 	 "&eii0BD&I&I!J!J eiiV<<==CCEEKKMM'IIl##=uyy'='=
 
 !>6!B!BCCIIKKQQSS!-II())HUYY7G-H-H"
 "

 :>>B9=487;*. *.(,.0 >@02 -/13 -1(+"<>> 9;  	" &*%G"""r'   c                    dS )Nre   rX   r$   s    r&   namezQQAdapter.name   s    wr'   rT   c                T  K   t           s=d}|                     d|d           t                              d| j        |           dS t
          s=d}|                     d|d           t                              d| j        |           dS | j        r| j        s=d	}|                     d
|d           t                              d| j        |           dS |                     d| j        d          sdS 	 ddl	m
} t          j        dddt          gi |                      | _        |                                  d{V  |                                  d{V }t                              d| j        |           |                     |           d{V  t'          j        |                                           | _        t'          j        |                                           | _        |                                  t                              d| j                   dS # t4          $ ry}d| }|                     d|d           t                              d| j        |d           |                                  d{V  |                                  Y d}~dS d}~ww xY w)z9Authenticate, obtain gateway URL, and open the WebSocket.z(QQ startup failed: aiohttp not installedqq_missing_dependencyT	retryablez![%s] %s. Run: pip install aiohttpFz&QQ startup failed: httpx not installedz[%s] %s. Run: pip install httpxz>QQ startup failed: QQ_APP_ID and QQ_CLIENT_SECRET are requiredqq_missing_credentialsz[%s] %szqqbot-appidzQQBot app IDr   )platform_httpx_limitsr   response)timeoutfollow_redirectsevent_hookslimitsNz[%s] Gateway URL: %sz[%s] ConnectedzQQ startup failed: qq_connect_errorexc_info)rV   _set_fatal_errorloggerwarningrh   rW   rd   r   _acquire_platform_lock%gateway.platforms._http_client_limitsr   httpxAsyncClientr   r   _ensure_token_get_gateway_urlinfo_open_wsr   create_task_listen_loopr   _heartbeat_loopr   _mark_connected	Exceptionerror_cleanup_release_platform_lock)r$   messager   gateway_urlexcs        r&   connectzQQAdapter.connect   s       	@G!!"97d!SSSNN>wWWW5 	>G!!"97d!SSSNN<dmWUUU5| 	4#6 	VG!!":Gt!TTTNN9dmW===5 **=$,WW 	5!	 TSSSSS % 1!%'*>)?@,,..	! ! !D $$&&&&&&&&& !% 5 5 7 7777777KKK.{KKK --,,,,,,,,, !( 3D4E4E4G4G H HD#*#6t7K7K7M7M#N#ND   """KK($-8884 	 	 	1C11G!!"4g!NNNLLDM7TLJJJ--//!!!!!!!'')))55555	s   5D-H$ $
J'.A.J""J'c                  K   d| _         |                                  | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        |                                  d{V  |                                  t          
                    d| j                   dS )z)Close all connections and stop listeners.FNz[%s] Disconnected)_running_mark_disconnectedr   cancelr   CancelledErrorr   r   r   r   r   rh   r   s    r&   
disconnectzQQAdapter.disconnect8  sJ     !!! 	%$$&&&''''''''')    $D 	( '')))*********)   #'D mmoo##%%%'77777s#   A AA
B B*)B*c                  K   | j         r+| j         j        s| j                                          d{V  d| _         | j        r+| j        j        s| j                                         d{V  d| _        | j        r&| j                                         d{V  d| _        | j                                        D ]8}|                                s"|	                    t          d                     9| j                                         dS )z*Close WebSocket, HTTP session, and client.NDisconnected)r   closedcloser   r   acloserk   rl   rm   rn   ro   rp   )r$   rq   s     r&   r   zQQAdapter._cleanupQ  s.     8 	#DHO 	#(.."""""""""= 	(!5 	(-%%''''''''' 	%#**,,,,,,,,, $D *1133 	@ 	@C88:: @!!,~">">???%%'''''r'   c                  K   | j         r&t          j                    | j        dz
  k     r| j         S | j        4 d{V  | j         r8t          j                    | j        dz
  k     r| j         cddd          d{V  S 	 | j                            t          | j        | j        dt                     d{V }|
                                 |                                }n%# t          $ r}t          d|           |d}~ww xY w|                    d          }|st          d|           t          |                    dd	                    }|| _         t          j                    |z   | _        t                               d
| j        |           | j         cddd          d{V  S # 1 d{V swxY w Y   dS )zFReturn a valid access token, refreshing if needed (with singleflight).ra   N)appIdclientSecret)jsonr   z#Failed to get QQ Bot access token: access_tokenz,QQ Bot token response missing access_token: 
expires_ini   z+[%s] Access token refreshed, expires in %ds)r   timer   r   r   postr0   rd   r   r2   raise_for_statusr   r   ro   r   r   r   r   rh   )r$   respdatar   tokenr   s         r&   r   zQQAdapter._ensure_tokeni  s      	&$)++0F0K"K"K%%# 	& 	& 	& 	& 	& 	& 	& 	&! *dikkD4JR4O&O&O)	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&
	Y!.33#'<ATUU/ 4        
 %%'''yy{{ Y Y Y"#N#N#NOOUXXY HH^,,E "I4II   TXXlD99::J!&D%)Y[[:%=D"KK=t}j   %9	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&s7   -F.>A"C! F.!
D+C>>DBF..
F8;F8c                  K   |                                   d{V }	 | j                            t           t           d| t                      dt                     d{V }|                                 |                                }n%# t          $ r}t          d|           |d}~ww xY w|                    d          }|st          d|           |S )z2Fetch the WebSocket gateway URL from the REST API.NQQBot )Authorization
User-Agent)headersr   z"Failed to get QQ Bot gateway URL: urlz%QQ Bot gateway response missing url: )r   r   r   r/   r1   rF   r2   r   r   r   ro   )r$   r   r   r   r   r   s         r&   r   zQQAdapter._get_gateway_url  s2     ((********	T*../-//%5e%5%5"2"4"4  , /        D !!###99;;DD 	T 	T 	TICIIJJPSS	T hhuoo 	OMtMMNNN
s   A0B 
B1B,,B1r   c                  K   | j         r+| j         j        s| j                                          d{V  d| _         | j        r+| j        j        s| j                                         d{V  d| _        t	          j        d          | _        t          j        d          pct          j        d          pOt          j        d          p;t          j        d          p't          j        d          pt          j        d	          }| j                            |d
t                      it          |           d{V | _         t                              d| j        |           dS )z2Open a WebSocket connection to the QQ Bot gateway.NT)	trust_env	WSS_PROXY	wss_proxyHTTPS_PROXYhttps_proxy	ALL_PROXY	all_proxyr   )r   r   proxyz[%s] WebSocket connected to %s)r   r   r   r   aiohttpClientSessionr   r   
ws_connectrF   r4   r   r   rh   )r$   r   ws_proxys      r&   r   zQQAdapter._open_ws  s      8 	#DHO 	#(.."""""""""= 	(!5 	(-%%'''''''''  -===Ik"" &y%%&y''& y''& y%%	&
 y%% 	 11.00 , 2 
 
 
 
 
 
 
 
 	4dm[QQQQQr'   c                  K   d}d}d}| j         r	 t          j                    }|                                  d{V  d}d}n}# t          j        $ r Y dS t          $ r}| j         sY d}~dS |j        }t          	                    d| j
        ||j                   t          j                    |z
  }|t          k     rw|dk    rq|dz  }t                              d| j
        ||           |t          k    r>t                              d| j
                   |                     dd	d
           Y d}~dS nd}|                                  |                     d           |dv rO|dk    rdnd}t                              d| j
        |           |                     d| d| d           Y d}~dS |dk    rt                              d| j
        t&                     |t(          k    rY d}~dS t	          j        t&                     d{V  |                     |           d{V rd}d}n|dz  }Y d}~0|dk    r.t                              d| j
                   d| _        d| _        |dv r/t                              d| j
        |           d| _        d| _        |                     |           d{V rd}d}n6|dz  }|t(          k    r&t                              d| j
                   Y d}~dS Y d}~nd}~wt6          $ r}| j         sY d}~dS t          	                    d| j
        |           |                                  |                     d           |t(          k    r&t                              d| j
                   Y d}~dS |                     |           d{V rd}d}n|dz  }Y d}~nd}~ww xY w| j         dS dS )u  Read WebSocket events and reconnect on errors.

        Close code handling follows the OpenClaw qqbot reference implementation:
          4004 → invalid token, refresh and reconnect
          4006/4007/4009 → session invalid, clear session and re-identify
          4008 → rate limited, back off 60s
          4914 → bot offline/sandbox, stop reconnecting
          4915 → bot banned, stop reconnecting
        r   r   Nz([%s] WebSocket closed: code=%s reason=%s   z([%s] Quick disconnect (%.1fs), count: %dzf[%s] Too many quick disconnects. Check: 1) AppID/Secret correct 2) Bot permissions on QQ Open Platformqq_quick_disconnectu4   Too many quick disconnects — check bot permissionsTr   zConnection closed)2  i3  r   zoffline/sandbox-onlybannedz'[%s] Bot is %s. Check QQ Open Platform.qq_zBot is Fi  z%[%s] Rate limited (4008), waiting %dsi  z5[%s] Invalid token (4004), will refresh and reconnect)i  i  i  i$  i%  i&  i'  i(  i)  i*  i+  i,  i-  i.  i/  i0  i1  z9[%s] Session error (%d), clearing session for re-identifyz2[%s] Max reconnect attempts reached (QQCloseError)z[%s] WebSocket error: %szConnection interruptedz#[%s] Max reconnect attempts reached)r   r   	monotonic_read_eventsr   r   r   r   r   r   rh   r!   r8   r   r9   r   r   r   rr   r7   r6   sleep
_reconnectr   r   r   r   r   )r$   backoff_idxconnect_timequick_disconnect_countr   r   durationdescs           r&   r   zQQAdapter._listen_loop  s      !"m I	%H%#~//''))))))))))*&&)    p p p} FFFFFx>MJ	    >++l:888\A=M=M*a/*KKB .	   .1KKKd M  
 --1R&* .   
  L ./*'')))""#6777 <''59T\\11xDLLA4=RV   ))$d&6&6&6% *    FFFFF 4<<KK?(  
 #&<<<!-(8999999999!__[99999999 )&'12..#q(HHHH 4<<KKO   *.D&-0D*    & KKS  
 (,D$%)DN55555555 "#K-.**1$K"&<<<%Y[_[hiii % % %} FFFFF94=#NNN'')))""#;<<<"888LL!FVVVFFFFF55555555 %"#K-.**1$K%u m I	% I	% I	% I	% I	%sX   1A N?
N?!K;.B?K;3A9K;27K;/AK;9B7K;;N?N:A5N:%N::N?r   r   c                p  K   t           t          |t          t                     dz
                     }t                              d| j        ||dz              t          j        |           d{V  d| _        	 | 	                                 d{V  | 
                                 d{V }|                     |           d{V  |                                  t                              d| j                   dS # t          $ r,}t                              d| j        |           Y d}~dS d}~ww xY w)	z<Attempt to reconnect the WebSocket. Returns True on success.r   z([%s] Reconnecting in %ds (attempt %d)...Nr   z[%s] ReconnectedTz[%s] Reconnect failed: %sF)r5   minlenr   r   rh   r   r   r   r   r   r   r   r   r   )r$   r   delayr   r   s        r&   r   zQQAdapter._reconnect^  s_     !#k37H3I3IA3M"N"NO6M!O		
 	
 	
 mE"""""""""#' 		$$&&&&&&&&& $ 5 5 7 7777777K--,,,,,,,,,  """KK*DM:::4 	 	 	NN6sKKK55555	s   :BC? ?
D5	!D00D5c                  K   | j         st          d          | j        r#| j         r| j         j        s| j                                          d{V }|j        t          j        j        k    r2| 	                    |j
                  }|r|                     |           n|j        t          j        j        fv rnl|j        t          j        j        k    rt          |j
        |j                  |j        t          j        j        t          j        j        fv rt          d          | j        r| j         r| j         j        dS dS dS dS dS dS )z.Read WebSocket frames until connection closes.zWebSocket not connectedNzWebSocket closed)r   ro   r   r   receivetyper   	WSMsgTypeTEXT_parse_jsonr   _dispatch_payloadPINGCLOSEr   r   CLOSEDERROR)r$   msgpayloads      r&   r   zQQAdapter._read_eventsu  sp     x 	:8999m 	7 	7 	7((********Cx7,111**3844 4**7333g/4666W.444"38SY777g/68I8OPPP"#5666 m 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7r'   c                  K   	 | j         rt          j        | j                   d{V  | j        r| j        j        r:	 | j                            d| j        d           d{V  n8# t          $ r+}t          
                    d| j        |           Y d}~nd}~ww xY w| j         dS dS # t          j        $ r Y dS w xY w)zSend periodic heartbeats (QQ Gateway expects op 1 heartbeat with latest seq).

        The interval is set from the Hello (op 10) event's heartbeat_interval.
        QQ's default is ~41s; we send at 80% of the interval to stay safe.
        Nr   opdz[%s] Heartbeat failed: %s)r   r   r   r   r   r   	send_jsonr   r   r   debugrh   r   )r$   r   s     r&   r   zQQAdapter._heartbeat_loop  s%     	- RmD$<=========x 48? R(,,ADN-K-KLLLLLLLLLL  R R RLL!<dmSQQQQQQQQR - R R R R R % 	 	 	DD	s:   :B+ (A( 'B+ (
B2!BB+ B
B+ +B>=B>c                  K   |                                   d{V }dd| dddgdddd	d
d}	 | j        rN| j        j        sB| j                            |           d{V  t                              d| j                   dS t                              d| j                   dS # t          $ r,}t          	                    d| j        |           Y d}~dS d}~ww xY w)ae  Send op 2 Identify to authenticate the WebSocket connection.

        After receiving op 10 Hello, the client must send op 2 Identify with
        the bot token and intents. On success the server replies with a
        READY dispatch event.

        Reference: https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/reference.html
        N   r   i  Br   r   macOSzhermes-agent)z$osz$browserz$device)r   intentsshard
propertiesr  z[%s] Identify sentz2[%s] Cannot send Identify: WebSocket not connectedz [%s] Failed to send Identify: %s)
r   r   r   r  r   r   rh   r   r   r   )r$   r   identify_payloadr   s       r&   _send_identifyzQQAdapter._send_identify  sI      ((********)%))
 Q" .-  
 
"		Qx  h(()9:::::::::0$-@@@@@H$-      	Q 	Q 	QLL;T]CPPPPPPPPP	Qs   AB(  B( (
C2!CCc                  K   |                                   d{V }dd| | j        | j        dd}	 | j        rZ| j        j        sN| j                            |           d{V  t                              d| j        | j        | j                   dS t          	                    d| j                   dS # t          $ r:}t                              d| j        |           d| _        d| _        Y d}~dS d}~ww xY w)	zSend op 6 Resume to re-authenticate after a reconnection.

        Reference: https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/reference.html
        N   r   )r   
session_idseqr  z([%s] Resume sent (session_id=%s, seq=%s)z0[%s] Cannot send Resume: WebSocket not connectedz[%s] Failed to send Resume: %s)r   r   r   r   r   r  r   r   rh   r   r   r   )r$   r   resume_payloadr   s       r&   _send_resumezQQAdapter._send_resume  sM     
 ((********)%))".~ 
 
	"x  h((888888888>M$N	     F      	" 	" 	"LL94=#NNN#D!DNNNNNNN		"s   AB7  B7 7
C;/C66C;c                v    	 t          j                    }|                    |           S # t          $ r Y dS w xY w)zSchedule a coroutine, silently skipping if no event loop is running.

        This avoids ``RuntimeError: no running event loop`` when tests call
        ``_dispatch_payload`` synchronously outside of ``asyncio.run()``.
        N)r   get_running_loopr   ro   )coroloops     r&   _create_taskzQQAdapter._create_task  sK    	+--D##D))) 	 	 	44	s   '* 
88r  Dict[str, Any]c                   |                     d          }|                     d          }|                     d          }|                     d          }t          |t                    r| j        || j        k    r|| _        |dk    rt          |t                    r|ni }|                     dd          }|d	z  d
z  | _        t                              d| j        || j                   | j	        r/| j        (| 
                    |                                            n'| 
                    |                                            dS |dk    r|r|dk    r|                     |           n|dk    r!t                              d| j                   n}|dv r)t          j        |                     ||                     nP|dk    r)| 
                    |                     |                     n!t                              d| j        |           dS |dk    rdS t                              d| j        |           dS )zPRoute inbound WebSocket payloads (dispatch synchronously, spawn async handlers).r  tsr  N
   heartbeat_intervali0u  g     @@g?zB[%s] Hello received, heartbeat_interval=%dms (sending every %.1fs)r   READYRESUMEDz[%s] Session resumed)C2C_MESSAGE_CREATEGROUP_AT_MESSAGE_CREATEDIRECT_MESSAGE_CREATEGUILD_MESSAGE_CREATEGUILD_AT_MESSAGE_CREATEINTERACTION_CREATEz[%s] Unhandled dispatch: %s   z[%s] Unknown op: %s)r   
isinstancer   r   dictr   r   r  rh   r   r-  r(  r"  _handle_readyr   r   r   _on_message_on_interaction)r$   r  r  r0  r1  r  d_datainterval_mss           r&   r  zQQAdapter._dispatch_payload  sF   [[KKKKKKa 	4>#9Q=O=ODN 88$Q--5QQ2F **%95AAK'2V';c'AD$LLT(	    9DN$>!!$"3"3"5"56666!!$"5"5"7"7888F 77q7G||""1%%%%i2DMBBBB    #D$4$4Q$:$:;;;;***!!$"6"6q"9"9:::::DM1MMMF 88F*DM2>>>>>r'   r  r   c                    t          |t                    rB|                    d          | _        t                              d| j        | j                   dS dS )u7   Handle the READY event — store session_id for resume.r%  z[%s] Ready, session_id=%sN)r=  r>  r   r   r   r   rh   )r$   r  s     r&   r?  zQQAdapter._handle_ready&  sW    a 	V uu\22DKK3T]DDTUUUUU	V 	Vr'   rawOptional[Dict[str, Any]]c                    	 t          j        |           }n,# t          $ r t                              d|            Y d S w xY wt          |t                    r|nd S )Nz [QQBot] Failed to parse JSON: %r)r   loadsr   r   r   r=  r>  )rE  r  s     r&   r  zQQAdapter._parse_json0  sh    	jooGG 	 	 	NN=sCCC44	 %Wd33=ww=s    %A A msg_idc                    t          t          j                              dz  }t          t          j                    j        dd         d          }||z  dz  S )z5Generate a message sequence number in 0..65535 range.i N      i   )r   r   uuiduuid4hex)rI  	time_partrands      r&   _next_msg_seqzQQAdapter._next_msg_seq9  sM     	$$y0	4:<<#BQB',,D E))r'   eventr   c                   K   |j         r%|j        j        r|j         | j        |j        j        <   t	                                          |           d{V  dS )z:Cache the last message ID per chat, then delegate to base.N)
message_idsourcechat_idr   r"   handle_message)r$   rS  r%   s     r&   rX  zQQAdapter.handle_messageD  se       	G 4 	G6;6FDel23gg$$U+++++++++++r'   
event_typec                P  K   t          |t                    sdS t          |                    dd                    }|r|                     |          r#t
                              d| j        |           dS t          |                    dd                    }t          |                    dd                                                    }t          |                    d          t                    r|                    d          ni }|dk    r!| 	                    |||||           d{V  dS |d	v r!| 
                    |||||           d{V  dS |d
v r!|                     |||||           d{V  dS |dk    r!|                     |||||           d{V  dS dS )z(Process an inbound QQ Bot message event.Nidr   z([%s] Duplicate or missing message id: %s	timestampcontentauthorr6  )r7  )r9  r:  r8  )r=  r>  r    r   _is_duplicater   r  rh   r   _handle_c2c_message_handle_group_message_handle_guild_message_handle_dm_message)r$   rY  r  rI  r\  r]  r^  s          r&   r@  zQQAdapter._on_messageJ  s     !T"" 	F QUU4__%% 	++F33 	LL:DM6   Fk2..//	aeeIr**++1133$.quuX$E$EMx2 ---**1fgvyQQQQQQQQQQQ777,,QSSSSSSSSSSSNNN,,QSSSSSSSSSSS222))!VWfiPPPPPPPPPPP 32r'   callback7Optional[Callable[[InteractionEvent], Awaitable[None]]]c                    || _         dS )a\  Register (or clear) the interaction callback.

        Invoked once per ``INTERACTION_CREATE`` event *after* the adapter has
        ACKed the interaction. The callback is responsible for routing the
        button click to the right subsystem (approval resolver, update-prompt
        resolver, etc.) based on the ``button_data`` payload.
        N)r   )r$   rd  s     r&   set_interaction_callbackz"QQAdapter.set_interaction_callbacki  s     &."""r'   c                >  K   t          |t                    sdS 	 t          |          }n9# t          $ r,}t                              d| j        |           Y d}~dS d}~ww xY w|j        s"t                              d| j                   dS 	 |                     |j                   d{V  n># t          $ r1}t                              d| j        |j        |           Y d}~nd}~ww xY wt          	                    d| j        |j
        |j        |j                   | j        }|(t                              d| j        |j                   dS 	  ||           d{V  dS # t          $ r.}t                              d| j        |d	           Y d}~dS d}~ww xY w)
aZ  Handle an ``INTERACTION_CREATE`` event.

        Responsibilities:

        1. Parse the raw payload into an :class:`InteractionEvent`.
        2. ACK the interaction (``PUT /interactions/{id}``) so the client
           stops showing a loading indicator on the button.
        3. Dispatch to the registered interaction callback, if any.
        Nz+[%s] Failed to parse INTERACTION_CREATE: %sz0[%s] INTERACTION_CREATE missing id, skipping ACKz%[%s] Failed to ACK interaction %s: %sz5[%s] Interaction: scene=%s button_data=%r operator=%szA[%s] No interaction callback registered; dropping button click %rz$[%s] Interaction callback raised: %sTr   )r=  r>  rQ   r   r   r   rh   r[  _acknowledge_interactionr   scenebutton_dataoperator_openidr   r  r   )r$   r  rS  r   rd  s        r&   rA  zQQAdapter._on_interactionv  s5      !T"" 	F	+A..EE 	 	 	NN=t}c   FFFFF		 x 	NNBDM   F	//9999999999 	 	 	NN7ux       	 	CM5;(95;P	
 	
 	

 -LLu0  
 F	(5//!!!!!!!!! 	 	 	LL6sT          	sD   + 
A!!AA! B/ /
C*9'C%%C*E$ $
F.#FFr   interaction_idr   c                b  K   | j         st          d          |                                  d{V }d| dt                      d}| j                             t
           d| |d|it                     d{V }|j        d	k    r't          d
|j         d|j        dd                    dS )zACK a button interaction via ``PUT /interactions/{id}``.

        :param interaction_id: The ``id`` field from the
            ``INTERACTION_CREATE`` event.
        :param code: Response code (``0`` = success).
        .   HTTP client not initialized — not connected?Nr   application/jsonr   zContent-Typer   z/interactions/r   r   r   r     zInteraction ACK failed []:    )	r   ro   r   rF   putr/   r2   status_codetext)r$   rm  r   r   r   r   s         r&   ri  z"QQAdapter._acknowledge_interaction  s        	QOPPP((********-e--.*,,
 

 &**77~77$'	 + 
 
 
 
 
 
 
 
 s""%4+; % %9TcT?% %   #"r'   oncealwaysdeny)z
allow-oncezallow-alwaysr{  rM   c                f  K   |j         }|sdS t          |          }||\  }}| j                            |          }|$t                              d| j        ||           dS 	 ddlm}  |||          }t          	                    d| j        ||||j
                   n9# t          $ r,}	t                              d| j        ||	           Y d}	~	nd}	~	ww xY wdS t          |          }
|
|                     |
|j
                   dS t                              d| j        ||j                   dS )u  Route ``INTERACTION_CREATE`` button clicks to the right subsystem.

        - ``approve:<session_key>:<decision>`` →
          :func:`tools.approval.resolve_gateway_approval`
          (unblocks the agent thread waiting on a dangerous-command approval).
        - ``update_prompt:<answer>`` →
          writes the answer to ``~/.hermes/.update_response`` for the
          detached ``hermes update --gateway`` process to consume.
        - Anything else is logged at DEBUG and ignored.

        Installed as the adapter's default interaction callback in
        ``__init__``. Callers can replace via
        :meth:`set_interaction_callback` to route clicks elsewhere (or pass
        ``None`` to drop them entirely).
        Nz.[%s] Unknown approval decision %r (session=%s)r   )resolve_gateway_approvalzK[%s] Button resolved %d approval(s) for session %s (choice=%s, operator=%s)z7[%s] resolve_gateway_approval failed for session %s: %sz4[%s] Unrecognised button_data %r from interaction %s)rk  rP   _APPROVAL_BUTTON_TO_CHOICEr   r   r   rh   tools.approvalr}  r   rl  r   r   rR   _write_update_responser  r[  )r$   rS  rk  approvalsession_keydecisionchoicer}  countr   update_answers              r&   r   z'QQAdapter._default_interaction_dispatch  s     & ' 	F-k::$,!K488BBF~DM8[    DCCCCC00fEE/M5+v)	       MM;       
 F7DD$''u7LMMMFBM;	
 	
 	
 	
 	
s   %;B! !
C+"CCr   answeroperatorc                R   	 ddl m}  |            }|dz  }|                    d          }|                    |            |                    |           t
                              d| |pd           dS # t          $ r&}t
                              d|           Y d}~dS d}~ww xY w)	a~  Atomically write the update-prompt answer to ``.update_response``.

        Mirrors the Discord / Telegram / Feishu adapters: the detached
        ``hermes update --gateway`` watcher polls this file for a ``y``/``n``
        response to its interactive prompts (stash-restore, config migration).
        Writes via ``tmp + rename`` so a partial write can't fool the reader.
        r   )get_hermes_homez.update_responsez.tmpz"QQ update prompt answered %r by %sz	(unknown)z#Failed to write update response: %sN)	hermes_constantsr  with_suffix
write_textreplacer   r   r   r   )r  r  r  homeresponse_pathtmpr   s          r&   r  z QQAdapter._write_update_response  s    	E888888"?$$D #55M++F33CNN6"""KK&&&KK4/K      	E 	E 	ELL>DDDDDDDDD	Es   A2A6 6
B& B!!B&r]  r^  r\  c                  K   t          |                    dd                    }|sdS |                     |          sdS |}|                    d          }t                              d| j        ||r
|dd         nd|r)t          |t                    rt          |          nd dnd	           |rt          |t                    rt          |          D ]\  }	}
t          |
t                    rut                              d
| j        |	|
                    dd          t          |
                    dd                    dd         |
                    dd                     |                     |           d{V }|d         }|d         }|d         }|d         }|rEd                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}t                              d| j        t          |          t          |                     |                     |           d{V }|                     ||d                   }|d         r||d         z   }||d         z   }|                                s|sdS d| j        |<   t#          |                     ||d          ||                     ||          |||||                     |                    }|                     |           d{V  dS )z%Handle a C2C (private) message event.user_openidr   Nattachmentsz1[%s] C2C message: id=%s content=%r attachments=%srb   r   z itemsri   z7[%s] attachment[%d]: content_type=%s url=%s filename=%scontent_typer   P   filename
image_urlsimage_media_typesvoice_transcriptsattachment_info


z*[%s] After processing: images=%d, voice=%dquote_blockc2cdmrW  user_id	chat_typerV  rx  message_typeraw_messagerU  
media_urlsmedia_typesr\  )r    r   _is_dm_allowedr   r   rh   r=  listr  	enumerater>  _process_attachmentsjoinr   _process_quoted_context_merge_quote_intor   r   build_source_detect_message_type_parse_qq_timestamprX  )r$   r  rI  r]  r^  r\  r  rx  attachments_raw_i_att
att_resultr  r  r  r  voice_blockquotedrS  s                      r&   r`  zQQAdapter._handle_c2c_message-  s      &**]B7788 	F"";// 	F%%..?M#+GCRCLL #:ot+L+LS3'''RS[[[[
	
 
	
 
	
  
	z/4@@ 
	%o66 	 	DdD)) KKQ44DHHUB//00"5R00    44_EEEEEEEE
-
&':;&':;$%67  	))$566K9=V,33555;   	 ::<<%077999$  	8M
OO!""		
 	
 	
 33A66666666%%dF=,ABB, 	P#f\&::J 1F;N4O Ozz|| 	J 	F+0K($$## %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r'   c                  K   t          |                    dd                    }|sdS |                     |t          |                    dd                              sdS |                     |          }|                     |                    d                     d{V }|d         }	|d         }
|d         }|d	         }|rEd
                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}|                     |           d{V }|                     ||d                   }|d         r|	|d         z   }	|
|d         z   }
|                                s|	sdS d| j	        |<   t          |                     |t          |                    dd                    d          ||                     |	|
          |||	|
|                     |                    }|                     |           d{V  dS )zHandle a group @-message event.group_openidr   Nmember_openidr  r  r  r  r  r  r  r  groupr  r  )r    r   _is_group_allowed_strip_at_mentionr  r  r   r  r  r   r   r  r  r  rX  )r$   r  rI  r]  r^  r\  r  rx  r  r  r  r  r  r  r  rS  s                   r&   ra  zQQAdapter._handle_group_message  s      1554455 	F%%c&**_b"A"ABB
 
 	 F %%g..44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67  	))$566K9=V,33555;   	 ::<<%077999$  33A66666666%%dF=,ABB, 	P#f\&::J 1F;N4O Ozz|| 	J 	F,3L)$$$FJJ;;<<! %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r'   c                  K   t          |                    dd                    }|sdS t          |                    dd                    }t          |                    dd                    }|                     |p||          s$t                              d| j        ||           dS t          |                    d          t                    r|                    d          ni }	t          |	                    dd                    p"t          |                    d	d                    }
|}|                     |                    d
                     d{V }|d         }|d         }|d         }|d         }|rEd	                    |          }|
                                r|dz   |z   
                                n|}|r0|
                                r|dz   |z   
                                n|}|                     |           d{V }|                     ||d                   }|d         r||d         z   }||d         z   }|
                                s|sdS d| j        |<   t          |                     |t          |                    dd                    |
pdd          ||                     ||          |||||                     |                    }|                     |           d{V  dS )z%Handle a guild/channel message event.
channel_idr   Nguild_idr[  z5[%s] Guild message blocked by ACL: channel=%s user=%smembernickusernamer  r  r  r  r  r  r  r  guildr  )rW  r  	user_namer  r  )r    r   r  r   r  rh   r=  r>  r  r  r   r  r  r   r   r  r  r  rX  )r$   r  rI  r]  r^  r\  r  r  	author_idr  r  rx  r  r  r  r  r  r  r  rS  s                       r&   rb  zQQAdapter._handle_guild_message  sH      |R0011
 	F
 quuZ,,--

4,,--	%%h&<*iHH 	LLGz9   F$.quuX$E$EMx26::fb))**Mc&**Z2L2L.M.M44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67 	))$566K9=V,33555;   	 ::<<%077999$  33A66666666%%dF=,ABB, 	P#f\&::J 1F;N4O Ozz|| 	J 	F*1J'$$"FJJtR0011,$!	 %   22:?PQQ!)..y99
 
 
 !!%(((((((((((r'   c                  K   t          |                    dd                    }|sdS t          |                    dd                    }|                     |          s$t                              d| j        ||           dS |}|                     |                    d                     d{V }	|	d         }
|	d         }|	d	         }|	d
         }|rEd                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}| 	                    |           d{V }| 
                    ||d                   }|d         r|
|d         z   }
||d         z   }|                                s|
sdS d| j        |<   t          |                     |t          |                    dd                    d          ||                     |
|          |||
||                     |                    }|                     |           d{V  dS )z Handle a guild DM message event.r  r   Nr[  z.[%s] Guild DM blocked by ACL: guild=%s user=%sr  r  r  r  r  r  r  r  r  r  r  )r    r   r  r   r  rh   r  r  r   r  r  r   r   r  r  r  rX  )r$   r  rI  r]  r^  r\  r  r  rx  r  r  r  r  r  r  r  rS  s                    r&   rc  zQQAdapter._handle_dm_message  s      quuZ,,-- 	F
 

4,,--	""9-- 	LL@x   F44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67 	))$566K9=V,33555;   	 ::<<%077999$  33A66666666%%dF=,ABB, 	P#f\&::J 1F;N4O Ozz|| 	J 	F(,H%$$ FJJtR0011 %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r'   c                |  K   dg g d}	 t          |                    dd          pd          dk    r|S n# t          t          f$ r |cY S w xY w|                    d          }t	          |t
                    r|s|S g }g }|D ]}t	          |t                    st          |                    dd                                                    }|r|	                    |           |                    d          }t	          |t
                    r/|D ],}	t	          |	t                    r|	                    |	           -| 
                    |           d	{V }
|
                    d
          pg }|
                    d          pd}|
                    d          pg }|
                    d          pg }g }|r(|	                    d                    |                     |D ]}|	                    |           |r|	                    |           |s|s|S |rdd                    |          z   }nd}|||dS )u:  Process the quoted message a user is replying to.

        When a user replies while quoting another message, the platform sets
        ``message_type = 103`` and pushes the referenced message's content and
        attachments inside ``msg_elements[0]``. The old adapter ignored
        ``msg_elements`` entirely, so:

        - Quoted text was surfaced only when the user typed something of
          their own — bare quote-replies showed nothing.
        - Quoted attachments (images, voice, files) were never downloaded
          or described.
        - Quoted voice messages specifically produced no transcript, so the
          LLM had no way to see what the user was referring to.

        This method parses ``msg_elements`` and runs the quoted attachments
        through the same :meth:`_process_attachments` pipeline as the main
        message body, so quoted voice messages get STT transcripts and
        quoted images are cached identically.

        :param d: Raw inbound message dict (from the WS dispatch payload).
        :returns: Dict with keys:

            - ``quote_block``: string to prepend to the user's text body
              (empty when there's nothing quoted).
            - ``image_urls``: list of cached quoted-image paths.
            - ``image_media_types``: parallel list of image MIME types.
        r   )r  r  r  r  r   g   msg_elementsr]  r  Nr  r  r  r   z[Quoted message]:
r  z[Quoted message]: (image))r   r   	TypeError
ValueErrorr=  r  r>  r    r   appendr  r  )r$   r  emptyelementsquoted_text_partsall_attachmentselemetexteattsar  quoted_voicequoted_infoquoted_imagesquoted_image_typeslinesr0  r  s                     r&   r  z!QQAdapter._process_quoted_contextb  s     @ !#
 
	155++0q11S88 9:& 	 	 	LLL	 55(((D)) 	 	L
 (*02 
	2 
	2DdD)) B//006688E 0!((///HH]++E%&& 2 2 2A!!T** 2'..q11144_EEEEEEEE
!~~&9::@b nn%677=2"|44:'^^,?@@FB 	6LL"344555 	 	ALLOOOO 	&LL%%% 	] 	L 	6/$))E2B2BBKK 6K ''!3
 
 	
s   *6 AArx  r  c                h    |s| S |                                  r| d|                                   S |S )z=Prepend ``quote_block`` to *text*, separated by a blank line.r  )r   )rx  r  s     r&   r  zQQAdapter._merge_quote_into  sG      	K::<< 	6!--t--33555r'   r  r  r  c                <   | st           j        S |st           j        S |r|d                                         nd}d|v sd|v sd|v rt           j        S d|v rt           j        S d|v sd|v rt           j        S t                              d	|           t           j        S )
z4Determine MessageType from attachment content types.r   r   audiovoicesilkvideoimagephotoz3Unknown media content_type '%s', defaulting to TEXT)r   r  PHOTOr   VOICEVIDEOr   r  )r  r  
first_types      r&   r  zQQAdapter._detect_message_type  s      	$## 	%$$/:B[^))+++
j  Gz$9$9Vz=Q=Q$$j  $$j  Gz$9$9$$A	
 	
 	
 r'   r  c           	     0  K   t          |t                    sg g g ddS g }g }g }g }|D ]}t          |t                    st          |                    dd                                                                                    }t          |                    dd                                                    }t          |                    dd                    }	|                    d          rd| }
n|r|}
nd}
t          	                    d| j
        ||
d	d
         |	           |                     ||	          r_t          |                    d          t                    r5t          |                    dd                                                    nd}t          |                    d          t                    r5t          |                    dd                                                    nd}|                     |
||	|pd	|pd	           d	{V }|r;|                    d|            t          	                    d| j
        |           <t                              d| j
        |
d	d                    |                    d           ||                    d          r	 |                     |
|           d	{V }|rLt           j                            |          r-|                    |           |                    |pd           n#|r!t                              d| j
        |           !# t&          $ r,}t          	                    d| j
        |           Y d	}~Rd	}~ww xY w	 |                     |
|           d	{V }|r|                    d|	p| d           # t&          $ r,}t          	                    d| j
        |           Y d	}~d	}~ww xY w|rd                    |          nd}||||dS )u  Process inbound attachments (all message types).

        Mirrors OpenClaw's ``processAttachments`` — handles images, voice, and
        other files uniformly.

        Returns a dict with:
        - image_urls: list[str]  — cached local image paths
        - image_media_types: list[str] — MIME types of cached images
        - voice_transcripts: list[str] — STT transcripts for voice messages
        - attachment_info: str — text description of non-image, non-voice attachments
        r   )r  r  r  r  r  r   r  //https:z@[%s] Processing attachment: content_type=%s, url=%s, filename=%sNr  asr_refer_textvoice_wav_urlr  r  z[Voice] z[%s] Voice transcript: %sz[%s] Voice STT failed for %sra   u   [Voice] [语音识别失败]image/z
image/jpegz)[%s] Cached image path does not exist: %sz[%s] Failed to cache image: %sz[Attachment: ]z#[%s] Failed to cache attachment: %sr  )r=  r  r>  r    r   r   r   
startswithr   r  rh   _is_voice_content_type_stt_voice_attachmentr  r   _download_and_cacher   pathisfiler   r  )r$   r  r  r  r  other_attachmentsattcturl_rawr  r   	asr_referr  
transcriptcached_pathr   r  s                    r&   r  zQQAdapter._process_attachments  s      +t,, 	 %'%'#%	   !#
')')') G	\ G	\Cc4(( SWW^R00117799??AAB#''%,,--3355G377:r2233H!!$'' (w(( LLRCRC   **2x88 0\ "#''*:";";SAAC 0"5566<<>>>  "#''/":":C@@C4455;;===  $(#=#=#,#4"/"74 $> $ $      
  M%,,-D
-D-DEEELL!<dmZXXXXNN#A4=RUVYWYVYRZ[[[%,,-KLLLLx(( \W(,(@(@b(I(I"I"I"I"I"I"IK" rw~~k'B'B "))+666)001C|DDDD$ G M'  
 ! W W WLL!A4=RUVVVVVVVVW\(,(@(@b(I(I"I"I"I"I"I"IK" T)001RR1R1R1RSSS  \ \ \LL!FWZ[[[[[[[[\ ;LS$))$5666QS$!2!2.	
 
 	
s1   ;BM


N !M;;N 9N??
O5	!O00O5r   r  Optional[str]c                  K   ddl m}  ||          st          d|dd                    | j        sdS 	 | j                            |d|                                            d{V }|                                 |j        }nB# t          $ r5}t          
                    d| j        |dd         |           Y d}~dS d}~ww xY w|                    d	          r&t          j        |          pd
}t          ||          S |dk    s|                    d          r|                     ||           d{V S t#          t%          |          j                  j        pd}t+          ||          S )z$Download a URL and cache it locally.r   is_safe_urlzBlocked unsafe URL: Nr  r   )r   r   z[%s] Download failed for %s: %sr  z.jpgr  audio/qq_attachment)tools.url_safetyr  r  r   r   _qq_media_headersr   r]  r   r   r  rh   r  	mimetypesguess_extensionr   _convert_audio_to_wavr   r   r  r   r   )	r$   r   r  r  r   r   r   extr  s	            r&   r  zQQAdapter._download_and_cacheN  s     000000{3 	@>CH>>???  	4	*....00 /        D
 !!###<DD 	 	 	LL14=#crc(C   44444		 ""8,, 		=+L99CVC)$444W$$(?(?(I(I$ 33D#>>>>>>>>>HSMM.//4GH,T8<<<s   AB	 	
C*CCr  c                   |                                                                  }|                                                                 |dk    s|                    d          rdS d}t          fd|D                       rdS dS )z0Check if an attachment is a voice/audio message.r  r  T)	.silk.amr.mp3.wav.ogg.m4a.aacz.speex.flacc              3  B   K   | ]}                     |          V  d S N)endswith).0r	  fns     r&   	<genexpr>z3QQAdapter._is_voice_content_type.<locals>.<genexpr>  s/      ==Cr{{3======r'   F)r   r   r  any)r  r  r  _VOICE_EXTENSIONSr  s       @r&   r  z QQAdapter._is_voice_content_typeq  s     !!''))^^##%%==BMM(33=4

 ====+<===== 	4ur'   Dict[str, str]c                ,    | j         rdd| j          iS i S )zReturn Authorization headers for QQ multimedia CDN downloads.

        QQ's multimedia URLs (multimedia.nt.qq.com.cn) require the bot's
        access token in an Authorization header, otherwise the download
        returns a non-200 status.
        r   r   )r   r   s    r&   r  zQQAdapter._qq_media_headers  s,      	D#%Bd.@%B%BCC	r'   Nr  r  r  c          	       K   |r+t                               d| j        |dd                    |S |}d}|r>|                    d          rd| }|}d}t                               d| j                   d	d
lm}  ||          s%t                               d|dd                    dS 	 | j        s"t                               d| j                   dS |                                 }	t                               d| j        |dd         |t          |	                     | j        
                    |d|	d           d{V }
|
                                 |
j        }t                               d| j        t          |          |
j        
                    dd                     t          |          dk     r0t                               d| j        t          |                     dS |r~d	dl}|                    dd          5 }|                    |           |j        }ddd           n# 1 swxY w Y   t                               d| j        t          |                     nt                               d| j        |           |                     ||           d{V }|r!t)          |                                          s"t                               d| j                   dS t                               d| j        |           |                     |           d{V }	 t/          j        |           n# t2          $ r Y nw xY w|r*t                               d| j        |dd                    n t                               d| j                   |S # t4          j        t4          j        t:          f$ r?}t                               d| j        t=          |          j        |           Y d}~dS d}~ww xY w)u  Download a voice attachment, convert to wav, and transcribe.

        Priority:
        1. QQ's built-in ``asr_refer_text`` (Tencent's own ASR — free, no API call).
        2. Self-hosted STT on ``voice_wav_url`` (pre-converted WAV from QQ, avoids SILK decoding).
        3. Self-hosted STT on the original attachment URL (requires SILK→WAV conversion).

        Returns the transcript text, or None on failure.
        z%[%s] STT: using QQ asr_refer_text: %rNd   Fr  r  Tz1[%s] STT: using voice_wav_url (pre-converted WAV)r   r   z[QQ] STT blocked unsafe URL: %sr  z[%s] STT: no HTTP clientz<[%s] STT: downloading voice from %s (pre_wav=%s, headers=%s)r   )r   r   r   z.[%s] STT: downloaded %d bytes, content_type=%szcontent-typeunknownr2  z8[%s] STT: downloaded data too small (%d bytes), skippingr  suffixdeletez5[%s] STT: using pre-converted WAV directly (%d bytes)z([%s] STT: converting to wav, filename=%rz.[%s] STT: ffmpeg conversion produced no outputz[%s] STT: calling ASR on %sz[%s] STT success: %rz'[%s] STT: ASR returned empty transcriptz,[%s] STT failed for voice attachment: %s: %s) r   r  rh   r  r  r  r   r   r  rT   r   r   r]  r  r   tempfileNamedTemporaryFilewriter   _convert_audio_to_wav_filer   exists	_call_sttr   unlinkOSErrorr   HTTPStatusErrorTransportErrorIOErrorr
  r)   )r$   r   r  r  r  r  download_url
is_pre_wavr  download_headersr   
audio_datar#  r  wav_pathr  r   s                    r&   r  zQQAdapter._stt_voice_attachment  s     &  	"LL7W[X[W[H\   "! 
 	]''-- 9 8 8 8(LJLLLdm\\\000000{<(( 	NN<l3B3>OPPP4R	$ 94=IIIt#5577LLNSbS!%&&   *..(!%	 /        D !!###JLL@J  ;;	   :##NM
OO  
 t   00u0MM (QTIIj)))"xH( ( ( ( ( ( ( ( ( ( ( ( ( ( ( KM
OO    >x   "&!@!@X!V!VVVVVVV  tH~~'<'<'>'>  NNH$-    4 LL6xPPP#~~h77777777J	(####     Y3T]JtPStDTUUUUH$-XXX%u';WE 	 	 	NN>S		"	   44444	su   +'N DN *N H0$N 0H44N 7H48B2N ,<N )L> =N >
MN 
MAN !O<=4O77O<r1  bytesc           	       K   ddl }t          |          j        r&t          |          j                                        n|                     |          }t
                              d| j        t          |          ||dd                    |	                    |d          5 }|
                    |           |j        }ddd           n# 1 swxY w Y   |                    dd          d         d	z   }|                     ||           d{V }|s|                     ||           d{V }|s|                     ||           d{V }	 t!          j        |           n# t$          $ r Y nw xY w|S )
a"  Convert audio bytes to a temp .wav file using pilk (SILK) or ffmpeg.

        QQ voice messages are typically SILK format which ffmpeg cannot decode.
        Strategy: always try pilk first, fall back to ffmpeg if pilk fails.

        Returns the wav file path, or None on failure.
        r   Nz7[%s] STT: audio_data size=%d, ext=%r, first_20_bytes=%r   Fr   .r   r  )r#  r   r!  r   _guess_ext_from_datar   r   rh   r  r$  r%  r   rsplit_convert_silk_to_wav_convert_ffmpeg_to_wav_convert_raw_to_wavr   r)  r*  )	r$   r1  r  r#  r	  tmp_srcsrc_pathr2  results	            r&   r&  z$QQAdapter._convert_audio_to_wav_file  s      	 H~~$7DNN!'')))**:66 	
 	EM
OOssO	
 	
 	
 ((E(BB 	$gMM*%%%|H	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ ??3**1-6 008DDDDDDDD  	K66xJJJJJJJJF  	J33JIIIIIIIIF	Ih 	 	 	D	 s$   %CCCE& &
E32E3r   c                6   | dd         dk    s| dd         dk    rdS | dd         dk    rdS | dd	         d
k    rdS | dd	         dk    rdS | dd         dv rdS | dd	         dk    s| dd	         dk    rdS | dd	         dk    s| dd	         dk    rdS dS )z&Guess file extension from magic bytes.N	   	   #!SILK_V3      #!SILKr  r     !rK  s   RIFFr  s   fLaCr  )s   s   s   r  s   0&us   OggSr  s       s      r  rX   r   s    r&   r7  zQQAdapter._guess_ext_from_data@  s     8|##tBQBx9'<'<78x78w68w78>>>68***d2A2h:M.M.M68***d2A2h:M.M.M6vr'   c                V    | dd         dk    p| dd         dk    p| dd         dk    S )z+Check if bytes look like a SILK audio file.NrK  rC  r  rD  r@  rA  rX   rE  s    r&   _looks_like_silkzQQAdapter._looks_like_silkT  s;     BQBx9$XRaRH(<XRaRL@XXr'   r=  r2  c                  K   	 ddl }n1# t          $ r$ t                              d| j                   Y dS w xY w	 |                    ||d           t          |                                          rt          |                                          j	        dk    rZt          
                    d| j        t          |          j        t          |                                          j	                   |S n8# t          $ r+}t          
                    d| j        |           Y d}~nd}~ww xY w|                    d	d
          d         dz   }	 ddl}|                    ||           |                    ||d           t          |                                          rt          |                                          j	        dk    rt          
                    d| j        t          |          j        t          |                                          j	                   |	 t!          j        |           S # t$          $ r Y S w xY wn8# t          $ r+}t          
                    d| j        |           Y d}~nd}~ww xY w	 t!          j        |           n:# t$          $ r Y n.w xY w# 	 t!          j        |           w # t$          $ r Y w w xY wxY wdS )zConvert audio file to WAV using the pilk library.

        Tries the file as-is first, then as .silk if the extension differs.
        pilk can handle SILK files with various headers (or no header).
        r   NuK   [%s] pilk not installed — cannot decode SILK audio. Run: pip install pilk>  )rate,   z([%s] pilk converted %s to wav (%d bytes)z&[%s] pilk direct conversion failed: %sr6  r   r  z3[%s] pilk converted %s (as .silk) to wav (%d bytes)z%[%s] pilk .silk conversion failed: %s)pilkImportErrorr   r   rh   silk_to_wavr   r'  statst_sizer  r   r   r8  shutilcopy2r   r)  r*  )r$   r=  r2  rL  r   	silk_pathrQ  s          r&   r9  zQQAdapter._convert_silk_to_wavY  s&     	KKKK 	 	 	NN]   44		WXxe<<<H~~$$&&  4>>+>+>+@+@+H2+M+M>MNN'NN''))1	     	W 	W 	WLLA4=RUVVVVVVVV	W OOC++A.8		MMMLL9---Yu===H~~$$&&  4>>+>+>+@+@+H2+M+MIMNN'NN''))1	    	)$$$$     	V 	V 	VLL@$-QTUUUUUUUU	V	)$$$$   	)$$$$    ts   	 *77B<C9 9
D.!D))D.CI (H==
I
	I
J, 
J!I>9J, >JJ, J 
J)(J),K.KK
KKKKc                  K   	 ddl }|                    |d          5 }|                    d           |                    d           |                    d           |                    |           ddd           n# 1 swxY w Y   |S # t          $ r,}t                              d| j	        |           Y d}~dS d}~ww xY w)u   Last resort: try writing audio data as raw PCM 16-bit mono 16kHz WAV.

        This will produce garbage if the data isn't raw PCM, but at least
        the ASR engine won't crash — it'll just return empty.
        r   Nwr   r  rI  z [%s] raw PCM fallback failed: %s)
waverz   setnchannelssetsampwidthsetframeratewriteframesr   r   r  rh   )r$   r1  r2  rV  wfr   s         r&   r;  zQQAdapter._convert_raw_to_wav  s     	KKK8S)) +R""""""&&&z***	+ + + + + + + + + + + + + + +
 O 	 	 	LL;T]CPPP44444	s;   B AA?3B ?BB BB 
C!B==Cc                4  K   	 t          j        ddd|dddd|t           j        j        t           j        j                   d	{V }t          j        |                                d
           d	{V  |j        dk    rz|j        r|j        	                                 d	{V nd}t                              d| j        t          |          j        |d	d                             d                     d	S nE# t           j        t"          f$ r,}t                              d| j        |           Y d	}~d	S d	}~ww xY wt          |                                          r*t          |                                          j        dk    r5t                              d| j        t          |          j                   d	S t                              d| j        t          |          j        t          |                                          j                   |S )z'Convert audio file to WAV using ffmpeg.ffmpegz-yz-iz-ar16000z-ac1)stdoutstderrN   r   r   r'   z[%s] ffmpeg failed for %s: %sru  r  )errorsz [%s] ffmpeg conversion error: %srK  z+[%s] ffmpeg produced no/small output for %sz*[%s] ffmpeg converted %s to wav (%d bytes))r   create_subprocess_exec
subprocessDEVNULLPIPEwait_forwait
returncodera  readr   r   rh   r   r   decodeTimeoutErrorFileNotFoundErrorr'  rO  rP  r  )r$   r=  r2  procra  r   s         r&   r:  z QQAdapter._convert_ffmpeg_to_wav  s4     	 7)1).        D "499;;;;;;;;;;;;!##59[It{//111111111c3MNN'4C4L''y'99	   t $ $&78 	 	 	NN=t}cRRR44444	 H~~$$&& 	$x..*=*=*?*?*G2*M*MNN=X#  
 48MNNNN!!)		
 	
 	
 s   C4C; ;D=!D88D=Optional[Dict[str, str]]c                   | j         j        pi }|                    d          }t          |t                    r|                    d          dur|                    d          p|                    dd          }|                    d          p|                    dd          }|                    d	d          }|r|r|                    d
          ||pddS |rB|                    dd          }dddd}|                    |d          }|r|||p|dv rdnddS t          j        dd          }|rCt          j        dd          }t          j        dd          }|                    d
          ||dS dS )uz  Resolve STT backend configuration from config/environment.

        Priority:
        1. Plugin-specific: ``channels.qqbot.stt`` in config.yaml → ``self.config.extra["stt"]``
        2. QQ-specific env vars: ``QQ_STT_API_KEY`` / ``QQ_STT_BASE_URL`` / ``QQ_STT_MODEL``
        3. Return None if nothing is configured (STT will be skipped, QQ built-in ASR still works).
        sttenabledFbaseUrlbase_urlr   apiKeyapi_keymodel/z	whisper-1)rv  rx  ry  providerzaiz+https://open.bigmodel.cn/api/coding/paas/v4zhttps://api.openai.com/v1)r|  openaiglm)r|  r~  zglm-asrQQ_STT_API_KEYQQ_STT_BASE_URLQQ_STT_MODELN)rs   r   r   r=  r>  rstripr   r   )	r$   r   stt_cfgrv  rx  ry  r{  _PROVIDER_BASE_URLS
qq_stt_keys	            r&   _resolve_stt_configzQQAdapter._resolve_stt_config  s    !'R ))E""gt$$ 	Y)?)?u)L)L{{9--LZ1L1LHkk(++Iw{{9b/I/IGKK,,E G  ( 4 4&"1k    ";;z599 I9H' '#
 /228R@@ $,#*!& "_2:n2L2LYYR]	   Y/44
 
	y!= H Ini88E$OOC00%   tr'   c           	       K   |                                  }|s"t                              d| j                   dS |d         }|d         }|d         }	 t	          |d          5 }| j                            | ddd	| id
t          |          j        |dfid|id           d{V }ddd           n# 1 swxY w Y   |	                                 |
                                }|                    dg           }	|	rX|	d                             di                               dd          }
|
                                r|
                                S |                    dd          }|                                r|                                S dS # t          j        t          f$ r6}t                              d| j        ||dd         |           Y d}~dS d}~ww xY w)a  Call an OpenAI-compatible STT API to transcribe a wav file.

        Uses the provider configured in ``channels.qqbot.stt`` config,
        falling back to QQ's built-in ``asr_refer_text`` if not configured.
        Returns None if STT is not configured or the call fails.
        z9[%s] STT not configured (no stt config or QQ_STT_API_KEY)Nrv  rx  ry  rbz/audio/transcriptionsr   zBearer filez	audio/wavr   )r   filesr   r   choicesr   r   r]  r   rx  z0[%s] STT API call failed (model=%s, base=%s): %srb   )r  r   r   rh   rz   r   r   r   r   r   r   r   r   r   r+  r-  )r$   r2  r  rv  rx  ry  fr   r>  r  r]  rx  r   s                r&   r(  zQQAdapter._call_stt  sb      **,, 	NNK   4:&)$ 	h%% !.33666,.A.A.AB!DNN$7K#HI!5)  4                       !!###YY[[FjjB//G +!!*..B77;;IrJJ==?? +"==??*::fb))Dzz|| $zz||#4%w/ 	 	 	NNB"   44444	sD   F $AB7+F 7B;;F >B;?BF =F G&0+G!!G&
source_urlc                \  K   ddl }t          |          j        r8t          t          |          j                  j                                        nd}|r|dvr|                     |          }|                    |d          5 }|                    |           |j	        }ddd           n# 1 swxY w Y   |
                    dd          d         d	z   }	 |d
k    p|                     |          }|r|                     ||           d{V }	n|                     ||           d{V }	|	sbt                              d| j        |dd         |           t#          |d|           	 t%          j        |           S # t(          $ r Y S w xY wnH# t*          $ r; t#          |d|           cY 	 t%          j        |           S # t(          $ r Y S w xY ww xY w	 	 t%          j        |           n:# t(          $ r Y n.w xY w# 	 t%          j        |           w # t(          $ r Y w w xY wxY w	 t          |                                          }
t%          j        |           t#          |
d          S # t*          $ r,}t                              d| j        |           Y d}~dS d}~ww xY w)zLConvert audio bytes to .wav using pilk (SILK) or ffmpeg, caching the result.r   Nr   )r  r  r  r  r  r  r  r  Fr   r6  r   r  r  z/[%s] audio conversion failed for %s (format=%s)ra   qq_voicezqq_voice.wavz%[%s] Failed to read converted wav: %s)r#  r   r  r   r!  r   r7  r$  r%  r   r8  rG  r9  r:  r   r   rh   r   r   r)  r*  r   
read_bytesr  )r$   r1  r  r#  r	  r<  r=  r2  is_silkr>  wav_datar   s               r&   r  zQQAdapter._convert_audio_to_wav:  s      	
 
##(D*%%*++288::: 	
  
	8c 	"
 	
 	
 ++J77C((E(BB 	$gMM*%%%|H	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ ??3**1-6	WnI(=(=j(I(IG O#888LLLLLLLL#::8XNNNNNNNN OEMssO	   1=M=M=MNN	(####   O  	K 	K 	K,Z9IC9I9IJJJJ	(####   	KO	(####   	(####   	H~~0022HIh,X~FFF 	 	 	LL@$-QTUUU44444	s   B00B47B4BF 0F
FFH G3H 5G


GGGH  G5 5
HHH,HH,
H)&H,(H))H,0AI5 5
J+?!J&&J+methodr  bodyr   floatc                  K   | j         st          d          |                                  d{V }d| dt                      d}	 | j                             |t
           | |||           d{V }|                                }|j        dk    r1t          d|j         d	| d
|                    d|                     |S # t          j
        $ r}	t          d| d|	           |	d}	~	ww xY w)z5Make an authenticated REST API request to QQ Bot API.ro  Nr   rp  rq  rr  rs  zQQ Bot API error [z] z: r   zQQ Bot API timeout [rt  )r   ro   r   rF   requestr/   r   rw  r   r   TimeoutException)
r$   r  r  r  r   r   r   r   r   r   s
             r&   _api_requestzQQAdapter._api_request{  s|        	QOPPP((********-e--.*,,
 
	O*22#T## 3        D 99;;D3&&"3)9 3 3T 3 3xx	4003 3   K% 	O 	O 	ODdDDsDDEE3N	Os   	A?C	 	C3C..C3target_type	target_id	file_type	file_datasrv_send_msg	file_namec                  K   |dk    rd| dnd| d}||d}	|r||	d<   n|r||	d<   |t           k    r|r||	d<   t          d	          D ]}
	 |                     d
||	t                     d{V c S # t          $ r]}t          |          t          fddD                       r |
dk     r!t          j        d|
dz   z             d{V  n Y d}~d}~ww xY wdS )z"Upload media and return file_info.r  
/v2/users/z/files/v2/groups/)r  r  r   r  r     POSTrc  Nc              3      K   | ]}|v V  	d S r  rX   )r  kwerr_msgs     r&   r  z*QQAdapter._upload_media.<locals>.<genexpr>  s;         g     r'   )400401Invalidr   Timeoutr  g      ?r   )	rD   ranger  r3   ro   r    r  r   r   )r$   r  r  r  r   r  r  r  r  r  attemptr   r  s               @r&   _upload_mediazQQAdapter._upload_media  s      e## +****0y000 	 #( 
  
  	*DKK 	* )D''I' )D Qxx 	 	G!..D$0C /              
 
 
c((    "Q      Q;;!-w{(;<<<<<<<<<< =<<<<
	 	s   #A44
C>ACCg      .@g      ?c                x  K   t                               d| j        | j                   d}|| j        k     r^t	          j        | j                   d{V  || j        z  }| j        r#t                               d| j        |           dS || j        k     ^t                               d| j        | j                   dS )a  Wait for the WebSocket listener to reconnect.

        The listener loop (_listen_loop) auto-reconnects on disconnect, but
        there is a race window where send() is called right after a disconnect
        and before the reconnect completes.  This method polls is_connected
        for up to _RECONNECT_WAIT_SECONDS.

        Returns True if reconnected, False if still disconnected.
        u=   [%s] Not connected — waiting for reconnection (up to %.0fs)r   Nz[%s] Reconnected after %.1fsTz$[%s] Still not connected after %.0fsF)	r   r   rh   _RECONNECT_WAIT_SECONDSr   r   _RECONNECT_POLL_INTERVALis_connectedr   )r$   waiteds     r&   _wait_for_reconnectionz QQAdapter._wait_for_reconnection  s       	SM4#?	A 	A 	At333- =>>>>>>>>>d33F  :DM6RRRt t333 	=t}dNjkkkur'   rW  reply_tometadatar   c                  K   ~| j         s,|                                  d{V st          ddd          S |r|                                st          d          S |                     |          }|                     || j                  }t          dd          }|D ],}|                     |||           d{V }|j        s|c S d}-|S )	zSend a text or markdown message to a QQ user or group.

        Applies format_message(), splits long messages via truncate_message(),
        and retries transient failures with exponential backoff.
        NFNot connectedTsuccessr   r   )r  z	No chunksr  r   )	r  r  r   r   format_messagetruncate_messager:   _send_chunkr  )	r$   rW  r]  r  r  	formattedchunkslast_resultchunks	            r&   sendzQQAdapter.send  s        	X4466666666 X!%RVWWWW 	,gmmoo 	,d++++''00	&&y$2IJJ kBBB 	 	E $ 0 0% J JJJJJJJK& #""""HHr'   c           	       
K   d}|                      |          }t          d          D ]1}	 |dk    r|                     |||           d{V c S |dk    r|                     |||           d{V c S |dk    r|                     |||           d{V c S t          dd|           c S # t          $ r}|}t          |                                          
t          
fd	d
D                       rY d}~ n\|dk     rHdd|z  z  }t                              d| j        |dz   ||           t          j        |           d{V  Y d}~+d}~ww xY w|rt          |          ndt                              d| j                   t          fddD                        }	t          d|	          S )z5Send a single chunk with retry + exponential backoff.Nr  r  r  r  FzUnknown chat type for r  c              3      K   | ]}|v V  	d S r  rX   )r  kerrs     r&   r  z(QQAdapter._send_chunk.<locals>.<genexpr>#	  s;         S     r'   )invalid	forbidden	not foundzbad requestr  g      ?z$[%s] send retry %d/3 after %.1fs: %sr   zUnknown errorz[%s] Send failed: %sc              3  D   K   | ]}|                                 v V  d S r  )r   )r  r  	error_msgs     r&   r  z(QQAdapter._send_chunk.<locals>.<genexpr>6	  sC       
 
'(A"""
 
 
 
 
 
r'   )r  r  r  r  )_guess_chat_typer  _send_c2c_text_send_group_text_send_guild_textr   r   r    r   r  r   r   rh   r   r   r   )r$   rW  r]  r  last_excr  r  r   r  r   r  r  s             @@r&   r  zQQAdapter._send_chunk		  s      )-))'22	Qxx 	/ 	/G/%%!%!4!4Wgx!P!PPPPPPPPPP'))!%!6!6w!R!RRRRRRRRRR'))!%!6!6w!R!RRRRRRRRRR% %-Og-O-O       / / /#hhnn&&    !U      EEEEEQ;;1<0ENN>!   "-.........'/* &.BCMMM?	+T]IFFF 
 
 
 
,Q
 
 
 
 
 
	 %yINNNNs0   "B4"B49"B4B44
E>>EAEEopenidkeyboardOptional[InlineKeyboard]c                  K   |                      |p|           |                     ||          }|r||d<   ||                                |d<   |                     dd| d|           d{V }t	          |                    dt          j                    j        dd                             }t          d	||
          S )zzSend text to a C2C user via REST API.

        :param keyboard: Optional inline keyboard attached to the message.
        rI  Nr  r  r  	/messagesr[     Tr  rU  raw_response
rR  _build_text_bodyto_dictr  r    r   rM  rN  rO  r   )r$   r  r]  r  r  r  r   rI  s           r&   r  zQQAdapter._send_c2c_text;	  s       	8-v...$$Wh77 	&%DN'//11D&&v/MF/M/M/MtTTTTTTTTTXXdDJLL$4SbS$9::;;$6MMMMr'   r  c                  K   |                      |p|           |                     ||          }|r||d<   ||                                |d<   |                     dd| d|           d{V }t	          |                    dt          j                    j        dd                             }t          d	||
          S )zwSend text to a group via REST API.

        :param keyboard: Optional inline keyboard attached to the message.
        rI  Nr  r  r  r  r[  r  Tr  r  )r$   r  r]  r  r  r  r   rI  s           r&   r  zQQAdapter._send_group_textQ	  s       	83|444$$Wh77 	&%DN'//11D&&9,9994
 
 
 
 
 
 
 
 TXXdDJLL$4SbS$9::;;$6MMMMr'   r  c                  K   d|d| j                  i}|r||d<   |                     dd| d|           d{V }t          |                    dt	          j                    j        dd                             }t          d	||
          S )z*Send text to a guild channel via REST API.r]  NrI  r  z
/channels/r  r[  r  Tr  )r:   r  r    r   rM  rN  rO  r   )r$   r  r]  r  r  r   rI  s          r&   r  zQQAdapter._send_guild_texti	  s       !*73LT5L3L+MN 	&%DN&&v/QJ/Q/Q/QSWXXXXXXXXTXXdDJLL$4SbS$9::;;$6MMMMr'   rL   c                J  K   | j         s,|                                  d{V st          ddd          S |                     |          }|                     |          }|d| j                 }	 |dk    r|                     ||||           d{V S |dk    r|                     ||||           d{V S t          dd	|d          S # t          $ rI}t          
                    d
| j        |           t          dt          |                    cY d}~S d}~ww xY w)u  Send a single text message with an inline keyboard attached.

        Unlike :meth:`send`, this does NOT split long content into chunks —
        a keyboard message has exactly one interactive surface, and splitting
        would orphan the buttons from the first chunk. Callers should keep
        approval/update-prompt bodies short.

        Guild (channel) chats don't support inline keyboards; returns a
        non-retryable failure for those.
        NFr  Tr  r  )r  r  z-Inline keyboards not supported for chat_type z"[%s] send_with_keyboard failed: %sr  )r  r  r   r  r  r:   r  r  r   r   r   rh   r    )	r$   rW  r]  r  r  r  r  	truncatedr   s	            r&   send_with_keyboardzQQAdapter.send_with_keyboardy	  s     "   	4466666666 !!D    ))'22	''00	7 778		=E!!!00Y8 1          G##!22Y8 3          % % %       	= 	= 	=LL4dmS   e3s88<<<<<<<<<		=s*   0$C $C :C 
D">DD"D"reqrJ   c                   K   ddl m} |                     | ||          t          |j                  |           d{V S )u  Send a 3-button approval request (``allow-once / allow-always / deny``).

        The rendered text comes from :func:`build_approval_text`; callers can
        override by passing a custom :class:`ApprovalRequest`.

        Users click the button → ``INTERACTION_CREATE`` fires → the adapter's
        registered :meth:`set_interaction_callback` handler decodes
        ``button_data`` via :func:`parse_approval_button_data`.
        r   )build_approval_textr  N)!gateway.platforms.qqbot.keyboardsr  r  rN   r  )r$   rW  r  r  r  s        r&   send_approval_requestzQQAdapter.send_approval_request	  sw       	JIIIII,,$$#CO44	 - 
 
 
 
 
 
 
 
 	
r'   dangerous commandcommandr  descriptionc                   K   ~| j                             |          }t          |d||| j                  }|                     |||           d{V S )u|  Send a button-based exec-approval prompt for a dangerous command.

        Called by ``gateway/run.py``'s ``_approval_notify_sync`` when the
        agent is blocked waiting for approval. Button clicks resolve via
        :func:`tools.approval.resolve_gateway_approval` — dispatched by the
        adapter's interaction callback (:meth:`_default_interaction_dispatch`).
        zExecute this command?)r  titler  command_previewtimeout_secr  N)r   r   rJ   _APPROVAL_TIMEOUT_SECONDSr  )r$   rW  r  r  r  r  rI  r  s           r&   send_exec_approvalzQQAdapter.send_exec_approval	  s       
 "&&w//#*##6
 
 
 //S6 0 
 
 
 
 
 
 
 
 	
r'   i,  promptdefaultc                   K   ~~|rd| dnd}d| | }| j                             |          }|                     ||t                      |           d{V S )a  Send a Yes/No update-confirmation prompt with inline buttons.

        Matches the cross-adapter contract used by
        ``gateway/run.py``'s ``hermes update --gateway`` watcher. Button
        clicks surface as ``INTERACTION_CREATE`` with
        ``button_data = 'update_prompt:y'`` or ``'update_prompt:n'``;
        the adapter's interaction callback writes the answer to
        ``~/.hermes/.update_response`` so the detached update process
        can read it.
        z (default: r   r   u!   ⚕ **Update Needs Your Input**

r  N)r   r   r  rO   )	r$   rW  r  r  r  r  default_hintr]  rI  s	            r&   send_update_promptzQQAdapter.send_update_prompt	  s      $ 3:B/W////NNNN"&&w//,,(**	 - 
 
 
 
 
 
 
 
 	
r'   c                    |                      |pd          }| j        rd|d| j                 it          |d}n|d| j                 t          |d}|r| j        sd|i|d<   |S )z2Build the message body for C2C/group text sending.r  r]  N)markdownmsg_typemsg_seq)r]  r  r  rU  message_reference)rR  r   r:   r>   r=   )r$   r]  r  r  r  s        r&   r  zQQAdapter._build_text_body
  s     $$X%:;;! 	&0I$2I0I(JK-"$ $DD ##<T%<#<=)" D  	E) E-98,D()r'   	image_urlcaptionc                ,  K   ~|                      ||t          d||           d{V }|j        s|                     |          s|S t                              d| j        |j                   |r| d| n|}|                     |||           d{V S )z-Send an image natively via QQ Bot API upload.r  Nz0[%s] Image send failed, falling back to text: %sr  )rW  r]  r  )	_send_mediarA   r  _is_urlr   r   rh   r   r  )r$   rW  r  r  r  r  r>  fallbacks           r&   
send_imagezQQAdapter.send_image)
  s       ''Y 0'7H
 
 
 
 
 
 
 
 > 	i!8!8 	M 	>ML	
 	
 	

 18Fg,,,,,YYYw8YTTTTTTTTTr'   
image_pathc                R   K   ~|                      ||t          d||           d{V S )z!Send a local image file natively.r  N)r  rA   )r$   rW  r  r  r  kwargss         r&   send_image_filezQQAdapter.send_image_fileC
  O       %%Z!17GX
 
 
 
 
 
 
 
 	
r'   
audio_pathc                R   K   ~|                      ||t          d||           d{V S )zSend a voice message natively.r  N)r  rC   )r$   rW  r  r  r  r  s         r&   
send_voicezQQAdapter.send_voiceQ
  r  r'   
video_pathc                R   K   ~|                      ||t          d||           d{V S )zSend a video natively.r  N)r  rB   )r$   rW  r
  r  r  r  s         r&   
send_videozQQAdapter.send_video_
  r  r'   	file_pathc           	     V   K   ~|                      ||t          d|||           d{V S )zSend a file/document natively.r  )r  N)r  rD   )r$   rW  r  r  r  r  r  s          r&   send_documentzQQAdapter.send_documentm
  s[       %% & 
 
 
 
 
 
 
 
 	
r'   media_sourcekindc                R  K   | j         s,|                                  d{V st          ddd          S |                     |          }|dk    rt          dd          S 	 |                     |          rY|p't          t          |          j                  j        pd	}	| 	                    ||||d|t          k    r|	nd
           d{V }
n"|                     |||||           d{V \  }	}
|
                    d          p*|
                    di           pi                     d          }|st          dd|
           S |                     |          }t          d|i|d}|r|d| j                 |d<   |r||d<   |                     d|dk    rd| dnd| d|           d{V }t          dt#          |                    dt%          j                    j        dd                             |          S # t*          $ rY}t,                              d| j        |j        |j                   t          dd|j        d|j         dd          cY d}~S d}~wt6          $ rf}t,                              d| j        |j        |j        |j                   t          d|j        d|j         d|j         dd          cY d}~S d}~wt:          $ rI}t,                              d | j        |           t          dt#          |                    cY d}~S d}~ww xY w)!u  Upload media and send as a native message.

        Upload strategy:

        - **HTTP(S) URLs** → single ``POST /v2/{users|groups}/{id}/files``
          with ``url=...``. The QQ platform fetches the URL directly; fastest
          path when the source is already hosted.
        - **Local files** → three-step chunked upload (prepare / PUT parts /
          complete). Handles files up to the platform's ~100 MB per-file
          limit without the ~10 MB inline-base64 cap of the old adapter.
        NFr  Tr  r  z,Guild media send not supported via this pathr  media)r   r  r  	file_infor   zUpload returned no file_info: )r  r  r  r]  rI  r  r  r  r  r  r[  r  r  z,[%s] Daily upload limit exceeded for %s (%s)z#QQ daily upload limit exceeded for z (z). Retry tomorrow.z/[%s] File too large: %s (%s, platform limit %s)z() exceeds the QQ per-file upload limit (z).z[%s] Media send failed: %s)r  r  r   r  r  r   r   r  r   r  rD   _upload_local_filer   rR  r?   r:   r  r    rM  rN  rO  rH   r   r   rh   r  file_size_humanrI   limit_humanr   r   )r$   rW  r  r  r  r  r  r  r  resolved_nameuploadr  r  r  	send_datar   s                   r&   r  zQQAdapter._send_media
  s     *   	X4466666666 X!%RVWWWW))'22	D   
[	=||L))   H\22788= 
  $11$!&/8O/K/KmmQU  2           /3.E.E / / ) ) ) ) ) )%v 

;// 

62&&,"c+   !!C6CC    ((11G*%y1"$ $D
  E")*CD,C*C"DY *!)X"// !E)) 433339w999       I y}}T4:<<3CCRC3HIIJJ&   
 - 	 	 	 NN>s}c.A   @#- @ @+@ @ @           ' 	 	 	NNAs}c.A3?   } E E#*= E E14E E E            	= 	= 	=LL5t}cJJJe3s88<<<<<<<<<	=sL   #C%H 	B:H 
L&AI"L&"L&/AK
L&L&>L!L&!L&r  Tuple[str, Dict[str, Any]]c                  K   | j         st          d          t          |                                          }|                                s(t          j                    |z                                  }|                                r|                                sL|	                    d          st          |          dk     rt          d|          t          d|           |p|j        }t          | j        | j         j        | j                  }|                    ||t'          |          ||           d{V }	||	fS )	aN  Chunked-upload a local file and return ``(resolved_name, complete_response)``.

        The returned ``complete_response`` contains the ``file_info`` token
        that goes into the subsequent RichMedia message body.

        :raises UploadDailyLimitExceededError: On biz_code 40093002.
        :raises UploadFileTooLargeError: When the file exceeds the platform limit.
        :raises FileNotFoundError: If the path does not exist.
        :raises ValueError: If the path looks like a placeholder (``<path>``).
        :raises RuntimeError: If the HTTP client is not initialized.
        ro  <r  1Invalid media source (looks like a placeholder): Media file not found: )api_requesthttp_putlog_tag)r  r  r  r  r  N)r   ro   r   
expanduseris_absolutecwdresolver'  is_filer  r  r  ro  r   rG   r  rv  rh   r  r    )
r$   r  rW  r  r  r  
local_pathr  uploadercompletes
             r&   r  zQQAdapter._upload_local_file   s     &   	QOPPP,''2244
%%'' 	=(**z1::<<J  "" 	K**<*<*>*> 	K&&s++ s</@/@1/D/D XXX   $$IZ$I$IJJJ!4Z_")&*M
 
 

 "*oo# ) 
 
 
 
 
 
 
 
 h&&r'   rV  Tuple[str, str, str]c                  K   t          |                                          }|st          d          t          |          }|j        dv r>t          j        |          d         pd}|pt          |j                  j	        pd}|||fS t          |          
                                }|                                s(t          j                    |z                                  }|                                r|                                sL|                    d          st#          |          dk     rt          d|          t%          d	|           |                                }|p|j	        }t          j        t          |                    d         pd}t)          j        |                              d
          }|||fS )zSLoad media from URL or local path. Returns (base64_or_url, content_type, filename).zMedia source is requiredhttphttpsr   zapplication/octet-streamr  r  r  r  r  ascii)r    r   r  r   schemer  
guess_typer   r  r   r#  r$  r%  r&  r'  r'  r  r  ro  r  base64	b64encoderm  )	r$   rV  r  parsedr  r  r(  rE  b64s	            r&   _load_mediazQQAdapter._load_media0  s      V""$$ 	97888&!!=---$/77:X>XL%Jfk):):)?J7M<66 &\\,,..
%%'' 	=(**z1::<<J  "" 	K**<*<*>*> 	K   %% Vq RRR   $$IZ$I$IJJJ##%%!4Z_$S__55a8V<V 	 s##**733L-//r'   c                *  K   | j         sdS |                     |          }|dk    rdS | j                            |          }|sdS t	          j                    }| j                            |d          }||z
  | j        k     rdS 	 |                     |          }t          |d| j	        d|d}| 
                    dd| d	|           d{V  || j        |<   dS # t          $ r,}	t                              d
| j        |	           Y d}	~	dS d}	~	ww xY w)u  Send an input notify to a C2C user (only supported for C2C).

        Debounced to one request per ~50s (the API sets a 60s indicator).
        The QQ API requires the originating message ID — retrieved from
        ``_last_msg_id`` which is populated by ``_on_message``.
        Nr  r   r   )
input_typeinput_second)r  rI  input_notifyr  r  r  r  z[%s] send_typing failed: %s)r  r  r   r   r   r   _TYPING_DEBOUNCE_SECONDSrR  r@   _TYPING_INPUT_SECONDSr  r   r   r  rh   )
r$   rW  r  r  rI  now	last_sentr  r  r   s
             r&   send_typingzQQAdapter.send_typingZ  sv        	F))'22	F"&&w// 	F ikk(,,Wc::	?T:::F	L((11G1 "#$($>! ! # D ##F,K,K,K,KTRRRRRRRRR,/D ))) 	L 	L 	LLL6sKKKKKKKKK	Ls   AC 
D&!DDc                2    | j         r|S t          |          S )zFormat message for QQ.

        When markdown_support is enabled, content is sent as-is (QQ renders it).
        When disabled, strip markdown via shared helper (same as BlueBubbles/SMS).
        )r   r   )r$   r]  s     r&   r  zQQAdapter.format_message  s"     ! 	Ng&&&r'   c                F   K   |                      |          }||dv rdnddS )z/Return chat info based on chat type heuristics.)r  r  r  r  )r   r
  )r  )r$   rW  r  s      r&   get_chat_infozQQAdapter.get_chat_info  s=      ))'22	(,>>>GGD
 
 	
r'   c                H    t          t          |                     j        dv S )Nr-  )r   r    r1  )rV  s    r&   r  zQQAdapter._is_url  s    F$$+/@@@r'   c                2    || j         v r| j         |         S dS )zDDetermine chat type from stored inbound metadata, fallback to 'c2c'.r  )r   )r$   rW  s     r&   r  zQQAdapter._guess_chat_type  s#    d)))&w//ur'   c                `    ddl }|                    dd|                                           }|S )z9Strip the @bot mention prefix from group message content.r   Nz^@\S+\s*r   )resubr   )r]  rG  strippeds      r&   r  zQQAdapter._strip_at_mention  s/     				66+r7==??;;r'   r  c                l    | j         dk    rdS | j         dk    r|                     | j        |          S dS NdisabledF	allowlistT)r   _entry_matchesr   )r$   r  s     r&   r  zQQAdapter._is_dm_allowed  s?    ?j((5?k))&&t'7AAAtr'   group_idc                l    | j         dk    rdS | j         dk    r|                     | j        |          S dS rK  )r   rN  r   )r$   rO  r  s      r&   r  zQQAdapter._is_group_allowed  sA    ++5,,&&t'=xHHHtr'   entriesr[   targetc                    t          |                                                                          }| D ]D}t          |                                                                          }|dk    s||k    r dS EdS )N*TF)r    r   r   )rQ  rR  normalized_targetentry
normalizeds        r&   rN  zQQAdapter._entry_matches  s    KK--//5577 	 	EU))++1133JS  J2C$C$Ctt %Dur'   r   c                j   |st          j        t          j                  S 	 t          j        |          S # t
          t          f$ r Y nw xY w	 t          j        t          |          dz  t          j                  S # t
          t          f$ r Y nw xY wt          j        t          j                  S )zParse QQ API timestamp (ISO 8601 string or integer ms).

        The QQ API changed from integer milliseconds to ISO 8601 strings.
        This handles both formats gracefully.
        )tzi  )	r   r>  r   utcfromisoformatr  r  fromtimestampr   )r$   rE  s     r&   r  zQQAdapter._parse_qq_timestamp  s      	1<8<0000	)#...I& 	 	 	D		)#c((T/hlKKKKI& 	 	 	D	|x|,,,,s!   7 A
A/A? ?BBc                    t          j                     }t          | j                  t          k    r4|t          z
  fd| j                                        D             | _        || j        v rdS || j        |<   dS )Nc                (    i | ]\  }}|k    ||S rX   rX   )r  keytscutoffs      r&   
<dictcomp>z+QQAdapter._is_duplicate.<locals>.<dictcomp>  s+     # # ##Cb6kkRkkkr'   TF)r   r  r   r<   r;   items)r$   rI  r>  ra  s      @r&   r_  zQQAdapter._is_duplicate  s    ikkt"##n44//F# # # #'+':'@'@'B'B# # #D T(((4&)F#ur'   )rS   r    )r!   r    rS   ri   )rs   r   rS   rT   )rS   ri   )r   r    rS   ri   )r   r   rS   rT   )r  r.  rS   ri   )r  r   rS   ri   )rE  r   rS   rF  )rI  r    rS   r   )rS  r   rS   ri   )rY  r    r  r   rS   ri   )rd  re  rS   ri   )r   )rm  r    r   r   rS   ri   )rS  rM   rS   ri   r(   )r  r    r  r    rS   ri   )r  r.  rI  r    r]  r    r^  r.  r\  r    rS   ri   )r  r.  rS   r.  )rx  r    r  r    rS   r    )r  r  r  r  )r  r   rS   r.  )r   r    r  r    rS   r  )r  r    r  r    rS   rT   )rS   r  )r   r    r  r    r  r    r  r  r  r  rS   r  )r1  r3  r  r    rS   r  )r   r3  rS   r    )r   r3  rS   rT   )r=  r    r2  r    rS   r  )r1  r3  r2  r    rS   r  )rS   rq  )r2  r    rS   r  )r1  r3  r  r    rS   r  )
r  r    r  r    r  rF  r   r  rS   r.  )NNFN)r  r    r  r    r  r   r   r  r  r  r  rT   r  r  rS   r.  )NN)
rW  r    r]  r    r  r  r  rF  rS   r   r  )rW  r    r]  r    r  r  rS   r   )
r  r    r]  r    r  r  r  r  rS   r   )
r  r    r]  r    r  r  r  r  rS   r   )r  r    r]  r    r  r  rS   r   )
rW  r    r]  r    r  rL   r  r  rS   r   )rW  r    r  rJ   r  r  rS   r   )r  N)rW  r    r  r    r  r    r  r    r  rF  rS   r   )r   r   N)rW  r    r  r    r  r    r  r    r  rF  rS   r   )r]  r    r  r  rS   r.  )NNN)rW  r    r  r    r  r  r  r  r  rF  rS   r   )
rW  r    r  r    r  r  r  r  rS   r   )
rW  r    r  r    r  r  r  r  rS   r   )
rW  r    r
  r    r  r  r  r  rS   r   )rW  r    r  r    r  r  r  r  r  r  rS   r   )rW  r    r  r    r  r   r  r    r  r  r  r  r  r  rS   r   )r  r    rW  r    r  r    r  r   r  r  rS   r  )rV  r    r  r  rS   r+  )rW  r    rS   ri   )r]  r    rS   r    )rW  r    rS   r.  )rV  r    rS   rT   )rW  r    rS   r    )r  r    rS   rT   )rO  r    r  r    rS   rT   )rQ  r[   rR  r    rS   rT   )rE  r    rS   r   )rI  r    rS   rT   )ar)   r*   r+   r,   SUPPORTS_MESSAGE_EDITINGr:   r=  r<  propertyrh   rr   r#   r   r   r   r   r   r   r   r   r   r   r   r"  r(  staticmethodr-  r  r?  r  rR  rX  r@  rg  rA  ri  r~  r   r  r`  ra  rb  rc  r  r  r  r  r  r  r  r  r&  r7  rG  r9  r;  r:  r  r(  r  r2   r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r	  r  r  r  r  r7  r@  r  rC  r  r  r  r  r  rN  r  r_  r-   r.   s   @r&   r`   r`      s"	       TT  %+!   X( ( ( (>H >H >H >H >H >HH    X7 7 7 7r8 8 8 82( ( ( (0!& !& !& !&F   4R R R R>W% W% W% W%r   .7 7 7 7&   &$Q $Q $Q $QL" " " "B 
 
 \
5? 5? 5? 5?nV V V V > > > \> * * * \*, , , , , ,Q Q Q Q>. . . .7 7 7 7x     J  " ";
 ;
 ;
 ;
z E E E E \E,]) ]) ]) ])~?) ?) ?) ?)BI) I) I) I)VD) D) D) D)T\
 \
 \
 \
|    \       \ &k
 k
 k
 k
Z!= != != !=F    \*	 	 	 	" -1+/z z z z z zx0 0 0 0d    \& Y Y Y \Y4 4 4 4l   &+ + + +Z6 6 6 6p1 1 1 1f; ; ; ;J .20"O "O "O "O "OR "&'+!&'+, , , , ,^ #"   4 '+15    J '+	0O 0O 0O 0O 0Ol '+15N N N N N4 '+15N N N N N2 LP
N 
N 
N 
N 
N* '+/= /= /= /= /=j '+	
 
 
 
 
J  315
 
 
 
 
B !$ !15
 
 
 
 
> ;?    D &*&*15U U U U U< &*&*
 
 
 
 
$ &*&*
 
 
 
 
$ &*&*
 
 
 
 
$ &*'+&*
 
 
 
 
6 &*&*'+|= |= |= |= |=|.' .' .' .'b ;?$0 $0 $0 $0 $0T&L &L &L &L &LX' ' ' '
 
 
 
 A A A \A       \          \- - - -$
 
 
 
 
 
 
 
r'   r`   rd  )rZ   r   rS   r[   )\r,   
__future__r   r   r3  r   loggingr  r   r   rM  r   r   pathlibr   typingr   r   r	   r
   r   r   r   urllib.parser   r   rV   rM  r   rW   gateway.configr   r   gateway.platforms.baser   r   r   r   r   r   r   gateway.platforms.helpersr   	getLoggerr)   r   r   r   !gateway.platforms.qqbot.constantsr/   r0   r1   r2   r3   r4   r5   r6   r7   r8   r9   r:   r;   r<   r=   r>   r?   r@   rA   rB   rC   rD   gateway.platforms.qqbot.utilsrE   r]   rF   &gateway.platforms.qqbot.chunked_uploadrG   rH   rI   r  rJ   rK   rL   rM   rN   rO   rP   rQ   rR   rY   r^   r`   rX   r'   r&   <module>rt     sJ   > # " " " " "         				   ' ' ' ' ' ' ' '       H H H H H H H H H H H H H H H H H H ! ! ! ! ! !NNN   GGGLLLOO   OEEE 4 3 3 3 3 3 3 3                  5 4 4 4 4 4		8	$	$	W 	W 	W 	W 	W9 	W 	W 	W                                                0                

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1 1 1 1
$ $ $ $K- K- K- K- K-# K- K- K- K- K-s$   A 	A#"A#'A. .	A:9A: