feat: add Hermes Agent as 6th supported app type (Phase 1)

Register AppType::Hermes across the entire Rust backend:
- Add Hermes variant to AppType enum with additive mode and MCP support
- Add hermes field to McpApps, SkillApps, CommonConfigSnippets, and all
  per-app structs (McpRoot, PromptRoot, VisibleApps, AppSettings)
- Create minimal hermes_config.rs with get_hermes_dir() respecting
  settings override, matching the pattern of other app config modules
- Update all match arms in commands, services, deeplink, proxy, mcp,
  session_manager, and test files
- Extract shared build_additive_app_settings() to eliminate duplication
  between OpenClaw and Hermes deep link handling
- Combine identical OpenClaw/Hermes proxy match arms into unified arms
This commit is contained in:
Jason
2026-04-15 16:22:35 +08:00
parent 701e7d9581
commit 81af0a57f9
27 changed files with 272 additions and 103 deletions
+3
View File
@@ -130,6 +130,9 @@ impl ConfigService {
// OpenClaw uses additive mode, no live sync needed
// OpenClaw providers are managed directly in the config file
}
AppType::Hermes => {
// Hermes uses additive mode, no live sync needed
}
}
Ok(())
+9 -1
View File
@@ -128,6 +128,10 @@ impl McpService {
// Skip for now
log::debug!("OpenClaw MCP support is still in development, skipping sync");
}
AppType::Hermes => {
// TODO: Hermes MCP sync not yet implemented
log::debug!("Hermes MCP sync not yet implemented, skipping sync");
}
}
Ok(())
}
@@ -157,6 +161,10 @@ impl McpService {
// OpenClaw MCP support is still in development
log::debug!("OpenClaw MCP support is still in development, skipping remove");
}
AppType::Hermes => {
// TODO: Hermes MCP sync not yet implemented
log::debug!("Hermes MCP sync not yet implemented, skipping remove");
}
}
Ok(())
}
@@ -166,7 +174,7 @@ impl McpService {
let servers = Self::get_all_servers(state)?;
for app in AppType::all() {
if matches!(app, AppType::OpenClaw) {
if matches!(app, AppType::OpenClaw | AppType::Hermes) {
continue;
}
+28 -5
View File
@@ -42,6 +42,10 @@ 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)
}
_ => Ok(false),
}
}
@@ -345,7 +349,7 @@ fn settings_contain_common_config(app_type: &AppType, settings: &Value, snippet:
}
_ => false,
},
AppType::OpenCode | AppType::OpenClaw => false,
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => false,
}
}
@@ -415,7 +419,7 @@ pub(crate) fn remove_common_config_from_settings(
}
Ok(result)
}
AppType::OpenCode | AppType::OpenClaw => Ok(settings.clone()),
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => Ok(settings.clone()),
}
}
@@ -470,7 +474,7 @@ fn apply_common_config_to_settings(
}
Ok(result)
}
AppType::OpenCode | AppType::OpenClaw => Ok(settings.clone()),
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => Ok(settings.clone()),
}
}
@@ -790,6 +794,13 @@ 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
);
}
}
Ok(())
}
@@ -985,6 +996,18 @@ pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
let config = read_openclaw_config()?;
Ok(config)
}
AppType::Hermes => {
let config_path = crate::hermes_config::get_hermes_config_path();
if !config_path.exists() {
return Err(AppError::localized(
"hermes.config.missing",
"Hermes 配置文件不存在",
"Hermes configuration file not found",
));
}
// Return empty object until hermes_config is implemented
Ok(json!({}))
}
}
}
@@ -1067,8 +1090,8 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
"config": config_obj
})
}
// OpenCode and OpenClaw use additive mode and are handled by early return above
AppType::OpenCode | AppType::OpenClaw => {
// OpenCode, OpenClaw and Hermes use additive mode and are handled by early return above
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
unreachable!("additive mode apps are handled by early return")
}
};
+28 -2
View File
@@ -1282,6 +1282,12 @@ 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"
);
}
_ => {}
}
}
@@ -1344,6 +1350,10 @@ impl ProviderService {
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");
}
_ => {
return Err(AppError::Message(format!(
"App {} does not support remove from live config",
@@ -1532,6 +1542,10 @@ 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(())
}
_ => Ok(()),
};
@@ -1708,6 +1722,7 @@ impl ProviderService {
AppType::Gemini => Self::extract_gemini_common_config(&provider.settings_config),
AppType::OpenCode => Self::extract_opencode_common_config(&provider.settings_config),
AppType::OpenClaw => Self::extract_openclaw_common_config(&provider.settings_config),
AppType::Hermes => Ok(String::new()), // Hermes doesn't use common config snippets
}
}
@@ -1722,6 +1737,7 @@ impl ProviderService {
AppType::Gemini => Self::extract_gemini_common_config(settings_config),
AppType::OpenCode => Self::extract_opencode_common_config(settings_config),
AppType::OpenClaw => Self::extract_openclaw_common_config(settings_config),
AppType::Hermes => Ok(String::new()), // Hermes doesn't use common config snippets
}
}
@@ -2087,6 +2103,16 @@ impl ProviderService {
));
}
}
AppType::Hermes => {
// Hermes: accept any JSON object for now
if !provider.settings_config.is_object() {
return Err(AppError::localized(
"provider.hermes.settings.not_object",
"Hermes 配置必须是 JSON 对象",
"Hermes configuration must be a JSON object",
));
}
}
}
// Validate and clean UsageScript configuration (common for all app types)
@@ -2258,8 +2284,8 @@ impl ProviderService {
Ok((api_key, base_url))
}
AppType::OpenClaw => {
// OpenClaw uses apiKey and baseUrl directly on the object
AppType::OpenClaw | AppType::Hermes => {
// OpenClaw/Hermes use apiKey and baseUrl directly on the object
let api_key = provider
.settings_config
.get("apiKey")
+23 -56
View File
@@ -466,13 +466,9 @@ impl ProxyService {
AppType::Claude => self.read_claude_live()?,
AppType::Codex => self.read_codex_live()?,
AppType::Gemini => self.read_gemini_live()?,
AppType::OpenCode => {
// OpenCode doesn't support proxy features
return Err("OpenCode 不支持代理功能".to_string());
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features
return Err("OpenClaw 不支持代理功能".to_string());
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features
return Err("该应用不支持代理功能".to_string());
}
};
@@ -687,11 +683,8 @@ impl ProxyService {
}
}
}
AppType::OpenCode => {
// OpenCode doesn't support proxy features, skip silently
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features, skip silently
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features, skip silently
}
}
@@ -871,13 +864,9 @@ impl ProxyService {
AppType::Claude => ("claude", self.read_claude_live()?),
AppType::Codex => ("codex", self.read_codex_live()?),
AppType::Gemini => ("gemini", self.read_gemini_live()?),
AppType::OpenCode => {
// OpenCode doesn't support proxy features
return Err("OpenCode 不支持代理功能".to_string());
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features
return Err("OpenClaw 不支持代理功能".to_string());
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features
return Err("该应用不支持代理功能".to_string());
}
};
@@ -1019,13 +1008,9 @@ impl ProxyService {
self.write_gemini_live(&live_config)?;
log::info!("Gemini Live 配置已接管,代理地址: {proxy_url}");
}
AppType::OpenCode => {
// OpenCode doesn't support proxy features
return Err("OpenCode 不支持代理功能".to_string());
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features
return Err("OpenClaw 不支持代理功能".to_string());
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features
return Err("该应用不支持代理功能".to_string());
}
}
@@ -1076,11 +1061,8 @@ impl ProxyService {
let _ = self.write_gemini_live(&live_config);
}
}
AppType::OpenCode => {
// OpenCode doesn't support proxy features, skip silently
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features, skip silently
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features, skip silently
}
}
@@ -1119,11 +1101,8 @@ impl ProxyService {
log::info!("Gemini Live 配置已恢复");
}
}
AppType::OpenCode => {
// OpenCode doesn't support proxy features, skip silently
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features, skip silently
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features, skip silently
}
}
@@ -1213,13 +1192,9 @@ impl ProxyService {
AppType::Claude => self.write_claude_live(config),
AppType::Codex => self.write_codex_live(config),
AppType::Gemini => self.write_gemini_live(config),
AppType::OpenCode => {
// OpenCode doesn't support proxy features
Err("OpenCode 不支持代理功能".to_string())
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features
Err("OpenClaw 不支持代理功能".to_string())
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features
Err("该应用不支持代理功能".to_string())
}
}
}
@@ -1238,12 +1213,8 @@ impl ProxyService {
Ok(config) => Self::is_gemini_live_taken_over(&config),
Err(_) => false,
},
AppType::OpenCode => {
// OpenCode doesn't support proxy takeover
false
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy takeover
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy takeover
false
}
}
@@ -1285,12 +1256,8 @@ impl ProxyService {
AppType::Claude => self.cleanup_claude_takeover_placeholders_in_live(),
AppType::Codex => self.cleanup_codex_takeover_placeholders_in_live(),
AppType::Gemini => self.cleanup_gemini_takeover_placeholders_in_live(),
AppType::OpenCode => {
// OpenCode doesn't support proxy features
Ok(())
}
AppType::OpenClaw => {
// OpenClaw doesn't support proxy features
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// These apps don't support proxy features
Ok(())
}
}
@@ -1540,7 +1507,7 @@ impl ProxyService {
serde_json::to_string(&env_backup)
.map_err(|e| format!("序列化 Gemini 配置失败: {e}"))?
}
AppType::OpenCode | AppType::OpenClaw => {
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
return Err(format!("未知的应用类型: {app_type}"));
}
};
+6
View File
@@ -529,6 +529,11 @@ impl SkillService {
return Ok(custom.join("skills"));
}
}
AppType::Hermes => {
if let Some(custom) = crate::settings::get_hermes_override_dir() {
return Ok(custom.join("skills"));
}
}
}
// 默认路径:回退到用户主目录下的标准位置
@@ -544,6 +549,7 @@ impl SkillService {
AppType::Gemini => home.join(".gemini").join("skills"),
AppType::OpenCode => home.join(".config").join("opencode").join("skills"),
AppType::OpenClaw => home.join(".openclaw").join("skills"),
AppType::Hermes => crate::hermes_config::get_hermes_dir().join("skills"),
})
}
+20 -6
View File
@@ -207,7 +207,10 @@ impl StreamCheckService {
// OpenCode / OpenClaw 的 settings_config 结构与 Claude/Codex/Gemini 不同
// baseUrl / apiKey 直接作为根字段而非嵌套在 env),并且协议由 `api`
// 或 `npm` 字段显式指定。它们不走 get_adapter 路径,而是直接分发。
if matches!(app_type, AppType::OpenCode | AppType::OpenClaw) {
if matches!(
app_type,
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes
) {
return Self::check_once_without_adapter(app_type, provider, config, start).await;
}
@@ -270,9 +273,9 @@ impl StreamCheckService {
)
.await
}
AppType::OpenCode | AppType::OpenClaw => {
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => {
// Already handled via early dispatch above
unreachable!("OpenCode/OpenClaw 已通过 check_once_without_adapter 处理")
unreachable!("OpenCode/OpenClaw/Hermes 已通过 check_once_without_adapter 处理")
}
};
@@ -700,7 +703,18 @@ impl StreamCheckService {
)
.await
}
_ => unreachable!("check_once_without_adapter 只处理 OpenCode/OpenClaw"),
AppType::Hermes => {
// Hermes uses the same check path as OpenClaw for now
Self::check_openclaw_stream(
&client,
provider,
&model_to_test,
test_prompt,
request_timeout,
)
.await
}
_ => unreachable!("check_once_without_adapter 只处理 OpenCode/OpenClaw/Hermes"),
};
let response_time = start.elapsed().as_millis() as u64;
@@ -1241,8 +1255,8 @@ impl StreamCheckService {
// Try to extract first model from the models object
Self::extract_opencode_model(provider).unwrap_or_else(|| "gpt-4o".to_string())
}
AppType::OpenClaw => {
// OpenClaw uses models array in settings_config
AppType::OpenClaw | AppType::Hermes => {
// OpenClaw/Hermes use models array in settings_config
// Try to extract first model from the models array
Self::extract_openclaw_model(provider).unwrap_or_else(|| "gpt-4o".to_string())
}