Skip to content

一些BUG的修复,以及一些新功能。尽可能的没有动后端,只是对前端进行了部分修改 - #4784

Closed
ustoolscn wants to merge 22 commits into
QuantumNous:mainfrom
ustoolscn:cooper
Closed

一些BUG的修复,以及一些新功能。尽可能的没有动后端,只是对前端进行了部分修改#4784
ustoolscn wants to merge 22 commits into
QuantumNous:mainfrom
ustoolscn:cooper

Conversation

@ustoolscn

@ustoolscn ustoolscn commented May 11, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

完全没有动数据库字段,后端仅添加了测试启动内容不影响正式环境,前端做了不少BUG修复和新功能。

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
1. 【修复】注册邀请无效的相关bug:
注册接口识别的字段是aff_code,前端传递的却是aff;已修复。
钱包页面邀请链接还是旧UI的页面链接,导致邀请页404错误;已修复。
2. 【修复】系统公告相关bug:
新UI系统公告无法自动弹出;修改逻辑为复用旧UI的自动弹出逻辑,已修复。
3. 【优化】钱包页面推荐计划位置调整:
把邀请/推荐计划卡片放到钱包统计卡片下面,更容易让用户看到。
4. 【优化】数据看板新增token消耗统计图表:
数据看板->模型调用分析->消耗分布图表 新增切换查看token数量消耗。
数据看板->用户统计-> 新增切换查看token数量消耗
5. 【优化】左侧导航栏“聊天”菜单扁平化:
聊天板块中原来的一级“聊天”菜单不显示,原二级聊天预设直接作为一级菜单显示。
6. 【优化】数据库启动跳过迁移检查:
测试环境中明确知道数据库结构和数据稳定时,可以跳过启动时耗时的 DB migration/check。
7. 【修复】用户列表删除用户异常bug:
用户列表删除用户,前端会返回删除失败,但实际成功;后端返回的非JSON格式,已修复。
8. 【改动】docker容器部署我把dockerhub那边取消了
这个因为我自用就改了下。

🚀 变更类型 / Type of change

  • [x ] 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • [x ] ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
邀请修复与邀请模块前移
image
数据看板新增token图表
image
image
侧导航栏聊天子模块转一级菜单
image
系统公告自动弹出
image
用户列表删除异常修复
image

Summary by CodeRabbit

  • New Features

    • Video per-second billing: UI, pricing, model support and charts; AI automatic translation tools and on-demand translation generation.
  • Improvements

    • Unified currency/pricing display with configurable wallet top-up/payment symbols; token vs quota metric toggle in dashboard charts.
    • Simplified chat presets navigation; auto-open notifications for unread items; deterministic rankings display multiplier/jitter.
    • Enhanced image upload and request replay behavior.
  • Bug Fixes

    • Affiliate signup field/route corrected; improved user-deletion error handling.
  • Chores

    • Docker publish workflow made configurable and GHCR-only (Docker Hub removed).

Review Change Stack

luji3120346 and others added 6 commits May 10, 2026 21:51
SKIP_DB_MIGRATION now only skips schema migration and validation while still loading database-backed runtime data such as options, pricing, channel cache, custom OAuth providers, and setup status.

When migration is skipped, setup status is loaded without creating a missing setup record, so existing databases with root users are treated as initialized without mutating schema-related state.

Also fix the user deletion endpoint to return a real API error on delete failure and a success JSON response on successful deletion, preventing the default frontend from showing a false delete failure toast after the user is removed.

Verified with: go test ./controller

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update the default frontend with several UI improvements: fix invitation handling in the new sign-up UI, add token-focused dashboard charts, move the wallet invitation section to a more prominent position, and flatten chat presets into first-level sidebar navigation entries.

Also update Bun lockfile metadata generated by the frontend package manager.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Open the existing announcement dialog when users enter the site with unread notice or timeline items, while avoiding repeated prompts for the same unread batch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

GHCR-only Docker publish with configurable channel; optional DB migration skip and shared setup helper; channel-exclusion retries for relay; token and video metrics + video-seconds billing end-to-end; pricing/wallet formatting refactor; AI-translation endpoints and UI; UI tweaks (chat presets, affiliate field, notifications auto-open); many i18n updates.

Changes

CI/CD & Infrastructure

Layer / File(s) Summary
Channel derivation & per-arch tag generation
.github/workflows/docker-image-alpha.yml
Derives CHANNEL from GITHUB_REF_NAME (default alpha) and computes channel/version per-arch; adds cooper trigger branch.
Remove Docker Hub publishing & GHCR-only signing
.github/workflows/docker-image-alpha.yml
Removes Docker Hub login/push/manifest and cosign steps; generates GHCR-only metadata and signs/reports GHCR image digests; multi-arch manifest job is GHCR-only.

Backend Initialization & Options

Layer / File(s) Summary
checkSetup helper & SkipDBMigration
model/main.go
Adds checkSetup(createMissingSetup bool) and SkipDBMigration() env parser; refactors CheckSetup/LoadSetupStatus to use the helper.
InitDB/InitLogDB & InitResources wiring
model/main.go, main.go
InitDB()/InitLogDB() return early when SKIP_DB_MIGRATION is enabled; InitResources() calls LoadSetupStatus() when skipping, otherwise CheckSetup().
InitOptionMap optional DB load
model/option.go
InitOptionMap(loadFromDatabase ...bool) optionally skips loading persisted options from DB and seeds RankingsDisplay and AI-translation defaults.
User Deletion Error Handling
controller/user.go
DeleteUser returns common.ApiError(c, err) and exits on HardDeleteUserById failure.

Channel Selection & Retries

Layer / File(s) Summary
Excluding-aware model selection
model/ability.go, model/channel_cache.go
Adds GetChannelExcluding and GetRandomSatisfiedChannelExcluding to filter excluded channel IDs, recalc priorities, and perform weighted selection over filtered candidates.
RetryParam wiring & controller updates
service/channel_select.go, controller/relay.go
RetryParam now includes Ctx and ExcludeChannelIds; Relay and RelayTask initialize and mark used channel IDs so retries avoid them; service selection calls pass exclusions.

Relay request handling

Layer / File(s) Summary
Buffer request body for replay
relay/channel/api_request.go, relay/channel/api_request_test.go
Buffer outbound request body bytes, make GetBody return fresh reader; add test to confirm body replay across redirects.
Doubao adaptor & parsing
relay/channel/task/doubao/adaptor.go, .../adaptor_test.go
Convert metadata first, use ImageInputs, compute resolution/aspect/duration, preserve image roles, and add unit tests.
Task JSON parsing & encoding
relay/common/relay_info.go, relay/common/relay_utils.go, relay/common/relay_utils_test.go
Add ImageInputs shape handling, width/height/fps fields, robust UnmarshalJSON for TaskImageInput, GB18030 JSON decode fallback, and tests validating decoding and image parsing.

Video-seconds billing & pricing

