fix(sub-agent): 修复网络受限时无法创建子智能体,并修复详情页只显示最近一次工具调用的问题

This commit is contained in:
JOJO 2026-06-19 20:23:44 +08:00
parent 09af37055a
commit 2766a8ab57
6 changed files with 76 additions and 25 deletions

View File

@ -14,6 +14,11 @@ SUB_AGENT_SERVICE_BASE_URL = os.environ.get("SUB_AGENT_SERVICE_URL", "http://127
SUB_AGENT_DEFAULT_TIMEOUT = int(os.environ.get("SUB_AGENT_DEFAULT_TIMEOUT", "180")) # 秒
SUB_AGENT_STATUS_POLL_INTERVAL = float(os.environ.get("SUB_AGENT_STATUS_POLL_INTERVAL", "2.0"))
# 子智能体进程自身的网络权限(与工具级 HOST_SANDBOX_NETWORK_PERMISSION 独立)。
# 子智能体需要调用模型 API因此默认给予完整网络工具级网络限制仍由
# HOST_SANDBOX_NETWORK_PERMISSION 控制,并由主进程对命令单独加沙箱。
SUB_AGENT_HOST_NETWORK_PERMISSION = os.environ.get("SUB_AGENT_HOST_NETWORK_PERMISSION", "full")
# 存储与并发限制
# 子智能体任务目录与状态文件属于运行态数据,跟随 DATA_DIR默认 ~/.agents/<mode>/data
SUB_AGENT_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), f"{DATA_DIR}/sub_agent_tasks")
@ -34,4 +39,5 @@ __all__ = [
"SUB_AGENT_STATE_FILE",
"SUB_AGENT_MAX_ACTIVE",
"SUB_AGENT_MODELS_CONFIG_FILE",
"SUB_AGENT_HOST_NETWORK_PERMISSION",
]

View File

@ -17,7 +17,12 @@ const { applyUsage, normalizeUsagePayload } = require('../utils/token_usage');
// ── stdout 输出 ──
function emit(type, data) {
const line = JSON.stringify({ type, ...data });
process.stdout.write(line + '\n');
try {
// 使用同步写并刷新,避免管道缓冲导致主进程不能实时读到进度
fs.writeSync(1, line + '\n');
} catch (err) {
process.stdout.write(line + '\n');
}
}
// ── 解析命令行参数 ──

View File

@ -16,6 +16,7 @@ from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING
from config import (
OUTPUT_FORMATS,
SUB_AGENT_DEFAULT_TIMEOUT,
SUB_AGENT_HOST_NETWORK_PERMISSION,
SUB_AGENT_MAX_ACTIVE,
SUB_AGENT_MODELS_CONFIG_FILE,
SUB_AGENT_STATE_FILE,
@ -188,7 +189,10 @@ class SubAgentManager:
return {"success": False, "error": "宿主机子智能体执行被拒绝HOST_SANDBOX_ENABLED=0"}
try:
command_str = self._join_shell_words(cmd)
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
# 子智能体进程需要调用模型 API使用独立的网络权限默认 full
# 其内部执行的 run_command 等命令仍通过 HOST_SANDBOX_NETWORK_PERMISSION
# 保持工具级网络限制。
network_permission = SUB_AGENT_HOST_NETWORK_PERMISSION
plan = build_host_sandbox_plan(
command_str, self.project_path, env, network_permission=network_permission
)
@ -813,15 +817,32 @@ class SubAgentManager:
if self.state_file.exists():
try:
data = json.loads(self.state_file.read_text(encoding="utf-8"))
self.tasks = data.get("tasks", {})
self.conversation_agents = data.get("conversation_agents", {})
loaded_tasks = data.get("tasks", {})
loaded_agents = data.get("conversation_agents", {})
except json.JSONDecodeError:
logger.warning("子智能体状态文件损坏,已忽略。")
self.tasks = {}
self.conversation_agents = {}
return
else:
self.tasks = {}
self.conversation_agents = {}
return
# 与已加载的运行态任务合并,避免覆盖仅存在于内存中的状态
# (例如 _stdout_lines、notified 等运行时标记)。
runtime_only_keys = {"_stdout_lines"}
merged_tasks: Dict[str, Dict] = {}
for task_id, task in loaded_tasks.items():
existing = self.tasks.get(task_id)
if existing:
for key in runtime_only_keys:
if key in existing:
task[key] = existing[key]
merged_tasks[task_id] = task
self.tasks = merged_tasks
self.conversation_agents = loaded_agents
if self.tasks:
migrated = False
for task in self.tasks.values():

View File

@ -1457,11 +1457,12 @@ def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserW
progress_file = str(Path(task_root) / "progress.jsonl")
entries: List[Dict[str, Any]] = []
limit = request.args.get("limit", "200")
# 默认返回足够多的记录,确保历史工具调用完整展示;同时保留上限防止异常大文件拖垮响应。
limit = request.args.get("limit", "100000")
try:
limit_num = max(1, min(int(limit), 1000))
limit_num = max(1, min(int(limit), 100000))
except Exception:
limit_num = 200
limit_num = 100000
if progress_file and Path(progress_file).exists():
try:

View File

@ -111,11 +111,6 @@ const buildText = (entry: ActivityEntry) => {
return `${tool || '工具'}`;
};
const isTerminalStatus = (status?: string) => {
const normalized = (status || '').toString().toLowerCase();
return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized);
};
const canStop = computed(() => {
if (!activeAgent.value?.task_id) return false;
return !isTerminalStatus(activeAgent.value.status);
@ -137,26 +132,49 @@ const handleStop = async () => {
}
};
const isTerminalStatus = (status?: string) => {
const normalized = (status || '').toString().toLowerCase();
return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized);
};
const displayItems = computed(() => {
const order: string[] = [];
const map = new Map<string, ActivityEntry>();
for (const entry of activityEntries.value || []) {
const entries = activityEntries.value || [];
const groups: { key: string; entry: ActivityEntry }[] = [];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
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 });
const baseKey = entry.id || `${entry.tool || 'tool'}-${entry.ts || i}`;
const lastGroup = groups[groups.length - 1];
// id running -> completed
// id id
if (
lastGroup &&
(lastGroup.entry.id === entry.id || lastGroup.key === baseKey) &&
!isTerminalStatus(lastGroup.entry.status)
) {
lastGroup.entry = { ...lastGroup.entry, ...entry };
continue;
}
// keyid
let key = baseKey;
let suffix = 0;
while (groups.some((g) => g.key === key)) {
suffix++;
key = `${baseKey}--${suffix}`;
}
groups.push({ key, entry: { ...entry } });
}
return order.map((key) => {
const item = map.get(key) || {};
return groups.map((group) => {
const item = group.entry;
const state = normalizeStatus(item.status);
const stateLabel = state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中';
return {
key,
key: group.key,
state,
stateLabel,
text: buildText(item)

View File

@ -121,7 +121,7 @@ export const useSubAgentStore = defineStore('subAgent', {
if (!taskId) return;
this.activityLoading = true;
try {
const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=200`);
const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=100000`);
if (!resp.ok) {
throw new Error(await resp.text());
}