§
    ‰eîií] ã                  ó  — 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 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l.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZD ddlEmFZGmHZH dd„ZIdd„ZJ G d„ de ¦  «        ZKd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Ú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 )ÚQQCloseErrorzŒRaised 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      €úA/home/div/.hermes/hermes-agent/gateway/platforms/qqbot/adapter.pyr!   zQQCloseError.__init__T   se   ø€ Ø!%Ð/•C˜‘I”I�I¨4ˆŒ	Ø%+Ð3•c˜&‘k”k�k°ˆŒÝ‰Œ×ÒÐU°4´9ÐUÐUÀtÄ{Ð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ÚreturnÚboolc                 ó   — t           ot          S )z/Check if QQ runtime dependencies are available.)ÚAIOHTTP_AVAILABLEÚHTTPX_AVAILABLE© r%   r$   Úcheck_qq_requirementsrJ   |   s   € åÐ0¥Ð0r%   Úvaluer   ú	List[str]c                ó    — t          | ¦  «        S )z0Coerce config values into a trimmed string list.)Ú_coerce_list_impl)rK   s    r$   Ú_coerce_listrO   �   s   € å˜UÑ#Ô#Ð#r%   c                  ó€  ‡ — e Zd ZdZdZeZdZdZed™d„¦   «         Z	dšd
„Z
d›ˆ fd„Zed™d„¦   «         Zdœd„Zd�d„Zd�d„Zd™d„Zd™d„Zdžd„Zd�d„ZdŸd„Zd�d„Zd�d„Zd�d„Zd�d„Zed„ ¦   «         Zd d"„Zd¡d%„Zed¢d(„¦   «         Zed£d*„¦   «         Zd¤ˆ fd-„Zd¥d/„Z d¦d3„Z!d¦d4„Z"d¦d5„Z#d¦d6„Z$ed§d:„¦   «         Z%d¨d<„Z&d©d@„Z'edªdB„¦   «         Z(d«dD„Z)dEdEdFœd¬dI„Z*d­dL„Z+ed®dN„¦   «         Z,ed¯dO„¦   «         Z-d°dR„Z.d±dS„Z/d°dT„Z0d²dV„Z1d³dW„Z2d´dY„Z3dEe4fdµd_„Z5	 	 	 	 d¶d·df„Z6dgZ7dhZ8dœdi„Z9	 	 d¸d¹dn„Z:	 dºd»do„Z;	 dºd¼dq„Z<	 dºd½ds„Z=	 dºd¾du„Z>	 dºd¿dv„Z?	 	 	 dÀdÁdy„Z@	 	 d¸dÂd{„ZA	 	 d¸dÃd}„ZB	 	 d¸dÄd„ZC	 	 	 dÀdÅd�„ZD	 	 	 dÀdÆd„„ZE	 dºdÇd‡„ZFdºdÈdˆ„ZGdÉd‰„ZHdÊdŠ„ZIedËd‹„¦   «         ZJdÌdŒ„ZKedÉd�„¦   «         ZLdÍd�„ZMdÎd‘„ZNedÏd•„¦   «         ZOdÐd—„ZPdÑd˜„ZQˆ xZRS )ÒÚ	QQAdapterzJQQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API.Fé<   é2   rD   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ð 	8ˆCØ—8’8‘:”:ð 8Ø×!Ò!¥,¨vÑ"6Ô"6Ñ7Ô7Ð7øØÔ×%Ò%Ñ'Ô'Ð'Ð'Ð'r%   Úconfigr   c                ó*  •— 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 S )NrX   Ú	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ÚstriprU   Ú_client_secretrE   Ú_markdown_supportÚlowerÚ
_dm_policyrO   Ú_allow_fromÚ_group_policyÚ_group_allow_fromÚ_sessionÚ_wsÚ_http_clientÚ_listen_taskÚ_heartbeat_taskÚ_heartbeat_intervalÚ_session_idÚ	_last_seqÚ_chat_type_mapr\   Ú_seen_messagesÚ_last_msg_idÚ_typing_sent_atÚ_access_tokenÚ_token_expires_atÚasyncioÚLockÚ_token_lockÚ_upload_cache)r"   rd   rt   r#   s      €r$   r!   zQQAdapter.__init__£   s4  ø€ Ý‰Œ×Ò˜¥¤Ñ0Ô0Ð0à”Ð" ˆÝ˜5Ÿ9š9 XÑ.Ô.ÐLµ"´)¸KÈÑ2LÔ2LÑMÔM×SÒSÑUÔUˆŒÝ!Ø�IŠI�oÑ&Ô&ÐK­"¬)Ð4FÈÑ*KÔ*Kñ
ô 
ç
Š%‰'Œ'ð 	Ôõ "& e§i¢iÐ0BÀDÑ&IÔ&IÑ!JÔ!JˆÔõ ˜eŸiši¨°VÑ<Ô<Ñ=Ô=×CÒCÑEÔE×KÒKÑMÔMˆŒÝ'Ø�IŠI�lÑ#Ô#Ð= u§y¢y°Ñ'=Ô'=ñ
ô 
ˆÔõ ! §¢¨>¸6Ñ!BÔ!BÑCÔC×IÒIÑKÔK×QÒQÑSÔSˆÔÝ!-Ø�IŠIÐ(Ñ)Ô)ÐH¨U¯YªYÐ7GÑ-HÔ-Hñ"
ô "
ˆÔð
 :>ˆŒØ>BˆŒØ9=ˆÔØ48ˆÔØ7;ˆÔØ*.ˆÔ Ø*.ˆÔØ(,ˆŒØ.0ˆÔð >@ˆÔØ02ˆÔð -/ˆÔà13ˆÔð -1ˆÔØ(+ˆÔÝ"œ<™>œ>ˆÔð 9;ˆÔÐÐr%   c                ó   — dS )NrV   rI   ©r"   s    r$   ÚnamezQQAdapter.name×   s   € àˆwr%   rE   c              ƒ  ó6  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 	 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 # t0          $ 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 IDrq   Úresponse)ÚtimeoutÚfollow_redirectsÚevent_hooksNz[%s] Gateway URL: %sz[%s] ConnectedzQQ startup failed: Úqq_connect_error)Úexc_info)rG   Ú_set_fatal_errorÚloggerÚwarningrY   rH   rU   ry   Ú_acquire_platform_lockÚ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"   ÚmessageÚgateway_urlÚexcs       r$   ÚconnectzQQAdapter.connectß   sÕ  è è € å ð 	Ø@ˆGØ×!Ò!Ð"9¸7ÈdÐ!ÑSÔSÐSÝ�NŠNÐ>ÀÄÈwÑWÔWÐWØ�5Ýð 	Ø>ˆGØ×!Ò!Ð"9¸7ÈdÐ!ÑSÔSÐSÝ�NŠNÐ<¸d¼mÈWÑUÔUÐUØ�5ØŒ|ð 	 4Ô#6ð 	ØVˆGØ×!Ò!Ð":¸GÈtÐ!ÑTÔTÐTÝ�NŠN˜9 d¤m°WÑ=Ô=Ð=Ø�5ð ×*Ò*¨=¸$¼,ÈÑWÔWð 	Ø�5ð	Ý %Ô 1ØØ!%Ø'Õ*>Ð)?Ð@ð!ñ !ô !ˆDÔð ×$Ò$Ñ&Ô&Ð&Ð&Ð&Ð&Ð&Ð&Ð&ð !%× 5Ò 5Ñ 7Ô 7Ð7Ð7Ð7Ð7Ð7Ð7ˆKÝ�KŠKÐ.°´¸{ÑKÔKÐKð —-’- Ñ,Ô,Ð,Ð,Ð,Ð,Ð,Ð,Ð,õ !(Ô 3°D×4EÒ4EÑ4GÔ4GÑ HÔ HˆDÔÝ#*Ô#6°t×7KÒ7KÑ7MÔ7MÑ#NÔ#NˆDÔ Ø× Ò Ñ"Ô"Ð"Ý�KŠKÐ(¨$¬-Ñ8Ô8Ð8Ø�4øÝð 	ð 	ð 	Ø1¨CÐ1Ð1ˆGØ×!Ò!Ð"4°gÈÐ!ÑNÔNÐNÝ�LŠL˜ D¤M°7ÀTˆLÑJÔJÐJØ—-’-‘/”/Ð!Ð!Ð!Ð!Ð!Ð!Ð!Ø×'Ò'Ñ)Ô)Ð)Ø�5�5�5�5�5øøøøð	øøø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¨   rY   r“   s    r$   Ú
disconnectzQQAdapter.disconnect  sJ  è è € àˆŒØ×ÒÑ!Ô!Ð!àÔð 	%ØÔ×$Ò$Ñ&Ô&Ð&ðØÔ'Ð'Ð'Ð'Ð'Ð'Ð'Ð'Ð'øÝÔ)ð ð ð Ø�ðøøøà $ˆDÔàÔð 	(ØÔ ×'Ò'Ñ)Ô)Ð)ðØÔ*Ð*Ð*Ð*Ð*Ð*Ð*Ð*Ð*øÝÔ)ð ð ð Ø�ðøøøà#'ˆDÔ à�mŠm‰oŒoÐÐÐÐÐÐÐØ×#Ò#Ñ%Ô%Ð%Ý�ŠÐ'¨¬Ñ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‚   Úacloser\   r]   r^   r_   r`   ra   )r"   rb   s     r$   r°   zQQAdapter._cleanup-  s.  è è € àŒ8ð 	#˜DœHœOð 	#Ø”(—.’.Ñ"Ô"Ð"Ð"Ð"Ð"Ð"Ð"Ð"ØˆŒàŒ=ð 	( ¤Ô!5ð 	(Ø”-×%Ò%Ñ'Ô'Ð'Ð'Ð'Ð'Ð'Ð'Ð'ØˆŒàÔð 	%ØÔ#×*Ò*Ñ,Ô,Ð,Ð,Ð,Ð,Ð,Ð,Ð,Ø $ˆDÔð Ô*×1Ò1Ñ3Ô3ð 	@ð 	@ˆCØ—8’8‘:”:ð @Ø×!Ò!¥,¨~Ñ">Ô">Ñ?Ô?Ð?øØÔ×%Ò%Ñ'Ô'Ð'Ð'Ð'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).rR   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‚   Úpostr-   rU   ry   r/   Úraise_for_statusrÄ   r®   r`   ru   r   r¡   r¨   rY   )r"   ÚrespÚdatar´   ÚtokenrÆ   s         r$   r¦   zQQAdapter._ensure_tokenE  s¶  è è € àÔð 	&¥$¤)¡+¤+°Ô0FÈÑ0KÒ"KÐ"KØÔ%Ð%àÔ#ð 	&ð 	&ð 	&ð 	&ð 	&ð 	&ð 	&ð 	&àÔ!ð *¥d¤i¡k¤k°DÔ4JÈRÑ4OÒ&OÐ&OØÔ)ð	&ð 	&ð 	&ð 	&ñ 	&ô 	&ð 	&ð 	&ð 	&ð 	&ð 	&ð 	&ð 	&ð 	&ð
	YØ!Ô.×3Ò3ÝØ#'¤<ÀÔATÐUÐUÝ/ð 4ñ ô ð ð ð ð ð ð �ð
 ×%Ò%Ñ'Ô'Ð'Ø—y’y‘{”{��øÝð Yð Yð YÝ"Ð#NÈÐ#NÐ#NÑOÔOÐUXÐXøøøøðYøøøð —H’H˜^Ñ,Ô,ˆEØð Ý"ØIÀ4ÐIÐIñô ð õ ˜TŸXšX l°DÑ9Ô9Ñ:Ô:ˆJØ!&ˆDÔÝ%)¤Y¡[¤[°:Ñ%=ˆDÔ"Ý�KŠKØ=¸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‚   ru   r,   r.   rC   r/   rÉ   rÄ   r®   r`   )r"   rÌ   rÊ   rË   r´   rÓ   s         r$   r§   zQQAdapter._get_gateway_urlh  s2  è è € à×(Ò(Ñ*Ô*Ð*Ð*Ð*Ð*Ð*Ð*ˆð	TØÔ*×.Ò.ÝÐ/Õ-Ð/Ð/à%5¨eÐ%5Ð%5Ý"2Ñ"4Ô"4ðð õ ,ð /ñ ô ð ð ð ð ð ð ˆDð ×!Ò!Ñ#Ô#Ð#Ø—9’9‘;”;ˆDˆDøÝð 	Tð 	Tð 	TÝÐIÀCÐIÐIÑJÔJÐPSÐSøøøøð	Tøøøð �hŠh�u‰oŒoˆØð 	OÝÐMÀtÐ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        ¦   «         | _        | j                             |dt          ¦   «         it          ¬¦  «        ƒ d{V —†| _         t           
                    d| j        |¦  «         dS )z2Open a WebSocket connection to the QQ Bot gateway.NrÐ   rÑ   z[%s] WebSocket connected to %s)r�   r¾   r¿   r€   ÚaiohttpÚClientSessionÚ
