diff --git a/frontend/src/api/voice.ts b/frontend/src/api/voice.ts index d61b6d0..2611262 100644 --- a/frontend/src/api/voice.ts +++ b/frontend/src/api/voice.ts @@ -13,10 +13,6 @@ export function getTextChannels(guildId: string): Promise { return request(`/api/guilds/${guildId}/channels`); } -export function getThreads(guildId: string): Promise { - return request(`/api/guilds/${guildId}/threads`); -} - export function getVoiceStatus(): Promise { return request('/api/status'); } diff --git a/frontend/src/hooks/useVoiceControl.ts b/frontend/src/hooks/useVoiceControl.ts index 556acc2..dd7438a 100644 --- a/frontend/src/hooks/useVoiceControl.ts +++ b/frontend/src/hooks/useVoiceControl.ts @@ -4,7 +4,6 @@ import { disconnectVoice, getGuilds, getTextChannels, - getThreads, getVoiceChannels, getVoiceStatus, } from "../api/voice"; @@ -46,13 +45,9 @@ export function useVoiceControl() { setTextChannels([]); return []; } - const [channels, threads] = await Promise.all([ - getTextChannels(guildId), - getThreads(guildId).catch(() => []), - ]); - const combined = [...channels, ...threads]; - setTextChannels(combined); - return combined; + const channels = await getTextChannels(guildId); + setTextChannels(channels); + return channels; }, []); const joinVoice = useCallback(async (guildId: string, channelId: string) => { diff --git a/src/moderation/aiAnalysisWorker.ts b/src/moderation/aiAnalysisWorker.ts index 24619ae..1d5fa17 100644 --- a/src/moderation/aiAnalysisWorker.ts +++ b/src/moderation/aiAnalysisWorker.ts @@ -35,10 +35,16 @@ async function processAnalysisRequest({ messages, }: AnalysisWorkerRequest): Promise { try { - if (!dbInitialized) { - await initializeDatabase(); - dbInitialized = true; + try { + if (!dbInitialized) { + await initializeDatabase(); + dbInitialized = true; + } + } catch (dbError) { + const msg = dbError instanceof Error ? dbError.message : String(dbError); + return { ok: false, conversationKey, rows: [], error: `Database init failed: ${msg}` }; } + const firstMessage = messages[0]; if (!firstMessage) return { ok: true, conversationKey, rows: [] }; diff --git a/src/moderation/llmModerationClient.ts b/src/moderation/llmModerationClient.ts index 52b6b79..5008264 100644 --- a/src/moderation/llmModerationClient.ts +++ b/src/moderation/llmModerationClient.ts @@ -26,35 +26,39 @@ export function parseModerationResponse( content: string, targetIds: string[], ): AnalysisResult[] { - // Find first opening brace + // Find first opening brace and last closing brace const startIdx = content.indexOf("{"); - if (startIdx === -1) { + const endIdx = content.lastIndexOf("}"); + + if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { throw new Error("No JSON object found in response"); } - // Scan from start and try parsing at each closing brace + // Attempt to parse the largest possible JSON object let parsed: unknown; - let lastError: Error | null = null; + const candidate = content.substring(startIdx, endIdx + 1); - for (let i = startIdx + 1; i < content.length; i++) { - if (content[i] === "}") { - const candidate = content.substring(startIdx, i + 1); - try { - parsed = JSON.parse(candidate); - // Successfully parsed, break out - break; - } catch (error) { - // Store error and continue scanning - lastError = error instanceof Error ? error : new Error(String(error)); - continue; + try { + parsed = JSON.parse(candidate); + } catch (error) { + // If full substring fails, try scanning backwards from the last } + let lastError: Error = error instanceof Error ? error : new Error(String(error)); + + for (let i = endIdx - 1; i > startIdx; i--) { + if (content[i] === "}") { + try { + parsed = JSON.parse(content.substring(startIdx, i + 1)); + break; + } catch (innerError) { + lastError = innerError instanceof Error ? innerError : new Error(String(innerError)); + continue; + } } } - } - if (!parsed) { - throw new Error( - `Failed to parse JSON: ${lastError?.message || "No valid JSON object found"}`, - ); + if (!parsed) { + throw new Error(`Failed to parse JSON: ${lastError.message}`); + } } // Validate structure @@ -219,7 +223,18 @@ Return ONLY valid JSON, no other text.`; throw new Error(`LLM API error ${response.status}: ${text}`); } - return response.json(); + const bodyText = await response.text(); + try { + return JSON.parse(bodyText); + } catch (e) { + // Handle cases where the API provider returns trailing garbage + const start = bodyText.indexOf("{"); + const end = bodyText.lastIndexOf("}"); + if (start !== -1 && end !== -1 && end > start) { + return JSON.parse(bodyText.substring(start, end + 1)); + } + throw e; + } } finally { clearTimeout(timeoutId); } diff --git a/src/routes/voiceRoutes.ts b/src/routes/voiceRoutes.ts index 851394f..f20280d 100644 --- a/src/routes/voiceRoutes.ts +++ b/src/routes/voiceRoutes.ts @@ -89,22 +89,6 @@ export function createVoiceRoutes( } }); - // GET /api/guilds/:guildId/threads - List threads in a guild - router.get("/guilds/:guildId/threads", async (req, res, next) => { - try { - const { guildId } = req.params; - - if (!guildId) { - throw new AppError("Guild ID is required", "MISSING_GUILD_ID", 400); - } - - const threads = await voiceController.listThreads(guildId); - res.json(threads); - } catch (error) { - next(error); - } - }); - // POST /api/connect - Connect to a voice channel router.post("/connect", async (req, res, next) => { try { diff --git a/src/voiceController.ts b/src/voiceController.ts index a0d308d..ee2b95f 100644 --- a/src/voiceController.ts +++ b/src/voiceController.ts @@ -83,46 +83,6 @@ export class VoiceController { .sort((a, b) => a.name.localeCompare(b.name)); } - async listThreads(guildId: string): Promise { - const guild = this.getGuild(guildId); - await guild.channels.fetch().catch(() => null); - - const threads: ChannelSummary[] = []; - type ThreadFetchResult = { - threads: Map; - }; - for (const channel of guild.channels.cache.values()) { - const threadParent = channel as typeof channel & { - threads?: { - fetch: (options: { - archived: boolean; - limit: number; - }) => Promise; - }; - }; - if (!threadParent.threads?.fetch) continue; - - for (const archived of [false, true]) { - const fetched = await threadParent.threads - .fetch({ archived, limit: 100 }) - .catch(() => null); - if (!fetched?.threads) continue; - - for (const thread of fetched.threads.values()) { - threads.push({ - id: thread.id, - name: `${channel.name} / ${thread.name}`, - type: thread.type, - }); - } - } - } - - return Array.from( - new Map(threads.map((thread) => [thread.id, thread])).values(), - ).sort((a, b) => a.name.localeCompare(b.name)); - } - async connect(guildId: string, channelId: string): Promise { if (!this.client.isReady()) { throw new AppError( diff --git a/tests/moderation/llmLive.test.ts b/tests/moderation/llmLive.test.ts new file mode 100644 index 0000000..d5072b1 --- /dev/null +++ b/tests/moderation/llmLive.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { runModerationAnalysis } from "../../src/moderation/llmModerationClient"; +import { config } from "../../src/config"; +import type { MessageRecord } from "../../src/moderation/types"; + +describe("LLM Live Integration Test", () => { + // Hanya jalankan jika API Key tersedia + 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 () => { + console.log(`Using Model: ${config.AI_LLM_MODEL}`); + console.log(`Base URL: ${config.AI_LLM_BASE_URL}`); + + const mockMessages: MessageRecord[] = [ + { + id: "test-msg-1", + guild_id: "guild-1", + channel_id: "channel-1", + thread_id: null, + user_id: "user-1", + username: "Tester", + avatar_url: null, + content: "This is a clean test message.", + edited_content: null, + created_at: Date.now(), + edited_at: null, + deleted_at: null, + type: "text", + metadata: null + }, + { + id: "test-msg-2", + guild_id: "guild-1", + channel_id: "channel-1", + thread_id: null, + user_id: "user-2", + username: "BadActor", + avatar_url: null, + content: "I will kill you and steal your data! DIE!", + edited_content: null, + created_at: Date.now() + 1000, + edited_at: null, + deleted_at: null, + type: "text", + metadata: null + } + ]; + + const result = await runModerationAnalysis({ + targets: mockMessages, + contextText: "Testing moderation system stability." + }); + + console.log("Raw Response received (first 100 chars):", JSON.stringify(result.raw).substring(0, 100)); + + expect(result.results).toHaveLength(2); + + const cleanMsg = result.results.find(r => r.messageId === "test-msg-1"); + const badMsg = result.results.find(r => r.messageId === "test-msg-2"); + + expect(cleanMsg?.status).toBe("clean"); + expect(["warn", "flagged"]).toContain(badMsg?.status); + + console.log("Clean Message Result:", cleanMsg); + console.log("Bad Message Result:", badMsg); + }, 30000); // 30s timeout untuk LLM +});