From 960ac769620da5cfada697da757d4c28ec06dfec Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:30:25 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XianyuAutoAsync.py | 42 +++++++++++++++++++ reply_server.py | 101 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 7c93570..3de38cb 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -122,6 +122,10 @@ class XianyuLive: # 商品详情缓存(24小时有效) _item_detail_cache = {} # {item_id: {'detail': str, 'timestamp': float}} _item_detail_cache_lock = asyncio.Lock() + + # 类级别的实例管理字典,用于API调用 + _instances = {} # {cookie_id: XianyuLive实例} + _instances_lock = asyncio.Lock() def _safe_str(self, e): """安全地将异常转换为字符串""" @@ -213,7 +217,41 @@ class XianyuLive: self.max_connection_failures = 5 # 最大连续失败次数 self.last_successful_connection = 0 # 上次成功连接时间 + # 注册实例到类级别字典(用于API调用) + self._register_instance() + def _register_instance(self): + """注册当前实例到类级别字典""" + try: + # 使用同步方式注册,避免在__init__中使用async + XianyuLive._instances[self.cookie_id] = self + logger.debug(f"【{self.cookie_id}】实例已注册到全局字典") + except Exception as e: + logger.error(f"【{self.cookie_id}】注册实例失败: {self._safe_str(e)}") + + def _unregister_instance(self): + """从类级别字典中注销当前实例""" + try: + if self.cookie_id in XianyuLive._instances: + del XianyuLive._instances[self.cookie_id] + logger.debug(f"【{self.cookie_id}】实例已从全局字典中注销") + except Exception as e: + logger.error(f"【{self.cookie_id}】注销实例失败: {self._safe_str(e)}") + + @classmethod + def get_instance(cls, cookie_id: str): + """获取指定cookie_id的XianyuLive实例""" + return cls._instances.get(cookie_id) + + @classmethod + def get_all_instances(cls): + """获取所有活跃的XianyuLive实例""" + return dict(cls._instances) + + @classmethod + def get_instance_count(cls): + """获取当前活跃实例数量""" + return len(cls._instances) def is_auto_confirm_enabled(self) -> bool: """检查当前账号是否启用自动确认发货""" @@ -4991,6 +5029,10 @@ class XianyuLive: self.cookie_refresh_task.cancel() await self.close_session() # 确保关闭session + # 从全局实例字典中注销当前实例 + self._unregister_instance() + logger.info(f"【{self.cookie_id}】XianyuLive主程序已完全退出") + async def get_item_list_info(self, page_number=1, page_size=20, retry_count=0): """获取商品信息,自动处理token失效的情况 diff --git a/reply_server.py b/reply_server.py index 238a230..0ebf46d 100644 --- a/reply_server.py +++ b/reply_server.py @@ -848,6 +848,107 @@ async def register(request: RegisterRequest): ) +# ------------------------- 发送消息接口 ------------------------- + +# 固定的API秘钥(生产环境中应该从配置文件或环境变量读取) +API_SECRET_KEY = "xianyu_api_secret_2024" + +class SendMessageRequest(BaseModel): + api_key: str + cookie_id: str + chat_id: str + to_user_id: str + message: str + + +class SendMessageResponse(BaseModel): + success: bool + message: str + + +def verify_api_key(api_key: str) -> bool: + """验证API秘钥""" + return api_key == API_SECRET_KEY + + +@app.post('/send-message', response_model=SendMessageResponse) +async def send_message_api(request: SendMessageRequest): + """发送消息API接口(使用秘钥验证)""" + try: + # 验证API秘钥 + if not verify_api_key(request.api_key): + logger.warning(f"API秘钥验证失败: {request.api_key}") + return SendMessageResponse( + success=False, + message="API秘钥验证失败" + ) + + # 检查cookie_manager是否可用 + if not cookie_manager.manager: + logger.error("CookieManager未初始化") + return SendMessageResponse( + success=False, + message="系统未就绪,请稍后重试" + ) + + # 检查Cookie是否存在 + if request.cookie_id not in cookie_manager.manager.cookies: + logger.warning(f"Cookie不存在: {request.cookie_id}") + return SendMessageResponse( + success=False, + message="指定的Cookie账号不存在" + ) + + # 检查账号是否启用 + if not cookie_manager.manager.get_cookie_status(request.cookie_id): + logger.warning(f"尝试使用已禁用的账号发送消息: {request.cookie_id}") + return SendMessageResponse( + success=False, + message="该账号已被禁用,无法发送消息" + ) + + # 获取XianyuLive实例 + from XianyuAutoAsync import XianyuLive + live_instance = XianyuLive.get_instance(request.cookie_id) + + if not live_instance: + logger.warning(f"账号实例不存在或未连接: {request.cookie_id}") + return SendMessageResponse( + success=False, + message="账号实例不存在或未连接,请检查账号状态" + ) + + # 检查WebSocket连接状态 + if not live_instance.ws or live_instance.ws.closed: + logger.warning(f"账号WebSocket连接已断开: {request.cookie_id}") + return SendMessageResponse( + success=False, + message="账号WebSocket连接已断开,请等待重连" + ) + + # 发送消息 + await live_instance.send_msg( + live_instance.ws, + request.chat_id, + request.to_user_id, + request.message + ) + + logger.info(f"API成功发送消息: {request.cookie_id} -> {request.to_user_id}, 内容: {request.message[:50]}{'...' if len(request.message) > 50 else ''}") + + return SendMessageResponse( + success=True, + message="消息发送成功" + ) + + except Exception as e: + logger.error(f"API发送消息异常: {request.cookie_id} -> {request.to_user_id}, 错误: {str(e)}") + return SendMessageResponse( + success=False, + message=f"发送消息失败: {str(e)}" + ) + + @app.post("/xianyu/reply", response_model=ResponseModel) async def xianyu_reply(req: RequestModel): msg_template = match_reply(req.cookie_id, req.send_message) From b7c88f99bf1f79124588eae03cf2632311e0a8cf Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Fri, 29 Aug 2025 20:59:19 +0800 Subject: [PATCH 2/5] Update reply_server.py --- reply_server.py | 65 ++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/reply_server.py b/reply_server.py index 0ebf46d..d125724 100644 --- a/reply_server.py +++ b/reply_server.py @@ -875,44 +875,34 @@ def verify_api_key(api_key: str) -> bool: async def send_message_api(request: SendMessageRequest): """发送消息API接口(使用秘钥验证)""" try: + # 清理所有参数中的换行符 + def clean_param(param_str): + """清理参数中的换行符""" + if isinstance(param_str, str): + return param_str.replace('\\n', '').replace('\n', '') + return param_str + + # 清理所有参数 + cleaned_api_key = clean_param(request.api_key) + cleaned_cookie_id = clean_param(request.cookie_id) + cleaned_chat_id = clean_param(request.chat_id) + cleaned_to_user_id = clean_param(request.to_user_id) + cleaned_message = clean_param(request.message) + # 验证API秘钥 - if not verify_api_key(request.api_key): - logger.warning(f"API秘钥验证失败: {request.api_key}") + if not verify_api_key(cleaned_api_key): + logger.warning(f"API秘钥验证失败: {cleaned_api_key}") return SendMessageResponse( success=False, message="API秘钥验证失败" ) - # 检查cookie_manager是否可用 - if not cookie_manager.manager: - logger.error("CookieManager未初始化") - return SendMessageResponse( - success=False, - message="系统未就绪,请稍后重试" - ) - - # 检查Cookie是否存在 - if request.cookie_id not in cookie_manager.manager.cookies: - logger.warning(f"Cookie不存在: {request.cookie_id}") - return SendMessageResponse( - success=False, - message="指定的Cookie账号不存在" - ) - - # 检查账号是否启用 - if not cookie_manager.manager.get_cookie_status(request.cookie_id): - logger.warning(f"尝试使用已禁用的账号发送消息: {request.cookie_id}") - return SendMessageResponse( - success=False, - message="该账号已被禁用,无法发送消息" - ) - - # 获取XianyuLive实例 + # 直接获取XianyuLive实例,跳过cookie_manager检查 from XianyuAutoAsync import XianyuLive - live_instance = XianyuLive.get_instance(request.cookie_id) + live_instance = XianyuLive.get_instance(cleaned_cookie_id) if not live_instance: - logger.warning(f"账号实例不存在或未连接: {request.cookie_id}") + logger.warning(f"账号实例不存在或未连接: {cleaned_cookie_id}") return SendMessageResponse( success=False, message="账号实例不存在或未连接,请检查账号状态" @@ -920,21 +910,21 @@ async def send_message_api(request: SendMessageRequest): # 检查WebSocket连接状态 if not live_instance.ws or live_instance.ws.closed: - logger.warning(f"账号WebSocket连接已断开: {request.cookie_id}") + logger.warning(f"账号WebSocket连接已断开: {cleaned_cookie_id}") return SendMessageResponse( success=False, message="账号WebSocket连接已断开,请等待重连" ) - # 发送消息 + # 发送消息(使用清理后的所有参数) await live_instance.send_msg( live_instance.ws, - request.chat_id, - request.to_user_id, - request.message + cleaned_chat_id, + cleaned_to_user_id, + cleaned_message ) - logger.info(f"API成功发送消息: {request.cookie_id} -> {request.to_user_id}, 内容: {request.message[:50]}{'...' if len(request.message) > 50 else ''}") + logger.info(f"API成功发送消息: {cleaned_cookie_id} -> {cleaned_to_user_id}, 内容: {cleaned_message[:50]}{'...' if len(cleaned_message) > 50 else ''}") return SendMessageResponse( success=True, @@ -942,7 +932,10 @@ async def send_message_api(request: SendMessageRequest): ) except Exception as e: - logger.error(f"API发送消息异常: {request.cookie_id} -> {request.to_user_id}, 错误: {str(e)}") + # 使用清理后的参数记录日志 + cookie_id_for_log = clean_param(request.cookie_id) if 'clean_param' in locals() else request.cookie_id + to_user_id_for_log = clean_param(request.to_user_id) if 'clean_param' in locals() else request.to_user_id + logger.error(f"API发送消息异常: {cookie_id_for_log} -> {to_user_id_for_log}, 错误: {str(e)}") return SendMessageResponse( success=False, message=f"发送消息失败: {str(e)}" From ce43a1e6dbe38444208bf7f948f4dfac81227a83 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Sat, 30 Aug 2025 07:36:46 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_manager.py | 3 +- quick_test_api.py | 76 ++++++++++++++++++++++++++ reply_server.py | 50 ++++++++++++++++- static/index.html | 88 ++++++++++++++++++++++++++++++ static/js/app.js | 123 +++++++++++++++++++++++++++++++++++++++++- static/update_log.txt | 4 ++ 6 files changed, 339 insertions(+), 5 deletions(-) create mode 100644 quick_test_api.py create mode 100644 static/update_log.txt diff --git a/db_manager.py b/db_manager.py index f968481..1924f29 100644 --- a/db_manager.py +++ b/db_manager.py @@ -414,7 +414,8 @@ class DBManager: ('smtp_password', '', 'SMTP登录密码/授权码'), ('smtp_from', '', '发件人显示名(留空则使用用户名)'), ('smtp_use_tls', 'true', '是否启用TLS'), - ('smtp_use_ssl', 'false', '是否启用SSL') + ('smtp_use_ssl', 'false', '是否启用SSL'), + ('qq_reply_secret_key', 'xianyu_qq_reply_2024', 'QQ回复消息API秘钥') ''') # 检查并升级数据库 diff --git a/quick_test_api.py b/quick_test_api.py new file mode 100644 index 0000000..55bff76 --- /dev/null +++ b/quick_test_api.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +快速测试QQ回复消息API的脚本 +""" + +import requests +import json + +def test_api(api_key, cookie_id="test", chat_id="test", to_user_id="test", message="test"): + """测试API调用""" + url = "http://localhost:8000/send-message" + + data = { + "api_key": api_key, + "cookie_id": cookie_id, + "chat_id": chat_id, + "to_user_id": to_user_id, + "message": message + } + + try: + response = requests.post(url, json=data, timeout=10) + result = response.json() + + print(f"秘钥: {api_key}") + print(f"状态: {response.status_code}") + print(f"响应: {json.dumps(result, ensure_ascii=False, indent=2)}") + print("-" * 50) + + return result.get("success", False) + + except Exception as e: + print(f"请求失败: {e}") + return False + +def main(): + print("🚀 快速API测试") + print("=" * 50) + + # 测试用例 + test_cases = [ + ("默认秘钥", "xianyu_qq_reply_2024"), + ("测试秘钥", "zhinina_test_key"), + ("错误秘钥", "wrong_key"), + ("空秘钥", ""), + ] + + for name, key in test_cases: + print(f"\n📋 测试: {name}") + test_api(key) + + # 测试参数验证 + print("\n📋 测试参数验证:") + + # 测试空参数 + param_tests = [ + ("空cookie_id", {"cookie_id": ""}), + ("空chat_id", {"chat_id": ""}), + ("空to_user_id", {"to_user_id": ""}), + ("空message", {"message": ""}), + ] + + for name, params in param_tests: + print(f"\n测试: {name}") + default_params = { + "cookie_id": "test", + "chat_id": "test", + "to_user_id": "test", + "message": "test" + } + default_params.update(params) + test_api("xianyu_qq_reply_2024", **default_params) + +if __name__ == "__main__": + main() diff --git a/reply_server.py b/reply_server.py index d125724..354155c 100644 --- a/reply_server.py +++ b/reply_server.py @@ -851,7 +851,8 @@ async def register(request: RegisterRequest): # ------------------------- 发送消息接口 ------------------------- # 固定的API秘钥(生产环境中应该从配置文件或环境变量读取) -API_SECRET_KEY = "xianyu_api_secret_2024" +# 注意:现在从系统设置中读取QQ回复消息秘钥 +API_SECRET_KEY = "xianyu_api_secret_2024" # 保留作为后备 class SendMessageRequest(BaseModel): api_key: str @@ -868,7 +869,20 @@ class SendMessageResponse(BaseModel): def verify_api_key(api_key: str) -> bool: """验证API秘钥""" - return api_key == API_SECRET_KEY + try: + # 从系统设置中获取QQ回复消息秘钥 + from db_manager import db_manager + qq_secret_key = db_manager.get_system_setting('qq_reply_secret_key') + + # 如果系统设置中没有配置,使用默认值 + if not qq_secret_key: + qq_secret_key = API_SECRET_KEY + + return api_key == qq_secret_key + except Exception as e: + logger.error(f"验证API秘钥时发生异常: {e}") + # 异常情况下使用默认秘钥验证 + return api_key == API_SECRET_KEY @app.post('/send-message', response_model=SendMessageResponse) @@ -889,6 +903,22 @@ async def send_message_api(request: SendMessageRequest): cleaned_to_user_id = clean_param(request.to_user_id) cleaned_message = clean_param(request.message) + # 验证API秘钥不能为空 + if not cleaned_api_key: + logger.warning("API秘钥为空") + return SendMessageResponse( + success=False, + message="API秘钥不能为空" + ) + + # 特殊测试秘钥处理 + if cleaned_api_key == "zhinina_test_key": + logger.info("使用测试秘钥,直接返回成功") + return SendMessageResponse( + success=True, + message="接口验证成功" + ) + # 验证API秘钥 if not verify_api_key(cleaned_api_key): logger.warning(f"API秘钥验证失败: {cleaned_api_key}") @@ -897,6 +927,22 @@ async def send_message_api(request: SendMessageRequest): message="API秘钥验证失败" ) + # 验证必需参数不能为空 + required_params = { + 'cookie_id': cleaned_cookie_id, + 'chat_id': cleaned_chat_id, + 'to_user_id': cleaned_to_user_id, + 'message': cleaned_message + } + + for param_name, param_value in required_params.items(): + if not param_value: + logger.warning(f"必需参数 {param_name} 为空") + return SendMessageResponse( + success=False, + message=f"参数 {param_name} 不能为空" + ) + # 直接获取XianyuLive实例,跳过cookie_manager检查 from XianyuAutoAsync import XianyuLive live_instance = XianyuLive.get_instance(cleaned_cookie_id) diff --git a/static/index.html b/static/index.html index cf0c6e3..d095d9c 100644 --- a/static/index.html +++ b/static/index.html @@ -1579,6 +1579,94 @@ + + +