Merge remote-tracking branch 'origin/main'

This commit is contained in:
xintaofei
2026-04-16 09:40:31 +08:00
21 changed files with 695 additions and 73 deletions

View File

@@ -17,6 +17,7 @@ import {
CheckCircle2,
ChevronDown,
ChevronRight,
Copy,
Download,
Eye,
EyeOff,
@@ -75,6 +76,8 @@ import {
acpUninstallAgent,
acpUpdateAgentConfig,
acpUpdateAgentEnv,
codexPollDeviceCode,
codexRequestDeviceCode,
listModelProviders,
} from "@/lib/api"
import type {
@@ -1549,6 +1552,18 @@ function inferCodexAuthMode(authJsonText: string): CodexAuthMode {
return "api_key"
}
function hasCodexChatgptTokens(authJsonText: string): boolean {
const { authObject } = parseCodexAuthJsonObject(authJsonText)
if (!authObject) return false
const tokens = authObject.tokens as Record<string, unknown> | undefined
if (tokens && typeof tokens === "object") {
return (
typeof tokens.access_token === "string" && tokens.access_token.length > 0
)
}
return false
}
function extractCodexImportantValues(
authJsonText: string,
configTomlText: string
@@ -2685,6 +2700,17 @@ export function AcpAgentSettings() {
const installStream = useAgentInstallStream()
const [streamAgentType, setStreamAgentType] = useState<AgentType | null>(null)
const installLogEndRef = useRef<HTMLDivElement | null>(null)
const [codexDeviceCode, setCodexDeviceCode] = useState<{
userCode: string
verificationUrl: string
deviceAuthId: string
interval: number
} | null>(null)
const [codexLoginStatus, setCodexLoginStatus] = useState<
"idle" | "requesting" | "polling" | "success" | "error"
>("idle")
const [codexLoginError, setCodexLoginError] = useState<string | null>(null)
const codexPollCancelledRef = useRef(false)
const sortedAgents = useMemo(
() =>
@@ -3426,13 +3452,8 @@ export function AcpAgentSettings() {
const selectedMissingModelProvider =
selectedNeedsModelProvider && selectedDraft?.modelProviderId == null
const selectedCodexAuthJsonText = selectedDraft?.codexAuthJsonText ?? ""
const selectedConfigText = selectedDraft?.configText ?? ""
const selectedOpenCodeAuthJsonText = selectedDraft?.openCodeAuthJsonText ?? ""
const selectedCodexAuthError = useMemo(() => {
if (selectedAgentKind !== "codex" || !locale) return null
return parseCodexAuthJsonText(selectedCodexAuthJsonText)
}, [locale, selectedAgentKind, selectedCodexAuthJsonText])
const selectedCodexReasoningEffortOption =
selectedAgent?.agent_type === "codex" && selectedDraft
? (CODEX_REASONING_EFFORT_OPTIONS.find(
@@ -4593,31 +4614,6 @@ export function AcpAgentSettings() {
[handleOpenCodeConfigPatch]
)
const handleCodexAuthJsonTextChange = useCallback(
(nextText: string) => {
if (!selectedAgent || selectedAgent.agent_type !== "codex") return
const important = extractCodexImportantValues(
nextText,
selectedDraft?.codexConfigTomlText ?? ""
)
updateSelectedDraft((current) => ({
...current,
codexAuthMode: inferCodexAuthMode(nextText),
codexAuthJsonText: nextText,
apiBaseUrl: important.apiBaseUrl,
apiKey: important.apiKey ?? current.apiKey,
model: important.model,
codexModelProvider: important.modelProvider,
codexProviderOptions: important.providerOptions,
codexReasoningEffort: important.reasoningEffort,
codexSupportsWebsockets: important.supportsWebsockets,
codexSkills: important.skills,
codexServiceTierFast: important.serviceTierFast,
}))
},
[selectedAgent, selectedDraft, updateSelectedDraft]
)
const handleCodexConfigTomlTextChange = useCallback(
(nextText: string) => {
if (!selectedAgent || selectedAgent.agent_type !== "codex") return
@@ -4900,6 +4896,138 @@ export function AcpAgentSettings() {
[selectedAgent, selectedDraft, updateSelectedDraft]
)
const handleCodexDeviceLogin = useCallback(async () => {
setCodexLoginStatus("requesting")
setCodexLoginError(null)
setCodexDeviceCode(null)
codexPollCancelledRef.current = false
try {
const resp = await codexRequestDeviceCode()
setCodexDeviceCode(resp)
setCodexLoginStatus("polling")
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
setCodexLoginError(msg)
setCodexLoginStatus("error")
}
}, [])
const cancelCodexDeviceLogin = useCallback(() => {
codexPollCancelledRef.current = true
setCodexLoginStatus("idle")
setCodexDeviceCode(null)
setCodexLoginError(null)
}, [])
useEffect(() => {
if (codexLoginStatus !== "polling" || !codexDeviceCode) return
codexPollCancelledRef.current = false
const pollInterval = (codexDeviceCode.interval || 5) * 1000
const deadline = Date.now() + 15 * 60 * 1000
let timer: ReturnType<typeof setTimeout> | null = null
let active = true
const poll = async () => {
if (!active || codexPollCancelledRef.current) return
if (Date.now() > deadline) {
setCodexLoginError(t("codex.loginTimeout"))
setCodexLoginStatus("error")
setCodexDeviceCode(null)
return
}
try {
const result = await codexPollDeviceCode({
deviceAuthId: codexDeviceCode.deviceAuthId,
userCode: codexDeviceCode.userCode,
})
if (!active || codexPollCancelledRef.current) return
if (result.status === "success") {
setCodexLoginStatus("success")
setCodexDeviceCode(null)
const authJson = JSON.stringify(
{
auth_mode: "chatgpt",
OPENAI_API_KEY: null,
tokens: {
id_token: result.idToken,
access_token: result.accessToken,
refresh_token: result.refreshToken,
account_id: result.accountId ?? "",
},
last_refresh: new Date().toISOString(),
},
null,
2
)
updateSelectedDraft((current) => ({
...current,
codexAuthJsonText: authJson,
}))
const draft = drafts.codex
if (draft) {
const codexEnvText =
draft.codexAuthMode === "chatgpt_subscription"
? patchEnvText(draft.envText, {
OPENAI_API_KEY: "",
OPENAI_BASE_URL: "",
})
: draft.envText
try {
await Promise.all([
persistEnv(
"codex",
draft.enabled,
codexEnvText,
draft.modelProviderId
),
persistConfig("codex", draft.configText, {
codexAuthJsonText: authJson,
codexConfigTomlText: draft.codexConfigTomlText,
}),
])
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
toast.error(t("codex.loginSaveFailed"), {
description: msg,
})
}
}
return
}
if (result.status === "error") {
setCodexLoginError(result.message ?? "Unknown error")
setCodexLoginStatus("error")
setCodexDeviceCode(null)
return
}
timer = setTimeout(poll, pollInterval)
} catch {
if (!active || codexPollCancelledRef.current) return
timer = setTimeout(poll, pollInterval)
}
}
timer = setTimeout(poll, pollInterval)
return () => {
active = false
if (timer) clearTimeout(timer)
}
}, [
codexLoginStatus,
codexDeviceCode,
drafts.codex,
persistConfig,
persistEnv,
updateSelectedDraft,
t,
])
useEffect(() => {
if (selectedAgent?.agent_type !== "codex" && codexLoginStatus !== "idle") {
cancelCodexDeviceLogin()
}
}, [selectedAgent, codexLoginStatus, cancelCodexDeviceLogin])
if (loadingAgents) {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
@@ -5303,6 +5431,108 @@ export function AcpAgentSettings() {
</p>
</div>
{selectedDraft.codexAuthMode === "chatgpt_subscription" && (
<div className="space-y-2">
{hasCodexChatgptTokens(
selectedDraft.codexAuthJsonText
) &&
codexLoginStatus !== "polling" &&
codexLoginStatus !== "requesting" && (
<div className="flex items-center gap-1.5 text-xs text-green-600">
<CheckCircle2 className="h-3 w-3" />
{t("codex.loggedIn")}
</div>
)}
{codexLoginStatus === "idle" && (
<Button
onClick={handleCodexDeviceLogin}
size="sm"
variant="outline"
>
{hasCodexChatgptTokens(
selectedDraft.codexAuthJsonText
)
? t("codex.loginRelogin")
: t("codex.loginButton")}
</Button>
)}
{codexLoginStatus === "requesting" && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
{t("codex.loginRequesting")}
</div>
)}
{codexLoginStatus === "polling" && codexDeviceCode && (
<div className="space-y-2 rounded-md border p-3">
<p className="text-xs">{t("codex.loginStep1")}</p>
<button
type="button"
className="text-xs text-primary underline cursor-pointer"
onClick={() =>
openUrl(codexDeviceCode.verificationUrl)
}
>
{codexDeviceCode.verificationUrl}
</button>
<p className="text-xs mt-1">
{t("codex.loginStep2")}
</p>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-2 py-1 text-sm font-mono font-bold tracking-widest">
{codexDeviceCode.userCode}
</code>
<Button
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
onClick={() => {
navigator.clipboard.writeText(
codexDeviceCode.userCode
)
toast.success(t("codex.loginCodeCopied"))
}}
>
<Copy className="h-3 w-3" />
</Button>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<Loader2 className="h-3 w-3 animate-spin" />
{t("codex.loginPolling")}
</div>
<Button
size="sm"
variant="outline"
onClick={cancelCodexDeviceLogin}
>
{t("codex.loginCancel")}
</Button>
</div>
)}
{codexLoginStatus === "success" && (
<div className="flex items-center gap-1.5 text-xs text-green-600">
<CheckCircle2 className="h-3 w-3" />
{t("codex.loginSuccess")}
</div>
)}
{codexLoginStatus === "error" && (
<div className="space-y-1.5">
<p className="text-xs text-destructive">
{t("codex.loginFailed", {
message: codexLoginError ?? "Unknown error",
})}
</p>
<Button
onClick={handleCodexDeviceLogin}
size="sm"
variant="outline"
>
{t("codex.loginRetry")}
</Button>
</div>
)}
</div>
)}
{selectedDraft.codexAuthMode === "model_provider" && (
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
@@ -5497,27 +5727,6 @@ export function AcpAgentSettings() {
</div>
</div>
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
{t("codex.authJsonNative")}
</label>
<Textarea
value={selectedDraft.codexAuthJsonText}
onChange={(event) => {
handleCodexAuthJsonTextChange(event.target.value)
}}
placeholder={`{
"OPENAI_API_KEY": "sk-..."
}`}
className="min-h-28 max-h-60 font-mono text-xs"
/>
{selectedCodexAuthError && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-2.5 py-1.5 text-[11px] text-red-400">
{selectedCodexAuthError}
</div>
)}
</div>
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
{t("codex.configTomlNative")}

View File

@@ -557,7 +557,21 @@
"enableFast": "تفعيل Fast",
"enableFastAria": "تفعيل مستوى خدمة Fast لـ Codex",
"authJsonNative": "auth.json (أصلي)",
"configTomlNative": "config.toml (أصلي)"
"configTomlNative": "config.toml (أصلي)",
"loginButton": "تسجيل الدخول باستخدام ChatGPT",
"loginRequesting": "جارٍ طلب رمز تسجيل الدخول...",
"loginStep1": "افتح الرابط التالي في متصفحك:",
"loginStep2": "أدخل الرمز أدناه:",
"loginPolling": "في انتظار التفويض...",
"loginCancel": "إلغاء",
"loginSuccess": "تم تسجيل الدخول بنجاح، تم حفظ الإعدادات!",
"loginFailed": "فشل تسجيل الدخول: {message}",
"loginRetry": "إعادة المحاولة",
"loginCodeCopied": "تم نسخ الرمز",
"loggedIn": "تم تسجيل الدخول",
"loginRelogin": "إعادة تسجيل الدخول / تبديل الحساب",
"loginTimeout": "انتهت مهلة تسجيل الدخول، يرجى المحاولة مرة أخرى",
"loginSaveFailed": "تم تسجيل الدخول بنجاح ولكن فشل حفظ الإعدادات"
},
"gemini": {
"authConfig": "إعداد مصادقة Gemini",

View File

@@ -557,7 +557,21 @@
"enableFast": "Fast aktivieren",
"enableFastAria": "Fast-Servicestufe für Codex aktivieren",
"authJsonNative": "auth.json (nativ)",
"configTomlNative": "config.toml (nativ)"
"configTomlNative": "config.toml (nativ)",
"loginButton": "Mit ChatGPT anmelden",
"loginRequesting": "Login-Code wird angefordert...",
"loginStep1": "Öffnen Sie die folgende URL in Ihrem Browser:",
"loginStep2": "Geben Sie den folgenden Code ein:",
"loginPolling": "Warte auf Autorisierung...",
"loginCancel": "Abbrechen",
"loginSuccess": "Erfolgreich angemeldet, Konfiguration gespeichert!",
"loginFailed": "Anmeldung fehlgeschlagen: {message}",
"loginRetry": "Erneut versuchen",
"loginCodeCopied": "Code kopiert",
"loggedIn": "Konto angemeldet",
"loginRelogin": "Erneut anmelden / Konto wechseln",
"loginTimeout": "Anmeldung abgelaufen, bitte erneut versuchen",
"loginSaveFailed": "Anmeldung erfolgreich, aber Konfiguration konnte nicht gespeichert werden"
},
"gemini": {
"authConfig": "Gemini-Auth-Konfiguration",

View File

@@ -557,7 +557,21 @@
"enableFast": "Enable Fast",
"enableFastAria": "Enable Fast service tier for Codex",
"authJsonNative": "auth.json (native)",
"configTomlNative": "config.toml (native)"
"configTomlNative": "config.toml (native)",
"loginButton": "Log in with ChatGPT",
"loginRequesting": "Requesting login code...",
"loginStep1": "Open the following URL in your browser:",
"loginStep2": "Enter the code below:",
"loginPolling": "Waiting for authorization...",
"loginCancel": "Cancel",
"loginSuccess": "Logged in successfully, config saved!",
"loginFailed": "Login failed: {message}",
"loginRetry": "Retry",
"loginCodeCopied": "Code copied",
"loggedIn": "Account logged in",
"loginRelogin": "Re-login / Switch account",
"loginTimeout": "Login timed out, please try again",
"loginSaveFailed": "Login succeeded but failed to save config"
},
"gemini": {
"authConfig": "Gemini Auth Config",

View File

@@ -557,7 +557,21 @@
"enableFast": "Habilitar Fast",
"enableFastAria": "Habilitar nivel de servicio Fast para Codex",
"authJsonNative": "auth.json (nativo)",
"configTomlNative": "config.toml (nativo)"
"configTomlNative": "config.toml (nativo)",
"loginButton": "Iniciar sesión con ChatGPT",
"loginRequesting": "Solicitando código de inicio de sesión...",
"loginStep1": "Abre la siguiente URL en tu navegador:",
"loginStep2": "Introduce el siguiente código:",
"loginPolling": "Esperando autorización...",
"loginCancel": "Cancelar",
"loginSuccess": "¡Inicio de sesión exitoso, configuración guardada!",
"loginFailed": "Error de inicio de sesión: {message}",
"loginRetry": "Reintentar",
"loginCodeCopied": "Código copiado",
"loggedIn": "Cuenta conectada",
"loginRelogin": "Reconectar / Cambiar cuenta",
"loginTimeout": "Tiempo de inicio de sesión agotado, inténtalo de nuevo",
"loginSaveFailed": "Inicio de sesión exitoso pero falló al guardar la configuración"
},
"gemini": {
"authConfig": "Configuración de autenticación de Gemini",

View File

@@ -557,7 +557,21 @@
"enableFast": "Activer Fast",
"enableFastAria": "Activer le niveau de service Fast pour Codex",
"authJsonNative": "auth.json (natif)",
"configTomlNative": "config.toml (natif)"
"configTomlNative": "config.toml (natif)",
"loginButton": "Se connecter avec ChatGPT",
"loginRequesting": "Demande du code de connexion...",
"loginStep1": "Ouvrez l'URL suivante dans votre navigateur :",
"loginStep2": "Entrez le code ci-dessous :",
"loginPolling": "En attente d'autorisation...",
"loginCancel": "Annuler",
"loginSuccess": "Connexion réussie, configuration enregistrée !",
"loginFailed": "Échec de la connexion : {message}",
"loginRetry": "Réessayer",
"loginCodeCopied": "Code copié",
"loggedIn": "Compte connecté",
"loginRelogin": "Reconnecter / Changer de compte",
"loginTimeout": "Connexion expirée, veuillez réessayer",
"loginSaveFailed": "Connexion réussie mais échec de la sauvegarde de la configuration"
},
"gemini": {
"authConfig": "Configuration dauthentification Gemini",

View File

@@ -557,7 +557,21 @@
"enableFast": "Fast を有効化",
"enableFastAria": "Codex の Fast サービスティアを有効化",
"authJsonNative": "auth.jsonネイティブ",
"configTomlNative": "config.tomlネイティブ"
"configTomlNative": "config.tomlネイティブ",
"loginButton": "ChatGPT でログイン",
"loginRequesting": "ログインコードを取得中...",
"loginStep1": "ブラウザで以下の URL を開いてください:",
"loginStep2": "以下のコードを入力してください:",
"loginPolling": "認証を待っています...",
"loginCancel": "キャンセル",
"loginSuccess": "ログイン成功、設定を保存しました!",
"loginFailed": "ログインに失敗しました:{message}",
"loginRetry": "再試行",
"loginCodeCopied": "コードをコピーしました",
"loggedIn": "アカウントにログイン済み",
"loginRelogin": "再ログイン / アカウント切替",
"loginTimeout": "ログインがタイムアウトしました。再試行してください",
"loginSaveFailed": "ログインは成功しましたが、設定の保存に失敗しました"
},
"gemini": {
"authConfig": "Gemini 認証設定",

View File

@@ -557,7 +557,21 @@
"enableFast": "Fast 활성화",
"enableFastAria": "Codex용 Fast 서비스 계층 활성화",
"authJsonNative": "auth.json (네이티브)",
"configTomlNative": "config.toml (네이티브)"
"configTomlNative": "config.toml (네이티브)",
"loginButton": "ChatGPT로 로그인",
"loginRequesting": "로그인 코드 요청 중...",
"loginStep1": "브라우저에서 다음 URL을 열어주세요:",
"loginStep2": "아래 코드를 입력하세요:",
"loginPolling": "인증 대기 중...",
"loginCancel": "취소",
"loginSuccess": "로그인 성공, 설정이 저장되었습니다!",
"loginFailed": "로그인 실패: {message}",
"loginRetry": "재시도",
"loginCodeCopied": "코드가 복사되었습니다",
"loggedIn": "계정 로그인됨",
"loginRelogin": "재로그인 / 계정 전환",
"loginTimeout": "로그인 시간 초과, 다시 시도해 주세요",
"loginSaveFailed": "로그인 성공했지만 설정 저장 실패"
},
"gemini": {
"authConfig": "Gemini 인증 설정",

View File

@@ -557,7 +557,21 @@
"enableFast": "Habilitar Fast",
"enableFastAria": "Habilitar nível de serviço Fast para Codex",
"authJsonNative": "auth.json (nativo)",
"configTomlNative": "config.toml (nativo)"
"configTomlNative": "config.toml (nativo)",
"loginButton": "Entrar com ChatGPT",
"loginRequesting": "Solicitando código de login...",
"loginStep1": "Abra a seguinte URL no seu navegador:",
"loginStep2": "Digite o código abaixo:",
"loginPolling": "Aguardando autorização...",
"loginCancel": "Cancelar",
"loginSuccess": "Login realizado com sucesso, configuração salva!",
"loginFailed": "Falha no login: {message}",
"loginRetry": "Tentar novamente",
"loginCodeCopied": "Código copiado",
"loggedIn": "Conta conectada",
"loginRelogin": "Reconectar / Trocar conta",
"loginTimeout": "Login expirou, tente novamente",
"loginSaveFailed": "Login realizado com sucesso, mas falha ao salvar configuração"
},
"gemini": {
"authConfig": "Configuração de autenticação do Gemini",

View File

@@ -557,7 +557,21 @@
"enableFast": "启用 Fast",
"enableFastAria": "Codex 启用 Fast 服务等级",
"authJsonNative": "auth.json原生",
"configTomlNative": "config.toml原生"
"configTomlNative": "config.toml原生",
"loginButton": "使用 ChatGPT 登录",
"loginRequesting": "正在请求登录码...",
"loginStep1": "在浏览器中打开以下链接:",
"loginStep2": "输入以下代码:",
"loginPolling": "等待授权中...",
"loginCancel": "取消",
"loginSuccess": "登录成功,配置已保存!",
"loginFailed": "登录失败:{message}",
"loginRetry": "重试",
"loginCodeCopied": "已复制代码",
"loggedIn": "账号已登录",
"loginRelogin": "重新登录 / 切换账号",
"loginTimeout": "登录超时,请重试",
"loginSaveFailed": "登录成功但配置保存失败"
},
"gemini": {
"authConfig": "Gemini 认证配置",

View File

@@ -557,7 +557,21 @@
"enableFast": "啟用 Fast",
"enableFastAria": "Codex 啟用 Fast 服務等級",
"authJsonNative": "auth.json原生",
"configTomlNative": "config.toml原生"
"configTomlNative": "config.toml原生",
"loginButton": "使用 ChatGPT 登入",
"loginRequesting": "正在請求登入碼...",
"loginStep1": "在瀏覽器中打開以下連結:",
"loginStep2": "輸入以下代碼:",
"loginPolling": "等待授權中...",
"loginCancel": "取消",
"loginSuccess": "登入成功,配置已儲存!",
"loginFailed": "登入失敗:{message}",
"loginRetry": "重試",
"loginCodeCopied": "已複製代碼",
"loggedIn": "帳號已登入",
"loginRelogin": "重新登入 / 切換帳號",
"loginTimeout": "登入逾時,請重試",
"loginSaveFailed": "登入成功但配置儲存失敗"
},
"gemini": {
"authConfig": "Gemini 認證配置",

View File

@@ -277,6 +277,32 @@ export async function acpReorderAgents(agentTypes: AgentType[]): Promise<void> {
return getTransport().call("acp_reorder_agents", { agentTypes })
}
export async function codexRequestDeviceCode(): Promise<{
userCode: string
verificationUrl: string
deviceAuthId: string
interval: number
}> {
return getTransport().call("codex_request_device_code", {})
}
export async function codexPollDeviceCode(params: {
deviceAuthId: string
userCode: string
}): Promise<{
status: "pending" | "success" | "error"
message?: string
idToken?: string
accessToken?: string
refreshToken?: string
accountId?: string
}> {
return getTransport().call("codex_poll_device_code", {
deviceAuthId: params.deviceAuthId,
userCode: params.userCode,
})
}
export async function acpPreflight(
agentType: AgentType,
forceRefresh?: boolean