```
feat(scheduler): 添加高级调度配置界面 - 实现调度模式选择(每天、每周、每月、自定义Cron) - 添加时间选择器和日期相关配置选项 - 提供可视化调度预览功能 - 支持Cron表达式验证和实时更新 - 修改默认调度时间为早上8点40分 - 更新邮件接收者地址 ```
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"scheduler": {
|
"scheduler": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"cronTime": "0 9 * * *",
|
"cronTime": "40 08 * * *",
|
||||||
"threshold": 100000,
|
"threshold": 100000,
|
||||||
"description": "每天9点采集当日项目",
|
"description": "每天9点采集当日项目",
|
||||||
"timeRange": "thisMonth"
|
"timeRange": "thisMonth"
|
||||||
@@ -16,6 +16,6 @@
|
|||||||
"smtpPort": 587,
|
"smtpPort": 587,
|
||||||
"smtpUser": "1076597680@qq.com",
|
"smtpUser": "1076597680@qq.com",
|
||||||
"smtpPass": "nfrjdiraqddsjeeh",
|
"smtpPass": "nfrjdiraqddsjeeh",
|
||||||
"recipients": "1650243281@qq.com"
|
"recipients": "5482498@qq.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
disable-all-tasks.bat
Normal file
13
disable-all-tasks.bat
Normal 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
65
disable-all-tasks.js
Normal 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);
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
22924
|
15556
|
||||||
|
|||||||
@@ -688,14 +688,51 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Cron 表达式</label>
|
<label>执行方式</label>
|
||||||
<input type="text" id="cfgCronTime" placeholder="0 9 * * *">
|
<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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>描述</label>
|
<label>描述</label>
|
||||||
<input type="text" id="cfgDescription" placeholder="每天9点采集">
|
<input type="text" id="cfgDescription" placeholder="每天9点采集">
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<button class="btn btn-secondary btn-sm" onclick="triggerScheduledTask()">立即执行定时任务</button>
|
||||||
<span id="schedulerStatus" style="margin-left:12px;font-size:13px;color:#888;"></span>
|
<span id="schedulerStatus" style="margin-left:12px;font-size:13px;color:#888;"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1433,6 +1470,123 @@
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
@@ -1448,8 +1602,14 @@
|
|||||||
|
|
||||||
// Scheduler
|
// Scheduler
|
||||||
document.getElementById('cfgSchedulerEnabled').value = String(cfg.scheduler?.enabled ?? false);
|
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 || '';
|
document.getElementById('cfgDescription').value = cfg.scheduler?.description || '';
|
||||||
|
handleSchedulerModeChange();
|
||||||
|
|
||||||
// Email
|
// Email
|
||||||
document.getElementById('cfgSmtpHost').value = cfg.email?.smtpHost || '';
|
document.getElementById('cfgSmtpHost').value = cfg.email?.smtpHost || '';
|
||||||
@@ -1473,6 +1633,14 @@
|
|||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
try {
|
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 = {
|
const cfg = {
|
||||||
agent: {
|
agent: {
|
||||||
baseUrl: document.getElementById('cfgBaseUrl').value.trim(),
|
baseUrl: document.getElementById('cfgBaseUrl').value.trim(),
|
||||||
@@ -1481,7 +1649,7 @@
|
|||||||
},
|
},
|
||||||
scheduler: {
|
scheduler: {
|
||||||
enabled: document.getElementById('cfgSchedulerEnabled').value === 'true',
|
enabled: document.getElementById('cfgSchedulerEnabled').value === 'true',
|
||||||
cronTime: document.getElementById('cfgCronTime').value.trim() || '0 9 * * *',
|
cronTime,
|
||||||
description: document.getElementById('cfgDescription').value.trim(),
|
description: document.getElementById('cfgDescription').value.trim(),
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
@@ -1545,6 +1713,7 @@
|
|||||||
// ===== 初始化 =====
|
// ===== 初始化 =====
|
||||||
initProjectSearchInputs();
|
initProjectSearchInputs();
|
||||||
loadTasks();
|
loadTasks();
|
||||||
|
handleSchedulerModeChange();
|
||||||
loadSettings();
|
loadSettings();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user