perf(discord-gateway): start AI moderation analysis immediately, running attachment uploads in parallel instead of blocking

This commit is contained in:
MythEclipse
2026-06-02 22:49:57 +07:00
parent b0a879647e
commit 6d2da7ce1d
4 changed files with 68 additions and 3 deletions
@@ -239,6 +239,7 @@ function getAnalysisWorkerUrl(): URL {
const workerPool = new Piscina({
filename: fileURLToPath(getAnalysisWorkerUrl()),
execArgv: process.execArgv,
maxThreads: config.PISCINA_MAX_THREADS ?? availableParallelism(),
});
interface AnalysisWorkerResponse {
@@ -275,8 +276,8 @@ export function pickBatchWithinBudget(
for (const msg of messages) {
const content = msg.edited_content ?? msg.content;
// Rough token estimate: ~3 chars per token + metadata overhead
const msgTokens = Math.ceil(content.length / 3) + tokensPerMessage;
// Accurate token count via tiktoken (+ overhead for JSON structure)
const msgTokens = estimateTokens(content) + tokensPerMessage;
if (usedTokens + msgTokens <= maxTokens) {
batch.push(msg);
@@ -26,6 +26,55 @@ interface BadwordCacheEntry {
const badwordCache = new Map<string, BadwordCacheEntry>();
const inFlightBadwordLookups = new Map<string, Promise<string[]>>();
// ---------------------------------------------------------------------------
// Local rule-based pre-filter — short-circuits definitively safe messages
// without calling the LLM badword detection API.
// Conservative: only flags messages that are 100% certainly clean.
// ---------------------------------------------------------------------------
const SAFE_PATTERNS: Array<{ test: (text: string) => boolean; reason: string }> = [
{
// Pure laughter patterns
test: (t) => /^(wkwk+|w+kw+k+|wkwkw+|haha+|hehe+|hihi+|huhu+|xixi+|wakak+|awkwa+)$/i.test(t),
reason: "laughter pattern",
},
{
// Single-word affirmatives (common in Indonesian Discord)
test: (t) => /^(ok|oke|okay|sip|siap|aman|mantap|gas|gass|gaskeun|santuy|gaskan|lah|wih|wah|eh|nah|loh|hmm|hm|heh)$/i.test(t),
reason: "single-word affirmative",
},
{
// Greetings / common short expressions
test: (t) => /^(hai|halo|hello|hi|oi|woy|woi|pagi|siang|sore|malam|mlm|p|w|L|F|gws|thx|thks|makasih|ty|thanks|yw|sama-sama|ok sip|ok bang|siap bang)$/i.test(t),
reason: "greeting/common expression",
},
{
// Very short messages (1-2 characters) — reactions, single letters
test: (t) => t.length <= 2,
reason: "very short message (1-2 chars)",
},
{
// Pure numeric or pure punctuation
test: (t) => /^[\d\s.,!?;:'"()\-_]+$/.test(t),
reason: "numeric/punctuation only",
},
];
/**
* Checks whether a text message is definitively safe and does not need
* LLM-based badword detection. This is a conservative local pre-filter
* — it only returns true for patterns that CANNOT be violations.
*/
export function isDefinitivelySafe(text: string): boolean {
// Normalize: strip Discord custom emoji, trim whitespace
const { text: normalized } = normalizeDiscordCustomEmoji(text);
const trimmed = normalized.trim();
if (trimmed.length === 0) return true;
return SAFE_PATTERNS.some((p) => p.test(trimmed));
}
export interface ModerationTextEvidence {
raw: string;
normalized: string;
@@ -123,6 +172,11 @@ export async function detectIndonesianBadwords(
return cached;
}
// ── Tier 0: Local rule-based pre-filter (fastest — no API call) ──
if (isDefinitivelySafe(text)) {
return [];
}
// De-duplicate concurrent lookups
const inFlight = inFlightBadwordLookups.get(cacheKey);
if (inFlight) {
@@ -1,5 +1,6 @@
import { createChildLogger } from "@bete/shared/logger";
import { retryWithBackoff } from "@bete/shared/utils";
import { LRUCache } from "lru-cache";
import type { ChatCompletion } from "openai/resources/chat/completions";
import { AbortError } from "p-retry";
import { z } from "zod";
@@ -28,11 +29,14 @@ import {
buildStickerVisionPrompt,
} from "./stickerPrompt.js";
import {
computeImagePhash,
getCachedMediaAnalysis,
getCachedMediaByPhash,
makeCustomEmojiCacheKey,
makeImageCacheKey,
makeStickerCacheKey,
upsertCachedMediaAnalysis,
upsertCachedMediaByPhash,
} from "./textCacheStore.js";
import { extractUrlsFromText, fetchUrlSafely } from "./urlFetcher.js";
@@ -184,9 +184,15 @@ CRITICAL:
// Composer: assembles all sections with XML delimiters
// ---------------------------------------------------------------------------
/** Prompt mode — determines which few-shot example section is included. */
export type PromptMode = "text" | "media" | "mixed";
export interface BuildSystemPromptOptions {
contextText: string;
includeMediaInstructions: boolean;
/** Prompt mode — determines which sections are included. */
mode: PromptMode;
/** @deprecated Use `mode` instead. */
includeMediaInstructions?: boolean;
correction?: { error: string; preview: string };
}