diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b336780a..517544c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to WebBrain are documented in this file. This changelog was generated from the repository Git history and release tags. Versions without a Git tag are inferred from version-bump commits and the current `package.json` / browser manifest versions. +## [24.1.0] - 2026-07-16 + +### Changed +- import/export configs + +## [24.0.2] - 2026-07-16 + +### Added +- Added `/export --config` to download a portable JSON snapshot of settings and `/import ` or `/import --file` to restore one in Chrome and Firefox. +- Included provider, vision, transcription, and CAPTCHA API keys in configuration snapshots, with an explicit plaintext-secret warning before export and import. + +### Changed +- Limited configuration snapshots to settings-backed state, excluding device-bound sync identifiers and tokens, conversations, traces, jobs, and usage counters. + +### Tests +- Added mirrored coverage for configuration schema completeness, round trips, validation failures, file selection, slash-command routing, and background import/export handlers. + ## [24.0.1] - 2026-07-16 ### Added diff --git a/README.md b/README.md index c2a592898..176927506 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,9 @@ WebBrain accepts slash commands as the first thing on a line in the input box. T | `/reset` | Clear the conversation and all per-conversation flags | | `/screenshot [--full-page]` | Capture the visible tab, or the full scrollable page with `--full-page` (Chrome only) | | `/record [--full-screen] [--transcribe]` | Record the current tab, or a selected screen/window with `--full-screen` (Chrome only); add `--transcribe` to save a transcript after stop | -| `/export [--traces]` | Download version-stamped conversation Markdown, or export the version-stamped tool chain with `--traces` | +| `/export [--traces \| --config]` | Download version-stamped conversation Markdown, export the version-stamped tool chain with `--traces`, or export a Settings snapshot with `--config` | +| `/import ` | Import a Settings snapshot pasted inline | +| `/import --file` | Choose and import a Settings snapshot JSON file | | `/profile` | Toggle profile auto-fill on/off without opening Settings | | `/vision` | Toggle vision mode (screenshot understanding) on the active provider | | `/ask` | Switch to Ask mode before sending | @@ -338,6 +340,13 @@ WebBrain accepts slash commands as the first thing on a line in the input box. T | `/dev` | Switch to Dev mode before sending | | `/plan` | Switch to Ask mode with planning intent | +Configuration snapshots use the `webbrain-config/1` schema and include all +portable Settings values, including provider, vision, transcription, and +CapSolver API keys, profile data, user memory, custom skills, and permission +choices. The JSON is plaintext and should be stored securely. Device-bound +Cloud Sync sessions/device IDs, conversations, traces, scheduled jobs, usage +counters, and accumulated spend are not exported. + Advanced run-capture suffixes are intentionally omitted from `/help` and autocomplete. Append `/record [--save-as ]` to the end of a normal prompt to start recording the current tab immediately before the run, then stop diff --git a/dist/webbrain-chrome-24.0.2.zip b/dist/webbrain-chrome-24.1.0.zip similarity index 84% rename from dist/webbrain-chrome-24.0.2.zip rename to dist/webbrain-chrome-24.1.0.zip index 3c8c16e4e..63ccefe2b 100644 Binary files a/dist/webbrain-chrome-24.0.2.zip and b/dist/webbrain-chrome-24.1.0.zip differ diff --git a/dist/webbrain-edge-24.0.2.zip b/dist/webbrain-edge-24.1.0.zip similarity index 84% rename from dist/webbrain-edge-24.0.2.zip rename to dist/webbrain-edge-24.1.0.zip index 831552369..63ccefe2b 100644 Binary files a/dist/webbrain-edge-24.0.2.zip and b/dist/webbrain-edge-24.1.0.zip differ diff --git a/dist/webbrain-firefox-24.0.2.zip b/dist/webbrain-firefox-24.1.0.zip similarity index 81% rename from dist/webbrain-firefox-24.0.2.zip rename to dist/webbrain-firefox-24.1.0.zip index b5ac9ec17..86cdaaabe 100644 Binary files a/dist/webbrain-firefox-24.0.2.zip and b/dist/webbrain-firefox-24.1.0.zip differ diff --git a/docs/architecture.md b/docs/architecture.md index e8892a960..86264602e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -83,6 +83,14 @@ trailing `/record [--save-as ]` wraps a Chrome run in tab recording, while trailing `/screenshot [--save-as ]` saves before/after viewport captures in both browsers. The panel strips the suffix before agent dispatch, starts capture before `chat`, and finalizes it from the run's `finally` path. + +Settings transfer is also slash-driven. `/export --config` asks the background +for an allowlisted, default-resolved `webbrain-config/1` snapshot, and +`/import ` or `/import --file` validates that schema before replacing the +portable Settings state and rehydrating providers and live agent settings. +Provider and auxiliary-model API keys are intentionally included in plaintext; +device-bound Cloud Sync credentials and device IDs, conversation/runtime data, +scheduled jobs, usage counters, and spend history are intentionally excluded. If a run activates another tab, the screenshot finalizer reactivates the originating run tab before capturing its after state. diff --git a/docs/privacy-and-data-flow.md b/docs/privacy-and-data-flow.md index aa2c6db59..bddfa82fb 100644 --- a/docs/privacy-and-data-flow.md +++ b/docs/privacy-and-data-flow.md @@ -141,6 +141,20 @@ maximum prompt characters injected. `/memory --add ` writes an explicit me immediately without an extractor call. Export/import JSON is local-only and is the v1 bridge for moving memory between browser profiles. +### Configuration Snapshot Transfer + +`/export --config` creates a local plaintext `webbrain-config/1` JSON file, and +`/import ` or `/import --file` reads that snapshot locally before writing +the validated Settings values to extension storage. The snapshot intentionally +includes provider, vision, transcription, and CapSolver API keys as well as +profile text, user memory, custom skills, and saved permission choices. Users +should treat the file like a credential backup and store it securely. + +The snapshot does not include device-bound Cloud Sync authentication/session +state, the WebBrain Cloud device ID, conversations, traces, scheduled jobs, +usage counters, or accumulated spend. Import does not upload the JSON to +WebBrain Cloud or to the configured LLM provider. + ### Optional Encrypted Cloud Sync Active WebBrain Cloud subscribers may explicitly enable encrypted profile sync in diff --git a/package-lock.json b/package-lock.json index 2196105f7..2a9fc9b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webbrain", - "version": "24.0.2", + "version": "24.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webbrain", - "version": "24.0.2", + "version": "24.1.0", "license": "MIT", "devDependencies": { "playwright": "^1.48.0" diff --git a/package.json b/package.json index 9730fd11a..215395fe6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webbrain", - "version": "24.0.2", + "version": "24.1.0", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "private": true, "type": "module", diff --git a/src/chrome/ARCHITECTURE.md b/src/chrome/ARCHITECTURE.md index 0cc55788d..07ff77bd1 100644 --- a/src/chrome/ARCHITECTURE.md +++ b/src/chrome/ARCHITECTURE.md @@ -1,6 +1,6 @@ # WebBrain Chrome/Edge Extension — Architecture -> Version 24.0.2 · Manifest V3 · Service Worker background +> Version 24.1.0 · Manifest V3 · Service Worker background ## High-Level Overview diff --git a/src/chrome/manifest.json b/src/chrome/manifest.json index f70a91531..a39a444a8 100644 --- a/src/chrome/manifest.json +++ b/src/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WebBrain", - "version": "24.0.2", + "version": "24.1.0", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "permissions": [ "sidePanel", diff --git a/src/chrome/src/background.js b/src/chrome/src/background.js index e8a3b5458..3d5fa22fd 100644 --- a/src/chrome/src/background.js +++ b/src/chrome/src/background.js @@ -56,6 +56,11 @@ import { parseUserMemoryExtractionResult, } from './agent/user-memory.js'; import { PROFILE_SYNC_DATA_KEYS, PROFILE_SYNC_KEYS, ProfileSyncManager } from './profile-sync.js'; +import { + CONFIG_STORAGE_KEYS, + createConfigExport, + parseConfigImport, +} from './config-transfer.js'; /** * WebBrain Service Worker (Background Script) @@ -1960,6 +1965,47 @@ async function handleMessage(msg, sender) { return { ok: true, ...(await agent.exportTraces(tabId)) }; } + case 'export_config': { + const stored = await chrome.storage.local.get(CONFIG_STORAGE_KEYS); + const config = createConfigExport(stored, { + locale: msg.locale, + webbrainVersion: chrome.runtime.getManifest().version, + }); + return { + ok: true, + json: JSON.stringify(config, null, 2), + settingCount: CONFIG_STORAGE_KEYS.length, + }; + } + + case 'import_config': { + const imported = parseConfigImport(msg.json); + await chrome.storage.local.set(imported.settings); + await providerManager.load(); + await Promise.all([ + loadMaxSteps(), + loadClarifyTimeout(), + loadAutoScreenshot(), + loadSiteAdapters(), + loadScreenshotRedaction(), + loadStrictSecretMode(), + loadProfile(), + syncAgentUserMemoryFromStorage(), + loadCustomSkills(), + loadCaptchaSolver(), + loadPlanBeforeAct(), + loadPlanReviewSettings(), + loadApiMutationObserverSetting(), + agent._ensureGateSetting({ force: true }), + ]); + agent._refreshSystemPrompts(); + return { + ok: true, + settingCount: CONFIG_STORAGE_KEYS.length, + ignoredKeys: imported.ignoredKeys, + }; + } + case 'get_progress': { const tabId = msg.tabId || sender.tab?.id; if (!tabId) return { ok: false, error: 'No tab ID' }; diff --git a/src/chrome/src/config-transfer.js b/src/chrome/src/config-transfer.js new file mode 100644 index 000000000..29ba90026 --- /dev/null +++ b/src/chrome/src/config-transfer.js @@ -0,0 +1,213 @@ +import { + CUSTOM_SKILLS_STORAGE_KEY, + DEFAULT_SKILLS_REMOVED_STORAGE_KEY, +} from './agent/skills.js'; +import { + USER_MEMORY_AUTO_CAPTURE_KEY, + USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS, + USER_MEMORY_ENABLED_KEY, + USER_MEMORY_FORM_CAPTURE_KEY, + USER_MEMORY_MAX_PROMPT_CHARS_KEY, + USER_MEMORY_STORAGE_KEY, +} from './agent/user-memory.js'; + +export const CONFIG_SCHEMA = 'webbrain-config/1'; +export const MAX_CONFIG_IMPORT_CHARS = 10_000_000; + +// This is intentionally an allowlist of user-controlled Settings state. It +// excludes conversations, traces, schedules, usage counters, the WebBrain +// Cloud device ID, and Cloud Sync session/token metadata. +export const DEFAULT_CONFIG_SETTINGS = Object.freeze({ + wbLocale: 'en', + themeMode: 'system', + verboseMode: false, + selectionShortcutEnabled: true, + helpImproveWebBrain: true, + screenshotFallback: true, + maxAgentSteps: 130, + requestTimeoutMs: 120_000, + clarifyTimeoutSec: 60, + clarifyTimeoutSemanticsV2: true, + autoScreenshot: 'state_change', + useSiteAdapters: true, + voiceInputEnabled: true, + apiMutationObserverEnabled: false, + planBeforeActMode: 'try', + planBeforeAct: true, + planReviewMode: 'confidence', + planReviewConfidenceThreshold: 75, + notifySound: true, + completionConfetti: true, + tracingEnabled: false, + strictSecretMode: false, + agentAllowLocalNetwork: false, + scheduledTasksEnabled: true, + scheduledRequireConsequentialConfirmation: true, + providerFilter: 'all', + costAllowanceSessionUsd: 10, + costAllowanceTotalUsd: 10, + screenshotRedaction: false, + askBeforeConsequentialActions: true, + wb_permissions: [], + providers: {}, + activeProvider: 'webbrain_cloud', + visionModel: null, + transcriptionModel: null, + profileEnabled: false, + profileText: '', + [USER_MEMORY_STORAGE_KEY]: { version: 1, records: [] }, + [USER_MEMORY_ENABLED_KEY]: true, + [USER_MEMORY_AUTO_CAPTURE_KEY]: false, + [USER_MEMORY_FORM_CAPTURE_KEY]: false, + [USER_MEMORY_MAX_PROMPT_CHARS_KEY]: USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS, + [CUSTOM_SKILLS_STORAGE_KEY]: [], + [DEFAULT_SKILLS_REMOVED_STORAGE_KEY]: [], + captchaSolverEnabled: false, + capsolverApiKey: '', +}); + +export const CONFIG_STORAGE_KEYS = Object.freeze(Object.keys(DEFAULT_CONFIG_SETTINGS)); +const CONFIG_STORAGE_KEY_SET = new Set(CONFIG_STORAGE_KEYS); + +const BOOLEAN_KEYS = new Set([ + 'verboseMode', + 'selectionShortcutEnabled', + 'helpImproveWebBrain', + 'screenshotFallback', + 'clarifyTimeoutSemanticsV2', + 'useSiteAdapters', + 'voiceInputEnabled', + 'apiMutationObserverEnabled', + 'planBeforeAct', + 'notifySound', + 'completionConfetti', + 'tracingEnabled', + 'strictSecretMode', + 'agentAllowLocalNetwork', + 'scheduledTasksEnabled', + 'scheduledRequireConsequentialConfirmation', + 'screenshotRedaction', + 'askBeforeConsequentialActions', + 'profileEnabled', + USER_MEMORY_ENABLED_KEY, + USER_MEMORY_AUTO_CAPTURE_KEY, + USER_MEMORY_FORM_CAPTURE_KEY, + 'captchaSolverEnabled', +]); +const NUMBER_KEYS = new Set([ + 'maxAgentSteps', + 'requestTimeoutMs', + 'clarifyTimeoutSec', + 'planReviewConfidenceThreshold', + 'costAllowanceSessionUsd', + 'costAllowanceTotalUsd', + USER_MEMORY_MAX_PROMPT_CHARS_KEY, +]); +const STRING_KEYS = new Set([ + 'wbLocale', + 'themeMode', + 'autoScreenshot', + 'planBeforeActMode', + 'planReviewMode', + 'providerFilter', + 'activeProvider', + 'profileText', + 'capsolverApiKey', +]); +const ARRAY_KEYS = new Set([ + 'wb_permissions', + CUSTOM_SKILLS_STORAGE_KEY, + DEFAULT_SKILLS_REMOVED_STORAGE_KEY, +]); +const NULLABLE_OBJECT_KEYS = new Set(['visionModel', 'transcriptionModel']); + +function isPlainObject(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function clone(value) { + return structuredClone(value); +} + +function sanitizeProviders(value, { strict = false } = {}) { + if (!isPlainObject(value)) return {}; + const providers = clone(value); + for (const [id, config] of Object.entries(providers)) { + if (!isPlainObject(config)) { + if (strict) throw new Error(`Invalid provider configuration "${id}".`); + delete providers[id]; + continue; + } + delete config.deviceGuid; + } + return providers; +} + +function validSettingValue(key, value) { + if (BOOLEAN_KEYS.has(key)) return typeof value === 'boolean'; + if (NUMBER_KEYS.has(key)) return typeof value === 'number' && Number.isFinite(value); + if (STRING_KEYS.has(key)) return typeof value === 'string'; + if (ARRAY_KEYS.has(key)) return Array.isArray(value); + if (NULLABLE_OBJECT_KEYS.has(key)) return value === null || isPlainObject(value); + if (key === 'providers' || key === USER_MEMORY_STORAGE_KEY) return isPlainObject(value); + return true; +} + +function normalizeSettings(source, { strict = false } = {}) { + const settings = clone(DEFAULT_CONFIG_SETTINGS); + if (!isPlainObject(source)) { + if (strict) throw new Error('Configuration settings must be a JSON object.'); + return settings; + } + + for (const key of CONFIG_STORAGE_KEYS) { + if (!Object.hasOwn(source, key)) continue; + const value = source[key]; + if (!validSettingValue(key, value)) { + if (strict) throw new Error(`Invalid value for configuration setting "${key}".`); + continue; + } + settings[key] = clone(value); + } + settings.providers = sanitizeProviders(settings.providers, { strict }); + return settings; +} + +export function createConfigExport(stored = {}, options = {}) { + const settings = normalizeSettings(stored); + if (typeof options.locale === 'string' && options.locale) settings.wbLocale = options.locale; + return { + schema: CONFIG_SCHEMA, + exportedAt: new Date(options.exportedAt ?? Date.now()).toISOString(), + webbrainVersion: String(options.webbrainVersion || 'unknown'), + warning: 'Contains plaintext provider API keys and other sensitive Settings data. Store securely.', + settings, + }; +} + +export function parseConfigImport(json) { + const text = String(json || ''); + if (text.length > MAX_CONFIG_IMPORT_CHARS) throw new Error('Configuration JSON is too large.'); + if (!text.trim()) throw new Error('Paste configuration JSON or use /import --file.'); + + let parsed; + try { + parsed = JSON.parse(text); + } catch { + throw new Error('Configuration is not valid JSON.'); + } + if (!isPlainObject(parsed) || parsed.schema !== CONFIG_SCHEMA) { + throw new Error(`Expected a ${CONFIG_SCHEMA} export.`); + } + if (!isPlainObject(parsed.settings)) { + throw new Error('Configuration settings must be a JSON object.'); + } + + return { + settings: normalizeSettings(parsed.settings, { strict: true }), + ignoredKeys: Object.keys(parsed.settings).filter((key) => !CONFIG_STORAGE_KEY_SET.has(key)), + sourceVersion: typeof parsed.webbrainVersion === 'string' ? parsed.webbrainVersion : '', + }; +} diff --git a/src/chrome/src/ui/locales/ar.js b/src/chrome/src/ui/locales/ar.js index 3c8f0a94c..197aabd24 100644 --- a/src/chrome/src/ui/locales/ar.js +++ b/src/chrome/src/ui/locales/ar.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'أقصى', 'st.providers.compat.value.system': 'نظام', 'st.providers.compat.value.developer': 'مطوّر', + "sp.slash.export_config": "تصدير جميع قيم الإعدادات، بما فيها مفاتيح API لموفري الخدمة", + "sp.slash.import_config": "استيراد لقطة إعداد WebBrain بصيغة JSON", + "sp.slash.import_config_file": "اختيار ملف إعداد WebBrain بصيغة JSON", + "sp.export_config.done": "تم تصدير الإعداد كنص JSON غير مشفر. يتضمن مفاتيح API لموفري الخدمة وبيانات الملف الشخصي وذاكرة المستخدم وقيم إعدادات حساسة أخرى. خزّنه بأمان.", + "sp.export_config.error": "تعذر تصدير الإعداد: {error}", + "sp.import_config.done": "تم استيراد {count} من قيم الإعدادات. مفاتيح موفري الخدمة والإعداد مفعّلة الآن.", + "sp.import_config.error": "تعذر استيراد الإعداد: {error}", }; diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index aafb27898..73cf50fb0 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -773,4 +773,11 @@ export default { "sp.export_traces.no_conversation": "Nothing to export yet — start a conversation first.", "sp.export_traces.partial": "Tool chain exported, but some turn events could not be read.", "sp.export_traces.truncated": "Tool chain exported. Older turns may be missing if this conversation has many traced runs.", + "sp.slash.export_config": "Export all Settings values, including provider API keys", + "sp.slash.import_config": "Import a WebBrain configuration JSON snapshot", + "sp.slash.import_config_file": "Choose a WebBrain configuration JSON file", + "sp.export_config.done": "Configuration exported as plaintext JSON. It includes provider API keys, profile data, user memory, and other sensitive Settings values. Store it securely.", + "sp.export_config.error": "Couldn't export configuration: {error}", + "sp.import_config.done": "Imported {count} Settings values. Provider keys and configuration are active.", + "sp.import_config.error": "Couldn't import configuration: {error}", }; diff --git a/src/chrome/src/ui/locales/es.js b/src/chrome/src/ui/locales/es.js index 5c60099b8..599c85e56 100644 --- a/src/chrome/src/ui/locales/es.js +++ b/src/chrome/src/ui/locales/es.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Máx', 'st.providers.compat.value.system': 'Sistema', 'st.providers.compat.value.developer': 'Desarrollador', + "sp.slash.export_config": "Exportar todos los valores de Configuración, incluidas las claves API de proveedores", + "sp.slash.import_config": "Importar una instantánea JSON de configuración de WebBrain", + "sp.slash.import_config_file": "Elegir un archivo JSON de configuración de WebBrain", + "sp.export_config.done": "La configuración se exportó como JSON de texto sin formato. Incluye claves API de proveedores, datos de perfil, memoria del usuario y otros valores sensibles. Guárdela de forma segura.", + "sp.export_config.error": "No se pudo exportar la configuración: {error}", + "sp.import_config.done": "Se importaron {count} valores de Configuración. Las claves de proveedores y la configuración están activas.", + "sp.import_config.error": "No se pudo importar la configuración: {error}", }; diff --git a/src/chrome/src/ui/locales/fr.js b/src/chrome/src/ui/locales/fr.js index 6db95eaa0..6bba30cb9 100644 --- a/src/chrome/src/ui/locales/fr.js +++ b/src/chrome/src/ui/locales/fr.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Max', 'st.providers.compat.value.system': 'Système', 'st.providers.compat.value.developer': 'Développeur', + "sp.slash.export_config": "Exporter tous les paramètres, y compris les clés API des fournisseurs", + "sp.slash.import_config": "Importer un instantané JSON de configuration WebBrain", + "sp.slash.import_config_file": "Choisir un fichier JSON de configuration WebBrain", + "sp.export_config.done": "La configuration a été exportée en JSON non chiffré. Elle contient les clés API des fournisseurs, les données de profil, la mémoire utilisateur et d’autres paramètres sensibles. Conservez-la en lieu sûr.", + "sp.export_config.error": "Impossible d’exporter la configuration : {error}", + "sp.import_config.done": "{count} paramètres ont été importés. Les clés des fournisseurs et la configuration sont actives.", + "sp.import_config.error": "Impossible d’importer la configuration : {error}", }; diff --git a/src/chrome/src/ui/locales/he.js b/src/chrome/src/ui/locales/he.js index 9875b8d9c..77d7e38af 100644 --- a/src/chrome/src/ui/locales/he.js +++ b/src/chrome/src/ui/locales/he.js @@ -724,4 +724,11 @@ export default { 'st.providers.compat.value.max': 'מקסימום', 'st.providers.compat.value.system': 'מערכת', 'st.providers.compat.value.developer': 'מפתח', + "sp.slash.export_config": "ייצוא כל ערכי ההגדרות, כולל מפתחות API של ספקים", + "sp.slash.import_config": "ייבוא תמונת מצב של תצורת WebBrain בקובץ JSON", + "sp.slash.import_config_file": "בחירת קובץ JSON של תצורת WebBrain", + "sp.export_config.done": "התצורה יוצאה כ-JSON בטקסט גלוי. היא כוללת מפתחות API של ספקים, נתוני פרופיל, זיכרון משתמש וערכי הגדרות רגישים נוספים. יש לשמור אותה באופן מאובטח.", + "sp.export_config.error": "לא ניתן לייצא את התצורה: {error}", + "sp.import_config.done": "יובאו {count} ערכי הגדרות. מפתחות הספקים והתצורה פעילים כעת.", + "sp.import_config.error": "לא ניתן לייבא את התצורה: {error}", }; diff --git a/src/chrome/src/ui/locales/id.js b/src/chrome/src/ui/locales/id.js index b2b122d90..ed9c5c206 100644 --- a/src/chrome/src/ui/locales/id.js +++ b/src/chrome/src/ui/locales/id.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Maks', 'st.providers.compat.value.system': 'Sistem', 'st.providers.compat.value.developer': 'Pengembang', + "sp.slash.export_config": "Ekspor semua nilai Pengaturan, termasuk kunci API penyedia", + "sp.slash.import_config": "Impor snapshot JSON konfigurasi WebBrain", + "sp.slash.import_config_file": "Pilih file JSON konfigurasi WebBrain", + "sp.export_config.done": "Konfigurasi diekspor sebagai JSON teks biasa. File ini berisi kunci API penyedia, data profil, memori pengguna, dan nilai Pengaturan sensitif lainnya. Simpan dengan aman.", + "sp.export_config.error": "Tidak dapat mengekspor konfigurasi: {error}", + "sp.import_config.done": "Berhasil mengimpor {count} nilai Pengaturan. Kunci penyedia dan konfigurasi sudah aktif.", + "sp.import_config.error": "Tidak dapat mengimpor konfigurasi: {error}", }; diff --git a/src/chrome/src/ui/locales/ja.js b/src/chrome/src/ui/locales/ja.js index 39f4220dd..ac387ecd0 100644 --- a/src/chrome/src/ui/locales/ja.js +++ b/src/chrome/src/ui/locales/ja.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': '最大', 'st.providers.compat.value.system': 'システム', 'st.providers.compat.value.developer': '開発者', + "sp.slash.export_config": "プロバイダーの API キーを含むすべての設定値をエクスポート", + "sp.slash.import_config": "WebBrain 設定の JSON スナップショットをインポート", + "sp.slash.import_config_file": "WebBrain 設定の JSON ファイルを選択", + "sp.export_config.done": "設定を平文の JSON としてエクスポートしました。プロバイダーの API キー、プロフィール情報、ユーザーメモリ、その他の機密設定が含まれます。安全に保管してください。", + "sp.export_config.error": "設定をエクスポートできませんでした: {error}", + "sp.import_config.done": "{count} 件の設定値をインポートしました。プロバイダーキーと設定は有効です。", + "sp.import_config.error": "設定をインポートできませんでした: {error}", }; diff --git a/src/chrome/src/ui/locales/ko.js b/src/chrome/src/ui/locales/ko.js index d4ed73439..e41cabbb0 100644 --- a/src/chrome/src/ui/locales/ko.js +++ b/src/chrome/src/ui/locales/ko.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': '최대', 'st.providers.compat.value.system': '시스템', 'st.providers.compat.value.developer': '개발자', + "sp.slash.export_config": "공급자 API 키를 포함한 모든 설정 값 내보내기", + "sp.slash.import_config": "WebBrain 구성 JSON 스냅샷 가져오기", + "sp.slash.import_config_file": "WebBrain 구성 JSON 파일 선택", + "sp.export_config.done": "구성을 일반 텍스트 JSON으로 내보냈습니다. 공급자 API 키, 프로필 데이터, 사용자 메모리 및 기타 민감한 설정 값이 포함됩니다. 안전하게 보관하세요.", + "sp.export_config.error": "구성을 내보낼 수 없습니다: {error}", + "sp.import_config.done": "설정 값 {count}개를 가져왔습니다. 공급자 키와 구성이 활성화되었습니다.", + "sp.import_config.error": "구성을 가져올 수 없습니다: {error}", }; diff --git a/src/chrome/src/ui/locales/ms.js b/src/chrome/src/ui/locales/ms.js index 93eac40e4..5be03b689 100644 --- a/src/chrome/src/ui/locales/ms.js +++ b/src/chrome/src/ui/locales/ms.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Maks', 'st.providers.compat.value.system': 'Sistem', 'st.providers.compat.value.developer': 'Pembangun', + "sp.slash.export_config": "Eksport semua nilai Tetapan, termasuk kunci API penyedia", + "sp.slash.import_config": "Import petikan JSON konfigurasi WebBrain", + "sp.slash.import_config_file": "Pilih fail JSON konfigurasi WebBrain", + "sp.export_config.done": "Konfigurasi dieksport sebagai JSON teks biasa. Ia mengandungi kunci API penyedia, data profil, memori pengguna dan nilai Tetapan sensitif lain. Simpan dengan selamat.", + "sp.export_config.error": "Tidak dapat mengeksport konfigurasi: {error}", + "sp.import_config.done": "{count} nilai Tetapan telah diimport. Kunci penyedia dan konfigurasi kini aktif.", + "sp.import_config.error": "Tidak dapat mengimport konfigurasi: {error}", }; diff --git a/src/chrome/src/ui/locales/pl.js b/src/chrome/src/ui/locales/pl.js index 67ed5651e..b46db13b4 100644 --- a/src/chrome/src/ui/locales/pl.js +++ b/src/chrome/src/ui/locales/pl.js @@ -731,4 +731,11 @@ export default { "st.display.help_improve.label": "Pomóż ulepszać WebBrain", "st.display.help_improve.desc_html": "Zezwól na przechowywanie wybranych interakcji z WebBrain Cloud i używanie ich do oceny, ulepszania, dostrajania i trenowania. Domyślnie włączone. Wyłącz, aby przyszłe interakcje Cloud nie były używane do tych celów. WebBrain nigdy nie zbiera żądań do modeli lokalnych ani żądań wysyłanych bezpośrednio przy użyciu własnych danych logowania API. Polityka prywatności →", "st.providers.webbrain_data_use.body": "Darmowe dzienne korzystanie z WebBrain Cloud jest wliczone. Gdy opcja „Pomóż ulepszać WebBrain” jest domyślnie włączona, wybrane rozmowy Cloud mogą być przechowywane i używane do oceny, ulepszania, dostrajania i trenowania. Wyłącz ją w Ogólne, aby wykluczyć przyszłe interakcje Cloud z tych zastosowań. WebBrain nigdy nie zbiera żądań do modeli lokalnych ani żądań wysyłanych bezpośrednio przy użyciu własnych danych logowania API. {privacyLink}. Aby korzystać więcej, subskrybuj na {subscribeLink}. Zarządzaj rozliczeniami na {accountLink}.", + "sp.slash.export_config": "Eksportuj wszystkie ustawienia, w tym klucze API dostawców", + "sp.slash.import_config": "Importuj migawkę konfiguracji WebBrain w formacie JSON", + "sp.slash.import_config_file": "Wybierz plik JSON konfiguracji WebBrain", + "sp.export_config.done": "Konfigurację wyeksportowano jako niezaszyfrowany JSON. Zawiera klucze API dostawców, dane profilu, pamięć użytkownika i inne poufne ustawienia. Przechowuj ją bezpiecznie.", + "sp.export_config.error": "Nie udało się wyeksportować konfiguracji: {error}", + "sp.import_config.done": "Zaimportowano ustawienia: {count}. Klucze dostawców i konfiguracja są aktywne.", + "sp.import_config.error": "Nie udało się zaimportować konfiguracji: {error}", }; diff --git a/src/chrome/src/ui/locales/ru.js b/src/chrome/src/ui/locales/ru.js index adccced8f..bc32dca61 100644 --- a/src/chrome/src/ui/locales/ru.js +++ b/src/chrome/src/ui/locales/ru.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Макс.', 'st.providers.compat.value.system': 'Система', 'st.providers.compat.value.developer': 'Разработчик', + "sp.slash.export_config": "Экспортировать все настройки, включая API-ключи провайдеров", + "sp.slash.import_config": "Импортировать JSON-снимок конфигурации WebBrain", + "sp.slash.import_config_file": "Выбрать JSON-файл конфигурации WebBrain", + "sp.export_config.done": "Конфигурация экспортирована как незашифрованный JSON. Она содержит API-ключи провайдеров, данные профиля, пользовательскую память и другие конфиденциальные настройки. Храните её безопасно.", + "sp.export_config.error": "Не удалось экспортировать конфигурацию: {error}", + "sp.import_config.done": "Импортировано настроек: {count}. Ключи провайдеров и конфигурация активны.", + "sp.import_config.error": "Не удалось импортировать конфигурацию: {error}", }; diff --git a/src/chrome/src/ui/locales/th.js b/src/chrome/src/ui/locales/th.js index 6aca05cd5..e1217398f 100644 --- a/src/chrome/src/ui/locales/th.js +++ b/src/chrome/src/ui/locales/th.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'สูงสุด', 'st.providers.compat.value.system': 'ระบบ', 'st.providers.compat.value.developer': 'นักพัฒนา', + "sp.slash.export_config": "ส่งออกค่าการตั้งค่าทั้งหมด รวมถึงคีย์ API ของผู้ให้บริการ", + "sp.slash.import_config": "นำเข้าสแนปช็อต JSON การกำหนดค่า WebBrain", + "sp.slash.import_config_file": "เลือกไฟล์ JSON การกำหนดค่า WebBrain", + "sp.export_config.done": "ส่งออกการกำหนดค่าเป็น JSON แบบข้อความธรรมดาแล้ว ไฟล์นี้มีคีย์ API ของผู้ให้บริการ ข้อมูลโปรไฟล์ หน่วยความจำผู้ใช้ และค่าการตั้งค่าที่ละเอียดอ่อนอื่น ๆ โปรดเก็บไว้อย่างปลอดภัย", + "sp.export_config.error": "ส่งออกการกำหนดค่าไม่ได้: {error}", + "sp.import_config.done": "นำเข้าค่าการตั้งค่า {count} รายการแล้ว คีย์ผู้ให้บริการและการกำหนดค่ามีผลแล้ว", + "sp.import_config.error": "นำเข้าการกำหนดค่าไม่ได้: {error}", }; diff --git a/src/chrome/src/ui/locales/tl.js b/src/chrome/src/ui/locales/tl.js index 3b8248995..aa4ad177b 100644 --- a/src/chrome/src/ui/locales/tl.js +++ b/src/chrome/src/ui/locales/tl.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Max', 'st.providers.compat.value.system': 'System', 'st.providers.compat.value.developer': 'Developer', + "sp.slash.export_config": "I-export ang lahat ng value ng Settings, kasama ang mga provider API key", + "sp.slash.import_config": "Mag-import ng WebBrain configuration JSON snapshot", + "sp.slash.import_config_file": "Pumili ng WebBrain configuration JSON file", + "sp.export_config.done": "Na-export ang configuration bilang plaintext JSON. Kasama rito ang mga provider API key, profile data, user memory, at iba pang sensitibong Settings value. Itago ito nang ligtas.", + "sp.export_config.error": "Hindi ma-export ang configuration: {error}", + "sp.import_config.done": "Na-import ang {count} Settings value. Aktibo na ang mga provider key at configuration.", + "sp.import_config.error": "Hindi ma-import ang configuration: {error}", }; diff --git a/src/chrome/src/ui/locales/tr.js b/src/chrome/src/ui/locales/tr.js index 4ae9f8729..52fe0c9ca 100644 --- a/src/chrome/src/ui/locales/tr.js +++ b/src/chrome/src/ui/locales/tr.js @@ -770,4 +770,11 @@ export default { 'st.providers.compat.value.max': 'Maks', 'st.providers.compat.value.system': 'Sistem', 'st.providers.compat.value.developer': 'Geliştirici', + "sp.slash.export_config": "Sağlayıcı API anahtarları dahil tüm Ayarlar değerlerini dışa aktar", + "sp.slash.import_config": "Bir WebBrain yapılandırma JSON anlık görüntüsünü içe aktar", + "sp.slash.import_config_file": "Bir WebBrain yapılandırma JSON dosyası seç", + "sp.export_config.done": "Yapılandırma düz metin JSON olarak dışa aktarıldı. Sağlayıcı API anahtarlarını, profil verilerini, kullanıcı belleğini ve diğer hassas Ayarlar değerlerini içerir. Güvenli biçimde saklayın.", + "sp.export_config.error": "Yapılandırma dışa aktarılamadı: {error}", + "sp.import_config.done": "{count} Ayarlar değeri içe aktarıldı. Sağlayıcı anahtarları ve yapılandırma etkin.", + "sp.import_config.error": "Yapılandırma içe aktarılamadı: {error}", }; diff --git a/src/chrome/src/ui/locales/uk.js b/src/chrome/src/ui/locales/uk.js index f310119c1..a035801ec 100644 --- a/src/chrome/src/ui/locales/uk.js +++ b/src/chrome/src/ui/locales/uk.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': 'Макс.', 'st.providers.compat.value.system': 'Система', 'st.providers.compat.value.developer': 'Розробник', + "sp.slash.export_config": "Експортувати всі налаштування, включно з API-ключами провайдерів", + "sp.slash.import_config": "Імпортувати JSON-знімок конфігурації WebBrain", + "sp.slash.import_config_file": "Вибрати JSON-файл конфігурації WebBrain", + "sp.export_config.done": "Конфігурацію експортовано як незашифрований JSON. Вона містить API-ключі провайдерів, дані профілю, пам’ять користувача та інші конфіденційні налаштування. Зберігайте її безпечно.", + "sp.export_config.error": "Не вдалося експортувати конфігурацію: {error}", + "sp.import_config.done": "Імпортовано налаштувань: {count}. Ключі провайдерів і конфігурація активні.", + "sp.import_config.error": "Не вдалося імпортувати конфігурацію: {error}", }; diff --git a/src/chrome/src/ui/locales/zh.js b/src/chrome/src/ui/locales/zh.js index 4e663724e..fc9c0aa62 100644 --- a/src/chrome/src/ui/locales/zh.js +++ b/src/chrome/src/ui/locales/zh.js @@ -771,4 +771,11 @@ export default { 'st.providers.compat.value.max': '最大', 'st.providers.compat.value.system': '系统', 'st.providers.compat.value.developer': '开发者', + "sp.slash.export_config": "导出所有设置值,包括提供商 API 密钥", + "sp.slash.import_config": "导入 WebBrain 配置 JSON 快照", + "sp.slash.import_config_file": "选择 WebBrain 配置 JSON 文件", + "sp.export_config.done": "配置已导出为明文 JSON。其中包含提供商 API 密钥、个人资料、用户记忆和其他敏感设置。请安全保存。", + "sp.export_config.error": "无法导出配置:{error}", + "sp.import_config.done": "已导入 {count} 个设置值。提供商密钥和配置现已生效。", + "sp.import_config.error": "无法导入配置:{error}", }; diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index cdae524b5..6e601e074 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -35,7 +35,7 @@ import { // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '24.0.2'; +const EXT_VERSION = '24.1.0'; const providersContainer = document.getElementById('providers'); const displaySettings = document.getElementById('display-settings'); diff --git a/src/chrome/src/ui/sidepanel.js b/src/chrome/src/ui/sidepanel.js index 74b340e41..fe3a18191 100644 --- a/src/chrome/src/ui/sidepanel.js +++ b/src/chrome/src/ui/sidepanel.js @@ -417,12 +417,23 @@ const SLASH_COMMANDS = [ }, { value: '/export', - usage: '/export [--traces]', + usage: '/export [--traces | --config]', descriptionKey: 'sp.slash.export', action: 'conversation', outOfBand: true, options: [ - { value: '--traces', descriptionKey: 'sp.slash.export_traces', action: 'traces', outOfBand: true, disallowPayload: true }, + { value: '--traces', descriptionKey: 'sp.slash.export_traces', action: 'traces', outOfBand: true, disallowPayload: true, exclusiveGroup: 'export-format' }, + { value: '--config', descriptionKey: 'sp.slash.export_config', action: 'config', outOfBand: true, disallowPayload: true, exclusiveGroup: 'export-format' }, + ], + }, + { + value: '/import', + usage: '/import | /import --file', + descriptionKey: 'sp.slash.import_config', + action: 'json', + acceptsPayload: true, + options: [ + { value: '--file', descriptionKey: 'sp.slash.import_config_file', action: 'file', disallowPayload: true }, ], }, { value: '/profile', usage: '/profile', descriptionKey: 'sp.slash.profile', action: 'toggle' }, @@ -4026,6 +4037,38 @@ function resolvePendingPermissionPromptsForTab(tabId) { return resolved; } +async function importConfigurationJson(json, tabId) { + try { + const res = await sendToBackground('import_config', { json }); + await loadProviders(); + if (currentTabId === tabId) { + addPersistentSlashMessage(t('sp.import_config.done', { count: res?.settingCount || 0 })); + } + } catch (error) { + if (currentTabId === tabId) { + addPersistentSlashMessage(t('sp.import_config.error', { error: error?.message || 'invalid configuration' })); + } + } +} + +function requestConfigurationFile(tabId) { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json,application/json'; + input.addEventListener('change', () => { + const file = input.files?.[0]; + if (!file) return; + file.text() + .then((json) => importConfigurationJson(json, tabId)) + .catch((error) => { + if (currentTabId === tabId) { + addPersistentSlashMessage(t('sp.import_config.error', { error: error?.message || 'file read failed' })); + } + }); + }, { once: true }); + input.click(); +} + /** * Parse leading slash commands out of the user's message. * Returns the cleaned text (empty string if fully consumed). @@ -4214,6 +4257,39 @@ async function parseSlashCommands(text, tabId = currentTabId) { return ''; } + if (command.value === '/import' && action === 'file') { + requestConfigurationFile(tabId); + return ''; + } + + if (command.value === '/import' && action === 'json') { + await importConfigurationJson(payload, tabId); + return ''; + } + + if (command.value === '/export' && action === 'config') { + try { + const res = await sendToBackground('export_config', { locale: getLocale() }); + if (!res?.ok || !res.json) throw new Error(res?.error || 'empty configuration'); + const blob = new Blob([res.json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `webbrain-config-${Date.now()}.json`; + document.body.appendChild(a); + try { + a.click(); + } finally { + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 7000); + } + addPersistentSlashMessage(t('sp.export_config.done')); + } catch (error) { + addPersistentSlashMessage(t('sp.export_config.error', { error: error?.message || 'unknown error' })); + } + return ''; + } + if (command.value === '/export' && action === 'traces') { let res; try { diff --git a/src/firefox/ARCHITECTURE.md b/src/firefox/ARCHITECTURE.md index 0d4bf8b3f..c07cd5ba8 100644 --- a/src/firefox/ARCHITECTURE.md +++ b/src/firefox/ARCHITECTURE.md @@ -1,6 +1,6 @@ # WebBrain Firefox Extension — Architecture -> Version 24.0.2 · Manifest V2 · Background Page +> Version 24.1.0 · Manifest V2 · Background Page ## How Firefox Differs from Chrome diff --git a/src/firefox/manifest.json b/src/firefox/manifest.json index fe9d63c10..c4ddaae8b 100644 --- a/src/firefox/manifest.json +++ b/src/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "WebBrain", - "version": "24.0.2", + "version": "24.1.0", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "permissions": [ "activeTab", diff --git a/src/firefox/src/background.js b/src/firefox/src/background.js index de648a1fe..2e46f44c9 100644 --- a/src/firefox/src/background.js +++ b/src/firefox/src/background.js @@ -45,6 +45,11 @@ import { parseUserMemoryExtractionResult, } from './agent/user-memory.js'; import { PROFILE_SYNC_DATA_KEYS, PROFILE_SYNC_KEYS, ProfileSyncManager } from './profile-sync.js'; +import { + CONFIG_STORAGE_KEYS, + createConfigExport, + parseConfigImport, +} from './config-transfer.js'; import { RUN_CAPTURE_START_ERROR_PREFIX, createRunCaptureController } from './run-capture.js'; /** @@ -1689,6 +1694,47 @@ async function handleMessage(msg, sender) { return { ok: true, ...(await agent.exportTraces(tabId)) }; } + case 'export_config': { + const stored = await browser.storage.local.get(CONFIG_STORAGE_KEYS); + const config = createConfigExport(stored, { + locale: msg.locale, + webbrainVersion: browser.runtime.getManifest().version, + }); + return { + ok: true, + json: JSON.stringify(config, null, 2), + settingCount: CONFIG_STORAGE_KEYS.length, + }; + } + + case 'import_config': { + const imported = parseConfigImport(msg.json); + await browser.storage.local.set(imported.settings); + await providerManager.load(); + await Promise.all([ + loadMaxSteps(), + loadClarifyTimeout(), + loadAutoScreenshot(), + loadSiteAdapters(), + loadScreenshotRedaction(), + loadStrictSecretMode(), + loadProfile(), + syncAgentUserMemoryFromStorage(), + loadCustomSkills(), + loadCaptchaSolver(), + loadPlanBeforeAct(), + loadPlanReviewSettings(), + loadApiMutationObserverSetting(), + agent._ensureGateSetting({ force: true }), + ]); + agent._refreshSystemPrompts(); + return { + ok: true, + settingCount: CONFIG_STORAGE_KEYS.length, + ignoredKeys: imported.ignoredKeys, + }; + } + case 'get_progress': { const tabId = msg.tabId || sender.tab?.id; if (!tabId) return { ok: false, error: 'No tab ID' }; diff --git a/src/firefox/src/config-transfer.js b/src/firefox/src/config-transfer.js new file mode 100644 index 000000000..29ba90026 --- /dev/null +++ b/src/firefox/src/config-transfer.js @@ -0,0 +1,213 @@ +import { + CUSTOM_SKILLS_STORAGE_KEY, + DEFAULT_SKILLS_REMOVED_STORAGE_KEY, +} from './agent/skills.js'; +import { + USER_MEMORY_AUTO_CAPTURE_KEY, + USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS, + USER_MEMORY_ENABLED_KEY, + USER_MEMORY_FORM_CAPTURE_KEY, + USER_MEMORY_MAX_PROMPT_CHARS_KEY, + USER_MEMORY_STORAGE_KEY, +} from './agent/user-memory.js'; + +export const CONFIG_SCHEMA = 'webbrain-config/1'; +export const MAX_CONFIG_IMPORT_CHARS = 10_000_000; + +// This is intentionally an allowlist of user-controlled Settings state. It +// excludes conversations, traces, schedules, usage counters, the WebBrain +// Cloud device ID, and Cloud Sync session/token metadata. +export const DEFAULT_CONFIG_SETTINGS = Object.freeze({ + wbLocale: 'en', + themeMode: 'system', + verboseMode: false, + selectionShortcutEnabled: true, + helpImproveWebBrain: true, + screenshotFallback: true, + maxAgentSteps: 130, + requestTimeoutMs: 120_000, + clarifyTimeoutSec: 60, + clarifyTimeoutSemanticsV2: true, + autoScreenshot: 'state_change', + useSiteAdapters: true, + voiceInputEnabled: true, + apiMutationObserverEnabled: false, + planBeforeActMode: 'try', + planBeforeAct: true, + planReviewMode: 'confidence', + planReviewConfidenceThreshold: 75, + notifySound: true, + completionConfetti: true, + tracingEnabled: false, + strictSecretMode: false, + agentAllowLocalNetwork: false, + scheduledTasksEnabled: true, + scheduledRequireConsequentialConfirmation: true, + providerFilter: 'all', + costAllowanceSessionUsd: 10, + costAllowanceTotalUsd: 10, + screenshotRedaction: false, + askBeforeConsequentialActions: true, + wb_permissions: [], + providers: {}, + activeProvider: 'webbrain_cloud', + visionModel: null, + transcriptionModel: null, + profileEnabled: false, + profileText: '', + [USER_MEMORY_STORAGE_KEY]: { version: 1, records: [] }, + [USER_MEMORY_ENABLED_KEY]: true, + [USER_MEMORY_AUTO_CAPTURE_KEY]: false, + [USER_MEMORY_FORM_CAPTURE_KEY]: false, + [USER_MEMORY_MAX_PROMPT_CHARS_KEY]: USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS, + [CUSTOM_SKILLS_STORAGE_KEY]: [], + [DEFAULT_SKILLS_REMOVED_STORAGE_KEY]: [], + captchaSolverEnabled: false, + capsolverApiKey: '', +}); + +export const CONFIG_STORAGE_KEYS = Object.freeze(Object.keys(DEFAULT_CONFIG_SETTINGS)); +const CONFIG_STORAGE_KEY_SET = new Set(CONFIG_STORAGE_KEYS); + +const BOOLEAN_KEYS = new Set([ + 'verboseMode', + 'selectionShortcutEnabled', + 'helpImproveWebBrain', + 'screenshotFallback', + 'clarifyTimeoutSemanticsV2', + 'useSiteAdapters', + 'voiceInputEnabled', + 'apiMutationObserverEnabled', + 'planBeforeAct', + 'notifySound', + 'completionConfetti', + 'tracingEnabled', + 'strictSecretMode', + 'agentAllowLocalNetwork', + 'scheduledTasksEnabled', + 'scheduledRequireConsequentialConfirmation', + 'screenshotRedaction', + 'askBeforeConsequentialActions', + 'profileEnabled', + USER_MEMORY_ENABLED_KEY, + USER_MEMORY_AUTO_CAPTURE_KEY, + USER_MEMORY_FORM_CAPTURE_KEY, + 'captchaSolverEnabled', +]); +const NUMBER_KEYS = new Set([ + 'maxAgentSteps', + 'requestTimeoutMs', + 'clarifyTimeoutSec', + 'planReviewConfidenceThreshold', + 'costAllowanceSessionUsd', + 'costAllowanceTotalUsd', + USER_MEMORY_MAX_PROMPT_CHARS_KEY, +]); +const STRING_KEYS = new Set([ + 'wbLocale', + 'themeMode', + 'autoScreenshot', + 'planBeforeActMode', + 'planReviewMode', + 'providerFilter', + 'activeProvider', + 'profileText', + 'capsolverApiKey', +]); +const ARRAY_KEYS = new Set([ + 'wb_permissions', + CUSTOM_SKILLS_STORAGE_KEY, + DEFAULT_SKILLS_REMOVED_STORAGE_KEY, +]); +const NULLABLE_OBJECT_KEYS = new Set(['visionModel', 'transcriptionModel']); + +function isPlainObject(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function clone(value) { + return structuredClone(value); +} + +function sanitizeProviders(value, { strict = false } = {}) { + if (!isPlainObject(value)) return {}; + const providers = clone(value); + for (const [id, config] of Object.entries(providers)) { + if (!isPlainObject(config)) { + if (strict) throw new Error(`Invalid provider configuration "${id}".`); + delete providers[id]; + continue; + } + delete config.deviceGuid; + } + return providers; +} + +function validSettingValue(key, value) { + if (BOOLEAN_KEYS.has(key)) return typeof value === 'boolean'; + if (NUMBER_KEYS.has(key)) return typeof value === 'number' && Number.isFinite(value); + if (STRING_KEYS.has(key)) return typeof value === 'string'; + if (ARRAY_KEYS.has(key)) return Array.isArray(value); + if (NULLABLE_OBJECT_KEYS.has(key)) return value === null || isPlainObject(value); + if (key === 'providers' || key === USER_MEMORY_STORAGE_KEY) return isPlainObject(value); + return true; +} + +function normalizeSettings(source, { strict = false } = {}) { + const settings = clone(DEFAULT_CONFIG_SETTINGS); + if (!isPlainObject(source)) { + if (strict) throw new Error('Configuration settings must be a JSON object.'); + return settings; + } + + for (const key of CONFIG_STORAGE_KEYS) { + if (!Object.hasOwn(source, key)) continue; + const value = source[key]; + if (!validSettingValue(key, value)) { + if (strict) throw new Error(`Invalid value for configuration setting "${key}".`); + continue; + } + settings[key] = clone(value); + } + settings.providers = sanitizeProviders(settings.providers, { strict }); + return settings; +} + +export function createConfigExport(stored = {}, options = {}) { + const settings = normalizeSettings(stored); + if (typeof options.locale === 'string' && options.locale) settings.wbLocale = options.locale; + return { + schema: CONFIG_SCHEMA, + exportedAt: new Date(options.exportedAt ?? Date.now()).toISOString(), + webbrainVersion: String(options.webbrainVersion || 'unknown'), + warning: 'Contains plaintext provider API keys and other sensitive Settings data. Store securely.', + settings, + }; +} + +export function parseConfigImport(json) { + const text = String(json || ''); + if (text.length > MAX_CONFIG_IMPORT_CHARS) throw new Error('Configuration JSON is too large.'); + if (!text.trim()) throw new Error('Paste configuration JSON or use /import --file.'); + + let parsed; + try { + parsed = JSON.parse(text); + } catch { + throw new Error('Configuration is not valid JSON.'); + } + if (!isPlainObject(parsed) || parsed.schema !== CONFIG_SCHEMA) { + throw new Error(`Expected a ${CONFIG_SCHEMA} export.`); + } + if (!isPlainObject(parsed.settings)) { + throw new Error('Configuration settings must be a JSON object.'); + } + + return { + settings: normalizeSettings(parsed.settings, { strict: true }), + ignoredKeys: Object.keys(parsed.settings).filter((key) => !CONFIG_STORAGE_KEY_SET.has(key)), + sourceVersion: typeof parsed.webbrainVersion === 'string' ? parsed.webbrainVersion : '', + }; +} diff --git a/src/firefox/src/ui/locales/ar.js b/src/firefox/src/ui/locales/ar.js index 8b0b8380a..db71a2692 100644 --- a/src/firefox/src/ui/locales/ar.js +++ b/src/firefox/src/ui/locales/ar.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'أقصى', 'st.providers.compat.value.system': 'نظام', 'st.providers.compat.value.developer': 'مطوّر', + "sp.slash.export_config": "تصدير جميع قيم الإعدادات، بما فيها مفاتيح API لموفري الخدمة", + "sp.slash.import_config": "استيراد لقطة إعداد WebBrain بصيغة JSON", + "sp.slash.import_config_file": "اختيار ملف إعداد WebBrain بصيغة JSON", + "sp.export_config.done": "تم تصدير الإعداد كنص JSON غير مشفر. يتضمن مفاتيح API لموفري الخدمة وبيانات الملف الشخصي وذاكرة المستخدم وقيم إعدادات حساسة أخرى. خزّنه بأمان.", + "sp.export_config.error": "تعذر تصدير الإعداد: {error}", + "sp.import_config.done": "تم استيراد {count} من قيم الإعدادات. مفاتيح موفري الخدمة والإعداد مفعّلة الآن.", + "sp.import_config.error": "تعذر استيراد الإعداد: {error}", }; diff --git a/src/firefox/src/ui/locales/en.js b/src/firefox/src/ui/locales/en.js index 20cbbe9cf..2d6accf5f 100644 --- a/src/firefox/src/ui/locales/en.js +++ b/src/firefox/src/ui/locales/en.js @@ -771,4 +771,11 @@ export default { "sp.export_traces.no_conversation": "Nothing to export yet — start a conversation first.", "sp.export_traces.partial": "Tool chain exported, but some turn events could not be read.", "sp.export_traces.truncated": "Tool chain exported. Older turns may be missing if this conversation has many traced runs.", + "sp.slash.export_config": "Export all Settings values, including provider API keys", + "sp.slash.import_config": "Import a WebBrain configuration JSON snapshot", + "sp.slash.import_config_file": "Choose a WebBrain configuration JSON file", + "sp.export_config.done": "Configuration exported as plaintext JSON. It includes provider API keys, profile data, user memory, and other sensitive Settings values. Store it securely.", + "sp.export_config.error": "Couldn't export configuration: {error}", + "sp.import_config.done": "Imported {count} Settings values. Provider keys and configuration are active.", + "sp.import_config.error": "Couldn't import configuration: {error}", }; diff --git a/src/firefox/src/ui/locales/es.js b/src/firefox/src/ui/locales/es.js index 2084ce3b6..ecb2c89a7 100644 --- a/src/firefox/src/ui/locales/es.js +++ b/src/firefox/src/ui/locales/es.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Máx', 'st.providers.compat.value.system': 'Sistema', 'st.providers.compat.value.developer': 'Desarrollador', + "sp.slash.export_config": "Exportar todos los valores de Configuración, incluidas las claves API de proveedores", + "sp.slash.import_config": "Importar una instantánea JSON de configuración de WebBrain", + "sp.slash.import_config_file": "Elegir un archivo JSON de configuración de WebBrain", + "sp.export_config.done": "La configuración se exportó como JSON de texto sin formato. Incluye claves API de proveedores, datos de perfil, memoria del usuario y otros valores sensibles. Guárdela de forma segura.", + "sp.export_config.error": "No se pudo exportar la configuración: {error}", + "sp.import_config.done": "Se importaron {count} valores de Configuración. Las claves de proveedores y la configuración están activas.", + "sp.import_config.error": "No se pudo importar la configuración: {error}", }; diff --git a/src/firefox/src/ui/locales/fr.js b/src/firefox/src/ui/locales/fr.js index d0de712ed..d22b8b1ee 100644 --- a/src/firefox/src/ui/locales/fr.js +++ b/src/firefox/src/ui/locales/fr.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Max', 'st.providers.compat.value.system': 'Système', 'st.providers.compat.value.developer': 'Développeur', + "sp.slash.export_config": "Exporter tous les paramètres, y compris les clés API des fournisseurs", + "sp.slash.import_config": "Importer un instantané JSON de configuration WebBrain", + "sp.slash.import_config_file": "Choisir un fichier JSON de configuration WebBrain", + "sp.export_config.done": "La configuration a été exportée en JSON non chiffré. Elle contient les clés API des fournisseurs, les données de profil, la mémoire utilisateur et d’autres paramètres sensibles. Conservez-la en lieu sûr.", + "sp.export_config.error": "Impossible d’exporter la configuration : {error}", + "sp.import_config.done": "{count} paramètres ont été importés. Les clés des fournisseurs et la configuration sont actives.", + "sp.import_config.error": "Impossible d’importer la configuration : {error}", }; diff --git a/src/firefox/src/ui/locales/he.js b/src/firefox/src/ui/locales/he.js index f5ab4944b..7c599e310 100644 --- a/src/firefox/src/ui/locales/he.js +++ b/src/firefox/src/ui/locales/he.js @@ -722,4 +722,11 @@ export default { 'st.providers.compat.value.max': 'מקסימום', 'st.providers.compat.value.system': 'מערכת', 'st.providers.compat.value.developer': 'מפתח', + "sp.slash.export_config": "ייצוא כל ערכי ההגדרות, כולל מפתחות API של ספקים", + "sp.slash.import_config": "ייבוא תמונת מצב של תצורת WebBrain בקובץ JSON", + "sp.slash.import_config_file": "בחירת קובץ JSON של תצורת WebBrain", + "sp.export_config.done": "התצורה יוצאה כ-JSON בטקסט גלוי. היא כוללת מפתחות API של ספקים, נתוני פרופיל, זיכרון משתמש וערכי הגדרות רגישים נוספים. יש לשמור אותה באופן מאובטח.", + "sp.export_config.error": "לא ניתן לייצא את התצורה: {error}", + "sp.import_config.done": "יובאו {count} ערכי הגדרות. מפתחות הספקים והתצורה פעילים כעת.", + "sp.import_config.error": "לא ניתן לייבא את התצורה: {error}", }; diff --git a/src/firefox/src/ui/locales/id.js b/src/firefox/src/ui/locales/id.js index b83582b87..9d3485c3f 100644 --- a/src/firefox/src/ui/locales/id.js +++ b/src/firefox/src/ui/locales/id.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Maks', 'st.providers.compat.value.system': 'Sistem', 'st.providers.compat.value.developer': 'Pengembang', + "sp.slash.export_config": "Ekspor semua nilai Pengaturan, termasuk kunci API penyedia", + "sp.slash.import_config": "Impor snapshot JSON konfigurasi WebBrain", + "sp.slash.import_config_file": "Pilih file JSON konfigurasi WebBrain", + "sp.export_config.done": "Konfigurasi diekspor sebagai JSON teks biasa. File ini berisi kunci API penyedia, data profil, memori pengguna, dan nilai Pengaturan sensitif lainnya. Simpan dengan aman.", + "sp.export_config.error": "Tidak dapat mengekspor konfigurasi: {error}", + "sp.import_config.done": "Berhasil mengimpor {count} nilai Pengaturan. Kunci penyedia dan konfigurasi sudah aktif.", + "sp.import_config.error": "Tidak dapat mengimpor konfigurasi: {error}", }; diff --git a/src/firefox/src/ui/locales/ja.js b/src/firefox/src/ui/locales/ja.js index 4dbe792e1..35f6ee102 100644 --- a/src/firefox/src/ui/locales/ja.js +++ b/src/firefox/src/ui/locales/ja.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': '最大', 'st.providers.compat.value.system': 'システム', 'st.providers.compat.value.developer': '開発者', + "sp.slash.export_config": "プロバイダーの API キーを含むすべての設定値をエクスポート", + "sp.slash.import_config": "WebBrain 設定の JSON スナップショットをインポート", + "sp.slash.import_config_file": "WebBrain 設定の JSON ファイルを選択", + "sp.export_config.done": "設定を平文の JSON としてエクスポートしました。プロバイダーの API キー、プロフィール情報、ユーザーメモリ、その他の機密設定が含まれます。安全に保管してください。", + "sp.export_config.error": "設定をエクスポートできませんでした: {error}", + "sp.import_config.done": "{count} 件の設定値をインポートしました。プロバイダーキーと設定は有効です。", + "sp.import_config.error": "設定をインポートできませんでした: {error}", }; diff --git a/src/firefox/src/ui/locales/ko.js b/src/firefox/src/ui/locales/ko.js index fa9cc7b2b..d1b5f663f 100644 --- a/src/firefox/src/ui/locales/ko.js +++ b/src/firefox/src/ui/locales/ko.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': '최대', 'st.providers.compat.value.system': '시스템', 'st.providers.compat.value.developer': '개발자', + "sp.slash.export_config": "공급자 API 키를 포함한 모든 설정 값 내보내기", + "sp.slash.import_config": "WebBrain 구성 JSON 스냅샷 가져오기", + "sp.slash.import_config_file": "WebBrain 구성 JSON 파일 선택", + "sp.export_config.done": "구성을 일반 텍스트 JSON으로 내보냈습니다. 공급자 API 키, 프로필 데이터, 사용자 메모리 및 기타 민감한 설정 값이 포함됩니다. 안전하게 보관하세요.", + "sp.export_config.error": "구성을 내보낼 수 없습니다: {error}", + "sp.import_config.done": "설정 값 {count}개를 가져왔습니다. 공급자 키와 구성이 활성화되었습니다.", + "sp.import_config.error": "구성을 가져올 수 없습니다: {error}", }; diff --git a/src/firefox/src/ui/locales/ms.js b/src/firefox/src/ui/locales/ms.js index 1b80bdeeb..206e88d76 100644 --- a/src/firefox/src/ui/locales/ms.js +++ b/src/firefox/src/ui/locales/ms.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Maks', 'st.providers.compat.value.system': 'Sistem', 'st.providers.compat.value.developer': 'Pembangun', + "sp.slash.export_config": "Eksport semua nilai Tetapan, termasuk kunci API penyedia", + "sp.slash.import_config": "Import petikan JSON konfigurasi WebBrain", + "sp.slash.import_config_file": "Pilih fail JSON konfigurasi WebBrain", + "sp.export_config.done": "Konfigurasi dieksport sebagai JSON teks biasa. Ia mengandungi kunci API penyedia, data profil, memori pengguna dan nilai Tetapan sensitif lain. Simpan dengan selamat.", + "sp.export_config.error": "Tidak dapat mengeksport konfigurasi: {error}", + "sp.import_config.done": "{count} nilai Tetapan telah diimport. Kunci penyedia dan konfigurasi kini aktif.", + "sp.import_config.error": "Tidak dapat mengimport konfigurasi: {error}", }; diff --git a/src/firefox/src/ui/locales/pl.js b/src/firefox/src/ui/locales/pl.js index 29836fd3b..eae4a36d1 100644 --- a/src/firefox/src/ui/locales/pl.js +++ b/src/firefox/src/ui/locales/pl.js @@ -729,4 +729,11 @@ export default { "st.display.help_improve.label": "Pomóż ulepszać WebBrain", "st.display.help_improve.desc_html": "Zezwól na przechowywanie wybranych interakcji z WebBrain Cloud i używanie ich do oceny, ulepszania, dostrajania i trenowania. Domyślnie włączone. Wyłącz, aby przyszłe interakcje Cloud nie były używane do tych celów. WebBrain nigdy nie zbiera żądań do modeli lokalnych ani żądań wysyłanych bezpośrednio przy użyciu własnych danych logowania API. Polityka prywatności →", "st.providers.webbrain_data_use.body": "Darmowe dzienne korzystanie z WebBrain Cloud jest wliczone. Gdy opcja „Pomóż ulepszać WebBrain” jest domyślnie włączona, wybrane rozmowy Cloud mogą być przechowywane i używane do oceny, ulepszania, dostrajania i trenowania. Wyłącz ją w Ogólne, aby wykluczyć przyszłe interakcje Cloud z tych zastosowań. WebBrain nigdy nie zbiera żądań do modeli lokalnych ani żądań wysyłanych bezpośrednio przy użyciu własnych danych logowania API. {privacyLink}. Aby korzystać więcej, subskrybuj na {subscribeLink}. Zarządzaj rozliczeniami na {accountLink}.", + "sp.slash.export_config": "Eksportuj wszystkie ustawienia, w tym klucze API dostawców", + "sp.slash.import_config": "Importuj migawkę konfiguracji WebBrain w formacie JSON", + "sp.slash.import_config_file": "Wybierz plik JSON konfiguracji WebBrain", + "sp.export_config.done": "Konfigurację wyeksportowano jako niezaszyfrowany JSON. Zawiera klucze API dostawców, dane profilu, pamięć użytkownika i inne poufne ustawienia. Przechowuj ją bezpiecznie.", + "sp.export_config.error": "Nie udało się wyeksportować konfiguracji: {error}", + "sp.import_config.done": "Zaimportowano ustawienia: {count}. Klucze dostawców i konfiguracja są aktywne.", + "sp.import_config.error": "Nie udało się zaimportować konfiguracji: {error}", }; diff --git a/src/firefox/src/ui/locales/ru.js b/src/firefox/src/ui/locales/ru.js index 9f6583352..5582613b2 100644 --- a/src/firefox/src/ui/locales/ru.js +++ b/src/firefox/src/ui/locales/ru.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Макс.', 'st.providers.compat.value.system': 'Система', 'st.providers.compat.value.developer': 'Разработчик', + "sp.slash.export_config": "Экспортировать все настройки, включая API-ключи провайдеров", + "sp.slash.import_config": "Импортировать JSON-снимок конфигурации WebBrain", + "sp.slash.import_config_file": "Выбрать JSON-файл конфигурации WebBrain", + "sp.export_config.done": "Конфигурация экспортирована как незашифрованный JSON. Она содержит API-ключи провайдеров, данные профиля, пользовательскую память и другие конфиденциальные настройки. Храните её безопасно.", + "sp.export_config.error": "Не удалось экспортировать конфигурацию: {error}", + "sp.import_config.done": "Импортировано настроек: {count}. Ключи провайдеров и конфигурация активны.", + "sp.import_config.error": "Не удалось импортировать конфигурацию: {error}", }; diff --git a/src/firefox/src/ui/locales/th.js b/src/firefox/src/ui/locales/th.js index b6ee94cec..ead8e30b0 100644 --- a/src/firefox/src/ui/locales/th.js +++ b/src/firefox/src/ui/locales/th.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'สูงสุด', 'st.providers.compat.value.system': 'ระบบ', 'st.providers.compat.value.developer': 'นักพัฒนา', + "sp.slash.export_config": "ส่งออกค่าการตั้งค่าทั้งหมด รวมถึงคีย์ API ของผู้ให้บริการ", + "sp.slash.import_config": "นำเข้าสแนปช็อต JSON การกำหนดค่า WebBrain", + "sp.slash.import_config_file": "เลือกไฟล์ JSON การกำหนดค่า WebBrain", + "sp.export_config.done": "ส่งออกการกำหนดค่าเป็น JSON แบบข้อความธรรมดาแล้ว ไฟล์นี้มีคีย์ API ของผู้ให้บริการ ข้อมูลโปรไฟล์ หน่วยความจำผู้ใช้ และค่าการตั้งค่าที่ละเอียดอ่อนอื่น ๆ โปรดเก็บไว้อย่างปลอดภัย", + "sp.export_config.error": "ส่งออกการกำหนดค่าไม่ได้: {error}", + "sp.import_config.done": "นำเข้าค่าการตั้งค่า {count} รายการแล้ว คีย์ผู้ให้บริการและการกำหนดค่ามีผลแล้ว", + "sp.import_config.error": "นำเข้าการกำหนดค่าไม่ได้: {error}", }; diff --git a/src/firefox/src/ui/locales/tl.js b/src/firefox/src/ui/locales/tl.js index e2be27149..c0a736a6f 100644 --- a/src/firefox/src/ui/locales/tl.js +++ b/src/firefox/src/ui/locales/tl.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Max', 'st.providers.compat.value.system': 'System', 'st.providers.compat.value.developer': 'Developer', + "sp.slash.export_config": "I-export ang lahat ng value ng Settings, kasama ang mga provider API key", + "sp.slash.import_config": "Mag-import ng WebBrain configuration JSON snapshot", + "sp.slash.import_config_file": "Pumili ng WebBrain configuration JSON file", + "sp.export_config.done": "Na-export ang configuration bilang plaintext JSON. Kasama rito ang mga provider API key, profile data, user memory, at iba pang sensitibong Settings value. Itago ito nang ligtas.", + "sp.export_config.error": "Hindi ma-export ang configuration: {error}", + "sp.import_config.done": "Na-import ang {count} Settings value. Aktibo na ang mga provider key at configuration.", + "sp.import_config.error": "Hindi ma-import ang configuration: {error}", }; diff --git a/src/firefox/src/ui/locales/tr.js b/src/firefox/src/ui/locales/tr.js index 8d7b149fe..3fe9026d0 100644 --- a/src/firefox/src/ui/locales/tr.js +++ b/src/firefox/src/ui/locales/tr.js @@ -768,4 +768,11 @@ export default { 'st.providers.compat.value.max': 'Maks', 'st.providers.compat.value.system': 'Sistem', 'st.providers.compat.value.developer': 'Geliştirici', + "sp.slash.export_config": "Sağlayıcı API anahtarları dahil tüm Ayarlar değerlerini dışa aktar", + "sp.slash.import_config": "Bir WebBrain yapılandırma JSON anlık görüntüsünü içe aktar", + "sp.slash.import_config_file": "Bir WebBrain yapılandırma JSON dosyası seç", + "sp.export_config.done": "Yapılandırma düz metin JSON olarak dışa aktarıldı. Sağlayıcı API anahtarlarını, profil verilerini, kullanıcı belleğini ve diğer hassas Ayarlar değerlerini içerir. Güvenli biçimde saklayın.", + "sp.export_config.error": "Yapılandırma dışa aktarılamadı: {error}", + "sp.import_config.done": "{count} Ayarlar değeri içe aktarıldı. Sağlayıcı anahtarları ve yapılandırma etkin.", + "sp.import_config.error": "Yapılandırma içe aktarılamadı: {error}", }; diff --git a/src/firefox/src/ui/locales/uk.js b/src/firefox/src/ui/locales/uk.js index 888040414..7f4ecfd0b 100644 --- a/src/firefox/src/ui/locales/uk.js +++ b/src/firefox/src/ui/locales/uk.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': 'Макс.', 'st.providers.compat.value.system': 'Система', 'st.providers.compat.value.developer': 'Розробник', + "sp.slash.export_config": "Експортувати всі налаштування, включно з API-ключами провайдерів", + "sp.slash.import_config": "Імпортувати JSON-знімок конфігурації WebBrain", + "sp.slash.import_config_file": "Вибрати JSON-файл конфігурації WebBrain", + "sp.export_config.done": "Конфігурацію експортовано як незашифрований JSON. Вона містить API-ключі провайдерів, дані профілю, пам’ять користувача та інші конфіденційні налаштування. Зберігайте її безпечно.", + "sp.export_config.error": "Не вдалося експортувати конфігурацію: {error}", + "sp.import_config.done": "Імпортовано налаштувань: {count}. Ключі провайдерів і конфігурація активні.", + "sp.import_config.error": "Не вдалося імпортувати конфігурацію: {error}", }; diff --git a/src/firefox/src/ui/locales/zh.js b/src/firefox/src/ui/locales/zh.js index 9acaf3f23..da4ac3cdc 100644 --- a/src/firefox/src/ui/locales/zh.js +++ b/src/firefox/src/ui/locales/zh.js @@ -769,4 +769,11 @@ export default { 'st.providers.compat.value.max': '最大', 'st.providers.compat.value.system': '系统', 'st.providers.compat.value.developer': '开发者', + "sp.slash.export_config": "导出所有设置值,包括提供商 API 密钥", + "sp.slash.import_config": "导入 WebBrain 配置 JSON 快照", + "sp.slash.import_config_file": "选择 WebBrain 配置 JSON 文件", + "sp.export_config.done": "配置已导出为明文 JSON。其中包含提供商 API 密钥、个人资料、用户记忆和其他敏感设置。请安全保存。", + "sp.export_config.error": "无法导出配置:{error}", + "sp.import_config.done": "已导入 {count} 个设置值。提供商密钥和配置现已生效。", + "sp.import_config.error": "无法导入配置:{error}", }; diff --git a/src/firefox/src/ui/settings.js b/src/firefox/src/ui/settings.js index 30ba9bb40..0c974027e 100644 --- a/src/firefox/src/ui/settings.js +++ b/src/firefox/src/ui/settings.js @@ -35,7 +35,7 @@ import { // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '24.0.2'; +const EXT_VERSION = '24.1.0'; const providersContainer = document.getElementById('providers'); const displaySettings = document.getElementById('display-settings'); diff --git a/src/firefox/src/ui/sidepanel.js b/src/firefox/src/ui/sidepanel.js index 59f02fa91..62e99bef7 100644 --- a/src/firefox/src/ui/sidepanel.js +++ b/src/firefox/src/ui/sidepanel.js @@ -408,12 +408,23 @@ const SLASH_COMMANDS = [ }, { value: '/export', - usage: '/export [--traces]', + usage: '/export [--traces | --config]', descriptionKey: 'sp.slash.export', action: 'conversation', outOfBand: true, options: [ - { value: '--traces', descriptionKey: 'sp.slash.export_traces', action: 'traces', outOfBand: true, disallowPayload: true }, + { value: '--traces', descriptionKey: 'sp.slash.export_traces', action: 'traces', outOfBand: true, disallowPayload: true, exclusiveGroup: 'export-format' }, + { value: '--config', descriptionKey: 'sp.slash.export_config', action: 'config', outOfBand: true, disallowPayload: true, exclusiveGroup: 'export-format' }, + ], + }, + { + value: '/import', + usage: '/import | /import --file', + descriptionKey: 'sp.slash.import_config', + action: 'json', + acceptsPayload: true, + options: [ + { value: '--file', descriptionKey: 'sp.slash.import_config_file', action: 'file', disallowPayload: true }, ], }, { value: '/profile', usage: '/profile', descriptionKey: 'sp.slash.profile', action: 'toggle' }, @@ -3970,6 +3981,38 @@ function resolvePendingPermissionPromptsForTab(tabId) { return resolved; } +async function importConfigurationJson(json, tabId) { + try { + const res = await sendToBackground('import_config', { json }); + await loadProviders(); + if (currentTabId === tabId) { + addPersistentSlashMessage(t('sp.import_config.done', { count: res?.settingCount || 0 })); + } + } catch (error) { + if (currentTabId === tabId) { + addPersistentSlashMessage(t('sp.import_config.error', { error: error?.message || 'invalid configuration' })); + } + } +} + +function requestConfigurationFile(tabId) { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json,application/json'; + input.addEventListener('change', () => { + const file = input.files?.[0]; + if (!file) return; + file.text() + .then((json) => importConfigurationJson(json, tabId)) + .catch((error) => { + if (currentTabId === tabId) { + addPersistentSlashMessage(t('sp.import_config.error', { error: error?.message || 'file read failed' })); + } + }); + }, { once: true }); + input.click(); +} + async function parseSlashCommands(text, tabId = currentTabId) { const invocation = parseSlashInvocation(text); if (!invocation) return text; @@ -4104,6 +4147,39 @@ async function parseSlashCommands(text, tabId = currentTabId) { return ''; } + if (command.value === '/import' && action === 'file') { + requestConfigurationFile(tabId); + return ''; + } + + if (command.value === '/import' && action === 'json') { + await importConfigurationJson(payload, tabId); + return ''; + } + + if (command.value === '/export' && action === 'config') { + try { + const res = await sendToBackground('export_config', { locale: getLocale() }); + if (!res?.ok || !res.json) throw new Error(res?.error || 'empty configuration'); + const blob = new Blob([res.json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `webbrain-config-${Date.now()}.json`; + document.body.appendChild(a); + try { + a.click(); + } finally { + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 7000); + } + addPersistentSlashMessage(t('sp.export_config.done')); + } catch (error) { + addPersistentSlashMessage(t('sp.export_config.error', { error: error?.message || 'unknown error' })); + } + return ''; + } + if (command.value === '/export' && action === 'traces') { let res; try { diff --git a/test/run.js b/test/run.js index edab3f727..db8bbb81e 100644 --- a/test/run.js +++ b/test/run.js @@ -196,6 +196,15 @@ const { tracesToMarkdown: tracesToMarkdownFx } = await import( 'file://' + path.join(ROOT, 'src/firefox/src/agent/trace-export.js').replace(/\\/g, '/') ); +// config-transfer.js is the pure schema/allowlist boundary used by the +// /export --config and /import slash-command handlers. +const ConfigTransferCh = await import( + 'file://' + path.join(ROOT, 'src/chrome/src/config-transfer.js').replace(/\\/g, '/') +); +const ConfigTransferFx = await import( + 'file://' + path.join(ROOT, 'src/firefox/src/config-transfer.js').replace(/\\/g, '/') +); + // network-tools.js references chrome.* inside a try/catch at module load, so // it imports cleanly under Node — the storage init silently no-ops and // validateFetchUrl / registrableDomain are pure functions. @@ -2041,7 +2050,7 @@ test('/export --traces is wired in both side panels and backgrounds', () => { const bg = fs.readFileSync(path.join(ROOT, bgRel), 'utf8'); assert.match( panel, - /usage: '\/export \[--traces\]'[\s\S]*?value: '--traces'[\s\S]*?action: 'traces'[\s\S]*?outOfBand: true/, + /usage: '\/export \[--traces \| --config\]'[\s\S]*?value: '--traces'[\s\S]*?action: 'traces'[\s\S]*?outOfBand: true/, `${label}: slash metadata should advertise /export --traces`, ); assert.match( @@ -2074,6 +2083,89 @@ test('/export --traces is wired in both side panels and backgrounds', () => { } }); +test('config transfer exports and restores Settings values including provider keys', () => { + const stored = { + wbLocale: 'tr', + strictSecretMode: true, + profileEnabled: true, + profileText: 'Use the test profile', + capsolverApiKey: 'capsolver-secret', + providers: { + openai: { type: 'openai', apiKey: 'provider-secret', model: 'gpt-test', configured: true }, + webbrain_cloud: { type: 'openai', deviceGuid: 'must-stay-on-device' }, + }, + activeProvider: 'openai', + visionModel: { baseUrl: 'https://vision.example/v1', apiKey: 'vision-secret', model: 'vision-test' }, + transcriptionModel: { baseUrl: 'https://audio.example/v1', apiKey: 'audio-secret', model: 'whisper-test' }, + wb_permissions: [{ capability: 'click', host: 'example.com' }], + wb_user_memory_v1: { version: 1, records: [{ id: 'mem_1', text: 'Prefer concise answers', kind: 'preference' }] }, + cloudCostSpentUsd: 8.5, + profileSyncToken: 'device-session-secret', + webbrainDeviceGuid: 'device-guid', + }; + const options = { exportedAt: '2026-07-16T12:00:00.000Z', webbrainVersion: '24.0.2', locale: 'tr' }; + + const chromeExport = ConfigTransferCh.createConfigExport(stored, options); + const firefoxExport = ConfigTransferFx.createConfigExport(stored, options); + assert.deepEqual(firefoxExport, chromeExport, 'Chrome and Firefox config schemas should remain identical'); + assert.equal(chromeExport.schema, 'webbrain-config/1'); + assert.equal(chromeExport.settings.providers.openai.apiKey, 'provider-secret'); + assert.equal(chromeExport.settings.visionModel.apiKey, 'vision-secret'); + assert.equal(chromeExport.settings.transcriptionModel.apiKey, 'audio-secret'); + assert.equal(chromeExport.settings.capsolverApiKey, 'capsolver-secret'); + assert.equal(chromeExport.settings.profileText, 'Use the test profile'); + assert.equal(chromeExport.settings.wb_user_memory_v1.records.length, 1); + assert.equal(chromeExport.settings.providers.webbrain_cloud.deviceGuid, undefined, 'device identity must not be portable'); + assert.equal(chromeExport.settings.cloudCostSpentUsd, undefined, 'spend counters are runtime state, not config'); + assert.equal(chromeExport.settings.profileSyncToken, undefined, 'Cloud Sync sessions must not be exported'); + assert.equal(chromeExport.settings.webbrainDeviceGuid, undefined, 'device GUID must not be exported'); + assert.match(chromeExport.warning, /plaintext provider API keys/i); + + const imported = ConfigTransferCh.parseConfigImport(JSON.stringify(chromeExport)); + assert.equal(imported.settings.providers.openai.apiKey, 'provider-secret'); + assert.equal(imported.settings.activeProvider, 'openai'); + assert.equal(imported.settings.themeMode, 'system', 'missing Settings values should restore their product defaults'); + assert.equal(Object.keys(imported.settings).length, ConfigTransferCh.CONFIG_STORAGE_KEYS.length); +}); + +test('config transfer validates schema, size, containers, and unknown keys', () => { + assert.throws(() => ConfigTransferCh.parseConfigImport('{nope'), /valid JSON/); + assert.throws(() => ConfigTransferCh.parseConfigImport('{}'), /webbrain-config\/1/); + assert.throws( + () => ConfigTransferCh.parseConfigImport(JSON.stringify({ schema: 'webbrain-config/1', settings: { providers: [] } })), + /providers/, + ); + assert.throws( + () => ConfigTransferCh.parseConfigImport(' '.repeat(ConfigTransferCh.MAX_CONFIG_IMPORT_CHARS + 1)), + /too large/, + ); + + const parsed = ConfigTransferCh.parseConfigImport(JSON.stringify({ + schema: 'webbrain-config/1', + settings: { verboseMode: true, futureSetting: 'ignored' }, + })); + assert.equal(parsed.settings.verboseMode, true); + assert.deepEqual(parsed.ignoredKeys, ['futureSetting']); +}); + +test('/export --config and /import JSON or --file are wired in both browsers', () => { + for (const [label, prefix] of [ + ['chrome', 'src/chrome'], + ['firefox', 'src/firefox'], + ]) { + const panel = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/sidepanel.js'), 'utf8'); + const bg = fs.readFileSync(path.join(ROOT, prefix, 'src/background.js'), 'utf8'); + assert.match(panel, /value: '--config'[\s\S]*?action: 'config'[\s\S]*?exclusiveGroup: 'export-format'/, `${label}: /export --config metadata missing`); + assert.match(panel, /usage: '\/import \| \/import --file'[\s\S]*?value: '--file'[\s\S]*?action: 'file'/, `${label}: /import --file metadata missing`); + assert.match(panel, /command\.value === '\/export' && action === 'config'[\s\S]*?sendToBackground\('export_config', \{ locale: getLocale\(\) \}\)/, `${label}: config export handler missing`); + assert.match(panel, /command\.value === '\/import' && action === 'json'[\s\S]*?importConfigurationJson\(payload, tabId\)/, `${label}: inline config import handler missing`); + assert.match(panel, /command\.value === '\/import' && action === 'file'[\s\S]*?requestConfigurationFile\(tabId\)/, `${label}: file config import handler missing`); + assert.match(panel, /input\.accept = '\.json,application\/json'[\s\S]*?file\.text\(\)/, `${label}: file picker should read JSON files`); + assert.match(bg, /case 'export_config':[\s\S]*?createConfigExport\(stored/, `${label}: background config export missing`); + assert.match(bg, /case 'import_config':[\s\S]*?parseConfigImport\(msg\.json\)[\s\S]*?storage\.local\.set\(imported\.settings\)[\s\S]*?providerManager\.load\(\)/, `${label}: background config import and live provider reload missing`); + } +}); + test('trace record and JSON exports carry WebBrain version metadata', () => { for (const [label, prefix, runtimeName] of [ ['chrome', 'src/chrome', 'chrome'],