Skip to content

💱 feat(settings): introduce site-wide quota display type (USD/CNY/TOKENS/CUSTOM)#1923

Merged
Calcium-Ion merged 1 commit into
mainfrom
pr/custom-currency
Oct 3, 2025
Merged

💱 feat(settings): introduce site-wide quota display type (USD/CNY/TOKENS/CUSTOM)#1923
Calcium-Ion merged 1 commit into
mainfrom
pr/custom-currency

Conversation

@t0ng7u
Copy link
Copy Markdown
Collaborator

@t0ng7u t0ng7u commented Sep 29, 2025

Replace the legacy boolean “DisplayInCurrencyEnabled” with an injected, type-safe configuration general_setting.quota_display_type, and wire it through the backend and frontend.

Backend

  • Add QuotaDisplayType to operation_setting.GeneralSetting with injected registration via config.GlobalConfig.Register("general_setting", ...). Helpers: IsCurrencyDisplay(), IsCNYDisplay(), GetQuotaDisplayType().
  • Expose quota_display_type in /api/status and keep legacy display_in_currency for backward compatibility.
  • Logger: update LogQuota and FormatQuota to support USD/CNY/TOKENS. When CNY is selected, convert using operation_setting.USDExchangeRate.
  • Controllers:
    • billing: compute subscription/usage amounts based on the selected type (USD: divide by QuotaPerUnit; CNY: USD→CNY; TOKENS: keep raw tokens).
    • topup / topup_stripe: treat inputs as “amount” for USD/CNY and as token-count for TOKENS; adjust min topup and pay money accordingly.
    • misc: include quota_display_type in status payload.
  • Compatibility: in model/option.UpdateOption, map updates to DisplayInCurrencyEnabledgeneral_setting.quota_display_type (true→USD, false→TOKENS). Keep exporting the legacy key in OptionMap.

Frontend

  • Settings: replace the “display in currency” switch with a Select (general_setting.quota_display_type) offering USD / CNY / Tokens. Provide fallback mapping from legacy DisplayInCurrencyEnabled.
  • Persist quota_display_type to localStorage (keep display_in_currency for legacy components).
  • Rendering helpers: base all quota/price rendering on quota_display_type; use usd_exchange_rate for CNY symbol/values.
  • Pricing page: default view currency follows site display type (USD/CNY), while TOKENS mode still allows per-view currency toggling when needed.

Notes

  • No database migrations required.
  • Legacy clients remain functional via compatibility fields.

Summary by CodeRabbit

  • New Features

    • Added configurable quota display types: USD, CNY, TOKENS, and CUSTOM with custom symbol and exchange rate.
    • Updated status and storage to include quota display type and custom currency fields.
    • Enhanced logs and UI to reflect selected display type across subscriptions, usage, top-ups, and pricing.
    • Settings revamped with a combined rate control and currency selector; CUSTOM currency inputs supported.
    • Localization updated with new keys for custom currency and display settings.
  • Style

    • Extensive formatting, whitespace, and readability improvements across backend and frontend files.

…ENS/CUSTOM)

Replace the legacy boolean “DisplayInCurrencyEnabled” with an injected, type-safe
configuration `general_setting.quota_display_type`, and wire it through the
backend and frontend.

Backend
- Add `QuotaDisplayType` to `operation_setting.GeneralSetting` with injected
  registration via `config.GlobalConfig.Register("general_setting", ...)`.
  Helpers: `IsCurrencyDisplay()`, `IsCNYDisplay()`, `GetQuotaDisplayType()`.
- Expose `quota_display_type` in `/api/status` and keep legacy
  `display_in_currency` for backward compatibility.
- Logger: update `LogQuota` and `FormatQuota` to support USD/CNY/TOKENS. When
  CNY is selected, convert using `operation_setting.USDExchangeRate`.
- Controllers:
  - `billing`: compute subscription/usage amounts based on the selected type
    (USD: divide by `QuotaPerUnit`; CNY: USD→CNY; TOKENS: keep raw tokens).
  - `topup` / `topup_stripe`: treat inputs as “amount” for USD/CNY and as
    token-count for TOKENS; adjust min topup and pay money accordingly.
  - `misc`: include `quota_display_type` in status payload.
- Compatibility: in `model/option.UpdateOption`, map updates to
  `DisplayInCurrencyEnabled` → `general_setting.quota_display_type`
  (true→USD, false→TOKENS). Keep exporting the legacy key in `OptionMap`.

Frontend
- Settings: replace the “display in currency” switch with a Select
  (`general_setting.quota_display_type`) offering USD / CNY / Tokens.
  Provide fallback mapping from legacy `DisplayInCurrencyEnabled`.
- Persist `quota_display_type` to localStorage (keep `display_in_currency`
  for legacy components).
- Rendering helpers: base all quota/price rendering on `quota_display_type`;
  use `usd_exchange_rate` for CNY symbol/values.
- Pricing page: default view currency follows site display type (USD/CNY),
  while TOKENS mode still allows per-view currency toggling when needed.

Notes
- No database migrations required.
- Legacy clients remain functional via compatibility fields.
@t0ng7u t0ng7u requested a review from Calcium-Ion September 29, 2025 15:24
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 29, 2025

Walkthrough

Introduces a configurable quota display type (USD, CNY, TOKENS, CUSTOM) via operation_setting, replacing legacy DisplayInCurrencyEnabled across backend controllers, logging, and frontend rendering. Adds supporting fields and helpers, updates status payload, UI settings, and i18n. Numerous formatting-only changes elsewhere. Maintains compatibility by mapping old flags to new settings.

Changes

Cohort / File(s) Summary
Core quota display setting
setting/operation_setting/general_setting.go
Adds QuotaDisplayType constants (USD/CNY/TOKENS/CUSTOM), new fields on GeneralSetting, and helpers (IsCurrencyDisplay, IsCNYDisplay, GetQuotaDisplayType, GetCurrencySymbol, GetUsdToCurrencyRate). Initializes defaults.
Controllers and logger use quota display type
controller/billing.go, controller/misc.go, controller/topup.go, controller/topup_stripe.go, logger/logger.go
Replace currency flag logic with operation_setting-driven switch for amount/quota formatting. Extend status payload with quota_display_type and custom currency fields. Adjust top-up calculations for TOKENS mode. Logger formats USD/CNY/CUSTOM/TOKENS.
Compatibility and common
common/constants.go, model/option.go, common/database.go, controller/setup.go
Adds exported DisplayInCurrencyEnabled = true (compat comment). Maps legacy DisplayInCurrencyEnabled updates to general_setting.quota_display_type at runtime. Other files: trailing newline/formatting only.
Ollama relay robustness and formatting
relay/channel/ollama/adaptor.go, relay/channel/ollama/dto.go, relay/channel/ollama/relay-ollama.go, relay/channel/ollama/stream.go
Refactors for clearer checks, preallocation, safer parsing, improved stream handling, and consistent finalization. DTO/adaptor mostly formatting; relay/stream enhance validation, tool call handling, and usage reporting. No API changes.
Vertex channel formatting
relay/channel/vertex/adaptor.go
Indentation-only change in header setup; no functional impact.
Web helpers: quota rendering and pricing
web/src/helpers/render.jsx, web/src/helpers/data.js, web/src/helpers/utils.jsx, web/src/hooks/model-pricing/useModelPricingData.jsx
Switch rendering to quota_display_type; add CUSTOM currency support (symbol, exchange rate) with safe fallbacks; persist quota_display_type; compute display prices accordingly.
Settings UI for quota display
web/src/pages/Setting/Operation/SettingsGeneral.jsx, web/src/components/settings/OperationSetting.jsx
Introduces combined control for quota_display_type and rate; supports CUSTOM symbol/exchange-rate; backfills from legacy flag. Replaces boolean DisplayInCurrencyEnabled with string general_setting.quota_display_type.
Pricing UI options
web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx, web/src/components/table/model-pricing/layout/header/SearchActions.jsx
Adds CUSTOM currency option to selectors.
i18n updates
web/src/i18n/locales/en.json, web/src/i18n/locales/fr.json
Replace USD exchange-rate note with keys for site quota display and custom currency; fr.json gains many additional UI strings.
Web misc formatting/whitespace
web/index.html, web/jsconfig.json, web/src/helpers/api.js, web/src/components/common/modals/TwoFactorAuthModal.jsx, web/src/components/settings/SystemSetting.jsx, web/src/components/settings/personal/cards/AccountManagement.jsx, web/src/components/table/channels/modals/EditChannelModal.jsx, web/src/components/table/mj-logs/MjLogsFilters.jsx, web/src/components/table/task-logs/TaskLogsColumnDefs.jsx, web/src/components/table/task-logs/TaskLogsFilters.jsx, web/src/components/table/task-logs/modals/ContentModal.jsx, web/src/components/table/usage-logs/UsageLogsFilters.jsx, web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx, web/src/components/topup/modals/PaymentConfirmModal.jsx, web/src/constants/console.constants.js, web/src/hooks/channels/useChannelsData.jsx, web/src/hooks/common/useSidebar.js, web/src/pages/Setting/Operation/SettingsMonitoring.jsx, web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx, web/src/pages/Setting/Ratio/ModelRatioSettings.jsx
Formatting-only (indentation, line breaks, trailing commas, quote style, minor JSX reflow). No functional changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin
  participant Server as Server (operation_setting)
  participant Client as Web Client
  participant Storage as LocalStorage

  Admin->>Server: Update settings (quota_display_type, custom symbol/rate)
  Server-->>Admin: 200 OK

  Client->>Server: GET /status
  Server-->>Client: quota_display_type, quota_per_unit, usd_exchange_rate,<br/>custom_currency_symbol, custom_currency_exchange_rate
  Client->>Storage: Persist status (including quota_display_type)
  Client->>Client: Render quota/prices per display type (USD/CNY/TOKENS/CUSTOM)
Loading
sequenceDiagram
  autonumber
  participant User
  participant API as Controller (billing/topup)
  participant Ops as operation_setting
  participant Logger

  User->>API: GetUsage / GetSubscription / Topup
  API->>Ops: GetQuotaDisplayType()
  Ops-->>API: USD | CNY | TOKENS | CUSTOM
  API->>API: Compute amounts per type<br/>(divide by QuotaPerUnit, apply rates)
  API->>Logger: Log quota with formatted currency/tokens
  Logger-->>API: OK
  API-->>User: Response with computed amounts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • Calcium-Ion
  • seefs001

Poem

A rabbit taps the rate with glee,
From $ to ¥ to tokens free;
A custom coin? We’ll hop along—
Set the symbol, make it strong.
Settings saved, the numbers sing—
Thump, thump, thump: a pricing spring! 🐇💱

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly captures the core feature of introducing a site-wide quota display type with USD, CNY, TOKENS, and CUSTOM options. It aligns with the main change detailed in the PR objectives, focusing on replacing the legacy DisplayInCurrencyEnabled with a new type-safe configuration. The title is clear and specific, enabling team members to understand the PR’s purpose at a glance.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr/custom-currency

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
web/src/components/settings/SystemSetting.jsx (1)

