feat(hermes): add web tool backend controls

This commit is contained in:
晴天
2026-05-27 04:33:03 +08:00
parent 0b4a70e7a4
commit 3bb3c9a7c3
8 changed files with 525 additions and 3 deletions

View File

@@ -294,6 +294,12 @@ const BROWSER_DEFAULTS = {
browserDialogTimeout: 300,
}
const WEB_DEFAULTS = {
webBackend: '',
webSearchBackend: '',
webExtractBackend: '',
}
const STT_DEFAULTS = {
sttEnabled: true,
sttProvider: 'auto',
@@ -361,6 +367,7 @@ const TERMINAL_MODAL_MODES = ['auto', 'managed', 'direct']
const TERMINAL_VERCEL_RUNTIMES = ['node24', 'node22', 'python3.13']
const BROWSER_ENGINES = ['auto', 'lightpanda', 'chrome']
const BROWSER_DIALOG_POLICIES = ['must_respond', 'auto_dismiss', 'auto_accept']
const WEB_BACKENDS = ['', 'tavily', 'firecrawl', 'parallel', 'exa', 'searxng', 'brave', 'brave_free', 'ddgs', 'xai', 'native']
const STT_PROVIDERS = ['auto', 'local', 'groq', 'openai', 'mistral']
const STT_LOCAL_MODELS = ['tiny', 'base', 'small', 'medium', 'large-v3', 'turbo']
const STT_OPENAI_MODELS = ['whisper-1', 'gpt-4o-mini-transcribe', 'gpt-4o-transcribe']
@@ -426,6 +433,7 @@ export function render() {
let approvalsValues = { ...APPROVALS_DEFAULTS }
let privacyValues = { ...PRIVACY_DEFAULTS }
let browserValues = { ...BROWSER_DEFAULTS }
let webValues = { ...WEB_DEFAULTS }
let sttValues = { ...STT_DEFAULTS }
let ttsVoiceValues = { ...TTS_VOICE_DEFAULTS }
let terminalValues = { ...TERMINAL_DEFAULTS }
@@ -465,6 +473,7 @@ export function render() {
let approvalsLoading = true
let privacyLoading = true
let browserLoading = true
let webLoading = true
let sttLoading = true
let ttsVoiceLoading = true
let terminalLoading = true
@@ -504,6 +513,7 @@ export function render() {
let approvalsSaving = false
let privacySaving = false
let browserSaving = false
let webSaving = false
let sttSaving = false
let ttsVoiceSaving = false
let terminalSaving = false
@@ -543,6 +553,7 @@ export function render() {
let approvalsError = null
let privacyError = null
let browserError = null
let webError = null
let sttError = null
let ttsVoiceError = null
let terminalError = null
@@ -556,7 +567,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || sessionsMaintenanceLoading || updatesLoading || compressionLoading || promptCachingLoading || openrouterCacheLoading || providerRoutingLoading || auxiliaryLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || curatorLoading || quickCommandsLoading || modelLoading || modelAliasesLoading || hooksLoading || providerOverridesLoading || mcpServersLoading || agentToolsetsLoading || platformToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || kanbanLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || sttLoading || ttsVoiceLoading || terminalLoading || saving || runtimeSaving || sessionsMaintenanceSaving || updatesSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || curatorSaving || quickCommandsSaving || modelSaving || modelAliasesSaving || hooksSaving || providerOverridesSaving || mcpServersSaving || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || kanbanSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || sttSaving || ttsVoiceSaving || terminalSaving
return loading || runtimeLoading || sessionsMaintenanceLoading || updatesLoading || compressionLoading || promptCachingLoading || openrouterCacheLoading || providerRoutingLoading || auxiliaryLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || curatorLoading || quickCommandsLoading || modelLoading || modelAliasesLoading || hooksLoading || providerOverridesLoading || mcpServersLoading || agentToolsetsLoading || platformToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || kanbanLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || cronLoading || loggingLoading || approvalsLoading || privacyLoading || browserLoading || webLoading || sttLoading || ttsVoiceLoading || terminalLoading || saving || runtimeSaving || sessionsMaintenanceSaving || updatesSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || curatorSaving || quickCommandsSaving || modelSaving || modelAliasesSaving || hooksSaving || providerOverridesSaving || mcpServersSaving || agentToolsetsSaving || platformToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || kanbanSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || cronSaving || loggingSaving || approvalsSaving || privacySaving || browserSaving || webSaving || sttSaving || ttsVoiceSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -2180,7 +2191,7 @@ export function render() {
}
function renderBrowserPanel() {
const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || sttSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
const disabled = loading || saving || browserLoading || browserSaving || webSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || sttSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-browser-panel">
<div class="hm-panel-header">
@@ -2245,8 +2256,50 @@ export function render() {
`
}
function renderWebConfigPanel() {
const disabled = loading || saving || webLoading || webSaving || browserSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || sttSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-web-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesWebConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesWebConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${webSaving ? t('engine.hermesConfigStatusSaving') : webLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesWebConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-web-config-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesWebConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(webError)}
<div class="hm-config-runtime-grid hm-config-web-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesWebConfigBackend')}</span>
<select id="hm-web-backend" class="hm-input" ${disabled ? 'disabled' : ''}>
${WEB_BACKENDS.map(backend => option(`engine.hermesWebConfigBackend_${backend || 'auto'}`, backend, webValues.webBackend)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesWebConfigSearchBackend')}</span>
<select id="hm-web-search-backend" class="hm-input" ${disabled ? 'disabled' : ''}>
${WEB_BACKENDS.map(backend => option(`engine.hermesWebConfigBackend_${backend || 'auto'}`, backend, webValues.webSearchBackend)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesWebConfigExtractBackend')}</span>
<select id="hm-web-extract-backend" class="hm-input" ${disabled ? 'disabled' : ''}>
${WEB_BACKENDS.map(backend => option(`engine.hermesWebConfigBackend_${backend || 'auto'}`, backend, webValues.webExtractBackend)).join('')}
</select>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesWebConfigFootnote')}</div>
</div>
</div>
`
}
function renderSttPanel() {
const disabled = loading || saving || sttLoading || sttSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
const disabled = loading || saving || sttLoading || sttSaving || webSaving || approvalsSaving || cronSaving || loggingSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || promptCachingSaving || openrouterCacheSaving || providerRoutingSaving || auxiliarySaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || providerOverridesSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-stt-panel">
<div class="hm-panel-header">
@@ -2574,6 +2627,7 @@ export function render() {
${renderApprovalsPanel()}
${renderPrivacyPanel()}
${renderBrowserPanel()}
${renderWebConfigPanel()}
${renderSttPanel()}
${renderTtsVoicePanel()}
${renderCompressionPanel()}
@@ -2653,6 +2707,7 @@ export function render() {
el.querySelector('#hm-approvals-save')?.addEventListener('click', saveApprovalsConfig)
el.querySelector('#hm-privacy-save')?.addEventListener('click', savePrivacyConfig)
el.querySelector('#hm-browser-save')?.addEventListener('click', saveBrowserConfig)
el.querySelector('#hm-web-config-save')?.addEventListener('click', saveWebConfig)
el.querySelector('#hm-stt-save')?.addEventListener('click', saveSttConfig)
el.querySelector('#hm-tts-voice-save')?.addEventListener('click', saveTtsVoiceConfig)
el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal)
@@ -2838,6 +2893,11 @@ export function render() {
browserValues = { ...BROWSER_DEFAULTS, ...(data?.values || {}) }
}
async function loadWebConfig() {
const data = await api.hermesWebConfigRead()
webValues = { ...WEB_DEFAULTS, ...(data?.values || {}) }
}
async function loadSttConfig() {
const data = await api.hermesSttConfigRead()
sttValues = { ...STT_DEFAULTS, ...(data?.values || {}) }
@@ -2890,6 +2950,7 @@ export function render() {
approvalsLoading = true
privacyLoading = true
browserLoading = true
webLoading = true
sttLoading = true
ttsVoiceLoading = true
terminalLoading = true
@@ -2928,6 +2989,7 @@ export function render() {
approvalsError = null
privacyError = null
browserError = null
webError = null
sttError = null
ttsVoiceError = null
terminalError = null
@@ -3083,6 +3145,14 @@ export function render() {
browserLoading = false
draw()
}
try {
await loadWebConfig()
} catch (err) {
webError = humanizeError(err, t('engine.hermesWebConfigLoadFailed') || 'Load web tool config failed')
} finally {
webLoading = false
draw()
}
try {
await loadSttConfig()
} catch (err) {
@@ -4401,6 +4471,33 @@ export function render() {
}
}
async function saveWebConfig() {
const form = {
webBackend: el.querySelector('#hm-web-backend')?.value || '',
webSearchBackend: el.querySelector('#hm-web-search-backend')?.value || '',
webExtractBackend: el.querySelector('#hm-web-extract-backend')?.value || '',
}
webSaving = true
webError = null
draw()
try {
const result = await api.hermesWebConfigSave(form)
webValues = { ...WEB_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesWebConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
webError = humanizeError(err, t('engine.hermesWebConfigSaveFailed') || 'Save web tool config failed')
toast(webError, 'error')
} finally {
webSaving = false
draw()
}
}
async function saveSttConfig() {
const form = {
sttEnabled: !!el.querySelector('#hm-stt-enabled')?.checked,

View File

@@ -579,6 +579,8 @@ export const api = {
hermesPrivacyConfigSave: (form) => invoke('hermes_privacy_config_save', { form }),
hermesBrowserConfigRead: () => invoke('hermes_browser_config_read'),
hermesBrowserConfigSave: (form) => invoke('hermes_browser_config_save', { form }),
hermesWebConfigRead: () => invoke('hermes_web_config_read'),
hermesWebConfigSave: (form) => invoke('hermes_web_config_save', { form }),
hermesSttConfigRead: () => invoke('hermes_stt_config_read'),
hermesSttConfigSave: (form) => invoke('hermes_stt_config_save', { form }),
hermesTtsVoiceConfigRead: () => invoke('hermes_tts_voice_config_read'),

View File

@@ -717,6 +717,28 @@ export default {
hermesBrowserConfigDialogPolicy_auto_accept: _('自动确认弹窗', 'Auto accept dialogs', '自動確認彈窗'),
hermesBrowserConfigDialogTimeout: _('弹窗等待超时秒数', 'Dialog wait timeout seconds', '彈窗等待逾時秒數'),
hermesBrowserConfigFootnote: _('Lightpanda 导航更快但不支持截图;录制会把 WebM 写入 Hermes browser_recordings 目录,请只在需要审计时开启。允许私网地址会放开 localhost / 192.168 等目标CDP 地址留空表示自动创建浏览器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. Allowing private URLs opens localhost / 192.168-style targets. Empty CDP URL means Hermes launches a browser automatically. Nested Camofox advanced fields stay in raw YAML.', 'Lightpanda 導覽更快但不支援截圖;錄製會把 WebM 寫入 Hermes browser_recordings 目錄,請只在需要稽核時開啟。允許私網位址會放開 localhost / 192.168 等目標CDP 位址留空表示自動建立瀏覽器Camofox 巢狀進階欄位會保留在 raw YAML 中。'),
hermesWebConfigTitle: _('Web 工具后端', 'Web tool backends', 'Web 工具後端'),
hermesWebConfigDesc: _('控制 web_search / web_extract 的默认和分能力后端,只保存后端选择,不保存 API Key。', 'Control default and per-capability backends for web_search / web_extract. This stores backend choices only, not API keys.', '控制 web_search / web_extract 的預設和分能力後端,只儲存後端選擇,不儲存 API Key。'),
hermesWebConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesWebConfigSave: _('保存 Web 工具配置', 'Save web tool settings', '儲存 Web 工具設定'),
hermesWebConfigSaveSuccess: _('Web 工具后端已保存,建议重启 Hermes Gateway 生效', 'Web tool backend settings saved. Restart Hermes Gateway to take effect.', 'Web 工具後端已儲存,建議重啟 Hermes Gateway 生效'),
hermesWebConfigLoadFailed: _('加载 Web 工具配置失败', 'Load web tool settings failed', '載入 Web 工具設定失敗'),
hermesWebConfigSaveFailed: _('保存 Web 工具配置失败', 'Save web tool settings failed', '儲存 Web 工具設定失敗'),
hermesWebConfigBackend: _('默认后端', 'Default backend', '預設後端'),
hermesWebConfigSearchBackend: _('搜索后端', 'Search backend', '搜尋後端'),
hermesWebConfigExtractBackend: _('提取后端', 'Extract backend', '提取後端'),
hermesWebConfigBackend_auto: _('自动选择', 'Auto select', '自動選擇'),
hermesWebConfigBackend_tavily: _('Tavily', 'Tavily', 'Tavily'),
hermesWebConfigBackend_firecrawl: _('Firecrawl', 'Firecrawl', 'Firecrawl'),
hermesWebConfigBackend_parallel: _('Parallel', 'Parallel', 'Parallel'),
hermesWebConfigBackend_exa: _('Exa', 'Exa', 'Exa'),
hermesWebConfigBackend_searxng: _('SearXNG', 'SearXNG', 'SearXNG'),
hermesWebConfigBackend_brave: _('Brave Search', 'Brave Search', 'Brave Search'),
hermesWebConfigBackend_brave_free: _('Brave Free', 'Brave Free', 'Brave Free'),
hermesWebConfigBackend_ddgs: _('DuckDuckGo', 'DuckDuckGo', 'DuckDuckGo'),
hermesWebConfigBackend_xai: _('xAI Search', 'xAI Search', 'xAI Search'),
hermesWebConfigBackend_native: _('原生提取', 'Native extraction', '原生提取'),
hermesWebConfigFootnote: _('这里写入 web.backend、web.search_backend 和 web.extract_backend。留空会删除覆盖让 Hermes 按密钥、可用 provider 和上游默认策略自动选择;未知 web 字段会保留在 raw YAML 中。', 'This writes web.backend, web.search_backend, and web.extract_backend. Leaving a value empty removes the override so Hermes can auto-select based on keys, available providers, and upstream defaults. Unknown web fields stay in raw YAML.', '這裡寫入 web.backend、web.search_backend 和 web.extract_backend。留空會刪除覆蓋讓 Hermes 按密鑰、可用 provider 和上游預設策略自動選擇;未知 web 欄位會保留在 raw YAML 中。'),
hermesSttConfigTitle: _('语音转写', 'Speech transcription', '語音轉寫'),
hermesSttConfigDesc: _('控制消息平台语音消息是否自动转写以及本地、OpenAI 和 Mistral 转写模型。适合需要处理语音反馈的渠道。', 'Control automatic voice-message transcription for messaging platforms, plus local, OpenAI, and Mistral transcription models. Useful for channels that receive voice feedback.', '控制訊息平台語音訊息是否自動轉寫以及本機、OpenAI 和 Mistral 轉寫模型。適合需要處理語音回饋的渠道。'),
hermesSttConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),