mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-25 07:20:41 +08:00
feat: implement Hermes config module and commands (Phase 3)
Add hermes_config.rs (~1190 lines) with YAML section-level replacement that preserves comments and formatting in unmanaged sections: - Type definitions: HermesModelConfig, HermesAgentConfig, HermesEnvConfig - YAML section finder (find_yaml_section_range) with column-0 key detection - Provider CRUD on custom_providers array (indexed by name field) - Model/Agent config get/set via yaml<->json conversion - .env dotenv read/write preserving comments and line ordering - Health check, backup with rotation, write lock (OnceLock<Mutex>) - MCP section access stubs for Phase 4 - 19 unit tests Add commands/hermes.rs with 10 Tauri commands registered in lib.rs. Replace all Hermes TODO stubs in services/provider/live.rs with real implementations (import, remove, write-to-live, read-live-settings).
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
use tauri::State;
|
||||
|
||||
use crate::hermes_config;
|
||||
use crate::store::AppState;
|
||||
|
||||
// ============================================================================
|
||||
// Hermes Provider Commands
|
||||
// ============================================================================
|
||||
|
||||
/// Import providers from Hermes live config to database.
|
||||
///
|
||||
/// Hermes uses additive mode — users may already have providers
|
||||
/// configured in config.yaml.
|
||||
#[tauri::command]
|
||||
pub fn import_hermes_providers_from_live(state: State<'_, AppState>) -> Result<usize, String> {
|
||||
crate::services::provider::import_hermes_providers_from_live(state.inner())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get provider names in the Hermes live config.
|
||||
#[tauri::command]
|
||||
pub fn get_hermes_live_provider_ids() -> Result<Vec<String>, String> {
|
||||
hermes_config::get_providers()
|
||||
.map(|providers| providers.keys().cloned().collect())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get a single Hermes provider fragment from live config.
|
||||
#[tauri::command]
|
||||
pub fn get_hermes_live_provider(
|
||||
#[allow(non_snake_case)] providerId: String,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
hermes_config::get_provider(&providerId).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Scan config.yaml for known configuration hazards.
|
||||
#[tauri::command]
|
||||
pub fn scan_hermes_config_health() -> Result<Vec<hermes_config::HermesHealthWarning>, String> {
|
||||
hermes_config::scan_hermes_config_health().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Model Configuration Commands
|
||||
// ============================================================================
|
||||
|
||||
/// Get Hermes model config (model section of config.yaml)
|
||||
#[tauri::command]
|
||||
pub fn get_hermes_model_config() -> Result<Option<hermes_config::HermesModelConfig>, String> {
|
||||
hermes_config::get_model_config().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Set Hermes model config (model section of config.yaml)
|
||||
#[tauri::command]
|
||||
pub fn set_hermes_model_config(
|
||||
model: hermes_config::HermesModelConfig,
|
||||
) -> Result<hermes_config::HermesWriteOutcome, String> {
|
||||
hermes_config::set_model_config(&model).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Agent Configuration Commands
|
||||
// ============================================================================
|
||||
|
||||
/// Get Hermes agent config (agent section of config.yaml)
|
||||
#[tauri::command]
|
||||
pub fn get_hermes_agent_config() -> Result<Option<hermes_config::HermesAgentConfig>, String> {
|
||||
hermes_config::get_agent_config().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Set Hermes agent config (agent section of config.yaml)
|
||||
#[tauri::command]
|
||||
pub fn set_hermes_agent_config(
|
||||
agent: hermes_config::HermesAgentConfig,
|
||||
) -> Result<hermes_config::HermesWriteOutcome, String> {
|
||||
hermes_config::set_agent_config(&agent).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Env Configuration Commands
|
||||
// ============================================================================
|
||||
|
||||
/// Get Hermes env config (.env file)
|
||||
#[tauri::command]
|
||||
pub fn get_hermes_env() -> Result<hermes_config::HermesEnvConfig, String> {
|
||||
hermes_config::read_env().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Set Hermes env config (.env file)
|
||||
#[tauri::command]
|
||||
pub fn set_hermes_env(
|
||||
env: hermes_config::HermesEnvConfig,
|
||||
) -> Result<hermes_config::HermesWriteOutcome, String> {
|
||||
hermes_config::write_env(&env).map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod deeplink;
|
||||
mod env;
|
||||
mod failover;
|
||||
mod global_proxy;
|
||||
mod hermes;
|
||||
mod import_export;
|
||||
mod mcp;
|
||||
mod misc;
|
||||
@@ -42,6 +43,7 @@ pub use deeplink::*;
|
||||
pub use env::*;
|
||||
pub use failover::*;
|
||||
pub use global_proxy::*;
|
||||
pub use hermes::*;
|
||||
pub use import_export::*;
|
||||
pub use mcp::*;
|
||||
pub use misc::*;
|
||||
|
||||
+1179
-2
File diff suppressed because it is too large
Load Diff
@@ -541,6 +541,13 @@ pub fn run() {
|
||||
Ok(_) => log::debug!("○ No new OpenClaw providers to import"),
|
||||
Err(e) => log::warn!("✗ Failed to import OpenClaw providers: {e}"),
|
||||
}
|
||||
match crate::services::provider::import_hermes_providers_from_live(&app_state) {
|
||||
Ok(count) if count > 0 => {
|
||||
log::info!("✓ Imported {count} Hermes provider(s) from live config");
|
||||
}
|
||||
Ok(_) => log::debug!("○ No new Hermes providers to import"),
|
||||
Err(e) => log::warn!("✗ Failed to import Hermes providers: {e}"),
|
||||
}
|
||||
|
||||
// 2. OMO 配置导入(当数据库中无 OMO provider 时,从本地文件导入)
|
||||
{
|
||||
@@ -1236,6 +1243,17 @@ pub fn run() {
|
||||
commands::set_openclaw_env,
|
||||
commands::get_openclaw_tools,
|
||||
commands::set_openclaw_tools,
|
||||
// Hermes specific
|
||||
commands::import_hermes_providers_from_live,
|
||||
commands::get_hermes_live_provider_ids,
|
||||
commands::get_hermes_live_provider,
|
||||
commands::scan_hermes_config_health,
|
||||
commands::get_hermes_model_config,
|
||||
commands::set_hermes_model_config,
|
||||
commands::get_hermes_agent_config,
|
||||
commands::set_hermes_agent_config,
|
||||
commands::get_hermes_env,
|
||||
commands::set_hermes_env,
|
||||
// Global upstream proxy
|
||||
commands::get_global_proxy_url,
|
||||
commands::set_global_proxy_url,
|
||||
|
||||
@@ -42,10 +42,8 @@ pub(crate) fn provider_exists_in_live_config(
|
||||
.map(|providers| providers.contains_key(provider_id)),
|
||||
AppType::OpenClaw => crate::openclaw_config::get_providers()
|
||||
.map(|providers| providers.contains_key(provider_id)),
|
||||
AppType::Hermes => {
|
||||
// TODO: hermes_config module not yet implemented
|
||||
Ok(false)
|
||||
}
|
||||
AppType::Hermes => crate::hermes_config::get_providers()
|
||||
.map(|providers| providers.contains_key(provider_id)),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
@@ -795,11 +793,8 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
}
|
||||
}
|
||||
AppType::Hermes => {
|
||||
// TODO: hermes_config module not yet implemented
|
||||
log::debug!(
|
||||
"Hermes provider '{}' write to live config not yet implemented",
|
||||
provider.id
|
||||
);
|
||||
crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())?;
|
||||
log::debug!("Hermes provider '{}' written to live config", provider.id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1005,8 +1000,9 @@ pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
|
||||
"Hermes configuration file not found",
|
||||
));
|
||||
}
|
||||
// Return empty object until hermes_config is implemented
|
||||
Ok(json!({}))
|
||||
let yaml_config = crate::hermes_config::read_hermes_config()?;
|
||||
let config = crate::hermes_config::yaml_to_json(&yaml_config)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1339,6 +1335,76 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, Ap
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
/// Import all providers from Hermes live config to database
|
||||
///
|
||||
/// This imports existing providers from ~/.hermes/config.yaml
|
||||
/// into the CC Switch database. Each provider found will be added to the
|
||||
/// database with is_current set to false.
|
||||
pub fn import_hermes_providers_from_live(state: &AppState) -> Result<usize, AppError> {
|
||||
use crate::hermes_config;
|
||||
|
||||
let providers = hermes_config::get_providers()?;
|
||||
if providers.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut imported = 0;
|
||||
let existing_ids = state.db.get_provider_ids("hermes")?;
|
||||
|
||||
for (name, config) in providers {
|
||||
// Validate: skip entries with empty name
|
||||
if name.trim().is_empty() {
|
||||
log::warn!("Skipping Hermes provider with empty name");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already exists in database
|
||||
if existing_ids.contains(&name) {
|
||||
log::debug!("Hermes provider '{name}' already exists in database, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create provider
|
||||
let mut provider = Provider::with_id(name.clone(), name.clone(), config, None);
|
||||
provider.meta = Some(crate::provider::ProviderMeta {
|
||||
live_config_managed: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Save to database
|
||||
if let Err(e) = state.db.save_provider("hermes", &provider) {
|
||||
log::warn!("Failed to import Hermes provider '{name}': {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
imported += 1;
|
||||
log::info!("Imported Hermes provider '{name}' from live config");
|
||||
}
|
||||
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
/// Remove a Hermes provider from live config
|
||||
///
|
||||
/// This removes a specific provider from ~/.hermes/config.yaml
|
||||
/// without affecting other providers in the file.
|
||||
pub fn remove_hermes_provider_from_live(provider_id: &str) -> Result<(), AppError> {
|
||||
use crate::hermes_config;
|
||||
|
||||
// Check if Hermes config directory exists
|
||||
if !hermes_config::get_hermes_dir().exists() {
|
||||
log::debug!(
|
||||
"Hermes config directory doesn't exist, skipping removal of '{provider_id}'"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
hermes_config::remove_provider(provider_id)?;
|
||||
log::info!("Hermes provider '{provider_id}' removed from live config");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an OpenClaw provider from live config
|
||||
///
|
||||
/// This removes a specific provider from ~/.openclaw/openclaw.json
|
||||
|
||||
@@ -21,8 +21,9 @@ use crate::store::AppState;
|
||||
|
||||
// Re-export sub-module functions for external access
|
||||
pub use live::{
|
||||
import_default_config, import_openclaw_providers_from_live,
|
||||
import_opencode_providers_from_live, read_live_settings, sync_current_to_live,
|
||||
import_default_config, import_hermes_providers_from_live,
|
||||
import_openclaw_providers_from_live, import_opencode_providers_from_live, read_live_settings,
|
||||
sync_current_to_live,
|
||||
};
|
||||
|
||||
// Internal re-exports (pub(crate))
|
||||
@@ -35,7 +36,8 @@ pub(crate) use live::{
|
||||
|
||||
// Internal re-exports
|
||||
use live::{
|
||||
remove_openclaw_provider_from_live, remove_opencode_provider_from_live, write_gemini_live,
|
||||
remove_hermes_provider_from_live, remove_openclaw_provider_from_live,
|
||||
remove_opencode_provider_from_live, write_gemini_live,
|
||||
};
|
||||
use usage::validate_usage_script;
|
||||
|
||||
@@ -1282,12 +1284,7 @@ impl ProviderService {
|
||||
match app_type {
|
||||
AppType::OpenCode => remove_opencode_provider_from_live(id)?,
|
||||
AppType::OpenClaw => remove_openclaw_provider_from_live(id)?,
|
||||
AppType::Hermes => {
|
||||
// TODO: hermes_config module not yet implemented
|
||||
log::debug!(
|
||||
"Hermes provider '{id}' removal from live config not yet implemented"
|
||||
);
|
||||
}
|
||||
AppType::Hermes => remove_hermes_provider_from_live(id)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1351,8 +1348,7 @@ impl ProviderService {
|
||||
remove_openclaw_provider_from_live(id)?;
|
||||
}
|
||||
AppType::Hermes => {
|
||||
// TODO: hermes_config module not yet implemented
|
||||
log::debug!("Hermes provider '{id}' removal from live config not yet implemented");
|
||||
remove_hermes_provider_from_live(id)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::Message(format!(
|
||||
@@ -1542,10 +1538,7 @@ impl ProviderService {
|
||||
let rollback_result = match app_type {
|
||||
AppType::OpenCode => remove_opencode_provider_from_live(&provider.id),
|
||||
AppType::OpenClaw => remove_openclaw_provider_from_live(&provider.id),
|
||||
AppType::Hermes => {
|
||||
// TODO: hermes_config module not yet implemented
|
||||
Ok(())
|
||||
}
|
||||
AppType::Hermes => remove_hermes_provider_from_live(&provider.id),
|
||||
_ => Ok(()),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user