添加qiming-rcoder模块
This commit is contained in:
@@ -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)))
|
||||
}
|
||||
@@ -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_CACHE(SSE 进度流需要从这里读取)
|
||||
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)))
|
||||
}
|
||||
@@ -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 返回 None,scan 最终也会结束
|
||||
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))
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 返回 None,scan 最终也会结束
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user