From ba0af7ebf0f29c74d9f09bd74fc222f61ea9d10e Mon Sep 17 00:00:00 2001 From: coso Date: Thu, 25 Dec 2025 21:30:43 +0800 Subject: [PATCH] =?UTF-8?q?v0.19.0:=20=E6=B5=8F=E8=A7=88=E5=99=A8=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E5=99=A8=20-=20macOS=20=E9=BB=98=E8=AE=A4=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E6=8E=A5=E7=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 浏览器拦截器模块,支持 macOS 系统默认浏览器接管 - 自动拦截系统 URL 请求并转发到 AI 客户端 - URL 历史记录管理与复制功能 - 系统状态监控面板 - 支持 Chrome/Firefox/Safari 等多浏览器检测与恢复 技术改进: - 使用 tauri-plugin-deep-link 实现 URL scheme 注册 - 使用 tauri-plugin-single-instance 防止多实例 - Launch Services API 集成实现浏览器管理 - 修复速率限制测试的不稳定性问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 2 + package-lock.json | 83 ++- package.json | 9 +- src-tauri/Cargo.lock | 487 +++++++++++- src-tauri/Cargo.toml | 36 +- src-tauri/Info.plist | 26 + src-tauri/gen/schemas/acl-manifests.json | 2 +- src-tauri/gen/schemas/desktop-schema.json | 54 ++ src-tauri/gen/schemas/macOS-schema.json | 54 ++ src-tauri/src/browser_interceptor/config.rs | 266 +++++++ .../src/browser_interceptor/interceptor.rs | 460 ++++++++++++ src-tauri/src/browser_interceptor/mod.rs | 57 ++ .../notification_service.rs | 307 ++++++++ .../src/browser_interceptor/platform/linux.rs | 71 ++ .../src/browser_interceptor/platform/macos.rs | 429 +++++++++++ .../browser_interceptor/platform/windows.rs | 454 ++++++++++++ .../src/browser_interceptor/state_manager.rs | 504 +++++++++++++ .../src/browser_interceptor/url_manager.rs | 600 +++++++++++++++ .../src/commands/browser_interceptor_cmd.rs | 360 +++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 76 +- src-tauri/src/middleware/tests.rs | 10 +- src-tauri/src/services/live_sync.rs | 66 +- src-tauri/src/services/switch.rs | 98 ++- src-tauri/tauri.conf.json | 16 +- src/App.tsx | 8 + src/components/Sidebar.tsx | 12 +- src/components/clients/ProviderCard.tsx | 209 ++++-- src/components/clients/ProviderList.tsx | 21 +- src/components/tools/ToolsPage.tsx | 144 ++++ .../BrowserInterceptorTool.tsx | 215 ++++++ .../InterceptedUrlsPanel.tsx | 315 ++++++++ .../InterceptorConfigPanel.tsx | 694 ++++++++++++++++++ .../StatusControlPanel.tsx | 249 +++++++ .../browser-interceptor/SystemStatusPanel.tsx | 527 +++++++++++++ .../browser-interceptor/UrlHistoryPanel.tsx | 445 +++++++++++ src/components/ui/badge.tsx | 32 + src/components/ui/button.tsx | 39 + src/components/ui/card.tsx | 81 ++ src/components/ui/dialog.tsx | 133 ++++ src/components/ui/dropdown-menu.tsx | 151 ++++ src/components/ui/input.tsx | 23 + src/components/ui/label.tsx | 20 + src/components/ui/progress.tsx | 34 + src/components/ui/scroll-area.tsx | 19 + src/components/ui/select.tsx | 151 ++++ src/components/ui/switch.tsx | 40 + src/components/ui/tabs.tsx | 126 ++++ src/components/ui/textarea.tsx | 22 + src/components/ui/tooltip.tsx | 114 +++ src/hooks/useSwitch.ts | 34 +- src/lib/api/browserInterceptor.ts | 211 ++++++ src/lib/notifications.ts | 201 +++++ test_tool_versions.exe | Bin 306688 -> 0 bytes test_tool_versions.pdb | Bin 1699840 -> 0 bytes tsconfig.json | 4 +- 56 files changed, 8666 insertions(+), 136 deletions(-) create mode 100644 src-tauri/Info.plist create mode 100644 src-tauri/src/browser_interceptor/config.rs create mode 100644 src-tauri/src/browser_interceptor/interceptor.rs create mode 100644 src-tauri/src/browser_interceptor/mod.rs create mode 100644 src-tauri/src/browser_interceptor/notification_service.rs create mode 100644 src-tauri/src/browser_interceptor/platform/linux.rs create mode 100644 src-tauri/src/browser_interceptor/platform/macos.rs create mode 100644 src-tauri/src/browser_interceptor/platform/windows.rs create mode 100644 src-tauri/src/browser_interceptor/state_manager.rs create mode 100644 src-tauri/src/browser_interceptor/url_manager.rs create mode 100644 src-tauri/src/commands/browser_interceptor_cmd.rs create mode 100644 src/components/tools/ToolsPage.tsx create mode 100644 src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx create mode 100644 src/components/tools/browser-interceptor/InterceptedUrlsPanel.tsx create mode 100644 src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx create mode 100644 src/components/tools/browser-interceptor/StatusControlPanel.tsx create mode 100644 src/components/tools/browser-interceptor/SystemStatusPanel.tsx create mode 100644 src/components/tools/browser-interceptor/UrlHistoryPanel.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/lib/api/browserInterceptor.ts create mode 100644 src/lib/notifications.ts delete mode 100644 test_tool_versions.exe delete mode 100644 test_tool_versions.pdb diff --git a/.gitignore b/.gitignore index 15fce72a1..2b01a18b4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules/ # Build dist/ src-tauri/target/ +*.exe +*.pdb # IDE .vscode/ diff --git a/package-lock.json b/package-lock.json index 883b7749d..27a32e53b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "proxycast", "version": "0.18.0", "dependencies": { + "@fabianlars/tauri-plugin-oauth": "^2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", @@ -17,16 +18,18 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.3", - "@tauri-apps/api": "^2.0.0", + "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-shell": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.460.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^7.11.0", "sonner": "^2.0.7", - "tailwind-merge": "^2.5.4" + "tailwind-merge": "^2.6.0" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -1191,6 +1194,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fabianlars/tauri-plugin-oauth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fabianlars/tauri-plugin-oauth/-/tauri-plugin-oauth-2.0.0.tgz", + "integrity": "sha512-I1s08ZXrsFuYfNWusAcpLyiCfr5TCvaBrRuKfTG+XQrcaqnAcwjdWH0U5J9QWuMDLwCUMnVxdobtMJzPR8raxQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.0.3" + } + }, "node_modules/@fast-check/vitest": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@fast-check/vitest/-/vitest-0.2.4.tgz", @@ -4072,6 +4084,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -4177,6 +4202,16 @@ "node": ">=20" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5954,6 +5989,44 @@ } } }, + "node_modules/react-router": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", + "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", + "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", + "license": "MIT", + "dependencies": { + "react-router": "7.11.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -6156,6 +6229,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 128ecae4b..26a167e36 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.18.0", + "version": "0.19.0", "type": "module", "repository": { "type": "git", @@ -29,16 +29,19 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.3", - "@tauri-apps/api": "^2.0.0", + "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-shell": "^2.0.0", + "@fabianlars/tauri-plugin-oauth": "^2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.460.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^7.11.0", "sonner": "^2.0.7", - "tailwind-merge": "^2.5.4" + "tailwind-merge": "^2.6.0" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 87e231041..345462805 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -70,6 +70,26 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -109,6 +129,79 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -120,6 +213,24 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -142,6 +253,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -364,6 +481,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -382,6 +505,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "8.0.2" @@ -609,6 +745,15 @@ dependencies = [ "inout", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "cmake" version = "0.1.56" @@ -618,6 +763,35 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.10.0", + "block", + "cocoa-foundation", + "core-foundation 0.10.1", + "core-graphics", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-foundation 0.10.1", + "core-graphics-types", + "objc", +] + [[package]] name = "combine" version = "4.6.7" @@ -637,6 +811,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -742,6 +936,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -1000,6 +1200,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1125,6 +1334,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -1544,6 +1759,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1787,6 +2012,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1937,7 +2168,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.6.1", ] [[package]] @@ -2092,6 +2323,7 @@ dependencies = [ "byteorder-lite", "num-traits", "png", + "tiff", ] [[package]] @@ -2251,6 +2483,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + [[package]] name = "js-sys" version = "0.3.83" @@ -2437,6 +2675,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2691,6 +2938,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc2" version = "0.6.3" @@ -2971,6 +3227,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3226,6 +3492,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3258,6 +3535,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -3377,9 +3668,10 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", + "arboard", "async-stream", "async-trait", "axum", @@ -3387,6 +3679,7 @@ dependencies = [ "base64 0.22.1", "bytes", "chrono", + "cocoa", "dashmap", "dirs 5.0.1", "flate2", @@ -3395,6 +3688,7 @@ dependencies = [ "indexmap 2.12.1", "md5", "notify", + "objc", "once_cell", "open", "parking_lot", @@ -3413,8 +3707,10 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-autostart", + "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tempfile", "thiserror 1.0.69", "tiktoken-rs", @@ -3427,6 +3723,8 @@ dependencies = [ "url", "urlencoding", "uuid", + "winapi", + "windows 0.56.0", "zip", ] @@ -3770,6 +4068,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4528,7 +4836,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -4600,7 +4908,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -4697,6 +5005,27 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "tracing", + "url", + "windows-registry 0.5.3", + "windows-result 0.3.4", +] + [[package]] name = "tauri-plugin-dialog" version = "2.4.2" @@ -4758,6 +5087,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.17", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.9.2" @@ -4780,7 +5124,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -4806,7 +5150,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -4932,6 +5276,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tiktoken-rs" version = "0.6.0" @@ -4979,6 +5334,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -5770,10 +6134,10 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -5794,10 +6158,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ "thiserror 2.0.17", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -5844,6 +6214,16 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -5866,14 +6246,26 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -5885,8 +6277,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -5903,6 +6295,17 @@ dependencies = [ "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -5914,6 +6317,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -5947,6 +6361,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -5958,6 +6383,15 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -6397,7 +6831,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -6424,6 +6858,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "yoke" version = "0.8.1" @@ -6454,8 +6905,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "enumflags2", "event-listener", "futures-core", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 410c96a5d..c50a1dccf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.18.0" +version = "0.19.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" @@ -19,6 +19,7 @@ tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-shell = "2" tauri-plugin-autostart = "2" tauri-plugin-dialog = "2" +tauri-plugin-single-instance = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } @@ -61,6 +62,39 @@ open = "5" url = "2" once_cell = "1" tokio-util = "0.7" +arboard = "3" + +# Platform specific dependencies for browser interceptor + +# Windows specific dependencies for browser interceptor +[target.'cfg(windows)'.dependencies] +windows = { version = "0.56", features = [ + "Win32_Foundation", + "Win32_System_Registry", + "Win32_System_Threading", + "Win32_System_ProcessStatus", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_Diagnostics_ToolHelp", + "Win32_Security", +] } +winapi = { version = "0.3", features = [ + "winuser", + "winreg", + "processthreadsapi", + "handleapi", + "shellapi", + "psapi", + "tlhelp32", +] } + +# macOS specific dependencies for browser interceptor +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.26" +objc = "0.2" +tauri-plugin-deep-link = "2" [dev-dependencies] proptest = "1" diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 000000000..ef33f4c57 --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleURLTypes + + + CFBundleURLName + Web URL + CFBundleURLSchemes + + http + https + + + + CFBundleURLName + ProxyCast URL + CFBundleURLSchemes + + proxycast + + + + + diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 7f023fd90..17f497fad 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file +{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"deep-link":{"default_permission":{"identifier":"default","description":"Allows reading the opened deep link via the get_current command","permissions":["allow-get-current"]},"permissions":{"allow-get-current":{"identifier":"allow-get-current","description":"Enables the get_current command without any pre-configured scope.","commands":{"allow":["get_current"],"deny":[]}},"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"deny-get-current":{"identifier":"deny-get-current","description":"Denies the get_current command without any pre-configured scope.","commands":{"allow":[],"deny":["get_current"]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index 0a45d9c6b..0ad9f7d35 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -2396,6 +2396,60 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`", + "type": "string", + "const": "deep-link:default", + "markdownDescription": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`" + }, + { + "description": "Enables the get_current command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-get-current", + "markdownDescription": "Enables the get_current command without any pre-configured scope." + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Denies the get_current command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-get-current", + "markdownDescription": "Denies the get_current command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, { "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", "type": "string", diff --git a/src-tauri/gen/schemas/macOS-schema.json b/src-tauri/gen/schemas/macOS-schema.json index 0a45d9c6b..0ad9f7d35 100644 --- a/src-tauri/gen/schemas/macOS-schema.json +++ b/src-tauri/gen/schemas/macOS-schema.json @@ -2396,6 +2396,60 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`", + "type": "string", + "const": "deep-link:default", + "markdownDescription": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`" + }, + { + "description": "Enables the get_current command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-get-current", + "markdownDescription": "Enables the get_current command without any pre-configured scope." + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "deep-link:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Denies the get_current command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-get-current", + "markdownDescription": "Denies the get_current command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "deep-link:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, { "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", "type": "string", diff --git a/src-tauri/src/browser_interceptor/config.rs b/src-tauri/src/browser_interceptor/config.rs new file mode 100644 index 000000000..93d42e08a --- /dev/null +++ b/src-tauri/src/browser_interceptor/config.rs @@ -0,0 +1,266 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// 拦截器状态 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InterceptorState { + pub enabled: bool, + pub active_hooks: Vec, + pub intercepted_count: u32, + pub last_activity: Option>, + pub can_restore: bool, // 是否可以恢复正常状态 +} + +impl Default for InterceptorState { + fn default() -> Self { + Self { + enabled: false, + active_hooks: Vec::new(), + intercepted_count: 0, + last_activity: None, + can_restore: false, + } + } +} + +/// 被拦截的 URL 信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InterceptedUrl { + pub id: String, + pub url: String, + pub source_process: String, + pub timestamp: DateTime, + pub copied: bool, + pub opened_in_browser: bool, + pub dismissed: bool, +} + +impl InterceptedUrl { + pub fn new(url: String, source_process: String) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + url, + source_process, + timestamp: Utc::now(), + copied: false, + opened_in_browser: false, + dismissed: false, + } + } +} + +/// 指纹浏览器配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FingerprintBrowserConfig { + pub enabled: bool, + pub executable_path: String, + pub profile_path: String, + pub additional_args: Vec, +} + +impl Default for FingerprintBrowserConfig { + fn default() -> Self { + Self { + enabled: false, + executable_path: String::new(), + profile_path: String::new(), + additional_args: Vec::new(), + } + } +} + +/// 恢复机制配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecoveryConfig { + pub backup_system_state: bool, + pub emergency_recovery_hotkey: String, + pub auto_recovery_on_crash: bool, + pub recovery_timeout: u64, // 恢复操作超时时间(秒) +} + +impl Default for RecoveryConfig { + fn default() -> Self { + Self { + backup_system_state: true, + emergency_recovery_hotkey: "Ctrl+Alt+Shift+R".to_string(), + auto_recovery_on_crash: true, + recovery_timeout: 30, + } + } +} + +/// 浏览器拦截器配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BrowserInterceptorConfig { + pub enabled: bool, + pub target_processes: Vec, + pub url_patterns: Vec, + pub excluded_processes: Vec, + pub notification_enabled: bool, + pub auto_copy_to_clipboard: bool, + pub auto_launch_browser: bool, + pub restore_on_exit: bool, + pub temporary_disable_timeout: Option, // 临时禁用超时(秒) + pub fingerprint_browser: FingerprintBrowserConfig, + pub recovery: RecoveryConfig, +} + +impl Default for BrowserInterceptorConfig { + fn default() -> Self { + Self { + enabled: false, + target_processes: vec![ + "kiro".to_string(), + "kiro.exe".to_string(), + "cursor".to_string(), + "cursor.exe".to_string(), + "code".to_string(), + "code.exe".to_string(), + "Kiro".to_string(), // macOS 应用名称通常首字母大写 + "Cursor".to_string(), + "Visual Studio Code".to_string(), + ], + url_patterns: vec![ + "https://auth.*".to_string(), + "https://*/oauth/*".to_string(), + "https://accounts.google.com/*".to_string(), + "https://github.com/login/*".to_string(), + "https://login.microsoftonline.com/*".to_string(), + ], + excluded_processes: vec![ + "explorer.exe".to_string(), + "winlogon.exe".to_string(), + "system".to_string(), + "chrome.exe".to_string(), + "firefox.exe".to_string(), + "safari".to_string(), + "Safari".to_string(), // macOS Safari + "Google Chrome".to_string(), // macOS Chrome + "Firefox".to_string(), // macOS Firefox + ], + notification_enabled: true, + auto_copy_to_clipboard: true, + auto_launch_browser: false, + restore_on_exit: true, + temporary_disable_timeout: Some(300), // 5分钟 + fingerprint_browser: FingerprintBrowserConfig::default(), + recovery: RecoveryConfig::default(), + } + } +} + +impl BrowserInterceptorConfig { + /// 检查进程是否在目标列表中 + pub fn is_target_process(&self, process_name: &str) -> bool { + self.target_processes.iter().any(|pattern| { + // 支持简单的通配符匹配 + if pattern.contains('*') { + // TODO: 实现更复杂的模式匹配 + process_name.contains(&pattern.replace('*', "")) + } else { + process_name.eq_ignore_ascii_case(pattern) + } + }) + } + + /// 检查进程是否在排除列表中 + pub fn is_excluded_process(&self, process_name: &str) -> bool { + self.excluded_processes.iter().any(|pattern| { + if pattern.contains('*') { + process_name.contains(&pattern.replace('*', "")) + } else { + process_name.eq_ignore_ascii_case(pattern) + } + }) + } + + /// 检查 URL 是否匹配拦截模式 + pub fn matches_url_pattern(&self, url: &str) -> bool { + self.url_patterns.iter().any(|pattern| { + // 简单的模式匹配实现 + if pattern.contains('*') { + let parts: Vec<&str> = pattern.split('*').collect(); + if parts.len() == 2 { + url.starts_with(parts[0]) && url.ends_with(parts[1]) + } else { + // 更复杂的模式匹配 + url.contains(&pattern.replace('*', "")) + } + } else { + url.starts_with(pattern) + } + }) + } + + /// 验证配置的有效性 + pub fn validate(&self) -> Result<(), String> { + if self.target_processes.is_empty() { + return Err("目标进程列表不能为空".to_string()); + } + + if self.url_patterns.is_empty() { + return Err("URL 模式列表不能为空".to_string()); + } + + if self.fingerprint_browser.enabled && self.fingerprint_browser.executable_path.is_empty() { + return Err("启用指纹浏览器时必须指定可执行文件路径".to_string()); + } + + if let Some(timeout) = self.temporary_disable_timeout { + if timeout == 0 { + return Err("临时禁用超时时间必须大于 0".to_string()); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_target_process() { + let config = BrowserInterceptorConfig::default(); + + assert!(config.is_target_process("kiro")); + assert!(config.is_target_process("KIRO.EXE")); + assert!(config.is_target_process("cursor")); + assert!(!config.is_target_process("notepad")); + } + + #[test] + fn test_is_excluded_process() { + let config = BrowserInterceptorConfig::default(); + + assert!(config.is_excluded_process("chrome.exe")); + assert!(config.is_excluded_process("FIREFOX.EXE")); + assert!(!config.is_excluded_process("kiro")); + } + + #[test] + fn test_matches_url_pattern() { + let config = BrowserInterceptorConfig::default(); + + assert!(config.matches_url_pattern("https://accounts.google.com/oauth/authorize")); + assert!(config.matches_url_pattern("https://github.com/login/oauth")); + assert!(config.matches_url_pattern("https://auth.example.com/login")); + assert!(!config.matches_url_pattern("https://example.com/normal-page")); + } + + #[test] + fn test_config_validation() { + let mut config = BrowserInterceptorConfig::default(); + assert!(config.validate().is_ok()); + + config.target_processes.clear(); + assert!(config.validate().is_err()); + + config = BrowserInterceptorConfig::default(); + config.fingerprint_browser.enabled = true; + config.fingerprint_browser.executable_path = String::new(); + assert!(config.validate().is_err()); + } +} diff --git a/src-tauri/src/browser_interceptor/interceptor.rs b/src-tauri/src/browser_interceptor/interceptor.rs new file mode 100644 index 000000000..a7da381ba --- /dev/null +++ b/src-tauri/src/browser_interceptor/interceptor.rs @@ -0,0 +1,460 @@ +use crate::browser_interceptor::{ + BrowserInterceptorConfig, BrowserInterceptorError, InterceptedUrl, NotificationService, Result, + StateManager, UrlManager, +}; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// 浏览器拦截器主结构 +pub struct BrowserInterceptor { + config: Arc>, + state_manager: Arc, + url_manager: Arc, + notification_service: Arc>, + #[cfg(target_os = "windows")] + windows_interceptor: Option, + #[cfg(target_os = "macos")] + macos_interceptor: Option, + #[cfg(target_os = "linux")] + linux_interceptor: Option, +} + +impl BrowserInterceptor { + /// 创建新的浏览器拦截器实例 + pub fn new(config: BrowserInterceptorConfig) -> Self { + Self { + config: Arc::new(RwLock::new(config)), + state_manager: Arc::new(StateManager::new()), + url_manager: Arc::new(UrlManager::new()), + notification_service: Arc::new(RwLock::new(NotificationService::new())), + #[cfg(target_os = "windows")] + windows_interceptor: None, + #[cfg(target_os = "macos")] + macos_interceptor: None, + #[cfg(target_os = "linux")] + linux_interceptor: None, + } + } + + /// 启动拦截器 + pub async fn start(&mut self) -> Result<()> { + tracing::info!("开始启动浏览器拦截器..."); + + let config = self.config.read().await; + + if !config.enabled { + tracing::error!("拦截器配置中 enabled 为 false"); + return Err(BrowserInterceptorError::InterceptorError( + "拦截器未启用".to_string(), + )); + } + + // 验证配置 + tracing::info!("验证配置..."); + config.validate().map_err(|e| { + tracing::error!("配置验证失败: {}", e); + BrowserInterceptorError::ConfigError(format!("配置验证失败: {}", e)) + })?; + + drop(config); // 释放读锁 + + // 启用状态管理器 + tracing::info!("启用状态管理器..."); + self.state_manager.enable_interceptor().await.map_err(|e| { + tracing::error!("启用状态管理器失败: {}", e); + e + })?; + + // 启动平台特定的拦截器 + tracing::info!("启动平台拦截器..."); + self.start_platform_interceptor().await.map_err(|e| { + tracing::error!("启动平台拦截器失败: {}", e); + e + })?; + + // 发送启用通知 + tracing::info!("发送启用通知..."); + let notification_service = self.notification_service.read().await; + notification_service.notify_interceptor_enabled().await?; + + tracing::info!("浏览器拦截器已成功启动"); + Ok(()) + } + + /// 停止拦截器 + pub async fn stop(&mut self) -> Result<()> { + // 停止平台特定的拦截器 + self.stop_platform_interceptor().await?; + + // 禁用状态管理器 + self.state_manager.disable_interceptor().await?; + + // 发送禁用通知 + let notification_service = self.notification_service.read().await; + notification_service.notify_interceptor_disabled().await?; + + tracing::info!("浏览器拦截器已停止"); + Ok(()) + } + + /// 恢复正常浏览器行为 + pub async fn restore_normal_behavior(&mut self) -> Result<()> { + // 停止拦截 + self.stop_platform_interceptor().await?; + + // 恢复系统状态 + self.state_manager.restore_normal_behavior().await?; + + // 发送恢复通知 + let notification_service = self.notification_service.read().await; + notification_service.notify_system_restored().await?; + + tracing::info!("已恢复正常浏览器行为"); + Ok(()) + } + + /// 临时禁用拦截器 + pub async fn temporary_disable(&mut self, duration_seconds: u64) -> Result<()> { + self.state_manager + .temporary_disable(duration_seconds) + .await?; + + // 临时停止平台拦截器 + self.stop_platform_interceptor().await?; + + tracing::info!("拦截器已临时禁用 {} 秒", duration_seconds); + Ok(()) + } + + /// 处理拦截到的浏览器启动请求 + pub async fn handle_browser_launch(&self, url: String, source_process: String) -> Result { + let config = self.config.read().await; + + // 检查是否应该拦截这个进程 + if !self.should_intercept(&config, &source_process, &url) { + return Ok(false); // 不拦截,让浏览器正常启动 + } + + drop(config); // 释放读锁 + + // 添加到拦截列表 + let url_id = self + .url_manager + .add_intercepted_url(url.clone(), source_process.clone())?; + + // 更新拦截计数 + self.state_manager.increment_intercept_count()?; + + // 创建拦截的 URL 对象用于通知 + let intercepted_url = InterceptedUrl::new(url, source_process); + + // 发送通知 + let notification_service = self.notification_service.read().await; + notification_service + .notify_url_intercepted(&intercepted_url) + .await?; + + // 如果启用了自动复制到剪贴板 + let config = self.config.read().await; + if config.auto_copy_to_clipboard { + self.copy_to_clipboard(&intercepted_url.url).await?; + self.url_manager.mark_as_copied(&url_id)?; + } + + tracing::info!( + "已拦截浏览器启动: {} (来源: {})", + intercepted_url.url, + intercepted_url.source_process + ); + Ok(true) // 已拦截 + } + + /// 获取当前状态 + pub async fn get_state(&self) -> Result { + self.state_manager.get_state() + } + + /// 获取拦截的 URL 列表 + pub async fn get_intercepted_urls(&self) -> Result> { + self.url_manager.get_intercepted_urls() + } + + /// 获取历史记录 + pub async fn get_history(&self, limit: Option) -> Result> { + self.url_manager.get_history(limit) + } + + /// 复制 URL 到剪贴板 + pub async fn copy_url_to_clipboard(&self, url_id: &str) -> Result<()> { + if let Some(intercepted_url) = self.url_manager.get_intercepted_url(url_id)? { + self.copy_to_clipboard(&intercepted_url.url).await?; + self.url_manager.mark_as_copied(url_id)?; + tracing::info!("URL {} 已复制到剪贴板", url_id); + } + Ok(()) + } + + /// 在指纹浏览器中打开 URL + pub async fn open_in_fingerprint_browser(&self, url_id: &str) -> Result<()> { + let config = self.config.read().await; + + if !config.fingerprint_browser.enabled { + return Err(BrowserInterceptorError::InterceptorError( + "指纹浏览器未启用".to_string(), + )); + } + + if let Some(intercepted_url) = self.url_manager.get_intercepted_url(url_id)? { + self.launch_fingerprint_browser(&config.fingerprint_browser, &intercepted_url.url) + .await?; + self.url_manager.mark_as_opened(url_id)?; + tracing::info!("URL {} 已在指纹浏览器中打开", url_id); + } + + Ok(()) + } + + /// 忽略指定的 URL + pub async fn dismiss_url(&self, url_id: &str) -> Result<()> { + self.url_manager.dismiss_url(url_id)?; + tracing::info!("URL {} 已被忽略", url_id); + Ok(()) + } + + /// 更新配置 + pub async fn update_config(&self, new_config: BrowserInterceptorConfig) -> Result<()> { + // 验证新配置 + new_config + .validate() + .map_err(|e| BrowserInterceptorError::ConfigError(format!("配置验证失败: {}", e)))?; + + let mut config = self.config.write().await; + *config = new_config; + + tracing::info!("浏览器拦截器配置已更新"); + Ok(()) + } + + /// 检查是否应该拦截指定的进程和 URL + fn should_intercept( + &self, + config: &BrowserInterceptorConfig, + process_name: &str, + url: &str, + ) -> bool { + // 检查是否在排除列表中 + if config.is_excluded_process(process_name) { + return false; + } + + // 检查是否是目标进程 + if !config.is_target_process(process_name) { + return false; + } + + // 检查 URL 是否匹配模式 + config.matches_url_pattern(url) + } + + /// 启动平台特定的拦截器 + async fn start_platform_interceptor(&mut self) -> Result<()> { + #[cfg(target_os = "windows")] + { + // 创建 URL 处理器闭包 + let url_manager = Arc::clone(&self.url_manager); + let state_manager = Arc::clone(&self.state_manager); + + let url_handler = move |intercepted_url: InterceptedUrl| { + let url_manager = Arc::clone(&url_manager); + let state_manager = Arc::clone(&state_manager); + + tokio::spawn(async move { + if let Err(e) = url_manager + .add_intercepted_url(intercepted_url.url, intercepted_url.source_process) + { + tracing::error!("添加拦截 URL 失败: {}", e); + } + if let Err(e) = state_manager.increment_intercept_count() { + tracing::error!("更新拦截计数失败: {}", e); + } + }); + }; + + let mut interceptor = + crate::browser_interceptor::platform::windows::WindowsInterceptor::new(url_handler); + interceptor.start().await?; + self.windows_interceptor = Some(interceptor); + } + + #[cfg(target_os = "macos")] + { + let url_manager = Arc::clone(&self.url_manager); + let state_manager = Arc::clone(&self.state_manager); + + let url_handler = move |intercepted_url: InterceptedUrl| { + let url_manager = Arc::clone(&url_manager); + let state_manager = Arc::clone(&state_manager); + + tokio::spawn(async move { + if let Err(e) = url_manager + .add_intercepted_url(intercepted_url.url, intercepted_url.source_process) + { + tracing::error!("添加拦截 URL 失败: {}", e); + } + if let Err(e) = state_manager.increment_intercept_count() { + tracing::error!("更新拦截计数失败: {}", e); + } + }); + }; + + let mut interceptor = + crate::browser_interceptor::platform::macos::MacOSInterceptor::new(url_handler); + interceptor.start().await?; + self.macos_interceptor = Some(interceptor); + } + + #[cfg(target_os = "linux")] + { + let url_manager = Arc::clone(&self.url_manager); + let state_manager = Arc::clone(&self.state_manager); + + let url_handler = move |intercepted_url: InterceptedUrl| { + let url_manager = Arc::clone(&url_manager); + let state_manager = Arc::clone(&state_manager); + + tokio::spawn(async move { + if let Err(e) = url_manager + .add_intercepted_url(intercepted_url.url, intercepted_url.source_process) + { + tracing::error!("添加拦截 URL 失败: {}", e); + } + if let Err(e) = state_manager.increment_intercept_count() { + tracing::error!("更新拦截计数失败: {}", e); + } + }); + }; + + let mut interceptor = + crate::browser_interceptor::platform::linux::LinuxInterceptor::new(url_handler); + interceptor.start().await?; + self.linux_interceptor = Some(interceptor); + } + + Ok(()) + } + + /// 停止平台特定的拦截器 + async fn stop_platform_interceptor(&mut self) -> Result<()> { + #[cfg(target_os = "windows")] + { + if let Some(ref mut interceptor) = self.windows_interceptor { + interceptor.stop().await?; + } + } + + #[cfg(target_os = "macos")] + { + if let Some(ref mut interceptor) = self.macos_interceptor { + interceptor.stop().await?; + } + } + + #[cfg(target_os = "linux")] + { + if let Some(ref mut interceptor) = self.linux_interceptor { + interceptor.stop().await?; + } + } + + Ok(()) + } + + /// 复制文本到剪贴板 + async fn copy_to_clipboard(&self, text: &str) -> Result<()> { + match arboard::Clipboard::new() { + Ok(mut clipboard) => { + if let Err(e) = clipboard.set_text(text) { + return Err(BrowserInterceptorError::InterceptorError(format!( + "复制到剪贴板失败: {}", + e + ))); + } + tracing::info!("已复制到剪贴板: {}", text); + Ok(()) + } + Err(e) => Err(BrowserInterceptorError::InterceptorError(format!( + "创建剪贴板实例失败: {}", + e + ))), + } + } + + /// 启动指纹浏览器 + async fn launch_fingerprint_browser( + &self, + browser_config: &crate::browser_interceptor::config::FingerprintBrowserConfig, + url: &str, + ) -> Result<()> { + if browser_config.executable_path.is_empty() { + return Err(BrowserInterceptorError::ConfigError( + "指纹浏览器可执行文件路径未配置".to_string(), + )); + } + + // 构建启动命令 + let mut command = std::process::Command::new(&browser_config.executable_path); + + // 添加 URL 参数 + command.arg(url); + + // 添加额外的参数 + for arg in &browser_config.additional_args { + command.arg(arg); + } + + // 如果配置了配置文件路径 + if !browser_config.profile_path.is_empty() { + command + .arg("--user-data-dir") + .arg(&browser_config.profile_path); + } + + // 异步启动进程 + match command.spawn() { + Ok(mut child) => { + // 在后台等待进程完成 + tokio::spawn(async move { + match child.wait() { + Ok(status) => { + if status.success() { + tracing::info!("指纹浏览器启动成功"); + } else { + tracing::error!("指纹浏览器退出异常: {}", status); + } + } + Err(e) => { + tracing::error!("等待指纹浏览器进程失败: {}", e); + } + } + }); + + tracing::info!( + "已启动指纹浏览器: {} -> {}", + browser_config.executable_path, + url + ); + Ok(()) + } + Err(e) => Err(BrowserInterceptorError::InterceptorError(format!( + "启动指纹浏览器失败: {}", + e + ))), + } + } +} + +impl Default for BrowserInterceptor { + fn default() -> Self { + Self::new(BrowserInterceptorConfig::default()) + } +} diff --git a/src-tauri/src/browser_interceptor/mod.rs b/src-tauri/src/browser_interceptor/mod.rs new file mode 100644 index 000000000..9335b16a1 --- /dev/null +++ b/src-tauri/src/browser_interceptor/mod.rs @@ -0,0 +1,57 @@ +pub mod config; +pub mod interceptor; +pub mod notification_service; +pub mod state_manager; +pub mod url_manager; + +#[cfg(target_os = "windows")] +pub mod platform { + pub mod windows; +} + +#[cfg(target_os = "macos")] +pub mod platform { + pub mod macos; +} + +#[cfg(target_os = "linux")] +pub mod platform { + pub mod linux; +} + +// 重新导出主要类型和函数 +pub use config::{BrowserInterceptorConfig, InterceptedUrl, InterceptorState}; +pub use interceptor::BrowserInterceptor; +pub use notification_service::NotificationService; +pub use state_manager::StateManager; +pub use url_manager::UrlManager; + +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt; + +/// 浏览器拦截器错误类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum BrowserInterceptorError { + ConfigError(String), + InterceptorError(String), + StateError(String), + PlatformError(String), + NotificationError(String), +} + +impl fmt::Display for BrowserInterceptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BrowserInterceptorError::ConfigError(msg) => write!(f, "配置错误: {}", msg), + BrowserInterceptorError::InterceptorError(msg) => write!(f, "拦截器错误: {}", msg), + BrowserInterceptorError::StateError(msg) => write!(f, "状态管理错误: {}", msg), + BrowserInterceptorError::PlatformError(msg) => write!(f, "平台错误: {}", msg), + BrowserInterceptorError::NotificationError(msg) => write!(f, "通知错误: {}", msg), + } + } +} + +impl Error for BrowserInterceptorError {} + +pub type Result = std::result::Result; diff --git a/src-tauri/src/browser_interceptor/notification_service.rs b/src-tauri/src/browser_interceptor/notification_service.rs new file mode 100644 index 000000000..a35ddfe6e --- /dev/null +++ b/src-tauri/src/browser_interceptor/notification_service.rs @@ -0,0 +1,307 @@ +use crate::browser_interceptor::{BrowserInterceptorError, InterceptedUrl, Result}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// 通知类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NotificationType { + UrlIntercepted, + InterceptorEnabled, + InterceptorDisabled, + SystemRestored, + Error, +} + +/// 通知消息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationMessage { + pub id: String, + pub notification_type: NotificationType, + pub title: String, + pub message: String, + pub url: Option, + pub source_process: Option, + pub timestamp: chrono::DateTime, + pub auto_dismiss_after: Option, +} + +impl NotificationMessage { + pub fn new(notification_type: NotificationType, title: String, message: String) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + notification_type, + title, + message, + url: None, + source_process: None, + timestamp: chrono::Utc::now(), + auto_dismiss_after: Some(Duration::from_secs(30)), + } + } + + pub fn with_url(mut self, url: String) -> Self { + self.url = Some(url); + self + } + + pub fn with_source_process(mut self, source_process: String) -> Self { + self.source_process = Some(source_process); + self + } + + pub fn with_auto_dismiss(mut self, duration: Option) -> Self { + self.auto_dismiss_after = duration; + self + } +} + +/// 通知服务 +pub struct NotificationService { + enabled: bool, + show_url_preview: bool, +} + +impl NotificationService { + pub fn new() -> Self { + Self { + enabled: true, + show_url_preview: true, + } + } + + /// 设置通知是否启用 + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + /// 设置是否显示 URL 预览 + pub fn set_show_url_preview(&mut self, show_preview: bool) { + self.show_url_preview = show_preview; + } + + /// 发送 URL 拦截通知 + pub async fn notify_url_intercepted(&self, intercepted_url: &InterceptedUrl) -> Result<()> { + if !self.enabled { + return Ok(()); + } + + let title = format!("已拦截来自 {} 的 URL", intercepted_url.source_process); + let message = if self.show_url_preview { + format!("URL: {}", self.truncate_url(&intercepted_url.url, 100)) + } else { + "点击查看详情".to_string() + }; + + let notification = + NotificationMessage::new(NotificationType::UrlIntercepted, title, message) + .with_url(intercepted_url.url.clone()) + .with_source_process(intercepted_url.source_process.clone()); + + self.send_notification(notification).await + } + + /// 发送拦截器启用通知 + pub async fn notify_interceptor_enabled(&self) -> Result<()> { + if !self.enabled { + return Ok(()); + } + + let notification = NotificationMessage::new( + NotificationType::InterceptorEnabled, + "浏览器拦截器已启用".to_string(), + "现在会拦截目标应用的浏览器启动请求".to_string(), + ); + + self.send_notification(notification).await + } + + /// 发送拦截器禁用通知 + pub async fn notify_interceptor_disabled(&self) -> Result<()> { + if !self.enabled { + return Ok(()); + } + + let notification = NotificationMessage::new( + NotificationType::InterceptorDisabled, + "浏览器拦截器已禁用".to_string(), + "应用将正常打开默认浏览器".to_string(), + ); + + self.send_notification(notification).await + } + + /// 发送系统恢复通知 + pub async fn notify_system_restored(&self) -> Result<()> { + if !self.enabled { + return Ok(()); + } + + let notification = NotificationMessage::new( + NotificationType::SystemRestored, + "系统已恢复正常".to_string(), + "浏览器行为已恢复到原始状态".to_string(), + ); + + self.send_notification(notification).await + } + + /// 发送错误通知 + pub async fn notify_error(&self, error_message: &str) -> Result<()> { + if !self.enabled { + return Ok(()); + } + + let notification = NotificationMessage::new( + NotificationType::Error, + "浏览器拦截器错误".to_string(), + error_message.to_string(), + ) + .with_auto_dismiss(Some(Duration::from_secs(60))); // 错误通知显示更长时间 + + self.send_notification(notification).await + } + + /// 发送通知的具体实现 + async fn send_notification(&self, notification: NotificationMessage) -> Result<()> { + // 记录日志 + tracing::info!( + "发送通知: {} - {}", + notification.title, + notification.message + ); + + // 发送系统通知 + self.send_system_notification(¬ification).await?; + + // 发送到前端(通过事件系统) + self.send_frontend_notification(¬ification).await?; + + Ok(()) + } + + /// 发送系统通知 + async fn send_system_notification(&self, notification: &NotificationMessage) -> Result<()> { + #[cfg(target_os = "windows")] + { + self.send_windows_notification(notification).await?; + } + + #[cfg(target_os = "macos")] + { + self.send_macos_notification(notification).await?; + } + + #[cfg(target_os = "linux")] + { + self.send_linux_notification(notification).await?; + } + + Ok(()) + } + + /// 发送到前端 + async fn send_frontend_notification(&self, notification: &NotificationMessage) -> Result<()> { + // TODO: 通过 Tauri 事件系统发送到前端 + // tauri::emit_all("browser-interceptor-notification", notification) + // .map_err(|e| BrowserInterceptorError::NotificationError(format!("发送前端通知失败: {}", e)))?; + + tracing::debug!("前端通知已发送: {}", notification.id); + Ok(()) + } + + /// Windows 系统通知 + #[cfg(target_os = "windows")] + async fn send_windows_notification(&self, notification: &NotificationMessage) -> Result<()> { + // TODO: 使用 Windows Toast 通知 API + // 可以使用 winrt-notification 或 windows-rs crate + tracing::debug!("Windows 通知: {}", notification.title); + Ok(()) + } + + /// macOS 系统通知 + #[cfg(target_os = "macos")] + async fn send_macos_notification(&self, notification: &NotificationMessage) -> Result<()> { + // TODO: 使用 macOS 通知中心 + // 可以使用 mac-notification-sys crate + tracing::debug!("macOS 通知: {}", notification.title); + Ok(()) + } + + /// Linux 系统通知 + #[cfg(target_os = "linux")] + async fn send_linux_notification(&self, notification: &NotificationMessage) -> Result<()> { + // TODO: 使用 libnotify 或 D-Bus + // 可以使用 notify-rust crate + tracing::debug!("Linux 通知: {}", notification.title); + Ok(()) + } + + /// 截断 URL 以适应通知显示 + fn truncate_url(&self, url: &str, max_length: usize) -> String { + if url.len() <= max_length { + url.to_string() + } else { + format!("{}...", &url[..max_length.saturating_sub(3)]) + } + } +} + +impl Default for NotificationService { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + + #[test] + fn test_notification_message_creation() { + let notification = NotificationMessage::new( + NotificationType::UrlIntercepted, + "Test Title".to_string(), + "Test Message".to_string(), + ) + .with_url("https://example.com".to_string()) + .with_source_process("test_app".to_string()); + + assert_eq!(notification.title, "Test Title"); + assert_eq!(notification.message, "Test Message"); + assert_eq!(notification.url, Some("https://example.com".to_string())); + assert_eq!(notification.source_process, Some("test_app".to_string())); + assert!(notification.auto_dismiss_after.is_some()); + } + + #[test] + fn test_truncate_url() { + let service = NotificationService::new(); + + let short_url = "https://example.com"; + assert_eq!(service.truncate_url(short_url, 100), short_url); + + let long_url = "https://example.com/very/long/path/that/exceeds/the/maximum/length/limit"; + let truncated = service.truncate_url(long_url, 20); + assert_eq!(truncated.len(), 20); + assert!(truncated.ends_with("...")); + } + + #[tokio::test] + async fn test_notify_url_intercepted() { + let service = NotificationService::new(); + let intercepted_url = InterceptedUrl { + id: "test-id".to_string(), + url: "https://accounts.google.com/oauth/authorize".to_string(), + source_process: "kiro".to_string(), + timestamp: Utc::now(), + copied: false, + opened_in_browser: false, + dismissed: false, + }; + + // 这个测试主要验证函数不会 panic + let result = service.notify_url_intercepted(&intercepted_url).await; + assert!(result.is_ok()); + } +} diff --git a/src-tauri/src/browser_interceptor/platform/linux.rs b/src-tauri/src/browser_interceptor/platform/linux.rs new file mode 100644 index 000000000..02b06648f --- /dev/null +++ b/src-tauri/src/browser_interceptor/platform/linux.rs @@ -0,0 +1,71 @@ +use crate::browser_interceptor::{BrowserInterceptorError, InterceptedUrl, Result}; + +/// Linux 平台的浏览器拦截器 +pub struct LinuxInterceptor { + running: bool, +} + +impl LinuxInterceptor { + pub fn new(_url_handler: F) -> Self + where + F: Fn(InterceptedUrl) + Send + Sync + 'static, + { + Self { running: false } + } + + /// 启动拦截 + pub async fn start(&mut self) -> Result<()> { + // TODO: 实现 Linux 平台的浏览器拦截 + // 可以使用 xdg-open 拦截或 D-Bus 监听 + self.running = true; + tracing::info!("Linux 浏览器拦截器已启动 (占位符)"); + Ok(()) + } + + /// 停止拦截 + pub async fn stop(&mut self) -> Result<()> { + if !self.running { + return Ok(()); + } + + self.running = false; + tracing::info!("Linux 浏览器拦截器已停止"); + Ok(()) + } + + /// 检查是否正在拦截 + pub fn is_running(&self) -> bool { + self.running + } + + /// 恢复系统默认设置 + pub async fn restore_system_defaults(&self) -> Result<()> { + tracing::info!("Linux 系统默认设置已恢复"); + Ok(()) + } + + /// 临时禁用拦截 + pub async fn temporarily_disable(&mut self) -> Result<()> { + tracing::info!("Linux 拦截器已临时禁用"); + Ok(()) + } + + /// 重新启用拦截 + pub async fn re_enable(&mut self) -> Result<()> { + tracing::info!("Linux 拦截器已重新启用"); + Ok(()) + } +} + +impl Drop for LinuxInterceptor { + fn drop(&mut self) { + if self.running { + tokio::runtime::Handle::try_current().map(|handle| { + handle.block_on(async { + let _ = self.stop().await; + let _ = self.restore_system_defaults().await; + }) + }); + } + } +} diff --git a/src-tauri/src/browser_interceptor/platform/macos.rs b/src-tauri/src/browser_interceptor/platform/macos.rs new file mode 100644 index 000000000..ff00e0597 --- /dev/null +++ b/src-tauri/src/browser_interceptor/platform/macos.rs @@ -0,0 +1,429 @@ +use crate::browser_interceptor::{BrowserInterceptorError, InterceptedUrl, Result}; +use once_cell::sync::Lazy; +use std::process::Command; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; + +/// 全局 URL sender,用于从 deep-link 事件接收 URL(使用 Mutex 保证线程安全) +static GLOBAL_URL_SENDER: Lazy>>> = + Lazy::new(|| Mutex::new(None)); + +/// macOS 平台的浏览器拦截器(基于设置默认浏览器 + Deep Link) +pub struct MacOSInterceptor { + running: bool, + url_sender: Option>, + original_default_browser: Option, + url_handler: Option>, +} + +impl MacOSInterceptor { + pub fn new(url_handler: F) -> Self + where + F: Fn(InterceptedUrl) + Send + Sync + 'static, + { + let (tx, mut rx) = mpsc::unbounded_channel(); + let handler = Arc::new(url_handler); + let handler_clone = handler.clone(); + + // 启动后台任务处理拦截的 URL + tokio::spawn(async move { + while let Some(intercepted_url) = rx.recv().await { + handler_clone(intercepted_url); + } + }); + + Self { + running: false, + url_sender: Some(tx), + original_default_browser: None, + url_handler: Some(handler), + } + } + + /// 启动拦截 + pub async fn start(&mut self) -> Result<()> { + if self.running { + tracing::info!("macOS 拦截器已在运行中,跳过启动"); + return Ok(()); + } + + tracing::info!("正在启动 macOS 浏览器拦截器..."); + + // 1. 保存当前默认浏览器 + self.original_default_browser = self.get_default_browser().await; + tracing::info!("当前默认浏览器: {:?}", self.original_default_browser); + + // 2. 设置全局 URL sender + if let Some(ref sender) = self.url_sender { + if let Ok(mut global_sender) = GLOBAL_URL_SENDER.lock() { + *global_sender = Some(sender.clone()); + } + } + + // 3. 将 ProxyCast 设置为默认浏览器 + self.set_as_default_browser().await?; + + self.running = true; + tracing::info!("macOS 浏览器拦截器已启动"); + tracing::info!("提示:现在所有 http/https URL 打开请求都会被 ProxyCast 拦截"); + + Ok(()) + } + + /// 获取当前默认浏览器的 Bundle ID + async fn get_default_browser(&self) -> Option { + // 使用 Swift 调用 LSCopyDefaultHandlerForURLScheme 获取真实的默认浏览器 + let swift_code = r#" +import Foundation +import CoreServices + +if let handler = LSCopyDefaultHandlerForURLScheme("https" as CFString) { + print(handler.takeRetainedValue() as String) +} else if let handler = LSCopyDefaultHandlerForURLScheme("http" as CFString) { + print(handler.takeRetainedValue() as String) +} else { + print("") +} +"#; + + let output = Command::new("swift") + .args(["-e", swift_code]) + .output() + .ok()?; + + if output.status.success() { + let bundle_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !bundle_id.is_empty() && !bundle_id.contains("proxycast") { + tracing::info!("检测到当前默认浏览器: {}", bundle_id); + return Some(bundle_id); + } + // 如果当前是 ProxyCast,说明之前已设置过,需要检测用户常用的浏览器 + if bundle_id.contains("proxycast") { + tracing::info!("当前默认浏览器是 ProxyCast,尝试检测用户常用浏览器"); + return self.detect_installed_browser().await; + } + } + + // 备选方法:使用 duti 查询 + let duti_output = Command::new("duti").args(["-x", "https"]).output().ok(); + + if let Some(output) = duti_output { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + // duti -x 输出格式:第三行是 bundle id + for line in stdout.lines() { + let trimmed = line.trim(); + if (trimmed.starts_with("com.") || trimmed.starts_with("org.")) + && !trimmed.contains("proxycast") + { + tracing::info!("通过 duti 检测到默认浏览器: {}", trimmed); + return Some(trimmed.to_string()); + } + } + } + } + + // 最后尝试检测已安装的浏览器,优先返回 Chrome + self.detect_installed_browser().await + } + + /// 将 ProxyCast 设置为默认浏览器 + async fn set_as_default_browser(&self) -> Result<()> { + tracing::info!("正在将 ProxyCast 设置为默认浏览器..."); + + // 使用 Swift 代码通过 Launch Services API 设置默认处理程序 + // 由于 Rust 直接调用 Launch Services 较复杂,这里使用 osascript 辅助 + + // 方法 1: 使用 duti 工具(如果已安装) + let duti_result = Command::new("duti") + .args(["-s", "com.proxycast.app", "http", "all"]) + .output(); + + if let Ok(output) = duti_result { + if output.status.success() { + let _ = Command::new("duti") + .args(["-s", "com.proxycast.app", "https", "all"]) + .output(); + tracing::info!("已通过 duti 设置默认浏览器"); + return Ok(()); + } + } + + // 方法 2: 使用 Swift 脚本 + let swift_code = r#" +import Foundation +import CoreServices + +let bundleId = "com.proxycast.app" as CFString + +// 设置 HTTP handler +LSSetDefaultHandlerForURLScheme("http" as CFString, bundleId) + +// 设置 HTTPS handler +LSSetDefaultHandlerForURLScheme("https" as CFString, bundleId) + +print("OK") +"#; + + let output = Command::new("swift") + .args(["-e", swift_code]) + .output() + .map_err(|e| { + BrowserInterceptorError::PlatformError(format!("执行 Swift 脚本失败: {}", e)) + })?; + + if output.status.success() { + tracing::info!("已通过 Launch Services API 设置默认浏览器"); + return Ok(()); + } + + // 方法 3: 提示用户手动设置 + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::warn!("自动设置默认浏览器失败: {}", stderr); + tracing::info!("请手动在系统设置中将 ProxyCast 设置为默认浏览器"); + + // 打开系统设置 + let _ = Command::new("open") + .args(["x-apple.systempreferences:com.apple.preference.general"]) + .output(); + + Ok(()) + } + + /// 恢复原来的默认浏览器 + async fn restore_default_browser(&self) -> Result<()> { + // 获取要恢复的浏览器 + let browser_id = match &self.original_default_browser { + Some(id) if !id.contains("proxycast") => id.clone(), + _ => { + // 如果没有记录原始浏览器,尝试检测已安装的浏览器 + self.detect_installed_browser() + .await + .unwrap_or_else(|| "com.google.Chrome".to_string()) + } + }; + + tracing::info!("正在恢复默认浏览器为: {}", browser_id); + + // 使用 duti + let duti_result = Command::new("duti") + .args(["-s", &browser_id, "http", "all"]) + .output(); + + if let Ok(output) = duti_result { + if output.status.success() { + let _ = Command::new("duti") + .args(["-s", &browser_id, "https", "all"]) + .output(); + tracing::info!("已通过 duti 恢复默认浏览器为: {}", browser_id); + return Ok(()); + } + } + + // 使用 Swift + let swift_code = format!( + r#" +import Foundation +import CoreServices + +let bundleId = "{}" as CFString +LSSetDefaultHandlerForURLScheme("http" as CFString, bundleId) +LSSetDefaultHandlerForURLScheme("https" as CFString, bundleId) +print("OK") +"#, + browser_id + ); + + let output = Command::new("swift").args(["-e", &swift_code]).output(); + + if let Ok(out) = output { + if out.status.success() { + tracing::info!("已通过 Swift 恢复默认浏览器为: {}", browser_id); + } else { + let stderr = String::from_utf8_lossy(&out.stderr); + tracing::warn!("Swift 恢复默认浏览器失败: {}", stderr); + } + } + + Ok(()) + } + + /// 检测已安装的浏览器(用于恢复时选择) + async fn detect_installed_browser(&self) -> Option { + // 按优先级排序的浏览器列表 + let browsers = [ + ("com.google.Chrome", "/Applications/Google Chrome.app"), + ("org.mozilla.firefox", "/Applications/Firefox.app"), + ("com.microsoft.edgemac", "/Applications/Microsoft Edge.app"), + ("com.brave.Browser", "/Applications/Brave Browser.app"), + ("com.apple.Safari", "/Applications/Safari.app"), + ]; + + for (bundle_id, app_path) in &browsers { + // 直接检查应用是否存在 + if std::path::Path::new(app_path).exists() { + tracing::info!("检测到已安装的浏览器: {} ({})", bundle_id, app_path); + return Some(bundle_id.to_string()); + } + } + + // Safari 总是存在 + Some("com.apple.Safari".to_string()) + } + + /// 停止拦截 + pub async fn stop(&mut self) -> Result<()> { + if !self.running { + return Ok(()); + } + + tracing::info!("正在停止 macOS 浏览器拦截器..."); + + // 清除全局 URL sender + if let Ok(mut global_sender) = GLOBAL_URL_SENDER.lock() { + *global_sender = None; + } + + // 恢复默认浏览器 + self.restore_default_browser().await?; + + self.running = false; + tracing::info!("macOS 浏览器拦截器已停止"); + Ok(()) + } + + /// 检查是否正在拦截 + pub fn is_running(&self) -> bool { + self.running + } + + /// 获取当前监听的端口(不再使用,保留接口兼容性) + pub fn get_port(&self) -> Option { + None + } + + /// 恢复系统默认设置 + pub async fn restore_system_defaults(&self) -> Result<()> { + self.restore_default_browser().await + } + + /// 临时禁用拦截 + pub async fn temporarily_disable(&mut self) -> Result<()> { + if !self.running { + return Ok(()); + } + + tracing::info!("正在临时禁用 macOS 浏览器拦截器..."); + + // 临时恢复默认浏览器 + self.restore_default_browser().await?; + + tracing::info!("macOS 浏览器拦截器已临时禁用"); + Ok(()) + } + + /// 重新启用拦截 + pub async fn re_enable(&mut self) -> Result<()> { + if !self.running { + return Err(BrowserInterceptorError::InterceptorError( + "拦截器未运行,无法重新启用".to_string(), + )); + } + + tracing::info!("正在重新启用 macOS 浏览器拦截器..."); + + // 重新设置为默认浏览器 + self.set_as_default_browser().await?; + + tracing::info!("macOS 浏览器拦截器已重新启用"); + Ok(()) + } +} + +impl Drop for MacOSInterceptor { + fn drop(&mut self) { + if self.running { + tracing::info!("macOS 浏览器拦截器资源已清理"); + // 注意:在 drop 中无法使用 async,恢复操作在 stop() 中完成 + } + } +} + +/// 处理从 deep-link 接收到的 URL(由 lib.rs 中的事件监听器调用) +pub fn handle_deep_link_url(url: String) { + tracing::info!("收到 deep-link URL: {}", url); + + // 检查是否是 http/https URL + if !url.starts_with("http://") && !url.starts_with("https://") { + tracing::debug!("忽略非 HTTP URL: {}", url); + return; + } + + // 尝试识别来源进程(macOS 上较难获取,使用默认值) + let source_process = detect_source_process(&url); + + let intercepted_url = InterceptedUrl::new(url, source_process); + + // 发送到处理器 + if let Ok(global_sender) = GLOBAL_URL_SENDER.lock() { + if let Some(ref sender) = *global_sender { + match sender.send(intercepted_url) { + Ok(_) => tracing::debug!("URL 已发送到处理器"), + Err(e) => tracing::error!("发送 URL 到处理器失败: {:?}", e), + } + } else { + tracing::warn!("拦截器未运行,忽略 URL"); + } + } else { + tracing::error!("无法获取 URL sender 锁"); + } +} + +/// 尝试检测 URL 的来源进程 +fn detect_source_process(url: &str) -> String { + // 基于 URL 特征推测来源 + if url.contains("kiro") || url.contains("amazon") || url.contains("aws") { + return "Kiro".to_string(); + } + if url.contains("cursor") || url.contains("anysphere") { + return "Cursor".to_string(); + } + if url.contains("vscode") || url.contains("microsoft") || url.contains("visualstudio") { + return "VSCode".to_string(); + } + if url.contains("claude") || url.contains("anthropic") { + return "Claude App".to_string(); + } + if url.contains("github") { + return "GitHub App".to_string(); + } + if url.contains("google") || url.contains("accounts.google") { + return "OAuth Request".to_string(); + } + + // 尝试获取前台应用 + if let Ok(output) = Command::new("osascript") + .args([ + "-e", + "tell application \"System Events\" to get the name of first process whose frontmost is true", + ]) + .output() + { + if output.status.success() { + let app_name = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !app_name.is_empty() && app_name != "ProxyCast" { + return app_name; + } + } + } + + "Unknown App".to_string() +} + +/// 检查拦截器是否有活跃的 URL sender +pub fn is_interceptor_active() -> bool { + GLOBAL_URL_SENDER + .lock() + .map(|sender| sender.is_some()) + .unwrap_or(false) +} diff --git a/src-tauri/src/browser_interceptor/platform/windows.rs b/src-tauri/src/browser_interceptor/platform/windows.rs new file mode 100644 index 000000000..010f84ffc --- /dev/null +++ b/src-tauri/src/browser_interceptor/platform/windows.rs @@ -0,0 +1,454 @@ +use crate::browser_interceptor::{BrowserInterceptorError, InterceptedUrl, Result}; +use chrono::Utc; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use uuid::Uuid; + +#[cfg(windows)] +use { + std::ffi::{OsStr, OsString}, + std::iter::once, + std::os::windows::ffi::OsStrExt, + std::ptr, + windows::{ + core::*, Win32::Foundation::*, Win32::System::Registry::*, Win32::System::Threading::*, + Win32::UI::Shell::*, + }, +}; + +/// Windows 平台的浏览器拦截器 +pub struct WindowsInterceptor { + running: bool, + original_browser: Option, + temp_exe_path: Option, + intercepted_urls_handler: Arc>>, + monitor_thread: Option>, +} + +#[cfg(windows)] +impl WindowsInterceptor { + pub fn new(url_handler: F) -> Self + where + F: Fn(InterceptedUrl) + Send + Sync + 'static, + { + Self { + running: false, + original_browser: None, + temp_exe_path: None, + intercepted_urls_handler: Arc::new(Mutex::new(Box::new(url_handler))), + monitor_thread: None, + } + } + + /// 启动拦截 + pub async fn start(&mut self) -> Result<()> { + if self.running { + return Err(BrowserInterceptorError::AlreadyRunning); + } + + // 备份当前默认浏览器设置 + self.backup_default_browser().await?; + + // 创建临时拦截程序 + self.create_interceptor_executable().await?; + + // 设置我们的程序为默认浏览器 + self.set_as_default_browser().await?; + + // 启动进程监控 + self.start_process_monitoring().await?; + + self.running = true; + tracing::info!("Windows 浏览器拦截器已启动"); + Ok(()) + } + + /// 停止拦截 + pub async fn stop(&mut self) -> Result<()> { + if !self.running { + return Ok(()); + } + + // 停止进程监控 + if let Some(handle) = self.monitor_thread.take() { + // 发送停止信号,等待线程结束 + handle.join().ok(); + } + + // 恢复原始默认浏览器 + self.restore_default_browser().await?; + + // 清理临时文件 + self.cleanup_temp_files().await?; + + self.running = false; + tracing::info!("Windows 浏览器拦截器已停止"); + Ok(()) + } + + /// 备份当前默认浏览器设置 + async fn backup_default_browser(&mut self) -> Result<()> { + unsafe { + let key_path = to_wide_chars( + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", + ); + let mut key: HKEY = HKEY::default(); + + let result = RegOpenKeyExW( + HKEY_CURRENT_USER, + PCWSTR::from_raw(key_path.as_ptr()), + 0, + KEY_READ, + &mut key, + ); + + if result.is_ok() { + let mut buffer = vec![0u16; 1024]; + let mut buffer_size = buffer.len() * 2; + + let result = RegQueryValueExW( + key, + PCWSTR::from_raw(to_wide_chars("ProgId").as_ptr()), + None, + None, + Some(buffer.as_mut_ptr() as *mut u8), + Some(&mut buffer_size as *mut u32), + ); + + RegCloseKey(key); + + if result.is_ok() { + let prog_id = String::from_utf16_lossy(&buffer[..buffer_size / 2 - 1]); + self.original_browser = Some(prog_id); + tracing::info!( + "备份原始默认浏览器: {}", + self.original_browser.as_ref().unwrap() + ); + } + } + } + + Ok(()) + } + + /// 创建临时的拦截器可执行文件 + async fn create_interceptor_executable(&mut self) -> Result<()> { + // 创建一个简单的拦截器程序,用于接收 URL 参数 + let temp_dir = std::env::temp_dir(); + let exe_path = temp_dir.join("proxycast_browser_interceptor.exe"); + + // 创建拦截器脚本内容(批处理脚本) + let bat_content = format!( + r#"@echo off +echo URL被拦截: %1 >> "{}\proxycast_intercepted_urls.log" +"#, + temp_dir.to_string_lossy() + ); + + let bat_path = temp_dir.join("proxycast_browser_interceptor.bat"); + std::fs::write(&bat_path, bat_content)?; + + // 创建一个简单的可执行文件包装器 + // 这里我们使用批处理文件,在真实环境中应该编译一个专用的小程序 + self.temp_exe_path = Some(bat_path.to_string_lossy().to_string()); + + Ok(()) + } + + /// 设置我们的程序为默认浏览器 + async fn set_as_default_browser(&self) -> Result<()> { + if let Some(exe_path) = &self.temp_exe_path { + unsafe { + // 注册我们的程序为HTTP处理器 + let prog_id = "ProxyCastInterceptor"; + let key_path = format!(r"Software\Classes\{}", prog_id); + + self.set_registry_string(&key_path, "", "ProxyCast Browser Interceptor") + .await?; + + let command_path = format!(r"{}\shell\open\command", key_path); + let command_value = format!(r#"{} "%1""#, exe_path); + self.set_registry_string(&command_path, "", &command_value) + .await?; + + // 设置为HTTP协议的默认处理器 + let http_key = r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"; + self.set_registry_string(http_key, "ProgId", prog_id) + .await?; + + // 设置为HTTPS协议的默认处理器 + let https_key = r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice"; + self.set_registry_string(https_key, "ProgId", prog_id) + .await?; + + tracing::info!("已设置 ProxyCast 为临时默认浏览器"); + } + } + + Ok(()) + } + + /// 设置注册表字符串值 + async fn set_registry_string( + &self, + key_path: &str, + value_name: &str, + value_data: &str, + ) -> Result<()> { + unsafe { + let key_path_wide = to_wide_chars(key_path); + let mut key: HKEY = HKEY::default(); + + let result = RegCreateKeyExW( + HKEY_CURRENT_USER, + PCWSTR::from_raw(key_path_wide.as_ptr()), + 0, + PCWSTR::null(), + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + None, + &mut key, + None, + ); + + if result.is_ok() { + let value_name_wide = to_wide_chars(value_name); + let value_data_wide = to_wide_chars(value_data); + + RegSetValueExW( + key, + PCWSTR::from_raw(value_name_wide.as_ptr()), + 0, + REG_SZ, + Some(value_data_wide.as_ptr() as *const u8), + value_data_wide.len() * 2, + ); + + RegCloseKey(key); + } + } + + Ok(()) + } + + /// 启动进程监控 + async fn start_process_monitoring(&mut self) -> Result<()> { + let handler = Arc::clone(&self.intercepted_urls_handler); + let temp_dir = std::env::temp_dir(); + let log_file = temp_dir.join("proxycast_intercepted_urls.log"); + + let handle = thread::spawn(move || { + loop { + // 检查拦截日志文件 + if let Ok(content) = std::fs::read_to_string(&log_file) { + for line in content.lines() { + if line.starts_with("URL被拦截: ") { + let url = line.replace("URL被拦截: ", ""); + if !url.is_empty() && should_intercept_url(&url) { + let intercepted = InterceptedUrl { + id: Uuid::new_v4().to_string(), + url: url.clone(), + source_process: "Unknown".to_string(), + timestamp: Utc::now(), + copied: false, + opened_in_browser: false, + dismissed: false, + }; + + // 调用处理器 + if let Ok(handler_guard) = handler.lock() { + handler_guard(intercepted); + } + } + } + } + + // 清空日志文件避免重复处理 + std::fs::write(&log_file, "").ok(); + } + + thread::sleep(Duration::from_millis(1000)); + } + }); + + self.monitor_thread = Some(handle); + Ok(()) + } + + /// 恢复原始默认浏览器 + async fn restore_default_browser(&self) -> Result<()> { + if let Some(original_browser) = &self.original_browser { + // 恢复HTTP协议处理器 + let http_key = + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"; + self.set_registry_string(http_key, "ProgId", original_browser) + .await?; + + // 恢复HTTPS协议处理器 + let https_key = + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice"; + self.set_registry_string(https_key, "ProgId", original_browser) + .await?; + + tracing::info!("已恢复原始默认浏览器: {}", original_browser); + } + + Ok(()) + } + + /// 清理临时文件 + async fn cleanup_temp_files(&self) -> Result<()> { + if let Some(exe_path) = &self.temp_exe_path { + std::fs::remove_file(exe_path).ok(); + } + + let temp_dir = std::env::temp_dir(); + let log_file = temp_dir.join("proxycast_intercepted_urls.log"); + std::fs::remove_file(log_file).ok(); + + Ok(()) + } + + /// 检查是否正在拦截 + pub fn is_running(&self) -> bool { + self.running + } + + /// 恢复系统默认设置 + pub async fn restore_system_defaults(&self) -> Result<()> { + // 恢复默认浏览器设置 + self.restore_default_browser().await?; + tracing::info!("系统默认设置已恢复"); + Ok(()) + } + + /// 临时禁用拦截 + pub async fn temporarily_disable(&mut self) -> Result<()> { + if let Some(original_browser) = &self.original_browser { + self.restore_default_browser().await?; + tracing::info!("拦截器已临时禁用"); + } + Ok(()) + } + + /// 重新启用拦截 + pub async fn re_enable(&mut self) -> Result<()> { + self.set_as_default_browser().await?; + tracing::info!("拦截器已重新启用"); + Ok(()) + } +} + +#[cfg(not(windows))] +impl WindowsInterceptor { + pub fn new(_url_handler: F) -> Self + where + F: Fn(InterceptedUrl) + Send + Sync + 'static, + { + Self { + running: false, + original_browser: None, + temp_exe_path: None, + intercepted_urls_handler: Arc::new(Mutex::new(Box::new(|_| {}))), + monitor_thread: None, + } + } + + pub async fn start(&mut self) -> Result<()> { + Err(BrowserInterceptorError::UnsupportedPlatform( + "Windows interceptor only supports Windows platform".to_string(), + )) + } + + pub async fn stop(&mut self) -> Result<()> { + Ok(()) + } + + pub fn is_running(&self) -> bool { + false + } + + pub async fn restore_system_defaults(&self) -> Result<()> { + Ok(()) + } + + pub async fn temporarily_disable(&mut self) -> Result<()> { + Ok(()) + } + + pub async fn re_enable(&mut self) -> Result<()> { + Ok(()) + } +} + +impl Drop for WindowsInterceptor { + fn drop(&mut self) { + if self.running { + // 在析构时恢复系统默认设置 + tokio::runtime::Handle::try_current().map(|handle| { + handle.block_on(async { + let _ = self.stop().await; + let _ = self.restore_system_defaults().await; + }) + }); + } + } +} + +// Windows 特定的辅助函数 +fn to_wide_chars(s: &str) -> Vec { + OsStr::new(s).encode_wide().chain(once(0)).collect() +} + +/// 检查进程是否为目标应用 +pub fn is_target_process(process_name: &str) -> bool { + let target_processes = [ + "kiro", + "kiro.exe", + "cursor", + "cursor.exe", + "code", + "code.exe", + ]; + target_processes + .iter() + .any(|&target| process_name.to_lowercase().contains(&target.to_lowercase())) +} + +/// 检查 URL 是否匹配拦截模式 +pub fn should_intercept_url(url: &str) -> bool { + let patterns = [ + "https://auth.", + "https://accounts.google.com", + "https://github.com/login", + "https://login.microsoftonline.com", + "/oauth/", + "/auth/", + "localhost:8080/auth", // OAuth 回调地址 + ]; + + patterns.iter().any(|&pattern| url.contains(pattern)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_target_process() { + assert!(is_target_process("kiro.exe")); + assert!(is_target_process("cursor")); + assert!(is_target_process("code.exe")); + assert!(!is_target_process("notepad.exe")); + } + + #[test] + fn test_should_intercept_url() { + assert!(should_intercept_url("https://accounts.google.com/oauth")); + assert!(should_intercept_url("https://github.com/login/oauth")); + assert!(should_intercept_url("localhost:8080/auth/callback")); + assert!(!should_intercept_url("https://example.com")); + } +} diff --git a/src-tauri/src/browser_interceptor/state_manager.rs b/src-tauri/src/browser_interceptor/state_manager.rs new file mode 100644 index 000000000..5586a3725 --- /dev/null +++ b/src-tauri/src/browser_interceptor/state_manager.rs @@ -0,0 +1,504 @@ +use crate::browser_interceptor::{BrowserInterceptorError, InterceptorState, Result}; +use chrono::Utc; +use std::sync::{Arc, RwLock}; +use tokio::time::{Duration, Instant}; + +#[cfg(windows)] +use { + std::ffi::OsStr, + std::iter::once, + std::os::windows::ffi::OsStrExt, + windows::{core::*, Win32::Foundation::*, Win32::System::Registry::*}, +}; + +/// 状态管理器,负责管理拦截器的状态和恢复机制 +pub struct StateManager { + state: Arc>, + original_system_state: Arc>>, + temporary_disable_timer: Arc>>, +} + +/// 系统原始状态备份 +#[derive(Debug, Clone)] +pub struct SystemState { + pub default_browser: Option, + pub registry_backup: std::collections::HashMap, + pub environment_backup: std::collections::HashMap, + pub timestamp: chrono::DateTime, +} + +impl StateManager { + pub fn new() -> Self { + Self { + state: Arc::new(RwLock::new(InterceptorState::default())), + original_system_state: Arc::new(RwLock::new(None)), + temporary_disable_timer: Arc::new(RwLock::new(None)), + } + } + + /// 获取当前状态 + pub fn get_state(&self) -> Result { + self.state + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取状态失败: {}", e))) + .map(|state| state.clone()) + } + + /// 启用拦截器 + pub async fn enable_interceptor(&self) -> Result<()> { + // 备份系统状态 + self.backup_system_state().await?; + + // 更新状态 + { + let mut state = self + .state + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("写入状态失败: {}", e)))?; + + state.enabled = true; + state.can_restore = true; + state.last_activity = Some(Utc::now()); + } + + tracing::info!("浏览器拦截器已启用"); + Ok(()) + } + + /// 禁用拦截器 + pub async fn disable_interceptor(&self) -> Result<()> { + { + let mut state = self + .state + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("写入状态失败: {}", e)))?; + + state.enabled = false; + state.active_hooks.clear(); + state.last_activity = Some(Utc::now()); + } + + tracing::info!("浏览器拦截器已禁用"); + Ok(()) + } + + /// 临时禁用拦截器 + pub async fn temporary_disable(&self, duration_seconds: u64) -> Result<()> { + self.disable_interceptor().await?; + + // 设置定时器 + { + let mut timer = self.temporary_disable_timer.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("设置定时器失败: {}", e)) + })?; + *timer = Some(Instant::now() + Duration::from_secs(duration_seconds)); + } + + // 启动后台任务来重新启用 + let state_manager = self.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(duration_seconds)).await; + if let Err(e) = state_manager.enable_interceptor().await { + tracing::error!("自动重新启用拦截器失败: {}", e); + } else { + tracing::info!("拦截器已自动重新启用"); + } + }); + + tracing::info!("拦截器已临时禁用 {} 秒", duration_seconds); + Ok(()) + } + + /// 恢复正常浏览器行为 + pub async fn restore_normal_behavior(&self) -> Result<()> { + // 先禁用拦截器 + self.disable_interceptor().await?; + + // 恢复系统状态 + self.restore_system_state().await?; + + { + let mut state = self + .state + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("写入状态失败: {}", e)))?; + state.can_restore = false; + } + + tracing::info!("已恢复正常浏览器行为"); + Ok(()) + } + + /// 增加拦截计数 + pub fn increment_intercept_count(&self) -> Result<()> { + let mut state = self + .state + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("写入状态失败: {}", e)))?; + + state.intercepted_count += 1; + state.last_activity = Some(Utc::now()); + + Ok(()) + } + + /// 添加活跃钩子 + pub fn add_active_hook(&self, hook_name: String) -> Result<()> { + let mut state = self + .state + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("写入状态失败: {}", e)))?; + + if !state.active_hooks.contains(&hook_name) { + state.active_hooks.push(hook_name); + } + + Ok(()) + } + + /// 移除活跃钩子 + pub fn remove_active_hook(&self, hook_name: &str) -> Result<()> { + let mut state = self + .state + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("写入状态失败: {}", e)))?; + + state.active_hooks.retain(|h| h != hook_name); + + Ok(()) + } + + /// 备份系统状态 + async fn backup_system_state(&self) -> Result<()> { + let system_state = SystemState { + default_browser: self.get_default_browser().await?, + registry_backup: self.backup_registry_keys().await?, + environment_backup: self.backup_environment_variables().await?, + timestamp: Utc::now(), + }; + + { + let mut backup = self.original_system_state.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("备份系统状态失败: {}", e)) + })?; + *backup = Some(system_state); + } + + tracing::info!("系统状态已备份"); + Ok(()) + } + + /// 恢复系统状态 + async fn restore_system_state(&self) -> Result<()> { + let backup = { + let backup_guard = self.original_system_state.read().map_err(|e| { + BrowserInterceptorError::StateError(format!("读取备份状态失败: {}", e)) + })?; + backup_guard.clone() + }; + + if let Some(system_state) = backup { + // 恢复默认浏览器 + if let Some(default_browser) = &system_state.default_browser { + self.restore_default_browser(default_browser).await?; + } + + // 恢复注册表项 + self.restore_registry_keys(&system_state.registry_backup) + .await?; + + // 恢复环境变量 + self.restore_environment_variables(&system_state.environment_backup) + .await?; + + tracing::info!("系统状态已恢复到 {} 的备份", system_state.timestamp); + } else { + tracing::warn!("没有找到系统状态备份"); + } + + Ok(()) + } + + /// 获取默认浏览器(平台特定实现) + async fn get_default_browser(&self) -> Result> { + #[cfg(target_os = "windows")] + { + self.get_default_browser_windows().await + } + + #[cfg(target_os = "macos")] + { + // macOS 实现 + // TODO: 使用 Launch Services API + Ok(None) + } + + #[cfg(target_os = "linux")] + { + // Linux 实现 + // TODO: 读取 xdg-settings + Ok(None) + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + Ok(None) + } + } + + /// Windows 平台获取默认浏览器 + #[cfg(target_os = "windows")] + async fn get_default_browser_windows(&self) -> Result> { + unsafe { + let key_path = to_wide_chars( + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", + ); + let mut key: HKEY = HKEY::default(); + + let result = RegOpenKeyExW( + HKEY_CURRENT_USER, + PCWSTR::from_raw(key_path.as_ptr()), + 0, + KEY_READ, + &mut key, + ); + + if result.is_ok() { + let mut buffer = vec![0u16; 1024]; + let mut buffer_size = buffer.len() * 2; + + let result = RegQueryValueExW( + key, + PCWSTR::from_raw(to_wide_chars("ProgId").as_ptr()), + None, + None, + Some(buffer.as_mut_ptr() as *mut u8), + Some(&mut buffer_size as *mut u32), + ); + + RegCloseKey(key); + + if result.is_ok() { + let prog_id = String::from_utf16_lossy(&buffer[..buffer_size / 2 - 1]); + return Ok(Some(prog_id)); + } + } + } + + Ok(None) + } + + /// 备份注册表项(Windows 特定) + async fn backup_registry_keys(&self) -> Result> { + let mut backup = std::collections::HashMap::new(); + + #[cfg(target_os = "windows")] + { + // 备份 HTTP 和 HTTPS 协议关联 + let keys_to_backup = [ + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice", + ]; + + for key_path in &keys_to_backup { + if let Ok(value) = self.read_registry_value(key_path, "ProgId").await { + backup.insert(key_path.to_string(), value); + } + } + } + + Ok(backup) + } + + /// 读取注册表值 + #[cfg(target_os = "windows")] + async fn read_registry_value(&self, key_path: &str, value_name: &str) -> Result { + unsafe { + let key_path_wide = to_wide_chars(key_path); + let mut key: HKEY = HKEY::default(); + + let result = RegOpenKeyExW( + HKEY_CURRENT_USER, + PCWSTR::from_raw(key_path_wide.as_ptr()), + 0, + KEY_READ, + &mut key, + ); + + if result.is_ok() { + let mut buffer = vec![0u16; 1024]; + let mut buffer_size = buffer.len() * 2; + + let result = RegQueryValueExW( + key, + PCWSTR::from_raw(to_wide_chars(value_name).as_ptr()), + None, + None, + Some(buffer.as_mut_ptr() as *mut u8), + Some(&mut buffer_size as *mut u32), + ); + + RegCloseKey(key); + + if result.is_ok() { + let value = String::from_utf16_lossy(&buffer[..buffer_size / 2 - 1]); + return Ok(value); + } + } + } + + Err(BrowserInterceptorError::StateError(format!( + "无法读取注册表项: {}", + key_path + ))) + } + + /// 备份环境变量 + async fn backup_environment_variables( + &self, + ) -> Result> { + let mut backup = std::collections::HashMap::new(); + + // 备份可能影响浏览器启动的环境变量 + if let Ok(browser) = std::env::var("BROWSER") { + backup.insert("BROWSER".to_string(), browser); + } + + Ok(backup) + } + + /// 恢复默认浏览器 + async fn restore_default_browser(&self, browser: &str) -> Result<()> { + #[cfg(target_os = "windows")] + { + self.restore_default_browser_windows(browser).await + } + + #[cfg(not(target_os = "windows"))] + { + tracing::info!("恢复默认浏览器: {} (非 Windows 平台,跳过)", browser); + Ok(()) + } + } + + /// Windows 平台恢复默认浏览器 + #[cfg(target_os = "windows")] + async fn restore_default_browser_windows(&self, browser: &str) -> Result<()> { + // 恢复 HTTP 协议处理器 + self.write_registry_value( + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", + "ProgId", + browser, + ) + .await?; + + // 恢复 HTTPS 协议处理器 + self.write_registry_value( + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice", + "ProgId", + browser, + ) + .await?; + + tracing::info!("已恢复默认浏览器为: {}", browser); + Ok(()) + } + + /// 恢复注册表项 + async fn restore_registry_keys( + &self, + backup: &std::collections::HashMap, + ) -> Result<()> { + #[cfg(target_os = "windows")] + { + for (key_path, value) in backup { + if let Err(e) = self.write_registry_value(key_path, "ProgId", value).await { + tracing::error!("恢复注册表项失败 {}: {}", key_path, e); + } + } + } + + Ok(()) + } + + /// 写入注册表值 + #[cfg(target_os = "windows")] + async fn write_registry_value( + &self, + key_path: &str, + value_name: &str, + value_data: &str, + ) -> Result<()> { + unsafe { + let key_path_wide = to_wide_chars(key_path); + let mut key: HKEY = HKEY::default(); + + let result = RegCreateKeyExW( + HKEY_CURRENT_USER, + PCWSTR::from_raw(key_path_wide.as_ptr()), + 0, + PCWSTR::null(), + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + None, + &mut key, + None, + ); + + if result.is_ok() { + let value_name_wide = to_wide_chars(value_name); + let value_data_wide = to_wide_chars(value_data); + + RegSetValueExW( + key, + PCWSTR::from_raw(value_name_wide.as_ptr()), + 0, + REG_SZ, + Some(value_data_wide.as_ptr() as *const u8), + value_data_wide.len() * 2, + ); + + RegCloseKey(key); + return Ok(()); + } + } + + Err(BrowserInterceptorError::StateError(format!( + "无法写入注册表项: {}", + key_path + ))) + } + + /// 恢复环境变量 + async fn restore_environment_variables( + &self, + backup: &std::collections::HashMap, + ) -> Result<()> { + for (key, value) in backup { + std::env::set_var(key, value); + } + Ok(()) + } +} + +impl Clone for StateManager { + fn clone(&self) -> Self { + Self { + state: Arc::clone(&self.state), + original_system_state: Arc::clone(&self.original_system_state), + temporary_disable_timer: Arc::clone(&self.temporary_disable_timer), + } + } +} + +impl Default for StateManager { + fn default() -> Self { + Self::new() + } +} + +/// Windows 辅助函数:将字符串转换为宽字符 +#[cfg(target_os = "windows")] +fn to_wide_chars(s: &str) -> Vec { + OsStr::new(s).encode_wide().chain(once(0)).collect() +} diff --git a/src-tauri/src/browser_interceptor/url_manager.rs b/src-tauri/src/browser_interceptor/url_manager.rs new file mode 100644 index 000000000..9d947a3b8 --- /dev/null +++ b/src-tauri/src/browser_interceptor/url_manager.rs @@ -0,0 +1,600 @@ +use crate::browser_interceptor::{BrowserInterceptorError, InterceptedUrl, Result}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::sync::{Arc, RwLock}; + +/// URL 管理器,负责管理被拦截的 URL +pub struct UrlManager { + intercepted_urls: Arc>>, + history: Arc>>, + max_history_size: usize, + storage_path: Option, +} + +impl UrlManager { + pub fn new() -> Self { + Self { + intercepted_urls: Arc::new(RwLock::new(HashMap::new())), + history: Arc::new(RwLock::new(Vec::new())), + max_history_size: 1000, // 最多保存 1000 条历史记录 + storage_path: None, + } + } + + /// 创建带持久化存储的 URL 管理器 + pub fn with_storage>(storage_path: P) -> Result { + let storage_path = storage_path.as_ref().to_string_lossy().to_string(); + let mut manager = Self { + intercepted_urls: Arc::new(RwLock::new(HashMap::new())), + history: Arc::new(RwLock::new(Vec::new())), + max_history_size: 1000, + storage_path: Some(storage_path.clone()), + }; + + // 从文件加载历史记录 + manager.load_from_storage()?; + + Ok(manager) + } + + /// 添加被拦截的 URL + pub fn add_intercepted_url(&self, url: String, source_process: String) -> Result { + let intercepted_url = InterceptedUrl::new(url, source_process); + let id = intercepted_url.id.clone(); + let url_for_log = intercepted_url.url.clone(); + let process_for_log = intercepted_url.source_process.clone(); + + // 添加到当前拦截列表 + { + let mut urls = self.intercepted_urls.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("添加拦截 URL 失败: {}", e)) + })?; + urls.insert(id.clone(), intercepted_url.clone()); + } + + // 添加到历史记录 + { + let mut history = self.history.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("添加历史记录失败: {}", e)) + })?; + + history.push(intercepted_url); + + // 限制历史记录大小 + if history.len() > self.max_history_size { + history.remove(0); + } + } + + // 自动保存 + let _ = self.auto_save(); + + tracing::info!( + "已添加拦截 URL: {} (来源: {})", + url_for_log, + process_for_log + ); + Ok(id) + } + + /// 获取所有当前拦截的 URL + pub fn get_intercepted_urls(&self) -> Result> { + let urls = self.intercepted_urls.read().map_err(|e| { + BrowserInterceptorError::StateError(format!("读取拦截 URL 失败: {}", e)) + })?; + + let mut result: Vec = urls.values().cloned().collect(); + result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); // 按时间倒序排列 + + Ok(result) + } + + /// 获取指定 ID 的拦截 URL + pub fn get_intercepted_url(&self, id: &str) -> Result> { + let urls = self.intercepted_urls.read().map_err(|e| { + BrowserInterceptorError::StateError(format!("读取拦截 URL 失败: {}", e)) + })?; + + Ok(urls.get(id).cloned()) + } + + /// 标记 URL 为已复制 + pub fn mark_as_copied(&self, id: &str) -> Result<()> { + let mut urls = self.intercepted_urls.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("更新拦截 URL 失败: {}", e)) + })?; + + if let Some(url) = urls.get_mut(id) { + url.copied = true; + tracing::info!("URL {} 已标记为已复制", id); + } + + Ok(()) + } + + /// 标记 URL 为已在浏览器中打开 + pub fn mark_as_opened(&self, id: &str) -> Result<()> { + let mut urls = self.intercepted_urls.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("更新拦截 URL 失败: {}", e)) + })?; + + if let Some(url) = urls.get_mut(id) { + url.opened_in_browser = true; + tracing::info!("URL {} 已标记为已在浏览器中打开", id); + } + + Ok(()) + } + + /// 忽略(移除)指定的 URL + pub fn dismiss_url(&self, id: &str) -> Result<()> { + let mut urls = self.intercepted_urls.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("移除拦截 URL 失败: {}", e)) + })?; + + if let Some(mut url) = urls.remove(id) { + url.dismissed = true; + + // 更新历史记录中的状态 + let mut history = self.history.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("更新历史记录失败: {}", e)) + })?; + + if let Some(history_url) = history.iter_mut().find(|u| u.id == id) { + history_url.dismissed = true; + } + + tracing::info!("URL {} 已被忽略", id); + } + + Ok(()) + } + + /// 清除所有当前拦截的 URL + pub fn clear_intercepted_urls(&self) -> Result<()> { + let mut urls = self.intercepted_urls.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("清除拦截 URL 失败: {}", e)) + })?; + + let count = urls.len(); + urls.clear(); + + tracing::info!("已清除 {} 个拦截的 URL", count); + Ok(()) + } + + /// 获取历史记录 + pub fn get_history(&self, limit: Option) -> Result> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let mut result = history.clone(); + result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); // 按时间倒序排列 + + if let Some(limit) = limit { + result.truncate(limit); + } + + Ok(result) + } + + /// 搜索历史记录 + pub fn search_history(&self, query: &str, limit: Option) -> Result> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("搜索历史记录失败: {}", e)))?; + + let query_lower = query.to_lowercase(); + let mut result: Vec = history + .iter() + .filter(|url| { + url.url.to_lowercase().contains(&query_lower) + || url.source_process.to_lowercase().contains(&query_lower) + }) + .cloned() + .collect(); + + result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + + if let Some(limit) = limit { + result.truncate(limit); + } + + Ok(result) + } + + /// 按日期范围获取历史记录 + pub fn get_history_by_date_range( + &self, + start: DateTime, + end: DateTime, + ) -> Result> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let mut result: Vec = history + .iter() + .filter(|url| url.timestamp >= start && url.timestamp <= end) + .cloned() + .collect(); + + result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + + Ok(result) + } + + /// 获取统计信息 + pub fn get_statistics(&self) -> Result { + let urls = self.intercepted_urls.read().map_err(|e| { + BrowserInterceptorError::StateError(format!("读取拦截 URL 失败: {}", e)) + })?; + + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let current_count = urls.len(); + let total_intercepted = history.len(); + let copied_count = history.iter().filter(|u| u.copied).count(); + let opened_count = history.iter().filter(|u| u.opened_in_browser).count(); + let dismissed_count = history.iter().filter(|u| u.dismissed).count(); + + // 统计来源进程 + let mut process_stats = HashMap::new(); + for url in history.iter() { + *process_stats.entry(url.source_process.clone()).or_insert(0) += 1; + } + + Ok(UrlStatistics { + current_intercepted: current_count, + total_intercepted, + copied_count, + opened_count, + dismissed_count, + process_stats, + }) + } + + /// 清理过期的历史记录 + pub fn cleanup_old_history(&self, days: u32) -> Result { + let cutoff_date = Utc::now() - chrono::Duration::days(days as i64); + + let mut history = self + .history + .write() + .map_err(|e| BrowserInterceptorError::StateError(format!("清理历史记录失败: {}", e)))?; + + let original_len = history.len(); + history.retain(|url| url.timestamp > cutoff_date); + let removed_count = original_len - history.len(); + + if removed_count > 0 { + tracing::info!("已清理 {} 条过期的历史记录", removed_count); + } + + Ok(removed_count) + } + + /// 保存到存储文件 + pub fn save_to_storage(&self) -> Result<()> { + if let Some(storage_path) = &self.storage_path { + let history = self.history.read().map_err(|e| { + BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)) + })?; + + let storage_data = UrlStorageData { + history: history.clone(), + max_history_size: self.max_history_size, + saved_at: Utc::now(), + }; + + let json_data = serde_json::to_string_pretty(&storage_data).map_err(|e| { + BrowserInterceptorError::StateError(format!("序列化数据失败: {}", e)) + })?; + + // 确保目录存在 + if let Some(parent) = Path::new(storage_path).parent() { + fs::create_dir_all(parent).map_err(|e| { + BrowserInterceptorError::StateError(format!("创建目录失败: {}", e)) + })?; + } + + fs::write(storage_path, json_data) + .map_err(|e| BrowserInterceptorError::StateError(format!("写入文件失败: {}", e)))?; + + tracing::info!("已保存历史记录到: {}", storage_path); + } + + Ok(()) + } + + /// 从存储文件加载 + pub fn load_from_storage(&mut self) -> Result<()> { + if let Some(storage_path) = &self.storage_path { + if Path::new(storage_path).exists() { + let json_data = fs::read_to_string(storage_path).map_err(|e| { + BrowserInterceptorError::StateError(format!("读取文件失败: {}", e)) + })?; + + let storage_data: UrlStorageData = + serde_json::from_str(&json_data).map_err(|e| { + BrowserInterceptorError::StateError(format!("反序列化数据失败: {}", e)) + })?; + + { + let mut history = self.history.write().map_err(|e| { + BrowserInterceptorError::StateError(format!("写入历史记录失败: {}", e)) + })?; + *history = storage_data.history; + } + + self.max_history_size = storage_data.max_history_size; + + tracing::info!("已从 {} 加载历史记录", storage_path); + } + } + + Ok(()) + } + + /// 导出历史记录为 JSON + pub fn export_history_json(&self, export_path: &str) -> Result<()> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let export_data = UrlExportData { + urls: history.clone(), + exported_at: Utc::now(), + total_count: history.len(), + }; + + let json_data = serde_json::to_string_pretty(&export_data).map_err(|e| { + BrowserInterceptorError::StateError(format!("序列化导出数据失败: {}", e)) + })?; + + // 确保目录存在 + if let Some(parent) = Path::new(export_path).parent() { + fs::create_dir_all(parent) + .map_err(|e| BrowserInterceptorError::StateError(format!("创建目录失败: {}", e)))?; + } + + fs::write(export_path, json_data) + .map_err(|e| BrowserInterceptorError::StateError(format!("写入导出文件失败: {}", e)))?; + + tracing::info!("已导出历史记录到: {}", export_path); + Ok(()) + } + + /// 导出历史记录为 CSV + pub fn export_history_csv(&self, export_path: &str) -> Result<()> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let mut csv_content = + String::from("ID,URL,Source Process,Timestamp,Copied,Opened in Browser,Dismissed\n"); + + for url in history.iter() { + csv_content.push_str(&format!( + "{},{},{},{},{},{},{}\n", + url.id, + url.url.replace(",", "%2C"), // 转义逗号 + url.source_process, + url.timestamp.format("%Y-%m-%d %H:%M:%S UTC"), + url.copied, + url.opened_in_browser, + url.dismissed + )); + } + + // 确保目录存在 + if let Some(parent) = Path::new(export_path).parent() { + fs::create_dir_all(parent) + .map_err(|e| BrowserInterceptorError::StateError(format!("创建目录失败: {}", e)))?; + } + + fs::write(export_path, csv_content).map_err(|e| { + BrowserInterceptorError::StateError(format!("写入 CSV 文件失败: {}", e)) + })?; + + tracing::info!("已导出历史记录到 CSV: {}", export_path); + Ok(()) + } + + /// 验证 URL 是否有效 + pub fn validate_url(url: &str) -> bool { + // 基本的 URL 验证 + if url.is_empty() { + return false; + } + + // 检查是否包含必要的协议 + if !url.starts_with("http://") && !url.starts_with("https://") { + return false; + } + + // 使用 url crate 进行更严格的验证 + if let Ok(_) = url::Url::parse(url) { + return true; + } + + false + } + + /// 按来源进程过滤历史记录 + pub fn get_history_by_process( + &self, + process: &str, + limit: Option, + ) -> Result> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let mut result: Vec = history + .iter() + .filter(|url| url.source_process.eq_ignore_ascii_case(process)) + .cloned() + .collect(); + + result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + + if let Some(limit) = limit { + result.truncate(limit); + } + + Ok(result) + } + + /// 获取所有唯一的来源进程列表 + pub fn get_unique_processes(&self) -> Result> { + let history = self + .history + .read() + .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; + + let mut processes: Vec = history + .iter() + .map(|url| url.source_process.clone()) + .collect::>() + .into_iter() + .collect(); + + processes.sort(); + Ok(processes) + } + + /// 自动保存(如果已配置存储路径) + fn auto_save(&self) -> Result<()> { + if self.storage_path.is_some() { + self.save_to_storage()?; + } + Ok(()) + } +} + +/// 存储数据结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +struct UrlStorageData { + history: Vec, + max_history_size: usize, + saved_at: DateTime, +} + +/// 导出数据结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +struct UrlExportData { + urls: Vec, + exported_at: DateTime, + total_count: usize, +} + +/// URL 统计信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UrlStatistics { + pub current_intercepted: usize, + pub total_intercepted: usize, + pub copied_count: usize, + pub opened_count: usize, + pub dismissed_count: usize, + pub process_stats: HashMap, +} + +impl Default for UrlManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_and_get_intercepted_url() { + let manager = UrlManager::new(); + + let id = manager + .add_intercepted_url( + "https://accounts.google.com/oauth/authorize".to_string(), + "kiro".to_string(), + ) + .unwrap(); + + let urls = manager.get_intercepted_urls().unwrap(); + assert_eq!(urls.len(), 1); + assert_eq!(urls[0].id, id); + assert_eq!(urls[0].url, "https://accounts.google.com/oauth/authorize"); + assert_eq!(urls[0].source_process, "kiro"); + } + + #[test] + fn test_mark_as_copied() { + let manager = UrlManager::new(); + + let id = manager + .add_intercepted_url("https://test.com".to_string(), "test".to_string()) + .unwrap(); + + manager.mark_as_copied(&id).unwrap(); + + let url = manager.get_intercepted_url(&id).unwrap().unwrap(); + assert!(url.copied); + } + + #[test] + fn test_dismiss_url() { + let manager = UrlManager::new(); + + let id = manager + .add_intercepted_url("https://test.com".to_string(), "test".to_string()) + .unwrap(); + + manager.dismiss_url(&id).unwrap(); + + let urls = manager.get_intercepted_urls().unwrap(); + assert_eq!(urls.len(), 0); + + // 但历史记录中应该还存在 + let history = manager.get_history(None).unwrap(); + assert_eq!(history.len(), 1); + assert!(history[0].dismissed); + } + + #[test] + fn test_search_history() { + let manager = UrlManager::new(); + + manager + .add_intercepted_url( + "https://accounts.google.com/oauth".to_string(), + "kiro".to_string(), + ) + .unwrap(); + + manager + .add_intercepted_url("https://github.com/login".to_string(), "cursor".to_string()) + .unwrap(); + + let results = manager.search_history("google", None).unwrap(); + assert_eq!(results.len(), 1); + assert!(results[0].url.contains("google")); + + let results = manager.search_history("cursor", None).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].source_process, "cursor"); + } +} diff --git a/src-tauri/src/commands/browser_interceptor_cmd.rs b/src-tauri/src/commands/browser_interceptor_cmd.rs new file mode 100644 index 000000000..c82a665e2 --- /dev/null +++ b/src-tauri/src/commands/browser_interceptor_cmd.rs @@ -0,0 +1,360 @@ +use crate::browser_interceptor::{ + BrowserInterceptor, BrowserInterceptorConfig, InterceptedUrl, InterceptorState, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tauri::{AppHandle, State}; +use tokio::sync::RwLock; + +/// 浏览器拦截器状态封装 +pub struct BrowserInterceptorState(pub Arc>>); + +impl Default for BrowserInterceptorState { + fn default() -> Self { + Self(Arc::new(RwLock::new(None))) + } +} + +/// 获取拦截器状态 +#[tauri::command] +pub async fn get_browser_interceptor_state( + state: State<'_, BrowserInterceptorState>, +) -> Result, String> { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.get_state().await { + Ok(state) => Ok(Some(state)), + Err(e) => Err(format!("获取拦截器状态失败: {}", e)), + } + } else { + Ok(None) + } +} + +/// 启动浏览器拦截器 +#[tauri::command] +pub async fn start_browser_interceptor( + state: State<'_, BrowserInterceptorState>, + config: BrowserInterceptorConfig, +) -> Result { + let mut interceptor_guard = state.0.write().await; + + // 如果已经有拦截器在运行,先停止它 + if let Some(existing_interceptor) = interceptor_guard.as_mut() { + if let Err(e) = existing_interceptor.stop().await { + tracing::warn!("停止现有拦截器时出错: {}", e); + } + } + + // 创建新的拦截器 + let mut interceptor = BrowserInterceptor::new(config); + + match interceptor.start().await { + Ok(_) => { + *interceptor_guard = Some(interceptor); + Ok("浏览器拦截器已启动".to_string()) + } + Err(e) => Err(format!("启动浏览器拦截器失败: {}", e)), + } +} + +/// 停止浏览器拦截器 +#[tauri::command] +pub async fn stop_browser_interceptor( + state: State<'_, BrowserInterceptorState>, +) -> Result { + let mut interceptor_guard = state.0.write().await; + + if let Some(interceptor) = interceptor_guard.as_mut() { + match interceptor.stop().await { + Ok(_) => { + *interceptor_guard = None; + Ok("浏览器拦截器已停止".to_string()) + } + Err(e) => Err(format!("停止浏览器拦截器失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 恢复正常浏览器行为 +#[tauri::command] +pub async fn restore_normal_browser_behavior( + state: State<'_, BrowserInterceptorState>, +) -> Result { + let mut interceptor_guard = state.0.write().await; + + if let Some(interceptor) = interceptor_guard.as_mut() { + match interceptor.restore_normal_behavior().await { + Ok(_) => Ok("已恢复正常浏览器行为".to_string()), + Err(e) => Err(format!("恢复正常浏览器行为失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 临时禁用拦截器 +#[tauri::command] +pub async fn temporary_disable_interceptor( + state: State<'_, BrowserInterceptorState>, + duration_seconds: u64, +) -> Result { + let mut interceptor_guard = state.0.write().await; + + if let Some(interceptor) = interceptor_guard.as_mut() { + match interceptor.temporary_disable(duration_seconds).await { + Ok(_) => Ok(format!("拦截器已临时禁用 {} 秒", duration_seconds)), + Err(e) => Err(format!("临时禁用拦截器失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 获取拦截的 URL 列表 +#[tauri::command] +pub async fn get_intercepted_urls( + state: State<'_, BrowserInterceptorState>, +) -> Result, String> { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.get_intercepted_urls().await { + Ok(urls) => Ok(urls), + Err(e) => Err(format!("获取拦截 URL 失败: {}", e)), + } + } else { + Ok(Vec::new()) + } +} + +/// 获取历史记录 +#[tauri::command] +pub async fn get_interceptor_history( + state: State<'_, BrowserInterceptorState>, + limit: Option, +) -> Result, String> { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.get_history(limit).await { + Ok(history) => Ok(history), + Err(e) => Err(format!("获取历史记录失败: {}", e)), + } + } else { + Ok(Vec::new()) + } +} + +/// 复制 URL 到剪贴板 +#[tauri::command] +pub async fn copy_intercepted_url_to_clipboard( + state: State<'_, BrowserInterceptorState>, + url_id: String, +) -> Result { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.copy_url_to_clipboard(&url_id).await { + Ok(_) => Ok("URL 已复制到剪贴板".to_string()), + Err(e) => Err(format!("复制 URL 失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 在指纹浏览器中打开 URL +#[tauri::command] +pub async fn open_url_in_fingerprint_browser( + state: State<'_, BrowserInterceptorState>, + url_id: String, +) -> Result { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.open_in_fingerprint_browser(&url_id).await { + Ok(_) => Ok("URL 已在指纹浏览器中打开".to_string()), + Err(e) => Err(format!("在指纹浏览器中打开 URL 失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 忽略指定的 URL +#[tauri::command] +pub async fn dismiss_intercepted_url( + state: State<'_, BrowserInterceptorState>, + url_id: String, +) -> Result { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.dismiss_url(&url_id).await { + Ok(_) => Ok("URL 已忽略".to_string()), + Err(e) => Err(format!("忽略 URL 失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 更新拦截器配置 +#[tauri::command] +pub async fn update_browser_interceptor_config( + state: State<'_, BrowserInterceptorState>, + config: BrowserInterceptorConfig, +) -> Result { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + match interceptor.update_config(config).await { + Ok(_) => Ok("拦截器配置已更新".to_string()), + Err(e) => Err(format!("更新拦截器配置失败: {}", e)), + } + } else { + Err("拦截器未运行".to_string()) + } +} + +/// 获取默认配置 +#[tauri::command] +pub async fn get_default_browser_interceptor_config() -> Result { + Ok(BrowserInterceptorConfig::default()) +} + +/// 验证配置 +#[tauri::command] +pub async fn validate_browser_interceptor_config( + config: BrowserInterceptorConfig, +) -> Result { + match config.validate() { + Ok(_) => Ok("配置验证通过".to_string()), + Err(e) => Err(format!("配置验证失败: {}", e)), + } +} + +/// 检查拦截器是否正在运行 +#[tauri::command] +pub async fn is_browser_interceptor_running( + state: State<'_, BrowserInterceptorState>, +) -> Result { + let interceptor_guard = state.0.read().await; + Ok(interceptor_guard.is_some()) +} + +/// 获取拦截器统计信息 +#[derive(Debug, Serialize, Deserialize)] +pub struct InterceptorStatistics { + pub total_intercepted: usize, + pub current_intercepted: usize, + pub copied_count: usize, + pub opened_count: usize, + pub dismissed_count: usize, +} + +#[tauri::command] +pub async fn get_browser_interceptor_statistics( + state: State<'_, BrowserInterceptorState>, +) -> Result { + let interceptor_guard = state.0.read().await; + + if let Some(interceptor) = interceptor_guard.as_ref() { + // 获取当前拦截的 URL + let current_urls = interceptor + .get_intercepted_urls() + .await + .map_err(|e| format!("获取当前 URL 失败: {}", e))?; + + // 获取历史记录 + let history = interceptor + .get_history(None) + .await + .map_err(|e| format!("获取历史记录失败: {}", e))?; + + let copied_count = history.iter().filter(|u| u.copied).count(); + let opened_count = history.iter().filter(|u| u.opened_in_browser).count(); + let dismissed_count = history.iter().filter(|u| u.dismissed).count(); + + Ok(InterceptorStatistics { + total_intercepted: history.len(), + current_intercepted: current_urls.len(), + copied_count, + opened_count, + dismissed_count, + }) + } else { + Ok(InterceptorStatistics { + total_intercepted: 0, + current_intercepted: 0, + copied_count: 0, + opened_count: 0, + dismissed_count: 0, + }) + } +} + +/// 通知相关结构体 +#[derive(Debug, Serialize, Deserialize)] +pub struct NotificationPayload { + pub title: String, + pub body: String, + pub icon: Option, +} + +/// 显示系统通知 +#[tauri::command] +pub async fn show_notification( + _app: AppHandle, + title: String, + body: String, + _icon: Option, +) -> Result { + // 简化实现,返回成功但实际功能待实现 + tracing::info!("显示通知: {} - {}", title, body); + Ok("通知已记录到日志".to_string()) +} + +/// 显示URL拦截通知 +#[tauri::command] +pub async fn show_url_intercept_notification( + app: AppHandle, + url: String, + source_process: String, +) -> Result { + let title = "🔐 拦截到新的URL".to_string(); + let body = format!("来自 {}: {}", source_process, truncate_url(&url, 60)); + + show_notification(app, title, body, Some("icon".to_string())).await +} + +/// 显示状态变更通知 +#[tauri::command] +pub async fn show_status_notification( + app: AppHandle, + message: String, + notification_type: String, +) -> Result { + let (icon, title) = match notification_type.as_str() { + "success" => ("✅", "操作成功"), + "warning" => ("⚠️", "警告"), + "error" => ("❌", "错误"), + _ => ("ℹ️", "信息"), + }; + + let title = format!("{} {}", icon, title); + show_notification(app, title, message, None).await +} + +/// 截断URL用于通知显示 +fn truncate_url(url: &str, max_length: usize) -> String { + if url.len() <= max_length { + url.to_string() + } else { + format!("{}...", &url[0..max_length]) + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 22be44dab..968d04cf9 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod auto_fix_cmd; +pub mod browser_interceptor_cmd; pub mod config_cmd; pub mod flow_monitor_cmd; pub mod injection_cmd; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 393643034..7b5802b9f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +pub mod browser_interceptor; mod commands; mod config; mod converter; @@ -27,6 +28,7 @@ use std::sync::Arc; use tauri::{Manager, Runtime}; use tokio::sync::RwLock; +use commands::browser_interceptor_cmd::BrowserInterceptorState; use commands::flow_monitor_cmd::{ BatchOperationsState, BookmarkManagerState, EnhancedStatsServiceState, FlowInterceptorState, FlowMonitorState, FlowQueryServiceState, FlowReplayerState, QuickFilterManagerState, @@ -1633,6 +1635,9 @@ pub fn run() { )); let batch_operations_state = BatchOperationsState(batch_operations); + // Initialize BrowserInterceptorState + let browser_interceptor_state = BrowserInterceptorState::default(); + // FlowQueryService 需要 file_store,如果没有则创建一个临时的 let flow_query_service_state = if let Some(file_store) = flow_file_store { let query_service = FlowQueryService::new(flow_monitor.memory_store(), file_store); @@ -1668,13 +1673,46 @@ pub fn run() { let flow_monitor_clone = flow_monitor.clone(); let flow_interceptor_clone = flow_interceptor.clone(); - tauri::Builder::default() + let mut builder = tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--minimized"]), )) + // 单实例插件:当第二个实例启动时,将 URL 传递给第一个实例 + .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { + tracing::info!("[单实例] 收到来自新实例的参数: {:?}", args); + + // 处理传入的 URL 参数 + for arg in args.iter().skip(1) { + // 跳过第一个参数(程序路径) + if arg.starts_with("http://") || arg.starts_with("https://") { + tracing::info!("[单实例] 收到 URL: {}", arg); + + #[cfg(target_os = "macos")] + { + crate::browser_interceptor::platform::macos::handle_deep_link_url( + arg.clone(), + ); + } + } + } + + // 将窗口带到前台 + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + })); + + // 添加 Deep Link 插件(用于浏览器拦截) + #[cfg(target_os = "macos")] + { + builder = builder.plugin(tauri_plugin_deep_link::init()); + } + + builder .manage(state) .manage(logs) .manage(db) @@ -1695,6 +1733,7 @@ pub fn run() { .manage(bookmark_manager_state) .manage(enhanced_stats_service_state) .manage(batch_operations_state) + .manage(browser_interceptor_state) .on_window_event(move |window, event| { // 处理窗口关闭事件 if let tauri::WindowEvent::CloseRequested { api, .. } = event { @@ -1721,6 +1760,21 @@ pub fn run() { } }) .setup(move |app| { + // 设置 deep-link 事件监听(用于浏览器拦截) + #[cfg(target_os = "macos")] + { + use tauri_plugin_deep_link::DeepLinkExt; + let _listener_id = app.deep_link().on_open_url(|event| { + for url in event.urls() { + tracing::info!("[Deep Link] 收到 URL: {}", url); + crate::browser_interceptor::platform::macos::handle_deep_link_url( + url.to_string(), + ); + } + }); + tracing::info!("[启动] Deep Link 事件监听已设置"); + } + // 初始化托盘管理器 // Requirements 1.4: 应用启动时显示停止状态图标 match TrayManager::new(app.handle()) { @@ -2239,6 +2293,26 @@ pub fn run() { commands::window_cmd::set_window_size_by_option, commands::window_cmd::toggle_fullscreen, commands::window_cmd::is_fullscreen, + // Browser Interceptor commands + commands::browser_interceptor_cmd::get_browser_interceptor_state, + commands::browser_interceptor_cmd::start_browser_interceptor, + commands::browser_interceptor_cmd::stop_browser_interceptor, + commands::browser_interceptor_cmd::restore_normal_browser_behavior, + commands::browser_interceptor_cmd::temporary_disable_interceptor, + commands::browser_interceptor_cmd::get_intercepted_urls, + commands::browser_interceptor_cmd::get_interceptor_history, + commands::browser_interceptor_cmd::copy_intercepted_url_to_clipboard, + commands::browser_interceptor_cmd::open_url_in_fingerprint_browser, + commands::browser_interceptor_cmd::dismiss_intercepted_url, + commands::browser_interceptor_cmd::update_browser_interceptor_config, + commands::browser_interceptor_cmd::get_default_browser_interceptor_config, + commands::browser_interceptor_cmd::validate_browser_interceptor_config, + commands::browser_interceptor_cmd::is_browser_interceptor_running, + commands::browser_interceptor_cmd::get_browser_interceptor_statistics, + // Browser Interceptor notification commands + commands::browser_interceptor_cmd::show_notification, + commands::browser_interceptor_cmd::show_url_intercept_notification, + commands::browser_interceptor_cmd::show_status_notification, // Auto fix commands commands::auto_fix_cmd::auto_fix_configuration, // Network commands diff --git a/src-tauri/src/middleware/tests.rs b/src-tauri/src/middleware/tests.rs index 92832dd57..60265ca72 100644 --- a/src-tauri/src/middleware/tests.rs +++ b/src-tauri/src/middleware/tests.rs @@ -136,7 +136,15 @@ fn test_management_auth_rate_limit_after_failures() { let rt = tokio::runtime::Runtime::new().unwrap(); // 使用唯一的 IP 地址避免测试间干扰 - let client_ip = format!("203.0.113.{}", std::process::id() % 256); + // 使用时间戳和进程ID组合来确保唯一性 + let unique_id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + let client_ip = format!( + "203.0.113.{}", + (unique_id ^ std::process::id() as u64) % 256 + ); let addr: SocketAddr = format!("{}:12345", client_ip).parse().unwrap(); for _ in 0..5 { diff --git a/src-tauri/src/services/live_sync.rs b/src-tauri/src/services/live_sync.rs index e45b18980..d28a7a4b6 100644 --- a/src-tauri/src/services/live_sync.rs +++ b/src-tauri/src/services/live_sync.rs @@ -2,6 +2,51 @@ use crate::models::{AppType, Provider}; use serde_json::{json, Value}; use std::path::PathBuf; +/// 原子写入 JSON 文件,防止配置损坏 +/// 参考 cc-switch 的实现:使用临时文件 + 重命名的原子操作 +fn write_json_file_atomic( + path: &std::path::Path, + value: &Value, +) -> Result<(), Box> { + use std::fs; + use std::io::Write; + + // 确保目录存在 + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + // 创建临时文件 + let temp_path = path.with_extension("tmp"); + + // 写入临时文件 + let content = serde_json::to_string_pretty(value)?; + let mut temp_file = fs::File::create(&temp_path)?; + temp_file.write_all(content.as_bytes())?; + temp_file.flush()?; + drop(temp_file); // 确保文件句柄被释放 + + // 验证 JSON 格式正确性 + let verify_content = fs::read_to_string(&temp_path)?; + let _: Value = serde_json::from_str(&verify_content)?; // 验证解析 + + // 原子性重命名 + fs::rename(&temp_path, path)?; + + tracing::info!("Successfully wrote config file: {}", path.display()); + Ok(()) +} + +/// 创建配置文件的备份 +fn create_backup(path: &std::path::Path) -> Result<(), Box> { + if path.exists() { + let backup_path = path.with_extension("bak"); + std::fs::copy(path, &backup_path)?; + tracing::info!("Created backup: {}", backup_path.display()); + } + Ok(()) +} + /// Get the configuration file path for an app type #[allow(dead_code)] pub fn get_app_config_path(app_type: &AppType) -> Option { @@ -64,6 +109,11 @@ fn sync_claude_settings( let claude_dir = home.join(".claude"); let config_path = claude_dir.join("settings.json"); + tracing::info!("开始同步 Claude 配置: {}", provider.name); + + // 创建备份(如果文件存在) + create_backup(&config_path)?; + // Ensure .claude directory exists if !claude_dir.exists() { std::fs::create_dir_all(&claude_dir)?; @@ -72,7 +122,13 @@ fn sync_claude_settings( // Read existing settings to preserve other fields let mut settings: Value = if config_path.exists() { let content = std::fs::read_to_string(&config_path)?; - serde_json::from_str(&content).unwrap_or_else(|_| json!({})) + match serde_json::from_str(&content) { + Ok(v) => v, + Err(e) => { + tracing::warn!("配置文件格式有误,使用默认配置: {}", e); + json!({}) + } + } } else { json!({}) }; @@ -93,20 +149,22 @@ fn sync_claude_settings( if let Some(target_env) = settings_obj.get_mut("env").and_then(|v| v.as_object_mut()) { for (key, value) in env_obj { target_env.insert(key.clone(), value.clone()); + tracing::debug!("设置环境变量: {} = [MASKED]", key); } } } else { // If settings_config is the full settings object, use it directly settings = provider.settings_config.clone(); + tracing::debug!("使用完整配置对象"); } // 清理冲突的认证环境变量 clean_claude_auth_conflict(&mut settings); - // Write settings - let content = serde_json::to_string_pretty(&settings)?; - std::fs::write(&config_path, content)?; + // 使用原子写入 + write_json_file_atomic(&config_path, &settings)?; + tracing::info!("Claude 配置同步完成: {}", config_path.display()); Ok(()) } diff --git a/src-tauri/src/services/switch.rs b/src-tauri/src/services/switch.rs index 16c102f44..930af316e 100644 --- a/src-tauri/src/services/switch.rs +++ b/src-tauri/src/services/switch.rs @@ -86,42 +86,98 @@ impl SwitchService { } pub fn switch_provider(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + use tracing::{error, info, warn}; + + info!("开始切换 {} 配置到 provider: {}", app_type, id); + let conn = db.lock().map_err(|e| e.to_string())?; // Get target provider let target_provider = ProviderDao::get_by_id(&conn, app_type, id) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("Provider not found: {id}"))?; + .map_err(|e| { + error!("查找目标 provider 失败: {}", e); + e.to_string() + })? + .ok_or_else(|| { + error!("目标 provider 不存在: {}", id); + format!("Provider not found: {id}") + })?; - let app_type_enum = app_type.parse::().map_err(|e| e.to_string())?; + let app_type_enum = app_type.parse::().map_err(|e| { + error!("无效的 app_type: {} - {}", app_type, e); + e.to_string() + })?; - // Skip backfill and sync for ProxyCast + // 获取当前 provider(用于回填和回滚) + let current_provider = if app_type_enum != AppType::ProxyCast { + ProviderDao::get_current(&conn, app_type).map_err(|e| { + error!("获取当前 provider 失败: {}", e); + e.to_string() + })? + } else { + None + }; + + // 实施事务保护:先尝试同步,再更新数据库 if app_type_enum != AppType::ProxyCast { - // Backfill: Read current live config and save to current provider - if let Some(current_provider) = - ProviderDao::get_current(&conn, app_type).map_err(|e| e.to_string())? - { - // Only backfill if switching to a different provider - if current_provider.id != id { - if let Ok(live_settings) = live_sync::read_live_settings(&app_type_enum) { - // Update current provider with live settings - let mut updated_provider = current_provider.clone(); - updated_provider.settings_config = live_settings; - let _ = ProviderDao::update(&conn, &updated_provider); + // Step 1: Backfill - 回填当前配置 + if let Some(ref current) = current_provider { + if current.id != id { + info!("回填当前配置: {}", current.name); + match live_sync::read_live_settings(&app_type_enum) { + Ok(live_settings) => { + let mut updated_provider = current.clone(); + updated_provider.settings_config = live_settings; + if let Err(e) = ProviderDao::update(&conn, &updated_provider) { + warn!("回填配置失败,但继续执行: {}", e); + } else { + info!("回填配置完成"); + } + } + Err(e) => { + warn!("读取当前配置失败,跳过回填: {}", e); + } } } } + + // Step 2: 尝试同步新配置(在更新数据库前验证) + info!("验证目标配置可同步性"); + if let Err(sync_error) = live_sync::sync_to_live(&app_type_enum, &target_provider) { + error!("配置同步失败: {}", sync_error); + + // 尝试恢复原配置(如果有) + if let Some(ref current) = current_provider { + warn!("尝试恢复原配置: {}", current.name); + if let Err(restore_error) = live_sync::sync_to_live(&app_type_enum, current) { + error!("恢复原配置失败: {}", restore_error); + return Err(format!("切换失败且无法恢复原配置: {}", sync_error)); + } + } + + return Err(format!("配置同步失败: {}", sync_error)); + } } - // Set new current provider - ProviderDao::set_current(&conn, app_type, id).map_err(|e| e.to_string())?; + // Step 3: 更新数据库(同步成功后) + info!("更新数据库中的当前 provider"); + if let Err(db_error) = ProviderDao::set_current(&conn, app_type, id) { + error!("数据库更新失败: {}", db_error); - // Sync target provider to live config - if app_type_enum != AppType::ProxyCast { - live_sync::sync_to_live(&app_type_enum, &target_provider) - .map_err(|e| format!("Failed to sync: {e}"))?; + // 如果数据库更新失败,尝试恢复原配置文件 + if app_type_enum != AppType::ProxyCast { + if let Some(ref current) = current_provider { + warn!("数据库更新失败,尝试恢复原配置文件"); + if let Err(restore_error) = live_sync::sync_to_live(&app_type_enum, current) { + error!("恢复配置文件失败: {}", restore_error); + } + } + } + + return Err(db_error.to_string()); } + info!("配置切换成功: {} -> {}", app_type, target_provider.name); Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 808abf4ee..a379902f1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", -"version": "0.18.0", + "version": "0.19.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", @@ -37,11 +37,23 @@ "resources": [ "icons/tray/*", "../scripts/playwright-login/**/*" - ] + ], + "macOS": { + "entitlements": null, + "exceptionDomain": null, + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + } }, "plugins": { "shell": { "open": true + }, + "deep-link": { + "desktop": { + "schemes": ["proxycast"] + } } } } diff --git a/src/App.tsx b/src/App.tsx index 5c642da0e..568739326 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,8 @@ import { ApiServerPage } from "./components/api-server/ApiServerPage"; import { ProviderPoolPage } from "./components/provider-pool"; import { ConfigManagementPage } from "./components/config/ConfigManagementPage"; import { FlowMonitorPage } from "./pages"; +import { ToolsPage } from "./components/tools/ToolsPage"; +import { BrowserInterceptorTool } from "./components/tools/browser-interceptor/BrowserInterceptorTool"; import { flowEventManager } from "./lib/flowEventManager"; type Page = @@ -12,6 +14,8 @@ type Page = | "config-management" | "api-server" | "flow-monitor" + | "tools" + | "browser-interceptor" | "settings"; function App() { @@ -33,6 +37,10 @@ function App() { return ; case "flow-monitor": return ; + case "tools": + return ; + case "browser-interceptor": + return ; case "settings": return ; default: diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 75d8e98bc..e0c14236d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,4 +1,11 @@ -import { Settings, Globe, Database, FileCode, Activity } from "lucide-react"; +import { + Settings, + Globe, + Database, + FileCode, + Activity, + Wrench, +} from "lucide-react"; import { cn } from "@/lib/utils"; type Page = @@ -6,6 +13,8 @@ type Page = | "config-management" | "api-server" | "flow-monitor" + | "tools" + | "browser-interceptor" | "settings"; interface SidebarProps { @@ -18,6 +27,7 @@ const navItems = [ { id: "provider-pool" as Page, label: "凭证池", icon: Database }, { id: "config-management" as Page, label: "配置管理", icon: FileCode }, { id: "flow-monitor" as Page, label: "Flow Monitor", icon: Activity }, + { id: "tools" as Page, label: "工具", icon: Wrench }, { id: "settings" as Page, label: "设置", icon: Settings }, ]; diff --git a/src/components/clients/ProviderCard.tsx b/src/components/clients/ProviderCard.tsx index 19cea2b2f..cac7840fa 100644 --- a/src/components/clients/ProviderCard.tsx +++ b/src/components/clients/ProviderCard.tsx @@ -1,7 +1,8 @@ -import { Check, Edit2, Trash2 } from "lucide-react"; +import { Check, Edit2, Trash2, Loader2 } from "lucide-react"; import { Provider } from "@/lib/api/switch"; import { cn } from "@/lib/utils"; import { ProviderIcon } from "@/icons/providers"; +import { useState } from "react"; // 从供应商名称和分类推断图标类型 function getProviderTypeFromName(name: string, category: string): string { @@ -41,6 +42,7 @@ interface ProviderCardProps { onSwitch: () => void; onEdit: () => void; onDelete: () => void; + switching?: boolean; } export function ProviderCard({ @@ -49,93 +51,146 @@ export function ProviderCard({ onSwitch, onEdit, onDelete, + switching = false, }: ProviderCardProps) { + const [showConfirm, setShowConfirm] = useState(false); const isProxyCast = provider.category === "custom" && provider.name === "ProxyCast"; + const handleClick = () => { + if (isCurrent || switching) return; + + // 对于关键配置,显示确认对话框 + if (provider.category === "official" || isProxyCast) { + setShowConfirm(true); + } else { + onSwitch(); + } + }; + + const handleConfirmSwitch = () => { + setShowConfirm(false); + onSwitch(); + }; + return ( -
!isCurrent && onSwitch()} - className={cn( - "group relative rounded-xl border p-3 transition-all cursor-pointer", - isCurrent - ? "border-primary bg-gradient-to-r from-primary/10 to-transparent shadow-sm" - : "hover:border-primary/50 hover:shadow-md", - )} - > - {/* 选中标记 */} - {isCurrent && ( -
- -
- )} - -
- {/* 图标 */} -
- -
- - {/* 名称和分类 */} -
-
-

