
    `gjd                      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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mZ ddlmZ ddlmZ ddlmZmZ ddlmZmZm Z m!Z!m"Z"m#Z#m$Z$m%Z% ddl&Z&ddl'Z'	 ddl(Z(ddl)Z(d	Z*ddl,m-Z-m.Z. ddl/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6 ddl7m8Z8 ddl9m:Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZA ddlBmCZCmDZDmEZEmFZFmGZGmHZHmIZImJZJmKZKmLZLmMZMmNZNmOZOmPZPmQZQmRZRmSZSmTZTmUZUmVZVmWZWmXZXmYZY ddlZm[Z[  ej                  e]      Z^	 ddl_m`Za eaZbeaZc edeJ      Zee&j                  ZgdZhdZidZjdZkdZldZmdZndZoh dZpdZqh dZrh dZsdZtdZudZvd Zwd!Zx ej                  d"      Zz ej                  d#      Z{ e|h d$      Z} ej                  d%      Z~d&Zd'Zd(Zd)Zd'Z G d* d+      Z G d, d-      Zdd.lmZmZ e G d/ d0             Z G d1 d2e      Z G d3 d4      Z G d5 d6e      Z G d7 d8e      Z G d9 d:e      Z G d; d<e      Z G d= d>e      Z G d? d@e      Z G dA dB      Z G dC dDe      Z G dE dFe      Z G dG dHe      Z G dI dJe      Z G dK dLe      Z G dM dNe      Z G dO dPe      Z G dQ dRe      Z G dS dTe      Z G dU dVe      Z G dW dXe      Z G dY dZe      Z G d[ d\e      Z G d] d^e      Z G d_ d`e      Z G da db      Z G dc dd      Z G de dfe      Z G dg dhe      Z G di dje      Z G dk dle      Z G dm dne      Z G do dpe      Z G dq dr      Z G ds dt      Z G du dv      Z G dw dx      Z G dy dz      Z G d{ d|e0      Zdd}Z	 d	 	 	 	 	 	 	 	 	 dd~Zy# e+$ r d
Z*dZ(Y w xY w# e+$ r dZaY |w xY w)aQ  
Yuanbao platform adapter.

Connects to the Yuanbao WebSocket gateway, handles authentication (AUTH_BIND),
heartbeat, reconnection, message receive (T05) and send (T06).

Configuration in config.yaml (or via env vars):
    platforms:
      yuanbao:
        extra:
          app_id: "..."              # or YUANBAO_APP_ID
          app_secret: "..."          # or YUANBAO_APP_SECRET
          bot_id: "..."              # or YUANBAO_BOT_ID  (optional, returned by sign-token)
          ws_url: "wss://..."        # or YUANBAO_WS_URL
          api_domain: "https://..."  # or YUANBAO_API_DOMAIN
    )annotationsN)datetimetimezone	timedelta)Enum)Path)ABCabstractmethod)AnyCallableClassVarDictIteratorListOptionalTupleTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResultcache_document_from_bytescache_image_from_bytescache_video_from_bytes)MessageDeduplicator)download_urlget_cos_credentialsupload_to_cosbuild_image_msg_bodybuild_file_msg_bodyguess_mime_typemd5_hex)CMD_TYPE_fields_to_dict_get_string_get_varint_parse_fieldsWS_HEARTBEAT_RUNNINGWS_HEARTBEAT_FINISHHERMES_INSTANCE_IDdecode_conn_msgdecode_inbound_pushdecode_forward_msg_datadecode_query_group_info_rsp decode_get_group_member_list_rspencode_auth_bindencode_pingencode_push_ackencode_send_c2c_messageencode_send_group_messageencode_send_private_heartbeatencode_send_group_heartbeatencode_query_group_infoencode_get_group_member_listnext_seq_no)build_session_key)__version__z0.0.0z0wss://bot-wss.yuanbao.tencent.com/wss/connectionzhttps://bot.yuanbao.tencent.comg      >@      .@      $@d         ?>                  >         >                @     r@g      ^@u?   任务有点复杂，正在努力处理中，请耐心等待...zB\[(image|voice|video|file(?::[^|\]]*)?)\|ybres:([A-Za-z0-9_\-]+)\]z\[(\w+):[^\]]*?(/[^\]]+?)\s*\]>   fileimagevideoz\s*\(\d+/\d+\)$2            c                     e Zd ZdZedd       Zedd       Ze	 d	 	 	 	 	 	 	 dd       Zedd       Zedd       Z	edd       Z
e	 	 d	 	 	 	 	 	 	 dd	       Zedd
       Zedd       Zedd       Zedd       Zedd       Zy)MarkdownProcessoraa  Encapsulates all Markdown-related utilities for the Yuanbao platform.

    Provides static methods for:
    - Fence detection and streaming merge
    - Table row detection and sanitization
    - Paragraph-boundary splitting
    - Atomic-block extraction and chunk splitting
    - Outer markdown fence stripping
    - Markdown hint prompt generation
    c                `    d}| j                  d      D ]  }|j                  d      s| } |S )a  
        Detect whether the text has unclosed code block fences.

        Scan line by line, toggling in/out state when encountering a line starting with ```.
        An odd number of toggles indicates an unclosed fence.

        Args:
            text: Markdown text to check

        Returns:
            Returns True if the text ends with an unclosed fence, otherwise False
        F
```)split
startswith)textin_fencelines      L/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/yuanbao.pyhas_unclosed_fencez$MarkdownProcessor.has_unclosed_fence   s;     JJt$ 	(Du%'<	(     c                    | j                         }|sy|j                  d      d   j                         }|j                  d      xr |j	                  d      S )z
        Detect whether the text ends with a table row (last non-empty line starts and ends with |).

        Args:
            text: Text to check

        Returns:
            Returns True if the last non-empty line is a table row
        FrZ   |)rstripr\   stripr]   endswith)r^   trimmed	last_lines      ra   ends_with_table_rowz%MarkdownProcessor.ends_with_table_row   sR     ++-MM$'+113	##C(DY-?-?-DDrc   Nc                &   |xs t         } ||       |k  r| dfS |t         u r| d| }n>dt        |       }}||k  r'||z   dz   dz  } || d|       |k  r|}n|dz
  }||k  r'| d| }|j                  d      }|dkD  r| d|dz    | |dz   d fS t        j                  d      }	d}
|	j	                  |      D ]  }|j                         }
 |
dkD  r
| d|
 | |
d fS |j                  d	      }|dkD  r| d|dz    | |dz   d fS t        |      }| d| | |d fS )
a  
        Find the nearest paragraph boundary split point within max_chars, return (head, tail).

        Split priority:
        1. Blank line (paragraph boundary)
        2. Newline after period/question mark/exclamation mark (Chinese and English)
        3. Last newline
        4. Force split at max_chars

        Args:
            text: Text to split
            max_chars: Maximum character count limit
            len_fn: Optional custom length function (e.g. UTF-16 length); defaults to built-in len

        Returns:
            (head, tail) tuple, head is the front part, tail is the back part, satisfying head + tail == text
         Nr   rV   rG   

u   [。！？.!?]\nre   rZ   )lenrfindrecompilefinditerend)r^   	max_charslen_fn_lenwindowlohimidpossentence_end_rebest_posmcuts                ra   split_at_paragraph_boundaryz-MarkdownProcessor.split_at_paragraph_boundary   s{   . }:"8O
 3;*9%FD	Br'Bw{q(Tc
#y0BqB r' #2YF ll6"7q>4a>11 **%89 ))&1 	AuuwH	a<	?DO33 ll4 7q>4a>11 &kDSz4:%%rc   c                @    | j                         j                  d      S )zDDetermine whether an atomic block is a code block (starts with ```).r[   )lstripr]   r^   s    ra   is_fence_atomzMarkdownProcessor.is_fence_atom9  s     {{}''..rc   c                    | j                  d      d   j                         }|j                  d      xr |j                  d      S )zHDetermine whether an atomic block is a table (first line starts with |).rZ   r   rf   )r\   rh   r]   ri   )r^   
first_lines     ra   is_table_atomzMarkdownProcessor.is_table_atom>  sA     ZZ%a(..0
$$S)Fj.A.A#.FFrc   c                   | j                  d      }g g d}d
d}dfd}|D ]  }|r<j                  |       |j                  d      s(t              dkD  s7d} |        A|j                  d      r |        d}j                  |       m ||      r&r |d         s |        j                  |       |j	                         d	k(  r |        r |d         r |        j                  |         |        S )a  
        Split text into a list of "atomic blocks", each being an indivisible logical unit:

        - Code block (fence): from opening ``` to closing ``` (including fence lines)
        - Table: consecutive |...| lines forming a whole segment
        - Normal paragraph: plain text segments separated by blank lines

        Blank lines serve as separators and are not included in any atomic block.

        Args:
            text: Markdown text to split

        Returns:
            List of atomic block strings (all non-empty)
        rZ   Fc                j    | j                         }|j                  d      xr |j                  d      S )Nrf   )rh   r]   ri   )r`   strippeds     ra   _is_table_linez:MarkdownProcessor.split_into_atoms.<locals>._is_table_line[  s.    zz|H&&s+F0A0A#0FFrc   c                     rCdj                        } | j                         rj                  |        j                          y y )NrZ   )joinrh   appendclear)atomatomscurrent_liness    ra   _flush_currentz:MarkdownProcessor.split_into_atoms.<locals>._flush_current_  s:    yy/::<LL&##%	 rc   r[   rV   Tre   rn   )r`   strreturnboolr   None)r\   r   r]   rp   rh   )r^   linesr_   r   r   r`   r   r   s         @@ra   split_into_atomsz"MarkdownProcessor.split_into_atomsD  s   " 

4 #%	G	&  	+D$$T*??5)c-.@1.D$H"$' $$T*% b8I)J"$$$T*#  ^M"4E%F"$$$T*'	+* 	rc   c                D   |xs t         }|sg S  ||      |k  r|gS | j                  |      }g t               }g d}d	fd}|D ]  }	 ||	      }
rdnd}||z   |
z   }||kD  rr |        g d}d}sS|
|kD  rN| j                  |	      s| j	                  |	      r,|j                  t                     j                  |	       j                  |	       |||
z   z  }  |        g }t              D ]  \  }} ||      |k  r|j                  |       #||v r|j                  |       9| j                  |      r|j                  |       \|} ||      |kD  rB| j                  |||      \  }}|s
|d| ||d }}|r|j                  |        ||      |kD  rB|s|j                  |        t        |      dkD  rA|d   g}|dd D ]1  }|d   }|dz   |z   } ||      |k  r||d<   !|j                  |       3 |}|D cg c]  }|s|	 c}S c c}w )
a  
        Split Markdown text into multiple chunks by max_chars.

        Guarantees:
        - Each chunk <= max_chars characters (unless a single code block/table itself exceeds the limit)
        - Code blocks (```...```) are not split in the middle
        - Table rows are not split in the middle (tables output as atomic blocks)
        - Split at paragraph boundaries (blank lines, after periods, etc.)
        - Small trailing/leading chunks are merged with neighbours when possible

        Args:
            text: Markdown text to split
            max_chars: Max characters per chunk, default 4000
            len_fn: Optional custom length function (e.g. UTF-16 length); defaults to built-in len

        Returns:
            List of text chunks after splitting (non-empty)
        r   c                 L    r! j                  dj                               y y )Nro   )r   r   )chunkscurrent_partss   ra   _flush_partsz;MarkdownProcessor.chunk_markdown_text.<locals>._flush_parts  s     fkk-89 rc   rG   rw   NrV   re   ro   r   )
rp   r   setr   r   addr   	enumeraterb   r   )clsr^   rv   rw   rx   r   indivisible_setcurrent_lenr   r   atom_lensep_lenprojected_lenresultidxchunk	remainingheadmergedprevcombinedcr   r   s                         @@ra   chunk_markdown_textz%MarkdownProcessor.chunk_markdown_text  s   2 }I:"6M $$T* $'E#%	:  	.DDzH(aaG''1H<My(] "! 9,**40C4E4Ed4K##CK0d#  &7X--K'	.* 	 #F+ 	)JCE{i'e$o%e$%%e,e$Iy/I-"%"A"Ay #B #i &/
&;Yyz=R)DMM$' y/I- i(/	)4 v;?!'F )bz&=50>Y.!)F2JMM%() F!'aQ'''s   HHc                >   |j                         }|j                         }|j                  d      s|j                  d      ry| j	                  |      rI|r"|j                  d      d   j                         nd}|j                  d      r|j                  d      ryy)u'  
        Infer the separator to use between two split chunks.

        Rules (aligned with TS markdown-stream.ts):
        - Previous chunk ends with code fence or next chunk starts with fence → single newline '\n'
        - Previous chunk ends with table row and next chunk starts with table row → single newline '\n' (continued table)
        - Otherwise → double newline '\n\n' (paragraph separator)

        Args:
            prev_chunk: Previous chunk
            next_chunk: Next chunk

        Returns:
            '\n' or '\n\n'
        r[   rZ   r   rn   rf   ro   )rg   r   ri   r]   rl   r\   rh   )r   
prev_chunk
next_chunkprev_trimmednext_trimmedr   s         ra   infer_block_separatorz'MarkdownProcessor.infer_block_separator  s    " "((*!((*   '<+B+B5+I "":.@L++D1!4::<RTJ$$S)j.A.A#.Frc   c                f   |sg S g }d}|t        |      k  r||   }| j                  |      r_|dz   t        |      k  rN| j                  |||dz            }||z   ||dz      z   }|dz  }| j                  |      r|dz   t        |      k  rN|j                  |       |dz  }|t        |      k  r|S )aM  
        Stream-aware fence-conscious chunk merging.

        When streaming output produces multiple chunks truncated in the middle of a fence,
        attempt to merge adjacent chunks to complete the fence.

        Rules:
        - If chunk i has an unclosed fence and chunk i+1 starts with ```,
            merge i+1 into i (until the fence is closed or no more chunks).
        - Use infer_block_separator to infer the separator during merging.

        Args:
            chunks: Original chunk list

        Returns:
            Merged chunk list (length <= original length)
        r   rV   )rp   rb   r   r   )r   r   r   icurrentseps         ra   merge_block_streaming_fencesz.MarkdownProcessor.merge_block_streaming_fences  s    & I#f+oQiG((1a!ec&k6I//AG!C-&Q-7Q ((1a!ec&k6I MM'"FA #f+o rc   c                    | s| S | j                  d      }t        |      dk  r| S |d   j                         }|d   j                         }t        j                  d|t        j
                        s| S |dk7  r| S dj                  |dd       }|S )a  
        Strip outer Markdown fence.

        When AI reply is entirely wrapped in ```markdown\n...\n```, remove the outer fence,
        keeping the content. Only strip when the first line is ```markdown (case-insensitive) and the last line is ```.

        Args:
            text: Text to process

        Returns:
            Text with outer fence stripped (returns original if no match)
        rZ      r   re   z^```(?:markdown|md)?\s*$r[   rV   )r\   rp   rh   rr   match
IGNORECASEr   )r^   r   r   rk   inners        ra   strip_outer_markdown_fencez,MarkdownProcessor.strip_outer_markdown_fence9  s     K

4 u:>K1X^^%
"IOO%	 xx3ZOK K 		%"+&rc   c                   d| vr| S | j                  d      }g }|D ]  }|j                         }|j                  d      r|j                  d      rt	        j
                  d|      r;|j                  d      }dj                  d |D              }|j                  |       |dk(  s#|j                  dd      j                         dk(  r|j                  |       |j                  |        dj                  |      S )a  
        Table output sanitization.

        Handle common formatting issues in AI-generated Markdown tables:
        1. Remove extra whitespace before/after table rows
        2. Ensure separator rows (|---|---|) are correctly formatted
        3. Remove empty table rows

        Args:
            text: Markdown text containing tables

        Returns:
            Sanitized text
        rf   rZ   z^\|[\s\-:]+(\|[\s\-:]+)+\|$c              3  `   K   | ]&  }|j                         r|j                         n| ( y wN)rh   ).0cells     ra   	<genexpr>z<MarkdownProcessor.sanitize_markdown_table.<locals>.<genexpr>}  s+      *  )-



$>*s   ,.z||rn   )	r\   rh   r]   ri   rr   r   r   r   replace)r^   r   result_linesr`   r   cells
normalizeds          ra   sanitize_markdown_tablez)MarkdownProcessor.sanitize_markdown_table_  s      d?K

4 "$ 	*Dzz|H ""3'H,=,=c,B88:HE$NN3/E!$ *$)* "J !''
3%)9)9#r)B)H)H)Jb)P ''1##D)'	** yy&&rc   c                      	 y)z
        Markdown rendering hint (appended to system prompt).

        Tell AI that Yuanbao platform supports Markdown rendering, including:
        - Code blocks (```lang)
        - Tables (| col | col |)
        - Bold/italic
        a  The current platform supports Markdown rendering. You can use the following formats:
- Code blocks: ```language\ncode\n```
- Tables: | col1 | col2 |\n|---|---|\n| val1 | val2 |
- Bold: **text** / Italic: *text*
Please use Markdown formatting when appropriate to improve readability. r   rc   ra   markdown_hint_system_promptz-MarkdownProcessor.markdown_hint_system_prompt  s    V	
rc   )r^   r   r   r   r   )r^   r   rv   intrw   Optional[Callable[[str], int]]r   ztuple[str, str])r^   r   r   	list[str]  N)r^   r   rv   r   rw   r   r   r   )r   r   r   r   r   r   )r   r   r   r   r^   r   r   r   r   r   )__name__
__module____qualname____doc__staticmethodrb   rl   r   r   r   r   classmethodr   r   r   r   r   r   r   rc   ra   rX   rX      s_   	  * E E$  26=&=&=& /=& 
	=& =&B / / G G
 8 8x  15	k(k( k( /	k(
 
k( k(^  B ! !J ! !J *' *'\ 
 
rc   rX   c                  *   e Zd ZU dZdZdZdZdZdZdZ	i Z
ded	<   i Zd
ed<   edd       Zedd       Zedd       Zedd       Zedd       Zedd       Ze	 d	 	 	 	 	 	 	 	 	 dd       Ze	 d	 	 	 	 	 	 	 	 	 dd       Ze	 d	 	 	 	 	 	 	 	 	 dd       Zy)SignManagera  Encapsulates all sign-token related logic for the Yuanbao platform.

    Manages token acquisition, caching, signature computation, and
    automatic retry.  All state (cache, locks) is kept as class-level
    attributes so that a single shared client serves the whole process.
    z/api/v5/robotLogic/sign-tokenis'  r   r@   <   r>   zdict[str, dict[str, Any]]_cachezdict[str, asyncio.Lock]_locksc                ~    || j                   vr!t        j                         | j                   |<   | j                   |   S )zReturn (creating if needed) the per-app_key refresh lock.

        Must only be called from within a running event loop (async context).
        )r   asyncioLock)r   app_keys     ra   get_refresh_lockzSignManager.get_refresh_lock  s3     #**$"),,.CJJwzz'""rc   c                    | |z   |z   |z   }t        j                  |j                         |j                         t        j                        j                         S )zCompute HMAC-SHA256 signature (aligned with TypeScript original).

        plain     = nonce + timestamp + app_key + app_secret
        signature = HMAC-SHA256(key=app_secret, msg=plain).hexdigest()
        )hmacnewencodehashlibsha256	hexdigest)nonce	timestampr   
app_secretplains        ra   compute_signaturezSignManager.compute_signature  sG     	!G+j8xx
))+U\\^W^^LVVXXrc   c                 v    t        j                  t        t        d                  } | j	                  d      S )zlBuild Beijing-time ISO-8601 timestamp (no milliseconds).

        Format: 2006-01-02T15:04:05+08:00
           )hourstzz%Y-%m-%dT%H:%M:%S+08:00)r   nowr   r   strftime)bjtimes    ra   build_timestampzSignManager.build_timestamp  s,     )!*<!=>899rc   c                P    |d   t        j                          z
  | j                  kD  S )zEDetermine whether the cache entry is valid (not expired with margin).	expire_ts)timeCACHE_REFRESH_MARGIN_S)r   entrys     ra   is_cache_validzSignManager.is_cache_valid  s%     [!DIIK/#2L2LLLrc   c                8    | j                   j                          y)z;Clear all per-app_key refresh locks (called on disconnect).N)r   r   r   s    ra   clear_lockszSignManager.clear_locks  s     	

rc   c           	        t        j                          }| j                  j                         D cg c]  \  }}||j                  dd      z
  dkD  r|! }}}|D ]  }| j                  j	                  |d         t        |      S c c}}w )zRemove all expired entries from the token cache.

        Returns the number of entries purged.  Called lazily from
        ``get_token()`` so that stale app_key entries don't accumulate
        indefinitely in long-running processes.
        r  r   N)r  r   itemsgetpoprp   )r   r   kvexpired_keyss        ra   purge_expiredzSignManager.purge_expired  s     iik****,
!QQUU;**Q. 
 
  	$AJJNN1d#	$<  
s   $Bc                  K   |j                  d       | j                   }t        j                  | j                        4 d{   }t        | j                  dz         D ]  }t        j                  d      }| j                         }	| j                  ||	||      }
|||
|	d}dt        t        t        t        d}|r||d	<   t        j!                  d
||dkD  rd| d| j                   dnd       |j#                  |||       d{   }|j$                  dk7  r*|j&                  }t)        d|j$                   d|dd        	 |j+                         }|j1                  d      }|dk(  ri|j1                  d      }t3        |t4              st/        d|       t        j!                  d|j1                  d             |c cddd      d{    S || j6                  k(  rh|| j                  k  rYt        j9                  d|| j:                  |dz   | j                         t=        j>                  | j:                         d{    |j1                  dd      }t)        d| d|        ddd      d{    t)        d      7 J7 # t,        $ r}t/        d|       |d}~ww xY w7 7 m7 9# 1 d{  7  sw Y   t)        d      xY ww)zHSend sign-ticket HTTP request with auto-retry (up to MAX_RETRIES times)./timeoutNrV      )r   r   	signaturer   application/json)Content-TypezX-AppVersionzX-OperationSystemzX-Instance-IdzX-Bot-VersionzX-Route-EnvzSign token request: url=%s%sr   z (retry )rn   )jsonheaders   zSign token API returned : z!Sign token response parse error: codedataz*Sign token response missing 'data' field: zSign token success: bot_id=%sbot_idz>Sign token retryable: code=%s, retrying in %ss (attempt=%d/%d)msgzSign token error: code=, msg=z'Sign token failed: max retries exceeded) rg   
TOKEN_PATHhttpxAsyncClientHTTP_TIMEOUT_SrangeMAX_RETRIESsecrets	token_hexr   r   _APP_VERSION_OPERATION_SYSTEM_YUANBAO_INSTANCE_ID_BOT_VERSIONloggerinfopoststatus_coder^   RuntimeErrorr  	Exception
ValueErrorr  
isinstancedictRETRYABLE_CODEwarningRETRY_DELAY_Sr   sleep)r   r   r   
api_domain	route_envurlclientattemptr   r   r  payloadr  responsebodyresult_dataexcr  r   r"  s                       ra   fetchzSignManager.fetch  s     ""3'((89$$S-?-?@ <	P <	PF 1!45 ;P))"-//1	11%GZX	  '"!*!*	 %7$0):%9%1 -6GM*2?F{hwiq(9;PR "(Sw!PP''3.#==D&)A(BVBVAWWYZ^_c`cZdYe'fggY2:--/K #v.19&??62D%dD1(+UVaUb)cddKK ?(ASTK]<	P <	P <	P` 3---'COO2KNNX))! "--(9(9:::!ooeR0"%<TF&#NOOw;P<	P <	P| DEE}<	P: Q ! Y$'H%NOUXXYK<	Pp ;q<	P <	P <	P <	P| DEEs   AKJK
CJ<J=J<JA-J<KJ6KA0J<
J8*J<5K J:KJ<	J3J..J33J<6K8J<:K<KKK
Kc           	       K   | j                          | j                  j                  |      }|rU| j                  |      rDt	        |d   t        j
                         z
        }t        j                  d|       t        |      S | j                  |      4 d{    | j                  j                  |      }|r-| j                  |      rt        |      cddd      d{    S | j                  ||||       d{   }|j                  dd      }|dkD  rt        j
                         |z   nt        j
                         dz   }	|j                  dd      |j                  d	d      ||j                  d
d      |j                  dd      |	d| j                  |<   ddd      d{    t        | j                  |         S 7 .7 7 7 ## 1 d{  7  sw Y   3xY ww)zGet WS auth token (with cache).

        Return directly on cache hit without re-requesting; treat as expiring
        60 seconds before actual expiry, triggering refresh.
        r  z"Using cached token (%ds remaining)Ndurationr     tokenrn   r!  productsourcerK  r!  rI  rL  rM  r  )r  r   r  r  r   r  r0  r1  r8  r   rG  )
r   r   r   r=  r>  cachedremainr   rI  r  s
             ra   	get_tokenzSignManager.get_tokenI  s     	(c((0,tyy{:;FKK<fE<''0 	 	ZZ^^G,F#,,V4F|	 	 	
 7J
INND HHZ3H2:Q,		h.DIIKRVDVI '2.((8R0$88Ir2((8R0&#CJJw	 	& CJJw'(('	 	
 O	 	 	 	sy   BG&GG&9GG&#G$G&)G GBG G&+G,G&G&GG&G#GG#G&c           	       K   t         j                  d|dd        | j                  |      4 d{    | j                  j	                  |d       | j                  ||||       d{   }|j                  dd      }|dkD  rt        j                         |z   nt        j                         dz   }|j                  dd      |j                  d	d      ||j                  d
d      |j                  dd      |d| j                  |<   ddd      d{    t        | j                  |         S 7 7 7  # 1 d{  7  sw Y   0xY ww)z.Force refresh token (clear cache and re-sign).zC[force-refresh] Clearing cache and re-signing token: app_key=****%sNrI  r   rJ  rK  rn   r!  rL  rM  rN  )	r0  r:  r   r   r  rG  r  r  r8  )r   r   r   r=  r>  r   rI  r  s           ra   force_refreshzSignManager.force_refreshv  s%     	\^efhfi^jk''0 	 	JJNN7D)7J
INND HHZ3H2:Q,		h.DIIKRVDVI '2.((8R0$88Ir2((8R0&#CJJw	 	  CJJw'((!	N	 	 	 	sW   /ED1E4D7)D3*BD7	ED5E3D75E7E	=E >E	EN)r   r   r   asyncio.Lock)
r   r   r   r   r   r   r   r   r   r   r   )r  dict[str, Any]r   r   r   r   r   rn   )
r   r   r   r   r=  r   r>  r   r   rV  )r   r   r   r   r$  r9  r)  r;  r  r'  r   __annotations__r   r   r   r   r   r   r  r	  r  rG  rQ  rT  r   rc   ra   r   r     s    1JNKM   N
 )+F%* ')F#( # # Y Y : : M M   ! !$  GFGF GF 	GF
 GF 
GF GFV  ()() () 	()
 () 
() ()X  )) ) 	)
 ) 
) )rc   r   )	dataclassfieldc                     e Zd ZU dZded<    ee      Zded<   dZded	<   d
Z	ded<   d
Z
ded<   d
Zded<   d
Zded<   d
Zded<    ee      Zded<   d
Zded<   d
Zded<   d
Zded<   d
Zded<   d
Zded<   d
Zded<    ee      Zded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded <    ee      Zded!<    ee      Zded"<    ee      Zded#<    ee      Zded$<   dZ ded%<   y)&InboundContextzMutable context flowing through the inbound middleware pipeline.

    Each middleware reads/writes fields on this context.  The pipeline
    engine passes it to every middleware in registration order.
    r   adapter)default_factorylist
