fix: merge general improvements from release branch
Backend: - gateway_handler: pass subject.UserID instead of int64(0) for user-level routing - setting_handler: add missing BalanceLowNotifyRechargeURL to UpdateSettings response - openai_gateway_service: use applyAccountStatsCost for account stats pricing integration - embed_on: add local file override (data/public/) for embedded frontend assets Frontend: - useTableSelection: add batchUpdate method for batch operations - AccountsView: virtual scrolling params, Set-based isSelected, swipe virtualization - ProxiesView: add batchUpdate to selection and swipe-select - BulkEditAccountModal: fix submit handler to prevent event object as argument - SettingsView: move payload construction outside try block - i18n: add general translation keys (saved, deleted, view, validation, allowUserRefund) - api/client: reorder error fields for consistency - stores/payment: clarify pollOrderStatus JSDoc
This commit is contained in:
@@ -1071,6 +1071,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
||||
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL,
|
||||
AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
|
||||
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
|
||||
PaymentEnabled: updatedPaymentCfg.Enabled,
|
||||
|
||||
@@ -522,7 +522,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
|
||||
for {
|
||||
// 选择支持该模型的账号
|
||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, parsedReq.MetadataUserID, int64(0))
|
||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, parsedReq.MetadataUserID, subject.UserID)
|
||||
if err != nil {
|
||||
if len(fs.FailedAccountIDs) == 0 {
|
||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
||||
|
||||
@@ -4575,14 +4575,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
||||
|
||||
// 计算账号统计定价费用(使用最终上游模型匹配自定义规则)
|
||||
if apiKey.GroupID != nil {
|
||||
statsModel := result.UpstreamModel
|
||||
if statsModel == "" {
|
||||
statsModel = result.Model
|
||||
}
|
||||
usageLog.AccountStatsCost = resolveAccountStatsCost(
|
||||
ctx, s.channelService, s.billingService,
|
||||
account.ID, *apiKey.GroupID, statsModel,
|
||||
tokens, 1, cost.TotalCost,
|
||||
applyAccountStatsCost(ctx, usageLog, s.channelService, s.billingService,
|
||||
account.ID, *apiKey.GroupID, result.UpstreamModel, result.Model,
|
||||
tokens, cost.TotalCost,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,11 +34,12 @@ type PublicSettingsProvider interface {
|
||||
|
||||
// FrontendServer serves the embedded frontend with settings injection
|
||||
type FrontendServer struct {
|
||||
distFS fs.FS
|
||||
fileServer http.Handler
|
||||
baseHTML []byte
|
||||
cache *HTMLCache
|
||||
settings PublicSettingsProvider
|
||||
distFS fs.FS
|
||||
fileServer http.Handler
|
||||
baseHTML []byte
|
||||
cache *HTMLCache
|
||||
settings PublicSettingsProvider
|
||||
overrideDir string // local file override directory
|
||||
}
|
||||
|
||||
// NewFrontendServer creates a new frontend server with settings injection
|
||||
@@ -62,11 +65,12 @@ func NewFrontendServer(settingsProvider PublicSettingsProvider) (*FrontendServer
|
||||
cache.SetBaseHTML(baseHTML)
|
||||
|
||||
return &FrontendServer{
|
||||
distFS: distFS,
|
||||
fileServer: http.FileServer(http.FS(distFS)),
|
||||
baseHTML: baseHTML,
|
||||
cache: cache,
|
||||
settings: settingsProvider,
|
||||
distFS: distFS,
|
||||
fileServer: http.FileServer(http.FS(distFS)),
|
||||
baseHTML: baseHTML,
|
||||
cache: cache,
|
||||
settings: settingsProvider,
|
||||
overrideDir: filepath.Join("data", "public"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -99,6 +103,11 @@ func (s *FrontendServer) Middleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Try local override first
|
||||
if s.tryServeOverride(c, cleanPath) {
|
||||
return
|
||||
}
|
||||
|
||||
// Serve static files normally
|
||||
s.fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
c.Abort()
|
||||
@@ -114,6 +123,22 @@ func (s *FrontendServer) fileExists(path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// tryServeOverride checks if a local override file exists and serves it.
|
||||
// Files in overrideDir take precedence over embedded files.
|
||||
func (s *FrontendServer) tryServeOverride(c *gin.Context, cleanPath string) bool {
|
||||
if s.overrideDir == "" {
|
||||
return false
|
||||
}
|
||||
filePath := filepath.Join(s.overrideDir, filepath.Clean("/"+cleanPath))
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil || info.IsDir() {
|
||||
return false
|
||||
}
|
||||
c.File(filePath)
|
||||
c.Abort()
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *FrontendServer) serveIndexHTML(c *gin.Context) {
|
||||
// Get nonce from context (generated by SecurityHeaders middleware)
|
||||
nonce := middleware.GetNonceFromContext(c)
|
||||
@@ -226,6 +251,7 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
|
||||
panic("failed to get dist subdirectory: " + err.Error())
|
||||
}
|
||||
fileServer := http.FileServer(http.FS(distFS))
|
||||
overrideDir := filepath.Join("data", "public")
|
||||
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
@@ -242,6 +268,10 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
|
||||
|
||||
if file, err := distFS.Open(cleanPath); err == nil {
|
||||
_ = file.Close()
|
||||
// Try local override first
|
||||
if tryServeOverrideFile(c, overrideDir, cleanPath) {
|
||||
return
|
||||
}
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
c.Abort()
|
||||
return
|
||||
@@ -251,6 +281,21 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// tryServeOverrideFile is a standalone version of tryServeOverride for legacy usage.
|
||||
func tryServeOverrideFile(c *gin.Context, overrideDir, cleanPath string) bool {
|
||||
if overrideDir == "" {
|
||||
return false
|
||||
}
|
||||
filePath := filepath.Join(overrideDir, filepath.Clean("/"+cleanPath))
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil || info.IsDir() {
|
||||
return false
|
||||
}
|
||||
c.File(filePath)
|
||||
c.Abort()
|
||||
return true
|
||||
}
|
||||
|
||||
func shouldBypassEmbeddedFrontend(path string) bool {
|
||||
trimmed := strings.TrimSpace(path)
|
||||
return strings.HasPrefix(trimmed, "/api/") ||
|
||||
|
||||
Reference in New Issue
Block a user