ws_connectrC   r1   r¡   r¨   rY   )r"   r³   s     r$   r©   zQQAdapter._open_ws‚  s  è è € ð Œ8ð 	#˜DœHœOð 	#Ø”(—.’.Ñ"Ô"Ð"Ð"Ð"Ð"Ð"Ð"Ð"ØˆŒØŒ=ð 	( ¤Ô!5ð 	(Ø”-×%Ò%Ñ'Ô'Ð'Ð'Ð'Ð'Ð'Ð'Ð'ØˆŒåÔ-Ñ/Ô/ˆŒØœ×1Ò1ØàÕ.Ñ0Ô0ðõ ,ð 2ñ 
ô 
ð 
ð 
ð 
ð 
ð 
ð 
ˆŒõ 	�ŠÐ4°d´mÀ[Ñ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    r�t                               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}~nÇd}~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   rr   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¢   rY   r   r5   r¨   r6   r¯   r    r¸   rc   r4   r3   Ú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Ø”}ð Ø�F�F�F�F�Fà”x�Ý—’Ø>Ø”MØØ”Jñ	ô ð õ  œ>Ñ+Ô+¨lÑ:�ØÕ8Ò8Ð8¸\ÈAÒ=MÐ=MØ*¨aÑ/Ð*Ý—K’KØBØœØ Ø.ñ	ô ð ð .Õ1KÒKÐKÝŸšðdà œMñô ð ð
 ×-Ò-Ø1ØRØ&*ð .ñ ô ð ð
 ˜˜˜˜˜ð Lð ./Ð*à×'Ò'Ñ)Ô)Ð)Ø×"Ò"Ð#6Ñ7Ô7Ð7ð ˜<Ð'Ð'Ø59¸T²\°\Ð1Ð1Àx�DÝ—L’LØAÀ4Ä=ÐRVñô ð ð ×)Ò)Ø$˜d˜˜Ð&6°Ð&6Ð&6À%ð *ñ ô ð ð �F�F�F�F�Fð ˜4’<�<Ý—K’KØ?ØœÝ(ñô ð ð
 #Õ&<Ò<Ð<Ø˜˜˜˜˜Ý!œ-Õ(8Ñ9Ô9Ð9Ð9Ð9Ð9Ð9Ð9Ð9Ø!Ÿ_š_¨[Ñ9Ô9Ð9Ð9Ð9Ð9Ð9Ð9ð )Ø&'˜Ø12Ð.Ð.à# qÑ(˜Ø�H�H�H‘Hð ˜4’<�<Ý—K’KØOØœñô ð ð *.�DÔ&Ø-0�DÔ*ð ð ð ð õ& —K’KØSØœØñô ð ð
 (,�DÔ$Ø%)�D”NàŸš¨Ñ5Ô5Ð5Ð5Ð5Ð5Ð5Ð5ð Ø"#�KØ-.Ð*Ð*à 1Ñ$�KØ"Õ&<Ò<Ð<ÝŸšÐ%YÐ[_Ô[hÑiÔiÐiØ˜˜˜˜˜øøøøøøøøøåð %ð %ð %Ø”}ð Ø�F�F�F�F�FÝ—’Ð9¸4¼=È#ÑNÔNÐNØ×'Ò'Ñ)Ô)Ð)Ø×"Ò"Ð#;Ñ<Ô<Ð<àÕ"8Ò8Ð8Ý—L’LÐ!FÈÌÑVÔVÐVØ�F�F�F�F�FàŸš¨Ñ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)...Nrq   z[%s] ReconnectedTz[%s] Reconnect failed: %sF)r2   ÚminÚlenr¡   r¨   rY   rŽ   rà   r…   r¦   r§   r©   r­   r®   r¢   )r"   râ   Údelayr³   r´   s        r$   rá   zQQAdapter._reconnect/  s_  è è € å!¥# kµ3Õ7HÑ3IÔ3IÈAÑ3MÑ"NÔ"NÔOˆÝ�ŠØ6ØŒMØØ˜!‰Oñ		
ô 	
ð 	
õ Œm˜EÑ"Ô"Ð"Ð"Ð"Ð"Ð"Ð"Ð"à#'ˆÔ ð		Ø×$Ò$Ñ&Ô&Ð&Ð&Ð&Ð&Ð&Ð&Ð&Ø $× 5Ò 5Ñ 7Ô 7Ð7Ð7Ð7Ð7Ð7Ð7ˆKØ—-’- Ñ,Ô,Ð,Ð,Ð,Ð,Ð,Ð,Ð,Ø× Ò Ñ"Ô"Ð"Ý�KŠKÐ*¨D¬MÑ:Ô:Ð:Ø�4øÝð 	ð 	ð 	Ý�NŠNÐ6¸¼ÀsÑKÔKÐKØ�5�5�5�5�5øøøøð	øøø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�   r`   r·   r¾   ÚreceiveÚtyperÕ   Ú	WSMsgTypeÚTEXTÚ_parse_jsonrË   Ú_dispatch_payloadÚPINGÚCLOSEr   rt   ÚCLOSEDÚERROR)r"   ÚmsgÚpayloads      r$   rß   zQQAdapter._read_eventsF  sp  è è € àŒxð 	:ÝÐ8Ñ9Ô9Ð9àŒmñ 	7 ¤ñ 	7°´´ñ 	7Øœ×(Ò(Ñ*Ô*Ð*Ð*Ð*Ð*Ð*Ð*ˆCØŒx�7Ô,Ô1Ò1Ð1Ø×*Ò*¨3¬8Ñ4Ô4�Øð 4Ø×*Ò*¨7Ñ3Ô3Ð3øØ”�gÔ/Ô4Ð6Ð6Ð6àØ”�WÔ.Ô4Ò4Ð4Ý" 3¤8¨S¬YÑ7Ô7Ð7Ø”�gÔ/Ô6½Ô8IÔ8OÐ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         ržt          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)zñSend 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¡   ÚdebugrY   rº   )r"   r´   s     r$   r¬   zQQAdapter._heartbeat_loopY  s%  è è € ð	Ø”-ð RÝ”m DÔ$<Ñ=Ô=Ð=Ð=Ð=Ð=Ð=Ð=Ð=Ø”xð  4¤8¤?ð ØðRàœ(×,Ò,°A¸D¼NÐ-KÐ-KÑLÔLÐLÐLÐLÐLÐLÐLÐLÐLøÝ ð Rð Rð RÝ—L’LÐ!<¸d¼mÈSÑQÔQÐQÐQÐQÐQÐQÐQøøøøðRøøøð ”-ð Rð Rð Rð Rð Røõ Ô%ð 	ð 	ð 	ØˆDˆDð	øøø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¨   rY   r¢   r®   r¯   )r"   rÌ   Úidentify_payloadr´   s       r$   Ú_send_identifyzQQAdapter._send_identifyl  sI  è è € ð ×(Ò(Ñ*Ô*Ð*Ð*Ð*Ð*Ð*Ð*ˆàà) %Ð)Ð)ðð
 ˜Q˜à"Ø .Ø-ðð ðð ð
ð 
Ðð"		QØŒxð  ¤¤ð Ø”h×(Ò(Ð)9Ñ:Ô:Ð:Ð:Ð:Ð:Ð:Ð:Ð:Ý—’Ð0°$´-Ñ@Ô@Ð@Ð@Ð@å—’ØHÈ$Ì-ñô ð ð ð øõ ð 	Qð 	Qð 	QÝ�LŠLÐ;¸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)	z®Send 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¨   rY   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ÈÌñô ð ð ð øõ ð 	"ð 	"ð 	"Ý�LŠLÐ9¸4¼=È#ÑNÔNÐNà#ˆDÔØ!ˆDŒNˆNˆNˆNˆNˆNˆNøøøøð		"øøøs   ´AB7 Â B7 Â7
C;Ã/C6Ã6C;c                óv   — 	 t          j        ¦   «         }|                     | ¦  «        S # t          $ r Y dS w xY w)zäSchedule 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ª   r`   )ÚcoroÚloops     r$   Ú_create_taskzQQAdapter._create_task³  sK   € ð	ÝÔ+Ñ-Ô-ˆDØ×#Ò# DÑ)Ô)Ð)øÝð 	ð 	ð 	Ø�4�4ð	øøøs   ‚'* ª
8·8r÷   úDict[str, Any]c                ó"  — |                      d¦  «        }|                      d¦  «        }|                      d¦  «        }|                      d¦  «        }t          |t          ¦  «        r| j        �|| j        k    r|| _        |dk    rÂt          |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|                      |¦  «         nu|dk    r!t                               d| j        ¦  «         nN|dv r)t          j        |                      ||¦  «        ¦  «         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_CREATEz[%s] Unhandled dispatch: %sé   z[%s] Unknown op: %s)ru   Ú
isinstancer   r‡   Údictr…   r¡   rý   rY   r†   r  r  r  Ú_handle_readyr¨   rŽ   rª   Ú_on_message)r"   r÷   rú   r  r  rû   Úd_dataÚinterval_mss           r$   rñ   zQQAdapter._dispatch_payloadÀ  s  € à�[Š[˜ÑÔˆØ�KŠK˜ÑÔˆØ�KŠK˜ÑÔˆØ�KŠK˜ÑÔˆÝ�a�ÑÔð 	 4¤>Ð#9¸QÀÄÒ=OÐ=OØˆDŒNð �Š8ˆ8Ý$ Q­Ñ-Ô-Ð5�Q�Q°2ˆFØ Ÿ*š*Ð%9¸5ÑAÔAˆKà'2°VÑ';¸cÑ'AˆDÔ$Ý�LŠLØTØ”ØØÔ(ñ	ô ð ð Ôð 9 D¤NÐ$>Ø×!Ò! $×"3Ò"3Ñ"5Ô"5Ñ6Ô6Ð6Ð6à×!Ò! $×"5Ò"5Ñ"7Ô"7Ñ8Ô8Ð8ØˆFð �Š7ˆ7�qˆ7Ø�GŠ|ˆ|Ø×"Ò" 1Ñ%Ô%Ð%Ð%Ø�i’�Ý—’Ð2°D´MÑBÔBÐBÐBØð ð ð õ Ô# D×$4Ò$4°Q¸Ñ$:Ô$:Ñ;Ô;Ð;Ð;å—’Ð:¸D¼MÈ1ÑMÔMÐMØˆFð �Š8ˆ8ØˆFå�ŠÐ*¨D¬M¸2Ñ>Ô>Ð>Ð>Ð>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   ru   r†   r¡   r¨   rY   )r"   rû   s     r$   r!  zQQAdapter._handle_readyõ  sW   € å�a�ÑÔð 	VØ Ÿušu \Ñ2Ô2ˆDÔÝ�KŠKÐ3°T´]ÀDÔDTÑ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   )r&  r÷   s     r$   rð   zQQAdapter._parse_jsonÿ  sh   € ð	Ý”j ‘o”oˆGˆGøÝð 	ð 	ð 	Ý�NŠNÐ=¸sÑCÔCÐCØ�4�4ð	øøøõ % W­dÑ3Ô3Ð=ˆwˆw¸Ð=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)r*  Ú	time_partÚrands      r$   Ú_next_msg_seqzQQAdapter._next_msg_seq  sM   € õ �œ	™œÑ$Ô$ yÑ0ˆ	Ý•4”:‘<”<Ô# B Q BÔ'¨Ñ,Ô,ˆØ˜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"   r4  r#   s     €r$   r9  zQQAdapter.handle_message  se   øè è € àÔð 	G ¤Ô 4ð 	GØ6;Ô6FˆDÔ˜eœlÔ2Ñ3Ý‰gŒg×$Ò$ 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Úauthorr  )r  )r  r  r  )r  r   r   ru   Ú_is_duplicater¡   rý   rY   rx   Ú_handle_c2c_messageÚ_handle_group_messageÚ_handle_guild_messageÚ_handle_dm_message)r"   r:  rû   r*  r=  r>  r?  s          r$   r"  zQQAdapter._on_message  só  è è € å˜!�TÑ"Ô"ð 	ØˆFõ �Q—U’U˜4 ‘_”_Ñ%Ô%ˆØð 	˜×+Ò+¨FÑ3Ô3ð 	Ý�LŠLØ:¸D¼MÈ6ñô ð ð ˆFå˜Ÿš˜k¨2Ñ.Ô.Ñ/Ô/ˆ	Ý�a—e’e˜I rÑ*Ô*Ñ+Ô+×1Ò1Ñ3Ô3ˆÝ$.¨q¯uªu°X©¬ÅÑ$EÔ$EÐM�—’�x‘”�È2ˆð Ð-Ò-Ð-Ø×*Ò*¨1¨f°g¸vÀyÑ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Ø×)Ò)¨!¨V°W¸fÀiÑPÔPÐPÐPÐPÐPÐPÐPÐPÐPÐPð 3Ð2r%   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	¦  «         |r´t          |t          ¦  «        rŸt          |¦  «        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          |¦  «        ¦  «         |                     ¦   «         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=%srS   r   z itemsrZ   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Úc2cÚdm©r8  Úuser_idÚ	chat_type©r7  ÚtextÚmessage_typeÚraw_messager6  Ú
media_urlsÚmedia_typesr=  )r   ru   Ú_is_dm_allowedr¡   r¨   rY   r  Úlistré   Ú	enumerater   Ú_process_attachmentsÚjoinrx   rˆ   r   Úbuild_sourceÚ_detect_message_typeÚ_parse_qq_timestampr9  )r"   rû   r*  r>  r?  r=  rF  rW  Úattachments_rawÚ_iÚ_attÚ
att_resultrK  rL  rM  rN  Úvoice_blockr4  s                     r$   rA  zQQAdapter._handle_c2c_message4  sR  è è € õ ˜&Ÿ*š* ]°BÑ7Ô7Ñ8Ô8ˆØð 	ØˆFØ×"Ò" ;Ñ/Ô/ð 	ØˆFàˆØŸ%š% Ñ.Ô.ˆÝ�ŠØ?ØŒMØØ#Ð+ˆG�C�R�CŒLˆL¨ð #ð­:°oÅtÑ+LÔ+LÐS•3�Ñ'Ô'Ð'ÐRSÐ[Ð[Ð[Ð[àñ
	
