Skip to content

Comments

feat: 自定义错误替换规则(数据库管理)#2931

Open
TAYOTOoO wants to merge 1 commit intoQuantumNous:mainfrom
TAYOTOoO:feature/custom-error-rules
Open

feat: 自定义错误替换规则(数据库管理)#2931
TAYOTOoO wants to merge 1 commit intoQuantumNous:mainfrom
TAYOTOoO:feature/custom-error-rules

Conversation

@TAYOTOoO
Copy link

@TAYOTOoO TAYOTOoO commented Feb 12, 2026

支持通过后台管理界面增删改查错误消息替换规则,取代硬编码方式。

  • 新增 CustomErrorRule 模型,带内存缓存和自动刷新
  • 新增 CRUD 接口 /api/custom_error_rule/(管理员权限)
  • common/str.go 新增 CustomErrorRulesProvider 和 ReplaceCustomErrorMessage
  • types/error.go 在 ToOpenAIError/ToClaudeError 中调用替换逻辑
  • model/main.go 启动时初始化缓存并注册 Provider
  • 首次启动时自动插入一条示例规则
  • 新增前端管理页面 SettingsCustomErrorRules.jsx

Summary by CodeRabbit

  • New Features
    • Administrators can define priority-based custom error message replacements (match text, optional status code, replacement message, enabled/disabled).
    • New admin UI to list, create, edit, toggle, and delete custom error rules with immediate effect.
    • Error messages shown to users are now automatically transformed according to configured rules.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a configurable error-message replacement system: new GORM model and migration, thread-safe cached rules with CRUD API and admin UI, startup provider registration, and runtime integration that rewrites API error messages via rule matching.

Changes

Cohort / File(s) Summary
Core Replacement Mechanism
common/str.go
Adds CustomErrorReplacement type, CustomErrorRulesProvider hook, and ReplaceCustomErrorMessage(statusCode int, message string) string.
Database Model & Cache Layer
model/custom_error_rule.go, model/main.go
Adds CustomErrorRule GORM model, thread-safe lazy-initialized cache with refresh, CRUD functions, seeding, migrates model in AutoMigrate, and registers common.CustomErrorRulesProvider to return cached replacements.
HTTP Controller & Router
controller/custom_error_rule.go, router/api-router.go
New Gin handlers GetCustomErrorRules, CreateCustomErrorRule, UpdateCustomErrorRule, DeleteCustomErrorRule; routes mounted under /api/custom_error_rule/ with RootAuth; handlers use uniform JSON payloads.
Error Pipeline Integration
types/error.go
Applies ReplaceCustomErrorMessage(e.StatusCode, result.Message) in ToOpenAIError and ToClaudeError to transform masked error messages before returning them.
Administrative UI
web/src/pages/Setting/Operation/SettingsCustomErrorRules.jsx
New React page to list, create, edit, toggle, prioritize, and delete rules via the new API; includes table, SideSheet form, loading states, and i18n strings.

Sequence Diagram(s)

sequenceDiagram
    participant AdminUI as "Admin UI"
    participant API as "Server API (Gin)"
    participant Model as "Model / DB"
    participant Cache as "Rules Cache"
    participant Common as "common.ReplaceLogic"
    participant Client as "Client / Error Path"

    AdminUI->>API: GET /api/custom_error_rule/
    API->>Model: GetAllCustomErrorRules()
    Model-->>API: rules
    API-->>AdminUI: JSON list

    AdminUI->>API: POST/PUT/DELETE /api/custom_error_rule/
    API->>Model: Create/Update/Delete rule
    Model->>Cache: refreshCustomErrorRulesCache()
    Cache-->>Common: provider returns updated replacements

    Client->>Common: ReplaceCustomErrorMessage(statusCode, message)
    Common-->>Client: replacedMessage
    Client-->>Consumer: final transformed error message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through rules beneath a code-tree bright,
I cached each match by day and by night,
Admins tune the lines, priorities set,
Messages softened and neatly reset,
A carrot for clarity — hop, delight! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title in Chinese (feat: 自定义错误替换规则(数据库管理)) accurately captures the main objective: adding database-managed custom error replacement rules as an alternative to hardcoding.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
model/main.go (1)

