添加qiming-rcoder模块

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

View File

@@ -0,0 +1,348 @@
//! 默认 Agent 配置定义
//!
//! 此模块从编译时嵌入的 JSON 配置文件加载默认配置。
//! 支持两种服务类型的配置:
//! - RCoder: `configs/default_agents.json`
//! - ComputerAgentRunner: `configs/computer_agent_default.json`
use std::collections::HashMap;
use std::sync::LazyLock;
use crate::types::agent_config::AgentConfig;
use crate::types::mcp_config::ContextServerConfig;
/// 编译时嵌入的 RCoder 默认配置 JSON
const DEFAULT_CONFIG_JSON: &str = include_str!("../../configs/default_agents.json");
/// 编译时嵌入的 ComputerAgentRunner 默认配置 JSON
const COMPUTER_AGENT_CONFIG_JSON: &str = include_str!("../../configs/computer_agent_default.json");
/// Claude Code ACP Agent 的默认 ID
pub const CLAUDE_CODE_ACP_AGENT_ID: &str = "claude-code-acp-ts";
/// 默认配置的内部结构(用于 JSON 反序列化)
#[derive(serde::Deserialize)]
pub struct EmbeddedConfig {
agent_servers: HashMap<String, AgentConfig>,
#[serde(default)]
context_servers: HashMap<String, ContextServerConfig>,
}
/// 懒加载的默认配置
///
/// 在首次访问时解析 JSON之后复用解析结果。
/// 如果 JSON 解析失败,程序会 panic这是编译时嵌入的文件不应失败
static DEFAULT_CONFIG: LazyLock<EmbeddedConfig> = LazyLock::new(|| {
serde_json::from_str(DEFAULT_CONFIG_JSON).expect(
"Failed to parse embedded default_agents.json. This is a bug - please check the JSON syntax.",
)
});
/// 懒加载的 ComputerAgentRunner 默认配置
///
/// 在首次访问时解析 JSON之后复用解析结果。
static COMPUTER_AGENT_CONFIG: LazyLock<EmbeddedConfig> = LazyLock::new(|| {
serde_json::from_str(COMPUTER_AGENT_CONFIG_JSON).expect(
"Failed to parse embedded computer_agent_default.json. This is a bug - please check the JSON syntax.",
)
});
/// 获取默认的 Claude Code ACP Agent 配置
///
/// 从 `configs/default_agents.json` 加载配置。
/// 修改 JSON 文件后重新编译即可生效。
///
/// # Panics
/// 如果 JSON 中不存在 `claude-code-acp-ts` 配置,会 panic。
pub fn default_claude_code_agent() -> AgentConfig {
DEFAULT_CONFIG
.agent_servers
.get(CLAUDE_CODE_ACP_AGENT_ID)
.cloned()
.expect("claude-code-acp-ts not found in default_agents.json")
}
/// 获取默认的 Context Servers 配置
///
/// 从 `configs/default_agents.json` 加载配置。
/// 修改 JSON 文件后重新编译即可生效。
pub fn default_context_servers() -> HashMap<String, ContextServerConfig> {
DEFAULT_CONFIG.context_servers.clone()
}
/// 获取默认的 Agent Servers 配置
///
/// 从 `configs/default_agents.json` 加载配置。
/// 修改 JSON 文件后重新编译即可生效。
pub fn default_agent_servers() -> HashMap<String, AgentConfig> {
DEFAULT_CONFIG.agent_servers.clone()
}
/// 获取指定 Agent 的默认配置
///
/// # 参数
/// - `agent_id`: Agent 标识符
///
/// # 返回
/// 如果存在返回配置的克隆,否则返回 None
pub fn get_default_agent(agent_id: &str) -> Option<AgentConfig> {
DEFAULT_CONFIG.agent_servers.get(agent_id).cloned()
}
/// 获取指定 Context Server 的默认配置
///
/// # 参数
/// - `name`: Context Server 名称
///
/// # 返回
/// 如果存在返回配置的克隆,否则返回 None
pub fn get_default_context_server(name: &str) -> Option<ContextServerConfig> {
DEFAULT_CONFIG.context_servers.get(name).cloned()
}
/// 根据服务类型获取默认配置
///
/// # 参数
/// - `service_type`: 服务类型RCoder 或 ComputerAgentRunner
///
/// # 返回
/// 对应服务类型的默认配置引用
pub fn get_default_config_by_service_type(
service_type: &shared_types::ServiceType,
) -> &'static EmbeddedConfig {
match service_type {
shared_types::ServiceType::RCoder => &DEFAULT_CONFIG,
shared_types::ServiceType::ComputerAgentRunner => &COMPUTER_AGENT_CONFIG,
}
}
/// 根据服务类型获取默认 Agent Servers 配置
///
/// # 参数
/// - `service_type`: 服务类型
///
/// # 返回
/// Agent Servers 配置的克隆
pub fn default_agent_servers_for_service(
service_type: &shared_types::ServiceType,
) -> HashMap<String, AgentConfig> {
get_default_config_by_service_type(service_type)
.agent_servers
.clone()
}
/// 根据服务类型获取默认 Context Servers 配置
///
/// # 参数
/// - `service_type`: 服务类型
///
/// # 返回
/// Context Servers 配置的克隆
pub fn default_context_servers_for_service(
service_type: &shared_types::ServiceType,
) -> HashMap<String, ContextServerConfig> {
get_default_config_by_service_type(service_type)
.context_servers
.clone()
}
/// 根据服务类型获取指定 Agent 的默认配置
///
/// # 参数
/// - `agent_id`: Agent 标识符
/// - `service_type`: 服务类型
///
/// # 返回
/// 如果存在返回配置的克隆,否则返回 None
pub fn get_default_agent_for_service(
agent_id: &str,
service_type: &shared_types::ServiceType,
) -> Option<AgentConfig> {
get_default_config_by_service_type(service_type)
.agent_servers
.get(agent_id)
.cloned()
}
/// 根据服务类型获取指定 Context Server 的默认配置
///
/// # 参数
/// - `name`: Context Server 名称
/// - `service_type`: 服务类型
///
/// # 返回
/// 如果存在返回配置的克隆,否则返回 None
pub fn get_default_context_server_for_service(
name: &str,
service_type: &shared_types::ServiceType,
) -> Option<ContextServerConfig> {
get_default_config_by_service_type(service_type)
.context_servers
.get(name)
.cloned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_loads_successfully() {
// 确保 JSON 能成功解析
let _ = &*DEFAULT_CONFIG;
}
#[test]
fn test_default_claude_code_agent() {
let agent = default_claude_code_agent();
assert_eq!(agent.agent_id, CLAUDE_CODE_ACP_AGENT_ID);
assert_eq!(agent.command, "claude-code-acp-ts");
assert!(agent.enabled);
assert!(agent.env.contains_key("ANTHROPIC_API_KEY"));
}
#[test]
fn test_default_context_servers() {
let servers = default_context_servers();
// 验证 JSON 中定义的 context servers
assert!(servers.contains_key("fetch") || servers.contains_key("context7"));
}
#[test]
fn test_default_agent_servers() {
let agents = default_agent_servers();
assert!(agents.contains_key(CLAUDE_CODE_ACP_AGENT_ID));
}
#[test]
fn test_get_default_agent() {
let agent = get_default_agent(CLAUDE_CODE_ACP_AGENT_ID);
assert!(agent.is_some());
assert_eq!(agent.unwrap().agent_id, CLAUDE_CODE_ACP_AGENT_ID);
let non_existent = get_default_agent("non-existent-agent");
assert!(non_existent.is_none());
}
#[test]
fn test_get_default_context_server() {
// 测试存在的 server
let servers = default_context_servers();
if let Some(first_server_name) = servers.keys().next() {
let server = get_default_context_server(first_server_name);
assert!(server.is_some());
}
// 测试不存在的 server
let non_existent = get_default_context_server("non-existent-server");
assert!(non_existent.is_none());
}
#[test]
fn test_computer_agent_config_loads_successfully() {
// 确保 ComputerAgentRunner 配置能成功解析
let _ = &*COMPUTER_AGENT_CONFIG;
}
#[test]
fn test_get_default_config_by_service_type() {
// 测试 RCoder 配置
let rcoder_config = get_default_config_by_service_type(&shared_types::ServiceType::RCoder);
assert!(
rcoder_config
.agent_servers
.contains_key(CLAUDE_CODE_ACP_AGENT_ID)
);
// 测试 ComputerAgentRunner 配置
let car_config =
get_default_config_by_service_type(&shared_types::ServiceType::ComputerAgentRunner);
assert!(
car_config
.agent_servers
.contains_key(CLAUDE_CODE_ACP_AGENT_ID)
);
// ComputerAgentRunner 应该有更多 context servers
assert!(
car_config.context_servers.contains_key("chrome-devtools")
|| car_config.context_servers.contains_key("puppeteer")
);
}
#[test]
fn test_default_agent_servers_for_service() {
// RCoder
let rcoder_servers = default_agent_servers_for_service(&shared_types::ServiceType::RCoder);
assert!(!rcoder_servers.is_empty());
// ComputerAgentRunner
let car_servers =
default_agent_servers_for_service(&shared_types::ServiceType::ComputerAgentRunner);
assert!(!car_servers.is_empty());
}
#[test]
fn test_default_context_servers_for_service() {
// RCoder - 基础 MCP servers
let rcoder_contexts =
default_context_servers_for_service(&shared_types::ServiceType::RCoder);
assert!(rcoder_contexts.contains_key("fetch") || rcoder_contexts.contains_key("context7"));
// ComputerAgentRunner - 增强 MCP servers
let car_contexts =
default_context_servers_for_service(&shared_types::ServiceType::ComputerAgentRunner);
// 至少应该包含浏览器相关的 server
assert!(car_contexts.len() >= rcoder_contexts.len());
}
#[test]
fn test_get_default_agent_for_service() {
// 测试 RCoder
let agent = get_default_agent_for_service(
CLAUDE_CODE_ACP_AGENT_ID,
&shared_types::ServiceType::RCoder,
);
assert!(agent.is_some());
// 测试 ComputerAgentRunner
let agent = get_default_agent_for_service(
CLAUDE_CODE_ACP_AGENT_ID,
&shared_types::ServiceType::ComputerAgentRunner,
);
assert!(agent.is_some());
// 测试不存在的 agent
let agent =
get_default_agent_for_service("non-existent-agent", &shared_types::ServiceType::RCoder);
assert!(agent.is_none());
}
#[test]
fn test_get_default_context_server_for_service() {
// RCoder context server
let rcoder_contexts =
default_context_servers_for_service(&shared_types::ServiceType::RCoder);
if let Some(first_server_name) = rcoder_contexts.keys().next() {
let server = get_default_context_server_for_service(
first_server_name,
&shared_types::ServiceType::RCoder,
);
assert!(server.is_some());
}
// ComputerAgentRunner context server
let _server = get_default_context_server_for_service(
"chrome-devtools",
&shared_types::ServiceType::ComputerAgentRunner,
);
// 可能存在或不存在,取决于配置
// 测试不存在的 server
let server = get_default_context_server_for_service(
"non-existent-server",
&shared_types::ServiceType::RCoder,
);
assert!(server.is_none());
}
}