ô 
	
ð 
	
ð ð 
	�z¨/½4Ñ@Ô@ð 
	Ý% oÑ6Ô6ð 	ð 	‘��DÝ˜d¥DÑ)Ô)ð Ý—K’KØQØœØØŸš °Ñ4Ô4Ý˜DŸHšH U¨BÑ/Ô/Ñ0Ô0°°"°Ô5ØŸš ¨RÑ0Ô0ñô ð øð  ×4Ò4°_ÑEÔEÐEÐEÐEÐEÐEÐEˆ
Ø Ô-ˆ
Ø&Ð':Ô;ÐØ&Ð':Ô;ÐØ$Ð%6Ô7ˆð ð 	ØŸ)š)Ð$5Ñ6Ô6ˆKà9=¿º¹¼ÐV�˜‘ Ñ,×3Ò3Ñ5Ô5Ð5È;ð ð ð 	ð —:’:‘<”<ð%�˜‘ Ñ0×7Ò7Ñ9Ô9Ð9à$ð õ 	�ŠØ8ØŒMÝ�
‰OŒOÝÐ!Ñ"Ô"ñ		
ô 	
ð 	
ð �zŠz‰|Œ|ð 	 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|}|                     ¦   «         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_openidrG  rK  rL  rM  rN  rO  rP  ÚgrouprS  rV  )r   ru   Ú_is_group_allowedÚ_strip_at_mentionr_  r`  rx   rˆ   r   ra  rb  rc  r9  )r"   rû   r*  r>  r?  r=  rj  rW  rg  rK  rL  rM  rN  rh  r4  s                  r$   rB  zQQAdapter._handle_group_messageŒ  s@  è è € õ ˜1Ÿ5š5 °Ñ4Ô4Ñ5Ô5ˆØð 	ØˆFØ×%Ò%Ø�c &§*¢*¨_¸bÑ"AÔ"AÑBÔBñ
ô 
ð 	ð ˆFð ×%Ò% gÑ.Ô.ˆØ×4Ò4°Q·U²U¸=Ñ5IÔ5IÑJÔJÐJÐJÐJÐJÐJÐJˆ
Ø Ô-ˆ
Ø&Ð':Ô;ÐØ&Ð':Ô;ÐØ$Ð%6Ô7ˆð ð 	ØŸ)š)Ð$5Ñ6Ô6ˆKà9=¿º¹¼ÐV�˜‘ Ñ,×3Ò3Ñ5Ô5Ð5È;ð ð ð 	ð —:’:‘<”<ð%�˜‘ Ñ0×7Ò7Ñ9Ô9Ð9à$ð ð �zŠz‰|Œ|ð 	 Jð 	ØˆFà,3ˆÔ˜LÑ)ÝØ×$Ò$Ø$Ý˜FŸJšJ ¸Ñ;Ô;Ñ<Ô<Ø!ð %ñ ô ð
 Ø×2Ò2°:Ð?PÑQÔQØØØ!Ø)Ø×.Ò.¨yÑ9Ô9ð
ñ 
ô 
ˆð ×!Ò! %Ñ(Ô(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(r%   c              ƒ  ó^  K  — t          |                     dd¦  «        ¦  «        }|s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|}	|	                     ¦   «         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ÚmemberÚnickÚusernamerG  rK  rL  rM  rN  rO  rP  Úguildr<  rl  )r8  rT  Ú	user_namerU  rV  )r   ru   r  r   r_  r`  rx   rˆ   r   ra  rb  rc  r9  )r"   rû   r*  r>  r?  r=  rp  rq  rr  rW  rg  rK  rL  rM  rN  rh  r4  s                    r$   rC  zQQAdapter._handle_guild_messageÆ  s]  è è € õ ˜Ÿš˜|¨RÑ0Ô0Ñ1Ô1ˆ
Øð 	ØˆFå$.¨q¯uªu°X©¬ÅÑ$EÔ$EÐM�—’�x‘”�È2ˆÝ�6—:’:˜f bÑ)Ô)Ñ*Ô*ÐM­c°&·*²*¸ZÈÑ2LÔ2LÑ.MÔ.MˆàˆØ×4Ò4°Q·U²U¸=Ñ5IÔ5IÑJÔJÐJÐJÐJÐJÐJÐJˆ
Ø Ô-ˆ
Ø&Ð':Ô;ÐØ&Ð':Ô;ÐØ$Ð%6Ô7ˆàð 	ØŸ)š)Ð$5Ñ6Ô6ˆKà9=¿º¹¼ÐV�˜‘ Ñ,×3Ò3Ñ5Ô5Ð5È;ð ð ð 	ð —:’:‘<”<ð%�˜‘ Ñ0×7Ò7Ñ9Ô9Ð9à$ð ð �zŠz‰|Œ|ð 	 Jð 	ØˆFà*1ˆÔ˜JÑ'ÝØ×$Ò$Ø"Ý˜FŸJšJ t¨RÑ0Ô0Ñ1Ô1Ø˜, $Ø!ð	 %ñ ô ð Ø×2Ò2°:Ð?PÑQÔQØØØ!Ø)Ø×.Ò.¨yÑ9Ô9ð
ñ 
ô 
ˆð ×!Ò! %Ñ(Ô(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(r%   c              ƒ  óN  K  — 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|}|                     ¦   «         s|	sdS d| j        |<   t          |                      |t          |                     dd¦  «        ¦  «        d¬¦  «        ||                      |	|
¦  «        |||	|
|  	                    |¦  «        ¬¦  «        }|  
                    |¦  «        ƒ d{V —† dS )z Handle a guild DM message event.Úguild_idr   NrG  rK  rL  rM  rN  rO  rP  rR  r<  rS  rV  )r   ru   r_  r`  rx   rˆ   r   ra  rb  rc  r9  )r"   rû   r*  r>  r?  r=  rw  rW  rg  rK  rL  rM  rN  rh  r4  s                  r$   rD  zQQAdapter._handle_dm_messageþ  sô  è è € õ �q—u’u˜Z¨Ñ,Ô,Ñ-Ô-ˆØð 	ØˆFàˆØ×4Ò4°Q·U²U¸=Ñ5IÔ5IÑJÔJÐJÐJÐJÐJÐJÐJˆ
Ø Ô-ˆ
Ø&Ð':Ô;ÐØ&Ð':Ô;ÐØ$Ð%6Ô7ˆàð 	ØŸ)š)Ð$5Ñ6Ô6ˆKà9=¿º¹¼ÐV�˜‘ Ñ,×3Ò3Ñ5Ô5Ð5È;ð ð ð 	ð —:’:‘<”<ð%�˜‘ Ñ0×7Ò7Ñ9Ô9Ð9à$ð ð �zŠz‰|Œ|ð 	 Jð 	ØˆFà(,ˆÔ˜HÑ%ÝØ×$Ò$Ø Ý˜FŸJšJ t¨RÑ0Ô0Ñ1Ô1Øð %ñ ô ð
 Ø×2Ò2°:Ð?PÑQÔQØØØ!Ø)Ø×.Ò.¨yÑ9Ô9ð
ñ 
ô 
ˆð ×!Ò! %Ñ(Ô(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(Ð(r%   rZ  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ý   )rZ  r[  Ú
first_types      r$   rb  zQQAdapter._detect_message_type6  sÆ   € ð ð 	$ÝÔ#Ð#Øð 	%ÝÔ$Ð$Ø/:ÐB�[ ”^×)Ò)Ñ+Ô+Ð+Àˆ
Ø�jÐ Ð  G¨zÐ$9Ð$9¸VÀzÐ=QÐ=QÝÔ$Ð$Ø�jÐ Ð ÝÔ$Ð$Ø�jÐ Ð  G¨zÐ$9Ð$9ÝÔ$Ð$Ý�ŠØAØñ	
ô 	
ð 	
õ ÔÐr%   rG  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   )rK  rL  rM  rN  rH  rÓ   rJ  ú//úhttps:z@[%s] Processing attachment: content_type=%s, url=%s, filename=%sNrI  Úasr_refer_textÚvoice_wav_url©r†  r‡  z[Voice] z[%s] Voice transcript: %sz[%s] Voice STT failed for %srR   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: %srO  )r  r]  r   r   ru   rx   r{   Ú
startswithr¡   rý   rY   Ú_is_voice_content_typeÚ_stt_voice_attachmentÚappendr¢   Ú_download_and_cacherv   ÚpathÚisfiler®   r`  )r"   rG  rK  rL  rM  Úother_attachmentsÚattÚctÚurl_rawrJ  rÓ   Ú	asr_referr‡  Ú
transcriptÚcached_pathr´   rN  s                    r$   r_  zQQAdapter._process_attachmentsJ  sÏ  è è € õ ˜+¥tÑ,Ô,ð 	à Ø%'Ø%'Ø#%ð	ð ð ð !#ˆ
Ø')ÐØ')ÐØ')Ðàð G	\ñ G	\ˆCÝ˜c¥4Ñ(Ô(ð Øå�S—W’W˜^¨RÑ0Ô0Ñ1Ô1×7Ò7Ñ9Ô9×?Ò?ÑAÔAˆBÝ˜#Ÿ'š' %¨Ñ,Ô,Ñ-Ô-×3Ò3Ñ5Ô5ˆGÝ˜3Ÿ7š7 :¨rÑ2Ô2Ñ3Ô3ˆHØ×!Ò! $Ñ'Ô'ð Ø(˜wÐ(Ð(��Øð Ø��à�Øå�LŠLØRØ”ØØ�C�R�C”Øñô ð ð ×*Ò*¨2¨xÑ8Ô8ñ 0\õ " #§'¢'Ð*:Ñ";Ô";½SÑAÔAð•C˜ŸšÐ 0°"Ñ5Ô5Ñ6Ô6×<Ò<Ñ>Ô>Ð>àð õ " #§'¢'¨/Ñ":Ô":½CÑ@Ô@ð•C˜Ÿš °Ñ4Ô4Ñ5Ô5×;Ò;Ñ=Ô=Ð=àð ð $(×#=Ò#=ØØØØ#,Ð#4°Ø"/Ð"7°4ð $>ñ $ô $ð ð ð ð ð ð �
ð ð MØ%×,Ò,Ð-D¸
Ð-DÐ-DÑEÔEÐEÝ—L’LÐ!<¸d¼mÈZÑXÔXÐXÑXå—N’NÐ#AÀ4Ä=ÐRUÐVYÐWYÐVYÔRZÑ[Ô[Ð[Ø%×,Ò,Ð-KÑLÔLÐLÑLØ—’˜xÑ(Ô(ð \ðWØ(,×(@Ò(@ÀÀbÑ(IÔ(IÐ"IÐ"IÐ"IÐ"IÐ"IÐ"I�KØ"ð ¥r¤w§~¢~°kÑ'BÔ'Bð Ø"×)Ò)¨+Ñ6Ô6Ð6Ø)×0Ò0°Ð1C°|ÑDÔDÐDÐDØ$ð ÝŸšØGØ œMØ'ñô ð ùøõ
 !ð Wð Wð WÝ—L’LÐ!AÀ4Ä=ÐRUÑVÔVÐVÐVÐVÐVÐVÑVøøøøðWøøøð\Ø(,×(@Ò(@ÀÀbÑ(IÔ(IÐ"IÐ"IÐ"IÐ"IÐ"IÐ"I�KØ"ð TØ)×0Ò0Ð1RÀÀÈRÐ1RÐ1RÐ1RÑSÔSÐSùøÝ ð \ð \ð \Ý—L’LÐ!FÈÌÐWZÑ[Ô[Ð[Ð[Ð[Ð[Ð[Ñ[øøøøð\øøøð ;LÐS˜$Ÿ)š)Ð$5Ñ6Ô6Ð6ÐQSˆà$Ø!2Ø!2Ø.ð	
ð 
ð 	
s1   Ê;BM
Í

N Í!M;Í;N Î9N?Î?
O5Ï	!O0Ï0O5rÓ   rH  ú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: NrI  rq   )r›   rÒ   z[%s] Download failed for %s: %sr‰  z.jpgrz  úaudio/Úqq_attachment)Útools.url_safetyrœ  Ú
ValueErrorr‚   ru   Ú_qq_media_headersrÉ   r>  r®   r¡   rý   rY   r‹  Ú	mimetypesÚguess_extensionr   Ú_convert_audio_to_wavr   r   r�  r”   r   )	r"   rÓ   rH  rœ  rÊ   rË   r´   ÚextrJ  s	            r$   r�  zQQAdapter._download_and_cache·  sÖ  è è € à0Ð0Ð0Ð0Ð0Ð0àˆ{˜3ÑÔð 	@ÝÐ>°C¸¸¸´HÐ>Ð>Ñ?Ô?Ð?àÔ ð 	Ø�4ð	ØÔ*×.Ò.ØØØ×.Ò.Ñ0Ô0ð /ñ ô ð ð ð ð ð ð ˆDð
 ×!Ò!Ñ#Ô#Ð#Ø”<ˆDˆDøÝð 	ð 	ð 	Ý�LŠLØ1°4´=À#ÀcÀrÀcÄ(ÈCñô ð ð �4�4�4�4�4øøøøð		øøøð ×"Ò" 8Ñ,Ô,ð 		=ÝÔ+¨LÑ9Ô9ÐC¸VˆCÝ)¨$°Ñ4Ô4Ð4Ø˜WÒ$Ð$¨×(?Ò(?ÀÑ(IÔ(IÐ$ð ×3Ò3°D¸#Ñ>Ô>Ð>Ð>Ð>Ð>Ð>Ð>Ð>å�H S™MœMÔ.Ñ/Ô/Ô4ÐG¸ˆHÝ,¨T°8Ñ<Ô<Ð<s   ¸AB	 Â	
CÂ*CÃCrJ  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.rz  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/   øè è € Ð=Ð= Cˆr�{Š{˜3ÑÔÐ=Ð=Ð=Ð=Ð=Ð=r%   F)rx   r{   r‹  Úany)rH  rJ  r”  Ú_VOICE_EXTENSIONSr³  s       @r$   rŒ  z QQAdapter._is_voice_content_typeÚ  s•   ø€ ð ×ÒÑ!Ô!×'Ò'Ñ)Ô)ˆØ�^Š^ÑÔ×#Ò#Ñ%Ô%ˆØ�Š=ˆ=˜BŸMšM¨(Ñ3Ô3ˆ=Ø�4ð

Ðõ Ð=Ð=Ð=Ð=Ð+<Ð=Ñ=Ô=Ñ=Ô=ð 	Ø�4Øˆur%   úDict[str, str]c                ó,   — | j         rdd| j         › �iS i S )zúReturn 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Ø#Ð%B¨dÔ.@Ð%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          |¦  «        ¦  «         n‚t                                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: %rNéd   Fr„  r…  Tz1[%s] STT: using voice_wav_url (pre-converted WAV)r   r›  z[QQ] STT blocked unsafe URL: %srI  z[%s] STT: no HTTP clientz<[%s] STT: downloading voice from %s (pre_wav=%s, headers=%s)rq   )r›   rÒ   rœ   z.[%s] STT: downloaded %d bytes, content_type=%szcontent-typeÚunknownr  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ý   rY   r‹  rŸ  rœ  r¢   r‚   r¡  rE   ru   rÉ   r>  ré   rÒ   ÚtempfileÚNamedTemporaryFileÚwriter”   Ú_convert_audio_to_wav_filer   ÚexistsÚ	_call_sttrv   ÚunlinkÚOSErrorr¤   ÚHTTPStatusErrorÚTransportErrorÚIOErrorrí   r&   )r"   rÓ   rH  rJ  r†  r‡  Údownload_urlÚ
is_pre_wavrœ  Údownload_headersrÊ   Ú
audio_datar¿  ÚtmpÚwav_pathr—  r´   s                    r$   r�  zQQAdapter._stt_voice_attachmentû  s½  è è € ð& ð 	"Ý�LŠLØ7¸¼ÈÐW[ÐX[ÐW[ÔH\ñô ð ð "Ð!ð ˆØˆ
Øð 	]Ø×'Ò'¨Ñ-Ô-ð 9Ø 8¨Ð 8Ð 8�Ø(ˆLØˆJÝ�LŠLÐLÈdÌmÑ\Ô\Ð\à0Ð0Ð0Ð0Ð0Ð0Øˆ{˜<Ñ(Ô(ð 	Ý�NŠNÐ<¸lÈ3ÈBÈ3Ô>OÑPÔPÐPØ�4ðR	àÔ$ð Ý—’Ð9¸4¼=ÑIÔIÐIØ�tà#×5Ò5Ñ7Ô7ÐÝ�LŠLØNØ”Ø˜S˜b˜SÔ!ØÝÐ%Ñ&Ô&ñô ð ð Ô*×.Ò.ØØØ(Ø!%ð	 /ñ ô ð ð ð ð ð ð ˆDð ×!Ò!Ñ#Ô#Ð#ØœˆJÝ�LŠLØ@Ø”Ý�J‘”Ø”× Ò  °Ñ;Ô;ñ	ô ð õ �:‰Œ Ò#Ð#Ý—’ØNØ”MÝ˜
‘O”Oñô ð ð
 �tð ð  Ø���à×0Ò0¸ÀuÐ0ÑMÔMð (ÐQTØ—I’I˜jÑ)Ô)Ð)Ø"œx�Hð(ð (ð (ñ (ô (ð (ð (ð (ð (ð (ð (øøøð (ð (ð (ð (õ —’ØKØ”MÝ˜
‘O”Oñô ð ð õ —’Ø>ÀÄÈxñô ð ð "&×!@Ò!@ÀÈXÑ!VÔ!VÐVÐVÐVÐVÐVÐV�Øð  ¥t¨H¡~¤~×'<Ò'<Ñ'>Ô'>ð  Ý—N’NØHÈ$Ì-ñô ð ð  ˜4õ �LŠLÐ6¸¼ÀxÑPÔPÐPØ#Ÿ~š~¨hÑ7Ô7Ð7Ð7Ð7Ð7Ð7Ð7ˆJðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøð ð YÝ—’Ð3°T´]ÀJÈtÐPSÈtÔDTÑUÔUÐUÐUå—’ÐHÈ$Ì-ÑXÔXÐXØÐøÝÔ%¥uÔ';½WÐEð 	ð 	ð 	Ý�NŠNØ>Ø”Ý�S‘	”	Ô"Øñ	ô ð ð �4�4�4�4�4øøøøð	øøø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<rÍ  Ú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¨   rY   ré   rÀ  rÁ  r”   ÚrsplitÚ_convert_silk_to_wavÚ_convert_ffmpeg_to_wavÚ_convert_raw_to_wavrv   rÅ  rÆ  )	r"   rÍ  rJ  r¿  r¥  Útmp_srcÚsrc_pathrÏ  Úresults	            r$   rÂ  z$QQAdapter._convert_audio_to_wav_filew  sü  è è € ð 	ˆˆˆõ �H‰~Œ~Ô$ð7�D�‰NŒNÔ!×'Ò'Ñ)Ô)Ð)à×*Ò*¨:Ñ6Ô6ð 	õ
 	�ŠØEØŒMÝ�
‰OŒOØØ�s˜�sŒOñ	
ô 	
ð 	
ð ×(Ò(°¸EÐ(ÑBÔBð 	$ÀgØ�MŠM˜*Ñ%Ô%Ð%Ø”|ˆHð	$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð —?’? 3¨Ñ*Ô*¨1Ô-°Ñ6ˆð ×0Ò0°¸8ÑDÔDÐDÐDÐDÐDÐDÐDˆð ð 	KØ×6Ò6°xÀÑJÔJÐJÐJÐJÐJÐJÐJˆFð ð 	JØ×3Ò3°JÀÑIÔIÐIÐIÐIÐIÐIÐIˆFð	ÝŒI�hÑÔÐÐøÝð 	ð 	ð 	Øˆ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ÿ   ó   !r,  s   RIFFrª  s   fLaCr®  )s   ÿûs   ÿós   ÿòr©  s   0&²us   OggSr«  s       s      r¨  rI   ©rË   s    r$   rÔ  zQQAdapter._guess_ext_from_data©  sì   € ð ���Œ8�|Ò#Ð# t¨B¨Q¨B¤x°9Ò'<Ð'<Ø�7Ø���Œ8�xÒÐØ�7Ø���Œ8�wÒÐØ�6Ø���Œ8�wÒÐØ�7Ø���Œ8Ð>Ð>Ð>Ø�6Ø���Œ8Ð*Ò*Ð*¨d°2°A°2¬hÐ:MÒ.MÐ.MØ�6Ø���Œ8Ð*Ò*Ð*¨d°2°A°2¬hÐ: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.Nr,  rà  rÿ   rá  rÝ  rÞ  rI   râ  s    r$   Ú_looks_like_silkzQQAdapter._looks_like_silk½  s;   € ð �B�Q�BŒx˜9Ò$ÐX¨¨R¨a¨R¬°HÒ(<ÐXÀÀRÀaÀRÄÈLÒ@XÐXr%   rÚ  rÏ  c              ƒ  ó°  K  — 	 ddl }n1# t          $ r$ t                               d| j        ¦  «         Y dS w xY w	 |                     ||d¬¦  «         t          |¦  «                             ¦   «         r„t          |¦  «                             ¦   «         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          |¦  «                             ¦   «         r©t          |¦  «                             ¦   «         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 )zÏConvert 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: %srÓ  rÙ   r§  z3[%s] pilk converted %s (as .silk) to wav (%d bytes)z%[%s] pilk .silk conversion failed: %s)ÚpilkÚImportErrorr¡   r¢   rY   Úsilk_to_wavr   rÃ  ÚstatÚst_sizerý   r”   r®   rÕ  ÚshutilÚcopy2rv   rÅ  rÆ  )r"   rÚ  rÏ  ré  r´   Ú	silk_pathrî  s          r$   rÖ  zQQAdapter._convert_silk_to_wavÂ  s&  è è € ð	ØˆKˆKˆKˆKøÝð 	ð 	ð 	Ý�NŠNØ]Ø”ñô ð ð �4�4ð	øøøð	WØ×Ò˜X x°eÐÑ<Ô<Ð<Ý�H‰~Œ~×$Ò$Ñ&Ô&ð  ­4°©>¬>×+>Ò+>Ñ+@Ô+@Ô+HÈ2Ò+MÐ+MÝ—’Ø>Ø”MÝ˜‘N”NÔ'Ý˜‘N”N×'Ò'Ñ)Ô)Ô1ñ	ô ð ð  �øøÝð 	Wð 	Wð 	WÝ�LŠLÐAÀ4Ä=ÐRUÑVÔVÐVÐVÐVÐVÐVÐVøøøøð	Wøøøð —O’O C¨Ñ+Ô+¨AÔ.°Ñ8ˆ	ð	ØˆMˆMˆMà�LŠL˜ 9Ñ-Ô-Ð-Ø×Ò˜Y¨°uÐÑ=Ô=Ð=Ý�H‰~Œ~×$Ò$Ñ&Ô&ð  ­4°©>¬>×+>Ò+>Ñ+@Ô+@Ô+HÈ2Ò+MÐ+MÝ—’ØIØ”MÝ˜‘N”NÔ'Ý˜‘N”N×'Ò'Ñ)Ô)Ô1ñ	ô ð ð  ðÝ”	˜)Ñ$Ô$Ð$Ð$øÝð ð ð Ø�ðøøøøøõ ð 	Vð 	Vð 	VÝ�LŠLÐ@À$Ä-Ð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ÿ   ræ  z [%s] raw PCM fallback failed: %s)
Úwaverk   ÚsetnchannelsÚsetsampwidthÚsetframerateÚwriteframesr®   r¡   rý   rY   )r"   rÍ  rÏ  ró  Úwfr´   s         r$   rØ  zQQAdapter._convert_raw_to_wavø  s  è è € ð	ØˆKˆKˆKà—’˜8 SÑ)Ô)ð +¨RØ—’ Ñ"Ô"Ð"Ø—’ Ñ"Ô"Ð"Ø—’ Ñ&Ô&Ð&Ø—’˜zÑ*Ô*Ð*ð	+ð +ð +ñ +ô +ð +ð +ð +ð +ð +ð +øøøð +ð +ð +ð +ð
 ˆOøÝð 	ð 	ð 	Ý�LŠLÐ;¸T¼]ÈCÑPÔPÐPØ�4�4�4�4�4øøøøð	øøø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: %séÈ   Úreplace)Úerrorsz [%s] ffmpeg conversion error: %srè  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Ú