975-983: Form field binding bug: use array literal for fields with dots

Form fields like "['oidc.enabled']" and "['oidc.well_known']" are passed as strings, not arrays. Semi UI Form expects field={['oidc.enabled']} (array literal) to correctly handle keys containing dots. As written, these fields won’t bind to inputs['oidc.*'] or setValues(newInputs). Fix all OIDC fields and the checkbox.

Apply representative fixes:

- <Form.Checkbox
-   field="['oidc.enabled']"
+ <Form.Checkbox
+   field={['oidc.enabled']}
    noLabel
    onChange={(e) => handleCheckboxChange('oidc.enabled', e)}
  >
- <Form.Input field="['oidc.well_known']" label={t('Well-Known URL')} />
+ <Form.Input field={['oidc.well_known']} label={t('Well-Known URL')} />
- <Form.Input field="['oidc.client_id']" label={t('Client ID')} />
+ <Form.Input field={['oidc.client_id']} label={t('Client ID')} />
- <Form.Input field="['oidc.client_secret']" label={t('Client Secret')} />
+ <Form.Input field={['oidc.client_secret']} label={t('Client Secret')} />
- <Form.Input field="['oidc.authorization_endpoint']" label={t('Authorization Endpoint')} />
+ <Form.Input field={['oidc.authorization_endpoint']} label={t('Authorization Endpoint')} />
- <Form.Input field="['oidc.token_endpoint']" label={t('Token Endpoint')} />
+ <Form.Input field={['oidc.token_endpoint']} label={t('Token Endpoint')} />
- <Form.Input field="['oidc.user_info_endpoint']" label={t('User Info Endpoint')} />
+ <Form.Input field={['oidc.user_info_endpoint']} label={t('User Info Endpoint')} />

Also applies to: 1125-1176

web/src/components/topup/RechargeCard.jsx (1)

223-228: Honor configured minTopUp in validation/reset (currently hardcoded to 1).

onChange/onBlur compare against 1 and reset to 1, which breaks flows when site min topup > 1 (USD/CNY) or large token minimums. Use minTopUp consistently for both checks and resets.

Apply this diff:

-                        onChange={async (value) => {
-                          if (value && value >= 1) {
-                            setTopUpCount(value);
-                            setSelectedPreset(null);
-                            await getAmount(value);
-                          }
-                        }}
+                        onChange={async (value) => {
+                          const v = Number(value) || 0;
+                          if (v >= Number(minTopUp || 1)) {
+                            setTopUpCount(v);
+                            setSelectedPreset(null);
+                            await getAmount(v);
+                          }
+                        }}
-                        onBlur={(e) => {
-                          const value = parseInt(e.target.value);
-                          if (!value || value < 1) {
-                            setTopUpCount(1);
-                            getAmount(1);
-                          }
-                        }}
+                        onBlur={(e) => {
+                          const v = parseInt(e.target.value, 10);
+                          const min = Number(minTopUp || 1);
+                          if (!v || v < min) {
+                            setTopUpCount(min);
+                            getAmount(min);
+                          }
+                        }}

Also applies to: 229-235

web/src/components/topup/index.jsx (4)

321-330: Guard against undefined/invalid minTopUp; coerce and fallback to 1

minTopUpValue can become undefined/0 if backend omits fields, leading to NaN in state and requests. Coerce to a positive number and default to 1.

-          const minTopUpValue = enableOnlineTopUp
-            ? data.min_topup
-            : enableStripeTopUp
-              ? data.stripe_min_topup
-              : 1;
+          const rawMin = enableOnlineTopUp
+            ? data.min_topup
+            : enableStripeTopUp
+              ? data.stripe_min_topup
+              : 1;
+          const numRawMin = Number(rawMin);
+          const minTopUpValue =
+            Number.isFinite(numRawMin) && numRawMin > 0 ? numRawMin : 1;

181-183: i18n consistency for error text

Wrap the string with t() like other instances.

-      showError('充值数量不能小于' + minTopUp);
+      showError(t('充值数量不能小于') + minTopUp);

444-446: Localize toast error prefix

Use t('错误') for consistency.

-          Toast.error({ content: '错误:' + data, id: 'getAmount' });
+          Toast.error({ content: `${t('错误')}:${data}`, id: 'getAmount' });

Apply the same change in getStripeAmount.

Also applies to: 470-472


426-428: Use centralized formatter and derive symbol from quota_display_type
Remove the hard-coded “元” suffix and call the shared helper that already reads quota_display_type from statusState.status. For example:

import { renderQuota } from 'web/src/helpers/render';

const renderAmount = () => renderQuota(amount);

This covers CNY, USD or TOKENS automatically.

web/src/components/topup/modals/PaymentConfirmModal.jsx (1)

96-111: Currency unit is hard-coded; not compatible with USD/TOKENS/CUSTOM

UI shows "元" regardless of quota_display_type. Use a formatter consistent with renderAmount() for original/discount values.

-                  <Text delete className='text-slate-500 dark:text-slate-400'>
-                    {`${originalAmount.toFixed(2)} ${t('元')}`}
-                  </Text>
+                  <Text delete className='text-slate-500 dark:text-slate-400'>
+                    {renderAmountValue
+                      ? renderAmountValue(originalAmount)
+                      : `${originalAmount.toFixed(2)}`}
+                  </Text>
...
-                  <Text className='text-emerald-600 dark:text-emerald-400'>
-                    {`- ${discountAmount.toFixed(2)} ${t('元')}`}
-                  </Text>
+                  <Text className='text-emerald-600 dark:text-emerald-400'>
+                    {renderAmountValue
+                      ? `- ${renderAmountValue(discountAmount)}`
+                      : `- ${discountAmount.toFixed(2)}`}
+                  </Text>

Add a new prop renderAmountValue: (n: number) => ReactNode and implement it with the same currency/token rules as renderAmount(). Based on PR objectives.

web/src/components/settings/personal/cards/AccountManagement.jsx (1)

431-438: React prop casing: use readOnly instead of readonly

Current prop won’t set the input to read-only in React.

-                            <Input
-                              readonly
+                            <Input
+                              readOnly
web/src/helpers/utils.jsx (1)

638-640: Pricing unit divisor bug for tokenUnit 'M'.

When tokenUnit is 'M', unitDivisor should be 1_000_000, not 1. Current code mislabels per‑M prices and inflates values 1e6×.

-    const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
-    const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
+    const unitDivisor =
+      tokenUnit === 'K' ? 1000 : tokenUnit === 'M' ? 1000000 : 1;
+    const unitLabel =
+      tokenUnit === 'K' ? 'K' : tokenUnit === 'M' ? 'M' : tokenUnit;
controller/topup.go (2)

119-123: Also convert min topup for CNY and CUSTOM display types.

Currently only TOKENS is handled. If MinTopUp is configured in USD, convert to display units for consistent validation.

 func getMinTopup() int64 {
   minTopup := operation_setting.MinTopUp
-  if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
-      dMinTopup := decimal.NewFromInt(int64(minTopup))
-      dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
-      minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
-  }
+  switch operation_setting.GetQuotaDisplayType() {
+  case operation_setting.QuotaDisplayTypeTokens:
+      dMinTopup := decimal.NewFromInt(int64(minTopup))
+      dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
+      minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
+  case operation_setting.QuotaDisplayTypeCNY:
+      dMinTopup := decimal.NewFromInt(int64(minTopup))
+      minTopup = int(dMinTopup.Mul(decimal.NewFromFloat(operation_setting.USDExchangeRate)).IntPart())
+  case operation_setting.QuotaDisplayTypeCustom:
+      rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate
+      if rate <= 0 { rate = 1 }
+      dMinTopup := decimal.NewFromInt(int64(minTopup))
+      minTopup = int(dMinTopup.Mul(decimal.NewFromFloat(rate)).IntPart())
+  }
   return int64(minTopup)
 }

179-184: Critical: Normalize stored TopUp.Amount to USD-equivalent for non-TOKENS display types.

EpayNotify later adds quota as Amount * QuotaPerUnit, assuming Amount is in USD. With CNY/CUSTOM, this over-credits users. Normalize Amount to USD here (like TOKENS path), while still charging payMoney in the actual payment currency.

-amount := req.Amount
-if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
-    dAmount := decimal.NewFromInt(int64(amount))
-    dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
-    amount = dAmount.Div(dQuotaPerUnit).IntPart()
-}
+amount := req.Amount
+switch operation_setting.GetQuotaDisplayType() {
+case operation_setting.QuotaDisplayTypeTokens:
+    {
+        dAmount := decimal.NewFromInt(int64(amount))
+        dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
+        amount = dAmount.Div(dQuotaPerUnit).IntPart() // tokens -> USD
+    }
+case operation_setting.QuotaDisplayTypeCNY:
+    {
+        dAmount := decimal.NewFromInt(int64(amount))
+        amount = dAmount.Div(decimal.NewFromFloat(operation_setting.USDExchangeRate)).IntPart() // CNY -> USD
+    }
+case operation_setting.QuotaDisplayTypeCustom:
+    {
+        rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate
+        if rate <= 0 { rate = 1 }
+        dAmount := decimal.NewFromInt(int64(amount))
+        amount = dAmount.Div(decimal.NewFromFloat(rate)).IntPart() // CUSTOM -> USD
+    }
+}
🧹 Nitpick comments (39)
web/src/components/common/modals/TwoFactorAuthModal.jsx (1)

138-140: No functional change; consider minor i18n/compat polish.

  • Keep as is if preferred. Optionally:
    • Drop the trailing comma in the function call to avoid edge tooling compatibility issues.
    • Move the long literal into a keyed translation for maintainability.
-            {t(
-              '支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。',
-            )}
+            {t('支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。')}

Or (keyed, with default value to keep behavior):

-            {t('支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。')}
+            {t('twoFactor.helpText', {
+              defaultValue:
+                '支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。',
+            })}

Based on learnings

web/src/components/settings/SystemSetting.jsx (1)

695-707: Unify save semantics for SSRF toggles; include them in submitSSRF payload

These three checkboxes save immediately via handleCheckboxChange, while the rest of the SSRF config saves via the “更新SSRF防护设置” button. This inconsistency can confuse users and produce partial updates if any request fails. Include the toggles in submitSSRF so the button persists the full SSRF config in one go (you can keep the immediate save if desired).

