feat:增加日志导出功能 - #3098
Conversation
- 在创建记录前保存原始状态和同步官方字段值 - 使用独立的更新操作确保零值能够正确保存到数据库 - 修改更新方法使用 Select 强制更新所有字段包括零值 - 避免 GORM 默认行为对零值字段应用默认值导致数据丢失
* ci: create docker automation * ✨ feat: add subscription billing system with admin management and user purchase flow Implement a new subscription-based billing model alongside existing metered/per-request billing: Backend: - Add subscription plan models (SubscriptionPlan, SubscriptionPlanItem, UserSubscription, etc.) - Implement CRUD APIs for subscription plan management (admin only) - Add user subscription queries with support for multiple active/expired subscriptions - Integrate payment gateways (Stripe, Creem, Epay) for subscription purchases - Implement pre-consume and post-consume billing logic for subscription quota tracking - Add billing preference settings (subscription_first, wallet_first, etc.) - Enhance usage logs with subscription deduction details Frontend - Admin: - Add subscription management page with table view and drawer-based edit form - Match UI/UX style with existing admin pages (redemption codes, users) - Support enabling/disabling plans, configuring payment IDs, and model quotas - Add user subscription binding modal in user management Frontend - Wallet: - Add subscription plans card with current subscription status display - Show all subscriptions (active and expired) with remaining days/usage percentage - Display purchasable plans with pricing cards following SaaS best practices - Extract purchase modal to separate component matching payment confirm modal style - Add skeleton loading states with active animation - Implement billing preference selector in card header - Handle payment gateway availability based on admin configuration Frontend - Usage Logs: - Display subscription deduction details in log entries - Show step-by-step breakdown of subscription usage (pre-consumed, delta, final, remaining) - Add subscription deduction tag for subscription-covered requests * ✨ feat(admin): add user subscription management and refine UI/pagination Add admin APIs to list/create/invalidate/delete user subscriptions Add model helpers to fetch all user subscriptions (incl. expired) and support cancel/hard-delete Wire new admin routes for user subscription operations Replace “Bind subscription plan” entry with a dedicated User Subscriptions SideSheet in Users table Use CardTable with responsive layout and working client-side pagination inside the SideSheet Improve subscription purchase modal empty-gateway state with a Banner notice * ✨ feat(admin): streamline subscription plan benefits editor with bulk actions Restore the avatar/icon header for the “Model Benefits” section Replace scattered controls with a compact toolbar-style workflow Support multi-select add with a default quota for new items Add row selection with bulk apply-to-selected / apply-to-all quota updates Enable delete-selected to manage benefits faster and reduce mistakes * ✨ fix(subscription): finalize payments, log billing, and clean up dead code Complete subscription orders by creating a matching top-up record and writing billing logs Add Epay return handler to verify and finalize browser callbacks Require Stripe/Creem webhook configuration before starting subscription payments Show subscription purchases in topup history with clearer labels/methods Remove unused subscription helper, legacy Creem webhook struct, and unused topup fields Simplify subscription self API payload to active/all lists only * 🎨 style: format all code with gofmt and lint:fix Apply consistent code formatting across the entire codebase using gofmt and lint:fix tools. This ensures adherence to Go community standards and improves code readability and maintainability. Changes include: - Run gofmt on all .go files to standardize formatting - Apply lint:fix to automatically resolve linting issues - Fix code style inconsistencies and formatting violations No functional changes were made in this commit. * ✨ feat(subscription): add quota reset periods and admin configuration - Add reset period fields on subscription plans and user items - Apply automatic quota resets during pre-consume based on plan schedule - Expose reset-period configuration in the admin plan editor - Display reset cadence in subscription cards and purchase modal - Validate custom reset seconds on plan create/update * ✨ feat(subscription): harden subscription billing with resets, idempotency, and production-grade stability Add plan-level quota reset periods and display/reset cadence in admin/UI Enforce natural reset alignment with background reset task and cleanup job Make subscription pre-consume/refund idempotent with request-scoped records and retries Use database time for consistent resets across multi-instance deployments Harden payment callbacks with locking and idempotent order completion Record subscription purchases in topup history and billing logs Optimize subscription queries and add critical composite indexes * ✨ feat(subscription): cache plan lookups and stabilize pre-consume Introduce hybrid caches for subscription plans, items, and plan info with explicit invalidation on admin updates. Streamline pre-consume transactions to reduce redundant queries while preserving idempotency and reset logic. * 🐛 fix(subscription): avoid pre-consume lookup noise Use a RowsAffected check for the idempotency lookup so missing records no longer surface as "record not found" errors while preserving behavior. * 🔧 ci: Change workflow trigger to sub branch Update the Docker image workflow to run on pushes to the sub branch instead of main. * 💸 chore: Align subscription pricing display with global currency settings Unify subscription price rendering to use the site-wide currency symbol/rate on the wallet and admin views. Make subscription plan currency read-only in the editor and force USD on create/update to avoid drift. Use global currency display type when creating Creem checkout payloads. * 🔧 chore: Unify subscription plan status toggle with PATCH endpoint Replace separate enable/disable flows with a single PATCH API that updates the enabled flag. Update frontend hooks and table actions to call the unified endpoint and keep UI behavior consistent. Introduce a minimal admin controller handler and route for the status update. * ✨ feat: Add subscription limits and UI tags consistency Add per-plan purchase limits with backend enforcement and UI disable states. Expose limit configuration in admin plan editor and show limits in plan tables/cards. Refine subscription UI tags with unified badge style and streamlined “My Subscriptions” layout. * 🎨 style: tag color to white * 🚀 refactor: Simplify subscription quota to total amount model Remove per-model subscription items and switch to a single total quota per plan and user subscription. Update billing, reset, and logging flows to operate on total quota, and refactor admin/user UI to configure and display total quota consistently. * 🚀 chore: Remove duplicate subscription usage percentage display Keep the usage percentage shown only in the total quota line to avoid redundant “已用 0%” text while preserving remaining days in the summary. * ✨ feat: Add subscription upgrade group with auto downgrade * ✨ feat: Update subscription purchase modal display Show total quota as currency with tooltip for raw quota, hide reset cycle when never, and display upgrade group when configured to match card display rules. * ✨ feat: Extract quota conversion helpers to shared utils Move quota display/conversion helpers into web/src/helpers/quota.js and update the subscription plan editor to import and use the shared utilities instead of inline functions. * ✨ chore: Add upgrade group guidance in subscription editor Add explanatory helper text under the upgrade group field to clarify automatic group upgrades, rollback conditions, and the expected delay before downgrading takes effect. * 🔧 chore: remove unused Creem settings state Drop the unused originInputs state and redundant updates to keep the Creem settings form state minimal and easier to maintain. * 🚀 chore: Remove useless action * ✨ Add full i18n coverage for subscription-related UI across locales * ✨ feat: harden subscription billing and improve UI consistency Improve subscription payment safety and data integrity by handling user/URL lookup failures, fixing Stripe subscription mode, persisting quota reset fields, and correcting subscription delta accounting and DB timestamp casting. Refine the UI with stricter custom duration validation, accurate currency rounding, conditional Epay labeling, rollback on preference update failure, and shared subscription formatting helpers plus clearer component naming. * 🔧 fix: make epay webhook and return flow subscription-aware Ensure Epay webhook acknowledges success only after order completion, returning fail on processing errors to allow retries. Redirect subscription payment returns to the subscription page instead of top-up for correct user flow. * 🚦 fix: guard epay return success on order completion Redirect subscription return flow to failure when order completion fails, preventing false success states after payment verification. * 🔧 fix: normalize epay error handling and webhook retries Standardize SubscriptionRequestEpay error responses via ApiErrorMsg for a consistent schema. Return "fail" on non-success trade statuses in the epay webhook to preserve retry behavior. * 🧾 fix: persist epay orders before purchase Create the subscription order before initiating epay payment and expire it if the provider call fails, preventing orphaned transactions and improving reconciliation. * 🔧 fix: harden epay callbacks and billing fallbacks Use POST and form parsing for epay notify/return routes, persist epay orders before provider calls with expiry on failure, and ensure notify handlers retry correctly. Restrict subscription-first fallback to insufficient-subscription errors and log refund failures after retries to avoid silent quota drift. * 🔧 fix: harden billing flow and sidebar settings Add missing strings import for subscription fallback checks, log failed subscription refunds after retries, and extend sidebar module settings with a subscription management toggle plus translations. * 🛡️ fix: fail fast on epay form parse errors Handle ParseForm errors in epay notify/return handlers by returning fail or redirecting to failure, avoiding unsafe fallback to query parameters. * ✨ fix: refine Japanese subscription status labels Adjust Japanese UI wording for active-count labels to read more naturally and consistently. * ✅ fix: standardize epay success response schema Return subscription epay pay success responses via ApiSuccess to include the consistent success field and align with error schema.
…-function fix: 移除不必要的 unescapeMapOrSlice 调用,修复 Windows 路径转义问题
… type - Add validation to ensure subscription plan price amount is non-negative and does not exceed 9999. - Migrate the price_amount column from float/double to decimal(10,6) in the database for improved precision. - Update SubscriptionPlan model to reflect the new decimal type for price_amount.
- Update EpayNotify and SubscriptionEpayNotify functions to handle both POST and GET requests for parameter parsing. - Improve error handling by logging failures and returning appropriate responses when parameters are missing or parsing fails. - Ensure consistent behavior across both notify and return routes for better reliability in payment processing.
- Introduce a `workflow_dispatch` event to allow manual triggering of the Docker image build workflow. - Add an input parameter for specifying the tag name, enhancing flexibility in build processes. - Update tag resolution logic to prioritize the input tag when provided, ensuring accurate versioning during builds.
…le creation and migration QuantumNous#2823
…78addbb73be4739dc54f41acd8b feat: add useTimeSeconds in error log
…log-column feat: log search field request_id && conversion_path display
… URL and base64 data
…nd cleanup optimizations
…group-colors Revert "fix(ui): use distinct color palette for group tags"
… image count logic
…gurable thresholds
…-monitoring feat(performance): implement system performance monitoring
…bfe886b93606003f6753fcb4e9d feat: task log show username
…management across multiple languages
…ference - Add go-i18n library for internationalization - Create i18n package with translation keys and YAML locale files (zh/en) - Implement i18n middleware for language detection from user settings and Accept-Language header - Add Language field to UserSetting DTO - Update API response helpers with i18n support (ApiErrorI18n, ApiSuccessI18n) - Migrate hardcoded messages in token, redemption, and user controllers - Add frontend language preference settings component - Sync language preference across header selector and user settings - Auto-restore user language preference on login
fix: default summary = detailed fix ReasoningContent fix ReasoningContent fix ReasoningContent fix ReasoningContent Revert "fix ReasoningContent" This reverts commit 45a88f7. fix ReasoningContent fix ReasoningContent
- Change default language fallback to English instead of Chinese - Add ErrRedeemFailed typed error for model layer translation - Migrate remaining hardcoded messages in controller/user.go - Add translation keys: redeem.failed, user.create_default_token_error, common.uuid_duplicate, common.invalid_input
The i18n middleware runs before UserAuth, so user settings weren't available when language was detected. Now GetLangFromContext checks user settings first (set by UserAuth) before falling back to the language set by middleware or Accept-Language header.
Unify Epay subscription response format with top-up flow, and harden frontend error handling to avoid object-to-string issues. Refine subscription plan cards layout to be wider, left-aligned, and visually consistent across breakpoints.
fix: show built-in user bindings from user detail API in admin modal
…timation - Added Gemini video generation configuration structures and payloads. - Introduced functions for parsing and resolving video duration and resolution from metadata. - Enhanced the Vertex adaptor to support Gemini video generation requests and billing estimation based on duration and resolution. - Updated model pricing settings for new Gemini video models.
…t-url fix: redirect subscription payment return to user-accessible page
feat: add image model to supported image presets
…soning_split feat: minimax reasoning_split
feat(gemini): implement video generation configuration
…am-override feat: improve channel override ui/ux
Bumps [axios](https://github.com/axios/axios) from 1.12.0 to 1.13.5. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](axios/axios@v1.12.0...v1.13.5) --- updated-dependencies: - dependency-name: axios dependency-version: 1.13.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
- Refactored the request URL and body construction methods to align with the Veo predictLongRunning endpoint. - Introduced new data structures for Veo instances and parameters, replacing the previous Gemini video generation configurations. - Updated the Vertex adaptor to utilize the new Veo request payload format.
…nd_yarn/web/axios-1.13.5 chore(deps): bump axios from 1.12.0 to 1.13.5 in /web
…nd_yarn/electron/multi-227d46b8ec chore(deps): bump tar and electron-builder in /electron
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](isaacs/minimatch@v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…nd_yarn/electron/lodash-4.17.23 build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /electron
…nd_yarn/electron/minimatch-3.1.5 chore(deps-dev): bump minimatch from 3.1.2 to 3.1.5 in /electron
…nore fix: preserve explicit zero values in native relay requests
- Introduced a new CCSwitchModal component for managing CCSwitch configurations. - Updated the TokensPage to include functionality for opening the CCSwitch modal. - Enhanced the useTokensData hook to handle CCSwitch URLs and trigger the modal. - Modified chat settings to include a new "CC Switch" entry. - Updated sidebar logic to skip certain links based on the new configuration.
fix: aws text content blocks must be non-empty
WalkthroughAdds CSV export functionality for usage logs across backend and frontend. Introduces new export endpoints in the API layer, corresponding data retrieval functions in the model layer, and integrates export UI controls and download logic in the frontend, along with supporting translations across multiple language locales. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser / Client
participant UI as Frontend Component
participant Hook as useUsageLogsData Hook
participant API as Backend API
participant Model as Model Layer
participant DB as Database
Browser->>UI: Click "Export CSV" button
UI->>Hook: Call exportLogs()
Hook->>Hook: Collect form values & build request body
alt Admin Export Path
Hook->>API: POST /api/log/export (admin context)
else User Export Path
Hook->>API: POST /api/log/self/export (user context)
end
API->>Model: GetAllLogsForExport() or GetUserLogsForExport()
Model->>DB: Query logs with filters (logType, timestamps, etc.)
DB-->>Model: Return log records
Model->>Model: Format logs & cap at maxExportCount
Model-->>API: Return filtered log slice
alt Logs Found
API->>API: writeLogCSV() - build CSV with BOM & headers
API-->>Hook: Return CSV blob (Content-Type: text/csv)
else No Logs
API-->>Hook: Return JSON error (Content-Type: application/json)
end
Hook->>Hook: Detect response Content-Type
alt CSV Blob
Hook->>Browser: Trigger download via blob URL
Browser->>Browser: Save CSV file
Hook->>Browser: Revoke blob URL
Hook->>UI: Display success message
else JSON Error
Hook->>UI: Parse & display error message
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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: 5
🧹 Nitpick comments (2)
model/log.go (1)
386-388: AlignmodelNamefiltering behavior with user export path.Line 387 uses raw
LIKE ?, whileGetUserLogsForExportescapes user input. This can make admin export results inconsistent with user export/search semantics when%/_appear.🔧 Suggested refactor
- if modelName != "" { - tx = tx.Where("logs.model_name like ?", modelName) - } + if modelName != "" { + modelNamePattern, err := sanitizeLikePattern(modelName) + if err != nil { + return nil, err + } + tx = tx.Where("logs.model_name LIKE ? ESCAPE '!'", modelNamePattern) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@model/log.go` around lines 386 - 388, The modelName filter uses a raw LIKE which differs from the escaped semantics in GetUserLogsForExport; update the filter in the code that sets tx (when modelName != "") to apply the same escaping and pattern construction used by GetUserLogsForExport (e.g., call the same EscapeLike helper or escape '%' and '_' then surround with '%' as that function does) and pass the escaped pattern into tx.Where (and include the same ESCAPE clause if used) so admin exports/searches behave identically to user exports.web/src/i18n/locales/fr.json (1)
1103-1103: Prefer more natural French wording for the export label.Line 1103 would read more idiomatically as
Exporter en CSV.💡 Suggested wording tweak
- "导出 CSV": "Exporter CSV", + "导出 CSV": "Exporter en CSV",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/i18n/locales/fr.json` at line 1103, The French translation for the key "导出 CSV" is slightly unnatural; update its value in the fr.json locale so the pair reads "导出 CSV": "Exporter en CSV" (replace "Exporter CSV" with "Exporter en CSV") to match idiomatic French.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@controller/log.go`:
- Around line 253-289: The CSV writer calls are currently ignoring errors;
update the export logic around w := csv.NewWriter(c.Writer) and each
w.Write(...) plus w.Flush() to check returned errors (from w.Write and after
w.Flush via w.Error() or the returned error), handle them by logging the error
and terminating the loop/returning an appropriate HTTP error or aborting the
response to avoid producing incomplete CSVs; ensure you handle the initial
c.Writer.Write(BOM) error as well and use the unique identifiers log loop,
w.Write calls, w.Flush(), and c.Writer to locate and implement these checks.
- Around line 264-285: The CSV export writes untrusted text fields directly (in
the loop calling w.Write) which can trigger Excel formula injection; add a
helper function sanitizeCSVCell(s string) string that returns the original s
unless it begins with one of the dangerous prefixes ('=', '+', '-', '@'), in
which case it returns a safe-prefixed value (e.g., prepend a single quote) and
then call sanitizeCSVCell on all user-controlled fields used in the Write
call—specifically Username, TokenName, ModelName, ChannelName, Group, Ip,
RequestId, and Content—so those values are sanitized before being passed to
w.Write.
In `@model/log.go`:
- Around line 379-413: GetAllLogsForExport currently fetches logs without
loading ChannelName, so exported CSV shows an empty "渠道名称"; modify
GetAllLogsForExport to include the channel name by joining or preloading the
channel data: either add a LEFT JOIN on the channels table and select
channels.name AS channel_name (or the correct column) so GORM scans it into
Log.ChannelName, or Preload("Channel") and after Find iterate logs to set
Log.ChannelName = log.Channel.Name; update the tx query (in GetAllLogsForExport)
to include this join/select or the Preload call and ensure the selected alias
matches the Log.ChannelName field.
In `@web/src/hooks/usage-logs/useUsageLogsData.jsx`:
- Around line 779-781: The catch block in useUsageLogsData.jsx currently
replaces the caught error with a generic t('导出日志失败') string (catch (err) {
showError(t('导出日志失败')); }), losing structured error information; change the
catch to pass the original error to showError (e.g., showError(err)) or at
minimum include err details (showError(err?.message || t('导出日志失败'))) so
status-based handling and backend-provided details are preserved; update the
catch in the export-related function where showError and t are used.
In `@web/src/i18n/locales/vi.json`:
- Around line 1095-1097: The translation for export-related keys contains
English text; update the i18n entries "导出日志失败" (currently "Failed to export
logs") and the other export keys at the same group (also referenced around lines
with keys for exporting at 1540-1541) to complete Vietnamese translations so the
export flow shows consistent Vietnamese copy; locate the keys "导出 CSV",
"导出日志失败", and "导出配置" in vi.json and replace the English value(s) with
appropriate Vietnamese strings (e.g., "导出日志失败" -> "Xuất nhật ký thất bại")
ensuring all export-related keys in that file are translated.
---
Nitpick comments:
In `@model/log.go`:
- Around line 386-388: The modelName filter uses a raw LIKE which differs from
the escaped semantics in GetUserLogsForExport; update the filter in the code
that sets tx (when modelName != "") to apply the same escaping and pattern
construction used by GetUserLogsForExport (e.g., call the same EscapeLike helper
or escape '%' and '_' then surround with '%' as that function does) and pass the
escaped pattern into tx.Where (and include the same ESCAPE clause if used) so
admin exports/searches behave identically to user exports.
In `@web/src/i18n/locales/fr.json`:
- Line 1103: The French translation for the key "导出 CSV" is slightly unnatural;
update its value in the fr.json locale so the pair reads "导出 CSV": "Exporter en
CSV" (replace "Exporter CSV" with "Exporter en CSV") to match idiomatic French.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
controller/log.gomodel/log.gorouter/api-router.goweb/src/components/table/usage-logs/UsageLogsFilters.jsxweb/src/hooks/usage-logs/useUsageLogsData.jsxweb/src/i18n/locales/en.jsonweb/src/i18n/locales/fr.jsonweb/src/i18n/locales/ja.jsonweb/src/i18n/locales/ru.jsonweb/src/i18n/locales/vi.jsonweb/src/i18n/locales/zh-CN.jsonweb/src/i18n/locales/zh-TW.jsonweb/src/i18n/locales/zh.json
| _, _ = c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) | ||
|
|
||
| w := csv.NewWriter(c.Writer) | ||
|
|
||
| headers := []string{ | ||
| "ID", "时间", "类型", "用户名", "令牌名称", "模型名称", | ||
| "提示词Tokens", "补全Tokens", "消耗额度", "耗时(秒)", | ||
| "是否流式", "渠道ID", "渠道名称", "分组", "IP", "Request ID", "内容", | ||
| } | ||
| _ = w.Write(headers) | ||
|
|
||
| for _, log := range logs { | ||
| _ = w.Write([]string{ | ||
| strconv.Itoa(log.Id), | ||
| // Leading tab forces Excel to treat this cell as text, preventing | ||
| // the datetime auto-detection that causes "####" in narrow columns. | ||
| "\t" + time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"), | ||
| strconv.Itoa(log.Type), | ||
| log.Username, | ||
| log.TokenName, | ||
| log.ModelName, | ||
| strconv.Itoa(log.PromptTokens), | ||
| strconv.Itoa(log.CompletionTokens), | ||
| strconv.Itoa(log.Quota), | ||
| strconv.Itoa(log.UseTime), | ||
| strconv.FormatBool(log.IsStream), | ||
| strconv.Itoa(log.ChannelId), | ||
| log.ChannelName, | ||
| log.Group, | ||
| log.Ip, | ||
| log.RequestId, | ||
| log.Content, | ||
| }) | ||
| } | ||
|
|
||
| w.Flush() | ||
| } |
There was a problem hiding this comment.
Handle CSV write errors instead of discarding them.
Lines 253, 262, 265, and 288 currently ignore write/flush errors, which can silently produce incomplete downloads on client disconnect or stream failures.
🔍 Suggested fix
- _, _ = c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
+ if _, err := c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
+ _ = c.Error(err)
+ return
+ }
@@
- _ = w.Write(headers)
+ if err := w.Write(headers); err != nil {
+ _ = c.Error(err)
+ return
+ }
@@
- _ = w.Write([]string{
+ if err := w.Write([]string{
...
- })
+ }); err != nil {
+ _ = c.Error(err)
+ return
+ }
}
w.Flush()
+ if err := w.Error(); err != nil {
+ _ = c.Error(err)
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@controller/log.go` around lines 253 - 289, The CSV writer calls are currently
ignoring errors; update the export logic around w := csv.NewWriter(c.Writer) and
each w.Write(...) plus w.Flush() to check returned errors (from w.Write and
after w.Flush via w.Error() or the returned error), handle them by logging the
error and terminating the loop/returning an appropriate HTTP error or aborting
the response to avoid producing incomplete CSVs; ensure you handle the initial
c.Writer.Write(BOM) error as well and use the unique identifiers log loop,
w.Write calls, w.Flush(), and c.Writer to locate and implement these checks.
| for _, log := range logs { | ||
| _ = w.Write([]string{ | ||
| strconv.Itoa(log.Id), | ||
| // Leading tab forces Excel to treat this cell as text, preventing | ||
| // the datetime auto-detection that causes "####" in narrow columns. | ||
| "\t" + time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"), | ||
| strconv.Itoa(log.Type), | ||
| log.Username, | ||
| log.TokenName, | ||
| log.ModelName, | ||
| strconv.Itoa(log.PromptTokens), | ||
| strconv.Itoa(log.CompletionTokens), | ||
| strconv.Itoa(log.Quota), | ||
| strconv.Itoa(log.UseTime), | ||
| strconv.FormatBool(log.IsStream), | ||
| strconv.Itoa(log.ChannelId), | ||
| log.ChannelName, | ||
| log.Group, | ||
| log.Ip, | ||
| log.RequestId, | ||
| log.Content, | ||
| }) |
There was a problem hiding this comment.
Prevent CSV formula injection in exported fields.
Lines 271-285 write untrusted text cells directly. If a value starts with =, +, -, or @, opening the CSV in Excel-like tools can execute formulas.
🛡️ Suggested fix
func writeLogCSV(c *gin.Context, logs []*model.Log) {
@@
for _, log := range logs {
_ = w.Write([]string{
strconv.Itoa(log.Id),
@@
- log.Username,
- log.TokenName,
- log.ModelName,
+ sanitizeCSVCell(log.Username),
+ sanitizeCSVCell(log.TokenName),
+ sanitizeCSVCell(log.ModelName),
@@
- log.ChannelName,
- log.Group,
- log.Ip,
- log.RequestId,
- log.Content,
+ sanitizeCSVCell(log.ChannelName),
+ sanitizeCSVCell(log.Group),
+ sanitizeCSVCell(log.Ip),
+ sanitizeCSVCell(log.RequestId),
+ sanitizeCSVCell(log.Content),
})
}
@@
}
+
+func sanitizeCSVCell(v string) string {
+ if v == "" {
+ return v
+ }
+ switch v[0] {
+ case '=', '+', '-', '@':
+ return "'" + v
+ default:
+ return v
+ }
+}📝 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.
| for _, log := range logs { | |
| _ = w.Write([]string{ | |
| strconv.Itoa(log.Id), | |
| // Leading tab forces Excel to treat this cell as text, preventing | |
| // the datetime auto-detection that causes "####" in narrow columns. | |
| "\t" + time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"), | |
| strconv.Itoa(log.Type), | |
| log.Username, | |
| log.TokenName, | |
| log.ModelName, | |
| strconv.Itoa(log.PromptTokens), | |
| strconv.Itoa(log.CompletionTokens), | |
| strconv.Itoa(log.Quota), | |
| strconv.Itoa(log.UseTime), | |
| strconv.FormatBool(log.IsStream), | |
| strconv.Itoa(log.ChannelId), | |
| log.ChannelName, | |
| log.Group, | |
| log.Ip, | |
| log.RequestId, | |
| log.Content, | |
| }) | |
| for _, log := range logs { | |
| _ = w.Write([]string{ | |
| strconv.Itoa(log.Id), | |
| // Leading tab forces Excel to treat this cell as text, preventing | |
| // the datetime auto-detection that causes "####" in narrow columns. | |
| "\t" + time.Unix(log.CreatedAt, 0).Format("2006-01-02 15:04:05"), | |
| strconv.Itoa(log.Type), | |
| sanitizeCSVCell(log.Username), | |
| sanitizeCSVCell(log.TokenName), | |
| sanitizeCSVCell(log.ModelName), | |
| strconv.Itoa(log.PromptTokens), | |
| strconv.Itoa(log.CompletionTokens), | |
| strconv.Itoa(log.Quota), | |
| strconv.Itoa(log.UseTime), | |
| strconv.FormatBool(log.IsStream), | |
| strconv.Itoa(log.ChannelId), | |
| sanitizeCSVCell(log.ChannelName), | |
| sanitizeCSVCell(log.Group), | |
| sanitizeCSVCell(log.Ip), | |
| sanitizeCSVCell(log.RequestId), | |
| sanitizeCSVCell(log.Content), | |
| }) | |
| } | |
| } | |
| func sanitizeCSVCell(v string) string { | |
| if v == "" { | |
| return v | |
| } | |
| switch v[0] { | |
| case '=', '+', '-', '@': | |
| return "'" + v | |
| default: | |
| return v | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@controller/log.go` around lines 264 - 285, The CSV export writes untrusted
text fields directly (in the loop calling w.Write) which can trigger Excel
formula injection; add a helper function sanitizeCSVCell(s string) string that
returns the original s unless it begins with one of the dangerous prefixes ('=',
'+', '-', '@'), in which case it returns a safe-prefixed value (e.g., prepend a
single quote) and then call sanitizeCSVCell on all user-controlled fields used
in the Write call—specifically Username, TokenName, ModelName, ChannelName,
Group, Ip, RequestId, and Content—so those values are sanitized before being
passed to w.Write.
| func GetAllLogsForExport(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string, requestId string) ([]*Log, error) { | ||
| var tx *gorm.DB | ||
| if logType == LogTypeUnknown { | ||
| tx = LOG_DB | ||
| } else { | ||
| tx = LOG_DB.Where("logs.type = ?", logType) | ||
| } | ||
| if modelName != "" { | ||
| tx = tx.Where("logs.model_name like ?", modelName) | ||
| } | ||
| if username != "" { | ||
| tx = tx.Where("logs.username = ?", username) | ||
| } | ||
| if tokenName != "" { | ||
| tx = tx.Where("logs.token_name = ?", tokenName) | ||
| } | ||
| if requestId != "" { | ||
| tx = tx.Where("logs.request_id = ?", requestId) | ||
| } | ||
| if startTimestamp != 0 { | ||
| tx = tx.Where("logs.created_at >= ?", startTimestamp) | ||
| } | ||
| if endTimestamp != 0 { | ||
| tx = tx.Where("logs.created_at <= ?", endTimestamp) | ||
| } | ||
| if channel != 0 { | ||
| tx = tx.Where("logs.channel_id = ?", channel) | ||
| } | ||
| if group != "" { | ||
| tx = tx.Where("logs."+logGroupCol+" = ?", group) | ||
| } | ||
|
|
||
| var logs []*Log | ||
| err := tx.Order("logs.id desc").Limit(maxExportCount).Find(&logs).Error | ||
| return logs, err |
There was a problem hiding this comment.
ChannelName will be empty in admin CSV export output.
writeLogCSV outputs a "渠道名称" column, but GetAllLogsForExport (Lines 379-413) does not populate Log.ChannelName. This makes exported data incomplete.
💡 Suggested fix
func GetAllLogsForExport(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string, requestId string) ([]*Log, error) {
var tx *gorm.DB
@@
var logs []*Log
- err := tx.Order("logs.id desc").Limit(maxExportCount).Find(&logs).Error
+ err := tx.
+ Select("logs.*, channels.name AS channel_name").
+ Joins("LEFT JOIN channels ON channels.id = logs.channel_id").
+ Order("logs.id desc").
+ Limit(maxExportCount).
+ Find(&logs).Error
return logs, err
}📝 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.
| func GetAllLogsForExport(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string, requestId string) ([]*Log, error) { | |
| var tx *gorm.DB | |
| if logType == LogTypeUnknown { | |
| tx = LOG_DB | |
| } else { | |
| tx = LOG_DB.Where("logs.type = ?", logType) | |
| } | |
| if modelName != "" { | |
| tx = tx.Where("logs.model_name like ?", modelName) | |
| } | |
| if username != "" { | |
| tx = tx.Where("logs.username = ?", username) | |
| } | |
| if tokenName != "" { | |
| tx = tx.Where("logs.token_name = ?", tokenName) | |
| } | |
| if requestId != "" { | |
| tx = tx.Where("logs.request_id = ?", requestId) | |
| } | |
| if startTimestamp != 0 { | |
| tx = tx.Where("logs.created_at >= ?", startTimestamp) | |
| } | |
| if endTimestamp != 0 { | |
| tx = tx.Where("logs.created_at <= ?", endTimestamp) | |
| } | |
| if channel != 0 { | |
| tx = tx.Where("logs.channel_id = ?", channel) | |
| } | |
| if group != "" { | |
| tx = tx.Where("logs."+logGroupCol+" = ?", group) | |
| } | |
| var logs []*Log | |
| err := tx.Order("logs.id desc").Limit(maxExportCount).Find(&logs).Error | |
| return logs, err | |
| func GetAllLogsForExport(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string, requestId string) ([]*Log, error) { | |
| var tx *gorm.DB | |
| if logType == LogTypeUnknown { | |
| tx = LOG_DB | |
| } else { | |
| tx = LOG_DB.Where("logs.type = ?", logType) | |
| } | |
| if modelName != "" { | |
| tx = tx.Where("logs.model_name like ?", modelName) | |
| } | |
| if username != "" { | |
| tx = tx.Where("logs.username = ?", username) | |
| } | |
| if tokenName != "" { | |
| tx = tx.Where("logs.token_name = ?", tokenName) | |
| } | |
| if requestId != "" { | |
| tx = tx.Where("logs.request_id = ?", requestId) | |
| } | |
| if startTimestamp != 0 { | |
| tx = tx.Where("logs.created_at >= ?", startTimestamp) | |
| } | |
| if endTimestamp != 0 { | |
| tx = tx.Where("logs.created_at <= ?", endTimestamp) | |
| } | |
| if channel != 0 { | |
| tx = tx.Where("logs.channel_id = ?", channel) | |
| } | |
| if group != "" { | |
| tx = tx.Where("logs."+logGroupCol+" = ?", group) | |
| } | |
| var logs []*Log | |
| err := tx. | |
| Select("logs.*, channels.name AS channel_name"). | |
| Joins("LEFT JOIN channels ON channels.id = logs.channel_id"). | |
| Order("logs.id desc"). | |
| Limit(maxExportCount). | |
| Find(&logs).Error | |
| return logs, err | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@model/log.go` around lines 379 - 413, GetAllLogsForExport currently fetches
logs without loading ChannelName, so exported CSV shows an empty "渠道名称"; modify
GetAllLogsForExport to include the channel name by joining or preloading the
channel data: either add a LEFT JOIN on the channels table and select
channels.name AS channel_name (or the correct column) so GORM scans it into
Log.ChannelName, or Preload("Channel") and after Find iterate logs to set
Log.ChannelName = log.Channel.Name; update the tx query (in GetAllLogsForExport)
to include this join/select or the Preload call and ensure the selected alias
matches the Log.ChannelName field.
| } catch (err) { | ||
| showError(t('导出日志失败')); | ||
| } finally { |
There was a problem hiding this comment.
Do not collapse export failures to a generic string.
At Line 780, replacing the caught error with t('导出日志失败') drops existing structured error behavior (including status-based handling) and backend-provided details.
🛠️ Suggested fix
- } catch (err) {
- showError(t('导出日志失败'));
+ } catch (err) {
+ const blob = err?.response?.data;
+ if (blob instanceof Blob && blob.type?.includes('application/json')) {
+ try {
+ const text = await blob.text();
+ const json = JSON.parse(text);
+ showError(json.message || t('导出日志失败'));
+ return;
+ } catch (_) {
+ // ignore parse error
+ }
+ }
+ showError(err);
} finally {
setExportLoading(false);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (err) { | |
| showError(t('导出日志失败')); | |
| } finally { | |
| } catch (err) { | |
| const blob = err?.response?.data; | |
| if (blob instanceof Blob && blob.type?.includes('application/json')) { | |
| try { | |
| const text = await blob.text(); | |
| const json = JSON.parse(text); | |
| showError(json.message || t('导出日志失败')); | |
| return; | |
| } catch (_) { | |
| // ignore parse error | |
| } | |
| } | |
| showError(err); | |
| } finally { | |
| setExportLoading(false); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/hooks/usage-logs/useUsageLogsData.jsx` around lines 779 - 781, The
catch block in useUsageLogsData.jsx currently replaces the caught error with a
generic t('导出日志失败') string (catch (err) { showError(t('导出日志失败')); }), losing
structured error information; change the catch to pass the original error to
showError (e.g., showError(err)) or at minimum include err details
(showError(err?.message || t('导出日志失败'))) so status-based handling and
backend-provided details are preserved; update the catch in the export-related
function where showError and t are used.
| "导出 CSV": "Xuất CSV", | ||
| "导出日志失败": "Failed to export logs", | ||
| "导出配置": "Xuất cấu hình", |
There was a problem hiding this comment.
vi 导出提示存在中英混用,建议补齐越南语文案。
Line 1096 和 Line 1540 仍是英文,会导致导出流程提示在越南语界面下出现混合语言。
🌐 Suggested i18n fix
- "导出日志失败": "Failed to export logs",
+ "导出日志失败": "Xuất nhật ký thất bại",
...
- "日志导出成功": "Logs exported successfully",
+ "日志导出成功": "Xuất nhật ký thành công",Also applies to: 1540-1541
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/i18n/locales/vi.json` around lines 1095 - 1097, The translation for
export-related keys contains English text; update the i18n entries "导出日志失败"
(currently "Failed to export logs") and the other export keys at the same group
(also referenced around lines with keys for exporting at 1540-1541) to complete
Vietnamese translations so the export flow shows consistent Vietnamese copy;
locate the keys "导出 CSV", "导出日志失败", and "导出配置" in vi.json and replace the
English value(s) with appropriate Vietnamese strings (e.g., "导出日志失败" -> "Xuất
nhật ký thất bại") ensuring all export-related keys in that file are translated.
|
暂时不考虑增加对日志的这种批量导出功能,如果有需要可以自己连接数据库去导报表。 PR被关闭的原因是昨天进行了force push,Github这边自动操作的close,可以按照新的模版再提交一份,未来如果有考虑增加这个功能可能会看一下这个PR。 |
日志模块新增导出日志为 CSV 文件的功能,覆盖前后端全链路:
后端:新增两个 POST 接口,接收筛选条件 JSON,查询数据库后直接以 CSV 流响应,最大导出 10000 条防止内存溢出。
前端:在日志列表页操作栏新增「导出 CSV」按钮,携带当前所有筛选条件(时间、模型、令牌、渠道、分组、Request ID、日志类型等)发起请求,Blob 流触发浏览器下载,含 Loading 状态、成功提示、空数据提示和错误提示。
相关议题:#1778
Summary by CodeRabbit
Release Notes