296-376: ⚠️ Potential issue | 🔴 Critical

Move cache initialization from dead code function to the active migration path.

The InitCustomErrorRulesCache() and CustomErrorRulesProvider registration (lines 363–375) are located in migrateDBFast(), which is never called—InitDB() calls only migrateDB() (line 205). As a result, the CustomErrorRule table is created (line 279), but the cache is never initialized at startup, leaving ReplaceCustomErrorMessage() non-functional.

Move the cache initialization block (lines 363–375) from migrateDBFast() into migrateDB() after the AutoMigrate calls, or remove the dead migrateDBFast() function entirely if it is not planned for use.

🤖 Fix all issues with AI agents
In `@common/str.go`:
- Around line 192-206: ReplaceCustomErrorMessage currently treats rule.Contains
== "" as a match because strings.Contains(message, "") is always true; update
the function (which calls CustomErrorRulesProvider and iterates rules) to skip
any rule whose rule.Contains is empty (or only whitespace) before calling
strings.Contains, preserving existing statusCode checks and returning
rule.NewMessage only for non-empty Contains matches.

In `@controller/custom_error_rule.go`:
- Around line 56-83: UpdateCustomErrorRule is missing validation for the
CustomErrorRule.Contains field, allowing an admin to set it to empty which would
match every message; add the same non-empty check used in CreateCustomErrorRule
to reject rule.Contains == "" (or trimmed empty) and return a JSON error
response (e.g., "Contains不能为空") before calling model.UpdateCustomErrorRule,
referencing UpdateCustomErrorRule, model.CustomErrorRule and
CreateCustomErrorRule to mirror behavior.

In `@model/custom_error_rule.go`:
- Around line 113-122: The count check in InitCustomErrorRulesCache uses
DB.Model(&CustomErrorRule{}).Count(&count) which ignores soft-deleted rows and
causes reseeding when all rows are soft-deleted; change the count query to
include soft-deleted records by using
DB.Unscoped().Model(&CustomErrorRule{}).Count(&count) so seedDefaultErrorRules()
only runs when the physical table is empty, keeping calls to
seedDefaultErrorRules() and refreshCustomErrorRulesCache() intact.
- Around line 11-21: The Enabled field on the CustomErrorRule struct uses a
numeric default (`default:1`); change its GORM tag to use a boolean literal
(`default:true`) to match other models and ensure cross-database
compatibility—update the struct field `Enabled bool` tag in CustomErrorRule
(reference symbol: CustomErrorRule.Enabled) to `gorm:"not null;default:true"`.

In `@web/src/pages/Setting/Operation/SettingsCustomErrorRules.jsx`:
- Around line 244-326: The SideSheet keeps children mounted so the Form's
initValues (in the Form component) aren't reapplied when switching editing
targets; to fix, force the Form to remount by adding a key that changes with the
editing target (e.g., key={editingRule?.id ?? 'new'} on the Form) or enable
SideSheet's destroy-on-close behavior (destroyOnClose) and also clear formRef
(formRef.current = null) when opening a different rule so the Form always
initializes from initValues for the current editingRule.
🧹 Nitpick comments (3)
model/main.go (1)

362-375: Provider closure allocates a new slice on every call — hot path concern.

CustomErrorRulesProvider is invoked on every error response (via ReplaceCustomErrorMessage in ToOpenAIError/ToClaudeError). The closure allocates a new []common.CustomErrorReplacement slice each time, copying every cached rule. Since the cache is only refreshed on CRUD operations, consider caching the []common.CustomErrorReplacement representation itself to avoid per-request allocations.

♻️ Suggested approach: cache the mapped slice

