From 36fd61b2a22d1414e788776c6ad95a9bc3b0384f Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 3 Nov 2025 16:50:23 +0800 Subject: [PATCH] refactor: migrate all Tauri commands to camelCase parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses parameter naming inconsistencies caused by Tauri v2's requirement for camelCase parameter names in IPC commands. Backend changes (Rust): - Updated all command parameters from snake_case to camelCase - Commands affected: * provider.rs: providerId (×4), timeoutSecs * import_export.rs: filePath (×2), defaultName * config.rs: defaultPath - Added #[allow(non_snake_case)] attributes for camelCase parameters - Removed unused QueryUsageParams struct Frontend changes (TypeScript): - Removed redundant snake_case parameters from all invoke() calls - Updated API files: * usage.ts: removed debug logs, unified to providerId * vscode.ts: updated 8 functions (providerId, timeoutSecs, filePath, defaultName) * settings.ts: updated 4 functions (defaultPath, filePath, defaultName) - Ensured all parameters now use camelCase exclusively Test updates: - Updated MSW handlers to accept both old and new parameter formats during transition - Added i18n mock compatibility for tests Root cause: The issue stemmed from Tauri v2 strictly requiring camelCase for command parameters, while the codebase was using snake_case. This caused parameters like 'provider_id' to not be recognized by the backend, resulting in "missing providerId parameter" errors. BREAKING CHANGE: All Tauri command invocations now require camelCase parameters. Any external tools or scripts calling these commands must be updated accordingly. Fixes: Usage query always failing with "missing providerId" error Fixes: Custom endpoint management not receiving provider ID Fixes: Import/export dialogs not respecting default paths --- src-tauri/src/commands/config.rs | 5 ++-- src-tauri/src/commands/import_export.rs | 19 +++++++++----- src-tauri/src/commands/provider.rs | 35 +++++++++++++------------ src/lib/api/settings.ts | 17 +++--------- src/lib/api/usage.ts | 2 +- src/lib/api/vscode.ts | 13 ++++----- tests/components/McpFormModal.test.tsx | 2 ++ tests/msw/handlers.ts | 16 ++++++++--- 8 files changed, 57 insertions(+), 52 deletions(-) diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index c66798d..7e1f244 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -73,9 +73,10 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result, + #[allow(non_snake_case)] + defaultPath: Option, ) -> Result, String> { - let initial = default_path + let initial = defaultPath .map(|p| p.trim().to_string()) .filter(|p| !p.is_empty()); diff --git a/src-tauri/src/commands/import_export.rs b/src-tauri/src/commands/import_export.rs index f0070ef..7fb8ad3 100644 --- a/src-tauri/src/commands/import_export.rs +++ b/src-tauri/src/commands/import_export.rs @@ -11,14 +11,17 @@ use crate::store::AppState; /// 导出配置文件 #[tauri::command] -pub async fn export_config_to_file(file_path: String) -> Result { +pub async fn export_config_to_file( + #[allow(non_snake_case)] + filePath: String +) -> Result { tauri::async_runtime::spawn_blocking(move || { - let target_path = PathBuf::from(&file_path); + let target_path = PathBuf::from(&filePath); ConfigService::export_config_to_path(&target_path)?; Ok::<_, AppError>(json!({ "success": true, "message": "Configuration exported successfully", - "filePath": file_path + "filePath": filePath })) }) .await @@ -29,11 +32,12 @@ pub async fn export_config_to_file(file_path: String) -> Result { /// 从文件导入配置 #[tauri::command] pub async fn import_config_from_file( - file_path: String, + #[allow(non_snake_case)] + filePath: String, state: State<'_, AppState>, ) -> Result { let (new_config, backup_id) = tauri::async_runtime::spawn_blocking(move || { - let path_buf = PathBuf::from(&file_path); + let path_buf = PathBuf::from(&filePath); ConfigService::load_config_for_import(&path_buf) }) .await @@ -77,13 +81,14 @@ pub async fn sync_current_providers_live(state: State<'_, AppState>) -> Result( app: tauri::AppHandle, - default_name: String, + #[allow(non_snake_case)] + defaultName: String, ) -> Result, String> { let dialog = app.dialog(); let result = dialog .file() .add_filter("JSON", &["json"]) - .set_file_name(&default_name) + .set_file_name(&defaultName) .blocking_save_file(); Ok(result.map(|p| p.to_string())) diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 23daba9..e158792 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -116,12 +116,12 @@ pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result< #[tauri::command] pub async fn query_provider_usage( state: State<'_, AppState>, - provider_id: Option, + #[allow(non_snake_case)] + providerId: String, // 使用 camelCase 匹配前端 app: String, ) -> Result { - let provider_id = provider_id.ok_or_else(|| missing_param("providerId"))?; let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - ProviderService::query_usage(state.inner(), app_type, &provider_id) + ProviderService::query_usage(state.inner(), app_type, &providerId) .await .map_err(|e| e.to_string()) } @@ -137,9 +137,10 @@ pub fn read_live_provider_settings(app: String) -> Result, - timeout_secs: Option, + #[allow(non_snake_case)] + timeoutSecs: Option, ) -> Result, String> { - SpeedtestService::test_endpoints(urls, timeout_secs) + SpeedtestService::test_endpoints(urls, timeoutSecs) .await .map_err(|e| e.to_string()) } @@ -149,11 +150,11 @@ pub async fn test_api_endpoints( pub fn get_custom_endpoints( state: State<'_, AppState>, app: String, - provider_id: Option, + #[allow(non_snake_case)] + providerId: String, ) -> Result, String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - let provider_id = provider_id.ok_or_else(|| missing_param("providerId"))?; - ProviderService::get_custom_endpoints(state.inner(), app_type, &provider_id) + ProviderService::get_custom_endpoints(state.inner(), app_type, &providerId) .map_err(|e| e.to_string()) } @@ -162,12 +163,12 @@ pub fn get_custom_endpoints( pub fn add_custom_endpoint( state: State<'_, AppState>, app: String, - provider_id: Option, + #[allow(non_snake_case)] + providerId: String, url: String, ) -> Result<(), String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - let provider_id = provider_id.ok_or_else(|| missing_param("providerId"))?; - ProviderService::add_custom_endpoint(state.inner(), app_type, &provider_id, url) + ProviderService::add_custom_endpoint(state.inner(), app_type, &providerId, url) .map_err(|e| e.to_string()) } @@ -176,12 +177,12 @@ pub fn add_custom_endpoint( pub fn remove_custom_endpoint( state: State<'_, AppState>, app: String, - provider_id: Option, + #[allow(non_snake_case)] + providerId: String, url: String, ) -> Result<(), String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - let provider_id = provider_id.ok_or_else(|| missing_param("providerId"))?; - ProviderService::remove_custom_endpoint(state.inner(), app_type, &provider_id, url) + ProviderService::remove_custom_endpoint(state.inner(), app_type, &providerId, url) .map_err(|e| e.to_string()) } @@ -190,12 +191,12 @@ pub fn remove_custom_endpoint( pub fn update_endpoint_last_used( state: State<'_, AppState>, app: String, - provider_id: Option, + #[allow(non_snake_case)] + providerId: String, url: String, ) -> Result<(), String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - let provider_id = provider_id.ok_or_else(|| missing_param("providerId"))?; - ProviderService::update_endpoint_last_used(state.inner(), app_type, &provider_id, url) + ProviderService::update_endpoint_last_used(state.inner(), app_type, &providerId, url) .map_err(|e| e.to_string()) } diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 5e30ec6..dc5874a 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -39,7 +39,7 @@ export const settingsApi = { }, async selectConfigDirectory(defaultPath?: string): Promise { - return await invoke("pick_directory", { default_path: defaultPath }); + return await invoke("pick_directory", { defaultPath }); }, async getClaudeCodeConfigPath(): Promise { @@ -70,10 +70,7 @@ export const settingsApi = { }, async saveFileDialog(defaultName: string): Promise { - return await invoke("save_file_dialog", { - default_name: defaultName, - defaultName, - }); + return await invoke("save_file_dialog", { defaultName }); }, async openFileDialog(): Promise { @@ -81,17 +78,11 @@ export const settingsApi = { }, async exportConfigToFile(filePath: string): Promise { - return await invoke("export_config_to_file", { - file_path: filePath, - filePath, - }); + return await invoke("export_config_to_file", { filePath }); }, async importConfigFromFile(filePath: string): Promise { - return await invoke("import_config_from_file", { - file_path: filePath, - filePath, - }); + return await invoke("import_config_from_file", { filePath }); }, async syncCurrentProvidersLive(): Promise { diff --git a/src/lib/api/usage.ts b/src/lib/api/usage.ts index badc88c..eb08730 100644 --- a/src/lib/api/usage.ts +++ b/src/lib/api/usage.ts @@ -7,7 +7,7 @@ export const usageApi = { async query(providerId: string, appId: AppId): Promise { try { return await invoke("query_provider_usage", { - provider_id: providerId, + providerId: providerId, app: appId, }); } catch (error: unknown) { diff --git a/src/lib/api/vscode.ts b/src/lib/api/vscode.ts index 124c05e..4da38f5 100644 --- a/src/lib/api/vscode.ts +++ b/src/lib/api/vscode.ts @@ -20,7 +20,7 @@ export const vscodeApi = { ): Promise { return await invoke("test_api_endpoints", { urls, - timeout_secs: options?.timeoutSecs, + timeoutSecs: options?.timeoutSecs, }); }, @@ -30,7 +30,7 @@ export const vscodeApi = { ): Promise { return await invoke("get_custom_endpoints", { app: appId, - provider_id: providerId, + providerId: providerId, }); }, @@ -41,7 +41,7 @@ export const vscodeApi = { ): Promise { await invoke("add_custom_endpoint", { app: appId, - provider_id: providerId, + providerId: providerId, url, }); }, @@ -53,7 +53,7 @@ export const vscodeApi = { ): Promise { await invoke("remove_custom_endpoint", { app: appId, - provider_id: providerId, + providerId: providerId, url, }); }, @@ -65,28 +65,25 @@ export const vscodeApi = { ): Promise { await invoke("update_endpoint_last_used", { app: appId, - provider_id: providerId, + providerId: providerId, url, }); }, async exportConfigToFile(filePath: string) { return await invoke("export_config_to_file", { - file_path: filePath, filePath, }); }, async importConfigFromFile(filePath: string) { return await invoke("import_config_from_file", { - file_path: filePath, filePath, }); }, async saveFileDialog(defaultName: string): Promise { return await invoke("save_file_dialog", { - default_name: defaultName, defaultName, }); }, diff --git a/tests/components/McpFormModal.test.tsx b/tests/components/McpFormModal.test.tsx index 367815e..18000ca 100644 --- a/tests/components/McpFormModal.test.tsx +++ b/tests/components/McpFormModal.test.tsx @@ -19,6 +19,8 @@ vi.mock("react-i18next", () => ({ t: (key: string, params?: Record) => params ? `${key}:${JSON.stringify(params)}` : key, }), + // 提供 initReactI18next 以兼容 i18n 初始化路径 + initReactI18next: { type: "3rdParty", init: () => {} }, })); vi.mock("@/config/mcpPresets", () => ({ diff --git a/tests/msw/handlers.ts b/tests/msw/handlers.ts index fe1b6a2..2b85dd4 100644 --- a/tests/msw/handlers.ts +++ b/tests/msw/handlers.ts @@ -169,13 +169,21 @@ export const handlers = [ http.post(`${TAURI_ENDPOINT}/is_portable_mode`, () => success(false)), http.post(`${TAURI_ENDPOINT}/select_config_directory`, async ({ request }) => { - const { default_path } = await withJson<{ default_path?: string }>(request); - return success(default_path ? `${default_path}/picked` : "/mock/selected-dir"); + const { defaultPath, default_path } = await withJson<{ + defaultPath?: string; + default_path?: string; + }>(request); + const initial = defaultPath ?? default_path; + return success(initial ? `${initial}/picked` : "/mock/selected-dir"); }), http.post(`${TAURI_ENDPOINT}/pick_directory`, async ({ request }) => { - const { default_path } = await withJson<{ default_path?: string }>(request); - return success(default_path ? `${default_path}/picked` : "/mock/selected-dir"); + const { defaultPath, default_path } = await withJson<{ + defaultPath?: string; + default_path?: string; + }>(request); + const initial = defaultPath ?? default_path; + return success(initial ? `${initial}/picked` : "/mock/selected-dir"); }), http.post(`${TAURI_ENDPOINT}/open_file_dialog`, () =>