mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
8ccfbd36d6
* refactor(toolsearch): replace binary patch with ENABLE_TOOL_SEARCH env var toggle
- Remove toolsearch_patch.rs binary patching mechanism (~590 lines)
- Delete `toolsearch_patch.rs` and `commands/toolsearch.rs`
- Remove auto-patch startup logic and command registration from lib.rs
- Remove `tool_search_bypass` field from settings.rs
- Remove frontend settings ToggleRow, useSettings hook sync logic, and API methods
- Clean up zh/en/ja i18n keys (notifications + settings)
- Add ENABLE_TOOL_SEARCH toggle to Claude provider form
- Add checkbox in CommonConfigEditor.tsx (alongside teammates toggle)
- When enabled, writes `"env": { "ENABLE_TOOL_SEARCH": "true" }`
- When disabled, removes the key; takes effect on provider switch
- Add zh/en/ja i18n key: `claudeConfig.enableToolSearch`
Claude Code 2.1.76+ natively supports this env var, eliminating the need for binary patching.
* feat(claude): add effortLevel high toggle to provider form
- Add "high-effort thinking" checkbox to Claude provider config form
- When checked, writes `"effortLevel": "high"`; when unchecked, removes the field
- Add zh/en/ja i18n translations
* refactor(claude): remove deprecated alwaysThinking toggle
- Claude Code now enables extended thinking by default; alwaysThinkingEnabled is a no-op
- Thinking control is now handled via effortLevel (added in prior commit)
- Remove state, switch case, and checkbox UI from CommonConfigEditor
- Clean up alwaysThinking i18n keys across zh/en/ja locales
* feat(opencode): add setCacheKey: true to all provider presets
- Add setCacheKey: true to options in all 33 regular presets
- Add setCacheKey: true to OPENCODE_DEFAULT_CONFIG for custom providers
- Exclude 2 OMO presets (Oh My OpenCode / Slim) which have their own config mechanism
Closes #1523
* fix(codex): resolve 1M context window toggle causing MCP editor flicker
- Add localValueRef to short-circuit duplicate CodeMirror updateListener callbacks,
breaking the React state → CodeMirror → stale onChange → React state feedback loop
- Use localValueRef.current in handleContextWindowToggle and handleCompactLimitChange
to avoid stale closure reads
- Change compact limit input from type="number" to type="text" with inputMode="numeric"
to remove unnecessary spinner buttons
* feat(codex): add 1M context window toggle utilities and i18n keys
- Add extractCodexTopLevelInt, setCodexTopLevelInt, removeCodexTopLevelField
TOML helpers in providerConfigUtils.ts
- Add i18n keys for contextWindow1M, autoCompactLimit in zh/en/ja locales
* feat(claude): collapse model mapping fields by default
- Wrap 5 model mapping inputs in a Collapsible, collapsed by default
- Auto-expand when any model value is present (including preset-filled)
- Show hint text when collapsed explaining most users need no config
- Add zh/en/ja i18n keys for toggle label and collapsed hint
- Use variant={null} to avoid ghost button hover style clash in dark mode
* feat(claude): merge advanced fields into single collapsible section
- Merge API format, auth field, and model mapping into a unified "Advanced Options" collapsible
- Extend smart-expand logic to detect non-default values across all advanced fields
- Preserve model mapping sub-header and hint with a separator line
- Update zh/en/ja i18n keys (advancedOptionsToggle, advancedOptionsHint, modelMappingLabel, modelMappingHint)
* feat(copilot): add GitHub Copilot reverse proxy support
Add GitHub Copilot as a Claude provider variant with OAuth device code
authentication and Anthropic ↔ OpenAI format transformation.
Backend:
- Add CopilotAuthManager for GitHub OAuth device code flow
- Implement Copilot token auto-refresh (60s before expiry)
- Persist GitHub token to ~/.cc-switch/copilot_auth.json
- Add ProviderType::GitHubCopilot and AuthStrategy::GitHubCopilot
- Modify forwarder to use /chat/completions for Copilot
- Add Copilot-specific headers (Editor-Version, Editor-Plugin-Version)
Frontend:
- Add CopilotAuthSection component for OAuth UI
- Add useCopilotAuth hook for OAuth state management
- Auto-copy user code to clipboard and open browser
- Use 8-second polling interval to avoid GitHub rate limits
- Skip API Key validation for Copilot providers
- Add GitHub Copilot preset with claude-sonnet-4 model
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(copilot): remove is_expired() calls from tests
Remove references to deleted is_expired() method in test code.
Only is_expiring_soon() is needed for token refresh logic.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* feat(copilot): add real-time model listing from Copilot API
- Add fetch_models() to CopilotAuthManager calling GET /models endpoint
- Add copilot_get_models Tauri command
- Add copilotGetModels() frontend API wrapper
- Modify ClaudeFormFields to show model dropdown for Copilot providers
- Fetches available models on component mount when isCopilotPreset
- Groups models by vendor (Anthropic, OpenAI, Google, etc.)
- Input + dropdown button combo allows both manual entry and selection
- Non-Copilot providers keep original plain Input behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(copilot): add usage query integration
- Add Copilot usage API integration (fetch_usage method)
- Add copilot_get_usage Tauri command
- Add GitHub Copilot template in usage query modal
- Unify naming: copilot → github_copilot
- Add constants management (TEMPLATE_TYPES, PROVIDER_TYPES)
- Improve error handling with detailed error messages
- Add database migration (v5 → v6) for template type update
- Add i18n translations (zh, en, ja)
- Improve type safety with TemplateType
- Apply code formatting (cargo fmt, prettier)
* 修复github 登录和注销问题 ,模型选择问题
* feat(copilot): add multi-account support for GitHub Copilot
- Add multi-account storage structure with v1 to v2 migration
- Add per-account token caching and auto-refresh
- Add new Tauri commands for account management
- Integrate account selection in Proxy forwarder
- Add account selection UI in CopilotAuthSection
- Save githubAccountId to ProviderMeta
- Add i18n translations for multi-account features (zh/en/ja)
* 修复用量查询Reset字段出现多余字符
* refactor(auth-binding): introduce generic provider auth binding primitives
- add shared authBinding types in Rust and TypeScript while keeping githubAccountId as a compatibility field\n- resolve Copilot token, models, and usage through provider-bound account lookup instead of only the implicit default account\n- fix the Unix build regression in settings.rs by restoring std::io::Write for write_all()\n- remove the accidental .github ignore entry and drop leftover Copilot form debug logs\n- keep the first migration step non-breaking by writing both authBinding and the legacy githubAccountId field from the form
* refactor(auth-service): add managed auth command surface and explicit default account state
- introduce generic managed auth commands and frontend auth API wrappers for provider-scoped login, status, account listing, removal, logout, and default-account selection\n- store an explicit Copilot default_account_id instead of relying on HashMap iteration order, and use it consistently for fallback token/model/usage resolution\n- sort managed accounts deterministically and surface default-account state to the UI\n- refactor the Copilot form hook to wrap a generic useManagedAuth implementation while preserving the existing component contract\n- add default-account controls to the Copilot auth section and extend Copilot auth status serialization/tests for the new state
* feat(auth-center): add a dedicated settings entrypoint for managed OAuth accounts
- add an Auth Center tab to Settings so managed OAuth accounts are no longer hidden inside individual provider forms\n- introduce a first AuthCenterPanel that hosts GitHub Copilot account management as the initial managed auth provider\n- keep the provider form experience intact while establishing a global account-management surface for future providers such as OpenAI\n- validate that the new settings tab works cleanly with the generic managed auth hook and existing Copilot account controls
* feat(add-provider): expose managed OAuth sources alongside universal providers
- add an OAuth tab to the Add Provider flow so managed auth sources sit beside app-specific and universal providers\n- reuse the new Auth Center panel inside the dialog, keeping account management discoverable during provider creation\n- make the dialog footer adapt to the OAuth tab so account setup does not pretend to create a provider directly\n- align the add-provider UX with the new architecture where OAuth accounts are global assets and providers bind to them later
* fix(auth-reliability): harden managed auth persistence and refresh behavior
- replace direct Copilot auth store writes with private temp-file writes and atomic rename semantics, and document the local token storage limitation\n- add per-account refresh locks plus a double-check path so concurrent requests do not stampede GitHub token refresh\n- surface legacy migration failures through auth status, expose them in the UI, and add translated copy for the new account-state labels\n- stop writing the legacy githubAccountId field from the provider form while keeping compatibility reads in place\n- add logout error recovery and Copilot model-load toasts so auth failures are no longer silently swallowed
* refactor(copilot-detection): prefer provider type before URL fallbacks
- update forwarder endpoint rewriting to treat providerType as the primary GitHub Copilot signal\n- keep githubcopilot.com string matching only as a compatibility fallback for older provider records without providerType\n- reduce one more path where Copilot behavior depended purely on URL heuristics
* fix(copilot-auth): add cancel button to error state in CopilotAuthSection
- 错误状态下仅有"重试"按钮,用户无法退出(如不可恢复的 403 未订阅错误)
- 新增"取消"按钮,复用已有的 cancelAuth 逻辑重置为 idle 状态
* 修复打包后github账号头像显示异常
* 修复github copilot 来源的模型测试报错
* feat(copilot-preset): add default model presets for GitHub Copilot
- 补充 Copilot 预设的默认模型配置,用户选完预设即可直接使用
- ANTHROPIC_MODEL: claude-opus-4.6
- ANTHROPIC_DEFAULT_HAIKU_MODEL: claude-haiku-4.5
- ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4.6
- ANTHROPIC_DEFAULT_OPUS_MODEL: claude-opus-4.6
---------
Co-authored-by: Jason <farion1231@gmail.com>
Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
434 lines
15 KiB
TypeScript
434 lines
15 KiB
TypeScript
import { useMemo, useState, useEffect, useRef } from "react";
|
||
import { GripVertical, ChevronDown, ChevronUp } from "lucide-react";
|
||
import { useTranslation } from "react-i18next";
|
||
import type {
|
||
DraggableAttributes,
|
||
DraggableSyntheticListeners,
|
||
} from "@dnd-kit/core";
|
||
import type { Provider } from "@/types";
|
||
import type { AppId } from "@/lib/api";
|
||
import { cn } from "@/lib/utils";
|
||
import { ProviderActions } from "@/components/providers/ProviderActions";
|
||
import { ProviderIcon } from "@/components/ProviderIcon";
|
||
import UsageFooter from "@/components/UsageFooter";
|
||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||
import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge";
|
||
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
|
||
import { useProviderHealth } from "@/lib/query/failover";
|
||
import { useUsageQuery } from "@/lib/query/queries";
|
||
|
||
interface DragHandleProps {
|
||
attributes: DraggableAttributes;
|
||
listeners: DraggableSyntheticListeners;
|
||
isDragging: boolean;
|
||
}
|
||
|
||
interface ProviderCardProps {
|
||
provider: Provider;
|
||
isCurrent: boolean;
|
||
appId: AppId;
|
||
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
|
||
isOmo?: boolean;
|
||
isOmoSlim?: boolean;
|
||
onSwitch: (provider: Provider) => void;
|
||
onEdit: (provider: Provider) => void;
|
||
onDelete: (provider: Provider) => void;
|
||
onRemoveFromConfig?: (provider: Provider) => void;
|
||
onDisableOmo?: () => void;
|
||
onDisableOmoSlim?: () => void;
|
||
onConfigureUsage: (provider: Provider) => void;
|
||
onOpenWebsite: (url: string) => void;
|
||
onDuplicate: (provider: Provider) => void;
|
||
onTest?: (provider: Provider) => void;
|
||
onOpenTerminal?: (provider: Provider) => void;
|
||
isTesting?: boolean;
|
||
isProxyRunning: boolean;
|
||
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
|
||
dragHandleProps?: DragHandleProps;
|
||
isAutoFailoverEnabled?: boolean; // 是否开启自动故障转移
|
||
failoverPriority?: number; // 故障转移优先级(1 = P1, 2 = P2, ...)
|
||
isInFailoverQueue?: boolean; // 是否在故障转移队列中
|
||
onToggleFailover?: (enabled: boolean) => void; // 切换故障转移队列
|
||
activeProviderId?: string; // 代理当前实际使用的供应商 ID(用于故障转移模式下标注绿色边框)
|
||
// OpenClaw: default model
|
||
isDefaultModel?: boolean;
|
||
onSetAsDefault?: () => void;
|
||
}
|
||
|
||
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
||
if (provider.notes?.trim()) {
|
||
return provider.notes.trim();
|
||
}
|
||
|
||
if (provider.websiteUrl) {
|
||
return provider.websiteUrl;
|
||
}
|
||
|
||
const config = provider.settingsConfig;
|
||
|
||
if (config && typeof config === "object") {
|
||
const envBase =
|
||
(config as Record<string, any>)?.env?.ANTHROPIC_BASE_URL ||
|
||
(config as Record<string, any>)?.env?.GOOGLE_GEMINI_BASE_URL;
|
||
if (typeof envBase === "string" && envBase.trim()) {
|
||
return envBase;
|
||
}
|
||
|
||
const baseUrl = (config as Record<string, any>)?.config;
|
||
|
||
if (typeof baseUrl === "string" && baseUrl.includes("base_url")) {
|
||
const extractedBaseUrl = extractCodexBaseUrl(baseUrl);
|
||
if (extractedBaseUrl) {
|
||
return extractedBaseUrl;
|
||
}
|
||
}
|
||
}
|
||
|
||
return fallbackText;
|
||
};
|
||
|
||
export function ProviderCard({
|
||
provider,
|
||
isCurrent,
|
||
appId,
|
||
isInConfig = true,
|
||
isOmo = false,
|
||
isOmoSlim = false,
|
||
onSwitch,
|
||
onEdit,
|
||
onDelete,
|
||
onRemoveFromConfig,
|
||
onDisableOmo,
|
||
onDisableOmoSlim,
|
||
onConfigureUsage,
|
||
onOpenWebsite,
|
||
onDuplicate,
|
||
onTest,
|
||
onOpenTerminal,
|
||
isTesting,
|
||
isProxyRunning,
|
||
isProxyTakeover = false,
|
||
dragHandleProps,
|
||
isAutoFailoverEnabled = false,
|
||
failoverPriority,
|
||
isInFailoverQueue = false,
|
||
onToggleFailover,
|
||
activeProviderId,
|
||
// OpenClaw: default model
|
||
isDefaultModel,
|
||
onSetAsDefault,
|
||
}: ProviderCardProps) {
|
||
const { t } = useTranslation();
|
||
|
||
// OMO and OMO Slim share the same card behavior
|
||
const isAnyOmo = isOmo || isOmoSlim;
|
||
const handleDisableAnyOmo = isOmoSlim ? onDisableOmoSlim : onDisableOmo;
|
||
|
||
const { data: health } = useProviderHealth(provider.id, appId);
|
||
|
||
const fallbackUrlText = t("provider.notConfigured", {
|
||
defaultValue: "未配置接口地址",
|
||
});
|
||
|
||
const displayUrl = useMemo(() => {
|
||
return extractApiUrl(provider, fallbackUrlText);
|
||
}, [provider, fallbackUrlText]);
|
||
|
||
const isClickableUrl = useMemo(() => {
|
||
if (provider.notes?.trim()) {
|
||
return false;
|
||
}
|
||
if (displayUrl === fallbackUrlText) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}, [provider.notes, displayUrl, fallbackUrlText]);
|
||
|
||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||
|
||
// 获取用量数据以判断是否有多套餐
|
||
// 累加模式应用(OpenCode/OpenClaw):使用 isInConfig 代替 isCurrent
|
||
const shouldAutoQuery =
|
||
appId === "opencode" || appId === "openclaw" ? isInConfig : isCurrent;
|
||
const autoQueryInterval = shouldAutoQuery
|
||
? 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);
|
||
|
||
const actionsRef = useRef<HTMLDivElement>(null);
|
||
const [actionsWidth, setActionsWidth] = useState(0);
|
||
|
||
useEffect(() => {
|
||
if (hasMultiplePlans) {
|
||
setIsExpanded(true);
|
||
}
|
||
}, [hasMultiplePlans]);
|
||
|
||
useEffect(() => {
|
||
if (actionsRef.current) {
|
||
const updateWidth = () => {
|
||
const width = actionsRef.current?.offsetWidth || 0;
|
||
setActionsWidth(width);
|
||
};
|
||
updateWidth();
|
||
window.addEventListener("resize", updateWidth);
|
||
return () => window.removeEventListener("resize", updateWidth);
|
||
}
|
||
}, [onTest, onOpenTerminal]); // 按钮数量可能变化时重新计算
|
||
|
||
const handleOpenWebsite = () => {
|
||
if (!isClickableUrl) {
|
||
return;
|
||
}
|
||
onOpenWebsite(displayUrl);
|
||
};
|
||
|
||
// 判断是否是"当前使用中"的供应商
|
||
// - OMO/OMO Slim 供应商:使用 isCurrent
|
||
// - OpenClaw:使用默认模型归属的 provider 作为当前项(蓝色边框)
|
||
// - OpenCode(非 OMO):不存在"当前"概念,返回 false
|
||
// - 故障转移模式:代理实际使用的供应商(activeProviderId)
|
||
// - 普通模式:isCurrent
|
||
const isActiveProvider = isAnyOmo
|
||
? isCurrent
|
||
: appId === "openclaw"
|
||
? Boolean(isDefaultModel)
|
||
: appId === "opencode"
|
||
? false
|
||
: isAutoFailoverEnabled
|
||
? activeProviderId === provider.id
|
||
: isCurrent;
|
||
|
||
const shouldUseGreen = !isAnyOmo && isProxyTakeover && isActiveProvider;
|
||
const shouldUseBlue =
|
||
(isAnyOmo && isActiveProvider) ||
|
||
(!isAnyOmo && !isProxyTakeover && isActiveProvider);
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"relative overflow-hidden rounded-xl border border-border p-4 transition-all duration-300",
|
||
"bg-card text-card-foreground group",
|
||
isAutoFailoverEnabled || isProxyTakeover
|
||
? "hover:border-emerald-500/50"
|
||
: "hover:border-border-active",
|
||
shouldUseGreen &&
|
||
"border-emerald-500/60 shadow-sm shadow-emerald-500/10",
|
||
shouldUseBlue && "border-blue-500/60 shadow-sm shadow-blue-500/10",
|
||
!isActiveProvider && "hover:shadow-sm",
|
||
dragHandleProps?.isDragging &&
|
||
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
|
||
)}
|
||
>
|
||
<div
|
||
className={cn(
|
||
"absolute inset-0 bg-gradient-to-r to-transparent transition-opacity duration-500 pointer-events-none",
|
||
shouldUseGreen && "from-emerald-500/10",
|
||
shouldUseBlue && "from-blue-500/10",
|
||
!isActiveProvider && "from-primary/10",
|
||
isActiveProvider ? "opacity-100" : "opacity-0",
|
||
)}
|
||
/>
|
||
<div className="relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<div className="flex flex-1 items-center gap-2">
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
"-ml-1.5 flex-shrink-0 cursor-grab active:cursor-grabbing p-1.5",
|
||
"text-muted-foreground/50 hover:text-muted-foreground transition-colors",
|
||
dragHandleProps?.isDragging && "cursor-grabbing",
|
||
)}
|
||
aria-label={t("provider.dragHandle")}
|
||
{...(dragHandleProps?.attributes ?? {})}
|
||
{...(dragHandleProps?.listeners ?? {})}
|
||
>
|
||
<GripVertical className="h-4 w-4" />
|
||
</button>
|
||
|
||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center border border-border group-hover:scale-105 transition-transform duration-300">
|
||
<ProviderIcon
|
||
icon={provider.icon}
|
||
name={provider.name}
|
||
color={provider.iconColor}
|
||
size={20}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<div className="flex flex-wrap items-center gap-2 min-h-7">
|
||
<h3 className="text-base font-semibold leading-none">
|
||
{provider.name}
|
||
</h3>
|
||
|
||
{isOmo && (
|
||
<span className="inline-flex items-center rounded-md bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-700 dark:bg-violet-900/40 dark:text-violet-300">
|
||
OMO
|
||
</span>
|
||
)}
|
||
|
||
{isOmoSlim && (
|
||
<span className="inline-flex items-center rounded-md bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
|
||
Slim
|
||
</span>
|
||
)}
|
||
|
||
{isProxyRunning && isInFailoverQueue && health && (
|
||
<ProviderHealthBadge
|
||
consecutiveFailures={health.consecutive_failures}
|
||
/>
|
||
)}
|
||
|
||
{isAutoFailoverEnabled &&
|
||
isInFailoverQueue &&
|
||
failoverPriority && (
|
||
<FailoverPriorityBadge priority={failoverPriority} />
|
||
)}
|
||
|
||
{provider.category === "third_party" &&
|
||
provider.meta?.isPartner && (
|
||
<span
|
||
className="text-yellow-500 dark:text-yellow-400"
|
||
title={t("provider.officialPartner", {
|
||
defaultValue: "官方合作伙伴",
|
||
})}
|
||
>
|
||
⭐
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{displayUrl && (
|
||
<button
|
||
type="button"
|
||
onClick={handleOpenWebsite}
|
||
className={cn(
|
||
"inline-flex items-center text-sm max-w-[280px]",
|
||
isClickableUrl
|
||
? "text-blue-500 transition-colors hover:underline dark:text-blue-400 cursor-pointer"
|
||
: "text-muted-foreground cursor-default",
|
||
)}
|
||
title={displayUrl}
|
||
disabled={!isClickableUrl}
|
||
>
|
||
<span className="truncate">{displayUrl}</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className="relative flex items-center ml-auto min-w-0 gap-3"
|
||
style={
|
||
{
|
||
"--actions-width": `${actionsWidth || 320}px`,
|
||
} as React.CSSProperties
|
||
}
|
||
>
|
||
<div className="ml-auto">
|
||
<div className="flex items-center gap-1 transition-transform duration-200 group-hover:-translate-x-[var(--actions-width)] group-focus-within:-translate-x-[var(--actions-width)]">
|
||
{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}
|
||
isInConfig={isInConfig}
|
||
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
|
||
ref={actionsRef}
|
||
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 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
|
||
appId={appId}
|
||
isCurrent={isCurrent}
|
||
isInConfig={isInConfig}
|
||
isTesting={isTesting}
|
||
isProxyTakeover={isProxyTakeover}
|
||
isOmo={isAnyOmo}
|
||
onSwitch={() => onSwitch(provider)}
|
||
onEdit={() => onEdit(provider)}
|
||
onDuplicate={() => onDuplicate(provider)}
|
||
onTest={onTest ? () => onTest(provider) : undefined}
|
||
onConfigureUsage={() => onConfigureUsage(provider)}
|
||
onDelete={() => onDelete(provider)}
|
||
onRemoveFromConfig={
|
||
onRemoveFromConfig
|
||
? () => onRemoveFromConfig(provider)
|
||
: undefined
|
||
}
|
||
onDisableOmo={handleDisableAnyOmo}
|
||
onOpenTerminal={
|
||
onOpenTerminal ? () => onOpenTerminal(provider) : undefined
|
||
}
|
||
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
||
isInFailoverQueue={isInFailoverQueue}
|
||
onToggleFailover={onToggleFailover}
|
||
// OpenClaw: default model
|
||
isDefaultModel={isDefaultModel}
|
||
onSetAsDefault={onSetAsDefault}
|
||
/>
|
||
</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}
|
||
isInConfig={isInConfig}
|
||
inline={false}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|