mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +00:00
Community Management Phase 1 completion: - Add Community Inbox admin view with filters, stats, and reply functionality - Add Rules Engine service for automated interaction processing - Add YouTube OAuth flow (auth, callback, token refresh) - Add Comment Sync cron job (every 15 minutes) - Add Community Export API (PDF/Excel/CSV) - Fix database schema for community_rules hasMany fields - Fix access control in communityAccess.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
325 lines
8.8 KiB
TypeScript
325 lines
8.8 KiB
TypeScript
/**
|
|
* Rules Engine
|
|
*
|
|
* Wertet Community-Regeln aus und wendet automatische Aktionen an.
|
|
* Wird beim Import neuer Interaktionen aufgerufen.
|
|
*/
|
|
|
|
import type { Payload } from 'payload'
|
|
|
|
interface RuleAction {
|
|
action: string
|
|
value?: string
|
|
targetUser?: number | { id: number }
|
|
targetTemplate?: number | { id: number }
|
|
}
|
|
|
|
interface RuleTrigger {
|
|
type: string
|
|
keywords?: { keyword: string; matchType: string }[]
|
|
sentimentValues?: string[]
|
|
influencerMinFollowers?: number
|
|
}
|
|
|
|
interface Rule {
|
|
id: number
|
|
name: string
|
|
priority: number
|
|
isActive: boolean
|
|
platforms?: Array<{ id: number } | number>
|
|
channel?: { id: number } | number
|
|
trigger: RuleTrigger
|
|
actions: RuleAction[]
|
|
stats?: {
|
|
timesTriggered?: number
|
|
lastTriggeredAt?: string
|
|
}
|
|
}
|
|
|
|
interface Interaction {
|
|
id: number
|
|
platform?: { id: number } | number
|
|
message?: string
|
|
author?: {
|
|
name?: string
|
|
subscriberCount?: number
|
|
isSubscriber?: boolean
|
|
}
|
|
analysis?: {
|
|
sentiment?: string
|
|
suggestedTemplate?: number
|
|
}
|
|
flags?: {
|
|
isMedicalQuestion?: boolean
|
|
requiresEscalation?: boolean
|
|
isSpam?: boolean
|
|
isFromInfluencer?: boolean
|
|
}
|
|
priority?: string
|
|
linkedContent?: { id: number } | number
|
|
}
|
|
|
|
interface EvaluationResult {
|
|
appliedRules: string[]
|
|
changes: Record<string, unknown>
|
|
}
|
|
|
|
export class RulesEngine {
|
|
private payload: Payload
|
|
|
|
constructor(payload: Payload) {
|
|
this.payload = payload
|
|
}
|
|
|
|
/**
|
|
* Wertet alle aktiven Regeln für eine Interaktion aus
|
|
*/
|
|
async evaluateRules(interactionId: number): Promise<EvaluationResult> {
|
|
const appliedRules: string[] = []
|
|
const changes: Record<string, unknown> = {}
|
|
|
|
// Interaktion laden
|
|
const interaction = (await this.payload.findByID({
|
|
collection: 'community-interactions',
|
|
id: interactionId,
|
|
depth: 2,
|
|
})) as Interaction | null
|
|
|
|
if (!interaction) {
|
|
throw new Error('Interaction not found')
|
|
}
|
|
|
|
// Aktive Regeln nach Priorität sortiert laden
|
|
const rules = await this.payload.find({
|
|
collection: 'community-rules',
|
|
where: {
|
|
isActive: { equals: true },
|
|
},
|
|
sort: 'priority',
|
|
limit: 100,
|
|
})
|
|
|
|
for (const doc of rules.docs) {
|
|
const rule = doc as unknown as Rule
|
|
|
|
// Platform-Filter prüfen
|
|
if (rule.platforms && rule.platforms.length > 0) {
|
|
const platformIds = rule.platforms.map((p) => (typeof p === 'object' ? p.id : p))
|
|
const interactionPlatformId =
|
|
typeof interaction.platform === 'object' ? interaction.platform.id : interaction.platform
|
|
|
|
if (!platformIds.includes(interactionPlatformId as number)) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Trigger prüfen
|
|
const triggered = await this.checkTrigger(rule, interaction)
|
|
|
|
if (triggered) {
|
|
// Aktionen anwenden
|
|
const ruleChanges = await this.applyActions(rule, interaction)
|
|
Object.assign(changes, ruleChanges)
|
|
appliedRules.push(rule.name)
|
|
|
|
// Stats aktualisieren
|
|
await this.payload.update({
|
|
collection: 'community-rules',
|
|
id: rule.id,
|
|
data: {
|
|
stats: {
|
|
timesTriggered: (rule.stats?.timesTriggered || 0) + 1,
|
|
lastTriggeredAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// Änderungen an der Interaktion speichern
|
|
if (Object.keys(changes).length > 0) {
|
|
await this.payload.update({
|
|
collection: 'community-interactions',
|
|
id: interactionId,
|
|
data: changes,
|
|
})
|
|
}
|
|
|
|
return { appliedRules, changes }
|
|
}
|
|
|
|
/**
|
|
* Prüft ob ein Trigger zutrifft
|
|
*/
|
|
private async checkTrigger(rule: Rule, interaction: Interaction): Promise<boolean> {
|
|
const { trigger } = rule
|
|
const message = interaction.message?.toLowerCase() || ''
|
|
|
|
switch (trigger.type) {
|
|
case 'keyword':
|
|
if (!trigger.keywords?.length) return false
|
|
return trigger.keywords.some(({ keyword, matchType }) => {
|
|
const kw = keyword.toLowerCase()
|
|
switch (matchType) {
|
|
case 'exact':
|
|
return message === kw
|
|
case 'regex':
|
|
try {
|
|
return new RegExp(keyword, 'i').test(interaction.message || '')
|
|
} catch {
|
|
return false
|
|
}
|
|
case 'contains':
|
|
default:
|
|
return message.includes(kw)
|
|
}
|
|
})
|
|
|
|
case 'sentiment':
|
|
if (!trigger.sentimentValues?.length) return false
|
|
return trigger.sentimentValues.includes(interaction.analysis?.sentiment || '')
|
|
|
|
case 'influencer':
|
|
const minFollowers = trigger.influencerMinFollowers || 10000
|
|
return (interaction.author?.subscriberCount || 0) >= minFollowers
|
|
|
|
case 'medical':
|
|
return interaction.flags?.isMedicalQuestion === true
|
|
|
|
case 'new_subscriber':
|
|
return interaction.author?.isSubscriber === true
|
|
|
|
case 'negative_sentiment':
|
|
return interaction.analysis?.sentiment === 'negative'
|
|
|
|
case 'positive_sentiment':
|
|
return interaction.analysis?.sentiment === 'positive'
|
|
|
|
case 'all':
|
|
// Immer auslösen (für Default-Regeln)
|
|
return true
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wendet die Aktionen einer Regel an
|
|
*/
|
|
private async applyActions(
|
|
rule: Rule,
|
|
interaction: Interaction
|
|
): Promise<Record<string, unknown>> {
|
|
const changes: Record<string, unknown> = {}
|
|
|
|
for (const action of rule.actions) {
|
|
switch (action.action) {
|
|
case 'set_priority':
|
|
if (action.value) {
|
|
changes.priority = action.value
|
|
}
|
|
break
|
|
|
|
case 'assign_to':
|
|
const userId = typeof action.targetUser === 'object' ? action.targetUser.id : action.targetUser
|
|
if (userId) {
|
|
changes.assignedTo = userId
|
|
}
|
|
break
|
|
|
|
case 'apply_template':
|
|
const templateId =
|
|
typeof action.targetTemplate === 'object' ? action.targetTemplate.id : action.targetTemplate
|
|
if (templateId) {
|
|
// Nested update für analysis.suggestedTemplate
|
|
const currentAnalysis = interaction.analysis || {}
|
|
changes.analysis = {
|
|
...currentAnalysis,
|
|
suggestedTemplate: templateId,
|
|
}
|
|
}
|
|
break
|
|
|
|
case 'flag':
|
|
// Flags werden als separate Objekte aktualisiert
|
|
const currentFlags = interaction.flags || {}
|
|
if (action.value === 'medical') {
|
|
changes.flags = { ...currentFlags, isMedicalQuestion: true }
|
|
} else if (action.value === 'escalation') {
|
|
changes.flags = { ...currentFlags, requiresEscalation: true }
|
|
} else if (action.value === 'spam') {
|
|
changes.flags = { ...currentFlags, isSpam: true }
|
|
} else if (action.value === 'influencer') {
|
|
changes.flags = { ...currentFlags, isFromInfluencer: true }
|
|
}
|
|
break
|
|
|
|
case 'set_status':
|
|
if (action.value) {
|
|
changes.status = action.value
|
|
}
|
|
break
|
|
|
|
case 'notify':
|
|
await this.sendNotification(action, interaction, rule)
|
|
break
|
|
|
|
case 'auto_reply':
|
|
// Auto-Reply könnte hier implementiert werden
|
|
// Für jetzt nur loggen
|
|
console.log(`[RulesEngine] Auto-reply triggered for interaction ${interaction.id}`)
|
|
break
|
|
}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
/**
|
|
* Sendet eine Benachrichtigung
|
|
*/
|
|
private async sendNotification(
|
|
action: RuleAction,
|
|
interaction: Interaction,
|
|
rule: Rule
|
|
): Promise<void> {
|
|
const userId = typeof action.targetUser === 'object' ? action.targetUser.id : action.targetUser
|
|
|
|
if (!userId) return
|
|
|
|
try {
|
|
// Prüfen ob yt-notifications Collection existiert
|
|
await this.payload.create({
|
|
collection: 'yt-notifications',
|
|
data: {
|
|
user: userId,
|
|
type: 'community_rule_triggered',
|
|
title: `🔔 Rule "${rule.name}" triggered`,
|
|
message: `New interaction from ${interaction.author?.name || 'Unknown'}: "${(interaction.message || '').substring(0, 100)}..."`,
|
|
relatedContent:
|
|
typeof interaction.linkedContent === 'object'
|
|
? interaction.linkedContent.id
|
|
: interaction.linkedContent,
|
|
priority: interaction.priority === 'urgent' ? 'urgent' : 'normal',
|
|
isRead: false,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
// Collection existiert möglicherweise nicht - nur loggen, nicht crashen
|
|
console.warn('[RulesEngine] Failed to create notification:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton-Instanz für einfachen Zugriff
|
|
*/
|
|
let rulesEngineInstance: RulesEngine | null = null
|
|
|
|
export function getRulesEngine(payload: Payload): RulesEngine {
|
|
if (!rulesEngineInstance) {
|
|
rulesEngineInstance = new RulesEngine(payload)
|
|
}
|
|
return rulesEngineInstance
|
|
}
|