• XSS.stack #1 – первый литературный журнал от юзеров форума

Получаем доступ к топовым моделям LLM полностью бесплатно..

society

ripper
КИДАЛА
Регистрация
22.06.2025
Сообщения
150
Решения
1
Реакции
179
Пожалуйста, обратите внимание, что пользователь заблокирован
Всем привет, для решения бытовых задач мне требовалась хорошая модель LLM (например, новенький ChatGPT 5), но платить я за него не хотел и находился в поисках бесплатного решения. Наткнулся на один немецкий сайт https://sidekick.ki/, немного покопался и нашел уязвимость, благодаря которой мое желание бесплатно пользоваться новыми моделями LLM осуществилось, хоть и имеет некоторые нюансы.

Нюансы, с которыми я столкнулся:
1. Благодаря этому способу мы получаем доступ к множеству новых моделей LLM, но к сожалению, это всего лишь недельный Trial. Доступ к бесплатным моделям останется, а для возобновления доступа к новым необходимо будет создать новый аккаунт и повторить нижеописанные шаги. Можно будет полностью автоматизировать этот процесс: написать авторегер аккаунтов, который будет извлекать необходимые данные самостоятельно и возобновлять доступ к PRO подписке автоматически.
2. Токен авторизации живет не больше трех дней, его необходимо так же постоянно обновлять для стабильной работы. Решение выше - написать скрипт, который в автоматическом режиме будет извлекать и обновлять эти данные.

Больше нюансов за непродолжительное время использования не было выявлено.


Приступаем к установке:
Этап 1. Подготовка.

1. Переходим на сайт https://sidekick.ki/, создаем новый аккаунт и попадаем в главное меню:
1758801453575.png

1.2. Открываем среду разработчика (devtools) через F12 или ПКМ -> "Просмотр кода страницы (CTRL+U)"
1.3. Начинаем диалог с LLM, отправляем любое сообщение. Например 123123123.
1.4. Переходим во вкладку Networks и ищем запрос thread?forceCreate=true. Переходим в Headers и листаем ниже, ищем интересующий нас токен Autorization и копируем все значение, начиная с brearer.
1758801265250.png

1.5. После этого переходим в раздел Payload и ищем наш ID пользователя
1758801281914.png

Да, такое тоже может быть, что запрос thread?forceCreate=true не находится в списке запросов Network, поэтому не переживайте, на это так же есть решение.
1. Во вкладке Networks в поиске пишем thread и переходим по любому запросу, пока не найдем токен авторизации. Копируем его.
2. Для получения ID нажимаем на иконку нашего профиля и переходим по первому пункту.
1758801299714.png

И там видим наш ID пользователя
1758801308242.png
На этом этап подготовки завершен, мы получили все необходимые нам данные. Переходим к следующему этапу:

Этап 2. Установка.
Использовать полученные данные можно любыми методами, но я использую в целях решения своих внутренних задач, поэтому подготовил бэкэнд на Python 3.n + FastAPI.

2. Берем готовый код бэкэнда ниже и заполняем его нашими данными, которые получили из первого этапа.
Python:
from fastapi import FastAPI, HTTPException, UploadFile, File, Depends, Header, Request
from pydantic import BaseModel
import requests
import uuid
from typing import Optional, List, Dict
import time
import asyncio
import websockets
import json
from threading import Thread, Lock
import re
import sqlite3
from sqlite3 import Error
import datetime
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
    title="#FREELLM by society.",
    description="special for XSS.pro",
    version="1.0.0"
)

