Merge branch 'main' into alpha
This commit is contained in:
21
.github/workflows/pr-target-branch-check.yml
vendored
21
.github/workflows/pr-target-branch-check.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Check PR Branching Strategy
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, edited]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-branching-strategy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Enforce branching strategy
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.base_ref }}" == "main" ]]; then
|
|
||||||
if [[ "${{ github.head_ref }}" != "alpha" ]]; then
|
|
||||||
echo "Error: Pull requests to 'main' are only allowed from the 'alpha' branch."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.base_ref }}" != "alpha" ]]; then
|
|
||||||
echo "Error: Pull requests must be targeted to the 'alpha' or 'main' branch."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Branching strategy check passed."
|
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
"one-api/setting"
|
"one-api/setting/operation_setting"
|
||||||
"one-api/types"
|
"one-api/types"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -342,7 +342,7 @@ func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) {
|
|||||||
return 0, fmt.Errorf("failed to update moonshot balance, status: %v, code: %d, scode: %s", response.Status, response.Code, response.Scode)
|
return 0, fmt.Errorf("failed to update moonshot balance, status: %v, code: %d, scode: %s", response.Status, response.Code, response.Scode)
|
||||||
}
|
}
|
||||||
availableBalanceCny := response.Data.AvailableBalance
|
availableBalanceCny := response.Data.AvailableBalance
|
||||||
availableBalanceUsd := decimal.NewFromFloat(availableBalanceCny).Div(decimal.NewFromFloat(setting.Price)).InexactFloat64()
|
availableBalanceUsd := decimal.NewFromFloat(availableBalanceCny).Div(decimal.NewFromFloat(operation_setting.Price)).InexactFloat64()
|
||||||
channel.UpdateBalance(availableBalanceUsd)
|
channel.UpdateBalance(availableBalanceUsd)
|
||||||
return availableBalanceUsd, nil
|
return availableBalanceUsd, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
|
"one-api/dto"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -560,7 +561,7 @@ func AddChannel(c *gin.Context) {
|
|||||||
case "multi_to_single":
|
case "multi_to_single":
|
||||||
addChannelRequest.Channel.ChannelInfo.IsMultiKey = true
|
addChannelRequest.Channel.ChannelInfo.IsMultiKey = true
|
||||||
addChannelRequest.Channel.ChannelInfo.MultiKeyMode = addChannelRequest.MultiKeyMode
|
addChannelRequest.Channel.ChannelInfo.MultiKeyMode = addChannelRequest.MultiKeyMode
|
||||||
if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi {
|
if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi && addChannelRequest.Channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
|
||||||
array, err := getVertexArrayKeys(addChannelRequest.Channel.Key)
|
array, err := getVertexArrayKeys(addChannelRequest.Channel.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -585,7 +586,7 @@ func AddChannel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
keys = []string{addChannelRequest.Channel.Key}
|
keys = []string{addChannelRequest.Channel.Key}
|
||||||
case "batch":
|
case "batch":
|
||||||
if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi {
|
if addChannelRequest.Channel.Type == constant.ChannelTypeVertexAi && addChannelRequest.Channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
|
||||||
// multi json
|
// multi json
|
||||||
keys, err = getVertexArrayKeys(addChannelRequest.Channel.Key)
|
keys, err = getVertexArrayKeys(addChannelRequest.Channel.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -840,7 +841,7 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理 Vertex AI 的特殊情况
|
// 处理 Vertex AI 的特殊情况
|
||||||
if channel.Type == constant.ChannelTypeVertexAi {
|
if channel.Type == constant.ChannelTypeVertexAi && channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
|
||||||
// 尝试解析新密钥为JSON数组
|
// 尝试解析新密钥为JSON数组
|
||||||
if strings.HasPrefix(strings.TrimSpace(channel.Key), "[") {
|
if strings.HasPrefix(strings.TrimSpace(channel.Key), "[") {
|
||||||
array, err := getVertexArrayKeys(channel.Key)
|
array, err := getVertexArrayKeys(channel.Key)
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ func GetStatus(c *gin.Context) {
|
|||||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
"wechat_login": common.WeChatAuthEnabled,
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
"server_address": setting.ServerAddress,
|
"server_address": setting.ServerAddress,
|
||||||
"price": setting.Price,
|
|
||||||
"stripe_unit_price": setting.StripeUnitPrice,
|
|
||||||
"min_topup": setting.MinTopUp,
|
|
||||||
"stripe_min_topup": setting.StripeMinTopUp,
|
|
||||||
"turnstile_check": common.TurnstileCheckEnabled,
|
"turnstile_check": common.TurnstileCheckEnabled,
|
||||||
"turnstile_site_key": common.TurnstileSiteKey,
|
"turnstile_site_key": common.TurnstileSiteKey,
|
||||||
"top_up_link": common.TopUpLink,
|
"top_up_link": common.TopUpLink,
|
||||||
@@ -75,15 +71,15 @@ func GetStatus(c *gin.Context) {
|
|||||||
"enable_data_export": common.DataExportEnabled,
|
"enable_data_export": common.DataExportEnabled,
|
||||||
"data_export_default_time": common.DataExportDefaultTime,
|
"data_export_default_time": common.DataExportDefaultTime,
|
||||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
|
||||||
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
|
||||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||||
"chats": setting.Chats,
|
"chats": setting.Chats,
|
||||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||||
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||||
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
"default_use_auto_group": setting.DefaultUseAutoGroup,
|
||||||
"pay_methods": setting.PayMethods,
|
|
||||||
"usd_exchange_rate": setting.USDExchangeRate,
|
"usd_exchange_rate": operation_setting.USDExchangeRate,
|
||||||
|
"price": operation_setting.Price,
|
||||||
|
"stripe_unit_price": setting.StripeUnitPrice,
|
||||||
|
|
||||||
// 面板启用开关
|
// 面板启用开关
|
||||||
"api_info_enabled": cs.ApiInfoEnabled,
|
"api_info_enabled": cs.ApiInfoEnabled,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/operation_setting"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,6 +20,44 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func GetTopUpInfo(c *gin.Context) {
|
||||||
|
// 获取支付方式
|
||||||
|
payMethods := operation_setting.PayMethods
|
||||||
|
|
||||||
|
// 如果启用了 Stripe 支付,添加到支付方法列表
|
||||||
|
if setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "" {
|
||||||
|
// 检查是否已经包含 Stripe
|
||||||
|
hasStripe := false
|
||||||
|
for _, method := range payMethods {
|
||||||
|
if method["type"] == "stripe" {
|
||||||
|
hasStripe = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasStripe {
|
||||||
|
stripeMethod := map[string]string{
|
||||||
|
"name": "Stripe",
|
||||||
|
"type": "stripe",
|
||||||
|
"color": "rgba(var(--semi-purple-5), 1)",
|
||||||
|
"min_topup": strconv.Itoa(setting.StripeMinTopUp),
|
||||||
|
}
|
||||||
|
payMethods = append(payMethods, stripeMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := gin.H{
|
||||||
|
"enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
|
||||||
|
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
||||||
|
"pay_methods": payMethods,
|
||||||
|
"min_topup": operation_setting.MinTopUp,
|
||||||
|
"stripe_min_topup": setting.StripeMinTopUp,
|
||||||
|
"amount_options": operation_setting.GetPaymentSetting().AmountOptions,
|
||||||
|
"discount": operation_setting.GetPaymentSetting().AmountDiscount,
|
||||||
|
}
|
||||||
|
common.ApiSuccess(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
type EpayRequest struct {
|
type EpayRequest struct {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
PaymentMethod string `json:"payment_method"`
|
PaymentMethod string `json:"payment_method"`
|
||||||
@@ -31,13 +70,13 @@ type AmountRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetEpayClient() *epay.Client {
|
func GetEpayClient() *epay.Client {
|
||||||
if setting.PayAddress == "" || setting.EpayId == "" || setting.EpayKey == "" {
|
if operation_setting.PayAddress == "" || operation_setting.EpayId == "" || operation_setting.EpayKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
withUrl, err := epay.NewClient(&epay.Config{
|
withUrl, err := epay.NewClient(&epay.Config{
|
||||||
PartnerID: setting.EpayId,
|
PartnerID: operation_setting.EpayId,
|
||||||
Key: setting.EpayKey,
|
Key: operation_setting.EpayKey,
|
||||||
}, setting.PayAddress)
|
}, operation_setting.PayAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -58,15 +97,23 @@ func getPayMoney(amount int64, group string) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio)
|
dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio)
|
||||||
dPrice := decimal.NewFromFloat(setting.Price)
|
dPrice := decimal.NewFromFloat(operation_setting.Price)
|
||||||
|
// apply optional preset discount by the original request amount (if configured), default 1.0
|
||||||
|
discount := 1.0
|
||||||
|
if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok {
|
||||||
|
if ds > 0 {
|
||||||
|
discount = ds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dDiscount := decimal.NewFromFloat(discount)
|
||||||
|
|
||||||
payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio)
|
payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio).Mul(dDiscount)
|
||||||
|
|
||||||
return payMoney.InexactFloat64()
|
return payMoney.InexactFloat64()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMinTopup() int64 {
|
func getMinTopup() int64 {
|
||||||
minTopup := setting.MinTopUp
|
minTopup := operation_setting.MinTopUp
|
||||||
if !common.DisplayInCurrencyEnabled {
|
if !common.DisplayInCurrencyEnabled {
|
||||||
dMinTopup := decimal.NewFromInt(int64(minTopup))
|
dMinTopup := decimal.NewFromInt(int64(minTopup))
|
||||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||||
@@ -99,7 +146,7 @@ func RequestEpay(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !setting.ContainsPayMethod(req.PaymentMethod) {
|
if !operation_setting.ContainsPayMethod(req.PaymentMethod) {
|
||||||
c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
|
c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/operation_setting"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -254,6 +255,7 @@ func GetChargedAmount(count float64, user model.User) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getStripePayMoney(amount float64, group string) float64 {
|
func getStripePayMoney(amount float64, group string) float64 {
|
||||||
|
originalAmount := amount
|
||||||
if !common.DisplayInCurrencyEnabled {
|
if !common.DisplayInCurrencyEnabled {
|
||||||
amount = amount / common.QuotaPerUnit
|
amount = amount / common.QuotaPerUnit
|
||||||
}
|
}
|
||||||
@@ -262,7 +264,14 @@ func getStripePayMoney(amount float64, group string) float64 {
|
|||||||
if topupGroupRatio == 0 {
|
if topupGroupRatio == 0 {
|
||||||
topupGroupRatio = 1
|
topupGroupRatio = 1
|
||||||
}
|
}
|
||||||
payMoney := amount * setting.StripeUnitPrice * topupGroupRatio
|
// apply optional preset discount by the original request amount (if configured), default 1.0
|
||||||
|
discount := 1.0
|
||||||
|
if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
|
||||||
|
if ds > 0 {
|
||||||
|
discount = ds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payMoney := amount * setting.StripeUnitPrice * topupGroupRatio * discount
|
||||||
return payMoney
|
return payMoney
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ type ChannelSettings struct {
|
|||||||
SystemPromptOverride bool `json:"system_prompt_override,omitempty"`
|
SystemPromptOverride bool `json:"system_prompt_override,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VertexKeyType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VertexKeyTypeJSON VertexKeyType = "json"
|
||||||
|
VertexKeyTypeAPIKey VertexKeyType = "api_key"
|
||||||
|
)
|
||||||
|
|
||||||
type ChannelOtherSettings struct {
|
type ChannelOtherSettings struct {
|
||||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||||
|
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/logger"
|
"one-api/logger"
|
||||||
"one-api/types"
|
"one-api/types"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GeminiChatRequest struct {
|
type GeminiChatRequest struct {
|
||||||
@@ -269,15 +268,14 @@ type GeminiChatResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GeminiUsageMetadata struct {
|
type GeminiUsageMetadata struct {
|
||||||
PromptTokenCount int `json:"promptTokenCount"`
|
PromptTokenCount int `json:"promptTokenCount"`
|
||||||
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
||||||
TotalTokenCount int `json:"totalTokenCount"`
|
TotalTokenCount int `json:"totalTokenCount"`
|
||||||
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
|
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
|
||||||
PromptTokensDetails []GeminiModalityTokenCount `json:"promptTokensDetails"`
|
PromptTokensDetails []GeminiPromptTokensDetails `json:"promptTokensDetails"`
|
||||||
CandidatesTokensDetails []GeminiModalityTokenCount `json:"candidatesTokensDetails"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GeminiModalityTokenCount struct {
|
type GeminiPromptTokensDetails struct {
|
||||||
Modality string `json:"modality"`
|
Modality string `json:"modality"`
|
||||||
TokenCount int `json:"tokenCount"`
|
TokenCount int `json:"tokenCount"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ type Channel struct {
|
|||||||
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
||||||
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
||||||
OtherInfo string `json:"other_info"`
|
OtherInfo string `json:"other_info"`
|
||||||
OtherSettings string `json:"settings" gorm:"column:settings"` // 其他设置
|
|
||||||
Tag *string `json:"tag" gorm:"index"`
|
Tag *string `json:"tag" gorm:"index"`
|
||||||
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
|
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
|
||||||
ParamOverride *string `json:"param_override" gorm:"type:text"`
|
ParamOverride *string `json:"param_override" gorm:"type:text"`
|
||||||
@@ -51,6 +50,8 @@ type Channel struct {
|
|||||||
// add after v0.8.5
|
// add after v0.8.5
|
||||||
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`
|
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`
|
||||||
|
|
||||||
|
OtherSettings string `json:"settings" gorm:"column:settings"` // 其他设置,存储azure版本等不需要检索的信息,详见dto.ChannelOtherSettings
|
||||||
|
|
||||||
// cache info
|
// cache info
|
||||||
Keys []string `json:"-" gorm:"-"`
|
Keys []string `json:"-" gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["CustomCallbackAddress"] = ""
|
common.OptionMap["CustomCallbackAddress"] = ""
|
||||||
common.OptionMap["EpayId"] = ""
|
common.OptionMap["EpayId"] = ""
|
||||||
common.OptionMap["EpayKey"] = ""
|
common.OptionMap["EpayKey"] = ""
|
||||||
common.OptionMap["Price"] = strconv.FormatFloat(setting.Price, 'f', -1, 64)
|
common.OptionMap["Price"] = strconv.FormatFloat(operation_setting.Price, 'f', -1, 64)
|
||||||
common.OptionMap["USDExchangeRate"] = strconv.FormatFloat(setting.USDExchangeRate, 'f', -1, 64)
|
common.OptionMap["USDExchangeRate"] = strconv.FormatFloat(operation_setting.USDExchangeRate, 'f', -1, 64)
|
||||||
common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
|
common.OptionMap["MinTopUp"] = strconv.Itoa(operation_setting.MinTopUp)
|
||||||
common.OptionMap["StripeMinTopUp"] = strconv.Itoa(setting.StripeMinTopUp)
|
common.OptionMap["StripeMinTopUp"] = strconv.Itoa(setting.StripeMinTopUp)
|
||||||
common.OptionMap["StripeApiSecret"] = setting.StripeApiSecret
|
common.OptionMap["StripeApiSecret"] = setting.StripeApiSecret
|
||||||
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
||||||
@@ -85,7 +85,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||||
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
||||||
common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup)
|
common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup)
|
||||||
common.OptionMap["PayMethods"] = setting.PayMethods2JsonString()
|
common.OptionMap["PayMethods"] = operation_setting.PayMethods2JsonString()
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
common.OptionMap["TelegramBotToken"] = ""
|
common.OptionMap["TelegramBotToken"] = ""
|
||||||
@@ -299,23 +299,23 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
case "WorkerValidKey":
|
case "WorkerValidKey":
|
||||||
setting.WorkerValidKey = value
|
setting.WorkerValidKey = value
|
||||||
case "PayAddress":
|
case "PayAddress":
|
||||||
setting.PayAddress = value
|
operation_setting.PayAddress = value
|
||||||
case "Chats":
|
case "Chats":
|
||||||
err = setting.UpdateChatsByJsonString(value)
|
err = setting.UpdateChatsByJsonString(value)
|
||||||
case "AutoGroups":
|
case "AutoGroups":
|
||||||
err = setting.UpdateAutoGroupsByJsonString(value)
|
err = setting.UpdateAutoGroupsByJsonString(value)
|
||||||
case "CustomCallbackAddress":
|
case "CustomCallbackAddress":
|
||||||
setting.CustomCallbackAddress = value
|
operation_setting.CustomCallbackAddress = value
|
||||||
case "EpayId":
|
case "EpayId":
|
||||||
setting.EpayId = value
|
operation_setting.EpayId = value
|
||||||
case "EpayKey":
|
case "EpayKey":
|
||||||
setting.EpayKey = value
|
operation_setting.EpayKey = value
|
||||||
case "Price":
|
case "Price":
|
||||||
setting.Price, _ = strconv.ParseFloat(value, 64)
|
operation_setting.Price, _ = strconv.ParseFloat(value, 64)
|
||||||
case "USDExchangeRate":
|
case "USDExchangeRate":
|
||||||
setting.USDExchangeRate, _ = strconv.ParseFloat(value, 64)
|
operation_setting.USDExchangeRate, _ = strconv.ParseFloat(value, 64)
|
||||||
case "MinTopUp":
|
case "MinTopUp":
|
||||||
setting.MinTopUp, _ = strconv.Atoi(value)
|
operation_setting.MinTopUp, _ = strconv.Atoi(value)
|
||||||
case "StripeApiSecret":
|
case "StripeApiSecret":
|
||||||
setting.StripeApiSecret = value
|
setting.StripeApiSecret = value
|
||||||
case "StripeWebhookSecret":
|
case "StripeWebhookSecret":
|
||||||
@@ -413,7 +413,7 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
case "StreamCacheQueueLength":
|
case "StreamCacheQueueLength":
|
||||||
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||||
case "PayMethods":
|
case "PayMethods":
|
||||||
err = setting.UpdatePayMethodsByJsonString(value)
|
err = operation_setting.UpdatePayMethodsByJsonString(value)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,16 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
|||||||
if request == nil {
|
if request == nil {
|
||||||
return nil, errors.New("request is nil")
|
return nil, errors.New("request is nil")
|
||||||
}
|
}
|
||||||
|
// 检查是否为Nova模型
|
||||||
|
if isNovaModel(request.Model) {
|
||||||
|
novaReq := convertToNovaRequest(request)
|
||||||
|
c.Set("request_model", request.Model)
|
||||||
|
c.Set("converted_request", novaReq)
|
||||||
|
c.Set("is_nova_model", true)
|
||||||
|
return novaReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原有的Claude模型处理逻辑
|
||||||
var claudeReq *dto.ClaudeRequest
|
var claudeReq *dto.ClaudeRequest
|
||||||
var err error
|
var err error
|
||||||
claudeReq, err = claude.RequestOpenAI2ClaudeMessage(c, *request)
|
claudeReq, err = claude.RequestOpenAI2ClaudeMessage(c, *request)
|
||||||
@@ -69,6 +78,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
|||||||
}
|
}
|
||||||
c.Set("request_model", claudeReq.Model)
|
c.Set("request_model", claudeReq.Model)
|
||||||
c.Set("converted_request", claudeReq)
|
c.Set("converted_request", claudeReq)
|
||||||
|
c.Set("is_nova_model", false)
|
||||||
return claudeReq, err
|
return claudeReq, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package aws
|
package aws
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
var awsModelIDMap = map[string]string{
|
var awsModelIDMap = map[string]string{
|
||||||
"claude-instant-1.2": "anthropic.claude-instant-v1",
|
"claude-instant-1.2": "anthropic.claude-instant-v1",
|
||||||
"claude-2.0": "anthropic.claude-v2",
|
"claude-2.0": "anthropic.claude-v2",
|
||||||
@@ -14,6 +16,11 @@ var awsModelIDMap = map[string]string{
|
|||||||
"claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0",
|
"claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0",
|
||||||
"claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0",
|
"claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0",
|
||||||
"claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0",
|
"claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0",
|
||||||
|
// Nova models
|
||||||
|
"nova-micro-v1:0": "amazon.nova-micro-v1:0",
|
||||||
|
"nova-lite-v1:0": "amazon.nova-lite-v1:0",
|
||||||
|
"nova-pro-v1:0": "amazon.nova-pro-v1:0",
|
||||||
|
"nova-premier-v1:0": "amazon.nova-premier-v1:0",
|
||||||
}
|
}
|
||||||
|
|
||||||
var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
||||||
@@ -58,7 +65,27 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
|||||||
"anthropic.claude-opus-4-1-20250805-v1:0": {
|
"anthropic.claude-opus-4-1-20250805-v1:0": {
|
||||||
"us": true,
|
"us": true,
|
||||||
},
|
},
|
||||||
}
|
// Nova models - all support three major regions
|
||||||
|
"amazon.nova-micro-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
"eu": true,
|
||||||
|
"apac": true,
|
||||||
|
},
|
||||||
|
"amazon.nova-lite-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
"eu": true,
|
||||||
|
"apac": true,
|
||||||
|
},
|
||||||
|
"amazon.nova-pro-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
"eu": true,
|
||||||
|
"apac": true,
|
||||||
|
},
|
||||||
|
"amazon.nova-premier-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
"eu": true,
|
||||||
|
"apac": true,
|
||||||
|
}}
|
||||||
|
|
||||||
var awsRegionCrossModelPrefixMap = map[string]string{
|
var awsRegionCrossModelPrefixMap = map[string]string{
|
||||||
"us": "us",
|
"us": "us",
|
||||||
@@ -67,3 +94,8 @@ var awsRegionCrossModelPrefixMap = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ChannelName = "aws"
|
var ChannelName = "aws"
|
||||||
|
|
||||||
|
// 判断是否为Nova模型
|
||||||
|
func isNovaModel(modelId string) bool {
|
||||||
|
return strings.HasPrefix(modelId, "nova-")
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,3 +34,92 @@ func copyRequest(req *dto.ClaudeRequest) *AwsClaudeRequest {
|
|||||||
Thinking: req.Thinking,
|
Thinking: req.Thinking,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NovaMessage Nova模型使用messages-v1格式
|
||||||
|
type NovaMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content []NovaContent `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NovaContent struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NovaRequest struct {
|
||||||
|
SchemaVersion string `json:"schemaVersion"` // 请求版本,例如 "1.0"
|
||||||
|
Messages []NovaMessage `json:"messages"` // 对话消息列表
|
||||||
|
InferenceConfig *NovaInferenceConfig `json:"inferenceConfig,omitempty"` // 推理配置,可选
|
||||||
|
}
|
||||||
|
|
||||||
|
type NovaInferenceConfig struct {
|
||||||
|
MaxTokens int `json:"maxTokens,omitempty"` // 最大生成的 token 数
|
||||||
|
Temperature float64 `json:"temperature,omitempty"` // 随机性 (默认 0.7, 范围 0-1)
|
||||||
|
TopP float64 `json:"topP,omitempty"` // nucleus sampling (默认 0.9, 范围 0-1)
|
||||||
|
TopK int `json:"topK,omitempty"` // 限制候选 token 数 (默认 50, 范围 0-128)
|
||||||
|
StopSequences []string `json:"stopSequences,omitempty"` // 停止生成的序列
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换OpenAI请求为Nova格式
|
||||||
|
func convertToNovaRequest(req *dto.GeneralOpenAIRequest) *NovaRequest {
|
||||||
|
novaMessages := make([]NovaMessage, len(req.Messages))
|
||||||
|
for i, msg := range req.Messages {
|
||||||
|
novaMessages[i] = NovaMessage{
|
||||||
|
Role: msg.Role,
|
||||||
|
Content: []NovaContent{{Text: msg.StringContent()}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
novaReq := &NovaRequest{
|
||||||
|
SchemaVersion: "messages-v1",
|
||||||
|
Messages: novaMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置推理配置
|
||||||
|
if req.MaxTokens != 0 || (req.Temperature != nil && *req.Temperature != 0) || req.TopP != 0 || req.TopK != 0 || req.Stop != nil {
|
||||||
|
novaReq.InferenceConfig = &NovaInferenceConfig{}
|
||||||
|
if req.MaxTokens != 0 {
|
||||||
|
novaReq.InferenceConfig.MaxTokens = int(req.MaxTokens)
|
||||||
|
}
|
||||||
|
if req.Temperature != nil && *req.Temperature != 0 {
|
||||||
|
novaReq.InferenceConfig.Temperature = *req.Temperature
|
||||||
|
}
|
||||||
|
if req.TopP != 0 {
|
||||||
|
novaReq.InferenceConfig.TopP = req.TopP
|
||||||
|
}
|
||||||
|
if req.TopK != 0 {
|
||||||
|
novaReq.InferenceConfig.TopK = req.TopK
|
||||||
|
}
|
||||||
|
if req.Stop != nil {
|
||||||
|
if stopSequences := parseStopSequences(req.Stop); len(stopSequences) > 0 {
|
||||||
|
novaReq.InferenceConfig.StopSequences = stopSequences
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return novaReq
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStopSequences 解析停止序列,支持字符串或字符串数组
|
||||||
|
func parseStopSequences(stop any) []string {
|
||||||
|
if stop == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := stop.(type) {
|
||||||
|
case string:
|
||||||
|
if v != "" {
|
||||||
|
return []string{v}
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
return v
|
||||||
|
case []interface{}:
|
||||||
|
var sequences []string
|
||||||
|
for _, item := range v {
|
||||||
|
if str, ok := item.(string); ok && str != "" {
|
||||||
|
sequences = append(sequences, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sequences
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package aws
|
package aws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
@@ -93,7 +94,19 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
awsModelId := awsModelID(c.GetString("request_model"))
|
awsModelId := awsModelID(c.GetString("request_model"))
|
||||||
|
// 检查是否为Nova模型
|
||||||
|
isNova, _ := c.Get("is_nova_model")
|
||||||
|
if isNova == true {
|
||||||
|
// Nova模型也支持跨区域
|
||||||
|
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
|
||||||
|
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
|
||||||
|
if canCrossRegion {
|
||||||
|
awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix)
|
||||||
|
}
|
||||||
|
return handleNovaRequest(c, awsCli, info, awsModelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原有的Claude处理逻辑
|
||||||
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
|
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
|
||||||
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
|
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
|
||||||
if canCrossRegion {
|
if canCrossRegion {
|
||||||
@@ -130,7 +143,12 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
|
|||||||
Usage: &dto.Usage{},
|
Usage: &dto.Usage{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerErr := claude.HandleClaudeResponseData(c, info, claudeInfo, awsResp.Body, RequestModeMessage)
|
// 复制上游 Content-Type 到客户端响应头
|
||||||
|
if awsResp.ContentType != nil && *awsResp.ContentType != "" {
|
||||||
|
c.Writer.Header().Set("Content-Type", *awsResp.ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerErr := claude.HandleClaudeResponseData(c, info, claudeInfo, nil, awsResp.Body, RequestModeMessage)
|
||||||
if handlerErr != nil {
|
if handlerErr != nil {
|
||||||
return handlerErr, nil
|
return handlerErr, nil
|
||||||
}
|
}
|
||||||
@@ -204,3 +222,74 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
|||||||
claude.HandleStreamFinalResponse(c, info, claudeInfo, RequestModeMessage)
|
claude.HandleStreamFinalResponse(c, info, claudeInfo, RequestModeMessage)
|
||||||
return nil, claudeInfo.Usage
|
return nil, claudeInfo.Usage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nova模型处理函数
|
||||||
|
func handleNovaRequest(c *gin.Context, awsCli *bedrockruntime.Client, info *relaycommon.RelayInfo, awsModelId string) (*types.NewAPIError, *dto.Usage) {
|
||||||
|
novaReq_, ok := c.Get("converted_request")
|
||||||
|
if !ok {
|
||||||
|
return types.NewError(errors.New("nova request not found"), types.ErrorCodeInvalidRequest), nil
|
||||||
|
}
|
||||||
|
novaReq := novaReq_.(*NovaRequest)
|
||||||
|
|
||||||
|
// 使用InvokeModel API,但使用Nova格式的请求体
|
||||||
|
awsReq := &bedrockruntime.InvokeModelInput{
|
||||||
|
ModelId: aws.String(awsModelId),
|
||||||
|
Accept: aws.String("application/json"),
|
||||||
|
ContentType: aws.String("application/json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(novaReq)
|
||||||
|
if err != nil {
|
||||||
|
return types.NewError(errors.Wrap(err, "marshal nova request"), types.ErrorCodeBadResponseBody), nil
|
||||||
|
}
|
||||||
|
awsReq.Body = reqBody
|
||||||
|
|
||||||
|
awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq)
|
||||||
|
if err != nil {
|
||||||
|
return types.NewError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeChannelAwsClientError), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析Nova响应
|
||||||
|
var novaResp struct {
|
||||||
|
Output struct {
|
||||||
|
Message struct {
|
||||||
|
Content []struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"content"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"output"`
|
||||||
|
Usage struct {
|
||||||
|
InputTokens int `json:"inputTokens"`
|
||||||
|
OutputTokens int `json:"outputTokens"`
|
||||||
|
TotalTokens int `json:"totalTokens"`
|
||||||
|
} `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(awsResp.Body, &novaResp); err != nil {
|
||||||
|
return types.NewError(errors.Wrap(err, "unmarshal nova response"), types.ErrorCodeBadResponseBody), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造OpenAI格式响应
|
||||||
|
response := dto.OpenAITextResponse{
|
||||||
|
Id: helper.GetResponseID(c),
|
||||||
|
Object: "chat.completion",
|
||||||
|
Created: common.GetTimestamp(),
|
||||||
|
Model: info.UpstreamModelName,
|
||||||
|
Choices: []dto.OpenAITextResponseChoice{{
|
||||||
|
Index: 0,
|
||||||
|
Message: dto.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: novaResp.Output.Message.Content[0].Text,
|
||||||
|
},
|
||||||
|
FinishReason: "stop",
|
||||||
|
}},
|
||||||
|
Usage: dto.Usage{
|
||||||
|
PromptTokens: novaResp.Usage.InputTokens,
|
||||||
|
CompletionTokens: novaResp.Usage.OutputTokens,
|
||||||
|
TotalTokens: novaResp.Usage.TotalTokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
return nil, &response.Usage
|
||||||
|
}
|
||||||
|
|||||||
@@ -716,7 +716,7 @@ func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.
|
|||||||
return claudeInfo.Usage, nil
|
return claudeInfo.Usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data []byte, requestMode int) *types.NewAPIError {
|
func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, httpResp *http.Response, data []byte, requestMode int) *types.NewAPIError {
|
||||||
var claudeResponse dto.ClaudeResponse
|
var claudeResponse dto.ClaudeResponse
|
||||||
err := common.Unmarshal(data, &claudeResponse)
|
err := common.Unmarshal(data, &claudeResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -754,7 +754,7 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
|||||||
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
|
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
service.IOCopyBytesGracefully(c, nil, responseData)
|
service.IOCopyBytesGracefully(c, httpResp, responseData)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +775,7 @@ func ClaudeHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
|
|||||||
if common.DebugEnabled {
|
if common.DebugEnabled {
|
||||||
println("responseBody: ", string(responseBody))
|
println("responseBody: ", string(responseBody))
|
||||||
}
|
}
|
||||||
handleErr := HandleClaudeResponseData(c, info, claudeInfo, responseBody, requestMode)
|
handleErr := HandleClaudeResponseData(c, info, claudeInfo, resp, responseBody, requestMode)
|
||||||
if handleErr != nil {
|
if handleErr != nil {
|
||||||
return nil, handleErr
|
return nil, handleErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,32 +46,6 @@ func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, re
|
|||||||
|
|
||||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||||
|
|
||||||
if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") {
|
|
||||||
imageOutputCounts := 0
|
|
||||||
for _, candidate := range geminiResponse.Candidates {
|
|
||||||
for _, part := range candidate.Content.Parts {
|
|
||||||
if part.InlineData != nil && strings.HasPrefix(part.InlineData.MimeType, "image/") {
|
|
||||||
imageOutputCounts++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if imageOutputCounts != 0 {
|
|
||||||
usage.CompletionTokens = usage.CompletionTokens - imageOutputCounts*1290
|
|
||||||
usage.TotalTokens = usage.TotalTokens - imageOutputCounts*1290
|
|
||||||
c.Set("gemini_image_tokens", imageOutputCounts*1290)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") {
|
|
||||||
// for _, detail := range geminiResponse.UsageMetadata.CandidatesTokensDetails {
|
|
||||||
// if detail.Modality == "IMAGE" {
|
|
||||||
// usage.CompletionTokens = usage.CompletionTokens - detail.TokenCount
|
|
||||||
// usage.TotalTokens = usage.TotalTokens - detail.TokenCount
|
|
||||||
// c.Set("gemini_image_tokens", detail.TokenCount)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||||
if detail.Modality == "AUDIO" {
|
if detail.Modality == "AUDIO" {
|
||||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||||
@@ -162,16 +136,6 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn
|
|||||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") {
|
|
||||||
for _, detail := range geminiResponse.UsageMetadata.CandidatesTokensDetails {
|
|
||||||
if detail.Modality == "IMAGE" {
|
|
||||||
usage.CompletionTokens = usage.CompletionTokens - detail.TokenCount
|
|
||||||
usage.TotalTokens = usage.TotalTokens - detail.TokenCount
|
|
||||||
c.Set("gemini_image_tokens", detail.TokenCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接发送 GeminiChatResponse 响应
|
// 直接发送 GeminiChatResponse 响应
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"one-api/common"
|
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/relay/channel"
|
"one-api/relay/channel"
|
||||||
@@ -89,22 +88,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
|
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
|
||||||
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
||||||
// Accept only POST /v1/video/generations as "generate" action.
|
// Accept only POST /v1/video/generations as "generate" action.
|
||||||
action := constant.TaskActionGenerate
|
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
|
||||||
info.Action = action
|
|
||||||
|
|
||||||
req := relaycommon.TaskSubmitReq{}
|
|
||||||
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
|
|
||||||
taskErr = service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(req.Prompt) == "" {
|
|
||||||
taskErr = service.TaskErrorWrapperLocal(fmt.Errorf("prompt is required"), "invalid_request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store into context for later usage
|
|
||||||
c.Set("task_request", req)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildRequestURL constructs the upstream URL.
|
// BuildRequestURL constructs the upstream URL.
|
||||||
@@ -334,11 +318,11 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle one-of image_urls or binary_data_base64
|
// Handle one-of image_urls or binary_data_base64
|
||||||
if req.Image != "" {
|
if req.HasImage() {
|
||||||
if strings.HasPrefix(req.Image, "http") {
|
if strings.HasPrefix(req.Images[0], "http") {
|
||||||
r.ImageUrls = []string{req.Image}
|
r.ImageUrls = req.Images
|
||||||
} else {
|
} else {
|
||||||
r.BinaryDataBase64 = []string{req.Image}
|
r.BinaryDataBase64 = req.Images
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata := req.Metadata
|
metadata := req.Metadata
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"one-api/common"
|
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/relay/channel"
|
"one-api/relay/channel"
|
||||||
@@ -28,16 +27,6 @@ import (
|
|||||||
// Request / Response structures
|
// Request / Response structures
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
type SubmitReq struct {
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
Model string `json:"model,omitempty"`
|
|
||||||
Mode string `json:"mode,omitempty"`
|
|
||||||
Image string `json:"image,omitempty"`
|
|
||||||
Size string `json:"size,omitempty"`
|
|
||||||
Duration int `json:"duration,omitempty"`
|
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrajectoryPoint struct {
|
type TrajectoryPoint struct {
|
||||||
X int `json:"x"`
|
X int `json:"x"`
|
||||||
Y int `json:"y"`
|
Y int `json:"y"`
|
||||||
@@ -121,23 +110,8 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
|
|
||||||
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
|
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
|
||||||
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
||||||
// Accept only POST /v1/video/generations as "generate" action.
|
// Use the standard validation method for TaskSubmitReq
|
||||||
action := constant.TaskActionGenerate
|
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
|
||||||
info.Action = action
|
|
||||||
|
|
||||||
var req SubmitReq
|
|
||||||
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
|
|
||||||
taskErr = service.TaskErrorWrapperLocal(err, "invalid_request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(req.Prompt) == "" {
|
|
||||||
taskErr = service.TaskErrorWrapperLocal(fmt.Errorf("prompt is required"), "invalid_request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store into context for later usage
|
|
||||||
c.Set("task_request", req)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildRequestURL constructs the upstream URL.
|
// BuildRequestURL constructs the upstream URL.
|
||||||
@@ -166,7 +140,7 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
|
|||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("request not found in context")
|
return nil, fmt.Errorf("request not found in context")
|
||||||
}
|
}
|
||||||
req := v.(SubmitReq)
|
req := v.(relaycommon.TaskSubmitReq)
|
||||||
|
|
||||||
body, err := a.convertToRequestPayload(&req)
|
body, err := a.convertToRequestPayload(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -255,7 +229,7 @@ func (a *TaskAdaptor) GetChannelName() string {
|
|||||||
// helpers
|
// helpers
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
func (a *TaskAdaptor) convertToRequestPayload(req *SubmitReq) (*requestPayload, error) {
|
func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) {
|
||||||
r := requestPayload{
|
r := requestPayload{
|
||||||
Prompt: req.Prompt,
|
Prompt: req.Prompt,
|
||||||
Image: req.Image,
|
Image: req.Image,
|
||||||
|
|||||||
@@ -23,16 +23,6 @@ import (
|
|||||||
// Request / Response structures
|
// Request / Response structures
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
type SubmitReq struct {
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
Model string `json:"model,omitempty"`
|
|
||||||
Mode string `json:"mode,omitempty"`
|
|
||||||
Image string `json:"image,omitempty"`
|
|
||||||
Size string `json:"size,omitempty"`
|
|
||||||
Duration int `json:"duration,omitempty"`
|
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestPayload struct {
|
type requestPayload struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Images []string `json:"images"`
|
Images []string `json:"images"`
|
||||||
@@ -90,23 +80,8 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
|
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
|
||||||
var req SubmitReq
|
// Use the unified validation method for TaskSubmitReq with image-based action determination
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
return relaycommon.ValidateTaskRequestWithImageBinding(c, info)
|
||||||
return service.TaskErrorWrapper(err, "invalid_request_body", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Prompt == "" {
|
|
||||||
return service.TaskErrorWrapperLocal(fmt.Errorf("prompt is required"), "missing_prompt", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Image != "" {
|
|
||||||
info.Action = constant.TaskActionGenerate
|
|
||||||
} else {
|
|
||||||
info.Action = constant.TaskActionTextGenerate
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("task_request", req)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.RelayInfo) (io.Reader, error) {
|
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.RelayInfo) (io.Reader, error) {
|
||||||
@@ -114,7 +89,7 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.RelayInfo)
|
|||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("request not found in context")
|
return nil, fmt.Errorf("request not found in context")
|
||||||
}
|
}
|
||||||
req := v.(SubmitReq)
|
req := v.(relaycommon.TaskSubmitReq)
|
||||||
|
|
||||||
body, err := a.convertToRequestPayload(&req)
|
body, err := a.convertToRequestPayload(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -211,7 +186,7 @@ func (a *TaskAdaptor) GetChannelName() string {
|
|||||||
// helpers
|
// helpers
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
func (a *TaskAdaptor) convertToRequestPayload(req *SubmitReq) (*requestPayload, error) {
|
func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) {
|
||||||
var images []string
|
var images []string
|
||||||
if req.Image != "" {
|
if req.Image != "" {
|
||||||
images = []string{req.Image}
|
images = []string{req.Image}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/relay/channel"
|
"one-api/relay/channel"
|
||||||
"one-api/relay/channel/claude"
|
"one-api/relay/channel/claude"
|
||||||
@@ -80,16 +81,64 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix string) (string, error) {
|
||||||
adc := &Credentials{}
|
|
||||||
if err := json.Unmarshal([]byte(info.ApiKey), adc); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to decode credentials file: %w", err)
|
|
||||||
}
|
|
||||||
region := GetModelRegion(info.ApiVersion, info.OriginModelName)
|
region := GetModelRegion(info.ApiVersion, info.OriginModelName)
|
||||||
a.AccountCredentials = *adc
|
if info.ChannelOtherSettings.VertexKeyType != dto.VertexKeyTypeAPIKey {
|
||||||
|
adc := &Credentials{}
|
||||||
|
if err := common.Unmarshal([]byte(info.ApiKey), adc); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode credentials file: %w", err)
|
||||||
|
}
|
||||||
|
a.AccountCredentials = *adc
|
||||||
|
|
||||||
|
if a.RequestMode == RequestModeLlama {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
|
||||||
|
region,
|
||||||
|
adc.ProjectID,
|
||||||
|
region,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if region == "global" {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s",
|
||||||
|
adc.ProjectID,
|
||||||
|
modelName,
|
||||||
|
suffix,
|
||||||
|
), nil
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
|
||||||
|
region,
|
||||||
|
adc.ProjectID,
|
||||||
|
region,
|
||||||
|
modelName,
|
||||||
|
suffix,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if region == "global" {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"https://aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s",
|
||||||
|
modelName,
|
||||||
|
suffix,
|
||||||
|
info.ApiKey,
|
||||||
|
), nil
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"https://%s-aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s",
|
||||||
|
region,
|
||||||
|
modelName,
|
||||||
|
suffix,
|
||||||
|
info.ApiKey,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||||
suffix := ""
|
suffix := ""
|
||||||
if a.RequestMode == RequestModeGemini {
|
if a.RequestMode == RequestModeGemini {
|
||||||
|
|
||||||
if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
|
if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
|
||||||
// 新增逻辑:处理 -thinking-<budget> 格式
|
// 新增逻辑:处理 -thinking-<budget> 格式
|
||||||
if strings.Contains(info.UpstreamModelName, "-thinking-") {
|
if strings.Contains(info.UpstreamModelName, "-thinking-") {
|
||||||
@@ -111,24 +160,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
|||||||
if strings.HasPrefix(info.UpstreamModelName, "imagen") {
|
if strings.HasPrefix(info.UpstreamModelName, "imagen") {
|
||||||
suffix = "predict"
|
suffix = "predict"
|
||||||
}
|
}
|
||||||
|
return a.getRequestUrl(info, info.UpstreamModelName, suffix)
|
||||||
if region == "global" {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s",
|
|
||||||
adc.ProjectID,
|
|
||||||
info.UpstreamModelName,
|
|
||||||
suffix,
|
|
||||||
), nil
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
|
|
||||||
region,
|
|
||||||
adc.ProjectID,
|
|
||||||
region,
|
|
||||||
info.UpstreamModelName,
|
|
||||||
suffix,
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
} else if a.RequestMode == RequestModeClaude {
|
} else if a.RequestMode == RequestModeClaude {
|
||||||
if info.IsStream {
|
if info.IsStream {
|
||||||
suffix = "streamRawPredict?alt=sse"
|
suffix = "streamRawPredict?alt=sse"
|
||||||
@@ -139,42 +171,25 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
|||||||
if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
|
if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
|
||||||
model = v
|
model = v
|
||||||
}
|
}
|
||||||
if region == "global" {
|
return a.getRequestUrl(info, model, suffix)
|
||||||
return fmt.Sprintf(
|
|
||||||
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/anthropic/models/%s:%s",
|
|
||||||
adc.ProjectID,
|
|
||||||
model,
|
|
||||||
suffix,
|
|
||||||
), nil
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s",
|
|
||||||
region,
|
|
||||||
adc.ProjectID,
|
|
||||||
region,
|
|
||||||
model,
|
|
||||||
suffix,
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
} else if a.RequestMode == RequestModeLlama {
|
} else if a.RequestMode == RequestModeLlama {
|
||||||
return fmt.Sprintf(
|
return a.getRequestUrl(info, "", "")
|
||||||
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
|
|
||||||
region,
|
|
||||||
adc.ProjectID,
|
|
||||||
region,
|
|
||||||
), nil
|
|
||||||
}
|
}
|
||||||
return "", errors.New("unsupported request mode")
|
return "", errors.New("unsupported request mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||||
channel.SetupApiRequestHeader(info, c, req)
|
channel.SetupApiRequestHeader(info, c, req)
|
||||||
accessToken, err := getAccessToken(a, info)
|
if info.ChannelOtherSettings.VertexKeyType != dto.VertexKeyTypeAPIKey {
|
||||||
if err != nil {
|
accessToken, err := getAccessToken(a, info)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
}
|
||||||
|
if a.AccountCredentials.ProjectID != "" {
|
||||||
|
req.Set("x-goog-user-project", a.AccountCredentials.ProjectID)
|
||||||
}
|
}
|
||||||
req.Set("Authorization", "Bearer "+accessToken)
|
|
||||||
req.Set("x-goog-user-project", a.AccountCredentials.ProjectID)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -481,11 +481,20 @@ type TaskSubmitReq struct {
|
|||||||
Model string `json:"model,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
|
Images []string `json:"images,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
Duration int `json:"duration,omitempty"`
|
Duration int `json:"duration,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t TaskSubmitReq) GetPrompt() string {
|
||||||
|
return t.Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TaskSubmitReq) HasImage() bool {
|
||||||
|
return len(t.Images) > 0
|
||||||
|
}
|
||||||
|
|
||||||
type TaskInfo struct {
|
type TaskInfo struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
TaskID string `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
|
|||||||
@@ -2,12 +2,23 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
|
"one-api/dto"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type HasPrompt interface {
|
||||||
|
GetPrompt() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HasImage interface {
|
||||||
|
HasImage() bool
|
||||||
|
}
|
||||||
|
|
||||||
func GetFullRequestURL(baseURL string, requestURL string, channelType int) string {
|
func GetFullRequestURL(baseURL string, requestURL string, channelType int) string {
|
||||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||||
|
|
||||||
@@ -30,3 +41,72 @@ func GetAPIVersion(c *gin.Context) string {
|
|||||||
}
|
}
|
||||||
return apiVersion
|
return apiVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createTaskError(err error, code string, statusCode int, localError bool) *dto.TaskError {
|
||||||
|
return &dto.TaskError{
|
||||||
|
Code: code,
|
||||||
|
Message: err.Error(),
|
||||||
|
StatusCode: statusCode,
|
||||||
|
LocalError: localError,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeTaskRequest(c *gin.Context, info *RelayInfo, action string, requestObj interface{}) {
|
||||||
|
info.Action = action
|
||||||
|
c.Set("task_request", requestObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePrompt(prompt string) *dto.TaskError {
|
||||||
|
if strings.TrimSpace(prompt) == "" {
|
||||||
|
return createTaskError(fmt.Errorf("prompt is required"), "invalid_request", http.StatusBadRequest, true)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *dto.TaskError {
|
||||||
|
var req TaskSubmitReq
|
||||||
|
if err := common.UnmarshalBodyReusable(c, &req); err != nil {
|
||||||
|
return createTaskError(err, "invalid_request", http.StatusBadRequest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskErr := validatePrompt(req.Prompt); taskErr != nil {
|
||||||
|
return taskErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Images) == 0 && strings.TrimSpace(req.Image) != "" {
|
||||||
|
// 兼容单图上传
|
||||||
|
req.Images = []string{req.Image}
|
||||||
|
}
|
||||||
|
|
||||||
|
storeTaskRequest(c, info, action, req)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateTaskRequestWithImage(c *gin.Context, info *RelayInfo, requestObj interface{}) *dto.TaskError {
|
||||||
|
hasPrompt, ok := requestObj.(HasPrompt)
|
||||||
|
if !ok {
|
||||||
|
return createTaskError(fmt.Errorf("request must have prompt"), "invalid_request", http.StatusBadRequest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskErr := validatePrompt(hasPrompt.GetPrompt()); taskErr != nil {
|
||||||
|
return taskErr
|
||||||
|
}
|
||||||
|
|
||||||
|
action := constant.TaskActionTextGenerate
|
||||||
|
if hasImage, ok := requestObj.(HasImage); ok && hasImage.HasImage() {
|
||||||
|
action = constant.TaskActionGenerate
|
||||||
|
}
|
||||||
|
|
||||||
|
storeTaskRequest(c, info, action, requestObj)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateTaskRequestWithImageBinding(c *gin.Context, info *RelayInfo) *dto.TaskError {
|
||||||
|
var req TaskSubmitReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
return createTaskError(err, "invalid_request_body", http.StatusBadRequest, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateTaskRequestWithImage(c, info, req)
|
||||||
|
}
|
||||||
|
|||||||
@@ -326,22 +326,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
|||||||
} else {
|
} else {
|
||||||
quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
|
quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
|
||||||
}
|
}
|
||||||
var dGeminiImageOutputQuota decimal.Decimal
|
|
||||||
var imageOutputPrice float64
|
|
||||||
if strings.HasPrefix(modelName, "gemini-2.5-flash-image-preview") {
|
|
||||||
imageOutputPrice = operation_setting.GetGeminiImageOutputPricePerMillionTokens(modelName)
|
|
||||||
if imageOutputPrice > 0 {
|
|
||||||
dImageOutputTokens := decimal.NewFromInt(int64(ctx.GetInt("gemini_image_tokens")))
|
|
||||||
dGeminiImageOutputQuota = decimal.NewFromFloat(imageOutputPrice).Div(decimal.NewFromInt(1000000)).Mul(dImageOutputTokens).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 添加 responses tools call 调用的配额
|
// 添加 responses tools call 调用的配额
|
||||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
|
quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
|
||||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
||||||
// 添加 audio input 独立计费
|
// 添加 audio input 独立计费
|
||||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
||||||
// 添加 Gemini image output 计费
|
|
||||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(dGeminiImageOutputQuota)
|
|
||||||
|
|
||||||
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
||||||
totalTokens := promptTokens + completionTokens
|
totalTokens := promptTokens + completionTokens
|
||||||
@@ -440,10 +429,6 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
|||||||
other["audio_input_token_count"] = audioTokens
|
other["audio_input_token_count"] = audioTokens
|
||||||
other["audio_input_price"] = audioInputPrice
|
other["audio_input_price"] = audioInputPrice
|
||||||
}
|
}
|
||||||
if !dGeminiImageOutputQuota.IsZero() {
|
|
||||||
other["image_output_token_count"] = ctx.GetInt("gemini_image_tokens")
|
|
||||||
other["image_output_price"] = imageOutputPrice
|
|
||||||
}
|
|
||||||
model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
|
model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
|
||||||
ChannelId: relayInfo.ChannelId,
|
ChannelId: relayInfo.ChannelId,
|
||||||
PromptTokens: promptTokens,
|
PromptTokens: promptTokens,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
|
|||||||
}
|
}
|
||||||
adaptor.Init(info)
|
adaptor.Init(info)
|
||||||
var requestBody io.Reader
|
var requestBody io.Reader
|
||||||
if model_setting.GetGlobalSettings().PassThroughRequestEnabled {
|
if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
|
||||||
body, err := common.GetRequestBody(c)
|
body, err := common.GetRequestBody(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.NewError(err, types.ErrorCodeReadRequestBodyFailed, types.ErrOptionWithSkipRetry())
|
return types.NewError(err, types.ErrorCodeReadRequestBodyFailed, types.ErrOptionWithSkipRetry())
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||||
selfRoute.GET("/aff", controller.GetAffCode)
|
selfRoute.GET("/aff", controller.GetAffCode)
|
||||||
|
selfRoute.GET("/topup/info", controller.GetTopUpInfo)
|
||||||
selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
|
selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
|
||||||
selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay)
|
selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay)
|
||||||
selfRoute.POST("/amount", controller.RequestAmount)
|
selfRoute.POST("/amount", controller.RequestAmount)
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/operation_setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCallbackAddress() string {
|
func GetCallbackAddress() string {
|
||||||
if setting.CustomCallbackAddress == "" {
|
if operation_setting.CustomCallbackAddress == "" {
|
||||||
return setting.ServerAddress
|
return setting.ServerAddress
|
||||||
}
|
}
|
||||||
return setting.CustomCallbackAddress
|
return operation_setting.CustomCallbackAddress
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
|
|||||||
for i, file := range meta.Files {
|
for i, file := range meta.Files {
|
||||||
switch file.FileType {
|
switch file.FileType {
|
||||||
case types.FileTypeImage:
|
case types.FileTypeImage:
|
||||||
if info.RelayFormat == types.RelayFormatGemini && !strings.HasPrefix(model, "gemini-2.5-flash-image-preview") {
|
if info.RelayFormat == types.RelayFormatGemini {
|
||||||
tkm += 256
|
tkm += 256
|
||||||
} else {
|
} else {
|
||||||
token, err := getImageToken(file, model, info.IsStream)
|
token, err := getImageToken(file, model, info.IsStream)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ var defaultGeminiSettings = GeminiSettings{
|
|||||||
SupportedImagineModels: []string{
|
SupportedImagineModels: []string{
|
||||||
"gemini-2.0-flash-exp-image-generation",
|
"gemini-2.0-flash-exp-image-generation",
|
||||||
"gemini-2.0-flash-exp",
|
"gemini-2.0-flash-exp",
|
||||||
"gemini-2.5-flash-image-preview",
|
|
||||||
},
|
},
|
||||||
ThinkingAdapterEnabled: false,
|
ThinkingAdapterEnabled: false,
|
||||||
ThinkingAdapterBudgetTokensPercentage: 0.6,
|
ThinkingAdapterBudgetTokensPercentage: 0.6,
|
||||||
|
|||||||
23
setting/operation_setting/payment_setting.go
Normal file
23
setting/operation_setting/payment_setting.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package operation_setting
|
||||||
|
|
||||||
|
import "one-api/setting/config"
|
||||||
|
|
||||||
|
type PaymentSetting struct {
|
||||||
|
AmountOptions []int `json:"amount_options"`
|
||||||
|
AmountDiscount map[int]float64 `json:"amount_discount"` // 充值金额对应的折扣,例如 100 元 0.9 表示 100 元充值享受 9 折优惠
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
var paymentSetting = PaymentSetting{
|
||||||
|
AmountOptions: []int{10, 20, 50, 100, 200, 500},
|
||||||
|
AmountDiscount: map[int]float64{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册到全局配置管理器
|
||||||
|
config.GlobalConfig.Register("payment_setting", &paymentSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPaymentSetting() *PaymentSetting {
|
||||||
|
return &paymentSetting
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
package setting
|
/**
|
||||||
|
此文件为旧版支付设置文件,如需增加新的参数、变量等,请在 payment_setting.go 中添加
|
||||||
|
This file is the old version of the payment settings file. If you need to add new parameters, variables, etc., please add them in payment_setting.go
|
||||||
|
*/
|
||||||
|
|
||||||
import "encoding/json"
|
package operation_setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common"
|
||||||
|
)
|
||||||
|
|
||||||
var PayAddress = ""
|
var PayAddress = ""
|
||||||
var CustomCallbackAddress = ""
|
var CustomCallbackAddress = ""
|
||||||
@@ -21,15 +28,21 @@ var PayMethods = []map[string]string{
|
|||||||
"color": "rgba(var(--semi-green-5), 1)",
|
"color": "rgba(var(--semi-green-5), 1)",
|
||||||
"type": "wxpay",
|
"type": "wxpay",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "自定义1",
|
||||||
|
"color": "black",
|
||||||
|
"type": "custom1",
|
||||||
|
"min_topup": "50",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdatePayMethodsByJsonString(jsonString string) error {
|
func UpdatePayMethodsByJsonString(jsonString string) error {
|
||||||
PayMethods = make([]map[string]string, 0)
|
PayMethods = make([]map[string]string, 0)
|
||||||
return json.Unmarshal([]byte(jsonString), &PayMethods)
|
return common.Unmarshal([]byte(jsonString), &PayMethods)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PayMethods2JsonString() string {
|
func PayMethods2JsonString() string {
|
||||||
jsonBytes, err := json.Marshal(PayMethods)
|
jsonBytes, err := common.Marshal(PayMethods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "[]"
|
return "[]"
|
||||||
}
|
}
|
||||||
@@ -24,10 +24,6 @@ const (
|
|||||||
ClaudeWebSearchPrice = 10.00
|
ClaudeWebSearchPrice = 10.00
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
Gemini25FlashImagePreviewImageOutputPrice = 30.00
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetClaudeWebSearchPricePerThousand() float64 {
|
func GetClaudeWebSearchPricePerThousand() float64 {
|
||||||
return ClaudeWebSearchPrice
|
return ClaudeWebSearchPrice
|
||||||
}
|
}
|
||||||
@@ -69,10 +65,3 @@ func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGeminiImageOutputPricePerMillionTokens(modelName string) float64 {
|
|
||||||
if strings.HasPrefix(modelName, "gemini-2.5-flash-image-preview") {
|
|
||||||
return Gemini25FlashImagePreviewImageOutputPrice
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ var defaultModelRatio = map[string]float64{
|
|||||||
"gemini-2.5-flash-lite-preview-thinking-*": 0.05,
|
"gemini-2.5-flash-lite-preview-thinking-*": 0.05,
|
||||||
"gemini-2.5-flash-lite-preview-06-17": 0.05,
|
"gemini-2.5-flash-lite-preview-06-17": 0.05,
|
||||||
"gemini-2.5-flash": 0.15,
|
"gemini-2.5-flash": 0.15,
|
||||||
"gemini-2.5-flash-image-preview": 0.15, // $0.30(text/image) / 1M tokens
|
|
||||||
"text-embedding-004": 0.001,
|
"text-embedding-004": 0.001,
|
||||||
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
||||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||||
@@ -294,11 +293,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var defaultCompletionRatio = map[string]float64{
|
var defaultCompletionRatio = map[string]float64{
|
||||||
"gpt-4-gizmo-*": 2,
|
"gpt-4-gizmo-*": 2,
|
||||||
"gpt-4o-gizmo-*": 3,
|
"gpt-4o-gizmo-*": 3,
|
||||||
"gpt-4-all": 2,
|
"gpt-4-all": 2,
|
||||||
"gpt-image-1": 8,
|
"gpt-image-1": 8,
|
||||||
"gemini-2.5-flash-image-preview": 8.3333333333,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitRatioSettings initializes all model related settings maps
|
// InitRatioSettings initializes all model related settings maps
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const TwoFactorAuthModal = ({
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<Typography.Text type='tertiary' size='small' className='mt-2 block'>
|
<Typography.Text type='tertiary' size='small' className='mt-2 block'>
|
||||||
{t('支持6位TOTP验证码或8位备用码')}
|
{t('支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const PaymentSetting = () => {
|
|||||||
TopupGroupRatio: '',
|
TopupGroupRatio: '',
|
||||||
CustomCallbackAddress: '',
|
CustomCallbackAddress: '',
|
||||||
PayMethods: '',
|
PayMethods: '',
|
||||||
|
AmountOptions: '',
|
||||||
|
AmountDiscount: '',
|
||||||
|
|
||||||
StripeApiSecret: '',
|
StripeApiSecret: '',
|
||||||
StripeWebhookSecret: '',
|
StripeWebhookSecret: '',
|
||||||
@@ -66,6 +68,30 @@ const PaymentSetting = () => {
|
|||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'payment_setting.amount_options':
|
||||||
|
try {
|
||||||
|
newInputs['AmountOptions'] = JSON.stringify(
|
||||||
|
JSON.parse(item.value),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析AmountOptions出错:', error);
|
||||||
|
newInputs['AmountOptions'] = item.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'payment_setting.amount_discount':
|
||||||
|
try {
|
||||||
|
newInputs['AmountDiscount'] = JSON.stringify(
|
||||||
|
JSON.parse(item.value),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析AmountDiscount出错:', error);
|
||||||
|
newInputs['AmountDiscount'] = item.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'Price':
|
case 'Price':
|
||||||
case 'MinTopUp':
|
case 'MinTopUp':
|
||||||
case 'StripeUnitPrice':
|
case 'StripeUnitPrice':
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ const EditChannelModal = (props) => {
|
|||||||
system_prompt: '',
|
system_prompt: '',
|
||||||
system_prompt_override: false,
|
system_prompt_override: false,
|
||||||
settings: '',
|
settings: '',
|
||||||
|
// 仅 Vertex: 密钥格式(存入 settings.vertex_key_type)
|
||||||
|
vertex_key_type: 'json',
|
||||||
};
|
};
|
||||||
const [batch, setBatch] = useState(false);
|
const [batch, setBatch] = useState(false);
|
||||||
const [multiToSingle, setMultiToSingle] = useState(false);
|
const [multiToSingle, setMultiToSingle] = useState(false);
|
||||||
@@ -409,11 +411,17 @@ const EditChannelModal = (props) => {
|
|||||||
const parsedSettings = JSON.parse(data.settings);
|
const parsedSettings = JSON.parse(data.settings);
|
||||||
data.azure_responses_version =
|
data.azure_responses_version =
|
||||||
parsedSettings.azure_responses_version || '';
|
parsedSettings.azure_responses_version || '';
|
||||||
|
// 读取 Vertex 密钥格式
|
||||||
|
data.vertex_key_type = parsedSettings.vertex_key_type || 'json';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解析其他设置失败:', error);
|
console.error('解析其他设置失败:', error);
|
||||||
data.azure_responses_version = '';
|
data.azure_responses_version = '';
|
||||||
data.region = '';
|
data.region = '';
|
||||||
|
data.vertex_key_type = 'json';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
|
||||||
|
data.vertex_key_type = 'json';
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
@@ -745,59 +753,56 @@ const EditChannelModal = (props) => {
|
|||||||
let localInputs = { ...formValues };
|
let localInputs = { ...formValues };
|
||||||
|
|
||||||
if (localInputs.type === 41) {
|
if (localInputs.type === 41) {
|
||||||
if (useManualInput) {
|
const keyType = localInputs.vertex_key_type || 'json';
|
||||||
// 手动输入模式
|
if (keyType === 'api_key') {
|
||||||
if (localInputs.key && localInputs.key.trim() !== '') {
|
// 直接作为普通字符串密钥处理
|
||||||
try {
|
if (!isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
|
||||||
// 验证 JSON 格式
|
|
||||||
const parsedKey = JSON.parse(localInputs.key);
|
|
||||||
// 确保是有效的密钥格式
|
|
||||||
localInputs.key = JSON.stringify(parsedKey);
|
|
||||||
} catch (err) {
|
|
||||||
showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (!isEdit) {
|
|
||||||
showInfo(t('请输入密钥!'));
|
showInfo(t('请输入密钥!'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 文件上传模式
|
// JSON 服务账号密钥
|
||||||
let keys = vertexKeys;
|
if (useManualInput) {
|
||||||
|
if (localInputs.key && localInputs.key.trim() !== '') {
|
||||||
// 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
|
try {
|
||||||
if (keys.length === 0 && vertexFileList.length > 0) {
|
const parsedKey = JSON.parse(localInputs.key);
|
||||||
try {
|
localInputs.key = JSON.stringify(parsedKey);
|
||||||
const parsed = await Promise.all(
|
} catch (err) {
|
||||||
vertexFileList.map(async (item) => {
|
showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
|
||||||
const fileObj = item.fileInstance;
|
return;
|
||||||
if (!fileObj) return null;
|
}
|
||||||
const txt = await fileObj.text();
|
} else if (!isEdit) {
|
||||||
return JSON.parse(txt);
|
showInfo(t('请输入密钥!'));
|
||||||
}),
|
|
||||||
);
|
|
||||||
keys = parsed.filter(Boolean);
|
|
||||||
} catch (err) {
|
|
||||||
showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 创建模式必须上传密钥;编辑模式可选
|
|
||||||
if (keys.length === 0) {
|
|
||||||
if (!isEdit) {
|
|
||||||
showInfo(t('请上传密钥文件!'));
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// 编辑模式且未上传新密钥,不修改 key
|
|
||||||
delete localInputs.key;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 有新密钥,则覆盖
|
// 文件上传模式
|
||||||
if (batch) {
|
let keys = vertexKeys;
|
||||||
localInputs.key = JSON.stringify(keys);
|
if (keys.length === 0 && vertexFileList.length > 0) {
|
||||||
|
try {
|
||||||
|
const parsed = await Promise.all(
|
||||||
|
vertexFileList.map(async (item) => {
|
||||||
|
const fileObj = item.fileInstance;
|
||||||
|
if (!fileObj) return null;
|
||||||
|
const txt = await fileObj.text();
|
||||||
|
return JSON.parse(txt);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
keys = parsed.filter(Boolean);
|
||||||
|
} catch (err) {
|
||||||
|
showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keys.length === 0) {
|
||||||
|
if (!isEdit) {
|
||||||
|
showInfo(t('请上传密钥文件!'));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
delete localInputs.key;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
localInputs.key = JSON.stringify(keys[0]);
|
localInputs.key = batch ? JSON.stringify(keys) : JSON.stringify(keys[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -853,6 +858,8 @@ const EditChannelModal = (props) => {
|
|||||||
delete localInputs.pass_through_body_enabled;
|
delete localInputs.pass_through_body_enabled;
|
||||||
delete localInputs.system_prompt;
|
delete localInputs.system_prompt;
|
||||||
delete localInputs.system_prompt_override;
|
delete localInputs.system_prompt_override;
|
||||||
|
// 顶层的 vertex_key_type 不应发送给后端
|
||||||
|
delete localInputs.vertex_key_type;
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
|
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
|
||||||
@@ -1178,8 +1185,40 @@ const EditChannelModal = (props) => {
|
|||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{inputs.type === 41 && (
|
||||||
|
<Form.Select
|
||||||
|
field='vertex_key_type'
|
||||||
|
label={t('密钥格式')}
|
||||||
|
placeholder={t('请选择密钥格式')}
|
||||||
|
optionList={[
|
||||||
|
{ label: 'JSON', value: 'json' },
|
||||||
|
{ label: 'API Key', value: 'api_key' },
|
||||||
|
]}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={inputs.vertex_key_type || 'json'}
|
||||||
|
onChange={(value) => {
|
||||||
|
// 更新设置中的 vertex_key_type
|
||||||
|
handleChannelOtherSettingsChange('vertex_key_type', value);
|
||||||
|
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
|
||||||
|
if (value === 'api_key') {
|
||||||
|
setBatch(false);
|
||||||
|
setUseManualInput(false);
|
||||||
|
setVertexKeys([]);
|
||||||
|
setVertexFileList([]);
|
||||||
|
if (formApiRef.current) {
|
||||||
|
formApiRef.current.setValue('vertex_files', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
extraText={
|
||||||
|
inputs.vertex_key_type === 'api_key'
|
||||||
|
? t('API Key 模式下不支持批量创建')
|
||||||
|
: t('JSON 模式支持手动输入或上传服务账号 JSON')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{batch ? (
|
{batch ? (
|
||||||
inputs.type === 41 ? (
|
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)')}
|
||||||
@@ -1243,7 +1282,7 @@ const EditChannelModal = (props) => {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{inputs.type === 41 ? (
|
{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'>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import React, { useRef } from 'react';
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Typography,
|
Typography,
|
||||||
|
Tag,
|
||||||
Card,
|
Card,
|
||||||
Button,
|
Button,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Spin,
|
Spin, Tooltip
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
|
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
|
||||||
import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react';
|
import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react';
|
||||||
@@ -68,6 +69,7 @@ const RechargeCard = ({
|
|||||||
userState,
|
userState,
|
||||||
renderQuota,
|
renderQuota,
|
||||||
statusLoading,
|
statusLoading,
|
||||||
|
topupInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const onlineFormApiRef = useRef(null);
|
const onlineFormApiRef = useRef(null);
|
||||||
const redeemFormApiRef = useRef(null);
|
const redeemFormApiRef = useRef(null);
|
||||||
@@ -261,44 +263,58 @@ const RechargeCard = ({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
|
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
|
||||||
<Form.Slot label={t('选择支付方式')}>
|
<Form.Slot label={t('选择支付方式')}>
|
||||||
<Space wrap>
|
{payMethods && payMethods.length > 0 ? (
|
||||||
{payMethods.map((payMethod) => (
|
<Space wrap>
|
||||||
<Button
|
{payMethods.map((payMethod) => {
|
||||||
key={payMethod.type}
|
const minTopupVal = Number(payMethod.min_topup) || 0;
|
||||||
theme='outline'
|
const isStripe = payMethod.type === 'stripe';
|
||||||
type='tertiary'
|
const disabled =
|
||||||
onClick={() => preTopUp(payMethod.type)}
|
(!enableOnlineTopUp && !isStripe) ||
|
||||||
disabled={
|
(!enableStripeTopUp && isStripe) ||
|
||||||
(!enableOnlineTopUp &&
|
minTopupVal > Number(topUpCount || 0);
|
||||||
payMethod.type !== 'stripe') ||
|
|
||||||
(!enableStripeTopUp &&
|
const buttonEl = (
|
||||||
payMethod.type === 'stripe')
|
<Button
|
||||||
}
|
key={payMethod.type}
|
||||||
loading={
|
theme='outline'
|
||||||
paymentLoading && payWay === payMethod.type
|
type='tertiary'
|
||||||
}
|
onClick={() => preTopUp(payMethod.type)}
|
||||||
icon={
|
disabled={disabled}
|
||||||
payMethod.type === 'alipay' ? (
|
loading={paymentLoading && payWay === payMethod.type}
|
||||||
<SiAlipay size={18} color='#1677FF' />
|
icon={
|
||||||
) : payMethod.type === 'wxpay' ? (
|
payMethod.type === 'alipay' ? (
|
||||||
<SiWechat size={18} color='#07C160' />
|
<SiAlipay size={18} color='#1677FF' />
|
||||||
) : payMethod.type === 'stripe' ? (
|
) : payMethod.type === 'wxpay' ? (
|
||||||
<SiStripe size={18} color='#635BFF' />
|
<SiWechat size={18} color='#07C160' />
|
||||||
) : (
|
) : payMethod.type === 'stripe' ? (
|
||||||
<CreditCard
|
<SiStripe size={18} color='#635BFF' />
|
||||||
size={18}
|
) : (
|
||||||
color={
|
<CreditCard
|
||||||
payMethod.color ||
|
size={18}
|
||||||
'var(--semi-color-text-2)'
|
color={payMethod.color || 'var(--semi-color-text-2)'}
|
||||||
}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
}
|
className='!rounded-lg !px-4 !py-2'
|
||||||
>
|
>
|
||||||
{payMethod.name}
|
{payMethod.name}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
);
|
||||||
</Space>
|
|
||||||
|
return disabled && minTopupVal > Number(topUpCount || 0) ? (
|
||||||
|
<Tooltip content={t('此支付方式最低充值金额为') + ' ' + minTopupVal} key={payMethod.type}>
|
||||||
|
{buttonEl}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<React.Fragment key={payMethod.type}>{buttonEl}</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<div className='text-gray-500 text-sm p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300'>
|
||||||
|
{t('暂无可用的支付方式,请联系管理员配置')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Form.Slot>
|
</Form.Slot>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -306,41 +322,60 @@ const RechargeCard = ({
|
|||||||
|
|
||||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||||
<Form.Slot label={t('选择充值额度')}>
|
<Form.Slot label={t('选择充值额度')}>
|
||||||
<Space wrap>
|
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'>
|
||||||
{presetAmounts.map((preset, index) => (
|
{presetAmounts.map((preset, index) => {
|
||||||
<Button
|
const discount = preset.discount || topupInfo?.discount?.[preset.value] || 1.0;
|
||||||
key={index}
|
const originalPrice = preset.value * priceRatio;
|
||||||
theme={
|
const discountedPrice = originalPrice * discount;
|
||||||
selectedPreset === preset.value
|
const hasDiscount = discount < 1.0;
|
||||||
? 'solid'
|
const actualPay = discountedPrice;
|
||||||
: 'outline'
|
const save = originalPrice - discountedPrice;
|
||||||
}
|
|
||||||
type={
|
return (
|
||||||
selectedPreset === preset.value
|
<Card
|
||||||
? 'primary'
|
key={index}
|
||||||
: 'tertiary'
|
style={{
|
||||||
}
|
cursor: 'pointer',
|
||||||
onClick={() => {
|
border: selectedPreset === preset.value
|
||||||
selectPresetAmount(preset);
|
? '2px solid var(--semi-color-primary)'
|
||||||
onlineFormApiRef.current?.setValue(
|
: '1px solid var(--semi-color-border)',
|
||||||
'topUpCount',
|
height: '100%',
|
||||||
preset.value,
|
width: '100%'
|
||||||
);
|
}}
|
||||||
}}
|
bodyStyle={{ padding: '12px' }}
|
||||||
className='!rounded-lg !py-2 !px-3'
|
onClick={() => {
|
||||||
>
|
selectPresetAmount(preset);
|
||||||
<div className='flex items-center gap-2'>
|
onlineFormApiRef.current?.setValue(
|
||||||
<Coins size={14} className='opacity-80' />
|
'topUpCount',
|
||||||
<span className='font-medium'>
|
preset.value,
|
||||||
{formatLargeNumber(preset.value)}
|
);
|
||||||
</span>
|
}}
|
||||||
<span className='text-xs text-gray-500'>
|
>
|
||||||
¥{(preset.value * priceRatio).toFixed(2)}
|
<div style={{ textAlign: 'center' }}>
|
||||||
</span>
|
<Typography.Title heading={6} style={{ margin: '0 0 8px 0' }}>
|
||||||
</div>
|
<Coins size={18} />
|
||||||
</Button>
|
{formatLargeNumber(preset.value)}
|
||||||
))}
|
{hasDiscount && (
|
||||||
</Space>
|
<Tag style={{ marginLeft: 4 }} color="green">
|
||||||
|
{t('折').includes('off') ?
|
||||||
|
((1 - parseFloat(discount)) * 100).toFixed(1) :
|
||||||
|
(discount * 10).toFixed(1)}{t('折')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Typography.Title>
|
||||||
|
<div style={{
|
||||||
|
color: 'var(--semi-color-text-2)',
|
||||||
|
fontSize: '12px',
|
||||||
|
margin: '4px 0'
|
||||||
|
}}>
|
||||||
|
{t('实付')} {actualPay.toFixed(2)},
|
||||||
|
{hasDiscount ? `${t('节省')} ${save.toFixed(2)}` : `${t('节省')} 0.00`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</Form.Slot>
|
</Form.Slot>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ const TopUp = () => {
|
|||||||
const [presetAmounts, setPresetAmounts] = useState([]);
|
const [presetAmounts, setPresetAmounts] = useState([]);
|
||||||
const [selectedPreset, setSelectedPreset] = useState(null);
|
const [selectedPreset, setSelectedPreset] = useState(null);
|
||||||
|
|
||||||
|
// 充值配置信息
|
||||||
|
const [topupInfo, setTopupInfo] = useState({
|
||||||
|
amount_options: [],
|
||||||
|
discount: {}
|
||||||
|
});
|
||||||
|
|
||||||
const topUp = async () => {
|
const topUp = async () => {
|
||||||
if (redemptionCode === '') {
|
if (redemptionCode === '') {
|
||||||
showInfo(t('请输入兑换码!'));
|
showInfo(t('请输入兑换码!'));
|
||||||
@@ -248,6 +254,99 @@ const TopUp = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取充值配置信息
|
||||||
|
const getTopupInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/user/topup/info');
|
||||||
|
const { message, data, success } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setTopupInfo({
|
||||||
|
amount_options: data.amount_options || [],
|
||||||
|
discount: data.discount || {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理支付方式
|
||||||
|
let payMethods = data.pay_methods || [];
|
||||||
|
try {
|
||||||
|
if (typeof payMethods === 'string') {
|
||||||
|
payMethods = JSON.parse(payMethods);
|
||||||
|
}
|
||||||
|
if (payMethods && payMethods.length > 0) {
|
||||||
|
// 检查name和type是否为空
|
||||||
|
payMethods = payMethods.filter((method) => {
|
||||||
|
return method.name && method.type;
|
||||||
|
});
|
||||||
|
// 如果没有color,则设置默认颜色
|
||||||
|
payMethods = payMethods.map((method) => {
|
||||||
|
// 规范化最小充值数
|
||||||
|
const normalizedMinTopup = Number(method.min_topup);
|
||||||
|
method.min_topup = Number.isFinite(normalizedMinTopup) ? normalizedMinTopup : 0;
|
||||||
|
|
||||||
|
// Stripe 的最小充值从后端字段回填
|
||||||
|
if (method.type === 'stripe' && (!method.min_topup || method.min_topup <= 0)) {
|
||||||
|
const stripeMin = Number(data.stripe_min_topup);
|
||||||
|
if (Number.isFinite(stripeMin)) {
|
||||||
|
method.min_topup = stripeMin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!method.color) {
|
||||||
|
if (method.type === 'alipay') {
|
||||||
|
method.color = 'rgba(var(--semi-blue-5), 1)';
|
||||||
|
} else if (method.type === 'wxpay') {
|
||||||
|
method.color = 'rgba(var(--semi-green-5), 1)';
|
||||||
|
} else if (method.type === 'stripe') {
|
||||||
|
method.color = 'rgba(var(--semi-purple-5), 1)';
|
||||||
|
} else {
|
||||||
|
method.color = 'rgba(var(--semi-primary-5), 1)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return method;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
payMethods = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果启用了 Stripe 支付,添加到支付方法列表
|
||||||
|
// 这个逻辑现在由后端处理,如果 Stripe 启用,后端会在 pay_methods 中包含它
|
||||||
|
|
||||||
|
setPayMethods(payMethods);
|
||||||
|
const enableStripeTopUp = data.enable_stripe_topup || false;
|
||||||
|
const enableOnlineTopUp = data.enable_online_topup || false;
|
||||||
|
const minTopUpValue = enableOnlineTopUp? data.min_topup : enableStripeTopUp? data.stripe_min_topup : 1;
|
||||||
|
setEnableOnlineTopUp(enableOnlineTopUp);
|
||||||
|
setEnableStripeTopUp(enableStripeTopUp);
|
||||||
|
setMinTopUp(minTopUpValue);
|
||||||
|
setTopUpCount(minTopUpValue);
|
||||||
|
|
||||||
|
// 如果没有自定义充值数量选项,根据最小充值金额生成预设充值额度选项
|
||||||
|
if (topupInfo.amount_options.length === 0) {
|
||||||
|
setPresetAmounts(generatePresetAmounts(minTopUpValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化显示实付金额
|
||||||
|
getAmount(minTopUpValue);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('解析支付方式失败:', e);
|
||||||
|
setPayMethods([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有自定义充值数量选项,使用它们替换默认的预设选项
|
||||||
|
if (data.amount_options && data.amount_options.length > 0) {
|
||||||
|
const customPresets = data.amount_options.map(amount => ({
|
||||||
|
value: amount,
|
||||||
|
discount: data.discount[amount] || 1.0
|
||||||
|
}));
|
||||||
|
setPresetAmounts(customPresets);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('获取充值配置失败:', data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取充值配置异常:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 获取邀请链接
|
// 获取邀请链接
|
||||||
const getAffLink = async () => {
|
const getAffLink = async () => {
|
||||||
const res = await API.get('/api/user/aff');
|
const res = await API.get('/api/user/aff');
|
||||||
@@ -290,52 +389,7 @@ const TopUp = () => {
|
|||||||
getUserQuota().then();
|
getUserQuota().then();
|
||||||
}
|
}
|
||||||
setTransferAmount(getQuotaPerUnit());
|
setTransferAmount(getQuotaPerUnit());
|
||||||
|
}, []);
|
||||||
let payMethods = localStorage.getItem('pay_methods');
|
|
||||||
try {
|
|
||||||
payMethods = JSON.parse(payMethods);
|
|
||||||
if (payMethods && payMethods.length > 0) {
|
|
||||||
// 检查name和type是否为空
|
|
||||||
payMethods = payMethods.filter((method) => {
|
|
||||||
return method.name && method.type;
|
|
||||||
});
|
|
||||||
// 如果没有color,则设置默认颜色
|
|
||||||
payMethods = payMethods.map((method) => {
|
|
||||||
if (!method.color) {
|
|
||||||
if (method.type === 'alipay') {
|
|
||||||
method.color = 'rgba(var(--semi-blue-5), 1)';
|
|
||||||
} else if (method.type === 'wxpay') {
|
|
||||||
method.color = 'rgba(var(--semi-green-5), 1)';
|
|
||||||
} else if (method.type === 'stripe') {
|
|
||||||
method.color = 'rgba(var(--semi-purple-5), 1)';
|
|
||||||
} else {
|
|
||||||
method.color = 'rgba(var(--semi-primary-5), 1)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return method;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
payMethods = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果启用了 Stripe 支付,添加到支付方法列表
|
|
||||||
if (statusState?.status?.enable_stripe_topup) {
|
|
||||||
const hasStripe = payMethods.some((method) => method.type === 'stripe');
|
|
||||||
if (!hasStripe) {
|
|
||||||
payMethods.push({
|
|
||||||
name: 'Stripe',
|
|
||||||
type: 'stripe',
|
|
||||||
color: 'rgba(var(--semi-purple-5), 1)',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPayMethods(payMethods);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
showError(t('支付方式配置错误, 请联系管理员'));
|
|
||||||
}
|
|
||||||
}, [statusState?.status?.enable_stripe_topup]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (affFetchedRef.current) return;
|
if (affFetchedRef.current) return;
|
||||||
@@ -343,20 +397,18 @@ const TopUp = () => {
|
|||||||
getAffLink().then();
|
getAffLink().then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 在 statusState 可用时获取充值信息
|
||||||
|
useEffect(() => {
|
||||||
|
getTopupInfo().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusState?.status) {
|
if (statusState?.status) {
|
||||||
const minTopUpValue = statusState.status.min_topup || 1;
|
// const minTopUpValue = statusState.status.min_topup || 1;
|
||||||
setMinTopUp(minTopUpValue);
|
// setMinTopUp(minTopUpValue);
|
||||||
setTopUpCount(minTopUpValue);
|
// setTopUpCount(minTopUpValue);
|
||||||
setTopUpLink(statusState.status.top_up_link || '');
|
setTopUpLink(statusState.status.top_up_link || '');
|
||||||
setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
|
|
||||||
setPriceRatio(statusState.status.price || 1);
|
setPriceRatio(statusState.status.price || 1);
|
||||||
setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
|
|
||||||
|
|
||||||
// 根据最小充值金额生成预设充值额度选项
|
|
||||||
setPresetAmounts(generatePresetAmounts(minTopUpValue));
|
|
||||||
// 初始化显示实付金额
|
|
||||||
getAmount(minTopUpValue);
|
|
||||||
|
|
||||||
setStatusLoading(false);
|
setStatusLoading(false);
|
||||||
}
|
}
|
||||||
@@ -431,7 +483,11 @@ const TopUp = () => {
|
|||||||
const selectPresetAmount = (preset) => {
|
const selectPresetAmount = (preset) => {
|
||||||
setTopUpCount(preset.value);
|
setTopUpCount(preset.value);
|
||||||
setSelectedPreset(preset.value);
|
setSelectedPreset(preset.value);
|
||||||
setAmount(preset.value * priceRatio);
|
|
||||||
|
// 计算实际支付金额,考虑折扣
|
||||||
|
const discount = preset.discount || topupInfo.discount[preset.value] || 1.0;
|
||||||
|
const discountedAmount = preset.value * priceRatio * discount;
|
||||||
|
setAmount(discountedAmount);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化大数字显示
|
// 格式化大数字显示
|
||||||
@@ -475,6 +531,8 @@ const TopUp = () => {
|
|||||||
renderAmount={renderAmount}
|
renderAmount={renderAmount}
|
||||||
payWay={payWay}
|
payWay={payWay}
|
||||||
payMethods={payMethods}
|
payMethods={payMethods}
|
||||||
|
amountNumber={amount}
|
||||||
|
discountRate={topupInfo?.discount?.[topUpCount] || 1.0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 用户信息头部 */}
|
{/* 用户信息头部 */}
|
||||||
@@ -512,6 +570,7 @@ const TopUp = () => {
|
|||||||
userState={userState}
|
userState={userState}
|
||||||
renderQuota={renderQuota}
|
renderQuota={renderQuota}
|
||||||
statusLoading={statusLoading}
|
statusLoading={statusLoading}
|
||||||
|
topupInfo={topupInfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ const PaymentConfirmModal = ({
|
|||||||
renderAmount,
|
renderAmount,
|
||||||
payWay,
|
payWay,
|
||||||
payMethods,
|
payMethods,
|
||||||
|
// 新增:用于显示折扣明细
|
||||||
|
amountNumber,
|
||||||
|
discountRate,
|
||||||
}) => {
|
}) => {
|
||||||
|
const hasDiscount = discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0;
|
||||||
|
const originalAmount = hasDiscount ? (amountNumber / discountRate) : 0;
|
||||||
|
const discountAmount = hasDiscount ? (originalAmount - amountNumber) : 0;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
@@ -71,11 +77,38 @@ const PaymentConfirmModal = ({
|
|||||||
{amountLoading ? (
|
{amountLoading ? (
|
||||||
<Skeleton.Title style={{ width: '60px', height: '16px' }} />
|
<Skeleton.Title style={{ width: '60px', height: '16px' }} />
|
||||||
) : (
|
) : (
|
||||||
<Text strong className='font-bold' style={{ color: 'red' }}>
|
<div className='flex items-baseline space-x-2'>
|
||||||
{renderAmount()}
|
<Text strong className='font-bold' style={{ color: 'red' }}>
|
||||||
</Text>
|
{renderAmount()}
|
||||||
|
</Text>
|
||||||
|
{hasDiscount && (
|
||||||
|
<Text size='small' className='text-rose-500'>
|
||||||
|
{Math.round(discountRate * 100)}%
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{hasDiscount && !amountLoading && (
|
||||||
|
<>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<Text className='text-slate-500 dark:text-slate-400'>
|
||||||
|
{t('原价')}:
|
||||||
|
</Text>
|
||||||
|
<Text delete className='text-slate-500 dark:text-slate-400'>
|
||||||
|
{`${originalAmount.toFixed(2)} ${t('元')}`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<Text className='text-slate-500 dark:text-slate-400'>
|
||||||
|
{t('优惠')}:
|
||||||
|
</Text>
|
||||||
|
<Text className='text-emerald-600 dark:text-emerald-400'>
|
||||||
|
{`- ${discountAmount.toFixed(2)} ${t('元')}`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Text strong className='text-slate-700 dark:text-slate-200'>
|
<Text strong className='text-slate-700 dark:text-slate-200'>
|
||||||
{t('支付方式')}:
|
{t('支付方式')}:
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export function setStatusData(data) {
|
|||||||
localStorage.setItem('enable_task', data.enable_task);
|
localStorage.setItem('enable_task', data.enable_task);
|
||||||
localStorage.setItem('enable_data_export', data.enable_data_export);
|
localStorage.setItem('enable_data_export', data.enable_data_export);
|
||||||
localStorage.setItem('chats', JSON.stringify(data.chats));
|
localStorage.setItem('chats', JSON.stringify(data.chats));
|
||||||
localStorage.setItem('pay_methods', JSON.stringify(data.pay_methods));
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'data_export_default_time',
|
'data_export_default_time',
|
||||||
data.data_export_default_time,
|
data.data_export_default_time,
|
||||||
|
|||||||
@@ -1017,7 +1017,7 @@ export function renderModelPrice(
|
|||||||
cacheRatio = 1.0,
|
cacheRatio = 1.0,
|
||||||
image = false,
|
image = false,
|
||||||
imageRatio = 1.0,
|
imageRatio = 1.0,
|
||||||
imageInputTokens = 0,
|
imageOutputTokens = 0,
|
||||||
webSearch = false,
|
webSearch = false,
|
||||||
webSearchCallCount = 0,
|
webSearchCallCount = 0,
|
||||||
webSearchPrice = 0,
|
webSearchPrice = 0,
|
||||||
@@ -1027,8 +1027,6 @@ export function renderModelPrice(
|
|||||||
audioInputSeperatePrice = false,
|
audioInputSeperatePrice = false,
|
||||||
audioInputTokens = 0,
|
audioInputTokens = 0,
|
||||||
audioInputPrice = 0,
|
audioInputPrice = 0,
|
||||||
imageOutputTokens = 0,
|
|
||||||
imageOutputPrice = 0,
|
|
||||||
) {
|
) {
|
||||||
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
||||||
groupRatio,
|
groupRatio,
|
||||||
@@ -1059,9 +1057,9 @@ export function renderModelPrice(
|
|||||||
let effectiveInputTokens =
|
let effectiveInputTokens =
|
||||||
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
||||||
// Handle image tokens if present
|
// Handle image tokens if present
|
||||||
if (image && imageInputTokens > 0) {
|
if (image && imageOutputTokens > 0) {
|
||||||
effectiveInputTokens =
|
effectiveInputTokens =
|
||||||
inputTokens - imageInputTokens + imageInputTokens * imageRatio;
|
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
|
||||||
}
|
}
|
||||||
if (audioInputTokens > 0) {
|
if (audioInputTokens > 0) {
|
||||||
effectiveInputTokens -= audioInputTokens;
|
effectiveInputTokens -= audioInputTokens;
|
||||||
@@ -1071,8 +1069,7 @@ export function renderModelPrice(
|
|||||||
(audioInputTokens / 1000000) * audioInputPrice * groupRatio +
|
(audioInputTokens / 1000000) * audioInputPrice * groupRatio +
|
||||||
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
||||||
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
|
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
|
||||||
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
|
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
|
||||||
(imageOutputTokens / 1000000) * imageOutputPrice * groupRatio;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1107,7 +1104,7 @@ export function renderModelPrice(
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{image && imageInputTokens > 0 && (
|
{image && imageOutputTokens > 0 && (
|
||||||
<p>
|
<p>
|
||||||
{i18next.t(
|
{i18next.t(
|
||||||
'图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
|
'图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
|
||||||
@@ -1134,26 +1131,17 @@ export function renderModelPrice(
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{imageOutputPrice > 0 && imageOutputTokens > 0 && (
|
|
||||||
<p>
|
|
||||||
{i18next.t('图片输出价格:${{price}} * 分组倍率{{ratio}} = ${{total}} / 1M tokens', {
|
|
||||||
price: imageOutputPrice,
|
|
||||||
ratio: groupRatio,
|
|
||||||
total: imageOutputPrice * groupRatio,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p></p>
|
<p></p>
|
||||||
<p>
|
<p>
|
||||||
{(() => {
|
{(() => {
|
||||||
// 构建输入部分描述
|
// 构建输入部分描述
|
||||||
let inputDesc = '';
|
let inputDesc = '';
|
||||||
if (image && imageInputTokens > 0) {
|
if (image && imageOutputTokens > 0) {
|
||||||
inputDesc = i18next.t(
|
inputDesc = i18next.t(
|
||||||
'(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}',
|
'(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}',
|
||||||
{
|
{
|
||||||
nonImageInput: inputTokens - imageInputTokens,
|
nonImageInput: inputTokens - imageOutputTokens,
|
||||||
imageInput: imageInputTokens,
|
imageInput: imageOutputTokens,
|
||||||
imageRatio: imageRatio,
|
imageRatio: imageRatio,
|
||||||
price: inputRatioPrice,
|
price: inputRatioPrice,
|
||||||
},
|
},
|
||||||
@@ -1223,16 +1211,6 @@ export function renderModelPrice(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: '',
|
: '',
|
||||||
imageOutputPrice > 0 && imageOutputTokens > 0
|
|
||||||
? i18next.t(
|
|
||||||
' + 图片输出 {{tokenCounts}} tokens * ${{price}} / 1M tokens * 分组倍率{{ratio}}',
|
|
||||||
{
|
|
||||||
tokenCounts: imageOutputTokens,
|
|
||||||
price: imageOutputPrice,
|
|
||||||
ratio: groupRatio,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
return i18next.t(
|
return i18next.t(
|
||||||
|
|||||||
@@ -447,8 +447,6 @@ export const useLogsData = () => {
|
|||||||
other?.audio_input_seperate_price || false,
|
other?.audio_input_seperate_price || false,
|
||||||
other?.audio_input_token_count || 0,
|
other?.audio_input_token_count || 0,
|
||||||
other?.audio_input_price || 0,
|
other?.audio_input_price || 0,
|
||||||
other?.image_output_token_count || 0,
|
|
||||||
other?.image_output_price || 0,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expandDataLocal.push({
|
expandDataLocal.push({
|
||||||
|
|||||||
@@ -1993,7 +1993,7 @@
|
|||||||
"安全验证": "Security verification",
|
"安全验证": "Security verification",
|
||||||
"验证": "Verify",
|
"验证": "Verify",
|
||||||
"为了保护账户安全,请验证您的两步验证码。": "To protect account security, please verify your two-factor authentication code.",
|
"为了保护账户安全,请验证您的两步验证码。": "To protect account security, please verify your two-factor authentication code.",
|
||||||
"支持6位TOTP验证码或8位备用码": "Supports 6-digit TOTP verification code or 8-digit backup code",
|
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Supports 6-digit TOTP verification code or 8-digit backup code, can be configured or viewed in `Personal Settings - Security Settings - Two-Factor Authentication Settings`.",
|
||||||
"获取密钥失败": "Failed to get key",
|
"获取密钥失败": "Failed to get key",
|
||||||
"查看密钥": "View key",
|
"查看密钥": "View key",
|
||||||
"查看渠道密钥": "View channel key",
|
"查看渠道密钥": "View channel key",
|
||||||
@@ -2080,5 +2080,9 @@
|
|||||||
"官方": "Official",
|
"官方": "Official",
|
||||||
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "The model community needs everyone's contribution. If you find incorrect data or want to contribute new models, please visit:",
|
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "The model community needs everyone's contribution. If you find incorrect data or want to contribute new models, please visit:",
|
||||||
"是": "Yes",
|
"是": "Yes",
|
||||||
"否": "No"
|
"否": "No",
|
||||||
|
"原价": "Original price",
|
||||||
|
"优惠": "Discount",
|
||||||
|
"折": "% off",
|
||||||
|
"节省": "Save"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,17 +130,19 @@ export default function GeneralSettings(props) {
|
|||||||
showClear
|
showClear
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
{inputs.QuotaPerUnit !== '500000' && inputs.QuotaPerUnit !== 500000 && (
|
||||||
<Form.Input
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
field={'QuotaPerUnit'}
|
<Form.Input
|
||||||
label={t('单位美元额度')}
|
field={'QuotaPerUnit'}
|
||||||
initValue={''}
|
label={t('单位美元额度')}
|
||||||
placeholder={t('一单位货币能兑换的额度')}
|
initValue={''}
|
||||||
onChange={handleFieldChange('QuotaPerUnit')}
|
placeholder={t('一单位货币能兑换的额度')}
|
||||||
showClear
|
onChange={handleFieldChange('QuotaPerUnit')}
|
||||||
onClick={() => setShowQuotaWarning(true)}
|
showClear
|
||||||
/>
|
onClick={() => setShowQuotaWarning(true)}
|
||||||
</Col>
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field={'USDExchangeRate'}
|
field={'USDExchangeRate'}
|
||||||
@@ -194,7 +196,7 @@ export default function GeneralSettings(props) {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field={'DemoSiteEnabled'}
|
field={'DemoSiteEnabled'}
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export default function SettingsHeaderNavModules(props) {
|
|||||||
headerNavModules.pricing?.requireAuth || false
|
headerNavModules.pricing?.requireAuth || false
|
||||||
}
|
}
|
||||||
onChange={handlePricingAuthChange}
|
onChange={handlePricingAuthChange}
|
||||||
size='small'
|
size='default'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
TopupGroupRatio: '',
|
TopupGroupRatio: '',
|
||||||
CustomCallbackAddress: '',
|
CustomCallbackAddress: '',
|
||||||
PayMethods: '',
|
PayMethods: '',
|
||||||
|
AmountOptions: '',
|
||||||
|
AmountDiscount: '',
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
@@ -62,7 +64,30 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
TopupGroupRatio: props.options.TopupGroupRatio || '',
|
TopupGroupRatio: props.options.TopupGroupRatio || '',
|
||||||
CustomCallbackAddress: props.options.CustomCallbackAddress || '',
|
CustomCallbackAddress: props.options.CustomCallbackAddress || '',
|
||||||
PayMethods: props.options.PayMethods || '',
|
PayMethods: props.options.PayMethods || '',
|
||||||
|
AmountOptions: props.options.AmountOptions || '',
|
||||||
|
AmountDiscount: props.options.AmountDiscount || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 美化 JSON 展示
|
||||||
|
try {
|
||||||
|
if (currentInputs.AmountOptions) {
|
||||||
|
currentInputs.AmountOptions = JSON.stringify(
|
||||||
|
JSON.parse(currentInputs.AmountOptions),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
if (currentInputs.AmountDiscount) {
|
||||||
|
currentInputs.AmountDiscount = JSON.stringify(
|
||||||
|
JSON.parse(currentInputs.AmountDiscount),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
setInputs(currentInputs);
|
setInputs(currentInputs);
|
||||||
setOriginInputs({ ...currentInputs });
|
setOriginInputs({ ...currentInputs });
|
||||||
formApiRef.current.setValues(currentInputs);
|
formApiRef.current.setValues(currentInputs);
|
||||||
@@ -93,6 +118,20 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (originInputs['AmountOptions'] !== inputs.AmountOptions && inputs.AmountOptions.trim() !== '') {
|
||||||
|
if (!verifyJSON(inputs.AmountOptions)) {
|
||||||
|
showError(t('自定义充值数量选项不是合法的 JSON 数组'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount && inputs.AmountDiscount.trim() !== '') {
|
||||||
|
if (!verifyJSON(inputs.AmountDiscount)) {
|
||||||
|
showError(t('充值金额折扣配置不是合法的 JSON 对象'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const options = [
|
const options = [
|
||||||
@@ -123,6 +162,12 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
if (originInputs['PayMethods'] !== inputs.PayMethods) {
|
if (originInputs['PayMethods'] !== inputs.PayMethods) {
|
||||||
options.push({ key: 'PayMethods', value: inputs.PayMethods });
|
options.push({ key: 'PayMethods', value: inputs.PayMethods });
|
||||||
}
|
}
|
||||||
|
if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
|
||||||
|
options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions });
|
||||||
|
}
|
||||||
|
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) {
|
||||||
|
options.push({ key: 'payment_setting.amount_discount', value: inputs.AmountDiscount });
|
||||||
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
const requestQueue = options.map((opt) =>
|
const requestQueue = options.map((opt) =>
|
||||||
@@ -228,6 +273,37 @@ export default function SettingsPaymentGateway(props) {
|
|||||||
placeholder={t('为一个 JSON 文本')}
|
placeholder={t('为一个 JSON 文本')}
|
||||||
autosize
|
autosize
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Row
|
||||||
|
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
>
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.TextArea
|
||||||
|
field='AmountOptions'
|
||||||
|
label={t('自定义充值数量选项')}
|
||||||
|
placeholder={t('为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]')}
|
||||||
|
autosize
|
||||||
|
extraText={t('设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row
|
||||||
|
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
>
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.TextArea
|
||||||
|
field='AmountDiscount'
|
||||||
|
label={t('充值金额折扣配置')}
|
||||||
|
placeholder={t('为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}')}
|
||||||
|
autosize
|
||||||
|
extraText={t('设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
|
<Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
Reference in New Issue
Block a user