View File

@@ -0,0 +1,13 @@
//! Configuration management module.
//!
//! - `default_agent_config`: 默认 Agent 配置定义
//! - `servers_config`: 配置文件加载和管理
//! - `prompt_assembler`: 提示词配置组装工具
pub mod default_agent_config;
pub mod prompt_assembler;
pub mod servers_config;
pub use default_agent_config::*;
pub use prompt_assembler::PromptConfigAssembler;
pub use servers_config::*;

View File

@@ -0,0 +1,384 @@
//! 提示词配置组装工具
//!
//! 将用户入参组装为内部使用的配置结构。
//! 简化设计:直接使用入参,无优先级冲突。
use shared_types::{ChatAgentConfig, ChatAgentServerConfig};
use std::collections::HashMap;
use super::servers_config::AgentServersConfig;
use crate::types::agent_config::AgentConfig;
use crate::types::mcp_config::ContextServerConfig;
/// 提示词配置组装器
///
/// 职责:
/// 1. 组装系统提示词(入参 > 默认配置)
/// 2. 应用用户提示词模板
/// 3. 组装 Agent 服务器配置
/// 4. 合并 MCP 服务器配置
pub struct PromptConfigAssembler {
/// 系统提示词入参
system_prompt: Option<String>,
/// 用户提示词模板入参
user_prompt_template: Option<String>,
/// Agent 运行时配置入参
agent_config: Option<ChatAgentConfig>,
/// 默认配置
default_config: AgentServersConfig,
}
impl PromptConfigAssembler {
/// 创建新的配置组装器
///
/// # 参数
/// - `default_config`: 默认配置(从文件或内置配置加载)
pub fn new(default_config: AgentServersConfig) -> Self {
Self {
system_prompt: None,
user_prompt_template: None,
agent_config: None,
default_config,
}
}
/// 设置系统提示词覆盖
pub fn with_system_prompt(mut self, system_prompt: Option<String>) -> Self {
self.system_prompt = system_prompt;
self
}
/// 设置用户提示词模板覆盖
pub fn with_user_prompt_template(mut self, template: Option<String>) -> Self {
self.user_prompt_template = template;
self
}
/// 设置 Agent 运行时配置覆盖
pub fn with_agent_config(mut self, config: Option<ChatAgentConfig>) -> Self {
self.agent_config = config;
self
}
/// 获取最终的系统提示词
///
/// 逻辑:入参有值则使用入参,否则使用默认配置
pub fn get_system_prompt(&self, agent_id: &str) -> String {
// 入参有值且非空,直接使用
if let Some(ref sp) = self.system_prompt {
if !sp.is_empty() {
return sp.clone();
}
}
// 使用默认配置
self.default_config.get_system_prompt(agent_id)
}
/// 应用用户提示词模板
///
/// 逻辑:
/// 1. 如果有模板入参,使用模板替换 `{user_prompt}`
/// 2. 如果没有模板入参,检查默认配置
/// 3. 都没有,直接返回原始输入
pub fn apply_user_prompt(&self, agent_id: &str, user_input: &str) -> String {
// 入参有模板且非空,使用入参模板
if let Some(ref template) = self.user_prompt_template {
if !template.is_empty() {
return template.replace("{user_prompt}", user_input);
}
}
// 检查默认配置中的 user_prompt 模板
if let Some(agent) = self.default_config.get_agent(agent_id) {
if let Some(ref prompt_config) = agent.user_prompt {
if prompt_config.enabled {
return prompt_config.apply(user_input);
}
}
}
// 无模板,直接返回原始输入
user_input.to_string()
}
/// 获取最终的 Agent 服务器配置
///
/// 逻辑:
/// 1. 如果入参有 agent_server与默认配置合并入参字段覆盖默认值
/// 2. 如果入参没有 agent_server使用默认配置
pub fn get_agent_server_config(&self, default_agent_id: &str) -> AgentConfig {
// 获取默认的 Agent 配置
let default_agent = self
.default_config
.get_agent(default_agent_id)
.cloned()
.unwrap_or_default();
// 如果入参有 agent_server 配置,合并覆盖
if let Some(ref config) = self.agent_config {
if let Some(ref agent_server) = config.agent_server {
return self.merge_agent_config(&default_agent, agent_server);
}
}
// 使用默认配置
default_agent
}
/// 合并 Agent 配置(入参覆盖默认值)
fn merge_agent_config(
&self,
default: &AgentConfig,
override_config: &ChatAgentServerConfig,
) -> AgentConfig {
let mut merged = default.clone();
// agent_id: 入参有值则覆盖
if let Some(ref agent_id) = override_config.agent_id {
merged.agent_id = agent_id.clone();
}
// command: 入参有值则覆盖
if let Some(ref command) = override_config.command {
merged.command = command.clone();
}
// args: 入参有值则覆盖(替换而非追加)
if let Some(ref args) = override_config.args {
merged.args = args.clone();
}
// env: 入参有值则合并(入参优先)
if let Some(ref env) = override_config.env {
for (key, value) in env {
merged.env.insert(key.clone(), value.clone());
}
}
// metadata: 入参有值则合并
if let Some(ref metadata) = override_config.metadata {
for (key, value) in metadata {
merged.metadata.insert(key.clone(), value.clone());
}
}
merged
}
/// 获取最终的 MCP 服务器配置
///
/// 逻辑:
/// 1. 如果入参明确提供了非空的 context_servers使用入参
/// 2. 否则使用默认配置
///
/// 注意:即使提供了 agent_config但 context_servers 为空时,仍使用默认配置
pub fn get_context_servers(&self) -> HashMap<String, ContextServerConfig> {
// 入参有非空的 MCP 配置,使用入参
if let Some(ref config) = self.agent_config {
if config.has_context_servers() {
return config
.context_servers
.iter()
.map(|(name, chat_config)| {
let ctx_config = ContextServerConfig {
source: chat_config.source.clone(),
enabled: chat_config.enabled,
command: chat_config.command.clone(),
args: chat_config.args.clone(),
env: chat_config.env.clone(),
};
(name.clone(), ctx_config)
})
.collect();
}
}
// context_servers 为空或未提供,使用默认配置
self.default_config.context_servers.clone()
}
/// 获取使用的 Agent ID
///
/// 逻辑:入参有指定则使用入参,否则使用默认
pub fn get_agent_id(&self, default_agent_id: &str) -> String {
if let Some(ref config) = self.agent_config {
if let Some(ref agent_server) = config.agent_server {
return agent_server.get_agent_id().to_string();
}
}
default_agent_id.to_string()
}
/// 检查是否有系统提示词覆盖
pub fn has_system_prompt_override(&self) -> bool {
self.system_prompt.as_ref().is_some_and(|s| !s.is_empty())
}
/// 检查是否有用户提示词模板覆盖
pub fn has_user_prompt_template_override(&self) -> bool {
self.user_prompt_template
.as_ref()
.is_some_and(|s| !s.is_empty())
}
/// 检查是否有 Agent 配置覆盖
pub fn has_agent_config_override(&self) -> bool {
self.agent_config.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_default_config() -> AgentServersConfig {
AgentServersConfig::default()
}
#[test]
fn test_system_prompt_with_override() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config)
.with_system_prompt(Some("自定义系统提示词".to_string()));
let result = assembler.get_system_prompt("claude-code-acp-ts");
assert_eq!(result, "自定义系统提示词");
}
#[test]
fn test_system_prompt_without_override() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config);
let result = assembler.get_system_prompt("claude-code-acp-ts");
// 应该返回默认系统提示词
assert!(!result.is_empty());
}
#[test]
fn test_system_prompt_empty_override() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config).with_system_prompt(Some("".to_string()));
let result = assembler.get_system_prompt("claude-code-acp-ts");
// 空字符串应该回退到默认值
assert!(!result.is_empty());
}
#[test]
fn test_user_prompt_with_template() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config)
.with_user_prompt_template(Some("请用 Rust 完成:{user_prompt}".to_string()));
let result = assembler.apply_user_prompt("claude-code-acp-ts", "Hello World");
assert_eq!(result, "请用 Rust 完成Hello World");
}
#[test]
fn test_user_prompt_without_template() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config);
let result = assembler.apply_user_prompt("claude-code-acp-ts", "Hello World");
// 没有模板时应该返回原始输入
assert_eq!(result, "Hello World");
}
#[test]
fn test_context_servers_with_override() {
let config = create_test_default_config();
let mut context_servers = HashMap::new();
context_servers.insert(
"my-mcp".to_string(),
shared_types::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 agent_config = ChatAgentConfig {
agent_server: None,
context_servers,
resource_limits: None,
};
let assembler = PromptConfigAssembler::new(config).with_agent_config(Some(agent_config));
let result = assembler.get_context_servers();
assert!(result.contains_key("my-mcp"));
assert_eq!(
result.get("my-mcp").unwrap().command,
Some("bunx".to_string())
);
}
#[test]
fn test_context_servers_without_override() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config);
let result = assembler.get_context_servers();
// 应该返回默认的 context servers
assert!(!result.is_empty());
}
#[test]
fn test_agent_id_with_override() {
let config = create_test_default_config();
let agent_config = ChatAgentConfig {
agent_server: Some(shared_types::ChatAgentServerConfig {
agent_id: Some("custom-agent".to_string()),
command: None,
args: None,
env: None,
metadata: None,
}),
context_servers: HashMap::new(),
resource_limits: None,
};
let assembler = PromptConfigAssembler::new(config).with_agent_config(Some(agent_config));
let result = assembler.get_agent_id("claude-code-acp-ts");
assert_eq!(result, "custom-agent");
}
#[test]
fn test_agent_id_without_override() {
let config = create_test_default_config();
let assembler = PromptConfigAssembler::new(config);
let result = assembler.get_agent_id("claude-code-acp-ts");
assert_eq!(result, "claude-code-acp-ts");
}
#[test]
fn test_merge_agent_config_env() {
let config = create_test_default_config();
let mut override_env = HashMap::new();
override_env.insert("NEW_VAR".to_string(), "new_value".to_string());
let agent_config = ChatAgentConfig {
agent_server: Some(shared_types::ChatAgentServerConfig {
agent_id: None,
command: None,
args: None,
env: Some(override_env),
metadata: None,
}),
context_servers: HashMap::new(),
resource_limits: None,
};
let assembler = PromptConfigAssembler::new(config).with_agent_config(Some(agent_config));
let result = assembler.get_agent_server_config("claude-code-acp-ts");
assert!(result.env.contains_key("NEW_VAR"));
assert_eq!(result.env.get("NEW_VAR"), Some(&"new_value".to_string()));
}
}

