Files
qiming/qimingclaw/specs/agent-client/vendorsdoc/rcoder.md

25 KiB
Raw Permalink Blame History

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 的生命周期。

核心结构体

// 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
    }
}

心跳超时检测

// 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 核心逻辑

// 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 协议消息类型

// 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 模式管理容器状态,避免并发问题。

核心结构体

// 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 模式状态管理

// 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);
                }
                // ... 其他命令处理
            }
        }
    }
}

容器创建流程

// 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;
        }
    }
}

超时自动销毁机制

// 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 管理

// 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 自动安装

// 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 协议适配器

// 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 依赖管理界面

// 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 安装器          # 环境隔离安装