Layer / File(s) Summary
Billing settings & model pricing
setting/billing_setting/tiered_billing.go, model/pricing.go
Add VideoPriceConfig and billing_setting.video_price map; persist and expose per-model video pricing; populate model Pricing.VideoPrice when applicable.
Server-side price helpers & trace
relay/helper/price.go, types/price_data.go, relay/helper/price_test.go
Add video-seconds billing flow, compute per-second pricing with FPS multiplier, populate VideoSecondsTrace, and add unit tests.
Logging & task records
service/task_billing.go, web/default/src/features/usage-logs/*
Log video billing metadata in consumption logs; usage logs and details dialog display video billing rows and download links for result_url.

Dashboard Token Consumption Metrics

Layer / File(s) Summary
Type definitions for token metrics
web/default/src/features/dashboard/types.ts
Adds ConsumptionDistributionMetric and token-based spec fields (spec_token_line, spec_token_area, totalTokensDisplay, user token specs).
Chart processing: token series
web/default/src/features/dashboard/lib/charts.ts
Compute token aggregates, per-time token series, user token rank/trend specs, and token-specific tooltip formatting.
Chart components metric UI
web/default/src/features/dashboard/components/*
Add metric toggle UI and update chart rendering to select metric-specific specs and include metric in chart key.

Pricing, Wallet & Subscriptions

Layer / File(s) Summary
Pricing helpers & format pipeline
web/default/src/features/pricing/lib/price.ts
Refactor pricing formatting to use wallet/payment currency config; adjust format helpers to delegate to centralized formatting.
Dynamic-price & use-pricing-data
web/default/src/features/pricing/lib/dynamic-price.ts, web/default/src/features/pricing/hooks/use-pricing-data.ts
Dynamic-price uses wallet payment formatting; exchange-rate selection moved to system currency config.
Wallet helpers & components
web/default/src/features/wallet/lib/format.ts, .../recharge-form-card.tsx, .../payment-confirm-dialog.tsx, index.tsx
Add getWalletCurrencyConfig and formatWalletCurrencyAmount; pass currency-symbol props into components; move AffiliateRewardsCard position.
Subscriptions price formatting
web/default/src/features/subscriptions/lib/format.ts, .../subscriptions-columns.tsx, subscription-purchase-dialog.tsx
Add formatSubscriptionPrice and use it in subscriptions columns and purchase dialog; show localized payment amounts.

Frontend UI & Auth

Layer / File(s) Summary
Chat Presets Menu Simplification
web/default/src/components/layout/components/chat-presets-item.tsx
Refactor ChatPresetsItem to parameterless component rendering flat list; remove dropdown/collapsible UI.
NavGroup integration
web/default/src/components/layout/components/nav-group.tsx
Render ChatPresetsItem without item prop.
Auth affiliate field & affiliate link
web/default/src/features/auth/types.ts, .../sign-up-form.tsx, web/default/src/features/wallet/lib/affiliate.ts
Rename affaff_code and change affiliate path to /sign-up.
Notifications auto-open
web/default/src/hooks/use-notifications.ts
Auto-open notifications dialog for new unread notices/announcements with deduplication across instances.

Models & System Settings

Layer / File(s) Summary
Model pricing UI & ratio persistence
web/default/src/features/system-settings/models/*, web/default/src/features/pricing/components/*
Add video_seconds mode across model pricing editors, visual editor, ratio persistence under billing_setting.video_price, and pricing display changes across model list/details.
Model mutate drawer simplification
web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
Remove embedded pricing/ratio configuration from model create/update drawer; simplify form initialization and submit flow.

Usage Logs & Details Dialog

Layer / File(s) Summary
Usage log columns & details dialog
web/default/src/features/usage-logs/*
Add video billing detail segment and result_url download link; update BillingBreakdown to show video-specific rows/formula and hide quota total when video billing is active.

i18n

Layer / File(s) Summary
Locale updates
web/default/src/i18n/locales/*
Add many translation keys for video pricing, FPS, rankings display multiplier/jitter, download labels, AI-translation UI, and minor reordering across multiple locale files.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • Calcium-Ion
  • seefs001
  • creamlike1024

🐰
I nibble code and headers through the night,
Channels hop to GHCR and tokens come to light,
Wallets wear new symbols, presets trim their frills,
Alerts pop once — the rabbit's happy with these drills.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (2)
web/default/src/features/auth/sign-up/components/sign-up-form.tsx (1)

153-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize affiliate code before sending payload.

At Line 158, aff_code is always sent even when empty. Normalize it to undefined (same pattern as email and verification_code) to avoid backend validation edge cases.

Suggested patch
     try {
+      const affiliateCode = getAffiliateCode()?.trim()
       const res = await register({
         username: data.username,
         password: data.password,
         email: data.email || undefined,
         verification_code: verificationCode || undefined,
-        aff_code: getAffiliateCode(),
+        aff_code: affiliateCode || undefined,
         turnstile: turnstileToken,
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/auth/sign-up/components/sign-up-form.tsx` around
lines 153 - 159, The register payload always sends an aff_code even when empty;
update the payload construction in sign-up-form.tsx where register(...) is
called (the object including username, password, email, verification_code,
aff_code, turnstile) to normalize aff_code the same way email and
verification_code are normalized — use the result of getAffiliateCode() but pass
undefined when it returns a falsy/empty value so aff_code is omitted from the
request payload.
.github/workflows/docker-image-alpha.yml (1)

44-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make VERSION deterministic across matrix/jobs.

Line 47 and Line 128 compute VERSION from current time (date +%Y%m%d) independently. If jobs cross midnight UTC, per-arch tags and manifest tags can diverge and the manifest step will fail to find one of the images.

Suggested fix
-          VERSION="${CHANNEL}-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
+          BUILD_DATE="$(git show -s --format=%cd --date=format:%Y%m%d HEAD)"
+          VERSION="${CHANNEL}-${BUILD_DATE}-$(git rev-parse --short HEAD)"

Apply the same change in both Determine channel version steps.

Also applies to: 125-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-image-alpha.yml around lines 44 - 51, The VERSION
calculation must be deterministic across matrix jobs; update the "Determine
channel version" step (id: version) in both places so it does not use date, e.g.
compute VERSION using a workflow-stable value such as github.run_id or
github.run_number combined with the commit hash (for example:
VERSION="${CHANNEL}-${{ github.run_id }}-$(git rev-parse --short HEAD)"), then
write it to VERSION, $GITHUB_OUTPUT and $GITHUB_ENV exactly as before; apply
this same change to the second "Determine channel version" occurrence so all
jobs use the identical VERSION.
🧹 Nitpick comments (2)
model/option.go (1)

188-190: ⚡ Quick win

Add observability when DB option loading is intentionally skipped.

When loadFromDatabase is false, startup behavior changes silently. A one-line log here will make operational diagnosis much easier.

Proposed diff
 if len(loadFromDatabase) == 0 || loadFromDatabase[0] {
 	loadOptionsFromDatabase()
+} else {
+	common.SysLog("skipping option load from database")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/option.go` around lines 188 - 190, When the conditional that decides
whether to call loadOptionsFromDatabase() skips DB loading because
loadFromDatabase is present and false, add a one-line log indicating that DB
option loading was intentionally skipped; place this log in the same branch that
currently avoids calling loadOptionsFromDatabase() and reference
loadFromDatabase and loadOptionsFromDatabase in your change so the startup log
clearly records the skip event.
web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx (1)

129-140: ⚡ Quick win

Replace the multi-level ternary for spec selection.

This new branch is harder to scan and violates the repo rule against nested ternaries with 2+ levels. A small lookup or if/else block would keep the metric/chart-type switch clearer.

♻️ Suggested cleanup
-  const spec =
-    metric === 'tokens'
-      ? chartType === 'bar'
-        ? chartData.spec_token_line
-        : chartData.spec_token_area
-      : chartType === 'bar'
-        ? chartData.spec_line
-        : chartData.spec_area
+  let spec = chartData.spec_line
+  if (metric === 'tokens') {
+    spec =
+      chartType === 'bar'
+        ? chartData.spec_token_line
+        : chartData.spec_token_area
+  } else {
+    spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
+  }

As per coding guidelines, Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead; single-level ternaries are acceptable if concise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx`
around lines 129 - 140, Replace the nested ternary used to compute spec (and
mirror for totalDisplay) with a clear if/else or a small lookup map to avoid 2+
level nested ternaries; locate the logic that sets spec (currently using metric,
chartType and chartData.spec_*) and rewrite it into either a switch/if-else
block or a lookup like const specMap = { tokens: { bar:
chartData.spec_token_line, area: chartData.spec_token_area }, default: { bar:
chartData.spec_line, area: chartData.spec_area } } and then select spec =
(metric === 'tokens' ? specMap.tokens : specMap.default)[chartType]; also update
totalDisplay selection similarly (use metric to pick
chartData.totalTokensDisplay or chartData.totalQuotaDisplay) so the code is
easier to read and conforms to the no-nested-ternaries rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/docker-image-alpha.yml:
- Around line 38-42: The job currently sets CHANNEL="${GITHUB_REF_NAME:-alpha}"
(in the "Determine image channel" step and the other job around lines 119-123)
which can pick up arbitrary branch names when triggered via workflow_dispatch;
update both places to validate GITHUB_REF_NAME against an explicit allowlist
(e.g., only accept "alpha", "beta", "stable"/"prod" or whatever channels you
support) and fallback to "alpha" otherwise, or require workflow inputs for
dispatch; implement the same guard logic in both jobs so CHANNEL is set only to
a permitted value and never directly taken from GITHUB_REF_NAME.

In `@README.md`:
- Line 2: Remove the accidental test text "测试下这是我自己的修改" from README by deleting
or replacing the content on Line 2; update the English README so it contains
only intended project documentation (or a proper English sentence) and ensure no
leftover personal/debug placeholder strings remain.

In
`@web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx`:
- Around line 163-204: The card title is static and doesn't reflect when metric
=== 'tokens'; update the heading in the ConsumptionDistributionChart component
to derive its text from the current metric (the metric state used with setMetric
and METRIC_OPTIONS) — e.g., choose the appropriate translation key via t(...) or
map METRIC_OPTIONS to a title and render that instead of the hardcoded "Quota
Distribution" so the title changes to the token-specific label when metric ===
'tokens'.

In `@web/default/src/features/dashboard/components/users/user-charts.tsx`:
- Around line 49-69: USER_CHARTS currently stores a single static labelKey
("User Consumption Ranking/Trend") so the card header stays quota-oriented in
token mode; update the data shape and usage so labels are metric-aware: change
USER_CHARTS to hold per-metric labels (e.g. labelKey: { quota: 'User Consumption
Ranking', tokens: 'User Token Ranking' } or similar) alongside specKeys, then
update the render path that builds the card header (the component rendering user
charts that reads USER_CHARTS) to pick the appropriate label for the current
metric mode (quota vs tokens) instead of using the single labelKey; reference
USER_CHARTS, labelKey, specKeys and the render code that sets the card title to
make the header follow the selected metric.

In `@web/default/src/features/wallet/lib/affiliate.ts`:
- Line 28: The invite URL currently interpolates affCode directly into the query
string (`${window.location.origin}/sign-up?aff=${affCode}`), which can break
when affCode contains reserved characters; update the return to URL-encode the
affiliate code using encodeURIComponent (i.e., replace the raw affCode
interpolation with encodeURIComponent(affCode)) so the generated link is always
valid while keeping window.location.origin and the /sign-up?aff= parameter
intact.

In `@web/default/src/hooks/use-notifications.ts`:
- Around line 60-61: The global Set autoOpenedNotificationSignatures is never
bounded or reset, causing signatures to be blocked for the tab lifetime and the
Set to grow unbounded; replace the module-level Set with a bounded cache (e.g.,
a Map<string,number> storing timestamps or an LRU structure) and update the code
paths that currently call autoOpenedNotificationSignatures.has(signature) /
.add(signature) to first prune expired entries or evict oldest entries when size
limit is exceeded; additionally register a reset (clear) on session
end/visibilitychange/unload or on hook unmount to ensure long-lived tabs don’t
retain stale signatures.
- Around line 126-141: The autoOpenSignature currently serializes
unreadAnnouncementKeys (derived from announcements via getAnnouncementKey and
isAnnouncementRead) in arrival/backend order which can change and cause spurious
signature differences; fix by normalizing the unread keys before serialization:
derive unreadAnnouncementKeys using getAnnouncementKey and isAnnouncementRead as
now, then sort (stable, e.g., localeCompare) and deduplicate if needed before
passing into autoOpenSignature so the signature is deterministic regardless of
backend ordering; update references to unreadAnnouncementKeys used in
autoOpenSignature to use the normalized (sorted) list.

---

Outside diff comments:
In @.github/workflows/docker-image-alpha.yml:
- Around line 44-51: The VERSION calculation must be deterministic across matrix
jobs; update the "Determine channel version" step (id: version) in both places
so it does not use date, e.g. compute VERSION using a workflow-stable value such
as github.run_id or github.run_number combined with the commit hash (for
example: VERSION="${CHANNEL}-${{ github.run_id }}-$(git rev-parse --short
HEAD)"), then write it to VERSION, $GITHUB_OUTPUT and $GITHUB_ENV exactly as
before; apply this same change to the second "Determine channel version"
occurrence so all jobs use the identical VERSION.

In `@web/default/src/features/auth/sign-up/components/sign-up-form.tsx`:
- Around line 153-159: The register payload always sends an aff_code even when
empty; update the payload construction in sign-up-form.tsx where register(...)
is called (the object including username, password, email, verification_code,
aff_code, turnstile) to normalize aff_code the same way email and
verification_code are normalized — use the result of getAffiliateCode() but pass
undefined when it returns a falsy/empty value so aff_code is omitted from the
request payload.

---

Nitpick comments:
In `@model/option.go`:
- Around line 188-190: When the conditional that decides whether to call
loadOptionsFromDatabase() skips DB loading because loadFromDatabase is present
and false, add a one-line log indicating that DB option loading was
intentionally skipped; place this log in the same branch that currently avoids
calling loadOptionsFromDatabase() and reference loadFromDatabase and
loadOptionsFromDatabase in your change so the startup log clearly records the
skip event.

In
`@web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx`:
- Around line 129-140: Replace the nested ternary used to compute spec (and
mirror for totalDisplay) with a clear if/else or a small lookup map to avoid 2+
level nested ternaries; locate the logic that sets spec (currently using metric,
chartType and chartData.spec_*) and rewrite it into either a switch/if-else
block or a lookup like const specMap = { tokens: { bar:
chartData.spec_token_line, area: chartData.spec_token_area }, default: { bar:
chartData.spec_line, area: chartData.spec_area } } and then select spec =
(metric === 'tokens' ? specMap.tokens : specMap.default)[chartType]; also update
totalDisplay selection similarly (use metric to pick
chartData.totalTokensDisplay or chartData.totalQuotaDisplay) so the code is
easier to read and conforms to the no-nested-ternaries rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da61beb5-91f3-4229-a0de-3239a6437c53

📥 Commits

Reviewing files that changed from the base of the PR and between ba47439 and 8852083.

⛔ Files ignored due to path filters (2)
  • web/classic/bun.lock is excluded by !**/*.lock
  • web/default/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .github/workflows/docker-image-alpha.yml
  • README.md
  • controller/user.go
  • main.go
  • model/main.go
  • model/option.go
  • web/default/src/components/layout/components/chat-presets-item.tsx
  • web/default/src/components/layout/components/nav-group.tsx
  • web/default/src/features/auth/sign-up/components/sign-up-form.tsx
  • web/default/src/features/auth/types.ts
  • web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx
  • web/default/src/features/dashboard/components/users/user-charts.tsx
  • web/default/src/features/dashboard/lib/charts.ts
  • web/default/src/features/dashboard/types.ts
  • web/default/src/features/wallet/index.tsx
  • web/default/src/features/wallet/lib/affiliate.ts
  • web/default/src/hooks/use-notifications.ts

Comment on lines +38 to +42
- name: Determine image channel
run: |
CHANNEL="${GITHUB_REF_NAME:-alpha}"
echo "CHANNEL=$CHANNEL" >> $GITHUB_ENV
echo "Publishing channel: $CHANNEL"

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 | ⚡ Quick win

Validate channel before publishing tags.

Line 40 and Line 121 use GITHUB_REF_NAME directly as channel. With workflow_dispatch, this can publish unexpected channels (for example from a non-release branch) and produce unintended image tags.

Suggested fix
-          CHANNEL="${GITHUB_REF_NAME:-alpha}"
+          RAW_CHANNEL="${GITHUB_REF_NAME:-alpha}"
+          case "$RAW_CHANNEL" in
+            alpha|cooper) CHANNEL="$RAW_CHANNEL" ;;
+            *) echo "Unsupported publish channel: $RAW_CHANNEL"; exit 1 ;;
+          esac
           echo "CHANNEL=$CHANNEL" >> $GITHUB_ENV

Use the same guard in both jobs.

