mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): add kanban dispatch config
This commit is contained in:
@@ -3752,6 +3752,42 @@ export function mergeHermesDisplayConfig(config = {}, form = {}) {
|
||||
return next
|
||||
}
|
||||
|
||||
export function buildHermesKanbanConfigValues(config = {}) {
|
||||
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
||||
const kanban = root.kanban && typeof root.kanban === 'object' && !Array.isArray(root.kanban)
|
||||
? root.kanban
|
||||
: {}
|
||||
return {
|
||||
dispatchStaleTimeoutSeconds: parseHermesInteger(
|
||||
kanban.dispatch_stale_timeout_seconds,
|
||||
'kanban.dispatch_stale_timeout_seconds',
|
||||
14400,
|
||||
0,
|
||||
604800,
|
||||
false,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeHermesKanbanConfig(config = {}, form = {}) {
|
||||
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
|
||||
const currentValues = buildHermesKanbanConfigValues(next)
|
||||
const kanban = next.kanban && typeof next.kanban === 'object' && !Array.isArray(next.kanban)
|
||||
? mergeConfigsPreservingFields(next.kanban, {})
|
||||
: {}
|
||||
|
||||
kanban.dispatch_stale_timeout_seconds = parseHermesInteger(
|
||||
Object.hasOwn(form, 'dispatchStaleTimeoutSeconds') ? form.dispatchStaleTimeoutSeconds : currentValues.dispatchStaleTimeoutSeconds,
|
||||
'kanban.dispatch_stale_timeout_seconds',
|
||||
14400,
|
||||
0,
|
||||
604800,
|
||||
true,
|
||||
)
|
||||
next.kanban = kanban
|
||||
return next
|
||||
}
|
||||
|
||||
function putHermesChannelDisplayFields(form, config, platform) {
|
||||
const { display, platformDisplay } = hermesDisplayConfigParts(config, platform)
|
||||
const legacyToolProgress = display.tool_progress_overrides && typeof display.tool_progress_overrides === 'object' && !Array.isArray(display.tool_progress_overrides)
|
||||
@@ -12028,6 +12064,27 @@ const handlers = {
|
||||
}
|
||||
},
|
||||
|
||||
hermes_kanban_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
exists,
|
||||
configPath,
|
||||
values: buildHermesKanbanConfigValues(config),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_kanban_config_save({ form } = {}) {
|
||||
const { configPath, config } = readHermesConfigYamlObject()
|
||||
const next = mergeHermesKanbanConfig(config, form || {})
|
||||
const backup = writeHermesConfigYamlObject(configPath, next)
|
||||
return {
|
||||
ok: true,
|
||||
configPath,
|
||||
backup,
|
||||
values: buildHermesKanbanConfigValues(next),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_streaming_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
|
||||
@@ -6539,6 +6539,40 @@ fn build_hermes_human_delay_config_values(config: &serde_yaml::Value) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_hermes_kanban_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let root = config.as_mapping();
|
||||
let kanban = root.and_then(|map| yaml_get_mapping(map, "kanban"));
|
||||
serde_json::json!({
|
||||
"dispatchStaleTimeoutSeconds": kanban
|
||||
.map(|map| bounded_hermes_i64(
|
||||
yaml_i64_field(map, "dispatch_stale_timeout_seconds"),
|
||||
14400,
|
||||
0,
|
||||
604800,
|
||||
))
|
||||
.unwrap_or(14400),
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_hermes_kanban_config(config: &mut serde_yaml::Value, form: &Value) -> Result<(), String> {
|
||||
let current = build_hermes_kanban_config_values(config);
|
||||
let stale_timeout = validate_hermes_i64(
|
||||
form_i64(form, "dispatchStaleTimeoutSeconds")
|
||||
.or_else(|| current["dispatchStaleTimeoutSeconds"].as_i64()),
|
||||
"kanban.dispatch_stale_timeout_seconds",
|
||||
14400,
|
||||
0,
|
||||
604800,
|
||||
)?;
|
||||
|
||||
let kanban = yaml_child_object(ensure_yaml_object(config)?, "kanban")?;
|
||||
kanban.insert(
|
||||
yaml_key("dispatch_stale_timeout_seconds"),
|
||||
serde_yaml::Value::Number(serde_yaml::Number::from(stale_timeout)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_hermes_human_delay_config(
|
||||
config: &mut serde_yaml::Value,
|
||||
form: &Value,
|
||||
@@ -9575,6 +9609,29 @@ pub fn hermes_display_config_save(form: Value) -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_kanban_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
Ok(serde_json::json!({
|
||||
"exists": exists,
|
||||
"configPath": config_path.to_string_lossy(),
|
||||
"values": build_hermes_kanban_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_kanban_config_save(form: Value) -> Result<Value, String> {
|
||||
let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?;
|
||||
merge_hermes_kanban_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_kanban_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_human_delay_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
@@ -19263,6 +19320,84 @@ memory:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_kanban_config_tests {
|
||||
use super::{build_hermes_kanban_config_values, merge_hermes_kanban_config};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn kanban_values_have_upstream_defaults() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap();
|
||||
let values = build_hermes_kanban_config_values(&config);
|
||||
assert_eq!(values["dispatchStaleTimeoutSeconds"], 14400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kanban_values_normalize_existing_fields() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
kanban:
|
||||
dispatch_stale_timeout_seconds: "7200"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let values = build_hermes_kanban_config_values(&config);
|
||||
assert_eq!(values["dispatchStaleTimeoutSeconds"], 7200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_kanban_config_preserves_unknown_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
model:
|
||||
provider: anthropic
|
||||
kanban:
|
||||
dispatch_interval_seconds: 30
|
||||
custom_flag: keep-me
|
||||
memory:
|
||||
memory_enabled: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_hermes_kanban_config(
|
||||
&mut config,
|
||||
&json!({
|
||||
"dispatchStaleTimeoutSeconds": 0,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config["model"]["provider"].as_str(), Some("anthropic"));
|
||||
assert_eq!(config["memory"]["memory_enabled"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
config["kanban"]["dispatch_interval_seconds"].as_i64(),
|
||||
Some(30)
|
||||
);
|
||||
assert_eq!(config["kanban"]["custom_flag"].as_str(), Some("keep-me"));
|
||||
assert_eq!(
|
||||
config["kanban"]["dispatch_stale_timeout_seconds"].as_i64(),
|
||||
Some(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_kanban_config_rejects_invalid_timeout() {
|
||||
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
|
||||
let err =
|
||||
merge_hermes_kanban_config(&mut config, &json!({ "dispatchStaleTimeoutSeconds": -1 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("kanban.dispatch_stale_timeout_seconds"));
|
||||
|
||||
let err = merge_hermes_kanban_config(
|
||||
&mut config,
|
||||
&json!({ "dispatchStaleTimeoutSeconds": 604801 }),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("kanban.dispatch_stale_timeout_seconds"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_security_config_tests {
|
||||
use super::{build_hermes_security_config_values, merge_hermes_security_config};
|
||||
|
||||
@@ -299,6 +299,8 @@ pub fn run() {
|
||||
hermes::hermes_security_config_save,
|
||||
hermes::hermes_display_config_read,
|
||||
hermes::hermes_display_config_save,
|
||||
hermes::hermes_kanban_config_read,
|
||||
hermes::hermes_kanban_config_save,
|
||||
hermes::hermes_human_delay_config_read,
|
||||
hermes::hermes_human_delay_config_save,
|
||||
hermes::hermes_streaming_config_read,
|
||||
|
||||
@@ -173,6 +173,10 @@ const HUMAN_DELAY_DEFAULTS = {
|
||||
humanDelayMaxMs: 2500,
|
||||
}
|
||||
|
||||
const KANBAN_DEFAULTS = {
|
||||
dispatchStaleTimeoutSeconds: 14400,
|
||||
}
|
||||
|
||||
const STREAMING_DEFAULTS = {
|
||||
enabled: false,
|
||||
transport: 'edit',
|
||||
@@ -333,6 +337,7 @@ export function render() {
|
||||
let securityValues = { ...SECURITY_DEFAULTS }
|
||||
let displayValues = { ...DISPLAY_DEFAULTS }
|
||||
let humanDelayValues = { ...HUMAN_DELAY_DEFAULTS }
|
||||
let kanbanValues = { ...KANBAN_DEFAULTS }
|
||||
let streamingValues = { ...STREAMING_DEFAULTS }
|
||||
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
|
||||
let ioSafetyValues = { ...IO_SAFETY_DEFAULTS }
|
||||
@@ -367,6 +372,7 @@ export function render() {
|
||||
let securityLoading = true
|
||||
let displayLoading = true
|
||||
let humanDelayLoading = true
|
||||
let kanbanLoading = true
|
||||
let streamingLoading = true
|
||||
let executionLimitsLoading = true
|
||||
let ioSafetyLoading = true
|
||||
@@ -401,6 +407,7 @@ export function render() {
|
||||
let securitySaving = false
|
||||
let displaySaving = false
|
||||
let humanDelaySaving = false
|
||||
let kanbanSaving = false
|
||||
let streamingSaving = false
|
||||
let executionLimitsSaving = false
|
||||
let ioSafetySaving = false
|
||||
@@ -435,6 +442,7 @@ export function render() {
|
||||
let securityError = null
|
||||
let displayError = null
|
||||
let humanDelayError = null
|
||||
let kanbanError = null
|
||||
let streamingError = null
|
||||
let executionLimitsError = null
|
||||
let ioSafetyError = null
|
||||
@@ -456,7 +464,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function isBusy() {
|
||||
return loading || runtimeLoading || compressionLoading || promptCachingLoading || openrouterCacheLoading || providerRoutingLoading || auxiliaryLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || modelLoading || modelAliasesLoading || hooksLoading || providerOverridesLoading || mcpServersLoading || agentToolsetsLoading || platformToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || sttLoading || terminalLoading || saving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || modelSaving || modelAliasesSaving || hooksSaving || providerOverridesSaving || mcpServersSaving || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || sttSaving || terminalSaving
|
||||
return loading || runtimeLoading || compressionLoading || promptCachingLoading || openrouterCacheLoading || providerRoutingLoading || auxiliaryLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || modelLoading || modelAliasesLoading || hooksLoading || providerOverridesLoading || mcpServersLoading || agentToolsetsLoading || platformToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || kanbanLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || sttLoading || terminalLoading || saving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || modelSaving || modelAliasesSaving || hooksSaving || providerOverridesSaving || mcpServersSaving || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || kanbanSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || sttSaving || terminalSaving
|
||||
}
|
||||
|
||||
function option(labelKey, value, selected) {
|
||||
@@ -1461,6 +1469,34 @@ export function render() {
|
||||
`
|
||||
}
|
||||
|
||||
function renderKanbanConfigPanel() {
|
||||
const disabled = loading || saving || kanbanLoading || kanbanSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-kanban-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div>
|
||||
<div class="hm-panel-title">${t('engine.hermesKanbanConfigTitle')}</div>
|
||||
<div class="hm-channel-panel-desc">${t('engine.hermesKanbanConfigDesc')}</div>
|
||||
</div>
|
||||
<div class="hm-panel-actions">
|
||||
<span class="hm-muted">${kanbanSaving ? t('engine.hermesConfigStatusSaving') : kanbanLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesKanbanConfigStatusReady')}</span>
|
||||
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-kanban-config-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesKanbanConfigSave')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
${renderError(kanbanError)}
|
||||
<div class="hm-config-runtime-grid hm-config-kanban-grid">
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesKanbanConfigDispatchStaleTimeoutSeconds')}</span>
|
||||
<input id="hm-kanban-dispatch-stale-timeout-seconds" class="hm-input" type="number" inputmode="numeric" min="0" max="604800" step="60" value="${esc(kanbanValues.dispatchStaleTimeoutSeconds)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-channel-footnote">${t('engine.hermesKanbanConfigFootnote')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderStreamingPanel() {
|
||||
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving
|
||||
return `
|
||||
@@ -2110,6 +2146,7 @@ export function render() {
|
||||
${renderSecurityConfigPanel()}
|
||||
${renderDisplayConfigPanel()}
|
||||
${renderHumanDelayConfigPanel()}
|
||||
${renderKanbanConfigPanel()}
|
||||
|
||||
<div class="hm-panel">
|
||||
<div class="hm-panel-header">
|
||||
@@ -2151,6 +2188,7 @@ export function render() {
|
||||
el.querySelector('#hm-security-save')?.addEventListener('click', saveSecurityConfig)
|
||||
el.querySelector('#hm-display-save')?.addEventListener('click', saveDisplayConfig)
|
||||
el.querySelector('#hm-human-delay-save')?.addEventListener('click', saveHumanDelayConfig)
|
||||
el.querySelector('#hm-kanban-config-save')?.addEventListener('click', saveKanbanConfig)
|
||||
el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming)
|
||||
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
|
||||
el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety)
|
||||
@@ -2279,6 +2317,11 @@ export function render() {
|
||||
humanDelayValues = { ...HUMAN_DELAY_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadKanbanConfig() {
|
||||
const data = await api.hermesKanbanConfigRead()
|
||||
kanbanValues = { ...KANBAN_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadStreaming() {
|
||||
const data = await api.hermesStreamingConfigRead()
|
||||
streamingValues = { ...STREAMING_DEFAULTS, ...(data?.values || {}) }
|
||||
@@ -2358,6 +2401,7 @@ export function render() {
|
||||
securityLoading = true
|
||||
displayLoading = true
|
||||
humanDelayLoading = true
|
||||
kanbanLoading = true
|
||||
streamingLoading = true
|
||||
executionLimitsLoading = true
|
||||
ioSafetyLoading = true
|
||||
@@ -2675,6 +2719,14 @@ export function render() {
|
||||
humanDelayLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadKanbanConfig()
|
||||
} catch (err) {
|
||||
kanbanError = humanizeError(err, t('engine.hermesKanbanConfigLoadFailed') || 'Load Kanban config failed')
|
||||
} finally {
|
||||
kanbanLoading = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRawAfterStructuredSave() {
|
||||
@@ -2759,6 +2811,9 @@ export function render() {
|
||||
try {
|
||||
await loadHumanDelayConfig()
|
||||
} catch {}
|
||||
try {
|
||||
await loadKanbanConfig()
|
||||
} catch {}
|
||||
try {
|
||||
await loadStreaming()
|
||||
} catch {}
|
||||
@@ -3427,6 +3482,31 @@ export function render() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveKanbanConfig() {
|
||||
const form = {
|
||||
dispatchStaleTimeoutSeconds: el.querySelector('#hm-kanban-dispatch-stale-timeout-seconds')?.value || '14400',
|
||||
}
|
||||
kanbanSaving = true
|
||||
kanbanError = null
|
||||
draw()
|
||||
try {
|
||||
const result = await api.hermesKanbanConfigSave(form)
|
||||
kanbanValues = { ...KANBAN_DEFAULTS, ...(result?.values || form) }
|
||||
await refreshRawAfterStructuredSave()
|
||||
const backup = result?.backup || ''
|
||||
toast({
|
||||
message: t('engine.hermesKanbanConfigSaveSuccess'),
|
||||
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
|
||||
}, 'success')
|
||||
} catch (err) {
|
||||
kanbanError = humanizeError(err, t('engine.hermesKanbanConfigSaveFailed') || 'Save Kanban config failed')
|
||||
toast(kanbanError, 'error')
|
||||
} finally {
|
||||
kanbanSaving = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStreaming() {
|
||||
const form = {
|
||||
enabled: !!el.querySelector('#hm-streaming-enabled')?.checked,
|
||||
|
||||
@@ -551,6 +551,8 @@ export const api = {
|
||||
hermesSecurityConfigSave: (form) => invoke('hermes_security_config_save', { form }),
|
||||
hermesDisplayConfigRead: () => invoke('hermes_display_config_read'),
|
||||
hermesDisplayConfigSave: (form) => invoke('hermes_display_config_save', { form }),
|
||||
hermesKanbanConfigRead: () => invoke('hermes_kanban_config_read'),
|
||||
hermesKanbanConfigSave: (form) => invoke('hermes_kanban_config_save', { form }),
|
||||
hermesHumanDelayConfigRead: () => invoke('hermes_human_delay_config_read'),
|
||||
hermesHumanDelayConfigSave: (form) => invoke('hermes_human_delay_config_save', { form }),
|
||||
hermesStreamingConfigRead: () => invoke('hermes_streaming_config_read'),
|
||||
|
||||
@@ -1028,6 +1028,15 @@ export default {
|
||||
hermesHumanDelayConfigMinMs: _('最小延迟 ms', 'Minimum delay ms', '最小延遲 ms'),
|
||||
hermesHumanDelayConfigMaxMs: _('最大延迟 ms', 'Maximum delay ms', '最大延遲 ms'),
|
||||
hermesHumanDelayConfigFootnote: _('natural 使用 800-2500ms;custom 使用下方范围。Signal 等平台可能忽略或仅部分支持该设置。', 'natural uses 800-2500ms; custom uses the range below. Platforms such as Signal may ignore or only partially support this setting.', 'natural 使用 800-2500ms;custom 使用下方範圍。Signal 等平台可能忽略或僅部分支援此設定。'),
|
||||
hermesKanbanConfigTitle: _('Kanban 调度稳定性', 'Kanban dispatch reliability', 'Kanban 調度穩定性'),
|
||||
hermesKanbanConfigDesc: _('控制 Kanban Worker 多久没有心跳后被自动回收,避免长任务卡在运行中。', 'Control how long a Kanban worker may go without a heartbeat before it is reclaimed, preventing long tasks from staying stuck as running.', '控制 Kanban Worker 多久沒有心跳後被自動回收,避免長任務卡在執行中。'),
|
||||
hermesKanbanConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
hermesKanbanConfigSave: _('保存 Kanban 设置', 'Save Kanban settings', '儲存 Kanban 設定'),
|
||||
hermesKanbanConfigSaveSuccess: _('Kanban 调度配置已保存,建议重启 Hermes Gateway 生效', 'Kanban dispatch settings saved. Restart Hermes Gateway to take effect.', 'Kanban 調度設定已儲存,建議重啟 Hermes Gateway 生效'),
|
||||
hermesKanbanConfigLoadFailed: _('加载 Kanban 调度配置失败', 'Load Kanban dispatch settings failed', '載入 Kanban 調度設定失敗'),
|
||||
hermesKanbanConfigSaveFailed: _('保存 Kanban 调度配置失败', 'Save Kanban dispatch settings failed', '儲存 Kanban 調度設定失敗'),
|
||||
hermesKanbanConfigDispatchStaleTimeoutSeconds: _('无心跳回收时间(秒)', 'Heartbeat reclaim timeout (s)', '無心跳回收時間(秒)'),
|
||||
hermesKanbanConfigFootnote: _('写入 kanban.dispatch_stale_timeout_seconds。默认 14400 秒;设为 0 会关闭无心跳自动回收。建议只在确认 Worker 会长时间离线且由外部系统接管时关闭。', 'Writes kanban.dispatch_stale_timeout_seconds. Default is 14400 seconds; set 0 to disable heartbeat-based reclaim. Disable it only when workers may stay offline for long periods and an external supervisor handles recovery.', '寫入 kanban.dispatch_stale_timeout_seconds。預設 14400 秒;設為 0 會關閉無心跳自動回收。建議只在確認 Worker 會長時間離線且由外部系統接管時關閉。'),
|
||||
hermesSecurityConfigTitle: _('Tirith 安全扫描', 'Tirith security scanning', 'Tirith 安全掃描'),
|
||||
hermesSecurityConfigDesc: _('控制终端命令执行前的 Tirith 内容扫描,拦截明显的 URL 伪装、管道执行和注入风险。', 'Control Tirith content scanning before terminal commands run to catch obvious URL spoofing, pipe-to-shell, and injection risks.', '控制終端命令執行前的 Tirith 內容掃描,攔截明顯的 URL 偽裝、管道執行和注入風險。'),
|
||||
hermesSecurityConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
|
||||
@@ -425,6 +425,15 @@ test('Hermes 配置页会暴露语音转写结构化配置字段', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露 Kanban 调度稳定性结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-kanban-config-save',
|
||||
'hm-kanban-dispatch-stale-timeout-seconds',
|
||||
]) {
|
||||
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页数值输入会保留 0 值显示', () => {
|
||||
assert.doesNotMatch(source, /String\(value \|\| ''\)/, 'esc(value) 不能把合法 0 渲染为空字符串')
|
||||
})
|
||||
@@ -455,6 +464,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => {
|
||||
key.includes('BrowserConfig') ||
|
||||
key.includes('TerminalConfig') ||
|
||||
key.includes('SttConfig') ||
|
||||
key.includes('KanbanConfig') ||
|
||||
key.includes('CheckpointsConfig') ||
|
||||
key.includes('ApprovalsConfig') ||
|
||||
key.includes('CronConfig') ||
|
||||
|
||||
59
tests/hermes-kanban-config.test.js
Normal file
59
tests/hermes-kanban-config.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildHermesKanbanConfigValues,
|
||||
mergeHermesKanbanConfig,
|
||||
} from '../scripts/dev-api.js'
|
||||
|
||||
test('Hermes Kanban 配置读取会提供上游默认值', () => {
|
||||
const values = buildHermesKanbanConfigValues({})
|
||||
|
||||
assert.deepEqual(values, {
|
||||
dispatchStaleTimeoutSeconds: 14400,
|
||||
})
|
||||
})
|
||||
|
||||
test('Hermes Kanban 配置读取会规范化已有字段', () => {
|
||||
const values = buildHermesKanbanConfigValues({
|
||||
kanban: {
|
||||
dispatch_stale_timeout_seconds: '7200',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(values.dispatchStaleTimeoutSeconds, 7200)
|
||||
})
|
||||
|
||||
test('Hermes Kanban 配置保存会保留未知 YAML 并写入 kanban', () => {
|
||||
const next = mergeHermesKanbanConfig({
|
||||
model: { provider: 'anthropic' },
|
||||
kanban: {
|
||||
dispatch_interval_seconds: 30,
|
||||
custom_flag: 'keep-me',
|
||||
},
|
||||
memory: { memory_enabled: true },
|
||||
}, {
|
||||
dispatchStaleTimeoutSeconds: '0',
|
||||
})
|
||||
|
||||
assert.deepEqual(next.model, { provider: 'anthropic' })
|
||||
assert.deepEqual(next.memory, { memory_enabled: true })
|
||||
assert.equal(next.kanban.dispatch_interval_seconds, 30)
|
||||
assert.equal(next.kanban.custom_flag, 'keep-me')
|
||||
assert.equal(next.kanban.dispatch_stale_timeout_seconds, 0)
|
||||
})
|
||||
|
||||
test('Hermes Kanban 配置保存会拒绝非法超时', () => {
|
||||
assert.throws(
|
||||
() => mergeHermesKanbanConfig({}, { dispatchStaleTimeoutSeconds: '-1' }),
|
||||
/kanban\.dispatch_stale_timeout_seconds/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesKanbanConfig({}, { dispatchStaleTimeoutSeconds: '604801' }),
|
||||
/kanban\.dispatch_stale_timeout_seconds/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesKanbanConfig({}, { dispatchStaleTimeoutSeconds: '12.5' }),
|
||||
/kanban\.dispatch_stale_timeout_seconds/,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user