Этот PowerShell-скрипт — сетевое ядро переводчика QuillBot. Он использует системный класс .NET System.Net.WebSockets.ClientWebSocket для работы по протоколу WebSocket.
Схема протокола QuillBot WebSocket:
 
    Подключение к wss://stream.quillbot.com/?anonId=....
 
    Ожидание от сервера события ping -> отправка в ответ pong.
 
    Ожидание события авторизации auth_success -> отправка метаданных клиента (update_connection) и инициализация редактора переводчика (init_editor).
 
    Подтверждение приема пакетов (система ack / квитирование).
 
    При получении подтверждения об успешной инициализации редактора отправляется пакет on_content_change с исходным текстом для перевода.
 
    Ожидание ответа с типом processing_success. Из него извлекаются данные автоопределения языка (detect-language) и массив переведенных предложений (translator).
 
    Результат собирается и записывается в локальный JSON-файл.
 
Вот этот скрипт с подробным разбором:
code Powershell

# Останавливать выполнение скрипта при любой возникшей ошибке
$ErrorActionPreference = "Stop"
 
# Принудительная установка вывода консоли в UTF-8 без сигнатуры BOM
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
 
# ============================================================================
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ============================================================================
 
/**
 * Генерирует уникальный ID события для WebSocket протокола Quillbot.
 * Состоит из текущего UNIX-времени в миллисекундах и случайной строки (хвост GUID).
 */
function New-EventId {
    "{0}-{1}" -f [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(), ([guid]::NewGuid().ToString("N").Substring(0, 9))
}
 
/**
 * Записывает финальный хеш-результат перевода/определения в JSON-файл обмена.
 */
function Write-BridgeResult {
    param(
        [hashtable]$Data
    )
    $path = $env:QTB_QUILLBOT_RESULT_FILE
    $directory = [System.IO.Path]::GetDirectoryName($path)
     
    # Создаем папку _temp, если она ещё не создана
    if (-not [string]::IsNullOrEmpty($directory) -and -not [System.IO.Directory]::Exists($directory)) {
        [System.IO.Directory]::CreateDirectory($directory) | Out-Null
    }
     
    # Сериализуем данные в компактный JSON и сохраняем в файл (UTF-8 без BOM)
    $json = $Data | ConvertTo-Json -Depth 10 -Compress
    [System.IO.File]::WriteAllText($path, $json, [System.Text.UTF8Encoding]::new($false))
}
 
/**
 * Отправляет JSON-сообщение в WebSocket-канал.
 */
function Send-BridgeJson {
    param(
        [System.Net.WebSockets.ClientWebSocket]$Socket,
        [object]$Data,
        [System.Threading.CancellationToken]$Token
    )
    $json = $Data | ConvertTo-Json -Depth 12 -Compress
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
    $segment = [ArraySegment[byte]]::new($bytes)
     
    # Синхронная отправка кадра типа Text
    $null = $Socket.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $Token).GetAwaiter().GetResult()
}
 
/**
 * Принимает входящее сообщение из WebSocket-канала.
 * Собирает фрагментированные пакеты данных в единый массив байт и парсит JSON.
 */
function Receive-BridgeJson {
    param(
        [System.Net.WebSockets.ClientWebSocket]$Socket,
        [System.Threading.CancellationToken]$Token
    )
    $buffer = New-Object byte[] 8192 # Буфер чтения размером 8 Кб
    $stream = New-Object System.IO.MemoryStream
    try {
        do {
            $segment = [ArraySegment[byte]]::new($buffer)
            # Ожидание чтения порции данных
            $result = $Socket.ReceiveAsync($segment, $Token).GetAwaiter().GetResult()
            if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
                throw "Socket closed"
            }
            # Пишем прочитанные байты в поток памяти
            $stream.Write($buffer, 0, $result.Count)
        } while (-not $result.EndOfMessage) # Цикл крутится, пока не придет флаг конца фрейма
         
        $json = [System.Text.Encoding]::UTF8.GetString($stream.ToArray())
        return $json | ConvertFrom-Json
    } finally {
        $stream.Dispose()
    }
}
 
/**
 * Отправка подтверждения о получении сообщения (Acknowledge) на сервер.
 * Сервер QuillBot требует 'ack' на каждое значимое событие, иначе разорвет связь.
 */
function Send-BridgeAck {
    param(
        [System.Net.WebSockets.ClientWebSocket]$Socket,
        [object]$Message,
        [System.Threading.CancellationToken]$Token
    )
    # Не подтверждаем сообщения без ID события, а также системные ack / auth_success
    if (-not $Message.eventId -or $Message.event -eq "ack" -or $Message.event -eq "auth_success") {
        return
    }
    Send-BridgeJson $Socket @{
        event = "ack"
        data = @{
            responseEventId = [string]$Message.eventId
            eventId = New-EventId
            eventTimestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
        }
    } $Token
}
 
