From b51bde929e83b6bc9d8c7b9654849e4029c874ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Wed, 27 May 2026 06:43:41 +0800 Subject: [PATCH] feat(hermes): add browser camofox controls --- scripts/dev-api.js | 30 +++++ src-tauri/src/commands/hermes.rs | 188 +++++++++++++++++++++++++++- src/engines/hermes/pages/config.js | 29 +++++ src/locales/modules/engine.js | 7 +- tests/hermes-browser-config.test.js | 73 ++++++++++- tests/hermes-config-page-ui.test.js | 4 + 6 files changed, 327 insertions(+), 4 deletions(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 831873b..35736ef 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -4770,6 +4770,17 @@ function normalizeHermesOptionalString(value, key) { return value.trim() } +function normalizeHermesCamofoxIdentity(value, key) { + if (value == null || value === '') return '' + if (typeof value !== 'string') throw new Error(`${key} 必须是字符串`) + const text = value.trim() + if (!text) return '' + if (!/^[A-Za-z0-9_.:@+-]+$/.test(text)) { + throw new Error(`${key} 只能包含字母、数字、下划线、点、冒号、@、加号和短横线`) + } + return text +} + export function buildHermesModelConfigValues(config = {}) { const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} const model = root.model && typeof root.model === 'object' && !Array.isArray(root.model) ? root.model : {} @@ -5639,6 +5650,9 @@ export function buildHermesBrowserConfigValues(config = {}) { const browser = root.browser && typeof root.browser === 'object' && !Array.isArray(root.browser) ? root.browser : {} + const camofox = browser.camofox && typeof browser.camofox === 'object' && !Array.isArray(browser.camofox) + ? browser.camofox + : {} 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), @@ -5647,6 +5661,10 @@ export function buildHermesBrowserConfigValues(config = {}) { browserAllowPrivateUrls: readHermesBool(browser.allow_private_urls, false), browserAutoLocalForPrivateUrls: readHermesBool(browser.auto_local_for_private_urls, true), browserCdpUrl: normalizeHermesOptionalString(browser.cdp_url, 'browser.cdp_url'), + browserCamofoxManagedPersistence: readHermesBool(camofox.managed_persistence, false), + browserCamofoxUserId: normalizeHermesCamofoxIdentity(camofox.user_id, 'browser.camofox.user_id'), + browserCamofoxSessionKey: normalizeHermesCamofoxIdentity(camofox.session_key, 'browser.camofox.session_key'), + browserCamofoxAdoptExistingTab: readHermesBool(camofox.adopt_existing_tab, false), browserDialogPolicy: normalizeHermesBrowserDialogPolicy(browser.dialog_policy, false), browserDialogTimeout: parseHermesInteger(browser.dialog_timeout_s, 'browser.dialog_timeout_s', 300, 1, 86400, false), } @@ -5667,6 +5685,18 @@ export function mergeHermesBrowserConfig(config = {}, form = {}) { const cdpUrl = normalizeHermesOptionalString(Object.hasOwn(form, 'browserCdpUrl') ? form.browserCdpUrl : currentValues.browserCdpUrl, 'browser.cdp_url') if (cdpUrl) browser.cdp_url = cdpUrl else delete browser.cdp_url + const camofox = browser.camofox && typeof browser.camofox === 'object' && !Array.isArray(browser.camofox) + ? mergeConfigsPreservingFields(browser.camofox, {}) + : {} + camofox.managed_persistence = formHermesBool(form, 'browserCamofoxManagedPersistence', currentValues.browserCamofoxManagedPersistence) + const camofoxUserId = normalizeHermesCamofoxIdentity(Object.hasOwn(form, 'browserCamofoxUserId') ? form.browserCamofoxUserId : currentValues.browserCamofoxUserId, 'browser.camofox.user_id') + if (camofoxUserId) camofox.user_id = camofoxUserId + else delete camofox.user_id + const camofoxSessionKey = normalizeHermesCamofoxIdentity(Object.hasOwn(form, 'browserCamofoxSessionKey') ? form.browserCamofoxSessionKey : currentValues.browserCamofoxSessionKey, 'browser.camofox.session_key') + if (camofoxSessionKey) camofox.session_key = camofoxSessionKey + else delete camofox.session_key + camofox.adopt_existing_tab = formHermesBool(form, 'browserCamofoxAdoptExistingTab', currentValues.browserCamofoxAdoptExistingTab) + browser.camofox = camofox browser.dialog_policy = normalizeHermesBrowserDialogPolicy(Object.hasOwn(form, 'browserDialogPolicy') ? form.browserDialogPolicy : currentValues.browserDialogPolicy, true) browser.dialog_timeout_s = parseHermesInteger(Object.hasOwn(form, 'browserDialogTimeout') ? form.browserDialogTimeout : currentValues.browserDialogTimeout, 'browser.dialog_timeout_s', 300, 1, 86400, true) next.browser = browser diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 6b63c6d..d3aa0ce 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2603,6 +2603,23 @@ fn set_optional_yaml_string(map: &mut serde_yaml::Mapping, key: &str, value: Str } } +fn normalize_hermes_camofox_identity(value: Option, key: &str) -> Result { + let text = value.unwrap_or_default().trim().to_string(); + if text.is_empty() { + return Ok(String::new()); + } + if text + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | ':' | '@' | '+' | '-')) + { + Ok(text) + } else { + Err(format!( + "{key} 只能包含字母、数字、下划线、点、冒号、@、加号和短横线" + )) + } +} + fn yaml_string_sequence_field(map: &serde_yaml::Mapping, key: &str) -> Vec { yaml_get(map, key) .and_then(|value| value.as_sequence()) @@ -8583,6 +8600,23 @@ fn build_hermes_browser_config_values(config: &serde_yaml::Value) -> Value { let browser_cdp_url = browser .and_then(|map| yaml_string_field(map, "cdp_url")) .unwrap_or_default(); + let camofox = browser.and_then(|map| yaml_get_mapping(map, "camofox")); + let browser_camofox_managed_persistence = camofox + .and_then(|map| yaml_bool_field(map, "managed_persistence")) + .unwrap_or(false); + let browser_camofox_user_id = normalize_hermes_camofox_identity( + camofox.and_then(|map| yaml_string_field(map, "user_id")), + "browser.camofox.user_id", + ) + .unwrap_or_default(); + let browser_camofox_session_key = normalize_hermes_camofox_identity( + camofox.and_then(|map| yaml_string_field(map, "session_key")), + "browser.camofox.session_key", + ) + .unwrap_or_default(); + let browser_camofox_adopt_existing_tab = camofox + .and_then(|map| yaml_bool_field(map, "adopt_existing_tab")) + .unwrap_or(false); let browser_dialog_policy = normalize_hermes_browser_dialog_policy( browser.and_then(|map| yaml_string_field(map, "dialog_policy")), false, @@ -8600,6 +8634,10 @@ fn build_hermes_browser_config_values(config: &serde_yaml::Value) -> Value { "browserAllowPrivateUrls": browser_allow_private_urls, "browserAutoLocalForPrivateUrls": browser_auto_local_for_private_urls, "browserCdpUrl": browser_cdp_url, + "browserCamofoxManagedPersistence": browser_camofox_managed_persistence, + "browserCamofoxUserId": browser_camofox_user_id, + "browserCamofoxSessionKey": browser_camofox_session_key, + "browserCamofoxAdoptExistingTab": browser_camofox_adopt_existing_tab, "browserDialogPolicy": browser_dialog_policy, "browserDialogTimeout": browser_dialog_timeout, }) @@ -8663,6 +8701,44 @@ fn merge_hermes_browser_config(config: &mut serde_yaml::Value, form: &Value) -> .trim() .to_string() }; + let browser_camofox_managed_persistence = form_bool(form, "browserCamofoxManagedPersistence") + .unwrap_or_else(|| { + current["browserCamofoxManagedPersistence"] + .as_bool() + .unwrap_or(false) + }); + let browser_camofox_user_id = normalize_hermes_camofox_identity( + if form.get("browserCamofoxUserId").is_some() { + Some( + form_string(form, "browserCamofoxUserId") + .ok_or_else(|| "browser.camofox.user_id 必须是字符串".to_string())?, + ) + } else { + current["browserCamofoxUserId"] + .as_str() + .map(ToString::to_string) + }, + "browser.camofox.user_id", + )?; + let browser_camofox_session_key = normalize_hermes_camofox_identity( + if form.get("browserCamofoxSessionKey").is_some() { + Some( + form_string(form, "browserCamofoxSessionKey") + .ok_or_else(|| "browser.camofox.session_key 必须是字符串".to_string())?, + ) + } else { + current["browserCamofoxSessionKey"] + .as_str() + .map(ToString::to_string) + }, + "browser.camofox.session_key", + )?; + let browser_camofox_adopt_existing_tab = form_bool(form, "browserCamofoxAdoptExistingTab") + .unwrap_or_else(|| { + current["browserCamofoxAdoptExistingTab"] + .as_bool() + .unwrap_or(false) + }); let browser_dialog_policy = normalize_hermes_browser_dialog_policy( if form.get("browserDialogPolicy").is_some() { form_string(form, "browserDialogPolicy") @@ -8712,6 +8788,17 @@ fn merge_hermes_browser_config(config: &mut serde_yaml::Value, form: &Value) -> serde_yaml::Value::Bool(browser_auto_local_for_private_urls), ); set_optional_yaml_string(browser, "cdp_url", browser_cdp_url); + let camofox = yaml_child_object(browser, "camofox")?; + camofox.insert( + yaml_key("managed_persistence"), + serde_yaml::Value::Bool(browser_camofox_managed_persistence), + ); + set_optional_yaml_string(camofox, "user_id", browser_camofox_user_id); + set_optional_yaml_string(camofox, "session_key", browser_camofox_session_key); + camofox.insert( + yaml_key("adopt_existing_tab"), + serde_yaml::Value::Bool(browser_camofox_adopt_existing_tab), + ); browser.insert( yaml_key("dialog_policy"), serde_yaml::Value::String(browser_dialog_policy), @@ -18257,6 +18344,10 @@ mod hermes_browser_config_tests { assert_eq!(values["browserAllowPrivateUrls"], false); assert_eq!(values["browserAutoLocalForPrivateUrls"], true); assert_eq!(values["browserCdpUrl"], ""); + assert_eq!(values["browserCamofoxManagedPersistence"], false); + assert_eq!(values["browserCamofoxUserId"], ""); + assert_eq!(values["browserCamofoxSessionKey"], ""); + assert_eq!(values["browserCamofoxAdoptExistingTab"], false); assert_eq!(values["browserDialogPolicy"], "must_respond"); assert_eq!(values["browserDialogTimeout"], 300); } @@ -18273,6 +18364,11 @@ browser: allow_private_urls: true auto_local_for_private_urls: false cdp_url: ws://127.0.0.1:9222/devtools/browser/demo + camofox: + managed_persistence: true + user_id: shared-camofox-user + session_key: shared-session-key + adopt_existing_tab: true dialog_policy: auto_accept dialog_timeout_s: 120 "#, @@ -18289,6 +18385,10 @@ browser: values["browserCdpUrl"], "ws://127.0.0.1:9222/devtools/browser/demo" ); + assert_eq!(values["browserCamofoxManagedPersistence"], true); + assert_eq!(values["browserCamofoxUserId"], "shared-camofox-user"); + assert_eq!(values["browserCamofoxSessionKey"], "shared-session-key"); + assert_eq!(values["browserCamofoxAdoptExistingTab"], true); assert_eq!(values["browserDialogPolicy"], "auto_accept"); assert_eq!(values["browserDialogTimeout"], 120); } @@ -18306,7 +18406,11 @@ browser: engine: auto cdp_url: ws://127.0.0.1:9222/devtools/browser/demo camofox: - managed_persistence: true + managed_persistence: false + user_id: old-user + session_key: old-session + adopt_existing_tab: false + custom_flag: keep-camofox custom_flag: keep-browser streaming: enabled: true @@ -18324,6 +18428,10 @@ streaming: "browserAllowPrivateUrls": true, "browserAutoLocalForPrivateUrls": false, "browserCdpUrl": "http://127.0.0.1:9222", + "browserCamofoxManagedPersistence": true, + "browserCamofoxUserId": "shared-camofox-user", + "browserCamofoxSessionKey": "shared-session-key", + "browserCamofoxAdoptExistingTab": true, "browserDialogPolicy": "auto_dismiss", "browserDialogTimeout": "45", }), @@ -18357,6 +18465,69 @@ streaming: config["browser"]["camofox"]["managed_persistence"].as_bool(), Some(true) ); + assert_eq!( + config["browser"]["camofox"]["user_id"].as_str(), + Some("shared-camofox-user") + ); + assert_eq!( + config["browser"]["camofox"]["session_key"].as_str(), + Some("shared-session-key") + ); + assert_eq!( + config["browser"]["camofox"]["adopt_existing_tab"].as_bool(), + Some(true) + ); + assert_eq!( + config["browser"]["camofox"]["custom_flag"].as_str(), + Some("keep-camofox") + ); + assert_eq!( + config["browser"]["custom_flag"].as_str(), + Some("keep-browser") + ); + } + + #[test] + fn merge_browser_config_removes_empty_camofox_identity_fields() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +browser: + camofox: + managed_persistence: true + user_id: old-user + session_key: old-session + adopt_existing_tab: true + custom_flag: keep-camofox + custom_flag: keep-browser +"#, + ) + .unwrap(); + + merge_hermes_browser_config( + &mut config, + &json!({ + "browserCamofoxManagedPersistence": false, + "browserCamofoxUserId": " ", + "browserCamofoxSessionKey": "", + "browserCamofoxAdoptExistingTab": false, + }), + ) + .unwrap(); + + assert_eq!( + config["browser"]["camofox"]["managed_persistence"].as_bool(), + Some(false) + ); + assert!(config["browser"]["camofox"]["user_id"].is_null()); + assert!(config["browser"]["camofox"]["session_key"].is_null()); + assert_eq!( + config["browser"]["camofox"]["adopt_existing_tab"].as_bool(), + Some(false) + ); + assert_eq!( + config["browser"]["camofox"]["custom_flag"].as_str(), + Some("keep-camofox") + ); assert_eq!( config["browser"]["custom_flag"].as_str(), Some("keep-browser") @@ -18406,6 +18577,21 @@ browser: let err = merge_hermes_browser_config(&mut config, &json!({ "browserCdpUrl": 123 })).unwrap_err(); assert!(err.contains("browser.cdp_url")); + let err = merge_hermes_browser_config(&mut config, &json!({ "browserCamofoxUserId": 123 })) + .unwrap_err(); + assert!(err.contains("browser.camofox.user_id")); + let err = merge_hermes_browser_config( + &mut config, + &json!({ "browserCamofoxUserId": "bad user" }), + ) + .unwrap_err(); + assert!(err.contains("browser.camofox.user_id")); + let err = merge_hermes_browser_config( + &mut config, + &json!({ "browserCamofoxSessionKey": "bad session" }), + ) + .unwrap_err(); + assert!(err.contains("browser.camofox.session_key")); } } diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index b88d720..a8687d0 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -309,6 +309,10 @@ const BROWSER_DEFAULTS = { browserAllowPrivateUrls: false, browserAutoLocalForPrivateUrls: true, browserCdpUrl: '', + browserCamofoxManagedPersistence: false, + browserCamofoxUserId: '', + browserCamofoxSessionKey: '', + browserCamofoxAdoptExistingTab: false, browserDialogPolicy: 'must_respond', browserDialogTimeout: 300, } @@ -2411,6 +2415,27 @@ export function render() { ${t('engine.hermesBrowserConfigAutoLocalForPrivateUrls')} +
${t('engine.hermesBrowserConfigCamofoxTitle')}
+
+ + +
+
+ + +
${t('engine.hermesBrowserConfigFootnote')}
@@ -4816,6 +4841,10 @@ export function render() { browserAllowPrivateUrls: !!el.querySelector('#hm-browser-allow-private-urls')?.checked, browserAutoLocalForPrivateUrls: !!el.querySelector('#hm-browser-auto-local-for-private-urls')?.checked, browserCdpUrl: el.querySelector('#hm-browser-cdp-url')?.value || '', + browserCamofoxManagedPersistence: !!el.querySelector('#hm-browser-camofox-managed-persistence')?.checked, + browserCamofoxUserId: el.querySelector('#hm-browser-camofox-user-id')?.value || '', + browserCamofoxSessionKey: el.querySelector('#hm-browser-camofox-session-key')?.value || '', + browserCamofoxAdoptExistingTab: !!el.querySelector('#hm-browser-camofox-adopt-existing-tab')?.checked, browserDialogPolicy: el.querySelector('#hm-browser-dialog-policy')?.value || 'must_respond', browserDialogTimeout: el.querySelector('#hm-browser-dialog-timeout')?.value || '300', } diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index bf5df1b..67d3b60 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -714,12 +714,17 @@ export default { hermesBrowserConfigAutoLocalForPrivateUrls: _('私网地址自动切到本地浏览器', 'Use local browser automatically for private URLs', '私網位址自動切到本機瀏覽器'), hermesBrowserConfigCdpUrl: _('CDP 连接地址', 'CDP endpoint URL', 'CDP 連線位址'), hermesBrowserConfigCdpUrlPlaceholder: _('留空则自动创建浏览器', 'Leave empty to launch browser automatically', '留空則自動建立瀏覽器'), + hermesBrowserConfigCamofoxTitle: _('Camofox 高级身份', 'Camofox advanced identity', 'Camofox 進階身分'), + hermesBrowserConfigCamofoxManagedPersistence: _('启用 Camofox 托管持久化', 'Enable Camofox managed persistence', '啟用 Camofox 託管持久化'), + hermesBrowserConfigCamofoxUserId: _('Camofox 用户 ID', 'Camofox user ID', 'Camofox 使用者 ID'), + hermesBrowserConfigCamofoxSessionKey: _('Camofox 会话 Key', 'Camofox session key', 'Camofox 工作階段 Key'), + hermesBrowserConfigCamofoxAdoptExistingTab: _('接管已有 Camofox 标签页', 'Adopt existing Camofox tab', '接管既有 Camofox 分頁'), hermesBrowserConfigDialogPolicy: _('弹窗处理策略', 'Dialog handling policy', '彈窗處理策略'), hermesBrowserConfigDialogPolicy_must_respond: _('等待用户处理', 'Wait for user response', '等待使用者處理'), hermesBrowserConfigDialogPolicy_auto_dismiss: _('自动取消弹窗', 'Auto dismiss dialogs', '自動取消彈窗'), hermesBrowserConfigDialogPolicy_auto_accept: _('自动确认弹窗', 'Auto accept dialogs', '自動確認彈窗'), hermesBrowserConfigDialogTimeout: _('弹窗等待超时秒数', 'Dialog wait timeout seconds', '彈窗等待逾時秒數'), - hermesBrowserConfigFootnote: _('Lightpanda 导航更快但不支持截图;录制会把 WebM 写入 Hermes browser_recordings 目录,请只在需要审计时开启。允许私网地址会放开 localhost / 192.168 等目标;CDP 地址留空表示自动创建浏览器;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. Allowing private URLs opens localhost / 192.168-style targets. Empty CDP URL means Hermes launches a browser automatically. Nested Camofox advanced fields stay in raw YAML.', 'Lightpanda 導覽更快但不支援截圖;錄製會把 WebM 寫入 Hermes browser_recordings 目錄,請只在需要稽核時開啟。允許私網位址會放開 localhost / 192.168 等目標;CDP 位址留空表示自動建立瀏覽器;Camofox 巢狀進階欄位會保留在 raw YAML 中。'), + hermesBrowserConfigFootnote: _('Lightpanda 导航更快但不支持截图;录制会把 WebM 写入 Hermes browser_recordings 目录,请只在需要审计时开启。CDP 地址留空表示自动创建浏览器。Camofox 身份字段只影响 Camofox 后端,user_id / session_key 会复用远端 profile 或 session,不保存密码或 API Key;接管已有标签页仅在 Camofox 服务返回 tabs 时生效。', 'Lightpanda navigates faster but does not support screenshots. Recording writes WebM files into the Hermes browser_recordings directory, so enable it only for audits. Empty CDP URL means Hermes launches a browser automatically. Camofox identity fields only affect the Camofox backend; user_id / session_key reuse a remote profile or session and do not store passwords or API keys. Existing-tab adoption only works when the Camofox service returns tabs.', 'Lightpanda 導覽更快但不支援截圖;錄製會把 WebM 寫入 Hermes browser_recordings 目錄,請只在需要稽核時開啟。CDP 位址留空表示自動建立瀏覽器。Camofox 身分欄位只影響 Camofox 後端,user_id / session_key 會複用遠端 profile 或 session,不儲存密碼或 API Key;接管既有分頁僅在 Camofox 服務返回 tabs 時生效。'), hermesWebConfigTitle: _('Web 工具后端', 'Web tool backends', 'Web 工具後端'), hermesWebConfigDesc: _('控制 web_search / web_extract 的默认和分能力后端,只保存后端选择,不保存 API Key。', 'Control default and per-capability backends for web_search / web_extract. This stores backend choices only, not API keys.', '控制 web_search / web_extract 的預設和分能力後端,只儲存後端選擇,不儲存 API Key。'), hermesWebConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-browser-config.test.js b/tests/hermes-browser-config.test.js index 1bd9119..78a0fb6 100644 --- a/tests/hermes-browser-config.test.js +++ b/tests/hermes-browser-config.test.js @@ -17,6 +17,10 @@ test('Hermes 浏览器配置读取会提供上游默认值', () => { browserAllowPrivateUrls: false, browserAutoLocalForPrivateUrls: true, browserCdpUrl: '', + browserCamofoxManagedPersistence: false, + browserCamofoxUserId: '', + browserCamofoxSessionKey: '', + browserCamofoxAdoptExistingTab: false, browserDialogPolicy: 'must_respond', browserDialogTimeout: 300, }) @@ -32,6 +36,12 @@ test('Hermes 浏览器配置读取会回显 YAML 字段', () => { allow_private_urls: true, auto_local_for_private_urls: false, cdp_url: 'ws://127.0.0.1:9222/devtools/browser/demo', + camofox: { + managed_persistence: true, + user_id: 'shared-camofox-user', + session_key: 'shared-session-key', + adopt_existing_tab: true, + }, dialog_policy: 'auto_accept', dialog_timeout_s: 120, }, @@ -44,6 +54,10 @@ test('Hermes 浏览器配置读取会回显 YAML 字段', () => { assert.equal(values.browserAllowPrivateUrls, true) assert.equal(values.browserAutoLocalForPrivateUrls, false) assert.equal(values.browserCdpUrl, 'ws://127.0.0.1:9222/devtools/browser/demo') + assert.equal(values.browserCamofoxManagedPersistence, true) + assert.equal(values.browserCamofoxUserId, 'shared-camofox-user') + assert.equal(values.browserCamofoxSessionKey, 'shared-session-key') + assert.equal(values.browserCamofoxAdoptExistingTab, true) assert.equal(values.browserDialogPolicy, 'auto_accept') assert.equal(values.browserDialogTimeout, 120) }) @@ -57,7 +71,13 @@ test('Hermes 浏览器配置保存会保留未知字段并写入上游结构', ( record_sessions: false, engine: 'auto', cdp_url: 'ws://127.0.0.1:9222/devtools/browser/demo', - camofox: { managed_persistence: true }, + camofox: { + managed_persistence: false, + user_id: 'old-user', + session_key: 'old-session', + adopt_existing_tab: false, + custom_flag: 'keep-camofox', + }, custom_flag: 'keep-browser', }, streaming: { enabled: true }, @@ -69,6 +89,10 @@ test('Hermes 浏览器配置保存会保留未知字段并写入上游结构', ( browserAllowPrivateUrls: true, browserAutoLocalForPrivateUrls: false, browserCdpUrl: 'http://127.0.0.1:9222', + browserCamofoxManagedPersistence: true, + browserCamofoxUserId: 'shared-camofox-user', + browserCamofoxSessionKey: 'shared-session-key', + browserCamofoxAdoptExistingTab: true, browserDialogPolicy: 'auto_dismiss', browserDialogTimeout: '45', }) @@ -84,7 +108,40 @@ test('Hermes 浏览器配置保存会保留未知字段并写入上游结构', ( assert.equal(next.browser.cdp_url, 'http://127.0.0.1:9222') assert.equal(next.browser.dialog_policy, 'auto_dismiss') assert.equal(next.browser.dialog_timeout_s, 45) - assert.deepEqual(next.browser.camofox, { managed_persistence: true }) + assert.deepEqual(next.browser.camofox, { + managed_persistence: true, + user_id: 'shared-camofox-user', + session_key: 'shared-session-key', + adopt_existing_tab: true, + custom_flag: 'keep-camofox', + }) + assert.equal(next.browser.custom_flag, 'keep-browser') +}) + +test('Hermes 浏览器配置保存空 Camofox 身份字段会删除对应覆盖', () => { + const next = mergeHermesBrowserConfig({ + browser: { + camofox: { + managed_persistence: true, + user_id: 'old-user', + session_key: 'old-session', + adopt_existing_tab: true, + custom_flag: 'keep-camofox', + }, + custom_flag: 'keep-browser', + }, + }, { + browserCamofoxManagedPersistence: false, + browserCamofoxUserId: ' ', + browserCamofoxSessionKey: '', + browserCamofoxAdoptExistingTab: false, + }) + + assert.equal(next.browser.camofox.managed_persistence, false) + assert.equal(Object.hasOwn(next.browser.camofox, 'user_id'), false) + assert.equal(Object.hasOwn(next.browser.camofox, 'session_key'), false) + assert.equal(next.browser.camofox.adopt_existing_tab, false) + assert.equal(next.browser.camofox.custom_flag, 'keep-camofox') assert.equal(next.browser.custom_flag, 'keep-browser') }) @@ -127,4 +184,16 @@ test('Hermes 浏览器配置保存会拒绝非法引擎和越界值', () => { () => mergeHermesBrowserConfig({}, { browserCdpUrl: 123 }), /browser\.cdp_url/, ) + assert.throws( + () => mergeHermesBrowserConfig({}, { browserCamofoxUserId: 123 }), + /browser\.camofox\.user_id/, + ) + assert.throws( + () => mergeHermesBrowserConfig({}, { browserCamofoxUserId: 'bad user' }), + /browser\.camofox\.user_id/, + ) + assert.throws( + () => mergeHermesBrowserConfig({}, { browserCamofoxSessionKey: 'bad session' }), + /browser\.camofox\.session_key/, + ) }) diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 56a777a..76500a7 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -458,6 +458,10 @@ test('Hermes 配置页会暴露浏览器基础结构化配置字段', () => { 'hm-browser-allow-private-urls', 'hm-browser-auto-local-for-private-urls', 'hm-browser-cdp-url', + 'hm-browser-camofox-managed-persistence', + 'hm-browser-camofox-user-id', + 'hm-browser-camofox-session-key', + 'hm-browser-camofox-adopt-existing-tab', 'hm-browser-dialog-policy', 'hm-browser-dialog-timeout', ]) {