💱 feat(settings): introduce site-wide quota display type (USD/CNY/TOKENS/CUSTOM)#1923
Conversation
…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.
WalkthroughIntroduces 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
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)
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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.
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. Comment |
There was a problem hiding this comment.
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 dotsForm 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
minTopUpValuecan becomeundefined/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 textWrap the string with
t()like other instances.- showError('充值数量不能小于' + minTopUp); + showError(t('充值数量不能小于') + minTopUp);
444-446: Localize toast error prefixUse
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 readsquota_display_typefromstatusState.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/CUSTOMUI 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 readonlyCurrent prop won’t set the input to read-only in React.
- <Input - readonly + <Input + readOnlyweb/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 payloadThese 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 truthLocal
priceRatio * discountcan drift from server logic/rounding. Reuse/api/user/amountto 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 fallbackDefaulting 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 availableCompare
topUpCountwith the selected payment method’s ownmin_topupinstead 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 handlingUse
finallylikegetStripeAmountto guaranteesetAmountLoading(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 argumentsIf upstream sends nested JSON,
json.RawMessageavoids 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 throughSemi 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 4xxThese 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 playbackAdd 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 constructionString 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 consistencyKeeps 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 errorsSwallowing 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 fieldOllamaEmbeddingRequest 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 discountsEnsure 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 entriesThe 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 wiringshowQuotaWarning 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 submitIf 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 callsYou 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 coercionEarly 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 currencyCurrently 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 numbersAvoid 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 readsstatus 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
📒 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 modesLocal domain/ip filter mode is correctly derived from fetched options before rendering the form. No action needed.
755-762: Copy/text tweak LGTMHelper tip text and styling change is fine.
767-775: Radio onChange normalization is robustGracefully handling both event and direct value. Looks good.
786-789: Propagate TagInput changes into form stateKeeping inputs in sync is correct; no issues.
805-808: Info text onlyNo behavioral impact.
815-823: IP mode toggle handling is consistent with domain modeState wiring is correct.
834-837: IP list TagInput sync is correctUpdates both local and form state. Good.
851-856: Ports helper textNo concerns.
870-875: Additional helper textNo 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 correctThe frontend POST to
/api/option/rest_model_ratiomatches 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 goodNo 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 mappingParenthesized param and trailing comma are fine; no behavior change.
web/src/components/table/usage-logs/UsageLogsFilters.jsx (1)
60-64: LGTM: consistent presets mappingNo 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 LGTMEmbeddings → /api/embed, completions → /api/generate, else → /api/chat reads correctly given RelayMode.
66-74: Nil‑request guard LGTMEarly 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 goodTranslations 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 exposedKeeping 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).
| // 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制 | ||
| var DisplayInCurrencyEnabled = true | ||
| var DisplayTokenStatEnabled = true |
There was a problem hiding this comment.
🛠️ 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.
| // 保留旧变量以兼容历史逻辑,实际展示由 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.
| 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" |
There was a problem hiding this comment.
🧩 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 兼容旧字段:同步到新配置 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}) | ||
| } |
There was a problem hiding this comment.
🧩 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 goLength 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"incommon.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.
| // 兼容旧字段:同步到新配置 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| // 兼容:保留旧字段,同时写入新的额度展示类型 | ||
| 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); |
There was a problem hiding this comment.
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.
| // 兼容:保留旧字段,同时写入新的额度展示类型 | |
| 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.
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; | ||
| if (quotaDisplayType === 'TOKENS') { | ||
| return renderNumber(renderUnitWithQuota(amount)); | ||
| } |
There was a problem hiding this comment.
🧩 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_symbolcustom_currency_exchange_rateusd_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.
| if ( | ||
| originInputs['AmountOptions'] !== inputs.AmountOptions && | ||
| inputs.AmountOptions.trim() !== '' | ||
| ) { | ||
| if (!verifyJSON(inputs.AmountOptions)) { | ||
| showError(t('自定义充值数量选项不是合法的 JSON 数组')); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
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
| 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, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 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.jsxLength 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.
and similarly for
options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions }); options.push({ key: 'AmountOptions', value: inputs.AmountOptions });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.
| 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]', | ||
| )} | ||
| /> |
There was a problem hiding this comment.
🧩 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 | catLength 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.
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
QuotaDisplayTypetooperation_setting.GeneralSettingwith injected registration viaconfig.GlobalConfig.Register("general_setting", ...). Helpers:IsCurrencyDisplay(),IsCNYDisplay(),GetQuotaDisplayType().quota_display_typein/api/statusand keep legacydisplay_in_currencyfor backward compatibility.LogQuotaandFormatQuotato support USD/CNY/TOKENS. When CNY is selected, convert usingoperation_setting.USDExchangeRate.billing: compute subscription/usage amounts based on the selected type (USD: divide byQuotaPerUnit; 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: includequota_display_typein status payload.model/option.UpdateOption, map updates toDisplayInCurrencyEnabled→general_setting.quota_display_type(true→USD, false→TOKENS). Keep exporting the legacy key inOptionMap.Frontend
general_setting.quota_display_type) offering USD / CNY / Tokens. Provide fallback mapping from legacyDisplayInCurrencyEnabled.quota_display_typeto localStorage (keepdisplay_in_currencyfor legacy components).quota_display_type; useusd_exchange_ratefor CNY symbol/values.Notes
Summary by CodeRabbit
New Features
Style