From 1292b8b2d5e90480521d45af05ff0e8b38a199f6 Mon Sep 17 00:00:00 2001 From: Seefs <40468931+seefs001@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:45:15 +0800 Subject: [PATCH] chore: update Codex channel (#5461) --- constant/channel.go | 2 +- controller/codex_oauth.go | 247 ------------------ router/api-router.go | 4 - service/codex_oauth.go | 157 +---------- .../table/channels/modals/CodexOAuthModal.jsx | 172 ------------ .../channels/modals/EditChannelModal.jsx | 24 -- .../src/constants/channel.constants.js | 2 +- web/classic/src/i18n/locales/en.json | 6 - web/classic/src/i18n/locales/fr.json | 6 - web/classic/src/i18n/locales/ja.json | 6 - web/classic/src/i18n/locales/ru.json | 6 - web/classic/src/i18n/locales/vi.json | 6 - web/classic/src/i18n/locales/zh-CN.json | 6 - web/classic/src/i18n/locales/zh-TW.json | 6 - web/default/scripts/sync-i18n.mjs | 2 +- web/default/src/components/provider-badge.tsx | 15 +- web/default/src/features/channels/api.ts | 40 --- .../channels/components/channels-columns.tsx | 28 +- .../components/dialogs/codex-oauth-dialog.tsx | 215 --------------- .../drawers/channel-mutate-drawer.tsx | 91 +++---- .../src/features/channels/constants.ts | 2 +- web/default/src/i18n/locales/en.json | 16 +- web/default/src/i18n/locales/fr.json | 16 +- web/default/src/i18n/locales/ja.json | 16 +- web/default/src/i18n/locales/ru.json | 16 +- web/default/src/i18n/locales/vi.json | 16 +- web/default/src/i18n/locales/zh.json | 16 +- 27 files changed, 100 insertions(+), 1039 deletions(-) delete mode 100644 controller/codex_oauth.go delete mode 100644 web/classic/src/components/table/channels/modals/CodexOAuthModal.jsx delete mode 100644 web/default/src/features/channels/components/dialogs/codex-oauth-dialog.tsx diff --git a/constant/channel.go b/constant/channel.go index 48502bed..e1489512 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -174,7 +174,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeDoubaoVideo: "DoubaoVideo", ChannelTypeSora: "Sora", ChannelTypeReplicate: "Replicate", - ChannelTypeCodex: "Codex", + ChannelTypeCodex: "ChatGPT Subscription (Codex)", } func GetChannelTypeName(channelType int) string { diff --git a/controller/codex_oauth.go b/controller/codex_oauth.go deleted file mode 100644 index de9743ab..00000000 --- a/controller/codex_oauth.go +++ /dev/null @@ -1,247 +0,0 @@ -package controller - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" - "github.com/QuantumNous/new-api/model" - "github.com/QuantumNous/new-api/relay/channel/codex" - "github.com/QuantumNous/new-api/service" - - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" -) - -type codexOAuthCompleteRequest struct { - Input string `json:"input"` -} - -func codexOAuthSessionKey(channelID int, field string) string { - return fmt.Sprintf("codex_oauth_%s_%d", field, channelID) -} - -func parseCodexAuthorizationInput(input string) (code string, state string, err error) { - v := strings.TrimSpace(input) - if v == "" { - return "", "", errors.New("empty input") - } - if strings.Contains(v, "#") { - parts := strings.SplitN(v, "#", 2) - code = strings.TrimSpace(parts[0]) - state = strings.TrimSpace(parts[1]) - return code, state, nil - } - if strings.Contains(v, "code=") { - u, parseErr := url.Parse(v) - if parseErr == nil { - q := u.Query() - code = strings.TrimSpace(q.Get("code")) - state = strings.TrimSpace(q.Get("state")) - return code, state, nil - } - q, parseErr := url.ParseQuery(v) - if parseErr == nil { - code = strings.TrimSpace(q.Get("code")) - state = strings.TrimSpace(q.Get("state")) - return code, state, nil - } - } - - code = v - return code, "", nil -} - -func StartCodexOAuth(c *gin.Context) { - startCodexOAuthWithChannelID(c, 0) -} - -func StartCodexOAuthForChannel(c *gin.Context) { - channelID, err := strconv.Atoi(c.Param("id")) - if err != nil { - common.ApiError(c, fmt.Errorf("invalid channel id: %w", err)) - return - } - startCodexOAuthWithChannelID(c, channelID) -} - -func startCodexOAuthWithChannelID(c *gin.Context, channelID int) { - if channelID > 0 { - ch, err := model.GetChannelById(channelID, false) - if err != nil { - common.ApiError(c, err) - return - } - if ch == nil { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel not found"}) - return - } - if ch.Type != constant.ChannelTypeCodex { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel type is not Codex"}) - return - } - } - - flow, err := service.CreateCodexOAuthAuthorizationFlow() - if err != nil { - common.ApiError(c, err) - return - } - - session := sessions.Default(c) - session.Set(codexOAuthSessionKey(channelID, "state"), flow.State) - session.Set(codexOAuthSessionKey(channelID, "verifier"), flow.Verifier) - session.Set(codexOAuthSessionKey(channelID, "created_at"), time.Now().Unix()) - _ = session.Save() - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "authorize_url": flow.AuthorizeURL, - }, - }) -} - -func CompleteCodexOAuth(c *gin.Context) { - completeCodexOAuthWithChannelID(c, 0) -} - -func CompleteCodexOAuthForChannel(c *gin.Context) { - channelID, err := strconv.Atoi(c.Param("id")) - if err != nil { - common.ApiError(c, fmt.Errorf("invalid channel id: %w", err)) - return - } - completeCodexOAuthWithChannelID(c, channelID) -} - -func completeCodexOAuthWithChannelID(c *gin.Context, channelID int) { - req := codexOAuthCompleteRequest{} - if err := c.ShouldBindJSON(&req); err != nil { - common.ApiError(c, err) - return - } - - code, state, err := parseCodexAuthorizationInput(req.Input) - if err != nil { - common.SysError("failed to parse codex authorization input: " + err.Error()) - c.JSON(http.StatusOK, gin.H{"success": false, "message": "解析授权信息失败,请检查输入格式"}) - return - } - if strings.TrimSpace(code) == "" { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "missing authorization code"}) - return - } - if strings.TrimSpace(state) == "" { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "missing state in input"}) - return - } - - channelProxy := "" - if channelID > 0 { - ch, err := model.GetChannelById(channelID, false) - if err != nil { - common.ApiError(c, err) - return - } - if ch == nil { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel not found"}) - return - } - if ch.Type != constant.ChannelTypeCodex { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel type is not Codex"}) - return - } - channelProxy = ch.GetSetting().Proxy - } - - session := sessions.Default(c) - expectedState, _ := session.Get(codexOAuthSessionKey(channelID, "state")).(string) - verifier, _ := session.Get(codexOAuthSessionKey(channelID, "verifier")).(string) - if strings.TrimSpace(expectedState) == "" || strings.TrimSpace(verifier) == "" { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "oauth flow not started or session expired"}) - return - } - if state != expectedState { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "state mismatch"}) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - tokenRes, err := service.ExchangeCodexAuthorizationCodeWithProxy(ctx, code, verifier, channelProxy) - if err != nil { - common.SysError("failed to exchange codex authorization code: " + err.Error()) - c.JSON(http.StatusOK, gin.H{"success": false, "message": "授权码交换失败,请重试"}) - return - } - - accountID, ok := service.ExtractCodexAccountIDFromJWT(tokenRes.AccessToken) - if !ok { - c.JSON(http.StatusOK, gin.H{"success": false, "message": "failed to extract account_id from access_token"}) - return - } - email, _ := service.ExtractEmailFromJWT(tokenRes.AccessToken) - - key := codex.OAuthKey{ - AccessToken: tokenRes.AccessToken, - RefreshToken: tokenRes.RefreshToken, - AccountID: accountID, - LastRefresh: time.Now().Format(time.RFC3339), - Expired: tokenRes.ExpiresAt.Format(time.RFC3339), - Email: email, - Type: "codex", - } - encoded, err := common.Marshal(key) - if err != nil { - common.ApiError(c, err) - return - } - - session.Delete(codexOAuthSessionKey(channelID, "state")) - session.Delete(codexOAuthSessionKey(channelID, "verifier")) - session.Delete(codexOAuthSessionKey(channelID, "created_at")) - _ = session.Save() - - if channelID > 0 { - if err := model.DB.Model(&model.Channel{}).Where("id = ?", channelID).Update("key", string(encoded)).Error; err != nil { - common.ApiError(c, err) - return - } - model.InitChannelCache() - service.ResetProxyClientCache() - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "saved", - "data": gin.H{ - "channel_id": channelID, - "account_id": accountID, - "email": email, - "expires_at": key.Expired, - "last_refresh": key.LastRefresh, - }, - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "generated", - "data": gin.H{ - "key": string(encoded), - "account_id": accountID, - "email": email, - "expires_at": key.Expired, - "last_refresh": key.LastRefresh, - }, - }) -} diff --git a/router/api-router.go b/router/api-router.go index e98dc66a..baf7cda2 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -249,10 +249,6 @@ func SetApiRouter(router *gin.Engine) { channelRoute.POST("/fix", controller.FixChannelsAbilities) channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels) channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels) - channelRoute.POST("/codex/oauth/start", controller.StartCodexOAuth) - channelRoute.POST("/codex/oauth/complete", controller.CompleteCodexOAuth) - channelRoute.POST("/:id/codex/oauth/start", controller.StartCodexOAuthForChannel) - channelRoute.POST("/:id/codex/oauth/complete", controller.CompleteCodexOAuthForChannel) channelRoute.POST("/:id/codex/refresh", controller.RefreshCodexChannelCredential) channelRoute.GET("/:id/codex/usage", controller.GetCodexChannelUsage) channelRoute.POST("/ollama/pull", controller.OllamaPullModel) diff --git a/service/codex_oauth.go b/service/codex_oauth.go index 33ef1d60..c201b59a 100644 --- a/service/codex_oauth.go +++ b/service/codex_oauth.go @@ -2,10 +2,7 @@ package service import ( "context" - "crypto/rand" - "crypto/sha256" "encoding/base64" - "encoding/json" "errors" "fmt" "net/http" @@ -17,13 +14,10 @@ import ( ) const ( - codexOAuthClientID = "app_EMoamEEZ73f0CkXaXp7hrann" - codexOAuthAuthorizeURL = "https://auth.openai.com/oauth/authorize" - codexOAuthTokenURL = "https://auth.openai.com/oauth/token" - codexOAuthRedirectURI = "http://localhost:1455/auth/callback" - codexOAuthScope = "openid profile email offline_access" - codexJWTClaimPath = "https://api.openai.com/auth" - defaultHTTPTimeout = 20 * time.Second + codexOAuthClientID = "app_EMoamEEZ73f0CkXaXp7hrann" + codexOAuthTokenURL = "https://auth.openai.com/oauth/token" + codexJWTClaimPath = "https://api.openai.com/auth" + defaultHTTPTimeout = 20 * time.Second ) type CodexOAuthTokenResult struct { @@ -32,13 +26,6 @@ type CodexOAuthTokenResult struct { ExpiresAt time.Time } -type CodexOAuthAuthorizationFlow struct { - State string - Verifier string - Challenge string - AuthorizeURL string -} - func RefreshCodexOAuthToken(ctx context.Context, refreshToken string) (*CodexOAuthTokenResult, error) { return RefreshCodexOAuthTokenWithProxy(ctx, refreshToken, "") } @@ -51,39 +38,6 @@ func RefreshCodexOAuthTokenWithProxy(ctx context.Context, refreshToken string, p return refreshCodexOAuthToken(ctx, client, codexOAuthTokenURL, codexOAuthClientID, refreshToken) } -func ExchangeCodexAuthorizationCode(ctx context.Context, code string, verifier string) (*CodexOAuthTokenResult, error) { - return ExchangeCodexAuthorizationCodeWithProxy(ctx, code, verifier, "") -} - -func ExchangeCodexAuthorizationCodeWithProxy(ctx context.Context, code string, verifier string, proxyURL string) (*CodexOAuthTokenResult, error) { - client, err := getCodexOAuthHTTPClient(proxyURL) - if err != nil { - return nil, err - } - return exchangeCodexAuthorizationCode(ctx, client, codexOAuthTokenURL, codexOAuthClientID, code, verifier, codexOAuthRedirectURI) -} - -func CreateCodexOAuthAuthorizationFlow() (*CodexOAuthAuthorizationFlow, error) { - state, err := createStateHex(16) - if err != nil { - return nil, err - } - verifier, challenge, err := generatePKCEPair() - if err != nil { - return nil, err - } - u, err := buildCodexAuthorizeURL(state, challenge) - if err != nil { - return nil, err - } - return &CodexOAuthAuthorizationFlow{ - State: state, - Verifier: verifier, - Challenge: challenge, - AuthorizeURL: u, - }, nil -} - func refreshCodexOAuthToken( ctx context.Context, client *http.Client, @@ -138,65 +92,6 @@ func refreshCodexOAuthToken( }, nil } -func exchangeCodexAuthorizationCode( - ctx context.Context, - client *http.Client, - tokenURL string, - clientID string, - code string, - verifier string, - redirectURI string, -) (*CodexOAuthTokenResult, error) { - c := strings.TrimSpace(code) - v := strings.TrimSpace(verifier) - if c == "" { - return nil, errors.New("empty authorization code") - } - if v == "" { - return nil, errors.New("empty code_verifier") - } - - form := url.Values{} - form.Set("grant_type", "authorization_code") - form.Set("client_id", clientID) - form.Set("code", c) - form.Set("code_verifier", v) - form.Set("redirect_uri", redirectURI) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode())) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var payload struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - } - if err := common.DecodeJson(resp.Body, &payload); err != nil { - return nil, err - } - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("codex oauth code exchange failed: status=%d", resp.StatusCode) - } - if strings.TrimSpace(payload.AccessToken) == "" || strings.TrimSpace(payload.RefreshToken) == "" || payload.ExpiresIn <= 0 { - return nil, errors.New("codex oauth token response missing fields") - } - return &CodexOAuthTokenResult{ - AccessToken: strings.TrimSpace(payload.AccessToken), - RefreshToken: strings.TrimSpace(payload.RefreshToken), - ExpiresAt: time.Now().Add(time.Duration(payload.ExpiresIn) * time.Second), - }, nil -} - func getCodexOAuthHTTPClient(proxyURL string) (*http.Client, error) { baseClient, err := GetHttpClientWithProxy(strings.TrimSpace(proxyURL)) if err != nil { @@ -210,48 +105,6 @@ func getCodexOAuthHTTPClient(proxyURL string) (*http.Client, error) { return &clientCopy, nil } -func buildCodexAuthorizeURL(state string, challenge string) (string, error) { - u, err := url.Parse(codexOAuthAuthorizeURL) - if err != nil { - return "", err - } - q := u.Query() - q.Set("response_type", "code") - q.Set("client_id", codexOAuthClientID) - q.Set("redirect_uri", codexOAuthRedirectURI) - q.Set("scope", codexOAuthScope) - q.Set("code_challenge", challenge) - q.Set("code_challenge_method", "S256") - q.Set("state", state) - q.Set("id_token_add_organizations", "true") - q.Set("codex_cli_simplified_flow", "true") - q.Set("originator", "codex_cli_rs") - u.RawQuery = q.Encode() - return u.String(), nil -} - -func createStateHex(nBytes int) (string, error) { - if nBytes <= 0 { - return "", errors.New("invalid state bytes length") - } - b := make([]byte, nBytes) - if _, err := rand.Read(b); err != nil { - return "", err - } - return fmt.Sprintf("%x", b), nil -} - -func generatePKCEPair() (verifier string, challenge string, err error) { - b := make([]byte, 32) - if _, err := rand.Read(b); err != nil { - return "", "", err - } - verifier = base64.RawURLEncoding.EncodeToString(b) - sum := sha256.Sum256([]byte(verifier)) - challenge = base64.RawURLEncoding.EncodeToString(sum[:]) - return verifier, challenge, nil -} - func ExtractCodexAccountIDFromJWT(token string) (string, bool) { claims, ok := decodeJWTClaims(token) if !ok { @@ -310,7 +163,7 @@ func decodeJWTClaims(token string) (map[string]any, bool) { return nil, false } var claims map[string]any - if err := json.Unmarshal(payloadRaw, &claims); err != nil { + if err := common.Unmarshal(payloadRaw, &claims); err != nil { return nil, false } return claims, true diff --git a/web/classic/src/components/table/channels/modals/CodexOAuthModal.jsx b/web/classic/src/components/table/channels/modals/CodexOAuthModal.jsx deleted file mode 100644 index 7f3f349b..00000000 --- a/web/classic/src/components/table/channels/modals/CodexOAuthModal.jsx +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Modal, - Button, - Space, - Typography, - Input, - Banner, -} from '@douyinfe/semi-ui'; -import { API, copy, showError, showSuccess } from '../../../../helpers'; - -const { Text } = Typography; - -const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [authorizeUrl, setAuthorizeUrl] = useState(''); - const [input, setInput] = useState(''); - - const startOAuth = async () => { - setLoading(true); - try { - const res = await API.post( - '/api/channel/codex/oauth/start', - {}, - { skipErrorHandler: true }, - ); - if (!res?.data?.success) { - console.error('Codex OAuth start failed:', res?.data?.message); - throw new Error(t('启动授权失败')); - } - const url = res?.data?.data?.authorize_url || ''; - if (!url) { - console.error( - 'Codex OAuth start response missing authorize_url:', - res?.data, - ); - throw new Error(t('响应缺少授权链接')); - } - setAuthorizeUrl(url); - window.open(url, '_blank', 'noopener,noreferrer'); - showSuccess(t('已打开授权页面')); - } catch (error) { - showError(error?.message || t('启动授权失败')); - } finally { - setLoading(false); - } - }; - - const completeOAuth = async () => { - if (!input || !input.trim()) { - showError(t('请先粘贴回调 URL')); - return; - } - - setLoading(true); - try { - const res = await API.post( - '/api/channel/codex/oauth/complete', - { input }, - { skipErrorHandler: true }, - ); - if (!res?.data?.success) { - console.error('Codex OAuth complete failed:', res?.data?.message); - throw new Error(t('授权失败')); - } - - const key = res?.data?.data?.key || ''; - if (!key) { - console.error('Codex OAuth complete response missing key:', res?.data); - throw new Error(t('响应缺少凭据')); - } - - onSuccess && onSuccess(key); - showSuccess(t('已生成授权凭据')); - onCancel && onCancel(); - } catch (error) { - showError(error?.message || t('授权失败')); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (!visible) return; - setAuthorizeUrl(''); - setInput(''); - }, [visible]); - - return ( - - - - - } - > - - - - - - - - - setInput(value)} - placeholder={t('请粘贴完整回调 URL(包含 code 与 state)')} - showClear - /> - - - {t( - '说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。', - )} - - - - ); -}; - -export default CodexOAuthModal; diff --git a/web/classic/src/components/table/channels/modals/EditChannelModal.jsx b/web/classic/src/components/table/channels/modals/EditChannelModal.jsx index fad105b1..cad79773 100644 --- a/web/classic/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/classic/src/components/table/channels/modals/EditChannelModal.jsx @@ -60,7 +60,6 @@ import { import ModelSelectModal from './ModelSelectModal'; import SingleModelSelectModal from './SingleModelSelectModal'; import OllamaModelModal from './OllamaModelModal'; -import CodexOAuthModal from './CodexOAuthModal'; import ParamOverrideEditorModal from './ParamOverrideEditorModal'; import JSONEditor from '../../../common/ui/JSONEditor'; import SecureVerificationModal from '../../../common/modals/SecureVerificationModal'; @@ -381,7 +380,6 @@ const EditChannelModal = (props) => { }, [inputs.param_override, t]); const [isIonetChannel, setIsIonetChannel] = useState(false); const [ionetMetadata, setIonetMetadata] = useState(null); - const [codexOAuthModalVisible, setCodexOAuthModalVisible] = useState(false); const [codexCredentialRefreshing, setCodexCredentialRefreshing] = useState(false); const [paramOverrideEditorVisible, setParamOverrideEditorVisible] = @@ -1227,11 +1225,6 @@ const EditChannelModal = (props) => { } }; - const handleCodexOAuthGenerated = (key) => { - handleInputChange('key', key); - formatJsonField('key'); - }; - const handleRefreshCodexCredential = async () => { if (!isEdit) return; @@ -2847,17 +2840,6 @@ const EditChannelModal = (props) => { - {isEdit && ( - - - } - > -
- - - {t( - '1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".' - )} - - - -
- - - -
- -
-
{t('Callback URL')}
- - setState((prev) => ({ ...prev, callbackUrl: e.target.value })) - } - placeholder={t( - 'Paste the full callback URL (includes code & state)' - )} - autoComplete='off' - spellCheck={false} - /> -
- {t( - 'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.' - )} -
-
-
- - ) -} diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 25258d92..87dcc84a 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -38,7 +38,6 @@ import { Eraser, Plus, Eye, - Link2, RefreshCw, Code, Route, @@ -148,7 +147,6 @@ import { } from '../../lib/status-code-risk-guard' import type { Channel } from '../../types' import { useChannels } from '../channels-provider' -import { CodexOAuthDialog } from '../dialogs/codex-oauth-dialog' import { FetchModelsDialog } from '../dialogs/fetch-models-dialog' import { MissingModelsConfirmationDialog, @@ -281,7 +279,6 @@ export function ChannelMutateDrawer({ const [fetchModelsDialogOpen, setFetchModelsDialogOpen] = useState(false) const [channelKey, setChannelKey] = useState(null) const [isChannelKeyLoading, setIsChannelKeyLoading] = useState(false) - const [codexOAuthDialogOpen, setCodexOAuthDialogOpen] = useState(false) const [isCodexCredentialRefreshing, setIsCodexCredentialRefreshing] = useState(false) const initialModelsRef = useRef([]) @@ -373,6 +370,7 @@ export function ChannelMutateDrawer({ const currentName = form.watch('name') const currentModelMapping = form.watch('model_mapping') const awsKeyType = form.watch('aws_key_type') + const vertexKeyType = form.watch('vertex_key_type') const upstreamModelUpdateCheckEnabled = form.watch( 'upstream_model_update_check_enabled' ) @@ -399,6 +397,15 @@ export function ChannelMutateDrawer({ const isBatchMode = multiKeyMode === 'batch' || multiKeyMode === 'multi_to_single' const isChannelDetailLoading = isEditing && isChannelLoading + const supportsMultiKeyAddMode = + currentType !== 57 && !(currentType === 41 && vertexKeyType === 'api_key') + const addModeOptions = useMemo( + () => + supportsMultiKeyAddMode + ? ADD_MODE_OPTIONS + : ADD_MODE_OPTIONS.filter((option) => option.value === 'single'), + [supportsMultiKeyAddMode] + ) // Get all models list const allModelsList = useMemo( @@ -622,6 +629,25 @@ export function ChannelMutateDrawer({ } }, [currentType, isEditing, form]) + useEffect(() => { + if (currentType !== 45 || currentBaseUrl !== 'doubao-coding-plan') return + + form.setValue('base_url', 'https://ark.cn-beijing.volces.com', { + shouldDirty: false, + shouldValidate: true, + }) + }, [currentBaseUrl, currentType, form]) + + useEffect(() => { + if (isEditing || supportsMultiKeyAddMode) return + if (multiKeyMode && multiKeyMode !== 'single') { + form.setValue('multi_key_mode', 'single', { + shouldDirty: true, + shouldValidate: true, + }) + } + }, [form, isEditing, multiKeyMode, supportsMultiKeyAddMode]) + // Validate base_url - warn if it ends with /v1 useEffect(() => { if (!currentBaseUrl || !currentBaseUrl.endsWith('/v1')) return @@ -1550,7 +1576,7 @@ export function ChannelMutateDrawer({ )} /> - {form.watch('vertex_key_type') === 'json' && ( + {vertexKeyType === 'json' && ( {t('Service account JSON file(s)')} @@ -1682,15 +1708,13 @@ export function ChannelMutateDrawer({ 'https://ark.ap-southeast.bytepluses.com' ), }, - { - value: 'doubao-coding-plan', - label: t('Doubao Coding Plan'), - }, ]} onValueChange={field.onChange} value={ - field.value || - 'https://ark.cn-beijing.volces.com' + field.value === 'doubao-coding-plan' + ? 'https://ark.cn-beijing.volces.com' + : field.value || + 'https://ark.cn-beijing.volces.com' } > @@ -1708,9 +1732,6 @@ export function ChannelMutateDrawer({ 'https://ark.ap-southeast.bytepluses.com' )} - - {t('Doubao Coding Plan')} - @@ -1806,7 +1827,7 @@ export function ChannelMutateDrawer({ {t('Add Mode')} - {t(FIELD_DESCRIPTIONS.BATCH_ADD)} + {t( + supportsMultiKeyAddMode + ? FIELD_DESCRIPTIONS.BATCH_ADD + : FIELD_DESCRIPTIONS.KEY + )} @@ -1988,26 +2013,12 @@ export function ChannelMutateDrawer({ {currentType === 57 && (
-
-
- {t('Codex Authorization')} -
-
- {t( - 'Codex channels use an OAuth JSON credential as the key.' - )} -
+
+ {t( + 'Codex channels use an OAuth JSON credential as the key.' + )}
- {isEditing && channelId && (
- + {t( - 'If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.' + "Disclaimer: Personal use only. Do not distribute or share any credentials. This channel has prerequisites and requires prior setup; use it only if you understand the flow and risks, and comply with OpenAI's terms and policies. Credentials and configuration are for Codex CLI integration only, and are not intended for any other client, platform, or channel." )}
)} - { - form.setValue('key', key, { shouldDirty: true }) - }} - /> - {isEditing && isMultiKeyChannel && (