// 定时任务 modal:只读两栏 master-detail(左列表 / 右详情),仅「停用/启用」「删除」 // 两个便捷动作;新建 / 修改全走对话(schedule_* 工具,DESIGN §8.5)。 // 左侧 rail 底部「定时」按钮触发。 // 后端:GET /v1/schedules(列表)、PATCH /v1/schedules/{id}{enabled}(停用/启用)、 // DELETE /v1/schedules/{id}(删除)。建/改无 REST —— 故意只读。 import { $ } from "./dom.js"; import { api } from "./api.js"; import { escapeHtml } from "./format.js"; import { selectTask } from "./chat.js"; const PLACEHOLDER = '
← 选一个定时任务查看详情
'; let _jobs = []; function openCronsModal() { $("crons-modal").classList.add("show"); $("cr-detail").innerHTML = PLACEHOLDER; renderList(); } export function closeCronsModal() { $("crons-modal").classList.remove("show"); } // UTC ISO → 浏览器本地短时刻(用户浏览器基本就在 CST,直观)。 function ts(iso) { if (!iso) return "—"; const d = new Date(iso); if (isNaN(d)) return "—"; return d.toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, }); } function statusBadge(j) { if (!j.enabled) return '已停用'; if (j.last_status === "error") return '上次失败'; if (j.last_status === "ok") return '正常'; return '待运行'; } function itemHtml(j) { return `
${escapeHtml(j.name)} ${statusBadge(j)}
${escapeHtml(j.schedule_desc || j.cron)}
下次 ${ts(j.next_run_at)} · 上次 ${ts(j.last_run_at)}
`; } async function renderList() { const list = $("cr-list"); list.innerHTML = '
加载中…
'; let data; try { data = await api("GET", "/v1/schedules"); } catch (e) { list.innerHTML = `
加载失败: ${escapeHtml(e.message)}
`; return; } _jobs = data.results || []; if (!_jobs.length) { list.innerHTML = '
还没有定时任务。' + '对助手说「每天早上八点用 brief skill 出份简报发我邮箱」即可创建。
'; return; } const active = _jobs.filter((j) => j.enabled); const paused = _jobs.filter((j) => !j.enabled); let html = `
活跃 (${active.length})
`; html += active.map(itemHtml).join("") || '
(无)
'; if (paused.length) { html += `
已停用 (${paused.length})
`; html += paused.map(itemHtml).join(""); } list.innerHTML = html; } function row(k, v) { return `
${k}${v}
`; } function selectJob(id, itemEl) { $("cr-list").querySelectorAll(".sk-item.active").forEach((el) => el.classList.remove("active")); if (itemEl) itemEl.classList.add("active"); const j = _jobs.find((x) => x.job_id === id); if (!j) return; const notify = j.notify && j.notify.to ? `必达邮件 → ${escapeHtml(j.notify.to)}` : "无(结果进任务线程 / 由指令自行投递)"; const modeDesc = j.mode === "persistent" ? "持续(同一任务线程,有连续性)" : "独立(每次新建任务,省 token)"; let rows = row("排程", `${escapeHtml(j.schedule_desc || "")} (${escapeHtml(j.cron)} @${escapeHtml(j.tz)})`) + row("模式", escapeHtml(modeDesc)) + row("指令", escapeHtml(j.prompt)) + (j.skill ? row("技能", escapeHtml(j.skill)) : "") + row("通知", notify) + row("下次触发", ts(j.next_run_at)) + row("上次执行", `${ts(j.last_run_at)} · ${escapeHtml(j.last_status || "—")}`); if (j.last_error) rows += row("上次错误", `${escapeHtml(j.last_error)}`); rows += row("累计运行", `${j.run_count} 次` + (j.consecutive_failures ? ` · 连续失败 ${j.consecutive_failures}` : "")); const openTask = j.last_task_id ? `` : ""; $("cr-detail").innerHTML = `
${escapeHtml(j.name)} ${statusBadge(j)}
` + rows + `
${openTask}
`; } // ───── 顶层绑定 ───── $("hd-crons").onclick = openCronsModal; $("cr-close").onclick = closeCronsModal; $("crons-modal").addEventListener("click", (e) => { if (e.target.id === "crons-modal") closeCronsModal(); // 点遮罩关闭 }); $("cr-list").addEventListener("click", (e) => { const item = e.target.closest(".sk-item"); if (item) selectJob(item.getAttribute("data-id"), item); }); $("cr-detail").addEventListener("click", async (e) => { const toggle = e.target.closest("[data-toggle]"); const del = e.target.closest("[data-del]"); const openTask = e.target.closest("[data-open-task]"); if (openTask) { closeCronsModal(); selectTask(openTask.getAttribute("data-open-task")); return; } if (toggle) { const id = toggle.getAttribute("data-toggle"); const j = _jobs.find((x) => x.job_id === id); toggle.disabled = true; try { await api("PATCH", "/v1/schedules/" + encodeURIComponent(id), { enabled: !j.enabled }); await renderList(); selectJob(id); } catch (err) { alert("操作失败: " + err.message); toggle.disabled = false; } return; } if (del) { const id = del.getAttribute("data-del"); const j = _jobs.find((x) => x.job_id === id); if (!confirm(`删除定时任务「${j ? j.name : id}」?不可撤销(停用可保留改用「停用」)。`)) return; del.disabled = true; try { await api("DELETE", "/v1/schedules/" + encodeURIComponent(id)); $("cr-detail").innerHTML = PLACEHOLDER; await renderList(); } catch (err) { alert("删除失败: " + err.message); del.disabled = false; } } });