raw_framesNOptional[dict]pushrn   r   decoded_viafrom_account
group_code
group_namesender_nicknamemsg_bodymsg_idcloud_custom_datachat_id	chat_type	chat_nameraw_text
media_refsforwarded_recordsOptional[str]owner_commandzOptional[Any]rM  msg_typereply_to_message_idreply_to_textquote_media_refs
media_urlsmedia_types	link_urlschannel_prompt)!r   r   r   r   rY  dc_fieldr`  ra  rc  rd  re  rf  rg  rh  ri  rj  rk  rl  rm  rn  ro  rp  rq  rs  rM  rt  ru  rv  rw  rx  ry  rz  r{  r   rc   ra   r]  r]    sQ    L5J5  D.K L#JJOSd3Hd3FCs GSIsIs Hc5J5 )-~, $(M=' !FM  #Hm" *.-#'M='%d;d;  5J5 6K6 t4It4 %)NM(rc   r]  c                  B    e Zd ZU dZdZded<   ed	d       Zd	dZd
dZ	y)InboundMiddlewarea  Abstract base class for all inbound pipeline middlewares.

    Subclasses must:
      - Set ``name`` as a class-level attribute (used for pipeline registration
        and dynamic insertion/removal).
      - Implement ``async handle(ctx, next_fn)`` containing the middleware logic.

    Convention:
      - Call ``await next_fn()`` to pass control to the next middleware.
      - Return without calling ``next_fn`` to **stop** the pipeline.
    rn   r   namec                   K   yw)zEProcess *ctx* and optionally call *next_fn* to continue the pipeline.Nr   selfctxnext_fns      ra   handlezInboundMiddleware.handle          c                B   K   | j                  ||       d{   S 7 w)zFAllow middleware instances to be called directly (duck-typing compat).N)r  r  s      ra   __call__zInboundMiddleware.__call__  s     [[g....s   c                P    d| j                   j                   d| j                  dS )N<z name=>)	__class__r   r  r  s    ra   __repr__zInboundMiddleware.__repr__  s&    4>>**+6$))a@@rc   N)r  r]  r  r   r   r   r   )
r   r   r   r   r  rY  r
   r  r  r  r   rc   ra   r~  r~    s1    
 D#NT T/Arc   r~  c                  j    e Zd ZdZddZedd       ZdddZdddZdddZ	ddZ
edd	       Zdd
Zy)InboundPipelinea  Onion-model middleware pipeline engine for inbound message processing.

    Inspired by OpenClaw's MessagePipeline (extensions/yuanbao/src/business/
    pipeline/engine.ts).  Supports named middlewares, conditional guards
    (``when``), and ``use_before`` / ``use_after`` / ``remove`` for dynamic
    composition.

    Accepts both ``InboundMiddleware`` instances (OOP style) and plain
    ``async def(ctx, next_fn)`` callables (functional style) for flexibility.
    c                    g | _         y r   _middlewaresr  s    ra   __init__zInboundPipeline.__init__  s
    "$rc   Nc                F    t        | t              r| j                  | fS | |fS )zHNormalize (name, handler) or (InboundMiddleware,) into (name, callable).)r7  r~  r  )
name_or_mwhandlers     ra   
_normalizezInboundPipeline._normalize  s)     j"34??J..7""rc   c                l    | j                  ||      \  }}| j                  j                  |||f       | S )u   Append a middleware to the end of the pipeline.

        Accepts either:
          - ``pipeline.use(SomeMiddleware())``  — OOP style
          - ``pipeline.use("name", some_fn)``   — functional style
        )r  r  r   )r  r  r  whenr  hs         ra   usezInboundPipeline.use  s7     //*g6a  $41rc   c                   | j                  ||      \  }}t        fdt        | j                        D        d      }|||f}|| j                  j	                  |       | S | j                  j                  ||       | S )zEInsert a middleware before *target* (by name).  Appends if not found.c              3  <   K   | ]  \  }\  }}}|k(  s|  y wr   r   r   r   n_targets       ra   r   z-InboundPipeline.use_before.<locals>.<genexpr>  !     V,!YaA!v+AV   Nr  nextr   r  r   insert	r  r  r  r  r  r  r  r   r  s	    `       ra   
use_beforezInboundPipeline.use_before  s}    //*g6aV)D4E4E*FVX\]q$;$$U+  $$S%0rc   c                   | j                  ||      \  }}t        fdt        | j                        D        d      }|||f}|| j                  j	                  |       | S | j                  j                  |dz   |       | S )zDInsert a middleware after *target* (by name).  Appends if not found.c              3  <   K   | ]  \  }\  }}}|k(  s|  y wr   r   r  s       ra   r   z,InboundPipeline.use_after.<locals>.<genexpr>&  r  r  NrV   r  r  s	    `       ra   	use_afterzInboundPipeline.use_after#  s    //*g6aV)D4E4E*FVX\]q$;$$U+  $$S1We4rc   c                t    | j                   D cg c]  \  }}}||k7  s|||f c}}}| _         | S c c}}}w )zRemove a middleware by name.r  )r  r  r  r  ws        ra   removezInboundPipeline.remove.  s<    6:6G6GUU71a1PT9aAYU Vs   33c                N    | j                   D cg c]  \  }}}|
 c}}S c c}}w )zAReturn ordered list of registered middleware names (for testing).r  )r  r  r  s      ra   middleware_namesz InboundPipeline.middleware_names3  s$     "&!2!23gaA333s   !c                d   K   | j                   ddfd         d{    y7 w)zKRun all middlewares in order.  Each middleware receives ``(ctx, next_fn)``.r   c                    K   t              k  r,   \  } }}dz  |	 |      s'	  |       d {    y y 7 # t        $ r t        j                  d| d        w xY ww)NrV   z'[InboundPipeline] middleware [%s] errorTexc_info)rp   r5  r0  error)r  r  when_fnchainr  indexr  s      ra   r  z(InboundPipeline.execute.<locals>.next_fn?  s     #e*$).u&gw
&ws|!#w///  % 0  LL!JD[_L`s)   (A'A ?A A'A #A$$A'Nr   r  )r  r  r  r  r  s    `@@@ra   executezInboundPipeline.execute:  s,     !!	 	  is   "0.0r   r   NN)r   'InboundPipeline')r  r   r   r  )r  r   r   r  )r   r`  r  r]  r   r   )r   r   r   r   r  r   r  r  r  r  r  propertyr  r  r   rc   ra   r  r    sM    	%
 # #			
 4 4rc   r  c                  H    e Zd ZdZdZedd       Zed	d       Zd
dZddZ	y)DecodeMiddlewarezDecode raw inbound frames from JSON or Protobuf into ctx.push.

    Encapsulates JSON push parsing (aligned with TS decodeFromContent)
    and Protobuf decoding via ``decode_inbound_push``.
    decodec                x   g }| xs g D ]  }t        |t              s|j                  d      xs |j                  dd      }|j                  d      xs |j                  di       }t        |t              r	 t	        j
                  |      }|j                  ||xs i d        |S # t        $ r d|i}Y +w xY w)zNormalize raw JSON msg_body array to [{"msg_type": str, "msg_content": dict}].

        Compatible with both PascalCase (MsgType/MsgContent) and
        snake_case (msg_type/msg_content) naming.
        rt  MsgTypern   msg_content
MsgContentr^   rt  r  )r7  r8  r  r   r  loadsr5  r   )raw_bodyr   itemrt  r  s        ra   convert_json_msg_bodyz&DecodeMiddleware.convert_json_msg_body[  s     N 
	TDdD)xx
+Ftxx	2/FH((=1OTXXlB5OK+s+8"&**["9K MMx@QrRS
	T  ! 8#);"7K8s   8B))B98B9c                T   | sy| j                  dd      xs | j                  dd      }| j                  dd      xs& | j                  dd      xs | j                  dd      }| j                  dg       xs | j                  d	g       }t        j                  |      }|s|s| j                  d
      sy| j                  d
d      || j                  dd      xs | j                  dd      | j                  dd      xs | j                  dd      || j                  dd      | j                  dd      xs | j                  dd      | j                  dd      xs& | j                  dd      xs | j                  dd      || j                  dd      xs | j                  dd      | j                  dd      xs | j                  dd      | j                  d      xs dt        | j                  d      t              r'| j                  d      xs i j                  dd      dS ddS )a  Convert JSON-format push to a dict with the same structure as
        ``decode_inbound_push``.

        Supports standard callback format (callback_command + from_account +
        msg_body) and legacy format fields (GroupId, MsgSeq, MsgKey, MsgBody,
        etc.).
        Nre  rn   From_Accountrf  GroupIdgroup_idri  MsgBodycallback_command
to_account
To_Accountrh  	nick_namerg  msg_seqr   MsgSeqrj  msg_keyMsgKeyrk  CloudCustomDatabot_owner_id
botOwnerIdrecall_msg_seq_listlog_exttrace_id)r  re  r  rh  rf  rg  r  rj  ri  rk  r  r  r  )r  r  r  r7  r8  )raw_jsonre  rf  msg_body_rawri  s        ra   parse_json_pushz DecodeMiddleware.parse_json_pushp  s     LL, 0||NB/ 	
 LLr* ,||Ir*,||J+ 	 LLR( +||Ir* 	 $99,G HX\\BT5U !)-? D(",,|R8ZHLLWY<Z'||,=rBchllS^`bFc$",,|R8||Iq1NX\\(A5Nll8R0mHLLB4OmS[S_S_`hjlSm !).A2!F!m(,,WhjlJm$LL<^\[]@^#+<<0E#F#N$OYZbZfZfgpZqswOxi06B;;JK
 	
 A
 	
rc   c                   	 t        j                  |j                  d            }t	        |t
              r| j                  |      }|r|dfS y	 t        |      }|r|dfS y# t        $ r d}Y Hw xY w# t        $ r d}Y %w xY w)zFDecode a single raw frame into (push_dict, decoded_via) or (None, '').utf-8Nr  protobufNrn   )r  r  r  r5  r7  r8  r  r-   )r  r^  r   	conn_jsonrc  s        ra   _decode_singlezDecodeMiddleware._decode_single  s    	

4;;w#78I i&''	2DV|# *40 Z''  	I	  s#   $A! A2 !A/.A/2B ?B c                  K   |j                   }|sy d }d}|D ]  }| j                  |j                  |      \  }}|sAt        j	                  d|j                  j
                  |r|j                         d d nd       f|:|}|}t        j	                  d|j                  j
                  |t        |             |j                  dg       }	|	sddd	id
}
|j                  dg       |
gz   |	z   |d<   t        j	                  d|j                  j
                  t        |	              |sy ||_	        ||_
        t        j	                  d|j                  j
                  |j                  |j                  j                  dd      |j                  j                  dd      |j                  j                  dd      |j                  j                  dg       D cg c]  }|j                  dd       c}       t        j                  d|j                  j
                  |j                          |        d {    y c c}w 7 
w)Nrn   z;[%s] Push decoded but no valid message. raw hex(first64)=%s   z(empty)z#[%s] Frame decoded (via=%s): len=%dri  TIMTextElemr^   rZ   r  z;[%s] Merged %d extra msg_body elements from aggregated pushzC[%s] Push decoded (via=%s): from=%s group=%s msg_id=%s msg_types=%sre  rf  rj  rt  z[%s] Push payload: %s)ra  r  r^  r0  r1  r  hexrp   r  rc  rd  debug)r  r  r  	data_listmerged_pushrd  r   rc  via
extra_body_sepes               ra   r  zDecodeMiddleware.handle  s    NN	 	D++CKK>ID#MKK$$$dhhj#&6I ""!5KK$$c3t9 "XXj"5
(5vtnUD.9ooj".MQUPV.VYc.cK
+KKU((#j//	8 %QKKcooHHLL,HHLLr*HHLL2&,/HHLLR,HIqQUU:r"I	
 	,ckk.>.>Ii	 J 	s&   CID
IH>2AI8I9IN)r  r`  r   r`  )r  r8  r   zdict | None)r   bytesr   tupler  )
r   r   r   r   r  r   r  r  r  r  r   rc   ra   r  r  P  sA     D  ( /
 /
f*4rc   r  c                      e Zd ZdZdZddZy)ExtractFieldsMiddlewarez8Extract common fields from ctx.push into ctx attributes.zextract-fieldsc                  K   |j                   }|j                  dd      |_        |j                  dd      |_        |j                  dd      |_        |j                  dd      |_        |j                  dg       |_        |j                  dd      |_        |j                  dd      |_         |        d {    y 7 w)	Nre  rn   rf  rg  rh  ri  rj  rk  )	rc  r  re  rf  rg  rh  ri  rj  rk  )r  r  r  rc  s       ra   r  zExtractFieldsMiddleware.handle  s     xx88NB7,3,3"hh'8"=xx
B/XXh+
 $)<b Ais   B8C:C ;CNr  r   r   r   r   r  r  r   rc   ra   r  r    s    BD	rc   r  c                      e Zd ZdZdZddZy)DedupMiddlewarezInbound message deduplication.dedupc                  K   |j                   re|j                  j                  j                  |j                         r6t        j                  d|j                  j                  |j                          y  |        d {    y 7 w)Nz)[%s] Duplicate message ignored: msg_id=%s)rj  r^  _dedupis_duplicater0  r  r  r  s      ra   r  zDedupMiddleware.handle  sV     ::#++,,99#**ELLDckkFVFVX[XbXbcis   A<B>B?BNr  r  r   rc   ra   r  r    s    (Drc   r  c                      e Zd ZdZdZ eddh      ZdZddZe	dd       Z
ddZe	dd	       Ze	 	 	 	 	 	 dd
       Ze	 	 	 	 	 	 dd       Ze	 d	 	 	 	 	 dd       Zy)RecallGuardMiddlewareu5  Intercept Group.CallbackAfterRecallMsg / C2C.CallbackAfterMsgWithDraw.

    Branch A: message in transcript (observed, not yet consumed) → redact content
    Branch B: message not in transcript → append system note
    Branch C: message currently being processed → silent interrupt + delayed redact
    recall_guardGroup.CallbackAfterRecallMsgzC2C.CallbackAfterMsgWithDrawzM[This message was recalled/withdrawn by the sender; original content removed]c                   K   |j                   xs i j                  dd      }|| j                  vr |        d {    y | j                  ||       y 7 w)Nr  rn   )rc  r  _RECALL_COMMANDS_handle_recall)r  r  r  cmds       ra   r  zRecallGuardMiddleware.handle  sP     xx~2""#5r:d+++)OOC% s   9AAAc                b    | j                  |rd| nd| |rdnd|xs d |rd      S d       S )Ngroup:direct:groupdmmain)rl  rm  user_id	thread_id)build_source)r^  rf  re  s      ra   _build_sourcez#RecallGuardMiddleware._build_source$  sW    ##.8vj\*~>V!+g (D *f	 $ 
 	
 15	 $ 
 	
rc   c                   |j                   }|j                  xs i }|dk(  r|j                  d      xs g }n2|j                  d      xs d}|j                  d      }|s|r||dgng }|s!t        j	                  d|j
                         y |j                  d      xs dj                         }|j                  d	      xs dj                         }	|D ]  }
|
j                  d      xs t        |
j                  d      xs d      }|s7| j                  ||      }|| j                  |||||	       a|j                  j                  |      }| j                  ||||	|        y )
Nr  r  rj  rn   r  )rj  r  z2[%s] Recall callback with empty seq_list, skippingrf  re  )r^  rc  r  r0  r  r  rh   r   _find_processing_session_interrupt_for_recall_msg_content_cache_patch_transcript)r  r  r  r^  rc  seq_listr|   seqrf  re  	seq_entryrecalled_id
matched_skrecalled_contents                 ra   r  z$RecallGuardMiddleware._handle_recall-  sU   ++xx~200xx 56<"H((8$*C((9%C=@C3378bHLLMw||\hh|,299;
06B==?! 
	iI#--1XSy9Q9WUW5XK66wLJ%**7JZYef#*#=#=#A#A+#N &&wZWgh
	irc   c                x    | j                   j                         D ]  \  }}||k(  s|| j                  v s|c S  y r   )_processing_msg_idsr  _active_sessions)r^  r  skr|   s       ra   r  z.RecallGuardMiddleware._find_processing_sessionM  sC    2288: 	GBk!bG,D,D&D		 rc   c           	        |rd| nd| }d| d| d}t        |t        j                  | j                  |||      d      }||j                  |<   |j
                  j                  |      }	|	|	j                          t        j                  d|j                  ||d d	        |j                  j                  |d
      }
|
r| j                  |||
||       y y )Nzgroup zdirect chat with u_   [CRITICAL — MESSAGE RECALLED] The user message that triggered your current task (message_id="z") in ud   has been recalled/withdrawn by the sender. IGNORE any prior system note asking you to finish processing tool results — the original request is void. Do NOT continue the task, do NOT call more tools, do NOT reference the recalled content. Reply only with a brief acknowledgment such as "The message has been recalled." in the language the user was using.T)r^   message_typerM  internalz+[%s] Recall interrupt: msg_id=%s session=%s   rn   )r   r   TEXTr  _pending_messagesr  r  r   r0  r1  r  _processing_msg_texts_schedule_content_redact)r   r^  session_keyr  rf  re  whererecall_textsynth_eventactive_eventrecalled_texts              ra   r  z+RecallGuardMiddleware._interrupt_for_recallT  s     *4&%;L\N9[//:m75' J+	, 	 #$))$$Wj,G	
 2=!!+.//33K@#A7<<Q\^ijmkm^no  5599+rJ((+}jZfg rc   c                     d fd}t        j                   |             }j                  j                  |       |j	                  j                  j
                         y )Nc            	       K   t        dd       } | sy 	 | j                  j                  	            j                  }t        d      D ]  }t        j                  d       d {    	 | j                  |      }|D ]t  }|j                  d      dk(  s|j                  d      
k(  s-j                  |d<   	 | j                  ||       t        j                  dj                  d d          y   t        j!                  d	j                  d d        y # t        $ r Y y w xY w7 # t        $ r Y w xY w# t        $ r-}t        j                  dj                  |       Y d }~  y d }~ww xY ww)
N_session_storer#  g      ?roleusercontentz[%s] Recall redact: session %sz[%s] Recall redact failed: %sz?[%s] Recall redact: content not found after polling, session %s)getattrget_or_create_sessionr  
session_idr5  r(  r   r<  load_transcriptr  	_REDACTEDrewrite_transcriptr0  r1  r  r:  r  )storesidr  
transcriptr  rF  r^  r   re  rf  r-  r(  s         ra   _redactz?RecallGuardMiddleware._schedule_content_redact.<locals>._redact}  s    G%5t<E11%%gz<H*  2Y mmC(((!&!6!6s!;J ( Eyy(F2uyy7K}7\+.==i(_!44S*E"KK(H',,XcdgegXhi  LLZ\c\h\hjuvywyjz{'  
 ) !   ) _"NN+JGLLZ]^^_s   E7,D &E7'D-(E7-D/>E7E7-E7=6D>3+E7	D*'E7)D**E7/	D;8E7:D;;E7>	E4!E/(E7/E44E7r   )r   create_task_background_tasksr   add_done_callbackdiscard)r   r^  r(  r-  rf  re  r=  tasks   ``````  ra   r'  z.RecallGuardMiddleware._schedule_content_redactz  sQ    	| 	|: ""79-!!%%d+w88@@Arc   Nc                   t        |dd       }|sy 	 |j                  | j                  |||            j                  }	 |j                  |      }	d }
d}|	D ]  }|j                  d      |k(  s|}
d} n |
8|r6|	D ]1  }|j                  d      dk(  s|j                  d	      |k(  s-|}
d
} n |
E| j                  |
d	<   	 |j                  ||	       t
        j                  d|j                  ||       y |j                  |dd| dt        j                  t         j"                        j%                         d       t
        j                  d|j                  |       y # t        $ r+}t
        j                  d|j                  |       Y d }~y d }~ww xY w# t        $ r+}t
        j                  d|j                  |       Y d }~y d }~ww xY w# t        $ r+}t
        j                  d|j                  |       Y d }~y d }~ww xY w)Nr0  z*[%s] Recall: failed to resolve session: %sz*[%s] Recall: failed to load transcript: %srn   
message_idzbranch A1: id matchr1  r2  r3  zbranch A2: content matchz$[%s] Recall: redacted msg_id=%s (%s)z*[%s] Recall: rewrite_transcript failed: %ssystemz[recall] message_id="z2" has been recalled; do not quote or reference it.r   )r1  r3  r   z1[%s] Recall: system note for msg_id=%s (branch B))r4  r5  r  r6  r5  r0  r:  r  r7  r  r8  r9  r1  append_to_transcriptr   r   r   utc	isoformat)r   r^  r  rf  re  r  r:  r;  rF  r<  r  branch_labelr  s                ra   r  z'RecallGuardMiddleware._patch_transcript  s    !148	--c.?.?Ua.bcnnC	..s3J  	Eyy&+54		 >.# 99V$.599Y3GK[3["F#=L	
  #F9`((j9BGLLR]_kl  	""3.{m;mn!6@@B)
 	
 	GWbcg  	NNGWZ[	  	NNGWZ[	:  `KW\\[^__`sF   ,E" F 4G "	F+!FF	G"!GG	H!G??Hr  )rf  r   re  r   )r  r]  r  r   r   r   )r  r   r   rr  )
r(  r   r  r   rf  r   re  r   r   r   )
r(  r   r-  r   rf  r   re  r   r   r   r   )
r  r   rf  r   re  r   r  rr  r   r   )r   r   r   r   r  	frozensetr  r8  r  r   r  r  r  r   r  r'  r  r   rc   ra   r   r     s     D &&"  `I& 
 
i@   #h*-#h=@#hEI#h #hJ !B-0!B@C!BHL!B !BJ OS:d(+:d?L:dX\:d :drc   r   c                  .    e Zd ZdZdZedd       ZddZy)SkipSelfMiddlewarezFilter out bot's own messages.z	skip-selfc                    | r|sy| |k(  S )z2Detect whether the message is from the bot itself.Fr   )re  r!  s     ra   _is_self_referencez%SkipSelfMiddleware._is_self_reference  s     6v%%rc   c                   K   | j                  |j                  |j                  j                        r6t        j                  d|j                  j                  |j                         y  |        d {    y 7 w)Nz'[%s] Ignoring self-sent message from %s)rN  re  r^  _bot_idr0  r  r  r  s      ra   r  zSkipSelfMiddleware.handle  sT     ""3#3#3S[[5H5HILLBCKKDTDTVYVfVfgis   A1A;3A94A;N)re  r   r!  rr  r   r   r  )r   r   r   r   r  r   rN  r  r   rc   ra   rL  rL    s!    (D& &rc   rL  c                      e Zd ZdZdZddZy)ChatRoutingMiddlewarez9Determine chat_id, chat_type, chat_name from push fields.zchat-routingc                0  K   |j                   r;d|j                    |_        d|_        |j                  xs |j                   |_        n:d|j
                   |_        d|_        |j                  xs |j
                  |_         |        d {    y 7 w)Nr  r
  r	  r  )rf  rl  rm  rg  rn  re  rh  r  s      ra   r  zChatRoutingMiddleware.handle  sx     >>"3>>"23CK#CMNN<cnnCM#C$4$4#56CK CM//C33C3CCMis   BBBBNr  r  r   rc   ra   rR  rR    s    CD	rc   rR  c                  p    e Zd ZdZ	 	 	 	 	 	 	 	 	 	 d
dZddZddZddZddZe	dd       Z
e	dd       Zy	)AccessPolicyzPlatform-level DM / Group access control policy.

    Encapsulates the allow/deny logic so that both inbound middleware
    and outbound ``send_dm`` can share the same rules without reaching
    into adapter internals.
    c                <    || _         || _        || _        || _        y r   )
_dm_policy_dm_allow_from_group_policy_group_allow_from)r  	dm_policydm_allow_fromgroup_policygroup_allow_froms        ra   r  zAccessPolicy.__init__
  s#     $+)!1rc   c                    t        j                  dd      j                         dv ryt        j                  dd      j                         dv S )NGATEWAY_ALLOW_ALL_USERSrn   >   1yestrueTYUANBAO_ALLOW_ALL_USERS)osgetenvlowerr  s    ra   _open_dm_opted_inzAccessPolicy._open_dm_opted_in  sD    99.399;?SSyy2B7==?CWWWrc   c                    | j                   dk(  ry| j                   dk(  r|j                         | j                  v S | j                   dk(  r| j                         S y)u:   Strict DM authorization — pairing does not imply access.disabledF	allowlistopen)rW  rh   rX  rh  )r  	sender_ids     ra   is_dm_allowedzAccessPolicy.is_dm_allowed  sS    ??j(??k)??$(;(;;;??f$))++rc   c                    t        |xs d      j                         }|sy| j                  dk(  ry| j                  dk(  r|| j                  v S | j                  dk(  ry| j                  dk(  r| j	                         S y)z?Whether a DM may reach gateway intake (pairing handshake path).rn   Frj  rk  pairingTrl  )r   rh   rW  rX  rh  )r  rm  	principals      ra   is_dm_intake_allowedz!AccessPolicy.is_dm_intake_allowed%  sx    	R(..0	??j(??k) 3 333??i'??f$))++rc   c                    | j                   dk(  ry| j                   dk(  r|j                         | j                  v S | j                   dk(  ry| j                   dk(  r| j                         S y)zGPlatform-level group chat inbound filter (open / allowlist / disabled).rj  Frk  rp  rl  )rY  rh   rZ  rh  r  rf  s     ra   is_group_allowedzAccessPolicy.is_group_allowed4  sl    +,##%)?)???*'))++rc   c                    | j                   S r   )rW  r  s    ra   r[  zAccessPolicy.dm_policy@  s    rc   c                    | j                   S r   )rY  r  s    ra   r]  zAccessPolicy.group_policyD  s    !!!rc   N)
r[  r   r\  r   r]  r   r^  r   r   r   r   r   )rm  r   r   r   )rf  r   r   r   r   )r   r   r   r   r  rh  rn  rr  ru  r  r[  r]  r   rc   ra   rU  rU    sy    
2
2 !
2 	
2
 $
