feat(hermes): add terminal execution config form

This commit is contained in:
晴天
2026-05-24 20:06:50 +08:00
parent 122d7a63be
commit 56e41dd512
8 changed files with 771 additions and 8 deletions

View File

@@ -64,9 +64,23 @@ const EXECUTION_LIMITS_DEFAULTS = {
delegationInheritMcpToolsets: true,
}
const TERMINAL_DEFAULTS = {
terminalBackend: 'local',
terminalCwd: '.',
terminalTimeout: 180,
terminalLifetimeSeconds: 300,
terminalDockerMountCwdToWorkspace: false,
terminalDockerRunAsHostUser: false,
terminalContainerCpu: 1,
terminalContainerMemory: 5120,
terminalContainerDisk: 51200,
terminalContainerPersistent: true,
}
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']
export function render() {
const el = document.createElement('div')
@@ -79,6 +93,7 @@ export function render() {
let memoryValues = { ...MEMORY_DEFAULTS }
let streamingValues = { ...STREAMING_DEFAULTS }
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
let terminalValues = { ...TERMINAL_DEFAULTS }
let loading = true
let runtimeLoading = true
let compressionLoading = true
@@ -86,6 +101,7 @@ export function render() {
let memoryLoading = true
let streamingLoading = true
let executionLimitsLoading = true
let terminalLoading = true
let saving = false
let runtimeSaving = false
let compressionSaving = false
@@ -93,6 +109,7 @@ export function render() {
let memorySaving = false
let streamingSaving = false
let executionLimitsSaving = false
let terminalSaving = false
let error = null
let runtimeError = null
let compressionError = null
@@ -100,6 +117,7 @@ export function render() {
let memoryError = null
let streamingError = null
let executionLimitsError = null
let terminalError = null
function esc(value) {
return String(value ?? '')
@@ -110,7 +128,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || streamingLoading || executionLimitsLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -127,7 +145,7 @@ export function render() {
}
function renderRuntimePanel() {
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel">
<div class="hm-panel-header">
@@ -175,7 +193,7 @@ export function render() {
}
function renderCompressionPanel() {
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-compression-panel">
<div class="hm-panel-header">
@@ -225,7 +243,7 @@ export function render() {
}
function renderToolGuardrailsPanel() {
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-guardrails-panel">
<div class="hm-panel-header">
@@ -287,7 +305,7 @@ export function render() {
}
function renderMemoryPanel() {
const disabled = loading || saving || memoryLoading || memorySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || memoryLoading || memorySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-memory-panel">
<div class="hm-panel-header">
@@ -333,7 +351,7 @@ export function render() {
}
function renderStreamingPanel() {
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || executionLimitsSaving
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-streaming-panel">
<div class="hm-panel-header">
@@ -385,7 +403,7 @@ export function render() {
}
function renderExecutionLimitsPanel() {
const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving
const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-execution-limits-panel">
<div class="hm-panel-header">
@@ -456,6 +474,77 @@ export function render() {
`
}
function renderTerminalPanel() {
const disabled = loading || saving || terminalLoading || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-terminal-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesTerminalConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesTerminalConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${terminalSaving ? t('engine.hermesConfigStatusSaving') : terminalLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesTerminalConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-terminal-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesTerminalConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(terminalError)}
<div class="hm-config-runtime-grid hm-config-terminal-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigBackend')}</span>
<select id="hm-terminal-backend" class="hm-input" ${disabled ? 'disabled' : ''}>
${TERMINAL_BACKENDS.map(mode => option(`engine.hermesTerminalConfigBackend_${mode}`, mode, terminalValues.terminalBackend)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigCwd')}</span>
<input id="hm-terminal-cwd" class="hm-input" value="${esc(terminalValues.terminalCwd)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigTimeout')}</span>
<input id="hm-terminal-timeout" class="hm-input" type="number" inputmode="numeric" min="1" max="86400" step="1" value="${esc(terminalValues.terminalTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigLifetimeSeconds')}</span>
<input id="hm-terminal-lifetime-seconds" class="hm-input" type="number" inputmode="numeric" min="0" max="86400" step="1" value="${esc(terminalValues.terminalLifetimeSeconds)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-config-check-grid">
<label class="hm-channel-check hm-channel-check--danger">
<input id="hm-terminal-docker-mount-cwd-to-workspace" type="checkbox" ${terminalValues.terminalDockerMountCwdToWorkspace ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesTerminalConfigDockerMountCwd')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-terminal-docker-run-as-host-user" type="checkbox" ${terminalValues.terminalDockerRunAsHostUser ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesTerminalConfigDockerRunAsHostUser')}</span>
</label>
<label class="hm-channel-check">
<input id="hm-terminal-container-persistent" type="checkbox" ${terminalValues.terminalContainerPersistent ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesTerminalConfigContainerPersistent')}</span>
</label>
</div>
<div class="hm-config-subtitle">${t('engine.hermesTerminalConfigContainerTitle')}</div>
<div class="hm-config-runtime-grid hm-config-terminal-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigContainerCpu')}</span>
<input id="hm-terminal-container-cpu" class="hm-input" type="number" inputmode="numeric" min="1" max="64" step="1" value="${esc(terminalValues.terminalContainerCpu)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigContainerMemory')}</span>
<input id="hm-terminal-container-memory" class="hm-input" type="number" inputmode="numeric" min="128" max="1048576" step="128" value="${esc(terminalValues.terminalContainerMemory)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesTerminalConfigContainerDisk')}</span>
<input id="hm-terminal-container-disk" class="hm-input" type="number" inputmode="numeric" min="1024" max="10485760" step="1024" value="${esc(terminalValues.terminalContainerDisk)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesTerminalConfigFootnote')}</div>
</div>
</div>
`
}
function draw() {
el.innerHTML = `
<div class="hm-hero">
@@ -471,6 +560,7 @@ export function render() {
</div>
${renderRuntimePanel()}
${renderTerminalPanel()}
${renderStreamingPanel()}
${renderExecutionLimitsPanel()}
${renderCompressionPanel()}
@@ -501,6 +591,7 @@ export function render() {
el.querySelector('#hm-memory-save')?.addEventListener('click', saveMemory)
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)
}
async function loadRaw() {
@@ -538,6 +629,11 @@ export function render() {
executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS, ...(data?.values || {}) }
}
async function loadTerminal() {
const data = await api.hermesTerminalConfigRead()
terminalValues = { ...TERMINAL_DEFAULTS, ...(data?.values || {}) }
}
async function load() {
loading = true
runtimeLoading = true
@@ -546,6 +642,7 @@ export function render() {
memoryLoading = true
streamingLoading = true
executionLimitsLoading = true
terminalLoading = true
error = null
runtimeError = null
compressionError = null
@@ -553,6 +650,7 @@ export function render() {
memoryError = null
streamingError = null
executionLimitsError = null
terminalError = null
draw()
try {
await loadRaw()
@@ -601,6 +699,14 @@ export function render() {
executionLimitsLoading = false
draw()
}
try {
await loadTerminal()
} catch (err) {
terminalError = humanizeError(err, t('engine.hermesTerminalConfigLoadFailed') || 'Load terminal config failed')
} finally {
terminalLoading = false
draw()
}
try {
await loadMemory()
} catch (err) {
@@ -648,6 +754,9 @@ export function render() {
try {
await loadExecutionLimits()
} catch {}
try {
await loadTerminal()
} catch {}
} catch (err) {
error = humanizeError(err, t('engine.hermesConfigSaveFailed') || 'Save failed')
toast(error, 'error')
@@ -841,6 +950,40 @@ export function render() {
}
}
async function saveTerminal() {
const form = {
terminalBackend: el.querySelector('#hm-terminal-backend')?.value || 'local',
terminalCwd: el.querySelector('#hm-terminal-cwd')?.value || '.',
terminalTimeout: el.querySelector('#hm-terminal-timeout')?.value || '180',
terminalLifetimeSeconds: el.querySelector('#hm-terminal-lifetime-seconds')?.value || '300',
terminalDockerMountCwdToWorkspace: !!el.querySelector('#hm-terminal-docker-mount-cwd-to-workspace')?.checked,
terminalDockerRunAsHostUser: !!el.querySelector('#hm-terminal-docker-run-as-host-user')?.checked,
terminalContainerCpu: el.querySelector('#hm-terminal-container-cpu')?.value || '1',
terminalContainerMemory: el.querySelector('#hm-terminal-container-memory')?.value || '5120',
terminalContainerDisk: el.querySelector('#hm-terminal-container-disk')?.value || '51200',
terminalContainerPersistent: !!el.querySelector('#hm-terminal-container-persistent')?.checked,
}
terminalSaving = true
terminalError = null
draw()
try {
const result = await api.hermesTerminalConfigSave(form)
terminalValues = { ...TERMINAL_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesTerminalConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
terminalError = humanizeError(err, t('engine.hermesTerminalConfigSaveFailed') || 'Save terminal config failed')
toast(terminalError, 'error')
} finally {
terminalSaving = false
draw()
}
}
draw()
load()
return el

View File

@@ -521,6 +521,8 @@ export const api = {
hermesStreamingConfigSave: (form) => invoke('hermes_streaming_config_save', { form }),
hermesExecutionLimitsConfigRead: () => invoke('hermes_execution_limits_config_read'),
hermesExecutionLimitsConfigSave: (form) => invoke('hermes_execution_limits_config_save', { form }),
hermesTerminalConfigRead: () => invoke('hermes_terminal_config_read'),
hermesTerminalConfigSave: (form) => invoke('hermes_terminal_config_save', { form }),
hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000),
hermesLazyDepsStatus: (features) => invoke('hermes_lazy_deps_status', { features }),
hermesLazyDepsEnsure: (feature) => invoke('hermes_lazy_deps_ensure', { feature }),

View File

@@ -498,6 +498,32 @@ export default {
hermesGroupSessionsPerUser: _('群聊按成员隔离会话', 'Isolate group sessions per user', '群聊依成員隔離會話'),
hermesThreadSessionsPerUser: _('线程也按成员隔离', 'Isolate thread sessions per user', '討論串也依成員隔離'),
hermesSessionRuntimeFootnote: _('推荐保持群聊隔离开启。关闭后,同一群/频道会共用上下文和中断状态。', 'Keeping group isolation on is recommended. Turning it off shares context and interrupt state across the same group or channel.', '建議保持群聊隔離開啟。關閉後,同一群組/頻道會共用上下文和中斷狀態。'),
hermesTerminalConfigTitle: _('终端执行', 'Terminal execution', '終端執行'),
hermesTerminalConfigDesc: _('控制 Hermes 工具命令的执行环境、工作目录、超时和容器资源,避免长任务卡死或沙箱范围误配。', 'Control command execution backend, working directory, timeouts, and container resources to avoid stuck runs or sandbox misconfiguration.', '控制 Hermes 工具命令的執行環境、工作目錄、逾時和容器資源,避免長任務卡死或沙箱範圍誤配。'),
hermesTerminalConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesTerminalConfigSave: _('保存终端配置', 'Save terminal settings', '儲存終端設定'),
hermesTerminalConfigSaveSuccess: _('终端执行配置已保存,建议重启 Hermes Gateway 生效', 'Terminal execution settings saved. Restart Hermes Gateway to take effect.', '終端執行設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesTerminalConfigLoadFailed: _('加载终端执行配置失败', 'Load terminal execution settings failed', '載入終端執行設定失敗'),
hermesTerminalConfigSaveFailed: _('保存终端执行配置失败', 'Save terminal execution settings failed', '儲存終端執行設定失敗'),
hermesTerminalConfigBackend: _('执行后端', 'Execution backend', '執行後端'),
hermesTerminalConfigBackend_local: _('本机', 'Local machine', '本機'),
hermesTerminalConfigBackend_ssh: _('SSH 远程', 'SSH remote', 'SSH 遠端'),
hermesTerminalConfigBackend_docker: _('Docker 容器', 'Docker container', 'Docker 容器'),
hermesTerminalConfigBackend_singularity: _('Singularity / Apptainer', 'Singularity / Apptainer', 'Singularity / Apptainer'),
hermesTerminalConfigBackend_modal: _('Modal 云沙箱', 'Modal cloud sandbox', 'Modal 雲端沙箱'),
hermesTerminalConfigBackend_daytona: _('Daytona 云沙箱', 'Daytona cloud sandbox', 'Daytona 雲端沙箱'),
hermesTerminalConfigBackend_vercel_sandbox: _('Vercel 沙箱', 'Vercel sandbox', 'Vercel 沙箱'),
hermesTerminalConfigCwd: _('工作目录', 'Working directory', '工作目錄'),
hermesTerminalConfigTimeout: _('命令超时秒数', 'Command timeout seconds', '命令逾時秒數'),
hermesTerminalConfigLifetimeSeconds: _('沙箱生命周期秒数', 'Sandbox lifetime seconds', '沙箱生命週期秒數'),
hermesTerminalConfigDockerMountCwd: _('Docker 挂载启动目录到 /workspace', 'Mount launch cwd into Docker /workspace', 'Docker 掛載啟動目錄到 /workspace'),
hermesTerminalConfigDockerRunAsHostUser: _('Docker 使用宿主用户运行', 'Run Docker as host user', 'Docker 使用宿主使用者執行'),
hermesTerminalConfigContainerPersistent: _('容器文件系统持久化', 'Persist container filesystem', '容器檔案系統持久化'),
hermesTerminalConfigContainerTitle: _('容器资源限制', 'Container resource limits', '容器資源限制'),
hermesTerminalConfigContainerCpu: _('CPU 核数', 'CPU cores', 'CPU 核心數'),
hermesTerminalConfigContainerMemory: _('内存 MB', 'Memory MB', '記憶體 MB'),
hermesTerminalConfigContainerDisk: _('磁盘 MB', 'Disk MB', '磁碟 MB'),
hermesTerminalConfigFootnote: _('Docker 挂载启动目录会把宿主目录暴露给沙箱仅在可信项目和无人值守任务中开启SSH、镜像、环境变量等高级参数仍可在 raw YAML 中编辑。', 'Mounting the launch cwd exposes host files to the sandbox. Enable it only for trusted projects or unattended jobs. SSH, image, and env advanced fields remain editable in raw YAML.', 'Docker 掛載啟動目錄會把宿主目錄暴露給沙箱僅在可信專案和無人值守任務中開啟SSH、映像、環境變數等進階參數仍可在 raw YAML 中編輯。'),
hermesStreamingConfigTitle: _('网关流式输出', 'Gateway streaming output', '閘道流式輸出'),
hermesStreamingConfigDesc: _('控制 Hermes Gateway 回复时是否边生成边更新消息,以及消息刷新节奏。适合需要更快看到长回复进度的渠道。', 'Control whether Hermes Gateway updates messages while replies are generated, plus the refresh cadence. Useful when channels need quicker progress for long replies.', '控制 Hermes Gateway 回覆時是否邊生成邊更新訊息,以及訊息刷新節奏。適合需要更快看到長回覆進度的渠道。'),
hermesStreamingConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),