Files
backend/internal/pkg/hook/moderation_hooks.go
lafay 639ee55696
All checks were successful
Build Backend / build (push) Successful in 3m57s
Build Backend / build-docker (push) Successful in 4m43s
feat(wire): add moderation and builtin hooks to post service wiring
Add wire providers for ModerationHooks and BuiltinHooks, injecting them
into PostService via dependency injection. Hooks are passed as blank
identifiers to trigger their initialization and registration side effects.
2026-03-21 02:53:38 +08:00

362 lines
9.1 KiB
Go

package hook
import (
"context"
"strings"
"time"
"carrot_bbs/internal/repository"
"go.uber.org/zap"
)
type PostModerationHook struct {
name string
postAIService PostAIService
postRepo *repository.PostRepository
strictMode bool
}
type PostAIService interface {
IsEnabled() bool
ModeratePost(ctx context.Context, title, content string, images []string) error
ModerateComment(ctx context.Context, content string, images []string) error
}
func NewPostModerationHook(postAIService PostAIService, postRepo *repository.PostRepository, strictMode bool) *PostModerationHook {
return &PostModerationHook{
name: "moderation.post.ai",
postAIService: postAIService,
postRepo: postRepo,
strictMode: strictMode,
}
}
func (h *PostModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookPostPreModerate,
Priority: PriorityHighest,
Func: h.Execute,
Async: false, // 必须同步执行,因为调用方需要审核结果
Timeout: 30 * time.Second,
})
}
func (h *PostModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*PostModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
zap.L().Debug("AI moderation disabled, auto approve post",
zap.String("post_id", data.PostID),
)
return nil
}
err := h.postAIService.ModeratePost(ctx.Ctx, data.Title, data.Content, data.Images)
if err != nil {
if rejectErr, ok := err.(interface{ UserMessage() string }); ok {
result.Approved = false
result.RejectReason = rejectErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("Post rejected by AI moderation",
zap.String("post_id", data.PostID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
zap.L().Error("AI post moderation failed in strict mode, reject post",
zap.String("post_id", data.PostID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI moderation error, fallback approve",
zap.String("post_id", data.PostID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
zap.L().Debug("Post approved by AI moderation",
zap.String("post_id", data.PostID),
)
return nil
}
type CommentModerationHook struct {
name string
postAIService PostAIService
commentRepo *repository.CommentRepository
strictMode bool
}
func NewCommentModerationHook(postAIService PostAIService, commentRepo *repository.CommentRepository, strictMode bool) *CommentModerationHook {
return &CommentModerationHook{
name: "moderation.comment.ai",
postAIService: postAIService,
commentRepo: commentRepo,
strictMode: strictMode,
}
}
func (h *CommentModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookCommentPreModerate,
Priority: PriorityHighest,
Func: h.Execute,
Async: false, // 必须同步执行,因为调用方需要审核结果
Timeout: 30 * time.Second,
})
}
func (h *CommentModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*CommentModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
zap.L().Debug("AI moderation disabled, auto approve comment",
zap.String("comment_id", data.CommentID),
)
return nil
}
err := h.postAIService.ModerateComment(ctx.Ctx, data.Content, data.Images)
if err != nil {
if rejectErr, ok := err.(interface{ UserMessage() string }); ok {
result.Approved = false
result.RejectReason = rejectErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("Comment rejected by AI moderation",
zap.String("comment_id", data.CommentID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
zap.L().Error("AI comment moderation failed in strict mode, reject comment",
zap.String("comment_id", data.CommentID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI comment moderation error, fallback approve",
zap.String("comment_id", data.CommentID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
zap.L().Debug("Comment approved by AI moderation",
zap.String("comment_id", data.CommentID),
)
return nil
}
type SensitiveWordHook struct {
name string
sensitive SensitiveService
replaceStr string
}
type SensitiveService interface {
Check(ctx context.Context, text string) (bool, []string)
Replace(ctx context.Context, text string, repl string) string
}
func NewSensitiveWordHook(sensitive SensitiveService, replaceStr string) *SensitiveWordHook {
return &SensitiveWordHook{
name: "moderation.sensitive_word",
sensitive: sensitive,
replaceStr: replaceStr,
}
}
func (h *SensitiveWordHook) RegisterPostHook(manager *Manager) {
manager.Register(&Hook{
Name: h.name + ".post",
HookType: HookPostPreModerate,
Priority: PriorityHigh,
Func: h.executePost,
Async: false,
})
}
func (h *SensitiveWordHook) RegisterCommentHook(manager *Manager) {
manager.Register(&Hook{
Name: h.name + ".comment",
HookType: HookCommentPreModerate,
Priority: PriorityHigh,
Func: h.executeComment,
Async: false,
})
}
func (h *SensitiveWordHook) executePost(ctx *HookContext) error {
if h.sensitive == nil {
return nil
}
data, ok := ctx.Data.(*PostModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if result.RejectReason != "" {
return nil
}
hasSensitive, words := h.sensitive.Check(ctx.Ctx, data.Title+" "+data.Content)
if hasSensitive {
result.Approved = false
result.RejectReason = "内容包含敏感词: " + strings.Join(words, ", ")
result.ReviewedBy = "sensitive_word"
zap.L().Info("Post rejected by sensitive word filter",
zap.String("post_id", data.PostID),
zap.Strings("words", words),
)
}
return nil
}
func (h *SensitiveWordHook) executeComment(ctx *HookContext) error {
if h.sensitive == nil {
return nil
}
data, ok := ctx.Data.(*CommentModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if result.RejectReason != "" {
return nil
}
hasSensitive, words := h.sensitive.Check(ctx.Ctx, data.Content)
if hasSensitive {
result.Approved = false
result.RejectReason = "内容包含敏感词: " + strings.Join(words, ", ")
result.ReviewedBy = "sensitive_word"
zap.L().Info("Comment rejected by sensitive word filter",
zap.String("comment_id", data.CommentID),
zap.Strings("words", words),
)
}
return nil
}
type ModerationHooks struct {
postAIService PostAIService
sensitiveService SensitiveService
postRepo *repository.PostRepository
commentRepo *repository.CommentRepository
strictMode bool
replaceStr string
}
func NewModerationHooks(
postAIService PostAIService,
sensitiveService SensitiveService,
postRepo *repository.PostRepository,
commentRepo *repository.CommentRepository,
strictMode bool,
replaceStr string,
) *ModerationHooks {
return &ModerationHooks{
postAIService: postAIService,
sensitiveService: sensitiveService,
postRepo: postRepo,
commentRepo: commentRepo,
strictMode: strictMode,
replaceStr: replaceStr,
}
}
func (m *ModerationHooks) RegisterAll(manager *Manager) {
postHook := NewPostModerationHook(m.postAIService, m.postRepo, m.strictMode)
postHook.Register(manager)
commentHook := NewCommentModerationHook(m.postAIService, m.commentRepo, m.strictMode)
commentHook.Register(manager)
if m.sensitiveService != nil {
sensitiveHook := NewSensitiveWordHook(m.sensitiveService, m.replaceStr)
sensitiveHook.RegisterPostHook(manager)
sensitiveHook.RegisterCommentHook(manager)
}
zap.L().Info("Registered moderation hooks",
zap.Bool("ai_enabled", m.postAIService != nil && m.postAIService.IsEnabled()),
zap.Bool("sensitive_enabled", m.sensitiveService != nil),
zap.Bool("strict_mode", m.strictMode),
)
}
func (m *ModerationHooks) ModeratePost(ctx context.Context, manager *Manager, data *PostModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookPostPreModerate, data.AuthorID, data, map[string]any{
"result": result,
})
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{
"result": result,
})
return result
}