2 

2X

   " "rc   rU  c                      e Zd ZdZdZddZy)AccessGuardMiddlewarez.Platform-level DM/Group access control filter.zaccess-guardc                  K   |j                   }|j                  }|j                  dk(  rR|j                  |j                        st
        j                  d|j                  |j                  |j                         y |j                  dk(  rR|j                  |j                        s7t
        j                  d|j                  |j                  |j                         y  |        d {    y 7 w)Nr  z'[%s] DM from %s blocked by dm_policy=%sr
  z([%s] Group %s blocked by group_policy=%s)r^  _access_policyrm  rr  re  r0  r  r  r[  ru  rf  r]  )r  r  r  r^  policys        ra   r  zAccessGuardMiddleware.handleN  s     ++&55==D ..s/?/?@=LL#"2"2F4D4D ]]g%**3>>:>LL#..&2E2E is   C%C/'C-(C/Nr  r  r   rc   ra   rz  rz  I  s    8Drc   rz  c                      e Zd ZdZdZddZy)AutoSetHomeMiddlewarea  Auto-designate the first inbound conversation as Yuanbao home channel.

    Triggers when no home channel is configured, or when an existing group-chat
    home is superseded by the first DM (direct > group upgrade).
    Silent: writes config.yaml and env, no user-facing message.

    Runs after :class:`BuildSourceMiddleware` and :class:`GroupAtGuardMiddleware`
    so unaddressed group traffic is dropped before home-channel persistence.
    Only senders that pass strict authorization (allowlist / explicit open
    opt-in / pairing-store approval) may claim ``YUANBAO_HOME_CHANNEL``.
    Intake-only pairing forwards must not claim ``YUANBAO_HOME_CHANNEL``.
    zauto-sethomec                P  K   |j                   }|j                  s5|j                  |      r#t        j                  dd      }| xs" |j                  d      xr |j                  dk(  }|j                  dk(  rd|_        |r	 ddlm} ddl	m
} dd l} |       }	|	d	z  }
i }|
j                         r+t        |
d
      5 }|j                  |      xs i }d d d        |j                  |d<    ||
|       t!        |j                        t        j"                  d<   t$        j'                  d|j(                  |j                  |j*                          |        d {    y # 1 sw Y   xY w# t,        $ r+}t$        j/                  d|j(                  |       Y d }~Kd }~ww xY w7 Hw)NYUANBAO_HOME_CHANNELrn   r  r  Tr   )get_hermes_home)atomic_config_writezconfig.yamlr  )encodingz=[%s] Auto-sethome: designated %s (%s) as Yuanbao home channelz[%s] Auto-sethome failed: %s)r^  _auto_sethome_done_sender_may_designate_homere  rf  r]   rm  hermes_constantsr  hermes_cli.configr  yamlexistsrl  	safe_loadrl  r   environr0  r1  r  rn  r5  r:  )r  r  r  r^  	_cur_home_should_setr  r  r  _homeconfig_pathuser_configfr  s                 ra   r  zAutoSetHomeMiddleware.handler  ss    ++))g.P.PQT.U		"8"=I N((2Ls}}7L  }}$-1*T@E+-E"'-"7K(*K"))+!+@ BA*...*;*ArKB:=++K 67'[A9<S[[9IBJJ56KKWckk3== iB B ! TNN#A7<<QRSSTsU   BF&;E- ?E!A<E- 
F&F$F&!E*&E- -	F!6!FF&F!!F&Nr  r  r   rc   ra   r  r  b  s     D rc   r  c                      e Zd ZdZdZdZedd       Zedd       Zedd       Z	e
dd       Zedd       Zedd	       Zedd
       Zeddd       ZddZy)ExtractContentMiddlewarez.Extract raw text and media refs from msg_body.zextract-content  c                   | j                  dd      }| j                  dd      }|r	d| d| dnd| d}|g}t        j                  }dD ]T  }| j                  |      }|st        |t              s(t        |      |kD  r|d| d	z   n|}|j                  d
|         n |r|j                  d       dj                  |      S )zAFormat elem_type 1010 (share card) into bracket-placeholder text.titlern   linkz[share_card: z | ])card_content
wechat_desNz...(truncated)z	Preview: z[visit link for full content]rZ   )r  r  _CARD_CONTENT_MAX_LENGTHr7  r   rp   r   r   )	customr  r  headerr   max_lenr[  valpreviews	            ra   _format_shared_linkz,ExtractContentMiddleware._format_shared_link  s     

7B'zz&"%6:=s4&2-PUwVW@X*CC3 	E**U#Cz#s+>A#h>P#hw-*::VYy	23	 LL89yyrc   c                   | j                  d      }|sy	 t        j                  |      }t        |t              r|j                  d      nd}|rt        |t              syd| dS # t        j
                  t        f$ r d}Y 6w xY w)zNFormat elem_type 1007 (link understanding card) into bracket-placeholder text.r3  Nr  z[link: z | visit link for full content])r  r  r  r7  r8  JSONDecodeError	TypeErrorr   )r  r3  parsedr  s       ra   _format_link_understandingz3ExtractContentMiddleware._format_link_understanding  s     **Y'	ZZ(F)3FD)A6::f%tD :dC0=>>	 $$i0 	D	s   8A' 'BBc                D   | sy	 t         j                  j                  t         j                  j                  |       j                        }|j                  d      xs |j                  d      xs g }|rt        |d         j                         S dS # t        $ r Y yw xY w)zExtract resourceId from Yuanbao resource URL query parameters.

        Args:
            url: Resource URL (e.g., https://...?resourceId=abc123)

        Returns:
            Resource ID string, or empty string if not found
        rn   
resourceId
resourceidr   )	urllibparseparse_qsurlparsequeryr  r   rh   r5  )r?  r  idss      ra   _parse_resource_idz+ExtractContentMiddleware._parse_resource_id  s     	LL))&,,*?*?*D*J*JKE))L)JUYY|-DJC*-3s1v;$$&525 		s   BB B 	BBc                	   g }|D ]V  }|j                  dd      }|j                  di       }|dk(  r'|j                  dd      }|sB|j                  |       T|dk(  r|j                  d      }t        |t              sg }d}t	        |      d	kD  rt        |d	   t
              r|d	   }n&t	        |      d
kD  rt        |d
   t
              r|d
   }t        |xs i j                  d      xs d      j                         }	| j                  |	      }
|j                  |
rd|
 dnd       '|dk(  r|j                  d|j                  d|j                  dd                  }t        |j                  d      xs d      j                         }| j                  |      }
|
r"|j                  |r	d| d|
 dnd|
 d       |j                  |rd| dnd       |dk(  rXt        |j                  d      xs d      j                         }| j                  |      }
|j                  |
rd|
 dnd       7|dk(  rXt        |j                  d      xs d      j                         }| j                  |      }
|j                  |
rd|
 dnd       |dk(  r5|j                  dd      }|r	 t        j                  |      }t        |t
              s|j                  d        |j                  d!      }|d"k(  r"|j                  |j                  dd#             n|d$k(  r!|j                  | j                  |             nt|d%k(  r7| j                  |      }|r|j                  |       nJ|j                  d        n8|d&k(  r"|j                  |j                  dd'             n|j                  d        |j                  d        |d(k(  rj|j                  dd      }d}|r9	 t        j                  |      }|j                  d)      xs dj                         }|j                  |rd*| dnd+       >|sB|j                  d,| d       Y |rd-j!                  |      S dS # t        j                  t        f$ r |j                  |       Y w xY w# t        j                  t        t        f$ r Y w xY w).a#  Extract plain text content from MsgBody.

        - TIMTextElem      -> text field
        - TIMImageElem     -> "[image]" / "[image|ybres:RID]"
        - TIMFileElem      -> "[file: {filename}]" / "[file:{name}|ybres:RID]"
        - TIMSoundElem     -> "[voice]" / "[voice|ybres:RID]"
        - TIMVideoFileElem -> "[video]" / "[video|ybres:RID]"
        - TIMFaceElem      -> "[emoji: {name}]" or "[emoji]"
        - TIMCustomElem    -> try to extract data field, otherwise "[custom message]"
        - Multiple elems joined with spaces
        rt  rn   r  r  r^   TIMImageElemimage_info_arrayNrV   r   r?  [image|ybres:r  [image]TIMFileElem	file_namefileNamefilenamez[file:|ybres:[file|ybres:[file: [file]TIMSoundElemz[voice|ybres:[voice]TIMVideoFileElem[video|ybres:[video]TIMCustomElemr   z[unsupported message type]	elem_type  z	[mention]      z[chat record]TIMFaceElemr  z[emoji: z[emoji][ )r  r   r7  r`  rp   r8  r   rh   r  r  r  r  r  r  r  AttributeErrorr   )r   ri  partselemr  r3  r^   r  
image_info	image_urlridr  file_url	sound_url	video_urldata_valr  ctyperaw_data	face_name	face_datas                        ra   _extract_textz&ExtractContentMiddleware._extract_text  sa     O	/D!XXj"5I HH]B7GM){{62.LL&n,#*;;/A#B !"2D9')$!
'(1,<LQ<OQU1V!1!!4J)*Q.:>Nq>QSW3X!1!!4J!1r 6 6u = CDJJL	,,Y7s}SE3	Jm+";;{GKK
GKKXbdfLg4hiw{{517R8>>@,,X6LLX6(73%q!AUabeaffgShiLL(78*A!6Qn,E 2 8b9??A	,,Y7s}SE3	J00E 2 8b9??A	,,Y7s}SE3	Jo-";;vr2/!%H!5)&$7!LL)EF$ &

; 7 D=!LLFK)HI"d]!LL)@)@)HI"d]#&#A#A&#ID# %T 2 %-I J"d]!LLFO)LM!LL)EF LL!=>m+";;vr2	$(JJx$8	%.]]6%:%@b$G$G$I	 	x	{!4yQq1-._O	/b #(sxx/R/' !00)< /X./ !00)^L s+   56Q4-CQ408R%4*R"!R"%SSc                X    | j                         } | j                  d      rd| dd z   } | S )zNormalize input text: strip whitespace and convert full-width slash
        (Chinese input method) to ASCII slash so commands are recognized correctly.
           ／r  rV   Nrh   r]   r   s    ra   _rewrite_slash_commandz/ExtractContentMiddleware._rewrite_slash_command1  s/    
 zz|??8$ab>Drc   c                   g }| xs g D ]  }t        |t              s|j                  dd      }|j                  di       xs i }t        |t              sN|dk(  r|j                  d      }t        |t              sg }d}t	        |      dkD  rt        |d   t              r|d   }n&t	        |      dkD  rt        |d   t              r|d   }t        |xs i j                  d	      xs d      j                         }|r|j                  d
|d       |dk(  st        |j                  d	      xs d      j                         }t        |j                  d      xs d      j                         xsZ t        |j                  d      xs d      j                         xs, t        |j                  d      xs d      j                         }	|sd|d}
|	r|	|
d<   |j                  |
        |S )zExtract inbound image/file references from TIM msg_body.

        Return example:
          [{"kind": "image", "url": "https://..."}, {"kind": "file", "url": "...", "name": "a.pdf"}]
        rt  rn   r  r  r  NrV   r   r?  rQ   kindr?  r  r  r  r  rP   r  )r7  r8  r  r`  rp   r   rh   r   )ri  refsr  rt  r3  r  r  r  r  r  refs              ra   _extract_inbound_media_refsz4ExtractContentMiddleware._extract_inbound_media_refs;  s    &(N "	%DdD)xx
B/Hhh}b17RGgt,>)#*;;/A#B !"2D9')$!
'(1,<LQ<OQU1V!1!!4J)*Q.:>Nq>QSW3X!1!!4J!1r 6 6u = CDJJL	KK CD=(w{{517R8>>@K06B7==? B7;;z28b9??AB7;;z28b9??A 
 39(*KC &/FKK$E"	%F rc   c                   g }| xs g D ]:  }t        |t              r|j                  d      dk7  r)|j                  d      xs i j                  dd      }|sQ	 t        j                  |      }t        |t              sx|j                  d      }|dk(  r7|j                  d      }|st        |t              s|j                  |       |d	k(  s|j                  d
      }|s	 t        j                  |      }t        |t              r|j                  d      nd}|r!t        |t              r|j                  |       = |S # t        j
                  t        f$ r Y \w xY w# t        j
                  t        f$ r Y |w xY w)zTExtract link URLs from share-card (1010) and link-understanding (1007) custom elems.rt  r  r  r   rn   r  r  r  r  r3  N)	r7  r8  r  r  r  r  r  r   r   )	ri  urlsr  data_strr  r  r  r3  r  s	            ra   _extract_link_urlsz+ExtractContentMiddleware._extract_link_urlsh  s^    N 	DdD)TXXj-A_-T/52::62FHH- fd+JJ{+E}zz&)JtS1KK%$ **Y/!%G!45?5Mvzz&1SWJtS$9 KK-1	6 ' (()4 " !00)< s%   E	)AE)	E&%E&)FFc                b   | xs g D ]  }t        |t              r|j                  d      dk7  r)|j                  di       xs i }t        |t              sP|j                  dd      }|se	 t        j                  |      }t        |t              r|j                  d      dk(  s|j                  d      xs i }t        |t              r|s y	d
 }|j                         D ]'  \  }}	|j                  d      s ||	      }
|
#|
c c S   y	# t        j
                  t        f$ r Y &w xY w)a  Extract ForwardMsgData from ext_map for elem_type 1009 (WeChat forward).

        The detailed chat-record payload lives in ``msg_content.ext_map``
        (protobuf field 999, ``map<string, string>``):
          - key format: ``wexin_forward_msg_[forward_msg_id]_[userid]``
          - value: a **base64-encoded protobuf** ``ForwardMsgData`` (NOT JSON).
            Decode with base64 then ``decode_forward_msg_data`` to recover the
            ``sub_type`` / ``nick_name`` / ``msg`` structure.

        Matching strategy: take the first ``wexin_forward_msg_`` entry whose
        decoded payload is a valid ``ForwardMsgData`` (``sub_type == 1``).

        Returns the parsed ``ForwardMsgData`` dict or ``None``.
        rt  r  r  r   rn   r  r  ext_mapNc                    t        | t              r| sy 	 t        j                  |       }t        |      }t        |t              r|j                  d      dk(  r|S y # t        j
                  t        f$ r Y y w xY w)Nsub_typerV   )
r7  r   base64	b64decodebinasciiErrorr6  r.   r8  r  )valuepbr   s      ra   _parse_valuezIExtractContentMiddleware._extract_forwarded_records.<locals>._parse_value  sq    !%-U ))%0B /r2dD)dhhz.Ba.GK !
3   s   A A87A8wexin_forward_msg_)	r7  r8  r  r  r  r  r  r  r]   )ri  r  r  r3  r  r  r  r  keyr  r  s              ra   _extract_forwarded_recordsz3ExtractContentMiddleware._extract_forwarded_records  s+     N '	"DdD)TXXj-A_-Thh}b17RGgt,{{62.HH- vt,K1HD1Pkk),2Ggt,G &mmo "
U~~&:;%e,%!M"E'	"R = (()4 s   -DD.-D.c                b  K   | j                  | j                  |j                              |_        | j	                  |j                        |_        | j                  |j                        |_        | j                  |j                  |j                        |_
         |        d {    y 7 wr   )r  r  ri  ro  r  rp  r  rz  r  re  rq  r  s      ra   r  zExtractContentMiddleware.handle  s{     2243E3Ecll3ST99#,,G//= $ ? ?cN^N^ _is   B%B/'B-(B/N)r  r8  r   r   )r  r8  r   rr  r?  r   r   r   )ri  r`  r   r   r   )ri  r`  r   List[Dict[str, str]])ri  r`  r   r`  rX  )ri  r`  r  r   r   rb  r  )r   r   r   r   r  r  r   r  r  r  r   r  r  r  r  r  r  r   rc   ra   r  r    s    8D#   " ? ?  $ ^0 ^0@   * *X  @ 8 8trc   r  c                  P    e Zd ZU dZdZ eh d      Zded<   ed	d
d       Z	ddZ
y)PlaceholderFilterMiddlewarez>Skip pure placeholder messages (e.g. '[image]' with no media).zplaceholder-filter>      [图片]   [文件]   [视频]   [语音]r  r  r  r  rJ  SKIPPABLE_PLACEHOLDERSc                J    |dkD  ry|j                         }|| j                  v S )zEDetect whether the message is a pure placeholder (should be skipped).r   F)rh   r  )r   r^   media_countr   s       ra   is_skippable_placeholderz4PlaceholderFilterMiddleware.is_skippable_placeholder  s*     ?::<35555rc   c                   K   | j                  |j                  t        |j                              r6t        j                  d|j                  j                  |j                         y  |        d {    y 7 w)Nz%[%s] Skipping placeholder message: %r)r  ro  rp   rp  r0  r  r^  r  r  s      ra   r  z"PlaceholderFilterMiddleware.handle  sQ     ((s3>>7JKLL@#++BRBRTWT`T`ais   A0A:2A83A:N)r   )r^   r   r  r   r   r   r  )r   r   r   r   r  rJ  r  rY  r   r  r  r   rc   ra   r  r    s:    HD(1 3 )I 
 6 6rc   r  c                  t    e Zd ZU dZdZ eh d      Zded<   ed
d       Z	e
	 	 	 	 	 	 	 	 	 	 dd       ZddZy	)OwnerCommandMiddlewarezDetect bot-owner slash commands in group chat.

    Identifies in-group allowlisted slash commands and determines sender identity.
    Owner commands skip @Bot detection; non-owner attempts are rejected.
    zowner-command>   /q/bg/btw/new/deny/stop/undo/queue/reset/retry/approve/backgroundrJ  	ALLOWLISTc                X    | j                         } | j                  d      rd| dd z   } | S )z?Normalize full-width slash to ASCII slash and strip whitespace.r  r  rV   Nr  r   s    ra   r  z-OwnerCommandMiddleware._rewrite_slash_command  s/     zz|??8$ab>Drc   c               $   |dk7  s| j                   sy|xs g D cg c]  }|j                  d      dk(  r| }}t        |      dk7  ry|d   j                  d      xs i j                  dd	      }| j                  |      }|j	                  d
      sy|j                  d      d   j                         }	|	| j                   vryt        |xs i j                  d      xs d	      j                         }
t        |
      xr |
|k(  }|	||fS c c}w )a2  Identify allowlisted slash commands and determine sender identity.

        Returns (cmd, cmd_line, is_owner):
          - (None, None, False): Not an allowlisted command
          - (cmd, cmd_line, True): Owner match
          - (cmd, cmd_line, False): Allowlisted command but sender is not owner
        r
  )NNFrt  r  rV   r   r  r^   rn   r  )maxsplitr  )
r  r  rp   r  r]   r\   rg  r   rh   r   )r   rc  ri  rm  re  r  
text_elemsr^   cmd_liner  owner_idis_owners               ra   _detect_owner_commandz,OwnerCommandMiddleware._detect_owner_command  s     s}}$ !B
uuZ M1 

 
 z?a$1!!-06B;;FBG--d3""3'$nnan(+113cmm#$ 
''7=2>DDF>>h,&>Hh&&+
s   Dc           
     d  K   |j                   }| j                  |j                  |j                  |j                  |j
                        \  }}}|r|st        j                  d|j                  |j                  |j
                  |       |j                  t        j                  |j                  |j                  d| d      d|              y |rI|rG|rEt        j                  d|j                  |j                  |j
                  |       ||_        ||_         |        d {    y 7 w)N)rc  ri  rm  re  z;[%s] Reject non-owner slash command: chat=%s from=%s cmd=%su   ⚠️ z6 is only available to the creator in private chat modezyuanbao-owner-cmd-denial-r  z4[%s] Bot owner slash command: chat=%s from=%s cmd=%s)r^  r  rc  ri  rm  re  r0  r1  r  rl  _track_taskr   r>  sendrs  ro  )r  r  r  r^  matched_cmdr  r  s          ra   r  zOwnerCommandMiddleware.handle'  s    ++*.*D*D\\mm))	 +E +
