
    `gjZS              
         U d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
ZddlmZmZ ddlZ ej                   e      ZdZdZdZdZd	d	d
ddddddZded<   i ddddddddddddddddd d!d"d#d$d%d&d'd(d)d*d+d,d-d.d/d0d1d2d3d4d5d6d7d8d9d:Zd;ed<<   dNd=ZdOdPd>ZdQd?ZdRd@ZdSdAZdTdBZdUdCZ dUdDZ!dUdEZ"dUdFZ#ef	 	 	 	 	 dVdGZ$	 	 dW	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dXdHZ%	 	 	 	 dY	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dZdIZ&	 	 	 	 	 	 	 	 	 	 	 	 	 	 d[dJZ'	 	 	 	 	 	 d\	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d]dKZ(	 	 d^	 	 	 	 	 	 	 	 	 d_dLZ)d`dMZ*y)au=  
yuanbao_media.py — 元宝平台媒体处理模块

提供 COS 上传、文件下载、TIM 媒体消息构建等功能。
移植自 TypeScript 版 media.ts（yuanbao-openclaw-plugin），
使用 httpx 替代 cos-nodejs-sdk-v5，避免引入额外 SDK 依赖。

COS 上传流程：
  1. 调用 genUploadInfo 获取临时凭证（tmpSecretId/tmpSecretKey/sessionToken）
  2. 用临时凭证通过 HMAC-SHA1 签名构建 Authorization 头
  3. HTTP PUT 上传到 COS

TIM 消息体构建：
  - buildImageMsgBody() → TIMImageElem
  - buildFileMsgBody()  → TIMFileElem
    )annotationsN)OptionalAnyz/api/resource/genUploadInfozyuanbao.tencent.com2   T               )
image/jpegz	image/jpg	image/gif	image/png	image/bmp
image/webp
image/heic
image/tiffzdict[str, int]_MIME_TO_IMAGE_FORMAT.jpgr   .jpeg.pngr   .gifr   .webpr   .bmpr   .heicr   .tiffr   .icozimage/x-iconz.pdfzapplication/pdfz.doczapplication/mswordz.docxzGapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentz.xlszapplication/vnd.ms-excelz.xlsxzAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetz.pptzapplication/vnd.ms-powerpointz.pptxzIapplication/vnd.openxmlformats-officedocument.presentationml.presentationz.txtz
text/plainzapplication/zipzapplication/x-tarzapplication/gzipz
audio/mpegz	video/mp4z	audio/wavz	audio/oggz
video/webm)z.zipz.tarz.gzz.mp3z.mp4z.wavz.oggz.webmdict[str, str]_EXT_TO_MIMEc                    t         j                  j                  |       d   j                         }t        j                  |d      S )u*   根据文件扩展名猜测 MIME 类型。application/octet-stream)ospathsplitextlowerr   get)filenameexts     R/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/yuanbao_media.pyguess_mime_typer*   Y   s8    
''

8
$R
(
.
.
0CC!;<<    c                    |j                  d      ryt        j                  j                  |       d   j	                         }|dv S )u   判断是否为图片类型。image/Tr    >	   r   r   r   r   r   r   r   r   r   )
startswithr"   r#   r$   r%   )r'   	mime_typer(   s      r)   is_imager0   _   sA    H%
''

