Skip to content

feat: SEO improvements, invite rebate fixes, upstream sync - #6531

Open
seven7763 wants to merge 32 commits into
QuantumNous:mainfrom
seven7763:feat/daoxe-customizations
Open

feat: SEO improvements, invite rebate fixes, upstream sync#6531
seven7763 wants to merge 32 commits into
QuantumNous:mainfrom
seven7763:feat/daoxe-customizations

Conversation

@seven7763

@seven7763 seven7763 commented Jul 29, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

本次合并包含以下内容:

Upstream 同步

  • 合并上游 21 个 commit(relaykit 模块拆分、OIDC 自定义名称、CI workflows、per-channel HTTP transport 等)

SEO 功能完善

  • 管理后台 SEO 设置重构为独立 section + RobotsIndex 开关
  • sitemap.xml 增加 lastmod/changefreq/priority + 更多路由
  • robots.txt 增加更多 Disallow 路径(/token /topup /log /setting 等)
  • og:site_name 修复为始终使用品牌短名
  • 保存 SEO 设置后立即刷新前端 status 缓存
  • controller/seo.go 加 mutex 并发保护 + XML 转义
  • 新增 readCachedStatus 共享 helper,去除重复 localStorage 读取

邀请返利维护

  • 系统任务面板增加 invite_rebate_backfill 类型标签和中文翻译

文档站优化

  • 配色统一对齐主站(neutral + 蓝色 primary)
  • 导航栏与主站完全一致(滚动前透明、滚动后胶囊)
  • Hero 组件升级(装饰元素、渐变、更大间距)
  • 卡片 hover 效果增强
  • 代码块行高优化、表格斑马纹
  • 文档助手增加 fenced code block 语法高亮
  • DeepChat URL 修正为 deepchat.thinkinai.xyz
  • 系统公告 Markdown 渲染修复
  • 首页面包屑隐藏

i18n

  • 7 种语言补齐 SEO/任务相关翻译

🚀 变更类型 / Type of change

  • ✨ 新功能 (New feature)
  • 🐛 Bug 修复 (Bug fix)
  • ⚡ 性能优化 / 重构 (Refactor)

✅ 提交前检查项 / Checklist

  • Go build 通过
  • Frontend build 通过
  • 所有测试通过
  • 已部署验证(服务器 152.53.242.23 运行正常)

此 PR 由 AI 辅助生成代码。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added invite top-up rebates with configurable rate, user/admin reporting (logs, invitees, leaderboard), and pending rebate quota transfers.
    • Added an on-demand backfill tool for missing rebate records.
    • Introduced configurable, language-aware SEO settings (title/description/keywords/canonical/OG), plus homepage crawler support via robots.txt and sitemap.xml.
  • Bug Fixes
    • Improved affiliate/inviter code validation and ensured disabled accounts can’t receive or transfer referral benefits.
    • Hardened top-up completion and quota update behavior.
  • Documentation
    • Added/updated implementation plans and completion notes for rebates and homepage SEO.

hei and others added 30 commits July 12, 2026 17:35
Document merge-friendly design for invitee top-up rebates into aff_quota
with ledger, configurable ratio, and user/admin pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step-by-step merge-friendly plan covering ledger, hooks, APIs, and
default/classic UIs with TDD-oriented tasks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Options default off; unique topup_id ledger; unit tests for formula,
disabled path, no inviter, and idempotent grant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hook Stripe/Creem/Waffo/Pancake/manual complete/epay success paths;
add user and admin invite_rebate endpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Default and classic themes: stats pages, transfer reuse, settings
options for enable flag and basis-point ratio.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skip grant when inviter missing; admin route guard; classic sidebar
module mapping; coerce enable switch; transfer loading state; grant log
only on first success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI already sends aff_code in body; API clients and invite links using
only ?aff= now also resolve inviter_id on password register.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add zh/zh-TW translations for rebate pages, admin filters, settings,
and transfer dialog labels used by the feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Overflow-safe rebate math with hard cap
- Skip grants for disabled inviters/invitees and user_id mismatch
- Treat gorm.ErrDuplicatedKey as idempotent
- Reject non-positive aff_transfer amounts
- Ignore disabled inviters at register; bound aff code length
- Mask invitee username/display on inviter invitee list

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add scheduled system task to re-grant missing rebates for successful
top-ups without ledger rows. Reject aff_transfer when account is disabled
at both API and model layers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recover from RecordLog/cache panics so rebate crediting cannot be
rolled back or aborted by logging/redis issues.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add root-only POST /api/system-task/invite-rebate-backfill, option
InviteTopupRebateBackfillMinutes (1-1440), admin UI trigger button, and
settings fields on default/classic themes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Redesign user/admin pages with SectionPageLayout, stats tiles, tabs,
invite link copy, and a masked public leaderboard (by rebate/invitees).
Add GET /api/user/invite_rebate/leaderboard and classic leaderboard table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Load summary/logs/invitees independently so a missing leaderboard route
does not break the whole page; silence global error toast for that call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avoid wiping board state when leaderboard request fails; show error/empty
state with retry. Keep tab panels mounted for smoother switching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only expose raw user_id for the viewer's own row; others remain 0 with
masked names to reduce cross-user enumeration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge-friendly SPA SEO plan: configurable meta/OG, robots/sitemap,
JSON-LD, minimal upstream hooks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task breakdown for merge-friendly SPA SEO: options, robots/sitemap,
default lib/seo, settings UI, classic light parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add configurable SEO.* options, expose them via /api/status, and serve
crawler-friendly /robots.txt and /sitemap.xml with merge-friendly files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement merge-friendly SPA SEO: SEO.* options, /robots.txt and
/sitemap.xml, default lib/seo DOM helpers, homepage JSON-LD, and light
classic parity with settings fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix broken import insertion that prevented the SPA bundle from loading,
and fetch /api/status on the homepage so SEO meta and JSON-LD apply reliably.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow SEO.Title (full) or SEO.TitleSuffix appended to SystemName so
homepage titles can include long-tail keywords instead of short brand-only names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge origin/main into local main while keeping invite top-up rebate and homepage SEO.

- Keep backend rebate/SEO modules and thin hooks
- Drop removed classic theme paths (upstream web consolidated to web/src)
- Port invite-rebate feature, SEO lib, and routes from web/default to web/src
- Preserve robots.txt/sitemap.xml registration with WebAssets router API

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove leftover web/default invite-rebate/SEO paths (live under web/src)
- Reapply pending invite rebate and SEO hardening on new layout
- Keep OAuth aff binding disabled-inviter guard with upstream affiliateCode API
- Sync route-level SEO apply in __root with new auth-session bootstrap

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CI workflows

Brings in 21 upstream commits including:
- relaykit module extraction and import path refactoring
- trusted_proxies renamed to middleware package
- per-channel HTTP transport controls
- OIDC custom login display name
- Qwen thinking_budget passthrough fix
- CI workflow for build/typecheck/test

