diff --git a/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts b/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts index a7a5ba7..35a474e 100644 --- a/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts +++ b/services/discord-gateway/src/modules/ai-moderation/conversationContext.ts @@ -6,6 +6,7 @@ import { } from "../message-capture/messageMetadata.js"; import type { MessageRecord } from "../message-capture/types.js"; import { sanitizeDiscordTokens } from "./discordTokens.js"; +import { resolveDisplayName } from "./moderationBuilders.js"; const logger = createChildLogger("conversationContext"); @@ -130,7 +131,7 @@ export function formatMessageForPrompt( const mediaEvidence = formatMediaEvidenceForPrompt(msg.metadata); const mediaSuffix = mediaEvidence ? ` ${mediaEvidence}` : ""; const refInfo = formatReferenceInfo(msg); - return `[${label}] id=${msg.id} time=${timestamp} user=${msg.username}: ${content}${mediaSuffix}${refInfo}`; + return `[${label}] id=${msg.id} time=${timestamp} user=${resolveDisplayName(msg)}: ${content}${mediaSuffix}${refInfo}`; } /** diff --git a/services/discord-gateway/src/modules/ai-moderation/moderationBuilders.ts b/services/discord-gateway/src/modules/ai-moderation/moderationBuilders.ts index a45872f..75f021b 100644 --- a/services/discord-gateway/src/modules/ai-moderation/moderationBuilders.ts +++ b/services/discord-gateway/src/modules/ai-moderation/moderationBuilders.ts @@ -36,6 +36,27 @@ export function getAnalysisContent(message: MessageRecord): string { ).trim(); } +/** + * Server nickname (member.displayName) when captured, else the author + * username. Discord shows the server nickname to other members, so the LLM + * should see the same name the channel sees — and a nickname can carry + * moderation signal itself (offensive nick + clean message → low warn). + */ +export function resolveDisplayName(msg: MessageRecord): string { + if (msg.metadata) { + try { + const meta = JSON.parse(msg.metadata) as { + member?: { displayName?: string | null } | null; + }; + const dn = meta?.member?.displayName; + if (dn && dn.trim().length > 0) return dn; + } catch { + // malformed metadata — fall back to username + } + } + return msg.username; +} + /** * Builds a XML element for reply/forward/crosspost context. */ diff --git a/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts b/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts index 05868a1..8814e73 100644 --- a/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts +++ b/services/discord-gateway/src/modules/ai-moderation/textBatchProcessor.ts @@ -21,6 +21,7 @@ import { buildReferenceXml, escapeXml, getAnalysisContent, + resolveDisplayName, } from "./moderationBuilders.js"; import { buildSystemPrompt as buildSystemPromptModular, @@ -308,7 +309,7 @@ export async function runTextOnlyBatch( const userCtx = userContexts.get(msg.user_id) ?? ""; const userProfileCtx = userProfiles.get(msg.user_id) ?? ""; const refXml = await buildReferenceXml(msg); - return `\n ${userCtx}${userProfileCtx ? `\n ${userProfileCtx}` : ""}${refXml ? `\n ${refXml}` : ""}\n ${escapeXml(content)}${webContext}${mediaEvidenceCtx}\n`; + return `\n ${userCtx}${userProfileCtx ? `\n ${userProfileCtx}` : ""}${refXml ? `\n ${refXml}` : ""}\n ${escapeXml(content)}${webContext}${mediaEvidenceCtx}\n`; }), ) ).join("\n"); diff --git a/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts b/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts index f02f757..2c85277 100644 --- a/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts +++ b/services/discord-gateway/src/modules/ai-moderation/visionAnalyzer.ts @@ -39,6 +39,7 @@ import { buildReferenceXml, escapeXml, getAnalysisContent, + resolveDisplayName, } from "./moderationBuilders.js"; import { buildCustomEmojiVisionPrompt, @@ -367,6 +368,6 @@ export async function prepareMediaMessage( const profile = await getUserProfile(target.user_id); const refXml = await buildReferenceXml(target); - const messageBlock = `\n ${profile ? `\n ${sanitizeAiContent(profile.profile_summary)}` : ""}${refXml ? `\n ${refXml}` : ""}\n ${escapeXml(content)}${mediaContext ? ` ${escapeXml(mediaContext)}` : ""}${webContext}${mediaAnalysisContext}${searxngXml}\n`; + const messageBlock = `\n ${profile ? `\n ${sanitizeAiContent(profile.profile_summary)}` : ""}${refXml ? `\n ${refXml}` : ""}\n ${escapeXml(content)}${mediaContext ? ` ${escapeXml(mediaContext)}` : ""}${webContext}${mediaAnalysisContext}${searxngXml}\n`; return { targetId, messageBlock }; } diff --git a/services/discord-gateway/tests/conversationContext.test.ts b/services/discord-gateway/tests/conversationContext.test.ts index 5eae191..da8af7a 100644 --- a/services/discord-gateway/tests/conversationContext.test.ts +++ b/services/discord-gateway/tests/conversationContext.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { buildConversationContext, buildLocationContext, + formatMessageForPrompt, } from "../src/modules/ai-moderation/conversationContext.js"; import { extractOgMeta } from "../src/modules/ai-moderation/urlFetcher.js"; import type { MessageRecord } from "../src/modules/message-capture/types.js"; @@ -139,6 +140,30 @@ describe("buildConversationContext — recency gating", () => { }); }); +describe("formatMessageForPrompt — server nickname (displayName)", () => { + it("renders member.displayName when captured (per-server nickname)", () => { + const m = msg("n1", NOW - MIN); + m.metadata = JSON.stringify({ + member: { + displayName: "Si Goblok Server", + roles: [], + joinedTimestamp: null, + }, + }); + const line = formatMessageForPrompt(m, "context"); + expect(line).toContain("user=Si Goblok Server"); + expect(line).not.toContain("user_user_n1"); + }); + + it("falls back to global username when displayName missing", () => { + const line = formatMessageForPrompt( + msg("n2", NOW - MIN, "halo"), + "context", + ); + expect(line).toContain("user=user_n2"); + }); +}); + describe("buildLocationContext — channel/thread/nsfw enrichment", () => { it("renders channel name + thread name from captured metadata", () => { const t = target();