feat(ai-moderation): fetch web content from URLs in text-only messages for accurate LLM analysis
This commit is contained in:
@@ -953,6 +953,41 @@ async function runTextOnlyBatch(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Fetch web content from URLs in text-only messages ──
|
||||||
|
// Prevents LLM from guessing based on domain name alone (e.g., false "scam" flags).
|
||||||
|
// The fetched text content is injected into the message XML so the LLM can
|
||||||
|
// analyze the actual page rather than pattern-match the URL string.
|
||||||
|
const urlFetchMap = new Map<string, string>(); // url → fetched text content
|
||||||
|
{
|
||||||
|
const allUrls = new Set<string>();
|
||||||
|
for (const msg of targets) {
|
||||||
|
const content = msg.edited_content ?? msg.content;
|
||||||
|
for (const url of extractUrlsFromText(content)) {
|
||||||
|
allUrls.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const urlArr = Array.from(allUrls).slice(0, 10); // cap to 10 fetches per batch
|
||||||
|
if (urlArr.length > 0) {
|
||||||
|
log.debug(
|
||||||
|
{ urlCount: urlArr.length },
|
||||||
|
"Fetching web content for text-only batch URLs",
|
||||||
|
);
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
urlArr.map((url) => fetchUrlSafely(url)),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < urlArr.length; i++) {
|
||||||
|
const r = results[i];
|
||||||
|
if (
|
||||||
|
r.status === "fulfilled" &&
|
||||||
|
r.value.type === "text" &&
|
||||||
|
r.value.textContent
|
||||||
|
) {
|
||||||
|
urlFetchMap.set(urlArr[i], r.value.textContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Group identical short messages (< 20 chars) to reduce redundant analysis ──
|
// ── Group identical short messages (< 20 chars) to reduce redundant analysis ──
|
||||||
// Messages with identical normalized content share a single representative.
|
// Messages with identical normalized content share a single representative.
|
||||||
// Results are fanned out to all group members after the LLM call.
|
// Results are fanned out to all group members after the LLM call.
|
||||||
@@ -1044,8 +1079,21 @@ async function runTextOnlyBatch(
|
|||||||
const content = getAnalysisContent(msg);
|
const content = getAnalysisContent(msg);
|
||||||
const textEvidence = textEvidenceMap.get(msg.id) ?? "";
|
const textEvidence = textEvidenceMap.get(msg.id) ?? "";
|
||||||
const textContext = textEvidence ? `\n${textEvidence}` : "";
|
const textContext = textEvidence ? `\n${textEvidence}` : "";
|
||||||
|
|
||||||
|
// Inject fetched web content for URLs found in this message
|
||||||
|
const msgUrls = extractUrlsFromText(content);
|
||||||
|
const urlContexts = msgUrls
|
||||||
|
.map((url) => {
|
||||||
|
const fetchedText = urlFetchMap.get(url);
|
||||||
|
if (!fetchedText) return null;
|
||||||
|
return `<web_content url="${url}">${fetchedText}</web_content>`;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
const webContext = urlContexts ? `\n${urlContexts}` : "";
|
||||||
|
|
||||||
// XML delimiters wrap each message for prompt safety (R1)
|
// XML delimiters wrap each message for prompt safety (R1)
|
||||||
return `<message id="${msg.id}" user="${msg.username}">${content}${textContext}</message>`;
|
return `<message id="${msg.id}" user="${msg.username}">${content}${textContext}${webContext}</message>`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user