feat(moderation): make moderation more lenient and add timeout fallback behavior
All checks were successful
Build Backend / build (push) Successful in 2m20s
Build Backend / build-docker (push) Successful in 1m7s

The moderation system now uses a "pass/review" strategy instead of "pass/review/block", treating hard violations the same as review (only routing to manual review rather than auto-rejecting). Add fallback-approve behavior in the hook system so slow moderation APIs never silently block normal content when they time out or get cancelled - timed-out moderation now approves with "ReviewedBy: system" instead of leaving the zero-value result that would reject content. Update post/comment/vote services and handlers to use the new unified behavior. Simplify moderation prompts to emphasize "宁可放过,不可误伤" (prefer false positives over false negatives).
This commit is contained in:
lafay
2026-07-08 01:47:48 +08:00
parent eb931bf1c6
commit a971fb0e29
9 changed files with 324 additions and 267 deletions

View File

@@ -2,6 +2,7 @@
import (
"context"
"errors"
"strings"
"time"
@@ -382,40 +383,69 @@ func (m *ModerationHooks) RegisterAll(manager *Manager) {
func (m *ModerationHooks) ModeratePost(ctx context.Context, manager *Manager, data *PostModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookPostPreModerate, data.AuthorID, data, map[string]any{
if err := manager.TriggerWithMetadata(ctx, HookPostPreModerate, data.AuthorID, data, map[string]any{
"result": result,
})
}); err != nil {
applyModerationFallback(result, err)
}
return result
}
func (m *ModerationHooks) ModerateComment(ctx context.Context, manager *Manager, data *CommentModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookCommentPreModerate, data.AuthorID, data, map[string]any{
if err := manager.TriggerWithMetadata(ctx, HookCommentPreModerate, data.AuthorID, data, map[string]any{
"result": result,
})
}); err != nil {
applyModerationFallback(result, err)
}
return result
}
func (m *ModerationHooks) ModerateAvatar(ctx context.Context, manager *Manager, data *UserAvatarModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookUserAvatarPreModerate, 0, data, map[string]any{
if err := manager.TriggerWithMetadata(ctx, HookUserAvatarPreModerate, 0, data, map[string]any{
"result": result,
})
}); err != nil {
applyModerationFallback(result, err)
}
return result
}
func (m *ModerationHooks) ModerateCover(ctx context.Context, manager *Manager, data *UserCoverModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookUserCoverPreModerate, 0, data, map[string]any{
if err := manager.TriggerWithMetadata(ctx, HookUserCoverPreModerate, 0, data, map[string]any{
"result": result,
})
}); err != nil {
applyModerationFallback(result, err)
}
return result
}
func (m *ModerationHooks) ModerateBio(ctx context.Context, manager *Manager, data *UserBioModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookUserBioPreModerate, 0, data, map[string]any{
if err := manager.TriggerWithMetadata(ctx, HookUserBioPreModerate, 0, data, map[string]any{
"result": result,
})
}); err != nil {
applyModerationFallback(result, err)
}
return result
}
// applyModerationFallback handles moderation hook timeouts/cancellations by
// falling back to "approved by system" instead of leaving the zero-value
// result (Approved=false) which would silently reject otherwise-fine content.
func applyModerationFallback(result *ModerationResult, err error) {
if err == nil {
return
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
result.Approved = true
result.ReviewedBy = "system"
result.RejectReason = ""
result.NeedsReview = false
result.Error = err
zap.L().Warn("Moderation hook timed out or was cancelled, fallback approve",
zap.Error(err),
)
}
}