Apply this minimal change to submitSSRF:

   const submitSSRF = async () => {
     const options = [];
 
+    // 开关项一起纳入一次性保存
+    options.push({
+      key: 'fetch_setting.enable_ssrf_protection',
+      value: inputs['fetch_setting.enable_ssrf_protection'],
+    });
+    options.push({
+      key: 'fetch_setting.allow_private_ip',
+      value: inputs['fetch_setting.allow_private_ip'],
+    });
+    options.push({
+      key: 'fetch_setting.apply_ip_filter_for_domain',
+      value: inputs['fetch_setting.apply_ip_filter_for_domain'],
+    });
+
     // 处理域名过滤模式与列表
     options.push({
       key: 'fetch_setting.domain_filter_mode',
       value: domainFilterMode,
     });

Also applies to: 710-731, 739-747

web/src/components/table/channels/modals/EditChannelModal.jsx (2)

335-339: Avoid triple-sourcing the default Ark base_url; rely on a single source of truth.

base_url for type 45 is set in handleInputChange, loadChannel, and via Select defaultValue. This can drift. Prefer form state as the single source; drop the Select’s defaultValue and keep the programmatic defaults (on type change and on load).

Apply this diff to remove the redundant defaultValue:

-                          defaultValue='https://ark.cn-beijing.volces.com'

Also applies to: 447-453, 1941-1961


1949-1958: Confirm Ark endpoints and link official docs
These two entries match the official VolcEngine Ark host (华北2: ark.cn-beijing.volces.com) and BytePlus ModelArk host (ap-southeast: ark.ap-southeast.bytepluses.com). Additional region-specific endpoints exist (e.g. ark..byteplusapi.com for signed API calls). Please link to the Ark/ModelArk documentation near this Select or include other regions as needed.

web/src/pages/Setting/Ratio/ModelRatioSettings.jsx (3)

161-161: Use functional setState in onChange to avoid stale closures.

Current pattern spreads from a possibly stale inputs reference. Prefer functional updates for reliability during rapid edits.

Apply this diff:

-              onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
+              onChange={(value) => setInputs((prev) => ({ ...prev, ModelPrice: value }))}

-              onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
+              onChange={(value) => setInputs((prev) => ({ ...prev, ModelRatio: value }))}

-              onChange={(value) => setInputs({ ...inputs, CacheRatio: value })}
+              onChange={(value) => setInputs((prev) => ({ ...prev, CacheRatio: value }))}

-              onChange={(value) =>
-                setInputs({ ...inputs, CompletionRatio: value })
-              }
+              onChange={(value) =>
+                setInputs((prev) => ({ ...prev, CompletionRatio: value }))
+              }

-              onChange={(value) => setInputs({ ...inputs, ImageRatio: value })}
+              onChange={(value) => setInputs((prev) => ({ ...prev, ImageRatio: value }))}

-              onChange={(value) => setInputs({ ...inputs, AudioRatio: value })}
+              onChange={(value) => setInputs((prev) => ({ ...prev, AudioRatio: value }))}

-              onChange={(value) =>
-                setInputs({ ...inputs, AudioCompletionRatio: value })
-              }
+              onChange={(value) =>
+                setInputs((prev) => ({ ...prev, AudioCompletionRatio: value }))
+              }

Also applies to: 180-180, 199-199, 220-221, 245-245, 267-267, 291-293


156-160: Localize rule error messages.

messages are raw strings; wrap with t(...) for consistency with the rest of the page.

-                  message: '不是合法的 JSON 字符串',
+                  message: t('不是合法的 JSON 字符串'),

Also applies to: 174-179, 193-198, 213-218, 239-244, 261-266, 285-290


147-151: Clarify currency in ModelPrice to match new quota_display_type.

Label/extraText say “刀”(USD). If ModelPrice is always USD regardless of display type, append “(USD)” to avoid confusion; otherwise, make it dynamic with the site-wide quota_display_type.

web/src/components/topup/RechargeCard.jsx (3)

315-318: Show min-topup with proper units/symbol in tooltip.

Display formatted amount (USD/CNY/Tokens) instead of a bare number for clarity.

-                                    t('此支付方式最低充值金额为') +
-                                    ' ' +
-                                    minTopupVal
+                                    `${t('此支付方式最低充值金额为')} ${renderQuotaWithAmount(minTopupVal)}`

279-281: Avoid duplicate React keys; key the array item wrapper only.

Button already sits under a keyed Tooltip/Fragment; remove the inner key on Button to reduce noise.

-                              const buttonEl = (
-                                <Button
-                                  key={payMethod.type}
+                              const buttonEl = (
+                                <Button
                                   theme='outline'
                                   type='tertiary'

Also applies to: 311-327


383-391: Discount badge i18n logic is brittle; prefer dedicated keys.

Using t('折').includes('off') to branch locales is fragile. Add distinct translation keys like discount.percentOff and discount.rate to render locale-appropriate formats without string checks. I can draft the i18n entries and render helper if helpful.

web/src/components/topup/index.jsx (4)

492-500: Avoid client-side price math; use backend as source of truth

Local priceRatio * discount can drift from server logic/rounding. Reuse /api/user/amount to compute final display.

   const selectPresetAmount = (preset) => {
     setTopUpCount(preset.value);
     setSelectedPreset(preset.value);
-
-    // 计算实际支付金额,考虑折扣
-    const discount = preset.discount || topupInfo.discount[preset.value] || 1.0;
-    const discountedAmount = preset.value * priceRatio * discount;
-    setAmount(discountedAmount);
+    // 统一由后端计算金额,避免前后端不一致
+    getAmount(preset.value);
   };

281-296: Normalize per-method min_topup with sensible fallback

Defaulting invalid values to 0 is risky. Fall back to the global min (or Stripe’s when applicable).

-              const normalizedMinTopup = Number(method.min_topup);
-              method.min_topup = Number.isFinite(normalizedMinTopup)
-                ? normalizedMinTopup
-                : 0;
+              const normalizedMinTopup = Number(method.min_topup);
+              const globalMin =
+                Number.isFinite(Number(data.min_topup)) && Number(data.min_topup) > 0
+                  ? Number(data.min_topup)
+                  : 1;
+              method.min_topup =
+                Number.isFinite(normalizedMinTopup) && normalizedMinTopup > 0
+                  ? normalizedMinTopup
+                  : globalMin;

156-160: Validate against method-specific minimums when available

Compare topUpCount with the selected payment method’s own min_topup instead of a single global min.

-      if (topUpCount < minTopUp) {
-        showError(t('充值数量不能小于') + minTopUp);
+      const methodMin =
+        (payMethods.find((m) => m.type === payment)?.min_topup) ?? minTopUp;
+      if (topUpCount < methodMin) {
+        showError(t('充值数量不能小于') + methodMin);
         return;
       }

436-454: Symmetric loading handling

Use finally like getStripeAmount to guarantee setAmountLoading(false) on early exits.

-    try {
+    try {
       const res = await API.post('/api/user/amount', {
         amount: parseFloat(value),
       });
       ...
-    } catch (err) {
-      console.log(err);
-    }
-    setAmountLoading(false);
+    } catch (err) {
+      console.log(err);
+    } finally {
+      setAmountLoading(false);
+    }
relay/channel/ollama/dto.go (1)

28-32: Consider json.RawMessage for tool call arguments

If upstream sends nested JSON, json.RawMessage avoids unnecessary decoding/encoding and preserves original payload.

 type OllamaToolCall struct {
   Function struct {
     Name      string      `json:"name"`
-    Arguments interface{} `json:"arguments"`
+    Arguments json.RawMessage `json:"arguments"`
   } `json:"function"`
 }
web/src/components/table/mj-logs/MjLogsFilters.jsx (1)

59-63: Don’t eagerly invoke preset functions; pass them through

Semi UI presets typically accept functions, evaluated when the user selects a preset. Passing values changes timing (“今天” computed at render) and may break the expected API.

-              presets={DATE_RANGE_PRESETS.map((preset) => ({
+              presets={DATE_RANGE_PRESETS.map((preset) => ({
                 text: t(preset.text),
-                start: preset.start(),
-                end: preset.end(),
+                start: preset.start,
+                end: preset.end,
               }))}

If the current Semi UI version supports direct Date values, keeping functions still ensures “fresh now” evaluation and aligns with the shared constants.

relay/channel/ollama/adaptor.go (1)

21-23: Explicitly mark unsupported paths to fail fast and surface clear 4xx

These return generic errors. If these code paths can be reached, consider returning typed upstream-style errors (or pre-checking and short‑circuiting earlier) so clients get a consistent 4xx and logs carry a clear reason. At minimum, add unit tests asserting these routes are rejected.

Also applies to: 38-40, 42-44, 84-86

web/src/components/table/task-logs/modals/ContentModal.jsx (1)

135-141: Harden video privacy and mobile playback

Add referrerPolicy and playsInline to reduce referer leakage and prevent forced fullscreen on iOS. Optional: mute to improve autoplay reliability.

-        <video
+        <video
           src={modalContent}
           controls
           style={{ width: '100%' }}
           autoPlay
           crossOrigin='anonymous'
+          referrerPolicy='no-referrer'
+          playsInline
+          // muted
           onError={handleVideoError}
           onLoadedData={handleVideoLoaded}
           onLoadStart={() => setIsLoading(true)}
         />

Also consider opening external links with noopener to prevent tab‑nabbing in handleOpenInNewTab:

// outside this hunk
const handleOpenInNewTab = () => {
  const win = window.open(modalContent, '_blank', 'noopener');
  if (win) win.opener = null;
};
web/src/hooks/channels/useChannelsData.jsx (2)

369-373: Use URLSearchParams to correctly encode query and simplify construction

String concatenation risks malformed URLs on special characters. Prefer URLSearchParams.

-      const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : '';
-      const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
-      const res = await API.get(
-        `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`,
-      );
+      const params = new URLSearchParams({
+        keyword: searchKeyword,
+        group: searchGroup,
+        model: searchModel,
+        id_sort: String(sortFlag),
+        tag_mode: String(enableTagMode),
+        p: String(page),
+        page_size: String(pageSz),
+      });
+      if (typeKey !== 'all') params.append('type', String(typeKey));
+      if (statusF !== 'all') params.append('status', String(statusF));
+      const res = await API.get(`/api/channel/search?${params.toString()}`);

317-321: Apply the same URLSearchParams pattern here for consistency

Keeps encoding correct and avoids repeated string templates.

-    const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : '';
-    const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
-    const res = await API.get(
-      `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`,
-    );
+    const params = new URLSearchParams({
+      p: String(page),
+      page_size: String(pageSize),
+      id_sort: String(idSort),
+      tag_mode: String(enableTagMode),
+    });
+    if (typeKey !== 'all') params.append('type', String(typeKey));
+    if (statusF !== 'all') params.append('status', String(statusF));
+    const res = await API.get(`/api/channel/?${params.toString()}`);
relay/channel/ollama/relay-ollama.go (2)

133-145: Don’t ignore tool argument JSON errors

Swallowing unmarshal errors silently can hide bad client input. Defaulting is fine, but log or at least branch on error.

-          var args interface{}
-          if tc.Function.Arguments != "" {
-            _ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
-          }
-          if args == nil {
-            args = map[string]any{}
-          }
+          var args interface{}
+          if tc.Function.Arguments != "" {
+            if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+              // optionally: common.SysError("invalid tool args: " + err.Error())
+              args = map[string]any{}
+            }
+          }
+          if args == nil {
+            args = map[string]any{}
+          }

237-254: Avoid duplicating Dimensions in both options and top‑level field

OllamaEmbeddingRequest already has Dimensions; sending another “dimensions” inside Options may be redundant/confusing.

-  if r.Dimensions != 0 {
-    opts["dimensions"] = r.Dimensions
-  }
relay/channel/ollama/stream.go (2)

146-148: Populate InputTokens/OutputTokens for consistency.

Not mandatory, but helps downstream metrics stay consistent.

 		usage.PromptTokens = chunk.PromptEvalCount
 		usage.CompletionTokens = chunk.EvalCount
 		usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
+		usage.InputTokens = usage.PromptTokens
+		usage.OutputTokens = usage.CompletionTokens

245-246: Optional: also fill InputTokens/OutputTokens in non‑stream.

-	usage := &dto.Usage{PromptTokens: lastChunk.PromptEvalCount, CompletionTokens: lastChunk.EvalCount, TotalTokens: lastChunk.PromptEvalCount + lastChunk.EvalCount}
+	usage := &dto.Usage{
+		PromptTokens:     lastChunk.PromptEvalCount,
+		CompletionTokens: lastChunk.EvalCount,
+		TotalTokens:      lastChunk.PromptEvalCount + lastChunk.EvalCount,
+		InputTokens:      lastChunk.PromptEvalCount,
+		OutputTokens:     lastChunk.EvalCount,
+	}
web/src/components/table/model-pricing/layout/header/SearchActions.jsx (1)

108-111: Use shared constants and standardize currency labels

  • Replace inline currency values with existing shared constants (USD, CNY, CUSTOM).
  • Localize USD/CNY labels to match other views (e.g., “USD ($)” / “CNY (¥)”); the custom-currency key (“自定义货币”) is already defined in locales.
web/src/components/topup/modals/PaymentConfirmModal.jsx (1)

43-46: Minor robustness: coerce numeric inputs before computing discounts

Ensure amountNumber is numeric to avoid surprises when a string is passed.

-  const hasDiscount =
-    discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0;
-  const originalAmount = hasDiscount ? amountNumber / discountRate : 0;
-  const discountAmount = hasDiscount ? originalAmount - amountNumber : 0;
+  const amt = Number(amountNumber) || 0;
+  const rate = Number(discountRate);
+  const hasDiscount = rate > 0 && rate < 1 && amt > 0;
+  const originalAmount = hasDiscount ? amt / rate : 0;
+  const discountAmount = hasDiscount ? originalAmount - amt : 0;
controller/billing.go (1)

25-27: Do not overwrite errors from sequential calls.

err is assigned twice; a failure in GetUserQuota can be masked by GetUserUsedQuota success. Check errors after each call.

- remainQuota, err = model.GetUserQuota(userId, false)
- usedQuota, err = model.GetUserUsedQuota(userId)
+ remainQuota, err = model.GetUserQuota(userId, false)
+ if err != nil { /* handle & return */ }
+ usedQuota, err = model.GetUserUsedQuota(userId)
web/src/helpers/utils.jsx (1)

649-664: LocalStorage symbol fetch is fine; consider centralizing currency symbol logic.

This duplicates symbol resolution done elsewhere (e.g., render.jsx). Extract a shared helper (e.g., getSiteCurrencySymbol()) to avoid drift and simplify tests.

web/src/hooks/model-pricing/useModelPricingData.jsx (1)

67-75: Clamp invalid custom exchange rates.

Defensive clamp avoids negative/zero custom rates propagating to UI.

-  const customExchangeRate = useMemo(
-    () => statusState?.status?.custom_currency_exchange_rate ?? 1,
-    [statusState],
-  );
+  const customExchangeRate = useMemo(() => {
+    const r = statusState?.status?.custom_currency_exchange_rate;
+    return r && r > 0 ? r : 1;
+  }, [statusState]);
logger/logger.go (1)

122-145: Minor: Prefer consistent rate sources.

CNY uses USDExchangeRate, CUSTOM reads from GeneralSetting. If USDExchangeRate also moves into GeneralSetting, unify reads to a single source for consistency.

web/src/pages/Setting/Operation/SettingsGeneral.jsx (4)

104-115: Tighten memo deps and sanitize numeric updates

  • Narrow useMemo deps to avoid needless recompute.
  • Ensure numeric-only input is saved for rate fields; otherwise empty/invalid strings can be posted.

Apply:

-  }, [inputs]);
+  }, [
+    inputs['general_setting.quota_display_type'],
+    inputs['USDExchangeRate'],
+    inputs['QuotaPerUnit'],
+    inputs['general_setting.custom_currency_exchange_rate'],
+  ]);

For updates, consider coercing to trimmed numeric strings and rejecting non-finite values before set/submit. I can draft a small validator if you want.

Also applies to: 117-126


204-231: Use number-friendly input for rates; prevent invalid entries

The rate field accepts free text. Prefer numeric constraints to reduce backend parse errors and UX surprises.

Apply:

-                    <Input
+                    <Input
+                      type='number'
+                      inputMode='decimal'
+                      min={0}
+                      step='any'
                       prefix={'1 USD = '}
                       style={{ width: '50%' }}
                       value={combinedRate}
                       onChange={onCombinedRateChange}
                       disabled={
                         inputs['general_setting.quota_display_type'] === 'USD'
                       }
                     />

This keeps your current state wiring while constraining input.


45-46: Remove dead modal wiring

showQuotaWarning is never set true; the Modal is unreachable. Remove state and Modal to reduce noise.

-  const [showQuotaWarning, setShowQuotaWarning] = useState(false);
+  // removed: unused showQuotaWarning state
-      <Modal
-        title={t('警告')}
-        visible={showQuotaWarning}
-        onOk={() => setShowQuotaWarning(false)}
-        onCancel={() => setShowQuotaWarning(false)}
-        closeOnEsc={true}
-        width={500}
-      >
-        <Banner
-          type='warning'
-          description={t(
-            '此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。',
-          )}
-          bordered
-          fullMode={false}
-          closeIcon={null}
-        />
-      </Modal>
+      {/* removed unused Modal for quota warning */}

Also applies to: 299-317


69-102: Validate numeric fields on submit

If a user clears the combined rate, we may PUT empty strings for USDExchangeRate/QuotaPerUnit/custom rate. Add a simple guard (reject non-finite, <=0 for rates where applicable) before building requestQueue. I can provide a small helper if desired.

controller/misc.go (1)

65-71: Minor: cache general setting locally to reduce repeated calls

You can avoid multiple GetGeneralSetting() calls and make intent clearer:

gs := operation_setting.GetGeneralSetting()
// ...
"docs_link": gs.DocsLink,
// ...
"custom_currency_symbol":        gs.CustomCurrencySymbol,
"custom_currency_exchange_rate": gs.CustomCurrencyExchangeRate,

Optional, no behavior change.

Also applies to: 84-86

web/src/helpers/render.jsx (4)

829-852: Ensure consistent return type and safe number coercion

Early return 0 mixes types (number vs string). Coerce then format, defaulting invalid to 0.

-export function renderQuotaNumberWithDigit(num, digits = 2) {
-  if (typeof num !== 'number' || isNaN(num)) {
-    return 0;
-  }
-  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
-  num = num.toFixed(digits);
+export function renderQuotaNumberWithDigit(num, digits = 2) {
+  num = Number(num);
+  if (!Number.isFinite(num)) num = 0;
+  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
+  num = num.toFixed(digits);
   if (quotaDisplayType === 'CNY') {
     return '¥' + num;
   } else if (quotaDisplayType === 'USD') {
     return '$' + num;
   } else if (quotaDisplayType === 'CUSTOM') {
     const statusStr = localStorage.getItem('status');
     let symbol = '¤';
     try {
       if (statusStr) {
         const s = JSON.parse(statusStr);
         symbol = s?.custom_currency_symbol || symbol;
       }
     } catch (e) {}
     return symbol + num;
   } else {
     return num;
   }
 }

902-921: Format amount with fixed digits for currency

Currently returns raw amount (potentially long floats). Format to 2 decimals.

-export function renderQuotaWithAmount(amount) {
+export function renderQuotaWithAmount(amount, digits = 2) {
   const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
+  const formatted = Number(amount);
+  const num = Number.isFinite(formatted) ? formatted.toFixed(digits) : '0.00';
   if (quotaDisplayType === 'TOKENS') {
-    return renderNumber(renderUnitWithQuota(amount));
+    return renderNumber(renderUnitWithQuota(formatted || 0));
   }
   if (quotaDisplayType === 'CNY') {
-    return '¥' + amount;
+    return '¥' + num;
   } else if (quotaDisplayType === 'CUSTOM') {
     const statusStr = localStorage.getItem('status');
     let symbol = '¤';
     try {
       if (statusStr) {
         const s = JSON.parse(statusStr);
         symbol = s?.custom_currency_symbol || symbol;
       }
     } catch (e) {}
-    return symbol + amount;
+    return symbol + num;
   }
-  return '$' + amount;
+  return '$' + num;
 }

923-964: Guard missing quota_per_unit and parse rates as numbers

Avoid NaN results and string-math quirks.

 export function renderQuota(quota, digits = 2) {
-  let quotaPerUnit = localStorage.getItem('quota_per_unit');
-  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
-  quotaPerUnit = parseFloat(quotaPerUnit);
+  let quotaPerUnit = parseFloat(localStorage.getItem('quota_per_unit'));
+  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
+  if (!Number.isFinite(quotaPerUnit) || quotaPerUnit <= 0) {
+    // Fallback to sane default to keep UI stable
+    quotaPerUnit = 500000;
+  }
   if (quotaDisplayType === 'TOKENS') {
     return renderNumber(quota);
   }
   const resultUSD = quota / quotaPerUnit;
   let symbol = '$';
   let value = resultUSD;
   if (quotaDisplayType === 'CNY') {
     const statusStr = localStorage.getItem('status');
-    let usdRate = 1;
+    let usdRate = 1;
     try {
       if (statusStr) {
         const s = JSON.parse(statusStr);
-        usdRate = s?.usd_exchange_rate || 1;
+        usdRate = Number(s?.usd_exchange_rate) || 1;
       }
     } catch (e) {}
     value = resultUSD * usdRate;
     symbol = '¥';
   } else if (quotaDisplayType === 'CUSTOM') {
     const statusStr = localStorage.getItem('status');
     let symbolCustom = '¤';
-    let rate = 1;
+    let rate = 1;
     try {
       if (statusStr) {
         const s = JSON.parse(statusStr);
         symbolCustom = s?.custom_currency_symbol || symbolCustom;
-        rate = s?.custom_currency_exchange_rate || rate;
+        rate = Number(s?.custom_currency_exchange_rate) || rate;
       }
     } catch (e) {}
     value = resultUSD * rate;
     symbol = symbolCustom;
   }
   const fixedResult = value.toFixed(digits);
   if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) {
     const minValue = Math.pow(10, -digits);
     return symbol + minValue.toFixed(digits);
   }
   return symbol + fixedResult;
 }

833-852: Reduce repetition: centralize status reads

status parsing is repeated across helpers. Consider a tiny util:

// near top of file
function getStatusLS() {
  try { return JSON.parse(localStorage.getItem('status') || '{}'); } catch { return {}; }
}

Then use const s = getStatusLS() where needed. Optional.

Also applies to: 902-921, 923-964, 1559-1565

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb4b64 and 39a868f.

📒 Files selected for processing (46)
  • common/constants.go (1 hunks)
  • common/database.go (1 hunks)
  • controller/billing.go (3 hunks)
  • controller/misc.go (1 hunks)
  • controller/setup.go (1 hunks)
  • controller/topup.go (3 hunks)
  • controller/topup_stripe.go (2 hunks)
  • logger/logger.go (2 hunks)
  • model/option.go (1 hunks)
  • relay/channel/ollama/adaptor.go (4 hunks)
  • relay/channel/ollama/dto.go (3 hunks)
  • relay/channel/ollama/relay-ollama.go (4 hunks)
  • relay/channel/ollama/stream.go (1 hunks)
  • relay/channel/vertex/adaptor.go (1 hunks)
  • setting/operation_setting/general_setting.go (2 hunks)
  • web/index.html (1 hunks)
  • web/jsconfig.json (1 hunks)
  • web/src/components/common/modals/TwoFactorAuthModal.jsx (1 hunks)
  • web/src/components/settings/OperationSetting.jsx (1 hunks)
  • web/src/components/settings/SystemSetting.jsx (9 hunks)
  • web/src/components/settings/personal/cards/AccountManagement.jsx (3 hunks)
  • web/src/components/table/channels/modals/EditChannelModal.jsx (7 hunks)
  • web/src/components/table/mj-logs/MjLogsFilters.jsx (1 hunks)
  • web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx (1 hunks)
  • web/src/components/table/model-pricing/layout/header/SearchActions.jsx (1 hunks)
  • web/src/components/table/task-logs/TaskLogsColumnDefs.jsx (1 hunks)
  • web/src/components/table/task-logs/TaskLogsFilters.jsx (1 hunks)
  • web/src/components/table/task-logs/modals/ContentModal.jsx (3 hunks)
  • web/src/components/table/usage-logs/UsageLogsFilters.jsx (1 hunks)
  • web/src/components/topup/RechargeCard.jsx (7 hunks)
  • web/src/components/topup/index.jsx (6 hunks)
  • web/src/components/topup/modals/PaymentConfirmModal.jsx (1 hunks)
  • web/src/constants/console.constants.js (1 hunks)
  • web/src/helpers/api.js (0 hunks)
  • web/src/helpers/data.js (1 hunks)
  • web/src/helpers/render.jsx (4 hunks)
  • web/src/helpers/utils.jsx (1 hunks)
  • web/src/hooks/channels/useChannelsData.jsx (24 hunks)
  • web/src/hooks/common/useSidebar.js (2 hunks)
  • web/src/hooks/model-pricing/useModelPricingData.jsx (2 hunks)
  • web/src/i18n/locales/en.json (1 hunks)
  • web/src/i18n/locales/fr.json (2 hunks)
  • web/src/pages/Setting/Operation/SettingsGeneral.jsx (5 hunks)
  • web/src/pages/Setting/Operation/SettingsMonitoring.jsx (1 hunks)
  • web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx (5 hunks)
  • web/src/pages/Setting/Ratio/ModelRatioSettings.jsx (4 hunks)
💤 Files with no reviewable changes (1)
  • web/src/helpers/api.js
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-08T17:12:43.157Z
Learnt from: RedwindA
PR: QuantumNous/new-api#1537
File: relay/gemini_handler.go:330-342
Timestamp: 2025-08-08T17:12:43.157Z
Learning: In the new-api repository, the `GeminiEmbeddingHandler` function in `relay/gemini_handler.go` is designed specifically for native Gemini embedding requests and therefore does not require the `ConvertGeminiRequest` step that is used in the chat handler. The embedding requests are already in the native Gemini format and don't need conversion.

Applied to files:

  • relay/channel/ollama/adaptor.go
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the applyModelMapping function transforms the models list by replacing original model names (mapping values) with display names (mapping keys). The database stores this transformed list containing mapped keys. On channel load, data.models contains these mapped display names, making the initialization filter if (data.models.includes(key)) correct.

Applied to files:

  • web/src/components/table/channels/modals/EditChannelModal.jsx
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the database stores mapped keys (display names) in the models field after applying model mapping transformations. When loading a channel, data.models contains the mapped keys, not the original model names. The filtering logic if (data.models.includes(key)) in the initialization is correct.

Applied to files:

  • web/src/components/table/channels/modals/EditChannelModal.jsx
🧬 Code graph analysis (24)
web/src/helpers/utils.jsx (2)
web/src/helpers/render.jsx (11)
  • symbol (841-841)
  • symbol (911-911)
  • symbol (931-931)
  • statusStr (840-840)
  • statusStr (910-910)
  • statusStr (934-934)
  • statusStr (945-945)
  • s (844-844)
  • s (914-914)
  • s (938-938)
  • s (950-950)
web/src/hooks/model-pricing/useModelPricingData.jsx (1)
  • currency (44-44)
controller/topup.go (1)
setting/operation_setting/general_setting.go (2)
  • GetQuotaDisplayType (55-57)
  • QuotaDisplayTypeTokens (9-9)
web/src/hooks/common/useSidebar.js (2)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
  • adminConfig (90-90)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
  • adminConfig (120-120)
relay/channel/ollama/dto.go (1)
dto/claude.go (1)
  • Thinking (399-402)
logger/logger.go (3)
setting/operation_setting/general_setting.go (5)
  • GetQuotaDisplayType (55-57)
  • QuotaDisplayTypeCNY (8-8)
  • QuotaDisplayTypeCustom (10-10)
  • GetGeneralSetting (40-42)
  • QuotaDisplayTypeTokens (9-9)
common/constants.go (1)
  • QuotaPerUnit (21-21)
setting/operation_setting/payment_setting_old.go (1)
  • USDExchangeRate (18-18)
web/src/components/settings/personal/cards/AccountManagement.jsx (1)
web/src/components/settings/PersonalSetting.jsx (2)
  • userState (44-44)
  • status (57-57)
controller/billing.go (3)
setting/operation_setting/general_setting.go (3)
  • GetQuotaDisplayType (55-57)
  • QuotaDisplayTypeCNY (8-8)
  • QuotaDisplayTypeTokens (9-9)
common/constants.go (1)
  • QuotaPerUnit (21-21)
setting/operation_setting/payment_setting_old.go (1)
  • USDExchangeRate (18-18)
relay/channel/ollama/adaptor.go (5)
relay/common/relay_info.go (1)
  • RelayInfo (74-120)
dto/gemini.go (1)
  • GeminiChatRequest (12-20)
dto/audio.go (1)
  • AudioRequest (9-15)
relay/constant/relay_mode.go (2)
  • RelayModeEmbeddings (12-12)
  • RelayModeCompletions (11-11)
dto/openai_request.go (1)
  • OpenAIResponsesRequest (768-791)
web/src/components/topup/RechargeCard.jsx (1)
web/src/components/topup/index.jsx (7)
  • paymentLoading (69-69)
  • payWay (67-67)
  • topUpCount (49-51)
  • topupInfo (85-88)
  • priceRatio (58-58)
  • selectedPreset (82-82)
  • formatLargeNumber (503-505)
relay/channel/ollama/stream.go (7)
relay/helper/common.go (6)
  • Done (92-94)
  • SetEventStreamHeaders (27-41)
  • GenerateStartEmptyResponse (140-156)
  • StringData (67-73)
  • GenerateStopResponse (158-171)
  • GenerateFinalUsageResponse (173-183)
dto/openai_response.go (8)
  • Usage (221-234)
  • ChatCompletionsStreamResponse (140-148)
  • ChatCompletionsStreamResponseChoice (79-84)
  • ChatCompletionsStreamResponseChoiceDelta (86-92)
  • ToolCallResponse (120-126)
  • FunctionResponse (132-138)
  • OpenAITextResponse (38-46)
  • OpenAITextResponseChoice (32-36)
types/error.go (2)
  • NewAPIError (82-90)
  • NewOpenAIError (215-238)
service/http.go (2)
  • CloseResponseBodyGracefully (14-22)
  • IOCopyBytesGracefully (24-59)
common/utils.go (1)
  • GetUUID (227-231)
common/json.go (1)
  • Marshal (20-22)
logger/logger.go (1)
  • LogError (64-66)
web/src/components/table/task-logs/modals/ContentModal.jsx (1)
web/src/hooks/task-logs/useTaskLogsData.js (1)
  • modalContent (68-68)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
web/src/components/table/channels/modals/EditTagModal.jsx (1)
  • handleInputChange (77-129)
model/option.go (1)
setting/config/config.go (2)
  • GlobalConfig (18-18)
  • UpdateConfigFromMap (235-237)
web/src/hooks/model-pricing/useModelPricingData.jsx (2)
web/src/components/topup/index.jsx (1)
  • statusState (44-44)
web/src/hooks/common/useSidebar.js (1)
  • statusState (29-29)
web/src/pages/Setting/Ratio/ModelRatioSettings.jsx (1)
web/src/components/settings/RatioSetting.jsx (1)
  • inputs (35-49)
web/src/components/table/mj-logs/MjLogsFilters.jsx (1)
web/src/constants/console.constants.js (2)
  • DATE_RANGE_PRESETS (23-49)
  • DATE_RANGE_PRESETS (23-49)
web/src/helpers/render.jsx (1)
web/src/helpers/utils.jsx (3)
  • statusStr (654-654)
  • symbol (649-649)
  • s (656-656)
controller/topup_stripe.go (1)
setting/operation_setting/general_setting.go (2)
  • GetQuotaDisplayType (55-57)
  • QuotaDisplayTypeTokens (9-9)
relay/channel/ollama/relay-ollama.go (7)
relay/channel/ollama/dto.go (6)
  • OllamaTool (22-25)
  • OllamaToolFunction (16-20)
  • OllamaChatMessage (7-14)
  • OllamaToolCall (27-32)
  • OllamaEmbeddingRequest (57-62)
  • OllamaEmbeddingResponse (64-69)
dto/openai_request.go (2)
  • ContentTypeText (374-374)
  • ResponseFormat (13-16)
common/json.go (1)
  • Unmarshal (8-10)
dto/embedding.go (1)
  • EmbeddingRequest (21-32)
dto/openai_response.go (3)
  • Usage (221-234)
  • OpenAIEmbeddingResponseItem (53-57)
  • OpenAIEmbeddingResponse (59-64)
types/error.go (3)
  • NewAPIError (82-90)
  • NewOpenAIError (215-238)
  • ErrorCodeBadResponseBody (68-68)
service/http.go (1)
  • CloseResponseBodyGracefully (14-22)
web/src/components/table/task-logs/TaskLogsFilters.jsx (1)
web/src/constants/console.constants.js (2)
  • DATE_RANGE_PRESETS (23-49)
  • DATE_RANGE_PRESETS (23-49)
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)
web/src/components/settings/OperationSetting.jsx (1)
  • inputs (32-73)
web/src/components/table/usage-logs/UsageLogsFilters.jsx (1)
web/src/constants/console.constants.js (2)
  • DATE_RANGE_PRESETS (23-49)
  • DATE_RANGE_PRESETS (23-49)
web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx (2)
web/src/components/settings/PaymentSetting.jsx (1)
  • inputs (30-49)
web/src/helpers/utils.jsx (3)
  • verifyJSON (265-272)
  • verifyJSON (265-272)
  • showError (122-151)
controller/misc.go (1)
setting/operation_setting/general_setting.go (3)
  • IsCurrencyDisplay (45-47)
  • GetQuotaDisplayType (55-57)
  • GetGeneralSetting (40-42)
🔇 Additional comments (46)
web/jsconfig.json (1)

1-9: No issues found.

Looks good—no action needed.

web/src/constants/console.constants.js (1)

27-47: Formatting change looks good.

Trailing commas are fine here and don’t affect behavior.

web/src/components/settings/SystemSetting.jsx (9)

190-197: Good: sync UI state with fetched SSRF modes

Local domain/ip filter mode is correctly derived from fetched options before rendering the form. No action needed.


755-762: Copy/text tweak LGTM

Helper tip text and styling change is fine.


767-775: Radio onChange normalization is robust

Gracefully handling both event and direct value. Looks good.


786-789: Propagate TagInput changes into form state

Keeping inputs in sync is correct; no issues.


805-808: Info text only

No behavioral impact.


815-823: IP mode toggle handling is consistent with domain mode

State wiring is correct.


834-837: IP list TagInput sync is correct

Updates both local and form state. Good.


851-856: Ports helper text

No concerns.


870-875: Additional helper text

No concerns.

web/src/components/table/channels/modals/EditChannelModal.jsx (4)

90-91: Confirm fetchable type coverage (type 45?).

MODEL_FETCHABLE_TYPES doesn’t include 45. If the Ark provider supports listing models, consider adding it; otherwise, acknowledge it’s intentionally excluded.


859-865: Good guard: require base_url for type 45.

The validation prevents empty API address for Ark channels. LGTM.


432-434: Enterprise flag mapping looks consistent; ensure backend contract.

is_enterprise_account ⇄ settings.openrouter_enterprise is symmetric on load/save. Confirm server expects the boolean at settings.openrouter_enterprise for type 20.

Also applies to: 906-909


1313-1316: No action.

Text-only hint; nothing to change.

web/src/pages/Setting/Ratio/ModelRatioSettings.jsx (4)

229-234: Good i18n wrapping on extraText/placeholder (ImageRatio).

Wrapping these strings with t(...) aligns with the rest of the page. No functional changes introduced.


254-256: Good i18n wrapping on placeholder (AudioRatio).

Consistent with other fields; looks correct.


275-280: Good i18n wrapping on extraText/placeholder (AudioCompletionRatio).

Consistent, no issues spotted.


112-114: No changes needed—endpoint name is correct

The frontend POST to /api/option/rest_model_ratio matches the backend handler (optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio)) and the API docs (POST | /api/option/rest_model_ratio | … 重置模型倍率).

web/index.html (1)

13-13: LGTM – formatting only.

controller/setup.go (1)

181-181: LGTM – no behavioral change.

relay/channel/ollama/dto.go (1)

7-14: Struct tag/style normalization looks good

No shape changes; JSON compatibility maintained. LGTM.

Also applies to: 23-25, 45-55, 57-62, 64-69

web/src/components/topup/index.jsx (1)

188-199: Remove parseInt suggestion — topUpCount is always integer
The UI’s Form.InputNumber uses precision={0}, parser/formatter and onChange to guarantee topUpCount is a whole-number value. The pay endpoints expect a unit count (not a currency value), so using parseInt(topUpCount) is both correct and harmless. Currency decimals are already handled via getAmount / getStripeAmount with parseFloat.

Likely an incorrect or invalid review comment.

web/src/components/table/task-logs/TaskLogsFilters.jsx (1)

59-63: LGTM: minor style consistency in presets mapping

Parenthesized param and trailing comma are fine; no behavior change.

web/src/components/table/usage-logs/UsageLogsFilters.jsx (1)

60-64: LGTM: consistent presets mapping

No functional impact; improves diff stability.

web/src/i18n/locales/en.json (1)

1776-1780: i18n keys verification complete

  • No references found to the removed key “美元汇率(非充值汇率,仅用于定价页面换算)”.
  • New keys are in use across SettingsGeneral.jsx, PricingDisplaySettings.jsx, and SearchActions.jsx.
relay/channel/ollama/adaptor.go (2)

50-57: Routing logic LGTM

Embeddings → /api/embed, completions → /api/generate, else → /api/chat reads correctly given RelayMode.


66-74: Nil‑request guard LGTM

Early return on nil request avoids panics; branch to generate/chat is clear.

web/src/i18n/locales/fr.json (1)

1776-1779: New quota/currency i18n keys look good

Translations read well and align with the new quota_display_type feature. No JSON syntax issues spotted in this hunk.

relay/channel/ollama/stream.go (1)

117-119: Confirm helper setters exist.

Verify dto.ChatCompletionsStreamResponseChoiceDelta has methods SetContentString and SetReasoningContent; otherwise this won’t compile.

Also applies to: 123-124

web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx (1)

59-60: Translation key exists; no shared currency constants found, so literal use is acceptable.

web/src/components/settings/personal/cards/AccountManagement.jsx (3)

88-89: LGTM (formatting only).


229-232: LGTM (clarified disabled condition layout).


389-391: LGTM (clarified disabled condition layout).

web/src/components/settings/OperationSetting.jsx (1)

45-46: LGTM: switched to type-safe 'general_setting.quota_display_type'.

controller/billing.go (1)

43-47: OpenAI compatibility risk: TotalUsage/SoftLimitUSD semantics now vary by display type.

You re-interpret *_USD and total_usage to mean “site display type” units (CNY or tokens). This may break third‑party clients expecting USD cents. Confirm all consumers are updated, or gate this via a flag/API version.

Also applies to: 102-105

web/src/helpers/utils.jsx (1)

666-668: Formatting looks good.

Prefixing resolved symbol and respecting precision aligns with displayPrice; OK.

web/src/hooks/model-pricing/useModelPricingData.jsx (2)

76-90: Currency sync with site display type is correct.

Auto‑syncing USD/CNY/CUSTOM while leaving TOKENS free for per‑view toggling matches the PR intent.

Please confirm this doesn’t override an explicit user selection when siteDisplayType changes mid‑session.


180-186: displayPrice path for CUSTOM is correct.

Symbol + rate applied consistently with USD/CNY branches.

controller/topup.go (2)

89-94: Clarify/verify getPayMoney semantics for USD/CNY/CUSTOM.

Comment says “USD/CNY: 前端传金额; TOKENS: 前端传 tokens”, but this function always multiplies amount by Price and group ratio. If amount is already currency, payMoney might need to be the same currency (or converted only for TOKENS). Please confirm front‑end/back‑end agreement and the unit expected by epay/stripe.


275-279: Confirm quota crediting uses normalized USD amount.

With the above normalization, quotaToAdd = Amount * QuotaPerUnit remains correct. Without it, CNY/CUSTOM would over‑credit.

logger/logger.go (1)

95-119: Good: Logger formatting covers USD/CNY/TOKENS/CUSTOM.

Handles empty symbol and non‑positive rate; matches frontend behavior.

controller/topup_stripe.go (2)

259-278: LGTM! Token display mode handling is correct.

The refactored logic properly handles the TOKENS display mode by dividing the amount by QuotaPerUnit, while preserving the original amount for discount calculations. This maintains backward compatibility while supporting the new quota display type system.


280-286: LGTM! Minimum topup scaling is properly implemented.

The function correctly scales the minimum topup amount when in TOKENS display mode, maintaining consistency with the payment amount calculation in getStripePayMoney.

setting/operation_setting/general_setting.go (1)

67-74: Custom currency fallback looks solid.

Good call returning the configured symbol when present and falling back to ¤; this keeps formatting resilient when admins forget to set a custom symbol.

controller/misc.go (1)

67-71: Compatibility preserved; quota_display_type exposed

Keeping display_in_currency for legacy and adding quota_display_type plus custom currency fields looks correct. No issues.

Please confirm old UIs that only read display_in_currency still behave (CUSTOM and CNY should both imply true).

Comment thread common/constants.go
Comment on lines +22 to 24
// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
var DisplayInCurrencyEnabled = true
var DisplayTokenStatEnabled = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Mark legacy flag as deprecated and point to new getters

Add a Go doc “Deprecated:” comment so linters/docs guide developers to use operation_setting.GetQuotaDisplayType()/IsCurrencyDisplay(). This avoids accidental reliance on the boolean.

-// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
-var DisplayInCurrencyEnabled = true
+// Deprecated: 仅为兼容历史逻辑保留。实际展示请使用 general_setting.quota_display_type,
+// 并通过 operation_setting.GetQuotaDisplayType()/IsCurrencyDisplay() 判断。
+var DisplayInCurrencyEnabled = true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
var DisplayInCurrencyEnabled = true
var DisplayTokenStatEnabled = true
// Deprecated: 仅为兼容历史逻辑保留。实际展示请使用 general_setting.quota_display_type,
// 并通过 operation_setting.GetQuotaDisplayType()/IsCurrencyDisplay() 判断。
var DisplayInCurrencyEnabled = true
var DisplayTokenStatEnabled = true
🤖 Prompt for AI Agents
In common/constants.go around lines 22 to 24, the two legacy flags
DisplayInCurrencyEnabled and DisplayTokenStatEnabled should be marked as
deprecated and point maintainers to the new getter functions; update the Go doc
comment above each var to start with "Deprecated:" and reference
operation_setting.GetQuotaDisplayType() / operation_setting.IsCurrencyDisplay()
(or the correct getter names) so linters/docs and developers know to use those
instead of relying on the booleans, keeping the vars for compatibility but
clearly documented as deprecated.

Comment thread common/database.go
var UsingClickHouse = false

var SQLitePath = "one-api.db?_busy_timeout=30000" No newline at end of file
var SQLitePath = "one-api.db?_busy_timeout=30000"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

SQLite DSN change: verify driver compatibility and DSN composition.

Adding _busy_timeout=30000 is good for “database is locked” resilience, but:

  • Ensure the SQLite driver in use supports _busy_timeout (mattn/go-sqlite3 does; others may differ).
  • Confirm DSN isn’t further concatenated elsewhere with another ? (should use & after the first ?). If other params are appended naïvely, this could break connections.

Suggestion:

  • Centralize DSN building (helper that merges base path + query params).
  • Optionally make busy timeout configurable via env (e.g., SQLITE_BUSY_TIMEOUT_MS).

Run to inspect usage and DSN building:


🏁 Script executed:

#!/bin/bash
# Where SQLitePath is used and how DSN is built/appended
rg -n -C3 '\bSQLitePath\b'
# Look for sqlite open calls to confirm driver
rg -n -C3 -P '\b(sqlite3|sqlite)\b' | rg -n -C1 -P 'gorm|Open|driver'
# Any manual DSN concatenations with "?" vs "&"
rg -n -C2 -P '\?[_a-zA-Z0-9]+=|&_busy_timeout'

Length of output: 41909


🌐 Web query:

Does the Go driver github.com/glebarez/sqlite support the `_busy_timeout` query parameter in its DSN?

💡 Result:

Short answer: No — glebarez/go-sqlite does not expose a dedicated _busy_timeout DSN key. Use the _pragma query parameter (e.g. ?_pragma=busy_timeout(5000)) or run PRAGMA busy_timeout after connecting. [1][2]

For comparison, the cgo driver mattn/go-sqlite3 does provide a _busy_timeout DSN option. [3]

Sources:

  • glebarez/go-sqlite README (shows _pragma=busy_timeout example). [1]
  • pkg.go.dev for github.com/glebarez/go-sqlite (documents _pragma support). [2]
  • mattn/go-sqlite3 connection-DSN docs (shows _busy_timeout parameter). [3]

Replace unsupported _busy_timeout DSN param with _pragma=busy_timeout(...) in common/database.go
The github.com/glebarez/sqlite driver doesn’t support _busy_timeout; update the DSN to use ? _pragma=busy_timeout(30000) or run PRAGMA busy_timeout=30000 immediately after connecting. Optionally make the timeout configurable via an environment variable.

🤖 Prompt for AI Agents
In common/database.go around line 15, the DSN uses the unsupported
`_busy_timeout` param; update the connection to either use the supported
`_pragma=busy_timeout(30000)` form in the DSN (e.g.
"?_pragma=busy_timeout(30000)") or execute `PRAGMA busy_timeout = 30000` right
after opening the DB connection; optionally read the timeout value from an
environment variable and apply it to the DSN or PRAGMA so the timeout is
configurable.

Comment thread controller/billing.go
Comment on lines +48 to 55
switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY:
amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
case operation_setting.QuotaDisplayTypeTokens:
// amount 保持 tokens 数值
default:
amount = amount / common.QuotaPerUnit
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add CUSTOM display handling (and avoid static USD rate).

Switch lacks QuotaDisplayTypeCustom; CUSTOM currently falls back to USD. Align with logger/frontend by converting tokens→USD→custom using configured rate; also prefer reading dynamic rates from GeneralSetting rather than a static USDExchangeRate constant.

Apply:

 switch operation_setting.GetQuotaDisplayType() {
 case operation_setting.QuotaDisplayTypeCNY:
-    amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
+    amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
+case operation_setting.QuotaDisplayTypeCustom:
+    {
+      rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate
+      if rate <= 0 {
+        rate = 1
+      }
+      amount = amount / common.QuotaPerUnit * rate
+    }
 case operation_setting.QuotaDisplayTypeTokens:
     // amount 保持 tokens 数值
 default:
     amount = amount / common.QuotaPerUnit
 }
🤖 Prompt for AI Agents
In controller/billing.go around lines 48 to 55, the switch is missing handling
for QuotaDisplayTypeCustom and uses a static USDExchangeRate; update it to add a
case for operation_setting.QuotaDisplayTypeCustom that converts
tokens→USD→custom using the configured custom rate from GeneralSetting (or
equivalent dynamic rate lookup) instead of the static USDExchangeRate constant,
keep the QuotaDisplayTypeTokens branch unchanged, and change the
QuotaDisplayTypeCNY and default branches to read dynamic rates from
GeneralSetting so all currency conversions use runtime-configured rates
consistent with logger/frontend.

Comment thread model/option.go
Comment on lines +243 to +251
// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
// true -> USD, false -> TOKENS
newVal := "USD"
if !boolValue {
newVal = "TOKENS"
}
if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil {
_ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Keep legacy bool updated and OptionMap in sync with new key; log update errors

  • common.DisplayInCurrencyEnabled isn’t updated.
  • OptionMap lacks the mirrored general_setting.quota_display_type, so /api/option may return stale data.
  • Errors from UpdateConfigFromMap are ignored.
 		case "DisplayInCurrencyEnabled":
-			// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
+			// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
 			// true -> USD, false -> TOKENS
+			common.DisplayInCurrencyEnabled = boolValue
 			newVal := "USD"
 			if !boolValue {
 				newVal = "TOKENS"
 			}
 			if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil {
-				_ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal})
+				if err := config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal}); err != nil {
+					common.SysLog("failed to update general_setting.quota_display_type from legacy flag: " + err.Error())
+				}
+				// 保持 OptionMap 同步,确保 /api/option 读取一致
+				common.OptionMap["general_setting.quota_display_type"] = newVal
 			}

