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.
362 lines
9.1 KiB
Go
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
|
|
}
|