mirror of
https://github.com/rachelos/we-mp-rss.git
synced 2026-08-30 18:01:55 +08:00
1.4.9-Fix
This commit is contained in:
@@ -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))
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
+78
-15
@@ -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
|
||||
|
||||
|
||||
|
||||
+31
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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授权成功
|
||||
|
||||
+6
-2
@@ -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", ""))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<QueueStatus> => {
|
||||
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<void> => {
|
||||
await http.post('/wx/task-queue/clear');
|
||||
};
|
||||
|
||||
export const clearHistory = async (): Promise<void> => {
|
||||
await http.post('/wx/task-queue/history/clear');
|
||||
};
|
||||
|
||||
export const getSchedulerStatus = async (): Promise<SchedulerStatus> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -47,6 +47,12 @@
|
||||
</template>
|
||||
系统信息
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/task-queue">
|
||||
<template #icon>
|
||||
<icon-list />
|
||||
</template>
|
||||
任务队列
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/access-keys">
|
||||
<template #icon>
|
||||
<icon-key />
|
||||
|
||||
@@ -14,6 +14,7 @@ import MessageTaskForm from '../views/MessageTaskForm.vue'
|
||||
import NovelReader from '../views/NovelReader.vue'
|
||||
import FilterRuleList from '../views/FilterRuleList.vue'
|
||||
import FilterRuleForm from '../views/FilterRuleForm.vue'
|
||||
import TaskQueueView from '../views/TaskQueueView.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -202,6 +203,15 @@ const routes = [
|
||||
permissions: ['wechat:manage']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'task-queue',
|
||||
name: 'TaskQueue',
|
||||
component: TaskQueueView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permissions: ['admin']
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -51,26 +51,6 @@
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-statistic
|
||||
title="受影响公众号"
|
||||
:value="Object.keys(stats.mp_stats || {}).length"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-statistic
|
||||
title="统计日期"
|
||||
:value="stats.date"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-calendar />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
@@ -118,48 +98,6 @@
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 最近异常日志 -->
|
||||
<a-card
|
||||
:bordered="false"
|
||||
class="stats-card"
|
||||
title="最近异常日志"
|
||||
v-if="stats.recent_logs && stats.recent_logs.length > 0"
|
||||
>
|
||||
<a-list :bordered="false" size="small">
|
||||
<a-list-item v-for="(log, index) in stats.recent_logs.slice(0, 20)" :key="index">
|
||||
<a-list-item-meta :title="parseLog(log).mp_name || '未知公众号'">
|
||||
<template #avatar>
|
||||
<a-avatar :style="{ backgroundColor: '#f53f3f' }">
|
||||
<icon-exclamation-circle />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<div style="margin-bottom: 4px">
|
||||
<icon-clock-circle style="margin-right: 4px" />
|
||||
{{ parseLog(log).timestamp }}
|
||||
<a-tag v-if="parseLog(log).mp_id" color="blue" style="margin-left: 8px">
|
||||
{{ parseLog(log).mp_id }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div>
|
||||
<a-link :href="parseLog(log).url" target="_blank" hoverable>
|
||||
{{ parseLog(log).url }}
|
||||
</a-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-card>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty
|
||||
v-if="!loading && stats.total === 0"
|
||||
description="暂无环境异常记录"
|
||||
style="margin-top: 40px"
|
||||
/>
|
||||
</a-spin>
|
||||
</a-page-header>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<div class="task-queue-view">
|
||||
<a-page-header title="任务队列" subtitle="查看任务队列状态和执行历史">
|
||||
<!-- 操作栏 -->
|
||||
<a-card :bordered="false" class="stats-card">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="refreshAll" :loading="loading">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-popconfirm content="确定要清空队列吗?正在执行的任务不会被中断。" @ok="handleClearQueue">
|
||||
<a-button status="warning" :loading="clearingQueue">
|
||||
<template #icon><icon-delete /></template>
|
||||
清空队列
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm content="确定要清空历史记录吗?" @ok="handleClearHistory">
|
||||
<a-button status="danger" :loading="clearingHistory">
|
||||
<template #icon><icon-close /></template>
|
||||
清空历史
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</a-card>
|
||||
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<!-- 队列状态概览 -->
|
||||
<a-card :bordered="false" class="stats-card" title="队列状态">
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-title"><icon-tag /> 队列标签</div>
|
||||
<div class="stat-value">{{ queueStatus.tag || '默认队列' }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<div class="stat-item">
|
||||
<div class="stat-title">
|
||||
<icon-check-circle-fill v-if="queueStatus.is_running" style="color: #00b42a" />
|
||||
<icon-close-circle-fill v-else style="color: #f53f3f" />
|
||||
运行状态
|
||||
</div>
|
||||
<div class="stat-value" :style="queueStatus.is_running ? { color: '#00b42a' } : { color: '#f53f3f' }">
|
||||
{{ queueStatus.is_running ? '运行中' : '已停止' }}
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-statistic
|
||||
title="待执行任务"
|
||||
:value="queueStatus.pending_count ?? 0"
|
||||
:value-style="(queueStatus.pending_count ?? 0) > 0 ? { color: '#ff7d00' } : { color: '#00b42a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-clock-circle />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-statistic title="历史记录数" :value="queueStatus.history_count ?? 0">
|
||||
<template #prefix>
|
||||
<icon-history />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 当前执行任务 -->
|
||||
<a-card
|
||||
:bordered="false"
|
||||
class="stats-card"
|
||||
title="当前执行任务"
|
||||
v-if="queueStatus.current_task"
|
||||
>
|
||||
<a-descriptions :column="{ xs: 1, sm: 2, md: 3 }" bordered>
|
||||
<a-descriptions-item label="任务名称">
|
||||
{{ queueStatus.current_task.task_name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间">
|
||||
{{ queueStatus.current_task.start_time }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag color="blue">{{ queueStatus.current_task.status }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 待执行任务列表 -->
|
||||
<a-card
|
||||
:bordered="false"
|
||||
class="stats-card"
|
||||
title="待执行任务"
|
||||
v-if="queueStatus.pending_tasks && queueStatus.pending_tasks.length > 0"
|
||||
>
|
||||
<a-table
|
||||
:columns="pendingColumns"
|
||||
:data="queueStatus.pending_tasks"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
>
|
||||
<template #task_name="{ record }">
|
||||
<a-tag color="arcoblue">{{ record.task_name }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 调度器状态 -->
|
||||
<a-card :bordered="false" class="stats-card" title="定时调度器">
|
||||
<a-descriptions :column="{ xs: 1, sm: 2, md: 3 }" bordered>
|
||||
<a-descriptions-item label="调度器状态">
|
||||
<a-tag :color="schedulerStatus.running ? 'green' : 'red'">
|
||||
{{ schedulerStatus.running ? '运行中' : '已停止' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="定时任务数">
|
||||
{{ schedulerStatus.job_count }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 定时任务列表 -->
|
||||
<a-table
|
||||
v-if="schedulerJobs.length > 0"
|
||||
:columns="schedulerColumns"
|
||||
:data="schedulerJobs"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<template #next_run_time="{ record }">
|
||||
{{ record.next_run_time || '-' }}
|
||||
</template>
|
||||
<template #trigger="{ record }">
|
||||
<a-tooltip :content="record.trigger">
|
||||
<span style="cursor: pointer">{{ formatTrigger(record.trigger) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 执行历史 -->
|
||||
<a-card :bordered="false" class="stats-card" title="执行历史(最近20条)">
|
||||
<a-table
|
||||
:columns="historyColumns"
|
||||
:data="queueStatus.recent_history || []"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:stripe="true"
|
||||
size="small"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #duration="{ record }">
|
||||
{{ record.duration ? `${record.duration}秒` : '-' }}
|
||||
</template>
|
||||
<template #error="{ record }">
|
||||
<a-tooltip v-if="record.error" :content="record.error">
|
||||
<span style="color: #f53f3f; cursor: pointer">{{ truncateError(record.error) }}</span>
|
||||
</a-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</a-page-header>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconRefresh,
|
||||
IconDelete,
|
||||
IconClose,
|
||||
IconTag,
|
||||
IconCheckCircleFill,
|
||||
IconCloseCircleFill,
|
||||
IconClockCircle,
|
||||
IconHistory,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getQueueStatus,
|
||||
clearQueue,
|
||||
clearHistory,
|
||||
getSchedulerStatus,
|
||||
getSchedulerJobs,
|
||||
type QueueStatus,
|
||||
type SchedulerStatus,
|
||||
type SchedulerJob,
|
||||
} from '@/api/taskQueue'
|
||||
|
||||
const loading = ref(false)
|
||||
const clearingQueue = ref(false)
|
||||
const clearingHistory = ref(false)
|
||||
|
||||
const queueStatus = ref<QueueStatus>({
|
||||
tag: '',
|
||||
is_running: false,
|
||||
pending_count: 0,
|
||||
pending_tasks: [],
|
||||
current_task: null,
|
||||
history_count: 0,
|
||||
recent_history: [],
|
||||
})
|
||||
|
||||
const schedulerStatus = ref<SchedulerStatus>({
|
||||
running: false,
|
||||
job_count: 0,
|
||||
next_run_times: [],
|
||||
})
|
||||
|
||||
const schedulerJobs = ref<SchedulerJob[]>([])
|
||||
|
||||
// 待执行任务表格列
|
||||
const pendingColumns = [
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'task_name',
|
||||
slotName: 'task_name',
|
||||
},
|
||||
{
|
||||
title: '添加时间',
|
||||
dataIndex: 'add_time',
|
||||
width: '180px',
|
||||
},
|
||||
]
|
||||
|
||||
// 定时任务表格列
|
||||
const schedulerColumns = [
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'id',
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
title: '触发器',
|
||||
dataIndex: 'trigger',
|
||||
slotName: 'trigger',
|
||||
},
|
||||
{
|
||||
title: '下次执行时间',
|
||||
dataIndex: 'next_run_time',
|
||||
slotName: 'next_run_time',
|
||||
width: '180px',
|
||||
},
|
||||
]
|
||||
|
||||
// 执行历史表格列
|
||||
const historyColumns = [
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'task_name',
|
||||
width: '120px',
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'start_time',
|
||||
width: '180px',
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'end_time',
|
||||
width: '180px',
|
||||
},
|
||||
{
|
||||
title: '耗时',
|
||||
dataIndex: 'duration',
|
||||
slotName: 'duration',
|
||||
width: '80px',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: '80px',
|
||||
},
|
||||
{
|
||||
title: '错误信息',
|
||||
dataIndex: 'error',
|
||||
slotName: 'error',
|
||||
},
|
||||
]
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'green'
|
||||
case 'running':
|
||||
return 'blue'
|
||||
case 'failed':
|
||||
return 'red'
|
||||
default:
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return '已完成'
|
||||
case 'running':
|
||||
return '执行中'
|
||||
case 'failed':
|
||||
return '失败'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
// 截断错误信息
|
||||
const truncateError = (error: string) => {
|
||||
if (error.length > 30) {
|
||||
return error.substring(0, 30) + '...'
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
// 格式化触发器显示
|
||||
const formatTrigger = (trigger: string) => {
|
||||
if (!trigger) return '-'
|
||||
// 简化显示
|
||||
const parts = trigger.split(',')
|
||||
if (parts.length > 2) {
|
||||
return parts.slice(0, 2).join(',') + '...'
|
||||
}
|
||||
return trigger
|
||||
}
|
||||
|
||||
// 加载所有数据
|
||||
const refreshAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [queueData, schedulerData, jobsData] = await Promise.all([
|
||||
getQueueStatus(),
|
||||
getSchedulerStatus(),
|
||||
getSchedulerJobs(),
|
||||
])
|
||||
console.log('Queue data:', queueData)
|
||||
console.log('Scheduler data:', schedulerData)
|
||||
console.log('Jobs data:', jobsData)
|
||||
|
||||
queueStatus.value = queueData
|
||||
schedulerStatus.value = schedulerData
|
||||
schedulerJobs.value = jobsData.jobs || []
|
||||
} catch (error: any) {
|
||||
console.error('Refresh error:', error)
|
||||
Message.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空队列
|
||||
const handleClearQueue = async () => {
|
||||
clearingQueue.value = true
|
||||
try {
|
||||
await clearQueue()
|
||||
Message.success('队列已清空')
|
||||
await refreshAll()
|
||||
} catch (error: any) {
|
||||
Message.error(error.message || '清空队列失败')
|
||||
} finally {
|
||||
clearingQueue.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空历史
|
||||
const handleClearHistory = async () => {
|
||||
clearingHistory.value = true
|
||||
try {
|
||||
await clearHistory()
|
||||
Message.success('历史记录已清空')
|
||||
await refreshAll()
|
||||
} catch (error: any) {
|
||||
Message.error(error.message || '清空历史失败')
|
||||
} finally {
|
||||
clearingHistory.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新定时器
|
||||
let refreshTimer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
refreshAll()
|
||||
// 每10秒自动刷新
|
||||
refreshTimer = window.setInterval(() => {
|
||||
refreshAll()
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-queue-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
:deep(.arco-statistic-title) {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.arco-statistic-content) {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
:deep(.arco-table-wrapper) {
|
||||
margin-top: -8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user