View File

@@ -0,0 +1,226 @@
//! Agent servers configuration structure.
//!
//! 此模块负责配置的查询和管理。
//! 默认配置从 `default_agent_config` 模块加载(来源于 `configs/default_agents.json`)。
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use super::default_agent_config::{
default_agent_servers, default_agent_servers_for_service, default_context_servers,
default_context_servers_for_service,
};
use crate::types::agent_config::AgentConfig;
use crate::types::error::{ConfigError, Result};
use crate::types::mcp_config::ContextServerConfig;
use crate::types::system_prompt::DEFAULT_SYSTEM_PROMPT;
/// Agent servers configuration
///
/// 包含所有 Agent 和 Context Server 的配置集合。
/// 默认配置来自 `configs/default_agents.json`(编译时嵌入)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentServersConfig {
/// Agent servers configuration
pub agent_servers: HashMap<String, AgentConfig>,
/// Context servers configuration (MCP servers)
#[serde(default)]
pub context_servers: HashMap<String, ContextServerConfig>,
}
impl AgentServersConfig {
/// 获取默认配置
///
/// 返回从 `configs/default_agents.json` 加载的配置。
///
/// # 已弃用
///
/// 此方法已弃用,请使用 `load_or_default_for_service` 以支持多服务类型。
#[deprecated(
since = "0.2.0",
note = "请使用 load_or_default_for_service 以支持多服务类型"
)]
pub async fn load_or_default() -> Self {
Self::default()
}
/// 根据服务类型加载或使用默认配置
///
/// # 参数
/// - `service_type`: 服务类型RCoder 或 ComputerAgentRunner
///
/// # 返回
/// 对应服务类型的默认配置
pub async fn load_or_default_for_service(service_type: &shared_types::ServiceType) -> Self {
Self::default_for_service(service_type)
}
/// 根据服务类型创建默认配置
///
/// # 参数
/// - `service_type`: 服务类型
///
/// # 返回
/// 对应服务类型的默认配置实例
pub fn default_for_service(service_type: &shared_types::ServiceType) -> Self {
Self {
agent_servers: default_agent_servers_for_service(service_type),
context_servers: default_context_servers_for_service(service_type),
}
}
/// 从外部文件加载配置
///
/// 用于加载用户自定义的配置文件,覆盖默认配置。
pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
return Err(ConfigError::file_not_found(path.display()).into());
}
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| ConfigError::Read(format!("{}: {}", path.display(), e)))?;
Self::from_json(&content)
}
/// 从 JSON 字符串加载配置
///
/// 用于解析用户传入的 JSON 配置。
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| ConfigError::Deserialization(e.to_string()).into())
}
/// Validate configuration
pub fn validate(&self) -> Result<()> {
// Validate agent servers
for (name, config) in &self.agent_servers {
if config.agent_id.is_empty() {
return Err(
ConfigError::missing_field(format!("agent_servers.{}.agent_id", name)).into(),
);
}
if config.command.is_empty() {
return Err(
ConfigError::missing_field(format!("agent_servers.{}.command", name)).into(),
);
}
}
// Validate context servers
for (name, config) in &self.context_servers {
if config.command.is_none() {
return Err(ConfigError::missing_field(format!(
"context_servers.{}.command",
name
))
.into());
}
}
Ok(())
}
/// Get all enabled agents
pub fn get_enabled_agents(&self) -> Vec<&AgentConfig> {
self.agent_servers
.values()
.filter(|config| config.enabled)
.collect()
}
/// Get an agent configuration by ID
pub fn get_agent(&self, agent_id: &str) -> Option<&AgentConfig> {
self.agent_servers.get(agent_id)
}
/// 获取系统提示词(优先使用配置,否则使用默认值)
///
/// # 参数
/// - `agent_id`: Agent 标识符
///
/// # 返回
/// 根据配置的 source 字段决定返回内容:
/// - `source: "embedded"` -> 返回编译时嵌入的默认提示词
/// - `source: "custom"` 且 template 非空 -> 返回自定义模板内容
/// - 其他情况 -> 回退到编译时嵌入的默认提示词
///
/// 注意:此方法始终返回有效的系统提示词,不受 enabled 字段影响。
/// 如需检查是否启用,请直接调用 `SystemPromptConfig::get_prompt()`。
pub fn get_system_prompt(&self, agent_id: &str) -> String {
if let Some(agent) = self.get_agent(agent_id) {
// 如果有系统提示词配置,使用 get_prompt_or_default() 确保始终返回有效提示词
if let Some(ref prompt_config) = agent.system_prompt {
return prompt_config.get_prompt_or_default().to_string();
}
}
// 回退到编译时嵌入的默认值
DEFAULT_SYSTEM_PROMPT.to_string()
}
/// Get all enabled context servers
pub fn get_enabled_context_servers(&self) -> Vec<&ContextServerConfig> {
self.context_servers
.values()
.filter(|config| config.enabled)
.collect()
}
/// Get a context server configuration by name
pub fn get_context_server(&self, name: &str) -> Option<&ContextServerConfig> {
self.context_servers.get(name)
}
/// 创建默认配置
///
/// 从 `configs/default_agents.json` 加载的配置(编译时嵌入)。
/// 修改 JSON 文件后重新编译即可生效。
pub fn default() -> Self {
Self {
agent_servers: default_agent_servers(),
context_servers: default_context_servers(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_load_or_default_for_service() {
// 测试 RCoder
let rcoder_config =
AgentServersConfig::load_or_default_for_service(&shared_types::ServiceType::RCoder)
.await;
assert!(!rcoder_config.agent_servers.is_empty());
// 测试 ComputerAgentRunner
let car_config = AgentServersConfig::load_or_default_for_service(
&shared_types::ServiceType::ComputerAgentRunner,
)
.await;
assert!(!car_config.agent_servers.is_empty());
}
#[test]
fn test_default_for_service() {
// 测试 RCoder
let rcoder_config =
AgentServersConfig::default_for_service(&shared_types::ServiceType::RCoder);
assert!(!rcoder_config.agent_servers.is_empty());
// 测试 ComputerAgentRunner
let car_config = AgentServersConfig::default_for_service(
&shared_types::ServiceType::ComputerAgentRunner,
);
assert!(!car_config.agent_servers.is_empty());
// 验证配置不同
// ComputerAgentRunner 应该有更多或不同的 context servers
assert!(car_config.context_servers.len() >= rcoder_config.context_servers.len());
}
}

View File

@@ -0,0 +1,305 @@
//! Agent Installation Manager
//!
//! Manages agent installation, validation, and updates.
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use thiserror::Error;
use tracing::{debug, info, warn};
use super::npm_installer::NpmInstaller;
use super::traits::AgentInstaller;
use crate::types::installation::{InstallationConfig, PackageManager};
/// Installation error types
#[derive(Error, Debug)]
pub enum InstallationError {
#[error("Package manager '{0}' is not available")]
PackageManagerNotAvailable(String),
#[error("Package '{package}' not found in {manager}")]
PackageNotFound { package: String, manager: String },
#[error("Installation failed for '{package}': {reason}")]
InstallFailed { package: String, reason: String },
#[error("Validation failed: {0}")]
ValidationFailed(String),
#[error("Command failed: {command} - {reason}")]
CommandFailed { command: String, reason: String },
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Unsupported package manager: {0}")]
UnsupportedPackageManager(String),
}
/// Installation result
#[derive(Debug, Clone)]
pub struct InstallResult {
/// Whether installation was successful
pub success: bool,
/// Installed version
pub version: Option<String>,
/// Installation path
pub install_path: Option<String>,
/// Additional message
pub message: String,
/// Whether the agent was already installed
pub already_installed: bool,
}
impl InstallResult {
/// Create a successful installation result
pub fn success(version: Option<String>, message: impl Into<String>) -> Self {
Self {
success: true,
version,
install_path: None,
message: message.into(),
already_installed: false,
}
}
/// Create a result indicating already installed
pub fn already_installed(version: Option<String>) -> Self {
Self {
success: true,
version,
install_path: None,
message: "Already installed".to_string(),
already_installed: true,
}
}
/// Create a failed installation result
pub fn failed(message: impl Into<String>) -> Self {
Self {
success: false,
version: None,
install_path: None,
message: message.into(),
already_installed: false,
}
}
/// Set installation path
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.install_path = Some(path.into());
self
}
}
/// Agent Installation Manager
///
/// Manages multiple package manager installers and handles
/// agent installation lifecycle.
pub struct AgentInstallationManager {
installers: HashMap<String, Arc<dyn AgentInstaller>>,
}
impl AgentInstallationManager {
/// Create a new installation manager with default installers
pub fn new() -> Self {
let mut installers: HashMap<String, Arc<dyn AgentInstaller>> = HashMap::new();
// Register npm installer
installers.insert("npm".to_string(), Arc::new(NpmInstaller::new()));
Self { installers }
}
/// Register a custom installer
pub fn register_installer(
&mut self,
name: impl Into<String>,
installer: Arc<dyn AgentInstaller>,
) {
self.installers.insert(name.into(), installer);
}
/// Get installer for package manager
fn get_installer(&self, pm: &PackageManager) -> Option<Arc<dyn AgentInstaller>> {
let name = pm.as_str();
self.installers.get(name).cloned()
}
/// Ensure agent is installed
///
/// This method checks if the agent is already installed and working.
/// If not, it attempts to install it.
pub async fn ensure_installed(
&self,
config: &InstallationConfig,
command: &str,
) -> Result<InstallResult, InstallationError> {
info!("Checking Agent installation status: {}", command);
// First, check if the command already exists
if self.is_command_available(command).await {
// Validate if it's working correctly
if self.validate_installation(config, command).await? {
info!("Agent is installed and verified: {}", command);
return Ok(InstallResult::already_installed(None));
}
warn!(
"Agent command exists but verification failed, trying reinstall: {}",
command
);
}
// Not installed or validation failed, try to install
info!("Trying to install Agent: {}", command);
let result = self.install(config, None).await;
match &result {
Ok(install_result) => {
if install_result.success {
info!(
"Agent installed successfully: {} - {}",
command, install_result.message
);
} else {
warn!(
"⚠️ Agent Installation failed: {} - {}",
command, install_result.message
);
}
}
Err(e) => {
warn!("Agent installation error: {} - {}", command, e);
}
}
result
}
/// Install agent using appropriate package manager
pub async fn install(
&self,
config: &InstallationConfig,
install_dir: Option<&Path>,
) -> Result<InstallResult, InstallationError> {
let installer = self.get_installer(&config.package_manager).ok_or_else(|| {
InstallationError::UnsupportedPackageManager(
config.package_manager.as_str().to_string(),
)
})?;
let package_name = config.package_name.as_ref().ok_or_else(|| {
InstallationError::ConfigError("Package name is required".to_string())
})?;
info!(
"Starting Agent installation: {} via {}",
package_name,
installer.package_manager_name()
);
installer.install(config, install_dir).await
}
/// Validate agent installation
pub async fn validate_installation(
&self,
config: &InstallationConfig,
command: &str,
) -> Result<bool, InstallationError> {
debug!("Verifying Agent installation: {}", command);
// If validate_command is specified, use it
if let Some(validate_cmd) = &config.validate_command {
if !validate_cmd.is_empty() {
return self.run_validation_command(validate_cmd).await;
}
}
// 使用 which 检查命令是否可用(适用于 Debian/Docker 环境)
// which::which 是 Rust crate不依赖系统 which 命令
// 注意:只检查命令是否在 PATH 中,不运行命令避免副作用(如 claude-code-acp-ts --version 会阻塞)
if self.is_command_available(command).await {
debug!("Command exists in PATH: {}", command);
return Ok(true);
}
debug!("Command not in PATH: {}", command);
Ok(false)
}
/// Check if a command is available in PATH
async fn is_command_available(&self, command: &str) -> bool {
which::which(command).is_ok()
}
/// Run custom validation command
async fn run_validation_command(&self, cmd: &[String]) -> Result<bool, InstallationError> {
if cmd.is_empty() {
return Ok(false);
}
let program = &cmd[0];
let args: Vec<&str> = cmd[1..].iter().map(|s| s.as_str()).collect();
debug!("Running verification command: {} {:?}", program, args);
let output = tokio::process::Command::new(program)
.args(&args)
.output()
.await
.map_err(|e| InstallationError::CommandFailed {
command: format!("{} {}", program, args.join(" ")),
reason: e.to_string(),
})?;
Ok(output.status.success())
}
/// Update agent to latest version
pub async fn update(
&self,
config: &InstallationConfig,
install_dir: Option<&Path>,
) -> Result<InstallResult, InstallationError> {
let installer = self.get_installer(&config.package_manager).ok_or_else(|| {
InstallationError::UnsupportedPackageManager(
config.package_manager.as_str().to_string(),
)
})?;
info!("Updating Agent via {}", installer.package_manager_name());
installer.update(config, install_dir).await
}
}
impl Default for AgentInstallationManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_command_availability() {
let manager = AgentInstallationManager::new();
// Test with a command that should exist on most systems
let ls_available = manager.is_command_available("ls").await;
assert!(ls_available);
// Test with a command that shouldn't exist
let fake_available = manager
.is_command_available("this_command_does_not_exist_12345")
.await;
assert!(!fake_available);
}
}

