Codex/sync upstream 20260709 - #6055
Conversation
WalkthroughThe PR adds GHCR image publishing, makes subscription plans publicly accessible, introduces a pricing-based home page, updates landing-page visuals, and removes font and xl-density theme customization. ChangesContainer publishing
Public subscription plans
Frontend home and theme experience
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Visitor
participant Home
participant PublicAPI
participant BrowserStorage
Visitor->>Home: Open landing page
Home->>BrowserStorage: Read cached home content
Home->>PublicAPI: Fetch status, pricing, plans, and home content
PublicAPI-->>Home: Return public data
Home->>BrowserStorage: Cache home content
Home-->>Visitor: Render custom content or pricing landing page
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (1)
web/default/src/features/home/index.tsx (1)
412-426: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winEliminate DRY violations: reuse
useHomePageContenthook and extract pricing notice Alert.
displayHomePageContent(lines 412–426) duplicates the logic inuseHomePageContent(use-home-page-content.ts). The inline version has weaker error handling (no toast), uses a hardcoded localStorage key, and doesn't guard againstdatabeingundefinedwhensuccessis true. Use the existing hook instead.Pricing notice Alert (lines 708–729 and 736–756) is duplicated verbatim. Extract a
PricingNoticeAlertcomponent to avoid drift.♻️ Proposed refactor: extract pricing notice component
function PricingNoticeAlert({ className }: { className?: string }) { if (!pricingNoticeConfig.enabled) return null return ( <Alert className={cn('text-center backdrop-blur-sm', className)}> <AlertDescription> {pricingNoticeConfig.text} {pricingNoticeConfig.linkText && pricingNoticeConfig.linkUrl ? ( <> {' '} <a href={pricingNoticeConfig.linkUrl} target='_blank' rel='noreferrer noopener' className='font-semibold' > {pricingNoticeConfig.linkText} </a> </> ) : null} </AlertDescription> </Alert> ) }Also applies to: 708-756
🤖 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/home/index.tsx` around lines 412 - 426, Replace the inline displayHomePageContent implementation with the existing useHomePageContent hook, including its loading state and error handling. Extract the duplicated pricing notice markup into a reusable PricingNoticeAlert component accepting an optional className, then render it in both existing locations while preserving the enabled check, configured text, and optional link.
🤖 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/ghcr-build.yml:
- Around line 19-20: Add persist-credentials: false to the actions/checkout@v4
step in the workflow’s “Check out” step, ensuring checkout does not store the
GITHUB_TOKEN in .git/config before the Docker build.
In `@router/api-router.go`:
- Line 35: Update controller.GetSubscriptionPlans to map each
model.SubscriptionPlan to a dedicated public response DTO containing only
client-safe plan fields, excluding stripe_price_id, creem_product_id,
waffo_pancake_product_id, upgrade_group, and downgrade_group; return the DTO
collection from the /subscription/plans endpoint instead of exposing
model.SubscriptionPlan directly.
In `@web/default/src/features/home/components/sections/model-pricing.tsx`:
- Around line 27-33: Update formatPrice to use getCurrencyDisplay() rather than
pricingCurrencyConfig.symbol, matching the implementation in index.tsx and
ensuring the displayed currency is correctly converted and labeled. Remove the
direct pricingCurrencyConfig symbol usage while preserving the existing
validation and decimal formatting behavior.
In `@web/default/src/features/home/index.tsx`:
- Around line 140-145: Update the listed Chinese translation keys in the home
page component to English source strings while preserving their meanings, and
wrap all rendered pricingHeaderConfig and imagePricingHeaderConfig values with
t(). Refactor formatSubscriptionPrice to obtain the symbol from
getCurrencyDisplay() instead of hardcoding ¥, while retaining numeric validation
and two-decimal formatting.
- Around line 451-466: Sanitize the HTML returned as homePageContent before
rendering it via dangerouslySetInnerHTML. Add a DOMPurify (or equivalent)
dependency/import, apply it to the non-URL content branch, and pass the
sanitized result to dangerouslySetInnerHTML while preserving the existing iframe
handling for HTTPS URLs.
- Around line 389-410: Update the image pricing calculation in useMemo
imageModelPricingRows so prices represent every configured image size instead of
always using multiplier 1. Derive the multipliers from configItem.types,
calculate the USD range for each via getImagePriceRangeUSD, and combine them
into an overall range before passing it to formatPerRequestPriceRange. Preserve
the existing grouped row and displayed type labels.
In `@web/default/src/features/home/model-pricing-config.ts`:
- Around line 40-53: The pricing UI configuration contains hardcoded Chinese
text and bypasses localization. Update pricingHeaderConfig,
imagePricingHeaderConfig, and pricingNoticeConfig.text, then adjust their
consumption in index.tsx to use the i18n t() function with English source-string
keys, or move the labels into locale files and translate them at render time.
- Around line 29-34: Fix the currency/symbol mismatch in pricingCurrencyConfig
by changing the USD symbol from ¥ to $, or update the configuration to derive
the symbol through getCurrencyDisplay() if dynamic currency handling is
required; ensure ModelPricing renders the documented USD prices consistently.
In `@web/default/src/styles/index.css`:
- Around line 47-51: Fix the Stylelint declaration-empty-line-before error in
the root stylesheet rule by inserting an empty line between the `@apply`
declaration and the background-image declaration; keep the existing
background-image gradients and background-attachment declarations unchanged.
- Around line 47-51: Update the body background styling near the existing radial
gradients to provide dark-mode-specific colors: replace the hardcoded light warm
stops with theme-aware tokens or add a `.dark body` `background-image` override
using suitably muted dark gradients, while preserving the existing light-mode
appearance and fixed attachment.
---
Nitpick comments:
In `@web/default/src/features/home/index.tsx`:
- Around line 412-426: Replace the inline displayHomePageContent implementation
with the existing useHomePageContent hook, including its loading state and error
handling. Extract the duplicated pricing notice markup into a reusable
PricingNoticeAlert component accepting an optional className, then render it in
both existing locations while preserving the enabled check, configured text, and
optional link.
🪄 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: 0a7127d6-88f3-4eac-9ae5-47d8147294cf
📒 Files selected for processing (23)
.github/workflows/ghcr-build.ymlrouter/api-router.goweb/default/src/components/config-drawer.tsxweb/default/src/context/theme-customization-provider.tsxweb/default/src/features/home/api.tsweb/default/src/features/home/components/gateway-card.tsxweb/default/src/features/home/components/hero-buttons.tsxweb/default/src/features/home/components/hero-terminal-demo.tsxweb/default/src/features/home/components/index.tsweb/default/src/features/home/components/scrolling-icons.tsxweb/default/src/features/home/components/sections/cta.tsxweb/default/src/features/home/components/sections/features.tsxweb/default/src/features/home/components/sections/hero.tsxweb/default/src/features/home/components/sections/how-it-works.tsxweb/default/src/features/home/components/sections/model-pricing.tsxweb/default/src/features/home/hooks/use-home-page-content.tsweb/default/src/features/home/index.tsxweb/default/src/features/home/model-pricing-config.tsweb/default/src/lib/theme-customization.tsweb/default/src/routes/index.tsxweb/default/src/styles/index.cssweb/default/src/styles/theme-presets.cssweb/default/src/styles/theme.css
💤 Files with no reviewable changes (7)
- web/default/src/features/home/components/gateway-card.tsx
- web/default/src/features/home/api.ts
- web/default/src/features/home/components/hero-buttons.tsx
- web/default/src/features/home/components/scrolling-icons.tsx
- web/default/src/features/home/components/sections/features.tsx
- web/default/src/features/home/components/sections/how-it-works.tsx
- web/default/src/routes/index.tsx
| - name: Check out | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Set persist-credentials: false on the checkout step.
actions/checkout@v4 defaults to persist-credentials: true, which stores the GITHUB_TOKEN in .git/config on the runner. Since the Dockerfile copies the entire build context (COPY . .), these credentials could leak into the image if .git is not excluded by .dockerignore. Adding persist-credentials: false is a simple hardening measure since this workflow only needs to read the source, not push back to the repo.
🔒 Proposed fix
- name: Check out
uses: actions/checkout@v4
+ with:
+ persist-credentials: false📝 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.
| - name: Check out | |
| uses: actions/checkout@v4 | |
| - name: Check out | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 19-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 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/ghcr-build.yml around lines 19 - 20, Add
persist-credentials: false to the actions/checkout@v4 step in the workflow’s
“Check out” step, ensuring checkout does not store the GITHUB_TOKEN in
.git/config before the Docker build.
Source: Linters/SAST tools
| //apiRouter.GET("/midjourney", controller.GetMidjourney) | ||
| apiRouter.GET("/home_page_content", controller.GetHomePageContent) | ||
| apiRouter.GET("/pricing", middleware.HeaderNavModuleAuth("pricing"), controller.GetPricing) | ||
| apiRouter.GET("/subscription/plans", controller.GetSubscriptionPlans) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the SubscriptionPlan model definition and its JSON tags
fd -t f 'subscription' model/ | head
rg -nP -C2 'type\s+SubscriptionPlan\s+struct' --type=go
# Show full struct with json tags to audit exposed fields
ast-grep run --pattern 'type SubscriptionPlan struct { $$$ }' --lang go
# Check NormalizeDefaults to confirm it does not populate sensitive data
rg -nP -C3 'func .*NormalizeDefaults' --type=goRepository: QuantumNous/new-api
Length of output: 471
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- model/subscription.go (lines) ---'
wc -l model/subscription.go
cat -n model/subscription.go | sed -n '1,260p'
echo '--- search GetSubscriptionPlans / DTO ---'
rg -n -C 3 'GetSubscriptionPlans|SubscriptionPlanDTO|type .*SubscriptionPlan.*DTO|subscription/plans' controller router modelRepository: QuantumNous/new-api
Length of output: 12692
Hide internal plan metadata from the public plans endpoint
controller.GetSubscriptionPlans returns model.SubscriptionPlan directly, so unauthenticated callers receive fields like stripe_price_id, creem_product_id, waffo_pancake_product_id, upgrade_group, and downgrade_group. Map this to a public DTO and omit internal/provider-specific fields.
🤖 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 `@router/api-router.go` at line 35, Update controller.GetSubscriptionPlans to
map each model.SubscriptionPlan to a dedicated public response DTO containing
only client-safe plan fields, excluding stripe_price_id, creem_product_id,
waffo_pancake_product_id, upgrade_group, and downgrade_group; return the DTO
collection from the /subscription/plans endpoint instead of exposing
model.SubscriptionPlan directly.
| function formatPrice(value?: number): string { | ||
| if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { | ||
| return '-' | ||
| } | ||
| const { symbol } = pricingCurrencyConfig | ||
| return `${symbol}${value.toFixed(value >= 1 ? 2 : 4).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')}` | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use getCurrencyDisplay() instead of pricingCurrencyConfig.symbol for currency formatting.
formatPrice here uses pricingCurrencyConfig.symbol (currently '¥') to format USD-denominated prices, producing incorrect output like ¥70. The formatPrice in index.tsx uses getCurrencyDisplay() which properly handles currency conversion and symbols. Align this implementation to avoid divergence and incorrect display.
🔧 Proposed fix
+import { getCurrencyDisplay } from '`@/lib/currency`'
+
-function formatPrice(value?: number): string {
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
- return '-'
- }
- const { symbol } = pricingCurrencyConfig
- return `${symbol}${value.toFixed(value >= 1 ? 2 : 4).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')}`
-}
+function formatPrice(value?: number): string {
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
+ return '-'
+ }
+ const { meta } = getCurrencyDisplay()
+ if (meta.kind === 'custom') {
+ return `${meta.symbol}${(value * meta.exchangeRate).toFixed(2)}`
+ }
+ if (meta.kind === 'currency') {
+ return new Intl.NumberFormat(undefined, {
+ style: 'currency',
+ currency: meta.currencyCode,
+ currencyDisplay: 'narrowSymbol',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 2,
+ }).format(value * meta.exchangeRate)
+ }
+ return `$${value.toFixed(2)}`
+}📝 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.
| function formatPrice(value?: number): string { | |
| if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { | |
| return '-' | |
| } | |
| const { symbol } = pricingCurrencyConfig | |
| return `${symbol}${value.toFixed(value >= 1 ? 2 : 4).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')}` | |
| } | |
| import { getCurrencyDisplay } from '`@/lib/currency`' | |
| function formatPrice(value?: number): string { | |
| if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { | |
| return '-' | |
| } | |
| const { meta } = getCurrencyDisplay() | |
| if (meta.kind === 'custom') { | |
| return `${meta.symbol}${(value * meta.exchangeRate).toFixed(2)}` | |
| } | |
| if (meta.kind === 'currency') { | |
| return new Intl.NumberFormat(undefined, { | |
| style: 'currency', | |
| currency: meta.currencyCode, | |
| currencyDisplay: 'narrowSymbol', | |
| minimumFractionDigits: 0, | |
| maximumFractionDigits: 2, | |
| }).format(value * meta.exchangeRate) | |
| } | |
| return `$${value.toFixed(2)}` | |
| } |
🤖 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/home/components/sections/model-pricing.tsx` around
lines 27 - 33, Update formatPrice to use getCurrencyDisplay() rather than
pricingCurrencyConfig.symbol, matching the implementation in index.tsx and
ensuring the displayed currency is correctly converted and labeled. Remove the
direct pricingCurrencyConfig symbol usage while preserving the existing
validation and decimal formatting behavior.
| function formatSubscriptionPrice(amount: number | string): string { | ||
| const numeric = | ||
| typeof amount === 'number' ? amount : Number.parseFloat(String(amount)) | ||
| if (!Number.isFinite(numeric)) return '-' | ||
| return `¥${numeric.toFixed(2)}` | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Fix i18n violations: use English t() keys, wrap config values in t(), and avoid hardcoded currency symbols.
Multiple i18n issues in this file:
- Chinese
t()keys:t('已复制到剪切板'),t('复制失败'),t('直连官方的'),t('企业级接口网关'),t('获取密钥'),t('模型价格对比'),t('暂无价格数据'),t('图像模型')— As per coding guidelines, use English source strings as keys. - Config values without
t():pricingHeaderConfig.*andimagePricingHeaderConfig.*are Chinese strings rendered directly in JSX (lines 556–571, 658–664) withoutt(), so they won't translate. formatSubscriptionPricehardcodes¥(lines 140–145): Ignores the system currency config (getCurrencyDisplay()), producing incorrect symbols for non-CNY deployments.
🔧 Proposed fixes
Fix Chinese t() keys to English:
- toast.success(t('已复制到剪切板'))
+ toast.success(t('Copied to clipboard'))
- toast.error(t('复制失败'))
+ toast.error(t('Copy failed'))Fix formatSubscriptionPrice to use currency config:
-function formatSubscriptionPrice(amount: number | string): string {
- const numeric =
- typeof amount === 'number' ? amount : Number.parseFloat(String(amount))
- if (!Number.isFinite(numeric)) return '-'
- return `¥${numeric.toFixed(2)}`
-}
+function formatSubscriptionPrice(amount: number | string): string {
+ const numeric =
+ typeof amount === 'number' ? amount : Number.parseFloat(String(amount))
+ if (!Number.isFinite(numeric)) return '-'
+ const { meta } = getCurrencyDisplay()
+ const converted = numeric * meta.exchangeRate
+ return `${meta.symbol}${converted.toFixed(2)}`
+}Also applies to: 431-433, 488-495, 556-571
🤖 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/home/index.tsx` around lines 140 - 145, Update the
listed Chinese translation keys in the home page component to English source
strings while preserving their meanings, and wrap all rendered
pricingHeaderConfig and imagePricingHeaderConfig values with t(). Refactor
formatSubscriptionPrice to obtain the symbol from getCurrencyDisplay() instead
of hardcoding ¥, while retaining numeric validation and two-decimal formatting.
Source: Coding guidelines
| const imageModelPricingRows = useMemo<ImageModelPricingRow[]>(() => { | ||
| const pricingModels = pricingData?.data || [] | ||
| const groupRatios = pricingData?.group_ratio || {} | ||
| const usableGroups = pricingData?.usable_group || {} | ||
| const modelMap = new Map( | ||
| pricingModels.map((model) => [model.model_name, model]) | ||
| ) | ||
|
|
||
| return imageModelPricingConfig.map((configItem) => { | ||
| const typeLabels = configItem.types.map((typeItem) => typeItem.type) | ||
| const model = findPricingModel(modelMap, [configItem.name]) | ||
| const priceRange = model | ||
| ? getImagePriceRangeUSD(model, groupRatios, usableGroups, 1) | ||
| : null | ||
|
|
||
| return { | ||
| name: configItem.name, | ||
| types: typeLabels.join('、'), | ||
| price: formatPerRequestPriceRange(priceRange), | ||
| } | ||
| }) | ||
| }, [pricingData]) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Image model pricing only computes price for 1K multiplier, misleading users.
getImagePriceRangeUSD is called with multiplier: 1 (line 401), but imageModelPricingConfig defines multipliers 1, 4, and 16 for 1K, 2K, and 4K respectively. The displayed types field shows "1K、2K、4K" but the price only reflects the 1K variant. Users see all three types listed but a price that doesn't cover 2K or 4K.
Either compute a price range across all multipliers, or render separate rows per type with correct prices.
🔧 Proposed fix: compute range across all multipliers
return imageModelPricingConfig.map((configItem) => {
const typeLabels = configItem.types.map((typeItem) => typeItem.type)
const model = findPricingModel(modelMap, [configItem.name])
- const priceRange = model
- ? getImagePriceRangeUSD(model, groupRatios, usableGroups, 1)
- : null
+ const priceRanges = model
+ ? configItem.types.map((typeItem) =>
+ getImagePriceRangeUSD(model, groupRatios, usableGroups, typeItem.multiplier)
+ ).filter((r): r is { min: number; max: number } => r !== null)
+ : []
+ const priceRange = priceRanges.length > 0
+ ? {
+ min: Math.min(...priceRanges.map((r) => r.min)),
+ max: Math.max(...priceRanges.map((r) => r.max)),
+ }
+ : null
return {
name: configItem.name,
types: typeLabels.join('、'),
price: formatPerRequestPriceRange(priceRange),
}
})📝 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.
| const imageModelPricingRows = useMemo<ImageModelPricingRow[]>(() => { | |
| const pricingModels = pricingData?.data || [] | |
| const groupRatios = pricingData?.group_ratio || {} | |
| const usableGroups = pricingData?.usable_group || {} | |
| const modelMap = new Map( | |
| pricingModels.map((model) => [model.model_name, model]) | |
| ) | |
| return imageModelPricingConfig.map((configItem) => { | |
| const typeLabels = configItem.types.map((typeItem) => typeItem.type) | |
| const model = findPricingModel(modelMap, [configItem.name]) | |
| const priceRange = model | |
| ? getImagePriceRangeUSD(model, groupRatios, usableGroups, 1) | |
| : null | |
| return { | |
| name: configItem.name, | |
| types: typeLabels.join('、'), | |
| price: formatPerRequestPriceRange(priceRange), | |
| } | |
| }) | |
| }, [pricingData]) | |
| const imageModelPricingRows = useMemo<ImageModelPricingRow[]>(() => { | |
| const pricingModels = pricingData?.data || [] | |
| const groupRatios = pricingData?.group_ratio || {} | |
| const usableGroups = pricingData?.usable_group || {} | |
| const modelMap = new Map( | |
| pricingModels.map((model) => [model.model_name, model]) | |
| ) | |
| return imageModelPricingConfig.map((configItem) => { | |
| const typeLabels = configItem.types.map((typeItem) => typeItem.type) | |
| const model = findPricingModel(modelMap, [configItem.name]) | |
| const priceRanges = model | |
| ? configItem.types | |
| .map((typeItem) => | |
| getImagePriceRangeUSD( | |
| model, | |
| groupRatios, | |
| usableGroups, | |
| typeItem.multiplier | |
| ) | |
| ) | |
| .filter( | |
| (r): r is { min: number; max: number } => r !== null | |
| ) | |
| : [] | |
| const priceRange = | |
| priceRanges.length > 0 | |
| ? { | |
| min: Math.min(...priceRanges.map((r) => r.min)), | |
| max: Math.max(...priceRanges.map((r) => r.max)), | |
| } | |
| : null | |
| return { | |
| name: configItem.name, | |
| types: typeLabels.join('、'), | |
| price: formatPerRequestPriceRange(priceRange), | |
| } | |
| }) | |
| }, [pricingData]) |
🤖 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/home/index.tsx` around lines 389 - 410, Update the
image pricing calculation in useMemo imageModelPricingRows so prices represent
every configured image size instead of always using multiplier 1. Derive the
multipliers from configItem.types, calculate the USD range for each via
getImagePriceRangeUSD, and combine them into an overall range before passing it
to formatPerRequestPriceRange. Preserve the existing grouped row and displayed
type labels.
| if (homePageContent) { | ||
| return ( | ||
| <PublicLayout> | ||
| <div className='mx-auto max-w-6xl px-4 py-8'> | ||
| <RichContent | ||
| mode='markdown' | ||
| content={content} | ||
| className='custom-home-content' | ||
| /> | ||
| </div> | ||
| <PublicLayout showMainContainer={false}> | ||
| <main className='w-full overflow-x-hidden'> | ||
| {homePageContent.startsWith('https://') ? ( | ||
| <iframe | ||
| src={homePageContent} | ||
| className='h-screen w-full border-none' | ||
| title={t('Custom Home Page')} | ||
| /> | ||
| ) : ( | ||
| <div | ||
| className='mt-[60px]' | ||
| dangerouslySetInnerHTML={{ __html: homePageContent }} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Sanitize dangerouslySetInnerHTML content to prevent XSS.
Content from /api/home_page_content is injected directly into the DOM without sanitization. If the endpoint is accessible to non-admin users or the backend content can be manipulated, this exposes the page to stored XSS. Use DOMPurify (or equivalent) to sanitize the HTML before injection.
🔒 Proposed fix
+import DOMPurify from 'dompurify'
+
) : (
<div
className='mt-[60px]'
- dangerouslySetInnerHTML={{ __html: homePageContent }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(homePageContent) }}
/>
)}📝 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.
| if (homePageContent) { | |
| return ( | |
| <PublicLayout> | |
| <div className='mx-auto max-w-6xl px-4 py-8'> | |
| <RichContent | |
| mode='markdown' | |
| content={content} | |
| className='custom-home-content' | |
| /> | |
| </div> | |
| <PublicLayout showMainContainer={false}> | |
| <main className='w-full overflow-x-hidden'> | |
| {homePageContent.startsWith('https://') ? ( | |
| <iframe | |
| src={homePageContent} | |
| className='h-screen w-full border-none' | |
| title={t('Custom Home Page')} | |
| /> | |
| ) : ( | |
| <div | |
| className='mt-[60px]' | |
| dangerouslySetInnerHTML={{ __html: homePageContent }} | |
| /> | |
| )} | |
| if (homePageContent) { | |
| return ( | |
| <PublicLayout showMainContainer={false}> | |
| <main className='w-full overflow-x-hidden'> | |
| {homePageContent.startsWith('https://') ? ( | |
| <iframe | |
| src={homePageContent} | |
| className='h-screen w-full border-none' | |
| title={t('Custom Home Page')} | |
| /> | |
| ) : ( | |
| <div | |
| className='mt-[60px]' | |
| dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(homePageContent) }} | |
| /> | |
| )} |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 463-463: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation
(react-unsafe-html-injection)
🤖 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/home/index.tsx` around lines 451 - 466, Sanitize the
HTML returned as homePageContent before rendering it via
dangerouslySetInnerHTML. Add a DOMPurify (or equivalent) dependency/import,
apply it to the non-URL content branch, and pass the sanitized result to
dangerouslySetInnerHTML while preserving the existing iframe handling for HTTPS
URLs.
| export const pricingCurrencyConfig = { | ||
| // 货币类型:'USD' 或 'CNY' | ||
| currency: 'USD' as 'USD' | 'CNY', | ||
| // 显示符号 | ||
| symbol: '¥', | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fix pricingCurrencyConfig currency/symbol mismatch.
currency is set to 'USD' but symbol is '¥' (CNY symbol). This causes ModelPricing (in model-pricing.tsx) to render USD prices with a ¥ symbol, e.g., ¥70 for a $70 price. The modelPricingConfig entries are documented as USD ("单位 USD"), so the symbol should be '$' or this should use getCurrencyDisplay() for proper currency handling.
🔧 Proposed fix
export const pricingCurrencyConfig = {
// 货币类型:'USD' 或 'CNY'
currency: 'USD' as 'USD' | 'CNY',
// 显示符号
- symbol: '¥',
+ symbol: '$',
}📝 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.
| export const pricingCurrencyConfig = { | |
| // 货币类型:'USD' 或 'CNY' | |
| currency: 'USD' as 'USD' | 'CNY', | |
| // 显示符号 | |
| symbol: '¥', | |
| } | |
| export const pricingCurrencyConfig = { | |
| // 货币类型:'USD' 或 'CNY' | |
| currency: 'USD' as 'USD' | 'CNY', | |
| // 显示符号 | |
| symbol: '$', | |
| } |
🤖 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/home/model-pricing-config.ts` around lines 29 - 34,
Fix the currency/symbol mismatch in pricingCurrencyConfig by changing the USD
symbol from ¥ to $, or update the configuration to derive the symbol through
getCurrencyDisplay() if dynamic currency handling is required; ensure
ModelPricing renders the documented USD prices consistently.
| export const pricingHeaderConfig = { | ||
| model: '模型', | ||
| input: '输入(1M)', | ||
| output: '输出(1M)', | ||
| official: '官方输入/输出(1M)', | ||
| discount: '折扣', | ||
| cacheHit: '缓存命中', | ||
| } | ||
|
|
||
| export const imagePricingHeaderConfig = { | ||
| model: '模型名称', | ||
| type: '图像类型', | ||
| price: '价格', | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Make config text i18n-ready.
pricingHeaderConfig, imagePricingHeaderConfig, and pricingNoticeConfig.text contain hardcoded Chinese strings. These are rendered directly in index.tsx without t(), so non-Chinese users see untranslated text. As per coding guidelines, frontend UI text must support i18n with English source strings as keys.
Either move these to i18n locale files and use t() at the consumption site, or wrap the values in t() calls.
Also applies to: 153-160
🤖 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/home/model-pricing-config.ts` around lines 40 - 53,
The pricing UI configuration contains hardcoded Chinese text and bypasses
localization. Update pricingHeaderConfig, imagePricingHeaderConfig, and
pricingNoticeConfig.text, then adjust their consumption in index.tsx to use the
i18n t() function with English source-string keys, or move the labels into
locale files and translate them at render time.
Source: Coding guidelines
| @apply bg-background text-foreground has-[div[data-variant='inset']]:bg-sidebar min-h-svh w-full font-sans; | ||
| background-image: | ||
| radial-gradient(circle at 14% 8%, oklch(0.96 0.025 68 / 65%), transparent 38%), | ||
| radial-gradient(circle at 86% 0%, oklch(0.93 0.04 48 / 35%), transparent 42%); | ||
| background-attachment: fixed; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix Stylelint error: add empty line before background-image declaration.
Stylelint reports declaration-empty-line-before at lines 48-50. Per coding guidelines, lint errors in changed files must be fixed before completing code changes.
🔧 Proposed fix
`@apply` bg-background text-foreground has-[div[data-variant='inset']]:bg-sidebar min-h-svh w-full font-sans;
+
background-image:
radial-gradient(circle at 14% 8%, oklch(0.96 0.025 68 / 65%), transparent 38%),
radial-gradient(circle at 86% 0%, oklch(0.93 0.04 48 / 35%), transparent 42%);📝 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.
| @apply bg-background text-foreground has-[div[data-variant='inset']]:bg-sidebar min-h-svh w-full font-sans; | |
| background-image: | |
| radial-gradient(circle at 14% 8%, oklch(0.96 0.025 68 / 65%), transparent 38%), | |
| radial-gradient(circle at 86% 0%, oklch(0.93 0.04 48 / 35%), transparent 42%); | |
| background-attachment: fixed; | |
| `@apply` bg-background text-foreground has-[div[data-variant='inset']]:bg-sidebar min-h-svh w-full font-sans; | |
| background-image: | |
| radial-gradient(circle at 14% 8%, oklch(0.96 0.025 68 / 65%), transparent 38%), | |
| radial-gradient(circle at 86% 0%, oklch(0.93 0.04 48 / 35%), transparent 42%); | |
| background-attachment: fixed; |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 48-50: Expected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
🤖 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/styles/index.css` around lines 47 - 51, Fix the Stylelint
declaration-empty-line-before error in the root stylesheet rule by inserting an
empty line between the `@apply` declaration and the background-image declaration;
keep the existing background-image gradients and background-attachment
declarations unchanged.
Sources: Coding guidelines, Linters/SAST tools
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for a dark-mode body background override in index.css.
rg -nU -A3 '\.dark\s+body\b|\.dark\s*\{[^}]*background-image' web/default/src/styles/index.css
# Also check theme.css for any body background override.
rg -nU -A3 '\.dark\s+body\b|body\s*\{[^}]*background' web/default/src/styles/theme.cssRepository: QuantumNous/new-api
Length of output: 157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant stylesheet files.
git ls-files 'web/default/src/styles/*' 'web/default/AGENTS.md'
# Show outlines/lengths before reading.
for f in web/default/src/styles/index.css web/default/src/styles/theme.css web/default/AGENTS.md; do
[ -f "$f" ] && { echo "== $f =="; wc -l "$f"; }
done
# Read the relevant sections with line numbers.
echo '== index.css =='
cat -n web/default/src/styles/index.css | sed -n '1,220p'
echo '== theme.css =='
cat -n web/default/src/styles/theme.css | sed -n '1,220p'
echo '== AGENTS.md =='
cat -n web/default/AGENTS.md | sed -n '1,220p'Repository: QuantumNous/new-api
Length of output: 25738
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focus on background-related declarations and dark-mode selectors in the styles tree.
rg -n --hidden --glob 'web/default/src/styles/**' \
'(background-image|background-attachment|body\b|\.dark\b|color-scheme|theme)' \
web/default/src/stylesRepository: QuantumNous/new-api
Length of output: 5378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether the body background gradient is defined using CSS variables elsewhere.
rg -n --hidden --glob 'web/default/src/styles/**' \
'oklch\(|radial-gradient|--.*background|--.*foreground|bg-background' \
web/default/src/stylesRepository: QuantumNous/new-api
Length of output: 32929
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the exact section in web/default/src/styles/index.css around the reported lines.
cat -n web/default/src/styles/index.css | sed -n '1,140p'
# Search the entire web/default/src tree for dark body/background overrides.
rg -n --hidden --glob 'web/default/src/**' '\.dark\s+body\b|body\s*\{[^}]*background|background-image' web/default/srcRepository: QuantumNous/new-api
Length of output: 5329
Add a dark-mode override for the body gradients. The hardcoded warm gradients stay bright in .dark; switch them to theme-aware tokens or add a .dark body background-image variant.
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 48-50: Expected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
🤖 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/styles/index.css` around lines 47 - 51, Update the body
background styling near the existing radial gradients to provide
dark-mode-specific colors: replace the hardcoded light warm stops with
theme-aware tokens or add a `.dark body` `background-image` override using
suitably muted dark gradients, while preserving the existing light-mode
appearance and fixed attachment.
Important
📝 变更描述 / Description
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
Summary by CodeRabbit
New Features
UI Improvements