No conflicts with custom features (invite rebate, SEO, etc.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tus cache refresh

- Restructure SEO admin settings into dedicated section with proper grid layout
- Add RobotsIndex toggle switch to admin UI
- Enhance sitemap.xml with lastmod/changefreq/priority metadata
- Add /login and /register to sitemap, more Disallow paths to robots.txt
- Fix og:site_name to always use brand name instead of full long-tail title
- Remove redundant getStatus() call in useHomeSeo (use localStorage cache)
- Add SEO keys to STATUS_RELATED_KEYS for immediate cache refresh on save
- Add invite_rebate_backfill to system task type labels
- Add missing i18n translations for SEO settings across all locales

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mapping

- Add OptionMapRWMutex.RLock/RUnlock around reads of SEOSiteURL,
  SEORobotsIndex, and ServerAddress in seoSiteBase() and RobotsTxt()
- XML-escape the site URL in sitemap <loc> elements using html.EscapeString
- Add proper native translations for SEO keys in fr/ja/ru/vi locales
  (previously untranslated English placeholders)
- Add missing description keys (Used for meta, Canonical base, etc.)
  to fr/ja/ru/vi locales
- Extract nested ternary for language detection into resolveDocumentLang()
  helper in web/src/lib/seo/apply.ts
- Use useRouterState pathname selector in __root.tsx (already done)
- Add .url().optional().or(z.literal('')) validation to SEO.SiteURL and
  SEO.OGImage fields in the zod schema
- Extract readCachedStatus() helper to web/src/lib/seo/cached-status.ts
  and use it in use-home-seo.ts and __root.tsx

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

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds invite top-up rebate accounting with ledger-backed grants, reporting APIs, scheduled backfill, and frontend pages/settings. It also adds configurable homepage SEO metadata, crawl endpoints, browser-side metadata updates, structured data, and DaoXE branding defaults.

Changes

Invite top-up rebate

Layer / File(s) Summary
Rebate domain and persistence
common/constants.go, model/invite_rebate.go, model/option.go, model/main.go, model/*_test.go
Adds rebate configuration, an idempotent ledger, quota calculation, reporting, leaderboard queries, historical backfill, migrations, and coverage for eligibility and retry behavior.
Payment and inviter integration
model/topup.go, controller/topup.go, controller/user.go, controller/oauth.go, model/user.go
Calls rebate granting after successful top-ups, normalizes and validates inviter codes, and blocks quota transfers for invalid amounts or disabled accounts.
HTTP APIs and system tasks
controller/invite_rebate*.go, controller/system_task.go, router/api-router.go
Adds user/admin rebate endpoints, leaderboard access, scheduled and on-demand backfill tasks, and route wiring.
Frontend feature and settings
web/src/features/invite-rebate/*, web/src/routes/_authenticated/invite-rebate/*, web/src/features/system-settings/..., web/src/hooks/*, web/src/i18n/*
Adds user/admin rebate pages, API clients, routes, navigation, billing controls, task labels, and translations.

Homepage SEO

Layer / File(s) Summary
Backend SEO and crawl routes
common/seo.go, model/option.go, controller/seo.go, controller/misc.go, router/main.go
Adds configurable SEO values and defaults, exposes them through status, and serves robots.txt and sitemap.xml.
Browser metadata and structured data
web/src/lib/seo/*, web/src/features/home/*, web/src/main.tsx, web/src/routes/__root.tsx, web/index.html
Adds browser SEO utilities, cached-status handling, homepage JSON-LD, dynamic metadata synchronization, and DaoXE-branded shell metadata.
SEO administration and documentation
web/src/features/system-settings/general/system-info-section.tsx, web/src/features/system-settings/site/*, web/src/features/system-settings/types.ts, docs/superpowers/*seo*
Adds SEO configuration controls, defaults, validation, cache refresh behavior, and implementation records/specifications.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

A rabbit hops where rebates grow,
Ledger carrots line up in a row.
SEO stars shine bright and clear,
DaoXE titles greet the ear.
Backfill burrows, pages bloom—
“Hop” to details in the room!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main themes: SEO changes, invite-rebate fixes, and upstream sync.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

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

391-413: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make Epay completion and user credit atomic before exposing the order to backfill.

topUp.Update() persists success/completion before IncreaseUserQuota. If quota credit fails, backfill later sees an eligible successful order and can reward the inviter even though the invitee received no quota. Commit the status and quota update in one transaction, then grant the rebate post-commit.

🤖 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/topup.go` around lines 391 - 413, Wrap the topUp.Update and
IncreaseUserQuota operations in a single database transaction so the completion
status and user quota are committed or rolled back together. Ensure the order is
not eligible for backfill until both succeed, then invoke GrantInviteTopupRebate
only after the transaction commits; preserve the existing error logging and
response behavior for failures.
🟡 Minor comments (15)
docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md-12-13 (1)

12-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the frontend paths in the completion note.

The documented paths use web/default/... and web/classic/..., but the supplied PR file inventory places the implementation under web/src/.... Replace these stale paths so future maintainers can locate the actual code.

🤖 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 `@docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md` around lines
12 - 13, Update the UI paths in the completion note to reference the
implementation under web/src/... instead of the stale web/default/... and
web/classic/... locations, while leaving the model and controller paths
unchanged.
controller/seo.go-88-99 (1)

88-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not report sitemap generation time as each page’s modification time.

This marks every URL as changed every day, even when its content did not change. Omit lastmod until a real per-page update time is available.

🤖 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/seo.go` around lines 88 - 99, Remove the shared lastmod generation
and omit the lastmod XML element from the sitemap entries built in the SEO
handler. Keep the remaining URL fields unchanged, and only reintroduce lastmod
when a real per-page modification timestamp is available.
web/src/features/system-settings/general/system-info-section.tsx-115-121 (1)

115-121: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore URL validation in the active resolver.

systemInfoSchemaWithI18n is the schema passed to zodResolver, but SEO.SiteURL and SEO.OGImage are only z.string().optional() here while _systemInfoSchema requires z.string().url().optional().or(z.literal('')). Use the same URL-or-empty constraints; otherwise invalid strings can be submitted and stored.

🤖 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/src/features/system-settings/general/system-info-section.tsx` around
lines 115 - 121, Update SEO.SiteURL and SEO.OGImage in systemInfoSchemaWithI18n
to use the same optional URL-or-empty validation as _systemInfoSchema,
preserving acceptance of empty strings while rejecting invalid URLs. Ensure the
active zodResolver schema enforces these constraints.
docs/superpowers/notes/2026-07-12-seo-homepage-done.md-20-24 (1)

20-24: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a blank line before the table.

Markdownlint reports MD058 because the table directly follows its heading.

🤖 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 `@docs/superpowers/notes/2026-07-12-seo-homepage-done.md` around lines 20 - 24,
Insert a blank line between the preceding heading and the SEO configuration
table in the documentation, leaving the table contents unchanged.

Source: Linters/SAST tools

docs/superpowers/plans/2026-07-12-seo-homepage.md-7-8 (1)

7-8: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align SEO documentation with the active frontend layout. Both documents refer to web/default/src/..., while the reviewed implementation is under web/src/....

  • docs/superpowers/plans/2026-07-12-seo-homepage.md#L7-L8: replace the obsolete web/default/src paths throughout the plan.
  • docs/superpowers/notes/2026-07-12-seo-homepage-done.md#L14-L17: record the delivered web/src/lib/seo/* path.
🤖 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 `@docs/superpowers/plans/2026-07-12-seo-homepage.md` around lines 7 - 8, Update
docs/superpowers/plans/2026-07-12-seo-homepage.md throughout to replace obsolete
web/default/src paths with web/src paths, including the SEO helper location.
Update docs/superpowers/notes/2026-07-12-seo-homepage-done.md at lines 14-17 to
record the delivered web/src/lib/seo/* path.
docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md-41-42 (1)

41-42: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize the rebate design and implementation plan with the final implementation.

  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L41-L42: remove backfill from non-goals.
  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L96-L97: document granted and skipped statuses.
  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L116-L119: document permanent skip rows.
  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L207-L207: update tiny-top-up behavior to record a skip.
  • docs/superpowers/plans/2026-07-12-invite-topup-rebate.md#L148-L153: change the expected 1% result from 50 to 5000.
  • docs/superpowers/plans/2026-07-12-invite-topup-rebate.md#L502-L503: document post-commit best-effort granting.
  • docs/superpowers/plans/2026-07-12-invite-topup-rebate.md#L1021-L1021: remove the claim that transactional paths grant inside the transaction.
🤖 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 `@docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md` around lines
41 - 42, Synchronize the documentation with the final implementation: in
docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md at lines 41-42
remove backfill from the non-goals, at lines 96-97 document granted and skipped
statuses, at lines 116-119 document permanent skip rows, and at line 207 state
that tiny top-ups record a skip; in
docs/superpowers/plans/2026-07-12-invite-topup-rebate.md at lines 148-153 change
the expected 1% result from 50 to 5000, at lines 502-503 document post-commit
best-effort granting, and at line 1021 remove the claim that transactional paths
grant inside the transaction.
model/invite_rebate.go-412-416 (1)

412-416: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate reporting query failures instead of returning fabricated zero values.

These ignored errors make invitee totals appear as zero and can report the viewer as rank 1 when the database query fails. Return the error so controllers can produce an accurate failure response.

Also applies to: 687-723

🤖 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/invite_rebate.go` around lines 412 - 416, Propagate database errors
from the reporting queries that scan invitee rebate totals, including the
InviteRebate aggregate query and the related queries in the additional
referenced section. Replace ignored Scan(...).Error results with returned errors
through the enclosing reporting methods so controllers receive failures instead
of fabricated zero totals or incorrect rank data.
web/src/i18n/locales/fr.json-5231-5283 (1)

5231-5283: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Invite-rebate strings left untranslated in fr.json.

Most of the new invite-rebate keys in this range are copied verbatim from English (e.g. "Your invite program", "Invitee", "Rebate", "Pending rewards", "Copy invite link", "Top inviters", "Failed to load leaderboard", and ~30 more), while the SEO keys added in the same hunk (lines 5285-5294) were properly translated. French-locale users will see the entire new Invite Rebate UI in English.

🤖 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/src/i18n/locales/fr.json` around lines 5231 - 5283, Translate all newly
added invite-rebate values in the French locale, including labels, messages,
actions, statuses, leaderboard text, and admin settings, while preserving the
existing interpolation placeholders such as {{rate}}. Keep the already
translated SEO entries unchanged and ensure no invite-rebate value in this
section remains identical to its English source unless it is intentionally
language-neutral.
web/src/i18n/locales/vi.json-5231-5246 (1)

5231-5246: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the new invite-rebate strings into Vietnamese.

These newly added user-facing values remain in English, so Vietnamese users will see untranslated rebate, leaderboard, and backfill UI text. Preserve all interpolation placeholders while providing Vietnamese translations.

Also applies to: 5248-5259, 5261-5283

🤖 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/src/i18n/locales/vi.json` around lines 5231 - 5246, Translate all newly
added invite-rebate, leaderboard, and backfill values in vi.json into natural
Vietnamese, including the ranges referenced by the comment. Preserve every
interpolation placeholder exactly, such as {{rate}}, while leaving the
translation keys unchanged.
web/src/i18n/locales/vi.json-5294-5294 (1)

5294-5294: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the duplicate Legal key.

Legal is already defined at Line 2478. Duplicate JSON keys have parser-dependent overwrite behavior and should not be added here.

Proposed fix
-    "Legal": "Pháp lý"
🤖 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/src/i18n/locales/vi.json` at line 5294, Remove the duplicate "Legal"
entry near the later locale section, keeping the existing "Legal" definition
already present earlier in vi.json unchanged.
web/src/i18n/locales/zh-TW.json-5309-5309 (1)

5309-5309: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid redefining the Legal key.

Legal is already defined at Line 2478. This duplicate relies on last-write-wins parser behavior and changes every existing Legal translation from 法律 to 法律條款. Remove this entry, update the original intentionally, or use a distinct key for the SEO label.

🤖 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/src/i18n/locales/zh-TW.json` at line 5309, Remove the duplicate “Legal”
entry from the locale object, or rename it to a distinct SEO-specific key and
update its callers; if the intended translation is global, modify the existing
“Legal” definition near the earlier declaration instead. Ensure the locale
contains only one “Legal” key and preserves the intended translations.
web/src/i18n/locales/zh.json-5310-5310 (1)

5310-5310: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the duplicate Legal key.

"Legal" already exists at Line 2478. Keeping both keys makes the parsed locale depend on duplicate-key behavior and causes all existing Legal usages to resolve to "法律条款" instead of "法律". Reuse the existing key or introduce a distinct source key for legal terms.

🤖 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/src/i18n/locales/zh.json` at line 5310, Remove the later duplicate
"Legal" entry from the Chinese locale and retain the existing "Legal" key so
lookups continue resolving to its original translation.

Source: Learnings

web/src/features/system-settings/general/quota-settings-section.tsx-59-60 (1)

59-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject fractional basis points and backfill intervals.

Both settings represent integers, but the schemas and inputs accept decimals. Add .int() and step={1} so invalid values cannot reach the option API.

Proposed fix
-  InviteTopupRebateRatioBp: z.coerce.number().min(0).max(10000),
-  InviteTopupRebateBackfillMinutes: z.coerce.number().min(1).max(1440),
+  InviteTopupRebateRatioBp: z.coerce.number().int().min(0).max(10000),
+  InviteTopupRebateBackfillMinutes: z.coerce.number().int().min(1).max(1440),

Add to both number inputs:

                       type='number'
+                      step={1}

Also applies to: 289-335

🤖 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/src/features/system-settings/general/quota-settings-section.tsx` around
lines 59 - 60, Update the Zod schemas for InviteTopupRebateRatioBp and
InviteTopupRebateBackfillMinutes to require integers with .int(), and set
step={1} on both corresponding number inputs in the quota settings section so
fractional values are rejected before reaching the option API.
web/src/features/wallet/components/affiliate-rewards-card.tsx-77-84 (1)

77-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the new details link visible.

The link follows a long sentence inside line-clamp-1, so it will commonly be truncated and unclickable. Remove the clamp or render the link separately.

-            <p className='text-muted-foreground line-clamp-1 text-xs'>
+            <p className='text-muted-foreground text-xs'>
🤖 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/src/features/wallet/components/affiliate-rewards-card.tsx` around lines
77 - 84, Update the paragraph rendering in the affiliate rewards card around the
details Link so the “View top-up rebate details” link remains visible and
clickable; remove the line-clamp-1 class or render the link outside the clamped
sentence while preserving the existing text and styling.
web/src/features/invite-rebate/index.tsx-758-758 (1)

758-758: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the admin table’s ID heading.

-                      <TableHead>ID</TableHead>
+                      <TableHead>{t('ID')}</TableHead>

As per coding guidelines, “All user-facing React UI text must support i18n.” <coding_guidelines>

🤖 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/src/features/invite-rebate/index.tsx` at line 758, Update the admin table
header near the TableHead containing “ID” to use the existing i18n translation
mechanism instead of a hardcoded string, adding the required translation key if
one does not already exist while preserving the displayed meaning.

Source: Coding guidelines

🧹 Nitpick comments (4)
web/src/features/home/hooks/use-home-seo.ts (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the hook’s explicit return type.

Use export function useHomeSeo(): void to meet the frontend typing rule.

🤖 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/src/features/home/hooks/use-home-seo.ts` at line 12, Add the explicit
void return type to the exported useHomeSeo function declaration, changing its
signature to return void while preserving the hook’s existing implementation.

Source: Coding guidelines

web/src/lib/seo/dom.ts (1)

1-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare the exported helper return types.

All changed helpers have typed parameters but omit their void return type. Add : void consistently.

🤖 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/src/lib/seo/dom.ts` around lines 1 - 55, Declare an explicit void return
type on each exported helper: upsertMetaByName, upsertMetaByProperty,
upsertLinkRel, upsertJsonLd, and removeJsonLd, without changing their existing
behavior.

Source: Coding guidelines

web/src/routes/__root.tsx (1)

49-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an explicit component return type.

RootComponent is changed TSX code but has no return annotation. Add a type-only ReactElement import and annotate the function, then verify with the project’s Bun lint/type-check commands.

🤖 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/src/routes/__root.tsx` around lines 49 - 55, Add a type-only ReactElement
import and annotate RootComponent with ReactElement as its explicit return type.
Preserve the existing component implementation, then run the project’s Bun lint
and type-check commands to validate the change.

Source: Coding guidelines

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

5230-5294: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Invite-rebate strings left untranslated in ja.json and ru.json, while SEO strings in the same block were translated. Both locale files show the same gap: the SEO configuration strings added in this PR (SEO Description, SEO Title Suffix, SEO Keywords, SEO Settings, SEO Site URL, SEO OG Image URL, etc.) are properly localized, but most invite-rebate strings (Your invite program, Rebate sum, Invite Top-up Rebate, Pending rewards, Top inviters, Backfill queued, Copy invite link, etc.) keep their English text as the translated value.

  • web/src/i18n/locales/ja.json#L5230-L5294: translate the invite-rebate strings into Japanese to match the SEO strings' translation quality in this same block.
  • web/src/i18n/locales/ru.json#L5230-L5294: translate the invite-rebate strings into Russian to match the SEO strings' translation quality in this same block.
🤖 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/src/i18n/locales/ja.json` around lines 5230 - 5294, Translate all
invite-rebate entries in web/src/i18n/locales/ja.json lines 5230-5294 into
natural Japanese, replacing English values while preserving keys and
interpolation placeholders. Apply the same change to
web/src/i18n/locales/ru.json lines 5230-5294 with natural Russian translations;
leave the already localized SEO strings 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 `@model/invite_rebate.go`:
- Around line 288-340: Update the rebate transaction around the quota updates
and ledger handling to use the checked quota helpers, propagate their clamp
results, and calculate the amount actually credited after saturation. Persist
that credited amount in InviteRebate rather than the full rebate, including zero
when fully capped, and set granted/log only according to the actual credited
result so RecordLog reports the credited amount and clamp outcome accurately.
- Line 105: Update both skip and grant insert paths in the invite rebate flow to
use GORM’s clause.OnConflict with the topup_id column and DoNothing enabled,
instead of detecting duplicate-key errors after Create. Use
Create(...).RowsAffected to distinguish whether each row was inserted or
skipped, and remove the post-insert unique-violation handling.
- Around line 309-312: Update the zero RowsAffected branch in the invite-rebate
transaction to check the error returned by inner.Delete for the newly inserted
InviteRebate row and return that failure instead of returning nil. Preserve the
existing successful cleanup behavior while ensuring any cleanup failure causes
the transaction to roll back rather than committing an uncredited granted
ledger.

In `@model/option.go`:
- Around line 333-349: The InviteTopupRebateEnabled transition must refresh and
persist the cutoff on every off→on enablement. In the InviteTopupRebateEnabled
case, generate a new timestamp and save it successfully before publishing the
enabled state, update the in-memory value only after persistence succeeds, and
return any DB.Save failure instead of ignoring it.

In `@web/src/features/invite-rebate/index.tsx`:
- Around line 645-674: Separate the invitee and inviter text input state from
the applied filter values used by reload. Keep reload and its useEffect
dependent only on applied filters, and have the Filter button copy the current
draft inputs into those applied values before triggering the request, preventing
requests on every keystroke and stale partial-ID results.
- Around line 125-138: Update the allSettled response handling in the
invite-rebate loading flow to explicitly handle fulfilled business-error
responses where success is false. Ensure failures for summary, logs, invitees,
and affiliate code set appropriate error state or user feedback instead of
silently leaving zero or disabled UI values, while preserving successful
response handling and the existing leaderboard error behavior.

In `@web/src/lib/seo/apply.ts`:
- Around line 154-155: Update the SEO options in applyDocumentSeo so the path
field always preserves the actual path, including for non-marketing pages,
instead of defaulting to '/'. Leave robotsIndex as the sole control for indexing
behavior and preserve the existing publicPath handling.

---

Outside diff comments:
In `@controller/topup.go`:
- Around line 391-413: Wrap the topUp.Update and IncreaseUserQuota operations in
a single database transaction so the completion status and user quota are
committed or rolled back together. Ensure the order is not eligible for backfill
until both succeed, then invoke GrantInviteTopupRebate only after the
transaction commits; preserve the existing error logging and response behavior
for failures.

---

Minor comments:
In `@controller/seo.go`:
- Around line 88-99: Remove the shared lastmod generation and omit the lastmod
XML element from the sitemap entries built in the SEO handler. Keep the
remaining URL fields unchanged, and only reintroduce lastmod when a real
per-page modification timestamp is available.

In `@docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md`:
- Around line 12-13: Update the UI paths in the completion note to reference the
implementation under web/src/... instead of the stale web/default/... and
web/classic/... locations, while leaving the model and controller paths
unchanged.

In `@docs/superpowers/notes/2026-07-12-seo-homepage-done.md`:
- Around line 20-24: Insert a blank line between the preceding heading and the
SEO configuration table in the documentation, leaving the table contents
unchanged.

In `@docs/superpowers/plans/2026-07-12-seo-homepage.md`:
- Around line 7-8: Update docs/superpowers/plans/2026-07-12-seo-homepage.md
throughout to replace obsolete web/default/src paths with web/src paths,
including the SEO helper location. Update
docs/superpowers/notes/2026-07-12-seo-homepage-done.md at lines 14-17 to record
the delivered web/src/lib/seo/* path.

In `@docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md`:
- Around line 41-42: Synchronize the documentation with the final
implementation: in
docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md at lines 41-42
remove backfill from the non-goals, at lines 96-97 document granted and skipped
statuses, at lines 116-119 document permanent skip rows, and at line 207 state
that tiny top-ups record a skip; in
docs/superpowers/plans/2026-07-12-invite-topup-rebate.md at lines 148-153 change
the expected 1% result from 50 to 5000, at lines 502-503 document post-commit
best-effort granting, and at line 1021 remove the claim that transactional paths
grant inside the transaction.

In `@model/invite_rebate.go`:
- Around line 412-416: Propagate database errors from the reporting queries that
scan invitee rebate totals, including the InviteRebate aggregate query and the
related queries in the additional referenced section. Replace ignored
Scan(...).Error results with returned errors through the enclosing reporting
methods so controllers receive failures instead of fabricated zero totals or
incorrect rank data.

In `@web/src/features/invite-rebate/index.tsx`:
- Line 758: Update the admin table header near the TableHead containing “ID” to
use the existing i18n translation mechanism instead of a hardcoded string,
adding the required translation key if one does not already exist while
preserving the displayed meaning.

In `@web/src/features/system-settings/general/quota-settings-section.tsx`:
- Around line 59-60: Update the Zod schemas for InviteTopupRebateRatioBp and
InviteTopupRebateBackfillMinutes to require integers with .int(), and set
step={1} on both corresponding number inputs in the quota settings section so
fractional values are rejected before reaching the option API.

In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 115-121: Update SEO.SiteURL and SEO.OGImage in
systemInfoSchemaWithI18n to use the same optional URL-or-empty validation as
_systemInfoSchema, preserving acceptance of empty strings while rejecting
invalid URLs. Ensure the active zodResolver schema enforces these constraints.

In `@web/src/features/wallet/components/affiliate-rewards-card.tsx`:
- Around line 77-84: Update the paragraph rendering in the affiliate rewards
card around the details Link so the “View top-up rebate details” link remains
visible and clickable; remove the line-clamp-1 class or render the link outside
the clamped sentence while preserving the existing text and styling.

In `@web/src/i18n/locales/fr.json`:
- Around line 5231-5283: Translate all newly added invite-rebate values in the
French locale, including labels, messages, actions, statuses, leaderboard text,
and admin settings, while preserving the existing interpolation placeholders
such as {{rate}}. Keep the already translated SEO entries unchanged and ensure
no invite-rebate value in this section remains identical to its English source
unless it is intentionally language-neutral.

In `@web/src/i18n/locales/vi.json`:
- Around line 5231-5246: Translate all newly added invite-rebate, leaderboard,
and backfill values in vi.json into natural Vietnamese, including the ranges
referenced by the comment. Preserve every interpolation placeholder exactly,
such as {{rate}}, while leaving the translation keys unchanged.
- Line 5294: Remove the duplicate "Legal" entry near the later locale section,
keeping the existing "Legal" definition already present earlier in vi.json
unchanged.

In `@web/src/i18n/locales/zh-TW.json`:
- Line 5309: Remove the duplicate “Legal” entry from the locale object, or
rename it to a distinct SEO-specific key and update its callers; if the intended
translation is global, modify the existing “Legal” definition near the earlier
declaration instead. Ensure the locale contains only one “Legal” key and
preserves the intended translations.

In `@web/src/i18n/locales/zh.json`:
- Line 5310: Remove the later duplicate "Legal" entry from the Chinese locale
and retain the existing "Legal" key so lookups continue resolving to its
original translation.

---

Nitpick comments:
In `@web/src/features/home/hooks/use-home-seo.ts`:
- Line 12: Add the explicit void return type to the exported useHomeSeo function
declaration, changing its signature to return void while preserving the hook’s
existing implementation.

In `@web/src/i18n/locales/ja.json`:
- Around line 5230-5294: Translate all invite-rebate entries in
web/src/i18n/locales/ja.json lines 5230-5294 into natural Japanese, replacing
English values while preserving keys and interpolation placeholders. Apply the
same change to web/src/i18n/locales/ru.json lines 5230-5294 with natural Russian
translations; leave the already localized SEO strings unchanged.

In `@web/src/lib/seo/dom.ts`:
- Around line 1-55: Declare an explicit void return type on each exported
helper: upsertMetaByName, upsertMetaByProperty, upsertLinkRel, upsertJsonLd, and
removeJsonLd, without changing their existing behavior.

In `@web/src/routes/__root.tsx`:
- Around line 49-55: Add a type-only ReactElement import and annotate
RootComponent with ReactElement as its explicit return type. Preserve the
existing component implementation, then run the project’s Bun lint and
type-check commands to validate 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 Plus

Run ID: b501ac9a-bdc3-46f9-ba39-fd35eb2f7fe3

📥 Commits

Reviewing files that changed from the base of the PR and between 66ee6b8 and 840eff4.

📒 Files selected for processing (64)
  • common/constants.go
  • common/seo.go
  • controller/invite_rebate.go
  • controller/invite_rebate_task.go
  • controller/misc.go
  • controller/oauth.go
  • controller/seo.go
  • controller/system_task.go
  • controller/system_task_handlers.go
  • controller/topup.go
  • controller/user.go
  • docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md
  • docs/superpowers/notes/2026-07-12-seo-homepage-done.md
  • docs/superpowers/plans/2026-07-12-invite-topup-rebate.md
  • docs/superpowers/plans/2026-07-12-seo-homepage.md
  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md
  • docs/superpowers/specs/2026-07-12-seo-homepage-design.md
  • model/invite_rebate.go
  • model/invite_rebate_test.go
  • model/main.go
  • model/option.go
  • model/system_task.go
  • model/task_cas_test.go
  • model/topup.go
  • model/user.go
  • router/api-router.go
  • router/main.go
  • web/index.html
  • web/src/features/home/hooks/use-home-seo.ts
  • web/src/features/home/index.tsx
  • web/src/features/invite-rebate/api.ts
  • web/src/features/invite-rebate/index.tsx
  • web/src/features/invite-rebate/types.ts
  • web/src/features/system-info/components/system-tasks-panel.tsx
  • web/src/features/system-settings/billing/index.tsx
  • web/src/features/system-settings/billing/section-registry.tsx
  • web/src/features/system-settings/general/quota-settings-section.tsx
  • web/src/features/system-settings/general/system-info-section.tsx
  • web/src/features/system-settings/hooks/use-update-option.ts
  • web/src/features/system-settings/site/index.tsx
  • web/src/features/system-settings/site/section-registry.tsx
  • web/src/features/system-settings/types.ts
  • web/src/features/wallet/components/affiliate-rewards-card.tsx
  • web/src/hooks/use-sidebar-config.ts
  • web/src/hooks/use-sidebar-data.ts
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json
  • web/src/i18n/static-keys.ts
  • web/src/lib/seo/apply.ts
  • web/src/lib/seo/cached-status.ts
  • web/src/lib/seo/defaults.ts
  • web/src/lib/seo/dom.ts
  • web/src/lib/seo/index.ts
  • web/src/lib/seo/types.ts
  • web/src/main.tsx
  • web/src/routeTree.gen.ts
  • web/src/routes/__root.tsx
  • web/src/routes/_authenticated/invite-rebate/admin.tsx
  • web/src/routes/_authenticated/invite-rebate/index.tsx

Comment thread model/invite_rebate.go
Status: InviteRebateStatusSkipped,
CreatedAt: common.GetTimestamp(),
}
if err := db.Create(row).Error; err != nil && !isDuplicateKeyError(err) {

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

rg -n -C3 'TranslateError|ErrDuplicatedKey|OnConflict|isDuplicateKeyError' \
  --glob '*.go' .
rg -n 'gorm.io/gorm' "$(fd -a '^go\.mod$' . | head -n1)"

Repository: QuantumNous/new-api

Length of output: 5241


🏁 Script executed:

#!/bin/bash
set -eu

echo "== invite_rebate.go relevant sections =="
sed -n '1,125p' model/invite_rebate.go | cat -n
sed -n '145,175p' model/invite_rebate.go | cat -n
sed -n '250,295p' model/invite_rebate.go | cat -n

echo "== invite_rebate.go schema/index definitions =="
sed -n '1,80p' model/invite_rebate.go | cat -n

echo "== imports =="
sed -n '1,40p' model/invite_rebate.go | cat -n

Repository: QuantumNous/new-api

Length of output: 13248


Use conflict-ignore insertion instead of handling unique violations after insert.

On PostgreSQL, a unique-constraint violation aborts the transaction at the database level, so recognizing gorm.ErrDuplicatedKey after db.Create(...) won’t make the transaction usable. Use clause.OnConflict{Columns: ... Columns: {{Name: "topup_id"}}, DoNothing: true} for both the skip and grant inserts, then rely on Create(...).RowsAffected to distinguish inserted versus skipped 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 `@model/invite_rebate.go` at line 105, Update both skip and grant insert paths
in the invite rebate flow to use GORM’s clause.OnConflict with the topup_id
column and DoNothing enabled, instead of detecting duplicate-key errors after
Create. Use Create(...).RowsAffected to distinguish whether each row was
inserted or skipped, and remove the post-insert unique-violation handling.

Source: Coding guidelines

Comment thread model/invite_rebate.go
Comment on lines +288 to +340
// Only credit enabled inviter (re-check status under same tx when possible).
// Saturate to MaxQuota without dialect-specific LEAST (SQLite has no LEAST by default).
capHeadroom := common.MaxQuota - rebate
if capHeadroom < 0 {
capHeadroom = 0
}
res := inner.Model(&User{}).
Where("id = ? AND status = ?", invitee.InviterId, common.UserStatusEnabled).
Updates(map[string]interface{}{
"aff_quota": gorm.Expr(
"CASE WHEN aff_quota > ? THEN ? ELSE aff_quota + ? END",
capHeadroom, common.MaxQuota, rebate,
),
"aff_history": gorm.Expr(
"CASE WHEN aff_history > ? THEN ? ELSE aff_history + ? END",
capHeadroom, common.MaxQuota, rebate,
),
})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
// Inviter became disabled between read and update: remove ledger so retry is possible.
_ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error
return nil
}
granted = true
return nil
}

var err error
if tx != nil {
err = run(tx)
} else {
err = DB.Transaction(run)
}
if err != nil {
return err
}

if granted {
// Log only on first successful grant (webhook retries must not spam).
// RecordLog may touch cache/redis; never let logging fail the grant path.
func() {
defer func() {
if rec := recover(); rec != nil {
common.SysError(fmt.Sprintf("invite rebate RecordLog panic: %v", rec))
}
}()
RecordLog(invitee.InviterId, LogTypeSystem, fmt.Sprintf(
"邀请充值返佣 %s(被邀请用户 #%d,订单 %s,基数 %s,比例 %d bp)",
logger.LogQuota(rebate), inviteeId, topUp.TradeNo, logger.LogQuota(topupQuota), ratioBp,
))

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Record and log the amount actually credited after saturation.

The ledger stores the full rebate, even when aff_quota or aff_history is capped and receives less—or nothing. That permanently marks the top-up granted while overstating accounting totals. Use the checked quota helpers, propagate clamp details, and persist the actual credited amount before logging.

As per coding guidelines, billing paths must use checked quota helpers, propagate clamp information, and make saturation auditable before logging. <coding_guidelines>

🤖 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/invite_rebate.go` around lines 288 - 340, Update the rebate transaction
around the quota updates and ledger handling to use the checked quota helpers,
propagate their clamp results, and calculate the amount actually credited after
saturation. Persist that credited amount in InviteRebate rather than the full
rebate, including zero when fully capped, and set granted/log only according to
the actual credited result so RecordLog reports the credited amount and clamp
outcome accurately.

Source: Coding guidelines

Comment thread model/invite_rebate.go
Comment on lines +309 to +312
if res.RowsAffected == 0 {
// Inviter became disabled between read and update: remove ledger so retry is possible.
_ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error
return 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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Return a failed ledger cleanup instead of committing it.

If deleting the newly inserted row fails after the inviter update affects zero rows, returning nil commits a granted ledger without crediting the inviter and prevents future retries.

Proposed fix
 		if res.RowsAffected == 0 {
 			// Inviter became disabled between read and update: remove ledger so retry is possible.
-			_ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error
+			if err := inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error; err != nil {
+				return err
+			}
 			return 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
if res.RowsAffected == 0 {
// Inviter became disabled between read and update: remove ledger so retry is possible.
_ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error
return nil
if res.RowsAffected == 0 {
// Inviter became disabled between read and update: remove ledger so retry is possible.
if err := inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error; err != nil {
return err
}
return 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 `@model/invite_rebate.go` around lines 309 - 312, Update the zero RowsAffected
branch in the invite-rebate transaction to check the error returned by
inner.Delete for the newly inserted InviteRebate row and return that failure
instead of returning nil. Preserve the existing successful cleanup behavior
while ensuring any cleanup failure causes the transaction to roll back rather
than committing an uncredited granted ledger.

Comment thread model/option.go
Comment on lines +333 to +349
case "InviteTopupRebateEnabled":
was := common.InviteTopupRebateEnabled
common.InviteTopupRebateEnabled = boolValue
// First/last turn-ON stamps the cutoff: only later top-ups earn rebate.
// Turning OFF does not clear the stamp (re-enable keeps original start).
// If admin never had a stamp and enables, set now.
if boolValue && !was {
if common.InviteTopupRebateEnabledAt <= 0 {
common.InviteTopupRebateEnabledAt = common.GetTimestamp()
common.OptionMap["InviteTopupRebateEnabledAt"] = strconv.FormatInt(common.InviteTopupRebateEnabledAt, 10)
// Persist cutoff so restarts / other nodes share the same boundary.
_ = DB.Save(&Option{
Key: "InviteTopupRebateEnabledAt",
Value: common.OptionMap["InviteTopupRebateEnabledAt"],
}).Error
}
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reset and atomically persist the cutoff on every off→on transition.

Keeping the original timestamp means re-enabling later allows backfill to grant top-ups completed while the feature was disabled. Additionally, ignoring DB.Save failures can leave nodes using different eligibility boundaries. Persist a fresh timestamp successfully before publishing the enabled state, and return persistence failures.

🤖 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 333 - 349, The InviteTopupRebateEnabled
transition must refresh and persist the cutoff on every off→on enablement. In
the InviteTopupRebateEnabled case, generate a new timestamp and save it
successfully before publishing the enabled state, update the in-memory value
only after persistence succeeds, and return any DB.Save failure instead of
ignoring it.

Comment on lines +125 to +138
if (s?.success && s.data) setSummary(s.data)
else if (!s) toast.error(t('Failed to load invite rebate data'))

if (l?.success && l.data) setLogs(l.data.items || [])
if (i?.success && i.data) setInvitees(i.data.items || [])
if (b?.success && b.data) {
setBoard(b.data.items || [])
setMyRank(b.data.my_rank || 0)
setLeaderboardError(false)
} else if (settled[3].status === 'rejected' || (b && !b.success)) {
// Keep previous board data if any; only flag error for empty UI retry
setLeaderboardError(true)
}
if (a?.success && a.data) setAffCode(a.data)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle fulfilled business-error responses instead of rendering zero data.

Promise.allSettled treats { success: false } as fulfilled, but summary, logs, invitees, and affiliate-code failures are silently ignored. On initial summary failure, loading ends and the page misleadingly renders zero balances and “Rebate disabled.”

Proposed summary handling
-      if (s?.success && s.data) setSummary(s.data)
-      else if (!s) toast.error(t('Failed to load invite rebate data'))
+      if (s?.success && s.data) {
+        setSummary(s.data)
+      } else {
+        toast.error(s?.message || t('Failed to load invite rebate data'))
+      }
📝 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 (s?.success && s.data) setSummary(s.data)
else if (!s) toast.error(t('Failed to load invite rebate data'))
if (l?.success && l.data) setLogs(l.data.items || [])
if (i?.success && i.data) setInvitees(i.data.items || [])
if (b?.success && b.data) {
setBoard(b.data.items || [])
setMyRank(b.data.my_rank || 0)
setLeaderboardError(false)
} else if (settled[3].status === 'rejected' || (b && !b.success)) {
// Keep previous board data if any; only flag error for empty UI retry
setLeaderboardError(true)
}
if (a?.success && a.data) setAffCode(a.data)
if (s?.success && s.data) {
setSummary(s.data)
} else {
toast.error(s?.message || t('Failed to load invite rebate data'))
}
if (l?.success && l.data) setLogs(l.data.items || [])
if (i?.success && i.data) setInvitees(i.data.items || [])
if (b?.success && b.data) {
setBoard(b.data.items || [])
setMyRank(b.data.my_rank || 0)
setLeaderboardError(false)
} else if (settled[3].status === 'rejected' || (b && !b.success)) {
// Keep previous board data if any; only flag error for empty UI retry
setLeaderboardError(true)
}
if (a?.success && a.data) setAffCode(a.data)
🤖 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/src/features/invite-rebate/index.tsx` around lines 125 - 138, Update the
allSettled response handling in the invite-rebate loading flow to explicitly
handle fulfilled business-error responses where success is false. Ensure
failures for summary, logs, invitees, and affiliate code set appropriate error
state or user feedback instead of silently leaving zero or disabled UI values,
while preserving successful response handling and the existing leaderboard error
behavior.

Comment on lines +645 to +674
const reload = useCallback(async () => {
setLoading(true)
try {
const params: {
p: number
page_size: number
inviter_id?: number
invitee_id?: number
} = { p: 1, page_size: 50 }
if (inviterId) params.inviter_id = Number(inviterId)
if (inviteeId) params.invitee_id = Number(inviteeId)
const [s, list] = await Promise.all([
fetchAdminInviteRebateSummary(
inviterId ? Number(inviterId) : undefined
),
fetchAdminInviteRebates(params),
])
if (s.success && s.data) setSummary(s.data)
if (list.success && list.data) setRows(list.data.items || [])
} catch (e) {
console.error(e)
toast.error(t('Failed to load admin rebate data'))
} finally {
setLoading(false)
}
}, [inviteeId, inviterId, t])

useEffect(() => {
void reload()
}, [reload])

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not apply filters automatically on every keystroke.

Because reload depends on inviterId and inviteeId, the effect sends requests after every input change. Concurrent responses can arrive out of order and replace the table with results for an older partial ID. Keep draft inputs separate from applied filters, and update the latter only from the Filter button.

🤖 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/src/features/invite-rebate/index.tsx` around lines 645 - 674, Separate
the invitee and inviter text input state from the applied filter values used by
reload. Keep reload and its useEffect dependent only on applied filters, and
have the Filter button copy the current draft inputs into those applied values
before triggering the request, preventing requests on every keystroke and stale
partial-ID results.

Comment thread web/src/lib/seo/apply.ts
Comment on lines +154 to +155
robotsIndex: publicPath ? baseRobots : false,
path: publicPath ? path : '/',

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the actual route path for non-marketing pages.

Rewriting every non-marketing path to / makes applyDocumentSeo treat console/auth pages as homepages: it appends the homepage long-tail title and emits the homepage canonical/OG URL. Keep path unchanged; use robotsIndex alone to control indexing.

Proposed fix
-    path: publicPath ? path : '/',
+    path,
📝 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
robotsIndex: publicPath ? baseRobots : false,
path: publicPath ? path : '/',
robotsIndex: publicPath ? baseRobots : false,
path,
🤖 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/src/lib/seo/apply.ts` around lines 154 - 155, Update the SEO options in
applyDocumentSeo so the path field always preserves the actual path, including
for non-marketing pages, instead of defaulting to '/'. Leave robotsIndex as the
sole control for indexing behavior and preserve the existing publicPath
handling.

…a validation, docs paths

- Wrap epay topUp.Update() and quota increase in a single DB transaction
  so order is not eligible for backfill until both succeed; rebate grant
  happens only after transaction commits
- Remove meaningless lastmod (today's date) from sitemap entries
- Add z.string().url() validation to SEO.SiteURL and SEO.OGImage fields
- Update web/default/src and web/classic/src paths to web/src in docs
- Add blank line before table in SEO done note (MD058)
- Sync invite-rebate spec: remove backfill from non-goals, document
  granted/skipped statuses and permanent skip rows
- Fix expected 1% result from 50 to 5000 in rebate plan test

Co-Authored-By: Claude Opus 5 <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: 3

Caution

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

⚠️ Outside diff range comments (1)
web/src/features/system-settings/general/system-info-section.tsx (1)

97-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the default-enabled robots setting.

When the option is absent, this expression normalizes it to false, while the browser SEO layer treats a missing seo_robots_index as enabled. The form therefore displays indexing as disabled for the default configuration.

Normalize only an explicit false/'false' as disabled; treat missing values as true.

🤖 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/src/features/system-settings/general/system-info-section.tsx` around
lines 97 - 99, Update the 'SEO.RobotsIndex' normalization in the system
information form to return false only when defaultValues explicitly contains
false or 'false'; treat missing or other values as true so the default-enabled
robots setting is preserved.
🧹 Nitpick comments (1)
web/src/features/system-settings/general/system-info-section.tsx (1)

91-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the any casts from SEO fields.

These casts bypass type checking for both default access and every SEO FormField name. Define a typed field-path contract instead, so renames and schema mismatches are caught at compile time.

Also applies to: 298-298, 324-324, 350-350, 375-375, 398-398, 422-422, 446-446

🤖 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/src/features/system-settings/general/system-info-section.tsx` around
lines 91 - 96, Replace the any casts used for SEO default-value access and all
SEO FormField names in the system info section with a typed field-path contract
derived from the form schema. Update the affected SEO fields, including
SEO.Title, SEO.TitleSuffix, SEO.Description, SEO.Keywords, SEO.SiteURL, and
SEO.OGImage, so both default access and field names are compile-time validated.

Source: Coding guidelines

🤖 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/topup.go`:
- Around line 402-405: Replace the raw quota Update in the top-up transaction
with a transaction-compatible checked quota helper that enforces the quota/clamp
contract and returns clamp information. Ensure quota-cache synchronization or
invalidation occurs only after the transaction commits, and propagate the
saturation result to call attachQuotaSaturation before writing any consume or
task logs.
- Around line 398-407: The transaction around saving topUp must atomically
update the order only when its current status is pending, rather than using an
unconditional tx.Save. Check the conditional update’s RowsAffected and treat
zero rows as an already-processed callback, ensuring quota crediting and the
log/rebate side effects execute only for the transaction that successfully
transitions the order from pending to success.

In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 119-120: Update the SEO.OGImage schema entry in the system-info
validation definition to accept empty values and paths beginning with “/”,
instead of requiring a full URL. Leave the SEO.SiteURL validation unchanged and
preserve rejection of other invalid OG image values.

---

Outside diff comments:
In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 97-99: Update the 'SEO.RobotsIndex' normalization in the system
information form to return false only when defaultValues explicitly contains
false or 'false'; treat missing or other values as true so the default-enabled
robots setting is preserved.

---

Nitpick comments:
In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 91-96: Replace the any casts used for SEO default-value access and
all SEO FormField names in the system info section with a typed field-path
contract derived from the form schema. Update the affected SEO fields, including
SEO.Title, SEO.TitleSuffix, SEO.Description, SEO.Keywords, SEO.SiteURL, and
SEO.OGImage, so both default access and field names are compile-time validated.
🪄 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 Plus

Run ID: bb31e385-393e-461c-a55d-f0bbfe8c91e5

📥 Commits

Reviewing files that changed from the base of the PR and between 840eff4 and d7e496d.

📒 Files selected for processing (8)
  • controller/seo.go
  • controller/topup.go
  • docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md
  • docs/superpowers/notes/2026-07-12-seo-homepage-done.md
  • docs/superpowers/plans/2026-07-12-invite-topup-rebate.md
  • docs/superpowers/plans/2026-07-12-seo-homepage.md
  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md
  • web/src/features/system-settings/general/system-info-section.tsx
💤 Files with no reviewable changes (1)
  • controller/seo.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md
  • docs/superpowers/plans/2026-07-12-invite-topup-rebate.md
  • docs/superpowers/plans/2026-07-12-seo-homepage.md
  • docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md

Comment thread controller/topup.go
Comment on lines +398 to +407
err := model.DB.Transaction(func(tx *gorm.DB) error {
if txErr := tx.Save(topUp).Error; txErr != nil {
return txErr
}
if txErr := tx.Model(&model.User{}).Where("id = ?", topUp.UserId).
Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; txErr != nil {
return txErr
}
return 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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)controller/topup\.go$|LockOrder|rebate|topup' || true

echo
echo "== controller/topup.go outline/stat =="
wc -l controller/topup.go 2>/dev/null || true
ast-grep outline controller/topup.go 2>/dev/null || true

echo
echo "== relevant topup.go sections =="
sed -n '320,450p' controller/topup.go 2>/dev/null || true

echo
echo "== LockOrder references =="
rg -n "LockOrder|ReleaseOrder|order status|Update\\(\"status\"|TopUp|topUp|top_up|quota" controller -S || true

Repository: QuantumNous/new-api

Length of output: 28836


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== LockOrder implementation =="
sed -n '270,310p' controller/topup.go

echo
echo "== payment webhook lock handlers =="
for f in controller/topup_stripe.go controller/topup_creem.go controller/topup_waffo.go controller/topup_waffo_pancake.go controller/subscription_payment_epay.go controller/subscription_payment_creem.go controller/subscription_payment_stripe.go controller/subscription_payment_waffo_pancake.go; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n -A 35 -B 10 "LockOrder\\(|UnlockOrder\\(|Status != common\\.TopUpStatusPending|UpdatePendingTopUpStatus|quota\\s*\\+|RecordTopupLog|GrantInviteTopupRebate" "$f"
  fi
done

echo
echo "== TopUp model methods =="
cat -n model/topup.go | sed -n '1,220p'

echo
echo "== rebate idempotency references =="
rg -n -A 30 -B 10 "func GrantInviteTopupRebate|GrantInviteTopupRebate\\(|Insert|top_up_rebate|topup.*rebate|topup_quota|rebate_quota|quota|UNIQUE|unique|duplicate" model/invite_rebate.go controller/invite_rebate.go

Repository: QuantumNous/new-api

Length of output: 18163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TopUp model methods =="
cat -n model/topup.go | sed -n '1,260p'

echo
echo "== rebate idempotency references =="
rg -n -A 40 -B 10 "func GrantInviteTopupRebate|GrantInviteTopupRebate\\(|Insert|top_up_rebate|topup.*rebate|topup_quota|rebate_quota|quota|UNIQUE|unique|duplicate|NOT NULL" model/invite_rebate.go controller/invite_rebate.go model -S || true

echo
echo "== deterministic transaction probe =="
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class Store:
    topup_status: str
    user_quota: int

class TxAlreadyCommitted(Exception):
    pass

def save(store, new_status):
    if store.topup_status != new_status:
        raise TxAlreadyCommitted()

def update_add_quota(store, amount):
    store.user_quota += amount

def handle(store, pending_status, target_status, amount):
    try:
        save(store, target_status)
    except TxAlreadyCommitted:
        return False
    update_add_quota(store, amount)
    return True

# Concurrent in-memory read before both attempt transaction.
s = Store(topup_status='pending', user_quota=100)
s2 = Store(**vars(s))
a = handle(s, 'pending', 'pending', 50)
b = handle(s2, 'pending', 'pending', 50)
print("r1_processed:", True if a else False)
print("r2_processed:", True if not b else False)
print("r1_quota_after:", s.user_quota)
print("r2_quota_after:", s2.user_quota)

# In a real DB, Save(status=pending->success) leaves status success; second Save fails.
PY

Repository: QuantumNous/new-api

Length of output: 50376


Make the pending-to-success transition database-idempotent.

LockOrder only serializes requests inside one process. Concurrent or duplicate epay callbacks can both pass the initial Status == Pending check, then both commit quota credits. Update the order only when its current status is pending, check RowsAffected, and run the log/rebate side effects only for the transaction that acquired the pending row.

🤖 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/topup.go` around lines 398 - 407, The transaction around saving
topUp must atomically update the order only when its current status is pending,
rather than using an unconditional tx.Save. Check the conditional update’s
RowsAffected and treat zero rows as an already-processed callback, ensuring
quota crediting and the log/rebate side effects execute only for the transaction
that successfully transitions the order from pending to success.

Comment thread controller/topup.go
Comment on lines +402 to +405
if txErr := tx.Model(&model.User{}).Where("id = ?", topUp.UserId).
Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; txErr != nil {
return txErr
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use the checked quota mutation path instead of a raw update.

This bypasses the quota-cache synchronization performed by model.IncreaseUserQuota, so cache-backed balance checks can retain the pre-top-up quota after commit. It also bypasses the required checked quota/clamp contract. Add a transaction-compatible checked helper, then synchronize or invalidate cache only after commit.

As per coding guidelines, “Billing paths must use checked quota helpers, propagate clamp information, and call attachQuotaSaturation before writing consume or task logs so saturation is auditable.”

🤖 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/topup.go` around lines 402 - 405, Replace the raw quota Update in
the top-up transaction with a transaction-compatible checked quota helper that
enforces the quota/clamp contract and returns clamp information. Ensure
quota-cache synchronization or invalidation occurs only after the transaction
commits, and propagate the saturation result to call attachQuotaSaturation
before writing any consume or task logs.

Source: Coding guidelines

Comment on lines +119 to +120
'SEO.SiteURL': z.string().url().optional().or(z.literal('')),
'SEO.OGImage': z.string().url().optional().or(z.literal('')),

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'system-info-section\.tsx$' . || true

echo "== outline =="
file="$(fd 'system-info-section\.tsx$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  ast-grep outline "$file" || true
  echo "== relevant schema lines =="
  sed -n '100,135p' "$file"
  echo "== relevant line 428 =="
  sed -n '420,435p' "$file"
fi

echo "== search OG/SiteURL related schemas/usages/renderers =="
rg -n "SEO\.OGImage|SEO\.SiteURL|OgImage|og:image|/logo\.png|OGImage|SiteURL|relative" web/src || true

Repository: QuantumNous/new-api

Length of output: 43117


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SEO apply outline =="
ast-grep outline web/src/lib/seo/apply.ts || true
echo "== SEO apply relevant lines =="
sed -n '1,120p' web/src/lib/seo/apply.ts
echo "== SEO apply full =="
wc -l web/src/bin/seo/apply.ts || true
sed -n '120,190p' web/src/lib/seo/apply.ts

echo "== Zod package availability/version =="
if [ -f web/package.json ]; then sed -n '1,140p' web/package.json; fi
if command -v bun >/dev/null 2>&1; then bun --version || true; fi

echo "== Standalone Zod URL behavior probe =="
node - <<'JS'
async function main() {
  let z
  try {
    const pkg = await import('zod')
    z = pkg.z
  } catch (e) {
    try {
      z = require('zod')
    } catch (e2) {
      console.log('zod runtime unavailable:', e2 && e2.message)
      return
    }
  }
  const schema = z.string().url().optional().or(z.literal(''))
  const values = ['', '/logo.png', 'https://example.com/logo.png', 'data:text/plain,x']
  for (const value of values) {
    try {
      console.log(JSON.stringify({value, result: schema.parse(value)}))
    } catch (e) {
      console.log(JSON.stringify({value, error: e.message || String(e)}))
    }
  }
}
main().catch(err => { console.error(err); process.exit(1) })
JS

Repository: QuantumNous/new-api

Length of output: 10309


🌐 Web query:

Zod 4 string url validation accepts relative URL /logo.png URL string

💡 Result:

No, Zod 4 string URL validation does not accept relative URLs like /logo.png by default [1][2]. The z.url() validator in Zod 4 internally utilizes the WHATWG new URL() constructor to validate inputs [1][3]. According to the WHATWG URL standard, a relative path alone cannot be instantiated as a valid absolute URL without a base URL, which causes new URL('/logo.png') to throw an error [1][4]. Consequently, z.url() will fail validation for such strings [1][2]. If you need to support relative URLs, you must use a custom validation method, such as .refine() [4][3]: const relativeUrlSchema = z.string.refine((val) => { // Check if it is a relative path (e.g., starts with /) if (val.startsWith("/")) return true; // Otherwise validate as an absolute URL try { new URL(val); return true; } catch { return false; } }); relativeUrlSchema.parse("/logo.png"); // ✅ relativeUrlSchema.parse("https://example.com/logo.png"); // ✅

Citations:


Allow relative OG image paths in the SEO schema.

SEO.OGImage is required to be a URL, but the form placeholder and SEO renderer support relative values like /logo.png; replace the URL validator there with one that accepts /-prefixed paths or empty values.

🤖 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/src/features/system-settings/general/system-info-section.tsx` around
lines 119 - 120, Update the SEO.OGImage schema entry in the system-info
validation definition to accept empty values and paths beginning with “/”,
instead of requiring a full URL. Leave the SEO.SiteURL validation unchanged and
preserve rejection of other invalid OG image values.

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.

1 participant