[patch] 新增JD图片视频类型数据发送方法编写(强化send_message方法) 新增下载视频临时处理存储逻辑
This commit is contained in:
@@ -13,6 +13,8 @@ import requests
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
# 定义持久化数据类
|
# 定义持久化数据类
|
||||||
@@ -155,6 +157,14 @@ class FixJdCookie:
|
|||||||
self.max_reconnect_delay = 60.0 # 最大重连延迟
|
self.max_reconnect_delay = 60.0 # 最大重连延迟
|
||||||
self.reconnect_backoff = 1.5 # 退避系数
|
self.reconnect_backoff = 1.5 # 退避系数
|
||||||
|
|
||||||
|
# 🔥 存储认证信息,用于文件上传
|
||||||
|
self.cookies_str = None
|
||||||
|
self.current_aid = None
|
||||||
|
self.current_pin_zj = None
|
||||||
|
|
||||||
|
# 🔥 启动时清理过期临时文件
|
||||||
|
self._cleanup_old_temp_files(max_age_hours=24)
|
||||||
|
|
||||||
def _log(self, message, log_type="INFO"):
|
def _log(self, message, log_type="INFO"):
|
||||||
"""内部日志方法"""
|
"""内部日志方法"""
|
||||||
if self.log_callback:
|
if self.log_callback:
|
||||||
@@ -313,10 +323,27 @@ class FixJdCookie:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_message(self, ws, pin, aid, pin_zj, vender_id, content):
|
async def send_message(self, ws, pin, aid, pin_zj, vender_id, content, msg_type="text"):
|
||||||
"""异步发送单条消息"""
|
"""异步发送消息 - 支持文本/图片/视频
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ws: WebSocket连接
|
||||||
|
pin: 客户pin
|
||||||
|
aid: 账号aid
|
||||||
|
pin_zj: 客服pin
|
||||||
|
vender_id: 商家ID
|
||||||
|
content: 消息内容(文本内容或URL)
|
||||||
|
msg_type: 消息类型 (text/image/video)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
print('本地发送消息')
|
# 根据消息类型调用不同的发送方法
|
||||||
|
if msg_type == "image":
|
||||||
|
return await self.send_image_message(ws, pin, aid, pin_zj, vender_id, content)
|
||||||
|
elif msg_type == "video":
|
||||||
|
return await self.send_video_message(ws, pin, aid, pin_zj, vender_id, content)
|
||||||
|
else:
|
||||||
|
# 文本消息(默认)
|
||||||
|
print('本地发送文本消息')
|
||||||
message = {
|
message = {
|
||||||
"ver": "4.3",
|
"ver": "4.3",
|
||||||
"type": "chat_message",
|
"type": "chat_message",
|
||||||
@@ -344,6 +371,528 @@ class FixJdCookie:
|
|||||||
logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}")
|
logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _get_temp_directory(self) -> str:
|
||||||
|
"""智能选择临时文件目录(优先级降级策略)
|
||||||
|
|
||||||
|
优先级:
|
||||||
|
1. 环境变量 SHUIDROP_TEMP_DIR(用户自定义)
|
||||||
|
2. 应用程序所在目录/temp(如果有写权限且不在Program Files)
|
||||||
|
3. 系统临时目录(兜底方案)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 临时目录路径
|
||||||
|
"""
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 优先级1:用户自定义环境变量
|
||||||
|
custom_temp = os.getenv('SHUIDROP_TEMP_DIR')
|
||||||
|
if custom_temp:
|
||||||
|
custom_temp = os.path.join(custom_temp, "shuidrop_jd_temp_uploads")
|
||||||
|
try:
|
||||||
|
os.makedirs(custom_temp, exist_ok=True)
|
||||||
|
# 测试写权限
|
||||||
|
test_file = os.path.join(custom_temp, ".write_test")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write("test")
|
||||||
|
os.remove(test_file)
|
||||||
|
self._log(f"✅ [JD路径] 使用自定义临时目录: {custom_temp}", "INFO")
|
||||||
|
return custom_temp
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD路径] 自定义目录不可用: {e}", "WARNING")
|
||||||
|
|
||||||
|
# 优先级2:应用程序所在目录(避免占用C盘)
|
||||||
|
try:
|
||||||
|
# 获取可执行文件路径
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# 打包后
|
||||||
|
app_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
# 开发环境
|
||||||
|
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# 检查是否在 Program Files(需要管理员权限)
|
||||||
|
if 'Program Files' not in app_dir and 'Program Files (x86)' not in app_dir:
|
||||||
|
app_temp = os.path.join(app_dir, "temp_uploads", "jd")
|
||||||
|
os.makedirs(app_temp, exist_ok=True)
|
||||||
|
|
||||||
|
# 测试写权限
|
||||||
|
test_file = os.path.join(app_temp, ".write_test")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write("test")
|
||||||
|
os.remove(test_file)
|
||||||
|
|
||||||
|
self._log(f"✅ [JD路径] 使用应用程序目录: {app_temp}", "INFO")
|
||||||
|
return app_temp
|
||||||
|
else:
|
||||||
|
self._log(f"ℹ️ [JD路径] 应用在Program Files,跳过应用目录", "DEBUG")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD路径] 应用目录不可用: {e}", "DEBUG")
|
||||||
|
|
||||||
|
# 优先级3:系统临时目录(兜底方案)
|
||||||
|
system_temp = os.path.join(tempfile.gettempdir(), "shuidrop_jd_temp_uploads")
|
||||||
|
os.makedirs(system_temp, exist_ok=True)
|
||||||
|
self._log(f"✅ [JD路径] 使用系统临时目录: {system_temp}", "INFO")
|
||||||
|
return system_temp
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 最终兜底
|
||||||
|
self._log(f"❌ [JD路径] 所有路径策略失败,使用默认: {e}", "ERROR")
|
||||||
|
import tempfile
|
||||||
|
return os.path.join(tempfile.gettempdir(), "shuidrop_jd_temp_uploads")
|
||||||
|
|
||||||
|
def _get_file_extension(self, url: str, default_ext: str) -> str:
|
||||||
|
"""智能提取文件扩展名
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def _cleanup_old_temp_files(self, max_age_hours=24):
|
||||||
|
"""清理过期的临时文件(24小时以上)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_age_hours: 文件最大保留时间(小时)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用智能路径选择
|
||||||
|
temp_dir = self._get_temp_directory()
|
||||||
|
if not os.path.exists(temp_dir):
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
max_age_seconds = max_age_hours * 3600
|
||||||
|
cleaned_count = 0
|
||||||
|
|
||||||
|
# 遍历临时目录中的文件
|
||||||
|
for filename in os.listdir(temp_dir):
|
||||||
|
file_path = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
# 只处理文件,跳过目录
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 检查文件年龄
|
||||||
|
file_mtime = os.path.getmtime(file_path)
|
||||||
|
file_age = now - file_mtime
|
||||||
|
|
||||||
|
if file_age > max_age_seconds:
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
os.remove(file_path)
|
||||||
|
cleaned_count += 1
|
||||||
|
self._log(f"🗑️ [JD清理] 删除过期文件: {filename} ({file_size} bytes, {file_age/3600:.1f}小时前)", "DEBUG")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD清理] 删除文件失败 {filename}: {e}", "DEBUG")
|
||||||
|
|
||||||
|
if cleaned_count > 0:
|
||||||
|
self._log(f"✅ [JD清理] 已清理 {cleaned_count} 个过期临时文件", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD清理] 临时文件清理失败: {e}", "DEBUG")
|
||||||
|
|
||||||
|
async def download_file(self, url: str, save_dir: str = None, max_retries: int = 3) -> str:
|
||||||
|
"""下载外部文件到本地(带重试机制 + 智能路径选择)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 文件URL
|
||||||
|
save_dir: 保存目录(None时自动选择最佳路径)
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 本地文件路径
|
||||||
|
"""
|
||||||
|
# 使用智能路径选择策略
|
||||||
|
if save_dir is None:
|
||||||
|
save_dir = self._get_temp_directory()
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 智能提取文件扩展名
|
||||||
|
default_ext = 'jpg'
|
||||||
|
ext = self._get_file_extension(url, default_ext)
|
||||||
|
|
||||||
|
# 生成唯一文件名
|
||||||
|
file_name = f"jd_download_{uuid.uuid4().hex[:12]}.{ext}"
|
||||||
|
save_path = os.path.join(save_dir, file_name)
|
||||||
|
|
||||||
|
# 重试机制
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
if attempt > 0:
|
||||||
|
self._log(f"🔄 [JD下载] 第{attempt + 1}次重试下载...", "INFO")
|
||||||
|
else:
|
||||||
|
self._log(f"📥 [JD下载] 开始下载: {url[:100]}...", "INFO")
|
||||||
|
|
||||||
|
# 使用线程池下载
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
response = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: requests.get(url, timeout=60, headers={
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查HTTP状态
|
||||||
|
if response.status_code != 200:
|
||||||
|
self._log(f"❌ [JD下载] HTTP状态码: {response.status_code}", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
continue
|
||||||
|
raise Exception(f"下载失败,HTTP状态码: {response.status_code}")
|
||||||
|
|
||||||
|
# 获取文件数据
|
||||||
|
file_data = response.content
|
||||||
|
file_size_kb = len(file_data) // 1024
|
||||||
|
|
||||||
|
# 检查文件大小(限制50MB)
|
||||||
|
if file_size_kb > 51200:
|
||||||
|
self._log(f"❌ [JD下载] 文件过大: {file_size_kb}KB,超过50MB限制", "ERROR")
|
||||||
|
raise Exception(f"文件过大: {file_size_kb}KB")
|
||||||
|
|
||||||
|
# 检查文件是否为空
|
||||||
|
if file_size_kb == 0:
|
||||||
|
self._log(f"❌ [JD下载] 下载的文件为空", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
raise Exception("下载的文件为空")
|
||||||
|
|
||||||
|
# 写入临时文件
|
||||||
|
with open(save_path, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
|
||||||
|
self._log(f"✅ [JD下载] 下载成功,大小: {file_size_kb}KB,文件: {file_name}", "SUCCESS")
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self._log(f"❌ [JD下载] 网络请求失败: {e}", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ [JD下载] 下载失败: {e}", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 所有重试都失败
|
||||||
|
raise Exception(f"下载失败:已重试{max_retries}次")
|
||||||
|
|
||||||
|
async def upload_file_to_jd(self, file_path: str, file_type: str) -> dict:
|
||||||
|
"""上传文件到京东服务器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 本地文件路径
|
||||||
|
file_type: 文件类型 (image/video)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {path: 京东URL, width: 宽度, height: 高度}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.cookies_str or not self.current_aid or not self.current_pin_zj:
|
||||||
|
raise Exception("缺少必要的认证信息(cookies/aid/pin)")
|
||||||
|
|
||||||
|
self._log(f"📤 开始上传文件到京东: {file_path}", "INFO")
|
||||||
|
|
||||||
|
# 读取文件内容
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
file_content = f.read()
|
||||||
|
|
||||||
|
# 根据文件类型选择API和MIME类型
|
||||||
|
if file_type == "image" or any(ext in file_path.lower() for ext in ['.png', '.jpg', '.jpeg', '.gif']):
|
||||||
|
url = "https://imio.jd.com/uploadfile/file/uploadImg.action"
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
elif file_type == "video" or '.mp4' in file_path.lower():
|
||||||
|
url = "https://imio.jd.com/uploadfile/file/uploadFile.action"
|
||||||
|
mime_type = 'video/mp4'
|
||||||
|
else:
|
||||||
|
raise Exception(f"不支持的文件类型: {file_path}")
|
||||||
|
|
||||||
|
# 准备请求
|
||||||
|
headers = {
|
||||||
|
"authority": "imio.jd.com",
|
||||||
|
"accept": "*/*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
"origin": "https://dongdong.jd.com",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
"referer": "https://dongdong.jd.com/",
|
||||||
|
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
|
||||||
|
"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/134.0.0.0 Safari/537.36",
|
||||||
|
"cookie": self.cookies_str
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'upload': (os.path.basename(file_path), file_content, mime_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'httpsEnable': 'true',
|
||||||
|
'clientType': 'comet',
|
||||||
|
'appId': 'im.waiter',
|
||||||
|
'pin': self.current_pin_zj,
|
||||||
|
'aid': self.current_aid
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同步请求(在异步上下文中使用 run_in_executor)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
response = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: requests.post(url, headers=headers, files=files, data=data, timeout=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('path'):
|
||||||
|
self._log(f"✅ 文件上传成功: {result.get('path')}", "SUCCESS")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise Exception(f"上传失败: {result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 文件上传失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_video_thumbnail(self, video_path: str) -> dict:
|
||||||
|
"""提取视频封面并上传
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_path: 视频文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {path: 封面URL, width: 宽度, height: 高度}
|
||||||
|
"""
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._log(f"🎬 开始提取视频封面: {video_path}", "INFO")
|
||||||
|
|
||||||
|
# 提取视频第一帧
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
frame_position = int(0 * fps) # 第0秒
|
||||||
|
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_position)
|
||||||
|
|
||||||
|
ret, frame = cap.read()
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
raise Exception("无法读取视频帧")
|
||||||
|
|
||||||
|
# 编码为JPG
|
||||||
|
success, encoded_image = cv2.imencode('.jpg', frame)
|
||||||
|
if not success:
|
||||||
|
raise Exception("无法编码图像")
|
||||||
|
|
||||||
|
thumbnail_content = encoded_image.tobytes()
|
||||||
|
|
||||||
|
# 上传封面(使用图片上传接口)
|
||||||
|
headers = {
|
||||||
|
"authority": "imio.jd.com",
|
||||||
|
"accept": "*/*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
"origin": "https://dongdong.jd.com",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
"referer": "https://dongdong.jd.com/",
|
||||||
|
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
|
||||||
|
"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/134.0.0.0 Safari/537.36",
|
||||||
|
"cookie": self.cookies_str
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "https://imio.jd.com/uploadfile/file/uploadImg.action"
|
||||||
|
files = {
|
||||||
|
'upload': ("thumbnail.jpg", thumbnail_content, 'image/jpeg')
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'httpsEnable': 'true',
|
||||||
|
'clientType': 'comet',
|
||||||
|
'appId': 'im.waiter',
|
||||||
|
'pin': self.current_pin_zj,
|
||||||
|
'aid': self.current_aid
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同步请求
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
response = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: requests.post(url, headers=headers, files=files, data=data, timeout=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('path'):
|
||||||
|
self._log(f"✅ 视频封面上传成功: {result.get('path')}", "SUCCESS")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise Exception(f"封面上传失败: {result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 视频封面提取失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def send_image_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, image_url: str):
|
||||||
|
"""发送图片消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ws: WebSocket连接
|
||||||
|
pin: 客户pin
|
||||||
|
aid: 账号aid
|
||||||
|
pin_zj: 客服pin
|
||||||
|
vender_id: 商家ID
|
||||||
|
image_url: 图片URL(外部URL,需要下载后上传)
|
||||||
|
"""
|
||||||
|
temp_file = None
|
||||||
|
try:
|
||||||
|
self._log(f"📷 开始发送图片消息: {image_url}", "INFO")
|
||||||
|
|
||||||
|
# 1. 下载图片
|
||||||
|
temp_file = await self.download_file(image_url)
|
||||||
|
|
||||||
|
# 2. 上传到京东服务器
|
||||||
|
upload_result = await self.upload_file_to_jd(temp_file, "image")
|
||||||
|
|
||||||
|
# 3. 发送图片消息
|
||||||
|
message = {
|
||||||
|
"ver": "4.3",
|
||||||
|
"type": "chat_message",
|
||||||
|
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
|
||||||
|
"to": {"app": "im.customer", "pin": pin},
|
||||||
|
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
|
||||||
|
"lang": "zh_CN",
|
||||||
|
"aid": aid,
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"readFlag": 0,
|
||||||
|
"body": {
|
||||||
|
"height": upload_result.get("height", 0),
|
||||||
|
"width": upload_result.get("width", 0),
|
||||||
|
"url": upload_result.get("path"),
|
||||||
|
"translated": "",
|
||||||
|
"param": {"cusVenderId": vender_id},
|
||||||
|
"type": "image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.send(json.dumps(message))
|
||||||
|
self._log(f"✅ 图片消息发送成功: {pin}", "SUCCESS")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 图片消息发送失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_file and os.path.exists(temp_file):
|
||||||
|
try:
|
||||||
|
os.remove(temp_file)
|
||||||
|
self._log(f"🗑️ 已清理临时文件: {temp_file}", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def send_video_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, video_url: str):
|
||||||
|
"""发送视频消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ws: WebSocket连接
|
||||||
|
pin: 客户pin
|
||||||
|
aid: 账号aid
|
||||||
|
pin_zj: 客服pin
|
||||||
|
vender_id: 商家ID
|
||||||
|
video_url: 视频URL(外部URL,需要下载后上传)
|
||||||
|
"""
|
||||||
|
temp_video = None
|
||||||
|
try:
|
||||||
|
self._log(f"🎥 开始发送视频消息: {video_url}", "INFO")
|
||||||
|
|
||||||
|
# 1. 下载视频
|
||||||
|
temp_video = await self.download_file(video_url)
|
||||||
|
|
||||||
|
# 2. 提取并上传封面
|
||||||
|
thumbnail_result = await self.get_video_thumbnail(temp_video)
|
||||||
|
|
||||||
|
# 3. 上传视频
|
||||||
|
await asyncio.sleep(2) # 等待2秒,避免请求过快
|
||||||
|
video_result = await self.upload_file_to_jd(temp_video, "video")
|
||||||
|
|
||||||
|
# 4. 发送视频消息
|
||||||
|
message = {
|
||||||
|
"ver": "4.3",
|
||||||
|
"type": "chat_message",
|
||||||
|
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
|
||||||
|
"to": {"app": "im.customer", "pin": pin},
|
||||||
|
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
|
||||||
|
"lang": "zh_CN",
|
||||||
|
"aid": aid,
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"readFlag": 0,
|
||||||
|
"body": {
|
||||||
|
"desc": "",
|
||||||
|
"duration": 6,
|
||||||
|
"param": {},
|
||||||
|
"reupload": "false",
|
||||||
|
"size": os.path.getsize(temp_video),
|
||||||
|
"thumbHeight": thumbnail_result.get("height", 0),
|
||||||
|
"thumbWidth": thumbnail_result.get("width", 0),
|
||||||
|
"thumbnail": thumbnail_result.get("path"),
|
||||||
|
"url": video_result.get("path"),
|
||||||
|
"type": "video"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.send(json.dumps(message))
|
||||||
|
self._log(f"✅ 视频消息发送成功: {pin}", "SUCCESS")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 视频消息发送失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_video and os.path.exists(temp_video):
|
||||||
|
try:
|
||||||
|
os.remove(temp_video)
|
||||||
|
self._log(f"🗑️ 已清理临时视频: {temp_video}", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_userinfo(self, response_text):
|
def get_userinfo(self, response_text):
|
||||||
"""获取用户 pin 并存入 pins 列表"""
|
"""获取用户 pin 并存入 pins 列表"""
|
||||||
pin = jsonpath.jsonpath(json.loads(response_text), "$..from.pin")
|
pin = jsonpath.jsonpath(json.loads(response_text), "$..from.pin")
|
||||||
@@ -555,6 +1104,11 @@ class FixJdCookie:
|
|||||||
print("✅ DEBUG 进入message_monitoring方法")
|
print("✅ DEBUG 进入message_monitoring方法")
|
||||||
print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}")
|
print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}")
|
||||||
|
|
||||||
|
# 🔥 保存认证信息,用于文件上传
|
||||||
|
self.cookies_str = cookies_str
|
||||||
|
self.current_aid = aid
|
||||||
|
self.current_pin_zj = pin_zj
|
||||||
|
|
||||||
# 连接后端AI服务 - 使用店铺ID或venderId
|
# 连接后端AI服务 - 使用店铺ID或venderId
|
||||||
store_id = str(store.get('id', '')) or str(vender_id)
|
store_id = str(store.get('id', '')) or str(vender_id)
|
||||||
self._log(f"🔗 尝试连接后端服务,店铺ID: {store_id}", "DEBUG")
|
self._log(f"🔗 尝试连接后端服务,店铺ID: {store_id}", "DEBUG")
|
||||||
@@ -603,7 +1157,8 @@ class FixJdCookie:
|
|||||||
aid=aid,
|
aid=aid,
|
||||||
pin_zj=pin_zj,
|
pin_zj=pin_zj,
|
||||||
platform="京东",
|
platform="京东",
|
||||||
loop=loop
|
loop=loop,
|
||||||
|
cookies_str=cookies_str # 🔥 传递cookies用于文件上传
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
|
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
|
||||||
@@ -614,9 +1169,9 @@ class FixJdCookie:
|
|||||||
try:
|
try:
|
||||||
while not stop_event.is_set(): # 检查是否收到中止信号
|
while not stop_event.is_set(): # 检查是否收到中止信号
|
||||||
try:
|
try:
|
||||||
print(f"[JD]等待监听消息-{datetime.now()}")
|
print(f"等待监听消息-{datetime.now()}")
|
||||||
response = await asyncio.wait_for(ws.recv(), timeout=1)
|
response = await asyncio.wait_for(ws.recv(), timeout=1)
|
||||||
print(f"[JD]原始消息类型:{type(response)}, 消息体为: {response}")
|
print(f"原始消息类型:{type(response)}, 消息体为: {response}")
|
||||||
|
|
||||||
# 🔧 修复:检测被踢下线消息
|
# 🔧 修复:检测被踢下线消息
|
||||||
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
|
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
|
||||||
@@ -654,10 +1209,10 @@ class FixJdCookie:
|
|||||||
|
|
||||||
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
|
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
|
||||||
|
|
||||||
# print(json_resp) 暂去解析后消息结构
|
# print(json_resp)
|
||||||
|
|
||||||
ver = json_resp.get("ver")
|
ver = json_resp.get("ver")
|
||||||
print(f"[jd]信息编码版本号:{ver}")
|
print(f"版本{ver}")
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
continue
|
continue
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
|
|||||||
@@ -729,7 +729,8 @@ class BackendClient:
|
|||||||
platform_type = self._get_platform_by_store_id(store_id)
|
platform_type = self._get_platform_by_store_id(store_id)
|
||||||
|
|
||||||
if platform_type == "京东":
|
if platform_type == "京东":
|
||||||
self._forward_to_jd(store_id, recv_pin, content)
|
# 🔥 传递msg_type参数,支持图片/视频等类型
|
||||||
|
self._forward_to_jd(store_id, recv_pin, content, msg_type)
|
||||||
elif platform_type == "抖音":
|
elif platform_type == "抖音":
|
||||||
# 传递msg_type参数,支持图片/视频等类型
|
# 传递msg_type参数,支持图片/视频等类型
|
||||||
self._forward_to_douyin(store_id, recv_pin, content, msg_type)
|
self._forward_to_douyin(store_id, recv_pin, content, msg_type)
|
||||||
@@ -761,8 +762,15 @@ class BackendClient:
|
|||||||
print(f"获取平台类型失败: {e}")
|
print(f"获取平台类型失败: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str):
|
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"):
|
||||||
"""转发消息到京东平台"""
|
"""转发消息到京东平台(支持文本/图片/视频)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
store_id: 店铺ID
|
||||||
|
recv_pin: 接收者pin
|
||||||
|
content: 消息内容(文本内容或URL)
|
||||||
|
msg_type: 消息类型(text/image/video)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
|
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
|
||||||
jd_mgr = JDWSManager()
|
jd_mgr = JDWSManager()
|
||||||
@@ -778,49 +786,52 @@ class BackendClient:
|
|||||||
pin_zj = platform_info.get('pin_zj')
|
pin_zj = platform_info.get('pin_zj')
|
||||||
vender_id = platform_info.get('vender_id')
|
vender_id = platform_info.get('vender_id')
|
||||||
loop = platform_info.get('loop')
|
loop = platform_info.get('loop')
|
||||||
|
cookies_str = platform_info.get('cookies_str') # 🔥 获取cookies用于文件上传
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} recv_pin={recv_pin}")
|
f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} has_cookies={bool(cookies_str)} recv_pin={recv_pin} msg_type={msg_type}")
|
||||||
|
|
||||||
if ws and aid and pin_zj and vender_id and loop and content:
|
if ws and aid and pin_zj and vender_id and loop and content:
|
||||||
|
# 🔥 获取 FixJdCookie 实例,使用其 send_message 方法(支持多媒体)
|
||||||
async def _send():
|
async def _send():
|
||||||
import hashlib as _hashlib
|
from Utils.JD.JdUtils import FixJdCookie
|
||||||
import time as _time
|
# 创建临时实例用于发送
|
||||||
import json as _json
|
jd_instance = FixJdCookie()
|
||||||
msg = {
|
# 🔥 设置认证信息(用于图片/视频上传)
|
||||||
"ver": "4.3",
|
jd_instance.cookies_str = cookies_str
|
||||||
"type": "chat_message",
|
jd_instance.current_aid = aid
|
||||||
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
|
jd_instance.current_pin_zj = pin_zj
|
||||||
"to": {"app": "im.customer", "pin": recv_pin},
|
# 调用支持多媒体的 send_message 方法
|
||||||
"id": _hashlib.md5(str(int(_time.time() * 1000)).encode()).hexdigest(),
|
await jd_instance.send_message(
|
||||||
"lang": "zh_CN",
|
ws=ws,
|
||||||
"aid": aid,
|
pin=recv_pin,
|
||||||
"timestamp": int(_time.time() * 1000),
|
aid=aid,
|
||||||
"readFlag": 0,
|
pin_zj=pin_zj,
|
||||||
"body": {
|
vender_id=vender_id,
|
||||||
"content": content,
|
content=content,
|
||||||
"translated": False,
|
msg_type=msg_type
|
||||||
"param": {"cusVenderId": vender_id},
|
)
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await ws.send(_json.dumps(msg))
|
|
||||||
|
|
||||||
import asyncio as _asyncio
|
import asyncio as _asyncio
|
||||||
_future = _asyncio.run_coroutine_threadsafe(_send(), loop)
|
_future = _asyncio.run_coroutine_threadsafe(_send(), loop)
|
||||||
try:
|
try:
|
||||||
_future.result(timeout=2)
|
_future.result(timeout=60) # 图片/视频需要更长时间
|
||||||
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
|
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, type={msg_type}, content_len={len(content)}")
|
||||||
except Exception as fe:
|
except Exception as fe:
|
||||||
print(f"[JD Forward] 转发提交失败: {fe}")
|
print(f"[JD Forward] 转发提交失败: {fe}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print("[JD Forward] 条件不足,未转发:",
|
print("[JD Forward] 条件不足,未转发:",
|
||||||
{
|
{
|
||||||
'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj),
|
'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj),
|
||||||
'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_content': bool(content)
|
'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_cookies': bool(cookies_str),
|
||||||
|
'has_content': bool(content)
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[JD Forward] 转发失败: {e}")
|
print(f"[JD Forward] 转发失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"):
|
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"):
|
||||||
"""转发消息到抖音平台
|
"""转发消息到抖音平台
|
||||||
|
|||||||
Reference in New Issue
Block a user