
     Yjnz                      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*n# e+$ r d
Z*dZ(Y nw xY w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 n# e+$ r dZaY nw xY weaZbeaZc edeJ          Zee&jf        ZgdZhdZidZjdZkdZldZmdZndZoh dZpdZqh dZrh dZsdZtdZudZvd Zwd!Zx ejy        d"          Zz ejy        d#          Z{ e|h d$          Z} ejy        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          ZddZ	 dddZdS )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dS )'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
    textstrreturnboolc                j    d}|                      d          D ]}|                    d          r| }|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)rY   in_fencelines      NC:\Users\server\AppData\Local\hermes\hermes-agent\gateway\platforms\yuanbao.pyhas_unclosed_fencez$MarkdownProcessor.has_unclosed_fence   sE     JJt$$ 	( 	(Du%% ('<    c                    |                                  }|sdS |                    d          d                                         }|                    d          o|                    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
        Fr^   |)rstripr`   stripra   endswith)rY   trimmed	last_lines      rd   ends_with_table_rowz%MarkdownProcessor.ends_with_table_row   sg     ++-- 	5MM$''+1133	##C((DY-?-?-D-DDrf   N	max_charsintlen_fnOptional[Callable[[str], int]]tuple[str, str]c                   |pt           } ||           |k    r| dfS |t           u r| d|         }nQdt          |           }}||k     r0||z   dz   dz  } || d|                   |k    r|}n|dz
  }||k     0| d|         }|                    d          }|dk    r| d|dz            | |dz   d         fS t          j        d          }	d}
|	                    |          D ]}|                                }
|
dk    r| d|
         | |
d         fS |                    d	          }|dk    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   [。！？.!?]\nrh   r^   )lenrfindrecompilefinditerend)rY   rp   rr   _lenwindowlohimidpossentence_end_rebest_posmcuts                rd   split_at_paragraph_boundaryz-MarkdownProcessor.split_at_paragraph_boundary   s   . }4::""8O
 3;;*9*%FFD		Br''Bw{q(4TcT
##y00BBqB r'' #2#YF ll6""77q>4a>11 *%899 ))&11 	 	AuuwwHHa<<		?DO33 ll4  77q>4a>11 &kkDSDz4:%%rf   c                P    |                                                      d          S )zDDetermine whether an atomic block is a code block (starts with ```).r_   )lstripra   rY   s    rd   is_fence_atomzMarkdownProcessor.is_fence_atom9  s      {{}}''...rf   c                    |                      d          d                                         }|                    d          o|                    d          S )zHDetermine whether an atomic block is a table (first line starts with |).r^   r   ri   )r`   rk   ra   rl   )rY   
first_lines     rd   is_table_atomzMarkdownProcessor.is_table_atom>  sM     ZZ%%a(..00
$$S))Fj.A.A#.F.FFrf   	list[str]c                   |                      d          }g g d}dd}dfd	}|D ]}|rJ                    |           |                    d
          rt                    dk    rd} |             O|                    d
          r" |             d}                    |            ||          r3r |d                   s
 |                                 |           |                                dk    r |             r |d                   r
 |                                 |            |             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)
        r^   Frc   rZ   r[   r\   c                ~    |                                  }|                    d          o|                    d          S )Nri   )rk   ra   rl   )rc   strippeds     rd   _is_table_linez:MarkdownProcessor.split_into_atoms.<locals>._is_table_line[  s6    zz||H&&s++F0A0A#0F0FFrf   Nonec                     rTd                               } |                                 r                    |                                             d S d S )Nr^   )joinrk   appendclear)atomatomscurrent_liness    rd   _flush_currentz:MarkdownProcessor.split_into_atoms.<locals>._flush_current_  s`     &yy//::<< 'LL&&&##%%%%%	& &rf   r_   rV   Trh   rv   )rc   rZ   r[   r\   r[   r   )r`   r   ra   rx   rk   )rY   linesrb   r   r   rc   r   r   s         @@rd   split_into_atomsz"MarkdownProcessor.split_into_atomsD  s   " 

4  #%	G 	G 	G 	G	& 	& 	& 	& 	& 	& 	&  	+ 	+D +$$T***??5)) %c-.@.@1.D.D$H"N$$$'' +   $$T****%% 	+  %b8I)J)J %"N$$$$$T****##      %^^M"4E%F%F %"N$$$$$T****rf     c                   |pt           }|sg S  ||          |k    r|gS |                     |          }g t                      }g d}dfd}|D ]}	 ||	          }
rdnd}||z   |
z   }||k    rr |             g d}d}sh|
|k    rb|                     |	          s|                     |	          r8|                    t                                                   |	                               |	           |||
z   z  } |             g }t                    D ]\  }} ||          |k    r|                    |           *||v r|                    |           D|                     |          r|                    |           o|} ||          |k    rW| 	                    |||          \  }}|s|d|         ||d         }}|r|                    |            ||          |k    W|r|                    |           t          |          dk    rR|d         g}|dd         D ]<}|d	         }|d
z   |z   } ||          |k    r||d	<   '|                    |           =|}d |D             S )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   r[   r   c                 `    r*                      d                                         d S d S )Nrw   )r   r   )chunkscurrent_partss   rd   _flush_partsz;MarkdownProcessor.chunk_markdown_text.<locals>._flush_parts  s9     :fkk-8899999: :rf   rG   rr   NrV   rh   rw   c                    g | ]}||S  r   .0cs     rd   
<listcomp>z9MarkdownProcessor.chunk_markdown_text.<locals>.<listcomp>  s    '''aQ''''rf   r   )
rx   r   setr   r   addr   	enumeratere   r   )clsrY   rp   rr   r~   r   indivisible_setcurrent_lenr   r   atom_lensep_lenprojected_lenresultidxchunk	remainingheadmergedprevcombinedr   r   s                        @@rd   chunk_markdown_textz%MarkdownProcessor.chunk_markdown_text  sf   2 } 	I4::""6M $$T** $'EE#%	: 	: 	: 	: 	: 	: 	:  	. 	.DtDzzH(/aaaG''1H<My((]( "!  9,,**400 -474E4Ed4K4K -##CKK000d###  &&&7X--KK #F++ 	) 	)JCtE{{i''e$$$o%%e$$$%%e,, e$$$I$y//I--"%"A"Ay #B # #i  S&/

&;Yyzz=R)D (MM$''' $y//I--  )i((( v;;??!'F ) )bz&=504>>Y..!)F2JJMM%((((F''6''''rf   
prev_chunk
next_chunkc                   |                                 }|                                }|                    d          s|                    d          rdS |                     |          r]|r-|                    d          d                                         nd}|                    d          r|                    d          rdS dS )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_   r^   r   rv   ri   rw   )rj   r   rl   ra   ro   r`   rk   )r   r   r   prev_trimmednext_trimmedr   s         rd   infer_block_separatorz'MarkdownProcessor.infer_block_separator  s    " "((**!((**   '' 	<+B+B5+I+I 	4 "":.. 	@LT++D11!4::<<<RTJ$$S)) j.A.A#.F.F tvrf   r   c                   |sg S g }d}|t          |          k     r||         }|                     |          rv|dz   t          |          k     r`|                     |||dz                      }||z   ||dz            z   }|dz  }|                     |          r|dz   t          |          k     `|                    |           |dz  }|t          |          k     |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   )rx   re   r   r   )r   r   r   icurrentseps         rd   merge_block_streaming_fencesz.MarkdownProcessor.merge_block_streaming_fences  s    &  	I#f++ooQiG((11 a!ec&kk6I6I//AGG!C-&Q-7Q ((11 a!ec&kk6I6I MM'"""FA #f++oo rf   c                X   | s| S |                      d          }t          |          dk     r| S |d                                         }|d                                         }t          j        d|t          j                  s| S |dk    r| S d                    |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)
        r^      r   rh   z^```(?:markdown|md)?\s*$r_   rV   )r`   rx   rk   rz   match
IGNORECASEr   )rY   r   r   rn   inners        rd   strip_outer_markdown_fencez,MarkdownProcessor.strip_outer_markdown_fence9  s      	K

4  u::>>K1X^^%%
"IOO%%	 x3ZOO 	K K 		%"+&&rf   c                f   d| vr| S |                      d          }g }|D ]}|                                }|                    d          r|                    d          rt	          j        d|          rJ|                     d          }d                    d |D                       }|                    |           |dk    s,|                    dd                                          dk    r|                    |           |                    |           d                    |          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
        ri   r^   z^\|[\s\-:]+(\|[\s\-:]+)+\|$c              3  j   K   | ].}|                                 r|                                 n|V  /d S Nrk   )r   cells     rd   	<genexpr>z<MarkdownProcessor.sanitize_markdown_table.<locals>.<genexpr>}  sO       * *  )-

>

$* * * * * *rf   z||rv   )	r`   rk   ra   rl   rz   r   r   r   replace)rY   r   result_linesrc   r   cells
normalizeds          rd   sanitize_markdown_tablez)MarkdownProcessor.sanitize_markdown_table_  sS     d??K

4  "$ 	* 	*Dzz||H ""3'' *H,=,=c,B,B *8:HEE 2$NN3//E!$ * *$)* * * " "J !''
3333%%)9)9#r)B)B)H)H)J)Jb)P)P ''1111##D))))yy&&&rf   c                     	 dS )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   r   rf   rd   markdown_hint_system_promptz-MarkdownProcessor.markdown_hint_system_prompt  s    V	
 	
rf   )rY   rZ   r[   r\   r   )rY   rZ   rp   rq   rr   rs   r[   rt   )rY   rZ   r[   r   r   N)rY   rZ   rp   rq   rr   rs   r[   r   )r   rZ   r   rZ   r[   rZ   )r   r   r[   r   rY   rZ   r[   rZ   r[   rZ   )__name__
__module____qualname____doc__staticmethodre   ro   r   r   r   r   classmethodr   r   r   r   r   r   r   rf   rd   rX   rX      s       	 	    \* E E E \E$  26=& =& =& =& \=&B / / / \/ G G G \G
 8 8 8 \8x  15	k( k( k( k( [k(^    [B ! ! ! [!J ! ! ! \!J *' *' *' \*'\ 
 
 
 \
 
 
rf   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d$S )-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]_locksapp_keyrZ   r[   asyncio.Lockc                d    || 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   r   s     rd   get_refresh_lockzSignManager.get_refresh_lock  s0     #*$$"),..CJwz'""rf   nonce	timestamp
app_secretc                    | |z   |z   |z   }t          j        |                                |                                t          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)r   r   r   r   plains        rd   compute_signaturezSignManager.compute_signature  sN     	!G+j8x
))++U\\^^W^LLVVXXXrf   c                     t          j        t          t          d                              } |                     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    rd   build_timestampzSignManager.build_timestamp  s<     )!*<*<*<!=!=>>>8999rf   entrydict[str, Any]r\   c                L    |d         t          j                     z
  | j        k    S )zEDetermine whether the cache entry is valid (not expired with margin).	expire_ts)timeCACHE_REFRESH_MARGIN_S)r   r  s     rd   is_cache_validzSignManager.is_cache_valid  s"     [!DIKK/#2LLLrf   r   c                8    | j                                          dS )z;Clear all per-app_key refresh locks (called on disconnect).N)r   r   r   s    rd   clear_lockszSignManager.clear_locks  s     	
rf   rq   c                    t          j                     fd| j                                        D             }|D ]}| j                            |d           t	          |          S )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.
        c                T    g | ]$\  }}|                     d d          z
  dk    "|%S )r
  r   get)r   kvr  s      rd   r   z-SignManager.purge_expired.<locals>.<listcomp>  sE     
 
 
!QQUU;***Q.. ...rf   N)r  r   itemspoprx   )r   expired_keysr  r  s      @rd   purge_expiredzSignManager.purge_expired  s~     ikk
 
 
 
***,,
 
 
  	$ 	$AJNN1d####<   rf   rv   
api_domain	route_envc                  K   |                     d           | j         }t          j        | j                  4 d{V }t          | j        dz             D ]P}t          j        d          }| 	                                }	| 
                    ||	||          }
|||
|	d}dt          t          t          t          d}|r||d	<   t                              d
||dk    rd| d| j         dnd           |                    |||           d{V }|j        dk    r)|j        }t)          d|j         d|dd                    	 |                                }n%# t,          $ r}t/          d|           |d}~ww xY w|                    d          }|dk    r|                    d          }t3          |t4                    st/          d|           t                              d|                    d                     |c cddd          d{V  S || j        k    rW|| j        k     rLt                              d|| j        |dz   | j                   t=          j        | j                   d{V  '|                    dd          }t)          d| d|           	 ddd          d{V  n# 1 d{V swxY w Y   t)          d          )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 )rv   )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) rj   
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_coderY   RuntimeErrorr&  	Exception
ValueErrorr  
isinstancedictRETRYABLE_CODEwarningRETRY_DELAY_Sr   sleep)r   r   r   r  r  urlclientattemptr   r   r"  payloadr'  responsebodyresult_dataexcr*  r+  r-  s                       rd   fetchzSignManager.fetch  sK      ""3''999$S-?@@@ <	P <	P <	P <	P <	P <	P <	PF 1!455 ;P ;P)"--//11	11%GZXX	  '"!*!*	  %7$0):%9%1   7-6GM*2?F{{;w;;;;;;PR   "(Sw!P!PPPPPPP'3..#=D&'f(BV'f'fZ^_c`c_cZd'f'fgggY2:--//KK  Y Y Y$%N%N%NOOUXXY #v..199&??622D%dD11 e()cVa)c)cdddKK ?(ASASTTTKK]<	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P` 3---'CO2K2KNNX)!   "-(9:::::::::!ooeR00"#NT#N#N#N#NOOOw;P<	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P <	P| DEEEs?   DJ>	EJ>
F (E;;F  BJ>BJ>>
KKc           	       K   |                                   | j                            |          }|rh|                     |          rSt	          |d         t          j                    z
            }t                              d|           t          |          S | 	                    |          4 d{V  | j                            |          }|r6|                     |          r!t          |          cddd          d{V  S | 
                    ||||           d{V }|                    dd          }|dk    rt          j                    |z   nt          j                    dz   }	|                    dd          |                    d	d          ||                    d
d          |                    dd          |	d| j        |<   ddd          d{V  n# 1 d{V swxY w Y   t          | j        |                   S )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     tokenrv   r,  productsourcerT  r,  rR  rU  rV  r
  )r  r   r  r  rq   r  r;  r<  rC  r   rP  )
r   r   r   r  r  cachedremainr+  rR  r
  s
             rd   	get_tokenzSignManager.get_tokenI  s      	(( 	 c((00 	 ,ty{{:;;FKK<fEEE<<''00 	 	 	 	 	 	 	 	Z^^G,,F $#,,V44 $F||	 	 	 	 	 	 	 	 	 	 	 	 	 	
 7J
INNNNNNNND HHZ33H2:Q,,	h..DIKKRVDVI '2..((8R00$88Ir22((8R00&# #CJw	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	& CJw'(((s   6A G"	CG""
G,/G,c           	       K   t                               d|dd                    |                     |          4 d{V  | j                            |d           |                     ||||           d{V }|                    dd          }|dk    rt          j                    |z   nt          j                    dz   }|                    dd          |                    d	d          ||                    d
d          |                    dd          |d| j        |<   ddd          d{V  n# 1 d{V swxY w Y   t          | j        |                   S )z.Force refresh token (clear cache and re-sign).zC[force-refresh] Clearing cache and re-signing token: app_key=****%sNrR  r   rS  rT  rv   r,  rU  rV  rW  )	r;  rE  r   r   r  rP  r  r  rC  )r   r   r   r  r  r+  rR  r
  s           rd   force_refreshzSignManager.force_refreshv  s      	\^efhfifi^jkkk''00 	 	 	 	 	 	 	 	JNN7D)))7J
INNNNNNNND HHZ33H2:Q,,	h..DIKKRVDVI '2..((8R00$88Ir22((8R00&# #CJw	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	  CJw'(((s   C#D66
E E N)r   rZ   r[   r   )
r   rZ   r   rZ   r   rZ   r   rZ   r[   rZ   r   )r  r  r[   r\   r   r[   rq   rv   )
r   rZ   r   rZ   r  rZ   r  rZ   r[   r  )r   r   r   r   r/  rD  r4  rF  r  r2  r   __annotations__r   r   r   r   r   r  r  r  r  rP  rZ  r]  r   rf   rd   r   r     s          1JNKM   N
 )+F**** ')F(((( # # # [# Y Y Y \Y : : : \: M M M [M    [ ! ! ! [!$  GF GF GF GF [GFV  () () () () [()X  ) ) ) ) [) ) )rf   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%<   dS )&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]pushrv   rZ   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]rV  msg_typereply_to_message_idreply_to_textquote_media_refs
media_urlsmedia_types	link_urlschannel_prompt)!r   r   r   r   r`  dc_fieldrg  rh  rj  rk  rl  rm  rn  ro  rp  rq  rr  rs  rt  ru  rv  rw  rx  rz  rV  r{  r|  r}  r~  r  r  r  r  r   rf   rd   rd  rd    sX          LLLx555J5555  DK LJJOXd333H3333F GII Hx555J5555 )-,,,, $(M'''' !F     #H"""" *.----#'M''''%Xd;;;;;;;  x555J5555 666K6666 ht444I4444 %)N((((((rf   rd  c                  J    e Zd ZU dZdZded<   edd            ZddZddZ	dS )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.
    rv   rZ   namectxrd  next_fnr   r[   r   c                
   K   dS )zEProcess *ctx* and optionally call *next_fn* to continue the pipeline.Nr   selfr  r  s      rd   handlezInboundMiddleware.handle  
        rf   c                >   K   |                      ||           d{V S )zFAllow middleware instances to be called directly (duck-typing compat).N)r  r  s      rd   __call__zInboundMiddleware.__call__  s,      [[g.........rf   c                2    d| j         j         d| j        dS )N<z name=>)	__class__r   r  r  s    rd   __repr__zInboundMiddleware.__repr__  s"    @4>*@@$)@@@@rf   N)r  rd  r  r   r[   r   r   )
r   r   r   r   r  r`  r
   r  r  r  r   rf   rd   r  r    s         
 
 DNNNNT T T ^T/ / / /A A A A A Arf   r  c                  x    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dS )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.
    r[   r   c                    g | _         d S r   _middlewaresr  s    rd   __init__zInboundPipeline.__init__  s    "$rf   Nc                F    t          | t                    r	| j        | fS | |fS )zHNormalize (name, handler) or (InboundMiddleware,) into (name, callable).)rB  r  r  )
name_or_mwhandlers     rd   
_normalizezInboundPipeline._normalize  s/     j"344 	/?J..7""rf   'InboundPipeline'c                r    |                      ||          \  }}| 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         rd   usezInboundPipeline.use  s=     //*g66a  $4111rf   targetrZ   c                   |                      ||          \  }}t          fdt          | j                  D             d          }|||f}|| j                            |           n| j                            ||           | S )zEInsert a middleware before *target* (by name).  Appends if not found.c              3  6   K   | ]\  }\  }}}|k    |V  d S r   r   r   r   n_r  s       rd   r   z-InboundPipeline.use_before.<locals>.<genexpr>  1      VV,!YaA!v++A++++VVrf   Nr  nextr   r  r   insert	r  r  r  r  r  r  r  r   r  s	    `       rd   
use_beforezInboundPipeline.use_before  s    //*g66aVVVV)D4E*F*FVVVX\]]q$;$$U++++$$S%000rf   c                   |                      ||          \  }}t          fdt          | j                  D             d          }|||f}|| j                            |           n| j                            |dz   |           | S )zDInsert a middleware after *target* (by name).  Appends if not found.c              3  6   K   | ]\  }\  }}}|k    |V  d S r   r   r  s       rd   r   z,InboundPipeline.use_after.<locals>.<genexpr>&  r  rf   NrV   r  r  s	    `       rd   	use_afterzInboundPipeline.use_after#  s    //*g66aVVVV)D4E*F*FVVVX\]]q$;$$U++++$$S1We444rf   r  c                8    fd| j         D             | _         | S )zRemove a middleware by name.c                .    g | ]\  }}}|k    |||fS r   r   )r   r  r  wr  s       rd   r   z*InboundPipeline.remove.<locals>.<listcomp>0  s+    UUU71a1PT99aAY999rf   r  )r  r  s    `rd   removezInboundPipeline.remove.  s'    UUUUd6GUUUrf   rg  c                $    d | j         D             S )zAReturn ordered list of registered middleware names (for testing).c                    g | ]\  }}}|	S r   r   )r   r  r  s      rd   r   z4InboundPipeline.middleware_names.<locals>.<listcomp>6  s    333gaA333rf   r  r  s    rd   middleware_namesz InboundPipeline.middleware_names3  s     43!23333rf   r  rd  c                V   K   | j         ddfd              d{V  dS )zKRun all middlewares in order.  Each middleware receives ``(ctx, next_fn)``.r   r[   r   c                    K   t                    k     ra         \  } }}dz  | |          s2	  |           d {V  n,# t          $ r t                              d| d            w xY wd S d S )NrV   z'[InboundPipeline] middleware [%s] errorTexc_info)rx   r@  r;  error)r  r  when_fnchainr  indexr  s      rd   r  z(InboundPipeline.execute.<locals>.next_fn?  s      #e**$$).u&gw
&wws||&!'#w//////////    LL!JD[_L```  %$s   A
 
)A3Nr   r  )r  r  r  r  r  s    `@@@rd   executezInboundPipeline.execute:  sa      !	 	 	 	 	 	 	 	 	  giirf   r   r   NN)r[   r  )r  rZ   r[   r  )r  rZ   r[   r  )r[   rg  r  rd  r[   r   )r   r   r   r   r  r   r  r  r  r  r  propertyr  r  r   rf   rd   r  r    s        	 	% % % %
 # # # \#	 	 	 	 		 	 	 	 		 	 	 	 	   
 4 4 4 X4     rf   r  c                  V    e Zd ZdZdZedd            Zedd
            ZddZddZ	dS )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``.
    decoderaw_bodyrg  r[   c                   g }| pg D ]}t          |t                    s|                    d          p|                    dd          }|                    d          p|                    di           }t          |t                    r*	 t	          j        |          }n# t          $ r d|i}Y nw xY w|                    ||pi d           |S )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.
        r{  MsgTyperv   msg_content
MsgContentrY   r{  r  )rB  rC  r  rZ   r&  loadsr@  r   )r  r   itemr{  r  s        rd   convert_json_msg_bodyz&DecodeMiddleware.convert_json_msg_body[  s     N 
	T 
	TDdD)) xx
++Ftxx	2/F/FH((=11OTXXlB5O5OK+s++ 88"&*["9"9KK  8 8 8#);"7KKK8MMx@QrRRSSSSs   B  B10B1raw_jsonrC  dict | Nonec                   | sdS |                      dd          p|                      dd          }|                      dd          p+|                      dd          p|                      dd          }|                      dg           p|                      d	g           }t                              |          }|s|s|                      d
          sdS |                      d
d          ||                      dd          p|                      dd          |                      dd          p|                      dd          ||                      dd          |                      dd          p|                      dd          |                      dd          p+|                      dd          p|                      dd          ||                      dd          p|                      dd          |                      dd          p|                      dd          |                      d          pdt          |                      d          t                    r+|                      d          pi                      dd          n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.).
        Nrl  rv   From_Accountrm  GroupIdgroup_idrp  MsgBodycallback_command
to_account
To_Accountro  	nick_namern  msg_seqr   MsgSeqrq  msg_keyMsgKeyrr  CloudCustomDatabot_owner_id
botOwnerIdrecall_msg_seq_listlog_exttrace_id)r  rl  r  ro  rm  rn  r  rq  rp  rr  r  r  r  )r  r  r  rB  rC  )r  rl  rm  msg_body_rawrp  s        rd   parse_json_pushz DecodeMiddleware.parse_json_pushp  s     	4 LL,, 0||NB// 	
 LLr** ,||Ir**,||J++ 	 LLR(( +||Ir** 	 $99,GG  	H 	X\\BT5U5U 	4 !)-? D D(",,|R88ZHLLWY<Z<Z'||,=rBBchllS^`bFcFc$",,|R88||Iq11NX\\(A5N5Nll8R00mHLLB4O4OmS[S_S_`hjlSmSm !).A2!F!F!m(,,WhjlJmJm$LL<<^\[]@^@^#+<<0E#F#F#N$OYZbZfZfgpZqZqswOxOx  Ai006B;;JKKK  A
 
 	
rf   r+  bytestuplec                0   	 t          j        |                    d                    }n# t          $ r d}Y nw xY wt	          |t
                    r|                     |          }|r|dfS n)	 t          |          }n# t          $ r d}Y nw xY w|r|dfS dS )zFDecode a single raw frame into (push_dict, decoded_via) or (None, '').utf-8Nr&  protobufNrv   )r&  r  r  r@  rB  rC  r  r-   )r  re  r+  	conn_jsonrj  s        rd   _decode_singlezDecodeMiddleware._decode_single  s    	
