- 14 Apr, 2026 40 commits
-
-
erio authored
Security (HIGH): - Normalize all Redis cache keys to lowercase (verifyCode, passwordReset) - Fix verify code TTL renewal on failed attempts: use remaining TTL via ExpiresAt field instead of resetting to full 15-minute window - Add 3 missing fields to diffSettings audit log (promo_code, invitation_code, custom_endpoints) Code quality (MEDIUM): - Extract filterVerifiedEmails shared helper (balance_notify_service.go) - Add Pricing array non-empty validation for channel pricing rules - Add platform token semantics comment in gateway_service.go - Complete validatePlanPatch test coverage (+10 test cases) - Replace string types with QuotaThresholdType/QuotaResetMode across frontend - Remove duplicate getPlatformTextColor/getRateBadgeClass in ChannelsView - Return EMAIL_NOT_FOUND error on RemoveNotifyEmail miss UI improvements: - Reorder cost tooltip: user billing above separator, account billing below - Add NaN guard to accountBilled function - Move timezone selector inline into reset-mode row (no longer standalone)
-
erio authored
- Fix websearch provider failover: proxy error from provider-specific proxy now continues to next provider instead of aborting the entire loop - Fix SMTP failure locking users out: send email first, then write cache and increment rate counter - Fix notify email cache key case sensitivity: normalize to lowercase - Add OriginalPrice validation to validatePlanPatch and validatePlanRequired - Add empty scope validation for channel pricing rules (group_ids/account_ids) - Add platform color to account search dropdown in channel pricing rules
-
erio authored
refactor: batch 3 — decompose CheckBalanceAfterDeduction, merge crossing checks, add QuotaNotifyConfig M1: CheckBalanceAfterDeduction (63→18 lines) decomposed into: canNotifyBalance, resolveUserEffectiveThreshold, crossedDownward, dispatchBalanceLowEmail M3: New Account.QuotaNotifyConfig(dim) method replaces 9 hardcoded getters (getters kept as thin wrappers for backward compatibility) M4: checkQuotaDimCrossings + checkQuotaDimCrossingsFromState merged into one function taking pre-built []quotaDim; caller builds dims conditionally -
erio authored
H5: diffSettings now tracks 5 balance/quota notify fields in audit log M15: log.Printf audit log migrated to slog.Info, removed "log" import M14: New frontend/src/constants/account.ts with shared constants QuotaNotifyToggle.vue uses QUOTA_THRESHOLD_TYPE_FIXED/PERCENTAGE L2: UsageTable.vue uses BILLING_MODE_TOKEN/IMAGE from billingMode.ts -
erio authored
fix: batch 1 audit fixes — quota SQL fixed mode, public recharge URL, WebSearch bool fallback, UpdatePlan validation H1: incrementUsageBillingAccountQuota now uses shared dailyExpiredExpr/weeklyExpiredExpr constants (supporting fixed reset mode) instead of hardcoded '24 hours'/'168 hours' H4: public settings endpoint now maps balance_low_notify_recharge_url H6: GetWebSearchEmulationMode tolerates legacy bool values (true→enabled) H7: UpdatePlan validates non-nil patch fields (rejects negative price, empty name, etc.) H8: UsageTable accountBilled() helper with total_cost ?? 0 null guard H9: AdminUsageLog TS type adds channel_id + billing_tier M2: account.go "fixed" literals replaced with thresholdTypeFixed constant M13: SystemSettings TS type adds web_search_emulation_enabled UI: QuotaLimitCard title labels now use flex-1 to align with flex-1 input boxes -
erio authored
balance_notify_service_test.go (27 tests): - resolveBalanceThreshold: fixed/percentage/zero recharged/empty type - quotaDim.resolvedThreshold: fixed normal/exceed/equal limit, percentage 0/30/100/>100, zero/negative limit - sanitizeEmailHeader: CRLF/CR/LF/clean/empty/multiple newlines - buildQuotaDims / buildQuotaDimsFromState: all dimensions, empty extra, state-vs-account precedence - collectBalanceNotifyRecipients: empty, filter disabled/unverified, case-insensitive dedup, skip empty, trim balance_notify_check_test.go (16 tests): - CheckBalanceAfterDeduction guard clauses: nil user/disabled/global-off/threshold=0/user-override/no-crossing - CheckAccountQuotaAfterIncrement guards: nil account/zero cost/negative cost/global-disabled - getBalanceNotifyConfig: all fields, disabled, invalid threshold - isAccountQuotaNotifyEnabled: missing/false/true - getSiteName: default fallback + configured balance_notify_email_body_test.go (10 tests): - Guards against fmt.Sprintf arg-count mismatches in email templates - Verifies HTML escaping of recharge URL - Verifies CSS %% escape produces literal % in output - Verifies unlimited/percentage/over-quota display branches payment_config_plans_validation_test.go (13 tests): - validatePlanRequired: all 5 validation branches + whitespace handling
-
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
-
erio authored
-
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
-
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
-
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.
-
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.
-
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
-
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
-
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)
-
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
-
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: "启用后将支持自定义账号统计的模型价格"
-
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
-
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)
-
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 -
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
-
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.
-
erio authored
-
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
-
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
-
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.
-
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
-
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
-
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
-
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)
-
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
-
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
-
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
-
erio authored
-
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
-
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)
-
erio authored
The settings API response now includes the new field; update the expected snapshot in TestAPIContracts to match.
-
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)
-
erio authored
Backend fixes: - #1: doSub subscription idempotency via audit log check - #2: markFailed only when status=RECHARGING (prevents overwriting COMPLETED) - #3: ExpireTimedOutOrders checks upstream payment before expiring - #4: Public verify endpoint for payment result page (no auth required) - #5: EasyPay QueryOrder returns amount, confirmPayment handles zero amount - #6: WxPay notifyUrl priority: request-first, config-fallback - #7: EasyPay remove double URL decode in VerifyNotification - #8: checkPaid/cancelUpstreamPayment use order's provider instance - #9: Amount NaN/Inf/negative validation in order creation and refund - #10: Refund amount comparison uses tolerance instead of float64 == - #11: Skip balance deduction on retry when previous rollback failed - #12: checkPaid logs fulfillment errors instead of silently ignoring - #13: WxPay certSerial added to required config fields Frontend fixes: - Payment result page no longer requires authentication - Public verify API fallback for expired sessions
-
erio authored
Backend: - Define OrderTypeBalance/Subscription, EntityStatusActive, DeductionType*, NotificationStatus* constants in payment/types.go - Replace all magic strings in payment_order, payment_fulfillment, payment_refund - Add local constants in easypay.go (tradeStatusSuccess, signTypeMD5) - Add 27 unit tests for load balancer (filterByLimits, pickLeastAmount, getInstanceChannelLimits, startOfDay) Frontend: - Remove all `any` types in SettingsView.vue (18 catch blocks + 1 payload) - Fix bare catch blocks in PaymentResultView, PaymentView - Add `unknown` type annotation to all catch blocks chore: bump version to 0.1.108.140
-