1668 lines
57 KiB
Markdown
1668 lines
57 KiB
Markdown
# Computer Agent Runner 需求与设计文档
|
||
|
||
**文档版本**: v1.0
|
||
**创建日期**: 2025-12-10
|
||
**作者**: Claude (基于用户需求分析)
|
||
**项目**: rcoder - AI 驱动开发平台
|
||
|
||
---
|
||
|
||
## 一、项目背景
|
||
|
||
### 1.1 现有架构
|
||
|
||
rcoder 当前采用的是"一个 project_id 对应一个 Docker 容器"的架构模式:
|
||
|
||
- **容器类型**: `ServiceType::RCoder`
|
||
- **容器命名**: `rcoder-agent-{project_id}`
|
||
- **工作目录**: `/app/project_workspace/{project_id}`
|
||
- **通信协议**: gRPC (50051) + HTTP (8086)
|
||
- **核心功能**: AI 代码生成、项目管理、会话管理
|
||
|
||
### 1.2 新需求背景
|
||
|
||
用户希望构建一个带有虚拟远程桌面的 Agentic AI 系统,使 AI Agent 能够:
|
||
|
||
1. **操作浏览器**: 在虚拟桌面中打开 Chromium,自主搜索和访问网络资料
|
||
2. **远程监控**: 用户可通过 VNC 远程查看 Agent 的操作过程
|
||
3. **复杂任务处理**: Agent 在容器内完成复杂的多步骤任务(如网页抓取、数据处理等)
|
||
4. **资源共享**: 一个用户可以有多个项目,共享同一个桌面环境容器
|
||
|
||
### 1.3 项目目标
|
||
|
||
设计并实现 **Computer Agent Runner** 服务,作为 rcoder 的扩展功能模块,具备以下特性:
|
||
|
||
- ✅ 一个 `user_id` 对应一个带桌面环境的容器
|
||
- ✅ 容器内可同时运行多个 `project_id` 对应的 AI Agent 实例
|
||
- ✅ 提供 VNC 远程桌面访问,用户可实时查看 Agent 操作
|
||
- ✅ 集成 Chrome DevTools MCP,赋予 Agent 浏览器操作能力
|
||
- ✅ 智能闲置检测:只有当用户下所有项目都闲置时才销毁容器
|
||
|
||
---
|
||
|
||
## 二、需求分析
|
||
|
||
### 2.1 功能需求
|
||
|
||
#### FR-1: 用户容器管理
|
||
|
||
| ID | 需求描述 | 优先级 |
|
||
|----|---------|-------|
|
||
| FR-1.1 | 系统根据 `user_id` 自动创建和管理容器 | P0 |
|
||
| FR-1.2 | 容器命名规则:`computer-agent-runner-{user_id}` | P0 |
|
||
| FR-1.3 | 容器挂载路径:`/app/computer-project-workspace/{user_id}` | P0 |
|
||
| FR-1.4 | 支持资源限额配置(内存、CPU) | P1 |
|
||
| FR-1.5 | 容器启动后自动加载 XFCE 桌面环境和 VNC 服务 | P0 |
|
||
|
||
#### FR-2: 多 Agent 实例管理
|
||
|
||
| ID | 需求描述 | 优先级 |
|
||
|----|---------|-------|
|
||
| FR-2.1 | 容器内支持同时运行多个 `project_id` 对应的 Agent 实例 | P0 |
|
||
| FR-2.2 | 每个 Agent 实例独立管理,互不干扰(无上下文污染) | P0 |
|
||
| FR-2.3 | 通过 gRPC `Chat` RPC 根据 `project_id` 路由到对应 Agent | P0 |
|
||
| FR-2.4 | 支持按 `project_id` 停止单个 Agent(不销毁容器) | P0 |
|
||
| FR-2.5 | Agent 使用 `computer_agent_default.json` 配置(包含 Chrome DevTools MCP) | P1 |
|
||
|
||
#### FR-3: VNC 远程桌面访问
|
||
|
||
| ID | 需求描述 | 优先级 |
|
||
|----|---------|-------|
|
||
| FR-3.1 | 提供 HTTP 接口访问 VNC 桌面:`GET /computer/desktop/{user_id}/{project_id}` | P0 |
|
||
| FR-3.2 | 通过 Pingora 或 Nginx 透明代理 WebSocket 到容器的 6080 端口 | P0 |
|
||
| FR-3.3 | 支持实时桌面查看和交互(通过 noVNC) | P0 |
|
||
| FR-3.4 | VNC 连接与 `user_id` 绑定,保障安全隔离 | P1 |
|
||
|
||
#### FR-4: 浏览器操作能力
|
||
|
||
| ID | 需求描述 | 优先级 |
|
||
|----|---------|-------|
|
||
| FR-4.1 | 容器内预装 Chromium 浏览器(远程调试端口 9222) | P0 |
|
||
| FR-4.2 | Agent 通过 Chrome DevTools MCP 操作浏览器 | P0 |
|
||
| FR-4.3 | 支持网页导航、元素操作、截图等功能 | P1 |
|
||
|
||
#### FR-5: HTTP 接口
|
||
|
||
| ID | 接口路径 | 方法 | 说明 |
|
||
|----|---------|------|------|
|
||
| FR-5.1 | `/computer/chat` | POST | 发送聊天请求(必需 `user_id`) |
|
||
| FR-5.2 | `/computer/agent/stop` | POST | 停止特定 `project_id` 的 Agent |
|
||
| FR-5.3 | `/computer/progress/{session_id}` | GET | SSE 进度流(同现有 `/agent/progress`) |
|
||
| FR-5.4 | `/computer/desktop/{user_id}/{project_id}` | GET | VNC 桌面访问 |
|
||
|
||
#### FR-6: 闲置检测和资源回收
|
||
|
||
| ID | 需求描述 | 优先级 |
|
||
|----|---------|-------|
|
||
| FR-6.1 | 区分 `ServiceType` 的闲置检测策略 | P0 |
|
||
| FR-6.2 | `ComputerAgentRunner`:只有当 `user_id` 下所有 `project_id` 都闲置时才销毁容器 | P0 |
|
||
| FR-6.3 | 闲置超时时间:默认 30 分钟(可配置) | P1 |
|
||
| FR-6.4 | 容器保护期:创建后 5 分钟内不进行清理 | P1 |
|
||
|
||
### 2.2 非功能需求
|
||
|
||
| ID | 需求描述 | 指标 |
|
||
|----|---------|------|
|
||
| NFR-1 | 容器创建时间 | < 30 秒 |
|
||
| NFR-2 | VNC 桌面访问延迟 | < 500ms |
|
||
| NFR-3 | 单容器并发 Agent 数 | ≥ 3 个 |
|
||
| NFR-4 | 内存占用(单容器) | < 4GB(可配置) |
|
||
| NFR-5 | CPU 占用(单容器) | < 2 核(可配置) |
|
||
| NFR-6 | 系统可用性 | 99.5% |
|
||
|
||
### 2.3 约束条件
|
||
|
||
1. **架构约束**:
|
||
- 复用现有 `agent_runner` 模块,通过 `ServiceType::ComputerAgentRunner` 区分
|
||
- 保持与现有 `ServiceType::RCoder` 的兼容性,互不影响
|
||
|
||
2. **技术约束**:
|
||
- ACP 协议连接不是 `Send` trait,必须在 `LocalSet` 中运行
|
||
- Docker 镜像基于 Debian 12,包含完整桌面环境(2GB+)
|
||
- gRPC 通信端口 50051 不可修改(与 `shared_types` 保持一致)
|
||
|
||
3. **安全约束**:
|
||
- `user_id` 必须经过身份验证(后续实现)
|
||
- 容器间网络隔离
|
||
- VNC 访问需要与 `user_id` 绑定
|
||
|
||
---
|
||
|
||
## 三、系统架构设计
|
||
|
||
### 3.1 整体架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 外部客户端 (HTTP/SSE) │
|
||
└────────────────────────┬────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ RCoder 主服务 (HTTP API Server) │
|
||
│ - Axum 路由: /computer/chat, /computer/agent/stop │
|
||
│ - Pingora 代理: /computer/desktop/{user_id}/{project_id} │
|
||
│ - 状态管理: AppState (DashMap) │
|
||
└────────────────┬────────────────────────────────────────────┘
|
||
│
|
||
│ gRPC (Chat, StopAgent)
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Docker 容器: computer-agent-runner-{user_id} │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ agent_runner gRPC Server (50051) │ │
|
||
│ │ - ProjectAgentManager │ │
|
||
│ │ - DashMap<project_id, AgentInstance> │ │
|
||
│ └──────────────┬──────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Agent Instance 1 (project_id_1) │ │
|
||
│ │ - ACP Agent (claude-code-acp) │ │
|
||
│ │ - Chrome DevTools MCP │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Agent Instance 2 (project_id_2) │ │
|
||
│ │ - ACP Agent (claude-code-acp) │ │
|
||
│ │ - Chrome DevTools MCP │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Desktop 环境 (XFCE4 + noVNC) │ │
|
||
│ │ - Xvfb (:0) │ │
|
||
│ │ - x11vnc (5900) → noVNC (6080) │ │
|
||
│ │ - Chromium (CDP 9222) │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 工作区: /app/computer-project-workspace/{user_id}/ │
|
||
│ ├── project_id_1/ │
|
||
│ ├── project_id_2/ │
|
||
│ └── ... │
|
||
└───────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 容器管理模式对比
|
||
|
||
| 维度 | RCoder (现有) | Computer Agent Runner (新) |
|
||
|------|--------------|---------------------------|
|
||
| 容器标识 | `project_id` | `user_id` |
|
||
| 容器命名 | `rcoder-agent-{project_id}` | `computer-agent-runner-{user_id}` |
|
||
| Agent 实例数 | 1 个 | 多个(按 `project_id` 区分) |
|
||
| 工作目录 | `/app/project_workspace/{project_id}` | `/app/computer-project-workspace/{user_id}` |
|
||
| 桌面环境 | 无 | XFCE4 + noVNC |
|
||
| 浏览器 | 无 | Chromium + CDP |
|
||
| 闲置策略 | project_id 闲置即销毁 | user_id 下所有 project_id 都闲置才销毁 |
|
||
|
||
### 3.3 数据流转
|
||
|
||
#### 3.3.1 聊天请求流程
|
||
|
||
```
|
||
1. 客户端 → POST /computer/chat
|
||
{
|
||
"user_id": "user_123",
|
||
"project_id": "proj_456", // 可选
|
||
"prompt": "帮我爬取网页数据"
|
||
}
|
||
|
||
2. handle_computer_chat()
|
||
├─ 生成 project_id(若未提供)
|
||
├─ get_or_create_container_for_user(user_id)
|
||
│ ├─ 检查 containers[ContainerKey::User(user_id)] 是否已存在
|
||
│ ├─ 若不存在,调用 DockerManager 创建容器
|
||
│ └─ 返回 ContainerBasicInfo
|
||
├─ 更新 UnifiedContainerInfo
|
||
│ ├─ 创建/更新 ProjectInfo
|
||
│ └─ 添加到 container_info.projects
|
||
└─ gRPC Chat RPC → agent_runner
|
||
|
||
3. agent_runner (gRPC Server)
|
||
├─ ProjectAgentManager.get_or_create_agent(project_id)
|
||
│ ├─ 检查 agents: DashMap<project_id, AgentInstance>
|
||
│ ├─ 若不存在,在 LocalSet 中 spawn_local 新 Agent
|
||
│ └─ 返回 AgentInstance
|
||
├─ 调用 agent.handle_chat(prompt)
|
||
└─ 返回 GrpcChatResponse
|
||
|
||
4. handle_computer_chat()
|
||
├─ 更新会话映射: sessions[session_id] = SessionInfo
|
||
└─ 返回 ChatResponse 给客户端
|
||
|
||
5. 客户端 → GET /computer/progress/{session_id}
|
||
├─ 通过 sessions 查找 SessionInfo(包含 ContainerKey)
|
||
├─ 建立 SSE 连接到容器
|
||
└─ 实时推送进度事件
|
||
```
|
||
|
||
#### 3.3.2 VNC 桌面访问流程
|
||
|
||
```
|
||
1. 客户端 → GET /computer/desktop/{user_id}/{project_id}
|
||
|
||
2. computer_desktop_vnc()
|
||
├─ 查找 containers[ContainerKey::User(user_id)]
|
||
├─ 获取 container_ip
|
||
└─ 构建目标 URL: ws://{container_ip}:6080
|
||
|
||
3. Pingora WebSocket 代理 (或 Nginx)
|
||
├─ 接收客户端 HTTP Upgrade 请求
|
||
├─ 升级为 WebSocket 连接
|
||
├─ 透明转发到容器的 6080 端口
|
||
└─ 双向传输 WebSocket 帧
|
||
|
||
4. 容器内 noVNC (6080)
|
||
├─ 接收 WebSocket 连接
|
||
├─ 转发到 x11vnc (5900)
|
||
└─ 返回桌面画面给客户端
|
||
|
||
5. 客户端浏览器显示远程桌面
|
||
```
|
||
|
||
#### 3.3.3 Agent 停止流程
|
||
|
||
```
|
||
1. 客户端 → POST /computer/agent/stop
|
||
{
|
||
"user_id": "user_123",
|
||
"project_id": "proj_456"
|
||
}
|
||
|
||
2. computer_agent_stop()
|
||
├─ 查找 containers[ContainerKey::User(user_id)]
|
||
├─ 从 container_info.projects 移除 project_id
|
||
├─ gRPC StopAgent RPC → agent_runner
|
||
│ └─ ProjectAgentManager.stop_agent(project_id)
|
||
│ ├─ 从 agents: DashMap 移除
|
||
│ ├─ 取消 Agent 的 spawn_local 任务
|
||
│ └─ 清理 Agent 资源
|
||
├─ 清理会话映射
|
||
└─ 返回成功响应
|
||
|
||
注意:容器不会被销毁,继续运行其他 project_id
|
||
```
|
||
|
||
---
|
||
|
||
## 四、核心模块设计
|
||
|
||
### 4.1 数据模型
|
||
|
||
**设计目标**:使用统一的数据结构管理 RCoder 和 ComputerAgentRunner 两种模式的容器,避免维护多套独立的映射和结构。
|
||
|
||
**文件位置**: `crates/shared_types/src/model/computer_agent_model.rs`
|
||
|
||
#### 4.1.1 统一容器标识符(ContainerKey)
|
||
|
||
**设计目标**:使用统一的标识符来区分不同模式的容器,避免维护多套独立的映射。
|
||
|
||
```rust
|
||
/// 统一的容器标识符
|
||
/// 用于区分 RCoder 和 ComputerAgentRunner 两种模式的容器
|
||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||
pub enum ContainerKey {
|
||
/// RCoder 模式:一个 project_id 对应一个容器
|
||
Project(String),
|
||
|
||
/// ComputerAgentRunner 模式:一个 user_id 对应一个容器
|
||
User(String),
|
||
}
|
||
|
||
impl ContainerKey {
|
||
/// 获取容器标识符的字符串形式(用于 Docker 容器查询)
|
||
pub fn as_str(&self) -> &str {
|
||
match self {
|
||
ContainerKey::Project(id) => id,
|
||
ContainerKey::User(id) => id,
|
||
}
|
||
}
|
||
|
||
/// 获取 ServiceType
|
||
pub fn service_type(&self) -> ServiceType {
|
||
match self {
|
||
ContainerKey::Project(_) => ServiceType::RCoder,
|
||
ContainerKey::User(_) => ServiceType::ComputerAgentRunner,
|
||
}
|
||
}
|
||
|
||
/// 从 project_id 创建
|
||
pub fn from_project(project_id: String) -> Self {
|
||
ContainerKey::Project(project_id)
|
||
}
|
||
|
||
/// 从 user_id 创建
|
||
pub fn from_user(user_id: String) -> Self {
|
||
ContainerKey::User(user_id)
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for ContainerKey {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
ContainerKey::Project(id) => write!(f, "project:{}", id),
|
||
ContainerKey::User(id) => write!(f, "user:{}", id),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.2 统一容器信息结构(UnifiedContainerInfo)
|
||
|
||
**设计目标**:合并 `UserContainerInfo` 和 `ProjectAndContainerInfo`,使用一个统一的结构来表示所有容器信息。
|
||
|
||
```rust
|
||
/// 统一的容器信息结构
|
||
/// 同时支持 RCoder 和 ComputerAgentRunner 两种模式
|
||
#[derive(Debug, Clone)]
|
||
pub struct UnifiedContainerInfo {
|
||
/// 容器标识符(区分模式)
|
||
pub key: ContainerKey,
|
||
|
||
/// 容器基本信息
|
||
pub container: ContainerBasicInfo,
|
||
|
||
/// 服务类型
|
||
pub service_type: ServiceType,
|
||
|
||
/// 容器创建时间
|
||
pub created_at: DateTime<Utc>,
|
||
|
||
/// 最后活动时间(容器级别)
|
||
pub last_activity: DateTime<Utc>,
|
||
|
||
// ========== RCoder 模式字段 ==========
|
||
|
||
/// RCoder 模式:当前会话 ID
|
||
pub session_id: Option<String>,
|
||
|
||
/// RCoder 模式:Agent 状态
|
||
pub status: Option<AgentStatus>,
|
||
|
||
/// RCoder 模式:模型配置
|
||
pub model_provider: Option<ModelProviderConfig>,
|
||
|
||
// ========== ComputerAgentRunner 模式字段 ==========
|
||
|
||
/// ComputerAgentRunner 模式:容器内的所有项目映射
|
||
/// key: project_id, value: ProjectInfo
|
||
pub projects: Option<Arc<DashMap<String, Arc<ProjectInfo>>>>,
|
||
}
|
||
|
||
impl UnifiedContainerInfo {
|
||
/// 创建 RCoder 模式的容器信息
|
||
pub fn new_rcoder(project_id: String, container: ContainerBasicInfo) -> Self {
|
||
let now = Utc::now();
|
||
Self {
|
||
key: ContainerKey::Project(project_id),
|
||
container,
|
||
service_type: ServiceType::RCoder,
|
||
created_at: now,
|
||
last_activity: now,
|
||
session_id: None,
|
||
status: None,
|
||
model_provider: None,
|
||
projects: None,
|
||
}
|
||
}
|
||
|
||
/// 创建 ComputerAgentRunner 模式的容器信息
|
||
pub fn new_computer(user_id: String, container: ContainerBasicInfo) -> Self {
|
||
let now = Utc::now();
|
||
Self {
|
||
key: ContainerKey::User(user_id),
|
||
container,
|
||
service_type: ServiceType::ComputerAgentRunner,
|
||
created_at: now,
|
||
last_activity: now,
|
||
session_id: None,
|
||
status: None,
|
||
model_provider: None,
|
||
projects: Some(Arc::new(DashMap::new())),
|
||
}
|
||
}
|
||
|
||
/// 更新活动时间
|
||
pub fn update_activity(&mut self) {
|
||
self.last_activity = Utc::now();
|
||
}
|
||
|
||
/// 获取或创建 projects 映射(仅 ComputerAgentRunner 模式)
|
||
fn ensure_projects(&self) -> Arc<DashMap<String, Arc<ProjectInfo>>> {
|
||
self.projects.clone().unwrap_or_else(|| Arc::new(DashMap::new()))
|
||
}
|
||
|
||
// ========== ComputerAgentRunner 专用方法 ==========
|
||
|
||
/// 添加或更新项目(仅 ComputerAgentRunner 模式)
|
||
pub fn upsert_project(&self, project_id: String, project_info: Arc<ProjectInfo>) {
|
||
if let Some(projects) = &self.projects {
|
||
projects.insert(project_id, project_info);
|
||
}
|
||
}
|
||
|
||
/// 获取项目(仅 ComputerAgentRunner 模式)
|
||
pub fn get_project(&self, project_id: &str) -> Option<Arc<ProjectInfo>> {
|
||
self.projects.as_ref()?.get(project_id).map(|r| r.clone())
|
||
}
|
||
|
||
/// 移除项目(仅 ComputerAgentRunner 模式)
|
||
pub fn remove_project(&self, project_id: &str) -> Option<Arc<ProjectInfo>> {
|
||
self.projects.as_ref()?.remove(project_id).map(|(_, v)| v)
|
||
}
|
||
|
||
/// 列出所有项目 ID(仅 ComputerAgentRunner 模式)
|
||
pub fn list_projects(&self) -> Vec<String> {
|
||
self.projects
|
||
.as_ref()
|
||
.map(|p| p.iter().map(|r| r.key().clone()).collect())
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// 检查容器是否完全闲置
|
||
/// - RCoder 模式:检查 status 是否为 Idle 且超时
|
||
/// - ComputerAgentRunner 模式:检查所有项目是否都闲置且超时
|
||
pub fn is_fully_idle(&self, idle_timeout: Duration) -> bool {
|
||
let now = Utc::now();
|
||
let idle_duration = now - self.last_activity;
|
||
let is_timeout = idle_duration > chrono::Duration::from_std(idle_timeout).unwrap_or_default();
|
||
|
||
match self.service_type {
|
||
ServiceType::RCoder => {
|
||
// RCoder 模式:检查自身状态
|
||
let is_idle_status = matches!(self.status, Some(AgentStatus::Idle) | None);
|
||
is_idle_status && is_timeout
|
||
}
|
||
ServiceType::ComputerAgentRunner => {
|
||
// ComputerAgentRunner 模式:检查所有项目
|
||
if let Some(projects) = &self.projects {
|
||
if projects.is_empty() {
|
||
return true; // 没有项目,可以清理
|
||
}
|
||
|
||
// 所有项目都必须闲置
|
||
projects.iter().all(|entry| {
|
||
let project_info = entry.value();
|
||
let project_idle_duration = now - project_info.last_activity;
|
||
let project_is_timeout = project_idle_duration >
|
||
chrono::Duration::from_std(idle_timeout).unwrap_or_default();
|
||
|
||
let is_idle_status = matches!(
|
||
project_info.status,
|
||
Some(AgentStatus::Idle) | None
|
||
);
|
||
|
||
is_idle_status && project_is_timeout
|
||
})
|
||
} else {
|
||
true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 获取容器 IP
|
||
pub fn container_ip(&self) -> &str {
|
||
&self.container.container_ip
|
||
}
|
||
}
|
||
|
||
/// 项目信息(用于 ComputerAgentRunner 模式)
|
||
/// 简化版的项目元数据,不包含容器信息
|
||
#[derive(Debug, Clone)]
|
||
pub struct ProjectInfo {
|
||
pub project_id: String,
|
||
pub session_id: Option<String>,
|
||
pub status: Option<AgentStatus>,
|
||
pub model_provider: Option<ModelProviderConfig>,
|
||
pub created_at: DateTime<Utc>,
|
||
pub last_activity: DateTime<Utc>,
|
||
}
|
||
|
||
impl ProjectInfo {
|
||
pub fn new(project_id: String) -> Self {
|
||
let now = Utc::now();
|
||
Self {
|
||
project_id,
|
||
session_id: None,
|
||
status: None,
|
||
model_provider: None,
|
||
created_at: now,
|
||
last_activity: now,
|
||
}
|
||
}
|
||
|
||
pub fn update_activity(&mut self) {
|
||
self.last_activity = Utc::now();
|
||
}
|
||
|
||
pub fn update_session(&mut self, session_id: String) {
|
||
self.session_id = Some(session_id);
|
||
self.update_activity();
|
||
}
|
||
|
||
pub fn update_status(&mut self, status: AgentStatus) {
|
||
self.status = Some(status);
|
||
self.update_activity();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.3 会话信息结构(SessionInfo)
|
||
|
||
```rust
|
||
/// 会话信息
|
||
/// 统一管理 RCoder 和 ComputerAgentRunner 的会话
|
||
#[derive(Debug, Clone)]
|
||
pub struct SessionInfo {
|
||
pub session_id: String,
|
||
pub container_key: ContainerKey,
|
||
pub project_id: String, // ComputerAgentRunner 模式下的 project_id
|
||
pub created_at: DateTime<Utc>,
|
||
}
|
||
|
||
impl SessionInfo {
|
||
pub fn new(session_id: String, container_key: ContainerKey, project_id: String) -> Self {
|
||
Self {
|
||
session_id,
|
||
container_key,
|
||
project_id,
|
||
created_at: Utc::now(),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.4 简化后的 AppState
|
||
|
||
**文件**: `crates/rcoder/src/router.rs`
|
||
|
||
```rust
|
||
/// 应用状态(统一架构)
|
||
#[derive(Clone)]
|
||
pub struct AppState {
|
||
/// 应用配置
|
||
pub config: AppConfig,
|
||
|
||
// ========== 核心映射(统一管理) ==========
|
||
|
||
/// 统一的容器映射
|
||
/// key: ContainerKey (Project/User), value: UnifiedContainerInfo
|
||
///
|
||
/// 示例:
|
||
/// - ContainerKey::Project("proj_123") -> RCoder 容器
|
||
/// - ContainerKey::User("user_456") -> ComputerAgentRunner 容器
|
||
pub containers: DashMap<ContainerKey, Arc<UnifiedContainerInfo>>,
|
||
|
||
/// 会话映射
|
||
/// key: session_id, value: SessionInfo (包含 ContainerKey 和 project_id)
|
||
///
|
||
/// 用途:
|
||
/// - 通过 session_id 快速查找对应的容器和项目
|
||
/// - SSE 进度流使用此映射定位容器
|
||
pub sessions: DashMap<String, Arc<SessionInfo>>,
|
||
|
||
// ========== 索引映射(加速查询) ==========
|
||
|
||
/// 项目到容器的索引
|
||
/// key: project_id, value: ContainerKey
|
||
///
|
||
/// 用途:
|
||
/// - ComputerAgentRunner 模式:通过 project_id 查找所属的 user 容器
|
||
/// - RCoder 模式:直接映射到 ContainerKey::Project
|
||
pub project_to_container: DashMap<String, ContainerKey>,
|
||
|
||
// ========== 共享组件 ==========
|
||
|
||
/// Pingora 代理服务引用(用于读取指标)
|
||
pub pingora_service: Option<Arc<pingora_proxy::PingoraProxyService>>,
|
||
|
||
/// gRPC 连接池(与 agent_runner 通信)
|
||
pub grpc_pool: Arc<crate::grpc::GrpcChannelPool>,
|
||
}
|
||
|
||
impl AppState {
|
||
pub fn new(config: AppConfig, grpc_pool: Arc<crate::grpc::GrpcChannelPool>) -> Self {
|
||
Self {
|
||
config,
|
||
containers: DashMap::new(),
|
||
sessions: DashMap::new(),
|
||
project_to_container: DashMap::new(),
|
||
pingora_service: None,
|
||
grpc_pool,
|
||
}
|
||
}
|
||
|
||
// ========== 便捷方法 ==========
|
||
|
||
/// 获取容器信息(通过 ContainerKey)
|
||
pub fn get_container(&self, key: &ContainerKey) -> Option<Arc<UnifiedContainerInfo>> {
|
||
self.containers.get(key).map(|r| r.clone())
|
||
}
|
||
|
||
/// 获取容器信息(通过 project_id)
|
||
pub fn get_container_by_project(&self, project_id: &str) -> Option<Arc<UnifiedContainerInfo>> {
|
||
let key = self.project_to_container.get(project_id)?;
|
||
self.containers.get(key.value()).map(|r| r.clone())
|
||
}
|
||
|
||
/// 获取会话信息
|
||
pub fn get_session(&self, session_id: &str) -> Option<Arc<SessionInfo>> {
|
||
self.sessions.get(session_id).map(|r| r.clone())
|
||
}
|
||
|
||
/// 添加或更新容器
|
||
pub fn upsert_container(&self, key: ContainerKey, info: Arc<UnifiedContainerInfo>) {
|
||
self.containers.insert(key, info);
|
||
}
|
||
|
||
/// 添加会话
|
||
pub fn add_session(&self, session_id: String, session_info: Arc<SessionInfo>) {
|
||
self.sessions.insert(session_id, session_info);
|
||
}
|
||
|
||
/// 移除容器(包括清理所有相关映射)
|
||
pub fn remove_container(&self, key: &ContainerKey) -> Option<Arc<UnifiedContainerInfo>> {
|
||
// 移除容器
|
||
let container = self.containers.remove(key).map(|(_, v)| v)?;
|
||
|
||
// 清理 project_to_container 索引
|
||
match key {
|
||
ContainerKey::Project(project_id) => {
|
||
self.project_to_container.remove(project_id);
|
||
}
|
||
ContainerKey::User(_) => {
|
||
// ComputerAgentRunner 模式:清理所有项目的索引
|
||
if let Some(projects) = &container.projects {
|
||
for entry in projects.iter() {
|
||
self.project_to_container.remove(entry.key());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理相关会话
|
||
let sessions_to_remove: Vec<String> = self.sessions
|
||
.iter()
|
||
.filter(|entry| &entry.value().container_key == key)
|
||
.map(|entry| entry.key().clone())
|
||
.collect();
|
||
|
||
for session_id in sessions_to_remove {
|
||
self.sessions.remove(&session_id);
|
||
}
|
||
|
||
Some(container)
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.5 对比:简化前后的映射数量
|
||
|
||
| 项目 | 简化前 | 简化后 | 减少 |
|
||
|------|-------|-------|------|
|
||
| 核心映射 | 6 个 DashMap | 3 个 DashMap | **-50%** |
|
||
| 数据结构 | 2 个独立结构 | 1 个统一结构 | **统一** |
|
||
| 维护成本 | 高(独立逻辑) | 低(共享逻辑) | **显著降低** |
|
||
|
||
**简化收益**:
|
||
1. ✅ **统一标识符**:`ContainerKey` 统一管理不同模式的容器
|
||
2. ✅ **统一数据结构**:`UnifiedContainerInfo` 合并两种模式的信息
|
||
3. ✅ **减少映射数量**:从 6 个减少到 3 个
|
||
4. ✅ **简化查询逻辑**:通过 `ContainerKey` 直接查询,无需判断模式
|
||
5. ✅ **便于扩展**:未来添加新模式只需扩展 `ContainerKey` 枚举
|
||
6. ✅ **降低维护成本**:清理、查询、更新逻辑统一处理
|
||
|
||
### 4.2 容器管理模块
|
||
|
||
#### 4.2.1 ComputerContainerManager
|
||
|
||
**文件**: `crates/rcoder/src/service/computer_container_manager.rs`
|
||
|
||
```rust
|
||
use crate::error::AppError;
|
||
use docker_manager::{self, DockerContainerInfo, ContainerBasicInfo};
|
||
use shared_types::{ServiceType, ServiceResourceLimits};
|
||
use std::path::PathBuf;
|
||
use tracing::{info, error};
|
||
|
||
/// Computer Agent Runner 容器管理器
|
||
pub struct ComputerContainerManager;
|
||
|
||
impl ComputerContainerManager {
|
||
/// 根据 user_id 获取或创建容器
|
||
///
|
||
/// 容器命名: computer-agent-runner-{user_id}
|
||
/// 工作区: /app/computer-project-workspace/{user_id}
|
||
pub async fn get_or_create_container_for_user(
|
||
user_id: &str,
|
||
resource_limits: Option<ServiceResourceLimits>,
|
||
) -> Result<ContainerBasicInfo, AppError> {
|
||
info!("🔍 [COMPUTER] 获取/创建用户容器: user_id={}", user_id);
|
||
|
||
let docker_manager = docker_manager::global::get_global_docker_manager()
|
||
.await
|
||
.map_err(|e| AppError::internal_server_error(
|
||
&format!("获取 DockerManager 失败: {}", e)
|
||
))?;
|
||
|
||
// 使用 user_id 作为 project_id 来查询容器
|
||
// 因为 ComputerAgentRunner 容器是按 user_id 创建的
|
||
if let Ok(Some(info)) = docker_manager.get_agent_info(user_id).await {
|
||
info!("✅ [COMPUTER] 用户容器已存在: container_id={}", info.container_id);
|
||
return Ok(info);
|
||
}
|
||
|
||
// 创建新容器
|
||
info!("🏗️ [COMPUTER] 创建新用户容器: user_id={}", user_id);
|
||
Self::create_container_for_user(user_id, &docker_manager, resource_limits).await
|
||
}
|
||
|
||
/// 为用户创建容器
|
||
async fn create_container_for_user(
|
||
user_id: &str,
|
||
docker_manager: &std::sync::Arc<docker_manager::DockerManager>,
|
||
resource_limits: Option<ServiceResourceLimits>,
|
||
) -> Result<ContainerBasicInfo, AppError> {
|
||
// 1. 准备用户级工作目录
|
||
let user_workspace = Self::get_user_workspace(user_id).await?;
|
||
Self::create_user_workspace(user_id).await?;
|
||
|
||
// 2. 解析宿主机路径
|
||
let host_path = crate::utils::resolve_container_path_to_host(&user_workspace)
|
||
.await
|
||
.map_err(|e| AppError::internal_server_error(
|
||
&format!("路径解析失败: {}", e)
|
||
))?;
|
||
|
||
info!(
|
||
"📁 [COMPUTER] 用户工作区路径映射: 容器内={:?}, 宿主机={:?}",
|
||
user_workspace, host_path
|
||
);
|
||
|
||
// 3. 调用 DockerManager 启动容器
|
||
// 注意: 使用 user_id 作为 project_id 传递给 Docker Manager
|
||
let container_info = docker_manager
|
||
.start_agent_container(
|
||
user_id, // 使用 user_id 作为容器标识
|
||
&host_path.to_string_lossy(),
|
||
ServiceType::ComputerAgentRunner,
|
||
resource_limits,
|
||
)
|
||
.await
|
||
.map_err(|e| AppError::internal_server_error(
|
||
&format!("启动容器失败: {}", e)
|
||
))?;
|
||
|
||
info!(
|
||
"🚀 [COMPUTER] 用户容器创建成功: container_id={}",
|
||
container_info.container_id
|
||
);
|
||
|
||
Ok(container_info)
|
||
}
|
||
|
||
/// 获取用户工作区路径
|
||
///
|
||
/// 格式: /app/computer-project-workspace/{user_id}
|
||
/// 注意:project_id 作为子目录由 agent 自己管理
|
||
pub async fn get_user_workspace(user_id: &str) -> Result<PathBuf, AppError> {
|
||
let workspace_dir = PathBuf::from("/app/computer-project-workspace");
|
||
let user_dir = workspace_dir.join(user_id);
|
||
Ok(user_dir)
|
||
}
|
||
|
||
/// 创建用户工作区目录
|
||
async fn create_user_workspace(user_id: &str) -> Result<PathBuf, AppError> {
|
||
let workspace_dir = PathBuf::from("/app/computer-project-workspace");
|
||
|
||
// 确保根目录存在
|
||
tokio::fs::create_dir_all(&workspace_dir).await
|
||
.map_err(|e| AppError::internal_server_error(
|
||
&format!("创建 workspace 目录失败: {}", e)
|
||
))?;
|
||
|
||
// 创建用户目录
|
||
let user_dir = workspace_dir.join(user_id);
|
||
tokio::fs::create_dir_all(&user_dir).await
|
||
.map_err(|e| AppError::internal_server_error(
|
||
&format!("创建用户目录失败: {}", e)
|
||
))?;
|
||
|
||
Ok(user_dir)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 HTTP 接口模块
|
||
|
||
#### 4.3.1 ComputerChatRequest 结构
|
||
|
||
**文件**: `crates/rcoder/src/handler/computer_chat_handler.rs`
|
||
|
||
```rust
|
||
use serde::{Deserialize, Serialize};
|
||
use utoipa::ToSchema;
|
||
use shared_types::*;
|
||
|
||
/// Computer Agent 聊天请求
|
||
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
|
||
pub struct ComputerChatRequest {
|
||
/// 用户 ID (必填) - 一个用户对应一个容器
|
||
#[schema(example = "user_123")]
|
||
pub user_id: String,
|
||
|
||
/// 项目 ID (可选) - 一个容器内可以有多个项目
|
||
/// 若未提供,系统自动生成 UUID
|
||
#[schema(example = "proj_456")]
|
||
pub project_id: Option<String>,
|
||
|
||
/// 用户输入的 prompt
|
||
#[schema(example = "帮我创建一个 React 应用")]
|
||
pub prompt: String,
|
||
|
||
// ========== 以下字段与现有 ChatRequest 保持一致 ==========
|
||
|
||
/// 会话 ID (可选) - 用于续传会话
|
||
pub session_id: Option<String>,
|
||
|
||
/// 多媒体附件
|
||
#[serde(default)]
|
||
pub attachments: Vec<Attachment>,
|
||
|
||
/// 数据源附件
|
||
#[serde(default)]
|
||
pub data_source_attachments: Vec<String>,
|
||
|
||
/// 模型配置
|
||
pub model_provider: Option<ModelProviderConfig>,
|
||
|
||
/// 请求 ID
|
||
pub request_id: Option<String>,
|
||
|
||
/// 系统提示词覆盖
|
||
pub system_prompt: Option<String>,
|
||
|
||
/// 用户提示词模板
|
||
pub user_prompt: Option<String>,
|
||
|
||
/// Agent 运行时配置
|
||
pub agent_config: Option<ChatAgentConfig>,
|
||
}
|
||
|
||
/// Computer Agent 停止请求
|
||
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
|
||
pub struct ComputerAgentStopRequest {
|
||
/// 用户 ID (必填)
|
||
pub user_id: String,
|
||
|
||
/// 项目 ID (必填) - 只停止特定项目的 agent
|
||
pub project_id: String,
|
||
|
||
/// 可选的会话 ID
|
||
pub session_id: Option<String>,
|
||
}
|
||
```
|
||
|
||
#### 4.3.2 handler 实现(关键代码)
|
||
|
||
```rust
|
||
/// 处理 Computer Agent 聊天请求(使用统一架构)
|
||
pub async fn handle_computer_chat(
|
||
State(state): State<Arc<AppState>>,
|
||
Json(mut request): Json<ComputerChatRequest>,
|
||
) -> Result<HttpResult<ChatResponse>, AppError> {
|
||
let user_id = request.user_id.clone();
|
||
|
||
// 生成或使用提供的 project_id
|
||
let project_id = match &request.project_id {
|
||
Some(id) => id.clone(),
|
||
None => {
|
||
let project_id = generate_project_id();
|
||
request.project_id = Some(project_id.clone());
|
||
project_id
|
||
}
|
||
};
|
||
|
||
info!(
|
||
"🚀 [COMPUTER_CHAT] user_id={}, project_id={}, prompt_len={}",
|
||
user_id, project_id, request.prompt.len()
|
||
);
|
||
|
||
// 步骤 1: 获取或创建用户容器
|
||
let container_basic_info = ComputerContainerManager::get_or_create_container_for_user(
|
||
&user_id,
|
||
request.agent_config.as_ref().and_then(|c| c.resource_limits.clone()),
|
||
).await?;
|
||
|
||
// 步骤 2: 获取或创建统一容器信息
|
||
let container_key = ContainerKey::from_user(user_id.clone());
|
||
let container_info = {
|
||
let entry = state.containers.entry(container_key.clone());
|
||
match entry {
|
||
dashmap::mapref::entry::Entry::Occupied(occupied) => {
|
||
occupied.get().clone()
|
||
}
|
||
dashmap::mapref::entry::Entry::Vacant(vacant) => {
|
||
let new_info = Arc::new(UnifiedContainerInfo::new_computer(
|
||
user_id.clone(),
|
||
container_basic_info.clone(),
|
||
));
|
||
vacant.insert(new_info.clone());
|
||
new_info
|
||
}
|
||
}
|
||
};
|
||
|
||
// 步骤 3: 创建或更新项目信息
|
||
let project_info = {
|
||
if let Some(existing) = container_info.get_project(&project_id) {
|
||
// 更新现有项目
|
||
let mut updated = (**existing).clone();
|
||
updated.update_activity();
|
||
if let Some(model) = request.model_provider.clone() {
|
||
updated.model_provider = Some(model);
|
||
}
|
||
Arc::new(updated)
|
||
} else {
|
||
// 创建新项目
|
||
let mut new_project = ProjectInfo::new(project_id.clone());
|
||
new_project.model_provider = request.model_provider.clone();
|
||
Arc::new(new_project)
|
||
}
|
||
};
|
||
|
||
// 将项目添加到容器
|
||
container_info.upsert_project(project_id.clone(), project_info.clone());
|
||
|
||
// 步骤 4: 建立项目到容器的索引
|
||
state.project_to_container.insert(
|
||
project_id.clone(),
|
||
container_key.clone(),
|
||
);
|
||
|
||
// 步骤 5: 转发请求到容器 (仅使用 gRPC)
|
||
let result = forward_computer_request_to_container(
|
||
&request,
|
||
&container_basic_info,
|
||
&state.grpc_pool,
|
||
).await?;
|
||
|
||
// 步骤 6: 更新会话映射
|
||
if let Some(chat_response) = &result.data {
|
||
let session_id = chat_response.session_id.clone();
|
||
|
||
// 更新项目的 session_id
|
||
if let Some(project) = container_info.get_project(&project_id) {
|
||
let mut updated = (**project).clone();
|
||
updated.update_session(session_id.clone());
|
||
container_info.upsert_project(project_id.clone(), Arc::new(updated));
|
||
}
|
||
|
||
// 建立会话映射
|
||
let session_info = Arc::new(SessionInfo::new(
|
||
session_id.clone(),
|
||
container_key,
|
||
project_id.clone(),
|
||
));
|
||
state.add_session(session_id.clone(), session_info);
|
||
|
||
info!(
|
||
"✅ [COMPUTER_CHAT] 会话建立: session_id={}, user_id={}, project_id={}",
|
||
session_id, user_id, project_id
|
||
);
|
||
}
|
||
|
||
Ok(result)
|
||
}
|
||
```
|
||
|
||
### 4.4 agent_runner 复用 agent_abstraction 模块
|
||
|
||
#### 4.4.1 设计原则
|
||
|
||
**不要重复造轮!** agent_runner 应该直接复用现有的 `crates/agent_abstraction` 模块,该模块已经提供了完整的多 Agent 实例管理功能。
|
||
|
||
#### 4.4.2 现有模块功能分析
|
||
|
||
**文件位置**: `crates/agent_abstraction/`
|
||
|
||
**核心组件**:
|
||
|
||
1. **AcpSessionManager** - 多会话管理器
|
||
```rust
|
||
pub struct AcpSessionManager<N: SessionNotifier, C: Client + 'static> {
|
||
/// project_id -> SessionInfo 映射(DashMap 并发安全)
|
||
sessions: DashMap<String, Arc<SessionInfo>>,
|
||
notifier: Arc<N>,
|
||
_client_marker: std::marker::PhantomData<C>,
|
||
}
|
||
```
|
||
|
||
**功能**:
|
||
- 基于 project_id 的会话管理和复用
|
||
- 自动检测模型配置变化并重建会话
|
||
- 线程安全的并发访问
|
||
- `get_or_create_session()` - 智能会话复用
|
||
- `send_prompt_request()` - 发送请求到指定会话
|
||
- `list_sessions()` - 列出所有会话
|
||
|
||
2. **AgentLifecycleManager** - 生命周期管理器
|
||
```rust
|
||
pub struct AgentLifecycleManager {
|
||
/// 活跃的 agent 进程
|
||
processes: DashMap<String, Arc<ConnectedAgent>>,
|
||
/// Agent 状态信息
|
||
agent_status_map: DashMap<String, AgentStatusInfo>,
|
||
/// 保存的启动配置(用于重启)
|
||
launch_configs: DashMap<String, ProcessLaunchInfo>,
|
||
}
|
||
```
|
||
|
||
**功能**:
|
||
- 管理多个 Agent 进程(按 agent_id)
|
||
- 跟踪 Agent 状态(Idle/Active/Terminating)
|
||
- `spawn_agent()` - 启动新 Agent
|
||
- `stop_agent()` - 停止特定 Agent
|
||
- `restart_agent()` - 重启 Agent
|
||
- `set_agent_active/idle()` - 更新 Agent 状态
|
||
- `get_running_agents()` - 列出运行中的 Agent
|
||
|
||
3. **AcpAgentWorker** - Worker 模式处理器
|
||
```rust
|
||
pub struct AcpAgentWorker<N: SessionNotifier, C: Client + 'static> {
|
||
session_manager: Arc<AcpSessionManager<N, C>>,
|
||
}
|
||
|
||
impl<N, C> AgentWorker for AcpAgentWorker<N, C> {
|
||
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse>;
|
||
}
|
||
```
|
||
|
||
**功能**:
|
||
- 统一的请求处理接口
|
||
- 自动路径规范化
|
||
- 项目目录管理
|
||
- 配置组装和 Client 实例化
|
||
|
||
#### 4.4.3 集成方案
|
||
|
||
**文件**: `crates/agent_runner/src/main.rs`
|
||
|
||
```rust
|
||
use agent_abstraction::{
|
||
AcpSessionManager,
|
||
AgentLifecycleManager,
|
||
AcpAgentWorker,
|
||
SessionNotifier,
|
||
};
|
||
use shared_types::Client;
|
||
|
||
/// Agent Runner 状态
|
||
pub struct AgentRunnerState<N: SessionNotifier, C: Client + 'static> {
|
||
/// 会话管理器(复用 agent_abstraction)
|
||
session_manager: Arc<AcpSessionManager<N, C>>,
|
||
|
||
/// 生命周期管理器(复用 agent_abstraction)
|
||
lifecycle_manager: Arc<AgentLifecycleManager>,
|
||
|
||
/// Worker 处理器(复用 agent_abstraction)
|
||
worker: Arc<AcpAgentWorker<N, C>>,
|
||
}
|
||
|
||
impl<N: SessionNotifier, C: Client + 'static> AgentRunnerState<N, C> {
|
||
pub fn new(
|
||
notifier: Arc<N>,
|
||
lifecycle_manager: Arc<AgentLifecycleManager>,
|
||
) -> Self {
|
||
let session_manager = Arc::new(AcpSessionManager::new(notifier));
|
||
let worker = Arc::new(AcpAgentWorker::new(session_manager.clone()));
|
||
|
||
Self {
|
||
session_manager,
|
||
lifecycle_manager,
|
||
worker,
|
||
}
|
||
}
|
||
|
||
/// 处理聊天请求(自动路由到对应的 project_id)
|
||
pub async fn handle_chat(&self, request: WorkerRequest) -> Result<WorkerResponse> {
|
||
self.worker.process_request(request).await
|
||
}
|
||
|
||
/// 停止特定 project_id 的 agent
|
||
pub async fn stop_agent(&self, project_id: &str) -> Result<()> {
|
||
// 1. 从会话管理器移除会话
|
||
self.session_manager.remove_session(project_id);
|
||
|
||
// 2. 停止对应的 agent 进程
|
||
self.lifecycle_manager.stop_agent(project_id).await
|
||
}
|
||
|
||
/// 列出所有活跃的会话
|
||
pub fn list_sessions(&self) -> Vec<String> {
|
||
self.session_manager.list_sessions()
|
||
}
|
||
|
||
/// 获取 Agent 状态
|
||
pub fn get_agent_status(&self, project_id: &str) -> Option<AgentStatus> {
|
||
self.lifecycle_manager.get_agent_status(project_id)
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.4.4 gRPC 服务实现
|
||
|
||
**文件**: `crates/agent_runner/src/grpc/agent_service_impl.rs`
|
||
|
||
```rust
|
||
use tonic::{Request, Response, Status};
|
||
use agent_runner_state::AgentRunnerState;
|
||
|
||
pub struct AgentServiceImpl<N: SessionNotifier, C: Client + 'static> {
|
||
state: Arc<AgentRunnerState<N, C>>,
|
||
}
|
||
|
||
#[tonic::async_trait]
|
||
impl<N: SessionNotifier, C: Client + 'static> AgentService for AgentServiceImpl<N, C> {
|
||
async fn chat(
|
||
&self,
|
||
request: Request<GrpcChatRequest>,
|
||
) -> Result<Response<GrpcChatResponse>, Status> {
|
||
let req = request.into_inner();
|
||
|
||
// 构建 WorkerRequest
|
||
let worker_request = WorkerRequest {
|
||
prompt_message: /* 转换 */,
|
||
model_provider: req.model_provider,
|
||
attachment_blocks: /* 转换 */,
|
||
};
|
||
|
||
// 调用 worker 处理(自动路由到 project_id)
|
||
let response = self.state.handle_chat(worker_request)
|
||
.await
|
||
.map_err(|e| Status::internal(e.to_string()))?;
|
||
|
||
// 转换为 gRPC 响应
|
||
Ok(Response::new(GrpcChatResponse {
|
||
session_id: response.session_id,
|
||
project_id: response.project_id,
|
||
// ...
|
||
}))
|
||
}
|
||
|
||
async fn stop_agent(
|
||
&self,
|
||
request: Request<StopAgentRequest>,
|
||
) -> Result<Response<StopAgentResponse>, Status> {
|
||
let req = request.into_inner();
|
||
|
||
// 调用 state 停止 agent
|
||
self.state.stop_agent(&req.project_id)
|
||
.await
|
||
.map_err(|e| Status::internal(e.to_string()))?;
|
||
|
||
Ok(Response::new(StopAgentResponse {
|
||
success: true,
|
||
message: format!("Agent {} stopped", req.project_id),
|
||
}))
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.4.5 gRPC Proto 扩展
|
||
|
||
**文件**: `crates/shared_types/proto/agent.proto`
|
||
|
||
```protobuf
|
||
service AgentService {
|
||
// 现有方法
|
||
rpc Chat(GrpcChatRequest) returns (GrpcChatResponse);
|
||
rpc SubscribeProgress(ProgressRequest) returns (stream ProgressEvent);
|
||
rpc CancelSession(CancelRequest) returns (CancelResponse);
|
||
rpc GetStatus(StatusRequest) returns (StatusResponse);
|
||
|
||
// 新增:停止特定 project_id 的 agent
|
||
rpc StopAgent(StopAgentRequest) returns (StopAgentResponse);
|
||
}
|
||
|
||
message StopAgentRequest {
|
||
string project_id = 1;
|
||
}
|
||
|
||
message StopAgentResponse {
|
||
bool success = 1;
|
||
string message = 2;
|
||
string project_id = 3;
|
||
}
|
||
```
|
||
|
||
### 4.5 闲置检测和清理
|
||
|
||
#### 4.5.1 统一的清理逻辑
|
||
|
||
**文件**: `crates/rcoder/src/proxy_agent/cleanup_task.rs`
|
||
|
||
```rust
|
||
impl AgentCleaner {
|
||
/// 清理闲置 agent(统一处理 RCoder 和 ComputerAgentRunner)
|
||
async fn cleanup_idle_agents(&mut self) -> Result<CleanupStats> {
|
||
let mut stats = CleanupStats::default();
|
||
let current_time = Utc::now();
|
||
|
||
// 收集需要清理的容器
|
||
let mut containers_to_clean = Vec::new();
|
||
|
||
for entry in self.state.containers.iter() {
|
||
let container_key = entry.key();
|
||
let container_info = entry.value();
|
||
|
||
// 检查容器保护期(创建后 5 分钟内不清理)
|
||
let protection_time = chrono::Duration::minutes(5);
|
||
if current_time - container_info.created_at < protection_time {
|
||
info!(
|
||
"🛡️ [CLEANUP] 容器处于保护期: key={}",
|
||
container_key
|
||
);
|
||
continue;
|
||
}
|
||
|
||
// 使用统一的闲置判断方法
|
||
if container_info.is_fully_idle(self.config.idle_timeout) {
|
||
info!(
|
||
"🎯 [CLEANUP] 发现闲置容器: key={}, service_type={:?}",
|
||
container_key,
|
||
container_info.service_type
|
||
);
|
||
|
||
// 记录额外信息
|
||
match &container_info.service_type {
|
||
ServiceType::RCoder => {
|
||
info!(" - RCoder 模式,单项目容器");
|
||
}
|
||
ServiceType::ComputerAgentRunner => {
|
||
let project_count = container_info.list_projects().len();
|
||
info!(" - ComputerAgentRunner 模式,包含 {} 个项目", project_count);
|
||
}
|
||
}
|
||
|
||
containers_to_clean.push(container_key.clone());
|
||
}
|
||
}
|
||
|
||
// 执行清理
|
||
for container_key in containers_to_clean {
|
||
match self.cleanup_container(&container_key).await {
|
||
Ok(_) => {
|
||
stats.cleaned_count += 1;
|
||
info!("✅ [CLEANUP] 成功清理容器: {}", container_key);
|
||
}
|
||
Err(e) => {
|
||
stats.failed_count += 1;
|
||
warn!("❌ [CLEANUP] 清理失败: {} - {}", container_key, e);
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(stats)
|
||
}
|
||
|
||
/// 清理单个容器(统一处理)
|
||
async fn cleanup_container(&self, container_key: &ContainerKey) -> Result<()> {
|
||
info!("🔥 [CLEANUP] 开始清理容器: {}", container_key);
|
||
|
||
// 1. 获取容器信息
|
||
let container_info = self.state.containers.get(container_key)
|
||
.ok_or_else(|| anyhow::anyhow!("容器不存在"))?
|
||
.clone();
|
||
|
||
// 2. 获取 Docker Manager
|
||
let docker_manager = docker_manager::global::get_global_docker_manager().await?;
|
||
|
||
// 3. 销毁 Docker 容器
|
||
// 注意:使用 container_key.as_str() 作为标识符查询
|
||
if let Ok(Some(container_basic_info)) = docker_manager.get_agent_info(container_key.as_str()).await {
|
||
// 清理 gRPC 连接池
|
||
let grpc_addr = format!("{}:{}",
|
||
container_basic_info.container_ip,
|
||
shared_types::GRPC_DEFAULT_PORT
|
||
);
|
||
self.state.grpc_pool.remove(&grpc_addr);
|
||
|
||
// 执行物理销毁
|
||
docker_manager.stop_container_by_id(&container_basic_info.container_id).await?;
|
||
|
||
info!(
|
||
"✅ [CLEANUP] Docker 容器已销毁: container_id={}",
|
||
container_basic_info.container_id
|
||
);
|
||
}
|
||
|
||
// 4. 使用 AppState 的统一清理方法
|
||
self.state.remove_container(container_key);
|
||
|
||
info!("✅ [CLEANUP] 容器清理完成: {}", container_key);
|
||
Ok(())
|
||
}
|
||
|
||
/// 孤立容器检测(统一处理)
|
||
async fn cleanup_orphaned_containers(&mut self) -> u64 {
|
||
let mut cleaned_count = 0;
|
||
let docker_manager = match docker_manager::global::get_global_docker_manager().await {
|
||
Ok(dm) => dm,
|
||
Err(e) => {
|
||
error!("❌ [CLEANUP] 获取 DockerManager 失败: {}", e);
|
||
return 0;
|
||
}
|
||
};
|
||
|
||
// 收集所有应该存在的容器 ID
|
||
let expected_containers: HashSet<String> = self.state.containers
|
||
.iter()
|
||
.map(|entry| entry.value().container.container_id.clone())
|
||
.collect();
|
||
|
||
// 列出所有 Docker 容器(RCoder 和 ComputerAgentRunner)
|
||
let patterns = vec![
|
||
"rcoder-agent-",
|
||
"computer-agent-runner-",
|
||
];
|
||
|
||
for pattern in patterns {
|
||
if let Ok(containers) = docker_manager.list_containers_by_pattern(pattern).await {
|
||
for container in containers {
|
||
if !expected_containers.contains(&container.id) {
|
||
info!(
|
||
"🎯 [CLEANUP] 发现孤立容器: id={}, name={}",
|
||
container.id, container.names.join(",")
|
||
);
|
||
|
||
// 清理孤立容器
|
||
if let Err(e) = docker_manager.stop_container_by_id(&container.id).await {
|
||
warn!("❌ [CLEANUP] 清理孤立容器失败: {} - {}", container.id, e);
|
||
} else {
|
||
cleaned_count += 1;
|
||
info!("✅ [CLEANUP] 孤立容器已清理: {}", container.id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
cleaned_count
|
||
}
|
||
}
|
||
|
||
/// 清理统计
|
||
#[derive(Debug, Default)]
|
||
pub struct CleanupStats {
|
||
pub cleaned_count: u64,
|
||
pub failed_count: u64,
|
||
}
|
||
```
|
||
|
||
**关键改进**:
|
||
|
||
1. ✅ **统一清理逻辑**:不再区分 RCoder 和 ComputerAgentRunner 的清理流程
|
||
2. ✅ **使用 `is_fully_idle()`**:统一的闲置判断方法,自动根据 `ServiceType` 处理
|
||
3. ✅ **使用 `remove_container()`**:AppState 的统一清理方法,自动处理所有相关映射
|
||
4. ✅ **孤立容器检测**:支持检测两种模式的孤立容器
|
||
5. ✅ **简化代码**:从原来的独立清理方法合并为一个统一方法
|
||
|
||
---
|
||
|
||
## 五、接口设计
|
||
|
||
### 5.1 HTTP API
|
||
|
||
| 端点 | 方法 | 请求体 | 响应体 | 说明 |
|
||
|------|------|--------|--------|------|
|
||
| `/computer/chat` | POST | `ComputerChatRequest` | `HttpResult<ChatResponse>` | 发送聊天请求 |
|
||
| `/computer/agent/stop` | POST | `ComputerAgentStopRequest` | `HttpResult<StopAgentResponse>` | 停止 Agent |
|
||
| `/computer/progress/{session_id}` | GET | - | SSE Stream | 实时进度流 |
|
||
| `/computer/desktop/{user_id}/{project_id}` | GET | - | WebSocket Upgrade | VNC 桌面访问 |
|
||
|
||
### 5.2 gRPC API
|
||
|
||
| RPC 方法 | 请求 | 响应 | 说明 |
|
||
|---------|------|------|------|
|
||
| `Chat` | `GrpcChatRequest` | `GrpcChatResponse` | 聊天请求(已扩展支持 project_id 路由) |
|
||
| `StopAgent` | `StopAgentRequest` | `StopAgentResponse` | 停止特定 project_id 的 agent |
|
||
| `SubscribeProgress` | `ProgressRequest` | Stream `ProgressEvent` | 订阅进度事件 |
|
||
|
||
---
|
||
|
||
## 六、部署配置
|
||
|
||
### 6.1 Docker Compose 配置
|
||
|
||
**文件**: `docker/docker-compose.yml`(确认挂载)
|
||
|
||
```yaml
|
||
services:
|
||
rcoder:
|
||
image: ${RCODER_IMAGE:-master-rcoder:latest}
|
||
container_name: rcoder
|
||
volumes:
|
||
- /var/run/docker.sock:/var/run/docker.sock
|
||
- ./project_workspace:/app/project_workspace # RCoder 工作区
|
||
- ./computer-project-workspace:/app/computer-project-workspace # Computer Agent 工作区
|
||
- ./logs:/app/logs
|
||
ports:
|
||
- "8087:8087"
|
||
environment:
|
||
- RUST_LOG=info
|
||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||
```
|
||
|
||
### 6.2 Agent 配置文件
|
||
|
||
**文件**: `crates/agent_config/configs/computer_agent_default.json`
|
||
|
||
```json
|
||
{
|
||
"agent_servers": {
|
||
"claude-code-acp": {
|
||
"agent_id": "claude-code-acp",
|
||
"agent_type": "claude",
|
||
"command": "claude-code-acp",
|
||
"args": [],
|
||
"env": {
|
||
"ANTHROPIC_API_KEY": "{MODEL_PROVIDER_API_KEY}",
|
||
"ANTHROPIC_MODEL": "{MODEL_PROVIDER_DEFAULT_MODEL}",
|
||
"ANTHROPIC_BASE_URL": "{MODEL_PROVIDER_BASE_URL}",
|
||
"RUST_LOG": "info"
|
||
},
|
||
"system_prompt": {
|
||
"source": "embedded",
|
||
"template": "",
|
||
"enabled": true
|
||
},
|
||
"user_prompt": {
|
||
"template": "{user_prompt}",
|
||
"enabled": false
|
||
},
|
||
"installation": {
|
||
"package_manager": "npm",
|
||
"package_name": "@anthropics/claude-code-acp",
|
||
"version": "latest"
|
||
},
|
||
"enabled": true,
|
||
"metadata": {
|
||
"description": "Claude Code ACP Agent - Computer Agent Runner 配置",
|
||
"version": "1.0.0"
|
||
}
|
||
}
|
||
},
|
||
"context_servers": {
|
||
"chrome-devtools": {
|
||
"source": "custom",
|
||
"enabled": true,
|
||
"command": "npx",
|
||
"args": ["-y", "chrome-devtools-mcp@latest"],
|
||
"env": {
|
||
"CHROME_REMOTE_DEBUGGING_PORT": "9222"
|
||
},
|
||
"metadata": {
|
||
"description": "Chrome DevTools MCP - 浏览器操作能力",
|
||
"documentation": "https://github.com/ChromeDevTools/chrome-devtools-mcp"
|
||
}
|
||
},
|
||
"context7": {
|
||
"source": "custom",
|
||
"enabled": true,
|
||
"command": "bunx",
|
||
"args": ["-y", "@upstash/context7-mcp"],
|
||
"env": {}
|
||
},
|
||
"fetch": {
|
||
"source": "custom",
|
||
"enabled": true,
|
||
"command": "uvx",
|
||
"args": ["mcp-server-fetch"],
|
||
"env": {}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、实施计划
|
||
|
||
### 7.1 阶段划分
|
||
|
||
| 阶段 | 工作内容 | 工作量 | 优先级 | 依赖 |
|
||
|------|---------|--------|--------|------|
|
||
| 阶段 1 | 核心数据结构 | 1-2 天 | P0 | 无 |
|
||
| 阶段 2 | HTTP 接口实现 | 1 天 | P0 | 阶段 1 |
|
||
| 阶段 3 | agent_runner 多实例支持 | 2-3 天 | P0 | 阶段 1 |
|
||
| 阶段 4 | 闲置检测和清理 | 1 天 | P1 | 阶段 1, 2 |
|
||
| 阶段 5 | VNC 代理实现 | 1-2 天 | P1 | 阶段 2 |
|
||
| 阶段 6 | MCP 配置和优化 | 0.5 天 | P2 | 阶段 3 |
|
||
| **总计** | | **6.5-9.5 天** | | |
|
||
|
||
### 7.2 关键里程碑
|
||
|
||
- ✅ **M1**: 完成核心数据结构定义(阶段 1)
|
||
- ✅ **M2**: HTTP `/computer/chat` 接口可用(阶段 2)
|
||
- ✅ **M3**: agent_runner 支持多 Agent 实例(阶段 3,关键里程碑)
|
||
- ✅ **M4**: 闲置检测和清理正常运行(阶段 4)
|
||
- ✅ **M5**: VNC 桌面可访问(阶段 5)
|
||
- ✅ **M6**: 完整功能上线(阶段 6)
|
||
|
||
### 7.3 验收标准
|
||
|
||
#### 功能验收
|
||
- [ ] 可以通过 `POST /computer/chat` 发送请求,自动创建 user_id 对应的容器
|
||
- [ ] 同一 user_id 的多个 project_id 可以在同一容器内运行
|
||
- [ ] 可以通过 `GET /computer/desktop/{user_id}/{project_id}` 访问 VNC 桌面
|
||
- [ ] Agent 可以通过 Chrome DevTools MCP 操作 Chromium 浏览器
|
||
- [ ] 可以通过 `POST /computer/agent/stop` 停止单个 project_id 的 agent(不销毁容器)
|
||
- [ ] 只有当 user_id 下所有 project_id 都闲置时才销毁容器
|
||
|
||
#### 性能验收
|
||
- [ ] 单个容器可以稳定运行 3+ 个 project_id 的 agent
|
||
- [ ] VNC 桌面访问延迟 < 500ms
|
||
- [ ] 容器创建时间 < 30s
|
||
|
||
#### 安全验收
|
||
- [ ] user_id 只能访问自己的容器和 VNC
|
||
- [ ] project_id 之间没有上下文污染
|
||
- [ ] 容器资源限额生效
|
||
|
||
---
|
||
|
||
## 八、风险和挑战
|
||
|
||
### 8.1 技术风险
|
||
|
||
| 风险 | 影响 | 概率 | 缓解措施 | 责任人 |
|
||
|------|------|------|----------|--------|
|
||
| agent_runner LocalSet 并发管理复杂 | 高 | 高 | 参考现有 ACP agent worker 模式,使用 `spawn_local` 隔离 | 开发 |
|
||
| Pingora 不支持 WebSocket | 中 | 中 | 备用方案:使用 Nginx 作为 VNC 专用代理 | 开发 |
|
||
| 容器资源耗尽 | 高 | 中 | 实现严格的资源限额和监控 | 运维 |
|
||
| VNC 安全风险 | 高 | 低 | 实现一次性 token 和会话超时(后续阶段) | 安全 |
|
||
| 多 agent 内存泄漏 | 中 | 中 | 实现 agent 生命周期管理和定期清理 | 开发 |
|
||
|
||
### 8.2 业务风险
|
||
|
||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||
|------|------|------|----------|
|
||
| 用户需求理解偏差 | 高 | 低 | 原型演示,快速迭代 |
|
||
| 与现有功能冲突 | 中 | 中 | 充分的兼容性测试 |
|
||
| 容器成本过高 | 中 | 中 | 实施严格的闲置清理策略 |
|
||
|
||
---
|
||
|
||
## 九、后续规划
|
||
|
||
### 9.1 第一阶段优化(MVP 后)
|
||
|
||
- 实现 VNC 会话录制和回放
|
||
- 增加剪贴板 API 支持
|
||
- 增强 Agent 监控和日志
|
||
|
||
### 9.2 第二阶段扩展
|
||
|
||
- 支持多用户协作(共享 VNC 桌面)
|
||
- 集成更多 MCP 工具(文件操作、数据库等)
|
||
- 实现 Agent 性能分析和优化
|
||
|
||
### 9.3 长期愿景
|
||
|
||
- 构建 Agent 市场,用户可以共享和复用 Agent 配置
|
||
- 支持自定义桌面环境和应用
|
||
- 实现 Agent 集群调度和负载均衡
|
||
|
||
---
|
||
|
||
## 十、附录
|
||
|
||
### 10.1 术语表
|
||
|
||
| 术语 | 说明 |
|
||
|------|------|
|
||
| user_id | 用户唯一标识符 |
|
||
| project_id | 项目唯一标识符 |
|
||
| ServiceType | 服务类型枚举(RCoder, ComputerAgentRunner) |
|
||
| ACP | Agent Client Protocol,AI Agent 通信协议 |
|
||
| MCP | Model Context Protocol,模型上下文协议 |
|
||
| CDP | Chrome DevTools Protocol,浏览器调试协议 |
|
||
| noVNC | 基于 HTML5 的 VNC 客户端 |
|
||
| LocalSet | Tokio 的本地任务集,支持 !Send 任务 |
|
||
| DashMap | 高性能的并发 HashMap |
|
||
|
||
### 10.2 参考资料
|
||
|
||
1. **rcoder 项目文档**:
|
||
- `CLAUDE.md` - 项目概述
|
||
- `crates/agent_runner/README.md` - Agent Runner 文档
|
||
|
||
2. **外部依赖文档**:
|
||
- [ACP Protocol](https://github.com/anthropics/agent-client-protocol)
|
||
- [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp)
|
||
- [noVNC Documentation](https://github.com/novnc/noVNC)
|
||
|
||
3. **技术博客**:
|
||
- Rust Tokio LocalSet 使用指南
|
||
- WebSocket 代理实现最佳实践
|
||
|
||
---
|
||
|
||
**文档变更历史**
|
||
|
||
| 版本 | 日期 | 作者 | 变更说明 |
|
||
|------|------|------|----------|
|
||
| v1.0 | 2025-12-10 | Claude | 初始版本 |
|
||
|
||
---
|
||
|
||
**审批记录**
|
||
|
||
| 角色 | 姓名 | 审批意见 | 日期 |
|
||
|------|------|----------|------|
|
||
| 产品负责人 | | | |
|
||
| 技术负责人 | | | |
|
||
| 安全负责人 | | | |
|