mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
feat(hermes): add compression config form
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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', '已匯出'),
|
||||
|
||||
Reference in New Issue
Block a user