fix: improve sub-agent ui and state
This commit is contained in:
parent
f09621fd86
commit
ed82fc966e
@ -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++;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,8 +479,8 @@ 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'}:
|
||||
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)
|
||||
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}
|
||||
|
||||
|
||||
@ -452,20 +452,36 @@ 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", {
|
||||
"conversation_id": conversation_id,
|
||||
"count": len(data),
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": item.get("task_id"),
|
||||
"status": item.get("status"),
|
||||
"run_in_background": item.get("run_in_background"),
|
||||
"conversation_id": item.get("conversation_id")
|
||||
} for item in data
|
||||
]
|
||||
})
|
||||
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": [
|
||||
{
|
||||
"task_id": item.get("task_id"),
|
||||
"status": item.get("status"),
|
||||
"run_in_background": item.get("run_in_background"),
|
||||
"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", {
|
||||
"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")
|
||||
} for item in data
|
||||
]
|
||||
})
|
||||
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"),
|
||||
} 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
|
||||
|
||||
@ -347,6 +347,7 @@
|
||||
@confirm="handleConfirmReview"
|
||||
/>
|
||||
</transition>
|
||||
<SubAgentActivityDialog />
|
||||
|
||||
<div
|
||||
v-if="isMobileViewport"
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -11,6 +11,8 @@ export function dataState() {
|
||||
usePollingMode: true,
|
||||
// 后台子智能体等待状态
|
||||
waitingForSubAgent: false,
|
||||
// 是否在对话区展示 system 消息
|
||||
hideSystemMessages: true,
|
||||
|
||||
// 工具状态跟踪
|
||||
preparingTools: new Map(),
|
||||
|
||||
119
static/src/components/overlay/SubAgentActivityDialog.vue
Normal file
119
static/src/components/overlay/SubAgentActivityDialog.vue
Normal 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>
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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 '';
|
||||
|
||||
@ -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] 停止轮询');
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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", "")
|
||||
if isinstance(content, str):
|
||||
merged_contents.append(content)
|
||||
else:
|
||||
merged_contents.append(json.dumps(content, ensure_ascii=False))
|
||||
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:
|
||||
new_messages.append(msg)
|
||||
merged_contents.append(json.dumps(content, ensure_ascii=False))
|
||||
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):
|
||||
"""配置深度思考模式(持续使用思考模型)。"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user