diff --git a/src-tauri/src/deeplink/mod.rs b/src-tauri/src/deeplink/mod.rs index 5c1e1a72..4111089e 100644 --- a/src-tauri/src/deeplink/mod.rs +++ b/src-tauri/src/deeplink/mod.rs @@ -113,4 +113,27 @@ pub struct DeepLinkImportRequest { /// Remote config URL #[serde(skip_serializing_if = "Option::is_none")] pub config_url: Option, + + // ============ Usage script fields (v3.9+) ============ + /// Whether to enable usage query (default: true if usage_script is provided) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_enabled: Option, + /// Base64 encoded usage query script code + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_script: Option, + /// Usage query API key (if different from provider API key) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_api_key: Option, + /// Usage query base URL (if different from provider endpoint) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_base_url: Option, + /// Usage query access token (for NewAPI template) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_access_token: Option, + /// Usage query user ID (for NewAPI template) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_user_id: Option, + /// Auto query interval in minutes (0 to disable) + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_auto_interval: Option, } diff --git a/src-tauri/src/deeplink/parser.rs b/src-tauri/src/deeplink/parser.rs index 316af003..61cf7b68 100644 --- a/src-tauri/src/deeplink/parser.rs +++ b/src-tauri/src/deeplink/parser.rs @@ -122,6 +122,19 @@ fn parse_provider_deeplink( let config_url = params.get("configUrl").cloned(); let enabled = params.get("enabled").and_then(|v| v.parse::().ok()); + // Extract usage script fields (v3.9+) + let usage_enabled = params + .get("usageEnabled") + .and_then(|v| v.parse::().ok()); + let usage_script = params.get("usageScript").cloned(); + let usage_api_key = params.get("usageApiKey").cloned(); + let usage_base_url = params.get("usageBaseUrl").cloned(); + let usage_access_token = params.get("usageAccessToken").cloned(); + let usage_user_id = params.get("usageUserId").cloned(); + let usage_auto_interval = params + .get("usageAutoInterval") + .and_then(|v| v.parse::().ok()); + Ok(DeepLinkImportRequest { version, resource, @@ -146,6 +159,13 @@ fn parse_provider_deeplink( config, config_format, config_url, + usage_enabled, + usage_script, + usage_api_key, + usage_base_url, + usage_access_token, + usage_user_id, + usage_auto_interval, }) } @@ -206,6 +226,13 @@ fn parse_prompt_deeplink( config: None, config_format: None, config_url: None, + usage_enabled: None, + usage_script: None, + usage_api_key: None, + usage_base_url: None, + usage_access_token: None, + usage_user_id: None, + usage_auto_interval: None, }) } @@ -261,6 +288,13 @@ fn parse_mcp_deeplink( directory: None, branch: None, config_url: None, + usage_enabled: None, + usage_script: None, + usage_api_key: None, + usage_base_url: None, + usage_access_token: None, + usage_user_id: None, + usage_auto_interval: None, }) } @@ -309,5 +343,12 @@ fn parse_skill_deeplink( config: None, config_format: None, config_url: None, + usage_enabled: None, + usage_script: None, + usage_api_key: None, + usage_base_url: None, + usage_access_token: None, + usage_user_id: None, + usage_auto_interval: None, }) } diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 3b6cd863..f1bac94c 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -5,7 +5,7 @@ use super::utils::{decode_base64_param, infer_homepage_from_endpoint}; use super::DeepLinkImportRequest; use crate::error::AppError; -use crate::provider::Provider; +use crate::provider::{Provider, ProviderMeta, UsageScript}; use crate::services::ProviderService; use crate::store::AppState; use crate::AppType; @@ -117,6 +117,9 @@ pub(crate) fn build_provider_from_request( AppType::Gemini => build_gemini_settings(request), }; + // Build usage script configuration if provided + let meta = build_provider_meta(request)?; + let provider = Provider { id: String::new(), // Will be generated by caller name: request.name.clone().unwrap_or_default(), @@ -126,7 +129,7 @@ pub(crate) fn build_provider_from_request( created_at: None, sort_index: None, notes: request.notes.clone(), - meta: None, + meta, icon: request.icon.clone(), icon_color: None, is_proxy_target: None, @@ -135,6 +138,57 @@ pub(crate) fn build_provider_from_request( Ok(provider) } +/// Build provider meta with usage script configuration +fn build_provider_meta(request: &DeepLinkImportRequest) -> Result, AppError> { + // Check if any usage script fields are provided + if request.usage_script.is_none() + && request.usage_enabled.is_none() + && request.usage_api_key.is_none() + && request.usage_base_url.is_none() + && request.usage_access_token.is_none() + && request.usage_user_id.is_none() + && request.usage_auto_interval.is_none() + { + return Ok(None); + } + + // Decode usage script code if provided + let code = if let Some(script_b64) = &request.usage_script { + let decoded = decode_base64_param("usage_script", script_b64)?; + String::from_utf8(decoded) + .map_err(|e| AppError::InvalidInput(format!("Invalid UTF-8 in usage_script: {e}")))? + } else { + String::new() + }; + + // Determine enabled state: explicit param > has code > false + let enabled = request.usage_enabled.unwrap_or(!code.is_empty()); + + // Build UsageScript - use provider's API key and endpoint as defaults + let usage_script = UsageScript { + enabled, + language: "javascript".to_string(), + code, + timeout: Some(10), + api_key: request + .usage_api_key + .clone() + .or_else(|| request.api_key.clone()), + base_url: request + .usage_base_url + .clone() + .or_else(|| request.endpoint.clone()), + access_token: request.usage_access_token.clone(), + user_id: request.usage_user_id.clone(), + auto_query_interval: request.usage_auto_interval, + }; + + Ok(Some(ProviderMeta { + usage_script: Some(usage_script), + ..Default::default() + })) +} + /// Build Claude settings configuration fn build_claude_settings(request: &DeepLinkImportRequest) -> serde_json::Value { let mut env = serde_json::Map::new(); diff --git a/src/components/DeepLinkImportDialog.tsx b/src/components/DeepLinkImportDialog.tsx index 96035e38..5f6f9cc6 100644 --- a/src/components/DeepLinkImportDialog.tsx +++ b/src/components/DeepLinkImportDialog.tsx @@ -32,7 +32,7 @@ export function DeepLinkImportDialog() { // 容错判断:MCP 导入结果可能缺少 type 字段 const isMcpImportResult = ( - value: unknown, + value: unknown ): value is { importedCount: number; importedIds: string[]; @@ -59,7 +59,7 @@ export function DeepLinkImportDialog() { if (event.payload.config || event.payload.configUrl) { try { const mergedRequest = await deeplinkApi.mergeDeeplinkConfig( - event.payload, + event.payload ); console.log("Config merged successfully:", mergedRequest); setRequest(mergedRequest); @@ -77,7 +77,7 @@ export function DeepLinkImportDialog() { } setIsOpen(true); - }, + } ); // Listen for deep link error events @@ -148,7 +148,7 @@ export function DeepLinkImportDialog() { window.dispatchEvent( new CustomEvent("prompt-imported", { detail: { app: request.app }, - }), + }) ); toast.success(t("deeplink.promptImportSuccess"), { description: t("deeplink.promptImportSuccessDescription", { @@ -279,7 +279,7 @@ export function DeepLinkImportDialog() { const maskValue = (key: string, value: string): string => { const sensitiveKeys = ["TOKEN", "KEY", "SECRET", "PASSWORD"]; const isSensitive = sensitiveKeys.some((k) => - key.toUpperCase().includes(k), + key.toUpperCase().includes(k) ); if (isSensitive && value.length > 8) { return `${value.substring(0, 8)}${"*".repeat(12)}`; @@ -521,7 +521,7 @@ export function DeepLinkImportDialog() { {maskValue(key, String(value))} - ), + ) )} )} @@ -548,7 +548,7 @@ export function DeepLinkImportDialog() { {maskValue(key, String(value))} - ), + ) )} )} @@ -584,7 +584,7 @@ export function DeepLinkImportDialog() { {maskValue(key, String(value))} - ), + ) )} )} @@ -605,6 +605,86 @@ export function DeepLinkImportDialog() { )} + {/* Usage Script Configuration (v3.9+) */} + {request.usageScript && ( +
+
+
+ {t("deeplink.usageScript", { + defaultValue: "用量查询", + })} +
+
+ + {request.usageEnabled !== false + ? t("deeplink.usageScriptEnabled", { + defaultValue: "已启用", + }) + : t("deeplink.usageScriptDisabled", { + defaultValue: "未启用", + })} + +
+
+ + {/* Usage API Key (if different from provider) */} + {request.usageApiKey && + request.usageApiKey !== request.apiKey && ( +
+
+ {t("deeplink.usageApiKey", { + defaultValue: "用量 API Key", + })} +
+
+ {request.usageApiKey.length > 4 + ? `${request.usageApiKey.substring(0, 4)}${"*".repeat(12)}` + : "****"} +
+
+ )} + + {/* Usage Base URL (if different from provider) */} + {request.usageBaseUrl && + request.usageBaseUrl !== request.endpoint && ( +
+
+ {t("deeplink.usageBaseUrl", { + defaultValue: "用量查询地址", + })} +
+
+ {request.usageBaseUrl} +
+
+ )} + + {/* Auto Query Interval */} + {request.usageAutoInterval && + request.usageAutoInterval > 0 && ( +
+
+ {t("deeplink.usageAutoInterval", { + defaultValue: "自动查询", + })} +
+
+ {t("deeplink.usageAutoIntervalValue", { + defaultValue: "每 {{minutes}} 分钟", + minutes: request.usageAutoInterval, + })} +
+
+ )} +
+ )} + {/* Warning */}
{t("deeplink.warning")} diff --git a/src/lib/api/deeplink.ts b/src/lib/api/deeplink.ts index 003c19f6..6b03175f 100644 --- a/src/lib/api/deeplink.ts +++ b/src/lib/api/deeplink.ts @@ -38,6 +38,15 @@ export interface DeepLinkImportRequest { config?: string; configFormat?: string; configUrl?: string; + + // Usage script fields (v3.9+) + usageEnabled?: boolean; + usageScript?: string; + usageApiKey?: string; + usageBaseUrl?: string; + usageAccessToken?: string; + usageUserId?: string; + usageAutoInterval?: number; } export interface McpImportResult { @@ -77,7 +86,7 @@ export const deeplinkApi = { * @returns Merged deep link request with config fields populated */ mergeDeeplinkConfig: async ( - request: DeepLinkImportRequest, + request: DeepLinkImportRequest ): Promise => { return invoke("merge_deeplink_config", { request }); }, @@ -88,7 +97,7 @@ export const deeplinkApi = { * @returns Import result based on resource type */ importFromDeeplink: async ( - request: DeepLinkImportRequest, + request: DeepLinkImportRequest ): Promise => { return invoke("import_from_deeplink_unified", { request }); }, diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 0f5a52c6..04e23a95 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -1,6 +1,6 @@ /** * 格式化 JSON 字符串 - * @param value - 原始 JSON 字符串(支持带键名包装的格式) + * @param value - 原始 JSON 字符串 * @returns 格式化后的 JSON 字符串(2 空格缩进) * @throws 如果 JSON 格式无效 */ @@ -9,9 +9,8 @@ export function formatJSON(value: string): string { if (!trimmed) { return ""; } - // 使用智能解析器来处理可能的片段格式 - const result = parseSmartMcpJson(trimmed); - return result.formattedConfig; + const parsed = JSON.parse(trimmed); + return JSON.stringify(parsed, null, 2); } /**