Skip to content

Commit aca666e

Browse files
committed
feat: v0.10.0 - 配置与凭证统一导入导出功能
主要更新: - 实现以 YAML 配置文件为单一数据源的统一配置管理 - 支持凭证池配置同步到 YAML 文件 - 实现配置和凭证的统一导入导出 - 支持 OAuth Token 文件存储在可配置的 auth-dir 目录 - 实现配置热重载时自动同步凭证池 - 添加导出脱敏功能保护敏感信息 - 支持导入时合并或替换模式
1 parent a18384b commit aca666e

48 files changed

Lines changed: 11340 additions & 231 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "proxycast",
33
"private": true,
4-
"version": "0.9.0",
4+
"version": "0.10.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "proxycast"
3-
version = "0.9.0"
3+
version = "0.10.0"
44
description = "AI API Proxy Desktop App"
55
authors = ["you"]
66
edition = "2021"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc c887db8633047b16f94b48762dc9bfe3c65bb683295d9047265207226e4e0f0b # shrinks to path = "~/."
8+
cc 16635f3c212b2769f7fb51aec0142ac6e9e1a66d2f9e3118829cfba4ba86e4f8 # shrinks to (yaml_with_comments, original_comments) = ("# 0\nserver:\n# \n host: 127.0.0.1\n port: 1\n api_key: 0__AAA0_\nproviders:\n kiro:\n# a\n enabled: false\n region: us-east-1\n gemini:\n enabled: false\n credentials_path: 0-Aaa\n# 1\n qwen:\n enabled: false\n openai:\n enabled: false\n claude:\n enabled: false\ndefault_provider: kiro\nrouting:\n default_provider: kiro\n rules: []\n model_aliases: {}\n exclusions: {}\nretry:\n max_retries: 1\n base_delay_ms: 1\n max_delay_ms: 5000\n auto_switch_provider: false\nlogging:\n enabled: false\n level: debug\n retention_days: 1\n include_request_body: false\ninjection:\n enabled: false\n rules: []\nauth_dir: ~/.proxycast/auth\ncredential_pool: {}", ["# 0", "# ", "# a", "# 1"]), new_config = Config { server: ServerConfig { host: "127.0.0.1", port: 1, api_key: "a_a--a-a" }, providers: ProvidersConfig { kiro: ProviderConfig { enabled: false, credentials_path: None, region: None, project_id: None }, gemini: ProviderConfig { enabled: false, credentials_path: None, region: None, project_id: None }, qwen: ProviderConfig { enabled: false, credentials_path: None, region: None, project_id: Some("J3JRS6") }, openai: CustomProviderConfig { enabled: false, api_key: None, base_url: None }, claude: CustomProviderConfig { enabled: true, api_key: None, base_url: None } }, default_provider: "kiro", routing: RoutingConfig { default_provider: "kiro", rules: [RoutingRuleConfig { pattern: "cmbvvlhoabjthfkoczp-*", provider: "gemini", priority: 33 }], model_aliases: {}, exclusions: {} }, retry: RetrySettings { max_retries: 86, base_delay_ms: 4635, max_delay_ms: 9567, auto_switch_provider: false }, logging: LoggingConfig { enabled: false, level: "debug", retention_days: 16, include_request_body: false }, injection: InjectionSettings { enabled: false, rules: [] }, auth_dir: "~/.proxycast/auth", credential_pool: CredentialPoolConfig { kiro: [], gemini: [], qwen: [], openai: [], claude: [] } }
9+
cc d22f0e24d166175ada35e5f5c4874b91f97fc63e0b40e6207ad9c70c07ecddda # shrinks to content = "{\"version\": }"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc bba7155a7ed5c22d19990ebdd9935e761dbdc41bc1e11076fbbf4218b82c8002 # shrinks to request_id = "0-AAaA-a", (original_data, sse_body) = ([" "], "data: \n\n")
8+
cc 4f0d0f49ef961c396cdf1aa7ccbd2a01b525e96ba5a2048d18860dbe763abe37 # shrinks to request_id = "a00000aa", data = " ", index = 0

