[patch] 新增DY 图片 视频上传 发送等方法逻辑集成 优化抖音心跳维护与弹性发送心跳包 DY集成内置js环境 PDD取消过滤系统机器消息

This commit is contained in:
2025-10-27 16:32:07 +08:00
parent 787c52a8dc
commit 1f1deb5f7f
5 changed files with 1556 additions and 64 deletions

View File

@@ -14,12 +14,26 @@ from dataclasses import dataclass, asdict
from urllib.parse import urlencode from urllib.parse import urlencode
import re import re
import random import random
import base64
import os
import zlib
import hashlib
import string
from concurrent.futures import ThreadPoolExecutor
import config import config
# 导入 message_arg 中的方法 # 导入 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 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", "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" "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}") print(f"[DY Handler] 创建实例 {self.instance_id} for store {store_id}")
@@ -959,28 +981,52 @@ class DouYinMessageHandler:
def on_error(self, ws, error): def on_error(self, ws, error):
"""WebSocket错误处理""" """WebSocket错误处理"""
self._log(f"❌ WebSocket错误: {error}", "ERROR") 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): def on_close(self, ws, close_status_code, close_msg):
"""WebSocket关闭处理""" """WebSocket关闭处理"""
self._log(f"🔌 连接关闭: {close_status_code}, {close_msg}", "WARNING") 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): def heartbeat_wss(self):
"""心跳线程""" """心跳线程"""
heartbeat_count = 0
while self.is_running: while self.is_running:
try: try:
if self.ws and hasattr(self.ws, 'sock') and self.ws.sock and self.ws.sock.connected: if self.ws and hasattr(self.ws, 'sock') and self.ws.sock and self.ws.sock.connected:
self.send_heartbeat() 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: except Exception as e:
self._log(f"❌ 心跳发送失败: {e}", "ERROR") self._log(f"❌ 心跳发送失败: {e}", "ERROR")
import traceback
self._log(f"心跳异常详情: {traceback.format_exc()}", "DEBUG")
time.sleep(5) time.sleep(5)
def send_heartbeat(self): def send_heartbeat(self):
"""发送心跳包 - 使用 message_arg 中的方法""" """发送心跳包 - 使用 message_arg 中的方法"""
try: try:
# 使用 message_arg 中的 heartbeat_message 方法
value, message_type = heartbeat_message( value, message_type = heartbeat_message(
pigeon_sign=self.config["data"]["pigeon_sign"], pigeon_sign=self.config["data"]["pigeon_sign"],
token=self.config["data"]["token"], token=self.config["data"]["token"],
@@ -1880,6 +1926,719 @@ class DouYinMessageHandler:
self._log(f"📤 已请求用户 {receiver_id} 的token", "INFO") self._log(f"📤 已请求用户 {receiver_id} 的token", "INFO")
except Exception as e: except Exception as e:
self._log(f"❌ 请求token失败: {e}", "ERROR") 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): def _log_user_tokens_state(self):
"""记录当前 user_tokens 状态(用于调试)""" """记录当前 user_tokens 状态(用于调试)"""
@@ -2102,44 +2861,23 @@ class DouYinMessageHandler:
self._log("🛑 消息处理器已停止", "INFO") 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: 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") "INFO")
# 修复数据类型不匹配问题:将字符串转换为整数 # 修复数据类型不匹配问题:将字符串转换为整数
try: try:
receiver_id_int = int(receiver_id) receiver_id_int = int(receiver_id)
self._log(f"🔧 [External-{self.instance_id}] 转换 receiver_id: '{receiver_id}' -> {receiver_id_int}",
"DEBUG")
except ValueError: except ValueError:
self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR") self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR")
return False 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: 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}] 用户 {receiver_id_int} 不在活跃会话中", "WARNING")
self._log(f"💡 [External-{self.instance_id}] 提示:用户需要先在抖音平台发送消息建立会话", "INFO")
# 显示当前活跃用户的调试信息
self.print_active_users_debug() self.print_active_users_debug()
return False return False
user_info = self.user_tokens[receiver_id_int] user_info = self.user_tokens[receiver_id_int]
@@ -2147,45 +2885,91 @@ class DouYinMessageHandler:
p_id = user_info.get("p_id") p_id = user_info.get("p_id")
user_token = user_info.get("token") 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: if not talk_id or not p_id:
self._log( self._log(f"❌ [External-{self.instance_id}] 缺少会话信息", "ERROR")
f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 缺少必要的会话信息 (talk_id: {talk_id}, p_id: {p_id})",
"ERROR")
return False return False
if not user_token: if not user_token:
self._log(f"⚠️ [External-{self.instance_id}] 用户 {receiver_id_int} token为空尝试请求token", "WARNING") self._log(f"⚠️ [External-{self.instance_id}] token为空加入待发送队列", "WARNING")
# 请求用户token
self._request_user_token(receiver_id_int, p_id) self._request_user_token(receiver_id_int, p_id)
# 将消息加入待发送队列
if "pending_messages" not in user_info: if "pending_messages" not in user_info:
user_info["pending_messages"] = [] user_info["pending_messages"] = []
user_info["pending_messages"].append(content) user_info["pending_messages"].append(content)
self._log(
f"📝 [External-{self.instance_id}] 消息已加入待发送队列,队列长度: {len(user_info['pending_messages'])}",
"INFO")
return True 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: if success:
# 更新最后发送时间
user_info["last_sent"] = int(time.time() * 1000) user_info["last_sent"] = int(time.time() * 1000)
self._log(f"✅ [External-{self.instance_id}] 消息转发成功", "SUCCESS") self._log(f"✅ [External-{self.instance_id}] 消息转发成功", "SUCCESS")
else:
self._log(f"❌ [External-{self.instance_id}] 消息转发失败", "ERROR")
return success return success
except Exception as e: except Exception as e:
self._log(f"❌ [External-{self.instance_id}] 外部消息发送异常: {e}", "ERROR") self._log(f"❌ [External-{self.instance_id}] 异常: {e}", "ERROR")
self._log(f"❌ [External-{self.instance_id}] 错误详情: {traceback.format_exc()}", "DEBUG") self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False return False
finally:
# 清理临时文件
if temp_file:
self._cleanup_temp_file(temp_file)
def get_active_users_info(self) -> dict: def get_active_users_info(self) -> dict:
"""获取当前活跃用户的详细信息""" """获取当前活跃用户的详细信息"""

