diff --git a/CHANGELOG.md b/CHANGELOG.md index e6725a8..4d15b55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,19 @@ - **CI/CD** — GitHub Actions 持续集成 + 全平台发布构建(macOS ARM64/Intel、Windows x64、Linux x64) - **手动发布** — 支持 workflow_dispatch 手动触发构建,填入版本号即可一键发布 +### 优化 (Improvements) + +- **全局异步加载** — 所有页面 render() 非阻塞返回 DOM,数据在后台异步加载,消除页面切换卡顿 +- **路由模块缓存** — 已加载的页面模块缓存复用,二次切换跳过动态 import +- **Tauri API 预加载** — invoke 模块启动时预加载,避免每次 API 调用的动态 import 开销 +- **页面过渡动画** — 进入动画(220ms 上滑淡入)+ 退出动画(100ms 淡出),丝滑切换体验 +- **Windows 兼容** — Rust 后端通过 `#[cfg(target_os)]` 条件编译支持 Windows 平台(服务管理、版本检测、扩展工具等) +- **Setup 引导模式** — 未安装 OpenClaw 时自动进入引导页面,安装完成后切换到正常模式 + ### 技术亮点 - 零框架依赖:纯 Vanilla JS,无 React/Vue 等框架 - Tauri v2 + Rust 后端,原生性能 - 玻璃拟态暗色主题,现代化 UI - 全中文界面与代码注释 +- 跨平台支持:macOS (ARM64/Intel) + Windows + Linux diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f17a44..29c29d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,80 @@ cd clawpanel # 安装前端依赖 npm install +``` -# 启动开发模式 -cargo tauri dev +#### macOS / Linux + +```bash +# 启动开发模式(完整 Tauri 桌面应用) +./scripts/dev.sh + +# 仅启动前端(浏览器调试,使用 mock 数据) +./scripts/dev.sh web +``` + +#### Windows + +```powershell +# 启动开发模式(完整 Tauri 桌面应用) +npm run tauri dev + +# 仅启动前端(浏览器调试,使用 mock 数据) +npm run dev +``` + +> Windows 开发需要安装 [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)(勾选「使用 C++ 的桌面开发」工作负载)和 [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)(Win10+ 通常已预装)。 + +## 项目结构 + +``` +clawpanel/ +├── src/ # 前端源码(Vanilla JS) +│ ├── pages/ # 页面模块(每个页面导出 render 函数) +│ ├── components/ # 通用组件(侧边栏、弹窗、Toast) +│ ├── lib/ # 工具库(Tauri API 封装、主题切换) +│ ├── style/ # CSS 样式(CSS Variables 驱动) +│ ├── router.js # Hash 路由 +│ └── main.js # 入口文件 +├── src-tauri/ # Rust 后端 +│ ├── src/commands/ # Tauri 命令(按功能模块拆分) +│ ├── Cargo.toml # Rust 依赖 +│ └── tauri.conf.json # Tauri 配置 +├── public/ # 静态资源(图片、图标) +└── .github/workflows/ # CI/CD 工作流 +``` + +### 前端页面开发约定 + +每个页面是一个独立 JS 模块,导出 `render()` 函数: + +```javascript +export async function render() { + const page = document.createElement('div') + page.className = 'page' + page.innerHTML = `` + + // 非阻塞:先返回 DOM,数据在后台异步加载 + loadData(page) + return page +} +``` + +关键原则:`render()` 必须立即返回 DOM 元素,不要 `await` 数据加载,否则会阻塞页面切换。 + +### Rust 跨平台开发约定 + +平台相关代码使用条件编译: + +```rust +#[cfg(target_os = "macos")] +{ + // macOS: launchctl / plist +} +#[cfg(target_os = "windows")] +{ + // Windows: openclaw CLI / tasklist +} ``` ## 分支策略 @@ -48,6 +119,7 @@ cargo tauri dev | `docs` | 文档变更 | | `style` | 代码格式调整(不影响逻辑) | | `refactor` | 重构(非新功能、非 Bug 修复) | +| `perf` | 性能优化 | | `test` | 测试相关 | | `chore` | 构建/工具/依赖变更 | @@ -56,6 +128,7 @@ cargo tauri dev ``` feat(model): 新增模型批量测试功能 fix(gateway): 修复端口配置未生效的问题 +perf(router): 添加模块缓存避免重复加载 docs: 更新安装说明 ``` @@ -76,6 +149,7 @@ docs: 更新安装说明 - **风格**:简洁清晰,避免过度封装 - **命名**:变量和函数使用驼峰命名(camelCase),CSS 类名使用短横线命名(kebab-case) - **资源**:静态资源本地化,禁止引用远程 CDN +- **异步**:页面 render() 中禁止阻塞式 await,数据加载走后台异步 ## 问题反馈 diff --git a/README.md b/README.md index ac54343..77df41a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@

- ClawPanel Logo + ClawPanel

-

ClawPanel

-

OpenClaw 可视化管理面板 — 基于 Tauri v2 的跨平台桌面应用

@@ -72,7 +70,7 @@ ClawPanel 是 [OpenClaw](https://github.com/openclaw-labs/openclaw) AI Agent 框 ## 功能截图 -> 截图待补充 +> 截图待补充 — 欢迎提交 PR 补充各页面截图 ## 技术架构 @@ -122,7 +120,11 @@ clawpanel/ git clone https://github.com/qingchencloud/clawpanel.git cd clawpanel npm install +``` +#### macOS / Linux + +```bash # 启动完整 Tauri 桌面应用 ./scripts/dev.sh @@ -130,8 +132,20 @@ npm install ./scripts/dev.sh web ``` +#### Windows + +```powershell +# 启动完整 Tauri 桌面应用 +npm run tauri dev + +# 仅启动 Vite 前端(浏览器调试,使用 mock 数据) +npm run dev +``` + ### 构建 +#### macOS / Linux + ```bash # 编译 debug 版本 ./scripts/build.sh @@ -143,6 +157,21 @@ npm install ./scripts/build.sh release ``` +#### Windows + +```powershell +# 检查 Rust 编译 +cd src-tauri && cargo check + +# 编译正式发布版本 +npm run tauri build + +# 指定打包格式(NSIS 安装器) +npm run tauri build -- --bundles nsis +``` + +产物位于 `src-tauri/target/release/` 目录。 + ## 相关项目 | 项目 | 说明 | diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 0000000..9121ed6 --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,30 @@ +#!/usr/bin/env pwsh +# ClawPanel 开发服务器启动脚本 + +Write-Host "🚀 启动 ClawPanel 开发服务器..." -ForegroundColor Cyan + +# 检查 Node.js +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Host "❌ 未找到 Node.js,请先安装" -ForegroundColor Red + exit 1 +} + +# 检查 Rust +if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { + Write-Host "❌ 未找到 Rust,请先安装" -ForegroundColor Red + exit 1 +} + +# 检查依赖 +if (-not (Test-Path "node_modules")) { + Write-Host "📦 安装前端依赖..." -ForegroundColor Yellow + npm install +} + +if (-not (Test-Path "src-tauri/target")) { + Write-Host "🦀 首次编译 Rust(可能需要几分钟)..." -ForegroundColor Yellow +} + +# 启动开发服务器 +Write-Host "✨ 启动中..." -ForegroundColor Green +npm run tauri dev diff --git a/index.html b/index.html index 34b5544..a3c274e 100644 --- a/index.html +++ b/index.html @@ -4,12 +4,16 @@ ClawPanel +
-
+
+
+
+
diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..f8dbc2d Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/logo-brand.png b/public/images/logo-brand.png new file mode 100644 index 0000000..207e83e Binary files /dev/null and b/public/images/logo-brand.png differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..f92a89c Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/logo.svg b/public/images/logo.svg deleted file mode 100644 index 4e0c75a..0000000 --- a/public/images/logo.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/build.sh b/scripts/build.sh old mode 100755 new mode 100644 diff --git a/scripts/dev.sh b/scripts/dev.sh old mode 100755 new mode 100644 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 427e911..df45fac 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -181,6 +181,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -1464,7 +1470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -1581,6 +1587,19 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1922,6 +1941,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.1" @@ -1937,7 +1966,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -2516,6 +2545,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -2624,6 +2666,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3649,6 +3700,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "image", "jni", "libc", "log", @@ -3716,7 +3768,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -4205,7 +4257,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 54f3863..a6b5d46 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["lib", "cdylib", "staticlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/gen/schemas/windows-schema.json b/src-tauri/gen/schemas/windows-schema.json new file mode 100644 index 0000000..f827fe1 --- /dev/null +++ b/src-tauri/gen/schemas/windows-schema.json @@ -0,0 +1,2564 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "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\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + } + ] + } + }, + "deny": { + "items": { + "title": "ShellScopeEntry", + "description": "Shell scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "cmd", + "name" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "name", + "sidecar" + ], + "properties": { + "args": { + "description": "The allowed arguments for the command execution.", + "allOf": [ + { + "$ref": "#/definitions/ShellScopeEntryAllowedArgs" + } + ] + }, + "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" + } + }, + "additionalProperties": false + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "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\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "shell:default", + "markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-open`" + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "shell:deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "ShellScopeEntryAllowedArg": { + "description": "A command argument allowed to be executed by the webview API.", + "anyOf": [ + { + "description": "A non-configurable argument that is passed to the command in the order it was specified.", + "type": "string" + }, + { + "description": "A variable that is set while calling the command from the webview API.", + "type": "object", + "required": [ + "validator" + ], + "properties": { + "raw": { + "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.", + "default": false, + "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" + } + }, + "additionalProperties": false + } + ] + }, + "ShellScopeEntryAllowedArgs": { + "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.", + "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.", + "type": "array", + "items": { + "$ref": "#/definitions/ShellScopeEntryAllowedArg" + } + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/icons/1024x1024.png b/src-tauri/icons/1024x1024.png index 7699c32..3dbfd97 100644 Binary files a/src-tauri/icons/1024x1024.png and b/src-tauri/icons/1024x1024.png differ diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 82dafd5..cd450a7 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index 1a0b779..a1a0782 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/16x16.png b/src-tauri/icons/16x16.png index f8245f5..20f050f 100644 Binary files a/src-tauri/icons/16x16.png and b/src-tauri/icons/16x16.png differ diff --git a/src-tauri/icons/16x16@2x.png b/src-tauri/icons/16x16@2x.png index 60fda11..ee94723 100644 Binary files a/src-tauri/icons/16x16@2x.png and b/src-tauri/icons/16x16@2x.png differ diff --git a/src-tauri/icons/256x256.png b/src-tauri/icons/256x256.png index 1a0b779..a1a0782 100644 Binary files a/src-tauri/icons/256x256.png and b/src-tauri/icons/256x256.png differ diff --git a/src-tauri/icons/256x256@2x.png b/src-tauri/icons/256x256@2x.png index d9403e7..77ff968 100644 Binary files a/src-tauri/icons/256x256@2x.png and b/src-tauri/icons/256x256@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 60fda11..ee94723 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/32x32@2x.png b/src-tauri/icons/32x32@2x.png index 9fe28f1..f92a89c 100644 Binary files a/src-tauri/icons/32x32@2x.png and b/src-tauri/icons/32x32@2x.png differ diff --git a/src-tauri/icons/512x512.png b/src-tauri/icons/512x512.png index d9403e7..77ff968 100644 Binary files a/src-tauri/icons/512x512.png and b/src-tauri/icons/512x512.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png index 9fe28f1..f92a89c 100644 Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/64x64@2x.png b/src-tauri/icons/64x64@2x.png index 82dafd5..cd450a7 100644 Binary files a/src-tauri/icons/64x64@2x.png and b/src-tauri/icons/64x64@2x.png differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index fcb9a01..3072df5 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.iconset/icon_128x128.png b/src-tauri/icons/icon.iconset/icon_128x128.png index 82dafd5..cd450a7 100644 Binary files a/src-tauri/icons/icon.iconset/icon_128x128.png and b/src-tauri/icons/icon.iconset/icon_128x128.png differ diff --git a/src-tauri/icons/icon.iconset/icon_128x128@2x.png b/src-tauri/icons/icon.iconset/icon_128x128@2x.png index 1a0b779..a1a0782 100644 Binary files a/src-tauri/icons/icon.iconset/icon_128x128@2x.png and b/src-tauri/icons/icon.iconset/icon_128x128@2x.png differ diff --git a/src-tauri/icons/icon.iconset/icon_16x16.png b/src-tauri/icons/icon.iconset/icon_16x16.png index f8245f5..20f050f 100644 Binary files a/src-tauri/icons/icon.iconset/icon_16x16.png and b/src-tauri/icons/icon.iconset/icon_16x16.png differ diff --git a/src-tauri/icons/icon.iconset/icon_16x16@2x.png b/src-tauri/icons/icon.iconset/icon_16x16@2x.png index 60fda11..ee94723 100644 Binary files a/src-tauri/icons/icon.iconset/icon_16x16@2x.png and b/src-tauri/icons/icon.iconset/icon_16x16@2x.png differ diff --git a/src-tauri/icons/icon.iconset/icon_256x256.png b/src-tauri/icons/icon.iconset/icon_256x256.png index 1a0b779..a1a0782 100644 Binary files a/src-tauri/icons/icon.iconset/icon_256x256.png and b/src-tauri/icons/icon.iconset/icon_256x256.png differ diff --git a/src-tauri/icons/icon.iconset/icon_256x256@2x.png b/src-tauri/icons/icon.iconset/icon_256x256@2x.png index d9403e7..77ff968 100644 Binary files a/src-tauri/icons/icon.iconset/icon_256x256@2x.png and b/src-tauri/icons/icon.iconset/icon_256x256@2x.png differ diff --git a/src-tauri/icons/icon.iconset/icon_32x32.png b/src-tauri/icons/icon.iconset/icon_32x32.png index 60fda11..ee94723 100644 Binary files a/src-tauri/icons/icon.iconset/icon_32x32.png and b/src-tauri/icons/icon.iconset/icon_32x32.png differ diff --git a/src-tauri/icons/icon.iconset/icon_32x32@2x.png b/src-tauri/icons/icon.iconset/icon_32x32@2x.png index 9fe28f1..f92a89c 100644 Binary files a/src-tauri/icons/icon.iconset/icon_32x32@2x.png and b/src-tauri/icons/icon.iconset/icon_32x32@2x.png differ diff --git a/src-tauri/icons/icon.iconset/icon_512x512.png b/src-tauri/icons/icon.iconset/icon_512x512.png index d9403e7..77ff968 100644 Binary files a/src-tauri/icons/icon.iconset/icon_512x512.png and b/src-tauri/icons/icon.iconset/icon_512x512.png differ diff --git a/src-tauri/icons/icon.iconset/icon_512x512@2x.png b/src-tauri/icons/icon.iconset/icon_512x512@2x.png index 7699c32..3dbfd97 100644 Binary files a/src-tauri/icons/icon.iconset/icon_512x512@2x.png and b/src-tauri/icons/icon.iconset/icon_512x512@2x.png differ diff --git a/src-tauri/src/commands/agent.rs b/src-tauri/src/commands/agent.rs index 0083cd3..242e0ec 100644 --- a/src-tauri/src/commands/agent.rs +++ b/src-tauri/src/commands/agent.rs @@ -1,13 +1,13 @@ /// Agent 管理命令 — 调用 openclaw CLI 实现增删改查 use serde_json::Value; -use std::process::Command; use std::fs; use std::io::Write; +use crate::utils::openclaw_command; /// 获取 agent 列表 #[tauri::command] pub fn list_agents() -> Result { - let output = Command::new("openclaw") + let output = openclaw_command() .args(["agents", "list", "--json"]) .output() .map_err(|e| format!("执行失败: {e}"))?; @@ -48,7 +48,7 @@ pub fn add_agent(name: String, model: String, workspace: Option) -> Resu args.push(model); } - let output = Command::new("openclaw") + let output = openclaw_command() .args(&args) .output() .map_err(|e| format!("执行失败: {e}"))?; @@ -71,7 +71,7 @@ pub fn delete_agent(id: String) -> Result { return Err("不能删除默认 Agent".into()); } - let output = Command::new("openclaw") + let output = openclaw_command() .args(["agents", "delete", &id]) .output() .map_err(|e| format!("执行失败: {e}"))?; @@ -112,7 +112,7 @@ pub fn update_agent_identity( } } - let output = Command::new("openclaw") + let output = openclaw_command() .args(&args) .output() .map_err(|e| format!("执行失败: {e}"))?; diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 349e87d..6b4938f 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -3,6 +3,9 @@ use serde_json::Value; use std::fs; use std::path::PathBuf; use std::process::Command; +use crate::utils::openclaw_command; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; use crate::models::types::VersionInfo; @@ -20,10 +23,23 @@ fn get_configured_registry() -> String { } /// 创建使用配置源的 npm Command +/// Windows 上 npm 是 npm.cmd,需要通过 cmd /c 调用,并隐藏窗口 fn npm_command() -> Command { - let mut cmd = Command::new("npm"); - cmd.args(["--registry", &get_configured_registry()]); - cmd + let registry = get_configured_registry(); + #[cfg(target_os = "windows")] + { + const CREATE_NO_WINDOW: u32 = 0x08000000; + let mut cmd = Command::new("cmd"); + cmd.args(["/c", "npm", "--registry", ®istry]); + cmd.creation_flags(CREATE_NO_WINDOW); + cmd + } + #[cfg(not(target_os = "windows"))] + { + let mut cmd = Command::new("npm"); + cmd.args(["--registry", ®istry]); + cmd + } } fn backups_dir() -> PathBuf { @@ -45,13 +61,50 @@ pub fn write_openclaw_config(config: Value) -> Result<(), String> { // 备份 let bak = super::openclaw_dir().join("openclaw.json.bak"); let _ = fs::copy(&path, &bak); + // 清理 UI 专属字段,避免 CLI schema 校验失败 + let cleaned = strip_ui_fields(config); // 写入 - let json = serde_json::to_string_pretty(&config) + let json = serde_json::to_string_pretty(&cleaned) .map_err(|e| format!("序列化失败: {e}"))?; fs::write(&path, json) .map_err(|e| format!("写入失败: {e}")) } +/// 递归清理 models 数组中的 UI 专属字段(lastTestAt, latency, testStatus, testError) +/// 并为缺少 name 字段的模型自动补上 name = id +fn strip_ui_fields(mut val: Value) -> Value { + if let Some(obj) = val.as_object_mut() { + // 递归处理 providers -> xxx -> models 数组 + if let Some(models) = obj.get("models") { + if let Some(providers) = models.as_object() { + let mut new_models = providers.clone(); + for (_key, provider) in new_models.iter_mut() { + if let Some(pobj) = provider.as_object_mut() { + if let Some(Value::Array(arr)) = pobj.get_mut("models") { + for model in arr.iter_mut() { + if let Some(mobj) = model.as_object_mut() { + mobj.remove("lastTestAt"); + mobj.remove("latency"); + mobj.remove("testStatus"); + mobj.remove("testError"); + // 补上 name 字段(CLI 要求) + if !mobj.contains_key("name") { + if let Some(id) = mobj.get("id").and_then(|v| v.as_str()) { + mobj.insert("name".into(), Value::String(id.to_string())); + } + } + } + } + } + } + } + obj.insert("models".into(), Value::Object(new_models)); + } + } + } + val +} + #[tauri::command] pub fn read_mcp_config() -> Result { let path = super::openclaw_dir().join("mcp.json"); @@ -74,25 +127,29 @@ pub fn write_mcp_config(config: Value) -> Result<(), String> { } /// 获取本地安装的 openclaw 版本号 -/// 优先从 npm 包的 package.json 读取(含完整后缀),fallback 到 CLI +/// macOS: 优先从 npm 包的 package.json 读取(含完整后缀),fallback 到 CLI +/// Windows/Linux: 直接用 CLI fn get_local_version() -> Option { - // 通过 symlink 找到包目录,读 package.json 的 version - if let Ok(target) = fs::read_link("/opt/homebrew/bin/openclaw") { - let pkg_json = PathBuf::from("/opt/homebrew/bin") - .join(&target) - .parent()? - .join("package.json"); - if let Ok(content) = fs::read_to_string(&pkg_json) { - if let Some(ver) = serde_json::from_str::(&content) - .ok() - .and_then(|v| v.get("version")?.as_str().map(String::from)) - { - return Some(ver); + // macOS: 通过 symlink 找到包目录,读 package.json 的 version + #[cfg(target_os = "macos")] + { + if let Ok(target) = fs::read_link("/opt/homebrew/bin/openclaw") { + let pkg_json = PathBuf::from("/opt/homebrew/bin") + .join(&target) + .parent()? + .join("package.json"); + if let Ok(content) = fs::read_to_string(&pkg_json) { + if let Some(ver) = serde_json::from_str::(&content) + .ok() + .and_then(|v| v.get("version")?.as_str().map(String::from)) + { + return Some(ver); + } } } } - // fallback: CLI 输出 - let output = Command::new("openclaw").arg("--version").output().ok()?; + // 所有平台通用 fallback: CLI 输出 + let output = openclaw_command().arg("--version").output().ok()?; let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); raw.split_whitespace().last().filter(|s| !s.is_empty()).map(String::from) } @@ -114,25 +171,48 @@ async fn get_latest_version_for(source: &str) -> Option { } /// 检测当前安装的是官方版还是汉化版 -/// 优先检查文件系统(不依赖 npm 命令的 PATH),fallback 到 npm list +/// macOS: 优先检查 homebrew symlink,fallback 到 npm list +/// Windows: 优先检查 npm 全局目录下的 package.json,避免调用 npm list 阻塞 +/// Linux: 直接用 npm list fn detect_installed_source() -> String { - // 方法1:直接检查 openclaw bin 的 symlink 指向 - if let Ok(target) = std::fs::read_link("/opt/homebrew/bin/openclaw") { - if target.to_string_lossy().contains("openclaw-zh") { - return "chinese".into(); + // macOS: 检查 openclaw bin 的 symlink 指向 + #[cfg(target_os = "macos")] + { + if let Ok(target) = std::fs::read_link("/opt/homebrew/bin/openclaw") { + if target.to_string_lossy().contains("openclaw-zh") { + return "chinese".into(); + } + return "official".into(); + } + } + // Windows: 优先通过文件系统检测,避免 npm list 阻塞 + #[cfg(target_os = "windows")] + { + if let Some(appdata) = std::env::var_os("APPDATA") { + let zh_dir = PathBuf::from(&appdata) + .join("npm") + .join("node_modules") + .join("@qingchencloud") + .join("openclaw-zh"); + if zh_dir.exists() { + return "chinese".into(); + } } return "official".into(); } - // 方法2:fallback 到 npm list - if let Ok(o) = npm_command() - .args(["list", "-g", "@qingchencloud/openclaw-zh", "--depth=0"]) - .output() + // 所有平台通用: npm list 检测 + #[cfg(not(any(target_os = "macos", target_os = "windows")))] { - if String::from_utf8_lossy(&o.stdout).contains("openclaw-zh@") { - return "chinese".into(); + if let Ok(o) = npm_command() + .args(["list", "-g", "@qingchencloud/openclaw-zh", "--depth=0"]) + .output() + { + if String::from_utf8_lossy(&o.stdout).contains("openclaw-zh@") { + return "chinese".into(); + } } + "official".into() } - "official".into() } #[tauri::command] @@ -175,13 +255,15 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result Result Result Result { Ok(Value::Object(result)) } +/// 检测 Node.js 是否已安装,返回版本号 +#[tauri::command] +pub fn check_node() -> Result { + let mut result = serde_json::Map::new(); + let mut cmd = Command::new("node"); + cmd.arg("--version"); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + match cmd.output() { + Ok(o) if o.status.success() => { + let ver = String::from_utf8_lossy(&o.stdout).trim().to_string(); + result.insert("installed".into(), Value::Bool(true)); + result.insert("version".into(), Value::String(ver)); + } + _ => { + result.insert("installed".into(), Value::Bool(false)); + result.insert("version".into(), Value::Null); + } + } + Ok(Value::Object(result)) +} + #[tauri::command] pub fn write_env_file(path: String, config: String) -> Result<(), String> { let expanded = if path.starts_with("~/") { @@ -359,10 +472,14 @@ pub fn create_backup() -> Result { Ok(Value::Object(obj)) } +/// 检查备份文件名是否安全 +fn is_unsafe_backup_name(name: &str) -> bool { + name.contains("..") || name.contains('/') || name.contains('\\') +} + #[tauri::command] pub fn restore_backup(name: String) -> Result<(), String> { - // 安全检查 - if name.contains("..") || name.contains('/') { + if is_unsafe_backup_name(&name) { return Err("非法文件名".into()); } let backup_path = backups_dir().join(&name); @@ -383,7 +500,7 @@ pub fn restore_backup(name: String) -> Result<(), String> { #[tauri::command] pub fn delete_backup(name: String) -> Result<(), String> { - if name.contains("..") || name.contains('/') { + if is_unsafe_backup_name(&name) { return Err("非法文件名".into()); } let path = backups_dir().join(&name); @@ -394,32 +511,63 @@ pub fn delete_backup(name: String) -> Result<(), String> { .map_err(|e| format!("删除失败: {e}")) } -/// 获取当前用户 UID +/// 获取当前用户 UID(macOS/Linux 用 id -u,Windows 返回 0) +#[allow(dead_code)] fn get_uid() -> Result { - let output = Command::new("id") - .arg("-u") - .output() - .map_err(|e| format!("获取 UID 失败: {e}"))?; - String::from_utf8_lossy(&output.stdout) - .trim() - .parse::() - .map_err(|e| format!("解析 UID 失败: {e}")) + #[cfg(target_os = "windows")] + { + Ok(0) + } + #[cfg(not(target_os = "windows"))] + { + let output = Command::new("id") + .arg("-u") + .output() + .map_err(|e| format!("获取 UID 失败: {e}"))?; + String::from_utf8_lossy(&output.stdout) + .trim() + .parse::() + .map_err(|e| format!("解析 UID 失败: {e}")) + } } -/// 重载 Gateway 服务(使用 kickstart -k 强制重启) +/// 重载 Gateway 服务 +/// macOS: launchctl kickstart -k +/// Windows/Linux: openclaw gateway restart #[tauri::command] pub fn reload_gateway() -> Result { - let uid = get_uid()?; - let target = format!("gui/{uid}/ai.openclaw.gateway"); - let output = Command::new("launchctl") - .args(["kickstart", "-k", &target]) - .output() - .map_err(|e| format!("重载失败: {e}"))?; - if output.status.success() { - Ok("Gateway 已重载".to_string()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - Err(format!("重载失败: {stderr}")) + #[cfg(target_os = "macos")] + { + let uid = get_uid()?; + let target = format!("gui/{uid}/ai.openclaw.gateway"); + let output = Command::new("launchctl") + .args(["kickstart", "-k", &target]) + .output() + .map_err(|e| format!("重载失败: {e}"))?; + if output.status.success() { + Ok("Gateway 已重载".to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!("重载失败: {stderr}")) + } + } + #[cfg(not(target_os = "macos"))] + { + let cli_check = openclaw_command().arg("--version").output(); + match cli_check { + Ok(o) if o.status.success() => {} + _ => return Err("openclaw CLI 未安装,无法重载 Gateway".into()), + } + let output = openclaw_command() + .args(["gateway", "restart"]) + .output() + .map_err(|e| format!("重载失败: {e}"))?; + if output.status.success() { + Ok("Gateway 已重载".to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!("重载失败: {stderr}")) + } } } @@ -564,7 +712,20 @@ pub async fn list_remote_models( /// 安装 Gateway 服务(执行 openclaw gateway install) #[tauri::command] pub fn install_gateway() -> Result { - let output = Command::new("openclaw") + // 先检测 openclaw CLI 是否可用 + let cli_check = openclaw_command().arg("--version").output(); + match cli_check { + Ok(o) if o.status.success() => {} + _ => { + return Err( + "openclaw CLI 未安装。请先执行以下命令安装:\n\n\ + npm install -g @qingchencloud/openclaw-zh\n\n\ + 安装完成后再点击此按钮安装 Gateway 服务。".into() + ); + } + } + + let output = openclaw_command() .args(["gateway", "install"]) .output() .map_err(|e| format!("安装失败: {e}"))?; @@ -577,23 +738,35 @@ pub fn install_gateway() -> Result { } } -/// 卸载 Gateway 服务(先 bootout 再删除 plist) +/// 卸载 Gateway 服务 +/// macOS: launchctl bootout + 删除 plist +/// Windows/Linux: openclaw gateway stop #[tauri::command] pub fn uninstall_gateway() -> Result { - let uid = get_uid()?; - let target = format!("gui/{uid}/ai.openclaw.gateway"); + #[cfg(target_os = "macos")] + { + let uid = get_uid()?; + let target = format!("gui/{uid}/ai.openclaw.gateway"); - // 先停止服务 - let _ = Command::new("launchctl") - .args(["bootout", &target]) - .output(); + // 先停止服务 + let _ = Command::new("launchctl") + .args(["bootout", &target]) + .output(); - // 删除 plist 文件 - let home = dirs::home_dir().unwrap_or_default(); - let plist = home.join("Library/LaunchAgents/ai.openclaw.gateway.plist"); - if plist.exists() { - fs::remove_file(&plist) - .map_err(|e| format!("删除 plist 失败: {e}"))?; + // 删除 plist 文件 + let home = dirs::home_dir().unwrap_or_default(); + let plist = home.join("Library/LaunchAgents/ai.openclaw.gateway.plist"); + if plist.exists() { + fs::remove_file(&plist) + .map_err(|e| format!("删除 plist 失败: {e}"))?; + } + } + #[cfg(not(target_os = "macos"))] + { + // Windows/Linux: 停止 Gateway 服务 + let _ = openclaw_command() + .args(["gateway", "stop"]) + .output(); } Ok("Gateway 服务已卸载".to_string()) diff --git a/src-tauri/src/commands/extensions.rs b/src-tauri/src/commands/extensions.rs index 219b317..539fbd0 100644 --- a/src-tauri/src/commands/extensions.rs +++ b/src-tauri/src/commands/extensions.rs @@ -1,6 +1,8 @@ /// 扩展工具命令(cftunnel + ClawApp) use serde_json::Value; use std::process::Command; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; /// 解析 cftunnel status 输出 fn parse_cftunnel_status(output: &str) -> serde_json::Map { @@ -9,14 +11,12 @@ fn parse_cftunnel_status(output: &str) -> serde_json::Map { let line = line.trim(); if line.starts_with("隧道:") || line.starts_with("隧道:") { let rest = line.splitn(2, ':').nth(1).unwrap_or("").trim(); - // "mac-home (uuid)" → 取名称 let name = rest.split('(').next().unwrap_or(rest).trim(); map.insert("tunnel_name".into(), Value::String(name.to_string())); } else if line.starts_with("状态:") || line.starts_with("状态:") { let rest = line.splitn(2, ':').nth(1).unwrap_or("").trim(); let running = rest.contains("运行中"); map.insert("running".into(), Value::Bool(running)); - // 提取 PID if let Some(pid_str) = rest.split("PID:").nth(1) { let pid = pid_str.trim().trim_end_matches(')').trim(); if let Ok(p) = pid.parse::() { @@ -33,7 +33,6 @@ fn parse_cftunnel_routes(output: &str) -> Vec { let mut routes = Vec::new(); for line in output.lines() { let line = line.trim(); - // 跳过表头行 if line.is_empty() || line.starts_with("名称") || line.starts_with("---") { continue; } @@ -49,35 +48,92 @@ fn parse_cftunnel_routes(output: &str) -> Vec { routes } +/// 查找 cftunnel 可执行文件路径 fn cftunnel_bin() -> String { - // 优先查找用户 bin 目录 let home = dirs::home_dir().unwrap_or_default(); - let user_bin = home.join("bin").join("cftunnel"); - if user_bin.exists() { - return user_bin.to_string_lossy().to_string(); - } - "cftunnel".to_string() -} -/// 通过 launchctl 检测 cftunnel 服务实际运行状态 -fn check_cftunnel_launchctl() -> Option<(Option, bool)> { - let output = Command::new("launchctl") - .args(["list"]) - .output() - .ok()?; - let text = String::from_utf8_lossy(&output.stdout); - for line in text.lines() { - if line.contains("com.cftunnel") { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 3 { - let pid = parts[0].parse::().ok(); - // 第一列是 PID(数字表示在运行,- 表示未运行) - let running = pid.is_some(); - return Some((pid, running)); + #[cfg(target_os = "windows")] + { + // Windows: 查找 cftunnel.exe + let candidates = [ + home.join("bin").join("cftunnel.exe"), + home.join(".cftunnel").join("cftunnel.exe"), + home.join("AppData").join("Local").join("cftunnel").join("cftunnel.exe"), + ]; + for path in &candidates { + if path.exists() { + return path.to_string_lossy().to_string(); } } + "cftunnel.exe".to_string() + } + + #[cfg(not(target_os = "windows"))] + { + let user_bin = home.join("bin").join("cftunnel"); + if user_bin.exists() { + return user_bin.to_string_lossy().to_string(); + } + "cftunnel".to_string() + } +} + +/// 检测 cftunnel 进程是否在运行(平台相关的补充检测) +fn check_cftunnel_process() -> Option<(Option, bool)> { + #[cfg(target_os = "macos")] + { + // macOS: 通过 launchctl 检测 + let output = Command::new("launchctl") + .args(["list"]) + .output() + .ok()?; + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines() { + if line.contains("com.cftunnel") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 { + let pid = parts[0].parse::().ok(); + let running = pid.is_some(); + return Some((pid, running)); + } + } + } + None + } + + #[cfg(target_os = "windows")] + { + // Windows: 通过 tasklist 检测 cftunnel.exe 进程 + let mut cmd = Command::new("tasklist"); + cmd.args(["/FI", "IMAGENAME eq cftunnel.exe", "/FO", "CSV", "/NH"]); + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + let output = cmd.output().ok()?; + let text = String::from_utf8_lossy(&output.stdout); + if text.contains("cftunnel.exe") { + // 尝试提取 PID(CSV 格式: "cftunnel.exe","1234",...) + let pid = text.lines().next() + .and_then(|line| line.split(',').nth(1)) + .and_then(|s| s.trim_matches('"').parse::().ok()); + return Some((pid, true)); + } + None + } + + #[cfg(target_os = "linux")] + { + // Linux: 通过 pgrep 检测 + let output = Command::new("pgrep") + .args(["-f", "cftunnel"]) + .output() + .ok()?; + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout); + let pid = text.lines().next() + .and_then(|s| s.trim().parse::().ok()); + return Some((pid, true)); + } + None } - None } #[tauri::command] @@ -108,10 +164,10 @@ pub fn get_cftunnel_status() -> Result { } } - // 补充检测:如果 cftunnel status 报已停止,但 launchctl 显示进程在跑,以实际为准 + // 补充检测:如果 cftunnel status 报已停止,但进程实际在跑,以实际为准 let reported_running = result.get("running").and_then(|v| v.as_bool()).unwrap_or(false); if !reported_running { - if let Some((pid, running)) = check_cftunnel_launchctl() { + if let Some((pid, running)) = check_cftunnel_process() { if running { result.insert("running".into(), Value::Bool(true)); if let Some(p) = pid { @@ -164,29 +220,53 @@ pub fn get_cftunnel_logs(lines: Option) -> Result { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } +/// 检测 ClawApp 状态(端口 3210) +/// 使用 TcpStream 跨平台检测端口,macOS 额外用 lsof 获取 PID #[tauri::command] pub fn get_clawapp_status() -> Result { let mut result = serde_json::Map::new(); - // 用 lsof 检测 :3210 端口 - let output = Command::new("lsof") - .args(["-i", ":3210", "-P", "-t"]) - .output(); + // 跨平台方式:尝试连接端口检测是否在运行 + let running = std::net::TcpStream::connect_timeout( + &"127.0.0.1:3210".parse().unwrap(), + std::time::Duration::from_millis(500), + ).is_ok(); - match output { - Ok(out) => { + result.insert("running".into(), Value::Bool(running)); + + // macOS: 用 lsof 获取 PID + #[cfg(target_os = "macos")] + if running { + if let Ok(out) = Command::new("lsof") + .args(["-i", ":3210", "-P", "-t"]) + .output() + { let text = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if text.is_empty() { - result.insert("running".into(), Value::Bool(false)); - } else { - result.insert("running".into(), Value::Bool(true)); - if let Ok(pid) = text.lines().next().unwrap_or("").parse::() { - result.insert("pid".into(), Value::Number(pid.into())); - } + if let Ok(pid) = text.lines().next().unwrap_or("").parse::() { + result.insert("pid".into(), Value::Number(pid.into())); } } - Err(_) => { - result.insert("running".into(), Value::Bool(false)); + } + + // Windows: 用 netstat 获取 PID + #[cfg(target_os = "windows")] + if running { + let mut cmd = Command::new("netstat"); + cmd.args(["-ano"]); + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + if let Ok(out) = cmd.output() + { + let text = String::from_utf8_lossy(&out.stdout); + for line in text.lines() { + if line.contains(":3210") && line.contains("LISTENING") { + if let Some(pid_str) = line.split_whitespace().last() { + if let Ok(pid) = pid_str.parse::() { + result.insert("pid".into(), Value::Number(pid.into())); + break; + } + } + } + } } } @@ -196,6 +276,8 @@ pub fn get_clawapp_status() -> Result { } /// 一键安装 cftunnel +/// macOS/Linux: bash 脚本安装 +/// Windows: PowerShell 下载安装 #[tauri::command] pub async fn install_cftunnel(app: tauri::AppHandle) -> Result { use std::process::Stdio; @@ -205,8 +287,12 @@ pub async fn install_cftunnel(app: tauri::AppHandle) -> Result { let _ = app.emit("install-log", "开始安装 cftunnel..."); let _ = app.emit("install-progress", 10); - // 下载并安装脚本 - let install_script = r#" + let _ = app.emit("install-log", "下载安装脚本..."); + let _ = app.emit("install-progress", 30); + + #[cfg(not(target_os = "windows"))] + let mut child = { + let install_script = r#" #!/bin/bash set -e cd /tmp @@ -217,22 +303,43 @@ echo "执行安装..." ./cftunnel-install.sh echo "安装完成" "#; + Command::new("bash") + .arg("-c") + .arg(install_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("启动安装进程失败: {e}"))? + }; - let _ = app.emit("install-log", "下载安装脚本..."); - let _ = app.emit("install-progress", 30); - - let mut child = Command::new("bash") - .arg("-c") - .arg(install_script) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|e| format!("启动安装进程失败: {e}"))?; + #[cfg(target_os = "windows")] + let mut child = { + let install_script = r#" +$ErrorActionPreference = 'Stop' +$binDir = Join-Path $env:USERPROFILE 'bin' +if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null } +Write-Output '下载 cftunnel...' +$url = 'https://github.com/qingchencloud/cftunnel/releases/latest/download/cftunnel-windows-amd64.exe' +$dest = Join-Path $binDir 'cftunnel.exe' +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing +Write-Output '安装完成' +"#; + // 使用完整路径调用 PowerShell,避免 MSYS2/Git Bash 环境下找不到 + let ps_path = std::env::var("SystemRoot") + .map(|root| format!("{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", root)) + .unwrap_or_else(|_| "powershell.exe".to_string()); + Command::new(&ps_path) + .args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", install_script]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("启动安装进程失败: {e}"))? + }; let stderr = child.stderr.take(); let stdout = child.stdout.take(); - // 读取 stderr let app2 = app.clone(); let handle = std::thread::spawn(move || { if let Some(pipe) = stderr { @@ -242,7 +349,6 @@ echo "安装完成" } }); - // 读取 stdout let mut progress = 40; if let Some(pipe) = stdout { for line in BufReader::new(pipe).lines().map_while(Result::ok) { diff --git a/src-tauri/src/commands/memory.rs b/src-tauri/src/commands/memory.rs index 517d7ee..f441b8e 100644 --- a/src-tauri/src/commands/memory.rs +++ b/src-tauri/src/commands/memory.rs @@ -2,11 +2,21 @@ use std::fs; use std::io::Write; use std::path::PathBuf; +use crate::utils::openclaw_command; + +/// 检查路径是否包含不安全字符(目录遍历、绝对路径等) +fn is_unsafe_path(path: &str) -> bool { + path.contains("..") + || path.contains('\0') + || path.starts_with('/') + || path.starts_with('\\') + || (path.len() >= 2 && path.as_bytes()[1] == b':') // Windows 绝对路径 C:\ +} /// 根据 agent_id 获取 workspace 路径 /// 调用 openclaw agents list --json 解析 fn agent_workspace(agent_id: &str) -> Result { - let output = std::process::Command::new("openclaw") + let output = openclaw_command() .args(["agents", "list", "--json"]) .output() .map_err(|e| format!("执行 openclaw 失败: {e}"))?; @@ -96,7 +106,7 @@ fn collect_files( #[tauri::command] pub fn read_memory_file(path: String, agent_id: Option) -> Result { - if path.contains("..") || path.starts_with('/') || path.contains('\0') { + if is_unsafe_path(&path) { return Err("非法路径".to_string()); } @@ -122,7 +132,7 @@ pub fn read_memory_file(path: String, agent_id: Option) -> Result, agent_id: Option) -> Result<(), String> { - if path.contains("..") || path.starts_with('/') || path.contains('\0') { + if is_unsafe_path(&path) { return Err("非法路径".to_string()); } @@ -139,7 +149,7 @@ pub fn write_memory_file(path: String, content: String, category: Option #[tauri::command] pub fn delete_memory_file(path: String, agent_id: Option) -> Result<(), String> { - if path.contains("..") || path.starts_with('/') || path.contains('\0') { + if is_unsafe_path(&path) { return Err("非法路径".to_string()); } diff --git a/src-tauri/src/commands/service.rs b/src-tauri/src/commands/service.rs index 9303582..73f0fef 100644 --- a/src-tauri/src/commands/service.rs +++ b/src-tauri/src/commands/service.rs @@ -1,22 +1,10 @@ -/// 服务管理命令 (macOS launchd) -/// 只扫描 OpenClaw 核心服务 (ai.openclaw.* / com.openclaw.guardian.* / com.openclaw.watchdog) -/// 使用新版 launchctl bootstrap/bootout/kickstart API +/// 服务管理命令 +/// macOS: launchctl + LaunchAgents plist +/// Windows: openclaw CLI + 进程检测 use std::collections::HashMap; -use std::fs; -use std::process::Command; use crate::models::types::ServiceStatus; -/// 获取当前用户 UID -fn current_uid() -> Result { - let output = Command::new("id") - .arg("-u") - .output() - .map_err(|e| format!("获取 UID 失败: {e}"))?; - let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - uid_str.parse::().map_err(|e| format!("解析 UID 失败: {e}")) -} - /// OpenClaw 官方服务的友好名称映射 fn description_map() -> HashMap<&'static str, &'static str> { HashMap::from([ @@ -25,92 +13,372 @@ fn description_map() -> HashMap<&'static str, &'static str> { ]) } -/// OpenClaw 官方服务前缀(ai.openclaw.gateway / ai.openclaw.node 等) -const OPENCLAW_PREFIXES: &[&str] = &[ - "ai.openclaw.", -]; +// ===== macOS 实现 ===== -/// 动态扫描 LaunchAgents 目录,只返回 OpenClaw 核心服务 -fn scan_plist_labels() -> Vec { - let home = dirs::home_dir().unwrap_or_default(); - let agents_dir = home.join("Library/LaunchAgents"); - let mut labels = Vec::new(); +#[cfg(target_os = "macos")] +mod platform { + use std::fs; + use std::process::Command; - if let Ok(entries) = fs::read_dir(&agents_dir) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - if !name.ends_with(".plist") { + const OPENCLAW_PREFIXES: &[&str] = &["ai.openclaw."]; + + /// macOS 上 CLI 是否安装(检查 plist 是否存在即可) + pub fn is_cli_installed() -> bool { + true // macOS 通过 plist 扫描,不依赖 CLI 检测 + } + + pub fn current_uid() -> Result { + let output = Command::new("id") + .arg("-u") + .output() + .map_err(|e| format!("获取 UID 失败: {e}"))?; + let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + uid_str.parse::().map_err(|e| format!("解析 UID 失败: {e}")) + } + + /// 动态扫描 LaunchAgents 目录,只返回 OpenClaw 核心服务 + pub fn scan_service_labels() -> Vec { + let home = dirs::home_dir().unwrap_or_default(); + let agents_dir = home.join("Library/LaunchAgents"); + let mut labels = Vec::new(); + + if let Ok(entries) = fs::read_dir(&agents_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if !name.ends_with(".plist") { + continue; + } + let label = name.trim_end_matches(".plist"); + if OPENCLAW_PREFIXES.iter().any(|p| label.starts_with(p)) { + labels.push(label.to_string()); + } + } + } + labels.sort(); + labels + } + + fn plist_path(label: &str) -> String { + let home = dirs::home_dir().unwrap_or_default(); + format!( + "{}/Library/LaunchAgents/{}.plist", + home.display(), + label + ) + } + + /// 用 launchctl print 检测单个服务状态,返回 (running, pid) + pub fn check_service_status(uid: u32, label: &str) -> (bool, Option) { + let target = format!("gui/{}/{}", uid, label); + let output = Command::new("launchctl") + .args(["print", &target]) + .output(); + + let Ok(out) = output else { + return (false, None); + }; + + if !out.status.success() { + return (false, None); + } + + let stdout = String::from_utf8_lossy(&out.stdout); + let mut pid: Option = None; + let mut running = false; + + for line in stdout.lines() { + if !line.starts_with('\t') || line.starts_with("\t\t") { continue; } - let label = name.trim_end_matches(".plist"); - if OPENCLAW_PREFIXES.iter().any(|p| label.starts_with(p)) { - labels.push(label.to_string()); + let trimmed = line.trim(); + if trimmed.starts_with("pid = ") { + if let Ok(p) = trimmed["pid = ".len()..].trim().parse::() { + pid = Some(p); + } + } + if trimmed.starts_with("state = ") { + let state = trimmed["state = ".len()..].trim(); + running = state == "running"; + } + } + + (running, pid) + } + + pub fn start_service_impl(label: &str) -> Result<(), String> { + let uid = current_uid()?; + let path = plist_path(label); + let domain_target = format!("gui/{}", uid); + let service_target = format!("gui/{}/{}", uid, label); + + let bootstrap_out = Command::new("launchctl") + .args(["bootstrap", &domain_target, &path]) + .output() + .map_err(|e| format!("bootstrap 失败: {e}"))?; + + if !bootstrap_out.status.success() { + let stderr = String::from_utf8_lossy(&bootstrap_out.stderr); + if !stderr.contains("already bootstrapped") && !stderr.trim().is_empty() { + return Err(format!("启动 {label} 失败: {stderr}")); + } + } + + let kickstart_out = Command::new("launchctl") + .args(["kickstart", &service_target]) + .output() + .map_err(|e| format!("kickstart 失败: {e}"))?; + + if !kickstart_out.status.success() { + let stderr = String::from_utf8_lossy(&kickstart_out.stderr); + if !stderr.trim().is_empty() { + return Err(format!("kickstart {label} 失败: {stderr}")); + } + } + + Ok(()) + } + + pub fn stop_service_impl(label: &str) -> Result<(), String> { + let uid = current_uid()?; + let service_target = format!("gui/{}/{}", uid, label); + + let output = Command::new("launchctl") + .args(["bootout", &service_target]) + .output() + .map_err(|e| format!("停止失败: {e}"))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + if !stderr.contains("No such process") + && !stderr.contains("Could not find specified service") + && !stderr.trim().is_empty() + { + return Err(format!("停止 {label} 失败: {stderr}")); + } + } + + Ok(()) + } + + pub fn restart_service_impl(label: &str) -> Result<(), String> { + let uid = current_uid()?; + let path = plist_path(label); + let domain_target = format!("gui/{}", uid); + let service_target = format!("gui/{}/{}", uid, label); + + let _ = Command::new("launchctl") + .args(["bootout", &service_target]) + .output(); + + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); + loop { + let (running, _) = check_service_status(uid, label); + if !running || std::time::Instant::now() >= deadline { + break; + } + std::thread::sleep(std::time::Duration::from_millis(200)); + } + + let bootstrap_out = Command::new("launchctl") + .args(["bootstrap", &domain_target, &path]) + .output() + .map_err(|e| format!("重启 bootstrap 失败: {e}"))?; + + if !bootstrap_out.status.success() { + let stderr = String::from_utf8_lossy(&bootstrap_out.stderr); + if !stderr.contains("already bootstrapped") && !stderr.trim().is_empty() { + return Err(format!("重启 {label} 失败 (bootstrap): {stderr}")); + } + } + + let kickstart_out = Command::new("launchctl") + .args(["kickstart", "-k", &service_target]) + .output() + .map_err(|e| format!("重启 kickstart 失败: {e}"))?; + + if !kickstart_out.status.success() { + let stderr = String::from_utf8_lossy(&kickstart_out.stderr); + if !stderr.trim().is_empty() { + return Err(format!("重启 {label} 失败 (kickstart): {stderr}")); + } + } + + Ok(()) + } +} + +// ===== Windows 实现 ===== + +#[cfg(target_os = "windows")] +mod platform { + use crate::utils::openclaw_command; + + /// Windows 不需要 UID + pub fn current_uid() -> Result { + Ok(0) + } + + /// 检测 openclaw CLI 是否已安装 + pub fn is_cli_installed() -> bool { + openclaw_command() + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + /// Windows 上始终返回 Gateway 标签(不管 CLI 是否安装) + pub fn scan_service_labels() -> Vec { + vec!["ai.openclaw.gateway".to_string()] + } + + /// 通过端口探测检测 Gateway 状态 + pub fn check_service_status(_uid: u32, _label: &str) -> (bool, Option) { + match std::net::TcpStream::connect_timeout( + &"127.0.0.1:18789".parse().unwrap(), + std::time::Duration::from_millis(500), + ) { + Ok(_) => (true, None), + Err(_) => (false, None), + } + } + + /// 以前台模式 spawn Gateway(不需要管理员权限) + pub fn start_service_impl(_label: &str) -> Result<(), String> { + if !is_cli_installed() { + return Err("openclaw CLI 未安装,请先通过 npm install -g @qingchencloud/openclaw-zh 安装".into()); + } + if check_service_status(0, "").0 { + return Ok(()); + } + crate::utils::openclaw_command() + .arg("gateway") + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .map_err(|e| format!("启动 Gateway 失败: {e}"))?; + + for _ in 0..25 { + std::thread::sleep(std::time::Duration::from_millis(200)); + if check_service_status(0, "").0 { + return Ok(()); + } + } + Err("Gateway 启动超时,请检查日志".into()) + } + + pub fn stop_service_impl(_label: &str) -> Result<(), String> { + let _ = crate::utils::openclaw_command() + .args(["gateway", "stop"]) + .output(); + if check_service_status(0, "").0 { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + let _ = std::process::Command::new("cmd") + .args(["/c", "taskkill", "/f", "/im", "node.exe", "/fi", "WINDOWTITLE eq openclaw*"]) + .creation_flags(CREATE_NO_WINDOW) + .output(); + } + Ok(()) + } + + pub fn restart_service_impl(_label: &str) -> Result<(), String> { + let _ = stop_service_impl(_label); + for _ in 0..10 { + if !check_service_status(0, "").0 { break; } + std::thread::sleep(std::time::Duration::from_millis(300)); + } + start_service_impl(_label) + } +} + +// ===== Linux 实现(与 Windows 类似,使用 openclaw CLI) ===== + +#[cfg(target_os = "linux")] +mod platform { + use std::process::Command; + + pub fn current_uid() -> Result { + let output = Command::new("id") + .arg("-u") + .output() + .map_err(|e| format!("获取 UID 失败: {e}"))?; + let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + uid_str.parse::().map_err(|e| format!("解析 UID 失败: {e}")) + } + + pub fn is_cli_installed() -> bool { + Command::new("openclaw") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + pub fn scan_service_labels() -> Vec { + vec!["ai.openclaw.gateway".to_string()] + } + + pub fn check_service_status(_uid: u32, _label: &str) -> (bool, Option) { + match std::net::TcpStream::connect_timeout( + &"127.0.0.1:18789".parse().unwrap(), + std::time::Duration::from_secs(2), + ) { + Ok(_) => (true, None), + Err(_) => { + if let Ok(output) = Command::new("openclaw").arg("health").output() { + let text = String::from_utf8_lossy(&output.stdout); + if output.status.success() && !text.contains("not running") { + return (true, None); + } + } + (false, None) } } } - labels.sort(); - labels -} -fn plist_path(label: &str) -> String { - let home = dirs::home_dir().unwrap_or_default(); - format!( - "{}/Library/LaunchAgents/{}.plist", - home.display(), - label - ) -} + fn gateway_command(action: &str) -> Result<(), String> { + if !is_cli_installed() { + return Err("openclaw CLI 未安装,请先通过 npm install -g @qingchencloud/openclaw-zh 安装".into()); + } + let output = crate::utils::openclaw_command() + .args(["gateway", action]) + .output() + .map_err(|e| format!("执行 openclaw gateway {action} 失败: {e}"))?; -/// 用 `launchctl print gui/{uid}/{label}` 检测单个服务状态 -/// 返回 (running, pid) -fn check_service_status(uid: u32, label: &str) -> (bool, Option) { - let target = format!("gui/{}/{}", uid, label); - let output = Command::new("launchctl") - .args(["print", &target]) - .output(); - - let Ok(out) = output else { - return (false, None); - }; - - // launchctl print 返回非零 → 服务未注册 - if !out.status.success() { - return (false, None); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("openclaw gateway {action} 失败: {stderr}")); + } + Ok(()) } - let stdout = String::from_utf8_lossy(&out.stdout); - let mut pid: Option = None; - let mut running = false; - - for line in stdout.lines() { - // 只解析顶层字段(单个 tab 缩进),忽略嵌套的 state = active 等 - if !line.starts_with('\t') || line.starts_with("\t\t") { - continue; - } - let trimmed = line.trim(); - if trimmed.starts_with("pid = ") { - if let Ok(p) = trimmed["pid = ".len()..].trim().parse::() { - pid = Some(p); - } - } - if trimmed.starts_with("state = ") { - let state = trimmed["state = ".len()..].trim(); - running = state == "running"; - } + pub fn start_service_impl(_label: &str) -> Result<(), String> { + gateway_command("start") } - (running, pid) + pub fn stop_service_impl(_label: &str) -> Result<(), String> { + gateway_command("stop") + } + + pub fn restart_service_impl(_label: &str) -> Result<(), String> { + gateway_command("restart") + } } +// ===== 跨平台公共接口 ===== + #[tauri::command] pub fn get_services_status() -> Result, String> { - let uid = current_uid()?; - let labels = scan_plist_labels(); + let uid = platform::current_uid()?; + let labels = platform::scan_service_labels(); let desc_map = description_map(); + let cli_installed = platform::is_cli_installed(); let mut results = Vec::new(); for label in &labels { - let (running, pid) = check_service_status(uid, label); + let (running, pid) = platform::check_service_status(uid, label); results.push(ServiceStatus { label: label.clone(), pid, @@ -119,6 +387,7 @@ pub fn get_services_status() -> Result, String> { .get(label.as_str()) .unwrap_or(&"") .to_string(), + cli_installed, }); } @@ -127,115 +396,15 @@ pub fn get_services_status() -> Result, String> { #[tauri::command] pub fn start_service(label: String) -> Result<(), String> { - let uid = current_uid()?; - let path = plist_path(&label); - let domain_target = format!("gui/{}", uid); - let service_target = format!("gui/{}/{}", uid, label); - - // bootstrap 加载 plist - let bootstrap_out = Command::new("launchctl") - .args(["bootstrap", &domain_target, &path]) - .output() - .map_err(|e| format!("bootstrap 失败: {e}"))?; - - if !bootstrap_out.status.success() { - let stderr = String::from_utf8_lossy(&bootstrap_out.stderr); - // 如果已经加载过,忽略该错误,继续 kickstart - if !stderr.contains("already bootstrapped") && !stderr.trim().is_empty() { - return Err(format!("启动 {label} 失败: {stderr}")); - } - } - - // kickstart 触发服务运行 - let kickstart_out = Command::new("launchctl") - .args(["kickstart", &service_target]) - .output() - .map_err(|e| format!("kickstart 失败: {e}"))?; - - if !kickstart_out.status.success() { - let stderr = String::from_utf8_lossy(&kickstart_out.stderr); - if !stderr.trim().is_empty() { - return Err(format!("kickstart {label} 失败: {stderr}")); - } - } - - Ok(()) + platform::start_service_impl(&label) } #[tauri::command] pub fn stop_service(label: String) -> Result<(), String> { - let uid = current_uid()?; - let service_target = format!("gui/{}/{}", uid, label); - - let output = Command::new("launchctl") - .args(["bootout", &service_target]) - .output() - .map_err(|e| format!("停止失败: {e}"))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - // 忽略"未加载"类错误 - if !stderr.contains("No such process") - && !stderr.contains("Could not find specified service") - && !stderr.trim().is_empty() - { - return Err(format!("停止 {label} 失败: {stderr}")); - } - } - - Ok(()) + platform::stop_service_impl(&label) } #[tauri::command] pub fn restart_service(label: String) -> Result<(), String> { - let uid = current_uid()?; - let path = plist_path(&label); - let domain_target = format!("gui/{}", uid); - let service_target = format!("gui/{}/{}", uid, label); - - // 第一步:bootout 停止服务(忽略未加载错误) - let _ = Command::new("launchctl") - .args(["bootout", &service_target]) - .output(); - - // 第二步:轮询等待旧进程退出,最多等 3 秒 - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); - loop { - let (running, _) = check_service_status(uid, &label); - if !running { - break; - } - if std::time::Instant::now() >= deadline { - break; // 超时后继续尝试 - } - std::thread::sleep(std::time::Duration::from_millis(200)); - } - - // 第三步:bootstrap 重新加载 plist - let bootstrap_out = Command::new("launchctl") - .args(["bootstrap", &domain_target, &path]) - .output() - .map_err(|e| format!("重启 bootstrap 失败: {e}"))?; - - if !bootstrap_out.status.success() { - let stderr = String::from_utf8_lossy(&bootstrap_out.stderr); - if !stderr.contains("already bootstrapped") && !stderr.trim().is_empty() { - return Err(format!("重启 {label} 失败 (bootstrap): {stderr}")); - } - } - - // 第四步:kickstart -k 强制重启 - let kickstart_out = Command::new("launchctl") - .args(["kickstart", "-k", &service_target]) - .output() - .map_err(|e| format!("重启 kickstart 失败: {e}"))?; - - if !kickstart_out.status.success() { - let stderr = String::from_utf8_lossy(&kickstart_out.stderr); - if !stderr.trim().is_empty() { - return Err(format!("重启 {label} 失败 (kickstart): {stderr}")); - } - } - - Ok(()) + platform::restart_service_impl(&label) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3cc4178..a6ca1dd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,17 @@ mod commands; mod models; +mod tray; +mod utils; use commands::{agent, config, device, extensions, logs, memory, service}; pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .setup(|app| { + tray::setup_tray(app.handle())?; + Ok(()) + }) .invoke_handler(tauri::generate_handler![ // 配置 config::read_openclaw_config, @@ -14,6 +20,7 @@ pub fn run() { config::write_mcp_config, config::get_version_info, config::check_installation, + config::check_node, config::write_env_file, config::list_backups, config::create_backup, diff --git a/src-tauri/src/models/types.rs b/src-tauri/src/models/types.rs index b4e5e4f..a28d377 100644 --- a/src-tauri/src/models/types.rs +++ b/src-tauri/src/models/types.rs @@ -6,6 +6,8 @@ pub struct ServiceStatus { pub pid: Option, pub running: bool, pub description: String, + /// CLI 工具是否已安装(Windows/Linux: openclaw CLI) + pub cli_installed: bool, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs new file mode 100644 index 0000000..027c897 --- /dev/null +++ b/src-tauri/src/tray.rs @@ -0,0 +1,77 @@ +/// 系统托盘模块 +/// Windows / macOS / Linux 通用,Tauri v2 内置跨平台支持 +use tauri::{ + AppHandle, Manager, + menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem}, + tray::TrayIconBuilder, + image::Image, +}; + +pub fn setup_tray(app: &AppHandle) -> Result<(), Box> { + // 菜单项 + let show = MenuItemBuilder::with_id("show", "显示主窗口").build(app)?; + let separator1 = PredefinedMenuItem::separator(app)?; + let gateway_start = MenuItemBuilder::with_id("gateway_start", "启动 Gateway").build(app)?; + let gateway_stop = MenuItemBuilder::with_id("gateway_stop", "停止 Gateway").build(app)?; + let gateway_restart = MenuItemBuilder::with_id("gateway_restart", "重启 Gateway").build(app)?; + let separator2 = PredefinedMenuItem::separator(app)?; + let quit = MenuItemBuilder::with_id("quit", "退出 ClawPanel").build(app)?; + + let menu = MenuBuilder::new(app) + .item(&show) + .item(&separator1) + .item(&gateway_start) + .item(&gateway_stop) + .item(&gateway_restart) + .item(&separator2) + .item(&quit) + .build()?; + + // 托盘图标(使用内嵌 32x32 PNG) + let icon = Image::from_bytes(include_bytes!("../icons/32x32.png"))?; + + let _tray = TrayIconBuilder::new() + .icon(icon) + .tooltip("ClawPanel") + .menu(&menu) + .on_menu_event(move |app, event| { + handle_menu_event(app, event.id().as_ref()); + }) + .on_tray_icon_event(|tray, event| { + if let tauri::tray::TrayIconEvent::DoubleClick { .. } = event { + if let Some(window) = tray.app_handle().get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } + } + }) + .build(app)?; + + Ok(()) +} +fn handle_menu_event(app: &AppHandle, id: &str) { + match id { + "show" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } + } + "gateway_start" => { + let _ = crate::commands::service::start_service("ai.openclaw.gateway".into()); + } + "gateway_stop" => { + let _ = crate::commands::service::stop_service("ai.openclaw.gateway".into()); + } + "gateway_restart" => { + let _ = crate::commands::service::restart_service("ai.openclaw.gateway".into()); + } + "quit" => { + app.exit(0); + } + _ => {} + } +} + diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs new file mode 100644 index 0000000..510f457 --- /dev/null +++ b/src-tauri/src/utils.rs @@ -0,0 +1,20 @@ +use std::process::Command; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; + +/// 跨平台获取 openclaw 命令的方法 +/// 在 Windows 上使用 `cmd /c openclaw` 以兼容全局 npm 路径下的 `.cmd` 脚本 +pub fn openclaw_command() -> Command { + #[cfg(target_os = "windows")] + { + const CREATE_NO_WINDOW: u32 = 0x08000000; + let mut cmd = Command::new("cmd"); + cmd.arg("/c").arg("openclaw"); + cmd.creation_flags(CREATE_NO_WINDOW); + cmd + } + #[cfg(not(target_os = "windows"))] + { + Command::new("openclaw") + } +} diff --git a/src/components/sidebar.js b/src/components/sidebar.js index 55cbb27..18944eb 100644 --- a/src/components/sidebar.js +++ b/src/components/sidebar.js @@ -3,8 +3,9 @@ */ import { navigate, getCurrentRoute } from '../router.js' import { toggleTheme, getTheme } from '../lib/theme.js' +import { isOpenclawReady } from '../lib/app-state.js' -const NAV_ITEMS = [ +const NAV_ITEMS_FULL = [ { section: '概览', items: [ @@ -42,7 +43,29 @@ const NAV_ITEMS = [ } ] +const NAV_ITEMS_SETUP = [ + { + section: '', + items: [ + { route: '/setup', label: '初始设置', icon: 'setup' }, + ] + }, + { + section: '扩展', + items: [ + { route: '/extensions', label: '扩展工具', icon: 'extensions' }, + ] + }, + { + section: '', + items: [ + { route: '/about', label: '关于', icon: 'about' }, + ] + } +] + const ICONS = { + setup: '', dashboard: '', chat: '', services: '', @@ -63,21 +86,16 @@ export function renderSidebar(el) { let html = `