Files
shuidrop_gui/Utils/QianNiu/QianNiuUtils.py

2322 lines
87 KiB
Python
Raw Normal View History

2025-09-12 20:42:00 +08:00
# -*- coding: utf-8 -*-
# python let's go
# 编辑人:kris思成
# sainiu_local_test.py
import asyncio
import json
import os
import uuid
import glob
import time
import hashlib
import subprocess
import logging
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
import websockets
import aiohttp
import sys
import io
# 设置标准输出编码为UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("sainiu_test.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("SaiNiuTest")
# 赛牛插件配置
QN_WS_URL = "ws://127.0.0.1:3030" # 赛牛插件WebSocket地址
QN_HTTP_API_URL = "http://127.0.0.1:3030/QianNiu/Api" # 赛牛插件HTTP API地址
STORE_ID = "test_store_001" # 测试店铺ID
STORE_NAME = "测试店铺" # 测试店铺名称
USER_NICK = "tb420723827:redboat" # 测试账号
import ctypes
import hashlib
import time
from ctypes import c_int, c_bool, c_char_p
class SaiNiuService:
"""赛牛DLL服务管理类"""
def __init__(self):
self.dll_loaded = False
self.sainiu_api = None
self.port = 3030 # 默认端口
self.sign_key = b'' # 默认签名密钥
self.sha256 = True # 使用SHA256签名
self.python32_path = r"F:\飞书下载地址\shuidrop_gui\Utils\PythonNew32\python32.exe"
self.dll_process = None
def load_dll(self, dll_path='F:\\飞书下载地址\\shuidrop_gui\\Utils\\PythonNew32\\SaiNiuApi.dll'):
"""加载DLL文件 - 使用Python32执行"""
try:
# 切换到DLL目录
dll_dir = os.path.dirname(dll_path)
os.chdir(dll_dir)
print(f"📁 切换到DLL目录: {dll_dir}")
# 创建DLL启动脚本 - 完全按照demo的代码
script_content = '''import ctypes
import hashlib
import time
import json
# 加载DLL文件
sainiu_api = ctypes.CDLL('SaiNiuApi.dll')
# 定义函数参数类型和返回值类型
sainiu_api.Access_ServerStart.argtypes = [
ctypes.c_int,
ctypes.c_bool,
ctypes.c_bool,
ctypes.c_bool,
ctypes.c_bool,
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_bool,
ctypes.c_bool
]
sainiu_api.Access_ServerStart.restype = ctypes.c_char_p
# 调用函数时传入的参数
port = 3030
web_socket = True
http_server = True
remote = False
log = True
ws_max = 0
sign_key = b''
sha256 = True
version_tip = True
# 启动服务器
result = sainiu_api.Access_ServerStart(
port,
web_socket,
http_server,
remote,
log,
ws_max,
sign_key,
sha256,
version_tip
)
# 解码并打印结果
try:
result_str = result.decode('gbk')
print(f"Access_ServerStart 服务器启动: {result_str}")
except UnicodeDecodeError:
print(f"Access_ServerStart 服务器启动: {result}")
# 保持进程运行
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
'''
# 保存脚本
script_path = os.path.join(dll_dir, "dll_loader.py")
with open(script_path, "w", encoding="utf-8") as f:
f.write(script_content)
# 使用Python32启动脚本
self.dll_process = subprocess.Popen(
[self.python32_path, script_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
cwd=dll_dir,
encoding='gbk',
text=True
)
# 等待进程启动并获取结果
try:
stdout, stderr = self.dll_process.communicate(timeout=15)
if stderr:
print(f"❌ DLL启动错误: {stderr}")
return False
print(f"✅ 赛牛服务器启动结果: {stdout}")
# 检查是否启动成功
if "SUCCESS" in stdout or "成功" in stdout or "200" in stdout:
self.dll_loaded = True
print("✅ DLL加载成功")
return True
else:
print("❌ 服务器启动失败")
return False
except subprocess.TimeoutExpired:
# 超时不一定表示失败,可能服务正在运行
print("⏰ DLL启动超时但服务可能已在后台运行")
self.dll_loaded = True
return True
except Exception as e:
print(f"❌ 加载SaiNiu DLL失败: {e}")
return False
def sign_get_sign(self, post, timestamp):
"""计算签名 - 按照官方demo的逻辑修改"""
if not self.sign_key: # 如果签名为空,直接返回空字符串
return ""
# 拼接字符串
data = post + str(timestamp) + self.sign_key.decode('utf-8')
# 将字符串转换为字节集
byte_data = data.encode('utf-8')
# 根据sha256参数选择哈希算法
if self.sha256:
hash_object = hashlib.sha256(byte_data)
else:
hash_object = hashlib.md5(byte_data)
# 获取十六进制表示并转换为小写
return hash_object.hexdigest().lower()
def start_server(self, port=3030, sign_key=b'111111', sha256=True):
"""启动赛牛服务器已在load_dll中完成"""
if not self.dll_loaded:
print("❌ DLL未加载请先调用load_dll()")
return False
return True
def _check_port_listening(self):
"""检查端口是否在监听状态"""
try:
import socket
# 尝试连接端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(1)
result = s.connect_ex(('127.0.0.1', self.port))
if result == 0:
logger.info(f"端口 {self.port} 已在监听")
return True
else:
logger.warning(f"端口 {self.port} 未在监听 (错误码: {result})")
return False
except Exception as e:
logger.error(f"检查端口状态时出错: {e}")
return False
def _wait_for_port(self, timeout=30):
"""等待端口可用"""
start_time = time.time()
while time.time() - start_time < timeout:
if self._check_port_listening():
# 额外等待一下确保服务完全启动
time.sleep(2)
# 再次检查
if self._check_port_listening():
return True
time.sleep(1)
return False
def cleanup_old_process(self):
"""清理旧的进程和端口"""
try:
import os
import psutil
# 1. 终止所有相关Python进程
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if proc.info['name'] == 'python32.exe':
cmdline = proc.info['cmdline']
if any('SaiNiuApi.dll' in cmd for cmd in cmdline):
proc.kill() # 使用kill而不是terminate
logger.info(f"已强制终止旧进程: {proc.info['pid']}")
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# 2. 清理端口占用
for conn in psutil.net_connections():
if conn.laddr.port == self.port:
try:
proc = psutil.Process(conn.pid)
proc.kill()
logger.info(f"已终止占用端口{self.port}的进程: {conn.pid}")
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# 3. 等待进程完全退出
time.sleep(2)
# 4. 清理可能的临时文件
temp_files = [
os.path.join(os.path.dirname(self.dll_path), "dll_loader.py"),
os.path.join(os.path.dirname(self.dll_path), "*.tmp")
]
for pattern in temp_files:
for f in glob.glob(pattern):
try:
os.remove(f)
logger.info(f"已清理临时文件: {f}")
except:
pass
except Exception as e:
logger.error(f"清理旧进程时出错: {e}")
def close(self):
"""关闭DLL服务"""
if self.dll_process:
self.dll_process.terminate()
try:
self.dll_process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.dll_process.kill()
# 简单的内存缓存实现替换Django的cache
class SimpleCache:
def __init__(self):
self._cache = {}
def get(self, key: str) -> Optional[Any]:
data = self._cache.get(key)
if data and data['expire'] > datetime.now():
return data['value']
return None
def set(self, key: str, value: Any, timeout: int):
self._cache[key] = {
'value': value,
'expire': datetime.now() + timedelta(seconds=timeout)
}
# 创建全局缓存实例
cache = SimpleCache()
class MockStore:
"""模拟店铺对象"""
def __init__(self, pk, name, account, password):
self.pk = pk
self.store_name = name
self.store_account = account
self.store_password = password
@dataclass
class MessageTemplate:
"""通用消息结构体 - 严格按照WebSocket文档v2格式"""
# 必填字段
type: str # 消息类型:"message" | "staff_list" | "ping" | "pong" | "transfer" | "connect_success" | "error"
# 条件必填字段(根据消息类型)
content: Optional[str] = None # 消息内容
msg_type: Optional[str] = None # 消息实际类型:"text" | "image" | "video" | "product_card" | "order_card"
pin_image: Optional[str] = None # 用户头像URL
sender: Optional[Dict] = None # 发送者信息必须包含id字段
store_id: Optional[str] = None # 店铺ID
# 可选字段
receiver: Optional[Dict] = None # 接收者信息
data: Optional[Dict] = None # 扩展数据(如客服列表、验证链接等)
uuid: Optional[str] = None # 心跳包UUID
token: Optional[str] = None # 认证令牌(心跳包可能需要)
def __post_init__(self):
"""初始化后处理"""
# 自动设置msg_type仅对message类型
if self.type == "message" and self.msg_type is None and self.content:
self.msg_type = self._detect_msg_type()
def _detect_msg_type(self) -> str:
"""自动检测消息类型"""
if not self.content:
return "text"
content = self.content.lower()
# 图片检测
if any(ext in content for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
return "image"
# 视频检测
if any(ext in content for ext in ['.mp4', '.avi', '.mov', '.wmv', '.flv']):
return "video"
# 订单卡片检测 - 支持多种格式
if ("商品id" in self.content and "订单号:" in self.content) or \
("商品ID" in self.content and "订单号:" in self.content) or \
("咨询订单号:" in self.content and "商品ID" in self.content):
return "order_card"
# 商品卡片检测
if any(keyword in content for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id']):
return "product_card"
# 默认文本消息
return "text"
def to_json(self) -> str:
"""序列化为JSON字符串"""
return json.dumps(self.to_dict(), ensure_ascii=False)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典过滤None值"""
result = {}
for key, value in asdict(self).items():
if value is not None:
result[key] = value
return result
@classmethod
def from_json(cls, json_str: str):
"""从JSON字符串恢复结构体"""
data = json.loads(json_str)
return cls(**data)
@classmethod
def from_dict(cls, data: Dict[str, Any]):
"""从字典创建实例"""
return cls(**data)
@classmethod
def create_text_message(cls, content: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建文本消息"""
return cls(
type="message",
content=content,
msg_type="text",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_image_message(cls, image_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建图片消息"""
return cls(
type="message",
content=image_url,
msg_type="image",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_video_message(cls, video_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建视频消息"""
return cls(
type="message",
content=video_url,
msg_type="video",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_order_card_message(cls, product_id: str, order_number: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建订单卡片消息"""
return cls(
type="message",
content=f"商品id{product_id} 订单号:{order_number}",
msg_type="order_card",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_product_card_message(cls, product_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""创建商品卡片消息"""
return cls(
type="message",
content=product_url,
msg_type="product_card",
pin_image=pin_image,
sender={"id": sender_id},
store_id=store_id
)
@classmethod
def create_staff_list_message(cls, staff_list: list, store_id: str):
"""创建客服列表消息"""
return cls(
type="staff_list",
content="客服列表更新",
data={"staff_list": staff_list},
store_id=store_id
)
@classmethod
def create_ping_message(cls, uuid_str: str, token: str = None):
"""创建心跳检测消息"""
message = cls(
type="ping",
uuid=uuid_str
)
if token:
message.token = token
return message
@classmethod
def create_pong_message(cls, uuid_str: str):
"""创建心跳回复消息"""
return cls(
type="pong",
uuid=uuid_str
)
class AIServiceConnector:
"""AI服务连接器 - 负责与AI后端服务通信已改为单后端连接"""
def __init__(self, backend_host="192.168.5.155", backend_port=8000):
# 旧版后端WS地址移除统一使用单后端连接ws/gui/<exe_token>/
self.websocket = None
self.store_id = None
self.connected = False
self.connect_timeout = 60
self.service_cookie = None
self.message_handlers = {} # 新增:消息处理器字典
self.pending_ai_replies = {} # 新增等待AI回复的Future
self.reconnect_attempts = 0
self.max_reconnect_attempts = 3
self.reconnect_delay = 5 # 重连间隔时间(秒)
async def connect(self, store_id, auth_dict=None):
"""连接后端AI服务已不再直接连接后端统一走单后端连接"""
self.store_id = store_id
self.connected = True
return True
def send_to_backend_single(self, content: str, msg_type: str, sender_pin: str):
"""通过单后端连接发送消息携带store_id"""
try:
from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client()
if not backend:
return False
message = {
'type': 'message',
'content': content,
'msg_type': msg_type,
'sender': {'id': sender_pin},
'store_id': self.store_id
}
backend.send_message(message)
return True
except Exception:
return False
async def start_message_loop(self):
"""启动统一的消息循环处理所有后端消息"""
self._log("🔵 启动统一消息循环...", "INFO")
while True: # 改为无限循环,支持自动重连
try:
if not self.connected or not self.websocket:
self._log("🔌 连接断开,尝试重连...", "WARNING")
if not await self._reconnect():
await asyncio.sleep(self.reconnect_delay)
continue
try:
message = await asyncio.wait_for(self.websocket.recv(), timeout=30.0)
data = json.loads(message)
message_type = data.get("type")
self._log(f"📥 收到消息类型: {message_type}", "DEBUG")
# 分发消息给对应的处理器
await self._dispatch_message(data, message_type)
except asyncio.TimeoutError:
continue
except websockets.exceptions.ConnectionClosed as e:
self._log(f"🔌 WebSocket连接关闭: {e}", "WARNING")
self.connected = False
await self._handle_connection_closed()
continue
except Exception as e:
self._log(f"❌ 消息接收异常: {e}", "ERROR")
await asyncio.sleep(1)
except Exception as e:
self._log(f"❌ 消息循环异常: {e}", "ERROR")
await asyncio.sleep(1)
async def _reconnect(self):
"""重连机制"""
if self.reconnect_attempts >= self.max_reconnect_attempts:
self._log("❌ 重连次数超过限制,停止重连", "ERROR")
return False
try:
self.reconnect_attempts += 1
self._log(f"🔄 尝试第 {self.reconnect_attempts} 次重连...", "WARNING")
# 关闭旧连接
if self.websocket:
await self.websocket.close()
# 重新连接
success = await self.connect(self.store_id)
if success:
self.reconnect_attempts = 0 # 重置重试计数
self._log("✅ 重连成功", "SUCCESS")
return True
else:
self._log(f"❌ 第 {self.reconnect_attempts} 次重连失败", "WARNING")
return False
except Exception as e:
self._log(f"❌ 重连过程中出现异常: {e}", "ERROR")
return False
async def send_message_to_backend(self, message_template):
"""发送消息到后端并获取AI回复"""
# 简化连接检查主要依赖websockets库自身的错误处理
if not hasattr(self, 'websocket') or self.websocket is None:
self._log("❌ AI服务未连接尝试重连...", "WARNING")
if not await self._reconnect():
self._log("❌ 重连失败,无法发送消息", "ERROR")
return None
try:
message_dict = message_template.to_dict()
user_id = message_template.sender.get("id", "") # 使用sender.id作为key
# 详细的调试信息
print("=" * 60)
print("🔍 调试信息 - 准备发送消息到后端:")
print(f"连接状态: {self.connected}")
print(f"WebSocket状态: {self.websocket is not None}")
print(f"消息ID: {message_id}")
print(f"消息内容: {message_dict.get('content', '')[:100]}...")
print("=" * 60)
# 创建Future来等待回复
future = asyncio.Future()
self.pending_ai_replies[user_id] = future
await self.websocket.send(json.dumps(message_dict))
self._log(f"📤 已发送消息到AI服务: {message_template.content[:50]}...", "DEBUG")
# 等待AI回复设置超时
try:
ai_reply = await asyncio.wait_for(future, timeout=15.0)
self._log(f"✅ 成功获取AI回复: {ai_reply[:50]}...", "SUCCESS")
return ai_reply
except asyncio.TimeoutError:
self._log("⏰ 等待AI回复超时15秒", "WARNING")
self.pending_ai_replies.pop(user_id, None)
return None
except ConnectionError as e:
self._log(f"❌ 连接已断开: {e}", "ERROR")
self.pending_ai_replies.pop(user_id, None)
return None
except websockets.exceptions.ConnectionClosed:
self._log("❌ 发送消息时连接已关闭", "ERROR")
self.connected = False
self.pending_ai_replies.pop(user_id, None)
return None
except Exception as e:
self._log(f"❌ 发送消息到AI服务失败: {e}", "ERROR")
self.pending_ai_replies.pop(user_id, None)
return None
async def _dispatch_message(self, data, message_type):
"""分发消息到对应的处理器"""
# 处理customer_message类型客服消息
if message_type == "customer_message":
handler = self.message_handlers.get("customer_message")
if handler and callable(handler):
asyncio.create_task(handler(data))
# 处理message类型AI回复
elif message_type == "message":
# 检查是否有等待AI回复的Future - 使用receiver.id匹配
receiver_info = data.get("receiver", {})
user_id = receiver_info.get("id", "")
if user_id and user_id in self.pending_ai_replies:
future = self.pending_ai_replies.pop(user_id)
if not future.done():
future.set_result(data.get("content", ""))
# 同时调用消息处理器(用于监控)
handler = self.message_handlers.get("message")
if handler and callable(handler):
asyncio.create_task(handler(data))
# 可以添加其他消息类型的处理
else:
self._log(f"📨 未处理的消息类型: {message_type}", "DEBUG")
def register_message_handler(self, message_type, handler):
"""注册消息处理器"""
self.message_handlers[message_type] = handler
self._log(f"✅ 注册消息处理器: {message_type}", "DEBUG")
# async def listen_for_backend_messages(self, message_callback):
# """监听后端发送的消息"""
# self._log(f"🎯 进入后端消息监听方法,连接状态: {self.connected}", "DEBUG")
#
# if not self.connected or not self.websocket:
# self._log("❌ AI服务未连接无法监听消息", "ERROR")
# return
#
# self._log("🔵 开始监听后端消息...", "INFO")
# RECV_TIMEOUT = 15.0
#
# try:
# while self.connected and self.websocket:
# try:
# message = await asyncio.wait_for(
# self.websocket.recv(),
# timeout=RECV_TIMEOUT
# )
#
# data = json.loads(message)
# self._log(f"📥 收到消息类型: {data.get('type')}", "DEBUG")
#
# if data.get("type") == "customer_message":
# self._log("✅ 处理客服消息", "INFO")
# if callable(message_callback):
# # 使用create_task避免阻塞主循环
# asyncio.create_task(message_callback(data))
#
# except asyncio.TimeoutError:
# # 超时是正常的,继续监听
# continue
# except websockets.exceptions.ConnectionClosed:
# self._log("🔌 连接已关闭", "WARNING")
# self.connected = False
# break
# except Exception as e:
# self._log(f"❌ 消息处理异常: {e}", "ERROR")
# await asyncio.sleep(1) # 避免频繁报错
#
# except Exception as e:
# self._log(f"❌ 监听循环异常: {e}", "ERROR")
async def _parse_ai_response(self, response_data):
"""解析后端返回的AI回复"""
try:
# 处理连接成功消息
if response_data.get("type") == "connect_success":
content = response_data.get("content", "")
if content:
self.service_cookie = {
'cookie': content,
'status': True
}
self._log("✅ 从连接成功消息中提取到认证信息", "SUCCESS")
return content
# 检查是否为AI回复消息
if response_data.get("type") == "message":
content = response_data.get("content", "")
if content:
self._log(f"✅ 成功解析AI回复: {content}", "SUCCESS")
return content
# 检查其他可能的格式
if "content" in response_data:
content = response_data.get("content")
if content:
self._log(f"✅ 从content字段获取到回复: {content}", "SUCCESS")
return content
self._log(f"⚠️ 无法识别的回复格式: {response_data}", "WARNING")
return None
except Exception as e:
self._log(f"❌ 解析AI回复失败: {e}", "ERROR")
return None
async def close(self):
"""关闭连接"""
if self.websocket:
await self.websocket.close()
self.connected = False
self._log("🔌 已关闭AI服务连接", "INFO")
def _log(self, message, level="INFO"):
"""日志记录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 根据日志级别添加颜色
if level == "ERROR":
print(f"\033[91m[{timestamp}] [{level}] {message}\033[0m") # 红色
elif level == "WARNING":
print(f"\033[93m[{timestamp}] [{level}] {message}\033[0m") # 黄色
elif level == "SUCCESS":
print(f"\033[92m[{timestamp}] [{level}] {message}\033[0m") # 绿色
elif level == "DEBUG":
print(f"\033[96m[{timestamp}] [{level}] {message}\033[0m") # 蓝色
else:
print(f"[{timestamp}] [{level}] {message}")
def get_service_cookie(self):
"""获取服务认证信息"""
return self.service_cookie
class AIServiceIntegration:
"""AI服务集成类 - 负责处理AI回复相关功能"""
def __init__(self, store_id="4c4025e3-8702-42fc-bdc2-671e335c0ff7"):
self.store_id = store_id
self.ai_service = AIServiceConnector()
self.loop = None
self._ensure_event_loop()
self.retry_count = 0
self.max_retries = 3
def _ensure_event_loop(self):
"""确保事件循环存在"""
try:
self.loop = asyncio.get_event_loop()
except RuntimeError:
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
async def initialize_ai_service(self):
"""初始化AI服务"""
try:
self._log("正在初始化AI服务...", "INFO")
success = await self.ai_service.connect(self.store_id)
if success:
self._log("AI服务初始化成功", "SUCCESS")
self.retry_count = 0
# 启动统一的消息循环
asyncio.create_task(self.ai_service.start_message_loop())
self._log("✅ 消息循环已启动", "SUCCESS")
return True
else:
self._log("AI服务初始化失败", "ERROR")
return False
except Exception as e:
self._log(f"AI服务初始化异常: {e}", "ERROR")
return False
def register_message_handlers(self, handlers):
"""注册消息处理器"""
for message_type, handler in handlers.items():
self.ai_service.register_message_handler(message_type, handler)
async def get_ai_reply(self, message_content, sender_nick, avatar_url=""):
"""获取AI回复"""
try:
# 确保连接正常
if not await self.ensure_connection():
return "您好,感谢您的咨询,我们会尽快回复您!"
# 创建消息模板对象
message_template = MessageTemplate(
type="message",
content=message_content,
sender={
"id": sender_nick,
"name": f"淘宝用户_{sender_nick}",
"is_customer": True
},
data={
"msg_type": "text",
"pin_image": avatar_url
},
store_id=self.store_id
)
control_type = str(message_content)
if control_type.__contains__("http") and (control_type.__contains__(".jpg") or control_type.__contains__(".png")):
message_template = MessageTemplate(
type="message",
content=message_content,
msg_type="image",
pin_image=avatar_url,
sender={"id": sender_nick},
store_id=self.store_id
)
print(f"最终发送给后端的数据📕:{message_template.to_dict()}")
# 获取AI回复
ai_reply = await self.ai_service.send_message_to_backend(
message_template=message_template
)
if ai_reply:
self._log(f"成功获取AI回复: {ai_reply[:50]}...", "SUCCESS")
else:
self._log("未获取到AI回复", "WARNING")
# 未获取到正常回复 增加重试计数
self.retry_count += 1
if self.retry_count >= self.max_retries:
self._log(f"重试次数超过限制,停止重试", "ERROR")
return ai_reply
except Exception as e:
logger.error(f"获取AI回复失败: {e}")
return "您好,感谢您的咨询,我们会尽快回复您!"
# async def start_backend_listening(self, message_callback):
# """启动后端消息监听"""
# if not await self.ensure_connection():
# self._log("❌ AI服务连接失败无法启动后端监听", "ERROR")
# return False
#
# try:
# # 直接调用AIServiceConnector的监听方法
# asyncio.create_task(
# self.ai_service.listen_for_backend_messages(message_callback)
# )
# self._log("✅ 后端消息监听已启动", "SUCCESS")
# return True
# except Exception as e:
# self._log(f"❌ 启动后端监听失败: {e}", "ERROR")
# return False
def _log(self, message, level="INFO"):
"""日志记录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 根据日志级别添加颜色
if level == "ERROR":
print(f"\033[91m[{timestamp}] [{level}] {message}\033[0m") # 红色
elif level == "WARNING":
print(f"\033[93m[{timestamp}] [{level}] {message}\033[0m") # 黄色
elif level == "SUCCESS":
print(f"\033[92m[{timestamp}] [{level}] {message}\033[0m") # 绿色
elif level == "DEBUG":
print(f"\033[96m[{timestamp}] [{level}] {message}\033[0m") # 蓝色
else:
print(f"[{timestamp}] [{level}] {message}")
async def check_connection(self):
"""检查AI服务连接状态"""
if not self.ai_service.connected:
self._log("AI服务连接已断开尝试重新连接...", "WARNING")
return await self.initialize_ai_service()
return True
async def ensure_connection(self):
"""确保AI服务连接正常"""
try:
# 如果标记为未连接,直接尝试重连
if not self.ai_service.connected:
self._log("AI服务连接已断开尝试重新连接...", "WARNING")
success = await self.ai_service.connect(self.store_id)
return success
# 简单检查如果websocket对象存在且不是None就认为连接正常
# 实际的连接状态会在发送消息时由websockets库自动处理
if (hasattr(self.ai_service, 'websocket') and
self.ai_service.websocket is not None):
return True
else:
self._log("WebSocket连接不存在", "WARNING")
self.ai_service.connected = False
return await self.ensure_connection()
except Exception as e:
self._log(f"检查连接状态异常: {e}", "ERROR")
return False
async def close(self):
"""关闭AI服务"""
try:
self._log("正在关闭AI服务...", "INFO")
if self.ai_service:
await self.ai_service.close()
self._log("AI服务已关闭", "SUCCESS")
except Exception as e:
self._log(f"关闭AI服务时发生错误: {e}", "ERROR")
class QianNiuClient:
"""千牛客户端类 - 负责与赛牛插件通信"""
def __init__(self, user_nick=None):
self.user_nick: str = user_nick
self.qn_ws = None
self.pending: Dict[str, asyncio.Future] = {}
self.is_connected = False
self.is_authenticated = False # 改为连接成功即认证
self.message_handler = None
self._http_session = None # 添加会话
# 新增:正式账号认证信息
self.access_type = 1 # 企业版
self.access_id = "maguabishop" # 你的AccessId
self.access_key = "bWFndWFfYmlzaG9w" # 你的AccessKey
self.auth_token = None # 认证成功后获取的token
self.udid = None # 设备唯一标识
# 赛牛服务
self.sainiu_service = SaiNiuService()
async def __aenter__(self):
"""异步上下文管理器入口"""
self._http_session = aiohttp.ClientSession()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
if self._http_session:
await self._http_session.close()
# 在 QianNiuClient 类中添加HTTP API调用方法 (新增)
async def _http_api_call(self, post_name: str, data_obj: Dict[str, Any]) -> Dict[str, Any]:
"""按官方文档以 form-data 方式调用 QianNiu/Api 接口"""
form = aiohttp.FormData()
form.add_field("post", post_name)
form.add_field("data", json.dumps(data_obj))
logger.debug(f"[TB-DIAG] HTTP CALL -> {post_name} url={QN_HTTP_API_URL} data={data_obj}")
try:
async with aiohttp.ClientSession() as session:
async with session.post(QN_HTTP_API_URL, data=form, timeout=10) as resp:
text = await resp.text()
logger.debug(f"[TB-DIAG] HTTP RESP <- {post_name} status={resp.status} body={text[:300]}")
if resp.status != 200:
logger.error(f"[QianNiuClient] HTTP接口 {post_name} 调用失败: status={resp.status}")
return {}
try:
return json.loads(text)
except Exception:
logger.error(f"[TB-DIAG] HTTP RESP JSON decode error on {post_name}")
return {}
except Exception as e:
logger.error(f"[QianNiuClient] HTTP接口 {post_name} 调用异常: {e}")
return {}
async def fetch_buyer_avatar_by_http(self, buyer_nick: str) -> str:
"""通过 HTTP GetUserIcon 获取买家头像"""
if not buyer_nick:
return ""
cache_key = f"avatar:taobao:nick:{buyer_nick}"
cached = cache.get(cache_key)
if cached:
logger.info(f"[TB-头像调试] 💾 缓存命中: {cached}")
return cached
# 使用正确的 GetUserIcon API
logger.info(f"[TB-头像调试] 📡 调用GetUserIcon API获取头像 - 买家昵称: {buyer_nick}")
resp = await self._http_api_call("GetUserIcon", {
"userNick": self.user_nick,
"buyerNick": buyer_nick,
"siteid": "cntaobao"
})
if not resp:
logger.warning(f"[TB-头像调试] ❌ HTTP GetUserIcon 调用失败")
return ""
# 完整记录API返回数据
logger.info(f"[TB-头像调试] 📥 GetUserIcon 完整返回数据: {json.dumps(resp, ensure_ascii=False, indent=2)}")
# 解析返回数据获取头像
try:
if resp.get("code") != 200:
logger.error(f"[TB-头像调试] ❌ API返回错误: code={resp.get('code')}, msg={resp.get('msg')}")
return ""
data_block = resp.get("data") or {}
logger.info(f"[TB-头像调试] 🔍 data块内容: {json.dumps(data_block, ensure_ascii=False, indent=2)}")
# 根据API文档返回格式为: {"cntaobaotb266770389534":{"iconUrl":"https://..."}}
# 构建key: siteid + buyerNick
avatar_key = f"cntaobao{buyer_nick}"
avatar_data = data_block.get(avatar_key, {})
avatar_url = avatar_data.get("iconUrl", "")
if avatar_url:
logger.info(f"[TB-头像调试] ✅ 成功获取头像: {avatar_url}")
# 处理相对协议的URL
if avatar_url.startswith("//"):
avatar_url = "https:" + avatar_url
# 缓存头像URL24小时
cache.set(cache_key, avatar_url, 60 * 60 * 24)
return avatar_url
else:
logger.warning(f"[TB-头像调试] ❌ 未找到头像信息key={avatar_key}")
return ""
except Exception as e:
logger.error(f"[TB-头像调试] GetUserIcon 解析错误: {e}")
return ""
# 新增连接的初始化
async def _request_authorization(self):
"""请求授权 - 按照官方demo修改"""
trace_id = "Access_Init" # 使用固定的traceId
try:
# 获取当前的13位时间戳毫秒级
current_time = int(time.time() * 1000)
# 计算JSON数据的签名参数post参数为"Init"
json_sign = self.sainiu_service.sign_get_sign("Init", current_time)
auth_msg = {
"type": "Invoke_SaiNiu",
"traceId": trace_id,
"codeType": "Access",
"post": "Init",
"data": {
"AccessType": 1,
"AccessId": "maguabishop",
"AccessKey": "bWFndWFfYmlzaG9w",
"ForceLogin": True
},
"time": current_time,
"sign": json_sign
}
await self.qn_ws.send(json.dumps(auth_msg))
logger.info("已发送授权请求")
print(f"Sent message: {auth_msg}")
# 不再等待响应,让消息循环处理响应
return True
except Exception as e:
logger.error(f"授权请求异常: {e}")
return False
async def _system_initialization(self):
"""系统初始化 - 按照官方demo修改"""
trace_id = "0470bf9489729b2e8a2126a04ab3e272" # 使用固定的traceId
try:
# 获取当前的13位时间戳毫秒级
current_time = int(time.time() * 1000)
# 计算新的JSON数据的签名参数post参数为"SysInit"
sys_init_sign = self.sainiu_service.sign_get_sign("SysInit", current_time)
init_msg = {
"type": "Invoke_SaiNiu",
"traceId": trace_id,
"codeType": "Access",
"post": "SysInit",
"data": {
"number": 20,
"port": 2030,
"token": True,
"connect": True,
"disconnect": True,
"newMessage": True,
"groupMessage": True,
"notice": True,
"event": True,
"assistant": True
},
"time": current_time,
"sign": sys_init_sign
}
await self.qn_ws.send(json.dumps(init_msg))
logger.info("已发送系统初始化请求")
print(f"Sent message: {init_msg}")
# 不再等待响应,让消息循环处理
return True
except Exception as e:
logger.error(f"系统初始化异常: {e}")
return False
# async def connect(self):
# """连接到赛牛插件 - 修改为不发送初始化消息"""
# logger.info(f"尝试连接到赛牛插件: {QN_WS_URL}")
# print("=== 断点1: 开始连接赛牛插件 ===")
#
# try:
# # 连接WebSocket
# self.qn_ws = await websockets.connect(QN_WS_URL, ping_interval=20, ping_timeout=10)
# self.is_connected = True
# logger.info("成功连接到赛牛插件")
# print("=== 断点2: 成功连接到赛牛插件 ===")
#
# # 正式账号需要执行授权流程
# print("=== 开始正式账号授权流程 ===")
#
# # 1. 请求授权
# auth_success = await self._request_authorization()
# if not auth_success:
# logger.error("授权请求失败")
# return False
#
# # 2. 系统初始化
# init_success = await self._system_initialization()
# if not init_success:
# logger.error("系统初始化失败")
# return False
#
#
# # 测试账号不需要初始化,连接成功即认证成功
# self.is_authenticated = True
# logger.info("✅ 正式账号认证和初始化成功")
#
# return True
#
# except Exception as e:
# logger.error(f"连接赛牛插件失败: {e}")
# print(f"=== 断点3: 连接失败 - {e} ===")
# self.is_connected = False
# return False
async def connect(self):
"""连接到赛牛插件 - 完整流程"""
logger.info("开始完整的赛牛连接流程")
try:
# 1. 加载并启动DLL服务
if not await self._start_sainiu_service():
logger.error("启动赛牛DLL服务失败")
return False
# 2. 连接到WebSocket
if not await self._connect_websocket():
logger.error("连接WebSocket失败")
return False
# 3. 执行授权流程
if not await self._request_authorization():
logger.error("授权请求失败")
return False
# 4. 系统初始化
if not await self._system_initialization():
logger.error("系统初始化失败")
return False
self.is_authenticated = True
logger.info("✅ 完整的赛牛连接流程成功")
return True
except Exception as e:
logger.error(f"连接赛牛插件失败: {e}")
self.is_connected = False
return False
async def _start_sainiu_service(self):
"""启动赛牛DLL服务"""
try:
# 加载DLL
if not self.sainiu_service.load_dll('F:\\飞书下载地址\\shuidrop_gui\\Utils\\PythonNew32\\SaiNiuApi.dll'):
return False
success = True
if success:
logger.info("✅ 赛牛DLL服务启动成功")
# 等待服务完全启动
await asyncio.sleep(2)
return True
else:
logger.error("❌ 赛牛DLL服务启动失败")
return False
except Exception as e:
logger.error(f"启动赛牛服务异常: {e}")
return False
async def _connect_websocket(self):
"""连接到WebSocket服务器"""
max_retries = 5
retry_delay = 3
for attempt in range(max_retries):
try:
# 获取当前的13位时间戳毫秒级
current_time = int(time.time() * 1000)
# 计算签名
ws_sign = self.sainiu_service.sign_get_sign("ws", current_time)
# 构建URI
if ws_sign: # 如果有签名
uri = f"ws://127.0.0.1:{self.sainiu_service.port}?time={current_time}&sign={ws_sign}"
else: # 如果没有签名
uri = f"ws://127.0.0.1:{self.sainiu_service.port}"
print(f"连接URI: {uri}")
# 连接WebSocket
self.qn_ws = await websockets.connect(uri)
self.is_connected = True
print("✅ WebSocket连接成功")
# 连接成功后立即执行授权和初始化(只执行一次)
if not self.is_authenticated:
# 1. 发送授权请求
current_time = int(time.time() * 1000)
json_sign = self.sainiu_service.sign_get_sign("Init", current_time)
auth_msg = {
"type": "Invoke_SaiNiu",
"traceId": "Access_Init",
"codeType": "Access",
"post": "Init",
"data": {
"AccessType": 1,
"AccessId": self.access_id,
"AccessKey": self.access_key,
"ForceLogin": True
},
"time": current_time,
"sign": json_sign
}
await self.qn_ws.send(json.dumps(auth_msg))
print(f"已发送授权请求: {auth_msg}")
# 2. 等待授权响应
response = await self.qn_ws.recv()
response_data = json.loads(response)
print(f"收到授权响应: {response}")
if response_data.get("traceId") == "Access_Init":
# 解析授权响应
return_data = response_data.get("returnData", {})
code = return_data.get("code", 0)
if code == 200:
self.auth_token = return_data.get("token")
self.udid = return_data.get("udid")
logger.info(f"✅ 授权成功 - token: {self.auth_token}")
# 3. 发送系统初始化请求
current_time = int(time.time() * 1000)
sys_init_sign = self.sainiu_service.sign_get_sign("SysInit", current_time)
init_msg = {
"type": "Invoke_SaiNiu",
"traceId": "0470bf9489729b2e8a2126a04ab3e272",
"codeType": "Access",
"post": "SysInit",
"data": {
"number": 20,
"port": 3030,
"token": True,
"connect": True,
"disconnect": True,
"newMessage": True,
"groupMessage": True,
"notice": True,
"event": True,
"assistant": True
},
"time": current_time,
"sign": sys_init_sign
}
await self.qn_ws.send(json.dumps(init_msg))
print(f"已发送系统初始化请求: {init_msg}")
# 4. 等待初始化响应
response = await self.qn_ws.recv()
response_data = json.loads(response)
print(f"收到初始化响应: {response}")
return_data = response_data.get("returnData", {})
code = return_data.get("code", 0)
if code == 200:
self.is_authenticated = True
print("✅ 授权和初始化成功")
return True
else:
error_msg = return_data.get("msg", "未知错误")
print(f"❌ 系统初始化失败: {error_msg}")
return False
else:
error_msg = return_data.get("msg", "未知错误")
print(f"❌ 授权失败: {error_msg}")
return False
return True
except Exception as e:
print(f"连接失败 (尝试 {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
await asyncio.sleep(retry_delay)
continue
return False
return False
async def _check_port_available(self):
"""检查端口是否可用"""
try:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
result = s.connect_ex(('127.0.0.1', self.sainiu_service.port))
return result == 0
except:
return False
async def _sys_init(self):
"""系统初始化"""
logger.info("开始赛牛插件初始化")
print("=== 断点4: 开始初始化赛牛插件 ===")
# 发送初始化消息
init_msg = {
"type": "Invoke_SaiNiu",
"traceId": str(uuid.uuid4()),
"codeType": "Access",
"post": "Init",
"data": {
"userNick": self.user_nick
}
}
await self.qn_ws.send(json.dumps(init_msg))
logger.info("已发送初始化消息")
print("=== 断点5: 初始化消息已发送 ===")
print(f"初始化消息内容: {json.dumps(init_msg, indent=2, ensure_ascii=False)}")
async def start_listening(self, message_handler):
"""开始监听消息"""
self.message_handler = message_handler
# 启动消息监听和心跳任务
listen_task = asyncio.create_task(self._message_loop())
heartbeat_task = asyncio.create_task(self._heartbeat_loop())
logger.info("开始监听千牛消息...")
print("=== 断点6: 开始监听千牛消息 ===")
return listen_task, heartbeat_task
async def _heartbeat_loop(self):
"""心跳循环"""
print("=== 断点7: 心跳循环开始 ===")
while self.is_connected:
try:
# 发送心跳
heartbeat_msg = {"type": "ping"}
await self.qn_ws.send(json.dumps(heartbeat_msg))
logger.debug("发送心跳")
print("=== 断点8: 心跳已发送 ===")
await asyncio.sleep(20)
except Exception as e:
logger.error(f"心跳发送失败: {e}")
self.is_connected = False
break
async def _message_loop(self):
"""消息监听循环"""
print("=== 断点9: 消息监听循环开始 ===")
try:
# # 确保正确启动后端监听
# if (self.message_handler and
# hasattr(self.message_handler, 'ai_service') and
# self.message_handler.ai_service and
# hasattr(self.message_handler.ai_service.ai_service, 'listen_for_backend_messages')):
#
# print("🚀 启动后端消息监听任务", "DEBUG")
# # 创建监听任务
# asyncio.create_task(
# self.message_handler.ai_service.ai_service.listen_for_backend_messages(
# self.message_handler.handle_customer_message
# )
# )
# else:
# print("⚠️ 无法启动后端监听:缺少必要的属性或方法", "WARNING")
async for message in self.qn_ws:
print("=== 断点10: 收到原始消息 ===")
await self._handle_message(message)
except Exception as e:
logger.error(f"消息监听异常: {e}")
self.is_connected = False
async def _handle_message(self, raw_message):
"""处理接收到的消息"""
print("=== 断点11: 开始处理消息 ===")
try:
data = json.loads(raw_message)
msg_type = data.get("type")
logger.info(f"收到消息类型: {msg_type}")
print(f"=== 断点12: 解析消息类型 - {msg_type} ===")
print(f"收到的全消息体结构为: {data}")
# 处理心跳响应
if msg_type == "pong":
logger.debug("收到pong响应")
print("=== 断点13: 收到心跳响应(pong) ===")
return
# 处理连接成功消息 - 新增这个处理
if msg_type == "FUNCTION_CONNECTED":
print("=== 函数连接已建立 ===")
print(f"连接详情: {json.dumps(data, indent=2, ensure_ascii=False)}")
# 检查是否有认证信息
if hasattr(self, 'auth_token') and self.auth_token:
self.is_authenticated = True
logger.info("✅ 检测到认证token设置认证状态为 True")
else:
logger.warning("⚠️ 未检测到认证token认证状态保持为 False")
return
# 处理API响应
elif msg_type in ["Invoke_SaiNiu", "Invoke_QianNiu"]:
code_type = data.get("codeType")
trace_id = data.get("traceId")
print(f"=== API响应: type={msg_type}, codeType={code_type} ===")
print(f"响应内容: {json.dumps(data, indent=2, ensure_ascii=False)}")
# 处理pending的Future
if trace_id and trace_id in self.pending:
future = self.pending[trace_id]
if not future.done():
future.set_result(data)
return
# 处理买家消息
elif msg_type == "CHAT_DIALOGUE_MSG":
msg_data = data.get("data", {})
code_type = msg_data.get("codeType")
if code_type == "CHAT_RECEIVE_MSG":
message_content = msg_data.get("message", "")
if msg_data.get("data", {}) != {}:
try:
# krishao
store_msg_id_list = msg_data.get("data", {}).get("E3_keyValueDescArray", [])
if store_msg_id_list:
store_msg_id = store_msg_id_list[0].get("desc", "")
if store_msg_id:
dingdan_id = "738740221403"
logger.info("👌👌 👌👌👌👌👌👌👌👌👌👌👌👌👌👌👌👌👌 👌👌")
message_content = f"订单号: {store_msg_id} 商品ID: {dingdan_id}"
else:
pass
except Exception as e:
message_content = f"有需求要询问订单 优先回复"
sender_nick = msg_data.get("senderNick", "")
#### 核心处理逻辑!!!!
logger.info(f"收到买家消息: {sender_nick} -> {message_content}")
print(f"=== 断点14: 收到买家消息 - {sender_nick}: {message_content} ===")
# 调用消息处理回调
if self.message_handler:
await self.message_handler({
"type": "message",
"data": {
"msg_type": "text",
"content": message_content,
"pin": sender_nick,
"uid": str(uuid.uuid4())
},
"store_id": STORE_ID
})
return
# 记录未处理的消息类型
print(f"=== 未处理的消息类型: {msg_type} ===")
print(f"消息内容: {json.dumps(data, indent=2, ensure_ascii=False)}")
except json.JSONDecodeError:
logger.error(f"消息JSON解析失败: {raw_message}")
print("=== 断点16: JSON解析失败 ===")
except Exception as e:
logger.error(f"处理消息异常: {e}")
print(f"=== 断点17: 处理消息异常 - {e} ===")
async def ensure_connected(self):
"""确保连接正常"""
if not self.is_connected or not self.qn_ws:
print("=== 连接已断开,尝试重连 ===")
return await self.connect()
return True
async def send_message(self, to_nick: str, message: str) -> bool:
"""发送消息给买家"""
logger.info(f"准备发送消息 -> {to_nick}: {message}")
print(f"=== 断点18: 准备发送消息给 {to_nick} ===")
print(f"消息内容: {message}")
# 确保连接正常
if not self.is_connected or not self.qn_ws:
print("=== 连接已断开,尝试重连 ===")
if not await self.connect():
return False
# 修改认证检查逻辑
if not self.is_authenticated:
logger.error("认证未完成,无法发送消息")
print("=== 认证状态检查失败 ===")
# 添加调试信息
print(f"当前认证状态: {self.is_authenticated}")
print(f"是否有auth_token: {hasattr(self, 'auth_token')}")
if hasattr(self, 'auth_token'):
print(f"auth_token值: {self.auth_token}")
return False
try:
trace_id = str(uuid.uuid4())
future = asyncio.Future()
self.pending[trace_id] = future
# 构建发送消息请求
send_msg = {
"type": "Invoke_QianNiu",
"traceId": trace_id,
"codeType": "Function",
"post": "SendMessages",
"data": {
"userNick": self.user_nick,
"buyerNick": to_nick,
"text": message,
"siteid": "cntaobao",
"waitingTime": 5000
}
}
# 如果有认证token添加到消息中
if self.auth_token:
send_msg["token"] = self.auth_token
print(f"=== 添加认证token到消息中: {self.auth_token}")
await self.qn_ws.send(json.dumps(send_msg))
logger.info("已发送消息到赛牛插件")
print("=== 断点19: 消息已发送到赛牛插件 ===")
print(f"发送的消息内容: {json.dumps(send_msg, indent=2, ensure_ascii=False)}")
# 等待响应
try:
response = await asyncio.wait_for(future, timeout=5.0)
return_data = response.get("returnData") or response.get("data", {})
code = return_data.get("code", 0)
if code in [200, 0, 403]: # 403也视为成功测试账号特殊处理
logger.info("消息发送成功")
print("=== 断点20: 消息发送成功 ===")
return True
else:
error_msg = return_data.get("msg", "未知错误")
logger.warning(f"消息发送返回非成功码: {code}, 错误信息: {error_msg}")
print(f"=== 断点21: 消息发送失败 - 错误码: {code}, 错误信息: {error_msg} ===")
return False
except asyncio.TimeoutError:
logger.warning("消息发送超时,但可能已成功")
print("=== 断点22: 消息发送超时 ===")
return True
except Exception as e:
logger.error(f"发送消息异常: {e}")
print(f"=== 断点23: 发送消息异常 - {e} ===")
return False
async def close(self):
"""关闭连接"""
self.is_connected = False
self.is_authenticated = False
if self.qn_ws:
await self.qn_ws.close()
logger.info("千牛客户端已关闭")
class TestMessageHandler:
"""测试消息处理器"""
def __init__(self, qn_client):
self.qn_client = qn_client
self.received_messages = []
self.ai_service = AIServiceIntegration()
self.backend_listening_task = None # 新增:保存监听任务
async def initialize(self):
"""初始化消息处理器和AI服务"""
try:
# 初始化AI服务
success = await self.ai_service.initialize_ai_service()
if not success:
logger.error("AI服务初始化失败")
return False
# 注册消息处理器
self.ai_service.register_message_handlers({
"customer_message": self.handle_customer_message,
"message": self.handle_ai_message # 新增处理AI消息
})
return True
except Exception as e:
logger.error(f"消息处理器初始化失败: {e}")
return False
async def start_backend_listening(self):
"""启动后端消息监听"""
try:
# 简化检查逻辑
if not hasattr(self.ai_service, 'ai_service'):
self._log("⚠️ AI服务未正确初始化", "WARNING")
return False
# 直接启动监听
self.backend_listening_task = asyncio.create_task(
self.ai_service.ai_service.listen_for_backend_messages(
self.handle_customer_message
)
)
self._log("✅ 后端消息监听已启动", "SUCCESS")
return True
except Exception as e:
self._log(f"❌ 启动后端监听失败: {e}", "ERROR")
return False
async def handle_ai_message(self, message_data):
"""处理AI回复消息用于监控、日志记录等目的"""
try:
content = message_data.get("content", "")
sender_info = message_data.get("sender", {})
receiver_info = message_data.get("receiver", {})
sender_name = sender_info.get("name", "未知发送者")
receiver_name = receiver_info.get("name", "未知接收者")
# 记录AI回复信息
self._log(f"🤖 AI回复 [{sender_name}{receiver_name}]: {content[:100]}...", "DEBUG")
# 这里可以添加额外的监控和处理逻辑
if len(content) < 5:
self._log("⚠️ AI回复过短可能需要优化", "WARNING")
return True
except Exception as e:
self._log(f"❌ 处理AI消息异常: {e}", "ERROR")
return False
async def handle_message(self, message):
"""处理接收到的买家消息"""
try:
self.received_messages.append(message)
# 新增:打印完整的接收消息体
print("=" * 60)
print("📨 收到买家完整消息体:")
print(json.dumps(message, indent=2, ensure_ascii=False))
print("=" * 60)
msg_data = message.get("data", {})
# dingdan_id = "738740221403"
# store_msg_data = msg_data.get("data", {})
# store_msg_data_detail = store_msg_data.get("E3_keyValueDescArray", [])
# if len(store_msg_data_detail) > 0:
# store_msg_id = store_msg_data_detail[0].get("desc", "")
# else:
# store_msg_id = ""
content = msg_data.get("content", "")
sender_nick = msg_data.get("pin", "")
if not content or not sender_nick:
self._log("消息内容或发送者昵称为空", "WARNING")
return
self._log(f"处理买家消息: {sender_nick} -> {content}", "INFO")
# # 获取买家头像
# avatar_url = ""
# try:
# avatar_url = await self.qn_client.fetch_buyer_avatar_by_http(sender_nick)
# if avatar_url:
# self._log(f"成功获取头像!!!yes: {sender_nick}", "SUCCESS")
# else:
# self._log(f"未获取到头像nononno: {sender_nick}", "WARNING")
# except Exception as e:
# self._log(f"获取头像异常aaaa: {e}", "ERROR")
# 获取AI回复带重试机制
max_retries = 2 # 减少重试次数,避免长时间等待
for retry in range(max_retries):
try:
ai_reply = await self.ai_service.get_ai_reply(
message_content=content,
sender_nick=sender_nick,
# avatar_url=avatar_url # 新增: 传递 头像url
)
if ai_reply:
self._log(f"AI回复: {ai_reply}", "INFO")
success = await self.qn_client.send_message(sender_nick, ai_reply)
if success:
self._log("AI回复发送成功", "SUCCESS")
return
else:
self._log("AI回复发送失败", "ERROR")
if retry < max_retries - 1:
await asyncio.sleep(1)
continue
else:
self._log("未获取到AI回复", "WARNING")
if retry < max_retries - 1:
await asyncio.sleep(1)
continue
except Exception as e:
self._log(f"处理消息异常 (尝试 {retry + 1}/{max_retries}): {e}", "ERROR")
if retry < max_retries - 1:
await asyncio.sleep(1)
continue
# 如果所有重试都失败,发送默认回复
default_reply = "您好,感谢您的咨询,我们会尽快回复您!"
await self.qn_client.send_message(sender_nick, default_reply)
self._log("已发送默认回复", "WARNING")
except Exception as e:
self._log(f"处理消息异常: {e}", "ERROR")
async def handle_customer_message(self, message_data):
"""处理来自后端的客服消息(必须保留)"""
try:
content = message_data.get("content", "")
if not content:
self._log("❌ 客服消息内容为空", "WARNING")
return False
# 优先从receiver获取如果没有再从data获取
receiver_info = message_data.get("receiver", {})
receiver_id = receiver_info.get("id", "") or message_data.get("data", {}).get("pin", "")
if not receiver_id:
self._log("❌ 无法确定消息接收者", "WARNING")
return False
self._log(f"📤 发送客服消息给 {receiver_id}: {content[:50]}...", "INFO")
# 发送客服消息给买家
success = await self.qn_client.send_message(receiver_id, content)
if success:
self._log(f"✅ 客服消息发送成功", "SUCCESS")
else:
self._log(f"❌ 客服消息发送失败", "ERROR")
return success
except Exception as e:
self._log(f"❌ 处理客服消息异常: {e}", "ERROR")
return False
async def close(self):
"""关闭消息处理器"""
if self.ai_service:
await self.ai_service.close()
def _log(self, message, level="INFO"):
"""日志记录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 根据日志级别添加颜色
if level == "ERROR":
print(f"\033[91m[{timestamp}] [{level}] {message}\033[0m") # 红色
elif level == "WARNING":
print(f"\033[93m[{timestamp}] [{level}] {message}\033[0m") # 黄色
elif level == "SUCCESS":
print(f"\033[92m[{timestamp}] [{level}] {message}\033[0m") # 绿色
elif level == "DEBUG":
print(f"\033[96m[{timestamp}] [{level}] {message}\033[0m") # 蓝色
else:
print(f"[{timestamp}] [{level}] {message}")
class QianNiuListenerForGUI:
"""用于GUI集成的千牛监听包装器类"""
def __init__(self, log_callback=None):
"""初始化千牛监听包装器"""
self.qn_client = None
self.message_handler = None
self.running = False
self.stop_event = None
self.log_callback = log_callback
self.log_signal = None # 添加日志信号属性
self.tasks = [] # 新增:任务列表
self.user_nick = None
def _log(self, message, log_type="INFO"):
"""内部日志方法"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{log_type}] {message}"
if hasattr(self, 'log_signal') and self.log_signal:
self.log_signal.emit(message, log_type)
elif self.log_callback:
self.log_callback(message, log_type)
else:
color_map = {
"ERROR": "\033[91m",
"WARNING": "\033[93m",
"SUCCESS": "\033[92m",
"DEBUG": "\033[96m",
}
color = color_map.get(log_type, "")
print(f"{color}[{timestamp}] [{log_type}] {message}\033[0m")
async def _get_first_user(self):
"""获取第一个可用的账号"""
try:
self._log("🔵 获取所有连接的账号...", "INFO")
connected_users = await self.get_all_connected_users()
if not connected_users:
self._log("❌ 未找到可用的千牛账号", "ERROR")
return None
# 获取第一个账号
first_user = next(iter(connected_users.values()))
user_nick = first_user["userNick"]
self._log(f"✅ 获取到账号: {user_nick}", "SUCCESS")
self._log(f"账号详情: UID={first_user.get('userUid')}, 版本={first_user.get('version')}", "DEBUG")
return user_nick
except Exception as e:
self._log(f"❌ 获取账号失败: {e}", "ERROR")
return None
async def get_all_connected_users(self) -> Dict[str, Dict]:
"""获取所有已连接的千牛账号
返回格式: {"user_nick": {"userNick": "xxx", "userUid": "xxx", ...}}
"""
logger.info("开始获取所有已连接的千牛账号")
try:
# 通过HTTP API调用
response = await self._http_api_call("GetAllUser", {})
if not response:
logger.error("获取连接账号失败API返回为空")
return {}
logger.debug(f"获取连接账号响应: {response}")
# 检查返回数据格式
if isinstance(response, dict):
# 直接返回账号信息字典
logger.info(f"成功获取到 {len(response)} 个已连接账号")
return response
else:
logger.error(f"获取连接账号失败:返回数据格式错误 - {response}")
return {}
except Exception as e:
logger.error(f"获取连接账号异常: {e}")
return {}
async def _http_api_call(self, post_name: str, data_obj: Dict[str, Any]) -> Dict[str, Any]:
"""按官方文档以 form-data 方式调用 QianNiu/Api 接口"""
form = aiohttp.FormData()
form.add_field("post", post_name)
form.add_field("data", json.dumps(data_obj))
logger.debug(f"[TB-DIAG] HTTP CALL -> {post_name} url={QN_HTTP_API_URL} data={data_obj}")
try:
async with aiohttp.ClientSession() as session:
async with session.post(QN_HTTP_API_URL, data=form, timeout=26) as resp:
text = await resp.text()
logger.debug(f"[TB-DIAG] HTTP RESP <- {post_name} status={resp.status} body={text[:300]}")
if resp.status != 200:
logger.error(f"[QianNiuClient] HTTP接口 {post_name} 调用失败: status={resp.status}")
return {}
try:
return json.loads(text)
except Exception:
logger.error(f"[TB-DIAG] HTTP RESP JSON decode error on {post_name}")
return {}
except Exception as e:
logger.error(f"[QianNiuClient] HTTP接口 {post_name} 调用异常: {e}")
return {}
async def start_listening(self):
"""启动监听的主方法"""
try:
self._log("🔵 开始千牛平台连接流程", "INFO")
# New. 获取第一个可用账号
self.user_nick = await self._get_first_user()
if not self.user_nick:
self._log("❌ 无法获取可用账号", "ERROR")
return False
# 1. 创建千牛客户端
self.qn_client = QianNiuClient(user_nick=self.user_nick)
self._log("✅ 千牛客户端创建成功", "DEBUG")
# 2. 测试DLL加载和启动与main方法一致
self._log("🔵 步骤1: 测试DLL加载和启动...", "INFO")
if not await self.qn_client._start_sainiu_service():
self._log("❌ DLL服务启动失败", "ERROR")
return False
self._log("✅ DLL服务启动成功", "SUCCESS")
# 3. 测试WebSocket连接与main方法一致
self._log("🔵 步骤2: 测试WebSocket连接...", "INFO")
if not await self.qn_client._connect_websocket():
self._log("❌ WebSocket连接失败", "ERROR")
return False
self._log("✅ WebSocket连接成功", "SUCCESS")
# 4. 创建消息处理器
self.message_handler = TestMessageHandler(self.qn_client)
self._log("✅ 消息处理器创建成功", "DEBUG")
# 5. 初始化AI服务
success = await self.message_handler.initialize()
if not success:
self._log("❌ AI服务初始化失败", "ERROR")
return False
self._log("✅ AI服务初始化成功", "SUCCESS")
# 6. 开始监听消息
self._log("🔵 步骤3: 开始监听消息...", "INFO")
self.stop_event = asyncio.Event()
self.running = True
print(self.user_nick)
# 7. 启动监听任务
listen_task, heartbeat_task = await self.qn_client.start_listening(
self.message_handler.handle_message
)
self.tasks.extend([listen_task, heartbeat_task])
self._log("✅ 监听任务启动成功", "SUCCESS")
# 8. 等待任务完成或停止信号
try:
await asyncio.gather(*self.tasks, return_exceptions=True)
except asyncio.CancelledError:
self._log("🔵 监听任务被取消", "WARNING")
except Exception as e:
self._log(f"❌ 监听过程中出现错误: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
self._log("🎉 千牛平台消息监听已启动", "SUCCESS")
return True
except Exception as e:
self._log(f"❌ 启动监听失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False
def stop_listening(self):
"""停止监听"""
try:
self._log("🔵 开始停止监听...", "INFO")
self.running = False
# 取消所有任务
for task in self.tasks:
if not task.done():
task.cancel()
self.tasks.clear()
# 关闭千牛客户端
if self.qn_client:
self._log("🔵 关闭千牛客户端...", "DEBUG")
asyncio.create_task(self.qn_client.close())
# 关闭消息处理器
if self.message_handler:
self._log("🔵 关闭消息处理器...", "DEBUG")
asyncio.create_task(self.message_handler.close())
# 设置停止事件
if self.stop_event:
self.stop_event.set()
self._log("✅ 监听已停止", "SUCCESS")
except Exception as e:
self._log(f"❌ 停止监听时出现错误: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def is_running(self):
"""检查是否正在运行"""
return self.running
def set_log_signal(self, signal):
"""设置日志信号"""
self.log_signal = signal
"""用于GUI集成的千牛监听包装器类"""
def __init__(self, log_callback=None):
"""初始化千牛监听包装器"""
self.qn_client = None
self.message_handler = None
self.running = False
self.stop_event = None
self.log_callback = log_callback
self.log_signal = None # 添加日志信号属性
self.tasks = [] # 新增:任务列表
def _log(self, message, log_type="INFO"):
"""内部日志方法"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{log_type}] {message}"
if hasattr(self, 'log_signal') and self.log_signal:
self.log_signal.emit(message, log_type)
elif self.log_callback:
self.log_callback(message, log_type)
else:
color_map = {
"ERROR": "\033[91m",
"WARNING": "\033[93m",
"SUCCESS": "\033[92m",
"DEBUG": "\033[96m",
}
color = color_map.get(log_type, "")
print(f"{color}[{timestamp}] [{log_type}] {message}\033[0m")
async def start_listening(self, user_nick):
"""启动监听的主方法"""
try:
self._log("🔵 开始千牛平台连接流程", "INFO")
# 1. 创建千牛客户端
self.qn_client = QianNiuClient()
self._log("✅ 千牛客户端创建成功", "DEBUG")
# 2. 测试DLL加载和启动与main方法一致
self._log("🔵 步骤1: 测试DLL加载和启动...", "INFO")
if not await self.qn_client._start_sainiu_service():
self._log("❌ DLL服务启动失败", "ERROR")
return False
self._log("✅ DLL服务启动成功", "SUCCESS")
# 3. 测试WebSocket连接与main方法一致
self._log("🔵 步骤2: 测试WebSocket连接...", "INFO")
if not await self.qn_client._connect_websocket():
self._log("❌ WebSocket连接失败", "ERROR")
return False
self._log("✅ WebSocket连接成功", "SUCCESS")
# 4. 创建消息处理器
self.message_handler = TestMessageHandler(self.qn_client)
self._log("✅ 消息处理器创建成功", "DEBUG")
# 5. 初始化AI服务
success = await self.message_handler.initialize()
if not success:
self._log("❌ AI服务初始化失败", "ERROR")
return False
self._log("✅ AI服务初始化成功", "SUCCESS")
# 6. 开始监听消息
self._log("🔵 步骤3: 开始监听消息...", "INFO")
self.stop_event = asyncio.Event()
self.running = True
# 7. 启动监听任务
listen_task, heartbeat_task = await self.qn_client.start_listening(
self.message_handler.handle_message
)
self.tasks.extend([listen_task, heartbeat_task])
self._log("✅ 监听任务启动成功", "SUCCESS")
# 8. 等待任务完成或停止信号
try:
await asyncio.gather(*self.tasks, return_exceptions=True)
except asyncio.CancelledError:
self._log("🔵 监听任务被取消", "WARNING")
except Exception as e:
self._log(f"❌ 监听过程中出现错误: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
self._log("🎉 千牛平台消息监听已启动", "SUCCESS")
return True
except Exception as e:
self._log(f"❌ 启动监听失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False
def stop_listening(self):
"""停止监听"""
try:
self._log("🔵 开始停止监听...", "INFO")
self.running = False
# 取消所有任务
for task in self.tasks:
if not task.done():
task.cancel()
self.tasks.clear()
# 关闭千牛客户端
if self.qn_client:
self._log("🔵 关闭千牛客户端...", "DEBUG")
asyncio.create_task(self.qn_client.close())
# 关闭消息处理器
if self.message_handler:
self._log("🔵 关闭消息处理器...", "DEBUG")
asyncio.create_task(self.message_handler.close())
# 设置停止事件
if self.stop_event:
self.stop_event.set()
self._log("✅ 监听已停止", "SUCCESS")
except Exception as e:
self._log(f"❌ 停止监听时出现错误: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def is_running(self):
"""检查是否正在运行"""
return self.running
def set_log_signal(self, signal):
"""设置日志信号"""
self.log_signal = signal
async def main():
"""主测试函数"""
logger.info("=== 赛牛接口本地测试开始 ===")
print("=== 断点A: 测试开始 ===")
# 创建千牛客户端
qn_client = QianNiuClient()
message_handler = None # 提前定义变量
try:
# 1. 测试DLL加载和启动
print("\n📋 步骤1: 测试DLL加载和启动...")
if not await qn_client._start_sainiu_service():
print("❌ DLL服务启动失败")
return False
print("✅ DLL服务启动成功")
# 2. 测试WebSocket连接包含授权和初始化
print("\n📋 步骤2: 测试WebSocket连接...")
if not await qn_client._connect_websocket():
print("❌ WebSocket连接失败")
return False
print("✅ WebSocket连接成功")
# 3. 创建消息处理器
message_handler = TestMessageHandler(qn_client)
# 4. 初始化消息处理器和AI服务
print("\n📋 步骤3: 初始化AI服务...")
if not await message_handler.initialize():
logger.error("AI服务初始化失败")
print("❌ AI服务初始化失败")
return
print("✅ AI服务初始化成功")
# 5. 开始监听消息
print("\n📋 步骤4: 开始监听消息...")
listen_task, heartbeat_task = await qn_client.start_listening(message_handler.handle_message)
# 6. 测试发送消息功能
print("\n📋 步骤5: 测试发送消息功能...")
test_nick = "test_buyer"
test_message = "这是一条测试消息"
print(f"=== 准备发送测试消息给 {test_nick} ===")
success = await qn_client.send_message(test_nick, test_message)
if success:
print("✅ 测试消息发送成功")
else:
print("❌ 测试消息发送失败")
# 保持运行,直到用户中断
logger.info("测试程序已启动按Ctrl+C停止...")
print("=== 测试程序运行中,等待消息... ===")
# 等待任务完成或用户中断
await asyncio.gather(listen_task, heartbeat_task, return_exceptions=True)
except KeyboardInterrupt:
logger.info("用户中断测试")
print("=== 断点D: 用户中断测试 ===")
except Exception as e:
logger.error(f"测试异常: {e}")
print(f"=== 断点E: 测试异常 - {e} ===")
finally:
if message_handler: # 检查变量是否已定义
await message_handler.close() # 关闭消息处理器和AI服务
await qn_client.close()
logger.info("=== 赛牛接口本地测试结束 ===")
print("=== 断点F: 测试结束 ===")
async def test_auth():
"""测试完整的集成连接"""
print("开始测试完整的赛牛集成连接...")
sainiu_service = SaiNiuService()
if sainiu_service.load_dll():
print("DLL加载成功")
else:
print("DLL加载失败")
if __name__ == "__main__":
# 运行测试
asyncio.run(main())
# asyncio.run(test_auth())