1.4.9-Fix

This commit is contained in:
RachelOS
2026-03-27 20:31:44 +08:00
parent 688c41dd6b
commit 7eaaa5fc30
13 changed files with 1000 additions and 82 deletions
+145
View File
@@ -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))
+129
View File
@@ -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:
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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", ""))
+3 -1
View File
@@ -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)
+131
View File
@@ -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;
}
};
+6
View File
@@ -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 />
+10
View File
@@ -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']
}
},
]
},
{
-62
View File
@@ -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>
+455
View File
@@ -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>