diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 5e3b7f6..9af0a33 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3325,6 +3325,7 @@ const HERMES_STREAMING_TRANSPORTS = new Set(['auto', 'draft', 'edit', 'off']) const HERMES_CODE_EXECUTION_MODES = new Set(['project', 'strict']) const HERMES_TERMINAL_BACKENDS = new Set(['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox']) const HERMES_BROWSER_ENGINES = new Set(['auto', 'lightpanda', 'chrome']) +const HERMES_AGENT_IMAGE_INPUT_MODES = new Set(['auto', 'native', 'text']) const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose']) const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false']) const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal']) @@ -3409,6 +3410,13 @@ function normalizeHermesBrowserEngine(value, strict = false) { return 'auto' } +function normalizeHermesImageInputMode(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'auto' + if (HERMES_AGENT_IMAGE_INPUT_MODES.has(mode)) return mode + if (strict) throw new Error('agent.image_input_mode 必须是 auto、native 或 text') + return 'auto' +} + 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 @@ -3733,6 +3741,43 @@ export function mergeHermesAgentToolsetsConfig(config = {}, form = {}) { return next } +export function buildHermesAgentRuntimeConfigValues(config = {}) { + const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} + const agent = root.agent && typeof root.agent === 'object' && !Array.isArray(root.agent) + ? root.agent + : {} + return { + agentMaxTurns: parseHermesInteger(agent.max_turns, 'agent.max_turns', 90, 1, 10000, false), + gatewayTimeout: parseHermesInteger(agent.gateway_timeout, 'agent.gateway_timeout', 1800, 0, 604800, false), + restartDrainTimeout: parseHermesInteger(agent.restart_drain_timeout, 'agent.restart_drain_timeout', 180, 0, 86400, false), + apiMaxRetries: parseHermesInteger(agent.api_max_retries, 'agent.api_max_retries', 3, 1, 20, false), + gatewayTimeoutWarning: parseHermesInteger(agent.gateway_timeout_warning, 'agent.gateway_timeout_warning', 900, 0, 604800, false), + clarifyTimeout: parseHermesInteger(agent.clarify_timeout, 'agent.clarify_timeout', 600, 0, 86400, false), + gatewayNotifyInterval: parseHermesInteger(agent.gateway_notify_interval, 'agent.gateway_notify_interval', 180, 0, 86400, false), + gatewayAutoContinueFreshness: parseHermesInteger(agent.gateway_auto_continue_freshness, 'agent.gateway_auto_continue_freshness', 3600, 0, 604800, false), + imageInputMode: normalizeHermesImageInputMode(agent.image_input_mode, false), + } +} + +export function mergeHermesAgentRuntimeConfig(config = {}, form = {}) { + const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {}) + const currentValues = buildHermesAgentRuntimeConfigValues(next) + const agent = next.agent && typeof next.agent === 'object' && !Array.isArray(next.agent) + ? mergeConfigsPreservingFields(next.agent, {}) + : {} + agent.max_turns = parseHermesInteger(Object.hasOwn(form, 'agentMaxTurns') ? form.agentMaxTurns : currentValues.agentMaxTurns, 'agent.max_turns', 90, 1, 10000, true) + agent.gateway_timeout = parseHermesInteger(Object.hasOwn(form, 'gatewayTimeout') ? form.gatewayTimeout : currentValues.gatewayTimeout, 'agent.gateway_timeout', 1800, 0, 604800, true) + agent.restart_drain_timeout = parseHermesInteger(Object.hasOwn(form, 'restartDrainTimeout') ? form.restartDrainTimeout : currentValues.restartDrainTimeout, 'agent.restart_drain_timeout', 180, 0, 86400, true) + agent.api_max_retries = parseHermesInteger(Object.hasOwn(form, 'apiMaxRetries') ? form.apiMaxRetries : currentValues.apiMaxRetries, 'agent.api_max_retries', 3, 1, 20, true) + agent.gateway_timeout_warning = parseHermesInteger(Object.hasOwn(form, 'gatewayTimeoutWarning') ? form.gatewayTimeoutWarning : currentValues.gatewayTimeoutWarning, 'agent.gateway_timeout_warning', 900, 0, 604800, true) + agent.clarify_timeout = parseHermesInteger(Object.hasOwn(form, 'clarifyTimeout') ? form.clarifyTimeout : currentValues.clarifyTimeout, 'agent.clarify_timeout', 600, 0, 86400, true) + agent.gateway_notify_interval = parseHermesInteger(Object.hasOwn(form, 'gatewayNotifyInterval') ? form.gatewayNotifyInterval : currentValues.gatewayNotifyInterval, 'agent.gateway_notify_interval', 180, 0, 86400, true) + agent.gateway_auto_continue_freshness = parseHermesInteger(Object.hasOwn(form, 'gatewayAutoContinueFreshness') ? form.gatewayAutoContinueFreshness : currentValues.gatewayAutoContinueFreshness, 'agent.gateway_auto_continue_freshness', 3600, 0, 604800, true) + agent.image_input_mode = normalizeHermesImageInputMode(Object.hasOwn(form, 'imageInputMode') ? form.imageInputMode : currentValues.imageInputMode, true) + next.agent = agent + return next +} + function validateHermesQuickCommands(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('quick_commands 必须是 JSON 对象') @@ -10493,6 +10538,27 @@ const handlers = { } }, + hermes_agent_runtime_config_read() { + const { configPath, exists, config } = readHermesConfigYamlObject() + return { + exists, + configPath, + values: buildHermesAgentRuntimeConfigValues(config), + } + }, + + hermes_agent_runtime_config_save({ form } = {}) { + const { configPath, config } = readHermesConfigYamlObject() + const next = mergeHermesAgentRuntimeConfig(config, form || {}) + const backup = writeHermesConfigYamlObject(configPath, next) + return { + ok: true, + configPath, + backup, + values: buildHermesAgentRuntimeConfigValues(next), + } + }, + hermes_unauthorized_dm_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 d46f69a..37f1052 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -3912,6 +3912,173 @@ fn merge_hermes_agent_toolsets_config( Ok(()) } +fn normalize_hermes_image_input_mode( + value: Option, + strict: bool, +) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "auto".to_string() + } else { + mode + }; + if matches!(mode.as_str(), "auto" | "native" | "text") { + return Ok(mode); + } + if strict { + Err("agent.image_input_mode 必须是 auto、native 或 text".to_string()) + } else { + Ok("auto".to_string()) + } +} + +fn build_hermes_agent_runtime_config_values(config: &serde_yaml::Value) -> Value { + let root = config.as_mapping(); + let agent = root.and_then(|map| yaml_get_mapping(map, "agent")); + + let image_input_mode = normalize_hermes_image_input_mode( + agent.and_then(|map| yaml_string_field(map, "image_input_mode")), + false, + ) + .unwrap_or_else(|_| "auto".to_string()); + + serde_json::json!({ + "agentMaxTurns": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_turns"), 90, 1, 10000)).unwrap_or(90), + "gatewayTimeout": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_timeout"), 1800, 0, 604800)).unwrap_or(1800), + "restartDrainTimeout": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "restart_drain_timeout"), 180, 0, 86400)).unwrap_or(180), + "apiMaxRetries": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "api_max_retries"), 3, 1, 20)).unwrap_or(3), + "gatewayTimeoutWarning": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_timeout_warning"), 900, 0, 604800)).unwrap_or(900), + "clarifyTimeout": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "clarify_timeout"), 600, 0, 86400)).unwrap_or(600), + "gatewayNotifyInterval": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_notify_interval"), 180, 0, 86400)).unwrap_or(180), + "gatewayAutoContinueFreshness": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_auto_continue_freshness"), 3600, 0, 604800)).unwrap_or(3600), + "imageInputMode": image_input_mode, + }) +} + +fn agent_runtime_i64_value( + form: &Value, + current: &Value, + form_key: &str, + default_value: i64, +) -> Option { + if form.get(form_key).is_some() { + form_i64(form, form_key) + } else { + Some(current[form_key].as_i64().unwrap_or(default_value)) + } +} + +fn merge_hermes_agent_runtime_config( + config: &mut serde_yaml::Value, + form: &Value, +) -> Result<(), String> { + let current = build_hermes_agent_runtime_config_values(config); + let agent_max_turns = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "agentMaxTurns", 90), + "agent.max_turns", + 90, + 1, + 10000, + )?; + let gateway_timeout = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "gatewayTimeout", 1800), + "agent.gateway_timeout", + 1800, + 0, + 604800, + )?; + let restart_drain_timeout = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "restartDrainTimeout", 180), + "agent.restart_drain_timeout", + 180, + 0, + 86400, + )?; + let api_max_retries = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "apiMaxRetries", 3), + "agent.api_max_retries", + 3, + 1, + 20, + )?; + let gateway_timeout_warning = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "gatewayTimeoutWarning", 900), + "agent.gateway_timeout_warning", + 900, + 0, + 604800, + )?; + let clarify_timeout = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "clarifyTimeout", 600), + "agent.clarify_timeout", + 600, + 0, + 86400, + )?; + let gateway_notify_interval = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "gatewayNotifyInterval", 180), + "agent.gateway_notify_interval", + 180, + 0, + 86400, + )?; + let gateway_auto_continue_freshness = validate_hermes_i64( + agent_runtime_i64_value(form, ¤t, "gatewayAutoContinueFreshness", 3600), + "agent.gateway_auto_continue_freshness", + 3600, + 0, + 604800, + )?; + let image_input_mode = normalize_hermes_image_input_mode( + if form.get("imageInputMode").is_some() { + form_string(form, "imageInputMode") + } else { + current["imageInputMode"].as_str().map(ToString::to_string) + }, + true, + )?; + + let root = ensure_yaml_object(config)?; + let agent = yaml_child_object(root, "agent")?; + agent.insert( + yaml_key("max_turns"), + serde_yaml::Value::Number(agent_max_turns.into()), + ); + agent.insert( + yaml_key("gateway_timeout"), + serde_yaml::Value::Number(gateway_timeout.into()), + ); + agent.insert( + yaml_key("restart_drain_timeout"), + serde_yaml::Value::Number(restart_drain_timeout.into()), + ); + agent.insert( + yaml_key("api_max_retries"), + serde_yaml::Value::Number(api_max_retries.into()), + ); + agent.insert( + yaml_key("gateway_timeout_warning"), + serde_yaml::Value::Number(gateway_timeout_warning.into()), + ); + agent.insert( + yaml_key("clarify_timeout"), + serde_yaml::Value::Number(clarify_timeout.into()), + ); + agent.insert( + yaml_key("gateway_notify_interval"), + serde_yaml::Value::Number(gateway_notify_interval.into()), + ); + agent.insert( + yaml_key("gateway_auto_continue_freshness"), + serde_yaml::Value::Number(gateway_auto_continue_freshness.into()), + ); + agent.insert( + yaml_key("image_input_mode"), + serde_yaml::Value::String(image_input_mode), + ); + Ok(()) +} + fn normalize_hermes_unauthorized_dm_behavior( value: Option, strict: bool, @@ -6149,6 +6316,30 @@ pub fn hermes_agent_toolsets_config_save(form: Value) -> Result { })) } +#[tauri::command] +pub fn hermes_agent_runtime_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_agent_runtime_config_values(&config), + })) +} + +#[tauri::command] +pub fn hermes_agent_runtime_config_save(form: Value) -> Result { + let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?; + merge_hermes_agent_runtime_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_agent_runtime_config_values(&config), + })) +} + #[tauri::command] pub fn hermes_unauthorized_dm_config_read() -> Result { let (config_path, exists, config) = read_hermes_channel_yaml_config()?; @@ -12707,6 +12898,161 @@ agent: } } +#[cfg(test)] +mod hermes_agent_runtime_config_tests { + use super::{build_hermes_agent_runtime_config_values, merge_hermes_agent_runtime_config}; + use serde_json::json; + + #[test] + fn agent_runtime_values_have_upstream_defaults() { + let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let values = build_hermes_agent_runtime_config_values(&config); + assert_eq!(values["agentMaxTurns"], 90); + assert_eq!(values["gatewayTimeout"], 1800); + assert_eq!(values["restartDrainTimeout"], 180); + assert_eq!(values["apiMaxRetries"], 3); + assert_eq!(values["gatewayTimeoutWarning"], 900); + assert_eq!(values["clarifyTimeout"], 600); + assert_eq!(values["gatewayNotifyInterval"], 180); + assert_eq!(values["gatewayAutoContinueFreshness"], 3600); + assert_eq!(values["imageInputMode"], "auto"); + } + + #[test] + fn agent_runtime_values_read_yaml_fields() { + let config: serde_yaml::Value = serde_yaml::from_str( + r#" +agent: + max_turns: 240 + gateway_timeout: 7200 + restart_drain_timeout: 600 + api_max_retries: 5 + gateway_timeout_warning: 1200 + clarify_timeout: 900 + gateway_notify_interval: 240 + gateway_auto_continue_freshness: 5400 + image_input_mode: native +"#, + ) + .unwrap(); + + let values = build_hermes_agent_runtime_config_values(&config); + assert_eq!(values["agentMaxTurns"], 240); + assert_eq!(values["gatewayTimeout"], 7200); + assert_eq!(values["restartDrainTimeout"], 600); + assert_eq!(values["apiMaxRetries"], 5); + assert_eq!(values["gatewayTimeoutWarning"], 1200); + assert_eq!(values["clarifyTimeout"], 900); + assert_eq!(values["gatewayNotifyInterval"], 240); + assert_eq!(values["gatewayAutoContinueFreshness"], 5400); + assert_eq!(values["imageInputMode"], "native"); + } + + #[test] + fn merge_agent_runtime_config_preserves_unrelated_yaml() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +model: + provider: anthropic +agent: + max_turns: 90 + disabled_toolsets: + - terminal + custom_flag: keep-agent +streaming: + enabled: true +"#, + ) + .unwrap(); + + merge_hermes_agent_runtime_config( + &mut config, + &json!({ + "agentMaxTurns": "180", + "gatewayTimeout": "3600", + "restartDrainTimeout": "300", + "apiMaxRetries": "2", + "gatewayTimeoutWarning": "600", + "clarifyTimeout": "300", + "gatewayNotifyInterval": "120", + "gatewayAutoContinueFreshness": "1800", + "imageInputMode": "text", + }), + ) + .unwrap(); + + assert_eq!(config["model"]["provider"].as_str(), Some("anthropic")); + assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true)); + assert_eq!(config["agent"]["max_turns"].as_i64(), Some(180)); + assert_eq!(config["agent"]["gateway_timeout"].as_i64(), Some(3600)); + assert_eq!(config["agent"]["restart_drain_timeout"].as_i64(), Some(300)); + assert_eq!(config["agent"]["api_max_retries"].as_i64(), Some(2)); + assert_eq!( + config["agent"]["gateway_timeout_warning"].as_i64(), + Some(600) + ); + assert_eq!(config["agent"]["clarify_timeout"].as_i64(), Some(300)); + assert_eq!( + config["agent"]["gateway_notify_interval"].as_i64(), + Some(120) + ); + assert_eq!( + config["agent"]["gateway_auto_continue_freshness"].as_i64(), + Some(1800) + ); + assert_eq!(config["agent"]["image_input_mode"].as_str(), Some("text")); + assert_eq!( + config["agent"]["disabled_toolsets"][0].as_str(), + Some("terminal") + ); + assert_eq!(config["agent"]["custom_flag"].as_str(), Some("keep-agent")); + } + + #[test] + fn merge_agent_runtime_config_allows_zero_disable_values() { + let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); + merge_hermes_agent_runtime_config( + &mut config, + &json!({ + "gatewayTimeout": "0", + "restartDrainTimeout": "0", + "gatewayTimeoutWarning": "0", + "gatewayNotifyInterval": "0", + "gatewayAutoContinueFreshness": "0", + }), + ) + .unwrap(); + + assert_eq!(config["agent"]["gateway_timeout"].as_i64(), Some(0)); + assert_eq!(config["agent"]["restart_drain_timeout"].as_i64(), Some(0)); + assert_eq!(config["agent"]["gateway_timeout_warning"].as_i64(), Some(0)); + assert_eq!(config["agent"]["gateway_notify_interval"].as_i64(), Some(0)); + assert_eq!( + config["agent"]["gateway_auto_continue_freshness"].as_i64(), + Some(0) + ); + } + + #[test] + fn merge_agent_runtime_config_rejects_invalid_values() { + let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); + let err = + merge_hermes_agent_runtime_config(&mut config, &json!({ "imageInputMode": "pixel" })) + .unwrap_err(); + assert!(err.contains("agent.image_input_mode")); + let err = merge_hermes_agent_runtime_config(&mut config, &json!({ "agentMaxTurns": "0" })) + .unwrap_err(); + assert!(err.contains("agent.max_turns")); + let err = merge_hermes_agent_runtime_config(&mut config, &json!({ "apiMaxRetries": "0" })) + .unwrap_err(); + assert!(err.contains("agent.api_max_retries")); + let err = + merge_hermes_agent_runtime_config(&mut config, &json!({ "clarifyTimeout": "-1" })) + .unwrap_err(); + assert!(err.contains("agent.clarify_timeout")); + } +} + #[cfg(test)] mod hermes_unauthorized_dm_config_tests { use super::{build_hermes_unauthorized_dm_config_values, merge_hermes_unauthorized_dm_config}; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9e750a9..a05e221 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -271,6 +271,8 @@ pub fn run() { hermes::hermes_quick_commands_config_save, hermes::hermes_agent_toolsets_config_read, hermes::hermes_agent_toolsets_config_save, + hermes::hermes_agent_runtime_config_read, + hermes::hermes_agent_runtime_config_save, hermes::hermes_unauthorized_dm_config_read, hermes::hermes_unauthorized_dm_config_save, hermes::hermes_security_config_read, diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index d5c3fb0..bf01d0e 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -56,6 +56,18 @@ const AGENT_TOOLSETS_DEFAULTS = { disabledToolsets: '', } +const AGENT_RUNTIME_DEFAULTS = { + agentMaxTurns: 90, + gatewayTimeout: 1800, + restartDrainTimeout: 180, + apiMaxRetries: 3, + gatewayTimeoutWarning: 900, + clarifyTimeout: 600, + gatewayNotifyInterval: 180, + gatewayAutoContinueFreshness: 3600, + imageInputMode: 'auto', +} + const UNAUTHORIZED_DM_DEFAULTS = { unauthorizedDmBehavior: 'pair', } @@ -143,6 +155,7 @@ const CODE_EXECUTION_MODES = ['project', 'strict'] const TERMINAL_BACKENDS = ['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox'] const BROWSER_ENGINES = ['auto', 'lightpanda', 'chrome'] const UNAUTHORIZED_DM_BEHAVIORS = ['pair', 'ignore'] +const IMAGE_INPUT_MODES = ['auto', 'native', 'text'] const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose'] const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu'] const DISPLAY_RESUME_VALUES = ['full', 'minimal'] @@ -160,6 +173,7 @@ export function render() { let skillsValues = { ...SKILLS_DEFAULTS } let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS } let agentToolsetsValues = { ...AGENT_TOOLSETS_DEFAULTS } + let agentRuntimeValues = { ...AGENT_RUNTIME_DEFAULTS } let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS } let securityValues = { ...SECURITY_DEFAULTS } let displayValues = { ...DISPLAY_DEFAULTS } @@ -178,6 +192,7 @@ export function render() { let skillsLoading = true let quickCommandsLoading = true let agentToolsetsLoading = true + let agentRuntimeLoading = true let unauthorizedDmLoading = true let securityLoading = true let displayLoading = true @@ -196,6 +211,7 @@ export function render() { let skillsSaving = false let quickCommandsSaving = false let agentToolsetsSaving = false + let agentRuntimeSaving = false let unauthorizedDmSaving = false let securitySaving = false let displaySaving = false @@ -214,6 +230,7 @@ export function render() { let skillsError = null let quickCommandsError = null let agentToolsetsError = null + let agentRuntimeError = null let unauthorizedDmError = null let securityError = null let displayError = null @@ -234,7 +251,7 @@ export function render() { } function isBusy() { - return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving + return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving } function option(labelKey, value, selected) { @@ -251,7 +268,7 @@ export function render() { } function renderRuntimePanel() { - const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -299,7 +316,7 @@ export function render() { } function renderCompressionPanel() { - const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -349,7 +366,7 @@ export function render() { } function renderToolGuardrailsPanel() { - const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -411,7 +428,7 @@ export function render() { } function renderMemoryPanel() { - const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -461,7 +478,7 @@ export function render() { } function renderSkillsConfigPanel() { - const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -493,7 +510,7 @@ export function render() { } function renderQuickCommandsConfigPanel() { - const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -519,7 +536,7 @@ export function render() { } function renderAgentToolsetsConfigPanel() { - const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -544,8 +561,70 @@ export function render() { ` } + function renderAgentRuntimeConfigPanel() { + const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving + return ` +
+
+
+
${t('engine.hermesAgentRuntimeConfigTitle')}
+
${t('engine.hermesAgentRuntimeConfigDesc')}
+
+
+ ${agentRuntimeSaving ? t('engine.hermesConfigStatusSaving') : agentRuntimeLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesAgentRuntimeConfigStatusReady')} + +
+
+
+ ${renderError(agentRuntimeError)} +
+ + + + + + + + + +
+
${t('engine.hermesAgentRuntimeConfigFootnote')}
+
+
+ ` + } + function renderUnauthorizedDmConfigPanel() { - const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -575,7 +654,7 @@ export function render() { } function renderSecurityConfigPanel() { - const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -617,7 +696,7 @@ export function render() { } function renderDisplayConfigPanel() { - const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -681,7 +760,7 @@ export function render() { } function renderHumanDelayConfigPanel() { - const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -719,7 +798,7 @@ export function render() { } function renderStreamingPanel() { - const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving return `
@@ -771,7 +850,7 @@ export function render() { } function renderExecutionLimitsPanel() { - const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving + const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving return `
@@ -843,7 +922,7 @@ export function render() { } function renderIoSafetyPanel() { - const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -883,7 +962,7 @@ export function render() { } function renderPrivacyPanel() { - const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -911,7 +990,7 @@ export function render() { } function renderBrowserPanel() { - const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -955,7 +1034,7 @@ export function render() { } function renderTerminalPanel() { - const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -1052,6 +1131,7 @@ export function render() { ${renderSkillsConfigPanel()} ${renderQuickCommandsConfigPanel()} ${renderAgentToolsetsConfigPanel()} + ${renderAgentRuntimeConfigPanel()} ${renderUnauthorizedDmConfigPanel()} ${renderSecurityConfigPanel()} ${renderDisplayConfigPanel()} @@ -1082,6 +1162,7 @@ export function render() { el.querySelector('#hm-skills-config-save')?.addEventListener('click', saveSkillsConfig) el.querySelector('#hm-quick-commands-save')?.addEventListener('click', saveQuickCommandsConfig) el.querySelector('#hm-agent-toolsets-save')?.addEventListener('click', saveAgentToolsetsConfig) + el.querySelector('#hm-agent-runtime-save')?.addEventListener('click', saveAgentRuntimeConfig) el.querySelector('#hm-unauthorized-dm-save')?.addEventListener('click', saveUnauthorizedDmConfig) el.querySelector('#hm-security-save')?.addEventListener('click', saveSecurityConfig) el.querySelector('#hm-display-save')?.addEventListener('click', saveDisplayConfig) @@ -1134,6 +1215,11 @@ export function render() { agentToolsetsValues = { ...AGENT_TOOLSETS_DEFAULTS, ...(data?.values || {}) } } + async function loadAgentRuntimeConfig() { + const data = await api.hermesAgentRuntimeConfigRead() + agentRuntimeValues = { ...AGENT_RUNTIME_DEFAULTS, ...(data?.values || {}) } + } + async function loadUnauthorizedDmConfig() { const data = await api.hermesUnauthorizedDmConfigRead() unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS, ...(data?.values || {}) } @@ -1193,6 +1279,7 @@ export function render() { skillsLoading = true quickCommandsLoading = true agentToolsetsLoading = true + agentRuntimeLoading = true unauthorizedDmLoading = true securityLoading = true displayLoading = true @@ -1211,6 +1298,7 @@ export function render() { skillsError = null quickCommandsError = null agentToolsetsError = null + agentRuntimeError = null unauthorizedDmError = null securityError = null displayError = null @@ -1333,6 +1421,14 @@ export function render() { agentToolsetsLoading = false draw() } + try { + await loadAgentRuntimeConfig() + } catch (err) { + agentRuntimeError = humanizeError(err, t('engine.hermesAgentRuntimeConfigLoadFailed') || 'Load agent runtime config failed') + } finally { + agentRuntimeLoading = false + draw() + } try { await loadUnauthorizedDmConfig() } catch (err) { @@ -1407,6 +1503,9 @@ export function render() { try { await loadAgentToolsetsConfig() } catch {} + try { + await loadAgentRuntimeConfig() + } catch {} try { await loadUnauthorizedDmConfig() } catch {} @@ -1643,6 +1742,39 @@ export function render() { } } + async function saveAgentRuntimeConfig() { + const form = { + agentMaxTurns: el.querySelector('#hm-agent-max-turns')?.value || '90', + gatewayTimeout: el.querySelector('#hm-agent-gateway-timeout')?.value || '1800', + restartDrainTimeout: el.querySelector('#hm-agent-restart-drain-timeout')?.value || '180', + apiMaxRetries: el.querySelector('#hm-agent-api-max-retries')?.value || '3', + gatewayTimeoutWarning: el.querySelector('#hm-agent-gateway-timeout-warning')?.value || '900', + clarifyTimeout: el.querySelector('#hm-agent-clarify-timeout')?.value || '600', + gatewayNotifyInterval: el.querySelector('#hm-agent-gateway-notify-interval')?.value || '180', + gatewayAutoContinueFreshness: el.querySelector('#hm-agent-gateway-auto-continue-freshness')?.value || '3600', + imageInputMode: el.querySelector('#hm-agent-image-input-mode')?.value || 'auto', + } + agentRuntimeSaving = true + agentRuntimeError = null + draw() + try { + const result = await api.hermesAgentRuntimeConfigSave(form) + agentRuntimeValues = { ...AGENT_RUNTIME_DEFAULTS, ...(result?.values || form) } + await refreshRawAfterStructuredSave() + const backup = result?.backup || '' + toast({ + message: t('engine.hermesAgentRuntimeConfigSaveSuccess'), + hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '', + }, 'success') + } catch (err) { + agentRuntimeError = humanizeError(err, t('engine.hermesAgentRuntimeConfigSaveFailed') || 'Save agent runtime config failed') + toast(agentRuntimeError, 'error') + } finally { + agentRuntimeSaving = false + draw() + } + } + async function saveUnauthorizedDmConfig() { const form = { unauthorizedDmBehavior: el.querySelector('#hm-unauthorized-dm-behavior')?.value || 'pair', diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index df41da8..5624442 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -523,6 +523,8 @@ export const api = { hermesQuickCommandsConfigSave: (form) => invoke('hermes_quick_commands_config_save', { form }), hermesAgentToolsetsConfigRead: () => invoke('hermes_agent_toolsets_config_read'), hermesAgentToolsetsConfigSave: (form) => invoke('hermes_agent_toolsets_config_save', { form }), + hermesAgentRuntimeConfigRead: () => invoke('hermes_agent_runtime_config_read'), + hermesAgentRuntimeConfigSave: (form) => invoke('hermes_agent_runtime_config_save', { form }), hermesUnauthorizedDmConfigRead: () => invoke('hermes_unauthorized_dm_config_read'), hermesUnauthorizedDmConfigSave: (form) => invoke('hermes_unauthorized_dm_config_save', { form }), hermesSecurityConfigRead: () => invoke('hermes_security_config_read'), diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 107101a..8946db4 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -674,6 +674,26 @@ export default { hermesAgentToolsetsConfigSaveFailed: _('保存全局工具集配置失败', 'Save global toolset settings failed', '儲存全域工具集設定失敗'), hermesAgentToolsetsConfigDisabledToolsets: _('禁用工具集(每行一个)', 'Disabled toolsets, one per line', '停用工具集(每行一個)'), hermesAgentToolsetsConfigFootnote: _('常见值包括 terminal、browser、memory、web。该设置会覆盖平台级工具配置;留空表示不做全局禁用。高级 agent 字段会保留在 raw YAML 中。', 'Common values include terminal, browser, memory, and web. This setting overrides platform-level tool configuration; leave it empty for no global disables. Advanced agent fields stay in raw YAML.', '常見值包括 terminal、browser、memory、web。此設定會覆蓋平台級工具設定;留空表示不做全域停用。進階 agent 欄位會保留在 raw YAML 中。'), + hermesAgentRuntimeConfigTitle: _('Agent 长跑保护', 'Agent runtime guards', 'Agent 長跑保護'), + hermesAgentRuntimeConfigDesc: _('控制 Agent 轮次上限、Gateway 等待、重启排水、重试、超时预警、澄清等待和自动续跑新鲜度,减少长时间任务无人值守失控。', 'Control turn limits, Gateway waits, restart drain, retries, timeout warnings, clarification waits, and auto-continue freshness to keep unattended long runs bounded.', '控制 Agent 輪次上限、Gateway 等待、重啟排水、重試、逾時預警、澄清等待和自動續跑新鮮度,減少長時間任務無人值守失控。'), + hermesAgentRuntimeConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), + hermesAgentRuntimeConfigSave: _('保存长跑保护', 'Save runtime guards', '儲存長跑保護'), + hermesAgentRuntimeConfigSaveSuccess: _('Agent 长跑保护配置已保存,建议重启 Hermes Gateway 生效', 'Agent runtime guard settings saved. Restart Hermes Gateway to take effect.', 'Agent 長跑保護設定已儲存,建議重啟 Hermes Gateway 生效'), + hermesAgentRuntimeConfigLoadFailed: _('加载 Agent 长跑保护配置失败', 'Load agent runtime guard settings failed', '載入 Agent 長跑保護設定失敗'), + hermesAgentRuntimeConfigSaveFailed: _('保存 Agent 长跑保护配置失败', 'Save agent runtime guard settings failed', '儲存 Agent 長跑保護設定失敗'), + hermesAgentRuntimeConfigMaxTurns: _('单次运行最大轮数', 'Max turns per run', '單次執行最大輪數'), + hermesAgentRuntimeConfigGatewayTimeout: _('Gateway 等待超时(秒)', 'Gateway timeout (seconds)', 'Gateway 等待逾時(秒)'), + hermesAgentRuntimeConfigRestartDrainTimeout: _('重启排水等待(秒)', 'Restart drain timeout (seconds)', '重啟排水等待(秒)'), + hermesAgentRuntimeConfigApiMaxRetries: _('API 最大重试次数', 'API max retries', 'API 最大重試次數'), + hermesAgentRuntimeConfigGatewayTimeoutWarning: _('超时预警阈值(秒)', 'Timeout warning threshold (seconds)', '逾時預警閾值(秒)'), + hermesAgentRuntimeConfigClarifyTimeout: _('澄清等待超时(秒)', 'Clarification timeout (seconds)', '澄清等待逾時(秒)'), + hermesAgentRuntimeConfigGatewayNotifyInterval: _('Gateway 心跳通知间隔(秒)', 'Gateway notify interval (seconds)', 'Gateway 心跳通知間隔(秒)'), + hermesAgentRuntimeConfigGatewayAutoContinueFreshness: _('自动续跑新鲜度(秒)', 'Auto-continue freshness (seconds)', '自動續跑新鮮度(秒)'), + hermesAgentRuntimeConfigImageInputMode: _('图片输入模式', 'Image input mode', '圖片輸入模式'), + hermesAgentRuntimeConfigImageInputMode_auto: _('自动选择', 'Auto', '自動選擇'), + hermesAgentRuntimeConfigImageInputMode_native: _('原生图片输入', 'Native image input', '原生圖片輸入'), + hermesAgentRuntimeConfigImageInputMode_text: _('转文本描述', 'Convert to text description', '轉文字描述'), + hermesAgentRuntimeConfigFootnote: _('这些字段会写入 agent.*,影响 CLI 与 Gateway 长跑行为。将可选超时设为 0 表示关闭对应限制或通知;disabled_toolsets 和其他高级 agent 字段会保留在 raw YAML 中。', 'These fields are written under agent.* and affect CLI and Gateway long-running behavior. Set optional timeouts to 0 to disable the corresponding limit or notification. disabled_toolsets and other advanced agent fields stay in raw YAML.', '這些欄位會寫入 agent.*,影響 CLI 與 Gateway 長跑行為。將可選逾時設為 0 表示關閉對應限制或通知;disabled_toolsets 和其他進階 agent 欄位會保留在 raw YAML 中。'), hermesUnauthorizedDmConfigTitle: _('未授权私信', 'Unauthorized DMs', '未授權私訊'), hermesUnauthorizedDmConfigDesc: _('控制陌生用户直接私信 Hermes 时的全局响应策略,适合公网部署时减少无效打扰或保留配对入口。', 'Control the global response when unknown users send Hermes a direct message. Useful for public deployments that need fewer unsolicited replies or a pairing entry point.', '控制陌生使用者直接私訊 Hermes 時的全域回應策略,適合公開部署時減少無效打擾或保留配對入口。'), hermesUnauthorizedDmConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-agent-runtime-config.test.js b/tests/hermes-agent-runtime-config.test.js new file mode 100644 index 0000000..abbc1e8 --- /dev/null +++ b/tests/hermes-agent-runtime-config.test.js @@ -0,0 +1,120 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildHermesAgentRuntimeConfigValues, + mergeHermesAgentRuntimeConfig, +} from '../scripts/dev-api.js' + +test('Hermes Agent 长跑保护配置读取会提供上游默认值', () => { + const values = buildHermesAgentRuntimeConfigValues({}) + + assert.deepEqual(values, { + agentMaxTurns: 90, + gatewayTimeout: 1800, + restartDrainTimeout: 180, + apiMaxRetries: 3, + gatewayTimeoutWarning: 900, + clarifyTimeout: 600, + gatewayNotifyInterval: 180, + gatewayAutoContinueFreshness: 3600, + imageInputMode: 'auto', + }) +}) + +test('Hermes Agent 长跑保护配置读取会回显 YAML 字段', () => { + const values = buildHermesAgentRuntimeConfigValues({ + agent: { + max_turns: 240, + gateway_timeout: 7200, + restart_drain_timeout: 600, + api_max_retries: 5, + gateway_timeout_warning: 1200, + clarify_timeout: 900, + gateway_notify_interval: 240, + gateway_auto_continue_freshness: 5400, + image_input_mode: 'native', + }, + }) + + assert.equal(values.agentMaxTurns, 240) + assert.equal(values.gatewayTimeout, 7200) + assert.equal(values.restartDrainTimeout, 600) + assert.equal(values.apiMaxRetries, 5) + assert.equal(values.gatewayTimeoutWarning, 1200) + assert.equal(values.clarifyTimeout, 900) + assert.equal(values.gatewayNotifyInterval, 240) + assert.equal(values.gatewayAutoContinueFreshness, 5400) + assert.equal(values.imageInputMode, 'native') +}) + +test('Hermes Agent 长跑保护配置保存会保留未知字段并写入 agent', () => { + const next = mergeHermesAgentRuntimeConfig({ + model: { provider: 'anthropic' }, + agent: { + max_turns: 90, + disabled_toolsets: ['terminal'], + custom_flag: 'keep-agent', + }, + streaming: { enabled: true }, + }, { + agentMaxTurns: '180', + gatewayTimeout: '3600', + restartDrainTimeout: '300', + apiMaxRetries: '2', + gatewayTimeoutWarning: '600', + clarifyTimeout: '300', + gatewayNotifyInterval: '120', + gatewayAutoContinueFreshness: '1800', + imageInputMode: 'text', + }) + + assert.deepEqual(next.model, { provider: 'anthropic' }) + assert.deepEqual(next.streaming, { enabled: true }) + assert.equal(next.agent.max_turns, 180) + assert.equal(next.agent.gateway_timeout, 3600) + assert.equal(next.agent.restart_drain_timeout, 300) + assert.equal(next.agent.api_max_retries, 2) + assert.equal(next.agent.gateway_timeout_warning, 600) + assert.equal(next.agent.clarify_timeout, 300) + assert.equal(next.agent.gateway_notify_interval, 120) + assert.equal(next.agent.gateway_auto_continue_freshness, 1800) + assert.equal(next.agent.image_input_mode, 'text') + assert.deepEqual(next.agent.disabled_toolsets, ['terminal']) + assert.equal(next.agent.custom_flag, 'keep-agent') +}) + +test('Hermes Agent 长跑保护配置保存允许 0 表示关闭或无限制', () => { + const next = mergeHermesAgentRuntimeConfig({}, { + gatewayTimeout: '0', + restartDrainTimeout: '0', + gatewayTimeoutWarning: '0', + gatewayNotifyInterval: '0', + gatewayAutoContinueFreshness: '0', + }) + + assert.equal(next.agent.gateway_timeout, 0) + assert.equal(next.agent.restart_drain_timeout, 0) + assert.equal(next.agent.gateway_timeout_warning, 0) + assert.equal(next.agent.gateway_notify_interval, 0) + assert.equal(next.agent.gateway_auto_continue_freshness, 0) +}) + +test('Hermes Agent 长跑保护配置保存会拒绝非法枚举和越界值', () => { + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { imageInputMode: 'pixel' }), + /agent\.image_input_mode/, + ) + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { agentMaxTurns: '0' }), + /agent\.max_turns/, + ) + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { apiMaxRetries: '0' }), + /agent\.api_max_retries/, + ) + assert.throws( + () => mergeHermesAgentRuntimeConfig({}, { clarifyTimeout: '-1' }), + /agent\.clarify_timeout/, + ) +}) diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index c3202ac..7313751 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -67,6 +67,23 @@ test('Hermes 配置页会暴露全局禁用工具集结构化配置字段', () = } }) +test('Hermes 配置页会暴露 Agent 长跑保护结构化配置字段', () => { + for (const id of [ + 'hm-agent-runtime-save', + 'hm-agent-max-turns', + 'hm-agent-gateway-timeout', + 'hm-agent-restart-drain-timeout', + 'hm-agent-api-max-retries', + 'hm-agent-gateway-timeout-warning', + 'hm-agent-clarify-timeout', + 'hm-agent-gateway-notify-interval', + 'hm-agent-gateway-auto-continue-freshness', + 'hm-agent-image-input-mode', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露未授权 DM 全局策略字段', () => { for (const id of [ 'hm-unauthorized-dm-save', @@ -209,6 +226,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('SkillsConfig') || key.includes('QuickCommandsConfig') || key.includes('AgentToolsetsConfig') || + key.includes('AgentRuntimeConfig') || key.includes('UnauthorizedDmConfig') || key.includes('SecurityConfig') || key.includes('HumanDelayConfig') ||