feat(hermes): add curator maintenance controls

This commit is contained in:
晴天
2026-05-27 01:59:06 +08:00
parent ec0f7ec64a
commit d2236afc2d
8 changed files with 601 additions and 1 deletions

View File

@@ -84,6 +84,16 @@ const SKILLS_DEFAULTS = {
guardAgentCreated: false,
}
const CURATOR_DEFAULTS = {
curatorEnabled: true,
curatorIntervalHours: 168,
curatorMinIdleHours: 2,
curatorStaleAfterDays: 30,
curatorArchiveAfterDays: 90,
curatorBackupEnabled: true,
curatorBackupKeep: 5,
}
const QUICK_COMMANDS_DEFAULTS = {
quickCommandsJson: '{}',
}
@@ -345,6 +355,7 @@ export function render() {
let toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS }
let memoryValues = { ...MEMORY_DEFAULTS }
let skillsValues = { ...SKILLS_DEFAULTS }
let curatorValues = { ...CURATOR_DEFAULTS }
let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS }
let modelValues = { ...MODEL_DEFAULTS }
let modelAliasesValues = { ...MODEL_ALIASES_DEFAULTS }
@@ -380,6 +391,7 @@ export function render() {
let toolGuardrailsLoading = true
let memoryLoading = true
let skillsLoading = true
let curatorLoading = true
let quickCommandsLoading = true
let modelLoading = true
let modelAliasesLoading = true
@@ -415,6 +427,7 @@ export function render() {
let toolGuardrailsSaving = false
let memorySaving = false
let skillsSaving = false
let curatorSaving = false
let quickCommandsSaving = false
let modelSaving = false
let modelAliasesSaving = false
@@ -450,6 +463,7 @@ export function render() {
let toolGuardrailsError = null
let memoryError = null
let skillsError = null
let curatorError = null
let quickCommandsError = null
let modelError = null
let modelAliasesError = null
@@ -485,7 +499,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 || 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
return loading || runtimeLoading || compressionLoading || promptCachingLoading || openrouterCacheLoading || providerRoutingLoading || auxiliaryLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || curatorLoading || 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 || curatorSaving || 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) {
@@ -968,6 +982,60 @@ export function render() {
`
}
function renderCuratorConfigPanel() {
const disabled = loading || saving || curatorLoading || curatorSaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-curator-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesCuratorConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesCuratorConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${curatorSaving ? t('engine.hermesConfigStatusSaving') : curatorLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesCuratorConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-curator-config-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesCuratorConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(curatorError)}
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-curator-enabled" type="checkbox" ${curatorValues.curatorEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesCuratorConfigEnabled')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-curator-backup-enabled" type="checkbox" ${curatorValues.curatorBackupEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesCuratorConfigBackupEnabled')}</span>
</label>
</div>
<div class="hm-config-runtime-grid hm-config-curator-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCuratorConfigIntervalHours')}</span>
<input id="hm-curator-interval-hours" class="hm-input" type="number" inputmode="numeric" min="1" max="87600" step="1" value="${esc(curatorValues.curatorIntervalHours)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCuratorConfigMinIdleHours')}</span>
<input id="hm-curator-min-idle-hours" class="hm-input" type="number" inputmode="numeric" min="0" max="87600" step="1" value="${esc(curatorValues.curatorMinIdleHours)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCuratorConfigStaleAfterDays')}</span>
<input id="hm-curator-stale-after-days" class="hm-input" type="number" inputmode="numeric" min="1" max="36500" step="1" value="${esc(curatorValues.curatorStaleAfterDays)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCuratorConfigArchiveAfterDays')}</span>
<input id="hm-curator-archive-after-days" class="hm-input" type="number" inputmode="numeric" min="1" max="36500" step="1" value="${esc(curatorValues.curatorArchiveAfterDays)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCuratorConfigBackupKeep')}</span>
<input id="hm-curator-backup-keep" class="hm-input" type="number" inputmode="numeric" min="0" max="1000" step="1" value="${esc(curatorValues.curatorBackupKeep)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesCuratorConfigFootnote')}</div>
</div>
</div>
`
}
function renderQuickCommandsConfigPanel() {
const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || modelSaving || modelAliasesSaving || hooksSaving || providerOverridesSaving || mcpServersSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || terminalSaving
return `
@@ -2242,6 +2310,7 @@ export function render() {
${renderToolGuardrailsPanel()}
${renderMemoryPanel()}
${renderSkillsConfigPanel()}
${renderCuratorConfigPanel()}
${renderQuickCommandsConfigPanel()}
${renderModelConfigPanel()}
${renderModelAliasesConfigPanel()}
@@ -2284,6 +2353,7 @@ export function render() {
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)
el.querySelector('#hm-curator-config-save')?.addEventListener('click', saveCuratorConfig)
el.querySelector('#hm-quick-commands-save')?.addEventListener('click', saveQuickCommandsConfig)
el.querySelector('#hm-model-config-save')?.addEventListener('click', saveModelConfig)
el.querySelector('#hm-model-aliases-save')?.addEventListener('click', saveModelAliasesConfig)
@@ -2361,6 +2431,11 @@ export function render() {
skillsValues = { ...SKILLS_DEFAULTS, ...(data?.values || {}) }
}
async function loadCuratorConfig() {
const data = await api.hermesCuratorConfigRead()
curatorValues = { ...CURATOR_DEFAULTS, ...(data?.values || {}) }
}
async function loadQuickCommandsConfig() {
const data = await api.hermesQuickCommandsConfigRead()
quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS, ...(data?.values || {}) }
@@ -2497,6 +2572,7 @@ export function render() {
toolGuardrailsLoading = true
memoryLoading = true
skillsLoading = true
curatorLoading = true
quickCommandsLoading = true
modelLoading = true
modelAliasesLoading = true
@@ -2532,6 +2608,7 @@ export function render() {
toolGuardrailsError = null
memoryError = null
skillsError = null
curatorError = null
quickCommandsError = null
modelError = null
modelAliasesError = null
@@ -2724,6 +2801,14 @@ export function render() {
skillsLoading = false
draw()
}
try {
await loadCuratorConfig()
} catch (err) {
curatorError = humanizeError(err, t('engine.hermesCuratorConfigLoadFailed') || 'Load curator config failed')
} finally {
curatorLoading = false
draw()
}
try {
await loadQuickCommandsConfig()
} catch (err) {
@@ -2884,6 +2969,9 @@ export function render() {
try {
await loadSkillsConfig()
} catch {}
try {
await loadCuratorConfig()
} catch {}
try {
await loadQuickCommandsConfig()
} catch {}
@@ -3229,6 +3317,37 @@ export function render() {
}
}
async function saveCuratorConfig() {
const form = {
curatorEnabled: !!el.querySelector('#hm-curator-enabled')?.checked,
curatorIntervalHours: el.querySelector('#hm-curator-interval-hours')?.value || '168',
curatorMinIdleHours: el.querySelector('#hm-curator-min-idle-hours')?.value || '2',
curatorStaleAfterDays: el.querySelector('#hm-curator-stale-after-days')?.value || '30',
curatorArchiveAfterDays: el.querySelector('#hm-curator-archive-after-days')?.value || '90',
curatorBackupEnabled: !!el.querySelector('#hm-curator-backup-enabled')?.checked,
curatorBackupKeep: el.querySelector('#hm-curator-backup-keep')?.value || '5',
}
curatorSaving = true
curatorError = null
draw()
try {
const result = await api.hermesCuratorConfigSave(form)
curatorValues = { ...CURATOR_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesCuratorConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
curatorError = humanizeError(err, t('engine.hermesCuratorConfigSaveFailed') || 'Save curator config failed')
toast(curatorError, 'error')
} finally {
curatorSaving = false
draw()
}
}
async function saveQuickCommandsConfig() {
const form = {
quickCommandsJson: el.querySelector('#hm-quick-commands-json')?.value || '{}',

View File

@@ -527,6 +527,8 @@ export const api = {
hermesMemoryConfigSave: (form) => invoke('hermes_memory_config_save', { form }),
hermesSkillsConfigRead: () => invoke('hermes_skills_config_read'),
hermesSkillsConfigSave: (form) => invoke('hermes_skills_config_save', { form }),
hermesCuratorConfigRead: () => invoke('hermes_curator_config_read'),
hermesCuratorConfigSave: (form) => invoke('hermes_curator_config_save', { form }),
hermesQuickCommandsConfigRead: () => invoke('hermes_quick_commands_config_read'),
hermesQuickCommandsConfigSave: (form) => invoke('hermes_quick_commands_config_save', { form }),
hermesModelConfigRead: () => invoke('hermes_model_config_read'),

View File

@@ -840,6 +840,21 @@ export default {
hermesSkillsConfigGuardAgentCreated: _('扫描 Agent 创建的技能', 'Scan agent-created skills', '掃描 Agent 建立的技能'),
hermesSkillsConfigExternalDirs: _('外部技能目录(每行一个)', 'External skill directories, one per line', '外部技能目錄(每行一個)'),
hermesSkillsConfigFootnote: _('提醒间隔按用户消息轮数计算,设为 0 可关闭创建提醒。内联命令会在本机执行仅对可信技能源开启外部目录、disabled 和 custom flag 等字段会保留在 raw YAML 中。', 'The nudge interval is counted in user turns. Set it to 0 to disable creation nudges. Inline shell commands run on this machine, so enable them only for trusted skill sources. External dirs, disabled skills, and custom flags are preserved in raw YAML.', '提醒間隔依使用者訊息輪數計算,設為 0 可關閉建立提醒。內嵌命令會在本機執行僅對可信技能來源開啟外部目錄、disabled 和 custom flag 等欄位會保留在 raw YAML 中。'),
hermesCuratorConfigTitle: _('技能维护 Curator', 'Skill curator', '技能維護 Curator'),
hermesCuratorConfigDesc: _('配置 Hermes 后台整理 Agent 创建技能的周期、闲置阈值、归档阈值和备份保留数量。', 'Configure how Hermes reviews, marks stale, archives, and backs up agent-created skills in the background.', '設定 Hermes 在背景整理 Agent 建立技能的週期、閒置門檻、封存門檻和備份保留數量。'),
hermesCuratorConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesCuratorConfigSave: _('保存 Curator 配置', 'Save curator settings', '儲存 Curator 設定'),
hermesCuratorConfigSaveSuccess: _('Curator 配置已保存,建议重启 Hermes Gateway 生效', 'Curator settings saved. Restart Hermes Gateway to take effect.', 'Curator 設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesCuratorConfigLoadFailed: _('加载 Curator 配置失败', 'Load curator settings failed', '載入 Curator 設定失敗'),
hermesCuratorConfigSaveFailed: _('保存 Curator 配置失败', 'Save curator settings failed', '儲存 Curator 設定失敗'),
hermesCuratorConfigEnabled: _('启用技能维护', 'Enable skill curator', '啟用技能維護'),
hermesCuratorConfigIntervalHours: _('整理间隔(小时)', 'Review interval, hours', '整理間隔(小時)'),
hermesCuratorConfigMinIdleHours: _('最少闲置时间(小时)', 'Minimum idle time, hours', '最少閒置時間(小時)'),
hermesCuratorConfigStaleAfterDays: _('标记过期天数', 'Mark stale after days', '標記過期天數'),
hermesCuratorConfigArchiveAfterDays: _('归档天数', 'Archive after days', '封存天數'),
hermesCuratorConfigBackupEnabled: _('整理前创建备份', 'Create backup before curating', '整理前建立備份'),
hermesCuratorConfigBackupKeep: _('保留备份数量', 'Backups to keep', '保留備份數量'),
hermesCuratorConfigFootnote: _('Curator 只整理 Agent 创建的技能,不会处理内置或 hub 安装技能归档天数不能小于过期天数。custom flag 等高级字段会保留在 raw YAML 中。', 'Curator only maintains agent-created skills, never bundled or hub-installed skills. Archive days must be greater than or equal to stale days. Custom flags stay in raw YAML.', 'Curator 只整理 Agent 建立的技能,不會處理內建或 hub 安裝技能封存天數不可小於過期天數。custom flag 等進階欄位會保留在 raw YAML 中。'),
hermesQuickCommandsConfigTitle: _('快捷命令', 'Quick commands', '快捷命令'),
hermesQuickCommandsConfigDesc: _('配置消息平台和 CLI 可直接触发的零 token 运维命令,例如状态检查、磁盘空间和 Gateway 重启别名。', 'Configure zero-token operations commands that messaging platforms and the CLI can trigger directly, such as status checks, disk usage, and Gateway restart aliases.', '設定訊息平台和 CLI 可直接觸發的零 token 維運命令,例如狀態檢查、磁碟空間和 Gateway 重啟別名。'),
hermesQuickCommandsConfigStatusReady: _('结构化 JSON', 'structured JSON', '結構化 JSON'),