feat(hermes): add compression config form

This commit is contained in:
晴天
2026-05-24 07:16:00 +08:00
parent f500da39c1
commit 5dd6f1be40
8 changed files with 560 additions and 2 deletions

View File

@@ -14,6 +14,15 @@ const SESSION_RUNTIME_DEFAULTS = {
threadSessionsPerUser: false,
}
const COMPRESSION_DEFAULTS = {
enabled: true,
threshold: 0.5,
targetRatio: 0.2,
protectLastN: 20,
protectFirstN: 3,
abortOnSummaryFailure: false,
}
const SESSION_RESET_MODES = ['both', 'idle', 'daily', 'none']
export function render() {
@@ -22,12 +31,16 @@ export function render() {
el.dataset.engine = 'hermes'
let yaml = ''
let runtimeValues = { ...SESSION_RUNTIME_DEFAULTS }
let compressionValues = { ...COMPRESSION_DEFAULTS }
let loading = true
let runtimeLoading = true
let compressionLoading = true
let saving = false
let runtimeSaving = false
let compressionSaving = false
let error = null
let runtimeError = null
let compressionError = null
function esc(value) {
return String(value || '')
@@ -38,7 +51,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || saving || runtimeSaving
return loading || runtimeLoading || compressionLoading || saving || runtimeSaving || compressionSaving
}
function option(labelKey, value, selected) {
@@ -55,7 +68,7 @@ export function render() {
}
function renderRuntimePanel() {
const disabled = loading || saving || runtimeLoading || runtimeSaving
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving
return `
<div class="hm-panel hm-config-runtime-panel">
<div class="hm-panel-header">
@@ -102,6 +115,56 @@ export function render() {
`
}
function renderCompressionPanel() {
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-compression-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesCompressionTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesCompressionDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${compressionSaving ? t('engine.hermesConfigStatusSaving') : compressionLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesCompressionStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-compression-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesCompressionSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(compressionError)}
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-compression-enabled" type="checkbox" ${compressionValues.enabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesCompressionEnabled')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-compression-abort-on-summary-failure" type="checkbox" ${compressionValues.abortOnSummaryFailure ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesCompressionAbortOnSummaryFailure')}</span>
</label>
</div>
<div class="hm-config-runtime-grid hm-config-compression-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCompressionThreshold')}</span>
<input id="hm-compression-threshold" class="hm-input" type="number" inputmode="decimal" min="0.1" max="0.95" step="0.05" value="${esc(compressionValues.threshold)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCompressionTargetRatio')}</span>
<input id="hm-compression-target-ratio" class="hm-input" type="number" inputmode="decimal" min="0.1" max="0.8" step="0.05" value="${esc(compressionValues.targetRatio)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCompressionProtectLastN')}</span>
<input id="hm-compression-protect-last-n" class="hm-input" type="number" inputmode="numeric" min="1" max="500" step="1" value="${esc(compressionValues.protectLastN)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesCompressionProtectFirstN')}</span>
<input id="hm-compression-protect-first-n" class="hm-input" type="number" inputmode="numeric" min="0" max="100" step="1" value="${esc(compressionValues.protectFirstN)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesCompressionFootnote')}</div>
</div>
</div>
`
}
function draw() {
el.innerHTML = `
<div class="hm-hero">
@@ -117,6 +180,7 @@ export function render() {
</div>
${renderRuntimePanel()}
${renderCompressionPanel()}
<div class="hm-panel">
<div class="hm-panel-header">
@@ -137,6 +201,7 @@ export function render() {
el.querySelector('#hm-config-reload')?.addEventListener('click', load)
el.querySelector('#hm-config-save')?.addEventListener('click', save)
el.querySelector('#hm-runtime-save')?.addEventListener('click', saveRuntime)
el.querySelector('#hm-compression-save')?.addEventListener('click', saveCompression)
}
async function loadRaw() {
@@ -149,11 +214,18 @@ export function render() {
runtimeValues = { ...SESSION_RUNTIME_DEFAULTS, ...(data?.values || {}) }
}
async function loadCompression() {
const data = await api.hermesCompressionConfigRead()
compressionValues = { ...COMPRESSION_DEFAULTS, ...(data?.values || {}) }
}
async function load() {
loading = true
runtimeLoading = true
compressionLoading = true
error = null
runtimeError = null
compressionError = null
draw()
try {
await loadRaw()
@@ -170,6 +242,14 @@ export function render() {
runtimeLoading = false
draw()
}
try {
await loadCompression()
} catch (err) {
compressionError = humanizeError(err, t('engine.hermesCompressionLoadFailed') || 'Load compression config failed')
} finally {
compressionLoading = false
draw()
}
}
async function refreshRawAfterStructuredSave() {
@@ -194,6 +274,9 @@ export function render() {
try {
await loadRuntime()
} catch {}
try {
await loadCompression()
} catch {}
} catch (err) {
error = humanizeError(err, t('engine.hermesConfigSaveFailed') || 'Save failed')
toast(error, 'error')
@@ -232,6 +315,36 @@ export function render() {
}
}
async function saveCompression() {
const form = {
enabled: !!el.querySelector('#hm-compression-enabled')?.checked,
threshold: el.querySelector('#hm-compression-threshold')?.value || '0.5',
targetRatio: el.querySelector('#hm-compression-target-ratio')?.value || '0.2',
protectLastN: el.querySelector('#hm-compression-protect-last-n')?.value || '20',
protectFirstN: el.querySelector('#hm-compression-protect-first-n')?.value || '3',
abortOnSummaryFailure: !!el.querySelector('#hm-compression-abort-on-summary-failure')?.checked,
}
compressionSaving = true
compressionError = null
draw()
try {
const result = await api.hermesCompressionConfigSave(form)
compressionValues = { ...COMPRESSION_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesCompressionSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
compressionError = humanizeError(err, t('engine.hermesCompressionSaveFailed') || 'Save compression config failed')
toast(compressionError, 'error')
} finally {
compressionSaving = false
draw()
}
}
draw()
load()
return el

View File

@@ -6753,6 +6753,10 @@ body[data-active-engine="hermes"][data-theme="dark"] {
gap: 16px;
align-items: end;
}
[data-engine="hermes"] .hm-config-compression-grid {
grid-template-columns: repeat(4, minmax(140px, 1fr));
margin-top: 18px;
}
[data-engine="hermes"] .hm-config-check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -6876,6 +6880,7 @@ body[data-active-engine="hermes"][data-theme="dark"] {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
[data-engine="hermes"] .hm-config-runtime-grid,
[data-engine="hermes"] .hm-config-compression-grid,
[data-engine="hermes"] .hm-config-check-grid {
grid-template-columns: 1fr;
}
@@ -6941,6 +6946,9 @@ body[data-active-engine="hermes"][data-theme="dark"] {
width: 100%;
flex-wrap: wrap;
}
[data-engine="hermes"] .hm-config-runtime-panel .hm-panel-actions .hm-muted {
width: 100%;
}
[data-engine="hermes"] .hm-config-runtime-panel .hm-panel-actions .hm-btn {
width: 100%;
}

View File

@@ -511,6 +511,8 @@ export const api = {
hermesChannelConfigSave: (platform, form) => invoke('hermes_channel_config_save', { platform, form }),
hermesSessionRuntimeConfigRead: () => invoke('hermes_session_runtime_config_read'),
hermesSessionRuntimeConfigSave: (form) => invoke('hermes_session_runtime_config_save', { form }),
hermesCompressionConfigRead: () => invoke('hermes_compression_config_read'),
hermesCompressionConfigSave: (form) => invoke('hermes_compression_config_save', { form }),
hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000),
hermesLazyDepsStatus: (features) => invoke('hermes_lazy_deps_status', { features }),
hermesLazyDepsEnsure: (feature) => invoke('hermes_lazy_deps_ensure', { feature }),

View File

@@ -498,6 +498,20 @@ export default {
hermesGroupSessionsPerUser: _('群聊按成员隔离会话', 'Isolate group sessions per user', '群聊依成員隔離會話'),
hermesThreadSessionsPerUser: _('线程也按成员隔离', 'Isolate thread sessions per user', '討論串也依成員隔離'),
hermesSessionRuntimeFootnote: _('推荐保持群聊隔离开启。关闭后,同一群/频道会共用上下文和中断状态。', 'Keeping group isolation on is recommended. Turning it off shares context and interrupt state across the same group or channel.', '建議保持群聊隔離開啟。關閉後,同一群組/頻道會共用上下文和中斷狀態。'),
hermesCompressionTitle: _('上下文压缩', 'Context compression', '上下文壓縮'),
hermesCompressionDesc: _('控制长对话何时触发压缩、压缩目标和保留范围,降低上下文过长导致的失败与费用浪费。', 'Control when long conversations are compressed, the target size, and protected message ranges to reduce failures and wasted cost from oversized context.', '控制長對話何時觸發壓縮、壓縮目標和保留範圍,降低上下文過長導致的失敗與費用浪費。'),
hermesCompressionStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesCompressionSave: _('保存压缩配置', 'Save compression settings', '儲存壓縮設定'),
hermesCompressionSaveSuccess: _('压缩配置已保存,建议重启 Hermes Gateway 生效', 'Compression settings saved. Restart Hermes Gateway to take effect.', '壓縮設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesCompressionLoadFailed: _('加载压缩配置失败', 'Load compression settings failed', '載入壓縮設定失敗'),
hermesCompressionSaveFailed: _('保存压缩配置失败', 'Save compression settings failed', '儲存壓縮設定失敗'),
hermesCompressionEnabled: _('启用自动压缩', 'Enable automatic compression', '啟用自動壓縮'),
hermesCompressionThreshold: _('触发阈值', 'Trigger threshold', '觸發閾值'),
hermesCompressionTargetRatio: _('压缩目标比例', 'Target ratio', '壓縮目標比例'),
hermesCompressionProtectLastN: _('保护最近消息数', 'Protect latest messages', '保護最近訊息數'),
hermesCompressionProtectFirstN: _('保护开头消息数', 'Protect first messages', '保護開頭訊息數'),
hermesCompressionAbortOnSummaryFailure: _('摘要失败时中止回复', 'Abort when summarization fails', '摘要失敗時中止回覆'),
hermesCompressionFootnote: _('阈值和目标比例越低,压缩越早、越激进。建议先使用默认值,再根据真实 Gateway 日志调整。', 'Lower thresholds and target ratios compress earlier and more aggressively. Start with the defaults, then tune with real Gateway logs.', '閾值和目標比例越低,壓縮越早、越激進。建議先使用預設值,再根據真實 Gateway 日誌調整。'),
// Batch 1 §E: 会话导出
sessionsExport: _('导出', 'Export', '匯出'),
sessionsExportSuccess: _('已导出', 'Exported', '已匯出'),