mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +08:00
3d69da5b66
* feat(providers): add notes field for provider management - Add notes field to Provider model (backend and frontend) - Display notes with higher priority than URL in provider card - Style notes as non-clickable text to differentiate from URLs - Add notes input field in provider form - Add i18n support (zh/en) for notes field * chore: format code and clean up unused props - Run cargo fmt on Rust backend code - Format TypeScript imports and code style - Remove unused appId prop from ProviderPresetSelector - Clean up unused variables in tests - Integrate notes field handling in provider dialogs * feat(deeplink): implement ccswitch:// protocol for provider import Add deep link support to enable one-click provider configuration import via ccswitch:// URLs. Backend: - Implement URL parsing and validation (src-tauri/src/deeplink.rs) - Add Tauri commands for parse and import (src-tauri/src/commands/deeplink.rs) - Register ccswitch:// protocol in macOS Info.plist - Add comprehensive unit tests (src-tauri/tests/deeplink_import.rs) Frontend: - Create confirmation dialog with security review UI (src/components/DeepLinkImportDialog.tsx) - Add API wrapper (src/lib/api/deeplink.ts) - Integrate event listeners in App.tsx Configuration: - Update Tauri config for deep link handling - Add i18n support for Chinese and English - Include test page for deep link validation (deeplink-test.html) Files: 15 changed, 1312 insertions(+) * chore(deeplink): integrate deep link handling into app lifecycle Wire up deep link infrastructure with app initialization and event handling. Backend Integration: - Register deep link module and commands in mod.rs - Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url) - Handle deep links from single instance callback (Windows/Linux CLI) - Handle deep links from macOS system events - Add tauri-plugin-deep-link dependency (Cargo.toml) Frontend Integration: - Listen for deeplink-import/deeplink-error events in App.tsx - Update DeepLinkImportDialog component imports Configuration: - Enable deep link plugin in tauri.conf.json - Update Cargo.lock for new dependencies Localization: - Add Chinese translations for deep link UI (zh.json) - Add English translations for deep link UI (en.json) Files: 9 changed, 359 insertions(+), 18 deletions(-) * refactor(deeplink): enhance Codex provider template generation Align deep link import with UI preset generation logic by: - Adding complete config.toml template matching frontend defaults - Generating safe provider name from sanitized input - Including model_provider, reasoning_effort, and wire_api settings - Removing minimal template that only contained base_url - Cleaning up deprecated test file deeplink-test.html * style: fix clippy uninlined_format_args warnings Apply clippy --fix to use inline format arguments in: - src/mcp.rs (8 fixes) - src/services/env_manager.rs (10 fixes) * style: apply code formatting and cleanup - Format TypeScript files with Prettier (App.tsx, EnvWarningBanner.tsx, formatters.ts) - Organize Rust imports and module order alphabetically - Add newline at end of JSON files (en.json, zh.json) - Update Cargo.lock for dependency changes * feat: add model name configuration support for Codex and fix Gemini model handling - Add visual model name input field for Codex providers - Add model name extraction and update utilities in providerConfigUtils - Implement model name state management in useCodexConfigState hook - Add conditional model field rendering in CodexFormFields (non-official only) - Integrate model name sync with TOML config in ProviderForm - Fix Gemini deeplink model injection bug - Correct environment variable name from GOOGLE_GEMINI_MODEL to GEMINI_MODEL - Add test cases for Gemini model injection (with/without model) - All tests passing (9/9) - Fix Gemini model field binding in edit mode - Add geminiModel state to useGeminiConfigState hook - Extract model value during initialization and reset - Sync model field with geminiEnv state to prevent data loss on submit - Fix missing model value display when editing Gemini providers Changes: - 6 files changed, 245 insertions(+), 13 deletions(-)
235 lines
7.2 KiB
TypeScript
235 lines
7.2 KiB
TypeScript
import { useMemo } from "react";
|
|
import { MoveVertical, Copy } 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 { Button } from "@/components/ui/button";
|
|
import { ProviderActions } from "@/components/providers/ProviderActions";
|
|
import UsageFooter from "@/components/UsageFooter";
|
|
|
|
interface DragHandleProps {
|
|
attributes: DraggableAttributes;
|
|
listeners: DraggableSyntheticListeners;
|
|
isDragging: boolean;
|
|
}
|
|
|
|
interface ProviderCardProps {
|
|
provider: Provider;
|
|
isCurrent: boolean;
|
|
appId: AppId;
|
|
isEditMode?: boolean;
|
|
onSwitch: (provider: Provider) => void;
|
|
onEdit: (provider: Provider) => void;
|
|
onDelete: (provider: Provider) => void;
|
|
onConfigureUsage: (provider: Provider) => void;
|
|
onOpenWebsite: (url: string) => void;
|
|
onDuplicate: (provider: Provider) => void;
|
|
dragHandleProps?: DragHandleProps;
|
|
}
|
|
|
|
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
|
// 优先级 1: 备注
|
|
if (provider.notes?.trim()) {
|
|
return provider.notes.trim();
|
|
}
|
|
|
|
// 优先级 2: 官网地址
|
|
if (provider.websiteUrl) {
|
|
return provider.websiteUrl;
|
|
}
|
|
|
|
// 优先级 3: 从配置中提取请求地址
|
|
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 match = baseUrl.match(/base_url\s*=\s*['"]([^'"]+)['"]/);
|
|
if (match?.[1]) {
|
|
return match[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return fallbackText;
|
|
};
|
|
|
|
export function ProviderCard({
|
|
provider,
|
|
isCurrent,
|
|
appId,
|
|
isEditMode = false,
|
|
onSwitch,
|
|
onEdit,
|
|
onDelete,
|
|
onConfigureUsage,
|
|
onOpenWebsite,
|
|
onDuplicate,
|
|
dragHandleProps,
|
|
}: ProviderCardProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const fallbackUrlText = t("provider.notConfigured", {
|
|
defaultValue: "未配置接口地址",
|
|
});
|
|
|
|
const displayUrl = useMemo(() => {
|
|
return extractApiUrl(provider, fallbackUrlText);
|
|
}, [provider, fallbackUrlText]);
|
|
|
|
// 判断是否为可点击的 URL(备注不可点击)
|
|
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;
|
|
|
|
const handleOpenWebsite = () => {
|
|
if (!isClickableUrl) {
|
|
return;
|
|
}
|
|
onOpenWebsite(displayUrl);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"rounded-lg bg-card p-4 shadow-sm",
|
|
"transition-[border-color,background-color,box-shadow,ring] duration-200",
|
|
isCurrent
|
|
? "border border-border-default bg-primary/5 ring-2 ring-blue-500/30 dark:ring-blue-400/30"
|
|
: "border border-border-default hover:border-border-hover",
|
|
dragHandleProps?.isDragging &&
|
|
"cursor-grabbing border-active border-border-dragging shadow-lg",
|
|
)}
|
|
>
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex flex-1 items-center gap-2">
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-1 overflow-hidden",
|
|
"transition-[max-width,opacity] duration-200 ease-in-out",
|
|
isEditMode ? "max-w-20 opacity-100" : "max-w-0 opacity-0",
|
|
)}
|
|
aria-hidden={!isEditMode}
|
|
>
|
|
<Button
|
|
type="button"
|
|
size="icon"
|
|
variant="ghost"
|
|
className={cn(
|
|
"flex-shrink-0 cursor-grab active:cursor-grabbing",
|
|
dragHandleProps?.isDragging && "cursor-grabbing",
|
|
)}
|
|
aria-label={t("provider.dragHandle")}
|
|
disabled={!isEditMode}
|
|
{...(dragHandleProps?.attributes ?? {})}
|
|
{...(dragHandleProps?.listeners ?? {})}
|
|
>
|
|
<MoveVertical className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<Button
|
|
type="button"
|
|
size="icon"
|
|
variant="ghost"
|
|
className="flex-shrink-0"
|
|
onClick={() => onDuplicate(provider)}
|
|
disabled={!isEditMode}
|
|
aria-label={t("provider.duplicate")}
|
|
title={t("provider.duplicate")}
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="flex flex-wrap items-center gap-2 min-h-[20px]">
|
|
<h3 className="text-base font-semibold leading-none">
|
|
{provider.name}
|
|
</h3>
|
|
{provider.category === "third_party" &&
|
|
provider.meta?.isPartner && (
|
|
<span
|
|
className="text-yellow-500 dark:text-yellow-400"
|
|
title={t("provider.officialPartner", {
|
|
defaultValue: "官方合作伙伴",
|
|
})}
|
|
>
|
|
⭐
|
|
</span>
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-500 dark:text-green-400 transition-opacity duration-200",
|
|
isCurrent ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
)}
|
|
>
|
|
{t("provider.currentlyUsing")}
|
|
</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="flex items-center gap-3">
|
|
<UsageFooter
|
|
provider={provider}
|
|
providerId={provider.id}
|
|
appId={appId}
|
|
usageEnabled={usageEnabled}
|
|
isCurrent={isCurrent}
|
|
inline={true}
|
|
/>
|
|
|
|
<ProviderActions
|
|
isCurrent={isCurrent}
|
|
onSwitch={() => onSwitch(provider)}
|
|
onEdit={() => onEdit(provider)}
|
|
onConfigureUsage={() => onConfigureUsage(provider)}
|
|
onDelete={() => onDelete(provider)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|