diff --git a/apis/task_queue.py b/apis/task_queue.py new file mode 100644 index 00000000..73f2689d --- /dev/null +++ b/apis/task_queue.py @@ -0,0 +1,145 @@ +"""任务队列管理API""" +from fastapi import APIRouter, Depends, Query +from typing import Optional +from core.auth import get_current_user_or_ak +from core.queue.queue import TaskQueue +from core.task.task import TaskScheduler +from .base import success_response, error_response +from core.log import logger + +router = APIRouter(prefix="/task-queue", tags=["任务队列"]) + +@router.get("/status", summary="获取任务队列状态") +async def get_queue_status( + current_user: dict = Depends(get_current_user_or_ak) +): + """ + 获取任务队列的详细状态信息 + + 返回: + - tag: 队列标签 + - is_running: 是否运行中 + - pending_count: 待执行任务数 + - pending_tasks: 待执行任务列表 + - current_task: 当前执行的任务 + - history_count: 历史记录总数 + - recent_history: 最近执行记录 + """ + try: + status = TaskQueue.get_detailed_status() + logger.info(f"Queue status: {status}") + return success_response(data=status) + except Exception as e: + logger.error(f"Get queue status error: {str(e)}") + return error_response(code=500, message=str(e)) + +@router.get("/history", summary="获取任务执行历史") +async def get_queue_history( + limit: int = Query(20, ge=1, le=100, description="返回记录数量"), + current_user: dict = Depends(get_current_user_or_ak) +): + """ + 获取任务执行历史记录 + + 参数: + limit: 返回记录数量,默认20条 + """ + try: + status = TaskQueue.get_detailed_status() + history = status.get('recent_history', [])[:limit] + return success_response(data={ + 'history': history, + 'total': status.get('history_count', 0) + }) + except Exception as e: + return error_response(code=500, message=str(e)) + +@router.post("/clear", summary="清空任务队列") +async def clear_queue( + current_user: dict = Depends(get_current_user_or_ak) +): + """ + 清空任务队列中的所有待执行任务 + + 注意: 正在执行的任务不会被中断 + """ + try: + TaskQueue.clear_queue() + return success_response(message="队列已清空") + except Exception as e: + return error_response(code=500, message=str(e)) + +@router.post("/history/clear", summary="清空任务历史") +async def clear_history( + current_user: dict = Depends(get_current_user_or_ak) +): + """ + 清空任务执行历史记录 + """ + try: + TaskQueue.clear_history() + return success_response(message="任务历史已清空") + except Exception as e: + return error_response(code=500, message=str(e)) + +@router.get("/scheduler/status", summary="获取调度器状态") +async def get_scheduler_status( + current_user: dict = Depends(get_current_user_or_ak) +): + """ + 获取定时任务调度器的状态信息 + + 返回: + - running: 调度器是否运行中 + - job_count: 定时任务数量 + - next_run_times: 各任务下次执行时间 + """ + try: + # 从 jobs.mps 导入调度器实例 + from jobs.mps import scheduler + status = scheduler.get_scheduler_status() + logger.info(f"Scheduler status: {status}") + return success_response(data=status) + except ImportError as e: + logger.error(f"Import scheduler error: {str(e)}") + return success_response(data={ + 'running': False, + 'job_count': 0, + 'next_run_times': [] + }) + except Exception as e: + logger.error(f"Get scheduler status error: {str(e)}") + return error_response(code=500, message=str(e)) + +@router.get("/scheduler/jobs", summary="获取定时任务列表") +async def get_scheduler_jobs( + current_user: dict = Depends(get_current_user_or_ak) +): + """ + 获取所有定时任务的详细信息 + """ + try: + from jobs.mps import scheduler + job_ids = scheduler.get_job_ids() + jobs = [] + for job_id in job_ids: + try: + details = scheduler.get_job_details(job_id) + jobs.append(details) + except Exception as job_error: + logger.warning(f"Get job {job_id} details error: {str(job_error)}") + jobs.append({'id': job_id, 'error': '获取详情失败'}) + logger.info(f"Scheduler jobs: {len(jobs)} jobs") + return success_response(data={ + 'jobs': jobs, + 'total': len(jobs) + }) + except ImportError as e: + logger.error(f"Import scheduler error: {str(e)}") + return success_response(data={ + 'jobs': [], + 'total': 0 + }) + except Exception as e: + logger.error(f"Get scheduler jobs error: {str(e)}") + return error_response(code=500, message=str(e)) diff --git a/core/queue/queue.py b/core/queue/queue.py index 2abaa58c..82d25fd4 100644 --- a/core/queue/queue.py +++ b/core/queue/queue.py @@ -3,7 +3,28 @@ import threading import time import gc from typing import Callable, Any, Optional +from datetime import datetime +from dataclasses import dataclass, field from core.print import print_error, print_info, print_warning, print_success + +@dataclass +class TaskRecord: + """任务执行记录""" + task_name: str + start_time: str + end_time: Optional[str] = None + duration: Optional[float] = None + status: str = "running" # running, completed, failed + error: Optional[str] = None + +@dataclass +class TaskItem: + """队列中的任务项""" + task_name: str + args: tuple = field(default_factory=tuple) + kwargs: dict = field(default_factory=dict) + add_time: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + class TaskQueueManager: """任务队列管理器,用于管理和执行排队任务""" @@ -13,6 +34,13 @@ class TaskQueueManager: self._lock = threading.Lock() self._is_running = False self.tag=tag + # 任务历史记录(最近100条) + self._history: list[TaskRecord] = [] + self._history_max_size = 100 + # 当前执行的任务 + self._current_task: Optional[TaskRecord] = None + # 待执行任务列表(用于展示) + self._pending_items: list[TaskItem] = [] def add_task(self, task: Callable[..., Any], *args: Any, **kwargs: Any) -> None: """添加任务到队列 @@ -24,6 +52,13 @@ class TaskQueueManager: """ with self._lock: self._queue.put((task, args, kwargs)) + # 记录待执行任务 + task_name = getattr(task, '__name__', str(task)) + self._pending_items.append(TaskItem( + task_name=task_name, + args=args, + kwargs=kwargs + )) print_success(f"{self.tag}队列任务添加成功\n") def run_task_background(self)->None: threading.Thread(target=self.run_tasks, daemon=True).start() @@ -46,17 +81,55 @@ class TaskQueueManager: # 阻塞获取任务,避免CPU空转 task, args, kwargs = self._queue.get(timeout=timeout) + # 从待执行列表中移除 + with self._lock: + if self._pending_items: + self._pending_items.pop(0) + + # 记录任务开始 + task_name = getattr(task, '__name__', str(task)) + with self._lock: + self._current_task = TaskRecord( + task_name=task_name, + start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ) + try: # 记录任务开始时间 start_time = time.time() task(*args, **kwargs) # 记录任务执行时间 duration = time.time() - start_time + + # 更新当前任务记录 + with self._lock: + if self._current_task: + self._current_task.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self._current_task.duration = duration + self._current_task.status = "completed" + print_info(f"\n任务执行完成,耗时: {duration:.2f}秒") except Exception as e: + # 更新当前任务记录为失败 + with self._lock: + if self._current_task: + self._current_task.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self._current_task.duration = time.time() - start_time + self._current_task.status = "failed" + self._current_task.error = str(e) + print_error(f"队列任务执行失败: {e}") # raise finally: + # 保存到历史记录 + with self._lock: + if self._current_task: + self._history.append(self._current_task) + # 限制历史记录大小 + if len(self._history) > self._history_max_size: + self._history = self._history[-self._history_max_size:] + self._current_task = None + # 确保任务完成标记和资源释放 self._queue.task_done() # 强制垃圾回收 @@ -92,6 +165,61 @@ class TaskQueueManager: 'is_running': self._is_running, 'pending_tasks': self._queue.qsize() } + + def get_detailed_status(self) -> dict: + """ + 获取队列的详细状态信息 + + 返回: + dict: 包含详细队列信息的字典 + """ + with self._lock: + # 转换待执行任务为可序列化格式 + pending_list = [] + for item in self._pending_items: + pending_list.append({ + 'task_name': item.task_name, + 'args': str(item.args) if item.args else '', + 'kwargs': str(item.kwargs) if item.kwargs else '', + 'add_time': item.add_time + }) + + # 转换历史记录为可序列化格式 + history_list = [] + for record in self._history[-20:]: # 只返回最近20条 + history_list.append({ + 'task_name': record.task_name, + 'start_time': record.start_time, + 'end_time': record.end_time, + 'duration': round(record.duration, 2) if record.duration else None, + 'status': record.status, + 'error': record.error + }) + + # 当前执行任务 + current = None + if self._current_task: + current = { + 'task_name': self._current_task.task_name, + 'start_time': self._current_task.start_time, + 'status': self._current_task.status + } + + return { + 'tag': self.tag, + 'is_running': self._is_running, + 'pending_count': self._queue.qsize(), + 'pending_tasks': pending_list, + 'current_task': current, + 'history_count': len(self._history), + 'recent_history': history_list + } + + def clear_history(self) -> None: + """清空任务历史记录""" + with self._lock: + self._history.clear() + print_success("任务历史记录已清空") def clear_queue(self) -> None: """清空队列中的所有任务""" @@ -102,6 +230,7 @@ class TaskQueueManager: self._queue.task_done() except queue.Empty: break + self._pending_items.clear() print_success("队列已清空") def delete_queue(self) -> None: diff --git a/core/wx/base.py b/core/wx/base.py index ae8007eb..7fe9e7a5 100644 --- a/core/wx/base.py +++ b/core/wx/base.py @@ -251,6 +251,10 @@ class WxGather: def Start(self,mp_id=None): self.articles=[] self.get_token() + from driver.success import getLockStatus + if getLockStatus(): + self.Error("正在切换帐号码,请等待切换完成") + return if self.token=="" or self.token is None: self.Error("请先扫码登录公众号平台") return diff --git a/driver/cookies.py b/driver/cookies.py index 016618d4..3fed09e3 100644 --- a/driver/cookies.py +++ b/driver/cookies.py @@ -4,23 +4,86 @@ def expire(cookies:any) : raise TypeError("cookies参数必须是列表类型") cookie_expiry=None + + # 优先检查的 cookie 名称列表(按优先级排序) + # slave_sid 是微信公众平台的会话 cookie,但可能不存在或没有 expires + # 添加更多可能携带有效期的 cookie + priority_cookies = ['slave_sid', 'slave_user', 'bizuin', 'uin', 'pass_ticket'] + + # 首先尝试从优先列表中查找 + for priority_name in priority_cookies: + for cookie in cookies: + if not isinstance(cookie, dict): + continue + if cookie.get('name') == priority_name: + expiry = _extract_expiry_from_cookie(cookie) + if expiry: + return expiry + + # 如果优先列表都没找到,遍历所有 cookie 找有 expires 的 for cookie in cookies: if not isinstance(cookie, dict): continue - if cookie['name'] == 'slave_sid' and 'expires' in str(cookie): - try: - expiry_time = float(cookie['expires']) - remaining_time = expiry_time - time.time() - if remaining_time > 0: - cookie_expiry = { - 'expiry_timestamp': expiry_time, - 'remaining_seconds': int(remaining_time), - 'expiry_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expiry_time)) - } - break - except ValueError: - print(f"slave_sid 的过期时间戳无效: {cookie['expiry']}") - break - return cookie_expiry + expiry = _extract_expiry_from_cookie(cookie) + if expiry: + return expiry + + # 如果所有 cookie 都没有有效过期时间,返回一个默认值 + # 表示当前会话有效,但不清楚具体过期时间 + # 这样不会导致登录状态被标记为失败 + from core.print import print_warning + print_warning("未能从 cookies 中提取有效过期时间,使用默认2小时有效期") + default_expiry = time.time() + 7200 # 默认2小时 + return { + 'expiry_timestamp': default_expiry, + 'remaining_seconds': 7200, + 'expiry_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(default_expiry)) + } + +def _extract_expiry_from_cookie(cookie: dict): + """从单个 cookie 中提取过期时间""" + # 检查多种可能的过期时间字段名 + expiry_fields = ['expires', 'expiry', 'expire'] + expiry_time = None + + for field in expiry_fields: + if field in cookie: + try: + val = cookie[field] + # 处理不同类型的值 + if isinstance(val, (int, float)): + expiry_time = float(val) + elif isinstance(val, str): + # 尝试解析字符串 + if val.isdigit(): + expiry_time = float(val) + else: + # 可能是日期字符串,尝试解析 + import datetime + try: + # 尝试常见日期格式 + for fmt in ['%Y-%m-%d %H:%M:%S', '%a, %d-%b-%Y %H:%M:%S %Z', '%a, %d %b %Y %H:%M:%S %Z']: + try: + dt = datetime.datetime.strptime(val, fmt) + expiry_time = dt.timestamp() + break + except ValueError: + continue + except Exception: + pass + break + except (ValueError, TypeError) as e: + continue + + if expiry_time: + remaining_time = expiry_time - time.time() + if remaining_time > 0: + return { + 'expiry_timestamp': expiry_time, + 'remaining_seconds': int(remaining_time), + 'expiry_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expiry_time)) + } + + return None diff --git a/driver/success.py b/driver/success.py index 1e611e4c..fbddfee8 100644 --- a/driver/success.py +++ b/driver/success.py @@ -1,3 +1,5 @@ +from sqlalchemy.util import b + from .token import set_token from core.print import print_warning,print_success from core.redis_client import redis_client @@ -6,6 +8,7 @@ import json # 初始化全局变量(作为Redis不可用时的回退) WX_LOGIN_ED = True +WX_LOCKED_STATUS = False WX_LOGIN_INFO = None import threading @@ -15,6 +18,7 @@ login_lock = threading.Lock() # Redis key 常量 REDIS_KEY_STATUS = "werss:login:status" +REDIS_KEY_LOCK_STATUS = "werss:login:lock" def setStatus(status:bool): """设置登录状态,优先存储到Redis,失败则使用全局变量""" @@ -28,7 +32,33 @@ def setStatus(status:bool): # 同时更新全局变量作为回退 with login_lock: WX_LOGIN_ED = status - +def setLockStatus(status:bool): + """设置登录状态,优先存储到Redis,失败则使用全局变量""" + global WX_LOCKED_STATUS + # 尝试存储到Redis + if redis_client.is_connected: + try: + redis_client._client.set(REDIS_KEY_LOCK_STATUS, "1" if status else "0") + except Exception: + pass + # 同时更新全局变量作为回退 + with login_lock: + WX_LOCKED_STATUS = status +def getLockStatus(): + """获取登录状态,优先从Redis读取,失败则使用全局变量""" + global WX_LOCKED_STATUS + # 尝试从Redis读取 + if redis_client.is_connected: + try: + val = redis_client._client.get(REDIS_KEY_LOCK_STATUS) + if val is not None: + return val == "1" + except Exception: + pass + # 回退到全局变量 + with login_lock: + return WX_LOCKED_STATUS + def getStatus(): """获取登录状态,优先从Redis读取,失败则使用全局变量""" global WX_LOGIN_ED diff --git a/driver/token.py b/driver/token.py index 46f10188..99027472 100644 --- a/driver/token.py +++ b/driver/token.py @@ -3,7 +3,6 @@ from core.config import Config,cfg # 确保data目录和wx.lic文件存在 import os import json - from core.print import print_success, print_warning from core.redis_client import redis_client @@ -46,6 +45,8 @@ def set_token(data:any,ext_data:any=None): _save_to_local(token_data) print_success(f"Token:{data.get('token')} \n到期时间:{data.get('expiry')['expiry_time']}\n") + from driver.success import setLockStatus + setLockStatus(False) from jobs.notice import sys_notice # sys_notice(f"""WeRss授权成功 diff --git a/driver/wx.py b/driver/wx.py index 01239c92..adad7a21 100644 --- a/driver/wx.py +++ b/driver/wx.py @@ -10,7 +10,7 @@ from PIL import Image from .success import Success import time import os -from driver.success import getStatus +from driver.success import getStatus,getLockStatus,setLockStatus from driver.store import Store import re from threading import Timer, Lock @@ -241,7 +241,11 @@ class Wx: if not getStatus(): print_warning("登录状态检查失败") return None - + if getLockStatus(): + print_warning("正在切换帐号,请稍后") + return None + + setLockStatus(True) from driver.token import wx_cfg token = str(wx_cfg.get("token", "")) diff --git a/web.py b/web.py index 66d53d04..76e114c3 100644 --- a/web.py +++ b/web.py @@ -22,6 +22,7 @@ from apis.github_update import router as github_router from apis.cascade import router as cascade_router from apis.env_exception import router as env_exception_router from apis.filter_rule import router as filter_rule_router +from apis.task_queue import router as task_queue_router from views import router as views_router import apis import os @@ -94,7 +95,8 @@ api_router.include_router(tools_router) api_router.include_router(github_router) api_router.include_router(cascade_router) api_router.include_router(env_exception_router) -api_router.include_router(filter_rule_router) +api_router.include_router(filter_rule_router) +api_router.include_router(task_queue_router) resource_router = APIRouter(prefix="/static") resource_router.include_router(res_router) diff --git a/web_ui/src/api/taskQueue.ts b/web_ui/src/api/taskQueue.ts new file mode 100644 index 00000000..b2faf855 --- /dev/null +++ b/web_ui/src/api/taskQueue.ts @@ -0,0 +1,131 @@ +import http from './http'; + +export interface TaskRecord { + task_name: string; + start_time: string; + end_time: string | null; + duration: number | null; + status: 'running' | 'completed' | 'failed'; + error: string | null; +} + +export interface PendingTask { + task_name: string; + args: string; + kwargs: string; + add_time: string; +} + +export interface CurrentTask { + task_name: string; + start_time: string; + status: string; +} + +export interface QueueStatus { + tag: string; + is_running: boolean; + pending_count: number; + pending_tasks: PendingTask[]; + current_task: CurrentTask | null; + history_count: number; + recent_history: TaskRecord[]; +} + +export interface SchedulerJob { + id: string; + name: string; + trigger: string; + next_run_time: string | null; + last_run_time: string | null; +} + +export interface SchedulerStatus { + running: boolean; + job_count: number; + next_run_times: [string, string | null][]; +} + +export const getQueueStatus = async (): Promise => { + try { + const response = await http.get('/wx/task-queue/status'); + console.log('Queue status response:', response); + // http 拦截器已经解包了 response.data.data + // response 可能是 data 本身 + const data = response as any; + if (data && typeof data === 'object') { + return { + tag: data.tag || '', + is_running: data.is_running || false, + pending_count: data.pending_count || 0, + pending_tasks: data.pending_tasks || [], + current_task: data.current_task || null, + history_count: data.history_count || 0, + recent_history: data.recent_history || [], + }; + } + return { + tag: '', + is_running: false, + pending_count: 0, + pending_tasks: [], + current_task: null, + history_count: 0, + recent_history: [], + }; + } catch (error) { + console.error('Get queue status error:', error); + throw error; + } +}; + +export const getQueueHistory = async (limit: number = 20): Promise<{ history: TaskRecord[]; total: number }> => { + try { + const response = await http.get('/wx/task-queue/history', { params: { limit } }); + const data = response as any; + return { + history: data?.history || [], + total: data?.total || 0, + }; + } catch (error) { + console.error('Get queue history error:', error); + throw error; + } +}; + +export const clearQueue = async (): Promise => { + await http.post('/wx/task-queue/clear'); +}; + +export const clearHistory = async (): Promise => { + await http.post('/wx/task-queue/history/clear'); +}; + +export const getSchedulerStatus = async (): Promise => { + try { + const response = await http.get('/wx/task-queue/scheduler/status'); + const data = response as any; + return { + running: data?.running || false, + job_count: data?.job_count || 0, + next_run_times: data?.next_run_times || [], + }; + } catch (error) { + console.error('Get scheduler status error:', error); + throw error; + } +}; + +export const getSchedulerJobs = async (): Promise<{ jobs: SchedulerJob[]; total: number }> => { + try { + const response = await http.get('/wx/task-queue/scheduler/jobs'); + const data = response as any; + return { + jobs: data?.jobs || [], + total: data?.total || 0, + }; + } catch (error) { + console.error('Get scheduler jobs error:', error); + throw error; + } +}; diff --git a/web_ui/src/components/Layout/Navbar.vue b/web_ui/src/components/Layout/Navbar.vue index 6d03d7f4..d707acba 100644 --- a/web_ui/src/components/Layout/Navbar.vue +++ b/web_ui/src/components/Layout/Navbar.vue @@ -47,6 +47,12 @@ 系统信息 + + + 任务队列 + - - - - - - - - - - @@ -118,48 +98,6 @@ - - - - - - - - - - - - - - diff --git a/web_ui/src/views/TaskQueueView.vue b/web_ui/src/views/TaskQueueView.vue new file mode 100644 index 00000000..f7beee0f --- /dev/null +++ b/web_ui/src/views/TaskQueueView.vue @@ -0,0 +1,455 @@ + + + + +