returncoderþ  Úreadr¡   r¢   rY   r   r”   ÚdecodeÚTimeoutErrorÚFileNotFoundErrorrÃ  rì  rí  rý   )r"   rÚ  rÏ  Úprocrþ  r´   s         r$   r×  z QQAdapter._convert_ffmpeg_to_wav  s4  è è € ð	Ý Ô7ØØØØØØØØØÝÔ)Ô1ÝÔ)Ô.ðñ ô ð ð ð ð ð ð ˆDõ Ô" 4§9¢9¡;¤;¸Ð;Ñ;Ô;Ð;Ð;Ð;Ð;Ð;Ð;Ð;ØŒ !Ò#Ð#Ø59´[ÐI˜tœ{×/Ò/Ñ1Ô1Ð1Ð1Ð1Ð1Ð1Ð1Ð1Àc�Ý—’Ø3Ø”MÝ˜‘N”NÔ'Ø˜4˜C˜4”L×'Ò'¨yÐ'Ñ9Ô9ñ	ô ð ð �tð $øõ Ô$Õ&7Ð8ð 	ð 	ð 	Ý�NŠNÐ=¸t¼}ÈcÑRÔRÐRØ�4�4�4�4�4øøøøð	øøøõ �H‰~Œ~×$Ò$Ñ&Ô&ð 	­$¨x©.¬.×*=Ò*=Ñ*?Ô*?Ô*GÈ2Ò*MÐ*MÝ�NŠNØ=Ø”Ý�X‘”Ô#ñô ð ð
 �4Ý�ŠØ8ØŒMÝ�‰NŒNÔÝ�‰NŒN×ÒÑ!Ô!Ô)ñ		
