Rechedar
Войти в кабинет

Плагины: свой код обработки внутри платформы

Модуль на Rust или TinyGo компилируется в WASM и исполняется в изолированной песочнице: сеть закрыта, данные не покидают контур. Загрузка и проверка — в кабинете.

Плагин — небольшой модуль, который компилируется в вебассемблер (WASM) и исполняется на площадке Rechedar в изолированной песочнице: жёсткие лимиты памяти и времени, сеть из песочницы закрыта — модуль физически не может никуда обратиться. Через песочницу проходят только данные самого звонка; персональные данные абонента не покидают защищённый контур обработки. Это главный аргумент против «отдайте нам транскрипт в наш сервис» — обработка происходит там же, где данные уже находятся.

Документ для разработчика клиента: как написать модуль, собрать, загрузить и проверить. Загрузка и управление — в кабинете, раздел меню «Ещё» → «Плагины» (роль «владелец» или «администратор»).

Что доступно сейчас

Точка «Преобразование ответа webhook-узла» (webhook.response_transform@v1) — подключается к конкретному узлу сценария: в форме узла «Вызов webhook» заполните поле «Плагин преобразования ответа» и выберите модуль из списка своих включённых. Это «розетка»: у одного узла — один модуль, у разных узлов могут быть разные модули одной точки, узлы без поля зовут платформу как обычно, без плагина.

Сценарный узел webhook умеет спрашивать вашу систему прямо во время разговора (проверить баланс, получить статус заявки). Если система отвечает неудобным форматом — SOAP/XML, значение запрятано в глубине, нужна склейка полей — а отдавать тело наружу нельзя, плагин приводит ответ в порядок внутри платформы:

узел webhook (с выбранным плагином) → HTTP-ответ вашей системы → [плагин] → переменные сценария
                                                                        → обычный маппинг узла

Плагин вызывается после получения ответа и до применения маппинга узла: результат плагина — новые переменные сценария, которые маппинг (и дальнейшие узлы) используют как обычно. Маппинг при этом остаётся обязательным — см. «Отказы и предохранитель».

На точке может быть несколько включённых модулей одновременно (режим socket): имена модулей — это ключи розеток, они уникальны среди включённых. Отдельно работает точка пакетного извлечения полей из транскриптов (batch.extract) — там активен один модуль-«извлекатель по умолчанию» (режим single), задания аналитики создаются через API или CLI.

Галерея готовых модулей

Писать свой модуль нужно не всегда: в разделе «Плагины» есть «Галерея готовых» — модули платформы, закрытые частые задачи:

  • address-extract — извлечение адреса из реплики клиента;
  • pdn-inventory — инвентаризация персональных данных в транскриптах;
  • callback-time — разбор времени перезвона («после пяти», «завтра утром») в машинное значение;
  • answer-code — вытащить код из ответа собеседника;
  • contact-extract — контактные данные из текста;
  • liability-censor — маскирование чувствительных формулировок;
  • soap-flat — развернуть SOAP/XML-ответ в плоские переменные (для webhook-розетки).

Установка — в один клик прямо из галереи: карточка показывает пример «вход → выход», статус (установлен/доступно обновление) и занятость точки. Если точка уже занята вашим модулем — галерея честно скажет «заменит такой-то»; свой модуль молча не выключается. Обновление готового модуля — тоже клик; потолок числа модулей — общий, с вашими загруженными.

Готовый модуль ведёт себя как свой: те же розетки в форме webhook-узла, тот же предохранитель, та же статистика вызовов.

Контракт точки

Вход (что получает модуль):

{
  "status": 200,
  "content_type": "application/soap+xml",
  "body": "<soap:Envelope>…</soap:Envelope>",
  "vars": {"session_id": "abc"}
}
  • status — HTTP-код ответа;
  • content_type — заголовок ответа (может отсутствовать);
  • body — тело ответа как строка (обрезается до 1 МиБ);
  • vars — текущие переменные сценария (только для чтения).

Выход (что модуль возвращает):

{"vars": {"client_status": "OK"}}
  • только vars — новые переменные сценария;
  • существующие переменные затереть нельзя: имя, которое уже есть в сценарии, молча пропускается;
  • до 32 переменных, каждая до 200 байт (лишнее обрезается по границе символа), весь ответ до 64 КиБ;
  • имя с точками («путь через точку», как у модуля SOAP/XML) становится вложенной переменной: order.status читается в сценарии как {{ vars.order.status }}.

Лимиты исполнения: память 8 МиБ, один вызов до 1500 мс, вызовов — до 120 в минуту на аккаунт. Это рамка контракта, не настройка модуля.

Отказы и предохранитель

Точка построена по принципу «звонок важнее»: любой отказ модуля — ошибка, таймаут, превышение лимитов, недоступность исполнителя — не роняет узел. Узел продолжает работу обычным маппингом, как будто плагина нет (fail-open). Серия из 10 отказов подряд отключает модуль на 5 минут (предохранитель), звонки всё это время идут мимо плагина.

На экране «Плагины» видно, применялся ли модуль за последний час и с каким результатом; там же кнопка «Проверить на примере» — прогон на своём тексте без загрузки в звонок.

Как написать модуль

Язык — Rust (или TinyGo); пример ниже на Rust. Один модуль = одна точка (модуль с несколькими точками отклоняется при загрузке).

Cargo.toml:

[package]
name = "mycorp-webhook-transform"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# паника модуля = остановка вызова, хост пересоздаст инстанс;
# оптимизация размера — модуль живёт в памяти исполнителя
[profile.release]
panic = "abort"
opt-level = "z"
lto = true

src/lib.rs — каркас ABI (обязателен как есть, логика — в process_impl внизу):

