feat: add hook system, QR code login, and layered cache
- Implement extensible hook system for content moderation with builtin and AI-powered moderation hooks - Add QR code login feature with SSE for real-time status updates and scan/confirm/cancel workflow - Introduce layered cache with local LRU + Redis backend for improved read performance - Refactor post and comment services to use hook-based moderation instead of direct AI service calls - Add local cache configuration options (size, buckets, ttl)
This commit is contained in:
353
internal/pkg/hook/moderation_hooks.go
Normal file
353
internal/pkg/hook/moderation_hooks.go
Normal file
@@ -0,0 +1,353 @@
|
||||
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"
|
||||
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"
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user