mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-24 14:50:20 +08:00
de49f6fbbe
* fix(copilot): 修复 GitHub Copilot 400 认证错误 问题:使用 GitHub Copilot provider 时报错 400 bad request 根因:与 copilot-api 项目对比发现多处差异 修复内容: - 更新版本号 0.26.7 到 0.38.2 - 更新 API 版本 2025-04-01 到 2025-10-01 - 添加缺失的关键 headers - 修正 openai-intent 值 - 添加动态 API endpoint 支持 - 同步更新 stream_check.rs headers Closes #1777 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: flush stream after write_all in hyper_client proxy Add explicit flush() calls after write_all() for TLS stream, plain TCP stream, and CONNECT tunnel requests to ensure buffered data is sent immediately, preventing connection hangs in Copilot auth header flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 修复登录时的剪切板在mac与linux端可能没复制验证码 * fix: flush stream after write_all in hyper_client proxy Add explicit flush() calls after write_all() for TLS stream, plain TCP stream, and CONNECT tunnel requests to ensure buffered data is sent immediately, preventing connection hangs in Copilot auth header flow. * 修复登录时的剪切板在mac与linux端可能没复制验证码 * 1、修复不同类型的个人商业等不同类型的copilot账号问题 2、将验证码复制改为异步操作 * fix: address PR review comments for Copilot auth │ │ │ │ - Fix clipboard blocking by using spawn_blocking for arboard ops │ │ - Implement dynamic endpoint routing for enterprise Copilot users │ │ - Add api_endpoints cache cleanup in remove_account() and clear_auth() │ │ - Change API endpoint log level from info to debug │ │ - Fix clear_auth() to continue cleanup even if file deletion fails │ │ - Add 9 unit tests for Copilot detection and api_endpoints cachin * style: fix cargo fmt formatting * Fix Copilot dynamic endpoint handling * fix: restore clear_auth() memory-first cleanup order and fix cache leaks - Restore clear_auth() to clean memory state before deleting the storage file. The previous order (file deletion first) caused a regression where users could get stuck in a "cannot log out" state if file removal failed. - Add missing copilot_models.clear() in clear_auth() — this cache was cleaned in remove_account() but never in the full clear path. - Add endpoint_locks cleanup in both remove_account() and clear_auth() to prevent minor in-process memory leaks. - Update test to assert the correct behavior: memory should be cleaned even when file deletion fails. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com>
235 lines
6.9 KiB
TypeScript
235 lines
6.9 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { authApi, settingsApi } from "@/lib/api";
|
|
import { copyText } from "@/lib/clipboard";
|
|
import type {
|
|
ManagedAuthProvider,
|
|
ManagedAuthStatus,
|
|
ManagedAuthDeviceCodeResponse,
|
|
} from "@/lib/api";
|
|
|
|
type PollingState = "idle" | "polling" | "success" | "error";
|
|
|
|
export function useManagedAuth(authProvider: ManagedAuthProvider) {
|
|
const queryClient = useQueryClient();
|
|
const queryKey = ["managed-auth-status", authProvider];
|
|
|
|
const [pollingState, setPollingState] = useState<PollingState>("idle");
|
|
const [deviceCode, setDeviceCode] =
|
|
useState<ManagedAuthDeviceCodeResponse | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
|
|
null,
|
|
);
|
|
const pollingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const {
|
|
data: authStatus,
|
|
isLoading: isLoadingStatus,
|
|
refetch: refetchStatus,
|
|
} = useQuery<ManagedAuthStatus>({
|
|
queryKey,
|
|
queryFn: () => authApi.authGetStatus(authProvider),
|
|
staleTime: 30000,
|
|
});
|
|
|
|
const stopPolling = useCallback(() => {
|
|
if (pollingIntervalRef.current) {
|
|
clearInterval(pollingIntervalRef.current);
|
|
pollingIntervalRef.current = null;
|
|
}
|
|
if (pollingTimeoutRef.current) {
|
|
clearTimeout(pollingTimeoutRef.current);
|
|
pollingTimeoutRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
stopPolling();
|
|
};
|
|
}, [stopPolling]);
|
|
|
|
const startLoginMutation = useMutation({
|
|
mutationFn: () => authApi.authStartLogin(authProvider),
|
|
onSuccess: async (response) => {
|
|
setDeviceCode(response);
|
|
setPollingState("polling");
|
|
setError(null);
|
|
|
|
try {
|
|
await copyText(response.user_code);
|
|
} catch (e) {
|
|
console.debug("[ManagedAuth] Failed to copy user code:", e);
|
|
}
|
|
|
|
try {
|
|
await settingsApi.openExternal(response.verification_uri);
|
|
} catch (e) {
|
|
console.debug("[ManagedAuth] Failed to open browser:", e);
|
|
}
|
|
|
|
// Add a small buffer on top of GitHub's suggested interval to avoid
|
|
// hitting slow_down responses too aggressively during device polling.
|
|
const interval = Math.max((response.interval || 5) + 3, 8) * 1000;
|
|
const expiresAt = Date.now() + response.expires_in * 1000;
|
|
|
|
const pollOnce = async () => {
|
|
if (Date.now() > expiresAt) {
|
|
stopPolling();
|
|
setPollingState("error");
|
|
setError("Device code expired. Please try again.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const newAccount = await authApi.authPollForAccount(
|
|
authProvider,
|
|
response.device_code,
|
|
);
|
|
if (newAccount) {
|
|
stopPolling();
|
|
setPollingState("success");
|
|
await refetchStatus();
|
|
await queryClient.invalidateQueries({ queryKey });
|
|
setPollingState("idle");
|
|
setDeviceCode(null);
|
|
}
|
|
} catch (e) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
if (
|
|
!errorMessage.includes("pending") &&
|
|
!errorMessage.includes("slow_down")
|
|
) {
|
|
stopPolling();
|
|
setPollingState("error");
|
|
setError(errorMessage);
|
|
}
|
|
}
|
|
};
|
|
|
|
void pollOnce();
|
|
pollingIntervalRef.current = setInterval(pollOnce, interval);
|
|
pollingTimeoutRef.current = setTimeout(() => {
|
|
stopPolling();
|
|
setPollingState("error");
|
|
setError("Device code expired. Please try again.");
|
|
}, response.expires_in * 1000);
|
|
},
|
|
onError: (e) => {
|
|
setPollingState("error");
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
},
|
|
});
|
|
|
|
const logoutMutation = useMutation({
|
|
mutationFn: () => authApi.authLogout(authProvider),
|
|
onSuccess: async () => {
|
|
setPollingState("idle");
|
|
setDeviceCode(null);
|
|
setError(null);
|
|
queryClient.setQueryData(queryKey, {
|
|
provider: authProvider,
|
|
authenticated: false,
|
|
default_account_id: null,
|
|
accounts: [],
|
|
});
|
|
await queryClient.invalidateQueries({ queryKey });
|
|
},
|
|
onError: async (e) => {
|
|
console.error("[ManagedAuth] Failed to logout:", e);
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
await refetchStatus();
|
|
},
|
|
});
|
|
|
|
const removeAccountMutation = useMutation({
|
|
mutationFn: (accountId: string) =>
|
|
authApi.authRemoveAccount(authProvider, accountId),
|
|
onSuccess: async () => {
|
|
setPollingState("idle");
|
|
setDeviceCode(null);
|
|
setError(null);
|
|
await refetchStatus();
|
|
await queryClient.invalidateQueries({ queryKey });
|
|
},
|
|
onError: (e) => {
|
|
console.error("[ManagedAuth] Failed to remove account:", e);
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
},
|
|
});
|
|
|
|
const setDefaultAccountMutation = useMutation({
|
|
mutationFn: (accountId: string) =>
|
|
authApi.authSetDefaultAccount(authProvider, accountId),
|
|
onSuccess: async () => {
|
|
await refetchStatus();
|
|
await queryClient.invalidateQueries({ queryKey });
|
|
},
|
|
onError: (e) => {
|
|
console.error("[ManagedAuth] Failed to set default account:", e);
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
},
|
|
});
|
|
|
|
const startAuth = useCallback(() => {
|
|
setPollingState("idle");
|
|
setDeviceCode(null);
|
|
setError(null);
|
|
stopPolling();
|
|
startLoginMutation.mutate();
|
|
}, [startLoginMutation, stopPolling]);
|
|
|
|
const cancelAuth = useCallback(() => {
|
|
stopPolling();
|
|
setPollingState("idle");
|
|
setDeviceCode(null);
|
|
setError(null);
|
|
}, [stopPolling]);
|
|
|
|
const logout = useCallback(() => {
|
|
logoutMutation.mutate();
|
|
}, [logoutMutation]);
|
|
|
|
const removeAccount = useCallback(
|
|
(accountId: string) => {
|
|
removeAccountMutation.mutate(accountId);
|
|
},
|
|
[removeAccountMutation],
|
|
);
|
|
|
|
const setDefaultAccount = useCallback(
|
|
(accountId: string) => {
|
|
setDefaultAccountMutation.mutate(accountId);
|
|
},
|
|
[setDefaultAccountMutation],
|
|
);
|
|
|
|
const accounts = authStatus?.accounts ?? [];
|
|
|
|
return {
|
|
authStatus,
|
|
isLoadingStatus,
|
|
accounts,
|
|
hasAnyAccount: accounts.length > 0,
|
|
isAuthenticated: authStatus?.authenticated ?? false,
|
|
defaultAccountId: authStatus?.default_account_id ?? null,
|
|
migrationError: authStatus?.migration_error ?? null,
|
|
pollingState,
|
|
deviceCode,
|
|
error,
|
|
isPolling: pollingState === "polling",
|
|
isAddingAccount: startLoginMutation.isPending || pollingState === "polling",
|
|
isRemovingAccount: removeAccountMutation.isPending,
|
|
isSettingDefaultAccount: setDefaultAccountMutation.isPending,
|
|
startAuth,
|
|
addAccount: startAuth,
|
|
cancelAuth,
|
|
logout,
|
|
removeAccount,
|
|
setDefaultAccount,
|
|
refetchStatus,
|
|
};
|
|
}
|