From 1f1deb5f7ff9886951f2d996cc39716a1f9d10ac Mon Sep 17 00:00:00 2001 From: haosicheng Date: Mon, 27 Oct 2025 16:32:07 +0800 Subject: [PATCH] =?UTF-8?q?[patch]=20=E6=96=B0=E5=A2=9EDY=20=E5=9B=BE?= =?UTF-8?q?=E7=89=87=20=E8=A7=86=E9=A2=91=E4=B8=8A=E4=BC=A0=20=E5=8F=91?= =?UTF-8?q?=E9=80=81=E7=AD=89=E6=96=B9=E6=B3=95=E9=80=BB=E8=BE=91=E9=9B=86?= =?UTF-8?q?=E6=88=90=20=E4=BC=98=E5=8C=96=E6=8A=96=E9=9F=B3=E5=BF=83?= =?UTF-8?q?=E8=B7=B3=E7=BB=B4=E6=8A=A4=E4=B8=8E=E5=BC=B9=E6=80=A7=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=BF=83=E8=B7=B3=E5=8C=85=20DY=E9=9B=86=E6=88=90?= =?UTF-8?q?=E5=86=85=E7=BD=AEjs=E7=8E=AF=E5=A2=83=20PDD=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E7=B3=BB=E7=BB=9F=E6=9C=BA=E5=99=A8=E6=B6=88?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Utils/Dy/DyUtils.py | 886 ++++++++++++++++++++++++++++++++++--- Utils/Dy/message_arg.py | 230 +++++++++- Utils/Pdd/PddUtils.py | 6 +- WebSocket/BackendClient.py | 24 +- static/js/sign.js | 474 ++++++++++++++++++++ 5 files changed, 1556 insertions(+), 64 deletions(-) create mode 100644 static/js/sign.js diff --git a/Utils/Dy/DyUtils.py b/Utils/Dy/DyUtils.py index 44dc470..cf52fec 100644 --- a/Utils/Dy/DyUtils.py +++ b/Utils/Dy/DyUtils.py @@ -14,12 +14,26 @@ from dataclasses import dataclass, asdict from urllib.parse import urlencode import re import random +import base64 +import os +import zlib +import hashlib +import string +from concurrent.futures import ThreadPoolExecutor import config # 导入 message_arg 中的方法 -from Utils.Dy.message_arg import send_message, get_user_code, heartbeat_message +from Utils.Dy.message_arg import send_message, get_user_code, heartbeat_message, send_img, send_video from Utils.message_models import PlatformMessage +# 🔧 尝试导入PyMiniRacer(内置V8引擎,无需外部JavaScript环境) +try: + from py_mini_racer import MiniRacer + PYMINIRACER_AVAILABLE = True +except ImportError: + PYMINIRACER_AVAILABLE = False + MiniRacer = None + # ===== 抖音登录相关类集成开始 ===== @@ -663,6 +677,14 @@ class DouYinMessageHandler: "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "x-secsdk-csrf-token": "0001000000017e890e18651b2ef5f6d36d0485a64cae6b0bfc36d69e27fdc20fe7d423670eba1861a5bcb5baaf40,a25cfb6098b498c33ee5f0a5dcafe47b" } + + # 🔥 新增:线程池用于异步操作(下载、上传等) + self.pool = ThreadPoolExecutor(max_workers=3) + + # 🔥 新增:签名引擎相关 + self.sign_engine_initialized = False + self.sign_ctx = None + self.js_engine = None # 打印实例创建信息 print(f"[DY Handler] 创建实例 {self.instance_id} for store {store_id}") @@ -959,28 +981,52 @@ class DouYinMessageHandler: def on_error(self, ws, error): """WebSocket错误处理""" self._log(f"❌ WebSocket错误: {error}", "ERROR") - self.is_running = False + # 🔥 不立即设置 is_running = False,让重连机制处理 + # self.is_running = False def on_close(self, ws, close_status_code, close_msg): """WebSocket关闭处理""" self._log(f"🔌 连接关闭: {close_status_code}, {close_msg}", "WARNING") - self.is_running = False + # 🔥 添加详细的关闭原因分析 + if close_status_code: + if close_status_code == 1006: + self._log("⚠️ 异常关闭(1006),可能是网络问题或心跳超时", "WARNING") + elif close_status_code == 1000: + self._log("✅ 正常关闭(1000),服务器主动断开", "INFO") + elif close_status_code == 1001: + self._log("⚠️ 端点离开(1001)", "WARNING") + else: + self._log(f"⚠️ 关闭代码: {close_status_code}", "WARNING") + + # 🔥 不立即设置 is_running = False,让重连机制处理 + # self.is_running = False def heartbeat_wss(self): """心跳线程""" + heartbeat_count = 0 while self.is_running: try: if self.ws and hasattr(self.ws, 'sock') and self.ws.sock and self.ws.sock.connected: self.send_heartbeat() - time.sleep(3) + heartbeat_count += 1 + # 🔍 每10次心跳输出一次统计 + if heartbeat_count % 10 == 0: + self._log(f"📊 [心跳统计] 已发送 {heartbeat_count} 次心跳", "INFO") + + # 🔥 优化:增加心跳间隔到5秒(避免被抖音风控) + # 并添加随机抖动(4.5-5.5秒) + import random + sleep_time = 5 + random.uniform(-0.5, 0.5) + time.sleep(sleep_time) except Exception as e: self._log(f"❌ 心跳发送失败: {e}", "ERROR") + import traceback + self._log(f"心跳异常详情: {traceback.format_exc()}", "DEBUG") time.sleep(5) def send_heartbeat(self): """发送心跳包 - 使用 message_arg 中的方法""" try: - # 使用 message_arg 中的 heartbeat_message 方法 value, message_type = heartbeat_message( pigeon_sign=self.config["data"]["pigeon_sign"], token=self.config["data"]["token"], @@ -1880,6 +1926,719 @@ class DouYinMessageHandler: self._log(f"📤 已请求用户 {receiver_id} 的token", "INFO") except Exception as e: self._log(f"❌ 请求token失败: {e}", "ERROR") + + # ==================== 🔥 新增:图片/视频处理方法 ==================== + + def _initialize_sign_engine(self): + """初始化JavaScript签名引擎(用于图片/视频上传签名)""" + if self.sign_engine_initialized: + return True + + try: + self._log("🔧 [DY上传] 初始化JavaScript签名引擎...", "INFO") + + # 获取 sign.js 文件路径 + from windows_taskbar_fix import get_resource_path + sign_js_path = get_resource_path("static/js/sign.js") + + if not os.path.exists(sign_js_path): + self._log(f"❌ [DY上传] 签名文件不存在: {sign_js_path}", "ERROR") + return False + + # 读取 sign.js 文件 + with open(sign_js_path, 'r', encoding='utf-8') as f: + jscode = f.read() + + # 🔧 优先使用PyMiniRacer(内置V8,无需外部JavaScript环境) + if PYMINIRACER_AVAILABLE: + self._log("✅ [DY上传] 使用PyMiniRacer内置JavaScript引擎", "INFO") + self.js_engine = "PyMiniRacer" + self.sign_ctx = MiniRacer() + self.sign_ctx.eval(jscode) + self._log("✅ [DY上传] PyMiniRacer引擎初始化成功", "SUCCESS") + else: + # 回退到execjs(需要Node.js环境) + self._log("⚠️ [DY上传] PyMiniRacer不可用,回退到execjs", "WARNING") + try: + import execjs + self.js_engine = "execjs" + self.sign_ctx = execjs.compile(jscode) + self._log("✅ [DY上传] execjs引擎初始化成功", "SUCCESS") + except ImportError: + self._log("❌ [DY上传] execjs未安装,无法使用签名功能", "ERROR") + return False + + self.sign_engine_initialized = True + return True + + except Exception as e: + self._log(f"❌ [DY上传] 初始化签名引擎失败: {e}", "ERROR") + import traceback + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") + return False + + def _call_sign_function(self, function_name, *args): + """调用JavaScript签名函数(统一接口)""" + try: + if not self.sign_engine_initialized: + if not self._initialize_sign_engine(): + raise Exception("签名引擎未初始化") + + if self.js_engine == "PyMiniRacer": + # PyMiniRacer调用方式 + js_args = ", ".join([json.dumps(arg) if not isinstance(arg, (int, float, bool)) else str(arg) for arg in args]) + js_call = f"{function_name}({js_args})" + return self.sign_ctx.eval(js_call) + else: + # execjs调用方式 + return self.sign_ctx.call(function_name, *args) + + except Exception as e: + self._log(f"❌ [DY上传] 调用签名函数失败: {e}", "ERROR") + raise + + def _get_file_extension(self, url, default_ext): + """智能提取文件扩展名 + + Args: + url: 文件URL + default_ext: 默认扩展名(jpg/mp4) + + Returns: + str: 文件扩展名 + """ + try: + # 移除查询参数 + url_without_params = url.split('?')[0] + + # 检查是否有有效的文件扩展名 + if '.' in url_without_params: + parts = url_without_params.split('.') + ext = parts[-1].lower() + + # 验证扩展名是否合法(只包含字母数字) + if ext and len(ext) <= 5 and ext.isalnum(): + # 常见图片/视频扩展名 + valid_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'] + if ext in valid_exts: + return ext + + # 如果无法提取有效扩展名,使用默认值 + return default_ext + + except Exception: + return default_ext + + async def _download_media(self, media_url, media_type="image", max_retries=3): + """下载图片或视频文件(带重试机制 + 智能扩展名识别) + + Args: + media_url: 媒体文件URL + media_type: 媒体类型(image/video) + max_retries: 最大重试次数 + + Returns: + tuple: (local_file_path, file_size_kb) 或 (None, None) + """ + for attempt in range(max_retries): + try: + if attempt > 0: + self._log(f"🔄 [DY{media_type}] 第{attempt + 1}次重试下载...", "INFO") + else: + self._log(f"🔽 [DY{media_type}] 开始下载: {media_url[:100]}...", "INFO") + + # 使用线程池下载文件(避免阻塞) + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.get(media_url, timeout=30, headers={ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + ) + + if response.status_code != 200: + self._log(f"❌ [DY{media_type}] 下载失败,HTTP状态码: {response.status_code}", "ERROR") + if attempt < max_retries - 1: + await asyncio.sleep(1) + continue + return None, None + + # 获取文件数据 + file_data = response.content + file_size_kb = len(file_data) // 1024 + + # 检查文件大小(限制50MB) + if file_size_kb > 51200: + self._log(f"❌ [DY{media_type}] 文件过大: {file_size_kb}KB,超过50MB限制", "ERROR") + return None, None + + # 检查文件是否为空 + if file_size_kb == 0: + self._log(f"❌ [DY{media_type}] 下载的文件为空", "ERROR") + if attempt < max_retries - 1: + await asyncio.sleep(1) + continue + return None, None + + # 保存到临时文件 + temp_dir = os.path.join(os.path.dirname(__file__), "temp_uploads") + os.makedirs(temp_dir, exist_ok=True) + + # 🔥 修复:智能提取文件扩展名 + default_ext = 'mp4' if media_type == 'video' else 'jpg' + ext = self._get_file_extension(media_url, default_ext) + temp_filename = f"{media_type}_{uuid.uuid4().hex[:12]}.{ext}" + temp_filepath = os.path.join(temp_dir, temp_filename) + + # 写入临时文件 + with open(temp_filepath, 'wb') as f: + f.write(file_data) + + self._log(f"✅ [DY{media_type}] 下载成功,大小: {file_size_kb}KB,文件: {temp_filename}", "SUCCESS") + return temp_filepath, file_size_kb + + except requests.exceptions.RequestException as e: + self._log(f"❌ [DY{media_type}] 网络请求失败: {e}", "ERROR") + if attempt < max_retries - 1: + await asyncio.sleep(2) + continue + return None, None + except Exception as e: + self._log(f"❌ [DY{media_type}] 下载失败: {e}", "ERROR") + if attempt < max_retries - 1: + await asyncio.sleep(1) + continue + return None, None + + self._log(f"❌ [DY{media_type}] 下载失败,已重试{max_retries}次", "ERROR") + return None, None + + def _cleanup_temp_file(self, file_path): + """清理临时文件""" + try: + if file_path and os.path.exists(file_path): + os.remove(file_path) + self._log(f"🗑️ [DY上传] 临时文件已删除: {os.path.basename(file_path)}", "DEBUG") + except Exception as e: + self._log(f"⚠️ [DY上传] 删除临时文件失败: {e}", "WARNING") + + async def _get_upload_token(self, upload_type="image"): + """获取上传Token(图片或视频)""" + try: + self._log(f"📝 [DY上传] 请求{upload_type}上传Token...", "DEBUG") + + headers = { + "authority": "pigeon.jinritemai.com", + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "origin": "https://im.jinritemai.com", + "pragma": "no-cache", + "referer": "https://im.jinritemai.com/", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "x-secsdk-csrf-token": "000100000001d87d9750fd37e74337d4d45e4b1d099ae24c4a56636764617b7be09bbe989ab618710dd6176c296d,796bb03b67483620a4e078de481a1100" + } + + if upload_type == "video": + url = "https://pigeon.jinritemai.com/backstage/video/getUploadToken" + else: + url = "https://pigeon.jinritemai.com/backstage/getSTS2Token" + + params = { + "biz_type": "4", + "PIGEON_BIZ_TYPE": "2", + "_ts": int(time.time() * 1000), + "_pms": "1", + "FUSION": "true", + "verifyFp": "verify_mh2rv3to_ZqgJkknT_7C5g_4Ov9_9Rz5_zIPjTzd4Ee6L", + "_v": "1.0.1.4380" + } + + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.get(url, headers=headers, cookies=self.cookie, params=params) + ) + + if response.status_code != 200: + self._log(f"❌ [DY上传] 获取上传Token失败: HTTP {response.status_code}", "ERROR") + return None + + result = response.json() + data = result.get("data") + + if upload_type == "video": + token_data = data.get("token", {}) + return { + "access_key_id": token_data.get("access_key_id"), + "secret_access_key": token_data.get("secret_access_key"), + "session_token": token_data.get("session_token") + } + else: + return { + "access_key_id": data.get("AccessKeyID"), + "secret_access_key": data.get("SecretAccessKey"), + "service_id": data.get("ServiceId"), + "session_token": data.get("SessionToken") + } + + except Exception as e: + self._log(f"❌ [DY上传] 获取上传Token失败: {e}", "ERROR") + return None + + async def _upload_image_to_douyin(self, image_path, token_info): + """上传图片到抖音服务器(完整流程)""" + try: + self._log("📤 [DY图片] 开始上传图片到抖音服务器...", "INFO") + + current_time = datetime.utcnow() + formatted_time = current_time.strftime("%Y%m%dT%H%M%SZ") + random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + + # 步骤1: 申请上传 + upload_node = await self._apply_image_upload( + formatted_time, random_string, token_info, action="ApplyImageUpload" + ) + if not upload_node: + return None + + session_key = upload_node.get("SessionKey") + store_uri = upload_node.get("StoreInfos", [{}])[0].get("StoreUri") + authorization = upload_node.get("StoreInfos", [{}])[0].get("Auth") + + # 步骤2-4: 分片上传 + upload_id = await self._upload_file_part(store_uri, authorization, image_path, step=1) + if not upload_id: + return None + + success = await self._upload_file_part(store_uri, authorization, image_path, step=2, upload_id=upload_id) + if not success: + return None + + with open(image_path, 'rb') as f: + content = f.read() + crc32_hex = format(zlib.crc32(content), '08x') + + success = await self._upload_file_part(store_uri, authorization, image_path, step=3, upload_id=upload_id, crc=crc32_hex) + if not success: + return None + + # 步骤5: 提交上传获取图片URI + result = await self._apply_image_upload( + formatted_time, random_string, token_info, + action="CommitImageUpload", session_key=session_key + ) + if result: + image_uri = result.get("ImageUri") + self._log(f"✅ [DY图片] 上传成功,URI: {image_uri}", "SUCCESS") + return image_uri + return None + + except Exception as e: + self._log(f"❌ [DY图片] 上传失败: {e}", "ERROR") + return None + + async def _apply_image_upload(self, formatted_time, random_string, token_info, action, session_key=None): + """申请或提交图片上传""" + try: + url = "https://imagex.bytedanceapi.com/" + + if action == "ApplyImageUpload": + params = { + "Action": "ApplyImageUpload", + "Version": "2018-08-01", + "ServiceId": token_info["service_id"], + "s": random_string + } + sign = self._call_sign_function( + "signature", "GET", "imagex", + "AWS4" + token_info["secret_access_key"], + formatted_time, token_info["session_token"], params + ) + else: + params = { + "Action": "CommitImageUpload", + "Version": "2018-08-01", + "SessionKey": session_key, + "ServiceId": token_info["service_id"] + } + sign = self._call_sign_function( + "signature", "POST", "imagex", + "AWS4" + token_info["secret_access_key"], + formatted_time, token_info["session_token"], params + ) + + authorization = f"AWS4-HMAC-SHA256 Credential={token_info['access_key_id']}/{formatted_time.split('T')[0]}/cn-north-1/imagex/aws4_request, SignedHeaders=x-amz-date;x-amz-security-token, Signature={sign}" + + headers = { + "authority": "imagex.bytedanceapi.com", + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "authorization": authorization, + "cache-control": "no-cache", + "origin": "https://im.jinritemai.com", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "x-amz-date": formatted_time, + "x-amz-security-token": token_info["session_token"] + } + + if action == "ApplyImageUpload": + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.get(url, headers=headers, params=params) + ) + result = response.json() + return result.get("Result", {}).get("InnerUploadAddress", {}).get("UploadNodes", [{}])[0] + else: + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=headers, params=params) + ) + result = response.json() + return result.get("Result", {}).get("PluginResult", [{}])[0] + + except Exception as e: + self._log(f"❌ [DY图片] {action}失败: {e}", "ERROR") + return None + + async def _upload_file_part(self, uri, authorization, file_path, step, crc=None, upload_id=None): + """上传图片文件分片(带超时和重试)""" + try: + headers = { + "Authorization": authorization, + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Host": "tos-d-x-lf.douyin.com", + "Origin": "https://im.jinritemai.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "X-Storage-U": self.cookie["PIGEON_CID"], + } + url = f"https://tos-d-x-lf.douyin.com/{uri}" + + if step == 1: + headers["Content-Type"] = "multipart/form-data" + params = {"uploads": ""} + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=headers, params=params, timeout=30) + ) + return response.json().get("payload", {}).get("uploadID") + + elif step == 2: + with open(file_path, 'rb') as f: + content = f.read() + headers["Content-Crc32"] = format(zlib.crc32(content), '08x') + headers["Content-Length"] = str(len(content)) + headers["Content-Type"] = 'application/octet-stream' + params = {"partNumber": "1", "uploadID": upload_id} + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.put(url, headers=headers, params=params, data=content, timeout=120) + ) + return response.json().get("error", {}).get("code") == 200 + + elif step == 3: + headers["Content-Type"] = 'text/plain;charset=UTF-8' + params = {"uploadID": upload_id} + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=headers, params=params, data=f"1:{crc}", timeout=30) + ) + return response.json().get("error", {}).get("code") == 200 + + except Exception as e: + self._log(f"❌ [DY图片] 分片上传失败(步骤{step}): {e}", "ERROR") + return None if step == 1 else False + + async def _send_image_message(self, receiver_id, talk_id, p_id, user_token, image_uri): + """发送图片消息给用户""" + try: + self._log(f"📤 [DY图片] 发送图片消息给用户 {receiver_id}", "INFO") + + value, message_type = send_img( + pigeon_sign=self.config["data"]["pigeon_sign"], + token=self.config["data"]["token"], + receiver_id=receiver_id, + shop_id=self.cookie["SHOP_ID"], + talk_id=talk_id, + session_did=self.cookie["PIGEON_CID"], + p_id=p_id, + user_code=user_token, + img=image_uri + ) + + form_data = blackboxprotobuf.encode_message(value=value, message_type=message_type) + self.ws.send_bytes(form_data) + + self._log(f"✅ [DY图片] 图片消息已发送", "SUCCESS") + return True + + except Exception as e: + self._log(f"❌ [DY图片] 发送失败: {e}", "ERROR") + return False + + async def _upload_video_to_douyin(self, video_path, token_info): + """上传视频到抖音服务器(完整流程)""" + try: + self._log("📤 [DY视频] 开始上传视频...", "INFO") + + # 🔥 获取实际文件大小 + file_size = os.path.getsize(video_path) + self._log(f"🔍 [DY视频] 文件大小: {file_size} 字节", "DEBUG") + + current_time = datetime.utcnow() + formatted_time = current_time.strftime("%Y%m%dT%H%M%SZ") + random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + + upload_node = await self._apply_video_upload(formatted_time, random_string, token_info, file_size, action="ApplyUploadInner") + if not upload_node: + return None + + session_key = upload_node.get("SessionKey") + store_uri = upload_node.get("StoreInfos", [{}])[0].get("StoreUri") + authorization = upload_node.get("StoreInfos", [{}])[0].get("Auth") + + upload_id = await self._upload_video_part(store_uri, authorization, video_path, step=1) + if not upload_id: + return None + + success = await self._upload_video_part(store_uri, authorization, video_path, step=2, upload_id=upload_id) + if not success: + return None + + with open(video_path, 'rb') as f: + content = f.read() + crc32_hex = format(zlib.crc32(content), '08x') + + success = await self._upload_video_part(store_uri, authorization, video_path, step=3, upload_id=upload_id, crc=crc32_hex) + if not success: + return None + + result = await self._apply_video_upload(formatted_time, random_string, token_info, file_size, action="CommitUploadInner", session_key=session_key) + if result: + # 🔥 添加调试日志,查看返回结构 + self._log(f"🔍 [DY视频] CommitUpload返回结构: {json.dumps(result, ensure_ascii=False)[:500]}", "DEBUG") + + poster_uri = result.get("PosterUri") + cover_url = await self._get_url_for_uri(poster_uri) if poster_uri else "" + + # 🔥 提取视频元数据 + video_meta = result.get("VideoMeta", {}) + if not video_meta: + self._log(f"⚠️ [DY视频] VideoMeta为空,使用默认值", "WARNING") + + vid = result.get("Vid") + width = video_meta.get("Width") if video_meta else 1920 + height = video_meta.get("Height") if video_meta else 1080 + duration = video_meta.get("Duration") if video_meta else 0 + + self._log(f"🔍 [DY视频] 提取的元数据: vid={vid}, width={width}, height={height}, duration={duration}", "DEBUG") + + return { + "vid": vid, + "width": str(width), + "height": str(height), + "duration": str(duration), + "cover_url": cover_url + } + return None + + except Exception as e: + self._log(f"❌ [DY视频] 上传失败: {e}", "ERROR") + return None + + async def _apply_video_upload(self, formatted_time, random_string, token_info, file_size, action, session_key=None): + """申请或提交视频上传""" + try: + url = "https://open.bytedanceapi.com/" + + if action == "ApplyUploadInner": + # 🔥 修复:使用实际文件大小,不是固定值 + params = {"Action": "ApplyUploadInner", "Version": "2020-11-19", "SpaceName": "pigeon-video", "FileType": "video", "IsInner": "1", "FileSize": str(file_size), "s": random_string} + sign = self._call_sign_function("signature", "GET", "vod", "AWS4" + token_info["secret_access_key"], formatted_time, token_info["session_token"], params) + else: + params = {"Action": "CommitUploadInner", "Version": "2020-11-19", "SpaceName": "pigeon-video"} + payload = {"SessionKey": session_key, "Functions": [{"name": "GetMeta"}, {"name": "Snapshot", "input": {"SnapshotTime": 0}}]} + body_sha256 = hashlib.sha256(json.dumps(payload).replace(" ", "").encode()).hexdigest() + sign = self._call_sign_function("signature", "POST", "vod", "AWS4" + token_info["secret_access_key"], formatted_time, token_info["session_token"], params, payload) + + authorization = f"AWS4-HMAC-SHA256 Credential={token_info['access_key_id']}/{formatted_time.split('T')[0]}/cn-north-1/vod/aws4_request, SignedHeaders=" + authorization += "x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=" + sign if action == "CommitUploadInner" else "x-amz-date;x-amz-security-token, Signature=" + sign + + headers = { + "authorization": authorization, + "x-amz-date": formatted_time, + "x-amz-security-token": token_info["session_token"], + "User-Agent": "Mozilla/5.0" + } + + if action == "ApplyUploadInner": + response = await asyncio.get_event_loop().run_in_executor(self.pool, lambda: requests.get(url, headers=headers, params=params)) + result = response.json() + self._log(f"🔍 [DY视频] ApplyUploadInner响应: {json.dumps(result, ensure_ascii=False)[:300]}", "DEBUG") + return result.get("Result", {}).get("InnerUploadAddress", {}).get("UploadNodes", [{}])[0] + else: + headers["X-Amz-Content-Sha256"] = body_sha256 + response = await asyncio.get_event_loop().run_in_executor(self.pool, lambda: requests.post(url, headers=headers, params=params, data=json.dumps(payload).replace(" ", ""))) + full_result = response.json() + self._log(f"🔍 [DY视频] CommitUploadInner完整响应: {json.dumps(full_result, ensure_ascii=False)[:800]}", "DEBUG") + # 🔥 修复:直接返回 Result 对象,而不是 Results 数组的第一个元素 + # 因为视频元数据在 Result 中,不在 Results[0] 中 + result_data = full_result.get("Result", {}) + + # 检查是否有 Results 数组(某些情况下返回格式不同) + if "Results" in result_data and result_data["Results"]: + self._log(f"🔍 [DY视频] 使用Results数组: {result_data['Results'][0]}", "DEBUG") + return result_data["Results"][0] + else: + # 直接返回 Result 对象(包含 Vid, VideoMeta, PosterUri 等) + self._log(f"🔍 [DY视频] 使用Result对象", "DEBUG") + return result_data + + except Exception as e: + self._log(f"❌ [DY视频] {action}失败: {e}", "ERROR") + return None + + async def _upload_video_part(self, uri, authorization, file_path, step, crc=None, upload_id=None): + """上传视频文件分片""" + try: + headers = { + "Authorization": authorization, + "Host": "tos-hl-x.snssdk.com", + "Origin": "https://im.jinritemai.com", + "X-Storage-U": self.cookie["PIGEON_CID"] + } + url = f"https://tos-hl-x.snssdk.com/{uri}" + + if step == 1: + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=headers, params={"uploads": ""}, timeout=30) + ) + return response.json().get("payload", {}).get("uploadID") + + elif step == 2: + with open(file_path, 'rb') as f: + content = f.read() + headers["Content-Crc32"] = format(zlib.crc32(content), '08x') + headers["Content-Type"] = 'application/octet-stream' + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.put(url, headers=headers, params={"partNumber": "1", "uploadID": upload_id}, data=content, timeout=120) + ) + return response.json().get("error", {}).get("code") == 200 + + elif step == 3: + headers["Content-Type"] = 'text/plain;charset=UTF-8' + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=headers, params={"uploadID": upload_id}, data=f"1:{crc}", timeout=30) + ) + return response.json().get("error", {}).get("code") == 200 + + except Exception as e: + self._log(f"❌ [DY视频] 分片上传失败(步骤{step}): {e}", "ERROR") + return None if step == 1 else False + + async def _get_url_for_uri(self, uri): + """根据URI获取实际URL""" + try: + params = {"biz_type": "4", "PIGEON_BIZ_TYPE": "2", "_ts": int(time.time() * 1000), "_pms": "1", "FUSION": "true", "uri": uri, "file_type": "image"} + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.get("https://pigeon.jinritemai.com/backstage/getURLForURI", headers=self.video_headers, params=params, cookies=self.cookie) + ) + return response.json().get("data", {}).get("k3s_url") + except Exception as e: + self._log(f"❌ [DY视频] 获取封面URL失败: {e}", "ERROR") + return None + + async def _put_video(self, vid, receiver_id): + """激活视频(关键步骤:告诉抖音这个视频可以使用了) + + Args: + vid: 视频ID + receiver_id: 接收者ID + + Returns: + bool: 是否成功 + """ + try: + self._log(f"🔥 [DY视频] 激活视频: vid={vid}, receiver_id={receiver_id}", "INFO") + + headers = { + "authority": "pigeon.jinritemai.com", + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/json;charset=UTF-8", + "origin": "https://im.jinritemai.com", + "pragma": "no-cache", + "referer": "https://im.jinritemai.com/", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "x-secsdk-csrf-token": "0001000000012c9d3ac3a79ed92ea62c56059c02dea78e3b9f2c856b2095ac3596036ea5d508187236fe1d77f3fd,796bb03b67483620a4e078de481a1100" + } + url = "https://pigeon.jinritemai.com/backstage/video/putVideo" + params = { + "vid": vid, + "receiver_id": str(receiver_id), + "biz_type": "4", + "PIGEON_BIZ_TYPE": "2", + "_ts": int(time.time() * 1000), + "_pms": "1", + "FUSION": "true", + "verifyFp": "verify_mh8hyodv_XxOgxVPT_WN66_4mBk_9Ht4_O0qGQxZuhqqo", + "_v": "1.0.1.4423" + } + data = {} + data_str = json.dumps(data, separators=(',', ':')) + + response = await asyncio.get_event_loop().run_in_executor( + self.pool, + lambda: requests.post(url, headers=headers, cookies=self.cookie, params=params, data=data_str) + ) + + self._log(f"✅ [DY视频] 视频激活成功: {response.text[:200]}", "SUCCESS") + return True + + except Exception as e: + self._log(f"❌ [DY视频] 视频激活失败: {e}", "ERROR") + return False + + async def _send_video_message(self, receiver_id, talk_id, p_id, user_token, video_info): + """发送视频消息给用户""" + try: + self._log(f"📤 [DY视频] 发送视频消息给用户 {receiver_id}", "INFO") + + value, message_type = send_video( + pigeon_sign=self.config["data"]["pigeon_sign"], + token=self.config["data"]["token"], + receiver_id=receiver_id, + shop_id=self.cookie["SHOP_ID"], + talk_id=talk_id, + session_did=self.cookie["PIGEON_CID"], + p_id=p_id, + user_code=user_token, + vid=video_info["vid"], + cover_url=video_info["cover_url"], + height=video_info["height"], + width=video_info["width"], + duration=video_info["duration"] + ) + + form_data = blackboxprotobuf.encode_message(value=value, message_type=message_type) + self.ws.send_bytes(form_data) + + self._log(f"✅ [DY视频] 视频消息已发送", "SUCCESS") + return True + + except Exception as e: + self._log(f"❌ [DY视频] 发送失败: {e}", "ERROR") + return False def _log_user_tokens_state(self): """记录当前 user_tokens 状态(用于调试)""" @@ -2102,44 +2861,23 @@ class DouYinMessageHandler: self._log("🛑 消息处理器已停止", "INFO") - async def send_message_external(self, receiver_id: str, content: str) -> bool: - """外部调用的发送消息方法 - 用于后端消息转发""" + async def send_message_external(self, receiver_id: str, content: str, msg_type: str = "text") -> bool: + """外部调用的发送消息方法 - 用于后端消息转发(支持图片/视频)""" + temp_file = None try: - self._log(f"🔄 [External-{self.instance_id}] 收到转发请求: receiver_id={receiver_id}, content={content}", + self._log(f"🔄 [External-{self.instance_id}] 收到转发请求: receiver_id={receiver_id}, msg_type={msg_type}, content={content[:100] if content else ''}", "INFO") # 修复数据类型不匹配问题:将字符串转换为整数 try: receiver_id_int = int(receiver_id) - self._log(f"🔧 [External-{self.instance_id}] 转换 receiver_id: '{receiver_id}' -> {receiver_id_int}", - "DEBUG") except ValueError: self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR") return False - # 调试信息:显示当前活跃用户 - active_users = list(self.user_tokens.keys()) - self._log(f"🔍 [External-{self.instance_id}] 当前活跃用户列表: {active_users}", "DEBUG") - self._log(f"🔍 [External-{self.instance_id}] 活跃用户数量: {len(active_users)}", "DEBUG") - - # 检查用户是否存在于user_tokens中(使用整数类型) - self._log(f"🔍 [External-{self.instance_id}] 调试信息:", "DEBUG") - self._log( - f"🔍 [External-{self.instance_id}] receiver_id_int: {receiver_id_int} (类型: {type(receiver_id_int)})", - "DEBUG") - self._log(f"🔍 [External-{self.instance_id}] user_tokens keys: {list(self.user_tokens.keys())}", "DEBUG") - if self.user_tokens: - first_key = list(self.user_tokens.keys())[0] - self._log(f"🔍 [External-{self.instance_id}] 第一个key: {first_key} (类型: {type(first_key)})", "DEBUG") - self._log(f"🔍 [External-{self.instance_id}] 直接比较: {receiver_id_int == first_key}", "DEBUG") - if receiver_id_int not in self.user_tokens: self._log(f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 不在活跃会话中", "WARNING") - self._log(f"💡 [External-{self.instance_id}] 提示:用户需要先在抖音平台发送消息建立会话", "INFO") - - # 显示当前活跃用户的调试信息 self.print_active_users_debug() - return False user_info = self.user_tokens[receiver_id_int] @@ -2147,45 +2885,91 @@ class DouYinMessageHandler: p_id = user_info.get("p_id") user_token = user_info.get("token") - self._log( - f"🔍 [External-{self.instance_id}] 用户会话信息: talk_id={talk_id}, p_id={p_id}, has_token={bool(user_token)}", - "DEBUG") - - # 检查必要参数 if not talk_id or not p_id: - self._log( - f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 缺少必要的会话信息 (talk_id: {talk_id}, p_id: {p_id})", - "ERROR") + self._log(f"❌ [External-{self.instance_id}] 缺少会话信息", "ERROR") return False if not user_token: - self._log(f"⚠️ [External-{self.instance_id}] 用户 {receiver_id_int} token为空,尝试请求token", "WARNING") - # 请求用户token + self._log(f"⚠️ [External-{self.instance_id}] token为空,加入待发送队列", "WARNING") self._request_user_token(receiver_id_int, p_id) - # 将消息加入待发送队列 if "pending_messages" not in user_info: user_info["pending_messages"] = [] user_info["pending_messages"].append(content) - self._log( - f"📝 [External-{self.instance_id}] 消息已加入待发送队列,队列长度: {len(user_info['pending_messages'])}", - "INFO") return True - # 发送消息 (注意:_send_message_to_user 可能期望字符串类型的receiver_id) - success = await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, content) + # 🔥 根据消息类型分发处理 + success = False + + if msg_type == "image": + # 图片处理流程 + self._log(f"🖼️ [External-{self.instance_id}] 开始图片发送流程", "INFO") + + # 1. 下载图片 + temp_file, file_size = await self._download_media(content, "image") + if not temp_file: + self._log(f"❌ [External-{self.instance_id}] 图片下载失败,回退发送提示", "WARNING") + return await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, "[图片发送失败:下载失败]") + + # 2. 获取上传Token + token_info = await self._get_upload_token("image") + if not token_info: + self._log(f"❌ [External-{self.instance_id}] 获取Token失败", "WARNING") + return await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, "[图片发送失败:获取凭证失败]") + + # 3. 上传图片 + image_uri = await self._upload_image_to_douyin(temp_file, token_info) + if not image_uri: + self._log(f"❌ [External-{self.instance_id}] 图片上传失败", "WARNING") + return await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, "[图片发送失败:上传失败]") + + # 4. 发送图片消息 + success = await self._send_image_message(receiver_id_int, talk_id, p_id, user_token, image_uri) + + elif msg_type == "video": + # 视频处理流程 + self._log(f"🎥 [External-{self.instance_id}] 开始视频发送流程", "INFO") + + temp_file, file_size = await self._download_media(content, "video") + if not temp_file: + return await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, "[视频发送失败:下载失败]") + + token_info = await self._get_upload_token("video") + if not token_info: + return await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, "[视频发送失败:获取凭证失败]") + + video_info = await self._upload_video_to_douyin(temp_file, token_info) + if not video_info: + return await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, "[视频发送失败:上传失败]") + + # 🔥 关键新增:激活视频(告诉抖音这个视频可以使用) + vid = video_info.get("vid") + if vid: + put_success = await self._put_video(vid, receiver_id_int) + if not put_success: + self._log(f"⚠️ [External-{self.instance_id}] 视频激活失败,但继续发送", "WARNING") + else: + self._log(f"⚠️ [External-{self.instance_id}] 未获取到VID,跳过激活步骤", "WARNING") + + success = await self._send_video_message(receiver_id_int, talk_id, p_id, user_token, video_info) + + else: + # 文本消息(默认) + success = await self._send_message_to_user(receiver_id_int, talk_id, p_id, user_token, content) + if success: - # 更新最后发送时间 user_info["last_sent"] = int(time.time() * 1000) self._log(f"✅ [External-{self.instance_id}] 消息转发成功", "SUCCESS") - else: - self._log(f"❌ [External-{self.instance_id}] 消息转发失败", "ERROR") return success except Exception as e: - self._log(f"❌ [External-{self.instance_id}] 外部消息发送异常: {e}", "ERROR") - self._log(f"❌ [External-{self.instance_id}] 错误详情: {traceback.format_exc()}", "DEBUG") + self._log(f"❌ [External-{self.instance_id}] 异常: {e}", "ERROR") + self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") return False + finally: + # 清理临时文件 + if temp_file: + self._cleanup_temp_file(temp_file) def get_active_users_info(self) -> dict: """获取当前活跃用户的详细信息""" diff --git a/Utils/Dy/message_arg.py b/Utils/Dy/message_arg.py index 8275870..9840b83 100644 --- a/Utils/Dy/message_arg.py +++ b/Utils/Dy/message_arg.py @@ -8,7 +8,7 @@ import uuid import json -# 发送消息 +# 发送文本消息 def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, text: str): """ 构造发送消息消息体 @@ -78,7 +78,7 @@ def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, t '15': [ {'1': b'pigeon_source', '2': b'web'}, {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, - {'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'}, + {'1': b'pigeon_sign', '2': pigeon_sign.encode()}, # 🔥 修复:使用动态参数,不是硬编码 {'1': b'session_aid', '2': b'1383'}, {'1': b'session_did', '2': session_did.encode()}, {'1': b'app_name', '2': b'im'}, @@ -373,3 +373,229 @@ def heartbeat_message(pigeon_sign: str, token: str, session_did: str): message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'200': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}} return value, message_type + +# 🔥 新增:发送图片消息 +def send_img(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, img: str, image_width: str = "2000", image_height: str = "1125", image_format: str = "png", image_size: str = "3157512"): + """ + 构造发送图片消息体 + :param image_width: 图片宽度 + :param image_size: 图片大小 + :param image_height: 图片高度 + :param image_format: 图片格式 + :param pigeon_sign: 接口返回 + :param token: 接口返回 + :param receiver_id: wss消息返回 对方用户id + :param shop_id: cookie自带 + :param talk_id: wss消息返回 激活窗口id + :param session_did: cookie自带 + :param p_id: wss消息返回 + :param user_code: 用户token + :param img: 图片URI或URL + :return: (value, message_type) + """ + value = { + '1': 11778, + '2': int(time.time() * 1000), + '3': 10001, + '4': 1, + '5': [ + {'1': b'pigeon_source', '2': b'web'}, + {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, + {'1': b'pigeon_sign', '2': pigeon_sign.encode()}, + ], + '7': {'14': 98}, + '8': { + '1': 100, + '2': 11778, + '3': b'1.0.4-beta.2', + '4': token.encode(), + '5': 3, + '6': 3, + '7': b'2d97ea6:feat/add_init_callback', + '8': { + '100': { + '1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(), + '2': 11, + '3': p_id, + '4': "[图片]".encode(), + '5': [ + {'1': b'type', '2': b'file_image'}, + {'1': b'shop_id', '2': shop_id.encode()}, + {'1': b'sender_role', '2': b'2'}, + {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, + {'1': b'src', '2': b'pc'}, + {'1': b'srcType', '2': b'1'}, + {'1': b'source', '2': b'pc-web'}, + {'1': b'receiver_id', '2': str(receiver_id).encode()}, + {'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'}, + {'1': b'tag_valid', '2': b'1'}, + {'1': b'imageUrl', '2': img.encode()}, + {'1': b'imageWidth', '2': image_width.encode()}, + {'1': b'imageHeight', '2': image_height.encode()}, + {'1': b'imageFormat', '2': image_format.encode()}, + {'1': b'imageSize', '2': image_size.encode()}, + {'1': b'uuid', '2': str(uuid.uuid4()).encode()}, + {'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()}, + {'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}, + {'1': b'sender_id', '2': b''}, + {'1': b'biz_ext', '2': b'{}'}, + {'1': b'p:from_source', '2': b'web'}, + {'1': b's:mentioned_users', '2': b''}, + {'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()} + ], + '6': 1000, + '7': user_code.encode(), + '8': str(uuid.uuid4()).encode(), + '14': talk_id + } + }, + '9': session_did.encode(), + '11': b'web', + '15': [ + {'1': b'pigeon_source', '2': b'web'}, + {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, + {'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'}, + {'1': b'session_aid', '2': b'1383'}, + {'1': b'session_did', '2': session_did.encode()}, + {'1': b'app_name', '2': b'im'}, + {'1': b'priority_region', '2': b'cn'}, + {'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}, + {'1': b'cookie_enabled', '2': b'true'}, + {'1': b'browser_language', '2': b'zh-CN'}, + {'1': b'browser_platform', '2': b'Win32'}, + {'1': b'browser_name', '2': b'Mozilla'}, + {'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}, + {'1': b'browser_online', '2': b'true'}, + {'1': b'screen_width', '2': b'1707'}, + {'1': b'screen_height', '2': b'1067'}, + {'1': b'referer', '2': b''}, + {'1': b'timezone_name', '2': b'Asia/Shanghai'} + ], + '18': 2 + } + } + message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'100': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'bytes', 'name': ''}, '14': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}} + return value, message_type + + +# 🔥 新增:发送视频消息 +def send_video(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, vid: str, cover_url: str, height: str, width: str, duration: str): + """ + 构造发送视频消息体 + :param duration: 视频时长 + :param width: 视频宽度 + :param height: 视频高度 + :param cover_url: 封面URL + :param pigeon_sign: 接口返回 + :param token: 接口返回 + :param receiver_id: wss消息返回 对方用户id + :param shop_id: cookie自带 + :param talk_id: wss消息返回 激活窗口id + :param session_did: cookie自带 + :param p_id: wss消息返回 + :param user_code: 用户token + :param vid: 视频id + :return: (value, message_type) + """ + # 🔥 修复:确保数值类型正确(height/width为int,duration为float) + try: + height_int = int(height) if isinstance(height, str) else height + width_int = int(width) if isinstance(width, str) else width + duration_float = float(duration) if isinstance(duration, str) else duration + except (ValueError, TypeError): + # 如果转换失败,使用默认值 + height_int = 1080 + width_int = 1920 + duration_float = 0.0 + + msg_render_model = json.dumps({ + "msg_render_type": "video", + "render_body": { + "vid": vid, + "coverURL": cover_url, + "height": height_int, + "width": width_int, + "duration": duration_float + } + }).encode() + + value = { + '1': 10015, + '2': int(time.time() * 1000), + '3': 10001, + '4': 1, + '5': [ + {'1': b'pigeon_source', '2': b'web'}, + {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, + {'1': b'pigeon_sign', '2': pigeon_sign.encode()}, + ], + '7': {'14': 98}, + '8': { + '1': 100, + '2': 10015, + '3': b'1.0.4-beta.2', + '4': token.encode(), + '5': 3, + '6': 3, + '7': b'2d97ea6:feat/add_init_callback', + '8': { + '100': { + '1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(), + '2': 11, + '3': p_id, + '4': "[视频]".encode(), + '5': [ + {'1': b'type', '2': b'video'}, + {'1': b'shop_id', '2': shop_id.encode()}, + {'1': b'sender_role', '2': b'2'}, + {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, + {'1': b'src', '2': b'pc'}, + {'1': b'srcType', '2': b'1'}, + {'1': b'source', '2': b'pc-web'}, + {'1': b'receiver_id', '2': str(receiver_id).encode()}, + {'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'}, + {'1': b'msg_render_model', '2': msg_render_model}, + {'1': b'uuid', '2': str(uuid.uuid4()).encode()}, + {'1': b'start_scene', '2': b'1'}, + {'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()}, + {'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}, + {'1': b'sender_id', '2': b''}, + {'1': b'biz_ext', '2': b'{}'}, + {'1': b'p:from_source', '2': b'web'}, + {'1': b's:mentioned_users', '2': b''}, + {'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()} + ], + '6': 1000, + '7': user_code.encode(), + '8': str(uuid.uuid4()).encode(), + '14': talk_id + } + }, + '9': session_did.encode(), + '11': b'web', + '15': [ + {'1': b'pigeon_source', '2': b'web'}, + {'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, + {'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'}, + {'1': b'session_aid', '2': b'1383'}, + {'1': b'session_did', '2': session_did.encode()}, + {'1': b'app_name', '2': b'im'}, + {'1': b'priority_region', '2': b'cn'}, + {'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}, + {'1': b'cookie_enabled', '2': b'true'}, + {'1': b'browser_language', '2': b'zh-CN'}, + {'1': b'browser_platform', '2': b'Win32'}, + {'1': b'browser_name', '2': b'Mozilla'}, + {'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}, + {'1': b'browser_online', '2': b'true'}, + {'1': b'screen_width', '2': b'1707'}, + {'1': b'screen_height', '2': b'1067'}, + {'1': b'referer', '2': b''}, + {'1': b'timezone_name', '2': b'Asia/Shanghai'} + ], + '18': 2 + } + } + message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'100': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'bytes', 'name': ''}, '14': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}} + return value, message_type + diff --git a/Utils/Pdd/PddUtils.py b/Utils/Pdd/PddUtils.py index 34da6a3..3c5e9b2 100644 --- a/Utils/Pdd/PddUtils.py +++ b/Utils/Pdd/PddUtils.py @@ -3450,9 +3450,9 @@ class ChatPdd: """处理接收到的消息""" try: # 🔥 过滤机器人消息 - if self.should_filter_robot_message(message_data): - self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG") - return + # if self.should_filter_robot_message(message_data): + # self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG") + # return message_info = message_data.get("message", {}) if not message_info: diff --git a/WebSocket/BackendClient.py b/WebSocket/BackendClient.py index d17944a..634d867 100644 --- a/WebSocket/BackendClient.py +++ b/WebSocket/BackendClient.py @@ -731,7 +731,8 @@ class BackendClient: if platform_type == "京东": self._forward_to_jd(store_id, recv_pin, content) elif platform_type == "抖音": - self._forward_to_douyin(store_id, recv_pin, content) + # 传递msg_type参数,支持图片/视频等类型 + self._forward_to_douyin(store_id, recv_pin, content, msg_type) elif platform_type == "千牛": self._forward_to_qianniu(store_id, recv_pin, content) elif platform_type == "拼多多": @@ -821,8 +822,15 @@ class BackendClient: except Exception as e: print(f"[JD Forward] 转发失败: {e}") - def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str): - """转发消息到抖音平台""" + def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"): + """转发消息到抖音平台 + + Args: + store_id: 店铺ID + recv_pin: 接收者ID + content: 消息内容 + msg_type: 消息类型(text/image/video) + """ try: from Utils.Dy.DyUtils import DouYinWebsocketManager dy_mgr = DouYinWebsocketManager() @@ -838,7 +846,7 @@ class BackendClient: message_handler = platform_info.get('message_handler') print( - f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin}") + f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin} msg_type={msg_type}") if douyin_bot and message_handler and content: # 在消息处理器的事件循环中发送消息 @@ -847,16 +855,16 @@ class BackendClient: # 获取消息处理器的事件循环 loop = message_handler._loop if loop and not loop.is_closed(): - # 在事件循环中执行发送 + # 在事件循环中执行发送(传递msg_type参数) future = asyncio.run_coroutine_threadsafe( - message_handler.send_message_external(recv_pin, content), + message_handler.send_message_external(recv_pin, content, msg_type), loop ) # 等待结果 try: - result = future.result(timeout=5) + result = future.result(timeout=30) # 图片/视频需要更长时间 if result: - print(f"[DY Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}") + print(f"[DY Forward] 已转发到平台: pin={recv_pin}, type={msg_type}, content_len={len(content)}") else: print(f"[DY Forward] 转发失败: 消息处理器返回False") except Exception as fe: diff --git a/static/js/sign.js b/static/js/sign.js new file mode 100644 index 0000000..312c510 --- /dev/null +++ b/static/js/sign.js @@ -0,0 +1,474 @@ + +var CryptoJS = CryptoJS || (function (Math, undefined) { + var C = {}; + var C_lib = C.lib = {}; + var Base = C_lib.Base = (function () { + function F() {}; + return { + extend: function (overrides) { + F.prototype = this; + var subtype = new F(); + if (overrides) { + subtype.mixIn(overrides); + } + if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + subtype.init.prototype = subtype; + subtype.$super = this; + return subtype; + }, create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + return instance; + }, init: function () {}, mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + var WordArray = C_lib.WordArray = Base.extend({ + init: function (words, sigBytes) { + words = this.words = words || []; + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, concat: function (wordArray) { + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + this.clamp(); + if (thisSigBytes % 4) { + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else if (thatWords.length > 0xffff) { + for (var i = 0; i < thatSigBytes; i += 4) { + thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; + } + } else { + thisWords.push.apply(thisWords, thatWords); + } + this.sigBytes += thatSigBytes; + return this; + }, clamp: function () { + var words = this.words; + var sigBytes = this.sigBytes; + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + return clone; + }, random: function (nBytes) { + var words = []; + var r = (function (m_w) { + var m_w = m_w; + var m_z = 0x3ade68b1; + var mask = 0xffffffff; + return function () { + m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask; + m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask; + var result = ((m_z << 0x10) + m_w) & mask; + result /= 0x100000000; + result += 0.5; + return result * (Math.random() > .5 ? 1 : -1); + } + }); + for (var i = 0, rcache; i < nBytes; i += 4) { + var _r = r((rcache || Math.random()) * 0x100000000); + rcache = _r() * 0x3ade67b7; + words.push((_r() * 0x100000000) | 0); + } + return new WordArray.init(words, nBytes); + } + }); + var C_enc = C.enc = {}; + var Hex = C_enc.Hex = { + stringify: function (wordArray) { + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + return hexChars.join(''); + }, parse: function (hexStr) { + var hexStrLength = hexStr.length; + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + return new WordArray.init(words, hexStrLength / 2); + } + }; + var Latin1 = C_enc.Latin1 = { + stringify: function (wordArray) { + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + return latin1Chars.join(''); + }, parse: function (latin1Str) { + var latin1StrLength = latin1Str.length; + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + return new WordArray.init(words, latin1StrLength); + } + }; + var Utf8 = C_enc.Utf8 = { + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + reset: function () { + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, _append: function (data) { + if (typeof data == 'string') { + data = Utf8.parse(data); + } + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, _process: function (doFlush) { + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + nBlocksReady = Math.ceil(nBlocksReady); + } else { + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + var nWordsReady = nBlocksReady * blockSize; + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + this._doProcessBlock(dataWords, offset); + } + var processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + return new WordArray.init(processedWords, nBytesReady); + }, clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + return clone; + }, _minBufferSize: 0 + }); + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + cfg: Base.extend(), + init: function (cfg) { + this.cfg = this.cfg.extend(cfg); + this.reset(); + }, reset: function () { + BufferedBlockAlgorithm.reset.call(this); + this._doReset(); + }, update: function (messageUpdate) { + this._append(messageUpdate); + this._process(); + return this; + }, finalize: function (messageUpdate) { + if (messageUpdate) { + this._append(messageUpdate); + } + var hash = this._doFinalize(); + return hash; + }, blockSize: 512 / 32, + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + var C_algo = C.algo = {}; + return C; +}(Math)); + +(function () { + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var C_algo = C.algo; + var HMAC = C_algo.HMAC = Base.extend({ + init: function (hasher, key) { + hasher = this._hasher = new hasher.init(); + if (typeof key == 'string') { + key = Utf8.parse(key); + } + var hasherBlockSize = hasher.blockSize; + var hasherBlockSizeBytes = hasherBlockSize * 4; + if (key.sigBytes > hasherBlockSizeBytes) { + key = hasher.finalize(key); + } + key.clamp(); + var oKey = this._oKey = key.clone(); + var iKey = this._iKey = key.clone(); + var oKeyWords = oKey.words; + var iKeyWords = iKey.words; + for (var i = 0; i < hasherBlockSize; i++) { + oKeyWords[i] ^= 0x5c5c5c5c; + iKeyWords[i] ^= 0x36363636; + } + oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes; + this.reset(); + }, reset: function () { + var hasher = this._hasher; + hasher.reset(); + hasher.update(this._iKey); + }, update: function (messageUpdate) { + this._hasher.update(messageUpdate); + return this; + }, finalize: function (messageUpdate) { + var hasher = this._hasher; + var innerHash = hasher.finalize(messageUpdate); + hasher.reset(); + var hmac = hasher.finalize(this._oKey.clone().concat(innerHash)); + return hmac; + } + }); +}()); + +(function (Math) { + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + var H = []; + var K = []; + (function () { + function isPrime(n) { + var sqrtN = Math.sqrt(n); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n % factor)) { + return false; + } + } + return true; + } + function getFractionalBits(n) { + return ((n - (n | 0)) * 0x100000000) | 0; + } + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); + nPrime++; + } + n++; + } + }()); + var W = []; + var SHA256 = C_algo.SHA256 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init(H.slice(0)); + }, _doProcessBlock: function (M, offset) { + var H = this._hash.words; + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + var f = H[5]; + var g = H[6]; + var h = H[7]; + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ ((gamma0x << 14) | (gamma0x >>> 18)) ^ (gamma0x >>> 3); + var gamma1x = W[i - 2]; + var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ ((gamma1x << 13) | (gamma1x >>> 19)) ^ (gamma1x >>> 10); + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + var ch = (e & f) ^ (~e & g); + var maj = (a & b) ^ (a & c) ^ (b & c); + var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); + var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + H[5] = (H[5] + f) | 0; + H[6] = (H[6] + g) | 0; + H[7] = (H[7] + h) | 0; + }, _doFinalize: function () { + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + this._process(); + return this._hash; + }, clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + return clone; + } + }); + C.SHA256 = Hasher._createHelper(SHA256); + C.HmacSHA256 = Hasher._createHmacHelper(SHA256); +}(Math)); + + +function u(e) { + try { + return encodeURIComponent(e).replace(/[^A-Za-z0-9_.~\-%]+/g, escape).replace(/[*]/g, (function(e) { + return "%".concat(e.charCodeAt(0).toString(16).toUpperCase()) + } + )) + } catch (e) { + return "" + } +} + +function d(e) { + return Object.keys(e).sort().map((function(t) { + var n = e[t]; + if (null != n) { + var r = u(t); + if (r) + return Array.isArray(n) ? "".concat(r, "=").concat(n.map(u).sort().join("&".concat(r, "="))) : "".concat(r, "=").concat(u(n)) + } + } + )).filter((function(e) { + return e + } + )).join("&") +}; + + +var glo = "" + +function canonicalString(method, params, x_amz_date, x_amz_security_token, data){ + var date = "x-amz-date:" + x_amz_date; + var token = "x-amz-security-token:" + x_amz_security_token; + + if (data){ + var sha512 = CryptoJS.SHA256(JSON.stringify(data).replace(" ", "")).toString() + glo = sha512; + var content = "x-amz-content-sha256:" + sha512; + var sss = content + "\n" + date + "\n" + token + var ttt = JSON.stringify(data).replace(" ", ""); + var zzz = "x-amz-content-sha256;x-amz-date;x-amz-security-token" + }else{ + var sss = date + "\n" + token + var zzz = "x-amz-date;x-amz-security-token" + var ttt = "" + } + + + var e = [] + , t = "/"; + return e.push(method), + e.push(t), + e.push(d(params) || ""), + e.push("".concat((sss), "\n")), + e.push(zzz), + e.push(CryptoJS.SHA256(ttt)), + e.join("\n") +} + + +function stringToSign(method, type, key, params, x_amz_date, x_amz_security_token, data){ + var t = []; + return t.push("AWS4-HMAC-SHA256"), + t.push(x_amz_date), + t.push(key[0] + "/cn-north-1/" + type + "/aws4_request"), + t.push(CryptoJS.SHA256(canonicalString(method, params, x_amz_date, x_amz_security_token, data))), + t.join("\n") +} + + +function signature(method, type, secret_access_key, x_amz_date, x_amz_security_token, params, data){ + var key = x_amz_date.split("T"); + var o = CryptoJS.HmacSHA256(key[0], secret_access_key); + var i = CryptoJS.HmacSHA256("cn-north-1", o); + var s = CryptoJS.HmacSHA256(type, i); + var n = CryptoJS.HmacSHA256("aws4_request", s); + + return CryptoJS.HmacSHA256(stringToSign(method, type, key, params, x_amz_date, x_amz_security_token, data), n).toString(); +} + +// console.log(signature("POST", "vod", "AWS4D2OsX7tT/swv6JehKT62QmrUjrnR+KYH63SNma7buX3P46UoZqcHH+JHvykozVlr", "20251024T033313Z", "STS2eyJMVEFjY2Vzc0tleUlEIjoiQUtMVE5tRmhZVGhsWTJJM1lqbGpORGc0WkRsbE1UTXlZelJoWkRkaU1EaGlaV1EiLCJBY2Nlc3NLZXlJRCI6IkFLVFBOVEF6TVRSalpqSTNZV00wTkRjd1lUazBNak14WVRrelpEUXhPRGxsWXpjIiwiU2lnbmVkU2VjcmV0QWNjZXNzS2V5IjoiTWcrU1laeXlCajNyc05UL0dKaFlHYTcxUEloWURvMms1Z1J4Q2QyQk5nUDV6NXdZaWpyeEhwUmYzT1dKNW9PRWNwK3FCTVNGNXhsaG40Y3hhOHhDZmkrQWgvZU0wWS95QVhqbGhMZWJmYU09IiwiRXhwaXJlZFRpbWUiOjE3NjEyODAxNDksIlBvbGljeVN0cmluZyI6IntcIlN0YXRlbWVudFwiOlt7XCJFZmZlY3RcIjpcIkFsbG93XCIsXCJBY3Rpb25cIjpbXCJ2b2Q6QXBwbHlVcGxvYWRcIixcInZvZDpBcHBseVVwbG9hZElubmVyXCIsXCJ2b2Q6Q29tbWl0VXBsb2FkXCIsXCJ2b2Q6Q29tbWl0VXBsb2FkSW5uZXJcIixcInZvZDpHZXRVcGxvYWRDYW5kaWRhdGVzXCIsXCJJbWFnZVg6QXBwbHlJbWFnZVVwbG9hZFwiLFwiSW1hZ2VYOkNvbW1pdEltYWdlVXBsb2FkXCIsXCJJbWFnZVg6QXBwbHlVcGxvYWRJbWFnZUZpbGVcIixcIkltYWdlWDpDb21taXRVcGxvYWRJbWFnZUZpbGVcIl0sXCJSZXNvdXJjZVwiOltcIipcIl0sXCJDb25kaXRpb25cIjpcIntcXFwiUFNNXFxcIjpcXFwiY21wLmVjb20ucGlnZW9uX2FwaVxcXCJ9XCJ9XX0iLCJTaWduYXR1cmUiOiIxOTBmNzkxZTU2NTk0MzgwOGIzNTllYTJhOWI4OGExOTAyZDE5MTEwODBhMmNmYmVhMTQwZmVmYWNhYTViNGU5In0=", { +// "Action": "CommitUploadInner", +// "Version": "2020-11-19", +// "SpaceName": "pigeon-video" +// }, { +// "SessionKey": "eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyNzY3OTI3Nzk2NDYzMTUiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlWSnJSM2xRUkVNaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VG1wTmVFOVVTWE5KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2V1hwTlZHdDVURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUbENZbTFhVDFKWFZrWlJibXg0WWxab1EwOUZXbTVoVmtGNlVrZHNTVk51UW5CbFdGcElZakJHVlZaVVVYZGhSVTB5U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM0eVh6UlNVakpXYkROeWN6SmFUV3B5ZGt4eFEyTmxkVE54WTI1RWFGa3RPWGhmU1RkTGFtRXdUVE5KSW4wPTo0ZTU0MTNhZTY2OTFjM2U2ZTU2ODc0NGEyYmQzNzZiMDFiODY0YjI4OGRhODg0MzY4ZDA1YzJiMTE4ZWE1MGM4IiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L29BbmZORWVFQnlxbVhCOEZnaVAzRGlISnBpeXZHb0FUVTQwaEM2IiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0ZjZ1N29nNjVvcGJ1YWk1cTAifQ==", +// "Functions": [ +// { +// "name": "GetMeta" +// }, +// { +// "name": "Snapshot", +// "input": { +// "SnapshotTime": 0 +// } +// } +// ] +// })) + +//console.log(d({"SessionKey":"eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyMTU2Nzc4NDc0NTE1ODMiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlVRlhkVmx1VDNBaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VFVSSmQwNTZZM05KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2UVhsTlJHTXpURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUa3pVMVZTV1U5V1JtMVNTR2hFV214V2RsbFlXblZYYlhjelVsZGFRbG93TURSU01GWlpVV3BTZGxFd1RrNVZhMFp1U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM1SVJEWktTRGxYUzJoVFh6Uk9TRWhyY0VGSVQzZHpPV3hrTmxjNGNXeGFSVGcwTkVKTVFtOVZXa3BySW4wPTpjM2I3YjRmY2U1ZjFlMDQ4NWNhYTFhMjNmNTE1ZTM0NjUxN2QxNjI3YzQ3YzAyNDdlMjlmNTE4ODcxYjlhYzNhIiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L293SURYOVFmRHhDZlVvYXZuWmw3RWZBZ004R0VYQjRvQ0NNUkFnIiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0MDlmZm9nNjVxczRwMWdsMGcifQ==","Functions":[{"name":"GetMeta"},{"name":"Snapshot","input":{"SnapshotTime":0}}]})) + + +// console.log(CryptoJS.SHA256(JSON.stringify({"SessionKey":"eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyMTU2Nzc4NDc0NTE1ODMiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlVRlhkVmx1VDNBaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VFVSSmQwNTZZM05KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2UVhsTlJHTXpURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUa3pVMVZTV1U5V1JtMVNTR2hFV214V2RsbFlXblZYYlhjelVsZGFRbG93TURSU01GWlpVV3BTZGxFd1RrNVZhMFp1U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM1SVJEWktTRGxYUzJoVFh6Uk9TRWhyY0VGSVQzZHpPV3hrTmxjNGNXeGFSVGcwTkVKTVFtOVZXa3BySW4wPTpjM2I3YjRmY2U1ZjFlMDQ4NWNhYTFhMjNmNTE1ZTM0NjUxN2QxNjI3YzQ3YzAyNDdlMjlmNTE4ODcxYjlhYzNhIiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L293SURYOVFmRHhDZlVvYXZuWmw3RWZBZ004R0VYQjRvQ0NNUkFnIiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0MDlmZm9nNjVxczRwMWdsMGcifQ==","Functions":[{"name":"GetMeta"},{"name":"Snapshot","input":{"SnapshotTime":0}}]}).replace(" ")).toString()) \ No newline at end of file