feat: add model configuration support and fix Gemini deeplink bug (#251)

* feat(providers): add notes field for provider management

- Add notes field to Provider model (backend and frontend)
- Display notes with higher priority than URL in provider card
- Style notes as non-clickable text to differentiate from URLs
- Add notes input field in provider form
- Add i18n support (zh/en) for notes field

* chore: format code and clean up unused props

- Run cargo fmt on Rust backend code
- Format TypeScript imports and code style
- Remove unused appId prop from ProviderPresetSelector
- Clean up unused variables in tests
- Integrate notes field handling in provider dialogs

* feat(deeplink): implement ccswitch:// protocol for provider import

Add deep link support to enable one-click provider configuration import via ccswitch:// URLs.

Backend:
- Implement URL parsing and validation (src-tauri/src/deeplink.rs)
- Add Tauri commands for parse and import (src-tauri/src/commands/deeplink.rs)
- Register ccswitch:// protocol in macOS Info.plist
- Add comprehensive unit tests (src-tauri/tests/deeplink_import.rs)

Frontend:
- Create confirmation dialog with security review UI (src/components/DeepLinkImportDialog.tsx)
- Add API wrapper (src/lib/api/deeplink.ts)
- Integrate event listeners in App.tsx

Configuration:
- Update Tauri config for deep link handling
- Add i18n support for Chinese and English
- Include test page for deep link validation (deeplink-test.html)

Files: 15 changed, 1312 insertions(+)

* chore(deeplink): integrate deep link handling into app lifecycle

Wire up deep link infrastructure with app initialization and event handling.

Backend Integration:
- Register deep link module and commands in mod.rs
- Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url)
- Handle deep links from single instance callback (Windows/Linux CLI)
- Handle deep links from macOS system events
- Add tauri-plugin-deep-link dependency (Cargo.toml)

Frontend Integration:
- Listen for deeplink-import/deeplink-error events in App.tsx
- Update DeepLinkImportDialog component imports

Configuration:
- Enable deep link plugin in tauri.conf.json
- Update Cargo.lock for new dependencies

Localization:
- Add Chinese translations for deep link UI (zh.json)
- Add English translations for deep link UI (en.json)

Files: 9 changed, 359 insertions(+), 18 deletions(-)

* refactor(deeplink): enhance Codex provider template generation

Align deep link import with UI preset generation logic by:
- Adding complete config.toml template matching frontend defaults
- Generating safe provider name from sanitized input
- Including model_provider, reasoning_effort, and wire_api settings
- Removing minimal template that only contained base_url
- Cleaning up deprecated test file deeplink-test.html

* style: fix clippy uninlined_format_args warnings

Apply clippy --fix to use inline format arguments in:
- src/mcp.rs (8 fixes)
- src/services/env_manager.rs (10 fixes)

* style: apply code formatting and cleanup

- Format TypeScript files with Prettier (App.tsx, EnvWarningBanner.tsx, formatters.ts)
- Organize Rust imports and module order alphabetically
- Add newline at end of JSON files (en.json, zh.json)
- Update Cargo.lock for dependency changes

* feat: add model name configuration support for Codex and fix Gemini model handling

- Add visual model name input field for Codex providers
  - Add model name extraction and update utilities in providerConfigUtils
  - Implement model name state management in useCodexConfigState hook
  - Add conditional model field rendering in CodexFormFields (non-official only)
  - Integrate model name sync with TOML config in ProviderForm

- Fix Gemini deeplink model injection bug
  - Correct environment variable name from GOOGLE_GEMINI_MODEL to GEMINI_MODEL
  - Add test cases for Gemini model injection (with/without model)
  - All tests passing (9/9)

- Fix Gemini model field binding in edit mode
  - Add geminiModel state to useGeminiConfigState hook
  - Extract model value during initialization and reset
  - Sync model field with geminiEnv state to prevent data loss on submit
  - Fix missing model value display when editing Gemini providers

Changes:
  - 6 files changed, 245 insertions(+), 13 deletions(-)
This commit is contained in:
YoVinchen
2025-11-19 09:03:18 +08:00
committed by GitHub
parent 0ae9ed5a17
commit 3d69da5b66
39 changed files with 2097 additions and 81 deletions

View File