View File

@@ -8,7 +8,7 @@ import uuid
import json 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): 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': [ '15': [
{'1': b'pigeon_source', '2': b'web'}, {'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, {'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_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()}, {'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'}, {'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': ''}} 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 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为intduration为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

View File

@@ -3450,9 +3450,9 @@ class ChatPdd:
"""处理接收到的消息""" """处理接收到的消息"""
try: try:
# 🔥 过滤机器人消息 # 🔥 过滤机器人消息
if self.should_filter_robot_message(message_data): # if self.should_filter_robot_message(message_data):
self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG") # self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG")
return # return
message_info = message_data.get("message", {}) message_info = message_data.get("message", {})
if not message_info: if not message_info:

View File

@@ -731,7 +731,8 @@ class BackendClient:
if platform_type == "京东": if platform_type == "京东":
self._forward_to_jd(store_id, recv_pin, content) self._forward_to_jd(store_id, recv_pin, content)
elif platform_type == "抖音": 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 == "千牛": elif platform_type == "千牛":
self._forward_to_qianniu(store_id, recv_pin, content) self._forward_to_qianniu(store_id, recv_pin, content)
elif platform_type == "拼多多": elif platform_type == "拼多多":
@@ -821,8 +822,15 @@ class BackendClient:
except Exception as e: except Exception as e:
print(f"[JD Forward] 转发失败: {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: try:
from Utils.Dy.DyUtils import DouYinWebsocketManager from Utils.Dy.DyUtils import DouYinWebsocketManager
dy_mgr = DouYinWebsocketManager() dy_mgr = DouYinWebsocketManager()
@@ -838,7 +846,7 @@ class BackendClient:
message_handler = platform_info.get('message_handler') message_handler = platform_info.get('message_handler')
print( 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: if douyin_bot and message_handler and content:
# 在消息处理器的事件循环中发送消息 # 在消息处理器的事件循环中发送消息
@@ -847,16 +855,16 @@ class BackendClient:
# 获取消息处理器的事件循环 # 获取消息处理器的事件循环
loop = message_handler._loop loop = message_handler._loop
if loop and not loop.is_closed(): if loop and not loop.is_closed():
# 在事件循环中执行发送 # 在事件循环中执行发送传递msg_type参数
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
message_handler.send_message_external(recv_pin, content), message_handler.send_message_external(recv_pin, content, msg_type),
loop loop
) )
# 等待结果 # 等待结果
try: try:
result = future.result(timeout=5) result = future.result(timeout=30) # 图片/视频需要更长时间
if result: 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: else:
print(f"[DY Forward] 转发失败: 消息处理器返回False") print(f"[DY Forward] 转发失败: 消息处理器返回False")
except Exception as fe: except Exception as fe:

474
static/js/sign.js Normal file
View File

@@ -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())