fix(topup): add currency symbol to amounts in RechargeCard - #1952
Conversation
…onent pages - Replace blanket console route footer hiding with specific page targeting - Only hide footer on pages that use CardPro component: * /console/channel (channels management) * /console/log (usage logs) * /console/redemption (redemption codes) * /console/user (user management) * /console/token (token management) * /console/midjourney (midjourney logs) * /console/task (task logs) * /console/models (model management) * /pricing (pricing page) - Footer now displays on other console pages (dashboard, settings, topup, etc.) - Improves UI consistency by showing footer where CardPro's internal pagination isn't used This change ensures footer is only hidden when CardPro component provides its own pagination/footer functionality, while preserving footer visibility on other pages that benefit from the global footer navigation.
WalkthroughAdds Doubao (豆包) video channel and task adaptor, token-aware post-task billing adjustments for video tasks, new user agreement & privacy policy storage/endpoints and frontend pages/consent flows, content-type utility, header override helper and Claude header commonization, PostgreSQL in docker-compose, Go tool/dependency bumps, extensive i18n/docs updates, and multiple UI/formatting edits. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Frontend
participant API as new-api
participant Upstream as Doubao
Note over Client,API: Submit Doubao video generation
Client->>API: POST /v1/video/generations (task submit)
API->>API: ValidateRequestAndSetAction
API->>Upstream: BuildRequestBody/POST (Authorization Bearer)
Upstream-->>API: 200 response with task_id
API->>Client: 202 accepted (task_id)
Note over API,Upstream: Polling flow
loop polling
API->>Upstream: GET /tasks/{task_id} (auth header)
Upstream-->>API: task status & total_tokens
API->>API: ParseTaskResult -> if success and total_tokens>0 compute actualQuota
API->>API: Adjust user/channel quotas (charge/refund) and log
API->>Client: Update task status / final result
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/src/components/topup/RechargeCard.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/topup/RechargeCard.jsx (1)
web/src/components/topup/modals/PaymentConfirmModal.jsx (1)
hasDiscount(43-43)
| {t('实付')} ${actualPay.toFixed(2)}, | ||
| {hasDiscount ? `${t('节省')} $${save.toFixed(2)}` : `${t('节省')} $0.00`} | ||
| </div> |
There was a problem hiding this comment.
Avoid hardcoding the USD symbol here
This UI previously respected whatever currency the backend/formatters supplied; forcing a literal “$” now mislabels every non-USD top up (e.g., CNY users will see dollars). Pull the symbol from config/props (e.g., topupInfo?.currency_symbol) or reuse the existing formatter instead of hardcoding.
- {t('实付')} ${actualPay.toFixed(2)},
- {hasDiscount ? `${t('节省')} $${save.toFixed(2)}` : `${t('节省')} $0.00`}
+ {t('实付')} {(topupInfo?.currency_symbol ?? topupInfo?.currencySymbol ?? '') + actualPay.toFixed(2)},
+ {hasDiscount
+ ? `${t('节省')} ${(topupInfo?.currency_symbol ?? topupInfo?.currencySymbol ?? '')}${save.toFixed(2)}`
+ : `${t('节省')} ${(topupInfo?.currency_symbol ?? topupInfo?.currencySymbol ?? '')}0.00`}📝 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.
| {t('实付')} ${actualPay.toFixed(2)}, | |
| {hasDiscount ? `${t('节省')} $${save.toFixed(2)}` : `${t('节省')} $0.00`} | |
| </div> | |
| {t('实付')} {(topupInfo?.currency_symbol ?? topupInfo?.currencySymbol ?? '') + actualPay.toFixed(2)}, | |
| {hasDiscount | |
| ? `${t('节省')} ${(topupInfo?.currency_symbol ?? topupInfo?.currencySymbol ?? '')}${save.toFixed(2)}` | |
| : `${t('节省')} ${(topupInfo?.currency_symbol ?? topupInfo?.currencySymbol ?? '')}0.00`} | |
| </div> |
🤖 Prompt for AI Agents
In web/src/components/topup/RechargeCard.jsx around lines 371-373, the code
hardcodes the USD symbol ("$") when rendering actualPay and save; replace the
literal "$" with the currency symbol provided by data or formatter (e.g., use
topupInfo?.currency_symbol or the existing number formatter) for both actualPay
and save, and add a safe fallback (e.g., an empty string or a default symbol
from config) so non-USD users display the correct currency symbol.
alpha -> main
feat(i18n): add and update French translations
chore: go version & sonic dep
…onent pages - Replace blanket console route footer hiding with specific page targeting - Only hide footer on pages that use CardPro component: * /console/channel (channels management) * /console/log (usage logs) * /console/redemption (redemption codes) * /console/user (user management) * /console/token (token management) * /console/midjourney (midjourney logs) * /console/task (task logs) * /console/models (model management) * /pricing (pricing page) - Footer now displays on other console pages (dashboard, settings, topup, etc.) - Improves UI consistency by showing footer where CardPro's internal pagination isn't used This change ensures footer is only hidden when CardPro component provides its own pagination/footer functionality, while preserving footer visibility on other pages that benefit from the global footer navigation.
…le-footer # Conflicts: # web/src/components/table/channels/modals/EditChannelModal.jsx # web/src/hooks/common/useSidebar.js
- Removed the 'chatnio' link from the footer. - Added new links for 'CoAI' and 'GPT-Load' in the footer. - Updated the localization key for '基于New API的项目' to '友情链接' for better clarity. - Adjusted the design of the footer to improve layout and visibility of the developer credit.
✨ feat(layout): refine footer visibility logic to target CardPro component pages
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
462-493: Missing space in Bearer token template.Line 2474 has
'Bearer{api_key}'but should be'Bearer {api_key}'(note the space after "Bearer"). The standard Bearer token authentication format requires a space between "Bearer" and the token value. Without this space, authentication will fail when the variable is substituted.Apply this diff to fix the template:
{ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - 'Authorization': 'Bearer{api_key}', + 'Authorization': 'Bearer {api_key}', },web/src/components/auth/RegisterForm.jsx (1)
201-238: Terms consent must be enforced in submission handlerAlthough the button is disabled, the active
handleSubmitimplementation never checksagreedToTerms. Users can bypass the checkbox (e.g., by re-enabling the button in devtools or submitting the form programmatically) and register without accepting the agreement/privacy policy. Add a guard in this handler before firing the request so the flow remains blocked without consent.async function handleSubmit(e) { if (password.length < 8) { showInfo('密码长度不得小于 8 位!'); return; } if (password !== password2) { showInfo('两次输入的密码不一致'); return; } + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showError(t('请先阅读并同意用户协议和隐私政策')); + return; + } if (username && password) {
🧹 Nitpick comments (1)
relay/channel/api_request.go (1)
40-58: Consider documenting supported placeholders.The processHeaderOverride helper correctly validates header values and replaces the
{api_key}placeholder. However, consider documenting the supported placeholders (currently only{api_key}) in the function comment or in a central location to guide future enhancements.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
go.sumis excluded by!**/*.sumweb/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (50)
README.en.md(5 hunks)README.fr.md(5 hunks)README.ja.md(1 hunks)README.md(6 hunks)constant/channel.go(2 hunks)controller/channel-test.go(1 hunks)controller/misc.go(1 hunks)controller/task_video.go(2 hunks)docker-compose.yml(3 hunks)go.mod(5 hunks)model/option.go(1 hunks)relay/channel/api_request.go(5 hunks)relay/channel/aws/adaptor.go(1 hunks)relay/channel/claude/adaptor.go(2 hunks)relay/channel/task/doubao/adaptor.go(1 hunks)relay/channel/task/doubao/constants.go(1 hunks)relay/common/relay_info.go(1 hunks)relay/relay_adaptor.go(3 hunks)router/api-router.go(1 hunks)web/jsconfig.json(1 hunks)web/src/App.jsx(2 hunks)web/src/components/auth/RegisterForm.jsx(5 hunks)web/src/components/common/modals/TwoFactorAuthModal.jsx(1 hunks)web/src/components/layout/Footer.jsx(3 hunks)web/src/components/layout/PageLayout.jsx(1 hunks)web/src/components/settings/OtherSetting.jsx(4 hunks)web/src/components/settings/SystemSetting.jsx(9 hunks)web/src/components/table/channels/modals/EditChannelModal.jsx(7 hunks)web/src/components/table/mj-logs/MjLogsFilters.jsx(1 hunks)web/src/components/table/task-logs/TaskLogsColumnDefs.jsx(1 hunks)web/src/components/table/task-logs/TaskLogsFilters.jsx(1 hunks)web/src/components/table/usage-logs/UsageLogsFilters.jsx(1 hunks)web/src/components/topup/RechargeCard.jsx(7 hunks)web/src/components/topup/index.jsx(6 hunks)web/src/components/topup/modals/PaymentConfirmModal.jsx(1 hunks)web/src/constants/channel.constants.js(1 hunks)web/src/constants/console.constants.js(1 hunks)web/src/helpers/api.js(0 hunks)web/src/helpers/render.jsx(2 hunks)web/src/hooks/common/useSidebar.js(1 hunks)web/src/i18n/locales/en.json(4 hunks)web/src/i18n/locales/fr.json(1 hunks)web/src/i18n/locales/zh.json(1 hunks)web/src/pages/PrivacyPolicy/index.jsx(1 hunks)web/src/pages/Setting/Operation/SettingsGeneral.jsx(1 hunks)web/src/pages/Setting/Operation/SettingsMonitoring.jsx(1 hunks)web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx(5 hunks)web/src/pages/Setting/Ratio/ModelRatioSettings.jsx(4 hunks)web/src/pages/UserAgreement/index.jsx(1 hunks)web/src/utils/contentDetector.js(1 hunks)
💤 Files with no reviewable changes (1)
- web/src/helpers/api.js
✅ Files skipped from review due to trivial changes (14)
- web/src/components/common/modals/TwoFactorAuthModal.jsx
- README.ja.md
- web/src/components/table/usage-logs/UsageLogsFilters.jsx
- web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx
- web/src/components/settings/SystemSetting.jsx
- web/src/constants/console.constants.js
- web/src/components/table/task-logs/TaskLogsColumnDefs.jsx
- web/jsconfig.json
- web/src/components/topup/index.jsx
- web/src/pages/Setting/Operation/SettingsMonitoring.jsx
- web/src/components/table/mj-logs/MjLogsFilters.jsx
- web/src/components/table/task-logs/TaskLogsFilters.jsx
- web/src/pages/Setting/Operation/SettingsGeneral.jsx
- web/src/components/topup/RechargeCard.jsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the applyModelMapping function transforms the models list by replacing original model names (mapping values) with display names (mapping keys). The database stores this transformed list containing mapped keys. On channel load, data.models contains these mapped display names, making the initialization filter if (data.models.includes(key)) correct.
Applied to files:
web/src/components/table/channels/modals/EditChannelModal.jsx
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the database stores mapped keys (display names) in the models field after applying model mapping transformations. When loading a channel, data.models contains the mapped keys, not the original model names. The filtering logic if (data.models.includes(key)) in the initialization is correct.
Applied to files:
web/src/components/table/channels/modals/EditChannelModal.jsx
🧬 Code graph analysis (20)
controller/channel-test.go (1)
constant/channel.go (1)
ChannelTypeDoubaoVideo(54-54)
relay/channel/claude/adaptor.go (1)
relay/common/relay_info.go (1)
RelayInfo(74-121)
controller/misc.go (1)
common/constants.go (2)
OptionMapRWMutex(37-37)OptionMap(36-36)
router/api-router.go (1)
controller/misc.go (2)
GetUserAgreement(150-159)GetPrivacyPolicy(161-170)
web/src/components/layout/PageLayout.jsx (1)
web/src/components/layout/SiderBar.jsx (1)
location(66-66)
web/src/hooks/common/useSidebar.js (1)
web/src/components/dashboard/index.jsx (1)
handleRefresh(98-103)
model/option.go (1)
common/constants.go (1)
OptionMap(36-36)
web/src/pages/PrivacyPolicy/index.jsx (5)
web/src/App.jsx (1)
PrivacyPolicy(55-55)web/src/pages/UserAgreement/index.jsx (7)
useTranslation(32-32)contentType(35-35)htmlBody(36-36)htmlStyles(37-37)htmlLinks(38-38)HEADER_HEIGHT(40-40)renderContent(175-247)web/src/utils/contentDetector.js (2)
getContentType(50-61)getContentType(50-61)web/src/helpers/utils.jsx (1)
showError(122-151)web/src/components/common/markdown/MarkdownRenderer.jsx (1)
MarkdownRenderer(594-652)
web/src/App.jsx (3)
web/src/pages/UserAgreement/index.jsx (1)
UserAgreement(31-250)web/src/pages/PrivacyPolicy/index.jsx (1)
PrivacyPolicy(31-247)web/src/components/common/ui/Loading.jsx (1)
Loading(23-29)
web/src/components/table/channels/modals/EditChannelModal.jsx (2)
web/src/components/settings/PersonalSetting.jsx (1)
handleInputChange(168-170)web/src/components/table/channels/modals/EditTagModal.jsx (1)
handleInputChange(77-132)
web/src/components/settings/OtherSetting.jsx (1)
web/src/helpers/utils.jsx (2)
showSuccess(157-159)showError(122-151)
web/src/pages/Setting/Ratio/ModelRatioSettings.jsx (1)
web/src/components/settings/RatioSetting.jsx (1)
inputs(35-49)
relay/channel/aws/adaptor.go (1)
relay/channel/claude/adaptor.go (1)
CommonClaudeHeadersOperation(67-74)
relay/channel/task/doubao/adaptor.go (7)
relay/common/relay_info.go (3)
RelayInfo(74-121)TaskSubmitReq(483-492)TaskInfo(502-511)relay/common/relay_utils.go (2)
ValidateBasicTaskRequest(67-96)HasImage(18-20)relay/channel/api_request.go (1)
DoTaskApiRequest(300-322)service/error.go (1)
TaskErrorWrapper(139-155)service/http_client.go (1)
GetHttpClient(32-34)relay/channel/task/doubao/constants.go (2)
ModelList(3-7)ChannelName(9-9)model/task.go (4)
TaskStatusQueued(16-16)TaskStatusInProgress(17-17)TaskStatusSuccess(19-19)TaskStatusFailure(18-18)
relay/channel/api_request.go (1)
types/error.go (2)
NewError(193-213)ErrorCodeChannelHeaderOverrideInvalid(53-53)
web/src/utils/contentDetector.js (1)
web/src/components/table/users/UsersColumnDefs.jsx (1)
content(115-119)
web/src/pages/UserAgreement/index.jsx (4)
web/src/pages/PrivacyPolicy/index.jsx (4)
contentType(35-35)htmlBody(36-36)HEADER_HEIGHT(40-40)renderContent(172-244)web/src/utils/contentDetector.js (2)
getContentType(50-61)getContentType(50-61)web/src/helpers/utils.jsx (1)
showError(122-151)web/src/components/common/markdown/MarkdownRenderer.jsx (1)
MarkdownRenderer(594-652)
controller/task_video.go (6)
setting/ratio_setting/model_ratio.go (1)
GetModelRatio(430-441)model/user.go (4)
GetUserById(287-299)DecreaseUserQuota(777-792)UpdateUserUsedQuotaAndRequestCount(823-830)IncreaseUserQuota(752-767)setting/ratio_setting/group_ratio.go (2)
GetGroupRatio(64-74)GetGroupGroupRatio(76-89)logger/logger.go (3)
LogInfo(55-57)LogQuota(94-100)LogError(63-65)model/channel.go (1)
UpdateChannelUsedQuota(740-746)model/log.go (1)
RecordLog(78-94)
web/src/helpers/render.jsx (1)
web/src/hooks/model-pricing/useModelPricingData.jsx (1)
groupRatio(50-50)
relay/relay_adaptor.go (3)
constant/channel.go (1)
ChannelTypeDoubaoVideo(54-54)relay/channel/task/doubao/adaptor.go (1)
TaskAdaptor(67-71)relay/channel/adapter.go (1)
TaskAdaptor(32-51)
🪛 Biome (2.1.2)
web/src/pages/PrivacyPolicy/index.jsx
[error] 231-231: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
web/src/pages/UserAgreement/index.jsx
[error] 231-233: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
web/src/components/layout/Footer.jsx
[error] 229-229: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🔇 Additional comments (24)
web/src/components/table/channels/modals/EditChannelModal.jsx (3)
94-95: LGTM!The consolidation of
MODEL_FETCHABLE_TYPESto a single-line format is a cosmetic change that improves readability while preserving all original values.
267-272: LGTM!The
scrollIntoViewoptions are correctly formatted with standard properties (behavior,block,inline). The inline style improves code consistency.
328-354: LGTM!The inline styles for the navigation buttons correctly implement circular icon button styling with proper flexbox centering. The properties are valid and appropriate.
controller/channel-test.go (1)
73-78: LGTM!The early-return branch for the Doubao Video channel follows the established pattern for unsupported channel types. The placement is consistent and the error message is clear.
router/api-router.go (1)
23-24: Verify rate limiting for public endpoints.The new endpoints for user agreement and privacy policy follow the established pattern. However, ensure that public access without rate limiting is acceptable for these endpoints, especially if the content could be large.
relay/relay_adaptor.go (3)
107-108: LGTM!The addition of the Submodel adaptor follows the established pattern and correctly returns a submodel.Adaptor instance.
138-139: LGTM!The Doubao video task adaptor is correctly wired to the ChannelTypeDoubaoVideo constant (54) and follows the established pattern for task adaptors.
4-4: Keep gin import - gin.Context is used in GetTaskPlatform signature (relay/relay_adaptor.go:113).Likely an incorrect or invalid review comment.
constant/channel.go (2)
54-54: LGTM!The ChannelTypeDoubaoVideo constant follows the sequential numbering pattern and is correctly integrated throughout the codebase.
114-114: LGTM!The base URL for the Doubao Video channel matches the Volcengine base URL and is correctly placed at index 54.
relay/common/relay_info.go (1)
503-510: LGTM!The new token fields in TaskInfo enable per-billing token tracking without breaking changes. The fields are correctly marked as optional with
omitemptytags.relay/channel/task/doubao/constants.go (1)
3-9: LGTM!The model list and channel name are clearly defined for the Doubao video channel. The model identifiers follow a consistent naming pattern.
relay/channel/api_request.go (3)
73-79: LGTM!The integration of processHeaderOverride in DoApiRequest follows the DRY principle and correctly handles errors.
106-112: LGTM!The integration of processHeaderOverride in DoFormRequest is consistent with the pattern used in DoApiRequest.
130-136: LGTM!The integration of processHeaderOverride in DoWssRequest maintains consistency across all request functions.
controller/misc.go (1)
150-170: LGTM!The GetUserAgreement and GetPrivacyPolicy handlers follow the established pattern used by GetAbout and GetNotice. The read lock usage is correct and consistent.
relay/channel/claude/adaptor.go (1)
67-74: Good consolidation of Claude header handlingCentralizing anthropic-beta passthrough and settings-based header writes improves maintainability. No issues spotted.
Also applies to: 84-84
web/src/pages/Setting/Ratio/ModelRatioSettings.jsx (1)
229-246: i18n text and handlers look goodWrapping strings with t() and simplifying onChange are fine; no logic changes.
Also applies to: 254-256, 267-267, 275-281
README.md (1)
78-78: Docs updates read wellWording and links align with added features; looks consistent.
Also applies to: 122-125, 138-141, 185-185, 199-201
model/option.go (1)
64-65: New option defaults added correctlyUserAgreement/PrivacyPolicy keys initialized; aligns with new endpoints/UI.
web/src/i18n/locales/fr.json (1)
2176-2257: FR locale additions look consistentNew UserAgreement/PrivacyPolicy/Passkey strings are coherent; no JSON issues spotted.
web/src/helpers/render.jsx (1)
340-341: Channel icon for Doubao VideoCase 54 mapping added; icon choice is consistent with 45 (Doubao).
web/src/App.jsx (1)
54-56: New routes wired correctlyLazy imports and Suspense usage for /user-agreement and /privacy-policy follow existing patterns.
Also applies to: 306-321
web/src/constants/channel.constants.js (1)
168-171: Channel option addedEntry for value 54 (豆包视频) looks correct and consistent with icon mapping.
| user, err := model.GetUserById(task.UserId, false) | ||
| if err == nil { | ||
| groupRatio := ratio_setting.GetGroupRatio(user.Group) | ||
| userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, user.Group) | ||
|
|
||
| var finalGroupRatio float64 | ||
| if hasUserGroupRatio { | ||
| finalGroupRatio = userGroupRatio | ||
| } else { | ||
| finalGroupRatio = groupRatio | ||
| } |
There was a problem hiding this comment.
Incorrect group ratio lookup breaks cross-group billing
GetGroupGroupRatio expects the user's group and the channel's group. Passing user.Group twice means any configured cross-group ratio is ignored, so the final quota never reflects channel-specific pricing. This under/over-charges whenever channel groups carry custom ratios. Please pass the channel's group when computing the final ratio.
- groupRatio := ratio_setting.GetGroupRatio(user.Group)
- userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, user.Group)
+ groupRatio := ratio_setting.GetGroupRatio(user.Group)
+ userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, channel.Group)📝 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.
| user, err := model.GetUserById(task.UserId, false) | |
| if err == nil { | |
| groupRatio := ratio_setting.GetGroupRatio(user.Group) | |
| userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, user.Group) | |
| var finalGroupRatio float64 | |
| if hasUserGroupRatio { | |
| finalGroupRatio = userGroupRatio | |
| } else { | |
| finalGroupRatio = groupRatio | |
| } | |
| user, err := model.GetUserById(task.UserId, false) | |
| if err == nil { | |
| groupRatio := ratio_setting.GetGroupRatio(user.Group) | |
| userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(user.Group, channel.Group) | |
| var finalGroupRatio float64 | |
| if hasUserGroupRatio { | |
| finalGroupRatio = userGroupRatio | |
| } else { | |
| finalGroupRatio = groupRatio | |
| } |
🤖 Prompt for AI Agents
In controller/task_video.go around lines 137 to 147, the call to
GetGroupGroupRatio mistakenly passes user.Group for both parameters which
prevents cross-group ratios from being applied; change the second argument to
the channel's group (e.g., channel.Group) so the function is invoked as
GetGroupGroupRatio(user.Group, channel.Group), then use that returned ratio when
hasUserGroupRatio is true so channel-specific pricing is honored.
| # - mysql # Uncomment if using MySQL | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $$2}'"] | ||
| test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"] |
There was a problem hiding this comment.
Healthcheck fails when JSON contains spaces
grep without -P/-E treats \s literally, so the pattern only matches when there is no space between : and true. If the API returns \"success\": true, the healthcheck exits 1 and keeps the container unhealthy. Please adjust the regex to tolerate whitespace, e.g.:
- test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
+ test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -q '\"success\":[[:space:]]*true' || exit 1"]📝 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.
| test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"] | |
| test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -q '\"success\":[[:space:]]*true' || exit 1"] |
🤖 Prompt for AI Agents
In docker-compose.yml at line 44, the healthcheck grep pattern treats \s
literally so it fails when JSON contains spaces; update the test command to use
a regex that tolerates optional whitespace (for example use grep -E with a
pattern like '"success":[[:space:]]*true' or grep -P with '\s*') so the
healthcheck matches both "\"success\":true" and "\"success\": true" and returns
success accordingly.
| go 1.24.0 | ||
|
|
||
| toolchain go1.24.6 | ||
| go 1.25.1 |
There was a problem hiding this comment.
Unsupported Go toolchain version.
go 1.25.1 will fail today—the Go team hasn’t shipped 1.25 (nor 1.25.1) yet, so go refuses to build this module. Please stick with the latest released toolchain (e.g., 1.23.x or whatever you’ve validated) until 1.25 officially lands.
🤖 Prompt for AI Agents
In go.mod around line 4, the declared Go toolchain version "go 1.25.1" is not
released and will cause builds to fail; change it to a released, supported
version you’ve validated (for example "go 1.23" or "go 1.24" if tested) by
updating the go directive to that released version and re-run go mod tidy/build
to ensure module compatibility and update any go-version-dependent code if
needed.
| responseBody, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | ||
| return | ||
| } | ||
| _ = resp.Body.Close() | ||
|
|
There was a problem hiding this comment.
Ensure response body closes on every exit path.
If io.ReadAll errors, the current code returns without closing resp.Body, leaking the connection. Add a defer resp.Body.Close() before reading so the body is closed even when the read fails.
Here is an example fix:
- responseBody, err := io.ReadAll(resp.Body)
+ defer resp.Body.Close()
+
+ responseBody, err := io.ReadAll(resp.Body)
if err != nil {
taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
return
}
- _ = resp.Body.Close()📝 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.
| responseBody, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } | |
| _ = resp.Body.Close() | |
| defer resp.Body.Close() | |
| responseBody, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } |
🤖 Prompt for AI Agents
In relay/channel/task/doubao/adaptor.go around lines 124 to 130, the response
body is closed only after a successful ReadAll which leaks the connection when
ReadAll returns an error; add defer resp.Body.Close() immediately after
confirming resp.Body is non-nil (before calling io.ReadAll) so the body is
closed on every exit path, and remove or stop using the later explicit _ =
resp.Body.Close() to avoid double-close.
| var dResp responsePayload | ||
| if err := json.Unmarshal(responseBody, &dResp); err != nil { | ||
| taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| if dResp.ID == "" { | ||
| taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) | ||
| return dResp.ID, responseBody, nil |
There was a problem hiding this comment.
Propagate upstream error responses instead of masking them.
When Doubao returns a non-2xx status, we still attempt to unmarshal an id, end up with an empty ID, and report a 500 “invalid_response”. This hides the real upstream failure code/message from the caller. Check resp.StatusCode right after reading the body and surface a TaskError that carries the upstream status (and, ideally, the upstream error payload) so clients receive accurate feedback.
A possible adjustment:
if err := json.Unmarshal(responseBody, &dResp); err != nil {
taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
return
}
+
+ if resp.StatusCode >= http.StatusBadRequest {
+ taskErr = service.TaskErrorWrapper(
+ fmt.Errorf("upstream returned status %d: %s", resp.StatusCode, responseBody),
+ "upstream_request_failed",
+ resp.StatusCode,
+ )
+ return
+ }📝 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.
| var dResp responsePayload | |
| if err := json.Unmarshal(responseBody, &dResp); err != nil { | |
| taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } | |
| if dResp.ID == "" { | |
| taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) | |
| return | |
| } | |
| c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) | |
| return dResp.ID, responseBody, nil | |
| var dResp responsePayload | |
| if err := json.Unmarshal(responseBody, &dResp); err != nil { | |
| taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } | |
| if resp.StatusCode >= http.StatusBadRequest { | |
| taskErr = service.TaskErrorWrapper( | |
| fmt.Errorf("upstream returned status %d: %s", resp.StatusCode, responseBody), | |
| "upstream_request_failed", | |
| resp.StatusCode, | |
| ) | |
| return | |
| } | |
| if dResp.ID == "" { | |
| taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) | |
| return | |
| } | |
| c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) | |
| return dResp.ID, responseBody, nil |
🤖 Prompt for AI Agents
In relay/channel/task/doubao/adaptor.go around lines 132 to 144, the code
unmarshals the response and returns a 500 when the ID is empty, which masks
upstream non-2xx errors; after reading responseBody check resp.StatusCode and if
it's not 2xx wrap and return a TaskError that includes the upstream status code
and the raw response body (or payload) instead of attempting to unmarshal an ID,
only unmarshal when status is 2xx and then validate ID as now; this surfaces the
real upstream error code/message to callers.
| const cardProPages = [ | ||
| '/console/channel', | ||
| '/console/log', | ||
| '/console/redemption', | ||
| '/console/user', | ||
| '/console/token', | ||
| '/console/midjourney', | ||
| '/console/task', | ||
| '/console/models', | ||
| '/pricing', | ||
| ]; | ||
|
|
||
| const shouldHideFooter = cardProPages.includes(location.pathname); | ||
|
|
There was a problem hiding this comment.
Footer now visible on console subroutes
Switching to exact-match checks means routes like /console/channel/123 or other nested console pages will no longer hide the footer, unlike the previous prefix-based logic. Reintroduce a prefix/regex check (or expand the list to cover subpaths) so every console view that should suppress the footer still does.
| (webSearchCallCount / 1000) * webSearchPrice * groupRatio + | ||
| (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + | ||
| (imageGenerationCallPrice * groupRatio); | ||
| imageGenerationCallPrice * groupRatio; |
There was a problem hiding this comment.
Bug: image generation fee always added to total
You always add imageGenerationCallPrice * groupRatio to price, even when imageGenerationCall is false. This inflates totals when no image generation occurs.
Fix by guarding with the flag:
- (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
- imageGenerationCallPrice * groupRatio;
+ (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
+ (imageGenerationCall ? imageGenerationCallPrice * groupRatio : 0);📝 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.
| imageGenerationCallPrice * groupRatio; | |
| (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + | |
| (imageGenerationCall ? imageGenerationCallPrice * groupRatio : 0); |
🤖 Prompt for AI Agents
In web/src/helpers/render.jsx around line 1077, the expression
imageGenerationCallPrice * groupRatio is always added to the total price even
when imageGenerationCall is false; wrap that addition in a conditional that
checks imageGenerationCall (or the appropriate boolean flag in scope) and only
add imageGenerationCallPrice * groupRatio when the flag is true, keeping all
other pricing logic unchanged.
| // inject inline styles for parsed HTML content and cleanup on unmount or styles change | ||
| useEffect(() => { | ||
| const styleId = 'privacy-policy-inline-styles'; | ||
| const createdLinkIds = []; | ||
|
|
||
| if (htmlStyles) { | ||
| let styleEl = document.getElementById(styleId); | ||
| if (!styleEl) { | ||
| styleEl = document.createElement('style'); | ||
| styleEl.id = styleId; | ||
| styleEl.type = 'text/css'; | ||
| document.head.appendChild(styleEl); | ||
| } | ||
| styleEl.innerHTML = htmlStyles; | ||
| } else { | ||
| const el = document.getElementById(styleId); | ||
| if (el) el.remove(); | ||
| } | ||
|
|
||
| if (htmlLinks && htmlLinks.length) { | ||
| htmlLinks.forEach((href, idx) => { | ||
| try { | ||
| const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); | ||
| if (existing) return; | ||
| const linkId = `${styleId}-link-${idx}`; | ||
| const linkEl = document.createElement('link'); | ||
| linkEl.id = linkId; | ||
| linkEl.rel = 'stylesheet'; | ||
| linkEl.href = href; | ||
| document.head.appendChild(linkEl); | ||
| createdLinkIds.push(linkId); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| return () => { | ||
| const el = document.getElementById(styleId); | ||
| if (el) el.remove(); | ||
| createdLinkIds.forEach((id) => { | ||
| const l = document.getElementById(id); | ||
| if (l) l.remove(); | ||
| }); | ||
| }; | ||
| }, [htmlStyles]); |
There was a problem hiding this comment.
Missing dependency prevents stylesheet injection
This effect reads htmlLinks, but the dependency list only includes htmlStyles. When cached HTML comes with external stylesheets (or when the inline styles stay the same), the effect never reruns, so required <link> tags are never added to <head>. The rendered policy loses its styles. Please include htmlLinks (and, if desired, memoize cleanup accordingly) so we always sync injected assets.
- }, [htmlStyles]);
+ }, [htmlStyles, htmlLinks]);📝 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.
| // inject inline styles for parsed HTML content and cleanup on unmount or styles change | |
| useEffect(() => { | |
| const styleId = 'privacy-policy-inline-styles'; | |
| const createdLinkIds = []; | |
| if (htmlStyles) { | |
| let styleEl = document.getElementById(styleId); | |
| if (!styleEl) { | |
| styleEl = document.createElement('style'); | |
| styleEl.id = styleId; | |
| styleEl.type = 'text/css'; | |
| document.head.appendChild(styleEl); | |
| } | |
| styleEl.innerHTML = htmlStyles; | |
| } else { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| } | |
| if (htmlLinks && htmlLinks.length) { | |
| htmlLinks.forEach((href, idx) => { | |
| try { | |
| const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); | |
| if (existing) return; | |
| const linkId = `${styleId}-link-${idx}`; | |
| const linkEl = document.createElement('link'); | |
| linkEl.id = linkId; | |
| linkEl.rel = 'stylesheet'; | |
| linkEl.href = href; | |
| document.head.appendChild(linkEl); | |
| createdLinkIds.push(linkId); | |
| } catch (e) { | |
| // ignore | |
| } | |
| }); | |
| } | |
| return () => { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| createdLinkIds.forEach((id) => { | |
| const l = document.getElementById(id); | |
| if (l) l.remove(); | |
| }); | |
| }; | |
| }, [htmlStyles]); | |
| // inject inline styles for parsed HTML content and cleanup on unmount or styles change | |
| useEffect(() => { | |
| const styleId = 'privacy-policy-inline-styles'; | |
| const createdLinkIds = []; | |
| if (htmlStyles) { | |
| let styleEl = document.getElementById(styleId); | |
| if (!styleEl) { | |
| styleEl = document.createElement('style'); | |
| styleEl.id = styleId; | |
| styleEl.type = 'text/css'; | |
| document.head.appendChild(styleEl); | |
| } | |
| styleEl.innerHTML = htmlStyles; | |
| } else { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| } | |
| if (htmlLinks && htmlLinks.length) { | |
| htmlLinks.forEach((href, idx) => { | |
| try { | |
| const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); | |
| if (existing) return; | |
| const linkId = `${styleId}-link-${idx}`; | |
| const linkEl = document.createElement('link'); | |
| linkEl.id = linkId; | |
| linkEl.rel = 'stylesheet'; | |
| linkEl.href = href; | |
| document.head.appendChild(linkEl); | |
| createdLinkIds.push(linkId); | |
| } catch (e) { | |
| // ignore | |
| } | |
| }); | |
| } | |
| return () => { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| createdLinkIds.forEach((id) => { | |
| const l = document.getElementById(id); | |
| if (l) l.remove(); | |
| }); | |
| }; | |
| }, [htmlStyles, htmlLinks]); |
🤖 Prompt for AI Agents
In web/src/pages/PrivacyPolicy/index.jsx around lines 125 to 170, the useEffect
reads htmlLinks but only lists htmlStyles in its dependency array which prevents
link tags from being injected when htmlLinks changes; update the effect to
include htmlLinks in the dependency array (and if htmlLinks is an array, ensure
a stable reference or memoize it e.g. via useMemo or by joining values) so the
effect reruns to inject/remove link elements, and keep the existing cleanup
logic to remove the style and any created link IDs on unmount or when
dependencies change.
| const displayUserAgreement = async () => { | ||
| // 先从缓存中获取 | ||
| const cachedContent = localStorage.getItem('user_agreement') || ''; | ||
| if (cachedContent) { | ||
| setUserAgreement(cachedContent); | ||
| const ct = getContentType(cachedContent); | ||
| setContentType(ct); | ||
| if (ct === 'html') { | ||
| try { | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(cachedContent, 'text/html'); | ||
| setHtmlBody(doc.body ? doc.body.innerHTML : cachedContent); | ||
| const styles = Array.from(doc.querySelectorAll('style')) | ||
| .map((s) => s.innerHTML) | ||
| .join('\n'); | ||
| setHtmlStyles(styles); | ||
| const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')) | ||
| .map((l) => l.getAttribute('href') || l.href) | ||
| .filter(Boolean); | ||
| setHtmlLinks(links); | ||
| } catch (e) { | ||
| setHtmlBody(cachedContent); | ||
| setHtmlStyles(''); | ||
| setHtmlLinks([]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| const res = await API.get('/api/user-agreement'); | ||
| const { success, message, data } = res.data; | ||
| if (success && data) { | ||
| // 直接使用原始数据,不进行任何预处理 | ||
| setUserAgreement(data); | ||
| const ct = getContentType(data); | ||
| setContentType(ct); | ||
| if (ct === 'html') { | ||
| try { | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(data, 'text/html'); | ||
| setHtmlBody(doc.body ? doc.body.innerHTML : data); | ||
| const styles = Array.from(doc.querySelectorAll('style')) | ||
| .map((s) => s.innerHTML) | ||
| .join('\n'); | ||
| setHtmlStyles(styles); | ||
| const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')) | ||
| .map((l) => l.getAttribute('href') || l.href) | ||
| .filter(Boolean); | ||
| setHtmlLinks(links); | ||
| } catch (e) { | ||
| setHtmlBody(data); | ||
| setHtmlStyles(''); | ||
| setHtmlLinks([]); | ||
| } | ||
| } else { | ||
| setHtmlBody(''); | ||
| setHtmlStyles(''); | ||
| setHtmlLinks([]); | ||
| } | ||
| localStorage.setItem('user_agreement', data); | ||
| } else { | ||
| if (!cachedContent) { | ||
| showError(message || t('加载用户协议内容失败...')); | ||
| setUserAgreement(''); | ||
| setContentType('empty'); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| if (!cachedContent) { | ||
| showError(t('加载用户协议内容失败...')); | ||
| setUserAgreement(''); | ||
| setContentType('empty'); | ||
| } | ||
| } | ||
| setUserAgreementLoaded(true); |
There was a problem hiding this comment.
Sanitize policy content before injecting into the DOM
displayUserAgreement stores whatever the backend returns and later feeds it straight into dangerouslySetInnerHTML (Line 226) and even hoists <style>/<link> nodes into <head>. Without sanitization this is a stored-XSS sink—any HTML entered through the admin panel (or intercepted in transit) can embed <script>, event handlers, or javascript: URLs that will execute for every visitor. Please sanitize both the cached content and the freshly fetched response (e.g., via DOMPurify) before setting state, persisting to localStorage, or parsing for styles/links.
+import DOMPurify from 'dompurify';
...
- const cachedContent = localStorage.getItem('user_agreement') || '';
- if (cachedContent) {
- setUserAgreement(cachedContent);
- const ct = getContentType(cachedContent);
+ const cachedRaw = localStorage.getItem('user_agreement') || '';
+ const cachedContent = cachedRaw ? DOMPurify.sanitize(cachedRaw, { USE_PROFILES: { html: true } }) : '';
+ if (cachedContent) {
+ setUserAgreement(cachedContent);
+ const ct = getContentType(cachedContent);
...
}
...
- const { success, message, data } = res.data;
+ const { success, message, data } = res.data;
if (success && data) {
- setUserAgreement(data);
- const ct = getContentType(data);
+ const sanitized = DOMPurify.sanitize(data, { USE_PROFILES: { html: true } });
+ setUserAgreement(sanitized);
+ const ct = getContentType(sanitized);
setContentType(ct);
if (ct === 'html') {
try {
const parser = new DOMParser();
- const doc = parser.parseFromString(data, 'text/html');
- setHtmlBody(doc.body ? doc.body.innerHTML : data);
+ const doc = parser.parseFromString(sanitized, 'text/html');
+ setHtmlBody(doc.body ? DOMPurify.sanitize(doc.body.innerHTML, { USE_PROFILES: { html: true } }) : sanitized);
...
} catch (e) {
- setHtmlBody(data);
+ setHtmlBody(sanitized);
setHtmlStyles('');
setHtmlLinks([]);
}
} else {
setHtmlBody('');
setHtmlStyles('');
setHtmlLinks([]);
}
- localStorage.setItem('user_agreement', data);
+ localStorage.setItem('user_agreement', sanitized);
} else {Also applies to: 215-227
🤖 Prompt for AI Agents
In web/src/pages/UserAgreement/index.jsx around lines 42 to 116, the function
displayUserAgreement currently takes cached and fetched HTML and sets it
directly into state and localStorage and parses it for styles/links — this is a
stored-XSS sink; sanitize both the cachedContent and the fetched data before any
setUserAgreement, localStorage.setItem, or parsing. Use a trusted sanitizer
(e.g., DOMPurify) to sanitize the raw string first, then parse the sanitized
string with DOMParser to extract safe body HTML, safe <style> contents, and
trusted stylesheet hrefs (validate/whitelist href protocols and strip
javascript: or data: URLs). For non-html types, leave behavior unchanged; ensure
any fallback/error paths also use sanitized values and do not persist
unsanitized content.
| // inject inline styles for parsed HTML content and cleanup on unmount or styles change | ||
| useEffect(() => { | ||
| // if there's nothing to inject, remove any existing injected elements | ||
| const styleId = 'user-agreement-inline-styles'; | ||
| const createdLinkIds = []; | ||
|
|
||
| // handle style tags | ||
| if (htmlStyles) { | ||
| let styleEl = document.getElementById(styleId); | ||
| if (!styleEl) { | ||
| styleEl = document.createElement('style'); | ||
| styleEl.id = styleId; | ||
| styleEl.type = 'text/css'; | ||
| document.head.appendChild(styleEl); | ||
| } | ||
| styleEl.innerHTML = htmlStyles; | ||
| } else { | ||
| const el = document.getElementById(styleId); | ||
| if (el) el.remove(); | ||
| } | ||
|
|
||
| // handle external stylesheet links | ||
| if (htmlLinks && htmlLinks.length) { | ||
| htmlLinks.forEach((href, idx) => { | ||
| try { | ||
| // avoid duplicate injection if a link with same href already exists | ||
| const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); | ||
| if (existing) return; | ||
| const linkId = `${styleId}-link-${idx}`; | ||
| const linkEl = document.createElement('link'); | ||
| linkEl.id = linkId; | ||
| linkEl.rel = 'stylesheet'; | ||
| linkEl.href = href; | ||
| document.head.appendChild(linkEl); | ||
| createdLinkIds.push(linkId); | ||
| } catch (e) { | ||
| // ignore malformed hrefs | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| return () => { | ||
| const el = document.getElementById(styleId); | ||
| if (el) el.remove(); | ||
| // remove only the links we created | ||
| createdLinkIds.forEach((id) => { | ||
| const l = document.getElementById(id); | ||
| if (l) l.remove(); | ||
| }); | ||
| }; | ||
| }, [htmlStyles]); | ||
|
|
There was a problem hiding this comment.
Include htmlLinks in the effect dependencies
The stylesheet injection effect reads htmlLinks, but it only depends on htmlStyles. If the backend returns new HTML that changes <link>s while the inline styles remain the same (very common when only external CSS references differ), the effect never re-runs, so new stylesheets are skipped and previously injected ones linger. Please add htmlLinks to the dependency list so React re-injects/cleans up when the link set changes.
- }, [htmlStyles]);
+ }, [htmlStyles, htmlLinks]);📝 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.
| // inject inline styles for parsed HTML content and cleanup on unmount or styles change | |
| useEffect(() => { | |
| // if there's nothing to inject, remove any existing injected elements | |
| const styleId = 'user-agreement-inline-styles'; | |
| const createdLinkIds = []; | |
| // handle style tags | |
| if (htmlStyles) { | |
| let styleEl = document.getElementById(styleId); | |
| if (!styleEl) { | |
| styleEl = document.createElement('style'); | |
| styleEl.id = styleId; | |
| styleEl.type = 'text/css'; | |
| document.head.appendChild(styleEl); | |
| } | |
| styleEl.innerHTML = htmlStyles; | |
| } else { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| } | |
| // handle external stylesheet links | |
| if (htmlLinks && htmlLinks.length) { | |
| htmlLinks.forEach((href, idx) => { | |
| try { | |
| // avoid duplicate injection if a link with same href already exists | |
| const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); | |
| if (existing) return; | |
| const linkId = `${styleId}-link-${idx}`; | |
| const linkEl = document.createElement('link'); | |
| linkEl.id = linkId; | |
| linkEl.rel = 'stylesheet'; | |
| linkEl.href = href; | |
| document.head.appendChild(linkEl); | |
| createdLinkIds.push(linkId); | |
| } catch (e) { | |
| // ignore malformed hrefs | |
| } | |
| }); | |
| } | |
| return () => { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| // remove only the links we created | |
| createdLinkIds.forEach((id) => { | |
| const l = document.getElementById(id); | |
| if (l) l.remove(); | |
| }); | |
| }; | |
| }, [htmlStyles]); | |
| // inject inline styles for parsed HTML content and cleanup on unmount or styles change | |
| useEffect(() => { | |
| // if there's nothing to inject, remove any existing injected elements | |
| const styleId = 'user-agreement-inline-styles'; | |
| const createdLinkIds = []; | |
| // handle style tags | |
| if (htmlStyles) { | |
| let styleEl = document.getElementById(styleId); | |
| if (!styleEl) { | |
| styleEl = document.createElement('style'); | |
| styleEl.id = styleId; | |
| styleEl.type = 'text/css'; | |
| document.head.appendChild(styleEl); | |
| } | |
| styleEl.innerHTML = htmlStyles; | |
| } else { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| } | |
| // handle external stylesheet links | |
| if (htmlLinks && htmlLinks.length) { | |
| htmlLinks.forEach((href, idx) => { | |
| try { | |
| // avoid duplicate injection if a link with same href already exists | |
| const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); | |
| if (existing) return; | |
| const linkId = `${styleId}-link-${idx}`; | |
| const linkEl = document.createElement('link'); | |
| linkEl.id = linkId; | |
| linkEl.rel = 'stylesheet'; | |
| linkEl.href = href; | |
| document.head.appendChild(linkEl); | |
| createdLinkIds.push(linkId); | |
| } catch (e) { | |
| // ignore malformed hrefs | |
| } | |
| }); | |
| } | |
| return () => { | |
| const el = document.getElementById(styleId); | |
| if (el) el.remove(); | |
| // remove only the links we created | |
| createdLinkIds.forEach((id) => { | |
| const l = document.getElementById(id); | |
| if (l) l.remove(); | |
| }); | |
| }; | |
| }, [htmlStyles, htmlLinks]); |
🤖 Prompt for AI Agents
In web/src/pages/UserAgreement/index.jsx around lines 123 to 174, the effect
injects external stylesheet links but only lists htmlStyles in its dependency
array; add htmlLinks to the dependencies (e.g., change [htmlStyles] to
[htmlStyles, htmlLinks]) so the effect re-runs and cleans up/re-injects when the
set of external links changes.
PR 类型
PR 是否包含破坏性更新?
PR 描述
请在下方详细描述您的 PR,包括目的、实现细节等。
重要提示
所有 PR 都必须提交到
alpha分支。请确保您的 PR 目标分支是alpha。Summary by CodeRabbit
New Features
Documentation
Style / Content