# --- НАСТРОЙКА CORS ---
origins = [
    "*",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

AUTHORIZATION = "сюда токен авторизации" # вместе с brearer
MY_PERSON_ID = "сюда юзер айди" # только айди
SOCKET_URL = "wss://intercom.tobit.cloud/ws/socket.io/?EIO=4&transport=websocket" # не меняем

MODELS_DATA = [
    {"name": "hermes-4-70b", "personId": "CAI-HM47B", "tobitId": 5362389, "usedModel": 72},
    {"name": "claude-3.5-haiku", "personId": "CAI-CLDHK", "tobitId": 5184880, "usedModel": 29},
    {"name": "claude-3.5-sonnet", "personId": "CAI-CLD35", "tobitId": 5145626, "usedModel": 15},
    {"name": "claude-3.7-sonnet", "personId": "CAI-CLD37", "tobitId": 5217590, "usedModel": 40},
    {"name": "claude-4-opus", "personId": "CAI-CLD4Z", "tobitId": 5270599, "usedModel": 58},
    {"name": "claude-4-sonnet", "personId": "CAI-CLD4S", "tobitId": 5270598, "usedModel": 57},
    {"name": "claude-opus-4.1", "personId": "CAI-CLD41", "tobitId": 5349587, "usedModel": 65},
    {"name": "deepseek-r1", "personId": "CAI-DPSK1", "tobitId": 5213560, "usedModel": 35},
    {"name": "gemini-2.0-flash", "personId": "CAI-GMN2F", "tobitId": 5193243, "usedModel": 33},
    {"name": "gemini-2.0-flash-thinking", "personId": "CAI-GMN2T", "tobitId": 5206944, "usedModel": 34},
    {"name": "gemini-2.5-flash", "personId": "CAI-GM25F", "tobitId": 5239517, "usedModel": 54},
    {"name": "gemini-2.5-flash-lite", "personId": "CAI-G25FL", "tobitId": 5335775, "usedModel": 63},
    {"name": "gemini-2.5-pro", "personId": "CAI-GM25P", "tobitId": 5229293, "usedModel": 44},
    {"name": "glm-4.5", "personId": "CAI-GLM45", "tobitId": 5357855, "usedModel": 70},
    {"name": "glm-4.5-air", "personId": "CAI-GL45A", "tobitId": 5357856, "usedModel": 71},
    {"name": "gpt-4.1", "personId": "CAI-GP410", "tobitId": 5236820, "usedModel": 47},
    {"name": "gpt-4.1-mini", "personId": "CAI-GP41M", "tobitId": 5236821, "usedModel": 48},
    {"name": "gpt-4.1-nano", "personId": "CAI-GP41N", "tobitId": 5236822, "usedModel": 49},
    {"name": "gpt-4o", "personId": "CAI-GPT4Z", "tobitId": 5127650, "usedModel": 11},
    {"name": "gpt-4o-mini", "personId": "CAI-GPT4M", "tobitId": 5155563, "usedModel": 19},
    {"name": "gpt-5", "personId": "CAI-GPT50", "tobitId": 5348470, "usedModel": 66},
    {"name": "gpt-5-mini", "personId": "CAI-GPT5M", "tobitId": 5348471, "usedModel": 67},
    {"name": "gpt-5-nano", "personId": "CAI-GPT5N", "tobitId": 5348472, "usedModel": 68},
    {"name": "grok-3", "personId": "CAI-GRK30", "tobitId": 5236521, "usedModel": 45},
    {"name": "grok-3-mini", "personId": "CAI-GRK3M", "tobitId": 5236522, "usedModel": 46},
    {"name": "grok-4", "personId": "CAI-GRK40", "tobitId": 5331803, "usedModel": 62},
    {"name": "kimi-k2", "personId": "CAI-KIMK2", "tobitId": 5354970, "usedModel": 64},
    {"name": "llama-4-maverick", "personId": "CAI-LLM4M", "tobitId": 5237190, "usedModel": 51},
    {"name": "llama-4-scout", "personId": "CAI-LLM4S", "tobitId": 5237189, "usedModel": 50},
    {"name": "magistral-medium", "personId": "CAI-MGTRM", "tobitId": 5331735, "usedModel": 61},
    {"name": "magistral-small", "personId": "CAI-MGTRS", "tobitId": 5331734, "usedModel": 60},
    {"name": "mistral-medium-3", "personId": "CAI-MSTM3", "tobitId": 5257695, "usedModel": 55},
    {"name": "mistral-small-3", "personId": "CAI-MSTS3", "tobitId": 5218143, "usedModel": 41},
    {"name": "o3", "personId": "CAI-0AI03", "tobitId": 5237862, "usedModel": 52},
    {"name": "o3-pro", "personId": "CAI-0A03P", "tobitId": 5277247, "usedModel": 59},
    {"name": "o4-mini", "personId": "CAI-0A04M", "tobitId": 5237863, "usedModel": 53},
    {"name": "perplexity", "personId": "CAI-PLXTY", "tobitId": 5184881, "usedModel": 30},
    {"name": "qwen-2.5-14b-1m", "personId": "CAI-QWN25", "tobitId": 5214105, "usedModel": 39},
    {"name": "qwen-3", "personId": "CAI-QWEN3", "tobitId": 5262741, "usedModel": 56},
    {"name": "qwq-32b", "personId": "CAI-QWQ32", "tobitId": 5226068, "usedModel": 43},
]

MODEL_MAPPING = {
    model["name"]: {"personId": model["personId"]}
    for model in MODELS_DATA
}

class ThreadCreateModel(BaseModel):
    model: str
    first_message: Optional[str] = None

class Message(BaseModel):
    thread_id: str
    message_text: str
    image_url: Optional[str] = None

class Database:
    def __init__(self, db_file="sidekick_api.db"):
        self.db_file = db_file
        self.connection = None
        self.init_database()

    def get_connection(self):
        try:
            if self.connection is None:
                self.connection = sqlite3.connect(self.db_file, check_same_thread=False)
                self.connection.row_factory = sqlite3.Row
            return self.connection
        except Error as e:
            print(f"Error connecting to SQLite database: {e}")
            return None

    def init_database(self):
        conn = self.get_connection()
        if conn:
            try:
                cursor = conn.cursor()
                cursor.execute('CREATE TABLE IF NOT EXISTS threads (thread_id TEXT PRIMARY KEY, model_name TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)')
                cursor.execute('CREATE TABLE IF NOT EXISTS messages (message_id TEXT PRIMARY KEY, thread_id TEXT, text TEXT, author TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (thread_id) REFERENCES threads (thread_id))')
                cursor.execute('CREATE TABLE IF NOT EXISTS connections (thread_id TEXT PRIMARY KEY, is_connected INTEGER, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (thread_id) REFERENCES threads (thread_id))')
                conn.commit()
            except Error as e:
                print(f"Error initializing database: {e}")

    def save_thread_model(self, thread_id, model_name):
        conn = self.get_connection()
        if conn:
            try:
                cursor = conn.cursor()
                cursor.execute("INSERT OR REPLACE INTO threads (thread_id, model_name) VALUES (?, ?)", (thread_id, model_name))
                conn.commit()
            except Error as e:
                print(f"Error saving thread model: {e}")

    def get_thread_model(self, thread_id):
        conn = self.get_connection()
        if conn:
            try:
                cursor = conn.cursor()
                cursor.execute("SELECT model_name FROM threads WHERE thread_id = ?", (thread_id,))
                row = cursor.fetchone()
                return row["model_name"] if row else None
            except Error as e:
                print(f"Error getting thread model: {e}")
                return None

    def save_message(self, message_id, thread_id, text, author):
        conn = self.get_connection()
        if conn:
            try:
                cursor = conn.cursor()
                cursor.execute("INSERT INTO messages (message_id, thread_id, text, author) VALUES (?, ?, ?, ?)", (message_id, thread_id, text, author))
                conn.commit()
            except Error as e:
                print(f"Error saving message: {e}")

    def update_connection_status(self, thread_id, is_connected):
        conn = self.get_connection()
        if conn:
            try:
                cursor = conn.cursor()
                cursor.execute("INSERT OR REPLACE INTO connections (thread_id, is_connected, last_updated) VALUES (?, ?, CURRENT_TIMESTAMP)", (thread_id, 1 if is_connected else 0))
                conn.commit()
            except Error as e:
                print(f"Error updating connection status: {e}")

    def get_connection_status(self, thread_id):
        conn = self.get_connection()
        if conn:
            try:
                cursor = conn.cursor()
                cursor.execute("SELECT is_connected FROM connections WHERE thread_id = ?", (thread_id,))
                row = cursor.fetchone()
                return bool(row["is_connected"]) if row else False
            except Error as e:
                print(f"Error getting connection status: {e}")
                return False

    def close(self):
        if self.connection:
            self.connection.close()
            self.connection = None

class SocketManager:
    def __init__(self):
        self.socket_connections = {}
        self.lock = Lock()
        self.response_buffers = {}
        self.response_events = {}
        self.db = Database()
        self.last_activity_time = {}
        self.inactive_timeout = 240
        self.request_pending = {}

    def set_thread_model(self, thread_id, model_name):
        self.db.save_thread_model(thread_id, model_name)

    async def connect_socket(self, thread_id):
        for attempt in range(1, 8):
            try:
                websocket = await websockets.connect(SOCKET_URL)
                await websocket.recv()
                await websocket.send("40")
                auth_message = f'42["authenticate",{{"accessToken":"{AUTHORIZATION.replace("bearer ", "")}","addMessageWithThreadData":false,"threadTypeFilter":[8,10],"ischaynsAIView":true}}]'
                await websocket.send(auth_message)
                await websocket.recv()
                join_message = f'42["joinThread",{{"threadId":"{thread_id}"}}]'
                await websocket.send(join_message)
                await websocket.recv()
                await websocket.send('42["getThreadData"]')
                with self.lock:
                    self.socket_connections[thread_id] = websocket
                    self.response_buffers[thread_id] = []
                    self.last_activity_time[thread_id] = time.time()
                    self.request_pending[thread_id] = False
                self.db.update_connection_status(thread_id, True)
                asyncio.create_task(self.listen_messages(thread_id, websocket))
                return True
            except Exception as e:
                print(f"[{thread_id}] Error connecting (attempt {attempt}): {e}")
                if attempt < 7: await asyncio.sleep(2)
        self.db.update_connection_status(thread_id, False)
        return False

    async def listen_messages(self, thread_id, websocket):
        try:
            message_chunks = {}
            while True:
                message = await websocket.recv()
                if message == "2":
                    await websocket.send("3")
                    continue
                if '42["typingMessage"' in message:
                    match = re.search(r'42\["typingMessage",(.+)\]', message)
                    if match:
                        data = json.loads(match.group(1))
                        guid = data.get("guid")
                        if data.get("typeId") == 1:
                            if "messageChunk" in data:
                                if guid not in message_chunks: message_chunks[guid] = []
                                message_chunks[guid].append(data["messageChunk"])
                            if data.get("isLastChunk") and guid in message_chunks:
                                complete_message = "".join(message_chunks[guid])
                                message_id = str(uuid.uuid4())
                                model_name = self.db.get_thread_model(thread_id)
                                author_name = f"AI {model_name.replace('-', ' ').title()}" if model_name else "AI Assistant"
                                self.db.save_message(message_id, thread_id, complete_message, author_name)
                                with self.lock:
                                    self.response_buffers[thread_id].append({"id": message_id, "text": complete_message, "author": {"name": author_name}})
                                    if thread_id in self.response_events:
                                        self.response_events[thread_id].set()
                                        self.request_pending[thread_id] = False
        except websockets.exceptions.ConnectionClosed:
            print(f"[{thread_id}] WebSocket connection closed.")
            self.db.update_connection_status(thread_id, False)
        except Exception as e:
            print(f"[{thread_id}] Error in listener: {e}")
            self.db.update_connection_status(thread_id, False)

    async def get_response(self, thread_id, timeout=180):
        event = asyncio.Event()
        with self.lock:
            self.response_events[thread_id] = event
        try:
            await asyncio.wait_for(event.wait(), timeout)
            with self.lock:
                if thread_id in self.response_buffers and self.response_buffers[thread_id]:
                    return self.response_buffers[thread_id].pop(0)
            return None
        except asyncio.TimeoutError:
            return None
        finally:
            with self.lock:
                if thread_id in self.response_events: del self.response_events[thread_id]

    async def ensure_connected(self, thread_id):
        with self.lock:
            is_connected = thread_id in self.socket_connections
        if not is_connected and not self.db.get_connection_status(thread_id):
            return await self.connect_socket(thread_id)
        return True

    async def close_socket(self, thread_id):
        websocket = self.socket_connections.pop(thread_id, None)
        if websocket: await websocket.close()
        self.db.update_connection_status(thread_id, False)

    async def check_inactive_sockets(self):
        while True:
            await asyncio.sleep(30)
            current_time = time.time()
            threads_to_disconnect = [
                thread_id for thread_id, last_active in self.last_activity_time.items()
                if current_time - last_active > self.inactive_timeout
            ]
            for thread_id in threads_to_disconnect:
                await self.close_socket(thread_id)

socket_manager = SocketManager()

def create_thread_api(model_person_id: str):
    url = "https://cube.tobit.cloud/intercom-backend/v2/thread?forceCreate=true"
    headers = {"Authorization": AUTHORIZATION, "Content-Type": "application/json"}
    body = {"members": [{"personId": MY_PERSON_ID}, {"personId": model_person_id}], "nerMode": "None", "priority": 0, "typeId": 8}
    response = requests.post(url, headers=headers, json=body)
    response.raise_for_status()
    return response.json().get('id')

def send_message_api(thread_id: str, message_text: str, image_url: Optional[str] = None):
    url = f"https://intercom.tobit.cloud/api/thread/{thread_id}/message"
    headers = {"Authorization": AUTHORIZATION, "Content-Type": "application/json"}
    message_body = {"guid": str(uuid.uuid4()), "meta": {}, "text": message_text, "typeId": 1}
    if image_url:
        message_body["images"] = [{"url": image_url}]
    body = {"author": {"personId": MY_PERSON_ID}, "message": message_body}
    response = requests.post(url, headers=headers, json=body)
    response.raise_for_status()

def upload_image_api(image: UploadFile = File(...)):
    url = f"https://cube.tobit.cloud/image-service/v3/Images/{MY_PERSON_ID}"
    headers = {"Authorization": AUTHORIZATION}
    files = {'File': (image.filename, image.file, image.content_type)}
    response = requests.post(url, headers=headers, files=files)
    response.raise_for_status()
    api_response = response.json()
    return f"{api_response.get('baseDomain', '')}{api_response.get('image', {}).get('path', '')}"

@app.get("/models")
async def get_models():
    return [model["name"] for model in MODELS_DATA]

@app.post("/create_thread")
def create_thread(thread_create: ThreadCreateModel):
    model_data = MODEL_MAPPING.get(thread_create.model)
    if not model_data: raise HTTPException(status_code=400, detail="Model not found")
    thread_id = create_thread_api(model_data["personId"])
    socket_manager.set_thread_model(thread_id, thread_create.model)
    return {"thread_id": thread_id}

@app.post("/send")
async def send_and_read_message(message: Message):
    thread_id = message.thread_id
    with socket_manager.lock:
        if socket_manager.request_pending.get(thread_id): raise HTTPException(status_code=400, detail="Wait for previous response")
        socket_manager.request_pending[thread_id] = True
    try:
        if not await socket_manager.ensure_connected(thread_id): raise HTTPException(status_code=500, detail="WebSocket connection failed")
        with socket_manager.lock:
            socket_manager.response_buffers[thread_id] = []
        send_message_api(thread_id, message.message_text, message.image_url)
        response_data = await socket_manager.get_response(thread_id)
        if not response_data: raise HTTPException(status_code=408, detail="Timeout")
        original_text = response_data.get("text", "")
        cleaned_text = re.sub(r'(TESTING|TESTNET|TEST|NET)\s*', '', original_text, flags=re.IGNORECASE).strip()
        author_name = response_data.get("author", {}).get("name", "")
        model_name = author_name.replace("Sidekick ", "").strip()
        return {"response": {"id": response_data.get("id"), "model": model_name, "text": cleaned_text}}
    finally:
        with socket_manager.lock:
            socket_manager.request_pending[thread_id] = False

@app.post("/upload_image")
async def upload_image(image: UploadFile = File(...)):
    return upload_image_api(image)

@app.on_event("startup")
async def startup_event():
    asyncio.create_task(socket_manager.check_inactive_sockets())

@app.on_event("shutdown")
async def shutdown_event():
    for thread_id in list(socket_manager.socket_connections.keys()):
        await socket_manager.close_socket(thread_id)
    socket_manager.db.close()

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
AUTHORIZATION = "сюда токен авторизации" # вместе с brearer
MY_PERSON_ID = "сюда юзер айди" # только айди

2.1. Устанавливаем все необходимые зависимости
pip install "fastapi[all]" uvicorn requests websockets

2.2. Запускаем сервер uvicorn api:app --host 0.0.0.0 --port 8000 и проверяем работоспособность.

Преимущества API:
1. Получение списка всех доступных моделей.
2. Создание новых диалогов с любой моделью.
3. Поддержка медиа (картинок)
4. Загрузка картинок на их сервер.


На этом все готово, мы получили бесплатный доступ к новейшим LLM моделям. Использовать API можно в любых сферах, в которых только пожелаете. На базе этого бэкэнда можно создать фронт с диалоговым окном для более привычного использования. Для максимально-комфортного использования можно написать ко всему этому авторег аккаунтов с импортом необходимых данных прямо в бэкэнд, как я сказал ранее. Хорошего использования!

Представляю вам обновленную версию проекта: #freeLLMbysociety v. 2.0 с пользовательским интерфейсом и обновленной серверной частью.
Разработал минималистичный интерфейс на Vue.js и полностью переписал серверную логику на JS с использованием Express.js - теперь весь стек работает на одном языке, что существенно упрощает деплой и поддержку. Архитектура получилась достаточно гибкой и не требует сложной настройки окружения.
Для полного автоматизирования процесса остается реализовать модуль автоматической ротации учетных записей и динамическое обновление конфигурации, но на текущем этапе решил остановиться - проект уже забрал немало времени и ресурсов. Вполне возможно, вернусь к доработке этого функционала позже.

Часть 1. Клиентская часть (frontend)

Внешний вид:
1758883617876.png


Структура приложения
Фронт написан на Vue 3 без всяких webpack'ов - подключаем через CDN и погнали:

HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <title>FreeLLM by society</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" href="style.css">
</head>

Инициализация Vue приложения
Основной компонент содержит всю логику работы с чатом:

JavaScript:
createApp({
    data() {
        return {
            API_URL: 'http://localhost:8000',
            models: [],           // Список доступных моделей
            selectedModel: '',    // Выбранная модель
            threadId: null,       // ID текущего треда
            messages: [],         // История сообщений
            inputMessage: '',     // Текущее сообщение
            loading: false,       // Флаг загрузки
            uploadedImageUrl: null // URL загруженного изображения
        };
    },
    mounted() {
        this.loadModels();
        this.adjustTextareaHeight();
    }
}).mount('#app');


Создание нового чата
При выборе модели создается новый thread:

JavaScript:
async createNewThread() {
    if (!this.selectedModel) {
        this.showError('Please select a model first');
        return;
    }

    this.loading = true;
    try {
        const response = await axios.post(`${this.API_URL}/create_thread`, {
            model: this.selectedModel
        });
        this.threadId = response.data.thread_id;
        this.messages = [];
        this.showSuccess('New chat created');
    } catch (error) {
        this.showError('Failed to create thread');
    } finally {
        this.loading = false;
    }
}


Отправка сообщений
Самая важная функция - отправка сообщения и получение ответа:

JavaScript:
async sendMessage() {
    if (!this.inputMessage.trim() || !this.threadId || this.loading) {
        return;
    }

    const messageText = this.inputMessage.trim();
    this.inputMessage = '';
   
    // Добавляем сообщение пользователя в UI
    this.messages.push({
        role: 'user',
        author: 'You',
        content: messageText,
        time: this.getCurrentTime()
    });

    this.loading = true;

    try {
        const requestData = {
            thread_id: this.threadId,
            message_text: messageText
        };

        // Если есть изображение - добавляем его
        if (this.uploadedImageUrl) {
            requestData.image_url = this.uploadedImageUrl;
            this.uploadedImageUrl = null;
        }

        const response = await axios.post(`${this.API_URL}/send`, requestData);
       
        // Добавляем ответ AI
        this.messages.push({
            role: 'assistant',
            author: response.data.response.model,
            content: response.data.response.text,
            time: this.getCurrentTime()
        });
    } catch (error) {
        if (error.response?.status === 408) {
            this.showError('Response timeout. Please try again.');
        }
    } finally {
        this.loading = false;
    }
}


Загрузка изображений
Для работы с изображениями используем FormData:

JavaScript:
async handleFileUpload(event) {
    const file = event.target.files[0];
   
    if (!file.type.startsWith('image/')) {
        this.showError('Please select an image file');
        return;
    }

    const maxSize = 10 * 1024 * 1024; // 10MB
    if (file.size > maxSize) {
        this.showError('File size must be less than 10MB');
        return;
    }

    const formData = new FormData();
    formData.append('image', file);

    try {
        const response = await axios.post(`${this.API_URL}/upload_image`, formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
        });
        this.uploadedImageUrl = response.data;
        this.showSuccess('Image uploaded successfully');
    } catch (error) {
        this.showError('Failed to upload image');
    }
}

