feat(hermes): add cron output settings

This commit is contained in:
晴天
2026-05-25 03:01:14 +08:00
parent e74df5f288
commit 92d64efd8e
8 changed files with 411 additions and 22 deletions

View File

@@ -4066,6 +4066,31 @@ export function mergeHermesCheckpointsConfig(config = {}, form = {}) {
return next
}
export function buildHermesCronConfigValues(config = {}) {
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
const cron = root.cron && typeof root.cron === 'object' && !Array.isArray(root.cron)
? root.cron
: {}
return {
cronWrapResponse: readHermesBool(cron.wrap_response, true),
cronMaxParallelJobs: parseHermesInteger(cron.max_parallel_jobs, 'cron.max_parallel_jobs', 0, 0, 10000, false),
}
}
export function mergeHermesCronConfig(config = {}, form = {}) {
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
const currentValues = buildHermesCronConfigValues(next)
const cron = next.cron && typeof next.cron === 'object' && !Array.isArray(next.cron)
? mergeConfigsPreservingFields(next.cron, {})
: {}
cron.wrap_response = formHermesBool(form, 'cronWrapResponse', currentValues.cronWrapResponse)
const maxParallelJobs = parseHermesInteger(Object.hasOwn(form, 'cronMaxParallelJobs') ? form.cronMaxParallelJobs : currentValues.cronMaxParallelJobs, 'cron.max_parallel_jobs', 0, 0, 10000, true)
cron.max_parallel_jobs = maxParallelJobs === 0 ? null : maxParallelJobs
next.cron = cron
return next
}
export function buildHermesApprovalsConfigValues(config = {}) {
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
const approvals = root.approvals && typeof root.approvals === 'object' && !Array.isArray(root.approvals)
@@ -10809,6 +10834,27 @@ const handlers = {
}
},
hermes_cron_config_read() {
const { configPath, exists, config } = readHermesConfigYamlObject()
return {
exists,
configPath,
values: buildHermesCronConfigValues(config),
}
},
hermes_cron_config_save({ form } = {}) {
const { configPath, config } = readHermesConfigYamlObject()
const next = mergeHermesCronConfig(config, form || {})
const backup = writeHermesConfigYamlObject(configPath, next)
return {
ok: true,
configPath,
backup,
values: buildHermesCronConfigValues(next),
}
},
hermes_approvals_config_read() {
const { configPath, exists, config } = readHermesConfigYamlObject()
return {

View File

@@ -5088,6 +5088,55 @@ fn merge_hermes_checkpoints_config(
Ok(())
}
fn build_hermes_cron_config_values(config: &serde_yaml::Value) -> Value {
let root = config.as_mapping();
let cron = root.and_then(|map| yaml_get_mapping(map, "cron"));
let cron_wrap_response = cron
.and_then(|map| yaml_bool_field(map, "wrap_response"))
.unwrap_or(true);
let cron_max_parallel_jobs = cron
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_parallel_jobs"), 0, 0, 10000))
.unwrap_or(0);
serde_json::json!({
"cronWrapResponse": cron_wrap_response,
"cronMaxParallelJobs": cron_max_parallel_jobs,
})
}
fn merge_hermes_cron_config(config: &mut serde_yaml::Value, form: &Value) -> Result<(), String> {
let current = build_hermes_cron_config_values(config);
let cron_wrap_response = form_bool(form, "cronWrapResponse")
.unwrap_or_else(|| current["cronWrapResponse"].as_bool().unwrap_or(true));
let cron_max_parallel_jobs = validate_hermes_i64(
if form.get("cronMaxParallelJobs").is_some() {
form_i64(form, "cronMaxParallelJobs")
} else {
Some(current["cronMaxParallelJobs"].as_i64().unwrap_or(0))
},
"cron.max_parallel_jobs",
0,
0,
10000,
)?;
let root = ensure_yaml_object(config)?;
let cron = yaml_child_object(root, "cron")?;
cron.insert(
yaml_key("wrap_response"),
serde_yaml::Value::Bool(cron_wrap_response),
);
cron.insert(
yaml_key("max_parallel_jobs"),
if cron_max_parallel_jobs == 0 {
serde_yaml::Value::Null
} else {
serde_yaml::Value::Number(cron_max_parallel_jobs.into())
},
);
Ok(())
}
fn build_hermes_approvals_config_values(config: &serde_yaml::Value) -> Value {
let root = config.as_mapping();
let approvals = root.and_then(|map| yaml_get_mapping(map, "approvals"));
@@ -6813,6 +6862,30 @@ pub fn hermes_checkpoints_config_save(form: Value) -> Result<Value, String> {
}))
}
#[tauri::command]
pub fn hermes_cron_config_read() -> Result<Value, String> {
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_cron_config_values(&config),
}))
}
#[tauri::command]
pub fn hermes_cron_config_save(form: Value) -> Result<Value, String> {
let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?;
merge_hermes_cron_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_cron_config_values(&config),
}))
}
#[tauri::command]
pub fn hermes_approvals_config_read() -> Result<Value, String> {
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
@@ -12784,6 +12857,93 @@ streaming:
}
}
#[cfg(test)]
mod hermes_cron_config_tests {
use super::{build_hermes_cron_config_values, merge_hermes_cron_config};
use serde_json::json;
#[test]
fn cron_values_have_upstream_defaults() {
let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap();
let values = build_hermes_cron_config_values(&config);
assert_eq!(values["cronWrapResponse"], true);
assert_eq!(values["cronMaxParallelJobs"], 0);
}
#[test]
fn cron_values_read_yaml_fields() {
let config: serde_yaml::Value = serde_yaml::from_str(
r#"
cron:
wrap_response: false
max_parallel_jobs: 4
"#,
)
.unwrap();
let values = build_hermes_cron_config_values(&config);
assert_eq!(values["cronWrapResponse"], false);
assert_eq!(values["cronMaxParallelJobs"], 4);
}
#[test]
fn merge_cron_config_preserves_unknown_fields() {
let mut config: serde_yaml::Value = serde_yaml::from_str(
r#"
cron:
wrap_response: true
custom_flag: keep-cron
approvals:
cron_mode: deny
streaming:
enabled: true
"#,
)
.unwrap();
merge_hermes_cron_config(
&mut config,
&json!({
"cronWrapResponse": false,
"cronMaxParallelJobs": "3",
}),
)
.unwrap();
assert_eq!(config["approvals"]["cron_mode"].as_str(), Some("deny"));
assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true));
assert_eq!(config["cron"]["wrap_response"].as_bool(), Some(false));
assert_eq!(config["cron"]["max_parallel_jobs"].as_i64(), Some(3));
assert_eq!(config["cron"]["custom_flag"].as_str(), Some("keep-cron"));
}
#[test]
fn merge_cron_config_writes_unbounded_null_and_rejects_invalid_values() {
let mut config: serde_yaml::Value = serde_yaml::from_str(
r#"
cron:
max_parallel_jobs: 8
"#,
)
.unwrap();
merge_hermes_cron_config(
&mut config,
&json!({
"cronMaxParallelJobs": "0",
}),
)
.unwrap();
assert_eq!(config["cron"]["max_parallel_jobs"], serde_yaml::Value::Null);
let err = merge_hermes_cron_config(&mut config, &json!({ "cronMaxParallelJobs": -1 }))
.unwrap_err();
assert!(err.contains("cron.max_parallel_jobs"));
let err = merge_hermes_cron_config(&mut config, &json!({ "cronMaxParallelJobs": 10001 }))
.unwrap_err();
assert!(err.contains("cron.max_parallel_jobs"));
}
}
#[cfg(test)]
mod hermes_approvals_config_tests {
use super::{build_hermes_approvals_config_values, merge_hermes_approvals_config};

View File

@@ -289,6 +289,8 @@ pub fn run() {
hermes::hermes_io_safety_config_save,
hermes::hermes_checkpoints_config_read,
hermes::hermes_checkpoints_config_save,
hermes::hermes_cron_config_read,
hermes::hermes_cron_config_save,
hermes::hermes_approvals_config_read,
hermes::hermes_approvals_config_save,
hermes::hermes_privacy_config_read,

View File

@@ -136,6 +136,11 @@ const CHECKPOINTS_DEFAULTS = {
checkpointMinIntervalHours: 24,
}
const CRON_DEFAULTS = {
cronWrapResponse: true,
cronMaxParallelJobs: 0,
}
const APPROVALS_DEFAULTS = {
approvalMode: 'manual',
approvalTimeout: 60,
@@ -203,6 +208,7 @@ export function render() {
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
let ioSafetyValues = { ...IO_SAFETY_DEFAULTS }
let checkpointsValues = { ...CHECKPOINTS_DEFAULTS }
let cronValues = { ...CRON_DEFAULTS }
let approvalsValues = { ...APPROVALS_DEFAULTS }
let privacyValues = { ...PRIVACY_DEFAULTS }
let browserValues = { ...BROWSER_DEFAULTS }
@@ -224,6 +230,7 @@ export function render() {
let executionLimitsLoading = true
let ioSafetyLoading = true
let checkpointsLoading = true
let cronLoading = true
let approvalsLoading = true
let privacyLoading = true
let browserLoading = true
@@ -245,6 +252,7 @@ export function render() {
let executionLimitsSaving = false
let ioSafetySaving = false
let checkpointsSaving = false
let cronSaving = false
let approvalsSaving = false
let privacySaving = false
let browserSaving = false
@@ -266,6 +274,7 @@ export function render() {
let executionLimitsError = null
let ioSafetyError = null
let checkpointsError = null
let cronError = null
let approvalsError = null
let privacyError = null
let browserError = null
@@ -280,7 +289,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || approvalsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || approvalsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -297,7 +306,7 @@ export function render() {
}
function renderRuntimePanel() {
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel">
<div class="hm-panel-header">
@@ -345,7 +354,7 @@ export function render() {
}
function renderCompressionPanel() {
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-compression-panel">
<div class="hm-panel-header">
@@ -395,7 +404,7 @@ export function render() {
}
function renderToolGuardrailsPanel() {
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-guardrails-panel">
<div class="hm-panel-header">
@@ -457,7 +466,7 @@ export function render() {
}
function renderMemoryPanel() {
const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-memory-panel">
<div class="hm-panel-header">
@@ -507,7 +516,7 @@ export function render() {
}
function renderSkillsConfigPanel() {
const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-skills-panel">
<div class="hm-panel-header">
@@ -539,7 +548,7 @@ export function render() {
}
function renderQuickCommandsConfigPanel() {
const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-quick-commands-panel">
<div class="hm-panel-header">
@@ -565,7 +574,7 @@ export function render() {
}
function renderAgentToolsetsConfigPanel() {
const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-agent-toolsets-panel">
<div class="hm-panel-header">
@@ -591,7 +600,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 || approvalsSaving || privacySaving || browserSaving || terminalSaving
const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-agent-runtime-panel">
<div class="hm-panel-header">
@@ -653,7 +662,7 @@ export function render() {
}
function renderUnauthorizedDmConfigPanel() {
const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-unauthorized-dm-panel">
<div class="hm-panel-header">
@@ -683,7 +692,7 @@ export function render() {
}
function renderSecurityConfigPanel() {
const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-security-panel">
<div class="hm-panel-header">
@@ -725,7 +734,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 || approvalsSaving || terminalSaving
const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-display-panel">
<div class="hm-panel-header">
@@ -789,7 +798,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 || approvalsSaving || terminalSaving
const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-human-delay-panel">
<div class="hm-panel-header">
@@ -827,7 +836,7 @@ export function render() {
}
function renderStreamingPanel() {
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-streaming-panel">
<div class="hm-panel-header">
@@ -879,7 +888,7 @@ export function render() {
}
function renderExecutionLimitsPanel() {
const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || approvalsSaving
const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || cronSaving || approvalsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-execution-limits-panel">
<div class="hm-panel-header">
@@ -951,7 +960,7 @@ export function render() {
}
function renderIoSafetyPanel() {
const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || cronSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-io-safety-panel">
<div class="hm-panel-header">
@@ -991,7 +1000,7 @@ export function render() {
}
function renderCheckpointsPanel() {
const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || cronSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-checkpoints-panel">
<div class="hm-panel-header">
@@ -1048,8 +1057,42 @@ export function render() {
`
}
function renderCronPanel() {
const disabled = loading || saving || cronLoading || cronSaving || checkpointsSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-cron-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesCronConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesCronConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${cronSaving ? t('engine.hermesConfigStatusSaving') : cronLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesCronConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-cron-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesCronConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(cronError)}
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-cron-wrap-response" type="checkbox" ${cronValues.cronWrapResponse ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesCronConfigWrapResponse')}</span>
</label>
</div>
<div class="hm-config-runtime-grid hm-config-cron-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCronConfigMaxParallelJobs')}</span>
<input id="hm-cron-max-parallel-jobs" class="hm-input" type="number" inputmode="numeric" min="0" max="10000" step="1" value="${esc(cronValues.cronMaxParallelJobs)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesCronConfigFootnote')}</div>
</div>
</div>
`
}
function renderApprovalsPanel() {
const disabled = loading || saving || approvalsLoading || approvalsSaving || checkpointsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
const disabled = loading || saving || approvalsLoading || approvalsSaving || checkpointsSaving || cronSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-approvals-panel">
<div class="hm-panel-header">
@@ -1099,7 +1142,7 @@ export function render() {
}
function renderPrivacyPanel() {
const disabled = loading || saving || privacyLoading || privacySaving || approvalsSaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
const disabled = loading || saving || privacyLoading || privacySaving || approvalsSaving || cronSaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-privacy-panel">
<div class="hm-panel-header">
@@ -1127,7 +1170,7 @@ export function render() {
}
function renderBrowserPanel() {
const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || cronSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-browser-panel">
<div class="hm-panel-header">
@@ -1171,7 +1214,7 @@ export function render() {
}
function renderTerminalPanel() {
const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving
const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || cronSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-terminal-panel">
<div class="hm-panel-header">
@@ -1261,6 +1304,7 @@ export function render() {
${renderExecutionLimitsPanel()}
${renderIoSafetyPanel()}
${renderCheckpointsPanel()}
${renderCronPanel()}
${renderApprovalsPanel()}
${renderPrivacyPanel()}
${renderBrowserPanel()}
@@ -1310,6 +1354,7 @@ export function render() {
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety)
el.querySelector('#hm-checkpoints-save')?.addEventListener('click', saveCheckpoints)
el.querySelector('#hm-cron-save')?.addEventListener('click', saveCronConfig)
el.querySelector('#hm-approvals-save')?.addEventListener('click', saveApprovalsConfig)
el.querySelector('#hm-privacy-save')?.addEventListener('click', savePrivacyConfig)
el.querySelector('#hm-browser-save')?.addEventListener('click', saveBrowserConfig)
@@ -1401,6 +1446,11 @@ export function render() {
checkpointsValues = { ...CHECKPOINTS_DEFAULTS, ...(data?.values || {}) }
}
async function loadCronConfig() {
const data = await api.hermesCronConfigRead()
cronValues = { ...CRON_DEFAULTS, ...(data?.values || {}) }
}
async function loadApprovalsConfig() {
const data = await api.hermesApprovalsConfigRead()
approvalsValues = { ...APPROVALS_DEFAULTS, ...(data?.values || {}) }
@@ -1439,6 +1489,7 @@ export function render() {
executionLimitsLoading = true
ioSafetyLoading = true
checkpointsLoading = true
cronLoading = true
approvalsLoading = true
privacyLoading = true
browserLoading = true
@@ -1460,6 +1511,7 @@ export function render() {
executionLimitsError = null
ioSafetyError = null
checkpointsError = null
cronError = null
approvalsError = null
privacyError = null
browserError = null
@@ -1528,6 +1580,14 @@ export function render() {
checkpointsLoading = false
draw()
}
try {
await loadCronConfig()
} catch (err) {
cronError = humanizeError(err, t('engine.hermesCronConfigLoadFailed') || 'Load cron config failed')
} finally {
cronLoading = false
draw()
}
try {
await loadApprovalsConfig()
} catch (err) {
@@ -1701,6 +1761,9 @@ export function render() {
try {
await loadCheckpoints()
} catch {}
try {
await loadCronConfig()
} catch {}
try {
await loadApprovalsConfig()
} catch {}
@@ -2188,6 +2251,32 @@ export function render() {
}
}
async function saveCronConfig() {
const form = {
cronWrapResponse: !!el.querySelector('#hm-cron-wrap-response')?.checked,
cronMaxParallelJobs: el.querySelector('#hm-cron-max-parallel-jobs')?.value || '0',
}
cronSaving = true
cronError = null
draw()
try {
const result = await api.hermesCronConfigSave(form)
cronValues = { ...CRON_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesCronConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
cronError = humanizeError(err, t('engine.hermesCronConfigSaveFailed') || 'Save cron config failed')
toast(cronError, 'error')
} finally {
cronSaving = false
draw()
}
}
async function saveApprovalsConfig() {
const form = {
approvalMode: el.querySelector('#hm-approval-mode')?.value || 'manual',

View File

@@ -541,6 +541,8 @@ export const api = {
hermesIoSafetyConfigSave: (form) => invoke('hermes_io_safety_config_save', { form }),
hermesCheckpointsConfigRead: () => invoke('hermes_checkpoints_config_read'),
hermesCheckpointsConfigSave: (form) => invoke('hermes_checkpoints_config_save', { form }),
hermesCronConfigRead: () => invoke('hermes_cron_config_read'),
hermesCronConfigSave: (form) => invoke('hermes_cron_config_save', { form }),
hermesApprovalsConfigRead: () => invoke('hermes_approvals_config_read'),
hermesApprovalsConfigSave: (form) => invoke('hermes_approvals_config_save', { form }),
hermesPrivacyConfigRead: () => invoke('hermes_privacy_config_read'),

View File

@@ -592,6 +592,16 @@ export default {
hermesCheckpointsConfigRetentionDays: _('保留天数', 'Retention days', '保留天數'),
hermesCheckpointsConfigMinIntervalHours: _('自动清理最小间隔小时', 'Minimum auto-prune interval hours', '自動清理最小間隔小時'),
hermesCheckpointsConfigFootnote: _('这里写入 checkpoints.*。容量上限或单文件上限设为 0 表示关闭对应限制;其他 checkpoints 高级字段会保留在 raw YAML 中。', 'This writes checkpoints.*. Set total size cap or single-file cap to 0 to disable that limit. Other advanced checkpoint fields stay in raw YAML.', '這裡寫入 checkpoints.*。容量上限或單檔上限設為 0 表示關閉對應限制;其他 checkpoints 進階欄位會保留在 raw YAML 中。'),
hermesCronConfigTitle: _('定时任务输出', 'Cron job output', '定時任務輸出'),
hermesCronConfigDesc: _('控制定时任务结果是否包装任务名和提示,并限制同一轮最多并发任务数,降低无人值守长跑时的噪音和资源冲高。', 'Control whether scheduled job results are wrapped with task context, and cap jobs running in parallel per tick to reduce unattended-run noise and resource spikes.', '控制定時任務結果是否包裝任務名稱和提示,並限制同一輪最多並發任務數,降低無人值守長跑時的噪音和資源衝高。'),
hermesCronConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesCronConfigSave: _('保存定时任务配置', 'Save cron settings', '儲存定時任務設定'),
hermesCronConfigSaveSuccess: _('定时任务输出配置已保存,建议重启 Hermes Gateway 生效', 'Cron job output settings saved. Restart Hermes Gateway to take effect.', '定時任務輸出設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesCronConfigLoadFailed: _('加载定时任务输出配置失败', 'Load cron job output settings failed', '載入定時任務輸出設定失敗'),
hermesCronConfigSaveFailed: _('保存定时任务输出配置失败', 'Save cron job output settings failed', '儲存定時任務輸出設定失敗'),
hermesCronConfigWrapResponse: _('结果回传时附加任务名和系统提示', 'Add job name and system notice when delivering results', '結果回傳時附加任務名稱和系統提示'),
hermesCronConfigMaxParallelJobs: _('每轮最多并发任务数', 'Max parallel jobs per tick', '每輪最多並發任務數'),
hermesCronConfigFootnote: _('这里写入 cron.wrap_response 与 cron.max_parallel_jobs。并发数设为 0 表示不额外限制,由 Hermes 按上游默认语义写入 null其他 cron 高级字段会保留在 raw YAML 中。', 'This writes cron.wrap_response and cron.max_parallel_jobs. Set parallel jobs to 0 for no extra cap; Hermes writes null to match upstream semantics. Other advanced cron fields stay in raw YAML.', '這裡寫入 cron.wrap_response 與 cron.max_parallel_jobs。並發數設為 0 表示不額外限制,由 Hermes 依上游預設語義寫入 null其他 cron 進階欄位會保留在 raw YAML 中。'),
hermesApprovalsConfigTitle: _('审批安全', 'Approval safety', '審批安全'),
hermesApprovalsConfigDesc: _('控制危险命令、Cron 任务和破坏性 slash 命令的审批策略,避免无人值守长跑任务误放行高风险操作。', 'Control approval policy for dangerous commands, cron jobs, and destructive slash commands so unattended long runs do not approve risky operations by mistake.', '控制危險命令、Cron 任務和破壞性 slash 命令的審批策略,避免無人值守長跑任務誤放行高風險操作。'),
hermesApprovalsConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),

View File

@@ -205,6 +205,16 @@ test('Hermes 配置页会暴露审批安全结构化配置字段', () => {
}
})
test('Hermes 配置页会暴露定时任务结构化配置字段', () => {
for (const id of [
'hm-cron-save',
'hm-cron-wrap-response',
'hm-cron-max-parallel-jobs',
]) {
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
}
})
test('Hermes 配置页会暴露隐私脱敏结构化配置字段', () => {
for (const id of [
'hm-privacy-save',
@@ -266,7 +276,8 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => {
key.includes('BrowserConfig') ||
key.includes('TerminalConfig') ||
key.includes('CheckpointsConfig') ||
key.includes('ApprovalsConfig')
key.includes('ApprovalsConfig') ||
key.includes('CronConfig')
)))
assert.ok(keys.size > 0, '应能提取新增结构化配置用到的 engine 翻译 key')

View File

@@ -0,0 +1,69 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildHermesCronConfigValues,
mergeHermesCronConfig,
} from '../scripts/dev-api.js'
test('Hermes 定时任务配置读取会提供上游默认值', () => {
const values = buildHermesCronConfigValues({})
assert.deepEqual(values, {
cronWrapResponse: true,
cronMaxParallelJobs: 0,
})
})
test('Hermes 定时任务配置读取会回显 YAML 字段', () => {
const values = buildHermesCronConfigValues({
cron: {
wrap_response: false,
max_parallel_jobs: 4,
},
})
assert.equal(values.cronWrapResponse, false)
assert.equal(values.cronMaxParallelJobs, 4)
})
test('Hermes 定时任务配置保存会保留未知字段并写入 cron', () => {
const next = mergeHermesCronConfig({
cron: {
wrap_response: true,
custom_flag: 'keep-cron',
},
approvals: { cron_mode: 'deny' },
streaming: { enabled: true },
}, {
cronWrapResponse: false,
cronMaxParallelJobs: '3',
})
assert.deepEqual(next.approvals, { cron_mode: 'deny' })
assert.deepEqual(next.streaming, { enabled: true })
assert.equal(next.cron.wrap_response, false)
assert.equal(next.cron.max_parallel_jobs, 3)
assert.equal(next.cron.custom_flag, 'keep-cron')
})
test('Hermes 定时任务配置保存 0 会写回不限制并拒绝越界值', () => {
const next = mergeHermesCronConfig({
cron: {
max_parallel_jobs: 8,
},
}, {
cronMaxParallelJobs: '0',
})
assert.equal(next.cron.max_parallel_jobs, null)
assert.throws(
() => mergeHermesCronConfig({}, { cronMaxParallelJobs: '-1' }),
/cron\.max_parallel_jobs/,
)
assert.throws(
() => mergeHermesCronConfig({}, { cronMaxParallelJobs: '10001' }),
/cron\.max_parallel_jobs/,
)
})