View File

@@ -0,0 +1,11 @@
//! Agent Installation Management
//!
//! This module provides automatic installation and validation for agents.
mod manager;
mod npm_installer;
mod traits;
pub use manager::{AgentInstallationManager, InstallResult, InstallationError};
pub use npm_installer::NpmInstaller;
pub use traits::AgentInstaller;

View File

@@ -0,0 +1,236 @@
//! NPM Package Installer
//!
//! Implements agent installation via npm.
use std::path::Path;
use tracing::{debug, info, warn};
use super::manager::{InstallResult, InstallationError};
use super::traits::AgentInstaller;
use crate::types::installation::InstallationConfig;
/// NPM Installer
///
/// Installs agents via npm global installation.
pub struct NpmInstaller {
/// Use pnpm instead of npm if available
prefer_pnpm: bool,
}
impl NpmInstaller {
/// Create a new NPM installer
pub fn new() -> Self {
Self { prefer_pnpm: false }
}
/// Create NPM installer that prefers pnpm
pub fn with_pnpm_preference() -> Self {
Self { prefer_pnpm: true }
}
/// Get the npm command to use (npm or pnpm)
async fn get_npm_command(&self) -> &'static str {
if self.prefer_pnpm && which::which("pnpm").is_ok() {
"pnpm"
} else if which::which("npm").is_ok() {
"npm"
} else {
"npm" // Default, will fail if not available
}
}
/// Parse version from npm output
fn parse_installed_version(&self, output: &str) -> Option<String> {
// npm list output format: "package@version"
for line in output.lines() {
if line.contains('@') {
if let Some(version) = line.split('@').last() {
return Some(version.trim().to_string());
}
}
}
None
}
}
impl Default for NpmInstaller {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl AgentInstaller for NpmInstaller {
async fn install(
&self,
config: &InstallationConfig,
_install_dir: Option<&Path>,
) -> Result<InstallResult, InstallationError> {
let npm_cmd = self.get_npm_command().await;
// Check if npm is available
if !self.command_exists(npm_cmd).await {
return Err(InstallationError::PackageManagerNotAvailable(
npm_cmd.to_string(),
));
}
let package_name = config.package_name.as_ref().ok_or_else(|| {
InstallationError::ConfigError("Package name is required".to_string())
})?;
// Build package spec with version
let package_spec = if let Some(version) = &config.version {
if version == "latest" {
package_name.clone()
} else {
format!("{}@{}", package_name, version)
}
} else {
package_name.clone()
};
info!(" running {} : {}", npm_cmd, package_spec);
// Run npm install -g
// 仅 nuwaxcode 使用官方源(--registry 参数优先级高于 .npmrc
let output = if package_name == "nuwaxcode" {
info!("🌐 nuwaxcode using registry: https://registry.npmjs.org/");
self.run_command(
npm_cmd,
&[
"install",
"-g",
&package_spec,
"--registry=https://registry.npmjs.org/",
],
)
.await?
} else {
self.run_command(npm_cmd, &["install", "-g", &package_spec])
.await?
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
debug!("npm install stdout: {}", stdout);
if !stderr.is_empty() {
debug!("npm install stderr: {}", stderr);
}
// Try to get installed version
let version = self.parse_installed_version(&stdout);
info!("Package installed: {}", package_spec);
Ok(InstallResult::success(
version,
format!("Successfully installed {}", package_spec),
))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
warn!("Installation failed: {}", stderr);
Err(InstallationError::InstallFailed {
package: package_spec,
reason: stderr.to_string(),
})
}
}
async fn validate(&self, config: &InstallationConfig) -> Result<bool, InstallationError> {
// Check if the package is globally installed
let npm_cmd = self.get_npm_command().await;
if !self.command_exists(npm_cmd).await {
return Ok(false);
}
let package_name = match &config.package_name {
Some(name) => name,
None => return Ok(false),
};
// Check global list
let output = self
.run_command(npm_cmd, &["list", "-g", "--depth=0", package_name])
.await?;
Ok(output.status.success())
}
async fn update(
&self,
config: &InstallationConfig,
_install_dir: Option<&Path>,
) -> Result<InstallResult, InstallationError> {
let npm_cmd = self.get_npm_command().await;
if !self.command_exists(npm_cmd).await {
return Err(InstallationError::PackageManagerNotAvailable(
npm_cmd.to_string(),
));
}
let package_name = config.package_name.as_ref().ok_or_else(|| {
InstallationError::ConfigError("Package name is required".to_string())
})?;
info!("Updating global package: {}", package_name);
// Run npm update -g
// 仅 nuwaxcode 使用官方源(--registry 参数优先级高于 .npmrc
let output = if package_name == "nuwaxcode" {
info!("🌐 nuwaxcode using registry: https://registry.npmjs.org/");
self.run_command(
npm_cmd,
&[
"update",
"-g",
package_name,
"--registry=https://registry.npmjs.org/",
],
)
.await?
} else {
self.run_command(npm_cmd, &["update", "-g", package_name])
.await?
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let version = self.parse_installed_version(&stdout);
info!("Update succeeded: {}", package_name);
Ok(InstallResult::success(
version,
format!("Successfully updated {}", package_name),
))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
warn!("Update failed: {}", stderr);
Err(InstallationError::InstallFailed {
package: package_name.clone(),
reason: stderr.to_string(),
})
}
}
fn package_manager_name(&self) -> &'static str {
"npm"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_npm_command_detection() {
let installer = NpmInstaller::new();
let cmd = installer.get_npm_command().await;
// Should return "npm" or "pnpm"
assert!(cmd == "npm" || cmd == "pnpm");
}
}

View File

@@ -0,0 +1,75 @@
//! Agent Installer Trait
//!
//! Defines the interface for agent installers.
use std::path::Path;
use super::manager::{InstallResult, InstallationError};
use crate::types::installation::InstallationConfig;
/// Agent installer trait
///
/// Defines the interface for installing, validating, and updating agents.
#[async_trait::async_trait]
pub trait AgentInstaller: Send + Sync {
/// Install the agent
///
/// # Arguments
/// * `config` - Installation configuration
/// * `install_dir` - Directory to install to (optional, for global installs)
///
/// # Returns
/// Installation result with details
async fn install(
&self,
config: &InstallationConfig,
install_dir: Option<&Path>,
) -> Result<InstallResult, InstallationError>;
/// Validate if the agent is installed correctly
///
/// # Arguments
/// * `config` - Installation configuration containing validate_command
///
/// # Returns
/// true if installed and working, false otherwise
async fn validate(&self, config: &InstallationConfig) -> Result<bool, InstallationError>;
/// Update the agent to latest version
///
/// # Arguments
/// * `config` - Installation configuration
/// * `install_dir` - Directory where agent is installed
///
/// # Returns
/// Installation result with new version info
async fn update(
&self,
config: &InstallationConfig,
install_dir: Option<&Path>,
) -> Result<InstallResult, InstallationError>;
/// Get the package manager name
fn package_manager_name(&self) -> &'static str;
/// Check if a command exists in PATH
async fn command_exists(&self, command: &str) -> bool {
which::which(command).is_ok()
}
/// Run a command and get output
async fn run_command(
&self,
command: &str,
args: &[&str],
) -> Result<std::process::Output, InstallationError> {
tokio::process::Command::new(command)
.args(args)
.output()
.await
.map_err(|e| InstallationError::CommandFailed {
command: format!("{} {}", command, args.join(" ")),
reason: e.to_string(),
})
}
}

View File

@@ -0,0 +1,33 @@
//! Agent Configuration Management
//!
//! This crate provides configuration management for agents in RCoder.
pub mod config;
pub mod installer;
pub mod types;
// Re-export main types
pub use config::prompt_assembler::PromptConfigAssembler;
pub use config::servers_config::AgentServersConfig;
pub use types::agent_config::AgentConfig;
pub use types::agent_spec::AgentSpec;
pub use types::error::{AgentConfigError, ConfigError, Result};
pub use types::installation::{InstallationConfig, PackageManager};
pub use types::mcp_config::{ContextServerConfig, McpServerConfig, McpServerSource};
pub use types::prompt_config::{SystemPromptConfig, UserPromptConfig};
pub use types::system_prompt::DEFAULT_SYSTEM_PROMPT;
#[allow(deprecated)]
pub use types::system_prompt::PromptBuilder;
// Installer exports
pub use installer::{
AgentInstallationManager, AgentInstaller, InstallResult, InstallationError, NpmInstaller,
};
// Default configuration exports
pub use config::default_agent_config::{
CLAUDE_CODE_ACP_AGENT_ID, default_agent_servers, default_agent_servers_for_service,
default_context_servers, default_context_servers_for_service, get_default_agent,
get_default_agent_for_service, get_default_config_by_service_type, get_default_context_server,
get_default_context_server_for_service,
};

View File

@@ -0,0 +1,115 @@
//! Agent configuration structure.
use serde::{Deserialize, Serialize};
use shared_types::ModelProviderConfig;
use std::collections::HashMap;
/// Agent configuration structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
/// Agent identifier
pub agent_id: String,
/// Command to execute
pub command: String,
/// Command arguments
#[serde(default)]
pub args: Vec<String>,
/// Environment variables
#[serde(default)]
pub env: HashMap<String, String>,
/// Installation configuration
pub installation: super::installation::InstallationConfig,
/// System prompt configuration
#[serde(default)]
pub system_prompt: Option<super::prompt_config::SystemPromptConfig>,
/// User prompt configuration
#[serde(default)]
pub user_prompt: Option<super::prompt_config::UserPromptConfig>,
/// Model provider configuration
#[serde(default)]
pub model_provider: Option<ModelProviderConfig>,
/// Whether the agent is enabled
#[serde(default = "default_enabled")]
pub enabled: bool,
/// Metadata
#[serde(default)]
pub metadata: HashMap<String, String>,
}
/// Default enabled value
fn default_enabled() -> bool {
true
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
agent_id: String::new(),
command: String::new(),
args: Vec::new(),
env: HashMap::new(),
installation: super::installation::InstallationConfig::default(),
system_prompt: None,
user_prompt: None,
model_provider: None,
enabled: true,
metadata: HashMap::new(),
}
}
}
impl AgentConfig {
/// Create a new agent config
pub fn new(agent_id: String) -> Self {
Self {
agent_id,
..Default::default()
}
}
/// Get an environment variable
pub fn get_env(&self, key: &str) -> Option<&String> {
self.env.get(key)
}
/// Set an environment variable
pub fn set_env(&mut self, key: String, value: String) {
self.env.insert(key, value);
}
/// Check if agent is enabled
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Get model provider configuration
pub fn get_model_provider(&self) -> Option<&ModelProviderConfig> {
self.model_provider.as_ref()
}
/// Set model provider configuration
pub fn set_model_provider(&mut self, provider: ModelProviderConfig) {
self.model_provider = Some(provider);
}
/// Get the default model name if configured
pub fn get_default_model(&self) -> Option<&str> {
self.model_provider
.as_ref()
.map(|p| p.default_model.as_str())
}
/// Get the model provider name if configured
pub fn get_model_provider_name(&self) -> Option<&str> {
self.model_provider.as_ref().map(|p| p.name.as_str())
}
}

