refactor: 扫码登录改用缓冲期窗口替代预热 token 缓存

This commit is contained in:
GuDong
2026-04-21 23:04:00 +08:00
parent 49edf5028c
commit d7c2992467
5 changed files with 164 additions and 101 deletions
+117 -55
View File
@@ -295,13 +295,9 @@ class XianyuLive:
_init_auth_failure_threshold = 3
_init_auth_cooldown = 60
# 扫码登录token预热缓存,避免扫码成功后正式任务立即再次刷新token
_qr_prewarmed_tokens = {} # {cookie_id: {'token': str, 'timestamp': float}}
_qr_prewarmed_token_ttl = 180 # 秒
# 扫码登录后的短期缓冲状态:首轮 token 刷新命中风控时,先做浏览器侧稳定化再决定是否上滑块
_qr_login_grace_state = {} # {cookie_id: {'timestamp': float, 'captcha_buffer_used': bool, 'browser_stabilized': bool}}
_qr_login_grace_ttl = 180 # 秒
_qr_login_grace_ttl = max(300, int(RISK_CONTROL.get('qr_login_grace_minutes', 15) or 15) * 60)
@classmethod
def _cleanup_auth_prewarmed_tokens(cls):
@@ -346,49 +342,6 @@ class XianyuLive:
return
cls._auth_prewarmed_tokens.pop(cookie_id, None)
@classmethod
def _cleanup_qr_prewarmed_tokens(cls):
"""清理过期的扫码预热token缓存"""
now = time.time()
expired_cookie_ids = [
cookie_id
for cookie_id, token_info in cls._qr_prewarmed_tokens.items()
if now - token_info.get('timestamp', 0) > cls._qr_prewarmed_token_ttl
]
for cookie_id in expired_cookie_ids:
cls._qr_prewarmed_tokens.pop(cookie_id, None)
@classmethod
def cache_qr_prewarmed_token(cls, cookie_id: str, token: str):
"""缓存扫码登录预热好的token,供后续正式实例直接复用"""
if not cookie_id or not token:
return
cls._cleanup_qr_prewarmed_tokens()
cls._qr_prewarmed_tokens[cookie_id] = {
'token': token,
'timestamp': time.time()
}
@classmethod
def pop_qr_prewarmed_token(cls, cookie_id: str) -> Optional[Dict[str, Any]]:
"""弹出扫码登录预热token,过期则忽略"""
if not cookie_id:
return None
cls._cleanup_qr_prewarmed_tokens()
token_info = cls._qr_prewarmed_tokens.pop(cookie_id, None)
if not token_info:
return None
if time.time() - token_info.get('timestamp', 0) > cls._qr_prewarmed_token_ttl:
return None
return token_info
@classmethod
def clear_qr_prewarmed_token(cls, cookie_id: str):
"""清理指定账号的扫码预热token缓存"""
if not cookie_id:
return
cls._qr_prewarmed_tokens.pop(cookie_id, None)
@classmethod
def _cleanup_manual_refresh_state(cls):
"""清理过期的刷新交接恢复状态。"""
@@ -592,6 +545,10 @@ class XianyuLive:
state.update(extra_state)
cls._qr_login_grace_state[cookie_id] = state
@classmethod
def get_qr_login_grace_ttl_seconds(cls) -> int:
return max(300, int(RISK_CONTROL.get('qr_login_grace_minutes', 15) or 15) * 60)
@classmethod
def get_qr_login_grace(cls, cookie_id: str) -> Optional[Dict[str, Any]]:
"""获取扫码登录缓冲状态,过期则自动忽略"""
@@ -623,6 +580,58 @@ class XianyuLive:
return
cls._qr_login_grace_state.pop(cookie_id, None)
def _get_qr_login_grace_until(self) -> int:
try:
account_info = db_manager.get_cookie_details(self.cookie_id) or {}
return int(account_info.get('qr_login_grace_until') or 0)
except Exception as e:
logger.warning(f"{self.cookie_id}】读取扫码稳定期截止时间失败: {self._safe_str(e)}")
return 0
def _get_qr_login_grace_remaining_seconds(self, current_time: Optional[float] = None) -> int:
current_time = current_time or time.time()
grace_until = self._get_qr_login_grace_until()
return max(0, int(grace_until - current_time))
def _is_in_qr_login_grace_period(self, current_time: Optional[float] = None) -> bool:
return self._get_qr_login_grace_remaining_seconds(current_time) > 0
def _set_qr_login_grace_until(self, grace_until: int) -> None:
db_manager.set_cookie_qr_login_grace_until(self.cookie_id, int(grace_until or 0))
def _clear_qr_login_grace_period(self) -> None:
self.clear_qr_login_grace(self.cookie_id)
self._set_qr_login_grace_until(0)
def _enter_qr_login_grace_period(self, *, stage: str = 'qr_login_success') -> int:
now = time.time()
grace_until = int(now + self.get_qr_login_grace_ttl_seconds())
self.mark_qr_login_grace(self.cookie_id, stage=stage, entered_at=now)
self._set_qr_login_grace_until(grace_until)
return grace_until
def _consume_qr_login_grace_period_if_expired(self, current_time: Optional[float] = None) -> bool:
current_time = current_time or time.time()
grace_until = self._get_qr_login_grace_until()
if not grace_until:
return False
if current_time < grace_until:
return False
self._clear_qr_login_grace_period()
logger.info(f"{self.cookie_id}】扫码登录稳定期已结束,恢复自动认证链路")
return True
def _should_defer_auth_recovery_for_qr_grace(self, current_time: Optional[float] = None) -> bool:
current_time = current_time or time.time()
self._consume_qr_login_grace_period_if_expired(current_time)
remaining = self._get_qr_login_grace_remaining_seconds(current_time)
if remaining <= 0:
return False
self.last_token_refresh_status = "qr_login_grace_wait"
self.last_token_refresh_error_message = f"扫码登录稳定期中,剩余{remaining}"
logger.warning(f"{self.cookie_id}】扫码登录稳定期中,暂缓自动认证恢复,还需等待 {remaining}")
return True
@classmethod
def _cleanup_password_login_failure_backoff(cls):
"""清理已过期的密码登录失败退避状态"""
@@ -919,6 +928,7 @@ class XianyuLive:
async def _clear_account_pause_state(self, reason: str = "认证恢复成功") -> None:
self.last_token_refresh_error_message = ""
self._clear_qr_login_grace_period()
try:
db_manager.update_cookie_status_note(self.cookie_id, '')
@@ -1702,6 +1712,16 @@ class XianyuLive:
def _calculate_retry_delay(self, error_msg: str) -> int:
"""根据错误类型和失败次数计算重试延迟"""
current_time = time.time()
if self._is_account_pause_status(getattr(self, 'last_token_refresh_status', None)):
return max(300, self._compute_token_retry_wait_seconds(current_time))
if self._is_in_qr_login_grace_period(current_time):
return max(60, self._get_qr_login_grace_remaining_seconds(current_time))
if getattr(self, 'last_token_refresh_status', None) in {"password_login_backoff_wait", "verification_pending_manual", "qr_login_grace_wait"}:
return max(60, self._compute_token_retry_wait_seconds(current_time))
# WebSocket意外断开 - 短延迟
if "no close frame received or sent" in error_msg:
return min(3 * self.connection_failures, 15)
@@ -1949,12 +1969,6 @@ class XianyuLive:
f"{cookie_id}】已复用认证预热token,来源: {prewarmed_token_info.get('source') or 'unknown'}"
)
prewarmed_token_info = self.pop_qr_prewarmed_token(self.cookie_id)
if prewarmed_token_info and not self.current_token:
self.current_token = prewarmed_token_info.get('token')
self.last_token_refresh_time = prewarmed_token_info.get('timestamp', time.time())
logger.info(f"{cookie_id}】已复用扫码预热token,跳过首次token刷新")
# 通知防重复机制
self.last_notification_time = {} # 记录每种通知类型的最后发送时间
self.notification_cooldown = 300 # 5分钟内不重复发送相同类型的通知
@@ -6166,7 +6180,7 @@ class XianyuLive:
# 【消息接收时间重置】Token刷新成功后重置消息接收标志,与 cookie_refresh_loop 保持一致
self.last_message_received_time = 0
logger.warning(f"{self.cookie_id}】Token刷新成功,已重置消息接收时间标识")
self.clear_qr_login_grace(self.cookie_id)
self._clear_qr_login_grace_period()
self.clear_init_auth_failure_state(self.cookie_id)
self.last_init_failure_reason = None
self.last_init_failure_type = None
@@ -7109,6 +7123,36 @@ class XianyuLive:
)
return False
if self._is_account_pause_status(getattr(self, 'last_token_refresh_status', None)):
logger.warning(f"{self.cookie_id}】账号处于人工验证/风控暂停状态,跳过自动密码登录刷新")
if refresh_risk_log_id:
self._update_risk_log(
refresh_risk_log_id,
session_id=risk_session_id,
trigger_scene=trigger_scene,
result_code='account_pause_active',
processing_status='failed',
error_message='账号处于人工验证/风控暂停状态,自动密码登录刷新已跳过',
duration_ms=max(0, int((time.time() - risk_log_started_at) * 1000)),
event_meta=self._build_risk_event_meta(trigger_scene=trigger_scene, extra=base_event_meta),
)
return False
if self._should_defer_auth_recovery_for_qr_grace():
logger.warning(f"{self.cookie_id}】扫码登录稳定期内,跳过自动密码登录刷新")
if refresh_risk_log_id:
self._update_risk_log(
refresh_risk_log_id,
session_id=risk_session_id,
trigger_scene=trigger_scene,
result_code='qr_login_grace_active',
processing_status='failed',
error_message=self.last_token_refresh_error_message or '扫码登录稳定期内,自动密码登录刷新已跳过',
duration_ms=max(0, int((time.time() - risk_log_started_at) * 1000)),
event_meta=self._build_risk_event_meta(trigger_scene=trigger_scene, extra=base_event_meta),
)
return False
recovery_lock_owner = f"{self.cookie_id}:{trigger_scene or 'auto_cookie_refresh'}:{int(time.time() * 1000)}"
recovery_lock_acquired = False
@@ -11547,6 +11591,15 @@ class XianyuLive:
break
current_time = time.time()
if self._is_account_pause_status(getattr(self, 'last_token_refresh_status', None)):
logger.warning(f"{self.cookie_id}】账号处于人工验证/风控暂停状态,暂停会话保活循环")
await self._interruptible_sleep(300)
continue
if self._should_defer_auth_recovery_for_qr_grace(current_time):
await self._interruptible_sleep(max(60, self._get_qr_login_grace_remaining_seconds(current_time)))
continue
effective_keepalive_interval = self._get_effective_keepalive_interval()
if current_time - self.last_session_keepalive_time >= effective_keepalive_interval:
logger.info(f"{self.cookie_id}】开始执行轻量会话保活...")
@@ -12187,6 +12240,15 @@ class XianyuLive:
continue
current_time = time.time()
if self._is_account_pause_status(getattr(self, 'last_token_refresh_status', None)):
logger.warning(f"{self.cookie_id}】账号处于人工验证/风控暂停状态,跳过自动Cookie刷新")
await self._interruptible_sleep(300)
continue
if self._should_defer_auth_recovery_for_qr_grace(current_time):
await self._interruptible_sleep(max(60, self._get_qr_login_grace_remaining_seconds(current_time)))
continue
if self._should_skip_token_refresh_for_login_backoff(current_time):
logger.info(f"{self.cookie_id}】当前处于密码登录退避期,跳过自动Cookie刷新")
await self._interruptible_sleep(60)
+1
View File
@@ -126,6 +126,7 @@ RISK_CONTROL = config.get('RISK_CONTROL', {
'night_mode_enabled': False,
'night_start_hour': 1,
'night_end_hour': 6,
'qr_login_grace_minutes': 15,
'night_keepalive_multiplier': 3,
'night_cookie_refresh_multiplier': 2,
'backoff_escalation_factor': 1.5,
+31 -11
View File
@@ -325,6 +325,7 @@ class DBManager:
auto_confirm INTEGER DEFAULT 1,
remark TEXT DEFAULT '',
status_note TEXT DEFAULT '',
qr_login_grace_until INTEGER DEFAULT 0,
pause_duration INTEGER DEFAULT 10,
username TEXT DEFAULT '',
password TEXT DEFAULT '',
@@ -979,6 +980,11 @@ Cookie数量: {cookie_count}
cursor.execute("ALTER TABLE cookies ADD COLUMN status_note TEXT DEFAULT ''")
logger.info("数据库迁移完成:添加status_note列")
if 'qr_login_grace_until' not in cookie_columns:
logger.info("添加cookies表的qr_login_grace_until列...")
cursor.execute("ALTER TABLE cookies ADD COLUMN qr_login_grace_until INTEGER DEFAULT 0")
logger.info("数据库迁移完成:添加qr_login_grace_until列")
# 检查cookies表是否存在pause_duration列
if 'pause_duration' not in cookie_columns:
logger.info("添加cookies表的pause_duration列...")
@@ -2117,15 +2123,15 @@ Cookie数量: {cookie_count}
cursor = self.conn.cursor()
self._execute_sql(cursor, """
SELECT id, value, user_id, auto_confirm, remark, status_note,
pause_duration, username, password, show_browser, created_at,
qr_login_grace_until, pause_duration, username, password, show_browser, created_at,
proxy_type, proxy_host, proxy_port, proxy_user, proxy_pass
FROM cookies WHERE id = ?
""", (cookie_id,))
result = cursor.fetchone()
if result:
cookie_value = self._decrypt_secret(result[1])
password = self._decrypt_secret(result[8])
proxy_pass = self._decrypt_secret(result[15])
password = self._decrypt_secret(result[9])
proxy_pass = self._decrypt_secret(result[16])
return {
'id': result[0],
'value': cookie_value,
@@ -2133,16 +2139,17 @@ Cookie数量: {cookie_count}
'auto_confirm': bool(result[3]),
'remark': result[4] or '',
'status_note': result[5] or '',
'pause_duration': result[6] if result[6] is not None else 10, # 0是有效值,表示不暂停
'username': result[7] or '',
'qr_login_grace_until': int(result[6] or 0),
'pause_duration': result[7] if result[7] is not None else 10, # 0是有效值,表示不暂停
'username': result[8] or '',
'password': password,
'show_browser': bool(result[9]) if result[9] is not None else False,
'created_at': result[10],
'show_browser': bool(result[10]) if result[10] is not None else False,
'created_at': result[11],
# 代理配置
'proxy_type': result[11] or 'none',
'proxy_host': result[12] or '',
'proxy_port': result[13] or 0,
'proxy_user': result[14] or '',
'proxy_type': result[12] or 'none',
'proxy_host': result[13] or '',
'proxy_port': result[14] or 0,
'proxy_user': result[15] or '',
'proxy_pass': proxy_pass
}
return None
@@ -2189,6 +2196,19 @@ Cookie数量: {cookie_count}
logger.error(f"更新账号状态文案失败: {e}")
return False
def set_cookie_qr_login_grace_until(self, cookie_id: str, grace_until: int) -> bool:
"""更新账号扫码登录稳定期截止时间"""
with self.lock:
try:
cursor = self.conn.cursor()
self._execute_sql(cursor, "UPDATE cookies SET qr_login_grace_until = ? WHERE id = ?", (int(grace_until or 0), cookie_id))
self.conn.commit()
logger.info(f"更新账号 {cookie_id} 扫码稳定期截止时间: {int(grace_until or 0)}")
return True
except Exception as e:
logger.error(f"更新账号扫码稳定期失败: {e}")
return False
def update_cookie_pause_duration(self, cookie_id: str, pause_duration: int) -> bool:
"""更新Cookie的自动回复暂停时间"""
with self.lock:
+1
View File
@@ -72,6 +72,7 @@ RISK_CONTROL:
night_keepalive_multiplier: 3
night_mode_enabled: false
night_start_hour: 1
qr_login_grace_minutes: 15
post_slider_retry_delay_max: 10.0
post_slider_retry_delay_min: 5.0
token_refresh_dedup_window_seconds: 60
+14 -35
View File
@@ -23,6 +23,7 @@ from collections import defaultdict
import cookie_manager
from db_manager import db_manager
from config import RISK_CONTROL
from file_log_collector import setup_file_logging, get_file_log_collector
from ai_reply_engine import ai_reply_engine
from utils.qr_login import qr_login_manager
@@ -5336,30 +5337,12 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
real_cookies = updated_cookie_info['cookies_str']
log_with_user('info', f"已获取真实cookie,长度: {len(real_cookies)}", current_user)
XianyuLive.mark_qr_login_grace(account_id, stage='real_cookie_ready')
token_prewarmed = False
qr_login_grace_minutes = max(5, int(RISK_CONTROL.get('qr_login_grace_minutes', 15) or 15))
qr_login_grace_until = int(time.time() + (qr_login_grace_minutes * 60))
task_restarted = False
warning_message = None
final_cookies = temp_instance.cookies_str or real_cookies
try:
log_with_user('info', f"开始预热扫码登录Token: {account_id}", current_user)
prewarmed_token = await temp_instance.refresh_token()
final_cookies = temp_instance.cookies_str or real_cookies
if prewarmed_token:
XianyuLive.cache_qr_prewarmed_token(account_id, prewarmed_token)
token_prewarmed = True
XianyuLive.clear_qr_login_grace(account_id)
log_with_user('info', f"扫码登录Token预热成功: {account_id}", current_user)
else:
warning_message = "真实Cookie已获取,但首次Token初始化未完成,将在账号任务启动后继续重试"
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
except Exception as token_e:
final_cookies = temp_instance.cookies_str or real_cookies
warning_message = f"真实Cookie已获取,但首次Token初始化异常,将在账号任务启动后继续重试: {str(token_e)}"
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
try:
if cookie_manager.manager:
@@ -5371,22 +5354,21 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
cookie_manager.manager.update_cookie(account_id, final_cookies, save_to_db=False)
log_with_user('info', f"已更新cookie_manager中的真实cookie: {account_id}", current_user)
task_restarted = True
if not token_prewarmed:
warning_message = warning_message or "真实Cookie已获取,账号任务已切换;首次Token将在后台继续初始化"
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
db_manager.set_cookie_qr_login_grace_until(account_id, qr_login_grace_until)
XianyuLive.mark_qr_login_grace(account_id, stage='real_cookie_ready', grace_until=qr_login_grace_until)
warning_message = f"真实Cookie已获取,账号任务已切换;为降低再次触发风控的概率,将进入 {qr_login_grace_minutes} 分钟稳定期,稳定期内不自动预热Token"
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
else:
warning_message = "真实Cookie已获取,但任务管理器未初始化,未启动账号任务"
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
except Exception as task_switch_e:
if token_prewarmed:
XianyuLive.clear_qr_prewarmed_token(account_id)
db_manager.set_cookie_qr_login_grace_until(account_id, 0)
XianyuLive.clear_qr_login_grace(account_id)
warning_message = f"真实Cookie已获取,但切换账号任务失败: {str(task_switch_e)}"
log_with_user('warning', f"{warning_message}: {account_id}", current_user)
if not task_restarted:
if token_prewarmed:
XianyuLive.clear_qr_prewarmed_token(account_id)
db_manager.set_cookie_qr_login_grace_until(account_id, 0)
XianyuLive.clear_qr_login_grace(account_id)
if not warning_message:
warning_message = "真实Cookie已获取,但任务管理器未初始化,未启动账号任务"
@@ -5405,10 +5387,7 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
try:
if task_restarted:
processing_result = '扫码登录真实Cookie获取成功,账号任务已启动'
if token_prewarmed:
processing_result += 'Token预热完成'
else:
processing_result += ';Token预热未完成,将在首次刷新时继续重试'
processing_result += f';已进入 {qr_login_grace_minutes} 分钟稳定期,稳定期内不自动预热Token'
db_manager.update_risk_control_log(
log_id=risk_log_id,
processing_status='success',
@@ -5421,7 +5400,7 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
'account_id': account_id,
'is_new_account': is_new_account,
'task_restarted': task_restarted,
'token_prewarmed': token_prewarmed,
'token_prewarmed': False,
})
)
else:
@@ -5438,7 +5417,7 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
'account_id': account_id,
'is_new_account': is_new_account,
'task_restarted': task_restarted,
'token_prewarmed': token_prewarmed,
'token_prewarmed': False,
})
)
except Exception:
@@ -5449,7 +5428,7 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
'is_new_account': is_new_account,
'real_cookie_refreshed': task_restarted, # 回滚时为 False,成功切换时为 True
'cookie_length': len(final_cookies),
'token_prewarmed': token_prewarmed,
'token_prewarmed': False,
'task_restarted': task_restarted,
'warning_message': warning_message
}