8
$R
(
.
.
0C^^^r+   c                J    t         j                  | j                         d      S )u    获取 TIM 图片格式编号。r   )r   r&   r%   )r/   s    r)   get_image_formatr2   g   s     $$Y__%6<<r+   c                H    t        j                  |       j                         S )u    计算 MD5 十六进制摘要。)hashlibmd5	hexdigestdatas    r)   md5_hexr9   l   s    ;;t&&((r+   c                 ,    t        j                  d      S )u(   生成随机文件 ID（32 位 hex）。   )secrets	token_hex r+   r)   generate_file_idr?   q   s    R  r+   c                f    t        |       xs% t        |       xs t        |       xs t        |       S )u   
    解析图片宽高（支持 JPEG/PNG/GIF/WebP），无需第三方依赖。
    返回 {"width": w, "height": h} 或 None（无法识别）。
    )_parse_png_size_parse_jpeg_size_parse_gif_size_parse_webp_sizer7   s    r)   parse_image_sizerE   y   s:     	 	"D!	"4 	" D!	r+   c                    t        |       dk  ry | d d dk7  ry t        j                  d| dd       d   }t        j                  d| dd       d   }||dS )	N   r
   s   PNGz>Ir;      r   widthheightlenstructunpack)bufwhs      r)   rA   rA      sb    
3x"}
2Aw*dC2J'*AdC2J'*A!$$r+   c           	        t        |       dk  s| d   dk7  s| d   dk7  ry d}|t        |       dz
  k  r| |   dk7  r|dz  }| |dz      }|dv rIt        j                  d	| |d
z   |dz          d   }t        j                  d	| |dz   |dz          d   }||dS |dz   t        |       k  r)|dt        j                  d	| |dz   |dz          d   z   z  }n	 y |t        |       dz
  k  ry )Nr
   r   r   r      r   	   >         z>H      rI   r	   rL   )rP   imarkerrR   rQ   s        r)   rB   rB      s   
3x!|s1v~Q4	A
c#hl
q6T>FAQU\!dCAq1u$56q9AdCAq1u$56q9A!,,q53s8V]]4QUAE):;A>>>A c#hl
 r+   c                    t        |       dk  ry | d d j                  dd      }|dvry t        j                  d| dd       d	   }t        j                  d| dd       d	   }||d
S )N
      asciireplaceerrors>   GIF87aGIF89a<H   r   rI   rM   decoderN   rO   )rP   sigrQ   rR   s       r)   rC   rC      su    