4;;w#7#788II 	 	 	III	 i&& 
	(''	22D $V|#$*400    (Z''xs   '* 99.A> >BBr  rd  r   c                .  K   |j         }|sd S d }d}|D ]}|                     |j        |          \  }}|sEt                              d|j        j        |r|                                d d         nd           h|9|}|}t                              d|j        j        |t          |                     |                    dg           }	|	rZddd	id
}
|                    dg           |
gz   |	z   |d<   t                              d|j        j        t          |	                     |sd S ||_	        ||_
        t                              d|j        j        |j
        |j	                            dd          |j	                            dd          |j	                            dd          d |j	                            dg           D                        t                              d|j        j        |j	                    |             d {V  d S )Nrv   z;[%s] Push decoded but no valid message. raw hex(first64)=%s   z(empty)z#[%s] Frame decoded (via=%s): len=%drp  TIMTextElemrY   r^   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=%srl  rm  rq  c                :    g | ]}|                     d d          S )r{  rv   r  r   es     rd   r   z+DecodeMiddleware.handle.<locals>.<listcomp>  s&    IIIqQUU:r""IIIrf   z[%s] Push payload: %s)rh  r  re  r;  r<  r  hexrx   r  rj  rk  debug)r  r  r  	data_listmerged_pushrk  r+  rj  via
extra_body_seps              rd   r  zDecodeMiddleware.handle  s/     N	 	F 	 	D++CK>>ID# MK$$&Mdhhjj#&6&6I   ""!5K$c3t99    "XXj"55
 (5vtnUUD.9ooj".M.MQUPV.VYc.cK
+KKU(#j//  
  	F%QKcoHLL,,HLLr**HLL2&&IICHLLR,H,HIII	
 	
 	
 	,ck.>IIIgiirf   N)r  rg  r[   rg  )r  rC  r[   r  )r+  r  r[   r  r  )
r   r   r   r   r  r   r  r  r  r  r   rf   rd   r  r  P  s          D    \( /
 /
 /
 \/
f   *4 4 4 4 4 4rf   r  c                      e Zd ZdZdZd	dZdS )
ExtractFieldsMiddlewarez8Extract common fields from ctx.push into ctx attributes.zextract-fieldsr  rd  r[   r   c                  K   |j         }|                    dd          |_        |                    dd          |_        |                    dd          |_        |                    dd          |_        |                    dg           |_        |                    dd          |_        |                    dd          |_         |             d {V  d S )	Nrl  rv   rm  rn  ro  rp  rq  rr  )	rj  r  rl  rm  rn  ro  rp  rq  rr  )r  r  r  rj  s       rd   r  zExtractFieldsMiddleware.handle  s      x88NB77,33,33"hh'8"==xx
B//XXh++
 $)<b A Agiirf   Nr  r   r   r   r   r  r  r   rf   rd   r   r     s3        BBD	 	 	 	 	 	rf   r   c                      e Zd ZdZdZd	dZdS )
DedupMiddlewarezInbound message deduplication.dedupr  rd  r[   r   c                   K   |j         rQ|j        j                            |j                   r-t                              d|j        j        |j                    d S  |             d {V  d S )Nz)[%s] Duplicate message ignored: msg_id=%s)rq  re  _dedupis_duplicater;  r  r  r  s      rd   r  zDedupMiddleware.handle  sl      : 	#+,99#*EE 	LLDckFVX[XbcccFgiirf   Nr  r  r   rf   rd   r  r    s3        ((D     rf   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dS )#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]r  rd  r[   r   c                   K   |j         pi                     dd          }|| j        vr |             d {V  d S |                     ||           d S )Nr  rv   )rj  r  _RECALL_COMMANDS_handle_recall)r  r  r  cmds       rd   r  zRecallGuardMiddleware.handle  sh      x~2""#5r::d+++'))OOOOOOOFC%%%%%rf   rm  rZ   rl  c                \    |                      |rd| nd| |rdnd|pd |rdnd           S )Ngroup:direct:groupdmmain)rs  rt  user_id	thread_id)build_source)re  rm  rl  s      rd   _build_sourcez#RecallGuardMiddleware._build_source$  s^    ##.8V*j***>V>V>V!+5gg (D *4ff	 $ 
 
 	
rf   r  c                
   |j         }|j        pi }|dk    r|                    d          pg }n8|                    d          pd}|                    d          }|s|r||dgng }|s"t                              d|j                   d S |                    d          pd                                }|                    d	          pd                                }	|D ]}
|
                    d          p#t          |
                    d          pd          }|s>|                     ||          }|| 	                    |||||	           p|j
                            |          }|                     ||||	|           d S )
Nr  r  rq  rv   r  )rq  r  z2[%s] Recall callback with empty seq_list, skippingrm  rl  )re  rj  r  r;  r  r  rk   rZ   _find_processing_session_interrupt_for_recall_msg_content_cache_patch_transcript)r  r  r  re  rj  seq_listr   seqrm  rl  	seq_entryrecalled_id
matched_skrecalled_contents                 rd   r  z$RecallGuardMiddleware._handle_recall-  s   +x~2000xx 566<"HH((8$$*C((9%%C=@PCP337788bH 	LLMw|\\\Fhh|,,299;;
006B==??! 
	i 
	iI#--11XSy9Q9Q9WUW5X5XK 66wLLJ%**7JZYeffff#*#=#A#A+#N#N &&wZWghhhh
	i 
	irf   r#  ry  c                j    | j                                         D ]\  }}||k    r|| j        v r|c S d S r   )_processing_msg_idsr  _active_sessions)re  r#  skr   s       rd   r  z.RecallGuardMiddleware._find_processing_sessionM  sK    288:: 	 	GBk!!bG,D&D&D			trf   session_keyc           	        |rd| nd| }d| d| d}t          |t          j        |                     |||          d          }||j        |<   |j                            |          }	|	|	                                 t          	                    d|j
        ||d d	                    |j                            |d
          }
|
r|                     |||
||           d S d S )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)rY   message_typerV  internalz+[%s] Recall interrupt: msg_id=%s session=%s   rv   )r   r   TEXTr  _pending_messagesr(  r  r   r;  r<  r  _processing_msg_texts_schedule_content_redact)r   re  r*  r#  rm  rl  whererecall_textsynth_eventactive_eventrecalled_texts              rd   r  z+RecallGuardMiddleware._interrupt_for_recallT  s=    *4[%%%%9[\9[9[	,/:	, 	,CH	, 	, 	, 	 #$)$$Wj,GG	
 
 
 2=!+./33K@@#A7<Q\^ijmkmjm^nooo  599+rJJ 	h((+}jZfggggg	h 	hrf   r7  c                     d fd}t          j         |                      }j                            |           |                    j        j                   d S )Nr[   r   c            	     2  K   t          dd           } | sd S 	 |                                         	                    j        }n# t          $ r Y d S w xY wt          d          D ]}t          j        d           d {V  	 |                     |          }n# t          $ r Y @w xY w|D ]}|	                    d          dk    r|	                    d          
k    rj
        |d<   	 |                     ||           t                              dj        d d                    n8# t          $ r+}t                              dj        |           Y d }~nd }~ww xY w  d S t                              d	j        d d                    d S )
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_idr@  r3  r   rG  load_transcriptr  	_REDACTEDrewrite_transcriptr;  r<  r  rE  r  )storesidr  
transcriptr  rO  re  r   rl  rm  r7  r*  s         rd   _redactz?RecallGuardMiddleware._schedule_content_redact.<locals>._redact}  s     G%5t<<E 11%%gz<HH       2YY  mC(((((((((!&!6!6s!;!;JJ    H'  Eyy((F22uyy7K7K}7\7\+.=i(_!44S*EEE"KK(H',XcdgegdgXhiiii( _ _ _"NN+JGLZ]^^^^^^^^_ LLZ\c\hjuvywyvyjz{{{{{s;   /A
 

AAB
B+*B+/?D//
E$9!EE$r   )r   create_task_background_tasksr   add_done_callbackdiscard)r   re  r*  r7  rm  rl  rG  tasks   ``````  rd   r2  z.RecallGuardMiddleware._schedule_content_redactz  s    	| 	| 	| 	| 	| 	| 	| 	| 	| 	| 	|: "7799--!%%d+++w8@AAAAArf   Nr%  c                R   t          |dd           }|sd S 	 |                    |                     |||                    j        }n9# t          $ r,}t
                              d|j        |           Y d }~d S d }~ww xY w	 |                    |          }	n9# t          $ r,}t
                              d|j        |           Y d }~d S d }~ww xY wd }
d}|	D ]!}|	                    d          |k    r|}
d} n"|
?|r=|	D ]:}|	                    d          dk    r|	                    d	          |k    r|}
d
} n;|
~| j
        |
d	<   	 |                    ||	           t
                              d|j        ||           n8# t          $ r+}t
                              d|j        |           Y d }~nd }~ww xY wd S |                    |dd| dt          j        t           j                                                  d           t
                              d|j        |           d S )Nr:  z*[%s] Recall: failed to resolve session: %sz*[%s] Recall: failed to load transcript: %srv   
message_idzbranch A1: id matchr;  r<  r=  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  )r;  r=  r   z1[%s] Recall: system note for msg_id=%s (branch B))r>  r?  r  r@  r@  r;  rE  r  rA  r  rB  rC  r<  append_to_transcriptr   r  r   utc	isoformat)r   re  r#  rm  rl  r%  rD  rE  rO  rF  r  branch_labelr  s                rd   r  z'RecallGuardMiddleware._patch_transcript  s    !1488 	F	--c.?.?Ua.b.bccnCC 	 	 	NNGWZ[[[FFFFF		..s33JJ 	 	 	NNGWZ[[[FFFFF	  	 	Eyy&&+554 6 >.>#  99V$$..599Y3G3GK[3[3["F#=LE #F9`((j999BGLR]_kllll ` ` `KW\[^________`F 	""3n{nnn!666@@BB)
 )
 	 	 	
 	GWbcccccsG   /A 
A=!A88A=B 
C!!CC8E? ?
F4	!F//F4r  )rm  rZ   rl  rZ   )r  rd  r  rZ   r[   r   )r#  rZ   r[   ry  )
r*  rZ   r#  rZ   rm  rZ   rl  rZ   r[   r   )
r*  rZ   r7  rZ   rm  rZ   rl  rZ   r[   r   r   )
r#  rZ   rm  rZ   rl  rZ   r%  ry  r[   r   )r   r   r   r   r  	frozensetr  rB  r  r   r  r  r  r   r  r2  r  r   rf   rd   r
  r
    s0         D y&&"   `I& & & & 
 
 
 \
i i i i@    \ #h #h #h [#hJ !B !B !B [!BJ OS:d :d :d :d [:d :d :drf   r
  c                  6    e Zd ZdZdZedd	            ZddZdS )SkipSelfMiddlewarezFilter out bot's own messages.z	skip-selfrl  rZ   r,  ry  r[   r\   c                    | r|sdS | |k    S )z2Detect whether the message is from the bot itself.Fr   )rl  r,  s     rd   _is_self_referencez%SkipSelfMiddleware._is_self_reference  s#      	6 	5v%%rf   r  rd  r   c                   K   |                      |j        |j        j                  r-t                              d|j        j        |j                   d S  |             d {V  d S )Nz'[%s] Ignoring self-sent message from %s)rX  rl  re  _bot_idr;  r  r  r  s      rd   r  zSkipSelfMiddleware.handle  sf      ""3#3S[5HII 	LLBCKDTVYVfgggFgiirf   N)rl  rZ   r,  ry  r[   r\   r  )r   r   r   r   r  r   rX  r  r   rf   rd   rV  rV    sQ        ((D& & & \&     rf   rV  c                      e Zd ZdZdZd	dZdS )
ChatRoutingMiddlewarez9Determine chat_id, chat_type, chat_name from push fields.zchat-routingr  rd  r[   r   c                   K   |j         r*d|j          |_        d|_        |j        p|j         |_        n)d|j         |_        d|_        |j        p|j        |_         |             d {V  d S )Nr  r  r  r  )rm  rs  rt  rn  ru  rl  ro  r  s      rd   r  zChatRoutingMiddleware.handle  s      > 	D33>33CK#CMN<cnCMM6C$466CK CM/C33CCMgiirf   Nr  r  r   rf   rd   r\  r\    s3        CCD	 	 	 	 	 	rf   r\  c                  j    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dS )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.
    	dm_policyrZ   dm_allow_fromr   group_policygroup_allow_fromr[   r   c                >    || _         || _        || _        || _        d S r   )
_dm_policy_dm_allow_from_group_policy_group_allow_from)r  r`  ra  rb  rc  s        rd   r  zAccessPolicy.__init__
  s)     $+)!1rf   r\   c                    t          j        dd                                          dv rdS t          j        dd                                          dv S )NGATEWAY_ALLOW_ALL_USERSrv   >   1yestrueTYUANBAO_ALLOW_ALL_USERS)osgetenvlowerr  s    rd   _open_dm_opted_inzAccessPolicy._open_dm_opted_in  sQ    9.3399;;?SSS4y2B77==??CWWWrf   	sender_idc                    | j         dk    rdS | j         dk    r|                                | j        v S | j         dk    r|                                 S dS )u:   Strict DM authorization — pairing does not imply access.disabledF	allowlistopen)re  rk   rf  rr  )r  rs  s     rd   is_dm_allowedzAccessPolicy.is_dm_allowed  s]    ?j((5?k))??$$(;;;?f$$))+++urf   c                    t          |pd                                          }|sdS | j        dk    rdS | j        dk    r	|| j        v S | j        dk    rdS | j        dk    r|                                 S dS )z?Whether a DM may reach gateway intake (pairing handshake path).rv   Fru  rv  pairingTrw  )rZ   rk   re  rf  rr  )r  rs  	principals      rd   is_dm_intake_allowedz!AccessPolicy.is_dm_intake_allowed%  s    	R((..00	 	5?j((5?k)) 333?i''4?f$$))+++urf   rm  c                    | j         dk    rdS | j         dk    r|                                | j        v S | j         dk    rdS | j         dk    r|                                 S dS )zGPlatform-level group chat inbound filter (open / allowlist / disabled).ru  Frv  rz  rw  )rg  rk   rh  rr  r  rm  s     rd   is_group_allowedzAccessPolicy.is_group_allowed4  su    ++5,,##%%)???**5''))+++urf   c                    | j         S r   )re  r  s    rd   r`  zAccessPolicy.dm_policy@  s
    rf   c                    | j         S r   )rg  r  s    rd   rb  zAccessPolicy.group_policyD  s    !!rf   N)
r`  rZ   ra  r   rb  rZ   rc  r   r[   r   r[   r\   )rs  rZ   r[   r\   )rm  rZ   r[   r\   r   )r   r   r   r   r  rr  rx  r|  r  r  r`  rb  r   rf   rd   r_  r_    s         
2 
2 
2 
2X X X X
      
 
 
 
    X " " " X" " "rf   r_  c                      e Zd ZdZdZd	dZdS )
AccessGuardMiddlewarez.Platform-level DM/Group access control filter.zaccess-guardr  rd  r[   r   c                  K   |j         }|j        }|j        dk    rI|                    |j                  s.t
                              d|j        |j        |j                   d S nS|j        dk    rH|	                    |j
                  s.t
                              d|j        |j
        |j                   d S  |             d {V  d S )Nr  z'[%s] DM from %s blocked by dm_policy=%sr  z([%s] Group %s blocked by group_policy=%s)re  _access_policyrt  r|  rl  r;  r  r  r`  r  rm  rb  )r  r  r  re  policys        rd   r  zAccessGuardMiddleware.handleN  s      +&5=D  ..s/?@@ =L#"2F4D    ]g%%**3>:: >L#.&2E   giirf   Nr  r  r   rf   rd   r  r  I  s3        88D     rf   r  c                      e Zd ZdZdZd	dZdS )
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-sethomer  rd  r[   r   c                2  K   |j         }|j        su|                    |          r_t          j        dd          }| p|                    d          o
|j        dk    }|j        dk    rd|_        |r	 ddlm} ddl	m
} dd l} |            }	|	d	z  }
i }|
                                r@t          |
d
          5 }|                    |          pi }d d d            n# 1 swxY w Y   |j        |d<    ||
|           t!          |j                  t          j        d<   t$                              d|j        |j        |j                   n8# t,          $ r+}t$                              d|j        |           Y d }~nd }~ww xY w |             d {V  d S )NYUANBAO_HOME_CHANNELrv   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)re  _auto_sethome_done_sender_may_designate_homero  rp  ra   rt  hermes_constantsr  hermes_cli.configr  yamlexistsrw  	safe_loadrs  rZ   environr;  r<  r  ru  r@  rE  )r  r  r  re  	_cur_home_should_setr  r  r  _homeconfig_pathuser_configfr  s                 rd   r  zAutoSetHomeMiddleware.handler  sI     +) 	Tg.P.PQT.U.U 	T	"8"==I N((22Ls}7L  }$$-1* TT@@@@@@EEEEEEKKK+O--E"'-"7K(*K"))++ B!+@@@ BA*...*;*;*ArKB B B B B B B B B B B B B B B:=+K 67''[AAA9<S[9I9IBJ56KKWck3=   
 ! T T TNN#A7<QRSSSSSSSSTgiis>   6AE <C E  C$$E 'C$(A&E 
F!E??FNr  r  r   rf   rd   r  r  b  s9          D           rf   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dS )(ExtractContentMiddlewarez.Extract raw text and media refs from msg_body.zextract-content  customrC  r[   rZ   c                   |                      dd          }|                      dd          }|r	d| d| dnd| d}|g}t          j        }dD ]j}|                      |          }|rQt          |t                    r<t          |          |k    r|d|         d	z   n|}|                    d
|             nk|r|                    d           d                    |          S )zAFormat elem_type 1010 (share card) into bracket-placeholder text.titlerv   linkz[share_card: z | ])card_content
wechat_desNz...(truncated)z	Preview: z[visit link for full content]r^   )r  r  _CARD_CONTENT_MAX_LENGTHrB  rZ   rx   r   r   )	r  r  r  headerr   max_lenrb  valpreviews	            rd   _format_shared_linkz,ExtractContentMiddleware._format_shared_link  s    

