feat(hermes): add streaming config form

This commit is contained in:
晴天
2026-05-24 08:40:53 +08:00
parent 1883e18f02
commit a1307716dd
9 changed files with 656 additions and 6 deletions

View File

@@ -42,7 +42,17 @@ const MEMORY_DEFAULTS = {
nudgeInterval: 10,
}
const STREAMING_DEFAULTS = {
enabled: false,
transport: 'edit',
editInterval: 0.8,
bufferThreshold: 24,
cursor: ' ▉',
freshFinalAfterSeconds: 60,
}
const SESSION_RESET_MODES = ['both', 'idle', 'daily', 'none']
const STREAMING_TRANSPORTS = ['edit', 'auto', 'draft', 'off']
export function render() {
const el = document.createElement('div')
@@ -53,21 +63,25 @@ export function render() {
let compressionValues = { ...COMPRESSION_DEFAULTS }
let toolGuardrailsValues = { ...TOOL_GUARDRAILS_DEFAULTS }
let memoryValues = { ...MEMORY_DEFAULTS }
let streamingValues = { ...STREAMING_DEFAULTS }
let loading = true
let runtimeLoading = true
let compressionLoading = true
let toolGuardrailsLoading = true
let memoryLoading = true
let streamingLoading = true
let saving = false
let runtimeSaving = false
let compressionSaving = false
let toolGuardrailsSaving = false
let memorySaving = false
let streamingSaving = false
let error = null
let runtimeError = null
let compressionError = null
let toolGuardrailsError = null
let memoryError = null
let streamingError = null
function esc(value) {
return String(value ?? '')
@@ -78,7 +92,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || streamingLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving
}
function option(labelKey, value, selected) {
@@ -95,7 +109,7 @@ export function render() {
}
function renderRuntimePanel() {
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving
return `
<div class="hm-panel hm-config-runtime-panel">
<div class="hm-panel-header">
@@ -143,7 +157,7 @@ export function render() {
}
function renderCompressionPanel() {
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || streamingSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-compression-panel">
<div class="hm-panel-header">
@@ -193,7 +207,7 @@ export function render() {
}
function renderToolGuardrailsPanel() {
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || streamingSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-guardrails-panel">
<div class="hm-panel-header">
@@ -255,7 +269,7 @@ export function render() {
}
function renderMemoryPanel() {
const disabled = loading || saving || memoryLoading || memorySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving
const disabled = loading || saving || memoryLoading || memorySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-memory-panel">
<div class="hm-panel-header">
@@ -300,6 +314,58 @@ export function render() {
`
}
function renderStreamingPanel() {
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-streaming-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesStreamingConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesStreamingConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${streamingSaving ? t('engine.hermesConfigStatusSaving') : streamingLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesStreamingConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-streaming-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesStreamingConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(streamingError)}
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-streaming-enabled" type="checkbox" ${streamingValues.enabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesStreamingConfigEnabled')}</span>
</label>
</div>
<div class="hm-config-runtime-grid hm-config-streaming-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesStreamingConfigTransport')}</span>
<select id="hm-streaming-transport" class="hm-input" ${disabled ? 'disabled' : ''}>
${STREAMING_TRANSPORTS.map(mode => option(`engine.hermesStreamingConfigTransport_${mode}`, mode, streamingValues.transport)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesStreamingConfigEditInterval')}</span>
<input id="hm-streaming-edit-interval" class="hm-input" type="number" inputmode="decimal" min="0.05" max="60" step="0.05" value="${esc(streamingValues.editInterval)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesStreamingConfigBufferThreshold')}</span>
<input id="hm-streaming-buffer-threshold" class="hm-input" type="number" inputmode="numeric" min="1" max="5000" step="1" value="${esc(streamingValues.bufferThreshold)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesStreamingConfigFreshFinalAfterSeconds')}</span>
<input id="hm-streaming-fresh-final-after-seconds" class="hm-input" type="number" inputmode="decimal" min="0" max="86400" step="1" value="${esc(streamingValues.freshFinalAfterSeconds)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesStreamingConfigCursor')}</span>
<input id="hm-streaming-cursor" class="hm-input" value="${esc(streamingValues.cursor)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesStreamingConfigFootnote')}</div>
</div>
</div>
`
}
function draw() {
el.innerHTML = `
<div class="hm-hero">
@@ -315,6 +381,7 @@ export function render() {
</div>
${renderRuntimePanel()}
${renderStreamingPanel()}
${renderCompressionPanel()}
${renderToolGuardrailsPanel()}
${renderMemoryPanel()}
@@ -341,6 +408,7 @@ export function render() {
el.querySelector('#hm-compression-save')?.addEventListener('click', saveCompression)
el.querySelector('#hm-tool-guardrails-save')?.addEventListener('click', saveToolGuardrails)
el.querySelector('#hm-memory-save')?.addEventListener('click', saveMemory)
el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming)
}
async function loadRaw() {
@@ -368,17 +436,24 @@ export function render() {
memoryValues = { ...MEMORY_DEFAULTS, ...(data?.values || {}) }
}
async function loadStreaming() {
const data = await api.hermesStreamingConfigRead()
streamingValues = { ...STREAMING_DEFAULTS, ...(data?.values || {}) }
}
async function load() {
loading = true
runtimeLoading = true
compressionLoading = true
toolGuardrailsLoading = true
memoryLoading = true
streamingLoading = true
error = null
runtimeError = null
compressionError = null
toolGuardrailsError = null
memoryError = null
streamingError = null
draw()
try {
await loadRaw()
@@ -411,6 +486,14 @@ export function render() {
toolGuardrailsLoading = false
draw()
}
try {
await loadStreaming()
} catch (err) {
streamingError = humanizeError(err, t('engine.hermesStreamingConfigLoadFailed') || 'Load streaming config failed')
} finally {
streamingLoading = false
draw()
}
try {
await loadMemory()
} catch (err) {
@@ -452,6 +535,9 @@ export function render() {
try {
await loadMemory()
} catch {}
try {
await loadStreaming()
} catch {}
} catch (err) {
error = humanizeError(err, t('engine.hermesConfigSaveFailed') || 'Save failed')
toast(error, 'error')
@@ -581,6 +667,36 @@ export function render() {
}
}
async function saveStreaming() {
const form = {
enabled: !!el.querySelector('#hm-streaming-enabled')?.checked,
transport: el.querySelector('#hm-streaming-transport')?.value || 'edit',
editInterval: el.querySelector('#hm-streaming-edit-interval')?.value || '0.8',
bufferThreshold: el.querySelector('#hm-streaming-buffer-threshold')?.value || '24',
cursor: el.querySelector('#hm-streaming-cursor')?.value ?? ' ▉',
freshFinalAfterSeconds: el.querySelector('#hm-streaming-fresh-final-after-seconds')?.value || '60',
}
streamingSaving = true
streamingError = null
draw()
try {
const result = await api.hermesStreamingConfigSave(form)
streamingValues = { ...STREAMING_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesStreamingConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
streamingError = humanizeError(err, t('engine.hermesStreamingConfigSaveFailed') || 'Save streaming config failed')
toast(streamingError, 'error')
} finally {
streamingSaving = false
draw()
}
}
draw()
load()
return el

View File

@@ -6765,6 +6765,10 @@ body[data-active-engine="hermes"][data-theme="dark"] {
grid-template-columns: repeat(3, minmax(160px, 1fr));
margin-top: 18px;
}
[data-engine="hermes"] .hm-config-streaming-grid {
grid-template-columns: repeat(5, minmax(140px, 1fr));
margin-top: 18px;
}
[data-engine="hermes"] .hm-config-subtitle {
margin-top: 20px;
color: var(--hm-text-secondary);
@@ -6898,6 +6902,7 @@ body[data-active-engine="hermes"][data-theme="dark"] {
[data-engine="hermes"] .hm-config-compression-grid,
[data-engine="hermes"] .hm-config-guardrails-grid,
[data-engine="hermes"] .hm-config-memory-grid,
[data-engine="hermes"] .hm-config-streaming-grid,
[data-engine="hermes"] .hm-config-check-grid {
grid-template-columns: 1fr;
}

View File

@@ -517,6 +517,8 @@ export const api = {
hermesToolLoopGuardrailsConfigSave: (form) => invoke('hermes_tool_loop_guardrails_config_save', { form }),
hermesMemoryConfigRead: () => invoke('hermes_memory_config_read'),
hermesMemoryConfigSave: (form) => invoke('hermes_memory_config_save', { form }),
hermesStreamingConfigRead: () => invoke('hermes_streaming_config_read'),
hermesStreamingConfigSave: (form) => invoke('hermes_streaming_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,24 @@ 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.', '建議保持群聊隔離開啟。關閉後,同一群組/頻道會共用上下文和中斷狀態。'),
hermesStreamingConfigTitle: _('网关流式输出', 'Gateway streaming output', '閘道流式輸出'),
hermesStreamingConfigDesc: _('控制 Hermes Gateway 回复时是否边生成边更新消息,以及消息刷新节奏。适合需要更快看到长回复进度的渠道。', 'Control whether Hermes Gateway updates messages while replies are generated, plus the refresh cadence. Useful when channels need quicker progress for long replies.', '控制 Hermes Gateway 回覆時是否邊生成邊更新訊息,以及訊息刷新節奏。適合需要更快看到長回覆進度的渠道。'),
hermesStreamingConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesStreamingConfigSave: _('保存流式配置', 'Save streaming settings', '儲存流式設定'),
hermesStreamingConfigSaveSuccess: _('流式配置已保存,建议重启 Hermes Gateway 生效', 'Streaming settings saved. Restart Hermes Gateway to take effect.', '流式設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesStreamingConfigLoadFailed: _('加载流式配置失败', 'Load streaming settings failed', '載入流式設定失敗'),
hermesStreamingConfigSaveFailed: _('保存流式配置失败', 'Save streaming settings failed', '儲存流式設定失敗'),
hermesStreamingConfigEnabled: _('启用流式输出', 'Enable streaming output', '啟用流式輸出'),
hermesStreamingConfigTransport: _('消息更新方式', 'Message update mode', '訊息更新方式'),
hermesStreamingConfigTransport_edit: _('编辑原消息', 'Edit original message', '編輯原訊息'),
hermesStreamingConfigTransport_auto: _('自动选择', 'Auto select', '自動選擇'),
hermesStreamingConfigTransport_draft: _('草稿式更新', 'Draft updates', '草稿式更新'),
hermesStreamingConfigTransport_off: _('关闭更新', 'Disable updates', '關閉更新'),
hermesStreamingConfigEditInterval: _('消息编辑间隔(秒)', 'Message edit interval (s)', '訊息編輯間隔(秒)'),
hermesStreamingConfigBufferThreshold: _('触发刷新字符数', 'Refresh trigger characters', '觸發刷新字元數'),
hermesStreamingConfigFreshFinalAfterSeconds: _('长回复完成新消息时间(秒)', 'Fresh final message after (s)', '長回覆完成新訊息時間(秒)'),
hermesStreamingConfigCursor: _('生成中标记', 'In-progress marker', '生成中標記'),
hermesStreamingConfigFootnote: _('这里写入顶层 streaming 配置;旧版 gateway.streaming、display.streaming 和其他高级字段会保留在 raw YAML 中。将“长回复完成新消息时间”设为 0 可关闭完成后新消息。', 'This writes the top-level streaming settings. Legacy gateway.streaming, display.streaming, and other advanced fields are preserved in raw YAML. Set fresh final message time to 0 to disable the final follow-up message.', '這裡寫入頂層 streaming 設定;舊版 gateway.streaming、display.streaming 和其他進階欄位會保留在 raw YAML 中。將「長回覆完成新訊息時間」設為 0 可關閉完成後新訊息。'),
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', '結構化設定'),