'Xx xKKMckk3+;+;[  3 3S[[GK=@v*wx0>!  8KKFckk3+;+;[ !,C#CLis   D&D0(D.)D0Nr   )
rc  r8  ri  r`  rm  r   re  r   r   z)Tuple[Optional[str], Optional[str], bool]r  )r   r   r   r   r  rJ  r  rY  r   r  r   r  r  r   rc   ra   r  r    s     D % & Iy    (' (' 	('
 (' (' 
3(' ('Trc   r  c                      e Zd ZdZdZddZy)BuildSourceMiddlewarez(Build SessionSource from context fields.zbuild-sourcec           	     (  K   |j                   }|j                  |j                  |j                  |j                  |j
                  xs d |j                  xs |j
                  |j                  dk(  rdnd       |_         |        d {    y 7 w)Nr
  r  )rl  rm  rn  r  	user_namer  )r^  r  rl  rm  rn  re  rh  rM  r  r  r  r^  s       ra   r  zBuildSourceMiddleware.handleJ  sx     ++))KKmmmm$$,))=S-=-= # 8fd * 

 is   BB
BBNr  r  r   rc   ra   r#  r#  E  s    2D
rc   r#  c                      e Zd ZdZdZed
d       Zedd       Zedd       Ze	ddd	 	 	 	 	 	 	 	 	 	 	 dd       Z
dd	Zy)GroupAtGuardMiddlewarezIn group chat, observe non-@bot messages; only reply on @Bot.

    Owner commands skip @Bot detection (owner doesn't need to @Bot).
    zgroup-at-guardc                L   |sy| D ]}  }|j                  d      dk7  r|j                  di       j                  dd      }|s=	 t        j                  |      }|j                  d      dk(  sh|j                  d	      |k(  s} y
 y# t        j                  t        f$ r Y w xY w)a  Detect whether the message @Bot.

        AT element format: TIMCustomElem, msg_content.data is a JSON string:
            {"elem_type": 1002, "text": "@xxx", "user_id": "<botId>"}
        Considered @Bot when elem_type == 1002 and user_id == bot_id.
        Frt  r  r  r   rn   r  r  r  T)r  r  r  r  r  )ri  r!  r  r  r  s        ra   
_is_at_botz!GroupAtGuardMiddleware._is_at_bot_  s      	Dxx
#6xxr266vrBHH- zz+&$.6::i3HF3R	 	 (()4 s   BB#"B#c                   |sy| D ]  }|j                  d      dk7  r|j                  di       j                  dd      }|s=	 t        j                  |      }|j                  d      dk(  sh|j                  d      |k(  s}t        |j                  d	      xs d      j                         }|s|c S  y# t        j                  t        f$ r Y w xY w)
zLExtract the display text used to @-mention this bot (e.g. ``@yuanbao-bot``).rn   rt  r  r  r   r  r  r  r^   )r  r  r  r  r  r   rh   )ri  r!  r  r  r  mention_texts         ra   _extract_bot_mention_textz0GroupAtGuardMiddleware._extract_bot_mention_textw  s      	(Dxx
#6xxr266vrBHH- zz+&$.6::i3HF3R"6::f#5#;<BBD''	(  (()4 s   B88CCc                f    t        |xs d      }t        j                  | |      xs d}d| d| dS )zOBuild a per-turn group-chat prompt that highlights which message to respond to.unknownzHYou are handling a Yuanbao group chat message.
- Your identity: user_id=z, @-mention name in this group=z
- Lines in history prefixed with `[nickname|user_id]` are observed group context and are not necessarily addressed to you.
- Treat only the current new message as a request explicitly directed at you, and answer it directly.)r   r(  r-  )ri  r!  bidbot_mentions       ra   _build_group_channel_promptz2GroupAtGuardMiddleware._build_group_channel_prompt  sM     &%I&,FFxQWXe\e((+u,KK= Y&&	
rc   N)rj  rq  c                  t        |dd      }|sy	 |j                  |      }	|j                  xs d}
|}|r%t        j	                  ||d      }|r|r| d| n|}d| d|
 d	| }d
|t        j                  t        j                        j                         dd}|r||d<   |j                  |	j                  |       y# t        $ r+}t        j                  d|j                  |       Y d}~yd}~ww xY w)as  Write a group message into the session transcript without triggering the agent.

        This allows the model to see the full group conversation when it is
        eventually invoked via @bot.  Messages are stored with ``role: "user"``
        in the format ``[nickname|user_id]\n<content>`` so the model
        can distinguish participants and their user ids.
        r0  Nr/  Fr  is_dispatchrZ   r  rf   ]
r2  r   T)r1  r3  r   observedrD  z([%s] Failed to observe group message: %s)r4  r5  r  ForwardedRecordsParseMiddlewarebuild_forward_textr   r   r   rG  rH  rF  r6  r5  r0  r:  r  )r   r^  rM  sender_displayr^   r  rj  rq  r:  session_entryr  	body_textsummary
attributedr  rF  s                   ra   _observe_group_messagez-GroupAtGuardMiddleware._observe_group_message  s     !148	Z!77?Mnn1	GI 9LL%3E M  8<4&7) 4'I^,AgYc)EJ%%\\X\\:DDF 	E &,l#&&((  	ZNNEw||UXYY	Zs   B/C 	C6!C11C6c           	       K   |j                   }|j                  dk(  r|j                  s| j                  |j                  |j
                        s| j                  ||j                  |j                  xs |j                  |j                  |j                  xs d |j                  |       t        j                  d|j                  |j                   |j                         y  |        d {    y 7 w)Nr
  )rj  rq  r  z6[%s] Group message observed (no @bot): chat=%s from=%s)r^  rm  rs  r*  ri  rP  r?  rM  rh  re  ro  rj  rq  r0  r1  r  rl  r&  s       ra   r  zGroupAtGuardMiddleware.handle  s     ++==G#C,=,=dooVYVbVbdkdsdsFt''S%8%8%LC<L<Lcllzz)T"%"7"7	 (  KKHckk3+;+; is   C+C5-C3.C5)ri  r`  r!  rr  r   r   )ri  r`  r!  rr  r   r   )r:  r   r^   r   r  r]  rj  rr  rq  rb  r   r   r  )r   r   r   r   r  r   r*  r-  r2  r   r?  r  r   rc   ra   r(  r(  W  s    
 D .  ( 
 
  !%,0*Z),*Z47*Z 	*Z
 *Z **Z 
*Z *ZXrc   r(  c                      e Zd ZdZdZddZy)GroupAttributionMiddlewarea  Tag group @bot messages with [nickname|user_id] attribution and channel_prompt.

    For group messages that pass the @bot guard (i.e. the bot is mentioned),
    this middleware:
      - Builds a per-turn channel_prompt so the model knows its identity and
        the attribution scheme.
      - Rewrites ctx.raw_text to ``[nickname|user_id]\n<content>`` to match
        the observed-history format.
      - Suppresses the runner's default ``[user_name]`` shared-thread prefix
        by clearing ``source.user_name``.
    zgroup-attributionc                  K   |j                   dk(  r|j                  s|j                  }t        j	                  |j
                  |j                        |_        |j                  xs d}|j                  xs |j                  xs d}d| d| d|j                   |_
        |j                  &t        j                  |j                  d       |_         |        d {    y 7 w)Nr
  r/  r  rf   r6  )r%  )rm  rs  r^  r(  r2  ri  rP  r{  re  rh  ro  rM  dataclassesr   )r  r  r  r^  user_id_labelnickname_labels         ra   r  z!GroupAttributionMiddleware.handle  s     ==G#C,=,=kkG!7!S!Sgoo"C  ,,9	M 00QC4D4DQ	N~.ac#,,PCL zz%(00tL
is   CC%C#C%Nr  r  r   rc   ra   rB  rB    s    
 Drc   rB  c                      e Zd ZdZdZy)YuanbaoMessageTypezYuanbao-local message subtypes; coerced back to :class:`MessageType`
    before leaving the adapter (see :class:`DispatchMiddleware`).chat_recordN)r   r   r   r   CHAT_RECORDr   rc   ra   rH  rH    s    E  Krc   rH  c                  .    e Zd ZdZdZedd       ZddZy)ClassifyMessageTypeMiddlewarez>Determine MessageType from text content and msg_body elements.zclassify-msg-typec                   | j                  d      rt        j                  S |D ]  }|j                  dd      }|dk(  rt        j                  c S |dk(  rt        j
                  c S |dk(  rt        j                  c S |dk(  rt        j                  c S |dk(  sw|j                  d	      xs i j                  d
d      }	 t        j                  |      }t        |t              s|j                  d      dk(  st        j                  c S  t        j                   S # t        j                  t        f$ r d}Y fw xY w)zClassify message type based on text and msg_body.

        Returns a base :class:`MessageType`, or a yuanbao-local
        :class:`YuanbaoMessageType` for platform-specific subtypes.
        r  rt  rn   r  r  r  r  r  r  r   Nr  r  )r]   r   COMMANDr  PHOTOVOICEVIDEODOCUMENTr  r  r  r  r7  r8  rH  rJ  r$  )r^   ri  r  etyper  r  s         ra   	_classifyz'ClassifyMessageTypeMiddleware._classify	  s    ??3&&& 	:DHHZ,E&"(((&"(((**"(((%"+++' HH]39r>>vrJ"!ZZ1F fd+

;0G40O-999#	:$ 	 ,,i8 "!F"s   DD=<D=c                   K   | j                  |j                  |j                        |_         |        d {    y 7 wr   )rT  ro  ri  rt  r  s      ra   r  z$ClassifyMessageTypeMiddleware.handle!	  s)     ~~cllCLLAis   6A >A N)r^   r   ri  r`  r  )r   r   r   r   r  r   rT  r  r   rc   ra   rL  rL    s!    HD   8rc   rL  c                  4    e Zd ZdZdZddZ	 	 	 	 ddZd	dZy)
QuoteContextMiddlewarez3Extract quote/reply context from cloud_custom_data.zquote-contextc                6   |sy	 t        j                  |      }t	        |t
              r|j                  d      nd}t	        |t
              syt        |j                  d      xs d      j                         xs d}t        |j                  d      xs d      j                         }t        |j                  d      xs |j                  d      xs d      j                         }|r|r| d	| n|nd}||fS # t         j                  t        f$ r Y yw xY w)
zHExtract quote text context, mapping to MessageEvent.reply_to_*.
        r  quoteNidrn   descrh  rm  r  )	r  r  r  r  r7  r8  r  r   rh   )r  rk  r  rY  quote_idr[  sender
quote_texts           ra   _extract_quote_contextz-QuoteContextMiddleware._extract_quote_context+	  s     !	ZZ 12F (2&$'?

7#T%&uyy,"-335=599V$*+113UYY01QUYY{5KQrRXXZBFV4&)D
## $$i0 		s   C< <DDc           	     :  K   |j                   g S |j                  }g }	 t        |dd      }|r|j                  g S |j	                  |j                        }|j                  |j                        }t        |xs g       D ]  }|j                  dd      }|r||j                   k7  r'|j                  dd      }	t        |	t              rd|	v rt        j                  |	      D ]t  }
|
j                  d      }|
j                  d      }|j                  d	      \  }}}|j                         }|t         v sS|j#                  |||j                         f       v  |S  	 |S # t$        $ r-}t&        j)                  d
t        |dd      |       Y d}~|S d}~ww xY ww)ay  Look up the quoted message in the transcript history and return any
        ``[kind|ybres:RID]`` anchors found in its content as
        ``(rid, kind, filename)`` tuples.

        Returns ``[]`` when ``ctx.reply_to_message_id`` is unset, when the
        transcript store / source is unavailable, or when the quoted message
        carries no resolvable media anchors.
        Nr0  rD  rn   r3  r  rV   rG   :z'[%s] quote transcript lookup failed: %sr  yuanbao)ru  r^  r4  rM  r5  r7  r6  reversedr  r7  r   _YB_RES_REF_RErt   r
  	partitionrh   _RESOLVABLE_MEDIA_KINDSr   r5  r0  r:  )r  r  r^  rp  r:  r;  historyr"  r|   _contentr   r   r  r  r  r  rF  s                    ra   #_extract_media_refs_from_transcriptz:QuoteContextMiddleware._extract_media_refs_from_transcript@	  s     ""*I++13
	G%5t<ECJJ.	!77

CM++M,D,DEG2. gglB/cS%<%<<779b1h,h1F+44X> M wwqzggaj,0NN3,?)a#zz|#::&--sD(..:J.KLM  '&   	NN93S  	sK   FE" FC9E" 7%E" FE"  F"	F+"FFFFc                   K   | j                  |j                        \  |_        |_        | j	                  |       d {   |_         |        d {    y 7 7 wr   )r_  rk  ru  rv  ri  rw  r  s      ra   r  zQuoteContextMiddleware.handlej	  sQ     595P5PQTQfQf5g2!2%)%M%Mc%RRi  Ss"   =AA AAAAN)rk  r   r   z#Tuple[Optional[str], Optional[str]])r  r]  r   List[Tuple[str, str, str]]r  )r   r   r   r   r  r_  ri  r  r   rc   ra   rW  rW  &	  s*    =D$*(!(	#(Trc   rW  c                      e Zd ZdZdZd
dZed
d       Ze	 d	 	 	 	 	 dd       Z	dZ
e	 	 	 	 dd       Ze	 	 	 	 	 	 	 	 dd       Zy	)r8  u  Deep-parse WeChat forwarded chat records (elem_type 1009) for dispatch.

    Activates when a full ``ForwardMsgData`` dict is available on the current
    turn, carried by the current message (``ctx.forwarded_records``).
    Resolves media to ``[kind|ybres:RID]``
    placeholders, appends downloadable refs to ``ctx.media_refs`` (for
    :class:`MediaResolveMiddleware`), and rewrites ``ctx.raw_text``.

    Group @bot turns *without* a forward on the current message rely on the
    eagerly-rendered summaries that :class:`GroupAtGuardMiddleware` writes to
    the transcript at observe time — there is no run-time summary fallback
    here.

    On any failure the middleware leaves ``ctx.raw_text`` untouched
    (graceful degradation, design §2.8).
    zforwarded-records-parsec           	     4  K   	 |j                   r4| j                  |       | j                  |j                   |d      |_         |        d {    y # t        $ r6}t
        j                  dt        |j                  dd      |       Y d }~Jd }~ww xY w7 Gw)NTr4  z,[%s] forwarded-records deep parse failed: %sr  rb  )	rq  _send_loading_heartbeatr9  ro  r5  r0  r:  r4  r^  )r  r  r  rF  s       ra   r  z&ForwardedRecordsParseMiddleware.handle	  s     		$$,,S1#66s7L7LRUcg6h i  	NN>VY7 	 	s;   BA A 
BBB	B,B	BBBc                   K   	 | j                   j                  j                  j                  | j                  t
               d{    y7 # t        $ r Y yw xY ww)z@Best-effort RUNNING heartbeat so the user sees a loading bubble.N)r^  	_outbound	heartbeatsend_heartbeat_oncerl  r)   r5  )r  s    ra   rn  z7ForwardedRecordsParseMiddleware._send_loading_heartbeat	  sP     	++''11EE1    		s;   AAA AA 
AA 	AAAAc                   |j                  dd      xs |j                  dd      j                         j                         }t        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }|xs t        j                  |      }|dk(  r8|r|rd| d	| j                         d|d
fS d|xs | j                         dfS |dv r=|r&|r$d|d
}|r||d<   d| d	| j                         |fS d| j                         dfS |dk(  r:|xs t        |j                  d      xs d      }	d|	 d| j                         dfS |dk(  r8|r|rd| d	| j                         d|d
fS d|xs | j                         dfS d|xs d d	|xs | j                         dfS )a:  Render one ``msgContent.multimedia`` entry as a textual marker.

        Returns ``(marker, ref)``. Downloadable media emits a
        ``[kind|ybres:RID]`` marker and a ``ctx.media_refs`` ref dict when a
        usable RID/URL is present; otherwise a plain ``[kind] name`` marker
        and ``ref=None``.
        typern   doc_typer?  media_idr  rQ   r  z] r  z[image] N)rP   documentr  rP   r  r  z[file] r  z[link] r  rR   r  z[video] r  media)r  rh   rg  r   r  r  rg   )
r   rx  
plain_text
media_typer?  rv  r  r  r  
link_titles
             ra   _media_markerz-ForwardedRecordsParseMiddleware._media_marker	  s.    ii+HuyyR/HOOQWWY
%))E"(b)//1uyy,2399;		+.4"5;;=	 J2EEcJ s&se2i[9@@BW]`Daaai5:67>>@$FF55s/5c&B"+CK%cU"YK8??A3FFYK(//1477"Cc%))G*<*B&CJZL#/668$>> s&se2i[9@@BW]`Daaai.3/07794??:()C,<9+=>EEGMMrc   r  c              #  |  K   t        |t              r|j                  d      ndxs g D ]  }t        |t              s|j                  dd      }|j                  dd      }|j                  dg       xs g }g }|s|}ng }|D ]  }	t        |	t              s|	j                  dd      }
|
d	k(  r"|j                  |	j                  d
d             M|
dk(  rg|	j                  dg       xs g D ]M  }t        |t              s| j	                  ||      \  }}|j                  |       |=|j                  |       O |
dk(  r|j                  d       |s|j                  |        dj                  d |D              xs |}t        |      | j                  kD  r|d| j                   dz   }|||f  yw)u4  Walk ``ForwardMsgData['msg']`` and yield ``(sender, body, refs)``.

        Per-record dispatch over ``msgContent`` (text / multimedia / nested
        forward / fallback); ``body`` is capped at
        :attr:`FORWARD_MSG_TEXT_MAX_CHARS`. Media goes through
        :meth:`_media_marker`, always building full ``[kind|ybres:RID]``
        markers; ``refs`` holds that record's downloadable ``ctx.media_refs``
        entries in textual order — the order PatchAnchorsMiddleware relies on
        (design §2.10.6). Headers / footers are the caller's job.
        r"  Nr]  rn   	plainText
msgContentrt  r   rV   r^   rG   
multimediar   u   [嵌套聊天记录]z  c              3  &   K   | ]	  }|s|  y wr   r   )r   ps     ra   r   zEForwardedRecordsParseMiddleware._walk_forward_msgs.<locals>.<genexpr>	  s     $;1Q$;   u   …(已截断))r7  r8  r  r   r|  r   rp   FORWARD_MSG_TEXT_MAX_CHARS)r   forward_datar"  r]  ry  msg_contentsr  renderedr  mcmc_typerx  markerr  s                 ra   _walk_forward_msgsz2ForwardedRecordsParseMiddleware._walk_forward_msgs	  s     0:,/ML$$U+SW^\^ $	)Cc4(WWXr*Fb1J77<4:L)+D%#%& 5B%b$/  ffVQ/G!|RVVFB%78 A%'VVL"%=%C 5E)%6.1.?.?$):/" !&V 4#&?$(KK$45 !A%;<%!LL4'5(  99$;$;;Iz8}s===#$Dc&D&DEHXX(D((I$	)s   C>F<(F<*-F<A$F<c               j   |j                   xs d}d| dg}| j                  |      D ]:  \  }}}|j                  | d|        |s |j                  j	                  |       < dj                  |      }	|r:|j                  j                         r |	d|j                  j                          z  }	|	S )u  Render ``ForwardMsgData`` into forward text.

        Body lines are ``发送人：正文`` with full ``[kind|ybres:RID]`` media
        markers preserved. When ``is_dispatch`` is true, refs are appended to
        ``ctx.media_refs`` for downstream resolution and a ``用户附言：
        {ctx.raw_text}`` footer is added; observed callers skip both since
        no later middleware runs.
        u   用户u   当前用户的昵称为u   以下为用户的聊天记录u   ：rZ   u   

用户附言：)rh  r  r   rp  extendr   ro  rh   )
r   r  r  r5  nicknamer   r]  rD  r  r^   s
             ra   r9  z2ForwardedRecordsParseMiddleware.build_forward_text
  s     &&2(+H:68XY"%"8"8"F 	,FD$LLF83tf-.%%d+	, yy3<<--/)#,,*<*<*>)?@@Drc   Nr  rX  )rx  r8  ry  r   r   z$Tuple[str, Optional[Dict[str, str]]])r  r8  r   z/Iterator[Tuple[str, str, List[Dict[str, str]]]])r  r8  r  r]  r5  r   r   r   )r   r   r   r   r  r  r   rn  r   r|  r  r  r9  r   rc   ra   r8  r8  p	  s    " %D    ,.)N)N&))N	-)N )NX "&2)2) 
92) 2)l )7FJ	 rc   r8  c                  p   e Zd ZU dZdZi Zded<   dZded<   dZded	<   e	dd
       Z
e	dd       Ze		 	 	 	 	 	 	 	 dd       Zedd       Zedd       Zedd       Ze	dddd	 	 	 	 	 	 	 	 	 	 	 dd       Ze		 	 	 	 d d       Ze		 	 	 	 	 	 d!d       Ze		 	 d"d       Ze		 	 	 	 d#d       Zed$d       Zd%dZy)&MediaResolveMiddlewarez6Resolve inbound media references to downloadable URLs.zmedia-resolvez+ClassVar[Dict[str, Tuple[str, str, float]]]_resource_cacheiQ ClassVar[int]_RESOURCE_CACHE_TTL_S   _RESOURCE_CACHE_MAX_SIZEc                R   |sy| j                   j                  |      }|y|\  }}}t        j                         |z
  | j                  kD  r| j                   j	                  |d       yt
        j                  j                  |      s| j                   j	                  |d       y||fS )zOReturn cached ``(local_path, mime)`` if still valid and file exists, else None.N)r  r  r  r  r  re  pathisfile)r   resource_idr  
local_pathmimetss         ra   _get_cached_resourcez+MediaResolveMiddleware._get_cached_resource)
  s     ##''4=$
D"99;c777##K6ww~~j)##K64rc   c                2    |syt         j                         j                  k\  rMt         j                   fd      }|d j                  dz   D ]  } j                  j	                  |d         ||t        j
                         f j                  |<   y)zIStore download result in cache. Evicts oldest entries when over capacity.Nc                (    j                   |    d   S )NrG   )r  )r  r   s    ra   <lambda>z=MediaResolveMiddleware._put_cached_resource.<locals>.<lambda>B
  s    CDWDWXYDZ[\D] rc   )r     )rp   r  r  sortedr  r  )r   r  r  r  sorted_keysr  s   `     ra   _put_cached_resourcez+MediaResolveMiddleware._put_cached_resource;
  s     s""#s'C'CC !4!4:]^K !D3#?#?1#DE 1##''401,6diik+JK(rc   c                    | j                  |      }|y|\  }}t        j                  d|j                  ||       |j	                  |       |j	                  |       y)z8Append a cached resource to output lists when available.F'[%s] resource cache hit: rid=%s path=%sT)r  r0  r  r  r   )r   r^  r  media_pathsmimeshitr  r  s           ra   _append_cached_resourcez.MediaResolveMiddleware._append_cached_resourceG
  s_     &&{3;
D5LL+z	
 	:&Trc   c                    t         j                  j                  |       j                  }t        j                  j                  |      d   j                         }|dv r|S y)z$Guess image extension from URL path.rV   >   .heic.tiff.bmp.gif.jpg.png.jpeg.webpr  )r  r  r  r  re  splitextrg  )r?  r  exts      ra   _guess_image_ext_from_urlz0MediaResolveMiddleware._guess_image_ext_from_url\
  sQ     ||$$S)..ggt$Q'--/VVJrc   c                *  K   |j                         }|st        d      | j                          d{   }t        |j	                  d      xs d      j                         }t        |j	                  d      xs d      j                         xs d}t        |j	                  d      xs | j
                  xs | j                        j                         }|r|st        d      | j                   d	}d
|||d}t        j                  dd      4 d{   }t        d      D ]  }	|j	                  |d|i|       d{   }
|
j                  dk(  r|	dk(  rt        j                  | j                  | j                  | j                         d{   }t        |j	                  d      xs d      j                         }t        |j	                  d      xs |xs d      j                         xs d}t        |j	                  d      xs | j
                  xs | j                        j                         }|r|s n||d<   ||d<   ||d<   /|
j                          |
j!                         }|j	                  d      }|dvr!t        d| d|j	                  dd             t#        |j	                  d      t$              r|j	                  d      n|}t        |xs i j	                  d      xs |xs i j	                  d      xs d      j                         }|r|c cddd      d{    S t        d       ddd      d{    t        d       7 17 O7 %7 7 87 # 1 d{  7  sw Y   t        d       xY ww)!zLow-level helper: exchange a ``resourceId`` for a direct download URL.

        Handles token retrieval, the ``/api/resource/v1/download`` API call,
        and a single 401-retry with token force-refresh.  Raises on failure.
        zmissing resource_idNrK  rn   rM  webr!  z-missing token or bot_id for resource downloadz/api/resource/v1/downloadr  )r  X-IDX-TokenX-Sourcer=   T)r  follow_redirectsrG   r  )paramsr  i  r   r  r  r  r  >   Nr   z"resource/v1/download failed: code=r#  r"  r   r?  realUrlz(resource/v1/download missing url/realUrlz)resource/v1/download did not return a URL)rh   r4  _get_cached_tokenr   r  rP  _app_key_api_domainr%  r&  r(  r3  r   rT  _app_secretraise_for_statusr  r7  r8  )r^  r  
token_datarK  rM  r!  api_urlr  r@  rA  resprB  r  r   real_urls                  ra   _fetch_resource_urlz*MediaResolveMiddleware._fetch_resource_urle
  sb     "'')455"4466
JNN7+1r288:Z^^H-67==?H5Z^^H-TTGDTDTU[[]FNOO(())BC.	
 $$TDI 	O 	OV 8 O#ZZ{8S]dZee##s*w!|'2'@'@(('*=*=w?R?R( "J  
w 7 =2>DDFE !9!LV!LuMSSU^Y^F !9!`W__!`PWP`P`aggiF &,GFO).GI&*0GJ'%%'))+{{6*y(&<TF&UZ\^I_H`a  /9V9Ld.Sw{{6*Y`
//6[4:2:J:J9:U[Y[\bbd#O9	O 	O 	O: ##MNN9O	O 	O> FGG] 7	Oe"	O 	O 	O 	O 	O> FGGs   1NM$C$NM'N)M4M*AM4M-FM4+N7M08N=M4	NM2N'N*M4-M40N2N4N:M=;NNc                  K   	 t         j                  j                  |      }t         j                  j	                  |j
                        }|j                  d      xs |j                  d      xs g }|rt        |d         j                         nd}|s|S 	 t        j                  | |       d{   S # t        $ r |cY S w xY w7 # t        $ r |cY S w xY ww)a"  Resolve Yuanbao resource placeholder to a directly fetchable real URL.

        Common URL patterns:
          https://hunyuan.tencent.com/api/resource/download?resourceId=...
        Direct GET returns 401; need business API:
          GET /api/resource/v1/download?resourceId=...
        r  r  r   rn   N)r  r  r  r5  r  r  r  r   rh   r  r  )r^  r?  r  r  resource_idsr  s         ra   _resolve_download_urlz,MediaResolveMiddleware._resolve_download_url
  s     	\\**3/F %%fll3yy.O%))L2IOR6Bc,q/*002J	/CCG[YYY  	J	 Z 	J	s^   CB7 A5CC
 2C3C
 6C7CCCCC
 
CCCCNrn   )r  log_tagr  c                 K   |r:| j                  |      }|'t        j                  d|j                  ||d          |S 	 t	        ||j
                         d{   \  }}	|dk(  rj| j                  |      }	 t        ||      }t        d|       }|j                  d	      s|	j                  d	      r|	nd
}| j                  |||       ||fS |dk(  rDt        |      }t        |      xs |	j                  d      r|	nd}| j                  |||       ||fS |sLt        j                   j#                  |      }t$        j&                  j)                  |j&                        xs d}	 t+        ||      }t        |      xs |	xs d}| j                  |||       ||fS 7 E# t        $ r-}
t        j                  d|j                  |||
       Y d}
~
yd}
~
ww xY w# t        $ r,}
t        j                  d|j                  ||
       Y d}
~
yd}
~
ww xY w# t        $ r,}
t        j                  d|j                  ||
       Y d}
~
yd}
~
ww xY ww)a  Download a Yuanbao resource and cache locally. Returns ``(local_path, mime)`` or ``None``.

        When *resource_id* is provided, an in-memory cache keyed by resourceId
        is consulted first to skip redundant downloads of the same resource
        within the TTL window.
        Nr  r   max_size_mbz5[%s] inbound media download failed: kind=%s %s err=%srQ   )r  z,[%s] inbound image cache rejected: %s err=%simage/
image/jpegrR   zvideo/z	video/mp4rP   z)[%s] inbound file cache failed: %s err=%sapplication/octet-stream)r  r0  r  r  media_download_urlMEDIA_MAX_SIZE_MBr5  r:  r  r   r6  r"   r]   r  r   r  r  r  re  r  basenamer   )r   r^  	fetch_urlr  r  r  r  r  
file_bytescontent_typerF  r  r  r  r  s                  ra   _download_and_cachez*MediaResolveMiddleware._download_and_cache
  sI     **;7C=LL+s1v 
		-?w'@'@. ($J 7?//	:C3JCH
 #U3%=1D??8,'3'>'>x'H|l$$[*dCt##7?/
;J":.  , 7 7 A{  $$[*dCt## \\**95F((5?I	2:yIJ y)W\W=W  j$?4c(  	NNGdGS 	  BLL'3 6  	NN;gs 	s   =I F" FF" "I9G C"I)H 5*IF" "	G+#GIGI	H$"HIHI	I"I>IIIc           	        K   g }g }g }|D ]  }t        |j                  d      xs d      j                         j                         }t        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }	|t        vs|st
        j                  |      }
|
r j                  |
||      r|j                  |||	|
xs df        |s||fS t        j                  j                        	 	 	 	 	 	 	 	 	 	 d fdt        j                         }t        j                  fd|D        ddi d	{   }t        t        j                         |z
  d
z        }d}t!        ||      D ]u  \  \  }}}}}t#        |t$              r,t&        j)                  dj*                  ||d	d |       |dz  }G||dz  }O|\  }}|j                  |       |j                  |       w t&        j-                  dj*                  j                  t/        |      t/        |      ||       ||fS 7 w)a  Resolve inbound media refs: download to local cache, return (local_paths, mime_types).

        Yuanbao COS hostnames resolve to private IPs, tripping the SSRF guard
        in vision_tools. We download ourselves and return local cache paths.

        Resolution runs with bounded concurrency
        (``adapter.media_resolve_concurrency``); see :meth:`_resolve_ybres_refs`
        for the same order-preserving / exception-isolated contract.
        r  rn   r?  r  c                  K   4 d {    	 j                  |       d {   }j                  || |xs d d|d d  |       d {   cd d d       d {    S 7 Y7 @# t        $ r>}t        j                  dj                  | ||       Y d }~d d d       d {  7   y d }~ww xY w7 c7 U# 1 d {  7  sw Y   y xY ww)Nz8[%s] inbound media resolve failed: kind=%s url=%s err=%szplaceholder_url=P   r  r  r  r  r  )r  r5  r0  r:  r  r  )	r  r?  r  r  r  rF  r^  r   	semaphores	         ra   _resolve_onez@MediaResolveMiddleware._resolve_media_urls.<locals>._resolve_one#  s      !   &)&?&?&M MI !44'&.$.s3Bxj9 # 5     M   NNRdC        s   CA$CB6A(A&A($B6B2B6CB4C&A((	B/1#B*B6C#B&$C*B//B64C6C<B?=CCc              3  >   K   | ]  \  }}}} ||||        y wr   r   )r   r  r?  r  r  r  s        ra   r   z=MediaResolveMiddleware._resolve_media_urls.<locals>.<genexpr>:  s,      6*$Xs 4h4 6s   return_exceptionsTNr  r   z9[%s] inbound media resolve crashed: kind=%s url=%s err=%sr  rV   z[[%s] media resolve batch: scope=media concurrency=%d total=%d ok=%d failed=%d elapsed_ms=%d)
r  r   r?  r   r  r   r  r   r   Optional[Tuple[str, str]])r   r  rh   rg  rf  r  r  r  r   r   	Semaphoremedia_resolve_concurrencyr  	monotonicgatherr   zipr7  BaseExceptionr0  r:  r  r1  rp   )r   r^  rp  rx  ry  activer  r  r?  r  r  _t0results_elapsed_ms_failed	_filename_ridr   r  r  r  r  s   ``                  @@ra   _resolve_media_urlsz*MediaResolveMiddleware._resolve_media_urls  sj     !#
!#24 		<Cswwv,"-335;;=Dcggen*+113C3776?0b1779H22#*==cBCs227C[YMM4h	r:;		< {**%%g&G&GH				+.	58	&	* nn6.46
 #
 

 4>>+c1T9:474H 	%0(T3	4&&-0OLL$CR& 1~1%Jj)t$	%" 	iLL--K
O	
 ;&&E
s   EI"I#C3Ic          	     D   K   g }g }g }|D ]8  \  }}}	|t         vr j                  |||      r%|j                  |||	f       : |s||fS t        j                  j
                        d fdt        j                         }
t        j                  fd|D        ddi d{   }t        t        j                         |
z
  dz        }d}t        ||      D ]r  \  \  }}}	}t        |t              r*t        j                  dj                  |||       |d	z  }D||d	z  }L|\  }}|j                  |       |j                  |       t t        j!                  d
j                  j
                  t#        |      t#        |      ||       ||fS 7 w)u9  Resolve ``(rid, kind, filename)`` ybres tuples to local paths.

        Runs with bounded concurrency (``adapter.media_resolve_concurrency``)
        so cold-start turns with many anchors don't pay ``N × (RPC + download)``
        sequentially. Output order matches input; per-rid failures are isolated.
        c           
       K   4 d {    	 j                  |        d {   }j                  |||xs d  d|  |        d {   cd d d       d {    S 7 X7 ?# t        $ r?}t        j                  dj                  | ||       Y d }~d d d       d {  7   y d }~ww xY w7 d7 V# 1 d {  7  sw Y   y xY ww)Nz-[%s] %s resolve failed: rid=%s kind=%s err=%sz rid=r  )r  r5  r0  r:  r  r  )	r  r  r  	fresh_urlrF  r^  r   
log_prefixr  s	        ra   r  z@MediaResolveMiddleware._resolve_ybres_refs.<locals>._resolve_one{  s         &)&=&=gs&K KI !44'&.$)l%u5 # 5     K   NNGj#tS        s   CA#CB6A'A%A'#B6B2B6CB4C%A''	B/0$B*B6C#B&$C*B//B64C6C<B?=CCc              3  :   K   | ]  \  }}} |||        y wr   r   )r   r  r  r  r  s       ra   r   z=MediaResolveMiddleware._resolve_ybres_refs.<locals>.<genexpr>  s!     R4GCxl3h/Rs   r  TNr  r   z.[%s] %s resolve crashed: rid=%s kind=%s err=%srV   z[[%s] media resolve batch: scope=ybres concurrency=%d total=%d ok=%d failed=%d elapsed_ms=%d)r  r   r  r   r  r   r   r  )rf  r  r   r   r  r  r  r  r  r   r  r7  r  r0  r:  r  r1  rp   )r   r^  r  r  r  r  r  r  r  r  r  r  r  r  r   r  r  r  r  s   `` `             @@ra   _resolve_ybres_refsz*MediaResolveMiddleware._resolve_ybres_refs]  s      "$-/#' 	1Cx22**7CeLMM3h/0	1 %%%%g&G&GH		 	( nnR6R
"
 
 4>>+c1T9:-0-A 	)!S$6&-0DLL*c4 1~1JD$t$LL	  	iLL--K	
 E!!A