View File

@@ -0,0 +1,68 @@
//! Agent specification structure.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Agent specification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSpec {
/// Agent identifier (matches the key in config file)
pub agent_id: String,
/// Command to execute
pub command: String,
/// Command arguments
pub args: Vec<String>,
/// Environment variables
pub env: HashMap<String, String>,
/// Installation configuration
pub installation: super::installation::InstallationConfig,
/// System prompt configuration
pub system_prompt: Option<super::prompt_config::SystemPromptConfig>,
/// User prompt configuration
pub user_prompt: Option<super::prompt_config::UserPromptConfig>,
/// Whether the agent is enabled
pub enabled: bool,
/// Metadata
pub metadata: HashMap<String, String>,
}
impl AgentSpec {
/// Create a new agent spec
pub fn new(agent_id: String) -> Self {
Self {
agent_id,
command: String::new(),
args: Vec::new(),
env: HashMap::new(),
installation: super::installation::InstallationConfig::default(),
system_prompt: None,
user_prompt: None,
enabled: true,
metadata: HashMap::new(),
}
}
/// Convert to AgentConfig
pub fn to_agent_config(&self) -> super::agent_config::AgentConfig {
super::agent_config::AgentConfig {
agent_id: self.agent_id.clone(),
command: self.command.clone(),
args: self.args.clone(),
env: self.env.clone(),
installation: self.installation.clone(),
system_prompt: self.system_prompt.clone(),
user_prompt: self.user_prompt.clone(),
model_provider: None,
enabled: self.enabled,
metadata: self.metadata.clone(),
}
}
}