/** Нормализация кодов языков */
function Normalize-DetectedLanguage {
    param(
        [string]$Code
    )
    if ($null -eq $Code) { $Code = "" }
    switch ($Code.Trim()) {
        "he" { return "iw" }
        "he-IL" { return "iw" }
        "zh" { return "zh-CN" }
        "zh-CN" { return "zh-CN" }
        "zh-Hans" { return "zh-CN" }
        "en-US" { return "en" }
        "en-GB" { return "en" }
        "de-DE" { return "de" }
        "de-CH" { return "de" }
        default {
            if ($Code -match "-") {
                return $Code.Split("-")[0]
            }
            return $Code
        }
    }
}
 
/**
 * Собирает разрозненные переведенные предложения обратно в текст в правильном порядке.
 */
function Join-TranslatedSentences {
    param(
        [object[]]$Sentences
    )
    if (-not $Sentences) { return "" }
    # Сортировка по индексу предложения sentenceIndex и склейка в единую строку
    return (($Sentences | Sort-Object sentenceIndex | ForEach-Object { [string]$_.translation }) -join "").Trim()
}
 
// ============================================================================
// ОСНОВНОЙ КОД ВЫПОЛНЕНИЯ
// ============================================================================
 
try {
    # Чтение настроек из переменных окружения (переданы из JScript)
    $mode = if ([string]::IsNullOrEmpty($env:QTB_QUILLBOT_MODE)) { "translate" } else { $env:QTB_QUILLBOT_MODE }
    $mode = $mode.Trim().ToLowerInvariant()
    $sourceLanguage = if ([string]::IsNullOrEmpty($env:QTB_QUILLBOT_SOURCE)) { "auto" } else { $env:QTB_QUILLBOT_SOURCE.Trim() }
    $targetLanguage = if ([string]::IsNullOrEmpty($env:QTB_QUILLBOT_TARGET)) { "en-US" } else { $env:QTB_QUILLBOT_TARGET.Trim() }
     
    # Декодирование текста из Base64
    $textB64 = if ([string]::IsNullOrEmpty($env:QTB_QUILLBOT_TEXT_B64)) { "" } else { $env:QTB_QUILLBOT_TEXT_B64 }
    $text = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($textB64))
     
    # Генерация случайных ID сессии и редактора
    $anonId = ([guid]::NewGuid().ToString("N")).Substring(0, 16)
    $editorId = ([guid]::NewGuid().ToString("N")).Substring(0, 16)
    $requestEventId = New-EventId
     
    # Формирование строки подключения
    $uri = [Uri]::new(("wss://stream.quillbot.com/?anonId={0}&abIdV2=8&platformType=webapp" -f $anonId))
     
    # Настройка таймаута операции (30 секунд)
    $tokenSource = [System.Threading.CancellationTokenSource]::new()
    $tokenSource.CancelAfter(30000)
    $token = $tokenSource.Token
     
    # Инициализация и настройка WebSocket клиента
    $socket = [System.Net.WebSockets.ClientWebSocket]::new()
    $socket.Options.SetRequestHeader("Origin", "https://quillbot.com")
     
    # Подключение к серверу
    $null = $socket.ConnectAsync($uri, $token).GetAwaiter().GetResult()
 
    # Вспомогательные флаги состояний конечного автомата
    $updateAck = $false
    $initAck = $false
    $requestSent = $false
    $translatorPayload = $null
    $detectedLanguage = ""
    $done = $false
 
    # Цикл чтения и обработки событий от сервера
    while (-not $token.IsCancellationRequested -and -not $done) {
        $message = Receive-BridgeJson $socket $token
         
        # Шаг А: Обработка пинга (поддержание соединения активным)
        if ($message.event -eq "ping") {
            Send-BridgeJson $socket @{
                event = "pong"
                data = @{
                    eventId = New-EventId
                    eventTimestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
                }
            } $token
            continue
        }
 
        # Шаг B: Успешная первичная авторизация -> отправка метаданных и инициализация редактора
        if ($message.event -eq "auth_success") {
            # 1. Отправляем фейковые метаданные браузера/ОС
            Send-BridgeJson $socket @{
                event = "update_connection"
                data = @{
                    deviceMeta = @{
                        operatingSystem = "Windows 10"
                        browser = "Edge"
                        browserVersion = "147"
                        deviceLanguage = "de"
                        networkMeta = @{ connectionType = "4g"; downlink = 10 }
                        platformType = "webapp"
                        platformVersion = "v41.17.1"
                        type = "Desktop"
                    }
                    userMeta = @{ anonId = $anonId; abIdV2 = "8" }
                    eventId = New-EventId
                    eventTimestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
                }
            } $token
             
            # 2. Инициализируем редактор (модуль TRANSLATOR)
            Send-BridgeJson $socket @{
                event = "init_editor"
                data = @{
                    editorGroupId = "tltr"
                    editorId = $editorId
                    editorVersion = "1"
                    hostUrl = "https://quillbot.com/translate"
                    product = "TRANSLATOR"
                    eventId = New-EventId
                    eventTimestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
                    retryCount = 0
                }
            } $token
            continue
        }
 
        # Отправляем подтверждение получения любого другого пакета
        Send-BridgeAck $socket $message $token
 
        # Шаг C: Контроль последовательности рукопожатия
        # Мы должны убедиться, что сервер QuillBot подтвердил (ack) как 'update_connection', так и 'init_editor'
        if (-not $requestSent -and $message.event -eq "ack" -and $message.requestEventId) {
            if (-not $updateAck) {
                $updateAck = $true # Подтверждено обновление соединения
                continue
            }
            if (-not $initAck) {
                $initAck = $true # Подтверждена инициализации редактора -> отправляем текст на перевод!
                Send-BridgeJson $socket @{
                    event = "on_content_change"
                    data = @{
                        editorGroupId = "tltr"
                        editorId = $editorId
                        editorContentVersion = "1"
                        processingType = "TRANSLATOR_TRANSLATE"
                        meta = @{
                            sourceLanguage = $sourceLanguage
                            targetLanguage = $targetLanguage
                            tone = "auto"
                            editorStartText = $text
                        }
                        changes = @(@{
                            text = $text
                            originalIndex = 0
                            processingType = "TRANSLATOR_TRANSLATE"
                        })
                        hostUrl = "https://quillbot.com/translate"
                        product = "TRANSLATOR"
                        changeType = "USER_CHANGE"
                        eventId = $requestEventId
                        eventTimestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
                        retryCount = 0
                    }
                } $token
                $requestSent = $true
                continue
            }
        }
 
        # Игнорируем сторонние события, не относящиеся к нашему запросу перевода
        if ($message.requestEventId -ne $requestEventId -or $message.event -ne "processing_success") {
            continue
        }
 
        # Шаг D: Разбор результатов обработки текста от сервера
        switch ($message.data.type) {
            "detect-language" {
                # Сохраняем определенный сервером язык
                $detectedLanguage = Normalize-DetectedLanguage ([string]$message.data.language.langCode)
                if ($mode -eq "detect") {
                    Write-BridgeResult @{ sourceLanguage = $detectedLanguage }
                    $done = $true
                }
            }
            "translator" {
                # Сохраняем сырой массив перевода предложений
                $translatorPayload = $message.data.translatedSentences
            }
            "request-complete" {
                # Запрос полностью обработан сервером, можно завершать работу и отдавать данные
                if ($mode -eq "detect") {
                    Write-BridgeResult @{ sourceLanguage = $detectedLanguage }
                    $done = $true
                }
                elseif ($translatorPayload) {
                    Write-BridgeResult @{
                        translation = (Join-TranslatedSentences $translatorPayload)
                        sourceLanguage = $detectedLanguage
                        targetLanguage = Normalize-DetectedLanguage $targetLanguage
                    }
                    $done = $true
                }
            }
        }
    }
 
    # Безопасное закрытие WebSocket соединения
    try {
        if ($socket.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
            $null = $socket.CloseOutputAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "done", [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()
        }
    } catch {}
    $socket.Dispose()
     
    # Если за отведенный таймаут файл результата не появился — бросаем ошибку таймаута
    if (-not (Test-Path -LiteralPath $env:QTB_QUILLBOT_RESULT_FILE)) {
        Write-BridgeResult @{ error = "Timed out waiting for QuillBot response" }
    }
} catch {
    # В случае критического сбоя (.NET Exception) — записываем её текст в файл обмена, чтобы QTranslate отобразил ошибку
    Write-BridgeResult @{ error = $_.Exception.Message }
}

Зачем это нужно для поддержки и написания новых сервисов?
 
При проектировании интеграции с новыми сайтами-переводчиками, которые работают по протоколу WebSocket (или используют сложную защиту Cloudflare, заголовки, ротацию куки), схема с внешним скриптом-мостом является единственным решением для QTranslate.
 
Вы можете скопировать логику Service.js и переписать QuillBotBridge.ps1 под новые сервисы (например, DeepL Pro, ChatGPT, Claude или Gemini API), изменив тело WebSocket/HTTP-запроса внутри PowerShell-скрипта [1, 2]. JScript-оболочка при этом останется практически без изменений, выполняя лишь роль связующего звена с QTranslate [1, 2].