Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| deff59a5be | |||
| 3c516084f8 | |||
| 4d675b4d1f | |||
| 87b426f306 | |||
| 49db5147c3 | |||
| 13122aa0fa | |||
| dcd0911612 | |||
| e904579a5b | |||
| e80d867f38 | |||
| cf86fe5fea | |||
| 42846c692e | |||
| 1911520eba | |||
| 2c3ae32c8e | |||
| 64f41efc47 | |||
| 498199b37d | |||
| ff29900f30 | |||
| ed6ff0f267 | |||
| d955a0c080 | |||
| d096a2e5b7 | |||
| d2fb485d34 | |||
| 04f5dd0206 | |||
| ede0ad117b | |||
| 5bb8fe6af5 | |||
| a1a92c1918 | |||
| a4d1ed6da5 | |||
| 669e596ff7 | |||
| 1daeac42ef | |||
| e70bfa2d57 | |||
| bd09b47ef4 | |||
| d595ef4990 | |||
| 2270f63c00 | |||
| 202a433f86 |
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://afdian.com/a/new-api'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
+1
-1
@@ -383,7 +383,7 @@ docker run --name new-api -d --restart always \
|
||||
2. 在应用商店搜索 **New-API**
|
||||
3. 一键安装
|
||||
|
||||
📖 [图文教程](./docs/BT.md)
|
||||
📖 [图文教程](./docs/installation/BT.md)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ var (
|
||||
DownloadRateLimitDuration int64 = 60
|
||||
|
||||
// Per-user search rate limit (applies after authentication, keyed by user ID)
|
||||
SearchRateLimitEnable = true
|
||||
SearchRateLimitNum = 10
|
||||
SearchRateLimitDuration int64 = 60
|
||||
)
|
||||
@@ -211,5 +212,6 @@ const (
|
||||
const (
|
||||
TopUpStatusPending = "pending"
|
||||
TopUpStatusSuccess = "success"
|
||||
TopUpStatusFailed = "failed"
|
||||
TopUpStatusExpired = "expired"
|
||||
)
|
||||
|
||||
+5
-1
@@ -120,6 +120,10 @@ func InitEnv() {
|
||||
CriticalRateLimitEnable = GetEnvOrDefaultBool("CRITICAL_RATE_LIMIT_ENABLE", true)
|
||||
CriticalRateLimitNum = GetEnvOrDefault("CRITICAL_RATE_LIMIT", 20)
|
||||
CriticalRateLimitDuration = int64(GetEnvOrDefault("CRITICAL_RATE_LIMIT_DURATION", 20*60))
|
||||
|
||||
SearchRateLimitEnable = GetEnvOrDefaultBool("SEARCH_RATE_LIMIT_ENABLE", true)
|
||||
SearchRateLimitNum = GetEnvOrDefault("SEARCH_RATE_LIMIT", 10)
|
||||
SearchRateLimitDuration = int64(GetEnvOrDefault("SEARCH_RATE_LIMIT_DURATION", 60))
|
||||
initConstantEnv()
|
||||
}
|
||||
|
||||
@@ -127,7 +131,7 @@ func initConstantEnv() {
|
||||
constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 300)
|
||||
constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
||||
constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 64)
|
||||
constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 64)
|
||||
constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 128)
|
||||
// MaxRequestBodyMB 请求体最大大小(解压后),用于防止超大请求/zip bomb导致内存暴涨
|
||||
constant.MaxRequestBodyMB = GetEnvOrDefault("MAX_REQUEST_BODY_MB", 128)
|
||||
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
||||
|
||||
+15
-8
@@ -3,53 +3,60 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LogWriterMu protects concurrent access to gin.DefaultWriter/gin.DefaultErrorWriter
|
||||
// during log file rotation. Acquire RLock when reading/writing through the writers,
|
||||
// acquire Lock when swapping writers and closing old files.
|
||||
var LogWriterMu sync.RWMutex
|
||||
|
||||
func SysLog(s string) {
|
||||
t := time.Now()
|
||||
LogWriterMu.RLock()
|
||||
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
LogWriterMu.RUnlock()
|
||||
}
|
||||
|
||||
func SysError(s string) {
|
||||
t := time.Now()
|
||||
LogWriterMu.RLock()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
LogWriterMu.RUnlock()
|
||||
}
|
||||
|
||||
func FatalLog(v ...any) {
|
||||
t := time.Now()
|
||||
LogWriterMu.RLock()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||
LogWriterMu.RUnlock()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func LogStartupSuccess(startTime time.Time, port string) {
|
||||
|
||||
duration := time.Since(startTime)
|
||||
durationMs := duration.Milliseconds()
|
||||
|
||||
// Get network IPs
|
||||
networkIps := GetNetworkIps()
|
||||
|
||||
// Print blank line for spacing
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
LogWriterMu.RLock()
|
||||
defer LogWriterMu.RUnlock()
|
||||
|
||||
// Print the main success message
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m ready in %d ms\n", SystemName, Version, durationMs)
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
|
||||
// Skip fancy startup message in container environments
|
||||
if !IsRunningInContainer() {
|
||||
// Print local URL
|
||||
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port)
|
||||
}
|
||||
|
||||
// Print network URLs
|
||||
for _, ip := range networkIps {
|
||||
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
|
||||
}
|
||||
|
||||
// Print blank line for spacing
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package constant
|
||||
|
||||
// WaffoPayMethod defines the display and API parameter mapping for Waffo payment methods.
|
||||
type WaffoPayMethod struct {
|
||||
Name string `json:"name"` // Frontend display name
|
||||
Icon string `json:"icon"` // Frontend icon identifier: credit-card, apple, google
|
||||
PayMethodType string `json:"payMethodType"` // Waffo API PayMethodType, can be comma-separated
|
||||
PayMethodName string `json:"payMethodName"` // Waffo API PayMethodName, empty means auto-select by Waffo checkout
|
||||
}
|
||||
|
||||
// DefaultWaffoPayMethods is the default list of supported payment methods.
|
||||
var DefaultWaffoPayMethods = []WaffoPayMethod{
|
||||
{Name: "Card", Icon: "/pay-card.png", PayMethodType: "CREDITCARD,DEBITCARD", PayMethodName: ""},
|
||||
{Name: "Apple Pay", Icon: "/pay-apple.png", PayMethodType: "APPLEPAY", PayMethodName: "APPLEPAY"},
|
||||
{Name: "Google Pay", Icon: "/pay-google.png", PayMethodType: "GOOGLEPAY", PayMethodName: "GOOGLEPAY"},
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -169,6 +175,183 @@ func ForceGC(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// LogFileInfo 日志文件信息
|
||||
type LogFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"mod_time"`
|
||||
}
|
||||
|
||||
// LogFilesResponse 日志文件列表响应
|
||||
type LogFilesResponse struct {
|
||||
LogDir string `json:"log_dir"`
|
||||
Enabled bool `json:"enabled"`
|
||||
FileCount int `json:"file_count"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
OldestTime *time.Time `json:"oldest_time,omitempty"`
|
||||
NewestTime *time.Time `json:"newest_time,omitempty"`
|
||||
Files []LogFileInfo `json:"files"`
|
||||
}
|
||||
|
||||
// getLogFiles 读取日志目录中的日志文件列表
|
||||
func getLogFiles() ([]LogFileInfo, error) {
|
||||
if *common.LogDir == "" {
|
||||
return nil, nil
|
||||
}
|
||||
entries, err := os.ReadDir(*common.LogDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var files []LogFileInfo
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !strings.HasPrefix(name, "oneapi-") || !strings.HasSuffix(name, ".log") {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, LogFileInfo{
|
||||
Name: name,
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
// 按文件名降序排列(最新在前)
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Name > files[j].Name
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetLogFiles 获取日志文件列表
|
||||
func GetLogFiles(c *gin.Context) {
|
||||
if *common.LogDir == "" {
|
||||
common.ApiSuccess(c, LogFilesResponse{Enabled: false})
|
||||
return
|
||||
}
|
||||
files, err := getLogFiles()
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var totalSize int64
|
||||
var oldest, newest time.Time
|
||||
for i, f := range files {
|
||||
totalSize += f.Size
|
||||
if i == 0 || f.ModTime.Before(oldest) {
|
||||
oldest = f.ModTime
|
||||
}
|
||||
if i == 0 || f.ModTime.After(newest) {
|
||||
newest = f.ModTime
|
||||
}
|
||||
}
|
||||
resp := LogFilesResponse{
|
||||
LogDir: *common.LogDir,
|
||||
Enabled: true,
|
||||
FileCount: len(files),
|
||||
TotalSize: totalSize,
|
||||
Files: files,
|
||||
}
|
||||
if len(files) > 0 {
|
||||
resp.OldestTime = &oldest
|
||||
resp.NewestTime = &newest
|
||||
}
|
||||
common.ApiSuccess(c, resp)
|
||||
}
|
||||
|
||||
// CleanupLogFiles 清理过期日志文件
|
||||
func CleanupLogFiles(c *gin.Context) {
|
||||
mode := c.Query("mode")
|
||||
valueStr := c.Query("value")
|
||||
if mode != "by_count" && mode != "by_days" {
|
||||
common.ApiErrorMsg(c, "invalid mode, must be by_count or by_days")
|
||||
return
|
||||
}
|
||||
value, err := strconv.Atoi(valueStr)
|
||||
if err != nil || value < 1 {
|
||||
common.ApiErrorMsg(c, "invalid value, must be a positive integer")
|
||||
return
|
||||
}
|
||||
if *common.LogDir == "" {
|
||||
common.ApiErrorMsg(c, "log directory not configured")
|
||||
return
|
||||
}
|
||||
|
||||
files, err := getLogFiles()
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
activeLogPath := logger.GetCurrentLogPath()
|
||||
var toDelete []LogFileInfo
|
||||
|
||||
switch mode {
|
||||
case "by_count":
|
||||
// files 已按名称降序(最新在前),保留前 value 个
|
||||
for i, f := range files {
|
||||
if i < value {
|
||||
continue
|
||||
}
|
||||
fullPath := filepath.Join(*common.LogDir, f.Name)
|
||||
if fullPath == activeLogPath {
|
||||
continue
|
||||
}
|
||||
toDelete = append(toDelete, f)
|
||||
}
|
||||
case "by_days":
|
||||
cutoff := time.Now().AddDate(0, 0, -value)
|
||||
for _, f := range files {
|
||||
if f.ModTime.Before(cutoff) {
|
||||
fullPath := filepath.Join(*common.LogDir, f.Name)
|
||||
if fullPath == activeLogPath {
|
||||
continue
|
||||
}
|
||||
toDelete = append(toDelete, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deletedCount int
|
||||
var freedBytes int64
|
||||
var failedFiles []string
|
||||
for _, f := range toDelete {
|
||||
fullPath := filepath.Join(*common.LogDir, f.Name)
|
||||
if err := os.Remove(fullPath); err != nil {
|
||||
failedFiles = append(failedFiles, f.Name)
|
||||
continue
|
||||
}
|
||||
deletedCount++
|
||||
freedBytes += f.Size
|
||||
}
|
||||
|
||||
result := gin.H{
|
||||
"deleted_count": deletedCount,
|
||||
"freed_bytes": freedBytes,
|
||||
"failed_files": failedFiles,
|
||||
}
|
||||
|
||||
if len(failedFiles) > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("部分文件删除失败(%d/%d)", len(failedFiles), len(toDelete)),
|
||||
"data": result,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
|
||||
// getDiskCacheInfo 获取磁盘缓存目录信息
|
||||
func getDiskCacheInfo() DiskCacheInfo {
|
||||
// 使用统一的缓存目录
|
||||
|
||||
+68
-14
@@ -48,14 +48,52 @@ func GetTopUpInfo(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果启用了 Waffo 支付,添加到支付方法列表
|
||||
enableWaffo := setting.WaffoEnabled &&
|
||||
((!setting.WaffoSandbox &&
|
||||
setting.WaffoApiKey != "" &&
|
||||
setting.WaffoPrivateKey != "" &&
|
||||
setting.WaffoPublicCert != "") ||
|
||||
(setting.WaffoSandbox &&
|
||||
setting.WaffoSandboxApiKey != "" &&
|
||||
setting.WaffoSandboxPrivateKey != "" &&
|
||||
setting.WaffoSandboxPublicCert != ""))
|
||||
if enableWaffo {
|
||||
hasWaffo := false
|
||||
for _, method := range payMethods {
|
||||
if method["type"] == "waffo" {
|
||||
hasWaffo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasWaffo {
|
||||
waffoMethod := map[string]string{
|
||||
"name": "Waffo (Global Payment)",
|
||||
"type": "waffo",
|
||||
"color": "rgba(var(--semi-blue-5), 1)",
|
||||
"min_topup": strconv.Itoa(setting.WaffoMinTopUp),
|
||||
}
|
||||
payMethods = append(payMethods, waffoMethod)
|
||||
}
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
|
||||
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
||||
"enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
|
||||
"creem_products": setting.CreemProducts,
|
||||
"enable_waffo_topup": enableWaffo,
|
||||
"waffo_pay_methods": func() interface{} {
|
||||
if enableWaffo {
|
||||
return setting.GetWaffoPayMethods()
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
"creem_products": setting.CreemProducts,
|
||||
"pay_methods": payMethods,
|
||||
"min_topup": operation_setting.MinTopUp,
|
||||
"stripe_min_topup": setting.StripeMinTopUp,
|
||||
"waffo_min_topup": setting.WaffoMinTopUp,
|
||||
"amount_options": operation_setting.GetPaymentSetting().AmountOptions,
|
||||
"discount": operation_setting.GetPaymentSetting().AmountDiscount,
|
||||
}
|
||||
@@ -204,27 +242,42 @@ func RequestEpay(c *gin.Context) {
|
||||
var orderLocks sync.Map
|
||||
var createLock sync.Mutex
|
||||
|
||||
// refCountedMutex 带引用计数的互斥锁,确保最后一个使用者才从 map 中删除
|
||||
type refCountedMutex struct {
|
||||
mu sync.Mutex
|
||||
refCount int
|
||||
}
|
||||
|
||||
// LockOrder 尝试对给定订单号加锁
|
||||
func LockOrder(tradeNo string) {
|
||||
lock, ok := orderLocks.Load(tradeNo)
|
||||
if !ok {
|
||||
createLock.Lock()
|
||||
defer createLock.Unlock()
|
||||
lock, ok = orderLocks.Load(tradeNo)
|
||||
if !ok {
|
||||
lock = new(sync.Mutex)
|
||||
orderLocks.Store(tradeNo, lock)
|
||||
}
|
||||
createLock.Lock()
|
||||
var rcm *refCountedMutex
|
||||
if v, ok := orderLocks.Load(tradeNo); ok {
|
||||
rcm = v.(*refCountedMutex)
|
||||
} else {
|
||||
rcm = &refCountedMutex{}
|
||||
orderLocks.Store(tradeNo, rcm)
|
||||
}
|
||||
lock.(*sync.Mutex).Lock()
|
||||
rcm.refCount++
|
||||
createLock.Unlock()
|
||||
rcm.mu.Lock()
|
||||
}
|
||||
|
||||
// UnlockOrder 释放给定订单号的锁
|
||||
func UnlockOrder(tradeNo string) {
|
||||
lock, ok := orderLocks.Load(tradeNo)
|
||||
if ok {
|
||||
lock.(*sync.Mutex).Unlock()
|
||||
v, ok := orderLocks.Load(tradeNo)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rcm := v.(*refCountedMutex)
|
||||
rcm.mu.Unlock()
|
||||
|
||||
createLock.Lock()
|
||||
rcm.refCount--
|
||||
if rcm.refCount == 0 {
|
||||
orderLocks.Delete(tradeNo)
|
||||
}
|
||||
createLock.Unlock()
|
||||
}
|
||||
|
||||
func EpayNotify(c *gin.Context) {
|
||||
@@ -410,3 +463,4 @@ func AdminCompleteTopUp(c *gin.Context) {
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/thanhpk/randstr"
|
||||
waffo "github.com/waffo-com/waffo-go"
|
||||
"github.com/waffo-com/waffo-go/config"
|
||||
"github.com/waffo-com/waffo-go/core"
|
||||
"github.com/waffo-com/waffo-go/types/order"
|
||||
)
|
||||
|
||||
func getWaffoSDK() (*waffo.Waffo, error) {
|
||||
env := config.Sandbox
|
||||
apiKey := setting.WaffoSandboxApiKey
|
||||
privateKey := setting.WaffoSandboxPrivateKey
|
||||
publicKey := setting.WaffoSandboxPublicCert
|
||||
if !setting.WaffoSandbox {
|
||||
env = config.Production
|
||||
apiKey = setting.WaffoApiKey
|
||||
privateKey = setting.WaffoPrivateKey
|
||||
publicKey = setting.WaffoPublicCert
|
||||
}
|
||||
builder := config.NewConfigBuilder().
|
||||
APIKey(apiKey).
|
||||
PrivateKey(privateKey).
|
||||
WaffoPublicKey(publicKey).
|
||||
Environment(env)
|
||||
if setting.WaffoMerchantId != "" {
|
||||
builder = builder.MerchantID(setting.WaffoMerchantId)
|
||||
}
|
||||
cfg, err := builder.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return waffo.New(cfg), nil
|
||||
}
|
||||
|
||||
func getWaffoUserEmail(user *model.User) string {
|
||||
return fmt.Sprintf("%d@examples.com", user.Id)
|
||||
}
|
||||
|
||||
func getWaffoCurrency() string {
|
||||
if setting.WaffoCurrency != "" {
|
||||
return setting.WaffoCurrency
|
||||
}
|
||||
return "USD"
|
||||
}
|
||||
|
||||
// zeroDecimalCurrencies 零小数位币种,金额不能带小数点
|
||||
var zeroDecimalCurrencies = map[string]bool{
|
||||
"IDR": true, "JPY": true, "KRW": true, "VND": true,
|
||||
}
|
||||
|
||||
func formatWaffoAmount(amount float64, currency string) string {
|
||||
if zeroDecimalCurrencies[currency] {
|
||||
return fmt.Sprintf("%.0f", amount)
|
||||
}
|
||||
return fmt.Sprintf("%.2f", amount)
|
||||
}
|
||||
|
||||
// getWaffoPayMoney converts the user-facing amount to USD for Waffo payment.
|
||||
// Waffo only accepts USD, so this function handles the conversion from different
|
||||
// display types (USD/CNY/TOKENS) to the actual USD amount to charge.
|
||||
func getWaffoPayMoney(amount float64, group string) float64 {
|
||||
originalAmount := amount
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
amount = amount / common.QuotaPerUnit
|
||||
}
|
||||
topupGroupRatio := common.GetTopupGroupRatio(group)
|
||||
if topupGroupRatio == 0 {
|
||||
topupGroupRatio = 1
|
||||
}
|
||||
discount := 1.0
|
||||
if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
|
||||
if ds > 0 {
|
||||
discount = ds
|
||||
}
|
||||
}
|
||||
return amount * setting.WaffoUnitPrice * topupGroupRatio * discount
|
||||
}
|
||||
|
||||
type WaffoPayRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
PayMethodIndex *int `json:"pay_method_index"` // 服务端支付方式列表的索引,nil 表示由 Waffo 自动选择
|
||||
PayMethodType string `json:"pay_method_type"` // Deprecated: 兼容旧前端,优先使用 pay_method_index
|
||||
PayMethodName string `json:"pay_method_name"` // Deprecated: 兼容旧前端,优先使用 pay_method_index
|
||||
}
|
||||
|
||||
// RequestWaffoPay 创建 Waffo 支付订单
|
||||
func RequestWaffoPay(c *gin.Context) {
|
||||
if !setting.WaffoEnabled {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "Waffo 支付未启用"})
|
||||
return
|
||||
}
|
||||
|
||||
var req WaffoPayRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
||||
return
|
||||
}
|
||||
waffoMinTopup := int64(setting.WaffoMinTopUp)
|
||||
if req.Amount < waffoMinTopup {
|
||||
c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", waffoMinTopup)})
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 从服务端配置查找支付方式,客户端只传索引或旧字段
|
||||
var resolvedPayMethodType, resolvedPayMethodName string
|
||||
methods := setting.GetWaffoPayMethods()
|
||||
if req.PayMethodIndex != nil {
|
||||
// 新协议:按索引查找
|
||||
idx := *req.PayMethodIndex
|
||||
if idx < 0 || idx >= len(methods) {
|
||||
log.Printf("Waffo 无效的支付方式索引: %d, UserId=%d, 可用范围: [0, %d)", idx, id, len(methods))
|
||||
c.JSON(200, gin.H{"message": "error", "data": "不支持的支付方式"})
|
||||
return
|
||||
}
|
||||
resolvedPayMethodType = methods[idx].PayMethodType
|
||||
resolvedPayMethodName = methods[idx].PayMethodName
|
||||
} else if req.PayMethodType != "" {
|
||||
// 兼容旧前端:验证客户端传的值在服务端列表中
|
||||
valid := false
|
||||
for _, m := range methods {
|
||||
if m.PayMethodType == req.PayMethodType && m.PayMethodName == req.PayMethodName {
|
||||
valid = true
|
||||
resolvedPayMethodType = m.PayMethodType
|
||||
resolvedPayMethodName = m.PayMethodName
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
log.Printf("Waffo 无效的支付方式: PayMethodType=%s, PayMethodName=%s, UserId=%d", req.PayMethodType, req.PayMethodName, id)
|
||||
c.JSON(200, gin.H{"message": "error", "data": "不支持的支付方式"})
|
||||
return
|
||||
}
|
||||
}
|
||||
// resolvedPayMethodType/Name 为空时,Waffo 自动选择支付方式
|
||||
|
||||
group, _ := model.GetUserGroup(id, true)
|
||||
payMoney := getWaffoPayMoney(float64(req.Amount), group)
|
||||
if payMoney < 0.01 {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成唯一订单号,paymentRequestId 与 merchantOrderId 保持一致,简化追踪
|
||||
merchantOrderId := fmt.Sprintf("WAFFO-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(6))
|
||||
paymentRequestId := merchantOrderId
|
||||
|
||||
// Token 模式下归一化 Amount(存等价美元/CNY 数量,避免 RechargeWaffo 双重放大)
|
||||
amount := req.Amount
|
||||
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
||||
amount = int64(float64(req.Amount) / common.QuotaPerUnit)
|
||||
if amount < 1 {
|
||||
amount = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 创建本地订单
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: amount,
|
||||
Money: payMoney,
|
||||
TradeNo: merchantOrderId,
|
||||
PaymentMethod: "waffo",
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
if err := topUp.Insert(); err != nil {
|
||||
log.Printf("Waffo 创建本地订单失败: %v", err)
|
||||
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
||||
return
|
||||
}
|
||||
|
||||
sdk, err := getWaffoSDK()
|
||||
if err != nil {
|
||||
log.Printf("Waffo SDK 初始化失败: %v", err)
|
||||
topUp.Status = common.TopUpStatusFailed
|
||||
_ = topUp.Update()
|
||||
c.JSON(200, gin.H{"message": "error", "data": "支付配置错误"})
|
||||
return
|
||||
}
|
||||
|
||||
callbackAddr := service.GetCallbackAddress()
|
||||
notifyUrl := callbackAddr + "/api/waffo/webhook"
|
||||
if setting.WaffoNotifyUrl != "" {
|
||||
notifyUrl = setting.WaffoNotifyUrl
|
||||
}
|
||||
returnUrl := system_setting.ServerAddress + "/console/topup?show_history=true"
|
||||
if setting.WaffoReturnUrl != "" {
|
||||
returnUrl = setting.WaffoReturnUrl
|
||||
}
|
||||
|
||||
currency := getWaffoCurrency()
|
||||
createParams := &order.CreateOrderParams{
|
||||
PaymentRequestID: paymentRequestId,
|
||||
MerchantOrderID: merchantOrderId,
|
||||
OrderAmount: formatWaffoAmount(payMoney, currency),
|
||||
OrderCurrency: currency,
|
||||
OrderDescription: fmt.Sprintf("Recharge %d credits", req.Amount),
|
||||
OrderRequestedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
NotifyURL: notifyUrl,
|
||||
MerchantInfo: &order.MerchantInfo{
|
||||
MerchantID: setting.WaffoMerchantId,
|
||||
},
|
||||
UserInfo: &order.UserInfo{
|
||||
UserID: strconv.Itoa(user.Id),
|
||||
UserEmail: getWaffoUserEmail(user),
|
||||
UserTerminal: "WEB",
|
||||
},
|
||||
PaymentInfo: &order.PaymentInfo{
|
||||
ProductName: "ONE_TIME_PAYMENT",
|
||||
PayMethodType: resolvedPayMethodType,
|
||||
PayMethodName: resolvedPayMethodName,
|
||||
},
|
||||
SuccessRedirectURL: returnUrl,
|
||||
FailedRedirectURL: returnUrl,
|
||||
}
|
||||
resp, err := sdk.Order().Create(c.Request.Context(), createParams, nil)
|
||||
if err != nil {
|
||||
log.Printf("Waffo 创建订单失败: %v", err)
|
||||
topUp.Status = common.TopUpStatusFailed
|
||||
_ = topUp.Update()
|
||||
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
||||
return
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
log.Printf("Waffo 创建订单业务失败: [%s] %s, 完整响应: %+v", resp.Code, resp.Message, resp)
|
||||
topUp.Status = common.TopUpStatusFailed
|
||||
_ = topUp.Update()
|
||||
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
||||
return
|
||||
}
|
||||
|
||||
orderData := resp.GetData()
|
||||
log.Printf("Waffo 订单创建成功 - 用户: %d, 订单: %s, 金额: %.2f", id, merchantOrderId, payMoney)
|
||||
|
||||
paymentUrl := orderData.FetchRedirectURL()
|
||||
if paymentUrl == "" {
|
||||
paymentUrl = orderData.OrderAction
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"message": "success",
|
||||
"data": gin.H{
|
||||
"payment_url": paymentUrl,
|
||||
"order_id": merchantOrderId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// webhookPayloadWithSubInfo 扩展 PAYMENT_NOTIFICATION,包含 SDK 未定义的 subscriptionInfo 字段
|
||||
type webhookPayloadWithSubInfo struct {
|
||||
EventType string `json:"eventType"`
|
||||
Result struct {
|
||||
core.PaymentNotificationResult
|
||||
SubscriptionInfo *webhookSubscriptionInfo `json:"subscriptionInfo,omitempty"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type webhookSubscriptionInfo struct {
|
||||
Period string `json:"period,omitempty"`
|
||||
MerchantRequest string `json:"merchantRequest,omitempty"`
|
||||
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||
SubscriptionRequest string `json:"subscriptionRequest,omitempty"`
|
||||
}
|
||||
|
||||
// WaffoWebhook 处理 Waffo 回调通知(支付/退款/订阅)
|
||||
func WaffoWebhook(c *gin.Context) {
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
log.Printf("Waffo Webhook 读取 body 失败: %v", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sdk, err := getWaffoSDK()
|
||||
if err != nil {
|
||||
log.Printf("Waffo Webhook SDK 初始化失败: %v", err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
wh := sdk.Webhook()
|
||||
bodyStr := string(bodyBytes)
|
||||
signature := c.GetHeader("X-SIGNATURE")
|
||||
|
||||
// 验证请求签名
|
||||
if !wh.VerifySignature(bodyStr, signature) {
|
||||
log.Printf("Waffo webhook 签名验证失败")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var event core.WebhookEvent
|
||||
if err := common.Unmarshal(bodyBytes, &event); err != nil {
|
||||
log.Printf("Waffo Webhook 解析失败: %v", err)
|
||||
sendWaffoWebhookResponse(c, wh, false, "invalid payload")
|
||||
return
|
||||
}
|
||||
|
||||
switch event.EventType {
|
||||
case core.EventPayment:
|
||||
// 解析为扩展类型,区分普通支付和订阅支付
|
||||
var payload webhookPayloadWithSubInfo
|
||||
if err := common.Unmarshal(bodyBytes, &payload); err != nil {
|
||||
sendWaffoWebhookResponse(c, wh, false, "invalid payment payload")
|
||||
return
|
||||
}
|
||||
log.Printf("Waffo Webhook - EventType: %s, MerchantOrderId: %s, OrderStatus: %s",
|
||||
event.EventType, payload.Result.MerchantOrderID, payload.Result.OrderStatus)
|
||||
handleWaffoPayment(c, wh, &payload.Result.PaymentNotificationResult)
|
||||
default:
|
||||
log.Printf("Waffo Webhook 未知事件: %s", event.EventType)
|
||||
sendWaffoWebhookResponse(c, wh, true, "")
|
||||
}
|
||||
}
|
||||
|
||||
// handleWaffoPayment 处理支付完成通知
|
||||
func handleWaffoPayment(c *gin.Context, wh *core.WebhookHandler, result *core.PaymentNotificationResult) {
|
||||
if result.OrderStatus != "PAY_SUCCESS" {
|
||||
log.Printf("Waffo 订单状态非成功: %s, 订单: %s", result.OrderStatus, result.MerchantOrderID)
|
||||
// 终态失败订单标记为 failed,避免永远停在 pending
|
||||
if result.MerchantOrderID != "" {
|
||||
if topUp := model.GetTopUpByTradeNo(result.MerchantOrderID); topUp != nil &&
|
||||
topUp.Status == common.TopUpStatusPending {
|
||||
topUp.Status = common.TopUpStatusFailed
|
||||
_ = topUp.Update()
|
||||
}
|
||||
}
|
||||
sendWaffoWebhookResponse(c, wh, true, "")
|
||||
return
|
||||
}
|
||||
|
||||
merchantOrderId := result.MerchantOrderID
|
||||
|
||||
LockOrder(merchantOrderId)
|
||||
defer UnlockOrder(merchantOrderId)
|
||||
|
||||
if err := model.RechargeWaffo(merchantOrderId); err != nil {
|
||||
log.Printf("Waffo 充值处理失败: %v, 订单: %s", err, merchantOrderId)
|
||||
sendWaffoWebhookResponse(c, wh, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Waffo 充值成功 - 订单: %s", merchantOrderId)
|
||||
sendWaffoWebhookResponse(c, wh, true, "")
|
||||
}
|
||||
|
||||
// sendWaffoWebhookResponse 发送签名响应
|
||||
func sendWaffoWebhookResponse(c *gin.Context, wh *core.WebhookHandler, success bool, msg string) {
|
||||
var body, sig string
|
||||
if success {
|
||||
body, sig = wh.BuildSuccessResponse()
|
||||
} else {
|
||||
body, sig = wh.BuildFailedResponse(msg)
|
||||
}
|
||||
c.Header("X-SIGNATURE", sig)
|
||||
c.Data(http.StatusOK, "application/json", []byte(body))
|
||||
}
|
||||
+150
-2
@@ -1,3 +1,151 @@
|
||||
密钥为环境变量SESSION_SECRET
|
||||
# 宝塔面板部署教程
|
||||
|
||||
本文档提供使用宝塔面板 Docker 功能部署 New API 的图文教程。
|
||||
|
||||
> 📖 官方文档:[宝塔面板部署](https://docs.newapi.pro/zh/docs/installation/deployment-methods/bt-docker-installation)
|
||||
|
||||
***
|
||||
|
||||
## 前置要求
|
||||
|
||||
| 项目 | 要求 |
|
||||
| ----- | ---------------------------------- |
|
||||
| 宝塔面板 | ≥ 9.2.0 版本 |
|
||||
| 推荐系统 | CentOS 7+、Ubuntu 18.04+、Debian 10+ |
|
||||
| 服务器配置 | 至少 1 核 2G 内存 |
|
||||
|
||||
***
|
||||
|
||||
## 步骤一:安装宝塔面板
|
||||
|
||||
1. 前往 [宝塔面板官网](https://www.bt.cn/new/download.html) 下载适合您系统的安装脚本
|
||||
2. 运行安装脚本安装宝塔面板
|
||||
3. 安装完成后,使用提供的地址、用户名和密码登录宝塔面板
|
||||
|
||||
***
|
||||
|
||||
## 步骤二:安装 Docker
|
||||
|
||||
1. 登录宝塔面板后,在左侧菜单栏找到并点击 **Docker**
|
||||
2. 首次进入会提示安装 Docker 服务,点击 **立即安装**
|
||||
3. 按照提示完成 Docker 服务的安装
|
||||
|
||||
***
|
||||
|
||||
## 步骤三:安装 New API
|
||||
|
||||
### 方法一:使用宝塔应用商店(推荐)
|
||||
|
||||
1. 在宝塔面板 Docker 功能中,点击 **应用商店**
|
||||
2. 搜索并找到 **New-API**
|
||||
3. 点击 **安装**
|
||||
4. 配置以下基本选项:
|
||||
- **容器名称**:可自定义,默认为 `new-api`
|
||||
- **端口映射**:默认为 `3000:3000`
|
||||
- **环境变量**:
|
||||
- `SESSION_SECRET`:会话密钥(**必填**,多机部署时必须一致)
|
||||
- `CRYPTO_SECRET`:加密密钥(使用 Redis 时必填)
|
||||
5. 点击 **确认** 开始安装
|
||||
6. 等待安装完成后,访问 `http://您的服务器IP:3000` 即可使用
|
||||
|
||||
### 方法二:使用 Docker Compose
|
||||
|
||||
1. 在宝塔面板中创建网站目录,如 `/www/wwwroot/new-api`
|
||||
2. 创建 `docker-compose.yml` 文件:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
new-api:
|
||||
image: calciumion/new-api:latest
|
||||
container_name: new-api
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
- SESSION_SECRET=your_session_secret_here # 请修改为随机字符串
|
||||
- TZ=Asia/Shanghai
|
||||
```
|
||||
|
||||
1. 在终端中进入目录并启动:
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/new-api
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 必要环境变量
|
||||
|
||||
| 变量名 | 说明 | 是否必填 |
|
||||
| ------------------- | ------------------ | ------ |
|
||||
| `SESSION_SECRET` | 会话密钥,多机部署必须一致 | **必填** |
|
||||
| `CRYPTO_SECRET` | 加密密钥,使用 Redis 时必填 | 条件必填 |
|
||||
| `SQL_DSN` | 数据库连接字符串(使用外部数据库时) | 可选 |
|
||||
| `REDIS_CONN_STRING` | Redis 连接字符串 | 可选 |
|
||||
|
||||
### 生成随机密钥
|
||||
|
||||
```bash
|
||||
# 生成 SESSION_SECRET
|
||||
openssl rand -hex 16
|
||||
|
||||
# 或使用 Linux 命令
|
||||
head -c 16 /dev/urandom | xxd -p
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1:无法访问 3000 端口?
|
||||
|
||||
1. 检查服务器防火墙是否开放 3000 端口
|
||||
2. 在宝塔面板 **安全** 中放行 3000 端口
|
||||
3. 检查云服务器安全组是否开放端口
|
||||
|
||||
### Q2:登录后提示会话失效?
|
||||
|
||||
确保设置了 `SESSION_SECRET` 环境变量,且值不为空。
|
||||
|
||||
### Q3:数据如何持久化?
|
||||
|
||||
使用 Docker 卷映射数据目录:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data:/data
|
||||
```
|
||||
|
||||
### Q4:如何更新版本?
|
||||
|
||||
```bash
|
||||
# 拉取最新镜像
|
||||
docker pull calciumion/new-api:latest
|
||||
|
||||
# 重启容器
|
||||
docker-compose down && docker-compose up -d
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [官方文档](https://docs.newapi.pro/zh/docs/installation)
|
||||
- [环境变量配置](https://docs.newapi.pro/zh/docs/installation/config-maintenance/environment-variables)
|
||||
- [常见问题](https://docs.newapi.pro/zh/docs/support/faq)
|
||||
- [GitHub 仓库](https://github.com/QuantumNous/new-api)
|
||||
|
||||
***
|
||||
|
||||
## 截图示例
|
||||
|
||||

|
||||
|
||||
> ⚠️ 注意:密钥为环境变量 `SESSION_SECRET`,请务必设置!
|
||||
|
||||

|
||||
|
||||
@@ -46,6 +46,7 @@ require (
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/tiktoken-go/tokenizer v0.6.2
|
||||
github.com/waffo-com/waffo-go v1.3.1
|
||||
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/image v0.23.0
|
||||
@@ -120,7 +121,6 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/samber/go-singleflightx v0.3.2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
||||
github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g=
|
||||
@@ -10,34 +12,18 @@ github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+Kc
|
||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
|
||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
|
||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0 h1:JzidOz4Hcn2RbP5fvIS1iAP+DcRv5VJtgixbEYDsI5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0/go.mod h1:9A4/PJYlWjvjEzzoOLGQjkLt4bYK9fRWi7uz1GSsAcA=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0 h1:TDKR8ACRw7G+GFaQlhoy6biu+8q6ZtSddQCy9avMdMI=
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0/go.mod h1:XlhOh5Ax/lesqN4aZCUgj9vVJed5VoXYHHFYGAlJEwU=
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -58,7 +44,6 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -132,12 +117,13 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -186,8 +172,6 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
@@ -245,7 +229,6 @@ github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -262,8 +245,9 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/samber/go-singleflightx v0.3.2 h1:jXbUU0fvis8Fdv4HGONboX5WdEZcYLoBEcKiE+ITCyQ=
|
||||
github.com/samber/go-singleflightx v0.3.2/go.mod h1:X2BR+oheHIYc73PvxRMlcASg6KYYTQyUYpdVU7t/ux4=
|
||||
github.com/samber/hot v0.11.0 h1:JhV9hk8SmZIqB0To8OyCzPubvszkuoSXWx/7FCEGO+Q=
|
||||
@@ -320,6 +304,8 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/waffo-com/waffo-go v1.3.1 h1:NCYD3oQ59DTJj1bwS5T/659LI4h8PuAIW4Qj/w7fKPw=
|
||||
github.com/waffo-com/waffo-go v1.3.1/go.mod h1:IaXVYq6mmYtrLFFsLxPslNwuIZx0mIadWWjhe+eWb0g=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -330,6 +316,8 @@ github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFi
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -339,14 +327,12 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/y
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -367,19 +353,14 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
+26
-4
@@ -29,6 +29,15 @@ const maxLogCount = 1000000
|
||||
var logCount int
|
||||
var setupLogLock sync.Mutex
|
||||
var setupLogWorking bool
|
||||
var currentLogPath string
|
||||
var currentLogPathMu sync.RWMutex
|
||||
var currentLogFile *os.File
|
||||
|
||||
func GetCurrentLogPath() string {
|
||||
currentLogPathMu.RLock()
|
||||
defer currentLogPathMu.RUnlock()
|
||||
return currentLogPath
|
||||
}
|
||||
|
||||
func SetupLogger() {
|
||||
defer func() {
|
||||
@@ -48,8 +57,19 @@ func SetupLogger() {
|
||||
if err != nil {
|
||||
log.Fatal("failed to open log file")
|
||||
}
|
||||
currentLogPathMu.Lock()
|
||||
oldFile := currentLogFile
|
||||
currentLogPath = logPath
|
||||
currentLogFile = fd
|
||||
currentLogPathMu.Unlock()
|
||||
|
||||
common.LogWriterMu.Lock()
|
||||
gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
|
||||
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
|
||||
if oldFile != nil {
|
||||
_ = oldFile.Close()
|
||||
}
|
||||
common.LogWriterMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +95,18 @@ func LogDebug(ctx context.Context, msg string, args ...any) {
|
||||
}
|
||||
|
||||
func logHelper(ctx context.Context, level string, msg string) {
|
||||
writer := gin.DefaultErrorWriter
|
||||
if level == loggerINFO {
|
||||
writer = gin.DefaultWriter
|
||||
}
|
||||
id := ctx.Value(common.RequestIdKey)
|
||||
if id == nil {
|
||||
id = "SYSTEM"
|
||||
}
|
||||
now := time.Now()
|
||||
common.LogWriterMu.RLock()
|
||||
writer := gin.DefaultErrorWriter
|
||||
if level == loggerINFO {
|
||||
writer = gin.DefaultWriter
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
|
||||
common.LogWriterMu.RUnlock()
|
||||
logCount++ // we don't need accurate count, so no lock here
|
||||
if logCount > maxLogCount && !setupLogWorking {
|
||||
logCount = 0
|
||||
|
||||
@@ -196,7 +196,10 @@ func userRedisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, key
|
||||
}
|
||||
|
||||
// SearchRateLimit returns a per-user rate limiter for search endpoints.
|
||||
// 10 requests per 60 seconds per user (by user ID, not IP).
|
||||
// Configurable via SEARCH_RATE_LIMIT_ENABLE / SEARCH_RATE_LIMIT / SEARCH_RATE_LIMIT_DURATION.
|
||||
func SearchRateLimit() func(c *gin.Context) {
|
||||
if !common.SearchRateLimitEnable {
|
||||
return defNext
|
||||
}
|
||||
return userRateLimitFactory(common.SearchRateLimitNum, common.SearchRateLimitDuration, "SR")
|
||||
}
|
||||
|
||||
@@ -89,6 +89,22 @@ func InitOptionMap() {
|
||||
common.OptionMap["CreemProducts"] = setting.CreemProducts
|
||||
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
|
||||
common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret
|
||||
common.OptionMap["WaffoEnabled"] = strconv.FormatBool(setting.WaffoEnabled)
|
||||
common.OptionMap["WaffoApiKey"] = setting.WaffoApiKey
|
||||
common.OptionMap["WaffoPrivateKey"] = setting.WaffoPrivateKey
|
||||
common.OptionMap["WaffoPublicCert"] = setting.WaffoPublicCert
|
||||
common.OptionMap["WaffoSandboxPublicCert"] = setting.WaffoSandboxPublicCert
|
||||
common.OptionMap["WaffoSandboxApiKey"] = setting.WaffoSandboxApiKey
|
||||
common.OptionMap["WaffoSandboxPrivateKey"] = setting.WaffoSandboxPrivateKey
|
||||
common.OptionMap["WaffoSandbox"] = strconv.FormatBool(setting.WaffoSandbox)
|
||||
common.OptionMap["WaffoMerchantId"] = setting.WaffoMerchantId
|
||||
common.OptionMap["WaffoNotifyUrl"] = setting.WaffoNotifyUrl
|
||||
common.OptionMap["WaffoReturnUrl"] = setting.WaffoReturnUrl
|
||||
common.OptionMap["WaffoSubscriptionReturnUrl"] = setting.WaffoSubscriptionReturnUrl
|
||||
common.OptionMap["WaffoCurrency"] = setting.WaffoCurrency
|
||||
common.OptionMap["WaffoUnitPrice"] = strconv.FormatFloat(setting.WaffoUnitPrice, 'f', -1, 64)
|
||||
common.OptionMap["WaffoMinTopUp"] = strconv.Itoa(setting.WaffoMinTopUp)
|
||||
common.OptionMap["WaffoPayMethods"] = setting.WaffoPayMethods2JsonString()
|
||||
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
||||
@@ -358,6 +374,36 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
setting.CreemTestMode = value == "true"
|
||||
case "CreemWebhookSecret":
|
||||
setting.CreemWebhookSecret = value
|
||||
case "WaffoEnabled":
|
||||
setting.WaffoEnabled = value == "true"
|
||||
case "WaffoApiKey":
|
||||
setting.WaffoApiKey = value
|
||||
case "WaffoPrivateKey":
|
||||
setting.WaffoPrivateKey = value
|
||||
case "WaffoPublicCert":
|
||||
setting.WaffoPublicCert = value
|
||||
case "WaffoSandboxPublicCert":
|
||||
setting.WaffoSandboxPublicCert = value
|
||||
case "WaffoSandboxApiKey":
|
||||
setting.WaffoSandboxApiKey = value
|
||||
case "WaffoSandboxPrivateKey":
|
||||
setting.WaffoSandboxPrivateKey = value
|
||||
case "WaffoSandbox":
|
||||
setting.WaffoSandbox = value == "true"
|
||||
case "WaffoMerchantId":
|
||||
setting.WaffoMerchantId = value
|
||||
case "WaffoNotifyUrl":
|
||||
setting.WaffoNotifyUrl = value
|
||||
case "WaffoReturnUrl":
|
||||
setting.WaffoReturnUrl = value
|
||||
case "WaffoSubscriptionReturnUrl":
|
||||
setting.WaffoSubscriptionReturnUrl = value
|
||||
case "WaffoCurrency":
|
||||
setting.WaffoCurrency = value
|
||||
case "WaffoUnitPrice":
|
||||
setting.WaffoUnitPrice, _ = strconv.ParseFloat(value, 64)
|
||||
case "WaffoMinTopUp":
|
||||
setting.WaffoMinTopUp, _ = strconv.Atoi(value)
|
||||
case "TopupGroupRatio":
|
||||
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||
case "GitHubClientId":
|
||||
@@ -458,6 +504,10 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||
case "PayMethods":
|
||||
err = operation_setting.UpdatePayMethodsByJsonString(value)
|
||||
case "WaffoPayMethods":
|
||||
// WaffoPayMethods is read directly from OptionMap via setting.GetWaffoPayMethods().
|
||||
// The value is already stored in OptionMap at the top of this function (line: common.OptionMap[key] = value).
|
||||
// No additional in-memory variable to update.
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+68
-9
@@ -12,15 +12,15 @@ import (
|
||||
)
|
||||
|
||||
type TopUp struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
Amount int64 `json:"amount"`
|
||||
Money float64 `json:"money"`
|
||||
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
|
||||
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
CompleteTime int64 `json:"complete_time"`
|
||||
Status string `json:"status"`
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
Amount int64 `json:"amount"`
|
||||
Money float64 `json:"money"`
|
||||
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
|
||||
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
CompleteTime int64 `json:"complete_time"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (topUp *TopUp) Insert() error {
|
||||
@@ -376,3 +376,62 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RechargeWaffo(tradeNo string) (err error) {
|
||||
if tradeNo == "" {
|
||||
return errors.New("未提供支付单号")
|
||||
}
|
||||
|
||||
var quotaToAdd int
|
||||
topUp := &TopUp{}
|
||||
|
||||
refCol := "`trade_no`"
|
||||
if common.UsingPostgreSQL {
|
||||
refCol = `"trade_no"`
|
||||
}
|
||||
|
||||
err = DB.Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error
|
||||
if err != nil {
|
||||
return errors.New("充值订单不存在")
|
||||
}
|
||||
|
||||
if topUp.Status == common.TopUpStatusSuccess {
|
||||
return nil // 幂等:已成功直接返回
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
return errors.New("充值订单状态错误")
|
||||
}
|
||||
|
||||
dAmount := decimal.NewFromInt(topUp.Amount)
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart())
|
||||
if quotaToAdd <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
topUp.CompleteTime = common.GetTimestamp()
|
||||
topUp.Status = common.TopUpStatusSuccess
|
||||
if err := tx.Save(topUp).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
common.SysError("waffo topup failed: " + err.Error())
|
||||
return errors.New("充值失败,请稍后重试")
|
||||
}
|
||||
|
||||
if quotaToAdd > 0 {
|
||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+9
-4
@@ -208,10 +208,7 @@ func (p *GenericOAuthProvider) GetUserInfo(ctx context.Context, token *OAuthToke
|
||||
}
|
||||
|
||||
// Set authorization header
|
||||
tokenType := token.TokenType
|
||||
if tokenType == "" {
|
||||
tokenType = "Bearer"
|
||||
}
|
||||
tokenType := normalizeAuthorizationTokenType(token.TokenType)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", tokenType, token.AccessToken))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
@@ -320,6 +317,14 @@ func (p *GenericOAuthProvider) GetProviderId() int {
|
||||
return p.config.Id
|
||||
}
|
||||
|
||||
func normalizeAuthorizationTokenType(tokenType string) string {
|
||||
tokenType = strings.TrimSpace(tokenType)
|
||||
if tokenType == "" || strings.EqualFold(tokenType, "Bearer") {
|
||||
return "Bearer"
|
||||
}
|
||||
return tokenType
|
||||
}
|
||||
|
||||
// IsGenericProvider returns true for generic providers
|
||||
func (p *GenericOAuthProvider) IsGenericProvider() bool {
|
||||
return true
|
||||
|
||||
@@ -627,6 +627,12 @@ func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, res
|
||||
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
|
||||
}
|
||||
}
|
||||
case constant.ChannelTypeOpenAI:
|
||||
if usage.PromptTokensDetails.CachedTokens == 0 {
|
||||
if cachedTokens, ok := extractLlamaCachedTokensFromBody(responseBody); ok {
|
||||
usage.PromptTokensDetails.CachedTokens = cachedTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,3 +695,25 @@ func extractMoonshotCachedTokensFromBody(body []byte) (int, bool) {
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// extractLlamaCachedTokensFromBody 从llama.cpp的非标准位置提取cache_n
|
||||
func extractLlamaCachedTokensFromBody(body []byte) (int, bool) {
|
||||
if len(body) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Timings struct {
|
||||
CachedTokens *int `json:"cache_n"`
|
||||
} `json:"timings"`
|
||||
}
|
||||
|
||||
if err := common.Unmarshal(body, &payload); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if payload.Timings.CachedTokens == nil {
|
||||
return 0, false
|
||||
}
|
||||
return *payload.Timings.CachedTokens, true
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
|
||||
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
||||
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
|
||||
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
|
||||
|
||||
// Universal secure verification routes
|
||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
||||
@@ -89,6 +90,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
|
||||
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
|
||||
selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
|
||||
selfRoute.POST("/waffo/pay", middleware.CriticalRateLimit(), controller.RequestWaffoPay)
|
||||
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
|
||||
selfRoute.PUT("/setting", controller.UpdateUserSetting)
|
||||
|
||||
@@ -192,6 +194,8 @@ func SetApiRouter(router *gin.Engine) {
|
||||
performanceRoute.DELETE("/disk_cache", controller.ClearDiskCache)
|
||||
performanceRoute.POST("/reset_stats", controller.ResetPerformanceStats)
|
||||
performanceRoute.POST("/gc", controller.ForceGC)
|
||||
performanceRoute.GET("/logs", controller.GetLogFiles)
|
||||
performanceRoute.DELETE("/logs", controller.CleanupLogFiles)
|
||||
}
|
||||
ratioSyncRoute := apiRouter.Group("/ratio_sync")
|
||||
ratioSyncRoute.Use(middleware.RootAuth())
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
WaffoEnabled bool
|
||||
WaffoApiKey string
|
||||
WaffoPrivateKey string
|
||||
WaffoPublicCert string
|
||||
WaffoSandboxPublicCert string
|
||||
WaffoSandboxApiKey string
|
||||
WaffoSandboxPrivateKey string
|
||||
WaffoSandbox bool
|
||||
WaffoMerchantId string
|
||||
WaffoNotifyUrl string
|
||||
WaffoReturnUrl string
|
||||
WaffoSubscriptionReturnUrl string
|
||||
WaffoCurrency string
|
||||
WaffoUnitPrice float64 = 1.0
|
||||
WaffoMinTopUp int = 1
|
||||
)
|
||||
|
||||
// GetWaffoPayMethods 从 options 读取 Waffo 支付方式配置
|
||||
func GetWaffoPayMethods() []constant.WaffoPayMethod {
|
||||
common.OptionMapRWMutex.RLock()
|
||||
jsonStr := common.OptionMap["WaffoPayMethods"]
|
||||
common.OptionMapRWMutex.RUnlock()
|
||||
|
||||
if jsonStr == "" {
|
||||
return copyDefaultWaffoPayMethods()
|
||||
}
|
||||
var methods []constant.WaffoPayMethod
|
||||
if err := common.UnmarshalJsonStr(jsonStr, &methods); err != nil {
|
||||
return copyDefaultWaffoPayMethods()
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
// SetWaffoPayMethods 序列化 Waffo 支付方式配置并更新 OptionMap
|
||||
func SetWaffoPayMethods(methods []constant.WaffoPayMethod) error {
|
||||
jsonBytes, err := common.Marshal(methods)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
common.OptionMapRWMutex.Lock()
|
||||
common.OptionMap["WaffoPayMethods"] = string(jsonBytes)
|
||||
common.OptionMapRWMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDefaultWaffoPayMethods() []constant.WaffoPayMethod {
|
||||
cp := make([]constant.WaffoPayMethod, len(constant.DefaultWaffoPayMethods))
|
||||
copy(cp, constant.DefaultWaffoPayMethods)
|
||||
return cp
|
||||
}
|
||||
|
||||
// WaffoPayMethods2JsonString 将默认 WaffoPayMethods 序列化为 JSON 字符串(供 InitOptionMap 使用)
|
||||
func WaffoPayMethods2JsonString() string {
|
||||
jsonBytes, err := common.Marshal(constant.DefaultWaffoPayMethods)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
@@ -510,6 +510,9 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
||||
// gpt-5 匹配
|
||||
if strings.HasPrefix(name, "gpt-5") {
|
||||
if strings.HasPrefix(name, "gpt-5.4") {
|
||||
if strings.HasPrefix(name, "gpt-5.4-nano") {
|
||||
return 6.25, true
|
||||
}
|
||||
return 6, true
|
||||
}
|
||||
return 8, true
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -23,6 +23,7 @@ import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralP
|
||||
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
|
||||
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
|
||||
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
|
||||
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
|
||||
import { API, showError, toBoolean } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -66,7 +67,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析TopupGroupRatio出错:', error);
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -78,7 +78,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析AmountOptions出错:', error);
|
||||
newInputs['AmountOptions'] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -90,7 +89,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析AmountDiscount出错:', error);
|
||||
newInputs['AmountDiscount'] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -146,6 +144,9 @@ const PaymentSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayWaffo options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,68 @@ const pickStrokeColor = (percent) => {
|
||||
return '#3b82f6';
|
||||
};
|
||||
|
||||
const normalizePlanType = (value) => {
|
||||
if (value == null) return '';
|
||||
return String(value).trim().toLowerCase();
|
||||
};
|
||||
|
||||
const getWindowDurationSeconds = (windowData) => {
|
||||
const value = Number(windowData?.limit_window_seconds);
|
||||
if (!Number.isFinite(value) || value <= 0) return null;
|
||||
return value;
|
||||
};
|
||||
|
||||
const classifyWindowByDuration = (windowData) => {
|
||||
const seconds = getWindowDurationSeconds(windowData);
|
||||
if (seconds == null) return null;
|
||||
return seconds >= 24 * 60 * 60 ? 'weekly' : 'fiveHour';
|
||||
};
|
||||
|
||||
const resolveRateLimitWindows = (data) => {
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
const primary = rateLimit?.primary_window ?? null;
|
||||
const secondary = rateLimit?.secondary_window ?? null;
|
||||
const windows = [primary, secondary].filter(Boolean);
|
||||
const planType = normalizePlanType(data?.plan_type ?? rateLimit?.plan_type);
|
||||
|
||||
let fiveHourWindow = null;
|
||||
let weeklyWindow = null;
|
||||
|
||||
for (const windowData of windows) {
|
||||
const bucket = classifyWindowByDuration(windowData);
|
||||
if (bucket === 'fiveHour' && !fiveHourWindow) {
|
||||
fiveHourWindow = windowData;
|
||||
continue;
|
||||
}
|
||||
if (bucket === 'weekly' && !weeklyWindow) {
|
||||
weeklyWindow = windowData;
|
||||
}
|
||||
}
|
||||
|
||||
if (planType === 'free') {
|
||||
if (!weeklyWindow) {
|
||||
weeklyWindow = primary ?? secondary ?? null;
|
||||
}
|
||||
return { fiveHourWindow: null, weeklyWindow };
|
||||
}
|
||||
|
||||
if (!fiveHourWindow && !weeklyWindow) {
|
||||
return {
|
||||
fiveHourWindow: primary ?? null,
|
||||
weeklyWindow: secondary ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!fiveHourWindow) {
|
||||
fiveHourWindow = windows.find((windowData) => windowData !== weeklyWindow) ?? null;
|
||||
}
|
||||
if (!weeklyWindow) {
|
||||
weeklyWindow = windows.find((windowData) => windowData !== fiveHourWindow) ?? null;
|
||||
}
|
||||
|
||||
return { fiveHourWindow, weeklyWindow };
|
||||
};
|
||||
|
||||
const formatDurationSeconds = (seconds, t) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const s = Number(seconds);
|
||||
@@ -68,6 +130,10 @@ const formatUnixSeconds = (unixSeconds) => {
|
||||
|
||||
const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const hasWindowData =
|
||||
!!windowData &&
|
||||
typeof windowData === 'object' &&
|
||||
Object.keys(windowData).length > 0;
|
||||
const percent = clampPercent(windowData?.used_percent ?? 0);
|
||||
const resetAt = windowData?.reset_at;
|
||||
const resetAfterSeconds = windowData?.reset_after_seconds;
|
||||
@@ -83,26 +149,30 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='mt-2'>
|
||||
<Progress
|
||||
percent={percent}
|
||||
stroke={pickStrokeColor(percent)}
|
||||
showInfo={true}
|
||||
/>
|
||||
</div>
|
||||
{hasWindowData ? (
|
||||
<div className='mt-2'>
|
||||
<Progress
|
||||
percent={percent}
|
||||
stroke={pickStrokeColor(percent)}
|
||||
showInfo={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-3 text-sm text-semi-color-text-2'>-</div>
|
||||
)}
|
||||
|
||||
<div className='mt-1 flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
|
||||
<div>
|
||||
{tt('已使用:')}
|
||||
{percent}%
|
||||
{hasWindowData ? `${percent}%` : '-'}
|
||||
</div>
|
||||
<div>
|
||||
{tt('距离重置:')}
|
||||
{formatDurationSeconds(resetAfterSeconds, tt)}
|
||||
{hasWindowData ? formatDurationSeconds(resetAfterSeconds, tt) : '-'}
|
||||
</div>
|
||||
<div>
|
||||
{tt('窗口:')}
|
||||
{formatDurationSeconds(limitWindowSeconds, tt)}
|
||||
{hasWindowData ? formatDurationSeconds(limitWindowSeconds, tt) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,9 +183,7 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const data = payload?.data ?? null;
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
|
||||
const primary = rateLimit?.primary_window ?? null;
|
||||
const secondary = rateLimit?.secondary_window ?? null;
|
||||
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data);
|
||||
|
||||
const allowed = !!rateLimit?.allowed;
|
||||
const limitReached = !!rateLimit?.limit_reached;
|
||||
@@ -163,12 +231,12 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
windowData={fiveHourWindow}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
windowData={weeklyWindow}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -87,6 +87,9 @@ const RechargeCard = ({
|
||||
statusLoading,
|
||||
topupInfo,
|
||||
onOpenHistory,
|
||||
enableWaffoTopUp,
|
||||
waffoTopUp,
|
||||
waffoPayMethods,
|
||||
subscriptionLoading = false,
|
||||
subscriptionPlans = [],
|
||||
billingPreference,
|
||||
@@ -224,19 +227,19 @@ const RechargeCard = ({
|
||||
<div className='py-8 flex justify-center'>
|
||||
<Spin size='large' />
|
||||
</div>
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? (
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
|
||||
<Form
|
||||
getFormApi={(api) => (onlineFormApiRef.current = api)}
|
||||
initValues={{ topUpCount: topUpCount }}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
|
||||
<Row gutter={12}>
|
||||
<Col xs={24} sm={24} md={24} lg={10} xl={10}>
|
||||
<Form.InputNumber
|
||||
field='topUpCount'
|
||||
label={t('充值数量')}
|
||||
disabled={!enableOnlineTopUp && !enableStripeTopUp}
|
||||
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWaffoTopUp}
|
||||
placeholder={
|
||||
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
|
||||
}
|
||||
@@ -288,11 +291,11 @@ const RechargeCard = ({
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
{payMethods && payMethods.filter(m => m.type !== 'waffo').length > 0 && (
|
||||
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
|
||||
<Form.Slot label={t('选择支付方式')}>
|
||||
{payMethods && payMethods.length > 0 ? (
|
||||
<Space wrap>
|
||||
{payMethods.map((payMethod) => {
|
||||
{payMethods.filter(m => m.type !== 'waffo').map((payMethod) => {
|
||||
const minTopupVal = Number(payMethod.min_topup) || 0;
|
||||
const isStripe = payMethod.type === 'stripe';
|
||||
const disabled =
|
||||
@@ -352,17 +355,13 @@ const RechargeCard = ({
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
|
||||
<Form.Slot
|
||||
label={
|
||||
<div className='flex items-center gap-2'>
|
||||
@@ -483,6 +482,46 @@ const RechargeCard = ({
|
||||
</Form.Slot>
|
||||
)}
|
||||
|
||||
{/* Waffo 充值区域 */}
|
||||
{enableWaffoTopUp &&
|
||||
waffoPayMethods &&
|
||||
waffoPayMethods.length > 0 && (
|
||||
<Form.Slot label={t('Waffo 充值')}>
|
||||
<Space wrap>
|
||||
{waffoPayMethods.map((method, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
onClick={() => waffoTopUp(index)}
|
||||
loading={paymentLoading}
|
||||
icon={
|
||||
method.icon ? (
|
||||
<img
|
||||
src={method.icon}
|
||||
alt={method.name}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CreditCard
|
||||
size={18}
|
||||
color='var(--semi-color-text-2)'
|
||||
/>
|
||||
)
|
||||
}
|
||||
className='!rounded-lg !px-4 !py-2'
|
||||
>
|
||||
{method.name}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Form.Slot>
|
||||
)}
|
||||
|
||||
{/* Creem 充值区域 */}
|
||||
{enableCreemTopUp && creemProducts.length > 0 && (
|
||||
<Form.Slot label={t('Creem 充值')}>
|
||||
|
||||
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useContext, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal';
|
||||
|
||||
const TopUp = () => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState] = useContext(StatusContext);
|
||||
|
||||
@@ -69,6 +71,11 @@ const TopUp = () => {
|
||||
const [creemOpen, setCreemOpen] = useState(false);
|
||||
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
|
||||
|
||||
// Waffo 相关状态
|
||||
const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
|
||||
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
|
||||
const [waffoMinTopUp, setWaffoMinTopUp] = useState(1);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [payWay, setPayWay] = useState('');
|
||||
@@ -256,7 +263,6 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setOpen(false);
|
||||
@@ -302,7 +308,6 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setCreemOpen(false);
|
||||
@@ -310,6 +315,37 @@ const TopUp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const waffoTopUp = async (payMethodIndex) => {
|
||||
try {
|
||||
if (topUpCount < waffoMinTopUp) {
|
||||
showError(t('充值数量不能小于') + waffoMinTopUp);
|
||||
return;
|
||||
}
|
||||
setPaymentLoading(true);
|
||||
const requestBody = {
|
||||
amount: parseInt(topUpCount),
|
||||
};
|
||||
if (payMethodIndex != null) {
|
||||
requestBody.pay_method_index = payMethodIndex;
|
||||
}
|
||||
const res = await API.post('/api/user/waffo/pay', requestBody);
|
||||
if (res !== undefined) {
|
||||
const { message, data } = res.data;
|
||||
if (message === 'success' && data?.payment_url) {
|
||||
window.open(data.payment_url, '_blank');
|
||||
} else {
|
||||
showError(data || t('支付请求失败'));
|
||||
}
|
||||
} else {
|
||||
showError(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setPaymentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processCreemCallback = (data) => {
|
||||
// 与 Stripe 保持一致的实现方式
|
||||
window.open(data.checkout_url, '_blank');
|
||||
@@ -449,17 +485,21 @@ const TopUp = () => {
|
||||
? data.min_topup
|
||||
: enableStripeTopUp
|
||||
? data.stripe_min_topup
|
||||
: 1;
|
||||
: data.enable_waffo_topup
|
||||
? data.waffo_min_topup
|
||||
: 1;
|
||||
setEnableOnlineTopUp(enableOnlineTopUp);
|
||||
setEnableStripeTopUp(enableStripeTopUp);
|
||||
setEnableCreemTopUp(enableCreemTopUp);
|
||||
const enableWaffoTopUp = data.enable_waffo_topup || false;
|
||||
setEnableWaffoTopUp(enableWaffoTopUp);
|
||||
setWaffoPayMethods(data.waffo_pay_methods || []);
|
||||
setWaffoMinTopUp(data.waffo_min_topup || 1);
|
||||
setMinTopUp(minTopUpValue);
|
||||
setTopUpCount(minTopUpValue);
|
||||
|
||||
// 设置 Creem 产品
|
||||
try {
|
||||
console.log(' data is ?', data);
|
||||
console.log(' creem products is ?', data.creem_products);
|
||||
const products = JSON.parse(data.creem_products || '[]');
|
||||
setCreemProducts(products);
|
||||
} catch (e) {
|
||||
@@ -474,7 +514,6 @@ const TopUp = () => {
|
||||
// 初始化显示实付金额
|
||||
getAmount(minTopUpValue);
|
||||
} catch (e) {
|
||||
console.log('解析支付方式失败:', e);
|
||||
setPayMethods([]);
|
||||
}
|
||||
|
||||
@@ -487,10 +526,10 @@ const TopUp = () => {
|
||||
setPresetAmounts(customPresets);
|
||||
}
|
||||
} else {
|
||||
console.error('获取充值配置失败:', data);
|
||||
showError(data || t('获取充值配置失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取充值配置异常:', error);
|
||||
showError(t('获取充值配置异常'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -531,6 +570,15 @@ const TopUp = () => {
|
||||
showSuccess(t('邀请链接已复制到剪切板'));
|
||||
};
|
||||
|
||||
// URL 参数自动打开账单弹窗(支付回跳时触发)
|
||||
useEffect(() => {
|
||||
if (searchParams.get('show_history') === 'true') {
|
||||
setOpenHistory(true);
|
||||
searchParams.delete('show_history');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 始终获取最新用户数据,确保余额等统计信息准确
|
||||
getUserQuota().then();
|
||||
@@ -587,7 +635,7 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// amount fetch failed silently
|
||||
}
|
||||
setAmountLoading(false);
|
||||
};
|
||||
@@ -613,7 +661,7 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// amount fetch failed silently
|
||||
} finally {
|
||||
setAmountLoading(false);
|
||||
}
|
||||
@@ -740,6 +788,9 @@ const TopUp = () => {
|
||||
enableCreemTopUp={enableCreemTopUp}
|
||||
creemProducts={creemProducts}
|
||||
creemPreTopUp={creemPreTopUp}
|
||||
enableWaffoTopUp={enableWaffoTopUp}
|
||||
waffoTopUp={waffoTopUp}
|
||||
waffoPayMethods={waffoPayMethods}
|
||||
presetAmounts={presetAmounts}
|
||||
selectedPreset={selectedPreset}
|
||||
selectPresetAmount={selectPresetAmount}
|
||||
|
||||
@@ -37,13 +37,13 @@ import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { API, timestamp2string } from '../../../helpers';
|
||||
import { isAdmin } from '../../../helpers/utils';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 状态映射配置
|
||||
const STATUS_CONFIG = {
|
||||
success: { type: 'success', key: '成功' },
|
||||
pending: { type: 'warning', key: '待支付' },
|
||||
failed: { type: 'danger', key: '失败' },
|
||||
expired: { type: 'danger', key: '已过期' },
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ const STATUS_CONFIG = {
|
||||
const PAYMENT_METHOD_MAP = {
|
||||
stripe: 'Stripe',
|
||||
creem: 'Creem',
|
||||
waffo: 'Waffo',
|
||||
alipay: '支付宝',
|
||||
wxpay: '微信',
|
||||
};
|
||||
@@ -62,7 +63,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const loadTopups = async (currentPage, currentPageSize) => {
|
||||
@@ -82,7 +82,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
Toast.error({ content: message || t('加载失败') });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load topups error:', error);
|
||||
Toast.error({ content: t('加载账单失败') });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -214,17 +213,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => {
|
||||
if (record.status !== 'pending') return null;
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
const actions = [];
|
||||
if (record.status === 'pending') {
|
||||
actions.push(
|
||||
<Button
|
||||
key="complete"
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return actions.length > 0 ? <>{actions}</> : null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+38
-18
@@ -36,6 +36,20 @@ export let API = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function redirectToOAuthUrl(url, options = {}) {
|
||||
const { openInNewTab = false } = options;
|
||||
const targetUrl = typeof url === 'string' ? url : url.toString();
|
||||
|
||||
if (openInNewTab) {
|
||||
window.open(targetUrl, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.assign(targetUrl);
|
||||
}
|
||||
|
||||
|
||||
function patchAPIInstance(instance) {
|
||||
const originalGet = instance.get.bind(instance);
|
||||
const inFlightGetRequests = new Map();
|
||||
@@ -249,7 +263,7 @@ export async function onDiscordOAuthClicked(client_id, options = {}) {
|
||||
const redirect_uri = `${window.location.origin}/oauth/discord`;
|
||||
const response_type = 'code';
|
||||
const scope = 'identify+openid';
|
||||
window.open(
|
||||
redirectToOAuthUrl(
|
||||
`https://discord.com/oauth2/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`,
|
||||
);
|
||||
}
|
||||
@@ -268,17 +282,13 @@ export async function onOIDCClicked(
|
||||
url.searchParams.set('response_type', 'code');
|
||||
url.searchParams.set('scope', 'openid profile email');
|
||||
url.searchParams.set('state', state);
|
||||
if (openInNewTab) {
|
||||
window.open(url.toString(), '_blank');
|
||||
} else {
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
redirectToOAuthUrl(url, { openInNewTab });
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id, options = {}) {
|
||||
const state = await prepareOAuthState(options);
|
||||
if (!state) return;
|
||||
window.open(
|
||||
redirectToOAuthUrl(
|
||||
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`,
|
||||
);
|
||||
}
|
||||
@@ -289,7 +299,7 @@ export async function onLinuxDOOAuthClicked(
|
||||
) {
|
||||
const state = await prepareOAuthState(options);
|
||||
if (!state) return;
|
||||
window.open(
|
||||
redirectToOAuthUrl(
|
||||
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
|
||||
);
|
||||
}
|
||||
@@ -307,29 +317,39 @@ export async function onLinuxDOOAuthClicked(
|
||||
export async function onCustomOAuthClicked(provider, options = {}) {
|
||||
const state = await prepareOAuthState(options);
|
||||
if (!state) return;
|
||||
|
||||
|
||||
try {
|
||||
const redirect_uri = `${window.location.origin}/oauth/${provider.slug}`;
|
||||
|
||||
|
||||
// Check if authorization_endpoint is a full URL or relative path
|
||||
let authUrl;
|
||||
if (provider.authorization_endpoint.startsWith('http://') ||
|
||||
provider.authorization_endpoint.startsWith('https://')) {
|
||||
if (
|
||||
provider.authorization_endpoint.startsWith('http://') ||
|
||||
provider.authorization_endpoint.startsWith('https://')
|
||||
) {
|
||||
authUrl = new URL(provider.authorization_endpoint);
|
||||
} else {
|
||||
// Relative path - this is a configuration error, show error message
|
||||
console.error('Custom OAuth authorization_endpoint must be a full URL:', provider.authorization_endpoint);
|
||||
showError('OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)');
|
||||
console.error(
|
||||
'Custom OAuth authorization_endpoint must be a full URL:',
|
||||
provider.authorization_endpoint,
|
||||
);
|
||||
showError(
|
||||
'OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
authUrl.searchParams.set('client_id', provider.client_id);
|
||||
authUrl.searchParams.set('redirect_uri', redirect_uri);
|
||||
authUrl.searchParams.set('response_type', 'code');
|
||||
authUrl.searchParams.set('scope', provider.scopes || 'openid profile email');
|
||||
authUrl.searchParams.set(
|
||||
'scope',
|
||||
provider.scopes || 'openid profile email',
|
||||
);
|
||||
authUrl.searchParams.set('state', state);
|
||||
|
||||
window.open(authUrl.toString());
|
||||
|
||||
redirectToOAuthUrl(authUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to initiate custom OAuth:', error);
|
||||
showError('OAuth 登录失败:' + (error.message || '未知错误'));
|
||||
|
||||
Vendored
+357
-329
File diff suppressed because it is too large
Load Diff
Vendored
+434
-406
File diff suppressed because it is too large
Load Diff
Vendored
+432
-404
File diff suppressed because it is too large
Load Diff
Vendored
+436
-408
File diff suppressed because it is too large
Load Diff
Vendored
+428
-400
File diff suppressed because it is too large
Load Diff
Vendored
+18
@@ -388,6 +388,10 @@
|
||||
"保存通用设置": "保存通用设置",
|
||||
"保存邮箱域名白名单设置": "保存邮箱域名白名单设置",
|
||||
"保存额度设置": "保存额度设置",
|
||||
"保留天数": "保留天数",
|
||||
"保留文件数": "保留文件数",
|
||||
"保留最近N个文件": "保留最近N个文件",
|
||||
"保留最近N天": "保留最近N天",
|
||||
"修复数据库一致性": "修复数据库一致性",
|
||||
"修改为": "修改为",
|
||||
"修改子渠道优先级": "修改子渠道优先级",
|
||||
@@ -880,7 +884,9 @@
|
||||
"将为选中的 ": "将为选中的 ",
|
||||
"将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "将仅保留第一个密钥文件,其余文件将被移除,是否继续?",
|
||||
"将删除": "将删除",
|
||||
"将删除 {{value}} 天前的日志文件。": "将删除 {{value}} 天前的日志文件。",
|
||||
"将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。",
|
||||
"将只保留最近 {{value}} 个日志文件,其余将被删除。": "将只保留最近 {{value}} 个日志文件,其余将被删除。",
|
||||
"将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?",
|
||||
"将清除选定时间之前的所有日志": "将清除选定时间之前的所有日志",
|
||||
"小时": "小时",
|
||||
@@ -944,6 +950,7 @@
|
||||
"已注销": "已注销",
|
||||
"已添加": "已添加",
|
||||
"已添加到白名单": "已添加到白名单",
|
||||
"已清理 {{count}} 个日志文件,释放 {{size}}": "已清理 {{count}} 个日志文件,释放 {{size}}",
|
||||
"已清空测试结果": "已清空测试结果",
|
||||
"已用": "已用",
|
||||
"已用/剩余": "已用/剩余",
|
||||
@@ -1240,8 +1247,12 @@
|
||||
"日志已下载": "日志已下载",
|
||||
"日志已加载": "日志已加载",
|
||||
"日志已复制到剪贴板": "日志已复制到剪贴板",
|
||||
"日志时间范围": "日志时间范围",
|
||||
"日志总大小": "日志总大小",
|
||||
"日志文件数": "日志文件数",
|
||||
"日志流": "日志流",
|
||||
"日志清理失败:": "日志清理失败:",
|
||||
"日志目录": "日志目录",
|
||||
"日志类型": "日志类型",
|
||||
"日志设置": "日志设置",
|
||||
"日志详情": "日志详情",
|
||||
@@ -1340,6 +1351,8 @@
|
||||
"服务可用性": "服务可用性",
|
||||
"服务商": "服务商",
|
||||
"服务器地址": "服务器地址",
|
||||
"服务器日志功能未启用(未配置日志目录)": "服务器日志功能未启用(未配置日志目录)",
|
||||
"服务器日志管理": "服务器日志管理",
|
||||
"服务显示名称": "服务显示名称",
|
||||
"未发现新增模型": "未发现新增模型",
|
||||
"未发现重复密钥": "未发现重复密钥",
|
||||
@@ -1591,6 +1604,8 @@
|
||||
"添加键值对": "添加键值对",
|
||||
"添加问答": "添加问答",
|
||||
"添加额度": "添加额度",
|
||||
"清理方式": "清理方式",
|
||||
"清理日志文件": "清理日志文件",
|
||||
"清空": "清空",
|
||||
"清空重定向": "清空重定向",
|
||||
"清除历史日志": "清除历史日志",
|
||||
@@ -1756,6 +1771,7 @@
|
||||
"确认延长容器时长": "确认延长容器时长",
|
||||
"确认操作": "确认操作",
|
||||
"确认新密码": "确认新密码",
|
||||
"确认清理日志文件?": "确认清理日志文件?",
|
||||
"确认清除历史日志": "确认清除历史日志",
|
||||
"确认禁用": "确认禁用",
|
||||
"确认补单": "确认补单",
|
||||
@@ -1817,6 +1833,7 @@
|
||||
"管理员账号": "管理员账号",
|
||||
"管理员账号已经初始化过,请继续设置其他参数": "管理员账号已经初始化过,请继续设置其他参数",
|
||||
"管理模型、标签、端点等预填组": "管理模型、标签、端点等预填组",
|
||||
"管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。",
|
||||
"类型": "类型",
|
||||
"粘贴图片失败": "粘贴图片失败",
|
||||
"精确": "精确",
|
||||
@@ -2211,6 +2228,7 @@
|
||||
"请输入新的部署名称": "请输入新的部署名称",
|
||||
"请输入显示名称": "请输入显示名称",
|
||||
"请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。",
|
||||
"请输入有效的数值": "请输入有效的数值",
|
||||
"请输入有效的数字": "请输入有效的数字",
|
||||
"请输入有效的镜像地址": "请输入有效的镜像地址",
|
||||
"请输入标签名称": "请输入标签名称",
|
||||
|
||||
Vendored
+28
@@ -389,6 +389,10 @@
|
||||
"保存通用设置": "儲存通用設定",
|
||||
"保存邮箱域名白名单设置": "儲存信箱域名白名單設定",
|
||||
"保存额度设置": "儲存額度設定",
|
||||
"保留天数": "保留天數",
|
||||
"保留文件数": "保留檔案數",
|
||||
"保留最近N个文件": "保留最近N個檔案",
|
||||
"保留最近N天": "保留最近N天",
|
||||
"修复数据库一致性": "修復資料庫一致性",
|
||||
"修改为": "修改為",
|
||||
"修改子渠道优先级": "修改子管道優先級",
|
||||
@@ -882,7 +886,9 @@
|
||||
"将 reasoning_content 转换为 <think> 标签拼接到内容中": "將 reasoning_content 轉換為 <think> 標籤拼接到內容中",
|
||||
"将为选中的 ": "將為選中的 ",
|
||||
"将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "將僅保留第一個密鑰檔案,其餘檔案將被移除,是否繼續?",
|
||||
"将只保留最近 {{value}} 个日志文件,其余将被删除。": "將只保留最近 {{value}} 個日誌檔案,其餘將被刪除。",
|
||||
"将删除": "將刪除",
|
||||
"将删除 {{value}} 天前的日志文件。": "將刪除 {{value}} 天前的日誌檔案。",
|
||||
"将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "將刪除已使用、已禁用及過期的兌換碼,此操作不可撤銷。",
|
||||
"将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "將清除所有儲存的設定並恢復預設設定,此操作不可撤銷。是否繼續?",
|
||||
"将清除选定时间之前的所有日志": "將清除選定時間之前的所有日誌",
|
||||
@@ -947,6 +953,7 @@
|
||||
"已注销": "已註銷",
|
||||
"已添加": "已添加",
|
||||
"已添加到白名单": "已添加到白名單",
|
||||
"已清理 {{count}} 个日志文件,释放 {{size}}": "已清理 {{count}} 個日誌檔案,釋放 {{size}}",
|
||||
"已清空测试结果": "已清空測試結果",
|
||||
"已用": "已用",
|
||||
"已用/剩余": "已用/剩餘",
|
||||
@@ -1243,8 +1250,12 @@
|
||||
"日志已下载": "日誌已下載",
|
||||
"日志已加载": "日誌已載入",
|
||||
"日志已复制到剪贴板": "日誌已複製到剪貼板",
|
||||
"日志时间范围": "日誌時間範圍",
|
||||
"日志总大小": "日誌總大小",
|
||||
"日志文件数": "日誌檔案數",
|
||||
"日志流": "日誌流",
|
||||
"日志清理失败:": "日誌清理失敗:",
|
||||
"日志目录": "日誌目錄",
|
||||
"日志类型": "日誌類型",
|
||||
"日志设置": "日誌設定",
|
||||
"日志详情": "日誌詳情",
|
||||
@@ -1344,6 +1355,8 @@
|
||||
"服务可用性": "服務可用性",
|
||||
"服务商": "服務商",
|
||||
"服务器地址": "伺服器位址",
|
||||
"服务器日志功能未启用(未配置日志目录)": "伺服器日誌功能未啟用(未配置日誌目錄)",
|
||||
"服务器日志管理": "伺服器日誌管理",
|
||||
"服务显示名称": "服務顯示名稱",
|
||||
"未发现新增模型": "未發現新增模型",
|
||||
"未发现重复密钥": "未發現重複密鑰",
|
||||
@@ -1597,6 +1610,8 @@
|
||||
"添加键值对": "添加鍵值對",
|
||||
"添加问答": "添加問答",
|
||||
"添加额度": "添加額度",
|
||||
"清理方式": "清理方式",
|
||||
"清理日志文件": "清理日誌檔案",
|
||||
"清空": "清空",
|
||||
"清空重定向": "清空重定向",
|
||||
"清除历史日志": "清除歷史日誌",
|
||||
@@ -1762,6 +1777,7 @@
|
||||
"确认延长容器时长": "確認延長容器時長",
|
||||
"确认操作": "確認操作",
|
||||
"确认新密码": "確認新密碼",
|
||||
"确认清理日志文件?": "確認清理日誌檔案?",
|
||||
"确认清除历史日志": "確認清除歷史日誌",
|
||||
"确认禁用": "確認禁用",
|
||||
"确认补单": "確認補單",
|
||||
@@ -1823,6 +1839,7 @@
|
||||
"管理员设置了外部链接,点击下方按钮访问": "管理員設定了外部連結,點擊下方按鈕訪問",
|
||||
"管理员账号": "管理員帳號",
|
||||
"管理员账号已经初始化过,请继续设置其他参数": "管理員帳號已經初始化過,請繼續設定其他參數",
|
||||
"管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。": "管理伺服器運行日誌檔案。日誌檔案會隨運行時間不斷累積,建議定期清理以釋放磁碟空間。",
|
||||
"管理模型、标签、端点等预填组": "管理模型、標籤、端點等預填組",
|
||||
"类型": "類型",
|
||||
"粘贴图片失败": "貼上圖片失敗",
|
||||
@@ -2219,6 +2236,7 @@
|
||||
"请输入新的部署名称": "請輸入新的部署名稱",
|
||||
"请输入显示名称": "請輸入顯示名稱",
|
||||
"请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "請輸入有效的JSON格式的請求體。您可以參考預覽面板中的預設請求體格式。",
|
||||
"请输入有效的数值": "請輸入有效的數值",
|
||||
"请输入有效的数字": "請輸入有效的數位",
|
||||
"请输入有效的镜像地址": "請輸入有效的鏡像位址",
|
||||
"请输入标签名称": "請輸入標籤名稱",
|
||||
@@ -2900,6 +2918,16 @@
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "提示:端點映射僅用於模型廣場展示,不會影響模型真實呼叫。如需配置真實呼叫,請前往「管道管理」。",
|
||||
"购买订阅获得模型额度/次数": "購買訂閱取得模型額度/次數",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "正式環境 RSA 私鑰 Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "沙盒環境 RSA 私鑰 Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "正式環境 Waffo 公鑰 Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "沙盒環境 Waffo 公鑰 Base64 (X.509 DER)",
|
||||
"支付方式类型": "付款方式類型",
|
||||
"支付方式名称": "付款方式名稱",
|
||||
"获取充值配置失败": "取得儲值設定失敗",
|
||||
"获取充值配置异常": "儲值設定異常",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "模型價格 {{price}}",
|
||||
|
||||
@@ -31,8 +31,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
const GEMINI_SETTING_EXAMPLE = {
|
||||
default: 'OFF',
|
||||
HARM_CATEGORY_CIVIC_INTEGRITY: 'BLOCK_NONE',
|
||||
default: 'OFF'
|
||||
};
|
||||
|
||||
const GEMINI_VERSION_EXAMPLE = {
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Spin,
|
||||
Table,
|
||||
Modal,
|
||||
Input,
|
||||
Space,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function SettingsPaymentGatewayWaffo(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
WaffoEnabled: false,
|
||||
WaffoApiKey: '',
|
||||
WaffoPrivateKey: '',
|
||||
WaffoPublicCert: '',
|
||||
WaffoSandboxPublicCert: '',
|
||||
WaffoSandboxApiKey: '',
|
||||
WaffoSandboxPrivateKey: '',
|
||||
WaffoSandbox: false,
|
||||
WaffoMerchantId: '',
|
||||
WaffoCurrency: 'USD',
|
||||
WaffoUnitPrice: 1.0,
|
||||
WaffoMinTopUp: 1,
|
||||
WaffoNotifyUrl: '',
|
||||
WaffoReturnUrl: '',
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
const formApiRef = useRef(null);
|
||||
const iconFileInputRef = useRef(null);
|
||||
|
||||
const handleIconFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const MAX_ICON_SIZE = 100 * 1024; // 100 KB
|
||||
if (file.size > MAX_ICON_SIZE) {
|
||||
showError(t('图标文件不能超过 100KB,请压缩后重新上传'));
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setPayMethodForm((prev) => ({ ...prev, icon: event.target.result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
// 支付方式列表
|
||||
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
|
||||
// 支付方式弹窗
|
||||
const [payMethodModalVisible, setPayMethodModalVisible] = useState(false);
|
||||
// 当前编辑的索引,-1 表示新增
|
||||
const [editingPayMethodIndex, setEditingPayMethodIndex] = useState(-1);
|
||||
// 弹窗内表单字段的临时状态
|
||||
const [payMethodForm, setPayMethodForm] = useState({
|
||||
name: '',
|
||||
icon: '',
|
||||
payMethodType: '',
|
||||
payMethodName: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (props.options && formApiRef.current) {
|
||||
const currentInputs = {
|
||||
WaffoEnabled: props.options.WaffoEnabled === 'true' || props.options.WaffoEnabled === true,
|
||||
WaffoApiKey: props.options.WaffoApiKey || '',
|
||||
WaffoPrivateKey: props.options.WaffoPrivateKey || '',
|
||||
WaffoPublicCert: props.options.WaffoPublicCert || '',
|
||||
WaffoSandboxPublicCert: props.options.WaffoSandboxPublicCert || '',
|
||||
WaffoSandboxApiKey: props.options.WaffoSandboxApiKey || '',
|
||||
WaffoSandboxPrivateKey: props.options.WaffoSandboxPrivateKey || '',
|
||||
WaffoSandbox: props.options.WaffoSandbox === 'true',
|
||||
WaffoMerchantId: props.options.WaffoMerchantId || '',
|
||||
WaffoCurrency: props.options.WaffoCurrency || 'USD',
|
||||
WaffoUnitPrice: parseFloat(props.options.WaffoUnitPrice) || 1.0,
|
||||
WaffoMinTopUp: parseInt(props.options.WaffoMinTopUp) || 1,
|
||||
WaffoNotifyUrl: props.options.WaffoNotifyUrl || '',
|
||||
WaffoReturnUrl: props.options.WaffoReturnUrl || '',
|
||||
};
|
||||
setInputs(currentInputs);
|
||||
setOriginInputs({ ...currentInputs });
|
||||
formApiRef.current.setValues(currentInputs);
|
||||
|
||||
// 解析支付方式列表
|
||||
try {
|
||||
const rawPayMethods = props.options.WaffoPayMethods;
|
||||
if (rawPayMethods) {
|
||||
const parsed = JSON.parse(rawPayMethods);
|
||||
if (Array.isArray(parsed)) {
|
||||
setWaffoPayMethods(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setWaffoPayMethods([]);
|
||||
}
|
||||
}
|
||||
}, [props.options]);
|
||||
|
||||
const handleFormChange = (values) => {
|
||||
setInputs(values);
|
||||
};
|
||||
|
||||
const submitWaffoSetting = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const options = [];
|
||||
|
||||
options.push({
|
||||
key: 'WaffoEnabled',
|
||||
value: inputs.WaffoEnabled ? 'true' : 'false',
|
||||
});
|
||||
|
||||
if (inputs.WaffoApiKey && inputs.WaffoApiKey !== '') {
|
||||
options.push({ key: 'WaffoApiKey', value: inputs.WaffoApiKey });
|
||||
}
|
||||
|
||||
if (inputs.WaffoPrivateKey && inputs.WaffoPrivateKey !== '') {
|
||||
options.push({ key: 'WaffoPrivateKey', value: inputs.WaffoPrivateKey });
|
||||
}
|
||||
|
||||
options.push({ key: 'WaffoPublicCert', value: inputs.WaffoPublicCert || '' });
|
||||
options.push({ key: 'WaffoSandboxPublicCert', value: inputs.WaffoSandboxPublicCert || '' });
|
||||
|
||||
if (inputs.WaffoSandboxApiKey && inputs.WaffoSandboxApiKey !== '') {
|
||||
options.push({ key: 'WaffoSandboxApiKey', value: inputs.WaffoSandboxApiKey });
|
||||
}
|
||||
|
||||
if (inputs.WaffoSandboxPrivateKey && inputs.WaffoSandboxPrivateKey !== '') {
|
||||
options.push({ key: 'WaffoSandboxPrivateKey', value: inputs.WaffoSandboxPrivateKey });
|
||||
}
|
||||
|
||||
options.push({
|
||||
key: 'WaffoSandbox',
|
||||
value: inputs.WaffoSandbox ? 'true' : 'false',
|
||||
});
|
||||
|
||||
options.push({ key: 'WaffoMerchantId', value: inputs.WaffoMerchantId || '' });
|
||||
options.push({ key: 'WaffoCurrency', value: inputs.WaffoCurrency || '' });
|
||||
|
||||
options.push({
|
||||
key: 'WaffoUnitPrice',
|
||||
value: String(inputs.WaffoUnitPrice || 1.0),
|
||||
});
|
||||
|
||||
options.push({
|
||||
key: 'WaffoMinTopUp',
|
||||
value: String(inputs.WaffoMinTopUp || 1),
|
||||
});
|
||||
|
||||
options.push({ key: 'WaffoNotifyUrl', value: inputs.WaffoNotifyUrl || '' });
|
||||
options.push({ key: 'WaffoReturnUrl', value: inputs.WaffoReturnUrl || '' });
|
||||
|
||||
// 保存支付方式列表
|
||||
options.push({
|
||||
key: 'WaffoPayMethods',
|
||||
value: JSON.stringify(waffoPayMethods),
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
const requestQueue = options.map((opt) =>
|
||||
API.put('/api/option/', {
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(requestQueue);
|
||||
|
||||
// 检查所有请求是否成功
|
||||
const errorResults = results.filter((res) => !res.data.success);
|
||||
if (errorResults.length > 0) {
|
||||
errorResults.forEach((res) => {
|
||||
showError(res.data.message);
|
||||
});
|
||||
} else {
|
||||
showSuccess(t('更新成功'));
|
||||
// 更新本地存储的原始值
|
||||
setOriginInputs({ ...inputs });
|
||||
props.refresh?.();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('更新失败'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 打开新增弹窗
|
||||
const openAddPayMethodModal = () => {
|
||||
setEditingPayMethodIndex(-1);
|
||||
setPayMethodForm({ name: '', icon: '', payMethodType: '', payMethodName: '' });
|
||||
setPayMethodModalVisible(true);
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEditPayMethodModal = (record, index) => {
|
||||
setEditingPayMethodIndex(index);
|
||||
setPayMethodForm({
|
||||
name: record.name || '',
|
||||
icon: record.icon || '',
|
||||
payMethodType: record.payMethodType || '',
|
||||
payMethodName: record.payMethodName || '',
|
||||
});
|
||||
setPayMethodModalVisible(true);
|
||||
};
|
||||
|
||||
// 确认保存弹窗(新增或编辑)
|
||||
const handlePayMethodModalOk = () => {
|
||||
if (!payMethodForm.name || payMethodForm.name.trim() === '') {
|
||||
showError(t('支付方式名称不能为空'));
|
||||
return;
|
||||
}
|
||||
const newMethod = {
|
||||
name: payMethodForm.name.trim(),
|
||||
icon: payMethodForm.icon.trim(),
|
||||
payMethodType: payMethodForm.payMethodType.trim(),
|
||||
payMethodName: payMethodForm.payMethodName.trim(),
|
||||
};
|
||||
if (editingPayMethodIndex === -1) {
|
||||
// 新增
|
||||
setWaffoPayMethods([...waffoPayMethods, newMethod]);
|
||||
} else {
|
||||
// 编辑
|
||||
const updated = [...waffoPayMethods];
|
||||
updated[editingPayMethodIndex] = newMethod;
|
||||
setWaffoPayMethods(updated);
|
||||
}
|
||||
setPayMethodModalVisible(false);
|
||||
};
|
||||
|
||||
// 删除支付方式
|
||||
const handleDeletePayMethod = (index) => {
|
||||
const updated = waffoPayMethods.filter((_, i) => i !== index);
|
||||
setWaffoPayMethods(updated);
|
||||
};
|
||||
|
||||
// 支付方式表格列定义
|
||||
const payMethodColumns = [
|
||||
{
|
||||
title: t('显示名称'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: t('图标'),
|
||||
dataIndex: 'icon',
|
||||
render: (text) =>
|
||||
text ? (
|
||||
<img
|
||||
src={text}
|
||||
alt='icon'
|
||||
style={{ width: 24, height: 24, objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<Text type='tertiary'>—</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('支付方式类型'),
|
||||
dataIndex: 'payMethodType',
|
||||
render: (text) => text || <Text type='tertiary'>—</Text>,
|
||||
},
|
||||
{
|
||||
title: t('支付方式名称'),
|
||||
dataIndex: 'payMethodName',
|
||||
render: (text) => text || <Text type='tertiary'>—</Text>,
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record, index) => (
|
||||
<Space>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() => openEditPayMethodModal(record, index)}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='danger'
|
||||
onClick={() => handleDeletePayMethod(index)}
|
||||
>
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
onValueChange={handleFormChange}
|
||||
getFormApi={(api) => (formApiRef.current = api)}
|
||||
>
|
||||
<Form.Section text={t('Waffo 设置')}>
|
||||
<Text>
|
||||
{t('Waffo 是一个支付聚合平台,支持多种支付方式。')}
|
||||
<a href='https://waffo.com' target='_blank' rel='noreferrer'>
|
||||
Waffo Official Site
|
||||
</a>
|
||||
<br />
|
||||
</Text>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t(
|
||||
'请在 Waffo 后台获取 API 密钥、商户 ID 以及 RSA 密钥对,并配置回调地址。',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
field='WaffoEnabled'
|
||||
label={t('启用 Waffo')}
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
field='WaffoSandbox'
|
||||
label={t('沙盒模式')}
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
extraText={t('启用后将使用 Waffo 沙盒环境')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field='WaffoApiKey'
|
||||
label={t('API 密钥 (生产)')}
|
||||
placeholder={t('生产环境 Waffo API 密钥')}
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field='WaffoSandboxApiKey'
|
||||
label={t('API 密钥 (沙盒)')}
|
||||
placeholder={t('沙盒环境 Waffo API 密钥')}
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field='WaffoMerchantId'
|
||||
label={t('商户 ID')}
|
||||
placeholder={t('Waffo 商户 ID')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.TextArea
|
||||
field='WaffoPrivateKey'
|
||||
label={t('RSA 私钥 (生产)')}
|
||||
placeholder={t('生产环境 RSA 私钥 Base64 (PKCS#8 DER)')}
|
||||
type='password'
|
||||
autosize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.TextArea
|
||||
field='WaffoSandboxPrivateKey'
|
||||
label={t('RSA 私钥 (沙盒)')}
|
||||
placeholder={t('沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)')}
|
||||
type='password'
|
||||
autosize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.TextArea
|
||||
field='WaffoPublicCert'
|
||||
label={t('Waffo 公钥 (生产)')}
|
||||
placeholder={t('生产环境 Waffo 公钥 Base64 (X.509 DER)')}
|
||||
type='password'
|
||||
autosize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.TextArea
|
||||
field='WaffoSandboxPublicCert'
|
||||
label={t('Waffo 公钥 (沙盒)')}
|
||||
placeholder={t('沙盒环境 Waffo 公钥 Base64 (X.509 DER)')}
|
||||
type='password'
|
||||
autosize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='WaffoCurrency'
|
||||
label={t('货币')}
|
||||
disabled
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='WaffoUnitPrice'
|
||||
label={t('单价 (USD)')}
|
||||
placeholder='1.0'
|
||||
min={0}
|
||||
step={0.1}
|
||||
extraText={t('每个充值单位对应的 USD 金额,默认 1.0')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='WaffoMinTopUp'
|
||||
label={t('最低充值数量')}
|
||||
placeholder='1'
|
||||
min={1}
|
||||
step={1}
|
||||
extraText={t('Waffo 充值的最低数量,默认 1')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field='WaffoNotifyUrl'
|
||||
label={t('回调通知地址')}
|
||||
placeholder={t('例如 https://example.com/api/waffo/webhook')}
|
||||
extraText={t('留空则自动使用 服务器地址 + /api/waffo/webhook')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field='WaffoReturnUrl'
|
||||
label={t('支付返回地址')}
|
||||
placeholder={t('例如 https://example.com/console/topup')}
|
||||
extraText={t('支付完成后用户跳转的页面,留空则自动使用 服务器地址 + /console/topup')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Button onClick={submitWaffoSetting} style={{ marginTop: 16 }}>
|
||||
{t('更新 Waffo 设置')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
|
||||
{/* 支付方式配置区块(独立于 Form,使用独立状态管理) */}
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Typography.Title heading={6} style={{ marginBottom: 8 }}>{t('支付方式')}</Typography.Title>
|
||||
<Text type='secondary'>
|
||||
{t('配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。')}
|
||||
</Text>
|
||||
<div style={{ marginTop: 12, marginBottom: 12 }}>
|
||||
<Button onClick={openAddPayMethodModal}>
|
||||
{t('新增支付方式')}
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={payMethodColumns}
|
||||
dataSource={waffoPayMethods}
|
||||
rowKey={(record, index) => index}
|
||||
pagination={false}
|
||||
size='small'
|
||||
empty={<Text type='tertiary'>{t('暂无支付方式,点击上方按钮新增')}</Text>}
|
||||
/>
|
||||
<Button onClick={submitWaffoSetting} style={{ marginTop: 16 }}>
|
||||
{t('更新 Waffo 设置')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 新增/编辑支付方式弹窗 */}
|
||||
<Modal
|
||||
title={editingPayMethodIndex === -1 ? t('新增支付方式') : t('编辑支付方式')}
|
||||
visible={payMethodModalVisible}
|
||||
onOk={handlePayMethodModalOk}
|
||||
onCancel={() => setPayMethodModalVisible(false)}
|
||||
okText={t('确定')}
|
||||
cancelText={t('取消')}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text strong>{t('显示名称')}</Text>
|
||||
<span style={{ color: 'var(--semi-color-danger)', marginLeft: 4 }}>*</span>
|
||||
</div>
|
||||
<Input
|
||||
value={payMethodForm.name}
|
||||
onChange={(val) => setPayMethodForm({ ...payMethodForm, name: val })}
|
||||
placeholder={t('例如:Credit Card')}
|
||||
/>
|
||||
<Text type='tertiary' size='small'>{t('用户在充值页面看到的支付方式名称,例如:Credit Card')}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text strong>{t('图标')}</Text>
|
||||
</div>
|
||||
<Space align='center'>
|
||||
{payMethodForm.icon && (
|
||||
<img
|
||||
src={payMethodForm.icon}
|
||||
alt='preview'
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
objectFit: 'contain',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type='file'
|
||||
accept='image/*'
|
||||
ref={iconFileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleIconFileChange}
|
||||
/>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() => iconFileInputRef.current?.click()}
|
||||
>
|
||||
{payMethodForm.icon ? t('重新上传') : t('上传图片')}
|
||||
</Button>
|
||||
{payMethodForm.icon && (
|
||||
<Button
|
||||
size='small'
|
||||
type='danger'
|
||||
onClick={() =>
|
||||
setPayMethodForm((prev) => ({ ...prev, icon: '' }))
|
||||
}
|
||||
>
|
||||
{t('清除')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
<div>
|
||||
<Text type='tertiary' size='small'>{t('上传 PNG/JPG/SVG 图片,建议尺寸 ≤ 128×128px')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text strong>{t('Pay Method Type')}</Text>
|
||||
</div>
|
||||
<Input
|
||||
value={payMethodForm.payMethodType}
|
||||
onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodType: val })}
|
||||
placeholder='CREDITCARD,DEBITCARD'
|
||||
maxLength={64}
|
||||
/>
|
||||
<Text type='tertiary' size='small'>{t('Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)')}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text strong>{t('Pay Method Name')}</Text>
|
||||
</div>
|
||||
<Input
|
||||
value={payMethodForm.payMethodName}
|
||||
onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodName: val })}
|
||||
placeholder={t('可空')}
|
||||
maxLength={64}
|
||||
/>
|
||||
<Text type='tertiary' size='small'>{t('Waffo API 参数,可空(最多64位)')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -23,12 +23,15 @@ import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
InputNumber,
|
||||
Row,
|
||||
Spin,
|
||||
Progress,
|
||||
Descriptions,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
@@ -72,6 +75,10 @@ export default function SettingsPerformance(props) {
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
const [logInfo, setLogInfo] = useState(null);
|
||||
const [logCleanupMode, setLogCleanupMode] = useState('by_count');
|
||||
const [logCleanupValue, setLogCleanupValue] = useState(10);
|
||||
const [logCleanupLoading, setLogCleanupLoading] = useState(false);
|
||||
|
||||
function handleFieldChange(fieldName) {
|
||||
return (value) => {
|
||||
@@ -167,6 +174,46 @@ export default function SettingsPerformance(props) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLogInfo() {
|
||||
try {
|
||||
const res = await API.get('/api/performance/logs');
|
||||
if (res.data.success) {
|
||||
setLogInfo(res.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch log info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupLogFiles() {
|
||||
if (logCleanupValue == null || isNaN(logCleanupValue) || logCleanupValue < 1) {
|
||||
showError(t('请输入有效的数值'));
|
||||
return;
|
||||
}
|
||||
setLogCleanupLoading(true);
|
||||
try {
|
||||
const res = await API.delete(
|
||||
`/api/performance/logs?mode=${logCleanupMode}&value=${logCleanupValue}`,
|
||||
);
|
||||
if (res.data.success) {
|
||||
const { deleted_count, freed_bytes } = res.data.data;
|
||||
showSuccess(
|
||||
t('已清理 {{count}} 个日志文件,释放 {{size}}', {
|
||||
count: deleted_count,
|
||||
size: formatBytes(freed_bytes),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
showError(res.data.message || t('清理失败'));
|
||||
}
|
||||
fetchLogInfo();
|
||||
} catch (error) {
|
||||
showError(t('清理失败'));
|
||||
} finally {
|
||||
setLogCleanupLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentInputs = {};
|
||||
for (let key in props.options) {
|
||||
@@ -187,6 +234,7 @@ export default function SettingsPerformance(props) {
|
||||
refForm.current.setValues({ ...inputs, ...currentInputs });
|
||||
}
|
||||
fetchStats();
|
||||
fetchLogInfo();
|
||||
}, [props.options]);
|
||||
|
||||
const diskCacheUsagePercent =
|
||||
@@ -351,6 +399,112 @@ export default function SettingsPerformance(props) {
|
||||
</Form>
|
||||
</Spin>
|
||||
|
||||
{/* 服务器日志管理 */}
|
||||
<Form.Section text={t('服务器日志管理')}>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t(
|
||||
'管理服务器运行日志文件。日志文件会随运行时间不断累积,建议定期清理以释放磁盘空间。',
|
||||
)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
{logInfo === null ? null : logInfo.enabled ? (
|
||||
<>
|
||||
<Descriptions
|
||||
data={[
|
||||
{ key: t('日志目录'), value: logInfo.log_dir },
|
||||
{
|
||||
key: t('日志文件数'),
|
||||
value: logInfo.file_count,
|
||||
},
|
||||
{
|
||||
key: t('日志总大小'),
|
||||
value: formatBytes(logInfo.total_size),
|
||||
},
|
||||
...(logInfo.oldest_time && logInfo.newest_time
|
||||
? [
|
||||
{
|
||||
key: t('日志时间范围'),
|
||||
value: `${new Date(logInfo.oldest_time).toLocaleDateString()} ~ ${new Date(logInfo.newest_time).toLocaleDateString()}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>
|
||||
{t('清理方式')}
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={logCleanupMode}
|
||||
onChange={(e) => setLogCleanupMode(e.target.value)}
|
||||
>
|
||||
<Radio value='by_count'>{t('保留最近N个文件')}</Radio>
|
||||
<Radio value='by_days'>{t('保留最近N天')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>
|
||||
{logCleanupMode === 'by_count'
|
||||
? t('保留文件数')
|
||||
: t('保留天数')}
|
||||
</Text>
|
||||
<InputNumber
|
||||
value={logCleanupValue}
|
||||
min={1}
|
||||
max={logCleanupMode === 'by_count' ? 1000 : 3650}
|
||||
onChange={(value) => setLogCleanupValue(value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
visibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
||||
</Text>
|
||||
<Popconfirm
|
||||
title={t('确认清理日志文件?')}
|
||||
content={
|
||||
logCleanupMode === 'by_count'
|
||||
? t(
|
||||
'将只保留最近 {{value}} 个日志文件,其余将被删除。',
|
||||
{ value: logCleanupValue },
|
||||
)
|
||||
: t('将删除 {{value}} 天前的日志文件。', {
|
||||
value: logCleanupValue,
|
||||
})
|
||||
}
|
||||
onConfirm={cleanupLogFiles}
|
||||
>
|
||||
<Button type='danger' loading={logCleanupLoading}>
|
||||
{t('清理日志文件')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('服务器日志功能未启用(未配置日志目录)')}
|
||||
/>
|
||||
)}
|
||||
</Form.Section>
|
||||
|
||||
{/* 性能统计 */}
|
||||
<Spin spinning={statsLoading}>
|
||||
<Form.Section text={t('性能监控')}>
|
||||
|
||||
Reference in New Issue
Block a user