mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): add browser tool settings
This commit is contained in:
@@ -3324,6 +3324,7 @@ 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_BROWSER_ENGINES = new Set(['auto', 'lightpanda', 'chrome'])
|
||||
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose'])
|
||||
const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false'])
|
||||
const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal'])
|
||||
@@ -3401,6 +3402,13 @@ function normalizeHermesTerminalBackend(value, strict = false) {
|
||||
return 'local'
|
||||
}
|
||||
|
||||
function normalizeHermesBrowserEngine(value, strict = false) {
|
||||
const engine = String(value ?? '').trim().toLowerCase() || 'auto'
|
||||
if (HERMES_BROWSER_ENGINES.has(engine)) return engine
|
||||
if (strict) throw new Error('browser.engine 必须是 auto、lightpanda 或 chrome')
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
function normalizeHermesDisplayToolProgress(value, strict = false, key = 'display.tool_progress') {
|
||||
const progress = String(value ?? '').trim().toLowerCase() || 'all'
|
||||
if (HERMES_DISPLAY_TOOL_PROGRESS_VALUES.has(progress)) return progress
|
||||
@@ -3943,6 +3951,33 @@ export function mergeHermesPrivacyConfig(config = {}, form = {}) {
|
||||
return next
|
||||
}
|
||||
|
||||
export function buildHermesBrowserConfigValues(config = {}) {
|
||||
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
||||
const browser = root.browser && typeof root.browser === 'object' && !Array.isArray(root.browser)
|
||||
? root.browser
|
||||
: {}
|
||||
return {
|
||||
browserInactivityTimeout: parseHermesInteger(browser.inactivity_timeout, 'browser.inactivity_timeout', 120, 1, 86400, false),
|
||||
browserCommandTimeout: parseHermesInteger(browser.command_timeout, 'browser.command_timeout', 30, 5, 3600, false),
|
||||
browserRecordSessions: readHermesBool(browser.record_sessions, false),
|
||||
browserEngine: normalizeHermesBrowserEngine(browser.engine, false),
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeHermesBrowserConfig(config = {}, form = {}) {
|
||||
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
|
||||
const currentValues = buildHermesBrowserConfigValues(next)
|
||||
const browser = next.browser && typeof next.browser === 'object' && !Array.isArray(next.browser)
|
||||
? mergeConfigsPreservingFields(next.browser, {})
|
||||
: {}
|
||||
browser.inactivity_timeout = parseHermesInteger(Object.hasOwn(form, 'browserInactivityTimeout') ? form.browserInactivityTimeout : currentValues.browserInactivityTimeout, 'browser.inactivity_timeout', 120, 1, 86400, true)
|
||||
browser.command_timeout = parseHermesInteger(Object.hasOwn(form, 'browserCommandTimeout') ? form.browserCommandTimeout : currentValues.browserCommandTimeout, 'browser.command_timeout', 30, 5, 3600, true)
|
||||
browser.record_sessions = formHermesBool(form, 'browserRecordSessions', currentValues.browserRecordSessions)
|
||||
browser.engine = normalizeHermesBrowserEngine(Object.hasOwn(form, 'browserEngine') ? form.browserEngine : currentValues.browserEngine, true)
|
||||
next.browser = browser
|
||||
return next
|
||||
}
|
||||
|
||||
export function buildHermesTerminalConfigValues(config = {}) {
|
||||
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
||||
const terminal = root.terminal && typeof root.terminal === 'object' && !Array.isArray(root.terminal)
|
||||
@@ -10566,6 +10601,27 @@ const handlers = {
|
||||
}
|
||||
},
|
||||
|
||||
hermes_browser_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
exists,
|
||||
configPath,
|
||||
values: buildHermesBrowserConfigValues(config),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_browser_config_save({ form } = {}) {
|
||||
const { configPath, config } = readHermesConfigYamlObject()
|
||||
const next = mergeHermesBrowserConfig(config, form || {})
|
||||
const backup = writeHermesConfigYamlObject(configPath, next)
|
||||
return {
|
||||
ok: true,
|
||||
configPath,
|
||||
backup,
|
||||
values: buildHermesBrowserConfigValues(next),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_terminal_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
|
||||
@@ -4391,6 +4391,23 @@ fn normalize_hermes_terminal_backend(
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
"auto".to_string()
|
||||
} else {
|
||||
engine
|
||||
};
|
||||
if matches!(engine.as_str(), "auto" | "lightpanda" | "chrome") {
|
||||
return Ok(engine);
|
||||
}
|
||||
if strict {
|
||||
Err("browser.engine 必须是 auto、lightpanda 或 chrome".to_string())
|
||||
} else {
|
||||
Ok("auto".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn hermes_streaming_config_source(config: &serde_yaml::Value) -> Option<&serde_yaml::Mapping> {
|
||||
let root = config.as_mapping()?;
|
||||
if let Some(streaming) = yaml_get_mapping(root, "streaming") {
|
||||
@@ -4690,6 +4707,88 @@ fn merge_hermes_privacy_config(config: &mut serde_yaml::Value, form: &Value) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_hermes_browser_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let root = config.as_mapping();
|
||||
let browser = root.and_then(|map| yaml_get_mapping(map, "browser"));
|
||||
let browser_inactivity_timeout = browser
|
||||
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "inactivity_timeout"), 120, 1, 86400))
|
||||
.unwrap_or(120);
|
||||
let browser_command_timeout = browser
|
||||
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "command_timeout"), 30, 5, 3600))
|
||||
.unwrap_or(30);
|
||||
let browser_record_sessions = browser
|
||||
.and_then(|map| yaml_bool_field(map, "record_sessions"))
|
||||
.unwrap_or(false);
|
||||
let browser_engine = normalize_hermes_browser_engine(
|
||||
browser.and_then(|map| yaml_string_field(map, "engine")),
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|_| "auto".to_string());
|
||||
|
||||
serde_json::json!({
|
||||
"browserInactivityTimeout": browser_inactivity_timeout,
|
||||
"browserCommandTimeout": browser_command_timeout,
|
||||
"browserRecordSessions": browser_record_sessions,
|
||||
"browserEngine": browser_engine,
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_hermes_browser_config(config: &mut serde_yaml::Value, form: &Value) -> Result<(), String> {
|
||||
let current = build_hermes_browser_config_values(config);
|
||||
let browser_inactivity_timeout = validate_hermes_i64(
|
||||
if form.get("browserInactivityTimeout").is_some() {
|
||||
form_i64(form, "browserInactivityTimeout")
|
||||
} else {
|
||||
Some(current["browserInactivityTimeout"].as_i64().unwrap_or(120))
|
||||
},
|
||||
"browser.inactivity_timeout",
|
||||
120,
|
||||
1,
|
||||
86400,
|
||||
)?;
|
||||
let browser_command_timeout = validate_hermes_i64(
|
||||
if form.get("browserCommandTimeout").is_some() {
|
||||
form_i64(form, "browserCommandTimeout")
|
||||
} else {
|
||||
Some(current["browserCommandTimeout"].as_i64().unwrap_or(30))
|
||||
},
|
||||
"browser.command_timeout",
|
||||
30,
|
||||
5,
|
||||
3600,
|
||||
)?;
|
||||
let browser_record_sessions = form_bool(form, "browserRecordSessions")
|
||||
.unwrap_or_else(|| current["browserRecordSessions"].as_bool().unwrap_or(false));
|
||||
let browser_engine = normalize_hermes_browser_engine(
|
||||
if form.get("browserEngine").is_some() {
|
||||
form_string(form, "browserEngine")
|
||||
} else {
|
||||
current["browserEngine"].as_str().map(ToString::to_string)
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
|
||||
let root = ensure_yaml_object(config)?;
|
||||
let browser = yaml_child_object(root, "browser")?;
|
||||
browser.insert(
|
||||
yaml_key("inactivity_timeout"),
|
||||
serde_yaml::Value::Number(browser_inactivity_timeout.into()),
|
||||
);
|
||||
browser.insert(
|
||||
yaml_key("command_timeout"),
|
||||
serde_yaml::Value::Number(browser_command_timeout.into()),
|
||||
);
|
||||
browser.insert(
|
||||
yaml_key("record_sessions"),
|
||||
serde_yaml::Value::Bool(browser_record_sessions),
|
||||
);
|
||||
browser.insert(
|
||||
yaml_key("engine"),
|
||||
serde_yaml::Value::String(browser_engine),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_hermes_execution_limits_config(
|
||||
config: &mut serde_yaml::Value,
|
||||
form: &Value,
|
||||
@@ -6161,6 +6260,30 @@ pub fn hermes_privacy_config_save(form: Value) -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_browser_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
ensure_yaml_object(&mut config.clone())?;
|
||||
Ok(serde_json::json!({
|
||||
"exists": exists,
|
||||
"configPath": config_path.to_string_lossy(),
|
||||
"values": build_hermes_browser_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_browser_config_save(form: Value) -> Result<Value, String> {
|
||||
let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?;
|
||||
merge_hermes_browser_config(&mut config, &form)?;
|
||||
let backup = write_hermes_yaml_config(&config_path, &config)?;
|
||||
Ok(serde_json::json!({
|
||||
"ok": true,
|
||||
"configPath": config_path.to_string_lossy(),
|
||||
"backup": backup,
|
||||
"values": build_hermes_browser_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_terminal_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
@@ -11830,6 +11953,108 @@ streaming:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_browser_config_tests {
|
||||
use super::{build_hermes_browser_config_values, merge_hermes_browser_config};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn browser_values_have_upstream_defaults() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap();
|
||||
let values = build_hermes_browser_config_values(&config);
|
||||
assert_eq!(values["browserInactivityTimeout"], 120);
|
||||
assert_eq!(values["browserCommandTimeout"], 30);
|
||||
assert_eq!(values["browserRecordSessions"], false);
|
||||
assert_eq!(values["browserEngine"], "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_values_read_yaml_fields() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
browser:
|
||||
inactivity_timeout: 300
|
||||
command_timeout: 45
|
||||
record_sessions: true
|
||||
engine: lightpanda
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let values = build_hermes_browser_config_values(&config);
|
||||
assert_eq!(values["browserInactivityTimeout"], 300);
|
||||
assert_eq!(values["browserCommandTimeout"], 45);
|
||||
assert_eq!(values["browserRecordSessions"], true);
|
||||
assert_eq!(values["browserEngine"], "lightpanda");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_browser_config_preserves_unknown_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
model:
|
||||
provider: anthropic
|
||||
browser:
|
||||
inactivity_timeout: 120
|
||||
command_timeout: 30
|
||||
record_sessions: false
|
||||
engine: auto
|
||||
cdp_url: ws://127.0.0.1:9222/devtools/browser/demo
|
||||
camofox:
|
||||
managed_persistence: true
|
||||
custom_flag: keep-browser
|
||||
streaming:
|
||||
enabled: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_hermes_browser_config(
|
||||
&mut config,
|
||||
&json!({
|
||||
"browserInactivityTimeout": "180",
|
||||
"browserCommandTimeout": "60",
|
||||
"browserRecordSessions": true,
|
||||
"browserEngine": "chrome",
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config["model"]["provider"].as_str(), Some("anthropic"));
|
||||
assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true));
|
||||
assert_eq!(config["browser"]["inactivity_timeout"].as_i64(), Some(180));
|
||||
assert_eq!(config["browser"]["command_timeout"].as_i64(), Some(60));
|
||||
assert_eq!(config["browser"]["record_sessions"].as_bool(), Some(true));
|
||||
assert_eq!(config["browser"]["engine"].as_str(), Some("chrome"));
|
||||
assert_eq!(
|
||||
config["browser"]["cdp_url"].as_str(),
|
||||
Some("ws://127.0.0.1:9222/devtools/browser/demo")
|
||||
);
|
||||
assert_eq!(
|
||||
config["browser"]["camofox"]["managed_persistence"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["browser"]["custom_flag"].as_str(),
|
||||
Some("keep-browser")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_browser_config_rejects_invalid_values() {
|
||||
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
|
||||
let err = merge_hermes_browser_config(&mut config, &json!({ "browserEngine": "firefox" }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("browser.engine"));
|
||||
let err =
|
||||
merge_hermes_browser_config(&mut config, &json!({ "browserInactivityTimeout": 0 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("browser.inactivity_timeout"));
|
||||
let err = merge_hermes_browser_config(&mut config, &json!({ "browserCommandTimeout": 4 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("browser.command_timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_terminal_config_tests {
|
||||
use super::{build_hermes_terminal_config_values, merge_hermes_terminal_config};
|
||||
|
||||
@@ -285,6 +285,8 @@ pub fn run() {
|
||||
hermes::hermes_io_safety_config_save,
|
||||
hermes::hermes_privacy_config_read,
|
||||
hermes::hermes_privacy_config_save,
|
||||
hermes::hermes_browser_config_read,
|
||||
hermes::hermes_browser_config_save,
|
||||
hermes::hermes_terminal_config_read,
|
||||
hermes::hermes_terminal_config_save,
|
||||
hermes::hermes_lazy_deps_features,
|
||||
|
||||
@@ -113,6 +113,13 @@ const PRIVACY_DEFAULTS = {
|
||||
redactPii: false,
|
||||
}
|
||||
|
||||
const BROWSER_DEFAULTS = {
|
||||
browserInactivityTimeout: 120,
|
||||
browserCommandTimeout: 30,
|
||||
browserRecordSessions: false,
|
||||
browserEngine: 'auto',
|
||||
}
|
||||
|
||||
const TERMINAL_DEFAULTS = {
|
||||
terminalBackend: 'local',
|
||||
terminalCwd: '.',
|
||||
@@ -130,6 +137,7 @@ 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 BROWSER_ENGINES = ['auto', 'lightpanda', 'chrome']
|
||||
const UNAUTHORIZED_DM_BEHAVIORS = ['pair', 'ignore']
|
||||
const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose']
|
||||
const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu']
|
||||
@@ -155,6 +163,7 @@ export function render() {
|
||||
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
|
||||
let ioSafetyValues = { ...IO_SAFETY_DEFAULTS }
|
||||
let privacyValues = { ...PRIVACY_DEFAULTS }
|
||||
let browserValues = { ...BROWSER_DEFAULTS }
|
||||
let terminalValues = { ...TERMINAL_DEFAULTS }
|
||||
let loading = true
|
||||
let runtimeLoading = true
|
||||
@@ -171,6 +180,7 @@ export function render() {
|
||||
let executionLimitsLoading = true
|
||||
let ioSafetyLoading = true
|
||||
let privacyLoading = true
|
||||
let browserLoading = true
|
||||
let terminalLoading = true
|
||||
let saving = false
|
||||
let runtimeSaving = false
|
||||
@@ -187,6 +197,7 @@ export function render() {
|
||||
let executionLimitsSaving = false
|
||||
let ioSafetySaving = false
|
||||
let privacySaving = false
|
||||
let browserSaving = false
|
||||
let terminalSaving = false
|
||||
let error = null
|
||||
let runtimeError = null
|
||||
@@ -203,6 +214,7 @@ export function render() {
|
||||
let executionLimitsError = null
|
||||
let ioSafetyError = null
|
||||
let privacyError = null
|
||||
let browserError = null
|
||||
let terminalError = null
|
||||
|
||||
function esc(value) {
|
||||
@@ -214,7 +226,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function isBusy() {
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || terminalSaving
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving
|
||||
}
|
||||
|
||||
function option(labelKey, value, selected) {
|
||||
@@ -837,7 +849,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function renderPrivacyPanel() {
|
||||
const disabled = loading || saving || privacyLoading || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
|
||||
const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-privacy-panel">
|
||||
<div class="hm-panel-header">
|
||||
@@ -864,8 +876,52 @@ export function render() {
|
||||
`
|
||||
}
|
||||
|
||||
function renderBrowserPanel() {
|
||||
const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-browser-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div>
|
||||
<div class="hm-panel-title">${t('engine.hermesBrowserConfigTitle')}</div>
|
||||
<div class="hm-channel-panel-desc">${t('engine.hermesBrowserConfigDesc')}</div>
|
||||
</div>
|
||||
<div class="hm-panel-actions">
|
||||
<span class="hm-muted">${browserSaving ? t('engine.hermesConfigStatusSaving') : browserLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesBrowserConfigStatusReady')}</span>
|
||||
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-browser-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesBrowserConfigSave')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
${renderError(browserError)}
|
||||
<div class="hm-config-runtime-grid hm-config-browser-grid">
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesBrowserConfigEngine')}</span>
|
||||
<select id="hm-browser-engine" class="hm-input" ${disabled ? 'disabled' : ''}>
|
||||
${BROWSER_ENGINES.map(mode => option(`engine.hermesBrowserConfigEngine_${mode}`, mode, browserValues.browserEngine)).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesBrowserConfigInactivityTimeout')}</span>
|
||||
<input id="hm-browser-inactivity-timeout" class="hm-input" type="number" inputmode="numeric" min="1" max="86400" step="1" value="${esc(browserValues.browserInactivityTimeout)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesBrowserConfigCommandTimeout')}</span>
|
||||
<input id="hm-browser-command-timeout" class="hm-input" type="number" inputmode="numeric" min="5" max="3600" step="1" value="${esc(browserValues.browserCommandTimeout)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-config-check-grid">
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-browser-record-sessions" type="checkbox" ${browserValues.browserRecordSessions ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesBrowserConfigRecordSessions')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-channel-footnote">${t('engine.hermesBrowserConfigFootnote')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderTerminalPanel() {
|
||||
const disabled = loading || saving || terminalLoading || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
|
||||
const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-terminal-panel">
|
||||
<div class="hm-panel-header">
|
||||
@@ -955,6 +1011,7 @@ export function render() {
|
||||
${renderExecutionLimitsPanel()}
|
||||
${renderIoSafetyPanel()}
|
||||
${renderPrivacyPanel()}
|
||||
${renderBrowserPanel()}
|
||||
${renderCompressionPanel()}
|
||||
${renderToolGuardrailsPanel()}
|
||||
${renderMemoryPanel()}
|
||||
@@ -997,6 +1054,7 @@ export function render() {
|
||||
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
|
||||
el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety)
|
||||
el.querySelector('#hm-privacy-save')?.addEventListener('click', savePrivacyConfig)
|
||||
el.querySelector('#hm-browser-save')?.addEventListener('click', saveBrowserConfig)
|
||||
el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal)
|
||||
}
|
||||
|
||||
@@ -1075,6 +1133,11 @@ export function render() {
|
||||
privacyValues = { ...PRIVACY_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadBrowserConfig() {
|
||||
const data = await api.hermesBrowserConfigRead()
|
||||
browserValues = { ...BROWSER_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadTerminal() {
|
||||
const data = await api.hermesTerminalConfigRead()
|
||||
terminalValues = { ...TERMINAL_DEFAULTS, ...(data?.values || {}) }
|
||||
@@ -1096,6 +1159,7 @@ export function render() {
|
||||
executionLimitsLoading = true
|
||||
ioSafetyLoading = true
|
||||
privacyLoading = true
|
||||
browserLoading = true
|
||||
terminalLoading = true
|
||||
error = null
|
||||
runtimeError = null
|
||||
@@ -1112,6 +1176,7 @@ export function render() {
|
||||
executionLimitsError = null
|
||||
ioSafetyError = null
|
||||
privacyError = null
|
||||
browserError = null
|
||||
terminalError = null
|
||||
draw()
|
||||
try {
|
||||
@@ -1177,6 +1242,14 @@ export function render() {
|
||||
privacyLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadBrowserConfig()
|
||||
} catch (err) {
|
||||
browserError = humanizeError(err, t('engine.hermesBrowserConfigLoadFailed') || 'Load browser config failed')
|
||||
} finally {
|
||||
browserLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadTerminal()
|
||||
} catch (err) {
|
||||
@@ -1304,6 +1377,9 @@ export function render() {
|
||||
try {
|
||||
await loadPrivacyConfig()
|
||||
} catch {}
|
||||
try {
|
||||
await loadBrowserConfig()
|
||||
} catch {}
|
||||
try {
|
||||
await loadTerminal()
|
||||
} catch {}
|
||||
@@ -1717,6 +1793,34 @@ export function render() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBrowserConfig() {
|
||||
const form = {
|
||||
browserInactivityTimeout: el.querySelector('#hm-browser-inactivity-timeout')?.value || '120',
|
||||
browserCommandTimeout: el.querySelector('#hm-browser-command-timeout')?.value || '30',
|
||||
browserRecordSessions: !!el.querySelector('#hm-browser-record-sessions')?.checked,
|
||||
browserEngine: el.querySelector('#hm-browser-engine')?.value || 'auto',
|
||||
}
|
||||
browserSaving = true
|
||||
browserError = null
|
||||
draw()
|
||||
try {
|
||||
const result = await api.hermesBrowserConfigSave(form)
|
||||
browserValues = { ...BROWSER_DEFAULTS, ...(result?.values || form) }
|
||||
await refreshRawAfterStructuredSave()
|
||||
const backup = result?.backup || ''
|
||||
toast({
|
||||
message: t('engine.hermesBrowserConfigSaveSuccess'),
|
||||
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
|
||||
}, 'success')
|
||||
} catch (err) {
|
||||
browserError = humanizeError(err, t('engine.hermesBrowserConfigSaveFailed') || 'Save browser config failed')
|
||||
toast(browserError, 'error')
|
||||
} finally {
|
||||
browserSaving = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTerminal() {
|
||||
const form = {
|
||||
terminalBackend: el.querySelector('#hm-terminal-backend')?.value || 'local',
|
||||
|
||||
@@ -537,6 +537,8 @@ export const api = {
|
||||
hermesIoSafetyConfigSave: (form) => invoke('hermes_io_safety_config_save', { form }),
|
||||
hermesPrivacyConfigRead: () => invoke('hermes_privacy_config_read'),
|
||||
hermesPrivacyConfigSave: (form) => invoke('hermes_privacy_config_save', { form }),
|
||||
hermesBrowserConfigRead: () => invoke('hermes_browser_config_read'),
|
||||
hermesBrowserConfigSave: (form) => invoke('hermes_browser_config_save', { form }),
|
||||
hermesTerminalConfigRead: () => invoke('hermes_terminal_config_read'),
|
||||
hermesTerminalConfigSave: (form) => invoke('hermes_terminal_config_save', { form }),
|
||||
hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000),
|
||||
|
||||
@@ -585,6 +585,21 @@ export default {
|
||||
hermesPrivacyConfigSaveFailed: _('保存隐私脱敏配置失败', 'Save privacy redaction failed', '儲存隱私脫敏設定失敗'),
|
||||
hermesPrivacyConfigRedactPii: _('送入模型前脱敏用户标识和手机号', 'Redact user identifiers and phone numbers before model context', '送入模型前脫敏使用者識別和電話號碼'),
|
||||
hermesPrivacyConfigFootnote: _('目前主要作用于 WhatsApp、Signal 和 Telegram;Discord 与 Slack 为保持 mention 语义不会脱敏真实用户 ID。其他 privacy 高级字段会保留在 raw YAML 中。', 'Currently applies mainly to WhatsApp, Signal, and Telegram. Discord and Slack keep real user IDs to preserve mention semantics. Other advanced privacy fields are preserved in raw YAML.', '目前主要作用於 WhatsApp、Signal 和 Telegram;Discord 與 Slack 為保持 mention 語義不會脫敏真實使用者 ID。其他 privacy 進階欄位會保留在 raw YAML 中。'),
|
||||
hermesBrowserConfigTitle: _('浏览器工具', 'Browser tools', '瀏覽器工具'),
|
||||
hermesBrowserConfigDesc: _('控制浏览器工具的空闲回收、单次命令超时、会话录制和本地引擎,降低长跑浏览器任务卡死和隐私误录风险。', 'Control browser-tool idle cleanup, per-command timeout, session recording, and local engine to reduce stuck long-run browsing tasks and accidental recording risk.', '控制瀏覽器工具的閒置回收、單次命令逾時、工作階段錄製和本機引擎,降低長跑瀏覽器任務卡死和隱私誤錄風險。'),
|
||||
hermesBrowserConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
hermesBrowserConfigSave: _('保存浏览器配置', 'Save browser settings', '儲存瀏覽器設定'),
|
||||
hermesBrowserConfigSaveSuccess: _('浏览器工具配置已保存,建议重启 Hermes Gateway 生效', 'Browser tool settings saved. Restart Hermes Gateway to take effect.', '瀏覽器工具設定已儲存,建議重啟 Hermes Gateway 生效'),
|
||||
hermesBrowserConfigLoadFailed: _('加载浏览器工具配置失败', 'Load browser tool settings failed', '載入瀏覽器工具設定失敗'),
|
||||
hermesBrowserConfigSaveFailed: _('保存浏览器工具配置失败', 'Save browser tool settings failed', '儲存瀏覽器工具設定失敗'),
|
||||
hermesBrowserConfigInactivityTimeout: _('空闲回收秒数', 'Idle cleanup seconds', '閒置回收秒數'),
|
||||
hermesBrowserConfigCommandTimeout: _('单次命令超时秒数', 'Command timeout seconds', '單次命令逾時秒數'),
|
||||
hermesBrowserConfigRecordSessions: _('录制浏览器会话', 'Record browser sessions', '錄製瀏覽器工作階段'),
|
||||
hermesBrowserConfigEngine: _('浏览器引擎', 'Browser engine', '瀏覽器引擎'),
|
||||
hermesBrowserConfigEngine_auto: _('自动选择', 'Auto select', '自動選擇'),
|
||||
hermesBrowserConfigEngine_lightpanda: _('Lightpanda 快速导航', 'Lightpanda fast navigation', 'Lightpanda 快速導覽'),
|
||||
hermesBrowserConfigEngine_chrome: _('Chrome 完整浏览器', 'Chrome full browser', 'Chrome 完整瀏覽器'),
|
||||
hermesBrowserConfigFootnote: _('Lightpanda 导航更快但不支持截图;录制会把 WebM 写入 Hermes browser_recordings 目录,请只在需要审计时开启。CDP、Dialog 和 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. Advanced CDP, Dialog, and Camofox fields stay in raw YAML.', 'Lightpanda 導覽更快但不支援截圖;錄製會把 WebM 寫入 Hermes browser_recordings 目錄,請只在需要稽核時開啟。CDP、Dialog 和 Camofox 進階欄位會保留在 raw YAML 中。'),
|
||||
hermesCompressionTitle: _('上下文压缩', 'Context compression', '上下文壓縮'),
|
||||
hermesCompressionDesc: _('控制长对话何时触发压缩、压缩目标和保留范围,降低上下文过长导致的失败与费用浪费。', 'Control when long conversations are compressed, the target size, and protected message ranges to reduce failures and wasted cost from oversized context.', '控制長對話何時觸發壓縮、壓縮目標和保留範圍,降低上下文過長導致的失敗與費用浪費。'),
|
||||
hermesCompressionStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
|
||||
80
tests/hermes-browser-config.test.js
Normal file
80
tests/hermes-browser-config.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildHermesBrowserConfigValues,
|
||||
mergeHermesBrowserConfig,
|
||||
} from '../scripts/dev-api.js'
|
||||
|
||||
test('Hermes 浏览器配置读取会提供上游默认值', () => {
|
||||
const values = buildHermesBrowserConfigValues({})
|
||||
|
||||
assert.deepEqual(values, {
|
||||
browserInactivityTimeout: 120,
|
||||
browserCommandTimeout: 30,
|
||||
browserRecordSessions: false,
|
||||
browserEngine: 'auto',
|
||||
})
|
||||
})
|
||||
|
||||
test('Hermes 浏览器配置读取会回显 YAML 字段', () => {
|
||||
const values = buildHermesBrowserConfigValues({
|
||||
browser: {
|
||||
inactivity_timeout: 300,
|
||||
command_timeout: 45,
|
||||
record_sessions: true,
|
||||
engine: 'lightpanda',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(values.browserInactivityTimeout, 300)
|
||||
assert.equal(values.browserCommandTimeout, 45)
|
||||
assert.equal(values.browserRecordSessions, true)
|
||||
assert.equal(values.browserEngine, 'lightpanda')
|
||||
})
|
||||
|
||||
test('Hermes 浏览器配置保存会保留未知字段并写入上游结构', () => {
|
||||
const next = mergeHermesBrowserConfig({
|
||||
model: { provider: 'anthropic' },
|
||||
browser: {
|
||||
inactivity_timeout: 120,
|
||||
command_timeout: 30,
|
||||
record_sessions: false,
|
||||
engine: 'auto',
|
||||
cdp_url: 'ws://127.0.0.1:9222/devtools/browser/demo',
|
||||
camofox: { managed_persistence: true },
|
||||
custom_flag: 'keep-browser',
|
||||
},
|
||||
streaming: { enabled: true },
|
||||
}, {
|
||||
browserInactivityTimeout: '180',
|
||||
browserCommandTimeout: '60',
|
||||
browserRecordSessions: true,
|
||||
browserEngine: 'chrome',
|
||||
})
|
||||
|
||||
assert.deepEqual(next.model, { provider: 'anthropic' })
|
||||
assert.deepEqual(next.streaming, { enabled: true })
|
||||
assert.equal(next.browser.inactivity_timeout, 180)
|
||||
assert.equal(next.browser.command_timeout, 60)
|
||||
assert.equal(next.browser.record_sessions, true)
|
||||
assert.equal(next.browser.engine, 'chrome')
|
||||
assert.equal(next.browser.cdp_url, 'ws://127.0.0.1:9222/devtools/browser/demo')
|
||||
assert.deepEqual(next.browser.camofox, { managed_persistence: true })
|
||||
assert.equal(next.browser.custom_flag, 'keep-browser')
|
||||
})
|
||||
|
||||
test('Hermes 浏览器配置保存会拒绝非法引擎和越界值', () => {
|
||||
assert.throws(
|
||||
() => mergeHermesBrowserConfig({}, { browserEngine: 'firefox' }),
|
||||
/browser\.engine/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesBrowserConfig({}, { browserInactivityTimeout: '0' }),
|
||||
/browser\.inactivity_timeout/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesBrowserConfig({}, { browserCommandTimeout: '4' }),
|
||||
/browser\.command_timeout/,
|
||||
)
|
||||
})
|
||||
@@ -159,6 +159,18 @@ test('Hermes 配置页会暴露隐私脱敏结构化配置字段', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露浏览器基础结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-browser-save',
|
||||
'hm-browser-inactivity-timeout',
|
||||
'hm-browser-command-timeout',
|
||||
'hm-browser-record-sessions',
|
||||
'hm-browser-engine',
|
||||
]) {
|
||||
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露终端执行结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-terminal-save',
|
||||
@@ -194,6 +206,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => {
|
||||
key.includes('StreamingConfig') ||
|
||||
key.includes('ExecutionLimits') ||
|
||||
key.includes('PrivacyConfig') ||
|
||||
key.includes('BrowserConfig') ||
|
||||
key.includes('TerminalConfig')
|
||||
)))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user