21 KiB
21 KiB
RCoder 双 Docker 镜像配置技术设计文档
📋 概述
本文档描述了 RCoder 系统中双 Docker 镜像配置的设计方案,支持在动态创建容器时指定 RCoder 或 AgentRunner 服务类型,以支持当前功能和未来新功能的开发。
背景
当前 RCoder 系统使用 registry.yichamao.com/rcoder 镜像。为了支持新功能开发,需要引入 registry.yichamao.com/rcoder-agent-runner 镜像,同时保持现有功能的稳定性。需要设计一个简化的双镜像配置系统。
目标
- 支持两种服务类型: rcoder (当前使用) 和 agent-runner (新功能)
- 保持架构兼容性: 支持 ARM64/AMD64 多架构
- 灵活配置: 支持默认镜像、服务特定镜像、项目级镜像覆盖
- 向后兼容: 不破坏现有配置和功能,默认使用 rcoder 服务
- 简化实现: 降低复杂度,便于维护和扩展
🏗️ 整体架构设计
配置层级结构
全局默认镜像配置
↓
服务类型特定配置 (rcoder/agent-runner/specialized-tools)
↓
项目级镜像覆盖 (可选)
↓
运行时镜像选择 (最终使用的镜像)
核心组件
- ServiceType: 服务类型枚举
- MultiImageConfig: 多镜像配置结构
- ImageSelector: 镜像选择器
- 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 天)
- 定义简化的 ServiceType (RCoder, AgentRunner)
- 实现基础的 MultiImageConfig
- 更新配置文件解析逻辑
阶段 2: 镜像选择器(1-2 天)
- 实现简化的 ImageSelector 核心逻辑
- 添加镜像缓存机制
- 实现向后兼容性支持
阶段 3: 容器创建集成(1-2 天)
- 更新 DockerManager 接口
- 集成服务类型到容器创建流程
- 实现项目级配置支持
阶段 4: API 和测试(1-2 天)
- 扩展 API 接口支持服务类型选择
- 更新文档和示例
- 编写核心测试用例
阶段 5: 部署和监控(1 天)
- 更新部署脚本和文档
- 添加基础监控和日志
- 简化性能测试
🔍 测试策略
单元测试
- 镜像选择逻辑测试
- 配置解析测试
- 缓存机制测试
集成测试
- 容器创建流程测试
- 多服务类型协同测试
- 项目级配置覆盖测试
端到端测试
- 完整的聊天流程测试
- 不同服务类型的容器启动测试
- 配置热更新测试
📊 性能考虑
镜像缓存
- 内存缓存减少重复计算
- TTL 机制避免过期配置
- LRU 策略控制内存使用
并发安全
- DashMap 用于线程安全的缓存访问
- 读写锁保护配置更新
- 原子操作确保一致性
🔒 安全考虑
配置验证
- 镜像名称格式验证
- 路径注入防护
- 环境变量安全检查
访问控制
- 项目级配置权限控制
- 服务类型启用/禁用控制
- 镜像仓库访问权限
📈 监控和日志
指标收集
- 镜像选择延迟
- 缓存命中率
- RCoder vs AgentRunner 使用统计
日志记录
- 镜像选择决策日志
- 配置解析错误日志
- 容器创建失败日志
本文档版本: v1.0
最后更新: 2025-12-02