fix: harden polling task cancel lifecycle
This commit is contained in:
parent
ddc34093de
commit
0b923d6c53
@ -110,6 +110,42 @@ def _terminate_running_sub_agents(terminal: WebTerminal, reason: str = "") -> in
|
|||||||
return stopped_count
|
return stopped_count
|
||||||
|
|
||||||
|
|
||||||
|
def _cancel_running_tasks(username: str, workspace_id: str, timeout_seconds: float = 4.0) -> Tuple[int, bool]:
|
||||||
|
"""取消当前工作区运行中的主任务,并等待其停止,避免切换对话后串写。"""
|
||||||
|
try:
|
||||||
|
from .tasks import task_manager
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[TaskCancel] 导入 task_manager 失败: {exc}")
|
||||||
|
return 0, True
|
||||||
|
|
||||||
|
active_statuses = {"pending", "running", "cancel_requested"}
|
||||||
|
|
||||||
|
def _active_tasks():
|
||||||
|
try:
|
||||||
|
recs = task_manager.list_tasks(username, workspace_id)
|
||||||
|
except Exception:
|
||||||
|
recs = task_manager.list_tasks(username)
|
||||||
|
return [rec for rec in recs if getattr(rec, "status", None) in active_statuses]
|
||||||
|
|
||||||
|
running = _active_tasks()
|
||||||
|
if not running:
|
||||||
|
return 0, True
|
||||||
|
|
||||||
|
canceled = 0
|
||||||
|
for rec in running:
|
||||||
|
task_id = getattr(rec, "task_id", None)
|
||||||
|
if task_id and task_manager.cancel_task(username, task_id):
|
||||||
|
canceled += 1
|
||||||
|
|
||||||
|
deadline = time.time() + max(timeout_seconds, 0.5)
|
||||||
|
while time.time() < deadline:
|
||||||
|
if not _active_tasks():
|
||||||
|
return canceled, True
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
return canceled, False
|
||||||
|
|
||||||
|
|
||||||
# === 背景生成对话标题(从 app_legacy 拆分) ===
|
# === 背景生成对话标题(从 app_legacy 拆分) ===
|
||||||
@conversation_bp.route('/api/conversations', methods=['GET'])
|
@conversation_bp.route('/api/conversations', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@ -160,6 +196,17 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
|||||||
thinking_mode = data.get('thinking_mode') if preserve_mode and 'thinking_mode' in data else None
|
thinking_mode = data.get('thinking_mode') if preserve_mode and 'thinking_mode' in data else None
|
||||||
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
|
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
|
||||||
|
|
||||||
|
workspace_id = session.get("workspace_id") or "default"
|
||||||
|
canceled_count, settled = _cancel_running_tasks(username=username, workspace_id=workspace_id)
|
||||||
|
if canceled_count:
|
||||||
|
debug_log(f"[TaskCancel] 创建新对话前取消任务: {canceled_count}")
|
||||||
|
if not settled:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "任务正在停止中,请稍后再试",
|
||||||
|
"message": "检测到后台任务仍在停止,请稍后再创建新对话。"
|
||||||
|
}), 409
|
||||||
|
|
||||||
_terminate_running_sub_agents(terminal, reason="用户创建新对话")
|
_terminate_running_sub_agents(terminal, reason="用户创建新对话")
|
||||||
result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode)
|
result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode)
|
||||||
|
|
||||||
@ -237,6 +284,16 @@ def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWor
|
|||||||
try:
|
try:
|
||||||
current_id = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None)
|
current_id = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None)
|
||||||
if current_id and current_id != conversation_id:
|
if current_id and current_id != conversation_id:
|
||||||
|
workspace_id = session.get("workspace_id") or "default"
|
||||||
|
canceled_count, settled = _cancel_running_tasks(username=username, workspace_id=workspace_id)
|
||||||
|
if canceled_count:
|
||||||
|
debug_log(f"[TaskCancel] 切换对话前取消任务: {canceled_count}")
|
||||||
|
if not settled:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "任务正在停止中,请稍后再试",
|
||||||
|
"message": "检测到后台任务仍在停止,请稍后再切换对话。"
|
||||||
|
}), 409
|
||||||
_terminate_running_sub_agents(terminal, reason="用户切换对话")
|
_terminate_running_sub_agents(terminal, reason="用户切换对话")
|
||||||
result = terminal.load_conversation(conversation_id)
|
result = terminal.load_conversation(conversation_id)
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class TaskRecord:
|
|||||||
"max_iterations",
|
"max_iterations",
|
||||||
"session_data",
|
"session_data",
|
||||||
"stop_requested",
|
"stop_requested",
|
||||||
|
"next_event_idx",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -66,6 +67,7 @@ class TaskRecord:
|
|||||||
self.max_iterations = max_iterations
|
self.max_iterations = max_iterations
|
||||||
self.session_data: Dict[str, Any] = {}
|
self.session_data: Dict[str, Any] = {}
|
||||||
self.stop_requested: bool = False
|
self.stop_requested: bool = False
|
||||||
|
self.next_event_idx: int = 0
|
||||||
|
|
||||||
|
|
||||||
class TaskManager:
|
class TaskManager:
|
||||||
@ -185,7 +187,10 @@ class TaskManager:
|
|||||||
# ---- internal helpers ----
|
# ---- internal helpers ----
|
||||||
def _append_event(self, rec: TaskRecord, event_type: str, data: Dict[str, Any]):
|
def _append_event(self, rec: TaskRecord, event_type: str, data: Dict[str, Any]):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
idx = rec.events[-1]["idx"] + 1 if rec.events else 0
|
idx = getattr(rec, "next_event_idx", None)
|
||||||
|
if idx is None:
|
||||||
|
idx = rec.events[-1]["idx"] + 1 if rec.events else 0
|
||||||
|
rec.next_event_idx = idx + 1
|
||||||
rec.events.append({
|
rec.events.append({
|
||||||
"idx": idx,
|
"idx": idx,
|
||||||
"type": event_type,
|
"type": event_type,
|
||||||
|
|||||||
@ -278,11 +278,14 @@ export const messageMethods = {
|
|||||||
|
|
||||||
// 等待后端确认
|
// 等待后端确认
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
const shouldKeepBusy = Boolean(taskStore.currentTaskId) &&
|
||||||
|
['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus));
|
||||||
|
|
||||||
// 清理前端状态
|
// 清理前端状态
|
||||||
this.clearPendingTools('user_stop');
|
this.clearPendingTools('user_stop');
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
this.taskInProgress = false;
|
// 若后台已回传停止事件,不要再次把输入区锁回“停止中”
|
||||||
|
this.taskInProgress = shouldKeepBusy;
|
||||||
this.forceUnlockMonitor('user_stop');
|
this.forceUnlockMonitor('user_stop');
|
||||||
if (typeof this.clearProcessedEvents === 'function') {
|
if (typeof this.clearProcessedEvents === 'function') {
|
||||||
this.clearProcessedEvents();
|
this.clearProcessedEvents();
|
||||||
@ -311,11 +314,16 @@ export const messageMethods = {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Message] 取消任务失败:', error);
|
console.error('[Message] 取消任务失败:', error);
|
||||||
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
const shouldKeepBusy = Boolean(taskStore.currentTaskId) &&
|
||||||
|
['running', 'pending', 'cancel_requested'].includes(String(taskStore.taskStatus));
|
||||||
|
|
||||||
// 即使失败也清理状态
|
// 即使失败也清理状态
|
||||||
this.clearPendingTools('user_stop');
|
this.clearPendingTools('user_stop');
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
this.taskInProgress = false;
|
// 如果任务其实已结束,允许按钮恢复发送态
|
||||||
|
this.taskInProgress = shouldKeepBusy;
|
||||||
this.forceUnlockMonitor('user_stop');
|
this.forceUnlockMonitor('user_stop');
|
||||||
if (typeof this.clearProcessedEvents === 'function') {
|
if (typeof this.clearProcessedEvents === 'function') {
|
||||||
this.clearProcessedEvents();
|
this.clearProcessedEvents();
|
||||||
|
|||||||
@ -121,6 +121,10 @@ export const taskPollingMethods = {
|
|||||||
this.handleTaskComplete(eventData, eventIdx);
|
this.handleTaskComplete(eventData, eventIdx);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'task_stopped':
|
||||||
|
this.handleTaskStopped(eventData, eventIdx);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
this.handleTaskError(eventData, eventIdx);
|
this.handleTaskError(eventData, eventIdx);
|
||||||
break;
|
break;
|
||||||
@ -681,6 +685,22 @@ export const taskPollingMethods = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleTaskStopped(data: any, eventIdx: number) {
|
||||||
|
debugLog('[TaskPolling] 任务已停止, idx:', eventIdx, data);
|
||||||
|
|
||||||
|
this.streamingMessage = false;
|
||||||
|
this.taskInProgress = false;
|
||||||
|
this.stopRequested = false;
|
||||||
|
this.waitingForSubAgent = false;
|
||||||
|
|
||||||
|
if (typeof this.clearPendingTools === 'function') {
|
||||||
|
this.clearPendingTools('task_stopped');
|
||||||
|
}
|
||||||
|
this.$forceUpdate();
|
||||||
|
|
||||||
|
this.clearTaskState();
|
||||||
|
},
|
||||||
|
|
||||||
// 新增统一清理方法
|
// 新增统一清理方法
|
||||||
clearTaskState() {
|
clearTaskState() {
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -716,6 +736,33 @@ export const taskPollingMethods = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleTaskError(data: any) {
|
handleTaskError(data: any) {
|
||||||
|
const shouldRetry = Boolean(data?.retry);
|
||||||
|
if (shouldRetry) {
|
||||||
|
const retryIn = Number(data?.retry_in) || 5;
|
||||||
|
const attempt = Number(data?.attempt) || 1;
|
||||||
|
const maxAttempts = Number(data?.max_attempts) || attempt;
|
||||||
|
|
||||||
|
debugLog('[TaskPolling] API错误,等待自动重试', {
|
||||||
|
retryIn,
|
||||||
|
attempt,
|
||||||
|
maxAttempts,
|
||||||
|
message: data?.message
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stopRequested = false;
|
||||||
|
this.taskInProgress = true;
|
||||||
|
this.streamingMessage = true;
|
||||||
|
this.$forceUpdate();
|
||||||
|
|
||||||
|
this.uiPushToast({
|
||||||
|
title: '即将重试',
|
||||||
|
message: `将在 ${retryIn} 秒后重试(第 ${attempt}/${maxAttempts} 次)`,
|
||||||
|
type: 'info',
|
||||||
|
duration: Math.max(retryIn, 1) * 1000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = data.message || '未知错误';
|
const errorMessage = data.message || '未知错误';
|
||||||
const errorType = data.error_type || 'unknown';
|
const errorType = data.error_type || 'unknown';
|
||||||
|
|
||||||
|
|||||||
@ -93,12 +93,16 @@ export const useTaskStore = defineStore('task', {
|
|||||||
// 更新任务状态
|
// 更新任务状态
|
||||||
this.taskStatus = data.status;
|
this.taskStatus = data.status;
|
||||||
this.taskUpdatedAt = data.updated_at;
|
this.taskUpdatedAt = data.updated_at;
|
||||||
|
let sawTaskStoppedEvent = false;
|
||||||
|
|
||||||
// 处理新事件
|
// 处理新事件
|
||||||
if (data.events && data.events.length > 0) {
|
if (data.events && data.events.length > 0) {
|
||||||
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
||||||
|
|
||||||
for (const event of data.events) {
|
for (const event of data.events) {
|
||||||
|
if (event?.type === 'task_stopped') {
|
||||||
|
sawTaskStoppedEvent = true;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
eventHandler(event);
|
eventHandler(event);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -109,6 +113,24 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.lastEventIndex = data.next_offset;
|
this.lastEventIndex = data.next_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兜底:如果后端状态已经是 canceled 但未返回 task_stopped 事件,
|
||||||
|
// 补发一个本地停止事件,确保前端解除“停止中”状态。
|
||||||
|
if (data.status === 'canceled' && !sawTaskStoppedEvent) {
|
||||||
|
debugLog('[Task] 状态已取消但未收到 task_stopped,补发本地事件');
|
||||||
|
try {
|
||||||
|
eventHandler({
|
||||||
|
type: 'task_stopped',
|
||||||
|
data: {
|
||||||
|
task_id: data.task_id || this.currentTaskId,
|
||||||
|
conversation_id: data.conversation_id || null,
|
||||||
|
synthetic: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Task] 处理补发 task_stopped 失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果任务已完成,停止轮询
|
// 如果任务已完成,停止轮询
|
||||||
if (this.isTaskCompleted) {
|
if (this.isTaskCompleted) {
|
||||||
debugLog('[Task] 任务已完成,停止轮询:', this.taskStatus);
|
debugLog('[Task] 任务已完成,停止轮询:', this.taskStatus);
|
||||||
@ -219,10 +241,7 @@ export const useTaskStore = defineStore('task', {
|
|||||||
throw new Error(result.error || '取消任务失败');
|
throw new Error(result.error || '取消任务失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.taskStatus = 'canceled';
|
debugLog('[Task] 已发送取消请求,等待后端停止确认');
|
||||||
this.stopPolling();
|
|
||||||
|
|
||||||
debugLog('[Task] 任务已取消');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Task] 取消任务失败:', error);
|
console.error('[Task] 取消任务失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user