7B''zz&"%%6:X22242222@XPU@X@X@X*C3 	 	E**U##C z#s++ >A#hh>P>P#hwh-*:::VY222333 	:LL8999yyrf   ry  c                2   |                      d          }|sdS 	 t          j        |          }t          |t                    r|                     d          nd}n# t          j        t          f$ r d}Y nw xY w|rt          |t                    sdS d| dS )zNFormat elem_type 1007 (link understanding card) into bracket-placeholder text.r=  Nr  z[link: z | visit link for full content])r  r&  r  rB  rC  JSONDecodeError	TypeErrorrZ   )r  r=  parsedr  s       rd   _format_link_understandingz3ExtractContentMiddleware._format_link_understanding  s     **Y'' 	4	Z((F)3FD)A)AK6::f%%%tDD$i0 	 	 	DDD	 	:dC00 	4>>>>>s   A A A76A7rH  c                ^   | sdS 	 t           j                            t           j                            |           j                  }|                    d          p|                    d          pg }|r't          |d                                                   ndS # t          $ r Y dS 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
        rv   
resourceId
resourceidr   )	urllibparseparse_qsurlparsequeryr  rZ   rk   r@  )rH  r  idss      rd   _parse_resource_idz+ExtractContentMiddleware._parse_resource_id  s      	2	L))&,*?*?*D*D*JKKE))L))JUYY|-D-DJC*-53s1v;;$$&&&25 	 	 	22	s   BB 
B,+B,rp  rg  c                `   g }|D ]}|                     dd          }|                     di           }|dk    r.|                     dd          }|r|                    |           c|dk    r|                     d          }t          |t                    sg }d}t	          |          d	k    r$t          |d	         t
                    r	|d	         }n6t	          |          d
k    r#t          |d
         t
                    r|d
         }t          |pi                      d          pd                                          }	|                     |	          }
|                    |
rd|
 dnd           q|dk    r|                     d|                     d|                     dd                              }t          |                     d          pd                                          }|                     |          }
|
r&|                    |r	d| d|
 dnd|
 d           (|                    |rd| dnd           G|dk    rjt          |                     d          pd                                          }|                     |          }
|                    |
rd|
 dnd           |dk    rjt          |                     d          pd                                          }|                     |          }
|                    |
rd|
 dnd           '|dk    r|                     dd          }|rw	 t          j
        |          }t          |t
                    s|                    d            |                     d!          }|d"k    r*|                    |                     dd#                     n|d$k    r)|                    |                     |                     n|d%k    rC|                     |          }|r|                    |           n[|                    d            nE|d&k    r*|                    |                     dd'                     n|                    d            # t          j        t          f$ r |                    |           Y w xY w|                    d            |d(k    r|                     dd          }d}|ra	 t          j
        |          }|                     d)          pd                                }n"# t          j        t          t          f$ r Y nw xY w|                    |rd*| dnd+           u|r|                    d,| d           |rd-                    |          ndS ).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
        r{  rv   r  r  rY   TIMImageElemimage_info_arrayNrV   r   rH  [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   rB  rg  rx   rC  rZ   rk   r  r&  r  r  r  r  r  AttributeErrorr   )r   rp  partselemr  r=  rY   r  
image_info	image_urlridr  file_url	sound_url	video_urldata_valr  ctyperaw_data	face_name	face_datas                        rd   _extract_textz&ExtractContentMiddleware._extract_text  s     O	/ O	/D!XXj"55I HH]B77GM)){{62.. 'LL&&&n,,#*;;/A#B#B !"2D99 *')$!
'((1,,<LQ<OQU1V1V,!1!!4JJ)**Q..:>Nq>QSW3X3X.!1!!4J!1r 6 6u = = CDDJJLL	,,Y77sI3S3333	JJJJm++";;{GKK
GKKXbdfLgLg4h4hiiw{{5117R88>>@@,,X66 RLLX!h!A(!A!A3!A!A!A!AShbeShShShiiiiLL(!P!68!6!6!6!6QQQQn,,E 2 2 8b99??AA	,,Y77sI3S3333	JJJJ000E 2 2 8b99??AA	,,Y77sI3S3333	JJJJo--";;vr22 ?/!%H!5!5)&$77 %!LL)EFFF$ &

; 7 7 D==!LLFK)H)HIIII"d]]!LL)@)@)H)HIIII"d]]#&#A#A&#I#ID# K %T 2 2 2 2 %-I J J J J"d]]!LLFO)L)LMMMM!LL)EFFF 0)< / / /X...../ LL!=>>>>m++";;vr22	 $(Jx$8$8	%.]]6%:%:%@b$G$G$I$I		 0)^L   	P4	4444yQQQQ /----..."'/sxxR/s+   >RDR+S ?S ;=T99UUrY   c                r    |                                  } |                     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rk   ra   r   s    rd   _rewrite_slash_commandz/ExtractContentMiddleware._rewrite_slash_command1  s;    
 zz||??8$$ 	"abb>Drf   List[Dict[str, str]]c                   g }| pg D ]S}t          |t                    s|                    dd          }|                    di           pi }t          |t                    s]|dk    r|                    d          }t          |t                    sg }d}t	          |          dk    r$t          |d         t                    r	|d         }n6t	          |          dk    r#t          |d         t                    r|d         }t          |pi                     d	          pd                                          }|r|                    d
|d           R|dk    rt          |                    d	          pd                                          }t          |                    d          pd                                          pkt          |                    d          pd                                          p5t          |                    d          pd                                          }	|r!d|d}
|	r|	|
d<   |                    |
           U|S )zExtract inbound image/file references from TIM msg_body.

        Return example:
          [{"kind": "image", "url": "https://..."}, {"kind": "file", "url": "...", "name": "a.pdf"}]
        r{  rv   r  r  r  NrV   r   rH  rQ   kindrH  r  r  r  r  rP   r  )rB  rC  r  rg  rx   rZ   rk   r   )rp  refsr  r{  r=  r  r  r  r  r  refs              rd   _extract_inbound_media_refsz4ExtractContentMiddleware._extract_inbound_media_refs;  sl    &(N "	% "	%DdD)) xx
B//Hhh}b117RGgt,, >))#*;;/A#B#B !"2D99 *')$!
'((1,,<LQ<OQU1V1V,!1!!4JJ)**Q..:>Nq>QSW3X3X.!1!!4J!1r 6 6u = = CDDJJLL	 EKK C CDDD=((w{{5117R88>>@@K006B77==?? B7;;z228b99??AAB7;;z228b99??AA 
  %39(*K*KC  0&/FKK$$$rf   c                t   g }| pg D ]}t          |t                    r|                    d          dk    r2|                    d          pi                     dd          }|s`	 t          j        |          }n# t          j        t          f$ r Y w xY wt          |t                    s|                    d          }|dk    rC|                    d          }|r*t          |t                    r|                    |           |d	k    r|                    d
          }|r	 t          j        |          }t          |t                    r|                    d          nd}|r*t          |t                    r|                    |           # t          j        t          f$ r Y w xY w|S )zTExtract link URLs from share-card (1010) and link-understanding (1007) custom elems.r{  r  r  r+  rv   r  r  r  r  r=  N)	rB  rC  r  r&  r  r  r  rZ   r   )	rp  urlsr  data_strr  r  r  r=  r  s	            rd   _extract_link_urlsz+ExtractContentMiddleware._extract_link_urlsh  s    N 	 	DdD)) TXXj-A-A_-T-T//52::62FFH H--()4   fd++ JJ{++E}}zz&)) &JtS11 &KK%%%$ **Y// !%G!4!45?5M5MWvzz&111SW .JtS$9$9 . KK--- 0)<   s%   (A==BB+A,FF32F3rv   r  ri  c                   | pg D ]Q}t          |t                    r|                    d          dk    r2|                    di           pi }t          |t                    s`|                    dd          }|sy	 t          j        |          }n# t          j        t          f$ r Y w xY wt          |t                    r|                    d          dk    s|                    d          pi }t          |t                    r|s d	S d
 }|                                D ].\  }}	|                    d          s ||	          }
|
|
c c S /Sd	S )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``.
        r{  r  r  r+  rv   r  r  ext_mapNc                   t          | t                    r| sd S 	 t          j        |           }n# t          j        t          f$ r Y d S w xY wt          |          }t          |t                    r|	                    d          dk    r|S d S )Nsub_typerV   )
rB  rZ   base64	b64decodebinasciiErrorrA  r.   rC  r  )valuepbr+  s      rd   _parse_valuezIExtractContentMiddleware._extract_forwarded_records.<locals>._parse_value  s    !%--  U  4 )%00BB 
3      44 .r22dD))  dhhz.B.Ba.G.GKts   0 A
	A
wexin_forward_msg_)	rB  rC  r  r&  r  r  r  r  ra   )rp  r  r  r=  r  r  r  r  keyr  r  s              rd   _extract_forwarded_recordsz3ExtractContentMiddleware._extract_forwarded_records  s     N '	" '	"DdD)) TXXj-A-A_-T-Thh}b117RGgt,, {{62..H H--()4   vt,, K1H1HD1P1Pkk),,2Ggt,, G tt   &mmoo " "
U~~&:;; %e,,%!MMMMM &	" ts   ?BB-,B-r  rd  r   c                T  K   |                      |                     |j                            |_        |                     |j                  |_        |                     |j                  |_        |                     |j        |j	                  |_
         |             d {V  d S r   )r  r  rp  rv  r  rw  r  r  r  rl  rx  r  s      rd   r  zExtractContentMiddleware.handle  s      2243E3Ecl3S3STT99#,GG//== $ ? ?cN^ _ _giirf   N)r  rC  r[   rZ   )r  rC  r[   ry  rH  rZ   r[   rZ   )rp  rg  r[   rZ   r   )rp  rg  r[   r  )rp  rg  r[   rg  r_  )rp  rg  r  rZ   r[   ri  r  )r   r   r   r   r  r  r   r  r  r  r   r  r  r  r  r  r  r   rf   rd   r  r    sD       88D#      \ " ? ? ? \?    \$ ^0 ^0 ^0 [^0@    \ * * * \*X    \@ 8 8 8 8 \8t     rf   r  c                  ^    e Zd ZU dZdZ eh d          Zded<   eddd            Z	ddZ
dS )PlaceholderFilterMiddlewarez>Skip pure placeholder messages (e.g. '[image]' with no media).zplaceholder-filter>      [图片]   [文件]   [视频]   [语音]r  r  r  r  rT  SKIPPABLE_PLACEHOLDERSr   rY   rZ   media_countrq   r[   r\   c                L    |dk    rdS |                                 }|| j        v S )zEDetect whether the message is a pure placeholder (should be skipped).r   F)rk   r  )r   rY   r  r   s       rd   is_skippable_placeholderz4PlaceholderFilterMiddleware.is_skippable_placeholder  s-     ??5::<<3555rf   r  rd  r   c                   K   |                      |j        t          |j                            r-t                              d|j        j        |j                   d S  |             d {V  d S )Nz%[%s] Skipping placeholder message: %r)r  rv  rx   rw  r;  r  re  r  r  s      rd   r  z"PlaceholderFilterMiddleware.handle  sj      ((s3>7J7JKK 	LL@#+BRTWT`aaaFgiirf   N)r   )rY   rZ   r  rq   r[   r\   r  )r   r   r   r   r  rT  r  r`  r   r  r  r   rf   rd   r  r    s         HHD(1	 3 3 3 ) )    
 6 6 6 6 [6     rf   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dS )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/backgroundrT  	ALLOWLISTrY   rZ   r[   c                r    |                                  } |                     d          rd| dd         z   } | S )z?Normalize full-width slash to ASCII slash and strip whitespace.r  r  rV   Nr  r   s    rd   r  z-OwnerCommandMiddleware._rewrite_slash_command  s;     zz||??8$$ 	"abb>Drf   rj  rC  rp  rg  rt  rl  )Tuple[Optional[str], Optional[str], bool]c               6   |dk    s| j         sdS d |pg D             }t          |          dk    rdS |d                             d          pi                     dd          }|                     |          }|                    d	          sdS |                    d
          d                                         }|| j         vrdS t          |pi                     d          pd                                          }	t          |	          o|	|k    }
|||
fS )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  )NNFc                D    g | ]}|                     d           dk    |S )r{  r  r  r  s     rd   r   z@OwnerCommandMiddleware._detect_owner_command.<locals>.<listcomp>  s9     
 
 
uuZ  M11 111rf   rV   r   r  rY   rv   r  )maxsplitr  )
r  rx   r  r  ra   r`   rq  rZ   rk   r\   )r   rj  rp  rt  rl  
text_elemsrY   cmd_liner  owner_idis_owners              rd   _detect_owner_commandz,OwnerCommandMiddleware._detect_owner_command  s=     s}$$
 
 B
 
 

 z??a$$1!!-006B;;FBGG--d33""3'' 	%$$nnan((+1133cm##$$ 
''77=2>>DDFF>>>h,&>Hh&&rf   r  rd  r   c           
       K   |j         }|                     |j        |j        |j        |j                  \  }}}|rz|sxt                              d|j        |j	        |j        |           |
                    t          j        |                    |j	        d| d          d|                      d S |r?|r=|r;t                              d|j        |j	        |j        |           ||_        ||_         |             d {V  d S )N)rj  rp  rt  rl  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)re  r(  rj  rp  rt  rl  r;  r<  r  rs  _track_taskr   rH  sendrz  rv  )r  r  r  re  matched_cmdr%  r'  s          rd   r  zOwnerCommandMiddleware.handle'  sH     +*.*D*D\m)	 +E +
 +
'Xx  
	x 
	KKMck3+;[    3S[*wK*w*w*wxx>>>! ! !    F 	$8 	$ 	$KKFck3+;[   !,C#CLgiirf   Nr   )
rj  rC  rp  rg  rt  rZ   rl  rZ   r[   r   r  )r   r   r   r   r  rT  r  r`  r   r  r   r(  r  r   rf   rd   r  r    s           D %9 & & &  I        \ (' (' (' [('T     rf   r  c                      e Zd ZdZdZd	dZdS )
BuildSourceMiddlewarez(Build SessionSource from context fields.zbuild-sourcer  rd  r[   r   c           	        K   |j         }|                    |j        |j        |j        |j        pd |j        p|j        |j        dk    rdnd           |_         |             d {V  d S )Nr  r  )rs  rt  ru  r  	user_namer  )re  r  rs  rt  ru  rl  ro  rV  r  r  r  re  s       rd   r  zBuildSourceMiddleware.handleJ  s      +))Kmm$,)=S-= # 8 8ffd * 
 

 giirf   Nr  r  r   rf   rd   r/  r/  E  s3        22D
 
 
 
 
 
rf   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dS )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-guardrp  rg  r,  ry  r[   r\   c                t   |sdS | D ]}|                     d          dk    r|                     di                                dd          }|sI	 t          j        |          }n# t          j        t          f$ r Y ww xY w|                     d          dk    r|                     d	          |k    r d
S dS )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.
        Fr{  r  r  r+  rv   r  r  r  T)r  r&  r  r  r  )rp  r,  r  r  r  s        rd   
_is_at_botz!GroupAtGuardMiddleware._is_at_bot_  s      	5 	 	Dxx
##66xxr2266vrBBH H--()4   zz+&&$..6::i3H3HF3R3Rttu   A&&A?>A?rZ   c                   |sdS | D ]}|                     d          dk    r|                     di                                dd          }|sI	 t          j        |          }n# t          j        t          f$ r Y ww xY w|                     d          dk    rU|                     d          |k    r<t          |                     d	          pd                                          }|r|c S dS )
zLExtract the display text used to @-mention this bot (e.g. ``@yuanbao-bot``).rv   r{  r  r  r+  r  r  r  rY   )r  r&  r  r  r  rZ   rk   )rp  r,  r  r  r  mention_texts         rd   _extract_bot_mention_textz0GroupAtGuardMiddleware._extract_bot_mention_textw  s     	2 	( 	(Dxx
##66xxr2266vrBBH H--()4   zz+&&$..6::i3H3HF3R3R"6::f#5#5#;<<BBDD (''''rr7  c                p    t          |pd          }t                              | |          p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.)rZ   r4  r:  )rp  r,  bidbot_mentions       rd   _build_group_channel_promptz2GroupAtGuardMiddleware._build_group_channel_prompt  s[     &%I&&,FFxQWXXe\e&(+& &LW& & &	
rf   N)rq  rx  sender_displayrY   r  rd  rq  rx  ri  r   c                  t          |dd          }|sdS 	 |                    |          }	|j        pd}
|}|r*t                              ||d          }|r|r| d| n|}d| d|
 d	| }d
|t          j        t          j                  	                                dd}|r||d<   |
                    |	j        |           dS # t          $ r,}t                              d|j        |           Y d}~dS 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.
        r:  Nr<  Fr  is_dispatchr^   r  ri   ]
r<  r  T)r;  r=  r   observedrN  z([%s] Failed to observe group message: %s)r>  r?  r  ForwardedRecordsParseMiddlewarebuild_forward_textr   r  r   rQ  rR  rP  r@  r@  r;  rE  r  )r   re  rV  r@  rY   r  rq  rx  rD  session_entryr  	body_textsummary
attributedr  rO  s                   rd   _observe_group_messagez-GroupAtGuardMiddleware._observe_group_message  s     !1488 	F	Z!77??Mn1	GI  J9LL%3E M    J8< I4 4 47 4 4 4'IE^EEgEE)EEJ%%\X\:::DDFF 	 E  -&,l#&&(      	Z 	Z 	ZNNEw|UXYYYYYYYYY	Zs   B/C 
C>!C99C>c           	     t  K   |j         }|j        dk    r|j        s|                     |j        |j                  sl|                     ||j        |j        p|j	        |j
        |j        pd |j        |           t                              d|j        |j        |j	                   d S  |             d {V  d S )Nr  )rq  rx  r  z6[%s] Group message observed (no @bot): chat=%s from=%s)re  rt  rz  r6  rp  rZ  rL  rV  ro  rl  rv  rq  rx  r;  r<  r  rs  r2  s       rd   r  zGroupAtGuardMiddleware.handle  s      +=G##C,=#dooVYVbdkdsFtFt#''S%8%LC<Lclz)T"%"7	 (    KKHck3+;   Fgiirf   )rp  rg  r,  ry  r[   r\   )rp  rg  r,  ry  r[   rZ   )r@  rZ   rY   rZ   r  rd  rq  ry  rx  ri  r[   r   r  )r   r   r   r   r  r   r6  r:  r?  r   rL  r  r   rf   rd   r4  r4  W  s         
 D   \.    \( 
 
 
 \
  !%,0*Z *Z *Z *Z *Z [*ZX     rf   r4  c                      e Zd ZdZdZd	dZdS )
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-attributionr  rd  r[   r   c                Z  K   |j         dk    r|j        s|j        }t                              |j        |j                  |_        |j        pd}|j	        p|j        pd}d| d| d|j
         |_
        |j         t          j        |j        d           |_         |             d {V  d S )Nr  r<  r  ri   rD  )r1  )rt  rz  re  r4  r?  rp  rZ  r  rl  ro  rv  rV  dataclassesr   )r  r  r  re  user_id_labelnickname_labels         rd   r  z!GroupAttributionMiddleware.handle  s      =G##C,=#kG!7!S!Sgo" "C  ,9	M 0QC4DQ	NP~PPPP#,PPCL z%(0tLLL
