# RCoder 双 Docker 镜像配置技术设计文档 ## 📋 概述 本文档描述了 RCoder 系统中双 Docker 镜像配置的设计方案,支持在动态创建容器时指定 RCoder 或 AgentRunner 服务类型,以支持当前功能和未来新功能的开发。 ### 背景 当前 RCoder 系统使用 `registry.yichamao.com/rcoder` 镜像。为了支持新功能开发,需要引入 `registry.yichamao.com/rcoder-agent-runner` 镜像,同时保持现有功能的稳定性。需要设计一个简化的双镜像配置系统。 ### 目标 1. **支持两种服务类型**: rcoder (当前使用) 和 agent-runner (新功能) 2. **保持架构兼容性**: 支持 ARM64/AMD64 多架构 3. **灵活配置**: 支持默认镜像、服务特定镜像、项目级镜像覆盖 4. **向后兼容**: 不破坏现有配置和功能,默认使用 rcoder 服务 5. **简化实现**: 降低复杂度,便于维护和扩展 ## 🏗️ 整体架构设计 ### 配置层级结构 ``` 全局默认镜像配置 ↓ 服务类型特定配置 (rcoder/agent-runner/specialized-tools) ↓ 项目级镜像覆盖 (可选) ↓ 运行时镜像选择 (最终使用的镜像) ``` ### 核心组件 1. **ServiceType**: 服务类型枚举 2. **MultiImageConfig**: 多镜像配置结构 3. **ImageSelector**: 镜像选择器 4. **ImageRegistry**: 镜像注册表 ## 📐 详细设计 ### 1. 服务类型定义 ```rust #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum ServiceType { /// 标准的 rcoder 服务 (当前默认使用的服务) RCoder, /// Agent Runner 服务 (新功能服务,后续开发使用) AgentRunner, } impl ServiceType { pub fn as_str(&self) -> &str { match self { ServiceType::RCoder => "rcoder", ServiceType::AgentRunner => "agent-runner", } } pub fn from_str(s: &str) -> Self { match s { "rcoder" => ServiceType::RCoder, "agent-runner" => ServiceType::AgentRunner, _ => { tracing::warn!("未知的服务类型 '{}',使用默认的 RCoder 服务", s); ServiceType::RCoder } } } /// 获取服务描述 pub fn description(&self) -> &str { match self { ServiceType::RCoder => "标准 RCoder 服务,提供完整的 AI 开发功能", ServiceType::AgentRunner => "Agent Runner 服务,专注于代理运行和执行", } } } // 注意:移除了 Default trait,强制要求明确指定服务类型 ``` ### 2. 服务镜像配置 ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServiceImageConfig { /// 服务类型 pub service_type: ServiceType, /// 通用镜像(优先级最高) pub image: Option, /// ARM64 架构专用镜像 pub arm64_image: Option, /// AMD64 架构专用镜像 pub amd64_image: Option, /// 默认回退镜像 pub default_image: Option, /// 镜像标签前缀(用于自动构建镜像名称) pub image_tag_prefix: Option, /// 是否启用该服务类型 pub enabled: bool, /// 服务特定的环境变量 pub environment: HashMap, /// 服务特定的挂载点 pub mounts: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServiceMountConfig { /// 容器内路径 pub container_path: String, /// 宿主机路径(支持变量替换) pub host_path: String, /// 是否只读 pub read_only: bool, /// 挂载类型 pub mount_type: String, // "bind", "volume" } ``` ### 3. 多镜像配置结构 ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MultiImageConfig { /// 全局默认镜像配置 pub global_defaults: GlobalImageDefaults, /// 各服务类型的镜像配置 pub services: HashMap, /// 镜像选择策略 pub selection_strategy: ImageSelectionStrategy, /// 镜像缓存配置 pub cache_config: ImageCacheConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GlobalImageDefaults { /// 通用默认镜像 pub image: Option, /// 默认 ARM64 镜像 pub arm64_image: Option, /// 默认 AMD64 镜像 pub amd64_image: Option, /// 默认回退镜像 pub default_image: Option, /// 镜像仓库前缀 pub registry_prefix: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ImageSelectionStrategy { /// 仅使用服务特定配置(强制明确指定服务类型) ServiceOnly, } impl Default for ImageSelectionStrategy { fn default() -> Self { ImageSelectionStrategy::ServiceOnly } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageCacheConfig { /// 是否启用镜像缓存 pub enabled: bool, /// 缓存过期时间(秒) pub ttl_seconds: u64, /// 最大缓存条目数 pub max_entries: usize, } ``` ### 4. 配置文件结构 ```yaml # Docker 双镜像配置 - 简化版本(仅支持 RCoder 和 AgentRunner) # 注意:创建容器时必须明确指定服务类型,不允许默认值 docker_config: # 全局默认配置 global_defaults: # 默认镜像仓库前缀 registry_prefix: "registry.yichamao.com" # 全局默认镜像配置 image: null # 留空使用服务特定配置 arm64_image: "registry.yichamao.com/default:latest-arm64" amd64_image: "registry.yichamao.com/default:latest-amd64" default_image: "registry.yichamao.com/default:latest" # 镜像选择策略 selection_strategy: "ServiceOnly" # 仅使用服务特定配置,强制明确指定 # 各服务类型的配置 services: # 标准 RCoder 服务配置 (当前项目使用) rcoder: service_type: "rcoder" image: null # 使用架构特定镜像 arm64_image: "registry.yichamao.com/rcoder:latest-arm64" amd64_image: "registry.yichamao.com/rcoder:latest-amd64" default_image: "registry.yichamao.com/rcoder:latest" image_tag_prefix: "rcoder" enabled: true # 当前启用 environment: RUST_LOG: "info" SERVICE_MODE: "full" API_PORT: "8086" mounts: - container_path: "/app/project_workspace" host_path: "./project_workspace" read_only: false mount_type: "bind" # Agent Runner 服务配置 (新功能,后续开发使用) agent-runner: service_type: "agent-runner" image: null # 使用架构特定镜像 arm64_image: "registry.yichamao.com/rcoder-agent-runner:latest-arm64" amd64_image: "registry.yichamao.com/rcoder-agent-runner:latest-amd64" default_image: "registry.yichamao.com/rcoder-agent-runner:latest" image_tag_prefix: "rcoder-agent-runner" enabled: false # 默认禁用,等待新功能开发 environment: RUST_LOG: "debug" SERVICE_MODE: "agent-only" AGENT_PORT: "8086" mounts: - container_path: "/app/workspace" host_path: "./project_workspace/{project_id}" read_only: false mount_type: "bind" - container_path: "/app/models" host_path: "./models" read_only: true mount_type: "bind" # 镜像缓存配置 cache_config: enabled: true ttl_seconds: 3600 # 1小时 max_entries: 50 # 减少缓存条目数,因为只有2个服务 # 其他现有配置保持不变 network_mode: "bridge" work_dir: "/app" auto_cleanup: true container_ttl_seconds: 3600 ``` ### 5. 镜像选择器实现 ```rust pub struct ImageSelector { config: MultiImageConfig, cache: Arc>>, platform: String, } #[derive(Debug, Clone)] pub struct CachedImageInfo { pub image_name: String, pub service_type: ServiceType, pub platform: String, pub cached_at: std::time::SystemTime, } impl ImageSelector { pub fn new(config: MultiImageConfig) -> Self { let platform = crate::utils::DockerUtils::get_optimal_platform(); Self { config, cache: Arc::new(RwLock::new(HashMap::new())), platform, } } /// 根据服务类型和项目配置选择镜像 /// 注意:service_type 不能为空,必须明确指定 pub fn select_image( &self, service_type: &ServiceType, project_overrides: Option<&ProjectImageOverrides>, ) -> DockerResult { // 强制验证:service_type 必须明确指定 if !self.is_service_enabled(service_type) { return Err(DockerError::ConfigurationError( format!("服务类型 '{}' 未启用或配置不存在", service_type.as_str()) )); } let cache_key = self.build_cache_key(service_type, project_overrides); // 检查缓存 if let Some(cached) = self.get_from_cache(&cache_key) { return Ok(cached.image_name); } // 强制使用 ServiceOnly 策略:仅使用服务特定配置 let image_name = self.select_service_only(service_type, project_overrides)?; // 缓存结果 self.cache_image_info(&cache_key, &image_name, service_type); Ok(image_name) } /// 检查服务是否已启用和配置 fn is_service_enabled(&self, service_type: &ServiceType) -> bool { let service_key = service_type.as_str(); if let Some(service_config) = self.config.services.get(service_key) { service_config.enabled } else { false } } fn select_service_first( &self, service_type: &ServiceType, project_overrides: Option<&ProjectImageOverrides>, ) -> DockerResult { let service_key = service_type.as_str(); // 1. 检查项目级覆盖 if let Some(overrides) = project_overrides { if let Some(project_image) = self.get_project_override_image(overrides, service_type) { return Ok(project_image); } } // 2. 检查服务特定配置 if let Some(service_config) = self.config.services.get(service_key) { if !service_config.enabled { return Err(DockerError::ConfigurationError( format!("服务类型 {} 未启用", service_key) )); } if let Some(image) = self.select_service_image(service_config) { return Ok(image); } } // 3. 回退到全局默认配置 self.select_global_default_image() } fn select_service_image(&self, config: &ServiceImageConfig) -> Option { // 1. 优先使用通用镜像 if let Some(image) = &config.image { return Some(image.clone()); } // 2. 根据架构选择特定镜像 match self.platform.as_str() { "linux/arm64" => { config.arm64_image .clone() .or_else(|| config.default_image.clone()) } "linux/amd64" => { config.amd64_image .clone() .or_else(|| config.default_image.clone()) } _ => config.default_image.clone(), } } fn select_global_default_image(&self) -> DockerResult { let defaults = &self.config.global_defaults; // 1. 优先使用全局通用镜像 if let Some(image) = &defaults.image { return Ok(image.clone()); } // 2. 根据架构选择全局默认镜像 let image = match self.platform.as_str() { "linux/arm64" => { defaults.arm64_image .clone() .or_else(|| defaults.default_image.clone()) } "linux/amd64" => { defaults.amd64_image .clone() .or_else(|| defaults.default_image.clone()) } _ => defaults.default_image.clone(), }; image.ok_or_else(|| { DockerError::ConfigurationError( "无法找到适合的默认镜像配置".to_string() ) }) } fn build_cache_key( &self, service_type: &ServiceType, project_overrides: Option<&ProjectImageOverrides>, ) -> String { let base_key = format!("{}:{}", service_type.as_str(), self.platform); if let Some(overrides) = project_overrides { format!("{}:overrides:{}", base_key, overrides.hash_key()) } else { base_key } } } /// 项目级镜像覆盖配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectImageOverrides { /// 项目特定的镜像配置 pub images: HashMap, /// 启用的服务类型 pub enabled_services: Vec, /// 项目特定的环境变量 pub environment: HashMap, } impl ProjectImageOverrides { pub fn hash_key(&self) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); // 哈希镜像配置 for (key, value) in &self.images { key.hash(&mut hasher); value.hash(&mut hasher); } // 哈希启用的服务 for service in &self.enabled_services { service.hash(&mut hasher); } format!("{:x}", hasher.finish()) } } ``` ## 🔧 实现建议 ### 1. 配置文件兼容性 为保持向后兼容,支持两种配置方式: ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DockerConfig { /// 新的多镜像配置(优先) #[serde(default)] pub multi_image_config: Option, /// 传统单一镜像配置(向后兼容) pub image: Option, pub arm64_image: Option, pub amd64_image: Option, pub default_image: Option, // ... 其他现有字段 } impl DockerConfig { /// 获取多镜像配置,如果未配置则使用传统配置创建默认配置 pub fn get_multi_image_config(&self) -> MultiImageConfig { if let Some(ref config) = self.multi_image_config { config.clone() } else { // 从传统配置创建默认多镜像配置 self.create_legacy_multi_config() } } fn create_legacy_multi_config(&self) -> MultiImageConfig { MultiImageConfig { default_service_type: ServiceType::RCoder, global_defaults: GlobalImageDefaults { image: self.image.clone(), arm64_image: self.arm64_image.clone(), amd64_image: self.amd64_image.clone(), default_image: self.default_image.clone(), registry_prefix: Some("registry.yichamao.com".to_string()), }, services: { let mut services = HashMap::new(); services.insert( "rcoder".to_string(), ServiceImageConfig { service_type: ServiceType::RCoder, image: self.image.clone(), arm64_image: self.arm64_image.clone(), amd64_image: self.amd64_image.clone(), default_image: self.default_image.clone(), image_tag_prefix: Some("rcoder".to_string()), enabled: true, environment: HashMap::new(), mounts: Vec::new(), }, ); services }, selection_strategy: ImageSelectionStrategy::ServiceFirst, cache_config: ImageCacheConfig { enabled: true, ttl_seconds: 3600, max_entries: 100, }, } } } ``` ### 2. 容器创建接口更新 ```rust impl DockerManager { /// 使用多镜像配置创建容器 pub async fn create_container_with_service_type( &self, mut config: DockerContainerConfig, service_type: ServiceType, project_overrides: Option, ) -> DockerResult { // 选择合适的镜像 let image_selector = ImageSelector::new(self.get_multi_image_config()); let selected_image = image_selector.select_image(&service_type, project_overrides.as_ref())?; // 更新容器配置 config.image = selected_image; // 添加服务特定的环境变量 if let Some(service_config) = image_selector.get_service_config(&service_type) { for (key, value) in &service_config.environment { config.env_vars.insert(key.clone(), value.clone()); } // 添加服务特定的挂载点 for mount_config in &service_config.mounts { let host_path = self.resolve_host_path(&mount_config.host_path, &config.project_id)?; config.extra_mounts.push(ExtraMount { host_path, container_path: mount_config.container_path.clone(), read_only: mount_config.read_only, }); } } // 使用现有逻辑创建容器 self.create_container(config).await } /// 获取多镜像配置 fn get_multi_image_config(&self) -> MultiImageConfig { self.config.get_multi_image_config() } /// 解析宿主机路径(支持变量替换) fn resolve_host_path(&self, path_template: &str, project_id: &str) -> DockerResult { let resolved = path_template .replace("{project_id}", project_id) .replace("{workspace_dir}", &self.config.default_work_dir); Ok(resolved) } } ``` ### 3. 项目级配置支持 支持在项目目录中创建 `.rcoder-image.yml` 文件来覆盖镜像配置: ```yaml # .rcoder-image.yml (项目级配置) project_id: "my-special-project" service_type: "agent-runner" # 可选择使用 AgentRunner 服务 # 项目特定的镜像覆盖 images: rcoder: "my-custom-registry/rcoder:custom-v1.0" agent-runner: "my-custom-registry/agent-runner:custom-v1.0" # 启用的服务类型 enabled_services: - "rcoder" - "agent-runner" # 项目特定的环境变量 environment: AGENT_MODE: "production" LOG_LEVEL: "debug" RCODER_FEATURES: "full" ``` ### 4. API 接口扩展 扩展现有的聊天接口支持服务类型选择: ```rust #[derive(Debug, Deserialize, Serialize)] pub struct ChatRequest { pub prompt: String, pub project_id: Option, pub session_id: Option, pub attachments: Vec, // 必填:服务类型选择 (强制要求指定) pub service_type: String, // "rcoder" 或 "agent-runner",不允许为空 // 可选:项目级镜像覆盖 pub image_overrides: Option>, // 现有字段... pub model_provider: Option, pub request_id: Option, } ``` ## 📋 实施计划 ### 阶段 1: 基础结构(1 天) 1. 定义简化的 ServiceType (RCoder, AgentRunner) 2. 实现基础的 MultiImageConfig 3. 更新配置文件解析逻辑 ### 阶段 2: 镜像选择器(1-2 天) 1. 实现简化的 ImageSelector 核心逻辑 2. 添加镜像缓存机制 3. 实现向后兼容性支持 ### 阶段 3: 容器创建集成(1-2 天) 1. 更新 DockerManager 接口 2. 集成服务类型到容器创建流程 3. 实现项目级配置支持 ### 阶段 4: API 和测试(1-2 天) 1. 扩展 API 接口支持服务类型选择 2. 更新文档和示例 3. 编写核心测试用例 ### 阶段 5: 部署和监控(1 天) 1. 更新部署脚本和文档 2. 添加基础监控和日志 3. 简化性能测试 ## 🔍 测试策略 ### 单元测试 - 镜像选择逻辑测试 - 配置解析测试 - 缓存机制测试 ### 集成测试 - 容器创建流程测试 - 多服务类型协同测试 - 项目级配置覆盖测试 ### 端到端测试 - 完整的聊天流程测试 - 不同服务类型的容器启动测试 - 配置热更新测试 ## 📊 性能考虑 ### 镜像缓存 - 内存缓存减少重复计算 - TTL 机制避免过期配置 - LRU 策略控制内存使用 ### 并发安全 - DashMap 用于线程安全的缓存访问 - 读写锁保护配置更新 - 原子操作确保一致性 ## 🔒 安全考虑 ### 配置验证 - 镜像名称格式验证 - 路径注入防护 - 环境变量安全检查 ### 访问控制 - 项目级配置权限控制 - 服务类型启用/禁用控制 - 镜像仓库访问权限 ## 📈 监控和日志 ### 指标收集 - 镜像选择延迟 - 缓存命中率 - RCoder vs AgentRunner 使用统计 ### 日志记录 - 镜像选择决策日志 - 配置解析错误日志 - 容器创建失败日志 --- *本文档版本: v1.0* *最后更新: 2025-12-02*