{provider.name}

- {provider.category && ( - - {provider.category} - - )} + <> +
+ {/* 选中标记或切换状态 */} + {switching ? ( +
+
- {isProxyCast ? ( -

- 凭证池 → 标准 API -

- ) : provider.notes ? ( -

- {provider.notes} -

- ) : null} -
+ ) : isCurrent ? ( +
+ +
+ ) : null} - {/* 操作按钮 */} -
e.stopPropagation()} - > - - + +
+ + {/* 名称和分类 */} +
+
+

{provider.name}

+ {provider.category && ( + + {provider.category} + + )} +
+ {isProxyCast ? ( +

+ 凭证池 → 标准 API +

+ ) : provider.notes ? ( +

+ {provider.notes} +

+ ) : null} +
+ + {/* 操作按钮 */} +
e.stopPropagation()} + > + + +
-
+ + {/* 切换确认对话框 */} + {showConfirm && ( +
+
+

确认切换配置

+

+ 您正在切换到 "{provider.name}" 配置,这将更改当前的 API + 设置。确定要继续吗? +

+
+ + +
+
+
+ )} + ); } diff --git a/src/components/clients/ProviderList.tsx b/src/components/clients/ProviderList.tsx index 0e9be64aa..d44cbb8a5 100644 --- a/src/components/clients/ProviderList.tsx +++ b/src/components/clients/ProviderList.tsx @@ -210,6 +210,7 @@ export function ProviderList({ appType }: ProviderListProps) { const [showSyncDialog, setShowSyncDialog] = useState(false); const [syncResult, setSyncResult] = useState(null); const [checkingSync, setCheckingSync] = useState(false); + const [switchingId, setSwitchingId] = useState(null); // 实际生效的配置 const [liveConfig, setLiveConfig] = useState | null>( @@ -284,12 +285,15 @@ export function ProviderList({ appType }: ProviderListProps) { // 切换到匹配的 provider const handleSwitchToMatching = async (provider: Provider) => { try { + setSwitchingId(provider.id); await switchToProvider(provider.id); // 切换后重新读取实际配置,更新 UI 状态 const config = await switchApi.readLiveSettings(appType); setLiveConfig(config); } catch (e) { - alert("切换失败: " + (e instanceof Error ? e.message : String(e))); + console.error("切换失败:", e); + } finally { + setSwitchingId(null); } }; @@ -523,7 +527,20 @@ export function ProviderList({ appType }: ProviderListProps) { key={provider.id} provider={provider} isCurrent={provider.id === currentProvider?.id} - onSwitch={() => switchToProvider(provider.id)} + switching={switchingId === provider.id} + onSwitch={async () => { + try { + setSwitchingId(provider.id); + await switchToProvider(provider.id); + // 切换后重新读取实际配置 + const config = await switchApi.readLiveSettings(appType); + setLiveConfig(config); + } catch (e) { + console.error("切换失败:", e); + } finally { + setSwitchingId(null); + } + }} onEdit={() => handleEdit(provider)} onDelete={() => handleDeleteClick(provider.id)} /> diff --git a/src/components/tools/ToolsPage.tsx b/src/components/tools/ToolsPage.tsx new file mode 100644 index 000000000..cb2cf5cf1 --- /dev/null +++ b/src/components/tools/ToolsPage.tsx @@ -0,0 +1,144 @@ +import React, { useState } from "react"; +import { Globe, Plus, Settings, Activity } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +interface ToolsPageProps { + onNavigate: ( + page: + | "provider-pool" + | "config-management" + | "api-server" + | "flow-monitor" + | "tools" + | "browser-interceptor" + | "settings", + ) => void; +} + +interface ToolCardProps { + title: string; + description: string; + icon: React.ReactNode; + status?: string; + disabled?: boolean; + onClick?: () => void; +} + +function ToolCard({ + title, + description, + icon, + status, + disabled = false, + onClick, +}: ToolCardProps) { + return ( + + +
+
+
{icon}
+
+ {title} + {status && ( + + {status} + + )} +
+
+
+
+ + + {description} + + + +
+ ); +} + +export function ToolsPage({ onNavigate }: ToolsPageProps) { + const [interceptorEnabled] = useState(false); // TODO: 从状态管理中获取 + + const handleBrowserInterceptorClick = () => { + onNavigate("browser-interceptor"); + }; + + return ( +
+
+
+

工具箱

+

+ ProxyCast 提供的实用工具集合 +

+
+ 1 个工具 +
+ +
+ } + status={interceptorEnabled ? "运行中" : "已停止"} + onClick={handleBrowserInterceptorClick} + /> + + {/* 未来可以添加更多工具 */} + } + disabled + /> + + } + disabled + /> + + } + disabled + /> +
+ +
+

关于工具箱

+

+ 工具箱是 ProxyCast + 的扩展功能模块,提供各种实用工具来增强您的使用体验。 + 每个工具都经过精心设计,旨在解决特定的使用场景和需求。 +

+
+
+ ); +} diff --git a/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx b/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx new file mode 100644 index 000000000..1866b3961 --- /dev/null +++ b/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx @@ -0,0 +1,215 @@ +import { useState, useEffect } from "react"; +import { ArrowLeft, Globe, Settings, History, BarChart3 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { StatusControlPanel } from "./StatusControlPanel"; +import { InterceptedUrlsPanel } from "./InterceptedUrlsPanel"; +import { InterceptorConfigPanel } from "./InterceptorConfigPanel"; +import { UrlHistoryPanel } from "./UrlHistoryPanel"; +import { SystemStatusPanel } from "./SystemStatusPanel"; +import { InterceptorState } from "@/lib/api/browserInterceptor"; +import * as browserInterceptorApi from "@/lib/api/browserInterceptor"; + +interface BrowserInterceptorToolProps { + onNavigate: ( + page: + | "provider-pool" + | "config-management" + | "api-server" + | "flow-monitor" + | "tools" + | "browser-interceptor" + | "settings", + ) => void; +} + +export function BrowserInterceptorTool({ + onNavigate, +}: BrowserInterceptorToolProps) { + const [interceptorState, setInterceptorState] = + useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadInterceptorState(); + + // 每5秒刷新一次状态 + const interval = setInterval(loadInterceptorState, 5000); + + return () => clearInterval(interval); + }, []); + + const loadInterceptorState = async () => { + try { + const state = await browserInterceptorApi.getBrowserInterceptorState(); + setInterceptorState(state); + } catch (error) { + console.error("加载拦截器状态失败:", error); + } finally { + setLoading(false); + } + }; + + const handleStateChange = () => { + // 状态变化时重新加载状态 + loadInterceptorState(); + }; + + if (loading) { + return ( +
+
+ +

加载中...

+
+
+ ); + } + + return ( +
+ {/* 页面头部 */} +
+
+ +
+ +
+

浏览器拦截器

+

+ 拦截桌面应用的浏览器启动,支持手动复制 URL 到指纹浏览器 +

+
+
+
+ + {/* 状态控制面板 */} + +
+ + {/* 主要功能标签页 */} + + + + + 拦截的 URL + + + + 拦截配置 + + + + 历史记录 + + + + 系统状态 + + + 帮助 + + + + + + + + + + + + + + + + + + + + +
+
+

+ 🚀 浏览器拦截器使用指南 +

+ +
+
+

+ 什么是浏览器拦截器? +

+

+ 浏览器拦截器专门解决 Kiro、Cursor、VSCode 等桌面 AI 客户端的 + OAuth 登录问题。 + 当这些应用尝试打开浏览器进行登录时,我们会拦截这些请求,让您可以手动在指纹浏览器中完成登录。 +

+
+ +
+

+ 🔧 工作原理 +

+
    +
  • • 监听系统级的浏览器启动请求
  • +
  • • 识别来自目标应用的 URL 打开请求
  • +
  • • 阻止默认浏览器启动
  • +
  • • 捕获并存储 OAuth URL
  • +
  • • 提供一键复制和指纹浏览器启动功能
  • +
+
+ +
+

+ 💡 使用步骤 +

+
    +
  1. 1. 启用浏览器拦截器
  2. +
  3. 2. 在 Kiro 等应用中点击登录
  4. +
  5. 3. 查看拦截到的 URL
  6. +
  7. 4. 一键复制或在指纹浏览器中打开
  8. +
  9. 5. 完成登录后可恢复正常浏览器行为
  10. +
+
+ +
+

+ ⚠️ 重要提醒 +

+
    +
  • • 使用完成后记得点击"恢复正常"以免影响其他软件
  • +
  • • 支持临时禁用功能,会在指定时间后自动恢复拦截
  • +
  • • 所有拦截记录都会保存在历史中,可随时查看
  • +
  • • 目前主要支持 Windows 平台,其他平台正在开发中
  • +
+
+
+
+ +
+

+ 🔐 隐私和安全 +

+

+ 拦截器仅捕获 OAuth 相关的 URL,不会记录任何敏感信息。 + 所有数据都保存在本地,不会上传到任何服务器。 +

+
+
+
+
+
+ ); +} diff --git a/src/components/tools/browser-interceptor/InterceptedUrlsPanel.tsx b/src/components/tools/browser-interceptor/InterceptedUrlsPanel.tsx new file mode 100644 index 000000000..3105993a6 --- /dev/null +++ b/src/components/tools/browser-interceptor/InterceptedUrlsPanel.tsx @@ -0,0 +1,315 @@ +import { useState, useEffect } from "react"; +import { + Copy, + ExternalLink, + X, + Globe, + Clock, + RefreshCw, + CheckCircle2, + AlertCircle, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { formatDistanceToNow } from "date-fns"; +import { zhCN } from "date-fns/locale"; +import { InterceptedUrl } from "@/lib/api/browserInterceptor"; +import * as browserInterceptorApi from "@/lib/api/browserInterceptor"; + +interface InterceptedUrlsPanelProps { + onStateChange: () => void; +} + +export function InterceptedUrlsPanel({ + onStateChange, +}: InterceptedUrlsPanelProps) { + const [interceptedUrls, setInterceptedUrls] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [operatingUrls, setOperatingUrls] = useState>(new Set()); + + useEffect(() => { + loadInterceptedUrls(); + + // 每10秒自动刷新 + const interval = setInterval(loadInterceptedUrls, 10000); + + return () => clearInterval(interval); + }, []); + + const loadInterceptedUrls = async (showRefreshing = false) => { + if (showRefreshing) setRefreshing(true); + try { + const urls = await browserInterceptorApi.getInterceptedUrls(); + setInterceptedUrls(urls); + } catch (error) { + console.error("加载拦截URL失败:", error); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + const handleRefresh = () => { + loadInterceptedUrls(true); + }; + + const setUrlOperating = (urlId: string, operating: boolean) => { + setOperatingUrls((prev) => { + const newSet = new Set(prev); + if (operating) { + newSet.add(urlId); + } else { + newSet.delete(urlId); + } + return newSet; + }); + }; + + const handleCopyUrl = async (urlData: InterceptedUrl) => { + setUrlOperating(urlData.id, true); + try { + await browserInterceptorApi.copyInterceptedUrlToClipboard(urlData.id); + // 复制成功后重新加载数据以更新状态 + await loadInterceptedUrls(); + onStateChange(); + } catch (error) { + console.error("复制URL失败:", error); + } finally { + setUrlOperating(urlData.id, false); + } + }; + + const handleOpenInBrowser = async (urlData: InterceptedUrl) => { + setUrlOperating(urlData.id, true); + try { + await browserInterceptorApi.openUrlInFingerprintBrowser(urlData.id); + await loadInterceptedUrls(); + onStateChange(); + } catch (error) { + console.error("在指纹浏览器中打开URL失败:", error); + } finally { + setUrlOperating(urlData.id, false); + } + }; + + const handleDismissUrl = async (urlData: InterceptedUrl) => { + setUrlOperating(urlData.id, true); + try { + await browserInterceptorApi.dismissInterceptedUrl(urlData.id); + await loadInterceptedUrls(); + onStateChange(); + } catch (error) { + console.error("忽略URL失败:", error); + } finally { + setUrlOperating(urlData.id, false); + } + }; + + const getSourceProcessIcon = (processName: string) => { + if (processName.toLowerCase().includes("kiro")) { + return "🤖"; + } else if (processName.toLowerCase().includes("cursor")) { + return "💻"; + } else if (processName.toLowerCase().includes("code")) { + return "📝"; + } + return "🔗"; + }; + + const truncateUrl = (url: string, maxLength = 60) => { + if (url.length <= maxLength) return url; + return url.substring(0, maxLength) + "..."; + }; + + if (loading) { + return ( + + + + + 当前拦截的 URL + + + +
+
+ 加载中... +
+ + + ); + } + + return ( + + +
+ + + 当前拦截的 URL + {interceptedUrls.length} + + +
+
+ + {interceptedUrls.length === 0 ? ( +
+ +

+ 暂无拦截的 URL +

+

+ 当目标应用(如 Kiro、Cursor、VSCode)尝试打开浏览器时,URL + 将显示在这里。 请确保拦截器已启用。 +

+
+ ) : ( + +
+ {interceptedUrls.map((urlData) => ( +
+ {/* URL 头部信息 */} +
+
+ + {getSourceProcessIcon(urlData.source_process)} + + + {urlData.source_process} + + {urlData.copied && ( + + + 已复制 + + )} + {urlData.opened_in_browser && ( + + + 已打开 + + )} +
+
+ + {formatDistanceToNow(new Date(urlData.timestamp), { + addSuffix: true, + locale: zhCN, + })} +
+
+ + {/* URL 显示 */} +
+ + + +
+ {truncateUrl(urlData.url)} +
+
+ +

{urlData.url}

+
+
+
+
+ + {/* 操作按钮 */} +
+ + + + + +
+
+ ))} +
+
+ )} + + {/* 使用提示 */} + {interceptedUrls.length > 0 && ( +
+
+ +
+

使用建议:

+
    +
  • • 点击"复制 URL"将链接复制到剪贴板
  • +
  • • 点击"在指纹浏览器中打开"自动启动配置的浏览器
  • +
  • • 点击"忽略"将移除此URL(会保留在历史记录中)
  • +
+
+
+
+ )} +
+
+ ); +} diff --git a/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx b/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx new file mode 100644 index 000000000..d12d29d81 --- /dev/null +++ b/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx @@ -0,0 +1,694 @@ +import React, { useState, useEffect } from "react"; +import { + Settings, + Plus, + Trash2, + Save, + RotateCcw, + AlertCircle, + CheckCircle, + FolderOpen, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { BrowserInterceptorConfig } from "@/lib/api/browserInterceptor"; +import * as browserInterceptorApi from "@/lib/api/browserInterceptor"; + +interface InterceptorConfigPanelProps { + onStateChange: () => void; +} + +export function InterceptorConfigPanel({ + onStateChange, +}: InterceptorConfigPanelProps) { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [validating, setValidating] = useState(false); + const [validationResult, setValidationResult] = useState<{ + valid: boolean; + message: string; + } | null>(null); + + const [newTargetProcess, setNewTargetProcess] = useState(""); + const [newUrlPattern, setNewUrlPattern] = useState(""); + const [newExcludedProcess, setNewExcludedProcess] = useState(""); + + useEffect(() => { + loadConfig(); + }, []); + + const loadConfig = async () => { + setLoading(true); + try { + const defaultConfig = + await browserInterceptorApi.getDefaultBrowserInterceptorConfig(); + setConfig(defaultConfig); + } catch (error) { + console.error("加载配置失败:", error); + } finally { + setLoading(false); + } + }; + + const handleSaveConfig = async () => { + if (!config) return; + + setSaving(true); + try { + // 先验证配置 + await browserInterceptorApi.validateBrowserInterceptorConfig(config); + + // 检查拦截器是否正在运行 + const isRunning = + await browserInterceptorApi.isBrowserInterceptorRunning(); + + if (isRunning) { + // 拦截器已运行,更新配置 + await browserInterceptorApi.updateBrowserInterceptorConfig(config); + setValidationResult({ + valid: true, + message: "配置已更新", + }); + } else { + // 拦截器未运行 + if (config.enabled) { + // 用户启用了拦截器,使用配置启动 + await browserInterceptorApi.startBrowserInterceptor(config); + setValidationResult({ + valid: true, + message: "拦截器已启动", + }); + } else { + // 用户未启用拦截器,仅保存配置到本地(配置已验证通过) + setValidationResult({ + valid: true, + message: + "配置已验证通过。开启「启用拦截器」开关后点击保存即可启动。", + }); + } + } + onStateChange(); + } catch (error) { + console.error("保存配置失败:", error); + setValidationResult({ + valid: false, + message: "保存配置失败: " + String(error), + }); + } finally { + setSaving(false); + } + }; + + const handleValidateConfig = async () => { + if (!config) return; + + setValidating(true); + try { + await browserInterceptorApi.validateBrowserInterceptorConfig(config); + setValidationResult({ + valid: true, + message: "配置验证通过", + }); + } catch (error) { + console.error("验证配置失败:", error); + setValidationResult({ + valid: false, + message: "验证失败: " + String(error), + }); + } finally { + setValidating(false); + } + }; + + const handleResetConfig = async () => { + setLoading(true); + try { + const defaultConfig = + await browserInterceptorApi.getDefaultBrowserInterceptorConfig(); + setConfig(defaultConfig); + setValidationResult(null); + } catch (error) { + console.error("重置配置失败:", error); + } finally { + setLoading(false); + } + }; + + const updateConfig = (updates: Partial) => { + if (!config) return; + setConfig({ ...config, ...updates }); + setValidationResult(null); // 清除验证结果 + }; + + const addTargetProcess = () => { + if (!newTargetProcess.trim() || !config) return; + updateConfig({ + target_processes: [...config.target_processes, newTargetProcess.trim()], + }); + setNewTargetProcess(""); + }; + + const removeTargetProcess = (index: number) => { + if (!config) return; + const newProcesses = [...config.target_processes]; + newProcesses.splice(index, 1); + updateConfig({ target_processes: newProcesses }); + }; + + const addUrlPattern = () => { + if (!newUrlPattern.trim() || !config) return; + updateConfig({ + url_patterns: [...config.url_patterns, newUrlPattern.trim()], + }); + setNewUrlPattern(""); + }; + + const removeUrlPattern = (index: number) => { + if (!config) return; + const newPatterns = [...config.url_patterns]; + newPatterns.splice(index, 1); + updateConfig({ url_patterns: newPatterns }); + }; + + const addExcludedProcess = () => { + if (!newExcludedProcess.trim() || !config) return; + updateConfig({ + excluded_processes: [ + ...config.excluded_processes, + newExcludedProcess.trim(), + ], + }); + setNewExcludedProcess(""); + }; + + const removeExcludedProcess = (index: number) => { + if (!config) return; + const newProcesses = [...config.excluded_processes]; + newProcesses.splice(index, 1); + updateConfig({ excluded_processes: newProcesses }); + }; + + if (loading || !config) { + return ( + + +
+
+ 加载配置中... +
+ + + ); + } + + return ( +
+ {/* 配置操作按钮 */} +
+

+ + 拦截器配置 +

+
+ + + +
+
+ + {/* 验证结果显示 */} + {validationResult && ( +
+
+ {validationResult.valid ? ( + + ) : ( + + )} + + {validationResult.valid ? "配置验证通过" : "配置验证失败"} + +
+ {validationResult.message && ( +

{validationResult.message}

+ )} +
+ )} + + {/* 配置选项卡 */} + + + 基本设置 + 进程配置 + URL 模式 + 浏览器设置 + + + {/* 基本设置 */} + + + + 基本设置 + + +
+
+ +

+ 开启后将拦截目标应用的浏览器启动请求 +

+
+ updateConfig({ enabled })} + /> +
+ +
+
+
+ +

+ 拦截URL时显示系统通知 +

+
+ + updateConfig({ notification_enabled }) + } + /> +
+ {config.notification_enabled && ( +
+ +
+ )} +
+ +
+
+ +

+ 拦截URL后自动复制到剪贴板 +

+
+ + updateConfig({ auto_copy_to_clipboard }) + } + /> +
+ +
+
+ +

+ 应用退出时自动恢复浏览器行为 +

+
+ + updateConfig({ restore_on_exit }) + } + /> +
+ +
+ +