giirf   Nr  r  r   rf   rd   rO  rO    s9        
 
 D     rf   rO  c                      e Zd ZdZdZdS )YuanbaoMessageTypezYuanbao-local message subtypes; coerced back to :class:`MessageType`
    before leaving the adapter (see :class:`DispatchMiddleware`).chat_recordN)r   r   r   r   CHAT_RECORDr   rf   rd   rU  rU    s!        E E  KKKrf   rU  c                  6    e Zd ZdZdZedd            ZddZdS )ClassifyMessageTypeMiddlewarez>Determine MessageType from text content and msg_body elements.zclassify-msg-typerY   rZ   rp  rg  c                x   |                      d          rt          j        S |D ]
}|                    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    r|                    d	          pi                     d
d          }	 t          j	        |          }n# t          j
        t          f$ r d}Y nw xY wt          |t                    r'|                    d          dk    rt          j        c S t          j        S )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  r{  rv   r  r  r  r  r  r  r+  Nr  r  )ra   r   COMMANDr  PHOTOVOICEVIDEODOCUMENTr&  r  r  r  rB  rC  rU  rW  r/  )rY   rp  r  etyper  r  s         rd   	_classifyz'ClassifyMessageTypeMiddleware._classify	  s`    ??3 	'&& 	: 	:DHHZ,,E&&"((((&&"((((***"((((%%"++++'' HH]339r>>vrJJ"!Z11FF,i8 " " "!FFF"fd++ :

;0G0G40O0O-9999s   ?CC/.C/r  rd  r[   r   c                t   K   |                      |j        |j                  |_         |             d {V  d S r   )ra  rv  rp  r{  r  s      rd   r  z$ClassifyMessageTypeMiddleware.handle!	  s:      ~~clCLAAgiirf   N)rY   rZ   rp  rg  r  )r   r   r   r   r  r   ra  r  r   rf   rd   rY  rY    sQ        HHD      \ 8     rf   rY  c                  .    e Zd ZdZdZddZddZddZdS )QuoteContextMiddlewarez3Extract quote/reply context from cloud_custom_data.zquote-contextrr  rZ   r[   #Tuple[Optional[str], Optional[str]]c                   |sdS 	 t          j        |          }n# t           j        t          f$ r Y dS w xY wt	          |t
                    r|                    d          nd}t	          |t
                    sdS t          |                    d          pd                                          pd}t          |                    d          pd                                          }t          |                    d          p|                    d          pd                                          }|r|r| d	| n|nd}||fS )
zHExtract quote text context, mapping to MessageEvent.reply_to_*.
        r  quoteNidrv   descro  rs  r)  )	r&  r  r  r  rB  rC  r  rZ   rk   )r  rr  r  rg  quote_idri  sender
quote_texts           rd   _extract_quote_contextz-QuoteContextMiddleware._extract_quote_context+	  sT    ! 	:	Z 122FF$i0 	 	 	::	 (2&$'?'?I

7###T%&& 	:uyy,"--3355=599V$$*++1133UYY011QUYY{5K5KQrRRXXZZBFPV=))4)))D
##s    55r  rd  List[Tuple[str, str, str]]c           	     r  K   |j         g S |j        }g }	 t          |dd          }|r|j        g S |                    |j                  }|                    |j                  }t          |pg           D ]}|                    dd          }|r||j         k    r&|                    dd          }	t          |	t                    rd|	v rt                              |	          D ]}
|
                    d          }|
                    d          }|                    d	          \  }}}|                                }|t           v r*|                    |||                                f            nB# t$          $ r5}t&                              d
t          |dd          |           Y d}~nd}~ww xY w|S )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.
        Nr:  rN  rv   r=  r  rV   rG   :z'[%s] quote transcript lookup failed: %sr  yuanbao)r|  re  r>  rV  r?  rA  r@  reversedr  rB  rZ   _YB_RES_REF_REr|   r  	partitionrk   _RESOLVABLE_MEDIA_KINDSr   r@  r;  rE  )r  r  re  rw  rD  rH  historyr-  r   _contentr   r   r  r  r  r  rO  s                    rd   #_extract_media_refs_from_transcriptz:QuoteContextMiddleware._extract_media_refs_from_transcript@	  s      "*I+13
	G%5t<<E CJ.	!77
CCM++M,DEEG2..  gglB// cS%<<<779b11h,, Mh1F1F+44X>> M M wwqzzggajj,0NN3,?,?)a#zz||#:::&--sD(..:J:J.KLLL 	 	 	NN933S       	
 s   E5 EE5 5
F4?+F//F4r   c                   K   |                      |j                  \  |_        |_        |                     |           d {V |_         |             d {V  d S r   )rm  rr  r|  r}  rx  r~  r  s      rd   r  zQuoteContextMiddleware.handlej	  sk      595P5PQTQf5g5g2!2%)%M%Mc%R%RRRRRRRgiirf   N)rr  rZ   r[   re  )r  rd  r[   rn  r  )r   r   r   r   r  rm  rx  r  r   rf   rd   rd  rd  &	  s\        ==D$ $ $ $*( ( ( (T     rf   rd  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dS )rF  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-parser  rd  r[   r   c           	     8  K   	 |j         r7|                     |           |                     |j         |d          |_        nG# t          $ r:}t
                              dt          |j        dd          |           Y d }~nd }~ww xY w |             d {V  d S )NTrB  z,[%s] forwarded-records deep parse failed: %sr  rq  )	rx  _send_loading_heartbeatrG  rv  r@  r;  rE  r>  re  )r  r  r  rO  s       rd   r  z&ForwardedRecordsParseMiddleware.handle	  s      		$ i,,S111#66s7LRUcg6hh 	 	 	NN>VY77       	 giis   >A 
B0BBc                   K   	 | j         j        j                            | j        t
                     d{V  dS # t          $ r Y dS w xY w)z@Best-effort RUNNING heartbeat so the user sees a loading bubble.N)re  	_outbound	heartbeatsend_heartbeat_oncers  r)   r@  )r  s    rd   r|  z7ForwardedRecordsParseMiddleware._send_loading_heartbeat	  s      	+'1EE1            	 	 	DD	s   5; 
A	A	rv   mediarC  
plain_textrZ   $Tuple[str, Optional[Dict[str, str]]]c                   |                     dd          p|                     dd                                                                          }t          |                     d          pd                                          }t          |                     d          pd                                          }t          |                     d          pd                                          }|pt                              |          }|dk    r>|r!|rd| d	|                                 d|d
fS d|p|                                 dfS |dv rE|r*|r(d|d
}|r||d<   d| d	|                                 |fS d|                                 dfS |dk    rB|p#t          |                     d          pd          }	d|	 d|                                 dfS |dk    r>|r!|rd| d	|                                 d|d
fS d|p|                                 dfS d|pd d	|p|                                 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``.
        typerv   doc_typerH  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  r  )r  rk   rq  rZ   r  r  rj   )
r   r  r  
media_typerH  r  r  r  r  
link_titles
             rd   _media_markerz-ForwardedRecordsParseMiddleware._media_marker	  s    ii++HuyyR/H/HOOQQWWYY
%))E""(b))//11uyy,,23399;;		+..4"55;;==	 J2EEcJJ   bs b9s99i99@@BBW]`DaDaaa7i5:77>>@@$FF555 Gs G/5c&B&B ,"+CK8c88Y88??AA3FF(Y((//11477"Cc%))G*<*<*B&C&CJ/Z//#//6688$>>   bs b9s99i99@@BBW]`DaDaaa0i.30077994??>:(>>C,<9>>EEGGMMrf   r  forward_data/Iterator[Tuple[str, str, List[Dict[str, str]]]]c              #     K   t          |t                    r|                    d          ndpg D ]}t          |t                    s|                    dd          }|                    dd          }|                    dg           pg }g }|s|}n5g }|D ]}	t          |	t                    s|	                    dd          }
|
d	k    r*|                    |	                    d
d                     _|
dk    rv|	                    dg           pg D ]\}t          |t                    rE|                     ||          \  }}|                    |           ||                    |           ]|
dk    r|                    d           |r|                    |           d                    d |D                       p|}t          |          | j        k    r|d| j                 dz   }|||fV  dS )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-  Nrk  rv   	plainText
msgContentr  r   rV   rY   rG   
multimediar   u   [嵌套聊天记录]z  c              3     K   | ]}||V  	d S r   r   )r   ps     rd   r   zEForwardedRecordsParseMiddleware._walk_forward_msgs.<locals>.<genexpr>	  s'      $;$;1$;Q$;$;$;$;$;$;rf   u   …(已截断))rB  rC  r  r   r  r   rx   FORWARD_MSG_TEXT_MAX_CHARS)r   r  r-  rk  r  msg_contentsr  renderedr  mcmc_typer  markerr  s                 rd   _walk_forward_msgsz2ForwardedRecordsParseMiddleware._walk_forward_msgs	  sK      0:,/M/MWL$$U+++SW^\^ $	) $	)Cc4(( WWXr**Fb11J77<44:L)+D J%#%& 5 5B%b$// !  ffVQ//G!||RVVFB%7%78888 A%'VVL"%=%=%C 5 5E)%66 5.1.?.?$):/" /" !&V 4 4 4#&?$(KK$4$4$45 !A%;<<<<% 5!LL44499$;$;$;$;$;;;Iz8}}s===#$Dc&D$DEHXX(D(((((I$	) $	)rf   rC  r\   c               h   |j         pd}d| dg}|                     |          D ]<\  }}}|                    | d|            |r|j                            |           =d                    |          }	|r8|j                                        r|	d|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   ：r^   u   

用户附言：)ro  r  r   rw  extendr   rv  rk   )
r   r  r  rC  nicknamer   rk  rM  r  rY   s
             rd   rG  z2ForwardedRecordsParseMiddleware.build_forward_text
  s     &2(6H668XY"%"8"8"F"F 	, 	,FD$LLF--t--... ,%%d+++yy 	A3<--// 	A@#,*<*<*>*>@@@Drf   Nr  r_  )r  rC  r  rZ   r[   r  )r  rC  r[   r  )r  rC  r  rd  rC  r\   r[   rZ   )r   r   r   r   r  r  r   r|  r   r  r  r  rG  r   rf   rd   rF  rF  p	  s         " %D        \ ,.)N )N )N )N [)NX "&2) 2) 2) [2)l    [  rf   rF  c                  t   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	d4d            Z
e	d5d            Ze	d6d            Zed7d            Zed8d            Zed7d            Ze	ddddd9d$            Ze	d:d(            Ze	d;d,            Ze	d<d-            Ze	d=d/            Zed>d2            Zd?d3ZdS )@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_SIZEresource_idrZ   r[   Optional[Tuple[str, str]]c                J   |sdS | j                             |          }|dS |\  }}}t          j                    |z
  | j        k    r| j                             |d           dS t
          j                            |          s| j                             |d           dS ||fS )zOReturn cached ``(local_path, mime)`` if still valid and file exists, else None.N)r  r  r  r  r  ro  pathisfile)r   r  r  
local_pathmimetss         rd   _get_cached_resourcez+MediaResolveMiddleware._get_cached_resource)
  s      	4#''44=4$
D"9;;c777##K6664w~~j)) 	##K66644rf   r  r  r   c                    |sdS t           j                   j        k    rIt           j         fd          }|d j        dz           D ]} j                            |d           ||t          j                    f j        |<   dS )zIStore download result in cache. Evicts oldest entries when over capacity.Nc                *    j         |          d         S )NrG   )r  )r  r   s    rd   <lambda>z=MediaResolveMiddleware._put_cached_resource.<locals>.<lambda>B
  s    CDWXYDZ[\D] rf   )r     )rx   r  r  sortedr  r  )r   r  r  r  sorted_keysr  s   `     rd   _put_cached_resourcez+MediaResolveMiddleware._put_cached_resource;
  s      	Fs"##s'CCC !4:]:]:]:]^^^K !D3#?1#D!DE 1 1#''40000,6dikk+JK(((rf   media_paths	List[str]mimesr\   c                    |                      |          }|dS |\  }}t                              d|j        ||           |                    |           |                    |           dS )z8Append a cached resource to output lists when available.NF'[%s] resource cache hit: rid=%s path=%sT)r  r;  r  r  r   )r   re  r  r  r  hitr  r  s           rd   _append_cached_resourcez.MediaResolveMiddleware._append_cached_resourceG
  sz     &&{33;5
D5L+z	
 	
 	
 	:&&&Ttrf   rH  c                    t           j                            |           j        }t          j                            |          d                                         }|dv r|S dS )z$Guess image extension from URL path.rV   >   .heic.tiff.bmp.gif.jpg.png.jpeg.webpr  )r  r  r  r  ro  splitextrq  )rH  r  exts      rd   _guess_image_ext_from_urlz0MediaResolveMiddleware._guess_image_ext_from_url\
  sX     |$$S)).gt$$Q'--//VVVJvrf   c                  K   |                                 }|st          d          |                                  d{V }t          |                    d          pd                                           }t          |                    d          pd                                           pd}t          |                    d          p| j        p| j                                                   }|r|st          d          | j         d	}d
|||d}t          j	        dd          4 d{V }t          d          D ]M}	|                    |d|i|           d{V }
|
j        dk    r|	dk    rt                              | j        | j        | j                   d{V }t          |                    d          pd                                           }t          |                    d          p|pd                                           pd}t          |                    d          p| j        p| j                                                   }|r|s n0||d<   ||d<   ||d<   0|
                                 |
                                }|                    d          }|dvr)t          d| d|                    dd                     t#          |                    d          t$                    r|                    d          n|}t          |pi                     d          p|pi                     d          pd                                           }|r|c cddd          d{V  S t          d          ddd          d{V  n# 1 d{V swxY w Y   t          d           )!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_idNrT  rv   rV  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+  rH  realUrlz(resource/v1/download missing url/realUrlz)resource/v1/download did not return a URL)rk   r?  _get_cached_tokenrZ   r  rZ  _app_key_api_domainr0  r1  r3  r>  r   r]  _app_secretraise_for_statusr&  rB  rC  )re  r  
token_datarT  rV  r,  api_urlr'  rI  rJ  resprK  r*  r+  real_urls                  rd   _fetch_resource_urlz*MediaResolveMiddleware._fetch_resource_urle
  s      "'')) 	64555"4466666666
JNN7++1r2288::Z^^H--677==??H5Z^^H--TTGDTUU[[]] 	PF 	PNOOO(CCC.	
 
 $TDIII 	O 	O 	O 	O 	O 	O 	OV 88 O O#ZZ{8S]dZeeeeeeee#s**w!||'2'@'@('*=w?R( ( " " " " " "J  
w 7 7 =2>>DDFFE !9!9!LV!LuMMSSUU^Y^F !9!9!`W_!`PWP`aaggiiF   &,GFO).GI&*0GJ'%%'''))++{{6**y((&aTaaUZ\^I_I_aa   /9V9L9Ld.S.S`w{{6***Y`
//66[4:2:J:J9:U:U[Y[\\bbdd $#OO9	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O: ##MNNN;	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O 	O> FGGGs   2H=N#N##
N-0N-c                  K   	 t           j                            |          }n# t          $ r |cY S w xY wt           j                            |j                  }|                    d          p|                    d          pg }|r't          |d                                                   nd}|s|S 	 t          
                    | |           d{V S # t          $ r |cY S w xY 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   rv   N)r  r  r  r@  r  r  r  rZ   rk   r  r  )re  rH  r  r  resource_idsr  s         rd   _resolve_download_urlz,MediaResolveMiddleware._resolve_download_url
  s     	\**3//FF 	 	 	JJJ	 %%fl33yy..O%))L2I2IOR6BJc,q/**00222 	J	/CCG[YYYYYYYYY 	 	 	JJJ	s   $ 336 C C&%C&Nrv   )r  log_tagr  	fetch_urlr  r  ry  r  c                 K   |rA|                      |          }|*t                              d|j        ||d                    |S 	 t	          ||j                   d{V \  }}	n;# t          $ r.}
t                              d|j        |||
           Y d}
~
dS d}
~
ww xY w|dk    r|                     |          }	 t          ||          }n:# t          $ r-}
t                              d|j        ||
           Y d}
~
dS d}
~
ww xY wt          d|           }|                    d	          s|	                    d	          r|	nd
}|                     |||           ||fS |dk    rRt          |          }t          |          p|	                    d          r|	nd}|                     |||           ||fS |sEt          j                            |          }t$          j                            |j                  pd}	 t+          ||          }n:# t          $ r-}
t                              d|j        ||
           Y d}
~
dS d}
~
ww xY wt          |          p|	pd}|                     |||           ||fS )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  r;  r  r  media_download_urlMEDIA_MAX_SIZE_MBr@  rE  r  r   rA  r"   ra   r  r   r  r  r  ro  r  basenamer   )r   re  r  r  r  r  r  r  
file_bytescontent_typerO  r  r  r  r  s                  rd   _download_and_cachez*MediaResolveMiddleware._download_and_cache
  s(       	**;77C=L+s1v   
		-?w'@. . . ( ( ( ( ( ($J  	 	 	NNGdGS   44444	 7??//	::C3JCHHH

   BL'3   ttttt #=3==11D??8,, ['3'>'>x'H'HZ||l$$[*dCCCt##7??/
;;J":..  , 7 7 A AR{  $$[*dCCCt##  	@\**955F((55?I	2:yIIJJ 	 	 	NN;gs   44444	 y))W\W=W  j$???4sH   A' '
B1#BB>C 
D"DDH 
I "IIrw  r  Tuple[List[str], List[str]]c           	        K   g }g }g }|D ]}t          |                    d          pd                                                                          }t          |                    d          pd                                          }t          |                    d          pd                                          }	|t          vs|st
                              |          }
|
r                     |
||          r|                    |||	|
pdf           |s||fS t          j
        j                  d fd
t          j                    }t          j        fd|D             ddi d{V }t          t          j                    |z
  dz            }d}t!          ||          D ]\  \  }}}}}t#          |t$                    r1t&                              dj        ||dd         |           |dz  }P||dz  }X|\  }}|                    |           |                    |           t&                              dj        j        t/          |          t/          |          ||           ||fS )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  rv   rH  r  rZ   r  r  r[   r  c                  K   4 d {V  	                      |           d {V }nL# t          $ r?}t                              dj        | ||           Y d }~d d d           d {V  d S d }~ww xY w                    || |pd d|d d          |           d {V cd d d           d {V  S # 1 d {V swxY w Y   d S )Nz8[%s] inbound media resolve failed: kind=%s url=%s err=%szplaceholder_url=P   r  r  r  r  r  )r  r@  r;  rE  r  r  )	r  rH  r  r  r  rO  re  r   	semaphores	         rd   _resolve_onez@MediaResolveMiddleware._resolve_media_urls.<locals>._resolve_one#  s      !         &)&?&?&M&M M M M M M MII       NNRdC    444               !44'&.$9s3B3x99 # 5                                     s6   B7+B7
A4#A/B7/A440B77
CCc              3  >   K   | ]\  }}}} ||||          V  d S r   r   )r   r  rH  r  r  r  s        rd   r   z=MediaResolveMiddleware._resolve_media_urls.<locals>.<genexpr>:  sO       6 6*$Xs l4h44 6 6 6 6 6 6rf   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  rZ   rH  rZ   r  rZ   r  rZ   r[   r  )rZ   r  rk   rq  ru  r  r  r  r   r   	Semaphoremedia_resolve_concurrencyr  	monotonicgatherrq   ziprB  BaseExceptionr;  rE  r  r<  rx   )r   re  rw  r  r  activer  r  rH  r  r  _t0results_elapsed_ms_failed	_filename_ridr   r  r  r  r  s   ``                  @@rd   _resolve_media_urlsz*MediaResolveMiddleware._resolve_media_urls  s      !#
!#24 		< 		<Cswwv,"--3355;;==Dcggenn*++1133C3776??0b117799H222#2*==cBBC s227C[YY MM4h	r:;;;; 	+{**%g&GHH		 	 	 	 	 	 	 	* n6 6 6 6.46 6 6
 #
 
 
 
 
 
 
 

 4>++c1T9::474H4H 	% 	%0(T3	4&&-00 OL$CRC&   1~1%Jj)))t$$$$ 	iL-KK
OO	
 	
 	
 ;&&rf   r  rn  
log_prefixc          	     `   K   g }g }g }|D ]A\  }}}	|t           vr                     |||          r)|                    |||	f           B|s||fS t          j        j                  d fdt          j                    }
t          j        fd|D             d	d
i d{V }t          t          j                    |
z
  dz            }d}t          ||          D ]\  \  }}}	}t          |t                    r*t                              dj        |||           |dz  }H||dz  }P|\  }}|                    |           |                    |           t                              dj        j        t#          |          t#          |          ||           ||fS )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.
        r  rZ   r  r  r[   r  c           
       K   4 d {V  	                      |            d {V }nM# t          $ r@}t                              dj        | ||           Y d }~d d d           d {V  d S d }~ww xY w                    |||pd  d|  |            d {V cd d d           d {V  S # 1 d {V swxY w Y   d S )Nz-[%s] %s resolve failed: rid=%s kind=%s err=%sz rid=r  )r  r@  r;  rE  r  r  )	r  r  r  	fresh_urlrO  re  r   r  r  s	        rd   r  z@MediaResolveMiddleware._resolve_ybres_refs.<locals>._resolve_one{  s               &)&=&=gs&K&K K K K K K KII       NNGj#tS    444               !44'&.$)5555 # 5                                     s6   B2+B2
A5$A0B20A55*B22
B<?B<c              3  :   K   | ]\  }}} |||          V  d S r   r   )r   r  r  r  r  s       rd   r   z=MediaResolveMiddleware._resolve_ybres_refs.<locals>.<genexpr>  s8      RR4GCxll3h//RRRRRRrf   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  rZ   r  rZ   r  rZ   r[   r  )ru  r  r   r   r  r  r  r  r  rq   r  rB  r  r;  rE  r  r<  rx   )r   re  r  r  r  r  r  r  r  r  r  r  r  r  r   r  r  r  r  s   `` `             @@rd   _resolve_ybres_refsz*MediaResolveMiddleware._resolve_ybres_refs]  sM       "$-/#' 	1 	1Cx222**7CeLL MM3h/0000 	&%%%g&GHH		 	 	 	 	 	 	 	 	( nRRRR6RRR
"
 
 
 
 
 
 
 
 4>++c1T9::-0-A-A 	 	)!S$6&-00 DL*c4   1~1JD$t$$$LL 	iL-KK	
 	
 	
 E!!rf   c                t  K   t          |dd          }|sg g fS 	 |                    |          }|                    |j                  }n<# t          $ r/}t
                              d|j        |           g g fcY d}~S d}~ww xY w|sg g fS |t           d         }g }t                      }	t          |          D ]D}
|
                    d          }t          |t                    rd|vr2t          t                              |                    }t          |          D ]}|                    d          }|                    d          }|                    d          \  }}}|                                }|t(          vrc||	v rh|	                    |           |                    |||                                f           t/          |          t0          k    r nt/          |          t0          k    r nF|                                 |sg g fS |                     ||d	
           d{V S )zYResolve recent observed image/file anchors from transcript into ``(local_paths, mimes)``.r:  Nz.[%s] Observed-media hydration setup failed: %sr=  r  rV   rG   rp  zobserved-mediar  )r>  r?  rA  r@  r@  r;  rE  r   OBSERVED_MEDIA_BACKFILL_LOOKBACKr   rr  r  rB  rZ   rg  rs  r|   r  rt  rk   ru  r   r   rx   ,OBSERVED_MEDIA_BACKFILL_MAX_RESOLVE_PER_TURNreverser  )r   re  rV  rD  rH  rv  rO  r   orderseenr-  r=  matchesr   r   r  r  r  r  s                      rd   _collect_observed_mediaz.MediaResolveMiddleware._collect_observed_media  s     
 !1488 	r6M	!77??M++M,DEEGG 	 	 	NN@c   r6MMMMMM	  	r6M ::;;<,.EEF## 	 	Cggi((Ggs++ y/G/G>227;;<<Gg&&  wwqzzggajj$(NN3$7$7!azz||666$;;c4)9)9:;;;u::!MMME N5zzIII J 	 	r6M,,U'7 - 
 
 
 
 
 
 
 
 	
