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 { useState, useCallback } from "react";
  import { toast } from "sonner";
  import { useTranslation } from "react-i18next";
  import {
    streamCheckProvider,
    type StreamCheckResult,
  } from "@/lib/api/model-test";
  import type { AppId } from "@/lib/api";
  import { useResetCircuitBreaker } from "@/lib/query/failover";
  
  export function useStreamCheck(appId: AppId) {
    const { t } = useTranslation();
    const [checkingIds, setCheckingIds] = useState<Set<string>>(new Set());
    const resetCircuitBreaker = useResetCircuitBreaker();
  
    const checkProvider = useCallback(
      async (
        providerId: string,
        providerName: string,
      ): Promise<StreamCheckResult | null> => {
        setCheckingIds((prev) => new Set(prev).add(providerId));
  
        try {
          const result = await streamCheckProvider(appId, providerId);
  
          if (result.status === "operational") {
            toast.success(
              t("streamCheck.operational", {
                providerName: providerName,
                responseTimeMs: result.responseTimeMs,
                defaultValue: `${providerName} 运行正常 (${result.responseTimeMs}ms)`,
              }),
              { closeButton: true },
            );
  
            // 测试通过后重置熔断器状态
            resetCircuitBreaker.mutate({ providerId, appType: appId });
          } else if (result.status === "degraded") {
            toast.warning(
              t("streamCheck.degraded", {
                providerName: providerName,
                responseTimeMs: result.responseTimeMs,
                defaultValue: `${providerName} 响应较慢 (${result.responseTimeMs}ms)`,
              }),
            );
  
            // 降级状态也重置熔断器，因为至少能通信
            resetCircuitBreaker.mutate({ providerId, appType: appId });
          } else if (result.errorCategory === "modelNotFound") {
            // 专门处理"模型不存在/已下架"：指向配置入口，比通用 404 文案更有指导性
            toast.error(
              t("streamCheck.modelNotFound", {
                providerName: providerName,
                model: result.modelUsed,
                defaultValue: `${providerName} 测试模型 ${result.modelUsed} 不存在或已下架`,
              }),
              {
                description: t("streamCheck.modelNotFoundHint", {
                  defaultValue: "",
                }),
                duration: 10000,
                closeButton: true,
              },
            );
          } else {
            const httpStatus = result.httpStatus;
            const hintKey = httpStatus
              ? `streamCheck.httpHint.${httpStatus >= 500 ? "5xx" : httpStatus}`
              : null;
            const description =
              (hintKey ? t(hintKey, { defaultValue: "" }) : "") || undefined;
  
            // 401/403/400 = 检查被拒（供应商可能正常）；429/5xx = 临时问题
            const isProbeRejection =
              httpStatus != null &&
              ([401, 403, 400, 429].includes(httpStatus) || httpStatus >= 500);
  
            if (isProbeRejection) {
              toast.warning(
                t("streamCheck.rejected", {
                  providerName: providerName,
                  message: result.message,
                  defaultValue: `${providerName} 检查被拒: ${result.message}`,
                }),
                { description, duration: 8000, closeButton: true },
              );
            } else {
              toast.error(
                t("streamCheck.failed", {
                  providerName: providerName,
                  message: result.message,
                  defaultValue: `${providerName} 检查失败: ${result.message}`,
                }),
                { description, duration: 8000, closeButton: true },
              );
            }
          }
  
          return result;
        } catch (e) {
          toast.error(
            t("streamCheck.error", {
              providerName: providerName,
              error: String(e),
              defaultValue: `${providerName} 检查出错: ${String(e)}`,
            }),
          );
          return null;
        } finally {
          setCheckingIds((prev) => {
            const next = new Set(prev);
            next.delete(providerId);
            return next;
          });
        }
      },
      [appId, t, resetCircuitBreaker],
    );
  
    const isChecking = useCallback(
      (providerId: string) => checkingIds.has(providerId),
      [checkingIds],
    );
  
    return { checkProvider, isChecking };
  }
  