From c4bf769eabdd845f7d07fe709757b1fbc9c6a91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Thu, 14 May 2026 03:56:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(hermes):=20P1-4=20hermes=5Fread=5Fconfig?= =?UTF-8?q?=5Ffull=20=E5=85=A8=E5=AD=97=E6=AE=B5=E8=A7=A3=E6=9E=90=20-=20?= =?UTF-8?q?=E8=A7=A3=E9=94=81=2014+=20Gateway=20=E9=AB=98=E4=BB=B7?= =?UTF-8?q?=E5=80=BC=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前 hermes_read_config 只读 5 字段(model/base_url/provider/api_key/config_exists), 为「快速面板」服务。Hermes Gateway 实际有 14+ 个顶层配置项,ClawPanel 完全没读到。 本次新增 hermes_read_config_full 命令,作为高级配置编辑器的数据源。 ## 后端实现 - 加 serde_yaml 0.9 依赖 - 新命令 hermes_read_config_full: · 用 serde_yaml 完整解析 config.yaml · 转 JSON 返回 { exists, raw, config, highlights } · highlights 字段单独抽出 14 个高价值顶层字段: streaming / stt_enabled / quick_commands / reset_triggers / default_reset_policy / unauthorized_dm_behavior / session_store_max_age_days / always_log_local / group_sessions_per_user / thread_sessions_per_user / platforms / dashboard / memory / skills · 已注册到 lib.rs ## 前端 - tauri-api.js 加 hermesReadConfigFull wrapper ## Web 模式 - dev-api.js 加 hermes_read_config_full handler(Web 模式不强制 yaml 解析, 返回 raw + null highlights,前端按需 fallback) ## 后续 - 实际「高级配置编辑器」UI 后续单独开 — 本次仅打通数据通道 - 与轻量版 hermes_read_config 互补共存,model 配置页继续用轻量版 --- scripts/dev-api.js | 22 +++++++++ src-tauri/Cargo.lock | 20 ++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/commands/hermes.rs | 82 ++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/lib/tauri-api.js | 1 + 6 files changed, 127 insertions(+) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index cd2377a..ef58a57 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -7159,6 +7159,28 @@ const handlers = { return { model: displayModel, model_raw: modelName, base_url: baseUrl, provider, api_key: apiKey, config_exists: fs.existsSync(configPath) } }, + // P1-4:完整解析 config.yaml,让前端能读 14+ 高价值字段 + // Web 模式不引入 yaml 依赖,简单返回 raw + null highlights(前端按需渲染) + hermes_read_config_full() { + const configPath = path.join(hermesHome(), 'config.yaml') + if (!fs.existsSync(configPath)) { + return { exists: false, raw: '', config: {}, highlights: {} } + } + let raw = '' + try { raw = fs.readFileSync(configPath, 'utf8') } catch {} + // Web 模式下不强制 yaml 解析(避免新增依赖),前端可走 raw 自己 parse 或者 fallback 到桌面端 + const highlightKeys = [ + 'streaming', 'stt_enabled', 'quick_commands', 'reset_triggers', + 'default_reset_policy', 'unauthorized_dm_behavior', + 'session_store_max_age_days', 'always_log_local', + 'group_sessions_per_user', 'thread_sessions_per_user', + 'platforms', 'dashboard', 'memory', 'skills', + ] + const highlights = {} + highlightKeys.forEach(k => { highlights[k] = null }) + return { exists: true, raw, config: {}, highlights } + }, + hermes_list_providers() { return HERMES_PROVIDER_REGISTRY.map(p => ({ ...p, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dd30394..34d910f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -379,6 +379,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", + "serde_yaml", "sha2", "tar", "tauri", @@ -3537,6 +3538,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -4638,6 +4652,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9c75a00..e43ee1e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yaml = "0.9" dirs = "6" chrono = "0.4" zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 2ecda04..8bda68b 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2068,6 +2068,88 @@ pub async fn hermes_read_config() -> Result { })) } +// --------------------------------------------------------------------------- +// hermes_read_config_full — 解析整个 config.yaml 为 JSON 返回给前端 +// +// 与轻量版 hermes_read_config(仅返回 5 个 model 相关字段)互补: +// 前者用于 model 配置页快速展示,本命令用于「高级配置编辑器」让用户能看到/改 +// Gateway 端 14+ 个顶层配置项,比如 quick_commands / streaming / reset_triggers / +// stt_enabled / unauthorized_dm_behavior 等。 +// +// 返回值结构: +// { +// "exists": true, // config.yaml 是否存在 +// "raw": "...yaml string...", // 原文(给 yaml editor) +// "config": { ...full json... }, // 整份 yaml 转成 JSON +// "highlights": { // 14 个高价值字段单独抽出,前端直接 .x 访问 +// "streaming": {...}, "stt_enabled": true, "quick_commands": {...}, +// "reset_triggers": [...], "default_reset_policy": {...}, +// "unauthorized_dm_behavior": "pair", "session_store_max_age_days": 90, +// "always_log_local": true, +// "group_sessions_per_user": false, "thread_sessions_per_user": false, +// ... 等 +// } +// } +// --------------------------------------------------------------------------- + +#[tauri::command] +pub async fn hermes_read_config_full() -> Result { + let config_path = hermes_home().join("config.yaml"); + + if !config_path.exists() { + return Ok(serde_json::json!({ + "exists": false, + "raw": "", + "config": {}, + "highlights": {}, + })); + } + + let raw = std::fs::read_to_string(&config_path) + .map_err(|e| format!("Failed to read config.yaml: {e}"))?; + + // 解析 YAML → JSON + let yaml_value: serde_yaml::Value = serde_yaml::from_str(&raw) + .map_err(|e| format!("Invalid YAML in config.yaml: {e}"))?; + let config_json: Value = serde_json::to_value(&yaml_value) + .map_err(|e| format!("YAML→JSON conversion failed: {e}"))?; + + // 抽取 14 个高价值顶层字段(如不存在保持 null,前端按需渲染) + let highlight_keys = [ + "streaming", + "stt_enabled", + "quick_commands", + "reset_triggers", + "default_reset_policy", + "unauthorized_dm_behavior", + "session_store_max_age_days", + "always_log_local", + "group_sessions_per_user", + "thread_sessions_per_user", + "platforms", + "dashboard", + "memory", + "skills", + ]; + let highlights: serde_json::Map = highlight_keys + .iter() + .map(|k| { + let v = config_json + .get(*k) + .cloned() + .unwrap_or(Value::Null); + ((*k).to_string(), v) + }) + .collect(); + + Ok(serde_json::json!({ + "exists": true, + "raw": raw, + "config": config_json, + "highlights": Value::Object(highlights), + })) +} + // --------------------------------------------------------------------------- // hermes_fetch_models — 从 API 获取模型列表(后端代理,避免 CORS) // --------------------------------------------------------------------------- diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b0b6cc4..0dda09e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -240,6 +240,7 @@ pub fn run() { hermes::hermes_api_proxy, hermes::hermes_agent_run, hermes::hermes_read_config, + hermes::hermes_read_config_full, hermes::hermes_fetch_models, hermes::hermes_update_model, hermes::hermes_detect_environments, diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index fd8298c..78db05f 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -476,6 +476,7 @@ export const api = { hermesAgentRun: (input, sessionId, conversationHistory, instructions) => invoke('hermes_agent_run', { input, sessionId: sessionId || null, conversationHistory: conversationHistory || null, instructions: instructions || null }), hermesAgentRunStream: (input, sessionId, conversationHistory, instructions, onEvent, options) => webStreamInvoke('hermes_agent_run_stream', { input, sessionId: sessionId || null, conversationHistory: conversationHistory || null, instructions: instructions || null }, onEvent, options), hermesReadConfig: () => invoke('hermes_read_config'), + hermesReadConfigFull: () => invoke('hermes_read_config_full'), hermesFetchModels: (baseUrl, apiKey, apiType, provider) => invoke('hermes_fetch_models', { baseUrl, apiKey, apiType: apiType || null, provider: provider || null }), hermesUpdateModel: (model, provider) => invoke('hermes_update_model', { model, provider: provider || null }), hermesListProviders: () => cachedInvoke('hermes_list_providers', {}, 600000),