feat(moderation): skip AI analysis for music bots via AI_SKIP_ANALYSIS_USER_IDS
Jockie Music (user 411916947773587456) posts now-playing embeds/spotify links ~1347 captured messages — every one consumed a moderation LLM call for zero signal and contributed to batch timeouts. Config AI_SKIP_ANALYSIS_USER_IDS (default=Jockie) skips them at ALL three analysis paths: - queueMessageAnalysis entry (direct skip-result like age-restricted) - batchScheduler processing (pre-batch filter) - individual recovery path (no fallback spam for already-skipped authors) Skip-result mirrors age_restricted: status=clean, flags=[skip_analysis_user], action=none — stays visible in the dashboard, never analyzed.
This commit is contained in:
@@ -7,8 +7,11 @@ import type { AnalysisQueueStatus } from "../message-capture/types.js";
|
|||||||
import {
|
import {
|
||||||
activeRequests,
|
activeRequests,
|
||||||
buildAgeRestrictedSkipResult,
|
buildAgeRestrictedSkipResult,
|
||||||
|
buildSkipAnalysisUserResult,
|
||||||
isAgeRestrictedMessage,
|
isAgeRestrictedMessage,
|
||||||
|
isSkipAnalysisUser,
|
||||||
skipAgeRestrictedMessages,
|
skipAgeRestrictedMessages,
|
||||||
|
skipAnalysisUserMessages,
|
||||||
} from "./batchProcessor.js";
|
} from "./batchProcessor.js";
|
||||||
import { scheduleConversationAnalysis } from "./batchScheduler.js";
|
import { scheduleConversationAnalysis } from "./batchScheduler.js";
|
||||||
import { getConversationKey } from "./circuitBreaker.js";
|
import { getConversationKey } from "./circuitBreaker.js";
|
||||||
@@ -84,6 +87,21 @@ export async function queueMessageAnalysis(messageId: string): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSkipAnalysisUser(message)) {
|
||||||
|
const updated = await messageStore.updateMessageAIAnalysis(
|
||||||
|
message.id,
|
||||||
|
buildSkipAnalysisUserResult(),
|
||||||
|
);
|
||||||
|
if (updated) {
|
||||||
|
broadcastAnalysisCompleted(updated);
|
||||||
|
}
|
||||||
|
logger.debug(
|
||||||
|
{ messageId, userId: message.user_id },
|
||||||
|
"Skipped AI analysis for configured skip-list user",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
queueConversationAnalysis(getConversationKey(message));
|
queueConversationAnalysis(getConversationKey(message));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -240,8 +258,9 @@ export function startPendingAIAnalysisWorker(
|
|||||||
messageStore
|
messageStore
|
||||||
.getIncompleteMessagesByConversation(key, 500)
|
.getIncompleteMessagesByConversation(key, 500)
|
||||||
.then(async (msgs) => {
|
.then(async (msgs) => {
|
||||||
const processableMessages =
|
const processableMessages = await skipAnalysisUserMessages(
|
||||||
await skipAgeRestrictedMessages(msgs);
|
await skipAgeRestrictedMessages(msgs),
|
||||||
|
);
|
||||||
return processableMessages;
|
return processableMessages;
|
||||||
})
|
})
|
||||||
.then((msgs) => {
|
.then((msgs) => {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import {
|
|||||||
|
|
||||||
const logger = createChildLogger("batch-processor");
|
const logger = createChildLogger("batch-processor");
|
||||||
|
|
||||||
|
/** User IDs whose messages are captured but never AI-analyzed (config). */
|
||||||
|
const AI_SKIP_ANALYSIS_USER_IDS = new Set(config.AI_SKIP_ANALYSIS_USER_IDS);
|
||||||
|
|
||||||
export interface AnalysisWorkerResponse {
|
export interface AnalysisWorkerResponse {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
conversationKey: string;
|
conversationKey: string;
|
||||||
@@ -67,6 +70,11 @@ export function isAgeRestrictedMessage(message: MessageRecord): boolean {
|
|||||||
return isAgeRestrictedMetadata(message.metadata);
|
return isAgeRestrictedMetadata(message.metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True when the author's user ID is in the AI_SKIP_ANALYSIS_USER_IDS set. */
|
||||||
|
export function isSkipAnalysisUser(message: MessageRecord): boolean {
|
||||||
|
return AI_SKIP_ANALYSIS_USER_IDS.has(message.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildAgeRestrictedSkipResult(): {
|
export function buildAgeRestrictedSkipResult(): {
|
||||||
status: "clean";
|
status: "clean";
|
||||||
flags: string | null;
|
flags: string | null;
|
||||||
@@ -93,6 +101,33 @@ export function buildAgeRestrictedSkipResult(): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Skip-result for authors in the AI_SKIP_ANALYSIS_USER_IDS set (music bots). */
|
||||||
|
export function buildSkipAnalysisUserResult(): {
|
||||||
|
status: "clean";
|
||||||
|
flags: string | null;
|
||||||
|
score: number;
|
||||||
|
analysis: string;
|
||||||
|
categories: string[];
|
||||||
|
severity: "none";
|
||||||
|
confidence: number;
|
||||||
|
recommendedAction: "none";
|
||||||
|
analyzedAt: number;
|
||||||
|
error: null;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
status: "clean",
|
||||||
|
flags: JSON.stringify(["skip_analysis_user"]),
|
||||||
|
score: 0,
|
||||||
|
analysis: "Skipped moderation for bot author (configured skip list).",
|
||||||
|
categories: ["skip_analysis_user"],
|
||||||
|
severity: "none",
|
||||||
|
confidence: 1,
|
||||||
|
recommendedAction: "none",
|
||||||
|
analyzedAt: Date.now(),
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function skipAgeRestrictedMessages(
|
export async function skipAgeRestrictedMessages(
|
||||||
messages: MessageRecord[],
|
messages: MessageRecord[],
|
||||||
): Promise<MessageRecord[]> {
|
): Promise<MessageRecord[]> {
|
||||||
@@ -118,6 +153,30 @@ export async function skipAgeRestrictedMessages(
|
|||||||
return messages.filter((message) => !skippedIds.has(message.id));
|
return messages.filter((message) => !skippedIds.has(message.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filter out messages from AI_SKIP_ANALYSIS_USER_IDS authors (no LLM call). */
|
||||||
|
export async function skipAnalysisUserMessages(
|
||||||
|
messages: MessageRecord[],
|
||||||
|
): Promise<MessageRecord[]> {
|
||||||
|
const skipUsers = messages.filter(isSkipAnalysisUser);
|
||||||
|
if (skipUsers.length === 0) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skippedRows = await messageStore.updateMessagesAIAnalysisBulk(
|
||||||
|
skipUsers.map((message) => ({
|
||||||
|
messageId: message.id,
|
||||||
|
result: buildSkipAnalysisUserResult(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const row of skippedRows) {
|
||||||
|
broadcastAnalysisCompleted(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const skippedIds = new Set(skipUsers.map((message) => message.id));
|
||||||
|
return messages.filter((message) => !skippedIds.has(message.id));
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Batch pipeline
|
// Batch pipeline
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
pickBatchWithinBudget,
|
pickBatchWithinBudget,
|
||||||
processBatch,
|
processBatch,
|
||||||
skipAgeRestrictedMessages,
|
skipAgeRestrictedMessages,
|
||||||
|
skipAnalysisUserMessages,
|
||||||
} from "./batchProcessor.js";
|
} from "./batchProcessor.js";
|
||||||
import {
|
import {
|
||||||
conversationConsecutiveErrors,
|
conversationConsecutiveErrors,
|
||||||
@@ -81,7 +82,9 @@ export function scheduleConversationAnalysis(conversationKey: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const processableMessages = await skipAgeRestrictedMessages(messages);
|
const processableMessages = await skipAnalysisUserMessages(
|
||||||
|
await skipAgeRestrictedMessages(messages),
|
||||||
|
);
|
||||||
if (processableMessages.length === 0) {
|
if (processableMessages.length === 0) {
|
||||||
if (
|
if (
|
||||||
conversationProcessing.get(conversationKey) === processingStartedAt
|
conversationProcessing.get(conversationKey) === processingStartedAt
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ export const configSchema = z
|
|||||||
.describe(
|
.describe(
|
||||||
"Channel IDs where bot messages are NOT captured/analyzed (bot detection stays on everywhere else)",
|
"Channel IDs where bot messages are NOT captured/analyzed (bot detection stays on everywhere else)",
|
||||||
),
|
),
|
||||||
|
// User IDs whose messages are captured but NEVER AI-analyzed (skip result
|
||||||
|
// directly, like age-restricted). Used for high-volume music/reaction
|
||||||
|
// bots that spam the chat log (e.g. Jockie Music) — their now-playing
|
||||||
|
// embeds carry no moderation signal.
|
||||||
|
AI_SKIP_ANALYSIS_USER_IDS: z
|
||||||
|
.string()
|
||||||
|
.default("411916947773587456")
|
||||||
|
.transform((v) => v.split(",").filter(Boolean))
|
||||||
|
.describe("User IDs to skip AI analysis for (captured but not analyzed)"),
|
||||||
|
|
||||||
// ── Legacy voice ─────────────────────────────────────────────────────
|
// ── Legacy voice ─────────────────────────────────────────────────────
|
||||||
VOICE_GUILD_ID: z.string().min(1).optional(),
|
VOICE_GUILD_ID: z.string().min(1).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user