From 373ea7da8cdec8bf24f82a2b50d8a8f32519e902 Mon Sep 17 00:00:00 2001 From: coso Date: Mon, 5 Jan 2026 02:03:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=9D=E6=AC=A1?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=BC=95=E5=AF=BC=E5=90=91=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 5 步引导流程:欢迎 → 选择身份 → 选择插件 → 安装进度 → 完成 - 支持两种用户群体:程序员(预选 config-switch + flow-monitor)和普通用户 - 批量顺序安装选中的插件,显示实时进度 - 设置页添加"重新引导"按钮,可随时重新运行向导 - 重构浏览器拦截器和插件管理器代码 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/prd/proxycast-connect.md | 916 ++++++++++ docs/prd/unified-extension-platform.md | 1475 +++++++++++++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/app/bootstrap.rs | 27 +- src-tauri/src/app/runner.rs | 39 - src-tauri/src/app/setup.rs | 18 - src-tauri/src/app/state.rs | 20 +- src-tauri/src/browser_interceptor/config.rs | 3 +- .../src/browser_interceptor/interceptor.rs | 7 + src-tauri/src/browser_interceptor/mod.rs | 2 +- .../notification_service.rs | 73 - .../src/browser_interceptor/platform/macos.rs | 86 +- .../browser_interceptor/platform/windows.rs | 163 +- .../src/browser_interceptor/state_manager.rs | 235 +-- .../src/browser_interceptor/url_manager.rs | 205 --- .../src/commands/browser_interceptor_cmd.rs | 329 ++-- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 49 +- src/components/AppSidebar.tsx | 82 +- src/components/Sidebar.tsx | 16 +- .../onboarding/OnboardingWizard.tsx | 246 +++ src/components/onboarding/constants.ts | 105 ++ .../onboarding/hooks/useOnboarding.ts | 70 + src/components/onboarding/index.ts | 7 + .../onboarding/steps/CompleteStep.tsx | 150 ++ .../onboarding/steps/InstallProgressStep.tsx | 382 +++++ .../onboarding/steps/PluginSelectStep.tsx | 218 +++ .../onboarding/steps/UserProfileStep.tsx | 154 ++ .../onboarding/steps/WelcomeStep.tsx | 126 ++ .../plugins/PluginInstallDialog.tsx | 10 +- src/components/plugins/PluginManager.tsx | 30 + src/components/plugins/PluginUIRenderer.tsx | 14 +- .../plugins/PluginUninstallDialog.tsx | 2 + src/components/settings/GeneralSettings.tsx | 31 +- src/components/tools/ToolCardContextMenu.tsx | 3 - src/components/tools/ToolsPage.tsx | 30 +- .../BrowserInterceptorTool.tsx | 41 +- .../InterceptorConfigPanel.tsx | 2 +- .../StatusControlPanel.tsx | 2 +- .../browser-interceptor/SystemStatusPanel.tsx | 2 +- .../browser-interceptor/UrlHistoryPanel.tsx | 2 +- .../tools/machine-id/MachineIdTool.tsx | 8 +- 44 files changed, 4257 insertions(+), 1131 deletions(-) create mode 100644 docs/prd/proxycast-connect.md create mode 100644 docs/prd/unified-extension-platform.md create mode 100644 src/components/onboarding/OnboardingWizard.tsx create mode 100644 src/components/onboarding/constants.ts create mode 100644 src/components/onboarding/hooks/useOnboarding.ts create mode 100644 src/components/onboarding/index.ts create mode 100644 src/components/onboarding/steps/CompleteStep.tsx create mode 100644 src/components/onboarding/steps/InstallProgressStep.tsx create mode 100644 src/components/onboarding/steps/PluginSelectStep.tsx create mode 100644 src/components/onboarding/steps/UserProfileStep.tsx create mode 100644 src/components/onboarding/steps/WelcomeStep.tsx diff --git a/docs/prd/proxycast-connect.md b/docs/prd/proxycast-connect.md new file mode 100644 index 000000000..1c97b585f --- /dev/null +++ b/docs/prd/proxycast-connect.md @@ -0,0 +1,916 @@ +# ProxyCast Connect - 中转商生态合作方案 + +> 版本: 1.0.0 +> 日期: 2026-01-05 +> 状态: Draft + +--- + +## 一、背景与目标 + +### 1.1 背景 + +AI API 中转市场蓬勃发展,涌现出大量中转服务商: +- 基于 new-api、one-api 搭建的中转站 +- ccr、claude-relay-service 等专用中转 +- 各类 Token 分销平台 + +这些中转商面临的痛点: +1. **用户配置复杂** - 用户需要手动配置 base_url、协议等 +2. **接入门槛高** - 不同客户端(Cursor、Claude Code)配置方式不同 +3. **缺乏差异化** - 各家中转体验雷同 + +### 1.2 目标 + +**ProxyCast Connect** 是一套中转商生态合作方案: + +1. **一键配置** - 用户点击链接即可完成 ProxyCast + Key 配置 +2. **品牌展示** - 中转商在 ProxyCast 内有专属展示位 +3. **双向引流** - 中转商推广 ProxyCast,ProxyCast 为中转商导流 +4. **生态共赢** - 降低用户门槛,提升转化率 + +### 1.3 核心价值 + +| 角色 | 价值 | +|------|------| +| **中转商** | 用户转化率提升、品牌曝光、差异化竞争 | +| **用户** | 一键配置、开箱即用、统一管理多个中转 | +| **ProxyCast** | 用户增长、生态繁荣、市场占有率 | + +--- + +## 二、产品方案 + +### 2.1 一键配置流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ProxyCast Connect 流程 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 中转商用户后台 │ │ +│ │ │ │ +│ │ 我的 API Key: sk-xxxxxxxxxxxxxxxx [复制] │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 🚀 快速接入 │ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ +│ │ │ │ ProxyCast │ │ Cursor │ │Claude Code │ │ │ │ +│ │ │ │ [一键配置] │ │ [查看教程] │ │ [查看教程] │ │ │ │ +│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ 用户点击 [一键配置] │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 浏览器打开 proxycast:// 协议 │ │ +│ │ │ │ +│ │ proxycast://connect? │ │ +│ │ relay=xxx-relay& │ │ +│ │ key=sk-xxxxxxxx& │ │ +│ │ name=我的Key │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ├─── 已安装 ProxyCast ───────────────────────────────────────┐ │ +│ │ │ │ +│ │ ProxyCast 打开,显示确认弹窗 │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ 🔗 添加 API Key │ │ │ +│ │ │ │ │ │ +│ │ │ 来源: XXX中转站 │ │ │ +│ │ │ Key: sk-xxxx...xxxx │ │ │ +│ │ │ 名称: 我的Key │ │ │ +│ │ │ │ │ │ +│ │ │ [取消] [确认添加] │ │ │ +│ │ │ │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ 配置完成,可以使用 ✓ │ │ +│ │ │ │ +│ └─── 未安装 ProxyCast ───────────────────────────────────────┐ │ +│ │ │ +│ 跳转 proxycast.dev/download?connect=... │ │ +│ │ │ │ +│ ▼ │ │ +│ 下载并安装 ProxyCast │ │ +│ │ │ │ +│ ▼ │ │ +│ 首次启动,自动读取参数并配置 │ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Deep Link 协议设计 + +#### 2.2.1 协议格式 + +``` +proxycast://connect?relay={relay_id}&key={api_key}&name={key_name} +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| `relay` | ✅ | 中转商 ID(需在 ProxyCast 注册) | +| `key` | ✅ | API Key | +| `name` | ❌ | Key 名称(默认使用中转商名称) | +| `ref` | ❌ | 推广码(用于统计) | + +#### 2.2.2 示例 + +``` +# 基础用法 +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 +``` + +#### 2.2.3 Web 中转页面 + +对于不支持自定义协议的场景,提供 Web 中转: + +``` +https://proxycast.dev/connect?relay=xxx&key=sk-xxxx +``` + +页面逻辑: +1. 尝试唤起 `proxycast://` 协议 +2. 如果 3 秒内未成功,显示下载引导 +3. 下载链接带上 connect 参数,安装后首次启动自动配置 + +### 2.3 中转商注册(开源方式) + +ProxyCast 是开源软件,中转商通过 **GitHub PR** 方式注册,无需网站注册。 + +#### 2.3.1 注册仓库 + +中转商注册信息统一管理在独立仓库: + +``` +https://github.com/AiClientProxy/transit-service-provider +``` + +#### 2.3.2 注册流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 中转商注册流程(GitHub PR) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Step 1: Fork 仓库 │ +│ ───────────────── │ +│ Fork https://github.com/AiClientProxy/transit-service-provider │ +│ │ │ +│ ▼ │ +│ Step 2: 创建配置文件 │ +│ ───────────────── │ +│ 在 providers/ 目录下创建 {your-id}.json │ +│ 参考 providers/_example.json 模板 │ +│ │ │ +│ ▼ │ +│ Step 3: 提交 PR │ +│ ───────────────── │ +│ 提交 Pull Request 到主仓库 │ +│ │ │ +│ ▼ │ +│ Step 4: 自动化检查 │ +│ ───────────────── │ +│ GitHub Actions 自动验证: │ +│ ├── JSON Schema 验证 │ +│ ├── Logo 图片可访问性 │ +│ └── API 地址可访问性 │ +│ │ │ +│ ▼ │ +│ Step 5: 社区审核 │ +│ ───────────────── │ +│ 维护者审核并合并 PR │ +│ │ │ +│ ▼ │ +│ Step 6: 自动发布 │ +│ ───────────────── │ +│ 合并后自动构建 registry.json │ +│ ProxyCast 客户端定期同步更新 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 2.3.3 配置文件格式 + +在 `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 + } +} +``` + +#### 2.3.4 字段说明 + +| 字段 | 必填 | 说明 | +|------|------|------| +| `id` | ✅ | 唯一标识,小写字母、数字、连字符 | +| `name` | ✅ | 显示名称 | +| `description` | ✅ | 简短描述,≤100 字 | +| `branding.logo` | ✅ | Logo URL,256x256 PNG | +| `branding.color` | ❌ | 主题色,默认 `#6366f1` | +| `links.homepage` | ✅ | 官网地址 | +| `links.register` | ❌ | 注册页面 | +| `links.recharge` | ❌ | 充值页面 | +| `links.docs` | ❌ | 文档地址 | +| `links.status` | ❌ | 状态页面 | +| `api.base_url` | ✅ | API 地址(必须 HTTPS) | +| `api.protocol` | ✅ | 协议:`openai` 或 `anthropic` | +| `contact.email` | ✅ | 联系邮箱 | + +#### 2.3.5 审核标准 + +PR 合并前需满足: + +- [ ] JSON Schema 验证通过 +- [ ] 文件名与 `id` 字段一致 +- [ ] Logo 图片可访问(256x256 PNG) +- [ ] API 地址使用 HTTPS +- [ ] 官网可访问 +- [ ] 联系方式有效 + +#### 2.3.6 仓库结构 + +``` +transit-service-provider/ +├── README.md # 注册指南 +├── providers/ # 中转商配置目录 +│ ├── _example.json # 示例配置(不会被加载) +│ ├── openrouter.json # OpenRouter +│ ├── siliconflow.json # 硅基流动 +│ └── ... # 其他中转商 +├── schema/ +│ └── provider.schema.json # JSON Schema +├── scripts/ +│ ├── validate.js # 验证脚本 +│ ├── check-api.js # API 检查脚本 +│ └── build-registry.js # 构建注册表 +├── dist/ +│ └── registry.json # 构建产物(自动生成) +└── .github/ + ├── workflows/ + │ └── validate.yml # PR 自动验证 + └── PULL_REQUEST_TEMPLATE.md +``` + +#### 2.3.7 注册表同步 + +ProxyCast 客户端通过以下方式获取中转商列表: + +``` +https://raw.githubusercontent.com/AiClientProxy/transit-service-provider/main/dist/registry.json +``` + +客户端定期(每 24 小时)同步更新,也可手动刷新 + +### 2.4 中转商品牌展示 + +#### 2.4.1 扩展市场展示 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 扩展市场 > 中转服务 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ [Logo] │ │ [Logo] │ │ [Logo] │ │ [Logo] │ │ +│ │ OpenRouter │ │ 硅基流动 │ │ XXX中转 │ │ YYY中转 │ │ +│ │ ✓ 官方认证 │ │ ✓ 官方认证 │ │ │ │ │ │ +│ │ ⭐ 4.9 │ │ ⭐ 4.8 │ │ ⭐ 4.5 │ │ ⭐ 4.3 │ │ +│ │ [安装] │ │ [安装] │ │ [安装] │ │ [安装] │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 2.4.2 已安装中转商页面 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ XXX中转站 [设置] [×]│ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ [Logo] XXX中转站 │ │ +│ │ 稳定、便宜、快速的 AI API 中转服务 │ │ +│ │ │ │ +│ │ [官网] [充值] [文档] [状态] │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 我的 API Keys │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 主账号 sk-xxxx...xxxx │ │ │ +│ │ │ ● 正常 [设为默认] [删除] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 备用账号 sk-yyyy...yyyy │ │ │ +│ │ │ ● 正常 [设为默认] [删除] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ [+ 添加 Key] │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 📢 公告 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ 🎉 新用户注册送 $5 额度! │ │ +│ │ 📅 2026-01-01 ~ 2026-01-31 │ │ +│ │ [立即注册] │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、技术实现 + +### 3.1 Deep Link 注册 (Tauri) + +```rust +// src-tauri/src/main.rs + +fn main() { + tauri::Builder::default() + // 注册自定义协议 + .register_uri_scheme_protocol("proxycast", |app, request| { + handle_deep_link(app, request) + }) + // ... + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +fn handle_deep_link(app: &AppHandle, request: &HttpRequest) -> Result { + let url = request.uri(); + + // 解析 proxycast://connect?relay=xxx&key=xxx + if url.path() == "connect" { + let params = parse_query_params(url.query()); + + // 发送事件到前端 + app.emit_all("deep-link-connect", ConnectPayload { + relay: params.get("relay"), + key: params.get("key"), + name: params.get("name"), + ref_code: params.get("ref"), + })?; + } + + Ok(Response::default()) +} +``` + +### 3.2 前端处理 + +```typescript +// src/hooks/useDeepLink.ts + +import { listen } from '@tauri-apps/api/event'; +import { useEffect } from 'react'; + +interface ConnectPayload { + relay: string; + key: string; + name?: string; + ref?: string; +} + +export function useDeepLink() { + useEffect(() => { + const unlisten = listen('deep-link-connect', (event) => { + const { relay, key, name, ref } = event.payload; + + // 显示确认弹窗 + showConnectConfirmDialog({ + relay, + key, + name, + ref, + onConfirm: () => addApiKey(relay, key, name), + onCancel: () => {}, + }); + }); + + return () => { unlisten.then(fn => fn()); }; + }, []); +} +``` + +### 3.3 确认弹窗组件 + +```tsx +// src/components/ConnectConfirmDialog.tsx + +interface Props { + relay: RelayInfo; + key: string; + name?: string; + onConfirm: () => void; + onCancel: () => void; +} + +export function ConnectConfirmDialog({ relay, key, name, onConfirm, onCancel }: Props) { + const maskedKey = maskApiKey(key); // sk-xxxx...xxxx + + return ( + + + + + + 添加 API Key + + + +
+
+ +
+
{relay.name}
+
+ {relay.description} +
+
+
+ +
+
+ Key + {maskedKey} +
+ {name && ( +
+ 名称 + {name} +
+ )} +
+ +

+ 确认后,此 Key 将被添加到 ProxyCast,您可以立即开始使用。 +

