Merge branch 'main' into traduction
# Conflicts: # web/src/i18n/locales/zh.json
This commit is contained in:
commit
51e02785e6
@ -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{
|
||||||
|
|||||||
@ -225,7 +225,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
|
|||||||
Quantity: stripe.Int64(amount),
|
Quantity: stripe.Int64(amount),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
||||||
|
AllowPromotionCodes: stripe.Bool(setting.StripePromotionCodesEnabled),
|
||||||
}
|
}
|
||||||
|
|
||||||
if "" == customerId {
|
if "" == customerId {
|
||||||
|
|||||||
@ -251,6 +251,7 @@ type GeminiChatTool struct {
|
|||||||
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
|
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
|
||||||
CodeExecution any `json:"codeExecution,omitempty"`
|
CodeExecution any `json:"codeExecution,omitempty"`
|
||||||
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
||||||
|
URLContext any `json:"urlContext,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GeminiChatGenerationConfig struct {
|
type GeminiChatGenerationConfig struct {
|
||||||
|
|||||||
18
main.go
18
main.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
"one-api/setting/ratio_setting"
|
"one-api/setting/ratio_setting"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/gopkg/util/gopool"
|
"github.com/bytedance/gopkg/util/gopool"
|
||||||
@ -147,6 +149,22 @@ func main() {
|
|||||||
})
|
})
|
||||||
server.Use(sessions.Sessions("session", store))
|
server.Use(sessions.Sessions("session", store))
|
||||||
|
|
||||||
|
analyticsInjectBuilder := &strings.Builder{}
|
||||||
|
if os.Getenv("UMAMI_WEBSITE_ID") != "" {
|
||||||
|
umamiSiteID := os.Getenv("UMAMI_WEBSITE_ID")
|
||||||
|
umamiScriptURL := os.Getenv("UMAMI_SCRIPT_URL")
|
||||||
|
if umamiScriptURL == "" {
|
||||||
|
umamiScriptURL = "https://analytics.umami.is/script.js"
|
||||||
|
}
|
||||||
|
analyticsInjectBuilder.WriteString("<script defer src=\"")
|
||||||
|
analyticsInjectBuilder.WriteString(umamiScriptURL)
|
||||||
|
analyticsInjectBuilder.WriteString("\" data-website-id=\"")
|
||||||
|
analyticsInjectBuilder.WriteString(umamiSiteID)
|
||||||
|
analyticsInjectBuilder.WriteString("\"></script>")
|
||||||
|
}
|
||||||
|
analyticsInject := analyticsInjectBuilder.String()
|
||||||
|
indexPage = bytes.ReplaceAll(indexPage, []byte("<analytics></analytics>\n"), []byte(analyticsInject))
|
||||||
|
|
||||||
router.SetRouter(server, buildFS, indexPage)
|
router.SetRouter(server, buildFS, indexPage)
|
||||||
var port = os.Getenv("PORT")
|
var port = os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
|
|||||||
@ -82,6 +82,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
||||||
common.OptionMap["StripePriceId"] = setting.StripePriceId
|
common.OptionMap["StripePriceId"] = setting.StripePriceId
|
||||||
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
|
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
|
||||||
|
common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled)
|
||||||
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||||
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
||||||
@ -330,6 +331,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
|
setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
|
||||||
case "StripeMinTopUp":
|
case "StripeMinTopUp":
|
||||||
setting.StripeMinTopUp, _ = strconv.Atoi(value)
|
setting.StripeMinTopUp, _ = strconv.Atoi(value)
|
||||||
|
case "StripePromotionCodesEnabled":
|
||||||
|
setting.StripePromotionCodesEnabled = value == "true"
|
||||||
case "TopupGroupRatio":
|
case "TopupGroupRatio":
|
||||||
err = common.UpdateTopupGroupRatioByJSONString(value)
|
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||||
case "GitHubClientId":
|
case "GitHubClientId":
|
||||||
|
|||||||
@ -245,6 +245,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
|||||||
functions := make([]dto.FunctionRequest, 0, len(textRequest.Tools))
|
functions := make([]dto.FunctionRequest, 0, len(textRequest.Tools))
|
||||||
googleSearch := false
|
googleSearch := false
|
||||||
codeExecution := false
|
codeExecution := false
|
||||||
|
urlContext := false
|
||||||
for _, tool := range textRequest.Tools {
|
for _, tool := range textRequest.Tools {
|
||||||
if tool.Function.Name == "googleSearch" {
|
if tool.Function.Name == "googleSearch" {
|
||||||
googleSearch = true
|
googleSearch = true
|
||||||
@ -254,6 +255,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
|||||||
codeExecution = true
|
codeExecution = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if tool.Function.Name == "urlContext" {
|
||||||
|
urlContext = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
if tool.Function.Parameters != nil {
|
if tool.Function.Parameters != nil {
|
||||||
|
|
||||||
params, ok := tool.Function.Parameters.(map[string]interface{})
|
params, ok := tool.Function.Parameters.(map[string]interface{})
|
||||||
@ -281,6 +286,11 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
|
|||||||
GoogleSearch: make(map[string]string),
|
GoogleSearch: make(map[string]string),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if urlContext {
|
||||||
|
geminiTools = append(geminiTools, dto.GeminiChatTool{
|
||||||
|
URLContext: make(map[string]string),
|
||||||
|
})
|
||||||
|
}
|
||||||
if len(functions) > 0 {
|
if len(functions) > 0 {
|
||||||
geminiTools = append(geminiTools, dto.GeminiChatTool{
|
geminiTools = append(geminiTools, dto.GeminiChatTool{
|
||||||
FunctionDeclarations: functions,
|
FunctionDeclarations: functions,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -5,3 +5,4 @@ var StripeWebhookSecret = ""
|
|||||||
var StripePriceId = ""
|
var StripePriceId = ""
|
||||||
var StripeUnitPrice = 8.0
|
var StripeUnitPrice = 8.0
|
||||||
var StripeMinTopUp = 1
|
var StripeMinTopUp = 1
|
||||||
|
var StripePromotionCodesEnabled = false
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
||||||
/>
|
/>
|
||||||
<title>New API</title>
|
<title>New API</title>
|
||||||
|
<analytics></analytics>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -45,6 +45,7 @@ const PaymentSetting = () => {
|
|||||||
StripePriceId: '',
|
StripePriceId: '',
|
||||||
StripeUnitPrice: 8.0,
|
StripeUnitPrice: 8.0,
|
||||||
StripeMinTopUp: 1,
|
StripeMinTopUp: 1,
|
||||||
|
StripePromotionCodesEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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,9 +169,11 @@ const AccountManagement = ({
|
|||||||
{t('微信')}
|
{t('微信')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm text-gray-500 truncate'>
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
{userState.user && userState.user.wechat_id !== ''
|
{!status.wechat_login
|
||||||
? t('已绑定')
|
? t('未启用')
|
||||||
: t('未绑定')}
|
: isBound(userState.user?.wechat_id)
|
||||||
|
? t('已绑定')
|
||||||
|
: t('未绑定')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -179,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('绑定')
|
||||||
@ -220,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('未启用')}
|
||||||
@ -264,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('未启用')}
|
||||||
@ -298,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'>
|
||||||
@ -350,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('未启用')}
|
||||||
|
|||||||
@ -455,6 +455,14 @@ const EditChannelModal = (props) => {
|
|||||||
data.is_enterprise_account = false;
|
data.is_enterprise_account = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.type === 45 &&
|
||||||
|
(!data.base_url ||
|
||||||
|
(typeof data.base_url === 'string' && data.base_url.trim() === ''))
|
||||||
|
) {
|
||||||
|
data.base_url = 'https://ark.cn-beijing.volces.com';
|
||||||
|
}
|
||||||
|
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
if (formApiRef.current) {
|
if (formApiRef.current) {
|
||||||
formApiRef.current.setValues(data);
|
formApiRef.current.setValues(data);
|
||||||
@ -837,7 +845,9 @@ const EditChannelModal = (props) => {
|
|||||||
delete localInputs.key;
|
delete localInputs.key;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
localInputs.key = batch ? JSON.stringify(keys) : JSON.stringify(keys[0]);
|
localInputs.key = batch
|
||||||
|
? JSON.stringify(keys)
|
||||||
|
: JSON.stringify(keys[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -954,6 +964,56 @@ const EditChannelModal = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 密钥去重函数
|
||||||
|
const deduplicateKeys = () => {
|
||||||
|
const currentKey = formApiRef.current?.getValue('key') || inputs.key || '';
|
||||||
|
|
||||||
|
if (!currentKey.trim()) {
|
||||||
|
showInfo(t('请先输入密钥'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按行分割密钥
|
||||||
|
const keyLines = currentKey.split('\n');
|
||||||
|
const beforeCount = keyLines.length;
|
||||||
|
|
||||||
|
// 使用哈希表去重,保持原有顺序
|
||||||
|
const keySet = new Set();
|
||||||
|
const deduplicatedKeys = [];
|
||||||
|
|
||||||
|
keyLines.forEach((line) => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine && !keySet.has(trimmedLine)) {
|
||||||
|
keySet.add(trimmedLine);
|
||||||
|
deduplicatedKeys.push(trimmedLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterCount = deduplicatedKeys.length;
|
||||||
|
const deduplicatedKeyText = deduplicatedKeys.join('\n');
|
||||||
|
|
||||||
|
// 更新表单和状态
|
||||||
|
if (formApiRef.current) {
|
||||||
|
formApiRef.current.setValue('key', deduplicatedKeyText);
|
||||||
|
}
|
||||||
|
handleInputChange('key', deduplicatedKeyText);
|
||||||
|
|
||||||
|
// 显示去重结果
|
||||||
|
const message = t(
|
||||||
|
'去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥',
|
||||||
|
{
|
||||||
|
before: beforeCount,
|
||||||
|
after: afterCount,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (beforeCount === afterCount) {
|
||||||
|
showInfo(t('未发现重复密钥'));
|
||||||
|
} else {
|
||||||
|
showSuccess(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const addCustomModels = () => {
|
const addCustomModels = () => {
|
||||||
if (customModel.trim() === '') return;
|
if (customModel.trim() === '') return;
|
||||||
const modelArray = customModel.split(',').map((model) => model.trim());
|
const modelArray = customModel.split(',').map((model) => model.trim());
|
||||||
@ -1049,24 +1109,41 @@ const EditChannelModal = (props) => {
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
)}
|
||||||
{batch && (
|
{batch && (
|
||||||
<Checkbox
|
<>
|
||||||
disabled={isEdit}
|
<Checkbox
|
||||||
checked={multiToSingle}
|
disabled={isEdit}
|
||||||
onChange={() => {
|
checked={multiToSingle}
|
||||||
setMultiToSingle((prev) => !prev);
|
onChange={() => {
|
||||||
setInputs((prev) => {
|
setMultiToSingle((prev) => {
|
||||||
const newInputs = { ...prev };
|
const nextValue = !prev;
|
||||||
if (!multiToSingle) {
|
setInputs((prevInputs) => {
|
||||||
newInputs.multi_key_mode = multiKeyMode;
|
const newInputs = { ...prevInputs };
|
||||||
} else {
|
if (nextValue) {
|
||||||
delete newInputs.multi_key_mode;
|
newInputs.multi_key_mode = multiKeyMode;
|
||||||
}
|
} else {
|
||||||
return newInputs;
|
delete newInputs.multi_key_mode;
|
||||||
});
|
}
|
||||||
}}
|
return newInputs;
|
||||||
>
|
});
|
||||||
{t('密钥聚合模式')}
|
return nextValue;
|
||||||
</Checkbox>
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('密钥聚合模式')}
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
{inputs.type !== 41 && (
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
theme='outline'
|
||||||
|
onClick={deduplicateKeys}
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
{t('密钥去重')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
) : null;
|
) : null;
|
||||||
@ -1268,7 +1345,10 @@ const EditChannelModal = (props) => {
|
|||||||
value={inputs.vertex_key_type || 'json'}
|
value={inputs.vertex_key_type || 'json'}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
// 更新设置中的 vertex_key_type
|
// 更新设置中的 vertex_key_type
|
||||||
handleChannelOtherSettingsChange('vertex_key_type', value);
|
handleChannelOtherSettingsChange(
|
||||||
|
'vertex_key_type',
|
||||||
|
value,
|
||||||
|
);
|
||||||
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
|
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
|
||||||
if (value === 'api_key') {
|
if (value === 'api_key') {
|
||||||
setBatch(false);
|
setBatch(false);
|
||||||
@ -1288,7 +1368,8 @@ const EditChannelModal = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{batch ? (
|
{batch ? (
|
||||||
inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? (
|
inputs.type === 41 &&
|
||||||
|
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||||
<Form.Upload
|
<Form.Upload
|
||||||
field='vertex_files'
|
field='vertex_files'
|
||||||
label={t('密钥文件 (.json)')}
|
label={t('密钥文件 (.json)')}
|
||||||
@ -1324,7 +1405,7 @@ const EditChannelModal = (props) => {
|
|||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
onChange={(value) => handleInputChange('key', value)}
|
onChange={(value) => handleInputChange('key', value)}
|
||||||
extraText={
|
extraText={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
{isEdit &&
|
{isEdit &&
|
||||||
isMultiKeyChannel &&
|
isMultiKeyChannel &&
|
||||||
keyMode === 'append' && (
|
keyMode === 'append' && (
|
||||||
@ -1352,7 +1433,8 @@ const EditChannelModal = (props) => {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? (
|
{inputs.type === 41 &&
|
||||||
|
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||||
<>
|
<>
|
||||||
{!batch && (
|
{!batch && (
|
||||||
<div className='flex items-center justify-between mb-3'>
|
<div className='flex items-center justify-between mb-3'>
|
||||||
|
|||||||
@ -17,8 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal } from '@douyinfe/semi-ui';
|
import { Modal, Button, Typography, Spin } from '@douyinfe/semi-ui';
|
||||||
|
import { IconExternalOpen, IconCopy } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
const ContentModal = ({
|
const ContentModal = ({
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
@ -26,17 +29,120 @@ const ContentModal = ({
|
|||||||
modalContent,
|
modalContent,
|
||||||
isVideo,
|
isVideo,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [videoError, setVideoError] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpen && isVideo) {
|
||||||
|
setVideoError(false);
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
}, [isModalOpen, isVideo]);
|
||||||
|
|
||||||
|
const handleVideoError = () => {
|
||||||
|
setVideoError(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVideoLoaded = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyUrl = () => {
|
||||||
|
navigator.clipboard.writeText(modalContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenInNewTab = () => {
|
||||||
|
window.open(modalContent, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderVideoContent = () => {
|
||||||
|
if (videoError) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px' }}>
|
||||||
|
视频无法在当前浏览器中播放,这可能是由于:
|
||||||
|
</Text>
|
||||||
|
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
|
||||||
|
• 视频服务商的跨域限制
|
||||||
|
</Text>
|
||||||
|
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
|
||||||
|
• 需要特定的请求头或认证
|
||||||
|
</Text>
|
||||||
|
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px', fontSize: '12px' }}>
|
||||||
|
• 防盗链保护机制
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '20px' }}>
|
||||||
|
<Button
|
||||||
|
icon={<IconExternalOpen />}
|
||||||
|
onClick={handleOpenInNewTab}
|
||||||
|
style={{ marginRight: '8px' }}
|
||||||
|
>
|
||||||
|
在新标签页中打开
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<IconCopy />}
|
||||||
|
onClick={handleCopyUrl}
|
||||||
|
>
|
||||||
|
复制链接
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '16px', padding: '8px', backgroundColor: '#f8f9fa', borderRadius: '4px' }}>
|
||||||
|
<Text
|
||||||
|
type="tertiary"
|
||||||
|
style={{ fontSize: '10px', wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{modalContent}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
{isLoading && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
zIndex: 10
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<video
|
||||||
|
src={modalContent}
|
||||||
|
controls
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
autoPlay
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
onError={handleVideoError}
|
||||||
|
onLoadedData={handleVideoLoaded}
|
||||||
|
onLoadStart={() => setIsLoading(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={isModalOpen}
|
visible={isModalOpen}
|
||||||
onOk={() => setIsModalOpen(false)}
|
onOk={() => setIsModalOpen(false)}
|
||||||
onCancel={() => setIsModalOpen(false)}
|
onCancel={() => setIsModalOpen(false)}
|
||||||
closable={null}
|
closable={null}
|
||||||
bodyStyle={{ height: '400px', overflow: 'auto' }}
|
bodyStyle={{
|
||||||
|
height: isVideo ? '450px' : '400px',
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: isVideo && videoError ? '0' : '24px'
|
||||||
|
}}
|
||||||
width={800}
|
width={800}
|
||||||
>
|
>
|
||||||
{isVideo ? (
|
{isVideo ? (
|
||||||
<video src={modalContent} controls style={{ width: '100%' }} autoPlay />
|
renderVideoContent()
|
||||||
) : (
|
) : (
|
||||||
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
|
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -837,6 +837,7 @@
|
|||||||
"确定要充值 $": "Confirm to top up $",
|
"确定要充值 $": "Confirm to top up $",
|
||||||
"微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:",
|
"微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:",
|
||||||
"Stripe 实付金额:": "Stripe actual payment amount:",
|
"Stripe 实付金额:": "Stripe actual payment amount:",
|
||||||
|
"允许在 Stripe 支付中输入促销码": "Allow entering promotion codes during Stripe checkout",
|
||||||
"支付中...": "Paying",
|
"支付中...": "Paying",
|
||||||
"支付宝": "Alipay",
|
"支付宝": "Alipay",
|
||||||
"收益统计": "Income statistics",
|
"收益统计": "Income statistics",
|
||||||
|
|||||||
@ -35,5 +35,6 @@
|
|||||||
"域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
|
"域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
|
||||||
"common": {
|
"common": {
|
||||||
"changeLanguage": "切换语言"
|
"changeLanguage": "切换语言"
|
||||||
}
|
},
|
||||||
|
"允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
StripePriceId: '',
|
StripePriceId: '',
|
||||||
StripeUnitPrice: 8.0,
|
StripeUnitPrice: 8.0,
|
||||||
StripeMinTopUp: 1,
|
StripeMinTopUp: 1,
|
||||||
|
StripePromotionCodesEnabled: false,
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
props.options.StripeMinTopUp !== undefined
|
props.options.StripeMinTopUp !== undefined
|
||||||
? parseFloat(props.options.StripeMinTopUp)
|
? parseFloat(props.options.StripeMinTopUp)
|
||||||
: 1,
|
: 1,
|
||||||
|
StripePromotionCodesEnabled:
|
||||||
|
props.options.StripePromotionCodesEnabled !== undefined
|
||||||
|
? props.options.StripePromotionCodesEnabled
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
setInputs(currentInputs);
|
setInputs(currentInputs);
|
||||||
setOriginInputs({ ...currentInputs });
|
setOriginInputs({ ...currentInputs });
|
||||||
@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
value: inputs.StripeMinTopUp.toString(),
|
value: inputs.StripeMinTopUp.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
originInputs['StripePromotionCodesEnabled'] !==
|
||||||
|
inputs.StripePromotionCodesEnabled &&
|
||||||
|
inputs.StripePromotionCodesEnabled !== undefined
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
key: 'StripePromotionCodesEnabled',
|
||||||
|
value: inputs.StripePromotionCodesEnabled ? 'true' : 'false',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
const requestQueue = options.map((opt) =>
|
const requestQueue = options.map((opt) =>
|
||||||
@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
placeholder={t('例如:2,就是最低充值2$')}
|
placeholder={t('例如:2,就是最低充值2$')}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field='StripePromotionCodesEnabled'
|
||||||
|
size='default'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
label={t('允许在 Stripe 支付中输入促销码')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Button onClick={submitStripeSetting}>{t('更新 Stripe 设置')}</Button>
|
<Button onClick={submitStripeSetting}>{t('更新 Stripe 设置')}</Button>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user