Files
IKAR/PLUGINS.md
T

17 KiB
Raw Blame History

Плагины 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:

{
  "$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:

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 должен лежать в корне архива:

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<string, string> значения аргументов по имени; не заданные — пустая строка
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.

Разрешения

Пользователь видит список разрешений при установке и выдаёт их все сразу. Вызов без нужного разрешения завершается ошибкой Нет разрешения <id>.

Разрешение Что даёт
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

import { chat, network, ui, contact, runtime, storage } from 'komet:api';

Все методы возвращают Promise. При ошибке Promise отклоняется с Error, в message — причина.

chat

chat.sendText(text) → Promise<string> — отправляет сообщение в текущий чат и возвращает его id. Текст — до 65 536 символов. В зашифрованном чате текст шифруется так же, как обычное сообщение.

chat.editText(messageId, text) → Promise<void> — меняет текст сообщения. Можно редактировать только сообщения, отправленные этим же запуском команды.

chat.sendPhoto({ base64, url, filename, caption }) → Promise<void> — отправляет фото до 15 МБ. Нужен base64 или url; если заданы оба, берётся base64. Загрузка по url требует разрешения network.

chat.sendFile({ base64, url, filename }) → Promise<void> — отправляет файл до 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<void> — показывает уведомление Komet внутри приложения, пока открыт чат.

contact

contact.getPeer() → Promise<Peer | null> — данные собеседника в личном диалоге, всегда свежие с сервера. Вне диалога возвращает null.

Peer: { id, displayName, country, registrationTime, updateTime, options }. registrationTime и updateTime — миллисекунды Unix или null, options — массив строк-флагов профиля.

runtime

runtime.sleep(milliseconds) → Promise<void> — пауза от 0 до 10 000 мс.

runtime.isOnline() → Promise<boolean> — есть ли соединение с сервером.

runtime.isActive() → Promise<boolean> — открыт ли ещё чат, из которого вызвана команда. Проверяйте в длинных сценариях, чтобы не работать впустую.

storage

Постоянное хранилище плагина. Оно переживает обновления и удаляется вместе с плагином.

storage.get(key) → Promise<value | null> — значение или null, если ключа нет.

storage.set(key, value) → Promise<void> — сохраняет любое JSON-значение.

storage.remove(key) → Promise<void> — удаляет ключ.

Ключ — от 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 должен быть в архиве.

Подпись

Подписанный плагин показывает при установке отпечаток ключа автора. Обновления подписанного плагина принимаются, только если они подписаны тем же ключом; снять подпись в обновлении тоже нельзя.

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-файла:

{
  "$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;
  • обновление просит разрешения, которых нет у установленной версии, — такой плагин нужно переустановить;
  • пакет подписан другим ключом или подпись снята.