feat(hermes): add display reliability settings

This commit is contained in:
晴天
2026-05-24 23:03:41 +08:00
parent 8c963cd3d4
commit d245d2e320
8 changed files with 839 additions and 1 deletions

View File

@@ -63,6 +63,17 @@ const SECURITY_DEFAULTS = {
tirithFailOpen: true,
}
const DISPLAY_DEFAULTS = {
displayToolProgress: 'all',
displayToolProgressCommand: false,
displayInterimAssistantMessages: true,
displayRuntimeFooterEnabled: false,
displayRuntimeFooterFields: 'model\ncontext_pct\ncwd',
displayFileMutationVerifier: true,
displayLanguage: 'en',
displayResumeDisplay: 'full',
}
const HUMAN_DELAY_DEFAULTS = {
humanDelayMode: 'off',
humanDelayMinMs: 800,
@@ -109,6 +120,9 @@ const STREAMING_TRANSPORTS = ['edit', 'auto', 'draft', 'off']
const CODE_EXECUTION_MODES = ['project', 'strict']
const TERMINAL_BACKENDS = ['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox']
const UNAUTHORIZED_DM_BEHAVIORS = ['pair', 'ignore']
const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose']
const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu']
const DISPLAY_RESUME_VALUES = ['full', 'minimal']
const HUMAN_DELAY_MODES = ['off', 'natural', 'custom']
export function render() {
@@ -124,6 +138,7 @@ export function render() {
let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS }
let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS }
let securityValues = { ...SECURITY_DEFAULTS }
let displayValues = { ...DISPLAY_DEFAULTS }
let humanDelayValues = { ...HUMAN_DELAY_DEFAULTS }
let streamingValues = { ...STREAMING_DEFAULTS }
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
@@ -137,6 +152,7 @@ export function render() {
let quickCommandsLoading = true
let unauthorizedDmLoading = true
let securityLoading = true
let displayLoading = true
let humanDelayLoading = true
let streamingLoading = true
let executionLimitsLoading = true
@@ -150,6 +166,7 @@ export function render() {
let quickCommandsSaving = false
let unauthorizedDmSaving = false
let securitySaving = false
let displaySaving = false
let humanDelaySaving = false
let streamingSaving = false
let executionLimitsSaving = false
@@ -163,6 +180,7 @@ export function render() {
let quickCommandsError = null
let unauthorizedDmError = null
let securityError = null
let displayError = null
let humanDelayError = null
let streamingError = null
let executionLimitsError = null
@@ -177,7 +195,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -533,6 +551,70 @@ export function render() {
`
}
function renderDisplayConfigPanel() {
const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-display-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesDisplayConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesDisplayConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${displaySaving ? t('engine.hermesConfigStatusSaving') : displayLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesDisplayConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-display-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesDisplayConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(displayError)}
<div class="hm-config-runtime-grid hm-config-display-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesDisplayConfigToolProgress')}</span>
<select id="hm-display-tool-progress" class="hm-input" ${disabled ? 'disabled' : ''}>
${DISPLAY_TOOL_PROGRESS_VALUES.map(mode => option(`engine.hermesDisplayConfigToolProgress_${mode}`, mode, displayValues.displayToolProgress)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesDisplayConfigLanguage')}</span>
<select id="hm-display-language" class="hm-input" ${disabled ? 'disabled' : ''}>
${DISPLAY_LANGUAGE_VALUES.map(mode => option(`engine.hermesDisplayConfigLanguage_${mode}`, mode, displayValues.displayLanguage)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesDisplayConfigResumeDisplay')}</span>
<select id="hm-display-resume-display" class="hm-input" ${disabled ? 'disabled' : ''}>
${DISPLAY_RESUME_VALUES.map(mode => option(`engine.hermesDisplayConfigResumeDisplay_${mode}`, mode, displayValues.displayResumeDisplay)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesDisplayConfigRuntimeFooterFields')}</span>
<textarea id="hm-display-runtime-footer-fields" class="hm-input" ${disabled ? 'disabled' : ''} style="min-height:96px;resize:vertical">${esc(displayValues.displayRuntimeFooterFields)}</textarea>
</label>
</div>
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-display-tool-progress-command" type="checkbox" ${displayValues.displayToolProgressCommand ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesDisplayConfigToolProgressCommand')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-display-interim-assistant-messages" type="checkbox" ${displayValues.displayInterimAssistantMessages ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesDisplayConfigInterimAssistantMessages')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-display-runtime-footer-enabled" type="checkbox" ${displayValues.displayRuntimeFooterEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesDisplayConfigRuntimeFooterEnabled')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-display-file-mutation-verifier" type="checkbox" ${displayValues.displayFileMutationVerifier ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesDisplayConfigFileMutationVerifier')}</span>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesDisplayConfigFootnote')}</div>
</div>
</div>
`
}
function renderHumanDelayConfigPanel() {
const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
@@ -791,6 +873,7 @@ export function render() {
${renderQuickCommandsConfigPanel()}
${renderUnauthorizedDmConfigPanel()}
${renderSecurityConfigPanel()}
${renderDisplayConfigPanel()}
${renderHumanDelayConfigPanel()}
<div class="hm-panel">
@@ -819,6 +902,7 @@ export function render() {
el.querySelector('#hm-quick-commands-save')?.addEventListener('click', saveQuickCommandsConfig)
el.querySelector('#hm-unauthorized-dm-save')?.addEventListener('click', saveUnauthorizedDmConfig)
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-streaming-save')?.addEventListener('click', saveStreaming)
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
@@ -870,6 +954,11 @@ export function render() {
securityValues = { ...SECURITY_DEFAULTS, ...(data?.values || {}) }
}
async function loadDisplayConfig() {
const data = await api.hermesDisplayConfigRead()
displayValues = { ...DISPLAY_DEFAULTS, ...(data?.values || {}) }
}
async function loadHumanDelayConfig() {
const data = await api.hermesHumanDelayConfigRead()
humanDelayValues = { ...HUMAN_DELAY_DEFAULTS, ...(data?.values || {}) }
@@ -900,6 +989,7 @@ export function render() {
quickCommandsLoading = true
unauthorizedDmLoading = true
securityLoading = true
displayLoading = true
humanDelayLoading = true
streamingLoading = true
executionLimitsLoading = true
@@ -913,6 +1003,7 @@ export function render() {
quickCommandsError = null
unauthorizedDmError = null
securityError = null
displayError = null
humanDelayError = null
streamingError = null
executionLimitsError = null
@@ -1013,6 +1104,14 @@ export function render() {
securityLoading = false
draw()
}
try {
await loadDisplayConfig()
} catch (err) {
displayError = humanizeError(err, t('engine.hermesDisplayConfigLoadFailed') || 'Load display config failed')
} finally {
displayLoading = false
draw()
}
try {
await loadHumanDelayConfig()
} catch (err) {
@@ -1066,6 +1165,9 @@ export function render() {
try {
await loadSecurityConfig()
} catch {}
try {
await loadDisplayConfig()
} catch {}
try {
await loadHumanDelayConfig()
} catch {}
@@ -1312,6 +1414,38 @@ export function render() {
}
}
async function saveDisplayConfig() {
const form = {
displayToolProgress: el.querySelector('#hm-display-tool-progress')?.value || 'all',
displayToolProgressCommand: !!el.querySelector('#hm-display-tool-progress-command')?.checked,
displayInterimAssistantMessages: !!el.querySelector('#hm-display-interim-assistant-messages')?.checked,
displayRuntimeFooterEnabled: !!el.querySelector('#hm-display-runtime-footer-enabled')?.checked,
displayRuntimeFooterFields: el.querySelector('#hm-display-runtime-footer-fields')?.value || 'model\ncontext_pct\ncwd',
displayFileMutationVerifier: !!el.querySelector('#hm-display-file-mutation-verifier')?.checked,
displayLanguage: el.querySelector('#hm-display-language')?.value || 'en',
displayResumeDisplay: el.querySelector('#hm-display-resume-display')?.value || 'full',
}
displaySaving = true
displayError = null
draw()
try {
const result = await api.hermesDisplayConfigSave(form)
displayValues = { ...DISPLAY_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesDisplayConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
displayError = humanizeError(err, t('engine.hermesDisplayConfigSaveFailed') || 'Save display config failed')
toast(displayError, 'error')
} finally {
displaySaving = false
draw()
}
}
async function saveHumanDelayConfig() {
const form = {
humanDelayMode: el.querySelector('#hm-human-delay-mode')?.value || 'off',

View File

@@ -525,6 +525,8 @@ export const api = {
hermesUnauthorizedDmConfigSave: (form) => invoke('hermes_unauthorized_dm_config_save', { form }),
hermesSecurityConfigRead: () => invoke('hermes_security_config_read'),
hermesSecurityConfigSave: (form) => invoke('hermes_security_config_save', { form }),
hermesDisplayConfigRead: () => invoke('hermes_display_config_read'),
hermesDisplayConfigSave: (form) => invoke('hermes_display_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

@@ -640,6 +640,44 @@ export default {
hermesUnauthorizedDmConfigBehavior_pair: _('回复配对码', 'Reply with pairing code', '回覆配對碼'),
hermesUnauthorizedDmConfigBehavior_ignore: _('静默忽略', 'Silently ignore', '靜默忽略'),
hermesUnauthorizedDmConfigFootnote: _('pair 是默认值会拒绝访问但在私信中回复一次性配对码ignore 会静默丢弃陌生私信。平台级覆盖仍可在渠道配置或 raw YAML 中单独设置。', 'pair is the default: Hermes denies access but replies with a one-time pairing code in DMs. ignore silently drops unknown DMs. Platform-level overrides can still be set in channel settings or raw YAML.', 'pair 是預設值會拒絕存取但在私訊中回覆一次性配對碼ignore 會靜默丟棄陌生私訊。平台級覆蓋仍可在頻道設定或 raw YAML 中單獨設定。'),
hermesDisplayConfigTitle: _('全局显示与可靠性', 'Global display and reliability', '全域顯示與可靠性'),
hermesDisplayConfigDesc: _('控制消息平台和 CLI 的默认进度展示、静态提示语言、运行信息页脚,以及文件写入失败校验。', 'Control default progress display, static prompt language, runtime footer, and failed file-mutation verification for messaging platforms and CLI.', '控制訊息平台和 CLI 的預設進度顯示、靜態提示語言、執行資訊頁腳,以及檔案寫入失敗校驗。'),
hermesDisplayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesDisplayConfigSave: _('保存显示设置', 'Save display settings', '儲存顯示設定'),
hermesDisplayConfigSaveSuccess: _('显示与可靠性配置已保存,建议重启 Hermes Gateway 生效', 'Display and reliability settings saved. Restart Hermes Gateway to take effect.', '顯示與可靠性設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesDisplayConfigLoadFailed: _('加载显示与可靠性配置失败', 'Load display and reliability settings failed', '載入顯示與可靠性設定失敗'),
hermesDisplayConfigSaveFailed: _('保存显示与可靠性配置失败', 'Save display and reliability settings failed', '儲存顯示與可靠性設定失敗'),
hermesDisplayConfigToolProgress: _('默认工具进度', 'Default tool progress', '預設工具進度'),
hermesDisplayConfigToolProgress_off: _('关闭', 'Off', '關閉'),
hermesDisplayConfigToolProgress_new: _('工具变化时显示', 'Only when tool changes', '工具變化時顯示'),
hermesDisplayConfigToolProgress_all: _('显示每次工具调用', 'Show every tool call', '顯示每次工具呼叫'),
hermesDisplayConfigToolProgress_verbose: _('详细显示参数和结果', 'Verbose args and results', '詳細顯示參數與結果'),
hermesDisplayConfigToolProgressCommand: _('在消息平台启用 /verbose 命令', 'Enable /verbose on messaging platforms', '在訊息平台啟用 /verbose 命令'),
hermesDisplayConfigInterimAssistantMessages: _('发送中途状态消息', 'Send interim assistant updates', '傳送中途狀態訊息'),
hermesDisplayConfigRuntimeFooterEnabled: _('在最终回复追加运行信息', 'Append runtime footer to final replies', '在最終回覆追加執行資訊'),
hermesDisplayConfigRuntimeFooterFields: _('运行信息字段(每行一个)', 'Runtime footer fields, one per line', '執行資訊欄位(每行一個)'),
hermesDisplayConfigFileMutationVerifier: _('启用文件写入失败校验', 'Enable failed file-mutation verifier', '啟用檔案寫入失敗校驗'),
hermesDisplayConfigLanguage: _('静态提示语言', 'Static prompt language', '靜態提示語言'),
hermesDisplayConfigLanguage_en: _('英语', 'English', '英語'),
hermesDisplayConfigLanguage_zh: _('简体中文', 'Simplified Chinese', '簡體中文'),
'hermesDisplayConfigLanguage_zh-hant': _('繁体中文', 'Traditional Chinese', '繁體中文'),
hermesDisplayConfigLanguage_ja: _('日语', 'Japanese', '日語'),
hermesDisplayConfigLanguage_de: _('德语', 'German', '德語'),
hermesDisplayConfigLanguage_es: _('西班牙语', 'Spanish', '西班牙語'),
hermesDisplayConfigLanguage_fr: _('法语', 'French', '法語'),
hermesDisplayConfigLanguage_tr: _('土耳其语', 'Turkish', '土耳其語'),
hermesDisplayConfigLanguage_uk: _('乌克兰语', 'Ukrainian', '烏克蘭語'),
hermesDisplayConfigLanguage_af: _('南非荷兰语', 'Afrikaans', '南非荷蘭語'),
hermesDisplayConfigLanguage_ko: _('韩语', 'Korean', '韓語'),
hermesDisplayConfigLanguage_it: _('意大利语', 'Italian', '義大利語'),
hermesDisplayConfigLanguage_ga: _('爱尔兰语', 'Irish', '愛爾蘭語'),
hermesDisplayConfigLanguage_pt: _('葡萄牙语', 'Portuguese', '葡萄牙語'),
hermesDisplayConfigLanguage_ru: _('俄语', 'Russian', '俄語'),
hermesDisplayConfigLanguage_hu: _('匈牙利语', 'Hungarian', '匈牙利語'),
hermesDisplayConfigResumeDisplay: _('恢复会话展示', 'Resume display', '恢復會話顯示'),
hermesDisplayConfigResumeDisplay_full: _('显示完整上下文', 'Show full context', '顯示完整上下文'),
hermesDisplayConfigResumeDisplay_minimal: _('仅显示一行摘要', 'Show one-line summary', '僅顯示一行摘要'),
hermesDisplayConfigFootnote: _('这里写入全局 display 配置平台级覆盖仍在渠道页管理。display.streaming 是 CLI-only本面板不会把它误写成 Gateway 全局流式设置。运行信息字段支持 model、context_pct、cwd、duration、tokens、cost。', 'This writes global display settings; per-platform overrides remain in channel settings. display.streaming is CLI-only, so this panel does not write it as a global Gateway streaming setting. Runtime footer fields support model, context_pct, cwd, duration, tokens, and cost.', '這裡寫入全域 display 設定平台級覆蓋仍在頻道頁管理。display.streaming 是 CLI-only本面板不會把它誤寫成 Gateway 全域串流設定。執行資訊欄位支援 model、context_pct、cwd、duration、tokens、cost。'),
hermesHumanDelayConfigTitle: _('响应节奏', 'Response pacing', '回應節奏'),
hermesHumanDelayConfigDesc: _('控制消息平台回复分块之间的等待时间,降低刷屏或模拟更自然发送节奏。', 'Control the wait time between reply chunks on messaging platforms to reduce flooding or mimic a more natural sending rhythm.', '控制訊息平台回覆分塊之間的等待時間,降低刷屏或模擬更自然的傳送節奏。'),
hermesHumanDelayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),