fix(usage): add fallback to provider config for usage credentials (#360)

- Make usage script credential fields optional with provider config fallback
- Optimize multi-plan card display: show plan count by default, expandable for details
- Add hint text to explain credential fallback mechanism
This commit is contained in:
Sirhexs
2025-12-15 17:09:46 +08:00
committed by GitHub
parent c49cfa5ac5
commit 1172209f49
6 changed files with 177 additions and 26 deletions
+45 -3
View File
@@ -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<String> {
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<String> {
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(),
)
+20 -7
View File
@@ -400,15 +400,25 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{/* 凭证配置 */}
{shouldShowCredentialsConfig && (
<div className="space-y-4">
<h4 className="text-sm font-medium text-foreground">
{t("usageScript.credentialsConfig")}
</h4>
<div className="flex items-start justify-between">
<h4 className="text-sm font-medium text-foreground">
{t("usageScript.credentialsConfig")}
</h4>
<p className="text-xs text-muted-foreground">
{t("usageScript.credentialsHint")}
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{selectedTemplate === TEMPLATE_KEYS.GENERAL && (
<>
<div className="space-y-2">
<Label htmlFor="usage-api-key">API Key</Label>
<Label htmlFor="usage-api-key">
API Key{" "}
<span className="text-xs text-muted-foreground font-normal">
({t("usageScript.optional")})
</span>
</Label>
<div className="relative">
<Input
id="usage-api-key"
@@ -417,7 +427,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
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<UsageScriptModalProps> = ({
<div className="space-y-2">
<Label htmlFor="usage-base-url">
{t("usageScript.baseUrl")}
{t("usageScript.baseUrl")}{" "}
<span className="text-xs text-muted-foreground font-normal">
({t("usageScript.optional")})
</span>
</Label>
<Input
id="usage-base-url"
@@ -453,7 +466,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
onChange={(e) =>
setScript({ ...script, baseUrl: e.target.value })
}
placeholder="https://api.example.com"
placeholder={t("usageScript.baseUrlPlaceholder")}
autoComplete="off"
className="border-white/10"
/>
+88 -13
View File
@@ -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({
</div>
</div>
<div className="relative flex items-center ml-auto">
<div className="ml-auto transition-transform duration-300 ease-out group-hover:-translate-x-[14.5rem] group-focus-within:-translate-x-[14.5rem] sm:group-hover:-translate-x-[16.5rem] sm:group-focus-within:-translate-x-[16.5rem]">
<UsageFooter
provider={provider}
providerId={provider.id}
appId={appId}
usageEnabled={usageEnabled}
isCurrent={isCurrent}
inline={true}
/>
<div className="relative flex items-center ml-auto min-w-0">
{/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */}
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[14.5rem] group-focus-within:-translate-x-[14.5rem] sm:group-hover:-translate-x-[16rem] sm:group-focus-within:-translate-x-[16rem]">
<div className="flex items-center gap-1">
{/* 多套餐时显示套餐数量,单套餐时显示详细信息 */}
{hasMultiplePlans ? (
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
<span className="font-medium">
{t("usage.multiplePlans", {
count: usage?.data?.length || 0,
defaultValue: `${usage?.data?.length || 0} 个套餐`,
})}
</span>
</div>
) : (
<UsageFooter
provider={provider}
providerId={provider.id}
appId={appId}
usageEnabled={usageEnabled}
isCurrent={isCurrent}
inline={true}
/>
)}
{/* 展开/折叠按钮 - 仅在有多套餐时显示 */}
{hasMultiplePlans && (
<button
onClick={(e) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
}}
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-gray-500 dark:text-gray-400 flex-shrink-0"
title={
isExpanded
? t("usage.collapse", { defaultValue: "收起" })
: t("usage.expand", { defaultValue: "展开" })
}
>
{isExpanded ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)}
</button>
)}
</div>
</div>
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-300 ease-out translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0">
{/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入 */}
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0">
<ProviderActions
isCurrent={isCurrent}
isTesting={isTesting}
@@ -281,6 +342,20 @@ export function ProviderCard({
</div>
</div>
</div>
{/* 展开的完整套餐列表 */}
{isExpanded && hasMultiplePlans && (
<div className="mt-4 pt-4 border-t border-border-default">
<UsageFooter
provider={provider}
providerId={provider.id}
appId={appId}
usageEnabled={usageEnabled}
isCurrent={isCurrent}
inline={false}
/>
</div>
)}
</div>
);
}
+8 -1
View File
@@ -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'",
+8 -1
View File
@@ -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」で生成",
+8 -1
View File
@@ -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": "在'安全设置'里生成",