ô 	
ð 	
ð ˆ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)r  r  r  Ú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)rd   rt   ru   r  r   Úrstriprv   rw   )	r"   rt   Ústt_cfgr  r  r  r  Ú_PROVIDER_BASE_URLSÚ
qq_stt_keys	            r$   Ú_resolve_stt_configzQQAdapter._resolve_stt_config8  s×  € ð ”Ô!Ð' Rˆð —)’)˜EÑ"Ô"ˆÝ�g�tÑ$Ô$ð 	¨¯ª°YÑ)?Ô)?ÀuÐ)LÐ)LØ—{’{ 9Ñ-Ô-ÐL°·²¸ZÈÑ1LÔ1LˆHØ—k’k (Ñ+Ô+ÐI¨w¯{ª{¸9ÀbÑ/IÔ/IˆGØ—K’K ¨Ñ,Ô,ˆEØð ˜Gð à (§¢°Ñ 4Ô 4Ø&Ø"Ð1 kðð ð ð ð Ø"Ÿ;š; z°5Ñ9Ô9�ð IØ9ØHð'ð 'Ð#ð
 /×2Ò2°8¸RÑ@Ô@�Øð à$,Ø#*Ø!&ð "_Ø2:¸nÐ2LÐ2L Y YÐR]ð	ð ð õ ”YÐ/°Ñ4Ô4ˆ
