diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 736230a5..eda422ab 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; -use crate::prompt_files::prompt_file_path; use crate::services::skill::SkillStore; /// MCP 服务器应用状态(标记应用到哪些客户端) @@ -636,132 +635,6 @@ impl MultiAppConfig { } } - /// 创建默认配置并自动导入已存在的提示词文件 - fn default_with_auto_import() -> Result { - log::info!("首次启动,创建默认配置并检测提示词文件"); - - let mut config = Self::default(); - - // 为每个应用尝试自动导入提示词 - Self::auto_import_prompt_if_exists(&mut config, AppType::Claude)?; - Self::auto_import_prompt_if_exists(&mut config, AppType::Codex)?; - Self::auto_import_prompt_if_exists(&mut config, AppType::Gemini)?; - Self::auto_import_prompt_if_exists(&mut config, AppType::OpenCode)?; - Self::auto_import_prompt_if_exists(&mut config, AppType::Hermes)?; - Self::auto_import_prompt_if_exists(&mut config, AppType::OpenClaw)?; - - Ok(config) - } - - /// 已存在配置文件时的 Prompt 自动导入逻辑 - /// - /// 适用于「老版本已经生成过 config.json,但当时还没有 Prompt 功能」的升级场景。 - /// 判定规则: - /// - 仅当所有应用的 prompts 都为空时才尝试导入(避免打扰已经在使用 Prompt 功能的用户) - /// - 每个应用最多导入一次,对应各自的提示词文件(如 CLAUDE.md/AGENTS.md/GEMINI.md) - /// - /// 返回值: - /// - Ok(true) 表示至少有一个应用成功导入了提示词 - /// - Ok(false) 表示无需导入或未导入任何内容 - fn maybe_auto_import_prompts_for_existing_config(&mut self) -> Result { - // 如果任一应用已经有提示词配置,说明用户已经在使用 Prompt 功能,避免再次自动导入 - if !self.prompts.claude.prompts.is_empty() - || !self.prompts.codex.prompts.is_empty() - || !self.prompts.gemini.prompts.is_empty() - || !self.prompts.opencode.prompts.is_empty() - || !self.prompts.hermes.prompts.is_empty() - || !self.prompts.openclaw.prompts.is_empty() - { - return Ok(false); - } - - log::info!("检测到已存在配置文件且 Prompt 列表为空,将尝试从现有提示词文件自动导入"); - - let mut imported = false; - for app in [ - AppType::Claude, - AppType::Codex, - AppType::Gemini, - AppType::OpenCode, - AppType::Hermes, - AppType::OpenClaw, - ] { - // 复用已有的单应用导入逻辑 - if Self::auto_import_prompt_if_exists(self, app)? { - imported = true; - } - } - - Ok(imported) - } - - /// 检查并自动导入单个应用的提示词文件 - /// - /// 返回值: - /// - Ok(true) 表示成功导入了非空文件 - /// - Ok(false) 表示未导入(文件不存在、内容为空或读取失败) - fn auto_import_prompt_if_exists(config: &mut Self, app: AppType) -> Result { - let file_path = prompt_file_path(&app)?; - - // 检查文件是否存在 - if !file_path.exists() { - log::debug!("提示词文件不存在,跳过自动导入: {file_path:?}"); - return Ok(false); - } - - // 读取文件内容 - let content = match std::fs::read_to_string(&file_path) { - Ok(c) => c, - Err(e) => { - log::warn!("读取提示词文件失败: {file_path:?}, 错误: {e}"); - return Ok(false); // 失败时不中断,继续处理其他应用 - } - }; - - // 检查内容是否为空 - if content.trim().is_empty() { - log::debug!("提示词文件内容为空,跳过导入: {file_path:?}"); - return Ok(false); - } - - log::info!("发现提示词文件,自动导入: {file_path:?}"); - - // 创建提示词对象 - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - let id = format!("auto-imported-{timestamp}"); - let prompt = crate::prompt::Prompt { - id: id.clone(), - name: format!( - "Auto-imported Prompt {}", - chrono::Local::now().format("%Y-%m-%d %H:%M") - ), - content, - description: Some("Automatically imported on first launch".to_string()), - enabled: true, // 自动启用 - created_at: Some(timestamp), - updated_at: Some(timestamp), - }; - - // 插入到对应的应用配置中 - let prompts = match app { - AppType::Claude => &mut config.prompts.claude.prompts, - AppType::Codex => &mut config.prompts.codex.prompts, - AppType::Gemini => &mut config.prompts.gemini.prompts, - AppType::OpenCode => &mut config.prompts.opencode.prompts, - AppType::Hermes => &mut config.prompts.hermes.prompts, - AppType::OpenClaw => &mut config.prompts.openclaw.prompts, - }; - - prompts.insert(id, prompt); - - log::info!("自动导入完成: {}", app.as_str()); - Ok(true) - } - /// 将 v3.6.x 的分应用 MCP 结构迁移到 v3.7.0 的统一结构 /// /// 迁移策略: diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index d17f75b0..fa22eb3c 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -9,6 +9,7 @@ use crate::error::AppError; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] pub struct McpStatus { pub user_config_path: String, pub user_config_exists: bool, @@ -79,6 +80,7 @@ fn write_json_value(path: &Path, value: &Value) -> Result<(), AppError> { atomic_write(path, json.as_bytes()) } +#[allow(dead_code)] pub fn get_mcp_status() -> Result { let path = user_config_path(); let (exists, count) = if path.exists() { @@ -154,6 +156,7 @@ pub fn clear_has_completed_onboarding() -> Result { Ok(true) } +#[allow(dead_code)] pub fn upsert_mcp_server(id: &str, spec: Value) -> Result { if id.trim().is_empty() { return Err(AppError::InvalidInput("MCP 服务器 ID 不能为空".into())); @@ -226,6 +229,7 @@ pub fn upsert_mcp_server(id: &str, spec: Value) -> Result { Ok(true) } +#[allow(dead_code)] pub fn delete_mcp_server(id: &str) -> Result { if id.trim().is_empty() { return Err(AppError::InvalidInput("MCP 服务器 ID 不能为空".into())); @@ -246,6 +250,7 @@ pub fn delete_mcp_server(id: &str) -> Result { Ok(true) } +#[allow(dead_code)] pub fn validate_command_in_path(cmd: &str) -> Result { if cmd.trim().is_empty() { return Ok(false); diff --git a/src-tauri/src/claude_plugin.rs b/src-tauri/src/claude_plugin.rs index 68f2f735..760417f5 100644 --- a/src-tauri/src/claude_plugin.rs +++ b/src-tauri/src/claude_plugin.rs @@ -39,6 +39,7 @@ pub fn read_claude_config() -> Result, AppError> { } } +#[allow(dead_code)] fn is_managed_config(content: &str) -> bool { match serde_json::from_str::(content) { Ok(value) => value @@ -120,11 +121,13 @@ pub fn clear_claude_config() -> Result { Ok(true) } +#[allow(dead_code)] pub fn claude_config_status() -> Result<(bool, PathBuf), AppError> { let path = claude_config_path()?; Ok((path.exists(), path)) } +#[allow(dead_code)] pub fn is_claude_config_applied() -> Result { match read_claude_config()? { Some(content) => Ok(is_managed_config(&content)), diff --git a/src-tauri/src/cli/claude_temp_launch.rs b/src-tauri/src/cli/claude_temp_launch.rs index 6085afcc..a438777e 100644 --- a/src-tauri/src/cli/claude_temp_launch.rs +++ b/src-tauri/src/cli/claude_temp_launch.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::error::AppError; +#[cfg(test)] use crate::provider::Provider; use crate::services::provider::ProviderService; use serde_json::Value; @@ -21,6 +22,8 @@ impl PreparedClaudeLaunch { } } +#[cfg(test)] +#[allow(dead_code)] pub(crate) fn prepare_launch( provider: &Provider, temp_dir: &Path, @@ -36,6 +39,7 @@ pub(crate) fn prepare_launch_from_settings( prepare_launch_from_settings_with(provider_id, settings, temp_dir, resolve_claude_binary) } +#[cfg(test)] pub(crate) fn prepare_launch_with( provider: &Provider, temp_dir: &Path, diff --git a/src-tauri/src/cli/commands/daemon.rs b/src-tauri/src/cli/commands/daemon.rs index cb17931b..f47726a8 100644 --- a/src-tauri/src/cli/commands/daemon.rs +++ b/src-tauri/src/cli/commands/daemon.rs @@ -141,6 +141,7 @@ fn current_executable() -> Result { fn detach_into_background() -> Result<(), AppError> { // Double-fork via libc::daemon. nochdir=1 keeps cwd, noclose=0 redirects // stdio to /dev/null so the daemon doesn't keep the parent terminal open. + #[allow(deprecated)] let rc = unsafe { libc::daemon(1, 0) }; if rc != 0 { let err = std::io::Error::last_os_error(); diff --git a/src-tauri/src/cli/commands/update.rs b/src-tauri/src/cli/commands/update.rs index a595173c..50afa08b 100644 --- a/src-tauri/src/cli/commands/update.rs +++ b/src-tauri/src/cli/commands/update.rs @@ -44,10 +44,10 @@ struct DownloadedAsset { #[derive(Debug, Deserialize, Clone)] struct UpdateManifest { version: String, - #[serde(default)] - notes: Option, - #[serde(default)] - pub_date: Option, + #[serde(default, rename = "notes")] + _notes: Option, + #[serde(default, rename = "pub_date")] + _pub_date: Option, platforms: BTreeMap, } @@ -657,18 +657,6 @@ async fn resolve_target_release( }) } -async fn resolve_target_tag( - client: &reqwest::Client, - version: Option<&str>, -) -> Result { - let tag = match version.map(str::trim).filter(|v| !v.is_empty()) { - Some(version) => normalize_tag(version), - None => fetch_latest_release_tag(client, REPO_URL).await?, - }; - validate_target_tag(&tag)?; - Ok(tag) -} - fn validate_target_tag(tag: &str) -> Result<(), AppError> { if !tag.starts_with('v') { return Err(AppError::Message(format!( diff --git a/src-tauri/src/cli/commands/update/tests.rs b/src-tauri/src/cli/commands/update/tests.rs index b8ce670b..4725f671 100644 --- a/src-tauri/src/cli/commands/update/tests.rs +++ b/src-tauri/src/cli/commands/update/tests.rs @@ -280,8 +280,8 @@ fn validate_download_size_limit_rejects_oversized_asset() { fn select_manifest_asset_prefers_linux_glibc_variant_when_overridden() { let manifest = UpdateManifest { version: "v4.6.3".to_string(), - notes: None, - pub_date: None, + _notes: None, + _pub_date: None, platforms: BTreeMap::from([( "linux-x86_64".to_string(), UpdatePlatformEntry { @@ -312,8 +312,8 @@ fn select_manifest_asset_prefers_linux_glibc_variant_when_overridden() { fn select_manifest_asset_accepts_glibc_primary_entry_without_variant() { let manifest = UpdateManifest { version: "v4.6.3".to_string(), - notes: None, - pub_date: None, + _notes: None, + _pub_date: None, platforms: BTreeMap::from([( "linux-x86_64".to_string(), UpdatePlatformEntry { @@ -334,8 +334,8 @@ fn select_manifest_asset_accepts_glibc_primary_entry_without_variant() { fn manifest_linux_asset_candidates_keep_musl_strict_when_forced() { let manifest = UpdateManifest { version: "v4.6.3".to_string(), - notes: None, - pub_date: None, + _notes: None, + _pub_date: None, platforms: BTreeMap::from([( "linux-x86_64".to_string(), UpdatePlatformEntry { diff --git a/src-tauri/src/cli/i18n.rs b/src-tauri/src/cli/i18n.rs index 74c96014..3e07a529 100644 --- a/src-tauri/src/cli/i18n.rs +++ b/src-tauri/src/cli/i18n.rs @@ -2458,22 +2458,6 @@ pub mod texts { } } - pub fn tui_usage_query_copilot_auto_auth() -> &'static str { - if is_chinese() { - "自动使用 OAuth 认证,无需手动配置凭证" - } else { - "Auto OAuth authentication, no manual credentials needed" - } - } - - pub fn tui_usage_query_token_plan_hint() -> &'static str { - if is_chinese() { - "自动使用供应商的 API Key 和 Base URL 查询 Token Plan 额度" - } else { - "Automatically uses the provider's API Key and Base URL to query Token Plan quota" - } - } - pub fn tui_usage_query_balance_hint() -> &'static str { if is_chinese() { "自动使用供应商的 API Key 查询账户余额" @@ -2498,14 +2482,6 @@ pub mod texts { } } - pub fn tui_usage_query_coding_plan_provider() -> &'static str { - if is_chinese() { - "Coding Plan 供应商" - } else { - "Coding Plan Provider" - } - } - pub fn tui_usage_query_info() -> &'static str { if is_chinese() { "说明" diff --git a/src-tauri/src/cli/i18n/texts/providers.rs b/src-tauri/src/cli/i18n/texts/providers.rs index 8b0d6348..a4bbb7f0 100644 --- a/src-tauri/src/cli/i18n/texts/providers.rs +++ b/src-tauri/src/cli/i18n/texts/providers.rs @@ -318,22 +318,6 @@ pub fn tui_usage_query_script_help_title() -> &'static str { } } -pub fn tui_usage_query_copilot_auto_auth() -> &'static str { - if is_chinese() { - "自动使用 OAuth 认证,无需手动配置凭证" - } else { - "Auto OAuth authentication, no manual credentials needed" - } -} - -pub fn tui_usage_query_token_plan_hint() -> &'static str { - if is_chinese() { - "自动使用供应商的 API Key 和 Base URL 查询 Token Plan 额度" - } else { - "Automatically uses the provider's API Key and Base URL to query Token Plan quota" - } -} - pub fn tui_usage_query_balance_hint() -> &'static str { if is_chinese() { "自动使用供应商的 API Key 查询账户余额" @@ -358,14 +342,6 @@ pub fn tui_usage_query_must_have_return() -> &'static str { } } -pub fn tui_usage_query_coding_plan_provider() -> &'static str { - if is_chinese() { - "Coding Plan 供应商" - } else { - "Coding Plan Provider" - } -} - pub fn tui_usage_query_info() -> &'static str { if is_chinese() { "说明" diff --git a/src-tauri/src/cli/tui/app/app_state.rs b/src-tauri/src/cli/tui/app/app_state.rs index 06360855..ce542ea2 100644 --- a/src-tauri/src/cli/tui/app/app_state.rs +++ b/src-tauri/src/cli/tui/app/app_state.rs @@ -3,6 +3,7 @@ use super::*; #[derive(Debug, Clone)] pub enum Action { None, + #[allow(dead_code)] ReloadData, SwitchRoute(Route), Quit, @@ -62,6 +63,7 @@ pub enum Action { enabled: bool, }, SkillsOpenImport, + #[allow(dead_code)] SkillsScanUnmanaged, SkillsImportFromApps { directories: Vec, @@ -144,6 +146,7 @@ pub enum Action { PromptDeactivate { id: String, }, + #[allow(dead_code)] PromptUpdateMetadata { old_id: String, new_id: String, @@ -236,6 +239,7 @@ pub enum Action { SetClaudePluginIntegration { enabled: bool, }, + #[allow(dead_code)] SetProxyEnabled { enabled: bool, }, @@ -290,6 +294,7 @@ pub enum ConfigItem { Restore, Validate, CommonSnippet, + #[allow(dead_code)] Proxy, OpenClawWorkspace, OpenClawEnv, @@ -566,6 +571,7 @@ pub struct App { Vec, pub config_webdav_idx: usize, pub webdav_quick_setup_username: Option, + #[allow(dead_code)] pub language_idx: usize, pub settings_idx: usize, pub settings_proxy_idx: usize, diff --git a/src-tauri/src/cli/tui/app/editor_state.rs b/src-tauri/src/cli/tui/app/editor_state.rs index 26846500..01441306 100644 --- a/src-tauri/src/cli/tui/app/editor_state.rs +++ b/src-tauri/src/cli/tui/app/editor_state.rs @@ -9,6 +9,7 @@ pub enum EditorKind { #[derive(Debug, Clone, PartialEq)] pub enum EditorSubmit { + #[allow(dead_code)] PromptCreate { id: String, name: String, diff --git a/src-tauri/src/cli/tui/app/form_handlers/provider.rs b/src-tauri/src/cli/tui/app/form_handlers/provider.rs index c72043d0..2a726d13 100644 --- a/src-tauri/src/cli/tui/app/form_handlers/provider.rs +++ b/src-tauri/src/cli/tui/app/form_handlers/provider.rs @@ -553,13 +553,6 @@ impl App { self.overlay = Overlay::UsageQueryTemplatePicker { selected }; Action::None } - form::UsageQueryField::CodingPlanProvider => { - let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { - return Action::None; - }; - provider.cycle_usage_query_coding_plan_provider(); - Action::None - } form::UsageQueryField::Script => self.open_usage_query_script_editor(), _ => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { @@ -1059,15 +1052,17 @@ fn normalize_usage_query_numeric_fields(provider: &mut form::ProviderAddFormStat } fn validate_usage_query_form(provider: &form::ProviderAddFormState) -> Option<&'static str> { + if provider.should_skip_usage_query_validation() { + return None; + } + if !provider.usage_query_enabled { return None; } if matches!( provider.usage_query_template, - form::UsageQueryTemplate::GitHubCopilot - | form::UsageQueryTemplate::TokenPlan - | form::UsageQueryTemplate::Balance + form::UsageQueryTemplate::Balance ) { return None; } diff --git a/src-tauri/src/cli/tui/app/helpers.rs b/src-tauri/src/cli/tui/app/helpers.rs index fb2b3064..ee5634fa 100644 --- a/src-tauri/src/cli/tui/app/helpers.rs +++ b/src-tauri/src/cli/tui/app/helpers.rs @@ -881,6 +881,7 @@ impl<'a> OpenClawDailyMemoryListItem<'a> { } } + #[allow(dead_code)] pub(crate) fn preview(&self) -> &str { match self { Self::File(row) => &row.preview, @@ -1205,6 +1206,7 @@ pub(crate) fn openclaw_workspace_entry_count() -> usize { OpenClawWorkspaceRow::all().len() } +#[cfg(test)] pub(crate) fn openclaw_workspace_rows() -> Vec { OpenClawWorkspaceRow::all() } @@ -1286,6 +1288,7 @@ pub(crate) fn app_type_for_picker_index(index: usize) -> AppType { } } +#[cfg(test)] pub(crate) fn snippet_picker_index_for_app_type(app_type: &AppType) -> usize { app_type_picker_index(app_type) } @@ -1294,6 +1297,8 @@ pub(crate) fn snippet_picker_app_type(index: usize) -> AppType { app_type_for_picker_index(index) } +#[cfg(test)] +#[allow(dead_code)] pub(crate) fn sync_method_picker_index(method: SyncMethod) -> usize { match method { SyncMethod::Auto => 0, diff --git a/src-tauri/src/cli/tui/app/tests.rs b/src-tauri/src/cli/tui/app/tests.rs index 1b656cc9..55b85fcf 100644 --- a/src-tauri/src/cli/tui/app/tests.rs +++ b/src-tauri/src/cli/tui/app/tests.rs @@ -13010,6 +13010,80 @@ mod tests { )); } + #[test] + fn provider_form_usage_query_hidden_template_edit_save_preserves_untouched_script() { + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::Providers; + app.focus = Focus::Content; + + let mut provider = Provider::with_id( + "kimi".to_string(), + "Kimi Coding".to_string(), + json!({ + "env": { + "ANTHROPIC_AUTH_TOKEN": "sk-kimi", + "ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/v1" + } + }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + usage_script: Some(crate::provider::UsageScript { + enabled: true, + language: "javascript".to_string(), + code: "".to_string(), + timeout: Some(10), + api_key: None, + base_url: None, + access_token: None, + user_id: None, + template_type: Some("token_plan".to_string()), + auto_query_interval: Some(5), + coding_plan_provider: Some("kimi".to_string()), + }), + ..Default::default() + }); + + let mut data = UiData::default(); + data.providers.rows.push(ProviderRow { + id: "kimi".to_string(), + provider, + api_url: Some("https://api.kimi.com/coding/v1".to_string()), + is_current: false, + is_in_config: true, + is_saved: true, + is_default_model: false, + primary_model_id: None, + default_model_id: None, + }); + + let open_action = app.on_key(key(KeyCode::Char('e')), &data); + assert!(matches!(open_action, Action::None)); + if let Some(FormState::ProviderAdd(form)) = app.form.as_mut() { + form.notes.set("Updated without touching Usage Query"); + } else { + panic!("expected ProviderAdd form"); + } + + let submit = app.on_key(ctrl(KeyCode::Char('s')), &data); + let Action::EditorSubmit { + submit: EditorSubmit::ProviderEdit { id }, + content, + } = submit + else { + panic!("expected ProviderEdit submit"); + }; + assert_eq!(id, "kimi"); + + let saved: serde_json::Value = + serde_json::from_str(&content).expect("provider submit JSON should parse"); + assert_eq!(saved["notes"], "Updated without touching Usage Query"); + let script = &saved["meta"]["usage_script"]; + assert_eq!(script["templateType"], "token_plan"); + assert_eq!(script["codingPlanProvider"], "kimi"); + assert_eq!(script["code"], ""); + } + #[test] fn provider_form_usage_query_script_opens_plain_editor() { let mut app = App::new(Some(AppType::Claude)); diff --git a/src-tauri/src/cli/tui/app/types.rs b/src-tauri/src/cli/tui/app/types.rs index 8a42cf14..41dcf576 100644 --- a/src-tauri/src/cli/tui/app/types.rs +++ b/src-tauri/src/cli/tui/app/types.rs @@ -331,6 +331,7 @@ pub enum ConfirmAction { filename: String, }, FormSaveBeforeClose, + #[allow(dead_code)] EditorDiscard, EditorSaveBeforeClose, WebDavMigrateV1ToV2, @@ -351,6 +352,7 @@ pub enum TextSubmit { SettingsProxyListenAddress, SettingsProxyListenPort, SettingsOpenClawConfigDir, + #[allow(dead_code)] SkillsInstallSpec, SkillsDiscoverQuery, SkillsRepoAdd, @@ -458,6 +460,7 @@ pub enum Overlay { selected: usize, }, TextView(TextViewState), + #[allow(dead_code)] CommonSnippetPicker { selected: usize, }, @@ -532,6 +535,7 @@ pub enum Overlay { selected_idx: usize, selected: HashSet, }, + #[allow(dead_code)] SkillsSyncMethodPicker { selected: usize, }, diff --git a/src-tauri/src/cli/tui/data.rs b/src-tauri/src/cli/tui/data.rs index e2c4f325..b1878f40 100644 --- a/src-tauri/src/cli/tui/data.rs +++ b/src-tauri/src/cli/tui/data.rs @@ -209,6 +209,7 @@ pub struct ConfigSnapshot { pub common_snippets: CommonConfigSnippets, pub webdav_sync: Option, pub openclaw_config_path: Option, + #[allow(dead_code)] pub openclaw_config_dir: Option, pub openclaw_env: Option, pub openclaw_tools: Option, @@ -265,6 +266,7 @@ pub struct SkillsSnapshot { #[derive(Debug, Clone, Default)] pub struct ProxyTargetSnapshot { + #[allow(dead_code)] pub provider_name: String, } @@ -273,23 +275,29 @@ pub struct ProxySnapshot { pub enabled: bool, pub running: bool, pub managed_runtime: bool, + #[allow(dead_code)] pub active_worker_apps: HashSet, pub auto_failover_enabled: bool, pub claude_takeover: bool, pub codex_takeover: bool, pub gemini_takeover: bool, + #[allow(dead_code)] pub default_cost_multiplier: Option, pub configured_listen_address: String, pub configured_listen_port: u16, pub listen_address: String, pub listen_port: u16, pub uptime_seconds: u64, + #[allow(dead_code)] pub total_requests: u64, pub estimated_input_tokens_total: u64, pub estimated_output_tokens_total: u64, + #[allow(dead_code)] pub success_rate: Option, + #[allow(dead_code)] pub current_provider: Option, pub last_error: Option, + #[allow(dead_code)] pub current_app_target: Option, } diff --git a/src-tauri/src/cli/tui/form.rs b/src-tauri/src/cli/tui/form.rs index 73aee838..ffebb7a1 100644 --- a/src-tauri/src/cli/tui/form.rs +++ b/src-tauri/src/cli/tui/form.rs @@ -10,6 +10,7 @@ mod provider_json; mod provider_state; mod provider_state_loading; mod provider_templates; +mod provider_usage_query; #[cfg(test)] mod tests; @@ -23,9 +24,7 @@ pub(crate) use provider_json::claude_hide_attribution_enabled; pub(crate) use provider_json::strip_common_config_from_settings; pub(crate) use provider_json::{normalize_usage_interval, normalize_usage_timeout}; pub(crate) use provider_state::resolve_provider_id_for_submit; -pub(crate) use provider_state::{ - detect_balance_provider_for_usage_query, detect_coding_plan_provider_for_usage_query, -}; +pub(crate) use provider_usage_query::detect_balance_provider_for_usage_query; pub const OPENCLAW_DEFAULT_API_PROTOCOL: &str = "openai-completions"; pub const OPENCLAW_DEFAULT_USER_AGENT: &str = @@ -140,6 +139,7 @@ pub enum CodexPreviewSection { } impl CodexPreviewSection { + #[allow(dead_code)] pub fn toggle(self) -> Self { match self { Self::Auth => Self::Config, @@ -175,8 +175,11 @@ pub enum ProviderAddField { CodexFastMode, CodexBaseUrl, CodexModel, + #[allow(dead_code)] CodexWireApi, + #[allow(dead_code)] CodexRequiresOpenaiAuth, + #[allow(dead_code)] CodexEnvKey, CodexApiKey, GeminiAuthType, @@ -224,8 +227,6 @@ pub enum UsageQueryTemplate { Custom, General, NewApi, - GitHubCopilot, - TokenPlan, Balance, } @@ -239,7 +240,6 @@ pub enum UsageQueryField { UserId, Timeout, AutoInterval, - CodingPlanProvider, Script, } @@ -341,7 +341,6 @@ pub struct ProviderAddFormState { pub usage_query_timeout: TextInput, pub usage_query_auto_interval: TextInput, pub usage_query_code: String, - pub usage_query_coding_plan_provider: TextInput, pub opencode_npm_package: TextInput, pub opencode_api_key: TextInput, pub opencode_base_url: TextInput, diff --git a/src-tauri/src/cli/tui/form/provider_json.rs b/src-tauri/src/cli/tui/form/provider_json.rs index 8a516b2e..fdc67156 100644 --- a/src-tauri/src/cli/tui/form/provider_json.rs +++ b/src-tauri/src/cli/tui/form/provider_json.rs @@ -590,6 +590,9 @@ impl ProviderAddFormState { } fn update_usage_script_meta(&self, meta_obj: &mut serde_json::Map) { + if self.has_usage_script_meta() && !self.usage_query_touched { + return; + } if !self.has_usage_script_meta() && !self.usage_query_enabled && !self.usage_query_touched { meta_obj.remove("usage_script"); return; @@ -628,16 +631,7 @@ impl ProviderAddFormState { ); set_or_remove_trimmed(&mut script, "userId", &self.usage_query_user_id.value); } - UsageQueryTemplate::TokenPlan => { - set_or_remove_trimmed( - &mut script, - "codingPlanProvider", - &self.usage_query_coding_plan_provider.value, - ); - } - UsageQueryTemplate::Custom - | UsageQueryTemplate::GitHubCopilot - | UsageQueryTemplate::Balance => {} + UsageQueryTemplate::Custom | UsageQueryTemplate::Balance => {} } meta_obj.insert("usage_script".to_string(), Value::Object(script)); diff --git a/src-tauri/src/cli/tui/form/provider_state.rs b/src-tauri/src/cli/tui/form/provider_state.rs index 7edb3914..2ccd0805 100644 --- a/src-tauri/src/cli/tui/form/provider_state.rs +++ b/src-tauri/src/cli/tui/form/provider_state.rs @@ -11,71 +11,10 @@ use super::provider_state_loading::populate_form_from_provider; use super::{ ClaudeApiFormat, CodexPreviewSection, CodexWireApi, FormFocus, FormMode, GeminiAuthType, HermesModelField, ProviderAddField, ProviderAddFormState, ProviderFormPage, TextInput, - UsageQueryField, UsageQueryTemplate, HERMES_API_MODES, HERMES_DEFAULT_API_MODE, - OPENCLAW_DEFAULT_API_PROTOCOL, + UsageQueryTemplate, HERMES_API_MODES, HERMES_DEFAULT_API_MODE, OPENCLAW_DEFAULT_API_PROTOCOL, }; impl ProviderAddFormState { - pub const USAGE_QUERY_GENERAL_PRESET: &'static str = r#"({ - request: { - url: "{{baseUrl}}/user/balance", - method: "GET", - headers: { - "Authorization": "Bearer {{apiKey}}", - "User-Agent": "cc-switch/1.0" - } - }, - extractor: function(response) { - return { - isValid: response.is_active || true, - remaining: response.balance, - unit: "USD" - }; - } -})"#; - - pub const USAGE_QUERY_CUSTOM_PRESET: &'static str = r#"({ - request: { - url: "", - method: "GET", - headers: {} - }, - extractor: function(response) { - return { - remaining: 0, - unit: "USD" - }; - } -})"#; - - pub const USAGE_QUERY_NEWAPI_PRESET: &'static str = r#"({ - request: { - url: "{{baseUrl}}/api/user/self", - method: "GET", - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer {{accessToken}}", - "User-Agent": "cc-switch/1.0", - "New-Api-User": "{{userId}}" - }, - }, - extractor: function (response) { - if (response.success && response.data) { - return { - planName: response.data.group || "Default Plan", - remaining: response.data.quota / 500000, - used: response.data.used_quota / 500000, - total: (response.data.quota + response.data.used_quota) / 500000, - unit: "USD", - }; - } - return { - isValid: false, - invalidMessage: response.message || "Query failed" - }; - }, -})"#; - pub fn new(app_type: AppType) -> Self { Self::new_with_common_snippet(app_type, "") } @@ -150,7 +89,6 @@ impl ProviderAddFormState { usage_query_timeout: TextInput::new("10"), usage_query_auto_interval: TextInput::new("5"), usage_query_code: Self::USAGE_QUERY_GENERAL_PRESET.to_string(), - usage_query_coding_plan_provider: TextInput::new("kimi"), opencode_npm_package: TextInput::new(openclaw_api_default), opencode_api_key: TextInput::new(""), opencode_base_url: TextInput::new(""), @@ -365,71 +303,6 @@ impl ProviderAddFormState { fields } - pub fn usage_query_fields(&self) -> Vec { - let mut fields = vec![UsageQueryField::Enabled]; - - if !self.usage_query_enabled { - return fields; - } - - fields.push(UsageQueryField::Template); - - match self.usage_query_template { - UsageQueryTemplate::General => { - fields.extend([ - UsageQueryField::ApiKey, - UsageQueryField::BaseUrl, - UsageQueryField::Timeout, - UsageQueryField::AutoInterval, - UsageQueryField::Script, - ]); - } - UsageQueryTemplate::NewApi => { - fields.extend([ - UsageQueryField::BaseUrl, - UsageQueryField::AccessToken, - UsageQueryField::UserId, - UsageQueryField::Timeout, - UsageQueryField::AutoInterval, - UsageQueryField::Script, - ]); - } - UsageQueryTemplate::Custom => { - fields.extend([ - UsageQueryField::Timeout, - UsageQueryField::AutoInterval, - UsageQueryField::Script, - ]); - } - UsageQueryTemplate::GitHubCopilot => { - fields.extend([UsageQueryField::Timeout, UsageQueryField::AutoInterval]); - } - UsageQueryTemplate::Balance => { - fields.extend([ - UsageQueryField::Timeout, - UsageQueryField::AutoInterval, - UsageQueryField::Script, - ]); - } - UsageQueryTemplate::TokenPlan => { - fields.extend([ - UsageQueryField::CodingPlanProvider, - UsageQueryField::Timeout, - UsageQueryField::AutoInterval, - ]); - } - } - - fields - } - - pub fn usage_query_table_fields(&self) -> Vec { - self.usage_query_fields() - .into_iter() - .filter(|field| *field != UsageQueryField::Script) - .collect() - } - pub fn input(&self, field: ProviderAddField) -> Option<&TextInput> { match field { ProviderAddField::Id => Some(&self.id), @@ -528,74 +401,6 @@ impl ProviderAddFormState { } } - pub fn usage_query_input(&self, field: UsageQueryField) -> Option<&TextInput> { - match field { - UsageQueryField::ApiKey => Some(&self.usage_query_api_key), - UsageQueryField::BaseUrl => Some(&self.usage_query_base_url), - UsageQueryField::AccessToken => Some(&self.usage_query_access_token), - UsageQueryField::UserId => Some(&self.usage_query_user_id), - UsageQueryField::Timeout => Some(&self.usage_query_timeout), - UsageQueryField::AutoInterval => Some(&self.usage_query_auto_interval), - UsageQueryField::CodingPlanProvider => Some(&self.usage_query_coding_plan_provider), - UsageQueryField::Enabled | UsageQueryField::Template | UsageQueryField::Script => None, - } - } - - pub fn usage_query_input_mut(&mut self, field: UsageQueryField) -> Option<&mut TextInput> { - match field { - UsageQueryField::ApiKey => Some(&mut self.usage_query_api_key), - UsageQueryField::BaseUrl => Some(&mut self.usage_query_base_url), - UsageQueryField::AccessToken => Some(&mut self.usage_query_access_token), - UsageQueryField::UserId => Some(&mut self.usage_query_user_id), - UsageQueryField::Timeout => Some(&mut self.usage_query_timeout), - UsageQueryField::AutoInterval => Some(&mut self.usage_query_auto_interval), - UsageQueryField::CodingPlanProvider => Some(&mut self.usage_query_coding_plan_provider), - UsageQueryField::Enabled | UsageQueryField::Template | UsageQueryField::Script => None, - } - } - - pub fn open_usage_query_page(&mut self) { - self.refresh_default_usage_query_template(); - self.page = ProviderFormPage::UsageQuery; - self.focus = FormFocus::Fields; - self.editing = false; - self.usage_query_editing = false; - let len = self.usage_query_table_fields().len(); - self.usage_query_field_idx = self.usage_query_field_idx.min(len.saturating_sub(1)); - } - - pub fn refresh_default_usage_query_template(&mut self) { - if self.usage_query_touched || self.has_usage_script_meta() { - return; - } - - let template = match self - .extra - .get("meta") - .and_then(|meta| meta.get("providerType")) - .and_then(|value| value.as_str()) - { - Some("github_copilot") => UsageQueryTemplate::GitHubCopilot, - _ if detect_balance_provider_for_usage_query(&self.current_provider_base_url()) => { - UsageQueryTemplate::Balance - } - _ => UsageQueryTemplate::General, - }; - - self.set_usage_query_template(template); - if let Some(provider) = - detect_coding_plan_provider_for_usage_query(&self.current_provider_base_url()) - { - self.usage_query_coding_plan_provider.set(provider); - } - } - - pub fn close_usage_query_page(&mut self) { - self.page = ProviderFormPage::Main; - self.focus = FormFocus::Fields; - self.usage_query_editing = false; - } - pub fn open_hermes_models_picker(&mut self) { if !matches!(self.app_type, AppType::Hermes) { return; @@ -765,235 +570,6 @@ impl ProviderAddFormState { true } - pub fn touch_usage_query(&mut self) { - self.usage_query_touched = true; - } - - pub fn toggle_usage_query_enabled(&mut self) { - self.usage_query_enabled = !self.usage_query_enabled; - self.touch_usage_query(); - } - - pub fn selected_usage_query_field(&self) -> Option { - let fields = self.usage_query_table_fields(); - fields - .get( - self.usage_query_field_idx - .min(fields.len().saturating_sub(1)), - ) - .copied() - } - - pub fn cycle_usage_query_coding_plan_provider(&mut self) { - let options = ["kimi", "zhipu", "minimax"]; - let current = options - .iter() - .position(|value| *value == self.usage_query_coding_plan_provider.value.trim()) - .unwrap_or(0); - self.usage_query_coding_plan_provider - .set(options[(current + 1) % options.len()]); - self.touch_usage_query(); - } - - pub fn available_usage_query_templates(&self) -> Vec { - vec![ - UsageQueryTemplate::Custom, - UsageQueryTemplate::General, - UsageQueryTemplate::NewApi, - UsageQueryTemplate::Balance, - ] - } - - pub fn set_usage_query_template(&mut self, template: UsageQueryTemplate) { - self.usage_query_template = template; - match template { - UsageQueryTemplate::Custom => { - self.usage_query_code = self.usage_query_custom_preset_with_variables(); - self.usage_query_api_key.set(""); - self.usage_query_base_url.set(""); - self.usage_query_access_token.set(""); - self.usage_query_user_id.set(""); - } - UsageQueryTemplate::General => { - self.usage_query_code = Self::USAGE_QUERY_GENERAL_PRESET.to_string(); - self.usage_query_access_token.set(""); - self.usage_query_user_id.set(""); - } - UsageQueryTemplate::NewApi => { - self.usage_query_code = Self::USAGE_QUERY_NEWAPI_PRESET.to_string(); - self.usage_query_api_key.set(""); - } - UsageQueryTemplate::GitHubCopilot | UsageQueryTemplate::Balance => { - self.usage_query_code.clear(); - self.usage_query_api_key.set(""); - self.usage_query_base_url.set(""); - self.usage_query_access_token.set(""); - self.usage_query_user_id.set(""); - } - UsageQueryTemplate::TokenPlan => { - self.usage_query_code.clear(); - self.usage_query_api_key.set(""); - self.usage_query_base_url.set(""); - self.usage_query_access_token.set(""); - self.usage_query_user_id.set(""); - if self - .usage_query_coding_plan_provider - .value - .trim() - .is_empty() - { - self.usage_query_coding_plan_provider.set("kimi"); - } - } - } - let len = self.usage_query_table_fields().len(); - self.usage_query_field_idx = self.usage_query_field_idx.min(len.saturating_sub(1)); - } - - pub fn refresh_usage_query_custom_variable_comment(&mut self) { - if self.usage_query_template != UsageQueryTemplate::Custom { - return; - } - - let Some(body) = Self::strip_usage_query_custom_variable_comment(&self.usage_query_code) - .map(str::to_string) - else { - return; - }; - let next = format!("{}{}", self.usage_query_custom_variable_comment(), body); - if self.usage_query_code != next { - self.usage_query_code = next; - self.touch_usage_query(); - } - } - - pub fn usage_query_script_help_lines() -> Vec { - vec![ - texts::tui_usage_query_config_format().to_string(), - "({".to_string(), - " request: {".to_string(), - " url: \"{{baseUrl}}/api/usage\",".to_string(), - " method: \"POST\",".to_string(), - " headers: {".to_string(), - " \"Authorization\": \"Bearer {{apiKey}}\",".to_string(), - " \"User-Agent\": \"cc-switch/1.0\"".to_string(), - " }".to_string(), - " },".to_string(), - " extractor: function(response) {".to_string(), - " return {".to_string(), - " isValid: !response.error,".to_string(), - " remaining: response.balance,".to_string(), - " unit: \"USD\"".to_string(), - " };".to_string(), - " }".to_string(), - "})".to_string(), - String::new(), - texts::tui_usage_query_extractor_format().to_string(), - texts::tui_usage_query_field_is_valid().to_string(), - texts::tui_usage_query_field_invalid_message().to_string(), - texts::tui_usage_query_field_remaining().to_string(), - texts::tui_usage_query_field_unit().to_string(), - texts::tui_usage_query_field_plan_name().to_string(), - texts::tui_usage_query_field_total().to_string(), - texts::tui_usage_query_field_used().to_string(), - texts::tui_usage_query_field_extra().to_string(), - String::new(), - texts::tui_usage_query_tips().to_string(), - texts::tui_usage_query_tip1().to_string(), - texts::tui_usage_query_tip2().to_string(), - texts::tui_usage_query_tip3().to_string(), - ] - } - - pub fn usage_query_template_value(&self) -> &'static str { - self.usage_query_template.as_str() - } - - pub fn usage_query_template_label(&self) -> &'static str { - self.usage_query_template.label() - } - - pub fn usage_query_extractor_available(&self) -> bool { - self.usage_query_enabled - && !matches!( - self.usage_query_template, - UsageQueryTemplate::GitHubCopilot | UsageQueryTemplate::TokenPlan - ) - } - - fn usage_query_custom_preset_with_variables(&self) -> String { - format!( - "{}{}", - self.usage_query_custom_variable_comment(), - Self::USAGE_QUERY_CUSTOM_PRESET - ) - } - - fn usage_query_custom_variable_comment(&self) -> String { - let (api_key, base_url) = self.usage_query_provider_credentials(); - format!( - "// 支持的变量\n// {{{{baseUrl}}}}\n// =\n// {base_url}\n// {{{{apiKey}}}}\n// =\n// {api_key}\n\n" - ) - } - - pub fn current_provider_base_url(&self) -> String { - if self.is_claude_codex_oauth_provider() { - return "https://chatgpt.com/backend-api/codex".to_string(); - } - - match self.app_type { - AppType::Claude => self.claude_base_url.value.clone(), - AppType::Codex => self.codex_base_url.value.clone(), - AppType::Gemini => self.gemini_base_url.value.clone(), - AppType::Hermes => self.hermes_base_url.value.clone(), - AppType::OpenCode | AppType::OpenClaw => self.opencode_base_url.value.clone(), - } - } - - fn usage_query_provider_credentials(&self) -> (String, String) { - if self.is_claude_codex_oauth_provider() { - return ( - String::new(), - "https://chatgpt.com/backend-api/codex".to_string(), - ); - } - - let (api_key, base_url) = match self.app_type { - AppType::Claude => (&self.claude_api_key.value, &self.claude_base_url.value), - AppType::Codex => (&self.codex_api_key.value, &self.codex_base_url.value), - AppType::Gemini => (&self.gemini_api_key.value, &self.gemini_base_url.value), - AppType::Hermes => (&self.hermes_api_key.value, &self.hermes_base_url.value), - AppType::OpenCode | AppType::OpenClaw => { - (&self.opencode_api_key.value, &self.opencode_base_url.value) - } - }; - ( - Self::usage_query_comment_value(api_key), - Self::usage_query_comment_value(base_url), - ) - } - - fn usage_query_comment_value(value: &str) -> String { - value.trim().replace(['\r', '\n'], " ").trim().to_string() - } - - fn strip_usage_query_custom_variable_comment(code: &str) -> Option<&str> { - if !code.starts_with("// 支持的变量\n") { - return None; - } - - let mut newline_count = 0; - for (idx, ch) in code.char_indices() { - if ch == '\n' { - newline_count += 1; - if newline_count == 8 { - return code.get(idx + ch.len_utf8()..); - } - } - } - None - } - pub fn claude_model_input(&self, index: usize) -> Option<&TextInput> { match index { 0 => Some(&self.claude_model), @@ -1378,90 +954,6 @@ impl ProviderAddFormState { } } -pub(crate) fn detect_coding_plan_provider_for_usage_query(base_url: &str) -> Option<&'static str> { - let url = base_url.to_lowercase(); - if url.contains("api.kimi.com/coding") { - Some("kimi") - } else if url.contains("open.bigmodel.cn") - || url.contains("bigmodel.cn") - || url.contains("api.z.ai") - { - Some("zhipu") - } else if url.contains("api.minimaxi.com") - || url.contains("api.minimax.io") - || url.contains("api.minimax.com") - { - Some("minimax") - } else { - None - } -} - -pub(crate) fn detect_balance_provider_for_usage_query(base_url: &str) -> bool { - let url = base_url.to_lowercase(); - url.contains("api.deepseek.com") - || url.contains("api.stepfun.ai") - || url.contains("api.stepfun.com") - || url.contains("api.siliconflow.cn") - || url.contains("api.siliconflow.com") - || url.contains("openrouter.ai") - || url.contains("api.novita.ai") -} - -impl UsageQueryTemplate { - pub const fn as_str(self) -> &'static str { - match self { - Self::Custom => "custom", - Self::General => "general", - Self::NewApi => "newapi", - Self::GitHubCopilot => "github_copilot", - Self::TokenPlan => "token_plan", - Self::Balance => "balance", - } - } - - pub fn label(self) -> &'static str { - match self { - Self::Custom => { - if crate::cli::i18n::is_chinese() { - "自定义" - } else { - "Custom" - } - } - Self::General => { - if crate::cli::i18n::is_chinese() { - "通用模板" - } else { - "General" - } - } - Self::NewApi => "NewAPI", - Self::GitHubCopilot => "GitHub Copilot", - Self::TokenPlan => "Token Plan", - Self::Balance => { - if crate::cli::i18n::is_chinese() { - "官方" - } else { - "Official" - } - } - } - } - - pub fn from_str(value: &str) -> Option { - match value { - "custom" => Some(Self::Custom), - "general" => Some(Self::General), - "newapi" => Some(Self::NewApi), - "github_copilot" => Some(Self::GitHubCopilot), - "token_plan" => Some(Self::TokenPlan), - "balance" => Some(Self::Balance), - _ => None, - } - } -} - pub(crate) fn resolve_provider_id_for_submit( name: &str, id: &str, diff --git a/src-tauri/src/cli/tui/form/provider_state_loading.rs b/src-tauri/src/cli/tui/form/provider_state_loading.rs index 63d6ca0c..3da72d40 100644 --- a/src-tauri/src/cli/tui/form/provider_state_loading.rs +++ b/src-tauri/src/cli/tui/form/provider_state_loading.rs @@ -1,8 +1,7 @@ use super::codex_config::parse_codex_config_snippet; use super::{ - claude_hide_attribution_enabled, detect_balance_provider_for_usage_query, - detect_coding_plan_provider_for_usage_query, ClaudeApiFormat, ProviderAddFormState, - UsageQueryTemplate, OPENCLAW_DEFAULT_API_PROTOCOL, + claude_hide_attribution_enabled, detect_balance_provider_for_usage_query, ClaudeApiFormat, + ProviderAddFormState, UsageQueryTemplate, OPENCLAW_DEFAULT_API_PROTOCOL, }; use crate::app_config::AppType; use crate::provider::Provider; @@ -51,14 +50,6 @@ fn populate_usage_query_form(form: &mut ProviderAddFormState, provider: &Provide if let Some(value) = script.user_id.as_deref() { form.usage_query_user_id.set(value); } - if let Some(value) = script.coding_plan_provider.as_deref() { - form.usage_query_coding_plan_provider.set(value); - } else if let Some(provider) = - detect_coding_plan_provider_for_usage_query(&form.current_provider_base_url()) - { - form.usage_query_coding_plan_provider.set(provider); - } - let template = script .template_type .as_deref() diff --git a/src-tauri/src/cli/tui/form/provider_usage_query.rs b/src-tauri/src/cli/tui/form/provider_usage_query.rs new file mode 100644 index 00000000..87d88790 --- /dev/null +++ b/src-tauri/src/cli/tui/form/provider_usage_query.rs @@ -0,0 +1,440 @@ +use crate::app_config::AppType; +use crate::cli::i18n::texts; + +use super::{ + FormFocus, ProviderAddFormState, ProviderFormPage, TextInput, UsageQueryField, + UsageQueryTemplate, +}; + +impl ProviderAddFormState { + pub const USAGE_QUERY_GENERAL_PRESET: &'static str = r#"({ + request: { + url: "{{baseUrl}}/user/balance", + method: "GET", + headers: { + "Authorization": "Bearer {{apiKey}}", + "User-Agent": "cc-switch/1.0" + } + }, + extractor: function(response) { + return { + isValid: response.is_active || true, + remaining: response.balance, + unit: "USD" + }; + } +})"#; + + pub const USAGE_QUERY_CUSTOM_PRESET: &'static str = r#"({ + request: { + url: "", + method: "GET", + headers: {} + }, + extractor: function(response) { + return { + remaining: 0, + unit: "USD" + }; + } +})"#; + + pub const USAGE_QUERY_NEWAPI_PRESET: &'static str = r#"({ + request: { + url: "{{baseUrl}}/api/user/self", + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer {{accessToken}}", + "User-Agent": "cc-switch/1.0", + "New-Api-User": "{{userId}}" + }, + }, + extractor: function (response) { + if (response.success && response.data) { + return { + planName: response.data.group || "Default Plan", + remaining: response.data.quota / 500000, + used: response.data.used_quota / 500000, + total: (response.data.quota + response.data.used_quota) / 500000, + unit: "USD", + }; + } + return { + isValid: false, + invalidMessage: response.message || "Query failed" + }; + }, +})"#; + + pub fn usage_query_fields(&self) -> Vec { + let mut fields = vec![UsageQueryField::Enabled]; + + if !self.usage_query_enabled { + return fields; + } + + fields.push(UsageQueryField::Template); + + match self.usage_query_template { + UsageQueryTemplate::General => { + fields.extend([ + UsageQueryField::ApiKey, + UsageQueryField::BaseUrl, + UsageQueryField::Timeout, + UsageQueryField::AutoInterval, + UsageQueryField::Script, + ]); + } + UsageQueryTemplate::NewApi => { + fields.extend([ + UsageQueryField::BaseUrl, + UsageQueryField::AccessToken, + UsageQueryField::UserId, + UsageQueryField::Timeout, + UsageQueryField::AutoInterval, + UsageQueryField::Script, + ]); + } + UsageQueryTemplate::Custom => { + fields.extend([ + UsageQueryField::Timeout, + UsageQueryField::AutoInterval, + UsageQueryField::Script, + ]); + } + UsageQueryTemplate::Balance => { + fields.extend([ + UsageQueryField::Timeout, + UsageQueryField::AutoInterval, + UsageQueryField::Script, + ]); + } + } + + fields + } + + pub fn usage_query_table_fields(&self) -> Vec { + self.usage_query_fields() + .into_iter() + .filter(|field| *field != UsageQueryField::Script) + .collect() + } + + pub fn usage_query_input(&self, field: UsageQueryField) -> Option<&TextInput> { + match field { + UsageQueryField::ApiKey => Some(&self.usage_query_api_key), + UsageQueryField::BaseUrl => Some(&self.usage_query_base_url), + UsageQueryField::AccessToken => Some(&self.usage_query_access_token), + UsageQueryField::UserId => Some(&self.usage_query_user_id), + UsageQueryField::Timeout => Some(&self.usage_query_timeout), + UsageQueryField::AutoInterval => Some(&self.usage_query_auto_interval), + UsageQueryField::Enabled | UsageQueryField::Template | UsageQueryField::Script => None, + } + } + + pub fn usage_query_input_mut(&mut self, field: UsageQueryField) -> Option<&mut TextInput> { + match field { + UsageQueryField::ApiKey => Some(&mut self.usage_query_api_key), + UsageQueryField::BaseUrl => Some(&mut self.usage_query_base_url), + UsageQueryField::AccessToken => Some(&mut self.usage_query_access_token), + UsageQueryField::UserId => Some(&mut self.usage_query_user_id), + UsageQueryField::Timeout => Some(&mut self.usage_query_timeout), + UsageQueryField::AutoInterval => Some(&mut self.usage_query_auto_interval), + UsageQueryField::Enabled | UsageQueryField::Template | UsageQueryField::Script => None, + } + } + + pub fn open_usage_query_page(&mut self) { + self.refresh_default_usage_query_template(); + self.page = ProviderFormPage::UsageQuery; + self.focus = FormFocus::Fields; + self.editing = false; + self.usage_query_editing = false; + let len = self.usage_query_table_fields().len(); + self.usage_query_field_idx = self.usage_query_field_idx.min(len.saturating_sub(1)); + } + + pub fn refresh_default_usage_query_template(&mut self) { + if self.usage_query_touched || self.has_usage_script_meta() { + return; + } + + let template = + match detect_balance_provider_for_usage_query(&self.current_provider_base_url()) { + true => UsageQueryTemplate::Balance, + _ => UsageQueryTemplate::General, + }; + + self.set_usage_query_template(template); + } + + pub fn close_usage_query_page(&mut self) { + self.page = ProviderFormPage::Main; + self.focus = FormFocus::Fields; + self.usage_query_editing = false; + } + + pub fn touch_usage_query(&mut self) { + self.usage_query_touched = true; + } + + pub fn toggle_usage_query_enabled(&mut self) { + self.usage_query_enabled = !self.usage_query_enabled; + self.touch_usage_query(); + } + + pub fn selected_usage_query_field(&self) -> Option { + let fields = self.usage_query_table_fields(); + fields + .get( + self.usage_query_field_idx + .min(fields.len().saturating_sub(1)), + ) + .copied() + } + + pub fn available_usage_query_templates(&self) -> Vec { + vec![ + UsageQueryTemplate::Custom, + UsageQueryTemplate::General, + UsageQueryTemplate::NewApi, + UsageQueryTemplate::Balance, + ] + } + + pub fn set_usage_query_template(&mut self, template: UsageQueryTemplate) { + self.usage_query_template = template; + match template { + UsageQueryTemplate::Custom => { + self.usage_query_code = self.usage_query_custom_preset_with_variables(); + self.usage_query_api_key.set(""); + self.usage_query_base_url.set(""); + self.usage_query_access_token.set(""); + self.usage_query_user_id.set(""); + } + UsageQueryTemplate::General => { + self.usage_query_code = Self::USAGE_QUERY_GENERAL_PRESET.to_string(); + self.usage_query_access_token.set(""); + self.usage_query_user_id.set(""); + } + UsageQueryTemplate::NewApi => { + self.usage_query_code = Self::USAGE_QUERY_NEWAPI_PRESET.to_string(); + self.usage_query_api_key.set(""); + } + UsageQueryTemplate::Balance => { + self.usage_query_code.clear(); + self.usage_query_api_key.set(""); + self.usage_query_base_url.set(""); + self.usage_query_access_token.set(""); + self.usage_query_user_id.set(""); + } + } + let len = self.usage_query_table_fields().len(); + self.usage_query_field_idx = self.usage_query_field_idx.min(len.saturating_sub(1)); + } + + pub fn refresh_usage_query_custom_variable_comment(&mut self) { + if self.usage_query_template != UsageQueryTemplate::Custom { + return; + } + + let Some(body) = Self::strip_usage_query_custom_variable_comment(&self.usage_query_code) + .map(str::to_string) + else { + return; + }; + let next = format!("{}{}", self.usage_query_custom_variable_comment(), body); + if self.usage_query_code != next { + self.usage_query_code = next; + self.touch_usage_query(); + } + } + + pub fn usage_query_script_help_lines() -> Vec { + vec![ + texts::tui_usage_query_config_format().to_string(), + "({".to_string(), + " request: {".to_string(), + " url: \"{{baseUrl}}/api/usage\",".to_string(), + " method: \"POST\",".to_string(), + " headers: {".to_string(), + " \"Authorization\": \"Bearer {{apiKey}}\",".to_string(), + " \"User-Agent\": \"cc-switch/1.0\"".to_string(), + " }".to_string(), + " },".to_string(), + " extractor: function(response) {".to_string(), + " return {".to_string(), + " isValid: !response.error,".to_string(), + " remaining: response.balance,".to_string(), + " unit: \"USD\"".to_string(), + " };".to_string(), + " }".to_string(), + "})".to_string(), + String::new(), + texts::tui_usage_query_extractor_format().to_string(), + texts::tui_usage_query_field_is_valid().to_string(), + texts::tui_usage_query_field_invalid_message().to_string(), + texts::tui_usage_query_field_remaining().to_string(), + texts::tui_usage_query_field_unit().to_string(), + texts::tui_usage_query_field_plan_name().to_string(), + texts::tui_usage_query_field_total().to_string(), + texts::tui_usage_query_field_used().to_string(), + texts::tui_usage_query_field_extra().to_string(), + String::new(), + texts::tui_usage_query_tips().to_string(), + texts::tui_usage_query_tip1().to_string(), + texts::tui_usage_query_tip2().to_string(), + texts::tui_usage_query_tip3().to_string(), + ] + } + + pub fn usage_query_template_value(&self) -> &'static str { + self.usage_query_template.as_str() + } + + pub fn usage_query_template_label(&self) -> &'static str { + self.usage_query_template.label() + } + + pub fn usage_query_extractor_available(&self) -> bool { + self.usage_query_enabled + } + + pub(crate) fn should_skip_usage_query_validation(&self) -> bool { + self.has_usage_script_meta() && !self.usage_query_touched + } + + fn usage_query_custom_preset_with_variables(&self) -> String { + format!( + "{}{}", + self.usage_query_custom_variable_comment(), + Self::USAGE_QUERY_CUSTOM_PRESET + ) + } + + fn usage_query_custom_variable_comment(&self) -> String { + let (api_key, base_url) = self.usage_query_provider_credentials(); + format!( + "// 支持的变量\n// {{{{baseUrl}}}}\n// =\n// {base_url}\n// {{{{apiKey}}}}\n// =\n// {api_key}\n\n" + ) + } + + pub fn current_provider_base_url(&self) -> String { + if self.is_claude_codex_oauth_provider() { + return "https://chatgpt.com/backend-api/codex".to_string(); + } + + match self.app_type { + AppType::Claude => self.claude_base_url.value.clone(), + AppType::Codex => self.codex_base_url.value.clone(), + AppType::Gemini => self.gemini_base_url.value.clone(), + AppType::Hermes => self.hermes_base_url.value.clone(), + AppType::OpenCode | AppType::OpenClaw => self.opencode_base_url.value.clone(), + } + } + + fn usage_query_provider_credentials(&self) -> (String, String) { + if self.is_claude_codex_oauth_provider() { + return ( + String::new(), + "https://chatgpt.com/backend-api/codex".to_string(), + ); + } + + let (api_key, base_url) = match self.app_type { + AppType::Claude => (&self.claude_api_key.value, &self.claude_base_url.value), + AppType::Codex => (&self.codex_api_key.value, &self.codex_base_url.value), + AppType::Gemini => (&self.gemini_api_key.value, &self.gemini_base_url.value), + AppType::Hermes => (&self.hermes_api_key.value, &self.hermes_base_url.value), + AppType::OpenCode | AppType::OpenClaw => { + (&self.opencode_api_key.value, &self.opencode_base_url.value) + } + }; + ( + Self::usage_query_comment_value(api_key), + Self::usage_query_comment_value(base_url), + ) + } + + fn usage_query_comment_value(value: &str) -> String { + value.trim().replace(['\r', '\n'], " ").trim().to_string() + } + + fn strip_usage_query_custom_variable_comment(code: &str) -> Option<&str> { + if !code.starts_with("// 支持的变量\n") { + return None; + } + + let mut newline_count = 0; + for (idx, ch) in code.char_indices() { + if ch == '\n' { + newline_count += 1; + if newline_count == 8 { + return code.get(idx + ch.len_utf8()..); + } + } + } + None + } +} + +pub(crate) fn detect_balance_provider_for_usage_query(base_url: &str) -> bool { + let url = base_url.to_lowercase(); + url.contains("api.deepseek.com") + || url.contains("api.stepfun.ai") + || url.contains("api.stepfun.com") + || url.contains("api.siliconflow.cn") + || url.contains("api.siliconflow.com") + || url.contains("openrouter.ai") + || url.contains("api.novita.ai") +} + +impl UsageQueryTemplate { + pub const fn as_str(self) -> &'static str { + match self { + Self::Custom => "custom", + Self::General => "general", + Self::NewApi => "newapi", + Self::Balance => "balance", + } + } + + pub fn label(self) -> &'static str { + match self { + Self::Custom => { + if crate::cli::i18n::is_chinese() { + "自定义" + } else { + "Custom" + } + } + Self::General => { + if crate::cli::i18n::is_chinese() { + "通用模板" + } else { + "General" + } + } + Self::NewApi => "NewAPI", + Self::Balance => { + if crate::cli::i18n::is_chinese() { + "官方" + } else { + "Official" + } + } + } + } + + pub fn from_str(value: &str) -> Option { + match value { + "custom" => Some(Self::Custom), + "general" => Some(Self::General), + "newapi" => Some(Self::NewApi), + "balance" => Some(Self::Balance), + _ => None, + } + } +} diff --git a/src-tauri/src/cli/tui/form/tests.rs b/src-tauri/src/cli/tui/form/tests.rs index 80146ca5..9be759b1 100644 --- a/src-tauri/src/cli/tui/form/tests.rs +++ b/src-tauri/src/cli/tui/form/tests.rs @@ -3205,9 +3205,114 @@ fn provider_add_form_usage_query_template_fields_match_upstream_visibility() { UsageQueryTemplate::Balance, ] ); - assert!(!form - .available_usage_query_templates() - .contains(&UsageQueryTemplate::TokenPlan)); + assert_eq!(UsageQueryTemplate::from_str("token_plan"), None); + assert_eq!(UsageQueryTemplate::from_str("github_copilot"), None); +} + +#[test] +fn provider_edit_form_preserves_hidden_usage_query_template_until_touched() { + let token_plan_provider = Provider::with_id( + "kimi".to_string(), + "Kimi Coding".to_string(), + json!({ + "env": { + "ANTHROPIC_AUTH_TOKEN": "sk-kimi", + "ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/v1" + } + }), + None, + ); + let provider = Provider { + meta: Some(crate::provider::ProviderMeta { + usage_script: Some(crate::provider::UsageScript { + enabled: true, + language: "javascript".to_string(), + code: "".to_string(), + timeout: Some(10), + api_key: None, + base_url: None, + access_token: None, + user_id: None, + template_type: Some("token_plan".to_string()), + auto_query_interval: Some(5), + coding_plan_provider: Some("kimi".to_string()), + }), + ..Default::default() + }), + ..token_plan_provider + }; + + let form = ProviderAddFormState::from_provider(AppType::Claude, &provider); + assert_eq!( + form.available_usage_query_templates(), + vec![ + UsageQueryTemplate::Custom, + UsageQueryTemplate::General, + UsageQueryTemplate::NewApi, + UsageQueryTemplate::Balance, + ] + ); + + let saved = form.to_provider_json_value(); + let script = &saved["meta"]["usage_script"]; + assert_eq!(script["templateType"], "token_plan"); + assert_eq!(script["codingPlanProvider"], "kimi"); + assert_eq!(script["code"], ""); + assert_eq!(script["enabled"], true); +} + +#[test] +fn provider_edit_form_preserves_hidden_github_copilot_usage_query_until_touched() { + let github_provider = Provider::with_id( + "github-copilot".to_string(), + "GitHub Copilot".to_string(), + json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://api.githubcopilot.com" + } + }), + None, + ); + let provider = Provider { + meta: Some(crate::provider::ProviderMeta { + provider_type: Some("github_copilot".to_string()), + github_account_id: Some("gh-123".to_string()), + usage_script: Some(crate::provider::UsageScript { + enabled: true, + language: "javascript".to_string(), + code: "".to_string(), + timeout: Some(10), + api_key: None, + base_url: None, + access_token: None, + user_id: None, + template_type: Some("github_copilot".to_string()), + auto_query_interval: Some(5), + coding_plan_provider: None, + }), + ..Default::default() + }), + ..github_provider + }; + + let form = ProviderAddFormState::from_provider(AppType::Claude, &provider); + let saved = form.to_provider_json_value(); + let script = &saved["meta"]["usage_script"]; + + assert_eq!(script["templateType"], "github_copilot"); + assert_eq!(script["code"], ""); + assert_eq!( + saved["meta"] + .get("providerType") + .and_then(|value| value.as_str()), + Some("github_copilot") + ); + assert_eq!( + saved["meta"] + .get("githubAccountId") + .and_then(|value| value.as_str()), + Some("gh-123") + ); } #[test] diff --git a/src-tauri/src/cli/tui/runtime_actions/helpers.rs b/src-tauri/src/cli/tui/runtime_actions/helpers.rs index f224985a..9194881e 100644 --- a/src-tauri/src/cli/tui/runtime_actions/helpers.rs +++ b/src-tauri/src/cli/tui/runtime_actions/helpers.rs @@ -191,6 +191,7 @@ pub(super) fn refresh_openclaw_daily_memory_search_results(app: &mut App) -> Res Ok(()) } +#[allow(dead_code)] pub(super) fn text_view(title: String, content: String) -> Overlay { Overlay::TextView(TextViewState { title, diff --git a/src-tauri/src/cli/tui/runtime_systems/handlers.rs b/src-tauri/src/cli/tui/runtime_systems/handlers.rs index 1710ef78..a21e1e29 100644 --- a/src-tauri/src/cli/tui/runtime_systems/handlers.rs +++ b/src-tauri/src/cli/tui/runtime_systems/handlers.rs @@ -700,6 +700,7 @@ pub(crate) fn handle_proxy_msg( Ok(()) } +#[allow(dead_code)] pub(crate) fn apply_webdav_jianguoyun_quick_setup( username: &str, password: &str, diff --git a/src-tauri/src/cli/tui/runtime_systems/types.rs b/src-tauri/src/cli/tui/runtime_systems/types.rs index 577ff4c9..bf399132 100644 --- a/src-tauri/src/cli/tui/runtime_systems/types.rs +++ b/src-tauri/src/cli/tui/runtime_systems/types.rs @@ -153,6 +153,7 @@ pub(crate) enum WebDavDone { decision: SyncDecision, message: String, }, + #[allow(dead_code)] V1Migrated { message: String, }, @@ -210,11 +211,14 @@ pub(crate) enum ManagedAuthMsg { result: Result, String>, }, DefaultSet { + #[allow(dead_code)] auth_provider: String, + #[allow(dead_code)] account_id: String, result: Result, }, Removed { + #[allow(dead_code)] auth_provider: String, account_id: String, result: Result, diff --git a/src-tauri/src/cli/tui/ui/config.rs b/src-tauri/src/cli/tui/ui/config.rs index 860e0f91..b76ee7df 100644 --- a/src-tauri/src/cli/tui/ui/config.rs +++ b/src-tauri/src/cli/tui/ui/config.rs @@ -393,6 +393,7 @@ fn pad_display_width(text: &str, width: usize) -> String { format!("{text}{}", " ".repeat(width - used)) } +#[allow(dead_code)] fn compact_two_column_lines(lines: &[String], total_width: u16) -> Option> { if lines.len() != 4 { return None; diff --git a/src-tauri/src/cli/tui/ui/forms/mod.rs b/src-tauri/src/cli/tui/ui/forms/mod.rs index ff246a88..c99621b9 100644 --- a/src-tauri/src/cli/tui/ui/forms/mod.rs +++ b/src-tauri/src/cli/tui/ui/forms/mod.rs @@ -4,9 +4,11 @@ use super::*; mod mcp; mod prompt; mod provider; +mod provider_usage_query; mod shared; pub(super) use mcp::*; pub(super) use prompt::*; pub(super) use provider::*; +pub(super) use provider_usage_query::*; pub(super) use shared::*; diff --git a/src-tauri/src/cli/tui/ui/forms/provider.rs b/src-tauri/src/cli/tui/ui/forms/provider.rs index 416396d7..29227ae6 100644 --- a/src-tauri/src/cli/tui/ui/forms/provider.rs +++ b/src-tauri/src/cli/tui/ui/forms/provider.rs @@ -436,395 +436,6 @@ pub(crate) fn render_provider_add_form( } } -fn render_usage_query_form( - frame: &mut Frame<'_>, - app: &App, - provider: &super::form::ProviderAddFormState, - area: Rect, - theme: &super::theme::Theme, -) { - let title = texts::tui_usage_query_title(provider.name.value.trim()); - let outer = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(pane_border_style(app, Focus::Content, theme)) - .title(title); - frame.render_widget(outer.clone(), area); - let inner = outer.inner(area); - - let fields = provider.usage_query_table_fields(); - let selected_field = fields - .get( - provider - .usage_query_field_idx - .min(fields.len().saturating_sub(1)), - ) - .copied(); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), - Constraint::Min(0), - Constraint::Length(3), - ]) - .split(inner); - - render_key_bar( - frame, - chunks[0], - theme, - &usage_query_form_key_items( - provider.focus, - provider.usage_query_editing, - selected_field, - provider.usage_query_extractor_available(), - ), - ); - - let body = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(55), Constraint::Percentage(45)]) - .split(chunks[1]); - - let fields_block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(focus_block_style( - matches!(provider.focus, FormFocus::Fields), - theme, - )) - .title(texts::tui_form_fields_title()); - frame.render_widget(fields_block.clone(), body[0]); - let fields_inner = fields_block.inner(body[0]); - - let rows_data = fields - .iter() - .map(|field| usage_query_field_label_and_value(provider, *field)) - .collect::>(); - - let label_col_width = field_label_column_width( - rows_data - .iter() - .map(|(label, _value)| label.as_str()) - .chain(std::iter::once(texts::tui_header_field())), - 1, - ); - - let header = Row::new(vec![ - Cell::from(cell_pad(texts::tui_header_field())), - Cell::from(texts::tui_header_value()), - ]) - .style(Style::default().fg(theme.dim).add_modifier(Modifier::BOLD)); - - let rows = rows_data.iter().map(|(label, value)| { - Row::new(vec![Cell::from(cell_pad(label)), Cell::from(value.clone())]) - }); - - let table = Table::new( - rows, - [Constraint::Length(label_col_width), Constraint::Min(10)], - ) - .header(header) - .block(Block::default().borders(Borders::NONE)) - .row_highlight_style(selection_style(theme)) - .highlight_symbol(highlight_symbol(theme)); - - let mut state = TableState::default(); - if !fields.is_empty() { - state.select(Some(provider.usage_query_field_idx.min(fields.len() - 1))); - } - frame.render_stateful_widget(table, fields_inner, &mut state); - - render_usage_query_side_panel(frame, provider, body[1], theme); - render_usage_query_input(frame, provider, selected_field, chunks[2], theme); -} - -fn render_usage_query_side_panel( - frame: &mut Frame<'_>, - provider: &super::form::ProviderAddFormState, - area: Rect, - theme: &super::theme::Theme, -) { - let extractor_available = provider.usage_query_extractor_available(); - if !extractor_available { - render_usage_query_info_panel(frame, provider, area, theme); - return; - } - - let sections = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(58), Constraint::Percentage(42)]) - .split(area); - render_usage_query_script_preview( - frame, - provider, - matches!(provider.focus, FormFocus::JsonPreview), - sections[0], - theme, - ); - render_usage_query_script_help( - frame, - matches!(provider.focus, FormFocus::Content), - sections[1], - theme, - ); -} - -fn render_usage_query_info_panel( - frame: &mut Frame<'_>, - provider: &super::form::ProviderAddFormState, - area: Rect, - theme: &super::theme::Theme, -) { - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(Style::default().fg(theme.dim)) - .title(texts::tui_usage_query_info()); - frame.render_widget(block.clone(), area); - let inner = block.inner(area); - - let hint = match provider.usage_query_template { - super::form::UsageQueryTemplate::GitHubCopilot => { - texts::tui_usage_query_copilot_auto_auth() - } - super::form::UsageQueryTemplate::TokenPlan => texts::tui_usage_query_token_plan_hint(), - super::form::UsageQueryTemplate::Custom - | super::form::UsageQueryTemplate::General - | super::form::UsageQueryTemplate::NewApi - | super::form::UsageQueryTemplate::Balance => "", - }; - - frame.render_widget( - Paragraph::new(Line::styled( - hint.to_string(), - Style::default().fg(theme.comment), - )) - .wrap(Wrap { trim: false }), - inner, - ); -} - -fn render_usage_query_script_preview( - frame: &mut Frame<'_>, - provider: &super::form::ProviderAddFormState, - active: bool, - area: Rect, - theme: &super::theme::Theme, -) { - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(focus_block_style(active, theme)) - .title(texts::tui_usage_query_script_preview_title()); - frame.render_widget(block.clone(), area); - let inner = block.inner(area); - - let script_preview = provider.usage_query_code.trim(); - let mut lines = Vec::new(); - if matches!( - provider.usage_query_template, - super::form::UsageQueryTemplate::Balance - ) { - lines.push(Line::styled( - texts::tui_usage_query_balance_hint().to_string(), - Style::default().fg(theme.comment), - )); - if !script_preview.is_empty() { - lines.push(Line::raw("")); - } - } - - let max_lines = inner.height.saturating_sub(lines.len() as u16) as usize; - for line in script_preview.lines().take(max_lines.max(1)) { - lines.push(Line::styled( - line.to_string(), - Style::default().fg(theme.comment), - )); - } - - frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner); -} - -fn render_usage_query_script_help( - frame: &mut Frame<'_>, - active: bool, - area: Rect, - theme: &super::theme::Theme, -) { - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(focus_block_style(active, theme)) - .title(texts::tui_usage_query_script_help_title()); - frame.render_widget(block.clone(), area); - let inner = block.inner(area); - - let lines = super::form::ProviderAddFormState::usage_query_script_help_lines() - .into_iter() - .enumerate() - .map(|(idx, line)| { - if idx == 0 || idx == 19 || idx == 30 { - Line::styled(line, Style::default().fg(theme.comment)) - } else { - Line::raw(line) - } - }) - .collect::>(); - - frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner); -} - -fn render_usage_query_input( - frame: &mut Frame<'_>, - provider: &super::form::ProviderAddFormState, - selected: Option, - area: Rect, - theme: &super::theme::Theme, -) { - let editor_active = provider.usage_query_editing; - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(focus_block_style(editor_active, theme)) - .title(if editor_active { - texts::tui_form_editing_title() - } else { - texts::tui_form_input_title() - }); - frame.render_widget(block.clone(), area); - let inner = block.inner(area); - - if let Some(field) = selected { - if let Some(input) = provider.usage_query_input(field) { - let (visible, cursor_x) = - visible_text_window(&input.value, input.cursor, inner.width as usize); - frame.render_widget( - Paragraph::new(Line::raw(visible)).wrap(Wrap { trim: false }), - inner, - ); - if editor_active { - let x = inner.x + cursor_x.min(inner.width.saturating_sub(1)); - frame.set_cursor_position((x, inner.y)); - } - } else { - let (line, _cursor_col) = - usage_query_field_editor_line(provider, selected, inner.width as usize); - frame.render_widget(Paragraph::new(line).wrap(Wrap { trim: false }), inner); - } - } -} - -pub(crate) fn usage_query_field_label_and_value( - provider: &super::form::ProviderAddFormState, - field: super::form::UsageQueryField, -) -> (String, String) { - let label = match field { - super::form::UsageQueryField::Enabled => texts::tui_usage_query_enable().to_string(), - super::form::UsageQueryField::Template => texts::tui_usage_query_template().to_string(), - super::form::UsageQueryField::ApiKey => { - if matches!( - provider.usage_query_template, - super::form::UsageQueryTemplate::General - ) { - format!( - "{} ({})", - texts::tui_label_api_key(), - texts::tui_usage_query_optional() - ) - } else { - texts::tui_label_api_key().to_string() - } - } - super::form::UsageQueryField::BaseUrl => { - if matches!( - provider.usage_query_template, - super::form::UsageQueryTemplate::General - ) { - format!( - "{} ({})", - texts::tui_usage_query_base_url(), - texts::tui_usage_query_optional() - ) - } else { - texts::tui_usage_query_base_url().to_string() - } - } - super::form::UsageQueryField::AccessToken => { - texts::tui_usage_query_access_token().to_string() - } - super::form::UsageQueryField::UserId => texts::tui_usage_query_user_id().to_string(), - super::form::UsageQueryField::Timeout => { - texts::tui_usage_query_timeout_seconds().to_string() - } - super::form::UsageQueryField::AutoInterval => { - texts::tui_usage_query_auto_interval().to_string() - } - super::form::UsageQueryField::CodingPlanProvider => { - texts::tui_usage_query_coding_plan_provider().to_string() - } - super::form::UsageQueryField::Script => texts::tui_usage_query_script().to_string(), - }; - - let value = match field { - super::form::UsageQueryField::Enabled => { - if provider.usage_query_enabled { - format!("[{}]", texts::tui_marker_active()) - } else { - "[ ]".to_string() - } - } - super::form::UsageQueryField::Template => provider.usage_query_template_label().to_string(), - super::form::UsageQueryField::Script => texts::tui_key_open().to_string(), - _ => provider - .usage_query_input(field) - .map(|input| input.value.trim().to_string()) - .unwrap_or_default(), - }; - - ( - label, - if value.is_empty() { - texts::tui_na().to_string() - } else { - value - }, - ) -} - -pub(crate) fn usage_query_field_editor_line( - provider: &super::form::ProviderAddFormState, - selected: Option, - _width: usize, -) -> (Line<'static>, usize) { - let Some(field) = selected else { - return (Line::raw(""), 0); - }; - - if let Some(input) = provider.usage_query_input(field) { - (Line::raw(input.value.clone()), input.cursor) - } else { - let text = match field { - super::form::UsageQueryField::Enabled => { - format!("enabled = {}", provider.usage_query_enabled) - } - super::form::UsageQueryField::Template => { - format!("templateType = {}", provider.usage_query_template_label()) - } - super::form::UsageQueryField::Script => { - format!( - "{} ({})", - texts::tui_key_open(), - provider.usage_query_template_value() - ) - } - _ => String::new(), - }; - (Line::raw(text), 0) - } -} - pub(crate) fn provider_field_label_and_value( provider: &super::form::ProviderAddFormState, field: ProviderAddField, diff --git a/src-tauri/src/cli/tui/ui/forms/provider_usage_query.rs b/src-tauri/src/cli/tui/ui/forms/provider_usage_query.rs new file mode 100644 index 00000000..d51ef929 --- /dev/null +++ b/src-tauri/src/cli/tui/ui/forms/provider_usage_query.rs @@ -0,0 +1,367 @@ +use super::*; + +pub(super) fn render_usage_query_form( + frame: &mut Frame<'_>, + app: &App, + provider: &super::form::ProviderAddFormState, + area: Rect, + theme: &super::theme::Theme, +) { + let title = texts::tui_usage_query_title(provider.name.value.trim()); + let outer = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(pane_border_style(app, Focus::Content, theme)) + .title(title); + frame.render_widget(outer.clone(), area); + let inner = outer.inner(area); + + let fields = provider.usage_query_table_fields(); + let selected_field = fields + .get( + provider + .usage_query_field_idx + .min(fields.len().saturating_sub(1)), + ) + .copied(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(inner); + + render_key_bar( + frame, + chunks[0], + theme, + &usage_query_form_key_items( + provider.focus, + provider.usage_query_editing, + selected_field, + provider.usage_query_extractor_available(), + ), + ); + + let body = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(55), Constraint::Percentage(45)]) + .split(chunks[1]); + + let fields_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(focus_block_style( + matches!(provider.focus, FormFocus::Fields), + theme, + )) + .title(texts::tui_form_fields_title()); + frame.render_widget(fields_block.clone(), body[0]); + let fields_inner = fields_block.inner(body[0]); + + let rows_data = fields + .iter() + .map(|field| usage_query_field_label_and_value(provider, *field)) + .collect::>(); + + let label_col_width = field_label_column_width( + rows_data + .iter() + .map(|(label, _value)| label.as_str()) + .chain(std::iter::once(texts::tui_header_field())), + 1, + ); + + let header = Row::new(vec![ + Cell::from(cell_pad(texts::tui_header_field())), + Cell::from(texts::tui_header_value()), + ]) + .style(Style::default().fg(theme.dim).add_modifier(Modifier::BOLD)); + + let rows = rows_data.iter().map(|(label, value)| { + Row::new(vec![Cell::from(cell_pad(label)), Cell::from(value.clone())]) + }); + + let table = Table::new( + rows, + [Constraint::Length(label_col_width), Constraint::Min(10)], + ) + .header(header) + .block(Block::default().borders(Borders::NONE)) + .row_highlight_style(selection_style(theme)) + .highlight_symbol(highlight_symbol(theme)); + + let mut state = TableState::default(); + if !fields.is_empty() { + state.select(Some(provider.usage_query_field_idx.min(fields.len() - 1))); + } + frame.render_stateful_widget(table, fields_inner, &mut state); + + render_usage_query_side_panel(frame, provider, body[1], theme); + render_usage_query_input(frame, provider, selected_field, chunks[2], theme); +} + +fn render_usage_query_side_panel( + frame: &mut Frame<'_>, + provider: &super::form::ProviderAddFormState, + area: Rect, + theme: &super::theme::Theme, +) { + let extractor_available = provider.usage_query_extractor_available(); + if !extractor_available { + render_usage_query_info_panel(frame, area, theme); + return; + } + + let sections = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(58), Constraint::Percentage(42)]) + .split(area); + render_usage_query_script_preview( + frame, + provider, + matches!(provider.focus, FormFocus::JsonPreview), + sections[0], + theme, + ); + render_usage_query_script_help( + frame, + matches!(provider.focus, FormFocus::Content), + sections[1], + theme, + ); +} + +fn render_usage_query_info_panel(frame: &mut Frame<'_>, area: Rect, theme: &super::theme::Theme) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(Style::default().fg(theme.dim)) + .title(texts::tui_usage_query_info()); + frame.render_widget(block.clone(), area); + let inner = block.inner(area); + + frame.render_widget( + Paragraph::new(Line::default()).wrap(Wrap { trim: false }), + inner, + ); +} + +fn render_usage_query_script_preview( + frame: &mut Frame<'_>, + provider: &super::form::ProviderAddFormState, + active: bool, + area: Rect, + theme: &super::theme::Theme, +) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(focus_block_style(active, theme)) + .title(texts::tui_usage_query_script_preview_title()); + frame.render_widget(block.clone(), area); + let inner = block.inner(area); + + let script_preview = provider.usage_query_code.trim(); + let mut lines = Vec::new(); + if matches!( + provider.usage_query_template, + super::form::UsageQueryTemplate::Balance + ) { + lines.push(Line::styled( + texts::tui_usage_query_balance_hint().to_string(), + Style::default().fg(theme.comment), + )); + if !script_preview.is_empty() { + lines.push(Line::raw("")); + } + } + + let max_lines = inner.height.saturating_sub(lines.len() as u16) as usize; + for line in script_preview.lines().take(max_lines.max(1)) { + lines.push(Line::styled( + line.to_string(), + Style::default().fg(theme.comment), + )); + } + + frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner); +} + +fn render_usage_query_script_help( + frame: &mut Frame<'_>, + active: bool, + area: Rect, + theme: &super::theme::Theme, +) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(focus_block_style(active, theme)) + .title(texts::tui_usage_query_script_help_title()); + frame.render_widget(block.clone(), area); + let inner = block.inner(area); + + let lines = super::form::ProviderAddFormState::usage_query_script_help_lines() + .into_iter() + .enumerate() + .map(|(idx, line)| { + if idx == 0 || idx == 19 || idx == 30 { + Line::styled(line, Style::default().fg(theme.comment)) + } else { + Line::raw(line) + } + }) + .collect::>(); + + frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner); +} + +fn render_usage_query_input( + frame: &mut Frame<'_>, + provider: &super::form::ProviderAddFormState, + selected: Option, + area: Rect, + theme: &super::theme::Theme, +) { + let editor_active = provider.usage_query_editing; + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(focus_block_style(editor_active, theme)) + .title(if editor_active { + texts::tui_form_editing_title() + } else { + texts::tui_form_input_title() + }); + frame.render_widget(block.clone(), area); + let inner = block.inner(area); + + if let Some(field) = selected { + if let Some(input) = provider.usage_query_input(field) { + let (visible, cursor_x) = + visible_text_window(&input.value, input.cursor, inner.width as usize); + frame.render_widget( + Paragraph::new(Line::raw(visible)).wrap(Wrap { trim: false }), + inner, + ); + if editor_active { + let x = inner.x + cursor_x.min(inner.width.saturating_sub(1)); + frame.set_cursor_position((x, inner.y)); + } + } else { + let (line, _cursor_col) = + usage_query_field_editor_line(provider, selected, inner.width as usize); + frame.render_widget(Paragraph::new(line).wrap(Wrap { trim: false }), inner); + } + } +} + +pub(crate) fn usage_query_field_label_and_value( + provider: &super::form::ProviderAddFormState, + field: super::form::UsageQueryField, +) -> (String, String) { + let label = match field { + super::form::UsageQueryField::Enabled => texts::tui_usage_query_enable().to_string(), + super::form::UsageQueryField::Template => texts::tui_usage_query_template().to_string(), + super::form::UsageQueryField::ApiKey => { + if matches!( + provider.usage_query_template, + super::form::UsageQueryTemplate::General + ) { + format!( + "{} ({})", + texts::tui_label_api_key(), + texts::tui_usage_query_optional() + ) + } else { + texts::tui_label_api_key().to_string() + } + } + super::form::UsageQueryField::BaseUrl => { + if matches!( + provider.usage_query_template, + super::form::UsageQueryTemplate::General + ) { + format!( + "{} ({})", + texts::tui_usage_query_base_url(), + texts::tui_usage_query_optional() + ) + } else { + texts::tui_usage_query_base_url().to_string() + } + } + super::form::UsageQueryField::AccessToken => { + texts::tui_usage_query_access_token().to_string() + } + super::form::UsageQueryField::UserId => texts::tui_usage_query_user_id().to_string(), + super::form::UsageQueryField::Timeout => { + texts::tui_usage_query_timeout_seconds().to_string() + } + super::form::UsageQueryField::AutoInterval => { + texts::tui_usage_query_auto_interval().to_string() + } + super::form::UsageQueryField::Script => texts::tui_usage_query_script().to_string(), + }; + + let value = match field { + super::form::UsageQueryField::Enabled => { + if provider.usage_query_enabled { + format!("[{}]", texts::tui_marker_active()) + } else { + "[ ]".to_string() + } + } + super::form::UsageQueryField::Template => provider.usage_query_template_label().to_string(), + super::form::UsageQueryField::Script => texts::tui_key_open().to_string(), + _ => provider + .usage_query_input(field) + .map(|input| input.value.trim().to_string()) + .unwrap_or_default(), + }; + + ( + label, + if value.is_empty() { + texts::tui_na().to_string() + } else { + value + }, + ) +} + +pub(crate) fn usage_query_field_editor_line( + provider: &super::form::ProviderAddFormState, + selected: Option, + _width: usize, +) -> (Line<'static>, usize) { + let Some(field) = selected else { + return (Line::raw(""), 0); + }; + + if let Some(input) = provider.usage_query_input(field) { + (Line::raw(input.value.clone()), input.cursor) + } else { + let text = match field { + super::form::UsageQueryField::Enabled => { + format!("enabled = {}", provider.usage_query_enabled) + } + super::form::UsageQueryField::Template => { + format!("templateType = {}", provider.usage_query_template_label()) + } + super::form::UsageQueryField::Script => { + format!( + "{} ({})", + texts::tui_key_open(), + provider.usage_query_template_value() + ) + } + _ => String::new(), + }; + (Line::raw(text), 0) + } +} diff --git a/src-tauri/src/cli/tui/ui/forms/shared.rs b/src-tauri/src/cli/tui/ui/forms/shared.rs index f0a650eb..1206d446 100644 --- a/src-tauri/src/cli/tui/ui/forms/shared.rs +++ b/src-tauri/src/cli/tui/ui/forms/shared.rs @@ -101,8 +101,7 @@ pub(crate) fn usage_query_form_key_items( let enter_action = match selected_field { Some( super::form::UsageQueryField::Enabled - | super::form::UsageQueryField::Template - | super::form::UsageQueryField::CodingPlanProvider, + | super::form::UsageQueryField::Template, ) => texts::tui_key_toggle(), Some(super::form::UsageQueryField::Script) => texts::tui_key_open(), Some(_) => texts::tui_key_edit_mode(), diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index baefeddc..7d476760 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -426,12 +426,14 @@ pub fn delete_file(path: &Path) -> Result<(), AppError> { /// 检查 Claude Code 配置状态 #[derive(Serialize, Deserialize)] +#[allow(dead_code)] pub struct ConfigStatus { pub exists: bool, pub path: String, } /// 获取 Claude Code 配置状态 +#[allow(dead_code)] pub fn get_claude_config_status() -> ConfigStatus { let path = get_claude_settings_path(); ConfigStatus { diff --git a/src-tauri/src/gemini_config.rs b/src-tauri/src/gemini_config.rs index 550b66b2..eb320c8e 100644 --- a/src-tauri/src/gemini_config.rs +++ b/src-tauri/src/gemini_config.rs @@ -384,6 +384,7 @@ pub fn write_generic_settings() -> Result<(), AppError> { since = "4.1.1", note = "PackyCode is now treated as a generic API key provider. Use write_generic_settings() instead." )] +#[allow(dead_code)] pub fn write_packycode_settings() -> Result<(), AppError> { write_generic_settings() } diff --git a/src-tauri/src/gemini_mcp.rs b/src-tauri/src/gemini_mcp.rs index a754f859..acec25f5 100644 --- a/src-tauri/src/gemini_mcp.rs +++ b/src-tauri/src/gemini_mcp.rs @@ -9,6 +9,7 @@ use crate::gemini_config::get_gemini_settings_path; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] pub struct McpStatus { pub user_config_path: String, pub user_config_exists: bool, @@ -39,6 +40,7 @@ fn write_json_value(path: &Path, value: &Value) -> Result<(), AppError> { } /// 读取 Gemini MCP 配置文件的完整 JSON 文本 +#[allow(dead_code)] pub fn read_mcp_json() -> Result, AppError> { let path = user_config_path(); if !path.exists() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e8637486..b61c9ab1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,6 +14,7 @@ mod gemini_config; mod gemini_mcp; pub mod hermes_config; mod import_export; +#[allow(dead_code)] mod init_status; mod mcp; mod openclaw_config; diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs index aa2ff310..eedf0350 100644 --- a/src-tauri/src/openclaw_config.rs +++ b/src-tauri/src/openclaw_config.rs @@ -699,6 +699,7 @@ pub fn get_providers() -> Result, AppError> { .unwrap_or_default()) } +#[allow(dead_code)] pub fn get_provider(id: &str) -> Result, AppError> { Ok(get_providers()?.get(id).cloned()) } @@ -807,6 +808,7 @@ pub fn set_typed_provider( set_provider(id, value) } +#[allow(dead_code)] pub fn get_model_catalog() -> Result>, AppError> { let config = read_openclaw_config()?; @@ -823,6 +825,7 @@ pub fn get_model_catalog() -> Result, ) -> Result { diff --git a/src-tauri/src/proxy/error.rs b/src-tauri/src/proxy/error.rs index 215b72ed..89897ad6 100644 --- a/src-tauri/src/proxy/error.rs +++ b/src-tauri/src/proxy/error.rs @@ -7,6 +7,7 @@ use serde_json::json; use thiserror::Error; #[derive(Debug, Error)] +#[allow(dead_code)] pub enum ProxyError { #[error("proxy server is already running")] AlreadyRunning, @@ -219,6 +220,7 @@ fn summarize_text_for_log(text: &str, max_chars: usize) -> String { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] pub enum ErrorCategory { Retryable, NonRetryable, diff --git a/src-tauri/src/proxy/http_client.rs b/src-tauri/src/proxy/http_client.rs index 6d4a4f87..69e5d4e9 100644 --- a/src-tauri/src/proxy/http_client.rs +++ b/src-tauri/src/proxy/http_client.rs @@ -56,6 +56,7 @@ pub fn init(proxy_url: Option<&str>) -> Result<(), String> { Ok(()) } +#[allow(dead_code)] pub fn validate_proxy(proxy_url: Option<&str>) -> Result<(), String> { let effective_url = proxy_url.filter(|value| !value.trim().is_empty()); build_client(effective_url)?; diff --git a/src-tauri/src/proxy/providers/adapter.rs b/src-tauri/src/proxy/providers/adapter.rs index 0fb2f6cf..6b43faba 100644 --- a/src-tauri/src/proxy/providers/adapter.rs +++ b/src-tauri/src/proxy/providers/adapter.rs @@ -7,6 +7,7 @@ use crate::proxy::error::ProxyError; use super::auth::AuthInfo; pub trait ProviderAdapter: Send + Sync { + #[allow(dead_code)] fn name(&self) -> &'static str; fn extract_base_url(&self, provider: &Provider) -> Result; fn extract_auth(&self, provider: &Provider) -> Option; diff --git a/src-tauri/src/proxy/providers/codex_oauth_auth.rs b/src-tauri/src/proxy/providers/codex_oauth_auth.rs index c78b06dd..32fe1ff3 100644 --- a/src-tauri/src/proxy/providers/codex_oauth_auth.rs +++ b/src-tauri/src/proxy/providers/codex_oauth_auth.rs @@ -23,6 +23,7 @@ pub enum CodexOAuthError { #[error("等待用户授权中")] AuthorizationPending, #[error("用户拒绝授权")] + #[allow(dead_code)] AccessDenied, #[error("Device Code 已过期")] ExpiredToken, @@ -492,6 +493,7 @@ impl CodexOAuthManager { self.resolve_default_account_id().await } + #[allow(dead_code)] pub async fn list_accounts(&self) -> Vec { let accounts = self.accounts.read().await.clone(); let default_id = self.resolve_default_account_id().await; @@ -548,6 +550,7 @@ impl CodexOAuthManager { Ok(()) } + #[allow(dead_code)] pub async fn is_authenticated(&self) -> bool { !self.accounts.read().await.is_empty() } diff --git a/src-tauri/src/proxy/providers/gemini.rs b/src-tauri/src/proxy/providers/gemini.rs index 9503df7e..0a0992a2 100644 --- a/src-tauri/src/proxy/providers/gemini.rs +++ b/src-tauri/src/proxy/providers/gemini.rs @@ -9,9 +9,6 @@ pub struct GeminiAdapter; #[derive(Debug, Clone)] pub struct OAuthCredentials { pub access_token: String, - pub refresh_token: Option, - pub client_id: Option, - pub client_secret: Option, } impl GeminiAdapter { @@ -44,9 +41,6 @@ impl GeminiAdapter { if key.starts_with("ya29.") { return Some(OAuthCredentials { access_token: key.to_string(), - refresh_token: None, - client_id: None, - client_secret: None, }); } @@ -64,25 +58,12 @@ impl GeminiAdapter { .get("refresh_token") .and_then(|v| v.as_str()) .map(|s| s.to_string()); - let client_id = json - .get("client_id") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let client_secret = json - .get("client_secret") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); if access_token.is_empty() && refresh_token.is_none() { return None; } - Some(OAuthCredentials { - access_token, - refresh_token, - client_id, - client_secret, - }) + Some(OAuthCredentials { access_token }) } } diff --git a/src-tauri/src/proxy/providers/mod.rs b/src-tauri/src/proxy/providers/mod.rs index fb03b307..56525e34 100644 --- a/src-tauri/src/proxy/providers/mod.rs +++ b/src-tauri/src/proxy/providers/mod.rs @@ -3,6 +3,7 @@ mod auth; mod claude; mod codex; pub mod codex_oauth_auth; +#[allow(dead_code)] pub mod copilot_auth; mod gemini; pub mod streaming; diff --git a/src-tauri/src/services/codex_oauth.rs b/src-tauri/src/services/codex_oauth.rs index 0d8882c7..4f86ac0b 100644 --- a/src-tauri/src/services/codex_oauth.rs +++ b/src-tauri/src/services/codex_oauth.rs @@ -63,6 +63,7 @@ impl CodexOAuthService { Self::manager().default_account_id().await } + #[allow(dead_code)] pub async fn list_accounts() -> Vec { Self::manager().list_accounts().await } diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 5198e70f..f211dfe0 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -5,6 +5,7 @@ pub mod codex_oauth_models; pub mod coding_plan; pub mod config; pub mod env_checker; +#[allow(dead_code)] pub mod env_manager; pub mod local_env_check; pub mod mcp; diff --git a/src-tauri/src/services/provider/claude.rs b/src-tauri/src/services/provider/claude.rs index 873cd16c..67065769 100644 --- a/src-tauri/src/services/provider/claude.rs +++ b/src-tauri/src/services/provider/claude.rs @@ -1,6 +1,7 @@ use super::*; impl ProviderService { + #[allow(dead_code)] pub(super) fn parse_common_claude_config_snippet(snippet: &str) -> Result { let value: Value = serde_json::from_str(snippet).map_err(|e| { AppError::localized( @@ -19,6 +20,7 @@ impl ProviderService { Ok(value) } + #[allow(dead_code)] pub(super) fn parse_common_claude_config_snippet_for_strip( snippet: &str, ) -> Result { @@ -108,6 +110,7 @@ impl ProviderService { } } + #[allow(dead_code)] pub(super) fn strip_common_claude_config_from_provider( provider: &mut Provider, common_config_snippet: Option<&str>, diff --git a/src-tauri/src/services/provider/codex.rs b/src-tauri/src/services/provider/codex.rs index a3104edd..8faced45 100644 --- a/src-tauri/src/services/provider/codex.rs +++ b/src-tauri/src/services/provider/codex.rs @@ -166,6 +166,7 @@ impl ProviderService { Ok(doc.to_string()) } + #[allow(dead_code)] pub(super) fn merge_toml_tables(dst: &mut toml_edit::Table, src: &toml_edit::Table) { for (key, src_item) in src.iter() { match (dst.get_mut(key), src_item.as_table()) { @@ -186,6 +187,7 @@ impl ProviderService { } } + #[cfg(test)] pub(super) fn strip_toml_tables(dst: &mut toml_edit::Table, src: &toml_edit::Table) { let mut keys_to_remove = Vec::new(); @@ -214,6 +216,7 @@ impl ProviderService { } } + #[cfg(test)] fn toml_items_equal(left: &toml_edit::Item, right: &toml_edit::Item) -> bool { match (left.as_value(), right.as_value()) { (Some(left_value), Some(right_value)) => { @@ -223,6 +226,7 @@ impl ProviderService { } } + #[allow(dead_code)] pub(super) fn strip_common_codex_config_from_provider( provider: &mut Provider, common_config_snippet: Option<&str>, diff --git a/src-tauri/src/services/provider/common.rs b/src-tauri/src/services/provider/common.rs index c26b894a..df472162 100644 --- a/src-tauri/src/services/provider/common.rs +++ b/src-tauri/src/services/provider/common.rs @@ -109,6 +109,7 @@ pub fn migrate_legacy_codex_config(cfg_text: &str, provider: &Provider) -> Optio /// When storing a provider snapshot, we remove keys that belong to the common /// config snippet so they don't get duplicated when the common snippet is /// merged back in during `write_codex_live`. +#[cfg(test)] pub(super) fn strip_codex_common_config_from_full_text( config_text: &str, common_snippet: &str, @@ -143,6 +144,7 @@ pub(super) fn strip_codex_common_config_from_full_text( Ok(doc.to_string()) } +#[allow(dead_code)] pub(super) fn merge_json_values(base: &mut Value, overlay: &Value) { match (base, overlay) { (Value::Object(base_map), Value::Object(overlay_map)) => { @@ -161,6 +163,7 @@ pub(super) fn merge_json_values(base: &mut Value, overlay: &Value) { } } +#[allow(dead_code)] pub(super) fn strip_common_values(target: &mut Value, common: &Value) { match (target, common) { (Value::Object(target_map), Value::Object(common_map)) => { diff --git a/src-tauri/src/services/provider/gemini.rs b/src-tauri/src/services/provider/gemini.rs index ccbb5dba..2275a6a0 100644 --- a/src-tauri/src/services/provider/gemini.rs +++ b/src-tauri/src/services/provider/gemini.rs @@ -1,6 +1,7 @@ use super::*; impl ProviderService { + #[allow(dead_code)] pub(super) fn parse_common_gemini_config_snippet(snippet: &str) -> Result { let value: Value = serde_json::from_str(snippet).map_err(|e| { AppError::localized( @@ -47,6 +48,7 @@ impl ProviderService { Ok(provider) } + #[allow(dead_code)] pub(super) fn strip_common_gemini_config_from_provider( provider: &mut Provider, common_config_snippet: Option<&str>, diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 1b91e910..22626c80 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -272,6 +272,7 @@ impl ProviderService { } } + #[allow(dead_code)] fn parse_common_opencode_config_snippet(snippet: &str) -> Result { let value: Value = serde_json::from_str(snippet).map_err(|e| { AppError::localized( diff --git a/src-tauri/src/services/provider/usage.rs b/src-tauri/src/services/provider/usage.rs index fa14b3a0..7a03d8d5 100644 --- a/src-tauri/src/services/provider/usage.rs +++ b/src-tauri/src/services/provider/usage.rs @@ -523,6 +523,7 @@ impl ProviderService { } } + #[cfg(test)] pub(super) fn extract_credentials( provider: &Provider, app_type: &AppType, diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 3a16b4f2..46246d01 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -86,6 +86,7 @@ impl PersistedProxyRuntimeSessionKind { } } + #[cfg(test)] fn as_env_value(&self) -> &'static str { match self { Self::Foreground => "foreground", @@ -2624,6 +2625,7 @@ impl ProxyService { false } + #[cfg(test)] async fn managed_session_ready_info( &self, child_pid: u32, @@ -2758,6 +2760,7 @@ impl ProxyService { } } + #[allow(dead_code)] fn spawn_managed_child_reaper(mut child: std::process::Child) { tokio::task::spawn_blocking(move || { let _ = child.wait(); diff --git a/src-tauri/src/services/webdav.rs b/src-tauri/src/services/webdav.rs index a1d3ccf2..03b33608 100644 --- a/src-tauri/src/services/webdav.rs +++ b/src-tauri/src/services/webdav.rs @@ -160,6 +160,7 @@ pub fn path_segments(raw: &str) -> impl Iterator { .filter(|segment| !segment.is_empty()) } +#[cfg(test)] pub fn is_jianguoyun(base_url: &str) -> bool { matches!( detect_service_from_base_url(base_url), diff --git a/src-tauri/src/session_manager/mod.rs b/src-tauri/src/session_manager/mod.rs index 512e8617..71ebb5db 100644 --- a/src-tauri/src/session_manager/mod.rs +++ b/src-tauri/src/session_manager/mod.rs @@ -38,6 +38,7 @@ pub struct SessionMessage { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] pub struct DeleteSessionRequest { pub provider_id: String, pub session_id: String, @@ -46,6 +47,7 @@ pub struct DeleteSessionRequest { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] pub struct DeleteSessionOutcome { pub provider_id: String, pub session_id: String, @@ -55,6 +57,7 @@ pub struct DeleteSessionOutcome { pub error: Option, } +#[allow(dead_code)] pub fn scan_sessions() -> Vec { let (r1, r2, r3, r4, r5, r6) = std::thread::scope(|s| { let h1 = s.spawn(codex::scan_sessions); @@ -148,6 +151,7 @@ pub fn delete_session( delete_session_with_root(provider_id, session_id, Path::new(source_path), &root) } +#[allow(dead_code)] pub fn delete_sessions(requests: &[DeleteSessionRequest]) -> Vec { collect_delete_session_outcomes(requests, |request| { delete_session( @@ -208,6 +212,7 @@ fn canonicalize_existing_path(path: &Path, label: &str) -> Result( requests: &[DeleteSessionRequest], mut deleter: F,