# 03 - 任务取消机制详细设计

## 目录

- [1. 问题定义](#1-问题定义)
  - [1.1 当前问题](#11-当前问题)
  - [1.2 目标定义](#12-目标定义)
- [2. 现有机制分析](#2-现有机制分析)
  - [2.1 当前取消流程](#21-当前取消流程)
  - [2.2 关键代码分析](#22-关键代码分析)
  - [2.3 问题根因](#23-问题根因)
- [3. 设计方案](#3-设计方案)
  - [3.1 方案对比](#31-方案对比)
  - [3.2 推荐方案：状态标记 + 协作取消](#32-推荐方案状态标记--协作取消)
  - [3.3 方案详细设计](#33-方案详细设计)
- [4. 时序图](#4-时序图)
- [5. 状态机](#5-状态机)
- [6. API 设计](#6-api-设计)
- [7. 数据模型](#7-数据模型)
- [8. 错误处理](#8-错误处理)
- [9. 开发事项与进度](#9-开发事项与进度)
- [10. 测试方案](#10-测试方案)


## 1. 问题定义

### 1.1 当前问题

**现象:**
用户在 RunManager 点击"取消"按钮后：
1. 任务状态变为 `cancelled`
2. 子任务状态也变为 `cancelled`
3. 但后台的子任务线程仍在运行
4. LangGraph 执行仍在继续
5. 资源没有被释放

**影响:**
- 表面取消，实际仍在消耗资源
- 用户以为任务已停止，实际后台仍在运行
- 可能导致资源泄漏（内存、线程、API 调用）
- 无法真正终止执行中的子任务

### 1.2 目标定义

**核心目标:**
用户点击"取消"后，任务及所有子任务真正停止执行，资源被释放。

**具体目标:**
1. 取消后主任务和所有子任务真正停止
2. background_task 被终止
3. LangGraph 执行被停止
4. 资源（线程、内存、API 连接）被释放
5. 取消过程可在 5 秒内完成
6. 有明确的取消成功/失败反馈


## 2. 现有机制分析

### 2.1 当前取消流程

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   前端页面   │     │   API 网关   │     │  任务存储   │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │
       │  POST /tasks/id/cancel                │
       │──────────────────────────────────────>│
       │                   │                   │
       │                   │  设置状态: cancelled
       │                   │  （仅改状态！）      │
       │                   │──────────────────>│
       │                   │                   │
       │  【缺失】没有终止后台执行！              │
       │                   │                   │
       │                   │  设置子任务状态     │
       │                   │  （仅改状态！）      │
       │                   │──────────────────>│
       │                   │                   │
       │  返回成功            │                   │
       │<──────────────────────────────────────│
       │                   │                   │
       │  【后台仍在运行】                        │
       │                   │                   │
       │                   │     ┌─────────────┐     ┌─────────────┐
       │                   │     │  Background │     │  LangGraph  │
       │                   │     │    Task     │     │   Thread    │
       │                   │     └──────┬──────┘     └──────┬──────┘
       │                   │            │ 仍在运行！         │ 仍在运行！
       │                   │            │                   │
       │                   │            └───────────────────┘
```

### 2.2 关键代码分析

#### 2.2.1 当前 `cancel` API
```python
# backend/app/gateway/routers/tasks.py:294-317
@router.post("/{task_id}/cancel")
async def cancel_task(task_id: str) -> dict:
    """Cancel a task: set status to cancelled and cancel all subtasks."""
    storage = get_project_storage()
    projects = storage.list_projects()

    for project_summary in projects:
        project = storage.load_project(project_summary["id"])
        if project:
            for i, task in enumerate(project.get("tasks", [])):
                if task.get("id") == task_id:
                    task["status"] = "cancelled"
                    task["completed_at"] = datetime.utcnow().isoformat() + "Z"
                    # 【仅改状态】同时取消所有子任务
                    for subtask in task.get("subtasks", []):
                        if subtask.get("status") not in ("completed", "failed", "cancelled"):
                            subtask["status"] = "cancelled"
                            subtask["completed_at"] = datetime.utcnow().isoformat() + "Z"
                    project["tasks"][i] = task
                    if storage.save_project(project):
                        return {"success": True, "message": "Task cancelled", "task_id": task_id}
    
    # 【问题】没有终止 background_task！
    # 【问题】没有停止 LangGraph 线程！
```

#### 2.2.2 子任务执行机制
```python
# backend/packages/harness/evoflow/subagents/executor.py:102-111

# Global storage for background task results
_background_tasks: dict[str, SubagentResult] = {}
_background_tasks_lock = threading.Lock()

# Thread pool for background task scheduling
_scheduler_pool = ThreadPoolExecutor(max_workers=3, thread_name_prefix="subagent-scheduler-")

# Thread pool for actual subagent execution
_execution_pool = ThreadPoolExecutor(max_workers=3, thread_name_prefix="subagent-exec-")
```

**关键发现:**
- 子任务使用 ThreadPoolExecutor 在后台执行
- 任务信息存储在 `_background_tasks` 字典中
- 当前没有取消机制！

#### 2.2.3 子任务提交和执行
```python
# executor.py:468-504
def submit(self, task: str, task_id: str | None = None) -> str:
    """Submit task for async background execution."""
    # ...
    
    def run_task():
        with _background_tasks_lock:
            _background_tasks[task_id].status = SubagentStatus.RUNNING
            # ...
        
        try:
            # Submit execution to execution pool with timeout
            execution_future: Future = _execution_pool.submit(self.execute, task, result_holder)
            try:
                exec_result = execution_future.result(timeout=self.config.timeout_seconds)
                # Update result...
            except FuturesTimeoutError:
                # 【有超时处理】但【没有主动取消】！
                execution_future.cancel()  # Best effort
        except Exception as e:
            # ...

    _scheduler_pool.submit(run_task)
    return task_id
```

**问题分析:**
1. `submit()` 返回 task_id，但 task_id 是子任务级别的
2. 主任务和子任务的 task_id 映射关系不明确
3. `execution_future.cancel()` 只是"尽力而为"，可能无法真正停止
4. 没有从主任务到所有子任务的取消链路

### 2.3 问题根因

**根本原因:**
```
取消 = 改状态 ≠ 终止执行
```

**技术原因:**
1. **缺少取消链路**: 主任务取消 → 子任务取消 → background_task 终止
2. **Future 取消不可靠**: `Future.cancel()` 只能取消未开始的任务，运行中的任务会继续
3. **ThreadPool 限制**: Python ThreadPoolExecutor 没有强制终止线程的机制
4. **LangGraph 集成**: LangGraph 的执行循环没有暴露取消接口

**取消难点:**
| 层级 | 问题 | 难度 |
|------|------|------|
| 主任务状态 | 容易，改状态即可 | 低 |
| 子任务状态 | 容易，改状态即可 | 低 |
| background_task | Future.cancel() 不可靠 | 中 |
| ThreadPool | 无法强制终止线程 | 高 |
| LangGraph | 没有取消接口 | 高 |
| API 调用 | 已发出的 API 请求无法撤回 | 中 |


## 3. 设计方案

### 3.1 方案对比

| 方案 | 原理 | 优点 | 缺点 | 可行性 |
|------|------|------|------|--------|
| **A. 仅状态标记** | 只改状态，不终止执行 | 简单 | 不彻底，资源泄漏 | ❌ 不可接受 |
| **B. 协作式取消** | 设置取消标志，执行点检查并退出 | 相对可靠，资源可释放 | 需要修改执行代码，有延迟 | ✅ 推荐 |
| **C. 强制终止线程** | 使用系统 API 强制终止线程 | 立即生效 | Python 不支持，不稳定，资源泄漏 | ❌ 不可行 |
| **D. 进程隔离** | 每个子任务单独进程，杀进程 | 可强制终止 | 性能开销大，通信复杂 | ⚠️ 后期考虑 |
| **E. 超时自动结束** | 依赖超时机制自动结束 | 简单 | 延迟大，不主动 | ⚠️ 备选 |

**推荐方案: B（协作式取消）+ 状态标记**

理由:
1. 可实现，可靠
2. 资源可正确释放
3. 延迟可接受（5秒内）
4. 符合 Python 最佳实践

### 3.2 推荐方案：状态标记 + 协作取消

#### 3.2.1 架构设计

```
取消流程:

用户点击"取消"
    ↓
API 设置主任务状态: cancelled
    ↓
发送取消事件: TaskCancelEvent
    ↓
事件处理器:
    1. 设置子任务状态: cancelled
    2. 标记 background_task 为取消中
    3. 通知所有执行点检查取消标志
    ↓
执行点（协作检查）:
    - 每个执行步骤检查 "is_cancelled?"
    - 如果取消，抛出 CancelledError
    - 清理资源，优雅退出
    ↓
资源释放:
    - ThreadPool 任务完成
    - API 连接关闭
    - 内存释放
    ↓
通知前端: 取消完成
```

#### 3.2.2 核心组件

1. **取消标志**
   - `_cancelled_tasks: set[str]` - 存储已取消的任务 ID
   - 线程安全，全局可访问

2. **取消检查点**
   - 子任务执行循环中定期检查
   - LangGraph 节点执行前后检查
   - API 调用前检查

3. **取消事件**
   - `TaskCancelEvent`: 触发取消流程
   - 异步处理，不阻塞 API 响应

4. **资源清理**
   - 捕获 CancelledError 后清理
   - 关闭连接、释放锁、保存状态

### 3.3 方案详细设计

#### 3.3.1 取消标志管理

```python
# backend/app/gateway/cancellation/task_cancellation.py

import threading
from typing import Set

# 全局取消任务集合
_cancelled_tasks: Set[str] = set()
_cancelled_lock = threading.Lock()

def mark_task_cancelled(task_id: str) -> None:
    """标记任务为已取消"""
    with _cancelled_lock:
        _cancelled_tasks.add(task_id)

def is_task_cancelled(task_id: str) -> bool:
    """检查任务是否已取消"""
    with _cancelled_lock:
        return task_id in _cancelled_tasks

def unmark_task_cancelled(task_id: str) -> None:
    """移除取消标记（任务完成清理后）"""
    with _cancelled_lock:
        _cancelled_tasks.discard(task_id)

def mark_subtasks_cancelled(project_id: str, task_id: str) -> list[str]:
    """标记任务下所有子任务为取消，返回子任务 ID 列表"""
    from evoflow.collab.storage import get_project_storage, find_main_task
    
    storage = get_project_storage()
    row = find_main_task(storage, task_id)
    if not row:
        return []
    
    project, task = row
    subtask_ids = []
    
    for subtask in task.get("subtasks", []):
        subtask_id = subtask.get("id")
        if subtask_id and subtask.get("status") not in ("completed", "failed", "cancelled"):
            mark_task_cancelled(subtask_id)
            subtask_ids.append(subtask_id)
    
    return subtask_ids
```

#### 3.3.2 取消检查装饰器

```python
# backend/app/gateway/cancellation/cancellation_checks.py

import functools
from typing import Callable, TypeVar

T = TypeVar('T')

class TaskCancelledError(Exception):
    """任务被取消异常"""
    pass

def check_cancellation(task_id_getter: Callable[..., str]) -> Callable:
    """装饰器：在执行前检查任务是否已取消"""
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> T:
            task_id = task_id_getter(*args, **kwargs)
            if task_id and is_task_cancelled(task_id):
                raise TaskCancelledError(f"Task {task_id} has been cancelled")
            return func(*args, **kwargs)
        return wrapper
    return decorator

def check_cancellation_periodic(task_id: str, interval: int = 10):
    """上下文管理器：周期性检查取消"""
    from contextlib import contextmanager
    
    @contextmanager
    def context():
        check_count = 0
        try:
            yield
            check_count += 1
            if check_count % interval == 0 and is_task_cancelled(task_id):
                raise TaskCancelledError(f"Task {task_id} has been cancelled")
        finally:
            pass
    
    return context()
```

#### 3.3.3 修改子任务执行器

```python
# backend/packages/harness/evoflow/subagents/executor.py

# 在 SubagentExecutor 中添加取消检查

class SubagentExecutor:
    # ... 现有代码 ...
    
    def _execute_with_cancellation(self, task: str, result_holder: SubagentResult) -> SubagentResult:
        """支持取消的执行"""
        task_id = result_holder.task_id
        
        try:
            # 每个步骤检查取消
            for step in self._execution_steps(task):
                # 检查取消标志
                if is_task_cancelled(task_id):
                    logger.info(f"Task {task_id} cancelled, stopping execution")
                    result_holder.status = SubagentStatus.CANCELLED
                    result_holder.error = "Task cancelled by user"
                    result_holder.completed_at = datetime.now()
                    raise TaskCancelledError(f"Task {task_id} cancelled")
                
                # 执行步骤
                self._execute_step(step)
                
        except TaskCancelledError:
            # 清理资源
            self._cleanup_on_cancel()
            raise
        
        return result_holder
    
    def _cleanup_on_cancel(self):
        """取消时清理资源"""
        # 关闭 API 连接
        # 释放锁
        # 保存中间状态
        logger.info(f"Cleaning up resources for cancelled task")
```

#### 3.3.4 修改 API

```python
# backend/app/gateway/routers/tasks.py

from app.gateway.cancellation.task_cancellation import (
    mark_task_cancelled,
    mark_subtasks_cancelled,
)
from app.gateway.events.event_queue import event_queue
from app.gateway.events.task_events import TaskCancelEvent

@router.post("/{task_id}/cancel")
async def cancel_task(task_id: str) -> dict:
    """Cancel a task and all its subtasks."""
    storage = get_project_storage()
    projects = storage.list_projects()

    for project_summary in projects:
        project = storage.load_project(project_summary["id"])
        if project:
            for i, task in enumerate(project.get("tasks", [])):
                if task.get("id") == task_id:
                    # 1. 检查状态
                    current_status = task.get("status")
                    if current_status in ("completed", "failed", "cancelled"):
                        return {
                            "success": False,
                            "message": f"Task already in terminal state: {current_status}",
                            "task_id": task_id
                        }
                    
                    # 2. 设置主任务状态
                    task["status"] = "cancelled"
                    task["cancelled_at"] = datetime.utcnow().isoformat() + "Z"
                    task["cancelled_by"] = "user"  # TODO: 从认证获取
                    
                    # 3. 标记子任务取消
                    cancelled_subtasks = []
                    for subtask in task.get("subtasks", []):
                        if subtask.get("status") not in ("completed", "failed", "cancelled"):
                            subtask["status"] = "cancelled"
                            subtask["cancelled_at"] = task["cancelled_at"]
                            cancelled_subtasks.append(subtask.get("id"))
                    
                    # 4. 【新增】标记取消标志
                    mark_task_cancelled(task_id)
                    subtask_ids = mark_subtasks_cancelled(project["id"], task_id)
                    
                    # 5. 保存状态
                    project["tasks"][i] = task
                    if storage.save_project(project):
                        # 6. 【新增】发送取消事件（异步处理）
                        event = TaskCancelEvent(
                            task_id=task_id,
                            project_id=project["id"],
                            subtask_ids=subtask_ids,
                            cancelled_by="user",
                        )
                        event_queue.publish("task_cancelled", event)
                        
                        return {
                            "success": True,
                            "message": "Task cancelled",
                            "task_id": task_id,
                            "cancelled_subtasks_count": len(cancelled_subtasks),
                            "status": "cancelling",  # 正在取消中
                        }
                    
                    raise HTTPException(status_code=500, detail="Failed to cancel task")

    raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")
```

#### 3.3.5 取消事件处理器

```python
# backend/app/gateway/events/task_event_handlers.py

async def handle_task_cancelled(event: TaskCancelEvent):
    """处理任务取消事件"""
    logger.info(f"Handling task cancel event for task {event.task_id}")
    
    # 1. 终止 background_tasks
    for subtask_id in event.subtask_ids:
        # 获取 background_task
        result = get_background_task_result(subtask_id)
        if result and result.status == SubagentStatus.RUNNING:
            # 标记为取消
            mark_task_cancelled(subtask_id)
            logger.info(f"Marked background task {subtask_id} as cancelled")
    
    # 2. 通知前端
    await emit_task_cancelled(event.task_id, event.subtask_ids)
    
    # 3. 等待执行点检测到取消并退出
    # （协作式，无法强制立即停止）
    logger.info(f"Task {event.task_id} cancel event processed")
```


## 4. 时序图

### 4.1 成功取消流程

```mermaid
sequenceDiagram
    participant User as 用户
    participant Frontend as RunManager
    participant API as API 网关
    participant CancelMgr as 取消管理器
    participant EventQueue as 事件队列
    participant Executor as 子任务执行器
    participant Storage as 任务存储

    User->>Frontend: 点击"取消"
    Frontend->>API: POST /tasks/{id}/cancel
    
    API->>Storage: 设置主任务状态: cancelled
    API->>CancelMgr: mark_task_cancelled(task_id)
    API->>CancelMgr: mark_subtasks_cancelled(project_id, task_id)
    CancelMgr->>Storage: 获取子任务列表
    Storage->>CancelMgr: 返回子任务
    CancelMgr->>CancelMgr: 标记所有子任务为取消
    
    API->>Storage: 保存任务状态
    API->>EventQueue: 发布 TaskCancelEvent
    API->>Frontend: 返回 {status: "cancelling"}
    
    Frontend->>Frontend: 显示"取消中..."
    
    EventQueue->>Executor: 处理取消事件
    Executor->>Executor: 检查取消标志
    
    Note over Executor: 执行点检查 is_cancelled()
    
    Executor->>Executor: 检测到取消，抛出 CancelledError
    Executor->>Executor: 清理资源
    Executor->>Storage: 更新子任务状态: cancelled
    
    Executor->>Frontend: WebSocket: task_cancelled
    Frontend->>Frontend: 显示"已取消"
```

### 4.2 取消失败流程

```mermaid
sequenceDiagram
    participant Frontend as RunManager
    participant API as API 网关
    participant Storage as 任务存储

    Frontend->>API: POST /tasks/{id}/cancel
    
    API->>Storage: 查找任务
    Storage->>API: 任务不存在
    
    API->>Frontend: 404 Task not found
    Frontend->>Frontend: 显示错误信息
```


## 5. 状态机

### 5.1 取消状态流转

```
                    取消请求
    ┌─────────┐    ┌─────────┐    ┌─────────┐
    │executing│───►│cancelling│───►│cancelled│
    └─────────┘    └─────────┘    └─────────┘
                        │              ▲
                        │ 取消失败      │
                        ▼              │
                   ┌─────────┐         │
                   │  failed │─────────┘
                   │(保留状态)│
                   └─────────┘
```

### 5.2 取消过程中的子状态

```
cancelling 状态的子状态:

┌─────────────┐
│  cancelling  │
└──────┬──────┘
       │
       ├───► marking      (标记取消中)
       │
       ├───► notifying    (通知执行点)
       │
       ├───► waiting      (等待执行点响应)
       │
       ├───► cleaning     (清理资源中)
       │
       └───► [cancelled/failed]
```


## 6. API 设计

### 6.1 修改现有接口

#### POST /tasks/{task_id}/cancel

**请求:**
```json
{
  "force": false,  // 是否强制取消（未来支持）
  "reason": "用户取消"
}
```

**成功响应 (200):**
```json
{
  "success": true,
  "message": "Task cancelled",
  "task_id": "task-xxx",
  "status": "cancelling",
  "cancelled_subtasks_count": 5,
  "estimated_completion_time": "5s"
}
```

**失败响应:**
- 400: 任务已完成/已取消/已失败
- 404: 任务不存在
- 409: 正在取消中


## 7. 数据模型

### 7.1 新增字段

```python
# 任务对象新增字段
class Task:
    # 已有字段...
    
    # 取消信息
    cancelled_at: Optional[str]      # ISO 时间
    cancelled_by: Optional[str]      # 取消者
    cancel_reason: Optional[str]     # 取消原因
    cancel_attempts: int = 0         # 取消尝试次数
```

### 7.2 取消标志存储

```python
# 全局取消集合（内存中）
_cancelled_tasks: Set[str] = set()

# 持久化取消记录（可选，用于审计）
class TaskCancellationRecord:
    task_id: str
    project_id: str
    cancelled_at: datetime
    cancelled_by: str
    subtasks_cancelled: List[str]
    status: str  # "completed", "failed"
```


## 8. 错误处理

### 8.1 错误分类

| 错误类型 | 场景 | 处理方式 | 用户提示 |
|----------|------|----------|----------|
| **重复取消** | 任务已取消 | 返回成功 | "任务已取消" |
| **已完成** | 任务已完成/失败 | 返回错误 | "任务已结束，无法取消" |
| **不存在** | 任务不存在 | 404 | "任务不存在" |
| **取消中** | 正在取消中 | 409 | "任务正在取消中" |
| **取消失败** | 执行点未响应 | 标记失败 | "取消失败，请重试" |


## 9. 开发事项与进度

### 开发进度总览

| 阶段 | 内容 | 计划工时 | 实际工时 | 进度 | 状态 |
|------|------|----------|----------|------|------|
| Phase 1 | 取消管理器 | 12h | 12h | 100% | 已完成 |
| Phase 2 | 执行器改造 | 16h | 10h | 63% | 进行中 |
| Phase 3 | API 集成 | 8h | 8h | 100% | 已完成 |
| Phase 4 | 前端适配 | 4h | 4h | 100% | 已完成 |
| Phase 5 | 调试验证 | 8h | - | 0% | 未开始 |
| **总计** | - | **48h** | **22.5h** | **47%** | **进行中** |


### Phase 1: 取消管理器 (计划 1.5 天 / 12 小时)

#### 9.1.1 创建取消管理模块

**开发内容:**
```
backend/app/gateway/cancellation/
├── __init__.py              # 模块导出
├── task_cancellation.py     # 取消标志管理
├── cancellation_checks.py   # 取消检查装饰器
└── exceptions.py            # 取消异常定义
```

**详细任务:**
| 序号 | 任务 | 文件 | 工时 | 进度 | 开发者 | 状态 |
|------|------|------|------|------|--------|------|
| 1.1 | 创建取消标志管理类 | task_cancellation.py | 2h | 100% | - | 已完成 |
| 1.2 | 实现 mark/is/unmark 方法 | task_cancellation.py | 2h | 100% | - | 已完成 |
| 1.3 | 实现子任务批量标记 | task_cancellation.py | 2h | 100% | - | 已完成 |
| 1.4 | 创建取消检查装饰器 | cancellation_checks.py | 3h | 100% | - | 已完成 |
| 1.5 | 创建 TaskCancelledError 异常 | exceptions.py | 1h | 100% | - | 已完成 |
| 1.6 | 编写单元测试 | test_cancellation.py | 2h | 100% | - | 已完成 |


### Phase 2: 执行器改造 (计划 2 天 / 16 小时)

#### 9.2.1 修改 SubagentExecutor

**开发内容:**
```
backend/packages/harness/evoflow/subagents/executor.py  # 修改
```

**详细任务:**
| 序号 | 任务 | 文件 | 工时 | 进度 | 开发者 | 状态 |
|------|------|------|------|------|--------|------|
| 2.1 | 导入取消管理模块 | executor.py | 0.5h | 100% | - | 已完成 |
| 2.2 | 创建 _execute_with_cancellation 方法 | executor.py | 4h | 100% | - | 已完成 |
| 2.3 | 在执行步骤中添加取消检查点 | executor.py | 4h | 100% | - | 已完成 |
| 2.4 | 实现 _cleanup_on_cancel 方法 | executor.py | 3h | 100% | - | 已完成 |
| 2.5 | 修改 submit 使用新的执行方法 | executor.py | 2h | 100% | - | 已完成 |
| 2.6 | 添加取消相关日志 | executor.py | 1h | 100% | - | 已完成 |
| 2.7 | 编写单元测试 | test_executor_cancel.py | 2.5h | 90% | - | 基本完成 |

**关键技术点:**
```python
# 需要解决的技术难点
1. 取消检查点位置
   - 每个 LangGraph 节点执行前后
   - 每个 API 调用前
   - 长时间操作（>1s）的中间
   
2. 资源清理
   - HTTP 连接池释放
   - 文件句柄关闭
   - 临时文件删除
   
3. 状态同步
   - 取消后立即更新状态
   - 通知前端
```


### Phase 3: API 集成 (计划 1 天 / 8 小时)

#### 9.3.1 修改取消 API

**开发内容:**
```
backend/app/gateway/routers/tasks.py  # 修改 cancel 接口
```

**详细任务:**
| 序号 | 任务 | 文件 | 工时 | 进度 | 开发者 | 状态 |
|------|------|------|------|------|--------|------|
| 3.1 | 导入取消管理模块 | tasks.py | 0.5h | 100% | - | 已完成 |
| 3.2 | 修改 cancel_task 标记取消标志 | tasks.py | 2h | 100% | - | 已完成 |
| 3.3 | 发送 TaskCancelEvent | tasks.py | 1.5h | 100% | - | 已完成 |
| 3.4 | 添加取消信息字段 | tasks.py | 1h | 100% | - | 已完成 |
| 3.5 | 实现 handle_task_cancelled 处理器 | task_event_handlers.py | 2h | 100% | - | 已完成 |
| 3.6 | 集成 SSE 通知 | task_event_handlers.py | 1h | 100% | - | 已完成 |


### Phase 4: 前端适配 (计划 0.5 天 / 4 小时)

#### 9.4.1 修改 RunManager 取消按钮

**开发内容:**
```
evopanel/src/react/components/RunManager.tsx  # 修改取消逻辑
```

**详细任务:**
| 序号 | 任务 | 文件 | 工时 | 进度 | 开发者 | 状态 |
|------|------|------|------|------|--------|------|
| 4.1 | 修改 handleCancelCurrentTask | RunManager.tsx | 1h | 100% | - | 已完成 |
| 4.2 | 添加"取消中"状态显示 | RunManager.tsx | 1h | 100% | - | 已完成 |
| 4.3 | SSE 监听取消完成事件 | RunManager.tsx | 1.5h | 100% | - | 已完成 |
| 4.4 | 取消失败重试处理 | RunManager.tsx | 0.5h | 100% | - | 已完成 |


### Phase 5: 调试验证 (计划 1 天 / 8 小时)

#### 9.5.1 功能调试

**调试内容:**
| 序号 | 任务 | 工时 | 进度 | 状态 |
|------|------|------|------|------|
| 5.1 | 单个子任务取消测试 | 2h | 0% | 待调试 |
| 5.2 | 多个子任务批量取消 | 2h | 0% | 待调试 |
| 5.3 | 取消边界情况测试 | 2h | 0% | 待调试 |
| 5.4 | 并发取消测试 | 2h | 0% | 待调试 |

**测试场景:**
- [ ] 正常取消流程
- [ ] 取消已完成任务（应失败）
- [ ] 取消不存在的任务（应 404）
- [ ] 并发取消同一任务
- [ ] 取消后立即重试启动
- [ ] 长时间任务中途取消


## 10. 测试方案

### 10.1 单元测试

```python
# test_task_cancellation.py

def test_mark_and_check_cancelled():
    task_id = "test-task-001"
    
    # 标记取消
    mark_task_cancelled(task_id)
    
    # 检查取消
    assert is_task_cancelled(task_id) is True
    
    # 移除标记
    unmark_task_cancelled(task_id)
    assert is_task_cancelled(task_id) is False

# test_executor_cancel.py

async def test_subagent_execution_cancelled():
    # 创建执行器
    executor = SubagentExecutor(config, tools)
    
    # 提交任务
    task_id = executor.submit("测试任务")
    
    # 标记取消
    mark_task_cancelled(task_id)
    
    # 等待任务检测到取消
    await asyncio.sleep(2)
    
    # 验证任务状态
    result = get_background_task_result(task_id)
    assert result.status == SubagentStatus.CANCELLED
```

### 10.2 集成测试

```python
# test_cancel_integration.py

async def test_cancel_task_api():
    # 1. 创建任务并启动
    task = create_task_with_subtasks()
    start_task(task.id)
    
    # 2. 等待子任务运行
    await asyncio.sleep(3)
    
    # 3. 调用取消 API
    response = await client.post(f"/tasks/{task.id}/cancel")
    
    # 4. 验证响应
    assert response.status_code == 200
    assert response.json()["status"] == "cancelling"
    
    # 5. 等待取消完成
    await asyncio.sleep(5)
    
    # 6. 验证任务已取消
    updated_task = get_task(task.id)
    assert updated_task.status == "cancelled"
    
    # 7. 验证子任务已取消
    for subtask in updated_task.subtasks:
        assert subtask.status == "cancelled"
```


### 风险与缓解

| 风险 | 状态 | 缓解措施 | 负责人 |
|------|------|----------|--------|
| LangGraph 无法取消 | 待观察 | 在节点边界检查取消标志 | - |
| 资源泄漏 | 待观察 | 完善 cleanup_on_cancel | - |
| 取消延迟过长 | 待观察 | 设置取消超时 | - |
| 并发取消竞态 | 待观察 | 使用锁保护 | - |

## 实现进度总结

### 已完成

#### Phase 1: 取消管理器 ✅
创建了 `backend/app/gateway/cancellation/` 模块:
- `__init__.py` - 模块导出
- `task_cancellation.py` - 取消标志管理（线程安全的全局集合）
- `exceptions.py` - 取消相关异常类
- `cancellation_checks.py` - 取消检查装饰器和上下文管理器

#### Phase 3: API 集成 ✅
修改了 `backend/app/gateway/routers/tasks.py`:
- 集成取消管理模块
- 增强 `cancel_task` API:
  - 检查任务状态（防止重复取消）
  - 标记取消标志（用于协作式取消）
  - 批量标记子任务
  - 添加取消信息字段（cancelled_at, cancelled_by）
  - 发送 TaskCancelEvent 事件

#### Phase 4: 前端适配 ✅
修改了 `evopanel/src/react/components/RunManager.tsx`:
- 增强 `handleCancelCurrentTask` 处理取消状态
- 添加 `cancellingTaskId` 状态跟踪
- 显示"正在取消..."状态
- SSE 监听取消完成事件
- 取消完成后自动刷新任务列表

### 待完成

#### Phase 2: 执行器改造 ⏳
- 在 SubagentExecutor 中添加取消检查点
- 修改 task_tool.py 使用取消检查
- 实现资源清理

#### Phase 5: 调试验证 ⏳
- 单个子任务取消测试
- 批量取消测试
- 边界情况测试

**说明**: 取消功能的核心架构已完成（状态标记+事件通知），但执行器尚未集成取消检查点，因此取消只是表面标记，后台任务不会立即停止。需要 Phase 2 完成后才能真正终止执行。


**设计完成日期**: 2025-04-17
**实现完成日期**: 2025-04-17
**评审人**: 
**状态**: 核心取消机制已完成，待执行器集成
