fix: improve sub-agent ui and state

This commit is contained in:
JOJO 2026-03-11 15:40:28 +08:00
parent f09621fd86
commit ed82fc966e
19 changed files with 681 additions and 67 deletions

View File

@ -22,6 +22,7 @@ function parseArgs() {
systemPromptFile: null,
outputFile: null,
statsFile: null,
progressFile: null,
agentId: null,
modelKey: null,
thinkingMode: null,
@ -40,6 +41,8 @@ function parseArgs() {
config.outputFile = args[++i];
} else if (arg === '--stats-file' && i + 1 < args.length) {
config.statsFile = args[++i];
} else if (arg === '--progress-file' && i + 1 < args.length) {
config.progressFile = args[++i];
} else if (arg === '--agent-id' && i + 1 < args.length) {
config.agentId = args[++i];
} else if (arg === '--model-key' && i + 1 < args.length) {
@ -82,6 +85,19 @@ function updateStats(statsFile, stats) {
}
}
function appendProgress(progressFile, entry) {
if (!progressFile) return;
try {
const dir = path.dirname(progressFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.appendFileSync(progressFile, `${JSON.stringify(entry)}\n`, 'utf8');
} catch (err) {
console.error(`写入进度失败: ${progressFile}`, err);
}
}
// 主函数
async function main() {
const config = parseArgs();
@ -404,6 +420,21 @@ async function main() {
process.exit(0);
}
let progressArgs = {};
try {
progressArgs = JSON.parse(toolCall.function.arguments || '{}');
} catch (err) {
progressArgs = { _raw: toolCall.function.arguments || '' };
}
const progressId = toolCall.id || `tool_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
appendProgress(config.progressFile, {
id: progressId,
tool: toolName,
status: 'running',
args: progressArgs,
ts: Date.now(),
});
// 执行其他工具
const result = await executeTool({
workspace: config.workspace,
@ -413,6 +444,14 @@ async function main() {
abortSignal: null,
});
appendProgress(config.progressFile, {
id: progressId,
tool: toolName,
status: result && result.success ? 'completed' : 'failed',
args: progressArgs,
ts: Date.now(),
});
// 更新统计
if (toolName === 'read_file') stats.files_read++;
else if (toolName === 'edit_file') stats.edit_files++;

View File

@ -108,6 +108,7 @@ class SubAgentManager:
system_prompt_file = task_root / "system_prompt.txt"
output_file = task_root / "output.json"
stats_file = task_root / "stats.json"
progress_file = task_root / "progress.jsonl"
# 构建用户消息
user_message = self._build_user_message(agent_id, summary, task, deliverables_path, timeout_seconds)
@ -127,6 +128,7 @@ class SubAgentManager:
"--system-prompt-file", str(system_prompt_file),
"--output-file", str(output_file),
"--stats-file", str(stats_file),
"--progress-file", str(progress_file),
"--agent-id", str(agent_id),
"--timeout", str(timeout_seconds),
]
@ -162,6 +164,7 @@ class SubAgentManager:
"task_root": str(task_root),
"output_file": str(output_file),
"stats_file": str(stats_file),
"progress_file": str(progress_file),
"pid": process.pid,
}
self.tasks[task_id] = task_record

View File

@ -165,6 +165,12 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id,
debug_log("[SubAgent] 用户请求停止,终止轮询")
break
# 若主对话仍在工具循环中,暂不消费完成事件,避免抢占 system 消息插入
if getattr(web_terminal, "_tool_loop_active", False):
debug_log("[SubAgent] 主对话工具循环中,延迟后台轮询发送 user 消息")
await asyncio.sleep(1)
continue
updates = manager.poll_updates()
debug_log(f"[SubAgent] poll_updates 返回 {len(updates)} 个更新")
@ -189,16 +195,20 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id,
debug_log(f"[SubAgent] 子智能体{agent_id}完成,状态: {status}")
# 构建 user 消息
user_message = f"""子智能体{agent_id} ({summary}) 已完成任务。
# 构建 user 消息(后台完成时才发送)
prefix = "这是一句系统自动发送的user消息用于通知你子智能体已经运行完成"
user_message = f"""{prefix}
子智能体{agent_id} ({summary}) 已完成任务
{result_summary}
交付目录{deliverables_dir}"""
debug_log(f"[SubAgent] 准备直接调用 process_message_task")
debug_log(f"[SubAgent] 消息内容: {user_message[:100]}...")
debug_log(f"[SubAgent] 准备发送 user_message: {user_message[:100]}...")
has_remaining = False
remaining_count = 0
try:
if task_id:
web_terminal._announced_sub_agent_tasks.add(task_id)
@ -209,10 +219,30 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id,
manager._save_state()
except Exception as exc:
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
sender('user_message', {
'message': user_message,
'conversation_id': conversation_id
})
# 计算剩余子智能体状态(用于前端清理等待标记)
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
web_terminal._announced_sub_agent_tasks = set()
announced = web_terminal._announced_sub_agent_tasks
running_tasks = [
task for task in manager.tasks.values()
if isinstance(task, dict)
and task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
and task.get("run_in_background")
and task.get("conversation_id") == conversation_id
]
pending_notice_tasks = [
task for task in manager.tasks.values()
if isinstance(task, dict)
and task.get("status") in TERMINAL_STATUSES.union({"terminated"})
and task.get("run_in_background")
and task.get("conversation_id") == conversation_id
and task.get("task_id") not in announced
and not task.get("notified")
]
remaining_count = len(running_tasks) + len(pending_notice_tasks)
has_remaining = remaining_count > 0
# 注册为后台任务,确保刷新后可恢复轮询
from .tasks import task_manager
workspace_id = getattr(workspace, "workspace_id", None) or "default"
@ -237,8 +267,23 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id,
session_data=session_data,
)
debug_log(f"[SubAgent] 已创建后台任务: task_id={rec.task_id}")
sender('user_message', {
'message': user_message,
'conversation_id': conversation_id,
'task_id': rec.task_id,
'sub_agent_notice': True,
'has_running_sub_agents': has_remaining,
'remaining_count': remaining_count
})
except Exception as e:
debug_log(f"[SubAgent] 创建后台任务失败,回退直接执行: {e}")
sender('user_message', {
'message': user_message,
'conversation_id': conversation_id,
'sub_agent_notice': True,
'has_running_sub_agents': has_remaining,
'remaining_count': remaining_count
})
try:
task = asyncio.create_task(handle_task_with_sender(
terminal=web_terminal,

View File

@ -4,6 +4,8 @@ import asyncio
import time
from typing import Dict, List, Optional
from modules.sub_agent_manager import TERMINAL_STATUSES
async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message):
"""轮询子智能体任务并通知前端,并把结果插入当前对话上下文。"""
@ -17,11 +19,43 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
try:
updates = manager.poll_updates()
debug_log(f"[SubAgent] poll inline={inline} updates={len(updates)}")
debug_log(f"[SubAgent] poll inline={inline} after_tool_call_id={after_tool_call_id} updates={len(updates)}")
except Exception as exc:
debug_log(f"子智能体状态检查失败: {exc}")
return
# 兜底:如果 poll_updates 没命中,但任务已被别处更新为终态且未通知,补发一次
if not updates:
synthesized = []
try:
for task_id, task in getattr(manager, "tasks", {}).items():
if not isinstance(task, dict):
continue
status = task.get("status")
if status not in TERMINAL_STATUSES.union({"terminated"}):
continue
if task.get("notified"):
continue
task_conv_id = task.get("conversation_id")
current_conv_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None)
if task_conv_id and current_conv_id and task_conv_id != current_conv_id:
continue
final_result = task.get("final_result")
if not final_result:
try:
final_result = manager._check_task_status(task)
except Exception:
final_result = None
if isinstance(final_result, dict):
synthesized.append(final_result)
except Exception as exc:
debug_log(f"[SubAgent] synthesized updates failed: {exc}")
synthesized = []
if synthesized:
updates = synthesized
debug_log(f"[SubAgent] synthesized updates count={len(updates)}")
for update in updates:
task_id = update.get("task_id")
task_info = manager.tasks.get(task_id) if task_id else None
@ -41,6 +75,7 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
message = update.get("system_message")
if not message:
debug_log(f"[SubAgent] update missing system_message: task={task_id} keys={list(update.keys())}")
continue
debug_log(f"[SubAgent] update task={task_id} inline={inline} msg={message}")
@ -72,16 +107,22 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
insert_index = idx + 1
break
# 直接插入 user 消息,确保下一轮调用能看到子智能体完成通知
# 运行中插入 system 消息,避免触发新的 user 轮次;非运行中保持 user 通知
insert_role = "system" if inline else "user"
if not inline:
prefix = "这是一句系统自动发送的user消息用于通知你子智能体已经运行完成"
if not message.startswith(prefix):
message = f"{prefix}\n\n{message}"
messages.insert(insert_index, {
"role": "user",
"role": insert_role,
"content": message,
"metadata": {"sub_agent_notice": True, "inline": inline, "task_id": task_id}
})
debug_log(f"[SubAgent] 插入子智能体通知位置: {insert_index}")
debug_log(f"[SubAgent] 插入子智能体通知位置: {insert_index} role={insert_role} after_tool_call_id={after_tool_call_id}")
sender('system_message', {
'content': message,
'inline': inline
'inline': inline,
'sub_agent_notice': True
})
maybe_mark_failure_from_message(web_terminal, message)

View File

@ -16,6 +16,8 @@ from config import TOOL_CALL_COOLDOWN
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag):
previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False)
web_terminal._tool_loop_active = True
# 执行每个工具
for tool_call in tool_calls:
# 检查停止标志
@ -51,6 +53,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
'reason': 'user_stop'
})
clear_stop_flag(client_sid, username)
web_terminal._tool_loop_active = previous_tool_loop_active
return {"stopped": True, "last_tool_call_time": last_tool_call_time}
# 工具调用间隔控制
@ -290,6 +293,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
})
clear_stop_flag(client_sid, username)
debug_log("[停止检测] 返回stopped=True")
web_terminal._tool_loop_active = previous_tool_loop_active
return {"stopped": True, "last_tool_call_time": last_tool_call_time}
else:
tool_result = await tool_task
@ -475,7 +479,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
"content": tool_message_content
})
if function_name not in {'write_file', 'edit_file'}:
debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates")
await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message)
await asyncio.sleep(0.2)
@ -484,5 +488,5 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
mark_force_thinking(web_terminal, reason=f"{function_name}_failed")
web_terminal._tool_loop_active = previous_tool_loop_active
return {"stopped": False, "last_tool_call_time": last_tool_call_time}

View File

@ -452,9 +452,24 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s
if not manager:
return jsonify({"success": True, "data": []})
try:
try:
# 防止不同进程创建的子智能体未被当前进程感知
manager._load_state()
except Exception:
pass
conversation_id = terminal.context_manager.current_conversation_id
data = manager.get_overview(conversation_id=conversation_id)
debug_log("[SubAgent] /api/sub_agents overview", {
if not data:
# 若当前对话暂无数据但存在运行中子智能体,回退为全局运行态,避免面板空白
all_overview = manager.get_overview(conversation_id=None)
if all_overview:
terminal_statuses = TERMINAL_STATUSES.union({"terminated"})
running_only = [item for item in all_overview if item.get("status") not in terminal_statuses]
if running_only:
data = running_only
debug_log(
"[SubAgent] /api/sub_agents overview "
+ json.dumps({
"conversation_id": conversation_id,
"count": len(data),
"tasks": [
@ -462,10 +477,11 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s
"task_id": item.get("task_id"),
"status": item.get("status"),
"run_in_background": item.get("run_in_background"),
"conversation_id": item.get("conversation_id")
"conversation_id": item.get("conversation_id"),
} for item in data
]
})
],
}, ensure_ascii=False)
)
if not hasattr(terminal, "_announced_sub_agent_tasks"):
terminal._announced_sub_agent_tasks = set()
announced = terminal._announced_sub_agent_tasks
@ -498,22 +514,82 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s
and (status in TERMINAL_STATUSES or status == "terminated")
)
item["notice_pending"] = notice_pending
debug_log("[SubAgent] /api/sub_agents notice_pending computed", {
debug_log(
"[SubAgent] /api/sub_agents notice_pending computed "
+ json.dumps({
"conversation_id": conversation_id,
"tasks": [
{
"task_id": item.get("task_id"),
"status": item.get("status"),
"run_in_background": item.get("run_in_background"),
"notice_pending": item.get("notice_pending")
"notice_pending": item.get("notice_pending"),
} for item in data
]
})
],
}, ensure_ascii=False)
)
return jsonify({"success": True, "data": data})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@conversation_bp.route('/api/sub_agents/<task_id>/activity', methods=['GET'])
@api_login_required
@with_terminal
def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""返回指定子智能体的活动记录(进度)。"""
manager = getattr(terminal, "sub_agent_manager", None)
if not manager:
return jsonify({"success": False, "error": "子智能体管理器不可用"}), 404
try:
try:
manager._load_state()
except Exception:
pass
task = manager.tasks.get(task_id)
if not task:
return jsonify({"success": False, "error": "未找到对应子智能体任务"}), 404
progress_file = task.get("progress_file")
if not progress_file:
task_root = task.get("task_root")
if task_root:
progress_file = str(Path(task_root) / "progress.jsonl")
entries: List[Dict[str, Any]] = []
limit = request.args.get("limit", "200")
try:
limit_num = max(1, min(int(limit), 1000))
except Exception:
limit_num = 200
if progress_file and Path(progress_file).exists():
try:
lines = Path(progress_file).read_text(encoding="utf-8").splitlines()
if limit_num:
lines = lines[-limit_num:]
for line in lines:
line = line.strip()
if not line:
continue
try:
entries.append(json.loads(line))
except Exception:
continue
except Exception as exc:
return jsonify({"success": False, "error": f"读取进度失败: {exc}"}), 500
payload = {
"task_id": task_id,
"status": task.get("status"),
"entries": entries,
}
return jsonify({"success": True, "data": payload})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@conversation_bp.route('/api/conversations/<conversation_id>/duplicate', methods=['POST'])
@api_login_required
@with_terminal

View File

@ -347,6 +347,7 @@
@confirm="handleConfirmReview"
/>
</transition>
<SubAgentActivityDialog />
<div
v-if="isMobileViewport"

View File

@ -9,6 +9,7 @@ import InputComposer from '../components/input/InputComposer.vue';
import AppShell from '../components/shell/AppShell.vue';
import ImagePicker from '../components/overlay/ImagePicker.vue';
import ConversationReviewDialog from '../components/overlay/ConversationReviewDialog.vue';
import SubAgentActivityDialog from '../components/overlay/SubAgentActivityDialog.vue';
export const appComponents = {
ChatArea,
@ -21,5 +22,6 @@ export const appComponents = {
InputComposer,
AppShell,
ImagePicker,
ConversationReviewDialog
ConversationReviewDialog,
SubAgentActivityDialog
};

View File

@ -566,6 +566,13 @@ export const taskPollingMethods = {
this.taskInProgress = true;
this.streamingMessage = false;
this.stopRequested = false;
if (data?.sub_agent_notice) {
if (typeof data?.has_running_sub_agents === 'boolean') {
this.waitingForSubAgent = data.has_running_sub_agents;
} else if (typeof data?.remaining_count === 'number') {
this.waitingForSubAgent = data.remaining_count > 0;
}
}
this.$forceUpdate();
this.conditionalScrollToBottom();
},

View File

@ -55,6 +55,43 @@ export const toolingMethods = {
this.toolActionIndex.clear();
},
clearPendingTools(reason = 'unspecified') {
this.messages.forEach(msg => {
if (!msg.actions) {
return;
}
msg.actions.forEach(action => {
if (action.type !== 'tool' || !action.tool) {
return;
}
const status =
typeof action.tool.status === 'string'
? action.tool.status.toLowerCase()
: '';
if (!status || ['preparing', 'running', 'pending', 'queued', 'stale'].includes(status)) {
action.tool.status = 'cancelled';
action.tool.message = action.tool.message || '已停止';
}
});
});
if (this.preparingTools && this.preparingTools.clear) {
this.preparingTools.clear();
}
if (this.activeTools && this.activeTools.clear) {
this.activeTools.clear();
}
if (this.toolActionIndex && this.toolActionIndex.clear) {
this.toolActionIndex.clear();
}
if (this.toolStacks && this.toolStacks.clear) {
this.toolStacks.clear();
}
if (typeof this.toolResetTracking === 'function') {
this.toolResetTracking();
}
debugLog('清理待处理工具', { reason });
},
hasPendingToolActions() {
const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0;
if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) {

View File

@ -207,6 +207,11 @@ export const uiMethods = {
selectPanelMode(mode) {
this.uiSetPanelMode(mode);
this.uiSetPanelMenuOpen(false);
if (mode === 'subAgents') {
// 切换到子智能体面板时立即刷新,避免等待轮询间隔
this.subAgentFetch();
this.subAgentStartPolling();
}
},
openPersonalPage() {
@ -754,11 +759,18 @@ export const uiMethods = {
},
addSystemMessage(content) {
if (this.hideSystemMessages) {
return;
}
this.chatAddSystemMessage(content);
this.$forceUpdate();
this.conditionalScrollToBottom();
},
appendSystemAction(content) {
this.addSystemMessage(content);
},
startTitleTyping(title: string, options: { animate?: boolean } = {}) {
if (this.titleTypingTimer) {
clearInterval(this.titleTypingTimer);

View File

@ -11,6 +11,8 @@ export function dataState() {
usePollingMode: true,
// 后台子智能体等待状态
waitingForSubAgent: false,
// 是否在对话区展示 system 消息
hideSystemMessages: true,
// 工具状态跟踪
preparingTools: new Map(),

View File

@ -0,0 +1,119 @@
<template>
<transition name="overlay-fade">
<div v-if="activeAgent" class="subagent-activity-overlay" @click.self="close">
<div class="subagent-activity-modal">
<div class="subagent-activity-header">
<div class="subagent-activity-title">
子智能体 #{{ activeAgent.agent_id || activeAgent.task_id }} 进度
</div>
<button type="button" class="subagent-activity-close" @click="close">×</button>
</div>
<div class="subagent-activity-meta">
<span class="subagent-activity-status" :class="activeAgent.status || ''">{{ activeAgent.status || 'running' }}</span>
<span class="subagent-activity-summary" v-if="activeAgent.summary">{{ activeAgent.summary }}</span>
</div>
<div class="subagent-activity-body">
<div v-if="activityError" class="subagent-activity-error">{{ activityError }}</div>
<div v-else-if="!displayItems.length" class="subagent-activity-empty">
{{ activityLoading ? '正在读取子智能体活动...' : '暂无活动记录' }}
</div>
<div v-else class="subagent-activity-list">
<div v-for="item in displayItems" :key="item.key" class="subagent-activity-item">
<span class="subagent-activity-text">{{ item.text }}</span>
<span class="subagent-activity-state" :class="item.state">{{ item.stateLabel }}</span>
</div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useSubAgentStore } from '@/stores/subAgent';
type ActivityEntry = {
id?: string;
tool?: string;
status?: string;
args?: Record<string, any>;
ts?: number;
};
const subAgentStore = useSubAgentStore();
const { activeAgent, activityEntries, activityLoading, activityError } = storeToRefs(subAgentStore);
const close = () => {
subAgentStore.closeSubAgent();
};
const normalizeStatus = (status?: string) => {
if (status === 'running' || status === 'in_progress') return 'running';
if (status === 'completed' || status === 'done' || status === 'success') return 'completed';
if (status === 'failed' || status === 'error') return 'failed';
return status || 'running';
};
const buildText = (entry: ActivityEntry, stateLabel: string) => {
const tool = entry.tool || '';
const args = entry.args || {};
if (tool === 'read_file') {
const path = args.path || args.file_path || '';
return `阅读 ${path} ${stateLabel}`;
}
if (tool === 'search_workspace') {
const query = args.query || args.keyword || '';
return `在工作区搜索 ${query} ${stateLabel}`;
}
if (tool === 'web_search') {
const query = args.query || args.q || '';
return `在互联网中搜索 ${query} ${stateLabel}`;
}
if (tool === 'extract_webpage') {
const url = args.url || '';
return `在互联网中提取 ${url} ${stateLabel}`;
}
if (tool === 'run_command') {
const command = args.command || '';
return `运行命令 ${command} ${stateLabel}`;
}
if (tool === 'edit_file') {
const path = args.path || args.file_path || '';
return `编辑 ${path} ${stateLabel}`;
}
if (tool === 'read_mediafile') {
const path = args.path || args.file_path || '';
return `读取媒体文件 ${path} ${stateLabel}`;
}
return `${tool || '工具'} ${stateLabel}`;
};
const displayItems = computed(() => {
const order: string[] = [];
const map = new Map<string, ActivityEntry>();
for (const entry of activityEntries.value || []) {
if (!entry) continue;
const key = entry.id || `${entry.tool || 'tool'}-${entry.ts || order.length}`;
if (!map.has(key)) {
map.set(key, { ...entry });
order.push(key);
} else {
map.set(key, { ...map.get(key), ...entry });
}
}
return order.map((key) => {
const item = map.get(key) || {};
const state = normalizeStatus(item.status);
const stateLabel = state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中';
return {
key,
state,
stateLabel,
text: buildText(item, stateLabel)
};
});
});
</script>

View File

@ -880,6 +880,22 @@ export async function initializeLegacySocket(ctx: any) {
ctx.taskInProgress = true;
ctx.streamingMessage = false;
ctx.stopRequested = false;
if (data?.sub_agent_notice && data?.task_id && ctx.usePollingMode) {
if (typeof data?.has_running_sub_agents === 'boolean') {
ctx.waitingForSubAgent = data.has_running_sub_agents;
} else if (typeof data?.remaining_count === 'number') {
ctx.waitingForSubAgent = data.remaining_count > 0;
}
(async () => {
try {
const { useTaskStore } = await import('../stores/task');
const taskStore = useTaskStore();
taskStore.resumeTask(data.task_id);
} catch (error) {
console.warn('恢复任务轮询失败', error);
}
})();
}
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
});

View File

@ -10,15 +10,34 @@ interface SubAgent {
conversation_id?: string;
}
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;
activityEntries: SubAgentActivityEntry[];
activityLoading: boolean;
activityError: string | null;
}
export const useSubAgentStore = defineStore('subAgent', {
state: (): SubAgentState => ({
subAgents: [],
pollTimer: null
pollTimer: null,
activityTimer: null,
activeAgent: null,
activityEntries: [],
activityLoading: false,
activityError: null
}),
actions: {
async fetchSubAgents() {
@ -56,13 +75,55 @@ export const useSubAgentStore = defineStore('subAgent', {
if (!agent || !agent.task_id) {
return;
}
const base = this.getBaseUrl();
const parentConv = agent.conversation_id || '';
const convSegment = this.stripConversationPrefix(parentConv);
const agentLabel = agent.agent_id ? `sub_agent${agent.agent_id}` : agent.task_id;
const pathSuffix = convSegment ? `/${convSegment}+${agentLabel}` : `/sub_agent/${agent.task_id}`;
const url = `${base}${pathSuffix}`;
window.open(url, '_blank');
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=200`);
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;
}
} catch (error: any) {
this.activityError = error?.message || String(error);
console.error('获取子智能体活动失败:', error);
} finally {
this.activityLoading = false;
}
},
stripConversationPrefix(conversationId: string) {
if (!conversationId) return '';

View File

@ -157,6 +157,22 @@ export const useTaskStore = defineStore('task', {
}, 150);
},
resumeTask(taskId: string, options: { status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled'; resetOffset?: boolean; eventHandler?: (event: any) => void } = {}) {
if (!taskId) {
return;
}
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.startPolling(options.eventHandler);
},
stopPolling() {
if (this.pollingInterval) {
debugLog('[Task] 停止轮询');

View File

@ -1890,6 +1890,143 @@
color: #fff;
}
.subagent-activity-overlay {
position: fixed;
inset: 0;
background: var(--theme-overlay-scrim);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 2200;
transition: opacity 0.25s ease, backdrop-filter 0.25s ease;
}
.subagent-activity-modal {
width: 720px;
height: 70vh;
background: var(--theme-surface-card);
border: 1px solid var(--theme-control-border-strong);
border-radius: 18px;
padding: 20px 22px;
box-shadow: var(--theme-shadow-soft);
display: flex;
flex-direction: column;
gap: 12px;
}
.subagent-activity-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.subagent-activity-title {
font-size: 16px;
font-weight: 600;
}
.subagent-activity-close {
border: none;
background: transparent;
color: var(--claude-text);
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
border-radius: 8px;
}
.subagent-activity-close:hover {
background: var(--theme-control-bg);
}
.subagent-activity-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--claude-text-secondary);
flex-wrap: wrap;
}
.subagent-activity-status {
padding: 2px 8px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.04em;
}
.subagent-activity-status.completed {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.subagent-activity-status.failed,
.subagent-activity-status.timeout {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.subagent-activity-body {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.subagent-activity-empty,
.subagent-activity-error {
color: var(--claude-text-secondary);
font-size: 13px;
}
.subagent-activity-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.subagent-activity-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
font-size: 13px;
}
.subagent-activity-text {
color: var(--claude-text);
line-height: 1.4;
word-break: break-all;
}
.subagent-activity-state {
flex-shrink: 0;
font-size: 12px;
padding: 2px 8px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.03em;
background: rgba(59, 130, 246, 0.12);
color: #3b82f6;
}
.subagent-activity-state.completed {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.subagent-activity-state.failed {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.confirm-dialog-fade-enter-active,
.confirm-dialog-fade-leave-active {
transition: opacity 0.25s ease, backdrop-filter 0.25s ease;

View File

@ -42,7 +42,7 @@ export const ICONS = Object.freeze({
export const TOOL_ICON_MAP = Object.freeze({
append_to_file: 'pencil',
close_sub_agent: 'octagon',
close_sub_agent: 'bot',
create_file: 'file',
create_folder: 'folder',
create_sub_agent: 'bot',
@ -66,7 +66,8 @@ export const TOOL_ICON_MAP = Object.freeze({
terminal_snapshot: 'clipboard',
unfocus_file: 'eye',
update_memory: 'brain',
wait_sub_agent: 'clock',
wait_sub_agent: 'bot',
get_sub_agent_status: 'bot',
web_search: 'search',
trigger_easter_egg: 'sparkles',
view_image: 'camera',

View File

@ -287,34 +287,29 @@ class DeepSeekClient:
def _merge_system_messages(self, messages: List[Dict]) -> List[Dict]:
"""
将多个 system 消息合并为一个部分模型仅支持单条 system
保留原有顺序把合并后的 system 放在第一条 system 的位置
仅合并最开头连续的 system 消息系统提示后续插入的 system 消息保持原样
"""
if not messages:
return messages
merged_contents: List[str] = []
new_messages: List[Dict] = []
first_system_index: Optional[int] = None
for msg in messages:
if msg.get("role") == "system":
if first_system_index is None:
first_system_index = len(new_messages)
content = msg.get("content", "")
idx = 0
while idx < len(messages) and messages[idx].get("role") == "system":
content = messages[idx].get("content", "")
if isinstance(content, str):
merged_contents.append(content)
else:
merged_contents.append(json.dumps(content, ensure_ascii=False))
else:
new_messages.append(msg)
idx += 1
if not merged_contents:
return messages
merged = {
"role": "system",
"content": "\n\n".join(c for c in merged_contents if c)
}
insert_at = first_system_index if first_system_index is not None else 0
new_messages.insert(insert_at, merged)
return new_messages
return [merged] + messages[idx:]
def set_deep_thinking_mode(self, enabled: bool):
"""配置深度思考模式(持续使用思考模型)。"""