1. 14 Apr, 2026 40 commits
    • erio's avatar
      fix(accounts): unify modal width, add notify props to create, fix quota layout · a43da622
      erio authored
      - EditAccountModal width changed from "normal" to "wide" (match CreateAccountModal)
      - CreateAccountModal now passes all quota notify props to QuotaLimitCard
      - QuotaLimitCard: when global notify disabled, hide title row, input takes full width
      - Quota alert email: show remaining quota + threshold (fixed/$, percentage/%) instead of usage trigger point
      a43da622
    • erio's avatar
      6e9146e7
    • erio's avatar
      f571d8ff
    • erio's avatar
    • erio's avatar
      feat(notify): add platform/ID to quota alert email, add recharge URL to balance alert · c1eb79e4
      erio authored
      - Quota alert email now shows account ID and platform
      - Balance low email includes a "Top Up Now" button when recharge URL is configured
      - New setting: balance_low_notify_recharge_url in admin settings
      c1eb79e4
    • erio's avatar
    • erio's avatar
      fix: change quota notify threshold semantics to "remaining quota" · 216bda58
      erio authored
      Threshold now represents remaining quota instead of usage amount:
      - Fixed ($): threshold=400, limit=1000 → alert when remaining drops to $400
        (i.e., usage reaches $600)
      - Percentage (%): threshold=30%, limit=1000 → alert when remaining drops
        to 30% (i.e., usage reaches $700)
      
      Also:
      - Rename 告警阈值 → 提醒阈值 in i18n
      - Widen type dropdown to w-16 for proper $ / % display
      216bda58
    • erio's avatar
      fix(frontend): place quota notify toggle inline with limit input · 7141dcee
      erio authored
      Move QuotaNotifyToggle to the same row as the limit $ input for all
      three dimensions (daily/weekly/total), significantly reducing card height.
      7141dcee
    • erio's avatar
      fix(frontend): collapsible quota card and compact notify layout · ac554432
      erio authored
      - QuotaLimitCard: add collapse/expand toggle (chevron icon + click header)
      - QuotaNotifyToggle: show $ or % suffix in threshold input
      - Reduce vertical spacing between reset mode hint and notify toggle
      ac554432
    • erio's avatar
      fix(frontend): quota notify UI improvements · 2066c478
      erio authored
      - QuotaNotifyToggle: add $ or % suffix to threshold input based on type
      - QuotaLimitCard: combine reset mode and notify toggle on same row
        to reduce vertical height for daily/weekly sections
      - Remove redundant ml-4 indentation from QuotaNotifyToggle
      2066c478
    • erio's avatar
      fix: correct account stats pricing priority order · 98c9d517
      erio authored
      Priority was wrong:
      - Before: custom rules → LiteLLM (when ApplyPricingToAccountStats) → nil
      - After:  custom rules → totalCost (when ApplyPricingToAccountStats) → LiteLLM → nil
      
      When ApplyPricingToAccountStats is enabled, use the request's actual
      client billing cost (before multiplier) as account_stats_cost, instead
      of recalculating from LiteLLM per-token prices which produced incorrect
      values for per-request billing mode.
      
      LiteLLM model pricing is now the final fallback (priority 3), used only
      when neither custom rules nor ApplyPricingToAccountStats apply.
      98c9d517
    • erio's avatar
      fix: add missing AccountQuotaNotifyEnabled to admin settings API · 42f8ef33
      erio authored
      The field was present in SystemSettings response DTO and service layer
      but missing from:
      - UpdateSettingsRequest (admin handler) - saves were silently ignored
      - GET/PUT response mapping in admin handler
      - UpdateSettingsRequest (non-admin dto)
      
      This caused the toggle to always revert to off after saving.
      42f8ef33
    • erio's avatar
      fix(frontend): hide quota notify toggle when global setting is disabled · 48e8efe3
      erio authored
      QuotaLimitCard now requires quotaNotifyGlobalEnabled prop to control
      visibility of QuotaNotifyToggle components. When the global account
      quota notification is disabled in admin settings, per-account threshold
      toggles are hidden in both Edit and Create account modals.
      48e8efe3
    • erio's avatar
      fix: round 3 audit fixes - SMTP header sanitization and goroutine safety · b1875f0b
      erio authored
      - Move sanitizeEmailHeader to SendEmailWithConfig entry point, covering all
        email senders (verify code, password reset, ops alerts, notifications)
      - Add panic recovery to UpdateBalance goroutine
      - Fix stale comment in getAccountQuotaNotifyEmails (email="" no longer used)
      - Log error instead of silently discarding verifyNotifyCode cache update failure
      b1875f0b
    • erio's avatar
      fix: audit fixes for websearch, notifications, and channel pricing · b7fb2e43
      erio authored
      P0: fix wildcard matching test assertion (config order, not longest prefix)
      P0: add TotalRecharged to auth cache snapshot (v5) for percentage threshold
      P1: move pricing rules into per-platform sections in ChannelsView
      P1: populate account name cache when editing existing channel rules
      P1: sanitize email subject headers to prevent SMTP injection
      P1: make Redis INCR+EXPIRE idempotent for rate limiting
      P1: deep copy FeaturesConfig in Channel.Clone()
      P2: clean up stale email="" placeholder comments
      P2: replace log.Printf with slog in email_service.go
      b7fb2e43
    • erio's avatar
      fix: address audit findings across websearch, notify, and channel pricing · a68df457
      erio authored
      Backend fixes:
      - Fix balance notify ignoring percentage threshold type (was treating
        percentage value as fixed USD amount)
      - Remove dead code parseJSONStringArray
      - Add ImageOutputTokens to tryModelFilePricing calculation
      - Unify zero-value check: cost == 0 → cost <= 0 in calculateTokenStatsCost
      - Use MarshalNotifyEmails instead of json.Marshal for consistency
      - Rename quotaDim.oldUsed → currentUsed for clarity
      - Extract HTML email templates to const variables (function ≤30 lines)
      
      Test fixes:
      - Rewrite account_websearch_test.go for GetWebSearchEmulationMode tri-state
      - Add 6 tryModelFilePricing test cases
      
      Frontend fixes:
      - Replace hardcoded '未命名' with i18n key
      - Extract getBillingModeLabel/getBillingModeBadgeClass to shared utils
      - Replace inline type with imported NotifyEmailEntry
      - Pass platform to AccountStats pricing rules via inferRulePlatform()
      - Add billing mode constants (BILLING_MODE_TOKEN/PER_REQUEST/IMAGE)
      a68df457
    • erio's avatar
      feat: WebSearch tri-state, account stats pricing fix, quota cache fix, usage tooltip · 1262654d
      erio authored
      WebSearch tri-state switch:
      - Account-level web_search_emulation changed from bool to tri-state
        string: "default" (follow channel) / "enabled" / "disabled"
      - shouldEmulateWebSearch checks channel config when account is "default"
      - SQL migration converts old bool values
      - Frontend select replaces toggle in Edit/CreateAccountModal
      
      Account stats pricing:
      - resolveAccountStatsCost uses upstream model (post-mapping) for matching
      - Priority: custom rules → model pricing file (when toggle on) → default
      - Custom rules always configurable, independent of toggle
      - Account ID field changed to searchable selector filtered by platform
      - Description updated to reflect new behavior
      
      Quota notification cache fix:
      - CheckAccountQuotaAfterIncrement fetches real-time account from DB
      - Reconstructs pre-increment usage for accurate threshold crossing detection
      - New AccountQuotaReader interface (minimal: GetByID only)
      
      Usage tooltip:
      - Per-request/image billing shows per-request price instead of $0 token price
      - Token billing continues to show input/output price per million tokens
      1262654d
    • erio's avatar
      fix(channel): use upstream model for account stats pricing and remove channel pricing fallback · 11c46068
      erio authored
      - resolveAccountStatsCost now uses the final upstream model (after
        account-level mapping) to match custom pricing rules, fixing the
        issue where requested model (e.g. claude-sonnet-4-5) didn't match
        rules configured for upstream model (e.g. claude-opus-4-6)
      - Remove tryChannelPricing fallback — only custom rules are applied,
        unmatched requests use default formula (total_cost × rate)
      - Remove unused billingService and serviceTier parameters
      - Update description: "启用后将支持自定义账号统计的模型价格"
      11c46068
    • erio's avatar
      fix(notify): add verification flow for saved unverified emails · 95f9b27e
      erio authored
      - Add "verify" button next to saved unverified emails in
        ProfileBalanceNotifyCard (send code → enter code → verify)
      - Backend: VerifyAndAddNotifyEmail now marks existing unverified
        emails as verified instead of returning "already exists"
      - Inline verification UI with countdown timer and resend button
      95f9b27e
    • erio's avatar
      fix(notify): use real-time balance for crossing detection and simplify email logic · 31550a2c
      erio authored
      - Fix cached balance causing threshold crossing to never trigger:
        read real-time balance from billingCacheService instead of stale
        API key auth snapshot
      - Remove email="" placeholder concept; all emails are user-managed
      - Only send notifications to verified && non-disabled emails
      - Frontend: pre-fill user's email in add input when list is empty
      - Remove FilterEnabledEmails/IsPrimaryDisabled helpers (no longer needed)
      31550a2c
    • erio's avatar
      feat(notify): convert email lists to NotifyEmailEntry struct with toggle support · 915b7a4a
      erio authored
      - Change balance_notify_extra_emails and account_quota_notify_emails
        from []string to []NotifyEmailEntry{email, disabled, verified}
      - Add per-email enable/disable toggle for both user and admin notifications
      - Add PUT /user/notify-email/toggle API endpoint
      - Fix critical bug: API key auth cache snapshot missing balance notify
        fields (Email, Username, BalanceNotifyEnabled, etc.), causing
        notifications to never fire on cached request paths
      - Bump cache snapshot version 3→4 to invalidate stale entries
      - Add SQL migration 104 to convert old format data
      - Backward compatible: parseNotifyEmails auto-detects old/new format
      - User balance notify: max 3 emails (primary + 2 extra)
      - Admin quota notify: unlimited emails, each with toggle
      915b7a4a
    • erio's avatar
      feat(notify): improve balance notify card UX · 81287e96
      erio authored
      - Show system default threshold as placeholder in custom threshold input
      - Display user's primary email with "Primary" badge
      - Support adding multiple pending emails before verification
      - Each pending email has independent send/verify/resend flow
      - Expose balance_low_notify_threshold in PublicSettings API
      - Clean up timers on unmount to prevent leaks
      81287e96
    • erio's avatar
      fix(notify): add balance/quota notify flags to PublicSettings DTO and handler · 79d154ed
      erio authored
      The service layer correctly populated BalanceLowNotifyEnabled and
      AccountQuotaNotifyEnabled in PublicSettings, but the handler-to-DTO
      mapping was missing. Users could not see the balance notify card because
      the public settings API never returned these flags.
      79d154ed
    • erio's avatar
      5df73099
    • erio's avatar
      fix: address audit findings for notify, websearch and security · 4e96a6fa
      erio authored
      - Fix GetByKeyForAuth missing user.FieldEmail and user.FieldUsername (notifications sent to empty address)
      - Guard against empty email in collectBalanceNotifyRecipients
      - Remove non-atomic TotalRecharged read-modify-write in admin balance adjustment
      - HTML-escape userName/siteName/accountName in notification email templates
      - Fix timer leak in ProfileBalanceNotifyCard (add onUnmounted cleanup)
      - Add warning log on websearch proxy URL resolution failure
      4e96a6fa
    • erio's avatar
      feat(notify): add global toggles, percentage threshold, and visibility control · eba289a7
      erio authored
      - Add global toggle for account quota notification in admin settings
      - Add percentage-based threshold type for per-account quota alerts
      - Hide balance notify card on user profile when global toggle is off
      - Expose balance_low_notify_enabled and account_quota_notify_enabled in PublicSettings
      - Add threshold type (fixed/percentage) to QuotaNotifyToggle with $ / % switcher
      eba289a7
    • erio's avatar
      fix(websearch): improve settings UI and hide config when globally disabled · 889b5b4f
      erio authored
      - API Key show/copy buttons moved inside input field (inline icons)
      - Proxy selector and test button on same row to save vertical space
      - Test opens a dialog modal instead of inline display
      - Hide all websearch config in channels/accounts when global toggle is off
      889b5b4f
    • erio's avatar
      fix(notify): remove percentage threshold from balance notification · cef22c70
      erio authored
      Balance low notification only supports fixed USD amount threshold.
      Percentage threshold is a quota concept, not applicable to balance.
      Reverted threshold_type from admin settings, user profile, and all
      backend/frontend layers. DB fields (balance_notify_threshold_type,
      total_recharged) retained for potential future quota use.
      cef22c70
    • erio's avatar
      fix: address audit findings for websearch and balance notification · 9e33d0c4
      erio authored
      - Fix GetByKeyForAuth not selecting balance notify fields (notifications
        never triggered in gateway path)
      - Fix provider-level ProxyURL never resolved: inject ProxyRepository into
        SettingService, resolve proxy URLs when building Manager
      - Fix admin manual balance adjustment not updating total_recharged
      - Add threshold_type input validation (reject invalid values)
      - Fix user threshold_type inheritance: custom threshold defaults to "fixed"
        instead of inheriting global type (prevents $5 being treated as 5%)
      - Add try-catch for clipboard.writeText (fails on non-HTTPS)
      - Add SetTotalRecharged to user Update for admin balance operations
      9e33d0c4
    • erio's avatar
      feat(notify): add percentage threshold type for balance low notification · f694afbb
      erio authored
      - Add threshold_type field (fixed/percentage) to system and user settings
      - Add total_recharged field to users table, auto-incremented on balance credit
      - Percentage mode: effective threshold = total_recharged × percentage / 100
      - User-level threshold_type inherits from system default when not set
      - Update admin settings UI with radio selector (fixed amount / percentage)
      - Migration: 102_add_balance_notify_threshold_type.sql
      f694afbb
    • erio's avatar
      feat(websearch): settings UI overhaul and quota improvements · d0674e0f
      erio authored
      - Remove Priority field, auto load-balance by quota remaining
      - Replace QuotaRefreshInterval (daily/weekly/monthly) with SubscribedAt
        (subscription date, monthly lazy refresh via Redis TTL)
      - Add collapsible provider cards, API key show/copy, usage progress bar
      - Add test endpoint (POST /web-search-emulation/test) bypassing quota
      - Wire WebSearchManagerBuilder on startup (was never called before)
      - Fix nextMonthlyReset day-of-month overflow (Jan 31 → Feb 28)
      - Fix non-deterministic sort in selectByQuotaWeight
      - Map ProxyID in builder for provider-level proxy tracking
      - Fix frontend timezone drift in subscribed_at date picker
      - Fix provider deletion index shift for expandedProviders state
      d0674e0f
    • erio's avatar
      fix(notify): per-recipient timeout and return user on email removal · 30b926ad
      erio authored
      - Use per-recipient context timeout in sendEmails to prevent later
        recipients from failing due to shared timeout exhaustion
      - Return updated user object from RemoveNotifyEmail handler for
        frontend state consistency (matching VerifyNotifyEmail pattern)
      30b926ad
    • erio's avatar
      fix(notify): address review findings - accountCost formula, dedup, refactor · c3812ce1
      erio authored
      - Fix accountCost calculation in finalizePostUsageBilling to match
        postUsageBilling (always multiply by AccountRateMultiplier)
      - Use strings.EqualFold for email dedup in collectBalanceNotifyRecipients
      - Extract CheckAccountQuotaAfterIncrement into smaller functions:
        buildQuotaDims + asyncSendQuotaAlert (< 30 lines each)
      - Add "not splittable" comments for HTML template functions
      - Extract QuotaNotifyToggle.vue sub-component to reduce
        QuotaLimitCard.vue from 404 to 339 lines
      c3812ce1
    • erio's avatar
      feat(notify): add balance low & account quota notification system · b32d1a2c
      erio authored
      - User balance low notification: email alert when balance drops below
        configurable threshold (user email + verified extra emails)
      - Account quota notification: broadcast email to admin-configured
        recipients when daily/weekly/total quota usage exceeds alert threshold
      - Admin settings: global enable/disable, default threshold, quota
        notification email list (Email Settings tab)
      - User profile: enable/disable, custom threshold, add/remove extra
        notification emails with verification code flow
      - Account quota: per-dimension alert toggle and threshold in quota
        control card
      - Trigger logic: first-crossing only (old >= threshold && new < threshold
        for balance; old < threshold && new >= threshold for quota), naturally
        prevents duplicate notifications without Redis dedup
      b32d1a2c
    • erio's avatar
      fix(websearch): improve isProxyError detection and add manager tests · 60b0fa81
      erio authored
      - Add TLS error detection to isProxyError (RecordHeaderError, handshake)
      - Case-insensitive error string matching
      - Add 19 unit tests for: isProviderAvailable, resolveProxyID,
        isProxyError, isProxyAvailable, selectByQuotaWeight, newHTTPClient
      60b0fa81
    • erio's avatar
      fix: gofmt websearch manager · 49915987
      erio authored
      49915987
    • erio's avatar
      feat(websearch): proxy failover, timeout, quota-weighted load balancing · fda61b06
      erio authored
      - Use proxyutil.ConfigureTransportProxy for unified proxy protocol support
        (HTTP/HTTPS/SOCKS5/SOCKS5H), replacing ad-hoc HTTP-only proxy code
      - Proxy errors return ErrProxyUnavailable → gateway triggers account switch
        via UpstreamFailoverError instead of fallback to direct connection
      - Timeout: proxy dial 3s, TLS handshake 3s, data transfer 60s
      - Mark proxy unavailable for 5 minutes in Redis on connectivity failure
      - Quota-weighted load balancing: providers with quota_limit>0 are selected
        by remaining quota (weighted random); quota_limit=0 providers treated as
        0% weight and placed last
      fda61b06
    • erio's avatar
      feat(channels): add custom account stats pricing rules · 7535e312
      erio authored
      Allow channels to configure independent model pricing for account
      statistics cost calculation, decoupled from user billing.
      
      Backend:
      - Migration 101: channels.apply_pricing_to_account_stats toggle,
        channel_account_stats_pricing_rules/model_pricing tables,
        usage_logs.account_stats_cost column
      - resolveAccountStatsCost: match rules by group/account, then channel
        pricing, fallback to original formula when unconfigured
      - Integrate into both GatewayService.recordUsageCore and
        OpenAIGatewayService.RecordUsage
      - Update 8 account stats SQL queries to use
        COALESCE(account_stats_cost, total_cost) * account_rate_multiplier
      - 23 unit tests for matching, pricing lookup, and cost calculation
      
      Frontend:
      - Channel edit dialog: toggle + custom rules UI with group/account
        multi-select and pricing entry cards
      - API types and i18n (zh/en)
      7535e312
    • erio's avatar
      fix(test): add web_search_emulation_enabled to API contract test · 7fad9f60
      erio authored
      The settings API response now includes the new field; update the
      expected snapshot in TestAPIContracts to match.
      7fad9f60
    • erio's avatar
      feat(gateway): add web search emulation for Anthropic API Key accounts · 1b53ffca
      erio authored
      Inject web search capability for Claude Console (API Key) accounts that
      don't natively support Anthropic's web_search tool. When a pure
      web_search request is detected, the gateway calls Brave Search or Tavily
      API directly and constructs an Anthropic-protocol-compliant SSE/JSON
      response without forwarding to upstream.
      
      Backend:
      - New `pkg/websearch/` SDK: Brave and Tavily provider implementations
        with io.LimitReader, proxy support, and Redis-based quota tracking
        (Lua atomic INCR + TTL, DECR rollback on failure)
      - Global config via `settings.web_search_emulation_config` (JSON) with
        in-process cache + singleflight, input validation, API key merge on
        save, and sanitized API responses
      - Channel-level toggle via `channels.features_config` JSONB column
        (DB migration 101)
      - Account-level toggle via `accounts.extra.web_search_emulation`
      - Request interception in `Forward()` with SSE streaming response
        construction using json.Marshal (no manual string concatenation)
      - Manager hot-reload: `RebuildWebSearchManager()` called on config save
        and startup via `SetWebSearchRedisClient()`
      - 70 unit tests covering providers, manager, config validation,
        sanitization, tool detection, query extraction, and response building
      
      Frontend:
      - Settings → Gateway tab: Web Search Emulation config card with global
        toggle, provider list (add/remove, API key, priority, quota, proxy)
      - Channels → Anthropic tab: web search emulation toggle with global
        state linkage (disabled when global off)
      - Account Create/Edit modals: web search emulation toggle for API Key
        type with Toggle component
      - Full i18n coverage (zh + en)
      1b53ffca