Add managed ChatGPT account binding for Codex

This commit is contained in:
saladday
2026-06-08 04:39:53 +08:00
parent 5c36ae066b
commit 7480cec0ba
11 changed files with 1080 additions and 102 deletions
@@ -19,6 +19,7 @@ import {
Trash2,
} from "lucide-react";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { CodexOAuthSection } from "./CodexOAuthSection";
import { ApiKeySection, EndpointField, ModelDropdown } from "./shared";
import {
fetchModelsForConfig,
@@ -46,6 +47,10 @@ interface CodexFormFieldsProps {
websiteUrl: string;
isPartner?: boolean;
partnerPromotionKey?: string;
isCodexOauthPreset?: boolean;
selectedCodexAccountId?: string | null;
onCodexAccountSelect?: (accountId: string | null) => void;
codexOauthNoneOptionLabel?: string;
// Base URL
shouldShowSpeedTest: boolean;
@@ -111,6 +116,10 @@ export function CodexFormFields({
websiteUrl,
isPartner,
partnerPromotionKey,
isCodexOauthPreset = false,
selectedCodexAccountId,
onCodexAccountSelect,
codexOauthNoneOptionLabel,
shouldShowSpeedTest,
codexBaseUrl,
onBaseUrlChange,
@@ -284,26 +293,37 @@ export function CodexFormFields({
return (
<>
{/* Codex OAuth 账号选择 */}
{isCodexOauthPreset && (
<CodexOAuthSection
selectedAccountId={selectedCodexAccountId}
onAccountSelect={onCodexAccountSelect}
noneOptionLabel={codexOauthNoneOptionLabel}
/>
)}
{/* Codex API Key 输入框 */}
<ApiKeySection
id="codexApiKey"
label="API Key"
value={codexApiKey}
onChange={onApiKeyChange}
category={category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
placeholder={{
official: t("providerForm.codexOfficialNoApiKey", {
defaultValue: "官方供应商无需 API Key",
}),
thirdParty: t("providerForm.codexApiKeyAutoFill", {
defaultValue: "输入 API Key,将自动填充到配置",
}),
}}
/>
{!isCodexOauthPreset && (
<ApiKeySection
id="codexApiKey"
label="API Key"
value={codexApiKey}
onChange={onApiKeyChange}
category={category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
placeholder={{
official: t("providerForm.codexOfficialNoApiKey", {
defaultValue: "官方供应商无需 API Key",
}),
thirdParty: t("providerForm.codexApiKeyAutoFill", {
defaultValue: "输入 API Key,将自动填充到配置",
}),
}}
/>
)}
{/* Codex Base URL 输入框 */}
{shouldShowSpeedTest && (
@@ -31,6 +31,8 @@ interface CodexOAuthSectionProps {
selectedAccountId?: string | null;
/** 账号选择回调 */
onAccountSelect?: (accountId: string | null) => void;
/** 空选择项文案;默认表示使用托管认证的默认账号 */
noneOptionLabel?: string;
/** 是否开启 Codex FAST mode */
fastModeEnabled?: boolean;
/** FAST mode 切换回调 */
@@ -47,6 +49,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
className,
selectedAccountId,
onAccountSelect,
noneOptionLabel,
fastModeEnabled = false,
onFastModeChange,
}) => {
@@ -111,7 +114,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
</div>
{/* 账号选择器 */}
{hasAnyAccount && onAccountSelect && (
{onAccountSelect && (hasAnyAccount || noneOptionLabel) && (
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">
{t("codexOauth.selectAccount", "选择账号")}
@@ -131,7 +134,8 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
<SelectContent>
<SelectItem value="none">
<span className="text-muted-foreground">
{t("codexOauth.useDefaultAccount", "使用默认账号")}
{noneOptionLabel ??
t("codexOauth.useDefaultAccount", "使用默认账号")}
</span>
</SelectItem>
{accounts.map((account) => (
+93 -40
View File
@@ -126,6 +126,16 @@ type PresetEntry = {
| HermesProviderPreset;
};
function getPresetProviderType(
preset: PresetEntry["preset"] | null | undefined,
): "github_copilot" | "codex_oauth" | undefined {
if (!preset || !("providerType" in preset)) return undefined;
return preset.providerType === "github_copilot" ||
preset.providerType === "codex_oauth"
? preset.providerType
: undefined;
}
const codexApiFormatFromWireApi = (
wireApi: string | undefined,
): CodexApiFormat | undefined => {
@@ -354,6 +364,13 @@ function ProviderFormFull({
initialData?.meta?.pricingModelSource,
),
});
setSelectedGitHubAccountId(
resolveManagedAccountId(initialData?.meta, "github_copilot"),
);
setSelectedCodexAccountId(
resolveManagedAccountId(initialData?.meta, "codex_oauth"),
);
setCodexFastMode(initialData?.meta?.codexFastMode ?? false);
setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {});
}, [appId, initialData, supportsFullUrl]);
@@ -635,6 +652,41 @@ function ProviderFormFull({
}));
}, [appId]);
const selectedPresetEntry = useMemo(
() =>
selectedPresetId && selectedPresetId !== "custom"
? (presetEntries.find((entry) => entry.id === selectedPresetId) ??
null)
: null,
[presetEntries, selectedPresetId],
);
const selectedPresetProviderType = getPresetProviderType(
selectedPresetEntry?.preset,
);
const initialProviderType = initialData?.meta?.providerType;
const isCopilotProvider =
appId === "claude" &&
(selectedPresetProviderType === "github_copilot" ||
initialProviderType === "github_copilot" ||
baseUrl.includes("githubcopilot.com"));
const isClaudeCodexOauthProvider =
appId === "claude" &&
(selectedPresetProviderType === "codex_oauth" ||
initialProviderType === "codex_oauth");
const isCodexOfficialProvider =
appId === "codex" &&
(category === "official" ||
(selectedPresetProviderType === "codex_oauth" &&
selectedPresetEntry?.preset.category === "official"));
const isCodexOfficialManagedOauthBound =
isCodexOfficialProvider && Boolean(selectedCodexAccountId);
const wasCodexOfficialManagedOauthBound =
appId === "codex" &&
initialData?.category === "official" &&
Boolean(resolveManagedAccountId(initialData?.meta, "codex_oauth"));
const requiresCodexOauthLogin =
isClaudeCodexOauthProvider || isCodexOfficialManagedOauthBound;
const {
templateValues,
templateValueEntries,
@@ -1051,13 +1103,6 @@ function ProviderFormFull({
}
// OAuth 未登录:B 类(token 根本不存在,保存了也没法建立)
const isCopilotProvider =
templatePreset?.providerType === "github_copilot" ||
initialData?.meta?.providerType === "github_copilot" ||
baseUrl.includes("githubcopilot.com");
const isCodexOauthProvider =
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth";
if (isCopilotProvider && !isCopilotAuthenticated) {
toast.error(
t("copilot.loginRequired", {
@@ -1066,7 +1111,7 @@ function ProviderFormFull({
);
return;
}
if (isCodexOauthProvider && !isCodexOauthAuthenticated) {
if (requiresCodexOauthLogin && !isCodexOauthAuthenticated) {
toast.error(
t("codexOauth.loginRequired", {
defaultValue: "请先登录 ChatGPT 账号",
@@ -1110,14 +1155,14 @@ function ProviderFormFull({
// cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验
if (category !== "official" && category !== "cloud_provider") {
if (appId === "claude") {
if (!isCodexOauthProvider && !baseUrl.trim()) {
if (!isClaudeCodexOauthProvider && !baseUrl.trim()) {
issues.push(
t("providerForm.endpointRequired", {
defaultValue: "非官方供应商请填写 API 端点",
}),
);
}
if (!isCopilotProvider && !isCodexOauthProvider && !apiKey.trim()) {
if (!isCopilotProvider && !isClaudeCodexOauthProvider && !apiKey.trim()) {
issues.push(
t("providerForm.apiKeyRequired", {
defaultValue: "非官方供应商请填写 API Key",
@@ -1168,20 +1213,17 @@ function ProviderFormFull({
};
const performSubmit = async (values: ProviderFormData) => {
// OAuth / 其它身份识别(与 handleSubmit 保持一致)
const isCopilotProvider =
templatePreset?.providerType === "github_copilot" ||
initialData?.meta?.providerType === "github_copilot" ||
baseUrl.includes("githubcopilot.com");
const isCodexOauthProvider =
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth";
let settingsConfig: string;
if (appId === "codex") {
try {
const authJson = JSON.parse(codexAuth);
const shouldStripManagedCodexAuth =
category === "official" &&
(isCodexOfficialManagedOauthBound ||
wasCodexOfficialManagedOauthBound);
const authJson = shouldStripManagedCodexAuth
? {}
: JSON.parse(codexAuth);
let normalizedCodexConfig =
category !== "official" && (codexConfig ?? "").trim()
? setCodexWireApi(codexConfig ?? "", "responses")
@@ -1343,9 +1385,11 @@ function ProviderFormFull({
const baseMeta: ProviderMeta | undefined =
payload.meta ?? (initialData?.meta ? { ...initialData.meta } : undefined);
// 确定 providerType(新建时从预设获取,编辑时从现有数据获取)
const providerType =
templatePreset?.providerType || initialData?.meta?.providerType;
const providerType = isCopilotProvider
? "github_copilot"
: isClaudeCodexOauthProvider || isCodexOfficialManagedOauthBound
? "codex_oauth"
: undefined;
const nextMeta: ProviderMeta = {
...(baseMeta ?? {}),
@@ -1367,19 +1411,25 @@ function ProviderFormFull({
authProvider: "github_copilot",
accountId: selectedGitHubAccountId ?? undefined,
}
: isCodexOauthProvider
: isClaudeCodexOauthProvider
? {
source: "managed_account",
authProvider: "codex_oauth",
accountId: selectedCodexAccountId ?? undefined,
}
: isCodexOfficialManagedOauthBound
? {
source: "managed_account",
authProvider: "codex_oauth",
accountId: selectedCodexAccountId ?? undefined,
}
: undefined,
// GitHub Copilot 多账号:保存关联的账号 ID
githubAccountId:
isCopilotProvider && selectedGitHubAccountId
? selectedGitHubAccountId
: undefined,
codexFastMode: isCodexOauthProvider ? codexFastMode : undefined,
codexFastMode: isClaudeCodexOauthProvider ? codexFastMode : undefined,
codexChatReasoning:
appId === "codex" &&
category !== "official" &&
@@ -1412,9 +1462,18 @@ function ProviderFormFull({
: undefined,
};
if (!isCodexOauthProvider && "codexFastMode" in nextMeta) {
if (!isClaudeCodexOauthProvider && "codexFastMode" in nextMeta) {
delete nextMeta.codexFastMode;
}
if (!providerType && "providerType" in nextMeta) {
delete nextMeta.providerType;
}
if (!nextMeta.authBinding && "authBinding" in nextMeta) {
delete nextMeta.authBinding;
}
if (!nextMeta.githubAccountId && "githubAccountId" in nextMeta) {
delete nextMeta.githubAccountId;
}
payload.meta = nextMeta;
@@ -1953,22 +2012,12 @@ function ProviderFormFull({
websiteUrl={claudeWebsiteUrl}
isPartner={isClaudePartner}
partnerPromotionKey={claudePartnerPromotionKey}
isCopilotPreset={
templatePreset?.providerType === "github_copilot" ||
initialData?.meta?.providerType === "github_copilot" ||
baseUrl.includes("githubcopilot.com")
}
isCodexOauthPreset={
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth"
}
isCopilotPreset={isCopilotProvider}
isCodexOauthPreset={isClaudeCodexOauthProvider}
usesOAuth={
templatePreset?.requiresOAuth === true ||
templatePreset?.providerType === "github_copilot" ||
initialData?.meta?.providerType === "github_copilot" ||
baseUrl.includes("githubcopilot.com") ||
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth"
isCopilotProvider ||
isClaudeCodexOauthProvider
}
isCopilotAuthenticated={isCopilotAuthenticated}
selectedGitHubAccountId={selectedGitHubAccountId}
@@ -2022,6 +2071,10 @@ function ProviderFormFull({
websiteUrl={codexWebsiteUrl}
isPartner={isCodexPartner}
partnerPromotionKey={codexPartnerPromotionKey}
isCodexOauthPreset={isCodexOfficialProvider}
selectedCodexAccountId={selectedCodexAccountId}
onCodexAccountSelect={setSelectedCodexAccountId}
codexOauthNoneOptionLabel="暂不绑定托管账号"
shouldShowSpeedTest={shouldShowSpeedTest}
codexBaseUrl={codexBaseUrl}
onBaseUrlChange={handleCodexBaseUrlChange}