Øð 
	Ý”yØ!Ø=ñô ˆHõ ”I˜n¨iÑ8Ô8ˆEà$ŸOšO¨CÑ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)Nr  r  r  Úrbz/audio/transcriptionsrÏ   zBearer Úfilez	audio/wavrq   )rÒ   ÚfilesrË   r›   Úchoicesr   r²   r>  r   rW  z0[%s] STT API call failed (model=%s, base=%s): %srS   )r%  r¡   r¢   rY   rk   r‚   rÈ   r   r”   rÉ   rÄ   ru   rx   r¤   rÇ  rÉ  )r"   rÏ  r"  r  r  r  ÚfrÊ   rÛ  r*  r>  rW  r´   s                r$   rÄ  zQQAdapter._call_sttp  sb  è è € ð ×*Ò*Ñ,Ô,ˆØð 	Ý�NŠNØKØ”ñô ð ð �4à˜:Ô&ˆØ˜)Ô$ˆØ˜Ô ˆð	Ý�h Ñ%Ô%ð ¨Ø!Ô.×3Ò3ØÐ6Ð6Ð6Ø,Ð.A¸Ð.AÐ.AÐBØ!¥D¨¡N¤NÔ$7¸¸KÐ#HÐIØ! 5Ð)Ø ð 4ñ ô ð ð ð ð ð ð �ðð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð ×!Ò!Ñ#Ô#Ð#Ø—Y’Y‘[”[ˆFà—j’j ¨BÑ/Ô/ˆGØð +Ø! !œ*Ÿ.š.¨°BÑ7Ô7×;Ò;¸IÀrÑJÔJ�Ø—=’=‘?”?ð +Ø"Ÿ=š=™?œ?Ð*à—:’:˜f bÑ)Ô)ˆDØ�zŠz‰|Œ|ð $Ø—z’z‘|”|Ð#Ø�4øÝÔ%¥wÐ/ð 	ð 	ð 	Ý�NŠNØBØ”ØØ˜˜"˜”Øñô ð ð �4�4�4�4�4øøøøð	øøø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¼  rÓ  rÙ   rª  r§  z/[%s] audio conversion failed for %s (format=%s)rR   Úqq_voicezqq_voice.wavz%[%s] Failed to read converted wav: %s)r¿  r   r�  r   r½  r{   rÔ  rÀ  rÁ  r”   rÕ  rä  rÖ  r×  r¡   r¢   rY   r   rv   rÅ  rÆ  r®   Ú
read_bytesrý   )r"   rÍ  r,  r¿  r¥  rÙ  rÚ  rÏ  Úis_silkrÛ  Úwav_datar´   s               r$   r¤  zQQAdapter._convert_audio_to_wav£  sž  è è € ð 	ˆˆˆõ
 ˜
Ñ#Ô#Ô(ð�D•˜*Ñ%Ô%Ô*Ñ+Ô+Ô2×8Ò8Ñ:Ô:Ð:àð 	ð
 ð 
	8�cð 	"
