diff --git a/src-tauri/src/services/provider/usage.rs b/src-tauri/src/services/provider/usage.rs index ee95c00c..6ff94ca9 100644 --- a/src-tauri/src/services/provider/usage.rs +++ b/src-tauri/src/services/provider/usage.rs @@ -79,6 +79,34 @@ pub(crate) async fn execute_and_format_usage_result( } } +/// Extract API key from provider configuration +fn extract_api_key_from_provider(provider: &crate::provider::Provider) -> Option { + if let Some(env) = provider.settings_config.get("env") { + // Try multiple possible API key fields + env.get("ANTHROPIC_AUTH_TOKEN") + .or_else(|| env.get("ANTHROPIC_API_KEY")) + .or_else(|| env.get("OPENROUTER_API_KEY")) + .or_else(|| env.get("GOOGLE_API_KEY")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } else { + None + } +} + +/// Extract base URL from provider configuration +fn extract_base_url_from_provider(provider: &crate::provider::Provider) -> Option { + if let Some(env) = provider.settings_config.get("env") { + // Try multiple possible base URL fields + env.get("ANTHROPIC_BASE_URL") + .or_else(|| env.get("GOOGLE_GEMINI_BASE_URL")) + .and_then(|v| v.as_str()) + .map(|s| s.trim_end_matches('/').to_string()) + } else { + None + } +} + /// Query provider usage (using saved script configuration) pub async fn query_usage( state: &AppState, @@ -114,12 +142,26 @@ pub async fn query_usage( )); } - // Get credentials directly from UsageScript, no longer extract from provider config + // Get credentials: prioritize UsageScript values, fallback to provider config + let api_key = usage_script + .api_key + .clone() + .filter(|k| !k.is_empty()) + .or_else(|| extract_api_key_from_provider(provider)) + .unwrap_or_default(); + + let base_url = usage_script + .base_url + .clone() + .filter(|u| !u.is_empty()) + .or_else(|| extract_base_url_from_provider(provider)) + .unwrap_or_default(); + ( usage_script.code.clone(), usage_script.timeout.unwrap_or(10), - usage_script.api_key.clone().unwrap_or_default(), - usage_script.base_url.clone().unwrap_or_default(), + api_key, + base_url, usage_script.access_token.clone(), usage_script.user_id.clone(), ) diff --git a/src/components/UsageScriptModal.tsx b/src/components/UsageScriptModal.tsx index bbda3c05..b7ca6a10 100644 --- a/src/components/UsageScriptModal.tsx +++ b/src/components/UsageScriptModal.tsx @@ -400,15 +400,25 @@ const UsageScriptModal: React.FC = ({ {/* 凭证配置 */} {shouldShowCredentialsConfig && (
-

- {t("usageScript.credentialsConfig")} -

+
+

+ {t("usageScript.credentialsConfig")} +

+

+ {t("usageScript.credentialsHint")} +