Store the []common.CustomErrorReplacement alongside the []CustomErrorRule cache, rebuilding it only in refreshCustomErrorRulesCache. The provider then just returns the pre-built slice:

 common.CustomErrorRulesProvider = func() []common.CustomErrorReplacement {
-	rules := GetCachedCustomErrorRules()
-	result := make([]common.CustomErrorReplacement, len(rules))
-	for i, r := range rules {
-		result[i] = common.CustomErrorReplacement{
-			Contains:   r.Contains,
-			StatusCode: r.StatusCode,
-			NewMessage: r.NewMessage,
-		}
-	}
-	return result
+	return GetCachedCustomErrorReplacements()
 }

Where GetCachedCustomErrorReplacements() returns a pre-built []common.CustomErrorReplacement that is updated atomically alongside the rule cache in refreshCustomErrorRulesCache.

model/custom_error_rule.go (1)

34-50: Benign thundering-herd on uninitialized cache.

Between the RUnlock() on Line 42 and refreshCustomErrorRulesCache() on Line 45, multiple concurrent goroutines can all observe !customErrorRulesCacheInit and trigger redundant DB queries. This is not a correctness issue since refreshCustomErrorRulesCache serializes via WLock, but it could cause a brief burst of identical queries on first access under load.

web/src/pages/Setting/Operation/SettingsCustomErrorRules.jsx (1)

82-96: Toggle sends the full rule object back — acceptable but fragile.

handleToggleEnabled spreads the entire rule object (including server-managed fields like created_time, updated_time) into the PUT body. This works because the backend's UpdateCustomErrorRule uses an explicit field map in Updates, so extra fields are ignored. Just noting for awareness.

@TAYOTOoO TAYOTOoO force-pushed the feature/custom-error-rules branch 2 times, most recently from 8cfd554 to e71a4d4 Compare February 12, 2026 11:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@model/main.go`:
- Line 279: migrateDBFast runs concurrent migrations but omits
&CustomErrorRule{}, so the table may not be created when using the fast path;
update the migrations slice in migrateDBFast to include &CustomErrorRule{} (same
as in migrateDB) so both migration paths create the same set of models—locate
the migrations list inside the migrateDBFast function and add &CustomErrorRule{}
to that slice.
🧹 Nitpick comments (1)
web/src/pages/Setting/Operation/SettingsCustomErrorRules.jsx (1)

108-137: Shared loading state causes table spinner during form submission.

handleSubmit sets setLoading(true) (Line 109), which also triggers the <Spin spinning={loading}> wrapping the entire table (Line 214). Consider using a separate submitting state for the form to avoid the table showing a loading overlay while submitting.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@controller/custom_error_rule.go`:
- Around line 75-85: The update path currently calls
model.UpdateCustomErrorRule(&rule) but GORM's Updates() can succeed with zero
rows affected; change the logic so non-existent IDs are detected: either (A)
modify model.UpdateCustomErrorRule to inspect the GORM result.RowsAffected and
return an error (e.g., gorm.ErrRecordNotFound or a custom error) when
RowsAffected == 0, or (B) fetch the rule first (e.g.,
model.GetCustomErrorRuleByID or similar) and return a not-found error before
calling model.UpdateCustomErrorRule; then update the controller code that calls
model.UpdateCustomErrorRule to treat that not-found error as a failure and avoid
triggering the cache refresh when the rule doesn't exist.

In `@model/custom_error_rule.go`:
- Around line 128-139: The seeding function seedDefaultErrorRules currently
ignores the result of DB.Create(&sample); capture the returned result (e.g., res
:= DB.Create(&sample)) and check res.Error; if non-nil, log the error with
contextual info (including the sample data) and return the error up to the
caller (change seedDefaultErrorRules to return error) so
refreshCustomErrorRulesCache can handle failures instead of proceeding with an
empty cache; update callers of seedDefaultErrorRules (e.g.,
refreshCustomErrorRulesCache) to handle the returned error and act accordingly.
🧹 Nitpick comments (1)
model/custom_error_rule.go (1)

38-53: Lazy init has a benign thundering-herd: multiple goroutines may call refreshCustomErrorRulesCache concurrently on first access.

