Files
cc-switch/src/lib/query/proxy.ts
T
Jason 600fdb0c15 fix(ui): drop the restart hints for Claude Code aggregation changes
Aggregated models used to reach Claude Code through gateway discovery,
which only runs at startup, so the UI asked for a restart after every
change. Now that they are written into settings.json as modelPicker, a
running Claude Code picks up added and removed models, a new default
and window changes on its own. Codex still reads its model catalog only
at startup, so its hints stay as they are.

- useStackModelsChangedHint, which only existed to toast "restart
  Claude Code" when the published models changed, is removed together
  with its test and provider.stackModelsChanged.
- Adding or removing an aggregated provider says
  "Saved. Takes effect in Claude Code right away" for Claude Code
  (new provider.stackSavedLive); entering Aggregation says the models
  are now in /model (new mode.toast.enteredStackLive), and the entry
  dialog no longer lists a restart note for it
  (mode.dialog.stackNoteRestart removed).
- The Claude Code model list hint drops "Restart Claude Code after
  changes", and the Aggregation setting's description now says only
  Codex needs a restart.
- The unused proxy.stackMode.tooltip keys are removed.

All four locales are updated.
2026-10-05 23:22:05 +08:00

323 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { proxyApi } from "@/lib/api/proxy";
import { toast } from "@/lib/toast";
import { useTranslation } from "react-i18next";
import type {
AppModeView,
GlobalProxyConfig,
AppProxyConfig,
ProxyStackWriteError,
ProxyTakeoverStatus,
} from "@/types/proxy";
import { extractErrorMessage } from "@/utils/errorUtils";
import { getAppLabel } from "@/config/appConfig";
export const proxyKeys = {
status: ["proxyStatus"] as const,
takeoverStatus: ["proxyTakeoverStatus"] as const,
globalConfig: ["globalProxyConfig"] as const,
appConfig: (appType: string) => ["appProxyConfig", appType] as const,
};
// ========== 代理服务器状态 Hooks ==========
/**
* 获取代理服务器状态
*/
export function useProxyStatusQuery() {
return useQuery({
queryKey: proxyKeys.status,
queryFn: () => proxyApi.getProxyStatus(),
// 仅在服务运行时轮询
refetchInterval: (query) => (query.state.data?.running ? 2000 : false),
// 保持之前的数据,避免闪烁
placeholderData: (previousData) => previousData,
});
}
/**
* 获取各应用接管状态
*/
export function useProxyTakeoverStatus(poll = true) {
return useQuery({
queryKey: proxyKeys.takeoverStatus,
queryFn: () => proxyApi.getProxyTakeoverStatus(),
refetchInterval: poll ? 2000 : false,
...(poll
? {}
: {
placeholderData: (previousData: ProxyTakeoverStatus | undefined) =>
previousData,
}),
});
}
/**
* 应用的模式状态(直连 / 路由 / 聚合、路由目标、直连那家)。放在 ["providers", appId] 前缀下:
* 进出模式、切换供应商时随供应商列表一起失效。
*/
export function useAppMode(appType: string, enabled = true) {
return useQuery({
queryKey: ["providers", appType, "mode"] as const,
queryFn: () => proxyApi.getAppMode(appType),
enabled,
placeholderData: (previous: AppModeView | undefined) => previous,
});
}
/**
* 直连供应商(路由模式下退出路由时写回的那家)。
* 放在 ["providers", appId] 前缀下:切换、编辑供应商时随供应商列表一起失效。
*/
export function useDirectProviderId(appType: string, enabled: boolean) {
return useQuery({
queryKey: ["providers", appType, "direct"] as const,
queryFn: () => proxyApi.getDirectProvider(appType),
enabled,
});
}
/**
* Stack 模型名单。放在 ["providers", appId] 前缀下:编辑、删除供应商时随列表一起失效。
*/
export function useProxyStack(appType: string, enabled: boolean) {
return useQuery({
queryKey: ["providers", appType, "stack"] as const,
queryFn: () => proxyApi.getProxyStack(appType),
enabled,
});
}
function isStackWriteError(error: unknown): error is ProxyStackWriteError {
return (
typeof error === "object" &&
error !== null &&
typeof (error as ProxyStackWriteError).partial === "boolean"
);
}
/**
* 把一家加入或移出 Stack 模型。Claude Code 运行中就会读到改过的 settings.json,Codex 只在
* 启动时读模型目录,成功后提示重启。失败分两种:
* 什么都没改(弹后端的错误),已部分写入(下次操作或重启 CC Switch 时补完)。两种都按
* 后端的状态重新显示,不在前端假设名单不变。
*/
export function useSetProxyStackMember() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: ({
appType,
providerId,
enabled,
}: {
appType: string;
providerId: string;
enabled: boolean;
}) => proxyApi.setProxyStackMember(appType, providerId, enabled),
onSuccess: (notice, variables) => {
toast.success(
t(
variables.appType === "codex"
? "provider.stackSaved"
: "provider.stackSavedLive",
{ client: getAppLabel(variables.appType) },
),
{
description: variables.enabled
? t("provider.stackReselectHint")
: undefined,
closeButton: true,
},
);
if (notice) {
toast.warning(t(`provider.${notice}`), { closeButton: true });
}
},
onError: (error: unknown) => {
if (isStackWriteError(error) && error.partial) {
toast.warning(t("provider.stackPartial"), {
description: error.message,
closeButton: true,
});
return;
}
toast.error(
t("provider.stackFailed", { error: extractErrorMessage(error) }),
);
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({
queryKey: ["providers", variables.appType],
});
},
});
}
/**
* Codex 聚合的模型被路由供应商自己的模型目录挡住(routeOwnsCatalog)时,改用 CC Switch
* 生成的目录。客户端只在启动时读模型目录,成功后提示重启;还剩别的提示照样弹出。
*/
export function useAdoptCodexStackCatalog() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: () => proxyApi.adoptCodexStackCatalog(),
onSuccess: (notice) => {
toast.success(t("provider.adoptCatalogDone"), { closeButton: true });
if (notice) {
toast.warning(t(`provider.${notice}`), { closeButton: true });
}
},
onError: (error: unknown) => {
toast.error(
t("provider.adoptCatalogFailed", {
error: extractErrorMessage(error),
}),
);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["providers", "codex"] });
},
});
}
/**
* 重启 Codex 的托管守护进程,让它重读模型目录。结束后重新查 Stack 名单:重启成功时
* 「还在用旧模型列表」的提示随之消失。
*/
export function useRestartCodexAppServerDaemon() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: () => proxyApi.restartCodexAppServerDaemon(),
onSuccess: (outcome) => {
if (outcome === "notRunning") {
toast.info(t("proxy.stackMode.codexStale.notRunning"), {
closeButton: true,
});
return;
}
toast.success(t("proxy.stackMode.codexStale.restarted"), {
closeButton: true,
});
},
onError: (error: unknown) => {
toast.error(
t("proxy.stackMode.codexStale.failed", {
error: extractErrorMessage(error),
}),
);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["providers", "codex"] });
},
});
}
// ========== 代理服务器控制 Hooks ==========
/**
* 设置应用接管状态
*/
export function useSetProxyTakeoverForApp() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
appType,
enabled,
stack = false,
}: {
appType: string;
enabled: boolean;
stack?: boolean;
}) => proxyApi.setProxyTakeoverForApp(appType, enabled, stack),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: proxyKeys.takeoverStatus });
// 进出路由模式会改「当前」显示的供应商(路由模式下是路由到的那家)。
queryClient.invalidateQueries({
queryKey: ["providers", variables.appType],
});
},
});
}
// ========== v3+ 全局/应用级配置 Hooks ==========
/**
* 获取全局代理配置
*/
export function useGlobalProxyConfig() {
return useQuery({
queryKey: proxyKeys.globalConfig,
queryFn: () => proxyApi.getGlobalProxyConfig(),
});
}
/**
* 更新全局代理配置
*/
export function useUpdateGlobalProxyConfig() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: (config: GlobalProxyConfig) =>
proxyApi.updateGlobalProxyConfig(config),
onSuccess: () => {
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
queryClient.invalidateQueries({ queryKey: proxyKeys.globalConfig });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
},
onError: (error: Error) => {
toast.error(
t("proxy.settings.toast.saveFailed", { error: error.message }),
);
},
});
}
/**
* 获取指定应用的代理配置
*/
export function useAppProxyConfig(appType: string) {
return useQuery({
queryKey: proxyKeys.appConfig(appType),
queryFn: () => proxyApi.getProxyConfigForApp(appType),
enabled: !!appType,
});
}
/**
* 更新指定应用的代理配置
*/
export function useUpdateAppProxyConfig() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: (config: AppProxyConfig) =>
proxyApi.updateProxyConfigForApp(config),
onSuccess: (_, variables) => {
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
queryClient.invalidateQueries({
queryKey: proxyKeys.appConfig(variables.appType),
});
queryClient.invalidateQueries({
queryKey: ["autoFailoverEnabled", variables.appType],
});
queryClient.invalidateQueries({ queryKey: ["circuitBreakerConfig"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
},
onError: (error: Error) => {
toast.error(
t("proxy.settings.toast.saveFailed", { error: error.message }),
);
},
});
}