Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
51e02785
authored
Sep 29, 2025
by
comeback01
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' into traduction
# Conflicts: # web/src/i18n/locales/zh.json
parents
cbbe4cb4
78b9b21a
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
413 additions
and
64 deletions
+413
-64
controller/channel.go
+3
-0
controller/topup_stripe.go
+2
-1
dto/gemini.go
+1
-0
main.go
+18
-0
model/option.go
+3
-0
relay/channel/gemini/relay-gemini.go
+10
-0
service/http_client.go
+39
-5
setting/payment_stripe.go
+1
-0
web/index.html
+1
-0
web/src/components/settings/PaymentSetting.jsx
+1
-0
web/src/components/settings/PersonalSetting.jsx
+39
-10
web/src/components/settings/personal/cards/AccountManagement.jsx
+53
-20
web/src/components/table/channels/modals/EditChannelModal.jsx
+105
-23
web/src/components/table/task-logs/modals/ContentModal.jsx
+110
-4
web/src/i18n/locales/en.json
+1
-0
web/src/i18n/locales/zh.json
+2
-1
web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
+24
-0
No files found.
controller/channel.go
View file @
51e02785
...
...
@@ -8,6 +8,7 @@ import (
"one-api/constant"
"one-api/dto"
"one-api/model"
"one-api/service"
"strconv"
"strings"
...
...
@@ -633,6 +634,7 @@ func AddChannel(c *gin.Context) {
common
.
ApiError
(
c
,
err
)
return
}
service
.
ResetProxyClientCache
()
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
...
...
@@ -894,6 +896,7 @@ func UpdateChannel(c *gin.Context) {
return
}
model
.
InitChannelCache
()
service
.
ResetProxyClientCache
()
channel
.
Key
=
""
clearChannelInfo
(
&
channel
.
Channel
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
...
...
controller/topup_stripe.go
View file @
51e02785
...
...
@@ -225,7 +225,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
Quantity
:
stripe
.
Int64
(
amount
),
},
},
Mode
:
stripe
.
String
(
string
(
stripe
.
CheckoutSessionModePayment
)),
Mode
:
stripe
.
String
(
string
(
stripe
.
CheckoutSessionModePayment
)),
AllowPromotionCodes
:
stripe
.
Bool
(
setting
.
StripePromotionCodesEnabled
),
}
if
""
==
customerId
{
...
...
dto/gemini.go
View file @
51e02785
...
...
@@ -251,6 +251,7 @@ type GeminiChatTool struct {
GoogleSearchRetrieval
any
`json:"googleSearchRetrieval,omitempty"`
CodeExecution
any
`json:"codeExecution,omitempty"`
FunctionDeclarations
any
`json:"functionDeclarations,omitempty"`
URLContext
any
`json:"urlContext,omitempty"`
}
type
GeminiChatGenerationConfig
struct
{
...
...
main.go
View file @
51e02785
package
main
import
(
"bytes"
"embed"
"fmt"
"log"
...
...
@@ -16,6 +17,7 @@ import (
"one-api/setting/ratio_setting"
"os"
"strconv"
"strings"
"time"
"github.com/bytedance/gopkg/util/gopool"
...
...
@@ -147,6 +149,22 @@ func main() {
})
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
)
var
port
=
os
.
Getenv
(
"PORT"
)
if
port
==
""
{
...
...
model/option.go
View file @
51e02785
...
...
@@ -82,6 +82,7 @@ func InitOptionMap() {
common
.
OptionMap
[
"StripeWebhookSecret"
]
=
setting
.
StripeWebhookSecret
common
.
OptionMap
[
"StripePriceId"
]
=
setting
.
StripePriceId
common
.
OptionMap
[
"StripeUnitPrice"
]
=
strconv
.
FormatFloat
(
setting
.
StripeUnitPrice
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"StripePromotionCodesEnabled"
]
=
strconv
.
FormatBool
(
setting
.
StripePromotionCodesEnabled
)
common
.
OptionMap
[
"TopupGroupRatio"
]
=
common
.
TopupGroupRatio2JSONString
()
common
.
OptionMap
[
"Chats"
]
=
setting
.
Chats2JsonString
()
common
.
OptionMap
[
"AutoGroups"
]
=
setting
.
AutoGroups2JsonString
()
...
...
@@ -330,6 +331,8 @@ func updateOptionMap(key string, value string) (err error) {
setting
.
StripeUnitPrice
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
case
"StripeMinTopUp"
:
setting
.
StripeMinTopUp
,
_
=
strconv
.
Atoi
(
value
)
case
"StripePromotionCodesEnabled"
:
setting
.
StripePromotionCodesEnabled
=
value
==
"true"
case
"TopupGroupRatio"
:
err
=
common
.
UpdateTopupGroupRatioByJSONString
(
value
)
case
"GitHubClientId"
:
...
...
relay/channel/gemini/relay-gemini.go
View file @
51e02785
...
...
@@ -245,6 +245,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
functions
:=
make
([]
dto
.
FunctionRequest
,
0
,
len
(
textRequest
.
Tools
))
googleSearch
:=
false
codeExecution
:=
false
urlContext
:=
false
for
_
,
tool
:=
range
textRequest
.
Tools
{
if
tool
.
Function
.
Name
==
"googleSearch"
{
googleSearch
=
true
...
...
@@ -254,6 +255,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
codeExecution
=
true
continue
}
if
tool
.
Function
.
Name
==
"urlContext"
{
urlContext
=
true
continue
}
if
tool
.
Function
.
Parameters
!=
nil
{
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
),
})
}
if
urlContext
{
geminiTools
=
append
(
geminiTools
,
dto
.
GeminiChatTool
{
URLContext
:
make
(
map
[
string
]
string
),
})
}
if
len
(
functions
)
>
0
{
geminiTools
=
append
(
geminiTools
,
dto
.
GeminiChatTool
{
FunctionDeclarations
:
functions
,
...
...
service/http_client.go
View file @
51e02785
...
...
@@ -7,12 +7,17 @@ import (
"net/http"
"net/url"
"one-api/common"
"sync"
"time"
"golang.org/x/net/proxy"
)
var
httpClient
*
http
.
Client
var
(
httpClient
*
http
.
Client
proxyClientLock
sync
.
Mutex
proxyClients
=
make
(
map
[
string
]
*
http
.
Client
)
)
func
InitHttpClient
()
{
if
common
.
RelayTimeout
==
0
{
...
...
@@ -28,12 +33,31 @@ func GetHttpClient() *http.Client {
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 客户端
func
NewProxyHttpClient
(
proxyURL
string
)
(
*
http
.
Client
,
error
)
{
if
proxyURL
==
""
{
return
http
.
DefaultClient
,
nil
}
proxyClientLock
.
Lock
()
if
client
,
ok
:=
proxyClients
[
proxyURL
];
ok
{
proxyClientLock
.
Unlock
()
return
client
,
nil
}
proxyClientLock
.
Unlock
()
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
)
if
err
!=
nil
{
return
nil
,
err
...
...
@@ -41,11 +65,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
switch
parsedURL
.
Scheme
{
case
"http"
,
"https"
:
return
&
http
.
Client
{
client
:=
&
http
.
Client
{
Transport
:
&
http
.
Transport
{
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"
:
// 获取认证信息
...
...
@@ -67,13 +96,18 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
return
nil
,
err
}
return
&
http
.
Client
{
client
:=
&
http
.
Client
{
Transport
:
&
http
.
Transport
{
DialContext
:
func
(
ctx
context
.
Context
,
network
,
addr
string
)
(
net
.
Conn
,
error
)
{
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
:
return
nil
,
fmt
.
Errorf
(
"unsupported proxy scheme: %s"
,
parsedURL
.
Scheme
)
...
...
setting/payment_stripe.go
View file @
51e02785
...
...
@@ -5,3 +5,4 @@ var StripeWebhookSecret = ""
var
StripePriceId
=
""
var
StripeUnitPrice
=
8.0
var
StripeMinTopUp
=
1
var
StripePromotionCodesEnabled
=
false
web/index.html
View file @
51e02785
...
...
@@ -10,6 +10,7 @@
content=
"OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
/>
<title>
New API
</title>
<analytics></analytics>
</head>
<body>
...
...
web/src/components/settings/PaymentSetting.jsx
View file @
51e02785
...
...
@@ -45,6 +45,7 @@ const PaymentSetting = () => {
StripePriceId
:
''
,
StripeUnitPrice
:
8.0
,
StripeMinTopUp
:
1
,
StripePromotionCodesEnabled
:
false
,
});
let
[
loading
,
setLoading
]
=
useState
(
false
);
...
...
web/src/components/settings/PersonalSetting.jsx
View file @
51e02785
...
...
@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
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
{
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
useTranslation
}
from
'react-i18next'
;
...
...
@@ -71,18 +78,40 @@ const PersonalSetting = () => {
});
useEffect
(()
=>
{
let
s
tatus
=
localStorage
.
getItem
(
'status'
);
if
(
s
tatus
)
{
status
=
JSON
.
parse
(
status
);
setStatus
(
status
);
if
(
status
.
turnstile_check
)
{
let
s
aved
=
localStorage
.
getItem
(
'status'
);
if
(
s
aved
)
{
const
parsed
=
JSON
.
parse
(
saved
);
setStatus
(
parsed
);
if
(
parsed
.
turnstile_check
)
{
setTurnstileEnabled
(
true
);
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
setTurnstileSiteKey
(
parsed
.
turnstile_site_key
);
}
else
{
setTurnstileEnabled
(
false
);
setTurnstileSiteKey
(
''
);
}
}
getUserData
().
then
((
res
)
=>
{
console
.
log
(
userState
);
});
// Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
(
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
(()
=>
{
...
...
web/src/components/settings/personal/cards/AccountManagement.jsx
View file @
51e02785
...
...
@@ -28,6 +28,7 @@ import {
Tabs
,
TabPane
,
Popover
,
Modal
,
}
from
'@douyinfe/semi-ui'
;
import
{
IconMail
,
...
...
@@ -83,6 +84,9 @@ const AccountManagement = ({
</
Popover
>
);
};
const
isBound
=
(
accountId
)
=>
Boolean
(
accountId
);
const
[
showTelegramBindModal
,
setShowTelegramBindModal
]
=
React
.
useState
(
false
);
return
(
<
Card
className=
'!rounded-2xl'
>
{
/* 卡片头部 */
}
...
...
@@ -142,7 +146,7 @@ const AccountManagement = ({
size=
'small'
onClick=
{
()
=>
setShowEmailBindModal
(
true
)
}
>
{
userState
.
user
&&
userState
.
user
.
email
!==
''
{
isBound
(
userState
.
user
?.
email
)
?
t
(
'修改绑定'
)
:
t
(
'绑定'
)
}
</
Button
>
...
...
@@ -165,9 +169,11 @@ const AccountManagement = ({
{
t
(
'微信'
)
}
</
div
>
<
div
className=
'text-sm text-gray-500 truncate'
>
{
userState
.
user
&&
userState
.
user
.
wechat_id
!==
''
?
t
(
'已绑定'
)
:
t
(
'未绑定'
)
}
{
!
status
.
wechat_login
?
t
(
'未启用'
)
:
isBound
(
userState
.
user
?.
wechat_id
)
?
t
(
'已绑定'
)
:
t
(
'未绑定'
)
}
</
div
>
</
div
>
</
div
>
...
...
@@ -179,7 +185,7 @@ const AccountManagement = ({
disabled=
{
!
status
.
wechat_login
}
onClick=
{
()
=>
setShowWeChatBindModal
(
true
)
}
>
{
userState
.
user
&&
userState
.
user
.
wechat_id
!==
''
{
isBound
(
userState
.
user
?.
wechat_id
)
?
t
(
'修改绑定'
)
:
status
.
wechat_login
?
t
(
'绑定'
)
...
...
@@ -220,8 +226,7 @@ const AccountManagement = ({
onGitHubOAuthClicked
(
status
.
github_client_id
)
}
disabled=
{
(
userState
.
user
&&
userState
.
user
.
github_id
!==
''
)
||
!
status
.
github_oauth
isBound
(
userState
.
user
?.
github_id
)
||
!
status
.
github_oauth
}
>
{
status
.
github_oauth
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
...
...
@@ -264,8 +269,7 @@ const AccountManagement = ({
)
}
disabled=
{
(
userState
.
user
&&
userState
.
user
.
oidc_id
!==
''
)
||
!
status
.
oidc_enabled
isBound
(
userState
.
user
?.
oidc_id
)
||
!
status
.
oidc_enabled
}
>
{
status
.
oidc_enabled
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
...
...
@@ -298,26 +302,56 @@ const AccountManagement = ({
</
div
>
<
div
className=
'flex-shrink-0'
>
{
status
.
telegram_oauth
?
(
userState
.
user
.
telegram_id
!==
''
?
(
<
Button
disabled=
{
true
}
size=
'small'
>
isBound
(
userState
.
user
?.
telegram_id
)
?
(
<
Button
disabled
size=
'small'
type=
'primary'
theme=
'outline'
>
{
t
(
'已绑定'
)
}
</
Button
>
)
:
(
<
div
className=
'scale-75'
>
<
TelegramLoginButton
dataAuthUrl=
'/api/oauth/telegram/bind'
botName=
{
status
.
telegram_bot_name
}
/>
</
div
>
<
Button
type=
'primary'
theme=
'outline'
size=
'small'
onClick=
{
()
=>
setShowTelegramBindModal
(
true
)
}
>
{
t
(
'绑定'
)
}
</
Button
>
)
)
:
(
<
Button
disabled=
{
true
}
size=
'small'
>
<
Button
disabled
size=
'small'
type=
'primary'
theme=
'outline'
>
{
t
(
'未启用'
)
}
</
Button
>
)
}
</
div
>
</
div
>
</
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绑定 */
}
<
Card
className=
'!rounded-xl'
>
...
...
@@ -350,8 +384,7 @@ const AccountManagement = ({
onLinuxDOOAuthClicked
(
status
.
linuxdo_client_id
)
}
disabled=
{
(
userState
.
user
&&
userState
.
user
.
linux_do_id
!==
''
)
||
!
status
.
linuxdo_oauth
isBound
(
userState
.
user
?.
linux_do_id
)
||
!
status
.
linuxdo_oauth
}
>
{
status
.
linuxdo_oauth
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
51e02785
...
...
@@ -455,6 +455,14 @@ const EditChannelModal = (props) => {
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
);
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValues
(
data
);
...
...
@@ -837,7 +845,9 @@ const EditChannelModal = (props) => {
delete
localInputs
.
key
;
}
}
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
=
()
=>
{
if
(
customModel
.
trim
()
===
''
)
return
;
const
modelArray
=
customModel
.
split
(
','
).
map
((
model
)
=>
model
.
trim
());
...
...
@@ -1049,24 +1109,41 @@ const EditChannelModal = (props) => {
</
Checkbox
>
)
}
{
batch
&&
(
<
Checkbox
disabled=
{
isEdit
}
checked=
{
multiToSingle
}
onChange=
{
()
=>
{
setMultiToSingle
((
prev
)
=>
!
prev
);
setInputs
((
prev
)
=>
{
const
newInputs
=
{
...
prev
};
if
(
!
multiToSingle
)
{
newInputs
.
multi_key_mode
=
multiKeyMode
;
}
else
{
delete
newInputs
.
multi_key_mode
;
}
return
newInputs
;
});
}
}
>
{
t
(
'密钥聚合模式'
)
}
</
Checkbox
>
<>
<
Checkbox
disabled=
{
isEdit
}
checked=
{
multiToSingle
}
onChange=
{
()
=>
{
setMultiToSingle
((
prev
)
=>
{
const
nextValue
=
!
prev
;
setInputs
((
prevInputs
)
=>
{
const
newInputs
=
{
...
prevInputs
};
if
(
nextValue
)
{
newInputs
.
multi_key_mode
=
multiKeyMode
;
}
else
{
delete
newInputs
.
multi_key_mode
;
}
return
newInputs
;
});
return
nextValue
;
});
}
}
>
{
t
(
'密钥聚合模式'
)
}
</
Checkbox
>
{
inputs
.
type
!==
41
&&
(
<
Button
size=
'small'
type=
'tertiary'
theme=
'outline'
onClick=
{
deduplicateKeys
}
style=
{
{
textDecoration
:
'underline'
}
}
>
{
t
(
'密钥去重'
)
}
</
Button
>
)
}
</>
)
}
</
Space
>
)
:
null
;
...
...
@@ -1268,7 +1345,10 @@ const EditChannelModal = (props) => {
value=
{
inputs
.
vertex_key_type
||
'json'
}
onChange=
{
(
value
)
=>
{
// 更新设置中的 vertex_key_type
handleChannelOtherSettingsChange
(
'vertex_key_type'
,
value
);
handleChannelOtherSettingsChange
(
'vertex_key_type'
,
value
,
);
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
if
(
value
===
'api_key'
)
{
setBatch
(
false
);
...
...
@@ -1288,7 +1368,8 @@ const EditChannelModal = (props) => {
/>
)
}
{
batch
?
(
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<
Form
.
Upload
field=
'vertex_files'
label=
{
t
(
'密钥文件 (.json)'
)
}
...
...
@@ -1324,7 +1405,7 @@ const EditChannelModal = (props) => {
autoComplete=
'new-password'
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
extraText=
{
<
div
className=
'flex items-center gap-2'
>
<
div
className=
'flex items-center gap-2
flex-wrap
'
>
{
isEdit
&&
isMultiKeyChannel
&&
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
&&
(
<
div
className=
'flex items-center justify-between mb-3'
>
...
...
web/src/components/table/task-logs/modals/ContentModal.jsx
View file @
51e02785
...
...
@@ -17,8 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
React
from
'react'
;
import
{
Modal
}
from
'@douyinfe/semi-ui'
;
import
React
,
{
useState
,
useEffect
}
from
'react'
;
import
{
Modal
,
Button
,
Typography
,
Spin
}
from
'@douyinfe/semi-ui'
;
import
{
IconExternalOpen
,
IconCopy
}
from
'@douyinfe/semi-icons'
;
const
{
Text
}
=
Typography
;
const
ContentModal
=
({
isModalOpen
,
...
...
@@ -26,17 +29,120 @@ const ContentModal = ({
modalContent
,
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
(
<
Modal
visible=
{
isModalOpen
}
onOk=
{
()
=>
setIsModalOpen
(
false
)
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
closable=
{
null
}
bodyStyle=
{
{
height
:
'400px'
,
overflow
:
'auto'
}
}
bodyStyle=
{
{
height
:
isVideo
?
'450px'
:
'400px'
,
overflow
:
'auto'
,
padding
:
isVideo
&&
videoError
?
'0'
:
'24px'
}
}
width=
{
800
}
>
{
isVideo
?
(
<
video
src=
{
modalContent
}
controls
style=
{
{
width
:
'100%'
}
}
autoPlay
/>
renderVideoContent
()
)
:
(
<
p
style=
{
{
whiteSpace
:
'pre-line'
}
}
>
{
modalContent
}
</
p
>
)
}
...
...
web/src/i18n/locales/en.json
View file @
51e02785
...
...
@@ -837,6 +837,7 @@
"确定要充值 $"
:
"Confirm to top up $"
,
"微信/支付宝 实付金额:"
:
"WeChat/Alipay actual payment amount:"
,
"Stripe 实付金额:"
:
"Stripe actual payment amount:"
,
"允许在 Stripe 支付中输入促销码"
:
"Allow entering promotion codes during Stripe checkout"
,
"支付中..."
:
"Paying"
,
"支付宝"
:
"Alipay"
,
"收益统计"
:
"Income statistics"
,
...
...
web/src/i18n/locales/zh.json
View file @
51e02785
...
...
@@ -35,5 +35,6 @@
"域名IP过滤详细说明"
:
"⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。"
,
"common"
:
{
"changeLanguage"
:
"切换语言"
}
},
"允许在 Stripe 支付中输入促销码"
:
"允许在 Stripe 支付中输入促销码"
}
web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
View file @
51e02785
...
...
@@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
StripePriceId
:
''
,
StripeUnitPrice
:
8.0
,
StripeMinTopUp
:
1
,
StripePromotionCodesEnabled
:
false
,
});
const
[
originInputs
,
setOriginInputs
]
=
useState
({});
const
formApiRef
=
useRef
(
null
);
...
...
@@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
props
.
options
.
StripeMinTopUp
!==
undefined
?
parseFloat
(
props
.
options
.
StripeMinTopUp
)
:
1
,
StripePromotionCodesEnabled
:
props
.
options
.
StripePromotionCodesEnabled
!==
undefined
?
props
.
options
.
StripePromotionCodesEnabled
:
false
,
};
setInputs
(
currentInputs
);
setOriginInputs
({
...
currentInputs
});
...
...
@@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
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
)
=>
...
...
@@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
placeholder=
{
t
(
'例如:2,就是最低充值2$'
)
}
/>
</
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
>
<
Button
onClick=
{
submitStripeSetting
}
>
{
t
(
'更新 Stripe 设置'
)
}
</
Button
>
</
Form
.
Section
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment