支持Codex CLI的官网订阅设置

This commit is contained in:
xintaofei
2026-03-15 18:17:46 +08:00
parent 9688248200
commit ca7f6ecd25
11 changed files with 248 additions and 81 deletions

View File

@@ -92,6 +92,7 @@ interface AgentDraft {
googleCloudProject: string
googleCloudLocation: string
googleApplicationCredentials: string
codexAuthMode: CodexAuthMode
codexModelProvider: string
codexProviderOptions: string[]
codexReasoningEffort: CodexReasoningEffort
@@ -970,6 +971,10 @@ interface CodexImportantValues {
}
const CODEX_DEFAULT_MODEL_PROVIDER = "codeg"
const CODEX_AUTH_MODES = ["api_key", "chatgpt_subscription"] as const
type CodexAuthMode = (typeof CODEX_AUTH_MODES)[number]
type CodexReasoningEffort = "low" | "medium" | "high" | "xhigh"
const CODEX_REASONING_EFFORT_OPTIONS: ReadonlyArray<{
@@ -1283,6 +1288,21 @@ function parseCodexAuthJsonText(authJsonText: string): string | null {
return parseCodexAuthJsonObject(authJsonText).error
}
function inferCodexAuthMode(authJsonText: string): CodexAuthMode {
const { authObject } = parseCodexAuthJsonObject(authJsonText)
if (authObject) {
// 官网订阅auth_mode 为 chatgpt或没有 OPENAI_API_KEY或值为 null
if (
authObject.auth_mode === "chatgpt" ||
!("OPENAI_API_KEY" in authObject) ||
authObject.OPENAI_API_KEY === null
) {
return "chatgpt_subscription"
}
}
return "api_key"
}
function extractCodexImportantValues(
authJsonText: string,
configTomlText: string
@@ -1982,6 +2002,10 @@ function buildAgentDraft(agent: AcpAgentInfo): AgentDraft {
googleCloudProject: geminiImportant.googleCloudProject,
googleCloudLocation: geminiImportant.googleCloudLocation,
googleApplicationCredentials: geminiImportant.googleApplicationCredentials,
codexAuthMode:
agent.agent_type === "codex"
? inferCodexAuthMode(codexAuthJsonText)
: "api_key",
codexModelProvider: codexImportant.modelProvider,
codexProviderOptions: codexImportant.providerOptions,
codexReasoningEffort: codexImportant.reasoningEffort,
@@ -3956,6 +3980,7 @@ export function AcpAgentSettings() {
)
updateSelectedDraft((current) => ({
...current,
codexAuthMode: inferCodexAuthMode(nextText),
codexAuthJsonText: nextText,
apiBaseUrl: important.apiBaseUrl,
apiKey: important.apiKey ?? current.apiKey,
@@ -4027,6 +4052,58 @@ export function AcpAgentSettings() {
[selectedAgent, selectedDraft, updateSelectedDraft]
)
const handleCodexAuthModeChange = useCallback(
(nextMode: CodexAuthMode) => {
if (
!selectedAgent ||
!selectedDraft ||
selectedAgent.agent_type !== "codex"
)
return
const nextAuthJsonText =
nextMode === "chatgpt_subscription"
? "{}"
: JSON.stringify({ OPENAI_API_KEY: "" }, null, 2)
const nextConfigTomlText =
nextMode === "chatgpt_subscription"
? ""
: selectedDraft.codexConfigTomlText
const nextEnvText =
nextMode === "chatgpt_subscription"
? patchEnvText(selectedDraft.envText, {
OPENAI_API_KEY: "",
OPENAI_BASE_URL: "",
})
: selectedDraft.envText
const synced = extractCodexImportantValues(
nextAuthJsonText,
nextConfigTomlText
)
updateSelectedDraft((current) => ({
...current,
codexAuthMode: nextMode,
codexAuthJsonText: nextAuthJsonText,
codexConfigTomlText: nextConfigTomlText,
envText: nextEnvText,
apiBaseUrl:
nextMode === "chatgpt_subscription" ? "" : synced.apiBaseUrl,
apiKey:
nextMode === "chatgpt_subscription" ? "" : (synced.apiKey ?? ""),
model: synced.model,
codexModelProvider: synced.modelProvider,
codexProviderOptions: synced.providerOptions,
codexReasoningEffort: synced.reasoningEffort,
codexSupportsWebsockets: synced.supportsWebsockets,
}))
},
[selectedAgent, selectedDraft, updateSelectedDraft]
)
const handleCodexImportantConfigChange = useCallback(
(
key: "apiBaseUrl" | "apiKey" | "model" | "reasoningEffort",
@@ -4484,106 +4561,148 @@ export function AcpAgentSettings() {
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
Provider
{t("codex.authMode")}
</label>
<Select
value={selectedDraft.codexModelProvider}
onValueChange={handleCodexModelProviderChange}
value={selectedDraft.codexAuthMode}
onValueChange={(value) => {
if (
CODEX_AUTH_MODES.includes(value as CodexAuthMode)
) {
handleCodexAuthModeChange(value as CodexAuthMode)
}
}}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t("codex.selectProvider")}
/>
<SelectValue />
</SelectTrigger>
<SelectContent align="start">
{selectedDraft.codexProviderOptions.map(
(provider) => (
<SelectItem key={provider} value={provider}>
{provider}
</SelectItem>
)
)}
{CODEX_AUTH_MODES.map((mode) => (
<SelectItem key={mode} value={mode}>
{mode === "chatgpt_subscription"
? t("codex.chatgptSubscription")
: "API Key"}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[11px] text-muted-foreground">
{selectedDraft.codexAuthMode === "chatgpt_subscription"
? t("codex.chatgptSubscriptionHint")
: t("codex.apiKeyHint")}
</p>
</div>
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
API URL
</label>
<Input
value={selectedDraft.apiBaseUrl}
onChange={(event) => {
handleCodexImportantConfigChange(
"apiBaseUrl",
event.target.value
)
}}
placeholder="https://api.openai.com/v1"
/>
</div>
{selectedDraft.codexAuthMode !== "chatgpt_subscription" && (
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
Provider
</label>
<Select
value={selectedDraft.codexModelProvider}
onValueChange={handleCodexModelProviderChange}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t("codex.selectProvider")}
/>
</SelectTrigger>
<SelectContent align="start">
{selectedDraft.codexProviderOptions.map(
(provider) => (
<SelectItem key={provider} value={provider}>
{provider}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
API Key
</label>
<div className="flex items-center gap-2">
{selectedDraft.codexAuthMode !== "chatgpt_subscription" && (
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
API URL
</label>
<Input
type={
showApiKeys[selectedAgent.agent_type]
? "text"
: "password"
}
value={selectedDraft.apiKey}
value={selectedDraft.apiBaseUrl}
onChange={(event) => {
handleCodexImportantConfigChange(
"apiKey",
"apiBaseUrl",
event.target.value
)
}}
placeholder="sk-..."
placeholder="https://api.openai.com/v1"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setShowApiKeys((prev) => ({
...prev,
[selectedAgent.agent_type]:
!prev[selectedAgent.agent_type],
}))
}}
title={
showApiKeys[selectedAgent.agent_type]
? t("actions.hideApiKey")
: t("actions.showApiKey")
}
>
{showApiKeys[selectedAgent.agent_type] ? (
<EyeOff className="h-3.5 w-3.5" />
) : (
<Eye className="h-3.5 w-3.5" />
)}
</Button>
</div>
</div>
)}
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
{t("codex.modelName")}
</label>
<Input
value={selectedDraft.model}
onChange={(event) => {
handleCodexImportantConfigChange(
"model",
event.target.value
)
}}
placeholder="gpt-5 / gpt-5-mini"
/>
</div>
{selectedDraft.codexAuthMode !== "chatgpt_subscription" && (
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
API Key
</label>
<div className="flex items-center gap-2">
<Input
type={
showApiKeys[selectedAgent.agent_type]
? "text"
: "password"
}
value={selectedDraft.apiKey}
onChange={(event) => {
handleCodexImportantConfigChange(
"apiKey",
event.target.value
)
}}
placeholder="sk-..."
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setShowApiKeys((prev) => ({
...prev,
[selectedAgent.agent_type]:
!prev[selectedAgent.agent_type],
}))
}}
title={
showApiKeys[selectedAgent.agent_type]
? t("actions.hideApiKey")
: t("actions.showApiKey")
}
>
{showApiKeys[selectedAgent.agent_type] ? (
<EyeOff className="h-3.5 w-3.5" />
) : (
<Eye className="h-3.5 w-3.5" />
)}
</Button>
</div>
</div>
)}
{selectedDraft.codexAuthMode !== "chatgpt_subscription" && (
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
{t("codex.modelName")}
</label>
<Input
value={selectedDraft.model}
onChange={(event) => {
handleCodexImportantConfigChange(
"model",
event.target.value
)
}}
placeholder="gpt-5 / gpt-5-mini"
/>
</div>
)}
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
@@ -4679,10 +4798,18 @@ supports_websockets = true`}
<Button
size="sm"
onClick={() => {
const codexEnvText =
selectedDraft.codexAuthMode ===
"chatgpt_subscription"
? patchEnvText(selectedDraft.envText, {
OPENAI_API_KEY: "",
OPENAI_BASE_URL: "",
})
: selectedDraft.envText
persistPreferences(
selectedAgent.agent_type,
selectedDraft.enabled,
selectedDraft.envText,
codexEnvText,
selectedDraft.configText,
{
codexAuthJsonText:

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "يدعم الإعداد السريع لعنوان API ومفتاح API واسم النموذج وجهد الاستدلال، مع المزامنة مع `auth.json` / `config.toml`.",
"authMode": "طريقة المصادقة",
"chatgptSubscription": "اشتراك رسمي",
"chatgptSubscriptionHint": "تسجيل الدخول باشتراك ChatGPT الرسمي، لا حاجة لـ API Key",
"apiKeyHint": "الاتصال باستخدام API Key بخدمات OpenAI أو الخدمات المتوافقة",
"selectProvider": "اختر المزود",
"modelName": "اسم النموذج",
"selectReasoningEffort": "اختر Reasoning Effort",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "Unterstützt schnelle Konfiguration von API-URL, API-Key, Modellname und reasoning effort und synchronisiert mit `auth.json` / `config.toml`.",
"authMode": "Authentifizierungsmodus",
"chatgptSubscription": "Offizielles Abonnement",
"chatgptSubscriptionHint": "Mit offiziellem ChatGPT-Abonnement anmelden, kein API Key erforderlich",
"apiKeyHint": "Mit API Key zu OpenAI oder kompatiblen API-Diensten verbinden",
"selectProvider": "Provider auswählen",
"modelName": "Modellname",
"selectReasoningEffort": "Reasoning Effort auswählen",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "Supports quick configuration for API URL, API Key, model name and reasoning effort, and syncs with `auth.json` / `config.toml`.",
"authMode": "Auth Mode",
"chatgptSubscription": "Official Subscription",
"chatgptSubscriptionHint": "Log in with ChatGPT official subscription, no API Key required",
"apiKeyHint": "Connect using API Key to OpenAI or compatible API services",
"selectProvider": "Select Provider",
"modelName": "Model Name",
"selectReasoningEffort": "Select Reasoning Effort",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "Admite configuración rápida de URL de API, API Key, nombre del modelo y reasoning effort, y sincroniza con `auth.json` / `config.toml`.",
"authMode": "Modo de Autenticación",
"chatgptSubscription": "Suscripción oficial",
"chatgptSubscriptionHint": "Iniciar sesión con suscripción oficial de ChatGPT, sin necesidad de API Key",
"apiKeyHint": "Conectar usando API Key a OpenAI o servicios API compatibles",
"selectProvider": "Seleccionar proveedor",
"modelName": "Nombre del modelo",
"selectReasoningEffort": "Seleccionar Reasoning Effort",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "Prend en charge la configuration rapide de lURL API, de la clé API, du nom du modèle et du reasoning effort, avec synchronisation vers `auth.json` / `config.toml`.",
"authMode": "Mode dauthentification",
"chatgptSubscription": "Abonnement officiel",
"chatgptSubscriptionHint": "Connectez-vous avec labonnement officiel ChatGPT, pas besoin dAPI Key",
"apiKeyHint": "Connectez-vous avec une API Key à OpenAI ou aux services API compatibles",
"selectProvider": "Sélectionner un provider",
"modelName": "Nom du modèle",
"selectReasoningEffort": "Sélectionner Reasoning Effort",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "API URL、API Key、モデル名、reasoning effort を素早く設定でき、`auth.json` / `config.toml` と同期します。",
"authMode": "認証方式",
"chatgptSubscription": "公式サブスクリプション",
"chatgptSubscriptionHint": "ChatGPT 公式サブスクリプションでログイン、API Key 不要",
"apiKeyHint": "API Key で OpenAI または互換 API サービスに接続",
"selectProvider": "プロバイダーを選択",
"modelName": "モデル名",
"selectReasoningEffort": "Reasoning Effort を選択",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "API URL, API Key, 모델 이름, reasoning effort를 빠르게 설정하고 `auth.json` / `config.toml`과 동기화합니다.",
"authMode": "인증 방식",
"chatgptSubscription": "공식 구독",
"chatgptSubscriptionHint": "ChatGPT 공식 구독으로 로그인, API Key 불필요",
"apiKeyHint": "API Key로 OpenAI 또는 호환 API 서비스에 연결",
"selectProvider": "Provider 선택",
"modelName": "모델 이름",
"selectReasoningEffort": "Reasoning Effort 선택",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "Suporta configuração rápida de URL da API, API Key, nome do modelo e reasoning effort, com sincronização para `auth.json` / `config.toml`.",
"authMode": "Modo de Autenticação",
"chatgptSubscription": "Assinatura oficial",
"chatgptSubscriptionHint": "Faça login com assinatura oficial do ChatGPT, sem necessidade de API Key",
"apiKeyHint": "Conecte-se usando API Key ao OpenAI ou serviços de API compatíveis",
"selectProvider": "Selecionar provedor",
"modelName": "Nome do modelo",
"selectReasoningEffort": "Selecionar Reasoning Effort",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "支持 API URL、API Key、模型名称、Reasoning Effort 快捷配置,并与 `auth.json` / `config.toml` 双向联动。",
"authMode": "认证方式",
"chatgptSubscription": "官网订阅",
"chatgptSubscriptionHint": "使用 ChatGPT 官网订阅登录,无需配置 API Key",
"apiKeyHint": "使用 API Key 连接 OpenAI 或兼容的 API 服务",
"selectProvider": "选择 Provider",
"modelName": "模型名称",
"selectReasoningEffort": "选择 Reasoning Effort",

View File

@@ -406,6 +406,10 @@
},
"codex": {
"configDescription": "支援 API URL、API Key、模型名稱、Reasoning Effort 快捷配置,並與 `auth.json` / `config.toml` 雙向聯動。",
"authMode": "認證方式",
"chatgptSubscription": "官網訂閱",
"chatgptSubscriptionHint": "使用 ChatGPT 官網訂閱登入,無需配置 API Key",
"apiKeyHint": "使用 API Key 連接 OpenAI 或相容的 API 服務",
"selectProvider": "選擇 Provider",
"modelName": "模型名稱",
"selectReasoningEffort": "選擇 Reasoning Effort",