diff --git a/docs/content/08.open-platform/1.overview.md b/docs/content/08.open-platform/1.overview.md new file mode 100644 index 000000000..c076289e7 --- /dev/null +++ b/docs/content/08.open-platform/1.overview.md @@ -0,0 +1,63 @@ +# 开放平台概述 + +ProxyCast 开放平台为开发者和服务商提供扩展能力,包括插件系统和中转商生态合作方案。 + +## 平台能力 + +### 🔌 插件系统 + +通过插件扩展 ProxyCast 功能: + +- **脚本插件** - JavaScript/TypeScript 插件,通过 Hook 机制扩展 +- **二进制插件** - 独立可执行文件,适合系统级操作 +- **工具插件** - 在工具箱中显示的独立工具 + +[了解更多 →](/open-platform/plugins) + +### 🔗 ProxyCast Connect + +中转商生态合作方案,实现一键配置: + +- **一键配置** - 用户点击链接即可完成配置 +- **品牌展示** - 中转商在 ProxyCast 内有专属展示位 +- **统计回调** - 追踪推广效果 + +[了解更多 →](/open-platform/connect) + +## 核心价值 + +| 角色 | 价值 | +|------|------| +| **开发者** | 扩展功能、定制工具、集成服务 | +| **中转商** | 用户转化率提升、品牌曝光、差异化竞争 | +| **用户** | 一键配置、开箱即用、统一管理 | +| **ProxyCast** | 生态繁荣、用户增长 | + +## 快速开始 + +
+ +

🔌 开发插件

+

创建自定义插件扩展 ProxyCast 功能

+
+ +

🔗 接入 Connect

+

中转商接入一键配置功能

+
+ +

📖 插件开发指南

+

详细的插件开发文档和规范

+
+ +

📖 Connect 接入指南

+

中转商接入的详细步骤

+
+
+ +## 相关仓库 + +| 仓库 | 说明 | +|------|------| +| [proxycast](https://github.com/aiclientproxy/proxycast) | ProxyCast 主项目 | +| [connect](https://github.com/aiclientproxy/connect) | 中转商注册仓库 | +| [MachineIdTool](https://github.com/aiclientproxy/MachineIdTool) | 插件示例项目 | diff --git a/docs/content/08.open-platform/2.plugins.md b/docs/content/08.open-platform/2.plugins.md new file mode 100644 index 000000000..e99879771 --- /dev/null +++ b/docs/content/08.open-platform/2.plugins.md @@ -0,0 +1,112 @@ +# 插件中心 + +ProxyCast 支持通过插件扩展功能。插件中心提供插件的安装、管理和配置。 + +## 访问插件中心 + +点击左侧导航栏的「插件中心」进入插件管理页面。 + +## 功能概览 + +### 推荐插件 + +插件中心会显示推荐的插件列表,点击「一键安装」即可快速安装。 + +### 已安装插件 + +显示所有已安装的插件,包括: +- 插件名称和版本 +- 安装来源(本地/URL/GitHub) +- 启用/禁用状态 +- 卸载按钮 + +### 已加载插件 + +显示当前运行中的插件状态: +- 执行次数 +- 错误次数 +- 最后执行时间 + +## 安装插件 + +### 方式一:推荐插件一键安装 + +1. 在「推荐插件」区域找到想要的插件 +2. 点击「一键安装」 +3. 等待下载和安装完成 + +### 方式二:从 URL 安装 + +1. 点击「安装插件」按钮 +2. 输入插件 ZIP 包的下载 URL +3. 点击「安装」 + +支持的 URL 格式: +- GitHub Release: `https://github.com/org/repo/releases/latest/download/plugin.zip` +- 直接下载链接: `https://example.com/plugin.zip` + +### 方式三:从本地文件安装 + +1. 点击「安装插件」按钮 +2. 点击「选择文件」 +3. 选择本地的 `.zip` 文件 +4. 点击「安装」 + +## 使用插件 + +安装完成后,插件会根据类型出现在不同位置: + +### 工具类插件 + +工具类插件会出现在「工具箱」页面: + +1. 点击左侧导航栏的「工具」 +2. 在工具列表中找到已安装的插件 +3. 点击「打开工具」使用 + +### 其他类型插件 + +- **Hook 插件**: 自动在请求/响应时执行 +- **侧边栏插件**: 出现在主侧边栏(规划中) + +## 管理插件 + +### 启用/禁用 + +在已加载插件列表中,点击电源图标可以启用或禁用插件。 + +### 卸载 + +1. 在「已安装插件包」列表中找到要卸载的插件 +2. 点击红色的删除按钮 +3. 确认卸载 + +卸载会删除插件文件和配置,但不会删除插件产生的数据。 + +## 二进制组件 + +部分功能需要安装额外的二进制组件: + +- **aster-server**: AI Agent 框架,提供 Agent 对话能力 + +在「二进制组件」区域可以查看和管理这些组件。 + +## 常见问题 + +### 插件安装失败 + +1. 检查网络连接 +2. 确认 URL 正确且可访问 +3. 检查 ZIP 包格式是否正确 + +### 插件无法加载 + +1. 检查 ProxyCast 版本是否满足插件要求 +2. 查看日志了解详细错误信息 +3. 尝试重新安装插件 + +### 二进制插件权限问题 + +部分二进制插件需要管理员权限: +- **Windows**: 以管理员身份运行 ProxyCast +- **macOS/Linux**: 插件会提示需要的权限 diff --git a/docs/content/08.open-platform/3.plugin-development.md b/docs/content/08.open-platform/3.plugin-development.md new file mode 100644 index 000000000..243f8e735 --- /dev/null +++ b/docs/content/08.open-platform/3.plugin-development.md @@ -0,0 +1,219 @@ +# 插件开发指南 + +本文档描述 ProxyCast 插件系统的规范和开发指南。 + +## 插件类型 + +ProxyCast 支持两种类型的插件: + +### 1. 脚本插件 (Script Plugin) + +纯 JavaScript/TypeScript 插件,通过 Hook 机制扩展功能。 + +```json +{ + "plugin_type": "script", + "entry": "main.js", + "hooks": ["on_request", "on_response"] +} +``` + +### 2. 二进制插件 (Binary Plugin) + +独立的可执行文件,通过 CLI 接口与 ProxyCast 通信。适合需要系统级操作的工具。 + +```json +{ + "plugin_type": "binary", + "entry": "my-tool-cli", + "binary": { + "binary_name": "my-tool-cli", + "github_owner": "your-org", + "github_repo": "your-repo", + "platform_binaries": { + "macos-arm64": "my-tool-aarch64-apple-darwin", + "macos-x64": "my-tool-x86_64-apple-darwin", + "linux-x64": "my-tool-x86_64-unknown-linux-gnu", + "linux-arm64": "my-tool-aarch64-unknown-linux-gnu", + "windows-x64": "my-tool-x86_64-pc-windows-msvc.exe" + }, + "checksum_file": "checksums.txt" + } +} +``` + +## 插件包结构 + +插件以 ZIP 包形式分发,包含以下文件: + +``` +my-plugin.zip +├── plugin.json # 插件元数据(必需) +└── config.json # 默认配置(可选) +``` + +### plugin.json 规范 + +```json +{ + "name": "my-plugin", + "version": "1.0.0", + "description": "插件描述", + "author": "作者名", + "homepage": "https://github.com/org/repo", + "license": "MIT", + "plugin_type": "binary", + "entry": "my-tool-cli", + "hooks": [], + "min_proxycast_version": "1.0.0", + "binary": { ... }, + "ui": { + "surfaces": ["tools"], + "icon": "Cpu", + "title": "我的工具", + "default_width": 800, + "default_height": 600 + } +} +``` + +### 字段说明 + +| 字段 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `name` | string | ✅ | 插件唯一标识符,小写字母和连字符 | +| `version` | string | ✅ | 语义化版本号 (semver) | +| `description` | string | ✅ | 插件描述 | +| `author` | string | ❌ | 作者名称 | +| `homepage` | string | ❌ | 项目主页 URL | +| `license` | string | ❌ | 开源许可证 | +| `plugin_type` | string | ✅ | `script` 或 `binary` | +| `entry` | string | ✅ | 入口文件/二进制名称 | +| `hooks` | array | ❌ | 注册的 Hook 列表 | +| `min_proxycast_version` | string | ❌ | 最低 ProxyCast 版本要求 | +| `binary` | object | ❌ | 二进制插件配置 | +| `ui` | object | ❌ | UI 配置 | + +## UI 展示位置 (Surfaces) + +插件可以在以下位置显示 UI: + +| Surface | 说明 | 入口位置 | +|---------|------|----------| +| `tools` | 工具箱 | 导航栏「工具」页面 | +| `sidebar` | 侧边栏 | 主侧边栏(规划中) | +| `settings` | 设置页 | 设置页扩展区域(规划中) | + +### 示例:工具类插件 + +```json +{ + "ui": { + "surfaces": ["tools"], + "icon": "Cpu", + "title": "机器码管理工具" + } +} +``` + +## 图标规范 + +使用 [Lucide Icons](https://lucide.dev/icons/) 图标名称: + +常用图标: +- `Cpu` - 系统/硬件工具 +- `Globe` - 网络工具 +- `Database` - 数据工具 +- `Shield` - 安全工具 +- `Wrench` - 通用工具 +- `Terminal` - 命令行工具 + +## 二进制插件 CLI 接口规范 + +二进制插件通过 CLI 与 ProxyCast 通信,必须遵循以下规范: + +### 输出格式 + +所有输出必须是 JSON 格式: + +```bash +# 成功 +$ my-tool-cli get +{"machine_id": "550e8400-e29b-41d4-a716-446655440000"} + +# 错误 +$ my-tool-cli invalid-command +{"error": "未知命令: invalid-command"} +``` + +### 退出码 + +- `0` - 成功 +- `1` - 错误 + +### 命令结构 + +```bash +my-tool-cli [arguments] +``` + +建议实现 `help` 命令: + +```bash +$ my-tool-cli help +MachineIdTool CLI v1.0.0 +用法: my-tool-cli <命令> [参数] + +命令: + get 获取当前值 + set 设置新值 + help 显示帮助 +``` + +## 插件安装流程 + +1. **下载插件包** - 从 URL 或本地文件获取 ZIP +2. **解压验证** - 解压并验证 plugin.json +3. **下载二进制** - 如果是二进制插件,根据当前平台下载对应二进制 +4. **校验完整性** - 验证 checksum +5. **注册插件** - 将插件信息写入数据库 +6. **加载插件** - 启用插件功能 + +## 插件发布 + +### GitHub Release 发布 + +推荐通过 GitHub Release 发布插件: + +1. 创建 `plugin/` 目录,包含 `plugin.json` 和 `config.json` +2. 在 GitHub Actions 中打包 ZIP +3. 上传到 Release Assets + +```yaml +- name: Package plugin + run: | + mkdir -p plugin-package + cp plugin/plugin.json plugin-package/ + cp plugin/config.json plugin-package/ + cd plugin-package + zip -j ../release/my-plugin.zip plugin.json config.json +``` + +## 开发调试 + +### 本地安装测试 + +1. 打包插件 ZIP +2. 在「插件中心」点击「安装插件」 +3. 选择本地 ZIP 文件安装 + +### 日志调试 + +插件执行日志保存在: +- macOS: `~/Library/Application Support/proxycast/logs/` +- Windows: `%APPDATA%/proxycast/logs/` +- Linux: `~/.local/share/proxycast/logs/` + +## 示例插件 + +参考 [MachineIdTool](https://github.com/aiclientproxy/MachineIdTool) 作为二进制插件的完整示例。 diff --git a/docs/content/08.open-platform/4.connect.md b/docs/content/08.open-platform/4.connect.md new file mode 100644 index 000000000..52f3a006b --- /dev/null +++ b/docs/content/08.open-platform/4.connect.md @@ -0,0 +1,169 @@ +# ProxyCast Connect + +ProxyCast Connect 是一套中转商生态合作方案,通过 Deep Link 协议实现一键配置功能。 + +## 核心价值 + +| 角色 | 价值 | +|------|------| +| **中转商** | 用户转化率提升、品牌曝光、差异化竞争 | +| **用户** | 一键配置、开箱即用、统一管理多个中转 | +| **ProxyCast** | 用户增长、生态繁荣、市场占有率 | + +## 工作原理 + +### 一键配置流程 + +1. 用户在中转商后台点击「一键配置 ProxyCast」 +2. 浏览器打开 `proxycast://connect?relay=xxx&key=sk-xxx` 链接 +3. ProxyCast 自动打开,显示确认弹窗 +4. 用户确认后,API Key 自动添加到 ProxyCast +5. 配置完成,可以立即使用 + +### Deep Link 协议 + +``` +proxycast://connect?relay={relay_id}&key={api_key}&name={key_name}&ref={ref_code} +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| `relay` | ✅ | 中转商 ID(需在 ProxyCast 注册) | +| `key` | ✅ | API Key | +| `name` | ❌ | Key 名称(默认使用中转商名称) | +| `ref` | ❌ | 推广码(用于统计) | + +### 示例 + +``` +# 基础用法 +proxycast://connect?relay=openrouter&key=sk-or-v1-xxxx + +# 带名称 +proxycast://connect?relay=siliconflow&key=sk-xxxx&name=硅基流动-主账号 + +# 带推广码 +proxycast://connect?relay=myrelay&key=sk-xxxx&ref=promo2024 +``` + +## 中转商注册 + +ProxyCast 是开源软件,中转商通过 **GitHub PR** 方式注册,无需网站注册。 + +### 注册仓库 + +``` +https://github.com/aiclientproxy/connect +``` + +### 注册流程 + +1. **Fork 仓库** - Fork [aiclientproxy/connect](https://github.com/aiclientproxy/connect) +2. **创建配置文件** - 在 `providers/` 目录下创建 `{your-id}.json` +3. **提交 PR** - 提交 Pull Request 到主仓库 +4. **自动化检查** - GitHub Actions 自动验证配置 +5. **社区审核** - 维护者审核并合并 PR +6. **自动发布** - 合并后自动构建 registry.json + +### 配置文件格式 + +```json +{ + "id": "myrelay", + "name": "我的中转站", + "description": "稳定、便宜、快速的 AI API 中转服务", + + "branding": { + "logo": "https://myrelay.com/logo.png", + "color": "#6366f1" + }, + + "links": { + "homepage": "https://myrelay.com", + "register": "https://myrelay.com/register", + "recharge": "https://myrelay.com/recharge", + "docs": "https://docs.myrelay.com", + "status": "https://status.myrelay.com" + }, + + "api": { + "base_url": "https://api.myrelay.com/v1", + "protocol": "openai", + "auth_header": "Authorization", + "auth_prefix": "Bearer " + }, + + "contact": { + "email": "support@myrelay.com", + "telegram": "@myrelay" + }, + + "features": { + "streaming": true, + "models_endpoint": true + }, + + "webhook": { + "callback_url": "https://api.myrelay.com/proxycast/callback", + "secret": "whsec_xxxxxxxxxxxxxxxx" + } +} +``` + +### 字段说明 + +| 字段 | 必填 | 说明 | +|------|------|------| +| `id` | ✅ | 唯一标识,小写字母、数字、连字符 | +| `name` | ✅ | 显示名称 | +| `description` | ✅ | 简短描述,≤100 字 | +| `branding.logo` | ✅ | Logo URL,256x256 PNG | +| `branding.color` | ❌ | 主题色,默认 `#6366f1` | +| `links.homepage` | ✅ | 官网地址 | +| `api.base_url` | ✅ | API 地址(必须 HTTPS) | +| `api.protocol` | ✅ | 协议:`openai` 或 `anthropic` | +| `contact.email` | ✅ | 联系邮箱 | +| `webhook.callback_url` | ❌ | 统计回调地址 | +| `webhook.secret` | ❌ | 回调签名密钥 | + +### 审核标准 + +PR 合并前需满足: + +- JSON Schema 验证通过 +- 文件名与 `id` 字段一致 +- Logo 图片可访问(256x256 PNG) +- API 地址使用 HTTPS +- 官网可访问 +- 联系方式有效 + +## 品牌展示 + +注册成功后,中转商会在 ProxyCast 内获得品牌展示: + +- **扩展市场** - 在中转服务分类中展示 +- **已安装页面** - 显示品牌信息、快捷链接 +- **API Key 管理** - 统一管理该中转商的所有 Key + +## 安全设计 + +### Deep Link 安全 + +| 风险 | 防护措施 | +|------|---------| +| 恶意链接 | relay_id 必须在注册表中存在 | +| Key 泄露 | 确认弹窗显示脱敏 Key,用户确认后才添加 | +| 钓鱼攻击 | 显示中转商完整信息,用户可核实 | + +### 确认弹窗 + +所有通过 Deep Link 添加的 Key 必须经过用户确认,显示: +- 中转商名称和 Logo +- 脱敏后的 API Key +- Key 名称(如果有) +- 安全提示 + +## 下一步 + +- [Connect 接入指南](/open-platform/connect-integration) - 详细的接入步骤 +- [统计回调](/open-platform/connect-webhook) - 配置统计回调追踪推广效果 diff --git a/docs/content/08.open-platform/5.connect-integration.md b/docs/content/08.open-platform/5.connect-integration.md new file mode 100644 index 000000000..2a15f50c1 --- /dev/null +++ b/docs/content/08.open-platform/5.connect-integration.md @@ -0,0 +1,229 @@ +# Connect 接入指南 + +本文档详细介绍中转商如何接入 ProxyCast Connect,实现一键配置功能。 + +## 接入流程 + +### Step 1: Fork 仓库 + +Fork [aiclientproxy/connect](https://github.com/aiclientproxy/connect) 仓库到你的 GitHub 账号。 + +### Step 2: 创建配置文件 + +在 `providers/` 目录下创建 `{your-id}.json` 文件: + +```json +{ + "id": "myrelay", + "name": "我的中转站", + "description": "稳定、便宜、快速的 AI API 中转服务", + + "branding": { + "logo": "https://myrelay.com/logo.png", + "color": "#6366f1" + }, + + "links": { + "homepage": "https://myrelay.com", + "register": "https://myrelay.com/register", + "recharge": "https://myrelay.com/recharge", + "docs": "https://docs.myrelay.com", + "status": "https://status.myrelay.com" + }, + + "api": { + "base_url": "https://api.myrelay.com/v1", + "protocol": "openai", + "auth_header": "Authorization", + "auth_prefix": "Bearer " + }, + + "contact": { + "email": "support@myrelay.com", + "telegram": "@myrelay" + }, + + "features": { + "streaming": true, + "models_endpoint": true + }, + + "webhook": { + "callback_url": "https://api.myrelay.com/proxycast/callback", + "secret": "whsec_xxxxxxxxxxxxxxxx" + } +} +``` + +### Step 3: 提交 PR + +提交 Pull Request 到主仓库,填写 PR 模板说明你的中转服务。 + +### Step 4: 等待审核 + +GitHub Actions 会自动验证配置文件,维护者会在 1-3 个工作日内审核。 + +### Step 5: 合并上线 + +PR 合并后,registry.json 会自动构建,ProxyCast 客户端会自动同步。 + +## 配置字段说明 + +### 必填字段 + +| 字段 | 说明 | 示例 | +|------|------|------| +| `id` | 唯一标识,小写字母、数字、连字符 | `myrelay` | +| `name` | 显示名称 | `我的中转站` | +| `description` | 简短描述,≤100 字 | `稳定、便宜、快速的 AI API 中转服务` | +| `branding.logo` | Logo URL,256x256 PNG | `https://myrelay.com/logo.png` | +| `links.homepage` | 官网地址 | `https://myrelay.com` | +| `api.base_url` | API 地址(必须 HTTPS) | `https://api.myrelay.com/v1` | +| `api.protocol` | 协议类型 | `openai` 或 `anthropic` | +| `contact.email` | 联系邮箱 | `support@myrelay.com` | + +### 可选字段 + +| 字段 | 说明 | 默认值 | +|------|------|--------| +| `branding.color` | 主题色 | `#6366f1` | +| `links.register` | 注册页面 | - | +| `links.recharge` | 充值页面 | - | +| `links.docs` | 文档地址 | - | +| `links.status` | 状态页面 | - | +| `api.auth_header` | 认证头 | `Authorization` | +| `api.auth_prefix` | 认证前缀 | `Bearer ` | +| `contact.telegram` | Telegram 联系方式 | - | +| `contact.discord` | Discord 联系方式 | - | +| `features.streaming` | 是否支持流式响应 | `true` | +| `features.models_endpoint` | 是否提供 /models 端点 | `false` | +| `webhook.callback_url` | 统计回调地址 | - | +| `webhook.secret` | 回调签名密钥 | - | + +## 集成方式 + +### 方式一:直接链接 + +最简单的方式,在用户后台放置链接: + +```html + + 一键配置 ProxyCast + +``` + +### 方式二:JavaScript SDK + +提供更好的用户体验: + +```html + + + +``` + +SDK 功能: +- 自动检测 ProxyCast 是否安装 +- 未安装时显示下载引导 +- 支持回调函数 + +```javascript +ProxyCast.connect({ + relay: 'myrelay', + key: userApiKey, + name: '我的Key', + onSuccess: () => { + showToast('配置成功!'); + }, + onNotInstalled: () => { + showDownloadModal(); + } +}); +``` + +### 方式三:配置文件下载 + +生成 `.proxycast` 配置文件供用户下载: + +```javascript +function downloadConfig(apiKey) { + const config = { + relay: 'myrelay', + key: apiKey, + name: '我的Key' + }; + + const blob = new Blob([JSON.stringify(config)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = 'myrelay.proxycast'; + a.click(); +} +``` + +用户双击 `.proxycast` 文件,ProxyCast 自动打开并导入配置。 + +## Deep Link 参数 + +``` +proxycast://connect?relay={relay_id}&key={api_key}&name={key_name}&ref={ref_code} +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| `relay` | ✅ | 中转商 ID(需在 ProxyCast 注册) | +| `key` | ✅ | API Key | +| `name` | ❌ | Key 名称(默认使用中转商名称) | +| `ref` | ❌ | 推广码(用于统计) | + +### 示例 + +``` +# 基础用法 +proxycast://connect?relay=openrouter&key=sk-or-v1-xxxx + +# 带名称 +proxycast://connect?relay=siliconflow&key=sk-xxxx&name=硅基流动-主账号 + +# 带推广码 +proxycast://connect?relay=myrelay&key=sk-xxxx&ref=promo2024 +``` + +## 品牌素材要求 + +| 素材 | 规格 | 说明 | +|------|------|------| +| Logo | 256x256 PNG | 透明背景,正方形 | +| 主题色 | HEX 色值 | 用于 UI 强调色 | +| 简介 | ≤50 字 | 一句话描述 | +| 详细描述 | ≤200 字 | 详细介绍 | + +## API 要求 + +中转商的 API 需要满足: + +| 要求 | 说明 | +|------|------| +| 协议兼容 | OpenAI 或 Anthropic 协议 | +| HTTPS | 必须使用 HTTPS | +| 模型列表 | 提供 `/models` 端点(可选) | +| 稳定性 | 99% 以上可用性 | + +## 审核标准 + +PR 合并前需满足: + +- JSON Schema 验证通过 +- 文件名与 `id` 字段一致 +- Logo 图片可访问(256x256 PNG) +- API 地址使用 HTTPS +- 官网可访问 +- 联系方式有效 + +## 下一步 + +- [统计回调](/open-platform/connect-webhook) - 配置 Webhook 追踪推广效果 diff --git a/docs/content/08.open-platform/6.connect-webhook.md b/docs/content/08.open-platform/6.connect-webhook.md new file mode 100644 index 000000000..950b18c72 --- /dev/null +++ b/docs/content/08.open-platform/6.connect-webhook.md @@ -0,0 +1,198 @@ +# 统计回调(Webhook) + +ProxyCast Connect 提供统计回调机制,让中转商追踪推广效果。 + +## 回调流程 + +``` +用户点击一键配置 + │ + ▼ +ProxyCast 打开,显示确认弹窗 + │ + ├─── 用户取消 ───▶ 发送回调: status=cancelled + │ + └─── 用户确认 ───▶ Key 添加成功 + │ + ▼ + 发送回调: status=success + │ + ▼ + 中转商收到回调,更新统计 +``` + +## 配置回调 + +在 `providers/{id}.json` 中添加 webhook 配置: + +```json +{ + "id": "myrelay", + "name": "我的中转站", + + "webhook": { + "callback_url": "https://api.myrelay.com/proxycast/callback" + } +} +``` + +| 字段 | 必填 | 说明 | +|------|------|------| +| `webhook.callback_url` | ✅ | 回调地址(必须 HTTPS) | + +## 回调请求格式 + +ProxyCast 向中转商发送 POST 请求: + +```http +POST https://api.myrelay.com/proxycast/callback +Content-Type: application/json +User-Agent: ProxyCast/1.2.0 + +{ + "event": "connect", + "status": "success", + "relay_id": "myrelay", + "ref": "promo2024", + "key_prefix": "sk-xxxx", + "timestamp": "2026-01-05T12:00:00Z", + "client": { + "version": "1.2.0", + "platform": "macos" + } +} +``` + +## 回调字段说明 + +| 字段 | 说明 | +|------|------| +| `event` | 事件类型:`connect` | +| `status` | 状态:`success`(成功)、`cancelled`(用户取消)、`error`(失败) | +| `relay_id` | 中转商 ID | +| `ref` | 推广码(如果有) | +| `key_prefix` | Key 前缀(脱敏,仅前 7 位) | +| `timestamp` | 事件时间(ISO 8601 格式) | +| `client.version` | ProxyCast 版本 | +| `client.platform` | 平台:`macos`、`windows`、`linux` | +| `error_code` | 错误码(仅 status=error 时) | +| `error_message` | 错误信息(仅 status=error 时) | + +## 请求验证 + +由于 ProxyCast 是开源软件,不使用签名验证。中转商应通过以下方式验证请求: + +1. **检查 key_prefix** - 验证该 Key 前缀是否为自己下发的 Key +2. **检查 relay_id** - 确认是自己的中转商 ID +3. **检查 User-Agent** - 确认包含 `ProxyCast` + +### Express 示例 + +```javascript +app.post('/proxycast/callback', async (req, res) => { + const { relay_id, key_prefix, status, ref } = req.body; + const userAgent = req.headers['user-agent'] || ''; + + // 验证 User-Agent + if (!userAgent.includes('ProxyCast')) { + return res.status(403).json({ error: 'Invalid User-Agent' }); + } + + // 验证 relay_id + if (relay_id !== 'myrelay') { + return res.status(403).json({ error: 'Invalid relay_id' }); + } + + // 验证 key_prefix 是否为自己下发的 Key + const isValidKey = await db.apiKeys.exists({ + key: { $regex: `^${key_prefix}` } + }); + + if (!isValidKey) { + return res.status(403).json({ error: 'Unknown key_prefix' }); + } + + // 处理回调 + if (status === 'success') { + await db.stats.increment({ + relay_id, + ref: ref || 'direct', + date: new Date().toISOString().split('T')[0] + }); + } + + res.json({ received: true }); +}); +``` + +## 重试机制 + +| 重试次数 | 间隔 | 说明 | +|----------|------|------| +| 第 1 次 | 立即 | 首次发送 | +| 第 2 次 | 1 分钟 | 首次失败后 | +| 第 3 次 | 5 分钟 | 第二次失败后 | +| 第 4 次 | 30 分钟 | 第三次失败后 | +| 放弃 | - | 超过 4 次不再重试 | + +成功响应:HTTP 2xx 状态码 + + +## 统计数据示例 + +基于回调数据,中转商可以构建统计面板: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ProxyCast Connect 统计 2026-01-05 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 今日概览 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 128 │ │ 115 │ │ 13 │ │ 89.8% │ │ +│ │ 点击次数 │ │ 成功配置 │ │ 用户取消 │ │ 转化率 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ 推广码效果 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 推广码 点击 成功 取消 转化率 │ │ +│ ├─────────────────────────────────────────────────────┤ │ +│ │ promo2024 56 52 4 92.9% │ │ +│ │ twitter 38 33 5 86.8% │ │ +│ │ (无推广码) 34 30 4 88.2% │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 隐私保护 + +回调数据遵循最小化原则: + +| 数据 | 是否发送 | 说明 | +|------|----------|------| +| 完整 API Key | ❌ | 仅发送前 7 位前缀 | +| 用户 ID | ❌ | 不发送任何用户标识 | +| 设备 ID | ❌ | 不发送设备标识 | +| IP 地址 | ❌ | 不发送用户 IP | +| 推广码 | ✅ | 用于统计推广效果 | +| 平台信息 | ✅ | 仅操作系统类型 | + +## 错误码 + +当 `status=error` 时,会包含错误信息: + +| 错误码 | 说明 | +|--------|------| +| `invalid_relay` | 中转商 ID 无效 | +| `invalid_key` | API Key 格式无效 | +| `relay_not_found` | 中转商未注册 | +| `network_error` | 网络错误 | +| `internal_error` | 内部错误 | + +## 最佳实践 + +1. **验证 key_prefix** - 检查是否为自己下发的 Key +2. **幂等处理** - 同一事件可能重复发送 +3. **快速响应** - 在 5 秒内返回响应 +4. **异步处理** - 复杂逻辑放到后台队列 \ No newline at end of file diff --git a/docs/prd/connect-test.html b/docs/prd/connect-test.html new file mode 100644 index 000000000..d62df0627 --- /dev/null +++ b/docs/prd/connect-test.html @@ -0,0 +1,534 @@ + + + + + + ProxyCast Connect 测试页面 + + + +
+

🔗 ProxyCast Connect 测试

+

模拟中转商后台的一键配置功能 & Webhook 回调测试

+ +
+ + + +
+ + + + + +
+
+

Webhook 回调测试

+

模拟 ProxyCast 发送的统计回调请求

+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + +
+ + + +
+
请求日志:
+
+
+ [--:--:--] + INFO + 等待发送回调请求... +
+
+
+ +
+
💡 验证说明
+
+ 由于 ProxyCast 是开源软件,不使用签名验证。
+ 中转商应通过检查 key_prefix 是否为自己下发的 Key 来验证请求。 +
+
+
+ +
+

回调字段说明

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段说明
status + success + cancelled + error +
relay_id中转商 ID
key_prefixKey 前缀(脱敏,仅前 7 位)- 用于验证
ref推广码(可选)
client客户端信息 {version, platform}
+
+
+ + +
+
+

Deep Link 协议

+
+

协议格式

+
proxycast://connect?relay={relay_id}&key={api_key}&name={key_name}&ref={ref_code}
+
+ + + + + + + + + + + + + + + + + + + + + +
relay必填中转商 ID
key必填API Key
name可选Key 显示名称
ref可选推广码
+
+
💡 测试前提
+
+ 1. 确保 ProxyCast 应用已安装并运行
+ 2. 确保 Deep Link 协议已正确注册
+ 3. 如果点击无反应,检查浏览器是否阻止了协议跳转 +
+
+
+ + +
+
+ +
+ + + + + diff --git a/package.json b/package.json index a8dc0405f..86edf2448 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.29.0", + "version": "0.30.0", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8928e629f..11eb28058 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.29.0" +version = "0.30.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 7f023fd90..17f497fad 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file +{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"deep-link":{"default_permission":{"identifier":"default","description":"Allows reading the opened deep link via the get_current command","permissions":["allow-get-current"]},"permissions":{"allow-get-current":{"identifier":"allow-get-current","description":"Enables the get_current command without any pre-configured scope.","commands":{"allow":["get_current"],"deny":[]}},"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"deny-get-current":{"identifier":"deny-get-current","description":"Denies the get_current command without any pre-configured scope.","commands":{"allow":[],"deny":["get_current"]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/src/commands/connect_cmd.rs b/src-tauri/src/commands/connect_cmd.rs index cc66eb15c..e52c62d8d 100644 --- a/src-tauri/src/commands/connect_cmd.rs +++ b/src-tauri/src/commands/connect_cmd.rs @@ -66,9 +66,10 @@ impl From for ConnectError { fn from(err: DeepLinkError) -> Self { let (code, message) = match &err { DeepLinkError::InvalidUrl(msg) => ("INVALID_URL".to_string(), msg.clone()), - DeepLinkError::MissingRelay => { - ("MISSING_RELAY".to_string(), "缺少必填参数: relay".to_string()) - } + DeepLinkError::MissingRelay => ( + "MISSING_RELAY".to_string(), + "缺少必填参数: relay".to_string(), + ), DeepLinkError::MissingKey => { ("MISSING_KEY".to_string(), "缺少必填参数: key".to_string()) } @@ -203,10 +204,13 @@ pub async fn save_relay_api_key( })?; // 获取中转商信息以确定协议类型和 base_url - let relay_info = connect_state.registry.get(&relay_id).ok_or_else(|| ConnectError { - code: "RELAY_NOT_FOUND".to_string(), - message: format!("中转商 {} 不在注册表中", relay_id), - })?; + let relay_info = connect_state + .registry + .get(&relay_id) + .ok_or_else(|| ConnectError { + code: "RELAY_NOT_FOUND".to_string(), + message: format!("中转商 {} 不在注册表中", relay_id), + })?; let protocol = relay_info.api.protocol.to_lowercase(); let base_url = relay_info.api.base_url.clone(); @@ -235,7 +239,9 @@ pub async fn save_relay_api_key( (provider_id, false) } else { // 创建新的自定义 Provider - let provider_name = name.clone().unwrap_or_else(|| format!("[Connect] {}", relay_info.name)); + let provider_name = name + .clone() + .unwrap_or_else(|| format!("[Connect] {}", relay_info.name)); let new_provider = api_key_service .0 .add_custom_provider( @@ -390,10 +396,7 @@ pub async fn send_connect_callback( let callback_url = match webhook.callback_url { Some(url) => url, None => { - tracing::debug!( - "[Connect] 中转商 {} webhook 配置不完整,跳过回调", - relay_id - ); + tracing::debug!("[Connect] 中转商 {} webhook 配置不完整,跳过回调", relay_id); return Ok(false); } }; diff --git a/src-tauri/src/connect/README.md b/src-tauri/src/connect/README.md new file mode 100644 index 000000000..76f9e9e6f --- /dev/null +++ b/src-tauri/src/connect/README.md @@ -0,0 +1,51 @@ +# connect + + + +## 架构说明 + +ProxyCast Connect 模块,实现中转商生态合作方案。 +通过 Deep Link 协议实现一键配置功能,支持中转商品牌展示。 +API Key 直接集成到凭证池系统,无需单独存储。 + +## 功能概述 + +1. **Deep Link 处理** - 解析 `proxycast://connect` 协议 URL +2. **中转商注册表** - 从 GitHub 加载和管理中转商信息 +3. **统计回调** - 向中转商发送配置结果回调(Webhook) + +## 文件索引 + +- `mod.rs` - 模块入口,导出子模块 +- `deep_link.rs` - Deep Link URL 解析器 ✅ + - `ConnectPayload` - 解析结果结构体 + - `DeepLinkError` - 错误类型枚举 + - `parse_deep_link()` - URL 解析函数 +- `registry.rs` - 中转商注册表管理 ✅ + - `RelayRegistry` - 注册表管理器 + - `RelayInfo` - 中转商信息结构体 + - `RelayBranding` - 品牌信息 + - `RelayLinks` - 相关链接 + - `RelayApi` - API 配置 + - `RelayContact` - 联系方式 + - `RelayFeatures` - 功能特性 + - `RelayWebhook` - Webhook 配置 + - `RegistryError` - 错误类型 +- `webhook.rs` - 统计回调服务 ✅ + - `CallbackPayload` - 回调数据结构 + - `CallbackStatus` - 回调状态枚举(success/cancelled/error) + - `WebhookSender` - 回调发送器(支持重试) + - `send_success_callback()` - 发送成功回调 + - `send_cancelled_callback()` - 发送取消回调 + - `send_error_callback()` - 发送错误回调 + +## 相关需求 + +- Requirements 1.x - Deep Link 协议处理 +- Requirements 2.x - 中转商注册表管理 +- Requirements 4.x - API Key 存储(已集成到凭证池系统) +- Requirements 5.3 - 统计回调(Webhook) + +## 更新提醒 + +任何文件变更后,请更新此文档和相关的上级文档。 diff --git a/src-tauri/src/connect/deep_link.rs b/src-tauri/src/connect/deep_link.rs new file mode 100644 index 000000000..e662155a8 --- /dev/null +++ b/src-tauri/src/connect/deep_link.rs @@ -0,0 +1,373 @@ +//! Deep Link URL 解析模块 +//! +//! 负责解析 `proxycast://connect` 协议的 URL,提取中转商配置参数。 +//! +//! ## 功能 +//! +//! - 解析 Deep Link URL 并提取参数 +//! - 验证必填参数(relay, key) +//! - 返回结构化的 ConnectPayload 或错误 +//! +//! ## 使用示例 +//! +//! ```rust +//! use proxycast_lib::connect::deep_link::{parse_deep_link, ConnectPayload, DeepLinkError}; +//! +//! let url = "proxycast://connect?relay=example&key=sk-xxx&name=MyKey"; +//! match parse_deep_link(url) { +//! Ok(payload) => println!("Relay: {}, Key: {}", payload.relay, payload.key), +//! Err(e) => eprintln!("Error: {:?}", e), +//! } +//! ``` + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +/// Deep Link 解析结果 +/// +/// 包含从 `proxycast://connect` URL 中提取的所有参数。 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ConnectPayload { + /// 中转商 ID(必填) + pub relay: String, + /// API Key(必填) + pub key: String, + /// Key 名称(可选) + pub name: Option, + /// 推广码(可选) + pub ref_code: Option, +} + +/// Deep Link 解析错误 +/// +/// 表示解析 Deep Link URL 时可能发生的各种错误。 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum DeepLinkError { + /// URL 格式无效 + InvalidUrl(String), + /// 缺少必填的 relay 参数 + MissingRelay, + /// 缺少必填的 key 参数 + MissingKey, +} + +impl std::fmt::Display for DeepLinkError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeepLinkError::InvalidUrl(msg) => write!(f, "无效的 URL: {}", msg), + DeepLinkError::MissingRelay => write!(f, "缺少必填参数: relay"), + DeepLinkError::MissingKey => write!(f, "缺少必填参数: key"), + } + } +} + +impl std::error::Error for DeepLinkError {} + +/// 解析 Deep Link URL +/// +/// 解析 `proxycast://connect` 格式的 URL,提取 relay、key、name 和 ref 参数。 +/// +/// # 参数 +/// +/// * `url` - Deep Link URL 字符串 +/// +/// # 返回值 +/// +/// * `Ok(ConnectPayload)` - 解析成功,返回包含所有参数的结构体 +/// * `Err(DeepLinkError)` - 解析失败,返回具体错误类型 +/// +/// # 示例 +/// +/// ```rust +/// use proxycast_lib::connect::deep_link::parse_deep_link; +/// +/// // 完整 URL +/// let result = parse_deep_link("proxycast://connect?relay=example&key=sk-xxx&name=MyKey&ref=abc"); +/// assert!(result.is_ok()); +/// +/// // 缺少 relay 参数 +/// let result = parse_deep_link("proxycast://connect?key=sk-xxx"); +/// assert!(result.is_err()); +/// ``` +pub fn parse_deep_link(url: &str) -> Result { + // 解析 URL + let parsed = Url::parse(url).map_err(|e| DeepLinkError::InvalidUrl(e.to_string()))?; + + // 验证协议 + if parsed.scheme() != "proxycast" { + return Err(DeepLinkError::InvalidUrl(format!( + "无效的协议: {},期望 proxycast", + parsed.scheme() + ))); + } + + // 验证路径(host 在自定义协议中作为路径的一部分) + if parsed.host_str() != Some("connect") { + return Err(DeepLinkError::InvalidUrl(format!( + "无效的路径: {:?},期望 connect", + parsed.host_str() + ))); + } + + // 提取查询参数 + let params: HashMap = parsed.query_pairs().into_owned().collect(); + + // 验证并提取必填参数 + let relay = params + .get("relay") + .filter(|s| !s.is_empty()) + .ok_or(DeepLinkError::MissingRelay)? + .clone(); + + let key = params + .get("key") + .filter(|s| !s.is_empty()) + .ok_or(DeepLinkError::MissingKey)? + .clone(); + + // 提取可选参数 + let name = params.get("name").filter(|s| !s.is_empty()).cloned(); + let ref_code = params.get("ref").filter(|s| !s.is_empty()).cloned(); + + Ok(ConnectPayload { + relay, + key, + name, + ref_code, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_valid_url_with_all_params() { + let url = "proxycast://connect?relay=example&key=sk-xxx&name=MyKey&ref=abc123"; + let result = parse_deep_link(url).unwrap(); + + assert_eq!(result.relay, "example"); + assert_eq!(result.key, "sk-xxx"); + assert_eq!(result.name, Some("MyKey".to_string())); + assert_eq!(result.ref_code, Some("abc123".to_string())); + } + + #[test] + fn test_parse_valid_url_with_required_params_only() { + let url = "proxycast://connect?relay=test-relay&key=sk-12345"; + let result = parse_deep_link(url).unwrap(); + + assert_eq!(result.relay, "test-relay"); + assert_eq!(result.key, "sk-12345"); + assert_eq!(result.name, None); + assert_eq!(result.ref_code, None); + } + + #[test] + fn test_parse_missing_relay() { + let url = "proxycast://connect?key=sk-xxx"; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::MissingRelay))); + } + + #[test] + fn test_parse_missing_key() { + let url = "proxycast://connect?relay=example"; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::MissingKey))); + } + + #[test] + fn test_parse_empty_relay() { + let url = "proxycast://connect?relay=&key=sk-xxx"; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::MissingRelay))); + } + + #[test] + fn test_parse_empty_key() { + let url = "proxycast://connect?relay=example&key="; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::MissingKey))); + } + + #[test] + fn test_parse_invalid_protocol() { + let url = "http://connect?relay=example&key=sk-xxx"; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::InvalidUrl(_)))); + } + + #[test] + fn test_parse_invalid_path() { + let url = "proxycast://other?relay=example&key=sk-xxx"; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::InvalidUrl(_)))); + } + + #[test] + fn test_parse_malformed_url() { + let url = "not a valid url"; + let result = parse_deep_link(url); + + assert!(matches!(result, Err(DeepLinkError::InvalidUrl(_)))); + } + + #[test] + fn test_parse_url_encoded_params() { + let url = "proxycast://connect?relay=test%20relay&key=sk-xxx&name=My%20Key"; + let result = parse_deep_link(url).unwrap(); + + assert_eq!(result.relay, "test relay"); + assert_eq!(result.key, "sk-xxx"); + assert_eq!(result.name, Some("My Key".to_string())); + } +} + +#[cfg(test)] +mod property_tests { + use super::*; + use proptest::prelude::*; + + /// 生成有效的 relay ID(字母数字和连字符) + fn arb_relay_id() -> impl Strategy { + "[a-z][a-z0-9-]{0,30}[a-z0-9]".prop_filter("非空", |s| !s.is_empty()) + } + + /// 生成有效的 API Key + fn arb_api_key() -> impl Strategy { + "sk-[a-zA-Z0-9]{8,64}".prop_filter("非空", |s| !s.is_empty()) + } + + /// 生成可选的 Key 名称 + fn arb_key_name() -> impl Strategy> { + prop_oneof![Just(None), "[a-zA-Z0-9 _-]{1,50}".prop_map(Some),] + } + + /// 生成可选的推广码 + fn arb_ref_code() -> impl Strategy> { + prop_oneof![Just(None), "[a-zA-Z0-9]{4,20}".prop_map(Some),] + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + /// Feature: proxycast-connect, Property 1: Deep Link URL Parsing Completeness + /// Validates: Requirements 1.1 + /// + /// *For any* valid Deep Link URL containing relay and key parameters, + /// parsing the URL SHALL extract all parameters correctly and return + /// a ConnectPayload with matching values. + #[test] + fn prop_deep_link_parsing_completeness( + relay in arb_relay_id(), + key in arb_api_key(), + name in arb_key_name(), + ref_code in arb_ref_code(), + ) { + // 构建 URL + let mut url = format!("proxycast://connect?relay={}&key={}", relay, key); + if let Some(ref n) = name { + url.push_str(&format!("&name={}", urlencoding::encode(n))); + } + if let Some(ref r) = ref_code { + url.push_str(&format!("&ref={}", r)); + } + + // 解析 URL + let result = parse_deep_link(&url); + + // 验证解析成功 + prop_assert!(result.is_ok(), "解析失败: {:?}", result); + + let payload = result.unwrap(); + + // 验证所有参数正确提取 + prop_assert_eq!(&payload.relay, &relay, "relay 不匹配"); + prop_assert_eq!(&payload.key, &key, "key 不匹配"); + prop_assert_eq!(&payload.name, &name, "name 不匹配"); + prop_assert_eq!(&payload.ref_code, &ref_code, "ref_code 不匹配"); + } + + /// Feature: proxycast-connect, Property 2: Deep Link Invalid Parameter Handling + /// Validates: Requirements 1.2, 1.3, 7.1 + /// + /// *For any* Deep Link URL that is malformed, missing the relay parameter, + /// or missing the key parameter, the parser SHALL return an appropriate error + /// and not produce a valid ConnectPayload. + #[test] + fn prop_deep_link_invalid_parameter_handling( + relay in arb_relay_id(), + key in arb_api_key(), + error_type in 0..4u8, + ) { + let url = match error_type { + // 缺少 relay 参数 + 0 => format!("proxycast://connect?key={}", key), + // 缺少 key 参数 + 1 => format!("proxycast://connect?relay={}", relay), + // 空 relay 参数 + 2 => format!("proxycast://connect?relay=&key={}", key), + // 空 key 参数 + _ => format!("proxycast://connect?relay={}&key=", relay), + }; + + let result = parse_deep_link(&url); + + // 验证解析失败 + prop_assert!(result.is_err(), "应该返回错误,但解析成功了: {:?}", result); + + // 验证错误类型正确 + match error_type { + 0 | 2 => prop_assert!( + matches!(result, Err(DeepLinkError::MissingRelay)), + "期望 MissingRelay 错误,实际: {:?}", result + ), + 1 | 3 => prop_assert!( + matches!(result, Err(DeepLinkError::MissingKey)), + "期望 MissingKey 错误,实际: {:?}", result + ), + _ => unreachable!(), + } + } + + /// 测试无效协议 + #[test] + fn prop_invalid_protocol( + protocol in "(http|https|ftp|file)", + relay in arb_relay_id(), + key in arb_api_key(), + ) { + let url = format!("{}://connect?relay={}&key={}", protocol, relay, key); + let result = parse_deep_link(&url); + + prop_assert!( + matches!(result, Err(DeepLinkError::InvalidUrl(_))), + "期望 InvalidUrl 错误,实际: {:?}", result + ); + } + + /// 测试无效路径 + #[test] + fn prop_invalid_path( + path in "(other|invalid|test|api)", + relay in arb_relay_id(), + key in arb_api_key(), + ) { + let url = format!("proxycast://{}?relay={}&key={}", path, relay, key); + let result = parse_deep_link(&url); + + prop_assert!( + matches!(result, Err(DeepLinkError::InvalidUrl(_))), + "期望 InvalidUrl 错误,实际: {:?}", result + ); + } + } +} diff --git a/src-tauri/src/connect/mod.rs b/src-tauri/src/connect/mod.rs new file mode 100644 index 000000000..289e01772 --- /dev/null +++ b/src-tauri/src/connect/mod.rs @@ -0,0 +1,40 @@ +//! ProxyCast Connect 模块 +//! +//! 实现中转商生态合作方案,通过 Deep Link 协议实现一键配置功能。 +//! +//! ## 子模块 +//! +//! - `deep_link` - Deep Link URL 解析 +//! - `registry` - 中转商注册表管理 +//! - `webhook` - 统计回调服务 +//! +//! ## 使用示例 +//! +//! ```rust,ignore +//! use proxycast::connect::{parse_deep_link, RelayRegistry}; +//! +//! // 解析 Deep Link +//! let payload = parse_deep_link("proxycast://connect?relay=example&key=sk-xxx")?; +//! +//! // 查询中转商信息 +//! let registry = RelayRegistry::new(cache_path); +//! let relay_info = registry.get(&payload.relay); +//! +//! // API Key 直接保存到凭证池系统(通过 ProviderPoolService) +//! ``` + +// 子模块声明 +pub mod deep_link; +pub mod registry; +pub mod webhook; + +// 重新导出核心类型 +pub use deep_link::{parse_deep_link, ConnectPayload, DeepLinkError}; +pub use registry::{ + RegistryData, RegistryError, RelayApi, RelayBranding, RelayContact, RelayFeatures, RelayInfo, + RelayLinks, RelayRegistry, RelayWebhook, +}; +pub use webhook::{ + send_cancelled_callback, send_error_callback, send_success_callback, CallbackPayload, + CallbackStatus, WebhookError, WebhookSender, +}; diff --git a/src-tauri/src/connect/registry.rs b/src-tauri/src/connect/registry.rs new file mode 100644 index 000000000..927d94130 --- /dev/null +++ b/src-tauri/src/connect/registry.rs @@ -0,0 +1,795 @@ +//! 中转商注册表管理模块 +//! +//! 负责从 GitHub 加载和管理中转商注册表,提供中转商信息查询功能。 +//! +//! ## 功能 +//! +//! - 从远程 GitHub 仓库加载注册表 +//! - 本地缓存支持离线访问 +//! - 中转商信息查询和验证 +//! +//! ## 使用示例 +//! +//! ```rust,ignore +//! use proxycast_lib::connect::registry::{RelayRegistry, RelayInfo}; +//! +//! let registry = RelayRegistry::new(cache_path); +//! registry.load_from_remote().await?; +//! +//! if let Some(info) = registry.get("example-relay") { +//! println!("中转商: {}", info.name); +//! } +//! ``` + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::RwLock; +use thiserror::Error; + +/// 注册表远程 URL +const REGISTRY_URL: &str = + "https://raw.githubusercontent.com/aiclientproxy/connect/main/dist/registry.json"; + +/// 注册表错误类型 +#[derive(Debug, Error)] +pub enum RegistryError { + /// 网络请求失败 + #[error("网络请求失败: {0}")] + NetworkError(String), + + /// JSON 解析失败 + #[error("JSON 解析失败: {0}")] + ParseError(String), + + /// 文件 IO 错误 + #[error("文件操作失败: {0}")] + IoError(#[from] std::io::Error), + + /// 缓存不存在 + #[error("缓存不存在")] + NoCacheError, +} + +/// 注册表数据结构(JSON 根对象) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegistryData { + /// 注册表版本 + pub version: String, + /// 更新时间 + pub updated_at: String, + /// 中转商列表 + pub providers: Vec, +} + +/// 中转商信息 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RelayInfo { + /// 中转商唯一 ID + pub id: String, + /// 中转商名称 + pub name: String, + /// 中转商描述 + pub description: String, + /// 品牌信息 + pub branding: RelayBranding, + /// 相关链接 + pub links: RelayLinks, + /// API 配置 + pub api: RelayApi, + /// 联系方式 + pub contact: RelayContact, + /// 功能特性(可选) + #[serde(default)] + pub features: RelayFeatures, + /// Webhook 配置(可选) + #[serde(default)] + pub webhook: Option, +} + +/// 品牌信息 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RelayBranding { + /// Logo URL + pub logo: String, + /// 主题色(默认 #6366f1) + #[serde(default = "default_color")] + pub color: String, +} + +fn default_color() -> String { + "#6366f1".to_string() +} + +/// 相关链接 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RelayLinks { + /// 主页 + pub homepage: String, + /// 注册链接(可选) + #[serde(default)] + pub register: Option, + /// 充值链接(可选) + #[serde(default)] + pub recharge: Option, + /// 文档链接(可选) + #[serde(default)] + pub docs: Option, + /// 状态页链接(可选) + #[serde(default)] + pub status: Option, +} + +/// API 配置 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RelayApi { + /// API 基础 URL + pub base_url: String, + /// 协议类型(如 openai, claude) + pub protocol: String, + /// 认证头名称(默认 Authorization) + #[serde(default = "default_auth_header")] + pub auth_header: String, + /// 认证前缀(默认 Bearer) + #[serde(default = "default_auth_prefix")] + pub auth_prefix: String, +} + +fn default_auth_header() -> String { + "Authorization".to_string() +} + +fn default_auth_prefix() -> String { + "Bearer".to_string() +} + +/// 联系方式 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RelayContact { + /// 邮箱 + #[serde(default)] + pub email: Option, + /// Discord + #[serde(default)] + pub discord: Option, + /// Telegram + #[serde(default)] + pub telegram: Option, + /// Twitter + #[serde(default)] + pub twitter: Option, +} + +/// 功能特性 +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct RelayFeatures { + /// 支持的模型列表 + #[serde(default)] + pub models: Vec, + /// 是否支持流式响应 + #[serde(default)] + pub streaming: bool, + /// 是否支持函数调用 + #[serde(default)] + pub function_calling: bool, + /// 是否支持视觉模型 + #[serde(default)] + pub vision: bool, +} + +/// Webhook 配置 +/// +/// 用于统计回调,让中转商追踪推广效果。 +/// 中转商通过检查 key_prefix 是否为自己下发的 Key 来验证请求。 +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct RelayWebhook { + /// 回调地址(必须 HTTPS) + #[serde(default)] + pub callback_url: Option, +} + +/// 中转商注册表管理器 +pub struct RelayRegistry { + /// 中转商数据(ID -> RelayInfo) + providers: RwLock>, + /// 缓存文件路径 + cache_path: PathBuf, +} + +impl RelayRegistry { + /// 创建新的注册表实例 + /// + /// # 参数 + /// + /// * `cache_path` - 缓存文件路径 + pub fn new(cache_path: PathBuf) -> Self { + Self { + providers: RwLock::new(HashMap::new()), + cache_path, + } + } + + /// 从远程 GitHub 加载注册表 + /// + /// # 返回值 + /// + /// * `Ok(())` - 加载成功 + /// * `Err(RegistryError)` - 加载失败 + pub async fn load_from_remote(&self) -> Result<(), RegistryError> { + tracing::info!("从远程加载中转商注册表: {}", REGISTRY_URL); + + let response = reqwest::get(REGISTRY_URL) + .await + .map_err(|e| RegistryError::NetworkError(e.to_string()))?; + + if !response.status().is_success() { + return Err(RegistryError::NetworkError(format!( + "HTTP 状态码: {}", + response.status() + ))); + } + + let text = response + .text() + .await + .map_err(|e| RegistryError::NetworkError(e.to_string()))?; + + let registry_data: RegistryData = + serde_json::from_str(&text).map_err(|e| RegistryError::ParseError(e.to_string()))?; + + // 更新内存中的数据 + let mut providers = self + .providers + .write() + .map_err(|_| RegistryError::ParseError("获取写锁失败".to_string()))?; + + providers.clear(); + for provider in registry_data.providers { + providers.insert(provider.id.clone(), provider); + } + + tracing::info!("成功加载 {} 个中转商", providers.len()); + + // 保存到缓存 + drop(providers); // 释放写锁 + self.save_to_cache()?; + + Ok(()) + } + + /// 从本地缓存加载注册表 + /// + /// # 返回值 + /// + /// * `Ok(())` - 加载成功 + /// * `Err(RegistryError)` - 加载失败 + pub fn load_from_cache(&self) -> Result<(), RegistryError> { + if !self.cache_path.exists() { + return Err(RegistryError::NoCacheError); + } + + tracing::info!("从缓存加载中转商注册表: {:?}", self.cache_path); + + let content = std::fs::read_to_string(&self.cache_path)?; + + let registry_data: RegistryData = + serde_json::from_str(&content).map_err(|e| RegistryError::ParseError(e.to_string()))?; + + let mut providers = self + .providers + .write() + .map_err(|_| RegistryError::ParseError("获取写锁失败".to_string()))?; + + providers.clear(); + for provider in registry_data.providers { + providers.insert(provider.id.clone(), provider); + } + + tracing::info!("从缓存加载 {} 个中转商", providers.len()); + + Ok(()) + } + + /// 保存注册表到本地缓存 + /// + /// # 返回值 + /// + /// * `Ok(())` - 保存成功 + /// * `Err(RegistryError)` - 保存失败 + pub fn save_to_cache(&self) -> Result<(), RegistryError> { + let providers = self + .providers + .read() + .map_err(|_| RegistryError::ParseError("获取读锁失败".to_string()))?; + + let registry_data = RegistryData { + version: "1.0.0".to_string(), + updated_at: chrono::Utc::now().to_rfc3339(), + providers: providers.values().cloned().collect(), + }; + + // 确保父目录存在 + if let Some(parent) = self.cache_path.parent() { + std::fs::create_dir_all(parent)?; + } + + let content = serde_json::to_string_pretty(®istry_data) + .map_err(|e| RegistryError::ParseError(e.to_string()))?; + + std::fs::write(&self.cache_path, content)?; + + tracing::info!("注册表已缓存到: {:?}", self.cache_path); + + Ok(()) + } + + /// 查询中转商信息 + /// + /// # 参数 + /// + /// * `id` - 中转商 ID + /// + /// # 返回值 + /// + /// * `Some(RelayInfo)` - 找到对应的中转商 + /// * `None` - 未找到 + pub fn get(&self, id: &str) -> Option { + self.providers + .read() + .ok() + .and_then(|providers| providers.get(id).cloned()) + } + + /// 验证中转商是否存在于注册表中 + /// + /// # 参数 + /// + /// * `id` - 中转商 ID + /// + /// # 返回值 + /// + /// * `true` - 存在 + /// * `false` - 不存在 + pub fn is_valid(&self, id: &str) -> bool { + self.providers + .read() + .ok() + .map(|providers| providers.contains_key(id)) + .unwrap_or(false) + } + + /// 获取所有中转商列表 + /// + /// # 返回值 + /// + /// 所有中转商信息的列表 + pub fn list(&self) -> Vec { + self.providers + .read() + .ok() + .map(|providers| providers.values().cloned().collect()) + .unwrap_or_default() + } + + /// 获取中转商数量 + pub fn len(&self) -> usize { + self.providers + .read() + .ok() + .map(|providers| providers.len()) + .unwrap_or(0) + } + + /// 检查注册表是否为空 + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// 直接从 RegistryData 加载(用于测试) + #[cfg(test)] + pub fn load_from_data(&self, data: RegistryData) { + if let Ok(mut providers) = self.providers.write() { + providers.clear(); + for provider in data.providers { + providers.insert(provider.id.clone(), provider); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + /// 创建测试用的 RelayInfo + fn create_test_relay_info(id: &str, name: &str) -> RelayInfo { + RelayInfo { + id: id.to_string(), + name: name.to_string(), + description: format!("{} 描述", name), + branding: RelayBranding { + logo: format!("https://example.com/{}/logo.png", id), + color: "#6366f1".to_string(), + }, + links: RelayLinks { + homepage: format!("https://{}.example.com", id), + register: Some(format!("https://{}.example.com/register", id)), + recharge: None, + docs: Some(format!("https://docs.{}.example.com", id)), + status: None, + }, + api: RelayApi { + base_url: format!("https://api.{}.example.com/v1", id), + protocol: "openai".to_string(), + auth_header: "Authorization".to_string(), + auth_prefix: "Bearer".to_string(), + }, + contact: RelayContact { + email: Some(format!("support@{}.example.com", id)), + discord: None, + telegram: None, + twitter: None, + }, + features: RelayFeatures::default(), + webhook: None, + } + } + + /// 创建测试用的 RegistryData + fn create_test_registry_data(providers: Vec) -> RegistryData { + RegistryData { + version: "1.0.0".to_string(), + updated_at: "2026-01-05T00:00:00Z".to_string(), + providers, + } + } + + #[test] + fn test_registry_new() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path.clone()); + + assert!(registry.is_empty()); + assert_eq!(registry.len(), 0); + } + + #[test] + fn test_registry_load_from_data() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + let relay1 = create_test_relay_info("relay-1", "中转站 1"); + let relay2 = create_test_relay_info("relay-2", "中转站 2"); + let data = create_test_registry_data(vec![relay1.clone(), relay2.clone()]); + + registry.load_from_data(data); + + assert_eq!(registry.len(), 2); + assert!(!registry.is_empty()); + } + + #[test] + fn test_registry_get_existing() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + let relay = create_test_relay_info("test-relay", "测试中转站"); + let data = create_test_registry_data(vec![relay.clone()]); + registry.load_from_data(data); + + let result = registry.get("test-relay"); + assert!(result.is_some()); + assert_eq!(result.unwrap().name, "测试中转站"); + } + + #[test] + fn test_registry_get_non_existing() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + let relay = create_test_relay_info("test-relay", "测试中转站"); + let data = create_test_registry_data(vec![relay]); + registry.load_from_data(data); + + let result = registry.get("non-existing"); + assert!(result.is_none()); + } + + #[test] + fn test_registry_is_valid() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + let relay = create_test_relay_info("valid-relay", "有效中转站"); + let data = create_test_registry_data(vec![relay]); + registry.load_from_data(data); + + assert!(registry.is_valid("valid-relay")); + assert!(!registry.is_valid("invalid-relay")); + } + + #[test] + fn test_registry_list() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + let relay1 = create_test_relay_info("relay-1", "中转站 1"); + let relay2 = create_test_relay_info("relay-2", "中转站 2"); + let data = create_test_registry_data(vec![relay1, relay2]); + registry.load_from_data(data); + + let list = registry.list(); + assert_eq!(list.len(), 2); + } + + #[test] + fn test_registry_cache_round_trip() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + + // 创建并保存 + let registry1 = RelayRegistry::new(cache_path.clone()); + let relay = create_test_relay_info("cached-relay", "缓存中转站"); + let data = create_test_registry_data(vec![relay.clone()]); + registry1.load_from_data(data); + registry1.save_to_cache().unwrap(); + + // 从缓存加载 + let registry2 = RelayRegistry::new(cache_path); + registry2.load_from_cache().unwrap(); + + assert_eq!(registry2.len(), 1); + let loaded = registry2.get("cached-relay").unwrap(); + assert_eq!(loaded.name, relay.name); + assert_eq!(loaded.description, relay.description); + } + + #[test] + fn test_registry_load_from_cache_no_file() { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("non_existing.json"); + let registry = RelayRegistry::new(cache_path); + + let result = registry.load_from_cache(); + assert!(matches!(result, Err(RegistryError::NoCacheError))); + } + + #[test] + fn test_relay_info_serialization() { + let relay = create_test_relay_info("test", "测试"); + let json = serde_json::to_string(&relay).unwrap(); + let deserialized: RelayInfo = serde_json::from_str(&json).unwrap(); + + assert_eq!(relay, deserialized); + } + + #[test] + fn test_relay_branding_default_color() { + let json = r#"{"logo": "https://example.com/logo.png"}"#; + let branding: RelayBranding = serde_json::from_str(json).unwrap(); + + assert_eq!(branding.color, "#6366f1"); + } + + #[test] + fn test_relay_api_default_auth() { + let json = r#"{"base_url": "https://api.example.com", "protocol": "openai"}"#; + let api: RelayApi = serde_json::from_str(json).unwrap(); + + assert_eq!(api.auth_header, "Authorization"); + assert_eq!(api.auth_prefix, "Bearer"); + } +} + +#[cfg(test)] +mod property_tests { + use super::*; + use proptest::prelude::*; + use tempfile::TempDir; + + /// 生成有效的中转商 ID + fn arb_relay_id() -> impl Strategy { + "[a-z][a-z0-9-]{0,20}[a-z0-9]" + .prop_filter("非空且有效", |s| !s.is_empty() && s.len() >= 2) + } + + /// 生成有效的中转商名称 + fn arb_relay_name() -> impl Strategy { + "[a-zA-Z\u{4e00}-\u{9fff}]{1,20}".prop_filter("非空", |s| !s.is_empty()) + } + + /// 生成测试用的 RelayInfo + fn arb_relay_info() -> impl Strategy { + (arb_relay_id(), arb_relay_name()).prop_map(|(id, name)| RelayInfo { + id: id.clone(), + name: name.clone(), + description: format!("{} 描述", name), + branding: RelayBranding { + logo: format!("https://example.com/{}/logo.png", id), + color: "#6366f1".to_string(), + }, + links: RelayLinks { + homepage: format!("https://{}.example.com", id), + register: None, + recharge: None, + docs: None, + status: None, + }, + api: RelayApi { + base_url: format!("https://api.{}.example.com/v1", id), + protocol: "openai".to_string(), + auth_header: "Authorization".to_string(), + auth_prefix: "Bearer".to_string(), + }, + contact: RelayContact { + email: None, + discord: None, + telegram: None, + twitter: None, + }, + features: RelayFeatures::default(), + webhook: None, + }) + } + + /// 生成多个不重复 ID 的 RelayInfo 列表 + fn arb_relay_info_list(max_size: usize) -> impl Strategy> { + prop::collection::vec(arb_relay_info(), 0..=max_size).prop_map(|relays| { + // 去重:保留每个 ID 的第一个出现 + let mut seen = std::collections::HashSet::new(); + relays + .into_iter() + .filter(|r| seen.insert(r.id.clone())) + .collect() + }) + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + /// Feature: proxycast-connect, Property 3: Registry Lookup Consistency + /// Validates: Requirements 2.3, 2.4 + /// + /// *For any* RelayRegistry and relay ID, if the ID exists in the registry + /// then `get(id)` SHALL return the corresponding RelayInfo, and if the ID + /// does not exist then `get(id)` SHALL return None. + #[test] + fn prop_registry_lookup_consistency( + relays in arb_relay_info_list(10), + query_id in arb_relay_id(), + ) { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + // 加载测试数据 + let data = RegistryData { + version: "1.0.0".to_string(), + updated_at: "2026-01-05T00:00:00Z".to_string(), + providers: relays.clone(), + }; + registry.load_from_data(data); + + // 查找是否存在于原始列表中 + let expected = relays.iter().find(|r| r.id == query_id); + + // 执行查询 + let result = registry.get(&query_id); + + // 验证一致性 + match (expected, result) { + (Some(expected_info), Some(result_info)) => { + // ID 存在时,返回的信息应该匹配 + prop_assert_eq!( + &result_info.id, &expected_info.id, + "ID 不匹配" + ); + prop_assert_eq!( + &result_info.name, &expected_info.name, + "名称不匹配" + ); + prop_assert_eq!( + &result_info.description, &expected_info.description, + "描述不匹配" + ); + } + (None, None) => { + // ID 不存在时,返回 None + } + (Some(_), None) => { + prop_assert!(false, "期望找到 ID {} 但返回 None", query_id); + } + (None, Some(_)) => { + prop_assert!(false, "ID {} 不应该存在但返回了结果", query_id); + } + } + + // 验证 is_valid 与 get 的一致性 + let is_valid = registry.is_valid(&query_id); + let get_result = registry.get(&query_id); + prop_assert_eq!( + is_valid, get_result.is_some(), + "is_valid 与 get 结果不一致" + ); + } + + /// 测试缓存往返一致性 + #[test] + fn prop_cache_round_trip(relays in arb_relay_info_list(5)) { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + + // 创建并保存 + let registry1 = RelayRegistry::new(cache_path.clone()); + let data = RegistryData { + version: "1.0.0".to_string(), + updated_at: "2026-01-05T00:00:00Z".to_string(), + providers: relays.clone(), + }; + registry1.load_from_data(data); + registry1.save_to_cache().unwrap(); + + // 从缓存加载 + let registry2 = RelayRegistry::new(cache_path); + registry2.load_from_cache().unwrap(); + + // 验证数量一致 + prop_assert_eq!( + registry1.len(), registry2.len(), + "缓存往返后数量不一致" + ); + + // 验证每个 relay 都能正确加载 + for relay in &relays { + let loaded = registry2.get(&relay.id); + prop_assert!( + loaded.is_some(), + "缓存往返后找不到 ID: {}", relay.id + ); + let loaded = loaded.unwrap(); + prop_assert_eq!( + &loaded.name, &relay.name, + "缓存往返后名称不匹配" + ); + } + } + + /// 测试 list 返回所有已加载的中转商 + #[test] + fn prop_list_returns_all(relays in arb_relay_info_list(10)) { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("registry.json"); + let registry = RelayRegistry::new(cache_path); + + let data = RegistryData { + version: "1.0.0".to_string(), + updated_at: "2026-01-05T00:00:00Z".to_string(), + providers: relays.clone(), + }; + registry.load_from_data(data); + + let list = registry.list(); + + // 验证数量 + prop_assert_eq!( + list.len(), relays.len(), + "list 返回数量不正确" + ); + + // 验证每个 relay 都在列表中 + for relay in &relays { + prop_assert!( + list.iter().any(|r| r.id == relay.id), + "list 中缺少 ID: {}", relay.id + ); + } + } + } +} diff --git a/src-tauri/src/connect/webhook.rs b/src-tauri/src/connect/webhook.rs new file mode 100644 index 000000000..a24700439 --- /dev/null +++ b/src-tauri/src/connect/webhook.rs @@ -0,0 +1,418 @@ +//! Webhook 回调模块 +//! +//! 实现统计回调功能,让中转商追踪推广效果。 +//! +//! ## 功能 +//! +//! - 在用户确认/取消配置后发送回调 +//! - 支持重试机制 +//! - 隐私保护(仅发送脱敏数据) +//! +//! ## 回调事件 +//! +//! - `success` - 配置成功 +//! - `cancelled` - 用户取消 +//! - `error` - 配置失败 +//! +//! ## 安全说明 +//! +//! 由于 ProxyCast 是开源软件,不使用签名验证。 +//! 中转商应通过检查 `key_prefix` 是否为自己下发的 Key 来验证请求。 +//! +//! _Requirements: 5.3_ + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use thiserror::Error; + +/// Webhook 错误类型 +#[derive(Debug, Error)] +pub enum WebhookError { + /// 网络请求失败 + #[error("网络请求失败: {0}")] + NetworkError(String), + + /// 回调地址无效 + #[error("回调地址无效: {0}")] + InvalidUrl(String), + + /// 重试次数耗尽 + #[error("重试次数耗尽")] + RetryExhausted, +} + +/// 回调状态 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum CallbackStatus { + /// 配置成功 + Success, + /// 用户取消 + Cancelled, + /// 配置失败 + Error, +} + +impl std::fmt::Display for CallbackStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CallbackStatus::Success => write!(f, "success"), + CallbackStatus::Cancelled => write!(f, "cancelled"), + CallbackStatus::Error => write!(f, "error"), + } + } +} + +/// 客户端信息 +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ClientInfo { + /// ProxyCast 版本 + pub version: String, + /// 平台(macos, windows, linux) + pub platform: String, +} + +impl Default for ClientInfo { + fn default() -> Self { + Self { + version: env!("CARGO_PKG_VERSION").to_string(), + platform: get_platform(), + } + } +} + +/// 获取当前平台 +fn get_platform() -> String { + #[cfg(target_os = "macos")] + return "macos".to_string(); + #[cfg(target_os = "windows")] + return "windows".to_string(); + #[cfg(target_os = "linux")] + return "linux".to_string(); + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + return "unknown".to_string(); +} + +/// 回调 Payload +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CallbackPayload { + /// 事件类型 + pub event: String, + /// 状态 + pub status: CallbackStatus, + /// 中转商 ID + pub relay_id: String, + /// 推广码(可选) + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_code: Option, + /// Key 前缀(脱敏,仅前 7 位) + pub key_prefix: String, + /// 事件时间 + pub timestamp: DateTime, + /// 客户端信息 + pub client: ClientInfo, + /// 错误码(仅 status=error 时) + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + /// 错误信息(仅 status=error 时) + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, +} + +impl CallbackPayload { + /// 创建成功回调 + pub fn success(relay_id: &str, key: &str, ref_code: Option) -> Self { + Self { + event: "connect".to_string(), + status: CallbackStatus::Success, + relay_id: relay_id.to_string(), + ref_code, + key_prefix: mask_key(key), + timestamp: Utc::now(), + client: ClientInfo::default(), + error_code: None, + error_message: None, + } + } + + /// 创建取消回调 + pub fn cancelled(relay_id: &str, key: &str, ref_code: Option) -> Self { + Self { + event: "connect".to_string(), + status: CallbackStatus::Cancelled, + relay_id: relay_id.to_string(), + ref_code, + key_prefix: mask_key(key), + timestamp: Utc::now(), + client: ClientInfo::default(), + error_code: None, + error_message: None, + } + } + + /// 创建错误回调 + pub fn error( + relay_id: &str, + key: &str, + ref_code: Option, + error_code: &str, + error_message: &str, + ) -> Self { + Self { + event: "connect".to_string(), + status: CallbackStatus::Error, + relay_id: relay_id.to_string(), + ref_code, + key_prefix: mask_key(key), + timestamp: Utc::now(), + client: ClientInfo::default(), + error_code: Some(error_code.to_string()), + error_message: Some(error_message.to_string()), + } + } +} + +/// 脱敏 API Key(仅保留前 7 位) +fn mask_key(key: &str) -> String { + if key.len() <= 7 { + key.to_string() + } else { + key[..7].to_string() + } +} + +/// Webhook 发送器 +pub struct WebhookSender { + /// HTTP 客户端 + client: reqwest::Client, + /// 最大重试次数 + max_retries: u32, + /// 重试间隔(秒) + retry_intervals: Vec, +} + +impl Default for WebhookSender { + fn default() -> Self { + Self::new() + } +} + +impl WebhookSender { + /// 创建新的 WebhookSender + pub fn new() -> Self { + Self { + client: reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap_or_default(), + max_retries: 4, + retry_intervals: vec![0, 60, 300, 1800], // 立即, 1分钟, 5分钟, 30分钟 + } + } + + /// 发送回调(带重试) + pub async fn send( + &self, + callback_url: &str, + payload: &CallbackPayload, + ) -> Result<(), WebhookError> { + // 验证 URL + if !callback_url.starts_with("https://") { + return Err(WebhookError::InvalidUrl( + "回调地址必须使用 HTTPS".to_string(), + )); + } + + let payload_json = serde_json::to_string(payload) + .map_err(|e| WebhookError::NetworkError(e.to_string()))?; + + // 重试循环 + for attempt in 0..self.max_retries { + // 等待重试间隔 + if attempt > 0 { + let interval = self.retry_intervals.get(attempt as usize).unwrap_or(&1800); + tracing::info!("[Webhook] 第 {} 次重试,等待 {} 秒", attempt, interval); + tokio::time::sleep(Duration::from_secs(*interval)).await; + } + + match self.send_once(callback_url, &payload_json).await { + Ok(_) => { + tracing::info!( + "[Webhook] 回调发送成功: relay={}, status={}", + payload.relay_id, + payload.status + ); + return Ok(()); + } + Err(e) => { + tracing::warn!( + "[Webhook] 回调发送失败 (尝试 {}/{}): {}", + attempt + 1, + self.max_retries, + e + ); + if attempt == self.max_retries - 1 { + return Err(WebhookError::RetryExhausted); + } + } + } + } + + Err(WebhookError::RetryExhausted) + } + + /// 发送单次请求 + async fn send_once(&self, url: &str, payload: &str) -> Result<(), WebhookError> { + let response = self + .client + .post(url) + .header("Content-Type", "application/json") + .header( + "User-Agent", + format!("ProxyCast/{}", env!("CARGO_PKG_VERSION")), + ) + .body(payload.to_string()) + .send() + .await + .map_err(|e| WebhookError::NetworkError(e.to_string()))?; + + if response.status().is_success() { + Ok(()) + } else { + Err(WebhookError::NetworkError(format!( + "HTTP 状态码: {}", + response.status() + ))) + } + } + + /// 异步发送回调(不阻塞,在后台执行) + pub fn send_async(&self, callback_url: String, payload: CallbackPayload) { + let client = self.client.clone(); + let max_retries = self.max_retries; + let retry_intervals = self.retry_intervals.clone(); + + tokio::spawn(async move { + let sender = WebhookSender { + client, + max_retries, + retry_intervals, + }; + + if let Err(e) = sender.send(&callback_url, &payload).await { + tracing::error!( + "[Webhook] 回调最终失败: relay={}, error={}", + payload.relay_id, + e + ); + } + }); + } +} + +/// 全局 WebhookSender 实例 +static WEBHOOK_SENDER: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// 获取全局 WebhookSender +pub fn get_webhook_sender() -> &'static WebhookSender { + WEBHOOK_SENDER.get_or_init(WebhookSender::new) +} + +/// 发送成功回调 +pub fn send_success_callback( + callback_url: &str, + relay_id: &str, + key: &str, + ref_code: Option, +) { + let payload = CallbackPayload::success(relay_id, key, ref_code); + get_webhook_sender().send_async(callback_url.to_string(), payload); +} + +/// 发送取消回调 +pub fn send_cancelled_callback( + callback_url: &str, + relay_id: &str, + key: &str, + ref_code: Option, +) { + let payload = CallbackPayload::cancelled(relay_id, key, ref_code); + get_webhook_sender().send_async(callback_url.to_string(), payload); +} + +/// 发送错误回调 +pub fn send_error_callback( + callback_url: &str, + relay_id: &str, + key: &str, + ref_code: Option, + error_code: &str, + error_message: &str, +) { + let payload = CallbackPayload::error(relay_id, key, ref_code, error_code, error_message); + get_webhook_sender().send_async(callback_url.to_string(), payload); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mask_key_short() { + assert_eq!(mask_key("sk-abc"), "sk-abc"); + } + + #[test] + fn test_mask_key_long() { + assert_eq!(mask_key("sk-1234567890abcdef"), "sk-1234"); + } + + #[test] + fn test_callback_payload_success() { + let payload = + CallbackPayload::success("test-relay", "sk-test123456", Some("promo".to_string())); + + assert_eq!(payload.event, "connect"); + assert_eq!(payload.status, CallbackStatus::Success); + assert_eq!(payload.relay_id, "test-relay"); + assert_eq!(payload.key_prefix, "sk-test"); + assert_eq!(payload.ref_code, Some("promo".to_string())); + assert!(payload.error_code.is_none()); + } + + #[test] + fn test_callback_payload_cancelled() { + let payload = CallbackPayload::cancelled("test-relay", "sk-test123456", None); + + assert_eq!(payload.status, CallbackStatus::Cancelled); + assert!(payload.ref_code.is_none()); + } + + #[test] + fn test_callback_payload_error() { + let payload = CallbackPayload::error( + "test-relay", + "sk-test123456", + None, + "INVALID_KEY", + "Key 验证失败", + ); + + assert_eq!(payload.status, CallbackStatus::Error); + assert_eq!(payload.error_code, Some("INVALID_KEY".to_string())); + assert_eq!(payload.error_message, Some("Key 验证失败".to_string())); + } + + #[test] + fn test_callback_payload_serialization() { + let payload = CallbackPayload::success("test", "sk-123456789", None); + let json = serde_json::to_string(&payload).unwrap(); + + assert!(json.contains(r#""event":"connect""#)); + assert!(json.contains(r#""status":"success""#)); + assert!(json.contains(r#""relay_id":"test""#)); + // ref_code 为 None 时不应该出现在 JSON 中 + assert!(!json.contains("ref_code")); + } +} diff --git a/src-tauri/src/server/handlers/credentials_api.rs b/src-tauri/src/server/handlers/credentials_api.rs index 1c690dbb4..6d51903aa 100644 --- a/src-tauri/src/server/handlers/credentials_api.rs +++ b/src-tauri/src/server/handlers/credentials_api.rs @@ -144,7 +144,10 @@ pub async fn credentials_select( // 没有找到可用凭证 Err(CredentialApiError { error: "no_available_credentials".to_string(), - message: format!("没有可用的 {} 凭证。您可以在 API Key Provider 中配置 API Key 作为降级选项。", request.provider_type), + message: format!( + "没有可用的 {} 凭证。您可以在 API Key Provider 中配置 API Key 作为降级选项。", + request.provider_type + ), status_code: 503, }) } diff --git a/src-tauri/src/server/handlers/kiro_credential.rs b/src-tauri/src/server/handlers/kiro_credential.rs index d5c409a80..ca9fe6b0b 100644 --- a/src-tauri/src/server/handlers/kiro_credential.rs +++ b/src-tauri/src/server/handlers/kiro_credential.rs @@ -259,7 +259,8 @@ pub async fn select_credential( })? .ok_or_else(|| ApiError { error: "no_available_credentials".to_string(), - message: "没有可用的 Kiro 凭证。Kiro 仅支持 OAuth 认证,无法降级到 API Key。".to_string(), + message: "没有可用的 Kiro 凭证。Kiro 仅支持 OAuth 认证,无法降级到 API Key。" + .to_string(), status_code: 503, })? }; diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs index 042756a6c..868dc3b0d 100644 --- a/src-tauri/src/server/mod.rs +++ b/src-tauri/src/server/mod.rs @@ -1302,17 +1302,13 @@ async fn anthropic_messages_with_selector( Some(cred) } // 最后尝试按 provider 类型轮询(带智能降级) - else if let Ok(Some(cred)) = - state - .pool_service - .select_credential_with_fallback( - db, - &state.api_key_service, - &selector, - Some(&request.model), - None, // provider_id_hint - ) - { + else if let Ok(Some(cred)) = state.pool_service.select_credential_with_fallback( + db, + &state.api_key_service, + &selector, + Some(&request.model), + None, // provider_id_hint + ) { Some(cred) } else { None @@ -1382,17 +1378,13 @@ async fn chat_completions_with_selector( Some(cred) } else if let Ok(Some(cred)) = state.pool_service.get_by_uuid(db, &selector) { Some(cred) - } else if let Ok(Some(cred)) = - state - .pool_service - .select_credential_with_fallback( - db, - &state.api_key_service, - &selector, - Some(&request.model), - None, // provider_id_hint - ) - { + } else if let Ok(Some(cred)) = state.pool_service.select_credential_with_fallback( + db, + &state.api_key_service, + &selector, + Some(&request.model), + None, // provider_id_hint + ) { Some(cred) } else { None @@ -1478,17 +1470,13 @@ async fn amp_chat_completions( let credential = match &state.db { Some(db) => { // 首先尝试按 provider 类型选择(带智能降级) - if let Ok(Some(cred)) = - state - .pool_service - .select_credential_with_fallback( - db, - &state.api_key_service, - &provider, - Some(&request.model), - Some(&provider), // provider_id_hint 使用路由中的 provider 名称 - ) - { + if let Ok(Some(cred)) = state.pool_service.select_credential_with_fallback( + db, + &state.api_key_service, + &provider, + Some(&request.model), + Some(&provider), // provider_id_hint 使用路由中的 provider 名称 + ) { Some(cred) } // 然后尝试按名称查找 @@ -1580,17 +1568,13 @@ async fn amp_messages( let credential = match &state.db { Some(db) => { // 首先尝试按 provider 类型选择(带智能降级) - if let Ok(Some(cred)) = - state - .pool_service - .select_credential_with_fallback( - db, - &state.api_key_service, - &provider, - Some(&request.model), - Some(&provider), // provider_id_hint 使用路由中的 provider 名称 - ) - { + if let Ok(Some(cred)) = state.pool_service.select_credential_with_fallback( + db, + &state.api_key_service, + &provider, + Some(&request.model), + Some(&provider), // provider_id_hint 使用路由中的 provider 名称 + ) { Some(cred) } // 然后尝试按名称查找 diff --git a/src-tauri/src/services/api_key_provider_service.rs b/src-tauri/src/services/api_key_provider_service.rs index 612244918..e18c19c04 100644 --- a/src-tauri/src/services/api_key_provider_service.rs +++ b/src-tauri/src/services/api_key_provider_service.rs @@ -698,11 +698,7 @@ impl ApiKeyProviderService { ) -> Result, String> { // 策略 1: 通过类型映射查找 if let Some(api_type) = self.map_pool_type_to_api_type(pool_type) { - tracing::debug!( - "[智能降级] 尝试类型映射: {:?} -> {:?}", - pool_type, - api_type - ); + tracing::debug!("[智能降级] 尝试类型映射: {:?} -> {:?}", pool_type, api_type); if let Some(cred) = self.find_by_api_type(db, pool_type, &api_type)? { return Ok(Some(cred)); } @@ -710,10 +706,7 @@ impl ApiKeyProviderService { // 策略 2: 通过 provider_id 直接查找 (支持 60+ Provider) if let Some(provider_id) = provider_id_hint { - tracing::debug!( - "[智能降级] 尝试 provider_id 查找: {}", - provider_id - ); + tracing::debug!("[智能降级] 尝试 provider_id 查找: {}", provider_id); if let Some(cred) = self.find_by_provider_id(db, provider_id)? { return Ok(Some(cred)); } @@ -730,10 +723,7 @@ impl ApiKeyProviderService { /// PoolProviderType → ApiProviderType 映射 /// /// 仅映射有明确对应关系的类型 - fn map_pool_type_to_api_type( - &self, - pool_type: &PoolProviderType, - ) -> Option { + fn map_pool_type_to_api_type(&self, pool_type: &PoolProviderType) -> Option { match pool_type { // API Key 类型 - 直接映射 PoolProviderType::Claude => Some(ApiProviderType::Anthropic), @@ -743,7 +733,7 @@ impl ApiKeyProviderService { // OAuth 类型 - 可降级到 API Key PoolProviderType::Gemini => Some(ApiProviderType::Gemini), // Gemini OAuth → Gemini API Key - PoolProviderType::Qwen => Some(ApiProviderType::Openai), // Qwen OAuth → Dashscope (OpenAI 兼容) + PoolProviderType::Qwen => Some(ApiProviderType::Openai), // Qwen OAuth → Dashscope (OpenAI 兼容) // API Key Provider 类型 - 直接映射 PoolProviderType::Anthropic => Some(ApiProviderType::Anthropic), @@ -770,8 +760,7 @@ impl ApiKeyProviderService { let conn = db.lock().map_err(|e| e.to_string())?; // 查找该类型的启用的 Provider(按 sort_order 排序) - let providers = - ApiKeyProviderDao::get_all_providers(&conn).map_err(|e| e.to_string())?; + let providers = ApiKeyProviderDao::get_all_providers(&conn).map_err(|e| e.to_string())?; let matching_providers: Vec<_> = providers .into_iter() @@ -838,8 +827,8 @@ impl ApiKeyProviderService { let conn = db.lock().map_err(|e| e.to_string())?; // 直接按 provider_id 查找 - let provider = ApiKeyProviderDao::get_provider_by_id(&conn, provider_id) - .map_err(|e| e.to_string())?; + let provider = + ApiKeyProviderDao::get_provider_by_id(&conn, provider_id).map_err(|e| e.to_string())?; let provider = match provider { Some(p) if p.enabled => p, @@ -870,11 +859,8 @@ impl ApiKeyProviderService { // 转换为 OpenAI 兼容的 ProviderCredential // 大多数 60+ Provider 都使用 OpenAI 兼容协议 - let credential = self.convert_to_openai_compatible_credential( - &provider, - &selected_key.id, - &api_key, - )?; + let credential = + self.convert_to_openai_compatible_credential(&provider, &selected_key.id, &api_key)?; tracing::info!( "[智能降级] 成功通过 provider_id 找到凭证: {} (key: {})", diff --git a/src-tauri/src/services/provider_pool_service.rs b/src-tauri/src/services/provider_pool_service.rs index 1ade7b91c..4e4fe2dfd 100644 --- a/src-tauri/src/services/provider_pool_service.rs +++ b/src-tauri/src/services/provider_pool_service.rs @@ -6,7 +6,6 @@ use crate::database::dao::provider_pool::ProviderPoolDao; use crate::database::DbConnection; -use crate::services::api_key_provider_service::ApiKeyProviderService; use crate::models::provider_pool_model::{ get_default_check_model, get_oauth_creds_path, CredentialData, CredentialDisplay, HealthCheckResult, OAuthStatus, PoolProviderType, PoolStats, ProviderCredential, @@ -15,6 +14,7 @@ use crate::models::provider_pool_model::{ use crate::models::route_model::RouteInfo; use crate::providers::antigravity::TokenRefreshError; use crate::providers::kiro::KiroProvider; +use crate::services::api_key_provider_service::ApiKeyProviderService; use chrono::Utc; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -351,9 +351,7 @@ impl ProviderPoolService { } // Step 2: 智能降级到 API Key Provider - let pt: PoolProviderType = provider_type - .parse() - .unwrap_or(PoolProviderType::OpenAI); + let pt: PoolProviderType = provider_type.parse().unwrap_or(PoolProviderType::OpenAI); // 传入 provider_id_hint 支持 60+ Provider if let Some(cred) = api_key_service.get_fallback_credential(db, &pt, provider_id_hint)? { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 372ad6751..d40db0704 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.29.0", + "version": "0.30.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/connect/ConnectConfirmDialog.test.tsx b/src/components/connect/ConnectConfirmDialog.test.tsx new file mode 100644 index 000000000..480c14e3b --- /dev/null +++ b/src/components/connect/ConnectConfirmDialog.test.tsx @@ -0,0 +1,196 @@ +/** + * @file Connect 确认弹窗属性测试 + * @description 测试 Provider Display 的完整性 + * @module components/connect/ConnectConfirmDialog.test + * + * **Feature: proxycast-connect, Property 7: Provider Display Completeness** + * **Validates: Requirements 6.1, 6.3** + */ + +import { describe, expect } from "vitest"; +import { test } from "@fast-check/vitest"; +import * as fc from "fast-check"; +import type { RelayInfo } from "@/hooks/useDeepLink"; + +/** + * 生成有效的十六进制颜色值 + */ +const hexColorArbitrary = fc + .array(fc.integer({ min: 0, max: 15 }), { minLength: 6, maxLength: 6 }) + .map((arr) => "#" + arr.map((n) => n.toString(16)).join("")); + +/** + * 生成有效的 RelayInfo 对象的 Arbitrary + */ +const relayInfoArbitrary: fc.Arbitrary = fc.record({ + id: fc + .string({ minLength: 1, maxLength: 50 }) + .filter((s) => s.trim().length > 0), + name: fc + .string({ minLength: 1, maxLength: 100 }) + .filter((s) => s.trim().length > 0), + description: fc + .string({ minLength: 1, maxLength: 500 }) + .filter((s) => s.trim().length > 0), + branding: fc.record({ + logo: fc.webUrl(), + color: hexColorArbitrary, + }), + links: fc.record({ + homepage: fc.webUrl(), + register: fc.option(fc.webUrl(), { nil: undefined }), + recharge: fc.option(fc.webUrl(), { nil: undefined }), + docs: fc.option(fc.webUrl(), { nil: undefined }), + status: fc.option(fc.webUrl(), { nil: undefined }), + }), + api: fc.record({ + base_url: fc.webUrl(), + protocol: fc.constantFrom("openai", "claude", "gemini"), + auth_header: fc.constant("Authorization"), + auth_prefix: fc.constant("Bearer"), + }), + contact: fc.record({ + email: fc.option(fc.emailAddress(), { nil: undefined }), + discord: fc.option(fc.string(), { nil: undefined }), + telegram: fc.option(fc.string(), { nil: undefined }), + twitter: fc.option(fc.string(), { nil: undefined }), + }), + features: fc.record({ + models: fc.array(fc.string({ minLength: 1 }), { + minLength: 0, + maxLength: 10, + }), + streaming: fc.boolean(), + function_calling: fc.boolean(), + vision: fc.boolean(), + }), +}); + +/** + * 提取 Provider 显示所需的核心信息 + * + * 这个函数模拟 ProviderInfo 组件的数据提取逻辑, + * 用于验证所有必要信息都能被正确提取和显示。 + * + * @param relay - 中转商信息 + * @returns 显示所需的核心字段 + */ +function extractProviderDisplayInfo(relay: RelayInfo): { + name: string; + logoUrl: string; + description: string; + homepageUrl: string | undefined; + themeColor: string; +} { + return { + name: relay.name, + logoUrl: relay.branding.logo, + description: relay.description, + homepageUrl: relay.links.homepage, + themeColor: relay.branding.color, + }; +} + +/** + * 验证 Provider 显示信息的完整性 + * + * @param displayInfo - 提取的显示信息 + * @param originalRelay - 原始 RelayInfo + * @returns 是否完整 + */ +function validateProviderDisplayCompleteness( + displayInfo: ReturnType, + originalRelay: RelayInfo, +): boolean { + // 验证名称存在且与原始数据匹配 + if (!displayInfo.name || displayInfo.name !== originalRelay.name) { + return false; + } + + // 验证 Logo URL 存在且与原始数据匹配 + if ( + !displayInfo.logoUrl || + displayInfo.logoUrl !== originalRelay.branding.logo + ) { + return false; + } + + // 验证描述存在且与原始数据匹配 + if ( + !displayInfo.description || + displayInfo.description !== originalRelay.description + ) { + return false; + } + + return true; +} + +describe("Provider Display 属性测试", () => { + /** + * Property 7: Provider Display Completeness + * + * *对于任意* RelayInfo 对象,渲染的 provider 显示应包含: + * - provider 的名称 + * - logo URL + * - 描述 + * + * **Feature: proxycast-connect, Property 7: Provider Display Completeness** + * **Validates: Requirements 6.1, 6.3** + */ + describe("Property 7: Provider Display Completeness", () => { + test.prop([relayInfoArbitrary], { numRuns: 100 })( + "对于任意 RelayInfo,提取的显示信息应包含 name、logo URL 和 description", + (relay: RelayInfo) => { + // 提取显示信息 + const displayInfo = extractProviderDisplayInfo(relay); + + // 验证完整性 + const isComplete = validateProviderDisplayCompleteness( + displayInfo, + relay, + ); + expect(isComplete).toBe(true); + + // 额外验证:确保所有必要字段都存在且非空 + expect(displayInfo.name).toBeTruthy(); + expect(displayInfo.name.length).toBeGreaterThan(0); + + expect(displayInfo.logoUrl).toBeTruthy(); + expect(displayInfo.logoUrl.length).toBeGreaterThan(0); + + expect(displayInfo.description).toBeTruthy(); + expect(displayInfo.description.length).toBeGreaterThan(0); + }, + ); + + test.prop([relayInfoArbitrary], { numRuns: 100 })( + "提取的显示信息应与原始 RelayInfo 数据完全匹配", + (relay: RelayInfo) => { + const displayInfo = extractProviderDisplayInfo(relay); + + // 验证数据一致性 + expect(displayInfo.name).toBe(relay.name); + expect(displayInfo.logoUrl).toBe(relay.branding.logo); + expect(displayInfo.description).toBe(relay.description); + expect(displayInfo.homepageUrl).toBe(relay.links.homepage); + expect(displayInfo.themeColor).toBe(relay.branding.color); + }, + ); + + test.prop([relayInfoArbitrary], { numRuns: 100 })( + "显示信息中的 URL 字段应为有效的 URL 格式", + (relay: RelayInfo) => { + const displayInfo = extractProviderDisplayInfo(relay); + + // 验证 Logo URL 格式 + expect(() => new URL(displayInfo.logoUrl)).not.toThrow(); + + // 验证主页 URL 格式(如果存在) + if (displayInfo.homepageUrl) { + expect(() => new URL(displayInfo.homepageUrl!)).not.toThrow(); + } + }, + ); + }); +}); diff --git a/src/components/connect/ConnectConfirmDialog.tsx b/src/components/connect/ConnectConfirmDialog.tsx new file mode 100644 index 000000000..7758a0679 --- /dev/null +++ b/src/components/connect/ConnectConfirmDialog.tsx @@ -0,0 +1,346 @@ +/** + * @file Connect 确认弹窗组件 + * @description 显示中转商信息和脱敏 API Key,让用户确认添加 + * @module components/connect/ConnectConfirmDialog + * + * _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 6.1, 6.2, 6.4_ + */ + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { maskApiKey } from "@/lib/utils/apiKeyMask"; +import type { RelayInfo, ConnectError } from "@/hooks/useDeepLink"; + +/** + * ConnectConfirmDialog 组件属性 + */ +export interface ConnectConfirmDialogProps { + /** 弹窗是否打开 */ + open: boolean; + /** 中转商信息(如果在注册表中找到) */ + relay: RelayInfo | null; + /** 中转商 ID(用于未验证警告显示) */ + relayId: string; + /** API Key */ + apiKey: string; + /** Key 名称(可选) */ + keyName?: string; + /** 是否为已验证的中转商 */ + isVerified: boolean; + /** 是否正在保存 */ + isSaving: boolean; + /** 错误信息 */ + error: ConnectError | null; + /** 确认回调 */ + onConfirm: () => void; + /** 取消回调 */ + onCancel: () => void; +} + +/** + * 中转商信息展示组件(已验证) + * _Requirements: 6.1, 6.2_ + */ +function VerifiedProviderInfo({ relay }: { relay: RelayInfo }) { + return ( +
+ {/* Logo */} +
+ {relay.branding.logo ? ( + {relay.name} { + e.currentTarget.style.display = "none"; + e.currentTarget.parentElement!.innerHTML = `${relay.name.charAt(0).toUpperCase()}`; + }} + /> + ) : ( + + {relay.name.charAt(0).toUpperCase()} + + )} +
+ + {/* 信息 */} +
+
+

{relay.name}

+ + ✓ 已验证 + +
+

+ {relay.description} +

+ {relay.links.homepage && ( + + 访问官网 + + + + + )} +
+
+ ); +} + +/** + * 未验证中转商信息展示组件 + * _Requirements: 3.5, 6.4_ + */ +function UnverifiedProviderInfo({ relayId }: { relayId: string }) { + return ( +
+
+ {/* 警告图标 */} +
+ + + +
+ + {/* 信息 */} +
+
+

{relayId}

+ + 未验证 + +
+

+ 此中转商不在官方注册表中,请确认您信任此来源后再添加。 +

+
+
+
+ ); +} + +/** + * API Key 信息展示组件 + * _Requirements: 3.2_ + */ +function KeyInfo({ + maskedKey, + keyName, +}: { + maskedKey: string; + keyName?: string; +}) { + return ( +
+
+
+ + + + API Key +
+ + {maskedKey} + +
+ {keyName && ( +
+
+ + + + 名称 +
+ {keyName} +
+ )} +
+ ); +} + +/** + * Connect 确认弹窗组件 + * + * 显示中转商信息和脱敏 API Key,让用户确认是否添加。 + * + * @param props - 组件属性 + */ +export function ConnectConfirmDialog({ + open, + relay, + relayId, + apiKey, + keyName, + isVerified, + isSaving, + error, + onConfirm, + onCancel, +}: ConnectConfirmDialogProps) { + const maskedKey = maskApiKey(apiKey); + + return ( + !isOpen && onCancel()}> + + + + + + + 添加 API Key + + + 确认添加以下中转商的 API Key 到 ProxyCast + + + +
+ {/* 中转商信息 */} + {isVerified && relay ? ( + + ) : ( + + )} + + {/* API Key 信息 */} + + + {/* 错误提示 */} + {error && ( +
+ + + +

{error.message}

+
+ )} +
+ + + + + +
+
+ ); +} + +export default ConnectConfirmDialog; diff --git a/src/components/connect/ConnectErrorToast.tsx b/src/components/connect/ConnectErrorToast.tsx new file mode 100644 index 000000000..a29f82d19 --- /dev/null +++ b/src/components/connect/ConnectErrorToast.tsx @@ -0,0 +1,99 @@ +/** + * @file Connect 错误提示 Alert 组件 + * @description 提供 ProxyCast Connect 功能的错误提示 Alert 组件 + * @module components/connect/ConnectErrorToast + * + * _Requirements: 7.1, 7.2, 7.3, 7.4_ + */ + +import { Button } from "@/components/ui/button"; +import { + type ConnectErrorType, + getErrorConfig, +} from "@/lib/utils/connectError"; + +/** + * ConnectErrorAlert 组件属性 + */ +export interface ConnectErrorAlertProps { + /** 错误类型 */ + type: ConnectErrorType; + /** 错误消息 */ + message: string; + /** 重试回调 */ + onRetry?: () => void; + /** 关闭回调 */ + onDismiss?: () => void; +} + +/** + * Connect 错误提示 Alert 组件 + * + * 用于在 UI 中内联显示错误信息,适用于需要持久显示的错误场景。 + * + * @param props - 组件属性 + */ +export function ConnectErrorAlert({ + type, + message, + onRetry, + onDismiss, +}: ConnectErrorAlertProps) { + const config = getErrorConfig(type, onRetry); + + return ( +
+
+ {/* 错误图标 */} + + + + +
+

{config.title}

+

+ {config.description(message)} +

+ + {/* 操作按钮 */} + {(onRetry || onDismiss) && ( +
+ {onRetry && ( + + )} + {onDismiss && ( + + )} +
+ )} +
+
+
+ ); +} + +export default ConnectErrorAlert; diff --git a/src/components/connect/README.md b/src/components/connect/README.md new file mode 100644 index 000000000..9157705cb --- /dev/null +++ b/src/components/connect/README.md @@ -0,0 +1,32 @@ +# connect + + + +## 架构说明 + +ProxyCast Connect 前端组件,实现中转商 API Key 添加确认流程。 +使用 shadcn/ui 组件库和 TailwindCSS 进行样式管理。 + +## 功能概述 + +1. **确认弹窗** - 显示中转商信息和脱敏 API Key +2. **品牌展示** - 展示中转商 Logo、名称、描述 +3. **警告提示** - 未验证中转商警告 +4. **错误提示** - Deep Link 解析、Registry 加载、API Key 存储错误提示 + +## 文件索引 + +- `index.ts` - 组件导出入口 +- `ConnectConfirmDialog.tsx` - 确认添加 API Key 弹窗 +- `ConnectConfirmDialog.test.tsx` - Provider Display 属性测试 (Property 7) +- `ConnectErrorToast.tsx` - 错误提示 Toast 组件和工具函数 + +## 相关需求 + +- Requirements 3.x - 连接确认弹窗 +- Requirements 6.x - 中转商品牌展示 +- Requirements 7.x - 错误处理 + +## 更新提醒 + +任何文件变更后,请更新此文档和相关的上级文档。 diff --git a/src/components/connect/index.ts b/src/components/connect/index.ts new file mode 100644 index 000000000..ae77f61c0 --- /dev/null +++ b/src/components/connect/index.ts @@ -0,0 +1,17 @@ +/** + * @file ProxyCast Connect 组件入口 + * @description 导出 Connect 功能相关的所有组件 + * @module components/connect + */ + +// 组件导出 +export { ConnectConfirmDialog } from "./ConnectConfirmDialog"; +export type { ConnectConfirmDialogProps } from "./ConnectConfirmDialog"; + +// 错误提示组件导出 +// _Requirements: 7.1, 7.2, 7.3, 7.4_ +export { ConnectErrorAlert } from "./ConnectErrorToast"; +export type { ConnectErrorAlertProps } from "./ConnectErrorToast"; + +// 错误提示工具函数从 lib/utils/connectError 导出 +// 使用方式: import { showDeepLinkError } from "@/lib/utils/connectError"; diff --git a/src/hooks/useConnectCallback.ts b/src/hooks/useConnectCallback.ts new file mode 100644 index 000000000..c9ac00fe2 --- /dev/null +++ b/src/hooks/useConnectCallback.ts @@ -0,0 +1,192 @@ +/** + * @file Connect 统计回调 Hook + * @description 提供统计回调功能,让中转商追踪推广效果 + * @module hooks/useConnectCallback + * + * _Requirements: 5.3_ + */ + +import { useCallback } from "react"; +import { invoke } from "@tauri-apps/api/core"; + +/** + * 回调状态类型 + */ +export type CallbackStatus = "success" | "cancelled" | "error"; + +/** + * 发送回调的参数 + */ +export interface SendCallbackParams { + /** 中转商 ID */ + relayId: string; + /** API Key */ + apiKey: string; + /** 回调状态 */ + status: CallbackStatus; + /** 推广码(可选) */ + refCode?: string; + /** 错误码(仅 status=error 时) */ + errorCode?: string; + /** 错误信息(仅 status=error 时) */ + errorMessage?: string; +} + +/** + * useConnectCallback Hook 返回值 + */ +export interface UseConnectCallbackReturn { + /** 发送成功回调 */ + sendSuccessCallback: ( + relayId: string, + apiKey: string, + refCode?: string, + ) => Promise; + /** 发送取消回调 */ + sendCancelledCallback: ( + relayId: string, + apiKey: string, + refCode?: string, + ) => Promise; + /** 发送错误回调 */ + sendErrorCallback: ( + relayId: string, + apiKey: string, + errorCode: string, + errorMessage: string, + refCode?: string, + ) => Promise; + /** 通用发送回调 */ + sendCallback: (params: SendCallbackParams) => Promise; +} + +/** + * Connect 统计回调 Hook + * + * 提供统计回调功能,在用户确认/取消配置后向中转商发送回调。 + * + * ## 功能 + * + * - 发送成功回调(用户确认添加 Key) + * - 发送取消回调(用户取消添加) + * - 发送错误回调(配置失败) + * - 异步发送,不阻塞 UI + * + * ## 使用示例 + * + * ```tsx + * function ConnectDialog() { + * const { sendSuccessCallback, sendCancelledCallback } = useConnectCallback(); + * + * const handleConfirm = async () => { + * // 保存 API Key... + * await sendSuccessCallback(relayId, apiKey, refCode); + * }; + * + * const handleCancel = async () => { + * await sendCancelledCallback(relayId, apiKey, refCode); + * }; + * } + * ``` + * + * @returns Hook 返回值 + */ +export function useConnectCallback(): UseConnectCallbackReturn { + /** + * 通用发送回调 + */ + const sendCallback = useCallback( + async (params: SendCallbackParams): Promise => { + try { + const result = await invoke("send_connect_callback", { + relayId: params.relayId, + apiKey: params.apiKey, + status: params.status, + refCode: params.refCode ?? null, + errorCode: params.errorCode ?? null, + errorMessage: params.errorMessage ?? null, + }); + + console.log( + `[useConnectCallback] 回调发送${result ? "成功" : "跳过"}: relay=${params.relayId}, status=${params.status}`, + ); + return result; + } catch (err) { + // 回调失败不应该影响主流程,只记录日志 + console.warn("[useConnectCallback] 发送回调失败:", err); + return false; + } + }, + [], + ); + + /** + * 发送成功回调 + */ + const sendSuccessCallback = useCallback( + async ( + relayId: string, + apiKey: string, + refCode?: string, + ): Promise => { + return sendCallback({ + relayId, + apiKey, + status: "success", + refCode, + }); + }, + [sendCallback], + ); + + /** + * 发送取消回调 + */ + const sendCancelledCallback = useCallback( + async ( + relayId: string, + apiKey: string, + refCode?: string, + ): Promise => { + return sendCallback({ + relayId, + apiKey, + status: "cancelled", + refCode, + }); + }, + [sendCallback], + ); + + /** + * 发送错误回调 + */ + const sendErrorCallback = useCallback( + async ( + relayId: string, + apiKey: string, + errorCode: string, + errorMessage: string, + refCode?: string, + ): Promise => { + return sendCallback({ + relayId, + apiKey, + status: "error", + refCode, + errorCode, + errorMessage, + }); + }, + [sendCallback], + ); + + return { + sendSuccessCallback, + sendCancelledCallback, + sendErrorCallback, + sendCallback, + }; +} + +export default useConnectCallback; diff --git a/src/hooks/useRelayRegistry.ts b/src/hooks/useRelayRegistry.ts new file mode 100644 index 000000000..b1893e03e --- /dev/null +++ b/src/hooks/useRelayRegistry.ts @@ -0,0 +1,158 @@ +/** + * @file Relay Registry 管理 Hook + * @description 管理中转商注册表的加载、刷新和状态 + * @module hooks/useRelayRegistry + * + * _Requirements: 2.1, 7.2, 7.3_ + */ + +import { useState, useEffect, useCallback } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import type { RelayInfo } from "./useDeepLink"; +import { + showRegistryLoadError, + showRegistryNoCacheError, +} from "@/lib/utils/connectError"; + +/** + * Registry 错误 + */ +export interface RegistryError { + code: string; + message: string; +} + +/** + * useRelayRegistry Hook 返回值 + */ +export interface UseRelayRegistryReturn { + /** 所有中转商列表 */ + providers: RelayInfo[]; + /** 是否正在加载 */ + isLoading: boolean; + /** 错误信息 */ + error: RegistryError | null; + /** 刷新注册表 */ + refresh: () => Promise; + /** 获取指定中转商信息 */ + getProvider: (relayId: string) => RelayInfo | undefined; +} + +/** + * Relay Registry 管理 Hook + * + * 管理中转商注册表的加载、刷新和状态。 + * + * ## 功能 + * + * - 应用启动时自动加载注册表(Requirements 2.1) + * - 加载失败时回退到缓存(Requirements 7.2) + * - 无缓存且加载失败时显示错误(Requirements 7.3) + * - 提供手动刷新功能 + * + * ## 使用示例 + * + * ```tsx + * function App() { + * const { providers, isLoading, error, refresh } = useRelayRegistry(); + * + * if (error) { + * return ; + * } + * + * return ; + * } + * ``` + * + * @returns Hook 返回值 + */ +export function useRelayRegistry(): UseRelayRegistryReturn { + const [providers, setProviders] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + /** + * 加载中转商列表 + * _Requirements: 2.1_ + */ + const loadProviders = useCallback(async () => { + try { + const list = await invoke("list_relay_providers"); + setProviders(list); + setError(null); + } catch (err) { + console.error("[useRelayRegistry] 加载中转商列表失败:", err); + // 不设置错误,因为可能是 Connect 模块还未初始化 + // 后端会自动处理缓存回退 + } + }, []); + + /** + * 刷新注册表 + * _Requirements: 2.5, 7.2, 7.3_ + */ + const refresh = useCallback(async () => { + setIsLoading(true); + setError(null); + + // 在调用前捕获当前 providers 长度,避免闭包问题 + const hasCache = providers.length > 0; + + try { + // 调用后端刷新注册表 + const count = await invoke("refresh_relay_registry"); + console.log(`[useRelayRegistry] 注册表已刷新,共 ${count} 个中转商`); + + // 重新加载列表 + await loadProviders(); + } catch (err) { + console.error("[useRelayRegistry] 刷新注册表失败:", err); + // _Requirements: 7.2, 7.3_ + const registryError = err as RegistryError; + setError(registryError); + + // 根据错误类型显示不同的 Toast + // 检查是否有缓存数据(providers 不为空表示有缓存) + if (hasCache) { + // _Requirements: 7.2_ - 有缓存,显示加载失败但已回退到缓存 + showRegistryLoadError(registryError.message); + } else { + // _Requirements: 7.3_ - 无缓存,显示错误并允许重试 + showRegistryNoCacheError(registryError.message); + } + } finally { + setIsLoading(false); + } + }, [loadProviders, providers.length]); + + /** + * 获取指定中转商信息 + */ + const getProvider = useCallback( + (relayId: string): RelayInfo | undefined => { + return providers.find((p) => p.id === relayId); + }, + [providers], + ); + + // 初始加载 + // _Requirements: 2.1_ + useEffect(() => { + // 延迟加载,等待 Connect 模块初始化 + const timer = setTimeout(() => { + loadProviders(); + }, 1000); + + return () => clearTimeout(timer); + }, [loadProviders]); + + return { + providers, + isLoading, + error, + refresh, + getProvider, + }; +} + +export default useRelayRegistry; diff --git a/src/lib/utils/README.md b/src/lib/utils/README.md new file mode 100644 index 000000000..426546411 --- /dev/null +++ b/src/lib/utils/README.md @@ -0,0 +1,37 @@ +# 工具函数库 + +本目录包含前端通用工具函数。 + +## 文件索引 + +| 文件 | 说明 | +|------|------| +| `apiKeyMask.ts` | API Key 脱敏工具,用于安全显示 API Key | +| `apiKeyMask.test.ts` | API Key 脱敏属性测试 | +| `apiKeyValidation.ts` | API Key 格式验证,各 Provider 的验证规则 | +| `apiKeyValidation.test.ts` | API Key 格式验证属性测试 | +| `connectError.ts` | Connect 错误处理工具函数,Toast 通知 | +| `syntaxHighlight.ts` | 代码语法高亮工具 | + +## 主要功能 + +### API Key 脱敏 (`apiKeyMask.ts`) + +对 API Key 进行脱敏处理,保护敏感信息: +- 长度 > 8:显示前 4 位 + "..." + 后 4 位 +- 长度 <= 8:返回 "****" + +### API Key 验证 (`apiKeyValidation.ts`) + +验证各 Provider 的 API Key 格式: +- 前缀验证(如 OpenAI 的 `sk-`) +- 长度验证 +- 字符验证 + +### Connect 错误处理 (`connectError.ts`) + +ProxyCast Connect 功能的错误提示 Toast 通知: +- `showDeepLinkError` - Deep Link 解析错误提示 +- `showRegistryLoadError` - Registry 加载失败提示(已回退到缓存) +- `showRegistryNoCacheError` - Registry 不可用错误(无缓存) +- `showApiKeySaveError` - API Key 保存失败提示 diff --git a/src/lib/utils/apiKeyMask.test.ts b/src/lib/utils/apiKeyMask.test.ts new file mode 100644 index 000000000..9eccb5ee5 --- /dev/null +++ b/src/lib/utils/apiKeyMask.test.ts @@ -0,0 +1,86 @@ +/** + * @file API Key 脱敏属性测试 + * @description 测试 API Key 脱敏功能的正确性 + * @module lib/utils/apiKeyMask.test + * + * **Feature: proxycast-connect, Property 4: API Key Masking Format** + * **Validates: Requirements 3.2** + */ + +import { describe, expect, it } from "vitest"; +import { test } from "@fast-check/vitest"; +import * as fc from "fast-check"; +import { maskApiKey } from "./apiKeyMask"; + +describe("API Key 脱敏", () => { + /** + * Property 4: API Key Masking Format + * + * *对于任意* API Key 字符串,脱敏后的版本应满足: + * - 如果原始 Key 长度 > 8,显示前 4 位 + "..." + 后 4 位 + * - 如果原始 Key 长度 <= 8,返回 "****" + * + * **Validates: Requirements 3.2** + */ + describe("Property 4: API Key Masking Format", () => { + test.prop([fc.string({ minLength: 9, maxLength: 200 })], { numRuns: 100 })( + "对于长度 > 8 的 Key,应显示前 4 位 + '...' + 后 4 位", + (apiKey: string) => { + const masked = maskApiKey(apiKey); + + // 验证格式:前4位 + ... + 后4位 + expect(masked).toMatch(/^.{4}\.\.\..{4}$/); + + // 验证前 4 位正确 + expect(masked.slice(0, 4)).toBe(apiKey.slice(0, 4)); + + // 验证后 4 位正确 + expect(masked.slice(-4)).toBe(apiKey.slice(-4)); + + // 验证中间是 "..." + expect(masked.slice(4, 7)).toBe("..."); + + // 验证总长度为 11(4 + 3 + 4) + expect(masked.length).toBe(11); + }, + ); + + test.prop([fc.string({ minLength: 0, maxLength: 8 })], { numRuns: 100 })( + "对于长度 <= 8 的 Key,应返回 '****'", + (apiKey: string) => { + const masked = maskApiKey(apiKey); + expect(masked).toBe("****"); + }, + ); + + // 边界测试:长度恰好为 8 和 9 + it("长度恰好为 8 的 Key 应返回 '****'", () => { + const key8 = "12345678"; + expect(maskApiKey(key8)).toBe("****"); + }); + + it("长度恰好为 9 的 Key 应显示前 4 后 4", () => { + const key9 = "123456789"; + const masked = maskApiKey(key9); + expect(masked).toBe("1234...6789"); + }); + + // 空字符串测试 + it("空字符串应返回 '****'", () => { + expect(maskApiKey("")).toBe("****"); + }); + + // 典型 API Key 格式测试 + it("典型 OpenAI Key 格式应正确脱敏", () => { + const key = "sk-1234567890abcdefghijklmnopqrstuvwxyz"; + const masked = maskApiKey(key); + expect(masked).toBe("sk-1...wxyz"); + }); + + it("典型 Anthropic Key 格式应正确脱敏", () => { + const key = "sk-ant-api03-abcdefghijklmnopqrstuvwxyz"; + const masked = maskApiKey(key); + expect(masked).toBe("sk-a...wxyz"); + }); + }); +}); diff --git a/src/lib/utils/apiKeyMask.ts b/src/lib/utils/apiKeyMask.ts new file mode 100644 index 000000000..fe1090434 --- /dev/null +++ b/src/lib/utils/apiKeyMask.ts @@ -0,0 +1,31 @@ +/** + * @file API Key 脱敏工具 + * @description 对 API Key 进行脱敏处理,保护敏感信息 + * @module lib/utils/apiKeyMask + * + * **Feature: proxycast-connect, Property 4: API Key Masking Format** + * **Validates: Requirements 3.2** + */ + +/** + * 对 API Key 进行脱敏处理 + * + * 规则: + * - 如果 Key 长度 > 8,显示前 4 位 + "..." + 后 4 位 + * - 如果 Key 长度 <= 8,返回 "****" + * + * @param apiKey - 原始 API Key + * @returns 脱敏后的字符串 + * + * @example + * maskApiKey("sk-1234567890abcdef") // "sk-1...cdef" + * maskApiKey("short") // "****" + * maskApiKey("12345678") // "****" + * maskApiKey("123456789") // "1234...6789" + */ +export function maskApiKey(apiKey: string): string { + if (apiKey.length <= 8) { + return "****"; + } + return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`; +} diff --git a/src/lib/utils/connectError.ts b/src/lib/utils/connectError.ts new file mode 100644 index 000000000..c6ba140b3 --- /dev/null +++ b/src/lib/utils/connectError.ts @@ -0,0 +1,194 @@ +/** + * @file Connect 错误处理工具函数 + * @description 提供 ProxyCast Connect 功能的错误提示 Toast 通知工具函数 + * @module lib/utils/connectError + * + * _Requirements: 7.1, 7.2, 7.3, 7.4_ + */ + +import { toast } from "sonner"; + +/** + * Connect 错误类型 + */ +export type ConnectErrorType = + | "deep_link_parse" + | "registry_load" + | "registry_no_cache" + | "api_key_save"; + +/** + * Connect 错误信息 + */ +export interface ConnectErrorInfo { + /** 错误类型 */ + type: ConnectErrorType; + /** 错误消息 */ + message: string; + /** 原始错误代码(可选) */ + code?: string; +} + +/** + * 错误配置 + */ +interface ErrorConfig { + title: string; + description: (message: string) => string; + action?: { + label: string; + onClick: () => void; + }; +} + +/** + * 获取错误配置 + */ +function getErrorConfig( + type: ConnectErrorType, + onRetry?: () => void, +): ErrorConfig { + switch (type) { + case "deep_link_parse": + // _Requirements: 7.1_ + return { + title: "链接解析失败", + description: (msg) => msg || "Deep Link 格式无效,请检查链接是否正确", + }; + + case "registry_load": + // _Requirements: 7.2_ + return { + title: "注册表加载失败", + description: (msg) => + msg || "无法从远程加载中转商注册表,已使用本地缓存", + }; + + case "registry_no_cache": + // _Requirements: 7.3_ + return { + title: "注册表不可用", + description: (msg) => + msg || "无法加载中转商注册表且没有本地缓存,请检查网络连接", + action: onRetry + ? { + label: "重试", + onClick: onRetry, + } + : undefined, + }; + + case "api_key_save": + // _Requirements: 7.4_ + return { + title: "保存失败", + description: (msg) => msg || "API Key 保存失败,请重试", + }; + + default: + return { + title: "操作失败", + description: (msg) => msg || "发生未知错误", + }; + } +} + +/** + * 显示 Connect 错误 Toast + * + * 根据错误类型显示对应的错误提示。 + * + * ## 错误类型 + * + * - `deep_link_parse`: Deep Link 解析错误(Requirements 7.1) + * - `registry_load`: Registry 加载失败,已回退到缓存(Requirements 7.2) + * - `registry_no_cache`: Registry 无缓存且加载失败(Requirements 7.3) + * - `api_key_save`: API Key 存储失败(Requirements 7.4) + * + * @param error - 错误信息 + * @param onRetry - 重试回调(仅用于 registry_no_cache 类型) + */ +export function showConnectError( + error: ConnectErrorInfo, + onRetry?: () => void, +): void { + const config = getErrorConfig(error.type, onRetry); + + // 使用 sonner 的 error toast + toast.error(config.title, { + description: config.description(error.message), + duration: error.type === "registry_no_cache" ? 10000 : 5000, + action: config.action + ? { + label: config.action.label, + onClick: config.action.onClick, + } + : undefined, + }); +} + +/** + * 显示 Deep Link 解析错误 + * _Requirements: 7.1_ + * + * @param message - 错误消息 + * @param code - 错误代码 + */ +export function showDeepLinkError(message: string, code?: string): void { + showConnectError({ + type: "deep_link_parse", + message, + code, + }); +} + +/** + * 显示 Registry 加载失败(已回退到缓存) + * _Requirements: 7.2_ + * + * @param message - 错误消息 + */ +export function showRegistryLoadError(message?: string): void { + showConnectError({ + type: "registry_load", + message: message || "无法从远程加载中转商注册表,已使用本地缓存", + }); +} + +/** + * 显示 Registry 不可用错误(无缓存) + * _Requirements: 7.3_ + * + * @param message - 错误消息 + * @param onRetry - 重试回调 + */ +export function showRegistryNoCacheError( + message?: string, + onRetry?: () => void, +): void { + showConnectError( + { + type: "registry_no_cache", + message: message || "无法加载中转商注册表且没有本地缓存", + }, + onRetry, + ); +} + +/** + * 显示 API Key 保存失败错误 + * _Requirements: 7.4_ + * + * @param message - 错误消息 + */ +export function showApiKeySaveError(message?: string): void { + showConnectError({ + type: "api_key_save", + message: message || "API Key 保存失败,请重试", + }); +} + +/** + * 获取错误配置(供组件使用) + */ +export { getErrorConfig };