ð 	
ð 	
ð ×+Ò+¨JÑ7Ô7ˆCà×(Ò(°¸EÐ(ÑBÔBð 	$ÀgØ�MŠM˜*Ñ%Ô%Ð%Ø”|ˆHð	$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð —?’? 3¨Ñ*Ô*¨1Ô-°Ñ6ˆð	Ø˜W’nÐI¨×(=Ò(=¸jÑ(IÔ(IˆGØð OØ#×8Ò8¸À8ÑLÔLÐLÐLÐLÐLÐLÐL��à#×:Ò:¸8ÀXÑNÔNÐNÐNÐNÐNÐNÐN�àð OÝ—’ØEØ”MØ˜s ˜s”OØñ	ô ð õ 1°Ð=MÈÐ=MÐ=MÑNÔNðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøðOøõ ð 	Kð 	Kð 	KÝ,¨ZÐ9IÀCÐ9IÐ9IÑJÔJÐJÐJðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøð	KøøøðOðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøøðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøøøøð	Ý˜H‘~”~×0Ò0Ñ2Ô2ˆHÝŒI�hÑÔÐÝ,¨X°~ÑFÔFÐFøÝð 	ð 	ð 	Ý�LŠLÐ@À$Ä-ÐQTÑUÔUÐUØ�4�4�4�4�4øøøøð	øøø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.u.   HTTP client not initialized â€” not connected?NrÎ   zapplication/json)rÏ   zContent-TyperÐ   )rÒ   rÄ   r›   i�  zQQ Bot API error [z] z: r²   zQQ Bot API timeout [z]: )r‚   r`   r¦   rC   Úrequestr,   rÄ   Ústatus_coderu   r¤   ÚTimeoutException)
r"   r2  r�  r3  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ð —9’9‘;”;ˆDØÔ 3Ò&Ð&Ý"ð3¨Ô)9ð 3ð 3¸Tð 3ð 3Ø—x’x 	¨4Ñ0Ô0ð3ð 3ñô ð ð ˆKøÝÔ%ð 	Oð 	Oð 	OÝÐD°dÐDÐD¸sÐDÐDÑEÔEÈ3Ð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.rQ  ú
/v2/users/ú/filesú/v2/groups/)r<  r>  rÓ   r=  r?  é   ÚPOSTr   Nc              3  ó    •K  — | ]}|‰v V — Œ	d S r°  rI   )r²  ÚkwÚerr_msgs     €r$   r´  z*QQAdapter._upload_media.<locals>.<genexpr>,  s;   øè è € ð ð àð ˜g˜ðð ð ð ð ð r%   )Ú400Ú401ÚInvalidr›   ÚTimeoutrÿ   g      ø?rÙ   )	rA   Úranger9  r0   r`   r   rµ  rŽ   rà   )r"   r:  r;  r<  rÓ   r=  r>  r?  r�  r3  Úattemptr´   rH  s               @r$   Ú_upload_mediazQQAdapter._upload_media  s¸  øè è € ð ˜eÒ#Ð#ð +˜Ð*Ð*Ð*Ð*à0˜yÐ0Ð0Ð0ð 	ð #Ø(ð 
ð  
ˆð ð 	*ØˆD�‰KˆKØð 	*Ø )ˆD�ÑØ�Ò'Ð'¨IÐ'Ø )ˆD�Ñõ ˜Q‘x”xð 	ð 	ˆ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)rr   Nz[%s] Reconnected after %.1fsTz$[%s] Still not connected after %.0fsF)	r¡   r¨   rY   Ú_RECONNECT_WAIT_SECONDSrŽ   rà   Ú_RECONNECT_POLL_INTERVALÚis_connectedr¢   )r"   Úwaiteds     r$   Ú_wait_for_reconnectionz QQAdapter._wait_for_reconnection;  sÏ   è è € õ 	�ŠÐSØ”M 4Ô#?ñ	Aô 	Að 	AàˆØ�tÔ3Ò3Ð3Ý”- Ô =Ñ>Ô>Ð>Ð>Ð>Ð>Ð>Ð>Ð>Ø�dÔ3Ñ3ˆFØÔ ð Ý—’Ð:¸D¼MÈ6ÑRÔRÐRØ�tð �tÔ3Ò3Ð3õ 	�ŠÐ=¸t¼}ÈdÔNjÑkÔkÐkØˆur%   r8  Ú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 )	zÐSend 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¯   )	rS  rU  r   rx   Úformat_messageÚtruncate_messager7   Ú_send_chunkr[  )	r"   r8  r>  rV  rW  Ú	formattedÚchunksÚlast_resultÚchunks	            r$   ÚsendzQQAdapter.sendQ  s  è è € ð àÔ ð 	XØ×4Ò4Ñ6Ô6Ð6Ð6Ð6Ð6Ð6Ð6ð XÝ!¨%°ÐRVÐWÑWÔWÐWàð 	,˜gŸmšm™oœoð 	,Ý dÐ+Ñ+Ô+Ð+à×'Ò'¨Ñ0Ô0ˆ	Ø×&Ò& y°$Ô2IÑJÔJˆå ¨°kÐBÑBÔBˆØð 	ð 	ˆEØ $× 0Ò 0°¸%ÀÑ JÔ JÐJÐJÐJÐJÐJÐJˆKØÔ&ð #Ø"Ð"Ð"Ð"àˆHˆHØÐ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.NrD  rQ  rl  rt  FzUnknown chat type for r\  c              3  ó    •K  — | ]}|‰v V — Œ	d S r°  rI   )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²  rg  Ú	error_msgs     €r$   r´  z(QQAdapter._send_chunk.<locals>.<genexpr>Ÿ  sC   øè è € ð 
ð 
Ø'(ˆA�—’Ñ"Ô"Ð"ð
ð 
ð 
ð 
ð 
ð 
r%   )ri  rj  rk  rZ  )Ú_guess_chat_typerM  Ú_send_c2c_textÚ_send_group_textÚ_send_guild_textr   r®   r   r{   rµ  r¡   r¢   rY   rŽ   rà   r¯   )r"   r8  r>  rV  Úlast_excrU  rN  r´   rê   r˜   rh  rm  s             @@r$   r_  zQQAdapter._send_chunkr  s‰  øøè è € ð )-ˆØ×)Ò)¨'Ñ2Ô2ˆ	å˜Q‘x”xð 	/ñ 	/ˆGð/Ø Ò%Ð%Ø!%×!4Ò!4°W¸gÀxÑ!PÔ!PÐPÐPÐPÐPÐPÐPÐPÐPÐPØ 'Ò)Ð)Ø!%×!6Ò!6°wÀÈÑ!RÔ!RÐRÐRÐRÐRÐRÐRÐRÐRÐRØ 'Ò)Ð)Ø!%×!6Ò!6°wÀÈÑ!RÔ!RÐRÐRÐRÐRÐRÐRÐRÐRÐRå%Ø %Ð-OÀgÐ-OÐ-Oðñ ô ð ð ð øõ ð /ð /ð /Ø�Ý˜#‘h”h—n’nÑ&Ô&�åð ð ð ð à!Uðñ ô ñ ô ð ð �E�E�E�E�Eà˜Q’;�;Ø 1¨¡<Ñ0�EÝ—N’NØ>ØœØ !™ØØñô ð õ "œ-¨Ñ.Ô.Ð.Ð.Ð.Ð.Ð.Ð.Ð.øøøøùøøøøð'/øøøð* &.ÐB•C˜‘M”M�M°?ˆ	Ý�ŠÐ+¨T¬]¸IÑFÔFÐFÝð 
ð 
ð 
ð 
Ø,Qð
ñ 
ô 
ñ 
ô 
ð 
ˆ	õ  %¨yÀIÐNÑNÔNÐNs0   ¯"B4Á"B4Á9"B4ÂB4Â4
EÂ>>EÄAEÅEÚopenidc              ƒ  óV  K  — |                       |p|¦  «        }|                      ||¦  «        }|r||d<   |                      dd|› d�|¦  «        ƒ d{V —†}t          |                     dt          j        ¦   «         j        dd…         ¦  «        ¦  «        }t          d||¬	¦  «        S )
z%Send text to a C2C user via REST API.r*  rE  rA  ú	/messagesNr<  é   T©r[  r6  Úraw_response©	r3  Ú_build_text_bodyr9  r   ru   r.  r/  r0  r   )r"   rs  r>  rV  Úmsg_seqr3  rË   r*  s           r$   ro  zQQAdapter._send_c2c_text¤  s½   è è € ð ×$Ò$ XÐ%7°Ñ8Ô8ˆØ×$Ò$ W¨hÑ7Ô7ˆØð 	&Ø%ˆD�‰Nà×&Ò& vÐ/M¸FÐ/MÐ/MÐ/MÈtÑTÔTÐTÐTÐTÐTÐTÐTˆÝ�T—X’X˜d¥D¤J¡L¤LÔ$4°S°b°SÔ$9Ñ:Ô:Ñ;Ô;ˆÝ $°6ÈÐMÑMÔMÐMr%   rj  c              ƒ  óV  K  — |                       |p|¦  «        }|                      ||¦  «        }|r||d<   |                      dd|› d�|¦  «        ƒ d{V —†}t          |                     dt          j        ¦   «         j        dd…         ¦  «        ¦  «        }t          d||¬	¦  «        S )
z"Send text to a group via REST API.r*  rE  rC  ru  Nr<  rv  Trw  ry  )r"   rj  r>  rV  r{  r3  rË   r*  s           r$   rp  zQQAdapter._send_group_text±  sÐ   è è € ð ×$Ò$ XÐ%=°Ñ>Ô>ˆØ×$Ò$ W¨hÑ7Ô7ˆØð 	&Ø%ˆD�‰Nà×&Ò&ØÐ9 ,Ð9Ð9Ð9¸4ñ
ô 
ð 
ð 
ð 
ð 
ð 
ð 
ˆõ �T—X’X˜d¥D¤J¡L¤LÔ$4°S°b°SÔ$9Ñ:Ô:Ñ;Ô;ˆÝ $°6ÈÐMÑMÔMÐMr%   rp  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>  Nr*  rE  z
/channels/ru  r<  rv  Trw  )r7   r9  r   ru   r.  r/  r0  r   )r"   rp  r>  rV  r3  rË   r*  s          r$   rq  zQQAdapter._send_guild_textÀ  s©   è è € ð !*¨7Ð3L°TÔ5LÐ3LÔ+MÐNˆØð 	&Ø%ˆD�‰Nà×&Ò& vÐ/Q¸JÐ/QÐ/QÐ/QÐSWÑXÔXÐXÐXÐXÐXÐXÐXˆÝ�T—X’X˜d¥D¤J¡L¤LÔ$4°S°b°SÔ$9Ñ:Ô:Ñ;Ô;ˆÝ $°6ÈÐMÑMÔMÐMr%   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.Údefaultr>  N)ÚmarkdownÚmsg_typer{  )r>  r�  r{  r6  Úmessage_reference)r3  rz   r7   r;   r:   )r"   r>  rV  r{  r3  s        r$   rz  zQQAdapter._build_text_bodyÌ  s¢   € ð ×$Ò$ XÐ%:°Ñ;Ô;ˆàÔ!ð 	à&¨Ð0I°$Ô2IÐ0IÔ(JÐKÝ-Ø"ð$ð $ˆDˆDð #Ð#< TÔ%<Ð#<Ô=Ý)Ø"ðð ˆDð ð 	EàÔ)ð EØ-9¸8Ð,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: %srO  )r8  r>  rV  )	Ú_send_mediar>   r[  Ú_is_urlr¡   r¢   rY   r¯   rd  )r"   r8  rƒ  r„  rV  rW  rÛ  Úfallbacks           r$   Ú
send_imagezQQAdapter.send_imageê  sÞ   è è € ð à×'Ò'Ø�YÕ 0°'¸7ÀHñ
ô 
ð 
ð 
ð 
ð 
ð 
ð 
ˆð Œ>ð 	 §¢¨iÑ!8Ô!8ð 	ØˆMõ 	�ŠØ>ØŒMØŒLñ	
ô 	
ð 	
ð
 18ÐF�gÐ,Ð, Ð,Ð,Ð,¸YˆØ—Y’Y w¸È8�YÑ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†  r>   )r"   r8  rŠ  r„  rV  Úkwargss         r$   Úsend_image_filezQQAdapter.send_image_file  óO   è è € ð Ø×%Ò%Ø�ZÕ!1°7¸GÀXñ
ô 
ð 
ð 
ð 
ð 
ð 
ð 
ð 	
r%   Ú
audio_pathc              ‹  óR   K  — ~|                       ||t          d||¦  «        ƒ d{V —†S )zSend a voice message natively.rz  N)r†  r@   )r"   r8  r�  r„  rV  rŒ  s         r$   Ú
send_voicezQQAdapter.send_voice  rŽ  r%   Ú
video_pathc              ‹  óR   K  — ~|                       ||t          d||¦  «        ƒ d{V —†S )zSend a video natively.r|  N)r†  r?   )r"   r8  r’  r„  rV  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†  rA   )r"   r8  r•  r„  r?  rV  rŒ  s          r$   Úsend_documentzQQAdapter.send_document.  s[   è è € ð Ø×%Ò%ØØÝØØØØð &ñ 
ô 
ð 
ð 
ð 
ð 
ð 
ð 
ð 	
r%   Úmedia_sourceÚkindc              ƒ  ó\  K  — | j         s,|                      ¦   «         ƒ d{V —†st          ddd¬¦  «        S 	 |                      ||¦  «        ƒ d{V —†\  }}	}
|                      |¦  «        }|dk    rd|› d�nd	|› d�}|d
k    rt          dd¬¦  «        S |                      ||||                      |¦  «        s|nd|                      |¦  «        r|ndd|t          k    r|
nd¬¦  «        ƒ d{V —†}|                     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"          $ rI}t$                               d| j        |¦  «         t          dt          |¦  «        ¬¦  «        cY d}~S d}~ww xY w)z*Upload media and send as a native message.NFrY  TrZ  rQ  rA  rB  rC  rt  z,Guild media send not supported via this pathr\  )r=  rÓ   r>  r?  Ú	file_infozUpload returned no file_info: )r�  Úmediar{  r>  r*  rE  ru  r<  rv  rw  z[%s] Media send failed: %s)rS  rU  r   Ú_load_mediarn  rO  r‡  rA   ru   r3  r<   r7   r9  r   r.  r/  r0  r®   r¡   r¯   rY   )r"   r8  r˜  r<  r™  r„  rV  r?  rË   rH  Úresolved_namerU  Útarget_pathÚuploadr›  r{  r3  Ú	send_datar´   s                      r$   r†  zQQAdapter._send_mediaC  sF  è è € ð Ô ð 	XØ×4Ò4Ñ6Ô6Ð6Ð6Ð6Ð6Ð6Ð6ð XÝ!¨%°ÐRVÐWÑWÔWÐWðB	=à6:×6FÒ6FØ˜iñ7ô 7ð 1ð 1ð 1ð 1ð 1ð 1Ñ-ˆD�, ð
 ×-Ò-¨gÑ6Ô6ˆIð  Ò%Ð%ð -˜WÐ,Ð,Ð,Ð,à2 7Ð2Ð2Ð2ð ð ˜GÒ#Ð#õ "Ø!Ð)Wðñ ô ð ð
  ×-Ò-ØØØØ&*§l¢l°<Ñ&@Ô&@ÐJ˜$˜$ÀdØ$(§L¢L°Ñ$>Ô$>ÐH�L�LÀDØ"Ø+4½Ò+GÐ+G˜-˜-ÈTð .ñ ô ð ð ð ð ð ð ˆFð Ÿ
š
 ;Ñ/Ô/ˆIØð Ý!Ø!Ð)RÈ&Ð)RÐ)Rðñ ô ð ð
 ×(Ò(¨Ñ1Ô1ˆGå*Ø% yÐ1Ø"ð$ð $ˆDð
 ð EØ")Ð*C¨DÔ,CÐ*CÔ"D��Y‘Øð *Ø!)��X‘à"×/Ò/Øð ! EÒ)Ð)ð 4 Ð3Ð3Ð3Ð3à9 wÐ9Ð9Ð9àñô ð ð ð ð ð ð ˆIõ ØÝ˜yŸ}š}¨Tµ4´:±<´<Ô3CÀCÀRÀCÔ3HÑIÔIÑJÔJØ&ðñ ô ð øõ
 ð 	=ð 	=ð 	=Ý�LŠLÐ5°t´}ÀcÑJÔJÐJÝ eµ3°s±8´8Ð<Ñ<Ô<Ð<Ð<Ð<Ð<Ð<Ð<øøøøð	=øøøs,   ·AG ÂBG ÄB:G Ç
H+Ç">H&È H+È&H+r7  ú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œ  Ú<rD  z1Invalid media source (looks like a placeholder): zMedia file not found: Úascii)r   rx   r   r   Úschemer¢  Ú
guess_typer   r�  r”   Ú
expanduserÚis_absoluteÚcwdÚresolverÃ  Úis_filer‹  ré   r  r/  Úbase64Ú	b64encoder  )	r"   r7  r?  ÚparsedrH  rž  Ú
local_pathr&  Úb64s	            r$   r�  zQQAdapter._load_media–  sÑ  è è € õ �V‘”×"Ò"Ñ$Ô$ˆØð 	9ÝÐ7Ñ8Ô8Ð8å˜&Ñ!Ô!ˆØŒ=Ð-Ð-Ð-å$Ô/°Ñ7Ô7¸Ô:ÐXÐ>XˆLØ%ÐJ­¨f¬kÑ):Ô):Ô)?ÐJÀ7ˆMØ˜<¨Ð6Ð6õ ˜&‘\”\×,Ò,Ñ.Ô.ˆ
Ø×%Ò%Ñ'Ô'ð 	=Ýœ(™*œ* zÑ1×:Ò:Ñ<Ô<ˆJà× Ò Ñ"Ô"ð 	K¨*×*<Ò*<Ñ*>Ô*>ð 	Kð × Ò  Ñ%Ô%ð ­¨V©¬°qª¨Ý ØRÈÐRÐRñô ð õ $Ð$I¸ZÐ$IÐ$IÑJÔJÐJà×#Ò#Ñ%Ô%ˆØ!Ð4 Z¤_ˆåÔ$¥S¨¡_¤_Ñ5Ô5°aÔ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``.
        NrQ  rr   rÙ   )Ú
input_typeÚinput_second)r�  r*  Úinput_notifyr{  rE  rA  ru  z[%s] send_typing failed: %s)rS  rn  rŠ   ru   rÇ   r‹   Ú_TYPING_DEBOUNCE_SECONDSr3  r=   Ú_TYPING_INPUT_SECONDSr9  r®   r¡   rý   rY   )
r"   r8  rW  rU  r*  ÚnowÚ	last_sentr{  r3  r´   s
             r$   Úsend_typingzQQAdapter.send_typingÀ  sv  è è € ð Ô ð 	ØˆFà×)Ò)¨'Ñ2Ô2ˆ	Ø˜ÒÐØˆFàÔ"×&Ò& wÑ/Ô/ˆØð 	ØˆFõ Œi‰kŒkˆØÔ(×,Ò,¨W°cÑ:Ô:ˆ	Ø�‰?˜TÔ:Ò:Ð:ØˆFð	LØ×(Ò(¨Ñ1Ô1ˆGå1Ø à"#Ø$(Ô$>ð!ð !ð #ðð ˆDð ×#Ò# FÐ,K¸Ð,KÐ,KÐ,KÈTÑRÔRÐRÐRÐRÐRÐRÐRÐRØ,/ˆDÔ  Ñ)Ð)Ð)øÝð 	Lð 	Lð 	LÝ�LŠLÐ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 )zÄFormat 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).
        )rz   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.)rl  rt  rl  rR  )r”   rí   )rn  )r"   r8  rU  s      r$   Úget_chat_infozQQAdapter.get_chat_infoú  s=   è è € à×)Ò)¨'Ñ2Ô2ˆ	àØ(Ð,>Ð>Ð>�G�GÀDð
ð 
ð 	
r%   c                óH   — t          t          | ¦  «        ¦  «        j        dv S )Nr¤  )r   r   r©  )r7  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'.rQ  )rˆ   )r"   r8  s     r$   rn  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Úsubrx   )r>  rÄ  Ústrippeds      r$   rn  zQQAdapter._strip_at_mention	  s/   € ð 	ˆ	ˆ	ˆ	à—6’6˜+ r¨7¯=ª=©?¬?Ñ;Ô;ˆØˆr%   rT  c                ól   — | j         dk    rdS | j         dk    r|                      | j        |¦  «        S dS ©NÚdisabledFÚ	allowlistT)r|   Ú_entry_matchesr}   )r"   rT  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 rÈ  )r~   rË  r   )r"   rÌ  rT  s      r$   rm  zQQAdapter._is_group_allowed 	  sA   € ØÔ Ò+Ð+Ø�5ØÔ Ò,Ð,Ø×&Ò& tÔ'=¸xÑHÔHÐHØˆtr%   ÚentriesrL   Útargetc                óú   — t          |¦  «                             ¦   «                              ¦   «         }| D ]D}t          |¦  «                             ¦   «                              ¦   «         }|dk    s||k    r dS ŒEdS )NÚ*TF)r   rx   r{   )rÎ  rÏ  Únormalized_targetÚentryÚ
normalizeds        r$   rË  zQQAdapter._entry_matches'	  s€   € å ™KœK×-Ò-Ñ/Ô/×5Ò5Ñ7Ô7ÐØð 	ð 	ˆEÝ˜U™œ×)Ò)Ñ+Ô+×1Ò1Ñ3Ô3ˆJØ˜SÒ Ð  JÐ2CÒ$CÐ$CØ�t�tð %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 )z¹Parse 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   Ú	TypeErrorÚfromtimestampr   )r"   r&  s     r$   rc  zQQAdapter._parse_qq_timestamp0	  s½   € ð ð 	1Ý”<¥8¤<Ð0Ñ0Ô0Ð0ð	ÝÔ)¨#Ñ.Ô.Ð.øÝ�IÐ&ð 	ð 	ð 	ØˆDð	øøøð	ÝÔ)­#¨c©(¬(°T©/½h¼lÐ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 rI   rI   )r²  ÚkeyÚtsÚcutoffs      €r$   ú
<dictcomp>z+QQAdapter._is_duplicate.<locals>.<dictcomp>F	  s+   ø€ ð #ð #ð #Ù#˜C ÀbÈ6ÂkÀk��RÀkÀkÀkr%   TF)rÇ   ré   r‰   r9   r8   Úitems)r"   r*  r»  rß  s      @r$   r@  zQQAdapter._is_duplicateB	  s�   ø€ ÝŒi‰kŒkˆÝˆtÔ"Ñ#Ô#¥nÒ4Ð4ØÕ/Ñ/ˆFð#ð #ð #ð #Ø'+Ô':×'@Ò'@Ñ'BÔ'Bð#ñ #ô #ˆDÔð �TÔ(Ð(Ð(Ø�4Ø&)ˆÔ˜FÑ#Øˆur%   )rD   r   )r   r   rD   rZ   )rd   r   ©rD   rE   )rD   rZ   )r³   r   rD   rZ   )râ   r   rD   rE   )r÷   r  rD   rZ   )rû   r   rD   rZ   )r&  r   rD   r'  )r*  r   rD   r   )r4  r   rD   rZ   )r:  r   rû   r   rD   rZ   )rû   r  r*  r   r>  r   r?  r  r=  r   rD   rZ   )rZ  r]  r[  r]  )rG  r   rD   r  )rÓ   r   rH  r   rD   r™  )rH  r   rJ  r   rD   rE   )rD   r·  )rÓ   r   rH  r   rJ  r   r†  r™  r‡  r™  rD   r™  )rÍ  rÐ  rJ  r   rD   r™  )rË   rÐ  rD   r   )rË   rÐ  rD   rE   )rÚ  r   rÏ  r   rD   r™  )rÍ  rÐ  rÏ  r   rD   r™  )rD   r  )rÏ  r   rD   r™  )rÍ  rÐ  r,  r   rD   r™  )
r2  r   r�  r   r3  r'  r›   r4  rD   r  )NNFN)r:  r   r;  r   r<  r   rÓ   r™  r=  r™  r>  rE   r?  r™  rD   r  )NN)
r8  r   r>  r   rV  r™  rW  r'  rD   r   r°  )r8  r   r>  r   rV  r™  rD   r   )rs  r   r>  r   rV  r™  rD   r   )rj  r   r>  r   rV  r™  rD   r   )rp  r   r>  r   rV  r™  rD   r   )r>  r   rV  r™  rD   r  )NNN)r8  r   rƒ  r   r„  r™  rV  r™  rW  r'  rD   r   )
r8  r   rŠ  r   r„  r™  rV  r™  rD   r   )
r8  r   r�  r   r„  r™  rV  r™  rD   r   )
r8  r   r’  r   r„  r™  rV  r™  rD   r   )r8  r   r•  r   r„  r™  r?  r™  rV  r™  rD   r   )r8  r   r˜  r   r<  r   r™  r   r„  r™  rV  r™  r?  r™  rD   r   )r7  r   r?  r™  rD   r¢  )r8  r   rD   rZ   )r>  r   rD   r   )r8  r   rD   r  )r7  r   rD   rE   )r8  r   rD   r   )rT  r   rD   rE   )rÌ  r   rT  r   rD   rE   )rÎ  rL   rÏ  r   rD   rE   )r&  r   rD   r   )r*  r   rD   rE   )Sr&   r'   r(   r)   ÚSUPPORTS_MESSAGE_EDITINGr7   rº  r¹  ÚpropertyrY   rc   r!   r”   rµ   r»   r°   r¦   r§   r©   r«   rá   rß   r¬   r  r  Ústaticmethodr  rñ   r!  rð   r3  r9  r"  rA  rB  rC  rD  rb  r_  r�  rŒ  r¡  r�  rÂ  rÔ  rä  rÖ  rØ  r×  r%  rÄ  r¤  r/   r9  rO  rQ  rR  rU  rd  r_  ro  rp  rq  rz  r‰  r�  r‘  r”  r—  r†  r�  r½  r]  rÀ  r‡  rn  rn  r\  rm  rË  rc  r@  r*   r+   s   @r$   rQ   rQ   ‹   s™  ø€ € € € € ØTÐTð  %ÐØ+ÐØÐØ!Ðàðð ð ñ „Xðð(ð (ð (ð (ð.;ð .;ð .;ð .;ð .;ð .;ðh ðð ð ñ „Xðð3ð 3ð 3ð 3ðj8ð 8ð 8ð 8ð2(ð (ð (ð (ð0!&ð !&ð !&ð !&ðFð ð ð ð4Rð Rð Rð Rð(W%ð W%ð W%ð W%ðrð ð ð ð.7ð 7ð 7ð 7ð&ð ð ð ð&$Qð $Qð $Qð $QðL"ð "ð "ð "ðB ð
ð 
ñ „\ð
ð3?ð 3?ð 3?ð 3?ðjVð Vð Vð Vð ð>ð >ð >ñ „\ð>ð ð*ð *ð *ñ „\ð*ð,ð ,ð ,ð ,ð ,ð ,ðQð Qð Qð Qð6V)ð V)ð V)ð V)ðp8)ð 8)ð 8)ð 8)ðt6)ð 6)ð 6)ð 6)ðp2)ð 2)ð 2)ð 2)ðp ð ð  ð  ñ „\ð ð&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ðf HLðNð Nð Nð Nð Nð NRðNð Nð Nð Nð Nð  LPð
Nð 
Nð 
Nð 
Nð 
Nð ;?ðð ð ð ð ðD &*Ø&*Ø15ðUð Uð Uð Uð Uð< &*Ø&*ð
ð 
ð 
ð 
ð 
ð$ &*Ø&*ð
ð 
ð 
ð 
ð 
ð$ &*Ø&*ð
ð 
ð 
ð 
ð 
ð$ &*Ø'+Ø&*ð
ð 
ð 
ð 
ð 
ð6 &*Ø&*Ø'+ðQ=ð Q=ð Q=ð Q=ð Q=ðh ;?ð$0ð $0ð $0ð $0ð $0ðT&Lð &Lð &Lð &Lð &LðX'ð 'ð 'ð 'ð
ð 
ð 
ð 
ð ðAð Að Añ „\ðAðð ð ð ð ðð ð ñ „\ððð ð ð ðð ð ð ð ðð ð ñ „\ðð-ð -ð -ð -ð$
ð 
ð 
ð 
ð 
ð 
ð 
ð 
r%   rQ   râ  )rK   r   rD   rL   )Lr)   Ú
__future__r   rŽ   r°  rÄ   Úloggingr¢  rv   rÇ   r.  r   r   Úpathlibr   Útypingr   r   r	   r
   r   Úurllib.parser   rÕ   rG   rê  r¤   rH   Úgateway.configr   r   Úgateway.platforms.baser   r   r   r   r   r   r   Úgateway.platforms.helpersr   Ú	getLoggerr&   r¡   r®   r   Ú!gateway.platforms.qqbot.constantsr,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   r9   r:   r;   r<   r=   r>   r?   r@   rA   Úgateway.platforms.qqbot.utilsrB   rN   rC   rJ   rO   rQ   rI   r%   r$   ú<module>rñ     sž  ððð ð> #Ð "Ð "Ð "Ð "Ð "à €€€Ø €€€Ø €€€Ø €€€Ø Ð Ð Ð Ø 	€	€	€	Ø €€€Ø €€€Ø 'Ð 'Ð 'Ð 'Ð 'Ð 'Ð 'Ð 'Ø Ð Ð Ð Ð Ð Ø 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ø !Ð !Ð !Ð !Ð !Ð !ðØ€N€N€NàÐÐøØð ð ð ØÐØ€G€G€GðøøøðØ€L€L€Là€O€OøØð ð ð Ø€OØ€E€E€Eðøøøð 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ð
$ð $ð $ð $ðA#ð A#ð A#ð A#ð A#Ð#ñ A#ô A#ð A#ð A#ð A#s$   ÁA Á	AÁAÁ#A* Á*	A6Á5A6