mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): add terminal cloud runtime controls
This commit is contained in:
@@ -3324,6 +3324,8 @@ const HERMES_SESSION_RESET_MODES = new Set(['both', 'idle', 'daily', 'none'])
|
||||
const HERMES_STREAMING_TRANSPORTS = new Set(['auto', 'draft', 'edit', 'off'])
|
||||
const HERMES_CODE_EXECUTION_MODES = new Set(['project', 'strict'])
|
||||
const HERMES_TERMINAL_BACKENDS = new Set(['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox'])
|
||||
const HERMES_TERMINAL_MODAL_MODES = new Set(['auto', 'managed', 'direct'])
|
||||
const HERMES_TERMINAL_VERCEL_RUNTIMES = new Set(['node24', 'node22', 'python3.13'])
|
||||
const HERMES_BROWSER_ENGINES = new Set(['auto', 'lightpanda', 'chrome'])
|
||||
const HERMES_BROWSER_DIALOG_POLICIES = new Set(['must_respond', 'auto_dismiss', 'auto_accept'])
|
||||
const HERMES_STT_PROVIDERS = new Set(['auto', 'local', 'groq', 'openai', 'mistral'])
|
||||
@@ -3458,6 +3460,20 @@ function normalizeHermesTerminalBackend(value, strict = false) {
|
||||
return 'local'
|
||||
}
|
||||
|
||||
function normalizeHermesTerminalModalMode(value, strict = false) {
|
||||
const mode = String(value ?? '').trim().toLowerCase() || 'auto'
|
||||
if (HERMES_TERMINAL_MODAL_MODES.has(mode)) return mode
|
||||
if (strict) throw new Error('terminal.modal_mode 必须是 auto、managed 或 direct')
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
function normalizeHermesTerminalVercelRuntime(value, strict = false) {
|
||||
const runtime = String(value ?? '').trim().toLowerCase() || 'node24'
|
||||
if (HERMES_TERMINAL_VERCEL_RUNTIMES.has(runtime)) return runtime
|
||||
if (strict) throw new Error('terminal.vercel_runtime 必须是 node24、node22 或 python3.13')
|
||||
return 'node24'
|
||||
}
|
||||
|
||||
function normalizeHermesBrowserEngine(value, strict = false) {
|
||||
const engine = String(value ?? '').trim().toLowerCase() || 'auto'
|
||||
if (HERMES_BROWSER_ENGINES.has(engine)) return engine
|
||||
@@ -5636,6 +5652,8 @@ export function buildHermesTerminalConfigValues(config = {}) {
|
||||
terminalDockerImage: typeof terminal.docker_image === 'string' ? terminal.docker_image.trim() : '',
|
||||
terminalSingularityImage: typeof terminal.singularity_image === 'string' ? terminal.singularity_image.trim() : '',
|
||||
terminalModalImage: typeof terminal.modal_image === 'string' ? terminal.modal_image.trim() : '',
|
||||
terminalModalMode: normalizeHermesTerminalModalMode(terminal.modal_mode, false),
|
||||
terminalVercelRuntime: normalizeHermesTerminalVercelRuntime(terminal.vercel_runtime, false),
|
||||
terminalDaytonaImage: typeof terminal.daytona_image === 'string' ? terminal.daytona_image.trim() : '',
|
||||
terminalDockerForwardEnv: normalizeHermesEnvNameList(terminal.docker_forward_env || [], 'terminal.docker_forward_env').join('\n'),
|
||||
terminalSshHost: typeof terminal.ssh_host === 'string' ? terminal.ssh_host.trim() : '',
|
||||
@@ -5669,6 +5687,8 @@ export function mergeHermesTerminalConfig(config = {}, form = {}) {
|
||||
else delete terminal.env_passthrough
|
||||
terminal.docker_mount_cwd_to_workspace = formHermesBool(form, 'terminalDockerMountCwdToWorkspace', currentValues.terminalDockerMountCwdToWorkspace)
|
||||
terminal.docker_run_as_host_user = formHermesBool(form, 'terminalDockerRunAsHostUser', currentValues.terminalDockerRunAsHostUser)
|
||||
terminal.modal_mode = normalizeHermesTerminalModalMode(Object.hasOwn(form, 'terminalModalMode') ? form.terminalModalMode : currentValues.terminalModalMode, true)
|
||||
terminal.vercel_runtime = normalizeHermesTerminalVercelRuntime(Object.hasOwn(form, 'terminalVercelRuntime') ? form.terminalVercelRuntime : currentValues.terminalVercelRuntime, true)
|
||||
for (const [formKey, yamlKey] of [
|
||||
['terminalDockerImage', 'docker_image'],
|
||||
['terminalSingularityImage', 'singularity_image'],
|
||||
|
||||
@@ -7079,6 +7079,46 @@ fn normalize_hermes_terminal_backend(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_terminal_modal_mode(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
) -> Result<String, String> {
|
||||
let mode = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let mode = if mode.is_empty() {
|
||||
"auto".to_string()
|
||||
} else {
|
||||
mode
|
||||
};
|
||||
if matches!(mode.as_str(), "auto" | "managed" | "direct") {
|
||||
return Ok(mode);
|
||||
}
|
||||
if strict {
|
||||
Err("terminal.modal_mode 必须是 auto、managed 或 direct".to_string())
|
||||
} else {
|
||||
Ok("auto".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_terminal_vercel_runtime(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
) -> Result<String, String> {
|
||||
let runtime = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let runtime = if runtime.is_empty() {
|
||||
"node24".to_string()
|
||||
} else {
|
||||
runtime
|
||||
};
|
||||
if matches!(runtime.as_str(), "node24" | "node22" | "python3.13") {
|
||||
return Ok(runtime);
|
||||
}
|
||||
if strict {
|
||||
Err("terminal.vercel_runtime 必须是 node24、node22 或 python3.13".to_string())
|
||||
} else {
|
||||
Ok("node24".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_browser_engine(value: Option<String>, strict: bool) -> Result<String, String> {
|
||||
let engine = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let engine = if engine.is_empty() {
|
||||
@@ -8964,6 +9004,16 @@ fn build_hermes_terminal_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let terminal_docker_image = terminal_string("docker_image");
|
||||
let terminal_singularity_image = terminal_string("singularity_image");
|
||||
let terminal_modal_image = terminal_string("modal_image");
|
||||
let terminal_modal_mode = normalize_hermes_terminal_modal_mode(
|
||||
terminal.and_then(|map| yaml_string_field(map, "modal_mode")),
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|_| "auto".to_string());
|
||||
let terminal_vercel_runtime = normalize_hermes_terminal_vercel_runtime(
|
||||
terminal.and_then(|map| yaml_string_field(map, "vercel_runtime")),
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|_| "node24".to_string());
|
||||
let terminal_daytona_image = terminal_string("daytona_image");
|
||||
let terminal_docker_forward_env = terminal
|
||||
.map(|map| yaml_string_sequence_field(map, "docker_forward_env").join("\n"))
|
||||
@@ -9001,6 +9051,8 @@ fn build_hermes_terminal_config_values(config: &serde_yaml::Value) -> Value {
|
||||
"terminalDockerImage": terminal_docker_image,
|
||||
"terminalSingularityImage": terminal_singularity_image,
|
||||
"terminalModalImage": terminal_modal_image,
|
||||
"terminalModalMode": terminal_modal_mode,
|
||||
"terminalVercelRuntime": terminal_vercel_runtime,
|
||||
"terminalDaytonaImage": terminal_daytona_image,
|
||||
"terminalDockerForwardEnv": terminal_docker_forward_env,
|
||||
"terminalSshHost": terminal_ssh_host,
|
||||
@@ -9098,6 +9150,26 @@ fn merge_hermes_terminal_config(
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let terminal_modal_mode = normalize_hermes_terminal_modal_mode(
|
||||
if form.get("terminalModalMode").is_some() {
|
||||
form_string(form, "terminalModalMode")
|
||||
} else {
|
||||
current["terminalModalMode"]
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
let terminal_vercel_runtime = normalize_hermes_terminal_vercel_runtime(
|
||||
if form.get("terminalVercelRuntime").is_some() {
|
||||
form_string(form, "terminalVercelRuntime")
|
||||
} else {
|
||||
current["terminalVercelRuntime"]
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
let terminal_docker_image = form_string(form, "terminalDockerImage")
|
||||
.or_else(|| {
|
||||
current["terminalDockerImage"]
|
||||
@@ -9268,6 +9340,14 @@ fn merge_hermes_terminal_config(
|
||||
set_optional_yaml_string(terminal, "docker_image", terminal_docker_image);
|
||||
set_optional_yaml_string(terminal, "singularity_image", terminal_singularity_image);
|
||||
set_optional_yaml_string(terminal, "modal_image", terminal_modal_image);
|
||||
terminal.insert(
|
||||
yaml_key("modal_mode"),
|
||||
serde_yaml::Value::String(terminal_modal_mode),
|
||||
);
|
||||
terminal.insert(
|
||||
yaml_key("vercel_runtime"),
|
||||
serde_yaml::Value::String(terminal_vercel_runtime),
|
||||
);
|
||||
set_optional_yaml_string(terminal, "daytona_image", terminal_daytona_image);
|
||||
if terminal_docker_forward_env.is_empty() {
|
||||
terminal.remove(yaml_key("docker_forward_env"));
|
||||
@@ -18450,6 +18530,8 @@ mod hermes_terminal_config_tests {
|
||||
assert_eq!(values["terminalDockerImage"], "");
|
||||
assert_eq!(values["terminalSingularityImage"], "");
|
||||
assert_eq!(values["terminalModalImage"], "");
|
||||
assert_eq!(values["terminalModalMode"], "auto");
|
||||
assert_eq!(values["terminalVercelRuntime"], "node24");
|
||||
assert_eq!(values["terminalDaytonaImage"], "");
|
||||
assert_eq!(values["terminalDockerForwardEnv"], "");
|
||||
assert_eq!(values["terminalSshHost"], "");
|
||||
@@ -18483,6 +18565,8 @@ terminal:
|
||||
- NPM_TOKEN
|
||||
singularity_image: docker://nikolaik/python-nodejs:python3.11-nodejs20
|
||||
modal_image: python:3.12
|
||||
modal_mode: managed
|
||||
vercel_runtime: python3.13
|
||||
daytona_image: ubuntu:24.04
|
||||
ssh_host: build.example.com
|
||||
ssh_user: deploy
|
||||
@@ -18525,6 +18609,8 @@ terminal:
|
||||
"docker://nikolaik/python-nodejs:python3.11-nodejs20"
|
||||
);
|
||||
assert_eq!(values["terminalModalImage"], "python:3.12");
|
||||
assert_eq!(values["terminalModalMode"], "managed");
|
||||
assert_eq!(values["terminalVercelRuntime"], "python3.13");
|
||||
assert_eq!(values["terminalDaytonaImage"], "ubuntu:24.04");
|
||||
assert_eq!(values["terminalSshHost"], "build.example.com");
|
||||
assert_eq!(values["terminalSshUser"], "deploy");
|
||||
@@ -18575,6 +18661,8 @@ streaming:
|
||||
"terminalDockerForwardEnv": "GITHUB_TOKEN\nNPM_TOKEN\nGITHUB_TOKEN",
|
||||
"terminalSingularityImage": "docker://ubuntu:24.04",
|
||||
"terminalModalImage": "debian:bookworm",
|
||||
"terminalModalMode": "direct",
|
||||
"terminalVercelRuntime": "node22",
|
||||
"terminalDaytonaImage": "ubuntu:22.04",
|
||||
"terminalSshHost": "ssh.example.com",
|
||||
"terminalSshUser": "hermes",
|
||||
@@ -18652,6 +18740,11 @@ streaming:
|
||||
config["terminal"]["modal_image"].as_str(),
|
||||
Some("debian:bookworm")
|
||||
);
|
||||
assert_eq!(config["terminal"]["modal_mode"].as_str(), Some("direct"));
|
||||
assert_eq!(
|
||||
config["terminal"]["vercel_runtime"].as_str(),
|
||||
Some("node22")
|
||||
);
|
||||
assert_eq!(
|
||||
config["terminal"]["daytona_image"].as_str(),
|
||||
Some("ubuntu:22.04")
|
||||
@@ -18852,6 +18945,14 @@ terminal:
|
||||
merge_hermes_terminal_config(&mut config, &json!({ "terminalBackend": "unsafe" }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("terminal.backend"));
|
||||
let err =
|
||||
merge_hermes_terminal_config(&mut config, &json!({ "terminalModalMode": "unsafe" }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("terminal.modal_mode"));
|
||||
let err =
|
||||
merge_hermes_terminal_config(&mut config, &json!({ "terminalVercelRuntime": "ruby" }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("terminal.vercel_runtime"));
|
||||
let err = merge_hermes_terminal_config(&mut config, &json!({ "terminalTimeout": 0 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("terminal.timeout"));
|
||||
|
||||
@@ -339,6 +339,8 @@ const TERMINAL_DEFAULTS = {
|
||||
terminalDockerImage: '',
|
||||
terminalSingularityImage: '',
|
||||
terminalModalImage: '',
|
||||
terminalModalMode: 'auto',
|
||||
terminalVercelRuntime: 'node24',
|
||||
terminalDaytonaImage: '',
|
||||
terminalDockerForwardEnv: '',
|
||||
terminalSshHost: '',
|
||||
@@ -355,6 +357,8 @@ 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 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 STT_PROVIDERS = ['auto', 'local', 'groq', 'openai', 'mistral']
|
||||
@@ -2508,6 +2512,18 @@ export function render() {
|
||||
<span class="hm-field-label">${t('engine.hermesTerminalConfigModalImage')}</span>
|
||||
<input id="hm-terminal-modal-image" class="hm-input" value="${esc(terminalValues.terminalModalImage)}" placeholder="nikolaik/python-nodejs:python3.11-nodejs20" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesTerminalConfigModalMode')}</span>
|
||||
<select id="hm-terminal-modal-mode" class="hm-input" ${disabled ? 'disabled' : ''}>
|
||||
${TERMINAL_MODAL_MODES.map(mode => option(`engine.hermesTerminalConfigModalMode_${mode}`, mode, terminalValues.terminalModalMode)).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesTerminalConfigVercelRuntime')}</span>
|
||||
<select id="hm-terminal-vercel-runtime" class="hm-input" ${disabled ? 'disabled' : ''}>
|
||||
${TERMINAL_VERCEL_RUNTIMES.map(runtime => option(`engine.hermesTerminalConfigVercelRuntime_${runtime.replace('.', '_')}`, runtime, terminalValues.terminalVercelRuntime)).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesTerminalConfigDaytonaImage')}</span>
|
||||
<input id="hm-terminal-daytona-image" class="hm-input" value="${esc(terminalValues.terminalDaytonaImage)}" placeholder="nikolaik/python-nodejs:python3.11-nodejs20" ${disabled ? 'disabled' : ''}>
|
||||
@@ -4474,6 +4490,8 @@ export function render() {
|
||||
terminalDockerForwardEnv: el.querySelector('#hm-terminal-docker-forward-env')?.value || '',
|
||||
terminalSingularityImage: el.querySelector('#hm-terminal-singularity-image')?.value || '',
|
||||
terminalModalImage: el.querySelector('#hm-terminal-modal-image')?.value || '',
|
||||
terminalModalMode: el.querySelector('#hm-terminal-modal-mode')?.value || 'auto',
|
||||
terminalVercelRuntime: el.querySelector('#hm-terminal-vercel-runtime')?.value || 'node24',
|
||||
terminalDaytonaImage: el.querySelector('#hm-terminal-daytona-image')?.value || '',
|
||||
terminalSshHost: el.querySelector('#hm-terminal-ssh-host')?.value || '',
|
||||
terminalSshUser: el.querySelector('#hm-terminal-ssh-user')?.value || '',
|
||||
|
||||
@@ -557,11 +557,19 @@ export default {
|
||||
hermesTerminalConfigDockerForwardEnv: _('Docker 转发环境变量名', 'Docker forwarded env names', 'Docker 轉發環境變數名'),
|
||||
hermesTerminalConfigSingularityImage: _('Singularity 镜像(可选)', 'Singularity image (optional)', 'Singularity 映像(可選)'),
|
||||
hermesTerminalConfigModalImage: _('Modal 镜像(可选)', 'Modal image (optional)', 'Modal 映像(可選)'),
|
||||
hermesTerminalConfigModalMode: _('Modal 运行模式', 'Modal runtime mode', 'Modal 執行模式'),
|
||||
hermesTerminalConfigModalMode_auto: _('自动选择', 'Auto select', '自動選擇'),
|
||||
hermesTerminalConfigModalMode_managed: _('Nous 托管 Modal', 'Nous-managed Modal', 'Nous 託管 Modal'),
|
||||
hermesTerminalConfigModalMode_direct: _('直连 Modal 账号', 'Direct Modal account', '直連 Modal 帳號'),
|
||||
hermesTerminalConfigVercelRuntime: _('Vercel 运行时', 'Vercel runtime', 'Vercel 執行時'),
|
||||
hermesTerminalConfigVercelRuntime_node24: _('Node.js 24', 'Node.js 24', 'Node.js 24'),
|
||||
hermesTerminalConfigVercelRuntime_node22: _('Node.js 22', 'Node.js 22', 'Node.js 22'),
|
||||
hermesTerminalConfigVercelRuntime_python3_13: _('Python 3.13', 'Python 3.13', 'Python 3.13'),
|
||||
hermesTerminalConfigDaytonaImage: _('Daytona 镜像(可选)', 'Daytona image (optional)', 'Daytona 映像(可選)'),
|
||||
hermesTerminalConfigContainerCpu: _('CPU 核数', 'CPU cores', 'CPU 核心數'),
|
||||
hermesTerminalConfigContainerMemory: _('内存 MB', 'Memory MB', '記憶體 MB'),
|
||||
hermesTerminalConfigContainerDisk: _('磁盘 MB', 'Disk MB', '磁碟 MB'),
|
||||
hermesTerminalConfigFootnote: _('Shell 初始化文件每行一个路径,支持 ~ 和 ${VAR} 占位;留空会移除 shell_init_files,让 Hermes 使用自动读取默认启动文件的行为。沙箱环境变量透传和 Docker 环境变量转发都只保存变量名,不保存密钥值;每行一个变量名,留空会移除覆盖。关闭自动读取可避免有问题的 rc 文件在非交互环境中退出。SSH 字段只在 SSH 后端生效,留空会移除主机、用户和密钥覆盖;面板不保存 SSH 密码。镜像字段只在对应 Docker、Singularity、Modal 或 Daytona 后端生效;留空会移除覆盖并使用 Hermes 默认值。Docker 挂载启动目录会把宿主目录暴露给沙箱,仅在可信项目和无人值守任务中开启。', 'Shell init files use one path per line and support ~ plus ${VAR} placeholders. Leaving the list blank removes shell_init_files so Hermes uses its automatic startup-file sourcing behavior. Sandbox env passthrough and Docker env forwarding both store variable names only, not secret values; use one name per line, or leave blank to remove the override. Turn auto-source off only when an rc file exits in non-interactive environments. SSH fields only apply to the SSH backend. Leaving them blank removes host, user, and key overrides; the panel does not save SSH passwords. Image fields only apply to the matching Docker, Singularity, Modal, or Daytona backend. Leaving them blank removes the override and uses Hermes defaults. Mounting the launch cwd exposes host files to the sandbox; enable it only for trusted projects or unattended jobs.', 'Shell 初始化檔案每行一個路徑,支援 ~ 和 ${VAR} 佔位;留空會移除 shell_init_files,讓 Hermes 使用自動讀取預設啟動檔案的行為。沙箱環境變數透傳和 Docker 環境變數轉發都只儲存變數名,不儲存密鑰值;每行一個變數名,留空會移除覆蓋。關閉自動讀取可避免有問題的 rc 檔案在非互動環境中退出。SSH 欄位只在 SSH 後端生效,留空會移除主機、使用者和金鑰覆蓋;面板不儲存 SSH 密碼。映像欄位只在對應 Docker、Singularity、Modal 或 Daytona 後端生效;留空會移除覆蓋並使用 Hermes 預設值。Docker 掛載啟動目錄會把宿主目錄暴露給沙箱,僅在可信專案和無人值守任務中開啟。'),
|
||||
hermesTerminalConfigFootnote: _('Shell 初始化文件每行一个路径,支持 ~ 和 ${VAR} 占位;留空会移除 shell_init_files,让 Hermes 使用自动读取默认启动文件的行为。沙箱环境变量透传和 Docker 环境变量转发都只保存变量名,不保存密钥值;每行一个变量名,留空会移除覆盖。关闭自动读取可避免有问题的 rc 文件在非交互环境中退出。SSH 字段只在 SSH 后端生效,留空会移除主机、用户和密钥覆盖;面板不保存 SSH 密码。镜像字段只在对应 Docker、Singularity、Modal 或 Daytona 后端生效;留空会移除覆盖并使用 Hermes 默认值。Modal 运行模式和 Vercel 运行时会写入上游 terminal.modal_mode / terminal.vercel_runtime,建议与实际云沙箱账号能力保持一致。Docker 挂载启动目录会把宿主目录暴露给沙箱,仅在可信项目和无人值守任务中开启。', 'Shell init files use one path per line and support ~ plus ${VAR} placeholders. Leaving the list blank removes shell_init_files so Hermes uses its automatic startup-file sourcing behavior. Sandbox env passthrough and Docker env forwarding both store variable names only, not secret values; use one name per line, or leave blank to remove the override. Turn auto-source off only when an rc file exits in non-interactive environments. SSH fields only apply to the SSH backend. Leaving them blank removes host, user, and key overrides; the panel does not save SSH passwords. Image fields only apply to the matching Docker, Singularity, Modal, or Daytona backend. Leaving them blank removes the override and uses Hermes defaults. Modal runtime mode and Vercel runtime write upstream terminal.modal_mode / terminal.vercel_runtime; keep them aligned with the actual cloud sandbox account. Mounting the launch cwd exposes host files to the sandbox; enable it only for trusted projects or unattended jobs.', 'Shell 初始化檔案每行一個路徑,支援 ~ 和 ${VAR} 佔位;留空會移除 shell_init_files,讓 Hermes 使用自動讀取預設啟動檔案的行為。沙箱環境變數透傳和 Docker 環境變數轉發都只儲存變數名,不儲存密鑰值;每行一個變數名,留空會移除覆蓋。關閉自動讀取可避免有問題的 rc 檔案在非互動環境中退出。SSH 欄位只在 SSH 後端生效,留空會移除主機、使用者和金鑰覆蓋;面板不儲存 SSH 密碼。映像欄位只在對應 Docker、Singularity、Modal 或 Daytona 後端生效;留空會移除覆蓋並使用 Hermes 預設值。Modal 執行模式和 Vercel 執行時會寫入上游 terminal.modal_mode / terminal.vercel_runtime,建議與實際雲端沙箱帳號能力保持一致。Docker 掛載啟動目錄會把宿主目錄暴露給沙箱,僅在可信專案和無人值守任務中開啟。'),
|
||||
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', '結構化設定'),
|
||||
|
||||
@@ -448,6 +448,8 @@ test('Hermes 配置页会暴露终端执行结构化配置字段', () => {
|
||||
'hm-terminal-docker-forward-env',
|
||||
'hm-terminal-singularity-image',
|
||||
'hm-terminal-modal-image',
|
||||
'hm-terminal-modal-mode',
|
||||
'hm-terminal-vercel-runtime',
|
||||
'hm-terminal-daytona-image',
|
||||
'hm-terminal-ssh-host',
|
||||
'hm-terminal-ssh-user',
|
||||
|
||||
@@ -27,6 +27,8 @@ test('Hermes 终端执行配置读取会提供上游默认值', () => {
|
||||
terminalDockerImage: '',
|
||||
terminalSingularityImage: '',
|
||||
terminalModalImage: '',
|
||||
terminalModalMode: 'auto',
|
||||
terminalVercelRuntime: 'node24',
|
||||
terminalDaytonaImage: '',
|
||||
terminalDockerForwardEnv: '',
|
||||
terminalSshHost: '',
|
||||
@@ -53,6 +55,8 @@ test('Hermes 终端执行配置读取会回显 YAML 字段', () => {
|
||||
docker_forward_env: ['GITHUB_TOKEN', 'NPM_TOKEN'],
|
||||
singularity_image: 'docker://nikolaik/python-nodejs:python3.11-nodejs20',
|
||||
modal_image: 'python:3.12',
|
||||
modal_mode: 'managed',
|
||||
vercel_runtime: 'python3.13',
|
||||
daytona_image: 'ubuntu:24.04',
|
||||
ssh_host: 'build.example.com',
|
||||
ssh_user: 'deploy',
|
||||
@@ -79,6 +83,8 @@ test('Hermes 终端执行配置读取会回显 YAML 字段', () => {
|
||||
assert.equal(values.terminalDockerForwardEnv, 'GITHUB_TOKEN\nNPM_TOKEN')
|
||||
assert.equal(values.terminalSingularityImage, 'docker://nikolaik/python-nodejs:python3.11-nodejs20')
|
||||
assert.equal(values.terminalModalImage, 'python:3.12')
|
||||
assert.equal(values.terminalModalMode, 'managed')
|
||||
assert.equal(values.terminalVercelRuntime, 'python3.13')
|
||||
assert.equal(values.terminalDaytonaImage, 'ubuntu:24.04')
|
||||
assert.equal(values.terminalSshHost, 'build.example.com')
|
||||
assert.equal(values.terminalSshUser, 'deploy')
|
||||
@@ -117,6 +123,8 @@ test('Hermes 终端执行配置保存会保留未知字段并写入上游结构'
|
||||
terminalDockerForwardEnv: 'GITHUB_TOKEN\nNPM_TOKEN\nGITHUB_TOKEN',
|
||||
terminalSingularityImage: 'docker://ubuntu:24.04',
|
||||
terminalModalImage: 'debian:bookworm',
|
||||
terminalModalMode: 'direct',
|
||||
terminalVercelRuntime: 'node22',
|
||||
terminalDaytonaImage: 'ubuntu:22.04',
|
||||
terminalSshHost: 'ssh.example.com',
|
||||
terminalSshUser: 'hermes',
|
||||
@@ -143,6 +151,8 @@ test('Hermes 终端执行配置保存会保留未知字段并写入上游结构'
|
||||
assert.equal(next.terminal.docker_image, 'nikolaik/python-nodejs:python3.12-nodejs22')
|
||||
assert.equal(next.terminal.singularity_image, 'docker://ubuntu:24.04')
|
||||
assert.equal(next.terminal.modal_image, 'debian:bookworm')
|
||||
assert.equal(next.terminal.modal_mode, 'direct')
|
||||
assert.equal(next.terminal.vercel_runtime, 'node22')
|
||||
assert.equal(next.terminal.daytona_image, 'ubuntu:22.04')
|
||||
assert.equal(next.terminal.ssh_host, 'ssh.example.com')
|
||||
assert.equal(next.terminal.ssh_user, 'hermes')
|
||||
@@ -249,6 +259,14 @@ test('Hermes 终端执行配置保存会拒绝非法后端和越界值', () => {
|
||||
() => mergeHermesTerminalConfig({}, { terminalBackend: 'unsafe' }),
|
||||
/terminal\.backend/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesTerminalConfig({}, { terminalModalMode: 'unsafe' }),
|
||||
/terminal\.modal_mode/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesTerminalConfig({}, { terminalVercelRuntime: 'ruby' }),
|
||||
/terminal\.vercel_runtime/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesTerminalConfig({}, { terminalTimeout: '0' }),
|
||||
/terminal\.timeout/,
|
||||
|
||||
Reference in New Issue
Block a user