+
{selectedTemplate === TEMPLATE_KEYS.GENERAL && ( <>
- +
= ({ onChange={(e) => setScript({ ...script, apiKey: e.target.value }) } - placeholder="sk-xxxxx" + placeholder={t("usageScript.apiKeyPlaceholder")} autoComplete="off" className="border-white/10" /> @@ -444,7 +454,10 @@ const UsageScriptModal: React.FC = ({
= ({ onChange={(e) => setScript({ ...script, baseUrl: e.target.value }) } - placeholder="https://api.example.com" + placeholder={t("usageScript.baseUrlPlaceholder")} autoComplete="off" className="border-white/10" /> diff --git a/src/components/providers/ProviderCard.tsx b/src/components/providers/ProviderCard.tsx index 2f8029b2..a4167b71 100644 --- a/src/components/providers/ProviderCard.tsx +++ b/src/components/providers/ProviderCard.tsx @@ -1,5 +1,5 @@ -import { useMemo } from "react"; -import { GripVertical } from "lucide-react"; +import { useMemo, useState, useEffect } from "react"; +import { GripVertical, ChevronDown, ChevronUp } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { DraggableAttributes, @@ -17,6 +17,7 @@ import { useResetCircuitBreaker, } from "@/lib/query/failover"; import { toast } from "sonner"; +import { useUsageQuery } from "@/lib/query/queries"; interface DragHandleProps { attributes: DraggableAttributes; @@ -146,6 +147,29 @@ export function ProviderCard({ const usageEnabled = provider.meta?.usage_script?.enabled ?? false; + // 获取用量数据以判断是否有多套餐 + const autoQueryInterval = isCurrent + ? provider.meta?.usage_script?.autoQueryInterval || 0 + : 0; + + const { data: usage } = useUsageQuery(provider.id, appId, { + enabled: usageEnabled, + autoQueryInterval, + }); + + const hasMultiplePlans = + usage?.success && usage.data && usage.data.length > 1; + + // 多套餐默认展开 + const [isExpanded, setIsExpanded] = useState(false); + + // 当检测到多套餐时自动展开 + useEffect(() => { + if (hasMultiplePlans) { + setIsExpanded(true); + } + }, [hasMultiplePlans]); + const handleOpenWebsite = () => { if (!isClickableUrl) { return; @@ -247,19 +271,56 @@ export function ProviderCard({
-
-
- +
+ {/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */} +
+
+ {/* 多套餐时显示套餐数量,单套餐时显示详细信息 */} + {hasMultiplePlans ? ( +
+ + {t("usage.multiplePlans", { + count: usage?.data?.length || 0, + defaultValue: `${usage?.data?.length || 0} 个套餐`, + })} + +
+ ) : ( + + )} + {/* 展开/折叠按钮 - 仅在有多套餐时显示 */} + {hasMultiplePlans && ( + + )} +
-
+ {/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入 */} +
+ + {/* 展开的完整套餐列表 */} + {isExpanded && hasMultiplePlans && ( +
+ +
+ )}
); } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 24b78579..d67eab03 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -365,7 +365,10 @@ "justNow": "Just now", "minutesAgo": "{{count}} min ago", "hoursAgo": "{{count}} hr ago", - "daysAgo": "{{count}} day ago" + "daysAgo": "{{count}} day ago", + "multiplePlans": "{{count}} plans", + "expand": "Expand", + "collapse": "Collapse" }, "usageScript": { "title": "Configure Usage Query", @@ -378,6 +381,10 @@ "templateGeneral": "General", "templateNewAPI": "NewAPI", "credentialsConfig": "Credentials", + "credentialsHint": "Leave empty to use provider config", + "optional": "optional", + "apiKeyPlaceholder": "Leave empty to use provider's API Key", + "baseUrlPlaceholder": "Leave empty to use provider's base URL", "baseUrl": "Base URL", "accessToken": "Access Token", "accessTokenPlaceholder": "Generate in 'Security Settings'", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0fab2caa..39f9252e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -365,7 +365,10 @@ "justNow": "たった今", "minutesAgo": "{{count}} 分前", "hoursAgo": "{{count}} 時間前", - "daysAgo": "{{count}} 日前" + "daysAgo": "{{count}} 日前", + "multiplePlans": "{{count}} プラン", + "expand": "展開", + "collapse": "折りたたむ" }, "usageScript": { "title": "利用状況を設定", @@ -378,6 +381,10 @@ "templateGeneral": "General", "templateNewAPI": "NewAPI", "credentialsConfig": "認証情報", + "credentialsHint": "空欄の場合はプロバイダー設定を使用", + "optional": "オプション", + "apiKeyPlaceholder": "空欄の場合はプロバイダーの API Key を使用", + "baseUrlPlaceholder": "空欄の場合はプロバイダーの Base URL を使用", "baseUrl": "Base URL", "accessToken": "Access Token", "accessTokenPlaceholder": "「Security Settings」で生成", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 8809e474..a7485e35 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -365,7 +365,10 @@ "justNow": "刚刚", "minutesAgo": "{{count}} 分钟前", "hoursAgo": "{{count}} 小时前", - "daysAgo": "{{count}} 天前" + "daysAgo": "{{count}} 天前", + "multiplePlans": "{{count}} 个套餐", + "expand": "展开", + "collapse": "收起" }, "usageScript": { "title": "配置用量查询", @@ -378,6 +381,10 @@ "templateGeneral": "通用模板", "templateNewAPI": "NewAPI", "credentialsConfig": "凭证配置", + "credentialsHint": "留空则自动使用供应商配置", + "optional": "可选", + "apiKeyPlaceholder": "留空则使用供应商的 API Key", + "baseUrlPlaceholder": "留空则使用供应商的请求地址", "baseUrl": "请求地址", "accessToken": "访问令牌(在个人安全设置里获取)", "accessTokenPlaceholder": "在'安全设置'里生成",