Files
qiming/qiming-rcoder/crates/rcoder/src/handler/computer_chat_handler.rs
2026-06-01 13:54:52 +08:00

993 lines
39 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<ChatResponse>,
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<String>
),
(
status = 401,
description = "API Key 鉴权失败",
body = HttpResult<String>
),
(
status = 500,
description = "服务器内部错误",
body = HttpResult<String>
)
),
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<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(mut request): I18nJsonOrQuery<ComputerChatRequest>,
) -> Result<HttpResult<ChatResponse>, 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<ContainerBasicInfo> = 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_keyuser_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_idComputerAgentRunner 模式)
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<crate::grpc::GrpcChannelPool>,
locale: &'static str,
rcoder_prefix: &str,
computer_prefix: &str,
) -> HttpResult<ChatResponse> {
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_idComputerAgentRunner 必需)
)
.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<crate::router::AppState>,
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_idComputerAgentRunner 模式)
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
// ============================================================================