Run to check lingering usages of the legacy bool (adjust follow-up accordingly):


🏁 Script executed:

#!/bin/bash
# Find reads of DisplayInCurrencyEnabled across backend to assess impact
rg -nP -C2 '\bDisplayInCurrencyEnabled\b' --type go

Length of output: 1199


Sync DisplayInCurrencyEnabled update with config and OptionMap

  • Set common.DisplayInCurrencyEnabled = boolValue.
  • Log errors returned by UpdateConfigFromMap.
  • Mirror "general_setting.quota_display_type" in common.OptionMap.
case "DisplayInCurrencyEnabled":
    // 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
    // true -> USD, false -> TOKENS
+   common.DisplayInCurrencyEnabled = boolValue
    newVal := "USD"
    if !boolValue {
        newVal = "TOKENS"
    }
    if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil {
-       _ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal})
+       if err := config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal}); err != nil {
+           common.SysLog("failed to update general_setting.quota_display_type from legacy flag: " + err.Error())
+       }
+       common.OptionMap["general_setting.quota_display_type"] = newVal
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
// true -> USD, false -> TOKENS
newVal := "USD"
if !boolValue {
newVal = "TOKENS"
}
if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil {
_ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal})
}
case "DisplayInCurrencyEnabled":
// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
// true -> USD, false -> TOKENS
common.DisplayInCurrencyEnabled = boolValue
newVal := "USD"
if !boolValue {
newVal = "TOKENS"
}
if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil {
if err := config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal}); err != nil {
common.SysLog("failed to update general_setting.quota_display_type from legacy flag: " + err.Error())
}
common.OptionMap["general_setting.quota_display_type"] = newVal
}
🤖 Prompt for AI Agents
In model/option.go around lines 243 to 251, after computing newVal from
boolValue, set common.DisplayInCurrencyEnabled = boolValue; when calling
UpdateConfigFromMap capture and check the returned error and log it instead of
ignoring it; also ensure you mirror the change into common.OptionMap by setting
common.OptionMap["general_setting.quota_display_type"] = newVal (after
confirming cfg != nil or regardless if OptionMap should reflect the intended
value); keep the existing cfg check and map update logic but wire in error
handling and the two updates described.

Comment on lines +279 to 283
usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens: 0, TotalTokens: oResp.PromptEvalCount}
embResp := &dto.OpenAIEmbeddingResponse{Object: "list", Data: data, Model: info.UpstreamModelName, Usage: *usage}
out, _ := common.Marshal(embResp)
service.IOCopyBytesGracefully(c, resp, out)
return usage, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle marshal errors instead of ignoring them

Ignoring the error can lead to writing an empty/invalid body.

-  out, _ := common.Marshal(embResp)
-  service.IOCopyBytesGracefully(c, resp, out)
+  out, err := common.Marshal(embResp)
+  if err != nil {
+    return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
+  }
+  service.IOCopyBytesGracefully(c, resp, out)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens: 0, TotalTokens: oResp.PromptEvalCount}
embResp := &dto.OpenAIEmbeddingResponse{Object: "list", Data: data, Model: info.UpstreamModelName, Usage: *usage}
out, _ := common.Marshal(embResp)
service.IOCopyBytesGracefully(c, resp, out)
return usage, nil
usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens: 0, TotalTokens: oResp.PromptEvalCount}
embResp := &dto.OpenAIEmbeddingResponse{Object: "list", Data: data, Model: info.UpstreamModelName, Usage: *usage}
out, err := common.Marshal(embResp)
if err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
}
service.IOCopyBytesGracefully(c, resp, out)
return usage, nil
🤖 Prompt for AI Agents
In relay/channel/ollama/relay-ollama.go around lines 279 to 283, the call to
common.Marshal ignores the returned error and may write an empty/invalid
response body; update the code to capture the error from common.Marshal, handle
it by logging or returning an error response (e.g., log the marshal error, set
appropriate HTTP status and error body or return the error up the stack), and
only call service.IOCopyBytesGracefully when marshalling succeeds to avoid
sending invalid output.

