diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 5ed498e..f311bed 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3329,6 +3329,7 @@ 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']) const HERMES_AGENT_IMAGE_INPUT_MODES = new Set(['auto', 'native', 'text']) +const HERMES_PROMPT_CACHE_TTLS = new Set(['5m', '1h']) 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']) @@ -3444,6 +3445,13 @@ function normalizeHermesImageInputMode(value, strict = false) { return 'auto' } +function normalizeHermesPromptCacheTtl(value, strict = false) { + const ttl = String(value ?? '').trim().toLowerCase() || '5m' + if (HERMES_PROMPT_CACHE_TTLS.has(ttl)) return ttl + if (strict) throw new Error('prompt_caching.cache_ttl 必须是 5m 或 1h') + return '5m' +} + 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 @@ -3645,6 +3653,27 @@ export function mergeHermesCompressionConfig(config = {}, form = {}) { return next } +export function buildHermesPromptCachingConfigValues(config = {}) { + const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} + const promptCaching = root.prompt_caching && typeof root.prompt_caching === 'object' && !Array.isArray(root.prompt_caching) + ? root.prompt_caching + : {} + return { + promptCacheTtl: normalizeHermesPromptCacheTtl(promptCaching.cache_ttl, false), + } +} + +export function mergeHermesPromptCachingConfig(config = {}, form = {}) { + const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {}) + const currentValues = buildHermesPromptCachingConfigValues(next) + const promptCaching = next.prompt_caching && typeof next.prompt_caching === 'object' && !Array.isArray(next.prompt_caching) + ? mergeConfigsPreservingFields(next.prompt_caching, {}) + : {} + promptCaching.cache_ttl = normalizeHermesPromptCacheTtl(Object.hasOwn(form, 'promptCacheTtl') ? form.promptCacheTtl : currentValues.promptCacheTtl, true) + next.prompt_caching = promptCaching + 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) @@ -10623,6 +10652,27 @@ const handlers = { } }, + hermes_prompt_caching_config_read() { + const { configPath, exists, config } = readHermesConfigYamlObject() + return { + exists, + configPath, + values: buildHermesPromptCachingConfigValues(config), + } + }, + + hermes_prompt_caching_config_save({ form } = {}) { + const { configPath, config } = readHermesConfigYamlObject() + const next = mergeHermesPromptCachingConfig(config, form || {}) + const backup = writeHermesConfigYamlObject(configPath, next) + return { + ok: true, + configPath, + backup, + values: buildHermesPromptCachingConfigValues(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 11aee45..b6f82a8 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2167,6 +2167,7 @@ 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"]; fn normalize_hermes_channel_platform(platform: &str) -> Option<&'static str> { let platform = platform.trim().to_ascii_lowercase(); @@ -2248,6 +2249,25 @@ fn normalize_hermes_display_streaming_json( normalize_hermes_display_streaming_text(None, strict, key) } +fn normalize_hermes_prompt_cache_ttl( + value: Option, + strict: bool, +) -> Result { + let ttl = value.unwrap_or_default().trim().to_ascii_lowercase(); + let ttl = if ttl.is_empty() { + "5m".to_string() + } else { + ttl + }; + if HERMES_PROMPT_CACHE_TTLS.contains(&ttl.as_str()) { + Ok(ttl) + } else if strict { + Err("prompt_caching.cache_ttl 必须是 5m 或 1h".to_string()) + } else { + Ok("5m".to_string()) + } +} + fn yaml_key(key: &str) -> serde_yaml::Value { serde_yaml::Value::String(key.to_string()) } @@ -3416,6 +3436,34 @@ fn merge_hermes_compression_config( Ok(()) } +fn build_hermes_prompt_caching_config_values(config: &serde_yaml::Value) -> Value { + let root = config.as_mapping(); + let prompt_caching = root.and_then(|map| yaml_get_mapping(map, "prompt_caching")); + serde_json::json!({ + "promptCacheTtl": normalize_hermes_prompt_cache_ttl( + prompt_caching.and_then(|map| yaml_string_field(map, "cache_ttl")), + false, + ).unwrap_or_else(|_| "5m".to_string()), + }) +} + +fn merge_hermes_prompt_caching_config( + config: &mut serde_yaml::Value, + form: &Value, +) -> Result<(), String> { + let current = build_hermes_prompt_caching_config_values(config); + let cache_ttl = normalize_hermes_prompt_cache_ttl( + form_string(form, "promptCacheTtl") + .or_else(|| current["promptCacheTtl"].as_str().map(ToString::to_string)), + true, + )?; + + let root = ensure_yaml_object(config)?; + let prompt_caching = yaml_child_object(root, "prompt_caching")?; + prompt_caching.insert(yaml_key("cache_ttl"), serde_yaml::Value::String(cache_ttl)); + 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")); @@ -6798,6 +6846,30 @@ pub fn hermes_compression_config_save(form: Value) -> Result { })) } +#[tauri::command] +pub fn hermes_prompt_caching_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_prompt_caching_config_values(&config), + })) +} + +#[tauri::command] +pub fn hermes_prompt_caching_config_save(form: Value) -> Result { + let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?; + merge_hermes_prompt_caching_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_prompt_caching_config_values(&config), + })) +} + #[tauri::command] pub fn hermes_tool_loop_guardrails_config_read() -> Result { let (config_path, exists, config) = read_hermes_channel_yaml_config()?; @@ -12335,6 +12407,74 @@ streaming: } } +#[cfg(test)] +mod hermes_prompt_caching_config_tests { + use super::{build_hermes_prompt_caching_config_values, merge_hermes_prompt_caching_config}; + use serde_json::json; + + #[test] + fn prompt_caching_values_have_upstream_defaults() { + let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let values = build_hermes_prompt_caching_config_values(&config); + assert_eq!(values["promptCacheTtl"], "5m"); + } + + #[test] + fn prompt_caching_values_normalize_existing_ttl() { + let config: serde_yaml::Value = serde_yaml::from_str( + r#" +prompt_caching: + cache_ttl: "1H" +"#, + ) + .unwrap(); + + let values = build_hermes_prompt_caching_config_values(&config); + assert_eq!(values["promptCacheTtl"], "1h"); + } + + #[test] + fn merge_prompt_caching_config_preserves_unrelated_yaml() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +model: + provider: anthropic +prompt_caching: + cache_ttl: 5m + custom_flag: keep-prompt-cache +compression: + enabled: true +"#, + ) + .unwrap(); + + merge_hermes_prompt_caching_config( + &mut config, + &json!({ + "promptCacheTtl": "1h", + }), + ) + .unwrap(); + + assert_eq!(config["model"]["provider"].as_str(), Some("anthropic")); + assert_eq!(config["compression"]["enabled"].as_bool(), Some(true)); + assert_eq!(config["prompt_caching"]["cache_ttl"].as_str(), Some("1h")); + assert_eq!( + config["prompt_caching"]["custom_flag"].as_str(), + Some("keep-prompt-cache") + ); + } + + #[test] + fn merge_prompt_caching_config_rejects_invalid_ttl() { + let mut config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let err = + merge_hermes_prompt_caching_config(&mut config, &json!({ "promptCacheTtl": "30m" })) + .unwrap_err(); + assert!(err.contains("prompt_caching.cache_ttl")); + } +} + #[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 8856dab..0f9924b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -261,6 +261,8 @@ pub fn run() { hermes::hermes_session_runtime_config_save, hermes::hermes_compression_config_read, hermes::hermes_compression_config_save, + hermes::hermes_prompt_caching_config_read, + hermes::hermes_prompt_caching_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 93d58ca..c9851af 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -23,6 +23,10 @@ const COMPRESSION_DEFAULTS = { abortOnSummaryFailure: false, } +const PROMPT_CACHING_DEFAULTS = { + promptCacheTtl: '5m', +} + const TOOL_GUARDRAILS_DEFAULTS = { warningsEnabled: true, hardStopEnabled: false, @@ -205,6 +209,7 @@ const HUMAN_DELAY_MODES = ['off', 'natural', 'custom'] const APPROVAL_MODES = ['manual', 'smart', 'off'] const APPROVAL_CRON_MODES = ['deny', 'approve'] const LOGGING_LEVELS = ['DEBUG', 'INFO', 'WARNING'] +const PROMPT_CACHE_TTLS = ['5m', '1h'] export function render() { const el = document.createElement('div') @@ -213,6 +218,7 @@ export function render() { let yaml = '' let runtimeValues = { ...SESSION_RUNTIME_DEFAULTS } let compressionValues = { ...COMPRESSION_DEFAULTS } + let promptCachingValues = { ...PROMPT_CACHING_DEFAULTS } let toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS } let memoryValues = { ...MEMORY_DEFAULTS } let skillsValues = { ...SKILLS_DEFAULTS } @@ -236,6 +242,7 @@ export function render() { let loading = true let runtimeLoading = true let compressionLoading = true + let promptCachingLoading = true let toolGuardrailsLoading = true let memoryLoading = true let skillsLoading = true @@ -259,6 +266,7 @@ export function render() { let saving = false let runtimeSaving = false let compressionSaving = false + let promptCachingSaving = false let toolGuardrailsSaving = false let memorySaving = false let skillsSaving = false @@ -282,6 +290,7 @@ export function render() { let error = null let runtimeError = null let compressionError = null + let promptCachingError = null let toolGuardrailsError = null let memoryError = null let skillsError = null @@ -312,7 +321,7 @@ export function render() { } function isBusy() { - return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving + return loading || runtimeLoading || compressionLoading || promptCachingLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving } function option(labelKey, value, selected) { @@ -329,7 +338,7 @@ export function render() { } function renderRuntimePanel() { - const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -377,7 +386,7 @@ export function render() { } function renderCompressionPanel() { - const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || compressionLoading || compressionSaving || promptCachingSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -426,8 +435,38 @@ 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 + return ` +
+
+
+
${t('engine.hermesPromptCachingConfigTitle')}
+
${t('engine.hermesPromptCachingConfigDesc')}
+
+
+ ${promptCachingSaving ? t('engine.hermesConfigStatusSaving') : promptCachingLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesPromptCachingConfigStatusReady')} + +
+
+
+ ${renderError(promptCachingError)} +
+ +
+
${t('engine.hermesPromptCachingConfigFootnote')}
+
+
+ ` + } + function renderToolGuardrailsPanel() { - const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || promptCachingSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -489,7 +528,7 @@ export function render() { } function renderMemoryPanel() { - const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -539,7 +578,7 @@ export function render() { } function renderSkillsConfigPanel() { - const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -571,7 +610,7 @@ export function render() { } function renderQuickCommandsConfigPanel() { - const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -597,7 +636,7 @@ export function render() { } function renderAgentToolsetsConfigPanel() { - const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -623,7 +662,7 @@ 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 || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving + const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving return `
@@ -685,7 +724,7 @@ export function render() { } function renderUnauthorizedDmConfigPanel() { - const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -715,7 +754,7 @@ export function render() { } function renderSecurityConfigPanel() { - const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -757,7 +796,7 @@ export function render() { } function renderDisplayConfigPanel() { - const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || 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 || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -855,7 +894,7 @@ export function render() { } function renderHumanDelayConfigPanel() { - const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -893,7 +932,7 @@ export function render() { } function renderStreamingPanel() { - const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving + const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving return `
@@ -945,7 +984,7 @@ export function render() { } function renderExecutionLimitsPanel() { - const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving + const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving return `
@@ -1017,7 +1056,7 @@ export function render() { } function renderIoSafetyPanel() { - const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -1057,7 +1096,7 @@ export function render() { } function renderCheckpointsPanel() { - const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || 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 || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -1115,7 +1154,7 @@ export function render() { } function renderCronPanel() { - const disabled = loading || saving || cronLoading || cronSaving || checkpointsSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || 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 || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -1149,7 +1188,7 @@ export function render() { } function renderLoggingPanel() { - const disabled = loading || saving || loggingLoading || loggingSaving || checkpointsSaving || cronSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || 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 || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -1197,7 +1236,7 @@ export function render() { } function renderApprovalsPanel() { - const disabled = loading || saving || approvalsLoading || approvalsSaving || checkpointsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || 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 || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving return `
@@ -1247,7 +1286,7 @@ export function render() { } function renderPrivacyPanel() { - const disabled = loading || saving || privacyLoading || privacySaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || 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 || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1275,7 +1314,7 @@ export function render() { } function renderBrowserPanel() { - const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving + const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1319,7 +1358,7 @@ export function render() { } function renderTerminalPanel() { - const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving + const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || cronSaving || loggingSaving || browserSaving || runtimeSaving || compressionSaving || promptCachingSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving return `
@@ -1415,6 +1454,7 @@ export function render() { ${renderPrivacyPanel()} ${renderBrowserPanel()} ${renderCompressionPanel()} + ${renderPromptCachingPanel()} ${renderToolGuardrailsPanel()} ${renderMemoryPanel()} ${renderSkillsConfigPanel()} @@ -1446,6 +1486,7 @@ export function render() { el.querySelector('#hm-config-save')?.addEventListener('click', save) 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-tool-guardrails-save')?.addEventListener('click', saveToolGuardrails) el.querySelector('#hm-memory-save')?.addEventListener('click', saveMemory) el.querySelector('#hm-skills-config-save')?.addEventListener('click', saveSkillsConfig) @@ -1483,6 +1524,11 @@ export function render() { compressionValues = { ...COMPRESSION_DEFAULTS, ...(data?.values || {}) } } + async function loadPromptCaching() { + const data = await api.hermesPromptCachingConfigRead() + promptCachingValues = { ...PROMPT_CACHING_DEFAULTS, ...(data?.values || {}) } + } + async function loadToolGuardrails() { const data = await api.hermesToolLoopGuardrailsConfigRead() toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS, ...(data?.values || {}) } @@ -1587,6 +1633,7 @@ export function render() { loading = true runtimeLoading = true compressionLoading = true + promptCachingLoading = true toolGuardrailsLoading = true memoryLoading = true skillsLoading = true @@ -1610,6 +1657,7 @@ export function render() { error = null runtimeError = null compressionError = null + promptCachingError = null toolGuardrailsError = null memoryError = null skillsError = null @@ -1654,6 +1702,14 @@ export function render() { compressionLoading = false draw() } + try { + await loadPromptCaching() + } catch (err) { + promptCachingError = humanizeError(err, t('engine.hermesPromptCachingConfigLoadFailed') || 'Load prompt caching config failed') + } finally { + promptCachingLoading = false + draw() + } try { await loadToolGuardrails() } catch (err) { @@ -1841,6 +1897,9 @@ export function render() { try { await loadCompression() } catch {} + try { + await loadPromptCaching() + } catch {} try { await loadToolGuardrails() } catch {} @@ -1969,6 +2028,31 @@ export function render() { } } + async function savePromptCaching() { + const form = { + promptCacheTtl: el.querySelector('#hm-prompt-cache-ttl')?.value || '5m', + } + promptCachingSaving = true + promptCachingError = null + draw() + try { + const result = await api.hermesPromptCachingConfigSave(form) + promptCachingValues = { ...PROMPT_CACHING_DEFAULTS, ...(result?.values || form) } + await refreshRawAfterStructuredSave() + const backup = result?.backup || '' + toast({ + message: t('engine.hermesPromptCachingConfigSaveSuccess'), + hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '', + }, 'success') + } catch (err) { + promptCachingError = humanizeError(err, t('engine.hermesPromptCachingConfigSaveFailed') || 'Save prompt caching config failed') + toast(promptCachingError, 'error') + } finally { + promptCachingSaving = 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 5d96ba4..92a30ac 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -513,6 +513,8 @@ export const api = { hermesSessionRuntimeConfigSave: (form) => invoke('hermes_session_runtime_config_save', { form }), hermesCompressionConfigRead: () => invoke('hermes_compression_config_read'), hermesCompressionConfigSave: (form) => invoke('hermes_compression_config_save', { form }), + hermesPromptCachingConfigRead: () => invoke('hermes_prompt_caching_config_read'), + hermesPromptCachingConfigSave: (form) => invoke('hermes_prompt_caching_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 ae1cb04..cae66e2 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -674,6 +674,17 @@ export default { hermesCompressionProtectFirstN: _('保护开头消息数', 'Protect first messages', '保護開頭訊息數'), hermesCompressionAbortOnSummaryFailure: _('摘要失败时中止回复', 'Abort when summarization fails', '摘要失敗時中止回覆'), hermesCompressionFootnote: _('阈值和目标比例越低,压缩越早、越激进。建议先使用默认值,再根据真实 Gateway 日志调整。', 'Lower thresholds and target ratios compress earlier and more aggressively. Start with the defaults, then tune with real Gateway logs.', '閾值和目標比例越低,壓縮越早、越激進。建議先使用預設值,再根據真實 Gateway 日誌調整。'), + hermesPromptCachingConfigTitle: _('提示缓存', 'Prompt caching', '提示快取'), + hermesPromptCachingConfigDesc: _('控制 Anthropic/OpenRouter 前缀缓存 TTL。长跑任务如果回合间隔较长,可以选择 1 小时以降低重复上下文成本。', 'Control the Anthropic/OpenRouter prefix cache TTL. For long runs with longer pauses between turns, choose 1 hour to reduce repeated context cost.', '控制 Anthropic/OpenRouter 前綴快取 TTL。長跑任務如果回合間隔較長,可以選擇 1 小時以降低重複上下文成本。'), + hermesPromptCachingConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), + hermesPromptCachingConfigSave: _('保存提示缓存', 'Save prompt caching', '儲存提示快取'), + hermesPromptCachingConfigSaveSuccess: _('提示缓存配置已保存,建议重启 Hermes Gateway 生效', 'Prompt caching settings saved. Restart Hermes Gateway to take effect.', '提示快取設定已儲存,建議重啟 Hermes Gateway 生效'), + hermesPromptCachingConfigLoadFailed: _('加载提示缓存配置失败', 'Load prompt caching settings failed', '載入提示快取設定失敗'), + hermesPromptCachingConfigSaveFailed: _('保存提示缓存配置失败', 'Save prompt caching settings failed', '儲存提示快取設定失敗'), + hermesPromptCachingConfigCacheTtl: _('缓存有效期', 'Cache TTL', '快取有效期'), + 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 中。'), 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-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 6430f00..a29566b 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -139,6 +139,15 @@ test('Hermes 配置页会暴露全局显示与可靠性结构化配置字段', ( } }) +test('Hermes 配置页会暴露提示缓存结构化配置字段', () => { + for (const id of [ + 'hm-prompt-caching-save', + 'hm-prompt-cache-ttl', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露网关流式结构化配置字段', () => { for (const id of [ 'hm-streaming-save', @@ -290,6 +299,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('SecurityConfig') || key.includes('HumanDelayConfig') || key.includes('DisplayConfig') || + key.includes('PromptCachingConfig') || key.includes('StreamingConfig') || key.includes('ExecutionLimits') || key.includes('PrivacyConfig') || diff --git a/tests/hermes-prompt-caching-config.test.js b/tests/hermes-prompt-caching-config.test.js new file mode 100644 index 0000000..0a6271f --- /dev/null +++ b/tests/hermes-prompt-caching-config.test.js @@ -0,0 +1,50 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildHermesPromptCachingConfigValues, + mergeHermesPromptCachingConfig, +} from '../scripts/dev-api.js' + +test('Hermes 提示缓存配置读取会提供上游默认 TTL', () => { + const values = buildHermesPromptCachingConfigValues({}) + + assert.deepEqual(values, { + promptCacheTtl: '5m', + }) +}) + +test('Hermes 提示缓存配置读取会规范化 YAML 中的 TTL', () => { + const values = buildHermesPromptCachingConfigValues({ + prompt_caching: { + cache_ttl: '1H', + }, + }) + + assert.equal(values.promptCacheTtl, '1h') +}) + +test('Hermes 提示缓存配置保存会保留无关 YAML 和未知字段', () => { + const next = mergeHermesPromptCachingConfig({ + model: { provider: 'anthropic' }, + prompt_caching: { + cache_ttl: '5m', + custom_flag: 'keep-prompt-cache', + }, + compression: { enabled: true }, + }, { + promptCacheTtl: '1h', + }) + + assert.deepEqual(next.model, { provider: 'anthropic' }) + assert.deepEqual(next.compression, { enabled: true }) + assert.equal(next.prompt_caching.cache_ttl, '1h') + assert.equal(next.prompt_caching.custom_flag, 'keep-prompt-cache') +}) + +test('Hermes 提示缓存配置保存会拒绝上游不支持的 TTL', () => { + assert.throws( + () => mergeHermesPromptCachingConfig({}, { promptCacheTtl: '30m' }), + /prompt_caching\.cache_ttl/, + ) +})