880 lines
25 KiB
Markdown
880 lines
25 KiB
Markdown
# rcoder
|
||
|
||
## 项目概述
|
||
|
||
基于 ACP (Agent Protocol) 协议的 AI 代理开发平台,提供 Docker 容器化运行时和反向代理。本项目的核心模块 `agent_runner` 提供了完整的 Agent 生命周期管理能力,包括 Docker 容器管理、消息收发、超时自动销毁等功能。
|
||
|
||
**本地路径**: `vendors/rcoder`
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
rcoder/
|
||
├── Cargo.toml # workspace 配置
|
||
├── crates/
|
||
│ ├── rcoder/ # 主应用服务
|
||
│ │ ├── src/
|
||
│ │ │ ├── lib.rs
|
||
│ │ │ ├── main.rs
|
||
│ │ │ ├── app.rs # 应用主逻辑
|
||
│ │ │ ├── router.rs # HTTP 路由
|
||
│ │ │ ├── state.rs # 应用状态
|
||
│ │ │ ├── error.rs # 错误处理
|
||
│ │ │ └── middlewares/ # 中间件
|
||
│ │ └── Cargo.toml
|
||
│ │
|
||
│ ├── agent_runner/ # Agent 运行时核心(最重要)
|
||
│ │ ├── src/
|
||
│ │ │ ├── lib.rs
|
||
│ │ │ ├── manager.rs # AgentWorkerManager
|
||
│ │ │ ├── worker.rs # Agent Worker
|
||
│ │ │ ├── adapter.rs # ACP 协议适配器
|
||
│ │ │ ├── types.rs # 类型定义
|
||
│ │ │ ├── message.rs # 消息处理
|
||
│ │ │ └── docker.rs # Docker 集成
|
||
│ │ └── Cargo.toml
|
||
│ │
|
||
│ ├── docker_manager/ # Docker 容器管理
|
||
│ │ ├── src/
|
||
│ │ │ ├── lib.rs
|
||
│ │ │ ├── manager.rs # DockerManager
|
||
│ │ │ ├── actor.rs # Actor 模式状态
|
||
│ │ │ ├── container.rs # 容器操作
|
||
│ │ │ └── network.rs # 网络管理
|
||
│ │ └── Cargo.toml
|
||
│ │
|
||
│ ├── acp_adapter/ # ACP 协议适配器
|
||
│ │ ├── src/
|
||
│ │ │ ├── lib.rs
|
||
│ │ │ ├── types.rs # ACP 类型定义
|
||
│ │ │ ├── client.rs # ACP 客户端
|
||
│ │ │ └── handler.rs # 消息处理
|
||
│ │ └── Cargo.toml
|
||
│ │
|
||
│ ├── shared_types/ # 共享类型定义
|
||
│ │ ├── src/
|
||
│ │ │ ├── lib.rs
|
||
│ │ │ ├── proto/ # gRPC proto 文件
|
||
│ │ │ │ └── agent.proto
|
||
│ │ │ └── types.rs
|
||
│ │ └── Cargo.toml
|
||
│ │
|
||
│ └── rcoder-proxy/ # 反向代理
|
||
│ ├── src/
|
||
│ │ ├── lib.rs
|
||
│ │ ├── proxy.rs
|
||
│ │ └── upstream.rs
|
||
│ └── Cargo.toml
|
||
└── Cargo.lock
|
||
```
|
||
|
||
## Agent Worker Manager (agent_runner)
|
||
|
||
这是 rcoder 的核心模块,负责管理 agent 的生命周期。
|
||
|
||
### 核心结构体
|
||
|
||
```rust
|
||
// crates/agent_runner/src/manager.rs
|
||
|
||
use tokio::sync::{mpsc, watch};
|
||
use std::sync::{Arc, Mutex};
|
||
use chrono::DateTime;
|
||
use dashmap::DashMap;
|
||
|
||
pub struct AgentWorkerManager {
|
||
// 任务发送器(使用 ArcSwap 支持热更新)
|
||
sender: ArcSwap<Option<mpsc::UnboundedSender<LocalSetAgentRequest>>>,
|
||
|
||
// 状态广播
|
||
state_tx: watch::Sender<WorkerState>,
|
||
|
||
// 最后心跳时间
|
||
last_heartbeat: Arc<Mutex<Option<DateTime<Utc>>>>,
|
||
|
||
// 状态变化时间
|
||
last_state_change: Arc<Mutex<DateTime<Utc>>>,
|
||
|
||
// 活跃请求追踪
|
||
active_requests: Arc<DashMap<String, DateTime<Utc>>>,
|
||
|
||
// 配置
|
||
config: AgentRunnerConfig,
|
||
}
|
||
|
||
// Worker 状态
|
||
pub enum WorkerState {
|
||
Starting,
|
||
Running,
|
||
Stopping,
|
||
Stopped,
|
||
}
|
||
|
||
impl Default for WorkerState {
|
||
fn default() -> Self {
|
||
WorkerState::Stopped
|
||
}
|
||
}
|
||
```
|
||
|
||
### 心跳超时检测
|
||
|
||
```rust
|
||
// crates/agent_runner/src/manager.rs
|
||
|
||
impl AgentWorkerManager {
|
||
/// 检查心跳是否超时
|
||
pub fn check_heartbeat_timeout(&self) -> bool {
|
||
let last_heartbeat_opt = {
|
||
let last = self.last_heartbeat.lock().expect("mutex poisoned");
|
||
*last // Copy 数据,立即释放锁
|
||
};
|
||
|
||
if let Some(timestamp) = last_heartbeat_opt {
|
||
// 正常运行时:15秒无心跳视为超时
|
||
let elapsed = Utc::now() - timestamp;
|
||
elapsed.num_seconds() > 15
|
||
} else {
|
||
// 首次启动:30秒内未收到心跳视为超时
|
||
let state_change = self.last_state_change.lock().expect("mutex poisoned");
|
||
let elapsed = Utc::now() - *state_change;
|
||
elapsed.num_seconds() > 30
|
||
}
|
||
}
|
||
|
||
/// 更新心跳时间
|
||
pub fn update_heartbeat(&self) {
|
||
let mut last = self.last_heartbeat.lock().expect("mutex poisoned");
|
||
*last = Some(Utc::now());
|
||
}
|
||
}
|
||
```
|
||
|
||
### Agent Worker 核心逻辑
|
||
|
||
```rust
|
||
// crates/agent_runner/src/worker.rs
|
||
|
||
pub struct AgentWorker {
|
||
config: AgentRunnerConfig,
|
||
receiver: mpsc::UnboundedReceiver<LocalSetAgentRequest>,
|
||
state_tx: watch::Sender<WorkerState>,
|
||
docker: DockerManager,
|
||
adapter: ACPAdapter,
|
||
}
|
||
|
||
impl AgentWorker {
|
||
pub async fn run(&mut self) {
|
||
tracing::info!("AgentWorker started");
|
||
self.state_tx.send(WorkerState::Running).unwrap();
|
||
|
||
loop {
|
||
tokio::select! {
|
||
Some(request) = self.receiver.recv() => {
|
||
self.handle_request(request).await;
|
||
}
|
||
_ = self.docker.health_check() => {
|
||
// Docker 健康检查
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async fn handle_request(&mut self, request: LocalSetAgentRequest) {
|
||
match request {
|
||
LocalSetAgentRequest::Message { session_id, content } => {
|
||
self.handle_message(&session_id, &content).await;
|
||
}
|
||
LocalSetAgentRequest::Stop => {
|
||
self.shutdown().await;
|
||
return;
|
||
}
|
||
LocalSetAgentRequest::Spawn { config } => {
|
||
self.spawn_container(&config).await;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 启动 Docker 容器
|
||
async fn spawn_container(&mut self, config: &AgentConfig) -> Result<(), AgentError> {
|
||
// 1. 创建容器
|
||
let container_id = self.docker.create_container(CreateContainerOptions {
|
||
image: config.image.clone(),
|
||
env: config.env_vars.clone(),
|
||
Cmd: config.command.clone().unwrap_or_else(|| vec!["/bin/sh"]),
|
||
..Default::default()
|
||
}).await?;
|
||
|
||
// 2. 启动容器
|
||
self.docker.start_container(&container_id).await?;
|
||
|
||
// 3. 等待容器就绪
|
||
self.docker.wait_until_ready(&container_id).await?;
|
||
|
||
// 4. 建立 ACP 连接
|
||
self.adapter.connect(&container_id, config.acp_url).await?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
```
|
||
|
||
### ACP 协议消息类型
|
||
|
||
```rust
|
||
// crates/agent_runner/src/types.rs
|
||
|
||
use agent_client_protocol::{Message, MessageChunk};
|
||
|
||
// ACP 流更新事件
|
||
pub enum StreamUpdate {
|
||
UserMessageChunk {
|
||
session_id: String,
|
||
content: String,
|
||
},
|
||
AgentMessageChunk {
|
||
session_id: String,
|
||
content: String,
|
||
},
|
||
ToolCall {
|
||
session_id: String,
|
||
tool_call: ToolCall,
|
||
},
|
||
ToolCallUpdate {
|
||
session_id: String,
|
||
tool_call_update: ToolCallUpdate,
|
||
},
|
||
SessionStateChanged {
|
||
session_id: String,
|
||
new_state: SessionState,
|
||
message: String,
|
||
},
|
||
Plan {
|
||
session_id: String,
|
||
plan: Plan,
|
||
},
|
||
StepFinished {
|
||
session_id: String,
|
||
step_id: String,
|
||
},
|
||
SessionFinished {
|
||
session_id: String,
|
||
reason: String,
|
||
},
|
||
}
|
||
|
||
pub enum SessionState {
|
||
Initializing,
|
||
Connected,
|
||
Prompting,
|
||
Paused,
|
||
Closed,
|
||
Error(String),
|
||
}
|
||
|
||
pub struct Plan {
|
||
pub entries: Vec<PlanEntry>,
|
||
pub created_at: SystemTime,
|
||
pub status: PlanStatus,
|
||
}
|
||
|
||
pub enum PlanStatus {
|
||
Pending,
|
||
Approved,
|
||
Executing,
|
||
Finished,
|
||
Cancelled,
|
||
}
|
||
|
||
pub struct ToolCall {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub arguments: serde_json::Value,
|
||
}
|
||
|
||
pub struct ToolCallUpdate {
|
||
pub id: String,
|
||
pub status: ToolCallStatus,
|
||
pub result: Option<serde_json::Value>,
|
||
pub error: Option<String>,
|
||
}
|
||
|
||
pub enum ToolCallStatus {
|
||
Pending,
|
||
Running,
|
||
Completed,
|
||
Failed,
|
||
Cancelled,
|
||
}
|
||
```
|
||
|
||
## Docker Manager (docker_manager)
|
||
|
||
Docker 容器管理模块,使用 Actor 模式管理容器状态,避免并发问题。
|
||
|
||
### 核心结构体
|
||
|
||
```rust
|
||
// crates/docker_manager/src/manager.rs
|
||
|
||
use bollard::{Docker, APIError};
|
||
use std::sync::Arc;
|
||
use tokio::sync::RwLock;
|
||
|
||
pub struct DockerManager {
|
||
docker: Docker, // Docker 客户端
|
||
config: DockerManagerConfig, // 配置
|
||
containers: ContainerStateHandle, // Actor 模式状态
|
||
main_network_name: Arc<RwLock<String>>, // 网络名称
|
||
}
|
||
|
||
pub struct DockerManagerConfig {
|
||
pub socket_path: Option<String>, // Docker socket 路径
|
||
pub network_name: String, // 网络名称
|
||
pub max_containers: usize, // 最大容器数
|
||
pub container_ttl: Duration, // 容器存活时间
|
||
pub auto_remove: bool, // 是否自动删除
|
||
}
|
||
```
|
||
|
||
### Actor 模式状态管理
|
||
|
||
```rust
|
||
// crates/docker_manager/src/container_state_actor.rs
|
||
|
||
// 使用 Actor 模式避免 DashMap 跨 await 持有锁导致的死锁
|
||
pub struct ContainerStateActor {
|
||
containers: HashMap<String, DockerContainerInfo>,
|
||
receiver: mpsc::Receiver<ContainerStateCommand>,
|
||
}
|
||
|
||
pub enum ContainerStateCommand {
|
||
Get { key: String, reply: oneshot::Sender<Option<DockerContainerInfo>> },
|
||
Insert { key: String, info: DockerContainerInfo },
|
||
Remove { key: String, reply: oneshot::Sender<Option<DockerContainerInfo>> },
|
||
List { reply: oneshot::Sender<Vec<DockerContainerInfo>> },
|
||
Keys { reply: oneshot::Sender<Vec<String>> },
|
||
RemoveIfContainerId { key: String, container_id: String, reply: oneshot::Sender<Option<DockerContainerInfo>> },
|
||
}
|
||
|
||
impl ContainerStateActor {
|
||
pub async fn run(&mut self) {
|
||
while let Some(cmd) = self.receiver.recv().await {
|
||
match cmd {
|
||
ContainerStateCommand::Get { key, reply } => {
|
||
let info = self.containers.get(&key).cloned();
|
||
reply.send(info).ok();
|
||
}
|
||
ContainerStateCommand::Insert { key, info } => {
|
||
self.containers.insert(key, info);
|
||
}
|
||
// ... 其他命令处理
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 容器创建流程
|
||
|
||
```rust
|
||
// crates/docker_manager/src/manager.rs
|
||
|
||
impl DockerManager {
|
||
pub async fn create_container(
|
||
&self,
|
||
config: DockerContainerConfig,
|
||
) -> DockerResult<DockerContainerInfo> {
|
||
let container_name = format!("{}-{}", self.config.network_name, config.name);
|
||
|
||
// 1. 清理同名旧容器
|
||
if let Ok(Some((existing_id, _, status, is_running))) =
|
||
self.find_container_realtime(&container_name).await
|
||
{
|
||
if is_running {
|
||
self.stop_container_by_id(&existing_id).await?;
|
||
}
|
||
self.remove_container_by_id(&existing_id).await?;
|
||
}
|
||
|
||
// 2. 拉取镜像
|
||
self.ensure_image_exists(&config.image).await?;
|
||
|
||
// 3. 构建挂载点
|
||
let mut mounts = Vec::new();
|
||
if !config.host_path.is_empty() {
|
||
mounts.push(Mount {
|
||
target: Some(config.container_path.clone()),
|
||
source: Some(config.host_path.clone()),
|
||
typ: Some(MountTypeEnum::BIND),
|
||
read_only: Some(false),
|
||
..Default::default()
|
||
});
|
||
}
|
||
|
||
// 4. 构建环境变量
|
||
let mut env_vars: Vec<String> = config.env_vars
|
||
.into_iter()
|
||
.map(|(k, v)| format!("{}={}", k, v))
|
||
.collect();
|
||
|
||
// 5. 构建网络配置
|
||
let (networking_config, container_network_name) = if config.network_mode != "host" {
|
||
let main_network = self.get_main_network_name().await;
|
||
let mut endpoints = HashMap::new();
|
||
endpoints.insert(main_network.clone(), EndpointSettings {
|
||
aliases: Some(vec![container_name.clone()]),
|
||
..Default::default()
|
||
});
|
||
(Some(NetworkingConfig { endpoints_config: Some(endpoints) }), main_network)
|
||
} else {
|
||
(None, "host".to_string())
|
||
};
|
||
|
||
// 6. 创建并启动容器
|
||
let container_config = ContainerConfig {
|
||
image: config.image.clone(),
|
||
env: Some(env_vars),
|
||
cmd: config.command.map(|c| vec![c]),
|
||
host_config: Some(HostConfig {
|
||
mounts: Some(mounts),
|
||
network_mode: Some(container_network_name),
|
||
auto_remove: Some(self.config.auto_remove),
|
||
..Default::default()
|
||
}),
|
||
..Default::default()
|
||
};
|
||
|
||
let response = self.docker.create_container(
|
||
Some(CreateContainerOptions {
|
||
name: container_name.clone(),
|
||
..Default::default()
|
||
}),
|
||
container_config,
|
||
).await?;
|
||
|
||
self.docker.start_container(&response.id, None::<StartContainerOptions>).await?;
|
||
|
||
// 7. 健康检查
|
||
self.check_container_health(&response.id).await?;
|
||
|
||
// 8. 获取容器信息
|
||
let inspect = self.docker.inspect_container(&response.id, None).await?;
|
||
|
||
Ok(DockerContainerInfo {
|
||
id: response.id,
|
||
name: container_name,
|
||
image: config.image,
|
||
status: "running".to_string(),
|
||
ip_address: inspect.network_settings?.ip_address,
|
||
created_at: Utc::now(),
|
||
})
|
||
}
|
||
|
||
/// 等待容器就绪
|
||
pub async fn wait_until_ready(&self, container_id: &str) -> Result<(), DockerError> {
|
||
let timeout = Duration::from_secs(60);
|
||
let start_time = Utc::now();
|
||
|
||
loop {
|
||
// 检查容器状态
|
||
let info = self.inspect_container(container_id).await?;
|
||
if info.state.as_ref().map(|s| &s.status) == Some(&ContainerStateStatusEnum::RUNNING) {
|
||
// 检查健康检查
|
||
if let Some(health) = &info.state.as_ref().unwrap().healthcheck {
|
||
if health.status == ContainerHealthStatusEnum::HEALTHY {
|
||
return Ok(());
|
||
}
|
||
} else {
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
// 超时检查
|
||
if (Utc::now() - start_time).num_seconds() > timeout.as_secs() as i64 {
|
||
return Err(DockerError::Timeout);
|
||
}
|
||
|
||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 超时自动销毁机制
|
||
|
||
```rust
|
||
// crates/agent_runner/src/proxy_agent/cleanup_task.rs
|
||
|
||
pub struct CleanupConfig {
|
||
pub idle_timeout: Duration, // 闲置超时(默认3分钟)
|
||
pub cleanup_interval: Duration, // 清理间隔(默认30秒)
|
||
}
|
||
|
||
impl Default for CleanupConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
idle_timeout: Duration::from_secs(3 * 60), // 3分钟
|
||
cleanup_interval: Duration::from_secs(30), // 30秒
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理逻辑:基于 RAII 模式
|
||
async fn cleanup_idle_agents(&mut self) -> Result<CleanupStats> {
|
||
for entry in AGENT_REGISTRY.iter_agents() {
|
||
let agent_info = entry.value();
|
||
|
||
// 只清理 Idle 状态的 agent
|
||
if agent_info.status == AgentStatus::Idle
|
||
&& self.is_agent_idle_timeout(agent_info.last_activity, current_time)
|
||
{
|
||
// 从 Registry 中移除,AgentLifecycleGuard 自动清理资源
|
||
AGENT_REGISTRY.remove_by_project(project_id);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 环境变量和 PATH 管理
|
||
|
||
```rust
|
||
// crates/agent_runner/src/docker.rs
|
||
|
||
pub fn build_environment(
|
||
node_path: &Path,
|
||
npm_global_path: &Path,
|
||
extra_env: &HashMap<String, String>,
|
||
) -> Vec<String> {
|
||
let mut env = vec![
|
||
// Node.js 环境
|
||
format!("PATH={}:{}:{}",
|
||
node_path.join("bin").to_string_lossy(),
|
||
npm_global_path.join("bin").to_string_lossy(),
|
||
std::env::var("PATH").unwrap_or_default()
|
||
),
|
||
format!("NODE_PATH={}", node_path.join("lib").to_string_lossy()),
|
||
format!("NPM_CONFIG_PREFIX={}", npm_global_path.to_string_lossy()),
|
||
];
|
||
|
||
// 添加额外的环境变量
|
||
for (k, v) in extra_env {
|
||
env.push(format!("{}={}", k, v));
|
||
}
|
||
|
||
env
|
||
}
|
||
|
||
// 在容器启动时设置环境变量
|
||
pub async fn start_container_with_env(
|
||
&self,
|
||
config: &AgentConfig,
|
||
) -> Result<String, AgentError> {
|
||
let env = build_environment(
|
||
&config.node_path,
|
||
&config.npm_global_path,
|
||
&config.extra_env,
|
||
);
|
||
|
||
let container_id = self.docker.create_container(CreateContainerOptions {
|
||
image: config.image.clone(),
|
||
env: Some(env),
|
||
Cmd: config.command.clone().unwrap_or_default(),
|
||
..Default::default()
|
||
}).await?;
|
||
|
||
self.docker.start_container(&container_id, None).await?;
|
||
Ok(container_id)
|
||
}
|
||
```
|
||
|
||
## Node.js 自动安装
|
||
|
||
```rust
|
||
// crates/agent_runner/src/installer/node_installer.rs
|
||
|
||
pub struct NodeInstaller {
|
||
install_dir: PathBuf,
|
||
platform: Platform,
|
||
}
|
||
|
||
pub enum Platform {
|
||
Windows,
|
||
MacOSIntel,
|
||
MacOSAppleSilicon,
|
||
Linux,
|
||
}
|
||
|
||
impl NodeInstaller {
|
||
/// 检测系统是否已有 Node.js
|
||
pub async fn detect_system_node(&self) -> Option<Version> {
|
||
// 检查系统 PATH
|
||
if let Ok(path) = which("node") {
|
||
if let Ok(output) = Command::new(&path)
|
||
.arg("--version")
|
||
.output()
|
||
.await
|
||
{
|
||
if output.status.success() {
|
||
let version_str = String::from_utf8_lossy(&output.stdout);
|
||
return Version::parse(&version_str.trim_start_matches('v')).ok();
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// 下载并安装 Node.js
|
||
pub async fn download_and_install(&self, version: &Version) -> Result<(), InstallationError> {
|
||
let url = self.get_download_url(version);
|
||
let archive_path = self.install_dir.join("cache").join(format!("node-{}.tar.xz", version));
|
||
|
||
// 1. 下载
|
||
if !archive_path.exists() {
|
||
self.download(&url, &archive_path).await?;
|
||
}
|
||
|
||
// 2. 解压
|
||
let extract_dir = self.install_dir.join("node").join(version.to_string());
|
||
self.extract(&archive_path, &extract_dir).await?;
|
||
|
||
// 3. 创建 symlink
|
||
let bin_path = extract_dir.join("bin");
|
||
let symlink_path = self.install_dir.join("bin").join("node");
|
||
if symlink_path.exists() {
|
||
std::fs::remove_file(&symlink_path)?;
|
||
}
|
||
std::os::unix::fs::symlink(&bin_path.join("node"), &symlink_path)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn get_download_url(&self, version: &Version) -> String {
|
||
match self.platform {
|
||
Platform::Windows => format!(
|
||
"https://nodejs.org/dist/v{}/node-v{}-win-x64.zip",
|
||
version, version
|
||
),
|
||
Platform::MacOSIntel => format!(
|
||
"https://nodejs.org/dist/v{}/node-v{}-darwin-x64.tar.gz",
|
||
version, version
|
||
),
|
||
Platform::MacOSAppleSilicon => format!(
|
||
"https://nodejs.org/dist/v{}/node-v{}-darwin-arm64.tar.gz",
|
||
version, version
|
||
),
|
||
Platform::Linux => format!(
|
||
"https://nodejs.org/dist/v{}/node-v{}-linux-x64.tar.xz",
|
||
version, version
|
||
),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## ACP 协议适配器
|
||
|
||
```rust
|
||
// crates/acp_adapter/src/client.rs
|
||
|
||
use agent_client_protocol::{Client, Message as AcpMessage};
|
||
|
||
pub struct ACPAdapter {
|
||
client: Option<Client<WebSocketStream<Upgraded>>>,
|
||
session_id: String,
|
||
pending_requests: Arc<DashMap<String, oneshot::Sender<Result<String>>>>,
|
||
}
|
||
|
||
impl ACPAdapter {
|
||
/// 连接到 Agent 容器
|
||
pub async fn connect(
|
||
&mut self,
|
||
container_id: &str,
|
||
url: &str,
|
||
) -> Result<(), AdapterError> {
|
||
// 获取容器 IP
|
||
let container_ip = self.get_container_ip(container_id).await?;
|
||
|
||
// 建立 WebSocket 连接
|
||
let stream = WebSocket::connect(url).await?;
|
||
let (ws_stream, _) = stream.split();
|
||
let (write, read) = ws_stream.split();
|
||
|
||
// 创建 ACP 客户端
|
||
self.client = Some(Client::new(read, write));
|
||
|
||
// 生成会话 ID
|
||
self.session_id = uuid::Uuid::new_v4().to_string();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 发送消息
|
||
pub async fn send_message(&self, session_id: &str, content: &str) {
|
||
if let Some(client) = &self.client {
|
||
let message = AcpMessage::user_message_chunk(session_id, content);
|
||
client.send(message).await;
|
||
}
|
||
}
|
||
|
||
/// 接收消息
|
||
pub async fn receive(&self) -> Option<StreamUpdate> {
|
||
if let Some(client) = &self.client {
|
||
if let Some(msg) = client.recv().await {
|
||
Some(self.convert_message(msg))
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 在本项目中的使用
|
||
|
||
### Agent 依赖管理界面
|
||
|
||
```rust
|
||
// agent-client/src/ui/dependency_management.rs
|
||
|
||
use crate::agent::AgentManager;
|
||
|
||
pub struct DependencyManager {
|
||
agent_manager: Arc<AgentManager>,
|
||
node_installer: Arc<NodeInstaller>,
|
||
}
|
||
|
||
impl DependencyManager {
|
||
/// 检查所有依赖状态
|
||
pub async fn check_all_dependencies(&self) -> Vec<DependencyStatus> {
|
||
let mut statuses = vec![];
|
||
|
||
// 检查 Node.js
|
||
statuses.push(self.check_node().await);
|
||
|
||
// 检查 npm
|
||
statuses.push(self.check_npm().await);
|
||
|
||
// 检查 opencode
|
||
statuses.push(self.check_agent("opencode").await);
|
||
|
||
// 检查 claude-code
|
||
statuses.push(self.check_agent("@anthropic-ai/claude-code").await);
|
||
|
||
statuses
|
||
}
|
||
|
||
async fn check_node(&self) -> DependencyStatus {
|
||
// 1. 先检查系统全局
|
||
if let Some(version) = self.node_installer.detect_system_node().await {
|
||
return DependencyStatus {
|
||
name: "Node.js",
|
||
version: Some(version.to_string()),
|
||
source: DependencySource::System,
|
||
status: DependencyStatus::Installed,
|
||
action: None,
|
||
};
|
||
}
|
||
|
||
// 2. 检查客户端目录
|
||
if let Some(version) = self.detect_local_node().await {
|
||
return DependencyStatus {
|
||
name: "Node.js",
|
||
version: Some(version.to_string()),
|
||
source: DependencySource::Local,
|
||
status: DependencyStatus::Installed,
|
||
action: Some(DependencyAction::Update),
|
||
};
|
||
}
|
||
|
||
// 3. 未安装
|
||
DependencyStatus {
|
||
name: "Node.js",
|
||
version: None,
|
||
source: DependencySource::None,
|
||
status: DependencyStatus::NotInstalled,
|
||
action: Some(DependencyAction::Install),
|
||
}
|
||
}
|
||
}
|
||
|
||
pub enum DependencySource {
|
||
System,
|
||
Local,
|
||
None,
|
||
}
|
||
|
||
pub enum DependencyStatus {
|
||
Installed,
|
||
Installing,
|
||
Failed,
|
||
NotInstalled,
|
||
NeedsUpdate,
|
||
}
|
||
```
|
||
|
||
### 安装目录结构
|
||
|
||
```
|
||
<APP_DATA_DIR>/ # 客户端应用数据目录
|
||
├── tools/
|
||
│ ├── node/ # Node.js 安装目录
|
||
│ │ ├── v20.10.0/
|
||
│ │ │ ├── bin/
|
||
│ │ │ │ ├── node
|
||
│ │ │ │ └── npm
|
||
│ │ │ └── lib/
|
||
│ │ └── current -> v20.10.0 # 当前版本 symlink
|
||
│ ├── npm-global/ # npm 全局安装目录(隔离)
|
||
│ │ ├── bin/
|
||
│ │ │ ├── opencode
|
||
│ │ │ └── claude
|
||
│ │ └── lib/
|
||
│ └── versions.json # 已安装工具的版本记录
|
||
├── config/ # 配置文件
|
||
├── logs/ # 日志文件
|
||
└── cache/ # 缓存文件(下载的安装包等)
|
||
```
|
||
|
||
## 关键设计模式
|
||
|
||
1. **Actor 模式**: DockerManager 使用 Actor 模式管理容器状态,避免死锁
|
||
2. **ArcSwap**: 热更新配置而不需要重启
|
||
3. **DashMap**: 高并发场景下的线程安全 HashMap
|
||
4. **Watch 通道**: 状态变更广播
|
||
5. **RAII 清理**: Agent 闲置超时自动销毁
|
||
6. **隔离安装**: Node.js 和 Agent 工具安装到独立目录,不污染全局环境
|
||
|
||
## 可复用代码
|
||
|
||
| 模块 | 路径 | 用途 |
|
||
|------|------|------|
|
||
| **agent_runner** | `crates/agent_runner/` | Agent 生命周期管理 |
|
||
| **docker_manager** | `crates/docker_manager/` | Docker 容器操作 |
|
||
| **acp_adapter** | `crates/acp_adapter/` | ACP 协议客户端 |
|
||
|
||
## 与本项目的集成
|
||
|
||
```
|
||
agent-client
|
||
│
|
||
├── UI 层
|
||
│ ├── 依赖管理界面 (DataTable, Badge, Button)
|
||
│ ├── 安装进度弹窗 (Dialog, Progress)
|
||
│ └── 设置界面 (Form, Switch, Input)
|
||
│
|
||
└── Agent 运行时
|
||
├── rcoder/agent_runner # Agent 生命周期管理
|
||
│ │
|
||
│ ├── Docker Manager # 容器管理
|
||
│ │ └── opencode 容器
|
||
│ │
|
||
│ └── ACP Adapter # ACP 协议通信
|
||
│ │
|
||
│ └── opencode ACP 端口
|
||
│
|
||
└── Node.js 安装器 # 环境隔离安装
|
||
```
|