Between the RUnlock on line 45 and the refreshCustomErrorRulesCache call on line 48, other goroutines can also observe !customErrorRulesCacheInit and trigger redundant DB queries. This is safe (each refresh atomically replaces the cache), but wasteful under high concurrency at startup.

A sync.Once would be a cleaner approach for the lazy init path:

Suggested refactor
 var (
 	customErrorRulesCache     []CustomErrorRule
 	customErrorRulesCacheLock sync.RWMutex
-	customErrorRulesCacheInit bool
+	customErrorRulesCacheOnce sync.Once
 )

 func GetCachedCustomErrorRules() []CustomErrorRule {
+	customErrorRulesCacheOnce.Do(func() {
+		refreshCustomErrorRulesCache()
+	})
 	customErrorRulesCacheLock.RLock()
-	if customErrorRulesCacheInit {
-		rules := customErrorRulesCache
-		customErrorRulesCacheLock.RUnlock()
-		return rules
-	}
-	customErrorRulesCacheLock.RUnlock()
-
-	// Initialize cache
-	refreshCustomErrorRulesCache()
-
-	customErrorRulesCacheLock.RLock()
 	defer customErrorRulesCacheLock.RUnlock()
 	return customErrorRulesCache
 }

Note: refreshCustomErrorRulesCache is also called from CRUD operations, so the RWMutex is still needed. sync.Once only guards the lazy-init path.

@TAYOTOoO TAYOTOoO force-pushed the feature/custom-error-rules branch from e71a4d4 to bbdc293 Compare February 12, 2026 11:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@model/custom_error_rule.go`:
- Around line 20-21: The gorm struct tags on CreatedTime and UpdatedTime use
gorm:"bigint" which GORM v2 ignores; update the struct field tags for
CreatedTime and UpdatedTime to use the explicit type key (gorm:"type:bigint") so
GORM recognizes the column type; locate the CreatedTime and UpdatedTime fields
in the struct and replace their gorm tag values accordingly.
🧹 Nitpick comments (3)
web/src/pages/Setting/Operation/SettingsCustomErrorRules.jsx (3)

43-43: Shared loading state causes the table spinner to activate during form submission.

A single loading state controls both the <Spin> wrapping the table (Line 214) and the submit button's loading prop (Line 320). Submitting the form will overlay the table with a spinner unnecessarily. Consider a separate submitting state for the form.

Proposed fix
  const [loading, setLoading] = useState(false);
+ const [submitting, setSubmitting] = useState(false);

In handleSubmit:

  const handleSubmit = async (values) => {
-   setLoading(true);
+   setSubmitting(true);
    ...
-   setLoading(false);
+   setSubmitting(false);
  };

On the submit button:

- loading={loading}
+ loading={submitting}

Also applies to: 108-137


64-66: Missing dependency in useEffect.

loadRules is not listed in the dependency array, which will trigger an ESLint react-hooks/exhaustive-deps warning. Since loadRules is stable in behavior (only depends on state setters and t), the simplest fix is to either wrap loadRules in useCallback or inline the fetch inside the effect.


82-96: Toggle sends all server-side fields back, including soft-delete metadata.

...rule spreads every property from the API response (including created_time, deleted_at, etc.) into the PUT payload. This works today because the backend's UpdateCustomErrorRule uses a field map, but it couples the frontend to the exact shape of the response and sends unnecessary data. Consider sending only the fields the update endpoint needs.

支持通过后台管理界面增删改查错误消息替换规则,取代硬编码方式。

- 新增 CustomErrorRule 模型,带内存缓存和自动刷新
- 新增 CRUD 接口 /api/custom_error_rule/(管理员权限)
- common/str.go 新增 CustomErrorRulesProvider 和 ReplaceCustomErrorMessage
- types/error.go 在 ToOpenAIError/ToClaudeError 中调用替换逻辑
- model/main.go 启动时初始化缓存并注册 Provider
- 首次启动时自动插入一条示例规则
- 新增前端管理页面 SettingsCustomErrorRules.jsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@TAYOTOoO TAYOTOoO force-pushed the feature/custom-error-rules branch from bbdc293 to f3451b3 Compare February 12, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant