feat: 调度简单设置(频率选择器)+ 运行窗口 + 下次运行列
This commit is contained in:
+72
-1
@@ -21,6 +21,7 @@ import json
|
||||
import time
|
||||
import uuid
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
@@ -45,6 +46,61 @@ _log = get_logger("core.tm")
|
||||
_START_STAGGER_SEC = 0.2
|
||||
|
||||
|
||||
def _in_run_window(schedule, now=None):
|
||||
"""是否在当前运行窗口内。
|
||||
|
||||
schedule.window = {"start": "HH:MM", "end": "HH:MM"},每天重复;
|
||||
窗口外(定时触发 + 手动执行)任务不会启动。未配置/非法配置视为不限制。
|
||||
支持跨午夜(如 21:00-09:00 = 晚上 9 点运行到次日早 9 点)。
|
||||
"""
|
||||
win = (schedule or {}).get("window") or {}
|
||||
start, end = win.get("start", ""), win.get("end", "")
|
||||
if not start or not end:
|
||||
return True
|
||||
try:
|
||||
cur = (now or datetime.now()).hour * 60 + (now or datetime.now()).minute
|
||||
s = int(start.split(":")[0]) * 60 + int(start.split(":")[1])
|
||||
e = int(end.split(":")[0]) * 60 + int(end.split(":")[1])
|
||||
except (ValueError, AttributeError, IndexError):
|
||||
return True # 配置非法按不限制处理
|
||||
if s == e:
|
||||
return True # 起止相同视为不限制
|
||||
if s < e:
|
||||
return s <= cur < e
|
||||
return cur >= s or cur < e # 跨午夜
|
||||
|
||||
|
||||
def _next_run_time(schedule, now=None):
|
||||
"""任务下次真正执行的时间(考虑运行窗口)。
|
||||
|
||||
用 CronTrigger.get_next_fire_time 从当前时间向后找触发点,
|
||||
跳过运行窗口外的触发点(最多找 200 次防死循环)。
|
||||
返回 datetime 或 None(无 cron / 配置非法)。
|
||||
"""
|
||||
sched = schedule or {}
|
||||
if sched.get("mode") not in ("cron", "cron_stop"):
|
||||
return None
|
||||
cron = sched.get("cron", "")
|
||||
if not cron:
|
||||
return None
|
||||
try:
|
||||
trigger = CronTrigger.from_crontab(cron)
|
||||
except Exception:
|
||||
return None
|
||||
now = now or datetime.now()
|
||||
win = sched.get("window") or {}
|
||||
if not win.get("start") or not win.get("end"):
|
||||
return trigger.get_next_fire_time(None, now)
|
||||
fire = trigger.get_next_fire_time(None, now)
|
||||
for _ in range(200):
|
||||
if fire is None:
|
||||
return None
|
||||
if _in_run_window({"window": win}, fire):
|
||||
return fire
|
||||
fire = trigger.get_next_fire_time(fire, fire)
|
||||
return None
|
||||
|
||||
|
||||
# ================== 设备分组 ==================
|
||||
class DeviceGroup:
|
||||
def __init__(self, name, serials=None, description=""):
|
||||
@@ -504,6 +560,11 @@ class TaskManager:
|
||||
job = self.jobs.get(job_id)
|
||||
if not job:
|
||||
return
|
||||
if not _in_run_window(job.schedule):
|
||||
win = job.schedule.get("window") or {}
|
||||
_log.info(f"定时触发 {job.name}({job.id}) 跳过:当前不在运行窗口内 "
|
||||
f"({win.get('start','')}-{win.get('end','')})")
|
||||
return
|
||||
_log.info(f"定时触发: {job.name}({job.id})")
|
||||
self._run_job(job)
|
||||
|
||||
@@ -526,11 +587,21 @@ class TaskManager:
|
||||
stopped.append(serial)
|
||||
_log.info(f"定时停止 {job.name}({job.id}): 停止 {len(stopped)} 台设备 {stopped}")
|
||||
|
||||
def next_run_of(self, job):
|
||||
"""任务下次真正执行的时间(考虑运行窗口)。停用/无 cron 返回 None。"""
|
||||
if not job or not job.enabled:
|
||||
return None
|
||||
return _next_run_time(job.schedule)
|
||||
|
||||
def run_job_now(self, job_id):
|
||||
"""立即执行任务(手动触发)。"""
|
||||
"""立即执行任务(手动触发)。运行窗口外拒绝启动。"""
|
||||
job = self.jobs.get(job_id)
|
||||
if not job:
|
||||
return {"ok": False, "error": "任务不存在"}
|
||||
if not _in_run_window(job.schedule):
|
||||
win = job.schedule.get("window") or {}
|
||||
return {"ok": False,
|
||||
"error": f"当前不在运行窗口内({win.get('start', '')}-{win.get('end', '')}),任务未启动"}
|
||||
# 在独立线程跑,不阻塞调用方
|
||||
t = threading.Thread(target=self._run_job, args=(job,), daemon=True)
|
||||
t.start()
|
||||
|
||||
+124
-14
@@ -498,6 +498,13 @@ async function loadJobsForMonitor(){
|
||||
|
||||
// 通用进度展示:解析 progress = {done, total, unit, action_counts, elapsed}
|
||||
// 任意 app 都能用:显示进度条 + "done/total unit" + 运行时长 + 操作计数徽章
|
||||
// 下次运行时间格式化:"2026-08-12 09:00:00" → "08-12 09:00"
|
||||
function fmtNextRun(iso){
|
||||
if(!iso)return '';
|
||||
const s=String(iso).replace('T',' ');
|
||||
return s.length>=16?s.slice(5,16):s;
|
||||
}
|
||||
|
||||
function fmtProgress(progress, workerStatus){
|
||||
if(!progress||!Object.keys(progress).length){
|
||||
// 空闲/未运行 → 显示"-"
|
||||
@@ -622,7 +629,7 @@ function renderTaskTable(){
|
||||
filter:(j,kw)=>((j.name||'').toLowerCase().includes(kw)||(j.task_type||'').toLowerCase().includes(kw)||(j.id||'').toLowerCase().includes(kw)),
|
||||
render:renderTaskRow,
|
||||
tbody:'tb-tasks',pager:'pager-tasks',pageSize:10,
|
||||
empty:'<tr><td colspan="8" class="empty">暂无任务,点击"新建任务"创建</td></tr>'
|
||||
empty:'<tr><td colspan="9" class="empty">暂无任务,点击"新建任务"创建</td></tr>'
|
||||
});
|
||||
applyListPager('tasks');
|
||||
initListSearch('tasks','search-tasks');
|
||||
@@ -630,8 +637,14 @@ function renderTaskTable(){
|
||||
function renderTaskRow(j){
|
||||
const tgt=TARGET_MAP[j.target.mode]||j.target.mode;
|
||||
const tgtDetail=j.target.mode==='group'?(j.target.group_name||''):(j.target.mode==='serial'?(j.target.serial||''):'');
|
||||
const sched=j.schedule.mode==='cron_stop'?('启停 '+esc(j.schedule.cron||'')+'→'+esc(j.schedule.stop_cron||'')):
|
||||
j.schedule.mode==='cron'?('定时 '+esc(j.schedule.cron||'')):'手动';
|
||||
const sched=j.schedule.mode==='cron_stop'?('启停 '+cronToText(j.schedule.cron||'')+' → '+cronToText(j.schedule.stop_cron||'')):
|
||||
j.schedule.mode==='cron'?('定时 '+cronToText(j.schedule.cron||'')):'手动';
|
||||
const schedTxt=sched+((j.schedule.window&&j.schedule.window.start&&j.schedule.window.end)
|
||||
?' [窗口 '+esc(j.schedule.window.start)+'-'+esc(j.schedule.window.end)+']':'');
|
||||
const schedOut='<td>'+schedTxt+'</td>';
|
||||
// 下次运行时间(后端已按运行窗口跳过窗口外触发点)
|
||||
const nextRun=fmtNextRun(j.next_run);
|
||||
const nextOut='<td>'+(nextRun||'<span class="text-muted">-</span>')+'</td>';
|
||||
const retry=j.retry.max_attempts>1?(j.retry.max_attempts+'次/'+j.retry.delay+'s'):'不重试';
|
||||
const en=j.enabled?'<span class="label label-success">启用</span>':'<span class="label label-default">停用</span>';
|
||||
return '<tr>'+
|
||||
@@ -640,13 +653,16 @@ function renderTaskRow(j){
|
||||
'<td>'+esc(j.task_type)+'</td>'+
|
||||
'<td>'+en+'</td>'+
|
||||
'<td>'+esc(tgt)+(tgtDetail?'('+esc(tgtDetail)+')':'')+'</td>'+
|
||||
'<td>'+sched+'</td>'+
|
||||
schedOut+
|
||||
nextOut+
|
||||
'<td>'+retry+'</td>'+
|
||||
'<td>'+
|
||||
(_can('tasks')?(
|
||||
'<button class="btn btn-primary btn-xs" onclick="apiAction(\'/api/jobs/'+j.id+'/run\')">执行</button>'+
|
||||
'<button class="btn btn-xs" onclick="apiAction(\'/api/jobs/'+j.id+'/toggle\',{enabled:'+(!j.enabled)+'})">'+(j.enabled?'停用':'启用')+'</button>'+
|
||||
'<button class="btn btn-xs" onclick="openTaskModal(\''+j.id+'\')">编辑</button>'+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteTask(\''+j.id+'\')">删除</button>'+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteTask(\''+j.id+'\')">删除</button>'
|
||||
):'<span class="text-muted">只读</span>')+
|
||||
'</td>'+
|
||||
'</tr>';
|
||||
}
|
||||
@@ -663,6 +679,15 @@ function openTaskModal(jobId){
|
||||
const sm=j.schedule.mode;
|
||||
const startCron=(sm==='cron'||sm==='cron_stop')?(j.schedule.cron||''):'';
|
||||
const stopCron=sm==='cron_stop'?(j.schedule.stop_cron||''):'';
|
||||
// 运行窗口(每天时间段,窗口外不启动)
|
||||
const win=j.schedule.window||null;
|
||||
const winStart=win?(win.start||'09:00'):'09:00';
|
||||
const winEnd=win?(win.end||'18:00'):'18:00';
|
||||
// 简单调度:把已有 cron 解析回"频率+时间",编辑时回填;解析不了落到自定义
|
||||
const sc=parseCronToFreq(startCron);
|
||||
const stopF=parseCronToFreq(stopCron).time;
|
||||
const weekdayOpts=[0,1,2,3,4,5,6].map(w=>
|
||||
'<option value="'+w+'"'+(sc.weekday===w?' selected':'')+'>'+WEEKDAY_NAMES[w]+'</option>').join('');
|
||||
|
||||
document.getElementById('modal-body').innerHTML=
|
||||
'<div class="form-row">'+
|
||||
@@ -686,14 +711,32 @@ function openTaskModal(jobId){
|
||||
'<div class="form-row">'+
|
||||
'<div class="form-group"><label>调度方式</label>'+
|
||||
'<select class="form-control" id="f-schedule_mode" onchange="toggleScheduleFields()">'+
|
||||
'<option value="once"'+(sm==='once'?' selected':'')+'>手动执行</option>'+
|
||||
'<option value="once"'+(sm==='once'?' selected':'')+'>手动(不自动启动)</option>'+
|
||||
'<option value="cron"'+(sm==='cron'?' selected':'')+'>定时启动</option>'+
|
||||
'<option value="cron_stop"'+(sm==='cron_stop'?' selected':'')+'>定时启停</option>'+
|
||||
'<option value="cron_stop"'+(sm==='cron_stop'?' selected':'')+'>定时启动+停止</option>'+
|
||||
'</select>'+
|
||||
'</div>'+
|
||||
'<div class="form-group" id="f-cron_wrap"><label>启动 Cron 表达式</label><input type="text" class="form-control" id="f-schedule_cron" value="'+esc(startCron)+'" placeholder="如 0 9 * * * = 每天9点启动" style="font-family:monospace"><div class="help">格式:分 时 日 月 周</div></div>'+
|
||||
'<div class="form-group" id="f-stop_cron_wrap"><label>停止 Cron 表达式</label><input type="text" class="form-control" id="f-schedule_stop_cron" value="'+esc(stopCron)+'" placeholder="如 0 18 * * * = 每天18点停止" style="font-family:monospace"><div class="help">到点自动停止所有运行中的 worker</div></div>'+
|
||||
'<div class="form-group" id="f-freq_wrap" style="display:none"><label>频率</label>'+
|
||||
'<select class="form-control" id="f-schedule_freq" onchange="toggleScheduleFields()">'+
|
||||
'<option value="daily"'+((sc.freq==='daily')?' selected':'')+'>每天</option>'+
|
||||
'<option value="hourly"'+((sc.freq==='hourly')?' selected':'')+'>每小时(整点)</option>'+
|
||||
'<option value="everyN"'+((sc.freq==='everyN')?' selected':'')+'>每隔 N 小时</option>'+
|
||||
'<option value="weekly"'+((sc.freq==='weekly')?' selected':'')+'>每周</option>'+
|
||||
'<option value="custom"'+((sc.freq==='custom')?' selected':'')+'>自定义 cron(高级)</option>'+
|
||||
'</select>'+
|
||||
'</div>'+
|
||||
'<div class="form-group" id="f-time_wrap" style="display:none"><label>启动时间</label><input type="time" class="form-control" id="f-schedule_time" value="'+esc(sc.time||'09:00')+'"></div>'+
|
||||
'<div class="form-group" id="f-interval_wrap" style="display:none"><label>间隔(小时)</label><input type="number" class="form-control" id="f-schedule_interval" value="'+(sc.interval||2)+'" min="1" max="12"><div class="help">从 0 点起算,如 2 = 0/2/4/6...点启动</div></div>'+
|
||||
'<div class="form-group" id="f-weekday_wrap" style="display:none"><label>星期</label><select class="form-control" id="f-schedule_weekday">'+weekdayOpts+'</select></div>'+
|
||||
'<div class="form-group" id="f-cron_wrap" style="display:none"><label>启动 Cron 表达式</label><input type="text" class="form-control" id="f-schedule_cron" value="'+esc(startCron)+'" placeholder="如 0 9 * * * = 每天9点启动" style="font-family:monospace"><div class="help">格式:分 时 日 月 周(0 0 * * 0 = 每周日零点)</div></div>'+
|
||||
'<div class="form-group" id="f-stop_wrap" style="display:none"><label>停止时间(每天)</label><input type="time" class="form-control" id="f-schedule_stop_time" value="'+esc(stopF||'18:00')+'"><div class="help">到点自动停止所有运行中的 worker</div></div>'+
|
||||
'</div>'+
|
||||
'<div class="form-group form-check" style="margin-top:4px"><input type="checkbox" id="f-window_enable" '+(win?'checked':'')+' onchange="toggleWindowFields()"><label for="f-window_enable">启用运行窗口(只在指定时间段内运行)</label></div>'+
|
||||
'<div class="form-row" id="f-window_wrap" style="display:'+(win?'flex':'none')+'">'+
|
||||
'<div class="form-group"><label>允许运行·开始</label><input type="time" class="form-control" id="f-window_start" value="'+esc(winStart)+'"></div>'+
|
||||
'<div class="form-group"><label>允许运行·结束</label><input type="time" class="form-control" id="f-window_end" value="'+esc(winEnd)+'"></div>'+
|
||||
'</div>'+
|
||||
'<div class="help" id="f-window_help" style="display:'+(win?'block':'none')+'">窗口外任务不会启动(定时触发和手动执行都受约束)。跨午夜示例:21:00-09:00 = 晚 9 点到次日早 9 点允许运行</div>'+
|
||||
'<div class="form-row">'+
|
||||
'<div class="form-group"><label>重试次数</label><input type="number" class="form-control" id="f-retry_attempts" value="'+j.retry.max_attempts+'" min="1" max="10"></div>'+
|
||||
'<div class="form-group"><label>重试间隔(秒)</label><input type="number" class="form-control" id="f-retry_delay" value="'+j.retry.delay+'" min="10" max="3600"></div>'+
|
||||
@@ -728,10 +771,69 @@ function toggleTargetFields(){
|
||||
document.getElementById('f-target_serial_wrap').style.display=mode==='serial'?'block':'none';
|
||||
}
|
||||
|
||||
// ===== 简单调度:cron(5段) ↔ 频率+时间 互转 =====
|
||||
const WEEKDAY_NAMES=['周日','周一','周二','周三','周四','周五','周六'];
|
||||
// cron → 简单设置 {freq: daily|hourly|everyN|weekly|custom, time:"HH:MM", interval, weekday(0-6)}
|
||||
function parseCronToFreq(cron){
|
||||
const r={freq:'custom',time:'',interval:2,weekday:1};
|
||||
const p=(cron||'').trim().split(/\s+/);
|
||||
if(p.length!==5)return r;
|
||||
const [mi,h,d,m,w]=p;
|
||||
if(mi==='0'&&h==='*'&&d==='*'&&m==='*'&&w==='*'){r.freq='hourly';return r;}
|
||||
const n=(h.match(/^\*\/(\d+)$/)||[])[1]; // 每隔N小时:时位是 */N
|
||||
if(n&&mi==='0'&&d==='*'&&m==='*'&&w==='*'){r.freq='everyN';r.interval=parseInt(n)||2;return r;}
|
||||
if(/^\d{1,2}$/.test(mi)&&/^\d{1,2}$/.test(h)&&d==='*'&&m==='*'&&w==='*'){
|
||||
r.freq='daily';r.time=h.padStart(2,'0')+':'+mi.padStart(2,'0');return r;
|
||||
}
|
||||
if(/^\d{1,2}$/.test(mi)&&/^\d{1,2}$/.test(h)&&d==='*'&&m==='*'&&/^[0-7]$/.test(w)){
|
||||
r.freq='weekly';r.time=h.padStart(2,'0')+':'+mi.padStart(2,'0');r.weekday=parseInt(w)%7;return r;
|
||||
}
|
||||
return r; // 解析不了 → 落到自定义
|
||||
}
|
||||
// 表单 → cron(5段)
|
||||
function buildCronFromFreq(){
|
||||
const freq=document.getElementById('f-schedule_freq').value;
|
||||
if(freq==='custom')return document.getElementById('f-schedule_cron').value.trim();
|
||||
if(freq==='hourly')return '0 * * * *';
|
||||
if(freq==='everyN'){
|
||||
const n=parseInt(document.getElementById('f-schedule_interval').value)||2;
|
||||
return '0 */'+Math.min(Math.max(n,1),12)+' * * *';
|
||||
}
|
||||
const t=(document.getElementById('f-schedule_time').value||'09:00').split(':');
|
||||
const mi=parseInt(t[1])||0, h=parseInt(t[0])||0;
|
||||
if(freq==='weekly'){
|
||||
const w=parseInt(document.getElementById('f-schedule_weekday').value)||0;
|
||||
return mi+' '+h+' * * '+w;
|
||||
}
|
||||
return mi+' '+h+' * * *'; // daily
|
||||
}
|
||||
// cron(5段) → 中文描述(任务列表显示)
|
||||
function cronToText(cron){
|
||||
const r=parseCronToFreq(cron||'');
|
||||
if(r.freq==='hourly')return '每小时';
|
||||
if(r.freq==='everyN')return '每'+r.interval+'小时';
|
||||
if(r.freq==='daily')return '每天 '+r.time;
|
||||
if(r.freq==='weekly')return WEEKDAY_NAMES[r.weekday]+' '+r.time;
|
||||
return 'cron: '+(cron||'-');
|
||||
}
|
||||
function toggleScheduleFields(){
|
||||
const mode=document.getElementById('f-schedule_mode').value;
|
||||
document.getElementById('f-cron_wrap').style.display=(mode==='cron'||mode==='cron_stop')?'block':'none';
|
||||
document.getElementById('f-stop_cron_wrap').style.display=mode==='cron_stop'?'block':'none';
|
||||
const showSched=(mode==='cron'||mode==='cron_stop');
|
||||
document.getElementById('f-freq_wrap').style.display=showSched?'block':'none';
|
||||
document.getElementById('f-stop_wrap').style.display=mode==='cron_stop'?'block':'none';
|
||||
if(showSched)showFreqFields();
|
||||
}
|
||||
function toggleWindowFields(){
|
||||
const on=document.getElementById('f-window_enable').checked;
|
||||
document.getElementById('f-window_wrap').style.display=on?'flex':'none';
|
||||
document.getElementById('f-window_help').style.display=on?'block':'none';
|
||||
}
|
||||
function showFreqFields(){
|
||||
const freq=document.getElementById('f-schedule_freq').value;
|
||||
document.getElementById('f-time_wrap').style.display=(freq==='daily'||freq==='weekly')?'block':'none';
|
||||
document.getElementById('f-interval_wrap').style.display=freq==='everyN'?'block':'none';
|
||||
document.getElementById('f-weekday_wrap').style.display=freq==='weekly'?'block':'none';
|
||||
document.getElementById('f-cron_wrap').style.display=freq==='custom'?'block':'none';
|
||||
}
|
||||
|
||||
function onTaskTypeChange(){
|
||||
@@ -1726,11 +1828,19 @@ async function saveTask(jobId){
|
||||
const schedMode=document.getElementById('f-schedule_mode').value;
|
||||
let schedule={mode:'once'};
|
||||
if(schedMode==='cron'){
|
||||
schedule={mode:'cron',cron:document.getElementById('f-schedule_cron').value.trim()};
|
||||
schedule={mode:'cron',cron:buildCronFromFreq()};
|
||||
}else if(schedMode==='cron_stop'){
|
||||
const st=(document.getElementById('f-schedule_stop_time').value||'18:00').split(':');
|
||||
schedule={mode:'cron_stop',
|
||||
cron:document.getElementById('f-schedule_cron').value.trim(),
|
||||
stop_cron:document.getElementById('f-schedule_stop_cron').value.trim()};
|
||||
cron:buildCronFromFreq(),
|
||||
stop_cron:(parseInt(st[1])||0)+' '+(parseInt(st[0])||0)+' * * *'};
|
||||
}
|
||||
// 运行窗口:窗口外(定时触发 + 手动执行)不启动
|
||||
if(document.getElementById('f-window_enable').checked){
|
||||
schedule.window={
|
||||
start:document.getElementById('f-window_start').value||'09:00',
|
||||
end:document.getElementById('f-window_end').value||'18:00',
|
||||
};
|
||||
}
|
||||
|
||||
const data={
|
||||
|
||||
@@ -337,13 +337,13 @@ select.form-control{cursor:pointer}
|
||||
<div class="page-title">任务计划管理</div>
|
||||
<div class="page-sub">新建、编辑、删除任务计划,支持立即执行和定时调度</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-primary" onclick="openTaskModal()">新建任务</button>
|
||||
<button class="btn btn-primary" onclick="openTaskModal()" data-perm="tasks">新建任务</button>
|
||||
<button class="btn" onclick="loadTasks()">刷新</button>
|
||||
<input class="form-control list-search" id="search-tasks" placeholder="搜索任务名/类型/ID...">
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr>
|
||||
<th>ID</th><th>任务名</th><th>类型</th><th>启用</th><th>目标</th><th>调度</th><th>重试</th><th>操作</th>
|
||||
<th>ID</th><th>任务名</th><th>类型</th><th>启用</th><th>目标</th><th>调度</th><th>下次运行</th><th>重试</th><th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody id="tb-tasks"></tbody>
|
||||
</table>
|
||||
|
||||
+21
-6
@@ -247,15 +247,26 @@ def api_devices():
|
||||
|
||||
|
||||
# ================== API:任务计划 CRUD ==================
|
||||
def _job_next_run(job):
|
||||
"""任务下次执行时间,格式化为 "YYYY-MM-DD HH:MM"(Flask 序列化 aware datetime 会变 GMT 格式)。"""
|
||||
nr = mgr.next_run_of(job)
|
||||
return nr.strftime("%Y-%m-%d %H:%M") if nr else None
|
||||
|
||||
|
||||
@app.route("/api/jobs")
|
||||
@login_required
|
||||
def api_jobs_list():
|
||||
return jsonify({"ok": True, "jobs": [j.to_dict() for j in mgr.jobs.values()],
|
||||
jobs = []
|
||||
for j in mgr.jobs.values():
|
||||
d = j.to_dict()
|
||||
d["next_run"] = _job_next_run(j)
|
||||
jobs.append(d)
|
||||
return jsonify({"ok": True, "jobs": jobs,
|
||||
"task_types": list_task_types()})
|
||||
|
||||
|
||||
@app.route("/api/jobs", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_jobs_create():
|
||||
data = request.json or {}
|
||||
name = (data.get("name") or "").strip()
|
||||
@@ -271,11 +282,13 @@ def api_jobs_create():
|
||||
schedule=data.get("schedule", {"mode": "once"}),
|
||||
retry=data.get("retry", {"max_attempts": 1, "delay": 60}),
|
||||
enabled=data.get("enabled", True))
|
||||
return jsonify({"ok": True, "msg": "任务已创建", "job": job.to_dict()})
|
||||
d = job.to_dict()
|
||||
d["next_run"] = _job_next_run(job)
|
||||
return jsonify({"ok": True, "msg": "任务已创建", "job": d})
|
||||
|
||||
|
||||
@app.route("/api/jobs/<job_id>", methods=["PUT"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_jobs_update(job_id):
|
||||
job = mgr.jobs.get(job_id)
|
||||
if not job:
|
||||
@@ -288,11 +301,13 @@ def api_jobs_update(job_id):
|
||||
if "task_type" in fields and not get_task_class(fields["task_type"]):
|
||||
return jsonify({"ok": False, "error": f"未知任务类型: {fields['task_type']}"}), 400
|
||||
mgr.update_job(job_id, **fields)
|
||||
return jsonify({"ok": True, "msg": "任务已更新", "job": job.to_dict()})
|
||||
d = job.to_dict()
|
||||
d["next_run"] = _job_next_run(job)
|
||||
return jsonify({"ok": True, "msg": "任务已更新", "job": d})
|
||||
|
||||
|
||||
@app.route("/api/jobs/<job_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_jobs_delete(job_id):
|
||||
if mgr.delete_job(job_id):
|
||||
return jsonify({"ok": True, "msg": "任务已删除"})
|
||||
|
||||
Reference in New Issue
Block a user