Comment thread web/src/helpers/data.js
Comment on lines +26 to 29
// 兼容:保留旧字段,同时写入新的额度展示类型
localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('quota_display_type', data.quota_display_type || 'USD');
localStorage.setItem('enable_drawing', data.enable_drawing);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Back-compat fallback: derive quota_display_type from legacy display_in_currency when missing

Defaulting to 'USD' ignores the legacy flag when quota_display_type isn’t provided. Map true→'USD', false→'TOKENS' to preserve behavior.

-  // 兼容:保留旧字段,同时写入新的额度展示类型
+  // 兼容:保留旧字段,同时写入新的额度展示类型
   localStorage.setItem('display_in_currency', data.display_in_currency);
-  localStorage.setItem('quota_display_type', data.quota_display_type || 'USD');
+  const fallbackQuotaType =
+    data.quota_display_type ??
+    (data.display_in_currency === false ? 'TOKENS' : 'USD');
+  localStorage.setItem('quota_display_type', fallbackQuotaType);
+
+  // 可选:自定义货币支持(若后端在 /api/status 返回这些字段)
+  if (data.custom_currency_symbol)
+    localStorage.setItem('custom_currency_symbol', data.custom_currency_symbol);
+  if (data.custom_currency_exchange_rate != null)
+    localStorage.setItem(
+      'custom_currency_exchange_rate',
+      String(data.custom_currency_exchange_rate),
+    );

