添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,992 @@
//! 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
// ============================================================================