s   /A 
B$A?9B?Br~  c                B   K   |                      ||d           d{V S )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.
        rg  r	  N)r  )r   re  r~  s      rd   _resolve_quote_mediaz+MediaResolveMiddleware._resolve_quote_media  sK       ,,%' - 
 
 
 
 
 
 
 
 	
rf   r  rd  c                   g }g }| j         }|s||fS t          | j        dd          }|s||fS |                    |          }t	          |t
                    r|s||fS t                      }t                              |          D ]}|	                    d          pd
                                                                }|	                    d          pd
                                }	|	r|	|v rnt          j                            |	          s|                    |	           t!          t          j                            |	                    p	|dk    rdnd}
|                    |	           |                    |
           ||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   rv   rG   rQ   r  r  )r|  r>  re  r  rB  rZ   r   _YB_LOCAL_MEDIA_REr|   r  rk   rq  ro  r  r  r   r"   r  r   )r  pathsr  rid_keycacherY   r  r   r  r  r  s              rd   _collect_quote_local_mediaz1MediaResolveMiddleware._collect_quote_local_media  s    ) 	 %<%94@@ 	 %<yy!!$$$ 	 D 	 %< EE#,,T22 	 	AGGAJJ$"++--3355DGGAJJ$"++--D 44<<7>>$'' HHTNNN"27#3#3D#9#9::  $5O  LLLLe|rf   c                B  	
K   |j         }g 
g 	t                      d	
fd}|                     ||j                   d {V }t	          d |d         D                       } ||           |j        Q|j        r+ ||                     ||j                   d {V            n ||                     |                     no|j	        dk    rd	  || 
                    ||j                   d {V            n8# t          $ r+}t                              d	|j        |           Y d }~nd }~ww xY w
|_        	|_        t$                              |j        |          r(t                              d
|j        |j                   d S  |             d {V  d S )N
pair_listsr  r[   r   c                    | \  }}t          ||          D ]K\  }}|r|v r                    |                               |                               |           Ld S r   )r  r   r   )r  u_listm_listur   r  typesr  s        rd   _add_unique_pairsz8MediaResolveMiddleware.handle.<locals>._add_unique_pairs2  su    'NFFFF++    1 AIIAQ   rf   c              3     K   | ]}|d V  	dS )rV   Nr   )r   r  s     rd   r   z0MediaResolveMiddleware.handle.<locals>.<genexpr>=  s'      55a15555555rf   r   r  z;[%s] observed-image hydration raised, continuing anyway: %sz.[%s] Skip placeholder after media download: %r)r  r  r[   r   )re  r   r  rw  sumr|  r~  r  r  rt  r  rV  r@  r;  rE  r  r  r  r  r  rv  r  )r  r  r  re  r   	own_pairs	own_countrO  r  r  r  s           @@@rd   r  zMediaResolveMiddleware.handle'  s(     
 +EE	  	  	  	  	  	  	  	  227CNKKKKKKKK	559Q<55555	)$$$
 ".# H!!(A(A'3K_(`(`"`"`"`"`"`"`aaaa "!$"A"A#"F"FGGGG]g%%!!(D(DWcj(Y(Y"Y"Y"Y"Y"Y"YZZZZ   QL#         '??iXX 	LLI7<Y\YefffFgiis   *D 
D6!D11D6)r  rZ   r[   r  )r  rZ   r  rZ   r  rZ   r[   r   )r  rZ   r  r  r  r  r[   r\   r  )r  rZ   r[   rZ   )r  rZ   r  rZ   r  ry  r  rZ   r  rZ   r[   r  )rw  r  r[   r  )r  rn  r  rZ   r[   r  )r[   r  )r~  rn  r[   r  )r  rd  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  r  r  r   rf   rd   r  r  
  s        @@D DFOEEEE+77777.11111      [ " 	K 	K 	K [	K    [(    \ 8H 8H 8H \8Ht    \0  $(F  F  F  F  F  [F P X' X' X' [X't R" R" R" [R"h :
 :
 :
 [:
x 

 

 

 [

 ( ( ( \(T; ; ; ; ; ;rf   r  c                  6    e Zd ZdZdZedd	            ZddZdS )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-anchorsrY   rZ   r  r  r  r[   c                   | r|s| S | }t          ||          D ]+\  }}|                    d          st                              |          }|s n|                    d          }|                    d          \  }}	}
|                                }|dk    r|                    d          rd| d}nQ|dk    r=|
                                pt          j        	                    |          }d	| d
| d}n|dk    rd| d}n|d |
                                         |z   ||                                d          z   }-|S )Nr  rV   rp  rQ   r  z[image: r  rP   r  u    → rR   z[video: )r  ra   rs  searchr  rt  rk   ro  r  r  startr}   )rY   r  r  patchedr  r   anchor_matchr   r  r  r  replacementlabels                rd   _patchzPatchAnchorsMiddleware._patcht  s    	4 	Ke$$ 	 	DAq<<$$ )0099L %%a((D $s 3 3D!X::<<Dw1<<#9#9-ooo ((?BG,<,<Q,?,?888A888-ooo.,,,.../,**,,--./ G
 rf   r  rd  r   c                   K   |                      |j        |j        |j                  |_         |             d {V  d S r   )r.  rv  r  r  r  s      rd   r  zPatchAnchorsMiddleware.handle  s>      {{3<QQgiirf   N)rY   rZ   r  r  r  r  r[   rZ   r  )r   r   r   r   r  r   r.  r  r   rf   rd   r&  r&  e  sW        
 
 D   \:     rf   r&  c                  6    e Zd ZdZdZddZedd            ZdS )DispatchMiddlewarez.Build MessageEvent and dispatch to AI handler.dispatchr  rd  r[   r   c                  	K   j         	t          j        	j        j                            dd          	j        j                            dd                    d	fd}j        d	k    r	j        v}	j                            t          j
                              }|                    |           t                              d
	j        |                                pdd d                    |rpt          j        |                     	          dpdd d                    }	j                            |           |                    	j        j                   nat          j         |            dj        pd           }	j                            |           |                    	j        j                    |             d {V  d S )Ngroup_sessions_per_userTthread_sessions_per_userF)r4  r5  r[   r   c                   K   t          j        t          d j        D                       rt          j        n,t          j        t                    rj        nt          j        j	        j
        pd j        t          j                  t          j                  j        j        j        
  
        } r'j
        r j
        j        <   j        pdj        <   j
        r]j        rVj        }j        |j
        <   t)          |          dk    r-t          |          d t)          |          dz
           D ]}||=                     |            d {V  d S )Nc              3  @   K   | ]}|                     d           V  dS ))zapplication/ztext/Nra   )r   mts     rd   r   zMDispatchMiddleware.handle.<locals>._dispatch_inbound_event.<locals>.<genexpr>  s/      ^^2==)BCC^^^^^^rf   )
rY   r,  rV  rN  raw_messager  r  r|  r}  r  rv   r(  )r   rv  anyr  r   r_  rB  r{  r/  rV  rq  rj  rg  r  r|  r}  r  r'  r1  r  rx   handle_message)eventr  r  _skre  r  s      rd   _dispatch_inbound_eventz:DispatchMiddleware.handle.<locals>._dispatch_inbound_event  s      \ ^^co^^^^^*K((
 *4CL+)N)N *$)z:-H// 11$'$;!/"1%  E(  Hsz H36:+C058\5GR-c2z %cl %2$'Lcj!u::##!%[[):#e**s*:):; % %!!HH((///////////rf   r  z-[%s] Group message enqueued (qsize=%d) for %srv   rS   zyuanbao-group-consumer-r.  r*  zyuanbao-inbound-r<  r   )re  r;   rV  configextrar  rt  _group_queues
setdefaultr   Queue
put_nowaitr;  r<  r  qsizerH  _consume_group_queue_inbound_tasksr   rJ  rK  rq  )
r  r  r  r?  is_newqueueconsumerrL  r>  re  s
    `      @@rd   r  zDispatchMiddleware.handle  s     +J$+N$8$<$<=VX\$]$]%,^%9%=%=>XZ_%`%`
 
 
	0 	0 	0 	0 	0 	0 	0 	0@ =G## 55F)44S'-//JJE4555KK?ekkmmciR"-=    K".--gs;;ECI2ss3CEE   &**8444**7+A+IJJJ&''))A
(?iAA  D "&&t,,,""7#9#ABBBgiirf   re  'YuanbaoAdapter'r*  rZ   c                  K   d}| j                             |          }|sdS 	 	 	 t          j        |                                |           d{V }n# t          j        $ r Y nw xY wt
                              d| j        |pddd         |                                           	  |             d{V  || j	        v r#t          j
        d           d{V  || j	        v #n0# t          $ r# t
                              d	| j                   Y nw xY w	 | j                             |d           dS # | j                             |d           w xY w)
zIDrain the group queue one dispatch at a time, waiting for each to finish.rN   NTr  z3[%s] Group queue: dispatching for %s (remaining=%d)rv   rS   g?z[%s] Group queue consumer error)rB  r  r   wait_forTimeoutErrorr;  r  r  rF  r(  rG  r@  	exceptionr  )re  r*  _IDLE_TIMEOUTrJ  dispatch_fns        rd   rG  z'DispatchMiddleware._consume_group_queue  s      %))+66 	F	9V(/(8m(\(\(\"\"\"\"\"\"\KK+   EIL;#4"crc":EKKMM  V%+--'''''''%)AAA%mC000000000 &)AAA  V V V$$%FUUUUUVV  !%%k488888G!%%k48888sS   D5 .A D5 A'$D5 &A''AD5 )<C& %D5 &*DD5 DD5 5ENr  )re  rL  r*  rZ   r[   r   )r   r   r   r   r  r  r   rG  r   rf   rd   r1  r1    sX        88D@ @ @ @D 9 9 9 \9 9 9rf   r1  c                  b    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dS )	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_MIDDLEWARESr[   r  c                p    t                      }| j        D ]}|                     |                        |S )z6Build the default inbound message processing pipeline.)r  rU  r  )r   pipelinemw_clss      rd   buildzInboundPipelineBuilder.build  sA     #$$. 	# 	#FLL""""rf   N)r[   r  )r   r   r   r   r  r   r
  r  rV  r\  r  r  r  r  r/  r4  r  rO  rY  rd  rF  r  r&  r1  rU  r`  r   rY  r   rf   rd   rT  rT    s           	 #"%')(    .    [  rf   rT  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
d0dZd1dZd2dZd0dZd0dZd3dZdZded<   d4dZd5d Zd6d"Zefd7d&Zd0d'Zd/d(Zd/d)Zd0d*Zd+S )8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
    re  rL  r[   r   c                    || _         d | _        d | _        d | _        d | _        i | _        d | _        d| _        d| _        d| _	        i | _
        i | _        d S )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  re  s     rd   r  zConnectionManager.__init__+  se    *.7;268:7;-.%() #(02?Arf   c                    | j         S r   )r^  r  s    rd   wszConnectionManager.ws<  s	    xrf   ry  c                    | j         S r   )r_  r  s    rd   
connect_idzConnectionManager.connect_id@  s    rf   rq   c                    | j         S r   )re  r  s    rd   reconnect_attemptsz$ConnectionManager.reconnect_attemptsD  s    ''rf   r\   c                    | j         dS t          | j         dd           }|du rdS t          |          r)	 t           |                      S # t          $ r Y dS w xY wdS )NFrw  T)r^  r>  callabler\   r@  )r  	open_attrs     rd   is_connectedzConnectionManager.is_connectedH  s    85DHfd33	4I 	IIKK(((   uuus   A 
AAc                   K   | j         }t          s=d}|                    d|d           t                              d|j        |           dS |j        r|j        s=d}|                    d|d           t                              d	|j        |           dS | j	        g	 t          | j	        dd
          }|du st          |          r, |            r"t                              d|j                   dS n# t          $ r Y nw xY w|                    d|j        d          sdS 	 t                              d|j        |j                   t"                              |j        |j        |j        |j                   d
{V }|                    d          rt+          |d                   |_        t                              d|j        |j                   t1          j        t5          j        |j        d
d
d          t8                     d
{V | _	        |                     |           d
{V }|s|                                  d
{V  dS d| _        |                                  t1          j!                    |_"        t1          j#        | $                                d| j%                   | _&        t1          j#        | '                                d| j%                   | _(        t                              d|j        | j%        |j                   tR          *                    |           dS # t0          j+        $ rR t                              d|j                   |                                  d
{V  |,                                 Y dS t          $ r\}t                              d|j        |d           |                                  d
{V  |,                                 Y d
}~dS d
}~ww xY 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] %sNrw  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  )-r]  WEBSOCKETS_AVAILABLE_set_fatal_errorr;  rE  r  r  r  r  r^  r>  rq  r  r@  _acquire_platform_lockr<  r  r   rZ  
_route_envr  rZ   rZ  _ws_urlr   rN  
websocketsconnectCONNECT_TIMEOUT_SECONDS_authenticate_cleanup_wsre  _mark_connectedget_running_loop_looprH  _heartbeat_loopr_  r`  _receive_loopra  YuanbaoAdapter
set_activerO  _release_platform_lock)r  re  r-  rr  r  authedrO  s          rd   rw  zConnectionManager.openX  s     
 -# 	NC$$%A3RV$WWWNNA7<QTUUU5 	w': 	E  $$%BCSX$YYYLLGL#6665 8#DHfd;;	$$))<)<$$LL!Mw|\\\4    --w/1B
 
 	 5:	KK:GL'J]^^^*44 '"5w7J!,  5          J ~~h'' <"%j&:";"; KK/wOOO$-"O"&!%"#	   0        DH  --j99999999F &&(((((((((u ()D$##%%%#466GM#*#6$$&&-T$BR-T-T$ $ $D  &1""$$+M4;K+M+M  DO KK7d.  
 %%g...4# 	 	 	LL4glCCC""$$$$$$$$$**,,,55 	 	 	LL4glCRVLWWW""$$$$$$$$$**,,,55555		s;   !AC7 7
DD&D+L8 C#L8 8AO=	O=!AO88O=c                v  K   | j         rD| j                                          	 | j          d{V  n# t          j        $ r Y nw xY wd| _         | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        t          d          }| j                                        D ]+}|                                s|	                    |           ,| j        
                                 t                                           |                                  d{V  dS )zGCancel background tasks, fail pending futures, and close the WebSocket.NzYuanbaoAdapter disconnected)r`  cancelr   CancelledErrorra  r?  rb  valuesdoneset_exceptionr   r   r  r  )r  disc_excfuts      rd   closezConnectionManager.close  s       	( '')))*********)   #'D ? 	#O""$$$o%%%%%%%%)   "DO   =>>%,,.. 	, 	,C88:: ,!!(+++  """ 	!!!           s!   2 AA/A= =BBr  rC  c                :  K   | j         }| j        dS |                    dd          }|j        p|                    dd          }|                    d          pd}|j        p|                    dd          pd}t          t          j                              }t          d	||||t          t          t          |
	  	        }| j                            |           d{V  t                              d|j        ||           	 t!          j                    }	|	                                t&          z   }
	 |
|	                                z
  }|dk    r"t                              d|j                   dS t!          j        | j                                        |           d{V }t/          |t0          t2          f          s	 t5          t1          |                    }n# t6          $ r Y w xY w|                    di           }|                    dd          }|                    dd          }|t8          d         k    ri|dk    rc|                     |          }|r*|| _        t                              d|j        |           dS t                              d|j                   dS {# t           j         $ r$ t                              d|j                   Y dS t6          $ r.}t                              d|j        |d           Y d}~dS d}~ww xY w)zSend AUTH_BIND and read frames until BIND_ACK is received.

        Returns True on success, False on failure/timeout.
        NFrT  rv   r,  rV  botr  ybBot)	biz_iduidrV  rT  rq  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_typerh   r  Responsez	auth-bindz$[%s] BIND_ACK received: connectId=%sz[%s] BIND_ACK missing connectIdz[%s] AUTH_BIND timeoutz[%s] AUTH_BIND error: %sr  )!r]  r^  r  rZ  r  rZ   uuiduuid4r1   r7  r8  r:  r,  r;  r  r  r   r  r  AUTH_TIMEOUT_SECONDSr  rN  recvrB  r  	bytearrayr,   r@  r$   _extract_connect_idr_  r<  rO  )r  r  re  rT  r  rV  r  rq  