Based on PR objectives.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 兼容:保留旧字段,同时写入新的额度展示类型
localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('quota_display_type', data.quota_display_type || 'USD');
localStorage.setItem('enable_drawing', data.enable_drawing);
// 兼容:保留旧字段,同时写入新的额度展示类型
localStorage.setItem('display_in_currency', data.display_in_currency);
const fallbackQuotaType =
data.quota_display_type ??
(data.display_in_currency === false ? 'TOKENS' : 'USD');
localStorage.setItem('quota_display_type', fallbackQuotaType);
// 可选:自定义货币支持(若后端在 /api/status 返回这些字段)
if (data.custom_currency_symbol)
localStorage.setItem('custom_currency_symbol', data.custom_currency_symbol);
if (data.custom_currency_exchange_rate != null)
localStorage.setItem(
'custom_currency_exchange_rate',
String(data.custom_currency_exchange_rate),
);
localStorage.setItem('enable_drawing', data.enable_drawing);
🤖 Prompt for AI Agents
In web/src/helpers/data.js around lines 26 to 29, the code unconditionally
defaults quota_display_type to 'USD' which ignores the legacy
display_in_currency flag; instead, when data.quota_display_type is missing,
derive it from data.display_in_currency mapping true -> 'USD' and false ->
'TOKENS' (while still writing the legacy display_in_currency to localStorage),
and ensure you only apply the mapping when quota_display_type is null/undefined.