Часть 2. Серверная часть (Node.js)

Настройка окружения
Все конфиги хранятся в .env файле:

Bash:
PORT=8000
AUTHORIZATION=bearer YOUR_TOKEN_HERE
MY_PERSON_ID=YOUR_PERSON_ID_HERE
SOCKET_URL=wss://intercom.tobit.cloud/ws/socket.io/?EIO=4&transport=websocket
DB_FILE=sidekick_api.db
INACTIVE_TIMEOUT=240
RESPONSE_TIMEOUT=180


Маппинг моделей
Система поддерживает 40+ моделей. Каждая модель имеет свой personId для API:

JavaScript:
const MODELS_DATA = [
    {"name": "hermes-4-70b", "personId": "CAI-HM47B", "tobitId": 5362389, "usedModel": 72},
    {"name": "claude-3.5-haiku", "personId": "CAI-CLDHK", "tobitId": 5184880, "usedModel": 29},
    {"name": "claude-3.5-sonnet", "personId": "CAI-CLD35", "tobitId": 5145626, "usedModel": 15},
    {"name": "claude-3.7-sonnet", "personId": "CAI-CLD37", "tobitId": 5217590, "usedModel": 40},
    {"name": "claude-4-opus", "personId": "CAI-CLD4Z", "tobitId": 5270599, "usedModel": 58},
    {"name": "claude-4-sonnet", "personId": "CAI-CLD4S", "tobitId": 5270598, "usedModel": 57},
    {"name": "claude-opus-4.1", "personId": "CAI-CLD41", "tobitId": 5349587, "usedModel": 65},
    {"name": "deepseek-r1", "personId": "CAI-DPSK1", "tobitId": 5213560, "usedModel": 35},
    {"name": "gemini-2.0-flash", "personId": "CAI-GMN2F", "tobitId": 5193243, "usedModel": 33},
    {"name": "gemini-2.0-flash-thinking", "personId": "CAI-GMN2T", "tobitId": 5206944, "usedModel": 34},
    {"name": "gemini-2.5-flash", "personId": "CAI-GM25F", "tobitId": 5239517, "usedModel": 54},
    {"name": "gemini-2.5-flash-lite", "personId": "CAI-G25FL", "tobitId": 5335775, "usedModel": 63},
    {"name": "gemini-2.5-pro", "personId": "CAI-GM25P", "tobitId": 5229293, "usedModel": 44},
    {"name": "glm-4.5", "personId": "CAI-GLM45", "tobitId": 5357855, "usedModel": 70},
    {"name": "glm-4.5-air", "personId": "CAI-GL45A", "tobitId": 5357856, "usedModel": 71},
    {"name": "gpt-4.1", "personId": "CAI-GP410", "tobitId": 5236820, "usedModel": 47},
    {"name": "gpt-4.1-mini", "personId": "CAI-GP41M", "tobitId": 5236821, "usedModel": 48},
    {"name": "gpt-4.1-nano", "personId": "CAI-GP41N", "tobitId": 5236822, "usedModel": 49},
    {"name": "gpt-4o", "personId": "CAI-GPT4Z", "tobitId": 5127650, "usedModel": 11},
    {"name": "gpt-4o-mini", "personId": "CAI-GPT4M", "tobitId": 5155563, "usedModel": 19},
    {"name": "gpt-5", "personId": "CAI-GPT50", "tobitId": 5348470, "usedModel": 66},
    {"name": "gpt-5-mini", "personId": "CAI-GPT5M", "tobitId": 5348471, "usedModel": 67},
    {"name": "gpt-5-nano", "personId": "CAI-GPT5N", "tobitId": 5348472, "usedModel": 68},
    {"name": "grok-3", "personId": "CAI-GRK30", "tobitId": 5236521, "usedModel": 45},
    {"name": "grok-3-mini", "personId": "CAI-GRK3M", "tobitId": 5236522, "usedModel": 46},
    {"name": "grok-4", "personId": "CAI-GRK40", "tobitId": 5331803, "usedModel": 62},
    {"name": "kimi-k2", "personId": "CAI-KIMK2", "tobitId": 5354970, "usedModel": 64},
    {"name": "llama-4-maverick", "personId": "CAI-LLM4M", "tobitId": 5237190, "usedModel": 51},
    {"name": "llama-4-scout", "personId": "CAI-LLM4S", "tobitId": 5237189, "usedModel": 50},
    {"name": "magistral-medium", "personId": "CAI-MGTRM", "tobitId": 5331735, "usedModel": 61},
    {"name": "magistral-small", "personId": "CAI-MGTRS", "tobitId": 5331734, "usedModel": 60},
    {"name": "mistral-medium-3", "personId": "CAI-MSTM3", "tobitId": 5257695, "usedModel": 55},
    {"name": "mistral-small-3", "personId": "CAI-MSTS3", "tobitId": 5218143, "usedModel": 41},
    {"name": "o3", "personId": "CAI-0AI03", "tobitId": 5237862, "usedModel": 52},
    {"name": "o3-pro", "personId": "CAI-0A03P", "tobitId": 5277247, "usedModel": 59},
    {"name": "o4-mini", "personId": "CAI-0A04M", "tobitId": 5237863, "usedModel": 53},
    {"name": "perplexity", "personId": "CAI-PLXTY", "tobitId": 5184881, "usedModel": 30},
    {"name": "qwen-2.5-14b-1m", "personId": "CAI-QWN25", "tobitId": 5214105, "usedModel": 39},
    {"name": "qwen-3", "personId": "CAI-QWEN3", "tobitId": 5262741, "usedModel": 56},
    {"name": "qwq-32b", "personId": "CAI-QWQ32", "tobitId": 5226068, "usedModel": 43},
];