src-tauri/src/commands/config_cmd.rs

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::config::{Config, ConfigManager};
1+
use crate::config::{
2+
Config, ConfigManager, ExportBundle, ExportOptions as ExportServiceOptions, ExportService,
3+
ImportOptions as ImportServiceOptions, ImportService, ValidationResult,
4+
};
25
use crate::models::AppType;
36
use serde::{Deserialize, Serialize};
47
use std::path::PathBuf;
@@ -356,3 +359,217 @@ pub fn get_config_paths() -> Result<ConfigPathInfo, String> {
356359
json_exists: json_path.exists(),
357360
})
358361
}
362+
363+
// ============ Enhanced Export/Import Commands (using ExportService/ImportService) ============
364+
365+
/// 统一导出选项
366+
#[derive(Debug, Clone, Serialize, Deserialize)]
367+
pub struct UnifiedExportOptions {
368+
/// 是否包含配置
369+
pub include_config: bool,
370+
/// 是否包含凭证
371+
pub include_credentials: bool,
372+
/// 是否脱敏敏感信息
373+
pub redact_secrets: bool,
374+
}
375+
376+
/// 统一导出结果
377+
#[derive(Debug, Clone, Serialize, Deserialize)]
378+
pub struct UnifiedExportResult {
379+
/// 导出包内容(JSON 格式)
380+
pub content: String,
381+
/// 建议的文件名
382+
pub suggested_filename: String,
383+
/// 是否已脱敏
384+
pub redacted: bool,
385+
/// 是否包含配置
386+
pub has_config: bool,
387+
/// 是否包含凭证
388+
pub has_credentials: bool,
389+
}
390+
391+
/// 导出完整的配置和凭证包
392+
///
393+
/// # Arguments
394+
/// * `config` - 当前配置
395+
/// * `options` - 导出选项
396+
///
397+
/// # Requirements: 3.1, 3.2
398+
#[tauri::command]
399+
pub fn export_bundle(
400+
config: Config,
401+
options: UnifiedExportOptions,
402+
) -> Result<UnifiedExportResult, String> {
403+
let export_options = ExportServiceOptions {
404+
include_config: options.include_config,
405+
include_credentials: options.include_credentials,
406+
redact_secrets: options.redact_secrets,
407+
};
408+
409+
// 获取应用版本
410+
let app_version = env!("CARGO_PKG_VERSION").to_string();
411+
412+
let bundle =
413+
ExportService::export(&config, &export_options, &app_version).map_err(|e| e.to_string())?;
414+
415+
let content = bundle.to_json().map_err(|e| e.to_string())?;
416+
417+
// 生成带时间戳的文件名
418+
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
419+
let suffix = if options.redact_secrets {
420+
"_redacted"
421+
} else {
422+
""
423+
};
424+
let scope = match (options.include_config, options.include_credentials) {
425+
(true, true) => "full",
426+
(true, false) => "config",
427+
(false, true) => "credentials",
428+
(false, false) => "empty",
429+
};
430+
let suggested_filename = format!("proxycast_{}_{}{}.json", scope, timestamp, suffix);
431+
432+
Ok(UnifiedExportResult {
433+
content,
434+
suggested_filename,
435+
redacted: bundle.redacted,
436+
has_config: bundle.has_config(),
437+
has_credentials: bundle.has_credentials(),
438+
})
439+
}
440+
441+
/// 仅导出配置为 YAML
442+
///
443+
/// # Arguments
444+
/// * `config` - 当前配置
445+
/// * `redact_secrets` - 是否脱敏敏感信息
446+
///
447+
/// # Requirements: 3.1, 5.1
448+
#[tauri::command]
449+
pub fn export_config_yaml(config: Config, redact_secrets: bool) -> Result<ExportResult, String> {
450+
let content = ExportService::export_yaml(&config, redact_secrets).map_err(|e| e.to_string())?;
451+
452+
// 生成带时间戳的文件名
453+
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
454+
let suffix = if redact_secrets { "_redacted" } else { "" };
455+
let suggested_filename = format!("proxycast_config_{}{}.yaml", timestamp, suffix);
456+
457+
Ok(ExportResult {
458+
content,
459+
suggested_filename,
460+
})
461+
}
462+
463+
/// 验证导入内容
464+
///
465+
/// # Arguments
466+
/// * `content` - 导入内容(JSON 导出包或 YAML 配置)
467+
///
468+
/// # Requirements: 4.1, 4.2
469+
#[tauri::command]
470+
pub fn validate_import(content: String) -> Result<ValidationResult, String> {
471+
Ok(ImportService::validate(&content))
472+
}
473+
474+
/// 导入完整的导出包
475+
///
476+
/// # Arguments
477+
/// * `current_config` - 当前配置
478+
/// * `content` - 导出包内容(JSON 格式)
479+
/// * `merge` - 是否合并到现有配置
480+
///
481+
/// # Requirements: 4.1, 4.3
482+
#[tauri::command]
483+
pub fn import_bundle(
484+
current_config: Config,
485+
content: String,
486+
merge: bool,
487+
) -> Result<ImportResult, String> {
488+
// 首先尝试解析为 ExportBundle
489+
if let Ok(bundle) = ExportBundle::from_json(&content) {
490+
let options = ImportServiceOptions { merge };
491+
let result =
492+
ImportService::import(&bundle, &current_config, &options, &current_config.auth_dir)
493+
.map_err(|e| e.to_string())?;
494+
495+
return Ok(ImportResult {
496+
success: result.success,
497+
config: result.config,
498+
warnings: result.warnings,
499+
});
500+
}
501+
502+
// 尝试解析为 YAML 配置
503+
let options = ImportServiceOptions { merge };
504+
let result = ImportService::import_yaml(&content, &current_config, &options)
505+
.map_err(|e| e.to_string())?;
506+
507+
Ok(ImportResult {
508+
success: result.success,
509+
config: result.config,
510+
warnings: result.warnings,
511+
})
512+
}
513+
514+
// ============ Path Utility Commands ============
515+
516+
/// 展开路径中的 tilde (~) 为用户主目录
517+
///
518+
/// # Arguments
519+
/// * `path` - 要展开的路径字符串
520+
///
521+
/// # Returns
522+
/// 展开后的完整路径字符串
523+
///
524+
/// # Requirements: 2.3
525+
#[tauri::command]
526+
pub fn expand_path(path: String) -> Result<String, String> {
527+
use crate::config::expand_tilde;
528+
529+
let expanded = expand_tilde(&path);
530+
Ok(expanded.to_string_lossy().to_string())
531+
}
532+
533+
/// 打开认证目录
534+
///
535+
/// # Arguments
536+
/// * `path` - 认证目录路径(支持 tilde 展开)
537+
///
538+
/// # Requirements: 2.2
539+
#[tauri::command]
540+
pub async fn open_auth_dir(path: String) -> Result<bool, String> {
541+
use crate::config::expand_tilde;
542+
543+
let expanded = expand_tilde(&path);
544+
545+
// 确保目录存在
546+
if !expanded.exists() {
547+
std::fs::create_dir_all(&expanded).map_err(|e| e.to_string())?;
548+
}
549+
550+
#[cfg(target_os = "macos")]
551+
{
552+
std::process::Command::new("open")
553+
.arg(&expanded)
554+
.spawn()
555+
.map_err(|e| e.to_string())?;
556+
}
557+
558+
#[cfg(target_os = "windows")]
559+
{
560+
std::process::Command::new("explorer")
561+
.arg(&expanded)
562+
.spawn()
563+
.map_err(|e| e.to_string())?;
564+
}
565+
566+
#[cfg(target_os = "linux")]
567+
{
568+
std::process::Command::new("xdg-open")
569+
.arg(&expanded)
570+
.spawn()
571+
.map_err(|e| e.to_string())?;
572+
}
573+
574+
Ok(true)
575+
}

0 commit comments

Comments
 (0)