refactor(ai-moderation): unify worker pool entry points with discriminated union routing

This commit is contained in:
MythEclipse
2026-06-04 17:39:40 +07:00
parent 686dc2de4f
commit 4996acfacf
2 changed files with 138 additions and 176 deletions
@@ -23,57 +23,76 @@ async function ensureDb() {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Batch analysis (existing) // Job types — the default export routes on `type`
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface AnalysisWorkerRequest { type WorkerJob =
conversationKey: string; | { type: "batch"; conversationKey: string; messages: MessageRecord[] }
messages: MessageRecord[]; | { type: "individual"; message: MessageRecord; skipNormalAnalysis: boolean };
}
export type AnalysisWorkerResponse = type BatchOkResponse = { ok: true; conversationKey: string; rows: MessageRecord[] };
| { type BatchErrorResponse = { ok: false; conversationKey: string; rows: MessageRecord[]; error: string };
ok: true; type IndividualOkResponse = { ok: true; results: AnalysisResult[] };
conversationKey: string; type IndividualErrorResponse = { ok: false; results: AnalysisResult[]; error: string };
rows: MessageRecord[];
}
| {
ok: false;
conversationKey: string;
rows: MessageRecord[];
error: string;
};
export default async function processAnalysisRequest({ type WorkerResponse = BatchOkResponse | BatchErrorResponse | IndividualOkResponse | IndividualErrorResponse;
conversationKey,
messages, /**
}: AnalysisWorkerRequest): Promise<AnalysisWorkerResponse> { * Default export — Piscina worker entry point.
* Routes to the correct handler based on `type` field.
*/
export default async function workerRouter(job: WorkerJob): Promise<WorkerResponse> {
if (!config.AI_LLM_API_KEY) { if (!config.AI_LLM_API_KEY) {
console.error( console.error(
JSON.stringify({ JSON.stringify({
level: "FATAL", level: "FATAL",
context: "aiAnalysisWorker", context: "aiAnalysisWorker",
error: error: "AI_LLM_API_KEY is missing from environment. Force closing worker operation.",
"AI_LLM_API_KEY is missing from environment. Force closing worker operation.",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}), }),
); );
process.exit(1); process.exit(1);
} }
try {
try { try {
await ensureDb(); await ensureDb();
} catch (dbError) { } catch (dbError) {
const msg = dbError instanceof Error ? dbError.message : String(dbError); const msg = dbError instanceof Error ? dbError.message : String(dbError);
return { if (job.type === "batch") {
ok: false, return { ok: false, conversationKey: job.conversationKey, rows: [], error: `Database init failed: ${msg}` };
conversationKey, }
rows: [], return { ok: false, results: [], error: `Database init failed: ${msg}` };
error: `Database init failed: ${msg}`,
};
} }
try {
if (job.type === "batch") {
return await processBatch(job);
}
return await processIndividual(job);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
console.error(JSON.stringify({
level: "ERROR",
context: "aiAnalysisWorker",
type: job.type,
error: errorMessage,
stack: errorStack,
timestamp: new Date().toISOString(),
}));
if (job.type === "batch") {
return { ok: false, conversationKey: job.conversationKey, rows: [], error: errorMessage };
}
return { ok: false, results: [], error: errorMessage };
}
}
// ---------------------------------------------------------------------------
// Batch handler
// ---------------------------------------------------------------------------
async function processBatch(job: { type: "batch"; conversationKey: string; messages: MessageRecord[] }): Promise<BatchOkResponse | BatchErrorResponse> {
const { conversationKey, messages } = job;
const firstMessage = messages[0]; const firstMessage = messages[0];
if (!firstMessage) return { ok: true, conversationKey, rows: [] }; if (!firstMessage) return { ok: true, conversationKey, rows: [] };
@@ -125,66 +144,14 @@ export default async function processAnalysisRequest({
`Failed to update DB: ${dbErr instanceof Error ? dbErr.message : String(dbErr)}`, `Failed to update DB: ${dbErr instanceof Error ? dbErr.message : String(dbErr)}`,
); );
} }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
const rows: MessageRecord[] = [];
console.error(
JSON.stringify({
level: "ERROR",
context: "aiAnalysisWorker",
conversationKey,
messageCount: messages.length,
error: errorMessage,
stack: errorStack,
timestamp: new Date().toISOString(),
}),
);
return { ok: false, conversationKey, rows, error: errorMessage };
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Individual fallback analysis (offloaded from main thread) // Individual fallback handler (offloaded from main thread)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface IndividualWorkerRequest { async function processIndividual(job: { type: "individual"; message: MessageRecord; skipNormalAnalysis: boolean }): Promise<IndividualOkResponse | IndividualErrorResponse> {
message: MessageRecord; const { message, skipNormalAnalysis } = job;
/** Optional — if true, skip normal analysis and go straight to simple fallback */
skipNormalAnalysis: boolean;
}
export type IndividualWorkerResponse =
| {
ok: true;
results: AnalysisResult[];
}
| {
ok: false;
results: AnalysisResult[];
error: string;
};
/**
* Processes a single message analysis in the worker thread.
* Fetches context, attachments, runs LLM analysis (or simple fallback),
* and returns the result — does NOT update DB or broadcast.
*
* The caller (main thread) handles DB writes, broadcasting, and auto-delete
* scheduling.
*/
export async function processIndividualAnalysis({
message,
skipNormalAnalysis,
}: IndividualWorkerRequest): Promise<IndividualWorkerResponse> {
if (!config.AI_LLM_API_KEY) {
return { ok: false, results: [], error: "AI_LLM_API_KEY is missing" };
}
try {
await ensureDb();
const contextBefore = await getConversationContextBefore({ const contextBefore = await getConversationContextBefore({
channelId: message.channel_id, channelId: message.channel_id,
@@ -205,11 +172,9 @@ export async function processIndividualAnalysis({
let results: AnalysisResult[]; let results: AnalysisResult[];
if (skipNormalAnalysis) { if (skipNormalAnalysis) {
// Go straight to simple text fallback (no JSON, no complex prompt)
const simpleResult = await runSimpleTextFallback(message); const simpleResult = await runSimpleTextFallback(message);
results = [simpleResult]; results = [simpleResult];
} else { } else {
// Try normal analysis first
const moderationResult = await runModerationAnalysis({ const moderationResult = await runModerationAnalysis({
targets: [message], targets: [message],
contextText: contextLines.join("\n"), contextText: contextLines.join("\n"),
@@ -219,8 +184,4 @@ export async function processIndividualAnalysis({
} }
return { ok: true, results }; return { ok: true, results };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { ok: false, results: [], error: errorMessage };
}
} }
@@ -412,7 +412,7 @@ async function processIndividualFallback(
); );
const simpleResult = await workerPool.run({ const simpleResult = await workerPool.run({
type: "individual_simple", type: "individual",
message, message,
skipNormalAnalysis: true, skipNormalAnalysis: true,
} as any) as } as any) as
@@ -654,6 +654,7 @@ async function processBatch(
conversationProcessing.set(conversationKey, processingStartedAt); conversationProcessing.set(conversationKey, processingStartedAt);
try { try {
const result = (await workerPool.run({ const result = (await workerPool.run({
type: "batch",
conversationKey, conversationKey,
messages, messages,
})) as AnalysisWorkerResponse; })) as AnalysisWorkerResponse;