diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 9b49150c..29c97b4b 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -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, diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 30065463..f5eff8c9 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -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) diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 9a6fbb8f..6087b7b6 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -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, ) } diff --git a/backend/internal/web/embed_on.go b/backend/internal/web/embed_on.go index ad5ac7d8..89d09eef 100644 --- a/backend/internal/web/embed_on.go +++ b/backend/internal/web/embed_on.go @@ -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/") || diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 2908c6b1..8a586902 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -270,9 +270,9 @@ apiClient.interceptors.response.use( return Promise.reject({ status, code: apiData.code, + reason: apiData.reason, error: apiData.error, message: apiData.message || apiData.detail || error.message, - reason: apiData.reason, metadata: apiData.metadata, }) } diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 2934fbd9..5461015b 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -5,7 +5,7 @@ width="wide" @close="handleClose" > -
+

diff --git a/frontend/src/composables/useTableSelection.ts b/frontend/src/composables/useTableSelection.ts index a65144a9..f0e096ff 100644 --- a/frontend/src/composables/useTableSelection.ts +++ b/frontend/src/composables/useTableSelection.ts @@ -76,6 +76,12 @@ export function useTableSelection({ rows, getId }: UseTableSelectionOptions) => void) => { + const draft = new Set(selectedSet.value) + updater(draft) + replaceSelectedSet(draft) + } + const selectVisible = () => { toggleVisible(true) } @@ -93,6 +99,7 @@ export function useTableSelection({ rows, getId }: UseTableSelectionOptions { return response.data } - /** Poll order status by ID */ + /** Poll order status by ID (read-only, no upstream check) */ async function pollOrderStatus(orderId: number): Promise { try { const response = await paymentAPI.getOrder(orderId) diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index d7fae112..4fec956b 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -144,6 +144,7 @@