s   B'F .F/C0F c                  K   t        |dd      }|sg g fS 	 |j                  |      }|j                  |j                        }|sg g fS |t         d }g }t               }	t        |      D ]  }
|
j                  d      }t        |t              rd|vr*t        t        j!                  |            }t        |      D ]  }|j#                  d      }|j#                  d      }|j%                  d      \  }}}|j'                         }|t(        vrS||	v rX|	j+                  |       |j-                  |||j'                         f       t/        |      t0        k\  s n t/        |      t0        k\  s n |j3                          |sg g fS | j5                  ||d	
       d{   S # t        $ r/}t
        j                  d|j                  |       g g fcY d}~S d}~ww xY w7 ?w)zYResolve recent observed image/file anchors from transcript into ``(local_paths, mimes)``.r0  Nz.[%s] Observed-media hydration setup failed: %sr3  r  rV   rG   ra  zobserved-mediar  )r4  r5  r7  r6  r5  r0  r:  r   OBSERVED_MEDIA_BACKFILL_LOOKBACKr   rc  r  r7  r   r`  rd  rt   r
  re  rh   rf  r   r   rp   ,OBSERVED_MEDIA_BACKFILL_MAX_RESOLVE_PER_TURNreverser  )r   r^  rM  r:  r;  rg  rF  ry   orderseenr"  r3  matchesr   r   r  r  r  r  s                      ra   _collect_observed_mediaz.MediaResolveMiddleware._collect_observed_media  s    
 !148r6M	!77?M++M,D,DEG r6M ::;<,.EF# 	Cggi(Ggs+y/G>227;<Gg& wwqzggaj$(NN3$7!azz|66$;c4)9:;u:!MM 5zII'	, 	r6M,,U'7 - 
 
 	
]  	NN@c r6M	\
sM   G$,F' DG$G$20G$"G"#G$'	G0$GGG$GG$c                F   K   | j                  ||d       d{   S 7 w)zResolve media anchors carried by the quoted message.

        ``quote_media_refs`` is a list of ``(rid, kind, filename)`` tuples
        produced by :class:`QuoteContextMiddleware` from the transcript.
        rY  r  N)r  )r   r^  rw  s      ra   _resolve_quote_mediaz+MediaResolveMiddleware._resolve_quote_media  s2      ,,%' - 
 
 	
 
s   !!c                   g }g }| j                   }|s||fS t        | j                  dd      }|s||fS |j                  |      }t	        |t
              r|s||fS t               }t        j                  |      D ]  }|j                  d      xs dj                         j                         }|j                  d      xs dj                         }	|	r|	|v r^t        j                  j                  |	      s~|j                  |	       t!        t        j                  j#                  |	            xs	 |dk(  rdnd}
|j%                  |	       |j%                  |
        ||fS )	u^  Private-chat fallback for recovering already-local quoted media.

        Only already-local media is handled here: by the time a turn is cached,
        ``PatchAnchorsMiddleware`` has rewritten resolved ``|ybres:`` anchors to
        ``[image: /path]`` / ``[file: name → /path]``. Unresolved anchors are an
        original-turn resolution failure and belong to that turn's handling, not
        this quote fallback — so no re-download happens here.

        Returns ``(local_paths, mimes)`` for media already downloaded to the
        local cache on its original turn, ready to inject as-is.
        r  NrV   rn   rG   rQ   r  r  )ru  r4  r^  r  r7  r   r   _YB_LOCAL_MEDIA_RErt   r
  rh   rg  re  r  r  r   r"   r  r   )r  pathsr  rid_keycacher^   r  r   r  r  r  s              ra   _collect_quote_local_mediaz1MediaResolveMiddleware._collect_quote_local_media  sI    ))%<%94@%<yy!$$D%< E#,,T2 	AGGAJ$"++-335DGGAJ$"++-D44<77>>$'HHTN"277#3#3D#9:  $5O  LLLL	 e|rc   c                T  	
K   |j                   }g 
g 	t               d	
fd}| j                  ||j                         d {   }t	        d |d   D              } ||       |j
                  O|j                  r+ || j                  ||j                         d {          nR || j                  |             n:|j                  dk(  r+	  || j                  ||j                         d {          
|_        	|_        t$        j'                  |j(                  |      r,t        j+                  d|j                  |j(                         y  |        d {    y 7 !7 7 w# t        $ r+}t        j                  d|j                  |       Y d }~d }~ww xY w7 Cw)Nc                    | \  }}t        ||      D ]?  \  }}|r|v rj                  |       j                  |       j                  |       A y r   )r  r   r   )
pair_listsu_listm_listur   r  typesr  s        ra   _add_unique_pairsz8MediaResolveMiddleware.handle.<locals>._add_unique_pairs2  sS    'NFFFF+  1AIAQ rc   c              3  &   K   | ]	  }|sd   yw)rV   Nr   )r   r  s     ra   r   z0MediaResolveMiddleware.handle.<locals>.<genexpr>=  s     5a15r  r   r
  z;[%s] observed-image hydration raised, continuing anyway: %sz.[%s] Skip placeholder after media download: %r)r
  Tuple[List[str], List[str]]r   r   )r^  r   r  rp  sumru  rw  r  r  rm  r  rM  r5  r0  r:  r  rx  ry  r  r  ro  r  )r  r  r  r^  r  	own_pairs	own_countrF  r  r  r  s           @@@ra   r  zMediaResolveMiddleware.handle'  sk    
 ++E	  227CNNKK	59Q<55	)$
 "".##!(A(A'3K_K_(`"`a "$"A"A#"FG]]g%!(D(DWcjj(Y"YZ  '??iXLLI7<<Y\YeYefiM L #a #Z QLL#   	sn   AF(E(AF("E+#0F(!E/ 5E-6E/ >A$F("F&#F(+F(-E/ /	F#8!FF(F##F()r  r   r   r  )r  r   r  r   r  r   r   r   )r  r   r  	List[str]r  r  r   r   r  )r  r   r   r   )r  r   r  r   r  rr  r  r   r  r   r   r  )rp  r  r   r  )r  rk  r  r   r   r  )r   r  )rw  rk  r   r  )r  r]  r   r  r  )r   r   r   r   r  r  rY  r  r  r   r  r  r  r   r  r  r  r  r  r  r  r  r  r  r   rc   ra   r  r  
  s   @D DFO@E+7=7.1m1   " 	K 	K   	
  
 (   8H 8Ht  0  $(F $'F /2F  F 25F  F  
#	F  F P X'"6X'	$X' X't R" )R"
 R" 
%R" R"h :
	$:
 :
x 

(B

	$

 

 ( (T;rc   r  c                  .    e Zd ZdZdZedd       ZddZy)PatchAnchorsMiddlewarea,  Replace ``[kind|ybres:RID]`` anchors in ``ctx.raw_text`` with local paths.

    Runs after :class:`MediaResolveMiddleware` so that ``ctx.media_urls`` /
    ``ctx.media_types`` are already populated with downloaded resources
    (own media + quote media or group-observed media).  The transcript
    written downstream then records usable local paths for the model
    instead of opaque ``ybres:`` references.

    Only resolved media (paths starting with ``/``) are substituted; any
    anchor without a corresponding local resource is left untouched.
    zpatch-anchorsc                *   | r|s| S | }t        ||      D ]  \  }}|j                  d      st        j                  |      }|s |S |j	                  d      }|j                  d      \  }}	}
|j                         }|dk(  r|j                  d      rd| d}nM|dk(  r;|
j                         xs t        j                  j                  |      }d	| d
| d}n|dk(  rd| d}n|d |j                          |z   ||j                         d  z   } |S )Nr  rV   ra  rQ   r  z[image: r  rP   r  u    → rR   z[video: )r  r]   rd  searchr
  re  rh   re  r  r  startru   )r^   r  r  patchedr  r   anchor_matchr   r  r  r  replacementlabels                ra   _patchzPatchAnchorsMiddleware._patcht  s;   4Ke$ 	DAq<<$)009L$ #  %%a(D $s 3D!X::<Dw1<<#9 (1o (?BGG,<,<Q,? 'weA3a8 (1o.,,,./,**,-./ %	. rc   c                   K   | j                  |j                  |j                  |j                        |_         |        d {    y 7 wr   )r  ro  rx  ry  r  s      ra   r  zPatchAnchorsMiddleware.handle  s/     {{3<<Qis   AAA	AN)r^   r   r  r  r  r  r   r   r  )r   r   r   r   r  r   r  r  r   rc   ra   r  r  e  s%    
 D :rc   r  c                  .    e Zd ZdZdZddZedd       Zy)DispatchMiddlewarez.Build MessageEvent and dispatch to AI handler.dispatchc                  	K   j                   	t        j                  	j                  j                  j                  dd      	j                  j                  j                  dd            d	fd}j                  dk(  r	j                  v}	j                  j                  t        j                               }|j                  |       t        j                  d	j                  |j                         xs d	d d
        |rt        j                   | j#                  	      dxs d	d d        }	j$                  j'                  |       |j)                  	j$                  j*                         nmt        j                    |       dj,                  xs d       }	j$                  j'                  |       |j)                  	j$                  j*                          |        d {    y 7 w)Ngroup_sessions_per_userTthread_sessions_per_userF)r%  r&  c                 p  K   t        j                  t        d j                  D              rt        j
                  n5t        j                  t              rj                  nt        j                  j                  j                  xs d j                  t        j                        t        j                        j                  j                  j                   
      } rBj                  r6j                  j"                  <   j                  xs dj$                  <   j                  raj                  rUj&                  }j                  |j                  <   t)        |      dkD  r"t        |      d t)        |      dz
   D ]  }||=  j+                  |        d {    y 7 w)Nc              3  >   K   | ]  }|j                  d         yw))zapplication/ztext/Nr]   )r   mts     ra   r   zMDispatchMiddleware.handle.<locals>._dispatch_inbound_event.<locals>.<genexpr>  s     ^2==)BC^s   )
r^   r!  rM  rD  raw_messagerx  ry  ru  rv  r{  rn   r  )r   ro  anyry  r   rR  r7  rt  r$  rM  rj  rc  r`  rx  ru  rv  r{  r  r&  r  rp   handle_message)eventr  r  _skr^  r  s      ra   _dispatch_inbound_eventz:DispatchMiddleware.handle.<locals>._dispatch_inbound_event  sP     \\ ^coo^^  ((
 *4CLL+)N$))zz::-HH/ 1$'$;$;!//"11%E( szz36::++C058\\5GR--c2zzcll22$'LLcjj!u:#!%[):#e*s*:; %!!H%((///s   F+F6.F4/F6r
  z-[%s] Group message enqueued (qsize=%d) for %srn   rS   zyuanbao-group-consumer-r#  r  zyuanbao-inbound-r/  r   )r^  r;   rM  configextrar  rm  _group_queues
setdefaultr   Queue
put_nowaitr0  r1  r  qsizer>  _consume_group_queue_inbound_tasksr   r@  rA  rj  )
r  r  r  r0  is_newqueueconsumerrB  r/  r^  s
    `      @@ra   r  zDispatchMiddleware.handle  s    ++JJ$+NN$8$8$<$<=VX\$]%,^^%9%9%=%=>XZ_%`
	0@ ==G# 5 55F))44S'--/JE45KK?ekkmciR"-= "..--gs;2CI2s3C2DE &&**84**7+A+A+I+IJ&&')'

(?i'@AD ""&&t,""7#9#9#A#ABis   G3H 8G>9H c                  K   d}| j                   j                  |      }|sy	 	 	 t        j                  |j                         |       d{   }t
        j                  d| j                  |xs ddd |j                                	  |        d{    || j                  v r,t        j                  d       d{    || j                  v r,7 # t        j                  $ r Y n7w xY w7 Z7 1# t        $ r# t
        j                  d| j                         Y Kw xY w	 | j                   j                  |d       y# | j                   j                  |d       w xY ww)	zIDrain the group queue one dispatch at a time, waiting for each to finish.rN   Nr  z3[%s] Group queue: dispatching for %s (remaining=%d)rn   rS   g?z[%s] Group queue consumer error)r3  r  r   wait_forTimeoutErrorr0  r  r  r7  r  r<  r5  	exceptionr  )r^  r(  _IDLE_TIMEOUTr;  dispatch_fns        ra   r8  z'DispatchMiddleware._consume_group_queue  sB     %%))+6	9(/(8(8m(\"\K ILL;#4"cr":EKKMV%-''%)A)AA%mmC000 &)A)AA "\++  (0  V$$%FUV  !!%%k48G!!%%k48s   !E"E (C CC 7E 
C4 C0*C4  C2C4 E C C-*E ,C--E 0C4 2C4 4)D E D  E $E"EE"Nr  )r^  'YuanbaoAdapter'r(  r   r   r   )r   r   r   r   r  r  r   r8  r   rc   ra   r"  r"    s%    8D@D 9 9rc   r"  c                  Z    e Zd ZU dZeeeeee	e
eeeeeeeeeeeeegZded<   edd       Zy)InboundPipelineBuilderzFactory for building InboundPipeline instances.

    Separates pipeline assembly (business knowledge) from the pipeline engine
    (InboundPipeline) so the engine stays generic and reusable.
    z
list[type]_DEFAULT_MIDDLEWARESc                h    t               }| j                  D ]  }|j                   |               |S )z6Build the default inbound message processing pipeline.)r  rF  r  )r   pipelinemw_clss      ra   buildzInboundPipelineBuilder.build  s4     #$.. 	#FLL"	#rc   N)r   r  )r   r   r   r   r  r  r   r  rL  rR  rz  r  r  r  r#  r(  r  rB  rL  rW  r8  r  r  r"  rF  rY  r   rJ  r   rc   ra   rE  rE    sh     	 #"%')(* .  rc   rE  c                      e Zd ZU dZddZed        Zedd       Zed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Zded<   d"dZd#dZd$dZef	 	 	 	 	 	 	 d%dZddZddZddZddZy)&ConnectionManagera  Manages the WebSocket connection lifecycle for YuanbaoAdapter.

    Responsibilities:
      - Opening and closing the WebSocket
      - AUTH_BIND handshake
      - Heartbeat (ping/pong) loop
      - Receive loop (frame dispatch)
      - Reconnect with exponential backoff
    c                    || _         d | _        d | _        d | _        d | _        i | _        d | _        d| _        d| _        d| _	        i | _
        i | _        y )Nr   F)_adapter_ws_connect_id_heartbeat_task
_recv_task_pending_acks_pending_pong_consecutive_hb_timeouts_reconnect_attempts_reconnecting_inbound_buffer_inbound_timersr  r^  s     ra   r  zConnectionManager.__init__+  s_    *.7;268:7;-.%() #(02?Arc   c                    | j                   S r   )rO  r  s    ra   wszConnectionManager.ws<  s    xxrc   c                    | j                   S r   )rP  r  s    ra   
connect_idzConnectionManager.connect_id@  s    rc   c                    | j                   S r   )rV  r  s    ra   reconnect_attemptsz$ConnectionManager.reconnect_attemptsD  s    '''rc   c                    | j                   yt        | j                   dd       }|du ryt        |      r	 t         |             S y# t        $ r Y yw xY w)NFrl  T)rO  r4  callabler   r5  )r  	open_attrs     ra   is_connectedzConnectionManager.is_connectedH  s_    88DHHfd3	IIK((   s   A 	AAc                <  K   | j                   }t        s8d}|j                  d|d       t        j	                  d|j
                  |       y|j                  r|j                  s8d}|j                  d|d       t        j                  d	|j
                  |       y| j                  O	 t        | j                  dd
      }|du st        |      r( |       r!t        j                  d|j
                         y|j                  d|j                  d      sy	 t        j                  d|j
                  |j                          t"        j%                  |j                  |j                  |j                   |j&                         d
{   }|j)                  d      rt+        |d         |_        t        j                  d|j
                  |j.                         t1        j2                  t5        j6                  |j.                  d
d
d      t8               d
{   | _	        | j;                  |       d
{   }|s| j=                          d
{    yd| _        |jA                          t1        jB                         |_"        t1        jF                  | jI                         d| jJ                         | _&        t1        jF                  | jO                         d| jJ                         | _(        t        j                  d|j
                  | jJ                  |j,                         tR        jU                  |       y# t        $ r Y Uw xY w7 7 ;7  7 	# t0        jV                  $ rL t        j                  d|j
                         | j=                          d
{  7   |jY                          Y yt        $ rV}t        j                  d|j
                  |d       | j=                          d
{  7   |jY                          Y d
}~yd
}~ww xY ww)u   Open WebSocket connection: sign-token → WS connect → AUTH_BIND → start loops.

        Returns True on success, False on failure.
        z:Yuanbao startup failed: 'websockets' package not installedyuanbao_missing_dependencyT)	retryablez$[%s] %s. Run: pip install websocketsFzJYuanbao startup failed: YUANBAO_APP_ID and YUANBAO_APP_SECRET are requiredyuanbao_missing_credentialsz[%s] %sNrl  z*[%s] Already connected, skipping connect()zyuanbao-app-keyzYuanbao app keyz [%s] Fetching sign token from %sr>  r!  z[%s] Connecting to %s   ping_intervalping_timeoutclose_timeoutr  r   yuanbao-heartbeat-r  yuanbao-recv-z%[%s] Connected. connectId=%s botId=%sz[%s] Connection timed outz[%s] connect() failed: %sr  )-rN  WEBSOCKETS_AVAILABLE_set_fatal_errorr0  r:  r  r  r  r  rO  r4  rb  r  r5  _acquire_platform_lockr1  r  r   rQ  
_route_envr  r   rP  _ws_urlr   r>  
websocketsconnectCONNECT_TIMEOUT_SECONDS_authenticate_cleanup_wsrV  _mark_connectedget_running_loop_loopr>  _heartbeat_looprP  rQ  _receive_looprR  YuanbaoAdapter
set_activer?  _release_platform_lock)r  r^  r"  rc  r  authedrF  s          ra   rl  zConnectionManager.openX  sx    
 --#NC$$%A3RV$WNNA7<<QTUw':':E  $$%BCSX$YLLGLL#6 88#DHHfd;	$))<LL!Mw||\
 --w//1B
 :	KK:GLL'J]J]^*44  '"5"5w7J7J!,,  5   J ~~h'"%j&:"; KK/wO$--""OO"&!%"#	 0 DH  --j99F&&((( ()D$##%#446GM#*#6#6$$&/A$BRBRAS-T$D  &11""$]4;K;K:L+MDO KK7d..
 %%g.q   :(, ## 	LL4gllC""$$$**, 	LL4gllCRVLW""$$$**,		s   B'P*AM 7PA/M MBM MM 7M8M MM PC)M  P	M
PMPM M M M AP#N&$P9P;P6P9O<:PPPPc                  K   | j                   r6| j                   j                          	 | j                    d{    d| _         | j                  r6| j                  j                          	 | j                   d{    d| _        t        d      }| j                  j                         D ]$  }|j                         r|j                  |       & | j                  j                          t        j                          | j                          d{    y7 # t        j                  $ r Y w xY w7 # t        j                  $ r Y w xY w7 ;w)zGCancel background tasks, fail pending futures, and close the WebSocket.NzYuanbaoAdapter disconnected)rQ  cancelr   CancelledErrorrR  r4  rS  valuesdoneset_exceptionr   r   r	  rz  )r  disc_excfuts      ra   closezConnectionManager.close  s(       '')**** $(D ??OO""$oo%% #DO   =>%%,,. 	,C88:!!(+	, 	  " 	!   / +))  &))  	!s   'ED DD -E,D7 ;D5<D7  ?E AEEED D2/E1D22E5D7 7E
EEEc                  K   | j                   }| j                  y|j                  dd      }|j                  xs |j                  dd      }|j                  d      xs d}|j                  xs |j                  dd      xs d}t        t        j                               }t        d	||||t        t        t        |
	      }| j                  j                  |       d{    t        j                  d|j                  ||       	 t!        j"                         }	|	j%                         t&        z   }
	 |
|	j%                         z
  }|dk  r!t        j)                  d|j                         yt!        j*                  | j                  j-                         |       d{   }t/        |t0        t2        f      s	 t5        t1        |            }|j                  di       }|j                  dd      }|j                  dd      }|t8        d   k(  rb|dk(  r]| j;                  |      }|r)|| _        t        j?                  d|j                  |       yt        j)                  d|j                         yB7 7 # t6        $ r Y Tw xY w# t         j@                  $ r# t        j)                  d|j                         Y yt6        $ r-}t        j)                  d|j                  |d       Y d}~yd}~ww xY ww)zSend AUTH_BIND and read frames until BIND_ACK is received.

        Returns True on success, False on failure/timeout.
        NFrK  rn   r!  rM  botr>  ybBot)	biz_iduidrM  rK  rj  app_versionoperation_systembot_versionr>  z&[%s] AUTH_BIND sent (msg_id=%s uid=%s)Tr   z+[%s] AUTH_BIND timeout waiting for BIND_ACKr  r   cmd_typere   r  Responsez	auth-bindz$[%s] BIND_ACK received: connectId=%sz[%s] BIND_ACK missing connectIdz[%s] AUTH_BIND timeoutz[%s] AUTH_BIND error: %sr  )!rN  rO  r  rP  rt  r   uuiduuid4r1   r,  r-  r/  r   r0  r  r  r   r|  r  AUTH_TIMEOUT_SECONDSr  r>  recvr7  r  	bytearrayr,   r5  r$   _extract_connect_idrP  r1  r?  )r  r  r^  rK  r  rM  r>  rj  
auth_bytesr}  deadliner   rawr"  r   r  r  r^  rF  s                      ra   ry  zConnectionManager._authenticate  s    
 --88w+oo="!=)2U&&O*..b*IOR	TZZ\"%$.$


 hhmmJ'''=w||VUXY%	,,.Ezz|&::H$uzz|3	>LL!NPWP\P\] #,,TXX]]_iPP!#y'9:)%*5C wwvr*88J3hhub)x
33{8J!%!9!9#!>J!+5($JGLLZde#%FU$7  	( Q ! " ## 	LL17<<@ 	LL3W\\3QULV	s   C K:"I8#&K:
A$J .K:/2J !I;"J >I= BJ K: J 5K:6J 8K:;J =	J
J 	J

J 3K7 K:K7
#K2-K:2K77K:c                   |j                  dd      }|sy	 t        t        |            }t        |d      }|dk7  r9t	        |d      }t
        j                  d| j                  j                  ||       yt	        |d      }|r|S dS # t        $ r5}t
        j                  d	| j                  j                  |       Y d}~yd}~ww xY w)
z0Extract connectId from decoded BIND_ACK message.r   rc   NrV   r   rG   z*[%s] AuthBindRsp error: code=%d message=%rr   z$[%s] Failed to extract connectId: %s)r  r%   r(   r'   r&   r0  r  rN  r  r5  r:  )r  decoded_msgr   fdictr  messager^  rF  s           ra   r  z%ConnectionManager._extract_connect_id   s    !oofc2	#M$$78Eua(Dqy%eQ/@MM&&g $UA.J!+:55 	NNA4==CUCUWZ[	s$   AB 5B B 	C+C  Cc                d  K   | j                   }	 |j                  r/t        j                  t               d{    | j
                  ;	 t        t        j                               }t        |      }t        j                         }|j                         }|| _        || j                  |<   | j
                  j                  |       d{    t        j!                  d|j"                  |       	 t        j$                  |d       d{    d| _        | j                  j+                  |d       d| _        	 |j                  r.yy7 7 7 D# t        j(                  $ r | j                  j+                  |d       | xj&                  dz  c_        t        j-                  d|j"                  | j&                  t.               | j&                  t.        k\  rUt        j-                  d|j"                         | j1                          Y | j                  j+                  |d       d| _        yY w xY w# | j                  j+                  |d       d| _        w xY w# t2        $ r,}t        j!                  d	|j"                  |       Y d}~Rd}~ww xY w# t        j4                  $ r Y yw xY ww)
zJSend HEARTBEAT (ping) every 30s; trigger reconnect after threshold misses.Nz[%s] PING sent (msg_id=%s)r>   r  r   rV   z[%s] PONG timeout (%d/%d)z7[%s] Heartbeat threshold exceeded, triggering reconnectz[%s] Heartbeat send failed: %s)rN  _runningr   r<  HEARTBEAT_INTERVAL_SECONDSrO  r   r  r  r2   r|  create_futurerT  rS  r   r0  r  r  r>  rU  r?  r  r:  HEARTBEAT_TIMEOUT_THRESHOLDschedule_reconnectr5  r  )r  r^  rj  
ping_byteslooppong_futurerF  s          ra   r~  z!ConnectionManager._heartbeat_loop7  s2    --"	""mm$>???88#V .F!,V!4J"335D262D2D2FK)4D&1<D&&v.((--
333LL!=w||VT2%..{DIII895 **..vt<-1*; ""? 4 J"// 
#**..vt<55:57#LL$*G*GId  88<WW"NN+dfmfrfrs 335"**..vt<-1* X
# **..vt<-1*  VLL!A7<<QTUUV%% 		s   J0)J EJ B I E%I 3E EE #I <J J0J I E B7H4H7 #I /J00H7 3H44H7 7%II 	J(!J	J JJ J-*J0,J--J0c                  K   | j                   }	 | j                  2 3 d{   }t        |t        t        f      s | j                  t        |             d{    C7 >7 6 y# t        j                  $ r Y yt        j                  j                  $ r}t        |dd      }t        j                  d|j                  |t        |dd             |r:|t        v r2t        j!                  d|j                  |       |j#                          n| j%                          Y d}~yY d}~yd}~wt&        $ r;}t        j                  d|j                  |       | j%                          Y d}~yd}~ww xY ww)z(Read WS frames and dispatch by cmd_type.Nr  z3[%s] WebSocket connection closed: code=%s reason=%sreasonrn   z7[%s] Close code %d is non-recoverable, NOT reconnectingz[%s] receive_loop exited: %s)rN  rO  r7  r  r  _handle_framer   r  rv  
exceptionsConnectionClosedr4  r0  r:  r  NO_RECONNECT_CLOSE_CODESr  _mark_disconnectedr  r5  )r  r^  r  	close_exc
close_coderF  s         ra   r  zConnectionManager._receive_loop`  s7    --	&!XX 5 5c!#y'9:((s4445 5 & %% 	$$55 	* FD9JNNEj')Xr*J j,DDMLL* **,'')) -  	&NN97<<M##%%	&s   E0A% A#AA#5A% A!A% A#!A% #A% $E0%E-8E0:E-BD&
E0&E-21E(#E0(E--E0c           	       K   | j                   }	 t        |      }|j                  di       }|j                  dd      }|j                  dd      }|j                  dd      }|j                  d	d
      }	|j                  dd      }