View File

@@ -0,0 +1,168 @@
//! Error types for agent configuration.
use thiserror::Error;
/// Agent configuration error
#[derive(Error, Debug)]
pub enum AgentConfigError {
/// Configuration error
#[error("configuration error: {0}")]
Configuration(#[from] ConfigError),
/// I/O error
#[error("I/O error: {0}")]
Io(#[from] IoError),
/// Installation error
#[error("installation error: {0}")]
Installation(#[from] InstallationError),
/// Validation error
#[error("validation error: {0}")]
Validation(#[from] ValidationError),
/// Other error
#[error("other error: {0}")]
Other(String),
}
impl From<serde_json::Error> for AgentConfigError {
fn from(e: serde_json::Error) -> Self {
Self::Configuration(ConfigError::Serialization(e.to_string()))
}
}
impl From<std::path::PathBuf> for AgentConfigError {
fn from(path: std::path::PathBuf) -> Self {
Self::Io(IoError::PathError(path))
}
}
/// Configuration error
#[derive(Error, Debug)]
pub enum ConfigError {
/// Serialization error
#[error("serialization error: {0}")]
Serialization(String),
/// Deserialization error
#[error("deserialization error: {0}")]
Deserialization(String),
/// Missing required field
#[error("missing required field: {0}")]
MissingField(String),
/// Invalid value
#[error("invalid value: {0}")]
InvalidValue(String),
/// File not found
#[error("file not found: {0}")]
FileNotFound(String),
/// Permission denied
#[error("permission denied: {0}")]
PermissionDenied(String),
/// Read error
#[error("read error: {0}")]
Read(String),
}
impl ConfigError {
/// Create a missing field error
pub fn missing_field(field: impl Into<String>) -> Self {
Self::MissingField(field.into())
}
/// Create an invalid value error
pub fn invalid_value(msg: impl Into<String>) -> Self {
Self::InvalidValue(msg.into())
}
/// Create a file not found error
pub fn file_not_found(path: impl std::fmt::Display) -> Self {
Self::FileNotFound(path.to_string())
}
/// Create a read error
pub fn read(path: impl std::fmt::Display, source: impl std::fmt::Display) -> Self {
Self::Read(format!("{}: {}", path, source))
}
}
/// I/O error
#[derive(Error, Debug)]
pub enum IoError {
/// Path error
#[error("path error: {0:?}")]
PathError(std::path::PathBuf),
/// Read error
#[error("read error: {0}")]
Read(String),
/// Write error
#[error("write error: {0}")]
Write(String),
/// Create error
#[error("create error: {0}")]
Create(String),
/// Delete error
#[error("delete error: {0}")]
Delete(String),
}
impl IoError {
/// Create a read error
pub fn read(path: impl Into<String>, source: impl ToString) -> Self {
Self::Read(format!("{}: {}", path.into(), source.to_string()))
}
/// Create a write error
pub fn write(path: impl Into<String>, source: impl ToString) -> Self {
Self::Write(format!("{}: {}", path.into(), source.to_string()))
}
}
/// Installation error
#[derive(Error, Debug)]
pub enum InstallationError {
/// Package not found
#[error("package not found: {0}")]
PackageNotFound(String),
/// Installation failed
#[error("installation failed: {0}")]
InstallationFailed(String),
/// Validation failed
#[error("validation failed: {0}")]
ValidationFailed(String),
/// Permission denied
#[error("permission denied: {0}")]
PermissionDenied(String),
}
/// Validation error
#[derive(Error, Debug)]
pub enum ValidationError {
/// Invalid configuration
#[error("invalid configuration: {0}")]
InvalidConfiguration(String),
/// Missing dependency
#[error("missing dependency: {0}")]
MissingDependency(String),
/// Constraint violation
#[error("constraint violation: {0}")]
ConstraintViolation(String),
}
// Convenience aliases
pub type Result<T> = std::result::Result<T, AgentConfigError>;

View File

@@ -0,0 +1,122 @@
//! Installation configuration structures.
use serde::{Deserialize, Serialize};
/// Package manager type
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PackageManager {
/// npm package manager
Npm,
/// Local binary
Local,
/// Custom package manager
Custom(String),
}
/// Installation configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstallationConfig {
/// Package manager
pub package_manager: PackageManager,
/// Package name
#[serde(default)]
pub package_name: Option<String>,
/// Version constraint
#[serde(default)]
pub version: Option<String>,
/// Installation source (for custom sources)
#[serde(default)]
pub source: Option<String>,
/// Validation command
#[serde(default)]
pub validate_command: Option<Vec<String>>,
/// Whether to auto-update
#[serde(default)]
pub auto_update: bool,
}
impl Default for InstallationConfig {
fn default() -> Self {
Self {
package_manager: PackageManager::Npm,
package_name: None,
version: None,
source: None,
validate_command: None,
auto_update: false,
}
}
}
impl InstallationConfig {
/// Create npm installation config
pub fn npm(package_name: String) -> Self {
Self {
package_manager: PackageManager::Npm,
package_name: Some(package_name),
version: Some("latest".to_string()),
source: None,
validate_command: None,
auto_update: false,
}
}
/// Create local installation config
pub fn local(command: String) -> Self {
Self {
package_manager: PackageManager::Local,
package_name: Some(command),
version: None,
source: None,
validate_command: None,
auto_update: false,
}
}
/// Create custom installation config
pub fn custom(package_manager: String, source: String) -> Self {
Self {
package_manager: PackageManager::Custom(package_manager),
package_name: None,
version: None,
source: Some(source),
validate_command: None,
auto_update: false,
}
}
/// Set version
pub fn with_version(mut self, version: String) -> Self {
self.version = Some(version);
self
}
/// Set validation command
pub fn with_validation(mut self, command: Vec<String>) -> Self {
self.validate_command = Some(command);
self
}
/// Enable auto-update
pub fn with_auto_update(mut self) -> Self {
self.auto_update = true;
self
}
}
impl PackageManager {
/// Get the manager name as string
pub fn as_str(&self) -> &str {
match self {
PackageManager::Npm => "npm",
PackageManager::Local => "local",
PackageManager::Custom(name) => name,
}
}
}

View File

@@ -0,0 +1,123 @@
//! MCP server configuration structures.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
/// MCP server source type
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum McpServerSource {
/// Custom command (npm, uvx, bun, etc.)
Custom,
/// Local executable file
Local,
}
/// MCP server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
/// Server name
pub name: String,
/// Server source type
pub source: McpServerSource,
/// Whether enabled
pub enabled: bool,
/// Command to execute (for custom/local sources)
pub command: Option<String>,
/// Command arguments
pub args: Option<Vec<String>>,
/// Environment variables
pub env: Option<HashMap<String, String>>,
/// Connection timeout
pub timeout: Option<Duration>,
}
/// Context server configuration (simplified format for config files)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextServerConfig {
/// Server source type
pub source: String,
/// Whether enabled
pub enabled: bool,
/// Command to execute
pub command: Option<String>,
/// Command arguments
pub args: Option<Vec<String>>,
/// Environment variables
pub env: Option<HashMap<String, String>>,
}
impl McpServerConfig {
/// Create a new MCP server config
pub fn new(name: String, source: McpServerSource) -> Self {
Self {
name,
source,
enabled: true,
command: None,
args: None,
env: None,
timeout: Some(Duration::from_secs(30)),
}
}
/// Create a custom MCP server
pub fn custom(name: String, command: String) -> Self {
Self {
name,
source: McpServerSource::Custom,
enabled: true,
command: Some(command),
args: None,
env: None,
timeout: Some(Duration::from_secs(30)),
}
}
/// Create a local MCP server
pub fn local(name: String, command: String) -> Self {
Self {
name,
source: McpServerSource::Local,
enabled: true,
command: Some(command),
args: None,
env: None,
timeout: Some(Duration::from_secs(30)),
}
}
/// Check if server is enabled
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
impl ContextServerConfig {
/// Create a new context server config
pub fn new(source: String) -> Self {
Self {
source,
enabled: true,
command: None,
args: None,
env: None,
}
}
/// Check if server is enabled
pub fn is_enabled(&self) -> bool {
self.enabled
}
}

View File

@@ -0,0 +1,19 @@
//! Core type definitions for agent configuration.
pub mod agent_config;
pub mod agent_spec;
pub mod error;
pub mod installation;
pub mod mcp_config;
pub mod prompt_config;
pub mod system_prompt;
pub use agent_config::*;
pub use agent_spec::*;
pub use error::*;
pub use installation::*;
pub use mcp_config::*;
pub use prompt_config::*;
pub use system_prompt::DEFAULT_SYSTEM_PROMPT;
#[allow(deprecated)]
pub use system_prompt::PromptBuilder;

View File

@@ -0,0 +1,240 @@
//! Prompt configuration structures.
use serde::{Deserialize, Serialize};
use super::system_prompt::DEFAULT_SYSTEM_PROMPT;
/// 系统提示词来源
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SystemPromptSource {
/// 使用编译时嵌入的默认提示词
#[default]
Embedded,
/// 使用自定义模板内容
Custom,
}
/// System prompt configuration
///
/// 支持两种模式:
/// 1. `source: "embedded"` - 使用编译时嵌入的默认提示词(忽略 template 字段)
/// 2. `source: "custom"` - 使用 template 字段的自定义内容
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemPromptConfig {
/// 提示词来源
/// - "embedded": 使用编译时嵌入的默认提示词
/// - "custom": 使用 template 字段的自定义内容
#[serde(default)]
pub source: SystemPromptSource,
/// Template content (仅当 source = "custom" 时使用)
#[serde(default)]
pub template: String,
/// Whether enabled (false 时不传递系统提示词)
#[serde(default = "default_enabled")]
pub enabled: bool,
}
/// User prompt configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPromptConfig {
/// Template content with {user_prompt} placeholder
pub template: String,
/// Whether enabled
#[serde(default = "default_enabled")]
pub enabled: bool,
}
/// Default enabled value
fn default_enabled() -> bool {
true
}
impl Default for SystemPromptConfig {
fn default() -> Self {
Self {
source: SystemPromptSource::Embedded,
template: String::new(),
enabled: true,
}
}
}
impl SystemPromptConfig {
/// 创建使用嵌入默认提示词的配置
pub fn embedded() -> Self {
Self {
source: SystemPromptSource::Embedded,
template: String::new(),
enabled: true,
}
}
/// 创建使用自定义模板的配置
pub fn custom(template: String) -> Self {
Self {
source: SystemPromptSource::Custom,
template,
enabled: true,
}
}
/// 创建禁用的配置(不传递系统提示词)
pub fn disabled() -> Self {
Self {
source: SystemPromptSource::Embedded,
template: String::new(),
enabled: false,
}
}
/// 获取系统提示词内容
///
/// - 如果 enabled = false返回 None
/// - 如果 source = "embedded",返回编译时嵌入的默认提示词
/// - 如果 source = "custom",返回 template 字段的内容
pub fn get_prompt(&self) -> Option<&str> {
if !self.enabled {
return None;
}
match self.source {
SystemPromptSource::Embedded => Some(DEFAULT_SYSTEM_PROMPT),
SystemPromptSource::Custom => {
if self.template.is_empty() {
// custom 模式但 template 为空,回退到默认
Some(DEFAULT_SYSTEM_PROMPT)
} else {
Some(&self.template)
}
}
}
}
/// 获取系统提示词内容(兼容旧接口,始终返回有效提示词)
///
/// 如果 enabled = false仍返回默认提示词用于需要始终有提示词的场景
pub fn get_prompt_or_default(&self) -> &str {
match self.source {
SystemPromptSource::Embedded => DEFAULT_SYSTEM_PROMPT,
SystemPromptSource::Custom => {
if self.template.is_empty() {
DEFAULT_SYSTEM_PROMPT
} else {
&self.template
}
}
}
}
/// 检查是否使用自定义模板
pub fn is_custom(&self) -> bool {
self.source == SystemPromptSource::Custom && !self.template.is_empty()
}
/// 检查是否使用嵌入的默认提示词
pub fn is_embedded(&self) -> bool {
self.source == SystemPromptSource::Embedded
}
}
impl UserPromptConfig {
/// Create a new user prompt config
pub fn new(template: String) -> Self {
Self {
template,
enabled: true,
}
}
/// Create disabled config
pub fn disabled() -> Self {
Self {
template: String::new(),
enabled: false,
}
}
/// Apply user prompt template
pub fn apply(&self, user_input: &str) -> String {
if self.enabled {
self.template.replace("{user_prompt}", user_input)
} else {
user_input.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedded_source() {
let config = SystemPromptConfig::embedded();
assert!(config.is_embedded());
assert!(!config.is_custom());
assert!(config.get_prompt().is_some());
assert!(
config
.get_prompt()
.unwrap()
.contains("<SYSTEM_INSTRUCTIONS>")
);
}
#[test]
fn test_custom_source() {
let config = SystemPromptConfig::custom("Custom prompt".to_string());
assert!(config.is_custom());
assert!(!config.is_embedded());
assert_eq!(config.get_prompt(), Some("Custom prompt"));
}
#[test]
fn test_disabled_config() {
let config = SystemPromptConfig::disabled();
assert!(config.get_prompt().is_none());
// get_prompt_or_default 仍返回默认值
assert!(
config
.get_prompt_or_default()
.contains("<SYSTEM_INSTRUCTIONS>")
);
}
#[test]
fn test_custom_empty_template_fallback() {
let config = SystemPromptConfig {
source: SystemPromptSource::Custom,
template: String::new(),
enabled: true,
};
// 空模板时回退到默认
assert!(
config
.get_prompt()
.unwrap()
.contains("<SYSTEM_INSTRUCTIONS>")
);
}
#[test]
fn test_json_deserialization() {
let json = r#"{"source": "embedded", "template": "", "enabled": true}"#;
let config: SystemPromptConfig = serde_json::from_str(json).unwrap();
assert!(config.is_embedded());
assert!(config.enabled);
}
#[test]
fn test_json_deserialization_custom() {
let json = r#"{"source": "custom", "template": "My prompt", "enabled": true}"#;
let config: SystemPromptConfig = serde_json::from_str(json).unwrap();
assert!(config.is_custom());
assert_eq!(config.get_prompt(), Some("My prompt"));
}
}

View File

@@ -0,0 +1,107 @@
//! System prompt configuration types
//!
//! This module provides compile-time embedded default system prompt constants and related helper functions.
//! Default system prompt is embedded at compile time from external files, with runtime override support via configuration.
/// Compile-time embedded default system prompt
pub const DEFAULT_SYSTEM_PROMPT: &str = include_str!("../../configs/prompts/frontend_expert.txt");
/// Prompt builder (kept for compatibility, recommend using SystemPromptConfig::get_prompt() directly)
#[derive(Debug, Clone)]
pub struct PromptBuilder;
impl PromptBuilder {
/// Build user prompt (without system prompt)
pub fn build_user_prompt(user_prompt: &str) -> String {
user_prompt.to_string()
}
/// Build user prompt with data sources
pub fn build_user_prompt_with_data_sources(
user_prompt: &str,
data_sources: &[String],
) -> String {
if data_sources.is_empty() {
return user_prompt.to_string();
}
let data_sources_section = format_data_sources(data_sources);
format!(
"{}\n\n\
<DATA_SOURCES>\n\
The following are available data source information, including backend API endpoints, database connections, and other external data sources.\n\
When developing frontend applications, you can use these data sources to fetch real data, such as querying Bitcoin transaction volumes, stock prices, weather information, etc.\n\
Please use these data sources reasonably according to development needs, and ensure the frontend application can correctly call the relevant interfaces.\n\
Use Axios client or Fetch API for API calls, or according to the interface calling method of the current framework.\n\n\
{}\n\
</DATA_SOURCES>",
user_prompt, data_sources_section
)
}
}
/// Format data source information into readable text
fn format_data_sources(data_sources: &[String]) -> String {
if data_sources.is_empty() {
return "No data sources".to_string();
}
let mut formatted = String::new();
for (index, data_source) in data_sources.iter().enumerate() {
formatted.push_str(&format!("Data source {}:\n", index + 1));
// 尝试解析 JSON 字符串并格式化
match serde_json::from_str::<serde_json::Value>(data_source) {
Ok(json_value) => {
// 成功解析,格式化为易读的 JSON
match serde_json::to_string_pretty(&json_value) {
Ok(pretty_json) => {
formatted.push_str(&pretty_json);
}
Err(_) => {
// 格式化失败,使用原始字符串
formatted.push_str(data_source);
}
}
}
Err(_) => {
// 不是有效的 JSON直接使用原始字符串
formatted.push_str(data_source);
}
}
formatted.push('\n');
}
formatted
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_system_prompt_loaded() {
// 验证默认提示词被正确加载
assert!(!DEFAULT_SYSTEM_PROMPT.is_empty());
assert!(DEFAULT_SYSTEM_PROMPT.contains("<SYSTEM_INSTRUCTIONS>"));
assert!(DEFAULT_SYSTEM_PROMPT.contains("</SYSTEM_INSTRUCTIONS>"));
}
#[test]
fn test_format_data_sources() {
let data_sources = vec![r#"{"api": "https://api.example.com"}"#.to_string()];
let formatted = format_data_sources(&data_sources);
assert!(formatted.contains("Data source 1"));
assert!(formatted.contains("api.example.com"));
}
#[test]
fn test_format_empty_data_sources() {
let data_sources: Vec<String> = vec![];
let formatted = format_data_sources(&data_sources);
assert_eq!(formatted, "No data sources");
}
}