auth_bytesr  deadliner   rawr-  r   r  r  rm  rO  s                      rd   r  zConnectionManager._authenticate  s%     
 -85w++o="!=!=))2U&O*..b*I*IOR	TZ\\""%$.$

 

 


 hmmJ'''''''''=w|VUXYYY%	,..Ezz||&::H%$uzz||3	>>LL!NPWP\]]] 5#,TX]]__iPPPPPPPPP!#y'9:: )%**55CC    H wwvr**88J33hhub))x
333{8J8J!%!9!9#!>!>J! %+5($JGLZdeee#t%FUUU$u7%: # 	 	 	LL17<@@@55 	 	 	LL3W\3QULVVV55555	sW   A-J1 5AJ1 G# "J1 #
G0-J1 /G00BJ1  J1 /J1 1/L#	L,#LLdecoded_msgc                   |                     dd          }|sdS 	 t          t          |                    }t          |d          }|dk    r9t	          |d          }t
                              d| j        j        ||           dS t	          |d          }|r|ndS # t          $ r1}t
          
                    d	| j        j        |           Y d}~dS d}~ww xY w)
z0Extract connectId from decoded BIND_ACK message.r+  rf   NrV   r   rG   z*[%s] AuthBindRsp error: code=%d message=%rr   z$[%s] Failed to extract connectId: %s)r  r%   r(   r'   r&   r;  r  r]  r  r@  rE  )r  r  r+  fdictr*  messagerm  rO  s           rd   r  z%ConnectionManager._extract_connect_id   s    !oofc22 	4	#M$$7$788Eua((Dqyy%eQ//@M&g   t$UA..J!+5::5 	 	 	NNA4=CUWZ[[[44444	s   A)B B 
C'&CCc                  K   | j         }	 |j        rWt          j        t                     d{V  | j        /	 t          t          j                              }t          |          }t          j
                    }|                                }|| _        || j        |<   | j                            |           d{V  t                              d|j        |           	 t          j        |d           d{V  d| _        n# t          j        $ r | j                            |d           | xj        dz  c_        t                              d|j        | j        t.                     | j        t.          k    rYt                              d|j                   |                                  Y | j                            |d           d| _        dS Y nw xY w| j                            |d           d| _        n'# | j                            |d           d| _        w xY wn8# t2          $ r+}t                              d	|j        |           Y d}~nd}~ww xY w|j        UdS dS # t          j        $ r Y dS w xY 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)r]  _runningr   rG  HEARTBEAT_INTERVAL_SECONDSr^  rZ   r  r  r2   r  create_futurerc  rb  r,  r;  r  r  rN  rd  rO  r  rE  HEARTBEAT_TIMEOUT_THRESHOLDschedule_reconnectr@  r  )r  re  rq  
ping_byteslooppong_futurerO  s          rd   r  z!ConnectionManager._heartbeat_loop7  s     -"	" Vm$>?????????8#V ..F!,V!4!4J"355D262D2D2F2FK)4D&1<D&v.(--
333333333LL!=w|VTTT2%.{DIIIIIIIIII8955"/ 
# 
# 
#*..vt<<<55:557#L$*GId    8<WWW"NN+dfmfrsss 33555"*..vt<<<-1*** XW
# *..vt<<<-1** *..vt<<<-1*1111*  V V VLL!A7<QTUUUUUUUUV= " V V V V V@ % 	 	 	DD	s~   /I, B(H( $#D H  B*G2H  3"H( H  GH  #H(  $H$$H( 'I, (
I2!II, II, ,I?>I?c                  K   | j         }	 | j        2 3 d{V }t          |t          t          f          s$|                     t          |                     d{V  M6 dS # t          j        $ r Y dS t          j	        j
        $ r}t          |dd          }t                              d|j        |t          |dd                     |r?|t          v r6t                              d|j        |           |                                 n|                                  Y d}~dS Y d}~dS d}~wt&          $ r@}t                              d|j        |           |                                  Y d}~dS d}~ww xY w)z(Read WS frames and dispatch by cmd_type.Nr*  z3[%s] WebSocket connection closed: code=%s reason=%sreasonrv   z7[%s] Close code %d is non-recoverable, NOT reconnectingz[%s] receive_loop exited: %s)r]  r^  rB  r  r  _handle_framer   r  r  
exceptionsConnectionClosedr>  r;  rE  r  NO_RECONNECT_CLOSE_CODESr  _mark_disconnectedr  r@  )r  re  r  	close_exc
close_coderO  s         rd   r  zConnectionManager._receive_loop`  s     -	&!X 5 5 5 5 5 5 5c!#y'9:: ((s4444444444 &XX % 	 	 	DD$5 	* 	* 	* FD99JNNEj')Xr*J*J    *j,DDDML*   **,,,,''))))))))) -,,,,,  	& 	& 	&NN97<MMM##%%%%%%%%%	&s5   A" AAA" "E74E7BD**E775E22E7r  r  c           	     .  K   | j         }	 t          |          }n9# t          $ r,}t                              d|j        |           Y d}~dS d}~ww xY w|                    di           }|                    dd          }|                    dd          }|                    dd          }|                    d	d
          }	|                    dd          }
|t          d         k    r|dk    rt                              d|j        |           | j        4| j        	                                s| j        
                    d           nN|rL|| j        v rC| j                            |          }|	                                s|
                    d           dS |t          d         k    r(|dv r$t                              d|j        ||           dS |t          d         k    r~|rX|| j        v rO| j                            |          }|	                                s d|i}|
r|
|d<   |
                    |           n"t                              d|j        ||           dS |t          d         k    r}t                              d|j        ||t          |
                     |	rp| j        i	 t!          |          }| j                            |           d{V  n8# t          $ r+}t                              d|j        |           Y d}~nd}~ww xY w|r|| j        v r| j                            |          }|	                                sX	 |
rt%          |
          nd|i}|
                    |           n,# t          $ r}|                    |           Y d}~nd}~ww xY wdS |
rDt                              d|j        |t          |
                     |                     |
           dS t                              d|j        |||           dS )z Handle a single WebSocket frame.z[%s] Failed to decode frame: %sNr   r  rh   r  rv   rq  need_ackFr+  rf   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)r]  r,   r@  r;  r  r  r  r$   rc  r  
set_resultrb  r  r<  rx   r^  r3   r,  r-   r  _push_to_inbound)r  r  re  r-  rO  r   r  r  rq  r  r+  r  r   	ack_bytesack_excdecodeds                   rd   r  zConnectionManager._handle_frame|  s     -	!#&&CC 	 	 	LL:GL#NNNFFFFF	 wwvr""88J++hhub!!(B''88J..ggfc** x
+++vLLBGLRXYYY!-d6H6M6M6O6O-"--d3333 )Fd&888(,,V44xxzz )NN4(((F x
+++ 8
 1
 1
 LLH',X[]cdddF x
+++ &D$666(,,V44xxzz +$d^F .)-vNN6***?L#v   F x'''KKJGLZ]_egjkogpgpqqq [DH0[ / 5 5I(--	2222222222  [ [ [LL!BGLRYZZZZZZZZ[  &D$666(,,V44xxzz //?C"W"5d";";";&RVw////$ / / /))#......../  ,bL#s4yy   %%d+++F?L(C	
 	
 	
 	
 	
sD    
A!AA//K 
L)!LL*M< <
N%N  N%g      ?float_DEBOUNCE_WINDOWr  rZ   c                T   	 t          j        |                    d                    }t          |t                    rw|                    dd          p|                    dd          }|                    dd          p+|                    dd          p|                    dd          }|r| d| S n# t          $ r Y nw xY w	 t          |          }|r/|                    dd           d|                    dd           S n# t          $ r Y nw xY wd	t          |           S )
zLightweight decode to extract sender key for debounce grouping.

        Returns 'from_account:group_code' or a fallback unique key.
        r  rl  rv   r  rm  r  r  rp  
__unknown_)	r&  r  r  rB  rC  r  r@  r-   rh  )r  r  r  rl  rm  rj  s         rd   _extract_sender_keyz%ConnectionManager._extract_sender_key  sk   
	Z 8 899F&$'' :JJ~r22 6zz."55 
 JJ|R00 2zz)R002zz*b11 
   :*99Z999 	 	 	D		&x00D V((>266UU,PR9S9SUUUV 	 	 	D	 +BxLL***s$   B2B6 6
CC?D 
DDc           	        |                      |          }| j                            |d          }|r|                                 || j        vr
g | j        |<   | j        |                             |           t                              d| j        j	        |t          | j        |                              t          j                    }|                    | j        | j        |          }|| j        |<   dS )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  rh  r  r  rg  r   r;  r  r]  r  rx   r   r  
call_laterr  _flush_inbound_buffer)r  r  r  existing_timerr  timers         rd   r  z"ConnectionManager._push_to_inbound  s     &&x00 -11#t<< 	$!!### d***(*D %S!((222@MS)=c)B%C%C	
 	
 	
 '))!&
 

 %*S!!!rf   r  c                   | j                             |d           | j                            |g           }|sdS | j        }t                              d|j        |t          |                     t          ||          }|	                    t          j        |j                            |          d|                      dS )uC   Flush the debounce buffer for a given key — execute the pipeline.Nz1[%s] Debounce flush: key=%s, aggregated %d frames)re  rh  zyuanbao-pipeline-r*  )rh  r  rg  r]  r;  r<  r  rx   rd  r+  r   rH  _inbound_pipeliner  )r  r  r  re  r  s        rd   r  z'ConnectionManager._flush_inbound_buffer  s      d+++(,,S"55	 	F-?L#s9~~	
 	
 	

 WCCCG/%--c22*S**
 
 
 	 	 	 	 	rf   encoded_conn_msgreq_idr   c                  K   | j         t          d          t          j                    }|                                }|| j        |<   	 | j                             |           d{V  t          j        t          j        |          |           d{V }|| j        	                    |d           S # t          j
        $ r  t          $ r  w xY w# | j        	                    |d           w xY 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  )r^  r?  r   r  r  rb  r,  rN  shieldr  rO  r@  )r  r  r  r   r  futurer   s          rd   send_biz_requestz"ConnectionManager.send_biz_request-  s"      8///'))!%!3!3!5!5%+6"		1(-- 0111111111"+GN6,B,BGTTTTTTTTTF ""640000 # 	 	 	 	 	 		 ""640000s   AB6 6CC C1c                    | j         j        r/| j        s*t          j        |                                            dS dS dS )zBSchedule a reconnect only if running and not already reconnecting.N)r]  r  rf  r   rH  _reconnect_with_backoffr  s    rd   r  z$ConnectionManager.schedule_reconnectM  sV    =! 	@$*< 	@ < < > >?????	@ 	@ 	@ 	@rf   c                   K   | j         r't                              d| j        j                   dS d| _         	 |                                  d{V 	 d| _         S # d| _         w xY w)u?   Reconnect with exponential backoff (1s, 2s, 4s, … up to 60s).z,[%s] Reconnect already in progress, skippingFTN)rf  r;  r  r]  r  _do_reconnectr  s    rd   r  z)ConnectionManager._reconnect_with_backoffR  s       	LLGI[\\\5!	'++---------!&DD&&&&s   A 	A$c           	     ,  K   | j         }t          t                    D ]:}|dz   | _        t	          d|z  d          }t
                              d|j        |dz   t          |           t          j	        |           d{V  | 
                                 d{V  	 t                              |j        |j        |j        |j                   d{V }|                    d          rt%          |d                   |_        t          j        t+          j        |j        ddd	          t0          
           d{V | _        |                     |           d{V }|s@t
                              d|j        |dz              | 
                                 d{V  d| _        d| _        |                                 | j        r2| j                                        s| j                                          t          j!        | "                                d| j#                   | _        | j$        r2| j$                                        s| j$                                          t          j!        | %                                d| j#                   | _$        t
                              d|j        |dz   | j#                   tL          '                    |            dS # t          j(        $ r( t
                              d|j        |dz              Y  tR          $ r0}t
                              d|j        |dz   |           Y d}~4d}~ww xY wt
          *                    d|j        t                     |+                                 dS )z>Internal reconnect loop, called under the _reconnecting guard.rV   rG   r   z#[%s] Reconnect attempt %d/%d in %dsNrx  r,  ry  rz  r  z![%s] Re-auth failed on attempt %dr   r~  r*  r  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),r]  r3  MAX_RECONNECT_ATTEMPTSre  minr;  r<  r  r   rG  r  r   r]  r  r  r  r  r  rZ   rZ  rN  r  r  r  r  r^  r  rE  rd  r  r`  r  r  rH  r  r_  ra  r  r  r  rO  r@  r  r  )r  re  rJ  waitr  r  rO  s          rd   r  zConnectionManager._do_reconnect]  s$     -344 A	 A	G'.{D$qG|R((DKK5gk+A4   -%%%%%%%%%""$$$$$$$$$6#.#<#<$g&97;N%0 $= $ $      
 >>(++ @&)*X*>&?&?GO!(!1&&*%)&'	   4" " "        $11*======== NN#FV]`aVabbb**,,,,,,,,,+,(01-'')))' 20D0I0I0K0K 2(//111'.':((**@d.>@@( ( ($
 ? -4?+?+?+A+A -O**,,,")"5&&((;)9;;# # #
 BL'A+t/?   ))'222tt' a a aDglT[^_T_`````   :GL'TU+WZ       
 	8',H^	
 	
 	
 	""$$$us&   DK(#EK((3M	M'%MMc                0  K   | j         }d| _         |	 t          j        |                                t                     d{V  dS # t          j        $ r/ t                              d| j        j	        t                     Y dS t          $ r Y dS w xY wdS )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)r^  r   rN  r  WS_CLOSE_TIMEOUT_SrO  r;  r  r]  r  r@  )r  rk  s     rd   r  zConnectionManager._cleanup_ws  s       X>&rxxzz;MNNNNNNNNNNNN'    TM&(:          >s   3A	 	:B	BBNre  rL  r[   r   )r[   ry  r^  r  r   )r  rC  r[   r\   )r  rC  r[   ry  )r  r  r[   r   )r  r  r[   rZ   )r  r  r[   r   )r  rZ   r[   r   )r  r  r  rZ   r   r  r[   rC  )r   r   r   r   r  r  rk  rm  ro  rs  rw  r  r  r  r  r  r  r  r`  r  r  r  DEFAULT_SEND_TIMEOUTr  r  r  r  r  r   rf   rd   r[  r[     s         B B B B"   X       X  ( ( ( X(    X` ` ` `D! ! ! !BC C C CJ   .% % % %R& & & &8T
 T
 T
 T
p "!!!!+ + + +> *  *  *  *D   4 .	1 1 1 1 1@@ @ @ @
	' 	' 	' 	'J J J JX     rf   r[  c                  X    e Zd ZdZedd            Zedd            ZddZ	 	 dddZdS )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.
    re  rL  kwargsr   r[   Tuple[bytes, str, str]c                
   K   dS )zReturn (file_bytes, filename, content_type).

        Raises:
            ValueError: when file cannot be acquired (not found, empty, etc.)
        Nr   r  re  r  s      rd   acquire_filezMediaSendHandler.acquire_file  r  rf   upload_resultrC  rg  c                    dS )z<Build platform-specific MsgBody list from COS upload result.Nr   r  r  r  s      rd   build_msg_bodyzMediaSendHandler.build_msg_body  s      rf   r\   c                    dS )z:Override to return False for non-COS media (e.g. sticker).Tr   r  s    rd   needs_cos_uploadz!MediaSendHandler.needs_cos_upload  s    trf   Nrs  rZ   reply_tory  caption'SendResult'c           	     4  K   |j         }|j        j        }|j        t	          ddd          S |j                            |           	  | j        |fi | d{V \  }}	}
|                                 r4t          	                    ||	|j
                  }|rt	          d|          S |                                 rt          |          }|                                 d{V }|                    dd          }|                    d	d          p|j        pd}t          |j        |j        ||	||j        
           d{V }t'          ||	|
||d         |d                    d{V }d |                                D             } | j        |f||	|
d|}n | j        i fi |}|r|                    dd|id           |                    dd          }|                    ||||           d{V S # t0          $ r(}t	          dt3          |                    cY d}~S d}~wt4          $ r`}t7          |           j        }t:                              d|j        ||d           t	          dt3          |                    cY d}~S d}~ww xY w)z(Template method: shared media send flow.NFr  Tsuccessr  rv  r  r  rT  rv   r,  )r   r  rT  r  r,  r  
bucketNameregion)r  r  r  credentialsbucketr  c                "    i | ]\  }}|d v	||S )>   r  	file_uuidr  r   )r   r  r  s      rd   
<dictcomp>z+MediaSendHandler.handle.<locals>.<dictcomp>  s4       !Q III qIIIrf   )r  r  r  r  rY   r  rm  rm  z[%s] %s.handle() failed: %sr  ) _connectionr~  rk  rk  r   cancel_slow_notifierr  r  MessageSendervalidate_mediar  r#   r  r  rZ  r   r  r  r  r   r  r  r   dispatch_msg_bodyrA  rZ   r@  r  r   r;  r  r  )r  re  rs  r  r  r  connrk  r  r  r  validation_errr  r  rT  r,  r  r  
fwd_kwargsrp  gcverO  handler_names                           rd   r  zMediaSendHandler.handle  s      "")7?e?dSSSS..w777P	=7Ht7H8 8!8 8 2 2 2 2 2 2.J, $$&& K!.!=!='*C" " " K%e>JJJJ$$&& ,=#J//	 $+#<#<#>#>>>>>>>
'^^GR88NN8R00IGOIr  %8#,&2%!%0% % %       '4)%!- +&|4&x0' ' ' ! ! ! ! ! ! %+\\^^  
 /4.!'%!-	 
 !  /4.r<<V<<  !.?PQQ  
 L"--B11'8XZ\1]]]]]]]]] 	< 	< 	<e3r77;;;;;;;;; 	= 	= 	=::.LLL-lC$     e3s88<<<<<<<<<	=s9   
AG= *EG= =
JH*$J*J7AJJJ)re  rL  r  r   r[   r  )r  rC  r  r   r[   rg  r  r  )re  rL  rs  rZ   r  ry  r  ry  r  r   r[   r  )	r   r   r   r   r
   r  r  r  r  r   rf   rd   r  r    s             ^ K K K ^K    #'!%a= a= a= a= a= a= a=rf   r  c                      e Zd ZdZd Zd ZdS )ImageUrlHandleruD   Strategy: send image from a URL (download → COS → TIMImageElem).c                |  K   |d         }t                               d|j        |           t          ||j                   d {V \  }}|r|dk    r,|                    d          d         }t          |          pd}t          j        	                    |                    d          d                   pd}|||fS )	Nr  z$[%s] ImageUrlHandler: downloading %sr  r  ?r   r  	image.jpg)
r;  r<  r  r  r  r`   r"   ro  r  r  )r  re  r  r  r  r  	path_partr  s           rd   r  zImageUrlHandler.acquire_fileA  s      ,	:GL)TTT);7#<*
 *
 *
 $
 $
 $
 $
 $
 $
 
L  	F|/III!,,Q/I*955EL7##IOOC$8$8$;<<K8\11rf   c                    t          |d         |d         |d         |d         |                    dd          |                    dd          |d         	          S 
NrH  r  r  sizewidthr   heightr  )rH  r  r  r  r  r   	mime_typer    r  r  s      rd   r  zImageUrlHandler.build_msg_bodyM  g    #e$$J'v&##GQ// $$Xq11^,
 
 
 	
rf   Nr   r   r   r   r  r  r   rf   rd   r  r  >  s8        NN
2 
2 
2	
 	
 	
 	
 	
rf   r  c                      e Zd ZdZd Zd ZdS )ImageFileHandleruL   Strategy: send image from a local file path (read → COS → TIMImageElem).c                  K   |d         }t           j                            |          st          d|           t                              d|j        |           t          |d          5 }|                                }d d d            n# 1 swxY w Y   t           j        	                    |          pd}t          |          pd}|||fS )N
image_pathFile not found: z![%s] ImageFileHandler: reading %srbr  r  )ro  r  r  rA  r;  r<  r  rw  readr  r"   )r  re  r  r(  r  r  r  r  s           rd   r  zImageFileHandler.acquire_file\  s       .
w~~j)) 	><
<<===7zRRR*d## 	"qJ	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"7##J//>;&x00@L8\11   -BBBc                    t          |d         |d         |d         |d         |                    dd          |                    dd          |d         	          S r  r"  r  s      rd   r  zImageFileHandler.build_msg_bodyg  r#  rf   Nr$  r   rf   rd   r&  r&  Y  s8        VV	2 	2 	2	
 	
 	
 	
 	
rf   r&  c                      e Zd ZdZd Zd ZdS )FileUrlHandleruB   Strategy: send file from a URL (download → COS → TIMFileElem).c                x  K   |d         }t                               d|j        |           t          ||j                   d {V \  }}|                    d          }|s<|                    d          d         }t          j        	                    |          pd}|r|dk    rt          |          pd}|||fS )	Nr  z#[%s] FileUrlHandler: downloading %sr  r  r  r   rP   r  )r;  r<  r  r  r  r  r`   ro  r  r  r"   )r  re  r  r  r  r  r  r  s           rd   r  zFileUrlHandler.acquire_filev  s      z*97<RRR);'";*
 *
 *
 $
 $
 $
 $
 $
 $
 
L ::j)) 	= s++A.Iw''	22<fH 	S|/III*844R8RL8\11rf   c                X    t          |d         |d         |d         |d                   S NrH  r  r  r  )rH  r  r  r  r!   r  s      rd   r  zFileUrlHandler.build_msg_body  9    "e$J'$v&	
 
 
 	
rf   Nr$  r   rf   rd   r/  r/  s  s8        LL2 2 2
 
 
 
 
rf   r/  c                      e Zd ZdZd Zd ZdS )DocumentHandleruB   Strategy: send local file/document (read → COS → TIMFileElem).c                  K   |d         }t           j                            |          st          d|           t                              d|j        |           t          |d          5 }|                                }d d d            n# 1 swxY w Y   |	                    d          p t           j        
                    |          pd}t          |          pd}|||fS )N	file_pathr)  z [%s] DocumentHandler: reading %sr*  r  r  r  )ro  r  r  rA  r;  r<  r  rw  r+  r  r  r"   )r  re  r  r8  r  r  r  r  s           rd   r  zDocumentHandler.acquire_file  s     ,	w~~i(( 	=;	;;<<<6iPPP)T"" 	"aJ	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"::j))VRW-=-=i-H-HVJ&x00N4N8\11r,  c                X    t          |d         |d         |d         |d                   S r2  r3  r  s      rd   r  zDocumentHandler.build_msg_body  r4  rf   Nr$  r   rf   rd   r6  r6    s8        LL	2 	2 	2
 
 
 
 
rf   r6  c                  &    e Zd ZdZddZd Zd ZdS )	StickerHandlerzAStrategy: send sticker/emoji (TIMFaceElem, no COS upload needed).r[   r\   c                    dS )NFr   r  s    rd   r  zStickerHandler.needs_cos_upload  s    urf   c                
   K   dS )N)rf   stickerr  r   r  s      rd   r  zStickerHandler.acquire_file  s      99rf   c                   ddl m}m}m}m} |                    d          }|                    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: )rE  )!gateway.platforms.yuanbao_stickerr@  rA  rB  rC  r  rA  )
r  r  r  r@  rA  rB  rC  rD  rE  r>  s
             rd   r  zStickerHandler.build_msg_body  s    	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 zz.11ZZ--
#)),77G !G|!G!GHHH))'222#&&*====((**G))'222rf   Nr  )r   r   r   r   r  r  r  r   rf   rd   r;  r;    sL        KK   : : :3 3 3 3 3rf   r;  c                  D    e Zd ZdZddZdd
Z	 dddZddZ	 	 dd dZdS )!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
    re  rL  r[   r   c                    || _         d S r   )r]  ri  s     rd   r  zGroupQueryService.__init__  s    rf   rm  rZ   ri  c                  K   | j         }|j        j        dS t          |          }ddlm}  ||          }|d         d         }	 |j                            ||           d{V }|                    di           }|                    dd          }	|	dk    r#t          	                    d|j
        |	           dS |                    d	d
          p|                    dd
          }
|
r$t          |
t                    rt          |
          S d|iS # t          j        $ r% t          	                    d|j
        |           Y dS t           $ r,}t          	                    d|j
        |           Y d}~dS d}~ww xY w)zQuery group info via WS (group name, owner, member count, etc.).

        Returns:
            Decoded dict or None on failure.
        Nr   r,   r   rq  r  statusz'[%s] query_group_info failed: status=%dr+  rf   rM  rm  z'[%s] query_group_info timeout: group=%sz [%s] query_group_info failed: %s)r]  r  rk  r8   gateway.platforms.yuanbao_protor,   r  r  r;  rE  r  rB  r  r/   r   rO  r@  )r  rm  re  encoded_decoder  r  rL  r   rM  biz_datarO  s               rd   query_group_info_rawz&GroupQueryService.query_group_info_raw  s      -!)4)*55NNNNNN''""*	$0AA'RXAYYYYYYYYH<<++DXXh**F{{H',X^___t||FC00MHLL4M4MH =Jx77 =28<<< *--# 	 	 	NNDglT^___44 	 	 	NN=w|SQQQ44444	s,   A5D >AD D 0E<	E<!E77E<r   r(  offsetrq   limitc                  K   | j         }|j        j        dS t          |||          }ddlm}  ||          }|d         d         }	 |j                            ||           d{V }	|	                    di           }
|
                    dd          }|dk    r#t          	                    d	|j
        |           dS |	                    d
d          p|	                    dd          }|r%t          |t                    rt          |          }ng ddd}|r8|                    d          r#t          j                    |d         f|j        |<   |S # t           j        $ r% t          	                    d|j
        |           Y dS t$          $ r,}t          	                    d|j
        |           Y d}~dS d}~ww xY w)zQuery group member list via WS.

        Returns:
            Decoded dict or None on failure.  Also populates adapter._member_cache.
        NrS  rT  r   rK  r   rq  rL  rM  z,[%s] get_group_member_list failed: status=%dr+  rf   rM  T)membersnext_offsetis_completerW  z,[%s] get_group_member_list timeout: group=%sz%[%s] get_group_member_list failed: %s)r]  r  rk  r9   rN  r,   r  r  r;  rE  r  rB  r  r0   r  _member_cacher   rO  r@  )r  rm  rS  rT  re  rO  rP  r  r  rL  r   rM  rQ  r   rO  s                  rd   get_group_member_list_rawz+GroupQueryService.get_group_member_list_raw  s      -!)4.z&PUVVVNNNNNN''""*	$0AA'RXAYYYYYYYYH<<++DXXh**F{{Mw|]cdddt||FC00MHLL4M4MH PJx77 P9(CC%'$OO U&**Y// U59Y[[&BS4T%j1M# 	 	 	NNI7<Ycddd44 	 	 	NNBGLRUVVV44444	s&   