Класс Database
Для работы с SQLite создан отдельный класс:

JavaScript:
class Database {
    constructor(dbFile = "sidekick_api.db") {
        this.db = new sqlite3.Database(dbFile);
        this.initDatabase();
    }

    createTables() {
        const queries = [
            `CREATE TABLE IF NOT EXISTS threads (
                thread_id TEXT PRIMARY KEY,
                model_name TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )`,
            `CREATE TABLE IF NOT EXISTS messages (
                message_id TEXT PRIMARY KEY,
                thread_id TEXT,
                text TEXT,
                author TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (thread_id) REFERENCES threads (thread_id)
            )`,
            `CREATE TABLE IF NOT EXISTS connections (
                thread_id TEXT PRIMARY KEY,
                is_connected INTEGER,
                last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )`
        ];

        queries.forEach(query => this.db.run(query));
    }

    saveMessage(messageId, threadId, text, author) {
        return new Promise((resolve, reject) => {
            const query = `INSERT INTO messages VALUES (?, ?, ?, ?)`;
            this.db.run(query, [messageId, threadId, text, author],
                err => err ? reject(err) : resolve());
        });
    }
}


SocketManager - сердце системы
Самый важный класс - управление WebSocket соединениями:

JavaScript:
class SocketManager {
    constructor() {
        this.socketConnections = {};  // Активные соединения
        this.responseBuffers = {};    // Буферы ответов
        this.responseCallbacks = {};  // Коллбеки для ответов
        this.lastActivityTime = {};   // Время последней активности
        this.db = new Database();
        this.inactiveTimeout = 240;   // Таймаут неактивности
        this.startInactivityCheck();
    }
}


