# Плагины Komet Плагин добавляет в чат slash-команды. Пользователь пишет `/weather Москва`, Komet запускает обработчик плагина в изолированном движке JavaScript, и тот работает с чатом через модуль `komet:api`. Плагин распространяется файлом `.kinet` — это zip-архив с `manifest.json` и ES-модулями. Установка: настройки Komet → «Плагины» → «Установить .kinet» или «Установить по URL». В этом репозитории: | Файл | Что это | |------------------------------------------|-------------------------------------------| | `plugin_sdk/komet-api.d.ts` | типы `komet:api` для подсказок в редакторе | | `plugin_sdk/manifest.schema.json` | JSON Schema для `manifest.json` | | `plugin_sdk/update-manifest.schema.json` | JSON Schema для манифеста обновлений | | `tool/kinet_sign.dart` | ключи и подпись пакетов | | `assets/plugins/` | встроенные плагины — готовые примеры | ## Быстрый старт ``` hello/ ├── manifest.json └── main.js ``` `manifest.json`: ```json { "$schema": "https://raw.githubusercontent.com/KometTeam/Komet/main/plugin_sdk/manifest.schema.json", "schemaVersion": 1, "id": "com.example.hello", "name": "Hello", "version": "1.0.0", "apiVersion": 1, "description": "Приветствие", "author": "example", "main": "main.js", "permissions": ["chat.write"], "commands": [ { "name": "/hello", "description": "поздороваться", "handler": "hello", "arguments": [ {"name": "name", "description": "кого приветствовать", "required": false, "rest": true} ] } ] } ``` `main.js`: ```js import { chat } from 'komet:api'; /** @param {import('komet:api').CommandContext} context */ export async function hello(context) { const name = context.arguments.name || 'мир'; await chat.sendText(`Привет, ${name}!`); } ``` Сборка пакета — `manifest.json` должен лежать в корне архива: ```bash cd hello && zip -r ../hello.kinet . -i 'manifest.json' '*.js' ``` Для подсказок в редакторе держите `komet-api.d.ts` рядом с проектом, но не кладите в пакет: в `.kinet` допустимы только `manifest.json` и файлы `.js`. ## Манифест | Поле | Обязательно | Описание | |-----------------|-------------|----------| | `schemaVersion` | да | всегда `1` | | `id` | да | уникальный id в обратной доменной записи: `com.example.hello`. Префикс `pw.komet.` занят встроенными плагинами | | `name` | да | название в списке плагинов | | `version` | да | SemVer: `1.2.3`, `1.2.3-beta.1`, `1.2.3+5` | | `apiVersion` | да | версия `komet:api`, которую ждёт плагин. Сейчас `1`; плагин с большей версией не установится | | `description` | нет | описание в списке плагинов | | `author` | нет | автор | | `main` | да | путь к главному модулю внутри пакета, `.js` | | `permissions` | да | разрешения, см. ниже. Может быть пустым массивом | | `commands` | да | хотя бы одна команда | | `updateUrl` | нет | HTTPS-адрес манифеста обновлений, см. «Обновления» | | `signature` | нет | подпись; её добавляет `tool/kinet_sign.dart` | ### Команды | Поле | Обязательно | Описание | |---------------|-------------|----------| | `name` | да | `/` и латиница, цифры, `_`, `-`, до 32 символов после `/`. Ведущий `/` можно не писать | | `description` | да | текст в подсказке команд | | `handler` | да | имя функции, экспортированной из `main` | | `arguments` | нет | аргументы, см. ниже | | `hidden` | нет | `true` — команда работает, но не показывается в подсказках | Имена команд внутри плагина не должны повторяться без учёта регистра. Если имя уже занято встроенной командой или другим плагином, команда не появится. ### Аргументы | Поле | По умолчанию | Описание | |---------------|--------------|----------| | `name` | — | латиница, цифры, `_`, `-`, до 32 символов, начинается с буквы | | `description` | `""` | подпись поля ввода | | `required` | `true` | без значения команда не запустится, Komet покажет формат вызова | | `rest` | `false` | забирает весь остаток строки; только у последнего аргумента | Аргументы разделяются пробелами. Значение с пробелами берут в двойные или одинарные кавычки, внутри кавычек `\` экранирует следующий символ. Если у команды есть аргументы, при её выборе в подсказке Komet показывает поле для каждого из них. ## Контекст команды Обработчик получает один объект: | Поле | Тип | Описание | |--------------|--------------------------|----------| | `args` | `string` | всё, что написано после имени команды | | `arguments` | `Record` | значения аргументов по имени; не заданные — пустая строка | | `reply` | `object \| null` | сообщение, на которое отвечает пользователь. Только с разрешением `message.readReply`, иначе `null` | | `apiVersion` | `number` | версия `komet:api` в приложении | `reply`: `{ id, senderId, text, time, attachments: [{ type }] }`. `time` — миллисекунды Unix, `text` может быть `null`, `type` — `photo`, `video`, `audio`, `file`, `sticker` и другие значения из `komet-api.d.ts`. ## Разрешения Пользователь видит список разрешений при установке и выдаёт их все сразу. Вызов без нужного разрешения завершается ошибкой `Нет разрешения `. | Разрешение | Что даёт | |---------------------|----------| | `chat.write` | `chat.sendText` | | `chat.edit` | `chat.editText` | | `chat.photo` | `chat.sendPhoto` | | `chat.file` | `chat.sendFile` | | `ui.notify` | `ui.notify` | | `contact.read` | `contact.getPeer` | | `message.readReply` | `context.reply` | | `network` | `network.fetch` и загрузка по `url` в `chat.sendPhoto` / `chat.sendFile` | | `storage` | `storage.get`, `storage.set`, `storage.remove` | Методы `runtime.*` разрешений не требуют. ## API `komet:api` ```js import { chat, network, ui, contact, runtime, storage } from 'komet:api'; ``` Все методы возвращают `Promise`. При ошибке `Promise` отклоняется с `Error`, в `message` — причина. ### chat `chat.sendText(text)` → `Promise` — отправляет сообщение в текущий чат и возвращает его id. Текст — до 65 536 символов. В зашифрованном чате текст шифруется так же, как обычное сообщение. `chat.editText(messageId, text)` → `Promise` — меняет текст сообщения. Можно редактировать только сообщения, отправленные этим же запуском команды. `chat.sendPhoto({ base64, url, filename, caption })` → `Promise` — отправляет фото до 15 МБ. Нужен `base64` или `url`; если заданы оба, берётся `base64`. Загрузка по `url` требует разрешения `network`. `chat.sendFile({ base64, url, filename })` → `Promise` — отправляет файл до 25 МБ, источник задаётся так же. В зашифрованных чатах пока недоступен. В `filename` символы кроме латиницы, кириллицы, цифр, `.`, `_`, `-` и пробела заменяются на `_`, длина обрезается до 120 символов. Загрузка по `url` принимает только HTTPS, следует не более чем 5 редиректам и делает до 3 попыток. ### network `network.fetch(url, { method, headers, body })` → `Promise<{ status, headers, body, base64 }>`. - Только `https://`. `localhost` и частные IP-адреса запрещены. - Методы: `GET` (по умолчанию), `POST`, `PUT`, `PATCH`, `DELETE`. - Редиректы не выполняются: ответ 301, 302, 303, 307 или 308 завершается ошибкой. - Заголовки `Host`, `Content-Length`, `Connection`, `Proxy-Authorization` игнорируются. - `body` — строка. Объект сначала превратите в JSON: `JSON.stringify(data)`. - Таймауты: соединение 10 с, заголовки ответа 15 с, тело 20 с. Ответ — до 1 МБ. - `body` ответа — текст в UTF-8 (некорректные байты заменяются), `base64` — те же байты без перекодирования, например для `chat.sendPhoto`. - Коды 4xx и 5xx не считаются ошибкой — проверяйте `status`. ### ui `ui.notify(message)` → `Promise` — показывает уведомление Komet внутри приложения, пока открыт чат. ### contact `contact.getPeer()` → `Promise` — данные собеседника в личном диалоге, всегда свежие с сервера. Вне диалога возвращает `null`. `Peer`: `{ id, displayName, country, registrationTime, updateTime, options }`. `registrationTime` и `updateTime` — миллисекунды Unix или `null`, `options` — массив строк-флагов профиля. ### runtime `runtime.sleep(milliseconds)` → `Promise` — пауза от 0 до 10 000 мс. `runtime.isOnline()` → `Promise` — есть ли соединение с сервером. `runtime.isActive()` → `Promise` — открыт ли ещё чат, из которого вызвана команда. Проверяйте в длинных сценариях, чтобы не работать впустую. ### storage Постоянное хранилище плагина. Оно переживает обновления и удаляется вместе с плагином. `storage.get(key)` → `Promise` — значение или `null`, если ключа нет. `storage.set(key, value)` → `Promise` — сохраняет любое JSON-значение. `storage.remove(key)` → `Promise` — удаляет ключ. Ключ — от 1 до 64 символов из `A-Z a-z 0-9 . _ -`. Всё хранилище плагина в JSON — до 64 КБ. ## Среда выполнения - Движок — QuickJS. Доступны стандартные объекты языка: `Promise`, `async`/`await`, `JSON`, `Date`, `Math`, `Map`, `RegExp` и т. д. - Нет `console`, `setTimeout`, глобального `fetch`, `require` и модулей Node. Вместо них — `runtime.sleep` и `network.fetch`. - Импортировать можно только `komet:api` и модули своего пакета по относительному пути (`./utils.js`). Выход за пределы пакета (`../`) запрещён. - Каждый запуск команды — новый движок: переменные модуля между запусками не сохраняются, для этого есть `storage`. - Лимиты: 16 МБ памяти, 256 КБ стека, 30 секунд на запуск. По истечении времени движок останавливается. - Необработанное исключение показывается пользователю как «Ошибка плагина: …». ## Пакет `.kinet` - zip-архив до 5 МБ, не больше 128 файлов; - каждый файл до 2 МБ, всё вместе в распакованном виде до 10 МБ; - только `manifest.json` в корне и файлы `.js`; - без символических ссылок, абсолютных путей и `..`; - файл `main` должен быть в архиве. ## Подпись Подписанный плагин показывает при установке отпечаток ключа автора. Обновления подписанного плагина принимаются, только если они подписаны тем же ключом; снять подпись в обновлении тоже нельзя. ```bash dart run tool/kinet_sign.dart generate-key ~/.komet/hello-key.json dart run tool/kinet_sign.dart sign hello.kinet ~/.komet/hello-key.json hello-signed.kinet dart run tool/kinet_sign.dart verify hello-signed.kinet ``` `generate-key` создаёт ключ Ed25519 с правами `0600` и печатает отпечаток. `sign` добавляет в манифест поле `signature`; без третьего аргумента пакет перезаписывается на месте. Подпись покрывает манифест без поля `signature` и SHA-256 каждого `.js` модуля, поэтому после подписи пакет менять нельзя. Храните приватный ключ в секрете. Если он потерян, обновить плагин у пользователей не получится — им придётся удалить его и установить заново. ## Обновления Укажите в манифесте `updateUrl` — HTTPS-адрес JSON-файла: ```json { "$schema": "https://raw.githubusercontent.com/KometTeam/Komet/main/plugin_sdk/update-manifest.schema.json", "version": "1.1.0", "packageUrl": "hello-1.1.0.kinet", "size": 2048, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" } ``` | Поле | Описание | |--------------|----------| | `version` | версия нового пакета, совпадает с `version` в его манифесте | | `packageUrl` | адрес `.kinet`, абсолютный или относительный к `updateUrl`; итоговый адрес — только HTTPS | | `size` | размер пакета в байтах; `0` отключает проверку | | `sha256` | SHA-256 пакета в hex (`shasum -a 256 hello.kinet`); пустая строка отключает проверку | Пользователь проверяет обновления из меню плагина. Komet предлагает обновление, только если `version` больше установленной по правилам SemVer, и отказывается его ставить, если: - у пакета другой `id` или `version`; - не сошлись `size` или `sha256`; - обновление просит разрешения, которых нет у установленной версии, — такой плагин нужно переустановить; - пакет подписан другим ключом или подпись снята.