2026-06-01 21:44:29 +07:00
|
|
|
import type { Client } from "discord.js-selfbot-v13";
|
2026-07-31 23:09:00 +07:00
|
|
|
import { createChildLogger } from "@/shared/logger/index";
|
2026-09-24 13:23:38 +07:00
|
|
|
import { config } from "../../shared/config/index.js";
|
2026-06-02 12:17:51 +07:00
|
|
|
import type { EventBroadcaster } from "../event-broadcaster/index.js";
|
2026-07-27 21:54:31 +07:00
|
|
|
import { messageStore } from "../message-capture/messageStore.js";
|
2026-06-09 19:46:08 +07:00
|
|
|
import type { AnalysisQueueStatus } from "../message-capture/types.js";
|
|
|
|
|
import {
|
2026-09-24 14:09:53 +07:00
|
|
|
activeMediaRequests,
|
2026-06-09 19:46:08 +07:00
|
|
|
activeRequests,
|
2026-09-24 14:09:53 +07:00
|
|
|
activeTextRequests,
|
2026-06-09 19:46:08 +07:00
|
|
|
buildAgeRestrictedSkipResult,
|
2026-09-09 23:38:32 +07:00
|
|
|
buildSkipAnalysisUserResult,
|
2026-06-09 19:46:08 +07:00
|
|
|
isAgeRestrictedMessage,
|
2026-09-09 23:38:32 +07:00
|
|
|
isSkipAnalysisUser,
|
2026-06-09 19:46:08 +07:00
|
|
|
} from "./batchProcessor.js";
|
|
|
|
|
import { scheduleConversationAnalysis } from "./batchScheduler.js";
|
2026-07-27 21:54:31 +07:00
|
|
|
import { getConversationKey } from "./circuitBreaker.js";
|
2026-09-24 15:04:53 +07:00
|
|
|
import { conversationDebounceTimers } from "./conversationState.js";
|
2026-06-09 19:46:08 +07:00
|
|
|
import {
|
|
|
|
|
activeIndividualRequests,
|
|
|
|
|
individualCooldownUntil,
|
|
|
|
|
individualInFlight,
|
|
|
|
|
} from "./individualFallbackProcessor.js";
|
2026-07-27 21:54:31 +07:00
|
|
|
import {
|
|
|
|
|
broadcastAnalysisCompleted,
|
|
|
|
|
LAST_ERROR,
|
|
|
|
|
setModerationClient,
|
|
|
|
|
setSharedEventBroadcaster,
|
|
|
|
|
} from "./moderationState.js";
|
2026-09-24 15:04:53 +07:00
|
|
|
import { startRecoveryWorker } from "./recovery-worker.js";
|
2026-06-01 21:44:29 +07:00
|
|
|
|
|
|
|
|
const logger = createChildLogger("ai-analyzer");
|
|
|
|
|
|
2026-07-31 23:09:00 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
2026-09-24 15:04:53 +07:00
|
|
|
// Public API — queueing, status, worker startup
|
2026-06-01 21:44:29 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Queues a message for analysis (debounced by conversation).
|
2026-09-24 15:04:53 +07:00
|
|
|
*
|
|
|
|
|
* Messages that never need an LLM call are short-circuited here and recorded
|
|
|
|
|
* with their skip verdict: age-restricted messages and configured skip-list
|
|
|
|
|
* users.
|
2026-06-01 21:44:29 +07:00
|
|
|
*/
|
|
|
|
|
export async function queueMessageAnalysis(messageId: string): Promise<void> {
|
|
|
|
|
if (!config.AI_ANALYSIS_ENABLED) return;
|
|
|
|
|
|
|
|
|
|
try {
|
2026-07-27 21:54:31 +07:00
|
|
|
const message = await messageStore.getMessageById(messageId);
|
2026-06-01 21:44:29 +07:00
|
|
|
if (!message) {
|
|
|
|
|
logger.warn({ messageId }, "Message not found for analysis queue");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isAgeRestrictedMessage(message)) {
|
2026-09-24 15:04:53 +07:00
|
|
|
await recordSkip(message.id, buildAgeRestrictedSkipResult());
|
2026-06-04 13:13:55 +07:00
|
|
|
logger.debug(
|
2026-06-01 21:44:29 +07:00
|
|
|
{ messageId },
|
|
|
|
|
"Skipped AI analysis for age-restricted message",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 23:38:32 +07:00
|
|
|
if (isSkipAnalysisUser(message)) {
|
2026-09-24 15:04:53 +07:00
|
|
|
await recordSkip(message.id, buildSkipAnalysisUserResult());
|
2026-09-09 23:38:32 +07:00
|
|
|
logger.debug(
|
|
|
|
|
{ messageId, userId: message.user_id },
|
|
|
|
|
"Skipped AI analysis for configured skip-list user",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
queueConversationAnalysis(getConversationKey(message));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
{
|
|
|
|
|
messageId,
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
},
|
|
|
|
|
"Failed to queue message for analysis",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 15:04:53 +07:00
|
|
|
/** Persist a skip verdict and broadcast it so the dashboard reflects it. */
|
|
|
|
|
async function recordSkip(
|
|
|
|
|
messageId: string,
|
|
|
|
|
result: Parameters<typeof messageStore.updateMessageAIAnalysis>[1],
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const updated = await messageStore.updateMessageAIAnalysis(messageId, result);
|
|
|
|
|
if (updated) {
|
|
|
|
|
broadcastAnalysisCompleted(updated);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
/**
|
|
|
|
|
* Queues a conversation for analysis (debounced).
|
|
|
|
|
*/
|
|
|
|
|
export function queueConversationAnalysis(conversationKey: string): void {
|
|
|
|
|
if (!config.AI_ANALYSIS_ENABLED) return;
|
|
|
|
|
scheduleConversationAnalysis(conversationKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns current status of both the batch and individual fallback queues.
|
|
|
|
|
*/
|
|
|
|
|
export function getAnalysisQueueStatus(): AnalysisQueueStatus {
|
|
|
|
|
return {
|
|
|
|
|
queuedConversations: conversationDebounceTimers.size,
|
|
|
|
|
activeRequests,
|
2026-09-24 14:09:53 +07:00
|
|
|
activeTextRequests,
|
|
|
|
|
activeMediaRequests,
|
2026-06-01 21:44:29 +07:00
|
|
|
activeIndividualRequests,
|
|
|
|
|
individualInFlightCount: individualInFlight.size,
|
|
|
|
|
individualCircuitBreakerActive: Date.now() < individualCooldownUntil,
|
2026-06-09 19:46:08 +07:00
|
|
|
lastError: LAST_ERROR.value,
|
2026-06-01 21:44:29 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-24 15:04:53 +07:00
|
|
|
* Starts the background workers behind the analysis pipeline:
|
|
|
|
|
* - the recovery worker (stranded pending / incomplete messages + cache prune)
|
|
|
|
|
* - the optional culture and user-profile learners.
|
2026-06-01 21:44:29 +07:00
|
|
|
*
|
2026-09-24 15:04:53 +07:00
|
|
|
* Also injects the Discord client and event broadcaster into the pipeline
|
|
|
|
|
* state so downstream modules can act and publish.
|
2026-06-01 21:44:29 +07:00
|
|
|
*/
|
2026-06-02 12:17:51 +07:00
|
|
|
export function startPendingAIAnalysisWorker(
|
|
|
|
|
client?: Client,
|
|
|
|
|
eventBroadcaster?: EventBroadcaster,
|
|
|
|
|
): void {
|
2026-06-09 19:46:08 +07:00
|
|
|
setModerationClient(client);
|
|
|
|
|
setSharedEventBroadcaster(eventBroadcaster);
|
2026-06-01 21:44:29 +07:00
|
|
|
if (!config.AI_ANALYSIS_ENABLED) return;
|
|
|
|
|
|
2026-06-06 15:38:36 +07:00
|
|
|
import("./cultureLearner.js")
|
|
|
|
|
.then((m) => m.startCultureLearnerWorker())
|
|
|
|
|
.catch(console.error);
|
2026-08-16 19:56:27 +07:00
|
|
|
if (config.AI_USER_PROFILE_LEARNING_ENABLED) {
|
|
|
|
|
import("./userProfileLearner.js")
|
|
|
|
|
.then((m) => m.startUserProfileLearnerWorker())
|
|
|
|
|
.catch(console.error);
|
|
|
|
|
}
|
2026-06-05 18:04:57 +07:00
|
|
|
|
2026-09-24 15:04:53 +07:00
|
|
|
startRecoveryWorker();
|
2026-06-01 21:44:29 +07:00
|
|
|
}
|