添加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))
|
||||
}
|
||||
11
qiming-rcoder/crates/agent_runner/src/http_server/mod.rs
Normal file
11
qiming-rcoder/crates/agent_runner/src/http_server/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! HTTP 服务器模块
|
||||
//!
|
||||
//! 仅在 `http-server` feature 启用时编译
|
||||
//! 提供 Computer Agent HTTP REST API
|
||||
|
||||
pub mod handlers;
|
||||
pub mod router;
|
||||
pub mod start;
|
||||
|
||||
pub use router::{AppState, create_router};
|
||||
pub use start::{HttpServerConfig, start_http_server};
|
||||
186
qiming-rcoder/crates/agent_runner/src/http_server/router.rs
Normal file
186
qiming-rcoder/crates/agent_runner/src/http_server/router.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
//! HTTP 路由定义
|
||||
//!
|
||||
//! 定义所有 HTTP 端点和路由
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use tower_http::limit::RequestBodyLimitLayer;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::agent_runtime::AgentRuntime;
|
||||
use crate::api_key_manager::ApiKeyManager;
|
||||
use crate::config::AppConfig;
|
||||
use crate::service::local_agent_service::LocalAgentHttpService;
|
||||
|
||||
/// HTTP 应用状态
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: AppConfig,
|
||||
pub agent_runtime: Arc<AgentRuntime>,
|
||||
pub api_key_manager: Arc<ApiKeyManager>,
|
||||
pub shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
|
||||
pub project_uuid_map: Arc<DashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(
|
||||
config: AppConfig,
|
||||
agent_runtime: Arc<AgentRuntime>,
|
||||
shared_api_key_manager: Arc<DashMap<String, shared_types::ModelProviderConfig>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
agent_runtime: agent_runtime.clone(),
|
||||
api_key_manager: Arc::new(ApiKeyManager::from_shared(shared_api_key_manager.clone())),
|
||||
shared_api_key_manager,
|
||||
project_uuid_map: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建 LocalAgentHttpService 实例用于 RCoder 模式
|
||||
pub fn create_local_agent_service(&self) -> Arc<LocalAgentHttpService> {
|
||||
Arc::new(LocalAgentHttpService::new(
|
||||
self.agent_runtime.clone(),
|
||||
self.shared_api_key_manager.clone(),
|
||||
self.project_uuid_map.clone(),
|
||||
self.config.projects_dir.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建 HTTP 路由
|
||||
///
|
||||
/// 组合 Computer Agent 路由和 RCoder Agent 路由
|
||||
pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
use super::handlers::{
|
||||
computer_cancel, computer_chat, computer_progress, computer_status, computer_stop,
|
||||
rcoder_progress,
|
||||
};
|
||||
use shared_types::http_handlers;
|
||||
|
||||
// 创建 LocalAgentHttpService 实例
|
||||
let local_agent_service = state.create_local_agent_service();
|
||||
|
||||
// Computer Agent 路由
|
||||
let computer_routes = Router::new()
|
||||
.route("/computer/chat", post(computer_chat::handle_computer_chat))
|
||||
.route(
|
||||
"/computer/agent/stop",
|
||||
post(computer_stop::handle_computer_stop),
|
||||
)
|
||||
.route(
|
||||
"/computer/agent/status",
|
||||
post(computer_status::handle_computer_status),
|
||||
)
|
||||
.route(
|
||||
"/computer/agent/session/cancel",
|
||||
post(computer_cancel::handle_computer_cancel),
|
||||
)
|
||||
.route(
|
||||
"/computer/progress/{session_id}",
|
||||
get(computer_progress::handle_computer_progress),
|
||||
)
|
||||
.with_state(state.clone());
|
||||
|
||||
// RCoder Agent 路由(使用 LocalAgentHttpService)
|
||||
let rcoder_routes = Router::new()
|
||||
.route("/chat", post(http_handlers::handle_chat::<LocalAgentHttpService>))
|
||||
.route(
|
||||
"/agent/session/cancel",
|
||||
post(http_handlers::handle_cancel::<LocalAgentHttpService>),
|
||||
)
|
||||
.route("/agent/stop", post(http_handlers::handle_stop::<LocalAgentHttpService>))
|
||||
.route(
|
||||
"/agent/status/{project_id}",
|
||||
get(http_handlers::handle_status::<LocalAgentHttpService>),
|
||||
)
|
||||
.route(
|
||||
"/agent/progress/{session_id}",
|
||||
get(rcoder_progress::handle_rcoder_progress),
|
||||
)
|
||||
.with_state(local_agent_service);
|
||||
|
||||
// 通用路由
|
||||
let api_routes = Router::new()
|
||||
.route("/health", get(health_check))
|
||||
.with_state(state.clone());
|
||||
|
||||
// 组合路由
|
||||
Router::new()
|
||||
.merge(computer_routes)
|
||||
.merge(rcoder_routes)
|
||||
.merge(api_routes)
|
||||
.merge(create_swagger_ui())
|
||||
.layer(RequestBodyLimitLayer::new(50 * 1024 * 1024)) // 🔥 50MB body 限制
|
||||
}
|
||||
|
||||
/// 健康检查端点
|
||||
///
|
||||
/// 检查服务的健康状态
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/health",
|
||||
responses(
|
||||
(status = 200, description = "服务健康状态", body = shared_types::HealthResponse)
|
||||
),
|
||||
tag = "system"
|
||||
)]
|
||||
pub async fn health_check() -> Json<shared_types::HealthResponse> {
|
||||
Json(shared_types::HealthResponse::new("agent-runner"))
|
||||
}
|
||||
|
||||
/// 创建 Swagger UI
|
||||
fn create_swagger_ui() -> SwaggerUi {
|
||||
use super::handlers::{
|
||||
computer_cancel::__path_handle_computer_cancel, computer_chat::__path_handle_computer_chat,
|
||||
computer_progress::__path_handle_computer_progress,
|
||||
computer_status::__path_handle_computer_status, computer_stop::__path_handle_computer_stop,
|
||||
};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
// Computer Agent 端点
|
||||
handle_computer_chat,
|
||||
handle_computer_status,
|
||||
handle_computer_stop,
|
||||
handle_computer_cancel,
|
||||
handle_computer_progress,
|
||||
// 健康检查
|
||||
health_check,
|
||||
),
|
||||
components(schemas(
|
||||
// Computer Agent 类型
|
||||
shared_types::ComputerChatRequest,
|
||||
shared_types::ChatResponse,
|
||||
shared_types::ComputerAgentStatusRequest,
|
||||
shared_types::ComputerAgentStatusResponse,
|
||||
shared_types::ComputerAgentStopRequest,
|
||||
shared_types::ComputerAgentStopResponse,
|
||||
shared_types::ComputerAgentCancelRequest,
|
||||
shared_types::ComputerAgentCancelResponse,
|
||||
// RCoder Agent 类型
|
||||
shared_types::RcoderChatRequest,
|
||||
shared_types::RcoderAgentCancelRequest,
|
||||
shared_types::RcoderAgentCancelResponse,
|
||||
shared_types::RcoderAgentStopRequest,
|
||||
shared_types::RcoderAgentStopResponse,
|
||||
shared_types::AgentStatusResponse,
|
||||
// 通用类型
|
||||
shared_types::HealthResponse,
|
||||
)),
|
||||
tags(
|
||||
(name = "Computer Agent", description = "Computer Agent HTTP API"),
|
||||
(name = "RCoder Agent", description = "RCoder Agent HTTP API"),
|
||||
(name = "System", description = "系统管理接口")
|
||||
)
|
||||
)]
|
||||
struct ApiDoc;
|
||||
|
||||
SwaggerUi::new("/api/docs").url("/api-docs/openapi.json", ApiDoc::openapi())
|
||||
}
|
||||
266
qiming-rcoder/crates/agent_runner/src/http_server/start.rs
Normal file
266
qiming-rcoder/crates/agent_runner/src/http_server/start.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
//! HTTP 服务器启动模块
|
||||
//!
|
||||
//! 提供便捷的 HTTP 服务器启动 API
|
||||
//! 支持 HTTP REST API 和可选的 Pingora 代理服务
|
||||
|
||||
use anyhow::Result;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::agent_runtime::AgentRuntime;
|
||||
use crate::config::AppConfig;
|
||||
use crate::http_server::router::{AppState, create_router};
|
||||
use crate::proxy_agent::cleanup_task::{CleanupConfig, start_cleanup_task};
|
||||
use crate::proxy_agent::set_unlimited_mode;
|
||||
#[cfg(feature = "proxy")]
|
||||
use crate::proxy_agent::start_pingora;
|
||||
|
||||
/// HTTP 服务器配置
|
||||
pub struct HttpServerConfig {
|
||||
/// HTTP 监听端口
|
||||
pub port: u16,
|
||||
/// 应用配置
|
||||
pub app_config: AppConfig,
|
||||
/// Agent 运行时
|
||||
pub agent_runtime: Arc<AgentRuntime>,
|
||||
/// 共享 API Key Manager
|
||||
pub shared_api_key_manager: Arc<dashmap::DashMap<String, shared_types::ModelProviderConfig>>,
|
||||
}
|
||||
|
||||
/// HTTP 服务器控制柄
|
||||
///
|
||||
/// 用于控制 HTTP 服务器的生命周期
|
||||
#[derive(Clone)]
|
||||
pub struct HttpServerHandle {
|
||||
/// 关闭信号令牌
|
||||
shutdown_token: CancellationToken,
|
||||
/// 活跃任务集合
|
||||
join_set: Arc<tokio::sync::Mutex<JoinSet<()>>>,
|
||||
/// Pingora 结果(用于调用 stop)
|
||||
#[cfg(feature = "proxy")]
|
||||
pingora_result: Arc<tokio::sync::Mutex<Option<crate::proxy_agent::PingoraStartResult>>>,
|
||||
}
|
||||
|
||||
impl HttpServerHandle {
|
||||
/// 检查是否收到关闭信号
|
||||
pub fn is_shutdown(&self) -> bool {
|
||||
self.shutdown_token.is_cancelled()
|
||||
}
|
||||
|
||||
/// 停止 HTTP 服务器并等待所有任务完成
|
||||
pub async fn stop(&self) {
|
||||
info!("Stopping HTTP server...");
|
||||
|
||||
// 1. 发送关闭信号
|
||||
self.shutdown_token.cancel();
|
||||
|
||||
// 2. 停止 Pingora 服务
|
||||
#[cfg(feature = "proxy")]
|
||||
{
|
||||
let mut pingora_guard = self.pingora_result.lock().await;
|
||||
if let Some(mut pingora) = pingora_guard.take() {
|
||||
pingora.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 等待所有任务完成(带超时)
|
||||
// 使用 3 秒超时:清理任务会立即退出,axum 有 3 秒进行连接排空
|
||||
let timeout = Duration::from_secs(3);
|
||||
let mut join_set = self.join_set.lock().await;
|
||||
|
||||
loop {
|
||||
match tokio::time::timeout(timeout, join_set.join_next()).await {
|
||||
Ok(Some(Ok(()))) => {
|
||||
info!("Task exited normally");
|
||||
}
|
||||
Ok(Some(Err(e))) => {
|
||||
warn!("Task error: {:?}", e);
|
||||
}
|
||||
Ok(None) => {
|
||||
// JoinSet 为空,所有任务已完成
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Timed out waiting for tasks (3s), aborting remaining tasks");
|
||||
join_set.abort_all();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("HTTP server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 HTTP 服务器
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```no_run
|
||||
/// use agent_runner::{AgentRuntime, start_http_server, HttpServerConfig, AppConfig, ProxyConfig, HealthCheckConfig};
|
||||
/// use std::sync::Arc;
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// // 创建 Agent Runtime
|
||||
/// let (runtime, receiver) = AgentRuntime::new(1000);
|
||||
/// let runtime = Arc::new(runtime);
|
||||
/// runtime.start(receiver).await;
|
||||
///
|
||||
/// // 配置 HTTP Server
|
||||
/// let config = HttpServerConfig {
|
||||
/// port: 8080,
|
||||
/// app_config: AppConfig {
|
||||
/// port: 8080,
|
||||
/// projects_dir: PathBuf::from("/app/computer-project-workspace"),
|
||||
/// // 可选:启用 Pingora 代理服务
|
||||
/// proxy_config: Some(ProxyConfig {
|
||||
/// listen_port: 8088,
|
||||
/// default_backend_port: 8080,
|
||||
/// backend_host: "127.0.0.1".to_string(),
|
||||
/// port_param: "port".to_string(),
|
||||
/// health_check: HealthCheckConfig::default(),
|
||||
/// }),
|
||||
/// ..Default::default()
|
||||
/// },
|
||||
/// agent_runtime: runtime,
|
||||
/// shared_api_key_manager: Arc::new(dashmap::DashMap::new()),
|
||||
/// };
|
||||
///
|
||||
/// // 启动 HTTP Server
|
||||
/// let handle = start_http_server(config).await.unwrap();
|
||||
///
|
||||
/// // 优雅停止
|
||||
/// handle.stop().await;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn start_http_server(config: HttpServerConfig) -> Result<HttpServerHandle> {
|
||||
// 设置 mcp-proxy 日志目录环境变量(如果配置了的话)
|
||||
if let Some(ref log_dir) = config.app_config.mcp_proxy_log_dir {
|
||||
// SAFETY: 在服务启动时设置环境变量是安全的,此时尚未启动多线程任务
|
||||
unsafe {
|
||||
std::env::set_var("MCP_PROXY_LOG_DIR", log_dir);
|
||||
}
|
||||
info!("🔧 Set MCP_PROXY_LOG_DIR={}", log_dir);
|
||||
}
|
||||
|
||||
// 设置无限制模式(HTTP Server 部署不限制槽位)
|
||||
set_unlimited_mode(true);
|
||||
|
||||
// 创建关闭信号令牌
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let join_set = Arc::new(tokio::sync::Mutex::new(JoinSet::new()));
|
||||
#[cfg(feature = "proxy")]
|
||||
let pingora_result = Arc::new(tokio::sync::Mutex::new(None));
|
||||
|
||||
// 1. 启动 Agent 清理任务
|
||||
let cleanup_config = CleanupConfig {
|
||||
idle_timeout: Duration::from_secs(
|
||||
config
|
||||
.app_config
|
||||
.agent_cleanup
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.idle_timeout_secs,
|
||||
),
|
||||
cleanup_interval: Duration::from_secs(
|
||||
config
|
||||
.app_config
|
||||
.agent_cleanup
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.cleanup_interval_secs,
|
||||
),
|
||||
};
|
||||
info!(
|
||||
"🧹 [HTTP] Agent cleanup config: idle_timeout={}s, cleanup_interval={}s",
|
||||
cleanup_config.idle_timeout.as_secs(),
|
||||
cleanup_config.cleanup_interval.as_secs()
|
||||
);
|
||||
let cleanup_token = shutdown_token.child_token();
|
||||
join_set.lock().await.spawn(async move {
|
||||
tokio::select! {
|
||||
_ = start_cleanup_task(cleanup_config) => {}
|
||||
_ = cleanup_token.cancelled() => {
|
||||
info!("Cleanup task received shutdown signal");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 启动 Pingora 代理服务(如果配置了且启用了 proxy feature)
|
||||
#[cfg(feature = "proxy")]
|
||||
if let Some(proxy_config) = &config.app_config.proxy_config {
|
||||
let result = start_pingora(proxy_config, config.shared_api_key_manager.clone());
|
||||
// 保存 Pingora 结果以便后续调用 stop
|
||||
*pingora_result.lock().await = Some(result);
|
||||
} else {
|
||||
info!("Pingora proxy service is not configured, skipping startup");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
info!("Pingora proxy service is disabled (proxy feature not enabled)");
|
||||
|
||||
// 3. 创建 HTTP 应用状态
|
||||
let state = Arc::new(AppState::new(
|
||||
config.app_config.clone(),
|
||||
config.agent_runtime,
|
||||
config.shared_api_key_manager,
|
||||
));
|
||||
|
||||
// 4. 创建路由
|
||||
let app = create_router(state.clone());
|
||||
|
||||
// 5. 绑定地址并启动 HTTP 服务器
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
|
||||
info!("HTTP server started on port {}", config.port);
|
||||
|
||||
info!("HTTP API endpoints:");
|
||||
info!(" POST /computer/chat - Computer Agent chat");
|
||||
info!(" POST /computer/agent/status - Computer Agent status");
|
||||
info!(" POST /computer/agent/stop - Computer Agent stop");
|
||||
info!(" POST /computer/agent/session/cancel - Computer Agent cancel");
|
||||
info!(" GET /computer/progress/:session_id - SSE progress stream");
|
||||
info!(" -- RCoder Agent endpoints (new) --");
|
||||
info!(" POST /chat - RCoder Agent chat");
|
||||
info!(" GET /agent/status/:project_id - RCoder Agent status");
|
||||
info!(" POST /agent/stop - RCoder Agent stop");
|
||||
info!(" POST /agent/session/cancel - RCoder Agent cancel");
|
||||
info!(" GET /agent/progress/:session_id - RCoder SSE progress stream");
|
||||
info!(" -- Common endpoints --");
|
||||
info!(" GET /health - Health check");
|
||||
info!(" GET /api/docs - Swagger API documentation");
|
||||
|
||||
// 6. 启动 HTTP 服务任务
|
||||
let http_token = shutdown_token.child_token();
|
||||
// 将 listener 和 app 移入任务中
|
||||
let http_app = app;
|
||||
let http_listener = listener;
|
||||
join_set.lock().await.spawn(async move {
|
||||
// 使用 graceful shutdown wrapper
|
||||
let server = axum::serve(http_listener, http_app).with_graceful_shutdown(async move {
|
||||
let _ = http_token.cancelled().await;
|
||||
});
|
||||
|
||||
match server.await {
|
||||
Ok(()) => info!("HTTP service exited normally"),
|
||||
Err(e) => error!("HTTP service error: {:?}", e),
|
||||
}
|
||||
});
|
||||
|
||||
// 创建 handle
|
||||
let handle = HttpServerHandle {
|
||||
shutdown_token,
|
||||
join_set,
|
||||
#[cfg(feature = "proxy")]
|
||||
pingora_result,
|
||||
};
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
Reference in New Issue
Block a user