Подключение к WebSocket
Процесс подключения состоит из нескольких этапов:

JavaScript:
async connectSocket(threadId) {
    for (let attempt = 1; attempt <= 7; attempt++) {
        try {
            const ws = new WebSocket(SOCKET_URL);
           
            // Ждем открытия соединения
            await new Promise((resolve, reject) => {
                ws.once('open', resolve);
                ws.once('error', reject);
            });

            // Socket.io handshake
            await this.waitForMessage(ws);
            ws.send("40");

            // Аутентификация
            const authMessage = `42["authenticate",{
                "accessToken":"${AUTHORIZATION.replace("bearer ", "")}",
                "threadTypeFilter":[8,10],
                "ischaynsAIView":true
            }]`;
            ws.send(authMessage);

            // Присоединяемся к треду
            const joinMessage = `42["joinThread",{"threadId":"${threadId}"}]`;
            ws.send(joinMessage);

            this.socketConnections[threadId] = ws;
            this.listenMessages(threadId, ws);
           
            return true;
        } catch (error) {
            if (attempt < 7) {
                await new Promise(resolve => setTimeout(resolve, 2000));
            }
        }
    }
    return false;
}


Обработка сообщений от AI
WebSocket отправляет сообщения частями (chunks). Нужно их собрать:

JavaScript:
listenMessages(threadId, ws) {
    const messageChunks = {};

    ws.on('message', async (data) => {
        const message = data.toString();
       
        // Ping-pong для поддержания соединения
        if (message === "2") {
            ws.send("3");
            return;
        }

        // Обработка typing сообщений
        if (message.includes('42["typingMessage"')) {
            const match = message.match(/42\["typingMessage",(.+)\]/);
            if (match) {
                const messageData = JSON.parse(match[1]);
                const guid = messageData.guid;

                // Собираем чанки сообщения
                if (messageData.typeId === 1) {
                    if (!messageChunks[guid]) {
                        messageChunks[guid] = [];
                    }
                    messageChunks[guid].push(messageData.messageChunk);

                    // Если последний чанк - собираем полное сообщение
                    if (messageData.isLastChunk) {
                        const completeMessage = messageChunks[guid].join('');
                       
                        this.responseBuffers[threadId].push({
                            text: completeMessage,
                            author: { name: modelName }
                        });

                        // Вызываем коллбек
                        if (this.responseCallbacks[threadId]) {
                            this.responseCallbacks[threadId]();
                        }

                        delete messageChunks[guid];
                    }
                }
            }
        }
    });
}