Comment on lines +903 to 906
const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
if (quotaDisplayType === 'TOKENS') {
return renderNumber(renderUnitWithQuota(amount));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify localStorage population for new keys

These helpers rely on quota_display_type and status.* being present in localStorage. Ensure the status bootstrap path writes:

  • quota_display_type
  • custom_currency_symbol
  • custom_currency_exchange_rate
  • quota_per_unit
  • usd_exchange_rate

Run:

Also applies to: 1559-1562


🏁 Script executed:

#!/bin/bash
rg -n -C2 "localStorage\.setItem\(" --type=ts --type=tsx --type=js --type=jsx | rg -n "quota_display_type|custom_currency_symbol|custom_currency_exchange_rate|quota_per_unit|usd_exchange_rate"

Length of output: 27


🏁 Script executed:

#!/bin/bash
rg -n -C2 "localStorage\\.setItem" -g "*.{js,jsx,ts,tsx}" | rg -n "quota_display_type|custom_currency_symbol|custom_currency_exchange_rate|quota_per_unit|usd_exchange_rate"

Length of output: 204


Persist missing localStorage keys in bootstrap

In web/src/helpers/data.js the bootstrap path currently sets only quota_per_unit and quota_display_type; please also persist the following keys so the render helpers work as expected:

  • custom_currency_symbol
  • custom_currency_exchange_rate
  • usd_exchange_rate
🤖 Prompt for AI Agents
In web/src/helpers/render.jsx around lines 903-906: the render helpers expect
localStorage keys for currency and exchange rates but the bootstrap path in
web/src/helpers/data.js currently only persists quota_per_unit and
quota_display_type; update the bootstrap logic in web/src/helpers/data.js to
also persist custom_currency_symbol, custom_currency_exchange_rate, and
usd_exchange_rate to localStorage (store sensible stringified defaults or values
from the bootstrap payload) so renderNumber/renderUnitWithQuota can read them as
expected.

Comment on lines +121 to 129
if (
originInputs['AmountOptions'] !== inputs.AmountOptions &&
inputs.AmountOptions.trim() !== ''
) {
if (!verifyJSON(inputs.AmountOptions)) {
showError(t('自定义充值数量选项不是合法的 JSON 数组'));
return;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validation improved, but also validate JSON shape (array/object) to match messages

Currently verifyJSON only parses; it doesn’t ensure an array for AmountOptions or an object for AmountDiscount. Add type checks before submit.

-  if (!verifyJSON(inputs.AmountOptions)) {
+  if (!verifyJSON(inputs.AmountOptions)) {
     showError(t('自定义充值数量选项不是合法的 JSON 数组'));
     return;
-  }
+  }
+  try {
+    const parsed = JSON.parse(inputs.AmountOptions);
+    if (!Array.isArray(parsed)) {
+      showError(t('自定义充值数量选项必须是 JSON 数组'));
+      return;
+    }
+  } catch {}
-  if (!verifyJSON(inputs.AmountDiscount)) {
+  if (!verifyJSON(inputs.AmountDiscount)) {
     showError(t('充值金额折扣配置不是合法的 JSON 对象'));
     return;
-  }
+  }
+  try {
+    const parsed = JSON.parse(inputs.AmountDiscount);
+    if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
+      showError(t('充值金额折扣配置必须是 JSON 对象'));
+      return;
+    }
+  } catch {}

Also applies to: 131-139

Comment on lines 171 to 176
if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions });
options.push({
key: 'payment_setting.amount_options',
value: inputs.AmountOptions,
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: option key mismatch will drop/ghost values

You read legacy props.options.AmountOptions/AmountDiscount but submit to ‘payment_setting.amount_options’/‘payment_setting.amount_discount’. If backend now returns only namespaced keys, the form won’t prefill; if it returns only legacy keys, new writes go to unused keys.

Fix by reading both (prefer namespaced), and writing both during a transition window or aligning to one canonical key across load/save.

-        AmountOptions: props.options.AmountOptions || '',
-        AmountDiscount: props.options.AmountDiscount || '',
+        AmountOptions: props.options['payment_setting.amount_options'] ??
+          props.options.AmountOptions ?? '',
+        AmountDiscount: props.options['payment_setting.amount_discount'] ??
+          props.options.AmountDiscount ?? '',
-      if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
-        options.push({
-          key: 'payment_setting.amount_options',
-          value: inputs.AmountOptions,
-        });
-      }
+      if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
+        // TODO: remove legacy key write after backend fully migrates
+        options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions });
+        options.push({ key: 'AmountOptions', value: inputs.AmountOptions });
+      }
-      if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) {
-        options.push({
-          key: 'payment_setting.amount_discount',
-          value: inputs.AmountDiscount,
-        });
-      }
+      if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) {
+        // TODO: remove legacy key write after backend fully migrates
+        options.push({ key: 'payment_setting.amount_discount', value: inputs.AmountDiscount });
+        options.push({ key: 'AmountDiscount', value: inputs.AmountDiscount });
+      }

If backend already aliases keys server-side, alternatively switch both read and write to the namespaced keys only and remove legacy handling—confirm which is canonical.

Also applies to: 178-182


🏁 Script executed:

#!/bin/bash
# Find the file path
fd SettingsPaymentGateway.jsx

# Show reading logic for AmountOptions/AmountDiscount
rg -nP 'AmountOptions|AmountDiscount' -C3 web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx

# Show all props.options usages
rg -nP '\bprops\.options' -C3 web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx

Length of output: 4236


Fix inconsistent key handling for AmountOptions and AmountDiscount

  • In useEffect, read both new namespaced and legacy keys (e.g. props.options['payment_setting.amount_options'] ?? props.options.AmountOptions ?? '') to ensure the form pre-fills regardless of which the backend returns.
  • In the submission logic, push both keys for each field (e.g.
    options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions });
    options.push({ key: 'AmountOptions',               value: inputs.AmountOptions });
    
    and similarly for AmountDiscount) and remove the legacy key writes once the backend fully migrates.
🤖 Prompt for AI Agents
In web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx around lines 171-176
the component only reads and writes the new namespaced keys for
AmountOptions/AmountDiscount causing inconsistent pre-fill and save behavior;
update the initialization/useEffect to read the namespaced key falling back to
the legacy key (e.g. props.options['payment_setting.amount_options'] ??
props.options.AmountOptions ?? '') so the form pre-fills regardless of backend,
and update the submission logic to push both the namespaced and legacy option
entries for each field (push {key: 'payment_setting.amount_options', value:
inputs.AmountOptions} and {key: 'AmountOptions', value: inputs.AmountOptions},
and similarly for AmountDiscount); keep both writes until the backend migration
is complete and then remove legacy-key writes.

Comment on lines +297 to 304
placeholder={t(
'为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]',
)}
autosize
extraText={t('设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]')}
extraText={t(
'设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]',
)}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add i18n entries for new labels/placeholders

Ensure en.json (and other locales) include:

  • “自定义充值数量选项”
  • “为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]”
  • “设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]”
  • “充值金额折扣配置”
  • “为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}”
  • “设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}”

Also applies to: 316-323


🏁 Script executed:

#!/bin/bash
# Check for missing translation keys in en.json
rg -n -S "自定义充值数量选项|充值金额折扣配置|为一个 JSON 数组 例如|\[10, 20|为一个 JSON 对象 例如|\{\"100\": 0\.95|设置用户可选择的充值数量选项|设置不同充值金额对应的折扣" web/src | cat
rg -n -S "自定义充值数量选项|充值金额折扣配置|为一个 JSON 数组 例如|\[10, 20|为一个 JSON 对象 例如|\{\"100\": 0\.95|设置用户可选择的充值数量选项|设置不同充值金额对应的折扣" web/src/i18n/locales/en.json | cat

Length of output: 2435


Add missing i18n entries for new Payment Gateway labels/placeholders

  • Add to en.json (and other locales):
    • “自定义充值数量选项”
    • “为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]”
    • “设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]”
    • “充值金额折扣配置”
    • “为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}”
    • “设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}”
🤖 Prompt for AI Agents
In web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx around lines 297 to
304, the new Chinese labels/placeholders added for custom recharge options and
discount configuration are not present in the i18n resource files; add
corresponding keys and English translations (and other locale files) to en.json
(and other locale JSONs): entries for "自定义充值数量选项", "为一个 JSON 数组,例如:[10, 20, 50,
100, 200, 500]", "设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]", "充值金额折扣配置",
"为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", and
"设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}" so
the t(...) calls resolve; update all locale files consistently and run the i18n
key validation/lint to ensure no missing keys.

@Calcium-Ion Calcium-Ion merged commit 3199e2e into main Oct 3, 2025
1 check passed
@Calcium-Ion Calcium-Ion deleted the pr/custom-currency branch October 3, 2025 13:52
@coderabbitai coderabbitai Bot mentioned this pull request Oct 14, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Dec 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants