repo_description: A cross-platform desktop All-in-One assistant tool for Claude Code, Codex, OpenCode, openclaw & Gemini CLI.
repo_topics: ai-tools claude-code codex desktop-app mcp minimax omo open-source openclaw openclaw-ui opencode provider-management rust skills skills-management tauri typescript wsl-support
code: 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,
    };
  }
  