A5E BE 0F>		F>!F99F>rs  rC  c                   K   |                     d          sddiS |t          d          d         }|                     |           d{V }|ddiS |S )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)ra   rx   rR  )r  rs  rm  r   s       rd   query_group_infoz"GroupQueryService.query_group_info  sz       !!(++ 	NLMMS]]^^,
00<<<<<<<<>9::rf   list_allNactionr  ry  c                  	K   |                     d          sddiS |t          d          d         }|                     |           d{V }|ddiS |                    dg           }|dk    r%|r#|                                		fd|D             }n|d	k    rd
 |D             }d}|r7t          |          dk    r$d |D             }dd                    |          z   }|dd         t          |          |dS )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  r]  NzFailed to query group membersrW  findc                   g | ]}|                     d d          pd                                v sX|                     dd          pd                                v s,|                     dd          pd                                v |S )r  rv   	name_cardr  r  rq  )r   r   r  s     rd   r   z;GroupQueryService.query_session_members.<locals>.<listcomp>E  s       QUU:r228b??AAAAQUU;339r@@BBBBQUU9b117R>>@@@@  A@@rf   	list_botsc                j    g | ]0}d |                     dd          pd                                v .|1S )r  r  rv   re  r   r   s     rd   r   z;GroupQueryService.query_session_members.<locals>.<listcomp>L  sB    \\\QUquuZ7L7L7RPR6Y6Y6[6[-[-[q-[-[-[rf   rv   
   c                    g | ]B}|                     d           p*|                     d          p|                     dd          CS )rd  r  r  rv   r  rh  s     rd   r   z;GroupQueryService.query_session_members.<locals>.<listcomp>Q  sJ    fffYZQUU;''T155+<+<TiQS@T@Tfffrf   zMention with @name: z, rS   )rW  totalmentionHint)ra   rx   r[  r  rq  r   )
r  rs  r`  r  rm  r   rW  mention_hintnamesr  s
            @rd   query_session_membersz'GroupQueryService.query_session_members*  sb       !!(++ 	NLMMS]]^^,
55jAAAAAAAA><==**Y++VJJLLE   "  GG {""\\'\\\G  	Es7||r))ff^efffE1DIIe4D4DDL ss|\\'
 
 	
rf   r  rm  rZ   r[   ri  r   r(  rm  rZ   rS  rq   rT  rq   r[   ri  )rs  rZ   r[   rC  )r_  N)rs  rZ   r`  rZ   r  ry  r[   rC  )	r   r   r   r   r  rR  r[  r^  ro  r   rf   rd   rH  rH    s                   @ >A# # # # #R   " !"	.
 .
 .
 .
 .
 .
 .
rf   rH  c                  D    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	dS )HeartbeatManagerzManages reply heartbeat (RUNNING / FINISH) lifecycle.

    Responsibilities:
      - Periodic RUNNING heartbeat sender (every 2s)
      - Auto-FINISH after 30s inactivity
      - Explicit stop with optional FINISH signal
    re  rL  r[   r   c                0    || _         i | _        i | _        d S r   )r]  _reply_heartbeat_tasks_reply_hb_last_activeri  s     rd   r  zHeartbeatManager.__init__d  s    ?A#79"""rf   rs  rZ   heartbeat_valrq   c                <  K   | j         }|j        }|j        |j        sdS 	 |                    d          r/|t          d          d         }t          |j        ||          }n,|                    d          }t          |j        ||          }|j        	                    |           d{V  |t          k    rdnd}t                              d|j        ||           dS # t          $ r,}	t                              d	|j        |	           Y d}	~	dS d}	~	ww xY w)
z9Send a single heartbeat (RUNNING or FINISH), best effort.Nr  )rl  rm  r  r  )rl  r  r  RUNNINGFINISHz%[%s] Reply heartbeat %s sent: chat=%sz#[%s] send_heartbeat_once failed: %s)r]  r  rk  rZ  ra   rx   r7   removeprefixr6   r,  r)   r;  r  r  r@  )
r  rs  rx  re  r  rm  rO  r  status_namerO  s
             rd   r  z$HeartbeatManager.send_heartbeat_oncei  se     -"7?'/?F	S!!(++ $S]]^^4
5!()+   %11)<<
7!()+  
 ',,w''''''''''48L'L'L))RZKLL7k7      	S 	S 	SLL>cRRRRRRRRR	Ss   CC% %
D/!DDc                  K   | j         }|j        }|j        |j        sdS | j                            |          }|r1|                                st          j                    | j        |<   dS t          j                    | j        |<   t          j
        |                     |          d|           }|| j        |<   dS )zGStart or renew the Reply Heartbeat periodic sender (RUNNING, every 2s).Nzyuanbao-reply-hb-r*  )r]  r  rk  rZ  rv  r  r  r  rw  r   rH  _worker)r  rs  re  r  existingrL  s         rd   r)  zHeartbeatManager.start  s      -"7?'/?F.227;; 	HMMOO 	26)++D&w/F.2ikk"7+"LL!!.W..
 
 
 04#G,,,rf   c                t  K   	 |                      |t                     d{V  	 t          j        t                     d{V  | j                            |d          }t          j                    |z
  t          k    rn6| j	        j
        }|j        n"|                      |t                     d{V  d}n$# t          j        $ r d}Y nt          $ r d}Y nw xY w|s3	 |                      |t                     d{V  n# t          $ r Y nw xY w| j                            |d           | j                            |d           dS # |s3	 |                      |t                     d{V  n# t          $ r Y nw xY w| j                            |d           | j                            |d           w xY w)ztBackground coroutine: send RUNNING heartbeat every 2s.
        30s without renewal -> send FINISH and exit.
        NTr   F)r  r)   r   rG  REPLY_HEARTBEAT_INTERVAL_Srw  r  r  REPLY_HEARTBEAT_TIMEOUT_Sr]  r  rk  r  r@  r*   rv  r  )r  rs  last_activer  	cancelleds        rd   r  zHeartbeatManager._worker  sF     	:**74HIIIIIIIIINm$>?????????"8<<WaHH9;;,/HHH}07?..w8LMMMMMMMMMN$ II % 	 	 	III 	 	 	III	
  227<OPPPPPPPPPP    D'++GT:::&**7D99999  227<OPPPPPPPPPP    D'++GT:::&**7D9999sl   B2B9 6E
 9C
E
 CE
 CE
  !D 
DD
F7!E0/F70
E=:F7<E==:F7Tsend_finishr\   c                @  K   | j                             |d          }|rG|                                s3|                                 	 | d{V  n# t          j        $ r Y nw xY w|r5	 |                     |t                     d{V  dS # t          $ r Y dS w xY wdS )z0Stop Reply Heartbeat and optionally send FINISH.N)	rv  r  r  r  r   r  r  r*   r@  )r  rs  r  rL  s       rd   stopzHeartbeatManager.stop  s      *..w== 			 	KKMMM







)    	..w8KLLLLLLLLLLL   	 	s$   	A A$#A$*!B 
BBc                  K   t          | j                                                  D ]*}|                                s|                                 +| j                                         | j                                         dS )z!Cancel all reply heartbeat tasks.N)rg  rv  r  r  r  r   rw  r  rL  s     rd   r  zHeartbeatManager.close  sz      4;;==>> 	 	D99;; #))+++"((*****rf   Nr  )rs  rZ   rx  rq   r[   r   rs  rZ   r[   r   )Trs  rZ   r  r\   r[   r   r   )
r   r   r   r   r  r  r)  r  r  r  r   rf   rd   rt  rt  [  s         : : : :
S S S S<4 4 4 4(!: !: !: !:F    + + + + + +rf   rt  c                  :    e Zd ZdZddZddZddZddZddZdS )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.
    re  rL  rk  'MessageSender'r[   r   c                0    || _         || _        i | _        d S r   )r]  _sender_tasks)r  re  rk  s      rd   r  zSlowResponseNotifier.__init__  s    /1rf   rs  rZ   c                   K   |                      |           t          j        |                     |          d|           }|| j        |<   dS )zCStart a delayed task that notifies the user when the agent is slow.zyuanbao-slow-resp-r*  N)r  r   rH  	_notifierr  r  rs  rL  s      rd   r)  zSlowResponseNotifier.start  s]      G"NN7##/g//
 
 
  $Grf   c                  K   	 t          j        t                     d{V  t                              d| j        j        t          t                    |           | j        	                    |t                     d{V  dS # t           j        $ r Y dS t          $ r1}t                              d| j        j        |           Y d}~dS d}~ww xY 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   rG  SLOW_RESPONSE_TIMEOUT_Sr;  r<  r]  r  rq   r  send_text_chunkSLOW_RESPONSE_MESSAGEr  r@  r  )r  rs  rO  s      rd   r  zSlowResponseNotifier._notifier  s      
	\- 7888888888KKN"C(?$@$@'   ,..w8MNNNNNNNNNNN% 	 	 	DD 	\ 	\ 	\LLA4=CUWZ[[[[[[[[[	\s   A>B C	C&CCc                    | j                             |d          }|r*|                                s|                                 dS dS dS )z@Cancel the pending slow-response notifier for *chat_id*, if any.N)r  r  r  r  r  s      rd   r  zSlowResponseNotifier.cancel  sS    {w-- 			 	KKMMMMM	 	 	 	rf   c                   K   t          | j                                                  D ]*}|                                s|                                 +| j                                         dS )zCancel all slow-response tasks.N)rg  r  r  r  r  r   r  s     rd   r  zSlowResponseNotifier.close  sc      ++--.. 	 	D99;; rf   N)re  rL  rk  r  r[   r   r  r   )	r   r   r   r   r  r)  r  r  r  r   rf   rd   r  r    s         2 2 2 2
$ $ $ $\ \ \ \        rf   r  c                  v   e Zd ZU dZ eh d          Zded<   dZded<   dIdZdJdZ	dKdZ
	 	 dLdMdZ	 	 dNdOd!Z	 dPdQd&Z	 	 dLdRd)Z	 	 	 dSdTd.ZdUdVd1Z	 dPdWd2Z ej        d3ej                  ZdXd4ZdUdYd5Z	 dPdZd6Zed[d:            Ze	 d\d]d@            Ze	 	 d^d_dF            Zed`dG            ZdadHZdS )br  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_SIZEre  rL  r[   r   c                    || _         t          j                    | _        d | _        d | _        t                      t                      t                      t                      t                      d| _        d S )N)r  
image_filer  r  r>  )r]  collectionsOrderedDict_chat_locks_on_send_start_on_send_finishr  r&  r/  r6  r;  _media_handlersri  s     rd   r  zMessageSender.__init__  sq    GRG^G`G` ?C?C )***,,&(('))%''=
 =
rf   r  rZ   r  r  c                    || j         |<   dS )z1Register (or replace) a named media send handler.N)r  )r  r  r  s      rd   register_handlerzMessageSender.register_handler(  s    %,T"""rf   rs  r   c                   || j         v r'| j                             |           | j         |         S t          | j                   | j        k    rd}t	          | j                   D ]?}| j         |                                         s| j                             |           d} n@|s9| 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_endrx   r  rg  lockedr  r  iterr   r   )r  rs  evictedr  s       rd   get_chat_lockzMessageSender.get_chat_lock.  s   d&&&((111#G,,t  D$;;;GD,--  ',3355 $((---"GE  C $$T$t/?*@*@%A%ABBB$+LNN!((rf   Nrv   r=  r  ry  rm  r  c           
     H  K   | j         }|j        }|j        t          ddd          S | j        r|                     |           |                     |          }|4 d{V  |                     |          }|                     ||j                  }	t          
                    d|j        t          |          |j        t          |	          d |	D                        t          |	          D ]K\  }
}|
dk    r|nd}|                     ||||	           d{V }|j        s|c cddd          d{V  S L	 ddd          d{V  n# 1 d{V swxY w Y   | j        r-	 |                     |           d{V  n# t"          $ r Y nw xY wt          d
          S )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=%sc                ,    g | ]}t          |          S r   rx   r   s     rd   r   z+MessageSender.send_text.<locals>.<listcomp>X  s    555c!ff555rf   r   r
  )r  )r]  r  rk  r   r  r  strip_cron_wrappertruncate_messageMAX_TEXT_CHUNKr;  r<  r  rx   r   r  r  r  r@  )r  rs  r=  r  rm  re  r  lockcontent_to_sendr   r   r   r_tor   s                 rd   	send_textzMessageSender.send_textA  s      -"7?e?dSSSS 	)(((!!'** 	" 	" 	" 	" 	" 	" 	" 	""55g>>O**?G<RSSFKK\c/22G4JF55f555  
 &f-- " "5#$66xxt#33GUDU_3````````~ "!MM	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"""	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"  	**73333333333   $''''s+   #CE;E
EE(F 
FFr  r  r  r   c                   K   | j                             |          }|t          dd|          S  |j        | j        |f||d| d{V S )2Dispatch media send to the named handler strategy.NFzUnknown media handler: r  )r  r  )r  r  r   r  r]  )r  rs  r  r  r  r  r  s          rd   
send_mediazMessageSender.send_mediah  s       &**<88?@@@    $W^M7
w
 
28
 
 
 
 
 
 
 
 	
rf   r  media_files Optional[List[Tuple[str, bool]]]Dict[str, Any]c                  K   | j         }d}|                                r/|                    ||           d{V }|j        sdd|j         iS |pg D ]\  }}t          |          j                                        }|| j        v r|	                    ||           d{V }n|
                    ||           d{V }|j        sdd|j         ic S |ddiS dd||r|j        nddS )	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 processingTrq  )r  platformrs  rN  )r]  rk   r,  r  r  r   suffixrq  r  send_image_filesend_documentrN  )	r  rs  r  r  re  last_result
media_path	_is_voicer  s	            rd   send_directzMessageSender.send_direct~  sr      -.2 ==?? 	N 'Wg > >>>>>>>K& N!L9J!L!LMM &1%6B 	T 	T!J	z"")//11Cdo%%$+$;$;GZ$P$PPPPPPP$+$9$9':$N$NNNNNNN& T!R{?P!R!RSSSST UVV !4?I+00T	
 
 	
rf   rp  rg  c                @  K   |                      |          }|4 d{V  |                    d          r5|t          d          d         }|                     |||           d{V }n3|                    d          }|                     |||           d{V }	 ddd          d{V  n# 1 d{V swxY w Y   |                    d          r$t          d|                    d                    S t          d	|                    d
d                    S )z5Lock + dispatch an arbitrary MsgBody to C2C or group.Nr  r  r
  r  Tr  r  rN  Fr  Unknown errorr  )r  ra   rx   send_group_msg_bodyr|  send_c2c_msg_bodyr  r   )	r  rs  rp  r  rm  r  grpr   r  s	            rd   r  zMessageSender.dispatch_msg_body  s      !!'** 	c 	c 	c 	c 	c 	c 	c 	c!!(++ cc(mmnn-#77XxPPPPPPPP$11)<<
#55j(Wa5bbbbbbbbb	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c 	c ::i   	Ndvzz)7L7LMMMM%vzz'?/S/STTTTs   A>B11
B;>B;r   rY   retryrq   c           	     |  K   | j         }d}t          |          D ]k}	 |                    d          r5|t          d          d         }	|                     |	||           d{V }
n3|                    d          }|                     |||           d{V }
|
                    d          r&t          d|
                    d          	          c S |
                    d
d          }t          
                    d|j        |dz   ||           nL# t          $ r?}t          |          }t          
                    d|j        |dz   ||           Y d}~nd}~ww xY w||dz
  k     rt          j        d|z             d{V  mt                              d|j        ||           t          dd|           S )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  )r]  r3  ra   rx   send_group_messager|  send_c2c_messager  r   r;  rE  r  r@  rZ   r   rG  r  )r  rs  rY   r  r  rm  re  
last_errorrJ  r  r  r  rO  s                rd   r  zMessageSender.send_text_chunk  s#      -)
U|| 	2 	2G%%h// _!#h--..1C $ 7 7T8 L LLLLLLLCC!(!5!5i!@!@J $ 5 5j$S] 5 ^ ^^^^^^^C779%% S%dswwy?Q?QRRRRRR WWWo>>
CL'A+uj        XX
FL'A+uj        ""mAL111111111LL%	
 	
 	
 %/T
/T/TUUUUs   B5D<D
E5EEr  rC  c                R   K   dd|idg}|                      |||           d{V S )z<Send C2C text message, return {success: bool, msg_key: str}.r  rY   r  r
  N)r  )r  r  rY   rm  rp  s        rd   r  zMessageSender.send_c2c_message  sG      !.~NNO++JZ+XXXXXXXXXrf   c                l   K   |                      ||          }|                     |||           d{V S )zDSend group text message, auto-converting @nickname to TIMCustomElem.N)_build_msg_body_with_mentionsr  )r  rm  rY   r  rp  s        rd   r  z MessageSender.send_group_message  sG       55dJGG--j(HMMMMMMMMMrf   z!(?:(?<=\s)|(?<=^))@(\S+?)(?=\s|$)c                   | j         j                            |          }|r.|\  }}t          j                    |z
  | j         j        k     r|ng }ng }|sdd|idgS i }|D ]b}|                    d          p|                    d          pd}	|                    d          pd}
|	r|
r|	|
f||	                                <   cg }d}| j                            |          D ]}|                                }||k    r8|||         	                                }|r|
                    dd|id           |                    d	          }|                    |                                          }|r9|\  }}
|
                    d
dt          j        dd| |
d          id           n|
                    ddd| id           |                                }|t          |          k     r8||d         	                                }|r|
                    dd|id           |s|
                    dd|id           |S )zNParse @nickname patterns and build mixed TIMTextElem + TIMCustomElem msg_body.r  rY   r  r  r  rv   r  r   rV   r  r+  r  @)r  rY   r  N)r]  rZ  r  r  MEMBER_CACHE_TTL_Srq  _AT_USER_REr|   r)  rk   r   r  r&  dumpsr}   rx   )r  rY   rm  rX  r  member_listrW  nickname_to_uidr   nickr  rp  last_idxr   r)  segr  r  	real_nicktails                       rd   r  z+MessageSender._build_msg_body_with_mentions  s   ,00<< 	$OB&*ikkB&69Y&Y&Ykk`bGGG 	P!.~NNOO 	< 	<A55$$@k(:(:@bD%%	""(bC < <15s

-%..t44 	# 	#EKKMMEx8E>*0022 _OOPVX[}$]$]^^^{{1~~H#''(8(899E 	f!&	3 /
9cf+g+g h h$! !     ]FTbX`TbTbKc d deeeyy{{HHc$ii		?((**D \]FTX> Z Z[[[ 	XOOPT~VVWWWrf   c                   K   | j         }dt                       }t          |||j        pd||          }|                     |||           d{V S )z(Send C2C message with arbitrary MsgBody.c2c_rv   )r  rp  rl  rq  rm  N)r]  r:   r4   rZ  _dispatch_encoded)r  r  rp  rm  re  r  rO  s          rd   r  zMessageSender.send_c2c_msg_body2  st      -''')! .B!
 
 
 ++GWfEEEEEEEEErf   c                   K   | j         }dt                       }t          |||j        pd||pd          }|                     |||           d{V S )z*Send group message with arbitrary MsgBody.grp_rv   )rm  rp  rl  rq  
ref_msg_idN)r]  r:   r5   rZ  r  )r  rm  rp  r  re  r  rO  s          rd   r  z!MessageSender.send_group_msg_body?  sz       -'''+! .B~2
 
 
 ++GWfEEEEEEEEErf   rO  r  r  c                  K   	 | j                             ||           d{V }d|                    dd          dS # 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)zBSend pre-encoded bytes via WS and return a normalised result dict.rL  NTrq  rv   )r  r  FzRequest timeout after sr  )r  r  r  r   rO  r  r@  rZ   )re  rO  r  rL  rO  s        rd   r  zMessageSender._dispatch_encodedS  s      
	9$0AA'RXAYYYYYYYYH#Xr0J0JKKK# 	a 	a 	a$/_H\/_/_/_````` 	9 	9 	9$s3xx88888888	9s!   :? B	B'A>8B>B   r  Optional[bytes]r  r  c                    | t          |           dk    rd| S |dz  dz  }t          |           |k    r"t          |           dz  dz  }d| d|dd| d	S dS )
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)r  )r  r  r  	max_bytessize_mbs        rd   r  zMessageSender.validate_mediab  s     ZA!5!5,(,,,$&-	z??Y&&*oo,t3GThTT'TTTKTTTTtrf   r   
max_lengthrr   rs   r  c                    |pt           } ||           |k    r| gS t                              | ||          }d |D             }|r|n| gS )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   c                D    g | ]}t                               d |          S r_  )_INDICATOR_REsubr   s     rd   r   z2MessageSender.truncate_message.<locals>.<listcomp>  s(    ;;;q-##B**;;;rf   )rx   rX   r   )r=  r  rr   r~   r   s        rd   r  zMessageSender.truncate_messageu  sq     }4==J&&9 #66Z 7 
 

 <;F;;;.vvgY.rf   c                8   |                      d          s| S d}d}|                     |          }|                     |          }|dk     s|dk     s||k    r| S | d|         }d|vr| S |t          |          z   }| ||                                         }|p| 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: )ra   rb  ry   rx   rk   )r=  dividerfooter_prefixdivider_pos
