From 81d686a230a0bab23be4d7bb24af071f70be1bcd Mon Sep 17 00:00:00 2001
From: JSRCode <139555610+jsrcode@users.noreply.github.com>
Date: Sun, 4 Jan 2026 13:03:01 +0800
Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=8E?=
=?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E5=AF=BC=E5=85=A5=E8=BF=9C=E7=A8=8B?=
=?UTF-8?q?=E4=BB=A4=E7=89=8C=E4=B8=BA=20Profile=20=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 动机
- 用户在供应商平台(如 NEW API)管理多个 API 令牌,需要手动复制配置到 DuckCoding
- 重复性操作易出错且效率低,缺乏导入溯源记录
## 核心改动
### 后端架构层
- 新增 `models/remote_token.rs`:定义 `RemoteToken`、`RemoteTokenGroup`、`CreateRemoteTokenRequest` 等数据模型
- 新增 `services/new_api/client.rs`:实现 NEW API 客户端(296行),提供令牌列表/创建/删除/分组查询功能
- 新增 `commands/token_commands.rs`:7个 Tauri 命令支持前端调用(`fetch_provider_tokens`、`import_token_as_profile` 等)
- 扩展 `ProfileSource` 枚举:新增 `ImportedFromProvider` 变体,记录导入溯源信息(供应商ID/令牌ID/导入时间等)
- 更新 `ProfileManager::load/save_profiles_store` 为 public,允许 token_commands 直接操作
### 前端功能层
- 供应商管理页(`ProviderManagementPage`):
- 新增可展开/折叠的表格行,展开后显示 `RemoteTokenManagement` 组件
- 支持查看令牌列表、创建令牌、删除令牌、导入到 Profile
- Profile 管理页(`ProfileManagementPage`):
- "创建 Profile" 按钮改为下拉菜单,新增"从供应商导入"选项
- 新增 `ImportFromProviderDialog` 组件(336行),支持选择供应商 → 选择令牌 → 填写配置 → 导入
- `ActiveProfileCard` 和 `ProfileCard` 显示来源信息(自定义 vs 从供应商导入)
- 类型定义和命令包装:
- `types/remote-token.ts`:前端类型定义
- `lib/tauri-commands/token.ts`:Tauri 命令的 TypeScript 包装器
### 数据迁移
- `profile_v2.rs`:所有 Profile 创建逻辑默认 `source: ProfileSource::Custom`,保证向后兼容
## 测试情况
- 后端单元测试:`token_commands.rs` 包含 4 个测试,`remote_token.rs` 包含 2 个测试
- 前端集成测试:手动验证导入流程(选择供应商 → 选择令牌 → 填写配置 → 成功导入)
## 影响范围
- **新增代码**:1059 行(前端 763 行 + 后端 296 行),无破坏性变更
- **修改模块**:ProfileManager、迁移系统、前端 Profile/Provider 管理页
- **数据兼容性**:旧 Profile 自动标记为 `Custom` 来源,无需手动迁移
---
src-tauri/src/commands/mod.rs | 2 +
src-tauri/src/commands/token_commands.rs | 307 ++++++++++++++++
src-tauri/src/main.rs | 8 +
src-tauri/src/models/mod.rs | 2 +
src-tauri/src/models/remote_token.rs | 147 ++++++++
.../migrations/profile_v2.rs | 12 +-
src-tauri/src/services/mod.rs | 5 +-
src-tauri/src/services/new_api/client.rs | 296 +++++++++++++++
src-tauri/src/services/new_api/mod.rs | 7 +
.../src/services/profile_manager/manager.rs | 10 +-
src-tauri/src/services/profile_manager/mod.rs | 2 +-
.../src/services/profile_manager/types.rs | 36 ++
src/lib/tauri-commands/token.ts | 87 +++++
.../components/ActiveProfileCard.tsx | 8 +
.../components/ImportFromProviderDialog.tsx | 336 ++++++++++++++++++
.../components/ProfileCard.tsx | 36 +-
src/pages/ProfileManagementPage/index.tsx | 46 ++-
.../components/CreateRemoteTokenDialog.tsx | 310 ++++++++++++++++
.../components/ImportTokenDialog.tsx | 199 +++++++++++
.../components/RemoteTokenManagement.tsx | 239 +++++++++++++
src/pages/ProviderManagementPage/index.tsx | 118 +++---
src/types/profile.ts | 20 +-
src/types/remote-token.ts | 101 ++++++
23 files changed, 2278 insertions(+), 56 deletions(-)
create mode 100644 src-tauri/src/commands/token_commands.rs
create mode 100644 src-tauri/src/models/remote_token.rs
create mode 100644 src-tauri/src/services/new_api/client.rs
create mode 100644 src-tauri/src/services/new_api/mod.rs
create mode 100644 src/lib/tauri-commands/token.ts
create mode 100644 src/pages/ProfileManagementPage/components/ImportFromProviderDialog.tsx
create mode 100644 src/pages/ProviderManagementPage/components/CreateRemoteTokenDialog.tsx
create mode 100644 src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx
create mode 100644 src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx
create mode 100644 src/types/remote-token.ts
diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs
index 434e111..c340bc1 100644
--- a/src-tauri/src/commands/mod.rs
+++ b/src-tauri/src/commands/mod.rs
@@ -10,6 +10,7 @@ pub mod proxy_commands;
pub mod session_commands;
pub mod startup_commands; // 开机自启动管理命令
pub mod stats_commands;
+pub mod token_commands; // 令牌资产管理命令(NEW API 集成)
pub mod tool_commands;
pub mod tool_management;
pub mod types;
@@ -29,6 +30,7 @@ pub use proxy_commands::*;
pub use session_commands::*;
pub use startup_commands::*; // 开机自启动管理命令
pub use stats_commands::*;
+pub use token_commands::*; // 令牌资产管理命令(NEW API 集成)
pub use tool_commands::*;
pub use tool_management::*;
pub use update_commands::*;
diff --git a/src-tauri/src/commands/token_commands.rs b/src-tauri/src/commands/token_commands.rs
new file mode 100644
index 0000000..5bfcba1
--- /dev/null
+++ b/src-tauri/src/commands/token_commands.rs
@@ -0,0 +1,307 @@
+// Token Management Commands
+//
+// NEW API 令牌管理相关命令
+
+use ::duckcoding::models::provider::Provider;
+use ::duckcoding::models::remote_token::{CreateRemoteTokenRequest, RemoteToken, RemoteTokenGroup};
+use ::duckcoding::services::{
+ ClaudeProfile, CodexProfile, GeminiProfile, NewApiClient, ProfileSource,
+};
+use anyhow::Result;
+use chrono::Utc;
+use tauri::State;
+
+/// 获取指定供应商的远程令牌列表
+#[tauri::command]
+pub async fn fetch_provider_tokens(provider: Provider) -> Result, String> {
+ let client = NewApiClient::new(provider).map_err(|e| e.to_string())?;
+ client.list_tokens().await.map_err(|e| e.to_string())
+}
+
+/// 获取指定供应商的令牌分组列表
+#[tauri::command]
+pub async fn fetch_provider_groups(provider: Provider) -> Result, String> {
+ let client = NewApiClient::new(provider).map_err(|e| e.to_string())?;
+ client.list_groups().await.map_err(|e| e.to_string())
+}
+
+/// 在供应商创建新的远程令牌
+#[tauri::command]
+pub async fn create_provider_token(
+ provider: Provider,
+ request: CreateRemoteTokenRequest,
+) -> Result {
+ let client = NewApiClient::new(provider).map_err(|e| e.to_string())?;
+ client
+ .create_token(request)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// 删除供应商的远程令牌
+#[tauri::command]
+pub async fn delete_provider_token(provider: Provider, token_id: i64) -> Result<(), String> {
+ let client = NewApiClient::new(provider).map_err(|e| e.to_string())?;
+ client
+ .delete_token(token_id)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// 更新供应商的远程令牌名称
+#[tauri::command]
+pub async fn update_provider_token(
+ provider: Provider,
+ token_id: i64,
+ name: String,
+) -> Result {
+ let client = NewApiClient::new(provider).map_err(|e| e.to_string())?;
+ client
+ .update_token(token_id, name)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// 导入远程令牌为本地 Profile
+#[tauri::command]
+pub async fn import_token_as_profile(
+ profile_manager: State<'_, crate::commands::profile_commands::ProfileManagerState>,
+ provider: Provider,
+ remote_token: RemoteToken,
+ tool_id: String,
+ profile_name: String,
+) -> Result<(), String> {
+ // 验证 tool_id
+ if tool_id != "claude-code" && tool_id != "codex" && tool_id != "gemini-cli" {
+ return Err(format!("不支持的工具类型: {}", tool_id));
+ }
+
+ // 构建 ProfileSource
+ let source = ProfileSource::ImportedFromProvider {
+ provider_id: provider.id.clone(),
+ provider_name: provider.name.clone(),
+ remote_token_id: remote_token.id,
+ remote_token_name: remote_token.name.clone(),
+ group: remote_token.group.clone(),
+ imported_at: Utc::now().timestamp(),
+ };
+
+ // 提取 API Key 和 Base URL
+ let api_key = remote_token.key.clone();
+ let base_url = provider.website_url.clone();
+
+ // 直接操作 ProfilesStore 以支持自定义 source 字段
+ let manager = profile_manager.manager.read().await;
+ let mut store = manager.load_profiles_store().map_err(|e| e.to_string())?;
+
+ // 根据工具类型创建对应的 Profile
+ match tool_id.as_str() {
+ "claude-code" => {
+ let profile = ClaudeProfile {
+ api_key,
+ base_url,
+ source,
+ created_at: Utc::now(),
+ updated_at: Utc::now(),
+ raw_settings: None,
+ raw_config_json: None,
+ };
+ store.claude_code.insert(profile_name.clone(), profile);
+ }
+ "codex" => {
+ let profile = CodexProfile {
+ api_key,
+ base_url,
+ wire_api: "responses".to_string(), // 默认使用 responses API
+ source,
+ created_at: Utc::now(),
+ updated_at: Utc::now(),
+ raw_config_toml: None,
+ raw_auth_json: None,
+ };
+ store.codex.insert(profile_name.clone(), profile);
+ }
+ "gemini-cli" => {
+ let profile = GeminiProfile {
+ api_key,
+ base_url,
+ model: None, // 不指定 model,保留用户原有配置
+ source,
+ created_at: Utc::now(),
+ updated_at: Utc::now(),
+ raw_settings: None,
+ raw_env: None,
+ };
+ store.gemini_cli.insert(profile_name.clone(), profile);
+ }
+ _ => return Err(format!("不支持的工具类型: {}", tool_id)),
+ }
+
+ store.metadata.last_updated = Utc::now();
+ manager
+ .save_profiles_store(&store)
+ .map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+/// 创建自定义 Profile(非导入令牌)
+#[tauri::command]
+pub async fn create_custom_profile(
+ profile_manager: State<'_, crate::commands::profile_commands::ProfileManagerState>,
+ tool_id: String,
+ profile_name: String,
+ api_key: String,
+ base_url: String,
+ extra_config: Option,
+) -> Result<(), String> {
+ // 验证 tool_id
+ if tool_id != "claude-code" && tool_id != "codex" && tool_id != "gemini-cli" {
+ return Err(format!("不支持的工具类型: {}", tool_id));
+ }
+
+ let source = ProfileSource::Custom;
+
+ // 直接操作 ProfilesStore 以支持自定义 source 字段
+ let manager = profile_manager.manager.read().await;
+ let mut store = manager.load_profiles_store().map_err(|e| e.to_string())?;
+
+ // 根据工具类型创建对应的 Profile
+ match tool_id.as_str() {
+ "claude-code" => {
+ let profile = ClaudeProfile {
+ api_key,
+ base_url,
+ source,
+ created_at: Utc::now(),
+ updated_at: Utc::now(),
+ raw_settings: None,
+ raw_config_json: None,
+ };
+ store.claude_code.insert(profile_name.clone(), profile);
+ }
+ "codex" => {
+ // 从 extra_config 中提取 wire_api
+ let wire_api = extra_config
+ .as_ref()
+ .and_then(|v| v.get("wire_api"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("responses")
+ .to_string();
+
+ let profile = CodexProfile {
+ api_key,
+ base_url,
+ wire_api,
+ source,
+ created_at: Utc::now(),
+ updated_at: Utc::now(),
+ raw_config_toml: None,
+ raw_auth_json: None,
+ };
+ store.codex.insert(profile_name.clone(), profile);
+ }
+ "gemini-cli" => {
+ // 从 extra_config 中提取 model
+ let model = extra_config
+ .as_ref()
+ .and_then(|v| v.get("model"))
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string());
+
+ let profile = GeminiProfile {
+ api_key,
+ base_url,
+ model,
+ source,
+ created_at: Utc::now(),
+ updated_at: Utc::now(),
+ raw_settings: None,
+ raw_env: None,
+ };
+ store.gemini_cli.insert(profile_name.clone(), profile);
+ }
+ _ => return Err(format!("不支持的工具类型: {}", tool_id)),
+ }
+
+ store.metadata.last_updated = Utc::now();
+ manager
+ .save_profiles_store(&store)
+ .map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_validate_tool_id() {
+ let valid_ids = vec!["claude-code", "codex", "gemini-cli"];
+ for id in valid_ids {
+ assert!(
+ id == "claude-code" || id == "codex" || id == "gemini-cli",
+ "Tool ID validation failed for: {}",
+ id
+ );
+ }
+
+ let invalid_ids = vec!["invalid", "unknown-tool", ""];
+ for id in invalid_ids {
+ assert!(
+ id != "claude-code" && id != "codex" && id != "gemini-cli",
+ "Tool ID validation should fail for: {}",
+ id
+ );
+ }
+ }
+
+ #[test]
+ fn test_provider_creation() {
+ let provider = Provider {
+ id: "test-provider".to_string(),
+ name: "Test Provider".to_string(),
+ website_url: "https://api.test.com".to_string(),
+ user_id: "123".to_string(),
+ access_token: "token123".to_string(),
+ username: None,
+ is_default: false,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ assert_eq!(provider.id, "test-provider");
+ assert_eq!(provider.website_url, "https://api.test.com");
+ }
+
+ #[test]
+ fn test_profile_source_custom() {
+ let source = ProfileSource::Custom;
+ assert_eq!(source, ProfileSource::Custom);
+ }
+
+ #[test]
+ fn test_profile_source_imported() {
+ let source = ProfileSource::ImportedFromProvider {
+ provider_id: "provider-1".to_string(),
+ provider_name: "Provider One".to_string(),
+ remote_token_id: 100,
+ remote_token_name: "Token Name".to_string(),
+ group: "default".to_string(),
+ imported_at: 1234567890,
+ };
+
+ if let ProfileSource::ImportedFromProvider {
+ provider_id,
+ remote_token_id,
+ ..
+ } = source
+ {
+ assert_eq!(provider_id, "provider-1");
+ assert_eq!(remote_token_id, 100);
+ } else {
+ panic!("Expected ImportedFromProvider variant");
+ }
+ }
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 569449b..3f0b838 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -376,6 +376,14 @@ fn main() {
update_provider,
delete_provider,
validate_provider_config,
+ // 令牌资产管理命令(NEW API 集成)
+ fetch_provider_tokens,
+ fetch_provider_groups,
+ create_provider_token,
+ delete_provider_token,
+ update_provider_token,
+ import_token_as_profile,
+ create_custom_profile,
// Dashboard 管理命令
get_tool_instance_selection,
set_tool_instance_selection,
diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs
index c9ea2bd..d9b2002 100644
--- a/src-tauri/src/models/mod.rs
+++ b/src-tauri/src/models/mod.rs
@@ -3,6 +3,7 @@ pub mod config;
pub mod dashboard;
pub mod provider;
pub mod proxy_config;
+pub mod remote_token;
pub mod tool;
pub mod update;
@@ -12,5 +13,6 @@ pub use dashboard::*;
pub use provider::*;
// 只导出新的 proxy_config 类型,避免与 config.rs 中的旧类型冲突
pub use proxy_config::{ProxyMetadata, ProxyStore};
+pub use remote_token::*;
pub use tool::*;
pub use update::*;
diff --git a/src-tauri/src/models/remote_token.rs b/src-tauri/src/models/remote_token.rs
new file mode 100644
index 0000000..a182161
--- /dev/null
+++ b/src-tauri/src/models/remote_token.rs
@@ -0,0 +1,147 @@
+// Remote Token Models
+//
+// NEW API 远程令牌数据模型
+
+use serde::{Deserialize, Serialize};
+
+/// 远程令牌(从 NEW API 拉取,不本地持久化)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RemoteToken {
+ /// 令牌 ID
+ pub id: i64,
+ /// 用户 ID
+ #[serde(default)]
+ pub user_id: i64,
+ /// 令牌名称
+ pub name: String,
+ /// 令牌密钥
+ pub key: String,
+ /// 所属分组
+ pub group: String,
+ /// 剩余额度
+ pub remain_quota: i64,
+ /// 已使用额度
+ #[serde(default)]
+ pub used_quota: i64,
+ /// 过期时间(Unix 时间戳,-1 表示永不过期)
+ pub expired_time: i64,
+ /// 状态(1=启用,2=禁用)
+ pub status: i32,
+ /// 是否无限额度
+ pub unlimited_quota: bool,
+ /// 是否启用模型限制
+ #[serde(default)]
+ pub model_limits_enabled: bool,
+ /// 模型限制(逗号分隔的模型列表)
+ #[serde(default)]
+ pub model_limits: String,
+ /// 允许的 IP 地址(逗号分隔)
+ #[serde(default)]
+ pub allow_ips: String,
+ /// 是否支持跨分组重试
+ #[serde(default)]
+ pub cross_group_retry: bool,
+ /// 创建时间(Unix 时间戳)
+ pub created_time: i64,
+ /// 最后访问时间(Unix 时间戳)
+ #[serde(default)]
+ pub accessed_time: i64,
+}
+
+/// 远程令牌分组(API 返回的分组信息)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RemoteTokenGroupInfo {
+ /// 分组描述
+ pub desc: String,
+ /// 倍率
+ pub ratio: f64,
+}
+
+/// 远程令牌分组(前端展示用)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RemoteTokenGroup {
+ /// 分组 ID(即分组名称)
+ pub id: String,
+ /// 分组描述
+ pub desc: String,
+ /// 倍率
+ pub ratio: f64,
+}
+
+/// 创建远程令牌请求
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateRemoteTokenRequest {
+ /// 令牌名称
+ pub name: String,
+ /// 分组 ID
+ pub group_id: String,
+ /// 初始额度(-1 表示无限)
+ pub quota: i64,
+ /// 过期天数(0 表示永不过期)
+ pub expire_days: i32,
+}
+
+/// NEW API 通用响应结构
+#[derive(Debug, Deserialize)]
+pub struct NewApiResponse {
+ pub success: bool,
+ pub message: Option,
+ pub data: Option,
+}
+
+/// NEW API 令牌列表响应的 data 部分
+#[derive(Debug, Deserialize)]
+pub struct TokenListData {
+ pub page: i32,
+ pub page_size: i32,
+ pub total: i32,
+ pub items: Vec,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_remote_token_serialization() {
+ let token = RemoteToken {
+ id: 123,
+ user_id: 2703,
+ name: "Test Token".to_string(),
+ key: "sk-test123".to_string(),
+ group: "default".to_string(),
+ remain_quota: 100,
+ used_quota: 50,
+ expired_time: 1735200000,
+ status: 1,
+ unlimited_quota: false,
+ model_limits_enabled: false,
+ model_limits: String::new(),
+ allow_ips: String::new(),
+ cross_group_retry: false,
+ created_time: 1704067200,
+ accessed_time: 1704067200,
+ };
+
+ let json = serde_json::to_string(&token).unwrap();
+ let deserialized: RemoteToken = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(deserialized.id, token.id);
+ assert_eq!(deserialized.name, token.name);
+ assert_eq!(deserialized.key, token.key);
+ }
+
+ #[test]
+ fn test_create_request_serialization() {
+ let request = CreateRemoteTokenRequest {
+ name: "New Token".to_string(),
+ group_id: "group1".to_string(),
+ quota: -1,
+ expire_days: 30,
+ };
+
+ let json = serde_json::to_string(&request).unwrap();
+ assert!(json.contains("\"name\":\"New Token\""));
+ assert!(json.contains("\"quota\":-1"));
+ }
+}
diff --git a/src-tauri/src/services/migration_manager/migrations/profile_v2.rs b/src-tauri/src/services/migration_manager/migrations/profile_v2.rs
index 42b3fe8..7f66ddf 100644
--- a/src-tauri/src/services/migration_manager/migrations/profile_v2.rs
+++ b/src-tauri/src/services/migration_manager/migrations/profile_v2.rs
@@ -9,7 +9,8 @@
use crate::data::DataManager;
use crate::services::migration_manager::migration_trait::{Migration, MigrationResult};
use crate::services::profile_manager::{
- ActiveProfile, ActiveStore, ClaudeProfile, CodexProfile, GeminiProfile, ProfilesStore,
+ ActiveProfile, ActiveStore, ClaudeProfile, CodexProfile, GeminiProfile, ProfileSource,
+ ProfilesStore,
};
use anyhow::{Context, Result};
use async_trait::async_trait;
@@ -216,6 +217,7 @@ impl ProfileV2Migration {
updated_at: Utc::now(),
raw_settings: Some(settings_value),
raw_config_json: None,
+ source: ProfileSource::Custom,
};
profiles.insert(profile_name.clone(), profile);
tracing::info!("已从原始 Claude Code 配置迁移 Profile: {}", profile_name);
@@ -320,6 +322,7 @@ impl ProfileV2Migration {
updated_at: Utc::now(),
raw_config_toml,
raw_auth_json: Some(auth_data),
+ source: ProfileSource::Custom,
};
profiles.insert(profile_name.clone(), profile);
tracing::info!("已从原始 Codex 配置迁移 Profile: {}", profile_name);
@@ -394,6 +397,7 @@ impl ProfileV2Migration {
updated_at: Utc::now(),
raw_settings: None,
raw_env,
+ source: ProfileSource::Custom,
};
profiles.insert(profile_name.clone(), profile);
tracing::info!("已从原始 Gemini CLI 配置迁移 Profile: {}", profile_name);
@@ -488,6 +492,7 @@ impl ProfileV2Migration {
updated_at: descriptor.updated_at.unwrap_or_else(Utc::now),
raw_settings,
raw_config_json,
+ source: ProfileSource::Custom,
},
CodexProfile::default_placeholder(),
GeminiProfile::default_placeholder(),
@@ -525,6 +530,7 @@ impl ProfileV2Migration {
updated_at: descriptor.updated_at.unwrap_or_else(Utc::now),
raw_config_toml,
raw_auth_json,
+ source: ProfileSource::Custom,
},
GeminiProfile::default_placeholder(),
))
@@ -561,6 +567,7 @@ impl ProfileV2Migration {
updated_at: descriptor.updated_at.unwrap_or_else(Utc::now),
raw_settings,
raw_env,
+ source: ProfileSource::Custom,
},
))
}
@@ -856,6 +863,7 @@ impl ClaudeProfile {
updated_at: Utc::now(),
raw_settings: None,
raw_config_json: None,
+ source: ProfileSource::Custom,
}
}
}
@@ -870,6 +878,7 @@ impl CodexProfile {
updated_at: Utc::now(),
raw_config_toml: None,
raw_auth_json: None,
+ source: ProfileSource::Custom,
}
}
}
@@ -884,6 +893,7 @@ impl GeminiProfile {
updated_at: Utc::now(),
raw_settings: None,
raw_env: None,
+ source: ProfileSource::Custom,
}
}
}
diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs
index 467c960..16bf614 100644
--- a/src-tauri/src/services/mod.rs
+++ b/src-tauri/src/services/mod.rs
@@ -9,11 +9,13 @@
// - migration_manager: 统一迁移管理(新)
// - balance: 余额监控配置管理
// - provider_manager: 供应商配置管理
+// - new_api: NEW API 客户端服务
pub mod balance;
pub mod config;
pub mod dashboard_manager; // 仪表板状态管理
pub mod migration_manager;
+pub mod new_api; // NEW API 客户端
pub mod profile_manager; // Profile管理(v2.1)
pub mod provider_manager; // 供应商配置管理
pub mod proxy;
@@ -27,9 +29,10 @@ pub use balance::*;
pub use config::types::*; // 仅导出类型
pub use dashboard_manager::DashboardManager;
pub use migration_manager::{create_migration_manager, MigrationManager};
+pub use new_api::NewApiClient;
pub use profile_manager::{
ActiveStore, ClaudeProfile, CodexProfile, GeminiProfile, ProfileDescriptor, ProfileManager,
- ProfilesStore,
+ ProfileSource, ProfilesStore,
}; // Profile管理(v2.0)
pub use provider_manager::ProviderManager;
pub use proxy::*;
diff --git a/src-tauri/src/services/new_api/client.rs b/src-tauri/src/services/new_api/client.rs
new file mode 100644
index 0000000..7a34d87
--- /dev/null
+++ b/src-tauri/src/services/new_api/client.rs
@@ -0,0 +1,296 @@
+// NEW API Client
+//
+// NEW API 客户端服务,用于与供应商的 API 交互
+
+use crate::models::provider::Provider;
+use crate::models::remote_token::{
+ CreateRemoteTokenRequest, NewApiResponse, RemoteToken, RemoteTokenGroup, RemoteTokenGroupInfo,
+ TokenListData,
+};
+use anyhow::{anyhow, Result};
+use reqwest::Client;
+use serde_json::json;
+use std::collections::HashMap;
+use std::time::Duration;
+
+/// NEW API 客户端
+pub struct NewApiClient {
+ provider: Provider,
+ client: Client,
+}
+
+impl NewApiClient {
+ /// 创建新的 NEW API 客户端
+ pub fn new(provider: Provider) -> Result {
+ let client = Client::builder()
+ .timeout(Duration::from_secs(30))
+ .build()
+ .map_err(|e| anyhow!("创建 HTTP 客户端失败: {}", e))?;
+
+ Ok(Self { provider, client })
+ }
+
+ /// 获取基础 URL
+ fn base_url(&self) -> String {
+ self.provider.website_url.trim_end_matches('/').to_string()
+ }
+
+ /// 构建请求头
+ fn build_headers(&self) -> reqwest::header::HeaderMap {
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert(
+ "Authorization",
+ format!("Bearer {}", self.provider.access_token)
+ .parse()
+ .unwrap(),
+ );
+ headers.insert("New-Api-User", self.provider.user_id.parse().unwrap());
+ headers.insert("Content-Type", "application/json".parse().unwrap());
+ headers
+ }
+
+ /// 获取所有远程令牌列表
+ pub async fn list_tokens(&self) -> Result> {
+ let url = format!("{}/api/token", self.base_url());
+ let response = self
+ .client
+ .get(&url)
+ .headers(self.build_headers())
+ .send()
+ .await
+ .map_err(|e| anyhow!("请求失败: {}", e))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow!(
+ "API 请求失败,状态码: {}",
+ response.status().as_u16()
+ ));
+ }
+
+ let api_response: NewApiResponse = response
+ .json()
+ .await
+ .map_err(|e| anyhow!("解析响应失败: {}", e))?;
+
+ if !api_response.success {
+ return Err(anyhow!(
+ "API 返回错误: {}",
+ api_response
+ .message
+ .unwrap_or_else(|| "未知错误".to_string())
+ ));
+ }
+
+ Ok(api_response.data.map(|d| d.items).unwrap_or_default())
+ }
+
+ /// 获取所有令牌分组
+ pub async fn list_groups(&self) -> Result> {
+ let url = format!("{}/api/user/self/groups", self.base_url());
+ let response = self
+ .client
+ .get(&url)
+ .headers(self.build_headers())
+ .send()
+ .await
+ .map_err(|e| anyhow!("请求失败: {}", e))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow!(
+ "API 请求失败,状态码: {}",
+ response.status().as_u16()
+ ));
+ }
+
+ let api_response: NewApiResponse> = response
+ .json()
+ .await
+ .map_err(|e| anyhow!("解析响应失败: {}", e))?;
+
+ if !api_response.success {
+ return Err(anyhow!(
+ "API 返回错误: {}",
+ api_response
+ .message
+ .unwrap_or_else(|| "未知错误".to_string())
+ ));
+ }
+
+ // 将 HashMap 转换为 Vec
+ let groups = api_response
+ .data
+ .unwrap_or_default()
+ .into_iter()
+ .map(|(id, info)| RemoteTokenGroup {
+ id,
+ desc: info.desc,
+ ratio: info.ratio,
+ })
+ .collect();
+
+ Ok(groups)
+ }
+
+ /// 创建新的远程令牌
+ pub async fn create_token(&self, request: CreateRemoteTokenRequest) -> Result {
+ let url = format!("{}/api/token", self.base_url());
+ let body = json!({
+ "name": request.name,
+ "group_id": request.group_id,
+ "quota": request.quota,
+ "expire_days": request.expire_days,
+ });
+
+ let response = self
+ .client
+ .post(&url)
+ .headers(self.build_headers())
+ .json(&body)
+ .send()
+ .await
+ .map_err(|e| anyhow!("请求失败: {}", e))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow!(
+ "API 请求失败,状态码: {}",
+ response.status().as_u16()
+ ));
+ }
+
+ let api_response: NewApiResponse = response
+ .json()
+ .await
+ .map_err(|e| anyhow!("解析响应失败: {}", e))?;
+
+ if !api_response.success {
+ return Err(anyhow!(
+ "API 返回错误: {}",
+ api_response
+ .message
+ .unwrap_or_else(|| "未知错误".to_string())
+ ));
+ }
+
+ api_response
+ .data
+ .ok_or_else(|| anyhow!("API 未返回令牌数据"))
+ }
+
+ /// 删除远程令牌
+ pub async fn delete_token(&self, token_id: i64) -> Result<()> {
+ let url = format!("{}/api/token/{}", self.base_url(), token_id);
+ let response = self
+ .client
+ .delete(&url)
+ .headers(self.build_headers())
+ .send()
+ .await
+ .map_err(|e| anyhow!("请求失败: {}", e))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow!(
+ "API 请求失败,状态码: {}",
+ response.status().as_u16()
+ ));
+ }
+
+ let api_response: NewApiResponse<()> = response
+ .json()
+ .await
+ .map_err(|e| anyhow!("解析响应失败: {}", e))?;
+
+ if !api_response.success {
+ return Err(anyhow!(
+ "API 返回错误: {}",
+ api_response
+ .message
+ .unwrap_or_else(|| "未知错误".to_string())
+ ));
+ }
+
+ Ok(())
+ }
+
+ /// 更新远程令牌信息
+ pub async fn update_token(&self, token_id: i64, name: String) -> Result {
+ let url = format!("{}/api/token/{}", self.base_url(), token_id);
+ let body = json!({
+ "name": name,
+ });
+
+ let response = self
+ .client
+ .patch(&url)
+ .headers(self.build_headers())
+ .json(&body)
+ .send()
+ .await
+ .map_err(|e| anyhow!("请求失败: {}", e))?;
+
+ if !response.status().is_success() {
+ return Err(anyhow!(
+ "API 请求失败,状态码: {}",
+ response.status().as_u16()
+ ));
+ }
+
+ let api_response: NewApiResponse = response
+ .json()
+ .await
+ .map_err(|e| anyhow!("解析响应失败: {}", e))?;
+
+ if !api_response.success {
+ return Err(anyhow!(
+ "API 返回错误: {}",
+ api_response
+ .message
+ .unwrap_or_else(|| "未知错误".to_string())
+ ));
+ }
+
+ api_response
+ .data
+ .ok_or_else(|| anyhow!("API 未返回令牌数据"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_client_creation() {
+ let provider = Provider {
+ id: "test".to_string(),
+ name: "Test Provider".to_string(),
+ website_url: "https://test.com".to_string(),
+ user_id: "123".to_string(),
+ access_token: "token123".to_string(),
+ username: None,
+ is_default: false,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ let client = NewApiClient::new(provider);
+ assert!(client.is_ok());
+ }
+
+ #[test]
+ fn test_base_url() {
+ let provider = Provider {
+ id: "test".to_string(),
+ name: "Test Provider".to_string(),
+ website_url: "https://test.com/".to_string(),
+ user_id: "123".to_string(),
+ access_token: "token123".to_string(),
+ username: None,
+ is_default: false,
+ created_at: 0,
+ updated_at: 0,
+ };
+
+ let client = NewApiClient::new(provider).unwrap();
+ assert_eq!(client.base_url(), "https://test.com");
+ }
+}
diff --git a/src-tauri/src/services/new_api/mod.rs b/src-tauri/src/services/new_api/mod.rs
new file mode 100644
index 0000000..f14d29d
--- /dev/null
+++ b/src-tauri/src/services/new_api/mod.rs
@@ -0,0 +1,7 @@
+// NEW API Service Module
+//
+// NEW API 服务模块
+
+mod client;
+
+pub use client::NewApiClient;
diff --git a/src-tauri/src/services/profile_manager/manager.rs b/src-tauri/src/services/profile_manager/manager.rs
index bc41c74..1f0ca0a 100644
--- a/src-tauri/src/services/profile_manager/manager.rs
+++ b/src-tauri/src/services/profile_manager/manager.rs
@@ -41,7 +41,7 @@ impl ProfileManager {
})
}
- fn load_profiles_store(&self) -> Result {
+ pub fn load_profiles_store(&self) -> Result {
if !self.profiles_path.exists() {
return Ok(ProfilesStore::new());
}
@@ -49,7 +49,7 @@ impl ProfileManager {
serde_json::from_value(value).context("反序列化 ProfilesStore 失败")
}
- fn save_profiles_store(&self, store: &ProfilesStore) -> Result<()> {
+ pub fn save_profiles_store(&self, store: &ProfilesStore) -> Result<()> {
// 创建锁文件(与 profiles.json 同目录)
let lock_path = self.profiles_path.with_extension("lock");
let lock_file = File::create(&lock_path).context("创建锁文件失败")?;
@@ -121,6 +121,7 @@ impl ProfileManager {
updated_at: Utc::now(),
raw_settings: None,
raw_config_json: None,
+ source: ProfileSource::Custom,
}
};
@@ -206,6 +207,7 @@ impl ProfileManager {
updated_at: Utc::now(),
raw_config_toml: None,
raw_auth_json: None,
+ source: ProfileSource::Custom,
}
};
@@ -293,6 +295,7 @@ impl ProfileManager {
updated_at: Utc::now(),
raw_settings: None,
raw_env: None,
+ source: ProfileSource::Custom,
}
};
@@ -486,6 +489,7 @@ impl ProfileManager {
updated_at: Utc::now(),
raw_settings: None,
raw_config_json: None,
+ source: ProfileSource::Custom,
}
};
@@ -533,6 +537,7 @@ impl ProfileManager {
updated_at: Utc::now(),
raw_config_toml: None,
raw_auth_json: None,
+ source: ProfileSource::Custom,
}
};
@@ -582,6 +587,7 @@ impl ProfileManager {
updated_at: Utc::now(),
raw_settings: None,
raw_env: None,
+ source: ProfileSource::Custom,
}
};
diff --git a/src-tauri/src/services/profile_manager/mod.rs b/src-tauri/src/services/profile_manager/mod.rs
index e608e41..1f5289f 100644
--- a/src-tauri/src/services/profile_manager/mod.rs
+++ b/src-tauri/src/services/profile_manager/mod.rs
@@ -11,5 +11,5 @@ mod types;
pub use manager::ProfileManager;
pub use types::{
ActiveMetadata, ActiveProfile, ActiveStore, ClaudeProfile, CodexProfile, GeminiProfile,
- ProfileDescriptor, ProfilesMetadata, ProfilesStore,
+ ProfileDescriptor, ProfileSource, ProfilesMetadata, ProfilesStore,
};
diff --git a/src-tauri/src/services/profile_manager/types.rs b/src-tauri/src/services/profile_manager/types.rs
index 7d413eb..7e7100e 100644
--- a/src-tauri/src/services/profile_manager/types.rs
+++ b/src-tauri/src/services/profile_manager/types.rs
@@ -6,6 +6,32 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
+// ==================== Profile 来源标记 ====================
+
+/// Profile 来源类型
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
+#[serde(tag = "type")]
+pub enum ProfileSource {
+ /// 用户自定义创建
+ #[default]
+ Custom,
+ /// 从供应商远程令牌导入
+ ImportedFromProvider {
+ /// 供应商 ID
+ provider_id: String,
+ /// 供应商名称
+ provider_name: String,
+ /// 远程令牌 ID
+ remote_token_id: i64,
+ /// 远程令牌名称
+ remote_token_name: String,
+ /// 所属分组
+ group: String,
+ /// 导入时间(Unix 时间戳)
+ imported_at: i64,
+ },
+}
+
// ==================== 具体 Profile 类型 ====================
/// Claude Code Profile
@@ -13,6 +39,8 @@ use std::collections::HashMap;
pub struct ClaudeProfile {
pub api_key: String,
pub base_url: String,
+ #[serde(default)]
+ pub source: ProfileSource,
pub created_at: DateTime,
pub updated_at: DateTime,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -28,6 +56,8 @@ pub struct CodexProfile {
pub base_url: String,
#[serde(default = "default_codex_wire_api")]
pub wire_api: String, // "responses" 或 "chat"
+ #[serde(default)]
+ pub source: ProfileSource,
pub created_at: DateTime,
pub updated_at: DateTime,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -47,6 +77,8 @@ pub struct GeminiProfile {
pub base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option,
+ #[serde(default)]
+ pub source: ProfileSource,
pub created_at: DateTime,
pub updated_at: DateTime,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -224,6 +256,7 @@ pub struct ProfileDescriptor {
pub name: String,
pub api_key_preview: String,
pub base_url: String,
+ pub source: ProfileSource,
pub created_at: DateTime,
pub updated_at: DateTime,
pub is_active: bool,
@@ -253,6 +286,7 @@ impl ProfileDescriptor {
name: name.to_string(),
api_key_preview: mask_api_key(&profile.api_key),
base_url: profile.base_url.clone(),
+ source: profile.source.clone(),
created_at: profile.created_at,
updated_at: profile.updated_at,
is_active,
@@ -279,6 +313,7 @@ impl ProfileDescriptor {
name: name.to_string(),
api_key_preview: mask_api_key(&profile.api_key),
base_url: profile.base_url.clone(),
+ source: profile.source.clone(),
created_at: profile.created_at,
updated_at: profile.updated_at,
is_active,
@@ -305,6 +340,7 @@ impl ProfileDescriptor {
name: name.to_string(),
api_key_preview: mask_api_key(&profile.api_key),
base_url: profile.base_url.clone(),
+ source: profile.source.clone(),
created_at: profile.created_at,
updated_at: profile.updated_at,
is_active,
diff --git a/src/lib/tauri-commands/token.ts b/src/lib/tauri-commands/token.ts
new file mode 100644
index 0000000..0236ee1
--- /dev/null
+++ b/src/lib/tauri-commands/token.ts
@@ -0,0 +1,87 @@
+// Token Management Tauri Commands
+//
+// NEW API 令牌管理 Tauri 命令包装器
+
+import { invoke } from '@tauri-apps/api/core';
+import type { Provider } from '@/types/provider';
+import type { CreateRemoteTokenRequest, RemoteToken, RemoteTokenGroup } from '@/types/remote-token';
+
+/**
+ * 获取指定供应商的远程令牌列表
+ */
+export async function fetchProviderTokens(provider: Provider): Promise {
+ return invoke('fetch_provider_tokens', { provider });
+}
+
+/**
+ * 获取指定供应商的令牌分组列表
+ */
+export async function fetchProviderGroups(provider: Provider): Promise {
+ return invoke('fetch_provider_groups', { provider });
+}
+
+/**
+ * 在供应商创建新的远程令牌
+ */
+export async function createProviderToken(
+ provider: Provider,
+ request: CreateRemoteTokenRequest,
+): Promise {
+ return invoke('create_provider_token', { provider, request });
+}
+
+/**
+ * 删除供应商的远程令牌
+ */
+export async function deleteProviderToken(provider: Provider, tokenId: number): Promise {
+ return invoke('delete_provider_token', { provider, tokenId });
+}
+
+/**
+ * 更新供应商的远程令牌名称
+ */
+export async function updateProviderToken(
+ provider: Provider,
+ tokenId: number,
+ name: string,
+): Promise {
+ return invoke('update_provider_token', { provider, tokenId, name });
+}
+
+/**
+ * 导入远程令牌为本地 Profile
+ */
+export async function importTokenAsProfile(
+ provider: Provider,
+ remoteToken: RemoteToken,
+ toolId: string,
+ profileName: string,
+): Promise {
+ return invoke('import_token_as_profile', {
+ profileManager: null, // Managed by Tauri State
+ provider,
+ remoteToken,
+ toolId,
+ profileName,
+ });
+}
+
+/**
+ * 创建自定义 Profile(非导入令牌)
+ */
+export async function createCustomProfile(
+ toolId: string,
+ profileName: string,
+ apiKey: string,
+ baseUrl: string,
+ extraConfig?: { wire_api?: string; model?: string },
+): Promise {
+ return invoke('create_custom_profile', {
+ profileManager: null, // Managed by Tauri State
+ toolId,
+ profileName,
+ apiKey,
+ baseUrl,
+ extraConfig: extraConfig || null,
+ });
+}
diff --git a/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx b/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx
index b0d6339..5aa4b89 100644
--- a/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx
+++ b/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx
@@ -338,6 +338,14 @@ export function ActiveProfileCard({ group, proxyRunning }: ActiveProfileCardProp
+
{selectedInstance && (
<>
void;
+ toolId: ToolId;
+ onSuccess: () => void;
+}
+
+/**
+ * 从供应商导入 Profile 对话框
+ */
+export function ImportFromProviderDialog({
+ open,
+ onOpenChange,
+ toolId,
+ onSuccess,
+}: ImportFromProviderDialogProps) {
+ const { toast } = useToast();
+ const [loading, setLoading] = useState(false);
+ const [importing, setImporting] = useState(false);
+
+ // 供应商和令牌数据
+ const [providers, setProviders] = useState([]);
+ const [tokens, setTokens] = useState([]);
+
+ // 表单数据
+ const [selectedProviderId, setSelectedProviderId] = useState('');
+ const [selectedTokenId, setSelectedTokenId] = useState(null);
+ const [profileName, setProfileName] = useState('');
+
+ // 获取当前选中的供应商和令牌
+ const selectedProvider = providers.find((p) => p.id === selectedProviderId);
+ const selectedToken = tokens.find((t) => t.id === selectedTokenId);
+
+ /**
+ * 加载供应商列表
+ */
+ const loadProviders = async () => {
+ try {
+ setLoading(true);
+ const result = await listProviders();
+ setProviders(result);
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '加载供应商失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ /**
+ * 加载选中供应商的令牌列表
+ */
+ const loadTokens = async (provider: Provider) => {
+ try {
+ setLoading(true);
+ const result = await fetchProviderTokens(provider);
+ setTokens(result);
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '加载令牌失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ setTokens([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ /**
+ * Dialog 打开时加载供应商列表
+ */
+ useEffect(() => {
+ if (open) {
+ loadProviders();
+ // 重置表单
+ setSelectedProviderId('');
+ setSelectedTokenId(null);
+ setProfileName('');
+ setTokens([]);
+ }
+ }, [open]);
+
+ /**
+ * 供应商变更时加载令牌列表
+ */
+ useEffect(() => {
+ if (selectedProvider) {
+ loadTokens(selectedProvider);
+ setSelectedTokenId(null);
+ } else {
+ setTokens([]);
+ setSelectedTokenId(null);
+ }
+ }, [selectedProviderId]);
+
+ /**
+ * 令牌变更时自动填充 Profile 名称
+ */
+ useEffect(() => {
+ if (selectedToken && !profileName) {
+ setProfileName(selectedToken.name + '_profile');
+ }
+ }, [selectedTokenId]);
+
+ /**
+ * 提交导入
+ */
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!selectedProvider || !selectedToken) {
+ toast({
+ title: '请选择供应商和令牌',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ if (!profileName.trim()) {
+ toast({
+ title: '请输入 Profile 名称',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ // 检查保留前缀
+ if (profileName.startsWith('dc_proxy_')) {
+ toast({
+ title: '验证失败',
+ description: 'Profile 名称不能以 dc_proxy_ 开头(系统保留)',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ setImporting(true);
+ try {
+ // 检查是否已存在同名 Profile
+ const existingProfiles = await pmListToolProfiles(toolId);
+ if (existingProfiles.includes(profileName)) {
+ toast({
+ title: '验证失败',
+ description: '该 Profile 名称已存在,请使用其他名称',
+ variant: 'destructive',
+ });
+ setImporting(false);
+ return;
+ }
+
+ await importTokenAsProfile(selectedProvider, selectedToken, toolId, profileName);
+ toast({
+ title: '导入成功',
+ description:
+ '令牌「' + selectedToken.name + '」已成功导入为 Profile「' + profileName + '」',
+ });
+ onSuccess();
+ onOpenChange(false);
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '导入失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/pages/ProfileManagementPage/components/ProfileCard.tsx b/src/pages/ProfileManagementPage/components/ProfileCard.tsx
index a74e6cd..73fdc25 100644
--- a/src/pages/ProfileManagementPage/components/ProfileCard.tsx
+++ b/src/pages/ProfileManagementPage/components/ProfileCard.tsx
@@ -3,7 +3,7 @@
*/
import { useState } from 'react';
-import { Check, MoreVertical, Pencil, Power, Trash2 } from 'lucide-react';
+import { Check, MoreVertical, Pencil, Power, Trash2, Tag } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
@@ -61,6 +61,36 @@ export function ProfileCard({
}
};
+ /**
+ * 获取来源显示文本和样式
+ */
+ const getSourceInfo = () => {
+ if (profile.source.type === 'Custom') {
+ return {
+ text: '自定义',
+ variant: 'secondary' as const,
+ tooltip: '用户手动创建的 Profile',
+ };
+ } else {
+ const importedAt = new Date(profile.source.imported_at * 1000);
+ return {
+ text: '从 ' + profile.source.provider_name + ' 导入',
+ variant: 'outline' as const,
+ tooltip:
+ '从供应商「' +
+ profile.source.provider_name +
+ '」的令牌「' +
+ profile.source.remote_token_name +
+ '」导入\n分组: ' +
+ profile.source.group +
+ '\n导入时间: ' +
+ importedAt.toLocaleString('zh-CN'),
+ };
+ }
+ };
+
+ const sourceInfo = getSourceInfo();
+
return (
<>
@@ -74,6 +104,10 @@ export function ProfileCard({
激活中
)}
+
+
+ {sourceInfo.text}
+
API Key: {profile.api_key_preview}
diff --git a/src/pages/ProfileManagementPage/index.tsx b/src/pages/ProfileManagementPage/index.tsx
index 84a9bd9..dc4f571 100644
--- a/src/pages/ProfileManagementPage/index.tsx
+++ b/src/pages/ProfileManagementPage/index.tsx
@@ -3,9 +3,15 @@
*/
import { useState, useEffect } from 'react';
-import { RefreshCw, Loader2, HelpCircle } from 'lucide-react';
+import { RefreshCw, Loader2, HelpCircle, Plus, Download, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
@@ -17,6 +23,7 @@ import { PageContainer } from '@/components/layout/PageContainer';
import { ProfileCard } from './components/ProfileCard';
import { ProfileEditor } from './components/ProfileEditor';
import { ActiveProfileCard } from './components/ActiveProfileCard';
+import { ImportFromProviderDialog } from './components/ImportFromProviderDialog';
import { useProfileManagement } from './hooks/useProfileManagement';
import type { ToolId, ProfileFormData, ProfileDescriptor } from '@/types/profile';
import { logoMap } from '@/utils/constants';
@@ -40,6 +47,7 @@ export default function ProfileManagementPage() {
const [editorMode, setEditorMode] = useState<'create' | 'edit'>('create');
const [editingProfile, setEditingProfile] = useState(null);
const [helpDialogOpen, setHelpDialogOpen] = useState(false);
+ const [importDialogOpen, setImportDialogOpen] = useState(false);
// 初始化加载透明代理状态
useEffect(() => {
@@ -167,13 +175,24 @@ export default function ProfileManagementPage() {
{group.active_profile && ` · 当前激活: ${group.active_profile.name}`}
-
+
+
+
+
+
+
+
+ 手动创建
+
+ setImportDialogOpen(true)}>
+
+ 从供应商导入
+
+
+
{/* Profile 卡片列表 */}
@@ -213,6 +232,17 @@ export default function ProfileManagementPage() {
{/* 帮助弹窗 */}
+
+ {/* 从供应商导入对话框 */}
+ {
+ setImportDialogOpen(false);
+ refresh();
+ }}
+ />
);
}
diff --git a/src/pages/ProviderManagementPage/components/CreateRemoteTokenDialog.tsx b/src/pages/ProviderManagementPage/components/CreateRemoteTokenDialog.tsx
new file mode 100644
index 0000000..468f4e4
--- /dev/null
+++ b/src/pages/ProviderManagementPage/components/CreateRemoteTokenDialog.tsx
@@ -0,0 +1,310 @@
+// Create Remote Token Dialog
+//
+// 创建远程令牌对话框
+
+import { useState, useEffect } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Loader2 } from 'lucide-react';
+import type { Provider } from '@/types/provider';
+import type { RemoteTokenGroup, CreateRemoteTokenRequest } from '@/types/remote-token';
+import { fetchProviderGroups, createProviderToken } from '@/lib/tauri-commands/token';
+import { useToast } from '@/hooks/use-toast';
+
+interface CreateRemoteTokenDialogProps {
+ provider: Provider;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSuccess: () => void;
+}
+
+/**
+ * 创建远程令牌对话框
+ */
+export function CreateRemoteTokenDialog({
+ provider,
+ open,
+ onOpenChange,
+ onSuccess,
+}: CreateRemoteTokenDialogProps) {
+ const { toast } = useToast();
+ const [groups, setGroups] = useState([]);
+ const [loadingGroups, setLoadingGroups] = useState(false);
+ const [creating, setCreating] = useState(false);
+
+ const [formData, setFormData] = useState({
+ name: '',
+ group_id: '',
+ quota: -1, // 默认无限额度 (-1 表示无限)
+ expire_days: 0, // 默认永不过期 (0 表示永不过期)
+ });
+
+ const [unlimitedQuota, setUnlimitedQuota] = useState(true); // 默认勾选无限额度
+ const [unlimitedExpire, setUnlimitedExpire] = useState(false); // 默认不勾选无限时长
+
+ /**
+ * 加载分组列表
+ */
+ const loadGroups = async () => {
+ setLoadingGroups(true);
+ try {
+ const result = await fetchProviderGroups(provider);
+ setGroups(result);
+ // 如果有分组,默认选择第一个
+ if (result.length > 0 && !formData.group_id) {
+ setFormData((prev) => ({ ...prev, group_id: result[0].id }));
+ }
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '加载分组失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ } finally {
+ setLoadingGroups(false);
+ }
+ };
+
+ /**
+ * 提交创建
+ */
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // 验证表单
+ if (!formData.name.trim()) {
+ toast({
+ title: '验证失败',
+ description: '请输入令牌名称',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ if (!formData.group_id) {
+ toast({
+ title: '验证失败',
+ description: '请选择分组',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ setCreating(true);
+ try {
+ await createProviderToken(provider, formData);
+ toast({
+ title: '令牌已创建',
+ description: `令牌「${formData.name}」已成功创建`,
+ });
+ onSuccess();
+ onOpenChange(false);
+ // 重置表单
+ setFormData({
+ name: '',
+ group_id: groups.length > 0 ? groups[0].id : '',
+ quota: -1,
+ expire_days: 0,
+ });
+ setUnlimitedQuota(true);
+ setUnlimitedExpire(false);
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '创建失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ } finally {
+ setCreating(false);
+ }
+ };
+
+ /**
+ * 对话框打开时加载分组
+ */
+ useEffect(() => {
+ if (open) {
+ loadGroups();
+ }
+ }, [open]);
+
+ return (
+
+ );
+}
diff --git a/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx b/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx
new file mode 100644
index 0000000..f037667
--- /dev/null
+++ b/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx
@@ -0,0 +1,199 @@
+// Import Token Dialog
+//
+// 导入令牌为 Profile 对话框
+
+import { useState } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Loader2 } from 'lucide-react';
+import type { Provider } from '@/types/provider';
+import type { RemoteToken } from '@/types/remote-token';
+import { importTokenAsProfile } from '@/lib/tauri-commands/token';
+import { pmListToolProfiles } from '@/lib/tauri-commands/profile';
+import type { ToolId } from '@/lib/tauri-commands/types';
+import { useToast } from '@/hooks/use-toast';
+
+interface ImportTokenDialogProps {
+ provider: Provider;
+ token: RemoteToken;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSuccess: () => void;
+}
+
+const TOOL_OPTIONS = [
+ { id: 'claude-code', name: 'Claude Code' },
+ { id: 'codex', name: 'Codex' },
+ { id: 'gemini-cli', name: 'Gemini CLI' },
+];
+
+/**
+ * 导入令牌为 Profile 对话框
+ */
+export function ImportTokenDialog({
+ provider,
+ token,
+ open,
+ onOpenChange,
+ onSuccess,
+}: ImportTokenDialogProps) {
+ const { toast } = useToast();
+ const [importing, setImporting] = useState(false);
+ const [toolId, setToolId] = useState('claude-code');
+ const [profileName, setProfileName] = useState('');
+
+ /**
+ * 提交导入
+ */
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // 验证表单
+ if (!profileName.trim()) {
+ toast({
+ title: '验证失败',
+ description: '请输入 Profile 名称',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ // 检查保留前缀
+ if (profileName.startsWith('dc_proxy_')) {
+ toast({
+ title: '验证失败',
+ description: 'Profile 名称不能以 dc_proxy_ 开头(系统保留)',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ setImporting(true);
+ try {
+ // 检查是否已存在同名 Profile
+ const existingProfiles = await pmListToolProfiles(toolId as ToolId);
+ if (existingProfiles.includes(profileName)) {
+ const toolName = TOOL_OPTIONS.find((t) => t.id === toolId)?.name || toolId;
+ toast({
+ title: '验证失败',
+ description: `Profile「${profileName}」已存在于 ${toolName} 中,请使用其他名称`,
+ variant: 'destructive',
+ });
+ setImporting(false);
+ return;
+ }
+
+ await importTokenAsProfile(provider, token, toolId, profileName);
+ toast({
+ title: '导入成功',
+ description: `令牌「${token.name}」已成功导入为 Profile「${profileName}」`,
+ });
+ onSuccess();
+ // 重置表单
+ setProfileName('');
+ setToolId('claude-code');
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '导入失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx b/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx
new file mode 100644
index 0000000..84fb442
--- /dev/null
+++ b/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx
@@ -0,0 +1,239 @@
+// Remote Token Management Component
+//
+// 远程令牌管理组件 - 显示和管理供应商的远程令牌
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Loader2, Plus, Download, Trash2, RefreshCw } from 'lucide-react';
+import type { Provider } from '@/types/provider';
+import type { RemoteToken } from '@/types/remote-token';
+import { TOKEN_STATUS_TEXT, TOKEN_STATUS_VARIANT, TokenStatus } from '@/types/remote-token';
+import { fetchProviderTokens, deleteProviderToken } from '@/lib/tauri-commands/token';
+import { useToast } from '@/hooks/use-toast';
+import { CreateRemoteTokenDialog } from './CreateRemoteTokenDialog';
+import { ImportTokenDialog } from './ImportTokenDialog';
+
+interface RemoteTokenManagementProps {
+ provider: Provider;
+}
+
+/**
+ * 远程令牌管理组件
+ */
+export function RemoteTokenManagement({ provider }: RemoteTokenManagementProps) {
+ const { toast } = useToast();
+ const [tokens, setTokens] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
+ const [importDialogOpen, setImportDialogOpen] = useState(false);
+ const [selectedToken, setSelectedToken] = useState(null);
+
+ /**
+ * 加载令牌列表
+ */
+ const loadTokens = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const result = await fetchProviderTokens(provider);
+ setTokens(result);
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ setError(errorMsg);
+ toast({
+ title: '加载失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ /**
+ * 删除令牌
+ */
+ const handleDelete = async (tokenId: number, tokenName: string) => {
+ try {
+ await deleteProviderToken(provider, tokenId);
+ toast({
+ title: '令牌已删除',
+ description: `令牌「${tokenName}」已成功删除`,
+ });
+ await loadTokens();
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ toast({
+ title: '删除失败',
+ description: errorMsg,
+ variant: 'destructive',
+ });
+ }
+ };
+
+ /**
+ * 打开导入对话框
+ */
+ const handleImport = (token: RemoteToken) => {
+ setSelectedToken(token);
+ setImportDialogOpen(true);
+ };
+
+ /**
+ * 格式化时间戳
+ */
+ const formatTimestamp = (timestamp: number) => {
+ if (timestamp === -1 || timestamp === 0) return '永不过期';
+ return new Date(timestamp * 1000).toLocaleString('zh-CN');
+ };
+
+ /**
+ * 格式化额度
+ */
+ const formatQuota = (quota: number, unlimited: boolean) => {
+ if (unlimited) return '无限';
+ return `$${(quota / 1000000).toFixed(2)}`;
+ };
+
+ /**
+ * 组件加载时获取令牌列表
+ */
+ useEffect(() => {
+ loadTokens();
+ }, [provider.id]);
+
+ return (
+
+ {/* 操作栏 */}
+
+
远程令牌
+
+
+
+
+
+
+ {/* 错误提示 */}
+ {error && (
+
+ )}
+
+ {/* 加载状态 */}
+ {loading ? (
+
+
+ 加载中...
+
+ ) : tokens.length === 0 ? (
+
+ ) : (
+
+
+
+
+ 名称
+ 分组
+ 剩余额度
+ 状态
+ 过期时间
+ 操作
+
+
+
+ {tokens.map((token) => (
+
+ {/* 名称 */}
+ {token.name}
+
+ {/* 分组 */}
+ {token.group}
+
+ {/* 剩余额度 */}
+
+ {formatQuota(token.remain_quota, token.unlimited_quota)}
+
+
+ {/* 状态 */}
+
+
+ {TOKEN_STATUS_TEXT[token.status as TokenStatus]}
+
+
+
+ {/* 过期时间 */}
+
+ {formatTimestamp(token.expired_time)}
+
+
+ {/* 操作 */}
+
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* 创建令牌对话框 */}
+
+
+ {/* 导入令牌对话框 */}
+ {selectedToken && (
+
{
+ setImportDialogOpen(false);
+ setSelectedToken(null);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/src/pages/ProviderManagementPage/index.tsx b/src/pages/ProviderManagementPage/index.tsx
index 3978b02..c6409db 100644
--- a/src/pages/ProviderManagementPage/index.tsx
+++ b/src/pages/ProviderManagementPage/index.tsx
@@ -9,13 +9,14 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
-import { Building2, Plus, Pencil, Trash2, Loader2 } from 'lucide-react';
+import { Building2, Plus, Pencil, Trash2, Loader2, ChevronDown, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import type { Provider } from '@/lib/tauri-commands';
import { useToast } from '@/hooks/use-toast';
import { useProviderManagement } from './hooks/useProviderManagement';
import { ProviderFormDialog } from './components/ProviderFormDialog';
import { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
+import { RemoteTokenManagement } from './components/RemoteTokenManagement';
/**
* 供应商管理页面
@@ -31,6 +32,7 @@ export function ProviderManagementPage() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deletingProvider, setDeletingProvider] = useState(null);
const [deleting, setDeleting] = useState(false);
+ const [expandedProviderId, setExpandedProviderId] = useState(null);
/**
* 打开新增对话框
@@ -102,6 +104,13 @@ export function ProviderManagementPage() {
return new Date(timestamp * 1000).toLocaleString('zh-CN');
};
+ /**
+ * 切换展开/折叠
+ */
+ const toggleExpand = (providerId: string) => {
+ setExpandedProviderId((prev) => (prev === providerId ? null : providerId));
+ };
+
return (
@@ -141,6 +150,7 @@ export function ProviderManagementPage() {
+
名称
官网地址
用户名
@@ -150,51 +160,79 @@ export function ProviderManagementPage() {
{providers.map((provider) => {
+ const isExpanded = expandedProviderId === provider.id;
return (
-
- {/* 名称 */}
- {provider.name}
-
- {/* 官网地址 */}
-
-
- {provider.website_url}
-
-
-
- {/* 用户名 */}
- {provider.username || '-'}
-
- {/* 更新时间 */}
-
- {formatTimestamp(provider.updated_at)}
-
-
- {/* 操作 */}
-
-
-
+ <>
+
+ {/* 展开按钮 */}
+
-
-
-
+
+
+ {/* 名称 */}
+ {provider.name}
+
+ {/* 官网地址 */}
+
+
+ {provider.website_url}
+
+
+
+ {/* 用户名 */}
+ {provider.username || '-'}
+
+ {/* 更新时间 */}
+
+ {formatTimestamp(provider.updated_at)}
+
+
+ {/* 操作 */}
+
+
+
+
+
+
+
+
+ {/* 展开内容:令牌管理 */}
+ {isExpanded && (
+
+
+
+
+
+ )}
+ >
);
})}
diff --git a/src/types/profile.ts b/src/types/profile.ts
index 59e1a8a..c6e8437 100644
--- a/src/types/profile.ts
+++ b/src/types/profile.ts
@@ -27,7 +27,7 @@ export interface CodexProfilePayload {
export interface GeminiProfilePayload {
api_key: string;
base_url: string;
- model?: string; // 可选,不填则不修改原生配置
+ model?: string; // 可选,不填则不修改原生配置
}
/**
@@ -58,6 +58,21 @@ export interface ProfileData {
raw_env?: string;
}
+/**
+ * Profile 来源类型
+ */
+export type ProfileSource =
+ | { type: 'Custom' }
+ | {
+ type: 'ImportedFromProvider';
+ provider_id: string;
+ provider_name: string;
+ remote_token_id: number;
+ remote_token_name: string;
+ group: string;
+ imported_at: number; // Unix 时间戳
+ };
+
/**
* Profile 描述符(前端展示用)
*/
@@ -66,11 +81,12 @@ export interface ProfileDescriptor {
name: string;
api_key_preview: string; // 脱敏显示(如 "sk-ant-***xxx")
base_url: string;
+ source: ProfileSource; // Profile 来源信息
created_at: string; // ISO 8601 时间字符串
updated_at: string; // ISO 8601 时间字符串
is_active: boolean;
switched_at?: string; // 激活时间(ISO 8601 时间字符串)
- // Codex 特定字段(注意:后端是 wire_api,前端展示用 provider 兼容)
+ // Codex 特定字段(注意:后端是 wire_api,前端展示用 provider 兼容)
wire_api?: string;
provider?: string; // 向后兼容
// Gemini 特定字段
diff --git a/src/types/remote-token.ts b/src/types/remote-token.ts
new file mode 100644
index 0000000..7fbfac6
--- /dev/null
+++ b/src/types/remote-token.ts
@@ -0,0 +1,101 @@
+// Remote Token Types
+//
+// NEW API 远程令牌类型定义
+
+/**
+ * 远程令牌
+ */
+export interface RemoteToken {
+ id: number;
+ user_id: number;
+ name: string;
+ key: string;
+ group: string;
+ remain_quota: number;
+ used_quota: number;
+ expired_time: number;
+ status: number;
+ unlimited_quota: boolean;
+ model_limits_enabled: boolean;
+ model_limits: string;
+ allow_ips: string;
+ cross_group_retry: boolean;
+ created_time: number;
+ accessed_time: number;
+}
+
+/**
+ * 远程令牌分组
+ */
+export interface RemoteTokenGroup {
+ id: string;
+ desc: string;
+ ratio: number;
+}
+
+/**
+ * 创建远程令牌请求
+ */
+export interface CreateRemoteTokenRequest {
+ name: string;
+ group_id: string;
+ quota: number;
+ expire_days: number;
+}
+
+/**
+ * 导入令牌为 Profile 请求
+ */
+export interface ImportTokenAsProfileRequest {
+ provider_id: string;
+ remote_token: RemoteToken;
+ tool_id: string;
+ profile_name: string;
+}
+
+/**
+ * 创建自定义 Profile 请求
+ */
+export interface CreateCustomProfileRequest {
+ tool_id: string;
+ profile_name: string;
+ api_key: string;
+ base_url: string;
+ extra_config?: {
+ wire_api?: string; // Codex specific
+ model?: string; // Gemini specific
+ };
+}
+
+/**
+ * 令牌状态枚举
+ */
+export enum TokenStatus {
+ Enabled = 1,
+ Disabled = 2,
+ Expired = 3,
+ Exhausted = 4,
+}
+
+/**
+ * 令牌状态文本映射
+ */
+export const TOKEN_STATUS_TEXT: Record = {
+ [TokenStatus.Enabled]: '启用',
+ [TokenStatus.Disabled]: '禁用',
+ [TokenStatus.Expired]: '已过期',
+ [TokenStatus.Exhausted]: '已用尽',
+};
+
+/**
+ * 令牌状态颜色映射(用于 Badge)
+ */
+export const TOKEN_STATUS_VARIANT: Record<
+ TokenStatus,
+ 'default' | 'secondary' | 'destructive' | 'outline'
+> = {
+ [TokenStatus.Enabled]: 'default',
+ [TokenStatus.Disabled]: 'secondary',
+ [TokenStatus.Expired]: 'destructive',
+ [TokenStatus.Exhausted]: 'outline',
+};
From 84f4812c3a0b183d8e2ce782991a3042391fbe3f Mon Sep 17 00:00:00 2001
From: JSRCode <139555610+jsrcode@users.noreply.github.com>
Date: Mon, 5 Jan 2026 12:56:15 +0800
Subject: [PATCH 02/10] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E4=B8=B4?=
=?UTF-8?q?=E6=97=B6=E5=A4=87=E4=BB=BD=E6=96=87=E4=BB=B6=20temp=5Fold=5Fma?=
=?UTF-8?q?in.rs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 清理代码重构过程中遗留的临时备份文件
- 该文件为旧版 main.rs 的副本,已完成模块化拆分
- 保持代码库整洁
---
temp_old_main.rs | 2256 ----------------------------------------------
1 file changed, 2256 deletions(-)
delete mode 100644 temp_old_main.rs
diff --git a/temp_old_main.rs b/temp_old_main.rs
deleted file mode 100644
index 835043b..0000000
--- a/temp_old_main.rs
+++ /dev/null
@@ -1,2256 +0,0 @@
-// Prevents additional console window on Windows in release, DO NOT REMOVE!!
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-
-use serde::Serialize;
-use serde_json::Value;
-use std::env;
-use std::fs;
-use std::path::PathBuf;
-use std::process::Command;
-use tauri::{
- menu::{Menu, MenuItem, PredefinedMenuItem},
- tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
- AppHandle, Emitter, Manager, Runtime, WebviewWindow,
-};
-
-// 导入服务层
-use duckcoding::models::update::{PackageFormatInfo, PlatformInfo};
-use duckcoding::{
- services::config::{CodexSettingsPayload, GeminiEnvPayload, GeminiSettingsPayload},
- ConfigService, InstallMethod, InstallerService, Tool, UpdateInfo, UpdateService, UpdateStatus,
- VersionService,
-};
-// Use the shared GlobalConfig from the library crate (models::config)
-use duckcoding::GlobalConfig;
-// 导入透明代理服务
-use duckcoding::{ProxyConfig, TransparentProxyConfigService, TransparentProxyService};
-use std::sync::Arc;
-use tokio::sync::Mutex as TokioMutex;
-
-// DuckCoding API 响应结构
-#[derive(serde::Deserialize, Debug)]
-struct TokenData {
- id: i64,
- key: String,
- #[allow(dead_code)]
- name: String,
- #[allow(dead_code)]
- group: String,
-}
-
-#[derive(serde::Deserialize, Debug)]
-struct ApiResponse {
- success: bool,
- message: String,
- data: Option>,
-}
-
-#[derive(serde::Serialize)]
-struct GenerateApiKeyResult {
- success: bool,
- message: String,
- api_key: Option,
-}
-
-// 用量统计数据结构
-#[derive(serde::Deserialize, Serialize, Debug, Clone)]
-struct UsageData {
- id: i64,
- user_id: i64,
- username: String,
- model_name: String,
- created_at: i64,
- token_used: i64,
- count: i64,
- quota: i64,
-}
-
-#[derive(serde::Deserialize, Debug)]
-struct UsageApiResponse {
- success: bool,
- message: String,
- data: Option>,
-}
-
-#[derive(serde::Serialize)]
-struct UsageStatsResult {
- success: bool,
- message: String,
- data: Vec,
-}
-
-// 用户信息数据结构
-#[derive(serde::Deserialize, Serialize, Debug)]
-struct UserInfo {
- id: i64,
- username: String,
- quota: i64,
- used_quota: i64,
- request_count: i64,
-}
-
-#[derive(serde::Deserialize, Debug)]
-struct UserApiResponse {
- success: bool,
- message: String,
- data: Option,
-}
-
-#[derive(serde::Serialize)]
-struct UserQuotaResult {
- success: bool,
- message: String,
- total_quota: f64,
- used_quota: f64,
- remaining_quota: f64,
- request_count: i64,
-}
-
-// Windows特定:隐藏命令行窗口
-#[cfg(target_os = "windows")]
-use std::os::windows::process::CommandExt;
-
-const CLOSE_CONFIRM_EVENT: &str = "duckcoding://request-close-action";
-const SINGLE_INSTANCE_EVENT: &str = "single-instance";
-
-#[derive(Clone, Serialize)]
-struct SingleInstancePayload {
- args: Vec,
- cwd: String,
-}
-
-// 辅助函数:获取扩展的PATH环境变量
-fn get_extended_path() -> String {
- #[cfg(target_os = "windows")]
- {
- let user_profile =
- env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\Default".to_string());
-
- let mut system_paths = vec![
- // Claude Code 可能的安装路径
- format!("{}\\AppData\\Local\\Programs\\claude-code", user_profile),
- format!("{}\\AppData\\Roaming\\npm", user_profile),
- format!(
- "{}\\AppData\\Local\\Programs\\Python\\Python312",
- user_profile
- ),
- format!(
- "{}\\AppData\\Local\\Programs\\Python\\Python312\\Scripts",
- user_profile
- ),
- // 常见安装路径
- "C:\\Program Files\\nodejs".to_string(),
- "C:\\Program Files\\Git\\cmd".to_string(),
- // 系统路径
- "C:\\Windows\\System32".to_string(),
- "C:\\Windows".to_string(),
- ];
-
- // nvm-windows支持
- if let Ok(nvm_home) = env::var("NVM_HOME") {
- system_paths.insert(0, format!("{}\\current", nvm_home));
- }
-
- let current_path = env::var("PATH").unwrap_or_default();
- format!("{};{}", system_paths.join(";"), current_path)
- }
-
- #[cfg(not(target_os = "windows"))]
- {
- let home_dir = env::var("HOME").unwrap_or_else(|_| "/Users/default".to_string());
-
- let mut system_paths = vec![
- // Claude Code 可能的安装路径
- format!("{}/.local/bin", home_dir),
- format!("{}/.claude/bin", home_dir),
- format!("{}/.claude/local", home_dir), // Claude Code local安装
- // Homebrew
- "/opt/homebrew/bin".to_string(),
- "/usr/local/bin".to_string(),
- // 系统路径
- "/usr/bin".to_string(),
- "/bin".to_string(),
- "/usr/sbin".to_string(),
- "/sbin".to_string(),
- ];
-
- // nvm支持 - 优先使用当前激活的版本
- if let Ok(nvm_dir) = env::var("NVM_DIR") {
- // 检查nvm current symlink
- let nvm_current = format!("{}/current/bin", nvm_dir);
- if std::path::Path::new(&nvm_current).exists() {
- system_paths.insert(0, nvm_current);
- } else {
- // 如果没有current symlink,尝试读取.nvmrc或使用default
- let nvm_default = format!("{}/.nvm/versions/node/default/bin", home_dir);
- if std::path::Path::new(&nvm_default).exists() {
- system_paths.insert(0, nvm_default);
- }
- }
- } else {
- // 如果NVM_DIR未设置,尝试默认路径
- let nvm_current = format!("{}/.nvm/current/bin", home_dir);
- if std::path::Path::new(&nvm_current).exists() {
- system_paths.insert(0, nvm_current);
- }
- }
-
- format!(
- "{}:{}",
- system_paths.join(":"),
- env::var("PATH").unwrap_or_default()
- )
- }
-}
-
-//定义 Tauri Commands
-#[tauri::command]
-async fn check_installations() -> Result, String> {
- let installer = InstallerService::new();
- let mut result = Vec::new();
-
- for tool in Tool::all() {
- let installed = installer.is_installed(&tool).await;
- let version = if installed {
- installer.get_installed_version(&tool).await
- } else {
- None
- };
-
- result.push(ToolStatus {
- id: tool.id.clone(),
- name: tool.name.clone(),
- installed,
- version,
- });
- }
-
- Ok(result)
-}
-
-// 检测node环境
-#[tauri::command]
-async fn check_node_environment() -> Result {
- let run_command = |cmd: &str| -> Result {
- #[cfg(target_os = "windows")]
- {
- Command::new("cmd")
- .env("PATH", get_extended_path())
- .arg("/C")
- .arg(cmd)
- .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏终端窗口
- .output()
- }
- #[cfg(not(target_os = "windows"))]
- {
- Command::new("sh")
- .env("PATH", get_extended_path())
- .arg("-c")
- .arg(cmd)
- .output()
- }
- };
-
- // 检测node
- let (node_available, node_version) = if let Ok(output) = run_command("node --version 2>&1") {
- if output.status.success() {
- let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
- (true, Some(version))
- } else {
- (false, None)
- }
- } else {
- (false, None)
- };
-
- // 检测npm
- let (npm_available, npm_version) = if let Ok(output) = run_command("npm --version 2>&1") {
- if output.status.success() {
- let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
- (true, Some(version))
- } else {
- (false, None)
- }
- } else {
- (false, None)
- };
-
- Ok(NodeEnvironment {
- node_available,
- node_version,
- npm_available,
- npm_version,
- })
-}
-
-// 辅助函数:从全局配置应用代理
-async fn apply_proxy_if_configured() {
- if let Ok(Some(config)) = get_global_config().await {
- duckcoding::ProxyService::apply_proxy_from_config(&config);
- }
-}
-
-#[tauri::command]
-async fn install_tool(
- tool: String,
- method: String,
- force: Option,
-) -> Result {
- // 应用代理配置(如果已配置)
- apply_proxy_if_configured().await;
-
- let force = force.unwrap_or(false);
- #[cfg(debug_assertions)]
- println!(
- "Installing {} via {} (using InstallerService, force={})",
- tool, method, force
- );
-
- // 获取工具定义
- let tool_obj =
- Tool::by_id(&tool).ok_or_else(|| "❌ 未知的工具\n\n请联系开发者报告此问题".to_string())?;
-
- // 转换安装方法
- let install_method = match method.as_str() {
- "npm" => InstallMethod::Npm,
- "brew" => InstallMethod::Brew,
- "official" => InstallMethod::Official,
- _ => return Err(format!("❌ 未知的安装方法: {}", method)),
- };
-
- // 使用 InstallerService 安装
- let installer = InstallerService::new();
-
- match installer.install(&tool_obj, &install_method, force).await {
- Ok(_) => {
- // 安装成功,构造成功消息
- let message = match method.as_str() {
- "npm" => format!("✅ {} 安装成功!(通过 npm)", tool_obj.name),
- "brew" => format!("✅ {} 安装成功!(通过 Homebrew)", tool_obj.name),
- "official" => format!("✅ {} 安装成功!", tool_obj.name),
- _ => format!("✅ {} 安装成功!", tool_obj.name),
- };
-
- Ok(InstallResult {
- success: true,
- message,
- output: String::new(),
- })
- }
- Err(e) => {
- // 安装失败,返回错误信息
- Err(e.to_string())
- }
- }
-}
-
-// 只检查更新,不执行
-#[tauri::command]
-async fn check_update(tool: String) -> Result {
- // 应用代理配置(如果已配置)
- apply_proxy_if_configured().await;
-
- #[cfg(debug_assertions)]
- println!("Checking updates for {} (using VersionService)", tool);
-
- let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("未知工具: {}", tool))?;
-
- let version_service = VersionService::new();
-
- match version_service.check_version(&tool_obj).await {
- Ok(version_info) => Ok(UpdateResult {
- success: true,
- message: "检查完成".to_string(),
- has_update: version_info.has_update,
- current_version: version_info.installed_version,
- latest_version: version_info.latest_version,
- mirror_version: version_info.mirror_version,
- mirror_is_stale: Some(version_info.mirror_is_stale),
- tool_id: Some(tool.clone()),
- }),
- Err(e) => {
- // 降级:如果检查失败,返回无法检查但不报错
- Ok(UpdateResult {
- success: true,
- message: format!("无法检查更新: {}", e),
- has_update: false,
- current_version: None,
- latest_version: None,
- mirror_version: None,
- mirror_is_stale: None,
- tool_id: Some(tool.clone()),
- })
- }
- }
-}
-
-// 批量检查所有工具更新(优化:单次网络请求)
-#[tauri::command]
-async fn check_all_updates() -> Result, String> {
- // 应用代理配置(如果已配置)
- apply_proxy_if_configured().await;
-
- #[cfg(debug_assertions)]
- println!("Checking updates for all tools (batch mode)");
-
- let version_service = VersionService::new();
- let version_infos = version_service.check_all_tools().await;
-
- let results = version_infos
- .into_iter()
- .map(|info| UpdateResult {
- success: true,
- message: "检查完成".to_string(),
- has_update: info.has_update,
- current_version: info.installed_version,
- latest_version: info.latest_version,
- mirror_version: info.mirror_version,
- mirror_is_stale: Some(info.mirror_is_stale),
- tool_id: Some(info.tool_id),
- })
- .collect();
-
- Ok(results)
-}
-
-#[tauri::command]
-async fn update_tool(tool: String, force: Option) -> Result {
- // 应用代理配置(如果已配置)
- apply_proxy_if_configured().await;
-
- let force = force.unwrap_or(false);
- #[cfg(debug_assertions)]
- println!(
- "Updating {} (using InstallerService, force={})",
- tool, force
- );
-
- // 获取工具定义
- let tool_obj =
- Tool::by_id(&tool).ok_or_else(|| "❌ 未知的工具\n\n请联系开发者报告此问题".to_string())?;
-
- // 使用 InstallerService 更新(内部有120秒超时)
- let installer = InstallerService::new();
-
- // 执行更新,添加超时控制
- use tokio::time::{timeout, Duration};
-
- let update_result = timeout(Duration::from_secs(120), installer.update(&tool_obj, force)).await;
-
- match update_result {
- Ok(Ok(_)) => {
- // 更新成功,获取新版本
- let new_version = installer.get_installed_version(&tool_obj).await;
-
- Ok(UpdateResult {
- success: true,
- message: "✅ 更新成功!".to_string(),
- has_update: false,
- current_version: new_version.clone(),
- latest_version: new_version,
- mirror_version: None,
- mirror_is_stale: None,
- tool_id: Some(tool.clone()),
- })
- }
- Ok(Err(e)) => {
- // 更新失败,检查特殊错误情况
- let error_str = e.to_string();
-
- // 检查是否是 Homebrew 版本滞后
- if error_str.contains("Not upgrading") && error_str.contains("already installed") {
- return Err(
- "⚠️ Homebrew版本滞后\n\nHomebrew cask的版本更新不及时,目前是旧版本。\n\n✅ 解决方案:\n\n方案1 - 使用npm安装最新版本(自动使用国内镜像):\n1. 卸载Homebrew版本:brew uninstall --cask codex\n2. 安装npm版本:npm install -g @openai/codex --registry https://registry.npmmirror.com\n\n方案2 - 等待Homebrew cask更新\n(可能需要几天到几周时间)\n\n推荐使用方案1,npm版本更新更及时。".to_string()
- );
- }
-
- // 检查npm是否显示已经是最新版本
- if error_str.contains("up to date") {
- return Err(
- "ℹ️ 已是最新版本\n\n当前安装的版本已经是最新版本,无需更新。".to_string(),
- );
- }
-
- // 检查是否是 npm 缓存权限错误
- if error_str.contains("EACCES") && error_str.contains(".npm") {
- return Err(
- "⚠️ npm 权限问题\n\n这是因为之前使用 sudo npm 安装导致的。\n\n✅ 解决方案(任选其一):\n\n方案1 - 修复 npm 权限(推荐):\n在终端运行:\nsudo chown -R $(id -u):$(id -g) \"$HOME/.npm\"\n\n方案2 - 配置 npm 使用用户目录:\nnpm config set prefix ~/.npm-global\nexport PATH=~/.npm-global/bin:$PATH\n\n方案3 - macOS 用户切换到 Homebrew(无需 sudo):\nbrew uninstall --cask codex\nbrew install --cask codex\n\n然后重试更新。".to_string()
- );
- }
-
- // 其他错误
- Err(error_str)
- }
- Err(_) => {
- // 超时
- Err("⏱️ 更新超时(120秒)\n\n可能的原因:\n• 网络连接不稳定\n• 服务器响应慢\n\n建议:\n1. 检查网络连接\n2. 重试更新\n3. 或尝试手动更新(详见文档)".to_string())
- }
- }
-}
-
-#[tauri::command]
-async fn configure_api(
- tool: String,
- _provider: String,
- api_key: String,
- base_url: Option,
- profile_name: Option,
-) -> Result<(), String> {
- #[cfg(debug_assertions)]
- println!("Configuring {} (using ConfigService)", tool);
-
- // 获取工具定义
- let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("❌ 未知的工具: {}", tool))?;
-
- // 获取 base_url,根据工具类型使用不同的默认值
- let base_url_str = base_url.unwrap_or_else(|| match tool.as_str() {
- "codex" => "https://jp.duckcoding.com/v1".to_string(),
- _ => "https://jp.duckcoding.com".to_string(),
- });
-
- // 使用 ConfigService 应用配置
- ConfigService::apply_config(&tool_obj, &api_key, &base_url_str, profile_name.as_deref())
- .map_err(|e| e.to_string())?;
-
- Ok(())
-}
-
-#[tauri::command]
-async fn list_profiles(tool: String) -> Result, String> {
- #[cfg(debug_assertions)]
- println!("Listing profiles for {} (using ConfigService)", tool);
-
- // 获取工具定义
- let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("❌ 未知的工具: {}", tool))?;
-
- // 使用 ConfigService 列出配置
- ConfigService::list_profiles(&tool_obj).map_err(|e| e.to_string())
-}
-
-#[tauri::command]
-async fn switch_profile(
- tool: String,
- profile: String,
- state: tauri::State<'_, TransparentProxyState>,
-) -> Result<(), String> {
- #[cfg(debug_assertions)]
- println!(
- "Switching profile for {} to {} (using ConfigService)",
- tool, profile
- );
-
- // 获取工具定义
- let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("❌ 未知的工具: {}", tool))?;
-
- // 使用 ConfigService 激活配置
- ConfigService::activate_profile(&tool_obj, &profile).map_err(|e| e.to_string())?;
-
- // 如果是 ClaudeCode 且透明代理已启用,需要更新真实配置
- if tool == "claude-code" {
- // 读取全局配置
- if let Ok(Some(mut global_config)) = get_global_config().await {
- if global_config.transparent_proxy_enabled {
- // 读取切换后的真实配置
- let config_path = tool_obj.config_dir.join(&tool_obj.config_file);
- if config_path.exists() {
- if let Ok(content) = fs::read_to_string(&config_path) {
- if let Ok(settings) = serde_json::from_str::(&content) {
- if let Some(env) = settings.get("env").and_then(|v| v.as_object()) {
- let new_api_key = env
- .get("ANTHROPIC_AUTH_TOKEN")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- let new_base_url = env
- .get("ANTHROPIC_BASE_URL")
- .and_then(|v| v.as_str())
- .unwrap_or("");
-
- // 检查透明代理功能是否启用
- let transparent_proxy_enabled =
- global_config.transparent_proxy_enabled;
-
- if !new_api_key.is_empty() && !new_base_url.is_empty() {
- // 总是保存新的真实配置到全局配置(不管代理是否在运行)
- TransparentProxyConfigService::update_real_config(
- &tool_obj,
- &mut global_config,
- new_api_key,
- new_base_url,
- )
- .map_err(|e| format!("更新真实配置失败: {}", e))?;
-
- // 保存全局配置
- save_global_config(global_config.clone())
- .await
- .map_err(|e| format!("保存全局配置失败: {}", e))?;
-
- // 如果透明代理功能启用且代理服务正在运行,更新代理配置
- if transparent_proxy_enabled {
- let service = state.service.lock().await;
- if service.is_running().await {
- let local_api_key = global_config
- .transparent_proxy_api_key
- .clone()
- .unwrap_or_default();
-
- let proxy_config = ProxyConfig {
- target_api_key: new_api_key.to_string(),
- target_base_url: new_base_url.to_string(),
- local_api_key,
- };
-
- service
- .update_config(proxy_config)
- .await
- .map_err(|e| format!("更新代理配置失败: {}", e))?;
-
- println!("✅ 透明代理配置已自动更新");
- drop(service); // 释放锁
- } // 闭合 if service.is_running()
- } // 闭合 if transparent_proxy_enabled
-
- // 只有在透明代理功能启用时才恢复 ClaudeCode 配置指向本地代理
- if transparent_proxy_enabled {
- let local_proxy_port = global_config.transparent_proxy_port;
- let local_proxy_key = global_config
- .transparent_proxy_api_key
- .unwrap_or_default();
-
- let mut settings_mut = settings.clone();
- if let Some(env_mut) = settings_mut
- .get_mut("env")
- .and_then(|v| v.as_object_mut())
- {
- env_mut.insert(
- "ANTHROPIC_AUTH_TOKEN".to_string(),
- Value::String(local_proxy_key),
- );
- env_mut.insert(
- "ANTHROPIC_BASE_URL".to_string(),
- Value::String(format!(
- "http://127.0.0.1:{}",
- local_proxy_port
- )),
- );
-
- let json = serde_json::to_string_pretty(&settings_mut)
- .map_err(|e| format!("序列化配置失败: {}", e))?;
- fs::write(&config_path, json)
- .map_err(|e| format!("写入配置失败: {}", e))?;
-
- println!("✅ ClaudeCode 配置已恢复指向本地代理");
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
-
- Ok(())
-}
-
-#[tauri::command]
-async fn delete_profile(tool: String, profile: String) -> Result<(), String> {
- #[cfg(debug_assertions)]
- println!("Deleting profile: tool={}, profile={}", tool, profile);
-
- // 获取工具定义
- let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("❌ 未知的工具: {}", tool))?;
-
- // 使用 ConfigService 删除配置
- ConfigService::delete_profile(&tool_obj, &profile).map_err(|e| e.to_string())?;
-
- #[cfg(debug_assertions)]
- println!("Successfully deleted profile: {}", profile);
-
- Ok(())
-}
-
-// 数据结构定义
-#[derive(serde::Serialize, serde::Deserialize, Clone)]
-struct ToolStatus {
- id: String,
- name: String,
- installed: bool,
- version: Option,
-}
-
-#[derive(serde::Serialize, serde::Deserialize)]
-struct NodeEnvironment {
- node_available: bool,
- node_version: Option,
- npm_available: bool,
- npm_version: Option,
-}
-
-#[derive(serde::Serialize, serde::Deserialize)]
-struct InstallResult {
- success: bool,
- message: String,
- output: String,
-}
-
-#[derive(serde::Serialize, serde::Deserialize)]
-struct UpdateResult {
- success: bool,
- message: String,
- has_update: bool,
- current_version: Option,
- latest_version: Option,
- mirror_version: Option, // 镜像实际可安装的版本
- mirror_is_stale: Option, // 镜像是否滞后
- tool_id: Option, // 新增:工具ID,用于批量检查时识别工具
-}
-
-#[derive(serde::Serialize, serde::Deserialize)]
-struct ActiveConfig {
- api_key: String,
- base_url: String,
- profile_name: Option, // 当前配置的名称
-}
-
-// 全局配置辅助函数
-fn get_global_config_path() -> Result {
- let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
- let config_dir = home_dir.join(".duckcoding");
- if !config_dir.exists() {
- fs::create_dir_all(&config_dir)
- .map_err(|e| format!("Failed to create config directory: {}", e))?;
- }
- Ok(config_dir.join("config.json"))
-}
-
-// Tauri命令:保存全局配置
-#[tauri::command]
-async fn save_global_config(config: GlobalConfig) -> Result<(), String> {
- let config_path = get_global_config_path()?;
-
- let json = serde_json::to_string_pretty(&config)
- .map_err(|e| format!("Failed to serialize config: {}", e))?;
-
- fs::write(&config_path, json).map_err(|e| format!("Failed to write config: {}", e))?;
-
- println!("Config saved successfully");
-
- // 设置文件权限为仅所有者可读写(Unix系统)
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let metadata = fs::metadata(&config_path)
- .map_err(|e| format!("Failed to get file metadata: {}", e))?;
- let mut perms = metadata.permissions();
- perms.set_mode(0o600); // -rw-------
- fs::set_permissions(&config_path, perms)
- .map_err(|e| format!("Failed to set file permissions: {}", e))?;
- }
-
- // 立即应用代理配置到环境变量
- duckcoding::ProxyService::apply_proxy_from_config(&config);
-
- Ok(())
-}
-
-// Tauri命令:读取全局配置
-#[tauri::command]
-async fn get_global_config() -> Result