//! Computer Agent Runner 聊天处理器 //! //! 处理 Computer Agent Runner 模式的聊天请求。 //! 与 RCoder 的 project_id 容器模式不同,ComputerAgentRunner 使用 user_id 作为容器标识。 //! //! ## 请求流程 //! ```text //! POST /computer/chat { user_id, project_id?, prompt, ... } //! ↓ //! 1. 验证 user_id //! 2. 生成 project_id(若未提供) //! 3. get_or_create_container_for_user(user_id) //! - 挂载配置: config.yml mounts (配置化管理) //! - 宿主机: /computer-project-workspace/{user_id} → 容器: /home/user //! 4. 创建项目工作目录: /home/user/{project_id} (通过挂载自动同步) //! 5. 创建/更新项目和会话信息 //! 6. gRPC Chat RPC → agent_runner (带 project_id) //! 7. 更新会话映射 //! 8. 返回 ChatResponse //! ``` //! //! 注意:Resume 会话的降级逻辑已在 agent_runner 层通过 list_sessions API 预检查处理 use axum::{extract::State, http::HeaderMap}; use shared_types::{ChatResponse, ComputerChatRequest, IsolationType}; use std::sync::Arc; use tracing::{debug, error, info, instrument, warn}; use crate::{AppError, HttpResult, router::AppState, service::ComputerContainerManager}; use docker_manager::ContainerBasicInfo; use super::utils::{ I18nJsonOrQuery, extract_grpc_addr_with_port, get_locale_from_headers, get_realtime_container_ip, project_dir, build_computer_workspace_path, }; /// 处理 Computer Agent 聊天请求 /// /// 1. 根据 user_id 获取或创建用户容器 /// 2. 将聊天请求转发到容器内的 agent_runner 服务 /// 3. 更新会话映射 /// /// 注意: /// - user_id 是必填的,用于标识用户的容器 /// - project_id 可选,若未提供则自动生成 /// - 一个用户容器内可以运行多个 project_id 的 Agent 实例 /// - Resume 会话的降级逻辑已在 agent_runner 层通过 list_sessions API 预检查处理 #[utoipa::path( post, path = "/computer/chat", request_body( content = ComputerChatRequest, description = "Computer Agent 聊天请求,包含 user_id 和 prompt", content_type = "application/json" ), responses( ( status = 200, description = "成功处理聊天请求", body = HttpResult, example = json!({ "success": true, "data": { "project_id": "proj_456", "session_id": "session789", "error": null, "request_id": "req_123456789" }, "error": null }) ), ( status = 400, description = "请求参数错误(如 user_id 为空)", body = HttpResult ), ( status = 401, description = "API Key 鉴权失败", body = HttpResult ), ( status = 500, description = "服务器内部错误", body = HttpResult ) ), tag = "computer", operation_id = "handle_computer_chat", summary = "发送聊天消息到 Computer Agent", description = "根据 user_id 动态管理容器,一个用户对应一个带桌面环境的容器" )] #[instrument(skip(state, request), fields(user_id = %request.user_id, project_id = ?request.project_id))] pub async fn handle_computer_chat( State(state): State>, headers: HeaderMap, I18nJsonOrQuery(mut request): I18nJsonOrQuery, ) -> Result, AppError> { // 获取语言设置 let locale = get_locale_from_headers(&headers); // 1. 验证 user_id if request.user_id.trim().is_empty() { error!("[COMPUTER_CHAT] user_id is required"); return Ok(HttpResult::error_with_locale( shared_types::error_codes::ERR_VALIDATION, locale, )); } let user_id = request.user_id.clone(); // ========== 隔离类型参数校验 ========== // IF pod_id IS NOT NULL THEN isolation_type, tenant_id, space_id 必须非空 if request.pod_id.is_some() { if request.isolation_type.is_none() { error!("[COMPUTER_CHAT] Validation failed: isolation_type is required when pod_id is provided"); return Ok(HttpResult::error_with_message( shared_types::error_codes::ERR_VALIDATION, locale, "isolation_type is required when pod_id is provided", )); } if request.tenant_id.is_none() { error!("[COMPUTER_CHAT] Validation failed: tenant_id is required when pod_id is provided"); return Ok(HttpResult::error_with_message( shared_types::error_codes::ERR_VALIDATION, locale, "tenant_id is required when pod_id is provided", )); } if request.space_id.is_none() { error!("[COMPUTER_CHAT] Validation failed: space_id is required when pod_id is provided"); return Ok(HttpResult::error_with_message( shared_types::error_codes::ERR_VALIDATION, locale, "space_id is required when pod_id is provided", )); } // 验证 isolation_type 值有效(大小写不敏感) if let Some(ref it) = request.isolation_type { if IsolationType::from_str(it).is_err() { error!("[COMPUTER_CHAT] Validation failed: invalid isolation_type '{}', expected tenant|space|project", it); return Ok(HttpResult::error_with_message( shared_types::error_codes::ERR_VALIDATION, locale, &format!("invalid isolation_type '{}', expected: tenant, space, project", it), )); } } // 记录验证通过的参数(此时 pod_id, isolation_type, tenant_id, space_id 必定为 Some) if let (Some(pid), Some(it), Some(tid), Some(sid)) = ( request.pod_id.as_deref(), request.isolation_type.as_deref(), request.tenant_id.as_deref(), request.space_id.as_deref(), ) { info!( "🔒 [COMPUTER_CHAT] Isolation parameters validated: pod_id={}, isolation_type={}, tenant_id={}, space_id={}", pid, it, tid, sid ); } } // 2. 生成或使用提供的 project_id let project_id = match &request.project_id { Some(id) if !id.trim().is_empty() => id.clone(), _ => { let generated_id = crate::service::container_manager::generate_project_id(); request.project_id = Some(generated_id.clone()); generated_id } }; info!( "🚀 [COMPUTER_CHAT] Starting to process request: user_id={}, project_id={}, session_id={:?}, prompt_len={}, attachments={}, model_provider={:?}, agent_config={:?}", user_id, project_id, request.session_id, request.prompt.len(), request.attachments.len(), request.model_provider, request.agent_config ); // 3. 验证资源限制配置 if let Some(ref agent_config) = request.agent_config { if let Some(ref resource_limits) = agent_config.resource_limits { if let Err(e) = resource_limits.validate() { error!("[COMPUTER_CHAT] Resource limits validation failed: {}", e); return Ok(HttpResult::error_with_locale( shared_types::error_codes::ERR_INVALID_RESOURCE_LIMITS, locale, )); } } } // 4. === 并发保护:检查是否有其他请求正在创建同一用户的容器 === // 使用原子标记(DashMap)避免并发请求互相干扰,无死锁风险 let mut waited_container_info: Option = None; if let Some(creating_since) = state.pod_creating.get(&user_id) { let elapsed = creating_since.elapsed(); drop(creating_since); // 释放 DashMap ref // 标记超过 60 秒视为过期(创建方可能已崩溃),忽略并继续 if elapsed < std::time::Duration::from_secs(60) { info!( "⏳ [COMPUTER_CHAT] Container is being created, waiting for completion: user_id={}, elapsed={:?}", user_id, elapsed ); // 轮询等待容器就绪(最多等 30 秒,每秒检查一次) for wait_sec in 1..=30 { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; // 标记已被移除 = 创建完成 if !state.pod_creating.contains_key(&user_id) { // 尝试获取容器信息 if let Ok(runtime) = docker_manager::runtime::RuntimeManager::get().await { if let Ok(Some(info)) = runtime .get_container_info_by_identifier( &user_id, &shared_types::ServiceType::ComputerAgentRunner, ) .await { info!( "✅ [COMPUTER_CHAT] Wait successful, container is ready (waited {}s): user_id={}, container_id={}", wait_sec, user_id, info.container_id ); waited_container_info = Some(info); break; } } } if wait_sec % 5 == 0 { debug!( "[COMPUTER_CHAT] Still waiting for container creation: user_id={}, {}s elapsed", user_id, wait_sec ); } } if waited_container_info.is_none() { // 等待超时,继续正常的创建流程(此时标记可能已过期被清理) warn!( "⚠️ [COMPUTER_CHAT] Wait for container creation timeout (30s), will try to create: user_id={}", user_id ); } } else { // 标记过期,清理后继续 warn!( "⚠️ [COMPUTER_CHAT] Creation marker expired ({:?}), cleaning up and continuing", elapsed ); state.pod_creating.remove(&user_id); } } // 5. 获取或创建用户容器 let container_info = if let Some(info) = waited_container_info { // 使用等待获得的容器信息 info!( "📦 [COMPUTER_CHAT] Using ready container (waiting for other request to finish creation): user_id={}, container_id={}", user_id, info.container_id ); info } else { // 正常创建容器 - 设置标记防止并发 state .pod_creating .insert(user_id.clone(), std::time::Instant::now()); let result = ComputerContainerManager::get_or_create_container_for_user( &user_id, request .agent_config .as_ref() .and_then(|c| c.resource_limits.clone()), request.pod_id.as_deref(), request.isolation_type.as_deref(), request.tenant_id.as_deref(), request.space_id.as_deref(), ) .await; // 清除标记(无论成功还是失败) state.pod_creating.remove(&user_id); match result { Ok(info) => info, Err(e) => { error!("[COMPUTER_CHAT] Failed to get or create container: {}", e); return Ok(HttpResult::error_with_locale( shared_types::error_codes::ERR_CONTAINER_ERROR, locale, )); } } }; // 🛡️ 二次验证:确保容器 IP 非空 // 容器管理器应该已经处理了空 IP 的情况,但缓存/Docker API 可能返回不一致结果 // 如果 IP 为空,先清理旧容器再强制重建(不返回错误给客户端) let container_info = if container_info.container_ip.trim().is_empty() { warn!( "⚠️ [COMPUTER_CHAT] Container has empty IP after get_or_create, cleaning up and recreating: \ user_id={}, old_container_id={}", user_id, container_info.container_id ); // 必须先清理旧容器,否则 create_container 发现同名 "running" 容器会复用它 let container_identifier = request.pod_id.as_deref().unwrap_or(&user_id); if let Ok(runtime) = docker_manager::runtime::RuntimeManager::get().await { if let Err(e) = runtime .stop_container_by_identifier( container_identifier, &shared_types::ServiceType::ComputerAgentRunner, ) .await { warn!( "⚠️ [COMPUTER_CHAT] Failed to cleanup broken container before recreate: {}", e ); } } ComputerContainerManager::force_create_container_for_user( &user_id, request .agent_config .as_ref() .and_then(|c| c.resource_limits.clone()), request.pod_id.as_deref(), request.isolation_type.as_deref(), request.tenant_id.as_deref(), request.space_id.as_deref(), ) .await .map_err(|e| { error!("[COMPUTER_CHAT] Force recreate container failed: {}", e); AppError::with_message( shared_types::error_codes::ERR_CONTAINER_ERROR, format!("Container recreation failed: {}", e), ) })? } else { container_info }; info!( "✅ [COMPUTER_CHAT] Container ready: user_id={}, container_id={}, ip={}", user_id, container_info.container_id, container_info.container_ip ); // 🔍 检测 user_id 变化:同一个 project_id 被不同的 user_id 请求 // 这通常意味着负载测试脚本使用了多个不同的 user_id,会导致创建多个容器浪费资源 if let Some(existing_info) = state.get_project(&project_id) { if let Some(existing_user_id) = existing_info.user_id() { if existing_user_id != user_id { warn!( "⚠️ [USER_ID_MISMATCH] Detected user_id change for project_id: \ project_id={}, original user_id={}, new user_id={}, time={}. \ This may be caused by load test scripts using different user_ids, \ which creates multiple containers and wastes resources. \ Please ensure the same project_id uses the same user_id in your test scripts.", project_id, existing_user_id, user_id, chrono::Utc::now().to_rfc3339() ); } } } // 🛡️ 关键修复:容器创建成功后立即插入 DuckDB 记录 // 这样可以防止孤立容器清理器误判并清理刚创建的容器 // // 必须在 gRPC 请求之前就插入记录,因为: // 1. 孤立容器清理器会检查 DuckDB 中是否存在该 user_id 的记录 // 2. 如果记录不存在,容器会被判定为孤立并清理 // 3. gRPC 请求是异步的,可能需要较长时间才能返回 ensure_project_mapping_in_state(&state, &user_id, &project_id, &container_info, &request)?; // 请求到达时立即更新活动时间(不等待请求执行结果) // 这样可以防止在 gRPC 请求期间被 cleanup_task 误清理 // 注意:这里使用 project_id 而不是 user_id,因为 DuckDB 的 key 是 project_id state.update_activity(&project_id); debug!( "🔄 [COMPUTER_CHAT] Updated activity time: project_id={}", project_id ); // 5. 创建项目工作目录(在用户容器内) // Computer Agent Runner 需要在用户工作区内为 project_id 创建子目录 if let Err(e) = ensure_project_workspace_exists( request.isolation_type.as_deref(), request.tenant_id.as_deref(), request.space_id.as_deref(), &user_id, &project_id, ) .await { error!("[COMPUTER_CHAT] Failed to create project workspace: {}", e); return Ok(HttpResult::error_with_locale( shared_types::error_codes::ERR_WORKSPACE_ERROR, locale, )); } // 6. 注册 VNC 后端到 Pingora(用于 WebSocket 代理) if let Some(ref pingora_service) = state.pingora_service { pingora_service.add_vnc_backend(&user_id, &container_info.container_ip); debug!( "🔗 [COMPUTER_CHAT] VNC backend registered: user_id={} -> {}", user_id, container_info.container_ip ); } // 6.5. 🆕 主动查询 Agent 状态 (User Request) // 在转发请求前,主动查询 Agent 状态,确保状态是最新的。 // 这有助于在容器重启后,确认 Agent 是否真正处于空闲状态。 { // 💫 使用实时 IP 获取,避免 restart 后 IP 过期的问题 let grpc_addr_result = async { let container_ip = get_realtime_container_ip( &container_info.container_name, &container_info.container_ip, &state.container_prefix_rcoder, &state.container_prefix_computer, ) .await .map_err(|e| format!("IP resolution error: {}", e))?; Ok::<_, String>(format!( "{}:{}", container_ip, shared_types::GRPC_DEFAULT_PORT )) } .await; if let Ok(grpc_addr) = grpc_addr_result { debug!("[COMPUTER_CHAT] Checking Agent status: {}", grpc_addr); if let Ok(mut client) = state.grpc_pool.get_client(&grpc_addr).await { let status_req = shared_types::grpc::GetStatusRequest { project_id: project_id.clone(), session_id: "".to_string(), // 我们只关心 project 级别的状态 }; let mut grpc_request = crate::grpc::new_request_with_locale(status_req, locale); grpc_request.set_timeout(std::time::Duration::from_secs(5)); match client.get_status(grpc_request).await { Ok(resp) => { let status = resp.into_inner().status; info!( "📊 [COMPUTER_CHAT] Agent current status: project_id={}, status={}", project_id, status ); // 如果状态是 idle,我们可以更有信心地继续 } Err(e) => { warn!("[COMPUTER_CHAT] Failed to get Agent status: {}", e); // Query failed不阻止请求继续,可能是网络波动,让后续的 Chat 请求去处理 } } } } } // 7. 🆕 自动查找 session_id 逻辑 // 如果用户没有传递 session_id,尝试从状态中查找最新的 session_id let session_id_to_use = match &request.session_id { Some(sid) if !sid.is_empty() => { debug!("[COMPUTER_CHAT] Using session_id: {}", sid); sid.clone() } _ => { // 用户没有传递 session_id,尝试查找最新的 match state.get_project(&project_id) { Some(project_info) => { let existing_session_id = project_info.session_id(); match existing_session_id { Some(sid) if !sid.is_empty() => { info!( "🔄 [COMPUTER_CHAT] No session_id provided, auto using latest session: project_id={}, session_id={}", project_id, sid ); sid.to_string() } _ => { debug!( "[COMPUTER_CHAT] Project exists, creating new session" ); String::new() } } } None => { debug!("[COMPUTER_CHAT] No project, creating new session"); String::new() } } } }; // 克隆 request 并修改 session_id let mut request_for_forward = request.clone(); request_for_forward.session_id = if session_id_to_use.is_empty() { None } else { Some(session_id_to_use) }; // 🆕 自动查找 session_id 逻辑结束 // 8. 转发请求到容器服务(使用 gRPC) let result = forward_computer_request_to_container( &request_for_forward, // 使用修改后的 request &project_id, &container_info, &state.grpc_pool, locale, &state.container_prefix_rcoder, &state.container_prefix_computer, ) .await; // 8. 更新会话映射(填充所有三个映射表,保持一致性) // 无论请求成功还是失败,只要响应中包含 session_id,都要更新映射 // 这样用户可以通过 SSE 接口获取错误通知,而不会收到 SESSION_EXPIRED 错误 if let Some(chat_response) = &result.data { let session_id = chat_response.session_id.clone(); // 只有当 session_id 非空时才更新映射 if !session_id.is_empty() { info!( "🔗 [COMPUTER_CHAT] Associated session: session_id={} -> user_id={}, project_id={}, success={}", session_id, user_id, project_id, result.is_success() ); // 从 Runtime API 获取最新容器信息,避免使用过期 IP let runtime = docker_manager::runtime::RuntimeManager::get() .await .map_err(|e| { error!("[COMPUTER_CHAT] Failed to get runtime: {}", e); AppError::internal_server_error(&format!("Failed to get runtime: {}", e)) })?; let container_info = match runtime .get_container_info_by_identifier( &user_id, &shared_types::ServiceType::ComputerAgentRunner, ) .await { Ok(Some(info)) => { info!( "🔄 [COMPUTER_CHAT] Getting latest container info from Runtime API: user_id={}, container_id={}, container_ip={}", user_id, info.container_id, info.container_ip ); info } Ok(None) => { warn!( "⚠️ [COMPUTER_CHAT] Container not found in runtime: user_id={}, using cached container info", user_id ); // 使用之前获取的容器信息 container_info.clone() } Err(e) => { warn!( "⚠️ [COMPUTER_CHAT] Failed to get container info from runtime: user_id={}, error={}, using cached container info", user_id, e ); // 使用之前获取的容器信息 container_info.clone() } }; // ComputerAgentRunner 模式:每个 project 独立记录 // 使用真正的 project_id 作为 map_key,user_id 存储在数据字段中 let map_key = project_id.clone(); // 检查是否已存在该 project_id 的记录 if let Some(existing_info) = state.get_project(&map_key) { // 已存在:更新信息 let mut updated_info = (*existing_info).clone(); // 更新活动时间 updated_info.update_activity(); updated_info.update_session(session_id.clone()); // 更新扩展信息 updated_info.update_extended_from_request( Some(container_info.clone()), request.model_provider.clone(), request.request_id.clone(), Some(shared_types::ServiceType::ComputerAgentRunner), ); state.insert_project(map_key.clone(), Arc::new(updated_info)); // 更新会话映射 state.update_session(&map_key, &session_id); info!( "🔄 [COMPUTER_CHAT] Updated existing container mapping: user_id={}, project_id={}, session_id={} (last_activity refreshed)", user_id, project_id, session_id ); } else { // 不存在:创建新的 ProjectAndContainerInfo let mut project_info = shared_types::ProjectAndContainerInfo::new(map_key.clone()); // 设置 user_id(ComputerAgentRunner 模式) project_info.set_user_id(Some(user_id.clone())); // 设置 pod_id(共享容器模式) project_info.set_pod_id(request.pod_id.clone()); // 更新会话ID project_info.update_session(session_id.clone()); // 更新扩展信息(容器、模型配置等) project_info.update_extended_from_request( Some(container_info.clone()), request.model_provider.clone(), request.request_id.clone(), Some(shared_types::ServiceType::ComputerAgentRunner), ); state.insert_project(map_key.clone(), Arc::new(project_info)); // 更新会话映射 state.update_session(&map_key, &session_id); info!( "🆕 [COMPUTER_CHAT] Created new container mapping: user_id={}, project_id={}, session_id={}", user_id, project_id, session_id ); } if result.is_success() { info!( "✅ [COMPUTER_CHAT] Request processed: user_id={}, project_id={}, session_id={} (all mappings updated)", user_id, project_id, session_id ); } else { warn!( "⚠️ [COMPUTER_CHAT] Request failed but session mapping saved: user_id={}, project_id={}, session_id={}, code={}, message={}", user_id, project_id, session_id, result.code, result.message ); } } } if !result.is_success() && result .data .as_ref() .map_or(true, |d| d.session_id.is_empty()) { error!( "❌ [COMPUTER_CHAT] Container service returned error (no session_id): user_id={}, project_id={}, code={}, message={}", user_id, project_id, result.code, result.message ); } Ok(result) } /// 转发请求到容器内的 agent_runner 服务(仅使用 gRPC) /// /// 与 RCoder 的 forward_request_to_container_service 类似, /// 但专门用于 ComputerAgentRunner 模式。 async fn forward_computer_request_to_container( request: &ComputerChatRequest, project_id: &str, container_info: &ContainerBasicInfo, grpc_pool: &Arc, locale: &'static str, rcoder_prefix: &str, computer_prefix: &str, ) -> HttpResult { info!( "📤 [COMPUTER_FORWARD] Forwarding request to container (gRPC): user_id={}, project_id={}, session_id={:?}, container_id={}", request.user_id, project_id, request.session_id, container_info.container_id ); // 直接使用 gRPC 的健康检查机制,不额外检查容器状态 // gRPC 连接失败会自动返回错误,由上层处理 // 从 service_url 提取 gRPC 地址 // 🆕 使用实时 IP 获取,避免 restart 后 IP 过期的问题 let mut grpc_addr = match get_realtime_container_ip( &container_info.container_name, &container_info.container_ip, rcoder_prefix, computer_prefix, ) .await { Ok(ip) => format!("{}:{}", ip, shared_types::GRPC_DEFAULT_PORT), Err(e) => { warn!( "⚠️ [COMPUTER_FORWARD] Real-time IP resolution failed: {}, trying to extract from service_url", e ); match extract_grpc_addr_with_port( &container_info.service_url, shared_types::GRPC_DEFAULT_PORT, ) { Ok(addr) => addr, Err(e) => { error!("[COMPUTER_FORWARD] Failed to extract gRPC address: {}", e); return HttpResult::error_with_locale( shared_types::error_codes::ERR_GRPC_ADDR_ERROR, locale, ); } } } }; debug!( "📡 [COMPUTER_FORWARD] gRPC address: {}, prompt_len={}, attachments={}", grpc_addr, request.prompt.len(), request.attachments.len() ); // Computer Agent Runner 的工作目录路径 // 在容器内:/app/computer-project-workspace/{user_id}/{project_id} let project_workspace = format!("{}/", project_dir(&request.user_id, &project_id)); debug!( "[COMPUTER_FORWARD] projectworkdirectory: {}", project_workspace ); // gRPC 调用(带重试机制) let max_retries = 2; let mut last_error = None; for attempt in 1..=max_retries { match crate::grpc::grpc_chat_with_pool( grpc_pool, &grpc_addr, project_id.to_string(), request.session_id.clone(), request.prompt.clone(), request.attachments.clone(), request.data_source_attachments.clone(), request.model_provider.clone(), request.request_id.clone(), Some(std::time::Duration::from_secs(300)), // 5 分钟超时,避免永久阻塞 request.system_prompt.clone(), request.user_prompt.clone(), request.agent_config.clone(), Some(shared_types::ServiceType::ComputerAgentRunner), // ✅ 传递正确的 ServiceType Some(request.user_id.clone()), // ✅ 传递 user_id(ComputerAgentRunner 必需) ) .await { Ok(grpc_response) => { if grpc_response.success { let chat_response = crate::grpc::grpc_response_to_chat_response(grpc_response); info!( "✅ [COMPUTER_FORWARD] gRPC response success: project_id={}, session_id={}", chat_response.project_id, chat_response.session_id ); return HttpResult::success(chat_response); } else { let error_msg = grpc_response .error .unwrap_or_else(|| "Unknown error".to_string()); // 🎯 从 gRPC 响应中提取错误码(完整透传) let error_code = grpc_response .error_code .unwrap_or_else(|| shared_types::error_codes::ERR_AGENT_ERROR.to_string()); error!( "❌ [COMPUTER_FORWARD] gRPC response error: code={}, message={}", error_code, error_msg ); return HttpResult::error(&error_code, &error_msg); } } Err(e) => { warn!( "⚠️ [COMPUTER_FORWARD] gRPC call failed (attempt {}/{}): {}", attempt, max_retries, e ); let should_retry = crate::grpc::should_retry_error(&e); if should_retry && attempt < max_retries { info!( "🔄 [COMPUTER_FORWARD] Detected retryable error, re-resolving container IP and retrying..." ); grpc_pool.remove(&grpc_addr); // 重新获取最新容器 IP(容器可能已重建,IP 可能变化) match get_realtime_container_ip( &container_info.container_name, &container_info.container_ip, rcoder_prefix, computer_prefix, ) .await { Ok(ip) => { let new_addr = format!("{}:{}", ip, shared_types::GRPC_DEFAULT_PORT); info!( "🔄 [COMPUTER_FORWARD] Container IP re-resolved: {} -> {}", grpc_addr, new_addr ); grpc_addr = new_addr; } Err(e) => { warn!( "⚠️ [COMPUTER_FORWARD] Failed to re-resolve container IP, keeping old address: {}", e ); } } last_error = Some(e); continue; } else if !should_retry { error!( "[COMPUTER_FORWARD] Retry error, stopped retry: {}", e ); last_error = Some(e); break; } last_error = Some(e); } } } // 所有重试都失败 if let Some(e) = last_error { error!( "❌ [COMPUTER_FORWARD] gRPC final call failed: {}, user_id={}, project_id={}", e, request.user_id, project_id ); // gRPC 通信失败,直接返回错误 // 注:业务错误码(如 Agent busy)现在由 agent_runner 通过 grpc_response.error_code 返回 HttpResult::error_with_locale(shared_types::error_codes::ERR_GRPC_ERROR, locale) } else { HttpResult::error_with_locale(shared_types::error_codes::ERR_UNKNOWN, locale) } } /// 确保 project_id 对应的工作目录存在 /// /// Computer Agent Runner 的目录结构: /// /app/computer-project-workspace/{user_id}/{project_id}/ /// /// 注意:这个目录已经在 docker-compose.yml 中挂载,可以直接在 rcoder 容器内创建 /// /// # 参数 /// - `isolation_type`: 隔离类型(可选) /// - `tenant_id`: 租户 ID(可选) /// - `space_id`: 空间 ID(可选) /// - `user_id`: 用户 ID(当 isolation_type 为 project 时使用) /// - `project_id`: 项目 ID async fn ensure_project_workspace_exists( isolation_type: Option<&str>, tenant_id: Option<&str>, space_id: Option<&str>, user_id: &str, project_id: &str, ) -> Result<(), AppError> { // 根据隔离类型构建工作空间路径 let project_workspace_path = std::path::PathBuf::from(build_computer_workspace_path( isolation_type, tenant_id, space_id, user_id, project_id, )); debug!( "📁 [COMPUTER_CHAT] Ensuring project workspace directory exists: {:?}", project_workspace_path ); // 直接在 rcoder 容器内创建目录 tokio::fs::create_dir_all(&project_workspace_path) .await .map_err(|e| { error!( "❌ [COMPUTER_CHAT] Failed to create project workspace: path={:?}, error={}", project_workspace_path, e ); AppError::internal_server_error(&format!("Failed to create project workspace: {}", e)) })?; info!( "✅ [COMPUTER_CHAT] Project workspace directory created: user_id={}, project_id={}, isolation_type={:?}, path={:?}", user_id, project_id, isolation_type, project_workspace_path ); Ok(()) } /// 确保 DuckDB 中存在 project_id 到容器的映射 /// /// 🛡️ 关键修复:容器创建成功后立即插入 DuckDB 记录 /// /// 这样可以防止孤立容器清理器误判并清理刚创建的容器,因为: /// 1. 孤立容器清理器会检查 DuckDB 中是否存在该 user_id 关联的记录 /// 2. 如果记录不存在,容器会被判定为孤立并清理 /// 3. gRPC 请求是异步的,可能需要较长时间才能返回 /// /// # Arguments /// * `state` - 应用状态 /// * `user_id` - 用户 ID /// * `project_id` - 项目 ID /// * `container_info` - 容器信息 /// * `request` - 聊天请求 fn ensure_project_mapping_in_state( state: &Arc, user_id: &str, project_id: &str, container_info: &ContainerBasicInfo, request: &ComputerChatRequest, ) -> Result<(), AppError> { // 检查是否已存在该 project_id 的记录 if let Some(existing_project) = state.get_project(project_id) { // 如果记录存在,检查容器ID是否变更 if let Some(existing_container) = existing_project.container() { if existing_container.container_id != container_info.container_id { info!( "🔄 [COMPUTER_CHAT] Detected container change: project_id={}, old_cid={}, new_cid={}", project_id, existing_container.container_id, container_info.container_id ); // 容器变更,继续执行后续的插入/更新逻辑(insert_project 会执行 upsert) } else { debug!( "🔄 [COMPUTER_CHAT] DuckDB record already exists and container unchanged: project_id={}", project_id ); return Ok(()); } } else { // 现有记录没有容器信息,继续更新 } } // 创建新的 ProjectAndContainerInfo let mut project_info = shared_types::ProjectAndContainerInfo::new(project_id.to_string()); // 设置 user_id(ComputerAgentRunner 模式) project_info.set_user_id(Some(user_id.to_string())); // 设置 pod_id(共享容器模式) project_info.set_pod_id(request.pod_id.clone()); // 更新容器信息 project_info.update_extended_from_request( Some(container_info.clone()), request.model_provider.clone(), request.request_id.clone(), Some(shared_types::ServiceType::ComputerAgentRunner), ); // 立即插入到 DuckDB state.insert_project(project_id.to_string(), Arc::new(project_info)); info!( "🆕 [COMPUTER_CHAT] Inserted DuckDB record (immediately after container creation): user_id={}, project_id={}, container_id={}", user_id, project_id, container_info.container_id ); Ok(()) } // ============================================================================ // SSE 进度流处理器(复用现有的 agent_session_notification) // ============================================================================