API endpoints
Express сервер предоставляет REST API:

JavaScript:
// Получение списка моделей
app.get('/models', (req, res) => {
    const modelNames = MODELS_DATA.map(model => model.name);
    res.json(modelNames);
});

// Создание нового треда
app.post('/create_thread', async (req, res) => {
    const { model } = req.body;
    const modelData = MODEL_MAPPING[model];
   
    if (!modelData) {
        return res.status(400).json({ detail: "Model not found" });
    }

    // Создаем thread через API
    const threadId = await createThreadApi(modelData.personId);
    await socketManager.setThreadModel(threadId, model);
   
    res.json({ thread_id: threadId });
});

// Отправка сообщения
app.post('/send', async (req, res) => {
    const { thread_id, message_text, image_url } = req.body;

    // Проверяем, нет ли уже запроса в обработке
    if (socketManager.requestPending[threadId]) {
        return res.status(400).json({ detail: "Wait for previous response" });
    }

    socketManager.requestPending[threadId] = true;

    try {
        // Убеждаемся что WebSocket подключен
        if (!await socketManager.ensureConnected(threadId)) {
            return res.status(500).json({ detail: "WebSocket connection failed" });
        }

        // Отправляем сообщение через API
        await sendMessageApi(threadId, message_text, image_url);

        // Ждем ответ с таймаутом 180 сек
        const responseData = await socketManager.getResponse(threadId);
       
        if (!responseData) {
            return res.status(408).json({ detail: "Timeout" });
        }

        res.json({
            response: {
                id: responseData.id,
                model: modelName,
                text: responseData.text
            }
        });
    } finally {
        socketManager.requestPending[threadId] = false;
    }
});