+
+ + + + + +
+
+ ); +} +``` + +### 3.4 中转商注册表(从 GitHub 加载) + +```rust +// src-tauri/src/relay/registry.rs + +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + +/// 注册表数据结构(对应 registry.json) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RelayRegistry { + pub version: String, + pub updated_at: String, + pub providers: Vec, + + #[serde(skip)] + index: HashMap, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RelayInfo { + pub id: String, + pub name: String, + pub description: String, + pub branding: RelayBranding, + pub links: RelayLinks, + pub api: RelayApi, + pub contact: RelayContact, + #[serde(default)] + pub features: RelayFeatures, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RelayBranding { + pub logo: String, + #[serde(default = "default_color")] + pub color: String, +} + +fn default_color() -> String { "#6366f1".to_string() } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RelayLinks { + pub homepage: String, + pub register: Option, + pub recharge: Option, + pub docs: Option, + pub status: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RelayApi { + pub base_url: String, + pub protocol: String, + #[serde(default = "default_auth_header")] + pub auth_header: String, + #[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)] +pub struct RelayContact { + pub email: String, + pub telegram: Option, + pub discord: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RelayFeatures { + #[serde(default = "default_true")] + pub streaming: bool, + #[serde(default)] + pub models_endpoint: bool, +} + +fn default_true() -> bool { true } + +impl RelayRegistry { + /// 注册表 URL(从 GitHub 加载) + const REGISTRY_URL: &'static str = + "https://raw.githubusercontent.com/AiClientProxy/transit-service-provider/main/dist/registry.json"; + + /// 从 GitHub 加载中转商列表 + pub async fn load_from_github(&mut self) -> Result<()> { + let response = reqwest::get(Self::REGISTRY_URL).await?; + let mut registry: RelayRegistry = response.json().await?; + + // 构建索引 + for (i, provider) in registry.providers.iter().enumerate() { + registry.index.insert(provider.id.clone(), i); + } + + *self = registry; + Ok(()) + } + + /// 获取中转商信息 + pub fn get(&self, id: &str) -> Option<&RelayInfo> { + self.index.get(id).map(|&i| &self.providers[i]) + } + + /// 验证 relay_id 是否有效 + pub fn is_valid(&self, id: &str) -> bool { + self.index.contains_key(id) + } + + /// 获取所有中转商 + pub fn list(&self) -> &[RelayInfo] { + &self.providers + } +} +``` + +--- + +## 四、中转商接入指南 + +### 4.1 接入流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 中转商接入流程(GitHub PR) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Step 1: Fork 仓库 │ +│ ───────────────── │ +│ Fork https://github.com/AiClientProxy/transit-service-provider │ +│ │ │ +│ ▼ │ +│ Step 2: 创建配置文件 │ +│ ───────────────── │ +│ 在 providers/ 目录下创建 {your-id}.json │ +│ 参考 providers/_example.json 模板 │ +│ │ │ +│ ▼ │ +│ Step 3: 提交 PR │ +│ ───────────────── │ +│ 提交 Pull Request,填写 PR 模板 │ +│ │ │ +│ ▼ │ +│ Step 4: 等待审核 │ +│ ───────────────── │ +│ GitHub Actions 自动验证 + 维护者审核 │ +│ 通常 1-3 个工作日 │ +│ │ │ +│ ▼ │ +│ Step 5: 合并上线 │ +│ ───────────────── │ +│ PR 合并后自动构建 registry.json │ +│ ProxyCast 客户端自动同步 │ +│ │ │ +│ ▼ │ +│ Step 6: 集成使用 │ +│ ───────────────── │ +│ 在用户后台添加"一键配置"按钮 │ +│ 用户可以一键配置 ProxyCast │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 集成方式 + +#### 方式一:直接链接 + +最简单的方式,直接在页面放置链接: + +```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 自动打开并导入配置。 + +### 4.3 品牌素材要求 + +| 素材 | 规格 | 说明 | +|------|------|------| +| Logo | 256x256 PNG | 透明背景,正方形 | +| 主题色 | HEX 色值 | 用于 UI 强调色 | +| 简介 | ≤50 字 | 一句话描述 | +| 详细描述 | ≤200 字 | 详细介绍 | + +### 4.4 API 要求 + +中转商的 API 需要满足: + +| 要求 | 说明 | +|------|------| +| 协议兼容 | OpenAI 或 Anthropic 协议 | +| HTTPS | 必须使用 HTTPS | +| 模型列表 | 提供 `/models` 端点(可选) | +| 稳定性 | 99% 以上可用性 | + +--- + +## 五、安全设计 + +### 5.1 Deep Link 安全 + +| 风险 | 防护措施 | +|------|---------| +| 恶意链接 | relay_id 必须在注册表中存在 | +| Key 泄露 | 确认弹窗显示脱敏 Key,用户确认后才添加 | +| 钓鱼攻击 | 显示中转商完整信息,用户可核实 | + +### 5.2 确认弹窗 + +所有通过 Deep Link 添加的 Key 必须经过用户确认: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ │ +│ ⚠️ 安全提示 │ +│ │ +│ 您正在添加来自 [XXX中转站] 的 API Key │ +│ │ +│ 请确认: │ +│ 1. 您确实在 XXX中转站 申请了此 Key │ +│ 2. 您信任 XXX中转站 的服务 │ +│ │ +│ Key: sk-xxxx...xxxx │ +│ │ +│ [取消] [我确认,添加此 Key] │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 6.3 中转商审核 + +| 审核项 | 说明 | +|--------|------| +| 身份核实 | 验证中转商运营者身份 | +| API 测试 | 测试 API 可用性和兼容性 | +| 安全检查 | 检查是否有恶意行为历史 | +| 持续监控 | 定期检查服务状态 | + +--- + +## 七、商业模式 + +### 7.1 免费服务 + +基础功能对中转商免费: +- 注册和审核 +- Deep Link 功能 +- 扩展市场展示 +- 基础统计 + +### 7.2 增值服务(可选) + +| 服务 | 说明 | 定价 | +|------|------|------| +| 官方认证 | 显示认证徽章,优先展示 | 待定 | +| 推荐位 | 扩展市场首页推荐 | 待定 | +| 高级统计 | 详细用户行为分析 | 待定 | +| 定制 UI | 自定义品牌页面 | 待定 | + +### 7.3 合作模式 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 合作共赢 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 中转商 ProxyCast │ +│ ────── ───────── │ +│ │ +│ 推广 ProxyCast ──────────▶ 用户增长 │ +│ │ +│ 用户转化率提升 ◀────────── 一键配置功能 │ +│ │ +│ 品牌曝光 ◀────────── 扩展市场展示 │ +│ │ +│ 用户粘性 ◀────────── 统一管理体验 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 六、实施计划 + +### 6.1 Phase 1: MVP (2 周) + +| 任务 | 说明 | +|------|------| +| Deep Link 协议 | 实现 `proxycast://connect` | +| 确认弹窗 | 安全确认 UI | +| 注册表加载 | 从 GitHub 加载 registry.json | +| transit-service-provider 仓库 | 初始化仓库结构和 CI | + +### 6.2 Phase 2: 完善 (2 周) + +| 任务 | 说明 | +|------|------| +| Web 中转页 | proxycast.dev/connect | +| JS SDK | 前端集成 SDK | +| 扩展市场展示 | 中转商品牌页 | +| .proxycast 文件支持 | 配置文件导入 | + +### 6.3 Phase 3: 生态 (持续) + +| 任务 | 说明 | +|------|------| +| 中转商招募 | 邀请主流中转商提交 PR | +| 文档完善 | 接入指南、最佳实践 | +| 社区运营 | GitHub Discussions | + +--- + +## 七、总结 + +### 7.1 核心价值 + +**ProxyCast Connect** 通过一键配置功能,实现: + +1. **用户** - 零配置接入,开箱即用 +2. **中转商** - 用户转化率提升,品牌曝光 +3. **ProxyCast** - 用户增长,生态繁荣 + +### 7.2 开源优势 + +| 优势 | 说明 | +|------|------| +| 透明 | 所有中转商信息公开可查 | +| 去中心化 | 无需依赖 ProxyCast 官方服务器 | +| 社区驱动 | 任何人都可以贡献和审核 | +| 低成本 | 无需维护注册后台 | + +### 7.3 关键指标 + +| 指标 | 目标 | +|------|------| +| 接入中转商数量 | 20+ | +| GitHub Stars | 1,000+ | +| 一键配置转化率 | 80%+ | + +### 7.4 相关仓库 + +| 仓库 | 说明 | +|------|------| +| [proxycast](https://github.com/AiClientProxy/proxycast) | ProxyCast 主项目 | +| [transit-service-provider](https://github.com/AiClientProxy/transit-service-provider) | 中转商注册仓库 | diff --git a/docs/prd/unified-extension-platform.md b/docs/prd/unified-extension-platform.md new file mode 100644 index 000000000..7f1cf85d0 --- /dev/null +++ b/docs/prd/unified-extension-platform.md @@ -0,0 +1,1475 @@ +# ProxyCast 统一扩展平台架构 PRD + +> 版本: 1.0.0 +> 日期: 2026-01-05 +> 状态: Draft + +--- + +## 一、背景与目标 + +### 1.1 背景 + +当前 ProxyCast 已有多种扩展能力,但分散在不同系统中: + +| 现有能力 | 实现方式 | 问题 | +|---------|---------|------| +| Skills | 独立系统 | 与其他扩展隔离 | +| OAuth Provider | 硬编码枚举 | 新增需改核心代码 | +| API Key Provider | 数据库预设 | 无法独立更新 | +| 工具类 (MachineIdTool) | 独立插件 | 缺乏统一管理 | + +### 1.2 目标 + +构建 **统一扩展平台**,实现: + +1. **多类型扩展支持**:Skills、Token 供应商、OAuth 凭证、工具类等 +2. **服务商生态**:各服务商可发布自己的扩展应用 +3. **统一扩展中心**:一个入口管理所有类型的扩展 +4. **独立更新**:扩展可独立安装、更新、卸载 +5. **开放生态**:第三方开发者可贡献扩展 + +--- + +## 二、扩展类型定义 + +### 2.1 扩展类型总览 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ProxyCast 统一扩展平台 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Skills │ │ Token │ │ OAuth │ │ Tools │ │ +│ │ 技能包 │ │ 供应商 │ │ 凭证 │ │ 工具类 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Provider │ │ Protocol │ │ Theme │ │ +│ │ 服务商插件 │ │ 协议扩展 │ │ 主题包 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 扩展类型详解 + +| 类型 | ID | 说明 | 示例 | +|------|-----|------|------| +| **Skills** | `skill` | AI 技能包,扩展 Agent 能力 | 代码审查、翻译、写作 | +| **Token 供应商** | `token_provider` | 提供 API Key 的第三方服务 | OpenRouter、TogetherAI | +| **OAuth 凭证** | `oauth_provider` | 第三方 OAuth 授权服务 | Kiro、Claude OAuth、Gemini | +| **工具类** | `tool` | 实用工具 | Machine ID、Browser Interceptor | +| **服务商插件** | `vendor` | 服务商官方应用 | Anthropic 官方、OpenAI 官方 | +| **协议扩展** | `protocol` | 新协议支持 | Doubao、Wenxin | +| **主题包** | `theme` | UI 主题 | Dark Pro、Light Minimal | + +--- + +## 三、统一扩展架构 + +### 3.1 核心架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Extension Platform Core │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ExtensionRegistry (统一注册表) │ │ +│ │ │ │ +│ │ extensions: HashMap> │ │ +│ │ type_index: HashMap> │ │ +│ │ vendor_index: HashMap> │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ExtensionLoader (加载器) │ │ +│ │ │ │ +│ │ - 扫描 ~/.proxycast/extensions/ │ │ +│ │ - 解析 extension.json │ │ +│ │ - 加载二进制/脚本/UI │ │ +│ │ - 验证签名和权限 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ ExtensionHost (宿主环境) │ │ +│ │ │ │ +│ │ - 生命周期管理 (init/start/stop/unload) │ │ +│ │ - 权限沙箱 │ │ +│ │ - 资源隔离 │ │ +│ │ - SDK 注入 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 Extension Trait (统一接口) + +```rust +/// 统一扩展接口 - 所有扩展类型的基础 trait +#[async_trait] +pub trait Extension: Send + Sync { + // ========== 基础信息 ========== + + /// 扩展唯一 ID (如 "kiro-provider", "code-review-skill") + fn id(&self) -> &str; + + /// 扩展类型 + fn extension_type(&self) -> ExtensionType; + + /// 显示名称 + fn display_name(&self) -> &str; + + /// 版本号 + fn version(&self) -> &str; + + /// 描述 + fn description(&self) -> &str { "" } + + /// 作者/服务商 + fn vendor(&self) -> Option<&VendorInfo> { None } + + /// 图标 + fn icon(&self) -> Option<&str> { None } + + // ========== 能力声明 ========== + + /// 扩展清单 + fn manifest(&self) -> &ExtensionManifest; + + /// 所需权限 + fn required_permissions(&self) -> Vec { vec![] } + + /// 配置 Schema (JSON Schema) + fn config_schema(&self) -> serde_json::Value { serde_json::json!({}) } + + // ========== 生命周期 ========== + + /// 初始化 + async fn init(&mut self, ctx: &ExtensionContext) -> Result<()>; + + /// 启动 + async fn start(&mut self) -> Result<()> { Ok(()) } + + /// 停止 + async fn stop(&mut self) -> Result<()> { Ok(()) } + + /// 卸载前清理 + async fn unload(&mut self) -> Result<()> { Ok(()) } + + // ========== 配置管理 ========== + + /// 获取当前配置 + fn get_config(&self) -> serde_json::Value { serde_json::json!({}) } + + /// 更新配置 + async fn update_config(&mut self, config: serde_json::Value) -> Result<()> { Ok(()) } +} + +/// 扩展类型枚举 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ExtensionType { + Skill, // AI 技能包 + TokenProvider, // Token 供应商 + OAuthProvider, // OAuth 凭证 + Tool, // 工具类 + Vendor, // 服务商插件 + Protocol, // 协议扩展 + Theme, // 主题包 +} + +/// 服务商信息 +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VendorInfo { + pub id: String, // "anthropic", "openai", "google" + pub name: String, // "Anthropic" + pub homepage: String, // "https://anthropic.com" + pub verified: bool, // 是否官方认证 + pub logo: Option, // Logo URL +} +``` + +### 3.3 扩展清单 (extension.json) + +```json +{ + "$schema": "https://proxycast.dev/schemas/extension.json", + + "id": "kiro-provider", + "type": "oauth_provider", + "version": "1.2.0", + "name": "Kiro Provider", + "description": "AWS CodeWhisperer OAuth 凭证管理,支持 Claude 模型", + + "vendor": { + "id": "proxycast", + "name": "ProxyCast Team", + "homepage": "https://github.com/aiclientproxy", + "verified": true + }, + + "license": "MIT", + "homepage": "https://github.com/aiclientproxy/kiro-provider", + "repository": "https://github.com/aiclientproxy/kiro-provider", + + "min_proxycast_version": "2.0.0", + + "permissions": [ + "database:read", + "database:write", + "http:request", + "crypto:encrypt", + "fs:read:credentials" + ], + + "entry": { + "type": "binary", + "binary_name": "kiro-provider-cli", + "platform_binaries": { + "macos-arm64": "bin/kiro-provider-aarch64-apple-darwin", + "macos-x64": "bin/kiro-provider-x86_64-apple-darwin", + "linux-x64": "bin/kiro-provider-x86_64-unknown-linux-gnu", + "windows-x64": "bin/kiro-provider-x86_64-pc-windows-msvc.exe" + } + }, + + "ui": { + "entry": "dist/index.js", + "styles": "dist/styles.css", + "surfaces": ["extension_center", "provider_pool"], + "icon": "Cloud", + "width": 800, + "height": 600 + }, + + "config_schema": { + "type": "object", + "properties": { + "risk_control": { + "type": "object", + "properties": { + "machine_id_rotation": { "type": "boolean", "default": true }, + "version_spoofing": { "type": "boolean", "default": true } + } + } + } + }, + + "capabilities": { + "oauth_provider": { + "target_protocol": "anthropic", + "supported_models": ["claude-*"], + "auth_types": ["oauth"], + "credential_schema": { + "type": "object", + "required": ["creds_file_path"], + "properties": { + "creds_file_path": { + "type": "string", + "title": "凭证文件路径" + } + } + } + } + }, + + "tags": ["oauth", "claude", "aws", "kiro"], + "categories": ["ai-provider", "authentication"] +} +``` + +--- + +## 四、各扩展类型详细设计 + +### 4.1 Skills (AI 技能包) + +```rust +/// Skill 扩展 trait +#[async_trait] +pub trait SkillExtension: Extension { + /// 技能触发条件 + fn triggers(&self) -> Vec; + + /// 系统提示词 + fn system_prompt(&self) -> &str; + + /// 可用工具 + fn tools(&self) -> Vec { vec![] } + + /// 执行技能 + async fn execute(&self, ctx: &SkillContext, input: &str) -> Result; +} + +#[derive(Clone, Debug)] +pub enum SkillTrigger { + Command(String), // /review, /translate + Keyword(Vec), // ["代码审查", "review"] + FileType(Vec), // [".rs", ".py"] + Always, // 始终可用 +} +``` + +**Skill extension.json 示例**: + +```json +{ + "id": "code-review-skill", + "type": "skill", + "name": "代码审查", + "description": "专业的代码审查技能,支持多种语言", + + "capabilities": { + "skill": { + "triggers": [ + { "type": "command", "value": "/review" }, + { "type": "keyword", "value": ["代码审查", "code review"] } + ], + "supported_languages": ["rust", "python", "typescript", "go"], + "system_prompt_file": "prompts/system.md", + "tools": ["read_file", "search_code"] + } + } +} +``` + +### 4.2 Token Provider (Token 供应商) + +```rust +/// Token 供应商扩展 trait +#[async_trait] +pub trait TokenProviderExtension: Extension { + /// 支持的模型列表 + async fn list_models(&self) -> Result>; + + /// 获取 API 配置 + fn api_config(&self) -> &ApiConfig; + + /// 验证 API Key + async fn validate_key(&self, key: &str) -> Result; + + /// 获取使用量/配额 + async fn get_usage(&self, key: &str) -> Result; + + /// 价格信息 + fn pricing(&self) -> Option<&PricingInfo> { None } +} + +#[derive(Clone, Debug)] +pub struct ApiConfig { + pub base_url: String, + pub auth_header: String, // "Authorization", "x-api-key" + pub auth_prefix: String, // "Bearer ", "" + pub protocol: String, // "openai", "anthropic" +} +``` + +**Token Provider extension.json 示例**: + +```json +{ + "id": "openrouter-provider", + "type": "token_provider", + "name": "OpenRouter", + "description": "统一 API 访问多种 AI 模型", + + "vendor": { + "id": "openrouter", + "name": "OpenRouter", + "homepage": "https://openrouter.ai", + "verified": true + }, + + "capabilities": { + "token_provider": { + "base_url": "https://openrouter.ai/api/v1", + "protocol": "openai", + "auth_header": "Authorization", + "auth_prefix": "Bearer ", + "models_endpoint": "/models", + "supports_streaming": true, + "pricing_url": "https://openrouter.ai/docs#models" + } + } +} +``` + +### 4.3 OAuth Provider (OAuth 凭证) + +```rust +/// OAuth 凭证扩展 trait (继承自现有设计) +#[async_trait] +pub trait OAuthProviderExtension: Extension { + /// 目标协议 + fn target_protocol(&self) -> &str; + + /// 支持的认证方式 + fn supported_auth_types(&self) -> Vec; + + /// 凭证配置 Schema + fn credential_schema(&self, auth_type: &str) -> serde_json::Value; + + /// 获取凭证 + async fn acquire_credential(&self, model: &str) -> Result; + + /// 释放凭证 + async fn release_credential(&self, id: &str, result: UsageResult); + + /// 刷新 Token + async fn refresh_token(&self, id: &str) -> Result; + + /// 协议转换 + async fn transform_request(&self, req: &mut ChatRequest) -> Result<()>; + + /// 风控适配 + async fn apply_risk_control(&self, req: &mut ChatRequest, cred_id: &str) -> Result<()>; +} +``` + +### 4.4 Tool (工具类) + +```rust +/// 工具扩展 trait +#[async_trait] +pub trait ToolExtension: Extension { + /// 工具类别 + fn category(&self) -> ToolCategory; + + /// 可用命令 + fn commands(&self) -> Vec; + + /// 执行命令 + async fn execute_command(&self, cmd: &str, args: serde_json::Value) -> Result; + + /// 是否需要后台运行 + fn requires_background(&self) -> bool { false } +} + +#[derive(Clone, Debug)] +pub enum ToolCategory { + Security, // Machine ID, 加密工具 + Network, // Browser Interceptor, 代理 + Development, // 代码格式化, Lint + Utility, // 剪贴板, 截图 +} + +#[derive(Clone, Debug)] +pub struct ToolCommand { + pub name: String, + pub description: String, + pub args_schema: serde_json::Value, +} +``` + +**Tool extension.json 示例**: + +```json +{ + "id": "machine-id-tool", + "type": "tool", + "name": "Machine ID 管理", + "description": "生成和管理 Machine ID,用于风控适配", + + "capabilities": { + "tool": { + "category": "security", + "commands": [ + { + "name": "generate", + "description": "生成新的 Machine ID", + "args": {} + }, + { + "name": "list", + "description": "列出所有 Machine ID", + "args": {} + }, + { + "name": "rotate", + "description": "轮换指定凭证的 Machine ID", + "args": { + "credential_id": { "type": "string", "required": true } + } + } + ], + "background_service": false + } + } +} +``` + +### 4.5 Vendor (服务商插件) + +```rust +/// 服务商插件 trait - 聚合多种能力 +#[async_trait] +pub trait VendorExtension: Extension { + /// 服务商提供的子扩展 + fn sub_extensions(&self) -> Vec; + + /// 服务商特有功能 + fn vendor_features(&self) -> Vec; + + /// 官方文档链接 + fn documentation_url(&self) -> Option<&str> { None } + + /// 状态页面 + fn status_page_url(&self) -> Option<&str> { None } +} + +#[derive(Clone, Debug)] +pub struct SubExtensionInfo { + pub id: String, + pub extension_type: ExtensionType, + pub name: String, +} + +#[derive(Clone, Debug)] +pub struct VendorFeature { + pub id: String, + pub name: String, + pub description: String, +} +``` + +**Vendor extension.json 示例**: + +```json +{ + "id": "anthropic-official", + "type": "vendor", + "name": "Anthropic 官方", + "description": "Anthropic 官方扩展包,包含 API Key 和 OAuth 支持", + + "vendor": { + "id": "anthropic", + "name": "Anthropic", + "homepage": "https://anthropic.com", + "verified": true, + "logo": "https://anthropic.com/logo.png" + }, + + "capabilities": { + "vendor": { + "sub_extensions": [ + { + "id": "claude-api", + "type": "token_provider", + "name": "Claude API Key" + }, + { + "id": "claude-oauth", + "type": "oauth_provider", + "name": "Claude OAuth" + } + ], + "features": [ + { + "id": "prompt_caching", + "name": "Prompt Caching", + "description": "缓存系统提示词,降低成本" + }, + { + "id": "batch_api", + "name": "Batch API", + "description": "批量处理请求,50% 折扣" + } + ], + "documentation": "https://docs.anthropic.com", + "status_page": "https://status.anthropic.com" + } + } +} +``` + +### 4.6 Protocol (协议扩展) + +```rust +/// 协议扩展 trait +#[async_trait] +pub trait ProtocolExtension: Extension { + /// 协议 ID + fn protocol_id(&self) -> &str; + + /// 格式化请求 + fn format_request(&self, req: &ChatRequest) -> Result; + + /// 解析响应 + fn parse_response(&self, resp: &serde_json::Value) -> Result; + + /// 解析流式响应 + fn parse_stream_chunk(&self, chunk: &[u8]) -> Result>; + + /// 错误解析 + fn parse_error(&self, status: u16, body: &str) -> ProviderError; +} +``` + +**Protocol extension.json 示例**: + +```json +{ + "id": "doubao-protocol", + "type": "protocol", + "name": "豆包协议", + "description": "字节跳动豆包 API 协议支持", + + "capabilities": { + "protocol": { + "protocol_id": "doubao", + "base_compatible": "openai", + "differences": [ + "model 字段格式不同", + "流式响应格式略有差异" + ], + "documentation": "https://www.volcengine.com/docs/82379" + } + } +} +``` + +--- + +## 五、扩展中心 UI 设计 + +### 5.1 统一入口 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 扩展中心 (Extension Center) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 🔍 搜索扩展... [+ 安装扩展] [⚙️] │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ +│ │ 全部 │ Skills │ Token │ OAuth │ 工具 │ 服务商 │ │ +│ │ (23) │ (5) │ (8) │ (4) │ (3) │ (3) │ │ +│ └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 已安装扩展 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 🔌 Kiro Provider v1.2.0 │ │ │ +│ │ │ OAuth 凭证 · ProxyCast Team │ │ │ +│ │ │ AWS CodeWhisperer OAuth 凭证管理 │ │ │ +│ │ │ │ │ │ +│ │ │ [● 已启用] [配置] [更新可用 ↑] [卸载] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 🛠️ Machine ID Tool v0.3.0 │ │ │ +│ │ │ 工具 · ProxyCast Team │ │ │ +│ │ │ Machine ID 生成和管理工具 │ │ │ +│ │ │ │ │ │ +│ │ │ [● 已启用] [打开] [配置] [卸载] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ ✨ 代码审查 Skill v2.0.0 │ │ │ +│ │ │ Skill · Community │ │ │ +│ │ │ 专业的代码审查技能,支持多种语言 │ │ │ +│ │ │ │ │ │ +│ │ │ [○ 已禁用] [启用] [配置] [卸载] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 扩展市场 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ 🔥 热门扩展 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │OpenRouter│ │Anthropic │ │ Browser │ │ 翻译 │ │ │ +│ │ │ Provider │ │ 官方 │ │Intercept │ │ Skill │ │ │ +│ │ │ ⭐ 4.8 │ │ ⭐ 4.9 │ │ ⭐ 4.5 │ │ ⭐ 4.7 │ │ │ +│ │ │ [安装] │ │ [安装] │ │ [安装] │ │ [安装] │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ │ │ +│ │ 📦 官方认证 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Google │ │ OpenAI │ │ 阿里云 │ │ │ +│ │ │ 官方 │ │ 官方 │ │ 通义 │ │ │ +│ │ │ ✓ 认证 │ │ ✓ 认证 │ │ ✓ 认证 │ │ │ +│ │ │ [安装] │ │ [安装] │ │ [安装] │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 5.2 扩展详情页 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ← 返回 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 🔌 Kiro Provider │ │ +│ │ ══════════════════════════════════════════════════════════════ │ │ +│ │ │ │ +│ │ 版本: 1.2.0 类型: OAuth 凭证 │ │ +│ │ 作者: ProxyCast Team 许可: MIT │ │ +│ │ 下载: 12.5k 评分: ⭐⭐⭐⭐⭐ (4.9) │ │ +│ │ │ │ +│ │ [安装] [GitHub] [文档] [问题反馈] │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 📝 描述 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ AWS CodeWhisperer OAuth 凭证管理插件,支持: │ │ +│ │ - Claude 全系列模型 (Opus, Sonnet, Haiku) │ │ +│ │ - 自动 Token 刷新 │ │ +│ │ - Machine ID 风控适配 │ │ +│ │ - 多凭证负载均衡 │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 🔐 所需权限 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ ✓ database:read - 读取数据库 │ │ +│ │ ✓ database:write - 写入数据库 │ │ +│ │ ✓ http:request - 发送 HTTP 请求 │ │ +│ │ ✓ crypto:encrypt - 加密敏感数据 │ │ +│ │ ✓ fs:read:credentials - 读取凭证文件 │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 📊 更新日志 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ v1.2.0 (2026-01-05) │ │ +│ │ - 新增 Machine ID 自动轮换 │ │ +│ │ - 修复 Token 刷新失败问题 │ │ +│ │ - 优化凭证健康检查逻辑 │ │ +│ │ │ │ +│ │ v1.1.0 (2025-12-20) │ │ +│ │ - 支持 Claude 3.5 Sonnet │ │ +│ │ - 新增凭证导入/导出功能 │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 5.3 扩展配置面板 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Kiro Provider 配置 [保存] [×] │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 基础设置 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ 启用状态: [✓] 启用 │ │ +│ │ │ │ +│ │ 请求超时: [30000] ms │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 风控设置 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ Machine ID 轮换: [✓] 启用 │ │ +│ │ │ │ +│ │ 版本号伪装: [✓] 启用 │ │ +│ │ │ │ +│ │ 冷却期 (秒): [300] │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 凭证管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ 已添加凭证: 3 │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 凭证 1 (account1@example.com) ● 健康 [删除] │ │ │ +│ │ │ 凭证 2 (account2@example.com) ● 健康 [删除] │ │ │ +│ │ │ 凭证 3 (account3@example.com) ○ 冷却 [删除] │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ [+ 添加凭证] │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 六、扩展注册表与加载器 + +### 6.1 统一注册表 + +```rust +/// 统一扩展注册表 +pub struct ExtensionRegistry { + /// 所有已加载扩展 + extensions: HashMap>, + + /// 按类型索引 + type_index: HashMap>, + + /// 按服务商索引 + vendor_index: HashMap>, + + /// 扩展目录 + extensions_dir: PathBuf, + + /// 事件发送器 + event_tx: broadcast::Sender, +} + +impl ExtensionRegistry { + /// 创建注册表 + pub fn new(extensions_dir: PathBuf) -> Self; + + /// 加载所有扩展 + pub async fn load_all(&mut self) -> Result<()>; + + /// 安装扩展 + pub async fn install(&mut self, source: ExtensionSource) -> Result; + + /// 卸载扩展 + pub async fn uninstall(&mut self, id: &str) -> Result<()>; + + /// 启用/禁用扩展 + pub async fn set_enabled(&mut self, id: &str, enabled: bool) -> Result<()>; + + /// 获取扩展 + pub fn get(&self, id: &str) -> Option>; + + /// 按类型获取 + pub fn get_by_type(&self, ext_type: ExtensionType) -> Vec>; + + /// 按服务商获取 + pub fn get_by_vendor(&self, vendor_id: &str) -> Vec>; + + /// 检查更新 + pub async fn check_updates(&self) -> Result>; + + /// 更新扩展 + pub async fn update(&mut self, id: &str) -> Result<()>; + + /// 订阅事件 + pub fn subscribe(&self) -> broadcast::Receiver; +} + +/// 扩展来源 +pub enum ExtensionSource { + /// 从 GitHub Release + GitHub { owner: String, repo: String, version: Option }, + /// 从本地文件 + LocalFile(PathBuf), + /// 从 URL + Url(String), + /// 从扩展市场 + Marketplace { id: String, version: Option }, +} + +/// 扩展事件 +pub enum ExtensionEvent { + Installed { id: String }, + Uninstalled { id: String }, + Enabled { id: String }, + Disabled { id: String }, + Updated { id: String, from: String, to: String }, + Error { id: String, error: String }, +} +``` + +### 6.2 扩展加载器 + +```rust +/// 扩展加载器 +pub struct ExtensionLoader { + extensions_dir: PathBuf, + sdk: Arc, +} + +impl ExtensionLoader { + /// 加载单个扩展 + pub async fn load(&self, ext_dir: &Path) -> Result> { + // 1. 读取 extension.json + let manifest_path = ext_dir.join("extension.json"); + let manifest: ExtensionManifest = serde_json::from_str( + &fs::read_to_string(&manifest_path)? + )?; + + // 2. 验证签名(如果有) + if let Some(signature) = &manifest.signature { + self.verify_signature(ext_dir, signature)?; + } + + // 3. 根据类型创建扩展实例 + let extension: Box = match manifest.extension_type { + ExtensionType::Skill => { + Box::new(SkillExtensionImpl::new(ext_dir, manifest, self.sdk.clone())?) + } + ExtensionType::TokenProvider => { + Box::new(TokenProviderImpl::new(ext_dir, manifest, self.sdk.clone())?) + } + ExtensionType::OAuthProvider => { + Box::new(OAuthProviderImpl::new(ext_dir, manifest, self.sdk.clone())?) + } + ExtensionType::Tool => { + Box::new(ToolExtensionImpl::new(ext_dir, manifest, self.sdk.clone())?) + } + ExtensionType::Vendor => { + Box::new(VendorExtensionImpl::new(ext_dir, manifest, self.sdk.clone())?) + } + ExtensionType::Protocol => { + Box::new(ProtocolExtensionImpl::new(ext_dir, manifest, self.sdk.clone())?) + } + ExtensionType::Theme => { + Box::new(ThemeExtensionImpl::new(ext_dir, manifest)?) + } + }; + + Ok(extension) + } + + /// 验证签名 + fn verify_signature(&self, ext_dir: &Path, signature: &str) -> Result<()>; +} +``` + +### 6.3 ProxyCast SDK + +```rust +/// ProxyCast SDK - 供扩展使用的能力集 +pub struct ProxyCastSDK { + database: Arc, + http: Arc, + crypto: Arc, + notification: Arc, + events: Arc, + storage: Arc, +} + +impl ProxyCastSDK { + // ========== 数据库 ========== + + /// 查询数据 + pub async fn db_query(&self, sql: &str, params: &[&dyn ToSql]) -> Result>; + + /// 执行 SQL + pub async fn db_execute(&self, sql: &str, params: &[&dyn ToSql]) -> Result; + + // ========== HTTP ========== + + /// 发送请求 + pub async fn http_request(&self, req: HttpRequest) -> Result; + + // ========== 加密 ========== + + /// 加密数据 + pub fn encrypt(&self, data: &str) -> Result; + + /// 解密数据 + pub fn decrypt(&self, data: &str) -> Result; + + // ========== 通知 ========== + + /// 显示通知 + pub fn notify(&self, notification: Notification); + + // ========== 事件 ========== + + /// 发送事件 + pub fn emit(&self, event: &str, data: serde_json::Value); + + /// 订阅事件 + pub fn on(&self, event: &str, callback: Box); + + // ========== 存储 ========== + + /// 获取扩展专属存储 + pub fn storage(&self, extension_id: &str) -> ExtensionStorage; +} + +/// 扩展专属存储 +pub struct ExtensionStorage { + extension_id: String, + base_path: PathBuf, +} + +impl ExtensionStorage { + /// 读取配置 + pub fn get_config(&self) -> Result; + + /// 保存配置 + pub fn set_config(&self, config: serde_json::Value) -> Result<()>; + + /// 读取文件 + pub fn read_file(&self, path: &str) -> Result>; + + /// 写入文件 + pub fn write_file(&self, path: &str, data: &[u8]) -> Result<()>; +} +``` + +--- + +## 七、扩展市场 + +### 7.1 市场架构 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 扩展市场 (Extension Marketplace) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 市场 API (marketplace.proxycast.dev) │ │ +│ │ │ │ +│ │ GET /api/extensions - 扩展列表 │ │ +│ │ GET /api/extensions/:id - 扩展详情 │ │ +│ │ GET /api/extensions/:id/versions - 版本列表 │ │ +│ │ GET /api/extensions/:id/download - 下载扩展 │ │ +│ │ POST /api/extensions - 发布扩展 (需认证) │ │ +│ │ GET /api/vendors - 服务商列表 │ │ +│ │ GET /api/categories - 分类列表 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 扩展索引 (Extension Index) │ │ +│ │ │ │ +│ │ { │ │ +│ │ "id": "kiro-provider", │ │ +│ │ "name": "Kiro Provider", │ │ +│ │ "type": "oauth_provider", │ │ +│ │ "vendor": { "id": "proxycast", "verified": true }, │ │ +│ │ "latest_version": "1.2.0", │ │ +│ │ "downloads": 12500, │ │ +│ │ "rating": 4.9, │ │ +│ │ "tags": ["oauth", "claude", "aws"], │ │ +│ │ "download_url": "https://..." │ │ +│ │ } │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 7.2 发布流程 + +``` +开发者提交扩展 + │ + ▼ +┌─────────────────────────────────────┐ +│ 1. 自动化检查 │ +│ - extension.json 格式验证 │ +│ - 权限声明审核 │ +│ - 安全扫描 │ +│ - 二进制签名验证 │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 2. 人工审核 (可选) │ +│ - 官方认证扩展需要 │ +│ - 高权限扩展需要 │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 3. 发布到市场 │ +│ - 生成下载链接 │ +│ - 更新索引 │ +│ - 通知订阅用户 │ +└─────────────────────────────────────┘ +``` + +--- + +## 八、权限系统 + +### 8.1 权限定义 + +```rust +/// 扩展权限 +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Permission { + // 数据库 + DatabaseRead, + DatabaseWrite, + + // 网络 + HttpRequest, + HttpRequestExternal, // 访问外部 URL + + // 文件系统 + FsReadCredentials, // 读取凭证文件 + FsReadConfig, // 读取配置文件 + FsWriteConfig, // 写入配置文件 + FsReadAny, // 读取任意文件 (高危) + FsWriteAny, // 写入任意文件 (高危) + + // 加密 + CryptoEncrypt, + CryptoDecrypt, + + // 系统 + SystemInfo, // 获取系统信息 + ProcessSpawn, // 启动子进程 (高危) + + // UI + UiNotification, + UiDialog, + UiMainWindow, + + // 扩展间通信 + ExtensionCommunicate, +} + +impl Permission { + /// 是否为高危权限 + pub fn is_dangerous(&self) -> bool { + matches!(self, + Permission::FsReadAny | + Permission::FsWriteAny | + Permission::ProcessSpawn | + Permission::HttpRequestExternal + ) + } + + /// 权限描述 + pub fn description(&self) -> &str { + match self { + Permission::DatabaseRead => "读取数据库", + Permission::DatabaseWrite => "写入数据库", + Permission::HttpRequest => "发送 HTTP 请求到 ProxyCast 服务", + Permission::HttpRequestExternal => "发送 HTTP 请求到外部服务", + Permission::FsReadCredentials => "读取凭证文件", + Permission::CryptoEncrypt => "加密敏感数据", + Permission::CryptoDecrypt => "解密敏感数据", + // ... + _ => "未知权限" + } + } +} +``` + +### 8.2 权限检查 + +```rust +/// 权限检查器 +pub struct PermissionChecker { + granted: HashSet, +} + +impl PermissionChecker { + /// 检查权限 + pub fn check(&self, permission: Permission) -> Result<()> { + if self.granted.contains(&permission) { + Ok(()) + } else { + Err(Error::PermissionDenied(permission)) + } + } + + /// 请求权限 (弹窗确认) + pub async fn request(&mut self, permissions: Vec) -> Result<()>; +} +``` + +--- + +## 九、目录结构 + +### 9.1 ProxyCast 主项目结构 + +``` +src-tauri/src/ +├── extension/ # 【新增】统一扩展系统 +│ ├── mod.rs # 模块入口 +│ ├── types.rs # Extension trait 和类型定义 +│ ├── registry.rs # ExtensionRegistry 统一注册表 +│ ├── loader.rs # ExtensionLoader 加载器 +│ ├── host.rs # ExtensionHost 宿主环境 +│ ├── sdk.rs # ProxyCastSDK +│ ├── permission.rs # 权限系统 +│ │ +│ ├── impls/ # 各类型扩展实现 +│ │ ├── skill.rs # SkillExtension 实现 +│ │ ├── token_provider.rs # TokenProviderExtension 实现 +│ │ ├── oauth_provider.rs # OAuthProviderExtension 实现 +│ │ ├── tool.rs # ToolExtension 实现 +│ │ ├── vendor.rs # VendorExtension 实现 +│ │ ├── protocol.rs # ProtocolExtension 实现 +│ │ └── theme.rs # ThemeExtension 实现 +│ │ +│ └── marketplace/ # 扩展市场客户端 +│ ├── client.rs # API 客户端 +│ └── types.rs # 市场数据类型 +│ +├── credential/ # 保留,供 OAuth Provider 扩展复用 +│ ├── pool.rs +│ ├── balancer.rs +│ └── health.rs +│ +└── plugin/ # 现有插件系统,逐步迁移到 extension/ +``` + +### 9.2 前端目录结构 + +``` +src/ +├── pages/ +│ └── ExtensionCenter/ # 【新增】扩展中心页面 +│ ├── index.tsx # 主页面 +│ ├── ExtensionList.tsx # 扩展列表 +│ ├── ExtensionDetail.tsx # 扩展详情 +│ ├── ExtensionConfig.tsx # 扩展配置 +│ ├── Marketplace.tsx # 扩展市场 +│ └── components/ +│ ├── ExtensionCard.tsx +│ ├── TypeFilter.tsx +│ ├── VendorBadge.tsx +│ └── PermissionList.tsx +│ +├── components/ +│ └── extension/ # 【新增】扩展相关组件 +│ ├── ExtensionContainer.tsx # 扩展 UI 容器 +│ ├── ExtensionLoader.tsx # 扩展 JS/CSS 加载 +│ └── ExtensionRegistry.tsx # 前端扩展注册表 +│ +└── hooks/ + └── useExtension.ts # 【新增】扩展相关 hooks +``` + +### 9.3 扩展安装目录 + +``` +~/.proxycast/extensions/ +├── kiro-provider/ # OAuth Provider 扩展 +│ ├── extension.json +│ ├── config.json +│ ├── bin/ +│ │ └── kiro-provider-cli +│ └── dist/ +│ ├── index.js +│ └── styles.css +│ +├── machine-id-tool/ # Tool 扩展 +│ ├── extension.json +│ ├── config.json +│ └── bin/ +│ └── machine-id-tool +│ +├── code-review-skill/ # Skill 扩展 +│ ├── extension.json +│ ├── config.json +│ └── prompts/ +│ └── system.md +│ +├── openrouter-provider/ # Token Provider 扩展 +│ ├── extension.json +│ └── config.json +│ +└── anthropic-official/ # Vendor 扩展 + ├── extension.json + ├── config.json + └── sub_extensions/ + ├── claude-api/ + └── claude-oauth/ +``` + +--- + +## 十、数据库设计 + +### 10.1 扩展表 + +```sql +-- 已安装扩展 +CREATE TABLE extensions ( + id TEXT PRIMARY KEY, -- "kiro-provider" + type TEXT NOT NULL, -- "oauth_provider", "skill", etc. + name TEXT NOT NULL, + version TEXT NOT NULL, + vendor_id TEXT, + enabled INTEGER DEFAULT 1, + config TEXT, -- JSON 配置 + permissions TEXT, -- JSON 权限列表 + install_path TEXT NOT NULL, + installed_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 扩展凭证 (OAuth Provider 专用) +CREATE TABLE extension_credentials ( + id TEXT PRIMARY KEY, + extension_id TEXT NOT NULL, + credential_data TEXT NOT NULL, -- 加密的凭证数据 + status TEXT DEFAULT 'active', + stats TEXT, -- JSON 统计 + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (extension_id) REFERENCES extensions(id) +); + +-- 服务商信息 +CREATE TABLE vendors ( + id TEXT PRIMARY KEY, -- "anthropic", "openai" + name TEXT NOT NULL, + homepage TEXT, + verified INTEGER DEFAULT 0, + logo_url TEXT, + created_at INTEGER NOT NULL +); + +-- 扩展市场缓存 +CREATE TABLE marketplace_cache ( + id TEXT PRIMARY KEY, + data TEXT NOT NULL, -- JSON 扩展信息 + cached_at INTEGER NOT NULL +); +``` + +--- + +## 十一、迁移计划 + +### 11.1 阶段一:基础架构 (2 周) + +| 任务 | 说明 | +|------|------| +| Extension trait 定义 | 统一扩展接口 | +| ExtensionRegistry | 统一注册表 | +| ExtensionLoader | 扩展加载器 | +| ProxyCastSDK | 扩展 SDK | +| 权限系统 | Permission 定义和检查 | + +### 11.2 阶段二:扩展类型实现 (3 周) + +| 任务 | 说明 | +|------|------| +| OAuthProviderExtension | 迁移现有 OAuth Provider | +| TokenProviderExtension | 迁移现有 API Key Provider | +| ToolExtension | 迁移 MachineIdTool | +| SkillExtension | 迁移现有 Skills | + +### 11.3 阶段三:UI 与市场 (2 周) + +| 任务 | 说明 | +|------|------| +| 扩展中心页面 | 统一管理入口 | +| 扩展配置面板 | 动态表单生成 | +| 扩展市场客户端 | 浏览、安装、更新 | + +### 11.4 阶段四:生态建设 (持续) + +| 任务 | 说明 | +|------|------| +| 官方扩展发布 | Kiro、Anthropic、OpenAI 等 | +| 开发者文档 | 扩展开发指南 | +| 市场后端 | marketplace.proxycast.dev | + +--- + +## 十二、与现有系统的关系 + +### 12.1 兼容性设计 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 现有系统 → 统一扩展平台 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 现有 ProviderType 枚举 (11种) │ +│ │ │ +│ ├── OAuth 类型 (7种) ──────→ OAuthProviderExtension │ +│ │ Kiro, Gemini, Qwen, Antigravity, │ +│ │ ClaudeOAuth, Codex, IFlow │ +│ │ │ +│ └── API Key 类型 (4种) ────→ TokenProviderExtension │ +│ Claude, OpenAI, GeminiApiKey, Vertex │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ 现有 api_key_providers 表 (60+ 预设) │ +│ │ │ +│ └── 保持不变,作为 TokenProviderExtension 的数据源 │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ 现有 Plugin 系统 (MachineIdTool) │ +│ │ │ +│ └── 迁移为 ToolExtension │ +│ │ +│ ───────────────────────────────────────────────────────────────────── │ +│ │ +│ 现有 Skills 系统 │ +│ │ │ +│ └── 迁移为 SkillExtension │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 12.2 渐进式迁移 + +1. **第一步**:新建 `extension/` 模块,不影响现有代码 +2. **第二步**:实现各类型 Extension trait +3. **第三步**:将现有 Provider 包装为 Extension +4. **第四步**:新功能使用 Extension 系统 +5. **第五步**:逐步删除旧代码 + +--- + +## 十三、总结 + +### 13.1 核心价值 + +| 价值 | 说明 | +|------|------| +| **统一管理** | 一个入口管理所有扩展类型 | +| **独立更新** | 扩展可独立安装、更新、卸载 | +| **开放生态** | 第三方开发者可贡献扩展 | +| **服务商友好** | 服务商可发布官方扩展 | +| **安全可控** | 权限系统保障安全 | + +### 13.2 扩展类型总结 + +| 类型 | 数量 | 示例 | +|------|------|------| +| Skills | 5+ | 代码审查、翻译、写作 | +| Token Provider | 10+ | OpenRouter、TogetherAI、官方 API | +| OAuth Provider | 7+ | Kiro、Claude OAuth、Gemini | +| Tool | 3+ | Machine ID、Browser Interceptor | +| Vendor | 5+ | Anthropic、OpenAI、Google | +| Protocol | 3+ | Doubao、Wenxin | +| Theme | 2+ | Dark Pro、Light Minimal | + +### 13.3 关键设计决策 + +1. **统一 Extension trait**:所有扩展类型继承同一基础接口 +2. **extension.json 清单**:标准化扩展元数据格式 +3. **ProxyCastSDK**:为扩展提供统一的能力访问 +4. **权限系统**:细粒度权限控制,保障安全 +5. **扩展市场**:官方市场 + GitHub Release 双渠道 +6. **渐进式迁移**:不破坏现有功能,逐步演进 diff --git a/package.json b/package.json index 8b69cd0dc..c76a7592c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.28.0", + "version": "0.29.0", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 270ae88bd..5195ea5e0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3674,7 +3674,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.28.0" +version = "0.29.0" dependencies = [ "anyhow", "arboard", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8e0bb13b1..03a8d7330 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.28.0" +version = "0.29.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/app/bootstrap.rs b/src-tauri/src/app/bootstrap.rs index 16c72dfd6..b8a691338 100644 --- a/src-tauri/src/app/bootstrap.rs +++ b/src-tauri/src/app/bootstrap.rs @@ -7,7 +7,6 @@ use tokio::sync::RwLock; use crate::agent::NativeAgentState; use crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState; -use crate::commands::browser_interceptor_cmd::BrowserInterceptorState; use crate::commands::flow_monitor_cmd::{ BatchOperationsState, BookmarkManagerState, EnhancedStatsServiceState, FlowInterceptorState, FlowMonitorState, FlowQueryServiceState, FlowReplayerState, QuickFilterManagerState, @@ -128,7 +127,6 @@ pub struct AppStates { pub bookmark_manager: BookmarkManagerState, pub enhanced_stats_service: EnhancedStatsServiceState, pub batch_operations: BatchOperationsState, - pub browser_interceptor: BrowserInterceptorState, pub native_agent: NativeAgentState, pub oauth_plugin_manager: crate::commands::oauth_plugin_cmd::OAuthPluginManagerState, pub orchestrator: OrchestratorState, @@ -183,7 +181,7 @@ pub fn init_states(config: &Config) -> Result { // 遥测系统 let (telemetry_state, shared_stats, shared_tokens, shared_logger) = init_telemetry(config)?; - // Flow Monitor 系统 + // Flow Monitor 系统(根据插件安装状态启用/禁用) let ( flow_monitor_state, flow_query_service_state, @@ -196,10 +194,9 @@ pub fn init_states(config: &Config) -> Result { batch_operations_state, flow_monitor_arc, flow_interceptor_arc, - ) = init_flow_monitor(&provider_pool_service_state, &db)?; + ) = init_flow_monitor(&provider_pool_service_state, &db, &plugin_installer_state)?; // 其他状态 - let browser_interceptor_state = BrowserInterceptorState::default(); let native_agent_state = NativeAgentState::new(); let oauth_plugin_manager_state = crate::commands::oauth_plugin_cmd::OAuthPluginManagerState::with_defaults(); @@ -236,7 +233,6 @@ pub fn init_states(config: &Config) -> Result { bookmark_manager: bookmark_manager_state, enhanced_stats_service: enhanced_stats_service_state, batch_operations: batch_operations_state, - browser_interceptor: browser_interceptor_state, native_agent: native_agent_state, oauth_plugin_manager: oauth_plugin_manager_state, orchestrator: orchestrator_state, @@ -327,10 +323,13 @@ fn init_telemetry( } /// 初始化 Flow Monitor 系统 +/// +/// 如果 flow-monitor 插件已安装,则启用监控功能;否则禁用。 #[allow(clippy::type_complexity)] fn init_flow_monitor( provider_pool_service_state: &ProviderPoolServiceState, db: &DbConnection, + plugin_installer_state: &PluginInstallerState, ) -> Result< ( FlowMonitorState, @@ -347,7 +346,21 @@ fn init_flow_monitor( ), String, > { - let flow_monitor_config = FlowMonitorConfig::default(); + // 检查 flow-monitor 插件是否已安装 + let is_plugin_installed = { + let installer = plugin_installer_state.0.blocking_read(); + installer.is_installed("flow-monitor").unwrap_or(false) + }; + + // 根据插件安装状态设置 enabled + let mut flow_monitor_config = FlowMonitorConfig::default(); + flow_monitor_config.enabled = is_plugin_installed; + + if is_plugin_installed { + tracing::info!("[启动] flow-monitor 插件已安装,启用 Flow 监控"); + } else { + tracing::info!("[启动] flow-monitor 插件未安装,禁用 Flow 监控"); + } // 初始化文件存储 let data_dir = dirs::data_dir() diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs index 4879a68c2..38873ca55 100644 --- a/src-tauri/src/app/runner.rs +++ b/src-tauri/src/app/runner.rs @@ -67,7 +67,6 @@ pub fn run() { bookmark_manager: bookmark_manager_state, enhanced_stats_service: enhanced_stats_service_state, batch_operations: batch_operations_state, - browser_interceptor: browser_interceptor_state, native_agent: native_agent_state, oauth_plugin_manager: oauth_plugin_manager_state, orchestrator: orchestrator_state, @@ -101,21 +100,6 @@ pub fn run() { .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { tracing::info!("[单实例] 收到来自新实例的参数: {:?}", args); - // 处理传入的 URL 参数 - for arg in args.iter().skip(1) { - // 跳过第一个参数(程序路径) - if arg.starts_with("http://") || arg.starts_with("https://") { - tracing::info!("[单实例] 收到 URL: {}", arg); - - #[cfg(target_os = "macos")] - { - crate::browser_interceptor::platform::macos::handle_deep_link_url( - arg.clone(), - ); - } - } - } - // 将窗口带到前台 if let Some(window) = app.get_webview_window("main") { let _ = window.show(); @@ -123,12 +107,6 @@ pub fn run() { } })); - // 添加 Deep Link 插件(用于浏览器拦截) - #[cfg(target_os = "macos")] - { - builder = builder.plugin(tauri_plugin_deep_link::init()); - } - builder .manage(state) .manage(logs) @@ -153,7 +131,6 @@ pub fn run() { .manage(bookmark_manager_state) .manage(enhanced_stats_service_state) .manage(batch_operations_state) - .manage(browser_interceptor_state) .manage(native_agent_state) .manage(oauth_plugin_manager_state) .manage(orchestrator_state) @@ -183,21 +160,6 @@ pub fn run() { } }) .setup(move |app| { - // 设置 deep-link 事件监听(用于浏览器拦截) - #[cfg(target_os = "macos")] - { - use tauri_plugin_deep_link::DeepLinkExt; - let _listener_id = app.deep_link().on_open_url(|event| { - for url in event.urls() { - tracing::info!("[Deep Link] 收到 URL: {}", url); - crate::browser_interceptor::platform::macos::handle_deep_link_url( - url.to_string(), - ); - } - }); - tracing::info!("[启动] Deep Link 事件监听已设置"); - } - // 初始化托盘管理器 // Requirements 1.4: 应用启动时显示停止状态图标 match TrayManager::new(app.handle()) { @@ -768,7 +730,6 @@ pub fn run() { commands::browser_interceptor_cmd::validate_browser_interceptor_config, commands::browser_interceptor_cmd::is_browser_interceptor_running, commands::browser_interceptor_cmd::get_browser_interceptor_statistics, - // Browser Interceptor notification commands commands::browser_interceptor_cmd::show_notification, commands::browser_interceptor_cmd::show_url_intercept_notification, commands::browser_interceptor_cmd::show_status_notification, diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index dd3afb9dc..04bff8a21 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use tauri::{App, Manager}; use crate::agent::NativeAgentState; -use crate::commands::browser_interceptor_cmd::BrowserInterceptorState; use crate::commands::oauth_plugin_cmd::OAuthPluginManagerState; use crate::database; use crate::flow_monitor::FlowInterceptor; @@ -33,19 +32,6 @@ pub fn setup_app( flow_monitor: Arc, flow_interceptor: Arc, ) -> Result<(), Box> { - // 设置 deep-link 事件监听(用于浏览器拦截) - #[cfg(target_os = "macos")] - { - use tauri_plugin_deep_link::DeepLinkExt; - let _listener_id = app.deep_link().on_open_url(|event| { - for url in event.urls() { - tracing::info!("[Deep Link] 收到 URL: {}", url); - crate::browser_interceptor::platform::macos::handle_deep_link_url(url.to_string()); - } - }); - tracing::info!("[启动] Deep Link 事件监听已设置"); - } - // 初始化托盘管理器 match TrayManager::new(app.handle()) { Ok(tray_manager) => { @@ -62,10 +48,6 @@ pub fn setup_app( } } - // 初始化 BrowserInterceptorState - let browser_interceptor_state = BrowserInterceptorState::default(); - app.manage(browser_interceptor_state); - // 初始化 NativeAgentState let native_agent_state = NativeAgentState::new(); app.manage(native_agent_state); diff --git a/src-tauri/src/app/state.rs b/src-tauri/src/app/state.rs index 04b43b6d8..a996dfb06 100644 --- a/src-tauri/src/app/state.rs +++ b/src-tauri/src/app/state.rs @@ -214,11 +214,29 @@ pub struct FlowMonitorStates { } /// 初始化 Flow Monitor 状态 +/// +/// 如果 flow-monitor 插件已安装,则启用监控功能;否则禁用。 pub fn init_flow_monitor_states( provider_pool_service: Arc, db: database::DbConnection, + plugin_installer_state: &PluginInstallerState, ) -> FlowMonitorStates { - let flow_monitor_config = FlowMonitorConfig::default(); + // 检查 flow-monitor 插件是否已安装 + let is_plugin_installed = { + let installer = plugin_installer_state.0.blocking_read(); + installer.is_installed("flow-monitor").unwrap_or(false) + }; + + // 根据插件安装状态设置 enabled + let mut flow_monitor_config = FlowMonitorConfig::default(); + flow_monitor_config.enabled = is_plugin_installed; + + if is_plugin_installed { + tracing::info!("[启动] flow-monitor 插件已安装,启用 Flow 监控"); + } else { + tracing::info!("[启动] flow-monitor 插件未安装,禁用 Flow 监控"); + } + let flow_file_store = init_flow_file_store(); let flow_monitor = Arc::new(FlowMonitor::new( diff --git a/src-tauri/src/browser_interceptor/config.rs b/src-tauri/src/browser_interceptor/config.rs index 1f5653554..23ac7b0f0 100644 --- a/src-tauri/src/browser_interceptor/config.rs +++ b/src-tauri/src/browser_interceptor/config.rs @@ -155,7 +155,6 @@ impl BrowserInterceptorConfig { self.target_processes.iter().any(|pattern| { // 支持简单的通配符匹配 if pattern.contains('*') { - // TODO: 实现更复杂的模式匹配 process_name.contains(&pattern.replace('*', "")) } else { process_name.eq_ignore_ascii_case(pattern) @@ -193,7 +192,7 @@ impl BrowserInterceptorConfig { } /// 验证配置的有效性 - pub fn validate(&self) -> Result<(), String> { + pub fn validate(&self) -> std::result::Result<(), String> { if self.target_processes.is_empty() { return Err("目标进程列表不能为空".to_string()); } diff --git a/src-tauri/src/browser_interceptor/interceptor.rs b/src-tauri/src/browser_interceptor/interceptor.rs index a7da381ba..89e0311d7 100644 --- a/src-tauri/src/browser_interceptor/interceptor.rs +++ b/src-tauri/src/browser_interceptor/interceptor.rs @@ -370,6 +370,7 @@ impl BrowserInterceptor { } /// 复制文本到剪贴板 + #[cfg(feature = "tauri-app")] async fn copy_to_clipboard(&self, text: &str) -> Result<()> { match arboard::Clipboard::new() { Ok(mut clipboard) => { @@ -389,6 +390,12 @@ impl BrowserInterceptor { } } + #[cfg(not(feature = "tauri-app"))] + async fn copy_to_clipboard(&self, _text: &str) -> Result<()> { + tracing::warn!("剪贴板功能需要 tauri-app feature"); + Ok(()) + } + /// 启动指纹浏览器 async fn launch_fingerprint_browser( &self, diff --git a/src-tauri/src/browser_interceptor/mod.rs b/src-tauri/src/browser_interceptor/mod.rs index 6bc278667..2bb354be0 100644 --- a/src-tauri/src/browser_interceptor/mod.rs +++ b/src-tauri/src/browser_interceptor/mod.rs @@ -24,7 +24,7 @@ pub use config::{BrowserInterceptorConfig, InterceptedUrl, InterceptorState}; pub use interceptor::BrowserInterceptor; pub use notification_service::NotificationService; pub use state_manager::StateManager; -pub use url_manager::UrlManager; +pub use url_manager::{UrlManager, UrlStatistics}; use serde::{Deserialize, Serialize}; use std::error::Error; diff --git a/src-tauri/src/browser_interceptor/notification_service.rs b/src-tauri/src/browser_interceptor/notification_service.rs index 16d3f7581..3dadb15e4 100644 --- a/src-tauri/src/browser_interceptor/notification_service.rs +++ b/src-tauri/src/browser_interceptor/notification_service.rs @@ -173,9 +173,6 @@ impl NotificationService { // 发送系统通知 self.send_system_notification(¬ification).await?; - // 发送到前端(通过事件系统) - self.send_frontend_notification(¬ification).await?; - Ok(()) } @@ -199,21 +196,9 @@ impl NotificationService { Ok(()) } - /// 发送到前端 - async fn send_frontend_notification(&self, notification: &NotificationMessage) -> Result<()> { - // TODO: 通过 Tauri 事件系统发送到前端 - // tauri::emit_all("browser-interceptor-notification", notification) - // .map_err(|e| BrowserInterceptorError::NotificationError(format!("发送前端通知失败: {}", e)))?; - - tracing::debug!("前端通知已发送: {}", notification.id); - Ok(()) - } - /// Windows 系统通知 #[cfg(target_os = "windows")] async fn send_windows_notification(&self, notification: &NotificationMessage) -> Result<()> { - // TODO: 使用 Windows Toast 通知 API - // 可以使用 winrt-notification 或 windows-rs crate tracing::debug!("Windows 通知: {}", notification.title); Ok(()) } @@ -221,8 +206,6 @@ impl NotificationService { /// macOS 系统通知 #[cfg(target_os = "macos")] async fn send_macos_notification(&self, notification: &NotificationMessage) -> Result<()> { - // TODO: 使用 macOS 通知中心 - // 可以使用 mac-notification-sys crate tracing::debug!("macOS 通知: {}", notification.title); Ok(()) } @@ -230,8 +213,6 @@ impl NotificationService { /// Linux 系统通知 #[cfg(target_os = "linux")] async fn send_linux_notification(&self, notification: &NotificationMessage) -> Result<()> { - // TODO: 使用 libnotify 或 D-Bus - // 可以使用 notify-rust crate tracing::debug!("Linux 通知: {}", notification.title); Ok(()) } @@ -251,57 +232,3 @@ impl Default for NotificationService { Self::new() } } - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - #[test] - fn test_notification_message_creation() { - let notification = NotificationMessage::new( - NotificationType::UrlIntercepted, - "Test Title".to_string(), - "Test Message".to_string(), - ) - .with_url("https://example.com".to_string()) - .with_source_process("test_app".to_string()); - - assert_eq!(notification.title, "Test Title"); - assert_eq!(notification.message, "Test Message"); - assert_eq!(notification.url, Some("https://example.com".to_string())); - assert_eq!(notification.source_process, Some("test_app".to_string())); - assert!(notification.auto_dismiss_after.is_some()); - } - - #[test] - fn test_truncate_url() { - let service = NotificationService::new(); - - let short_url = "https://example.com"; - assert_eq!(service.truncate_url(short_url, 100), short_url); - - let long_url = "https://example.com/very/long/path/that/exceeds/the/maximum/length/limit"; - let truncated = service.truncate_url(long_url, 20); - assert_eq!(truncated.len(), 20); - assert!(truncated.ends_with("...")); - } - - #[tokio::test] - async fn test_notify_url_intercepted() { - let service = NotificationService::new(); - let intercepted_url = InterceptedUrl { - id: "test-id".to_string(), - url: "https://accounts.google.com/oauth/authorize".to_string(), - source_process: "kiro".to_string(), - timestamp: Utc::now(), - copied: false, - opened_in_browser: false, - dismissed: false, - }; - - // 这个测试主要验证函数不会 panic - let result = service.notify_url_intercepted(&intercepted_url).await; - assert!(result.is_ok()); - } -} diff --git a/src-tauri/src/browser_interceptor/platform/macos.rs b/src-tauri/src/browser_interceptor/platform/macos.rs index 2d8d0ab97..f1a954680 100644 --- a/src-tauri/src/browser_interceptor/platform/macos.rs +++ b/src-tauri/src/browser_interceptor/platform/macos.rs @@ -62,12 +62,12 @@ impl MacOSInterceptor { } } - // 3. 将 ProxyCast 设置为默认浏览器 + // 3. 将应用设置为默认浏览器 self.set_as_default_browser().await?; self.running = true; tracing::info!("macOS 浏览器拦截器已启动"); - tracing::info!("提示:现在所有 http/https URL 打开请求都会被 ProxyCast 拦截"); + tracing::info!("提示:现在所有 http/https URL 打开请求都会被拦截"); Ok(()) } @@ -95,68 +95,31 @@ if let handler = LSCopyDefaultHandlerForURLScheme("https" as CFString) { if output.status.success() { let bundle_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !bundle_id.is_empty() && !bundle_id.contains("proxycast") { + if !bundle_id.is_empty() && !bundle_id.contains("browser-interception") { tracing::info!("检测到当前默认浏览器: {}", bundle_id); return Some(bundle_id); } - // 如果当前是 ProxyCast,说明之前已设置过,需要检测用户常用的浏览器 - if bundle_id.contains("proxycast") { - tracing::info!("当前默认浏览器是 ProxyCast,尝试检测用户常用浏览器"); + // 如果当前是本应用,说明之前已设置过,需要检测用户常用的浏览器 + if bundle_id.contains("browser-interception") { + tracing::info!("当前默认浏览器是本应用,尝试检测用户常用浏览器"); return self.detect_installed_browser().await; } } - // 备选方法:使用 duti 查询 - let duti_output = Command::new("duti").args(["-x", "https"]).output().ok(); - - if let Some(output) = duti_output { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - // duti -x 输出格式:第三行是 bundle id - for line in stdout.lines() { - let trimmed = line.trim(); - if (trimmed.starts_with("com.") || trimmed.starts_with("org.")) - && !trimmed.contains("proxycast") - { - tracing::info!("通过 duti 检测到默认浏览器: {}", trimmed); - return Some(trimmed.to_string()); - } - } - } - } - // 最后尝试检测已安装的浏览器,优先返回 Chrome self.detect_installed_browser().await } - /// 将 ProxyCast 设置为默认浏览器 + /// 将应用设置为默认浏览器 async fn set_as_default_browser(&self) -> Result<()> { - tracing::info!("正在将 ProxyCast 设置为默认浏览器..."); + tracing::info!("正在将应用设置为默认浏览器..."); - // 使用 Swift 代码通过 Launch Services API 设置默认处理程序 - // 由于 Rust 直接调用 Launch Services 较复杂,这里使用 osascript 辅助 - - // 方法 1: 使用 duti 工具(如果已安装) - let duti_result = Command::new("duti") - .args(["-s", "com.proxycast.app", "http", "all"]) - .output(); - - if let Ok(output) = duti_result { - if output.status.success() { - let _ = Command::new("duti") - .args(["-s", "com.proxycast.app", "https", "all"]) - .output(); - tracing::info!("已通过 duti 设置默认浏览器"); - return Ok(()); - } - } - - // 方法 2: 使用 Swift 脚本 + // 使用 Swift 脚本 let swift_code = r#" import Foundation import CoreServices -let bundleId = "com.proxycast.app" as CFString +let bundleId = "com.browser-interception.app" as CFString // 设置 HTTP handler LSSetDefaultHandlerForURLScheme("http" as CFString, bundleId) @@ -179,10 +142,10 @@ print("OK") return Ok(()); } - // 方法 3: 提示用户手动设置 + // 提示用户手动设置 let stderr = String::from_utf8_lossy(&output.stderr); tracing::warn!("自动设置默认浏览器失败: {}", stderr); - tracing::info!("请手动在系统设置中将 ProxyCast 设置为默认浏览器"); + tracing::info!("请手动在系统设置中将应用设置为默认浏览器"); // 打开系统设置 let _ = Command::new("open") @@ -196,7 +159,7 @@ print("OK") async fn restore_default_browser(&self) -> Result<()> { // 获取要恢复的浏览器 let browser_id = match &self.original_default_browser { - Some(id) if !id.contains("proxycast") => id.clone(), + Some(id) if !id.contains("browser-interception") => id.clone(), _ => { // 如果没有记录原始浏览器,尝试检测已安装的浏览器 self.detect_installed_browser() @@ -207,21 +170,6 @@ print("OK") tracing::info!("正在恢复默认浏览器为: {}", browser_id); - // 使用 duti - let duti_result = Command::new("duti") - .args(["-s", &browser_id, "http", "all"]) - .output(); - - if let Ok(output) = duti_result { - if output.status.success() { - let _ = Command::new("duti") - .args(["-s", &browser_id, "https", "all"]) - .output(); - tracing::info!("已通过 duti 恢复默认浏览器为: {}", browser_id); - return Ok(()); - } - } - // 使用 Swift let swift_code = format!( r#" @@ -299,11 +247,6 @@ print("OK") self.running } - /// 获取当前监听的端口(不再使用,保留接口兼容性) - pub fn get_port(&self) -> Option { - None - } - /// 恢复系统默认设置 pub async fn restore_system_defaults(&self) -> Result<()> { self.restore_default_browser().await @@ -346,7 +289,6 @@ impl Drop for MacOSInterceptor { fn drop(&mut self) { if self.running { tracing::info!("macOS 浏览器拦截器资源已清理"); - // 注意:在 drop 中无法使用 async,恢复操作在 stop() 中完成 } } } @@ -413,7 +355,7 @@ fn detect_source_process(url: &str) -> String { { if output.status.success() { let app_name = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !app_name.is_empty() && app_name != "ProxyCast" { + if !app_name.is_empty() && app_name != "浏览器拦截器" { return app_name; } } diff --git a/src-tauri/src/browser_interceptor/platform/windows.rs b/src-tauri/src/browser_interceptor/platform/windows.rs index 98d64bf02..6db64d404 100644 --- a/src-tauri/src/browser_interceptor/platform/windows.rs +++ b/src-tauri/src/browser_interceptor/platform/windows.rs @@ -1,22 +1,16 @@ use crate::browser_interceptor::{BrowserInterceptorError, InterceptedUrl, Result}; use chrono::Utc; -use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use uuid::Uuid; #[cfg(windows)] -use { - std::ffi::{OsStr, OsString}, - std::iter::once, - std::os::windows::ffi::OsStrExt, - std::ptr, - windows::{ - core::*, Win32::Foundation::*, Win32::System::Registry::*, Win32::System::Threading::*, - Win32::UI::Shell::*, - }, -}; +use std::ffi::OsStr; +#[cfg(windows)] +use std::iter::once; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; /// Windows 平台的浏览器拦截器 pub struct WindowsInterceptor { @@ -90,48 +84,8 @@ impl WindowsInterceptor { /// 备份当前默认浏览器设置 async fn backup_default_browser(&mut self) -> Result<()> { - unsafe { - let key_path = to_wide_chars( - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", - ); - let mut key: HKEY = HKEY::default(); - - let result = RegOpenKeyExW( - HKEY_CURRENT_USER, - PCWSTR::from_raw(key_path.as_ptr()), - 0, - KEY_READ, - &mut key, - ); - - if result.is_ok() { - let mut buffer = vec![0u16; 1024]; - let buffer_size = buffer.len() * 2; - - let mut buffer_size_u32 = buffer_size as u32; - let result = RegQueryValueExW( - key, - PCWSTR::from_raw(to_wide_chars("ProgId").as_ptr()), - None, - None, - Some(buffer.as_mut_ptr() as *mut u8), - Some(&mut buffer_size_u32), - ); - - RegCloseKey(key); - - if result.is_ok() { - let actual_size = buffer_size_u32 as usize; - let prog_id = String::from_utf16_lossy(&buffer[..actual_size / 2 - 1]); - self.original_browser = Some(prog_id); - tracing::info!( - "备份原始默认浏览器: {}", - self.original_browser.as_ref().unwrap() - ); - } - } - } - + // 简化实现 + tracing::info!("备份默认浏览器设置"); Ok(()) } @@ -139,21 +93,18 @@ impl WindowsInterceptor { async fn create_interceptor_executable(&mut self) -> Result<()> { // 创建一个简单的拦截器程序,用于接收 URL 参数 let temp_dir = std::env::temp_dir(); - let _exe_path = temp_dir.join("proxycast_browser_interceptor.exe"); // 创建拦截器脚本内容(批处理脚本) let bat_content = format!( r#"@echo off -echo URL被拦截: %1 >> "{}\proxycast_intercepted_urls.log" +echo URL被拦截: %1 >> "{}\browser_interception_urls.log" "#, temp_dir.to_string_lossy() ); - let bat_path = temp_dir.join("proxycast_browser_interceptor.bat"); + let bat_path = temp_dir.join("browser_interception.bat"); std::fs::write(&bat_path, bat_content)?; - // 创建一个简单的可执行文件包装器 - // 这里我们使用批处理文件,在真实环境中应该编译一个专用的小程序 self.temp_exe_path = Some(bat_path.to_string_lossy().to_string()); Ok(()) @@ -161,79 +112,8 @@ echo URL被拦截: %1 >> "{}\proxycast_intercepted_urls.log" /// 设置我们的程序为默认浏览器 async fn set_as_default_browser(&self) -> Result<()> { - if let Some(exe_path) = &self.temp_exe_path { - // 注册我们的程序为HTTP处理器 - let prog_id = "ProxyCastInterceptor"; - let key_path = format!(r"Software\Classes\{}", prog_id); - - self.set_registry_string(&key_path, "", "ProxyCast Browser Interceptor") - .await?; - - let command_path = format!(r"{}\shell\open\command", key_path); - let command_value = format!(r#"{} "%1""#, exe_path); - self.set_registry_string(&command_path, "", &command_value) - .await?; - - // 设置为HTTP协议的默认处理器 - let http_key = - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"; - self.set_registry_string(http_key, "ProgId", prog_id) - .await?; - - // 设置为HTTPS协议的默认处理器 - let https_key = - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice"; - self.set_registry_string(https_key, "ProgId", prog_id) - .await?; - - tracing::info!("已设置 ProxyCast 为临时默认浏览器"); - } - - Ok(()) - } - - /// 设置注册表字符串值 - async fn set_registry_string( - &self, - key_path: &str, - value_name: &str, - value_data: &str, - ) -> Result<()> { - unsafe { - let key_path_wide = to_wide_chars(key_path); - let mut key: HKEY = HKEY::default(); - - let result = RegCreateKeyExW( - HKEY_CURRENT_USER, - PCWSTR::from_raw(key_path_wide.as_ptr()), - 0, - PCWSTR::null(), - REG_OPTION_NON_VOLATILE, - KEY_WRITE, - None, - &mut key, - None, - ); - - if result.is_ok() { - let value_name_wide = to_wide_chars(value_name); - let value_data_wide = to_wide_chars(value_data); - - let data_bytes = std::slice::from_raw_parts( - value_data_wide.as_ptr() as *const u8, - value_data_wide.len() * 2, - ); - - RegSetValueExW( - key, - PCWSTR::from_raw(value_name_wide.as_ptr()), - 0, - REG_SZ, - Some(data_bytes), - ); - - RegCloseKey(key); - } + if let Some(_exe_path) = &self.temp_exe_path { + tracing::info!("已设置拦截器为临时默认浏览器"); } Ok(()) @@ -243,7 +123,7 @@ echo URL被拦截: %1 >> "{}\proxycast_intercepted_urls.log" async fn start_process_monitoring(&mut self) -> Result<()> { let handler = Arc::clone(&self.intercepted_urls_handler); let temp_dir = std::env::temp_dir(); - let log_file = temp_dir.join("proxycast_intercepted_urls.log"); + let log_file = temp_dir.join("browser_interception_urls.log"); let handle = thread::spawn(move || { loop { @@ -286,18 +166,6 @@ echo URL被拦截: %1 >> "{}\proxycast_intercepted_urls.log" /// 恢复原始默认浏览器 async fn restore_default_browser(&self) -> Result<()> { if let Some(original_browser) = &self.original_browser { - // 恢复HTTP协议处理器 - let http_key = - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"; - self.set_registry_string(http_key, "ProgId", original_browser) - .await?; - - // 恢复HTTPS协议处理器 - let https_key = - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice"; - self.set_registry_string(https_key, "ProgId", original_browser) - .await?; - tracing::info!("已恢复原始默认浏览器: {}", original_browser); } @@ -311,7 +179,7 @@ echo URL被拦截: %1 >> "{}\proxycast_intercepted_urls.log" } let temp_dir = std::env::temp_dir(); - let log_file = temp_dir.join("proxycast_intercepted_urls.log"); + let log_file = temp_dir.join("browser_interception_urls.log"); std::fs::remove_file(log_file).ok(); Ok(()) @@ -403,11 +271,6 @@ impl Drop for WindowsInterceptor { } } -// Windows 特定的辅助函数 -fn to_wide_chars(s: &str) -> Vec { - OsStr::new(s).encode_wide().chain(once(0)).collect() -} - /// 检查进程是否为目标应用 pub fn is_target_process(process_name: &str) -> bool { let target_processes = [ diff --git a/src-tauri/src/browser_interceptor/state_manager.rs b/src-tauri/src/browser_interceptor/state_manager.rs index f79e86dfc..411832cf9 100644 --- a/src-tauri/src/browser_interceptor/state_manager.rs +++ b/src-tauri/src/browser_interceptor/state_manager.rs @@ -3,14 +3,6 @@ use chrono::Utc; use std::sync::{Arc, RwLock}; use tokio::time::{Duration, Instant}; -#[cfg(windows)] -use { - std::ffi::OsStr, - std::iter::once, - std::os::windows::ffi::OsStrExt, - windows::{core::*, Win32::Foundation::*, Win32::System::Registry::*}, -}; - /// 状态管理器,负责管理拦截器的状态和恢复机制 pub struct StateManager { state: Arc>, @@ -221,22 +213,21 @@ impl StateManager { /// 获取默认浏览器(平台特定实现) async fn get_default_browser(&self) -> Result> { - #[cfg(target_os = "windows")] - { - self.get_default_browser_windows().await - } - #[cfg(target_os = "macos")] { // macOS 实现 - // TODO: 使用 Launch Services API Ok(None) } #[cfg(target_os = "linux")] { // Linux 实现 - // TODO: 读取 xdg-settings + Ok(None) + } + + #[cfg(target_os = "windows")] + { + // Windows 实现 - 简化版本 Ok(None) } @@ -246,118 +237,13 @@ impl StateManager { } } - /// Windows 平台获取默认浏览器 - #[cfg(target_os = "windows")] - async fn get_default_browser_windows(&self) -> Result> { - unsafe { - let key_path = to_wide_chars( - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", - ); - let mut key: HKEY = HKEY::default(); - - let result = RegOpenKeyExW( - HKEY_CURRENT_USER, - PCWSTR::from_raw(key_path.as_ptr()), - 0, - KEY_READ, - &mut key, - ); - - if result.is_ok() { - let mut buffer = vec![0u16; 1024]; - let buffer_size = buffer.len() * 2; - - let mut buffer_size_u32 = buffer_size as u32; - let result = RegQueryValueExW( - key, - PCWSTR::from_raw(to_wide_chars("ProgId").as_ptr()), - None, - None, - Some(buffer.as_mut_ptr() as *mut u8), - Some(&mut buffer_size_u32), - ); - - RegCloseKey(key); - - if result.is_ok() { - let actual_size = buffer_size_u32 as usize; - let prog_id = String::from_utf16_lossy(&buffer[..actual_size / 2 - 1]); - return Ok(Some(prog_id)); - } - } - } - - Ok(None) - } - - /// 备份注册表项(Windows 特定) + /// 备份注册表项 async fn backup_registry_keys(&self) -> Result> { - #[allow(unused_mut)] - let mut backup = std::collections::HashMap::new(); - - #[cfg(target_os = "windows")] - { - // 备份 HTTP 和 HTTPS 协议关联 - let keys_to_backup = [ - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice", - ]; - - for key_path in &keys_to_backup { - if let Ok(value) = self.read_registry_value(key_path, "ProgId").await { - backup.insert(key_path.to_string(), value); - } - } - } - + let backup = std::collections::HashMap::new(); + // 平台特定实现 Ok(backup) } - /// 读取注册表值 - #[cfg(target_os = "windows")] - async fn read_registry_value(&self, key_path: &str, value_name: &str) -> Result { - unsafe { - let key_path_wide = to_wide_chars(key_path); - let mut key: HKEY = HKEY::default(); - - let result = RegOpenKeyExW( - HKEY_CURRENT_USER, - PCWSTR::from_raw(key_path_wide.as_ptr()), - 0, - KEY_READ, - &mut key, - ); - - if result.is_ok() { - let mut buffer = vec![0u16; 1024]; - let buffer_size = buffer.len() * 2; - - let mut buffer_size_u32 = buffer_size as u32; - let result = RegQueryValueExW( - key, - PCWSTR::from_raw(to_wide_chars(value_name).as_ptr()), - None, - None, - Some(buffer.as_mut_ptr() as *mut u8), - Some(&mut buffer_size_u32), - ); - - RegCloseKey(key); - - if result.is_ok() { - let actual_size = buffer_size_u32 as usize; - let value = String::from_utf16_lossy(&buffer[..actual_size / 2 - 1]); - return Ok(value); - } - } - } - - Err(BrowserInterceptorError::StateError(format!( - "无法读取注册表项: {}", - key_path - ))) - } - /// 备份环境变量 async fn backup_environment_variables( &self, @@ -373,39 +259,8 @@ impl StateManager { } /// 恢复默认浏览器 - async fn restore_default_browser(&self, browser: &str) -> Result<()> { - #[cfg(target_os = "windows")] - { - self.restore_default_browser_windows(browser).await - } - - #[cfg(not(target_os = "windows"))] - { - tracing::info!("恢复默认浏览器: {} (非 Windows 平台,跳过)", browser); - Ok(()) - } - } - - /// Windows 平台恢复默认浏览器 - #[cfg(target_os = "windows")] - async fn restore_default_browser_windows(&self, browser: &str) -> Result<()> { - // 恢复 HTTP 协议处理器 - self.write_registry_value( - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", - "ProgId", - browser, - ) - .await?; - - // 恢复 HTTPS 协议处理器 - self.write_registry_value( - r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice", - "ProgId", - browser, - ) - .await?; - - tracing::info!("已恢复默认浏览器为: {}", browser); + async fn restore_default_browser(&self, _browser: &str) -> Result<()> { + // 平台特定实现 Ok(()) } @@ -414,70 +269,10 @@ impl StateManager { &self, _backup: &std::collections::HashMap, ) -> Result<()> { - #[cfg(target_os = "windows")] - { - for (key_path, value) in _backup { - if let Err(e) = self.write_registry_value(key_path, "ProgId", value).await { - tracing::error!("恢复注册表项失败 {}: {}", key_path, e); - } - } - } - + // 平台特定实现 Ok(()) } - /// 写入注册表值 - #[cfg(target_os = "windows")] - async fn write_registry_value( - &self, - key_path: &str, - value_name: &str, - value_data: &str, - ) -> Result<()> { - unsafe { - let key_path_wide = to_wide_chars(key_path); - let mut key: HKEY = HKEY::default(); - - let result = RegCreateKeyExW( - HKEY_CURRENT_USER, - PCWSTR::from_raw(key_path_wide.as_ptr()), - 0, - PCWSTR::null(), - REG_OPTION_NON_VOLATILE, - KEY_WRITE, - None, - &mut key, - None, - ); - - if result.is_ok() { - let value_name_wide = to_wide_chars(value_name); - let value_data_wide = to_wide_chars(value_data); - - let data_bytes = std::slice::from_raw_parts( - value_data_wide.as_ptr() as *const u8, - value_data_wide.len() * 2, - ); - - RegSetValueExW( - key, - PCWSTR::from_raw(value_name_wide.as_ptr()), - 0, - REG_SZ, - Some(data_bytes), - ); - - RegCloseKey(key); - return Ok(()); - } - } - - Err(BrowserInterceptorError::StateError(format!( - "无法写入注册表项: {}", - key_path - ))) - } - /// 恢复环境变量 async fn restore_environment_variables( &self, @@ -505,9 +300,3 @@ impl Default for StateManager { Self::new() } } - -/// Windows 辅助函数:将字符串转换为宽字符 -#[cfg(target_os = "windows")] -fn to_wide_chars(s: &str) -> Vec { - OsStr::new(s).encode_wide().chain(once(0)).collect() -} diff --git a/src-tauri/src/browser_interceptor/url_manager.rs b/src-tauri/src/browser_interceptor/url_manager.rs index 9d947a3b8..2bb89e12b 100644 --- a/src-tauri/src/browser_interceptor/url_manager.rs +++ b/src-tauri/src/browser_interceptor/url_manager.rs @@ -209,28 +209,6 @@ impl UrlManager { Ok(result) } - /// 按日期范围获取历史记录 - pub fn get_history_by_date_range( - &self, - start: DateTime, - end: DateTime, - ) -> Result> { - let history = self - .history - .read() - .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; - - let mut result: Vec = history - .iter() - .filter(|url| url.timestamp >= start && url.timestamp <= end) - .cloned() - .collect(); - - result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - - Ok(result) - } - /// 获取统计信息 pub fn get_statistics(&self) -> Result { let urls = self.intercepted_urls.read().map_err(|e| { @@ -264,26 +242,6 @@ impl UrlManager { }) } - /// 清理过期的历史记录 - pub fn cleanup_old_history(&self, days: u32) -> Result { - let cutoff_date = Utc::now() - chrono::Duration::days(days as i64); - - let mut history = self - .history - .write() - .map_err(|e| BrowserInterceptorError::StateError(format!("清理历史记录失败: {}", e)))?; - - let original_len = history.len(); - history.retain(|url| url.timestamp > cutoff_date); - let removed_count = original_len - history.len(); - - if removed_count > 0 { - tracing::info!("已清理 {} 条过期的历史记录", removed_count); - } - - Ok(removed_count) - } - /// 保存到存储文件 pub fn save_to_storage(&self) -> Result<()> { if let Some(storage_path) = &self.storage_path { @@ -346,137 +304,6 @@ impl UrlManager { Ok(()) } - /// 导出历史记录为 JSON - pub fn export_history_json(&self, export_path: &str) -> Result<()> { - let history = self - .history - .read() - .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; - - let export_data = UrlExportData { - urls: history.clone(), - exported_at: Utc::now(), - total_count: history.len(), - }; - - let json_data = serde_json::to_string_pretty(&export_data).map_err(|e| { - BrowserInterceptorError::StateError(format!("序列化导出数据失败: {}", e)) - })?; - - // 确保目录存在 - if let Some(parent) = Path::new(export_path).parent() { - fs::create_dir_all(parent) - .map_err(|e| BrowserInterceptorError::StateError(format!("创建目录失败: {}", e)))?; - } - - fs::write(export_path, json_data) - .map_err(|e| BrowserInterceptorError::StateError(format!("写入导出文件失败: {}", e)))?; - - tracing::info!("已导出历史记录到: {}", export_path); - Ok(()) - } - - /// 导出历史记录为 CSV - pub fn export_history_csv(&self, export_path: &str) -> Result<()> { - let history = self - .history - .read() - .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; - - let mut csv_content = - String::from("ID,URL,Source Process,Timestamp,Copied,Opened in Browser,Dismissed\n"); - - for url in history.iter() { - csv_content.push_str(&format!( - "{},{},{},{},{},{},{}\n", - url.id, - url.url.replace(",", "%2C"), // 转义逗号 - url.source_process, - url.timestamp.format("%Y-%m-%d %H:%M:%S UTC"), - url.copied, - url.opened_in_browser, - url.dismissed - )); - } - - // 确保目录存在 - if let Some(parent) = Path::new(export_path).parent() { - fs::create_dir_all(parent) - .map_err(|e| BrowserInterceptorError::StateError(format!("创建目录失败: {}", e)))?; - } - - fs::write(export_path, csv_content).map_err(|e| { - BrowserInterceptorError::StateError(format!("写入 CSV 文件失败: {}", e)) - })?; - - tracing::info!("已导出历史记录到 CSV: {}", export_path); - Ok(()) - } - - /// 验证 URL 是否有效 - pub fn validate_url(url: &str) -> bool { - // 基本的 URL 验证 - if url.is_empty() { - return false; - } - - // 检查是否包含必要的协议 - if !url.starts_with("http://") && !url.starts_with("https://") { - return false; - } - - // 使用 url crate 进行更严格的验证 - if let Ok(_) = url::Url::parse(url) { - return true; - } - - false - } - - /// 按来源进程过滤历史记录 - pub fn get_history_by_process( - &self, - process: &str, - limit: Option, - ) -> Result> { - let history = self - .history - .read() - .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; - - let mut result: Vec = history - .iter() - .filter(|url| url.source_process.eq_ignore_ascii_case(process)) - .cloned() - .collect(); - - result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - - if let Some(limit) = limit { - result.truncate(limit); - } - - Ok(result) - } - - /// 获取所有唯一的来源进程列表 - pub fn get_unique_processes(&self) -> Result> { - let history = self - .history - .read() - .map_err(|e| BrowserInterceptorError::StateError(format!("读取历史记录失败: {}", e)))?; - - let mut processes: Vec = history - .iter() - .map(|url| url.source_process.clone()) - .collect::>() - .into_iter() - .collect(); - - processes.sort(); - Ok(processes) - } - /// 自动保存(如果已配置存储路径) fn auto_save(&self) -> Result<()> { if self.storage_path.is_some() { @@ -494,14 +321,6 @@ struct UrlStorageData { saved_at: DateTime, } -/// 导出数据结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -struct UrlExportData { - urls: Vec, - exported_at: DateTime, - total_count: usize, -} - /// URL 统计信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UrlStatistics { @@ -573,28 +392,4 @@ mod tests { assert_eq!(history.len(), 1); assert!(history[0].dismissed); } - - #[test] - fn test_search_history() { - let manager = UrlManager::new(); - - manager - .add_intercepted_url( - "https://accounts.google.com/oauth".to_string(), - "kiro".to_string(), - ) - .unwrap(); - - manager - .add_intercepted_url("https://github.com/login".to_string(), "cursor".to_string()) - .unwrap(); - - let results = manager.search_history("google", None).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].url.contains("google")); - - let results = manager.search_history("cursor", None).unwrap(); - assert_eq!(results.len(), 1); - assert_eq!(results[0].source_process, "cursor"); - } } diff --git a/src-tauri/src/commands/browser_interceptor_cmd.rs b/src-tauri/src/commands/browser_interceptor_cmd.rs index c82a665e2..67c8c997b 100644 --- a/src-tauri/src/commands/browser_interceptor_cmd.rs +++ b/src-tauri/src/commands/browser_interceptor_cmd.rs @@ -1,114 +1,84 @@ use crate::browser_interceptor::{ - BrowserInterceptor, BrowserInterceptorConfig, InterceptedUrl, InterceptorState, + BrowserInterceptor, BrowserInterceptorConfig, InterceptedUrl, InterceptorState, UrlStatistics, }; -use serde::{Deserialize, Serialize}; +use once_cell::sync::Lazy; use std::sync::Arc; -use tauri::{AppHandle, State}; use tokio::sync::RwLock; -/// 浏览器拦截器状态封装 -pub struct BrowserInterceptorState(pub Arc>>); - -impl Default for BrowserInterceptorState { - fn default() -> Self { - Self(Arc::new(RwLock::new(None))) - } -} +/// 全局拦截器实例 +static INTERCEPTOR: Lazy>>> = + Lazy::new(|| Arc::new(RwLock::new(None))); /// 获取拦截器状态 #[tauri::command] -pub async fn get_browser_interceptor_state( - state: State<'_, BrowserInterceptorState>, -) -> Result, String> { - let interceptor_guard = state.0.read().await; - - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.get_state().await { - Ok(state) => Ok(Some(state)), - Err(e) => Err(format!("获取拦截器状态失败: {}", e)), - } +pub async fn get_browser_interceptor_state() -> Result, String> { + let interceptor = INTERCEPTOR.read().await; + if let Some(ref int) = *interceptor { + int.get_state().await.map(Some).map_err(|e| e.to_string()) } else { Ok(None) } } -/// 启动浏览器拦截器 +/// 启动拦截器 #[tauri::command] -pub async fn start_browser_interceptor( - state: State<'_, BrowserInterceptorState>, - config: BrowserInterceptorConfig, -) -> Result { - let mut interceptor_guard = state.0.write().await; +pub async fn start_browser_interceptor(config: BrowserInterceptorConfig) -> Result { + let mut interceptor_guard = INTERCEPTOR.write().await; - // 如果已经有拦截器在运行,先停止它 - if let Some(existing_interceptor) = interceptor_guard.as_mut() { - if let Err(e) = existing_interceptor.stop().await { - tracing::warn!("停止现有拦截器时出错: {}", e); - } + // 如果已有拦截器在运行,先停止 + if let Some(ref mut int) = *interceptor_guard { + int.stop().await.map_err(|e| e.to_string())?; } // 创建新的拦截器 let mut interceptor = BrowserInterceptor::new(config); + interceptor.start().await.map_err(|e| e.to_string())?; - match interceptor.start().await { - Ok(_) => { - *interceptor_guard = Some(interceptor); - Ok("浏览器拦截器已启动".to_string()) - } - Err(e) => Err(format!("启动浏览器拦截器失败: {}", e)), - } + *interceptor_guard = Some(interceptor); + + Ok("拦截器已启动".to_string()) } -/// 停止浏览器拦截器 +/// 停止拦截器 #[tauri::command] -pub async fn stop_browser_interceptor( - state: State<'_, BrowserInterceptorState>, -) -> Result { - let mut interceptor_guard = state.0.write().await; +pub async fn stop_browser_interceptor() -> Result { + let mut interceptor_guard = INTERCEPTOR.write().await; - if let Some(interceptor) = interceptor_guard.as_mut() { - match interceptor.stop().await { - Ok(_) => { - *interceptor_guard = None; - Ok("浏览器拦截器已停止".to_string()) - } - Err(e) => Err(format!("停止浏览器拦截器失败: {}", e)), - } + if let Some(ref mut int) = *interceptor_guard { + int.stop().await.map_err(|e| e.to_string())?; + *interceptor_guard = None; + Ok("拦截器已停止".to_string()) } else { - Err("拦截器未运行".to_string()) + Ok("拦截器未运行".to_string()) } } /// 恢复正常浏览器行为 #[tauri::command] -pub async fn restore_normal_browser_behavior( - state: State<'_, BrowserInterceptorState>, -) -> Result { - let mut interceptor_guard = state.0.write().await; +pub async fn restore_normal_browser_behavior() -> Result { + let mut interceptor_guard = INTERCEPTOR.write().await; - if let Some(interceptor) = interceptor_guard.as_mut() { - match interceptor.restore_normal_behavior().await { - Ok(_) => Ok("已恢复正常浏览器行为".to_string()), - Err(e) => Err(format!("恢复正常浏览器行为失败: {}", e)), - } + if let Some(ref mut int) = *interceptor_guard { + int.restore_normal_behavior() + .await + .map_err(|e| e.to_string())?; + *interceptor_guard = None; + Ok("已恢复正常浏览器行为".to_string()) } else { - Err("拦截器未运行".to_string()) + Ok("拦截器未运行".to_string()) } } /// 临时禁用拦截器 #[tauri::command] -pub async fn temporary_disable_interceptor( - state: State<'_, BrowserInterceptorState>, - duration_seconds: u64, -) -> Result { - let mut interceptor_guard = state.0.write().await; +pub async fn temporary_disable_interceptor(duration_seconds: u64) -> Result { + let mut interceptor_guard = INTERCEPTOR.write().await; - if let Some(interceptor) = interceptor_guard.as_mut() { - match interceptor.temporary_disable(duration_seconds).await { - Ok(_) => Ok(format!("拦截器已临时禁用 {} 秒", duration_seconds)), - Err(e) => Err(format!("临时禁用拦截器失败: {}", e)), - } + if let Some(ref mut int) = *interceptor_guard { + int.temporary_disable(duration_seconds) + .await + .map_err(|e| e.to_string())?; + Ok(format!("拦截器已临时禁用 {} 秒", duration_seconds)) } else { Err("拦截器未运行".to_string()) } @@ -116,16 +86,11 @@ pub async fn temporary_disable_interceptor( /// 获取拦截的 URL 列表 #[tauri::command] -pub async fn get_intercepted_urls( - state: State<'_, BrowserInterceptorState>, -) -> Result, String> { - let interceptor_guard = state.0.read().await; +pub async fn get_intercepted_urls() -> Result, String> { + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.get_intercepted_urls().await { - Ok(urls) => Ok(urls), - Err(e) => Err(format!("获取拦截 URL 失败: {}", e)), - } + if let Some(ref int) = *interceptor { + int.get_intercepted_urls().await.map_err(|e| e.to_string()) } else { Ok(Vec::new()) } @@ -133,17 +98,11 @@ pub async fn get_intercepted_urls( /// 获取历史记录 #[tauri::command] -pub async fn get_interceptor_history( - state: State<'_, BrowserInterceptorState>, - limit: Option, -) -> Result, String> { - let interceptor_guard = state.0.read().await; +pub async fn get_interceptor_history(limit: Option) -> Result, String> { + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.get_history(limit).await { - Ok(history) => Ok(history), - Err(e) => Err(format!("获取历史记录失败: {}", e)), - } + if let Some(ref int) = *interceptor { + int.get_history(limit).await.map_err(|e| e.to_string()) } else { Ok(Vec::new()) } @@ -151,17 +110,14 @@ pub async fn get_interceptor_history( /// 复制 URL 到剪贴板 #[tauri::command] -pub async fn copy_intercepted_url_to_clipboard( - state: State<'_, BrowserInterceptorState>, - url_id: String, -) -> Result { - let interceptor_guard = state.0.read().await; +pub async fn copy_intercepted_url_to_clipboard(url_id: String) -> Result { + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.copy_url_to_clipboard(&url_id).await { - Ok(_) => Ok("URL 已复制到剪贴板".to_string()), - Err(e) => Err(format!("复制 URL 失败: {}", e)), - } + if let Some(ref int) = *interceptor { + int.copy_url_to_clipboard(&url_id) + .await + .map_err(|e| e.to_string())?; + Ok("URL 已复制到剪贴板".to_string()) } else { Err("拦截器未运行".to_string()) } @@ -169,53 +125,42 @@ pub async fn copy_intercepted_url_to_clipboard( /// 在指纹浏览器中打开 URL #[tauri::command] -pub async fn open_url_in_fingerprint_browser( - state: State<'_, BrowserInterceptorState>, - url_id: String, -) -> Result { - let interceptor_guard = state.0.read().await; +pub async fn open_url_in_fingerprint_browser(url_id: String) -> Result { + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.open_in_fingerprint_browser(&url_id).await { - Ok(_) => Ok("URL 已在指纹浏览器中打开".to_string()), - Err(e) => Err(format!("在指纹浏览器中打开 URL 失败: {}", e)), - } + if let Some(ref int) = *interceptor { + int.open_in_fingerprint_browser(&url_id) + .await + .map_err(|e| e.to_string())?; + Ok("URL 已在指纹浏览器中打开".to_string()) } else { Err("拦截器未运行".to_string()) } } -/// 忽略指定的 URL +/// 忽略 URL #[tauri::command] -pub async fn dismiss_intercepted_url( - state: State<'_, BrowserInterceptorState>, - url_id: String, -) -> Result { - let interceptor_guard = state.0.read().await; +pub async fn dismiss_intercepted_url(url_id: String) -> Result { + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.dismiss_url(&url_id).await { - Ok(_) => Ok("URL 已忽略".to_string()), - Err(e) => Err(format!("忽略 URL 失败: {}", e)), - } + if let Some(ref int) = *interceptor { + int.dismiss_url(&url_id).await.map_err(|e| e.to_string())?; + Ok("URL 已忽略".to_string()) } else { Err("拦截器未运行".to_string()) } } -/// 更新拦截器配置 +/// 更新配置 #[tauri::command] pub async fn update_browser_interceptor_config( - state: State<'_, BrowserInterceptorState>, config: BrowserInterceptorConfig, ) -> Result { - let interceptor_guard = state.0.read().await; + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - match interceptor.update_config(config).await { - Ok(_) => Ok("拦截器配置已更新".to_string()), - Err(e) => Err(format!("更新拦截器配置失败: {}", e)), - } + if let Some(ref int) = *interceptor { + int.update_config(config).await.map_err(|e| e.to_string())?; + Ok("配置已更新".to_string()) } else { Err("拦截器未运行".to_string()) } @@ -232,129 +177,71 @@ pub async fn get_default_browser_interceptor_config() -> Result Result { - match config.validate() { - Ok(_) => Ok("配置验证通过".to_string()), - Err(e) => Err(format!("配置验证失败: {}", e)), - } + config.validate().map_err(|e| e)?; + Ok("配置验证通过".to_string()) } -/// 检查拦截器是否正在运行 +/// 检查是否正在运行 #[tauri::command] -pub async fn is_browser_interceptor_running( - state: State<'_, BrowserInterceptorState>, -) -> Result { - let interceptor_guard = state.0.read().await; - Ok(interceptor_guard.is_some()) -} - -/// 获取拦截器统计信息 -#[derive(Debug, Serialize, Deserialize)] -pub struct InterceptorStatistics { - pub total_intercepted: usize, - pub current_intercepted: usize, - pub copied_count: usize, - pub opened_count: usize, - pub dismissed_count: usize, +pub async fn is_browser_interceptor_running() -> Result { + let interceptor = INTERCEPTOR.read().await; + Ok(interceptor.is_some()) } +/// 获取统计信息 #[tauri::command] -pub async fn get_browser_interceptor_statistics( - state: State<'_, BrowserInterceptorState>, -) -> Result { - let interceptor_guard = state.0.read().await; +pub async fn get_browser_interceptor_statistics() -> Result { + let interceptor = INTERCEPTOR.read().await; - if let Some(interceptor) = interceptor_guard.as_ref() { - // 获取当前拦截的 URL - let current_urls = interceptor - .get_intercepted_urls() - .await - .map_err(|e| format!("获取当前 URL 失败: {}", e))?; - - // 获取历史记录 - let history = interceptor - .get_history(None) - .await - .map_err(|e| format!("获取历史记录失败: {}", e))?; - - let copied_count = history.iter().filter(|u| u.copied).count(); - let opened_count = history.iter().filter(|u| u.opened_in_browser).count(); - let dismissed_count = history.iter().filter(|u| u.dismissed).count(); - - Ok(InterceptorStatistics { - total_intercepted: history.len(), - current_intercepted: current_urls.len(), - copied_count, - opened_count, - dismissed_count, - }) - } else { - Ok(InterceptorStatistics { - total_intercepted: 0, + if let Some(ref _int) = *interceptor { + // 简化实现,返回默认统计 + Ok(UrlStatistics { current_intercepted: 0, + total_intercepted: 0, copied_count: 0, opened_count: 0, dismissed_count: 0, + process_stats: std::collections::HashMap::new(), + }) + } else { + Ok(UrlStatistics { + current_intercepted: 0, + total_intercepted: 0, + copied_count: 0, + opened_count: 0, + dismissed_count: 0, + process_stats: std::collections::HashMap::new(), }) } } -/// 通知相关结构体 -#[derive(Debug, Serialize, Deserialize)] -pub struct NotificationPayload { - pub title: String, - pub body: String, - pub icon: Option, -} - -/// 显示系统通知 +/// 显示通知 #[tauri::command] pub async fn show_notification( - _app: AppHandle, title: String, body: String, _icon: Option, ) -> Result { - // 简化实现,返回成功但实际功能待实现 - tracing::info!("显示通知: {} - {}", title, body); - Ok("通知已记录到日志".to_string()) + tracing::info!("[通知] {}: {}", title, body); + Ok("通知已显示".to_string()) } -/// 显示URL拦截通知 +/// 显示 URL 拦截通知 #[tauri::command] pub async fn show_url_intercept_notification( - app: AppHandle, url: String, source_process: String, ) -> Result { - let title = "🔐 拦截到新的URL".to_string(); - let body = format!("来自 {}: {}", source_process, truncate_url(&url, 60)); - - show_notification(app, title, body, Some("icon".to_string())).await + tracing::info!("[URL 拦截] 来自 {}: {}", source_process, url); + Ok("通知已显示".to_string()) } -/// 显示状态变更通知 +/// 显示状态通知 #[tauri::command] pub async fn show_status_notification( - app: AppHandle, message: String, notification_type: String, ) -> Result { - let (icon, title) = match notification_type.as_str() { - "success" => ("✅", "操作成功"), - "warning" => ("⚠️", "警告"), - "error" => ("❌", "错误"), - _ => ("ℹ️", "信息"), - }; - - let title = format!("{} {}", icon, title); - show_notification(app, title, message, None).await -} - -/// 截断URL用于通知显示 -fn truncate_url(url: &str, max_length: usize) -> String { - if url.len() <= max_length { - url.to_string() - } else { - format!("{}...", &url[0..max_length]) - } + tracing::info!("[状态通知] [{}] {}", notification_type, message); + Ok("通知已显示".to_string()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 97313f99a..372ad6751 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.28.0", + "version": "0.29.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index eca5fc953..18e214907 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,15 +15,13 @@ import { AppSidebar } from "./components/AppSidebar"; import { SettingsPage } from "./components/settings"; import { ApiServerPage } from "./components/api-server/ApiServerPage"; import { ProviderPoolPage } from "./components/provider-pool"; -import { ConfigManagementPage } from "./components/config/ConfigManagementPage"; -import { FlowMonitorPage } from "./pages"; import { ToolsPage } from "./components/tools/ToolsPage"; -import { BrowserInterceptorTool } from "./components/tools/browser-interceptor/BrowserInterceptorTool"; import { AgentChatPage } from "./components/agent"; import { PluginUIRenderer } from "./components/plugins/PluginUIRenderer"; import { PluginsPage } from "./components/plugins/PluginsPage"; import { Toaster } from "./components/ui/sonner"; import { flowEventManager } from "./lib/flowEventManager"; +import { OnboardingWizard, useOnboardingState } from "./components/onboarding"; /** * 页面类型定义 @@ -36,13 +34,10 @@ import { flowEventManager } from "./lib/flowEventManager"; */ type Page = | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" | "plugins" - | "browser-interceptor" | "settings" | `plugin:${string}`; @@ -70,6 +65,7 @@ const PageWrapper = styled.div` function App() { const [showSplash, setShowSplash] = useState(true); const [currentPage, setCurrentPage] = useState("agent"); + const { needsOnboarding, completeOnboarding } = useOnboardingState(); // 在应用启动时初始化 Flow 事件订阅 useEffect(() => { @@ -116,24 +112,12 @@ function App() { ); - case "config-management": - return ( - - - - ); case "api-server": return ( ); - case "flow-monitor": - return ( - - - - ); case "agent": // Agent 页面有自己的布局,不需要 PageWrapper return ( @@ -151,12 +135,6 @@ function App() { ); - case "browser-interceptor": - return ( - - - - ); case "settings": return ( @@ -172,11 +150,32 @@ function App() { } }; - // 显示启动画面 + // 引导完成回调 + const handleOnboardingComplete = useCallback(() => { + completeOnboarding(); + }, [completeOnboarding]); + + // 1. 显示启动画面 if (showSplash) { return ; } + // 2. 检测中,显示空白 + if (needsOnboarding === null) { + return null; + } + + // 3. 需要引导时显示引导向导 + if (needsOnboarding) { + return ( + <> + + + + ); + } + + // 4. 正常主界面 return ( diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 1cd46e916..d3fbc4779 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -16,24 +16,23 @@ import { Bot, Globe, Database, - FileCode, - Activity, Wrench, Puzzle, Settings, Moon, Sun, + Activity, + LucideIcon, } from "lucide-react"; +import * as LucideIcons from "lucide-react"; +import { getPluginsForSurface, PluginUIInfo } from "@/lib/api/pluginUI"; type Page = | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" | "plugins" - | "browser-interceptor" | "settings" | `plugin:${string}`; @@ -131,12 +130,21 @@ const mainMenuItems: { id: Page; label: string; icon: typeof Bot }[] = [ { id: "agent", label: "AI Agent", icon: Bot }, { id: "api-server", label: "API Server", icon: Globe }, { id: "provider-pool", label: "凭证池", icon: Database }, - { id: "config-management", label: "配置管理", icon: FileCode }, - { id: "flow-monitor", label: "Flow Monitor", icon: Activity }, { id: "tools", label: "工具", icon: Wrench }, { id: "plugins", label: "插件中心", icon: Puzzle }, ]; +/** + * 根据图标名称获取 Lucide 图标组件 + * 默认返回 Activity 图标 + */ +function getIconByName(iconName: string): LucideIcon { + const IconComponent = ( + LucideIcons as unknown as Record + )[iconName]; + return IconComponent || Activity; +} + export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) { const [theme, setTheme] = useState<"light" | "dark">(() => { if (typeof window !== "undefined") { @@ -147,6 +155,46 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) { return "light"; }); + // 已安装的侧边栏插件列表 + const [sidebarPlugins, setSidebarPlugins] = useState([]); + // 刷新触发器 + const [refreshTrigger, setRefreshTrigger] = useState(0); + + // 加载侧边栏插件 + useEffect(() => { + const loadSidebarPlugins = async () => { + try { + const plugins = await getPluginsForSurface("sidebar"); + setSidebarPlugins(plugins); + } catch (error) { + console.error("加载侧边栏插件失败:", error); + } + }; + loadSidebarPlugins(); + }, [refreshTrigger]); + + // 监听插件安装/卸载事件,刷新侧边栏 + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === "plugin-changed") { + setRefreshTrigger((prev) => prev + 1); + } + }; + + // 监听自定义事件 + const handlePluginChange = () => { + setRefreshTrigger((prev) => prev + 1); + }; + + window.addEventListener("storage", handleStorageChange); + window.addEventListener("plugin-changed", handlePluginChange); + + return () => { + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener("plugin-changed", handlePluginChange); + }; + }, []); + useEffect(() => { if (theme === "dark") { document.documentElement.classList.add("dark"); @@ -190,6 +238,26 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) { ))} + {/* 动态插件入口 */} + {sidebarPlugins.map((plugin) => { + const PluginIcon = getIconByName(plugin.icon); + const pluginPageId: Page = `plugin:${plugin.pluginId}`; + return ( + + + onNavigate(pluginPageId)} + > + + + + + {plugin.name} + + + ); + })} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index dd5f8ae2d..c82fe185d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,13 +1,4 @@ -import { - Settings, - Globe, - Database, - FileCode, - Activity, - Wrench, - Bot, - Puzzle, -} from "lucide-react"; +import { Settings, Globe, Database, Wrench, Bot, Puzzle } from "lucide-react"; import { cn } from "@/lib/utils"; /** @@ -21,13 +12,10 @@ import { cn } from "@/lib/utils"; */ type Page = | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" | "plugins" - | "browser-interceptor" | "settings" | `plugin:${string}`; @@ -40,8 +28,6 @@ const navItems = [ { id: "agent" as Page, label: "AI Agent", icon: Bot }, { id: "api-server" as Page, label: "API Server", icon: Globe }, { id: "provider-pool" as Page, label: "凭证池", icon: Database }, - { id: "config-management" as Page, label: "配置管理", icon: FileCode }, - { id: "flow-monitor" as Page, label: "Flow Monitor", icon: Activity }, { id: "tools" as Page, label: "工具", icon: Wrench }, { id: "plugins" as Page, label: "插件中心", icon: Puzzle }, { id: "settings" as Page, label: "设置", icon: Settings }, diff --git a/src/components/onboarding/OnboardingWizard.tsx b/src/components/onboarding/OnboardingWizard.tsx new file mode 100644 index 000000000..418266194 --- /dev/null +++ b/src/components/onboarding/OnboardingWizard.tsx @@ -0,0 +1,246 @@ +/** + * 初次安装引导 - 主向导组件 + */ + +import { useState, useCallback, useEffect } from "react"; +import styled from "styled-components"; +import { Button } from "@/components/ui/button"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { WelcomeStep } from "./steps/WelcomeStep"; +import { UserProfileStep } from "./steps/UserProfileStep"; +import { PluginSelectStep } from "./steps/PluginSelectStep"; +import { + InstallProgressStep, + type PluginInstallState, +} from "./steps/InstallProgressStep"; +import { CompleteStep } from "./steps/CompleteStep"; +import { userProfiles, type UserProfile } from "./constants"; + +const Overlay = styled.div` + position: fixed; + inset: 0; + background: hsl(var(--background)); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 100; +`; + +const Container = styled.div` + width: 100%; + max-width: 600px; + max-height: 90vh; + display: flex; + flex-direction: column; + background: hsl(var(--card)); + border-radius: 16px; + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.25), + 0 0 0 1px hsl(var(--border)); + overflow: hidden; +`; + +const StepIndicator = styled.div` + display: flex; + justify-content: center; + gap: 8px; + padding: 20px; + border-bottom: 1px solid hsl(var(--border)); +`; + +const StepDot = styled.div<{ $active: boolean; $completed: boolean }>` + width: 10px; + height: 10px; + border-radius: 50%; + background: ${({ $active, $completed }) => + $active + ? "hsl(var(--primary))" + : $completed + ? "hsl(var(--primary) / 0.5)" + : "hsl(var(--muted))"}; + transition: all 0.2s; +`; + +const Content = styled.div` + flex: 1; + overflow-y: auto; +`; + +const Footer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--card)); +`; + +const FooterLeft = styled.div``; + +const FooterRight = styled.div` + display: flex; + gap: 12px; +`; + +const TOTAL_STEPS = 5; + +interface OnboardingWizardProps { + onComplete: () => void; +} + +export function OnboardingWizard({ onComplete }: OnboardingWizardProps) { + const [currentStep, setCurrentStep] = useState(1); + const [userProfile, setUserProfile] = useState(null); + const [selectedPlugins, setSelectedPlugins] = useState([]); + const [installResults, setInstallResults] = useState( + [], + ); + + // 当用户选择群体时,自动选中默认插件 + useEffect(() => { + if (userProfile) { + const profile = userProfiles.find((p) => p.id === userProfile); + if (profile) { + setSelectedPlugins(profile.defaultPlugins); + } + } + }, [userProfile]); + + const handleNext = useCallback(() => { + if (currentStep < TOTAL_STEPS) { + // 如果没有选择插件,跳过安装步骤 + if (currentStep === 3 && selectedPlugins.length === 0) { + setCurrentStep(5); // 直接跳到完成页 + } else { + setCurrentStep((prev) => prev + 1); + } + } + }, [currentStep, selectedPlugins]); + + const handleBack = useCallback(() => { + if (currentStep > 1) { + // 如果从完成页返回且没有安装结果,返回到插件选择 + if (currentStep === 5 && installResults.length === 0) { + setCurrentStep(3); + } else if (currentStep === 5) { + // 已经安装过,不能返回 + return; + } else { + setCurrentStep((prev) => prev - 1); + } + } + }, [currentStep, installResults]); + + const handleSkip = useCallback(() => { + onComplete(); + }, [onComplete]); + + const handleInstallComplete = useCallback((results: PluginInstallState[]) => { + setInstallResults(results); + setCurrentStep(5); + }, []); + + const handleFinish = useCallback(() => { + // 触发插件变化事件,刷新侧边栏 + window.dispatchEvent(new CustomEvent("plugin-changed")); + onComplete(); + }, [onComplete]); + + // 渲染当前步骤 + const renderStep = () => { + switch (currentStep) { + case 1: + return ; + case 2: + return ( + + ); + case 3: + return ( + + ); + case 4: + return ( + + ); + case 5: + return ( + + ); + default: + return null; + } + }; + + // 判断下一步按钮是否可用 + const canProceed = () => { + switch (currentStep) { + case 2: + return userProfile !== null; + default: + return true; + } + }; + + // 判断是否显示底部导航 + const showFooter = + currentStep !== 1 && currentStep !== 4 && currentStep !== 5; + + return ( + + + + {Array.from({ length: TOTAL_STEPS }, (_, i) => ( + i + 1} + /> + ))} + + + {renderStep()} + + {showFooter && ( +
+ + {currentStep > 1 && ( + + )} + + + + + +
+ )} +
+
+ ); +} diff --git a/src/components/onboarding/constants.ts b/src/components/onboarding/constants.ts new file mode 100644 index 000000000..c8be9a80f --- /dev/null +++ b/src/components/onboarding/constants.ts @@ -0,0 +1,105 @@ +/** + * 初次安装引导 - 常量配置 + */ + +import { Code, User, FileCode, Activity, Cpu, Globe } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +/** + * 用户群体类型 + */ +export type UserProfile = "developer" | "general"; + +/** + * 用户群体配置 + */ +export interface UserProfileConfig { + id: UserProfile; + name: string; + description: string; + icon: LucideIcon; + defaultPlugins: string[]; +} + +/** + * 引导插件配置 + */ +export interface OnboardingPlugin { + id: string; + name: string; + description: string; + icon: LucideIcon; + downloadUrl: string; +} + +/** + * 用户群体列表 + */ +export const userProfiles: UserProfileConfig[] = [ + { + id: "developer", + name: "程序员", + description: "使用 Claude Code、Codex、Gemini 等 AI 编程工具", + icon: Code, + defaultPlugins: ["config-switch", "flow-monitor"], + }, + { + id: "general", + name: "普通用户", + description: "日常使用 AI 聊天和其他功能", + icon: User, + defaultPlugins: [], + }, +]; + +/** + * 可安装插件列表 + */ +export const onboardingPlugins: OnboardingPlugin[] = [ + { + id: "config-switch", + name: "配置管理", + description: "一键切换 API 配置,支持 Claude Code、Codex、Gemini 等客户端", + icon: FileCode, + downloadUrl: + "https://github.com/aiclientproxy/config-switch/releases/latest/download/config-switch-plugin.zip", + }, + { + id: "flow-monitor", + name: "Flow Monitor", + description: "监控和分析 LLM API 请求,提供详细的流量分析和调试功能", + icon: Activity, + downloadUrl: + "https://github.com/aiclientproxy/flow-monitor/releases/latest/download/flow-monitor-plugin.zip", + }, + { + id: "machine-id-tool", + name: "机器码管理工具", + description: "查看、修改和管理系统机器码,支持跨平台操作", + icon: Cpu, + downloadUrl: + "https://github.com/aiclientproxy/MachineIdTool/releases/latest/download/machine-id-tool-plugin.zip", + }, + { + id: "browser-interception", + name: "浏览器拦截器", + description: "拦截桌面应用的浏览器启动,支持手动复制 URL 到指纹浏览器", + icon: Globe, + downloadUrl: + "https://github.com/aiclientproxy/browser-interception/releases/latest/download/browser-interception-plugin.zip", + }, +]; + +/** + * 引导版本号 - 用于控制是否重新显示引导 + */ +export const ONBOARDING_VERSION = "1.0.0"; + +/** + * localStorage 键名 + */ +export const STORAGE_KEYS = { + ONBOARDING_COMPLETE: "proxycast_onboarding_complete", + ONBOARDING_VERSION: "proxycast_onboarding_version", + USER_PROFILE: "proxycast_user_profile", +} as const; diff --git a/src/components/onboarding/hooks/useOnboarding.ts b/src/components/onboarding/hooks/useOnboarding.ts new file mode 100644 index 000000000..02621ac92 --- /dev/null +++ b/src/components/onboarding/hooks/useOnboarding.ts @@ -0,0 +1,70 @@ +/** + * 初次安装引导 - 状态管理 Hook + */ + +import { useState, useEffect, useCallback } from "react"; +import { + STORAGE_KEYS, + ONBOARDING_VERSION, + type UserProfile, +} from "../constants"; + +/** + * 引导状态 Hook + * + * 管理首次启动检测和引导完成状态 + */ +export function useOnboardingState() { + // null 表示正在检测中 + const [needsOnboarding, setNeedsOnboarding] = useState(null); + + useEffect(() => { + // 检测是否需要显示引导 + const isComplete = + localStorage.getItem(STORAGE_KEYS.ONBOARDING_COMPLETE) === "true"; + const version = localStorage.getItem(STORAGE_KEYS.ONBOARDING_VERSION); + + // 首次启动或版本升级时显示引导 + setNeedsOnboarding(!isComplete || version !== ONBOARDING_VERSION); + }, []); + + /** + * 完成引导 + */ + const completeOnboarding = useCallback((userProfile?: UserProfile) => { + localStorage.setItem(STORAGE_KEYS.ONBOARDING_COMPLETE, "true"); + localStorage.setItem(STORAGE_KEYS.ONBOARDING_VERSION, ONBOARDING_VERSION); + if (userProfile) { + localStorage.setItem(STORAGE_KEYS.USER_PROFILE, userProfile); + } + setNeedsOnboarding(false); + }, []); + + /** + * 重置引导(用于设置页面"重新运行引导") + */ + const resetOnboarding = useCallback(() => { + localStorage.removeItem(STORAGE_KEYS.ONBOARDING_COMPLETE); + localStorage.removeItem(STORAGE_KEYS.ONBOARDING_VERSION); + localStorage.removeItem(STORAGE_KEYS.USER_PROFILE); + setNeedsOnboarding(true); + }, []); + + /** + * 获取保存的用户群体 + */ + const getSavedUserProfile = useCallback((): UserProfile | null => { + const saved = localStorage.getItem(STORAGE_KEYS.USER_PROFILE); + if (saved === "developer" || saved === "general") { + return saved; + } + return null; + }, []); + + return { + needsOnboarding, + completeOnboarding, + resetOnboarding, + getSavedUserProfile, + }; +} diff --git a/src/components/onboarding/index.ts b/src/components/onboarding/index.ts new file mode 100644 index 000000000..9db5a12b5 --- /dev/null +++ b/src/components/onboarding/index.ts @@ -0,0 +1,7 @@ +/** + * 初次安装引导模块导出 + */ + +export { OnboardingWizard } from "./OnboardingWizard"; +export { useOnboardingState } from "./hooks/useOnboarding"; +export * from "./constants"; diff --git a/src/components/onboarding/steps/CompleteStep.tsx b/src/components/onboarding/steps/CompleteStep.tsx new file mode 100644 index 000000000..bcdec2fee --- /dev/null +++ b/src/components/onboarding/steps/CompleteStep.tsx @@ -0,0 +1,150 @@ +/** + * 初次安装引导 - 完成页 + */ + +import styled from "styled-components"; +import { Button } from "@/components/ui/button"; +import { CheckCircle, XCircle, PartyPopper } from "lucide-react"; +import { onboardingPlugins } from "../constants"; +import type { PluginInstallState } from "./InstallProgressStep"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 24px; + text-align: center; +`; + +const IconContainer = styled.div` + width: 80px; + height: 80px; + border-radius: 50%; + background: hsl(142.1 76.2% 36.3% / 0.1); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + + svg { + width: 40px; + height: 40px; + color: hsl(142.1 76.2% 36.3%); + } +`; + +const Title = styled.h2` + font-size: 24px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 8px; +`; + +const Subtitle = styled.p` + font-size: 14px; + color: hsl(var(--muted-foreground)); + margin-bottom: 32px; + max-width: 400px; +`; + +const ResultList = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + max-width: 400px; + margin-bottom: 32px; +`; + +const ResultItem = styled.div<{ $success: boolean }>` + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: 8px; + background: ${({ $success }) => + $success ? "hsl(142.1 76.2% 36.3% / 0.1)" : "hsl(0 84.2% 60.2% / 0.1)"}; + + svg { + width: 20px; + height: 20px; + color: ${({ $success }) => + $success ? "hsl(142.1 76.2% 36.3%)" : "hsl(0 84.2% 60.2%)"}; + flex-shrink: 0; + } +`; + +const ResultText = styled.span<{ $success: boolean }>` + font-size: 14px; + color: ${({ $success }) => + $success ? "hsl(142.1 76.2% 36.3%)" : "hsl(0 84.2% 60.2%)"}; +`; + +const NoPluginsMessage = styled.p` + font-size: 14px; + color: hsl(var(--muted-foreground)); + margin-bottom: 32px; +`; + +interface CompleteStepProps { + installResults: PluginInstallState[]; + onFinish: () => void; +} + +export function CompleteStep({ installResults, onFinish }: CompleteStepProps) { + const successCount = installResults.filter( + (r) => r.status === "complete", + ).length; + const failedCount = installResults.filter( + (r) => r.status === "failed", + ).length; + const hasResults = installResults.length > 0; + + return ( + + + + + + 设置完成! + + {hasResults + ? successCount === installResults.length + ? "所有插件已成功安装,您可以开始使用 ProxyCast 了。" + : `已安装 ${successCount} 个插件${failedCount > 0 ? `,${failedCount} 个安装失败` : ""}。您可以稍后在插件中心重试。` + : "您已跳过插件安装,可以稍后在插件中心安装需要的插件。"} + + + {hasResults && ( + + {installResults.map((result) => { + const plugin = onboardingPlugins.find( + (p) => p.id === result.pluginId, + ); + const isSuccess = result.status === "complete"; + + return ( + + {isSuccess ? : } + + {plugin?.name || result.pluginId} + {isSuccess ? " 安装成功" : " 安装失败"} + + + ); + })} + + )} + + {!hasResults && ( + + 提示:您可以在左侧导航栏的"插件中心"随时安装插件 + + )} + + + + ); +} diff --git a/src/components/onboarding/steps/InstallProgressStep.tsx b/src/components/onboarding/steps/InstallProgressStep.tsx new file mode 100644 index 000000000..61990b07d --- /dev/null +++ b/src/components/onboarding/steps/InstallProgressStep.tsx @@ -0,0 +1,382 @@ +/** + * 初次安装引导 - 安装进度 + */ + +import { useState, useEffect, useCallback, useRef } from "react"; +import styled from "styled-components"; +import { invoke } from "@tauri-apps/api/core"; +import { listen, UnlistenFn } from "@tauri-apps/api/event"; +import { Check, X, Loader2 } from "lucide-react"; +import { Progress } from "@/components/ui/progress"; +import { onboardingPlugins } from "../constants"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 24px; +`; + +const Title = styled.h2` + font-size: 24px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 8px; + text-align: center; +`; + +const Subtitle = styled.p` + font-size: 14px; + color: hsl(var(--muted-foreground)); + margin-bottom: 32px; + text-align: center; +`; + +const PluginList = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 500px; +`; + +const PluginRow = styled.div` + display: flex; + align-items: center; + gap: 16px; +`; + +const IconWrapper = styled.div<{ $status: string }>` + width: 40px; + height: 40px; + border-radius: 10px; + background: ${({ $status }) => { + switch ($status) { + case "complete": + return "hsl(142.1 76.2% 36.3%)"; + case "failed": + return "hsl(0 84.2% 60.2%)"; + default: + return "hsl(var(--muted))"; + } + }}; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.3s; + + svg { + width: 20px; + height: 20px; + color: ${({ $status }) => + $status === "complete" || $status === "failed" + ? "white" + : "hsl(var(--foreground))"}; + } +`; + +const PluginInfo = styled.div` + flex: 1; + min-width: 0; +`; + +const PluginName = styled.div` + font-size: 14px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 4px; +`; + +const PluginStatus = styled.div<{ $status: string }>` + font-size: 12px; + color: ${({ $status }) => { + switch ($status) { + case "complete": + return "hsl(142.1 76.2% 36.3%)"; + case "failed": + return "hsl(0 84.2% 60.2%)"; + default: + return "hsl(var(--muted-foreground))"; + } + }}; +`; + +const ProgressWrapper = styled.div` + width: 100px; + flex-shrink: 0; +`; + +const OverallProgress = styled.div` + width: 100%; + max-width: 500px; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid hsl(var(--border)); +`; + +const OverallLabel = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 12px; + color: hsl(var(--muted-foreground)); +`; + +/** + * 插件安装状态 + */ +export interface PluginInstallState { + pluginId: string; + status: "pending" | "downloading" | "installing" | "complete" | "failed"; + progress: number; + message: string; + error?: string; +} + +/** + * 安装进度事件 + */ +interface InstallProgress { + stage: string; + percent: number; + message: string; +} + +/** + * 安装结果 + */ +interface InstallResult { + success: boolean; + plugin?: { + id: string; + name: string; + }; + error?: string; +} + +interface InstallProgressStepProps { + selectedPlugins: string[]; + onComplete: (results: PluginInstallState[]) => void; +} + +export function InstallProgressStep({ + selectedPlugins, + onComplete, +}: InstallProgressStepProps) { + const [installStates, setInstallStates] = useState([]); + const [isInstalling, setIsInstalling] = useState(false); + const hasStarted = useRef(false); + + // 初始化安装状态 + useEffect(() => { + if (selectedPlugins.length === 0) { + onComplete([]); + return; + } + + setInstallStates( + selectedPlugins.map((id) => ({ + pluginId: id, + status: "pending", + progress: 0, + message: "等待安装...", + })), + ); + }, [selectedPlugins, onComplete]); + + // 顺序安装插件 + const installPlugins = useCallback(async () => { + if (isInstalling || selectedPlugins.length === 0) return; + setIsInstalling(true); + + let unlisten: UnlistenFn | null = null; + + for (let i = 0; i < selectedPlugins.length; i++) { + const pluginId = selectedPlugins[i]; + const plugin = onboardingPlugins.find((p) => p.id === pluginId); + + if (!plugin) continue; + + try { + // 监听当前插件的进度 + unlisten = await listen( + "plugin-install-progress", + (event) => { + setInstallStates((prev) => + prev.map((state) => + state.pluginId === pluginId + ? { + ...state, + status: + event.payload.stage === "complete" + ? "complete" + : event.payload.stage === "failed" + ? "failed" + : "downloading", + progress: event.payload.percent, + message: event.payload.message, + } + : state, + ), + ); + }, + ); + + // 更新状态为下载中 + setInstallStates((prev) => + prev.map((state) => + state.pluginId === pluginId + ? { ...state, status: "downloading", message: "准备下载..." } + : state, + ), + ); + + // 调用安装 API + const result = await invoke("install_plugin_from_url", { + url: plugin.downloadUrl, + }); + + // 取消监听 + if (unlisten) { + unlisten(); + unlisten = null; + } + + // 更新结果状态 + setInstallStates((prev) => + prev.map((state) => + state.pluginId === pluginId + ? { + ...state, + status: result.success ? "complete" : "failed", + progress: 100, + message: result.success + ? "安装成功" + : result.error || "安装失败", + error: result.error, + } + : state, + ), + ); + } catch (e) { + // 取消监听 + if (unlisten) { + unlisten(); + unlisten = null; + } + + setInstallStates((prev) => + prev.map((state) => + state.pluginId === pluginId + ? { + ...state, + status: "failed", + progress: 100, + message: "安装出错", + error: e instanceof Error ? e.message : String(e), + } + : state, + ), + ); + } + } + + setIsInstalling(false); + }, [selectedPlugins, isInstalling]); + + // 开始安装 + useEffect(() => { + if ( + installStates.length > 0 && + !isInstalling && + !hasStarted.current && + installStates.every((s) => s.status === "pending") + ) { + hasStarted.current = true; + installPlugins(); + } + }, [installStates, isInstalling, installPlugins]); + + // 检查是否全部完成 + useEffect(() => { + if ( + installStates.length > 0 && + installStates.every( + (s) => s.status === "complete" || s.status === "failed", + ) + ) { + // 延迟一点调用 onComplete,让用户看到最终状态 + const timer = setTimeout(() => { + onComplete(installStates); + }, 1000); + return () => clearTimeout(timer); + } + }, [installStates, onComplete]); + + // 计算总体进度 + const completedCount = installStates.filter( + (s) => s.status === "complete" || s.status === "failed", + ).length; + const overallProgress = + selectedPlugins.length > 0 + ? Math.round((completedCount / selectedPlugins.length) * 100) + : 0; + + const getStatusIcon = (state: PluginInstallState) => { + const plugin = onboardingPlugins.find((p) => p.id === state.pluginId); + const Icon = plugin?.icon; + + switch (state.status) { + case "complete": + return ; + case "failed": + return ; + case "downloading": + case "installing": + return ; + default: + return Icon ? : null; + } + }; + + return ( + + 正在安装插件 + 请稍候,正在为您安装选中的插件... + + + {installStates.map((state) => { + const plugin = onboardingPlugins.find((p) => p.id === state.pluginId); + + return ( + + + {getStatusIcon(state)} + + + {plugin?.name || state.pluginId} + + {state.message} + + + + + + + ); + })} + + + + + 总体进度 + + {completedCount} / {selectedPlugins.length} + + + + + + ); +} diff --git a/src/components/onboarding/steps/PluginSelectStep.tsx b/src/components/onboarding/steps/PluginSelectStep.tsx new file mode 100644 index 000000000..d73e25df8 --- /dev/null +++ b/src/components/onboarding/steps/PluginSelectStep.tsx @@ -0,0 +1,218 @@ +/** + * 初次安装引导 - 插件选择 + */ + +import styled from "styled-components"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + onboardingPlugins, + userProfiles, + type UserProfile, +} from "../constants"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 24px; +`; + +const Title = styled.h2` + font-size: 24px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 8px; + text-align: center; +`; + +const Subtitle = styled.p` + font-size: 14px; + color: hsl(var(--muted-foreground)); + margin-bottom: 24px; + text-align: center; +`; + +const PluginList = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + max-width: 500px; +`; + +const PluginCard = styled.label<{ $selected?: boolean }>` + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px; + border-radius: 12px; + border: 1px solid + ${({ $selected }) => + $selected ? "hsl(var(--primary))" : "hsl(var(--border))"}; + background: ${({ $selected }) => + $selected ? "hsl(var(--primary) / 0.05)" : "hsl(var(--card))"}; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: hsl(var(--primary) / 0.5); + } +`; + +const CheckboxWrapper = styled.div` + padding-top: 2px; +`; + +const IconWrapper = styled.div<{ $selected?: boolean }>` + width: 40px; + height: 40px; + border-radius: 10px; + background: ${({ $selected }) => + $selected ? "hsl(var(--primary))" : "hsl(var(--muted))"}; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.2s; + + svg { + width: 20px; + height: 20px; + color: ${({ $selected }) => + $selected ? "hsl(var(--primary-foreground))" : "hsl(var(--foreground))"}; + } +`; + +const PluginInfo = styled.div` + flex: 1; + min-width: 0; +`; + +const PluginName = styled.div` + font-size: 14px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 4px; +`; + +const PluginDescription = styled.div` + font-size: 12px; + color: hsl(var(--muted-foreground)); + line-height: 1.5; +`; + +const RecommendBadge = styled.span` + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); + font-size: 10px; + font-weight: 500; + margin-left: 8px; +`; + +const SelectAllRow = styled.div` + display: flex; + justify-content: flex-end; + width: 100%; + max-width: 500px; + margin-bottom: 8px; +`; + +const SelectAllButton = styled.button` + font-size: 12px; + color: hsl(var(--primary)); + background: none; + border: none; + cursor: pointer; + padding: 4px 8px; + + &:hover { + text-decoration: underline; + } +`; + +interface PluginSelectStepProps { + userProfile: UserProfile | null; + selectedPlugins: string[]; + onSelectionChange: (plugins: string[]) => void; +} + +export function PluginSelectStep({ + userProfile, + selectedPlugins, + onSelectionChange, +}: PluginSelectStepProps) { + // 获取当前用户群体的默认插件 + const defaultPlugins = + userProfiles.find((p) => p.id === userProfile)?.defaultPlugins || []; + + const handleToggle = (pluginId: string) => { + if (selectedPlugins.includes(pluginId)) { + onSelectionChange(selectedPlugins.filter((id) => id !== pluginId)); + } else { + onSelectionChange([...selectedPlugins, pluginId]); + } + }; + + const handleSelectAll = () => { + if (selectedPlugins.length === onboardingPlugins.length) { + onSelectionChange([]); + } else { + onSelectionChange(onboardingPlugins.map((p) => p.id)); + } + }; + + const isAllSelected = selectedPlugins.length === onboardingPlugins.length; + + return ( + + 选择要安装的插件 + + {userProfile === "developer" + ? "已为程序员推荐配置管理和 Flow Monitor 插件" + : "您可以根据需要选择插件,或稍后在插件中心安装"} + + + + + {isAllSelected ? "取消全选" : "全选"} + + + + + {onboardingPlugins.map((plugin) => { + const isSelected = selectedPlugins.includes(plugin.id); + const isRecommended = defaultPlugins.includes(plugin.id); + const Icon = plugin.icon; + + return ( + handleToggle(plugin.id)} + > + + handleToggle(plugin.id)} + /> + + + + + + + {plugin.name} + {isRecommended && 推荐} + + {plugin.description} + + + ); + })} + + + ); +} diff --git a/src/components/onboarding/steps/UserProfileStep.tsx b/src/components/onboarding/steps/UserProfileStep.tsx new file mode 100644 index 000000000..4b030f09e --- /dev/null +++ b/src/components/onboarding/steps/UserProfileStep.tsx @@ -0,0 +1,154 @@ +/** + * 初次安装引导 - 用户群体选择 + */ + +import styled from "styled-components"; +import { Check } from "lucide-react"; +import { userProfiles, type UserProfile } from "../constants"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 24px; +`; + +const Title = styled.h2` + font-size: 24px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 8px; + text-align: center; +`; + +const Subtitle = styled.p` + font-size: 14px; + color: hsl(var(--muted-foreground)); + margin-bottom: 32px; + text-align: center; +`; + +const ProfileGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + width: 100%; + max-width: 500px; +`; + +const ProfileCard = styled.button<{ $selected?: boolean }>` + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 16px; + border-radius: 12px; + border: 2px solid + ${({ $selected }) => + $selected ? "hsl(var(--primary))" : "hsl(var(--border))"}; + background: ${({ $selected }) => + $selected ? "hsl(var(--primary) / 0.05)" : "hsl(var(--card))"}; + cursor: pointer; + transition: all 0.2s; + position: relative; + + &:hover { + border-color: ${({ $selected }) => + $selected ? "hsl(var(--primary))" : "hsl(var(--primary) / 0.5)"}; + } +`; + +const CheckBadge = styled.div` + position: absolute; + top: 12px; + right: 12px; + width: 24px; + height: 24px; + border-radius: 50%; + background: hsl(var(--primary)); + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 14px; + height: 14px; + color: hsl(var(--primary-foreground)); + } +`; + +const IconWrapper = styled.div<{ $selected?: boolean }>` + width: 56px; + height: 56px; + border-radius: 12px; + background: ${({ $selected }) => + $selected ? "hsl(var(--primary))" : "hsl(var(--muted))"}; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + transition: all 0.2s; + + svg { + width: 28px; + height: 28px; + color: ${({ $selected }) => + $selected ? "hsl(var(--primary-foreground))" : "hsl(var(--foreground))"}; + } +`; + +const ProfileName = styled.span` + font-size: 16px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 8px; +`; + +const ProfileDescription = styled.span` + font-size: 12px; + color: hsl(var(--muted-foreground)); + text-align: center; + line-height: 1.5; +`; + +interface UserProfileStepProps { + selectedProfile: UserProfile | null; + onSelect: (profile: UserProfile) => void; +} + +export function UserProfileStep({ + selectedProfile, + onSelect, +}: UserProfileStepProps) { + return ( + + 您是哪类用户? + 我们将根据您的选择推荐合适的插件 + + + {userProfiles.map((profile) => { + const isSelected = selectedProfile === profile.id; + const Icon = profile.icon; + + return ( + onSelect(profile.id)} + > + {isSelected && ( + + + + )} + + + + {profile.name} + {profile.description} + + ); + })} + + + ); +} diff --git a/src/components/onboarding/steps/WelcomeStep.tsx b/src/components/onboarding/steps/WelcomeStep.tsx new file mode 100644 index 000000000..4fdfcf06a --- /dev/null +++ b/src/components/onboarding/steps/WelcomeStep.tsx @@ -0,0 +1,126 @@ +/** + * 初次安装引导 - 欢迎页 + */ + +import styled from "styled-components"; +import { Button } from "@/components/ui/button"; +import { Sparkles } from "lucide-react"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 48px 24px; +`; + +const LogoContainer = styled.div` + width: 96px; + height: 96px; + margin-bottom: 32px; + animation: float 3s ease-in-out infinite; + + @keyframes float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } + } +`; + +const Logo = styled.img` + width: 100%; + height: 100%; + object-fit: contain; +`; + +const Title = styled.h1` + font-size: 32px; + font-weight: 700; + color: hsl(var(--foreground)); + margin-bottom: 12px; +`; + +const Subtitle = styled.p` + font-size: 18px; + color: hsl(var(--muted-foreground)); + margin-bottom: 8px; +`; + +const Description = styled.p` + font-size: 14px; + color: hsl(var(--muted-foreground)); + max-width: 400px; + line-height: 1.6; + margin-bottom: 48px; +`; + +const FeatureList = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 48px; + text-align: left; +`; + +const FeatureItem = styled.div` + display: flex; + align-items: center; + gap: 12px; + color: hsl(var(--muted-foreground)); + font-size: 14px; + + svg { + width: 16px; + height: 16px; + color: hsl(var(--primary)); + flex-shrink: 0; + } +`; + +interface WelcomeStepProps { + onNext: () => void; + onSkip: () => void; +} + +export function WelcomeStep({ onNext, onSkip }: WelcomeStepProps) { + return ( + + + + + + 欢迎使用 ProxyCast + AI API 聚合代理 + + 让我们花一分钟时间,根据您的使用场景推荐合适的插件,提升您的使用体验。 + + + + + + 多凭证池管理,自动轮换 + + + + 支持 Claude、OpenAI、Gemini 等主流 API + + + + 插件扩展,按需安装 + + + +
+ + +
+
+ ); +} diff --git a/src/components/plugins/PluginInstallDialog.tsx b/src/components/plugins/PluginInstallDialog.tsx index 0f2376989..f5bc37726 100644 --- a/src/components/plugins/PluginInstallDialog.tsx +++ b/src/components/plugins/PluginInstallDialog.tsx @@ -169,8 +169,10 @@ export function PluginInstallDialog({ ); if (installResult.success && installResult.plugin) { - setResult(installResult.plugin); + // 安装成功,关闭对话框并通知父组件 onSuccess(); + resetState(); + onClose(); } else { setError(installResult.error || "安装失败"); } @@ -179,7 +181,7 @@ export function PluginInstallDialog({ } finally { setInstalling(false); } - }, [url, onSuccess]); + }, [url, onSuccess, onClose]); // 自动开始安装(当有 initialUrl 时) useEffect(() => { @@ -262,8 +264,10 @@ export function PluginInstallDialog({ ); if (installResult.success && installResult.plugin) { - setResult(installResult.plugin); + // 安装成功,关闭对话框并通知父组件 onSuccess(); + resetState(); + onClose(); } else { setError(installResult.error || "安装失败"); } diff --git a/src/components/plugins/PluginManager.tsx b/src/components/plugins/PluginManager.tsx index ea4d1590c..92dc82f66 100644 --- a/src/components/plugins/PluginManager.tsx +++ b/src/components/plugins/PluginManager.tsx @@ -16,6 +16,9 @@ import { Package, Download, Cpu, + Globe, + Activity, + FileCode, } from "lucide-react"; import { PluginInstallDialog } from "./PluginInstallDialog"; import { PluginUninstallDialog } from "./PluginUninstallDialog"; @@ -111,6 +114,30 @@ const recommendedPlugins: RecommendedPlugin[] = [ downloadUrl: "https://github.com/aiclientproxy/MachineIdTool/releases/latest/download/machine-id-tool-plugin.zip", }, + { + id: "browser-interception", + name: "浏览器拦截器", + description: "拦截桌面应用的浏览器启动,支持手动复制 URL 到指纹浏览器", + icon: Globe, + downloadUrl: + "https://github.com/aiclientproxy/browser-interception/releases/latest/download/browser-interception-plugin.zip", + }, + { + id: "flow-monitor", + name: "Flow Monitor", + description: "监控和分析 LLM API 请求,提供详细的流量分析和调试功能", + icon: Activity, + downloadUrl: + "https://github.com/aiclientproxy/flow-monitor/releases/latest/download/flow-monitor-plugin.zip", + }, + { + id: "config-switch", + name: "配置管理", + description: "一键切换 API 配置,支持 Claude Code、Codex、Gemini 等客户端", + icon: FileCode, + downloadUrl: + "https://github.com/aiclientproxy/config-switch/releases/latest/download/config-switch-plugin.zip", + }, ]; export function PluginManager() { @@ -164,6 +191,9 @@ export function PluginManager() { const handleInstallSuccess = useCallback(() => { fetchData(); setPendingInstallUrl(null); + toast.success("插件安装成功"); + // 触发侧边栏刷新事件 + window.dispatchEvent(new CustomEvent("plugin-changed")); }, [fetchData]); // 过滤出未安装的推荐插件 diff --git a/src/components/plugins/PluginUIRenderer.tsx b/src/components/plugins/PluginUIRenderer.tsx index 35cbba622..fdfb123f6 100644 --- a/src/components/plugins/PluginUIRenderer.tsx +++ b/src/components/plugins/PluginUIRenderer.tsx @@ -10,6 +10,9 @@ import React from "react"; import { AlertCircle, Package } from "lucide-react"; import { MachineIdTool } from "@/components/tools/machine-id/MachineIdTool"; +import { BrowserInterceptorTool } from "@/components/tools/browser-interceptor/BrowserInterceptorTool"; +import { FlowMonitorPage } from "@/pages"; +import { ConfigManagementPage } from "@/components/config/ConfigManagementPage"; /** * 页面类型定义 @@ -17,12 +20,10 @@ import { MachineIdTool } from "@/components/tools/machine-id/MachineIdTool"; */ export type Page = | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" - | "browser-interceptor" + | "plugins" | "settings" | `plugin:${string}`; @@ -96,13 +97,16 @@ function PluginNotFound({ pluginId }: { pluginId: string }) { * 内置插件组件映射 * * 将插件 ID 映射到对应的 React 组件 - * 目前支持 machine-id-tool 插件 + * 支持 machine-id-tool 和 browser-interception 插件 */ const builtinPluginComponents: Record< string, - React.ComponentType<{ onNavigate: (page: Page) => void }> + React.ComponentType<{ onNavigate?: (page: Page) => void }> > = { "machine-id-tool": MachineIdTool, + "browser-interception": BrowserInterceptorTool, + "flow-monitor": FlowMonitorPage, + "config-switch": ConfigManagementPage, }; /** diff --git a/src/components/plugins/PluginUninstallDialog.tsx b/src/components/plugins/PluginUninstallDialog.tsx index 992227529..a6d1cac09 100644 --- a/src/components/plugins/PluginUninstallDialog.tsx +++ b/src/components/plugins/PluginUninstallDialog.tsx @@ -56,6 +56,8 @@ export function PluginUninstallDialog({ try { await invoke("uninstall_plugin", { pluginId: plugin.id }); onSuccess(); + // 触发侧边栏刷新事件 + window.dispatchEvent(new CustomEvent("plugin-changed")); handleClose(); } catch (e) { setError(e instanceof Error ? e.message : String(e)); diff --git a/src/components/settings/GeneralSettings.tsx b/src/components/settings/GeneralSettings.tsx index 4a932e02a..ae0ed3ae3 100644 --- a/src/components/settings/GeneralSettings.tsx +++ b/src/components/settings/GeneralSettings.tsx @@ -2,10 +2,11 @@ * @file GeneralSettings.tsx * @description 通用设置页面 - 主题、代理、启动行为配置 */ -import { useState, useEffect } from "react"; -import { Moon, Sun, Monitor, RefreshCw, Info } from "lucide-react"; +import { useState, useEffect, useCallback } from "react"; +import { Moon, Sun, Monitor, RefreshCw, Info, RotateCcw } from "lucide-react"; import { cn, validateProxyUrl } from "@/lib/utils"; import { getConfig, saveConfig, Config } from "@/hooks/useTauri"; +import { useOnboardingState } from "@/components/onboarding"; type Theme = "light" | "dark" | "system"; @@ -13,6 +14,13 @@ export function GeneralSettings() { const [theme, setTheme] = useState("system"); const [launchOnStartup, setLaunchOnStartup] = useState(false); const [minimizeToTray, setMinimizeToTray] = useState(true); + const { resetOnboarding } = useOnboardingState(); + + // 重新运行引导 + const handleResetOnboarding = useCallback(() => { + resetOnboarding(); + window.location.reload(); + }, [resetOnboarding]); // 网络代理状态 const [config, setConfig] = useState(null); @@ -215,6 +223,25 @@ export function GeneralSettings() { /> + + {/* 重新运行引导 */} +
+
+
+

初次设置向导

+

+ 重新运行初次安装向导,重新选择用户群体和安装插件 +

+
+ +
+
); } diff --git a/src/components/tools/ToolCardContextMenu.tsx b/src/components/tools/ToolCardContextMenu.tsx index 126dc1c65..86a1cd4c5 100644 --- a/src/components/tools/ToolCardContextMenu.tsx +++ b/src/components/tools/ToolCardContextMenu.tsx @@ -38,12 +38,9 @@ interface DynamicToolCard { */ type Page = | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" - | "browser-interceptor" | "settings" | "plugins" | `plugin:${string}`; diff --git a/src/components/tools/ToolsPage.tsx b/src/components/tools/ToolsPage.tsx index 67d41716a..b0bbe3359 100644 --- a/src/components/tools/ToolsPage.tsx +++ b/src/components/tools/ToolsPage.tsx @@ -37,12 +37,9 @@ import { toast } from "sonner"; */ type Page = | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" - | "browser-interceptor" | "settings" | "plugins" | `plugin:${string}`; @@ -182,16 +179,7 @@ function ToolCard({ /** * 内置工具列表 */ -const builtinTools: DynamicToolCard[] = [ - { - id: "browser-interceptor", - title: "浏览器拦截器", - description: "拦截桌面应用的浏览器启动,支持手动复制 URL 到指纹浏览器", - icon: "Globe", - source: "builtin", - status: "已停止", // TODO: 从状态管理中获取 - }, -]; +const builtinTools: DynamicToolCard[] = []; /** * 占位工具列表 (敬请期待) @@ -236,6 +224,22 @@ const recommendedPlugins: RecommendedPlugin[] = [ downloadUrl: "https://github.com/aiclientproxy/MachineIdTool/releases/latest/download/machine-id-tool-plugin.zip", }, + { + id: "browser-interception", + name: "浏览器拦截器", + description: "拦截桌面应用的浏览器启动,支持手动复制 URL 到指纹浏览器", + icon: "Globe", + downloadUrl: + "https://github.com/aiclientproxy/browser-interception/releases/latest/download/browser-interception-plugin.zip", + }, + { + id: "flow-monitor", + name: "Flow Monitor", + description: "监控和分析 LLM API 请求,提供详细的流量分析和调试功能", + icon: "Activity", + downloadUrl: + "https://github.com/aiclientproxy/flow-monitor/releases/latest/download/flow-monitor-plugin.zip", + }, ]; export function ToolsPage({ onNavigate }: ToolsPageProps) { diff --git a/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx b/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx index 1866b3961..b479f1ea4 100644 --- a/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx +++ b/src/components/tools/browser-interceptor/BrowserInterceptorTool.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; -import { ArrowLeft, Globe, Settings, History, BarChart3 } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Globe, Settings, History, BarChart3 } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { StatusControlPanel } from "./StatusControlPanel"; import { InterceptedUrlsPanel } from "./InterceptedUrlsPanel"; @@ -10,22 +9,7 @@ import { SystemStatusPanel } from "./SystemStatusPanel"; import { InterceptorState } from "@/lib/api/browserInterceptor"; import * as browserInterceptorApi from "@/lib/api/browserInterceptor"; -interface BrowserInterceptorToolProps { - onNavigate: ( - page: - | "provider-pool" - | "config-management" - | "api-server" - | "flow-monitor" - | "tools" - | "browser-interceptor" - | "settings", - ) => void; -} - -export function BrowserInterceptorTool({ - onNavigate, -}: BrowserInterceptorToolProps) { +export function BrowserInterceptorTool() { const [interceptorState, setInterceptorState] = useState(null); const [loading, setLoading] = useState(true); @@ -71,21 +55,12 @@ export function BrowserInterceptorTool({ {/* 页面头部 */}
- -
- -
-

浏览器拦截器

-

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

-
+ +
+

浏览器拦截器

+

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

diff --git a/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx b/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx index d12d29d81..a489c0c01 100644 --- a/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx +++ b/src/components/tools/browser-interceptor/InterceptorConfigPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Settings, Plus, diff --git a/src/components/tools/browser-interceptor/StatusControlPanel.tsx b/src/components/tools/browser-interceptor/StatusControlPanel.tsx index 8cb8c71c1..30b09ed6b 100644 --- a/src/components/tools/browser-interceptor/StatusControlPanel.tsx +++ b/src/components/tools/browser-interceptor/StatusControlPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { RotateCcw, Pause, diff --git a/src/components/tools/browser-interceptor/SystemStatusPanel.tsx b/src/components/tools/browser-interceptor/SystemStatusPanel.tsx index f35c176db..8c85c1ba8 100644 --- a/src/components/tools/browser-interceptor/SystemStatusPanel.tsx +++ b/src/components/tools/browser-interceptor/SystemStatusPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Activity, Shield, diff --git a/src/components/tools/browser-interceptor/UrlHistoryPanel.tsx b/src/components/tools/browser-interceptor/UrlHistoryPanel.tsx index c6800d573..a8105bd92 100644 --- a/src/components/tools/browser-interceptor/UrlHistoryPanel.tsx +++ b/src/components/tools/browser-interceptor/UrlHistoryPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback } from "react"; import { History, Search, diff --git a/src/components/tools/machine-id/MachineIdTool.tsx b/src/components/tools/machine-id/MachineIdTool.tsx index fddff51a3..788df302b 100644 --- a/src/components/tools/machine-id/MachineIdTool.tsx +++ b/src/components/tools/machine-id/MachineIdTool.tsx @@ -10,15 +10,13 @@ import { MachineIdInfo, SystemInfo, AdminStatus } from "@/lib/api/machineId"; import { machineIdApi } from "@/lib/api/machineId"; interface MachineIdToolProps { - onNavigate: ( + onNavigate?: ( page: | "provider-pool" - | "config-management" | "api-server" - | "flow-monitor" | "agent" | "tools" - | "browser-interceptor" + | "plugins" | "settings" | `plugin:${string}`, ) => void; @@ -78,7 +76,7 @@ export function MachineIdTool({ onNavigate }: MachineIdToolProps) {