Merge branch 'main-upstream' into fix/volcengine_default_baseurl

# Conflicts:
#	web/src/components/settings/personal/cards/AccountManagement.jsx
This commit is contained in:
Seefs
2025-09-29 12:17:44 +08:00
4 changed files with 134 additions and 36 deletions

View File

@@ -8,6 +8,7 @@ import (
"one-api/constant" "one-api/constant"
"one-api/dto" "one-api/dto"
"one-api/model" "one-api/model"
"one-api/service"
"strconv" "strconv"
"strings" "strings"
@@ -633,6 +634,7 @@ func AddChannel(c *gin.Context) {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
service.ResetProxyClientCache()
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@@ -894,6 +896,7 @@ func UpdateChannel(c *gin.Context) {
return return
} }
model.InitChannelCache() model.InitChannelCache()
service.ResetProxyClientCache()
channel.Key = "" channel.Key = ""
clearChannelInfo(&channel.Channel) clearChannelInfo(&channel.Channel)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@@ -7,12 +7,17 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"one-api/common" "one-api/common"
"sync"
"time" "time"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
) )
var httpClient *http.Client var (
httpClient *http.Client
proxyClientLock sync.Mutex
proxyClients = make(map[string]*http.Client)
)
func InitHttpClient() { func InitHttpClient() {
if common.RelayTimeout == 0 { if common.RelayTimeout == 0 {
@@ -28,12 +33,31 @@ func GetHttpClient() *http.Client {
return httpClient return httpClient
} }
// ResetProxyClientCache 清空代理客户端缓存,确保下次使用时重新初始化
func ResetProxyClientCache() {
proxyClientLock.Lock()
defer proxyClientLock.Unlock()
for _, client := range proxyClients {
if transport, ok := client.Transport.(*http.Transport); ok && transport != nil {
transport.CloseIdleConnections()
}
}
proxyClients = make(map[string]*http.Client)
}
// NewProxyHttpClient 创建支持代理的 HTTP 客户端 // NewProxyHttpClient 创建支持代理的 HTTP 客户端
func NewProxyHttpClient(proxyURL string) (*http.Client, error) { func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
if proxyURL == "" { if proxyURL == "" {
return http.DefaultClient, nil return http.DefaultClient, nil
} }
proxyClientLock.Lock()
if client, ok := proxyClients[proxyURL]; ok {
proxyClientLock.Unlock()
return client, nil
}
proxyClientLock.Unlock()
parsedURL, err := url.Parse(proxyURL) parsedURL, err := url.Parse(proxyURL)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -41,11 +65,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
switch parsedURL.Scheme { switch parsedURL.Scheme {
case "http", "https": case "http", "https":
return &http.Client{ client := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: http.ProxyURL(parsedURL), Proxy: http.ProxyURL(parsedURL),
}, },
}, nil }
client.Timeout = time.Duration(common.RelayTimeout) * time.Second
proxyClientLock.Lock()
proxyClients[proxyURL] = client
proxyClientLock.Unlock()
return client, nil
case "socks5", "socks5h": case "socks5", "socks5h":
// 获取认证信息 // 获取认证信息
@@ -67,13 +96,18 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
return nil, err return nil, err
} }
return &http.Client{ client := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr) return dialer.Dial(network, addr)
}, },
}, },
}, nil }
client.Timeout = time.Duration(common.RelayTimeout) * time.Second
proxyClientLock.Lock()
proxyClients[proxyURL] = client
proxyClientLock.Unlock()
return client, nil
default: default:
return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme) return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme)

View File