|t        d   k(  r|dk(  rt        j	                  d|j
                  |       | j                  6| j                  j                         s| j                  j                  d       y|rJ|| j                  v r<| j                  j                  |      }|j                         s|j                  d       y|t        d   k(  r'|dv r#t        j	                  d|j
                  ||       y|t        d   k(  r{|rV|| j                  v rH| j                  j                  |      }|j                         sd|i}|
r|
|d<   |j                  |       yt        j	                  d|j
                  ||       y|t        d   k(  rt        j                  d|j
                  ||t        |
             |	r;| j                  /	 t!        |      }| j                  j#                  |       d{    |r^|| j                  v rP| j                  j                  |      }|j                         s$	 |
rt%        |
      nd|i}|j                  |       yy|
r<t        j                  d|j
                  |t        |
             | j)                  |
       yt        j	                  d|j
                  |||       y# t        $ r+}t        j	                  d|j
                  |       Y d}~yd}~ww xY w7 # t        $ r,}t        j	                  d|j
                  |       Y d}~,d}~ww xY w# t        $ r}|j'                  |       Y d}~yd}~ww xY ww)z Handle a single WebSocket frame.z[%s] Failed to decode frame: %sNr   r  re   r  rn   rj  need_ackFr   rc   r  pingz'[%s] HEARTBEAT_ACK received (msg_id=%s)T>   send_group_heartbeatsend_private_heartbeatz-[%s] Heartbeat ACK received: cmd=%s msg_id=%sz)[%s] Unmatched Response: cmd=%s msg_id=%sPushz0[%s] Push received: cmd=%s msg_id=%s data_len=%dz[%s] Failed to send PushAck: %szL[%s] WS received inbound push, decoding and dispatching: cmd=%s, data_len=%dz1[%s] Ignoring frame: cmd_type=%d cmd=%s msg_id=%s)rN  r,   r5  r0  r  r  r  r$   rT  r  
set_resultrS  r  r1  rp   rO  r3   r   r-   r  _push_to_inbound)r  r  r^  r"  rF  r   r  r  rj  r  r   r  r   	ack_bytesack_excdecodeds                   ra   r  zConnectionManager._handle_frame|  so    --	!#&C
 wwvr"88J+hhub!(B'88J.ggfc* x
++vLLBGLLRXY!!-d6H6H6M6M6O""--d3
 	 Fd&8&88((,,V4xxzNN4( x
++ 8
 1
 LLH',,X[]cd x
++&D$6$66((,,V4xxz$d^F)-vNN6* 	 ?LL#v  x''KKJGLLZ]_egjkogpqDHH0[ / 5I((--	222 &D$6$66((,,V4xxz/?C"5d";&RVw/  bLL#s4y %%d+?LL(C	
Y  	LL:GLL#N	j 3  [LL!BGLLRYZZ[ % /))#../s   OL; H.O
)M4 3M24M4 8;O4"N, A%O;	M/!M*%O*M//O2M4 4	N)=!N$O$N))O,	O5OOOOg      ?float_DEBOUNCE_WINDOWc                   	 t        j                  |j                  d            }t        |t              ri|j                  dd      xs |j                  dd      }|j                  dd      xs& |j                  dd      xs |j                  dd      }|r| d| S 	 t        |      }|r'|j                  dd       d|j                  dd       S 	 d	t        |       S # t        $ r Y Ow xY w# t        $ r Y (w xY w)
zLightweight decode to extract sender key for debounce grouping.

        Returns 'from_account:group_code' or a fallback unique key.
        r  re  rn   r  rf  r  r  ra  
__unknown_)	r  r  r  r7  r8  r  r5  r-   rZ  )r  r  r  re  rf  rc  s         ra   _extract_sender_keyz%ConnectionManager._extract_sender_key  s   
	ZZ 89F&$'JJ~r2 6zz."5 
 JJ|R0 2zz)R02zz*b1 
  *^1ZL99	&x0D((>267q,PR9S8TUU 
 BxL>**  		  		s$   BC#  3C2 #	C/.C/2	C>=C>c           	        | j                  |      }| j                  j                  |d      }|r|j                          || j                  vrg | j                  |<   | j                  |   j                  |       t        j                  d| j                  j                  |t        | j                  |                t        j                         }|j                  | j                  | j                  |      }|| j                  |<   y)a=  Debounced inbound dispatch.

        Buffers raw frames from the same sender within a short time window,
        then dispatches all buffered data as a single aggregated pipeline
        execution.  This merges multi-part messages (e.g. image + text sent
        as separate WS pushes) into one pipeline run.
        Nz2[%s] Debounce: buffered frame for key=%s, count=%d)r  rY  r  r  rX  r   r0  r  rN  r  rp   r   r|  
call_laterr  _flush_inbound_buffer)r  r  r  existing_timerr  timers         ra   r  z"ConnectionManager._push_to_inbound  s     &&x0 --11#t<!!# d***(*D  %S!((2@MMS)=)=c)B%C	
 '')!!&&

 %*S!rc   c                   | j                   j                  |d       | j                  j                  |g       }|sy| j                  }t        j                  d|j                  |t        |             t        ||      }|j                  t        j                  |j                  j                  |      d|              y)uC   Flush the debounce buffer for a given key — execute the pipeline.Nz1[%s] Debounce flush: key=%s, aggregated %d frames)r^  ra  zyuanbao-pipeline-r  )rY  r  rX  rN  r0  r1  r  rp   r]  r  r   r>  _inbound_pipeliner  )r  r  r  r^  r  s        ra   r  z'ConnectionManager._flush_inbound_buffer  s      d+((,,S"5	--?LL#s9~	

 WCG//%%--c2$SE*
 	rc   c                  K   | j                   t        d      t        j                         }|j	                         }|| j
                  |<   	 | j                   j                  |       d{    t        j                  t        j                  |      |       d{   }|| j
                  j                  |d       S 7 T7 $# t        j                  $ r  t        $ r  w xY w# | j
                  j                  |d       w xY ww)a	  Send a business-layer request and wait for the response.

        1. Register a Future in pending_acks[req_id]
        2. Send encoded_conn_msg (bytes) to WS
        3. asyncio.wait_for(future, timeout)
        4. Clean up pending_acks on timeout/exception
        NNot connectedr  )rO  r4  r   r|  r  rS  r   r>  shieldr  r?  r5  )r  encoded_conn_msgreq_idr  r  futurer   s          ra   send_biz_requestz"ConnectionManager.send_biz_request-  s      88//'')!%!3!3!5%+6"		1((-- 0111"++GNN6,BGTTF ""640 2T## 	 		 ""640sN   ADC ,C-1C CC $DC C C##C& &DDc                    | j                   j                  r1| j                  s$t        j                  | j                                yyy)zBSchedule a reconnect only if running and not already reconnecting.N)rN  r  rW  r   r>  _reconnect_with_backoffr  s    ra   r  z$ConnectionManager.schedule_reconnectM  s6    ==!!$*<*< < < >? +=!rc   c                   K   | j                   r+t        j                  d| j                  j                         yd| _         	 | j                          d{   d| _         S 7 # d| _         w xY ww)u?   Reconnect with exponential backoff (1s, 2s, 4s, … up to 60s).z,[%s] Reconnect already in progress, skippingFTN)rW  r0  r  rN  r  _do_reconnectr  s    ra   r  z)ConnectionManager._reconnect_with_backoffR  s]     LLGI[I[\!	'++--!&D .!&Ds/   ?A/A# A!A# A/!A# #	A,,A/c           	     x  K   | j                   }t        t              D ]  }|dz   | _        t	        d|z  d      }t
        j                  d|j                  |dz   t        |       t        j                  |       d{    | j                          d{    	 t        j                  |j                  |j                  |j                  |j                          d{   }|j#                  d      rt%        |d         |_        t        j(                  t+        j,                  |j.                  ddd	      t0        
       d{   | _        | j5                  |       d{   }|s>t
        j7                  d|j                  |dz          | j                          d{    d| _        d| _        |j;                          | j<                  r4| j<                  j?                         s| j<                  jA                          t        jB                  | jE                         d| jF                         | _        | jH                  r4| jH                  j?                         s| jH                  jA                          t        jB                  | jK                         d| jF                         | _$        t
        j                  d|j                  |dz   | jF                         tL        jO                  |        y t
        jU                  d|j                  t               |jW                          y7 7 7 Z7 7 7 # t        jP                  $ r( t
        j7                  d|j                  |dz          Y atR        $ r0}t
        j7                  d|j                  |dz   |       Y d}~d}~ww xY ww)z>Internal reconnect loop, called under the _reconnecting guard.rV   rG   r   z#[%s] Reconnect attempt %d/%d in %dsNri  r!  rj  rk  r  z![%s] Re-auth failed on attempt %dr   ro  r  rp  z,[%s] Reconnected on attempt %d. connectId=%sTz#[%s] Reconnect attempt %d timed outz$[%s] Reconnect attempt %d failed: %sz*[%s] Giving up after %d reconnect attemptsF),rN  r(  MAX_RECONNECT_ATTEMPTSrV  minr0  r1  r  r   r<  rz  r   rT  r  r  r  rt  r  r   rP  r>  rv  rw  ru  rx  rO  ry  r:  rU  r{  rQ  r  r  r>  r~  rP  rR  r  r  r  r?  r5  r  r  )r  r^  rA  waitr  r  rF  s          ra   r  zConnectionManager._do_reconnect]  s#    --34 A	G'.{D$qG|R(DKK5gk+A4 --%%%""$$$6#.#<#<$$g&9&97;N;N%00 $= $ 
 >>(+&)*X*>&?GO!(!1!1&&&*%)&'	 4"   $11*==NN#FV]`aVab**,,,+,(01-'')''0D0D0I0I0K((//1'.':':((*-d.>.>-?@($
 ??4??+?+?+AOO**,")"5"5&&(()9)9(:;#
 BLL'A+t/?/? ))'2uA	F 	8',,H^	
 	""$A &$ > -8 '' aDgllT[^_T_` :GLL'TU+WZ s   A;N:=L3>N:L6N:AML9 A'ML<M%L?&=M#M$M(N:*EM::N:6N:9M<M?MM7N7<N:?N7%N2,N:2N77N:c                D  K   | j                   }d| _         |3	 t        j                  |j                         t               d{    yy7 # t        j
                  $ r2 t        j                  d| j                  j                  t               Y yt        $ r Y yw xY ww)zClose and clear the WebSocket connection, bounded by
        ``WS_CLOSE_TIMEOUT_S`` so an unresponsive server can't stall teardown
        (see the constant's definition for the full rationale).Nr  u>   [%s] WS close handshake exceeded %.1fs — dropping connection)rO  r   r>  r  WS_CLOSE_TIMEOUT_Sr?  r0  r  rN  r  r5  )r  r\  s     ra   rz  zConnectionManager._cleanup_ws  s      XX>&&rxxz;MNNN N''  TMM&&(:  sG   B ,A AA 
B A ABB BB BB Nr^  rC  r   r   )r   rr  rW  rx  r   )r  r8  r   r   )r  r8  r   rr  )r  r  r   r   )r  r  r   r   )r  r  r   r   )r  r   r   r   )r  r  r  r   r  r  r   r8  )r   r   r   r   r  r  r\  r^  r`  rd  rl  r  ry  r  r~  r  r  r  rY  r  r  r  DEFAULT_SEND_TIMEOUTr  r  r  r  rz  r   rc   ra   rL  rL     s    B"       ( (  `D!BCJ.%R&8T
p "e!+> *D4 .	11 1 	1
 
1@@
	'JXrc   rL  c                  l    e Zd ZdZe	 	 	 	 	 	 dd       Zedd       Zd	dZ	 	 d
	 	 	 	 	 	 	 	 	 	 	 ddZy)MediaSendHandleru{  Abstract base class for media send strategies.

    Subclasses implement:
      - acquire_file(): how to obtain file bytes (download URL / read local)
      - build_msg_body(): how to build TIMxxxElem from upload result

    The shared flow (check ws → cancel notifier → validate → COS upload
    → lock → dispatch) is handled by the base handle() template method.
    c                   K   yw)zReturn (file_bytes, filename, content_type).

        Raises:
            ValueError: when file cannot be acquired (not found, empty, etc.)
        Nr   r  r^  kwargss      ra   acquire_filezMediaSendHandler.acquire_file  r  r  c                     y)z<Build platform-specific MsgBody list from COS upload result.Nr   r  upload_resultr  s      ra   build_msg_bodyzMediaSendHandler.build_msg_body  s    rc   c                     y)z:Override to return False for non-COS media (e.g. sticker).Tr   r  s    ra   needs_cos_uploadz!MediaSendHandler.needs_cos_upload  s    rc   Nc           	     d  K   |j                   }|j                  j                  }|j                  t	        ddd      S |j                  j                  |       	  | j                  |fi | d{   \  }}	}
| j                         r0t        j                  ||	|j                        }|rt	        d|      S | j                         rt        |      }|j                          d{   }|j                  dd      }|j                  d	d      xs |j                  xs d}t        |j                   |j"                  ||	||j$                  
       d{   }t'        ||	|
