mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): add terminal shell environment controls
This commit is contained in:
@@ -3594,6 +3594,25 @@ function normalizeHermesEnvNameList(value, key) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeHermesShellInitFileList(value, key) {
|
||||
const seen = new Set()
|
||||
const normalized = []
|
||||
for (const item of normalizeHermesMultilineList(value)) {
|
||||
const path = String(item ?? '').trim()
|
||||
if (!path || /[\u0000-\u001f\u007f]/.test(path) || /\s/.test(path)) {
|
||||
throw new Error(`${key} 每行只能填写一个 shell 初始化文件路径,路径不能包含空白字符`)
|
||||
}
|
||||
if (!/^[~$%{}A-Za-z0-9_./:\\-]+$/.test(path)) {
|
||||
throw new Error(`${key} 只能包含路径字符、~、环境变量占位、点、斜杠、冒号和短横线`)
|
||||
}
|
||||
if (!seen.has(path)) {
|
||||
seen.add(path)
|
||||
normalized.push(path)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeHermesAuxiliaryProvider(value, key, strict = false) {
|
||||
const provider = String(value ?? '').trim().toLowerCase() || 'auto'
|
||||
if (HERMES_AUXILIARY_PROVIDERS.has(provider)) return provider
|
||||
@@ -5498,6 +5517,9 @@ export function buildHermesTerminalConfigValues(config = {}) {
|
||||
terminalCwd: typeof terminal.cwd === 'string' && terminal.cwd.trim() ? terminal.cwd : '.',
|
||||
terminalTimeout: parseHermesInteger(terminal.timeout, 'terminal.timeout', 180, 1, 86400, false),
|
||||
terminalLifetimeSeconds: parseHermesInteger(terminal.lifetime_seconds, 'terminal.lifetime_seconds', 300, 0, 86400, false),
|
||||
terminalShellInitFiles: normalizeHermesShellInitFileList(terminal.shell_init_files || [], 'terminal.shell_init_files').join('\n'),
|
||||
terminalAutoSourceBashrc: readHermesBool(terminal.auto_source_bashrc, true),
|
||||
terminalPersistentShell: readHermesBool(terminal.persistent_shell, true),
|
||||
terminalDockerMountCwdToWorkspace: readHermesBool(terminal.docker_mount_cwd_to_workspace, false),
|
||||
terminalDockerRunAsHostUser: readHermesBool(terminal.docker_run_as_host_user, false),
|
||||
terminalDockerImage: typeof terminal.docker_image === 'string' ? terminal.docker_image.trim() : '',
|
||||
@@ -5526,6 +5548,11 @@ export function mergeHermesTerminalConfig(config = {}, form = {}) {
|
||||
terminal.cwd = String(Object.hasOwn(form, 'terminalCwd') ? form.terminalCwd : currentValues.terminalCwd).trim() || '.'
|
||||
terminal.timeout = parseHermesInteger(Object.hasOwn(form, 'terminalTimeout') ? form.terminalTimeout : currentValues.terminalTimeout, 'terminal.timeout', 180, 1, 86400, true)
|
||||
terminal.lifetime_seconds = parseHermesInteger(Object.hasOwn(form, 'terminalLifetimeSeconds') ? form.terminalLifetimeSeconds : currentValues.terminalLifetimeSeconds, 'terminal.lifetime_seconds', 300, 0, 86400, true)
|
||||
const shellInitFiles = normalizeHermesShellInitFileList(Object.hasOwn(form, 'terminalShellInitFiles') ? form.terminalShellInitFiles : currentValues.terminalShellInitFiles, 'terminal.shell_init_files')
|
||||
if (shellInitFiles.length) terminal.shell_init_files = shellInitFiles
|
||||
else delete terminal.shell_init_files
|
||||
terminal.auto_source_bashrc = formHermesBool(form, 'terminalAutoSourceBashrc', currentValues.terminalAutoSourceBashrc)
|
||||
terminal.persistent_shell = formHermesBool(form, 'terminalPersistentShell', currentValues.terminalPersistentShell)
|
||||
terminal.docker_mount_cwd_to_workspace = formHermesBool(form, 'terminalDockerMountCwdToWorkspace', currentValues.terminalDockerMountCwdToWorkspace)
|
||||
terminal.docker_run_as_host_user = formHermesBool(form, 'terminalDockerRunAsHostUser', currentValues.terminalDockerRunAsHostUser)
|
||||
for (const [formKey, yamlKey] of [
|
||||
|
||||
@@ -2404,6 +2404,39 @@ fn normalize_hermes_env_name_list(raw: Option<String>, key: &str) -> Result<Vec<
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
fn normalize_hermes_shell_init_file_list(
|
||||
raw: Option<String>,
|
||||
key: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let mut values = Vec::new();
|
||||
for item in normalize_hermes_multiline_list(raw) {
|
||||
let path = item.trim().to_string();
|
||||
if path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if path.chars().any(|ch| ch.is_control() || ch.is_whitespace()) {
|
||||
return Err(format!(
|
||||
"{key} 每行只能填写一个 shell 初始化文件路径,路径不能包含空白字符"
|
||||
));
|
||||
}
|
||||
if !path.chars().all(|ch| {
|
||||
ch.is_ascii_alphanumeric()
|
||||
|| matches!(
|
||||
ch,
|
||||
'~' | '$' | '%' | '{' | '}' | '_' | '.' | '/' | '\\' | ':' | '-'
|
||||
)
|
||||
}) {
|
||||
return Err(format!(
|
||||
"{key} 只能包含路径字符、~、环境变量占位、点、斜杠、冒号和短横线"
|
||||
));
|
||||
}
|
||||
if !values.contains(&path) {
|
||||
values.push(path);
|
||||
}
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
fn normalize_hermes_auxiliary_provider(
|
||||
value: Option<String>,
|
||||
key: &str,
|
||||
@@ -8604,6 +8637,15 @@ fn build_hermes_terminal_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let terminal_lifetime_seconds = terminal
|
||||
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "lifetime_seconds"), 300, 0, 86400))
|
||||
.unwrap_or(300);
|
||||
let terminal_shell_init_files = terminal
|
||||
.map(|map| yaml_string_sequence_field(map, "shell_init_files").join("\n"))
|
||||
.unwrap_or_default();
|
||||
let terminal_auto_source_bashrc = terminal
|
||||
.and_then(|map| yaml_bool_field(map, "auto_source_bashrc"))
|
||||
.unwrap_or(true);
|
||||
let terminal_persistent_shell = terminal
|
||||
.and_then(|map| yaml_bool_field(map, "persistent_shell"))
|
||||
.unwrap_or(true);
|
||||
let terminal_docker_mount_cwd_to_workspace = terminal
|
||||
.and_then(|map| yaml_bool_field(map, "docker_mount_cwd_to_workspace"))
|
||||
.unwrap_or(false);
|
||||
@@ -8641,6 +8683,9 @@ fn build_hermes_terminal_config_values(config: &serde_yaml::Value) -> Value {
|
||||
"terminalCwd": terminal_cwd,
|
||||
"terminalTimeout": terminal_timeout,
|
||||
"terminalLifetimeSeconds": terminal_lifetime_seconds,
|
||||
"terminalShellInitFiles": terminal_shell_init_files,
|
||||
"terminalAutoSourceBashrc": terminal_auto_source_bashrc,
|
||||
"terminalPersistentShell": terminal_persistent_shell,
|
||||
"terminalDockerMountCwdToWorkspace": terminal_docker_mount_cwd_to_workspace,
|
||||
"terminalDockerRunAsHostUser": terminal_docker_run_as_host_user,
|
||||
"terminalDockerImage": terminal_docker_image,
|
||||
@@ -8707,6 +8752,22 @@ fn merge_hermes_terminal_config(
|
||||
0,
|
||||
86400,
|
||||
)?;
|
||||
let terminal_shell_init_files = normalize_hermes_shell_init_file_list(
|
||||
form_string(form, "terminalShellInitFiles").or_else(|| {
|
||||
current["terminalShellInitFiles"]
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
}),
|
||||
"terminal.shell_init_files",
|
||||
)?;
|
||||
let terminal_auto_source_bashrc =
|
||||
form_bool(form, "terminalAutoSourceBashrc").unwrap_or_else(|| {
|
||||
current["terminalAutoSourceBashrc"]
|
||||
.as_bool()
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let terminal_persistent_shell = form_bool(form, "terminalPersistentShell")
|
||||
.unwrap_or_else(|| current["terminalPersistentShell"].as_bool().unwrap_or(true));
|
||||
let terminal_docker_mount_cwd_to_workspace =
|
||||
form_bool(form, "terminalDockerMountCwdToWorkspace").unwrap_or_else(|| {
|
||||
current["terminalDockerMountCwdToWorkspace"]
|
||||
@@ -8844,6 +8905,27 @@ fn merge_hermes_terminal_config(
|
||||
yaml_key("lifetime_seconds"),
|
||||
serde_yaml::Value::Number(terminal_lifetime_seconds.into()),
|
||||
);
|
||||
if terminal_shell_init_files.is_empty() {
|
||||
terminal.remove(yaml_key("shell_init_files"));
|
||||
} else {
|
||||
terminal.insert(
|
||||
yaml_key("shell_init_files"),
|
||||
serde_yaml::Value::Sequence(
|
||||
terminal_shell_init_files
|
||||
.into_iter()
|
||||
.map(serde_yaml::Value::String)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
terminal.insert(
|
||||
yaml_key("auto_source_bashrc"),
|
||||
serde_yaml::Value::Bool(terminal_auto_source_bashrc),
|
||||
);
|
||||
terminal.insert(
|
||||
yaml_key("persistent_shell"),
|
||||
serde_yaml::Value::Bool(terminal_persistent_shell),
|
||||
);
|
||||
terminal.insert(
|
||||
yaml_key("docker_mount_cwd_to_workspace"),
|
||||
serde_yaml::Value::Bool(terminal_docker_mount_cwd_to_workspace),
|
||||
@@ -17764,6 +17846,9 @@ mod hermes_terminal_config_tests {
|
||||
assert_eq!(values["terminalCwd"], ".");
|
||||
assert_eq!(values["terminalTimeout"], 180);
|
||||
assert_eq!(values["terminalLifetimeSeconds"], 300);
|
||||
assert_eq!(values["terminalShellInitFiles"], "");
|
||||
assert_eq!(values["terminalAutoSourceBashrc"], true);
|
||||
assert_eq!(values["terminalPersistentShell"], true);
|
||||
assert_eq!(values["terminalDockerMountCwdToWorkspace"], false);
|
||||
assert_eq!(values["terminalDockerRunAsHostUser"], false);
|
||||
assert_eq!(values["terminalContainerCpu"], 1);
|
||||
@@ -17790,6 +17875,11 @@ terminal:
|
||||
cwd: /workspace
|
||||
timeout: 600
|
||||
lifetime_seconds: 1800
|
||||
shell_init_files:
|
||||
- ~/.zshrc
|
||||
- ${HOME}/.config/hermes/env.sh
|
||||
auto_source_bashrc: false
|
||||
persistent_shell: false
|
||||
docker_mount_cwd_to_workspace: true
|
||||
docker_run_as_host_user: true
|
||||
docker_image: nikolaik/python-nodejs:python3.11-nodejs20
|
||||
@@ -17815,6 +17905,12 @@ terminal:
|
||||
assert_eq!(values["terminalCwd"], "/workspace");
|
||||
assert_eq!(values["terminalTimeout"], 600);
|
||||
assert_eq!(values["terminalLifetimeSeconds"], 1800);
|
||||
assert_eq!(
|
||||
values["terminalShellInitFiles"],
|
||||
"~/.zshrc\n${HOME}/.config/hermes/env.sh"
|
||||
);
|
||||
assert_eq!(values["terminalAutoSourceBashrc"], false);
|
||||
assert_eq!(values["terminalPersistentShell"], false);
|
||||
assert_eq!(values["terminalDockerMountCwdToWorkspace"], true);
|
||||
assert_eq!(values["terminalDockerRunAsHostUser"], true);
|
||||
assert_eq!(
|
||||
@@ -17849,6 +17945,8 @@ model:
|
||||
provider: anthropic
|
||||
terminal:
|
||||
backend: local
|
||||
shell_init_files:
|
||||
- ~/.profile
|
||||
docker_image: custom/python-node
|
||||
docker_forward_env:
|
||||
- OLD_TOKEN
|
||||
@@ -17866,6 +17964,9 @@ streaming:
|
||||
"terminalCwd": "/workspace",
|
||||
"terminalTimeout": "900",
|
||||
"terminalLifetimeSeconds": "1200",
|
||||
"terminalShellInitFiles": "~/.zshrc\n${HOME}/.config/hermes/env.sh\n~/.zshrc",
|
||||
"terminalAutoSourceBashrc": false,
|
||||
"terminalPersistentShell": false,
|
||||
"terminalDockerMountCwdToWorkspace": true,
|
||||
"terminalDockerRunAsHostUser": true,
|
||||
"terminalDockerImage": "nikolaik/python-nodejs:python3.12-nodejs22",
|
||||
@@ -17891,6 +17992,29 @@ streaming:
|
||||
assert_eq!(config["terminal"]["cwd"].as_str(), Some("/workspace"));
|
||||
assert_eq!(config["terminal"]["timeout"].as_i64(), Some(900));
|
||||
assert_eq!(config["terminal"]["lifetime_seconds"].as_i64(), Some(1200));
|
||||
assert_eq!(
|
||||
config["terminal"]["shell_init_files"][0].as_str(),
|
||||
Some("~/.zshrc")
|
||||
);
|
||||
assert_eq!(
|
||||
config["terminal"]["shell_init_files"][1].as_str(),
|
||||
Some("${HOME}/.config/hermes/env.sh")
|
||||
);
|
||||
assert_eq!(
|
||||
config["terminal"]["shell_init_files"]
|
||||
.as_sequence()
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
config["terminal"]["auto_source_bashrc"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
config["terminal"]["persistent_shell"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
config["terminal"]["docker_mount_cwd_to_workspace"].as_bool(),
|
||||
Some(true)
|
||||
@@ -17980,6 +18104,33 @@ terminal:
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_terminal_config_removes_empty_shell_init_files() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
terminal:
|
||||
shell_init_files:
|
||||
- ~/.bashrc
|
||||
custom_flag: keep-terminal
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_hermes_terminal_config(
|
||||
&mut config,
|
||||
&json!({
|
||||
"terminalShellInitFiles": " \n",
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(config["terminal"]["shell_init_files"].is_null());
|
||||
assert_eq!(
|
||||
config["terminal"]["custom_flag"].as_str(),
|
||||
Some("keep-terminal")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_terminal_config_removes_empty_images() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
@@ -18083,6 +18234,12 @@ terminal:
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("terminal.docker_forward_env"));
|
||||
let err = merge_hermes_terminal_config(
|
||||
&mut config,
|
||||
&json!({ "terminalShellInitFiles": "valid.sh\nbad path.sh" }),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("terminal.shell_init_files"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,6 +308,9 @@ const TERMINAL_DEFAULTS = {
|
||||
terminalCwd: '.',
|
||||
terminalTimeout: 180,
|
||||
terminalLifetimeSeconds: 300,
|
||||
terminalShellInitFiles: '',
|
||||
terminalAutoSourceBashrc: true,
|
||||
terminalPersistentShell: true,
|
||||
terminalDockerMountCwdToWorkspace: false,
|
||||
terminalDockerRunAsHostUser: false,
|
||||
terminalDockerImage: '',
|
||||
@@ -2302,8 +2305,20 @@ export function render() {
|
||||
<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>
|
||||
<label class="hm-field hm-field--wide">
|
||||
<span class="hm-field-label">${t('engine.hermesTerminalConfigShellInitFiles')}</span>
|
||||
<textarea id="hm-terminal-shell-init-files" class="hm-input hm-textarea" rows="3" placeholder="~/.bashrc \${HOME}/.config/hermes/env.sh" ${disabled ? 'disabled' : ''}>${esc(terminalValues.terminalShellInitFiles)}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-config-check-grid">
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-terminal-auto-source-bashrc" type="checkbox" ${terminalValues.terminalAutoSourceBashrc ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesTerminalConfigAutoSourceBashrc')}</span>
|
||||
</label>
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-terminal-persistent-shell" type="checkbox" ${terminalValues.terminalPersistentShell ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesTerminalConfigPersistentShell')}</span>
|
||||
</label>
|
||||
<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>
|
||||
@@ -4250,6 +4265,9 @@ export function render() {
|
||||
terminalCwd: el.querySelector('#hm-terminal-cwd')?.value || '.',
|
||||
terminalTimeout: el.querySelector('#hm-terminal-timeout')?.value || '180',
|
||||
terminalLifetimeSeconds: el.querySelector('#hm-terminal-lifetime-seconds')?.value || '300',
|
||||
terminalShellInitFiles: el.querySelector('#hm-terminal-shell-init-files')?.value || '',
|
||||
terminalAutoSourceBashrc: !!el.querySelector('#hm-terminal-auto-source-bashrc')?.checked,
|
||||
terminalPersistentShell: !!el.querySelector('#hm-terminal-persistent-shell')?.checked,
|
||||
terminalDockerMountCwdToWorkspace: !!el.querySelector('#hm-terminal-docker-mount-cwd-to-workspace')?.checked,
|
||||
terminalDockerRunAsHostUser: !!el.querySelector('#hm-terminal-docker-run-as-host-user')?.checked,
|
||||
terminalDockerImage: el.querySelector('#hm-terminal-docker-image')?.value || '',
|
||||
|
||||
@@ -540,6 +540,9 @@ export default {
|
||||
hermesTerminalConfigCwd: _('工作目录', 'Working directory', '工作目錄'),
|
||||
hermesTerminalConfigTimeout: _('命令超时秒数', 'Command timeout seconds', '命令逾時秒數'),
|
||||
hermesTerminalConfigLifetimeSeconds: _('沙箱生命周期秒数', 'Sandbox lifetime seconds', '沙箱生命週期秒數'),
|
||||
hermesTerminalConfigShellInitFiles: _('Shell 初始化文件', 'Shell init files', 'Shell 初始化檔案'),
|
||||
hermesTerminalConfigAutoSourceBashrc: _('自动读取常见 Shell 启动文件', 'Auto-source common shell startup files', '自動讀取常見 Shell 啟動檔案'),
|
||||
hermesTerminalConfigPersistentShell: _('远程/沙箱后端保持持久 Shell', 'Keep persistent shell for remote and sandbox backends', '遠端/沙箱後端保持持久 Shell'),
|
||||
hermesTerminalConfigDockerMountCwd: _('Docker 挂载启动目录到 /workspace', 'Mount launch cwd into Docker /workspace', 'Docker 掛載啟動目錄到 /workspace'),
|
||||
hermesTerminalConfigDockerRunAsHostUser: _('Docker 使用宿主用户运行', 'Run Docker as host user', 'Docker 使用宿主使用者執行'),
|
||||
hermesTerminalConfigContainerPersistent: _('容器文件系统持久化', 'Persist container filesystem', '容器檔案系統持久化'),
|
||||
@@ -557,7 +560,7 @@ export default {
|
||||
hermesTerminalConfigContainerCpu: _('CPU 核数', 'CPU cores', 'CPU 核心數'),
|
||||
hermesTerminalConfigContainerMemory: _('内存 MB', 'Memory MB', '記憶體 MB'),
|
||||
hermesTerminalConfigContainerDisk: _('磁盘 MB', 'Disk MB', '磁碟 MB'),
|
||||
hermesTerminalConfigFootnote: _('SSH 字段只在 SSH 后端生效,留空会移除主机、用户和密钥覆盖;面板不保存 SSH 密码。镜像字段只在对应 Docker、Singularity、Modal 或 Daytona 后端生效;留空会移除覆盖并使用 Hermes 默认值。Docker 环境变量转发只保存变量名,不保存密钥值;每行一个变量名,留空会移除覆盖。Docker 挂载启动目录会把宿主目录暴露给沙箱,仅在可信项目和无人值守任务中开启。', '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. Docker env forwarding stores variable names only, not secret values; use one name per line, or leave blank to remove the override. Mounting the launch cwd exposes host files to the sandbox; enable it only for trusted projects or unattended jobs.', 'SSH 欄位只在 SSH 後端生效,留空會移除主機、使用者和金鑰覆蓋;面板不儲存 SSH 密碼。映像欄位只在對應 Docker、Singularity、Modal 或 Daytona 後端生效;留空會移除覆蓋並使用 Hermes 預設值。Docker 環境變數轉發只儲存變數名,不儲存密鑰值;每行一個變數名,留空會移除覆蓋。Docker 掛載啟動目錄會把宿主目錄暴露給沙箱,僅在可信專案和無人值守任務中開啟。'),
|
||||
hermesTerminalConfigFootnote: _('Shell 初始化文件每行一个路径,支持 ~ 和 ${VAR} 占位;留空会移除 shell_init_files,让 Hermes 使用自动读取默认启动文件的行为。关闭自动读取可避免有问题的 rc 文件在非交互环境中退出。SSH 字段只在 SSH 后端生效,留空会移除主机、用户和密钥覆盖;面板不保存 SSH 密码。镜像字段只在对应 Docker、Singularity、Modal 或 Daytona 后端生效;留空会移除覆盖并使用 Hermes 默认值。Docker 环境变量转发只保存变量名,不保存密钥值;每行一个变量名,留空会移除覆盖。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. 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. Docker env forwarding stores variable names only, not secret values; use one name per line, or leave blank to remove the override. 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 使用自動讀取預設啟動檔案的行為。關閉自動讀取可避免有問題的 rc 檔案在非互動環境中退出。SSH 欄位只在 SSH 後端生效,留空會移除主機、使用者和金鑰覆蓋;面板不儲存 SSH 密碼。映像欄位只在對應 Docker、Singularity、Modal 或 Daytona 後端生效;留空會移除覆蓋並使用 Hermes 預設值。Docker 環境變數轉發只儲存變數名,不儲存密鑰值;每行一個變數名,留空會移除覆蓋。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', '結構化設定'),
|
||||
|
||||
@@ -438,6 +438,9 @@ test('Hermes 配置页会暴露终端执行结构化配置字段', () => {
|
||||
'hm-terminal-cwd',
|
||||
'hm-terminal-timeout',
|
||||
'hm-terminal-lifetime-seconds',
|
||||
'hm-terminal-shell-init-files',
|
||||
'hm-terminal-auto-source-bashrc',
|
||||
'hm-terminal-persistent-shell',
|
||||
'hm-terminal-docker-mount-cwd-to-workspace',
|
||||
'hm-terminal-docker-run-as-host-user',
|
||||
'hm-terminal-docker-image',
|
||||
|
||||
@@ -14,6 +14,9 @@ test('Hermes 终端执行配置读取会提供上游默认值', () => {
|
||||
terminalCwd: '.',
|
||||
terminalTimeout: 180,
|
||||
terminalLifetimeSeconds: 300,
|
||||
terminalShellInitFiles: '',
|
||||
terminalAutoSourceBashrc: true,
|
||||
terminalPersistentShell: true,
|
||||
terminalDockerMountCwdToWorkspace: false,
|
||||
terminalDockerRunAsHostUser: false,
|
||||
terminalContainerCpu: 1,
|
||||
@@ -39,6 +42,9 @@ test('Hermes 终端执行配置读取会回显 YAML 字段', () => {
|
||||
cwd: '/workspace',
|
||||
timeout: 600,
|
||||
lifetime_seconds: 1800,
|
||||
shell_init_files: ['~/.zshrc', '${HOME}/.config/hermes/env.sh'],
|
||||
auto_source_bashrc: false,
|
||||
persistent_shell: false,
|
||||
docker_mount_cwd_to_workspace: true,
|
||||
docker_run_as_host_user: true,
|
||||
docker_image: 'nikolaik/python-nodejs:python3.11-nodejs20',
|
||||
@@ -61,6 +67,9 @@ test('Hermes 终端执行配置读取会回显 YAML 字段', () => {
|
||||
assert.equal(values.terminalCwd, '/workspace')
|
||||
assert.equal(values.terminalTimeout, 600)
|
||||
assert.equal(values.terminalLifetimeSeconds, 1800)
|
||||
assert.equal(values.terminalShellInitFiles, '~/.zshrc\n${HOME}/.config/hermes/env.sh')
|
||||
assert.equal(values.terminalAutoSourceBashrc, false)
|
||||
assert.equal(values.terminalPersistentShell, false)
|
||||
assert.equal(values.terminalDockerMountCwdToWorkspace, true)
|
||||
assert.equal(values.terminalDockerRunAsHostUser, true)
|
||||
assert.equal(values.terminalDockerImage, 'nikolaik/python-nodejs:python3.11-nodejs20')
|
||||
@@ -83,6 +92,7 @@ test('Hermes 终端执行配置保存会保留未知字段并写入上游结构'
|
||||
model: { provider: 'anthropic' },
|
||||
terminal: {
|
||||
backend: 'local',
|
||||
shell_init_files: ['~/.profile'],
|
||||
docker_image: 'custom/python-node',
|
||||
docker_forward_env: ['OLD_TOKEN'],
|
||||
custom_flag: 'keep-terminal',
|
||||
@@ -93,6 +103,9 @@ test('Hermes 终端执行配置保存会保留未知字段并写入上游结构'
|
||||
terminalCwd: '/workspace',
|
||||
terminalTimeout: '900',
|
||||
terminalLifetimeSeconds: '1200',
|
||||
terminalShellInitFiles: '~/.zshrc\n${HOME}/.config/hermes/env.sh\n~/.zshrc',
|
||||
terminalAutoSourceBashrc: false,
|
||||
terminalPersistentShell: false,
|
||||
terminalDockerMountCwdToWorkspace: true,
|
||||
terminalDockerRunAsHostUser: true,
|
||||
terminalDockerImage: 'nikolaik/python-nodejs:python3.12-nodejs22',
|
||||
@@ -116,6 +129,9 @@ test('Hermes 终端执行配置保存会保留未知字段并写入上游结构'
|
||||
assert.equal(next.terminal.cwd, '/workspace')
|
||||
assert.equal(next.terminal.timeout, 900)
|
||||
assert.equal(next.terminal.lifetime_seconds, 1200)
|
||||
assert.deepEqual(next.terminal.shell_init_files, ['~/.zshrc', '${HOME}/.config/hermes/env.sh'])
|
||||
assert.equal(next.terminal.auto_source_bashrc, false)
|
||||
assert.equal(next.terminal.persistent_shell, false)
|
||||
assert.equal(next.terminal.docker_mount_cwd_to_workspace, true)
|
||||
assert.equal(next.terminal.docker_run_as_host_user, true)
|
||||
assert.equal(next.terminal.docker_image, 'nikolaik/python-nodejs:python3.12-nodejs22')
|
||||
@@ -148,6 +164,20 @@ test('Hermes 终端执行配置保存空 Docker 环境变量转发会删除对
|
||||
assert.equal(next.terminal.custom_flag, 'keep-terminal')
|
||||
})
|
||||
|
||||
test('Hermes 终端执行配置保存空 Shell 初始化文件会删除对应字段', () => {
|
||||
const next = mergeHermesTerminalConfig({
|
||||
terminal: {
|
||||
shell_init_files: ['~/.bashrc'],
|
||||
custom_flag: 'keep-terminal',
|
||||
},
|
||||
}, {
|
||||
terminalShellInitFiles: ' \n',
|
||||
})
|
||||
|
||||
assert.equal(Object.hasOwn(next.terminal, 'shell_init_files'), false)
|
||||
assert.equal(next.terminal.custom_flag, 'keep-terminal')
|
||||
})
|
||||
|
||||
test('Hermes 终端执行配置保存空镜像会删除对应字段', () => {
|
||||
const next = mergeHermesTerminalConfig({
|
||||
terminal: {
|
||||
@@ -227,4 +257,8 @@ test('Hermes 终端执行配置保存会拒绝非法后端和越界值', () => {
|
||||
() => mergeHermesTerminalConfig({}, { terminalDockerForwardEnv: 'GOOD_TOKEN\nBAD TOKEN' }),
|
||||
/terminal\.docker_forward_env/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesTerminalConfig({}, { terminalShellInitFiles: 'valid.sh\nbad path.sh' }),
|
||||
/terminal\.shell_init_files/,
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user