feat(ai-moderation): enrich analysis context with recency, repetition, user history and channel topic

- <message> targets now carry time (ISO), repetitions (N identical short texts = spam signal), bot and edited flags; escape id/user XML
- rich <user_reputation>: total_infractions, clean_streak, last_offense_days_ago, repeat_offender (7-day window)
- <user_history> with last flagged messages for repeat offenders (wires dead getUserRecentInfractions)
- <user_profile as_of> staleness signal; <location_context topic> from captured channel topic
- prompt framing + output instructions teach the LLM to use the new signals without treating history as proof
- tests: contextEnrichment.test.ts (13) + topic cases in conversationContext.test.ts
This commit is contained in:
asepharyana
2026-08-10 17:15:33 +07:00
parent 0a5254bf20
commit 65c9c2cd9e
11 changed files with 488 additions and 26 deletions
@@ -9,6 +9,10 @@ export interface MessageLocation {
threadId: string | null;
threadName: string | null;
channelName: string | null;
/** Channel topic (resmi/deskripsi channel) — strong context for judging
* whether a message fits the channel's purpose. Guarded: some channel
* types (threads on older API builds) expose no topic. */
topic?: string | null;
nsfw?: boolean;
nsfwLevel?: string | null;
ageRestricted?: boolean;
@@ -107,12 +111,17 @@ export function getMessageLocation(message: Message): MessageLocation {
nsfw?: boolean;
nsfwLevel?: string | null;
};
const topic =
"topic" in channel && typeof channel.topic === "string"
? channel.topic
: null;
if (!channel.isThread?.()) {
return {
channelId: message.channelId,
threadId: null,
threadName: null,
channelName: "name" in channel ? channel.name : null,
topic,
nsfw:
typeof safetyChannel.nsfw === "boolean"
? safetyChannel.nsfw
@@ -133,6 +142,7 @@ export function getMessageLocation(message: Message): MessageLocation {
threadId: channel.id,
threadName: channel.name,
channelName: channel.parent?.name ?? null,
topic,
nsfw:
typeof safetyChannel.nsfw === "boolean" ? safetyChannel.nsfw : undefined,
nsfwLevel: