
    )j                      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mZmZ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  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*m+Z+m,Z,m-Z-m.Z.m/Z/m0Z0 ddl1m2Z2 ddl3m4Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z; ddl<m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZEmFZFmGZGmHZHmIZImJZJmKZKmLZLmMZMmNZNmOZOmPZPmQZQmRZR ddlSmTZT  e	jU        eV          ZW	 ddlXmYZZ n# e&$ r dZZY nw xY weZZ[eZZ\ e]eD          Z^e!j_        Z`dZadZbdZcdZddZedZfdZgdZhh dZidZjh dZkh dZldZmdZndZodZpd Zq ejr        d!          Zs ejr        d"          Zt eud#d$h          Zv ejr        d%          Zwd&Zxd'Zy G d( d)          Zz G d* d+          Z{dd,lm|Z|m}Z~ e| G d- d.                      Z G d/ d0e          Z G d1 d2          Z G d3 d4e          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@          Z G dA dBe          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\          Z G d] d^          Z G d_ d`e          Z G da dbe          Z G dc dde          Z G de dfe          Z G dg dhe          Z G di dje          Z G dk dl          Z G dm dn          Z G do dp          Z G dq dr          Z G ds dt          Z G du dve+          Zddy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)Path)ABCabstractmethod)AnyCallableClassVarDictListOptionalTupleTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResultcache_document_from_bytescache_image_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_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*\]imagefilez\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)rR   in_fencelines      @/home/wildlama/.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
        FrW   |)rstriprY   striprZ   endswith)rR   trimmed	last_lines      r]   ends_with_table_rowz%MarkdownProcessor.ends_with_table_row   sg     ++-- 	5MM$''+1133	##C((DY-?-?-D-DDr_   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      rC   

u   [。！？.!?]\nra   rW   )lenrfindrecompilefinditerend)rR   ri   rk   _lenwindowlohimidpossentence_end_rebest_posmcuts                r]   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:%%r_   c                P    |                                                      d          S )zDDetermine whether an atomic block is a code block (starts with ```).rX   )lstriprZ   rR   s    r]   is_fence_atomzMarkdownProcessor.is_fence_atom*  s      {{}}''...r_   c                    |                      d          d                                         }|                    d          o|                    d          S )zHDetermine whether an atomic block is a table (first line starts with |).rW   r   rb   )rY   rd   rZ   re   )rR   
first_lines     r]   is_table_atomzMarkdownProcessor.is_table_atom/  sM     ZZ%%a(..00
$$S))Fj.A.A#.F.FFr_   	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)
        rW   Fr\   rS   rT   rU   c                ~    |                                  }|                    d          o|                    d          S )Nrb   )rd   rZ   re   )r\   strippeds     r]   _is_table_linez:MarkdownProcessor.split_into_atoms.<locals>._is_table_lineL  s6    zz||H&&s++F0A0A#0F0FFr_   Nonec                     rTd                               } |                                 r                    |                                             d S d S )NrW   )joinrd   appendclear)atomatomscurrent_liness    r]   _flush_currentz:MarkdownProcessor.split_into_atoms.<locals>._flush_currentP  s`     &yy//::<< 'LL&&&##%%%%%	& &r_   rX   rp   Tra   ro   )r\   rS   rT   rU   rT   r   )rY   r   rZ   rr   rd   )rR   linesr[   r   r   r\   r   r   s         @@r]   split_into_atomsz"MarkdownProcessor.split_into_atoms5  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****r_     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   rT   r   c                 `    r*                      d                                         d S d S )Nrq   )r   r   )chunkscurrent_partss   r]   _flush_partsz;MarkdownProcessor.chunk_markdown_text.<locals>._flush_parts  s9     :fkk-8899999: :r_   rC   rk   Nrp   ra   rq   c                    g | ]}||S  r   .0cs     r]   
<listcomp>z9MarkdownProcessor.chunk_markdown_text.<locals>.<listcomp>  s    '''aQ''''r_   r   )
rr   r   setr   r   addr   	enumerater^   r   )clsrR   ri   rk   rx   r   indivisible_setcurrent_lenr   r   atom_lensep_lenprojected_lenresultidxchunk	remainingheadmergedprevcombinedr   r   s                        @@r]   chunk_markdown_textz%MarkdownProcessor.chunk_markdown_textr  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''''r_   
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'
        rX   rW   r   ro   rb   rq   )rc   r   re   rZ   rh   rY   rd   )r   r   r   prev_trimmednext_trimmedr   s         r]   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vr_   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   rp   )rr   r^   r   r   )r   r   r   icurrentseps         r]   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 r_   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)
        rW      r   ra   z^```(?:markdown|md)?\s*$rX   rp   )rY   rr   rd   rt   match
IGNORECASEr   )rR   r   r   rg   inners        r]   strip_outer_markdown_fencez,MarkdownProcessor.strip_outer_markdown_fence*  s      	K

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

>

$* * * * * *r_   z||ro   )	rY   rd   rZ   re   rt   r   r   r   replace)rR   r   result_linesr\   r   cells
normalizeds          r]   sanitize_markdown_tablez)MarkdownProcessor.sanitize_markdown_tableP  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&&&r_   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   r_   r]   markdown_hint_system_promptz-MarkdownProcessor.markdown_hint_system_prompt  s    V	
 	
r_   )rR   rS   rT   rU   r   )rR   rS   ri   rj   rk   rl   rT   rm   )rR   rS   rT   r   r   N)rR   rS   ri   rj   rk   rl   rT   r   )r   rS   r   rS   rT   rS   )r   r   rT   r   rR   rS   rT   rS   rT   rS   )__name__
__module____qualname____doc__staticmethodr^   rh   r   r   r   r   classmethodr   r   r   r   r   r   r   r_   r]   rQ   rQ      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 *' *' *' \*'\ 
 
 
 \
 
 
r_   rQ   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_keyrS   rT   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     r]   get_refresh_lockzSignManager.get_refresh_lock  s0     #*$$"),..CJwz'""r_   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        r]   compute_signaturezSignManager.compute_signature  sN     	!G+j8x
))++U\\^^W^LLVVXXXr_   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    r]   build_timestampzSignManager.build_timestamp  s<     )!*<*<*<!=!=>>>8999r_   entrydict[str, Any]rU   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     r]   is_cache_validzSignManager.is_cache_valid  s"     [!DIKK/#2LLLr_   r   c                8    | j                                          dS )z;Clear all per-app_key refresh locks (called on disconnect).N)r   r   r   s    r]   clear_lockszSignManager.clear_locks  s     	
r_   rj   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      r]   r   z-SignManager.purge_expired.<locals>.<listcomp>  sE     
 
 
!QQUU;***Q.. ...r_   N)r  r   itemspoprr   )r   expired_keysr  r   s      @r]   purge_expiredzSignManager.purge_expired  s~     ikk
 
 
 
***,,
 
 
  	$ 	$AJNN1d####<   r_   ro   
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)./timeoutNrp      )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 )ro   )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) rc   
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_coderR   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                       r]   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     tokenro   r&  productsourcerN  r&  rL  rO  rP  r  )r  r   r  r  rj   r  r5  r6  r=  r   rJ  )
r   r   r   r  r  cachedremainr%  rL  r  s
             r]   	get_tokenzSignManager.get_token:  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=****%sNrL  r   rM  rN  ro   r&  rO  rP  rQ  )	r5  r?  r   r   r  rJ  r  r  r=  )r   r   r   r  r  r%  rL  r  s           r]   force_refreshzSignManager.force_refreshg  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   rS   rT   r   )
r   rS   r   rS   r   rS   r   rS   rT   rS   r   )r  r  rT   rU   r   rT   rj   ro   )
r   rS   r   rS   r  rS   r  rS   rT   r  )r   r   r   r   r)  r>  r.  r@  r  r,  r   __annotations__r   r   r   r   r   r   r  r
  r  rJ  rT  rW  r   r_   r]   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  ) ) ) ) [) ) )r_   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<    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]pushro   rS   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Optional[str]owner_commandzOptional[Any]rP  msg_typereply_to_message_idreply_to_textquote_media_refs
media_urlsmedia_types	link_urlschannel_prompt) r   r   r   r   rZ  dc_fieldra  rb  rd  re  rf  rg  rh  ri  rj  rk  rl  rm  rn  ro  rp  rq  rs  rP  rt  ru  rv  rw  rx  ry  rz  r{  r   r_   r]   r^  r^    sD          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((((((r_   r^  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.
    ro   rS   namectxr^  next_fnr   rT   r   c                
   K   dS )zEProcess *ctx* and optionally call *next_fn* to continue the pipeline.Nr   selfr  r  s      r]   handlezInboundMiddleware.handle  
        r_   c                >   K   |                      ||           d{V S )zFAllow middleware instances to be called directly (duck-typing compat).N)r  r  s      r]   __call__zInboundMiddleware.__call__  s,      [[g.........r_   c                2    d| j         j         d| j        dS )N<z name=>)	__class__r   r  r  s    r]   __repr__zInboundMiddleware.__repr__  s"    @4>*@@$)@@@@r_   N)r  r^  r  r   rT   r   r   )
r   r   r   r   r  rZ  r	   r  r  r  r   r_   r]   r~  r~    s         
 
 DNNNNT T T ^T/ / / /A A A A A Ar_   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.
    rT   r   c                    g | _         d S r   _middlewaresr  s    r]   __init__zInboundPipeline.__init__  s    "$r_   Nc                F    t          | t                    r	| j        | fS | |fS )zHNormalize (name, handler) or (InboundMiddleware,) into (name, callable).)r<  r~  r  )
name_or_mwhandlers     r]   
_normalizezInboundPipeline._normalize  s/     j"344 	/?J..7""r_   '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         r]   usezInboundPipeline.use  s=     //*g66a  $4111r_   targetrS   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       r]   r   z-InboundPipeline.use_before.<locals>.<genexpr>  1      VV,!YaA!v++A++++VVr_   Nr  nextr   r  r   insert	r  r  r  r  r  r  r  r   r  s	    `       r]   
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r_   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       r]   r   z,InboundPipeline.use_after.<locals>.<genexpr>  r  r_   Nrp   r  r  s	    `       r]   	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r_   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       r]   r   z*InboundPipeline.remove.<locals>.<listcomp>  s+    UUU71a1PT99aAY999r_   r  )r  r  s    `r]   removezInboundPipeline.remove  s'    UUUUd6GUUUr_   ra  c                $    d | j         D             S )zAReturn ordered list of registered middleware names (for testing).c                    g | ]\  }}}|	S r   r   )r   r  r  s      r]   r   z4InboundPipeline.middleware_names.<locals>.<listcomp>#  s    333gaA333r_   r  r  s    r]   middleware_namesz InboundPipeline.middleware_names   s     43!23333r_   r  r^  c                V   K   | j         ddfd              d{V  dS )zKRun all middlewares in order.  Each middleware receives ``(ctx, next_fn)``.r   rT   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 )Nrp   z'[InboundPipeline] middleware [%s] errorTexc_info)rr   r:  r5  error)r  r  when_fnchainr  indexr  s      r]   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    `@@@r]   executezInboundPipeline.execute'  sa      !	 	 	 	 	 	 	 	 	  giir_   r   r   NN)rT   r  )r  rS   rT   r  )r  rS   rT   r  )rT   ra  r  r^  rT   r   )r   r   r   r   r  r   r  r  r  r  r  propertyr  r  r   r_   r]   r  r    s        	 	% % % %
 # # # \#	 	 	 	 		 	 	 	 		 	 	 	 	   
 4 4 4 X4     r_   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_bodyra  rT   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.
        rt  MsgTypero   msg_content
MsgContentrR   rt  r  )r<  r=  r  rS   r   loadsr:  r   )r  r   itemrt  r  s        r]   convert_json_msg_bodyz&DecodeMiddleware.convert_json_msg_bodyH  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_jsonr=  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.).
        Nrf  ro   From_Accountrg  GroupIdgroup_idrj  MsgBodycallback_command
to_account
To_Accountri  	nick_namerh  msg_seqr   MsgSeqrk  msg_keyMsgKeyrl  CloudCustomDatabot_owner_id
botOwnerIdrecall_msg_seq_listlog_exttrace_id)r  rf  r  ri  rg  rh  r  rk  rj  rl  r  r  r  )r  r  r  r<  r=  )r  rf  rg  msg_body_rawrj  s        r]   parse_json_pushz DecodeMiddleware.parse_json_push]  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
 
 	
r_   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Nro   )r   r  r  r:  r<  r=  r  r*   )r  r_  r%  	conn_jsonrd  s        r]   _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  r^  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 )Nro   z;[%s] Push decoded but no valid message. raw hex(first64)=%s   z(empty)z#[%s] Frame decoded (via=%s): len=%drj  TIMTextElemrR   rW   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=%srf  rg  rk  c                :    g | ]}|                     d d          S )rt  ro   r  r   es     r]   r   z+DecodeMiddleware.handle.<locals>.<listcomp>  s&    IIIqQUU:r""IIIr_   z[%s] Push payload: %s)rb  r  r_  r5  r6  r  hexrr   r  rd  re  debug)r  r  r  	data_listmerged_pushre  r%  rd  via
extra_body_seps              r]   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r_   N)r  ra  rT   ra  )r  r=  rT   r  )r%  r  rT   r  r  )
r   r   r   r   r  r   r  r  r  r  r   r_   r]   r  r  =  s          D    \( /
 /
 /
 \/
f   *4 4 4 4 4 4r_   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  r^  rT   r   c                  K   |j         }|                    dd          |_        |                    dd          |_        |                    dd          |_        |                    dd          |_        |                    dg           |_        |                    dd          |_        |                    dd          |_         |             d {V  d S )	Nrf  ro   rg  rh  ri  rj  rk  rl  )	rd  r  rf  rg  rh  ri  rj  rk  rl  )r  r  r  rd  s       r]   r  zExtractFieldsMiddleware.handle  s      x88NB77,33,33"hh'8"==xx
B//XXh++
 $)<b A Agiir_   Nr  r   r   r   r   r  r  r   r_   r]   r  r    s3        BBD	 	 	 	 	 	r_   r  c                      e Zd ZdZdZd	dZdS )
DedupMiddlewarezInbound message deduplication.dedupr  r^  rT   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)rk  r_  _dedupis_duplicater5  r  r  r  s      r]   r  zDedupMiddleware.handle  sl      : 	#+,99#*EE 	LLDckFVX[XbcccFgiir_   Nr  r  r   r_   r]   r  r    s3        ((D     r_   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  r^  rT   r   c                   K   |j         pi                     dd          }|| j        vr |             d {V  d S |                     ||           d S )Nr  ro   )rd  r  _RECALL_COMMANDS_handle_recall)r  r  r  cmds       r]   r  zRecallGuardMiddleware.handle
  sh      x~2""#5r::d+++'))OOOOOOOFC%%%%%r_   rg  rS   rf  c                \    |                      |rd| nd| |rdnd|pd |rdnd           S )Ngroup:direct:groupdmmain)rm  rn  user_id	thread_id)build_source)r_  rg  rf  s      r]   _build_sourcez#RecallGuardMiddleware._build_source  s^    ##.8V*j***>V>V>V!+5gg (D *4ff	 $ 
 
 	
r_   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  rk  ro   r  )rk  r  z2[%s] Recall callback with empty seq_list, skippingrg  rf  )r_  rd  r  r5  r  r  rd   rS   _find_processing_session_interrupt_for_recall_msg_content_cache_patch_transcript)r  r  r	  r_  rd  seq_listr|   seqrg  rf  	seq_entryrecalled_id
matched_skrecalled_contents                 r]   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r_   r  rr  c                j    | j                                         D ]\  }}||k    r|| j        v r|c S d S r   )_processing_msg_idsr  _active_sessions)r_  r  skr|   s       r]   r  z.RecallGuardMiddleware._find_processing_session:  sK    288:: 	 	GBk!!bG,D&D&D			tr_   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)rR   message_typerP  internalz+[%s] Recall interrupt: msg_id=%s session=%s   ro   )r   r   TEXTr  _pending_messagesr!  r  r   r5  r6  r  _processing_msg_texts_schedule_content_redact)r   r_  r#  r  rg  rf  whererecall_textsynth_eventactive_eventrecalled_texts              r]   r  z+RecallGuardMiddleware._interrupt_for_recallA  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r_   r0  c                     d fd}t          j         |                      }j                            |           |                    j        j                   d S )NrT   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:  r-  r   rA  load_transcriptr  	_REDACTEDrewrite_transcriptr5  r6  r  r?  r  )storesidr  
transcriptr  rI  r_  r   rf  rg  r0  r#  s         r]   _redactz?RecallGuardMiddleware._schedule_content_redact.<locals>._redactj  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   r_  r#  r0  rg  rf  r@  tasks   ``````  r]   r+  z.RecallGuardMiddleware._schedule_content_redactg  s    	| 	| 	| 	| 	| 	| 	| 	| 	| 	| 	|: "7799--!%%d+++w8@AAAAAr_   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 )Nr3  z*[%s] Recall: failed to resolve session: %sz*[%s] Recall: failed to load transcript: %sro   
message_idzbranch A1: id matchr4  r5  r6  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   )r4  r6  r   z1[%s] Recall: system note for msg_id=%s (branch B))r7  r8  r  r9  r:  r5  r?  r  r:  r  r;  r<  r6  append_to_transcriptr   r   r   utc	isoformat)r   r_  r  rg  rf  r  r=  r>  rI  r?  r  branch_labelr  s                r]   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  )rg  rS   rf  rS   )r  r^  r	  rS   rT   r   )r  rS   rT   rr  )
r#  rS   r  rS   rg  rS   rf  rS   rT   r   )
r#  rS   r0  rS   rg  rS   rf  rS   rT   r   r   )
r  rS   rg  rS   rf  rS   r  rr  rT   r   )r   r   r   r   r  	frozensetr  r;  r  r   r  r  r  r   r  r+  r  r   r_   r]   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r_   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-selfrf  rS   r&  rr  rT   rU   c                    | r|sdS | |k    S )z2Detect whether the message is from the bot itself.Fr   )rf  r&  s     r]   _is_self_referencez%SkipSelfMiddleware._is_self_reference  s#      	6 	5v%%r_   r  r^  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)rQ  rf  r_  _bot_idr5  r  r  r  s      r]   r  zSkipSelfMiddleware.handle  sf      ""3#3S[5HII 	LLBCKDTVYVfgggFgiir_   N)rf  rS   r&  rr  rT   rU   r  )r   r   r   r   r  r   rQ  r  r   r_   r]   rO  rO    sQ        ((D& & & \&     r_   rO  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  r^  rT   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  )rg  rm  rn  rh  ro  rf  ri  r  s      r]   r  zChatRoutingMiddleware.handle  s      > 	D33>33CK#CMN<cnCMM6C$466CK CM/C33CCMgiir_   Nr  r  r   r_   r]   rU  rU    s3        CCD	 	 	 	 	 	r_   rU  c                  Z    e Zd Z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_policyrS   dm_allow_fromr   group_policygroup_allow_fromrT   r   c                >    || _         || _        || _        || _        d S r   )
_dm_policy_dm_allow_from_group_policy_group_allow_from)r  rY  rZ  r[  r\  s        r]   r  zAccessPolicy.__init__  s)     $+)!1r_   	sender_idrU   c                l    | j         dk    rdS | j         dk    r|                                | j        v S dS )z?Platform-level DM inbound filter (open / allowlist / disabled).disabledF	allowlistT)r^  rd   r_  )r  rb  s     r]   is_dm_allowedzAccessPolicy.is_dm_allowed  s>    ?j((5?k))??$$(;;;tr_   rg  c                l    | j         dk    rdS | j         dk    r|                                | j        v S dS )zGPlatform-level group chat inbound filter (open / allowlist / disabled).rd  Fre  T)r`  rd   ra  r  rg  s     r]   is_group_allowedzAccessPolicy.is_group_allowed  sB    ++5,,##%%)???tr_   c                    | j         S r   )r^  r  s    r]   rY  zAccessPolicy.dm_policy  s
    r_   c                    | j         S r   )r`  r  s    r]   r[  zAccessPolicy.group_policy  s    !!r_   N)
rY  rS   rZ  r   r[  rS   r\  r   rT   r   )rb  rS   rT   rU   )rg  rS   rT   rU   r   )
r   r   r   r   r  rf  ri  r  rY  r[  r   r_   r]   rX  rX    s         
2 
2 
2 
2          X " " " X" " "r_   rX  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  r^  rT   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)r_  _access_policyrn  rf  rf  r5  r  r  rY  ri  rg  r[  )r  r  r  r_  policys        r]   r  zAccessGuardMiddleware.handle!  s      +&5=D  ''(899 =L#"2F4D    ]g%%**3>:: >L#.&2E   giir_   Nr  r  r   r_   r]   rm  rm    s3        88D     r_   rm  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.
    zauto-sethomer  r^  rT   r   c                  K   |j         }|j        s_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_CHANNELro   r  r  Tr   )get_hermes_home)atomic_yaml_writezconfig.yamlr  )encodingz=[%s] Auto-sethome: designated %s (%s) as Yuanbao home channelz[%s] Auto-sethome failed: %s)r_  _auto_sethome_doneosgetenvrZ   rn  hermes_constantsru  utilsrv  yamlexistsopen	safe_loadrm  rS   environr5  r6  r  ro  r:  r?  )r  r  r  r_  	_cur_home_should_setru  rv  r}  _homeconfig_pathuser_configfr  s                 r]   r  zAutoSetHomeMiddleware.handle?  s2     +) 	T	"8"==I N((22Ls}7L  }$$-1* TT@@@@@@777777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%%k;???9<S[9I9IBJ56KKWck3=   
 ! T T TNN#A7<QRSSSSSSSSTgiis>    AD9 &C
>D9 
CD9 CA&D9 9
E.!E))E.Nr  r  r   r_   r]   rr  rr  5  s9          D           r_   rr  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d!dZdS )"ExtractContentMiddlewarez.Extract raw text and media refs from msg_body.zextract-content  customr=  rT   rS   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.titlero   linkz[share_card: z | ])card_content
wechat_desNz...(truncated)z	Preview: z[visit link for full content]rW   )r  r  _CARD_CONTENT_MAX_LENGTHr<  rS   rr   r   r   )	r  r  r  headerr   max_lenr\  valpreviews	            r]   _format_shared_linkz,ExtractContentMiddleware._format_shared_linki  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r_   rr  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.r6  Nr  z[link: z | visit link for full content])r  r   r  r<  r=  JSONDecodeError	TypeErrorrS   )r  r6  parsedr  s       r]   _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rB  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
        ro   
resourceId
resourceidr   )	urllibparseparse_qsurlparsequeryr  rS   rd   r:  )rB  r  idss      r]   _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,rj  ra  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    rw|                     dd          }|rG	 t          j
        |          }t          |t
                    s|                    d            |                     d!          }|d"k    r*|                    |                     dd#                     n|d$k    r)|                    |                     |                     n^|d%k    rC|                     |          }|r|                    |           n+|                    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)           E|r|                    d*| d           b|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
        rt  ro   r  r  rR   TIMImageElemimage_info_arrayNrp   r   rB  z[image|ybres:r  [image]TIMFileElem	file_namefileNamefilenamez[file:|ybres:z[file|ybres:[file: [file]TIMSoundElemz[voice|ybres:[voice]TIMVideoFileElemz[video|ybres:[video]TIMCustomElemr%  z[unsupported message type]	elem_type  z	[mention]    TIMFaceElemr  z[emoji: z[emoji][ )r  r   r<  ra  rr   r=  rS   rd   r  r   r  r  r  r  r  AttributeErrorr   )r   rj  partselemr  r6  rR   r  
image_info	image_urlridr  file_url	sound_url	video_urldata_valr  ctyperaw_data	face_name	face_datas                        r]   _extract_textz&ExtractContentMiddleware._extract_text  s     L	/ L	/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!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+   >Q!CQ!!+RR=T		T('T(rR   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  rp   Nrd   rZ   r   s    r]   _rewrite_slash_commandz/ExtractContentMiddleware._rewrite_slash_command  s;    
 zz||??8$$ 	"abb>Dr_   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"}]
        rt  ro   r  r  r  Nrp   r   rB  rL   )kindrB  r  r  r  r  rM   r  )r<  r=  r  ra  rr   rS   rd   r   )rj  refsr  rt  r6  r  r  r  r  r  refs              r]   _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$$$r_   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.rt  r  r  r%  ro   r  r  r  r  r6  N)	r<  r=  r  r   r  r  r  rS   r   )	rj  urlsr  data_strr  r  r  r6  r  s	            r]   _extract_link_urlsz+ExtractContentMiddleware._extract_link_urls2  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r  r^  r   c                
  K   |                      |                     |j                            |_        |                     |j                  |_        |                     |j                  |_         |             d {V  d S r   )r  r  rj  rp  r  rq  r  rz  r  s      r]   r  zExtractContentMiddleware.handleS  st      2243E3Ecl3S3STT99#,GG//==giir_   N)r  r=  rT   rS   )r  r=  rT   rr  rB  rS   rT   rS   )rj  ra  rT   rS   r   )rj  ra  rT   r  )rj  ra  rT   ra  r  )r   r   r   r   r  r  r   r  r  r  r   r  r  r  r  r  r   r_   r]   r  r  b  s       88D#      \ " ? ? ? \?    \$ [0 [0 [0 [[0z    \ * * * \*X    \@     r_   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  rM  SKIPPABLE_PLACEHOLDERSr   rR   rS   media_countrj   rT   rU   c                L    |dk    rdS |                                 }|| j        v S )zEDetect whether the message is a pure placeholder (should be skipped).r   F)rd   r  )r   rR   r  r   s       r]   is_skippable_placeholderz4PlaceholderFilterMiddleware.is_skippable_placeholderc  s-     ??5::<<3555r_   r  r^  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  rp  rr   rq  r5  r  r_  r  r  s      r]   r  z"PlaceholderFilterMiddleware.handlek  sj      ((s3>7J7JKK 	LL@#+BRTWT`aaaFgiir_   N)r   )rR   rS   r  rj   rT   rU   r  )r   r   r   r   r  rM  r  rZ  r   r  r  r   r_   r]   r  r  Y  s         HHD(1	 3 3 3 ) )    
 6 6 6 6 [6     r_   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/backgroundrM  	ALLOWLISTrR   rS   rT   c                r    |                                  } |                     d          rd| dd         z   } | S )z?Normalize full-width slash to ASCII slash and strip whitespace.r  r  rp   Nr  r   s    r]   r  z-OwnerCommandMiddleware._rewrite_slash_command  s;     zz||??8$$ 	"abb>Dr_   rd  r=  rj  ra  rn  rf  )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 )rt  r  r  r  s     r]   r   z@OwnerCommandMiddleware._detect_owner_command.<locals>.<listcomp>  s9     
 
 
uuZ  M11 111r_   rp   r   r  rR   ro   r  )maxsplitr  )
r  rr   r  r  rZ   rY   lowerrS   rd   rU   )r   rd  rj  rn  rf  
text_elemsrR   cmd_liner	  owner_idis_owners              r]   _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&&r_   r  r^  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)rd  rj  rn  rf  z;[%s] Reject non-owner slash command: chat=%s from=%s cmd=%su   ⚠️ z6 is only available to the creator in private chat modezyuanbao-owner-cmd-denial-r  z4[%s] Bot owner slash command: chat=%s from=%s cmd=%s)r_  r  rd  rj  rn  rf  r5  r6  r  rm  _track_taskr   rA  sendrs  rp  )r  r  r  r_  matched_cmdr  r   s          r]   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r_   Nr   )
rd  r=  rj  ra  rn  rS   rf  rS   rT   r  r  )r   r   r   r   r  rM  r  rZ  r   r  r   r  r  r   r_   r]   r  r  r  s           D %9 & & &  I        \ (' (' (' [('T     r_   r  c                      e Zd ZdZdZd	dZdS )
BuildSourceMiddlewarez(Build SessionSource from context fields.zbuild-sourcer  r^  rT   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  )rm  rn  ro  r  	user_namer  )r_  r  rm  rn  ro  rf  ri  rP  r  r  r  r_  s       r]   r  zBuildSourceMiddleware.handle  s      +))Kmm$,)=S-= # 8 8ffd * 
 

 giir_   Nr  r  r   r_   r]   r  r    s3        22D
 
 
 
 
 
r_   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            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-guardrj  ra  r&  rr  rT   rU   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.
        Frt  r  r  r%  ro   r  r  r  T)r  r   r  r  r  )rj  r&  r  r  r  s        r]   
_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?rS   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``).ro   rt  r  r  r%  r  r  r  rR   )r  r   r  r  r  rS   rd   )rj  r&  r  r  r  mention_texts         r]   _extract_bot_mention_textz0GroupAtGuardMiddleware._extract_bot_mention_text  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 (''''rr  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.)rS   r  r  )rj  r&  bidbot_mentions       r]   _build_group_channel_promptz2GroupAtGuardMiddleware._build_group_channel_prompt  s[     &%I&&,FFxQWXXe\e&(+& &LW& & &	
r_   Nrk  sender_displayrR   rk  r   c                  t          | dd          }|sdS 	 |                    |          }|j        pd}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.
        r3  Nr  r  rb   ]
r5  r   T)r4  r6  r   observedrG  z([%s] Failed to observe group message: %s)r7  r8  r  r   r   r   rJ  rK  rI  r9  r:  r5  r?  r  )r_  rP  r  rR   rk  r=  session_entryr  
attributedr  rI  s              r]   _observe_group_messagez-GroupAtGuardMiddleware._observe_group_message(  s(    !1488 	F	Z!77??Mn1	G@^@@g@@$@@J%%\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B 
C$!CCr  r^  c                f  K   |j         }|j        dk    r|j        s|                     |j        |j                  se|                     ||j        |j        p|j	        |j
        |j        pd            t                              d|j        |j        |j	                   d S  |             d {V  d S )Nr  r  z6[%s] Group message observed (no @bot): chat=%s from=%s)r_  rn  rs  r  rj  rS  r   rP  ri  rf  rp  rk  r5  r6  r  rm  r  s       r]   r  zGroupAtGuardMiddleware.handleJ  s      +=G##C,=#dooVYVbdkdsFtFt#''S%8%LC<Lclz)T (    KKHck3+;   Fgiir_   )rj  ra  r&  rr  rT   rU   )rj  ra  r&  rr  rT   rS   )r  rS   rR   rS   rk  rr  rT   r   r  )r   r   r   r   r  r   r  r  r  r   r  r   r_   r]   r  r    s         
 D   \.    \( 
 
 
 \
  $(Z Z Z Z Z \ZB     r_   r  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  r^  rT   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  rb   r  )r
  )rn  rs  r_  r  r  rj  rS  r{  rf  ri  rp  rP  dataclassesr   )r  r  r  r_  user_id_labelnickname_labels         r]   r  z!GroupAttributionMiddleware.handleh  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r_   Nr  r  r   r_   r]   r#  r#  Y  s9        
 
 D     r_   r#  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-typerR   rS   rj  ra  rT   r   c                2   |                      d          rt          j        S |D ]h}|                    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 it          j        S )z1Classify message type based on text and msg_body.r  rt  ro   r  r  r  r  )	rZ   r   COMMANDr  PHOTOVOICEVIDEODOCUMENTr(  )rR   rj  r  etypes       r]   	_classifyz'ClassifyMessageTypeMiddleware._classify}  s     ??3 	'&& 		, 		,DHHZ,,E&&"((((&&"((((***"((((%%"++++ &r_   r  r^  r   c                t   K   |                      |j        |j                  |_         |             d {V  d S r   )r1  rp  rj  rt  r  s      r]   r  z$ClassifyMessageTypeMiddleware.handle  s:      ~~clCLAAgiir_   N)rR   rS   rj  ra  rT   r   r  )r   r   r   r   r  r   r1  r  r   r_   r]   r)  r)  x  sQ        HHD      \       r_   r)  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-contextrl  rS   rT   #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idro   descri  rb  r#  )	r   r  r  r  r<  r=  r  rS   rd   )r  rl  r  r7  quote_idr9  sender
quote_texts           r]   _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  r^  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.
        Nr3  rG  ro   r6  r  rp   rC   :z'[%s] quote transcript lookup failed: %sr  yuanbao)ru  r_  r7  rP  r8  r:  r9  reversedr  r<  rS   _YB_RES_REF_RErv   r  	partitionrd   _RESOLVABLE_MEDIA_KINDSr   r:  r5  r?  )r  r  r_  rq  r=  r  historyr'  r|   _contentr   r   r  r  r  r  rI  s                    r]   #_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   )r=  rl  ru  rv  rH  rw  r  s      r]   r  zQuoteContextMiddleware.handle  sk      595P5PQTQf5g5g2!2%)%M%Mc%R%RRRRRRRgiir_   N)rl  rS   rT   r5  )r  r^  rT   r>  r  )r   r   r   r   r  r=  rH  r  r   r_   r]   r4  r4    s\        ==D$ $ $ $*( ( ( (T     r_   r4  c                  \   e Zd ZU dZdZi Zded<   dZded<   dZded	<   e	d/d            Z
e	d0d            Zed1d            Zed2d            Zed1d            Ze	ddddd3d            Ze	d4d#            Ze	d5d'            Ze	d6d(            Ze	d7d*            Zed8d-            Zd9d.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_idrS   rT   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)rL  r  r  rN  r  ry  pathisfile)r   rQ  r  
local_pathmimetss         r]   _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r_   rV  rW  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 )NrC   )rL  )r  r   s    r]   <lambda>z=MediaResolveMiddleware._put_cached_resource.<locals>.<lambda>	  s    CDWXYDZ[\D] r_   )key   )rr   rL  rP  sortedr  r  )r   rQ  rV  rW  sorted_keysr  s   `     r]   _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(((r_   rB  c                    t           j                            |           j        }t          j                            |          d                                         }|dv r|S dS )z$Guess image extension from URL path.rp   >   .heic.tiff.bmp.gif.jpg.png.jpeg.webprg  )r  r  r  rT  ry  splitextr  )rB  rT  exts      r]   _guess_image_ext_from_urlz0MediaResolveMiddleware._guess_image_ext_from_url		  sX     |$$S)).gt$$Q'--//VVVJvr_   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_idNrN  ro   rP  webr&  z-missing token or bot_id for resource downloadz/api/resource/v1/downloadr  )r  X-IDX-TokenX-Sourcer9   T)r  follow_redirectsrC   r  )paramsr!  i  r   rp  rq  rr  r$  >   Nr   z"resource/v1/download failed: code=r(  r'  r%  rB  realUrlz(resource/v1/download missing url/realUrlz)resource/v1/download did not return a URL)rd   r9  _get_cached_tokenrS   r  rS  _app_key_api_domainr*  r+  r-  r8  r   rW  _app_secretraise_for_statusr   r<  r=  )r_  rQ  
token_datarN  rP  r&  api_urlr!  rC  rD  resprE  r$  r%  real_urls                  r]   _fetch_resource_urlz*MediaResolveMiddleware._fetch_resource_url	  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   ro   N)r  r  r  r:  r  r  r  rS   rd   rK  r  )r_  rB  r  r  resource_idsrQ  s         r]   _resolve_download_urlz,MediaResolveMiddleware._resolve_download_urlM	  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&Nro   )r  log_tagrQ  	fetch_urlr  r  rr  r  c               L  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 |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.
        Nz'[%s] resource cache hit: rid=%s path=%sr   max_size_mbz5[%s] inbound media download failed: kind=%s %s err=%srL   )rl  z,[%s] inbound image cache rejected: %s err=%simage/
image/jpegrM   z)[%s] inbound file cache failed: %s err=%sapplication/octet-stream)rY  r5  r  r  media_download_urlMEDIA_MAX_SIZE_MBr:  r?  rm  r   r;  r   rZ   ra  r  r  r  ry  rT  basenamer   )r   r_  r  r  r  r  rQ  hit
file_bytescontent_typerI  rl  rV  rW  r  s                  r]   _download_and_cachez*MediaResolveMiddleware._download_and_cachef	  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##  	@\**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-F> >
G5"G00G5rq  r  Tuple[List[str], List[str]]c                L  K   g }g }|D ]}t          |                    d          pd                                                                          }t          |                    d          pd                                          }t          |                    d          pd                                          }|t          vs|st
                              |          }		 |                     ||           d{V }
n;# t          $ r.}t          
                    d|j        |||           Y d}~.d}~ww xY w|                     ||
||pdd|dd          |		           d{V }|h|\  }}|                    |           |                    |           ||fS )
zResolve 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.
        r  ro   rB  r  Nz8[%s] inbound media resolve failed: kind=%s url=%s err=%szplaceholder_url=P   r  r  r  r  rQ  )rS   r  rd   r  rE  r  r  r  r:  r5  r?  r  r  r   )r   r_  rq  rx  ry  r  r  rB  r  r  r  rI  rR  rV  rW  s                  r]   _resolve_media_urlsz*MediaResolveMiddleware._resolve_media_urls	  s      !#
!# 	% 	%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"%";";GS"I"IIIIIII		   NL$S    22#"*d53ss855 3        F ~%Jj)))t$$$$;&&s   'D
D<#D77D<r  r>  
log_prefixc          
       K   g }g }|D ]\  }}}|t           vr	 |                     ||           d{V }	n;# t          $ r.}
t                              d|j        ||||
           Y d}
~
ad}
~
ww xY w|                     ||	||pd| d| |           d{V }||\  }}|                    |           |                    |           ||fS )zQResolve a list of ``(rid, kind, filename)`` ybres tuples to local paths.
        Nz-[%s] %s resolve failed: rid=%s kind=%s err=%sz rid=r  )rE  r  r:  r5  r?  r  r  r   )r   r_  r  r  media_pathsmimesr  r  r  	fresh_urlrI  rR  rT  rW  s                 r]   _resolve_ybres_refsz*MediaResolveMiddleware._resolve_ybres_refs	  s]      "$#' 	 	Cx222"%"9"9'3"G"GGGGGGG		   CL*c4    22#"*d%11C11 3        F ~JD$t$$$LLE!!s   7
A/$A**A/c                4  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t          |          t          z
            }g }t                      }	||d         D ](}
|
                    d          }t          |t                    rd|vr2t                              |          D ]}|                    d          }|                    d          }|                    d	          \  }}}|                                }|t(          vrc||	v rh|	                    |           |                    |||                                f           t          |          t.          k    r nt          |          t.          k    r n*|sg g fS |                     ||d
           d{V S )zYResolve recent observed image/file anchors from transcript into ``(local_paths, mimes)``.r3  Nz.[%s] Observed-media hydration setup failed: %sr   r6  r  rp   rC   r@  zobserved-mediar  )r7  r8  r:  r9  r:  r5  r?  r  maxrr    OBSERVED_MEDIA_BACKFILL_LOOKBACKr   r  r<  rS   rC  rv   r  rD  rd   rE  r   r   ,OBSERVED_MEDIA_BACKFILL_MAX_RESOLVE_PER_TURNr  )r   r_  rP  r=  r  rF  rI  startorderseenr'  r6  r   r   r  r  r  r  s                     r]   _collect_observed_mediaz.MediaResolveMiddleware._collect_observed_media	  so     
 !1488 	r6M	!77??M++M,DEEGG 	 	 	NN@c   r6MMMMMM	  	r6MAs7||&FFGG,.EE566? 	 	Cggi((Ggs++ y/G/G#,,W55  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rw  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.
        r7  r  N)r  )r   r_  rw  s      r]   _resolve_quote_mediaz+MediaResolveMiddleware._resolve_quote_media-
  sK       ,,%' - 
 
 
 
 
 
 
 
 	
r_   r  r^  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  Nrp   ro   rC   rL   r  r  )ru  r7  r_  r  r<  rS   r   _YB_LOCAL_MEDIA_RErv   r  rd   r  ry  rT  r~  r   r   r  r   )r  pathsr  rid_keycacherR   r  r   r  rT  rW  s              r]   _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|r_   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  rT   r   c                    | \  }}t          ||          D ]K\  }}|r|v r                    |                               |                               |           Ld S r   )zipr   r   )r  u_listm_listur   r  typesr  s        r]   _add_unique_pairsz8MediaResolveMiddleware.handle.<locals>._add_unique_pairsr
  su    'NFFFF++    1 AIIAQ   r_   c              3     K   | ]}|d V  	dS )rp   Nr   )r   r  s     r]   r   z0MediaResolveMiddleware.handle.<locals>.<genexpr>}
  s'      55a15555555r_   r   r  z;[%s] observed-image hydration raised, continuing anyway: %sz.[%s] Skip placeholder after media download: %r)r  r  rT   r   )r_  r   r  rq  sumru  rw  r  r  rn  r  rP  r:  r5  r?  r  rx  ry  r  r  rp  r  )r  r  r  r_  r  	own_pairs	own_countrI  r  r  r  s           @@@r]   r  zMediaResolveMiddleware.handleg
  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)rQ  rS   rT   rR  )rQ  rS   rV  rS   rW  rS   rT   r   r  )rQ  rS   rT   rS   )r  rS   r  rS   r  rr  r  rS   rQ  rS   rT   rR  )rq  r  rT   r  )r  r>  r  rS   rT   r  )rT   r  )rw  r>  rT   r  )r  r^  rT   r  r  )r   r   r   r   r  rL  rZ  rN  rP  r   rY  ra  r   rm  r  r  r  r  r  r  r  r  r  r   r_   r]   rK  rK    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  $(=  =  =  =  =  [= ~ ,' ,' ,' [,'\ #" #" #" [#"J /
 /
 /
 [/
b 

 

 

 [

 * * * \*X; ; ; ; ; ;r_   rK  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-anchorsrR   rS   r  	List[str]r  rT   c                v   | r|s| S | }t          ||          D ]\  }}|                    d          st                              |          }|s n|                    d          }|                    d          \  }}	}
|                                }|dk    r|                    d          rd| d}nD|dk    r=|
                                pt          j        	                    |          }d	| d
| d}n|d |
                                         |z   ||                                d          z   } |S )Nr  rp   r@  rL   r  z[image: r  rM   r  u    → )r  rZ   rC  searchr  rD  rd   ry  rT  r  r  rw   )rR   r  r  patchedr  r   anchor_matchr   r  r  r  replacementlabels                r]   _patchzPatchAnchorsMiddleware._patch
  sh    	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.,,,.../,**,,--./ G
 r_   r  r^  r   c                   K   |                      |j        |j        |j                  |_         |             d {V  d S r   )r  rp  rx  ry  r  s      r]   r  zPatchAnchorsMiddleware.handle
  s>      {{3<QQgiir_   N)rR   rS   r  r  r  r  rT   rS   r  )r   r   r   r   r  r   r  r  r   r_   r]   r  r  
  sW        
 
 D   \6     r_   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  r^  rT   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)r  r  rT   r   c                 ~  K   t          j        t          d j        D                       rt          j        n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rZ   )r   mts     r]   r   zMDispatchMiddleware.handle.<locals>._dispatch_inbound_event.<locals>.<genexpr>
  s/      ^^2==)BCC^^^^^^r_   )
rR   r%  rP  rG  raw_messagerx  ry  ru  rv  r{  ro   r"  )r   rp  anyry  r   r/  rt  rP  rk  rd  ra  rx  ru  rv  r{  r   r*  r  rr   handle_message)eventr  r  _skr_  r  s      r]   _dispatch_inbound_eventz:DispatchMiddleware.handle.<locals>._dispatch_inbound_event
  sb      \ ^^co^^^^^&K((z:-H// 11$'$;!/"1  E   Hsz H36:+C058\5GR-c2z %cl %2$'Lcj!u::##!%[[):#e**s*:):; % %!!HH((///////////r_   r  z-[%s] Group message enqueued (qsize=%d) for %sro   rN   zyuanbao-group-consumer-r'  r  zyuanbao-inbound-r  r   )r_  r7   rP  configextrar  rn  _group_queues
setdefaultr   Queue
put_nowaitr5  r6  r  qsizerA  _consume_group_queue_inbound_tasksr   rC  rD  rk  )
r  r  r  r  is_newqueueconsumerrE  r  r_  s
    `      @@r]   r  zDispatchMiddleware.handle
  s     +J$+N$8$<$<=VX\$]$]%,^%9%=%=>XZ_%`%`
 
 
	0 	0 	0 	0 	0 	0 	0 	08 =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r_   r_  'YuanbaoAdapter'r#  rS   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.rJ   NTr  z3[%s] Group queue: dispatching for %s (remaining=%d)ro   rN   g?z[%s] Group queue consumer error)r  r  r   wait_forTimeoutErrorr5  r  r  r  r!  rA  r:  	exceptionr  )r_  r#  _IDLE_TIMEOUTr  dispatch_fns        r]   r  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  )r_  r  r#  rS   rT   r   )r   r   r   r   r  r  r   r  r   r_   r]   r  r  
  sT        88D< < < <| 9 9 9 \9 9 9r_   r  c                  `    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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_MIDDLEWARESrT   r  c                p    t                      }| j        D ]}|                     |                        |S )z6Build the default inbound message processing pipeline.)r  r  r  )r   pipelinemw_clss      r]   buildzInboundPipelineBuilder.buildQ  sA     #$$. 	# 	#FLL""""r_   N)rT   r  )r   r   r   r   r  r  r  r  rO  rU  rm  rr  r  r  r  r  r  r#  r)  r4  rK  r  r  r  rZ  r   r  r   r_   r]   r  r  3  s           	 #"%'(    ,    [  r_   r  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
    r_  r  rT   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  r_  s     r]   r  zConnectionManager.__init__d  se    *.7;268:7;-.%() #(02?Ar_   c                    | j         S r   )r  r  s    r]   wszConnectionManager.wsu  s	    xr_   rr  c                    | j         S r   )r  r  s    r]   
connect_idzConnectionManager.connect_idy  s    r_   rj   c                    | j         S r   )r  r  s    r]   reconnect_attemptsz$ConnectionManager.reconnect_attempts}  s    ''r_   rU   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 )NFr  T)r  r7  callablerU   r:  )r  	open_attrs     r]   is_connectedzConnectionManager.is_connected  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] %sNr  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_errorr5  r?  r  rw  ry  r  r  r7  r  r  r:  _acquire_platform_lockr6  rx  r   rT  
_route_envr  rS   rS  _ws_urlr   r  
websocketsconnectCONNECT_TIMEOUT_SECONDS_authenticate_cleanup_wsr  _mark_connectedget_running_loop_looprA  _heartbeat_loopr  r  _receive_loopr  YuanbaoAdapter
set_activer  _release_platform_lock)r  r_  r'  r  r{  authedrI  s          r]   r  zConnectionManager.open  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   CancelledErrorr  r9  r  valuesdoneset_exceptionr   r   r
  r  )r  disc_excfuts      r]   closezConnectionManager.close  s       	( '')))*********)   #'D ? 	#O""$$$o%%%%%%%%)   "DO   =>>%,,.. 	, 	,C88:: ,!!(+++  """ 	!!!           s!   2 AA/A= =BBr{  r=  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.
        NFrN  ro   r&  rP  botr  ybBot)	biz_iduidrP  rN  rk  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_typera   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  rS  r  rS   uuiduuid4r-   r1  r2  r4  r  r5  r  r  r   r!  r  AUTH_TIMEOUT_SECONDSr  r  recvr<  r  	bytearrayr)   r:  r!   _extract_connect_idr  r6  r  )r  r{  r_  rN  r6  rP  r  rk  
auth_bytesr"  deadliner   rawr'  r   r:  r	  r  rI  s                      r]   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%  r_   Nrp   r   rC   z*[%s] AuthBindRsp error: code=%d message=%rr   z$[%s] Failed to extract connectId: %s)r  r"   r%   r$   r#   r5  r  r  r  r:  r?  )r  rE  r%  fdictr$  messager  rI  s           r]   rA  z%ConnectionManager._extract_connect_idY  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   rp   z[%s] PONG timeout (%d/%d)z7[%s] Heartbeat threshold exceeded, triggering reconnectz[%s] Heartbeat send failed: %s)r  _runningr   rA  HEARTBEAT_INTERVAL_SECONDSr  rS   r<  r=  r.   r!  create_futurer  r  r  r5  r  r  r  r  r  r  r?  HEARTBEAT_TIMEOUT_THRESHOLDschedule_reconnectr:  r+  )r  r_  rk  
ping_byteslooppong_futurerI  s          r]   r#  z!ConnectionManager._heartbeat_loopp  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reasonro   z7[%s] Close code %d is non-recoverable, NOT reconnectingz[%s] receive_loop exited: %s)r  r  r<  r  r@  _handle_framer   r+  r  
exceptionsConnectionClosedr7  r5  r?  r  NO_RECONNECT_CLOSE_CODESr  _mark_disconnectedrN  r:  )r  r_  rD  	close_exc
close_coderI  s         r]   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rD  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:  ra   r	  ro   rk  need_ackFr%  r_   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:  r5  r  r  r  r!   r  r-  
set_resultr  r  r6  rr   r  r/   r  r*   r.  _push_to_inbound)r  rD  r_  r'  rI  r   r:  r	  rk  r\  r%  r0  r   	ack_bytesack_excdecodeds                   r]   rT  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  rS   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  rf  ro   r  rg  r  r  r@  
__unknown_)	r   r  r  r<  r=  r  r:  r*   r8  )r  r  r  rf  rg  rd  s         r]   _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)rj  r  r  r*  r  r   r5  r  r  r  rr   r   r!  
call_laterrg  _flush_inbound_buffer)r  r  r]  existing_timerrP  timers         r]   rb  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!!!r_   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)r_  rb  zyuanbao-pipeline-r  )r  r  r  r  r5  r6  r  rr   r^  r  r   rA  _inbound_pipeliner  )r  r]  r  r_  r  s        r]   rm  z'ConnectionManager._flush_inbound_bufferP  s      d+++(,,S"55	 	F-?L#s9~~	
 	
 	

 WCCCG/%--c22*S**
 
 
 	 	 	 	 	r_   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  r9  r   r!  rL  r  r  r  shieldr  r  r:  )r  rr  rs  r  rP  futurer   s          r]   send_biz_requestz"ConnectionManager.send_biz_requestf  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  rJ  r  r   rA  _reconnect_with_backoffr  s    r]   rN  z$ConnectionManager.schedule_reconnect  sV    =! 	@$*< 	@ < < > >?????	@ 	@ 	@ 	@r_   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)r  r5  r  r  r  _do_reconnectr  s    r]   rz  z)ConnectionManager._reconnect_with_backoff  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#                    dS # t          j&        $ r( t
                              d|j        |dz              Y tN          $ r0}t
                              d|j        |dz   |           Y d}~d}~ww xY wt
          (                    d|j        t                     |)                                 dS )z>Internal reconnect loop, called under the _reconnecting guard.rp   rC   r   z#[%s] Reconnect attempt %d/%d in %dsNr  r&  r  r  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  r-  MAX_RECONNECT_ATTEMPTSr  minr5  r6  r  r   rA  r  r   rW  rw  ry  rx  r  r  rS   rS  r  r  r  r  r  r  r  r?  r  r   r  r-  r*  rA  r#  r  r  r$  r  r:  r  rX  )r  r_  rD  waitr{  r(  rI  s          r]   r|  zConnectionManager._do_reconnect  s     -344 @	 @	G'.{D$qG|R((DKK5gk+A4   -%%%%%%%%%""$$$$$$$$$5#.#<#<$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/?   tt' a a aDglT[^_T_`````   :GL'TU+WZ       
 	8',H^	
 	
 	
 	""$$$us&   DK#D(K3L=	L=%L88L=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   r  r1  WS_CLOSE_TIMEOUT_Sr  r5  r  r  r  r:  )r  r  s     r]   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r_  r  rT   r   )rT   rr  rX  rT   rU   r   )r{  r=  rT   rU   )rE  r=  rT   rr  )rD  r  rT   r   )r  r  rT   rS   )r  r  rT   r   )r]  rS   rT   r   )rr  r  rs  rS   r  rf  rT   r=  )r   r   r   r   r  r  r  r  r  r	  r  r1  r  rA  r#  r$  rT  rg  rZ  rj  rb  rm  DEFAULT_SEND_TIMEOUTrx  rN  rz  r|  r  r   r_   r]   r  r  Y  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@@ @ @ @
	' 	' 	' 	'I I I IV     r_   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.
    r_  r  kwargsr
   rT   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  r_  r  s      r]   acquire_filezMediaSendHandler.acquire_file   r  r_   upload_resultr=  ra  c                    dS )z<Build platform-specific MsgBody list from COS upload result.Nr   r  r  r  s      r]   build_msg_bodyzMediaSendHandler.build_msg_body
  s      r_   rU   c                    dS )z:Override to return False for non-COS media (e.g. sticker).Tr   r  s    r]   needs_cos_uploadz!MediaSendHandler.needs_cos_upload  s    tr_   Nrm  rS   reply_torr  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.NFru  Tsuccessr  r  r  r  rN  ro   r&  )r   r  rN  r  r&  r  
bucketNameregion)r  r  r  credentialsbucketr  c                "    i | ]\  }}|d v	||S )>   r  	file_uuidr  r   )r   r  r  s      r]   
<dictcomp>z+MediaSendHandler.handle.<locals>.<dictcomp>R  s4       !Q III qIIIr_   )r  r  r  r  rR   r  rg  rg  z[%s] %s.handle() failed: %sr  ) _connection	_outboundr;  r  r   cancel_slow_notifierr  r  MessageSendervalidate_mediar  r    rv  r  rS  r   rw  rx  r  r   r  r  r   dispatch_msg_bodyr;  rS   r:  typer   r5  r  r  )r  r_  rm  r  r  r  connr;  r  r  r  validation_errr  r{  rN  r&  r  r  
fwd_kwargsrj  gcverI  handler_names                           r]   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)r_  r  r  r
   rT   r  )r  r=  r  r
   rT   ra  r  r  )r_  r  rm  rS   r  rr  r  rr  r  r
   rT   r  )	r   r   r   r   r	   r  r  r  r  r   r_   r]   r  r    s             ^ K K K ^K    #'!%a= a= a= a= a= a= a=r_   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)
r5  r6  r  r  r  rY   r   ry  rT  r  )r  r_  r  r  r  r  	path_partr  s           r]   r  zImageUrlHandler.acquire_filey  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1r_   c                    t          |d         |d         |d         |d         |                    dd          |                    dd          |d         	          S 
NrB  r  r  sizewidthr   heightr  )rB  r<  r  r  r  r  	mime_typer   r  r  s      r]   r  zImageUrlHandler.build_msg_body  g    #e$$J'v&##GQ// $$Xq11^,
 
 
 	
r_   Nr   r   r   r   r  r  r   r_   r]   r  r  v  s8        NN
2 
2 
2	
 	
 	
 	
 	
r_   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  )ry  rT  rU  r;  r5  r6  r  r  readr  r   )r  r_  r  r  r  r  r  r  s           r]   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      r]   r  zImageFileHandler.build_msg_body  r  r_   Nr  r   r_   r]   r  r    s8        VV	2 	2 	2	
 	
 	
 	
 	
r_   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   rM   r  )r5  r6  r  r  r  r  rY   ry  rT  r  r   )r  r_  r  r  r  r  r  r  s           r]   r  zFileUrlHandler.acquire_file  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1r_   c                X    t          |d         |d         |d         |d                   S NrB  r  r  r  )rB  r  r<  r  r   r  s      r]   r  zFileUrlHandler.build_msg_body  9    "e$J'$v&	
 
 
 	
r_   Nr  r   r_   r]   r  r    s8        LL2 2 2
 
 
 
 
r_   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  documentr  )ry  rT  rU  r;  r5  r6  r  r  r  r  r  r   )r  r_  r  r  r  r  r  r  s           r]   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 r  r  r  s      r]   r  zDocumentHandler.build_msg_body  r  r_   Nr  r   r_   r]   r  r    s8        LL	2 	2 	2
 
 
 
 
r_   r  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).rT   rU   c                    dS )NFr   r  s    r]   r  zStickerHandler.needs_cos_upload  s    ur_   c                
   K   dS )N)r_   stickerr  r   r  s      r]   r  zStickerHandler.acquire_file  s      99r_   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: )r  )!gateway.platforms.yuanbao_stickerr  r  r  r  r  r;  )
r  r  r  r  r  r  r  r  r  r  s
             r]   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2r_   Nr  )r   r   r   r   r  r  r  r   r_   r]   r  r    sL        KK   : : :3 3 3 3 3r_   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
    r_  r  rT   r   c                    || _         d S r   )r  r  s     r]   r  zGroupQueryService.__init__  s    r_   rg  rS   rc  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   rk  rs  statusz'[%s] query_group_info failed: status=%dr%  r_   rG  rg  z'[%s] query_group_info timeout: group=%sz [%s] query_group_info failed: %s)r  r  r  r4   gateway.platforms.yuanbao_protor)   rx  r  r5  r?  r  r<  r  r+   r   r  r:  )r  rg  r_  encoded_decodere  rs  rF  r   r  biz_datarI  s               r]   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"  offsetrj   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r  r  r   r  r   rk  r  r  z,[%s] get_group_member_list failed: status=%dr%  r_   rG  T)membersnext_offsetis_completer  z,[%s] get_group_member_list timeout: group=%sz%[%s] get_group_member_list failed: %s)r  r  r  r5   r  r)   rx  r  r5  r?  r  r<  r  r,   r  _member_cacher   r  r:  )r  rg  r  r  r_  r  r  re  rs  rF  r   r  r  r   rI  s                  r]   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>rm  r=  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)rZ   rr   r  )r  rm  rg  r   s       r]   query_group_infoz"GroupQueryService.query_group_infoT  sz       !!(++ 	NLMMS]]^^,
00<<<<<<<<>9::r_   list_allNactionr  rr  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 membersr  findc                   g | ]}|                     d d          pd                                v sX|                     dd          pd                                v s,|                     dd          pd                                v |S )nicknamero   	name_cardr  r  r  )r   r   r  s     r]   r   z;GroupQueryService.query_session_members.<locals>.<listcomp>}  s       QUU:r228b??AAAAQUU;339r@@BBBBQUU9b117R>>@@@@  A@@r_   	list_botsc                j    g | ]0}d |                     dd          pd                                v .|1S )r3  r  ro   r   r   r   s     r]   r   z;GroupQueryService.query_session_members.<locals>.<listcomp>  sB    \\\QUquuZ7L7L7RPR6Y6Y6[6[-[-[q-[-[-[r_   ro   
   c                    g | ]B}|                     d           p*|                     d          p|                     dd          CS )r  r  r  ro   r  r  s     r]   r   z;GroupQueryService.query_session_members.<locals>.<listcomp>  sJ    fffYZQUU;''T155+<+<TiQS@T@Tfffr_   zMention with @name: z, rN   )r  totalmentionHint)rZ   rr   r  r  r  r   )
r  rm  r  r  rg  r   r  mention_hintnamesr  s
            @r]   query_session_membersz'GroupQueryService.query_session_membersb  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|\\'
 
 	
r_   r  rg  rS   rT   rc  r   r"  rg  rS   r  rj   r  rj   rT   rc  )rm  rS   rT   r=  )r  N)rm  rS   r  rS   r  rr  rT   r=  )	r   r   r   r   r  r  r  r  r
  r   r_   r]   r  r    s                   @ >A# # # # #R   " !"	.
 .
 .
 .
 .
 .
 .
r_   r  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
    r_  r  rT   r   c                0    || _         i | _        i | _        d S r   )r  _reply_heartbeat_tasks_reply_hb_last_activer  s     r]   r  zHeartbeatManager.__init__  s    ?A#79"""r_   rm  rS   heartbeat_valrj   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  )rf  rg  	heartbeatr  )rf  r  r  RUNNINGFINISHz%[%s] Reply heartbeat %s sent: chat=%sz#[%s] send_heartbeat_once failed: %s)r  r  r  rS  rZ   rr   r3   removeprefixr2   r  r&   r5  r  r  r:  )
r  rm  r  r_  r  rg  r  r  status_namerI  s
             r]   send_heartbeat_oncez$HeartbeatManager.send_heartbeat_once  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  r  rS  r  r  r-  r  r  r   rA  _worker)r  rm  r_  r  existingrE  s         r]   r  zHeartbeatManager.start  s      -"7?'/?F.227;; 	HMMOO 	26)++D&w/F.2ikk"7+"LL!!.W..
 
 
 04#G,,,r_   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   rA  REPLY_HEARTBEAT_INTERVAL_Sr  r  r  REPLY_HEARTBEAT_TIMEOUT_Sr  r  r  r+  r:  r'   r  r  )r  rm  last_activer  	cancelleds        r]   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_finishrU   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)	r  r  r-  r*  r   r+  r  r'   r:  )r  rm  r#  rE  s       r]   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)ra  r  r,  r-  r*  r   r  r  rE  s     r]   r1  zHeartbeatManager.close  sz      4;;==>> 	 	D99;; #))+++"((*****r_   Nr  )rm  rS   r  rj   rT   r   rm  rS   rT   r   )Trm  rS   r#  rU   rT   r   r   )
r   r   r   r   r  r  r  r  r%  r1  r   r_   r]   r  r    s         : : : :
S S S S<4 4 4 4(!: !: !: !:F    + + + + + +r_   r  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.
    r_  r  r;  'MessageSender'rT   r   c                0    || _         || _        i | _        d S r   )r  _sender_tasks)r  r_  r;  s      r]   r  zSlowResponseNotifier.__init__  s    /1r_   rm  rS   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   rA  	_notifierr/  r  rm  rE  s      r]   r  zSlowResponseNotifier.start  s]      G"NN7##/g//
 
 
  $Gr_   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   rA  SLOW_RESPONSE_TIMEOUT_Sr5  r6  r  r  rj   r.  send_text_chunkSLOW_RESPONSE_MESSAGEr+  r:  r  )r  rm  rI  s      r]   r1  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*  r2  s      r]   r*  zSlowResponseNotifier.cancel1  sS    {w-- 			 	KKMMMMM	 	 	 	r_   c                   K   t          | j                                                  D ]*}|                                s|                                 +| j                                         dS )zCancel all slow-response tasks.N)ra  r/  r,  r-  r*  r   r'  s     r]   r1  zSlowResponseNotifier.close7  sc      ++--.. 	 	D99;; r_   N)r_  r  r;  r,  rT   r   r(  r   )	r   r   r   r   r  r  r1  r*  r1  r   r_   r]   r+  r+    s         2 2 2 2
$ $ $ $\ \ \ \        r_   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)
    >   re  rf  rg  rh  ri  rj  zClassVar[frozenset]
IMAGE_EXTSr  rM  CHAT_DICT_MAX_SIZEr_  r  rT   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  r  r  _media_handlersr  s     r]   r  zMessageSender.__init__M  sq    GRG^G`G` ?C?C )***,,&(('))%''=
 =
r_   r  rS   r  r  c                    || j         |<   dS )z1Register (or replace) a named media send handler.N)rC  )r  r  r  s      r]   register_handlerzMessageSender.register_handler`  s    %,T"""r_   rm  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_endrr   r;  ra  lockedr  r  iterr   r   )r  rm  evictedr]  s       r]   get_chat_lockzMessageSender.get_chat_lockf  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!((r_   Nro   r6  r  rr  rg  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.NFru  Tr  zJ[%s] truncate_message: input=%d chars, max=%d, output=%d chunk(s) sizes=%sc                ,    g | ]}t          |          S r   rr   r   s     r]   r   z+MessageSender.send_text.<locals>.<listcomp>  s    555c!ff555r_   r   r  )r  )r  r  r  r   rA  rK  strip_cron_wrappertruncate_messageMAX_TEXT_CHUNKr5  r6  r  rr   r   r5  r  rB  r:  )r  rm  r6  r  rg  r_  r  lockcontent_to_sendr   r   r   r_tor   s                 r]   	send_textzMessageSender.send_texty  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  )rC  r  r   r  r  )r  rm  r  r  r  r  r  s          r]   
send_mediazMessageSender.send_media  s       &**<88?@@@    $W^M7
w
 
28
 
 
 
 
 
 
 
 	
r_   rH  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 processingTrA  )r  platformrm  rG  )r  rd   r  r  r  r   suffixr  r:  send_image_filesend_documentrG  )	r  rm  rH  rY  r_  last_result
media_path	_is_voicerl  s	            r]   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	
 
 	
r_   rj  ra  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  rG  Fr  Unknown errorr  )rK  rZ   rr   send_group_msg_bodyr  send_c2c_msg_bodyr  r   )	r  rm  rj  r  rg  rR  grpr   r  s	            r]   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   rR   retryrj   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).rg  r  Nr  r  r  Tr  rf  r  z-[%s] send_text_chunk attempt %d/%d failed: %srp   z0[%s] send_text_chunk attempt %d/%d exception: %srC   z>[%s] send_text_chunk max retries (%d) exceeded. Last error: %sFzMax retries exceeded: r  )r  r-  rZ   rr   send_group_messager  send_c2c_messager  r   r5  r?  r  r:  rS   r   rA  r  )r  rm  rR   r  rk  rg  r_  
last_errorrD  rj  rD  r  rI  s                r]   r5  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  r=  c                R   K   dd|idg}|                      |||           d{V S )z<Send C2C text message, return {success: bool, msg_key: str}.r  rR   r  r  N)ri  )r  r  rR   rg  rj  s        r]   rn  zMessageSender.send_c2c_message$  sG      !.~NNO++JZ+XXXXXXXXXr_   c                l   K   |                      ||          }|                     |||           d{V S )zDSend group text message, auto-converting @nickname to TIMCustomElem.N)_build_msg_body_with_mentionsrh  )r  rg  rR   r  rj  s        r]   rm  z MessageSender.send_group_message)  sG       55dJGG--j(HMMMMMMMMMr_   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  rR   r  r  r  ro   r  r   rp   r  r%  r  @)r  rR   r  N)r  r  r  r  MEMBER_CACHE_TTL_Sr  _AT_USER_RErv   r  rd   r   r  r   dumpsrw   rr   )r  rR   rg  rR  rX  member_listr  nickname_to_uidr   nickr6  rj  last_idxr   r  segr  r  	real_nicktails                       r]   rr  z+MessageSender._build_msg_body_with_mentions6  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r_   c                   K   | j         }dt                       }t          |||j        pd||          }|                     |||           d{V S )z(Send C2C message with arbitrary MsgBody.c2c_ro   )r  rj  rf  rk  rg  N)r  r6   r0   rS  _dispatch_encoded)r  r  rj  rg  r_  rs  r  s          r]   ri  zMessageSender.send_c2c_msg_bodyj  st      -''')! .B!
 
 
 ++GWfEEEEEEEEEr_   c                   K   | j         }dt                       }t          |||j        pd||pd          }|                     |||           d{V S )z*Send group message with arbitrary MsgBody.grp_ro   )rg  rj  rf  rk  
ref_msg_idN)r  r6   r1   rS  r  )r  rg  rj  r  r_  rs  r  s          r]   rh  z!MessageSender.send_group_msg_bodyw  sz       -'''+! .B~2
 
 
 ++GWfEEEEEEEEEr_   r  r  rs  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.r  NTrk  ro   )r  r  FzRequest timeout after sr  )r  rx  r  r   r  r  r:  rS   )r_  r  rs  rF  rI  s        r]   r  zMessageSender._dispatch_encoded  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)rN  )r  r  r  	max_bytessize_mbs        r]   r  zMessageSender.validate_media  s     ZA!5!5,(,,,$&-	z??Y&&*oo,t3GThTT'TTTKTTTTtr_   r   
max_lengthrk   rl   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 rY  )_INDICATOR_REsubr   s     r]   r   z2MessageSender.truncate_message.<locals>.<listcomp>  s(    ;;;q-##B**;;;r_   )rr   rQ   r   )r6  r  rk   rx   r   s        r]   rP  zMessageSender.truncate_message  sq     }4==J&&9 #66Z 7 
 

 <;F;;;.vvgY.r_   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: )rZ   r  rs   rr   rd   )r6  dividerfooter_prefixdivider_pos
footer_posr  
body_startrG  s           r]   rO  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r_   c                <   K   | j                                          dS )zCRelease chat locks (no-op for now; placeholder for future cleanup).N)r@  r   r  s    r]   r1  zMessageSender.close  s!           r_   r  )r  rS   r  r  rT   r   rm  rS   rT   r   r  
rm  rS   r6  rS   r  rr  rg  rS   rT   r  r  )rm  rS   r  rS   r  rr  r  rr  r  r
   rT   r  r   rm  rS   rH  rS   rY  rZ  rT   r[  )
rm  rS   rj  ra  r  rr  rg  rS   rT   r  )Nr   ro   )rm  rS   rR   rS   r  rr  rk  rj   rg  rS   rT   r  rY  )r  rS   rR   rS   rg  rS   rT   r=  )rg  rS   rR   rS   r  rr  rT   r=  )rR   rS   rg  rS   rT   ra  )r  rS   rj  ra  rg  rS   rT   r=  )rg  rS   rj  ra  r  rr  rT   r=  )r_  r  r  r  rs  rS   rT   r=  r  r  r  r  rS   r  rj   rT   rr  r   )r6  rS   r  rj   rk   rl   rT   r  )r6  rS   rT   rS   r   )r   r   r   r   rM  r:  rZ  r;  r  rE  rK  rU  rX  rd  r  r5  rn  rm  rt   ru   	MULTILINErv  rr  ri  rh  r   r  r  rP  rO  r1  r   r_   r]   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/ / / / \/<    \,! ! ! ! ! !r_   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.
    rM  r;  r_  r  rT   r   c                    || _         t          |          | _        t          |          | _        t          || j                  | _        | j        | j        _        | j	        | j        _
        d S r   )r  r  r;  r  r  r+  slow_notifier_handle_send_startrA  _handle_send_finishrB  r  s     r]   r  zOutboundManager.__init__  s_    %27%;%;+;G+D+D3GQUQ\3]3] &*%<"&*&>###r_   rm  rS   c                :    | j                             |           dS )zFCalled by MessageSender before sending: cancel slow-response notifier.Nr  r*  r  rm  s     r]   r  z"OutboundManager._handle_send_start      !!'*****r_   c                V   K   | j                             |t                     d{V  dS )z=Called by MessageSender after sending: send FINISH heartbeat.N)r  r  r'   r  s     r]   r  z#OutboundManager._handle_send_finish  s7      n00:MNNNNNNNNNNNr_   Nro   r6  r  rr  rg  r  c                N   K   | j                             ||||           d{V S )z%Send text message with auto-chunking.r  N)r;  rU  )r  rm  r6  r  rg  s        r]   rU  zOutboundManager.send_text  s:      
 [**7GXR\*]]]]]]]]]r_   r  r  r
   c                :   K    | j         j        ||fi | d{V S )rW  N)r;  rX  )r  rm  r  r  s       r]   rX  zOutboundManager.send_media  s9       ,T[+G\LLVLLLLLLLLLr_   rH  rY  rZ  r[  c                J   K   | j                             |||           d{V S )z.Send text + media (used by send_message tool).N)r;  rd  )r  rm  rH  rY  s       r]   rd  zOutboundManager.send_direct  s4      
 [,,Wg{KKKKKKKKKr_   c                J   K   | j                             |           d{V  dS )z Start reply heartbeat (RUNNING).N)r  r  r  s     r]   start_typingzOutboundManager.start_typing!  s4      n""7+++++++++++r_   Fr#  rU   c                N   K   | j                             ||           d{V  dS )zStop reply heartbeat.r#  N)r  r%  )r  rm  r#  s      r]   stop_typingzOutboundManager.stop_typing%  s9      n!!'{!CCCCCCCCCCCr_   c                J   K   | j                             |           d{V  dS )zStart slow-response notifier.N)r  r  r  s     r]   start_slow_notifierz#OutboundManager.start_slow_notifier)  s5       &&w///////////r_   c                :    | j                             |           dS )zCancel slow-response notifier.Nr  r  s     r]   r  z$OutboundManager.cancel_slow_notifier-  r  r_   r   c                6    | j                             |          S )z@Proxy to MessageSender.get_chat_lock for backward compatibility.)r;  rK  r  s     r]   rK  zOutboundManager.get_chat_lock1  s    {((111r_   collections.OrderedDictc                    | j         j        S )z>Proxy to MessageSender._chat_locks for backward compatibility.)r;  r@  r  s    r]   r@  zOutboundManager._chat_locks5  s     {&&r_   r  r  r  r  r  rj   c                :    t                               | ||          S )z&Proxy to MessageSender.validate_media.)r  r  )r  r  r  s      r]   r  zOutboundManager.validate_media:  s    
 ++J+NNNr_   c                   K   | j                                          d{V  | j                                         d{V  | j                                         d{V  dS )zShut down all sub-managers.N)r;  r1  r  r  r  s    r]   r1  zOutboundManager.closeA  s      k!!!!!!!!!n""$$$$$$$$$ &&(((((((((((r_   r  r(  r  r  )rm  rS   r  rS   r  r
   rT   r  r   r  )Fr)  r  )rT   r  r  r  r   )r   r   r   r   r  r;  rZ  r  r  r  rU  rX  rd  r  r  r  r  rK  r  r@  r   r  r1  r   r_   r]   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) ) ) ) ) )r_   r  c                      e Zd ZU dZej        ZdZded<   dZ	ded<   dZ
ded	<   d
Zded<   edLd            ZedMd            ZdN fdZdOdZedPd            ZdPdZdQdZ	 	 	 dRdSd)ZdTd+ZdUdVd-ZdWd.ZdX fd0ZdYd1Z	 dZd[d6Zd7Zd\d]d:Z	 	 	 d^d_d=Z	 	 	 d^d`d?Z	 	 	 d^dadBZ 	 	 	 d^dbdFZ!	 	 	 	 dcdddHZ"dedJZ#dedKZ$ xZ%S )fr%  zCYuanbao AI Bot adapter backed by a persistent WebSocket connection.r   rj   rQ  rN   r  i  rM  REPLY_REF_MAX_ENTRIESNz$ClassVar[Optional['YuanbaoAdapter']]_active_instancerT   Optional['YuanbaoAdapter']c                    | j         S )z7Return the currently connected YuanbaoAdapter, or None.r  r	  s    r]   
get_activezYuanbaoAdapter.get_activeT  s     ##r_   r_  r   c                    || _         dS )z0Register (or clear) the active adapter instance.Nr  )r   r_  s     r]   r&  zYuanbaoAdapter.set_activeY  s      'r_   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!          |           | _        t%          |           | _        t)                      | _        t)                      | _        i | _        d	| _        t3          d
          | _        i | _        i | _        i | _        i | _        |                    d          pt?          j         dd                                          !                                }|                    d          pt?          j         dd          }d |"                    d          D             }|                    d          pt?          j         dd                                          !                                }|                    d          pt?          j         dd          }d |"                    d          D             }	tG          ||||	          | _$        tK          |           | _&        tN          (                                | _)        t?          j         d          p|j*        r|j*        j+        nd}
