feat(hermes): add human delay settings

This commit is contained in:
晴天
2026-05-24 22:31:52 +08:00
parent b2146b54cb
commit 8c963cd3d4
8 changed files with 452 additions and 1 deletions

View File

@@ -63,6 +63,12 @@ const SECURITY_DEFAULTS = {
tirithFailOpen: true,
}
const HUMAN_DELAY_DEFAULTS = {
humanDelayMode: 'off',
humanDelayMinMs: 800,
humanDelayMaxMs: 2500,
}
const STREAMING_DEFAULTS = {
enabled: false,
transport: 'edit',
@@ -103,6 +109,7 @@ 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 HUMAN_DELAY_MODES = ['off', 'natural', 'custom']
export function render() {
const el = document.createElement('div')
@@ -117,6 +124,7 @@ export function render() {
let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS }
let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS }
let securityValues = { ...SECURITY_DEFAULTS }
let humanDelayValues = { ...HUMAN_DELAY_DEFAULTS }
let streamingValues = { ...STREAMING_DEFAULTS }
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
let terminalValues = { ...TERMINAL_DEFAULTS }
@@ -129,6 +137,7 @@ export function render() {
let quickCommandsLoading = true
let unauthorizedDmLoading = true
let securityLoading = true
let humanDelayLoading = true
let streamingLoading = true
let executionLimitsLoading = true
let terminalLoading = true
@@ -141,6 +150,7 @@ export function render() {
let quickCommandsSaving = false
let unauthorizedDmSaving = false
let securitySaving = false
let humanDelaySaving = false
let streamingSaving = false
let executionLimitsSaving = false
let terminalSaving = false
@@ -153,6 +163,7 @@ export function render() {
let quickCommandsError = null
let unauthorizedDmError = null
let securityError = null
let humanDelayError = null
let streamingError = null
let executionLimitsError = null
let terminalError = null
@@ -166,7 +177,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
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
}
function option(labelKey, value, selected) {
@@ -522,6 +533,44 @@ export function render() {
`
}
function renderHumanDelayConfigPanel() {
const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-human-delay-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesHumanDelayConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesHumanDelayConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${humanDelaySaving ? t('engine.hermesConfigStatusSaving') : humanDelayLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesHumanDelayConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-human-delay-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesHumanDelayConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(humanDelayError)}
<div class="hm-config-runtime-grid hm-config-human-delay-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesHumanDelayConfigMode')}</span>
<select id="hm-human-delay-mode" class="hm-input" ${disabled ? 'disabled' : ''}>
${HUMAN_DELAY_MODES.map(mode => option(`engine.hermesHumanDelayConfigMode_${mode}`, mode, humanDelayValues.humanDelayMode)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesHumanDelayConfigMinMs')}</span>
<input id="hm-human-delay-min-ms" class="hm-input" type="number" inputmode="numeric" min="0" max="60000" step="100" value="${esc(humanDelayValues.humanDelayMinMs)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesHumanDelayConfigMaxMs')}</span>
<input id="hm-human-delay-max-ms" class="hm-input" type="number" inputmode="numeric" min="0" max="60000" step="100" value="${esc(humanDelayValues.humanDelayMaxMs)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesHumanDelayConfigFootnote')}</div>
</div>
</div>
`
}
function renderStreamingPanel() {
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving
return `
@@ -742,6 +791,7 @@ export function render() {
${renderQuickCommandsConfigPanel()}
${renderUnauthorizedDmConfigPanel()}
${renderSecurityConfigPanel()}
${renderHumanDelayConfigPanel()}
<div class="hm-panel">
<div class="hm-panel-header">
@@ -769,6 +819,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-human-delay-save')?.addEventListener('click', saveHumanDelayConfig)
el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming)
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal)
@@ -819,6 +870,11 @@ export function render() {
securityValues = { ...SECURITY_DEFAULTS, ...(data?.values || {}) }
}
async function loadHumanDelayConfig() {
const data = await api.hermesHumanDelayConfigRead()
humanDelayValues = { ...HUMAN_DELAY_DEFAULTS, ...(data?.values || {}) }
}
async function loadStreaming() {
const data = await api.hermesStreamingConfigRead()
streamingValues = { ...STREAMING_DEFAULTS, ...(data?.values || {}) }
@@ -844,6 +900,7 @@ export function render() {
quickCommandsLoading = true
unauthorizedDmLoading = true
securityLoading = true
humanDelayLoading = true
streamingLoading = true
executionLimitsLoading = true
terminalLoading = true
@@ -856,6 +913,7 @@ export function render() {
quickCommandsError = null
unauthorizedDmError = null
securityError = null
humanDelayError = null
streamingError = null
executionLimitsError = null
terminalError = null
@@ -955,6 +1013,14 @@ export function render() {
securityLoading = false
draw()
}
try {
await loadHumanDelayConfig()
} catch (err) {
humanDelayError = humanizeError(err, t('engine.hermesHumanDelayConfigLoadFailed') || 'Load human delay config failed')
} finally {
humanDelayLoading = false
draw()
}
}
async function refreshRawAfterStructuredSave() {
@@ -1000,6 +1066,9 @@ export function render() {
try {
await loadSecurityConfig()
} catch {}
try {
await loadHumanDelayConfig()
} catch {}
try {
await loadStreaming()
} catch {}
@@ -1243,6 +1312,33 @@ export function render() {
}
}
async function saveHumanDelayConfig() {
const form = {
humanDelayMode: el.querySelector('#hm-human-delay-mode')?.value || 'off',
humanDelayMinMs: el.querySelector('#hm-human-delay-min-ms')?.value || '800',
humanDelayMaxMs: el.querySelector('#hm-human-delay-max-ms')?.value || '2500',
}
humanDelaySaving = true
humanDelayError = null
draw()
try {
const result = await api.hermesHumanDelayConfigSave(form)
humanDelayValues = { ...HUMAN_DELAY_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesHumanDelayConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
humanDelayError = humanizeError(err, t('engine.hermesHumanDelayConfigSaveFailed') || 'Save human delay config failed')
toast(humanDelayError, 'error')
} finally {
humanDelaySaving = false
draw()
}
}
async function saveStreaming() {
const form = {
enabled: !!el.querySelector('#hm-streaming-enabled')?.checked,

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 }),
hermesHumanDelayConfigRead: () => invoke('hermes_human_delay_config_read'),
hermesHumanDelayConfigSave: (form) => invoke('hermes_human_delay_config_save', { form }),
hermesStreamingConfigRead: () => invoke('hermes_streaming_config_read'),
hermesStreamingConfigSave: (form) => invoke('hermes_streaming_config_save', { form }),
hermesExecutionLimitsConfigRead: () => invoke('hermes_execution_limits_config_read'),

View File

@@ -640,6 +640,20 @@ 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 中單獨設定。'),
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', '結構化設定'),
hermesHumanDelayConfigSave: _('保存响应节奏', 'Save pacing', '儲存回應節奏'),
hermesHumanDelayConfigSaveSuccess: _('响应节奏已保存,建议重启 Hermes Gateway 生效', 'Response pacing saved. Restart Hermes Gateway to take effect.', '回應節奏已儲存,建議重啟 Hermes Gateway 生效'),
hermesHumanDelayConfigLoadFailed: _('加载响应节奏失败', 'Load response pacing failed', '載入回應節奏失敗'),
hermesHumanDelayConfigSaveFailed: _('保存响应节奏失败', 'Save response pacing failed', '儲存回應節奏失敗'),
hermesHumanDelayConfigMode: _('节奏模式', 'Pacing mode', '節奏模式'),
hermesHumanDelayConfigMode_off: _('关闭', 'Off', '關閉'),
hermesHumanDelayConfigMode_natural: _('自然节奏', 'Natural pacing', '自然節奏'),
hermesHumanDelayConfigMode_custom: _('自定义范围', 'Custom range', '自訂範圍'),
hermesHumanDelayConfigMinMs: _('最小延迟 ms', 'Minimum delay ms', '最小延遲 ms'),
hermesHumanDelayConfigMaxMs: _('最大延迟 ms', 'Maximum delay ms', '最大延遲 ms'),
hermesHumanDelayConfigFootnote: _('natural 使用 800-2500mscustom 使用下方范围。Signal 等平台可能忽略或仅部分支持该设置。', 'natural uses 800-2500ms; custom uses the range below. Platforms such as Signal may ignore or only partially support this setting.', 'natural 使用 800-2500mscustom 使用下方範圍。Signal 等平台可能忽略或僅部分支援此設定。'),
hermesSecurityConfigTitle: _('Tirith 安全扫描', 'Tirith security scanning', 'Tirith 安全掃描'),
hermesSecurityConfigDesc: _('控制终端命令执行前的 Tirith 内容扫描,拦截明显的 URL 伪装、管道执行和注入风险。', 'Control Tirith content scanning before terminal commands run to catch obvious URL spoofing, pipe-to-shell, and injection risks.', '控制終端命令執行前的 Tirith 內容掃描,攔截明顯的 URL 偽裝、管道執行和注入風險。'),
hermesSecurityConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),