footer_posr  
body_startrM  s           rd   r  z MessageSender.strip_cron_wrapper  s     !!"677 	N'ell7++]]=11
??j1nn
k0I0IN+&&&N 3w<</
z*,-3355wrf   c                <   K   | j                                          dS )zCRelease chat locks (no-op for now; placeholder for future cleanup).N)r  r   r  s    rd   r  zMessageSender.close  s!           rf   r  )r  rZ   r  r  r[   r   rs  rZ   r[   r   r  
rs  rZ   r=  rZ   r  ry  rm  rZ   r[   r  r  )rs  rZ   r  rZ   r  ry  r  ry  r  r   r[   r  r   rs  rZ   r  rZ   r  r  r[   r  )
rs  rZ   rp  rg  r  ry  rm  rZ   r[   r  )Nr   rv   )rs  rZ   rY   rZ   r  ry  r  rq   rm  rZ   r[   r  r_  )r  rZ   rY   rZ   rm  rZ   r[   rC  )rm  rZ   rY   rZ   r  ry  r[   rC  )rY   rZ   rm  rZ   r[   rg  )r  rZ   rp  rg  rm  rZ   r[   rC  )rm  rZ   rp  rg  r  ry  r[   rC  )re  rL  rO  r  r  rZ   r[   rC  r  r  r  r  rZ   r  rq   r[   ry  r   )r=  rZ   r  rq   rr   rs   r[   r  )r=  rZ   r[   rZ   r   )r   r   r   r   rT  r  r`  r  r  r  r  r  r  r  r  r  r  r  rz   r{   	MULTILINEr  r  r  r  r   r  r  r  r  r  r   rf   rd   r  r    s          '0i0b0b0b&c&cJcccc(,,,,,
 
 
 
&- - - -) ) ) ). #'%( %( %( %( %(V #'!%
 
 
 
 
4 9=	)
 )
 )
 )
 )
^ #'U U U U U2 #'*V *V *V *V *V\Y Y Y Y Y #'	N N N N N "*A2<PPK2 2 2 2hF F F F F" #'	F F F F F( 
9 
9 
9 \
9 GI    \$  15/ / / / \/<    \,! ! ! ! ! !rf   r  c                      e Zd ZU dZej        Zded<   d1dZd2dZd2dZ		 	 d3d4dZ
d5dZ	 d6d7dZd2dZd8d9d"Zd2d#Zd2d$Zd:d&Zed;d(            Ze	 d<d=d/            Zd>d0ZdS )?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  re  rL  r[   r   c                    || _         t          |          | _        t          |          | _        t          || j                  | _        | j        | j        _        | j	        | j        _
        d S r   )r]  r  rk  rt  r  r  slow_notifier_handle_send_startr  _handle_send_finishr  ri  s     rd   r  zOutboundManager.__init__  s_    %27%;%;+;G+D+D3GQUQ\3]3] &*%<"&*&>###rf   rs  rZ   c                :    | j                             |           dS )zFCalled by MessageSender before sending: cancel slow-response notifier.Nr  r  r  rs  s     rd   r  z"OutboundManager._handle_send_start      !!'*****rf   c                V   K   | j                             |t                     d{V  dS )z=Called by MessageSender after sending: send FINISH heartbeat.N)r  r  r*   r	  s     rd   r  z#OutboundManager._handle_send_finish  s7      n00:MNNNNNNNNNNNrf   Nrv   r=  r  ry  rm  r  c                N   K   | j                             ||||           d{V S )z%Send text message with auto-chunking.r
  N)rk  r  )r  rs  r=  r  rm  s        rd   r  zOutboundManager.send_text  s:      
 [**7GXR\*]]]]]]]]]rf   r  r  r   c                :   K    | j         j        ||fi | d{V S )r  N)rk  r  )r  rs  r  r  s       rd   r  zOutboundManager.send_media  s9       ,T[+G\LLVLLLLLLLLLrf   r  r  r  r  c                J   K   | j                             |||           d{V S )z.Send text + media (used by send_message tool).N)rk  r  )r  rs  r  r  s       rd   r  zOutboundManager.send_direct  s4      
 [,,Wg{KKKKKKKKKrf   c                J   K   | j                             |           d{V  dS )z Start reply heartbeat (RUNNING).N)r  r)  r	  s     rd   start_typingzOutboundManager.start_typing  s4      n""7+++++++++++rf   Fr  r\   c                N   K   | j                             ||           d{V  dS )zStop reply heartbeat.r  N)r  r  )r  rs  r  s      rd   stop_typingzOutboundManager.stop_typing  s9      n!!'{!CCCCCCCCCCCrf   c                J   K   | j                             |           d{V  dS )zStart slow-response notifier.N)r  r)  r	  s     rd   start_slow_notifierz#OutboundManager.start_slow_notifier  s5       &&w///////////rf   c                :    | j                             |           dS )zCancel slow-response notifier.Nr  r	  s     rd   r  z$OutboundManager.cancel_slow_notifier  r
  rf   r   c                6    | j                             |          S )z@Proxy to MessageSender.get_chat_lock for backward compatibility.)rk  r  r	  s     rd   r  zOutboundManager.get_chat_lock  s    {((111rf   collections.OrderedDictc                    | j         j        S )z>Proxy to MessageSender._chat_locks for backward compatibility.)rk  r  r  s    rd   r  zOutboundManager._chat_locks  s     {&&rf   r  r  r  r  r  rq   c                :    t                               | ||          S )z&Proxy to MessageSender.validate_media.)r  r  )r  r  r  s      rd   r  zOutboundManager.validate_media  s    
 ++J+NNNrf   c                   K   | j                                          d{V  | j                                         d{V  | j                                         d{V  dS )zShut down all sub-managers.N)rk  r  r  r  r  s    rd   r  zOutboundManager.close	  s      k!!!!!!!!!n""$$$$$$$$$ &&(((((((((((rf   r  r  r  r  )rs  rZ   r  rZ   r  r   r[   r  r   r  )Fr  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  r  r   r  r  r   rf   rd   r  r    s        	 	 )6(HHHHH? ? ? ?+ + + +O O O O EI^ ^ ^ ^ ^M M M M 9=L L L L L, , , ,D D D D D0 0 0 0+ + + +2 2 2 2 ' ' ' X' GIO O O O \O) ) ) ) ) )rf   r  c                      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dSd            ZedTd            ZdU fdZdVdZedWd            ZdXd Zd!d"dYd$ZdZd%Z	 	 	 d[d\d0Zd]d2Zd^d_d4Zd`d5Zda fd7Zdbd8Z	 dcddd=Zd>ZdedfdAZ	 	 	 dgdhdDZ 	 	 	 dgdidFZ!	 	 	 dgdjdIZ"	 	 	 dgdkdMZ#	 	 	 	 dldmdOZ$dndQZ%dndRZ& xZ'S )or  zCYuanbao AI Bot adapter backed by a persistent WebSocket connection.r   rq   r  TrS   r  i  r  REPLY_REF_MAX_ENTRIESNz$ClassVar[Optional['YuanbaoAdapter']]_active_instancer[   Optional['YuanbaoAdapter']c                    | j         S )z7Return the currently connected YuanbaoAdapter, or None.r  r  s    rd   
get_activezYuanbaoAdapter.get_active  s     ##rf   re  r   c                    || _         dS )z0Register (or clear) the active adapter instance.Nr!  )r   re  s     rd   r  zYuanbaoAdapter.set_active"  s      'rf   r@  r   r  r   c                &   t                                          |t          j                   |j        pi }|                    d          pd                                | _        |                    d          pd                                | _        |                    d          pd | _	        |                    d          pt                                          | _        |                    d          pt                              d          | _        |                    d          pd                                | _        	 t!          |                    d	t"                              }n# t$          t&          f$ r
 t"          }Y nw xY wt)          t*          t-          t.          |                    | _        t3          |           | _        t7          |           | _        t;                      | _        t;                      | _        i | _         d
| _!        tE          d          | _#        i | _$        i | _%        i | _&        i | _'        |                    d          ptQ          j)        dd                                          *                                }|                    d          ptQ          j)        dd          }d |+                    d          D             }|                    d          ptQ          j)        dd                                          *                                }|                    d          ptQ          j)        dd          }	d |	+                    d          D             }
tY          ||||
          | _-        t]          |           | _/        t`          1                                | _2        tQ          j)        d          p|j3        r|j3        j4        nd}tk          |          o|6                    d           | _7        d S )Napp_idrv   r   r,  ws_urlr  r  r  r  rO   i,  )ttl_secondsr`  YUANBAO_DM_POLICYrz  ra  YUANBAO_DM_ALLOW_FROMc                ^    g | ]*}|                                 |                                 +S r   r   r   xs     rd   r   z+YuanbaoAdapter.__init__.<locals>.<listcomp>l  s2    #b#b#b!XYX_X_XaXa#bAGGII#b#b#brf   ,rb  YUANBAO_GROUP_POLICYrc  YUANBAO_GROUP_ALLOW_FROMc                ^    g | ]*}|                                 |                                 +S r   r   r+  s     rd   r   z+YuanbaoAdapter.__init__.<locals>.<listcomp>w  s2    &h&h&hQ^_^e^e^g^g&hqwwyy&h&h&hrf   )r`  ra  rb  rc  r  r  )8superr  r   YUANBAOrA  r  rk   r  r  rZ  DEFAULT_WS_GATEWAY_URLr  DEFAULT_API_DOMAINrj   r  r  rq   _DEFAULT_RESOLVE_CONCURRENCYr  rA  max_MIN_RESOLVE_CONCURRENCYr  _MAX_RESOLVE_CONCURRENCYr  r[  r  r  r~  r   rH  rI  rZ  r  r   r  rB  r'  r1  r  ro  rp  rq  r`   r_  r  rH  _group_queryrT  rY  r  home_channelrs  r\   ra   r  )r  r@  r  _extra_raw_concurrencyr`  _dm_allow_from_rawra  rb  _group_allow_from_rawrc  _existing_homer  s               rd   r  zYuanbaoAdapter.__init__'  s   !1222 #$jj228b??AA!'L!9!9!?R F F H H&,jj&:&:&Bd#ZZ11K5KRRTT!'L!9!9!O=O W WX[ \ \ &

; 7 7 =2DDFF	<"6::.IKg#h#hii:& 	< 	< 	<;	<.1$(*:;;/
 /
& /@.E.E*9$*?*? 25 58EE
 =?). *c::: 8: 46 57" 35 JJ{## 9y,i88
%''%%'' 	 JJ'' 6y0"55 	 $c#b7I7O7OPS7T7T#b#b#b JJ~&& <y/;;
%''%%'' 	 JJ)** 9y3R88 	 'i&h:O:U:UVY:Z:Z&h&h&h*'%-	
 
 
 .d33 3I2N2N2P2P #9:: 
+1+>FF''B 	 )-^(<(<(h^E^E^_gEhEhAhs   (E. .F	F	rL  asyncio.Taskc                x    | j                             |           |                    | j         j                   |S )z@Register a fire-and-forget task so it won't be GC'd prematurely.)rI  r   rJ  rK  r  s     rd   r+  zYuanbaoAdapter._track_task  s8    ""4(((t5=>>>rf   r\   c                    dS )zCYuanbao gates DM/group access at intake via dm_policy/group_policy.Tr   r  s    rd   enforces_own_access_policyz)YuanbaoAdapter.enforces_own_access_policy  s	     trf   r  rd  c                    | j         }t          |j        pd                                          }|sdS |j        dk    rW|                    |          rdS |j        dk    r3ddlm}  |            	                    t          j        j        |          S dS |j        dk    rmt          |j        pd                                          }|sdS |j        d	k    r|                    |          S |j        d
k    r|                                S dS dS )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.
        rv   Fr  Trz  r   )PairingStorer  rv  rw  )r  rZ   rl  rk   rt  rx  r`  gateway.pairingrE  is_approvedr   r2  r  rm  rb  r  rr  )r  r  r  rk  rE  rm  s         rd   r  z)YuanbaoAdapter._sender_may_designate_home  s/     $2S%+,,2244 	5=D  ##F++ t9,,888888#|~~11(2B2H&QQQ5=G##S^1r2288::J u"k11..z:::"f,,//1115urf   F)is_reconnectrH  c               D   K   | j                                          d{V S )zhConnect to Yuanbao WS gateway and authenticate.

        Delegates to ConnectionManager.open().
        N)r  rw  )r  rH  s     rd   r  zYuanbaoAdapter.connect  s/      
 %**,,,,,,,,,rf   c                V  K   t           j        | u rt                               d           d| _        |                                  |                                  | j                                         d{V  | j                                         d{V  t          | j
                  D ]*}|                                s|                                 +| j
                                         | j                                         t                              d| j                   dS )z;Cancel background tasks and close the WebSocket connection.NFz[%s] Disconnected)r  r  r  r  r  r  r  r  r~  rg  rH  r  r  r   rB  r;  r<  r  r  s     rd   
disconnectzYuanbaoAdapter.disconnect  s#     *d22%%d+++!!!##%%% $$&&&&&&&&&n""$$$$$$$$$ ,-- 	 	D99;; !!###  """'33333rf   rv   rs  rZ   r=  r  ry  metadataOptional[Dict[str, Any]]rm  r   c                N   K   | j                             ||||           d{V S )zCSend text message with auto-chunking. Delegates to OutboundManager.r
  N)r~  r  )r  rs  r=  r  rL  rm  s         rd   r,  zYuanbaoAdapter.send  s:       ^--gwU_-`````````rf   r  c                D   K   |                     d          r|ddS |ddS )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  r  r  r8  r	  s     rd   get_chat_infozYuanbaoAdapter.get_chat_info  s:       h'' 	6#W555...rf   ri  c                n   K   	 | j                             |           d{V  dS # t          $ r Y dS w xY w)zGSend "typing" status heartbeat (RUNNING). Delegates to OutboundManager.N)r~  r  r@  )r  rs  rL  s      rd   send_typingzYuanbaoAdapter.send_typing  s[      	.--g66666666666 	 	 	DD	s    & 
44c                r   K   	 | j                             |d           d{V  dS # t          $ r Y dS w xY 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)r~  r  r@  r	  s     rd   r  zYuanbaoAdapter.stop_typing   s`      	.,,W%,HHHHHHHHHHH 	 	 	DD	s   "( 
66r*  c                (  K   |j         j        }| j                            |           d{V  	 t	                                          ||           d{V  | j                            |           dS # | j                            |           w xY w)z9Wrap base class processing with a slow-response notifier.N)rV  rs  r~  r  r1  _process_message_backgroundr  )r  r=  r*  rs  r  s       rd   rU  z*YuanbaoAdapter._process_message_background  s      ,&n00999999999	9''55e[IIIIIIIIIN//88888DN//8888s   (A5 5Bc                F   K   | j                             |           d{V S )z2Query group info (delegates to GroupQueryService).N)r9  rR  r~  s     rd   r^  zYuanbaoAdapter.query_group_info  s/      &;;JGGGGGGGGGrf   r   r(  rS  rT  c                L   K   | j                             |||           d{V S )z9Query group member list (delegates to GroupQueryService).rV  N)r9  r[  )r  rm  rS  rT  s       rd   get_group_member_listz$YuanbaoAdapter.get_group_member_list  s:       &@@TZbg@hhhhhhhhhrf   i'  r  rY   c                   K   | j                             |          st          dd          S t          |          | j        k    r|d| j                 dz   }d| }|                     |||           d{V S )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  rx  r   rx   DM_MAX_CHARSr,  )r  r  rY   rm  rs  s        rd   send_dmzYuanbaoAdapter.send_dm(  s       "0099 	Ue3STTTTt99t(((***+.@@D%G%%YYwYDDDDDDDDDrf   r  r  c                B   K    | j         j        |df|||d| d{V S )zKSend image message (URL). Delegates to OutboundManager via ImageUrlHandler.r  )r  r  r  Nr~  r  )r  rs  r  r  r  rL  r  s          rd   
send_imagezYuanbaoAdapter.send_image?  s_       /T^.[
w)
 
 
 
 
 
 
 
 
 
 	
rf   r(  c                B   K    | j         j        |df|||d| d{V S )zISend local image file. Delegates to OutboundManager via ImageFileHandler.r  )r  r  r(  Nr]  )r  rs  r(  r  r  rL  r  s          rd   r  zYuanbaoAdapter.send_image_fileO  s_       /T^.\
w:
 
 
 
 
 
 
 
 
 
 	
rf   r  r  c                B   K    | j         j        |df|||d| d{V S )zISend file message (URL). Delegates to OutboundManager via FileUrlHandler.r  )r  r  r  Nr]  )r  rs  r  r  r  rL  r  s          rd   	send_filezYuanbaoAdapter.send_file_  s_       /T^.Z
8
 
 
 
 
 
 
 
 
 
 	
rf   rD  rE  Optional[int]c                B   K    | j         j        |df|||d| d{V S )zDSend sticker/emoji. Delegates to OutboundManager via StickerHandler.r>  )r  rD  rE  Nr]  )r  rs  rD  rE  r  r  s         rd   send_stickerzYuanbaoAdapter.send_stickero  s`       /T^.Y
%*
 
 	
 
 
 
 
 
 
 
 	
rf   r8  c                D   K    | j         j        |df||||d| d{V S )zMSend local file (document). Delegates to OutboundManager via DocumentHandler.r  )r  r  r8  r  Nr]  )r  rs  r8  r  r  r  rL  r  s           rd   r  zYuanbaoAdapter.send_document  sb       /T^.Z
w(
 
 	
 
 
 
 
 
 
 
 	
rf   rC  c                v   K   t                               | j        | j        | j        | j                   d{V S )z<Get the current valid sign token (using module-level cache).rx  N)r   rZ  r  r  r  r  r  s    rd   r  z YuanbaoAdapter._get_cached_token  sU       **M4+T-=o + 
 
 
 
 
 
 
 
 	
rf   c                R    | j         }|j        | j        |j        |j        | j        dS )z3Return a snapshot of the current connection status.)	connectedr,  rm  ro  r&  )r  rs  rZ  rm  ro  r  )r  r  s     rd   
get_statuszYuanbaoAdapter.get_status  s4    *l/"&"9l
 
 	
rf   r[   r  )re  r  r[   r   )r@  r   r  r   r[   r   )rL  r@  r[   r@  r  )r  rd  r[   r\   )rH  r\   r[   r\   r   )NNrv   )rs  rZ   r=  rZ   r  ry  rL  rM  rm  rZ   r[   r   )rs  rZ   r[   r  r   )rs  rZ   rL  ri  r[   r   r  )r*  rZ   r[   r   rp  rq  rr  r_  )r  rZ   rY   rZ   rm  rZ   r[   r   )NNN)rs  rZ   r  rZ   r  ry  r  ry  rL  ri  r  r   r[   r   )rs  rZ   r(  rZ   r  ry  r  ry  rL  ri  r  r   r[   r   )rs  rZ   r  rZ   r  ry  r  ry  rL  ri  r  r   r[   r   )rs  rZ   rD  ry  rE  rb  r  ry  r  r   r[   r   )NNNN)rs  rZ   r8  rZ   r  ry  r  ry  r  ry  rL  ri  r  r   r[   r   )r[   rC  )(r   r   r   r   r   r2  PLATFORMr  r`  splits_long_messagesr  r  r  r   r"  r  r  r+  r  rC  r  r  rK  r,  rP  rR  r  rU  r^  rX  rZ  r[  r^  r  ra  rd  r  r  ri  __classcell__)r  s   @rd   r  r    s/        MMHN+..... >BAAAA$ $ $ [$ ' ' ' ['ii ii ii ii ii ii^       X   < 5: - - - - - -4 4 4 46 #'-1	a 	a 	a 	a 	a/ / / /    	 	 	 	9 9 9 9 9 9H H H H
 >Ai i i i i LE E E E E6 "&"&#'
 
 
 
 
( "&"&#'
 
 
 
 
( #'"&#'
 
 
 
 
& '+$("&
 
 
 
 
( #'!%"&#'
 
 
 
 
$
 
 
 
	
 	
 	
 	
 	
 	
 	
 	
rf   r  r[   r  c                 4    t                                           S )z,Delegate to ``YuanbaoAdapter.get_active()``.)r  r"  r   rf   rd   get_active_adapterro    s    $$&&&rf   re  rL  rs  rZ   r  r  r  r  c                J   K   | j                             |||           d{V S )z,Delegate to ``OutboundManager.send_direct``.N)r~  r  )re  rs  r  r  s       rd   send_yuanbao_directrq    s5       "..wMMMMMMMMMrf   rj  r   )
re  rL  rs  rZ   r  rZ   r  r  r[   r  )r   
__future__r   r   r  r  r  rQ  r   r   r&  loggingro  rz   r5  r  urllib.parser  r  r   r   r   enumr   pathlibr   abcr	   r
   typingr   r   r   r   r   r   r   r   sysr0  r  websockets.exceptionsr  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#   rN  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   r;  
hermes_clir<   _HERMES_VERSIONr7  r:  rZ   r9  r  r8  r3  r4  r  r  r  r  r  r  r  r  AUTH_FAILED_CODESAUTH_RETRYABLE_CODESr  r  REPLY_REF_TTL_Sr  r  r{   rs  r  rT  ru  r  r
  r  r5  r7  r8  rX   r   ra  rb  r  rd  r  r  r  r   r  r
  rV  r\  r_  r  r  r  r  r  r/  r4  rO  rU  rY  rd  rF  r  r&  r1  rT  r[  r  r  r&  r/  r6  r;  rH  rt  r  r  r  r  ro  rq  r   rf   rd   <module>r     s   " # " " " " "                				 				        2 2 2 2 2 2 2 2 2 2             # # # # # # # # Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q 



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