1. 14 Apr, 2026 15 commits
    • 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
    • erio's avatar
      fix(payment): critical audit fixes for security, idempotency and correctness · c738cfec
      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
      c738cfec
    • erio's avatar
      fix: audit fixes - magic strings to constants, frontend any/catch, LB tests · 56e4a9a9
      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
      56e4a9a9
    • erio's avatar
      test(payment): add unit tests for payment audit fixes + allow empty supported_types · 3c884f8e
      erio authored
      Tests (1033 new lines, 100% coverage on modified functions):
      - amount.go: YuanToFen/FenToYuan with precision edge cases
      - wxpay: mapWxState, wxSV, formatPEM, NewWxpay validation
      - alipay: isTradeNotExist, NewAlipay validation
      - webhook: writeSuccessResponse (wxpay JSON, stripe empty, others text)
      - config: validateProviderRequest, isSensitiveConfigField, joinTypes
      - fulfillment: resolveRedeemAction idempotency logic
      
      Business logic changes:
      - Allow empty supported_types on provider instances
      - Block removing payment types when instance has pending orders
      - Extract resolveRedeemAction as testable pure function
      3c884f8e
    • erio's avatar
      fix(payment): audit fixes for alipay/wxpay/stripe payment providers · 5bae3b05
      erio authored
      Backend:
      - Extract YuanToFen/FenToYuan to payment/amount.go using shopspring/decimal
      - Require alipay publicKey in config validation
      - Fix wxpay webhook response to return JSON per V3 spec
      - Remove wxpay certSerial fallback to publicKeyId
      - Define magic strings as named constants in wxpay/alipay providers
      - Add slog warning for wxpay H5→Native payment downgrade
      - Make EncryptionKey validation return error on invalid (non-empty) key
      - Make decryptConfig propagate errors instead of returning nil
      - Add idempotency check in doBalance to prevent stuck FAILED retries
      
      Frontend:
      - Fix dashboard currency symbol from $ to ¥
      - Fix AdminPaymentPlansView any type to proper SubscriptionPlan type
      - Make quick amount buttons follow selected payment method limits
      - Center help image with larger height and text below
      5bae3b05
    • erio's avatar
      fix(channel): add missing features column to List query · 1c63ea14
      erio authored
      The paginated List query was selecting 9 columns but scanning 10 fields,
      missing c.features. GetByID and ListAll already included it correctly.
      1c63ea14
    • erio's avatar
      fix: gofmt formatting after merge · 3d4d960d
      erio authored
      3d4d960d
    • erio's avatar
      refactor: remove PaymentChannel, reuse upstream Channel with features field · 794e8172
      erio authored
      - Delete payment_channels table and PaymentChannel Ent schema
      - Add `features` column to upstream channels table (migration 095)
      - Add Features field to Channel struct, input types, handler request/response
      - Payment user/admin handlers now use ChannelService directly
      - Remove Channel CRUD from PaymentConfigService and admin payment routes
      - Remove "渠道管理" tab from admin orders page (use /admin/channels)
      794e8172
    • erio's avatar
      fix: gofmt formatting · 37c23ecc
      erio authored
      37c23ecc
    • erio's avatar
      feat(channel): improve cache strategy and add restriction logging · e3748741
      erio authored
      - Change channel cache TTL from 60s to 10min (reduce unnecessary DB queries)
      - Actively rebuild cache after CRUD instead of lazy invalidation
      - Add slog.Warn logging for channel pricing restriction blocks (4 places)
      e3748741
    • erio's avatar
      fix: address review findings for channel restriction refactoring · 160903fc
      erio authored
      - Fix 7 stale comments still mentioning "限制检查" in handlers/services
      - Make billingModelForRestriction explicitly list channel_mapped case
      - Add slog.Warn for error swallowing in ResolveChannelMapping and
        needsUpstreamChannelRestrictionCheck
      - Document sticky session upstream check exemption
      160903fc
    • erio's avatar
      refactor: move channel model restriction from handler to scheduling phase · 2dce4306
      erio authored
      Move the model pricing restriction check from 8 handler entry points
      to the account scheduling phase (SelectAccountForModelWithExclusions /
      SelectAccountWithLoadAwareness), aligning restriction with billing:
      
      - requested: check original request model against pricing list
      - channel_mapped: check channel-mapped model against pricing list
      - upstream: per-account check using account-mapped model
      
      Handler layer now only resolves channel mapping (no restriction).
      Scheduling layer performs pre-check for requested/channel_mapped,
      and per-account filtering for upstream billing source.
      2dce4306
    • erio's avatar
      style: apply gofmt formatting · 1cd033e5
      erio authored
      
      Co-Authored-By: default avatarClaude Opus 4.6 <noreply@anthropic.com>
      1cd033e5
  2. 13 Apr, 2026 2 commits
    • sakurawztlt's avatar
      fix: Anthropic 非流式路径在上游终态事件 output 为空时从 delta 事件重建响应内容 · a1e299a3
      sakurawztlt authored
      b2e379cf 引入的 BufferedResponseAccumulator 已修复了 chat_completions
      非流式路径和 responses OAuth 非流式路径,但遗漏了 Anthropic /v1/messages
      非流式路径 (handleAnthropicBufferedStreamingResponse)。
      
      当客户端请求 stream=false 且模型开启思考时,上游 response.completed
      终态事件的 output 字段可能为空,实际 message 内容通过
      response.output_text.delta 增量事件下发。旧代码只读终态事件的 Response,
      导致客户端收到的 content 字段为空 ([{"type":"text"}])。
      
      本 commit 将 b2e379cf 的相同修复模式镜像到 Anthropic 路径:在 SSE 扫描
      过程中用 BufferedResponseAccumulator 累积 delta 内容,终态 output 为空
      时通过 SupplementResponseOutput 补充重建。
      
      同时修复 handleAnthropicBufferedStreamingResponse 遗漏 response.done
      事件类型的问题,与 chat completions 路径保持一致,避免上游发送
      response.done 时 handler 认不出终态事件、最终返回 502 的潜在问题。
      
      BufferedResponseAccumulator 已在 chatcompletions_responses_test.go 有
      完整单元测试覆盖(TextOnly/ToolCalls/Reasoning/Mixed/SupplementEmpty/
      NoSupplementWhenOutputExists/EmptyDeltas/IgnoresNonFunctionCallItems),
      本次复用相同累加器无需新增测试。
      a1e299a3
    • erio's avatar
      fix(payment): fix Alipay/Wxpay direct provider type mapping and enable... · f498eb8f
      erio authored
      fix(payment): fix Alipay/Wxpay direct provider type mapping and enable cross-provider load balancing
      
      Two issues fixed:
      
      1. Alipay.SupportedTypes() returned ["alipay_direct"] and Wxpay returned
         ["wxpay_direct"], but the frontend sends payment_type="alipay"/"wxpay".
         The registry lookup failed with "payment method (alipay) is not
         configured". Fix: return the base types ["alipay"]/["wxpay"].
      
      2. When multiple providers support the same payment type (e.g. EasyPay
         and Alipay direct both handle "alipay"), only the last-registered
         provider's instances were reachable — the registry mapped one type to
         one provider key, and SelectInstance queried by that single key.
      
         Fix: bypass the registry in invokeProvider and let SelectInstance
         query across all providers when providerKey is empty. The selected
         instance's own ProviderKey (now included in InstanceSelection) is
         used to create the correct provider, enabling true cross-provider
         load balancing.
      
      Closes #1592
      f498eb8f
  3. 12 Apr, 2026 1 commit
    • bot's avatar
      fix: handle Anthropic credit balance exhausted (400) as account error · cb016ad8
      bot authored
      When an Anthropic API key's credit balance is depleted, the upstream
      returns HTTP 400 with message containing "credit balance". Previously,
      the 400 handler only checked for "organization has been disabled",
      so credit-exhausted accounts kept being scheduled — every request
      returned the same error.
      
      Treat this case identically to 402 (Payment Required): call
      handleAuthError → SetError to stop scheduling the account until
      an admin manually recovers it after topping up credits.
      
      Closes #1586
      cb016ad8
  4. 11 Apr, 2026 5 commits
    • shuanbao0's avatar
      fix(gateway): 剥离 Cursor raw body 透传路径中 Codex 不支持的 Responses API 参数 · 422e25c9
      shuanbao0 authored
      
      
      在前一个 commit 的 isResponsesShape 短路路径基础上,补充对 Cursor 云端
      带过来的、Codex 上游统一不支持的顶层 Responses API 参数的剥离:
      
        - prompt_cache_retention
        - safety_identifier
        - metadata
        - stream_options
      
      根因补充:这条 raw-body 透传路径为了保留 Cursor 的 input 数组整体结构,
      不再经过 ChatCompletionsRequest 的反序列化过滤,所以这些 Go 结构体里
      没有对应字段的参数会被原样发到上游,上游返回:
          Unsupported parameter: <field>
      常规 Chat Completions 转换路径天然通过 ChatCompletionsRequest 丢弃未知字段,
      不受影响;此处仅在 isResponsesShape 分支内用 sjson.DeleteBytes 显式过滤,
      作用域最小。剥离列表与 openai_gateway_service.go:2034 的
      unsupportedFields 语义对齐。
      
      另外在 applyCodexOAuthTransform 的 OAuth 兜底 strip 列表里同步追加
      prompt_cache_retention,作为对该函数所有其他 OAuth 调用点的 defense
      in depth(当前只有 Cursor 路径的短路已在前面剥过,但保留这一层更稳)。
      
      测试:
      - TestCursorMixedShape_StripsUnsupportedFields — 验证所有 4 个字段都被剥
      - TestApplyCodexOAuthTransform_StripsPromptCacheRetention — OAuth 兜底路径
      Co-Authored-By: default avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
      422e25c9
    • shuanbao0's avatar
      fix(gateway): 兼容 Cursor /v1/chat/completions 的 Responses API body · b7edc3ed
      shuanbao0 authored
      
      
      Cursor 云端 (User-Agent: Go-http-client/2.0) 发往 /v1/chat/completions 的
      body 使用 Responses API 格式:
          {"model":"gpt-5.4","input":[{"role":"system","content":"..."}],"stream":true}
      
      原代码用 ChatCompletionsRequest 反序列化,该结构体没有 Input 字段,
      Cursor 的 input 数组被静默丢弃,ChatCompletionsToResponses 转换后产出
      input: null,Codex 上游以 "Invalid type for 'input': expected a string,
      but got an object" 拒绝请求(上游 typeof null === 'object')。
      
      修复:在 ForwardAsChatCompletions 里用 gjson 检测 body shape,当 input
      存在且 messages 缺失时,跳过 Chat→Responses 转换,用 sjson 仅改写 model
      字段后原样透传 body。billing 所需的 ServiceTier 和 Reasoning.Effort 通过
      gjson 从 raw body 提取,下游 codex OAuth transform 路径保持不变。
      
      测试:新增 openai_cursor_warmup_pipeline_test.go,覆盖 5 个 shape 检测
      用例(正向/标准请求不误伤/两字段共存/空 body/JSON 回读)。
      Co-Authored-By: default avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
      b7edc3ed
    • erio's avatar
      feat(payment): add H5/mobile payment support · 75155903
      erio authored
      Backend:
      - Parse EasyPay `payurl2` field, prefer H5 link on mobile
      - Add `device=mobile` to EasyPay submit.php (popup) mode
      - Expand isMobile detection keywords (add ipad/ipod)
      
      Frontend:
      - Add `isMobileDevice()` utility (userAgentData + UA regex)
      - Mobile + pay_url: direct redirect instead of QR/popup
      - Popup blocked fallback: auto-redirect when window.open fails
      - Stripe WeChat Pay: dynamic client param (mobile_web vs web)
      75155903
    • erio's avatar
      refactor(payment): code standards fixes and regression repairs · e3a000e0
      erio authored
      Backend:
      - Split payment_order.go (546→314 lines) into payment_order_lifecycle.go
      - Replace magic strings with constants in factory, easypay, webhook handler
      - Add rate limit/validity unit constants in payment_order_lifecycle, payment_service
      - Fix critical regression: add PaymentEnabled to GetPublicSettings response
      - Add missing migration 099_fix_migrated_purchase_menu_label_icon.sql
      
      Frontend:
      - Fix StripePopupView.vue: replace `as any` with typed interface, use extractApiErrorMessage
      - Fix AdminOrderTable.vue: replace hardcoded column labels with i18n t() calls
      - Fix SubscriptionsView.vue: replace hardcoded Today/Tomorrow with i18n
      - Extract duplicate statusBadgeClass/canRefund/formatOrderDateTime to orderUtils.ts
      - Add missing i18n keys: common.today, common.tomorrow, payment.orders.orderType/actions
      - Remove dead PurchaseSubscriptionView.vue (replaced by PaymentView)
      e3a000e0
    • erio's avatar
      feat(payment): add complete payment system with multi-provider support · 63d1860d
      erio authored
      Add a full payment and subscription system supporting EasyPay (Alipay/WeChat),
      Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
      63d1860d
  5. 10 Apr, 2026 4 commits
  6. 09 Apr, 2026 13 commits