2025-10-16 10:49:56 +08:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
2025-09-28 20:47:44 +08:00
|
|
|
import { useTranslation } from "react-i18next";
|
2025-10-16 10:49:56 +08:00
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { Plus, Settings } from "lucide-react";
|
|
|
|
|
import type { Provider, UsageScript } from "@/types";
|
|
|
|
|
import {
|
|
|
|
|
useProvidersQuery,
|
|
|
|
|
useAddProviderMutation,
|
|
|
|
|
useUpdateProviderMutation,
|
|
|
|
|
useDeleteProviderMutation,
|
|
|
|
|
useSwitchProviderMutation,
|
|
|
|
|
} from "@/lib/query";
|
|
|
|
|
import { providersApi, type AppType } from "@/lib/api";
|
|
|
|
|
import { extractErrorMessage } from "@/utils/errorUtils";
|
|
|
|
|
import { AppSwitcher } from "@/components/AppSwitcher";
|
|
|
|
|
import { ModeToggle } from "@/components/mode-toggle";
|
|
|
|
|
import { ProviderList } from "@/components/providers/ProviderList";
|
|
|
|
|
import { AddProviderDialog } from "@/components/providers/AddProviderDialog";
|
|
|
|
|
import { EditProviderDialog } from "@/components/providers/EditProviderDialog";
|
|
|
|
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
|
|
|
import SettingsModal from "@/components/SettingsModal";
|
|
|
|
|
import { UpdateBadge } from "@/components/UpdateBadge";
|
|
|
|
|
import UsageScriptModal from "@/components/UsageScriptModal";
|
|
|
|
|
import McpPanel from "@/components/mcp/McpPanel";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
|
|
|
|
interface ProviderSwitchEvent {
|
|
|
|
|
appType: string;
|
|
|
|
|
providerId: string;
|
|
|
|
|
}
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
function App() {
|
2025-09-28 20:47:44 +08:00
|
|
|
const { t } = useTranslation();
|
2025-10-16 10:49:56 +08:00
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
2025-09-07 10:48:27 +08:00
|
|
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
2025-10-16 10:49:56 +08:00
|
|
|
const [isAddOpen, setIsAddOpen] = useState(false);
|
2025-10-08 22:35:07 +08:00
|
|
|
const [isMcpOpen, setIsMcpOpen] = useState(false);
|
2025-10-16 10:49:56 +08:00
|
|
|
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
|
|
|
|
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
|
|
|
|
const [confirmDelete, setConfirmDelete] = useState<Provider | null>(null);
|
2025-08-06 20:48:03 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const { data, isLoading, refetch } = useProvidersQuery(activeApp);
|
|
|
|
|
const providers = useMemo(() => data?.providers ?? {}, [data]);
|
|
|
|
|
const currentProviderId = data?.currentProviderId ?? "";
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const addProviderMutation = useAddProviderMutation(activeApp);
|
|
|
|
|
const updateProviderMutation = useUpdateProviderMutation(activeApp);
|
|
|
|
|
const deleteProviderMutation = useDeleteProviderMutation(activeApp);
|
|
|
|
|
const switchProviderMutation = useSwitchProviderMutation(activeApp);
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
useEffect(() => {
|
2025-10-16 10:49:56 +08:00
|
|
|
let unsubscribe: (() => void) | undefined;
|
2025-09-06 16:21:21 +08:00
|
|
|
|
|
|
|
|
const setupListener = async () => {
|
|
|
|
|
try {
|
2025-10-16 10:49:56 +08:00
|
|
|
unsubscribe = await window.api.onProviderSwitched(
|
|
|
|
|
async (event: ProviderSwitchEvent) => {
|
|
|
|
|
if (event.appType === activeApp) {
|
|
|
|
|
await refetch();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-09-06 16:21:21 +08:00
|
|
|
} catch (error) {
|
2025-10-16 10:49:56 +08:00
|
|
|
console.error("[App] Failed to subscribe provider switch event", error);
|
2025-09-06 16:21:21 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setupListener();
|
|
|
|
|
return () => {
|
2025-10-16 10:49:56 +08:00
|
|
|
unsubscribe?.();
|
2025-09-06 16:21:21 +08:00
|
|
|
};
|
2025-10-16 10:49:56 +08:00
|
|
|
}, [activeApp, refetch]);
|
2025-09-06 16:21:21 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const handleNotify = useCallback(
|
|
|
|
|
(message: string, type: "success" | "error", duration?: number) => {
|
|
|
|
|
const options = duration ? { duration } : undefined;
|
|
|
|
|
if (type === "error") {
|
|
|
|
|
toast.error(message, options);
|
|
|
|
|
} else {
|
|
|
|
|
toast.success(message, options);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
2025-08-27 11:00:53 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const handleOpenWebsite = useCallback(
|
|
|
|
|
async (url: string) => {
|
|
|
|
|
try {
|
|
|
|
|
await window.api.openExternal(url);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const detail =
|
|
|
|
|
extractErrorMessage(error) ||
|
|
|
|
|
t("notifications.openLinkFailed", {
|
|
|
|
|
defaultValue: "链接打开失败",
|
|
|
|
|
});
|
|
|
|
|
toast.error(detail);
|
2025-10-10 16:35:21 +08:00
|
|
|
}
|
2025-10-16 10:49:56 +08:00
|
|
|
},
|
|
|
|
|
[t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleAddProvider = useCallback(
|
|
|
|
|
async (provider: Omit<Provider, "id">) => {
|
|
|
|
|
await addProviderMutation.mutateAsync(provider);
|
|
|
|
|
},
|
|
|
|
|
[addProviderMutation],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleEditProvider = useCallback(
|
|
|
|
|
async (provider: Provider) => {
|
|
|
|
|
try {
|
|
|
|
|
await updateProviderMutation.mutateAsync(provider);
|
|
|
|
|
await providersApi.updateTrayMenu();
|
|
|
|
|
setEditingProvider(null);
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误提示由 mutation 统一处理
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[updateProviderMutation],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleSyncClaudePlugin = useCallback(
|
|
|
|
|
async (provider: Provider) => {
|
|
|
|
|
if (activeApp !== "claude") return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const settings = await window.api.getSettings();
|
|
|
|
|
if (!settings?.enableClaudePluginIntegration) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isOfficial = provider.category === "official";
|
|
|
|
|
await window.api.applyClaudePluginConfig({ official: isOfficial });
|
|
|
|
|
|
|
|
|
|
toast.success(
|
2025-10-01 21:23:55 +08:00
|
|
|
isOfficial
|
2025-10-16 10:49:56 +08:00
|
|
|
? t("notifications.appliedToClaudePlugin", {
|
|
|
|
|
defaultValue: "已同步为官方配置",
|
|
|
|
|
})
|
|
|
|
|
: t("notifications.removedFromClaudePlugin", {
|
|
|
|
|
defaultValue: "已移除 Claude 插件配置",
|
|
|
|
|
}),
|
|
|
|
|
{ duration: 2200 },
|
2025-10-01 21:23:55 +08:00
|
|
|
);
|
2025-10-16 10:49:56 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
const detail =
|
|
|
|
|
extractErrorMessage(error) ||
|
|
|
|
|
t("notifications.syncClaudePluginFailed", {
|
|
|
|
|
defaultValue: "同步 Claude 插件失败",
|
|
|
|
|
});
|
|
|
|
|
toast.error(detail, { duration: 4200 });
|
2025-10-01 21:23:55 +08:00
|
|
|
}
|
2025-10-16 10:49:56 +08:00
|
|
|
},
|
|
|
|
|
[activeApp, t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleSwitchProvider = useCallback(
|
|
|
|
|
async (provider: Provider) => {
|
|
|
|
|
try {
|
|
|
|
|
await switchProviderMutation.mutateAsync(provider.id);
|
|
|
|
|
await handleSyncClaudePlugin(provider);
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误提示由 mutation 与同步函数处理
|
2025-10-01 21:23:55 +08:00
|
|
|
}
|
2025-10-16 10:49:56 +08:00
|
|
|
},
|
|
|
|
|
[switchProviderMutation, handleSyncClaudePlugin],
|
|
|
|
|
);
|
2025-10-01 21:23:55 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const handleRequestDelete = useCallback((provider: Provider) => {
|
|
|
|
|
setConfirmDelete(provider);
|
|
|
|
|
}, []);
|
2025-09-19 11:06:26 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const handleConfirmDelete = useCallback(async () => {
|
|
|
|
|
if (!confirmDelete) return;
|
|
|
|
|
try {
|
|
|
|
|
await deleteProviderMutation.mutateAsync(confirmDelete.id);
|
|
|
|
|
} finally {
|
|
|
|
|
setConfirmDelete(null);
|
2025-08-06 07:59:11 +08:00
|
|
|
}
|
2025-10-16 10:49:56 +08:00
|
|
|
}, [confirmDelete, deleteProviderMutation]);
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const handleImportSuccess = useCallback(async () => {
|
|
|
|
|
await refetch();
|
2025-10-05 23:33:07 +08:00
|
|
|
try {
|
2025-10-16 10:49:56 +08:00
|
|
|
await providersApi.updateTrayMenu();
|
2025-10-05 23:33:07 +08:00
|
|
|
} catch (error) {
|
2025-10-16 10:49:56 +08:00
|
|
|
console.error("[App] Failed to refresh tray menu", error);
|
2025-10-05 23:33:07 +08:00
|
|
|
}
|
2025-10-16 10:49:56 +08:00
|
|
|
}, [refetch]);
|
2025-08-27 11:00:53 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
const handleSaveUsageScript = useCallback(
|
|
|
|
|
async (provider: Provider, script: UsageScript) => {
|
|
|
|
|
try {
|
|
|
|
|
const updatedProvider: Provider = {
|
|
|
|
|
...provider,
|
|
|
|
|
meta: {
|
|
|
|
|
...provider.meta,
|
|
|
|
|
usage_script: script,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await providersApi.update(updatedProvider, activeApp);
|
|
|
|
|
await queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ["providers", activeApp],
|
|
|
|
|
});
|
|
|
|
|
toast.success(
|
|
|
|
|
t("provider.usageSaved", {
|
|
|
|
|
defaultValue: "用量查询配置已保存",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const detail =
|
|
|
|
|
extractErrorMessage(error) ||
|
|
|
|
|
t("provider.usageSaveFailed", {
|
|
|
|
|
defaultValue: "用量查询配置保存失败",
|
|
|
|
|
});
|
|
|
|
|
toast.error(detail);
|
2025-08-07 21:28:45 +08:00
|
|
|
}
|
2025-10-16 10:49:56 +08:00
|
|
|
},
|
|
|
|
|
[activeApp, queryClient, t],
|
|
|
|
|
);
|
2025-08-07 21:28:45 +08:00
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
return (
|
2025-10-16 10:49:56 +08:00
|
|
|
<div className="flex h-screen flex-col bg-gray-50 dark:bg-gray-950">
|
|
|
|
|
<header className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-800 dark:bg-gray-900">
|
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
|
|
|
<div className="flex items-center gap-3">
|
2025-09-15 10:24:41 +08:00
|
|
|
<a
|
|
|
|
|
href="https://github.com/farion1231/cc-switch"
|
|
|
|
|
target="_blank"
|
2025-10-16 10:49:56 +08:00
|
|
|
rel="noreferrer"
|
|
|
|
|
className="text-xl font-semibold text-blue-500 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
|
2025-09-15 10:24:41 +08:00
|
|
|
>
|
2025-09-07 10:48:27 +08:00
|
|
|
CC Switch
|
2025-09-15 10:24:41 +08:00
|
|
|
</a>
|
2025-10-16 10:49:56 +08:00
|
|
|
<ModeToggle />
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => setIsSettingsOpen(true)}
|
2025-09-08 15:38:06 +08:00
|
|
|
>
|
2025-10-16 10:49:56 +08:00
|
|
|
<Settings className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<UpdateBadge onClick={() => setIsSettingsOpen(true)} />
|
2025-09-07 10:48:27 +08:00
|
|
|
</div>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
2025-09-06 16:21:21 +08:00
|
|
|
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
2025-10-16 10:49:56 +08:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
2025-10-08 22:35:07 +08:00
|
|
|
onClick={() => setIsMcpOpen(true)}
|
2025-10-08 21:22:56 +08:00
|
|
|
>
|
|
|
|
|
MCP
|
2025-10-16 10:49:56 +08:00
|
|
|
</Button>
|
|
|
|
|
<Button onClick={() => setIsAddOpen(true)}>
|
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
|
{t("header.addProvider", { defaultValue: "添加供应商" })}
|
|
|
|
|
</Button>
|
2025-09-06 16:21:21 +08:00
|
|
|
</div>
|
2025-08-04 22:16:26 +08:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
<main className="flex-1 overflow-y-auto">
|
|
|
|
|
<div className="mx-auto max-w-4xl px-6 py-6">
|
|
|
|
|
<ProviderList
|
|
|
|
|
providers={providers}
|
|
|
|
|
currentProviderId={currentProviderId}
|
|
|
|
|
appType={activeApp}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
onSwitch={handleSwitchProvider}
|
|
|
|
|
onEdit={setEditingProvider}
|
|
|
|
|
onDelete={handleRequestDelete}
|
|
|
|
|
onConfigureUsage={setUsageProvider}
|
|
|
|
|
onOpenWebsite={handleOpenWebsite}
|
|
|
|
|
onCreate={() => setIsAddOpen(true)}
|
|
|
|
|
/>
|
2025-09-06 16:21:21 +08:00
|
|
|
</div>
|
2025-08-04 22:16:26 +08:00
|
|
|
</main>
|
|
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
<AddProviderDialog
|
|
|
|
|
open={isAddOpen}
|
|
|
|
|
onOpenChange={setIsAddOpen}
|
|
|
|
|
appType={activeApp}
|
|
|
|
|
onSubmit={handleAddProvider}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<EditProviderDialog
|
|
|
|
|
open={Boolean(editingProvider)}
|
|
|
|
|
provider={editingProvider}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setEditingProvider(null);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onSubmit={handleEditProvider}
|
|
|
|
|
/>
|
2025-08-05 09:51:41 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
{usageProvider && (
|
|
|
|
|
<UsageScriptModal
|
|
|
|
|
provider={usageProvider}
|
2025-08-30 21:54:11 +08:00
|
|
|
appType={activeApp}
|
2025-10-16 10:49:56 +08:00
|
|
|
onClose={() => setUsageProvider(null)}
|
|
|
|
|
onSave={(script) => {
|
|
|
|
|
void handleSaveUsageScript(usageProvider, script);
|
|
|
|
|
}}
|
|
|
|
|
onNotify={handleNotify}
|
2025-08-05 09:51:41 +08:00
|
|
|
/>
|
|
|
|
|
)}
|
2025-08-06 16:29:52 +08:00
|
|
|
|
2025-10-16 10:49:56 +08:00
|
|
|
<ConfirmDialog
|
|
|
|
|
isOpen={Boolean(confirmDelete)}
|
|
|
|
|
title={t("confirm.deleteProvider", { defaultValue: "删除供应商" })}
|
|
|
|
|
message={
|
|
|
|
|
confirmDelete
|
|
|
|
|
? t("confirm.deleteProviderMessage", {
|
|
|
|
|
name: confirmDelete.name,
|
|
|
|
|
defaultValue: `确定删除 ${confirmDelete.name} 吗?`,
|
|
|
|
|
})
|
|
|
|
|
: ""
|
|
|
|
|
}
|
|
|
|
|
onConfirm={() => void handleConfirmDelete()}
|
|
|
|
|
onCancel={() => setConfirmDelete(null)}
|
|
|
|
|
/>
|
2025-09-07 10:48:27 +08:00
|
|
|
|
|
|
|
|
{isSettingsOpen && (
|
2025-10-05 23:33:07 +08:00
|
|
|
<SettingsModal
|
|
|
|
|
onClose={() => setIsSettingsOpen(false)}
|
|
|
|
|
onImportSuccess={handleImportSuccess}
|
2025-10-16 10:49:56 +08:00
|
|
|
onNotify={handleNotify}
|
2025-10-05 23:33:07 +08:00
|
|
|
/>
|
2025-09-07 10:48:27 +08:00
|
|
|
)}
|
2025-10-08 22:35:07 +08:00
|
|
|
|
|
|
|
|
{isMcpOpen && (
|
2025-10-09 11:04:36 +08:00
|
|
|
<McpPanel
|
2025-10-10 12:35:02 +08:00
|
|
|
appType={activeApp}
|
2025-10-09 11:04:36 +08:00
|
|
|
onClose={() => setIsMcpOpen(false)}
|
2025-10-16 10:49:56 +08:00
|
|
|
onNotify={handleNotify}
|
2025-10-09 11:04:36 +08:00
|
|
|
/>
|
2025-10-08 22:35:07 +08:00
|
|
|
)}
|
2025-08-04 22:16:26 +08:00
|
|
|
</div>
|
2025-08-06 20:48:03 +08:00
|
|
|
);
|
2025-08-04 22:16:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-06 20:48:03 +08:00
|
|
|
export default App;
|