Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e70a3a877f |
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
### 1. 前端管理界面
|
### 1. 前端管理界面
|
||||||
- **总体统计区**:任务总数 / 运行中 / 累计运行次数 / 成功失败页面 / 图片数 / 磁盘占用
|
- **总体统计区**:任务总数 / 运行中 / 累计运行次数 / 成功失败页面 / 图片数 / 磁盘占用
|
||||||
|
- **🗑️ 回收站**:删除的任务统一进回收站(可恢复);只有手动在回收站中才能彻底删除或清空,防止误删
|
||||||
- **日间/夜间双主题**:右上角按钮一键切换,自动记忆选择
|
- **日间/夜间双主题**:右上角按钮一键切换,自动记忆选择
|
||||||
- 任务卡片总览:状态、进度、统计、下次调度时间一目了然
|
- 任务卡片总览:状态、进度、统计、下次调度时间一目了然
|
||||||
- 一键操作:开始 / 暂停 / 恢复 / 终止 / 编辑 / 删除
|
- 一键操作:开始 / 暂停 / 恢复 / 终止 / 编辑 / 删除
|
||||||
@@ -76,6 +77,10 @@
|
|||||||
| POST | `/api/tasks/<id>/pause` | 暂停 |
|
| POST | `/api/tasks/<id>/pause` | 暂停 |
|
||||||
| POST | `/api/tasks/<id>/resume` | 恢复 |
|
| POST | `/api/tasks/<id>/resume` | 恢复 |
|
||||||
| POST | `/api/tasks/<id>/stop` | 终止 |
|
| POST | `/api/tasks/<id>/stop` | 终止 |
|
||||||
|
| GET | `/api/trash` | 回收站列表 |
|
||||||
|
| POST | `/api/trash/<id>/restore` | 从回收站恢复任务 |
|
||||||
|
| DELETE | `/api/trash/<id>` | 彻底删除单个(默认输出目录一并清理) |
|
||||||
|
| DELETE | `/api/trash` | 清空回收站 |
|
||||||
| POST | `/api/probe` | 试爬取(表单规则预览链接清单) |
|
| POST | `/api/probe` | 试爬取(表单规则预览链接清单) |
|
||||||
| POST | `/api/tasks/<id>/probe` | 对已保存的自动任务试爬取 |
|
| POST | `/api/tasks/<id>/probe` | 对已保存的自动任务试爬取 |
|
||||||
| GET | `/api/runs/<rid>` | 运行详情(结果+日志) |
|
| GET | `/api/runs/<rid>` | 运行详情(结果+日志) |
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ def api_status():
|
|||||||
|
|
||||||
@app.route("/api/tasks", methods=["GET"])
|
@app.route("/api/tasks", methods=["GET"])
|
||||||
def api_tasks():
|
def api_tasks():
|
||||||
tasks = store.load_tasks()
|
tasks = [t for t in store.load_tasks() if not t.get("deleted_at")]
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
runs = store.get_runs(t["id"])
|
runs = store.get_runs(t["id"])
|
||||||
t["latest_run"] = runs[-1] if runs else None
|
t["latest_run"] = runs[-1] if runs else None
|
||||||
@@ -221,6 +221,8 @@ def api_update_task(tid):
|
|||||||
task = store.get_task(tid)
|
task = store.get_task(tid)
|
||||||
if not task:
|
if not task:
|
||||||
return jsonify({"error": "任务不存在"}), 404
|
return jsonify({"error": "任务不存在"}), 404
|
||||||
|
if task.get("deleted_at"):
|
||||||
|
return jsonify({"error": "任务在回收站中,请先恢复"}), 400
|
||||||
body = request.get_json(force=True) or {}
|
body = request.get_json(force=True) or {}
|
||||||
with JOBS_LOCK:
|
with JOBS_LOCK:
|
||||||
job = JOBS.get(tid)
|
job = JOBS.get(tid)
|
||||||
@@ -250,11 +252,71 @@ def api_update_task(tid):
|
|||||||
|
|
||||||
@app.route("/api/tasks/<tid>", methods=["DELETE"])
|
@app.route("/api/tasks/<tid>", methods=["DELETE"])
|
||||||
def api_delete_task(tid):
|
def api_delete_task(tid):
|
||||||
|
"""删除任务 -> 移入回收站 (可恢复)"""
|
||||||
|
task = store.get_task(tid)
|
||||||
|
if not task:
|
||||||
|
return jsonify({"error": "任务不存在"}), 404
|
||||||
|
if task.get("deleted_at"):
|
||||||
|
return jsonify({"error": "任务已在回收站中"}), 400
|
||||||
_stop_job(tid)
|
_stop_job(tid)
|
||||||
with JOBS_LOCK:
|
with JOBS_LOCK:
|
||||||
JOBS.pop(tid, None)
|
JOBS.pop(tid, None)
|
||||||
store.delete_task(tid)
|
store.soft_delete_task(tid, now_str())
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True, "msg": "已移入回收站"})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- API: 回收站 ----------------
|
||||||
|
|
||||||
|
def _purge_out_dir(out_dir):
|
||||||
|
"""删除任务输出目录; 仅当目录位于项目 out/ 下才删 (自定义目录保留), 返回是否删除"""
|
||||||
|
try:
|
||||||
|
d = os.path.realpath(out_dir)
|
||||||
|
base = os.path.realpath(os.path.join(HERE, "out"))
|
||||||
|
if d.startswith(base + os.sep) and os.path.isdir(d):
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(d, ignore_errors=True)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/trash", methods=["GET"])
|
||||||
|
def api_trash_list():
|
||||||
|
items = store.list_trash()
|
||||||
|
for t in items:
|
||||||
|
t["runs_count"] = len(store.get_runs(t["id"]))
|
||||||
|
t["out_dir"] = resolve_out_dir(t)
|
||||||
|
return jsonify(items)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/trash/<tid>/restore", methods=["POST"])
|
||||||
|
def api_trash_restore(tid):
|
||||||
|
task = store.get_task(tid)
|
||||||
|
if not task or not task.get("deleted_at"):
|
||||||
|
return jsonify({"error": "任务不在回收站中"}), 404
|
||||||
|
store.restore_task(tid)
|
||||||
|
return jsonify({"ok": True, "msg": "已恢复"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/trash/<tid>", methods=["DELETE"])
|
||||||
|
def api_trash_purge(tid):
|
||||||
|
task = store.get_task(tid)
|
||||||
|
if not task or not task.get("deleted_at"):
|
||||||
|
return jsonify({"error": "任务不在回收站中"}), 404
|
||||||
|
out_dir = resolve_out_dir(task)
|
||||||
|
removed = _purge_out_dir(out_dir)
|
||||||
|
store.purge_task(tid)
|
||||||
|
return jsonify({"ok": True, "purged": True, "files_removed": removed, "out_dir": out_dir})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/trash", methods=["DELETE"])
|
||||||
|
def api_trash_clear():
|
||||||
|
items = store.list_trash()
|
||||||
|
dirs = [resolve_out_dir(t) for t in items]
|
||||||
|
store.purge_trash()
|
||||||
|
removed = sum(1 for d in dirs if _purge_out_dir(d))
|
||||||
|
return jsonify({"ok": True, "purged": len(items), "dirs_removed": removed})
|
||||||
|
|
||||||
|
|
||||||
# ---------------- API: 运行控制 ----------------
|
# ---------------- API: 运行控制 ----------------
|
||||||
@@ -301,7 +363,8 @@ def api_resume(tid):
|
|||||||
|
|
||||||
@app.route("/api/stats")
|
@app.route("/api/stats")
|
||||||
def api_stats():
|
def api_stats():
|
||||||
tasks = store.load_tasks()
|
tasks = [t for t in store.load_tasks() if not t.get("deleted_at")]
|
||||||
|
trash_count = sum(1 for t in store.load_tasks() if t.get("deleted_at"))
|
||||||
total_runs = ok = fail = imgs = 0
|
total_runs = ok = fail = imgs = 0
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
for r in store.get_runs(t["id"]):
|
for r in store.get_runs(t["id"]):
|
||||||
@@ -334,6 +397,7 @@ def api_stats():
|
|||||||
"fail": fail,
|
"fail": fail,
|
||||||
"images": imgs,
|
"images": imgs,
|
||||||
"disk_mb": round(size / 1048576, 1),
|
"disk_mb": round(size / 1048576, 1),
|
||||||
|
"trash": trash_count,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+72
-4
@@ -63,6 +63,7 @@ async function loadStats() {
|
|||||||
$("stImgs").textContent = s.images;
|
$("stImgs").textContent = s.images;
|
||||||
$("stDisk").textContent = s.disk_mb >= 1024
|
$("stDisk").textContent = s.disk_mb >= 1024
|
||||||
? (s.disk_mb / 1024).toFixed(1) + " GB" : s.disk_mb + " MB";
|
? (s.disk_mb / 1024).toFixed(1) + " GB" : s.disk_mb + " MB";
|
||||||
|
$("trashCount").textContent = s.trash || 0;
|
||||||
} catch (e) { /* 统计失败忽略 */ }
|
} catch (e) { /* 统计失败忽略 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +153,75 @@ async function actTask(tid, act) {
|
|||||||
|
|
||||||
async function delTask(tid) {
|
async function delTask(tid) {
|
||||||
const t = state.tasks.find((x) => x.id === tid);
|
const t = state.tasks.find((x) => x.id === tid);
|
||||||
if (!confirm(`确定删除任务「${t ? t.name : tid}」?\n运行记录与任务配置将被删除(已爬取的文件保留在磁盘上)。`)) return;
|
if (!confirm(`确定删除任务「${t ? t.name : tid}」?\n任务将移入回收站,可在回收站中恢复或彻底删除。`)) return;
|
||||||
try {
|
try {
|
||||||
await api(`/api/tasks/${tid}`, { method: "DELETE" });
|
const r = await api(`/api/tasks/${tid}`, { method: "DELETE" });
|
||||||
toast("已删除");
|
toast(r.msg || "已删除");
|
||||||
|
loadTasks();
|
||||||
|
} catch (e) { toast(e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 回收站 ---------------- */
|
||||||
|
async function openTrash() {
|
||||||
|
try {
|
||||||
|
const items = await api("/api/trash");
|
||||||
|
state.trashItems = items;
|
||||||
|
renderTrash(items);
|
||||||
|
showModal("trashModal");
|
||||||
|
} catch (e) { toast(e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTrash(items) {
|
||||||
|
const body = $("trashBody");
|
||||||
|
if (!items.length) {
|
||||||
|
body.innerHTML = `<div class="empty" style="padding:40px 0"><div class="empty-icon">🗑️</div><p>回收站是空的</p></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.innerHTML = items.map((t) => `
|
||||||
|
<div class="trash-item">
|
||||||
|
<div class="card-head" style="min-width:0">
|
||||||
|
<span class="card-name" title="${esc(t.name)}">${esc(t.name)}</span>
|
||||||
|
<span class="badge ${esc(t.mode)}">${MODE_LABEL[t.mode] || esc(t.mode)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-line">删除时间: <b>${esc(t.deleted_at)}</b> · 运行次数: <b>${t.runs_count || 0}</b></div>
|
||||||
|
<div class="card-line">输出目录: <b>${esc(t.out_dir || "")}</b></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn sm primary" onclick="restoreTrash('${t.id}')">↩️ 恢复</button>
|
||||||
|
<button class="btn sm danger" onclick="purgeTrashTask('${t.id}')">🗑️ 彻底删除</button>
|
||||||
|
</div>
|
||||||
|
</div>`).join("")
|
||||||
|
+ `<div class="trash-foot">
|
||||||
|
<span class="card-line">共 ${items.length} 项 · 彻底删除将移除任务配置与运行记录(默认输出目录一并清理)</span>
|
||||||
|
<button class="btn danger" onclick="clearTrash()">🧹 清空回收站</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreTrash(tid) {
|
||||||
|
try {
|
||||||
|
const r = await api(`/api/trash/${tid}/restore`, { method: "POST" });
|
||||||
|
toast(r.msg || "已恢复");
|
||||||
|
openTrash();
|
||||||
|
loadTasks();
|
||||||
|
} catch (e) { toast(e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function purgeTrashTask(tid) {
|
||||||
|
const t = state.trashItems && state.trashItems.find((x) => x.id === tid);
|
||||||
|
if (!confirm(`彻底删除任务「${t ? t.name : tid}」?\n任务配置与所有运行记录将永久删除,不可恢复!`)) return;
|
||||||
|
try {
|
||||||
|
const r = await api(`/api/trash/${tid}`, { method: "DELETE" });
|
||||||
|
toast(r.files_removed ? "已彻底删除(含输出文件)" : "已彻底删除(自定义输出目录已保留)");
|
||||||
|
openTrash();
|
||||||
|
loadTasks();
|
||||||
|
} catch (e) { toast(e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearTrash() {
|
||||||
|
if (!confirm(`确定清空回收站?\n回收站中所有任务将永久删除,不可恢复!`)) return;
|
||||||
|
try {
|
||||||
|
const r = await api("/api/trash", { method: "DELETE" });
|
||||||
|
toast(`已清空 ${r.purged} 项`);
|
||||||
|
openTrash();
|
||||||
loadTasks();
|
loadTasks();
|
||||||
} catch (e) { toast(e.message, true); }
|
} catch (e) { toast(e.message, true); }
|
||||||
}
|
}
|
||||||
@@ -633,6 +699,7 @@ try {
|
|||||||
$("btnNew").onclick = openCreate;
|
$("btnNew").onclick = openCreate;
|
||||||
$("btnNew2").onclick = openCreate;
|
$("btnNew2").onclick = openCreate;
|
||||||
$("btnRefresh").onclick = loadTasks;
|
$("btnRefresh").onclick = loadTasks;
|
||||||
|
$("btnTrash").onclick = openTrash;
|
||||||
$("btnProbe").onclick = () => {
|
$("btnProbe").onclick = () => {
|
||||||
const p = collectProbeFromForm();
|
const p = collectProbeFromForm();
|
||||||
if (!p.seed_url) { toast("请先填写起始网址", true); return; }
|
if (!p.seed_url) { toast("请先填写起始网址", true); return; }
|
||||||
@@ -650,6 +717,7 @@ document.querySelectorAll("[data-close-detail]").forEach((b) => b.onclick = () =
|
|||||||
document.querySelectorAll("[data-close-preview]").forEach((b) => b.onclick = () => { $("previewFrame").src = "about:blank"; hideModal("previewModal"); });
|
document.querySelectorAll("[data-close-preview]").forEach((b) => b.onclick = () => { $("previewFrame").src = "about:blank"; hideModal("previewModal"); });
|
||||||
document.querySelectorAll("[data-close-probe]").forEach((b) => b.onclick = () => hideModal("probeModal"));
|
document.querySelectorAll("[data-close-probe]").forEach((b) => b.onclick = () => hideModal("probeModal"));
|
||||||
document.querySelectorAll("[data-close-meta]").forEach((b) => b.onclick = () => hideModal("metaModal"));
|
document.querySelectorAll("[data-close-meta]").forEach((b) => b.onclick = () => hideModal("metaModal"));
|
||||||
|
document.querySelectorAll("[data-close-trash]").forEach((b) => b.onclick = () => hideModal("trashModal"));
|
||||||
|
|
||||||
/* 表单改动监听 -> 脏标记 */
|
/* 表单改动监听 -> 脏标记 */
|
||||||
$("taskForm").addEventListener("input", () => { formDirty = true; });
|
$("taskForm").addEventListener("input", () => { formDirty = true; });
|
||||||
@@ -676,7 +744,7 @@ document.addEventListener("keydown", (e) => {
|
|||||||
}
|
}
|
||||||
stopLogPoll();
|
stopLogPoll();
|
||||||
$("previewFrame").src = "about:blank";
|
$("previewFrame").src = "about:blank";
|
||||||
["detailModal", "probeModal", "metaModal", "previewModal"].forEach((id) => $(id).classList.add("hidden"));
|
["detailModal", "probeModal", "metaModal", "trashModal", "previewModal"].forEach((id) => $(id).classList.add("hidden"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<span class="status-pill" id="statusPill">运行中任务: 0</span>
|
<span class="status-pill" id="statusPill">运行中任务: 0</span>
|
||||||
<button id="btnTheme" class="btn ghost" title="切换日间/夜间主题">☀️</button>
|
<button id="btnTheme" class="btn ghost" title="切换日间/夜间主题">☀️</button>
|
||||||
|
<button id="btnTrash" class="btn ghost" title="回收站">🗑️ 回收站 <span id="trashCount" class="trash-count">0</span></button>
|
||||||
<button id="btnRefresh" class="btn ghost">⟳ 刷新</button>
|
<button id="btnRefresh" class="btn ghost">⟳ 刷新</button>
|
||||||
<button id="btnNew" class="btn primary">+ 新建任务</button>
|
<button id="btnNew" class="btn primary">+ 新建任务</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,6 +144,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 回收站弹窗 -->
|
||||||
|
<div id="trashModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal wide">
|
||||||
|
<div class="modal-head">
|
||||||
|
<span>🗑️ 回收站(删除的任务统一在这里,可恢复或彻底删除)</span>
|
||||||
|
<button class="btn ghost sm" data-close-trash>✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="trashBody" class="detail-wrap"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 试爬取结果弹窗 -->
|
<!-- 试爬取结果弹窗 -->
|
||||||
<div id="probeModal" class="modal-overlay hidden">
|
<div id="probeModal" class="modal-overlay hidden">
|
||||||
<div class="modal wide">
|
<div class="modal wide">
|
||||||
|
|||||||
@@ -215,6 +215,21 @@ td.title-cell { max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
|
|||||||
/* ---------- preview ---------- */
|
/* ---------- preview ---------- */
|
||||||
#previewFrame { flex: 1; border: none; background: var(--preview-bg); border-radius: 0 0 14px 14px; }
|
#previewFrame { flex: 1; border: none; background: var(--preview-bg); border-radius: 0 0 14px 14px; }
|
||||||
|
|
||||||
|
/* ---------- 回收站 ---------- */
|
||||||
|
.trash-count {
|
||||||
|
display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 9px;
|
||||||
|
background: var(--red); color: #fff; font-size: 11px; line-height: 18px;
|
||||||
|
text-align: center; margin-left: 2px;
|
||||||
|
}
|
||||||
|
.trash-item {
|
||||||
|
border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px;
|
||||||
|
display: flex; flex-direction: column; gap: 6px; background: var(--panel2);
|
||||||
|
}
|
||||||
|
.trash-foot {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
gap: 10px; padding-top: 8px; border-top: 1px solid var(--border); margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 试爬取结果 / 元数据 ---------- */
|
/* ---------- 试爬取结果 / 元数据 ---------- */
|
||||||
.probe-summary { display: flex; flex-wrap: wrap; gap: 16px; font-size: 13px; }
|
.probe-summary { display: flex; flex-wrap: wrap; gap: 16px; font-size: 13px; }
|
||||||
.probe-list {
|
.probe-list {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def upsert_task(task):
|
|||||||
|
|
||||||
|
|
||||||
def delete_task(task_id):
|
def delete_task(task_id):
|
||||||
|
"""彻底删除任务 (任务 + 运行记录)"""
|
||||||
with _lock:
|
with _lock:
|
||||||
tasks = _load(TASKS_FILE, [])
|
tasks = _load(TASKS_FILE, [])
|
||||||
tasks = [t for t in tasks if t["id"] != task_id]
|
tasks = [t for t in tasks if t["id"] != task_id]
|
||||||
@@ -76,6 +77,69 @@ def delete_task(task_id):
|
|||||||
_save(RUNS_FILE, runs)
|
_save(RUNS_FILE, runs)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 回收站 ----------------
|
||||||
|
|
||||||
|
def soft_delete_task(task_id, ts):
|
||||||
|
"""删除任务 -> 移入回收站 (软删除, 记录保留可恢复)"""
|
||||||
|
with _lock:
|
||||||
|
tasks = _load(TASKS_FILE, [])
|
||||||
|
for t in tasks:
|
||||||
|
if t["id"] == task_id:
|
||||||
|
t["deleted_at"] = ts
|
||||||
|
break
|
||||||
|
_save(TASKS_FILE, tasks)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_task(task_id):
|
||||||
|
"""从回收站恢复任务"""
|
||||||
|
with _lock:
|
||||||
|
tasks = _load(TASKS_FILE, [])
|
||||||
|
for t in tasks:
|
||||||
|
if t["id"] == task_id:
|
||||||
|
t.pop("deleted_at", None)
|
||||||
|
break
|
||||||
|
_save(TASKS_FILE, tasks)
|
||||||
|
|
||||||
|
|
||||||
|
def list_trash():
|
||||||
|
"""回收站任务列表 (按删除时间倒序)"""
|
||||||
|
with _lock:
|
||||||
|
tasks = _load(TASKS_FILE, [])
|
||||||
|
return sorted(
|
||||||
|
[t for t in tasks if t.get("deleted_at")],
|
||||||
|
key=lambda x: x.get("deleted_at", ""), reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def purge_task(task_id):
|
||||||
|
"""从回收站彻底删除单个任务 (任务 + 运行记录)"""
|
||||||
|
with _lock:
|
||||||
|
tasks = _load(TASKS_FILE, [])
|
||||||
|
tasks = [t for t in tasks if t["id"] != task_id]
|
||||||
|
_save(TASKS_FILE, tasks)
|
||||||
|
runs = _load(RUNS_FILE, {})
|
||||||
|
runs.pop(task_id, None)
|
||||||
|
_save(RUNS_FILE, runs)
|
||||||
|
|
||||||
|
|
||||||
|
def purge_trash():
|
||||||
|
"""清空回收站, 返回被清空的任务 id 列表"""
|
||||||
|
with _lock:
|
||||||
|
tasks = _load(TASKS_FILE, [])
|
||||||
|
kept, purged = [], []
|
||||||
|
for t in tasks:
|
||||||
|
if t.get("deleted_at"):
|
||||||
|
purged.append(t["id"])
|
||||||
|
else:
|
||||||
|
kept.append(t)
|
||||||
|
_save(TASKS_FILE, kept)
|
||||||
|
runs = _load(RUNS_FILE, {})
|
||||||
|
for tid in purged:
|
||||||
|
runs.pop(tid, None)
|
||||||
|
_save(RUNS_FILE, runs)
|
||||||
|
return purged
|
||||||
|
|
||||||
|
|
||||||
# ---------------- 运行记录 ----------------
|
# ---------------- 运行记录 ----------------
|
||||||
|
|
||||||
def load_runs_map():
|
def load_runs_map():
|
||||||
|
|||||||
Reference in New Issue
Block a user