From 5b2565f6ef16aac2e1f19c3b80e876bfd11c5fbb Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:12:21 +0800 Subject: [PATCH 01/15] Update XianyuAutoAsync.py --- XianyuAutoAsync.py | 227 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 2 deletions(-) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index bfb4c95..207e7a4 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -195,6 +195,11 @@ class XianyuLive: # 启动定期清理过期暂停记录的任务 self.cleanup_task = None + # Cookie刷新定时任务 + self.cookie_refresh_task = None + self.cookie_refresh_interval = 300 # 1小时 = 3600秒 + self.last_cookie_refresh_time = 0 + def is_auto_confirm_enabled(self) -> bool: @@ -3228,16 +3233,225 @@ class XianyuLive: # 清理过期的暂停记录 pause_manager.cleanup_expired_pauses() - + # 清理过期的锁(每5分钟清理一次,保留24小时内的锁) self.cleanup_expired_locks(max_age_hours=24) - + # 每5分钟清理一次 await asyncio.sleep(300) except Exception as e: logger.error(f"【{self.cookie_id}】清理任务失败: {self._safe_str(e)}") await asyncio.sleep(300) # 出错后也等待5分钟再重试 + async def cookie_refresh_loop(self): + """Cookie刷新定时任务 - 每小时执行一次""" + while True: + try: + # 检查账号是否启用 + from cookie_manager import manager as cookie_manager + if cookie_manager and not cookie_manager.get_cookie_status(self.cookie_id): + logger.info(f"【{self.cookie_id}】账号已禁用,停止Cookie刷新循环") + break + + current_time = time.time() + if current_time - self.last_cookie_refresh_time >= self.cookie_refresh_interval: + logger.info(f"【{self.cookie_id}】开始执行Cookie刷新任务...") + # 在独立的任务中执行Cookie刷新,避免阻塞主循环 + asyncio.create_task(self._execute_cookie_refresh(current_time)) + + # 每分钟检查一次是否需要执行 + await asyncio.sleep(60) + except Exception as e: + logger.error(f"【{self.cookie_id}】Cookie刷新循环失败: {self._safe_str(e)}") + await asyncio.sleep(60) # 出错后也等待1分钟再重试 + + async def _execute_cookie_refresh(self, current_time): + """独立执行Cookie刷新任务,避免阻塞主循环""" + try: + success = await self._refresh_cookies_via_browser() + if success: + self.last_cookie_refresh_time = current_time + logger.info(f"【{self.cookie_id}】Cookie刷新任务完成") + else: + logger.warning(f"【{self.cookie_id}】Cookie刷新任务失败") + except Exception as e: + logger.error(f"【{self.cookie_id}】执行Cookie刷新任务异常: {self._safe_str(e)}") + + async def _refresh_cookies_via_browser(self): + """通过浏览器访问指定页面刷新Cookie""" + playwright = None + browser = None + try: + from playwright.async_api import async_playwright + + logger.info(f"【{self.cookie_id}】开始通过浏览器刷新Cookie...") + logger.info(f"【{self.cookie_id}】刷新前Cookie长度: {len(self.cookies_str)}") + logger.info(f"【{self.cookie_id}】刷新前Cookie字段数: {len(self.cookies)}") + + playwright = await async_playwright().start() + + # 启动浏览器(参照商品搜索的配置) + browser_args = [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--no-first-run', + '--no-zygote', + '--disable-gpu', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + '--disable-features=TranslateUI', + '--disable-ipc-flooding-protection', + '--disable-extensions', + '--disable-default-apps', + '--disable-sync', + '--disable-translate', + '--hide-scrollbars', + '--mute-audio', + '--no-default-browser-check', + '--no-pings' + ] + + # 在Docker环境中添加额外参数 + if os.getenv('DOCKER_ENV'): + browser_args.extend([ + '--single-process', + '--disable-background-networking', + '--disable-client-side-phishing-detection', + '--disable-hang-monitor', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-web-resources', + '--metrics-recording-only', + '--safebrowsing-disable-auto-update', + '--enable-automation', + '--password-store=basic', + '--use-mock-keychain' + ]) + + browser = await playwright.chromium.launch( + headless=True, + args=browser_args + ) + + # 创建浏览器上下文 + context = await browser.new_context( + viewport={'width': 1920, 'height': 1080}, + user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' + ) + + # 设置当前Cookie + cookies = [] + for cookie_pair in self.cookies_str.split('; '): + if '=' in cookie_pair: + name, value = cookie_pair.split('=', 1) + cookies.append({ + 'name': name.strip(), + 'value': value.strip(), + 'domain': '.goofish.com', + 'path': '/' + }) + + await context.add_cookies(cookies) + logger.info(f"【{self.cookie_id}】已设置 {len(cookies)} 个Cookie到浏览器") + + # 创建页面 + page = await context.new_page() + + # 访问指定页面 + target_url = "https://www.goofish.com/im?spm=a21ybx.home.sidebar.1.4c053da6vYwnmf" + logger.info(f"【{self.cookie_id}】访问页面: {target_url}") + + # 缩短超时时间,避免影响WebSocket连接 + await page.goto(target_url, wait_until='domcontentloaded', timeout=15000) + + # 缩短等待时间 + logger.info(f"【{self.cookie_id}】等待页面加载...") + await asyncio.sleep(2) + + # 第一次刷新 + logger.info(f"【{self.cookie_id}】执行第一次刷新...") + await page.reload(wait_until='domcontentloaded', timeout=15000) + await asyncio.sleep(2) + + # 第二次刷新 + logger.info(f"【{self.cookie_id}】执行第二次刷新...") + await page.reload(wait_until='domcontentloaded', timeout=15000) + await asyncio.sleep(2) + + # 获取更新后的Cookie + logger.info(f"【{self.cookie_id}】获取更新后的Cookie...") + updated_cookies = await context.cookies() + + # 构造新的Cookie字典 + new_cookies_dict = {} + for cookie in updated_cookies: + new_cookies_dict[cookie['name']] = cookie['value'] + + # 检查Cookie变化 + changed_cookies = [] + new_cookies = [] + for name, new_value in new_cookies_dict.items(): + old_value = self.cookies.get(name) + if old_value is None: + new_cookies.append(name) + elif old_value != new_value: + changed_cookies.append(name) + + # 更新self.cookies和cookies_str + self.cookies.update(new_cookies_dict) + self.cookies_str = '; '.join([f"{k}={v}" for k, v in self.cookies.items()]) + + logger.info(f"【{self.cookie_id}】Cookie已更新,包含 {len(new_cookies_dict)} 个字段") + + # 显示Cookie变化统计 + if changed_cookies: + logger.info(f"【{self.cookie_id}】发生变化的Cookie字段 ({len(changed_cookies)}个): {', '.join(changed_cookies)}") + if new_cookies: + logger.info(f"【{self.cookie_id}】新增的Cookie字段 ({len(new_cookies)}个): {', '.join(new_cookies)}") + if not changed_cookies and not new_cookies: + logger.info(f"【{self.cookie_id}】Cookie无变化") + + # 打印完整的更新后Cookie(可选择性启用) + logger.info(f"【{self.cookie_id}】更新后的完整Cookie: {self.cookies_str}") + + # 打印主要的Cookie字段详情 + important_cookies = ['_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 't', 'sgcookie', 'unb', 'uc1', 'uc3', 'uc4'] + logger.info(f"【{self.cookie_id}】重要Cookie字段详情:") + for cookie_name in important_cookies: + if cookie_name in new_cookies_dict: + cookie_value = new_cookies_dict[cookie_name] + # 对于敏感信息,只显示前后几位 + if len(cookie_value) > 20: + display_value = f"{cookie_value[:8]}...{cookie_value[-8:]}" + else: + display_value = cookie_value + + # 标记是否发生了变化 + change_mark = " [已变化]" if cookie_name in changed_cookies else " [新增]" if cookie_name in new_cookies else "" + logger.info(f"【{self.cookie_id}】 {cookie_name}: {display_value}{change_mark}") + + # 更新数据库中的Cookie + await self.update_config_cookies() + + logger.info(f"【{self.cookie_id}】Cookie刷新完成") + return True + + except Exception as e: + logger.error(f"【{self.cookie_id}】通过浏览器刷新Cookie失败: {self._safe_str(e)}") + return False + finally: + # 确保资源清理 + try: + if browser: + await browser.close() + if playwright: + await playwright.stop() + except Exception as cleanup_e: + logger.warning(f"【{self.cookie_id}】清理浏览器资源时出错: {self._safe_str(cleanup_e)}") + async def send_msg_once(self, toid, item_id, text): headers = { "Cookie": self.cookies_str, @@ -3919,6 +4133,11 @@ class XianyuLive: logger.info(f"【{self.cookie_id}】启动暂停记录清理任务...") self.cleanup_task = asyncio.create_task(self.pause_cleanup_loop()) + # 启动Cookie刷新任务 + if not self.cookie_refresh_task: + logger.info(f"【{self.cookie_id}】启动Cookie刷新任务...") + self.cookie_refresh_task = asyncio.create_task(self.cookie_refresh_loop()) + logger.info(f"【{self.cookie_id}】开始监听WebSocket消息...") logger.info(f"【{self.cookie_id}】WebSocket连接状态正常,等待服务器消息...") logger.info(f"【{self.cookie_id}】准备进入消息循环...") @@ -3948,6 +4167,8 @@ class XianyuLive: self.token_refresh_task.cancel() if self.cleanup_task: self.cleanup_task.cancel() + if self.cookie_refresh_task: + self.cookie_refresh_task.cancel() await asyncio.sleep(5) # 等待5秒后重试 continue finally: @@ -3958,6 +4179,8 @@ class XianyuLive: self.token_refresh_task.cancel() if self.cleanup_task: self.cleanup_task.cancel() + if self.cookie_refresh_task: + self.cookie_refresh_task.cancel() await self.close_session() # 确保关闭session async def get_item_list_info(self, page_number=1, page_size=20, retry_count=0): From bf619985e759621b4777d905efa85c90ef0cd601 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:22:59 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Start.py | 10 +++++++++ XianyuAutoAsync.py | 51 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Start.py b/Start.py index fb29ce4..313adfb 100644 --- a/Start.py +++ b/Start.py @@ -6,6 +6,7 @@ """ import os +import sys import asyncio import threading import uvicorn @@ -13,6 +14,15 @@ from urllib.parse import urlparse from pathlib import Path from loguru import logger +# 修复Linux环境下的asyncio子进程问题 +if sys.platform.startswith('linux'): + try: + # 在程序启动时就设置正确的事件循环策略 + asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) + logger.debug("已设置事件循环策略以支持子进程") + except Exception as e: + logger.debug(f"设置事件循环策略失败: {e}") + from config import AUTO_REPLY, COOKIES_LIST import cookie_manager as cm from db_manager import db_manager diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 207e7a4..62099f3 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -3282,13 +3282,62 @@ class XianyuLive: playwright = None browser = None try: + import asyncio from playwright.async_api import async_playwright logger.info(f"【{self.cookie_id}】开始通过浏览器刷新Cookie...") logger.info(f"【{self.cookie_id}】刷新前Cookie长度: {len(self.cookies_str)}") logger.info(f"【{self.cookie_id}】刷新前Cookie字段数: {len(self.cookies)}") - playwright = await async_playwright().start() + # Docker环境下修复asyncio子进程问题 + is_docker = os.getenv('DOCKER_ENV') or os.path.exists('/.dockerenv') + + if is_docker: + logger.debug(f"【{self.cookie_id}】检测到Docker环境,应用asyncio修复") + + # 创建一个完整的虚拟子进程监视器 + class DummyChildWatcher: + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def is_active(self): + return True + + def add_child_handler(self, *args, **kwargs): + pass + + def remove_child_handler(self, *args, **kwargs): + pass + + def attach_loop(self, *args, **kwargs): + pass + + def close(self): + pass + + def __del__(self): + pass + + # 创建自定义事件循环策略 + class DockerEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + def get_child_watcher(self): + return DummyChildWatcher() + + # 临时设置策略 + old_policy = asyncio.get_event_loop_policy() + asyncio.set_event_loop_policy(DockerEventLoopPolicy()) + + try: + playwright = await async_playwright().start() + finally: + # 恢复原策略 + asyncio.set_event_loop_policy(old_policy) + else: + # 非Docker环境,正常启动 + playwright = await async_playwright().start() # 启动浏览器(参照商品搜索的配置) browser_args = [ From 29b41864e5fabe03c3bdbfab189f5193eac9722d Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:27:08 +0800 Subject: [PATCH 03/15] Update XianyuAutoAsync.py --- XianyuAutoAsync.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 62099f3..54dd0df 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -3268,12 +3268,18 @@ class XianyuLive: async def _execute_cookie_refresh(self, current_time): """独立执行Cookie刷新任务,避免阻塞主循环""" try: - success = await self._refresh_cookies_via_browser() + # 为整个Cookie刷新任务添加超时保护(5分钟) + success = await asyncio.wait_for( + self._refresh_cookies_via_browser(), + timeout=300.0 # 5分钟超时 + ) if success: self.last_cookie_refresh_time = current_time logger.info(f"【{self.cookie_id}】Cookie刷新任务完成") else: logger.warning(f"【{self.cookie_id}】Cookie刷新任务失败") + except asyncio.TimeoutError: + logger.error(f"【{self.cookie_id}】Cookie刷新任务超时(5分钟)") except Exception as e: logger.error(f"【{self.cookie_id}】执行Cookie刷新任务异常: {self._safe_str(e)}") @@ -3331,13 +3337,28 @@ class XianyuLive: asyncio.set_event_loop_policy(DockerEventLoopPolicy()) try: - playwright = await async_playwright().start() + # 添加超时机制,避免无限等待 + playwright = await asyncio.wait_for( + async_playwright().start(), + timeout=30.0 # 30秒超时 + ) + logger.debug(f"【{self.cookie_id}】Docker环境下Playwright启动成功") + except asyncio.TimeoutError: + logger.error(f"【{self.cookie_id}】Docker环境下Playwright启动超时") + return False finally: # 恢复原策略 asyncio.set_event_loop_policy(old_policy) else: - # 非Docker环境,正常启动 - playwright = await async_playwright().start() + # 非Docker环境,正常启动(也添加超时保护) + try: + playwright = await asyncio.wait_for( + async_playwright().start(), + timeout=30.0 # 30秒超时 + ) + except asyncio.TimeoutError: + logger.error(f"【{self.cookie_id}】Playwright启动超时") + return False # 启动浏览器(参照商品搜索的配置) browser_args = [ From ea64b1adb7fb857afe646cee66d14f79f91fecd2 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:11:19 +0800 Subject: [PATCH 04/15] Update XianyuAutoAsync.py --- XianyuAutoAsync.py | 183 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 159 insertions(+), 24 deletions(-) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 54dd0df..046215c 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -197,8 +197,15 @@ class XianyuLive: # Cookie刷新定时任务 self.cookie_refresh_task = None - self.cookie_refresh_interval = 300 # 1小时 = 3600秒 + self.cookie_refresh_interval = 600 # 1小时 = 3600秒 self.last_cookie_refresh_time = 0 + self.cookie_refresh_running = False # 防止重复执行Cookie刷新 + self.cookie_refresh_enabled = True # 是否启用Cookie刷新功能 + + # WebSocket连接监控 + self.connection_failures = 0 # 连续连接失败次数 + self.max_connection_failures = 5 # 最大连续失败次数 + self.last_successful_connection = 0 # 上次成功连接时间 @@ -738,13 +745,20 @@ class XianyuLive: return new_token logger.error(f"【{self.cookie_id}】Token刷新失败: {res_json}") - + + # 清空当前token,确保下次重试时重新获取 + self.current_token = None + # 发送Token刷新失败通知 await self.send_token_refresh_notification(f"Token刷新失败: {res_json}", "token_refresh_failed") return None except Exception as e: logger.error(f"Token刷新异常: {self._safe_str(e)}") + + # 清空当前token,确保下次重试时重新获取 + self.current_token = None + # 发送Token刷新异常通知 await self.send_token_refresh_notification(f"Token刷新异常: {str(e)}", "token_refresh_exception") return None @@ -3051,6 +3065,10 @@ class XianyuLive: break else: logger.error(f"【{self.cookie_id}】Token刷新失败,将在{self.token_retry_interval // 60}分钟后重试") + + # 清空当前token,确保下次重试时重新获取 + self.current_token = None + # 发送Token刷新失败通知 await self.send_token_refresh_notification("Token定时刷新失败,将自动重试", "token_scheduled_refresh_failed") await asyncio.sleep(self.token_retry_interval) @@ -3196,6 +3214,9 @@ class XianyuLive: async def heartbeat_loop(self, ws): """心跳循环""" + consecutive_failures = 0 + max_failures = 3 # 连续失败3次后停止心跳 + while True: try: # 检查账号是否启用 @@ -3204,11 +3225,32 @@ class XianyuLive: logger.info(f"【{self.cookie_id}】账号已禁用,停止心跳循环") break + # 检查WebSocket连接状态 + if ws.closed: + logger.warning(f"【{self.cookie_id}】WebSocket连接已关闭,停止心跳循环") + break + await self.send_heartbeat(ws) + consecutive_failures = 0 # 重置失败计数 + + # 检查心跳响应超时 + current_time = time.time() + if (self.last_heartbeat_response > 0 and + current_time - self.last_heartbeat_response > self.heartbeat_timeout): + logger.warning(f"【{self.cookie_id}】心跳响应超时,可能存在连接问题") + await asyncio.sleep(self.heartbeat_interval) + except Exception as e: - logger.error(f"心跳发送失败: {self._safe_str(e)}") - break + consecutive_failures += 1 + logger.error(f"心跳发送失败 ({consecutive_failures}/{max_failures}): {self._safe_str(e)}") + + if consecutive_failures >= max_failures: + logger.error(f"【{self.cookie_id}】心跳连续失败{max_failures}次,停止心跳循环") + break + + # 失败后短暂等待再重试 + await asyncio.sleep(5) async def handle_heartbeat_response(self, message_data): """处理心跳响应""" @@ -3253,11 +3295,21 @@ class XianyuLive: logger.info(f"【{self.cookie_id}】账号已禁用,停止Cookie刷新循环") break + # 检查Cookie刷新功能是否启用 + if not self.cookie_refresh_enabled: + logger.debug(f"【{self.cookie_id}】Cookie刷新功能已禁用,跳过执行") + await asyncio.sleep(300) # 5分钟后再检查 + continue + current_time = time.time() if current_time - self.last_cookie_refresh_time >= self.cookie_refresh_interval: - logger.info(f"【{self.cookie_id}】开始执行Cookie刷新任务...") - # 在独立的任务中执行Cookie刷新,避免阻塞主循环 - asyncio.create_task(self._execute_cookie_refresh(current_time)) + # 检查是否已有Cookie刷新任务在执行 + if self.cookie_refresh_running: + logger.debug(f"【{self.cookie_id}】Cookie刷新任务已在执行中,跳过本次触发") + else: + logger.info(f"【{self.cookie_id}】开始执行Cookie刷新任务...") + # 在独立的任务中执行Cookie刷新,避免阻塞主循环 + asyncio.create_task(self._execute_cookie_refresh(current_time)) # 每分钟检查一次是否需要执行 await asyncio.sleep(60) @@ -3267,21 +3319,65 @@ class XianyuLive: async def _execute_cookie_refresh(self, current_time): """独立执行Cookie刷新任务,避免阻塞主循环""" + # 设置运行状态,防止重复执行 + self.cookie_refresh_running = True + try: - # 为整个Cookie刷新任务添加超时保护(5分钟) + logger.info(f"【{self.cookie_id}】开始Cookie刷新任务,暂时暂停心跳以避免连接冲突...") + + # 暂时暂停心跳任务,避免与浏览器操作冲突 + heartbeat_was_running = False + if self.heartbeat_task and not self.heartbeat_task.done(): + heartbeat_was_running = True + self.heartbeat_task.cancel() + logger.debug(f"【{self.cookie_id}】已暂停心跳任务") + + # 为整个Cookie刷新任务添加超时保护(3分钟,缩短时间减少影响) success = await asyncio.wait_for( self._refresh_cookies_via_browser(), - timeout=300.0 # 5分钟超时 + timeout=180.0 # 3分钟超时,减少对WebSocket的影响 ) + + # 重新启动心跳任务 + if heartbeat_was_running and self.ws and not self.ws.closed: + logger.debug(f"【{self.cookie_id}】重新启动心跳任务") + self.heartbeat_task = asyncio.create_task(self.heartbeat_loop(self.ws)) + if success: self.last_cookie_refresh_time = current_time - logger.info(f"【{self.cookie_id}】Cookie刷新任务完成") + logger.info(f"【{self.cookie_id}】Cookie刷新任务完成,心跳已恢复") else: logger.warning(f"【{self.cookie_id}】Cookie刷新任务失败") + # 即使失败也要更新时间,避免频繁重试 + self.last_cookie_refresh_time = current_time + except asyncio.TimeoutError: - logger.error(f"【{self.cookie_id}】Cookie刷新任务超时(5分钟)") + logger.error(f"【{self.cookie_id}】Cookie刷新任务超时(3分钟)") + # 超时也要更新时间,避免频繁重试 + self.last_cookie_refresh_time = current_time except Exception as e: logger.error(f"【{self.cookie_id}】执行Cookie刷新任务异常: {self._safe_str(e)}") + # 异常也要更新时间,避免频繁重试 + self.last_cookie_refresh_time = current_time + finally: + # 确保心跳任务恢复(如果WebSocket仍然连接) + if (self.ws and not self.ws.closed and + (not self.heartbeat_task or self.heartbeat_task.done())): + logger.info(f"【{self.cookie_id}】Cookie刷新完成,确保心跳任务正常运行") + self.heartbeat_task = asyncio.create_task(self.heartbeat_loop(self.ws)) + + # 清除运行状态 + self.cookie_refresh_running = False + + def enable_cookie_refresh(self, enabled: bool = True): + """启用或禁用Cookie刷新功能""" + self.cookie_refresh_enabled = enabled + status = "启用" if enabled else "禁用" + logger.info(f"【{self.cookie_id}】Cookie刷新功能已{status}") + + def disable_cookie_refresh(self): + """禁用Cookie刷新功能""" + self.enable_cookie_refresh(False) async def _refresh_cookies_via_browser(self): """通过浏览器访问指定页面刷新Cookie""" @@ -3434,22 +3530,22 @@ class XianyuLive: target_url = "https://www.goofish.com/im?spm=a21ybx.home.sidebar.1.4c053da6vYwnmf" logger.info(f"【{self.cookie_id}】访问页面: {target_url}") - # 缩短超时时间,避免影响WebSocket连接 - await page.goto(target_url, wait_until='domcontentloaded', timeout=15000) + # 进一步缩短超时时间,减少对WebSocket的影响 + await page.goto(target_url, wait_until='domcontentloaded', timeout=10000) - # 缩短等待时间 - logger.info(f"【{self.cookie_id}】等待页面加载...") - await asyncio.sleep(2) + # 最小化等待时间 + logger.info(f"【{self.cookie_id}】页面加载完成,快速刷新...") + await asyncio.sleep(1) - # 第一次刷新 + # 第一次刷新 - 缩短超时时间 logger.info(f"【{self.cookie_id}】执行第一次刷新...") - await page.reload(wait_until='domcontentloaded', timeout=15000) - await asyncio.sleep(2) + await page.reload(wait_until='domcontentloaded', timeout=8000) + await asyncio.sleep(1) - # 第二次刷新 + # 第二次刷新 - 缩短超时时间 logger.info(f"【{self.cookie_id}】执行第二次刷新...") - await page.reload(wait_until='domcontentloaded', timeout=15000) - await asyncio.sleep(2) + await page.reload(wait_until='domcontentloaded', timeout=8000) + await asyncio.sleep(1) # 获取更新后的Cookie logger.info(f"【{self.cookie_id}】获取更新后的Cookie...") @@ -4186,6 +4282,10 @@ class XianyuLive: logger.info(f"【{self.cookie_id}】WebSocket连接建立成功!") self.ws = websocket + # 更新连接状态 + self.connection_failures = 0 + self.last_successful_connection = time.time() + logger.info(f"【{self.cookie_id}】开始初始化WebSocket连接...") await self.init(websocket) logger.info(f"【{self.cookie_id}】WebSocket初始化完成!") @@ -4230,7 +4330,35 @@ class XianyuLive: continue except Exception as e: - logger.error(f"WebSocket连接异常: {self._safe_str(e)}") + error_msg = self._safe_str(e) + self.connection_failures += 1 + + logger.error(f"WebSocket连接异常 ({self.connection_failures}/{self.max_connection_failures}): {error_msg}") + + # 检查是否超过最大失败次数 + if self.connection_failures >= self.max_connection_failures: + logger.error(f"【{self.cookie_id}】连续连接失败{self.max_connection_failures}次,暂停重试30分钟") + await asyncio.sleep(1800) # 暂停30分钟 + self.connection_failures = 0 # 重置失败计数 + continue + + # 根据错误类型和失败次数决定处理策略 + if "no close frame received or sent" in error_msg: + logger.info(f"【{self.cookie_id}】检测到WebSocket连接意外断开,准备重新连接...") + retry_delay = min(3 * self.connection_failures, 15) # 递增重试间隔,最大15秒 + elif "Connection refused" in error_msg or "timeout" in error_msg.lower(): + logger.warning(f"【{self.cookie_id}】网络连接问题,延长重试间隔...") + retry_delay = min(10 * self.connection_failures, 60) # 递增重试间隔,最大60秒 + else: + logger.warning(f"【{self.cookie_id}】未知WebSocket错误,使用默认重试间隔...") + retry_delay = min(5 * self.connection_failures, 30) # 递增重试间隔,最大30秒 + + # 清空当前token,确保重新连接时会重新获取 + if self.current_token: + logger.info(f"【{self.cookie_id}】清空当前token,重新连接时将重新获取") + self.current_token = None + + # 取消所有任务 if self.heartbeat_task: self.heartbeat_task.cancel() if self.token_refresh_task: @@ -4239,9 +4367,16 @@ class XianyuLive: self.cleanup_task.cancel() if self.cookie_refresh_task: self.cookie_refresh_task.cancel() - await asyncio.sleep(5) # 等待5秒后重试 + + logger.info(f"【{self.cookie_id}】等待 {retry_delay} 秒后重试连接...") + await asyncio.sleep(retry_delay) continue finally: + # 清空当前token + if self.current_token: + logger.info(f"【{self.cookie_id}】程序退出,清空当前token") + self.current_token = None + # 清理所有任务 if self.heartbeat_task: self.heartbeat_task.cancel() From 9efe6226498c939c906862e7e420ecd5f3d8453c Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:54:05 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E4=BC=98=E5=8C=96token=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XianyuAutoAsync.py | 130 +++++++++++++++++++++++++++++++++++---------- global_config.yml | 4 +- 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 046215c..2df9811 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -197,11 +197,13 @@ class XianyuLive: # Cookie刷新定时任务 self.cookie_refresh_task = None - self.cookie_refresh_interval = 600 # 1小时 = 3600秒 + self.cookie_refresh_interval = 1200 # 1小时 = 3600秒 self.last_cookie_refresh_time = 0 self.cookie_refresh_running = False # 防止重复执行Cookie刷新 self.cookie_refresh_enabled = True # 是否启用Cookie刷新功能 + + # WebSocket连接监控 self.connection_failures = 0 # 连续连接失败次数 self.max_connection_failures = 5 # 最大连续失败次数 @@ -676,10 +678,13 @@ class XianyuLive: """刷新token""" try: logger.info(f"【{self.cookie_id}】开始刷新token...") + # 生成更精确的时间戳 + timestamp = str(int(time.time() * 1000)) + params = { 'jsv': '2.7.2', 'appKey': '34839810', - 't': str(int(time.time()) * 1000), + 't': timestamp, 'sign': '', 'v': '1.0', 'type': 'originaljson', @@ -688,7 +693,13 @@ class XianyuLive: 'timeout': '20000', 'api': 'mtop.taobao.idlemessage.pc.login.token', 'sessionOption': 'AutoLoginOnly', + 'dangerouslySetWindvaneParams': '%5Bobject%20Object%5D', + 'smToken': 'token', + 'queryToken': 'sm', + 'sm': 'sm', 'spm_cnt': 'a21ybx.im.0.0', + 'spm_pre': 'a21ybx.home.sidebar.1.4c053da6vYwnmf', + 'log_id': '4c053da6vYwnmf' } data_val = '{"appKey":"444e9908a51d1cb236a27862abc769c9","deviceId":"' + self.device_id + '"}' data = { @@ -702,9 +713,25 @@ class XianyuLive: sign = generate_sign(params['t'], token, data_val) params['sign'] = sign - # 发送请求 - headers = DEFAULT_HEADERS.copy() - headers['cookie'] = self.cookies_str + # 发送请求 - 使用与浏览器完全一致的请求头 + headers = { + 'accept': 'application/json', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'cache-control': 'no-cache', + 'content-type': 'application/x-www-form-urlencoded', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + 'sec-ch-ua': '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', + '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/139.0.0.0 Safari/537.36', + 'referer': 'https://www.goofish.com/', + 'origin': 'https://www.goofish.com', + 'cookie': self.cookies_str + } async with aiohttp.ClientSession() as session: async with session.post( @@ -3153,6 +3180,7 @@ class XianyuLive: if not self.current_token or (time.time() - self.last_token_refresh_time) >= self.token_refresh_interval: logger.info(f"【{self.cookie_id}】获取初始token...") token_refresh_attempted = True + await self.refresh_token() if not self.current_token: @@ -3319,6 +3347,8 @@ class XianyuLive: async def _execute_cookie_refresh(self, current_time): """独立执行Cookie刷新任务,避免阻塞主循环""" + + # 设置运行状态,防止重复执行 self.cookie_refresh_running = True @@ -3369,18 +3399,26 @@ class XianyuLive: # 清除运行状态 self.cookie_refresh_running = False + + def enable_cookie_refresh(self, enabled: bool = True): """启用或禁用Cookie刷新功能""" self.cookie_refresh_enabled = enabled status = "启用" if enabled else "禁用" logger.info(f"【{self.cookie_id}】Cookie刷新功能已{status}") - def disable_cookie_refresh(self): - """禁用Cookie刷新功能""" - self.enable_cookie_refresh(False) + + + + + + + async def _refresh_cookies_via_browser(self): """通过浏览器访问指定页面刷新Cookie""" + + playwright = None browser = None try: @@ -3401,25 +3439,18 @@ class XianyuLive: class DummyChildWatcher: def __enter__(self): return self - def __exit__(self, *args): pass - def is_active(self): return True - def add_child_handler(self, *args, **kwargs): pass - def remove_child_handler(self, *args, **kwargs): pass - def attach_loop(self, *args, **kwargs): pass - def close(self): pass - def __del__(self): pass @@ -3497,16 +3528,21 @@ class XianyuLive: '--use-mock-keychain' ]) + # Cookie刷新模式使用无头浏览器 browser = await playwright.chromium.launch( headless=True, args=browser_args ) # 创建浏览器上下文 - context = await browser.new_context( - viewport={'width': 1920, 'height': 1080}, - user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' - ) + context_options = { + 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' + } + + # 使用标准窗口大小 + context_options['viewport'] = {'width': 1920, 'height': 1080} + + context = await browser.new_context(**context_options) # 设置当前Cookie cookies = [] @@ -3526,28 +3562,66 @@ class XianyuLive: # 创建页面 page = await context.new_page() + # 等待页面准备 + await asyncio.sleep(0.1) + # 访问指定页面 target_url = "https://www.goofish.com/im?spm=a21ybx.home.sidebar.1.4c053da6vYwnmf" logger.info(f"【{self.cookie_id}】访问页面: {target_url}") - # 进一步缩短超时时间,减少对WebSocket的影响 - await page.goto(target_url, wait_until='domcontentloaded', timeout=10000) + # 使用更灵活的页面访问策略 + try: + # 首先尝试较短超时 + await page.goto(target_url, wait_until='domcontentloaded', timeout=15000) + logger.info(f"【{self.cookie_id}】页面访问成功") + except Exception as e: + if 'timeout' in str(e).lower(): + logger.warning(f"【{self.cookie_id}】页面访问超时,尝试降级策略...") + try: + # 降级策略:只等待基本加载 + await page.goto(target_url, wait_until='load', timeout=20000) + logger.info(f"【{self.cookie_id}】页面访问成功(降级策略)") + except Exception as e2: + logger.warning(f"【{self.cookie_id}】降级策略也失败,尝试最基本访问...") + # 最后尝试:不等待任何加载完成 + await page.goto(target_url, timeout=25000) + logger.info(f"【{self.cookie_id}】页面访问成功(最基本策略)") + else: + raise e - # 最小化等待时间 - logger.info(f"【{self.cookie_id}】页面加载完成,快速刷新...") + # Cookie刷新模式:执行两次刷新 + logger.info(f"【{self.cookie_id}】页面加载完成,开始刷新...") await asyncio.sleep(1) - # 第一次刷新 - 缩短超时时间 + # 第一次刷新 - 带重试机制 logger.info(f"【{self.cookie_id}】执行第一次刷新...") - await page.reload(wait_until='domcontentloaded', timeout=8000) + try: + await page.reload(wait_until='domcontentloaded', timeout=12000) + logger.info(f"【{self.cookie_id}】第一次刷新成功") + except Exception as e: + if 'timeout' in str(e).lower(): + logger.warning(f"【{self.cookie_id}】第一次刷新超时,使用降级策略...") + await page.reload(wait_until='load', timeout=15000) + logger.info(f"【{self.cookie_id}】第一次刷新成功(降级策略)") + else: + raise e await asyncio.sleep(1) - # 第二次刷新 - 缩短超时时间 + # 第二次刷新 - 带重试机制 logger.info(f"【{self.cookie_id}】执行第二次刷新...") - await page.reload(wait_until='domcontentloaded', timeout=8000) + try: + await page.reload(wait_until='domcontentloaded', timeout=12000) + logger.info(f"【{self.cookie_id}】第二次刷新成功") + except Exception as e: + if 'timeout' in str(e).lower(): + logger.warning(f"【{self.cookie_id}】第二次刷新超时,使用降级策略...") + await page.reload(wait_until='load', timeout=15000) + logger.info(f"【{self.cookie_id}】第二次刷新成功(降级策略)") + else: + raise e await asyncio.sleep(1) - # 获取更新后的Cookie + # Cookie刷新模式:正常更新Cookie logger.info(f"【{self.cookie_id}】获取更新后的Cookie...") updated_cookies = await context.cookies() diff --git a/global_config.yml b/global_config.yml index e3f9217..63c812b 100644 --- a/global_config.yml +++ b/global_config.yml @@ -45,7 +45,7 @@ DEFAULT_HEADERS: user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 HEARTBEAT_INTERVAL: 15 -HEARTBEAT_TIMEOUT: 5 +HEARTBEAT_TIMEOUT: 20 LOG_CONFIG: compression: zip format: '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} @@ -58,7 +58,7 @@ MANUAL_MODE: timeout: 3600 toggle_keywords: [] MESSAGE_EXPIRE_TIME: 300000 -TOKEN_REFRESH_INTERVAL: 72000 # 从3600秒(1小时)增加到18000秒(20小时) +TOKEN_REFRESH_INTERVAL: 72000 # 从3600秒(1小时)增加到72000秒(20小时) TOKEN_RETRY_INTERVAL: 7200 # 从300秒(5分钟)增加到7200秒(2小时) WEBSOCKET_HEADERS: Accept-Encoding: gzip, deflate, br, zstd From ffbd6a41f80456e6d12a267a51bde0e139f85581 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:57:39 +0800 Subject: [PATCH 06/15] Update global_config.yml --- global_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global_config.yml b/global_config.yml index 63c812b..0ab588d 100644 --- a/global_config.yml +++ b/global_config.yml @@ -45,7 +45,7 @@ DEFAULT_HEADERS: user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 HEARTBEAT_INTERVAL: 15 -HEARTBEAT_TIMEOUT: 20 +HEARTBEAT_TIMEOUT: 30 LOG_CONFIG: compression: zip format: '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} From 7da13c379c2b4bff31fdb860325451bf15680f28 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:00:06 +0800 Subject: [PATCH 07/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E6=9A=82=E5=81=9C=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ Dockerfile | 4 +++- README.md | 59 ++++++++++++++++++++++++------------------------ db_manager.py | 16 +++++++++---- requirements.txt | 3 ++- 5 files changed, 49 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 9695d70..d542d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -294,6 +294,8 @@ example_*.py *_example.py demo_*.py *_demo.py +fix_*.py +*_fix.py # 文档文件(除了README.md) *.md @@ -301,6 +303,7 @@ demo_*.py !CHANGELOG.md !CONTRIBUTING.md !LICENSE.md +!docs/*.md # 临时配置文件 *.local.yml diff --git a/Dockerfile b/Dockerfile index 870186a..108b1f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,11 +3,13 @@ FROM python:3.11-slim-bookworm # 设置标签信息 LABEL maintainer="zhinianboke" -LABEL version="2.1.0" +LABEL version="2.2.0" LABEL description="闲鱼自动回复系统 - 企业级多用户版本,支持自动发货和免拼发货" LABEL repository="https://github.com/zhinianboke/xianyu-auto-reply" LABEL license="仅供学习使用,禁止商业用途" LABEL author="zhinianboke" +LABEL build-date="" +LABEL vcs-ref="" # 设置工作目录 WORKDIR /app diff --git a/README.md b/README.md index e944514..6c91e4f 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,8 @@ xianyu-auto-reply/ │ ├── index.html # 主管理界面(集成所有功能模块) │ ├── login.html # 用户登录页面 │ ├── register.html # 用户注册页面(邮箱验证) -│ ├── user_management.html # 用户管理页面(管理员功能) -│ ├── data_management.html # 数据管理页面(导入导出) -│ ├── log_management.html # 日志管理页面(实时日志查看) -│ ├── item_search.html # 商品搜索页面(独立版本) │ ├── js/ -│ │ ├── app.js # 主要JavaScript逻辑 -│ │ └── modules/ # 模块化JavaScript文件 +│ │ └── app.js # 主要JavaScript逻辑和所有功能模块 │ ├── css/ │ │ ├── variables.css # CSS变量定义 │ │ ├── layout.css # 布局样式 @@ -137,16 +132,23 @@ xianyu-auto-reply/ │ ├── wechat-group.png # 微信群二维码 │ └── qq-group.png # QQ群二维码 ├── 🐳 Docker部署 -│ ├── Dockerfile # Docker镜像构建文件 +│ ├── Dockerfile # Docker镜像构建文件(优化版) +│ ├── Dockerfile-cn # 国内优化版Docker镜像构建文件 │ ├── docker-compose.yml # Docker Compose一键部署配置 +│ ├── docker-compose-cn.yml # 国内优化版Docker Compose配置 │ ├── docker-deploy.sh # Docker部署管理脚本(Linux/macOS) │ ├── docker-deploy.bat # Docker部署管理脚本(Windows) -│ └── entrypoint.sh # Docker容器启动脚本 +│ ├── entrypoint.sh # Docker容器启动脚本 +│ └── .dockerignore # Docker构建忽略文件 +├── 🌐 Nginx配置 +│ └── nginx/ +│ ├── nginx.conf # Nginx反向代理配置 +│ └── ssl/ # SSL证书目录 ├── 📋 配置文件 │ ├── global_config.yml # 全局配置文件(WebSocket、API等) -│ ├── requirements.txt # Python依赖包列表(精简版) -│ ├── .gitignore # Git忽略文件配置 -│ └── README.md # 项目说明文档 +│ ├── requirements.txt # Python依赖包列表(精简版,无内置模块) +│ ├── .gitignore # Git忽略文件配置(完整版) +│ └── README.md # 项目说明文档(本文件) └── 📊 数据目录(运行时创建) ├── data/ # 数据目录(Docker挂载) │ └── xianyu_data.db # SQLite数据库文件 @@ -407,30 +409,27 @@ python Start.py - **`image_uploader.py`** - 图片上传工具,支持多种CDN服务商、自动压缩、格式优化、批量上传 ### 🌐 前端界面 (`static/`) -- **`index.html`** - 主管理界面,包含账号管理、关键词管理、系统监控、实时状态显示 +- **`index.html`** - 主管理界面,集成所有功能模块:账号管理、关键词管理、商品管理、发货管理、系统监控、用户管理等 - **`login.html`** - 用户登录页面,支持图形验证码、记住登录状态、多重安全验证 - **`register.html`** - 用户注册页面,支持邮箱验证码、实时验证、密码强度检测 -- **`user_management.html`** - 用户管理页面,管理员专用,用户增删改查、权限管理 -- **`data_management.html`** - 数据管理页面,支持Excel导入导出、数据备份、批量操作 -- **`log_management.html`** - 日志管理页面,实时日志查看、日志搜索过滤、日志下载 -- **`item_search.html`** - 商品搜索页面,获取真实闲鱼商品数据,支持多条件搜索 -- **`js/app.js`** - 主要JavaScript逻辑,处理前端交互、API调用、实时更新 -- **`css/`** - 模块化样式文件,包含布局、组件、主题等分类样式,响应式设计 +- **`js/app.js`** - 主要JavaScript逻辑,包含所有功能模块:前端交互、API调用、实时更新、数据管理、用户界面控制 +- **`css/`** - 模块化样式文件,包含布局、组件、主题等分类样式,响应式设计,支持明暗主题切换 - **`xianyu_js_version_2.js`** - 闲鱼JavaScript工具库,加密解密、数据处理、API封装 -- **`lib/`** - 前端依赖库,包含Bootstrap、jQuery、Chart.js等第三方库 +- **`lib/`** - 前端依赖库,包含Bootstrap 5、Bootstrap Icons等第三方库 +- **`uploads/images/`** - 图片上传目录,支持发货图片和其他媒体文件存储 ### 🐳 部署配置 -- **`Dockerfile`** - Docker镜像构建文件,包含Python环境、Playwright浏览器、系统依赖,支持无头模式运行 -- **`docker-compose.yml`** - Docker Compose配置,支持一键部署、环境变量配置、资源限制、健康检查 -- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、监控、日志查看等功能(Linux/macOS) -- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署 -- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动 -- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡、SSL终端、WebSocket代理 -- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织,包含详细说明 -- **`.gitignore`** - Git忽略文件配置,完整覆盖Python、Docker、前端等开发文件 -- **`.dockerignore`** - Docker构建忽略文件,优化构建上下文大小和构建速度 -- **`Dockerfile-cn`** - 国内优化版Docker镜像构建文件,使用国内镜像源加速构建 -- **`docker-compose-cn.yml`** - 国内优化版Docker Compose配置文件 +- **`Dockerfile`** - Docker镜像构建文件,基于Python 3.11-slim,包含Playwright浏览器、系统依赖,支持无头模式运行,优化构建层级 +- **`Dockerfile-cn`** - 国内优化版Docker镜像构建文件,使用国内镜像源加速构建,适合国内网络环境 +- **`docker-compose.yml`** - Docker Compose配置,支持一键部署、完整环境变量配置、资源限制、健康检查、可选Nginx代理 +- **`docker-compose-cn.yml`** - 国内优化版Docker Compose配置文件,使用国内镜像源 +- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、停止、重启、监控、日志查看等功能(Linux/macOS) +- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署和管理 +- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化、目录创建、权限设置和服务启动 +- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡、SSL终端、WebSocket代理、静态文件服务 +- **`requirements.txt`** - Python依赖包列表,精简版本无内置模块,按功能分类组织,包含详细版本说明和安装指南 +- **`.gitignore`** - Git忽略文件配置,完整覆盖Python、Docker、前端、测试、临时文件等,支持项目特定文件类型 +- **`.dockerignore`** - Docker构建忽略文件,优化构建上下文大小和构建速度,排除不必要的文件和目录 ## 🏗️ 详细技术架构 diff --git a/db_manager.py b/db_manager.py index 5db8d8a..fed72cf 100644 --- a/db_manager.py +++ b/db_manager.py @@ -1189,7 +1189,7 @@ class DBManager: 'user_id': result[2], 'auto_confirm': bool(result[3]), 'remark': result[4] or '', - 'pause_duration': result[5] or 10, + 'pause_duration': result[5] if result[5] is not None else 10, 'created_at': result[6] } return None @@ -1244,11 +1244,19 @@ class DBManager: self._execute_sql(cursor, "SELECT pause_duration FROM cookies WHERE id = ?", (cookie_id,)) result = cursor.fetchone() if result: - return result[0] or 10 # 默认10分钟 - return 10 # 如果没有找到记录,返回默认值 + if result[0] is None: + logger.warning(f"账号 {cookie_id} 的pause_duration为NULL,使用默认值10分钟并修复数据库") + # 修复数据库中的NULL值 + self._execute_sql(cursor, "UPDATE cookies SET pause_duration = 10 WHERE id = ?", (cookie_id,)) + self.conn.commit() + return 10 + return result[0] # 返回实际值,不使用or操作符 + else: + logger.warning(f"账号 {cookie_id} 未找到记录,使用默认值10分钟") + return 10 except Exception as e: logger.error(f"获取账号自动回复暂停时间失败: {e}") - return 10 # 出错时返回默认值 + return 10 def get_auto_confirm(self, cookie_id: str) -> bool: """获取Cookie的自动确认发货设置""" diff --git a/requirements.txt b/requirements.txt index 0217286..72f2d28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -78,4 +78,5 @@ xlsxwriter>=3.1.0 # collections, itertools, functools, copy, pickle, gzip, zipfile, shutil # tempfile, io, csv, xml, html, http, socket, ssl, subprocess, signal # inspect, ast, enum, math, decimal, array, queue, contextlib, warnings -# typing, dataclasses, weakref, gc, platform, stat, glob, fnmatch \ No newline at end of file +# typing, dataclasses, weakref, gc, platform, stat, glob, fnmatch, mimetypes +# email, smtplib, imaplib, poplib, ftplib, telnetlib, configparser, argparse \ No newline at end of file From 0f6b756c2576e8dbcf63df79dbe460e31a796056 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:36:11 +0800 Subject: [PATCH 08/15] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E6=B8=A0=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- XianyuAutoAsync.py | 165 +++++++++++++++++++++++++++++++++++++++++++++ static/index.html | 36 +++++++++- static/js/app.js | 80 ++++++++++++++++++++++ 4 files changed, 283 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c91e4f..190d2c7 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ python Start.py - **多重安全验证** - 超级加密保护,防止误操作和数据泄露 - **批量处理能力** - 支持批量确认发货,提高处理效率 - **异常处理机制** - 完善的错误处理和重试机制,确保发货成功 -- **多渠道通知** - 支持QQ、钉钉、邮件等多种发货通知方式 +- **多渠道通知** - 支持QQ、钉钉、飞书、Bark、邮件等多种发货通知方式 ### 👥 多用户系统 - **用户注册登录** - 支持邮箱验证和图形验证码,安全可靠 @@ -362,10 +362,11 @@ python Start.py - **账号状态验证** - 自动检查cookies启用状态,确保搜索功能正常 ### 📱 通知系统 -- **多渠道支持** - QQ、钉钉、邮件、微信、Telegram等6种通知方式 +- **多渠道支持** - QQ、钉钉、飞书、Bark、邮件、微信、Telegram等8种通知方式 - **智能配置** - 可视化配置界面,支持复杂参数和加密设置 - **实时推送** - 重要事件实时通知,及时了解系统状态 - **通知模板** - 自定义通知内容和格式,个性化消息推送 +- **移动端支持** - Bark iOS推送,随时随地接收通知 ### 🔐 安全特性 - **Cookie安全管理** - 加密存储用户凭证,定期自动刷新 diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 2df9811..4c4f654 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -1863,6 +1863,12 @@ class XianyuLive: case 'ding_talk' | 'dingtalk': logger.info(f"📱 开始发送钉钉通知...") await self._send_dingtalk_notification(config_data, notification_msg) + case 'feishu' | 'lark': + logger.info(f"📱 开始发送飞书通知...") + await self._send_feishu_notification(config_data, notification_msg) + case 'bark': + logger.info(f"📱 开始发送Bark通知...") + await self._send_bark_notification(config_data, notification_msg) case 'email': logger.info(f"📱 开始发送邮件通知...") await self._send_email_notification(config_data, notification_msg) @@ -1989,6 +1995,159 @@ class XianyuLive: except Exception as e: logger.error(f"发送钉钉通知异常: {self._safe_str(e)}") + async def _send_feishu_notification(self, config_data: dict, message: str): + """发送飞书通知""" + try: + import aiohttp + import json + import hmac + import hashlib + import base64 + + logger.info(f"📱 飞书通知 - 开始处理配置数据: {config_data}") + + # 解析配置 + webhook_url = config_data.get('webhook_url', '') + secret = config_data.get('secret', '') + + logger.info(f"📱 飞书通知 - Webhook URL: {webhook_url[:50]}...") + logger.info(f"📱 飞书通知 - 是否有签名密钥: {'是' if secret else '否'}") + + if not webhook_url: + logger.warning("📱 飞书通知 - Webhook URL配置为空,无法发送通知") + return + + # 如果有加签密钥,生成签名 + timestamp = str(int(time.time())) + sign = "" + + if secret: + string_to_sign = f'{timestamp}\n{secret}' + hmac_code = hmac.new( + secret.encode('utf-8'), + string_to_sign.encode('utf-8'), + digestmod=hashlib.sha256 + ).digest() + sign = base64.b64encode(hmac_code).decode('utf-8') + logger.info(f"📱 飞书通知 - 已生成签名") + + # 构建请求数据 + data = { + "msg_type": "text", + "content": { + "text": message + }, + "timestamp": timestamp + } + + # 如果有签名,添加到请求数据中 + if sign: + data["sign"] = sign + + logger.info(f"📱 飞书通知 - 请求数据构建完成") + + # 发送POST请求 + async with aiohttp.ClientSession() as session: + async with session.post(webhook_url, json=data, timeout=10) as response: + response_text = await response.text() + logger.info(f"📱 飞书通知 - 响应状态: {response.status}") + logger.info(f"📱 飞书通知 - 响应内容: {response_text}") + + if response.status == 200: + try: + response_json = json.loads(response_text) + if response_json.get('code') == 0: + logger.info(f"📱 飞书通知发送成功") + else: + logger.warning(f"📱 飞书通知发送失败: {response_json.get('msg', '未知错误')}") + except json.JSONDecodeError: + logger.info(f"📱 飞书通知发送成功(响应格式异常)") + else: + logger.warning(f"📱 飞书通知发送失败: HTTP {response.status}, 响应: {response_text}") + + except Exception as e: + logger.error(f"📱 发送飞书通知异常: {self._safe_str(e)}") + import traceback + logger.error(f"📱 飞书通知异常详情: {traceback.format_exc()}") + + async def _send_bark_notification(self, config_data: dict, message: str): + """发送Bark通知""" + try: + import aiohttp + import json + from urllib.parse import quote + + logger.info(f"📱 Bark通知 - 开始处理配置数据: {config_data}") + + # 解析配置 + server_url = config_data.get('server_url', 'https://api.day.app').rstrip('/') + device_key = config_data.get('device_key', '') + title = config_data.get('title', '闲鱼自动回复通知') + sound = config_data.get('sound', 'default') + icon = config_data.get('icon', '') + group = config_data.get('group', 'xianyu') + url = config_data.get('url', '') + + logger.info(f"📱 Bark通知 - 服务器: {server_url}") + logger.info(f"📱 Bark通知 - 设备密钥: {device_key[:10]}..." if device_key else "📱 Bark通知 - 设备密钥: 未设置") + logger.info(f"📱 Bark通知 - 标题: {title}") + + if not device_key: + logger.warning("📱 Bark通知 - 设备密钥配置为空,无法发送通知") + return + + # 构建请求URL和数据 + # Bark支持两种方式:URL路径方式和POST JSON方式 + # 这里使用POST JSON方式,更灵活且支持更多参数 + + api_url = f"{server_url}/push" + + # 构建请求数据 + data = { + "device_key": device_key, + "title": title, + "body": message, + "sound": sound, + "group": group + } + + # 可选参数 + if icon: + data["icon"] = icon + if url: + data["url"] = url + + logger.info(f"📱 Bark通知 - API地址: {api_url}") + logger.info(f"📱 Bark通知 - 请求数据构建完成") + + # 发送POST请求 + async with aiohttp.ClientSession() as session: + async with session.post(api_url, json=data, timeout=10) as response: + response_text = await response.text() + logger.info(f"📱 Bark通知 - 响应状态: {response.status}") + logger.info(f"📱 Bark通知 - 响应内容: {response_text}") + + if response.status == 200: + try: + response_json = json.loads(response_text) + if response_json.get('code') == 200: + logger.info(f"📱 Bark通知发送成功") + else: + logger.warning(f"📱 Bark通知发送失败: {response_json.get('message', '未知错误')}") + except json.JSONDecodeError: + # 某些Bark服务器可能返回纯文本 + if 'success' in response_text.lower() or 'ok' in response_text.lower(): + logger.info(f"📱 Bark通知发送成功") + else: + logger.warning(f"📱 Bark通知响应格式异常: {response_text}") + else: + logger.warning(f"📱 Bark通知发送失败: HTTP {response.status}, 响应: {response_text}") + + except Exception as e: + logger.error(f"📱 发送Bark通知异常: {self._safe_str(e)}") + import traceback + logger.error(f"📱 Bark通知异常详情: {traceback.format_exc()}") + async def _send_email_notification(self, config_data: dict, message: str): """发送邮件通知""" try: @@ -2218,6 +2377,12 @@ class XianyuLive: case 'ding_talk' | 'dingtalk': await self._send_dingtalk_notification(config_data, notification_msg) notification_sent = True + case 'feishu' | 'lark': + await self._send_feishu_notification(config_data, notification_msg) + notification_sent = True + case 'bark': + await self._send_bark_notification(config_data, notification_msg) + notification_sent = True case 'email': await self._send_email_notification(config_data, notification_msg) notification_sent = True diff --git a/static/index.html b/static/index.html index 1919e93..0748fdb 100644 --- a/static/index.html +++ b/static/index.html @@ -1064,6 +1064,40 @@ +
+
+
+
+ +
+
飞书通知
+

飞书机器人消息

+
+ +
+
+
+
+ +
+
+
+
+ +
+
Bark通知
+

iOS推送通知

+
+ +
+
+
+
+
@@ -2720,7 +2754,7 @@ - +