停止按钮现在只停主智能体,与后台任务无关。 - 后端 cancel_task 简化:移除两段式判断、force_stop参数、is_ma_mode分支 - 删除 _force_stop_multi_agent 方法 - 新增 /api/sub_agents/stop_all 接口(mode=terminate/soft_stop) - 前端 stopTask 移除 forceStop 概念,只取消主智能体 - 取消期间不再二次点击终结子智能体 子智能体停止按钮: - InputComposer state bar 加'x个子智能体运行中'按钮 - 独立浮层显示,不依赖 git 栏(参考 askuser 方案) - 点击传统模式弹窗终结;多智能体弹窗软停止 - App.vue 新增 handleStopAllSubAgents 调 ui.confirmDialog 后台指令停止按钮: - LeftPanel 后台指令标签右边加停止按钮 - 仅当 panelMode=backgroundCommands 且存在运行中后台指令时显示 - 调 backgroundCommandStore.stopAllCommands 无二次确认 showStopIcon 只看主对话 streaming 状态: - 后台任务在跑但主对话空闲时显示发送按钮 - 多智能体 / 传统模式统一处理 子智能体 store 加 stopAllAgents(mode) action 调新 API useLegacySocket task_stopped handler 简化: - 移除 multi_agent_reloaded 重载 - 移除'再次按下停止按钮以结束后台任务'提示 - 保留 has_running_multi_agent/sub_agents 用于 taskInProgress 判定 清理调试日志:[TaskCancel][stop_debug], [STOP_DEBUG] 全部去掉 保留 ma_debug 框架用于多智能体调试
223 lines
6.9 KiB
TypeScript
223 lines
6.9 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import { useUiStore } from './ui';
|
|
|
|
interface SubAgent {
|
|
task_id: string;
|
|
agent_id?: string | number;
|
|
status?: string;
|
|
summary?: string;
|
|
last_tool?: string;
|
|
conversation_id?: string;
|
|
notice_pending?: boolean;
|
|
display_name?: string;
|
|
current_context_tokens?: number;
|
|
}
|
|
|
|
interface SubAgentActivityEntry {
|
|
id?: string;
|
|
tool?: string;
|
|
status?: string;
|
|
args?: Record<string, any>;
|
|
ts?: number;
|
|
error?: string;
|
|
}
|
|
|
|
interface SubAgentState {
|
|
subAgents: SubAgent[];
|
|
pollTimer: ReturnType<typeof setInterval> | null;
|
|
activityTimer: ReturnType<typeof setInterval> | null;
|
|
activeAgent: SubAgent | null;
|
|
stoppingTaskIds: Record<string, boolean>;
|
|
activityEntries: SubAgentActivityEntry[];
|
|
activityLoading: boolean;
|
|
activityError: string | null;
|
|
}
|
|
|
|
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'timeout', 'terminated']);
|
|
|
|
export const useSubAgentStore = defineStore('subAgent', {
|
|
state: (): SubAgentState => ({
|
|
subAgents: [],
|
|
pollTimer: null,
|
|
activityTimer: null,
|
|
activeAgent: null,
|
|
stoppingTaskIds: {},
|
|
activityEntries: [],
|
|
activityLoading: false,
|
|
activityError: null
|
|
}),
|
|
actions: {
|
|
async fetchSubAgents() {
|
|
try {
|
|
const resp = await fetch('/api/sub_agents');
|
|
if (!resp.ok) {
|
|
throw new Error(await resp.text());
|
|
}
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
this.subAgents = Array.isArray(data.data) ? data.data : [];
|
|
const activeTaskId = this.activeAgent?.task_id;
|
|
if (activeTaskId) {
|
|
const latest = this.subAgents.find((item) => item.task_id === activeTaskId);
|
|
if (latest) {
|
|
this.activeAgent = { ...latest };
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取子智能体列表失败:', error);
|
|
}
|
|
},
|
|
startPolling() {
|
|
if (this.pollTimer) {
|
|
return;
|
|
}
|
|
const uiStore = useUiStore();
|
|
this.pollTimer = setInterval(() => {
|
|
if (uiStore.panelMode === 'subAgents') {
|
|
this.fetchSubAgents();
|
|
}
|
|
}, 5000);
|
|
},
|
|
stopPolling() {
|
|
if (this.pollTimer) {
|
|
clearInterval(this.pollTimer);
|
|
this.pollTimer = null;
|
|
}
|
|
},
|
|
openSubAgent(agent: SubAgent) {
|
|
if (!agent || !agent.task_id) {
|
|
return;
|
|
}
|
|
this.activeAgent = agent;
|
|
this.activityEntries = [];
|
|
this.activityError = null;
|
|
this.fetchSubAgentActivity(agent.task_id);
|
|
this.startActivityPolling();
|
|
},
|
|
closeSubAgent() {
|
|
this.stopActivityPolling();
|
|
this.activeAgent = null;
|
|
this.activityEntries = [];
|
|
this.activityError = null;
|
|
this.activityLoading = false;
|
|
},
|
|
startActivityPolling() {
|
|
if (this.activityTimer) {
|
|
return;
|
|
}
|
|
this.activityTimer = setInterval(() => {
|
|
const taskId = this.activeAgent?.task_id;
|
|
if (taskId) {
|
|
this.fetchSubAgentActivity(taskId);
|
|
}
|
|
}, 2000);
|
|
},
|
|
stopActivityPolling() {
|
|
if (this.activityTimer) {
|
|
clearInterval(this.activityTimer);
|
|
this.activityTimer = null;
|
|
}
|
|
},
|
|
async fetchSubAgentActivity(taskId: string) {
|
|
if (!taskId) return;
|
|
this.activityLoading = true;
|
|
try {
|
|
const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=100000`);
|
|
if (!resp.ok) {
|
|
throw new Error(await resp.text());
|
|
}
|
|
const data = await resp.json();
|
|
if (data && data.success && data.data) {
|
|
const entries = Array.isArray(data.data.entries) ? data.data.entries : [];
|
|
this.activityEntries = entries;
|
|
if (this.activeAgent && this.activeAgent.task_id === taskId) {
|
|
this.activeAgent = {
|
|
...this.activeAgent,
|
|
status: data.data.status || this.activeAgent.status
|
|
};
|
|
}
|
|
const status = (data.data.status || '').toString();
|
|
if (TERMINAL_STATUSES.has(status)) {
|
|
this.stopActivityPolling();
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
this.activityError = error?.message || String(error);
|
|
console.error('获取子智能体活动失败:', error);
|
|
} finally {
|
|
this.activityLoading = false;
|
|
}
|
|
},
|
|
async terminateSubAgent(taskId: string) {
|
|
const normalizedId = (taskId || '').toString().trim();
|
|
if (!normalizedId) {
|
|
return { success: false, error: 'task_id 不能为空' };
|
|
}
|
|
this.stoppingTaskIds = {
|
|
...this.stoppingTaskIds,
|
|
[normalizedId]: true
|
|
};
|
|
try {
|
|
const resp = await fetch(`/api/sub_agents/${encodeURIComponent(normalizedId)}/terminate`, {
|
|
method: 'POST'
|
|
});
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !data?.success) {
|
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
|
}
|
|
await this.fetchSubAgents();
|
|
if (this.activeAgent?.task_id === normalizedId) {
|
|
await this.fetchSubAgentActivity(normalizedId);
|
|
this.activeAgent = {
|
|
...this.activeAgent,
|
|
status: 'terminated'
|
|
};
|
|
}
|
|
return { success: true, data: data?.data || null };
|
|
} catch (error: any) {
|
|
const message = error?.message || String(error);
|
|
return { success: false, error: message };
|
|
} finally {
|
|
const next = { ...this.stoppingTaskIds };
|
|
delete next[normalizedId];
|
|
this.stoppingTaskIds = next;
|
|
}
|
|
},
|
|
async stopAllAgents(mode: 'terminate' | 'soft_stop'): Promise<{ success: boolean; stoppedCount?: number; error?: string }> {
|
|
try {
|
|
const resp = await fetch('/api/sub_agents/stop_all', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode })
|
|
});
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !data?.success) {
|
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
|
}
|
|
await this.fetchSubAgents();
|
|
return { success: true, stoppedCount: data?.data?.stopped_count || 0 };
|
|
} catch (error: any) {
|
|
const message = error?.message || String(error);
|
|
return { success: false, error: message };
|
|
}
|
|
},
|
|
stripConversationPrefix(conversationId: string) {
|
|
if (!conversationId) return '';
|
|
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
|
|
},
|
|
getBaseUrl() {
|
|
const override = (window as any).SUB_AGENT_BASE_URL || (window as any).__SUB_AGENT_BASE_URL__;
|
|
if (override && typeof override === 'string') {
|
|
return override.replace(/\/$/, '');
|
|
}
|
|
const { protocol, hostname } = window.location;
|
|
if (hostname && hostname.includes('agent.')) {
|
|
const mappedHost = hostname.replace('agent.', 'subagent.');
|
|
return `${protocol}//${mappedHost}`;
|
|
}
|
|
return `${protocol}//${hostname}:8092`;
|
|
}
|
|
}
|
|
});
|