Previously isAgeRestrictedMessage() early-returned in messageCreate/messageUpdate, so NSFW-channel messages were never stored at all. Now they are captured like any message (visible in dashboard), while the existing age-restricted skip path (queueMessageAnalysis -> buildAgeRestrictedSkipResult) marks them clean with flag age_restricted WITHOUT calling the LLM. NSFW content is also deliberately kept OUT of the Qdrant public semantic-search archive (archiveMessageEmbedded skips when isAgeRestricted), so it can't be found via public web search. No schema change needed (metadata already carries channel.nsfw).
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { embedText } from "@/modules/ai-moderation/embeddingClient";
|
|
import {
|
|
ARCHIVE_COLLECTION,
|
|
qdrantPointId,
|
|
upsertQdrantPointV2,
|
|
} from "@/modules/ai-moderation/qdrantClient";
|
|
import { createChildLogger } from "@/shared/logger/index";
|
|
import { config } from "../../shared/config/config.js";
|
|
|
|
const log = createChildLogger("archive-embedder");
|
|
|
|
export interface ArchiveMessage {
|
|
id: string;
|
|
content: string;
|
|
username: string;
|
|
channel_id: string;
|
|
guild_id: string;
|
|
created_at: number;
|
|
/** True when the message came from an age-restricted (NSFW) channel. NSFW
|
|
* content is deliberately NOT embedded into the public archive so it can't
|
|
* be found via public semantic search. Defaults to false. */
|
|
isAgeRestricted?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Fire-and-forget: embed a captured message and upsert it into the persistent
|
|
* archive collection so the public web can semantic-search the corpus.
|
|
*
|
|
* Failures are swallowed — searching is a nice-to-have, never a precondition
|
|
* for capture or moderation. The message text is kept in the payload so the
|
|
* search endpoint can return results even for deleted messages.
|
|
*
|
|
* NSFW/age-restricted messages are skipped (never embedded) — they are stored
|
|
* in the database for the dashboard but kept out of the public search archive.
|
|
*/
|
|
export function archiveMessageEmbedded(message: ArchiveMessage): void {
|
|
if (message.isAgeRestricted) return; // never surface NSFW in public archive
|
|
if (!config.AI_LLM_EMBEDDING_MODEL) return; // embeddings disabled → skip
|
|
const text = message.content?.trim();
|
|
if (!text || text.length < 3) return;
|
|
|
|
void (async () => {
|
|
try {
|
|
const vector = await embedText(text);
|
|
if (!vector) return;
|
|
const ok = await upsertQdrantPointV2(
|
|
ARCHIVE_COLLECTION,
|
|
qdrantPointId(`archive:${message.id}`),
|
|
vector,
|
|
{
|
|
text: text.slice(0, 4000),
|
|
flags: "",
|
|
analyzed_at: Date.now(),
|
|
// 5-year persistent window (archive is NOT a TTL cache).
|
|
expires_at: Date.now() + 1000 * 60 * 60 * 24 * 365 * 5,
|
|
content_hash: message.id,
|
|
},
|
|
);
|
|
if (!ok) return;
|
|
log.debug({ messageId: message.id }, "Archived message embedding");
|
|
} catch (err) {
|
|
log.debug(
|
|
{
|
|
messageId: message.id,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
},
|
|
"archive embed skipped",
|
|
);
|
|
}
|
|
})();
|
|
}
|