agent-Specialization/static/src/stores/task.ts

633 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { defineStore } from 'pinia';
import { debugLog } from '../app/methods/common';
const debugNotifyLog = (...args: any[]) => {
void args;
};
const keyNotifyLog = (...args: any[]) => {
console.log(...args);
};
const jsonDebug = (...args: any[]) => {
console.log('[JSONDEBUG]', ...args);
};
const CONN_DIAG_PREFIX = '[CONN_DIAG]';
const TASK_POLL_DIAG_MAX = 2000;
const taskPollDiag = (event: string, payload: Record<string, any> = {}) => {
const ts = new Date().toISOString();
const record = { ts, event, ...payload };
try {
if (typeof window !== 'undefined') {
const w = window as any;
if (!Array.isArray(w.__CONN_DIAG_LOGS__)) {
w.__CONN_DIAG_LOGS__ = [];
}
w.__CONN_DIAG_LOGS__.push(record);
if (w.__CONN_DIAG_LOGS__.length > TASK_POLL_DIAG_MAX) {
w.__CONN_DIAG_LOGS__.splice(0, w.__CONN_DIAG_LOGS__.length - TASK_POLL_DIAG_MAX);
}
}
} catch {
// ignore
}
console.log(CONN_DIAG_PREFIX, event, record);
};
export const useTaskStore = defineStore('task', {
state: () => ({
currentTaskId: null as string | null,
lastEventIndex: 0,
pollingInterval: null as number | null,
taskStatus: 'idle' as 'idle' | 'running' | 'succeeded' | 'failed' | 'canceled',
isPolling: false,
pollingError: null as string | null,
pollingErrorCount: 0, // 新增错误计数
pollingInFlight: false,
pollingSeq: 0,
pollingWarned: false,
pollingRequestTimeoutMs: 12000,
pollingFailThreshold: 8,
pollingIntervalMs: 250,
taskCreatedAt: null as number | null,
taskUpdatedAt: null as number | null,
runtimeQueueSnapshotKey: ''
}),
getters: {
hasActiveTask: (state) => state.currentTaskId !== null && state.taskStatus === 'running',
isTaskCompleted: (state) => ['succeeded', 'failed', 'canceled'].includes(state.taskStatus)
},
actions: {
async createTask(
message: string,
images: any[] = [],
videos: any[] = [],
conversationId: string | null = null,
options: {
model_key?: string | null;
run_mode?: 'fast' | 'thinking' | 'deep' | null;
thinking_mode?: boolean | null;
message_source?: string | null;
goal_mode?: boolean | null;
skill_refs?: Array<{ name?: string; path: string }> | null;
eventHandler?: (event: any) => void;
} = {}
) {
try {
debugLog('[Task] 创建任务:', { message, conversationId });
const response = await fetch('/api/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message,
images,
videos,
conversation_id: conversationId,
model_key: options.model_key ?? undefined,
run_mode: options.run_mode ?? undefined,
thinking_mode:
typeof options.thinking_mode === 'boolean' ? options.thinking_mode : undefined,
message_source: options.message_source ?? undefined,
goal_mode: options.goal_mode === true ? true : undefined,
skill_refs: Array.isArray(options.skill_refs) ? options.skill_refs : undefined
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '创建任务失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '创建任务失败');
}
this.currentTaskId = result.data.task_id;
this.taskStatus = result.data.status;
this.lastEventIndex = 0;
this.taskCreatedAt = result.data.created_at;
this.runtimeQueueSnapshotKey = '';
this.pollingError = null;
this.pollingErrorCount = 0;
this.pollingWarned = false;
debugLog('[Task] 任务创建成功:', result.data.task_id);
taskPollDiag('task-created', {
taskId: result.data.task_id,
conversationId
});
// 立即开始轮询
this.startPolling(options.eventHandler);
return result.data;
} catch (error) {
debugLog('[Task] 创建任务失败:', error);
taskPollDiag('task-create-failed', {
error: error?.message || String(error),
conversationId
});
throw error;
}
},
async pollTaskEvents(eventHandler: (event: any) => void) {
if (!this.currentTaskId) {
debugLog('[Task] 没有活跃任务,停止轮询');
this.stopPolling('no-active-task');
return;
}
if (this.pollingInFlight) {
return;
}
this.pollingInFlight = true;
const taskId = this.currentTaskId;
const fromOffset = this.lastEventIndex;
const requestId = `${Date.now()}-${++this.pollingSeq}`;
const startedAt = Date.now();
try {
const response = await fetch(
`/api/tasks/${taskId}?from=${fromOffset}`,
{
signal: AbortSignal.timeout(this.pollingRequestTimeoutMs),
headers: {
'X-Task-Poll': requestId
}
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '轮询任务失败');
}
const data = result.data;
// 如果用户已切换/新建对话并清除了当前轮询,忽略这次已在路上的旧响应。
// 否则旧任务事件可能被写入当前新对话视图。
if (this.currentTaskId !== taskId) {
taskPollDiag('task-poll-stale-response-ignored', {
requestId,
responseTaskId: data?.task_id || taskId,
activeTaskId: this.currentTaskId,
from: fromOffset
});
return;
}
const eventsCount = Array.isArray(data?.events) ? data.events.length : 0;
jsonDebug('taskStore.poll:response', {
taskId,
from: fromOffset,
status: data?.status,
nextOffset: data?.next_offset,
eventsCount
});
if (eventsCount > 0 || this.pollingErrorCount > 0 || data?.status !== 'running' || fromOffset === 0) {
taskPollDiag('task-poll-ok', {
requestId,
taskId,
from: fromOffset,
nextOffset: data?.next_offset,
status: data?.status,
eventsCount,
elapsedMs: Date.now() - startedAt
});
}
// 更新任务状态
this.taskStatus = data.status;
this.taskUpdatedAt = data.updated_at;
const runtimeQueueMessages = Array.isArray(data?.runtime_queued_messages)
? data.runtime_queued_messages
: [];
const runtimeQueueSnapshotKey = JSON.stringify(
runtimeQueueMessages.map((item: any) => [
String(item?.id || ''),
String(item?.text || '')
])
);
if (runtimeQueueSnapshotKey !== this.runtimeQueueSnapshotKey) {
this.runtimeQueueSnapshotKey = runtimeQueueSnapshotKey;
try {
eventHandler({
type: 'runtime_queue_sync',
data: {
task_id: data?.task_id || this.currentTaskId,
conversation_id: data?.conversation_id || null,
messages: runtimeQueueMessages
}
});
} catch (err) {
console.warn('[Task] 处理 runtime_queue_sync 失败:', err);
}
}
let sawTaskCompleteEvent = false;
let sawTaskStoppedEvent = false;
// 处理新事件
if (data.events && data.events.length > 0) {
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
for (const event of data.events) {
if (this.currentTaskId !== taskId) {
taskPollDiag('task-poll-stale-event-loop-abort', {
requestId,
responseTaskId: data?.task_id || taskId,
activeTaskId: this.currentTaskId
});
return;
}
if (
event?.type === 'task_complete' ||
event?.type === 'task_stopped' ||
event?.type === 'error'
) {
jsonDebug('taskStore.poll:event', {
idx: event?.idx,
type: event?.type,
data: event?.data
});
}
if (event?.type === 'task_complete') {
sawTaskCompleteEvent = true;
}
if (event?.type === 'task_stopped') {
sawTaskStoppedEvent = true;
}
try {
eventHandler(event);
} catch (err) {
console.error('[Task] 处理事件失败:', err, event);
}
}
const interesting = data.events.filter((e: any) =>
[
'user_message',
'sub_agent_waiting',
'task_complete',
'task_stopped',
'error'
].includes(e?.type)
);
if (interesting.length > 0) {
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] events', {
taskId: this.currentTaskId,
from: this.lastEventIndex,
nextOffset: data.next_offset,
count: interesting.length,
items: interesting.map((e: any) => ({
idx: e?.idx,
type: e?.type,
sub_agent_notice: !!e?.data?.sub_agent_notice,
has_running_sub_agents: e?.data?.has_running_sub_agents,
has_running_background_commands: e?.data?.has_running_background_commands,
task_id: e?.data?.task_id
}))
});
}
this.lastEventIndex = data.next_offset;
}
// 兜底:如果后端状态已经是 canceled 但未返回 task_stopped 事件,
// 补发一个本地停止事件,确保前端解除“停止中”状态。
if (data.status === 'canceled' && !sawTaskStoppedEvent) {
debugLog('[Task] 状态已取消但未收到 task_stopped补发本地事件');
jsonDebug('taskStore.poll:emit-synthetic-task-stopped', {
taskId: data.task_id || this.currentTaskId,
conversation_id: data.conversation_id || null
});
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);
}
}
// 兜底:某些异常流程(如工具参数解析失败后恢复执行)可能没有 task_complete 事件,
// 但任务状态已经 succeeded。补发一次完成事件避免前端一直显示“工作中”。
if (data.status === 'succeeded' && !sawTaskCompleteEvent) {
debugLog('[Task] 状态已成功但未收到 task_complete补发本地事件');
jsonDebug('taskStore.poll:emit-synthetic-task-complete', {
taskId: data.task_id || this.currentTaskId,
conversation_id: data.conversation_id || null
});
try {
eventHandler({
type: 'task_complete',
data: {
task_id: data.task_id || this.currentTaskId,
conversation_id: data.conversation_id || null,
synthetic: true,
has_running_sub_agents: false,
has_running_background_commands: false
}
});
} catch (err) {
console.error('[Task] 处理补发 task_complete 失败:', err);
}
}
// 如果任务已完成,停止轮询
if (this.isTaskCompleted) {
debugLog('[Task] 任务已完成,停止轮询:', this.taskStatus);
this.stopPolling(`task-${this.taskStatus}`);
}
// 重置错误计数
this.pollingError = null;
this.pollingErrorCount = 0;
this.pollingWarned = false;
} catch (error) {
this.pollingErrorCount++;
this.pollingError = error.message;
const message = error?.message || String(error);
const isTimeoutLike = /timeout|timed out|abort|aborted/i.test(message);
console.error('[Task] 轮询失败:', error);
taskPollDiag('task-poll-failed', {
requestId,
taskId,
from: fromOffset,
elapsedMs: Date.now() - startedAt,
error: message,
isTimeoutLike,
pollingErrorCount: this.pollingErrorCount
});
debugNotifyLog('[DEBUG_NOTIFY][task] pollTaskEvents:error', {
taskId: this.currentTaskId,
from: this.lastEventIndex,
error: error?.message || String(error),
pollingErrorCount: this.pollingErrorCount
});
// 任务不存在时结束轮询
if (/HTTP 404|HTTP 410/.test(message)) {
this.stopPolling('task-not-found');
return;
}
// 连续失败达到阈值仅告警,不直接停轮询(避免后端仍运行但前端停止更新)
if (this.pollingErrorCount >= this.pollingFailThreshold && !this.pollingWarned) {
this.pollingWarned = true;
if ((window as any).__vueApp?.uiPushToast) {
(window as any).__vueApp.uiPushToast({
title: '轮询波动',
message: '消息更新暂时不稳定,正在自动重试',
type: 'warning',
duration: 5000
});
}
}
} finally {
this.pollingInFlight = false;
}
},
startPolling(eventHandler?: (event: any) => void) {
if (this.isPolling) {
debugLog('[Task] 轮询已在运行');
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling:already-running', {
taskId: this.currentTaskId,
lastEventIndex: this.lastEventIndex
});
return;
}
if (!this.currentTaskId) {
debugLog('[Task] 没有任务ID无法启动轮询');
return;
}
debugLog('[Task] 启动轮询:', this.currentTaskId);
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling', {
taskId: this.currentTaskId,
lastEventIndex: this.lastEventIndex
});
this.isPolling = true;
this.pollingError = null;
this.pollingErrorCount = 0;
this.pollingWarned = false;
// 如果没有传入 eventHandler从根实例获取
const handler = eventHandler || (window as any).__taskEventHandler;
if (!handler) {
console.error('[Task] 没有事件处理器,无法启动轮询');
taskPollDiag('task-poll-start-failed-no-handler', {
taskId: this.currentTaskId
});
this.isPolling = false;
return;
}
taskPollDiag('task-poll-start', {
taskId: this.currentTaskId,
from: this.lastEventIndex,
intervalMs: this.pollingIntervalMs,
timeoutMs: this.pollingRequestTimeoutMs
});
// 立即执行一次
void this.pollTaskEvents(handler);
// 设置定时轮询(单飞由 pollTaskEvents 内部保证)
this.pollingInterval = window.setInterval(() => {
void this.pollTaskEvents(handler);
}, this.pollingIntervalMs);
},
resumeTask(
taskId: string,
options: {
status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled';
resetOffset?: boolean;
eventHandler?: (event: any) => void;
} = {}
) {
if (!taskId) {
return;
}
debugNotifyLog('[DEBUG_NOTIFY][task] resumeTask', {
incomingTaskId: taskId,
currentTaskId: this.currentTaskId,
options
});
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] resumeTask', {
incomingTaskId: taskId,
currentTaskId: this.currentTaskId,
resetOffset: options?.resetOffset !== false
});
if (this.currentTaskId && this.currentTaskId !== taskId) {
this.stopPolling();
}
this.currentTaskId = taskId;
this.taskStatus = options.status || 'running';
if (options.resetOffset !== false) {
this.lastEventIndex = 0;
}
this.pollingError = null;
this.runtimeQueueSnapshotKey = '';
this.startPolling(options.eventHandler);
},
stopPolling(reason = 'manual') {
taskPollDiag('task-poll-stop', {
reason,
taskId: this.currentTaskId,
status: this.taskStatus,
from: this.lastEventIndex,
pollingErrorCount: this.pollingErrorCount
});
if (this.pollingInterval) {
debugLog('[Task] 停止轮询');
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
this.isPolling = false;
this.pollingInFlight = false;
this.pollingWarned = false;
this.runtimeQueueSnapshotKey = '';
this.currentTaskId = null; // 清除任务 ID
},
async cancelTask() {
if (!this.currentTaskId) {
return;
}
try {
debugLog('[Task] 取消任务:', this.currentTaskId);
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('取消任务失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '取消任务失败');
}
debugLog('[Task] 已发送取消请求,等待后端停止确认');
} catch (error) {
console.error('[Task] 取消任务失败:', error);
throw error;
}
},
async loadRunningTask(conversationId: string | null = null) {
try {
debugLog('[Task] 查找运行中的任务');
const response = await fetch('/api/tasks');
if (!response.ok) {
throw new Error('获取任务列表失败');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '获取任务列表失败');
}
// 查找当前对话的活动任务
const activeStatuses = new Set(['pending', 'running', 'cancel_requested']);
const runningTask = result.data.find(
(task: any) =>
activeStatuses.has(String(task.status || '')) &&
(!conversationId || task.conversation_id === conversationId)
);
if (runningTask) {
debugLog('[Task] 发现运行中的任务:', runningTask.task_id);
this.currentTaskId = runningTask.task_id;
this.taskStatus = runningTask.status;
this.taskCreatedAt = runningTask.created_at;
this.taskUpdatedAt = runningTask.updated_at;
this.pollingError = null;
this.pollingErrorCount = 0;
this.pollingWarned = false;
this.runtimeQueueSnapshotKey = '';
// 获取任务详情,计算已处理的事件数量
try {
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
if (detailResponse.ok) {
const detailResult = await detailResponse.json();
if (detailResult.success && detailResult.data.events) {
// 设置为当前事件数量,只获取新事件
this.lastEventIndex =
detailResult.data.next_offset || detailResult.data.events.length;
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
} else {
this.lastEventIndex = 0;
}
} else {
this.lastEventIndex = 0;
}
} catch (error) {
console.warn('[Task] 获取任务详情失败,从头开始:', error);
this.lastEventIndex = 0;
}
return runningTask;
}
debugLog('[Task] 没有运行中的任务');
return null;
} catch (error) {
console.error('[Task] 加载运行中任务失败:', error);
return null;
}
},
clearTask() {
debugLog('[Task] 清理任务状态');
this.stopPolling();
this.currentTaskId = null;
this.lastEventIndex = 0;
this.taskStatus = 'idle';
this.pollingError = null;
this.pollingErrorCount = 0;
this.pollingWarned = false;
this.taskCreatedAt = null;
this.taskUpdatedAt = null;
this.runtimeQueueSnapshotKey = '';
},
resetForNewConversation() {
debugLog('[Task] 重置任务状态(新对话)');
this.stopPolling();
this.currentTaskId = null;
this.lastEventIndex = 0;
this.taskStatus = 'idle';
this.pollingError = null;
this.pollingErrorCount = 0;
this.pollingWarned = false;
this.runtimeQueueSnapshotKey = '';
}
}
});