添加qiming-rcoder模块
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
13
qiming-rcoder/crates/agent_config/src/config/mod.rs
Normal file
13
qiming-rcoder/crates/agent_config/src/config/mod.rs
Normal 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::*;
|
||||
384
qiming-rcoder/crates/agent_config/src/config/prompt_assembler.rs
Normal file
384
qiming-rcoder/crates/agent_config/src/config/prompt_assembler.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
226
qiming-rcoder/crates/agent_config/src/config/servers_config.rs
Normal file
226
qiming-rcoder/crates/agent_config/src/config/servers_config.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
305
qiming-rcoder/crates/agent_config/src/installer/manager.rs
Normal file
305
qiming-rcoder/crates/agent_config/src/installer/manager.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
qiming-rcoder/crates/agent_config/src/installer/mod.rs
Normal file
11
qiming-rcoder/crates/agent_config/src/installer/mod.rs
Normal 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;
|
||||
236
qiming-rcoder/crates/agent_config/src/installer/npm_installer.rs
Normal file
236
qiming-rcoder/crates/agent_config/src/installer/npm_installer.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
75
qiming-rcoder/crates/agent_config/src/installer/traits.rs
Normal file
75
qiming-rcoder/crates/agent_config/src/installer/traits.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
33
qiming-rcoder/crates/agent_config/src/lib.rs
Normal file
33
qiming-rcoder/crates/agent_config/src/lib.rs
Normal 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,
|
||||
};
|
||||
115
qiming-rcoder/crates/agent_config/src/types/agent_config.rs
Normal file
115
qiming-rcoder/crates/agent_config/src/types/agent_config.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
68
qiming-rcoder/crates/agent_config/src/types/agent_spec.rs
Normal file
68
qiming-rcoder/crates/agent_config/src/types/agent_spec.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
168
qiming-rcoder/crates/agent_config/src/types/error.rs
Normal file
168
qiming-rcoder/crates/agent_config/src/types/error.rs
Normal 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>;
|
||||
122
qiming-rcoder/crates/agent_config/src/types/installation.rs
Normal file
122
qiming-rcoder/crates/agent_config/src/types/installation.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
123
qiming-rcoder/crates/agent_config/src/types/mcp_config.rs
Normal file
123
qiming-rcoder/crates/agent_config/src/types/mcp_config.rs
Normal 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
|
||||
}
|
||||
}
|
||||
19
qiming-rcoder/crates/agent_config/src/types/mod.rs
Normal file
19
qiming-rcoder/crates/agent_config/src/types/mod.rs
Normal 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;
|
||||
240
qiming-rcoder/crates/agent_config/src/types/prompt_config.rs
Normal file
240
qiming-rcoder/crates/agent_config/src/types/prompt_config.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
107
qiming-rcoder/crates/agent_config/src/types/system_prompt.rs
Normal file
107
qiming-rcoder/crates/agent_config/src/types/system_prompt.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user