feat: enhance screen share controller with Streamer integration and voice channel management
This commit is contained in:
57
debug-screen.ts
Normal file
57
debug-screen.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Client } from "discord.js-selfbot-v13";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { createYtDlp } from "./src/media/ytdlp.js";
|
||||||
|
import { Streamer } from "./vendor/Discord-video-stream/dist/client/index.js";
|
||||||
|
import {
|
||||||
|
playStream,
|
||||||
|
prepareStream,
|
||||||
|
} from "./vendor/Discord-video-stream/dist/media/newApi.js";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function test() {
|
||||||
|
const ytdlp = createYtDlp();
|
||||||
|
const url = "https://www.youtube.com/watch?v=aqz-KE-bpKQ"; // Small video
|
||||||
|
|
||||||
|
console.log("Getting direct video url...");
|
||||||
|
const directUrl = await ytdlp.getDirectVideoUrl(url);
|
||||||
|
console.log("Direct URL:", directUrl);
|
||||||
|
|
||||||
|
console.log("Preparing stream...");
|
||||||
|
const { command, output } = prepareStream(directUrl, {
|
||||||
|
logLevel: "debug",
|
||||||
|
customInputOptions: [
|
||||||
|
"-headers",
|
||||||
|
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\r\nConnection: keep-alive\r\n",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
command.on("stderr", (data) => {
|
||||||
|
console.log("FFMPEG STDERR:", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Testing demux manually...");
|
||||||
|
const { demux } = await import(
|
||||||
|
"./vendor/Discord-video-stream/dist/media/LibavDemuxer.js"
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const demuxPromise = demux(output, { format: "nut" });
|
||||||
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("Demux timeout")), 15000),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { video, audio } = (await Promise.race([
|
||||||
|
demuxPromise,
|
||||||
|
timeoutPromise,
|
||||||
|
])) as any;
|
||||||
|
console.log("Demux success!");
|
||||||
|
console.log("Video stream:", !!video);
|
||||||
|
console.log("Audio stream:", !!audio);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Demux failed:", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test();
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { Readable } from "node:stream";
|
import type { Readable } from "node:stream";
|
||||||
|
import type { WebRtcConnWrapper } from "@dank074/discord-video-stream";
|
||||||
import {
|
import {
|
||||||
playStream as defaultPlayStream,
|
playStream as defaultPlayStream,
|
||||||
prepareStream as defaultPrepareStream,
|
prepareStream as defaultPrepareStream,
|
||||||
Encoders,
|
Encoders,
|
||||||
|
Streamer,
|
||||||
Utils,
|
Utils,
|
||||||
} from "@dank074/discord-video-stream";
|
} from "@dank074/discord-video-stream";
|
||||||
import { AppError } from "../errors";
|
import { AppError } from "../errors";
|
||||||
@@ -10,6 +12,7 @@ import { createChildLogger } from "../logger";
|
|||||||
import { discordPlayer } from "../player";
|
import { discordPlayer } from "../player";
|
||||||
|
|
||||||
const logger = createChildLogger("screen-share");
|
const logger = createChildLogger("screen-share");
|
||||||
|
|
||||||
import type { DiscordPlayerOwner, ScreenSharePlayback } from "./mediaTypes";
|
import type { DiscordPlayerOwner, ScreenSharePlayback } from "./mediaTypes";
|
||||||
import { createYtDlp } from "./ytdlp";
|
import { createYtDlp } from "./ytdlp";
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ type PrepareScreenStream = (
|
|||||||
|
|
||||||
type PlayScreenStream = (
|
type PlayScreenStream = (
|
||||||
output: Readable,
|
output: Readable,
|
||||||
streamer: unknown,
|
streamer: Streamer,
|
||||||
options: { type: "go-live" },
|
options: { type: "go-live" },
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
@@ -41,7 +44,13 @@ export interface ScreenShareControllerDependencies {
|
|||||||
getDirectVideoUrl?: (source: string) => Promise<string>;
|
getDirectVideoUrl?: (source: string) => Promise<string>;
|
||||||
prepareStream?: PrepareScreenStream;
|
prepareStream?: PrepareScreenStream;
|
||||||
playStream?: PlayScreenStream;
|
playStream?: PlayScreenStream;
|
||||||
streamer: unknown;
|
streamer: Streamer;
|
||||||
|
joinVoice?: (
|
||||||
|
guildId: string,
|
||||||
|
channelId: string,
|
||||||
|
) => Promise<WebRtcConnWrapper>;
|
||||||
|
onStreamStart?: () => void;
|
||||||
|
onStreamEnd?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createScreenShareController(
|
export function createScreenShareController(
|
||||||
@@ -55,11 +64,9 @@ export function createScreenShareController(
|
|||||||
dependencies.getDirectVideoUrl ??
|
dependencies.getDirectVideoUrl ??
|
||||||
((source) => ytdlp.getDirectVideoUrl(source));
|
((source) => ytdlp.getDirectVideoUrl(source));
|
||||||
const prepareStream =
|
const prepareStream =
|
||||||
dependencies.prepareStream ??
|
dependencies.prepareStream ?? (defaultPrepareStream as PrepareScreenStream);
|
||||||
(defaultPrepareStream as unknown as PrepareScreenStream);
|
|
||||||
const playStream =
|
const playStream =
|
||||||
dependencies.playStream ??
|
dependencies.playStream ?? (defaultPlayStream as PlayScreenStream);
|
||||||
(defaultPlayStream as unknown as PlayScreenStream);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
@@ -68,6 +75,12 @@ export function createScreenShareController(
|
|||||||
|
|
||||||
async start(source: string): Promise<ScreenSharePlayback> {
|
async start(source: string): Promise<ScreenSharePlayback> {
|
||||||
const status = dependencies.getVoiceStatus();
|
const status = dependencies.getVoiceStatus();
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
active.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure bot is in the voice channel via Streamer for video streaming
|
||||||
if (
|
if (
|
||||||
!status.connected ||
|
!status.connected ||
|
||||||
!status.activeGuildId ||
|
!status.activeGuildId ||
|
||||||
@@ -80,11 +93,17 @@ export function createScreenShareController(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active) {
|
try {
|
||||||
active.stop();
|
// Join voice via Streamer if not already connected for streaming
|
||||||
|
if (dependencies.joinVoice) {
|
||||||
|
logger.info("Joining voice channel for screen share via Streamer");
|
||||||
|
await dependencies.joinVoice(
|
||||||
|
status.activeGuildId,
|
||||||
|
status.activeChannelId,
|
||||||
|
);
|
||||||
|
logger.info("Voice channel joined via Streamer for screen share");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const directUrl = await getDirectVideoUrl(source);
|
const directUrl = await getDirectVideoUrl(source);
|
||||||
const { command, output } = prepareStream(directUrl, {
|
const { command, output } = prepareStream(directUrl, {
|
||||||
encoder: Encoders.software({ x264: { preset: "superfast" } }),
|
encoder: Encoders.software({ x264: { preset: "superfast" } }),
|
||||||
@@ -105,11 +124,14 @@ export function createScreenShareController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies.onStreamStart?.();
|
||||||
|
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
const done = playStream(output, dependencies.streamer, {
|
const done = playStream(output, dependencies.streamer, {
|
||||||
type: "go-live",
|
type: "go-live",
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
active = null;
|
active = null;
|
||||||
|
dependencies.onStreamEnd?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
active = {
|
active = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
// Mock node-crc to provide pure JS implementation and bypass native build issues
|
// Mock node-crc to provide pure JS implementation and bypass native build issues
|
||||||
@@ -43,4 +44,5 @@ Module.prototype.require = function (id: string) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log("[mock] node-crc has been mocked globally for ESM.");
|
console.log("[mock] node-crc has been mocked globally for ESM.");
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -42,7 +42,12 @@ async function processAnalysisRequest({
|
|||||||
}
|
}
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
const msg = dbError instanceof Error ? dbError.message : String(dbError);
|
const msg = dbError instanceof Error ? dbError.message : String(dbError);
|
||||||
return { ok: false, conversationKey, rows: [], error: `Database init failed: ${msg}` };
|
return {
|
||||||
|
ok: false,
|
||||||
|
conversationKey,
|
||||||
|
rows: [],
|
||||||
|
error: `Database init failed: ${msg}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstMessage = messages[0];
|
const firstMessage = messages[0];
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export function parseModerationResponse(
|
|||||||
parsed = JSON.parse(candidate);
|
parsed = JSON.parse(candidate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If full substring fails, try scanning backwards from the last }
|
// If full substring fails, try scanning backwards from the last }
|
||||||
let lastError: Error = error instanceof Error ? error : new Error(String(error));
|
let lastError: Error =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
for (let i = endIdx - 1; i > startIdx; i--) {
|
for (let i = endIdx - 1; i > startIdx; i--) {
|
||||||
if (content[i] === "}") {
|
if (content[i] === "}") {
|
||||||
@@ -50,7 +51,10 @@ export function parseModerationResponse(
|
|||||||
parsed = JSON.parse(content.substring(startIdx, i + 1));
|
parsed = JSON.parse(content.substring(startIdx, i + 1));
|
||||||
break;
|
break;
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
lastError = innerError instanceof Error ? innerError : new Error(String(innerError));
|
lastError =
|
||||||
|
innerError instanceof Error
|
||||||
|
? innerError
|
||||||
|
: new Error(String(innerError));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +113,10 @@ export function parseModerationResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (foundIds.has(finalId)) {
|
if (foundIds.has(finalId)) {
|
||||||
log.warn({ duplicateId: finalId }, "Skipping duplicate/rounded message_id");
|
log.warn(
|
||||||
|
{ duplicateId: finalId },
|
||||||
|
"Skipping duplicate/rounded message_id",
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,21 @@ export function createMediaRoutes(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.get("/media/status", (_req: Request, res: Response, next: NextFunction) => {
|
router.get(
|
||||||
|
"/media/status",
|
||||||
|
(_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
res.json(controller.getState());
|
res.json(controller.getState());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/media/queue", adminAuth, async (req: Request, res: Response, next: NextFunction) => {
|
router.post(
|
||||||
|
"/media/queue",
|
||||||
|
adminAuth,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { source, mode = "music" } = req.body as {
|
const { source, mode = "music" } = req.body as {
|
||||||
source?: string;
|
source?: string;
|
||||||
@@ -58,23 +64,32 @@ export function createMediaRoutes(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/media/skip", adminAuth, async (_req: Request, res: Response, next: NextFunction) => {
|
router.post(
|
||||||
|
"/media/skip",
|
||||||
|
adminAuth,
|
||||||
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
res.json(await controller.skip());
|
res.json(await controller.skip());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/media/stop", adminAuth, async (_req: Request, res: Response, next: NextFunction) => {
|
router.post(
|
||||||
|
"/media/stop",
|
||||||
|
adminAuth,
|
||||||
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
res.json(await controller.stop());
|
res.json(await controller.stop());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ export function createVoiceRoutes(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/guilds/:guildId/voice-channels - List voice channels in a guild
|
// GET /api/guilds/:guildId/voice-channels - List voice channels in a guild
|
||||||
router.get("/guilds/:guildId/voice-channels", async (req: Request, res: Response, next: NextFunction) => {
|
router.get(
|
||||||
|
"/guilds/:guildId/voice-channels",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { guildId } = req.params;
|
const { guildId } = req.params;
|
||||||
|
|
||||||
@@ -79,15 +81,20 @@ export function createVoiceRoutes(
|
|||||||
throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400);
|
throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const channels = await voiceController.listVoiceChannels(guildId as string);
|
const channels = await voiceController.listVoiceChannels(
|
||||||
|
guildId as string,
|
||||||
|
);
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// GET /api/guilds/:guildId/channels - List text channels in a guild
|
// GET /api/guilds/:guildId/channels - List text channels in a guild
|
||||||
router.get("/guilds/:guildId/channels", async (req: Request, res: Response, next: NextFunction) => {
|
router.get(
|
||||||
|
"/guilds/:guildId/channels",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { guildId } = req.params;
|
const { guildId } = req.params;
|
||||||
|
|
||||||
@@ -95,15 +102,21 @@ export function createVoiceRoutes(
|
|||||||
throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400);
|
throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const channels = await voiceController.listWatchableChannels(guildId as string);
|
const channels = await voiceController.listWatchableChannels(
|
||||||
|
guildId as string,
|
||||||
|
);
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// POST /api/connect - Connect to a voice channel
|
// POST /api/connect - Connect to a voice channel
|
||||||
router.post("/connect", adminAuth, async (req: Request, res: Response, next: NextFunction) => {
|
router.post(
|
||||||
|
"/connect",
|
||||||
|
adminAuth,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { guildId, channelId } = req.body as {
|
const { guildId, channelId } = req.body as {
|
||||||
guildId?: string;
|
guildId?: string;
|
||||||
@@ -135,10 +148,14 @@ export function createVoiceRoutes(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// POST /api/disconnect - Disconnect from voice channel
|
// POST /api/disconnect - Disconnect from voice channel
|
||||||
router.post("/disconnect", adminAuth, async (_req: Request, res: Response, next: NextFunction) => {
|
router.post(
|
||||||
|
"/disconnect",
|
||||||
|
adminAuth,
|
||||||
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
logger.info("Disconnecting from voice channel");
|
logger.info("Disconnecting from voice channel");
|
||||||
|
|
||||||
@@ -157,7 +174,8 @@ export function createVoiceRoutes(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { Streamer } from "@dank074/discord-video-stream";
|
import { Streamer } from "@dank074/discord-video-stream";
|
||||||
import { AudioPlayerStatus } from "@discordjs/voice";
|
import { AudioPlayerStatus } from "@discordjs/voice";
|
||||||
import type { Client } from "discord.js-selfbot-v13";
|
import type { Client } from "discord.js-selfbot-v13";
|
||||||
import { config } from "./config";
|
|
||||||
import express, {
|
import express, {
|
||||||
type NextFunction,
|
type NextFunction,
|
||||||
type Request,
|
type Request,
|
||||||
@@ -14,6 +13,7 @@ import express, {
|
|||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import * as prism from "prism-media";
|
import * as prism from "prism-media";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
|
import { config } from "./config";
|
||||||
import { AppError } from "./errors";
|
import { AppError } from "./errors";
|
||||||
import { createChildLogger, logger } from "./logger";
|
import { createChildLogger, logger } from "./logger";
|
||||||
import { MediaController } from "./media/mediaController";
|
import { MediaController } from "./media/mediaController";
|
||||||
@@ -122,7 +122,9 @@ function patchSharedUIState(patch: SharedUIStatePatch) {
|
|||||||
if (typeof patch.selectedTextChannel === "string") {
|
if (typeof patch.selectedTextChannel === "string") {
|
||||||
sharedUIState.selectedTextChannel = patch.selectedTextChannel;
|
sharedUIState.selectedTextChannel = patch.selectedTextChannel;
|
||||||
}
|
}
|
||||||
if (["voice", "messages", "media", "review"].includes(patch.activeTab ?? "")) {
|
if (
|
||||||
|
["voice", "messages", "media", "review"].includes(patch.activeTab ?? "")
|
||||||
|
) {
|
||||||
sharedUIState.activeTab = patch.activeTab as
|
sharedUIState.activeTab = patch.activeTab as
|
||||||
| "voice"
|
| "voice"
|
||||||
| "messages"
|
| "messages"
|
||||||
@@ -189,6 +191,8 @@ export async function startWebserver(
|
|||||||
const screenController = createScreenShareController({
|
const screenController = createScreenShareController({
|
||||||
getVoiceStatus: () => voiceController.getStatus(),
|
getVoiceStatus: () => voiceController.getStatus(),
|
||||||
streamer,
|
streamer,
|
||||||
|
joinVoice: (guildId: string, channelId: string) =>
|
||||||
|
streamer.joinVoice(guildId, channelId),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mediaController = new MediaController({
|
const mediaController = new MediaController({
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { describe, it, expect, beforeAll } from "vitest";
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
import { runModerationAnalysis } from "../../src/moderation/llmModerationClient";
|
|
||||||
import { config } from "../../src/config";
|
import { config } from "../../src/config";
|
||||||
|
import { runModerationAnalysis } from "../../src/moderation/llmModerationClient";
|
||||||
import type { MessageRecord } from "../../src/moderation/types";
|
import type { MessageRecord } from "../../src/moderation/types";
|
||||||
|
|
||||||
describe("LLM Live Integration Test", () => {
|
describe("LLM Live Integration Test", () => {
|
||||||
// Hanya jalankan jika API Key tersedia
|
// Hanya jalankan jika API Key tersedia
|
||||||
const hasApiKey = !!config.AI_LLM_API_KEY && config.AI_LLM_API_KEY !== "your-api-key";
|
const hasApiKey =
|
||||||
|
!!config.AI_LLM_API_KEY && config.AI_LLM_API_KEY !== "your-api-key";
|
||||||
|
|
||||||
it.runIf(hasApiKey)("should successfully call real LLM API and parse response", async () => {
|
it.runIf(hasApiKey)(
|
||||||
|
"should successfully call real LLM API and parse response",
|
||||||
|
async () => {
|
||||||
console.log(`Using Model: ${config.AI_LLM_MODEL}`);
|
console.log(`Using Model: ${config.AI_LLM_MODEL}`);
|
||||||
console.log(`Base URL: ${config.AI_LLM_BASE_URL}`);
|
console.log(`Base URL: ${config.AI_LLM_BASE_URL}`);
|
||||||
|
|
||||||
@@ -26,7 +29,7 @@ describe("LLM Live Integration Test", () => {
|
|||||||
edited_at: null,
|
edited_at: null,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
type: "text",
|
type: "text",
|
||||||
metadata: null
|
metadata: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "test-msg-2",
|
id: "test-msg-2",
|
||||||
@@ -42,26 +45,31 @@ describe("LLM Live Integration Test", () => {
|
|||||||
edited_at: null,
|
edited_at: null,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
type: "text",
|
type: "text",
|
||||||
metadata: null
|
metadata: null,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await runModerationAnalysis({
|
const result = await runModerationAnalysis({
|
||||||
targets: mockMessages,
|
targets: mockMessages,
|
||||||
contextText: "Testing moderation system stability."
|
contextText: "Testing moderation system stability.",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Raw Response received (first 100 chars):", JSON.stringify(result.raw).substring(0, 100));
|
console.log(
|
||||||
|
"Raw Response received (first 100 chars):",
|
||||||
|
JSON.stringify(result.raw).substring(0, 100),
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.results).toHaveLength(2);
|
expect(result.results).toHaveLength(2);
|
||||||
|
|
||||||
const cleanMsg = result.results.find(r => r.messageId === "test-msg-1");
|
const cleanMsg = result.results.find((r) => r.messageId === "test-msg-1");
|
||||||
const badMsg = result.results.find(r => r.messageId === "test-msg-2");
|
const badMsg = result.results.find((r) => r.messageId === "test-msg-2");
|
||||||
|
|
||||||
expect(cleanMsg?.status).toBe("clean");
|
expect(cleanMsg?.status).toBe("clean");
|
||||||
expect(["warn", "flagged"]).toContain(badMsg?.status);
|
expect(["warn", "flagged"]).toContain(badMsg?.status);
|
||||||
|
|
||||||
console.log("Clean Message Result:", cleanMsg);
|
console.log("Clean Message Result:", cleanMsg);
|
||||||
console.log("Bad Message Result:", badMsg);
|
console.log("Bad Message Result:", badMsg);
|
||||||
}, 30000); // 30s timeout untuk LLM
|
},
|
||||||
|
30000,
|
||||||
|
); // 30s timeout untuk LLM
|
||||||
});
|
});
|
||||||
|
|||||||
2
vendor/Discord-video-stream
vendored
2
vendor/Discord-video-stream
vendored
Submodule vendor/Discord-video-stream updated: fb83645d73...134ae9288c
Reference in New Issue
Block a user