3x"}
bq'...
3C
&&dC!H%a(AdC"I&q)A!$$r+   c                   t        |       dk  ry | d d dk7  s| dd dk7  ry | dd j                  dd	      }|d
k(  rjt        |       dk\  r[| d   dk(  rS| d   dk(  rK| d   dk(  rCt        j                  d| dd       d   dz  }t        j                  d| dd       d   dz  }||dS y |dk(  rKt        |       dk\  r<| d   dk(  r4t        j                  d| dd       d   }|dz  dz   }|dz	  dz  dz   }||dS y |dk(  rGt        |       dk\  r9| d   | d   dz  z  | d   dz  z  dz   }| d   | d   dz  z  | d    dz  z  dz   }||dS y )!Nr;   r
   s   RIFFrf      s   WEBPr_   r`   ra   zVP8          rG   r      *   re         r   i?  rI   VP8LrH   /   z<I      VP8X      rg   )rP   chunkrQ   rR   bitss        r)   rD   rD      s   
3x"}
2Aw'S2Y'12Jgi8Es8r>c"go#b'T/c"gQUodC2J/2V;AdC2J/2V;A!,,  
&s8r>c"go==s2bz215D!#A"*&!+A!,,  
&s8r>RCGqL)SW];q@ARCGqL)SW];q@A!,,r+   c                  K   ddl m  |       st        d|        dfd}|dz  dz  }t        j                  ddd|gi	      4 d
{   }	 |j                  |        d
{   }t        |j                  j                  dd      xs d      }|dkD  r||kD  rt        d|dz  dz  dd| d      |j                  d|       4 d
{   }|j                          |j                  j                  dd      j                  d      d   j                         }g }	d}
|j                  d      2 3 d
{   }|
t        |      z  }
|
|kD  rt        d| d      |	j!                  |       =7 $7 # t        j                  $ r Y w xY w7 7 Y6 d
d
d
      d
{  7   n# 1 d
{  7  sw Y   nxY wdj#                  	      }|fcd
d
d
      d
{  7   S # 1 d
{  7  sw Y   y
xY ww)uR  
    下载 URL 内容，返回 (bytes, content_type)。

    Args:
        url:          HTTP(S) URL
        max_size_mb:  最大允许大小（MB），超过则抛出异常

    Returns:
        (data_bytes, content_type_string)

    Raises:
        ValueError:  内容超过大小限制
        httpx.HTTPError: 网络/HTTP 错误
    r   )is_safe_urlz&Blocked unsafe URL (SSRF protection): responsec                   K   | j                   rC| j                  r6t        | j                  j                        } |      st	        d|       y y y w)Nz.Blocked redirect to private/internal address: )is_redirectnext_requeststrurl
ValueError)r~   redirect_urlr}   s     r)   _redirect_guardz%download_url.<locals>._redirect_guard   sZ     H$9$9x44889L|, D\NS  - %:s   AAi   g      >@T)timeoutfollow_redirectsevent_hooksNzcontent-lengthu   文件过大: z.1fz MB > z MBGETcontent-type ;i   u   文件过大: 已超过 u
    MB 限制r+   )r~   zhttpx.ResponsereturnNone)tools.url_safetyr}   r   httpxAsyncClientheadintheadersr&   HTTPStatusErrorstreamraise_for_statussplitstripaiter_bytesrM   appendjoin)r   max_size_mbr   	max_bytesclientr   content_lengthrespcontent_typechunks
downloadedrz   r8   r}   s                @r)   download_urlr      s    * -sA#GHH d"T)I  /!23 !" !" 
	S))D !1!12BA!F!K!LN!ny&@ $^d%:T%A#$Ff[MY\]  ==, 	% 	%!!#<<++NB?EEcJ1MSSUL"$FJ#//6 % %ec%j(
	)$2;-zJ  e$=!" * $$ 			%%6	% 	% 	% 	% 	% xx\!C!" !" !" !" !"s   A
G8E1G8G#E7'E4(AE78G#FG#A"F'4F8F
9F<5F'1G84E77F
G#FG#FF'G# F#!G#'F9	-F0.F9	5G#G8GG8#G5)G,*G51G8c                   t        t        j                               }|xs | d|xs ||z    }	t        j                  |j	                  d      |	j	                  d      t
        j                        j                         }
t        d |j                         D              }t        d |j                         D              }dj                  d |D              }dj                  d |D              }dj                  d |D              }dj                  d	 |D              }d
j                  | j                         |||dg      }t        j                  |j	                  d            j                         }d
j                  d|	|dg      }t        j                  |
j	                  d      |j	                  d      t
        j                        j                         }d| d|	 d|	 d| d| d| S )u  
    构建 COS 请求签名（q-sign-algorithm=sha1 方案）。
    参考：https://cloud.tencent.com/document/product/436/7778

    Args:
        method:         HTTP 方法（小写，如 "put"）
        path:           URL 路径（URL encode 后的小写）
        params:         URL 查询参数 dict（用于签名）
        headers:        参与签名的请求头 dict（key 需小写）
        secret_id:      临时 SecretId（tmpSecretId）
        secret_key:     临时 SecretKey（tmpSecretKey）
        start_time:     签名起始 Unix 时间戳（默认 now）
        expire_seconds: 签名有效期（秒，默认 3600）

    Returns:
        Authorization header 值（完整字符串）
    r   zutf-8c              3     K   | ]A  \  }}|j                         t        j                  j                  t	        |      d       f C ywr   safeNr%   urllibparsequoter   .0kvs      r)   	<genexpr>z_cos_sign.<locals>.<genexpr>:  s:     hQUQRTUAGGIv||'9'9#a&r'9'JLh   AA	c              3     K   | ]A  \  }}|j                         t        j                  j                  t	        |      d       f C ywr   r   r   s      r)   r   z_cos_sign.<locals>.<genexpr>;  s:     jRVRSUVQWWY(:(:3q6(:(KMjr   c              3  &   K   | ]	  \  }}|  y wNr>   r   r   _s      r)   r   z_cos_sign.<locals>.<genexpr>=  s     :DAqa:   &c              3  0   K   | ]  \  }}| d |   yw=Nr>   r   s      r)   r   z_cos_sign.<locals>.<genexpr>>  s     ?AQCq*?   c              3  &   K   | ]	  \  }}|  y wr   r>   r   s      r)   r   z_cos_sign.<locals>.<genexpr>?  s     8A18r   c              3  0   K   | ]  \  }}| d |   ywr   r>   r   s      r)   r   z_cos_sign.<locals>.<genexpr>@  s     @AQCq*@r   
r   sha1zq-sign-algorithm=sha1&q-ak=z&q-sign-time=z&q-key-time=z&q-header-list=z&q-url-param-list=z&q-signature=)r   timehmacnewencoder4   r   r6   sorteditemsr   r%   )methodr#   paramsr   	secret_id
secret_key
start_timeexpire_secondsnowq_sign_timesign_keysorted_paramssorted_headersurl_param_list
url_paramsheader_list
header_strhttp_stringsha1_of_httpstring_to_sign	signatures                        r)   	_cos_signr     s   6 diik
C&3'q**;~)M(NOK xx'"7# ik	  hY_YeYeYghhMjZaZgZgZijjNXX:M::N???J((888K@@@J))
 K << 2 27 ;<FFHLYY
	  N  g& ik	 
}
{m
+
^,
	{	$r+   c           	       K   |
t               }|j                  d       t         }d||xs | dd}|r||d<   ||ddd	}	t        j                  d
      4 d{   }
|
j                  ||	|       d{   }|j                          |j                         }ddd      d{    j                  d      }|dk7  r#|!t        d| d|j                  dd             |j                  d      xs |}ddg}|D cg c]  }|j                  |      r| }}|rt        d|       |S 7 7 7 # 1 d{  7  sw Y   xY wc c}w w)u  
    调用 genUploadInfo 接口获取 COS 临时密钥及上传配置。

    Args:
        app_key:        应用 Key（用于 X-ID 头）
        api_domain:     API 域名（如 https://bot.yuanbao.tencent.com）
        token:          当前有效的签票 token（X-Token 头）
        filename:       待上传的文件名（含扩展名）
        file_id:        客户端生成的唯一文件 ID（不传则自动生成）
        bot_id:         Bot 账号 ID（用于 X-ID 头）

    Returns:
        COS 上传配置 dict，包含以下字段：
            bucketName         (str)  — COS Bucket 名称
            region             (str)  — COS 地域
            location           (str)  — 上传 Key（对象路径）
            encryptTmpSecretId (str)  — 临时 SecretId
            encryptTmpSecretKey(str)  — 临时 SecretKey
            encryptToken       (str)  — SessionToken
            startTime          (int)  — 凭证起始时间戳（Unix）
            expiredTime        (int)  — 凭证过期时间戳（Unix）
            resourceUrl        (str)  — 上传后的公网访问 URL
            resourceID         (str)  — 资源 ID（可选）

    Raises:
        RuntimeError: 接口返回非 0 code 或字段缺失
    N/zapplication/jsonweb)Content-TypezX-TokenzX-IDzX-SourcezX-Route-EnvlocalDocr   )fileNamefileIddocFrom	docOpenIdg      .@r   )jsonr   coder   u   genUploadInfo 失败: code=z, msg=msgr8   
bucketNamelocationu2   genUploadInfo 返回字段不完整: 缺少字段 )
r?   rstripUPLOAD_INFO_PATHr   r   postr   r   r&   RuntimeError)app_key
api_domaintokenr'   file_idbot_id	route_env
upload_urlr   bodyr   r   resultr   r8   required_fieldsfmissings                     r)   get_cos_credentialsr   g  s    H "$%%c*+,<+=>J +!'	G !*	D   . - -&[[$[HH!%- -
 ::fDqyT%)$vfjj6K5LM
 	
 ::f'D#Z0O)=Q!q=G=@	J
 	
 K'-H- - - - >sm   AED1ED77D38$D7E'D5(AEEEE3D75E7E	=E >E	Ec           
       K   |j                  dd      }|j                  dd      }|j                  dd      }|j                  dd      }	|j                  dd      }
|j                  d      }|j                  d      }|r|r|	s/t        d	t        |       d
t        |       dt        |	             t        r| d}n| d| d}t        j
                  j                  |	d      }d| d|j                  d       }|r|dk(  rt        |      rt        |      }nd}t        |       }t        |       }|||d}t        t        j                               }|r|n|}|r
||kD  r||z
  nd}t        dd|j                  d       i |||||      }|||d}t        j!                  d|||	||       t#        j$                  d      4 d{   }|j'                  || |       d{   }|j)                          ddd      d{    |
xs |||d}|j+                  d      rt-        |       }|r|d   |d<   |d    |d <   t        j!                  d!|d"   |       |S 7 7 z7 \# 1 d{  7  sw Y   lxY ww)#u  
    通过 httpx PUT 请求将文件上传到 COS。
    使用临时凭证（tmpSecretId/tmpSecretKey/sessionToken）构建 HMAC-SHA1 签名。

    Args:
        file_bytes:   文件二进制内容
        filename:     文件名（用于辅助计算 MIME、UUID）
        content_type: MIME 类型（如 "image/jpeg"）
        credentials:  get_cos_credentials() 返回的 dict，包含：
                        encryptTmpSecretId  → tmpSecretId
                        encryptTmpSecretKey → tmpSecretKey
                        encryptToken        → sessionToken
                        location            → COS key（对象路径）
                        resourceUrl         → 上传后公网 URL
                        startTime           → 凭证起始时间（Unix）
                        expiredTime         → 凭证过期时间（Unix）
        bucket:       COS Bucket 名称（如 chatbot-1234567890）
        region:       COS 地域（如 ap-guangzhou）

    Returns:
        上传结果 dict，包含：
            url       (str)           — COS 公网访问 URL
            uuid      (str)           — 文件内容 MD5
            size      (int)           — 文件大小（字节）
            width     (int, optional) — 图片宽度（仅图片）
            height    (int, optional) — 图片高度（仅图片）

    Raises:
        httpx.HTTPStatusError: COS 返回非 2xx 状态
        RuntimeError:          credentials 字段缺失
    encryptTmpSecretIdr   encryptTmpSecretKeyencryptTokenr   resourceUrl	startTimeexpiredTimeu$   COS credentials 不完整: secretId=z, secretKey=z, location=z.cos.accelerate.myqcloud.comz.cos.z.myqcloud.comr   r   zhttps://r!   )hostr   x-cos-security-token  put)r   r#   r   r   r   r   r   r   )Authorizationr   r  z3COS PUT: bucket=%s region=%s key=%s size=%d mime=%sg      ^@r   N)contentr   )r   uuidsizer-   rJ   rK   u    COS 上传成功: url=%s size=%dr   )r&   r   boolCOS_USE_ACCELERATEr   r   r   lstripr0   r*   r9   rM   r   r   r   loggerinfor   r   r	  r   r.   rE   )
file_bytesr'   r   credentialsbucketregionr   r   session_tokencos_keyresource_urlr   expired_timecos_hostencoded_keycos_url	file_uuid	file_sizesign_headersr   
sign_startsign_expireauthorizationput_headersr   r   r   	size_infos                               r)   upload_to_cosr&    s    N !__%92>I!oo&;R@J$<M??:r2G#r:L + <J"-//-"@LJg24	?2C Dj)*+d7m_F
 	
 X9:XU6(-8 ,,$$W3$7K
!K$6$6s$;#<=G <+EEH*84L5L 
#IJI $ -L diik
C)sJ*6<#;M<#%SWK##C()*"	M '$ -K KK=L
   /    6ZZ   
 

 	    &wF x($Z0	'0F7O(2F8
KK*uy M3 
       s[   G
I<I!I<I'(I#)I'=I<I%	AI<#I'%I<'I9-I0.I95I<c           	     p    |xs |xs t        |       xs d}|rt        |      nd}d||d|||| dgddgS )uO  
    构建腾讯 IM TIMImageElem 消息体。
    参考：https://cloud.tencent.com/document/product/269/2720

    Args:
        url:       图片公网访问 URL（COS resourceUrl）
        uuid:      文件 UUID（MD5 或其他唯一标识）
        filename:  文件名（uuid 为空时作为备用）
        size:      文件大小（字节）
        width:     图片宽度（像素）
        height:    图片高度（像素）
        mime_type: MIME 类型（用于确定 image_format）

    Returns:
        TIMImageElem 消息体列表（适合直接放入 msg_body）
    imager   TIMImageElemr   )typer  rJ   rK   r   )r  image_formatimage_info_arraymsg_typemsg_content)_basename_from_urlr2   )	r   r  r'   r  rJ   rK   r/   _uuidr+  s	            r)   build_image_msg_bodyr2  ?  sg    2 BHB 23 7B7E2;#I.L ' , !" $!&"("%	
 r+   c                $    |xs |}d|||| ddgS )u  
    构建腾讯 IM TIMFileElem 消息体。
    参考：https://cloud.tencent.com/document/product/269/2720

    Args:
        url:      文件公网访问 URL（COS resourceUrl）
        filename: 文件名（含扩展名）
        uuid:     文件 UUID（MD5 或其他唯一标识，不传则使用 filename）
        size:     文件大小（字节）

    Returns:
        TIMFileElem 消息体列表（适合直接放入 msg_body）
    TIMFileElem)r  	file_namer  r   r-  r>   )r   r'   r  r  r1  s        r)   build_file_msg_bodyr6  o  s3    & HE &%!		

 
r+   c                    	 t         j                  j                  |       }t        j                  j                  |j                        S # t        $ r Y yw xY w)u   从 URL 提取文件名。r   )r   r   urlparser"   r#   basename	Exception)r   parseds     r)   r0  r0    sF    &&s+ww,, s   AA
 
	AA)r'   r   r   r   )r   )r'   r   r/   r   r   r  )r/   r   r   r   )r8   bytesr   r   )r   r   )r8   r<  r   Optional[dict[str, int]])rP   r<  r   r=  )r   r   r   r   r   ztuple[bytes, str])Nr  )r   r   r#   r   r   r   r   r   r   r   r   r   r   zOptional[int]r   r   r   r   )fileNr   r   )r   r   r   r   r   r   r'   r   r   Optional[str]r   r   r   r   r   dict)r  r<  r'   r   r   r   r  r@  r  r   r  r   r   r@  )NNr   r   r   r   )r   r   r  r?  r'   r?  r  r   rJ   r   rK   r   r/   r   r   
list[dict])Nr   )
r   r   r'   r   r  r?  r  r   r   rA  )r   r   r   r   )+__doc__
__future__r   r4   r   loggingr"   r<   rN   r   urllib.parser   typingr   r   r   	getLogger__name__r  r   DEFAULT_API_DOMAINDEFAULT_MAX_SIZE_MBr  r   __annotations__r   r*   r0   r2   r9   r?   rE   rA   rB   rC   rD   r   r   r   r&  r2  r6  r0  r>   r+   r)   <module>rL     s  " #    	       			8	$ 1 *     	) ~ 	 
L \  K  K	 
 \  K  \  \  N       V  &  P  +   X! " L# $ 3 n >=_=
)
!
%(%: +D"	D"D" D"` !%OO
O O 	O
 O O O O 	Op !KKK K 	K
 K K K 
K\EEE E 	E
 E E 
EX "-	-
- - 	-
 - - - -f 		  	
 Hr+   