Управление неактивными соединениями
Чтобы не держать кучу открытых сокетов, отключаем неактивные:

JavaScript:
startInactivityCheck() {
    setInterval(async () => {
        const currentTime = Date.now();
        const threadsToDisconnect = [];

        for (const [threadId, lastActive] of Object.entries(this.lastActivityTime)) {
            if (currentTime - lastActive > this.inactiveTimeout * 1000) {
                threadsToDisconnect.push(threadId);
            }
        }

        for (const threadId of threadsToDisconnect) {
            await this.closeSocket(threadId);
            delete this.lastActivityTime[threadId];
        }
    }, 30000); // Проверяем каждые 30 сек
}


Graceful shutdown
При остановке сервера корректно закрываем все соединения:

JavaScript:
process.on('SIGINT', async () => {
    console.log('\nShutting down gracefully...');
   
    // Закрываем все WebSocket соединения
    for (const threadId of Object.keys(socketManager.socketConnections)) {
        await socketManager.closeSocket(threadId);
    }
   
    // Закрываем БД
    socketManager.db.close();
   
    server.close(() => {
        console.log('Server closed');
        process.exit(0);
    });
});


Фишки и оптимизации

1. Retry механизм: При подключении к WebSocket делаем до 7 попыток с задержкой 2 секунды.
2. Буферизация ответов: Ответы от AI приходят частями, собираем их в буфер перед отправкой клиенту.
3. Автоматическое отключение: Неактивные соединения автоматически закрываются через 4 минуты.
4. Защита от дублирования: Флаг requestPending не дает отправить новое сообщение, пока не получен ответ на предыдущее.



