896 lines
30 KiB
Python
896 lines
30 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import threading
|
||
import time
|
||
|
||
import database
|
||
import email_service
|
||
from api_browser import APIBrowser
|
||
from app_config import get_config
|
||
from app_logger import LoggerAdapter, get_logger
|
||
from services.checkpoints import get_checkpoint_mgr
|
||
from services.client_log import log_to_client
|
||
from services.proxy import get_proxy_from_api
|
||
from services.runtime import get_socketio
|
||
from services.screenshots import take_screenshot_for_account
|
||
from services.state import (
|
||
safe_get_account,
|
||
safe_remove_task,
|
||
safe_remove_task_status,
|
||
safe_set_task_status,
|
||
safe_update_task_status,
|
||
)
|
||
from services.task_batches import _batch_task_record_result, _get_batch_id_from_source
|
||
from services.task_scheduler import TaskScheduler
|
||
from task_checkpoint import TaskStage
|
||
|
||
logger = get_logger("app")
|
||
config = get_config()
|
||
|
||
# 并发默认值(启动后会由系统配置覆盖并调用 update_limits)
|
||
max_concurrent_per_account = config.MAX_CONCURRENT_PER_ACCOUNT
|
||
max_concurrent_global = config.MAX_CONCURRENT_GLOBAL
|
||
_SOURCE_UNSET = object()
|
||
|
||
|
||
def _emit(event: str, data: object, *, room: str | None = None) -> None:
|
||
try:
|
||
socketio = get_socketio()
|
||
socketio.emit(event, data, room=room)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
_task_scheduler = None
|
||
_task_scheduler_lock = threading.Lock()
|
||
|
||
|
||
def get_task_scheduler() -> TaskScheduler:
|
||
"""获取全局任务调度器(单例)"""
|
||
global _task_scheduler
|
||
with _task_scheduler_lock:
|
||
if _task_scheduler is None:
|
||
try:
|
||
max_queue_size = int(os.environ.get("TASK_QUEUE_MAXSIZE", "1000"))
|
||
except Exception:
|
||
max_queue_size = 1000
|
||
_task_scheduler = TaskScheduler(
|
||
max_global=max_concurrent_global,
|
||
max_per_user=max_concurrent_per_account,
|
||
max_queue_size=max_queue_size,
|
||
run_task_fn=run_task,
|
||
)
|
||
return _task_scheduler
|
||
|
||
|
||
def submit_account_task(
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
enable_screenshot: bool = True,
|
||
source: str = "manual",
|
||
retry_count: int = 0,
|
||
done_callback=None,
|
||
):
|
||
"""统一入口:提交账号任务进入队列"""
|
||
account = safe_get_account(user_id, account_id)
|
||
if not account:
|
||
return False, "账号不存在"
|
||
if getattr(account, "is_running", False):
|
||
return False, "任务已在运行中"
|
||
|
||
try:
|
||
is_vip_user = bool(database.is_user_vip(user_id))
|
||
except Exception:
|
||
is_vip_user = False
|
||
|
||
account.is_running = True
|
||
account.should_stop = False
|
||
account.status = "排队中" + (" (VIP)" if is_vip_user else "")
|
||
|
||
safe_set_task_status(
|
||
account_id,
|
||
{
|
||
"user_id": user_id,
|
||
"username": account.username,
|
||
"status": "排队中",
|
||
"detail_status": "等待资源" + (" [VIP优先]" if is_vip_user else ""),
|
||
"browse_type": browse_type,
|
||
"start_time": time.time(),
|
||
"source": source,
|
||
"progress": {"items": 0, "attachments": 0},
|
||
"is_vip": is_vip_user,
|
||
},
|
||
)
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
|
||
scheduler = get_task_scheduler()
|
||
ok, message = scheduler.submit_task(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
enable_screenshot=enable_screenshot,
|
||
source=source,
|
||
retry_count=retry_count,
|
||
is_vip=is_vip_user,
|
||
done_callback=done_callback,
|
||
)
|
||
if not ok:
|
||
account.is_running = False
|
||
account.status = "未开始"
|
||
safe_remove_task_status(account_id)
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
return False, message
|
||
|
||
log_to_client(message + (" [VIP优先]" if is_vip_user else ""), user_id, account_id)
|
||
return True, message
|
||
|
||
|
||
def _account_display_name(account) -> str:
|
||
return account.remark if account.remark else account.username
|
||
|
||
|
||
def _record_batch_result(batch_id, account, total_items: int, total_attachments: int) -> None:
|
||
if not batch_id:
|
||
return
|
||
_batch_task_record_result(
|
||
batch_id=batch_id,
|
||
account_name=_account_display_name(account),
|
||
screenshot_path=None,
|
||
total_items=total_items,
|
||
total_attachments=total_attachments,
|
||
)
|
||
|
||
|
||
def _close_account_automation(account, *, on_error=None) -> bool:
|
||
automation = getattr(account, "automation", None)
|
||
if not automation:
|
||
return False
|
||
|
||
closed = False
|
||
try:
|
||
automation.close()
|
||
closed = True
|
||
except Exception as e:
|
||
if on_error:
|
||
try:
|
||
on_error(e)
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
account.automation = None
|
||
|
||
return closed
|
||
|
||
|
||
def _create_task_log(
|
||
*,
|
||
user_id: int,
|
||
account_id: str,
|
||
account,
|
||
browse_type: str,
|
||
status: str,
|
||
total_items: int,
|
||
total_attachments: int,
|
||
error_message: str,
|
||
duration: int,
|
||
source=_SOURCE_UNSET,
|
||
) -> None:
|
||
payload = {
|
||
"user_id": user_id,
|
||
"account_id": account_id,
|
||
"username": account.username,
|
||
"browse_type": browse_type,
|
||
"status": status,
|
||
"total_items": total_items,
|
||
"total_attachments": total_attachments,
|
||
"error_message": error_message,
|
||
"duration": duration,
|
||
}
|
||
if source is not _SOURCE_UNSET:
|
||
payload["source"] = source
|
||
database.create_task_log(**payload)
|
||
|
||
|
||
def _handle_stop_requested(
|
||
*,
|
||
account,
|
||
user_id: int,
|
||
account_id: str,
|
||
batch_id,
|
||
remove_task_status: bool = False,
|
||
record_batch: bool = False,
|
||
) -> bool:
|
||
if not account.should_stop:
|
||
return False
|
||
|
||
log_to_client("任务已取消", user_id, account_id)
|
||
account.status = "已停止"
|
||
account.is_running = False
|
||
if remove_task_status:
|
||
safe_remove_task_status(account_id)
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
|
||
if record_batch and batch_id:
|
||
_record_batch_result(batch_id=batch_id, account=account, total_items=0, total_attachments=0)
|
||
return True
|
||
|
||
|
||
def _resolve_proxy_config(user_id: int, account_id: str, account):
|
||
proxy_config = None
|
||
system_config = database.get_system_config()
|
||
if system_config.get("proxy_enabled") != 1:
|
||
return proxy_config
|
||
|
||
proxy_api_url = system_config.get("proxy_api_url", "").strip()
|
||
if not proxy_api_url:
|
||
log_to_client("⚠ 代理已启用但未配置API地址", user_id, account_id)
|
||
return proxy_config
|
||
|
||
log_to_client("正在获取代理IP...", user_id, account_id)
|
||
proxy_server = get_proxy_from_api(proxy_api_url, max_retries=3)
|
||
if proxy_server:
|
||
proxy_config = {"server": proxy_server}
|
||
log_to_client(f"[OK] 将使用代理: {proxy_server}", user_id, account_id)
|
||
account.proxy_config = proxy_config
|
||
else:
|
||
log_to_client("✗ 代理获取失败,将不使用代理继续", user_id, account_id)
|
||
return proxy_config
|
||
|
||
|
||
def _refresh_account_remark(api_browser, account, user_id: int, account_id: str) -> None:
|
||
if account.remark:
|
||
return
|
||
try:
|
||
real_name = api_browser.get_real_name()
|
||
if not real_name:
|
||
return
|
||
account.remark = real_name
|
||
database.update_account_remark(account_id, real_name)
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
logger.info(f"[自动备注] 账号 {account.username} 自动设置备注为: {real_name}")
|
||
except Exception as e:
|
||
logger.warning(f"[自动备注] 获取姓名失败: {e}")
|
||
|
||
|
||
def _handle_login_failed(
|
||
*,
|
||
account,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
task_start_time: float,
|
||
task_id: str,
|
||
checkpoint_mgr,
|
||
time_module,
|
||
) -> None:
|
||
error_message = "登录失败"
|
||
log_to_client(f"❌ {error_message}", user_id, account_id)
|
||
|
||
is_suspended = database.increment_account_login_fail(account_id, error_message)
|
||
if is_suspended:
|
||
log_to_client("⚠ 该账号连续3次密码错误,已自动暂停", user_id, account_id)
|
||
log_to_client("请在前台修改密码后才能继续使用", user_id, account_id)
|
||
|
||
retry_action = checkpoint_mgr.record_error(task_id, error_message)
|
||
if retry_action == "paused":
|
||
logger.warning(f"[断点] 任务 {task_id} 已暂停(登录失败)")
|
||
|
||
account.status = "登录失败"
|
||
account.is_running = False
|
||
_create_task_log(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
account=account,
|
||
browse_type=browse_type,
|
||
status="failed",
|
||
total_items=0,
|
||
total_attachments=0,
|
||
error_message=error_message,
|
||
duration=int(time_module.time() - task_start_time),
|
||
source=source,
|
||
)
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
|
||
|
||
def _execute_single_attempt(
|
||
*,
|
||
account,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
checkpoint_mgr,
|
||
task_id: str,
|
||
task_start_time: float,
|
||
time_module,
|
||
):
|
||
proxy_config = _resolve_proxy_config(user_id=user_id, account_id=account_id, account=account)
|
||
|
||
checkpoint_mgr.update_stage(task_id, TaskStage.STARTING, progress_percent=10)
|
||
|
||
def custom_log(message: str):
|
||
log_to_client(message, user_id, account_id)
|
||
|
||
log_to_client("开始登录...", user_id, account_id)
|
||
safe_update_task_status(account_id, {"detail_status": "正在登录"})
|
||
checkpoint_mgr.update_stage(task_id, TaskStage.LOGGING_IN, progress_percent=25)
|
||
|
||
with APIBrowser(log_callback=custom_log, proxy_config=proxy_config) as api_browser:
|
||
if not api_browser.login(account.username, account.password):
|
||
_handle_login_failed(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
task_start_time=task_start_time,
|
||
task_id=task_id,
|
||
checkpoint_mgr=checkpoint_mgr,
|
||
time_module=time_module,
|
||
)
|
||
return "login_failed", None
|
||
|
||
log_to_client("[OK] 首次登录成功,刷新登录时间...", user_id, account_id)
|
||
|
||
if api_browser.login(account.username, account.password):
|
||
log_to_client("[OK] 二次登录成功!", user_id, account_id)
|
||
else:
|
||
log_to_client("⚠ 二次登录失败,继续使用首次登录状态", user_id, account_id)
|
||
|
||
api_browser.save_cookies_for_screenshot(account.username)
|
||
database.reset_account_login_status(account_id)
|
||
_refresh_account_remark(api_browser, account, user_id, account_id)
|
||
|
||
safe_update_task_status(account_id, {"detail_status": "正在浏览"})
|
||
log_to_client(f"开始浏览 '{browse_type}' 内容...", user_id, account_id)
|
||
account.total_items = 0
|
||
safe_update_task_status(account_id, {"progress": {"items": 0, "attachments": 0}})
|
||
|
||
def should_stop():
|
||
return account.should_stop
|
||
|
||
def on_browse_progress(progress: dict):
|
||
try:
|
||
total_items = int(progress.get("total_items") or 0)
|
||
browsed_items = int(progress.get("browsed_items") or 0)
|
||
if total_items > 0:
|
||
account.total_items = total_items
|
||
safe_update_task_status(account_id, {"progress": {"items": browsed_items, "attachments": 0}})
|
||
except Exception:
|
||
pass
|
||
|
||
checkpoint_mgr.update_stage(task_id, TaskStage.BROWSING, progress_percent=50)
|
||
result = api_browser.browse_content(
|
||
browse_type=browse_type,
|
||
should_stop_callback=should_stop,
|
||
progress_callback=on_browse_progress,
|
||
)
|
||
return "ok", result
|
||
|
||
|
||
def _record_success_without_screenshot(
|
||
*,
|
||
account,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
task_start_time: float,
|
||
result,
|
||
batch_id,
|
||
time_module,
|
||
) -> bool:
|
||
_create_task_log(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
account=account,
|
||
browse_type=browse_type,
|
||
status="success",
|
||
total_items=result.total_items,
|
||
total_attachments=result.total_attachments,
|
||
error_message="",
|
||
duration=int(time_module.time() - task_start_time),
|
||
source=source,
|
||
)
|
||
if batch_id:
|
||
_record_batch_result(
|
||
batch_id=batch_id,
|
||
account=account,
|
||
total_items=result.total_items,
|
||
total_attachments=result.total_attachments,
|
||
)
|
||
return True
|
||
|
||
if source and source.startswith("user_scheduled"):
|
||
try:
|
||
user_info = database.get_user_by_id(user_id)
|
||
if user_info and user_info.get("email") and database.get_user_email_notify(user_id):
|
||
email_service.send_task_complete_email_async(
|
||
user_id=user_id,
|
||
email=user_info["email"],
|
||
username=user_info["username"],
|
||
account_name=_account_display_name(account),
|
||
browse_type=browse_type,
|
||
total_items=result.total_items,
|
||
total_attachments=result.total_attachments,
|
||
screenshot_path=None,
|
||
log_callback=lambda msg: log_to_client(msg, user_id, account_id),
|
||
)
|
||
except Exception as email_error:
|
||
logger.warning(f"发送任务完成邮件失败: {email_error}")
|
||
|
||
return False
|
||
|
||
|
||
def _is_timeout_error(error_msg: str) -> bool:
|
||
return "Timeout" in error_msg or "timeout" in error_msg
|
||
|
||
|
||
def _record_failed_task_log(
|
||
*,
|
||
account,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
total_items: int,
|
||
total_attachments: int,
|
||
error_message: str,
|
||
task_start_time: float,
|
||
time_module,
|
||
) -> None:
|
||
account.status = "出错"
|
||
_create_task_log(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
account=account,
|
||
browse_type=browse_type,
|
||
status="failed",
|
||
total_items=total_items,
|
||
total_attachments=total_attachments,
|
||
error_message=error_message,
|
||
duration=int(time_module.time() - task_start_time),
|
||
source=source,
|
||
)
|
||
|
||
|
||
def _handle_failed_browse_result(
|
||
*,
|
||
account,
|
||
result,
|
||
error_msg: str,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
attempt: int,
|
||
max_attempts: int,
|
||
task_start_time: float,
|
||
time_module,
|
||
) -> str:
|
||
if _is_timeout_error(error_msg):
|
||
log_to_client(f"⚠ 检测到超时错误: {error_msg}", user_id, account_id)
|
||
|
||
close_ok = _close_account_automation(
|
||
account,
|
||
on_error=lambda e: logger.debug(f"关闭超时浏览器实例失败: {e}"),
|
||
)
|
||
if close_ok:
|
||
log_to_client("已关闭超时的浏览器实例", user_id, account_id)
|
||
|
||
if attempt < max_attempts:
|
||
log_to_client(f"⚠ 代理可能速度过慢,将换新IP重试 ({attempt}/{max_attempts})", user_id, account_id)
|
||
time_module.sleep(2)
|
||
return "continue"
|
||
|
||
log_to_client(f"❌ 已达到最大重试次数({max_attempts}),任务失败", user_id, account_id)
|
||
_record_failed_task_log(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
total_items=result.total_items,
|
||
total_attachments=result.total_attachments,
|
||
error_message=f"重试{max_attempts}次后仍失败: {error_msg}",
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
return "break"
|
||
|
||
log_to_client(f"浏览出错: {error_msg}", user_id, account_id)
|
||
_record_failed_task_log(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
total_items=result.total_items,
|
||
total_attachments=result.total_attachments,
|
||
error_message=error_msg,
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
return "break"
|
||
|
||
|
||
def _handle_attempt_exception(
|
||
*,
|
||
account,
|
||
error_msg: str,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
attempt: int,
|
||
max_attempts: int,
|
||
task_start_time: float,
|
||
time_module,
|
||
) -> str:
|
||
_close_account_automation(
|
||
account,
|
||
on_error=lambda e: logger.debug(f"关闭浏览器实例失败: {e}"),
|
||
)
|
||
|
||
if _is_timeout_error(error_msg):
|
||
log_to_client(f"⚠ 执行超时: {error_msg}", user_id, account_id)
|
||
if attempt < max_attempts:
|
||
log_to_client(f"⚠ 将换新IP重试 ({attempt}/{max_attempts})", user_id, account_id)
|
||
time_module.sleep(2)
|
||
return "continue"
|
||
|
||
log_to_client(f"❌ 已达到最大重试次数({max_attempts}),任务失败", user_id, account_id)
|
||
_record_failed_task_log(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
total_items=account.total_items,
|
||
total_attachments=account.total_attachments,
|
||
error_message=f"重试{max_attempts}次后仍失败: {error_msg}",
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
return "break"
|
||
|
||
log_to_client(f"任务执行异常: {error_msg}", user_id, account_id)
|
||
_record_failed_task_log(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
total_items=account.total_items,
|
||
total_attachments=account.total_attachments,
|
||
error_message=error_msg,
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
return "break"
|
||
|
||
|
||
def _schedule_auto_retry(
|
||
*,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
enable_screenshot: bool,
|
||
source: str,
|
||
retry_count: int,
|
||
) -> None:
|
||
def delayed_retry_submit():
|
||
fresh_account = safe_get_account(user_id, account_id)
|
||
if not fresh_account:
|
||
log_to_client("自动重试取消: 账户不存在", user_id, account_id)
|
||
return
|
||
if fresh_account.should_stop:
|
||
log_to_client("自动重试取消: 任务已被停止", user_id, account_id)
|
||
return
|
||
|
||
log_to_client(f"🔄 开始第 {retry_count + 1} 次自动重试...", user_id, account_id)
|
||
ok, msg = submit_account_task(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
enable_screenshot=enable_screenshot,
|
||
source=source,
|
||
retry_count=retry_count + 1,
|
||
)
|
||
if not ok:
|
||
log_to_client(f"自动重试提交失败: {msg}", user_id, account_id)
|
||
|
||
try:
|
||
threading.Timer(5, delayed_retry_submit).start()
|
||
except Exception:
|
||
delayed_retry_submit()
|
||
|
||
|
||
def _finalize_task_run(
|
||
*,
|
||
account,
|
||
user_id: int,
|
||
account_id: str,
|
||
browse_type: str,
|
||
source: str,
|
||
enable_screenshot: bool,
|
||
retry_count: int,
|
||
max_auto_retry: int,
|
||
task_start_time: float,
|
||
result,
|
||
batch_id,
|
||
batch_recorded: bool,
|
||
) -> None:
|
||
final_status = str(account.status or "")
|
||
account.is_running = False
|
||
screenshot_submitted = False
|
||
|
||
_close_account_automation(
|
||
account,
|
||
on_error=lambda e: log_to_client(f"关闭主任务浏览器时出错: {str(e)}", user_id, account_id),
|
||
)
|
||
|
||
safe_remove_task(account_id)
|
||
safe_remove_task_status(account_id)
|
||
|
||
if final_status == "已完成" and not account.should_stop:
|
||
account.status = "已完成"
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
|
||
if enable_screenshot:
|
||
log_to_client("等待2秒后开始截图...", user_id, account_id)
|
||
account.status = "等待截图"
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
|
||
safe_set_task_status(
|
||
account_id,
|
||
{
|
||
"user_id": user_id,
|
||
"username": account.username,
|
||
"status": "排队中",
|
||
"detail_status": "等待截图资源",
|
||
"browse_type": browse_type,
|
||
"start_time": time.time(),
|
||
"source": source,
|
||
"progress": {
|
||
"items": result.total_items if result else 0,
|
||
"attachments": result.total_attachments if result else 0,
|
||
},
|
||
},
|
||
)
|
||
|
||
browse_result_dict = {
|
||
"total_items": result.total_items if result else 0,
|
||
"total_attachments": result.total_attachments if result else 0,
|
||
}
|
||
screenshot_submitted = True
|
||
threading.Thread(
|
||
target=take_screenshot_for_account,
|
||
args=(user_id, account_id, browse_type, source, task_start_time, browse_result_dict),
|
||
daemon=True,
|
||
).start()
|
||
else:
|
||
account.status = "未开始"
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
log_to_client("截图功能已禁用,跳过截图", user_id, account_id)
|
||
else:
|
||
if final_status == "出错" and retry_count < max_auto_retry:
|
||
log_to_client(f"⚠ 任务执行失败,5秒后自动重试 ({retry_count + 1}/{max_auto_retry})...", user_id, account_id)
|
||
account.status = "等待重试"
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
_schedule_auto_retry(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
enable_screenshot=enable_screenshot,
|
||
source=source,
|
||
retry_count=retry_count,
|
||
)
|
||
elif final_status in ["登录失败", "出错"]:
|
||
account.status = final_status
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
else:
|
||
account.status = "未开始"
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
|
||
if batch_id and (not screenshot_submitted) and (not batch_recorded) and account.status != "等待重试":
|
||
_record_batch_result(
|
||
batch_id=batch_id,
|
||
account=account,
|
||
total_items=getattr(account, "total_items", 0) or 0,
|
||
total_attachments=getattr(account, "total_attachments", 0) or 0,
|
||
)
|
||
|
||
|
||
def run_task(user_id, account_id, browse_type, enable_screenshot=True, source="manual", retry_count=0):
|
||
"""运行自动化任务
|
||
|
||
Args:
|
||
retry_count: 当前重试次数,用于自动重试机制(最多重试2次)
|
||
"""
|
||
MAX_AUTO_RETRY = 2 # 最大自动重试次数
|
||
LoggerAdapter("app", {"user_id": user_id, "account_id": account_id, "source": source}).debug(
|
||
f"run_task enable_screenshot={enable_screenshot} ({type(enable_screenshot).__name__}), retry={retry_count}"
|
||
)
|
||
|
||
account = safe_get_account(user_id, account_id)
|
||
if not account:
|
||
return
|
||
batch_id = _get_batch_id_from_source(source)
|
||
batch_recorded = False
|
||
|
||
checkpoint_mgr = get_checkpoint_mgr()
|
||
|
||
import time as time_module
|
||
|
||
task_start_time = time_module.time()
|
||
result = None
|
||
|
||
try:
|
||
if _handle_stop_requested(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
batch_id=batch_id,
|
||
remove_task_status=True,
|
||
record_batch=True,
|
||
):
|
||
return
|
||
|
||
try:
|
||
if _handle_stop_requested(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
batch_id=batch_id,
|
||
):
|
||
return
|
||
|
||
task_id = checkpoint_mgr.create_checkpoint(
|
||
user_id=user_id, account_id=account_id, username=account.username, browse_type=browse_type
|
||
)
|
||
logger.info(f"[断点] 任务 {task_id} 已创建")
|
||
|
||
task_start_time = time_module.time()
|
||
|
||
account.status = "运行中"
|
||
_emit("account_update", account.to_dict(), room=f"user_{user_id}")
|
||
account.last_browse_type = browse_type
|
||
|
||
safe_update_task_status(
|
||
account_id, {"status": "运行中", "detail_status": "初始化", "start_time": task_start_time}
|
||
)
|
||
|
||
max_attempts = 3
|
||
|
||
for attempt in range(1, max_attempts + 1):
|
||
try:
|
||
if attempt > 1:
|
||
log_to_client(f"🔄 第 {attempt} 次尝试(共{max_attempts}次)...", user_id, account_id)
|
||
|
||
attempt_status, result = _execute_single_attempt(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
checkpoint_mgr=checkpoint_mgr,
|
||
task_id=task_id,
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
if attempt_status == "login_failed":
|
||
return
|
||
|
||
account.total_items = result.total_items
|
||
account.total_attachments = result.total_attachments
|
||
|
||
if result.success:
|
||
log_to_client(
|
||
f"浏览完成! 共 {result.total_items} 条内容,{result.total_attachments} 个附件",
|
||
user_id,
|
||
account_id,
|
||
)
|
||
safe_update_task_status(
|
||
account_id,
|
||
{
|
||
"detail_status": "浏览完成",
|
||
"progress": {"items": result.total_items, "attachments": result.total_attachments},
|
||
},
|
||
)
|
||
account.status = "已完成"
|
||
checkpoint_mgr.update_stage(task_id, TaskStage.COMPLETING, progress_percent=95)
|
||
checkpoint_mgr.complete_task(task_id, success=True)
|
||
logger.info(f"[断点] 任务 {task_id} 已完成")
|
||
|
||
if not enable_screenshot:
|
||
batch_recorded = _record_success_without_screenshot(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
task_start_time=task_start_time,
|
||
result=result,
|
||
batch_id=batch_id,
|
||
time_module=time_module,
|
||
)
|
||
break
|
||
|
||
error_msg = result.error_message
|
||
action = _handle_failed_browse_result(
|
||
account=account,
|
||
result=result,
|
||
error_msg=error_msg,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
attempt=attempt,
|
||
max_attempts=max_attempts,
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
if action == "continue":
|
||
continue
|
||
break
|
||
|
||
except Exception as retry_error:
|
||
error_msg = str(retry_error)
|
||
action = _handle_attempt_exception(
|
||
account=account,
|
||
error_msg=error_msg,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
attempt=attempt,
|
||
max_attempts=max_attempts,
|
||
task_start_time=task_start_time,
|
||
time_module=time_module,
|
||
)
|
||
if action == "continue":
|
||
continue
|
||
break
|
||
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
log_to_client(f"任务执行出错: {error_msg}", user_id, account_id)
|
||
account.status = "出错"
|
||
_create_task_log(
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
account=account,
|
||
browse_type=browse_type,
|
||
status="failed",
|
||
total_items=account.total_items,
|
||
total_attachments=account.total_attachments,
|
||
error_message=error_msg,
|
||
duration=int(time_module.time() - task_start_time),
|
||
source=source,
|
||
)
|
||
|
||
finally:
|
||
_finalize_task_run(
|
||
account=account,
|
||
user_id=user_id,
|
||
account_id=account_id,
|
||
browse_type=browse_type,
|
||
source=source,
|
||
enable_screenshot=enable_screenshot,
|
||
retry_count=retry_count,
|
||
max_auto_retry=MAX_AUTO_RETRY,
|
||
task_start_time=task_start_time,
|
||
result=result,
|
||
batch_id=batch_id,
|
||
batch_recorded=batch_recorded,
|
||
)
|
||
|
||
finally:
|
||
pass
|