feat(hermes): add kanban dispatch config

This commit is contained in:
晴天
2026-05-26 23:57:52 +08:00
parent 842cf83917
commit 425fcd847f
8 changed files with 355 additions and 1 deletions

View File

@@ -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,

View File

@@ -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'),

View File

@@ -1028,6 +1028,15 @@ export default {
hermesHumanDelayConfigMinMs: _('最小延迟 ms', 'Minimum delay ms', '最小延遲 ms'),
hermesHumanDelayConfigMaxMs: _('最大延迟 ms', 'Maximum delay ms', '最大延遲 ms'),
hermesHumanDelayConfigFootnote: _('natural 使用 800-2500mscustom 使用下方范围。Signal 等平台可能忽略或仅部分支持该设置。', 'natural uses 800-2500ms; custom uses the range below. Platforms such as Signal may ignore or only partially support this setting.', 'natural 使用 800-2500mscustom 使用下方範圍。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', '結構化設定'),