diff --git a/scripts/dev-api.js b/scripts/dev-api.js index cd42a11..475b721 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3329,6 +3329,7 @@ const HERMES_STT_PROVIDERS = new Set(['auto', 'local', 'groq', 'openai', 'mistra const HERMES_STT_LOCAL_MODELS = new Set(['tiny', 'base', 'small', 'medium', 'large-v3', 'turbo']) const HERMES_STT_OPENAI_MODELS = new Set(['whisper-1', 'gpt-4o-mini-transcribe', 'gpt-4o-transcribe']) const HERMES_STT_MISTRAL_MODELS = new Set(['voxtral-mini-latest', 'voxtral-mini-2602']) +const HERMES_AUXILIARY_PROVIDERS = new Set(['auto', 'openrouter', 'nous', 'gemini', 'ollama-cloud', 'codex', 'main']) const HERMES_APPROVAL_MODES = new Set(['manual', 'smart', 'off']) const HERMES_APPROVAL_CRON_MODES = new Set(['deny', 'approve']) const HERMES_LOGGING_LEVELS = new Set(['DEBUG', 'INFO', 'WARNING']) @@ -3505,6 +3506,21 @@ function normalizeHermesPromptCacheTtl(value, strict = false) { return '5m' } +function normalizeHermesAuxiliaryProvider(value, key, strict = false) { + const provider = String(value ?? '').trim().toLowerCase() || 'auto' + if (HERMES_AUXILIARY_PROVIDERS.has(provider)) return provider + if (strict) throw new Error(`${key} 必须是 auto、openrouter、nous、gemini、ollama-cloud、codex 或 main`) + return 'auto' +} + +function normalizeHermesAuxiliaryModel(value, key, strict = false) { + const model = String(value ?? '').trim() + if (!model) return '' + if (/^[a-zA-Z0-9_./:@+-]+$/.test(model) && !model.split('/').includes('..')) return model + if (strict) throw new Error(`${key} 只能包含字母、数字、下划线、点、斜杠、冒号、@、加号和短横线`) + return '' +} + function normalizeHermesDisplayToolProgress(value, strict = false, key = 'display.tool_progress') { const progress = String(value ?? '').trim().toLowerCase() || 'all' if (HERMES_DISPLAY_TOOL_PROGRESS_VALUES.has(progress)) return progress @@ -3727,6 +3743,68 @@ export function mergeHermesPromptCachingConfig(config = {}, form = {}) { return next } +function hermesAuxiliaryTask(root, key) { + const auxiliary = root.auxiliary && typeof root.auxiliary === 'object' && !Array.isArray(root.auxiliary) + ? root.auxiliary + : {} + return auxiliary[key] && typeof auxiliary[key] === 'object' && !Array.isArray(auxiliary[key]) + ? auxiliary[key] + : {} +} + +export function buildHermesAuxiliaryConfigValues(config = {}) { + const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} + const vision = hermesAuxiliaryTask(root, 'vision') + const webExtract = hermesAuxiliaryTask(root, 'web_extract') + const sessionSearch = hermesAuxiliaryTask(root, 'session_search') + return { + auxiliaryVisionProvider: normalizeHermesAuxiliaryProvider(vision.provider, 'auxiliary.vision.provider', false), + auxiliaryVisionModel: normalizeHermesAuxiliaryModel(vision.model, 'auxiliary.vision.model', false), + auxiliaryVisionTimeout: parseHermesInteger(vision.timeout, 'auxiliary.vision.timeout', 30, 1, 3600, false), + auxiliaryVisionDownloadTimeout: parseHermesInteger(vision.download_timeout, 'auxiliary.vision.download_timeout', 30, 1, 3600, false), + auxiliaryWebExtractProvider: normalizeHermesAuxiliaryProvider(webExtract.provider, 'auxiliary.web_extract.provider', false), + auxiliaryWebExtractModel: normalizeHermesAuxiliaryModel(webExtract.model, 'auxiliary.web_extract.model', false), + auxiliarySessionSearchProvider: normalizeHermesAuxiliaryProvider(sessionSearch.provider, 'auxiliary.session_search.provider', false), + auxiliarySessionSearchModel: normalizeHermesAuxiliaryModel(sessionSearch.model, 'auxiliary.session_search.model', false), + auxiliarySessionSearchTimeout: parseHermesInteger(sessionSearch.timeout, 'auxiliary.session_search.timeout', 30, 1, 3600, false), + auxiliarySessionSearchMaxConcurrency: parseHermesInteger(sessionSearch.max_concurrency, 'auxiliary.session_search.max_concurrency', 3, 1, 100, false), + } +} + +export function mergeHermesAuxiliaryConfig(config = {}, form = {}) { + const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {}) + const currentValues = buildHermesAuxiliaryConfigValues(next) + const auxiliary = next.auxiliary && typeof next.auxiliary === 'object' && !Array.isArray(next.auxiliary) + ? mergeConfigsPreservingFields(next.auxiliary, {}) + : {} + const vision = auxiliary.vision && typeof auxiliary.vision === 'object' && !Array.isArray(auxiliary.vision) + ? mergeConfigsPreservingFields(auxiliary.vision, {}) + : {} + const webExtract = auxiliary.web_extract && typeof auxiliary.web_extract === 'object' && !Array.isArray(auxiliary.web_extract) + ? mergeConfigsPreservingFields(auxiliary.web_extract, {}) + : {} + const sessionSearch = auxiliary.session_search && typeof auxiliary.session_search === 'object' && !Array.isArray(auxiliary.session_search) + ? mergeConfigsPreservingFields(auxiliary.session_search, {}) + : {} + + vision.provider = normalizeHermesAuxiliaryProvider(Object.hasOwn(form, 'auxiliaryVisionProvider') ? form.auxiliaryVisionProvider : currentValues.auxiliaryVisionProvider, 'auxiliary.vision.provider', true) + vision.model = normalizeHermesAuxiliaryModel(Object.hasOwn(form, 'auxiliaryVisionModel') ? form.auxiliaryVisionModel : currentValues.auxiliaryVisionModel, 'auxiliary.vision.model', true) + vision.timeout = parseHermesInteger(Object.hasOwn(form, 'auxiliaryVisionTimeout') ? form.auxiliaryVisionTimeout : currentValues.auxiliaryVisionTimeout, 'auxiliary.vision.timeout', 30, 1, 3600, true) + vision.download_timeout = parseHermesInteger(Object.hasOwn(form, 'auxiliaryVisionDownloadTimeout') ? form.auxiliaryVisionDownloadTimeout : currentValues.auxiliaryVisionDownloadTimeout, 'auxiliary.vision.download_timeout', 30, 1, 3600, true) + webExtract.provider = normalizeHermesAuxiliaryProvider(Object.hasOwn(form, 'auxiliaryWebExtractProvider') ? form.auxiliaryWebExtractProvider : currentValues.auxiliaryWebExtractProvider, 'auxiliary.web_extract.provider', true) + webExtract.model = normalizeHermesAuxiliaryModel(Object.hasOwn(form, 'auxiliaryWebExtractModel') ? form.auxiliaryWebExtractModel : currentValues.auxiliaryWebExtractModel, 'auxiliary.web_extract.model', true) + sessionSearch.provider = normalizeHermesAuxiliaryProvider(Object.hasOwn(form, 'auxiliarySessionSearchProvider') ? form.auxiliarySessionSearchProvider : currentValues.auxiliarySessionSearchProvider, 'auxiliary.session_search.provider', true) + sessionSearch.model = normalizeHermesAuxiliaryModel(Object.hasOwn(form, 'auxiliarySessionSearchModel') ? form.auxiliarySessionSearchModel : currentValues.auxiliarySessionSearchModel, 'auxiliary.session_search.model', true) + sessionSearch.timeout = parseHermesInteger(Object.hasOwn(form, 'auxiliarySessionSearchTimeout') ? form.auxiliarySessionSearchTimeout : currentValues.auxiliarySessionSearchTimeout, 'auxiliary.session_search.timeout', 30, 1, 3600, true) + sessionSearch.max_concurrency = parseHermesInteger(Object.hasOwn(form, 'auxiliarySessionSearchMaxConcurrency') ? form.auxiliarySessionSearchMaxConcurrency : currentValues.auxiliarySessionSearchMaxConcurrency, 'auxiliary.session_search.max_concurrency', 3, 1, 100, true) + + auxiliary.vision = vision + auxiliary.web_extract = webExtract + auxiliary.session_search = sessionSearch + next.auxiliary = auxiliary + return next +} + export function buildHermesToolLoopGuardrailsConfigValues(config = {}) { const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} const guardrails = root.tool_loop_guardrails && typeof root.tool_loop_guardrails === 'object' && !Array.isArray(root.tool_loop_guardrails) @@ -10830,6 +10908,27 @@ const handlers = { } }, + hermes_auxiliary_config_read() { + const { configPath, exists, config } = readHermesConfigYamlObject() + return { + exists, + configPath, + values: buildHermesAuxiliaryConfigValues(config), + } + }, + + hermes_auxiliary_config_save({ form } = {}) { + const { configPath, config } = readHermesConfigYamlObject() + const next = mergeHermesAuxiliaryConfig(config, form || {}) + const backup = writeHermesConfigYamlObject(configPath, next) + return { + ok: true, + configPath, + backup, + values: buildHermesAuxiliaryConfigValues(next), + } + }, + hermes_tool_loop_guardrails_config_read() { const { configPath, exists, config } = readHermesConfigYamlObject() return { diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 4d312b9..76e893e 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2168,6 +2168,15 @@ const HERMES_CHANNEL_PLATFORMS: [&str; 10] = [ const HERMES_DISPLAY_TOOL_PROGRESS_VALUES: [&str; 4] = ["off", "new", "all", "verbose"]; const HERMES_DISPLAY_STREAMING_VALUES: [&str; 3] = ["inherit", "true", "false"]; const HERMES_PROMPT_CACHE_TTLS: [&str; 2] = ["5m", "1h"]; +const HERMES_AUXILIARY_PROVIDERS: [&str; 7] = [ + "auto", + "openrouter", + "nous", + "gemini", + "ollama-cloud", + "codex", + "main", +]; fn normalize_hermes_channel_platform(platform: &str) -> Option<&'static str> { let platform = platform.trim().to_ascii_lowercase(); @@ -2268,6 +2277,52 @@ fn normalize_hermes_prompt_cache_ttl( } } +fn normalize_hermes_auxiliary_provider( + value: Option, + key: &str, + strict: bool, +) -> Result { + let provider = value.unwrap_or_default().trim().to_ascii_lowercase(); + let provider = if provider.is_empty() { + "auto".to_string() + } else { + provider + }; + if HERMES_AUXILIARY_PROVIDERS.contains(&provider.as_str()) { + Ok(provider) + } else if strict { + Err(format!( + "{key} 必须是 auto、openrouter、nous、gemini、ollama-cloud、codex 或 main" + )) + } else { + Ok("auto".to_string()) + } +} + +fn normalize_hermes_auxiliary_model( + value: Option, + key: &str, + strict: bool, +) -> Result { + let model = value.unwrap_or_default().trim().to_string(); + if model.is_empty() { + return Ok(String::new()); + } + if !model.split('/').any(|part| part == "..") + && model.chars().all(|ch| { + ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | '/' | ':' | '@' | '+' | '-') + }) + { + Ok(model) + } else if strict { + Err(format!( + "{key} 只能包含字母、数字、下划线、点、斜杠、冒号、@、加号和短横线" + )) + } else { + Ok(String::new()) + } +} + fn yaml_key(key: &str) -> serde_yaml::Value { serde_yaml::Value::String(key.to_string()) } @@ -3464,6 +3519,221 @@ fn merge_hermes_prompt_caching_config( Ok(()) } +fn hermes_auxiliary_task<'a>( + root: Option<&'a serde_yaml::Mapping>, + key: &str, +) -> Option<&'a serde_yaml::Mapping> { + root.and_then(|map| yaml_get_mapping(map, "auxiliary")) + .and_then(|map| yaml_get_mapping(map, key)) +} + +fn build_hermes_auxiliary_config_values(config: &serde_yaml::Value) -> Value { + let root = config.as_mapping(); + let vision = hermes_auxiliary_task(root, "vision"); + let web_extract = hermes_auxiliary_task(root, "web_extract"); + let session_search = hermes_auxiliary_task(root, "session_search"); + + serde_json::json!({ + "auxiliaryVisionProvider": normalize_hermes_auxiliary_provider( + vision.and_then(|map| yaml_string_field(map, "provider")), + "auxiliary.vision.provider", + false, + ).unwrap_or_else(|_| "auto".to_string()), + "auxiliaryVisionModel": normalize_hermes_auxiliary_model( + vision.and_then(|map| yaml_string_field(map, "model")), + "auxiliary.vision.model", + false, + ).unwrap_or_default(), + "auxiliaryVisionTimeout": vision.map(|map| bounded_hermes_i64(yaml_i64_field(map, "timeout"), 30, 1, 3600)).unwrap_or(30), + "auxiliaryVisionDownloadTimeout": vision.map(|map| bounded_hermes_i64(yaml_i64_field(map, "download_timeout"), 30, 1, 3600)).unwrap_or(30), + "auxiliaryWebExtractProvider": normalize_hermes_auxiliary_provider( + web_extract.and_then(|map| yaml_string_field(map, "provider")), + "auxiliary.web_extract.provider", + false, + ).unwrap_or_else(|_| "auto".to_string()), + "auxiliaryWebExtractModel": normalize_hermes_auxiliary_model( + web_extract.and_then(|map| yaml_string_field(map, "model")), + "auxiliary.web_extract.model", + false, + ).unwrap_or_default(), + "auxiliarySessionSearchProvider": normalize_hermes_auxiliary_provider( + session_search.and_then(|map| yaml_string_field(map, "provider")), + "auxiliary.session_search.provider", + false, + ).unwrap_or_else(|_| "auto".to_string()), + "auxiliarySessionSearchModel": normalize_hermes_auxiliary_model( + session_search.and_then(|map| yaml_string_field(map, "model")), + "auxiliary.session_search.model", + false, + ).unwrap_or_default(), + "auxiliarySessionSearchTimeout": session_search.map(|map| bounded_hermes_i64(yaml_i64_field(map, "timeout"), 30, 1, 3600)).unwrap_or(30), + "auxiliarySessionSearchMaxConcurrency": session_search.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_concurrency"), 3, 1, 100)).unwrap_or(3), + }) +} + +fn merge_hermes_auxiliary_config( + config: &mut serde_yaml::Value, + form: &Value, +) -> Result<(), String> { + let current = build_hermes_auxiliary_config_values(config); + let vision_provider = normalize_hermes_auxiliary_provider( + form_string(form, "auxiliaryVisionProvider").or_else(|| { + current["auxiliaryVisionProvider"] + .as_str() + .map(ToString::to_string) + }), + "auxiliary.vision.provider", + true, + )?; + let vision_model = normalize_hermes_auxiliary_model( + form_string(form, "auxiliaryVisionModel").or_else(|| { + current["auxiliaryVisionModel"] + .as_str() + .map(ToString::to_string) + }), + "auxiliary.vision.model", + true, + )?; + let vision_timeout = validate_hermes_i64( + if form.get("auxiliaryVisionTimeout").is_some() { + form_i64(form, "auxiliaryVisionTimeout") + } else { + Some(current["auxiliaryVisionTimeout"].as_i64().unwrap_or(30)) + }, + "auxiliary.vision.timeout", + 30, + 1, + 3600, + )?; + let vision_download_timeout = validate_hermes_i64( + if form.get("auxiliaryVisionDownloadTimeout").is_some() { + form_i64(form, "auxiliaryVisionDownloadTimeout") + } else { + Some( + current["auxiliaryVisionDownloadTimeout"] + .as_i64() + .unwrap_or(30), + ) + }, + "auxiliary.vision.download_timeout", + 30, + 1, + 3600, + )?; + let web_extract_provider = normalize_hermes_auxiliary_provider( + form_string(form, "auxiliaryWebExtractProvider").or_else(|| { + current["auxiliaryWebExtractProvider"] + .as_str() + .map(ToString::to_string) + }), + "auxiliary.web_extract.provider", + true, + )?; + let web_extract_model = normalize_hermes_auxiliary_model( + form_string(form, "auxiliaryWebExtractModel").or_else(|| { + current["auxiliaryWebExtractModel"] + .as_str() + .map(ToString::to_string) + }), + "auxiliary.web_extract.model", + true, + )?; + let session_search_provider = normalize_hermes_auxiliary_provider( + form_string(form, "auxiliarySessionSearchProvider").or_else(|| { + current["auxiliarySessionSearchProvider"] + .as_str() + .map(ToString::to_string) + }), + "auxiliary.session_search.provider", + true, + )?; + let session_search_model = normalize_hermes_auxiliary_model( + form_string(form, "auxiliarySessionSearchModel").or_else(|| { + current["auxiliarySessionSearchModel"] + .as_str() + .map(ToString::to_string) + }), + "auxiliary.session_search.model", + true, + )?; + let session_search_timeout = validate_hermes_i64( + if form.get("auxiliarySessionSearchTimeout").is_some() { + form_i64(form, "auxiliarySessionSearchTimeout") + } else { + Some( + current["auxiliarySessionSearchTimeout"] + .as_i64() + .unwrap_or(30), + ) + }, + "auxiliary.session_search.timeout", + 30, + 1, + 3600, + )?; + let session_search_max_concurrency = validate_hermes_i64( + if form.get("auxiliarySessionSearchMaxConcurrency").is_some() { + form_i64(form, "auxiliarySessionSearchMaxConcurrency") + } else { + Some( + current["auxiliarySessionSearchMaxConcurrency"] + .as_i64() + .unwrap_or(3), + ) + }, + "auxiliary.session_search.max_concurrency", + 3, + 1, + 100, + )?; + + let root = ensure_yaml_object(config)?; + let auxiliary = yaml_child_object(root, "auxiliary")?; + let vision = yaml_child_object(auxiliary, "vision")?; + vision.insert( + yaml_key("provider"), + serde_yaml::Value::String(vision_provider), + ); + vision.insert(yaml_key("model"), serde_yaml::Value::String(vision_model)); + vision.insert( + yaml_key("timeout"), + serde_yaml::Value::Number(vision_timeout.into()), + ); + vision.insert( + yaml_key("download_timeout"), + serde_yaml::Value::Number(vision_download_timeout.into()), + ); + + let web_extract = yaml_child_object(auxiliary, "web_extract")?; + web_extract.insert( + yaml_key("provider"), + serde_yaml::Value::String(web_extract_provider), + ); + web_extract.insert( + yaml_key("model"), + serde_yaml::Value::String(web_extract_model), + ); + + let session_search = yaml_child_object(auxiliary, "session_search")?; + session_search.insert( + yaml_key("provider"), + serde_yaml::Value::String(session_search_provider), + ); + session_search.insert( + yaml_key("model"), + serde_yaml::Value::String(session_search_model), + ); + session_search.insert( + yaml_key("timeout"), + serde_yaml::Value::Number(session_search_timeout.into()), + ); + session_search.insert( + yaml_key("max_concurrency"), + serde_yaml::Value::Number(session_search_max_concurrency.into()), + ); + Ok(()) +} + fn build_hermes_tool_loop_guardrails_config_values(config: &serde_yaml::Value) -> Value { let root = config.as_mapping(); let guardrails = root.and_then(|map| yaml_get_mapping(map, "tool_loop_guardrails")); @@ -7231,6 +7501,30 @@ pub fn hermes_prompt_caching_config_save(form: Value) -> Result { })) } +#[tauri::command] +pub fn hermes_auxiliary_config_read() -> Result { + let (config_path, exists, config) = read_hermes_channel_yaml_config()?; + ensure_yaml_object(&mut config.clone())?; + Ok(serde_json::json!({ + "exists": exists, + "configPath": config_path.to_string_lossy(), + "values": build_hermes_auxiliary_config_values(&config), + })) +} + +#[tauri::command] +pub fn hermes_auxiliary_config_save(form: Value) -> Result { + let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?; + merge_hermes_auxiliary_config(&mut config, &form)?; + let backup = write_hermes_yaml_config(&config_path, &config)?; + Ok(serde_json::json!({ + "ok": true, + "configPath": config_path.to_string_lossy(), + "backup": backup, + "values": build_hermes_auxiliary_config_values(&config), + })) +} + #[tauri::command] pub fn hermes_tool_loop_guardrails_config_read() -> Result { let (config_path, exists, config) = read_hermes_channel_yaml_config()?; @@ -12884,6 +13178,202 @@ compression: } } +#[cfg(test)] +mod hermes_auxiliary_config_tests { + use super::{build_hermes_auxiliary_config_values, merge_hermes_auxiliary_config}; + use serde_json::json; + + #[test] + fn auxiliary_values_have_upstream_defaults() { + let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let values = build_hermes_auxiliary_config_values(&config); + assert_eq!(values["auxiliaryVisionProvider"], "auto"); + assert_eq!(values["auxiliaryVisionModel"], ""); + assert_eq!(values["auxiliaryVisionTimeout"], 30); + assert_eq!(values["auxiliaryVisionDownloadTimeout"], 30); + assert_eq!(values["auxiliaryWebExtractProvider"], "auto"); + assert_eq!(values["auxiliaryWebExtractModel"], ""); + assert_eq!(values["auxiliarySessionSearchProvider"], "auto"); + assert_eq!(values["auxiliarySessionSearchModel"], ""); + assert_eq!(values["auxiliarySessionSearchTimeout"], 30); + assert_eq!(values["auxiliarySessionSearchMaxConcurrency"], 3); + } + + #[test] + fn auxiliary_values_read_yaml_fields() { + let config: serde_yaml::Value = serde_yaml::from_str( + r#" +auxiliary: + vision: + provider: openrouter + model: google/gemini-2.5-flash + timeout: 45 + download_timeout: 60 + web_extract: + provider: main + model: local-summary + session_search: + provider: nous + model: gemini-3-flash + timeout: 50 + max_concurrency: 5 +"#, + ) + .unwrap(); + + let values = build_hermes_auxiliary_config_values(&config); + assert_eq!(values["auxiliaryVisionProvider"], "openrouter"); + assert_eq!(values["auxiliaryVisionModel"], "google/gemini-2.5-flash"); + assert_eq!(values["auxiliaryVisionTimeout"], 45); + assert_eq!(values["auxiliaryVisionDownloadTimeout"], 60); + assert_eq!(values["auxiliaryWebExtractProvider"], "main"); + assert_eq!(values["auxiliaryWebExtractModel"], "local-summary"); + assert_eq!(values["auxiliarySessionSearchProvider"], "nous"); + assert_eq!(values["auxiliarySessionSearchModel"], "gemini-3-flash"); + assert_eq!(values["auxiliarySessionSearchTimeout"], 50); + assert_eq!(values["auxiliarySessionSearchMaxConcurrency"], 5); + } + + #[test] + fn merge_auxiliary_config_preserves_unknown_fields() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +model: + provider: anthropic +auxiliary: + vision: + provider: auto + custom_flag: keep-vision + web_extract: + custom_flag: keep-web + session_search: + extra_body: + enable_thinking: false + custom_flag: keep-search + custom_task: + provider: main +streaming: + enabled: true +"#, + ) + .unwrap(); + + merge_hermes_auxiliary_config( + &mut config, + &json!({ + "auxiliaryVisionProvider": "codex", + "auxiliaryVisionModel": "gpt-5.3-codex", + "auxiliaryVisionTimeout": "40", + "auxiliaryVisionDownloadTimeout": "55", + "auxiliaryWebExtractProvider": "gemini", + "auxiliaryWebExtractModel": "gemini-3-flash", + "auxiliarySessionSearchProvider": "ollama-cloud", + "auxiliarySessionSearchModel": "gpt-oss:20b", + "auxiliarySessionSearchTimeout": "70", + "auxiliarySessionSearchMaxConcurrency": "6", + }), + ) + .unwrap(); + + assert_eq!(config["model"]["provider"].as_str(), Some("anthropic")); + assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true)); + assert_eq!( + config["auxiliary"]["vision"]["provider"].as_str(), + Some("codex") + ); + assert_eq!( + config["auxiliary"]["vision"]["model"].as_str(), + Some("gpt-5.3-codex") + ); + assert_eq!(config["auxiliary"]["vision"]["timeout"].as_i64(), Some(40)); + assert_eq!( + config["auxiliary"]["vision"]["download_timeout"].as_i64(), + Some(55) + ); + assert_eq!( + config["auxiliary"]["vision"]["custom_flag"].as_str(), + Some("keep-vision") + ); + assert_eq!( + config["auxiliary"]["web_extract"]["provider"].as_str(), + Some("gemini") + ); + assert_eq!( + config["auxiliary"]["web_extract"]["model"].as_str(), + Some("gemini-3-flash") + ); + assert_eq!( + config["auxiliary"]["web_extract"]["custom_flag"].as_str(), + Some("keep-web") + ); + assert_eq!( + config["auxiliary"]["session_search"]["provider"].as_str(), + Some("ollama-cloud") + ); + assert_eq!( + config["auxiliary"]["session_search"]["model"].as_str(), + Some("gpt-oss:20b") + ); + assert_eq!( + config["auxiliary"]["session_search"]["timeout"].as_i64(), + Some(70) + ); + assert_eq!( + config["auxiliary"]["session_search"]["max_concurrency"].as_i64(), + Some(6) + ); + assert_eq!( + config["auxiliary"]["session_search"]["extra_body"]["enable_thinking"].as_bool(), + Some(false) + ); + assert_eq!( + config["auxiliary"]["session_search"]["custom_flag"].as_str(), + Some("keep-search") + ); + assert_eq!( + config["auxiliary"]["custom_task"]["provider"].as_str(), + Some("main") + ); + } + + #[test] + fn merge_auxiliary_config_rejects_invalid_values() { + let mut config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let err = merge_hermes_auxiliary_config( + &mut config, + &json!({ "auxiliaryVisionProvider": "bad-provider" }), + ) + .unwrap_err(); + assert!(err.contains("auxiliary.vision.provider")); + + let err = merge_hermes_auxiliary_config( + &mut config, + &json!({ "auxiliaryVisionModel": "../secret" }), + ) + .unwrap_err(); + assert!(err.contains("auxiliary.vision.model")); + + let err = + merge_hermes_auxiliary_config(&mut config, &json!({ "auxiliaryVisionTimeout": 0 })) + .unwrap_err(); + assert!(err.contains("auxiliary.vision.timeout")); + + let err = merge_hermes_auxiliary_config( + &mut config, + &json!({ "auxiliaryVisionDownloadTimeout": 0 }), + ) + .unwrap_err(); + assert!(err.contains("auxiliary.vision.download_timeout")); + + let err = merge_hermes_auxiliary_config( + &mut config, + &json!({ "auxiliarySessionSearchMaxConcurrency": 0 }), + ) + .unwrap_err(); + assert!(err.contains("auxiliary.session_search.max_concurrency")); + } +} + #[cfg(test)] mod hermes_tool_loop_guardrails_config_tests { use super::{ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8f21b88..4a68c42 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -263,6 +263,8 @@ pub fn run() { hermes::hermes_compression_config_save, hermes::hermes_prompt_caching_config_read, hermes::hermes_prompt_caching_config_save, + hermes::hermes_auxiliary_config_read, + hermes::hermes_auxiliary_config_save, hermes::hermes_tool_loop_guardrails_config_read, hermes::hermes_tool_loop_guardrails_config_save, hermes::hermes_memory_config_read, diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index f72d3cc..4d68cf8 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -27,6 +27,19 @@ const PROMPT_CACHING_DEFAULTS = { promptCacheTtl: '5m', } +const AUXILIARY_DEFAULTS = { + auxiliaryVisionProvider: 'auto', + auxiliaryVisionModel: '', + auxiliaryVisionTimeout: 30, + auxiliaryVisionDownloadTimeout: 30, + auxiliaryWebExtractProvider: 'auto', + auxiliaryWebExtractModel: '', + auxiliarySessionSearchProvider: 'auto', + auxiliarySessionSearchModel: '', + auxiliarySessionSearchTimeout: 30, + auxiliarySessionSearchMaxConcurrency: 3, +} + const TOOL_GUARDRAILS_DEFAULTS = { warningsEnabled: true, hardStopEnabled: false, @@ -227,6 +240,7 @@ const APPROVAL_MODES = ['manual', 'smart', 'off'] const APPROVAL_CRON_MODES = ['deny', 'approve'] const LOGGING_LEVELS = ['DEBUG', 'INFO', 'WARNING'] const PROMPT_CACHE_TTLS = ['5m', '1h'] +const AUXILIARY_PROVIDERS = ['auto', 'openrouter', 'nous', 'gemini', 'ollama-cloud', 'codex', 'main'] export function render() { const el = document.createElement('div') @@ -236,6 +250,7 @@ export function render() { let runtimeValues = { ...SESSION_RUNTIME_DEFAULTS } let compressionValues = { ...COMPRESSION_DEFAULTS } let promptCachingValues = { ...PROMPT_CACHING_DEFAULTS } + let auxiliaryValues = { ...AUXILIARY_DEFAULTS } let toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS } let memoryValues = { ...MEMORY_DEFAULTS } let skillsValues = { ...SKILLS_DEFAULTS } @@ -262,6 +277,7 @@ export function render() { let runtimeLoading = true let compressionLoading = true let promptCachingLoading = true + let auxiliaryLoading = true let toolGuardrailsLoading = true let memoryLoading = true let skillsLoading = true @@ -288,6 +304,7 @@ export function render() { let runtimeSaving = false let compressionSaving = false let promptCachingSaving = false + let auxiliarySaving = false let toolGuardrailsSaving = false let memorySaving = false let skillsSaving = false @@ -314,6 +331,7 @@ export function render() { let runtimeError = null let compressionError = null let promptCachingError = null + let auxiliaryError = null let toolGuardrailsError = null let memoryError = null let skillsError = null @@ -346,7 +364,7 @@ export function render() { } function isBusy() { - return loading || runtimeLoading || compressionLoading || promptCachingLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || platformToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || sttLoading || terminalLoading || saving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || sttSaving || terminalSaving + return loading || runtimeLoading || compressionLoading || promptCachingLoading || auxiliaryLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || platformToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || sttLoading || terminalLoading || saving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || sttSaving || terminalSaving } function option(labelKey, value, selected) { @@ -363,7 +381,7 @@ export function render() { } function renderRuntimePanel() { - const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -461,7 +479,7 @@ export function render() { } function renderPromptCachingPanel() { - const disabled = loading || saving || promptCachingLoading || promptCachingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || promptCachingLoading || promptCachingSaving || runtimeSaving || compressionSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -490,8 +508,91 @@ export function render() { ` } + function auxiliaryProviderOptions(selected) { + return AUXILIARY_PROVIDERS + .map(provider => option(`engine.hermesAuxiliaryConfigProvider_${provider}`, provider, selected)) + .join('') + } + + function renderAuxiliaryConfigPanel() { + const disabled = loading || saving || auxiliaryLoading || auxiliarySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + return ` +
+
+
+
${t('engine.hermesAuxiliaryConfigTitle')}
+
${t('engine.hermesAuxiliaryConfigDesc')}
+
+
+ ${auxiliarySaving ? t('engine.hermesConfigStatusSaving') : auxiliaryLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesAuxiliaryConfigStatusReady')} + +
+
+
+ ${renderError(auxiliaryError)} +
${t('engine.hermesAuxiliaryConfigVisionTitle')}
+
+ + + + +
+
${t('engine.hermesAuxiliaryConfigWebExtractTitle')}
+
+ + +
+
${t('engine.hermesAuxiliaryConfigSessionSearchTitle')}
+
+ + + + +
+
${t('engine.hermesAuxiliaryConfigFootnote')}
+
+
+ ` + } + function renderToolGuardrailsPanel() { - const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || promptCachingSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -553,7 +654,7 @@ export function render() { } function renderMemoryPanel() { - const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -603,7 +704,7 @@ export function render() { } function renderSkillsConfigPanel() { - const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -635,7 +736,7 @@ export function render() { } function renderQuickCommandsConfigPanel() { - const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -661,7 +762,7 @@ export function render() { } function renderAgentToolsetsConfigPanel() { - const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -687,7 +788,7 @@ export function render() { } function renderPlatformToolsetsConfigPanel() { - const disabled = loading || saving || platformToolsetsLoading || platformToolsetsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || platformToolsetsLoading || platformToolsetsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -713,7 +814,7 @@ export function render() { } function renderAgentRuntimeConfigPanel() { - const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || platformToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving + const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || platformToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving return `
@@ -775,7 +876,7 @@ export function render() { } function renderUnauthorizedDmConfigPanel() { - const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -805,7 +906,7 @@ export function render() { } function renderSecurityConfigPanel() { - const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -847,7 +948,7 @@ export function render() { } function renderDisplayConfigPanel() { - const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -945,7 +1046,7 @@ export function render() { } function renderHumanDelayConfigPanel() { - const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -983,7 +1084,7 @@ export function render() { } function renderStreamingPanel() { - const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -1035,7 +1136,7 @@ export function render() { } function renderExecutionLimitsPanel() { - const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving + const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving return `
@@ -1107,7 +1208,7 @@ export function render() { } function renderIoSafetyPanel() { - const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -1147,7 +1248,7 @@ export function render() { } function renderCheckpointsPanel() { - const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -1205,7 +1306,7 @@ export function render() { } function renderCronPanel() { - const disabled = loading || saving || cronLoading || cronSaving || checkpointsSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || cronLoading || cronSaving || checkpointsSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -1239,7 +1340,7 @@ export function render() { } function renderLoggingPanel() { - const disabled = loading || saving || loggingLoading || loggingSaving || checkpointsSaving || cronSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || loggingLoading || loggingSaving || checkpointsSaving || cronSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -1287,7 +1388,7 @@ export function render() { } function renderApprovalsPanel() { - const disabled = loading || saving || approvalsLoading || approvalsSaving || checkpointsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || approvalsLoading || approvalsSaving || checkpointsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -1337,7 +1438,7 @@ export function render() { } function renderPrivacyPanel() { - const disabled = loading || saving || privacyLoading || privacySaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving + const disabled = loading || saving || privacyLoading || privacySaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1365,7 +1466,7 @@ export function render() { } function renderBrowserPanel() { - const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || sttSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving + const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || sttSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1409,7 +1510,7 @@ export function render() { } function renderSttPanel() { - const disabled = loading || saving || sttLoading || sttSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving + const disabled = loading || saving || sttLoading || sttSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1467,7 +1568,7 @@ export function render() { } function renderTerminalPanel() { - const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || sttSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving + const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || sttSaving || runtimeSaving || compressionSaving || promptCachingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving return `
@@ -1565,6 +1666,7 @@ export function render() { ${renderSttPanel()} ${renderCompressionPanel()} ${renderPromptCachingPanel()} + ${renderAuxiliaryConfigPanel()} ${renderToolGuardrailsPanel()} ${renderMemoryPanel()} ${renderSkillsConfigPanel()} @@ -1598,6 +1700,7 @@ export function render() { el.querySelector('#hm-runtime-save')?.addEventListener('click', saveRuntime) el.querySelector('#hm-compression-save')?.addEventListener('click', saveCompression) el.querySelector('#hm-prompt-caching-save')?.addEventListener('click', savePromptCaching) + el.querySelector('#hm-auxiliary-save')?.addEventListener('click', saveAuxiliaryConfig) el.querySelector('#hm-tool-guardrails-save')?.addEventListener('click', saveToolGuardrails) el.querySelector('#hm-memory-save')?.addEventListener('click', saveMemory) el.querySelector('#hm-skills-config-save')?.addEventListener('click', saveSkillsConfig) @@ -1642,6 +1745,11 @@ export function render() { promptCachingValues = { ...PROMPT_CACHING_DEFAULTS, ...(data?.values || {}) } } + async function loadAuxiliaryConfig() { + const data = await api.hermesAuxiliaryConfigRead() + auxiliaryValues = { ...AUXILIARY_DEFAULTS, ...(data?.values || {}) } + } + async function loadToolGuardrails() { const data = await api.hermesToolLoopGuardrailsConfigRead() toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS, ...(data?.values || {}) } @@ -1757,6 +1865,7 @@ export function render() { runtimeLoading = true compressionLoading = true promptCachingLoading = true + auxiliaryLoading = true toolGuardrailsLoading = true memoryLoading = true skillsLoading = true @@ -1783,6 +1892,7 @@ export function render() { runtimeError = null compressionError = null promptCachingError = null + auxiliaryError = null toolGuardrailsError = null memoryError = null skillsError = null @@ -1837,6 +1947,14 @@ export function render() { promptCachingLoading = false draw() } + try { + await loadAuxiliaryConfig() + } catch (err) { + auxiliaryError = humanizeError(err, t('engine.hermesAuxiliaryConfigLoadFailed') || 'Load auxiliary config failed') + } finally { + auxiliaryLoading = false + draw() + } try { await loadToolGuardrails() } catch (err) { @@ -2043,6 +2161,9 @@ export function render() { try { await loadPromptCaching() } catch {} + try { + await loadAuxiliaryConfig() + } catch {} try { await loadToolGuardrails() } catch {} @@ -2199,6 +2320,40 @@ export function render() { } } + async function saveAuxiliaryConfig() { + const form = { + auxiliaryVisionProvider: el.querySelector('#hm-auxiliary-vision-provider')?.value || 'auto', + auxiliaryVisionModel: el.querySelector('#hm-auxiliary-vision-model')?.value || '', + auxiliaryVisionTimeout: el.querySelector('#hm-auxiliary-vision-timeout')?.value || '30', + auxiliaryVisionDownloadTimeout: el.querySelector('#hm-auxiliary-vision-download-timeout')?.value || '30', + auxiliaryWebExtractProvider: el.querySelector('#hm-auxiliary-web-extract-provider')?.value || 'auto', + auxiliaryWebExtractModel: el.querySelector('#hm-auxiliary-web-extract-model')?.value || '', + auxiliarySessionSearchProvider: el.querySelector('#hm-auxiliary-session-search-provider')?.value || 'auto', + auxiliarySessionSearchModel: el.querySelector('#hm-auxiliary-session-search-model')?.value || '', + auxiliarySessionSearchTimeout: el.querySelector('#hm-auxiliary-session-search-timeout')?.value || '30', + auxiliarySessionSearchMaxConcurrency: el.querySelector('#hm-auxiliary-session-search-max-concurrency')?.value || '3', + } + auxiliarySaving = true + auxiliaryError = null + draw() + try { + const result = await api.hermesAuxiliaryConfigSave(form) + auxiliaryValues = { ...AUXILIARY_DEFAULTS, ...(result?.values || form) } + await refreshRawAfterStructuredSave() + const backup = result?.backup || '' + toast({ + message: t('engine.hermesAuxiliaryConfigSaveSuccess'), + hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '', + }, 'success') + } catch (err) { + auxiliaryError = humanizeError(err, t('engine.hermesAuxiliaryConfigSaveFailed') || 'Save auxiliary config failed') + toast(auxiliaryError, 'error') + } finally { + auxiliarySaving = false + draw() + } + } + async function saveToolGuardrails() { const form = { warningsEnabled: !!el.querySelector('#hm-tool-guardrails-warnings-enabled')?.checked, diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index e8b969a..a47f5ef 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -515,6 +515,8 @@ export const api = { hermesCompressionConfigSave: (form) => invoke('hermes_compression_config_save', { form }), hermesPromptCachingConfigRead: () => invoke('hermes_prompt_caching_config_read'), hermesPromptCachingConfigSave: (form) => invoke('hermes_prompt_caching_config_save', { form }), + hermesAuxiliaryConfigRead: () => invoke('hermes_auxiliary_config_read'), + hermesAuxiliaryConfigSave: (form) => invoke('hermes_auxiliary_config_save', { form }), hermesToolLoopGuardrailsConfigRead: () => invoke('hermes_tool_loop_guardrails_config_read'), hermesToolLoopGuardrailsConfigSave: (form) => invoke('hermes_tool_loop_guardrails_config_save', { form }), hermesMemoryConfigRead: () => invoke('hermes_memory_config_read'), diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 4ab6533..2a73e2b 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -715,6 +715,34 @@ export default { hermesPromptCachingConfigCacheTtl_5m: _('5 分钟(默认)', '5 minutes (default)', '5 分鐘(預設)'), hermesPromptCachingConfigCacheTtl_1h: _('1 小时(长跑)', '1 hour (long runs)', '1 小時(長跑)'), hermesPromptCachingConfigFootnote: _('这里写入 prompt_caching.cache_ttl。上游仅支持 5m 和 1h;其他 prompt_caching 高级字段会保留在 raw YAML 中。', 'This writes prompt_caching.cache_ttl. Upstream only supports 5m and 1h; other advanced prompt_caching fields stay in raw YAML.', '這裡寫入 prompt_caching.cache_ttl。上游僅支援 5m 和 1h;其他 prompt_caching 進階欄位會保留在 raw YAML 中。'), + hermesAuxiliaryConfigTitle: _('辅助模型', 'Auxiliary models', '輔助模型'), + hermesAuxiliaryConfigDesc: _('为图片分析、网页提取和历史会话搜索指定独立模型,避免这些任务挤占主对话模型。', 'Assign separate models for image analysis, web extraction, and session search so these tasks do not compete with the main chat model.', '為圖片分析、網頁提取和歷史會話搜尋指定獨立模型,避免這些任務擠占主對話模型。'), + hermesAuxiliaryConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), + hermesAuxiliaryConfigSave: _('保存辅助模型', 'Save auxiliary models', '儲存輔助模型'), + hermesAuxiliaryConfigSaveSuccess: _('辅助模型配置已保存,建议重启 Hermes Gateway 生效', 'Auxiliary model settings saved. Restart Hermes Gateway to take effect.', '輔助模型設定已儲存,建議重啟 Hermes Gateway 生效'), + hermesAuxiliaryConfigLoadFailed: _('加载辅助模型配置失败', 'Load auxiliary model settings failed', '載入輔助模型設定失敗'), + hermesAuxiliaryConfigSaveFailed: _('保存辅助模型配置失败', 'Save auxiliary model settings failed', '儲存輔助模型設定失敗'), + hermesAuxiliaryConfigVisionTitle: _('图片分析', 'Image analysis', '圖片分析'), + hermesAuxiliaryConfigVisionProvider: _('图片 provider', 'Image provider', '圖片 provider'), + hermesAuxiliaryConfigVisionModel: _('图片模型', 'Image model', '圖片模型'), + hermesAuxiliaryConfigVisionTimeout: _('图片分析超时(秒)', 'Image analysis timeout (sec)', '圖片分析逾時(秒)'), + hermesAuxiliaryConfigVisionDownloadTimeout: _('图片下载超时(秒)', 'Image download timeout (sec)', '圖片下載逾時(秒)'), + hermesAuxiliaryConfigWebExtractTitle: _('网页提取', 'Web extraction', '網頁提取'), + hermesAuxiliaryConfigWebExtractProvider: _('网页 provider', 'Web provider', '網頁 provider'), + hermesAuxiliaryConfigWebExtractModel: _('网页模型', 'Web model', '網頁模型'), + hermesAuxiliaryConfigSessionSearchTitle: _('会话搜索', 'Session search', '會話搜尋'), + hermesAuxiliaryConfigSessionSearchProvider: _('搜索 provider', 'Search provider', '搜尋 provider'), + hermesAuxiliaryConfigSessionSearchModel: _('搜索模型', 'Search model', '搜尋模型'), + hermesAuxiliaryConfigSessionSearchTimeout: _('搜索超时(秒)', 'Search timeout (sec)', '搜尋逾時(秒)'), + hermesAuxiliaryConfigSessionSearchMaxConcurrency: _('搜索并发数', 'Search concurrency', '搜尋並發數'), + hermesAuxiliaryConfigFootnote: _('这里写入 auxiliary.vision、auxiliary.web_extract 和 auxiliary.session_search。extra_body、自定义任务和未知字段会保留在 raw YAML 中。', 'This writes auxiliary.vision, auxiliary.web_extract, and auxiliary.session_search. extra_body, custom tasks, and unknown fields stay in raw YAML.', '這裡寫入 auxiliary.vision、auxiliary.web_extract 和 auxiliary.session_search。extra_body、自訂任務和未知欄位會保留在 raw YAML 中。'), + hermesAuxiliaryConfigProvider_auto: _('自动选择', 'Auto', '自動選擇'), + hermesAuxiliaryConfigProvider_openrouter: _('OpenRouter', 'OpenRouter', 'OpenRouter'), + hermesAuxiliaryConfigProvider_nous: _('Nous', 'Nous', 'Nous'), + hermesAuxiliaryConfigProvider_gemini: _('Gemini', 'Gemini', 'Gemini'), + 'hermesAuxiliaryConfigProvider_ollama-cloud': _('Ollama Cloud', 'Ollama Cloud', 'Ollama Cloud'), + hermesAuxiliaryConfigProvider_codex: _('Codex', 'Codex', 'Codex'), + hermesAuxiliaryConfigProvider_main: _('跟随主模型', 'Use main model', '跟隨主模型'), hermesToolGuardrailsTitle: _('工具循环防护', 'Tool loop guardrails', '工具循環防護'), hermesToolGuardrailsDesc: _('当 Agent 重复失败或反复执行无进展工具时,先给模型修正提示;开启硬停止后可主动中止失控循环。', 'Warn the model when tools repeat failures or make no progress. Enable hard stops to halt runaway loops before they spend the full turn budget.', '當 Agent 重複失敗或反覆執行無進展工具時,先給模型修正提示;啟用硬停止後可主動中止失控循環。'), hermesToolGuardrailsStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-auxiliary-config.test.js b/tests/hermes-auxiliary-config.test.js new file mode 100644 index 0000000..ebc6db8 --- /dev/null +++ b/tests/hermes-auxiliary-config.test.js @@ -0,0 +1,133 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildHermesAuxiliaryConfigValues, + mergeHermesAuxiliaryConfig, +} from '../scripts/dev-api.js' + +test('Hermes 辅助模型配置读取会提供上游默认值', () => { + const values = buildHermesAuxiliaryConfigValues({}) + + assert.deepEqual(values, { + auxiliaryVisionProvider: 'auto', + auxiliaryVisionModel: '', + auxiliaryVisionTimeout: 30, + auxiliaryVisionDownloadTimeout: 30, + auxiliaryWebExtractProvider: 'auto', + auxiliaryWebExtractModel: '', + auxiliarySessionSearchProvider: 'auto', + auxiliarySessionSearchModel: '', + auxiliarySessionSearchTimeout: 30, + auxiliarySessionSearchMaxConcurrency: 3, + }) +}) + +test('Hermes 辅助模型配置读取会回显 YAML 字段', () => { + const values = buildHermesAuxiliaryConfigValues({ + auxiliary: { + vision: { + provider: 'openrouter', + model: 'google/gemini-2.5-flash', + timeout: 45, + download_timeout: 60, + }, + web_extract: { + provider: 'main', + model: 'local-summary', + }, + session_search: { + provider: 'nous', + model: 'gemini-3-flash', + timeout: 50, + max_concurrency: 5, + }, + }, + }) + + assert.equal(values.auxiliaryVisionProvider, 'openrouter') + assert.equal(values.auxiliaryVisionModel, 'google/gemini-2.5-flash') + assert.equal(values.auxiliaryVisionTimeout, 45) + assert.equal(values.auxiliaryVisionDownloadTimeout, 60) + assert.equal(values.auxiliaryWebExtractProvider, 'main') + assert.equal(values.auxiliaryWebExtractModel, 'local-summary') + assert.equal(values.auxiliarySessionSearchProvider, 'nous') + assert.equal(values.auxiliarySessionSearchModel, 'gemini-3-flash') + assert.equal(values.auxiliarySessionSearchTimeout, 50) + assert.equal(values.auxiliarySessionSearchMaxConcurrency, 5) +}) + +test('Hermes 辅助模型配置保存会保留未知字段并写入上游结构', () => { + const next = mergeHermesAuxiliaryConfig({ + model: { provider: 'anthropic' }, + auxiliary: { + vision: { + provider: 'auto', + custom_flag: 'keep-vision', + }, + web_extract: { + custom_flag: 'keep-web', + }, + session_search: { + extra_body: { enable_thinking: false }, + custom_flag: 'keep-search', + }, + custom_task: { + provider: 'main', + }, + }, + streaming: { enabled: true }, + }, { + auxiliaryVisionProvider: 'codex', + auxiliaryVisionModel: 'gpt-5.3-codex', + auxiliaryVisionTimeout: '40', + auxiliaryVisionDownloadTimeout: '55', + auxiliaryWebExtractProvider: 'gemini', + auxiliaryWebExtractModel: 'gemini-3-flash', + auxiliarySessionSearchProvider: 'ollama-cloud', + auxiliarySessionSearchModel: 'gpt-oss:20b', + auxiliarySessionSearchTimeout: '70', + auxiliarySessionSearchMaxConcurrency: '6', + }) + + assert.deepEqual(next.model, { provider: 'anthropic' }) + assert.deepEqual(next.streaming, { enabled: true }) + assert.equal(next.auxiliary.vision.provider, 'codex') + assert.equal(next.auxiliary.vision.model, 'gpt-5.3-codex') + assert.equal(next.auxiliary.vision.timeout, 40) + assert.equal(next.auxiliary.vision.download_timeout, 55) + assert.equal(next.auxiliary.vision.custom_flag, 'keep-vision') + assert.equal(next.auxiliary.web_extract.provider, 'gemini') + assert.equal(next.auxiliary.web_extract.model, 'gemini-3-flash') + assert.equal(next.auxiliary.web_extract.custom_flag, 'keep-web') + assert.equal(next.auxiliary.session_search.provider, 'ollama-cloud') + assert.equal(next.auxiliary.session_search.model, 'gpt-oss:20b') + assert.equal(next.auxiliary.session_search.timeout, 70) + assert.equal(next.auxiliary.session_search.max_concurrency, 6) + assert.deepEqual(next.auxiliary.session_search.extra_body, { enable_thinking: false }) + assert.equal(next.auxiliary.session_search.custom_flag, 'keep-search') + assert.deepEqual(next.auxiliary.custom_task, { provider: 'main' }) +}) + +test('Hermes 辅助模型配置保存会拒绝非法 provider、模型名和越界值', () => { + assert.throws( + () => mergeHermesAuxiliaryConfig({}, { auxiliaryVisionProvider: 'bad-provider' }), + /auxiliary\.vision\.provider/, + ) + assert.throws( + () => mergeHermesAuxiliaryConfig({}, { auxiliaryVisionModel: '../secret' }), + /auxiliary\.vision\.model/, + ) + assert.throws( + () => mergeHermesAuxiliaryConfig({}, { auxiliaryVisionTimeout: '0' }), + /auxiliary\.vision\.timeout/, + ) + assert.throws( + () => mergeHermesAuxiliaryConfig({}, { auxiliaryVisionDownloadTimeout: '0' }), + /auxiliary\.vision\.download_timeout/, + ) + assert.throws( + () => mergeHermesAuxiliaryConfig({}, { auxiliarySessionSearchMaxConcurrency: '0' }), + /auxiliary\.session_search\.max_concurrency/, + ) +}) diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index e21f716..ca5ea11 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -150,6 +150,24 @@ test('Hermes 配置页会暴露提示缓存结构化配置字段', () => { } }) +test('Hermes 配置页会暴露辅助模型结构化配置字段', () => { + for (const id of [ + 'hm-auxiliary-save', + 'hm-auxiliary-vision-provider', + 'hm-auxiliary-vision-model', + 'hm-auxiliary-vision-timeout', + 'hm-auxiliary-vision-download-timeout', + 'hm-auxiliary-web-extract-provider', + 'hm-auxiliary-web-extract-model', + 'hm-auxiliary-session-search-provider', + 'hm-auxiliary-session-search-model', + 'hm-auxiliary-session-search-timeout', + 'hm-auxiliary-session-search-max-concurrency', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露网关流式结构化配置字段', () => { for (const id of [ 'hm-streaming-save', @@ -316,6 +334,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('HumanDelayConfig') || key.includes('DisplayConfig') || key.includes('PromptCachingConfig') || + key.includes('AuxiliaryConfig') || key.includes('StreamingConfig') || key.includes('ExecutionLimits') || key.includes('PrivacyConfig') ||