mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
a514f27937
Prevent users from switching to official providers (Anthropic/OpenAI/Google)
when proxy takeover is active, as using a proxy with official APIs may cause
account bans.
Defense-in-depth across 4 layers:
- Backend: ProviderService::switch(), hot_switch_provider(), switch_proxy_provider command
- Frontend: useProviderActions soft guard with error toast
- UI: ProviderActions button disabled with ShieldAlert icon
- Tray menu: official provider items disabled with ⛔ indicator
Also warns when enabling proxy takeover while current provider is official.
354 lines
11 KiB
TypeScript
354 lines
11 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { toast } from "sonner";
|
|
import { useTranslation } from "react-i18next";
|
|
import { providersApi, settingsApi, openclawApi, type AppId } from "@/lib/api";
|
|
import type {
|
|
Provider,
|
|
UsageScript,
|
|
OpenClawProviderConfig,
|
|
OpenClawDefaultModel,
|
|
} from "@/types";
|
|
import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
|
|
import {
|
|
useAddProviderMutation,
|
|
useUpdateProviderMutation,
|
|
useDeleteProviderMutation,
|
|
useSwitchProviderMutation,
|
|
} from "@/lib/query";
|
|
import { extractErrorMessage } from "@/utils/errorUtils";
|
|
import { openclawKeys } from "@/hooks/useOpenClaw";
|
|
|
|
/**
|
|
* Hook for managing provider actions (add, update, delete, switch)
|
|
* Extracts business logic from App.tsx
|
|
*/
|
|
export function useProviderActions(
|
|
activeApp: AppId,
|
|
isProxyRunning?: boolean,
|
|
isProxyTakeover?: boolean,
|
|
) {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
|
|
const addProviderMutation = useAddProviderMutation(activeApp);
|
|
const updateProviderMutation = useUpdateProviderMutation(activeApp);
|
|
const deleteProviderMutation = useDeleteProviderMutation(activeApp);
|
|
const switchProviderMutation = useSwitchProviderMutation(activeApp);
|
|
|
|
// Claude 插件同步逻辑
|
|
const syncClaudePlugin = useCallback(
|
|
async (provider: Provider) => {
|
|
if (activeApp !== "claude") return;
|
|
|
|
try {
|
|
const settings = await settingsApi.get();
|
|
if (!settings?.enableClaudePluginIntegration) {
|
|
return;
|
|
}
|
|
|
|
const isOfficial = provider.category === "official";
|
|
await settingsApi.applyClaudePluginConfig({ official: isOfficial });
|
|
|
|
// 静默执行,不显示成功通知
|
|
} catch (error) {
|
|
const detail =
|
|
extractErrorMessage(error) ||
|
|
t("notifications.syncClaudePluginFailed", {
|
|
defaultValue: "同步 Claude 插件失败",
|
|
});
|
|
toast.error(detail, { duration: 4200 });
|
|
}
|
|
},
|
|
[activeApp, t],
|
|
);
|
|
|
|
// 添加供应商
|
|
const addProvider = useCallback(
|
|
async (
|
|
provider: Omit<Provider, "id"> & {
|
|
providerKey?: string;
|
|
suggestedDefaults?: OpenClawSuggestedDefaults;
|
|
addToLive?: boolean;
|
|
},
|
|
) => {
|
|
await addProviderMutation.mutateAsync(provider);
|
|
|
|
// OpenClaw: register models to allowlist after adding provider
|
|
if (activeApp === "openclaw" && provider.suggestedDefaults) {
|
|
const { model, modelCatalog } = provider.suggestedDefaults;
|
|
let modelsRegistered = false;
|
|
|
|
try {
|
|
// 1. Merge model catalog (allowlist)
|
|
if (modelCatalog && Object.keys(modelCatalog).length > 0) {
|
|
const existingCatalog = (await openclawApi.getModelCatalog()) || {};
|
|
const mergedCatalog = { ...existingCatalog, ...modelCatalog };
|
|
await openclawApi.setModelCatalog(mergedCatalog);
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
modelsRegistered = true;
|
|
}
|
|
|
|
// 2. Set default model (only if not already set)
|
|
if (model) {
|
|
const existingDefault = await openclawApi.getDefaultModel();
|
|
if (!existingDefault?.primary) {
|
|
await openclawApi.setDefaultModel(model);
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Show success toast if models were registered
|
|
if (modelsRegistered) {
|
|
toast.success(
|
|
t("notifications.openclawModelsRegistered", {
|
|
defaultValue: "模型已注册到 /model 列表",
|
|
}),
|
|
{ closeButton: true },
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// Log warning but don't block main flow - provider config is already saved
|
|
console.warn(
|
|
"[OpenClaw] Failed to register models to allowlist:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
[addProviderMutation, activeApp, queryClient, t],
|
|
);
|
|
|
|
// 更新供应商
|
|
const updateProvider = useCallback(
|
|
async (provider: Provider, originalId?: string) => {
|
|
await updateProviderMutation.mutateAsync({ provider, originalId });
|
|
|
|
// 更新托盘菜单(失败不影响主操作)
|
|
try {
|
|
await providersApi.updateTrayMenu();
|
|
} catch (trayError) {
|
|
console.error(
|
|
"Failed to update tray menu after updating provider",
|
|
trayError,
|
|
);
|
|
}
|
|
},
|
|
[updateProviderMutation],
|
|
);
|
|
|
|
// 切换供应商
|
|
const switchProvider = useCallback(
|
|
async (provider: Provider) => {
|
|
const isCopilotProvider =
|
|
activeApp === "claude" &&
|
|
provider.meta?.providerType === "github_copilot";
|
|
|
|
// Determine why this provider requires the proxy
|
|
let proxyRequiredReason: string | null = null;
|
|
if (!isProxyRunning && provider.category !== "official") {
|
|
if (isCopilotProvider) {
|
|
proxyRequiredReason = t("notifications.proxyReasonCopilot", {
|
|
defaultValue: "使用 GitHub Copilot 作为 Claude 供应商",
|
|
});
|
|
} else if (
|
|
provider.meta?.apiFormat === "openai_chat" &&
|
|
activeApp === "claude"
|
|
) {
|
|
proxyRequiredReason = t("notifications.proxyReasonOpenAIChat", {
|
|
defaultValue: "使用 OpenAI Chat 接口格式",
|
|
});
|
|
} else if (
|
|
provider.meta?.apiFormat === "openai_responses" &&
|
|
activeApp === "claude"
|
|
) {
|
|
proxyRequiredReason = t("notifications.proxyReasonOpenAIResponses", {
|
|
defaultValue: "使用 OpenAI Responses 接口格式",
|
|
});
|
|
} else if (
|
|
provider.meta?.isFullUrl &&
|
|
(activeApp === "claude" || activeApp === "codex")
|
|
) {
|
|
proxyRequiredReason = t("notifications.proxyReasonFullUrl", {
|
|
defaultValue: "开启了完整 URL 连接模式",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (proxyRequiredReason) {
|
|
toast.warning(
|
|
t("notifications.proxyRequiredForSwitch", {
|
|
reason: proxyRequiredReason,
|
|
defaultValue:
|
|
"此供应商{{reason}},需要代理服务才能正常使用,请先启动代理",
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Block official providers when proxy takeover is active
|
|
if (isProxyTakeover && provider.category === "official") {
|
|
toast.error(
|
|
t("notifications.officialBlockedByProxy", {
|
|
defaultValue:
|
|
"代理接管模式下不能切换到官方供应商,使用代理访问官方 API 可能导致账号被封禁",
|
|
}),
|
|
{ duration: 6000 },
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await switchProviderMutation.mutateAsync(provider.id);
|
|
await syncClaudePlugin(provider);
|
|
|
|
// Show backfill warning if present
|
|
if (result?.warnings?.length) {
|
|
toast.warning(
|
|
t("notifications.backfillWarning", {
|
|
defaultValue:
|
|
"切换成功,但旧供应商配置回填失败,您手动修改的配置可能未保存",
|
|
}),
|
|
{ duration: 5000 },
|
|
);
|
|
}
|
|
|
|
// 若已弹过 proxyRequired 警告则不再弹 success
|
|
if (!proxyRequiredReason) {
|
|
// OpenCode/OpenClaw: show "added to config" message instead of "switched"
|
|
const isMultiProviderApp =
|
|
activeApp === "opencode" || activeApp === "openclaw";
|
|
const messageKey = isMultiProviderApp
|
|
? "notifications.addToConfigSuccess"
|
|
: "notifications.switchSuccess";
|
|
const defaultMessage = isMultiProviderApp
|
|
? "已添加到配置"
|
|
: "切换成功!";
|
|
|
|
toast.success(t(messageKey, { defaultValue: defaultMessage }), {
|
|
closeButton: true,
|
|
});
|
|
}
|
|
} catch {
|
|
// 错误提示由 mutation 处理
|
|
}
|
|
},
|
|
[
|
|
switchProviderMutation,
|
|
syncClaudePlugin,
|
|
activeApp,
|
|
isProxyRunning,
|
|
isProxyTakeover,
|
|
t,
|
|
],
|
|
);
|
|
|
|
// 删除供应商
|
|
const deleteProvider = useCallback(
|
|
async (id: string) => {
|
|
await deleteProviderMutation.mutateAsync(id);
|
|
},
|
|
[deleteProviderMutation],
|
|
);
|
|
|
|
// 保存用量脚本
|
|
const saveUsageScript = 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],
|
|
});
|
|
// 🔧 保存用量脚本后,也应该失效该 provider 的用量查询缓存
|
|
// 这样主页列表会使用新配置重新查询,而不是使用测试时的缓存
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["usage", provider.id, activeApp],
|
|
});
|
|
toast.success(
|
|
t("provider.usageSaved", {
|
|
defaultValue: "用量查询配置已保存",
|
|
}),
|
|
{ closeButton: true },
|
|
);
|
|
} catch (error) {
|
|
const detail =
|
|
extractErrorMessage(error) ||
|
|
t("provider.usageSaveFailed", {
|
|
defaultValue: "用量查询配置保存失败",
|
|
});
|
|
toast.error(detail);
|
|
}
|
|
},
|
|
[activeApp, queryClient, t],
|
|
);
|
|
|
|
// Set provider as default model (OpenClaw only)
|
|
const setAsDefaultModel = useCallback(
|
|
async (provider: Provider) => {
|
|
const config = provider.settingsConfig as OpenClawProviderConfig;
|
|
if (!config.models || config.models.length === 0) {
|
|
toast.error(
|
|
t("notifications.openclawNoModels", {
|
|
defaultValue: "该供应商没有配置模型",
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const model: OpenClawDefaultModel = {
|
|
primary: `${provider.id}/${config.models[0].id}`,
|
|
fallbacks: config.models.slice(1).map((m) => `${provider.id}/${m.id}`),
|
|
};
|
|
|
|
try {
|
|
await openclawApi.setDefaultModel(model);
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.defaultModel,
|
|
});
|
|
await queryClient.invalidateQueries({
|
|
queryKey: openclawKeys.health,
|
|
});
|
|
toast.success(
|
|
t("notifications.openclawDefaultModelSet", {
|
|
defaultValue: "已设为默认模型",
|
|
}),
|
|
{ closeButton: true },
|
|
);
|
|
} catch (error) {
|
|
const detail =
|
|
extractErrorMessage(error) ||
|
|
t("notifications.openclawDefaultModelSetFailed", {
|
|
defaultValue: "设置默认模型失败",
|
|
});
|
|
toast.error(detail);
|
|
}
|
|
},
|
|
[queryClient, t],
|
|
);
|
|
|
|
return {
|
|
addProvider,
|
|
updateProvider,
|
|
switchProvider,
|
|
deleteProvider,
|
|
saveUsageScript,
|
|
setAsDefaultModel,
|
|
isLoading:
|
|
addProviderMutation.isPending ||
|
|
updateProviderMutation.isPending ||
|
|
deleteProviderMutation.isPending ||
|
|
switchProviderMutation.isPending,
|
|
};
|
|
}
|