feat(scheduler): 添加高级调度配置界面

- 实现调度模式选择(每天、每周、每月、自定义Cron)
- 添加时间选择器和日期相关配置选项
- 提供可视化调度预览功能
- 支持Cron表达式验证和实时更新
- 修改默认调度时间为早上8点40分
- 更新邮件接收者地址
```
This commit is contained in:
2026-03-20 16:28:27 +08:00
parent 89d0abd44c
commit 0d74cfe754
5 changed files with 255 additions and 8 deletions

View File

@@ -6,7 +6,7 @@
},
"scheduler": {
"enabled": true,
"cronTime": "0 9 * * *",
"cronTime": "40 08 * * *",
"threshold": 100000,
"description": "每天9点采集当日项目",
"timeRange": "thisMonth"
@@ -16,6 +16,6 @@
"smtpPort": 587,
"smtpUser": "1076597680@qq.com",
"smtpPass": "nfrjdiraqddsjeeh",
"recipients": "1650243281@qq.com"
"recipients": "5482498@qq.com"
}
}
}

13
disable-all-tasks.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
chcp 65001 >nul
cd /d "%~dp0"
echo ========================================
echo 批量禁用所有任务
echo ========================================
echo.
node disable-all-tasks.js
echo.
pause

65
disable-all-tasks.js Normal file
View File

@@ -0,0 +1,65 @@
// 批量禁用所有任务脚本
// 用法: node disable-all-tasks.js
import Database from 'better-sqlite3';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync, mkdirSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DEFAULT_DB_DIR = join(__dirname, 'data');
const DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, 'results.db');
const LEGACY_DB_PATH = join(DEFAULT_DB_DIR, 'results.sqlite');
// 确定数据库路径
let DB_PATH = process.env.APP_DB_PATH || process.env.RESULTS_DB_PATH;
if (!DB_PATH) {
if (existsSync(DEFAULT_DB_PATH)) {
DB_PATH = DEFAULT_DB_PATH;
} else if (existsSync(LEGACY_DB_PATH)) {
DB_PATH = LEGACY_DB_PATH;
} else {
DB_PATH = DEFAULT_DB_PATH;
}
}
if (!existsSync(DB_PATH)) {
console.error('数据库文件不存在:', DB_PATH);
console.log('请确保项目已初始化并运行过至少一次');
process.exit(1);
}
try {
const db = new Database(DB_PATH);
// 先查询当前启用的任务数量
const enabledCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE enabled = 1').get();
console.log(`当前启用的任务数量: ${enabledCount.count}`);
// 查询总任务数量
const totalCount = db.prepare('SELECT COUNT(*) as count FROM tasks').get();
console.log(`总任务数量: ${totalCount.count}`);
if (enabledCount.count === 0) {
console.log('没有需要禁用的任务');
db.close();
process.exit(0);
}
// 执行批量禁用
const result = db.prepare('UPDATE tasks SET enabled = 0').run();
console.log(`已禁用 ${result.changes} 个任务`);
// 验证结果
const newEnabledCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE enabled = 1').get();
console.log(`禁用后启用的任务数量: ${newEnabledCount.count}`);
db.close();
console.log('操作完成');
} catch (error) {
console.error('操作失败:', error.message);
process.exit(1);
}

View File

@@ -1 +1 @@
22924
15556

View File

@@ -688,14 +688,51 @@
</select>
</div>
<div class="form-group">
<label>Cron 表达</label>
<input type="text" id="cfgCronTime" placeholder="0 9 * * *">
<label>执行方</label>
<select id="cfgScheduleMode" onchange="handleSchedulerModeChange()">
<option value="daily">每天</option>
<option value="weekly">每周</option>
<option value="monthly">每月</option>
<option value="custom">高级 Cron</option>
</select>
</div>
<div class="form-group">
<label>描述</label>
<input type="text" id="cfgDescription" placeholder="每天9点采集">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>执行时间</label>
<input type="time" id="cfgScheduleTime" value="09:00" onchange="updateSchedulerCronPreview()">
</div>
<div class="form-group" id="cfgWeekdayGroup" style="display:none;">
<label>星期</label>
<select id="cfgWeekday" onchange="updateSchedulerCronPreview()">
<option value="1">周一</option>
<option value="2">周二</option>
<option value="3">周三</option>
<option value="4">周四</option>
<option value="5">周五</option>
<option value="6">周六</option>
<option value="0">周日</option>
</select>
</div>
<div class="form-group" id="cfgMonthDayGroup" style="display:none;">
<label>每月几号</label>
<input type="number" id="cfgMonthDay" min="1" max="31" value="1" onchange="updateSchedulerCronPreview()">
</div>
</div>
<div class="form-row">
<div class="form-group" id="cfgCustomCronGroup" style="display:none;">
<label>高级 Cron 表达式</label>
<input type="text" id="cfgCronTime" placeholder="0 9 * * *" oninput="updateSchedulerCronPreview()">
</div>
<div class="form-group">
<label>计划预览</label>
<input type="text" id="cfgCronPreview" readonly placeholder="每天 09:00">
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="triggerScheduledTask()">立即执行定时任务</button>
<span id="schedulerStatus" style="margin-left:12px;font-size:13px;color:#888;"></span>
</div>
@@ -1433,6 +1470,123 @@
.replace(/'/g, '&#39;');
}
function padSchedulerNumber(value) {
return String(value).padStart(2, '0');
}
function normalizeSchedulerTime(value) {
const match = /^(\d{1,2}):(\d{1,2})$/.exec(String(value || '').trim());
if (!match) return '09:00';
const hour = Math.min(23, Math.max(0, parseInt(match[1], 10)));
const minute = Math.min(59, Math.max(0, parseInt(match[2], 10)));
return `${padSchedulerNumber(hour)}:${padSchedulerNumber(minute)}`;
}
function splitSchedulerTime(value) {
const [hour, minute] = normalizeSchedulerTime(value).split(':');
return { hour, minute };
}
function describeScheduler(mode, time, weekday, monthDay, cronTime) {
const normalizedTime = normalizeSchedulerTime(time);
const weekdayMap = {
'0': '周日',
'1': '周一',
'2': '周二',
'3': '周三',
'4': '周四',
'5': '周五',
'6': '周六',
};
if (mode === 'daily') return `每天 ${normalizedTime}`;
if (mode === 'weekly') return `每周${weekdayMap[String(weekday)] || '周一'} ${normalizedTime}`;
if (mode === 'monthly') return `每月 ${monthDay}${normalizedTime}`;
return cronTime || '0 9 * * *';
}
function parseSchedulerCron(cronTime) {
const normalizedCron = String(cronTime || '0 9 * * *').trim().replace(/\s+/g, ' ');
const parts = normalizedCron.split(' ');
const fallback = {
mode: 'custom',
time: '09:00',
weekday: '1',
monthDay: '1',
cronTime: normalizedCron || '0 9 * * *',
};
if (parts.length !== 5) return fallback;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
if (!/^\d+$/.test(minute) || !/^\d+$/.test(hour)) return fallback;
const time = `${padSchedulerNumber(parseInt(hour, 10))}:${padSchedulerNumber(parseInt(minute, 10))}`;
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return { mode: 'daily', time, weekday: '1', monthDay: '1', cronTime: normalizedCron };
}
if (dayOfMonth === '*' && month === '*' && /^(0|1|2|3|4|5|6|7)$/.test(dayOfWeek)) {
return {
mode: 'weekly',
time,
weekday: dayOfWeek === '7' ? '0' : dayOfWeek,
monthDay: '1',
cronTime: normalizedCron,
};
}
if (/^\d+$/.test(dayOfMonth) && month === '*' && dayOfWeek === '*') {
const normalizedMonthDay = Math.min(31, Math.max(1, parseInt(dayOfMonth, 10)));
return {
mode: 'monthly',
time,
weekday: '1',
monthDay: String(normalizedMonthDay),
cronTime: normalizedCron,
};
}
return fallback;
}
function buildSchedulerCronFromInputs() {
const mode = document.getElementById('cfgScheduleMode').value;
const time = normalizeSchedulerTime(document.getElementById('cfgScheduleTime').value);
const { hour, minute } = splitSchedulerTime(time);
const weekday = document.getElementById('cfgWeekday').value || '1';
const monthDayValue = parseInt(document.getElementById('cfgMonthDay').value, 10);
const monthDay = Number.isFinite(monthDayValue) ? Math.min(31, Math.max(1, monthDayValue)) : 1;
if (mode === 'daily') return `${minute} ${hour} * * *`;
if (mode === 'weekly') return `${minute} ${hour} * * ${weekday}`;
if (mode === 'monthly') return `${minute} ${hour} ${monthDay} * *`;
return document.getElementById('cfgCronTime').value.trim() || '0 9 * * *';
}
function updateSchedulerCronPreview() {
const cronTime = buildSchedulerCronFromInputs();
document.getElementById('cfgCronTime').value = cronTime;
document.getElementById('cfgCronPreview').value = describeScheduler(
document.getElementById('cfgScheduleMode').value,
document.getElementById('cfgScheduleTime').value,
document.getElementById('cfgWeekday').value,
document.getElementById('cfgMonthDay').value,
cronTime
);
}
function handleSchedulerModeChange() {
const mode = document.getElementById('cfgScheduleMode').value;
document.getElementById('cfgWeekdayGroup').style.display = mode === 'weekly' ? '' : 'none';
document.getElementById('cfgMonthDayGroup').style.display = mode === 'monthly' ? '' : 'none';
document.getElementById('cfgCustomCronGroup').style.display = mode === 'custom' ? '' : 'none';
updateSchedulerCronPreview();
}
// ===== 设置 =====
async function loadSettings() {
try {
@@ -1448,8 +1602,14 @@
// Scheduler
document.getElementById('cfgSchedulerEnabled').value = String(cfg.scheduler?.enabled ?? false);
document.getElementById('cfgCronTime').value = cfg.scheduler?.cronTime || '0 9 * * *';
const schedulerUi = parseSchedulerCron(cfg.scheduler?.cronTime || '0 9 * * *');
document.getElementById('cfgScheduleMode').value = schedulerUi.mode;
document.getElementById('cfgScheduleTime').value = schedulerUi.time;
document.getElementById('cfgWeekday').value = schedulerUi.weekday;
document.getElementById('cfgMonthDay').value = schedulerUi.monthDay;
document.getElementById('cfgCronTime').value = schedulerUi.cronTime;
document.getElementById('cfgDescription').value = cfg.scheduler?.description || '';
handleSchedulerModeChange();
// Email
document.getElementById('cfgSmtpHost').value = cfg.email?.smtpHost || '';
@@ -1473,6 +1633,14 @@
async function saveSettings() {
try {
const cronTime = buildSchedulerCronFromInputs();
if (document.getElementById('cfgScheduleMode').value === 'custom') {
const cronParts = cronTime.trim().split(/\s+/);
if (cronParts.length !== 5) {
throw new Error('Cron 表达式格式错误,请填写 5 段');
}
}
const cfg = {
agent: {
baseUrl: document.getElementById('cfgBaseUrl').value.trim(),
@@ -1481,7 +1649,7 @@
},
scheduler: {
enabled: document.getElementById('cfgSchedulerEnabled').value === 'true',
cronTime: document.getElementById('cfgCronTime').value.trim() || '0 9 * * *',
cronTime,
description: document.getElementById('cfgDescription').value.trim(),
},
email: {
@@ -1545,6 +1713,7 @@
// ===== 初始化 =====
initProjectSearchInputs();
loadTasks();
handleSchedulerModeChange();
loadSettings();
</script>
</body>