feat(hermes): add browser tool settings

This commit is contained in:
晴天
2026-05-25 00:27:06 +08:00
parent 26d6ad18bc
commit 8d5d21f908
8 changed files with 500 additions and 3 deletions

View File

@@ -113,6 +113,13 @@ const PRIVACY_DEFAULTS = {
redactPii: false,
}
const BROWSER_DEFAULTS = {
browserInactivityTimeout: 120,
browserCommandTimeout: 30,
browserRecordSessions: false,
browserEngine: 'auto',
}
const TERMINAL_DEFAULTS = {
terminalBackend: 'local',
terminalCwd: '.',
@@ -130,6 +137,7 @@ const SESSION_RESET_MODES = ['both', 'idle', 'daily', 'none']
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 BROWSER_ENGINES = ['auto', 'lightpanda', 'chrome']
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']
@@ -155,6 +163,7 @@ export function render() {
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
let ioSafetyValues = { ...IO_SAFETY_DEFAULTS }
let privacyValues = { ...PRIVACY_DEFAULTS }
let browserValues = { ...BROWSER_DEFAULTS }
let terminalValues = { ...TERMINAL_DEFAULTS }
let loading = true
let runtimeLoading = true
@@ -171,6 +180,7 @@ export function render() {
let executionLimitsLoading = true
let ioSafetyLoading = true
let privacyLoading = true
let browserLoading = true
let terminalLoading = true
let saving = false
let runtimeSaving = false
@@ -187,6 +197,7 @@ export function render() {
let executionLimitsSaving = false
let ioSafetySaving = false
let privacySaving = false
let browserSaving = false
let terminalSaving = false
let error = null
let runtimeError = null
@@ -203,6 +214,7 @@ export function render() {
let executionLimitsError = null
let ioSafetyError = null
let privacyError = null
let browserError = null
let terminalError = null
function esc(value) {
@@ -214,7 +226,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || terminalSaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -837,7 +849,7 @@ export function render() {
}
function renderPrivacyPanel() {
const disabled = loading || saving || privacyLoading || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-privacy-panel">
<div class="hm-panel-header">
@@ -864,8 +876,52 @@ export function render() {
`
}
function renderBrowserPanel() {
const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-browser-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesBrowserConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesBrowserConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${browserSaving ? t('engine.hermesConfigStatusSaving') : browserLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesBrowserConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-browser-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesBrowserConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(browserError)}
<div class="hm-config-runtime-grid hm-config-browser-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesBrowserConfigEngine')}</span>
<select id="hm-browser-engine" class="hm-input" ${disabled ? 'disabled' : ''}>
${BROWSER_ENGINES.map(mode => option(`engine.hermesBrowserConfigEngine_${mode}`, mode, browserValues.browserEngine)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesBrowserConfigInactivityTimeout')}</span>
<input id="hm-browser-inactivity-timeout" class="hm-input" type="number" inputmode="numeric" min="1" max="86400" step="1" value="${esc(browserValues.browserInactivityTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesBrowserConfigCommandTimeout')}</span>
<input id="hm-browser-command-timeout" class="hm-input" type="number" inputmode="numeric" min="5" max="3600" step="1" value="${esc(browserValues.browserCommandTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-browser-record-sessions" type="checkbox" ${browserValues.browserRecordSessions ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesBrowserConfigRecordSessions')}</span>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesBrowserConfigFootnote')}</div>
</div>
</div>
`
}
function renderTerminalPanel() {
const disabled = loading || saving || terminalLoading || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-terminal-panel">
<div class="hm-panel-header">
@@ -955,6 +1011,7 @@ export function render() {
${renderExecutionLimitsPanel()}
${renderIoSafetyPanel()}
${renderPrivacyPanel()}
${renderBrowserPanel()}
${renderCompressionPanel()}
${renderToolGuardrailsPanel()}
${renderMemoryPanel()}
@@ -997,6 +1054,7 @@ export function render() {
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety)
el.querySelector('#hm-privacy-save')?.addEventListener('click', savePrivacyConfig)
el.querySelector('#hm-browser-save')?.addEventListener('click', saveBrowserConfig)
el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal)
}
@@ -1075,6 +1133,11 @@ export function render() {
privacyValues = { ...PRIVACY_DEFAULTS, ...(data?.values || {}) }
}
async function loadBrowserConfig() {
const data = await api.hermesBrowserConfigRead()
browserValues = { ...BROWSER_DEFAULTS, ...(data?.values || {}) }
}
async function loadTerminal() {
const data = await api.hermesTerminalConfigRead()
terminalValues = { ...TERMINAL_DEFAULTS, ...(data?.values || {}) }
@@ -1096,6 +1159,7 @@ export function render() {
executionLimitsLoading = true
ioSafetyLoading = true
privacyLoading = true
browserLoading = true
terminalLoading = true
error = null
runtimeError = null
@@ -1112,6 +1176,7 @@ export function render() {
executionLimitsError = null
ioSafetyError = null
privacyError = null
browserError = null
terminalError = null
draw()
try {
@@ -1177,6 +1242,14 @@ export function render() {
privacyLoading = false
draw()
}
try {
await loadBrowserConfig()
} catch (err) {
browserError = humanizeError(err, t('engine.hermesBrowserConfigLoadFailed') || 'Load browser config failed')
} finally {
browserLoading = false
draw()
}
try {
await loadTerminal()
} catch (err) {
@@ -1304,6 +1377,9 @@ export function render() {
try {
await loadPrivacyConfig()
} catch {}
try {
await loadBrowserConfig()
} catch {}
try {
await loadTerminal()
} catch {}
@@ -1717,6 +1793,34 @@ export function render() {
}
}
async function saveBrowserConfig() {
const form = {
browserInactivityTimeout: el.querySelector('#hm-browser-inactivity-timeout')?.value || '120',
browserCommandTimeout: el.querySelector('#hm-browser-command-timeout')?.value || '30',
browserRecordSessions: !!el.querySelector('#hm-browser-record-sessions')?.checked,
browserEngine: el.querySelector('#hm-browser-engine')?.value || 'auto',
}
browserSaving = true
browserError = null
draw()
try {
const result = await api.hermesBrowserConfigSave(form)
browserValues = { ...BROWSER_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesBrowserConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
browserError = humanizeError(err, t('engine.hermesBrowserConfigSaveFailed') || 'Save browser config failed')
toast(browserError, 'error')
} finally {
browserSaving = false
draw()
}
}
async function saveTerminal() {
const form = {
terminalBackend: el.querySelector('#hm-terminal-backend')?.value || 'local',

View File

@@ -537,6 +537,8 @@ export const api = {
hermesIoSafetyConfigSave: (form) => invoke('hermes_io_safety_config_save', { form }),
hermesPrivacyConfigRead: () => invoke('hermes_privacy_config_read'),
hermesPrivacyConfigSave: (form) => invoke('hermes_privacy_config_save', { form }),
hermesBrowserConfigRead: () => invoke('hermes_browser_config_read'),
hermesBrowserConfigSave: (form) => invoke('hermes_browser_config_save', { form }),
hermesTerminalConfigRead: () => invoke('hermes_terminal_config_read'),
hermesTerminalConfigSave: (form) => invoke('hermes_terminal_config_save', { form }),
hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000),

View File

@@ -585,6 +585,21 @@ export default {
hermesPrivacyConfigSaveFailed: _('保存隐私脱敏配置失败', 'Save privacy redaction failed', '儲存隱私脫敏設定失敗'),
hermesPrivacyConfigRedactPii: _('送入模型前脱敏用户标识和手机号', 'Redact user identifiers and phone numbers before model context', '送入模型前脫敏使用者識別和電話號碼'),
hermesPrivacyConfigFootnote: _('目前主要作用于 WhatsApp、Signal 和 TelegramDiscord 与 Slack 为保持 mention 语义不会脱敏真实用户 ID。其他 privacy 高级字段会保留在 raw YAML 中。', 'Currently applies mainly to WhatsApp, Signal, and Telegram. Discord and Slack keep real user IDs to preserve mention semantics. Other advanced privacy fields are preserved in raw YAML.', '目前主要作用於 WhatsApp、Signal 和 TelegramDiscord 與 Slack 為保持 mention 語義不會脫敏真實使用者 ID。其他 privacy 進階欄位會保留在 raw YAML 中。'),
hermesBrowserConfigTitle: _('浏览器工具', 'Browser tools', '瀏覽器工具'),
hermesBrowserConfigDesc: _('控制浏览器工具的空闲回收、单次命令超时、会话录制和本地引擎,降低长跑浏览器任务卡死和隐私误录风险。', 'Control browser-tool idle cleanup, per-command timeout, session recording, and local engine to reduce stuck long-run browsing tasks and accidental recording risk.', '控制瀏覽器工具的閒置回收、單次命令逾時、工作階段錄製和本機引擎,降低長跑瀏覽器任務卡死和隱私誤錄風險。'),
hermesBrowserConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesBrowserConfigSave: _('保存浏览器配置', 'Save browser settings', '儲存瀏覽器設定'),
hermesBrowserConfigSaveSuccess: _('浏览器工具配置已保存,建议重启 Hermes Gateway 生效', 'Browser tool settings saved. Restart Hermes Gateway to take effect.', '瀏覽器工具設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesBrowserConfigLoadFailed: _('加载浏览器工具配置失败', 'Load browser tool settings failed', '載入瀏覽器工具設定失敗'),
hermesBrowserConfigSaveFailed: _('保存浏览器工具配置失败', 'Save browser tool settings failed', '儲存瀏覽器工具設定失敗'),
hermesBrowserConfigInactivityTimeout: _('空闲回收秒数', 'Idle cleanup seconds', '閒置回收秒數'),
hermesBrowserConfigCommandTimeout: _('单次命令超时秒数', 'Command timeout seconds', '單次命令逾時秒數'),
hermesBrowserConfigRecordSessions: _('录制浏览器会话', 'Record browser sessions', '錄製瀏覽器工作階段'),
hermesBrowserConfigEngine: _('浏览器引擎', 'Browser engine', '瀏覽器引擎'),
hermesBrowserConfigEngine_auto: _('自动选择', 'Auto select', '自動選擇'),
hermesBrowserConfigEngine_lightpanda: _('Lightpanda 快速导航', 'Lightpanda fast navigation', 'Lightpanda 快速導覽'),
hermesBrowserConfigEngine_chrome: _('Chrome 完整浏览器', 'Chrome full browser', 'Chrome 完整瀏覽器'),
hermesBrowserConfigFootnote: _('Lightpanda 导航更快但不支持截图;录制会把 WebM 写入 Hermes browser_recordings 目录请只在需要审计时开启。CDP、Dialog 和 Camofox 高级字段会保留在 raw YAML 中。', 'Lightpanda navigates faster but does not support screenshots. Recording writes WebM files into the Hermes browser_recordings directory, so enable it only for audits. Advanced CDP, Dialog, and Camofox fields stay in raw YAML.', 'Lightpanda 導覽更快但不支援截圖;錄製會把 WebM 寫入 Hermes browser_recordings 目錄請只在需要稽核時開啟。CDP、Dialog 和 Camofox 進階欄位會保留在 raw YAML 中。'),
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', '結構化設定'),