[patch] 新增DY 图片 视频上传 发送等方法逻辑集成 优化抖音心跳维护与弹性发送心跳包 DY集成内置js环境 PDD取消过滤系统机器消息
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
# ===== 抖音登录相关类集成开始 =====
|
||||
|
||||
@@ -664,6 +678,14 @@ class DouYinMessageHandler:
|
||||
"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"],
|
||||
@@ -1881,6 +1927,719 @@ class DouYinMessageHandler:
|
||||
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 状态(用于调试)"""
|
||||
self._log("🔍 当前 user_tokens 状态:", "DEBUG")
|
||||
@@ -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:
|
||||
"""获取当前活跃用户的详细信息"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
474
static/js/sign.js
Normal file
474
static/js/sign.js
Normal 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())
|
||||
Reference in New Issue
Block a user