||d   |d          d{   }|j)                         D ci c]  \  }}|dvr|| }}} | j*                  |f||	|
d|}n | j*                  i fi |}|r|j-                  dd|id       |j                  dd      }|j/                  ||||       d{   S 7 7 -7 7 c c}}w 7 # t0        $ r }t	        dt3        |            cY d}~S d}~wt4        $ rY}t7        |       j8                  }t:        j=                  d|j>                  ||d       t	        dt3        |            cY d}~S d}~ww xY ww)z(Template method: shared media send flow.NFr  Tsuccessr  rg  r  r  rK  rn   r!  )r   r=  rK  r  r!  r>  
bucketNameregion)r  r  r  credentialsbucketr  >   r  	file_uuidr  )r  r  r  r  r^   r  rf  rf  z[%s] %s.handle() failed: %sr  ) _connectionrp  r]  r\  r   cancel_slow_notifierr  r  MessageSendervalidate_mediar  r#   r  r  rP  r   r  r  rt  r   r  r  r   dispatch_msg_bodyr6  r   r5  rt  r   r0  r  r  )r  r^  rl  reply_tocaptionr  connr]  r  r  r  validation_errr  r  rK  r!  r  r  r  r  
fwd_kwargsri  gcverF  handler_names                             ra   r  zMediaSendHandler.handle  s     """"))77?e?dSS..w7P	=7Ht7H7H8!8 2.J, $$&!.!=!='*C*C" "%e>JJ$$&#J/	 $+#<#<#>>
'^^GR8NN8R0IGOOIr  %8#,,&22%!%00%  '4)%!- +&|4&x0' ! &,\\^!Q II qD
  /4..!'%!-	
 ! /4..r<V< !.?PQ
 L"-B11'8XZ\1]]]I2$ ?!. ^ 	<e3r7;; 	=:..LLL-lC$   e3s8<<	=s   AJ0H$ 1H2AH$ 9J0:.H$ (H)A,H$ HH$ 4H5H$ HA0H$ H"H$ J0H$ H$ H$ H$ H$ $	J--IJ-J0J-AJ("J-#J0(J--J0)r^  rC  r  r   r   zTuple[bytes, str, str])r  r8  r  r   r   r`  rx  r  )r^  rC  rl  r   r  rr  r  rr  r  r   r   'SendResult')	r   r   r   r   r
   r  r  r  r  r   rc   ra   r  r    s     '36	  K K #'!%a=!a= a=  	a=
 a= a= 
a=rc   r  c                      e Zd ZdZd Zd Zy)ImageUrlHandleruD   Strategy: send image from a URL (download → COS → TIMImageElem).c                f  K   |d   }t         j                  d|j                  |       t        ||j                         d {   \  }}|r|dk(  r#|j                  d      d   }t        |      xs d}t        j                  j                  |j                  d      d         xs d}|||fS 7 kw)	Nr  z$[%s] ImageUrlHandler: downloading %sr  r  ?r   r  	image.jpg)
r0  r1  r  r  r  r\   r"   re  r  r  )r  r^  r  r  r  r  	path_partr  s           ra   r  zImageUrlHandler.acquire_fileA  s     ,	:GLL)T);7#<#<*
 $
 
L |/II!,Q/I*95EL77##IOOC$8$;<K8\11$
s   AB1B/A,B1c                    t        |d   |d   |d   |d   |j                  dd      |j                  dd      |d   	      S 
Nr?  r  r  sizewidthr   heightr  )r?  r  r  r  r  r  	mime_typer    r  r  s      ra   r  zImageUrlHandler.build_msg_bodyM  W    #e$$J'v&##GQ/ $$Xq1^,
 	
rc   Nr   r   r   r   r  r  r   rc   ra   r  r  >  s    N
2	
rc   r  c                      e Zd ZdZd Zd Zy)ImageFileHandleruL   Strategy: send image from a local file path (read → COS → TIMImageElem).c                  K   |d   }t         j                  j                  |      st        d|       t        j                  d|j                  |       t        |d      5 }|j                         }d d d        t         j                  j                  |      xs d}t        |      xs d}||fS # 1 sw Y   @xY ww)N
image_pathFile not found: z![%s] ImageFileHandler: reading %srbr  r  )re  r  r  r6  r0  r1  r  rl  readr  r"   )r  r^  r  r  r  r  r  r  s           ra   r  zImageFileHandler.acquire_file\  s      .
ww~~j)/
|<==7zR*d# 	"qJ	"77##J/>;&x0@L8\11		" 	"s   A B>"B23?B>2B;7B>c                    t        |d   |d   |d   |d   |j                  dd      |j                  dd      |d   	      S r  r  r  s      ra   r  zImageFileHandler.build_msg_bodyg  r  rc   Nr  r   rc   ra   r  r  Y  s    V	2	
rc   r  c                      e Zd ZdZd Zd Zy)FileUrlHandleruB   Strategy: send file from a URL (download → COS → TIMFileElem).c                h  K   |d   }t         j                  d|j                  |       t        ||j                         d {   \  }}|j                  d      }|s7|j                  d      d   }t        j                  j                  |      xs d}|r|dk(  rt        |      xs d}|||fS 7 lw)	Nr  z#[%s] FileUrlHandler: downloading %sr  r  r  r   rP   r  )r0  r1  r  r  r  r  r\   re  r  r  r"   )r  r^  r  r  r  r  r  r	  s           ra   r  zFileUrlHandler.acquire_filev  s     z*97<<R);'";";*
 $
 
L ::j) s+A.Iww''	2<fH|/II*84R8RL8\11$
s   AB2B0A-B2c                8    t        |d   |d   |d   |d         S Nr?  r  r  r  )r?  r  r  r  r!   r  s      ra   r  zFileUrlHandler.build_msg_body  /    "e$J'$v&	
 	
rc   Nr  r   rc   ra   r  r  s  s    L2
rc   r  c                      e Zd ZdZd Zd Zy)DocumentHandleruB   Strategy: send local file/document (read → COS → TIMFileElem).c                  K   |d   }t         j                  j                  |      st        d|       t        j                  d|j                  |       t        |d      5 }|j                         }d d d        |j                  d      xs# t         j                  j                  |      xs d}t        |      xs d}||fS # 1 sw Y   SxY ww)N	file_pathr  z [%s] DocumentHandler: reading %sr  r  rw  r  )re  r  r  r6  r0  r1  r  rl  r  r  r  r"   )r  r^  r  r%  r  r  r  r  s           ra   r  zDocumentHandler.acquire_file  s     ,	ww~~i(/	{;<<6iP)T" 	"aJ	"::j)VRWW-=-=i-HVJ&x0N4N8\11		" 	"s   A C"C3ACC
Cc                8    t        |d   |d   |d   |d         S r  r   r  s      ra   r  zDocumentHandler.build_msg_body  r!  rc   Nr  r   rc   ra   r#  r#    s    L	2
rc   r#  c                  $    e Zd ZdZddZd Zd Zy)StickerHandlerzAStrategy: send sticker/emoji (TIMFaceElem, no COS upload needed).c                     y)NFr   r  s    ra   r  zStickerHandler.needs_cos_upload  s    rc   c                   K   yw)N)rc   stickerr  r   r  s      ra   r  zStickerHandler.acquire_file  s	     9r  c                    ddl m}m}m}m} |j                  d      }|j                  d      }|  ||      }	|	t        d|       ||	      S |	 ||      S  |       }	 ||	      S )Nr   )get_sticker_by_nameget_random_stickerbuild_face_msg_bodybuild_sticker_msg_bodysticker_name
face_indexzSticker not found: )r2  )!gateway.platforms.yuanbao_stickerr-  r.  r/  r0  r  r6  )
r  r  r  r-  r.  r/  r0  r1  r2  r+  s
             ra   r  zStickerHandler.build_msg_body  s    	
 	
 zz.1ZZ-
#),7G #6|6F!GHH)'22#&*==(*G)'22rc   Nrx  )r   r   r   r   r  r  r  r   rc   ra   r(  r(    s    K:3rc   r(  c                  ^    e Zd ZdZddZd	dZ	 d
	 	 	 	 	 	 	 ddZddZ	 	 d	 	 	 	 	 	 	 ddZy)GroupQueryServiceaN  Encapsulates all group query operations (both low-level WS calls and
    higher-level AI-tool-facing wrappers).

    Responsibilities:
      - Low-level WS encode/decode for group info and member list queries
      - Chat-id parsing, error wrapping and result filtering for AI tools
      - Member cache population on the adapter
    c                    || _         y r   )rN  rZ  s     ra   r  zGroupQueryService.__init__  s	    rc   c                  K   | j                   }|j                  j                  yt        |      }ddlm}  ||      }|d   d   }	 |j                  j                  ||       d{   }|j                  di       }|j                  dd      }	|	dk7  r"t        j                  d|j                  |	       y|j                  d	d
      xs |j                  dd
      }
|
rt        |
t              rt        |
      S d|iS 7 # t        j                  $ r$ t        j                  d|j                  |       Y yt         $ r+}t        j                  d|j                  |       Y d}~yd}~ww xY ww)zQuery group info via WS (group name, owner, member count, etc.).

        Returns:
            Decoded dict or None on failure.
        Nr   r,   r   rj  r  statusz'[%s] query_group_info failed: status=%dr   rc   rD  rf  z'[%s] query_group_info timeout: group=%sz [%s] query_group_info failed: %s)rN  r  r\  r8   gateway.platforms.yuanbao_protor,   r  r  r0  r:  r  r7  r  r/   r   r?  r5  )r  rf  r^  encoded_decoder  r  rC  r   r:  biz_datarF  s               ra   query_group_info_rawz&GroupQueryService.query_group_info_raw  sG     --!!))*5N'"*	$00AA'RXAYYH<<+DXXh*F{H',,X^_||FC0MHLL4MHJx728<< *-- Z ## 	NNDgllT^_ 	NN=w||SQ	sh   AE- D (C?)AD 7E-8AD :E-;D >E-?D 4E*5E-7E*?!E% E-%E**E-c                `  K   | j                   }|j                  j                  yt        |||      }ddlm}  ||      }|d   d   }	 |j                  j                  ||       d{   }	|	j                  di       }
|
j                  dd      }|dk7  r"t        j                  d	|j                  |       y|	j                  d
d      xs |	j                  dd      }|rt        |t              rt        |      }ng ddd}|r7|j                  d      r&t        j                         |d   f|j                  |<   |S 7 # t         j"                  $ r$ t        j                  d|j                  |       Y yt$        $ r+}t        j                  d|j                  |       Y d}~yd}~ww xY ww)zQuery group member list via WS.

        Returns:
            Decoded dict or None on failure.  Also populates adapter._member_cache.
        Noffsetlimitr   r8  r   rj  r9  r:  z,[%s] get_group_member_list failed: status=%dr   rc   rD  T)membersnext_offsetis_completerD  z,[%s] get_group_member_list timeout: group=%sz%[%s] get_group_member_list failed: %s)rN  r  r\  r9   r;  r,   r  r  r0  r:  r  r7  r  r0   r  _member_cacher   r?  r5  )r  rf  rB  rC  r^  r<  r=  r  r  rC  r   r:  r>  r   rF  s                  ra   get_group_member_list_rawz+GroupQueryService.get_group_member_list_raw  s     --!!).z&PUVN'"*	$00AA'RXAYYH<<+DXXh*F{Mw||]cd||FC0MHLL4MHJx79(C%'$O&**Y/59YY[&BS4T%%j1M Z ## 	NNI7<<Ycd 	NNBGLLRUV	s\   AF. E +E ,AE :F.;BE ?F. E 4F+6F.8F+ !F&!F.&F++F.c                   K   |j                  d      sddiS |t        d      d }| j                  |       d{   }|ddiS |S 7 w)zAI tool: Query current group info.

        No parameters needed (group_code extracted from session context).
        Returns group name, owner, member count, etc.
        r  r  -This command is only available in group chatsNzFailed to query group info)r]   rp   r?  )r  rl  rf  r   s       ra   query_group_infoz"GroupQueryService.query_group_info  s`      !!(+LMMS]^,
00<<>9:: =s   8A	AA	Nc                x  K   |j                  d      sddiS |t        d      d }| j                  |       d{   }|ddiS |j                  dg       }|dk(  r|r|j	                         }|D cg c]v  }||j                  dd	      xs d	j	                         v sL||j                  d
d	      xs d	j	                         v s&||j                  dd	      xs d	j	                         v r|x }}n;|dk(  r6|D cg c]+  }d|j                  dd	      xs d	j	                         v s*|- }}d	}	|rgt        |      dk  rY|D cg c]:  }|j                  d
      xs% |j                  d      xs |j                  dd	      < }
}ddj                  |
      z   }	|dd t        |      |	dS 7 nc c}w c c}w c c}w w)a\  AI tool: Query group member list.

        Args:
            chat_id: Chat ID (extracted from session context)
            action: 'find' (search by name) | 'list_bots' (list bots) | 'list_all' (list all)
            name: Search keyword when action='find'

        Returns:
            {"members": [...], "total": int, "mentionHint": str}
        r  r  rJ  NzFailed to query group membersrD  findr  rn   	name_cardr  	list_botsr  
   zMention with @name: z, rS   )rD  totalmentionHint)r]   rp   rH  r  rg  r   )r  rl  actionr  rf  r   rD  r  r   mention_hintnamess              ra   query_session_membersz'GroupQueryService.query_session_members*  s      !!(+LMMS]^,
55jAA><==**Y+VJJLE"QUU:r28b??AAQUU;39r@@BBQUU9b17R>>@@ G  {"")\QUquuZ7L7RPR6Y6Y6[-[q\G\ s7|r)^efYZQUU;'T155+<TiQS@TTfEf1DIIe4DDL s|\'
 	
/ B ]
 gs@   8F:F(7F:2A;F+-F:9+F0%F0)F:?F5 )F:+F:r  rf  r   r   rb  r   r  rf  r   rB  r   rC  r   r   rb  )rl  r   r   r8  )list_allN)rl  r   rS  r   r  rr  r   r8  )	r   r   r   r   r  r?  rH  rK  rV  r   rc   ra   r5  r5    sv     @ >A##'*#7:#	#R" !"	.
.
 .
 	.

 
.
rc   r5  c                  B    e Zd ZdZd	dZd
dZddZddZdddZddZ	y)HeartbeatManagerzManages reply heartbeat (RUNNING / FINISH) lifecycle.

    Responsibilities:
      - Periodic RUNNING heartbeat sender (every 2s)
      - Auto-FINISH after 30s inactivity
      - Explicit stop with optional FINISH signal
    c                .    || _         i | _        i | _        y r   )rN  _reply_heartbeat_tasks_reply_hb_last_activerZ  s     ra   r  zHeartbeatManager.__init__d  s    ?A#79"rc   c                H  K   | j                   }|j                  }|j                  |j                  sy	 |j	                  d      r'|t        d      d }t        |j                  ||      }n)|j                  d      }t        |j                  ||      }|j                  j                  |       d{    |t        k(  rdnd}t        j                  d|j                  ||       y7 4# t        $ r+}	t        j                  d	|j                  |	       Y d}	~	yd}	~	ww xY ww)
z9Send a single heartbeat (RUNNING or FINISH), best effort.Nr  )re  rf  rq  r	  )re  r  rq  RUNNINGFINISHz%[%s] Reply heartbeat %s sent: chat=%sz#[%s] send_heartbeat_once failed: %s)rN  r  r\  rP  r]   rp   r7   removeprefixr6   r   r)   r0  r  r  r5  )
r  rl  heartbeat_valr^  r  rf  r<  r  status_namerF  s
             ra   rr  z$HeartbeatManager.send_heartbeat_oncei  s    --""77?'//	S!!(+$S]^4
5!()+ %11)<
7!()+
 '',,w''''48L'L)RZKLL7k7 (  	SLL>cRR	SsA   2D"A?C+ 4C)53C+ (D")C+ +	D4!DD"DD"c                  K   | j                   }|j                  }|j                  |j                  sy| j                  j                  |      }|r2|j                         s"t        j                         | j                  |<   yt        j                         | j                  |<   t        j                  | j                  |      d|       }|| j                  |<   yw)zGStart or renew the Reply Heartbeat periodic sender (RUNNING, every 2s).Nzyuanbao-reply-hb-r  )rN  r  r\  rP  r^  r  r  r  r_  r   r>  _worker)r  rl  r^  r  existingrB  s         ra   r  zHeartbeatManager.start  s     --""77?'//..227;HMMO26))+D&&w/.2iik""7+""LL!$WI.
 04##G,s   CCc                  K   	 | j                  |t               d{    	 t        j                  t               d{    | j
                  j                  |d      }t        j                         |z
  t        kD  rnB| j                  j                  }|j                  n| j                  |t               d{    d}|s	 | j                  |t               d{    | j                  j!                  |d       | j
                  j!                  |d       y7 7 7 e# t        j                  $ r d}Y wt        $ r d}Y w xY w7 j# t        $ r Y sw xY w# s0	 | j                  |t               d{  7   n# t        $ r Y nw xY w| j                  j!                  |d       | j
                  j!                  |d       w xY ww)ztBackground coroutine: send RUNNING heartbeat every 2s.
        30s without renewal -> send FINISH and exit.
        NTr   F)rr  r)   r   r<  REPLY_HEARTBEAT_INTERVAL_Sr_  r  r  REPLY_HEARTBEAT_TIMEOUT_SrN  r  r\  r  r5  r*   r^  r  )r  rl  last_activer  	cancelleds        ra   rg  zHeartbeatManager._worker  s    	:**74HIIImm$>???"88<<WaH99;,/HH}}0077?..w8LMMM $ I227<OPPP ''++GT:&&**7D99 J @ N%% 	I 	I	 Q   227<OPPP  ''++GT:&&**7D9s   G
D# D!D# D A;D# ;D!<D# E G
E E
 E $9G
D# D# !D# #E8E :
EE EE 
E 	EG
EG
GE?8E;9E?>G?	FG
F<GG
c                :  K   | j                   j                  |d      }|r+|j                         s|j                          	 | d{    |r 	 | j                  |t               d{    yy7 '# t        j
                  $ r Y :w xY w7 !# t        $ r Y yw xY ww)z0Stop Reply Heartbeat and optionally send FINISH.N)	r^  r  r  r  r   r  rr  r*   r5  )r  rl  send_finishrB  s       ra   stopzHeartbeatManager.stop  s     **..w=		KKM

 ..w8KLLL  ))  M sq   ?BA1 A/A1 BB (B
)B -B/A1 1BBBB
B 	BBBBc                  K   t        | j                  j                               D ]#  }|j                         r|j	                          % | j                  j                          | j                  j                          yw)z!Cancel all reply heartbeat tasks.N)r`  r^  r  r  r  r   r_  r  rB  s     ra   r  zHeartbeatManager.close  sa     44;;=> 	D99;	 	##))+""((*s   7BABNr  )rl  r   rd  r   r   r   rl  r   r   r   )Trl  r   ro  r   r   r   r   )
r   r   r   r   r  rr  r  rg  rp  r  r   rc   ra   r\  r\  [  s'    :
S<4(!:F+rc   r\  c                  8    e Zd ZdZddZd	dZd	dZd	dZd
dZy)SlowResponseNotifierzManages delayed 'please wait' notifications for slow agent responses.

    Starts a timer per chat_id; if the agent hasn't replied within
    SLOW_RESPONSE_TIMEOUT_S seconds, sends a courtesy message.
    c                .    || _         || _        i | _        y r   )rN  _sender_tasks)r  r^  r]  s      ra   r  zSlowResponseNotifier.__init__  s    /1rc   c                   K   | j                  |       t        j                  | j                  |      d|       }|| j                  |<   yw)zCStart a delayed task that notifies the user when the agent is slow.zyuanbao-slow-resp-r  N)r  r   r>  	_notifierry  r  rl  rB  s      ra   r  zSlowResponseNotifier.start  sH     G""NN7#%gY/
  $Gs   AAc                  K   	 t        j                  t               d{    t        j	                  d| j
                  j                  t        t              |       | j                  j                  |t               d{    y7 f7 # t         j                  $ r Y yt        $ r5}t        j                  d| j
                  j                  |       Y d}~yd}~ww xY ww)z@Wait SLOW_RESPONSE_TIMEOUT_S, then push a 'please wait' message.Nz<[%s] Agent response exceeded %ds for %s, sending wait noticez&[%s] Slow-response notifier failed: %s)r   r<  SLOW_RESPONSE_TIMEOUT_Sr0  r1  rN  r  r   rx  send_text_chunkSLOW_RESPONSE_MESSAGEr  r5  r  )r  rl  rF  s      ra   r{  zSlowResponseNotifier._notifier  s     
	\-- 7888KKN""C(?$@' ,,..w8MNNN 9
 O%% 	 	\LLA4==CUCUWZ[[	\s]   C B BA B B	B C B 	B CC  C(+CC CC c                    | j                   j                  |d      }|r"|j                         s|j                          yyy)z@Cancel the pending slow-response notifier for *chat_id*, if any.N)ry  r  r  r  r|  s      ra   r  zSlowResponseNotifier.cancel  s1    {{w-		KKM $4rc   c                   K   t        | j                  j                               D ]#  }|j                         r|j	                          % | j                  j                          yw)zCancel all slow-response tasks.N)r`  ry  r  r  r  r   rr  s     ra   r  zSlowResponseNotifier.close  sK     ++-. 	D99;	 	s
   7A'-A'N)r^  rC  r]  z'MessageSender'r   r   rs  r   )	r   r   r   r   r  r  r{  r  r  r   rc   ra   rv  rv    s!    2
$\rc   rv  c                  
   e Zd ZU dZ eh d      Zded<   dZded<   ddZdd	Z	dd
Z
	 	 d	 	 	 	 	 	 	 	 	 d dZ	 	 d!	 	 	 	 	 	 	 	 	 	 	 d"dZ	 d#	 	 	 	 	 	 	 d$dZ	 	 d	 	 	 	 	 	 	 	 	 d%dZ	 	 	 d&	 	 	 	 	 	 	 	 	 	 	 d'dZd(d)dZ	 d#	 	 	 	 	 	 	 d*dZ ej&                  dej(                        Zd+dZd(d,dZ	 d#	 	 	 	 	 	 	 d-dZe	 	 	 	 	 	 	 	 d.d       Ze	 d/	 	 	 	 	 	 	 d0d       Ze	 	 d1	 	 	 	 	 	 	 d2d       Zed3d       Zd4dZy)5r  ac  Core message sending dispatcher for YuanbaoAdapter.

    Responsibilities:
      - Per-chat-id lock management (serial send ordering)
      - Text chunk sending with retry
      - C2C / Group message encoding and dispatch
      - Media send helpers (image, file, sticker, document)
      - Direct send helper (text + media, used by send_message tool)
    >   r  r  r  r  r  r  zClassVar[frozenset]
IMAGE_EXTSr  r  CHAT_DICT_MAX_SIZEc                    || _         t        j                         | _        d | _        d | _        t               t               t               t               t               d| _        y )N)r  
image_filer  rw  r+  )rN  collectionsOrderedDict_chat_locks_on_send_start_on_send_finishr  r  r  r#  r(  _media_handlersrZ  s     ra   r  zMessageSender.__init__  sW    GRG^G^G` ?C?C )**,&(')%'=
rc   c                "    || j                   |<   y)z1Register (or replace) a named media send handler.N)r  )r  r  r  s      ra   register_handlerzMessageSender.register_handler(  s    %,T"rc   c                8   || j                   v r*| j                   j                  |       | j                   |   S t        | j                         | j                  k\  rd}t	        | j                         D ]>  }| j                   |   j                         r!| j                   j                  |       d} n |s7| j                   j                  t        t        | j                                      t        j                         | j                   |<   | j                   |   S )z=Return (or create) a per-chat-id lock with safe LRU eviction.FT)r  move_to_endrp   r  r`  lockedr  r  iterr   r   )r  rl  evictedr  s       ra   get_chat_lockzMessageSender.get_chat_lock.  s    d&&&((1##G,,t D$;$;;GD,,- '',335$$((-"G	
   $$T$t/?/?*@%AB$+LLN!((rc   Nc                  K   | j                   }|j                  }|j                  t        ddd      S | j                  r| j	                  |       | j                  |      }|4 d{    | j                  |      }| j                  ||j                        }	t        j                  d|j                  t        |      |j                  t        |	      |	D 
cg c]  }
t        |
       c}
       t        |	      D ]L  \  }}|dk(  r|nd}| j                  ||||       d{   }|j                  r9|c cddd      d{    S  ddd      d{    | j                   r	 | j!                  |       d{    t        d	      S 7 %c c}
w 7 q7 R7 B# 1 d{  7  sw Y   RxY w7 3# t"        $ r Y <w xY ww)
zHSend text message with auto-chunking and per-chat-id ordering guarantee.NFr  Tr  zJ[%s] truncate_message: input=%d chars, max=%d, output=%d chunk(s) sizes=%sr   r  )r  )rN  r  r\  r   r  r  strip_cron_wrappertruncate_messageMAX_TEXT_CHUNKr0  r1  r  rp   r   r  r  r  r5  )r  rl  r3  r  rf  r^  r  lockcontent_to_sendr   r   r   r   r_tor   s                  ra   	send_textzMessageSender.send_textA  s     --""77?e?dSS(!!'* 	" 	""55g>O**?G<R<RSFKK\c/2G4J4JFf5c!f5
 &f- "5#$6xt#33GUDU_3``~~!M	" 	" 	""	" 	" **7333 $'')	" 6 a	" 	" 	" 	" 	"" 4 s   A'G)F*G-A,FF+9F$F%F6F9GFGFGFG)F3 =F1>F3 GFGGF."F%#F.*G1F3 3	F?<G>F??Gc                   K   | j                   j                  |      }|t        dd|      S  |j                  | j                  |f||d| d{   S 7 w)2Dispatch media send to the named handler strategy.NFzUnknown media handler: r  )r  r  )r  r  r   r  rN  )r  rl  r  r  r  r  r  s          ra   
send_mediazMessageSender.send_mediah  sx      &&**<8?//?@  $W^^MM7
w
28
 
 	
 
   AAAAc                   K   | j                   }d}|j                         r7|j                  ||       d{   }|j                  sdd|j                   iS |xs g D ]  \  }}t        |      j                  j                         }|| j                  v r|j                  ||       d{   }n|j                  ||       d{   }|j                  rydd|j                   ic S  |ddiS dd||r|j                  dS ddS 7 7 _7 Fw)	a@  Send text + media via Yuanbao (used by the ``send_message`` tool).

        Unlike Weixin which creates a fresh adapter per call, Yuanbao reuses
        the running gateway adapter (persistent WebSocket).  Logic mirrors
        send_weixin_direct: send text first, then iterate media_files by
        extension.
        Nr  zYuanbao send failed: zYuanbao media send failed: z6No deliverable text or media remained after processingTrb  )r  platformrl  rD  )rN  rh   r   r  r  r   suffixrg  r  send_image_filesend_documentrD  )	r  rl  r  media_filesr^  last_result
media_path	_is_voicer  s	            ra   send_directzMessageSender.send_direct~  s;     --.2 ==? 'Wg >>K&&#89J9J8K!LMM &1%6B 	T!J	z"))//1Cdoo%$+$;$;GZ$PP$+$9$9':$NN&&#>{?P?P>Q!RSS	T UVV !4?+00	
 	
 FJ	
 	
% ? QNs:   4DDA3D*D
+DDD2D
DDc                   K   | j                  |      }|4 d{    |j                  d      r*|t        d      d }| j                  |||       d{   }n-|j	                  d      }| j                  |||       d{   }ddd      d{    j                  d      rt        d|j                  d            S t        d	|j                  d
d            S 7 7 7 b7 T# 1 d{  7  sw Y   dxY ww)z5Lock + dispatch an arbitrary MsgBody to C2C or group.Nr  r	  r  r  Tr  r  rD  Fr  Unknown errorr  )r  r]   rp   send_group_msg_bodyrc  send_c2c_msg_bodyr  r   )	r  rl  ri  r  rf  r  grpr   r  s	            ra   r  zMessageSender.dispatch_msg_body  s      !!'* 	c 	c!!(+c(mn-#77XxPP$11)<
#55j(Wa5bb	c 	c ::i dvzz)7LMM%vzz'?/STT	c Q c	c 	c 	c 	csi   C>C!C>6C)C#-C)C%C)C>C'AC>#C)%C)'C>)C;/C20C;7C>c           	     6  K   | j                   }d}t        |      D ]  }	 |j                  d      r*|t        d      d }	| j	                  |	||       d{   }
n-|j                  d      }| j                  |||       d{   }
|
j                  d      rt        d|
j                  d      	      c S |
j                  d
d      }t        j                  d|j                  |dz   ||       ||dz
  k  st        j                  d|z         d{     t        j!                  d|j                  ||       t        dd|       S 7 7 # t        $ r;}t        |      }t        j                  d|j                  |dz   ||       Y d}~d}~ww xY w7 w)zFSend a single text chunk with retry (exponential backoff: 1s, 2s, 4s).r  r  Nr	  r  r  Tr  r  r  z-[%s] send_text_chunk attempt %d/%d failed: %srV   z0[%s] send_text_chunk attempt %d/%d exception: %srG   z>[%s] send_text_chunk max retries (%d) exceeded. Last error: %sFzMax retries exceeded: r  )rN  r(  r]   rp   send_group_messagerc  send_c2c_messager  r   r0  r:  r  r5  r   r   r<  r  )r  rl  r^   r  retryrf  r^  
last_errorrA  r  r  r  rF  s                ra   r  zMessageSender.send_text_chunk  s     --)
U| 	2G%%h/!#h-.1C $ 7 7T8 LLC!(!5!5i!@J $ 5 5j$S] 5 ^^C779%%dswwy?QRR WWWo>
CLL'A+uj "mmAL1113	26 	LLL%	
 %1G
|/TUU7 M _   X
FLL'A+uj  2sp   F5EE-EE0E4F78E/F8FF8FEE	F1F
FFFc                V   K   dd|idg}| j                  |||       d{   S 7 w)z<Send C2C text message, return {success: bool, msg_key: str}.r  r^   r  r  N)r  )r  r  r^   rf  ri  s        ra   r  zMessageSender.send_c2c_message  s5     !.~NO++JZ+XXXXs    )')c                h   K   | j                  ||      }| j                  |||       d{   S 7 w)zDSend group text message, auto-converting @nickname to TIMCustomElem.N)_build_msg_body_with_mentionsr  )r  rf  r^   r  ri  s        ra   r  z MessageSender.send_group_message  s5      55dJG--j(HMMMMs   )202z!(?:(?<=\s)|(?<=^))@(\S+?)(?=\s|$)c                *   | j                   j                  j                  |      }|r8|\  }}t        j                         |z
  | j                   j                  k  r|ng }ng }|sdd|idgS i }|D ]Z  }|j                  d      xs |j                  d      xs d}	|j                  d      xs d}
|	sC|
sF|	|
f||	j                         <   \ g }d}| j                  j                  |      D ]  }|j                         }||kD  r+||| j                         }|r|j                  dd|id       |j                  d	      }|j                  |j                               }|r6|\  }}
|j                  d
dt        j                  dd| |
d      id       n|j                  ddd| id       |j                         } |t        |      k  r+||d j                         }|r|j                  dd|id       |s|j                  dd|id       |S )zNParse @nickname patterns and build mixed TIMTextElem + TIMCustomElem msg_body.r  r^   r  r  r  rn   r  r   rV   r  r   r  @)r  r^   r  N)rN  rG  r  r  MEMBER_CACHE_TTL_Srg  _AT_USER_RErt   r  rh   r   r
  r  dumpsru   rp   )r  r^   rf  rO  r  member_listrD  nickname_to_uidr   nickr  ri  last_idxr   r  segr  r  	real_nicktails                       ra   r  z+MessageSender._build_msg_body_with_mentions  s    ,,00<$OB&*iikB&69Y9Y&Yk`bGG!.~NOO 	<A55$@k(:@bD%%	"(bC15s

-		< %%..t4 	#EKKMEx8E*002OOPVX[}$]^{{1~H#''(89E!&	3 /

9+cf+g h$!  ]FVWX`WaTbKc deyy{H)	#, c$i	?((*D]FTX> Z[OOPT~VWrc   c                   K   | j                   }dt                }t        |||j                  xs d||      }| j	                  |||       d{   S 7 w)z(Send C2C message with arbitrary MsgBody.c2c_rn   )r  ri  re  rj  rf  N)rN  r:   r4   rP  _dispatch_encoded)r  r  ri  rf  r^  r  r<  s          ra   r  zMessageSender.send_c2c_msg_body2  s\     --')! .B!
 ++GWfEEEEs   AAAAc                   K   | j                   }dt                }t        |||j                  xs d||xs d      }| j	                  |||       d{   S 7 w)z*Send group message with arbitrary MsgBody.grp_rn   )rf  ri  re  rj  
ref_msg_idN)rN  r:   r5   rP  r  )r  rf  ri  r  r^  r  r<  s          ra   r  z!MessageSender.send_group_msg_body?  sb      --'+! .B~2
 ++GWfEEEEr  c                  K   	 | j                   j                  ||       d{   }d|j                  dd      dS 7 # t        j                  $ r ddt
         d	d
cY S t        $ r}dt        |      d
cY d}~S d}~ww xY ww)zBSend pre-encoded bytes via WS and return a normalised result dict.r9  NTrj  rn   )r  r  FzRequest timeout after sr  )r  r  r  r   r?  r  r5  r   )r^  r<  r  rC  rF  s        ra   r  zMessageSender._dispatch_encodedS  s     
	9$00AA'RXAYYH#Xr0JKK Z## 	a$1GH\G]]^/_`` 	9$s3x88	9sM   B A  >A  BA    B B"B*A=7B8B=BBc                    | t        |       dk(  rd| S |dz  dz  }t        |       |kD  rt        |       dz  dz  }d| d|dd| d	S y)
zMedia pre-validation: check file validity before sending/uploading.

        Returns:
            Error description (str) if validation fails, otherwise None.
        Nr   zEmpty file: i   zFile too large: z (z.1fzMB > zMB))rp   )r  r  r  	max_bytessize_mbs        ra   r  zMessageSender.validate_mediab  sq     ZA!5!(,,$&-	z?Y&*o,t3G%hZr'#eK=PSTTrc   c                    |xs t         } ||       |k  r| gS t        j                  | ||      }|D cg c]  }t        j	                  d|       }}|r|S | gS c c}w )aj  
        Split a long message into chunks with table-awareness.

        Delegates core splitting to ``MarkdownProcessor.chunk_markdown_text``
        and strips page indicators like ``(1/3)`` from the output.

        Falls back to ``BasePlatformAdapter.truncate_message`` for non-table
        content and for overall text that fits in a single chunk.
        r   rn   )rp   rX   r   _INDICATOR_REsub)r3  
max_lengthrw   rx   r   r   s         ra   r  zMessageSender.truncate_messageu  su     }=J&9 #66Z 7 

 5;;q-##B*;;v.gY. <s   Ac                    | j                  d      s| S d}d}| j                  |      }| j                  |      }|dk  s
|dk  s||k  r| S | d| }d|vr| S |t        |      z   }| || j	                         }|xs | S )zFStrip scheduler cron header/footer wrapper for cleaner Yuanbao output.zCronjob Response: z
-------------

zI

To stop or manage this job, send me a new message (e.g. "stop reminder r   Nz

(job_id: )r]   rM  rq   rp   rh   )r3  dividerfooter_prefixdivider_pos
footer_posr  
body_startrD  s           ra   r  z MessageSender.strip_cron_wrapper  s     !!"67N'ell7+]]=1
?j1n
k0IN+&&N 3w</
z*-335wrc   c                @   K   | j                   j                          yw)zCRelease chat locks (no-op for now; placeholder for future cleanup).N)r  r   r  s    ra   r  zMessageSender.close  s         r  )r  r   r  r  r   r   rl  r   r   rU  r  
rl  r   r3  r   r  rr  rf  r   r   r  r  )rl  r   r  r   r  rr  r  rr  r  r   r   r  r   rl  r   r  r   r   Optional[List[Tuple[str, bool]]]r   Dict[str, Any])
rl  r   ri  r`  r  rr  rf  r   r   r  )Nr   rn   )rl  r   r^   r   r  rr  r  r   rf  r   r   r  rX  )r  r   r^   r   rf  r   r   r8  )rf  r   r^   r   r  rr  r   r8  )r^   r   rf  r   r   r`  )r  r   ri  r`  rf  r   r   r8  )rf  r   ri  r`  r  rr  r   r8  )r^  rC  r<  r  r  r   r   r8     r  zOptional[bytes]r  r   r  r   r   rr  r   )r3  r   r  r   rw   r   r   r  )r3  r   r   r   r   )r   r   r   r   rJ  r  rY  r  r  r  r  r  r  r  r  r  r  r  rr   rs   	MULTILINEr  r  r  r  r   r  r  r  r  r  r   rc   ra   r  r    s    '00b&cJ#c(,,
&-). #'%(%( %(  	%(
 %( 
%(V #'!%

 
  	

 
 
 

4 9=	)
)
 )
 6	)

 
)
^ #'UU U  	U
 U 
U2 #'*V*V *V  	*V
 *V *V 
*V\Y #'	NN N  	N
 
N "**A2<<PK2hF" #'	FF F  	F
 
F( 
9!
9,1
9;>
9	
9 
9 GI#/2AD	 $  15/// // 
	/ /<  ,!rc   r  c                     e Zd ZU dZej
                  Zded<   ddZddZddZ		 	 d	 	 	 	 	 	 	 	 	 ddZ
	 	 	 	 	 	 	 	 dd	Z	 d	 	 	 	 	 	 	 dd
ZddZdddZddZddZddZedd       Ze	 d	 	 	 	 	 	 	 dd       Zd dZy)!OutboundManageru  Outbound coordinator that orchestrates sending, heartbeat and slow-response.

    Composes:
      - MessageSender   — core text/media sending
      - HeartbeatManager — reply heartbeat (RUNNING / FINISH) lifecycle
      - SlowResponseNotifier — delayed 'please wait' notifications

    YuanbaoAdapter holds a single ``_outbound: OutboundManager`` and delegates
    all outbound operations through it.
    r  r  c                    || _         t        |      | _        t        |      | _        t        || j                        | _        | j                  | j                  _        | j                  | j                  _
        y r   )rN  r  r]  r\  rq  rv  slow_notifier_handle_send_startr  _handle_send_finishr  rZ  s     ra   r  zOutboundManager.__init__  s]    %27%;+;G+D3GQUQ\Q\3] &*%<%<"&*&>&>#rc   c                :    | j                   j                  |       y)zFCalled by MessageSender before sending: cancel slow-response notifier.Nr  r  r  rl  s     ra   r  z"OutboundManager._handle_send_start      !!'*rc   c                `   K   | j                   j                  |t               d{    y7 w)z=Called by MessageSender after sending: send FINISH heartbeat.N)rq  rr  r*   r  s     ra   r  z#OutboundManager._handle_send_finish  s!     nn00:MNNNs   $.,.Nc                \   K   | j                   j                  ||||       d{   S 7 w)z%Send text message with auto-chunking.r  N)r]  r  )r  rl  r3  r  rf  s        ra   r  zOutboundManager.send_text  s-     
 [[**7GXR\*]]]]   #,*,c                Z   K    | j                   j                  ||fi | d{   S 7 w)r  N)r]  r  )r  rl  r  r  s       ra   r  zOutboundManager.send_media  s,      ,T[[++G\LVLLLL   "+)+c                X   K   | j                   j                  |||       d{   S 7 w)z.Send text + media (used by send_message tool).N)r]  r  )r  rl  r  r  s       ra   r  zOutboundManager.send_direct  s'     
 [[,,Wg{KKKK   !*(*c                V   K   | j                   j                  |       d{    y7 w)z Start reply heartbeat (RUNNING).N)rq  r  r  s     ra   start_typingzOutboundManager.start_typing  s     nn""7+++   )')c                Z   K   | j                   j                  ||       d{    y7 w)zStop reply heartbeat.ro  N)rq  rp  )r  rl  ro  s      ra   stop_typingzOutboundManager.stop_typing  s#     nn!!'{!CCCs   !+)+c                V   K   | j                   j                  |       d{    y7 w)zStart slow-response notifier.N)r  r  r  s     ra   start_slow_notifierz#OutboundManager.start_slow_notifier  s        &&w///r  c                :    | j                   j                  |       y)zCancel slow-response notifier.Nr  r  s     ra   r  z$OutboundManager.cancel_slow_notifier  r  rc   c                8    | j                   j                  |      S )z@Proxy to MessageSender.get_chat_lock for backward compatibility.)r]  r  r  s     ra   r  zOutboundManager.get_chat_lock  s    {{((11rc   c                .    | j                   j                  S )z>Proxy to MessageSender._chat_locks for backward compatibility.)r]  r  r  s    ra   r  zOutboundManager._chat_locks  s     {{&&&rc   c                0    t         j                  | ||      S )z&Proxy to MessageSender.validate_media.)r  r  )r  r  r  s      ra   r  zOutboundManager.validate_media  s    
 ++J+NNrc   c                   K   | j                   j                          d{    | j                  j                          d{    | j                  j                          d{    y7 I7 )7 	w)zShut down all sub-managers.N)r]  r  rq  r  r  s    ra   r  zOutboundManager.close	  sW     kk!!!nn""$$$  &&((( 	"$(s3   A0A*!A0A,!A0$A.%A0,A0.A0r  rs  r  r  )rl  r   r  r   r  r   r   r  r   r  )Frt  r  )r   zcollections.OrderedDictr  r  r   )r   r   r   r   r  r  rY  r  r  r  r  r  r  r  r  r  r  r  r  r  r   r  r  r   rc   ra   r  r    s4   	 )6(H(HH?+O EI^^%(^4A^^ 
^MM*-M9<M	M 9=LL%(L5L 
L,D0+2 ' ' GIO#O/2OADO	O O)rc   r  c                  6    e Zd ZU dZej
                  ZdZded<   dZ	dZ
ded<   dZd	ed
<   dZded<   ed(d       Zed)d       Zd* fdZd+dZed,d       Zd-dZddd.dZd/dZ	 	 	 d0	 	 	 	 	 	 	 	 	 	 	 d1dZd2dZd3d4dZd5dZd6 fdZd7dZ	 d8	 	 	 	 	 	 	 d9dZdZd:d;d Z	 	 	 d<	 	 	 	 	 	 	 	 	 	 	 	 	 d=d!Z 	 	 	 d<	 	 	 	 	 	 	 	 	 	 	 	 	 d>d"Z!	 	 	 d<	 	 	 	 	 	 	 	 	 	 	 	 	 d?d#Z"	 	 	 d<	 	 	 	 	 	 	 	 	 	 	 d@d$Z#	 	 	 	 dA	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dBd%Z$dCd&Z%dCd'Z& xZ'S )Dr  zCYuanbao AI Bot adapter backed by a persistent WebSocket connection.r   r   r  TrS   r  i  r  REPLY_REF_MAX_ENTRIESNz$ClassVar[Optional['YuanbaoAdapter']]_active_instancec                    | j                   S )z7Return the currently connected YuanbaoAdapter, or None.r  r  s    ra   
get_activezYuanbaoAdapter.get_active  s     ###rc   c                    || _         y)z0Register (or clear) the active adapter instance.Nr  )r   r^  s     ra   r  zYuanbaoAdapter.set_active"  s      'rc   c                   t         |   |t        j                         |j                  xs i }|j                  d      xs dj                         | _        |j                  d      xs dj                         | _        |j                  d      xs d | _	        |j                  d      xs t        j                         | _        |j                  d      xs t        j                  d      | _        |j                  d      xs dj                         | _        	 t!        |j                  d	t"                    }t)        t*        t-        t.        |            | _        t3        |       | _        t7        |       | _        t;               | _        t;               | _        i | _         d
| _!        tE        d      | _#        i | _$        i | _%        i | _&        i | _'        |j                  d      xs tQ        jR                  dd      j                         jU                         }|j                  d      xs tQ        jR                  dd      }|jW                  d      D cg c]#  }|j                         s|j                         % }}|j                  d      xs tQ        jR                  dd      j                         jU                         }	|j                  d      xs tQ        jR                  dd      }
|
jW                  d      D cg c]#  }|j                         s|j                         % }}tY        |||	|      | _-        t]        |       | _/        t`        jc                         | _2        tQ        jR                  d      xs$ |jf                  r|jf                  jh                  nd}tk        |      xr |jm                  d       | _7        y # t$        t&        f$ r
 t"        }Y w xY wc c}w c c}w )Napp_idrn   r   r!  ws_urlr=  r  r>  r  rO   i,  )ttl_secondsr[  YUANBAO_DM_POLICYrp  r\  YUANBAO_DM_ALLOW_FROM,r]  YUANBAO_GROUP_POLICYr^  YUANBAO_GROUP_ALLOW_FROM)r[  r\  r]  r^  r  r  )8superr  r   YUANBAOr2  r  rh   r  r  rP  DEFAULT_WS_GATEWAY_URLru  DEFAULT_API_DOMAINrg   r  rt  r   _DEFAULT_RESOLVE_CONCURRENCYr  r6  max_MIN_RESOLVE_CONCURRENCYr  _MAX_RESOLVE_CONCURRENCYr  rL  r  r  rp  r   r9  r?  rG  r  r   r  r3  r  r&  r  re  rf  rg  r\   rU  r|  r5  _group_queryrE  rJ  r  home_channelrl  r   r]   r  )r  r1  r  _extra_raw_concurrencyr[  _dm_allow_from_rawxr\  r]  _group_allow_from_rawr^  _existing_homer  s                ra   r  zYuanbaoAdapter.__init__'  si   !1!12 #$jj28b??A!'L!9!?R F F H&,jj&:&Bd#ZZ1K5KRRT!'L!9!O=O W WX[ \ &

; 7 =2DDF	<"6::.IKg#hi /2$(*:;/
& /@.E*9$*? 25 58E
 =?). *c: 8: 46 57" 35 JJ{# 9yy,i8
%'%%' 	 JJ' 6yy0"5 	 8J7O7OPS7T#b!XYX_X_XaAGGI#b#b JJ~& <yy/;
%'%%' 	 JJ)* 9yy3R8 	 ;P:U:UVY:Z&hQ^_^e^e^gqwwy&h&h*'%-	
 .d3 3I2N2N2P #9: 
+1+>+>F''B 	 )-^(<(h^E^E^_gEhAho :& 	<;	<f $c 'is*   O
 O&.O&O+O+
O#"O#c                    | j                   j                  |       |j                  | j                   j                         |S )z@Register a fire-and-forget task so it won't be GC'd prematurely.)r?  r   r@  rA  rr  s     ra   r  zYuanbaoAdapter._track_task  s5    ""4(t55==>rc   c                     y)zCYuanbao gates DM/group access at intake via dm_policy/group_policy.Tr   r  s    ra   enforces_own_access_policyz)YuanbaoAdapter.enforces_own_access_policy  s     rc   c                .   | j                   }t        |j                  xs d      j                         }|sy|j                  dk(  rW|j                  |      ry|j                  dk(  r5ddlm}  |       j                  t        j                  j                  |      S y|j                  dk(  rjt        |j                  xs d      j                         }|sy|j                  d	k(  r|j                  |      S |j                  d
k(  r|j!                         S yy)zTrue when the sender may persist ``YUANBAO_HOME_CHANNEL``.

        Intake-only pairing forwards are excluded until the sender is on the
        strict allowlist, has explicit open-world opt-in, or is approved in the
        pairing store.
        rn   Fr  Trp  r   )PairingStorer
  rk  rl  )r|  r   re  rh   rm  rn  r[  gateway.pairingr(  is_approvedr   r  r  rf  r]  ru  rh  )r  r  r}  r]  r(  rf  s         ra   r  z)YuanbaoAdapter._sender_may_designate_home  s      $22S%%+,224==D ##F+9,8#~11(2B2B2H2H&QQ==G#S^^1r288:J""k1..z::""f,//11rc   F)is_reconnectc               R   K   | j                   j                          d{   S 7 w)zhConnect to Yuanbao WS gateway and authenticate.

        Delegates to ConnectionManager.open().
        N)r  rl  )r  r+  s     ra   rw  zYuanbaoAdapter.connect  s#     
 %%**,,,,s   '%'c                V  K   t         j                  | u rt         j                  d       d| _        | j	                          | j                          | j                  j                          d{    | j                  j                          d{    t        | j                        D ]#  }|j                         r|j                          % | j                  j                          | j                  j                          t        j!                  d| j"                         y7 7 w)z;Cancel background tasks and close the WebSocket connection.NFz[%s] Disconnected)r  r  r  r  r  r  r  r  rp  r`  r9  r  r  r   r3  r0  r1  r  rr  s     ra   
disconnectzYuanbaoAdapter.disconnect  s     **d2%%d+!##% $$&&&nn""$$$ ,,- 	D99;	 	!!#  "'3 	'$s+   A,D).D%/!D)D',D)>A(D)'D)c                \   K   | j                   j                  ||||       d{   S 7 w)zCSend text message with auto-chunking. Delegates to OutboundManager.r  N)rp  r  )r  rl  r3  r  metadatarf  s         ra   r   zYuanbaoAdapter.send  s-      ^^--gwU_-````r  c                @   K   |j                  d      r|ddS |ddS w)u  Return basic chat metadata derived from the chat_id prefix.

        chat_id conventions:
          "group:<group_code>"  → group chat
          "direct:<account>"   → C2C / direct message (default)

        TODO (T06): fetch real chat name/member-count from Yuanbao API.
        r  r
  )r  rt  r  r)  r  s     ra   get_chat_infozYuanbaoAdapter.get_chat_info  s,      h'#W55..r  c                v   K   	 | j                   j                  |       d{    y7 # t        $ r Y yw xY ww)zGSend "typing" status heartbeat (RUNNING). Delegates to OutboundManager.N)rp  r  r5  )r  rl  r0  s      ra   send_typingzYuanbaoAdapter.send_typing  s4     	..--g666 		s(   9* (* 9* 	6969c                z   K   	 | j                   j                  |d       d{    y7 # t        $ r Y yw xY ww)zStop the RUNNING heartbeat loop without sending FINISH immediately.

        FINISH is sent by send() after actual message delivery to ensure correct ordering:
        RUNNING... -> message arrives -> FINISH.
        Fr  N)rp  r  r5  r  s     ra   r  zYuanbaoAdapter.stop_typing   s9     	..,,W%,HHH 		s(   ; , *, ;, 	8;8;c                0  K   |j                   j                  }| j                  j                  |       d{    	 t        |   ||       d{    | j                  j                  |       y7 97 "# | j                  j                  |       w xY ww)z9Wrap base class processing with a slow-response notifier.N)rM  rl  rp  r  r  _process_message_backgroundr  )r  r.  r(  rl  r  s       ra   r7  z*YuanbaoAdapter._process_message_background  sv     ,,&&nn00999	9'5e[IIINN//8	 	:INN//8s8   5BA2BA6 A4A6 B4A6 6BBc                T   K   | j                   j                  |       d{   S 7 w)z2Query group info (delegates to GroupQueryService).N)r  r?  rt  s     ra   rK  zYuanbaoAdapter.query_group_info  s#     &&;;JGGGGs   (&(c                Z   K   | j                   j                  |||       d{   S 7 w)z9Query group member list (delegates to GroupQueryService).rA  N)r  rH  )r  rf  rB  rC  s       ra   get_group_member_listz$YuanbaoAdapter.get_group_member_list  s.      &&@@TZbg@hhhhr  i'  c                   K   | j                   j                  |      st        dd      S t        |      | j                  kD  r|d| j                   dz   }d| }| j                  |||       d{   S 7 w)a  
        Actively send C2C private chat message.

        Args:
            user_id: Target user ID
            text: Message text (limit 10000 characters)
            group_code: Source group code (for group-originated DM context)

        Returns:
            SendResult
        FzDM access denied for this userr  Nz
...(truncated)r	  r  )r|  rn  r   rp   DM_MAX_CHARSr   )r  r  r^   rf  rl  s        ra   send_dmzYuanbaoAdapter.send_dm(  s|      ""009e3STTt9t(((***+.@@DG9%YYwYDDDDs   A/A81A62A8c                b   K    | j                   j                  |df|||d| d{   S 7 w)zKSend image message (URL). Delegates to OutboundManager via ImageUrlHandler.r  )r  r  r  Nrp  r  )r  rl  r  r  r  r0  r  s          ra   
send_imagezYuanbaoAdapter.send_image?  sF      /T^^..[
w)
 
 
 	
 
   &/-/c                b   K    | j                   j                  |df|||d| d{   S 7 w)zISend local image file. Delegates to OutboundManager via ImageFileHandler.r  )r  r  r  Nr?  )r  rl  r  r  r  r0  r  s          ra   r  zYuanbaoAdapter.send_image_fileO  sF      /T^^..\
w:
 
 
 	
 
rA  c                b   K    | j                   j                  |df|||d| d{   S 7 w)zISend file message (URL). Delegates to OutboundManager via FileUrlHandler.r  )r  r  r  Nr?  )r  rl  r  r  r  r0  r  s          ra   	send_filezYuanbaoAdapter.send_file_  sF      /T^^..Z
8
 
 
 	
 
rA  c                b   K    | j                   j                  |df|||d| d{   S 7 w)zDSend sticker/emoji. Delegates to OutboundManager via StickerHandler.r+  )r  r1  r2  Nr?  )r  rl  r1  r2  r  r  s         ra   send_stickerzYuanbaoAdapter.send_stickero  sG      /T^^..Y
%*
 	
 
 	
 
rA  c                d   K    | j                   j                  |df||||d| d{   S 7 w)zMSend local file (document). Delegates to OutboundManager via DocumentHandler.rw  )r  r  r%  r  Nr?  )r  rl  r%  r  r  r  r0  r  s           ra   r  zYuanbaoAdapter.send_document  sI      /T^^..Z
w(
 	
 
 	
 
s   '0.0c                   K   t         j                  | j                  | j                  | j                  | j
                         d{   S 7 w)z<Get the current valid sign token (using module-level cache).ri  N)r   rQ  r  r  r  rt  r  s    ra   r  z YuanbaoAdapter._get_cached_token  sF      **MM4++T-=-=oo + 
 
 	
 
s   AAAAc                    | j                   }|j                  | j                  |j                  |j                  | j
                  dS )z3Return a snapshot of the current connection status.)	connectedr!  r^  r`  r  )r  rd  rP  r^  r`  ru  )r  r  s     ra   
get_statuszYuanbaoAdapter.get_status  s>    **ll//"&"9"9ll
 	
rc   r   Optional['YuanbaoAdapter'])r^  rM  r   r   )r1  r   r  r   r   r   )rB  asyncio.Taskr   rN  rx  )r  r]  r   r   )r+  r   r   r   r   )NNrn   )rl  r   r3  r   r  rr  r0  zOptional[Dict[str, Any]]rf  r   r   r   )rl  r   r   r  r   )rl  r   r0  rb  r   r   rs  )r(  r   r   r   rW  rX  rY  rX  )r  r   r^   r   rf  r   r   r   )NNN)rl  r   r  r   r  rr  r  rr  r0  rb  r  r   r   r   )rl  r   r  r   r  rr  r  rr  r0  rb  r  r   r   r   )rl  r   r  r   r  rr  r  rr  r0  rb  r  r   r   r   )rl  r   r1  rr  r2  zOptional[int]r  rr  r  r   r   r   )NNNN)rl  r   r%  r   r  rr  r  rr  r  rr  r0  rb  r  r   r   r   )r   r8  )(r   r   r   r   r   r  PLATFORMr  rY  splits_long_messagesr  r  r  r   r	  r  r  r  r  r&  r  rw  r.  r   r2  r4  r  r7  rK  r:  r<  r=  r@  r  rD  rF  r  r  rK  __classcell__)r  s   @ra   r  r    s   MHNCs+.=. >B:A$ $ ' 'ii^  < 5: -46 #'-1	a	a 	a  		a
 +	a 	a 
	a/	9H
 >Aii'*i7:i	i LE6 "&"&#'

 
 	

  
 !
 
 

( "&"&#'

 
 	

  
 !
 
 

( #'"&#'

 
  	

  
 !
 
 

& '+$("&

 $
 "	

  
 
 

( #'!%"&#'

 
  	

 
  
 !
 
 

$
	
rc   r  c                 *    t         j                         S )z,Delegate to ``YuanbaoAdapter.get_active()``.)r  r	  r   rc   ra   get_active_adapterrS    s    $$&&rc   c                X   K   | j                   j                  |||       d{   S 7 w)z,Delegate to ``OutboundManager.send_direct``.N)rp  r  )r^  rl  r  r  s       ra   send_yuanbao_directrU    s)      ""..wMMMMr  rL  r   )
r^  rC  rl  r   r  r   r  r  r   r  )r   
__future__r   r   r  r  r  rD  r   r   r  loggingre  rr   r*  r  urllib.parser  r  r   r   r   enumr   pathlibr   abcr	   r
   typingr   r   r   r   r   r   r   r   sysr%  rv  websockets.exceptionsrq  ImportErrorgateway.configr   r   gateway.platforms.baser   r   r   r   r   r   r   gateway.platforms.helpersr   gateway.platforms.yuanbao_mediar   r  r   r   r    r!   r"   r#   r;  r$   r%   r&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   r9   r:   gateway.sessionr;   	getLoggerr   r0  
hermes_clir<   _HERMES_VERSIONr,  r/  r   r.  r  r-  r  r  r  rx  r  r  r  r  r  r  AUTH_FAILED_CODESAUTH_RETRYABLE_CODESrj  rk  REPLY_REF_TTL_Sr~  r  rs   rd  r  rJ  rf  r  r  r  r  r  r  rX   r   rZ  r[  r|  r]  r~  r  r  r  r  r   rL  rR  rU  rz  r  r  r  r  r#  r(  rB  rH  rL  rW  r8  r  r  r"  rE  rL  r  r  r  r  r#  r(  r5  r\  rv  r  r  r  rS  rU  r   rc   ra   <module>rk     s  " #          	 	     2 2   # Q Q Q 
  
 4   :        2 .			8	$
9 -. LL  L 6 !        @     ' )  !       Y  I  RZZ AB  $$>?  

-. $&  /1 ,  !   ^
 ^
@p) p)f 5
>) >) >)BA A6] ]|]( ]@/ "	' 	Nd- Ndb* &- "D" D"N- 20- 0ft0 tl	"3 2^. ^B- $~. ~B!2 >   $$5 $NG. GTk&7 k\D	. D	N/. /d_9* _9D% %N[
 [
z~=s ~=B
& 
6
' 
4
% 
4
& 
.3% 3>U
 U
px+ x+v. .bf! f!R]) ])@Q
( Q
r' 59	NNN N 2	N
 N{k   Jt  Os$    
M/ M? /	M<;M<?N
	N
