添加qiming-rcoder模块
This commit is contained in:
1916
qiming-rcoder/specs/agent-audio/agent-audio-spec.md
Normal file
1916
qiming-rcoder/specs/agent-audio/agent-audio-spec.md
Normal file
File diff suppressed because it is too large
Load Diff
10
qiming-rcoder/specs/agent-audio/agent-audio.md
Normal file
10
qiming-rcoder/specs/agent-audio/agent-audio.md
Normal 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
@@ -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 小时
|
||||
@@ -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 中,输出中文
|
||||
596
qiming-rcoder/specs/agent-resume-acp/agent-resume-acp-plan.md
Normal file
596
qiming-rcoder/specs/agent-resume-acp/agent-resume-acp-plan.md
Normal 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_session,resume 通过 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 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
592
qiming-rcoder/specs/agent-resume-acp/agent-resume-acp-spec.md
Normal file
592
qiming-rcoder/specs/agent-resume-acp/agent-resume-acp-spec.md
Normal 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_session(claude-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_session,resume 通过 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)
|
||||
16
qiming-rcoder/specs/agent-resume-acp/agent-resume-acp.md
Normal file
16
qiming-rcoder/specs/agent-resume-acp/agent-resume-acp.md
Normal 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
|
||||
1667
qiming-rcoder/specs/computer-agent-runner/0001-spec-claude.md
Normal file
1667
qiming-rcoder/specs/computer-agent-runner/0001-spec-claude.md
Normal file
File diff suppressed because it is too large
Load Diff
1679
qiming-rcoder/specs/computer-agent-runner/0002-plan-claude.md
Normal file
1679
qiming-rcoder/specs/computer-agent-runner/0002-plan-claude.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
# Instruction
|
||||
|
||||
## project alpha
|
||||
|
||||
我要做一个带有虚拟远程桌面的agentic ai,agent 可以在这个带有远程桌面的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
|
||||
959
qiming-rcoder/specs/docker-pod-info/0001-docker-pod-info.md
Normal file
959
qiming-rcoder/specs/docker-pod-info/0001-docker-pod-info.md
Normal 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 |
|
||||
13
qiming-rcoder/specs/docker-pod-info/docker-pod-info.md
Normal file
13
qiming-rcoder/specs/docker-pod-info/docker-pod-info.md
Normal 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
|
||||
2637
qiming-rcoder/specs/duckdb-memory/0001-duckdb-memory-spec.md
Normal file
2637
qiming-rcoder/specs/duckdb-memory/0001-duckdb-memory-spec.md
Normal file
File diff suppressed because it is too large
Load Diff
17
qiming-rcoder/specs/duckdb-memory/duckdb-memory.md
Normal file
17
qiming-rcoder/specs/duckdb-memory/duckdb-memory.md
Normal 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
|
||||
3713
qiming-rcoder/specs/history/agent-abstraction-layer-design.md
Normal file
3713
qiming-rcoder/specs/history/agent-abstraction-layer-design.md
Normal file
File diff suppressed because it is too large
Load Diff
699
qiming-rcoder/specs/history/grpc-migration-rcoder-agentrunner.md
Normal file
699
qiming-rcoder/specs/history/grpc-migration-rcoder-agentrunner.md
Normal 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 API(Axum) | ❌ 不变 |
|
||||
| **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): 初始版本
|
||||
710
qiming-rcoder/specs/history/multi-docker-image-design.md
Normal file
710
qiming-rcoder/specs/history/multi-docker-image-design.md
Normal 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*
|
||||
356
qiming-rcoder/specs/isolation_type/0001-spec-isolation-type.md
Normal file
356
qiming-rcoder/specs/isolation_type/0001-spec-isolation-type.md
Normal 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/)
|
||||
379
qiming-rcoder/specs/isolation_type/plan.md
Normal file
379
qiming-rcoder/specs/isolation_type/plan.md
Normal 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*
|
||||
303
qiming-rcoder/specs/isolation_type/tasks.md
Normal file
303
qiming-rcoder/specs/isolation_type/tasks.md
Normal 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*
|
||||
273
qiming-rcoder/specs/k8s-runtime-support/data-model.md
Normal file
273
qiming-rcoder/specs/k8s-runtime-support/data-model.md
Normal 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 |
|
||||
348
qiming-rcoder/specs/k8s-runtime-support/kubernetes-deploy.md
Normal file
348
qiming-rcoder/specs/k8s-runtime-support/kubernetes-deploy.md
Normal 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/)
|
||||
235
qiming-rcoder/specs/k8s-runtime-support/plan.md
Normal file
235
qiming-rcoder/specs/k8s-runtime-support/plan.md
Normal 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 在同一集群内可直接通信,无需额外 Service;Pod 重启后 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`*
|
||||
168
qiming-rcoder/specs/k8s-runtime-support/research.md
Normal file
168
qiming-rcoder/specs/k8s-runtime-support/research.md
Normal 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 权限如何预配置?
|
||||
65
qiming-rcoder/specs/k8s-runtime-support/spec.md
Normal file
65
qiming-rcoder/specs/k8s-runtime-support/spec.md
Normal 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 模块未使用 RuntimeManager,K8s 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 模块未使用 RuntimeManager,K8s 路径从未触发 | 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
|
||||
325
qiming-rcoder/specs/k8s-runtime-support/tasks.md
Normal file
325
qiming-rcoder/specs/k8s-runtime-support/tasks.md
Normal 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
|
||||
676
qiming-rcoder/specs/sacp/sacp-spec.md
Normal file
676
qiming-rcoder/specs/sacp/sacp-spec.md
Normal 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/`
|
||||
5
qiming-rcoder/specs/sacp/sacp.md
Normal file
5
qiming-rcoder/specs/sacp/sacp.md
Normal 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 等模块
|
||||
Reference in New Issue
Block a user