#![allow(static_mut_refs)] // гость однопоточный: хост сериализует вызовы

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Input {
    status: i64,
    #[serde(default)]
    content_type: Option<String>,
    body: String,
    #[serde(default)]
    vars: std::collections::BTreeMap<String, String>,
}

#[derive(Serialize)]
struct Output {
    vars: std::collections::BTreeMap<String, String>,
}

// Манифест: имя произвольное, версия обязательна, точка — точно эта.
const MANIFEST: &str = r#"{"name":"mycorp-webhook-transform","version":"0.1.0","points":[{"id":"webhook.response_transform","contract":"v1"}]}"#;

const ERR_BAD_INPUT: i32 = 1;
const ERR_INTERNAL: i32 = 2;

static mut ERR: i32 = 0;
static mut RESULT: Option<Vec<u8>> = None;
static mut BUFFER: Option<Vec<u8>> = None;

#[no_mangle]
pub extern "C" fn manifest() -> u64 {
    let p = MANIFEST.as_ptr() as u32;
    ((p as u64) << 32) | MANIFEST.len() as u64
}

#[no_mangle]
pub extern "C" fn alloc(len: u32) -> u32 {
    // Переиспользуемый буфер: аллокация на каждый вызов — утечка.
    unsafe {
        let b = BUFFER.get_or_insert_with(Vec::new);
        if b.len() < len as usize {
            b.resize(len as usize, 0);
        }
        b.as_ptr() as u32
    }
}

#[no_mangle]
pub extern "C" fn err_code() -> u32 {
    unsafe { ERR as u32 }
}

#[no_mangle]
pub extern "C" fn process(ptr: u32, len: u32) -> u64 {
    unsafe { ERR = 0 }
    let input: Input = match read_json(ptr, len) {
        Some(v) => v,
        None => {
            unsafe { ERR = ERR_BAD_INPUT }
            return 0;
        }
    };
    let out = match process_impl(&input) {
        Ok(o) => o,
        Err(_) => {
            unsafe { ERR = ERR_INTERNAL }
            return 0;
        }
    };
    let bytes = match serde_json::to_vec(&out) {
        Ok(b) => b,
        Err(_) => {
            unsafe { ERR = ERR_INTERNAL }
            return 0;
        }
    };
    unsafe {
        RESULT = Some(bytes);
        let r = RESULT.as_ref().unwrap();
        ((r.as_ptr() as u64) << 32) | r.len() as u64
    }
}

fn read_json(ptr: u32, len: u32) -> Option<Input> {
    if len == 0 || len > 8 << 20 {
        return None;
    }
    let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) };
    serde_json::from_slice(slice).ok()
}

/// ── Логика модуля: превращаем тело ответа в новые переменные ───────────
/// Инварианты: возвращаем ТОЛЬКО новые vars; существующие vars из input
/// заново не отдаём (хост их и так не перезапишет); никаких логов —
/// текст тела не покидает вызов.
fn process_impl(input: &Input) -> Result<Output, String> {
    let mut vars = std::collections::BTreeMap::new();

    // Пример: система отвечает SOAP, статус заявки — в <statusCode>…</statusCode>.
    // Без внешних парсеров: честный поиск подстроки.
    let status_code = input
        .body
        .split("<statusCode>")
        .nth(1)
        .and_then(|rest| rest.split("</statusCode>").next())
        .unwrap_or("none")
        .to_string();
    vars.insert("client_status".to_string(), status_code);

    // HTTP-код тоже полезен как переменная (маппинг увидит её).
    vars.insert("http_status".to_string(), input.status.to_string());

    Ok(Output { vars })
}

Сборка (нужен тулчейн WASM):

rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1
ls -la target/wasm32-wasip1/release/*.wasm   # до 2 МиБ

Загрузка и проверка

  1. Кабинет → меню «Ещё» → «Плагины» → «Загрузить модуль»: имя, точка (сейчас одна), файл .wasm.
  2. Платформа проверяет модуль в песочнице на контрольном примере: битый WASM, не та точка, модуль с несколькими точками или падающий на примере — не сохраняется вовсе, с причиной по-русски.
  3. Загруженный модуль — в статусе «выключен». Кнопка «Включить»: имя модуля становится ключом розетки — его увидит поле «Плагин преобразования ответа» в форме webhook-узла (применение — в течение минуты, кэш резолва 60 с).
  4. «Проверить на примере» — вставьте реальное тело ответа вашей системы и увидите переменные, которые вернёт модуль, и время выполнения. Проверка не грузит звонки и не пишется в журнал.
  5. На точке преобразования — несколько включённых модулей с РАЗНЫМИ именами: имя уникально среди включённых, тёзка получит отказ «имя уже занято» (переименуйте или выключите действующий). Узел без выбранного модуля работает как обычно. Модулей в списке — до 16, файл — до 2 МиБ. Выключенный модуль можно удалить; активный — сначала выключить. Выключенный или удалённый модуль в селекте узла помечается «недоступен» — выберите другой или верните модуль.

Правила площадки

  • Песочница: без сети, без файлов, без часов-расписаний — только чистое преобразование данных одного вызова.
  • Модуль не видит и не меняет: других звонков, секретов интеграций, настроек тарифов, хода диалога (узлы и фразы вне его досягаемости).
  • Переменные модуля наследуют контур сценарных переменных: те же сроки хранения и те же права — в выгрузках они выглядят как обычные переменные сценария.
  • Модуль исполняется на мощностях платформы в рамках лимитов точки; индивидуальный учёт в тарифе — при подключении (см. менеджера).

Дальше

В разработке — точка пакетной обработки транскриптов (batch.extract): извлечение анкетных полей из готовых записей без отправки данных наружу. О сроках и раннем доступе — по запросу у менеджера.

Обновлено: 2026-10-08

← Вся документация · Поиск по документации