]> acesimba.cloud Git - codebuddy-web.git/commitdiff
feat(console): 删除任务时可选一并删除历史会话与工作目录
authorCodebuddy <codebuddy@localhost>
Wed, 12 Aug 2026 09:35:01 +0000 (17:35 +0800)
committerCodebuddy <codebuddy@localhost>
Wed, 12 Aug 2026 09:35:01 +0000 (17:35 +0800)
- 删除确认弹窗新增两个复选框:同时删除历史会话(对话记录)、同时删除工作目录
- 后端 /api/tasks/{id} DELETE 新增 delete_session / delete_cwd 查询参数
- 按 session_id 删除 ~/.codebuddy/projects/*/*.jsonl 会话文件(含锁文件)
- 按 cwd 删除工作目录,带安全护栏:仅允许 TASK_HOME_ROOT 下的子目录,拒绝删除根目录或越界路径
- 返回 removed_session / removed_cwd / cwd_error 供前端提示

backend/app.py
frontend/index.html

index 1d130866f3855866f52d21a80cd85d6578b1b0f2..de6b15b04bad69e59a1adf7a06f10ae92bf2d38d 100644 (file)
@@ -11,6 +11,7 @@ import os
 import sys
 import json
 import glob
+import shutil
 import uuid
 import asyncio
 import signal
@@ -532,6 +533,10 @@ async def api_delete_task(task_id: str, request: Request):
         return JSONResponse({"ok": False, "error": "未授权"}, status_code=401)
     if task_id in ACTIVE_TASKS:
         return JSONResponse({"ok": False, "error": "任务正在运行,请先暂停"}, status_code=409)
+    # 可选项:是否一并删除历史会话文件 / 工作目录
+    params = request.query_params
+    delete_session = params.get("delete_session", "0") in ("1", "true", "yes", "on")
+    delete_cwd = params.get("delete_cwd", "0") in ("1", "true", "yes", "on")
     data = load_tasks()
     t = find_task(data, task_id)
     if not t:
@@ -550,10 +555,49 @@ async def api_delete_task(task_id: str, request: Request):
             return JSONResponse({"ok": False, "error": f"暂停不足 24 小时,还需 {hours_left:.1f} 小时"}, status_code=409)
     except (ValueError, TypeError):
         return JSONResponse({"ok": False, "error": "暂停时间格式错误"}, status_code=409)
+
+    removed_session = []
+    removed_cwd = False
+    cwd_error = None
+
+    # 可选:删除历史会话文件(<session_id>.jsonl 及其锁文件,跨所有 project 目录)
+    if delete_session:
+        sid = t.get("session_id")
+        if sid:
+            for f in glob.glob(str(CODEBUDDY_PROJECTS_DIR / "*" / f"{sid}.jsonl*")):
+                try:
+                    os.remove(f)
+                    removed_session.append(f)
+                except OSError as e:
+                    logger.warning(f"删除会话文件失败 {f}: {e}")
+
+    # 可选:删除工作目录(安全护栏:必须位于 TASK_HOME_ROOT 下且不等于根目录)
+    if delete_cwd:
+        cwd = t.get("cwd")
+        if cwd:
+            root = os.path.abspath(TASK_HOME_ROOT)
+            abs_cwd = os.path.abspath(cwd)
+            if abs_cwd == root or not abs_cwd.startswith(root + os.sep):
+                cwd_error = "工作目录不在允许范围内,已拒绝删除"
+            elif not os.path.exists(abs_cwd):
+                removed_cwd = True  # 已不存在,视为删除成功
+            else:
+                try:
+                    shutil.rmtree(abs_cwd)
+                    removed_cwd = True
+                except OSError as e:
+                    cwd_error = f"删除工作目录失败: {e}"
+
     data["tasks"] = [x for x in data["tasks"] if x["id"] != task_id]
     save_tasks(data)
-    logger.info(f"删除任务: id={task_id}, name={t.get('name')}")
-    return JSONResponse({"ok": True})
+    logger.info(f"删除任务: id={task_id}, name={t.get('name')}, "
+                f"session={len(removed_session)}个文件, cwd_removed={removed_cwd}")
+    return JSONResponse({
+        "ok": True,
+        "removed_session": removed_session,
+        "removed_cwd": removed_cwd,
+        "cwd_error": cwd_error,
+    })
 
 
 @app.post("/api/tasks/{task_id}/pause")
index 4857e0ac41cfebef4540dad7d709b39a5024023a..c2537f83b7413f0be09ca06f110fb2839c5d1d12 100644 (file)
@@ -159,6 +159,10 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over
 #del-modal .del-cancel { background:rgba(255,255,255,0.08); color:#ccc; }
 #del-modal .del-confirm { background:#ff6b6b; color:#fff; }
 #del-modal .del-confirm:disabled { background:rgba(255,107,107,0.3); cursor:not-allowed; }
+#del-modal .del-opts { text-align:left; margin:0 8px 14px; display:flex; flex-direction:column; gap:10px; }
+#del-modal .del-opt { color:#e0e0e0; font-size:14px; display:flex; align-items:center; gap:8px; cursor:pointer; line-height:1.4; }
+#del-modal .del-opt input { width:16px; height:16px; flex:none; }
+#del-modal .del-opt code { color:#4ecca3; font-size:12px; word-break:break-all; }
 </style>
 </head>
 <body>
@@ -194,7 +198,11 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over
   <div class="del-dialog">
     <div class="del-title">⚠ 删除任务</div>
     <div class="del-msg" id="del-msg"></div>
-    <div class="del-warn">此操作仅删除任务记录,不删除工作目录与历史会话。</div>
+    <div class="del-opts">
+      <label class="del-opt"><input type="checkbox" id="del-session"> 同时删除历史会话(对话记录)</label>
+      <label class="del-opt"><input type="checkbox" id="del-cwd"> 同时删除工作目录 <code id="del-cwd-path"></code></label>
+    </div>
+    <div class="del-warn">任务记录始终会被删除;勾选的项将一并清除。</div>
     <div class="del-count" id="del-count"></div>
     <div class="del-actions">
       <button class="del-btn del-cancel" id="del-cancel">取消</button>
@@ -459,6 +467,9 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over
   function startDeleteTask(task) {
     delState = { task: task, step: 0, timer: null, countdown: 10 };
     delMsg.textContent = '即将删除任务 "' + task.name + '"';
+    document.getElementById('del-session').checked = false;
+    document.getElementById('del-cwd').checked = false;
+    document.getElementById('del-cwd-path').textContent = task.cwd || '';
     delModal.style.display = 'flex';
     armDelConfirmStep();
   }
@@ -498,9 +509,23 @@ body { position: fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; over
     const task = delState.task;
     delConfirm.disabled = true;
     delCount.textContent = '正在删除...';
-    const res = await fetch(API + '/api/tasks/' + task.id, { method: 'DELETE', headers: authHeaders() });
+    const delSession = document.getElementById('del-session').checked;
+    const delCwd = document.getElementById('del-cwd').checked;
+    const qs = new URLSearchParams({
+      delete_session: delSession ? '1' : '0',
+      delete_cwd: delCwd ? '1' : '0',
+    });
+    const res = await fetch(API + '/api/tasks/' + task.id + '?' + qs.toString(), { method: 'DELETE', headers: authHeaders() });
     closeDelModal();
     if (!res.ok) { const d = await res.json().catch(()=>({})); alert(d.error || '删除失败'); }
+    else {
+      const d = await res.json().catch(()=>({}));
+      let note = '任务已删除';
+      if (d.removed_session && d.removed_session.length) note += ';历史会话 ' + d.removed_session.length + ' 个文件已删';
+      if (d.removed_cwd) note += ';工作目录已删';
+      if (d.cwd_error) note += '(' + d.cwd_error + ')';
+      console.log('[删除] ' + note);
+    }
     await renderTasks();
   };