Files
qiming/qiming-rcoder/specs/history/multi-docker-image-design.md
2026-06-01 13:54:52 +08:00

21 KiB
Raw Blame History

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. 服务类型定义

#[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. 服务镜像配置

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceImageConfig {
    /// 服务类型
    pub service_type: ServiceType,
    /// 通用镜像(优先级最高)
    pub image: Option<String>,
    /// ARM64 架构专用镜像
    pub arm64_image: Option<String>,
    /// AMD64 架构专用镜像
    pub amd64_image: Option<String>,
    /// 默认回退镜像
    pub default_image: Option<String>,
    /// 镜像标签前缀(用于自动构建镜像名称)
    pub image_tag_prefix: Option<String>,
    /// 是否启用该服务类型
    pub enabled: bool,
    /// 服务特定的环境变量
    pub environment: HashMap<String, String>,
    /// 服务特定的挂载点
    pub mounts: Vec<ServiceMountConfig>,
}

#[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. 多镜像配置结构

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiImageConfig {
    /// 全局默认镜像配置
    pub global_defaults: GlobalImageDefaults,
    /// 各服务类型的镜像配置
    pub services: HashMap<String, ServiceImageConfig>,
    /// 镜像选择策略
    pub selection_strategy: ImageSelectionStrategy,
    /// 镜像缓存配置
    pub cache_config: ImageCacheConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalImageDefaults {
    /// 通用默认镜像
    pub image: Option<String>,
    /// 默认 ARM64 镜像
    pub arm64_image: Option<String>,
    /// 默认 AMD64 镜像
    pub amd64_image: Option<String>,
    /// 默认回退镜像
    pub default_image: Option<String>,
    /// 镜像仓库前缀
    pub registry_prefix: Option<String>,
}

#[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. 配置文件结构

# 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. 镜像选择器实现

pub struct ImageSelector {
    config: MultiImageConfig,
    cache: Arc<RwLock<HashMap<String, CachedImageInfo>>>,
    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<String> {
        // 强制验证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<String> {
        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<String> {
        // 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<String> {
        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<String, String>,
    /// 启用的服务类型
    pub enabled_services: Vec<String>,
    /// 项目特定的环境变量
    pub environment: HashMap<String, String>,
}

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. 配置文件兼容性

为保持向后兼容,支持两种配置方式:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerConfig {
    /// 新的多镜像配置(优先)
    #[serde(default)]
    pub multi_image_config: Option<MultiImageConfig>,
    
    /// 传统单一镜像配置(向后兼容)
    pub image: Option<String>,
    pub arm64_image: Option<String>,
    pub amd64_image: Option<String>,
    pub default_image: Option<String>,
    
    // ... 其他现有字段
}

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. 容器创建接口更新

impl DockerManager {
    /// 使用多镜像配置创建容器
    pub async fn create_container_with_service_type(
        &self,
        mut config: DockerContainerConfig,
        service_type: ServiceType,
        project_overrides: Option<ProjectImageOverrides>,
    ) -> DockerResult<DockerContainerInfo> {
        // 选择合适的镜像
        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<String> {
        let resolved = path_template
            .replace("{project_id}", project_id)
            .replace("{workspace_dir}", &self.config.default_work_dir);
        
        Ok(resolved)
    }
}

3. 项目级配置支持

支持在项目目录中创建 .rcoder-image.yml 文件来覆盖镜像配置:

# .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 接口扩展

扩展现有的聊天接口支持服务类型选择:

#[derive(Debug, Deserialize, Serialize)]
pub struct ChatRequest {
    pub prompt: String,
    pub project_id: Option<String>,
    pub session_id: Option<String>,
    pub attachments: Vec<Attachment>,
    
    // 必填:服务类型选择 (强制要求指定)
    pub service_type: String,  // "rcoder" 或 "agent-runner",不允许为空
    
    // 可选:项目级镜像覆盖
    pub image_overrides: Option<HashMap<String, String>>,
    
    // 现有字段...
    pub model_provider: Option<ModelProviderConfig>,
    pub request_id: Option<String>,
}

📋 实施计划

阶段 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