添加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,146 @@
//! Computer Agent Cancel Handler
//!
//! 处理 POST /computer/agent/session/cancel 请求
use axum::{
Json,
extract::State,
http::HeaderMap,
};
use agent_client_protocol::schema::{CancelNotification, SessionId};
use std::sync::Arc;
use tokio::sync::oneshot;
use tracing::{info, warn};
use crate::CancelNotificationRequestWrapper;
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use shared_types::{
ComputerAgentCancelRequest, ComputerAgentCancelResponse, HttpResult, I18nJsonOrQuery,
error_codes::ERR_VALIDATION, get_i18n_message, AppError,
};
use super::locale_from_headers;
/// 取消 Computer Agent 会话任务
///
/// 直接操作 AGENT_REGISTRY 发送取消信号
#[utoipa::path(
post,
path = "/computer/agent/session/cancel",
params(
ComputerAgentCancelRequest
),
responses(
(status = 200, description = "Cancel request successful", body = HttpResult<ComputerAgentCancelResponse>),
(status = 400, description = "Bad request - missing fields"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_cancel(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentCancelRequest>,
) -> Result<Json<HttpResult<ComputerAgentCancelResponse>>, AppError> {
let locale = locale_from_headers(&headers);
info!(
"🚫 [HTTP] Computer Agent 取消请求: user_id={:?}, project_id={}, session_id={:?}",
request.user_id, request.project_id, request.session_id
);
// 1. 验证必填字段
// user_id 是 Option<String>,需要用 as_ref() 或直接检查
if request.user_id.as_ref().map_or(true, |s| s.is_empty()) {
return Err(AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.user_id_required", locale),
));
}
if request.project_id.is_empty() {
return Err(AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.project_id_required", locale),
));
}
// 2. 查找 session_id (如果未提供,从 AGENT_REGISTRY 获取)
let session_id = if let Some(sid) = request.session_id {
sid
} else {
// 从 AGENT_REGISTRY 查找
match AGENT_REGISTRY.get_agent_info(&request.project_id) {
Some(info) => {
// session_id 是 SessionId 类型,直接转换为 String
info.session_id.to_string()
}
None => {
// Agent 不存在,幂等返回成功
info!(
" [HTTP] Agent 不存在,幂等返回成功: project_id={}",
request.project_id
);
let response = ComputerAgentCancelResponse {
success: true,
session_id: String::new(),
};
return Ok(Json(HttpResult::success(response)));
}
}
};
// 3. 从 AGENT_REGISTRY 获取 Agent 信息并发送取消信号
if let Some(agent_info) = AGENT_REGISTRY.get_agent_info(&request.project_id) {
// 获取 cancel_tx
let cancel_tx = agent_info.cancel_tx.clone();
// 释放读锁
drop(agent_info);
// 检查是否已经空闲或停止中(幂等性)
if cancel_tx.is_closed() {
info!(
" [HTTP] Agent stopped, cancel channel is closed: session_id={}",
session_id
);
} else {
// 创建取消通知
let session_id_obj = SessionId::new(Arc::from(session_id.as_str()));
let cancel_notification = CancelNotification::new(session_id_obj);
// 创建 oneshot channel 接收取消结果 (HTTP 不等待结果,直接丢弃)
let (result_tx, _result_rx) = oneshot::channel();
let cancel_request = CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
// 发送取消信号 (异步)
match cancel_tx.send(cancel_request).await {
Ok(_) => {
info!("[HTTP] Cancel signal sent: session_id={}", session_id);
}
Err(e) => {
warn!(
"⚠️ [HTTP] Failed to send cancel signal: session_id={}, error={}",
session_id, e
);
}
}
}
} else {
// Session 不存在,幂等返回成功
info!(
" [HTTP] Agent not found, returning success idempotently: session_id={}",
session_id
);
}
let response = ComputerAgentCancelResponse {
success: true,
session_id: session_id.clone(),
};
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,211 @@
//! Computer Chat Handler
//!
//! 处理 POST /computer/chat 请求
use axum::{extract::State, http::HeaderMap, Json};
use std::sync::Arc;
use tracing::{error, info};
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use crate::service::chat_handler::{ChatHandlerContext, ChatHandlerInput, handle_chat_core};
use crate::service::{SESSION_CACHE, SessionData};
use dashmap::mapref::entry::Entry;
use shared_types::{
ChatResponse, ComputerChatRequest, HttpResult, I18nJsonOrQuery, ServiceType,
error_codes::ERR_INTERNAL_SERVER_ERROR, error_codes::ERR_VALIDATION, get_i18n_message,
};
use super::locale_from_headers;
/// 处理 Computer Agent Chat 请求
///
/// 直接调用 agent_runner 本地的 handle_chat_core(),无需 gRPC 转发
#[utoipa::path(
post,
path = "/computer/chat",
request_body = ComputerChatRequest,
responses(
(status = 200, description = "Chat request successful", body = HttpResult<ChatResponse>),
(status = 400, description = "Bad request - missing user_id"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_chat(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerChatRequest>,
) -> Result<Json<HttpResult<ChatResponse>>, shared_types::AppError> {
let locale = locale_from_headers(&headers);
info!(
"📨 [HTTP] Received Computer Chat request:\n\
├─ user_id: {:?}\n\
├─ project_id: {:?}\n\
├─ session_id: {:?}\n\
├─ request_id: {:?}\n\
├─ prompt ({}chars): {:?}\n\
├─ pod_id: {:?}\n\
├─ tenant_id: {:?}\n\
├─ space_id: {:?}\n\
├─ isolation_type: {:?}\n\
├─ attachments: {:?}\n\
├─ data_source_attachments: {:?}\n\
├─ model_provider: {:#?}\n\
├─ agent_config: {:#?}\n\
├─ system_prompt: {:?}\n\
└─ user_prompt: {:?}",
request.user_id,
request.project_id,
request.session_id,
request.request_id,
request.prompt.len(),
request.prompt,
request.pod_id,
request.tenant_id,
request.space_id,
request.isolation_type,
request.attachments,
request.data_source_attachments,
request.model_provider,
request.agent_config,
request.system_prompt,
request.user_prompt
);
// 1. 验证必填字段
if request.user_id.is_empty() {
let error_msg = get_i18n_message("error.user_id_required", locale);
error!("[HTTP] {}", error_msg);
return Err(shared_types::AppError::with_i18n_key(
ERR_VALIDATION,
&error_msg,
));
}
let user_id = request.user_id.clone();
// 2. 生成或使用提供的 project_id (直接用 UUID去掉连字符与 rcoder 保持一致)
let project_id = request
.project_id
.clone()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string().replace('-', ""));
// 3. 自动查找现有 session_id (如果未提供)
let session_id = request.session_id.or_else(|| {
AGENT_REGISTRY
.get_agent_info(&project_id)
.map(|info| info.session_id.to_string())
});
// 4. 创建项目工作目录(使用配置中的 projects_dir支持外部配置
// Docker 挂载:宿主机 /computer-project-workspace/{user_id} → 容器 /home/user
// Agent 工作目录:/home/user/{project_id}
let project_dir = state.config.projects_dir.join(&project_id);
if let Err(e) = tokio::fs::create_dir_all(&project_dir).await {
let error_msg = format!(
"{}: {}",
get_i18n_message("error.project_dir_create_failed", locale),
e
);
error!("[HTTP] {}", error_msg);
return Err(shared_types::AppError::with_message(
ERR_INTERNAL_SERVER_ERROR,
&error_msg,
));
}
// 5. 生成或使用提供的 request_id
let request_id = request
.request_id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// 6. 构建 ChatHandlerInput
let input = ChatHandlerInput {
project_id: project_id.clone(),
project_dir,
session_id,
prompt: request.prompt,
request_id: request_id.clone(),
attachments: request.attachments,
data_source_attachments: request.data_source_attachments,
model_config: request.model_provider,
service_type: ServiceType::ComputerAgentRunner,
agent_config_override: request.agent_config,
system_prompt_override: request.system_prompt,
user_prompt_template_override: request.user_prompt,
skip_slot_limit: true, // HTTP Server 部署,跳过槽位限制
};
// 7. 构建 ChatHandlerContext
let context = ChatHandlerContext {
agent_runtime: state.agent_runtime.clone(),
shared_api_key_manager: state.shared_api_key_manager.clone(),
project_uuid_map: state.project_uuid_map.clone(),
};
// 8. 调用核心 Chat 处理逻辑
let output = handle_chat_core(input, &context).await;
// 🔧 关键修复:将 session 写入 SESSION_CACHESSE 进度流需要从这里读取)
let session_id_str = output.session_id.clone();
match SESSION_CACHE.entry(session_id_str.clone()) {
Entry::Occupied(entry) => {
info!(
"[HTTP] SESSION_CACHE already exists, reusing: session_id={}",
session_id_str
);
entry.get().clone()
}
Entry::Vacant(entry) => {
let data = SessionData::new(1000);
info!(
"[HTTP] SESSION_CACHE created: session_id={}",
session_id_str
);
entry.insert(data.clone());
data
}
};
// 9. 构建响应
let response = ChatResponse {
project_id: output.project_id.clone(),
session_id: output.session_id.clone(),
error: output.error.clone(),
request_id: Some(request_id),
need_fallback: None,
fallback_reason: None,
};
// 10. 根据执行结果返回成功或错误
if output.error.is_some() || !output.success {
error!(
"❌ [HTTP] Computer Chat failed: session_id={}, error={:?}",
response.session_id, response.error
);
// 返回成功的 HTTP 状态码,但 HttpResult 包含错误信息
// 这与 rcoder 的行为一致HTTP 200 + HttpResult.error
return Ok(Json(HttpResult::error(
output
.error_code
.as_deref()
.unwrap_or(ERR_INTERNAL_SERVER_ERROR),
&shared_types::get_error_message(
output
.error_code
.as_deref()
.unwrap_or(ERR_INTERNAL_SERVER_ERROR),
locale,
),
)));
}
info!(
"✅ [HTTP] Computer Chat response: session_id={}, error={:?}",
response.session_id, response.error
);
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,247 @@
//! Computer Agent Progress Handler
//!
//! 处理 GET /computer/progress/{session_id} 请求 (SSE 流)
use axum::{
Json,
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::sse::{Event, KeepAlive, Sse},
};
use chrono::Utc;
use futures_util::stream::{Stream, StreamExt};
use std::convert::Infallible;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info, warn};
use crate::http_server::router::AppState;
use crate::service::{AGENT_REGISTRY, SESSION_CACHE};
use shared_types::{
AgentStatus, HttpResult, SessionMessageType, UnifiedSessionMessage,
error_codes::{ERR_INTERNAL_SERVER_ERROR, ERR_SESSION_NOT_FOUND},
get_i18n_message,
};
use super::locale_from_headers;
/// 统一的 SSE 流类型
type SseStream = Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>>;
/// 检查 Agent 是否处于 idle 状态
///
/// 返回 true 如果:
/// - Agent 不存在,或
/// - Agent 状态为 Idle
/// - Agent 的 session_id 与当前请求的 session_id 不匹配
async fn is_agent_idle(session_id: &str) -> bool {
// 通过 session_id 获取 agent_info
if let Some(info) = AGENT_REGISTRY.get_agent_info_by_session(session_id) {
// 检查状态是否为 Idle或者 session_id 是否匹配
info.status == AgentStatus::Idle || info.session_id.to_string() != session_id
} else {
// Agent 不存在,视为 idle
true
}
}
/// 创建 Agent idle 时的结束事件
///
/// 当 Agent 处于闲置状态时,发送此事件通知前端没有正在执行的任务
fn create_idle_end_event(session_id: &str) -> Event {
let unified_message = UnifiedSessionMessage {
session_id: session_id.to_string(),
message_type: SessionMessageType::SessionPromptEnd,
sub_type: "end_turn".to_string(),
data: serde_json::json!({
"reason": "EndTurn",
"description": "Agent 当前无在执行任务"
}),
timestamp: Utc::now(),
};
let json_data = match serde_json::to_string(&unified_message) {
Ok(json) => json,
Err(e) => {
warn!(
"⚠️ [HTTP] 序列化 SessionPromptEnd 消息失败: session_id={}, error={}",
session_id, e
);
// 返回包含 session_id 的最小可用结构
format!(
r#"{{"sessionId":"{}","messageType":"sessionPromptEnd","subType":"end_turn","data":{{"reason":"EndTurn","description":"Agent 当前无在执行任务"}}}}"#,
session_id
)
}
};
Event::default().event("end_turn").data(json_data)
}
/// 创建心跳消息流
///
/// 定期发送符合 UnifiedSessionMessage 格式的心跳消息
fn create_heartbeat_stream(
session_id: String,
) -> impl Stream<Item = Result<Event, Infallible>> + Send {
let heartbeat_interval = Duration::from_secs(15);
let (tx, rx) = tokio::sync::mpsc::channel(10);
tokio::spawn(async move {
let mut interval = tokio::time::interval(heartbeat_interval);
// 立即发送第一个心跳,然后按间隔发送
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
// 创建心跳消息
let heartbeat_msg = UnifiedSessionMessage::heartbeat(session_id.clone());
let json_str = match serde_json::to_string(&heartbeat_msg) {
Ok(s) => s,
Err(e) => {
error!("[HTTP] Failed to serialize heartbeat message: {}", e);
continue;
}
};
// 使用 sub_type ("ping") 作为事件名
if tx
.send(Ok(Event::default().event("ping").data(json_str)))
.await
.is_err()
{
// 接收端已关闭,停止发送心跳
break;
}
}
});
ReceiverStream::new(rx)
}
/// Computer Agent 进度流 (SSE)
///
/// 直接从 SESSION_CACHE 订阅消息流,无需 gRPC
#[utoipa::path(
get,
path = "/computer/progress/{session_id}",
responses(
(status = 200, description = "SSE progress stream", content_type = "text/event-stream"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_progress(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<Sse<SseStream>, (StatusCode, Json<HttpResult<String>>)> {
let locale = locale_from_headers(&headers);
info!(
"📡 [HTTP] Computer Agent progress stream subscribed: session_id={}",
session_id
);
// 0. 检查 Agent 状态(必须在 SESSION_CACHE 查找之前检查)
if is_agent_idle(&session_id).await {
info!(
"💤 [HTTP] Agent is idle, sending SessionPromptEnd and closing: session_id={}",
session_id
);
let end_event = create_idle_end_event(&session_id);
let stream: SseStream = Box::pin(futures_util::stream::iter([Ok(end_event)]));
return Ok(Sse::new(stream));
}
// 1. 从 SESSION_CACHE 获取 session_data
// 🛡️ 关键修复:先 clone Arc<SessionData>,立即释放 DashMap shard 读锁
// 之前直接在 Ref 上调用 create_new_connection().await导致 DashMap 读锁跨 await 持有
// 可能造成与 SESSION_CACHE.entry()/remove() 等写操作的死锁
let session_data = match SESSION_CACHE.get(&session_id) {
Some(data) => data.value().clone(),
None => {
warn!(" [HTTP] Session not found: session_id={}", session_id);
return Err((
StatusCode::NOT_FOUND,
Json(HttpResult::error_with_message(
ERR_SESSION_NOT_FOUND,
locale,
&format!(
"{}: {}",
get_i18n_message("error.session_not_found", locale),
session_id
),
)),
));
}
};
// 2. 创建新的消息订阅DashMap 锁已释放,此处 await 安全)
let (message_rx, _cancel_token) = match session_data.create_new_connection(1000).await {
Ok(conn) => conn,
Err(e) => {
error!(
"❌ [HTTP] Failed to create session connection: session_id={}, error={}",
session_id, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(HttpResult::error_with_message(
ERR_INTERNAL_SERVER_ERROR,
locale,
&format!(
"{}: {}",
get_i18n_message("error.internal_server_error", locale),
e
),
)),
));
}
};
// 3. 创建消息流和心跳流
// UnifiedSessionMessage 已使用 #[serde(rename_all = "camelCase")], 序列化后符合 RCoder 约定
let message_stream = ReceiverStream::new(message_rx).map(|msg| {
let is_terminal = matches!(msg.message_type, SessionMessageType::SessionPromptEnd);
let json_str = match serde_json::to_string(&msg) {
Ok(s) => s,
Err(e) => {
error!("[HTTP] Failed to serialize message: {}", e);
return (Ok(Event::default().data("{}")), false);
}
};
(Ok(Event::default().event(msg.sub_type).data(json_str)), is_terminal)
});
// 4. 创建心跳流(标记为非终端)
let heartbeat_stream = create_heartbeat_stream(session_id.clone())
.map(|event| (event, false));
// 5. 合并两个流,并用 scan 监测终止条件
// select 会继续轮询心跳流(永不结束),所以必须在合并流层面检测终止
// 终止条件:
// - 收到 SessionPromptEnd终端消息→ 发送后结束流
// - channel 关闭 → message_stream 返回 Nonescan 最终也会结束
let merged_stream = futures_util::stream::select(message_stream, heartbeat_stream)
.scan(false, |seen_terminal, (event, is_terminal)| {
if *seen_terminal {
// 已发送终端消息,结束流
return std::future::ready(None);
}
if is_terminal {
*seen_terminal = true;
}
std::future::ready(Some(event))
});
info!("[HTTP] SSE stream established: session_id={}", session_id);
let stream: SseStream = Box::pin(merged_stream);
Ok(Sse::new(stream))
}

View File

@@ -0,0 +1,95 @@
//! Computer Agent Status Handler
//!
//! 处理 POST /computer/agent/status 请求
use axum::{extract::State, http::HeaderMap, Json};
use std::sync::Arc;
use tracing::{info, warn};
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use shared_types::{
ComputerAgentStatusRequest, ComputerAgentStatusResponse, HttpResult, I18nJsonOrQuery,
error_codes::ERR_VALIDATION, get_i18n_message,
};
use super::locale_from_headers;
/// 查询 Computer Agent 状态
///
/// 直接使用 AGENT_REGISTRY 查询,无需 gRPC 调用
#[utoipa::path(
post,
path = "/computer/agent/status",
request_body = ComputerAgentStatusRequest,
responses(
(status = 200, description = "Status query successful", body = HttpResult<ComputerAgentStatusResponse>),
(status = 400, description = "Bad request - missing fields"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_status(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentStatusRequest>,
) -> Result<Json<HttpResult<ComputerAgentStatusResponse>>, shared_types::AppError> {
let locale = locale_from_headers(&headers);
// 使用 garde 进行字段校验
let I18nJsonOrQuery(request) = I18nJsonOrQuery(request).validate_into_app_error()?;
let project_id = request.project_id.as_ref().expect("validated: project_id is required and non-empty");
// 验证 user_id 或 project_id 至少有一个
let user_id_empty = request.user_id.as_ref().map_or(true, |s| s.is_empty());
if user_id_empty && project_id.is_empty() {
return Err(shared_types::AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.user_id_or_project_id_required", locale),
));
}
info!(
"🔍 [HTTP] Computer Agent 状态查询: user_id={:?}, project_id={}, pod_id={:?}, tenant_id={:?}, space_id={:?}, isolation_type={:?}",
request.user_id, project_id, request.pod_id, request.tenant_id, request.space_id, request.isolation_type
);
// 从 AGENT_REGISTRY 查询 Agent 状态
let agent_info = AGENT_REGISTRY.get_agent_info(project_id);
let response = match agent_info {
Some(info) => {
// Agent 存在且活跃
info!(
"✅ [HTTP] Agent status: project_id={}, is_alive=true, session_id={:?}",
project_id, info.session_id
);
ComputerAgentStatusResponse {
user_id: request.user_id.clone(),
project_id: project_id.to_string(),
is_alive: true,
session_id: Some(info.session_id.to_string()),
status: Some(format!("{:?}", info.status)),
last_activity: Some(info.last_activity),
created_at: Some(info.created_at),
}
}
None => {
// Agent 不存在
warn!(" [HTTP] Agent not found: project_id={}", project_id);
ComputerAgentStatusResponse {
user_id: request.user_id.clone(),
project_id: project_id.to_string(),
is_alive: false,
session_id: None,
status: None,
last_activity: None,
created_at: None,
}
}
};
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,135 @@
//! Computer Agent Stop Handler
//!
//! 处理 POST /computer/agent/stop 请求
use axum::{extract::State, http::HeaderMap, Json};
use agent_client_protocol::schema::{CancelNotification, SessionId};
use std::sync::Arc;
use tokio::sync::oneshot;
use tracing::{info, warn};
use crate::CancelNotificationRequestWrapper;
use crate::http_server::router::AppState;
use crate::service::AGENT_REGISTRY;
use shared_types::{
ComputerAgentStopRequest, ComputerAgentStopResponse, HttpResult, I18nJsonOrQuery,
error_codes::{ERR_VALIDATION, SUCCESS},
get_error_message, get_i18n_message,
};
use super::locale_from_headers;
/// 停止 Computer Agent
///
/// 1. 发送取消信号停止正在运行的任务
/// 2. 从 AGENT_REGISTRY 移除 Agent 状态
#[utoipa::path(
post,
path = "/computer/agent/stop",
request_body = ComputerAgentStopRequest,
responses(
(status = 200, description = "Stop request successful", body = HttpResult<ComputerAgentStopResponse>),
(status = 400, description = "Bad request - missing fields"),
(status = 500, description = "Internal server error")
),
tag = "Computer Agent"
)]
pub async fn handle_computer_stop(
State(_state): State<Arc<AppState>>,
headers: HeaderMap,
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentStopRequest>,
) -> Result<Json<HttpResult<ComputerAgentStopResponse>>, shared_types::AppError> {
let locale = locale_from_headers(&headers);
// 使用 garde 进行字段校验
let I18nJsonOrQuery(request) = I18nJsonOrQuery(request).validate_into_app_error()?;
let project_id = request.project_id.as_ref().expect("validated: project_id is required and non-empty");
// 验证 user_id 或 project_id 至少有一个
let user_id_empty = request.user_id.as_ref().map_or(true, |s| s.is_empty());
if user_id_empty && project_id.is_empty() {
return Err(shared_types::AppError::with_i18n_key(
ERR_VALIDATION,
&get_i18n_message("error.user_id_or_project_id_required", locale),
));
}
info!(
"🛑 [HTTP] Computer Agent 停止请求: user_id={:?}, project_id={}, pod_id={:?}, tenant_id={:?}, space_id={:?}, isolation_type={:?}",
request.user_id, project_id, request.pod_id, request.tenant_id, request.space_id, request.isolation_type
);
// 获取 Agent 信息并发送取消信号
let (success, message) =
if let Some(agent_info) = AGENT_REGISTRY.get_agent_info(project_id) {
let session_id = agent_info.session_id.to_string();
let cancel_tx = agent_info.cancel_tx.clone();
// 释放读锁
drop(agent_info);
// 发送取消信号(如果 channel 仍然打开)
if !cancel_tx.is_closed() {
let session_id_obj = SessionId::new(Arc::from(session_id.as_str()));
let cancel_notification = CancelNotification::new(session_id_obj);
let (result_tx, _result_rx) = oneshot::channel();
let cancel_request = CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
};
match cancel_tx.send(cancel_request).await {
Ok(_) => {
info!("[HTTP] Cancel signal sent: session_id={}", session_id);
}
Err(e) => {
warn!(
"⚠️ [HTTP] Failed to send cancel signal: session_id={}, error={}",
session_id, e
);
}
}
}
// 从 AGENT_REGISTRY 移除 Agent
let removed = AGENT_REGISTRY
.remove_by_project(project_id)
.is_some();
if removed {
info!("[HTTP] Agent stopped: project_id={}", project_id);
(true, get_error_message(SUCCESS, locale))
} else {
// 可能在取消期间已被清理
info!(
" [HTTP] Agent already cleaned up: project_id={}",
project_id
);
(
true,
get_i18n_message("success.agent_already_stopped", locale),
)
}
} else {
// Agent 不存在,幂等返回成功
info!(
" [HTTP] Agent not found, returning success idempotently: project_id={}",
project_id
);
(
true,
get_i18n_message("success.agent_already_stopped", locale),
)
};
let response = ComputerAgentStopResponse {
success,
message,
user_id: request.user_id.clone(),
pod_id: None,
project_id: project_id.to_string(),
};
Ok(Json(HttpResult::success(response)))
}

View File

@@ -0,0 +1,17 @@
//! HTTP 请求处理器
//!
//! 仅在 `http-server` feature 启用时编译
use axum::http::HeaderMap;
pub mod computer_cancel;
pub mod computer_chat;
pub mod computer_progress;
pub mod computer_status;
pub mod computer_stop;
pub mod rcoder_progress; // RCoder 模式的 SSE 进度流
pub(super) fn locale_from_headers(headers: &HeaderMap) -> &'static str {
let accept_language = headers.get("accept-language").and_then(|v| v.to_str().ok());
shared_types::parse_accept_language(accept_language)
}

View File

@@ -0,0 +1,247 @@
//! RCoder Agent Progress Handler
//!
//! 处理 GET /agent/progress/{session_id} 请求 (SSE 流)
//!
//! 与 computer_progress.rs 使用相同的 SSE 流模式,直接从 SESSION_CACHE 订阅消息
use std::convert::Infallible;
use std::pin::Pin;
use std::time::Duration;
use axum::{
extract::Path,
http::{HeaderMap, StatusCode},
Json,
response::sse::{Event, Sse},
};
use chrono::Utc;
use futures_util::stream::{Stream, StreamExt};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info, warn};
use crate::service::{AGENT_REGISTRY, SESSION_CACHE};
use shared_types::{
AgentStatus, HttpResult, SessionMessageType, UnifiedSessionMessage,
error_codes::{ERR_INTERNAL_SERVER_ERROR, ERR_SESSION_NOT_FOUND},
get_i18n_message,
};
use super::locale_from_headers;
/// 统一的 SSE 流类型
type SseStream = Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>>;
/// 检查 Agent 是否处于 idle 状态
///
/// 返回 true 如果:
/// - Agent 不存在,或
/// - Agent 状态为 Idle
/// - Agent 的 session_id 与当前请求的 session_id 不匹配
async fn is_agent_idle(session_id: &str) -> bool {
// 通过 session_id 获取 agent_info
if let Some(info) = AGENT_REGISTRY.get_agent_info_by_session(session_id) {
// 检查状态是否为 Idle或者 session_id 是否匹配
info.status == AgentStatus::Idle || info.session_id.to_string() != session_id
} else {
// Agent 不存在,视为 idle
true
}
}
/// 创建 Agent idle 时的结束事件
///
/// 当 Agent 处于闲置状态时,发送此事件通知前端没有正在执行的任务
fn create_idle_end_event(session_id: &str) -> Event {
let unified_message = UnifiedSessionMessage {
session_id: session_id.to_string(),
message_type: SessionMessageType::SessionPromptEnd,
sub_type: "end_turn".to_string(),
data: serde_json::json!({
"reason": "EndTurn",
"description": "Agent 当前无在执行任务"
}),
timestamp: Utc::now(),
};
let json_data = match serde_json::to_string(&unified_message) {
Ok(json) => json,
Err(e) => {
warn!(
"⚠️ [RCoder] 序列化 SessionPromptEnd 消息失败: session_id={}, error={}",
session_id, e
);
// 返回包含 session_id 的最小可用结构
format!(
r#"{{"sessionId":"{}","messageType":"sessionPromptEnd","subType":"end_turn","data":{{"reason":"EndTurn","description":"Agent 当前无在执行任务"}}}}"#,
session_id
)
}
};
Event::default().event("end_turn").data(json_data)
}
/// 创建心跳消息流
///
/// 定期发送符合 UnifiedSessionMessage 格式的心跳消息
fn create_heartbeat_stream(
session_id: String,
) -> impl Stream<Item = Result<Event, Infallible>> + Send {
let heartbeat_interval = Duration::from_secs(15);
let (tx, rx) = tokio::sync::mpsc::channel(10);
tokio::spawn(async move {
let mut interval = tokio::time::interval(heartbeat_interval);
// 立即发送第一个心跳,然后按间隔发送
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
// 创建心跳消息
let heartbeat_msg = UnifiedSessionMessage::heartbeat(session_id.clone());
let json_str = match serde_json::to_string(&heartbeat_msg) {
Ok(s) => s,
Err(e) => {
error!("[RCoder] Failed to serialize heartbeat message: {}", e);
continue;
}
};
// 使用 sub_type ("ping") 作为事件名
if tx
.send(Ok(Event::default().event("ping").data(json_str)))
.await
.is_err()
{
// 接收端已关闭,停止发送心跳
break;
}
}
});
ReceiverStream::new(rx)
}
/// RCoder Agent 进度流 (SSE)
///
/// 直接从 SESSION_CACHE 订阅消息流,无需 gRPC
#[utoipa::path(
get,
path = "/agent/progress/{session_id}",
params(
("session_id" = String, Path, description = "会话ID")
),
responses(
(status = 200, description = "SSE progress stream", content_type = "text/event-stream"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
tag = "RCoder Agent"
)]
pub async fn handle_rcoder_progress(
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<Sse<SseStream>, (StatusCode, Json<HttpResult<String>>)> {
let locale = locale_from_headers(&headers);
info!(
"📡 [RCoder] Progress stream subscribed: session_id={}",
session_id
);
// 0. 检查 Agent 状态(必须在 SESSION_CACHE 查找之前检查)
if is_agent_idle(&session_id).await {
info!(
"💤 [RCoder] Agent is idle, sending SessionPromptEnd and closing: session_id={}",
session_id
);
let end_event = create_idle_end_event(&session_id);
let stream: SseStream = Box::pin(futures_util::stream::iter([Ok(end_event)]));
return Ok(Sse::new(stream));
}
// 1. 从 SESSION_CACHE 获取 session_data
// 🛡️ 关键:先 clone Arc<SessionData>,立即释放 DashMap shard 读锁
let session_data = match SESSION_CACHE.get(&session_id) {
Some(data) => data.value().clone(),
None => {
warn!(" [RCoder] Session not found: session_id={}", session_id);
return Err((
StatusCode::NOT_FOUND,
Json(HttpResult::error_with_message(
ERR_SESSION_NOT_FOUND,
locale,
&format!(
"{}: {}",
get_i18n_message("error.session_not_found", locale),
session_id
),
)),
));
}
};
// 2. 创建新的消息订阅DashMap 锁已释放,此处 await 安全)
let (message_rx, _cancel_token) = match session_data.create_new_connection(1000).await {
Ok(conn) => conn,
Err(e) => {
error!(
"❌ [RCoder] Failed to create session connection: session_id={}, error={}",
session_id, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(HttpResult::error_with_message(
ERR_INTERNAL_SERVER_ERROR,
locale,
&format!(
"{}: {}",
get_i18n_message("error.internal_server_error", locale),
e
),
)),
));
}
};
// 3. 创建消息流和心跳流
let message_stream = ReceiverStream::new(message_rx).map(|msg| {
let is_terminal = matches!(msg.message_type, SessionMessageType::SessionPromptEnd);
let json_str = match serde_json::to_string(&msg) {
Ok(s) => s,
Err(e) => {
error!("[RCoder] Failed to serialize message: {}", e);
return (Ok(Event::default().data("{}")), false);
}
};
(Ok(Event::default().event(msg.sub_type).data(json_str)), is_terminal)
});
// 4. 创建心跳流(标记为非终端)
let heartbeat_stream = create_heartbeat_stream(session_id.clone())
.map(|event| (event, false));
// 5. 合并两个流,并用 scan 监测终止条件
// select 会继续轮询心跳流(永不结束),所以必须在合并流层面检测终止
// 终止条件:
// - 收到 SessionPromptEnd终端消息→ 发送后结束流
// - channel 关闭 → message_stream 返回 Nonescan 最终也会结束
let merged_stream = futures_util::stream::select(message_stream, heartbeat_stream)
.scan(false, |seen_terminal, (event, is_terminal)| {
if *seen_terminal {
// 已发送终端消息,结束流
return std::future::ready(None);
}
if is_terminal {
*seen_terminal = true;
}
std::future::ready(Some(event))
});
info!("[RCoder] SSE stream established: session_id={}", session_id);
let stream: SseStream = Box::pin(merged_stream);
Ok(Sse::new(stream))
}