306 lines
17 KiB
Markdown
306 lines
17 KiB
Markdown
# Плагины 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<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`
|
||
|
||
```js
|
||
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` должен быть в архиве.
|
||
|
||
## Подпись
|
||
|
||
Подписанный плагин показывает при установке отпечаток ключа автора. Обновления
|
||
подписанного плагина принимаются, только если они подписаны тем же ключом; снять
|
||
подпись в обновлении тоже нельзя.
|
||
|
||
```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`;
|
||
- обновление просит разрешения, которых нет у установленной версии, — такой плагин
|
||
нужно переустановить;
|
||
- пакет подписан другим ключом или подпись снята.
|