tY          |
          o|
-                    d           | _.        d S )Napp_idro   r   r&  ws_urlr  r  r  rK   i,  )ttl_secondsrY  YUANBAO_DM_POLICYr  rZ  YUANBAO_DM_ALLOW_FROMc                ^    g | ]*}|                                 |                                 +S r   r   r   xs     r]   r   z+YuanbaoAdapter.__init__.<locals>.<listcomp>  s2    #b#b#b!XYX_X_XaXa#bAGGII#b#b#br_   ,r[  YUANBAO_GROUP_POLICYr\  YUANBAO_GROUP_ALLOW_FROMc                ^    g | ]*}|                                 |                                 +S r   r   r  s     r]   r   z+YuanbaoAdapter.__init__.<locals>.<listcomp>  s2    &h&h&hQ^_^e^e^g^g&hqwwyy&h&h&hr_   )rY  rZ  r[  r\  rt  r  )/superr  r   YUANBAOr  r  rd   rw  ry  rS  DEFAULT_WS_GATEWAY_URLr  DEFAULT_API_DOMAINrc   rx  r  r  r  r  r  r   r  rB  r  ru  r   r   r  r   r*  r  ry  rz  r  rY   rX  ro  r  _group_queryr  r  rq  home_channelrm  rU   rZ   rx  )r  r  r  _extrarY  _dm_allow_from_rawrZ  r[  _group_allow_from_rawr\  _existing_homer  s              r]   r  zYuanbaoAdapter.__init__^  sR   !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 /@.E.E*9$*?*? 25 58EE
 =?). *c::: 8: 46 57" 35 JJ{## 6y,f55
%''%%'' 	 JJ'' 6y0"55 	 $c#b7I7O7OPS7T7T#b#b#b JJ~&& 9y/88
%''%%'' 	 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r_   rE  asyncio.Taskc                x    | j                             |           |                    | j         j                   |S )z@Register a fire-and-forget task so it won't be GC'd prematurely.)rB  r   rC  rD  r'  s     r]   r  zYuanbaoAdapter._track_task  s8    ""4(((t5=>>>r_   rU   c                    dS )zCYuanbao gates DM/group access at intake via dm_policy/group_policy.Tr   r  s    r]   enforces_own_access_policyz)YuanbaoAdapter.enforces_own_access_policy  s	     tr_   c                D   K   | j                                          d{V S )zhConnect to Yuanbao WS gateway and authenticate.

        Delegates to ConnectionManager.open().
        N)r  r  r  s    r]   r  zYuanbaoAdapter.connect  s/      
 %**,,,,,,,,,r_   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&  rJ  rX  r'  r  r1  r  ra  r  r-  r*  r   r  r5  r6  r  r'  s     r]   
disconnectzYuanbaoAdapter.disconnect  s#     *d22%%d+++!!!##%%% $$&&&&&&&&&n""$$$$$$$$$ ,-- 	 	D99;; !!###  """'33333r_   ro   rm  rS   r6  r  rr  metadataOptional[Dict[str, Any]]rg  r   c                N   K   | j                             ||||           d{V S )zCSend text message with auto-chunking. Delegates to OutboundManager.r  N)r  rU  )r  rm  r6  r  r  rg  s         r]   r  zYuanbaoAdapter.send  s:       ^--gwU_-`````````r_   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  r  r  s     r]   get_chat_infozYuanbaoAdapter.get_chat_info  s:       h'' 	6#W555...r_   rc  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  rm  r  s      r]   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     r]   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)rP  rm  r  r  r  _process_message_backgroundr  )r  r  r#  rm  r  s       r]   r  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)r  r  rh  s     r]   r  zYuanbaoAdapter.query_group_info$  s/      &;;JGGGGGGGGGr_   r   r"  r  r  c                L   K   | j                             |||           d{V S )z9Query group member list (delegates to GroupQueryService).r  N)r  r  )r  rg  r  r  s       r]   get_group_member_listz$YuanbaoAdapter.get_group_member_list(  s:       &@@TZbg@hhhhhhhhhr_   i'  r  rR   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  )ro  rf  r   rr   DM_MAX_CHARSr  )r  r  rR   rg  rm  s        r]   send_dmzYuanbaoAdapter.send_dm4  s       "0099 	Ue3STTTTt99t(((***+.@@D%G%%YYwYDDDDDDDDDr_   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  rX  )r  rm  r  r  r  r  r  s          r]   
send_imagezYuanbaoAdapter.send_imageK  s_       /T^.[
w)
 
 
 
 
 
 
 
 
 
 	
r_   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  rm  r  r  r  r  r  s          r]   r_  zYuanbaoAdapter.send_image_file[  s_       /T^.\
w:
 
 
 
 
 
 
 
 
 
 	
r_   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  rm  r  r  r  r  r  s          r]   	send_filezYuanbaoAdapter.send_filek  s_       /T^.Z
8
 
 
 
 
 
 
 
 
 
 	
r_   r  r  Optional[int]c                B   K    | j         j        |df|||d| d{V S )zDSend sticker/emoji. Delegates to OutboundManager via StickerHandler.r  )r  r  r  Nr  )r  rm  r  r  r  r  s         r]   send_stickerzYuanbaoAdapter.send_sticker{  s`       /T^.Y
%*
 
 	
 
 
 
 
 
 
 
 	
r_   r  c                D   K    | j         j        |df||||d| d{V S )zMSend local file (document). Delegates to OutboundManager via DocumentHandler.r  )r  r  r  r  Nr  )r  rm  r  r  r  r  r  r  s           r]   r`  zYuanbaoAdapter.send_document  sb       /T^.Z
w(
 
 	
 
 
 
 
 
 
 
 	
r_   r=  c                v   K   t                               | j        | j        | j        | j                   d{V S )z<Get the current valid sign token (using module-level cache).r  N)r   rT  rw  ry  rx  r  r  s    r]   rv  z YuanbaoAdapter._get_cached_token  sU       **M4+T-=o + 
 
 
 
 
 
 
 
 	
r_   c                R    | j         }|j        | j        |j        |j        | j        dS )z3Return a snapshot of the current connection status.)	connectedr&  r  r  r  )r  r	  rS  r  r  r  )r  r  s     r]   
get_statuszYuanbaoAdapter.get_status  s4    *l/"&"9l
 
 	
r_   rT   r  )r_  r  rT   r   )r  r   r  r
   rT   r   )rE  r  rT   r  r  r   )NNro   )rm  rS   r6  rS   r  rr  r  r  rg  rS   rT   r   )rm  rS   rT   r[  r   )rm  rS   r  rc  rT   r   r(  )r#  rS   rT   r   r  r  r  rY  )r  rS   rR   rS   rg  rS   rT   r   )NNN)rm  rS   r  rS   r  rr  r  rr  r  rc  r  r
   rT   r   )rm  rS   r  rS   r  rr  r  rr  r  rc  r  r
   rT   r   )rm  rS   r  rS   r  rr  r  rr  r  rc  r  r
   rT   r   )rm  rS   r  rr  r  r  r  rr  r  r
   rT   r   )NNNN)rm  rS   r  rS   r  rr  r  rr  r  rr  r  rc  r  r
   rT   r   )rT   r=  )&r   r   r   r   r   r  PLATFORMrQ  rZ  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`  rv  r  __classcell__)r  s   @r]   r%  r%  H  s        MMHN+..... >BAAAA$ $ $ [$ ' ' ' ['\i \i \i \i \i \iD       X- - - -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 "&"&#'
 
 
 
 
( "&"&#'
 
 
 
 
( #'"&#'
 
 
 
 
& '+$("&
 
 
 
 
( #'!%"&#'
 
 
 
 
$
 
 
 
	
 	
 	
 	
 	
 	
 	
 	
r_   r%  rT   r  c                 4    t                                           S )z,Delegate to ``YuanbaoAdapter.get_active()``.)r%  r  r   r_   r]   get_active_adapterr    s    $$&&&r_   r_  r  rm  rS   rH  rY  rZ  r[  c                J   K   | j                             |||           d{V S )z,Delegate to ``OutboundManager.send_direct``.N)r  rd  )r_  rm  rH  rY  s       r]   send_yuanbao_directr    s5       "..wMMMMMMMMMr_   r  r   )
r_  r  rm  rS   rH  rS   rY  rZ  rT   r[  )r   
__future__r   r   r>  r%  r   r   r   loggingry  rt   r/  r  urllib.parser  r<  r   r   r   pathlibr   abcr   r	   typingr
   r   r   r   r   r   r   sysr*  r  websockets.exceptionsr  ImportErrorgateway.configr   r   gateway.platforms.baser   r   r   r   r   r   gateway.platforms.helpersr   gateway.platforms.yuanbao_mediar   r  r   r   r   r   r   r    r  r!   r"   r#   r$   r%   r&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   gateway.sessionr7   	getLoggerr   r5  
hermes_clir8   _HERMES_VERSIONr1  r4  rS   r3  r]  r2  r  r  rK  r  r>  r~  r  r  rW  rM  AUTH_FAILED_CODESAUTH_RETRYABLE_CODESr  r   REPLY_REF_TTL_Sr4  r6  ru   rC  r  rM  rE  r  r  r  rQ   r   r[  r\  r|  r^  r~  r  r  r  r  r  rO  rU  rX  rm  rr  r  r  r  r  r  r#  r)  r4  rK  r  r  r  r  r  r  r  r  r  r  r  r  r+  r  r  r%  r  r  r   r_   r]   <module>r     sG   " # " " " " "              				 				        2 2 2 2 2 2 2 2 2 2       # # # # # # # # G G G G G G G G G G G G G G G G G G 



         JJJ 4 3 3 3 3 3 3 3                : 9 9 9 9 9                                                                0 . - - - - -		8	$	$
9999999   OOO s-.. L  L 6 !        @??     '&& )))  !       Y  I   RZ ABB  $)Wf$566  
-.. $&  /1 ,^
 ^
 ^
 ^
 ^
 ^
 ^
 ^
@p) p) p) p) p) p) p) p)f 5 4 4 4 4 4 4 4
:) :) :) :) :) :) :) :)z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    *   &    -   "*" *" *" *" *" *" *" *"Z    -   2* * * * *- * * *Zu u u u u0 u u un    "3   2^ ^ ^ ^ ^. ^ ^ ^B    -   $q q q q q. q q qh    !2   >    $5   6G G G G G. G G GTE E E E E. E E EP- - - - -. - - -`[9 [9 [9 [9 [9* [9 [9 [9|$ $ $ $ $ $ $ $LZ
 Z
 Z
 Z
 Z
 Z
 Z
 Z
x~= ~= ~= ~= ~=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]) ]) ]) ]) ]) ]) ]) ])@e
 e
 e
 e
 e
( e
 e
 e
Z' ' ' ' 59	N N N N N N Ns$   0
A; ;	BBD DD