@@ -5,6 +5,7 @@ mod claude_plugin;
mod codex_config;
mod commands;
mod config;
mod deeplink;
mod error;
mod gemini_config; // 新增
mod gemini_mcp;
@@ -22,6 +23,7 @@ pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
pub use commands::*;
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
pub use deeplink::{import_provider_from_deeplink, parse_deeplink_url, DeepLinkImportRequest};
pub use error::AppError;
pub use mcp::{
import_from_claude, import_from_codex, import_from_gemini, remove_server_from_claude,
@@ -36,6 +38,7 @@ pub use services::{
};
pub use settings::{update_settings, AppSettings};
pub use store::AppState;
use tauri_plugin_deep_link::DeepLinkExt;
use std::sync::Arc;
use tauri::{
@@ -283,6 +286,65 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
}
}
/// 统一处理 ccswitch:// 深链接 URL
///
/// - 解析 URL
/// - 向前端发射 `deeplink-import` / `deeplink-error` 事件
/// - 可选:在成功时聚焦主窗口
fn handle_deeplink_url(
app: &tauri::AppHandle,
url_str: &str,
focus_main_window: bool,
source: &str,
) -> bool {
if !url_str.starts_with("ccswitch://") {
return false;
}
log::info!("✓ Deep link URL detected from {source}: {url_str}");
match crate::deeplink::parse_deeplink_url(url_str) {
Ok(request) => {
log::info!(
"✓ Successfully parsed deep link: resource={}, app={}, name={}",
request.resource,
request.app,
request.name
);
if let Err(e) = app.emit("deeplink-import", &request) {
log::error!("✗ Failed to emit deeplink-import event: {e}");
} else {
log::info!("✓ Emitted deeplink-import event to frontend");
}
if focus_main_window {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
log::info!("✓ Window shown and focused");
}
}
}
Err(e) => {
log::error!("✗ Failed to parse deep link URL: {e}");
if let Err(emit_err) = app.emit(
"deeplink-error",
serde_json::json!({
"url": url_str,
"error": e.to_string()
}),
) {
log::error!("✗ Failed to emit deeplink-error event: {emit_err}");
}
}
}
true
}
//
/// 内部切换供应商函数
@@ -348,7 +410,27 @@ pub fn run() {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
log::info!("=== Single Instance Callback Triggered ===");
log::info!("Args count: {}", args.len());
for (i, arg) in args.iter().enumerate() {
log::info!(" arg[{i}]: {arg}");
}
// Check for deep link URL in args (mainly for Windows/Linux command line)
let mut found_deeplink = false;
for arg in &args {
if handle_deeplink_url(app, arg, false, "single_instance args") {
found_deeplink = true;
break;
}
}
if !found_deeplink {
log::info!(" No deep link URL found in args (this is expected on macOS when launched via system)");
}
// Show and focus window regardless
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
@@ -358,6 +440,8 @@ pub fn run() {
}
let builder = builder
// 注册 deep-link 插件(处理 macOS AppleEvent 和其他平台的深链接)
.plugin(tauri_plugin_deep_link::init())
// 拦截窗口关闭:根据设置决定是否最小化到托盘
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
@@ -473,7 +557,40 @@ pub fn run() {
config_guard.ensure_app(&app_config::AppType::Codex);
}
// 启动阶段不再无条件保存避免意外覆盖用户配置。
// 启动阶段不再无条件保存,避免意外覆盖用户配置。
// 注册 deep-link URL 处理器(使用正确的 DeepLinkExt API
log::info!("=== Registering deep-link URL handler ===");
// Linux 和 Windows 调试模式需要显式注册
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
if let Err(e) = app.deep_link().register_all() {
log::error!("✗ Failed to register deep link schemes: {}", e);
} else {
log::info!("✓ Deep link schemes registered (Linux/Windows)");
}
}
// 注册 URL 处理回调(所有平台通用)
app.deep_link().on_open_url({
let app_handle = app.handle().clone();
move |event| {
log::info!("=== Deep Link Event Received (on_open_url) ===");
let urls = event.urls();
log::info!("Received {} URL(s)", urls.len());
for (i, url) in urls.iter().enumerate() {
let url_str = url.as_str();
log::info!(" URL[{i}]: {url_str}");
if handle_deeplink_url(&app_handle, url_str, true, "on_open_url") {
break; // Process only first ccswitch:// URL
}
}
}
});
log::info!("✓ Deep-link URL handler registered");
// 创建动态托盘菜单
let menu = create_tray_menu(app.handle(), &app_state)?;
@@ -585,6 +702,9 @@ pub fn run() {
commands::save_file_dialog,
commands::open_file_dialog,
commands::sync_current_providers_live,
// Deep link import
commands::parse_deeplink,
commands::import_from_deeplink,
update_tray_menu,
// Environment variable management
commands::check_env_conflicts,
@@ -605,17 +725,74 @@ pub fn run() {
app.run(|app_handle, event| {
#[cfg(target_os = "macos")]
// macOS 在 Dock 图标被点击并重新激活应用时会触发 Reopen 事件,这里手动恢复主窗口
if let RunEvent::Reopen { .. } = event {
if let Some(window) = app_handle.get_webview_window("main") {
#[cfg(target_os = "windows")]
{
let _ = window.set_skip_taskbar(false);
{
match event {
// macOS 在 Dock 图标被点击并重新激活应用时会触发 Reopen 事件,这里手动恢复主窗口
RunEvent::Reopen { .. } => {
if let Some(window) = app_handle.get_webview_window("main") {
#[cfg(target_os = "windows")]
{
let _ = window.set_skip_taskbar(false);
}
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
apply_tray_policy(app_handle, true);
}
}
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
apply_tray_policy(app_handle, true);
// 处理通过自定义 URL 协议触发的打开事件(例如 ccswitch://...
RunEvent::Opened { urls } => {
if let Some(url) = urls.first() {
let url_str = url.to_string();
log::info!("RunEvent::Opened with URL: {url_str}");
if url_str.starts_with("ccswitch://") {
// 解析并广播深链接事件,复用与 single_instance 相同的逻辑
match crate::deeplink::parse_deeplink_url(&url_str) {
Ok(request) => {
log::info!(
"Successfully parsed deep link from RunEvent::Opened: resource={}, app={}",
request.resource,
request.app
);
if let Err(e) =
app_handle.emit("deeplink-import", &request)
{
log::error!(
"Failed to emit deep link event from RunEvent::Opened: {e}"
);
}
}
Err(e) => {
log::error!(
"Failed to parse deep link URL from RunEvent::Opened: {e}"
);
if let Err(emit_err) = app_handle.emit(
"deeplink-error",
serde_json::json!({
"url": url_str,
"error": e.to_string()
}),
) {
log::error!(
"Failed to emit deep link error event from RunEvent::Opened: {emit_err}"
);
}
}
}
// 确保主窗口可见
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
}
}
_ => {}
}
}