Also applies to: 119-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-image-alpha.yml around lines 38 - 42, The job
currently sets CHANNEL="${GITHUB_REF_NAME:-alpha}" (in the "Determine image
channel" step and the other job around lines 119-123) which can pick up
arbitrary branch names when triggered via workflow_dispatch; update both places
to validate GITHUB_REF_NAME against an explicit allowlist (e.g., only accept
"alpha", "beta", "stable"/"prod" or whatever channels you support) and fallback
to "alpha" otherwise, or require workflow inputs for dispatch; implement the
same guard logic in both jobs so CHANNEL is set only to a permitted value and
never directly taken from GITHUB_REF_NAME.

Comment thread README.md
Comment on lines +163 to +204
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
<div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-lg border p-0.5 sm:h-8 sm:w-auto'>
{METRIC_OPTIONS.map((item) => {
const Icon = item.icon
return (
<button
key={item.value}
type='button'
onClick={() => setMetric(item.value)}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 text-xs font-medium transition-colors ${
metric === item.value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className='size-3.5' />
{t(item.labelKey)}
</button>
)
})}
</div>

<div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-lg border p-0.5 sm:h-8 sm:w-auto'>
{CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
const Icon = CHART_TYPE_ICONS[item.value]
return (
<button
key={item.value}
type='button'
onClick={() => setChartType(item.value)}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 text-xs font-medium transition-colors ${
chartType === item.value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className='size-3.5' />
{t(item.labelKey)}
</button>
)
})}
</div>

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 | ⚡ Quick win

Update the card title when token mode is selected.

With this toggle in place, the fixed heading at Line 157 still says Quota Distribution, so the card title is wrong whenever metric === 'tokens'.

💡 Suggested fix
-          <div className='text-sm font-semibold'>{t('Quota Distribution')}</div>
+          <div className='text-sm font-semibold'>
+            {metric === 'tokens'
+              ? t('Token Distribution')
+              : t('Quota Distribution')}
+          </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx`
around lines 163 - 204, The card title is static and doesn't reflect when metric
=== 'tokens'; update the heading in the ConsumptionDistributionChart component
to derive its text from the current metric (the metric state used with setMetric
and METRIC_OPTIONS) — e.g., choose the appropriate translation key via t(...) or
map METRIC_OPTIONS to a title and render that instead of the hardcoded "Quota
Distribution" so the title changes to the token-specific label when metric ===
'tokens'.

Comment on lines 49 to 69
const USER_CHARTS: {
value: string
labelKey: string
specKey: keyof ProcessedUserChartData
specKeys: Record<ConsumptionDistributionMetric, keyof ProcessedUserChartData>
}[] = [
{
value: 'rank',
labelKey: 'User Consumption Ranking',
specKey: 'spec_user_rank',
specKeys: {
quota: 'spec_user_rank',
tokens: 'spec_user_token_rank',
},
},
{
value: 'trend',
labelKey: 'User Consumption Trend',
specKey: 'spec_user_trend',
specKeys: {
quota: 'spec_user_trend',
tokens: 'spec_user_token_trend',
},
},

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 | ⚡ Quick win

Make the card header metric-aware too.

These labels stay quota-oriented, so in token mode the outer card header still says User Consumption Ranking/Trend while the chart title switches to token wording. That leaves the UI inconsistent.

💡 Suggested fix
 const USER_CHARTS: {
   value: string
-  labelKey: string
+  labelKeys: Record<ConsumptionDistributionMetric, string>
   specKeys: Record<ConsumptionDistributionMetric, keyof ProcessedUserChartData>
 }[] = [
   {
     value: 'rank',
-    labelKey: 'User Consumption Ranking',
+    labelKeys: {
+      quota: 'User Consumption Ranking',
+      tokens: 'User Token Consumption Ranking',
+    },
     specKeys: {
       quota: 'spec_user_rank',
       tokens: 'spec_user_token_rank',
@@
   {
     value: 'trend',
-    labelKey: 'User Consumption Trend',
+    labelKeys: {
+      quota: 'User Consumption Trend',
+      tokens: 'User Token Consumption Trend',
+    },
     specKeys: {
       quota: 'spec_user_trend',
       tokens: 'spec_user_token_trend',

And in the render path:

-                <div className='text-sm font-semibold'>{t(chart.labelKey)}</div>
+                <div className='text-sm font-semibold'>
+                  {t(chart.labelKeys[metric])}
+                </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/components/users/user-charts.tsx` around
lines 49 - 69, USER_CHARTS currently stores a single static labelKey ("User
Consumption Ranking/Trend") so the card header stays quota-oriented in token
mode; update the data shape and usage so labels are metric-aware: change
USER_CHARTS to hold per-metric labels (e.g. labelKey: { quota: 'User Consumption
Ranking', tokens: 'User Token Ranking' } or similar) alongside specKeys, then
update the render path that builds the card header (the component rendering user
charts that reads USER_CHARTS) to pick the appropriate label for the current
metric mode (quota vs tokens) instead of using the single labelKey; reference
USER_CHARTS, labelKey, specKeys and the render code that sets the card title to
make the header follow the selected metric.

export function generateAffiliateLink(affCode: string): string {
if (typeof window === 'undefined') return ''
return `${window.location.origin}/register?aff=${affCode}`
return `${window.location.origin}/sign-up?aff=${affCode}`

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 | ⚡ Quick win

Encode affCode when generating the invite URL.

At Line 28, direct string interpolation can produce invalid links when affCode contains reserved URL characters.

Suggested patch
 export function generateAffiliateLink(affCode: string): string {
   if (typeof window === 'undefined') return ''
-  return `${window.location.origin}/sign-up?aff=${affCode}`
+  const url = new URL('/sign-up', window.location.origin)
+  url.searchParams.set('aff', affCode)
+  return url.toString()
 }
📝 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
return `${window.location.origin}/sign-up?aff=${affCode}`
export function generateAffiliateLink(affCode: string): string {
if (typeof window === 'undefined') return ''
const url = new URL('/sign-up', window.location.origin)
url.searchParams.set('aff', affCode)
return url.toString()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/wallet/lib/affiliate.ts` at line 28, The invite URL
currently interpolates affCode directly into the query string
(`${window.location.origin}/sign-up?aff=${affCode}`), which can break when
affCode contains reserved characters; update the return to URL-encode the
affiliate code using encodeURIComponent (i.e., replace the raw affCode
interpolation with encodeURIComponent(affCode)) so the generated link is always
valid while keeping window.location.origin and the /sign-up?aff= parameter
intact.

Comment on lines +60 to +61
const autoOpenedNotificationSignatures = new Set<string>()

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 | ⚡ Quick win

Bound and reset the global signature cache.

The module-level Set at Line 60 is never reset. With the guard at Line 162, previously seen signatures stay blocked for the full tab lifetime (including next-day sessions in long-lived tabs), and the Set can grow unbounded.

Proposed fix
 const autoOpenedNotificationSignatures = new Set<string>()
+let autoOpenedSignatureDay = new Date().toDateString()
+const MAX_AUTO_OPEN_SIGNATURES = 200

 useEffect(() => {
+  const today = new Date().toDateString()
+  if (autoOpenedSignatureDay !== today) {
+    autoOpenedNotificationSignatures.clear()
+    autoOpenedSignatureDay = today
+  }
+
   if (
     noticeLoading ||
     statusLoading ||
     dialogOpen ||
     !autoOpenSignature ||
     isNoticeClosed() ||
     autoOpenRef.current === autoOpenSignature ||
     autoOpenedNotificationSignatures.has(autoOpenSignature)
   ) {
     return
   }

   autoOpenRef.current = autoOpenSignature
   autoOpenedNotificationSignatures.add(autoOpenSignature)
+  if (autoOpenedNotificationSignatures.size > MAX_AUTO_OPEN_SIGNATURES) {
+    const first = autoOpenedNotificationSignatures.values().next().value
+    if (first) autoOpenedNotificationSignatures.delete(first)
+  }
   handleOpenDialog(unreadCounts.notice > 0 ? 'notice' : 'announcements')
 }, [
   autoOpenSignature,
   dialogOpen,
   noticeLoading,
   statusLoading,
   unreadCounts.notice,
   isNoticeClosed,
 ])

Also applies to: 154-169

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/hooks/use-notifications.ts` around lines 60 - 61, The global
Set autoOpenedNotificationSignatures is never bounded or reset, causing
signatures to be blocked for the tab lifetime and the Set to grow unbounded;
replace the module-level Set with a bounded cache (e.g., a Map<string,number>
storing timestamps or an LRU structure) and update the code paths that currently
call autoOpenedNotificationSignatures.has(signature) / .add(signature) to first
prune expired entries or evict oldest entries when size limit is exceeded;
additionally register a reset (clear) on session end/visibilitychange/unload or
on hook unmount to ensure long-lived tabs don’t retain stale signatures.

Comment on lines +126 to +141
const unreadAnnouncementKeys = useMemo(
() =>
announcements
.map((item: Record<string, unknown>) => getAnnouncementKey(item))
.filter((key) => key && !isAnnouncementRead(key)),
[announcements, isAnnouncementRead]
)

const autoOpenSignature = useMemo(() => {
if (unreadCounts.total === 0) return ''

return JSON.stringify({
notice: unreadCounts.notice > 0 ? noticeContent : '',
announcements: unreadAnnouncementKeys,
})
}, [noticeContent, unreadAnnouncementKeys, unreadCounts])

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 | ⚡ Quick win

Stabilize signature generation for unread announcements.

At Line 139, unreadAnnouncementKeys is serialized in arrival order. If backend ordering changes, the same unread set generates a different signature and can auto-open again unexpectedly.

Proposed fix
   const unreadAnnouncementKeys = useMemo(
     () =>
       announcements
         .map((item: Record<string, unknown>) => getAnnouncementKey(item))
         .filter((key) => key && !isAnnouncementRead(key)),
     [announcements, isAnnouncementRead]
   )

   const autoOpenSignature = useMemo(() => {
     if (unreadCounts.total === 0) return ''

+    const normalizedAnnouncementKeys = [...unreadAnnouncementKeys].sort()
+
     return JSON.stringify({
       notice: unreadCounts.notice > 0 ? noticeContent : '',
-      announcements: unreadAnnouncementKeys,
+      announcements: normalizedAnnouncementKeys,
     })
   }, [noticeContent, unreadAnnouncementKeys, unreadCounts])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/hooks/use-notifications.ts` around lines 126 - 141, The
autoOpenSignature currently serializes unreadAnnouncementKeys (derived from
announcements via getAnnouncementKey and isAnnouncementRead) in arrival/backend
order which can change and cause spurious signature differences; fix by
normalizing the unread keys before serialization: derive unreadAnnouncementKeys
using getAnnouncementKey and isAnnouncementRead as now, then sort (stable, e.g.,
localeCompare) and deduplicate if needed before passing into autoOpenSignature
so the signature is deterministic regardless of backend ordering; update
references to unreadAnnouncementKeys used in autoOpenSignature to use the
normalized (sorted) list.

在 relay 重试过程中记录已使用渠道,避免 auto 跨分组或后续重试重复选择同一渠道。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@model/ability.go`:
- Around line 133-155: The code dereferences ability_.Priority without nil
checks in two places; update both loops to guard ability_.Priority != nil before
using *ability_.Priority: when populating uniquePriorities (only add
uniquePriorities[int(*ability_.Priority)] if ability_.Priority != nil) and when
filtering abilities by targetPriority (skip abilities with nil Priority or treat
nil as the chosen default consistently), so you avoid panics during retry
selection while preserving the existing retry/priorities logic (adjust
availableAbilities, priorities, and the final comparison to use the guarded
value).

In `@service/channel_select.go`:
- Around line 119-120: Replace the ignored error from
GetRandomSatisfiedChannelExcluding: capture the returned error (e.g., channel,
err := model.GetRandomSatisfiedChannelExcluding(autoGroup, param.ModelName,
priorityRetry, param.ExcludeChannelIds)), then handle it instead of discarding —
for non-nil err either propagate it up (return err) or log and treat it as a
real selection failure rather than silently falling through; ensure callers of
this code (the auto-group retry path) can distinguish "no channel found" from an
actual selector error. Include references to GetRandomSatisfiedChannelExcluding,
channel, err, autoGroup, priorityRetry, and param.ExcludeChannelIds when making
the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 843ccd87-0156-42be-86ab-620685133976

📥 Commits

Reviewing files that changed from the base of the PR and between 8852083 and 05c4161.

📒 Files selected for processing (4)
  • controller/relay.go
  • model/ability.go
  • model/channel_cache.go
  • service/channel_select.go

Comment thread model/ability.go
Comment on lines +133 to +155
for _, ability_ := range abilities {
if excludeChannelIds[ability_.ChannelId] {
continue
}
availableAbilities = append(availableAbilities, ability_)
uniquePriorities[int(*ability_.Priority)] = true
}
if len(availableAbilities) == 0 {
return nil, nil
}
priorities := make([]int, 0, len(uniquePriorities))
for priority := range uniquePriorities {
priorities = append(priorities, priority)
}
sort.Sort(sort.Reverse(sort.IntSlice(priorities)))
if retry >= len(priorities) {
retry = len(priorities) - 1
}
targetPriority := priorities[retry]
abilities = abilities[:0]
for _, ability_ := range availableAbilities {
if int(*ability_.Priority) == targetPriority {
abilities = append(abilities, ability_)

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 | ⚡ Quick win

Guard nil Priority before dereferencing in exclusion path.

Line 138 and Line 154 dereference ability_.Priority directly. If any row has NULL priority, this can panic during retry selection.

Suggested fix
 		for _, ability_ := range abilities {
 			if excludeChannelIds[ability_.ChannelId] {
 				continue
 			}
 			availableAbilities = append(availableAbilities, ability_)
-			uniquePriorities[int(*ability_.Priority)] = true
+			priority := 0
+			if ability_.Priority != nil {
+				priority = int(*ability_.Priority)
+			}
+			uniquePriorities[priority] = true
 		}
@@
 		abilities = abilities[:0]
 		for _, ability_ := range availableAbilities {
-			if int(*ability_.Priority) == targetPriority {
+			currentPriority := 0
+			if ability_.Priority != nil {
+				currentPriority = int(*ability_.Priority)
+			}
+			if currentPriority == targetPriority {
 				abilities = append(abilities, ability_)
 			}
 		}
📝 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
for _, ability_ := range abilities {
if excludeChannelIds[ability_.ChannelId] {
continue
}
availableAbilities = append(availableAbilities, ability_)
uniquePriorities[int(*ability_.Priority)] = true
}
if len(availableAbilities) == 0 {
return nil, nil
}
priorities := make([]int, 0, len(uniquePriorities))
for priority := range uniquePriorities {
priorities = append(priorities, priority)
}
sort.Sort(sort.Reverse(sort.IntSlice(priorities)))
if retry >= len(priorities) {
retry = len(priorities) - 1
}
targetPriority := priorities[retry]
abilities = abilities[:0]
for _, ability_ := range availableAbilities {
if int(*ability_.Priority) == targetPriority {
abilities = append(abilities, ability_)
for _, ability_ := range abilities {
if excludeChannelIds[ability_.ChannelId] {
continue
}
availableAbilities = append(availableAbilities, ability_)
priority := 0
if ability_.Priority != nil {
priority = int(*ability_.Priority)
}
uniquePriorities[priority] = true
}
if len(availableAbilities) == 0 {
return nil, nil
}
priorities := make([]int, 0, len(uniquePriorities))
for priority := range uniquePriorities {
priorities = append(priorities, priority)
}
sort.Sort(sort.Reverse(sort.IntSlice(priorities)))
if retry >= len(priorities) {
retry = len(priorities) - 1
}
targetPriority := priorities[retry]
abilities = abilities[:0]
for _, ability_ := range availableAbilities {
currentPriority := 0
if ability_.Priority != nil {
currentPriority = int(*ability_.Priority)
}
if currentPriority == targetPriority {
abilities = append(abilities, ability_)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/ability.go` around lines 133 - 155, The code dereferences
ability_.Priority without nil checks in two places; update both loops to guard
ability_.Priority != nil before using *ability_.Priority: when populating
uniquePriorities (only add uniquePriorities[int(*ability_.Priority)] if
ability_.Priority != nil) and when filtering abilities by targetPriority (skip
abilities with nil Priority or treat nil as the chosen default consistently), so
you avoid panics during retry selection while preserving the existing
retry/priorities logic (adjust availableAbilities, priorities, and the final
comparison to use the guarded value).

Comment thread service/channel_select.go
Comment on lines +119 to 120
channel, _ = model.GetRandomSatisfiedChannelExcluding(autoGroup, param.ModelName, priorityRetry, param.ExcludeChannelIds)
if channel == 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 | 🟠 Major | ⚡ Quick win

Don’t drop selector errors in auto-group retry path.

Line 119 ignores GetRandomSatisfiedChannelExcluding errors, which can mask real selection failures and incorrectly fall through to next group.

Suggested fix
-			channel, _ = model.GetRandomSatisfiedChannelExcluding(autoGroup, param.ModelName, priorityRetry, param.ExcludeChannelIds)
+			channel, err = model.GetRandomSatisfiedChannelExcluding(autoGroup, param.ModelName, priorityRetry, param.ExcludeChannelIds)
+			if err != nil {
+				return nil, autoGroup, err
+			}
 			if channel == nil {
📝 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
channel, _ = model.GetRandomSatisfiedChannelExcluding(autoGroup, param.ModelName, priorityRetry, param.ExcludeChannelIds)
if channel == nil {
channel, err = model.GetRandomSatisfiedChannelExcluding(autoGroup, param.ModelName, priorityRetry, param.ExcludeChannelIds)
if err != nil {
return nil, autoGroup, err
}
if channel == nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/channel_select.go` around lines 119 - 120, Replace the ignored error
from GetRandomSatisfiedChannelExcluding: capture the returned error (e.g.,
channel, err := model.GetRandomSatisfiedChannelExcluding(autoGroup,
param.ModelName, priorityRetry, param.ExcludeChannelIds)), then handle it
instead of discarding — for non-nil err either propagate it up (return err) or
log and treat it as a real selection failure rather than silently falling
through; ensure callers of this code (the auto-group retry path) can distinguish
"no channel found" from an actual selector error. Include references to
GetRandomSatisfiedChannelExcluding, channel, err, autoGroup, priorityRetry, and
param.ExcludeChannelIds when making the change.

2、优化了订阅套餐价格的显示。目前默认显示平台展示货币,点击支付后会更具支付系数使用支付货币价格;立即订阅弹窗中将总额度以平台展示货币的形式展现,而不再是TOKEN数值;立即订阅弹窗中应付金额添加实际应支付的货币金额。
3、

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (6)
web/default/src/features/wallet/lib/format.ts (3)

21-21: ⚡ Quick win

Document the hardcoded exchange rate constant.

DEFAULT_PAYMENT_EXCHANGE_RATE = 7 appears to be a CNY/USD exchange rate fallback, but it's undocumented and will become stale over time as real exchange rates fluctuate.

Add a comment explaining:

  • Why this specific value was chosen
  • What currency pair it represents
  • When it's used as a fallback
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/wallet/lib/format.ts` at line 21, Add a clarifying
comment above the DEFAULT_PAYMENT_EXCHANGE_RATE constant explaining that the
value 7 is a hardcoded fallback representing the approximate CNY to USD exchange
rate (CNY per USD), why 7 was chosen (approximate historical/round figure), and
that it is only used as a fallback when no real-time exchange rate is available
(e.g., when the rate fetch in formatPayment()/formatCurrency() fails). Update
the comment to mention that this fallback should be replaced by live rates and
where to look to change it (the DEFAULT_PAYMENT_EXCHANGE_RATE constant) so
future maintainers know its purpose and limitations.

107-112: 💤 Low value

Fix misleading comment: returns CNY config, not JPY.

The default return case (lines 107-112) uses the Chinese Yuan symbol ¥ and is labeled "JPY-style" in earlier context. While both CNY and JPY share the ¥ symbol, the function clearly implements CNY behavior (using effectiveUsdExchangeRate as the rate).

Update any associated comments to clarify this returns CNY-style configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/wallet/lib/format.ts` around lines 107 - 112, The
default return object currently uses the Yuan symbol and CNY behavior but is
mislabeled as "JPY-style" in the surrounding comment; update the comment that
describes this fallback to say "CNY-style" (or "Chinese Yuan") and clarify that
the returned object (fields symbol, rate, paymentSymbol, paymentRate using
effectiveUsdExchangeRate) represents CNY behavior rather than JPY so readers
won't be misled.

85-92: ⚡ Quick win

Fragile assumption that exchange rate of 1 implies USD mode.

Line 89 uses effectiveUsdExchangeRate === 1 to decide whether to display $ or ¥. This assumption breaks if any other currency legitimately has an exchange rate of 1 relative to USD, or if USD itself is configured with a non-1 multiplier for business reasons.

Consider using an explicit currency-type indicator rather than inferring from the numeric rate value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/wallet/lib/format.ts` around lines 85 - 92, The code
currently infers paymentSymbol from numeric effectiveUsdExchangeRate (in the
quotaDisplayType === 'USD' branch), which is fragile; instead, change the logic
to rely on an explicit currency indicator (e.g., a provided quotaPaymentCurrency
or baseCurrency flag) rather than comparing effectiveUsdExchangeRate === 1.
Update the function that returns paymentSymbol/paymentRate (references:
quotaDisplayType, effectiveUsdExchangeRate, paymentSymbol, paymentRate) to
accept or derive an explicit payment currency value and use that to decide
whether to return '$' or '¥', leaving paymentRate set from
effectiveUsdExchangeRate; if no explicit currency exists, add and propagate a
new parameter or use an existing currency field from the caller.
web/default/src/features/pricing/lib/dynamic-price.ts (1)

85-101: ⚡ Quick win

Document direct store access in utility function.

formatDynamicPaymentCurrency directly calls useSystemConfigStore.getState() (line 88) to access global state outside of a React component. While this is valid for Zustand stores, it:

  1. Makes the function impure and dependent on global state
  2. Could be unexpected for developers who see a format function and assume it's pure
  3. The function name doesn't indicate this side-effect

Consider either:

  • Adding a JSDoc comment explaining this accesses global store state
  • Renaming to make the store dependency clear (e.g., formatDynamicPaymentCurrencyFromStore)
  • Passing currency config as a parameter to keep the function pure
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/lib/dynamic-price.ts` around lines 85 - 101,
formatDynamicPaymentCurrency currently reads global state via
useSystemConfigStore.getState(), making it impure and surprising; change the
function to accept the currency config as a parameter (e.g.,
formatDynamicPaymentCurrency(currencyConfig: CurrencyConfig, amount: number))
and use that instead of calling useSystemConfigStore.getState(), keep using
getWalletCurrencyConfig(...) and the Intl.NumberFormat logic, then update all
callers to pass the store value
(useSystemConfigStore.getState().config.currency) at call sites; if updating
callers is impractical, instead add a clear JSDoc on
formatDynamicPaymentCurrency documenting that it reads
useSystemConfigStore.getState() and consider renaming to
formatDynamicPaymentCurrencyFromStore to signal the dependency.
web/default/src/features/pricing/hooks/use-pricing-data.ts (1)

40-53: ⚡ Quick win

Clarify the fallback logic from exchange rate to price rate.

Line 42 falls back to priceRate when currency.usdExchangeRate is unavailable for CNY mode. This mixes a pricing multiplier (priceRate from status?.price) with a currency exchange rate concept, which could lead to confusion or incorrect calculations if these values have different semantics in your system.

Consider either:

  • Documenting why priceRate is a valid exchange-rate fallback for CNY, or
  • Using a distinct exchange-rate default (e.g., a constant like DEFAULT_CNY_RATE) to keep pricing and currency concerns separate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/hooks/use-pricing-data.ts` around lines 40 -
53, The usdExchangeRate fallback currently uses priceRate when
currency?.quotaDisplayType === 'CNY', which conflates a pricing multiplier
(priceRate) with a currency exchange rate; change this to use a dedicated
exchange-rate default or explicitly document the rationale. Replace the fallback
expression in the useMemo that computes usdExchangeRate (referencing
usdExchangeRate, currency?.usdExchangeRate, priceRate, and
currency?.quotaDisplayType) with a clear default constant like DEFAULT_CNY_RATE
(or another appropriately named constant) and update any imports/exports, or add
an inline comment explaining why priceRate is an intentional and safe fallback
if you choose to keep it. Ensure the dependency array remains correct after the
change.
web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx (1)

51-64: ⚖️ Poor tradeoff

Props destructuring violates coding guidelines.

The function signature destructures all props, but the coding guidelines specify: "Do not destructure component props; use props.xxx directly instead for clarity."

While this pattern is common in the existing codebase, consider refactoring to props.topupAmount, props.paymentAmount, etc. for consistency with project standards. As per coding guidelines, avoid destructuring object props unless necessary; use props.xxx directly to maintain clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx`
around lines 51 - 64, The PaymentConfirmDialog function currently destructures
its props in the parameter list (open, onOpenChange, onConfirm, topupAmount,
paymentAmount, paymentMethod, calculating, processing, discountRate,
topupCurrencySymbol, paymentCurrencySymbol, usdExchangeRate), which violates the
guideline to access props via the props object; refactor the component signature
to accept a single parameter (props: PaymentConfirmDialogProps) and update all
internal references to use props.open, props.onOpenChange, props.onConfirm,
props.topupAmount, props.paymentAmount, props.paymentMethod, props.calculating,
props.processing, props.discountRate, props.topupCurrencySymbol,
props.paymentCurrencySymbol, and props.usdExchangeRate (preserve default values
by applying them where those props are read or by setting defaults when
destructuring inside the function body if needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/subscription_payment_epay.go`:
- Line 89: Computed paymentMoney from getSubscriptionEpayMoney(plan.PriceAmount)
may be zero or negative after applying operation_setting.Price; validate it
before creating the order and before calling the ePay gateway. Add a check after
paymentMoney is computed (referencing getSubscriptionEpayMoney and
operation_setting.Price) to ensure paymentMoney is > 0 and meets the gateway
minimum, return or error out if not, and avoid executing the order creation and
epay call (the code paths that persist the order and call the ePay client).
Ensure the same validation is applied immediately before persisting the order
and immediately before sending to epay so no invalid amounts are stored or
transmitted.

---

Nitpick comments:
In `@web/default/src/features/pricing/hooks/use-pricing-data.ts`:
- Around line 40-53: The usdExchangeRate fallback currently uses priceRate when
currency?.quotaDisplayType === 'CNY', which conflates a pricing multiplier
(priceRate) with a currency exchange rate; change this to use a dedicated
exchange-rate default or explicitly document the rationale. Replace the fallback
expression in the useMemo that computes usdExchangeRate (referencing
usdExchangeRate, currency?.usdExchangeRate, priceRate, and
currency?.quotaDisplayType) with a clear default constant like DEFAULT_CNY_RATE
(or another appropriately named constant) and update any imports/exports, or add
an inline comment explaining why priceRate is an intentional and safe fallback
if you choose to keep it. Ensure the dependency array remains correct after the
change.

In `@web/default/src/features/pricing/lib/dynamic-price.ts`:
- Around line 85-101: formatDynamicPaymentCurrency currently reads global state
via useSystemConfigStore.getState(), making it impure and surprising; change the
function to accept the currency config as a parameter (e.g.,
formatDynamicPaymentCurrency(currencyConfig: CurrencyConfig, amount: number))
and use that instead of calling useSystemConfigStore.getState(), keep using
getWalletCurrencyConfig(...) and the Intl.NumberFormat logic, then update all
callers to pass the store value
(useSystemConfigStore.getState().config.currency) at call sites; if updating
callers is impractical, instead add a clear JSDoc on
formatDynamicPaymentCurrency documenting that it reads
useSystemConfigStore.getState() and consider renaming to
formatDynamicPaymentCurrencyFromStore to signal the dependency.

In
`@web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx`:
- Around line 51-64: The PaymentConfirmDialog function currently destructures
its props in the parameter list (open, onOpenChange, onConfirm, topupAmount,
paymentAmount, paymentMethod, calculating, processing, discountRate,
topupCurrencySymbol, paymentCurrencySymbol, usdExchangeRate), which violates the
guideline to access props via the props object; refactor the component signature
to accept a single parameter (props: PaymentConfirmDialogProps) and update all
internal references to use props.open, props.onOpenChange, props.onConfirm,
props.topupAmount, props.paymentAmount, props.paymentMethod, props.calculating,
props.processing, props.discountRate, props.topupCurrencySymbol,
props.paymentCurrencySymbol, and props.usdExchangeRate (preserve default values
by applying them where those props are read or by setting defaults when
destructuring inside the function body if needed).

In `@web/default/src/features/wallet/lib/format.ts`:
- Line 21: Add a clarifying comment above the DEFAULT_PAYMENT_EXCHANGE_RATE
constant explaining that the value 7 is a hardcoded fallback representing the
approximate CNY to USD exchange rate (CNY per USD), why 7 was chosen
(approximate historical/round figure), and that it is only used as a fallback
when no real-time exchange rate is available (e.g., when the rate fetch in
formatPayment()/formatCurrency() fails). Update the comment to mention that this
fallback should be replaced by live rates and where to look to change it (the
DEFAULT_PAYMENT_EXCHANGE_RATE constant) so future maintainers know its purpose
and limitations.
- Around line 107-112: The default return object currently uses the Yuan symbol
and CNY behavior but is mislabeled as "JPY-style" in the surrounding comment;
update the comment that describes this fallback to say "CNY-style" (or "Chinese
Yuan") and clarify that the returned object (fields symbol, rate, paymentSymbol,
paymentRate using effectiveUsdExchangeRate) represents CNY behavior rather than
JPY so readers won't be misled.
- Around line 85-92: The code currently infers paymentSymbol from numeric
effectiveUsdExchangeRate (in the quotaDisplayType === 'USD' branch), which is
fragile; instead, change the logic to rely on an explicit currency indicator
(e.g., a provided quotaPaymentCurrency or baseCurrency flag) rather than
comparing effectiveUsdExchangeRate === 1. Update the function that returns
paymentSymbol/paymentRate (references: quotaDisplayType,
effectiveUsdExchangeRate, paymentSymbol, paymentRate) to accept or derive an
explicit payment currency value and use that to decide whether to return '$' or
'¥', leaving paymentRate set from effectiveUsdExchangeRate; if no explicit
currency exists, add and propagate a new parameter or use an existing currency
field from the caller.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbc11d0d-3b0f-433a-994f-9dbd1a9149c0

📥 Commits

Reviewing files that changed from the base of the PR and between 05c4161 and 76559c6.

📒 Files selected for processing (13)
  • controller/subscription_payment_epay.go
  • web/default/src/features/pricing/hooks/use-pricing-data.ts
  • web/default/src/features/pricing/lib/dynamic-price.ts
  • web/default/src/features/pricing/lib/price.ts
  • web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx
  • web/default/src/features/subscriptions/components/subscriptions-columns.tsx
  • web/default/src/features/subscriptions/lib/format.ts
  • web/default/src/features/subscriptions/lib/index.ts
  • web/default/src/features/wallet/components/dialogs/payment-confirm-dialog.tsx
  • web/default/src/features/wallet/components/recharge-form-card.tsx
  • web/default/src/features/wallet/components/subscription-plans-card.tsx
  • web/default/src/features/wallet/index.tsx
  • web/default/src/features/wallet/lib/format.ts

return
}

paymentMoney := getSubscriptionEpayMoney(plan.PriceAmount)

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 | ⚡ Quick win

Validate computed payment amount before creating order and calling epay.

Line 89 computes paymentMoney, but there is no lower-bound check after applying operation_setting.Price. If config makes it <= 0 (or below gateway minimum), Line 93 stores an invalid order amount and Line 108 sends an invalid payment amount.

Suggested fix
 	paymentMoney := getSubscriptionEpayMoney(plan.PriceAmount)
+	if paymentMoney < 0.01 {
+		common.ApiErrorMsg(c, "支付金额过低")
+		return
+	}
 	order := &model.SubscriptionOrder{
 		UserId:          userId,
 		PlanId:          plan.Id,
 		Money:           paymentMoney,

Also applies to: 93-93, 108-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/subscription_payment_epay.go` at line 89, Computed paymentMoney
from getSubscriptionEpayMoney(plan.PriceAmount) may be zero or negative after
applying operation_setting.Price; validate it before creating the order and
before calling the ePay gateway. Add a check after paymentMoney is computed
(referencing getSubscriptionEpayMoney and operation_setting.Price) to ensure
paymentMoney is > 0 and meets the gateway minimum, return or error out if not,
and avoid executing the order creation and epay call (the code paths that
persist the order and call the ePay client). Ensure the same validation is
applied immediately before persisting the order and immediately before sending
to epay so no invalid amounts are stored or transmitted.

2.删除了模型卡片弹窗中的API板块,及概览下的推测元信息。
3.系统设置中的顶部导航,新增了排行榜显示倍率设置。
4.【大改】优化了视频生成模型接口,兼容多模态视频生成,并新增了按秒计费规则,相关配置如端点展示、按秒计费价格配置、模型广场展示、使用日志展示、任务日志新增下载视频。
本次所以改动仅针对前后端,未修改数据库。

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (2)
relay/common/relay_utils.go (1)

90-110: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Copy input_reference into the normalized image fields for multipart requests.

validateMultipartTaskRequest now normalizes images, but it still never copies the known input_reference field into req.InputReference/req.ImageInputs. A multipart task that only sends input_reference will therefore lose its image input and can fall through as a text-only request.

🛠️ Suggested fix
 	req = TaskSubmitReq{
 		Prompt:   formData.Get("prompt"),
 		Model:    formData.Get("model"),
 		Mode:     formData.Get("mode"),
 		Image:    formData.Get("image"),
 		Size:     formData.Get("size"),
+		InputReference: formData.Get("input_reference"),
 		Metadata: make(map[string]interface{}),
 	}
@@
 	if images := formData["images"]; len(images) > 0 {
 		req.Images = images
 		req.ImageInputs = make([]TaskImageInput, 0, len(images))
 		for _, image := range images {
 			req.ImageInputs = append(req.ImageInputs, TaskImageInput{URL: image})
 		}
 	}
+	if inputRef := strings.TrimSpace(req.InputReference); inputRef != "" && len(req.ImageInputs) == 0 {
+		req.Images = []string{inputRef}
+		req.ImageInputs = []TaskImageInput{{URL: inputRef}}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/common/relay_utils.go` around lines 90 - 110, The multipart
normalization currently fills req.Images and req.ImageInputs from
formData["images"] but never copies the single-value "input_reference" field,
causing requests that only include input_reference to lose their image input;
update validateMultipartTaskRequest (the code that constructs TaskSubmitReq) to
read formData.Get("input_reference") and, if non-empty and req.Images is empty,
set req.InputReference to that value and append a corresponding
TaskImageInput{URL: inputReference} to req.ImageInputs (or set req.Images to
[]string{inputReference}) so single-reference multipart uploads are preserved.
relay/common/relay_info.go (1)

3-4: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Re-export json.RawMessage through common/json.go to comply with the JSON wrapper rule.

This file imports encoding/json directly and uses json.RawMessage (lines 740, 744, 751), which violates the repository guideline: "Do NOT directly import or call encoding/json in business code." All JSON types and operations must be accessed through common/json.go wrappers. Add a type alias in common/json.go (e.g., type RawMessage = encoding/json.RawMessage) and use that instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/common/relay_info.go` around lines 3 - 4, The file uses encoding/json
directly (json.RawMessage) which violates the rule; add a re-export alias in
common/json.go such as `package common` with `type RawMessage =
encoding/json.RawMessage`, then update relay/common/relay_info.go to stop
importing encoding/json and instead use the alias from the common package
(replace uses of json.RawMessage with common.RawMessage and remove the
encoding/json import). Ensure imports compile after switching to the common
package alias.
🧹 Nitpick comments (5)
setting/billing_setting/tiered_billing.go (1)

72-74: ⚡ Quick win

Consider deep copy for defensive programming.

lo.Assign performs a shallow copy of the outer map. Since VideoPriceConfig contains a nested Prices map, the copied struct will reference the same underlying Prices map as the original. While the current codebase doesn't mutate Prices maps after copying (used only for JSON serialization), a deep copy would provide better defensive protection against future code changes:

func GetVideoPriceCopy() map[string]VideoPriceConfig {
	result := make(map[string]VideoPriceConfig, len(billingSetting.VideoPrice))
	for model, cfg := range billingSetting.VideoPrice {
		prices := make(map[string]float64, len(cfg.Prices))
		for k, v := range cfg.Prices {
			prices[k] = v
		}
		result[model] = VideoPriceConfig{
			BaseFPS: cfg.BaseFPS,
			Prices:  prices,
		}
	}
	return result
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/billing_setting/tiered_billing.go` around lines 72 - 74,
GetVideoPriceCopy currently uses lo.Assign which only shallow-copies the outer
map so each VideoPriceConfig in the result still references the original nested
Prices map; replace the shallow copy with a defensive deep copy by creating a
new result map, iterating billingSetting.VideoPrice entries, copying scalar
fields (e.g., BaseFPS) and allocating+copying the nested Prices map into a new
map for each VideoPriceConfig, then return the result so callers get independent
structs (refer to GetVideoPriceCopy, VideoPriceConfig,
billingSetting.VideoPrice, and the Prices field).
service/rankings.go (1)

591-609: 💤 Low value

Jitter is one-sided (always inflates, never decreases).

rankingStableRandom01 returns values in [0, 1), so scaled += scaled * jitter * random01(salt) only ever adds to the value. With jitter = 0.1, displayed totals are always in [value*multiplier, value*multiplier*1.1] — never below. If the intent is true symmetric jitter (typical meaning), consider centering the noise around zero; if the intent is a positive-only display inflation, the variable name jitter is misleading and rankingDisplayJitterOption should be renamed (e.g., RankingsDisplayInflationCap).

♻️ Proposed fix for symmetric jitter
 func rankingDisplayValue(value int64, settings rankingDisplaySettings, salt string) int64 {
 	if value <= 0 {
 		return 0
 	}
 	scaled := float64(value) * settings.multiplier
 	if settings.jitter > 0 {
-		scaled += scaled * settings.jitter * rankingStableRandom01(salt)
+		// Symmetric jitter in [-jitter, +jitter]
+		scaled += scaled * settings.jitter * (rankingStableRandom01(salt)*2 - 1)
 	}
 	if scaled <= 0 {
 		return 0
 	}
 	return int64(math.Round(scaled))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/rankings.go` around lines 591 - 609, The jitter is currently
one-sided because rankingStableRandom01 returns [0,1), so rankingDisplayValue
only inflates values; to make jitter symmetric, change rankingStableRandom01
(and its callers) so it returns a centered value (e.g.,
(hasher.Sum64()%1_000_000)/1_000_000 - 0.5 to yield [-0.5,0.5]) and keep the
multiplication in rankingDisplayValue (scaled += scaled * settings.jitter *
rankingStableRandom01(salt)), or alternatively if you want positive-only
inflation rename settings.jitter and rankingDisplayJitterOption to make that
intent explicit; update references to rankingStableRandom01,
rankingDisplayValue, settings.jitter and rankingDisplayJitterOption accordingly.
web/default/src/features/pricing/components/model-card.tsx (1)

276-281: ⚡ Quick win

Extract the billing-mode label before render.

This 3-way nested ternary now violates the repo's TS/TSX readability rule. Compute the label once above the JSX and render the variable here.

♻️ Suggested cleanup
+  let billingModeLabel = t('Per Request')
+  if (isVideoSeconds) {
+    billingModeLabel = t('Video per-second')
+  } else if (isTokenBased) {
+    billingModeLabel = t('Token-based')
+  }
+
   return (
@@
           <span className='text-muted-foreground max-w-full truncate text-xs font-medium whitespace-nowrap'>
-            {isVideoSeconds
-              ? t('Video per-second')
-              : isTokenBased
-                ? t('Token-based')
-                : t('Per Request')}
+            {billingModeLabel}
           </span>

As per coding guidelines, web/default/**/*.{ts,tsx}: "Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead; single-level ternaries are acceptable if concise"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-card.tsx` around lines 276
- 281, Replace the nested ternary inside the JSX with a precomputed variable:
above the return/JSX in the ModelCard component compute a billingLabel (e.g.,
let billingLabel = '') using if/else or a small helper that checks
isVideoSeconds, isTokenBased and falls back to 'Per Request', then render
{billingLabel} in the span instead of the 3-way nested ternary; reference the
existing booleans isVideoSeconds and isTokenBased when computing the label.
web/default/src/features/pricing/components/model-details.tsx (1)

216-220: ⚡ Quick win

Replace the nested ternary in the billing label.

This now has a two-level nested ternary, which makes the header harder to extend as more billing modes are added.

♻️ Suggested cleanup
+  let billingLabel = t('Per Request')
+  if (model.billing_mode === 'video_seconds') {
+    billingLabel = t('Video per-second')
+  } else if (model.quota_type === QUOTA_TYPE_VALUES.TOKEN) {
+    billingLabel = t('Token-based')
+  }
+
   return (
@@
         <span className='text-muted-foreground/70'>
-          {model.billing_mode === 'video_seconds'
-            ? t('Video per-second')
-            : model.quota_type === QUOTA_TYPE_VALUES.TOKEN
-              ? t('Token-based')
-              : t('Per Request')}
+          {billingLabel}
         </span>

As per coding guidelines, "Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead; single-level ternaries are acceptable if concise".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-details.tsx` around lines
216 - 220, The nested ternary used to compute the billing label (checking
model.billing_mode and model.quota_type against QUOTA_TYPE_VALUES.TOKEN) should
be replaced with clearer branching: extract a small helper or inline if/else
that returns the label (e.g., inside the ModelDetails component, create a
getBillingLabel function or use an if/else before JSX) and then use that result
in the header; ensure you still call t('Video per-second'), t('Token-based'), or
t('Per Request') accordingly and reference model.billing_mode, model.quota_type,
and QUOTA_TYPE_VALUES.TOKEN to determine the correct label.
web/default/src/i18n/locales/ru.json (1)

54-54: 🏗️ Heavy lift

Use hierarchical i18n keys for new entries instead of phrase-as-key strings.

These newly added/updated entries keep introducing raw text keys (e.g., "Price per second", "Video pricing"), which breaks the i18n key naming rule and increases long-term key churn. Please migrate these new keys to semantic hierarchical names (e.g., pricing.video.perSecond.label) and update call sites accordingly.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

Also applies to: 200-200, 244-244, 491-491, 524-524, 852-852, 860-860, 1222-1222, 1234-1234, 1253-1253, 1789-1789, 2084-2084, 2386-2386, 2683-2683, 2939-2939, 3002-3002, 3009-3009, 3114-3114, 3118-3118, 3276-3276, 3312-3314, 3459-3459, 3873-3873, 3982-3982, 4051-4051, 4274-4277, 4356-4356, 4432-4432

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/ru.json` at line 54, The translation file
contains phrase-as-key entries like "/ sec" that violate the hierarchical key
convention; replace each raw-text key (e.g., "/ sec") with a semantic
hierarchical key (suggested pattern: pricing.video.perSecond.label) and update
all call sites to use the new key (search for usages of the literal "/ sec" in
the codebase and substitute with the new key). Apply the same refactor for the
other listed raw keys (e.g., "Price per second", "Video pricing", etc.), keeping
names consistent (e.g., pricing.<feature>.<context>.<element>) and ensure tests
or components that reference the old keys are updated to the new hierarchical
keys. Ensure the ru.json values remain the same strings but keyed under the new
hierarchical identifiers and run the i18n extraction/validation to confirm no
missing keys remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@common/model.go`:
- Around line 20-31: The VideoGenerationModels slice currently contains the
overly generic entry "video" which will match any model name containing that
substring; remove or replace "video" with specific model identifiers (e.g., full
model names or clearer prefixes like "video-xyz") and adopt a "prefix:"
convention for broader matches (e.g., "prefix:veo-" or "prefix:seedance-")—then
update the model-matching logic (the function that checks VideoGenerationModels,
e.g., isVideoGenerationModel) to treat entries starting with "prefix:" as
startsWith checks and otherwise as exact matches to avoid accidental substring
matches.

In `@relay/helper/price.go`:
- Around line 254-276: The video-seconds path in modelPriceHelperVideoSeconds
never sets PriceData.FreeModel, so free-model pre-consume logic is skipped;
after computing trace.TotalPrice and quota (and before returning priceData),
consult operation_setting.GetQuotaSetting().EnableFreeModelPreConsume and set
priceData.FreeModel = true when that flag is enabled and either
groupRatioInfo.GroupRatio == 0 (or trace.TotalPrice == 0 as in the other
helpers) so behavior matches ModelPriceHelper/ModelPriceHelperPerCall; use the
existing symbols calculateVideoSecondsBilling, trace.TotalPrice,
billing_setting.GetVideoPriceConfig, groupRatioInfo.GroupRatio and
types.PriceData to locate where to add this assignment.

In `@service/rankings_test.go`:
- Around line 36-38: The assertion in the test that validates sorting of the
rows is inverted: it currently checks rows[0].TotalTokens < rows[1].TotalTokens
which asserts ascending order, but the test expects descending order. Update the
check on the rows slice (the TotalTokens field) to assert the first element is
greater than or equal to the second (e.g., use a >= comparison) and keep the
failure message descriptive (mention rows and that descending order was
expected) in the same test (rankings_test.go where rows and TotalTokens are
used).

In `@setting/billing_setting/tiered_billing.go`:
- Around line 22-25: VideoPriceConfig currently allows non-positive BaseFPS and
negative Prices, risking divide-by-zero and incorrect billing; add validation
when creating/updating/saving the config (e.g., implement a Validate method
called from your Save/Update handlers) that checks VideoPriceConfig.BaseFPS > 0
and iterates VideoPriceConfig.Prices to ensure each price >= 0, returning clear
errors like "base_fps must be positive" or "price for resolution X cannot be
negative" when violations occur; update any functions that persist or accept
VideoPriceConfig to call this validation before proceeding.

In `@web/default/src/features/pricing/components/model-details.tsx`:
- Around line 607-671: The branch handling billing_mode === 'video_seconds'
should detect when getVideoPriceEntries(props.model) returns an empty array and
avoid rendering the empty group/ratio table; update the component to check
videoEntries.length and if zero either render a clear fallback message (e.g.,
"No video pricing configured") or return null before rendering
AutoGroupChain/Table, so users see that no usable video pricing exists; adjust
references around getVideoPriceEntries, AutoGroupChain, availableGroups, and the
formatVideoSecondPrice usage so the table and formatVideoSecondPrice are only
used when videoEntries is non-empty.

In `@web/default/src/features/system-settings/models/model-pricing-sheet.tsx`:
- Around line 338-353: videoRowsToConfig currently silently drops blank/invalid
rows and allows duplicate resolutions to overwrite earlier entries, which can
result in an empty prices object that downstream interprets as deletion; update
videoRowsToConfig (and the caller handleSubmit) to validate input: within
videoRowsToConfig (VideoPriceRow -> VideoPricingConfig) trim and parse rows,
collect duplicates by resolution and if any duplicate or no valid priced rows
are found, throw or return a validation error (or null) instead of returning
prices: {}; update handleSubmit to check for that validation error/null and
surface a user-facing validation message (preventing the save) so duplicate
resolutions and entirely-empty price sets are rejected before persistence.

In
`@web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx`:
- Around line 979-983: The label 'Video per-second' is hard-coded; replace it
with a translated string using the t() function from useTranslation() so the
filter label reacts to locale changes; import and call useTranslation() in the
component (e.g., ModelRatioVisualEditor) and change the entry with label: 'Video
per-second' to label: t('systemSettings.modeFilters.videoSeconds') (or another
appropriate i18n key consistent with your translations), leaving value:
'video_seconds' and count: modeCounts.video_seconds untouched.

In
`@web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx`:
- Around line 258-271: The URL selection currently sets resultUrl =
log.result_url || failReason and only checks resultUrl.startsWith('http'), which
fails when log.result_url exists but is not a valid absolute URL; update the
logic in the component (around resultUrl, log.result_url, failReason, isSuccess,
isVideoTask) to independently validate both sources and choose the first that is
a valid absolute URL (e.g., check startsWith('http') on log.result_url first,
then on failReason), or else don't render the anchor; extract this into a small
helper like getValidHttpUrl(resultUrlCandidate, failReason) and use it where
isUrl is computed so the link reliably falls back to failReason when
log.result_url is present but invalid.

In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 200-205: The code concatenates the formatted number and unit
without spacing (e.g. `${formatNumber(other.video_duration)}${t('seconds')}`)
causing locale rendering issues; update the two occurrences (the rows with label
'Duration' and the 'Billing formula' row that use
formatNumber(other.video_duration) + t('seconds')) to either include an explicit
separator before the translated unit (e.g.
`${formatNumber(other.video_duration)} ${t('seconds')}` and ensure locale
strings include a leading space where needed) or, preferably, use a count-aware
translation key such as `t('duration_seconds', { count: other.video_duration,
value: formatNumber(other.video_duration) })` (or `t('seconds', { count:
other.video_duration })` with interpolation) so pluralization and spacing are
correct across locales; apply the same change to both locations referencing
`formatNumber(other.video_duration)` + `t('seconds')`.

In `@web/default/src/i18n/locales/en.json`:
- Line 54: The translation entry "/ sec" is a literal UI string and should be
replaced with a hierarchical, semantic i18n key (e.g.,
billing.video.perSecond.label) and the literal value moved under that key;
update all call sites that reference the flat string key to use the new
namespaced key (search for occurrences of "/ sec" and usages in components that
render per-second labels), add similar namespaced keys for the other listed
occurrences (lines 491, 852, 860, 1789, 2084, 3002, 3009, 3118, 3276, 3312-3314,
3873, 4274-4277) following the existing naming pattern (e.g., billing.*,
video.*, pricing.*), and ensure tests/translation loader still reference the new
keys.

In `@web/default/src/i18n/locales/fr.json`:
- Line 3276: Replace the French word "demandes" with "requêtes" in the
translation value for the key whose English text is "Requests with higher FPS
are multiplied by fps / base fps." (the current translation: "Les demandes avec
un FPS plus élevé sont multipliées par fps / FPS de base."). Edit the value in
fr.json so it reads "Les requêtes avec un FPS plus élevé sont multipliées par
fps / FPS de base." to match UI terminology consistency.

---

Outside diff comments:
In `@relay/common/relay_info.go`:
- Around line 3-4: The file uses encoding/json directly (json.RawMessage) which
violates the rule; add a re-export alias in common/json.go such as `package
common` with `type RawMessage = encoding/json.RawMessage`, then update
relay/common/relay_info.go to stop importing encoding/json and instead use the
alias from the common package (replace uses of json.RawMessage with
common.RawMessage and remove the encoding/json import). Ensure imports compile
after switching to the common package alias.

In `@relay/common/relay_utils.go`:
- Around line 90-110: The multipart normalization currently fills req.Images and
req.ImageInputs from formData["images"] but never copies the single-value
"input_reference" field, causing requests that only include input_reference to
lose their image input; update validateMultipartTaskRequest (the code that
constructs TaskSubmitReq) to read formData.Get("input_reference") and, if
non-empty and req.Images is empty, set req.InputReference to that value and
append a corresponding TaskImageInput{URL: inputReference} to req.ImageInputs
(or set req.Images to []string{inputReference}) so single-reference multipart
uploads are preserved.

---

Nitpick comments:
In `@service/rankings.go`:
- Around line 591-609: The jitter is currently one-sided because
rankingStableRandom01 returns [0,1), so rankingDisplayValue only inflates
values; to make jitter symmetric, change rankingStableRandom01 (and its callers)
so it returns a centered value (e.g., (hasher.Sum64()%1_000_000)/1_000_000 - 0.5
to yield [-0.5,0.5]) and keep the multiplication in rankingDisplayValue (scaled
+= scaled * settings.jitter * rankingStableRandom01(salt)), or alternatively if
you want positive-only inflation rename settings.jitter and
rankingDisplayJitterOption to make that intent explicit; update references to
rankingStableRandom01, rankingDisplayValue, settings.jitter and
rankingDisplayJitterOption accordingly.

In `@setting/billing_setting/tiered_billing.go`:
- Around line 72-74: GetVideoPriceCopy currently uses lo.Assign which only
shallow-copies the outer map so each VideoPriceConfig in the result still
references the original nested Prices map; replace the shallow copy with a
defensive deep copy by creating a new result map, iterating
billingSetting.VideoPrice entries, copying scalar fields (e.g., BaseFPS) and
allocating+copying the nested Prices map into a new map for each
VideoPriceConfig, then return the result so callers get independent structs
(refer to GetVideoPriceCopy, VideoPriceConfig, billingSetting.VideoPrice, and
the Prices field).

In `@web/default/src/features/pricing/components/model-card.tsx`:
- Around line 276-281: Replace the nested ternary inside the JSX with a
precomputed variable: above the return/JSX in the ModelCard component compute a
billingLabel (e.g., let billingLabel = '') using if/else or a small helper that
checks isVideoSeconds, isTokenBased and falls back to 'Per Request', then render
{billingLabel} in the span instead of the 3-way nested ternary; reference the
existing booleans isVideoSeconds and isTokenBased when computing the label.

In `@web/default/src/features/pricing/components/model-details.tsx`:
- Around line 216-220: The nested ternary used to compute the billing label
(checking model.billing_mode and model.quota_type against
QUOTA_TYPE_VALUES.TOKEN) should be replaced with clearer branching: extract a
small helper or inline if/else that returns the label (e.g., inside the
ModelDetails component, create a getBillingLabel function or use an if/else
before JSX) and then use that result in the header; ensure you still call
t('Video per-second'), t('Token-based'), or t('Per Request') accordingly and
reference model.billing_mode, model.quota_type, and QUOTA_TYPE_VALUES.TOKEN to
determine the correct label.

In `@web/default/src/i18n/locales/ru.json`:
- Line 54: The translation file contains phrase-as-key entries like "/ sec" that
violate the hierarchical key convention; replace each raw-text key (e.g., "/
sec") with a semantic hierarchical key (suggested pattern:
pricing.video.perSecond.label) and update all call sites to use the new key
(search for usages of the literal "/ sec" in the codebase and substitute with
the new key). Apply the same refactor for the other listed raw keys (e.g.,
"Price per second", "Video pricing", etc.), keeping names consistent (e.g.,
pricing.<feature>.<context>.<element>) and ensure tests or components that
reference the old keys are updated to the new hierarchical keys. Ensure the
ru.json values remain the same strings but keyed under the new hierarchical
identifiers and run the i18n extraction/validation to confirm no missing keys
remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63dc2e16-36db-4cd0-a2e7-de4479a26cc4

📥 Commits

Reviewing files that changed from the base of the PR and between 76559c6 and d6eb205.

📒 Files selected for processing (52)
  • common/endpoint_type.go
  • common/model.go
  • controller/relay.go
  • model/option.go
  • model/pricing.go
  • relay/channel/api_request.go
  • relay/channel/api_request_test.go
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/doubao/adaptor_test.go
  • relay/common/relay_info.go
  • relay/common/relay_utils.go
  • relay/common/relay_utils_test.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • service/rankings.go
  • service/rankings_test.go
  • service/task_billing.go
  • setting/billing_setting/tiered_billing.go
  • types/price_data.go
  • web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/default/src/features/pricing/components/model-card.tsx
  • web/default/src/features/pricing/components/model-details.tsx
  • web/default/src/features/pricing/components/pricing-columns.tsx
  • web/default/src/features/pricing/components/pricing-sidebar.tsx
  • web/default/src/features/pricing/constants.ts
  • web/default/src/features/pricing/lib/filters.ts
  • web/default/src/features/pricing/lib/price.ts
  • web/default/src/features/pricing/types.ts
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • web/default/src/features/system-settings/maintenance/header-navigation-section.tsx
  • web/default/src/features/system-settings/models/index.tsx
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • web/default/src/features/system-settings/models/upstream-ratio-sync.tsx
  • web/default/src/features/system-settings/site/index.tsx
  • web/default/src/features/system-settings/site/section-registry.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/i18n/locales/_reports/ja.untranslated.json
  • web/default/src/i18n/locales/_reports/ru.untranslated.json
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (4)
  • web/default/src/features/system-settings/models/upstream-ratio-sync.tsx
  • web/default/src/i18n/locales/_reports/ja.untranslated.json
  • web/default/src/i18n/locales/_reports/ru.untranslated.json
  • web/default/src/i18n/locales/vi.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • model/option.go
  • web/default/src/features/pricing/lib/price.ts

Comment thread common/model.go
Comment on lines +20 to +31
VideoGenerationModels = []string{
"doubao-seedance-",
"seedance-",
"sora-",
"veo-",
"kling",
"vidu",
"hailuo",
"jimeng",
"cogvideo",
"video",
}

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 | ⚡ Quick win

Review overly generic video model matching.

The VideoGenerationModels list includes the term "video", which will match any model name containing that substring. This could incorrectly classify non-video models that happen to include "video" in their name.

Consider:

  1. Using more specific prefixes or exact model names
  2. Adding a "prefix:" convention for broader patterns that should only match at the start
  3. Removing or replacing the generic "video" entry with more specific model identifiers
♻️ Example refinement
 VideoGenerationModels = []string{
     "doubao-seedance-",
     "seedance-",
     "sora-",
     "veo-",
     "kling",
     "vidu",
     "hailuo",
     "jimeng",
     "cogvideo",
-    "video",
+    "prefix:video-gen-",  // or remove if no such models exist
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/model.go` around lines 20 - 31, The VideoGenerationModels slice
currently contains the overly generic entry "video" which will match any model
name containing that substring; remove or replace "video" with specific model
identifiers (e.g., full model names or clearer prefixes like "video-xyz") and
adopt a "prefix:" convention for broader matches (e.g., "prefix:veo-" or
"prefix:seedance-")—then update the model-matching logic (the function that
checks VideoGenerationModels, e.g., isVideoGenerationModel) to treat entries
starting with "prefix:" as startsWith checks and otherwise as exact matches to
avoid accidental substring matches.

Comment thread relay/helper/price.go
Comment on lines +254 to +276
func modelPriceHelperVideoSeconds(c *gin.Context, info *relaycommon.RelayInfo, groupRatioInfo types.GroupRatioInfo) (types.PriceData, error) {
cfg, ok := billing_setting.GetVideoPriceConfig(info.OriginModelName)
if !ok || len(cfg.Prices) == 0 {
return types.PriceData{}, fmt.Errorf("model %s video per-second price not configured", info.OriginModelName)
}
req, err := relaycommon.GetTaskRequest(c)
if err != nil {
return types.PriceData{}, err
}
trace, err := calculateVideoSecondsBilling(req, cfg)
if err != nil {
return types.PriceData{}, err
}
quota := billingexpr.QuotaRound(trace.TotalPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
priceData := types.PriceData{
ModelPrice: trace.TotalPrice,
UsePrice: true,
Quota: quota,
GroupRatioInfo: groupRatioInfo,
VideoSecondsTrace: trace.toPriceDataTrace(),
}
return priceData, 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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how PriceData.FreeModel is consumed in task/relay flows to confirm impact.
rg -nP --type=go -C3 '\bFreeModel\b'

Repository: QuantumNous/new-api

Length of output: 2327


🏁 Script executed:

#!/bin/bash
# Get context around FreeModel initialization in other helper functions
rg -nP --type=go -B5 -A10 'FreeModel.*freeModel' relay/helper/price.go | head -100

Repository: QuantumNous/new-api

Length of output: 1555


🏁 Script executed:

#!/bin/bash
# Find the full functions for ModelPriceHelper and ModelPriceHelperPerCall to see freeModel logic
ast-grep --pattern $'func ModelPriceHelper($$$) {
  $$$
  freeModel := $_
  $$$
}'

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check the exact code around line 144 and 222 to understand context
sed -n '130,160p' relay/helper/price.go
echo "---"
sed -n '210,240p' relay/helper/price.go
echo "---"
sed -n '254,280p' relay/helper/price.go

Repository: QuantumNous/new-api

Length of output: 2727


🏁 Script executed:

#!/bin/bash
# Verify PriceData struct definition to confirm FreeModel field expectations
cat relay/types/price_data.go

Repository: QuantumNous/new-api

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Find the PriceData struct definition
rg -l 'type PriceData struct' --type=go

Repository: QuantumNous/new-api

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Read the PriceData struct definition
cat types/price_data.go

Repository: QuantumNous/new-api

Length of output: 1756


Free-model semantics not applied for video_seconds billing path.

The sibling functions (ModelPriceHelper, ModelPriceHelperPerCall) both check operation_setting.GetQuotaSetting().EnableFreeModelPreConsume and set FreeModel = true when GroupRatio == 0 (or price is 0). modelPriceHelperVideoSeconds does not initialize FreeModel at all, causing it to default to false. This breaks downstream logic in relay_task.go:206 (!info.PriceData.FreeModel) and controller/relay.go:161 that skip pre-consume for free models, resulting in unnecessary pre-consume calls for video models with zero group ratio.

♻️ Proposed fix for consistency
 	quota := billingexpr.QuotaRound(trace.TotalPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
+	freeModel := false
+	if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
+		if groupRatioInfo.GroupRatio == 0 || trace.TotalPrice == 0 {
+			quota = 0
+			freeModel = true
+		}
+	}
 	priceData := types.PriceData{
+		FreeModel:         freeModel,
 		ModelPrice:        trace.TotalPrice,
 		UsePrice:          true,
 		Quota:             quota,
 		GroupRatioInfo:    groupRatioInfo,
 		VideoSecondsTrace: trace.toPriceDataTrace(),
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/helper/price.go` around lines 254 - 276, The video-seconds path in
modelPriceHelperVideoSeconds never sets PriceData.FreeModel, so free-model
pre-consume logic is skipped; after computing trace.TotalPrice and quota (and
before returning priceData), consult
operation_setting.GetQuotaSetting().EnableFreeModelPreConsume and set
priceData.FreeModel = true when that flag is enabled and either
groupRatioInfo.GroupRatio == 0 (or trace.TotalPrice == 0 as in the other
helpers) so behavior matches ModelPriceHelper/ModelPriceHelperPerCall; use the
existing symbols calculateVideoSecondsBilling, trace.TotalPrice,
billing_setting.GetVideoPriceConfig, groupRatioInfo.GroupRatio and
types.PriceData to locate where to add this assignment.

Comment thread service/rankings_test.go
Comment on lines +36 to +38
if rows[0].TotalTokens < rows[1].TotalTokens {
t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
}

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 | ⚡ Quick win

Fix incorrect sort assertion.

The test expects descending order but checks rows[0].TotalTokens < rows[1].TotalTokens, which asserts ascending order.

🐛 Proposed fix
-	if rows[0].TotalTokens < rows[1].TotalTokens {
+	if rows[0].TotalTokens < rows[1].TotalTokens {
 		t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
 	}

Actually, for descending sort the first row should have a value greater than or equal to the second:

-	if rows[0].TotalTokens < rows[1].TotalTokens {
+	if len(rows) > 1 && rows[0].TotalTokens < rows[1].TotalTokens {
 		t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
 	}
📝 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
if rows[0].TotalTokens < rows[1].TotalTokens {
t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
}
if len(rows) > 1 && rows[0].TotalTokens > rows[1].TotalTokens {
t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
}
Suggested change
if rows[0].TotalTokens < rows[1].TotalTokens {
t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
}
if rows[0].TotalTokens > rows[1].TotalTokens {
t.Fatalf("expected rows sorted by displayed value descending: %+v", rows)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/rankings_test.go` around lines 36 - 38, The assertion in the test
that validates sorting of the rows is inverted: it currently checks
rows[0].TotalTokens < rows[1].TotalTokens which asserts ascending order, but the
test expects descending order. Update the check on the rows slice (the
TotalTokens field) to assert the first element is greater than or equal to the
second (e.g., use a >= comparison) and keep the failure message descriptive
(mention rows and that descending order was expected) in the same test
(rankings_test.go where rows and TotalTokens are used).

Comment on lines +22 to +25
type VideoPriceConfig struct {
BaseFPS float64 `json:"base_fps,omitempty"`
Prices map[string]float64 `json:"prices,omitempty"`
}

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 | ⚡ Quick win

Add validation for BaseFPS and Prices values.

The VideoPriceConfig struct allows BaseFPS to be zero or negative and Prices map values to be negative, which could cause division-by-zero errors or incorrect billing calculations downstream.

🛡️ Proposed validation in setter/update methods

When saving or updating VideoPriceConfig, validate that:

if cfg.BaseFPS <= 0 {
    return fmt.Errorf("base_fps must be positive, got %f", cfg.BaseFPS)
}
for resolution, price := range cfg.Prices {
    if price < 0 {
        return fmt.Errorf("price for resolution %s cannot be negative: %f", resolution, price)
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/billing_setting/tiered_billing.go` around lines 22 - 25,
VideoPriceConfig currently allows non-positive BaseFPS and negative Prices,
risking divide-by-zero and incorrect billing; add validation when
creating/updating/saving the config (e.g., implement a Validate method called
from your Save/Update handlers) that checks VideoPriceConfig.BaseFPS > 0 and
iterates VideoPriceConfig.Prices to ensure each price >= 0, returning clear
errors like "base_fps must be positive" or "price for resolution X cannot be
negative" when violations occur; update any functions that persist or accept
VideoPriceConfig to call this validation before proceeding.

Comment on lines +607 to +671
if (props.model.billing_mode === 'video_seconds') {
const videoEntries = getVideoPriceEntries(props.model)
return (
<section>
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
<div className='-mx-4 overflow-x-auto sm:mx-0'>
<Table className='text-sm'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead className={thClass}>{t('Group')}</TableHead>
<TableHead className={thClass}>{t('Ratio')}</TableHead>
{videoEntries.map((entry) => (
<TableHead
key={entry.resolution}
className={`${thClass} text-right`}
>
{entry.resolution}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{availableGroups.map((group) => {
const ratio = props.groupRatio[group] || 1
return (
<TableRow key={group}>
<TableCell className='py-2.5'>
<GroupBadge group={group} size='sm' />
</TableCell>
<TableCell className='text-muted-foreground py-2.5 font-mono text-xs'>
{ratio}x
</TableCell>
{videoEntries.map((entry) => (
<TableCell
key={entry.resolution}
className='py-2.5 text-right font-mono'
>
{formatVideoSecondPrice(
{
...props.model,
video_price: {
...props.model.video_price,
prices: { [entry.resolution]: entry.price },
},
},
showRechargePrice,
props.priceRate,
props.usdExchangeRate,
ratio
)}
</TableCell>
))}
</TableRow>
)
})}
</TableBody>
</Table>
<p className='text-muted-foreground/40 mt-1.5 px-4 text-[10px] sm:px-0'>
{t('Prices shown per second')}
</p>
</div>
</section>
)
}

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 | ⚡ Quick win

Handle empty video pricing before rendering the group table.

If getVideoPriceEntries(props.model) returns an empty array, this branch still renders a group/ratio table with no price columns, which hides that the model has no usable video pricing configured.

🩹 Suggested fix
   if (props.model.billing_mode === 'video_seconds') {
     const videoEntries = getVideoPriceEntries(props.model)
+    if (videoEntries.length === 0) {
+      return (
+        <section>
+          <SectionTitle>{t('Pricing by Group')}</SectionTitle>
+          <AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
+          <p className='text-muted-foreground text-sm'>-</p>
+        </section>
+      )
+    }
+
     return (
       <section>
📝 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
if (props.model.billing_mode === 'video_seconds') {
const videoEntries = getVideoPriceEntries(props.model)
return (
<section>
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
<div className='-mx-4 overflow-x-auto sm:mx-0'>
<Table className='text-sm'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead className={thClass}>{t('Group')}</TableHead>
<TableHead className={thClass}>{t('Ratio')}</TableHead>
{videoEntries.map((entry) => (
<TableHead
key={entry.resolution}
className={`${thClass} text-right`}
>
{entry.resolution}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{availableGroups.map((group) => {
const ratio = props.groupRatio[group] || 1
return (
<TableRow key={group}>
<TableCell className='py-2.5'>
<GroupBadge group={group} size='sm' />
</TableCell>
<TableCell className='text-muted-foreground py-2.5 font-mono text-xs'>
{ratio}x
</TableCell>
{videoEntries.map((entry) => (
<TableCell
key={entry.resolution}
className='py-2.5 text-right font-mono'
>
{formatVideoSecondPrice(
{
...props.model,
video_price: {
...props.model.video_price,
prices: { [entry.resolution]: entry.price },
},
},
showRechargePrice,
props.priceRate,
props.usdExchangeRate,
ratio
)}
</TableCell>
))}
</TableRow>
)
})}
</TableBody>
</Table>
<p className='text-muted-foreground/40 mt-1.5 px-4 text-[10px] sm:px-0'>
{t('Prices shown per second')}
</p>
</div>
</section>
)
}
if (props.model.billing_mode === 'video_seconds') {
const videoEntries = getVideoPriceEntries(props.model)
if (videoEntries.length === 0) {
return (
<section>
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
<p className='text-muted-foreground text-sm'>-</p>
</section>
)
}
return (
<section>
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
<div className='-mx-4 overflow-x-auto sm:mx-0'>
<Table className='text-sm'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead className={thClass}>{t('Group')}</TableHead>
<TableHead className={thClass}>{t('Ratio')}</TableHead>
{videoEntries.map((entry) => (
<TableHead
key={entry.resolution}
className={`${thClass} text-right`}
>
{entry.resolution}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{availableGroups.map((group) => {
const ratio = props.groupRatio[group] || 1
return (
<TableRow key={group}>
<TableCell className='py-2.5'>
<GroupBadge group={group} size='sm' />
</TableCell>
<TableCell className='text-muted-foreground py-2.5 font-mono text-xs'>
{ratio}x
</TableCell>
{videoEntries.map((entry) => (
<TableCell
key={entry.resolution}
className='py-2.5 text-right font-mono'
>
{formatVideoSecondPrice(
{
...props.model,
video_price: {
...props.model.video_price,
prices: { [entry.resolution]: entry.price },
},
},
showRechargePrice,
props.priceRate,
props.usdExchangeRate,
ratio
)}
</TableCell>
))}
</TableRow>
)
})}
</TableBody>
</Table>
<p className='text-muted-foreground/40 mt-1.5 px-4 text-[10px] sm:px-0'>
{t('Prices shown per second')}
</p>
</div>
</section>
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-details.tsx` around lines
607 - 671, The branch handling billing_mode === 'video_seconds' should detect
when getVideoPriceEntries(props.model) returns an empty array and avoid
rendering the empty group/ratio table; update the component to check
videoEntries.length and if zero either render a clear fallback message (e.g.,
"No video pricing configured") or return null before rendering
AutoGroupChain/Table, so users see that no usable video pricing exists; adjust
references around getVideoPriceEntries, AutoGroupChain, availableGroups, and the
formatVideoSecondPrice usage so the table and formatVideoSecondPrice are only
used when videoEntries is non-empty.

Comment on lines +979 to +983
{
label: 'Video per-second',
value: 'video_seconds',
count: modeCounts.video_seconds,
},

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 | ⚡ Quick win

Localize the new mode filter label.

This option label is rendered directly, so it won't switch with the rest of the UI when the locale changes.

🌐 Suggested fix
                     {
-                      label: 'Video per-second',
+                      label: t('Video per-second'),
                       value: 'video_seconds',
                       count: modeCounts.video_seconds,
                     },

As per coding guidelines, web/default/**/*.{tsx,ts}: "All user-facing text content must support i18n using the t() function from useTranslation() in React components"

📝 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
{
label: 'Video per-second',
value: 'video_seconds',
count: modeCounts.video_seconds,
},
{
label: t('Video per-second'),
value: 'video_seconds',
count: modeCounts.video_seconds,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx`
around lines 979 - 983, The label 'Video per-second' is hard-coded; replace it
with a translated string using the t() function from useTranslation() so the
filter label reacts to locale changes; import and call useTranslation() in the
component (e.g., ModelRatioVisualEditor) and change the entry with label: 'Video
per-second' to label: t('systemSettings.modeFilters.videoSeconds') (or another
appropriate i18n key consistent with your translations), leaving value:
'video_seconds' and count: modeCounts.video_seconds untouched.

Comment on lines +258 to 271
const resultUrl = log.result_url || failReason
const isUrl = resultUrl?.startsWith('http')

if (isSuccess && isVideoTask && isUrl) {
const videoUrl = `/v1/videos/${log.task_id}/content`
return (
<a
href={videoUrl}
href={resultUrl}
target='_blank'
rel='noopener noreferrer'
className='text-foreground text-xs hover:underline'
>
{t('Click to preview video')}
{t('Download video')}
</a>
)

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 | ⚡ Quick win

Verify URL selection logic for edge cases.

The current logic prefers log.result_url over failReason, but only displays a link if the chosen value starts with 'http'. If result_url exists but is invalid (e.g., a relative path or empty string), the code won't fall back to check if failReason contains a valid URL.

Consider checking both fields independently:

♻️ Proposed refinement
-        const resultUrl = log.result_url || failReason
-        const isUrl = resultUrl?.startsWith('http')
+        const resultUrl = log.result_url?.startsWith('http') 
+          ? log.result_url 
+          : failReason?.startsWith('http') 
+          ? failReason 
+          : null

-        if (isSuccess && isVideoTask && isUrl) {
+        if (isSuccess && isVideoTask && resultUrl) {
           return (
             <a
               href={resultUrl}
📝 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
const resultUrl = log.result_url || failReason
const isUrl = resultUrl?.startsWith('http')
if (isSuccess && isVideoTask && isUrl) {
const videoUrl = `/v1/videos/${log.task_id}/content`
return (
<a
href={videoUrl}
href={resultUrl}
target='_blank'
rel='noopener noreferrer'
className='text-foreground text-xs hover:underline'
>
{t('Click to preview video')}
{t('Download video')}
</a>
)
const resultUrl = (() => {
if (log.result_url?.startsWith('http')) {
return log.result_url
}
if (failReason?.startsWith('http')) {
return failReason
}
return null
})()
if (isSuccess && isVideoTask && resultUrl) {
return (
<a
href={resultUrl}
target='_blank'
rel='noopener noreferrer'
className='text-foreground text-xs hover:underline'
>
{t('Download video')}
</a>
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx`
around lines 258 - 271, The URL selection currently sets resultUrl =
log.result_url || failReason and only checks resultUrl.startsWith('http'), which
fails when log.result_url exists but is not a valid absolute URL; update the
logic in the component (around resultUrl, log.result_url, failReason, isSuccess,
isVideoTask) to independently validate both sources and choose the first that is
a valid absolute URL (e.g., check startsWith('http') on log.result_url first,
then on failReason), or else don't render the anchor; extract this into a small
helper like getValidHttpUrl(resultUrlCandidate, failReason) and use it where
isUrl is computed so the link reliably falls back to failReason when
log.result_url is present but invalid.

Comment on lines +200 to +205
if (other.video_duration != null) {
rows.push({
label: t('Duration'),
value: `${formatNumber(other.video_duration)}${t('seconds')}`,
})
}

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 | ⚡ Quick win

Missing space between number and translated unit "seconds" breaks English/French/Russian rendering.

${formatNumber(other.video_duration)}${t('seconds')} concatenates the number directly with the translated unit. In English this renders as e.g. 5seconds rather than 5 seconds. In Chinese () it happens to look correct, but the other locale files (en/fr/ru/ja) won't. Either insert a separator or use a count-aware interpolation key (e.g. t('duration_seconds', { count })).

🌐 Proposed fix
       rows.push({
         label: t('Duration'),
-        value: `${formatNumber(other.video_duration)}${t('seconds')}`,
+        value: t('duration_value_seconds', {
+          value: formatNumber(other.video_duration),
+          defaultValue: '{{value}} s',
+        }),
       })

Or, if you prefer to keep the existing key, add an explicit separator and update locale strings (e.g. " seconds" for en/fr/ru, "秒" for zh/ja). The same fix should be applied to the Billing formula row that also concatenates ${formatNumber(other.video_duration)}${t('seconds')}.

Also applies to: 216-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`
around lines 200 - 205, The code concatenates the formatted number and unit
without spacing (e.g. `${formatNumber(other.video_duration)}${t('seconds')}`)
causing locale rendering issues; update the two occurrences (the rows with label
'Duration' and the 'Billing formula' row that use
formatNumber(other.video_duration) + t('seconds')) to either include an explicit
separator before the translated unit (e.g.
`${formatNumber(other.video_duration)} ${t('seconds')}` and ensure locale
strings include a leading space where needed) or, preferably, use a count-aware
translation key such as `t('duration_seconds', { count: other.video_duration,
value: formatNumber(other.video_duration) })` (or `t('seconds', { count:
other.video_duration })` with interpolation) so pluralization and spacing are
correct across locales; apply the same change to both locations referencing
`formatNumber(other.video_duration)` + `t('seconds')`.

"{{value}}s": "{{value}}s",
"@lobehub/icons key": "@lobehub/icons key",
"@lobehub/icons key name": "@lobehub/icons key name",
"/ sec": "/ sec",

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 | 🏗️ Heavy lift

Use hierarchical semantic i18n keys for newly added entries

The new keys are literal UI text (e.g., "Video per-second", "Price per second") instead of semantic hierarchical identifiers. Please migrate these new entries to namespaced keys (for example: billing.video.perSecond.label) and update call sites accordingly to keep i18n consistent and maintainable.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

Also applies to: 491-491, 852-852, 860-860, 1789-1789, 2084-2084, 3002-3002, 3009-3009, 3118-3118, 3276-3276, 3312-3314, 3873-3873, 4274-4277

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/en.json` at line 54, The translation entry "/
sec" is a literal UI string and should be replaced with a hierarchical, semantic
i18n key (e.g., billing.video.perSecond.label) and the literal value moved under
that key; update all call sites that reference the flat string key to use the
new namespaced key (search for occurrences of "/ sec" and usages in components
that render per-second labels), add similar namespaced keys for the other listed
occurrences (lines 491, 852, 860, 1789, 2084, 3002, 3009, 3118, 3276, 3312-3314,
3873, 4274-4277) following the existing naming pattern (e.g., billing.*,
video.*, pricing.*), and ensure tests/translation loader still reference the new
keys.

"Requests per minute": "Requêtes par minute",
"requests served": "requêtes traitées",
"Requests will be forwarded to this worker. Trailing slashes are removed automatically.": "Les requêtes seront transmises à ce worker. Les barres obliques finales sont automatiquement supprimées.",
"Requests with higher FPS are multiplied by fps / base fps.": "Les demandes avec un FPS plus élevé sont multipliées par fps / FPS de base.",

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 | ⚡ Quick win

Use “requêtes” instead of “demandes” for API request terminology.

For consistency with the rest of the French UI, “requêtes” is the better term here.

✏️ Suggested fix
-    "Requests with higher FPS are multiplied by fps / base fps.": "Les demandes avec un FPS plus élevé sont multipliées par fps / FPS de base.",
+    "Requests with higher FPS are multiplied by fps / base fps.": "Les requêtes avec un FPS plus élevé sont multipliées par fps / FPS de base.",
📝 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
"Requests with higher FPS are multiplied by fps / base fps.": "Les demandes avec un FPS plus élevé sont multipliées par fps / FPS de base.",
"Requests with higher FPS are multiplied by fps / base fps.": "Les requêtes avec un FPS plus élevé sont multipliées par fps / FPS de base.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/fr.json` at line 3276, Replace the French word
"demandes" with "requêtes" in the translation value for the key whose English
text is "Requests with higher FPS are multiplied by fps / base fps." (the
current translation: "Les demandes avec un FPS plus élevé sont multipliées par
fps / FPS de base."). Edit the value in fr.json so it reads "Les requêtes avec
un FPS plus élevé sont multipliées par fps / FPS de base." to match UI
terminology consistency.

2.系统设置-站点与品牌新增了AI自动翻译模块,可将整站全部内容进行6国语言翻译,包括公告模型介绍等等。

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
controller/misc.go (1)

350-353: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace direct encoding/json usage with common.DecodeJson.

Line 352 directly uses json.NewDecoder(c.Request.Body).Decode(&req), which violates the coding guideline: "Use wrapper functions from common/json.go for all JSON marshal/unmarshal operations. Do NOT directly import or call encoding/json in business code." As per coding guidelines, all JSON operations must use the common package wrappers.

🐛 Proposed fix
 func ResetPassword(c *gin.Context) {
 	var req PasswordResetRequest
-	err := json.NewDecoder(c.Request.Body).Decode(&req)
+	err := common.DecodeJson(c.Request.Body, &req)
 	if req.Email == "" || req.Token == "" {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/misc.go` around lines 350 - 353, In ResetPassword, replace the
direct encoding/json decoder call with the common wrapper: change err :=
json.NewDecoder(c.Request.Body).Decode(&req) to err :=
common.DecodeJson(c.Request.Body, &req), keep the existing error handling (check
err and return the same response), and remove the direct use/import of
encoding/json; reference PasswordResetRequest and ResetPassword when making the
change so you update the correct decoder call and imports.
🧹 Nitpick comments (2)
web/default/src/features/system-settings/site/ai-translation-section.tsx (1)

205-213: ⚡ Quick win

Mask the API key input by default.

This field carries sensitive credentials and is currently displayed in clear text.

🔐 Suggested change
-                  <Input autoComplete='off' {...field} />
+                  <Input type='password' autoComplete='new-password' {...field} />

As per coding guidelines, "do not store sensitive information in frontend; use environment variables for configuration; prohibit hardcoding secrets."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/site/ai-translation-section.tsx`
around lines 205 - 213, The AITranslationAPIKey FormField currently renders the
API key in clear text; change the Input inside the render for the
AITranslationAPIKey field (in ai-translation-section.tsx within the FormField /
FormControl block) to be masked by default (e.g., use an Input with
type='password' or the library's secure input prop) and optionally add a reveal
toggle button to switch to plain text; ensure the field still uses form.control
and {...field} but does not render secrets as plain text by default.
service/ai_translation.go (1)

536-547: ⚡ Quick win

Remove redundant timeout configuration.

The HTTP request uses both context timeout (via NewRequestWithContext) and client timeout. The context passed to this function already has a timeout configured (line 145 in GenerateAITranslationSnapshot), making the client.Timeout at line 546 redundant. The context timeout will take precedence and cancel the request, but having both can cause confusion.

♻️ Proposed fix
-	client := &http.Client{Timeout: time.Duration(cfg.TimeoutSeconds) * time.Second}
+	client := &http.Client{}
 	resp, err := client.Do(req)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/ai_translation.go` around lines 536 - 547, The
doAITranslationHTTPRequest function sets a client-level timeout on http.Client
while the request already uses a context with timeout (created in
GenerateAITranslationSnapshot and passed into http.NewRequestWithContext), which
is redundant; remove the client.Timeout usage by creating the HTTP client
without setting Timeout (i.e., use &http.Client{} or a shared client) so the
request cancellation is driven solely by the context, and keep the rest of the
logic (request creation, headers, client.Do, logging) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/ai_translation.go`:
- Around line 58-68: The helper collectSource currently calls build() directly
and can let panics from builders like buildStatusResponse, buildNoticeResponse,
buildUserGroupsResponse, or buildPricingResponse crash the handler; add a
defer/recover inside collectSource around the call to build() to catch any
panic, log the recovered value and stack trace via common.SysLog (including the
scope name), and skip or set a safe payload when a panic occurs so you don’t
append a crashing payload to sources; ensure collectSource’s signature and the
append to sources remain intact and that normal (non-panicking) behavior is
unchanged.

In `@web/default/src/features/profile/components/language-preferences-card.tsx`:
- Around line 87-89: The call to i18n.changeLanguage(nextLanguage) must be moved
inside the guarded try/catch so any thrown error triggers the existing rollback
and setSaving(false) recovery; update the code in the function handling language
save to perform optimistic UI/state changes, then inside the try call await
i18n.changeLanguage(nextLanguage) and
refreshLanguageSensitiveQueries(queryClient), and keep the existing catch to
revert the optimistic update and call setSaving(false); apply the same change
for the other occurrence spanning the 90-119 region so all language-change
awaits are inside their try/catch blocks.

In `@web/default/src/i18n/locales/fr.json`:
- Line 4344: Fix the French translation for the cache-miss message by replacing
the incorrect "d absence" with the correct contracted form "d'absence" in the
JSON value for the key "Wait briefly on cache miss; timeout returns original
text and refreshes cache in the background." so the string reads "...en cas
d'absence du cache..." (update the value in fr.json for that exact key).

---

Outside diff comments:
In `@controller/misc.go`:
- Around line 350-353: In ResetPassword, replace the direct encoding/json
decoder call with the common wrapper: change err :=
json.NewDecoder(c.Request.Body).Decode(&req) to err :=
common.DecodeJson(c.Request.Body, &req), keep the existing error handling (check
err and return the same response), and remove the direct use/import of
encoding/json; reference PasswordResetRequest and ResetPassword when making the
change so you update the correct decoder call and imports.

---

Nitpick comments:
In `@service/ai_translation.go`:
- Around line 536-547: The doAITranslationHTTPRequest function sets a
client-level timeout on http.Client while the request already uses a context
with timeout (created in GenerateAITranslationSnapshot and passed into
http.NewRequestWithContext), which is redundant; remove the client.Timeout usage
by creating the HTTP client without setting Timeout (i.e., use &http.Client{} or
a shared client) so the request cancellation is driven solely by the context,
and keep the rest of the logic (request creation, headers, client.Do, logging)
unchanged.

In `@web/default/src/features/system-settings/site/ai-translation-section.tsx`:
- Around line 205-213: The AITranslationAPIKey FormField currently renders the
API key in clear text; change the Input inside the render for the
AITranslationAPIKey field (in ai-translation-section.tsx within the FormField /
FormControl block) to be masked by default (e.g., use an Input with
type='password' or the library's secure input prop) and optionally add a reveal
toggle button to switch to plain text; ensure the field still uses form.control
and {...field} but does not render secrets as plain text by default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c66ad489-20a0-4d39-9d1c-afaad607bbcb

📥 Commits

Reviewing files that changed from the base of the PR and between d6eb205 and 3344526.

📒 Files selected for processing (31)
  • controller/ai_translation.go
  • controller/ai_translation_paths.go
  • controller/group.go
  • controller/misc.go
  • controller/option.go
  • controller/pricing.go
  • controller/rankings.go
  • controller/subscription.go
  • controller/uptime_kuma.go
  • model/option.go
  • router/api-router.go
  • service/ai_translation.go
  • service/ai_translation_test.go
  • web/default/src/components/language-switcher.tsx
  • web/default/src/features/dashboard/components/overview/uptime-panel.tsx
  • web/default/src/features/profile/components/language-preferences-card.tsx
  • web/default/src/features/system-settings/api.ts
  • web/default/src/features/system-settings/hooks/use-update-option.ts
  • web/default/src/features/system-settings/site/ai-translation-section.tsx
  • web/default/src/features/system-settings/site/index.tsx
  • web/default/src/features/system-settings/site/section-registry.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/wallet/components/subscription-plans-card.tsx
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
  • web/default/src/lib/api.ts
  • web/default/src/lib/i18n-query-refresh.ts
✅ Files skipped from review due to trivial changes (3)
  • web/default/src/lib/i18n-query-refresh.ts
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/locales/ja.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/wallet/components/subscription-plans-card.tsx

Comment on lines +58 to +68
collectSource := func(scope string, build func() any, paths []string) {
start := time.Now()
payload := build()
sources = append(sources, service.AITranslationSource{Scope: scope, Payload: payload, Paths: paths})
common.SysLog("AI translation source collected: scope=" + scope + ", elapsed=" + time.Since(start).String())
}

collectSource("status", func() any { return buildStatusResponse() }, statusTranslationPaths)
collectSource("notice", func() any { return buildNoticeResponse() }, noticeTranslationPaths)
collectSource("user_groups", func() any { return buildUserGroupsResponse("default") }, userGroupsTranslationPaths)
collectSource("pricing", func() any { return buildPricingResponse("default") }, pricingTranslationPaths)

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 | ⚖️ Poor tradeoff

Add panic recovery to protect against builder function failures.

The inline collectSource helper invokes build() without panic recovery. If any builder function panics (e.g., buildStatusResponse, buildNoticeResponse, buildUserGroupsResponse, buildPricingResponse at lines 65-68), the entire request handler will crash. Consider adding panic recovery within collectSource or ensuring all builder functions are panic-safe.

🛡️ Proposed fix with panic recovery
 collectSource := func(scope string, build func() any, paths []string) {
+  defer func() {
+    if r := recover(); r != nil {
+      common.SysLog("AI translation source collection panic: scope=" + scope + ", error=" + fmt.Sprint(r))
+    }
+  }()
   start := time.Now()
   payload := build()
   sources = append(sources, service.AITranslationSource{Scope: scope, Payload: payload, Paths: paths})
   common.SysLog("AI translation source collected: scope=" + scope + ", elapsed=" + time.Since(start).String())
 }
📝 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
collectSource := func(scope string, build func() any, paths []string) {
start := time.Now()
payload := build()
sources = append(sources, service.AITranslationSource{Scope: scope, Payload: payload, Paths: paths})
common.SysLog("AI translation source collected: scope=" + scope + ", elapsed=" + time.Since(start).String())
}
collectSource("status", func() any { return buildStatusResponse() }, statusTranslationPaths)
collectSource("notice", func() any { return buildNoticeResponse() }, noticeTranslationPaths)
collectSource("user_groups", func() any { return buildUserGroupsResponse("default") }, userGroupsTranslationPaths)
collectSource("pricing", func() any { return buildPricingResponse("default") }, pricingTranslationPaths)
collectSource := func(scope string, build func() any, paths []string) {
defer func() {
if r := recover(); r != nil {
common.SysLog("AI translation source collection panic: scope=" + scope + ", error=" + fmt.Sprint(r))
}
}()
start := time.Now()
payload := build()
sources = append(sources, service.AITranslationSource{Scope: scope, Payload: payload, Paths: paths})
common.SysLog("AI translation source collected: scope=" + scope + ", elapsed=" + time.Since(start).String())
}
collectSource("status", func() any { return buildStatusResponse() }, statusTranslationPaths)
collectSource("notice", func() any { return buildNoticeResponse() }, noticeTranslationPaths)
collectSource("user_groups", func() any { return buildUserGroupsResponse("default") }, userGroupsTranslationPaths)
collectSource("pricing", func() any { return buildPricingResponse("default") }, pricingTranslationPaths)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/ai_translation.go` around lines 58 - 68, The helper collectSource
currently calls build() directly and can let panics from builders like
buildStatusResponse, buildNoticeResponse, buildUserGroupsResponse, or
buildPricingResponse crash the handler; add a defer/recover inside collectSource
around the call to build() to catch any panic, log the recovered value and stack
trace via common.SysLog (including the scope name), and skip or set a safe
payload when a panic occurs so you don’t append a crashing payload to sources;
ensure collectSource’s signature and the append to sources remain intact and
that normal (non-panicking) behavior is unchanged.

Comment thread web/default/src/i18n/locales/fr.json
2.设置页面-安全与限制-铭感词新增大模型审查
3.优化日志,如果审查开启,则会显示每次请求的审查结果
4.优化日志,针对请求失败的日志,会显示当此请求的脱敏后的请求头和请求体
2.新增管理员用户页面IP地址项
3.新增使用日志IP地址项
# Conflicts:
#	controller/rankings.go
#	web/default/index.html
#	web/default/src/components/language-switcher.tsx
#	web/default/src/components/layout/components/chat-presets-item.tsx
#	web/default/src/features/profile/components/language-preferences-card.tsx
#	web/default/src/i18n/locales/en.json
#	web/default/src/i18n/locales/fr.json
#	web/default/src/i18n/locales/ja.json
#	web/default/src/i18n/locales/ru.json
#	web/default/src/i18n/locales/vi.json
#	web/default/src/i18n/locales/zh.json
2.修复了请求流式阶段报错记录。
3.修复一个测试脚本
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.

3 participants