diff --git a/docs/content/02.user-guide/13.plugins.md b/docs/content/02.user-guide/13.plugins.md
index e99879771..37e0a4d80 100644
--- a/docs/content/02.user-guide/13.plugins.md
+++ b/docs/content/02.user-guide/13.plugins.md
@@ -1,5 +1,9 @@
# 插件中心
+::alert{type="info"}
+📢 本文档已迁移至开放平台。请访问 [开放平台 - 插件中心](/open-platform/plugins) 获取最新内容。
+::
+
ProxyCast 支持通过插件扩展功能。插件中心提供插件的安装、管理和配置。
## 访问插件中心
diff --git a/docs/content/06.development/5.plugin-development.md b/docs/content/06.development/5.plugin-development.md
index 0539b4375..befd0a89a 100644
--- a/docs/content/06.development/5.plugin-development.md
+++ b/docs/content/06.development/5.plugin-development.md
@@ -1,5 +1,9 @@
# 插件开发指南
+::alert{type="info"}
+📢 本文档已迁移至开放平台。请访问 [开放平台 - 插件开发](/open-platform/plugin-development) 获取最新内容。
+::
+
本文档描述 ProxyCast 插件系统的规范和开发指南,为插件市场做准备。
## 插件类型
diff --git a/docs/content/index.md b/docs/content/index.md
index b7a0701c2..e885c95fb 100644
--- a/docs/content/index.md
+++ b/docs/content/index.md
@@ -124,6 +124,19 @@ curl http://127.0.0.1:8999/v1/chat/completions \
+## 🌐 开放平台
+
+
+
## 🤝 社区与支持
- **GitHub Issues**: [报告问题](https://github.com/aiclientproxy/proxycast/issues)
diff --git a/docs/prd/proxycast-connect.md b/docs/prd/proxycast-connect.md
index 1c97b585f..6bf6a9bc3 100644
--- a/docs/prd/proxycast-connect.md
+++ b/docs/prd/proxycast-connect.md
@@ -155,7 +155,7 @@ ProxyCast 是开源软件,中转商通过 **GitHub PR** 方式注册,无需
中转商注册信息统一管理在独立仓库:
```
-https://github.com/AiClientProxy/transit-service-provider
+https://github.com/aiclientproxy/connect
```
#### 2.3.2 注册流程
@@ -167,7 +167,7 @@ https://github.com/AiClientProxy/transit-service-provider
│ │
│ Step 1: Fork 仓库 │
│ ───────────────── │
-│ Fork https://github.com/AiClientProxy/transit-service-provider │
+│ Fork https://github.com/aiclientproxy/connect │
│ │ │
│ ▼ │
│ Step 2: 创建配置文件 │
@@ -302,7 +302,7 @@ transit-service-provider/
ProxyCast 客户端通过以下方式获取中转商列表:
```
-https://raw.githubusercontent.com/AiClientProxy/transit-service-provider/main/dist/registry.json
+https://raw.githubusercontent.com/aiclientproxy/connect/main/dist/registry.json
```
客户端定期(每 24 小时)同步更新,也可手动刷新
@@ -596,7 +596,7 @@ 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";
+ "https://raw.githubusercontent.com/aiclientproxy/connect/main/dist/registry.json";
/// 从 GitHub 加载中转商列表
pub async fn load_from_github(&mut self) -> Result<()> {
@@ -642,7 +642,7 @@ impl RelayRegistry {
│ │
│ Step 1: Fork 仓库 │
│ ───────────────── │
-│ Fork https://github.com/AiClientProxy/transit-service-provider │
+│ Fork https://github.com/aiclientproxy/connect │
│ │ │
│ ▼ │
│ Step 2: 创建配置文件 │
@@ -798,7 +798,209 @@ function downloadConfig(apiKey) {
└─────────────────────────────────────────────────────────────────────────┘
```
-### 6.3 中转商审核
+### 5.3 统计回调(Webhook)
+
+为了让中转商追踪推广效果,ProxyCast 提供统计回调机制。
+
+#### 5.3.1 回调流程
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ 统计回调流程 │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ 用户点击一键配置 │
+│ │ │
+│ ▼ │
+│ ProxyCast 打开,显示确认弹窗 │
+│ │ │
+│ ├─── 用户取消 ─────────────────────────────────────────────────┐ │
+│ │ │ │
+│ │ 发送回调: status=cancelled │ │
+│ │ │ │
+│ └─── 用户确认 ─────────────────────────────────────────────────┐ │
+│ │ │
+│ Key 添加成功 │ │
+│ │ │ │
+│ ▼ │ │
+│ 发送回调: status=success │ │
+│ │ │ │
+│ ▼ │ │
+│ 中转商收到回调,更新统计 │ │
+│ │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+#### 5.3.2 回调配置
+
+中转商在 `providers/{id}.json` 中配置回调地址:
+
+```json
+{
+ "id": "myrelay",
+ "name": "我的中转站",
+ // ... 其他字段 ...
+
+ "webhook": {
+ "callback_url": "https://api.myrelay.com/proxycast/callback",
+ "secret": "whsec_xxxxxxxxxxxxxxxx"
+ }
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `webhook.callback_url` | ❌ | 回调地址(HTTPS) |
+| `webhook.secret` | ❌ | 签名密钥,用于验证请求来源 |
+
+#### 5.3.3 回调请求格式
+
+ProxyCast 向中转商发送 POST 请求:
+
+```http
+POST https://api.myrelay.com/proxycast/callback
+Content-Type: application/json
+X-ProxyCast-Signature: sha256=xxxxxxxxxxxxxxxx
+X-ProxyCast-Timestamp: 1704067200
+
+{
+ "event": "connect",
+ "status": "success",
+ "relay_id": "myrelay",
+ "ref": "promo2024",
+ "key_prefix": "sk-xxxx",
+ "timestamp": "2026-01-05T12:00:00Z",
+ "client": {
+ "version": "1.2.0",
+ "platform": "macos"
+ }
+}
+```
+
+#### 5.3.4 回调字段说明
+
+| 字段 | 说明 |
+|------|------|
+| `event` | 事件类型:`connect` |
+| `status` | 状态:`success`(成功)、`cancelled`(用户取消)、`error`(失败) |
+| `relay_id` | 中转商 ID |
+| `ref` | 推广码(如果有) |
+| `key_prefix` | Key 前缀(脱敏,仅前 7 位) |
+| `timestamp` | 事件时间 |
+| `client.version` | ProxyCast 版本 |
+| `client.platform` | 平台:`macos`、`windows`、`linux` |
+| `error_code` | 错误码(仅 status=error 时) |
+| `error_message` | 错误信息(仅 status=error 时) |
+
+#### 5.3.5 签名验证
+
+中转商应验证请求签名,防止伪造:
+
+```javascript
+const crypto = require('crypto');
+
+function verifySignature(payload, signature, secret, timestamp) {
+ // 检查时间戳,防止重放攻击(5分钟内有效)
+ const now = Math.floor(Date.now() / 1000);
+ if (Math.abs(now - parseInt(timestamp)) > 300) {
+ return false;
+ }
+
+ // 计算签名
+ const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
+ const expectedSignature = crypto
+ .createHmac('sha256', secret)
+ .update(signedPayload)
+ .digest('hex');
+
+ return `sha256=${expectedSignature}` === signature;
+}
+
+// Express 示例
+app.post('/proxycast/callback', (req, res) => {
+ const signature = req.headers['x-proxycast-signature'];
+ const timestamp = req.headers['x-proxycast-timestamp'];
+
+ if (!verifySignature(req.body, signature, WEBHOOK_SECRET, timestamp)) {
+ return res.status(401).json({ error: 'Invalid signature' });
+ }
+
+ // 处理回调
+ const { event, status, ref, key_prefix } = req.body;
+
+ if (status === 'success') {
+ // 记录成功安装
+ await db.stats.increment({
+ relay_id: req.body.relay_id,
+ ref: ref,
+ date: new Date().toISOString().split('T')[0]
+ });
+ }
+
+ res.json({ received: true });
+});
+```
+
+#### 5.3.6 回调重试机制
+
+| 重试次数 | 间隔 | 说明 |
+|----------|------|------|
+| 第 1 次 | 立即 | 首次发送 |
+| 第 2 次 | 1 分钟 | 首次失败后 |
+| 第 3 次 | 5 分钟 | 第二次失败后 |
+| 第 4 次 | 30 分钟 | 第三次失败后 |
+| 放弃 | - | 超过 4 次不再重试 |
+
+成功响应:HTTP 2xx 状态码
+
+#### 5.3.7 统计数据示例
+
+中转商可以基于回调数据构建统计面板:
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ ProxyCast Connect 统计 2026-01-05 │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ 今日概览 │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ 128 │ │ 115 │ │ 13 │ │ 89.8% │ │
+│ │ 点击次数 │ │ 成功配置 │ │ 用户取消 │ │ 转化率 │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
+│ │
+│ 推广码效果 │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ 推广码 点击 成功 取消 转化率 │ │
+│ ├─────────────────────────────────────────────────────────────────┤ │
+│ │ promo2024 56 52 4 92.9% │ │
+│ │ twitter 38 33 5 86.8% │ │
+│ │ (无推广码) 34 30 4 88.2% │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ 平台分布 │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ macOS ████████████████████████████████████ 72% │ │
+│ │ Windows ████████████████ 23% │ │
+│ │ Linux ████ 5% │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+#### 5.3.8 隐私保护
+
+回调数据遵循最小化原则:
+
+| 数据 | 是否发送 | 说明 |
+|------|----------|------|
+| 完整 API Key | ❌ | 仅发送前 7 位前缀 |
+| 用户 ID | ❌ | 不发送任何用户标识 |
+| 设备 ID | ❌ | 不发送设备标识 |
+| IP 地址 | ❌ | 不发送用户 IP |
+| 推广码 | ✅ | 用于统计推广效果 |
+| 平台信息 | ✅ | 仅操作系统类型 |
+
+### 5.4 中转商审核
| 审核项 | 说明 |
|--------|------|
@@ -912,5 +1114,5 @@ function downloadConfig(apiKey) {
| 仓库 | 说明 |
|------|------|
-| [proxycast](https://github.com/AiClientProxy/proxycast) | ProxyCast 主项目 |
-| [transit-service-provider](https://github.com/AiClientProxy/transit-service-provider) | 中转商注册仓库 |
+| [proxycast](https://github.com/aiclientproxy/proxycast) | ProxyCast 主项目 |
+| [transit-service-provider](https://github.com/aiclientproxy/connect) | 中转商注册仓库 |
diff --git a/package-lock.json b/package-lock.json
index 3ef1558a5..6b4924007 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "proxycast",
- "version": "0.25.0",
+ "version": "0.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "proxycast",
- "version": "0.25.0",
+ "version": "0.29.0",
"dependencies": {
"@fabianlars/tauri-plugin-oauth": "^2",
"@radix-ui/react-collapsible": "^1.1.12",
@@ -23,6 +23,7 @@
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.3",
"@tauri-apps/api": "^2.9.1",
+ "@tauri-apps/plugin-deep-link": "^2.4.5",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-shell": "^2.0.0",
"@types/styled-components": "^5.1.36",
@@ -3347,6 +3348,15 @@
"node": ">= 10"
}
},
+ "node_modules/@tauri-apps/plugin-deep-link": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-deep-link/-/plugin-deep-link-2.4.5.tgz",
+ "integrity": "sha512-Zf2RTj1D9IQQ45/jqW8XTKvql24HqlPjcpv0mV/O2jHQkNe11HOTZBVj6IK37qs+MWV7xZzcmazx/QVZnhAwaQ==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.8.0"
+ }
+ },
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.2.tgz",
diff --git a/package.json b/package.json
index c76a7592c..a8dc0405f 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.3",
"@tauri-apps/api": "^2.9.1",
+ "@tauri-apps/plugin-deep-link": "^2.4.5",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-shell": "^2.0.0",
"@types/styled-components": "^5.1.36",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 5195ea5e0..3618e216e 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -3692,6 +3692,7 @@ dependencies = [
"fs2",
"futures",
"glob",
+ "hex",
"indexmap 2.12.1",
"md5",
"notify",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 03a8d7330..8928e629f 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -65,6 +65,7 @@ once_cell = "1"
tokio-util = "0.7"
arboard = "3"
glob = "0.3.3"
+hex = "0.4.3"
# Platform specific dependencies for browser interceptor
diff --git a/src-tauri/src/README.md b/src-tauri/src/README.md
index 897aaf6f7..14f9bbfd5 100644
--- a/src-tauri/src/README.md
+++ b/src-tauri/src/README.md
@@ -11,6 +11,7 @@ Tauri 后端核心代码,处理系统级功能和 API 服务。
- `commands/` - Tauri 命令处理(前端调用入口)
- `config/` - 配置管理(导入/导出/热重载)
+- `connect/` - ProxyCast Connect 模块(中转商生态合作)
- `converter/` - 协议转换(OpenAI ↔ CW/Claude/Antigravity)
- `credential/` - 凭证池管理(负载均衡、健康检查)
- `database/` - 数据库层(SQLite + DAO)
diff --git a/src-tauri/src/app/bootstrap.rs b/src-tauri/src/app/bootstrap.rs
index b8a691338..0667b76a8 100644
--- a/src-tauri/src/app/bootstrap.rs
+++ b/src-tauri/src/app/bootstrap.rs
@@ -7,6 +7,7 @@ use tokio::sync::RwLock;
use crate::agent::NativeAgentState;
use crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState;
+use crate::commands::connect_cmd::ConnectStateWrapper;
use crate::commands::flow_monitor_cmd::{
BatchOperationsState, BookmarkManagerState, EnhancedStatsServiceState, FlowInterceptorState,
FlowMonitorState, FlowQueryServiceState, FlowReplayerState, QuickFilterManagerState,
@@ -130,6 +131,7 @@ pub struct AppStates {
pub native_agent: NativeAgentState,
pub oauth_plugin_manager: crate::commands::oauth_plugin_cmd::OAuthPluginManagerState,
pub orchestrator: OrchestratorState,
+ pub connect_state: ConnectStateWrapper,
// 用于 setup hook 的共享实例
pub shared_stats: Arc>,
pub shared_tokens: Arc>,
@@ -202,6 +204,9 @@ pub fn init_states(config: &Config) -> Result {
crate::commands::oauth_plugin_cmd::OAuthPluginManagerState::with_defaults();
let orchestrator_state = OrchestratorState::new();
+ // 初始化 Connect 状态(延迟初始化,在 setup hook 中完成)
+ let connect_state = ConnectStateWrapper(Arc::new(RwLock::new(None)));
+
// 初始化默认技能仓库
{
let conn = db.lock().expect("Failed to lock database");
@@ -236,6 +241,7 @@ pub fn init_states(config: &Config) -> Result {
native_agent: native_agent_state,
oauth_plugin_manager: oauth_plugin_manager_state,
orchestrator: orchestrator_state,
+ connect_state,
shared_stats,
shared_tokens,
shared_logger,
diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs
index 38873ca55..44a8242c6 100644
--- a/src-tauri/src/app/runner.rs
+++ b/src-tauri/src/app/runner.rs
@@ -3,7 +3,7 @@
//! 包含 Tauri 应用的主入口函数和命令注册。
use std::sync::Arc;
-use tauri::Manager;
+use tauri::{Emitter, Listener, Manager};
use crate::commands;
use crate::tray::{TrayIconStatus, TrayManager, TrayStateSnapshot};
@@ -70,6 +70,7 @@ pub fn run() {
native_agent: native_agent_state,
oauth_plugin_manager: oauth_plugin_manager_state,
orchestrator: orchestrator_state,
+ connect_state: connect_state,
shared_stats,
shared_tokens,
shared_logger,
@@ -95,7 +96,16 @@ pub fn run() {
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec!["--minimized"]),
- ))
+ ));
+
+ // 在 macOS 上注册 Deep Link 插件
+ // _Requirements: 1.4_
+ #[cfg(target_os = "macos")]
+ {
+ builder = builder.plugin(tauri_plugin_deep_link::init());
+ }
+
+ builder = builder
// 单实例插件:当第二个实例启动时,将 URL 传递给第一个实例
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
tracing::info!("[单实例] 收到来自新实例的参数: {:?}", args);
@@ -134,6 +144,7 @@ pub fn run() {
.manage(native_agent_state)
.manage(oauth_plugin_manager_state)
.manage(orchestrator_state)
+ .manage(connect_state)
.on_window_event(move |window, event| {
// 处理窗口关闭事件
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
@@ -178,6 +189,128 @@ pub fn run() {
app.manage(tray_state);
}
}
+
+ // 初始化 Connect 状态
+ // _Requirements: 1.4, 2.1_
+ {
+ let app_handle = app.handle().clone();
+ tauri::async_runtime::spawn(async move {
+ // 获取应用数据目录
+ let app_data_dir = dirs::data_dir()
+ .unwrap_or_else(|| std::path::PathBuf::from("."))
+ .join("proxycast");
+
+ // 初始化 Connect 状态
+ match crate::commands::connect_cmd::init_connect_state(app_data_dir).await {
+ Ok(connect_state_inner) => {
+ tracing::info!("[启动] Connect 模块初始化成功");
+ // 更新状态
+ if let Some(state) = app_handle
+ .try_state::()
+ {
+ let mut guard = state.0.write().await;
+ *guard = Some(connect_state_inner);
+ }
+ }
+ Err(e) => {
+ tracing::error!("[启动] Connect 模块初始化失败: {:?}", e);
+ }
+ }
+ });
+ }
+
+ // 注册 Deep Link 事件处理器(仅 macOS)
+ // _Requirements: 1.4_
+ #[cfg(target_os = "macos")]
+ {
+ let app_handle = app.handle().clone();
+ app.listen("deep-link://new-url", move |event| {
+ let urls = event.payload().to_string();
+ tracing::info!("[Deep Link] 收到 URL: {}", urls);
+ // 解析 URL 并处理
+ let app_handle_clone = app_handle.clone();
+ tauri::async_runtime::spawn(async move {
+ // 尝试解析为 JSON 数组(Tauri deep-link 插件返回的格式)
+ if let Ok(url_list) = serde_json::from_str::>(&urls) {
+ for url in url_list {
+ if url.starts_with("proxycast://connect") {
+ // 调用 handle_deep_link 命令
+ if let Some(state) = app_handle_clone
+ .try_state::()
+ {
+ match crate::connect::parse_deep_link(&url) {
+ Ok(payload) => {
+ // 查询中转商信息
+ let (relay_info, is_verified) = {
+ let state_guard = state.0.read().await;
+ if let Some(connect_state) = state_guard.as_ref() {
+ let info = connect_state.registry.get(&payload.relay);
+ let verified = info.is_some();
+ (info, verified)
+ } else {
+ (None, false)
+ }
+ };
+
+ let result = crate::commands::connect_cmd::DeepLinkResult {
+ payload,
+ relay_info,
+ is_verified,
+ };
+
+ // 发送事件到前端
+ if let Err(e) = app_handle_clone.emit("deep-link-connect", &result) {
+ tracing::error!("[Deep Link] 发送事件失败: {}", e);
+ }
+ }
+ Err(e) => {
+ tracing::error!("[Deep Link] 解析 URL 失败: {:?}", e);
+ // 发送错误事件到前端
+ let _ = app_handle_clone.emit("deep-link-error", &format!("{:?}", e));
+ }
+ }
+ }
+ }
+ }
+ } else if urls.starts_with("proxycast://connect") {
+ // 直接处理单个 URL
+ if let Some(state) = app_handle_clone
+ .try_state::()
+ {
+ match crate::connect::parse_deep_link(&urls) {
+ Ok(payload) => {
+ let (relay_info, is_verified) = {
+ let state_guard = state.0.read().await;
+ if let Some(connect_state) = state_guard.as_ref() {
+ let info = connect_state.registry.get(&payload.relay);
+ let verified = info.is_some();
+ (info, verified)
+ } else {
+ (None, false)
+ }
+ };
+
+ let result = crate::commands::connect_cmd::DeepLinkResult {
+ payload,
+ relay_info,
+ is_verified,
+ };
+
+ if let Err(e) = app_handle_clone.emit("deep-link-connect", &result) {
+ tracing::error!("[Deep Link] 发送事件失败: {}", e);
+ }
+ }
+ Err(e) => {
+ tracing::error!("[Deep Link] 解析 URL 失败: {:?}", e);
+ let _ = app_handle_clone.emit("deep-link-error", &format!("{:?}", e));
+ }
+ }
+ }
+ }
+ });
+ });
+ }
+
// 自动启动服务器
let state = state_clone.clone();
let logs = logs_clone.clone();
@@ -843,6 +976,14 @@ pub fn run() {
commands::orchestrator_cmd::list_strategies,
commands::orchestrator_cmd::list_service_tiers,
commands::orchestrator_cmd::list_task_hints,
+ // Connect commands
+ // _Requirements: 1.4, 2.3, 4.1, 5.3_
+ commands::connect_cmd::handle_deep_link,
+ commands::connect_cmd::get_relay_info,
+ commands::connect_cmd::save_relay_api_key,
+ commands::connect_cmd::refresh_relay_registry,
+ commands::connect_cmd::list_relay_providers,
+ commands::connect_cmd::send_connect_callback,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src-tauri/src/commands/connect_cmd.rs b/src-tauri/src/commands/connect_cmd.rs
new file mode 100644
index 000000000..cc66eb15c
--- /dev/null
+++ b/src-tauri/src/commands/connect_cmd.rs
@@ -0,0 +1,423 @@
+//! ProxyCast Connect 命令模块
+//!
+//! 提供 Deep Link 协议处理和中转商 API Key 管理的 Tauri 命令。
+//!
+//! ## 功能
+//!
+//! - Deep Link 协议处理和事件发送
+//! - 中转商注册表查询和刷新
+//! - API Key 集成到 API Key Provider 系统
+//! - 统计回调(Webhook)
+//!
+//! ## Tauri 命令
+//!
+//! - `get_relay_info` - 查询中转商信息
+//! - `save_relay_api_key` - 保存 API Key(添加到 API Key Provider 系统)
+//! - `refresh_relay_registry` - 刷新注册表
+//! - `handle_deep_link` - 处理 Deep Link URL
+//! - `send_connect_callback` - 发送统计回调
+//!
+//! _Requirements: 1.4, 2.3, 4.1, 5.3_
+
+use crate::connect::{
+ parse_deep_link, send_cancelled_callback, send_error_callback, send_success_callback,
+ ConnectPayload, DeepLinkError, RelayInfo, RelayRegistry,
+};
+use crate::database::dao::api_key_provider::ApiProviderType;
+use crate::database::DbConnection;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use std::sync::Arc;
+use tauri::{AppHandle, Emitter, State};
+use tokio::sync::RwLock;
+
+use super::api_key_provider_cmd::ApiKeyProviderServiceState;
+
+/// Connect 模块状态
+///
+/// 管理 RelayRegistry 的共享状态
+pub struct ConnectState {
+ /// 中转商注册表
+ pub registry: Arc,
+}
+
+/// Connect 状态包装器(用于 Tauri 状态管理)
+pub struct ConnectStateWrapper(pub Arc>>);
+
+/// Deep Link 处理结果
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct DeepLinkResult {
+ /// 解析后的 payload
+ pub payload: ConnectPayload,
+ /// 中转商信息(如果在注册表中找到)
+ pub relay_info: Option,
+ /// 是否为已验证的中转商
+ pub is_verified: bool,
+}
+
+/// 命令错误类型
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ConnectError {
+ pub code: String,
+ pub message: String,
+}
+
+impl From for ConnectError {
+ fn from(err: DeepLinkError) -> Self {
+ let (code, message) = match &err {
+ DeepLinkError::InvalidUrl(msg) => ("INVALID_URL".to_string(), msg.clone()),
+ DeepLinkError::MissingRelay => {
+ ("MISSING_RELAY".to_string(), "缺少必填参数: relay".to_string())
+ }
+ DeepLinkError::MissingKey => {
+ ("MISSING_KEY".to_string(), "缺少必填参数: key".to_string())
+ }
+ };
+ ConnectError { code, message }
+ }
+}
+
+impl From for ConnectError {
+ fn from(err: crate::connect::RegistryError) -> Self {
+ ConnectError {
+ code: "REGISTRY_ERROR".to_string(),
+ message: err.to_string(),
+ }
+ }
+}
+
+/// 初始化 Connect 状态
+///
+/// 在应用启动时调用,初始化 RelayRegistry
+pub async fn init_connect_state(app_data_dir: PathBuf) -> Result {
+ // 初始化 Registry
+ let cache_path = app_data_dir.join("connect").join("registry.json");
+ let registry = Arc::new(RelayRegistry::new(cache_path));
+
+ // 尝试从缓存加载,如果失败则从远程加载
+ if let Err(_) = registry.load_from_cache() {
+ tracing::info!("[Connect] 缓存不存在,尝试从远程加载注册表");
+ if let Err(e) = registry.load_from_remote().await {
+ tracing::warn!("[Connect] 从远程加载注册表失败: {}", e);
+ // 不返回错误,允许应用继续运行
+ }
+ }
+
+ Ok(ConnectState { registry })
+}
+
+/// 处理 Deep Link URL
+///
+/// 解析 Deep Link URL,查询中转商信息,并发送事件到前端
+///
+/// _Requirements: 1.4_
+#[tauri::command]
+pub async fn handle_deep_link(
+ app: AppHandle,
+ state: State<'_, ConnectStateWrapper>,
+ url: String,
+) -> Result {
+ tracing::info!("[Connect] 处理 Deep Link: {}", url);
+
+ // 解析 Deep Link
+ let payload = parse_deep_link(&url)?;
+
+ // 查询中转商信息
+ let (relay_info, is_verified) = {
+ let state_guard = state.0.read().await;
+ if let Some(connect_state) = state_guard.as_ref() {
+ let info = connect_state.registry.get(&payload.relay);
+ let verified = info.is_some();
+ (info, verified)
+ } else {
+ (None, false)
+ }
+ };
+
+ let result = DeepLinkResult {
+ payload: payload.clone(),
+ relay_info: relay_info.clone(),
+ is_verified,
+ };
+
+ // 发送事件到前端
+ // _Requirements: 1.4_
+ if let Err(e) = app.emit("deep-link-connect", &result) {
+ tracing::error!("[Connect] 发送 deep-link-connect 事件失败: {}", e);
+ }
+
+ Ok(result)
+}
+
+/// 查询中转商信息
+///
+/// _Requirements: 2.3_
+#[tauri::command]
+pub async fn get_relay_info(
+ state: State<'_, ConnectStateWrapper>,
+ relay_id: String,
+) -> Result