refactor: streamline error handling and attachment processing in message capture

This commit is contained in:
MythEclipse
2026-05-18 23:46:51 +07:00
parent b2cd9f672c
commit 6339d741a9
4 changed files with 52 additions and 51 deletions
-13
View File
@@ -97,19 +97,6 @@ async function processAnalysisRequest({
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
const rows: MessageRecord[] = []; const rows: MessageRecord[] = [];
for (const msg of messages) {
const row = await updateMessageAIAnalysis(msg.id, {
status: "error",
flags: null,
score: null,
raw: null,
analysis: null,
analyzedAt: Date.now(),
error: errorMessage,
});
if (row) rows.push(row);
}
return { ok: false, conversationKey, rows, error: errorMessage }; return { ok: false, conversationKey, rows, error: errorMessage };
} }
} }
+4 -32
View File
@@ -32,7 +32,6 @@ const conversationErrorCooldown = new Map<string, number>();
let activeRequests = 0; let activeRequests = 0;
let lastError: string | null = null; let lastError: string | null = null;
const MAX_ACTIVE_REQUESTS = 2;
const DEBOUNCE_MS = 1500; const DEBOUNCE_MS = 1500;
const RECOVERY_INTERVAL_MS = 15000; const RECOVERY_INTERVAL_MS = 15000;
const ERROR_COOLDOWN_MS = 30000; const ERROR_COOLDOWN_MS = 30000;
@@ -104,7 +103,7 @@ async function processBatch(
); );
logger.error( logger.error(
{ conversationKey, error: lastError }, { conversationKey, error: lastError },
"Batch analysis failed", "Batch analysis failed, will retry after cooldown",
); );
return; return;
} }
@@ -118,21 +117,8 @@ async function processBatch(
); );
logger.error( logger.error(
{ conversationKey, error: lastError }, { conversationKey, error: lastError },
"Analysis worker failed", "Analysis worker failed, will retry after cooldown",
); );
for (const msg of messages) {
const row = await updateMessageAIAnalysis(msg.id, {
status: "error",
flags: null,
score: null,
raw: null,
analysis: null,
analyzedAt: Date.now(),
error: lastError,
});
if (row) getModerationBroadcaster()?.messageAnalyzed(row);
}
} finally { } finally {
activeRequests--; activeRequests--;
conversationProcessing.delete(conversationKey); conversationProcessing.delete(conversationKey);
@@ -186,22 +172,13 @@ function scheduleConversationAnalysis(conversationKey: string): void {
clearTimeout(existingTimer); clearTimeout(existingTimer);
} }
// If we have available slots, process immediately with shorter debounce // Always use shorter debounce for immediate processing (no concurrency limit)
const debounceTime = const debounceTime = Math.min(DEBOUNCE_MS, 500);
activeRequests < MAX_ACTIVE_REQUESTS
? Math.min(DEBOUNCE_MS, 500)
: DEBOUNCE_MS;
// Set new debounced timer // Set new debounced timer
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
conversationDebounceTimers.delete(conversationKey); conversationDebounceTimers.delete(conversationKey);
// If activeRequests >= MAX_ACTIVE_REQUESTS, requeue instead of waiting
if (activeRequests >= MAX_ACTIVE_REQUESTS) {
scheduleConversationAnalysis(conversationKey);
return;
}
// Get pending messages for this conversation // Get pending messages for this conversation
const messages = await getPendingMessagesByConversation( const messages = await getPendingMessagesByConversation(
conversationKey, conversationKey,
@@ -277,11 +254,6 @@ export function startPendingAIAnalysisWorker(): void {
const conversationKeys = await getPendingConversationKeys(100); const conversationKeys = await getPendingConversationKeys(100);
for (const key of conversationKeys) { for (const key of conversationKeys) {
// Stop if we've reached max active requests
if (activeRequests >= MAX_ACTIVE_REQUESTS) {
break;
}
// Skip if already scheduled // Skip if already scheduled
if (conversationDebounceTimers.has(key)) { if (conversationDebounceTimers.has(key)) {
continue; continue;
+24
View File
@@ -81,6 +81,30 @@ export function parseModerationResponse(
// If parsed is a direct array, wrap it in a results object to handle LLM variations // If parsed is a direct array, wrap it in a results object to handle LLM variations
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
parsed = { results: parsed }; parsed = { results: parsed };
} else if (parsed && typeof parsed === "object" && !("results" in parsed)) {
// Handle single result object (has message_id or status)
if ("message_id" in parsed || "status" in parsed) {
const msgId = (parsed as any).message_id || (parsed as any).id;
parsed = {
results: [
{
message_id: msgId,
status: (parsed as any).status || "clean",
flags: (parsed as any).flags || [],
score: (parsed as any).score !== undefined ? (parsed as any).score : 0.1,
analysis: (parsed as any).analysis || "",
},
],
};
} else {
// Look for any array property (result, data, messages, moderation, etc.)
const arrayKey = Object.keys(parsed).find(
(key) => Array.isArray((parsed as any)[key]),
);
if (arrayKey) {
parsed.results = (parsed as any)[arrayKey];
}
}
} }
// Validate structure // Validate structure
+24 -6
View File
@@ -2,6 +2,7 @@ import type { Client, Message } from "discord.js-selfbot-v13";
import { config } from "../config"; import { config } from "../config";
import { createChildLogger } from "../logger"; import { createChildLogger } from "../logger";
import { queueMessageAnalysis } from "./aiAnalyzer"; import { queueMessageAnalysis } from "./aiAnalyzer";
import { processAttachmentUpload } from "./attachmentUploader";
import { import {
getDisplayContent, getDisplayContent,
getMessageLocation, getMessageLocation,
@@ -87,15 +88,13 @@ export async function captureMessage(
} }
const isBacklog = options.source === "backlog"; const isBacklog = options.source === "backlog";
if (!isBacklog) {
queueMessageAnalysis(message.id);
}
const broadcaster = getModerationBroadcaster(); const broadcaster = getModerationBroadcaster();
if (broadcaster && !isBacklog) { if (broadcaster && !isBacklog) {
broadcaster.messageCreated(messageRecord); broadcaster.messageCreated(messageRecord);
} }
// Insert attachments before queuing analysis to avoid race condition
if (message.attachments.size > 0) { if (message.attachments.size > 0) {
for (const [, attachment] of message.attachments) { for (const [, attachment] of message.attachments) {
const attachmentRecord: AttachmentRecord = { const attachmentRecord: AttachmentRecord = {
@@ -109,20 +108,39 @@ export async function captureMessage(
size: attachment.size, size: attachment.size,
type: attachment.contentType || "application/octet-stream", type: attachment.contentType || "application/octet-stream",
discord_url: attachment.url, discord_url: attachment.url,
uploaded_url: attachment.url, uploaded_url: null,
upload_status: "uploaded", upload_status: "pending",
upload_error: null, upload_error: null,
created_at: Date.now(), created_at: Date.now(),
uploaded_at: Date.now(), uploaded_at: null,
}; };
await insertAttachment(attachmentRecord); await insertAttachment(attachmentRecord);
// Initiate async upload to Picser (non-blocking, fire-and-forget)
if (!isBacklog) {
processAttachmentUpload(
attachment.id,
attachment.url,
attachment.name || "unknown",
).catch((err) => {
logger.error(
{ attachmentId: attachment.id, error: err },
"Failed to initiate attachment upload",
);
});
}
if (broadcaster) { if (broadcaster) {
broadcaster.attachmentCreated(attachmentRecord); broadcaster.attachmentCreated(attachmentRecord);
} }
} }
} }
// Queue analysis after attachments are inserted
if (!isBacklog) {
queueMessageAnalysis(message.id);
}
} }
export function registerMessageCapture(client: Client): void { export function registerMessageCapture(client: Client): void {