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 }