refactor: remove unused getThreads function and related code from voice API and controller
This commit is contained in:
@@ -13,10 +13,6 @@ export function getTextChannels(guildId: string): Promise<Channel[]> {
|
|||||||
return request<Channel[]>(`/api/guilds/${guildId}/channels`);
|
return request<Channel[]>(`/api/guilds/${guildId}/channels`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThreads(guildId: string): Promise<Channel[]> {
|
|
||||||
return request<Channel[]>(`/api/guilds/${guildId}/threads`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVoiceStatus(): Promise<VoiceStatus> {
|
export function getVoiceStatus(): Promise<VoiceStatus> {
|
||||||
return request<VoiceStatus>('/api/status');
|
return request<VoiceStatus>('/api/status');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
disconnectVoice,
|
disconnectVoice,
|
||||||
getGuilds,
|
getGuilds,
|
||||||
getTextChannels,
|
getTextChannels,
|
||||||
getThreads,
|
|
||||||
getVoiceChannels,
|
getVoiceChannels,
|
||||||
getVoiceStatus,
|
getVoiceStatus,
|
||||||
} from "../api/voice";
|
} from "../api/voice";
|
||||||
@@ -46,13 +45,9 @@ export function useVoiceControl() {
|
|||||||
setTextChannels([]);
|
setTextChannels([]);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [channels, threads] = await Promise.all([
|
const channels = await getTextChannels(guildId);
|
||||||
getTextChannels(guildId),
|
setTextChannels(channels);
|
||||||
getThreads(guildId).catch(() => []),
|
return channels;
|
||||||
]);
|
|
||||||
const combined = [...channels, ...threads];
|
|
||||||
setTextChannels(combined);
|
|
||||||
return combined;
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const joinVoice = useCallback(async (guildId: string, channelId: string) => {
|
const joinVoice = useCallback(async (guildId: string, channelId: string) => {
|
||||||
|
|||||||
@@ -34,11 +34,17 @@ async function processAnalysisRequest({
|
|||||||
conversationKey,
|
conversationKey,
|
||||||
messages,
|
messages,
|
||||||
}: AnalysisWorkerRequest): Promise<AnalysisWorkerResponse> {
|
}: AnalysisWorkerRequest): Promise<AnalysisWorkerResponse> {
|
||||||
|
try {
|
||||||
try {
|
try {
|
||||||
if (!dbInitialized) {
|
if (!dbInitialized) {
|
||||||
await initializeDatabase();
|
await initializeDatabase();
|
||||||
dbInitialized = true;
|
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];
|
const firstMessage = messages[0];
|
||||||
if (!firstMessage) return { ok: true, conversationKey, rows: [] };
|
if (!firstMessage) return { ok: true, conversationKey, rows: [] };
|
||||||
|
|
||||||
|
|||||||
@@ -26,35 +26,39 @@ export function parseModerationResponse(
|
|||||||
content: string,
|
content: string,
|
||||||
targetIds: string[],
|
targetIds: string[],
|
||||||
): AnalysisResult[] {
|
): AnalysisResult[] {
|
||||||
// Find first opening brace
|
// Find first opening brace and last closing brace
|
||||||
const startIdx = content.indexOf("{");
|
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");
|
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 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 {
|
try {
|
||||||
parsed = JSON.parse(candidate);
|
parsed = JSON.parse(candidate);
|
||||||
// Successfully parsed, break out
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Store error and continue scanning
|
// If full substring fails, try scanning backwards from the last }
|
||||||
lastError = 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--) {
|
||||||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
throw new Error(
|
throw new Error(`Failed to parse JSON: ${lastError.message}`);
|
||||||
`Failed to parse JSON: ${lastError?.message || "No valid JSON object found"}`,
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate structure
|
// Validate structure
|
||||||
@@ -219,7 +223,18 @@ Return ONLY valid JSON, no other text.`;
|
|||||||
throw new Error(`LLM API error ${response.status}: ${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 {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// POST /api/connect - Connect to a voice channel
|
||||||
router.post("/connect", async (req, res, next) => {
|
router.post("/connect", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -83,46 +83,6 @@ export class VoiceController {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
async listThreads(guildId: string): Promise<ChannelSummary[]> {
|
|
||||||
const guild = this.getGuild(guildId);
|
|
||||||
await guild.channels.fetch().catch(() => null);
|
|
||||||
|
|
||||||
const threads: ChannelSummary[] = [];
|
|
||||||
type ThreadFetchResult = {
|
|
||||||
threads: Map<string, { id: string; name: string; type: string }>;
|
|
||||||
};
|
|
||||||
for (const channel of guild.channels.cache.values()) {
|
|
||||||
const threadParent = channel as typeof channel & {
|
|
||||||
threads?: {
|
|
||||||
fetch: (options: {
|
|
||||||
archived: boolean;
|
|
||||||
limit: number;
|
|
||||||
}) => Promise<ThreadFetchResult>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
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<VoiceStatus> {
|
async connect(guildId: string, channelId: string): Promise<VoiceStatus> {
|
||||||
if (!this.client.isReady()) {
|
if (!this.client.isReady()) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
|
|||||||
67
tests/moderation/llmLive.test.ts
Normal file
67
tests/moderation/llmLive.test.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user