khojai.mjs

AI assistant real-time with multi-modal support.

#ai#realtime#chatbot
26
12 Jan 2026, 08:59
RawEdit
javascript0 lines
/***
  @ Base: https://app.khoj.dev/
  @ Author: Shannz
  @ Note: AI assistant real-time with multi-modal support.
***/

import axios from 'axios';
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
import WebSocket from 'ws';
import fs from 'fs';
import path from 'path';
import mime from 'mime-types';
import FormData from 'form-data';

const CONFIG = {
    BASE_URL: "https://app.khoj.dev",
    API: {
        SESSION: "/api/chat/sessions",
        WS: "wss://app.khoj.dev/api/chat/ws",
        CONVERT: "/api/content/convert"
    },
    DEFAULT_COOKIE: 'PASTE_COOKIE_DISINI', 
    HEADERS: {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Origin": "https://app.khoj.dev",
        "Referer": "https://app.khoj.dev/"
    },
    CLIENT_PARAMS: {
        client: "web",
        agent_slug: "khoj"
    }
};

const createClient = (cookieStr) => {
    const jar = new CookieJar();
    if (cookieStr) {
        jar.setCookieSync(cookieStr, CONFIG.BASE_URL);
    }
    return wrapper(axios.create({ jar }));
};

const fileUtils = {
    processImages: (imagePaths) => {
        const images = [];
        for (const filePath of imagePaths) {
            if (fs.existsSync(filePath)) {
                const fileBuffer = fs.readFileSync(filePath);
                const mimeType = mime.lookup(filePath) || 'image/jpeg';
                const base64 = fileBuffer.toString('base64');
                images.push(`data:${mimeType};base64,${base64}`);
            } else {
                console.warn(`[Warn] Gambar tidak ditemukan: ${filePath}`);
            }
        }
        return images;
    },

    processFiles: async (filePaths, client) => {
        if (!filePaths || filePaths.length === 0) return [];
        
        try {
            const form = new FormData();
            let validFiles = 0;

            for (const filePath of filePaths) {
                if (fs.existsSync(filePath)) {
                    form.append('files', fs.createReadStream(filePath));
                    validFiles++;
                } else {
                    console.warn(`[Warn] File tidak ditemukan: ${filePath}`);
                }
            }

            if (validFiles === 0) return [];

            const res = await client.post(CONFIG.BASE_URL + CONFIG.API.CONVERT, form, {
                headers: {
                    ...CONFIG.HEADERS,
                    ...form.getHeaders()
                }
            });

            return res.data; 

        } catch (error) {
            console.error(`[Error] Gagal convert file: ${error.message}`);
            return [];
        }
    }
};

export const khoj = {
    chat: async (query, options = {}) => {
        const { 
            chatId: initialChatId = null, 
            cookie = CONFIG.DEFAULT_COOKIE,
            images = [],
            files = []
        } = options;

        return new Promise(async (resolve, reject) => {
            const client = createClient(cookie);
            let chatId = initialChatId;
            
            let finalResult = {
                chatId: chatId,
                query: query,
                answer: "",
                tools: [],
                searchQueries: [],
                sources: [],
                debug: { images_sent: 0, files_sent: 0 }
            };
            
            let safetyTimer;

            try {
                if (!chatId) {
                    try {
                        const sessionRes = await client.post(
                            `${CONFIG.BASE_URL}${CONFIG.API.SESSION}`, 
                            {}, 
                            { 
                                params: CONFIG.CLIENT_PARAMS,
                                headers: { ...CONFIG.HEADERS, 'Cookie': cookie }
                            }
                        );
                        chatId = sessionRes.data.conversation_id;
                        finalResult.chatId = chatId;
                    } catch (err) {
                        return reject({ status: 'error', message: `Gagal membuat sesi: ${err.message}` });
                    }
                }

                const [processedImages, processedFiles] = await Promise.all([
                    images.length > 0 ? fileUtils.processImages(images) : [],
                    files.length > 0 ? fileUtils.processFiles(files, client) : []
                ]);

                finalResult.debug.images_sent = processedImages.length;
                finalResult.debug.files_sent = processedFiles.length;

                const wsUrl = `${CONFIG.API.WS}?client=web`;
                const ws = new WebSocket(wsUrl, {
                    headers: { 'Cookie': cookie, 'Origin': CONFIG.BASE_URL }
                });

                safetyTimer = setTimeout(() => {
                    if (ws.readyState === WebSocket.OPEN) {
                        ws.close();
                        reject({ status: 'timeout', message: 'No response received within 60s' });
                    }
                }, 60000);

                ws.on('open', () => {
                    const payload = {
                        "q": query,
                        "conversation_id": chatId,
                        "stream": true, 
                        "city": "Malang", "region": "East Java", "country": "ID", "timezone": "Asia/Jakarta"
                    };

                    if (processedImages.length > 0) {
                        payload.images = processedImages;
                    }

                    if (processedFiles.length > 0) {
                        payload.files = processedFiles;
                    }

                    ws.send(JSON.stringify(payload));
                });

                ws.on('message', (rawData) => {
                    let str = rawData.toString().trim();
                    str = str.replace(/␃|🔚|␗/g, "").trim();
                    if (!str) return;

                    try {
                        const json = JSON.parse(str);

                        if (json.type === "metadata") {
                            finalResult.chatId = json.data.conversationId;
                        } 
                        else if (json.type === "status") {
                            if (json.data.includes("Selected Tools:")) {
                                const toolText = json.data.replace("**Selected Tools:** ", "");
                                finalResult.tools = toolText.split(", ");
                            }
                            if (json.data.includes("Searching the web for")) {
                                const queries = json.data.split("\n- ").slice(1);
                                finalResult.searchQueries = queries.map(q => q.trim());
                            }
                        } 
                        else if (json.type === "references") {
                            const onlineContext = json.data.onlineContext;
                            if (onlineContext) {
                                Object.values(onlineContext).forEach(val => {
                                    if (val.organic) {
                                        val.organic.forEach(item => {
                                            finalResult.sources.push({ title: item.title, url: item.link });
                                        });
                                    }
                                });
                            }
                        }
                        else if (json.type === "end_response") {
                            finalResult.answer = finalResult.answer.trim();
                            clearTimeout(safetyTimer);
                            ws.close();
                            resolve(finalResult);
                        }

                    } catch (e) {
                        if (!str.startsWith('{"type":') && !str.includes('"turnId"')) {
                            finalResult.answer += str;
                        }
                    }
                });

                ws.on('error', (err) => {
                    clearTimeout(safetyTimer);
                    ws.close();
                    reject({ status: 'error', message: `WebSocket Error: ${err.message}` });
                });
                
                ws.on('close', () => {
                    clearTimeout(safetyTimer);
                });

            } catch (error) {
                clearTimeout(safetyTimer);
                reject({ status: 'error', message: `Fatal Error: ${error.message}` });
            }
        });
    }
};

/*(async () => {
    try {
        console.log("Mengirim request...");
        // Contoh: Kirim Text + Gambar + File Code
        const result = await khoj.chat("gambar apa ini? dan apa fungsi file ini?", {
            images: ['./orang.jpg'],
            files: ['./script.py']
        });
        console.log(result);
    } catch (error) {
        console.error("Error:", error);
    }
})();*/