Установка и запуск

Требования к системе
Перед установкой убедитесь, что у вас есть:
  • Node.js версии 14.0.0 или выше
  • NPM (идет в комплекте с Node.js)
  • Любой текстовый редактор
  • 100 MB свободного места

Шаг 1: Создание структуры проекта
Создаем папку проекта и переходим в нее: mkdir freellmcd freellm

Шаг 2: Инициализация проекта
Инициализируем npm проект: npm init -y

Шаг 3: Установка зависимостей
Устанавливаем необходимые пакеты: npm install express cors multer axios ws uuid sqlite3 form-data dotenv
Для разработки также установим nodemon: npm install --save-dev nodemon

Шаг 4: Создание файлов проекта
Создаем все необходимые файлы:

4.1. Создаем server.js: touch server.js → Скопируйте в него код сервера из проекта.
4.2. Создаем index.html: touch index.html → Вставьте HTML структуру интерфейса.
4.3. Создаем app.js: touch app.js → Добавьте Vue.js логику приложения.
4.4. Создаем style.css: touch style.css → Вставьте стили интерфейса.
4.5. Создаем .env файл: touch .env
4.1.1. Или просто возьмите готовые файлы прикрепленные под статьей и установите на ваш сервер.

Шаг 5: Настройка конфигурации
Откройте .env файл и добавьте конфигурацию:
Bash:
PORT=8000

# Authentication
AUTHORIZATION=bearer YOUR_TOKEN_HERE
MY_PERSON_ID=YOUR_PERSON_ID_HERE

# WebSocket URL (не меняем)
SOCKET_URL=wss://intercom.tobit.cloud/ws/socket.io/?EIO=4&transport=websocket

# Database
DB_FILE=sidekick_api.db

# Timeouts
INACTIVE_TIMEOUT=240
RESPONSE_TIMEOUT=180

Шаг 6: Обновление package.json
Откройте package.json и добавьте скрипты для запуска:
Bash:
{
  "name": "freellm-backend",
  "version": "1.0.0",
  "description": "#FREELLM by society - special for XSS.pro",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": [
    "freellm",
    "ai",
    "chat",
    "websocket"
  ],
  "author": "society",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5",
    "multer": "^1.4.5-lts.1",
    "axios": "^1.6.2",
    "ws": "^8.14.2",
    "uuid": "^9.0.1",
    "sqlite3": "^5.1.6",
    "form-data": "^4.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  },
  "engines": {
    "node": ">=14.0.0"
  }
}

Шаг 8: Запуск проекта

Для режима разработки с авто-перезагрузкой: npm run dev


После запуска вы увидите:
╔════════════════════════════════════╗
║ #FREELLM by society.
║ special for XSS.pro
║ Server running on port 8000
╚════════════════════════════════════╝

Шаг 10: Тестирование
Перейдите по адресу http://localhost:8000 и все должно работать.


Получился довольно простой, но очень качественный инструмент в надежных руках. Использовать backend можно как на питоне, так и в новой версии на JS. Идей для реализации множество, можно на базе этого создать своего бота в Телеграмм с поддержкой всех моделей и брать плату за пользование. Можно использовать в других личных целях: решать капчи, закрывать монотонную работу и так далее, сфер применения множество. Надеюсь, что материал станет полезным для вас. Ниже прикладываю исходники проекта (пароль xss.pro).

Автор: society, special for XSS.PRO forum.
 

Вложения

  • LLM.zip
    11.8 КБ · Просмотры: 21
А https://cube.tobit.cloud/ поддерживает только загрузку изображений: https://cube.tobit.cloud/image-service/v3/Images/ ? Хорошо бы прикрутить поддержку других типов файлов, если это возможно.
У всех медленно запросы приходят и со временем теряется соединение и приходится перезагружать страницу?
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх