fix: content quality, image format, task monitor, calendar data source, search UI & sort
This commit is contained in:
@@ -27,6 +27,8 @@ MODULES = {
|
||||
"scheduled_optimize": {"name": "🔍 合规审查", "cron": "03:00"},
|
||||
"scheduled_optimize_sources": {"name": "📡 信息源优化", "cron": "05:00"},
|
||||
"scheduled_metrics_sync": {"name": "📊 指标同步", "cron": "06:00"},
|
||||
"scheduled_reset_search_usage": {"name": "🔁 搜索用量重置", "cron": "00:05"},
|
||||
"scheduled_task_monitor": {"name": "⏰ 任务监控", "cron": "*"},
|
||||
}
|
||||
|
||||
def _log_task(module_id: str, status: str, message: str = None,
|
||||
@@ -107,6 +109,7 @@ class TaskScheduler:
|
||||
("scheduled_optimize", self._run_optimize, "合规审查"),
|
||||
("scheduled_optimize_sources", self._run_optimize_sources, "信息源优化"),
|
||||
("scheduled_metrics_sync", self._run_metrics_sync, "指标同步"),
|
||||
("scheduled_reset_search_usage", self._run_reset_search_usage, "搜索用量重置"),
|
||||
]
|
||||
|
||||
for module_id, fn, name in MODULE_JOBS:
|
||||
@@ -129,6 +132,17 @@ class TaskScheduler:
|
||||
)
|
||||
logger.info(f"调度任务: {module_id} -> {schedule}")
|
||||
|
||||
# 每小时运行的任务监控:检测卡死/中断任务
|
||||
self.scheduler.add_job(
|
||||
self._run_task_monitor,
|
||||
CronTrigger(hour='*/1'),
|
||||
id='scheduled_task_monitor',
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True
|
||||
)
|
||||
logger.info("调度任务: scheduled_task_monitor -> 每小时")
|
||||
|
||||
self.scheduler.start()
|
||||
self._started = True
|
||||
logger.info("Scheduler started with dynamic schedule from TaskConfig")
|
||||
@@ -441,6 +455,95 @@ class TaskScheduler:
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.exception("[Scheduled] Metrics sync failed: %s", e)
|
||||
|
||||
def _run_reset_search_usage(self):
|
||||
"""每日凌晨重置搜索 API 提供商用量计数"""
|
||||
started = datetime.now(timezone.utc)
|
||||
_log_task("scheduled_reset_search_usage", "running", started_at=started)
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import SearchProvider
|
||||
db = SessionLocal()
|
||||
try:
|
||||
total = db.query(SearchProvider).update({SearchProvider.usage_today: 0, SearchProvider.last_used_at: None})
|
||||
db.commit()
|
||||
_log_task("scheduled_reset_search_usage", "success",
|
||||
message=f"已重置 {total} 个提供商用量",
|
||||
result_data={"reset_count": total},
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.info("[Scheduled] Reset %d search providers usage", total)
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
_log_task("scheduled_reset_search_usage", "failed",
|
||||
message=str(e),
|
||||
error_trace=traceback.format_exc(),
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.exception("[Scheduled] Reset search usage failed: %s", e)
|
||||
|
||||
def _run_task_monitor(self):
|
||||
"""每小时检查卡死/中断的任务,标记为失败"""
|
||||
started = datetime.now(timezone.utc)
|
||||
_log_task("scheduled_task_monitor", "running", started_at=started)
|
||||
stuck_tasklog_timeout = 7200 # 超过2小时视为卡死
|
||||
stuck_contenttask_timeout = 10800 # 超过3小时视为卡死
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import TaskLog, ContentTask
|
||||
db = SessionLocal()
|
||||
try:
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff_tasklog = now.timestamp() - stuck_tasklog_timeout
|
||||
cutoff_content = now.timestamp() - stuck_contenttask_timeout
|
||||
marked = 0
|
||||
|
||||
# 检查 TaskLog 中卡死的 running 记录
|
||||
stuck_logs = db.query(TaskLog).filter(
|
||||
TaskLog.status == "running",
|
||||
TaskLog.started_at.isnot(None)
|
||||
).all()
|
||||
for log in stuck_logs:
|
||||
if log.started_at.timestamp() < cutoff_tasklog:
|
||||
log.status = "failed"
|
||||
log.finished_at = now
|
||||
log.error_trace = "系统监控:任务运行超时(超过2小时)或进程中断,已自动标记为失败"
|
||||
if log.started_at:
|
||||
log.duration = int((now - log.started_at).total_seconds())
|
||||
marked += 1
|
||||
logger.warning("[TaskMonitor] 标记 TaskLog %d (%s) 为失败(超时)", log.id, log.module_id)
|
||||
|
||||
# 检查 ContentTask 中卡死的 running 记录
|
||||
stuck_tasks = db.query(ContentTask).filter(
|
||||
ContentTask.status == "running",
|
||||
ContentTask.started_at.isnot(None)
|
||||
).all()
|
||||
for task in stuck_tasks:
|
||||
if task.started_at.timestamp() < cutoff_content:
|
||||
task.status = "failed"
|
||||
task.finished_at = now
|
||||
task.error_msg = "系统监控:任务运行超时(超过3小时)或进程中断,已自动标记为失败"
|
||||
if task.started_at:
|
||||
task.duration = int((now - task.started_at).total_seconds())
|
||||
marked += 1
|
||||
logger.warning("[TaskMonitor] 标记 ContentTask %s (%s) 为失败(超时)", task.task_id, task.stage)
|
||||
|
||||
if marked:
|
||||
db.commit()
|
||||
logger.info("[TaskMonitor] 已标记 %d 个卡死任务为失败", marked)
|
||||
|
||||
_log_task("scheduled_task_monitor", "success",
|
||||
message=f"检查完成,标记 {marked} 个卡死任务",
|
||||
result_data={"marked_failed": marked},
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
_log_task("scheduled_task_monitor", "failed",
|
||||
message=str(e),
|
||||
error_trace=traceback.format_exc(),
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.exception("[TaskMonitor] 监控检查失败: %s", e)
|
||||
|
||||
def get_jobs(self):
|
||||
"""返回当前所有定时任务的状态"""
|
||||
jobs = []
|
||||
|
||||
Reference in New Issue
Block a user