feat(hermes): add lsp diagnostic controls

This commit is contained in:
晴天
2026-05-27 04:49:03 +08:00
parent 3bb3c9a7c3
commit 18c0164bdb
8 changed files with 536 additions and 3 deletions

View File

@@ -300,6 +300,13 @@ const WEB_DEFAULTS = {
webExtractBackend: '',
}
const LSP_DEFAULTS = {
lspEnabled: true,
lspWaitMode: 'document',
lspWaitTimeout: 5,
lspInstallStrategy: 'auto',
}
const STT_DEFAULTS = {
sttEnabled: true,
sttProvider: 'auto',
@@ -368,6 +375,8 @@ 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 LSP_WAIT_MODES = ['document', 'full']
const LSP_INSTALL_STRATEGIES = ['auto', 'manual', 'off']
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']
@@ -434,6 +443,7 @@ export function render() {
let privacyValues = { ...PRIVACY_DEFAULTS }
let browserValues = { ...BROWSER_DEFAULTS }
let webValues = { ...WEB_DEFAULTS }
let lspValues = { ...LSP_DEFAULTS }
let sttValues = { ...STT_DEFAULTS }
let ttsVoiceValues = { ...TTS_VOICE_DEFAULTS }
let terminalValues = { ...TERMINAL_DEFAULTS }
@@ -474,6 +484,7 @@ export function render() {
let privacyLoading = true
let browserLoading = true
let webLoading = true
let lspLoading = true
let sttLoading = true
let ttsVoiceLoading = true
let terminalLoading = true
@@ -514,6 +525,7 @@ export function render() {
let privacySaving = false
let browserSaving = false
let webSaving = false
let lspSaving = false
let sttSaving = false
let ttsVoiceSaving = false
let terminalSaving = false
@@ -554,6 +566,7 @@ export function render() {
let privacyError = null
let browserError = null
let webError = null
let lspError = null
let sttError = null
let ttsVoiceError = null
let terminalError = null
@@ -567,7 +580,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 || 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
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 || lspLoading || 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 || lspSaving || sttSaving || ttsVoiceSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -2257,7 +2270,7 @@ 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
const disabled = loading || saving || webLoading || webSaving || browserSaving || lspSaving || 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">
@@ -2298,8 +2311,54 @@ export function render() {
`
}
function renderLspConfigPanel() {
const disabled = loading || saving || lspLoading || lspSaving || 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-lsp-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesLspConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesLspConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${lspSaving ? t('engine.hermesConfigStatusSaving') : lspLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesLspConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-lsp-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesLspConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(lspError)}
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-lsp-enabled" type="checkbox" ${lspValues.lspEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesLspConfigEnabled')}</span>
</label>
</div>
<div class="hm-config-runtime-grid hm-config-lsp-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesLspConfigWaitMode')}</span>
<select id="hm-lsp-wait-mode" class="hm-input" ${disabled ? 'disabled' : ''}>
${LSP_WAIT_MODES.map(mode => option(`engine.hermesLspConfigWaitMode_${mode}`, mode, lspValues.lspWaitMode)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesLspConfigWaitTimeout')}</span>
<input id="hm-lsp-wait-timeout" class="hm-input" type="number" inputmode="decimal" min="0.1" max="120" step="0.1" value="${esc(lspValues.lspWaitTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesLspConfigInstallStrategy')}</span>
<select id="hm-lsp-install-strategy" class="hm-input" ${disabled ? 'disabled' : ''}>
${LSP_INSTALL_STRATEGIES.map(strategy => option(`engine.hermesLspConfigInstallStrategy_${strategy}`, strategy, lspValues.lspInstallStrategy)).join('')}
</select>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesLspConfigFootnote')}</div>
</div>
</div>
`
}
function renderSttPanel() {
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
const disabled = loading || saving || sttLoading || sttSaving || webSaving || lspSaving || 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">
@@ -2628,6 +2687,7 @@ export function render() {
${renderPrivacyPanel()}
${renderBrowserPanel()}
${renderWebConfigPanel()}
${renderLspConfigPanel()}
${renderSttPanel()}
${renderTtsVoicePanel()}
${renderCompressionPanel()}
@@ -2708,6 +2768,7 @@ export function render() {
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-lsp-save')?.addEventListener('click', saveLspConfig)
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)
@@ -2898,6 +2959,11 @@ export function render() {
webValues = { ...WEB_DEFAULTS, ...(data?.values || {}) }
}
async function loadLspConfig() {
const data = await api.hermesLspConfigRead()
lspValues = { ...LSP_DEFAULTS, ...(data?.values || {}) }
}
async function loadSttConfig() {
const data = await api.hermesSttConfigRead()
sttValues = { ...STT_DEFAULTS, ...(data?.values || {}) }
@@ -2951,6 +3017,7 @@ export function render() {
privacyLoading = true
browserLoading = true
webLoading = true
lspLoading = true
sttLoading = true
ttsVoiceLoading = true
terminalLoading = true
@@ -2990,6 +3057,7 @@ export function render() {
privacyError = null
browserError = null
webError = null
lspError = null
sttError = null
ttsVoiceError = null
terminalError = null
@@ -3153,6 +3221,14 @@ export function render() {
webLoading = false
draw()
}
try {
await loadLspConfig()
} catch (err) {
lspError = humanizeError(err, t('engine.hermesLspConfigLoadFailed') || 'Load LSP config failed')
} finally {
lspLoading = false
draw()
}
try {
await loadSttConfig()
} catch (err) {
@@ -4498,6 +4574,34 @@ export function render() {
}
}
async function saveLspConfig() {
const form = {
lspEnabled: !!el.querySelector('#hm-lsp-enabled')?.checked,
lspWaitMode: el.querySelector('#hm-lsp-wait-mode')?.value || 'document',
lspWaitTimeout: el.querySelector('#hm-lsp-wait-timeout')?.value || '5',
lspInstallStrategy: el.querySelector('#hm-lsp-install-strategy')?.value || 'auto',
}
lspSaving = true
lspError = null
draw()
try {
const result = await api.hermesLspConfigSave(form)
lspValues = { ...LSP_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesLspConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
lspError = humanizeError(err, t('engine.hermesLspConfigSaveFailed') || 'Save LSP config failed')
toast(lspError, 'error')
} finally {
lspSaving = false
draw()
}
}
async function saveSttConfig() {
const form = {
sttEnabled: !!el.querySelector('#hm-stt-enabled')?.checked,

View File

@@ -581,6 +581,8 @@ export const api = {
hermesBrowserConfigSave: (form) => invoke('hermes_browser_config_save', { form }),
hermesWebConfigRead: () => invoke('hermes_web_config_read'),
hermesWebConfigSave: (form) => invoke('hermes_web_config_save', { form }),
hermesLspConfigRead: () => invoke('hermes_lsp_config_read'),
hermesLspConfigSave: (form) => invoke('hermes_lsp_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

@@ -739,6 +739,23 @@ export default {
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 中。'),
hermesLspConfigTitle: _('LSP 语义诊断', 'LSP semantic diagnostics', 'LSP 語意診斷'),
hermesLspConfigDesc: _('控制写文件和补丁后的语言服务器诊断等待、工作区扫描和缺失服务器自动安装策略。', 'Control language-server diagnostic waits, workspace scans, and missing-server install strategy after file writes and patches.', '控制寫檔和補丁後的語言伺服器診斷等待、工作區掃描和缺失伺服器自動安裝策略。'),
hermesLspConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesLspConfigSave: _('保存 LSP 配置', 'Save LSP settings', '儲存 LSP 設定'),
hermesLspConfigSaveSuccess: _('LSP 语义诊断配置已保存,建议重启 Hermes Gateway 生效', 'LSP semantic diagnostic settings saved. Restart Hermes Gateway to take effect.', 'LSP 語意診斷設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesLspConfigLoadFailed: _('加载 LSP 语义诊断配置失败', 'Load LSP semantic diagnostic settings failed', '載入 LSP 語意診斷設定失敗'),
hermesLspConfigSaveFailed: _('保存 LSP 语义诊断配置失败', 'Save LSP semantic diagnostic settings failed', '儲存 LSP 語意診斷設定失敗'),
hermesLspConfigEnabled: _('启用 LSP 语义诊断', 'Enable LSP semantic diagnostics', '啟用 LSP 語意診斷'),
hermesLspConfigWaitMode: _('诊断等待模式', 'Diagnostic wait mode', '診斷等待模式'),
hermesLspConfigWaitMode_document: _('仅当前文件', 'Current file only', '僅目前檔案'),
hermesLspConfigWaitMode_full: _('工作区诊断', 'Workspace diagnostics', '工作區診斷'),
hermesLspConfigWaitTimeout: _('诊断等待秒数', 'Diagnostic wait seconds', '診斷等待秒數'),
hermesLspConfigInstallStrategy: _('缺失服务器处理', 'Missing server handling', '缺失伺服器處理'),
hermesLspConfigInstallStrategy_auto: _('自动安装', 'Auto install', '自動安裝'),
hermesLspConfigInstallStrategy_manual: _('仅使用 PATH', 'Use PATH only', '僅使用 PATH'),
hermesLspConfigInstallStrategy_off: _('关闭自动安装', 'Disable auto install', '關閉自動安裝'),
hermesLspConfigFootnote: _('这里写入 lsp.enabled、lsp.wait_mode、lsp.wait_timeout 和 lsp.install_strategy。高级 lsp.servers 覆盖保留在 raw YAML远程或沙箱后端是否触发宿主 LSP 以 Hermes 运行时工作目录和 Git workspace 检测为准。', 'This writes lsp.enabled, lsp.wait_mode, lsp.wait_timeout, and lsp.install_strategy. Advanced lsp.servers overrides stay in raw YAML. Whether remote or sandbox backends trigger host LSP depends on the Hermes runtime cwd and Git workspace detection.', '這裡寫入 lsp.enabled、lsp.wait_mode、lsp.wait_timeout 和 lsp.install_strategy。進階 lsp.servers 覆蓋保留在 raw YAML遠端或沙箱後端是否觸發宿主 LSP 以 Hermes 執行時工作目錄和 Git workspace 偵測為準。'),
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', '結構化設定'),