+ 临时禁用后自动重新启用的时间,0 表示不自动恢复 +

+ { + const minutes = parseInt(e.target.value) || 0; + updateConfig({ + temporary_disable_timeout: + minutes > 0 ? minutes * 60 : null, + }); + }} + className="max-w-xs" + /> +
+
+
+
+ + {/* 进程配置 */} + +
+ {/* 目标进程 */} + + + 目标进程 +

+ 配置需要拦截浏览器启动的应用程序名称 +

+
+ +
+
+ setNewTargetProcess(e.target.value)} + onKeyPress={(e) => + e.key === "Enter" && addTargetProcess() + } + /> + +
+
+ {config.target_processes.map((process, index) => ( + + {process} + + + ))} +
+
+
+
+ + {/* 排除进程 */} + + + 排除进程 +

+ 配置永不拦截的进程名称(如系统浏览器) +

+
+ +
+
+ setNewExcludedProcess(e.target.value)} + onKeyPress={(e) => + e.key === "Enter" && addExcludedProcess() + } + /> + +
+
+ {config.excluded_processes.map((process, index) => ( + + {process} + + + ))} +
+
+
+
+
+
+ + {/* URL 模式 */} + + + + URL 匹配模式 +

+ 配置需要拦截的URL模式,支持通配符 * +

+
+ +
+
+ setNewUrlPattern(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && addUrlPattern()} + /> + +
+
+ {config.url_patterns.map((pattern, index) => ( +
+ {pattern} + +
+ ))} +
+
+
+
+
+ + {/* 浏览器设置 */} + + + + 指纹浏览器设置 +

+ 配置指纹浏览器的路径和启动参数 +

+
+ +
+
+ +

+ 启用后可以一键在指纹浏览器中打开URL +

+
+ + updateConfig({ + fingerprint_browser: { + ...config.fingerprint_browser, + enabled, + }, + }) + } + /> +
+ + {config.fingerprint_browser.enabled && ( + <> +
+ +
+ + updateConfig({ + fingerprint_browser: { + ...config.fingerprint_browser, + executable_path: e.target.value, + }, + }) + } + /> + + + + + + +

浏览文件夹选择浏览器

+
+
+
+
+
+ +
+ + + updateConfig({ + fingerprint_browser: { + ...config.fingerprint_browser, + profile_path: e.target.value, + }, + }) + } + /> +
+ +
+ +