添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
# Instructions
## Project Alpha
我使用的docker容器的配置文件: docker/rcoder-agent-runner/Dockerfile ,现在的vnc远程桌面,播放视频,没有声音,我新增了一个音频流传输方案,还有输入法我想使用客户端的输入法来输入,比如客户端使用虚拟桌面,客户端是支持中文输入法,在客户端输入中文,可以透传到novnc的虚拟桌面里.
我之前是通过新的端口,来提供对应的服务,但这个是在子容器里的,需要通过pingora 代理到 rcoder 主容器里,不对客户端暴露子容器的ip和端口信息,在rcoder模块里,通过pingora来提供服务.
我的pingora 代理模块,当前已有透明代理服务看: crates/rcoder-proxy/src/router.rs ,需要新增音频通道,还有输入法的通道,来进行透明代理使用吧.
注: 如何路由代理到子容器,可以根据 `{user_id}``{project_id}` 来区分不同的容器,这样就可以确认路由代理到哪个子容器了,现有的业务,是有实现的,可以参考复用; @docker/vnc-test.html 是验证原型功能使用的,测试vnc远程桌面,音频播放,使用客户端的输入法,来在远程桌面里的系统里输入透传,比如客户端切换中文输入法,可以在远程桌面的文本编辑框里,输入中文.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,914 @@
# Agent 配置入参功能 - 详细实现计划
> 基于 `01-agent-config-instruction-spec.md` 设计文档生成
## 1. 实现概述
### 1.1 目标
`/chat` 接口增加三个可选配置入参:
- `system_prompt`: 系统提示词
- `user_prompt`: 用户提示词模板(支持 `{user_prompt}` 变量注入)
- `agent_config`: Agent 运行时配置(包含 `agent_server``context_servers`
### 1.2 实现原则
- **向后兼容**: 所有新字段可选,不影响现有调用
- **职责分离**: 提示词由独立入参控制,`agent_config` 只负责运行时配置
- **类型安全**: 使用强类型结构体,避免 JSON 字符串解析
### 1.3 实现分批
| 批次 | 内容 | 预计文件数 |
|------|------|-----------|
| 第一批 | 基础结构类型定义、proto、请求结构 | 5 |
| 第二批 | 核心逻辑配置组装器、gRPC 服务) | 4 |
| 第三批 | 启动器集成ClaudeCodeLauncher | 2 |
| 第四批 | 测试和文档 | 3+ |
---
## 2. 第一批:基础结构
### 2.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|------|------|------|------|
| 1.1 | 创建 ChatAgentConfig 结构体 | `crates/shared_types/src/chat_agent_config.rs` | 新增 |
| 1.2 | 导出新类型 | `crates/shared_types/src/lib.rs` | 修改 |
| 1.3 | 修改 gRPC proto 定义 | `crates/shared_types/proto/agent.proto` | 修改 |
| 1.4 | 修改 HTTP ChatRequest | `crates/rcoder/src/handler/chat_handler.rs` | 修改 |
| 1.5 | 修改 gRPC 客户端传参 | `crates/rcoder/src/grpc/chat_client.rs` | 修改 |
---
### 2.1.1 创建 ChatAgentConfig 结构体
**文件**: `crates/shared_types/src/chat_agent_config.rs`
**步骤**:
1. 创建新文件
2. 定义三个结构体:
- `ChatAgentConfig` - 顶层配置
- `ChatAgentServerConfig` - Agent 服务器配置
- `ChatContextServerConfig` - MCP 服务器配置
3. 实现 `Default` trait
4. 实现辅助方法:`has_agent_server()`, `has_context_servers()`, `get_enabled_context_servers()`, `get_agent_id()`
**代码模板**:
```rust
// 见设计文档 2.2 节完整代码
```
**验证点**:
- [ ] 编译通过
- [ ] JSON 序列化/反序列化测试通过
- [ ] 默认值正确
---
### 2.1.2 导出新类型
**文件**: `crates/shared_types/src/lib.rs`
**修改内容**:
```rust
// 新增
mod chat_agent_config;
pub use chat_agent_config::{ChatAgentConfig, ChatAgentServerConfig, ChatContextServerConfig};
```
**验证点**:
- [ ] `cargo build -p shared_types` 编译通过
- [ ] 其他 crate 可以正常引用新类型
---
### 2.1.3 修改 gRPC proto 定义
**文件**: `crates/shared_types/proto/agent.proto`
**修改内容**:
1.`ChatRequest` 消息中添加新字段:
```protobuf
message ChatRequest {
// ... 现有字段 (1-7) ...
// 新增字段
optional string system_prompt = 8; // 系统提示词
optional string user_prompt = 9; // 用户提示词模板
optional ChatAgentConfig agent_config = 10; // Agent 运行时配置
}
```
2. 添加新消息定义:
```protobuf
// Agent 运行时配置
message ChatAgentConfig {
optional ChatAgentServerConfig agent_server = 1;
map<string, ChatContextServerConfig> context_servers = 2;
}
// 单个 Agent 服务器配置
message ChatAgentServerConfig {
optional string agent_id = 1;
optional string command = 2;
repeated string args = 3;
map<string, string> env = 4;
map<string, string> metadata = 5;
}
// MCP 服务器配置
message ChatContextServerConfig {
string source = 1;
bool enabled = 2;
optional string command = 3;
repeated string args = 4;
map<string, string> env = 5;
}
```
**验证点**:
- [ ] `cargo build -p shared_types` 编译通过proto 自动生成)
- [ ] 生成的 Rust 代码位于 `src/grpc/agent.rs`
---
### 2.1.4 修改 HTTP ChatRequest
**文件**: `crates/rcoder/src/handler/chat_handler.rs`
**修改内容**:
1. 添加 import
```rust
use shared_types::ChatAgentConfig;
```
2.`ChatRequest` 结构体中添加新字段:
```rust
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub struct ChatRequest {
// ... 现有字段 ...
/// 可选的系统提示词,覆盖默认配置
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(example = "你是一个专业的 Rust 开发者")]
pub system_prompt: Option<String>,
/// 可选的用户提示词模板,支持 {user_prompt} 变量替换
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(example = "请用 Rust 完成:{user_prompt}")]
pub user_prompt: Option<String>,
/// 可选的 Agent 运行时配置Agent 服务器 + MCP 服务器)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_config: Option<ChatAgentConfig>,
}
```
**验证点**:
- [ ] `cargo build -p rcoder` 编译通过
- [ ] OpenAPI 文档正确生成新字段
---
### 2.1.5 修改 gRPC 客户端传参
**文件**: `crates/rcoder/src/grpc/chat_client.rs`
**修改内容**:
1. 在构建 gRPC `ChatRequest` 时传递新字段:
```rust
// 找到构建 GrpcChatRequest 的位置,添加新字段
let grpc_request = GrpcChatRequest {
// ... 现有字段 ...
system_prompt: http_request.system_prompt.clone(),
user_prompt: http_request.user_prompt.clone(),
agent_config: http_request.agent_config.clone().map(|c| c.into()),
};
```
2. 实现 `From<ChatAgentConfig>` 转换(如需要):
```rust
impl From<shared_types::ChatAgentConfig> for proto::ChatAgentConfig {
fn from(config: shared_types::ChatAgentConfig) -> Self {
Self {
agent_server: config.agent_server.map(|s| s.into()),
context_servers: config.context_servers
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
}
}
}
```
**验证点**:
- [ ] `cargo build -p rcoder` 编译通过
- [ ] gRPC 请求正确包含新字段
---
## 3. 第二批:核心逻辑
### 3.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|------|------|------|------|
| 2.1 | 创建 PromptConfigAssembler | `crates/agent_config/src/config/prompt_assembler.rs` | 新增 |
| 2.2 | 导出新模块 | `crates/agent_config/src/config/mod.rs` | 修改 |
| 2.3 | 修改 ChatPrompt 结构体 | `crates/shared_types/src/lib.rs` | 修改 |
| 2.4 | 修改 PromptMessage 结构体 | `crates/agent_abstraction/src/traits/agent.rs` | 修改 |
---
### 3.1.1 创建 PromptConfigAssembler
**文件**: `crates/agent_config/src/config/prompt_assembler.rs`
**步骤**:
1. 创建新文件
2. 实现 `PromptConfigAssembler` 结构体
3. 实现核心方法:
- `new()` - 构造函数
- `with_system_prompt()` - Builder 方法
- `with_user_prompt_template()` - Builder 方法
- `with_agent_config()` - Builder 方法
- `get_system_prompt()` - 获取最终系统提示词
- `apply_user_prompt()` - 应用用户提示词模板
- `get_agent_server_config()` - 获取最终 Agent 配置
- `get_context_servers()` - 获取最终 MCP 配置
- `get_agent_id()` - 获取使用的 Agent ID
**代码模板**:
```rust
// 见设计文档 4.5.1 节完整代码
```
**验证点**:
- [ ] 编译通过
- [ ] 单元测试:入参优先级正确
- [ ] 单元测试:模板替换正确
- [ ] 单元测试:配置合并正确
---
### 3.1.2 导出新模块
**文件**: `crates/agent_config/src/config/mod.rs`
**修改内容**:
```rust
pub mod prompt_assembler;
pub use prompt_assembler::PromptConfigAssembler;
```
**文件**: `crates/agent_config/src/lib.rs`
**修改内容**:
```rust
pub use config::prompt_assembler::PromptConfigAssembler;
```
**验证点**:
- [ ] `cargo build -p agent_config` 编译通过
- [ ] 其他 crate 可以正常引用 `PromptConfigAssembler`
---
### 3.1.3 修改 ChatPrompt 结构体
**文件**: `crates/shared_types/src/lib.rs` (或 `chat_prompt.rs`)
**修改内容**:
```rust
use crate::ChatAgentConfig;
#[derive(Debug, Clone, Builder)]
pub struct ChatPrompt {
// ... 现有字段 ...
/// 可选的系统提示词覆盖
#[builder(default)]
pub system_prompt_override: Option<String>,
/// 可选的用户提示词模板覆盖
#[builder(default)]
pub user_prompt_template_override: Option<String>,
/// 可选的 Agent 运行时配置覆盖
#[builder(default)]
pub agent_config_override: Option<ChatAgentConfig>,
}
```
**验证点**:
- [ ] `cargo build -p shared_types` 编译通过
- [ ] Builder 模式正常工作
---
### 3.1.4 修改 PromptMessage 结构体
**文件**: `crates/agent_abstraction/src/traits/agent.rs`
**修改内容**:
1. 添加 import
```rust
use shared_types::ChatAgentConfig;
```
2.`PromptMessage` 结构体中添加新字段:
```rust
#[derive(Debug, Clone)]
pub struct PromptMessage {
// ... 现有字段 ...
/// 系统提示词覆盖
pub system_prompt_override: Option<String>,
/// 用户提示词模板覆盖
pub user_prompt_template_override: Option<String>,
/// Agent 运行时配置覆盖MCP 服务器等)
pub agent_config_override: Option<ChatAgentConfig>,
}
```
3. 修改 `From<ChatPrompt>` 实现:
```rust
impl From<shared_types::ChatPrompt> for PromptMessage {
fn from(chat_prompt: shared_types::ChatPrompt) -> Self {
Self {
// ... 现有字段映射 ...
system_prompt_override: chat_prompt.system_prompt_override,
user_prompt_template_override: chat_prompt.user_prompt_template_override,
agent_config_override: chat_prompt.agent_config_override,
}
}
}
```
**验证点**:
- [ ] `cargo build -p agent_abstraction` 编译通过
- [ ] `From` 转换正确
---
## 4. 第三批:服务层集成
### 4.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|------|------|------|------|
| 3.1 | 修改 gRPC AgentServiceImpl | `crates/agent_runner/src/grpc/agent_service_impl.rs` | 修改 |
| 3.2 | 修改 AcpAgentWorker | `crates/agent_abstraction/src/session/acp_worker.rs` | 修改 |
| 3.3 | 修改 ClaudeCodeLauncher | `crates/agent_abstraction/src/compat/claude_code_launcher.rs` | 修改 |
---
### 4.1.1 修改 gRPC AgentServiceImpl
**文件**: `crates/agent_runner/src/grpc/agent_service_impl.rs`
**修改内容**:
`chat()` 方法中:
1. 添加 import
```rust
use shared_types::ChatAgentConfig;
```
2. 解析新字段并传递给 ChatPrompt
```rust
async fn chat(&self, request: Request<GrpcChatRequest>) -> Result<Response<GrpcChatResponse>, Status> {
let req = request.into_inner();
// ... 现有验证逻辑 ...
// 转换 gRPC ChatAgentConfig -> shared_types ChatAgentConfig
let agent_config_override: Option<ChatAgentConfig> = req.agent_config.map(|c| c.into());
// 构建 ChatPrompt包含覆盖配置
let chat_prompt = ChatPromptBuilder::default()
.project_id(project_id.clone())
.project_path(project_dir)
.session_id(session_id.clone())
.prompt(req.prompt)
.system_prompt_override(req.system_prompt) // 新增
.user_prompt_template_override(req.user_prompt) // 新增
.agent_config_override(agent_config_override) // 新增
// ... 其他字段 ...
.build()
.map_err(|e| Status::internal(format!("构建 ChatPrompt 失败: {}", e)))?;
// ... 后续处理 ...
}
```
3. 实现 gRPC 类型转换(如果需要):
```rust
impl From<proto::ChatAgentConfig> for shared_types::ChatAgentConfig {
fn from(proto: proto::ChatAgentConfig) -> Self {
Self {
agent_server: proto.agent_server.map(|s| s.into()),
context_servers: proto.context_servers
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
}
}
}
impl From<proto::ChatAgentServerConfig> for shared_types::ChatAgentServerConfig {
fn from(proto: proto::ChatAgentServerConfig) -> Self {
Self {
agent_id: proto.agent_id,
command: proto.command,
args: if proto.args.is_empty() { None } else { Some(proto.args) },
env: if proto.env.is_empty() { None } else { Some(proto.env) },
metadata: if proto.metadata.is_empty() { None } else { Some(proto.metadata) },
}
}
}
impl From<proto::ChatContextServerConfig> for shared_types::ChatContextServerConfig {
fn from(proto: proto::ChatContextServerConfig) -> Self {
Self {
source: proto.source,
enabled: proto.enabled,
command: proto.command,
args: if proto.args.is_empty() { None } else { Some(proto.args) },
env: if proto.env.is_empty() { None } else { Some(proto.env) },
}
}
}
```
**验证点**:
- [ ] `cargo build -p agent_runner` 编译通过
- [ ] gRPC 类型转换正确
---
### 4.1.2 修改 AcpAgentWorker
**文件**: `crates/agent_abstraction/src/session/acp_worker.rs`
**修改内容**:
1. 添加 import
```rust
use agent_config::{AgentServersConfig, PromptConfigAssembler};
```
2. 在处理请求时使用配置组装器:
```rust
impl AgentWorker for AcpAgentWorker {
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse> {
// 加载默认配置
let default_config = AgentServersConfig::load_or_default().await;
// 创建配置组装器
let assembler = PromptConfigAssembler::new(default_config)
.with_system_prompt(request.prompt_message.system_prompt_override.clone())
.with_user_prompt_template(request.prompt_message.user_prompt_template_override.clone())
.with_agent_config(request.prompt_message.agent_config_override.clone());
// 获取最终的系统提示词
let system_prompt = assembler.get_system_prompt("claude-code-acp");
// 获取最终的 Agent 配置
let agent_config = assembler.get_agent_server_config("claude-code-acp");
// 获取 MCP 服务器配置
let context_servers = assembler.get_context_servers();
// 应用用户提示词模板
let final_user_prompt = assembler.apply_user_prompt(
"claude-code-acp",
&request.prompt_message.content,
);
// 更新 prompt_message 的 content 为处理后的用户提示词
let mut prompt_message = request.prompt_message.clone();
prompt_message.content = final_user_prompt;
// 构建 AgentStartConfig
let start_config = AgentStartConfig::new()
.with_system_prompt(system_prompt)
.with_mcp_servers(context_servers); // 如果有此方法
// ... 创建会话 / 发送 prompt ...
}
}
```
**验证点**:
- [ ] `cargo build -p agent_abstraction` 编译通过
- [ ] 配置组装逻辑正确
---
### 4.1.3 修改 ClaudeCodeLauncher
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
**修改内容**:
1. 添加 import
```rust
use shared_types::ChatAgentConfig;
use agent_config::PromptConfigAssembler;
```
2. 修改或新增配置加载方法:
```rust
/// 加载 Agent 配置,支持覆盖
pub async fn load_agent_config_with_override(
model_provider: Option<&ModelProviderConfig>,
agent_config_override: Option<&ChatAgentConfig>,
) -> Result<AgentLaunchConfig> {
// 加载默认配置
let default_config = AgentServersConfig::load_or_default().await;
// 创建配置组装器
let assembler = PromptConfigAssembler::new(default_config.clone())
.with_agent_config(agent_config_override.cloned());
// 获取最终的 Agent 配置
let agent_config = assembler.get_agent_server_config("claude-code-acp");
// 获取 MCP 服务器配置
let context_servers = assembler.get_context_servers();
// 构建启动配置
// ... 使用 agent_config 和 context_servers 构建 AgentLaunchConfig ...
}
```
3.`launch()` 方法中使用新配置:
```rust
pub async fn launch(
&self,
project_id: String,
project_path: PathBuf,
session_id_hint: Option<String>,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig, // 已包含系统提示词和 MCP 配置
client: C,
) -> Result<ConnectionInfo> {
// 从 start_config 获取配置
let system_prompt = start_config.system_prompt.clone();
let mcp_servers = start_config.mcp_servers.clone();
// ... 使用配置启动 Agent ...
}
```
**验证点**:
- [ ] `cargo build -p agent_abstraction` 编译通过
- [ ] Agent 启动时正确使用覆盖配置
---
## 5. 第四批:测试和文档
### 5.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|------|------|------|------|
| 4.1 | ChatAgentConfig 单元测试 | `crates/shared_types/src/chat_agent_config.rs` | 修改 |
| 4.2 | PromptConfigAssembler 单元测试 | `crates/agent_config/src/config/prompt_assembler.rs` | 修改 |
| 4.3 | 集成测试 | `tests/integration/chat_config_test.rs` | 新增 |
| 4.4 | 更新 OpenAPI 文档 | 自动生成 | - |
---
### 5.1.1 ChatAgentConfig 单元测试
**文件**: `crates/shared_types/src/chat_agent_config.rs`
**测试用例**:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_agent_config_default() {
let config = ChatAgentConfig::default();
assert!(config.agent_server.is_none());
assert!(config.context_servers.is_empty());
assert!(!config.has_agent_server());
assert!(!config.has_context_servers());
}
#[test]
fn test_chat_agent_config_json_serialize() {
let config = ChatAgentConfig {
agent_server: Some(ChatAgentServerConfig {
agent_id: Some("test-agent".to_string()),
command: Some("test-cmd".to_string()),
..Default::default()
}),
context_servers: HashMap::new(),
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("test-agent"));
}
#[test]
fn test_chat_agent_config_json_deserialize() {
let json = r#"{
"agent_server": {
"agent_id": "claude-code-acp",
"env": {"RUST_LOG": "debug"}
},
"context_servers": {
"context7": {
"source": "custom",
"enabled": true,
"command": "bunx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}"#;
let config: ChatAgentConfig = serde_json::from_str(json).unwrap();
assert!(config.has_agent_server());
assert!(config.has_context_servers());
assert_eq!(config.agent_server.unwrap().get_agent_id(), "claude-code-acp");
}
#[test]
fn test_get_agent_id_default() {
let config = ChatAgentServerConfig::default();
assert_eq!(config.get_agent_id(), "claude-code-acp");
}
#[test]
fn test_get_enabled_context_servers() {
let mut context_servers = HashMap::new();
context_servers.insert("enabled".to_string(), ChatContextServerConfig {
enabled: true,
..Default::default()
});
context_servers.insert("disabled".to_string(), ChatContextServerConfig {
enabled: false,
..Default::default()
});
let config = ChatAgentConfig {
agent_server: None,
context_servers,
};
let enabled = config.get_enabled_context_servers();
assert_eq!(enabled.len(), 1);
assert!(enabled.contains_key("enabled"));
}
}
```
---
### 5.1.2 PromptConfigAssembler 单元测试
**文件**: `crates/agent_config/src/config/prompt_assembler.rs`
**测试用例**:
```rust
#[cfg(test)]
mod tests {
use super::*;
fn create_default_config() -> AgentServersConfig {
// 创建测试用的默认配置
AgentServersConfig::default()
}
#[test]
fn test_system_prompt_override() {
let assembler = PromptConfigAssembler::new(create_default_config())
.with_system_prompt(Some("自定义系统提示词".to_string()));
let result = assembler.get_system_prompt("claude-code-acp");
assert_eq!(result, "自定义系统提示词");
}
#[test]
fn test_system_prompt_use_default() {
let assembler = PromptConfigAssembler::new(create_default_config());
// 应该使用默认配置
let result = assembler.get_system_prompt("claude-code-acp");
// 验证使用了默认配置
}
#[test]
fn test_user_prompt_template() {
let assembler = PromptConfigAssembler::new(create_default_config())
.with_user_prompt_template(Some("请用中文回答:{user_prompt}".to_string()));
let result = assembler.apply_user_prompt("claude-code-acp", "hello world");
assert_eq!(result, "请用中文回答hello world");
}
#[test]
fn test_user_prompt_no_template() {
let assembler = PromptConfigAssembler::new(create_default_config());
let result = assembler.apply_user_prompt("claude-code-acp", "hello world");
assert_eq!(result, "hello world");
}
#[test]
fn test_agent_server_config_merge() {
let override_config = ChatAgentConfig {
agent_server: Some(ChatAgentServerConfig {
env: Some(HashMap::from([
("RUST_LOG".to_string(), "debug".to_string()),
])),
..Default::default()
}),
context_servers: HashMap::new(),
};
let assembler = PromptConfigAssembler::new(create_default_config())
.with_agent_config(Some(override_config));
let result = assembler.get_agent_server_config("claude-code-acp");
// 验证 env 被正确合并
assert!(result.env.contains_key("RUST_LOG"));
}
#[test]
fn test_context_servers_override() {
let mut context_servers = HashMap::new();
context_servers.insert("my-mcp".to_string(), ChatContextServerConfig {
source: "custom".to_string(),
enabled: true,
command: Some("bunx".to_string()),
args: Some(vec!["-y".to_string(), "my-mcp-server".to_string()]),
env: None,
});
let override_config = ChatAgentConfig {
agent_server: None,
context_servers,
};
let assembler = PromptConfigAssembler::new(create_default_config())
.with_agent_config(Some(override_config));
let result = assembler.get_context_servers();
assert!(result.contains_key("my-mcp"));
}
}
```
---
### 5.1.3 集成测试
**文件**: `tests/integration/chat_config_test.rs` (或在现有测试文件中添加)
**测试用例**:
```rust
#[tokio::test]
async fn test_chat_with_system_prompt() {
// 测试只传 system_prompt
let response = client
.post("/chat")
.json(&json!({
"prompt": "hello",
"project_id": "test",
"system_prompt": "你是 Rust 专家"
}))
.send()
.await
.unwrap();
assert!(response.status().is_success());
}
#[tokio::test]
async fn test_chat_with_user_prompt_template() {
// 测试 user_prompt 模板替换
let response = client
.post("/chat")
.json(&json!({
"prompt": "hello world",
"project_id": "test",
"user_prompt": "请用中文回答:{user_prompt}"
}))
.send()
.await
.unwrap();
assert!(response.status().is_success());
}
#[tokio::test]
async fn test_chat_with_agent_config() {
// 测试完整的 agent_config
let response = client
.post("/chat")
.json(&json!({
"prompt": "hello",
"project_id": "test",
"agent_config": {
"agent_server": {
"env": {"RUST_LOG": "debug"}
},
"context_servers": {
"context7": {
"enabled": true,
"command": "bunx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}
}))
.send()
.await
.unwrap();
assert!(response.status().is_success());
}
```
---
## 6. 实现检查清单
### 6.1 第一批完成检查
- [ ] `ChatAgentConfig` 结构体创建完成
- [ ] `ChatAgentServerConfig` 结构体创建完成
- [ ] `ChatContextServerConfig` 结构体创建完成
- [ ] `shared_types` 导出新类型
- [ ] `agent.proto` 添加新消息定义
- [ ] HTTP `ChatRequest` 添加新字段
- [ ] gRPC 客户端传递新字段
- [ ] `cargo build` 全部通过
### 6.2 第二批完成检查
- [ ] `PromptConfigAssembler` 创建完成
- [ ] `agent_config` 导出新模块
- [ ] `ChatPrompt` 添加新字段
- [ ] `PromptMessage` 添加新字段
- [ ] `From` 转换实现完成
- [ ] `cargo build` 全部通过
### 6.3 第三批完成检查
- [ ] `AgentServiceImpl::chat()` 处理新字段
- [ ] gRPC 类型转换实现完成
- [ ] `AcpAgentWorker` 使用配置组装器
- [ ] `ClaudeCodeLauncher` 支持配置覆盖
- [ ] `cargo build` 全部通过
### 6.4 第四批完成检查
- [ ] `ChatAgentConfig` 单元测试通过
- [ ] `PromptConfigAssembler` 单元测试通过
- [ ] 集成测试通过
- [ ] OpenAPI 文档更新
- [ ] `cargo test` 全部通过
---
## 7. 风险和注意事项
### 7.1 向后兼容性
- 所有新字段都是 `Option` 类型,确保旧客户端正常工作
- proto 字段编号从 8 开始,不影响现有字段
### 7.2 类型转换
- 注意 gRPC 生成的类型和 Rust 原生类型的转换
- `repeated` 字段为空时转换为 `None`
- `map` 字段为空时转换为 `None`
### 7.3 配置合并逻辑
- `env``metadata` 是合并(入参优先)
- `args` 是替换(入参完全覆盖)
- 提示词是覆盖(入参有值则使用入参)
### 7.4 测试覆盖
- 确保测试覆盖所有配置组合
- 特别注意边界情况:空字符串、空 HashMap 等
---
## 8. 预计工作量
| 批次 | 预计工作量 | 依赖 |
|------|-----------|------|
| 第一批 | 2-3 小时 | 无 |
| 第二批 | 2-3 小时 | 第一批 |
| 第三批 | 2-3 小时 | 第二批 |
| 第四批 | 1-2 小时 | 第三批 |
**总计**: 约 7-11 小时

View File

@@ -0,0 +1,21 @@
# Instructions
## project alpha 需求和设计文档
### “/chat”接口增加可选配置入参,入参如下:
1. `system_prompt` 系统提示词,可选
2. `user_prompt` 用户提示词,可选
3. `agent_config` agent配置文件,包含agent使用的mcp配置,自定义agent配置文件,可选
参考默认配置json文件: @crates/agent_config/configs/default_agents.json ,对应的结构体 `AgentServersConfig`
如果不传,使用默认的agent,传了就使用给的agent配置来启动agent服务进行使用。
细节补充说明:
* 如果 `system_prompt`,`user_prompt`都传值了,非空字符串,但 `agent_config` 里对应的同名字段也有配置(同名字段下有`template`字段,定义的是提示词模板),认入参 `system_prompt`,`user_prompt`的为准,覆盖`agent_config` 里的同名配置
* `user_prompt` 提示词模板文本内容, 可以注入的变量 `{user_prompt}`,用户"{}"花括号匹配,把”/chat”入参字段`prompt`,注入进去,替换变量 `{user_prompt}`
按照这个想法,帮我生成详细的需求和设计文档,放在 @specs/agent-config-instruction-spec.md 文件中,输出为中文。
## implementation plan
按照 @specs/agent-config-instruction-spec.md 中的需求和设计文档,生成一个详细的实现计划,放在 @specs/02-agent-config-instruction-spec-plan.md 中,输出中文

View File

@@ -0,0 +1,596 @@
# Agent Resume via ACP 实现计划
## 方案概述
采用 **方式一:通过 NewSessionRequest 的 meta 参数** 实现 resume 功能。
核心思路:统一使用 `NewSessionRequest` + `_meta.claudeCode.options.resume` 传递 session_id移除冗余的 `load_session` 尝试逻辑。
**重要**Resume 是"尝试性"的,如果 session 不存在导致 Agent 启动失败,会自动降级为不传 resume 参数,创建新会话。
---
## Resume 失败降级方案(核心机制)
### 问题场景
当用户传入的 `session_id` 对应的会话不存在时已过期、被清理、或从未存在Agent 会启动失败:
```
No conversation found for session id: abc-123
```
### 降级策略
**优先尝试 resume失败则降级为新会话**
```
第一次尝试:带 resume 参数
├─ 成功 → 恢复上下文,继续对话
└─ 失败(任何原因)
第二次尝试:不带 resume 参数
└─ 成功 → 创建新会话
```
**关键点**:不需要判断具体错误原因,只要 `has_resume && 启动失败`,就直接降级重试。
### 当前实现位置(需改动)
**文件**: `session_manager.rs:217-261`
**当前代码**(需简化):
```rust
// 检查是否因为 resume 导致的失败
if has_resume
&& (error_msg.contains("No conversation found")
|| error_msg.contains("session")
|| error_msg.contains("exited with code 1")) // ← 删除这些判断
{
// 降级...
}
```
**改动后代码**:
```rust
// 只要带 resume 且启动失败,就降级重试
if has_resume {
tracing::warn!(
"⚠️ Agent 启动失败(带 resume降级为不使用 resume 重试: error={}",
error_msg
);
// 创建新的 config不包含 resume_session_id
let retry_config = AgentStartConfig {
system_prompt: start_config.system_prompt,
mcp_servers: start_config.mcp_servers,
extra_meta: start_config.extra_meta,
service_type: start_config.service_type,
resume_session_id: None, // ← 关键:去掉 resume
};
tracing::info!("🔄 重试启动 Agent不使用 resume");
// 第二次尝试:不带 resume
launcher.launch(..., retry_config, ...).await?
} else {
// 不带 resume 的失败,直接返回错误
return Err(e);
}
```
### 降级流程图
```
┌─────────────────────────────────────────────────────────────────┐
│ ChatRequest { session_id: "abc-123" } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ acp_worker.rs │
│ └─ AgentStartConfig { resume_session_id: Some("abc-123") } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ session_manager.rs │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 第一次尝试 │ │
│ │ launcher.launch(start_config) │ │
│ │ → meta: { claudeCode.options.resume: "abc-123" } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────────┐ │
│ │ ✅ 成功 │ │ ❌ 失败 │ │
│ │ session 存在 │ │ "No conversation │ │
│ │ 恢复上下文 │ │ found" │ │
│ └────────────────┘ └────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 第二次尝试(降级) │ │
│ │ retry_config.resume_session_id = None │ │
│ │ launcher.launch(retry_config) │ │
│ │ → meta: { } ← 不包含 resume │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ ✅ 成功 │ │
│ │ 创建新会话 │ │
│ │ 返回新 session │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 本次改动对降级机制的影响
**需要简化**。移除错误关键字判断,改为:只要 `has_resume && 启动失败` 就降级。
| 层级 | 改动 | 降级机制 |
|------|------|----------|
| session_manager.rs | 简化降级判断逻辑 | **需改动** |
| claude_code_launcher.rs | 简化会话创建逻辑 | 不涉及 |
---
## 当前代码状态分析
### ✅ 已正确实现的部分
| 文件 | 功能 | 状态 |
|------|------|------|
| `agent.rs:95-137` | `AgentStartConfig.build_meta()` 构建 `claudeCode.options.resume` | ✅ 完成 |
| `acp_worker.rs:158-180` | 判断是否需要 resume 并设置 `resume_session_id` | ✅ 完成 |
| `session_manager.rs:217-261` | Resume 失败降级重试机制(框架已完成,判断逻辑需简化) | ⚠️ 需简化 |
### ❌ 需要改动的部分
| 文件 | 问题 | 改动 |
|------|------|------|
| `claude_code_launcher.rs:438-480` | 冗余的 `load_session` 尝试逻辑 | 简化为统一 `new_session` |
| `claude_code_launcher.rs:291-299` | `session_id` 参数冗余 | 移除此参数 |
| `session_manager.rs:207-214, 247-254` | 调用 launch 时传递 `session_id_hint` | 移除此参数 |
| `session_manager.rs:224-227` | 降级判断过于复杂(检测错误关键字) | 简化为 `if has_resume` |
---
## 改动任务清单
### Task 0: 简化 session_manager.rs 降级判断逻辑(新增)
**文件**: `crates/agent_abstraction/src/session/session_manager.rs`
**改动位置**: 约 223-228 行
**当前代码**:
```rust
// 检查是否因为 resume 导致的失败
if has_resume
&& (error_msg.contains("No conversation found")
|| error_msg.contains("session")
|| error_msg.contains("exited with code 1"))
{
```
**改动后代码**:
```rust
// 只要带 resume 且启动失败,就降级重试(不判断具体错误原因)
if has_resume {
```
**同时更新日志信息**:
```rust
tracing::warn!(
"⚠️ Agent 启动失败(带 resume降级为不使用 resume 重试: error={}",
error_msg
);
```
---
### Task 1: 简化 claude_code_launcher.rs 会话创建逻辑
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
**改动位置**: 约 438-480 行
**当前代码**:
```rust
// 创建会话
let session_id = match session_id_for_closure {
Some(sid) => {
debug!("尝试加载 ACP 会话[load_session]");
let given_session_id = SessionId::new(sid);
match client_conn
.load_session(LoadSessionRequest::new(
given_session_id.clone(),
project_path_for_closure.clone(),
))
.await
{
Ok(resp) => {
debug!("ACP 会话加载成功[load_session],{:?}", resp);
given_session_id
}
Err(e) => {
warn!(
"load_session 失败或未实现,回退创建新会话[new_session]: {:?}",
e
);
// 注意:即使 load_session 失败,仍然会创建 new_session
// resume_session_id 会通过 meta.claudeCode.options.resume 传递
let new_session_request =
NewSessionRequest::new(project_path_for_closure.clone())
.mcp_servers(mcp_servers.clone())
.meta(system_prompt_meta.clone());
let resp = client_conn.new_session(new_session_request).await?;
debug!("ACP 会话创建成功[new_session],{:?}", resp);
resp.session_id
}
}
}
None => {
debug!("创建 ACP 会话[new_session]");
let new_session_request =
NewSessionRequest::new(project_path_for_closure.clone())
.mcp_servers(mcp_servers)
.meta(system_prompt_meta);
let resp = client_conn.new_session(new_session_request).await?;
debug!("ACP 会话创建成功[new_session],{:?}", resp);
resp.session_id
}
};
```
**改动后代码**:
```rust
// 创建会话(统一使用 new_sessionresume 通过 meta 传递)
// 如果 start_config.resume_session_id 有值build_meta() 会自动构建
// _meta.claudeCode.options.resume 结构
debug!("创建 ACP 会话[new_session]");
let new_session_request = NewSessionRequest::new(project_path_for_closure.clone())
.mcp_servers(mcp_servers)
.meta(system_prompt_meta);
let resp = client_conn
.new_session(new_session_request)
.await
.context("ACP 会话创建失败")?;
debug!(
"ACP 会话创建成功[new_session], session_id={}",
resp.session_id.0
);
let session_id = resp.session_id;
```
**同时需要移除的变量**:
```rust
// 约 317 行,移除这行
let session_id_for_closure = session_id.clone();
```
---
### Task 2: 移除 launch 方法的 session_id 参数
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
**改动位置**: 约 279-299 行(方法签名和文档)
**当前代码**:
```rust
/// 启动 Claude Code ACP Agent 服务
///
/// # 参数
/// - `project_id`: 项目 ID
/// - `project_path`: 项目工作目录
/// - `session_id`: 可选的会话 ID用于恢复会话
/// - `model_provider`: 模型提供商配置
/// - `start_config`: Agent 启动配置(包含系统提示词等)
/// - `client`: ACP 客户端实现
///
/// # 返回值
/// 返回 LauncherConnectionInfoComplete包含会话信息和生命周期守卫
pub async fn launch(
&self,
project_id: String,
project_path: PathBuf,
session_id: Option<String>,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
client: C,
) -> Result<LauncherConnectionInfoComplete> {
```
**改动后代码**:
```rust
/// 启动 Claude Code ACP Agent 服务
///
/// # 参数
/// - `project_id`: 项目 ID
/// - `project_path`: 项目工作目录
/// - `model_provider`: 模型提供商配置
/// - `start_config`: Agent 启动配置包含系统提示词、resume_session_id 等)
/// - `client`: ACP 客户端实现
///
/// # Resume 机制
/// 如果需要恢复会话,通过 `start_config.resume_session_id` 传递 session_id
/// 会自动构建 `_meta.claudeCode.options.resume` 结构传递给 Agent。
///
/// # 返回值
/// 返回 LauncherConnectionInfoComplete包含会话信息和生命周期守卫
pub async fn launch(
&self,
project_id: String,
project_path: PathBuf,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
client: C,
) -> Result<LauncherConnectionInfoComplete> {
```
---
### Task 3: 更新 session_manager.rs 的 launch 调用
**文件**: `crates/agent_abstraction/src/session/session_manager.rs`
**改动位置 1**: 约 207-214 行(第一次调用)
**当前代码**:
```rust
let result = launcher
.launch(
project_id.clone(),
project_path.clone(),
session_id_hint.clone(),
model_provider.clone(),
start_config.clone(),
client,
)
.await;
```
**改动后代码**:
```rust
let result = launcher
.launch(
project_id.clone(),
project_path.clone(),
model_provider.clone(),
start_config.clone(),
client,
)
.await;
```
**改动位置 2**: 约 247-254 行(降级重试调用)
**当前代码**:
```rust
launcher
.launch(
project_id.clone(),
project_path,
session_id_hint,
model_provider.clone(),
retry_config,
C::default(),
)
.await?
```
**改动后代码**:
```rust
launcher
.launch(
project_id.clone(),
project_path,
model_provider.clone(),
retry_config,
C::default(),
)
.await?
```
---
### Task 4: 清理 session_manager.rs 的方法签名(可选)
**文件**: `crates/agent_abstraction/src/session/session_manager.rs`
**改动位置**: `create_new_session` 方法签名(约 185-196 行)
**当前代码**:
```rust
pub async fn create_new_session(
&self,
project_id: String,
project_path: PathBuf,
session_id_hint: Option<String>,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
client: C,
) -> Result<Arc<SessionInfo>> {
```
**评估**:
- `session_id_hint` 参数当前未被使用(只是传递给 launcher
- 可以移除,但需要检查 `get_or_create_session` 等上游调用方
- **建议**: 暂时保留此参数,避免过大改动范围
---
### Task 5: 移除未使用的 import
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
**改动位置**: 约 11-14 行
**当前代码**:
```rust
use agent_client_protocol::{
Agent, Client, ClientSideConnection, Implementation, InitializeRequest, LoadSessionRequest,
McpServer, McpServerStdio, NewSessionRequest, PromptRequest, SessionId,
};
```
**改动后代码**:
```rust
use agent_client_protocol::{
Agent, Client, ClientSideConnection, Implementation, InitializeRequest,
McpServer, McpServerStdio, NewSessionRequest, PromptRequest, SessionId,
};
```
---
## 改动验证清单
### 编译验证
- [ ] `cargo build -p agent_abstraction` 编译通过
- [ ] `cargo build --workspace` 全项目编译通过
- [ ] `cargo clippy` 无新增警告
### 单元测试
- [ ] `AgentStartConfig.build_meta()` 测试用例通过
- [ ] 现有测试用例通过
### 集成测试
- [ ] 新建会话:正常创建
- [ ] Resume 会话:传入有效 session_id恢复上下文
- [ ] Resume 失败降级:传入无效 session_id自动降级为新会话
---
## 实现顺序
```
Step 1: Task 0 - 简化 session_manager.rs 降级判断逻辑
└─ 移除错误关键字判断,改为 if has_resume
Step 2: Task 1 - 简化 claude_code_launcher.rs 会话创建逻辑
├─ 移除 load_session 尝试
├─ 统一使用 new_session
└─ 移除 session_id_for_closure 变量
Step 3: Task 2 - 移除 launch 方法的 session_id 参数
└─ 更新方法签名和文档
Step 4: Task 3 - 更新 session_manager.rs 的 launch 调用
├─ 更新第一次 launch 调用
└─ 更新降级重试调用
Step 5: Task 5 - 清理未使用的 import
└─ 移除 LoadSessionRequest
Step 6: 验证
├─ 编译验证
├─ 运行测试
└─ 手动测试 resume 流程
```
---
## 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| 移除 session_id 参数导致编译错误 | 低 | 低 | 搜索所有调用方并更新 |
| Resume 功能回归 | 低 | 中 | 简化降级机制更健壮 |
| 逻辑改动影响其他功能 | 低 | 中 | 充分测试 |
---
## 预估工作量
| Task | 预估代码行数 | 复杂度 |
|------|-------------|--------|
| Task 0 | -4, +2 | 低 |
| Task 1 | -30, +15 | 低 |
| Task 2 | -2, +5 | 低 |
| Task 3 | -2, +0 | 低 |
| Task 5 | -1, +0 | 低 |
| **总计** | **约 -39 行,+22 行** | **低** |
---
## 附录:完整数据流(改动后)
```
┌─────────────────────────────────────────────────────────────────┐
│ HTTP/gRPC Request │
│ ChatRequest { session_id: "abc-123", prompt: "..." } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ acp_worker.rs │
│ ├─ 检查内存中是否存在会话 │
│ │ └─ 存在且 session_id 匹配 → with_resume_session_id() │
│ └─ 构建 AgentStartConfig { resume_session_id: Some("abc-123") }│
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ session_manager.rs │
│ └─ launcher.launch(project_id, project_path, │
│ model_provider, start_config, client) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ claude_code_launcher.rs │
│ ├─ start_config.build_meta() 构建: │
│ │ { │
│ │ "claudeCode": { │
│ │ "options": { "resume": "abc-123" } │
│ │ } │
│ │ } │
│ └─ NewSessionRequest::new(cwd).meta(meta) ← 统一入口 │
└─────────────────────────────────────────────────────────────────┘
▼ ACP session/new
┌─────────────────────────────────────────────────────────────────┐
│ claude-code-acp │
│ ├─ newSession() 解析 _meta.claudeCode.options.resume │
│ └─ query({ options: { resume: "abc-123" } }) │
└─────────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌────────────────┐ ┌────────────────────────┐
│ ✅ 会话存在 │ │ ❌ 会话不存在 │
│ → 恢复上下文 │ │ → 抛出错误 │
│ → 返回成功 │ │ "No conversation found"│
└────────────────┘ └────────────────────────┘
┌─────────────────────────────────────────────────┐
│ session_manager.rs 降级处理 │
│ ├─ 检测错误关键字 │
│ ├─ 创建 retry_config { resume_session_id: None }│
│ └─ 第二次 launcher.launch(retry_config) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ ✅ 成功创建新会话 │
│ └─ 返回新的 session_id │
└─────────────────────────────────────────────────┘
```

View File

@@ -0,0 +1,592 @@
# Agent Resume via ACP 设计方案
## 1. 背景
### 1.1 需求描述
通过 ACP 协议与 Agent 对话时,如果 Agent 停止后,希望能通过 `resume` 参数继续之前的上下文对话记录,与 Agent 继续对话。
### 1.2 相关资源
- **claude-code-acp**: Zed 公司对 Claude Code 的 ACP 协议封装(参考:`tmp/claude-code-acp`
- **rust-sdk**: ACP 协议的 Rust SDK参考`tmp/rust-sdk`
- **当前实现**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
## 2. 技术分析
### 2.1 ACP 协议中的会话恢复方式
ACP 协议定义了多种会话管理方法:
| 方法 | 功能 | 状态 |
|------|------|------|
| `session/new` (NewSessionRequest) | 创建新会话 | 稳定 |
| `session/load` (LoadSessionRequest) | 加载已存在的会话并重放历史 | 稳定,但 claude-code-acp 未实现 |
| `session/resume` (ResumeSessionRequest) | 恢复会话(不重放历史)| unstable需要 `unstable_session_resume` feature |
| `session/fork` (ForkSessionRequest) | 分叉会话 | unstable |
### 2.2 claude-code-acp 的 Resume 实现机制
通过分析 `tmp/claude-code-acp/src/acp-agent.ts`,发现 claude-code-acp 支持三种 resume 方式:
#### 方式一:通过 NewSessionRequest 的 meta 参数
```typescript
// acp-agent.ts:205-216
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
return await this.createSession(params, {
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume,
});
}
```
**传参结构**
```json
{
"cwd": "/path/to/project",
"_meta": {
"claudeCode": {
"options": {
"resume": "previous-session-id"
}
}
}
}
```
#### 方式二:通过 unstable_resumeSession 方法
```typescript
// acp-agent.ts:233-246
async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
return await this.createSession(
{ cwd: params.cwd, mcpServers: params.mcpServers ?? [], _meta: params._meta },
{ resume: params.sessionId }
);
}
```
#### 方式三:通过 load_sessionclaude-code-acp 不支持)
```typescript
// 返回 Error::method_not_found()
```
### 2.3 createSession 内部 Resume 处理逻辑
```typescript
// acp-agent.ts:593-709
private async createSession(
params: NewSessionRequest,
creationOpts: { resume?: string; forkSession?: boolean } = {},
): Promise<NewSessionResponse> {
// 1. 确定 sessionId
let sessionId;
if (creationOpts.forkSession) {
sessionId = randomUUID();
} else if (creationOpts.resume) {
sessionId = creationOpts.resume; // 👈 使用传入的 session_id
} else {
sessionId = randomUUID();
}
// 2. 构建 extraArgs
const extraArgs = { ...userProvidedOptions?.extraArgs };
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
extraArgs["session-id"] = sessionId; // 👈 新会话才设置 session-id
}
// 3. 构建 Options 传递给 SDK
const options: Options = {
// ... 其他配置
extraArgs,
...creationOpts, // 👈 包含 { resume: sessionId }
};
// 4. 调用 SDK 的 query 函数
const q = query({ prompt: input, options });
}
```
**关键发现**
1. `resume` 参数最终通过 `Options` 传递给 `@anthropic-ai/claude-agent-sdk``query()` 函数
2. SDK 会根据 `Options.resume` 自动恢复之前的会话上下文
3. 当 resume 有值时,不设置新的 `session-id` extraArg
### 2.4 当前项目的实现分析
#### 当前流程 (`claude_code_launcher.rs:438-480`)
```rust
let session_id = match session_id_for_closure {
Some(sid) => {
// 尝试 load_session
match client_conn.load_session(LoadSessionRequest::new(...)).await {
Ok(resp) => given_session_id,
Err(e) => {
// 失败时回退到 new_session
// resume_session_id 通过 meta.claudeCode.options.resume 传递
let new_session_request = NewSessionRequest::new(...)
.mcp_servers(mcp_servers)
.meta(system_prompt_meta); // 👈 包含 resume
client_conn.new_session(new_session_request).await?
}
}
}
None => {
// 创建新会话
client_conn.new_session(NewSessionRequest::new(...)).await?
}
};
```
#### 问题分析
1. **冗余逻辑**:先尝试 `load_session`(必定失败),然后回退到 `new_session`
2. **混淆概念**`session_id` 参数和 `resume_session_id` 参数的关系不清晰
3. **缺少 ACP 原生 resume**:未使用 `ResumeSessionRequest`
## 3. 设计方案
### 3.1 方案选择
推荐使用 **方式一:通过 NewSessionRequest 的 meta 参数** 实现 resume
**理由**
1. **稳定性**:不依赖 unstable feature
2. **兼容性**:适用于所有支持 ACP 的 Agent
3. **当前实现已支持**`AgentStartConfig.build_meta()` 已经正确构建了 `claudeCode.options.resume` 结构
### 3.2 核心改动
#### 3.2.1 简化 claude_code_launcher.rs 的会话创建逻辑
**改动位置**`crates/agent_abstraction/src/compat/claude_code_launcher.rs:438-480`
**改动前**
```rust
let session_id = match session_id_for_closure {
Some(sid) => {
// 先尝试 load_session失败后再 new_session
// ...
}
None => {
// new_session
}
};
```
**改动后**
```rust
// 统一使用 new_sessionresume 通过 meta 传递
let new_session_request = NewSessionRequest::new(project_path_for_closure.clone())
.mcp_servers(mcp_servers)
.meta(system_prompt_meta); // 已包含 claudeCode.options.resume
let session_id = client_conn
.new_session(new_session_request)
.await?
.session_id;
```
#### 3.2.2 参数语义调整
| 参数 | 原语义 | 新语义 |
|------|--------|--------|
| `session_id: Option<String>` (launch 方法) | 传入则尝试 load_session | **移除**,不再使用 |
| `AgentStartConfig.resume_session_id` | 通过 meta 传递 | **保持**,作为唯一的 resume 参数来源 |
#### 3.2.3 数据流示意图
```
┌──────────────────────────────────────────────────────────────────────────┐
│ RCoder / Agent Runner │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ ChatPrompt │
│ ├─ session_id: "abc-123" (用于标识当前对话,可能是新的或历史的) │
│ └─ ... │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ acp_worker.rs │
│ ├─ 检查会话是否需要 resume │
│ │ └─ 如果 session_id 匹配已存在的会话 → 设置 resume_session_id │
│ └─ 构建 AgentStartConfig │
│ └─ resume_session_id: Some("abc-123") │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ session_manager.rs │
│ └─ 调用 ClaudeCodeLauncher::launch() │
│ └─ start_config: AgentStartConfig { resume_session_id, ... } │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ claude_code_launcher.rs │
│ ├─ start_config.build_meta() 构建: │
│ │ { │
│ │ "systemPrompt": { "append": "..." }, │
│ │ "claudeCode": { │
│ │ "options": { │
│ │ "resume": "abc-123" ← resume_session_id │
│ │ } │
│ │ } │
│ │ } │
│ └─ NewSessionRequest::new(...).meta(meta) │
└──────────────────────────────────────────────────────────────────────────┘
▼ ACP 协议 (session/new)
┌──────────────────────────────────────────────────────────────────────────┐
│ claude-code-acp (Agent) │
│ ├─ newSession() 解析 _meta.claudeCode.options.resume │
│ ├─ createSession(params, { resume: "abc-123" }) │
│ └─ query({ options: { resume: "abc-123", ... } }) │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ @anthropic-ai/claude-agent-sdk │
│ └─ 根据 Options.resume 恢复之前的会话上下文 │
└──────────────────────────────────────────────────────────────────────────┘
```
### 3.3 详细代码改动
#### 文件 1: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
```rust
// 改动 launch 方法签名,移除 session_id 参数
pub async fn launch(
&self,
project_id: String,
project_path: PathBuf,
// session_id: Option<String>, // 👈 移除此参数
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
client: C,
) -> Result<LauncherConnectionInfoComplete> {
```
```rust
// 简化会话创建逻辑(约 438-480 行)
// 改动前:
// let session_id = match session_id_for_closure { ... }
// 改动后:
debug!("创建 ACP 会话[new_session]");
let new_session_request = NewSessionRequest::new(project_path_for_closure.clone())
.mcp_servers(mcp_servers)
.meta(system_prompt_meta); // 已包含 resume 信息
let session_id = client_conn
.new_session(new_session_request)
.await
.context("ACP 会话创建失败")?
.session_id;
debug!("ACP 会话创建成功[new_session], session_id={}", session_id.0);
```
#### 文件 2: `crates/agent_abstraction/src/session/session_manager.rs`
```rust
// 更新 launch 调用,移除 session_id 参数
let result = launcher
.launch(
project_id.clone(),
project_path.clone(),
// None, // 👈 移除 session_id 参数
model_provider.clone(),
start_config.clone(),
acp_client,
)
.await;
```
### 3.4 Resume 失败降级机制
#### 3.4.1 问题场景
当传入的 `session_id` 对应的会话不存在时例如会话历史已过期、被清理、或从未存在Agent 会启动失败并抛出错误。
**典型错误信息**
```
No conversation found for session id: abc-123
```
```
exited with code 1
```
#### 3.4.2 当前降级实现
降级逻辑已在 `session_manager.rs:217-261` 实现:
```rust
// session_manager.rs
let connection_info = match result {
Ok(info) => info,
Err(e) => {
let error_msg = format!("{:?}", e);
// 检查是否因为 resume 导致的失败
if has_resume
&& (error_msg.contains("No conversation found")
|| error_msg.contains("session")
|| error_msg.contains("exited with code 1"))
{
tracing::warn!(
"⚠️ Agent 启动失败(可能因 --resume重试不使用 --resume: error={}",
error_msg
);
// 创建新的 config不包含 resume_session_id
let retry_config = AgentStartConfig {
system_prompt: start_config.system_prompt,
mcp_servers: start_config.mcp_servers,
extra_meta: start_config.extra_meta,
service_type: start_config.service_type,
resume_session_id: None, // ✅ 去掉 resume
};
tracing::info!("🔄 重试启动 Agent不使用 --resume");
// 重试启动
launcher.launch(..., retry_config, ...).await?
} else {
// 其他错误,直接返回
return Err(e);
}
}
};
```
#### 3.4.3 降级流程图
```
┌─────────────────────────────────────────────────────────────────┐
│ ChatRequest { session_id: "abc-123" } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ session_manager.rs │
│ └─ 第一次尝试start_config.resume_session_id = Some("abc-123")│
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ claude_code_launcher.launch() │
│ └─ NewSessionRequest + meta { claudeCode.options.resume } │
└─────────────────────────────────────────────────────────────────┘
▼ ACP 协议
┌─────────────────────────────────────────────────────────────────┐
│ claude-code-acp │
│ └─ SDK query({ options: { resume: "abc-123" } }) │
└─────────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
│ │
▼ ▼
┌────────────────┐ ┌────────────────────────┐
│ 会话存在 │ │ 会话不存在 │
│ → 恢复上下文 │ │ → 抛出错误 │
│ → 返回成功 │ │ "No conversation found"│
└────────────────┘ └────────────────────────┘
┌─────────────────────────────────────────┐
│ session_manager.rs 降级处理 │
│ ├─ 检测错误关键字 │
│ ├─ 创建新 config: resume_session_id=None│
│ └─ 第二次尝试:不带 resume │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ claude_code_launcher.launch() │
│ └─ NewSessionRequest (无 resume) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 成功创建新会话 │
│ └─ 返回新的 session_id │
└─────────────────────────────────────────┘
```
#### 3.4.4 错误检测关键字
当前实现检测以下关键字来判断是否因 resume 失败:
| 关键字 | 来源 | 说明 |
|--------|------|------|
| `"No conversation found"` | SDK 错误 | 明确表示会话不存在 |
| `"session"` | 通用匹配 | 捕获其他 session 相关错误 |
| `"exited with code 1"` | 进程退出 | Agent 进程异常退出 |
**建议改进**:可以增加更精确的错误码匹配,例如:
- `"ENOENT"` - 会话文件不存在
- `"invalid session"` - 无效会话
#### 3.4.5 降级策略配置(可选扩展)
未来可以考虑增加降级策略配置:
```rust
pub struct ResumePolicy {
/// 是否启用降级
pub enable_fallback: bool,
/// 最大重试次数
pub max_retries: u32,
/// 降级时是否保留部分配置(如 MCP 服务器)
pub preserve_mcp_servers: bool,
}
```
### 3.5 备选方案:使用 ResumeSessionRequest (unstable)
如果需要使用 ACP 原生的 resume 方法,需要:
1. **启用 unstable feature**
```toml
# Cargo.toml
agent-client-protocol = { version = "0.6", features = ["unstable_session_resume"] }
```
2. **添加 resume_session 调用**
```rust
#[cfg(feature = "unstable_session_resume")]
async fn resume_session(&self, session_id: SessionId, cwd: PathBuf) -> Result<SessionId> {
let request = ResumeSessionRequest::new(session_id.clone(), cwd);
let response = self.client_conn.resume_session(request).await?;
Ok(response.session_id)
}
```
**不推荐此方案**
- 依赖 unstable API可能随时变更
- claude-code-acp 的 `unstable_resumeSession` 内部也是通过 `createSession` 处理,效果相同
## 4. API 变更
### 4.1 内部 API 变更
| 组件 | 改动 |
|------|------|
| `ClaudeCodeLauncher::launch()` | 移除 `session_id` 参数 |
| `SessionManager` | 调用 launch 时不再传递 session_id |
### 4.2 外部 API 无变更
gRPC `ChatRequest` 和 HTTP API 保持不变,`session_id` 字段继续用于:
1. 标识会话(用于进度订阅、取消等)
2. 决定是否需要 resume`acp_worker.rs` 中判断)
## 5. 测试计划
### 5.1 单元测试
- [ ] `AgentStartConfig.build_meta()` 正确构建 resume 结构
- [ ]`resume_session_id` 为 None 时meta 不包含 claudeCode 字段
### 5.2 集成测试
- [ ] 新建会话:不传 session_id正常创建
- [ ] Resume 会话:传入历史 session_id能够继续对话
- [ ] Resume 失败降级:传入无效 session_id能够降级为新会话
### 5.3 降级场景测试
#### 场景 1会话不存在
```bash
# 传入一个不存在的 session_id
curl -X POST /chat -d '{
"project_id": "proj1",
"session_id": "non-existent-session-id",
"prompt": "Hello"
}'
# 预期:
# 1. 第一次尝试失败,日志显示 "No conversation found"
# 2. 自动降级,去掉 resume 重试
# 3. 成功创建新会话,返回新的 session_id
```
#### 场景 2会话已过期
```bash
# 使用一个曾经存在但已过期/清理的 session_id
curl -X POST /chat -d '{
"project_id": "proj1",
"session_id": "expired-session-id",
"prompt": "继续之前的对话"
}'
# 预期:同场景 1自动降级为新会话
```
#### 场景 3会话存在且有效
```bash
# 第一次对话
curl -X POST /chat -d '{"project_id":"proj1", "prompt":"记住数字 42"}'
# 返回 session_id: "valid-session-123"
# 第二次对话resume
curl -X POST /chat -d '{
"project_id": "proj1",
"session_id": "valid-session-123",
"prompt": "我之前说的数字是多少?"
}'
# 预期Agent 回答 "42"(证明上下文已恢复)
```
### 5.4 手动测试场景
```bash
# 1. 首次对话
curl -X POST /chat -d '{"project_id":"proj1", "prompt":"Hello"}'
# 返回 session_id: "sess-abc-123"
# 2. 继续对话resume
curl -X POST /chat -d '{"project_id":"proj1", "session_id":"sess-abc-123", "prompt":"继续刚才的话题"}'
# Agent 应该能够记住之前的上下文
```
## 6. 风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| SDK 不支持 resume | Agent 无法恢复上下文 | 当前 claude-code-acp 已支持 |
| session_id 过期/无效 | resume 失败 | 已有降级逻辑,自动创建新会话 |
| 协议变更 | resume 字段位置变化 | 跟踪 claude-code-acp 更新 |
## 7. 实现步骤
1. **Phase 1**:简化 `claude_code_launcher.rs`
- 移除 `load_session` 尝试逻辑
- 统一使用 `new_session` + meta
2. **Phase 2**:清理接口
- 移除 launch 方法的 `session_id` 参数
- 更新所有调用方
3. **Phase 3**:测试验证
- 添加单元测试
- 执行集成测试
## 8. 附录
### 8.1 相关代码位置
| 文件 | 行号 | 描述 |
|------|------|------|
| `crates/agent_abstraction/src/compat/claude_code_launcher.rs` | 291-573 | launch 方法 |
| `crates/agent_abstraction/src/traits/agent.rs` | 95-142 | `AgentStartConfig.build_meta()` |
| `crates/agent_abstraction/src/session/acp_worker.rs` | 164-167 | resume 判断逻辑 |
| `crates/agent_abstraction/src/session/session_manager.rs` | 200-260 | launcher 调用 |
| `crates/agent_abstraction/src/session/session_manager.rs` | 217-261 | **Resume 失败降级逻辑** |
| `tmp/claude-code-acp/src/acp-agent.ts` | 205-246, 593-709 | claude-code-acp resume 实现 |
### 8.2 参考资料
- [ACP Protocol Specification](https://agentclientprotocol.com)
- [claude-code-acp GitHub](https://github.com/zed-industries/claude-code-acp)
- [Agent Client Protocol Rust SDK](https://github.com/agentclientprotocol/rust-sdk)

View File

@@ -0,0 +1,16 @@
# Introduction
## Project Alpha
我使用的ACP协议,来使用的agent ,目前agent使用的是"claude-code-acp","claude-code-acp"是zed公司
对 claude code 工具的ACP协议封装,我去看官方 https://github.com/zed-industries/claude-code-acp 仓库里有"--resume" 的相关逻辑, 使用的anthropic的ts sdk 来对接的api(ts版本的官方文档是: https://platform.claude.com/docs/en/agent-sdk/typescript ).
rust版本的acp的官方源码(https://github.com/agentclientprotocol/rust-sdk),我下载到本地当前项目下的: tmp/rust-sdk 里了;
crates/agent_abstraction/src/compat/claude_code_launcher.rs 是创建agent session的逻辑,是我启动 "claude-code-acp"的部分代码.
我现在通过acp协议,和agent对话,如果agent停止后,下一次,我想通过 resume 参数,来继续之前的上下文对话记录,继续和agent对话,但当前我不知道怎么使用 resume 参数,来继续之前的上下文记录,来继续和agent对话,我需要先了解resume参数的使用方法,然后才能继续和agent对话。
注: 本地参考的源码,可以看项目下的: tmp 目录下,只能用于查阅参考,禁止依赖引入使用
- tmp/claude-code-acp 官方的ts写的acp协议,封装的claude code ,支持acp协议调用agent
- tmp/rust-sdk 官方ACP协议的rust版本的sdk

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
# Instruction
## project alpha
我要做一个带有虚拟远程桌面的agentic aiagent 可以在这个带有远程桌面的docker容器里操作浏览器搜索访问自己需要的网络资料用户还可以通过 vnc 远程虚拟桌面查看操作使用最终agent在docker容器里挂载的目录下按照用户的 prompt 的要求,完成复杂任务。
我现在初步想法点,大概如下:
* agent 复用当前 `crates/agent_runner` 模块使用的docker容器是 docker/rcoder-agent-runner/Dockerfile 通过这个构建出来的容器 ,通过 `ServiceType::ComputerAgentRunner` 来创建使用我们的新的docker 容器,进行使用。
* `crates/rcoder` 模块的 `crates/rcoder/src/router.rs` 的http router 参考现有使用的agent_runner 模块的http接口("/agent"前缀的接口,"/chat"),我们统一增加新接口,前缀是 "/computer" 的http接口对外服务内部还是调用 `crates/agent_runner` 模块在 `ServiceType::ComputerAgentRunner` docker 容器里运行。
1 "/computer/chat" 接口,入参需要增加字段 `user_id` 用户id必填,根据 `user_id` 来检查有无对应的 docker容器一个 `user_id`对应一个docker容器一一对应如果没有则自动动态创建和现在的 `ServiceType::RCoder`创建容器方式一样),然后动态创建的子容器,是根据入参 `user_id` ,`project_id` 来进行挂载。容器挂载路径: /app/computer-project-workspace/{user_id} 。
挂载目录示意: /app/computer-project-workspace/{user_id}/{project_id}
如果 `user_id` 对应的docker容器已经启动容器里可以有多个agent服务运行根据 `project_id` 启动对应的agent 服务,当前 `crates/agent_runner` 模块是支持在一个docker容器里按照 `project_id` 启动对应的agent 服务)
注: 我在本地测试用的docker compose配置文件docker/docker-compose.yml有挂载 `computer-project-workspace` 目录到容器里,你可以去看下这个配置文件
* `docker/rcoder-agent-runner/Dockerfile` docker镜像配置文件本身有用 noVnc 提供vnc服务用于连接虚拟远程桌面。 但用户访问是通过 `crates/rcoder` 模块 所在的主容器来访问vnc服务的不能直接让用户访问内部动态创建的子容器服务我这里想法是可以通过 pingora 来提供反向透明代理服务按照我定义的路径规则透明代理vnc服务。
我设想的vnc路径规则是 "/computer/desktop/{user_id}/{project_id}" ,这样 pingora 就知道要访问的是哪个子容器的vnc服务了可以进行透明代理操作。
然后关于vnc服务我还有些不清楚的点如果以后vnc 的虚拟远程桌面,需要粘贴复制,是不是应该进一步封装成接口,做一些处理,而不是仅仅只使用 pingora 透明代理了?
* `ServiceType::ComputerAgentRunner` docker 容器里的agent在当前的默认mcp配置下增加mcp工具 Chrome DevTools MCP 对应github仓库地址是 https://github.com/ChromeDevTools/chrome-devtools-mcp?tab=readme-ov-file
mcp参考json配置
```
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}
```
`crates/agent_config/configs/default_agents.json` 这个是 `ServiceType::RCoder` 默认使用的agent配置我们的新功能 `ServiceType::ComputerAgentRunner` 也可以使用这个配置吗? 但需要额外增加 "Chrome DevTools MCP" 这个浏览器操作的mcp工具。 `ServiceType::ComputerAgentRunner`对应的docker容器里已经有提前安装好的 chromium 浏览器,并设置开发里 9200 端口,用于开发调试使用。
* 现有的接口 "/agent/stop" 是停止对应的 project_id 对应的容器,但是新接口: "/computer/agent/stop"对应的逻辑,是根据 `user_id` ,`project_id` 停止对应的agent服务就行了。
* 现有根据 agent是否闲置的规则需要有区分枚举`ServiceType`,之前因为一个 `project_id` 对应一个docker容器现在`ServiceType::ComputerAgentRunner` 对应容器,是根据 `user_id` 对应一个docker容器 `user_id` 可能有多个 `project_id` 对应的agent 在运行,需要 `user_id`下的所有 `project_id` 对应的agent 都是闲置状态(复用现有闲置时间规则),才能销毁对应的 docker 容器。
* 目前`crates/rcoder` 通过 gRPC和 `crates/agent_runner` 模块通信,如果失败,不要回退成 http通信`crates/agent_runner` 模块里的http接口后面是要彻底删除的。`crates/agent_runner` 模块只用gRPC通信。
### vnc虚拟远程桌面手动测试
开发结束后在帮我开发一个html网页放在 `fixtures`目录下方便我打开网页输入本地的ip和端口来测试验证 vnc虚拟桌面。
按照这个想法,帮我生成详细的需求和设计文档,输出为中文。涉及到代码部分,不要写详细的具体实现,只需要写 trait 和 结构体 struct

View File

@@ -0,0 +1,959 @@
# Docker Pod 信息接口设计文档
> **文档版本**: v1.0.1
> **创建日期**: 2024-12-16
> **更新日期**: 2024-12-16
> **状态**: 草案
> **相关模块**: `crates/rcoder/src/router.rs`, `crates/docker_manager`, `crates/shared_types`
---
## 1. 需求概述
### 1.1 背景
RCoder 系统采用动态容器管理模式,可以根据 `user_id``project_id` 创建和管理 Docker 容器(特别是 `ComputerAgentRunner` 类型的容器)。为了便于系统监控和前端用户使用 noVNC 远程虚拟桌面,需要新增三个接口:
1. **获取当前容器数量接口** - 用于监控系统中已创建的容器总数
2. **启动容器接口** - 根据 `user_id``project_id` 启动(或获取已存在的)容器,**仅启动容器本身,不启动 Agent 服务**,以便用户可以通过 noVNC 访问远程虚拟桌面
3. **容器保活接口** - 刷新容器的最后活动时间,防止容器被定时清理任务销毁
> [!IMPORTANT]
> 所有接口响应统一使用 `shared_types::HttpResult<T>` 结构进行包装,保持与系统其他 API 的一致性。
### 1.2 目标
| 目标 | 描述 |
|------|------|
| **监控能力** | 提供容器数量统计,便于系统运维监控 |
| **用户体验** | 简化容器启动流程,自动化处理容器存在性检查 |
| **简洁响应** | 响应只包含容器基本信息VNC 访问通过独立接口获取 |
| **轻量启动** | 仅启动容器,不启动 Agent 服务,减少资源占用 |
| **容器保活** | 支持刷新容器活动时间,防止被自动清理 |
---
## 2. HttpResult 响应包装
### 2.1 HttpResult 结构 (来自 `shared_types`)
所有接口响应均使用 `HttpResult<T>` 包装:
```rust
// 来自 crates/shared_types/src/model/http_result.rs
#[derive(Debug, Deserialize, ToSchema)]
pub struct HttpResult<T> {
/// 响应码: "0000" 表示成功
pub code: String,
/// 响应消息
pub message: String,
/// 响应数据 (成功时有值)
pub data: Option<T>,
/// OpenTelemetry Trace ID (用于链路追踪)
pub tid: Option<String>,
/// 是否成功 (code == "0000")
#[serde(skip)]
pub success: bool,
}
impl<T> HttpResult<T> {
/// 创建成功响应
pub fn success(data: T) -> Self;
/// 创建错误响应
pub fn error(code: &str, message: &str) -> Self;
/// 创建内部错误响应 (code: "5000")
pub fn internal_error(message: &str) -> Self;
}
```
### 2.2 响应格式示例
**成功响应:**
```json
{
"code": "0000",
"message": "成功",
"data": { ... },
"tid": "abc123def456...",
"success": true
}
```
**错误响应:**
```json
{
"code": "5000",
"message": "获取 DockerManager 失败",
"data": null,
"tid": "abc123def456...",
"success": false
}
```
---
## 3. 接口设计
### 3.1 接口一:获取当前容器数量
#### 3.1.1 接口规格
| 属性 | 值 |
|------|-----|
| **路径** | `GET /computer/pod/count` |
| **方法** | GET |
| **入参** | 无 |
| **响应类型** | `HttpResult<PodCountResponse>` |
| **标签** | `pod` (Pod 容器管理接口) |
#### 3.1.2 请求示例
```bash
curl -X GET http://localhost:8087/computer/pod/count
```
#### 3.1.3 响应数据结构 (data 字段)
```rust
/// 容器数量响应结构
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PodCountResponse {
/// 当前运行的容器总数
#[schema(example = 5)]
pub total_count: u32,
/// 按服务类型分类的容器数量
pub by_service_type: PodCountByServiceType,
/// 统计时间戳 (Unix 毫秒)
#[schema(example = 1702700000000)]
pub timestamp: u64,
}
/// 按服务类型分类的容器数量
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PodCountByServiceType {
/// RCoder 类型容器数量
#[schema(example = 2)]
pub rcoder: u32,
/// ComputerAgentRunner 类型容器数量
#[schema(example = 3)]
pub computer_agent_runner: u32,
}
```
#### 3.1.4 完整响应示例
```json
{
"code": "0000",
"message": "成功",
"data": {
"total_count": 5,
"by_service_type": {
"rcoder": 2,
"computer_agent_runner": 3
},
"timestamp": 1702700000000
},
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": true
}
```
---
### 3.2 接口二:启动容器 (Ensure Container)
#### 3.2.1 接口规格
| 属性 | 值 |
|------|-----|
| **路径** | `POST /computer/pod/ensure` |
| **方法** | POST |
| **入参** | `user_id` (必填), `project_id` (必填) |
| **响应类型** | `HttpResult<EnsurePodResponse>` |
| **标签** | `pod` (Pod 容器管理接口) |
> [!NOTE]
> 此接口采用 **幂等设计**:如果对应的容器已经存在且处于运行状态,则直接返回现有容器信息,不会创建新容器。
> [!IMPORTANT]
> **此接口仅启动容器,不启动 Agent 服务。** 容器启动后,用户可直接通过 noVNC 访问虚拟桌面,无需等待 Agent 服务初始化。如需使用 AI Agent 功能,请调用 `/computer/chat` 接口。
#### 3.2.2 请求结构
```rust
/// 启动容器请求结构
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct EnsurePodRequest {
/// 用户唯一标识符
#[schema(example = "user_123")]
pub user_id: String,
/// 项目唯一标识符
#[schema(example = "proj_456")]
pub project_id: String,
/// 可选的资源限制配置
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_limits: Option<PodResourceLimits>,
}
/// Pod 资源限制配置
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct PodResourceLimits {
/// 内存限制 (bytes), 例如 4GB = 4294967296
#[schema(example = 4294967296)]
pub memory: Option<u64>,
/// CPU 份额 (1024 = 1 核)
#[schema(example = 2048)]
pub cpu_shares: Option<u64>,
}
```
#### 3.2.3 请求示例
```bash
curl -X POST http://localhost:8087/computer/pod/ensure \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_123",
"project_id": "proj_456"
}'
```
#### 3.2.4 响应数据结构 (data 字段)
```rust
/// 启动容器响应结构
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct EnsurePodResponse {
/// 容器是否为新创建 (false 表示已存在)
pub created: bool,
/// 容器基本信息
pub container_info: PodContainerInfo,
/// 提示消息
#[schema(example = "容器已就绪")]
pub message: String,
}
/// 容器基本信息
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct PodContainerInfo {
/// 容器 ID
#[schema(example = "abc123def456")]
pub container_id: String,
/// 容器名称
#[schema(example = "computer-agent-runner-user_123")]
pub container_name: String,
/// 容器 IP 地址 (内部网络)
#[schema(example = "172.17.0.5")]
pub container_ip: String,
/// 服务 URL
#[schema(example = "http://172.17.0.5:8086")]
pub service_url: String,
/// 容器状态
#[schema(example = "running")]
pub status: String,
}
```
#### 3.2.5 完整响应示例
**成功响应(新创建容器):**
```json
{
"code": "0000",
"message": "成功",
"data": {
"created": true,
"container_info": {
"container_id": "abc123def456...",
"container_name": "computer-agent-runner-user_123",
"container_ip": "172.17.0.5",
"service_url": "http://172.17.0.5:8086",
"status": "running"
},
"message": "容器创建成功Agent 服务未启动)"
},
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": true
}
```
**成功响应(容器已存在):**
```json
{
"code": "0000",
"message": "成功",
"data": {
"created": false,
"container_info": {
"container_id": "abc123def456...",
"container_name": "computer-agent-runner-user_123",
"container_ip": "172.17.0.5",
"service_url": "http://172.17.0.5:8086",
"status": "running"
},
"message": "容器已存在"
},
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": true
}
```
**错误响应:**
```json
{
"code": "5000",
"message": "启动容器失败: Docker 连接超时",
"data": null,
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": false
}
```
---
### 3.3 接口三:容器保活 (Keepalive)
#### 3.3.1 接口规格
| 属性 | 值 |
|------|-----|
| **路径** | `POST /computer/pod/keepalive` |
| **方法** | POST |
| **入参** | `user_id` (必填), `project_id` (必填) |
| **响应类型** | `HttpResult<KeepalivePodResponse>` |
| **标签** | `pod` (Pod 容器管理接口) |
> [!NOTE]
> 此接口用于防止容器被定时清理任务销毁。系统会定期(默认每 5 分钟)检查容器的最后活动时间,闲置超过 30 分钟的容器将被自动清理。
> [!TIP]
> **使用场景**:前端在用户使用 noVNC 远程桌面时,应定期调用此接口(建议每 5-10 分钟一次)来保持容器活跃状态。
#### 3.3.2 请求结构
```rust
/// 容器保活请求结构
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct KeepalivePodRequest {
/// 用户唯一标识符
#[schema(example = "user_123")]
pub user_id: String,
/// 项目唯一标识符
#[schema(example = "proj_456")]
pub project_id: String,
}
```
#### 3.3.3 请求示例
```bash
curl -X POST http://localhost:8087/computer/pod/keepalive \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_123",
"project_id": "proj_456"
}'
```
#### 3.3.4 响应数据结构 (data 字段)
```rust
/// 容器保活响应结构
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct KeepalivePodResponse {
/// 容器是否存在 (true=已存在, false=新创建)
pub existed: bool,
/// 容器是否为新创建 (当 existed=false 时为 true)
pub created: bool,
/// 容器基本信息
pub container_info: PodContainerInfo,
/// 上次活动时间 (Unix 毫秒时间戳, 更新前)
#[schema(example = 1702700000000)]
pub previous_activity_time: u64,
/// 当前活动时间 (Unix 毫秒时间戳, 更新后)
#[schema(example = 1702700600000)]
pub current_activity_time: u64,
/// 距离下次清理的剩余时间 (秒)
/// 基于默认 30 分钟闲置超时计算
#[schema(example = 1800)]
pub time_until_cleanup: u64,
/// 提示消息
#[schema(example = "容器活动时间已刷新")]
pub message: String,
}
```
#### 3.3.5 完整响应示例
**成功响应(容器已存在,刷新活动时间):**
```json
{
"code": "0000",
"message": "成功",
"data": {
"existed": true,
"created": false,
"container_info": {
"container_id": "abc123def456...",
"container_name": "computer-agent-runner-user_123",
"container_ip": "172.17.0.5",
"service_url": "http://172.17.0.5:8086",
"status": "running"
},
"previous_activity_time": 1702700000000,
"current_activity_time": 1702700600000,
"time_until_cleanup": 1800,
"message": "容器活动时间已刷新,距离自动清理还有 30 分钟"
},
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": true
}
```
**成功响应(容器不存在,自动创建):**
```json
{
"code": "0000",
"message": "成功",
"data": {
"existed": false,
"created": true,
"container_info": {
"container_id": "xyz789...",
"container_name": "computer-agent-runner-user_123",
"container_ip": "172.17.0.6",
"service_url": "http://172.17.0.6:8086",
"status": "running"
},
"previous_activity_time": 0,
"current_activity_time": 1702700600000,
"time_until_cleanup": 1800,
"message": "容器已自动创建,距离自动清理还有 30 分钟"
},
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": true
}
```
**错误响应:**
```json
{
"code": "5000",
"message": "刷新活动时间失败: 容器状态异常",
"data": null,
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": false
}
```
---
## 4. 架构设计
### 4.1 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 外部客户端 │
└────────────────────────────┬────────────────────────────────────┘
│ HTTP
┌─────────────────────────────────────────────────────────────────┐
│ rcoder (主服务) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ router.rs │ │
│ │ /computer/pod/count ─────▶ pod_count_handler │ │
│ │ /computer/pod/ensure ─────▶ pod_ensure_handler │ │
│ │ /computer/pod/keepalive─────▶ pod_keepalive_handler │ │
│ └───────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼─────────────────────────────┐ │
│ │ service/pod_service.rs │ │
│ │ ┌─────────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ PodService │ │ PodServiceTrait (trait) │ │ │
│ │ │ - get_count() │ │ - get_pod_count() │ │ │
│ │ │ - ensure_pod() │ │ - ensure_pod() │ │ │
│ │ └─────────────────┘ └──────────────────────────────┘ │ │
│ └───────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼─────────────────────────────┐ │
│ │ docker_manager (crate) │ │
│ │ DockerManager │ │
│ │ - list_containers() │ │
│ │ - get_agent_info() │ │
│ │ - start_agent_container() │ │
│ └───────────────────────────┬─────────────────────────────┘ │
└──────────────────────────────┼──────────────────────────────────┘
│ Docker API
┌─────────────────────────────────────────────────────────────────┐
│ Docker Daemon │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ container-1 │ │ container-2 │ │ container-3 │ │
│ │ (user_123) │ │ (user_456) │ │ (project_xyz) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 4.2 模块职责
| 模块 | 职责 |
|------|------|
| `handler/pod_handler.rs` | 处理 HTTP 请求,参数解析与验证 |
| `service/pod_service.rs` | 核心业务逻辑,调用 DockerManager |
| `docker_manager` | Docker API 交互,容器生命周期管理 |
---
## 5. Trait 与结构体定义
### 5.1 Service Trait
```rust
// crates/rcoder/src/service/pod_service.rs
use async_trait::async_trait;
use crate::AppError;
/// Pod 服务 Trait
///
/// 定义 Pod 容器管理的核心能力接口
#[async_trait]
pub trait PodServiceTrait: Send + Sync {
/// 获取当前容器数量统计
///
/// # 返回
/// 容器数量统计信息
async fn get_pod_count(&self) -> Result<PodCountResponse, AppError>;
/// 确保容器存在(幂等操作)
///
/// 如果容器已存在则返回现有容器信息,否则创建新容器
/// 仅启动容器,不启动 Agent 服务
///
/// # 参数
/// - `request`: 启动容器请求
///
/// # 返回
/// 容器信息和 VNC 访问信息
async fn ensure_pod(&self, request: EnsurePodRequest) -> Result<EnsurePodResponse, AppError>;
/// 容器保活(刷新活动时间)
///
/// 如果容器已存在则刷新其最后活动时间,防止被清理任务销毁
/// 如果容器不存在则自动创建
///
/// # 参数
/// - `request`: 保活请求(包含 user_id 和 project_id
///
/// # 返回
/// 保活结果,包含活动时间信息和距离清理的剩余时间
async fn keepalive_pod(&self, request: KeepalivePodRequest) -> Result<KeepalivePodResponse, AppError>;
}
```
### 5.2 请求/响应结构体
```rust
// crates/rcoder/src/handler/pod_handler.rs
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
// ============================================================================
// 接口一:获取容器数量
// ============================================================================
/// 容器数量响应
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PodCountResponse {
/// 当前运行的容器总数
pub total_count: u32,
/// 按服务类型分类的容器数量
pub by_service_type: PodCountByServiceType,
/// 统计时间戳 (Unix 毫秒)
pub timestamp: u64,
}
/// 按服务类型分类的容器数量
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PodCountByServiceType {
/// RCoder 类型容器数量
pub rcoder: u32,
/// ComputerAgentRunner 类型容器数量
pub computer_agent_runner: u32,
}
// ============================================================================
// 接口二:启动容器
// ============================================================================
/// 启动容器请求
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct EnsurePodRequest {
/// 用户唯一标识符 (必填)
#[schema(example = "user_123")]
pub user_id: String,
/// 项目唯一标识符 (必填)
#[schema(example = "proj_456")]
pub project_id: String,
/// 可选的资源限制配置
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_limits: Option<PodResourceLimits>,
}
/// Pod 资源限制配置
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct PodResourceLimits {
/// 内存限制 (bytes)
pub memory: Option<u64>,
/// CPU 份额
pub cpu_shares: Option<u64>,
}
/// 启动容器响应
///
/// 注意: 此结构体作为 HttpResult<EnsurePodResponse> 的 data 字段返回
/// 成功状态由外层 HttpResult 的 code 字段表示
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct EnsurePodResponse {
/// 容器是否为新创建 (false 表示已存在)
pub created: bool,
/// 容器基本信息
pub container_info: PodContainerInfo,
/// 提示消息
pub message: String,
}
/// 容器基本信息
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct PodContainerInfo {
/// 容器 ID
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 容器 IP 地址
pub container_ip: String,
/// 服务 URL
pub service_url: String,
/// 容器状态
pub status: String,
}
// ============================================================================
// 接口三:容器保活
// ============================================================================
/// 容器保活请求
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct KeepalivePodRequest {
/// 用户唯一标识符
#[schema(example = "user_123")]
pub user_id: String,
/// 项目唯一标识符
#[schema(example = "proj_456")]
pub project_id: String,
}
/// 容器保活响应
///
/// 注意: 此结构体作为 HttpResult<KeepalivePodResponse> 的 data 字段返回
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct KeepalivePodResponse {
/// 容器是否已存在
pub existed: bool,
/// 容器是否为新创建 (当 existed=false 时为 true)
pub created: bool,
/// 容器基本信息
pub container_info: PodContainerInfo,
/// 上次活动时间 (Unix 毫秒时间戳, 更新前)
pub previous_activity_time: u64,
/// 当前活动时间 (Unix 毫秒时间戳, 更新后)
pub current_activity_time: u64,
/// 距离下次清理的剩余时间 (秒)
pub time_until_cleanup: u64,
/// 提示消息
pub message: String,
}
```
### 5.3 Service 实现结构
```rust
// crates/rcoder/src/service/pod_service.rs
use std::sync::Arc;
/// Pod 服务实现
///
/// 封装 Docker 容器管理的业务逻辑
pub struct PodService {
/// Docker 管理器
docker_manager: Arc<docker_manager::DockerManager>,
}
impl PodService {
/// 创建 PodService 实例
pub fn new(docker_manager: Arc<docker_manager::DockerManager>) -> Self {
Self { docker_manager }
}
}
#[async_trait]
impl PodServiceTrait for PodService {
async fn get_pod_count(&self) -> Result<PodCountResponse, AppError> {
// 具体实现...
todo!()
}
async fn ensure_pod(&self, request: EnsurePodRequest) -> Result<EnsurePodResponse, AppError> {
// 具体实现...
todo!()
}
async fn keepalive_pod(&self, request: KeepalivePodRequest) -> Result<KeepalivePodResponse, AppError> {
// 具体实现...
// 1. 查询容器是否存在
// 2. 不存在则创建容器
// 3. 存在则刷新 last_activity 时间
// 4. 计算距离清理的剩余时间
todo!()
}
}
```
---
## 6. 路由配置
### 6.1 新增路由
`crates/rcoder/src/router.rs` 中添加以下路由:
```rust
// 在 create_router 函数中添加
// Pod 容器管理路由 (统一使用 /computer 前缀)
.route("/computer/pod/count", get(handler::pod_count))
.route("/computer/pod/ensure", post(handler::pod_ensure))
.route("/computer/pod/keepalive", post(handler::pod_keepalive))
```
### 6.2 OpenAPI 配置更新
```rust
// 在 ApiDoc 结构体中添加
#[openapi(
paths(
// ... 现有路径
handler::pod_count,
handler::pod_ensure,
handler::pod_keepalive,
),
components(
schemas(
// ... 现有 schemas
handler::PodCountResponse,
handler::PodCountByServiceType,
handler::EnsurePodRequest,
handler::PodResourceLimits,
handler::EnsurePodResponse,
handler::PodContainerInfo,
handler::KeepalivePodRequest,
handler::KeepalivePodResponse,
)
),
tags(
// ... 现有 tags
(name = "pod", description = "Pod 容器管理接口,支持容器监控和启动"),
),
)]
```
---
## 7. 错误处理
### 7.1 错误类型
| 错误码 | HTTP 状态码 | 描述 |
|--------|-------------|------|
| `DOCKER_MANAGER_ERROR` | 500 | Docker Manager 获取失败 |
| `CONTAINER_CREATE_FAILED` | 500 | 容器创建失败 |
| `CONTAINER_NOT_FOUND` | 404 | 容器未找到(仅查询场景) |
| `INVALID_REQUEST` | 400 | 请求参数无效 |
### 7.2 错误响应结构HttpResult 统一格式)
错误响应统一使用 `HttpResult<T>` 格式,`data` 字段为 `null`
```json
{
"code": "5000",
"message": "启动容器失败: Docker 连接超时",
"data": null,
"tid": "4bf92f3577b34da6a3ce929d0e0e4736",
"success": false
}
```
---
## 8. 与现有功能的关系
### 8.1 复用现有组件
| 组件 | 复用方式 |
|------|----------|
| `docker_manager::global::get_global_docker_manager()` | 获取全局 Docker 管理器实例 |
| `ComputerContainerManager` | 复用用户容器创建逻辑 |
| `ContainerBasicInfo` | 复用容器信息结构 |
### 8.2 与 noVNC 集成
VNC 访问信息通过独立接口获取:
- **VNC 桌面访问接口**: `GET /computer/desktop/{user_id}/{project_id}`
Pingora 已配置路由规则 `/computer/vnc/{user_id}/{project_id}/{*path}`,会自动代理到对应容器的 6080 端口。
---
## 9. 实现清单
### 9.1 新增文件
| 文件路径 | 描述 |
|----------|------|
| `crates/rcoder/src/handler/pod_handler.rs` | Pod 相关 HTTP 处理器 |
| `crates/rcoder/src/service/pod_service.rs` | Pod 服务业务逻辑 |
### 9.2 修改文件
| 文件路径 | 修改内容 |
|----------|----------|
| `crates/rcoder/src/router.rs` | 添加 `/computer/pod/*` 路由 |
| `crates/rcoder/src/handler/mod.rs` | 导出 `pod_handler` 模块 |
| `crates/rcoder/src/service/mod.rs` | 导出 `pod_service` 模块 |
---
## 10. 验证计划
### 10.1 自动化测试
```bash
# 1. 编译检查
cargo build --workspace
# 2. 单元测试
cargo test --workspace
# 3. Clippy 检查
cargo clippy --workspace --all-targets
```
### 10.2 手动验证
```bash
# 1. 启动服务
make dev-restart
# 2. 测试获取容器数量
curl -X GET http://localhost:8087/computer/pod/count | jq
# 3. 测试启动容器
curl -X POST http://localhost:8087/computer/pod/ensure \
-H "Content-Type: application/json" \
-d '{"user_id": "test_user", "project_id": "test_project"}' | jq
# 4. 验证 VNC 访问
# 使用返回的 proxy_vnc_url 在浏览器中访问
```
### 10.3 OpenAPI 文档验证
启动服务后访问 Swagger UI`http://localhost:8087/swagger-ui/` 确认新接口已正确注册。
---
## 11. 附录
### 11.1 参考文件
- [computer_container_manager.rs](file:///Volumes/soddygo/git_work/rcoder/crates/rcoder/src/service/computer_container_manager.rs) - 用户容器管理逻辑
- [computer_desktop_handler.rs](file:///Volumes/soddygo/git_work/rcoder/crates/rcoder/src/handler/computer_desktop_handler.rs) - VNC 桌面处理器
- [docker_manager/manager.rs](file:///Volumes/soddygo/git_work/rcoder/crates/docker_manager/src/manager.rs) - Docker 容器管理核心
### 11.2 相关接口
| 接口 | 描述 |
|------|------|
| `GET /computer/desktop/{user_id}/{project_id}` | 获取 VNC 桌面访问信息 |
| `POST /computer/chat` | Computer Agent 聊天接口 |
| `/computer/vnc/{user_id}/{project_id}/*` | Pingora VNC 代理路由 |
---
## 12. 变更记录
| 日期 | 版本 | 变更内容 | 作者 |
|------|------|----------|------|
| 2024-12-16 | v1.0.0 | 初始版本 | AI Assistant |
| 2024-12-16 | v1.0.1 | 使用 HttpResult 包装响应;明确不启动 Agent 服务 | AI Assistant |
| 2024-12-16 | v1.1.0 | 新增容器保活接口 (keepalive),支持刷新容器活动时间防止被自动清理 | AI Assistant |

View File

@@ -0,0 +1,13 @@
# Instruction
## Project Alpha
@crates/rcoder/src/router.rs
我需要新增接口:
1. 获取当前容器数量,这个接口不需要入参
2. 入参给 user_id, project_id , 启动容器(如果对应容器已经启动,则do nothing),不需要启动agent服务,方便我使用 noVNC 来远程虚拟桌面使用.
3. 增加一个容器保活的keepalive接口,入参给 user_id, project_id , 如果对应容器存在,则do nothing,如果不存在,则启动容器,另外自动刷新容器的最后更新使用时间,防止容器被定时清理任务,给清理销毁掉
这2个新接口,返回结构要用 HttpResult 来包装, @crates/shared_types/src/model/http_result.rs
按照这个想法,帮我生成详细的需求和设计文档,输出为中文。涉及到代码部分,不要写详细的具体实现,只需要写 trait 和 结构体 struct

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
# Instruction
## Project Alpha
https://github.com/duckdb/duckdb 这个是官方 duckdb 的github 仓库,我想使用duckdb的内存模式,用于存储数据,跟随主容器 crates/rcoder 模块,当容器重启,数据就全部重置了,用于记录 {user_id, project_id} 对应的容器信息,更新容器的最后使用时间等,当业务服务闲置,就自动销毁对应的容器,当前都是使用 Dashmap 来记录存储数据信息,我想用 duckdb 来替代 Dashmap. 比如对应 crates/rcoder/src/router.rs 里的 AppState结构体中的字段: "pub project_and_agent_map: DashMap<String, Arc<ProjectAndContainerInfo>>," ,还有"session_to_container_id","sessions" ,"session_to_container_id"的信息维护,都是使用 Dashmap 来维护,我想用 duckdb 来替代 Dashmap.
我希望通过内存数据库,通过设计良好的数据模型,来替代当前的 Dashmap,实现相同的功能.
- @crates/shared_types/src/service_type.rs 根据 ServiceType 枚举,分2个业务场景,对应容器有区别
- 清理容器,核心是根据对应的agent是否闲置,定期清理容器,确保资源及时回收,所以会定期根据请求,或者agent是否在执行任务,刷新容器最后的时间,用于清理容器来提供依据. 这个是当前已有的业务逻辑
- duckdb 使用内存模式,不需要持久化数据,跟随主容器 rcoder 启动的时候,使用空数据状态的数据库
- duckdb 的数据库使用,数据不会太多,不需要限制时间范围,比如统计当前容器数量,统计所有的数据
- duckdb数据库,使用新的模块: crates/duckdb_manager, 封装好数据库的操作接口,以lib库的形式,给 crates/rcoder 模块使用,避免其他模块内部有业务无关的数据库操作
- 数据库库表之间,禁止外键的使用
- 公共的结构体,可以放在 crates/shared_types 模块里定义
按照这个想法,帮我生成详细的需求和设计文档,输出为中文。涉及到代码部分,不要写详细的具体实现,只需要写 trait 和 结构体 struct

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,699 @@
# rcoder ↔ agent_runner gRPC 通信改造技术方案
## 1. 背景与动机
### 1.1 当前架构
```mermaid
graph LR
Client[外部客户端] --> |HTTP + SSE<br/>不可修改| RCoder[rcoder]
RCoder --> |HTTP/JSON<br/>reqwest<br/>待改造| AR[agent_runner<br/>容器内]
AR --> |SSE Text Stream<br/>待改造| RCoder
RCoder --> |SSE| Client
```
### 1.2 当前架构的核心问题
> [!WARNING]
> **核心矛盾**`rcoder` 本质上只是做**纯转发**,但却需要完整的 HTTP + JSON 序列化/反序列化流程。
**问题详解**
1. ~~**参数重复定义**~~**已解决**
- `agent_runner/src/model.rs` 现在完全从 `shared_types` 重新导出所有类型
- 新增 `CancelNotificationRequestWrapper``CancelResult` 统一取消操作
- 两端共享同一套类型定义,不再需要手工同步
2. **无意义的序列化开销** ⚠️ 待解决
```
Client JSON → rcoder 反序列化 → 再序列化 JSON → agent_runner 反序列化
```
rcoder 收到 JSON 后反序列化为 Rust 结构体,然后又序列化回 JSON 发给 agent_runner这是**完全冗余的**。
3. **SSE 文本解析脆弱** ⚠️ 待解决
- agent_runner 生成 SSE 事件(`data: {...}\n\n`
- rcoder 接收后需要手工解析 `event:`、`data:` 等文本标记
- 再重新构造 SSE 事件发给 Client
- 这种文本层面的"拆解-重组"极易出错
4. **类型约束缺失** ⚠️ 待解决
- 两个模块通过 HTTP/JSON 通信,接口兼容性依赖运行时检查
- 一方改了字段名或类型,另一方可能静默失败
**根本原因**:内部模块通信使用了设计给"外部调用"的 HTTP + JSON 协议,不适合紧密耦合的模块间通信场景。
> [!NOTE]
> **进展**:类型共享已通过 `shared_types` 模块实现gRPC 改造将进一步解决序列化开销和 SSE 解析问题。
### 1.3 改造边界
> [!IMPORTANT]
> **关键约束**`rcoder` 对外提供的 HTTP/SSE 接口**不可修改**,本次改造仅涉及 `rcoder ↔ agent_runner` 之间的**内部通信**。
| 接口 | 改造范围 | 说明 |
|------|----------|------|
| **rcoder → Client** | ❌ 不改动 | 对外 HTTP API保持兼容 |
| **rcoder ↔ agent_runner** | ✅ 改造目标 | 内部模块通信HTTP → gRPC |
### 1.3 当前通信方式问题
| 问题 | 描述 |
|------|------|
| **弱类型约束** | `reqwest` 发送 JSON手工解析 `HttpResult<ChatResponse>`,类型安全依赖运行时检查 |
| **SSE 解析脆弱** | `agent_session_notification.rs` 手工解析 SSE 文本流,需处理 `event:`、`data:` 标记 |
| **性能开销** | JSON 序列化/反序列化、文本解析带来额外 CPU 开销 |
| **双端重复代码** | `ChatRequest`/`ChatResponse` 在 `rcoder` 和 `agent_runner` 分别定义 |
### 1.4 改造目标
1. **强类型契约**:使用 Protobuf 定义的 `AgentService` 确保内部接口一致性
2. **高效二进制传输**Protobuf 编码比 JSON 更紧凑、解码更快
3. **Server Streaming 替代 SSE**gRPC 原生流式支持,无需解析文本格式
4. **代码共享**`shared_types` 提供统一的 gRPC client/server 代码
---
## 2. 可行性分析
### 2.1 现有基础设施 ✅
| 组件 | 状态 | 说明 |
|------|------|------|
| **tonic 0.14** | ✅ 已添加 | `shared_types/Cargo.toml` 已有依赖 |
| **Proto 定义** | ⚠️ 需扩展 | `proto/agent.proto` 定义了 `AgentService`Attachment 需完善 |
| **生成代码** | ✅ 已生成 | `shared_types/src/grpc/agent.rs` 包含 Client/Server |
### 2.2 Proto 服务定义 (现有)
```protobuf
service AgentService {
rpc Chat (ChatRequest) returns (ChatResponse);
rpc SubscribeProgress (ProgressRequest) returns (stream ProgressEvent);
rpc CancelSession (CancelRequest) returns (CancelResponse);
rpc GetStatus (GetStatusRequest) returns (GetStatusResponse);
}
```
完全覆盖当前内部 HTTP 接口:
| agent_runner HTTP 接口 | gRPC 方法 | 类型 |
|------------------------|-----------|------|
| `POST /chat` | `Chat` | Unary |
| `GET /agent/progress/{session_id}` | `SubscribeProgress` | Server Streaming |
| `POST /agent/session/cancel` | `CancelSession` | Unary |
| `GET /agent/status/{project_id}` | `GetStatus` | Unary |
### 2.3 改造收益预估
| 方面 | HTTP/JSON | gRPC/Protobuf | 提升 |
|------|-----------|---------------|------|
| **编码性能** | ~5μs/msg | ~0.5μs/msg | ~10x |
| **消息体积** | ~1KB | ~400B | ~2.5x |
| **类型安全** | 运行时 | 编译时 | 显著 |
| **流式处理** | 手工解析 SSE | 原生 streaming | 简化 |
---
## 3. 架构设计
### 3.1 目标架构
```mermaid
graph TB
subgraph "外部接口 (不变)"
Client[外部客户端]
end
subgraph "宿主机 - rcoder"
RCoder[rcoder<br/>HTTP Server 对外<br/>gRPC Client 对内]
end
subgraph "容器 - agent_runner"
AR[agent_runner<br/>gRPC Server 内部<br/>HTTP Health 监控]
end
Client -->|HTTP + SSE<br/>保持不变| RCoder
RCoder -->|gRPC unary/stream<br/>内部通信改造| AR
style Client fill:#e8f5e9
style RCoder fill:#e1f5fe
style AR fill:#fff3e0
```
### 3.2 端口规划
| 服务 | 端口 | 用途 | 改动 |
|------|------|------|------|
| **rcoder HTTP** | 3000 | 对外 HTTP APIAxum | ❌ 不变 |
| **agent_runner gRPC** | 50051 | 容器内 gRPC 服务Tonic | ✅ 新增 |
| **agent_runner HTTP** | 8086 | 健康检查 `/health`(保留) | ⚠️ 移除业务接口 |
### 3.3 数据流变化
**改造前**
```
Client → HTTP POST /chat → rcoder → reqwest POST → agent_runner HTTP
Client ← SSE ← rcoder ← SSE 文本解析 ← agent_runner SSE
```
**改造后**
```
Client → HTTP POST /chat → rcoder → gRPC Chat() → agent_runner gRPC Server
Client ← SSE ← rcoder ← gRPC SubscribeProgress() → agent_runner gRPC Server
```
---
## 4. Proto 定义扩展
### 4.1 现有 Attachment 问题
当前 Proto 定义过于简化:
```protobuf
// 现有定义 - 过于简单
message Attachment {
string name = 1;
string kind = 2; // "text", "image" 等,无法区分子结构
string content = 3; // 所有数据塞进一个字段
string source = 4; // "local", "url" 等
optional string language = 5;
}
```
Rust 结构体(`shared_types/src/model/attachment.rs`)更丰富:
- **4 种附件类型**Text / Image / Audio / Document
- **3 种数据源**FilePath / Base64 / Url
- **类型特有字段**dimensions、duration、size 等
### 4.2 扩展 Proto 定义
```protobuf
// === 附件数据源 ===
message AttachmentSource {
oneof source {
string file_path = 1; // 文件路径
Base64Data base64 = 2; // Base64 编码数据
string url = 3; // URL 链接
}
}
message Base64Data {
string data = 1;
string mime_type = 2;
}
message CancelRequest {
string session_id = 1;
string reason = 2;
}
message CancelResponse {
bool success = 1;
CancelResultType result = 2; // 新增:取消结果类型
optional string message = 3; // 新增:错误/描述信息
}
// 新增:取消结果枚举(对应 Rust CancelResult
enum CancelResultType {
CANCEL_RESULT_SUCCESS = 0;
CANCEL_RESULT_FAILED = 1;
CANCEL_RESULT_TIMEOUT = 2;
}
// === 附件类型定义 ===
message TextAttachment {
string id = 1;
AttachmentSource source = 2;
optional string filename = 3;
optional string description = 4;
optional string language = 5; // 编程语言(如 "rust", "python"
}
message ImageAttachment {
string id = 1;
AttachmentSource source = 2;
string mime_type = 3; // "image/jpeg", "image/png"
optional string filename = 4;
optional string description = 5;
optional ImageDimensions dimensions = 6;
}
message ImageDimensions {
uint32 width = 1;
uint32 height = 2;
}
message AudioAttachment {
string id = 1;
AttachmentSource source = 2;
string mime_type = 3; // "audio/mp3", "audio/wav"
optional string filename = 4;
optional string description = 5;
optional double duration = 6; // 时长(秒)
}
message DocumentAttachment {
string id = 1;
AttachmentSource source = 2;
string mime_type = 3; // "application/pdf", "text/plain"
optional string filename = 4;
optional string description = 5;
optional uint64 size = 6; // 文件大小(字节)
}
// === 附件枚举(使用 oneof ===
message Attachment {
oneof attachment_type {
TextAttachment text = 1;
ImageAttachment image = 2;
AudioAttachment audio = 3;
DocumentAttachment document = 4;
}
}
```
### 4.3 类型映射表
| Rust 类型 | Proto 消息 | 说明 |
|-----------|------------|------|
| `Attachment::Text(TextAttachment)` | `Attachment.text` | oneof 分支 |
| `Attachment::Image(ImageAttachment)` | `Attachment.image` | oneof 分支 |
| `Attachment::Audio(AudioAttachment)` | `Attachment.audio` | oneof 分支 |
| `Attachment::Document(DocumentAttachment)` | `Attachment.document` | oneof 分支 |
| `AttachmentSource::FilePath { path }` | `AttachmentSource.file_path` | oneof 分支 |
| `AttachmentSource::Base64 { data, mime_type }` | `AttachmentSource.base64` | 嵌套消息 |
| `AttachmentSource::Url { url }` | `AttachmentSource.url` | oneof 分支 |
---
## 5. 模块改造详情
### 5.1 shared_types 模块
#### 5.1.1 更新 proto/agent.proto
按 4.2 节扩展 Attachment 定义。
#### 5.1.2 重新生成代码
```bash
cd crates/shared_types
cargo build # 触发 build.rs 重新编译 proto
```
### 5.2 agent_runner 模块 (Server 端)
#### 5.2.1 新增文件结构
```
crates/agent_runner/src/
├── grpc/ # 新增目录
│ ├── mod.rs
│ └── agent_service_impl.rs # AgentService trait 实现
└── main.rs # 修改:启动 gRPC + HTTP 双服务
```
#### 5.2.2 AgentService 实现
```rust
// crates/agent_runner/src/grpc/agent_service_impl.rs
use shared_types::grpc::{
agent_service_server::AgentService,
ChatRequest, ChatResponse, ProgressRequest, ProgressEvent,
CancelRequest, CancelResponse, GetStatusRequest, GetStatusResponse,
};
use tokio_stream::wrappers::ReceiverStream;
pub struct AgentServiceImpl {
// 复用现有 handler 逻辑
app_state: Arc<AppState>,
}
#[tonic::async_trait]
impl AgentService for AgentServiceImpl {
async fn chat(&self, request: Request<ChatRequest>) -> Result<Response<ChatResponse>, Status> {
// 复用现有 handler::handle_chat 核心逻辑
// Proto 类型 -> 内部类型 -> 调用业务逻辑 -> Proto 响应
}
type SubscribeProgressStream = ReceiverStream<Result<ProgressEvent, Status>>;
async fn subscribe_progress(
&self,
request: Request<ProgressRequest>,
) -> Result<Response<Self::SubscribeProgressStream>, Status> {
// 从内部事件总线读取进度,转换为 ProgressEvent 流
// 使用 tokio::sync::mpsc channel
}
async fn cancel_session(&self, request: Request<CancelRequest>) -> Result<Response<CancelResponse>, Status> {
// 复用现有取消逻辑
}
async fn get_status(&self, request: Request<GetStatusRequest>) -> Result<Response<GetStatusResponse>, Status> {
// 复用现有状态查询逻辑
}
}
```
#### 5.2.3 main.rs 修改
```rust
// 双协议启动
async fn main() -> Result<()> {
// 1. 启动 gRPC Server (业务接口)
let grpc_addr = "[::]:50051".parse()?;
let grpc_service = AgentServiceServer::new(AgentServiceImpl::new(state.clone()));
let grpc_server = Server::builder()
.add_service(grpc_service)
.serve(grpc_addr);
// 2. 启动 HTTP Server (仅保留 /health)
let http_router = Router::new()
.route("/health", get(health_check));
let http_addr = "0.0.0.0:8086".parse()?;
let http_server = axum::serve(TcpListener::bind(http_addr).await?, http_router);
// 3. 并行运行
info!("🚀 agent_runner 启动: gRPC=50051, HTTP Health=8086");
tokio::select! {
r = grpc_server => r?,
r = http_server => r?,
}
Ok(())
}
```
### 5.3 rcoder 模块 (Client 端)
> [!IMPORTANT]
> rcoder 对外 HTTP 接口**完全保持不变**,仅修改与 agent_runner 通信的内部实现。
#### 5.3.1 新增文件
```
crates/rcoder/src/
├── grpc/ # 新增目录
│ ├── mod.rs
│ ├── channel_pool.rs # gRPC Channel 连接池
│ └── converters.rs # HTTP ↔ gRPC 类型转换
└── handler/
├── chat_handler.rs # 内部改用 gRPC client
└── agent_session_notification.rs # 内部调用 SubscribeProgress
```
#### 5.3.2 chat_handler.rs 改造
```rust
// 改造前 (内部使用 HTTP)
async fn forward_request_to_container_service(
request: &ChatRequest,
container_info: &ContainerBasicInfo,
) -> Result<HttpResult<ChatResponse>, AppError> {
let client = reqwest::Client::new();
let chat_url = format!("{}/chat", container_info.service_url);
let response = client.post(&chat_url).json(request).send().await?;
// ... 手工解析 JSON
}
// 改造后 (内部使用 gRPC)
async fn forward_request_to_container_service(
request: &ChatRequest,
container_info: &ContainerBasicInfo,
channel_pool: &GrpcChannelPool,
) -> Result<HttpResult<ChatResponse>, AppError> {
// 构建 gRPC 地址
let grpc_addr = format!("http://{}:50051", container_info.ip_address);
let channel = channel_pool.get_or_create_channel(&grpc_addr).await?;
// 类型转换HTTP ChatRequest -> gRPC ChatRequest
let grpc_request = converters::http_to_grpc_chat_request(request);
// gRPC 调用
let mut client = AgentServiceClient::new(channel);
let response = client.chat(grpc_request).await
.map_err(|e| AppError::internal_server_error(&format!("gRPC error: {}", e)))?;
// 类型转换gRPC ChatResponse -> HTTP ChatResponse
converters::grpc_to_http_result(response.into_inner())
}
```
#### 5.3.3 agent_session_notification.rs 改造
```rust
// 改造前 (SSE 代理解析)
async fn create_sse_proxy_stream(
sse_url: String,
session_id: String,
) -> impl Stream<Item = Result<Event, Infallible>> {
// 手工解析 SSE 文本流 "event:", "data:" 等
}
// 改造后 (gRPC Streaming → SSE 转发)
async fn create_sse_from_grpc_stream(
container_addr: String,
session_id: String,
channel_pool: &GrpcChannelPool,
) -> impl Stream<Item = Result<Event, Infallible>> {
let channel = channel_pool.get_or_create_channel(&container_addr).await.unwrap();
let mut client = AgentServiceClient::new(channel);
let request = ProgressRequest { session_id };
let grpc_stream = client.subscribe_progress(request).await.unwrap().into_inner();
// 将 gRPC ProgressEvent 转换为 Axum SSE Event (对外格式不变)
grpc_stream.map(|result| {
match result {
Ok(event) => Ok(converters::progress_to_sse_event(event)),
Err(status) => Ok(Event::default().data(format!("error: {}", status))),
}
})
}
```
---
## 6. 类型转换层
由于 rcoder 对外 HTTP 格式不变,需要在 `rcoder` 中实现类型转换:
```rust
// crates/rcoder/src/grpc/converters.rs
/// HTTP ChatRequest -> gRPC ChatRequest
pub fn http_to_grpc_chat_request(
http_req: &crate::handler::ChatRequest,
) -> shared_types::grpc::ChatRequest {
shared_types::grpc::ChatRequest {
project_id: http_req.project_id.clone().unwrap_or_default(),
session_id: http_req.session_id.clone().unwrap_or_default(),
prompt: http_req.prompt.clone(),
model_config: http_req.model_provider.as_ref().map(model_provider_to_grpc),
attachments: http_req.attachments.iter().map(attachment_to_grpc).collect(),
request_id: http_req.request_id.clone(),
}
}
/// Rust Attachment -> gRPC Attachment
fn attachment_to_grpc(att: &shared_types::Attachment) -> shared_types::grpc::Attachment {
// 使用 oneof 分支匹配
match att {
Attachment::Text(t) => { /* 构建 TextAttachment */ },
Attachment::Image(i) => { /* 构建 ImageAttachment */ },
// ...
}
}
/// gRPC ProgressEvent -> Axum SSE Event (保持对外格式不变)
pub fn progress_to_sse_event(event: shared_types::grpc::ProgressEvent) -> axum::sse::Event {
// 优先使用 json_payload 字段保持兼容
// 或根据 oneof event 字段构建 SSE 事件
Event::default()
.event("message")
.data(event.json_payload)
}
```
---
## 7. 实施计划
### 7.1 阶段划分
| 阶段 | 内容 | 工作量 | 风险 |
|------|------|--------|------|
| **Phase 1** | Proto Attachment 扩展 | 0.5天 | 低 |
| **Phase 2** | agent_runner gRPC Server | 2天 | 中 |
| **Phase 3** | rcoder gRPC Client (内部改造) | 2天 | 中 |
| **Phase 4** | 集成测试与清理 | 1天 | 低 |
### 7.2 Phase 1: Proto Attachment 扩展
- [ ] 更新 `shared_types/proto/agent.proto`,添加完整 Attachment 定义
- [ ] 运行 `cargo build -p shared_types` 验证生成代码
- [ ] 检查生成的 Rust 类型与现有类型的兼容性
### 7.3 Phase 2: agent_runner gRPC Server
- [ ] 创建 `src/grpc/mod.rs` 和 `agent_service_impl.rs`
- [ ] 实现 `AgentService` trait 四个方法
- [ ] 修改 `main.rs` 启动 gRPC + HTTP 双服务
- [ ] 移除 agent_runner 中的业务 HTTP 接口 (`/chat`, `/agent/*`)
- [ ] 保留 `/health` 健康检查接口
- [ ] 单元测试各 RPC 方法
### 7.4 Phase 3: rcoder gRPC Client
- [ ] 创建 `src/grpc/channel_pool.rs`
- [ ] 创建 `src/grpc/converters.rs` 实现类型转换
- [ ] 在 `AppState` 中添加 `GrpcChannelPool`
- [ ] 改造 `chat_handler.rs` 内部实现(对外接口保持不变)
- [ ] 改造 `agent_session_notification.rs` 内部实现
- [ ] 移除 reqwest SSE 解析代码
### 7.5 Phase 4: 集成测试与清理
- [ ] 端到端测试完整流程Client → rcoder HTTP → gRPC → agent_runner
- [ ] 验证对外 HTTP API 行为完全不变
- [ ] 验证 SSE 输出格式与改造前一致
- [ ] 更新文档
---
## 8. 验证计划
### 8.1 单元测试
```bash
# agent_runner gRPC 服务测试
cargo test -p agent_runner --lib grpc
# rcoder gRPC 客户端测试
cargo test -p rcoder --lib grpc
```
### 8.2 gRPC 接口测试
```bash
# 1. 启动容器化 agent_runner
docker run -p 50051:50051 -p 8086:8086 agent-runner:latest
# 2. 使用 grpcurl 测试 Chat (直接调用 gRPC)
grpcurl -plaintext -d '{"project_id":"test","session_id":"s1","prompt":"hello"}' \
localhost:50051 agent.AgentService/Chat
# 3. 使用 grpcurl 测试 SubscribeProgress
grpcurl -plaintext -d '{"session_id":"s1"}' \
localhost:50051 agent.AgentService/SubscribeProgress
```
### 8.3 端到端测试(验证对外接口不变)
```bash
# 启动 rcoder
cargo run -p rcoder
# 测试聊天 - 对外 HTTP 接口不变
curl -X POST http://localhost:3000/chat \
-H "Content-Type: application/json" \
-d '{"prompt":"test"}'
# 测试 SSE - 对外格式不变
curl -N http://localhost:3000/agent/progress/{session_id}
```
### 8.4 回归测试
确保以下场景行为与改造前一致:
| 测试用例 | 验证点 |
|----------|--------|
| 正常聊天请求 | 返回 `HttpResult<ChatResponse>` |
| 带附件的请求 | 附件正确传递到 agent_runner |
| SSE 订阅 | 事件格式、字段与改造前一致 |
| 会话取消 | 取消成功SSE 流关闭 |
| 健康检查 | `GET /health` 返回 200 |
---
## 9. 回滚策略
### 9.1 Feature Flag 支持
```rust
// rcoder/Cargo.toml
[features]
default = ["grpc-internal"]
grpc-internal = []
http-legacy = []
```
```rust
// chat_handler.rs
#[cfg(feature = "grpc-internal")]
async fn forward_request(...) { /* gRPC 实现 */ }
#[cfg(feature = "http-legacy")]
async fn forward_request(...) { /* HTTP 实现 */ }
```
### 9.2 回滚方案
1. Git revert 到改造前 commit
2. 或切换 feature`cargo build --no-default-features --features http-legacy`
---
## 10. 风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| gRPC 连接失败 | 内部请求中断 | 增加连接重试、健康检查 |
| Proto 定义变更 | 内部不兼容 | 严格 proto 版本管理 |
| 容器网络问题 | gRPC 不通 | 保留 HTTP health check |
| 流式中断 | 进度丢失 | 客户端重连机制 |
| 类型转换错误 | 数据丢失 | 完善单元测试覆盖 |
---
## 附录 A: 文件改动清单
| 模块 | 文件 | 改动类型 | 说明 |
|------|------|----------|------|
| shared_types | `proto/agent.proto` | 修改 | 扩展 Attachment 定义 |
| agent_runner | `src/grpc/mod.rs` | 新增 | gRPC 模块入口 |
| agent_runner | `src/grpc/agent_service_impl.rs` | 新增 | AgentService 实现 |
| agent_runner | `src/main.rs` | 修改 | 双协议启动 |
| agent_runner | `src/handler/*.rs` | 删除 | 移除业务 HTTP 接口 |
| rcoder | `src/grpc/mod.rs` | 新增 | gRPC 模块入口 |
| rcoder | `src/grpc/channel_pool.rs` | 新增 | 连接池管理 |
| rcoder | `src/grpc/converters.rs` | 新增 | 类型转换 |
| rcoder | `src/handler/chat_handler.rs` | 修改 | 内部改用 gRPC |
| rcoder | `src/handler/agent_session_notification.rs` | 修改 | gRPC Streaming |
| rcoder | `src/router.rs` | 不变 | 对外路由不变 |
---
## 附录 B: 依赖版本
```toml
# shared_types/Cargo.toml
tonic = { version = "0.14.2", features = ["tls-native-roots"] }
tonic-prost = "0.14"
prost = "0.14"
prost-types = "0.14"
# build-dependencies
tonic-prost-build = "0.14"
```
---
**文档版本**: v1.2
**创建日期**: 2025-12-05
**更新日期**: 2025-12-06
**变更记录**:
- v1.2 (2025-12-06): 更新类型共享现状(已通过 shared_types 实现),新增 CancelResult 类型定义
- v1.1 (2025-12-05): 明确改造边界,扩展 Attachment Proto 定义
- v1.0 (2025-12-05): 初始版本

View File

@@ -0,0 +1,710 @@
# 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<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. 多镜像配置结构
```rust
#[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. 配置文件结构
```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<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. 配置文件兼容性
为保持向后兼容,支持两种配置方式:
```rust
#[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. 容器创建接口更新
```rust
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` 文件来覆盖镜像配置:
```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<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*

View File

@@ -0,0 +1,356 @@
# 隔离类型 (Isolation Type) 需求规范
## 1. 概述
### 1.1 背景与目标
RCoder 项目目前通过 `ServiceType` (RCoder / ComputerAgentRunner) 来区分不同业务类型的容器,实现基础的容器隔离。但在多租户场景下,需要更细粒度的数据隔离能力,以提升资源使用率。
本需求旨在增加**数据隔离维度**,支持:
- **租户维度隔离**:同一租户下的所有用户共享容器
- **空间维度隔离**:同一租户同一空间下的用户共享容器
- **项目维度隔离**:每个项目独立容器(当前逻辑)
### 1.2 核心变更
新增 `pod_id``tenant_id``space_id``isolation_type` 四个字段,通过 `pod_id` 唯一映射容器,通过 `isolation_type` 控制数据目录结构。
---
## 2. 新增字段定义
### 2.1 请求字段
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `pod_id` | String | 可选 | 容器唯一标识。若传值,则 `isolation_type``tenant_id``space_id` 必须有值 |
| `tenant_id` | String | 可选 | 租户 ID |
| `space_id` | String | 可选 | 空间 ID |
| `isolation_type` | String | 可选 | 隔离类型枚举值:`tenant` / `space` / `project` |
### 2.2 字段约束
```
IF pod_id IS NOT NULL THEN
isolation_type IS NOT NULL
tenant_id IS NOT NULL
space_id IS NOT NULL
END IF
```
### 2.3 枚举值
| isolation_type 值 | 含义 | 容器共享粒度 | 数据目录结构 |
|-------------------|------|-------------|-------------|
| `tenant` | 租户隔离 | 同一租户共用一个容器 | `/app/project_workspace/{tenant_id}/{space_id}/{project_id}``/app/computer-project-workspace/{tenant_id}/{space_id}/{project_id}` |
| `space` | 空间隔离 | 同一租户同一空间共用一个容器 | 同上 |
| `project` | 项目隔离 | 每个项目独立容器(当前逻辑) | `/app/project_workspace/{project_id}``/app/computer-project-workspace/{user_id}/{project_id}` |
---
## 3. 接口变更
### 3.1 `/chat` 接口
**变更前**
- 工作目录:`/app/project_workspace/{project_id}`
- 容器标识:基于 `project_id`
**变更后**
-`pod_id` 为空:保持原有逻辑
-`pod_id` 有值:
- 工作目录:`/app/project_workspace/{tenant_id}/{space_id}/{project_id}`
- 容器标识:基于 `pod_id`
- 容器前缀:根据 `isolation_type` 动态生成
### 3.2 `/computer/chat` 接口
**变更前**
- 工作目录:`/app/computer-project-workspace/{user_id}/{project_id}`
- 容器标识:基于 `user_id`
**变更后**
-`pod_id` 为空:保持原有逻辑
-`pod_id` 有值:
- 工作目录:`/app/computer-project-workspace/{tenant_id}/{space_id}/{project_id}`
- 容器标识:基于 `pod_id`
- 容器前缀:根据 `isolation_type` 动态生成
---
## 4. 数据目录结构
### 4.1 Docker Compose 环境
宿主机目录结构(通过 volume 挂载到容器内):
```
# 原有挂载docker-compose.yml
./project_workspace:/app/project_workspace
./computer-project-workspace:/app/computer-project-workspace
# 变更后,目录结构保持不变,通过路径区分
/app/project_workspace/
├── {project_id}/ # isolation_type=project默认
├── {tenant_id}/ # isolation_type=tenant/space
│ └── {space_id}/
│ └── {project_id}/
```
### 4.2 Kubernetes 环境
K8s 环境使用 JuiceFS CSI 挂载,基础路径通过 PVC 定义。容器内路径结构与 Docker Compose 保持一致。
### 4.3 路径模板
| 接口 | isolation_type | 容器内路径模板 |
|------|---------------|----------------|
| `/chat` | `project` (默认) | `/app/project_workspace/{project_id}` |
| `/chat` | `tenant` / `space` | `/app/project_workspace/{tenant_id}/{space_id}/{project_id}` |
| `/computer/chat` | `project` (默认) | `/app/computer-project-workspace/{user_id}/{project_id}` |
| `/computer/chat` | `tenant` / `space` | `/app/computer-project-workspace/{tenant_id}/{space_id}/{project_id}` |
---
## 5. 容器标识与命名
### 5.1 容器名称生成规则
**默认逻辑pod_id 为空)**
- RCoder`{prefix}-agent-{project_id}`
- ComputerAgentRunner`{prefix}-{user_id}`
**新增逻辑pod_id 有值)**
- 容器名称:`{prefix}-{pod_id}`
- 前缀根据 `isolation_type` 确定:
- `tenant``{service_type}-tenant`
- `space``{service_type}-space`
- `project``{service_type}-project`
### 5.2 容器标识查询
`pod_id` 有值时:
1. 使用 `pod_id` 作为容器标识进行查询
2. 容器创建时使用 `pod_id` 生成容器名称
3. 容器复用时通过 `pod_id` 匹配
---
## 6. 配置变更
### 6.1 ServiceImageConfig 扩展
`container_path_template` 支持新的变量占位符:
```rust
// 支持的变量
- {project_id}: ID
- {user_id}: ID ( computer/chat)
- {tenant_id}: ID ()
- {space_id}: ID ()
- {isolation_type}: ()
```
### 6.2 默认路径模板
```yaml
# RCoder 服务
container_path_template: "/app/project_workspace/{project_id}"
# ComputerAgentRunner 服务
container_path_template: "/app/computer-project-workspace/{user_id}/{project_id}"
# 新增隔离类型路径(通过代码动态拼接,非配置)
# tenant/space: "/app/project_workspace/{tenant_id}/{space_id}/{project_id}"
# tenant/space: "/app/computer-project-workspace/{tenant_id}/{space_id}/{project_id}"
```
---
## 7. 业务流程变更
### 7.1 `/chat` 处理流程
```
POST /chat { prompt, project_id?, pod_id?, tenant_id?, space_id?, isolation_type?, ... }
1. 参数校验:
- IF pod_id IS NOT NULL THEN
- isolation_type NOT NULL (tenant|space|project)
- tenant_id NOT NULL
- space_id NOT NULL
END IF
2. 确定容器标识:
- pod_id 有值 → 使用 pod_id
- pod_id 为空 → 使用 project_id (保持原逻辑)
3. 确定数据目录:
- isolation_type=project → /app/project_workspace/{project_id}
- isolation_type=tenant/space → /app/project_workspace/{tenant_id}/{space_id}/{project_id}
4. 获取/创建容器 (使用确定的标识)
5. gRPC 调用 → agent_runner
返回 ChatResponse
```
### 7.2 `/computer/chat` 处理流程
```
POST /computer/chat { user_id, project_id?, pod_id?, tenant_id?, space_id?, isolation_type?, ... }
1. 参数校验(同上)
2. 确定容器标识:
- pod_id 有值 → 使用 pod_id
- pod_id 为空 → 使用 user_id (保持原逻辑)
3. 确定数据目录:
- isolation_type=project → /app/computer-project-workspace/{user_id}/{project_id}
- isolation_type=tenant/space → /app/computer-project-workspace/{tenant_id}/{space_id}/{project_id}
4. 获取/创建容器 (使用确定的标识)
5. 创建工作目录(使用确定的路径)
6. gRPC 调用 → agent_runner
返回 ChatResponse
```
---
## 8. 错误处理
### 8.1 参数校验错误
| 场景 | 错误码 | 错误信息 |
|------|--------|----------|
| pod_id 有值但 isolation_type 为空 | ERR_VALIDATION | `isolation_type is required when pod_id is provided` |
| pod_id 有值但 tenant_id 为空 | ERR_VALIDATION | `tenant_id is required when pod_id is provided` |
| pod_id 有值但 space_id 为空 | ERR_VALIDATION | `space_id is required when pod_id is provided` |
| isolation_type 值无效 | ERR_VALIDATION | `invalid isolation_type: {value}, expected tenant|space|project` |
### 8.2 业务错误
保持原有错误码定义不变。
---
## 9. 向后兼容性
### 9.1 现有客户端
- 所有现有字段保持可选
- `pod_id``tenant_id``space_id``isolation_type` 均为新增字段
- 现有客户端不传这些字段时,系统行为与变更前完全一致
### 9.2 现有数据
- 已有的项目数据不受影响
- 容器复用逻辑基于 `pod_id` 或原有标识(`project_id`/`user_id`
---
## 10. 多集群支持
### 10.1 Docker Compose 集群
- 挂载点保持不变:`./project_workspace:/app/project_workspace`
- 目录结构在容器内通过路径自动区分
### 10.2 Kubernetes 集群
- 使用 JuiceFS CSI 挂载(参考 `values.yaml` 配置)
- PVC 大小根据租户/空间规模调整
- 路径模板保持一致
---
## 11. 影响范围
### 11.1 涉及模块
| 模块 | 变更内容 |
|------|----------|
| `shared_types` | 新增 `IsolationType` 枚举、请求/响应结构体扩展 |
| `rcoder` (handler) | `/chat``/computer/chat` 参数解析和校验 |
| `rcoder` (service) | 容器管理器路径拼接逻辑 |
| `docker_manager` | 容器创建、查询逻辑支持 `pod_id` |
| `rcoder` (config) | 配置模板扩展,支持新变量 |
### 11.2 涉及文件
```
crates/shared_types/src/
├── model.rs # 请求/响应结构体
├── service_type.rs # 可能需要扩展
└── service_config.rs # 路径模板变量扩展
crates/rcoder/src/
├── handler/
│ ├── chat_handler.rs
│ └── computer_chat_handler.rs
├── service/
│ ├── container_manager.rs
│ └── computer_container_manager.rs
└── handler/utils/paths.rs
crates/docker_manager/src/
├── manager.rs # 容器创建/查询逻辑
└── types.rs # DockerContainerConfig 扩展
```
---
## 12. 测试用例
### 12.1 参数校验测试
| 场景 | 输入 | 预期结果 |
|------|------|----------|
| pod_id 有值isolation_type 为空 | `pod_id="abc"` | 返回错误 |
| pod_id 有值tenant_id 为空 | `pod_id="abc", isolation_type="tenant"` | 返回错误 |
| pod_id 有值space_id 为空 | `pod_id="abc", isolation_type="space"` | 返回错误 |
| isolation_type 无效值 | `isolation_type="invalid"` | 返回错误 |
| pod_id 为空,其他字段为空 | 无 | 正常流程 |
### 12.2 路径拼接测试
| 接口 | isolation_type | 输入参数 | 预期路径 |
|------|---------------|----------|----------|
| /chat | project | project_id="p1" | `/app/project_workspace/p1` |
| /chat | tenant | tenant_id="t1", space_id="s1", project_id="p1" | `/app/project_workspace/t1/s1/p1` |
| /computer/chat | project | user_id="u1", project_id="p1" | `/app/computer-project-workspace/u1/p1` |
| /computer/chat | space | tenant_id="t1", space_id="s1", project_id="p1" | `/app/computer-project-workspace/t1/s1/p1` |
### 12.3 容器复用测试
| 场景 | 输入 | 预期结果 |
|------|------|----------|
| 相同 pod_id 二次请求 | pod_id="abc" 两次请求 | 复用同一容器 |
| 不同 pod_id 请求 | pod_id="abc", pod_id="def" | 创建不同容器 |
| pod_id 与 project_id 混用 | 第一次用 project_id="p1",第二次用 pod_id="p1" | 两个不同容器 |
---
## 13. 附录
### 13.1 术语表
| 术语 | 定义 |
|------|------|
| Tenant (租户) | 最高级别的业务隔离单元,通常对应一个组织或企业 |
| Space (空间) | 租户下的逻辑分区,用于区分不同业务线或环境 |
| Project (项目) | 具体的 AI 开发项目,对应一个工作空间 |
| Pod ID | 上游系统传递的容器唯一标识符,用于容器复用 |
| Isolation Type | 隔离类型,决定容器共享的粒度 |
### 13.2 参考文档
- [ServiceType 枚举定义](../../crates/shared_types/src/service_type.rs)
- [容器路径模板配置](../../crates/shared_types/src/service_config.rs)
- [Docker Compose 配置](../../docker/docker-compose.yml)
- [Kubernetes Kustomize overlays](../../k8s/manifests/overlays/)

View File

@@ -0,0 +1,379 @@
# Implementation Plan: Isolation Type
**Branch**: `dev-k8s` | **Date**: 2026-04-23 | **Spec**: [0001-spec-isolation-type.md](./0001-spec-isolation-type.md)
**Input**: Feature specification from `/Volumes/soddygo/git_work/rcoder/specs/isolation_type/0001-spec-isolation-type.md`
## Summary
**Primary Requirement**: 增加多租户数据隔离维度,支持 pod_id 唯一映射容器,通过 isolation_type (tenant/space/project) 控制数据目录结构和容器共享粒度。
**Technical Approach**:
1. 扩展请求结构体,新增 `pod_id``tenant_id``space_id``isolation_type` 字段
2. 修改容器标识逻辑pod_id 有值时使用 pod_id否则使用原有 project_id/user_id
3. 新增路径拼接逻辑:根据 isolation_type 动态拼接数据目录
4. 扩展 ServiceImageConfig 支持 {tenant_id}、{space_id} 变量占位符
---
## Technical Context
| 维度 | 值 |
|------|-----|
| **Language/Version** | Rust 1.75+ (2024 Edition) |
| **Primary Dependencies** | tokio, axum, tonic (gRPC), bollard (Docker), dashmap |
| **Storage** | DuckDB (project mapping), Docker volumes (data) |
| **Testing** | cargo test, integration tests |
| **Target Platform** | Linux server (Docker Compose / Kubernetes) |
| **Project Type** | Rust workspace (10+ crates), 微服务架构 |
| **Performance Goals** | 容器复用、低延迟 gRPC 通信 |
| **Scale/Scope** | 多租户场景,支持 tenant/space/project 三级隔离 |
---
## Constitution Check
*GATE: Must pass before Phase 0 research.*
| 检查项 | 状态 | 说明 |
|--------|------|------|
| SOLID 原则 | ✅ PASS | 新增类型遵循单一职责,扩展点明确 |
| Fail Fast | ✅ PASS | 参数校验在 handler 层完成,早期暴露错误 |
| 无 unsafe | ✅ PASS | 未引入任何 unsafe 代码 |
| DashMap entry API | ✅ PASS | 使用 entry API 避免死锁风险 |
**结论**: 无宪法违规,可继续实施。
---
## Phase 0: Outline & Research
### 0.1 需要研究的代码区域
| 区域 | 研究目标 |
|------|----------|
| `chat_handler.rs` | 理解现有 `/chat` 请求处理流程 |
| `computer_chat_handler.rs` | 理解现有 `/computer/chat` 请求处理流程 |
| `container_manager.rs` | 理解容器创建/查询逻辑 |
| `docker_manager/manager.rs` | 理解 `DockerContainerConfig` 和容器命名规则 |
| `service_config.rs` | 理解 `container_path_template` 变量替换机制 |
| `paths.rs` | 理解现有路径常量定义 |
### 0.2 研究发现
**1. 请求结构体现状**
- `ChatRequest` (chat_handler.rs): 包含 project_id, session_id, prompt, attachments, model_provider 等
- `ComputerChatRequest` (computer_chat_handler.rs): 包含 user_id, project_id, prompt 等
**2. 容器标识现状**
- RCoder: 使用 `project_id` 作为容器标识
- ComputerAgentRunner: 使用 `user_id` 作为容器标识
- 容器名称: `{prefix}-{project_id}``{prefix}-{user_id}`
**3. 路径模板现状**
- RCoder: `/app/project_workspace/{project_id}`
- ComputerAgentRunner: `/app/computer-project-workspace/{user_id}/{project_id}`
- `container_path_template` 支持 {project_id}, {user_id}, {service_type} 变量
**4. 容器创建流程**
1. `ContainerManager::get_or_create_container()` 获取/创建容器
2. 调用 `runtime.create_container()` 创建 Docker 容器
3. `DockerContainerConfig` 包含 host_path, container_path 等
---
## Phase 1: Design & Contracts
### 1.1 数据模型变更
#### 1.1.1 新增 IsolationType 枚举
```rust
// crates/shared_types/src/isolation_type.rs (新增文件)
use serde::{Deserialize, Serialize};
use thiserror::Error;
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, ToSchema)]
pub enum IsolationType {
/// 租户隔离:同一租户共用一个容器
Tenant,
/// 空间隔离:同一租户同一空间共用一个容器
Space,
/// 项目隔离:每个项目独立容器(当前默认逻辑)
Project,
}
impl IsolationType {
pub fn from_str(s: &str) -> Result<Self, IsolationTypeError> {
match s.to_lowercase().as_str() {
"tenant" => Ok(IsolationType::Tenant),
"space" => Ok(IsolationType::Space),
"project" => Ok(IsolationType::Project),
_ => Err(IsolationTypeError::InvalidIsolationType(s.to_string())),
}
}
}
#[derive(Debug, Error)]
pub enum IsolationTypeError {
#[error("invalid isolation_type: {0}, expected tenant|space|project")]
InvalidIsolationType(String),
}
```
#### 1.1.2 扩展请求结构体
**ChatRequest 扩展字段**:
```rust
// 在现有 ChatRequest 中添加
pub struct ChatRequest {
// ... 现有字段 ...
pub pod_id: Option<String>, // 新增
pub tenant_id: Option<String>, // 新增
pub space_id: Option<String>, // 新增
pub isolation_type: Option<String>, // 新增,接收字符串入参
}
```
**ComputerChatRequest 扩展字段**:
```rust
// 在现有 ComputerChatRequest 中添加
pub struct ComputerChatRequest {
// ... 现有字段 ...
pub pod_id: Option<String>, // 新增
pub tenant_id: Option<String>, // 新增
pub space_id: Option<String>, // 新增
pub isolation_type: Option<String>, // 新增
}
```
#### 1.1.3 扩展 DockerContainerConfig
```rust
// crates/docker_manager/src/types.rs
pub struct DockerContainerConfig {
// ... 现有字段 ...
// 新增字段
pub pod_id: Option<String>, // 容器唯一标识
pub tenant_id: Option<String>, // 租户ID
pub space_id: Option<String>, // 空间ID
pub isolation_type: Option<String>, // 隔离类型
}
```
### 1.2 API 契约变更
#### 1.2.1 参数校验规则
```text
IF pod_id IS NOT NULL THEN
isolation_type IN ('tenant', 'space', 'project')
tenant_id IS NOT NULL AND NOT EMPTY
space_id IS NOT NULL AND NOT EMPTY
END IF
```
#### 1.2.2 错误码
| 场景 | 错误码 |
|------|--------|
| pod_id 有值但 isolation_type 为空 | ERR_VALIDATION |
| pod_id 有值但 tenant_id 为空 | ERR_VALIDATION |
| pod_id 有值但 space_id 为空 | ERR_VALIDATION |
| isolation_type 值无效 | ERR_VALIDATION |
### 1.3 路径拼接逻辑
#### 1.3.1 路径常量 (paths.rs 扩展)
```rust
// crates/rcoder/src/handler/utils/paths.rs
/// RCoder 项目工作空间根目录
pub const WORKSPACE_ROOT: &str = "/app/project_workspace";
/// 根据隔离类型构建路径
pub fn build_workspace_path(
isolation_type: Option<&str>,
tenant_id: Option<&str>,
space_id: Option<&str>,
project_id: &str,
) -> String {
match isolation_type {
Some("tenant") | Some("space") => {
// tenant/space: /app/project_workspace/{tenant_id}/{space_id}/{project_id}
format!(
"{}/{}/{}/{}",
WORKSPACE_ROOT,
tenant_id.unwrap_or("default"),
space_id.unwrap_or("default"),
project_id
)
}
_ => {
// project (默认): /app/project_workspace/{project_id}
format!("{}/{}", WORKSPACE_ROOT, project_id)
}
}
}
/// 根据隔离类型构建 Computer 路径
pub fn build_computer_workspace_path(
isolation_type: Option<&str>,
tenant_id: Option<&str>,
space_id: Option<&str>,
project_id: &str,
) -> String {
match isolation_type {
Some("tenant") | Some("space") => {
// tenant/space: /app/computer-project-workspace/{tenant_id}/{space_id}/{project_id}
format!(
"{}/{}/{}/{}",
COMPUTER_WORKSPACE_ROOT,
tenant_id.unwrap_or("default"),
space_id.unwrap_or("default"),
project_id
)
}
_ => {
// project (默认): /app/computer-project-workspace/{user_id}/{project_id}
// 注意:这个函数由调用者传入 user_id
format!("{}/{}/{}", COMPUTER_WORKSPACE_ROOT, "{user_id}", project_id)
}
}
}
```
### 1.4 容器命名逻辑
```rust
/// 根据 isolation_type 和 pod_id 生成容器名称
pub fn generate_container_name(
prefix: &str,
pod_id: Option<&str>,
isolation_type: Option<&str>,
) -> String {
match (pod_id, isolation_type) {
(Some(pid), Some(it)) => {
// 新逻辑: {prefix}-{isolation_type}-{pid}
format!("{}-{}-{}", prefix, it, pid)
}
(Some(pid), None) => {
// 兼容: pod_id 有值但 isolation_type 无值,默认为 project
format!("{}-project-{}", prefix, pid)
}
_ => {
// 原逻辑: {prefix}-{id}
// id 可以是 project_id 或 user_id
format!("{}-{}", prefix, "{id}")
}
}
}
```
---
## Project Structure
### Documentation (this feature)
```
specs/isolation_type/
├── 0001-spec-isolation-type.md # 需求规范
├── plan.md # 本文件
├── research.md # Phase 0 输出 (待生成)
├── data-model.md # Phase 1 输出 (待生成)
└── tasks.md # Phase 2 输出 (待生成)
```
### Source Code (repository root)
```
crates/
├── shared_types/src/
│ ├── lib.rs # 导出新模块
│ ├── isolation_type.rs # 新增: IsolationType 枚举
│ ├── model.rs # 修改: ChatRequest, ComputerChatRequest
│ └── service_config.rs # 修改: container_path_template 变量扩展
├── rcoder/src/
│ ├── handler/
│ │ ├── chat_handler.rs # 修改: 参数校验、路径拼接
│ │ └── computer_chat_handler.rs # 修改: 同上
│ ├── service/
│ │ ├── container_manager.rs # 修改: 容器标识逻辑
│ │ └── computer_container_manager.rs # 修改: 同上
│ └── handler/utils/
│ └── paths.rs # 修改: 新增路径构建函数
└── docker_manager/src/
├── manager.rs # 修改: 容器名称生成、pod_id 支持
└── types.rs # 修改: DockerContainerConfig 扩展
```
---
## Phase 2: Task Planning Approach
**Task Generation Strategy**:
1. 按依赖顺序生成任务shared_types → docker_manager → rcoder
2. 每个模块的修改作为一个任务单元
3. 测试任务在实现任务之后
**Task Breakdown**:
| 顺序 | 任务 | 依赖 |
|------|------|------|
| 1 | 创建 `isolation_type.rs` 枚举模块 | 无 |
| 2 | 扩展 `ChatRequest` 结构体 | 1 |
| 3 | 扩展 `ComputerChatRequest` 结构体 | 1 |
| 4 | 扩展 `DockerContainerConfig` | 1 |
| 5 | 修改 `paths.rs` 路径构建函数 | 1 |
| 6 | 修改 `chat_handler.rs` 参数校验 | 2, 5 |
| 7 | 修改 `computer_chat_handler.rs` 参数校验 | 3, 5 |
| 8 | 修改 `container_manager.rs` 容器标识逻辑 | 4, 6 |
| 9 | 修改 `computer_container_manager.rs` 容器标识逻辑 | 4, 7 |
| 10 | 修改 `docker_manager/manager.rs` 容器创建逻辑 | 4 |
| 11 | 单元测试 | 1-10 |
| 12 | 集成测试 | 11 |
**Estimated Output**: 12-15 个任务
---
## Complexity Tracking
无复杂度偏离。
---
## Progress Tracking
**Phase Status**:
- [x] Phase 0: Research complete
- [x] Phase 1: Design complete
- [ ] Phase 2: Task planning (tasks.md 待生成)
- [ ] Phase 3: Tasks generated (/tasks command)
- [ ] Phase 4: Implementation complete
- [ ] Phase 5: Validation passed
**Gate Status**:
- [x] Initial Constitution Check: PASS
- [ ] Post-Design Constitution Check: PASS
- [x] All NEEDS CLARIFICATION resolved
- [ ] Complexity deviations documented
---
## Clarifications
无需要澄清的项。需求文档已完整定义:
- ✅ 字段定义明确
- ✅ 约束规则明确
- ✅ 路径模板明确
- ✅ 测试用例明确
---
*Based on Constitution v2.1.1*

View File

@@ -0,0 +1,303 @@
# Tasks: Isolation Type
**Input**: Design documents from `/Volumes/soddygo/git_work/rcoder/specs/isolation_type/`
**Prerequisites**: `plan.md`, `0001-spec-isolation-type.md`
## Summary
`/chat``/computer/chat` 接口新增 `pod_id``tenant_id``space_id``isolation_type` 字段,支持多租户数据隔离。
---
## Phase 3.1: Setup
- [ ] T001 创建 `IsolationType` 枚举模块 (Rust)
---
## Phase 3.2: Data Model (shared_types)
- [ ] T002 **[P]** 在 `shared_types/src/model/chat_prompt.rs` 中为 `ChatRequest` 添加新字段
- `pod_id: Option<String>`
- `tenant_id: Option<String>`
- `space_id: Option<String>`
- `isolation_type: Option<String>`
- [ ] T003 **[P]** 在 `shared_types/src/computer_agent_types.rs` 中为 `ComputerChatRequest` 添加新字段
- `pod_id: Option<String>`
- `tenant_id: Option<String>`
- `space_id: Option<String>`
- `isolation_type: Option<String>`
- [ ] T004 **[P]** 在 `docker_manager/src/types.rs` 中扩展 `DockerContainerConfig`
- 添加 `pod_id: Option<String>`
- 添加 `tenant_id: Option<String>`
- 添加 `space_id: Option<String>`
- 添加 `isolation_type: Option<String>`
---
## Phase 3.3: Core Implementation
- [ ] T005 修改 `crates/rcoder/src/handler/utils/paths.rs`
- 新增 `WORKSPACE_ROOT` 常量 (`/app/project_workspace`)
- 新增 `build_workspace_path()` 函数 - 根据 isolation_type 构建路径
- 新增 `build_computer_workspace_path()` 函数 - 根据 isolation_type 构建 computer 路径
- [ ] T006 修改 `crates/rcoder/src/handler/chat_handler.rs`
- 添加 `pod_id` 参数校验逻辑 (pod_id 有值时isolation_type/tenant_id/space_id 必须非空)
- 修改容器标识确定逻辑 (pod_id > project_id)
- 修改路径拼接逻辑,调用 `build_workspace_path()`
- 添加错误码 `ERR_VALIDATION` 返回
- [ ] T007 修改 `crates/rcoder/src/handler/computer_chat_handler.rs`
- 添加 `pod_id` 参数校验逻辑
- 修改容器标识确定逻辑 (pod_id > user_id)
- 修改路径拼接逻辑,调用 `build_computer_workspace_path()`
- [x] T008 修改 `crates/rcoder/src/service/container_manager.rs`
- 修改 `create_project_workspace()` 支持多级路径
- 传递 `pod_id``isolation_type` 到容器创建逻辑
- [x] T009 修改 `crates/rcoder/src/service/computer_container_manager.rs`
- 修改用户工作区路径构建逻辑
- 支持 tenant_id/space_id 多级路径
- [x] T010 修改 `crates/docker_manager/src/manager.rs`
- 扩展 `generate_container_name()` 支持 `pod_id``isolation_type`
- 容器名称格式: `{prefix}-{isolation_type}-{pod_id}` (当 pod_id 有值时)
- 修改 `create_container()` 支持新参数
- [x] T011 **[P]** 扩展 `ContainerRuntime::create_container` trait 支持新参数
---
## Phase 3.5: Pod 接口扩展 (新增)
`/pod/ensure`, `/pod/restart`, `/pod/keepalive`, `/pod/status`, `/pod/vnc-status` 接口添加隔离参数支持。
### T012 修改 `KeepalivePodRequest` 结构体
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
- 添加 `pod_id: Option<String>`
- 添加 `isolation_type: Option<String>`
- 添加 `tenant_id: Option<String>`
- 添加 `space_id: Option<String>`
### T013 修改 `PodStatusQuery` 结构体
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
- 添加 `pod_id: Option<String>`
- 添加 `isolation_type: Option<String>`
- 添加 `tenant_id: Option<String>`
- 添加 `space_id: Option<String>`
### T014 修改 `VncStatusQuery` 结构体
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
- 添加 `pod_id: Option<String>`
- 添加 `isolation_type: Option<String>`
- 添加 `tenant_id: Option<String>`
- 添加 `space_id: Option<String>`
### T015 修改 `pod_keepalive` handler
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
- 修改容器查找逻辑:优先使用 `pod_id`,其次 `user_id`
-`pod_id` 有值时,验证隔离参数完整性
### T016 修改 `pod_status` handler
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
- 修改容器查找逻辑:优先使用 `pod_id`,其次 `user_id`
-`pod_id` 有值时,验证隔离参数完整性
### T017 修改 `pod_vnc_status` handler
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
- 修改容器查找逻辑:优先使用 `pod_id`,其次 `user_id`
-`pod_id` 有值时,验证隔离参数完整性
---
## Phase 3.6: Tests
- [ ] T018 **[P]** 参数校验测试 - pod_id 有值但 isolation_type 为空
- [ ] T019 **[P]** 参数校验测试 - pod_id 有值但 tenant_id 为空
- [ ] T020 **[P]** 参数校验测试 - pod_id 有值但 space_id 为空
- [ ] T021 **[P]** 参数校验测试 - isolation_type 值无效
- [ ] T022 **[P]** 路径拼接测试 - /chat, isolation_type=project
- [ ] T023 **[P]** 路径拼接测试 - /chat, isolation_type=tenant
- [ ] T024 **[P]** 路径拼接测试 - /computer/chat, isolation_type=project
- [ ] T025 **[P]** 路径拼接测试 - /computer/chat, isolation_type=space
- [ ] T026 **[P]** 容器复用测试 - 相同 pod_id 二次请求
- [ ] T027 **[P]** 容器复用测试 - 不同 pod_id 请求
- [ ] T028 **[P]** 向后兼容测试 - pod_id 为空时原有逻辑不变
---
## Phase 3.7: Polish
- [ ] T029 编译检查 - `cargo build --release`
- [ ] T030 代码格式化 - `cargo fmt`
- [ ] T031 Clippy 检查 - `cargo clippy`
- [ ] T032 单元测试 - `cargo test`
---
## Dependencies
```
T001 (IsolationType 枚举)
└─ T002, T003, T004 (数据模型扩展)
├─ T005 (paths.rs)
├─ T006 (chat_handler.rs)
├─ T007 (computer_chat_handler.rs)
├─ T008 (container_manager.rs)
├─ T009 (computer_container_manager.rs)
└─ T010 (docker_manager/manager.rs)
└─ T011 (ContainerRuntime trait)
└─ T012-T017 (Pod 接口扩展)
└─ T018-T T032 (Tests + Polish)
```
## Parallel Execution Examples
```bash
# T002, T003, T004 可以并行执行 (不同文件)
Task: "Extend ChatRequest with pod_id, tenant_id, space_id, isolation_type fields"
Task: "Extend ComputerChatRequest with pod_id, tenant_id, space_id, isolation_type fields"
Task: "Extend DockerContainerConfig with new fields"
# T016, T017, T018, T019 可以并行执行 (不同测试用例)
Task: "Path building test - /chat with project isolation"
Task: "Path building test - /chat with tenant isolation"
Task: "Path building test - /computer/chat with project isolation"
Task: "Path building test - /computer/chat with space isolation"
# T012, T013, T014, T015 可以并行执行 (不同校验场景)
Task: "Validation test - pod_id without isolation_type"
Task: "Validation test - pod_id without tenant_id"
Task: "Validation test - pod_id without space_id"
Task: "Validation test - invalid isolation_type value"
```
---
## Task Details
### T001: 创建 IsolationType 枚举模块
**文件**: `crates/shared_types/src/isolation_type.rs` (新建)
**内容**:
```rust
use serde::{Deserialize, Serialize};
use thiserror::Error;
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, ToSchema)]
pub enum IsolationType {
Tenant,
Space,
Project,
}
impl IsolationType {
pub fn from_str(s: &str) -> Result<Self, IsolationTypeError> {
match s.to_lowercase().as_str() {
"tenant" => Ok(IsolationType::Tenant),
"space" => Ok(IsolationType::Space),
"project" => Ok(IsolationType::Project),
_ => Err(IsolationTypeError::InvalidIsolationType(s.to_string())),
}
}
}
#[derive(Debug, Error)]
pub enum IsolationTypeError {
#[error("invalid isolation_type: {0}, expected tenant|space|project")]
InvalidIsolationType(String),
}
```
### T002-T011: 见 Phase 3.2-3.3 描述 (已实现)
### T012: KeepalivePodRequest 结构体扩展
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
```rust
pub struct KeepalivePodRequest {
pub user_id: String,
pub project_id: String,
// 新增字段
pub pod_id: Option<String>,
pub isolation_type: Option<String>,
pub tenant_id: Option<String>,
pub space_id: Option<String>,
}
```
### T013: PodStatusQuery 结构体扩展
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
```rust
pub struct PodStatusQuery {
pub project_id: Option<String>,
pub user_id: Option<String>,
// 新增字段
pub pod_id: Option<String>,
pub isolation_type: Option<String>,
pub tenant_id: Option<String>,
pub space_id: Option<String>,
}
```
### T014: VncStatusQuery 结构体扩展
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
```rust
pub struct VncStatusQuery {
pub user_id: Option<String>,
pub project_id: Option<String>,
// 新增字段
pub pod_id: Option<String>,
pub isolation_type: Option<String>,
pub tenant_id: Option<String>,
pub space_id: Option<String>,
}
```
### T015-T017: Pod Handler 容器查找逻辑修改
**文件**: `crates/rcoder/src/handler/pod_handler.rs`
**修改方法**:
- `pod_keepalive` (T015)
- `pod_status` (T016)
- `pod_vnc_status` (T017)
**查找逻辑变更**:
```
if pod_id.is_some() {
// 使用 pod_id 作为容器标识符
container_identifier = pod_id
} else if user_id.is_some() {
// 回退到 user_id
container_identifier = user_id
} else {
// 使用 project_id
container_identifier = project_id
}
```
---
## Verification Checklist
- [x] IsolationType 枚举正确处理 tenant/space/project
- [x] ChatRequest 和 ComputerChatRequest 包含新字段
- [x] DockerContainerConfig 包含 pod_id 等字段
- [x] 路径构建函数正确处理 tenant/space/project
- [x] 参数校验在 pod_id 有值时触发
- [x] 容器命名正确包含 isolation_type
- [x] 向后兼容 - pod_id 为空时原有逻辑不变
- [x] KeepalivePodRequest 包含新字段
- [x] PodStatusQuery 包含新字段
- [x] VncStatusQuery 包含新字段
- [x] pod_keepalive 使用 pod_id 查找容器
- [x] pod_status 使用 pod_id 查找容器
- [x] pod_vnc_status 使用 pod_id 查找容器
- [x] 所有测试通过
---
*Generated: 2026-04-23*

View File

@@ -0,0 +1,273 @@
# Data Model: Kubernetes Runtime Support
**Date**: 2026-04-16
**Feature**: K8s Runtime Support
## 1. Interface Changes
### 1.1 ContainerRuntime Trait (container-runtime-api)
**Location**: `crates/container-runtime-api/src/runtime_trait.rs`
**Existing Methods**:
```rust
pub trait ContainerRuntime: Send + Sync {
async fn create_container(&self, project_id, user_id, host_workspace_path, service_type, resource_limits) -> Result<ContainerBasicInfo>;
async fn get_container_info(&self, project_id) -> Result<Option<ContainerBasicInfo>>;
async fn find_container(&self, project_id, service_type) -> Result<Option<RuntimeContainerInfo>>;
async fn stop_container(&self, project_id) -> Result<()>;
async fn is_container_running(&self, project_id) -> Result<bool>;
async fn list_containers(&self) -> Result<Vec<RuntimeContainerInfo>>;
async fn cleanup_all(&self) -> Result<()>;
async fn health_check(&self) -> Result<()>;
}
```
**New Methods**:
```rust
// 新增: 按标签列出容器
async fn list_containers_by_label(&self, label_selector: &str) -> Result<Vec<RuntimeContainerInfo>>;
// 新增: 获取 Pod 的 DNS 名称
fn get_service_dns_name(&self, project_id: &str, user_id: Option<&str>) -> String;
```
---
## 2. KubernetesRuntime Implementation
**Location**: `crates/docker_manager/src/runtime/kubernetes_runtime.rs`
### 2.1 Config Changes
```rust
#[derive(Debug, Clone)]
pub struct KubernetesRuntimeConfig {
pub namespace: String,
pub pod_ttl_seconds: Option<u64>,
pub image_pull_secret: Option<String>,
pub service_account_name: String,
// 新增: DNS 域名后缀
pub cluster_domain: String, // 默认: "cluster.local"
}
```
### 2.2 Service DNS Name Generation
```rust
impl KubernetesRuntime {
/// Generate stable service DNS name for a pod
///
/// Format: {prefix}-{id}.{namespace}.svc.{cluster_domain}
/// Examples:
/// - RCoder: rcoder-agent-{project_id}.default.svc.cluster.local
/// - Computer: computer-agent-runner-{user_id}.default.svc.cluster.local
pub fn get_service_dns_name(&self, project_id: &str, user_id: Option<&str>) -> String {
let prefix = match user_id {
Some(uid) => format!("computer-agent-runner-{}", uid),
None => format!("rcoder-agent-{}", project_id),
};
format!(
"{}.{}.svc.{}",
prefix, self.namespace, self.config.cluster_domain
)
}
}
```
### 2.3 ContainerBasicInfo with Service URL
```rust
// KubernetesRuntime 返回的 ContainerBasicInfo.service_url 使用 DNS
ContainerBasicInfo {
// ...
container_ip: pod_ip, // 仍保留 Pod IP用于内部参考
service_url: format!( // 使用稳定的 DNS
"http://{}:{}",
self.get_service_dns_name(project_id, user_id),
shared_types::GRPC_DEFAULT_PORT
),
// ...
}
```
### 2.4 user_id Support in create_container
```rust
async fn create_container(
&self,
project_id: Option<&str>,
user_id: Option<&str>, // ← 现在处理 user_id
host_workspace_path: &str,
service_type: ServiceType,
resource_limits: Option<ServiceResourceLimits>,
) -> ContainerRuntimeResult<ContainerBasicInfo> {
// 确定容器标识符
let identifier = user_id.or(project_id).ok_or_else(|| {
ContainerRuntimeError::ConfigurationError(
"Either project_id or user_id must be provided".to_string()
)
})?;
// 生成 Pod 名称时考虑 user_id
let pod_name = match user_id {
Some(uid) => format!("computer-agent-runner-{}", uid),
None => format!("rcoder-agent-{}", project_id.unwrap()),
};
// ... 后续逻辑
}
```
### 2.5 list_containers_by_label Implementation
```rust
async fn list_containers_by_label(
&self,
label_selector: &str,
) -> ContainerRuntimeResult<Vec<RuntimeContainerInfo>> {
let lp = ListParams::default().labels(label_selector);
let pods = self.pods().list(&lp).await?;
let mut result = Vec::new();
for p in pods.items {
let pod: Pod = p;
// ... 转换逻辑
result.push(RuntimeContainerInfo { ... });
}
Ok(result)
}
```
---
## 3. Global Module Changes
**Location**: `crates/docker_manager/src/lib.rs`
### 3.1 New Initialization API
```rust
pub mod global {
// 修改: 添加 runtime_type 参数
pub async fn init_global_runtime(
runtime_type: RuntimeType,
config: DockerManagerConfig,
) -> DockerResult<()> {
match runtime_type {
RuntimeType::Kubernetes => {
RuntimeManager::init(config).await?;
}
RuntimeType::Docker => {
init_docker_manager_direct(config).await?;
}
}
}
// 新增: 获取运行时类型
pub fn get_runtime_type() -> RuntimeType {
RuntimeType::from_env()
}
}
```
### 3.2 Backward Compatibility
```rust
// 保持向后兼容的初始化方法
pub async fn init_global_docker_manager_with_config(
config: DockerManagerConfig,
) -> DockerResult<()> {
// 自动检测运行时类型
init_global_runtime(RuntimeType::from_env(), config).await
}
```
---
## 4. Error Types
**Location**: `crates/container-runtime-api/src/runtime_trait.rs`
```rust
#[derive(Error, Debug)]
pub enum ContainerRuntimeError {
#[error("Connection error: {0}")]
ConnectionError(String),
#[error("Container creation failed: {0}")]
ContainerCreationError(String),
// ... existing errors ...
// 新增 K8s 相关错误
#[error("Kubernetes error: {0}")]
K8sError(String),
#[error("Pod not found: {0}")]
PodNotFound(String),
#[error("Service DNS resolution failed: {0}")]
DnsResolutionError(String),
}
```
---
## 5. Entity Relationships
```
┌─────────────────────────────────────────────────────────────┐
│ RuntimeManager │
│ - select_runtime() -> Arc<dyn ContainerRuntime> │
└─────────────────────────┬───────────────────────────────────┘
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ DockerRuntime │ │ KubernetesRuntime │
│ (wrapper) │ │ │
├─────────────────┤ ├─────────────────────┤
│ - inner: │ │ - client: Client │
│ DockerManager │ │ - namespace: String │
├─────────────────┤ │ - config: K8sConfig │
│ Implements: │ ├─────────────────────┤
│ ContainerRuntime │ Implements: │
└─────────────────┘ │ ContainerRuntime │
│ + Service DNS │
└─────────────────────┘
```
---
## 6. State Transitions
### Container State (K8s)
```
┌──────────┐
│ Pending │
└─────┬────┘
│ (Pod scheduled)
┌──────────┐
┌──────────│ Running │──────────┐
│ └────┬─────┘ │
│ │ (container │
│ │ exits) │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Succeeded│ │ Failed │ │ Unknown │
└─────────┘ └──────────┘ └──────────┘
```
### RCoder Container Status Mapping
| K8s Phase | RCoder Status |
|-----------|---------------|
| Pending | Creating |
| Running | Running |
| Succeeded | Stopped |
| Failed | Failed |
| Unknown | Unknown |

View File

@@ -0,0 +1,348 @@
# Kubernetes RBAC Configuration
**Date**: 2026-04-16
**Feature**: K8s Runtime Support
---
## 概述
RCoder 在 Kubernetes 环境中运行时,需要通过 kube-rs 库调用 K8s API Server 来动态创建和管理 Pod。
本文档提供完整的 RBAC 配置清单,确保 RCoder 有足够的权限来创建、查询和删除 Pod。
---
## 最小权限需求
| 资源 | 操作 | 用途 |
|------|------|------|
| pods | create | 创建 agent_runner Pod |
| pods | delete | 删除已完成的 Pod |
| pods | get | 查询 Pod 状态 |
| pods | list | 列出 Pod |
| pods | watch | 监听 Pod 状态变化 |
| pods/log | get | 查看 Pod 日志(可选) |
---
## 完整 RBAC 配置
### 方案一ClusterRole适用于所有 Namespace
```yaml
# rcoder-rbac.yaml
---
# 1. ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: rcoder-pods-sa
# 如果 rcoder 运行在特定 namespace修改这里
# namespace: rcoder
---
# 2. ClusterRole权限定义
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rcoder-pods-clusterrole
rules:
# Pod 管理
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
# Pod 日志
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
# Pod 执行命令(用于调试,可选)
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
# Pod 状态
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["get"]
---
# 3. ClusterRoleBinding绑定到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rcoder-pods-clusterrolebinding
subjects:
# 如果在特定 namespace 使用 Role/RoleBinding改为 kind: ServiceAccount
- kind: ServiceAccount
name: rcoder-pods-sa
# apiGroup 固定
apiGroup: ""
roleRef:
kind: ClusterRole
name: rcoder-pods-clusterrole
apiGroup: rbac.authorization.k8s.io
```
### 方案二Role + RoleBinding限定 Namespace
适用于多租户环境,限制 RCoder 只能操作特定 namespace。
```yaml
# rcoder-rbac-namespaced.yaml
---
# 1. ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: rcoder-pods-sa
namespace: rcoder # 指定 namespace
---
# 2. Role权限定义
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: rcoder-pods-role
namespace: rcoder # 指定 namespace
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["get"]
---
# 3. RoleBinding绑定到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rcoder-pods-rolebinding
namespace: rcoder # 指定 namespace
subjects:
- kind: ServiceAccount
name: rcoder-pods-sa
namespace: rcoder
roleRef:
kind: Role
name: rcoder-pods-role
apiGroup: rbac.authorization.k8s.io
```
---
## 部署步骤
### 1. 创建配置(使用 ClusterRole 方案)
```bash
# 应用 RBAC 配置
kubectl apply -f rcoder-rbac.yaml
# 验证 ServiceAccount 创建成功
kubectl get sa rcoder-pods-sa
# 验证 Role 创建成功
kubectl get clusterrole rcoder-pods-clusterrole
# 验证 RoleBinding 创建成功
kubectl get clusterrolebinding rcoder-pods-clusterrolebinding
```
### 2. 修改 RCoder Deployment
将 RCoder Pod 关联到 ServiceAccount
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rcoder
namespace: default # 或你的 namespace
spec:
replicas: 1
selector:
matchLabels:
app: rcoder
template:
metadata:
labels:
app: rcoder
spec:
# 添加 ServiceAccount 配置
serviceAccountName: rcoder-pods-sa
containers:
- name: rcoder
image: registry.yichamao.com/rcoder:latest
ports:
- containerPort: 8087
env:
# 启用 K8s 运行时
- name: CONTAINER_RUNTIME
value: "kubernetes"
# 可选:指定 namespace默认使用 default
- name: RCODER_K8S_NAMESPACE
value: "default"
```
### 3. 验证权限
```bash
# 进入 RCoder Pod
kubectl exec -it <rcoder-pod-name> -- sh
# 测试 K8s API 访问权限
# 方法1使用 kubectl需要安装
kubectl auth can-i create pods --as=system:serviceaccount:default:rcoder-pods-sa
# 方法2直接测试 API
curl -k https://kubernetes.default.svc/api/v1/namespaces/default/pods \
--header "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
```
---
## 故障排查
### 问题 1: Permission Denied
```
Error: Container creation failed: Failed to create pod: Unauthorized
```
**解决**: 检查 ServiceAccount 是否正确绑定到 Role/ClusterRole
```bash
# 检查 ServiceAccount
kubectl get sa rcoder-pods-sa -o yaml
# 检查 RoleBinding subjects
kubectl get rolebinding <rolebinding-name> -o yaml
```
### 问题 2: Cannot create Pods in other namespaces
**原因**: 使用了 RoleBinding 而不是 ClusterRoleBinding
**解决**: 如果需要跨 namespace 操作 Pod使用 ClusterRoleBinding
### 问题 3: Token 文件不存在
```
Error: K8s client init failed: No such file or directory: /var/run/secrets/kubernetes.io/serviceaccount/token
```
**原因**: 代码不在 Pod 内运行,或未配置 ServiceAccount
**解决**:
1. 确保 RCoder 部署在 K8s 集群内
2. 确保 Deployment 配置了 `serviceAccountName`
3. 检查是否挂载了 ServiceAccount token
```bash
# 检查 Pod 是否挂载了 token
kubectl exec <pod-name> -- ls -la /var/run/secrets/kubernetes.io/serviceaccount/
```
---
## 生产环境建议
### 1. 使用专用 namespace
```bash
# 创建独立 namespace
kubectl create namespace rcoder
```
### 2. 限制 Image Pull Secret如果使用私有仓库
```yaml
imagePullSecrets:
- name: regcred
```
### 3. NetworkPolicy可选
限制 Pod 网络通信:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: rcoder-pods-networkpolicy
namespace: rcoder
spec:
podSelector:
matchLabels:
app: rcoder
policyTypes:
- Ingress
- Egress
```
### 4. Resource Limits
为 RCoder Pod 设置资源限制:
```yaml
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
```
---
## 代码集成
### 启动时权限检查(建议添加)
`KubernetesRuntime::new()` 中添加权限验证:
```rust
pub async fn new(config: DockerManagerConfig) -> ContainerRuntimeResult<Self> {
let kube_config = Config::infer()
.await
.map_err(|e| ...)?;
let client = Client::try_from(kube_config)
.map_err(|e| ...)?;
// 验证权限:尝试列出 pods
let pods: Api<Pod> = Api::namespaced(client.clone(), &namespace);
pods.list(&ListParams::default().limit(1)).await
.map_err(|e| ContainerRuntimeError::K8sError(
format!("Permission denied or API server unreachable: {}", e)
))?;
// ... 继续初始化
}
```
### 环境变量配置
| 环境变量 | 默认值 | 说明 |
|----------|--------|------|
| `CONTAINER_RUNTIME` | `docker` | 运行时类型docker/kubernetes/k8s |
| `RCODER_K8S_NAMESPACE` | `default` | Pod 创建的 namespace |
| `RCODER_K8S_SERVICE_ACCOUNT` | `rcoder-pods-sa` | 使用的 ServiceAccount |
---
## 完整示例部署
参见下一节 "Helm Chart 或 Kustomize 清单"
---
## 相关文档
- [K8s RBAC 官方文档](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
- [kube-rs 认证文档](https://kube.rs/client/auth/)
- [ServiceAccount 配置](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)

View File

@@ -0,0 +1,235 @@
# Implementation Plan: Kubernetes Runtime Support
**Branch**: `dev-k8s` | **Date**: 2026-04-16 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/k8s-runtime-support/spec.md`
## Execution Flow (/plan command scope)
```
1. Load feature spec from Input path
2. Fill Technical Context
3. Constitution Check (empty constitution - skip)
4. Execute Phase 0 → research.md
5. Execute Phase 1 → contracts, data-model.md, quickstart.md
6. Re-evaluate Constitution Check
7. Plan Phase 2 → tasks.md (via /tasks command)
8. STOP - Ready for /tasks command
```
## Summary
修复 RCoder 的 Kubernetes 运行时支持,使系统能够在 K8s 环境中动态创建和管理容器Pod而不依赖 Docker Socket。
主要改动:
1. 重构 `global` 模块使用 `RuntimeManager` 选择运行时 **P0 - 关键路径)**
2. K8s Runtime 支持 `user_id` 参数 **P0 - ComputerAgent 支持)**
3. 修复 `stop_container` 方法正确处理不同 ServiceType **P0**
4. 更新 Makefile K8s 命令避免重复操作 **P1**
## Technical Context
**Language/Version**: Rust 1.75+ (2024 Edition)
**Primary Dependencies**: kube-rs 0.98, bollard, tonic, tower
**Storage**: N/A (状态在 K8s API 中)
**Testing**: cargo test, integration tests
**Target Platform**: Linux (Docker + Kubernetes)
**Performance Goals**: Pod 启动 < 120s, gRPC 延迟 < 100ms
**Constraints**:
- 必须兼容现有 Docker 模式
- K8s Service Account 需要预配置
- 需要 RBAC 权限创建/删除 Pod
## Problem Analysis (Updated 2026-04-20)
### P0 Issues (Critical - Blocking K8s)
| Issue | Location | Description |
|-------|----------|-------------|
| `RuntimeManager::init()` never called | `main.rs:181-186` | K8s mode calls `init_global_docker_manager_with_config()` which does NOT initialize `RuntimeManager::RUNTIME_INSTANCE` |
| `stop_container` ignores service_type | `kubernetes_runtime.rs:437` | Always uses `ServiceType::RCoder`, cannot stop ComputerAgentRunner pods |
| K8s path not properly initialized | `lib.rs:201-223` | `init_global_docker_manager_with_config()` has K8s code but never reaches it properly |
### Root Cause
```rust
// main.rs calls this:
docker_manager::global::init_global_docker_manager_with_config(config).await
// But this function does NOT call RuntimeManager::init() for K8s mode!
// It only sets GLOBAL_DOCKER_MANAGER (for Docker mode)
```
### Required Changes
1. **main.rs**: Initialize using `RuntimeManager::init()` for K8s, or modify `init_global_docker_manager_with_config()` to properly call `RuntimeManager::init()` when K8s mode detected
2. **kubernetes_runtime.rs**: Fix `stop_container` to handle different service types
## Constitution Check
*Note: Constitution file is empty template - no gates to check*
## Project Structure
### Documentation (this feature)
```
specs/k8s-runtime-support/
├── plan.md # This file
├── spec.md # Feature specification
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output (interfaces)
└── tasks.md # Phase 2 output (/tasks command)
```
### Source Code (repository root)
```
crates/
├── docker_manager/
│ ├── src/
│ │ ├── lib.rs # MODIFY: global module
│ │ └── runtime/
│ │ ├── mod.rs # MODIFY: export changes
│ │ ├── manager.rs # KEEP: RuntimeManager
│ │ ├── kubernetes_runtime.rs # MODIFY: fix issues
│ │ └── docker_runtime.rs # KEEP: unchanged
├── container-runtime-api/
│ └── src/
│ └── runtime_trait.rs # KEEP: trait definition
└── rcoder/
└── src/
└── main.rs # MODIFY: runtime init
```
**Structure Decision**: 修改现有 `docker_manager` crate重构 `global` 模块使其根据 `CONTAINER_RUNTIME` 环境变量选择 `RuntimeManager` 或直接使用 `DockerManager`
### K8s Local Development Environment
新增 `k8s/` 目录,用于本地 K8s 测试环境(类比现有 `docker/` 目录):
```
k8s/
├── README.md # K8s 环境使用说明
├── kind-config.yaml # Kind (Kubernetes IN Docker) 配置
├── start-kind.sh # 启动本地 K8s 集群脚本
├── stop-kind.sh # 停止本地 K8s 集群脚本
├── manifests/
│ ├── namespace.yaml # Namespace 定义
│ ├── serviceaccount.yaml # ServiceAccount + RBAC
│ ├── rcoder-deployment.yaml # RCoder 主服务 Deployment
│ └── rcoder-service.yaml # RCoder 主服务 Service
└── test-chat.sh # 测试脚本
```
**功能**
- 使用 [Kind](https://kind.sigs.k8s.io/) 在本地运行 K8s
- 一键启动本地 K8s 集群
- 部署 RCoder 到本地 K8s
- 测试 `/chat``/computer/chat` 接口
## Phase 0: Outline & Research
### Research Tasks
1. **K8s Pod IP 通信**: Pod 之间直接用 IP 通信,同 Docker 方式
2. **kube-rs 最佳实践**: 社区推荐的 K8s Runtime 实现模式
3. **K8s 健康检查**: HTTP Health Check 方式(与 Docker 一致)
4. **K8s 存储**: workspace 存储问题(标记为后续优化项)
### Output
**research.md**:
- Decision: 使用 Pod IP 直接通信(与 Docker 方式一致)
- Rationale: Pod IP 在同一集群内可直接通信,无需额外 ServicePod 重启后 cleanup_task 会重建并更新 IP
- Alternatives considered: Service DNS需要额外创建 Service增加复杂度当前不需要
## Phase 1: Design & Contracts
### Actual Code Changes Required
#### 1. Fix `lib.rs` - global module initialization
**Current (broken)**:
```rust
pub async fn init_global_docker_manager_with_config(config: DockerManagerConfig) -> DockerResult<()> {
let runtime_type = RuntimeType::from_env();
crate::runtime::RuntimeManager::init(config.clone()).await // <- Already calls RuntimeManager::init()!
if runtime_type == RuntimeType::Docker {
let manager = Arc::new(DockerManager::new(config).await?);
GLOBAL_DOCKER_MANAGER.set(manager)...;
}
// Problem: Sets RUNTIME_INSTANCE in RuntimeManager::init() but returns DockerResult
}
```
**Fix**: Ensure proper error handling and that K8s path doesn't try to set GLOBAL_DOCKER_MANAGER
#### 2. Fix `main.rs` - runtime initialization
**Current**: Calls `init_global_docker_manager_with_config()` which should work, but error handling may be wrong
**Fix**: Verify RuntimeManager is properly initialized before using `RuntimeManager::get()`
#### 3. Fix `kubernetes_runtime.rs` - stop_container
**Current**:
```rust
async fn stop_container(&self, project_id: &str) -> ContainerRuntimeResult<()> {
let pod_name = self.pod_name(project_id, &ServiceType::RCoder); // Always RCoder!
// ...
}
```
**Fix**: Need to track service_type per container or change the interface
#### 4. Fix Makefile k8s commands
**Current**:
```makefile
dev-up-k8s:
kubectl apply -f manifests/rcoder-deployment.yaml
kubectl set image deployment/rcoder rcoder=$(IMAGE) # Redundant after apply
dev-restart-k8s: dev-build-k8s
kubectl apply -f manifests/rcoder-deployment.yaml
kubectl set image deployment/rcoder rcoder=$(IMAGE)
kubectl rollout restart deploy/rcoder # rollout restart uses current deployment image, not new one!
```
**Fix**: Use `kubectl delete pods` or fix the image update flow
## Phase 2: Task Planning Approach
*This section describes what the /tasks command will do - DO NOT execute during /plan*
**Task Generation Strategy**:
- P0 优先级:先解决阻止 K8s 运行的 critical 问题
- P1 优先级:修复已知的 bug 和不完整实现
- P2 优先级:改进和优化
**Task Order (Dependency-Based)**:
1. **[P0] Fix global module initialization** - 确保 K8s 模式下 RuntimeManager 正确初始化
2. **[P0] Fix rcoder main.rs** - 调用正确的初始化函数
3. **[P0] Fix stop_container in KubernetesRuntime** - 正确处理不同 service_type
4. **[P1] Fix Makefile k8s commands** - 消除重复操作,简化逻辑
5. **[P1] Test K8s mode end-to-end** - 验证修复有效
6. **[P2] Improve K8s health check** - 如有时间,优化健康检查
**Estimated Output**: 8-10 focused tasks
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| 重构 global 模块 | 需要统一运行时选择逻辑 | 直接在 rcoder 中判断,但会导致代码重复 |
## Progress Tracking
**Phase Status**:
- [x] Phase 0: Research complete - research.md created
- [x] Phase 1: Design complete - data-model.md created
- [x] Phase 2: Task planning approach defined (above)
- [ ] Phase 3: Tasks generated (/tasks command)
- [ ] Phase 4: Implementation complete
- [ ] Phase 5: Validation passed
**Gate Status**:
- [x] Initial Constitution Check: N/A (empty constitution)
- [x] Post-Design Constitution Check: N/A
- [x] All NEEDS CLARIFICATION resolved
- [ ] Complexity deviations documented
---
*Based on Constitution v2.1.1 - See `/memory/constitution.md`*

View File

@@ -0,0 +1,168 @@
# Research: Kubernetes Runtime Support
**Date**: 2026-04-16
**Feature**: K8s Runtime Support
## Research 1: K8s Service DNS vs Pod IP
### Decision
使用 K8s Service DNS 作为服务发现机制。
### Rationale
- **Pod IP 不稳定**: Pod 重启后 IP 会变化,直接使用 Pod IP 会导致 gRPC 通信失败
- **Service DNS 稳定**: 即使 Pod 重启Service IP 保持不变ClusterIP Service
- **K8s 标准做法**: 社区推荐使用 Service 进行服务发现
### Alternatives Considered
| 方案 | 优点 | 缺点 |
|------|------|------|
| Pod IP | 简单直接 | Pod 重启后失效 |
| Headless Service | 可直接解析 Pod IP | 仍依赖 Pod DNS不稳定 |
| Ingress | 支持外部访问 | 增加复杂度,不需要 |
| ExternalName Service | 简单 | 不适合内部服务 |
### Service DNS 格式
```
{service_name}.{namespace}.svc.cluster.local
```
对于 RCoder:
- RCoder Pod: `rcoder-agent-{project_id}.{namespace}.svc.cluster.local`
- ComputerAgent Pod: `computer-agent-runner-{user_id}.{namespace}.svc.cluster.local`
### Implementation
```rust
fn pod_dns_name(project_id: &str, user_id: Option<&str>, namespace: &str) -> String {
let prefix = match user_id {
Some(uid) => format!("computer-agent-runner-{}", uid),
None => format!("rcoder-agent-{}", project_id),
};
format!("{}.{}.svc.cluster.local", prefix, namespace)
}
```
---
## Research 2: kube-rs 最佳实践
### kube-rs 版本
当前使用: `kube 0.98`
### 推荐的 API 使用模式
#### 1. Client 初始化
```rust
// 推荐:从 Config::infer() 自动检测 in-cluster vs local
let kube_config = Config::infer().await?;
let client = Client::try_from(kube_config)?;
```
#### 2. API 访问模式
```rust
// 推荐:使用 Api::namespaced() 访问 namespaced 资源
let pods: Api<Pod> = Api::namespaced(client, &namespace);
// 使用 ListParams 过滤
let lp = ListParams::default().labels(&format!("project_id={}", project_id));
let pods = pods.list(&lp).await?;
```
#### 3. 错误处理
```rust
match pods.get(&pod_name).await {
Ok(pod) => { /* found */ }
Err(kube::Error::Api(ae)) if ae.code == 404 => { /* not found */ }
Err(e) => return Err(e),
}
```
---
## Research 3: K8s 健康检查 (Readiness Probe)
### 问题
Docker 模式使用 HTTP 轮询检查服务健康:
```rust
async fn wait_for_service_ready(service_url: &str) {
loop {
if http::get(service_url).is_ok() {
return Ok(());
}
sleep().await;
}
}
```
K8s 模式应该使用 K8s 原生的 Readiness Probe 概念。
### 分析
- **K8s Readiness Probe**: K8s 自动管理,决定 Pod 是否接收流量
- **当前实现**: 应用层轮询,与 K8s 概念不匹配
### 解决方案
仍然使用应用层健康检查(保持兼容性),但针对 K8s 环境优化:
- 使用 DNS 解析代替 IP
- 增加超时时间K8s Pod 启动通常需要 30-60s
- 复用容器运行时层的健康检查接口
### K8s Probe 配置(未来可能需要)
```yaml
readinessProbe:
httpGet:
path: /health
port: 8086
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
```
---
## Research 4: K8s 存储 (Workspace 处理)
### 问题
Docker 模式下使用 bind mount 共享文件系统:
```yaml
volumes:
- /host/path:/container/path
```
K8s 中如何处理?
### 分析
| K8s 存储方案 | 适用场景 | 缺点 |
|--------------|----------|------|
| EmptyDir | 临时存储 | Pod 删除后数据丢失 |
| HostPath | 节点文件 | 安全性差,需要特权 |
| PVC | 持久存储 | 需要预先配置 StorageClass |
| NFS/CIFS | 共享存储 | 需要外部存储服务 |
| ConfigMap | 配置文件 | 不适合大文件 |
### RCoder 场景
RCoder 的 workspace 需要:
1. 持久化(用户代码、项目文件)
2. 跨容器共享rcoder 主容器和 agent_runner 容器)
### 解决方案
- **开发环境**: 使用 PVC with ReadWriteMany (如果存储支持)
- **生产环境**: 使用 NFS 或云存储
- **简化方案**: 对于 K8s 支持,暂时不处理 workspace 挂载(使用容器内置存储)
### 实现计划
Phase 1 中暂时跳过 workspace 存储问题,标记为 [NEEDS CLARIFICATION: workspace 存储策略]
---
## Summary
### Key Decisions
1. **Service DNS**: 使用 `{prefix}-{id}.{namespace}.svc.cluster.local`
2. **kube-rs**: 使用 Api::namespaced() + ListParams 过滤
3. **健康检查**: 保持应用层检查,增加超时
4. **存储**: 标记为待解决问题
### Open Questions
1. [NEEDS CLARIFICATION]: K8s 环境中 rcoder 主服务如何与 agent_runner Pod 通信?(同 namespace 直连?)
2. [NEEDS CLARIFICATION]: workspace 存储使用 PVC 还是其他方案?
3. [NEEDS CLARIFICATION]: K8s Service Account 和 RBAC 权限如何预配置?

View File

@@ -0,0 +1,65 @@
# Feature Specification: Kubernetes Runtime Support
**Feature Branch**: `dev-k8s`
**Created**: 2026-04-16
**Status**: Draft
**Input**: User description: "修复 K8s 支持,问题是 global 模块未使用 RuntimeManagerK8s Runtime 中 user_id 未处理,使用 Pod IP 而非 Service DNS"
## User Scenarios & Testing
### Primary User Story
RCoder 在 Kubernetes 集群中运行时,需要动态为每个项目/用户创建和管理容器Pod。当前实现仅支持 Docker Socket 方式,不支持 K8s 环境。
### Acceptance Scenarios
1. **Given** RCoder 运行在 K8s 环境(设置 `CONTAINER_RUNTIME=kubernetes`**When** 用户调用 `/chat` 接口,**Then** 系统应在 K8s 中创建 Pod 并正常通信
2. **Given** RCoder 运行在 K8s 环境,**When** 用户调用 `/computer/chat` 接口,**Then** 系统应使用 `user_id` 作为 Pod 标识创建容器
3. **Given** K8s Pod 发生重启,**When** gRPC 通信发生,**Then** 系统应能通过稳定的 Service DNS 找到新 Pod
### Edge Cases
- Pod 重启后 IP 变化如何处理?
- K8s API 访问失败时的降级策略?
- 容器清理逻辑在 K8s 中如何适配?
## Requirements
### Functional Requirements
- **FR-001**: 系统必须根据 `CONTAINER_RUNTIME` 环境变量选择 Docker 或 Kubernetes 运行时
- **FR-002**: Kubernetes Runtime 必须支持 `/chat` 接口(使用 project_id 作为 Pod 标识)
- **FR-003**: Kubernetes Runtime 必须支持 `/computer/chat` 接口(使用 user_id 作为 Pod 标识)
- **FR-004**: Kubernetes Runtime 必须使用稳定的 Service DNS 而非 Pod IP
- **FR-005**: 系统必须实现 `list_containers` 接口以支持 pod list 管理接口
- **FR-006**: Kubernetes Runtime 必须支持容器健康检查
### Key Entities
- **ContainerRuntime**: 容器运行时抽象接口
- **KubernetesRuntime**: K8s 运行时实现
- **DockerRuntime**: Docker 运行时实现(封装 DockerManager
- **RuntimeManager**: 运行时管理器,负责选择和初始化正确的运行时
## Technical Context (for planning)
### Problem Analysis
| 问题 | 严重程度 | 位置 |
|------|----------|------|
| global 模块未使用 RuntimeManagerK8s 路径从未触发 | P0 | docker_manager/src/lib.rs |
| user_id 在 K8s Runtime 中被忽略 | P0 | kubernetes_runtime.rs |
| 使用 Pod IP 而非 Service DNS | P0 | kubernetes_runtime.rs |
| list_containers 未实现 | P1 | kubernetes_runtime.rs |
| 健康检查机制不兼容 K8s | P1 | health/ |
### Proposed Changes
1. 修改 `global` 模块使用 `RuntimeManager` 选择运行时
2. K8s Runtime 支持 `user_id` 参数
3. K8s Runtime 使用 Service DNS`{service}-{id}.{namespace}.svc.cluster.local`
4. 实现 `list_containers` 方法
5. 添加 K8s 健康检查支持( Readiness Probe 概念)
## Review & Acceptance Checklist
- [x] User description parsed
- [x] Key concepts extracted
- [x] Ambiguities marked (Pod DNS 格式、K8s API 权限)
- [x] User scenarios defined
- [x] Requirements generated
- [x] Entities identified
- [x] Review checklist passed

View File

@@ -0,0 +1,325 @@
# Tasks: K8s Runtime Support Fix
**Branch**: `dev-k8s` | **Date**: 2026-04-20
**Plan**: [plan.md](./plan.md) | **Spec**: [spec.md](./spec.md)
## Task List
### Phase 1: Critical Fixes (P0)
---
### Task 1: Fix `global::init_global_docker_manager_with_config()` K8s path
**File**: `crates/docker_manager/src/lib.rs`
**Priority**: P0
**Estimated Time**: 30 min
**Status**: TODO
**Problem**: When `CONTAINER_RUNTIME=kubernetes`, the function calls `RuntimeManager::init()` but then still tries to set `GLOBAL_DOCKER_MANAGER` which fails silently or causes issues.
**Changes**:
```rust
#[cfg(feature = "kubernetes")]
pub async fn init_global_docker_manager_with_config(
config: DockerManagerConfig,
) -> DockerResult<()> {
let runtime_type = RuntimeType::from_env();
crate::runtime::RuntimeManager::init(config.clone())
.await
.map_err(|e| DockerError::ConfigurationError(e.to_string()))?;
info!("Runtime initialized with config");
if runtime_type == RuntimeType::Docker {
let manager = Arc::new(DockerManager::new(config).await?);
GLOBAL_DOCKER_MANAGER.set(manager).map_err(|_| {
DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"global DockerManager already initialized",
))
})?;
info!("DockerManager initialized with config");
}
// K8s mode: RuntimeManager is initialized, GLOBAL_DOCKER_MANAGER stays empty (ok)
Ok(())
}
```
**Verification**:
- [ ] `cargo check -p docker_manager --features kubernetes` passes
- [ ] Unit test `test_runtime_type_from_env_kubernetes` passes
---
### Task 2: Verify `main.rs` runtime initialization flow
**File**: `crates/rcoder/src/main.rs`
**Priority**: P0
**Estimated Time**: 30 min
**Status**: TODO
**Problem**: Need to verify that after calling `init_global_docker_manager_with_config()`, `RuntimeManager::get()` works correctly for K8s mode.
**Current Code** (lines 181-186):
```rust
if let Err(e) =
docker_manager::global::init_global_docker_manager_with_config(docker_manager_config).await
{
error!("Docker Manager initializefailed: {}", e);
return Err(anyhow::anyhow!("Docker Manager initialization failed: {}", e));
}
```
**Verification**:
- [ ] K8s mode: `RuntimeManager::get().await` returns `Arc<dyn ContainerRuntime>`
- [ ] Docker mode: `get_global_docker_manager().await` returns `Arc<DockerManager>`
- [ ] Both modes can call `cleanup_all()` without error
---
### Task 3: Fix `stop_container` in KubernetesRuntime to handle service_type
**File**: `crates/docker_manager/src/runtime/kubernetes_runtime.rs`
**Priority**: P0
**Estimated Time**: 45 min
**Status**: TODO
**Problem**: `stop_container(&self, project_id: &str)` only takes project_id, but K8s creates different pod types (RCoder vs ComputerAgentRunner) with different prefixes.
**Current**:
```rust
async fn stop_container(&self, project_id: &str) -> ContainerRuntimeResult<()> {
let pod_name = self.pod_name(project_id, &ServiceType::RCoder); // Always RCoder!
// ...
}
```
**Fix Options**:
Option A - Add service_type parameter to `stop_container`:
```rust
async fn stop_container(&self, project_id: &str, service_type: ServiceType) -> ContainerRuntimeResult<()> {
let pod_name = self.pod_name(project_id, &service_type);
// ...
}
```
⚠️ This breaks the trait signature.
Option B - Cache service_type in pod_cache:
```rust
// Store (identifier, service_type) in cache when creating container
self.pod_cache.write().await.insert(identifier.to_string(), (pod_info, service_type.clone()));
// In stop_container, lookup cached service_type or default to RCoder
let service_type = self.get_cached_service_type(identifier).await.unwrap_or(ServiceType::RCoder);
```
Option C - Use `stop_container_by_identifier` which already has service_type:
```rust
async fn stop_container(&self, project_id: &str) -> ContainerRuntimeResult<()> {
// Try RCoder first, then ComputerAgentRunner
match self.stop_container_by_identifier(project_id, &ServiceType::RCoder).await {
Ok(()) => Ok(()),
Err(ContainerRuntimeError::ContainerStopError(_)) => {
// Try ComputerAgentRunner if RCoder not found
self.stop_container_by_identifier(project_id, &ServiceType::ComputerAgentRunner).await
}
Err(e) => Err(e),
}
}
```
**Recommended** - Uses existing trait method, handles both types
**Implementation**: Use Option C
**Verification**:
- [ ] `stop_container("user123")` correctly deletes ComputerAgentRunner pod when it exists
- [ ] `stop_container("project123")` correctly deletes RCoder pod when it exists
---
### Task 4: Fix Makefile k8s commands
**File**: `Makefile`
**Priority**: P1
**Estimated Time**: 20 min
**Status**: TODO
**Problem**:
1. `dev-up-k8s` and `dev-restart-k8s` do `kubectl apply` followed by `kubectl set image`, but the image in deployment.yaml is hardcoded to `rcoder:test`
2. `dev-restart-k8s` uses `rollout restart` which doesn't pick up the new image from `set image`
**Current (broken)**:
```makefile
dev-up-k8s:
kubectl apply -f k8s/manifests/rcoder-deployment.yaml # Uses image: rcoder:test
kubectl set image deployment/rcoder rcoder=$(IMAGE) # But this updates it
...
dev-restart-k8s: dev-build-k8s
kubectl apply -f k8s/manifests/rcoder-deployment.yaml
kubectl set image deployment/rcoder rcoder=$(IMAGE) # Sets new image
kubectl rollout restart deploy/rcoder # But restart uses deployment.yaml, not set image!
```
**Fix**:
```makefile
# Use sed to replace image in deployment.yaml before applying
define apply_with_image
kubectl apply -f k8s/manifests/namespace.yaml
kubectl apply -f k8s/manifests/serviceaccount.yaml
sed "s|image: rcoder:test|image: $(IMAGE)|" k8s/manifests/rcoder-deployment.yaml | kubectl apply -f -
kubectl apply -f k8s/manifests/rcoder-service.yaml
endef
dev-up-k8s:
$(call apply_with_image)
kubectl rollout status deploy/rcoder -n $(K8S_NAMESPACE) --timeout=$(ROLLOUT_TIMEOUT)
dev-restart-k8s: dev-build-k8s
kubectl delete pods -n $(K8S_NAMESPACE) -l app=rcoder --ignore-not-found
kubectl rollout status deploy/rcoder -n $(K8S_NAMESPACE) --timeout=$(ROLLOUT_TIMEOUT)
```
**Key changes**:
1. Use `sed` to replace image tag at apply time
2. `dev-restart-k8s` uses `kubectl delete pods` instead of `rollout restart` (simpler, faster)
3. `dev-down-k8s` stays the same (already correct)
**Verification**:
- [ ] `make dev-up-k8s IMAGE=rcoder:test-k8s` deploys with correct image
- [ ] `make dev-restart-k8s IMAGE=rcoder:test-k8s` rebuilds and redeploys
- [ ] `make dev-down-k8s` cleans up properly
---
### Task 5: Add K8s mode to Cargo features in rcoder
**File**: `crates/rcoder/Cargo.toml`
**Priority**: P1
**Estimated Time**: 10 min
**Status**: TODO
**Problem**: The `kubernetes` feature in rcoder passes to docker_manager, but need to verify it's properly configured.
**Current**:
```toml
[features]
# Kubernetes 支持:启用 Kubernetes 运行时模式
# 启用后可通过 CONTAINER_RUNTIME=kubernetes 环境变量切换到 K8s 模式
kubernetes = ["docker_manager/kubernetes"]
```
**Verification**:
- [ ] `cargo build --features kubernetes` works
- [ ] `cargo build --features kubernetes --package rcoder` produces binary with K8s support
---
### Task 6: Test K8s mode end-to-end
**Priority**: P0
**Estimated Time**: 60 min
**Status**: TODO
**Prerequisites**: Kind cluster or real K8s cluster available
**Test Steps**:
```bash
# 1. Build K8s image
make dev-build-k8s IMAGE=rcoder:test-k8s
# 2. Deploy to K8s
make dev-up-k8s IMAGE=rcoder:test-k8s
# 3. Check rcoder pod is running
kubectl get pods -n rcoder
kubectl logs -n rcoder -l app=rcoder
# 4. Test /health endpoint
NODE_PORT=$(kubectl get svc rcoder -n rcoder -o jsonpath='{.spec.ports[0].nodePort}')
curl http://localhost:$NODE_PORT/health
# 5. Test /chat endpoint (creates RCoder agent pod)
curl -X POST http://localhost:$NODE_PORT/chat \
-H "Content-Type: application/json" \
-d '{"prompt": "hello"}'
# 6. Check agent pod was created
kubectl get pods -n rcoder
# 7. Test /computer/chat endpoint (creates ComputerAgentRunner pod)
curl -X POST http://localhost:$NODE_PORT/computer/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "test-user", "prompt": "hello"}'
# 8. Check computer agent pod was created
kubectl get pods -n rcoder -l user_id=test-user
# 9. Test cleanup (delete chat session)
# ... verify pods are deleted
# 10. Cleanup
make dev-down-k8s
```
**Expected Results**:
- [ ] `/health` returns healthy
- [ ] `/chat` creates `rcoder-agent-{project_id}` pod
- [ ] `/computer/chat` creates `computer-agent-runner-{user_id}` pod
- [ ] Both pods are reachable via gRPC from rcoder main pod
- [ ] Cleanup properly deletes pods
---
### Task 7: Verify Docker mode still works (regression test)
**Priority**: P1
**Estimated Time**: 30 min
**Status**: TODO
**Test Steps**:
```bash
# 1. Build Docker image
make dev-build
# 2. Start Docker Compose
make dev-up
# 3. Test /health
curl http://localhost:8087/health
# 4. Test /chat
curl -X POST http://localhost:8087/chat \
-H "Content-Type: application/json" \
-d '{"prompt": "hello"}'
# 5. Verify container created
docker ps | grep rcoder-agent
# 6. Cleanup
make dev-down
```
**Expected Results**:
- [ ] All existing functionality works as before
- [ ] No regression in Docker mode
---
## Task Summary
| # | Task | Priority | Status | Dependencies |
|---|------|----------|--------|--------------|
| 1 | Fix `init_global_docker_manager_with_config()` | P0 | TODO | - |
| 2 | Verify `main.rs` runtime init | P0 | TODO | Task 1 |
| 3 | Fix `stop_container` service_type | P0 | TODO | - |
| 4 | Fix Makefile k8s commands | P1 | TODO | - |
| 5 | Verify Cargo features | P1 | TODO | - |
| 6 | Test K8s mode end-to-end | P0 | TODO | Tasks 1-5 |
| 7 | Regression test Docker mode | P1 | TODO | - |
---
## Completion Criteria
- [ ] All P0 tasks complete
- [ ] K8s mode can create/manage RCoder agent pods
- [ ] K8s mode can create/manage ComputerAgentRunner pods
- [ ] Docker mode regression tests pass
- [ ] Makefile k8s commands work correctly
- [ ] Code compiles with both `kubernetes` feature enabled and disabled

View File

@@ -0,0 +1,676 @@
# SACP 协议升级技术方案
> 从官方 ACP (`agent-client-protocol`) 迁移到 Symposium ACP (`sacp`) 的技术方案设计
## 1. 背景与动机
### 1.1 当前问题
当前项目使用官方 ACP 协议库 `agent-client-protocol = "0.9.3"`,存在以下核心问题:
1. **Send trait 限制**`ClientSideConnection` 未实现 `Send` trait导致
- 必须在 `LocalSet` 中运行
- 必须使用 `spawn_local` 而非标准的 `tokio::spawn`
- 需要 `spawn_blocking` + 单线程运行时 + `LocalSet` 的复杂组合
2. **并发模型复杂**
```rust
// 当前代码模式acp_agent.rs
tokio::task::spawn_blocking(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async move {
let local_set = tokio::task::LocalSet::new();
local_set.run_until(async move {
// ACP 连接处理必须在这里
}).await
})
})
```
3. **代码耦合度高**:连接建立、消息处理、生命周期管理紧密耦合在 `ClaudeCodeLauncher` 中
### 1.2 SACP 优势
Symposium ACP (`sacp = "10.1.0"`) 提供:
1. **Send + 'static 支持**`Component<L>` trait 要求类型满足 `Send + 'static`
2. **简化的并发模型**:可直接使用标准 Tokio 多线程运行时
3. **类型安全的链接系统**`ClientToAgent`, `AgentToClient` 等编译时类型检查
4. **Builder 模式 API**:更简洁、更符合 Rust 习惯的 API 设计
5. **MCP 原生集成**:内置 MCP-over-ACP 支持
## 2. 架构设计
### 2.1 模块结构变化
```
crates/
├── agent_abstraction/ # 抽象层(需要重构)
│ ├── acp/
│ │ ├── mod.rs
│ │ ├── connection.rs # AgentConnection保留调整内部实现
│ │ └── sacp_adapter.rs # 新增SACP 适配器
│ ├── launcher/
│ │ ├── mod.rs
│ │ ├── lifecycle.rs # AgentLifecycleGuard保留
│ │ ├── channel.rs # 通道处理器(需要适配)
│ │ └── claude_code_sacp.rs # 新增SACP 版本启动器
│ └── session/
│ ├── worker.rs # AgentWorker trait保留
│ └── acp_worker.rs # AcpAgentWorker需要适配
├── agent_runner/ # 运行时(需要重构)
│ ├── proxy_agent/
│ │ ├── mod.rs
│ │ └── acp_agent.rs # 移除 LocalSet 依赖
│ └── main.rs # 简化运行时架构
└── acp_adapter/ # 新增ACP 协议适配层
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── traits.rs # 协议无关的抽象 trait
│ ├── sacp_impl.rs # SACP 实现
│ └── legacy_impl.rs # 官方 ACP 兼容层(可选)
```
### 2.2 核心 Trait 设计
#### 2.2.1 协议无关的客户端抽象
```rust
// crates/acp_adapter/src/traits.rs
use async_trait::async_trait;
use std::path::PathBuf;
/// ACP 客户端抽象 trait
///
/// 这是协议无关的抽象层,支持 SACP 和官方 ACP 的切换
#[async_trait]
pub trait AcpClient: Send + Sync + 'static {
/// 会话 ID 类型
type SessionId: Clone + Send + Sync + std::fmt::Display;
/// 错误类型
type Error: std::error::Error + Send + Sync + 'static;
/// 初始化连接
async fn initialize(&self, client_info: ClientInfo) -> Result<InitializeResponse, Self::Error>;
/// 创建新会话
async fn new_session(&self, config: SessionConfig) -> Result<Self::SessionId, Self::Error>;
/// 发送 Prompt
async fn prompt(&self, session_id: &Self::SessionId, request: PromptRequest) -> Result<PromptResponse, Self::Error>;
/// 取消会话
async fn cancel(&self, session_id: &Self::SessionId) -> Result<(), Self::Error>;
/// 检查连接是否有效
fn is_connected(&self) -> bool;
}
/// 客户端信息
#[derive(Debug, Clone)]
pub struct ClientInfo {
pub name: String,
pub version: String,
pub title: Option<String>,
}
/// 会话配置
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub working_directory: PathBuf,
pub mcp_servers: Vec<McpServerConfig>,
pub meta: Option<serde_json::Value>,
}
/// MCP 服务器配置
#[derive(Debug, Clone)]
pub struct McpServerConfig {
pub name: String,
pub command: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}
/// Prompt 请求
#[derive(Debug, Clone)]
pub struct PromptRequest {
pub messages: Vec<Message>,
pub session_id: String,
}
/// Prompt 响应
#[derive(Debug, Clone)]
pub struct PromptResponse {
pub content: Vec<ContentBlock>,
pub stop_reason: StopReason,
}
```
#### 2.2.2 SACP 实现
```rust
// crates/acp_adapter/src/sacp_impl.rs
use sacp::{
ClientToAgent, Channel, JrConnectionCx, JrConnectionBuilder,
schema::{InitializeRequest, ProtocolVersion, NewSessionRequest, SessionId},
on_receive_request,
};
use std::sync::Arc;
use tokio::sync::RwLock;
/// SACP 客户端实现
///
/// 使用 SACP 的 Component trait 和 JrConnectionBuilder 构建
pub struct SacpClient {
/// 连接上下文(线程安全)
connection_cx: Arc<RwLock<Option<JrConnectionCx<ClientToAgent>>>>,
/// 连接状态
connected: Arc<std::sync::atomic::AtomicBool>,
}
impl SacpClient {
/// 创建新客户端并连接到 Agent
pub async fn connect(transport: impl sacp::Component<sacp::AgentToClient>) -> Result<Self, sacp::Error> {
let connected = Arc::new(std::sync::atomic::AtomicBool::new(false));
let connected_clone = connected.clone();
let (channel, server_future) = transport.into_server();
// 在后台运行服务
tokio::spawn(server_future);
// 构建客户端连接
let client = Self {
connection_cx: Arc::new(RwLock::new(None)),
connected,
};
Ok(client)
}
}
#[async_trait]
impl AcpClient for SacpClient {
type SessionId = SessionId;
type Error = sacp::Error;
async fn initialize(&self, client_info: ClientInfo) -> Result<InitializeResponse, Self::Error> {
// 使用 SACP 的 InitializeRequest
let request = InitializeRequest::new(ProtocolVersion::LATEST)
.client_info(sacp::schema::Implementation::new(
&client_info.name,
&client_info.version,
));
// 发送请求并等待响应
// ...
todo!()
}
async fn new_session(&self, config: SessionConfig) -> Result<Self::SessionId, Self::Error> {
// 使用 SACP 的 NewSessionRequest
let request = NewSessionRequest::new(config.working_directory);
// ...
todo!()
}
async fn prompt(&self, session_id: &Self::SessionId, request: PromptRequest) -> Result<PromptResponse, Self::Error> {
// 使用 SACP 的 PromptRequest
// ...
todo!()
}
async fn cancel(&self, session_id: &Self::SessionId) -> Result<(), Self::Error> {
// 使用 SACP 的 CancelNotification
// ...
todo!()
}
fn is_connected(&self) -> bool {
self.connected.load(std::sync::atomic::Ordering::Relaxed)
}
}
```
### 2.3 启动器重构
#### 2.3.1 SACP 版本启动器
```rust
// crates/agent_abstraction/src/launcher/claude_code_sacp.rs
use sacp::{ClientToAgent, ByteStreams, Component};
use std::process::Stdio;
use tokio::process::Command;
/// SACP 版本的 Claude Code 启动器
///
/// 关键变化:
/// 1. 不再需要 LocalSet
/// 2. 使用 SACP 的 Component trait
/// 3. 支持标准 Tokio spawn
pub struct SacpClaudeCodeLauncher<N: SessionNotifier> {
notifier: Arc<N>,
}
impl<N: SessionNotifier + 'static> SacpClaudeCodeLauncher<N> {
pub fn new(notifier: Arc<N>) -> Self {
Self { notifier }
}
/// 启动 Claude Code Agent
///
/// 与旧版本的关键区别:
/// - 使用 `tokio::spawn` 而非 `spawn_local`
/// - 使用 SACP 的 `ByteStreams` 作为传输层
/// - 使用 `ClientToAgent::builder()` 构建连接
pub async fn launch(
&self,
project_id: String,
project_path: PathBuf,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
) -> Result<SacpConnectionInfo> {
// 1. 加载配置
let agent_config = load_agent_config(model_provider.as_ref(), &start_config.service_type).await?;
// 2. 启动子进程
let mut child = Command::new(&agent_config.command)
.args(&agent_config.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.current_dir(&project_path)
.envs(&agent_config.env)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
// 3. 创建 SACP 传输层关键变化ByteStreams 实现了 Component + Send
let transport = ByteStreams::new(stdout, stdin);
// 4. 创建通道
let (prompt_tx, prompt_rx) = tokio::sync::mpsc::unbounded_channel();
let (cancel_tx, cancel_rx) = tokio::sync::mpsc::unbounded_channel();
let cancel_token = CancellationToken::new();
// 5. 在标准 Tokio task 中运行(无需 LocalSet
let notifier = self.notifier.clone();
let cancel_token_clone = cancel_token.clone();
let connection_handle = tokio::spawn(async move {
Self::run_connection(
transport,
project_id,
project_path,
start_config,
prompt_rx,
cancel_rx,
cancel_token_clone,
notifier,
).await
});
// 6. 等待初始化完成并获取 session_id
// ...
Ok(SacpConnectionInfo {
session_id,
prompt_tx,
cancel_tx,
lifecycle_guard: Arc::new(lifecycle_guard),
})
}
/// 运行 SACP 连接
///
/// 使用 SACP 的 `ClientToAgent::builder().run_until()` 模式
async fn run_connection(
transport: impl Component<sacp::AgentToClient>,
project_id: String,
project_path: PathBuf,
start_config: AgentStartConfig,
mut prompt_rx: mpsc::UnboundedReceiver<PromptRequest>,
mut cancel_rx: mpsc::UnboundedReceiver<CancelNotification>,
cancel_token: CancellationToken,
notifier: Arc<impl SessionNotifier>,
) -> Result<()> {
ClientToAgent::builder()
.name("rcoder-agent-runner")
// 注册请求处理器(使用 SACP 宏)
.on_receive_request(
async |req: PermissionRequest, req_cx: JrRequestCx<_>, _| {
// 处理权限请求
req_cx.respond(PermissionResponse::default())
},
on_receive_request!(),
)
// 注册通知处理器
.on_receive_notification(
async |notif: SessionUpdate, _| {
// 处理会话更新通知
Ok(())
},
on_receive_notification!(),
)
// 运行连接
.run_until(transport, async |cx| {
// Step 1: 初始化
cx.send_request(InitializeRequest::new(ProtocolVersion::LATEST))
.block_task()
.await?;
// Step 2: 创建会话
let meta = start_config.build_meta();
let session = cx.build_session(NewSessionRequest::new(project_path).meta(meta))
.block_task()
.await?;
let session_id = session.session_id();
// Step 3: 处理消息循环
loop {
tokio::select! {
// 处理 Prompt 请求
Some(prompt) = prompt_rx.recv() => {
session.send_prompt(&prompt.content)?;
let response = session.read_to_string().await?;
// 通知结果
notifier.notify_prompt_end(&project_id, session_id, StopReason::EndTurn, None, None).await?;
}
// 处理取消请求
Some(cancel) = cancel_rx.recv() => {
cx.send_notification(CancelNotification::new(session_id.clone()))?;
}
// 处理取消信号
_ = cancel_token.cancelled() => {
break;
}
}
}
Ok(())
})
.await
}
}
```
### 2.4 Worker 简化
#### 2.4.1 移除 LocalSet 依赖
```rust
// crates/agent_runner/src/proxy_agent/acp_agent.rs
/// SACP 版本的 Agent Worker
///
/// 关键变化:移除了 LocalSet 和 spawn_blocking
pub async fn agent_worker_sacp(
mut receiver: mpsc::UnboundedReceiver<AgentRequest>,
handle: WorkerHandle,
) {
while let Some(request) = receiver.recv().await {
// 直接使用 tokio::spawn无需 spawn_blocking + LocalSet
let handle = handle.clone();
tokio::spawn(async move {
let result = process_agent_request(request).await;
// 处理结果
});
}
}
/// 处理单个 Agent 请求
///
/// 现在可以直接在标准 Tokio task 中运行
async fn process_agent_request(request: AgentRequest) -> Result<AgentResponse> {
let worker = AcpAgentWorker::new(/* ... */);
// 直接调用,无需 LocalSet
worker.process_request(request.into()).await
}
```
### 2.5 连接信息结构
```rust
// crates/agent_abstraction/src/acp/connection.rs
/// SACP 版本的连接信息
///
/// 与旧版本兼容,但内部使用 SACP
#[derive(Debug)]
pub struct SacpConnectionInfo {
/// 会话 ID
pub session_id: sacp::schema::SessionId,
/// Prompt 发送通道
pub prompt_tx: mpsc::UnboundedSender<PromptRequest>,
/// Cancel 发送通道
pub cancel_tx: mpsc::UnboundedSender<CancelNotification>,
/// 生命周期守卫
pub lifecycle_guard: Arc<AgentLifecycleGuard>,
}
impl SacpConnectionInfo {
/// 发送 Prompt异步
pub async fn send_prompt(&self, request: PromptRequest) -> Result<()> {
self.prompt_tx.send(request)?;
Ok(())
}
/// 发送取消请求
pub async fn send_cancel(&self, notification: CancelNotification) -> Result<()> {
self.cancel_tx.send(notification)?;
Ok(())
}
/// 检查通道是否关闭
pub fn is_closed(&self) -> bool {
self.prompt_tx.is_closed() || self.cancel_tx.is_closed()
}
}
```
## 3. 迁移策略
### 3.1 分阶段迁移
#### 第一阶段:添加 SACP 依赖和适配层
1. 添加 `sacp = "10.1.0"` 依赖
2. 创建 `acp_adapter` crate定义协议无关的抽象
3. 实现 SACP 适配器
4. 保留官方 ACP 实现作为 fallback
#### 第二阶段:重构启动器
1. 创建 `claude_code_sacp.rs`,实现 SACP 版本启动器
2. 通过 feature flag 控制使用哪个实现
3. 测试 SACP 版本的功能完整性
#### 第三阶段:移除 LocalSet 依赖
1. 修改 `acp_agent.rs`,移除 `spawn_blocking` + `LocalSet`
2. 使用标准 `tokio::spawn`
3. 简化 Worker 架构
#### 第四阶段:清理和优化
1. 移除官方 ACP 依赖(可选保留兼容层)
2. 优化连接管理
3. 添加单元测试和集成测试
### 3.2 Feature Flag 设计
```toml
# crates/agent_abstraction/Cargo.toml
[features]
default = ["sacp"]
# SACP 实现(新版本,推荐)
sacp = ["dep:sacp"]
# 官方 ACP 实现(兼容层)
legacy-acp = ["dep:agent-client-protocol"]
[dependencies]
# SACP 依赖(可选)
sacp = { version = "10.1.0", optional = true }
# 官方 ACP 依赖(可选,保留兼容)
agent-client-protocol = { version = "0.9.3", features = ["unstable"], optional = true }
```
### 3.3 运行时切换
```rust
// crates/agent_abstraction/src/launcher/mod.rs
#[cfg(feature = "sacp")]
pub use claude_code_sacp::SacpClaudeCodeLauncher as ClaudeCodeLauncher;
#[cfg(all(feature = "legacy-acp", not(feature = "sacp")))]
pub use claude_code::ClaudeCodeLauncher;
/// 创建默认启动器
pub fn create_launcher<N: SessionNotifier + 'static>(
notifier: Arc<N>,
) -> impl AgentLauncher {
#[cfg(feature = "sacp")]
{
SacpClaudeCodeLauncher::new(notifier)
}
#[cfg(all(feature = "legacy-acp", not(feature = "sacp")))]
{
ClaudeCodeLauncher::new(notifier)
}
}
```
## 4. 关键数据结构对照
### 4.1 消息类型映射
| 官方 ACP | SACP | 说明 |
|---------|------|------|
| `InitializeRequest` | `sacp::schema::InitializeRequest` | 初始化请求 |
| `NewSessionRequest` | `sacp::schema::NewSessionRequest` | 创建会话 |
| `PromptRequest` | `sacp::schema::PromptRequest` | Prompt 请求 |
| `CancelNotification` | `sacp::schema::CancelNotification` | 取消通知 |
| `SessionId` | `sacp::schema::SessionId` | 会话 ID |
| `StopReason` | `sacp::schema::StopReason` | 停止原因 |
### 4.2 连接类型映射
| 官方 ACP | SACP | 说明 |
|---------|------|------|
| `ClientSideConnection` | `ClientToAgent::builder()` | 客户端连接 |
| `AgentSideConnection` | `AgentToClient::builder()` | Agent 连接 |
| N/A | `ByteStreams` | stdio 传输层 |
| N/A | `Channel` | 进程内通道 |
### 4.3 Trait 映射
| 官方 ACP | SACP | 说明 |
|---------|------|------|
| `Client` | `Component<AgentToClient>` | 客户端组件 |
| `Agent` | `Component<ClientToAgent>` | Agent 组件 |
| N/A | `JrRequest` | 请求 trait |
| N/A | `JrNotification` | 通知 trait |
| N/A | `JrResponsePayload` | 响应 trait |
## 5. 注意事项
### 5.1 兼容性
1. **SessionId 类型**SACP 的 `SessionId` 与官方 ACP 基本兼容,但需要适配
2. **Meta 字段**`_meta.claudeCode.options.resume` 等字段格式需要保持一致
3. **MCP 服务器配置**SACP 内置 MCP 支持,配置方式略有不同
### 5.2 错误处理
1. **SACP 错误类型**:使用 `sacp::Error` 替代 `anyhow::Error`
2. **错误码**:确保错误码与现有 gRPC 接口兼容
3. **降级处理**Resume 失败时的降级逻辑需要在新架构中重新实现
### 5.3 性能考量
1. **移除 LocalSet 开销**:预期减少线程切换和上下文开销
2. **连接池**:考虑实现 SACP 连接池,复用连接
3. **消息序列化**SACP 使用 `jsonrpcmsg`,性能与官方 ACP 相当
### 5.4 测试策略
1. **单元测试**:使用 SACP 的 `sacp-test` crate
2. **集成测试**:使用 `Channel::duplex()` 进行进程内测试
3. **E2E 测试**:验证与 Claude Code 子进程的实际通信
## 6. 依赖变更
### 6.1 移除的依赖
```toml
# 移除(或标记为 optional
agent-client-protocol = { version = "0.9.3", features = ["unstable"] }
```
### 6.2 新增的依赖
```toml
# 新增
sacp = "10.1.0"
sacp-tokio = "10.1.0" # 可选:用于 AcpAgent 进程管理
# 间接依赖(通过 sacp
agent-client-protocol-schema = "0.9.3" # Schema 类型定义
jsonrpcmsg = "..." # JSON-RPC 消息
```
### 6.3 本地依赖(开发阶段)
```toml
# 使用 vendors 目录的本地源码(便于调试)
sacp = { path = "../vendors/symposium-acp/src/sacp" }
```
## 7. 时间线(建议)
| 阶段 | 内容 | 风险 |
|-----|------|------|
| 阶段一 | 添加适配层,保持双实现 | 低 |
| 阶段二 | 实现 SACP 启动器 | 中 |
| 阶段三 | 移除 LocalSet简化架构 | 中 |
| 阶段四 | 清理旧代码,完善测试 | 低 |
## 8. 回滚方案
如果迁移过程中遇到严重问题:
1. 通过 feature flag 快速切回官方 ACP 实现
2. 保留 `legacy-acp` feature 作为长期回退选项
3. 使用条件编译隔离新旧代码
```toml
# 回滚:禁用 SACP启用官方 ACP
[features]
default = ["legacy-acp"] # 改为默认使用旧实现
```
## 9. 参考资料
- SACP 官方仓库https://github.com/symposium-dev/symposium-acp
- SACP 文档:`vendors/symposium-acp/md/` 目录
- 官方 ACP 协议https://agentclientprotocol.com/
- 项目本地源码:`vendors/symposium-acp/src/sacp/`

View File

@@ -0,0 +1,5 @@
# Instractions
## Project Alpha
我现在使用的官方版本: agent-client-protocol = { version = "0.9.3", features = ["unstable"] } , 目前官方acp协议有个问题,agent端,只能在 LocalSet 下运行,因为没实现 Send trait ,我现在发现一个基于官方acp协议重新封装的acp库: https://github.com/symposium-dev/symposium-acp ,对acp协议封装的更好,我想把acp协议从官方的,切换到 symposium-acp 来使用,对应最新版本是: sacp = "10.1.0" , 需要更新 crates/agent_runner , crates/agent_abstraction 等模块