fix(sub-agent): 修复网络受限时无法创建子智能体,并修复详情页只显示最近一次工具调用的问题
This commit is contained in:
parent
09af37055a
commit
2766a8ab57
@ -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",
|
||||
]
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 解析命令行参数 ──
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
// 生成唯一 key:id 重复时加上序号
|
||||
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)
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user