Files
qiming/qiming-rcoder/specs/agent-config-instruction/02-agent-config-instruction-spec-plan.md
2026-06-01 13:54:52 +08:00

26 KiB
Raw Blame History

Agent 配置入参功能 - 详细实现计划

基于 01-agent-config-instruction-spec.md 设计文档生成

1. 实现概述

1.1 目标

/chat 接口增加三个可选配置入参:

  • system_prompt: 系统提示词
  • user_prompt: 用户提示词模板(支持 {user_prompt} 变量注入)
  • agent_config: Agent 运行时配置(包含 agent_servercontext_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()

代码模板:

// 见设计文档 2.2 节完整代码

验证点:

  • 编译通过
  • JSON 序列化/反序列化测试通过
  • 默认值正确

2.1.2 导出新类型

文件: crates/shared_types/src/lib.rs

修改内容:

// 新增
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 消息中添加新字段:
message ChatRequest {
  // ... 现有字段 (1-7) ...

  // 新增字段
  optional string system_prompt = 8;              // 系统提示词
  optional string user_prompt = 9;                // 用户提示词模板
  optional ChatAgentConfig agent_config = 10;     // Agent 运行时配置
}
  1. 添加新消息定义:
// 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
use shared_types::ChatAgentConfig;
  1. ChatRequest 结构体中添加新字段:
#[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 时传递新字段:
// 找到构建 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()),
};
  1. 实现 From<ChatAgentConfig> 转换(如需要):
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

代码模板:

// 见设计文档 4.5.1 节完整代码

验证点:

  • 编译通过
  • 单元测试:入参优先级正确
  • 单元测试:模板替换正确
  • 单元测试:配置合并正确

3.1.2 导出新模块

文件: crates/agent_config/src/config/mod.rs

修改内容:

pub mod prompt_assembler;
pub use prompt_assembler::PromptConfigAssembler;

文件: crates/agent_config/src/lib.rs

修改内容:

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)

修改内容:

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
use shared_types::ChatAgentConfig;
  1. PromptMessage 结构体中添加新字段:
#[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>,
}
  1. 修改 From<ChatPrompt> 实现:
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
use shared_types::ChatAgentConfig;
  1. 解析新字段并传递给 ChatPrompt
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)))?;

    // ... 后续处理 ...
}
  1. 实现 gRPC 类型转换(如果需要):
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
use agent_config::{AgentServersConfig, PromptConfigAssembler};
  1. 在处理请求时使用配置组装器:
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
use shared_types::ChatAgentConfig;
use agent_config::PromptConfigAssembler;
  1. 修改或新增配置加载方法:
/// 加载 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 ...
}
  1. launch() 方法中使用新配置:
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

测试用例:

#[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

测试用例:

#[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 (或在现有测试文件中添加)

测试用例:

#[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 配置合并逻辑

  • envmetadata 是合并(入参优先)
  • args 是替换(入参完全覆盖)
  • 提示词是覆盖(入参有值则使用入参)

7.4 测试覆盖

  • 确保测试覆盖所有配置组合
  • 特别注意边界情况:空字符串、空 HashMap 等

8. 预计工作量

批次 预计工作量 依赖
第一批 2-3 小时
第二批 2-3 小时 第一批
第三批 2-3 小时 第二批
第四批 1-2 小时 第三批

总计: 约 7-11 小时