@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { API, copy, showError, showInfo, showSuccess } from '../../helpers'; import {
API,
copy,
showError,
showInfo,
showSuccess,
setStatusData,
} from '../../helpers';
import { UserContext } from '../../context/User'; import { UserContext } from '../../context/User';
import { Modal } from '@douyinfe/semi-ui'; import { Modal } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -71,18 +78,40 @@ const PersonalSetting = () => {
}); });
useEffect(() => { useEffect(() => {
let status = localStorage.getItem('status'); let saved = localStorage.getItem('status');
if (status) { if (saved) {
status = JSON.parse(status); const parsed = JSON.parse(saved);
setStatus(status); setStatus(parsed);
if (status.turnstile_check) { if (parsed.turnstile_check) {
setTurnstileEnabled(true); setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key); setTurnstileSiteKey(parsed.turnstile_site_key);
} else {
setTurnstileEnabled(false);
setTurnstileSiteKey('');
} }
} }
getUserData().then((res) => { // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
console.log(userState); (async () => {
}); try {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success && data) {
setStatus(data);
setStatusData(data);
if (data.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(data.turnstile_site_key);
} else {
setTurnstileEnabled(false);
setTurnstileSiteKey('');
}
}
} catch (e) {
// ignore and keep local status
}
})();
getUserData();
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@@ -28,6 +28,7 @@ import {
Tabs, Tabs,
TabPane, TabPane,
Popover, Popover,
Modal,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconMail, IconMail,
@@ -83,6 +84,9 @@ const AccountManagement = ({
</Popover> </Popover>
); );
}; };
const isBound = (accountId) => Boolean(accountId);
const [showTelegramBindModal, setShowTelegramBindModal] = React.useState(false);
return ( return (
<Card className='!rounded-2xl'> <Card className='!rounded-2xl'>
{/* 卡片头部 */} {/* 卡片头部 */}
@@ -142,7 +146,7 @@ const AccountManagement = ({
size='small' size='small'
onClick={() => setShowEmailBindModal(true)} onClick={() => setShowEmailBindModal(true)}
> >
{userState.user && userState.user.email !== '' {isBound(userState.user?.email)
? t('修改绑定') ? t('修改绑定')
: t('绑定')} : t('绑定')}
</Button> </Button>
@@ -165,10 +169,11 @@ const AccountManagement = ({
{t('微信')} {t('微信')}
</div> </div>
<div className='text-sm text-gray-500 truncate'> <div className='text-sm text-gray-500 truncate'>
{renderAccountInfo( {!status.wechat_login
userState.user?.wechat_id, ? t('未启用')
t('微信 ID'), : isBound(userState.user?.wechat_id)
)} ? t('已绑定')
: t('未绑定')}
</div> </div>
</div> </div>
</div> </div>
@@ -180,7 +185,7 @@ const AccountManagement = ({
disabled={!status.wechat_login} disabled={!status.wechat_login}
onClick={() => setShowWeChatBindModal(true)} onClick={() => setShowWeChatBindModal(true)}
> >
{userState.user && userState.user?.wechat_id {isBound(userState.user?.wechat_id)
? t('修改绑定') ? t('修改绑定')
: status.wechat_login : status.wechat_login
? t('绑定') ? t('绑定')
@@ -221,8 +226,7 @@ const AccountManagement = ({
onGitHubOAuthClicked(status.github_client_id) onGitHubOAuthClicked(status.github_client_id)
} }
disabled={ disabled={
(userState.user && userState.user.github_id !== '') || isBound(userState.user?.github_id) || !status.github_oauth
!status.github_oauth
} }
> >
{status.github_oauth ? t('绑定') : t('未启用')} {status.github_oauth ? t('绑定') : t('未启用')}
@@ -265,8 +269,7 @@ const AccountManagement = ({
) )
} }
disabled={ disabled={
(userState.user && userState.user.oidc_id !== '') || isBound(userState.user?.oidc_id) || !status.oidc_enabled
!status.oidc_enabled
} }
> >
{status.oidc_enabled ? t('绑定') : t('未启用')} {status.oidc_enabled ? t('绑定') : t('未启用')}
@@ -299,26 +302,56 @@ const AccountManagement = ({
</div> </div>
<div className='flex-shrink-0'> <div className='flex-shrink-0'>
{status.telegram_oauth ? ( {status.telegram_oauth ? (
userState.user?.telegram_id ? ( isBound(userState.user?.telegram_id) ? (
<Button disabled={true} size='small'> <Button
disabled
size='small'
type='primary'
theme='outline'
>
{t('已绑定')} {t('已绑定')}
</Button> </Button>
) : ( ) : (
<div className='scale-75'> <Button
<TelegramLoginButton type='primary'
dataAuthUrl='/api/oauth/telegram/bind' theme='outline'
botName={status.telegram_bot_name} size='small'
/> onClick={() => setShowTelegramBindModal(true)}
</div> >
{t('绑定')}
</Button>
) )
) : ( ) : (
<Button disabled={true} size='small'> <Button
disabled
size='small'
type='primary'
theme='outline'
>
{t('未启用')} {t('未启用')}
</Button> </Button>
)} )}
</div> </div>
</div> </div>
</Card> </Card>
<Modal
title={t('绑定 Telegram')}
visible={showTelegramBindModal}
onCancel={() => setShowTelegramBindModal(false)}
footer={null}
>
<div className='my-3 text-sm text-gray-600'>
{t('点击下方按钮通过 Telegram 完成绑定')}
</div>
<div className='flex justify-center'>
<div className='scale-90'>
<TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind'
botName={status.telegram_bot_name}
/>
</div>
</div>
</Modal>
{/* LinuxDO绑定 */} {/* LinuxDO绑定 */}
<Card className='!rounded-xl'> <Card className='!rounded-xl'>
@@ -351,8 +384,7 @@ const AccountManagement = ({
onLinuxDOOAuthClicked(status.linuxdo_client_id) onLinuxDOOAuthClicked(status.linuxdo_client_id)
} }
disabled={ disabled={
(userState.user && userState.user.linux_do_id !== '') || isBound(userState.user?.linux_do_id) || !status.linuxdo_oauth
!status.linuxdo_oauth
} }
> >
{status.linuxdo_oauth ? t('绑定') : t('未启用')} {status.linuxdo_oauth ? t('绑定') : t('未启用')}