26 KiB
Верстак — архитектура плагинов
1. Принцип
Верстак — минималистичный движок с деревом дел и базовыми операциями (заметки, файлы, действия, журнал, поиск, активность).
Плагины расширяют функциональность без перекомпиляции:
- новые типы узлов и кастомные поля;
- календарь, канбан, импортёры;
- UI-элементы (пункты сайдбара, вкладки, страницы);
- фоновые задачи по расписанию;
- хуки на события (создание/удаление/изменение узлов).
2. Структура плагина
.verstak/plugins/<name>/
plugin.json # метаданные: имя, версия, хуки, UI-contributions
main.lua # точка входа (обязательно, если есть Lua-код)
panels/ # HTML-панели для GUI (опционально)
calendar.html
kanban.html
templates/ # шаблоны дел (опционально)
client.json
repair.json
migrations/ # SQL-миграции (опционально)
001_create_tables.sql
assets/ # статика: иконки, CSS, изображения (опционально)
icon.svg
2.1 plugin.json — полная схема
{
"name": "calendar",
"version": "1.0.0",
"author": "...",
"description": "Календарь событий, привязанных к делам",
"hooks": {
"on_init": "on_init",
"on_vault_open": "on_vault_open",
"on_node_create": "on_node_create",
"on_node_delete": "on_node_delete",
"on_shutdown": "on_shutdown"
},
"ui": {
"sidebar_items": [
{
"id": "calendar",
"label": "Календарь",
"icon": "calendar",
"page": "plugin:calendar:main"
}
],
"node_tabs": [
{
"id": "events",
"label": "События",
"page": "plugin:calendar:node_events"
}
],
"node_actions": [
{
"id": "add_event",
"label": "Добавить событие",
"icon": "plus",
"page": "plugin:calendar:add_event"
}
],
"settings_pages": [
{
"id": "calendar",
"label": "Календарь",
"page": "plugin:calendar:settings"
}
]
},
"background_tasks": [
{
"id": "check_events",
"interval": "5m",
"script": "check_events.lua"
}
],
"node_types": ["event"],
"panel": "panels/calendar.html",
"migrations": ["migrations/001_create_tables.sql"]
}
Поля plugin.json
| Поле | Тип | Описание |
|---|---|---|
name |
string | Уникальное имя плагина |
version |
string | SemVer |
hooks |
map[string]string | Имя Lua-функции для каждого события |
ui.sidebar_items |
[SidebarItem] | Пункты сайдбара |
ui.node_tabs |
[NodeTab] | Вкладки в карточке дела |
ui.node_actions |
[NodeAction] | Кнопки действий в деле |
ui.settings_pages |
[SettingsPage] | Страницы настроек |
background_tasks |
[BackgroundTask] | Фоновые задачи по расписанию |
node_types |
[string] | Имена новых типов узлов, которые регистрирует плагин |
panel |
string | HTML-файл панели (главная страница плагина) |
migrations |
[string] | SQL-файлы миграций |
2.2 SidebarItem
{
"id": "calendar",
"label": "Календарь",
"icon": "calendar",
"page": "plugin:calendar:main"
}
id— уникальный в рамках плагинаicon— имя иконки из библиотеки GUI (или путь кassets/icon.svg)page— идентификатор страницы в форматеplugin:<name>:<page_id>- При клике — фронтенд переключается на эту страницу, отправляет Lua-скрипту событие
on_page_open
2.3 NodeTab
{
"id": "events",
"label": "События",
"page": "plugin:calendar:node_events"
}
- Появляется как дополнительная вкладка в карточке дела
- Вкладка видна для всех типов узлов, если не указан
node_type_filter - При переключении — фронтенд запрашивает данные через Lua API
2.4 BackgroundTask
{
"id": "check_events",
"interval": "5m",
"script": "check_events.lua"
}
interval— интервал в человекочитаемом формате:5m,1h,30sscript— путь к Lua-файлу относительно корня плагина- Скрипт выполняется в том же Lua-стейте, что и main.lua, с полным доступом к API
3. Lua API — полный справочник
Плагины пишутся на Lua (gopher-lua). Доступны следующие модули:
3.1 verstak.node — работа с деревом дел
-- Получить узел по ID
local node = verstak.node.get(id)
-- { id, parent_id, type, title, slug, sort_order, created_at, updated_at, section }
-- Создать узел
local n = verstak.node.create(parent_id, "type", "title")
-- Можно с дополнительными полями:
local n = verstak.node.create(parent_id, "type", "title", { section = "work" })
-- Создать дочерний узел (сокращение)
local n = verstak.node.create_child(parent_id, "type", "title")
-- Обновить узел
verstak.node.update(id, { title = "Новый заголовок", section = "personal" })
-- Удалить узел (в корзину)
verstak.node.delete(id)
-- Найти узел по slug (относительно parent)
local n = verstak.node.find_by_slug(parent_id, "my-slug")
-- Поиск по дереву
local results = verstak.node.search("query")
-- results = [{ id, title, type, match_column, snippet }]
-- Получить детей узла
local children = verstak.node.children(parent_id)
-- children = [{ id, title, type, ... }]
-- Получить полный путь от корня
local path = verstak.node.path(id)
-- path = [{ id, title, type }, ...]
3.2 verstak.file — файлы
-- Получить файлы узла
local files = verstak.file.list(node_id)
-- Добавить файл (копировать в vault)
local f = verstak.file.add(node_id, "/path/to/file.pdf")
-- Удалить (в корзину)
verstak.file.delete(file_id)
-- Получить содержимое текстового файла
local content = verstak.file.read_text(file_id)
3.3 verstak.worklog — журнал работ
-- Добавить запись
verstak.worklog.add(node_id, minutes, "Описание работы")
-- Добавить с датой
verstak.worklog.add(node_id, minutes, "Описание", { date = "2025-06-07", billable = true })
-- Список записей узла
local entries = verstak.worklog.list(node_id, { limit = 10, offset = 0 })
-- Сводка
local summary = verstak.worklog.summary(node_id, { since = "2025-01-01", until = "2025-12-31" })
3.4 verstak.activity — лог активности
-- Записать событие
verstak.activity.log({
node_id = node_id,
event_type = "calendar_event",
title = "Встреча с клиентом",
description = "Обсуждение макета",
confidence = "high"
})
3.5 verstak.config — конфигурация плагина
-- Читать
local val = verstak.config.get("calendar.default_view")
-- val == nil если ключ не задан, иначе { key, value, plugin }
-- Писать (сохраняется в .verstak/plugins/<name>/config.json)
verstak.config.set("calendar.default_view", "month")
verstak.config.set("calendar.working_hours", { start = 9, end = 18 })
Конфиг плагина изолирован: ключи автоматически префиксируются именем плагина.
3.6 verstak.db — собственное хранилище плагина
Плагин может создавать и читать свои таблицы в отдельном SQLite-файле
.verstak/plugins/<name>/data.db.
-- Выполнить запрос (INSERT/UPDATE/DELETE)
verstak.db.exec("INSERT INTO events (title, date, node_id) VALUES (?, ?, ?)",
"Встреча", "2025-06-15", node_id)
-- Запрос с результатами
local rows = verstak.db.query("SELECT * FROM events WHERE date >= ? ORDER BY date", "2025-06-01")
-- rows = [{ id, title, date, node_id }, ...]
-- Одна строка
local row = verstak.db.query_row("SELECT * FROM events WHERE id = ?", event_id)
-- row = { id, title, date, node_id } или nil
3.7 verstak.ui — взаимодействие с GUI
-- Показать уведомление (тост)
verstak.ui.toast("Событие добавлено")
verstak.ui.toast("Что-то пошло не так", { type = "error", duration = 5000 })
-- Навигация
verstak.ui.navigate_to("plugin:calendar:main", { date = "2025-06-07" })
-- Обновить данные на текущей странице
verstak.ui.refresh()
-- Установить заголовок окна
verstak.ui.set_title("Календарь — Июнь 2025")
-- Открыть диалог
local result = verstak.ui.confirm("Удалить событие?")
-- result = true/false
3.8 verstak.http — HTTP-эндпоинты
Плагины могут регистрировать HTTP-эндпоинты на внутреннем сервере
(тот же порт, что и Bridge/Extension, но с префиксом /api/plugins/<name>/).
-- GET /api/plugins/calendar/events?month=2025-06
verstak.http.route("GET", "/events", function(params)
local events = verstak.db.query("SELECT * FROM events WHERE date LIKE ?", params.month .. "%")
return { json = events }
end)
-- POST /api/plugins/calendar/events
verstak.http.route("POST", "/events", function(body)
verstak.db.exec("INSERT INTO events (...) VALUES (?,?)", body.title, body.date)
return { json = { ok = true } }
end)
Эндпоинты аутентифицируются через X-Verstak-Secret (как и Bridge).
3.9 verstak.schedule — фоновые задачи (из Lua)
Помимо декларативных background_tasks в plugin.json, можно создавать
задачи динамически:
-- Каждые 15 минут
local timer_id = verstak.schedule.every("15m", function()
local events = verstak.db.query("SELECT * FROM events WHERE date = date('now')")
if #events > 0 then
verstak.ui.toast("Сегодня " .. #events .. " событий")
end
end)
-- Отменить
verstak.schedule.cancel(timer_id)
-- Однократно через N секунд
verstak.schedule.once("30s", function()
verstak.ui.toast("30 секунд прошло")
end)
-- Каждое N-е число месяца (cron-подобно)
verstak.schedule.cron("0 9 * * 1", function()
-- Каждый понедельник в 9 утра
verstak.ui.toast("Планирование недели")
end)
3.10 verstak.state — разделяемое состояние между хуками/тасками
-- Сохранить значение в памяти плагина
verstak.state.set("event_count", 42)
-- Прочитать
local n = verstak.state.get("event_count")
-- Удалить
verstak.state.delete("event_count")
Состояние хранится только в runtime — при перезапуске теряется.
Для постоянных данных используйте verstak.config или verstak.db.
4. Хуки — жизненный цикл
Имя в plugin.json → имя Lua-функции, которая вызывается в main.lua.
4.1 on_init
function on_init()
-- Вызывается при старте программы, ДО открытия vault.
-- Здесь плагин может выполнить миграции БД,
-- зарегистрировать свои таблицы.
-- ВАЖНО: vault ещё не открыт, verstak.node недоступен.
verstak.db.exec([[CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
date TEXT NOT NULL,
node_id TEXT,
created_at TEXT DEFAULT (datetime('now'))
)]])
end
4.2 on_vault_open
function on_vault_open(vault_path)
-- Вызывается после открытия vault.
-- verstak.node, verstak.file уже доступны.
-- Можно зарегистрировать HTTP-эндпоинты.
verstak.http.route("GET", "/events", list_events)
end
4.3 on_node_create
function on_node_create(node)
-- node = { id, parent_id, type, title, section, ... }
-- Вызывается после создания любого узла.
-- Нельзя отменить создание (позднее — вернём before_create).
if node.type == "event" then
verstak.activity.log({
node_id = node.id,
event_type = "calendar_event_created",
title = "Создано событие: " .. node.title
})
end
end
4.4 on_node_delete
function on_node_delete(node)
-- Вызывается при удалении узла.
-- Плагин может почистить свои данные.
verstak.db.exec("DELETE FROM events WHERE node_id = ?", node.id)
end
4.5 on_shutdown
function on_shutdown()
-- При закрытии программы.
-- Сохранить состояние, закрыть соединения.
end
4.6 on_page_open (автоматический)
Когда пользователь переходит на страницу плагина (plugin:calendar:main),
фронтенд посылает событие в Lua:
-- Не нужно объявлять в plugin.json — вызывается автоматически
function on_page_open(page_id, params)
-- page_id = "main" или "node_events"
-- params = { node_id = "...", date = "2025-06-07" }
if page_id == "main" then
local events = verstak.db.query("SELECT * FROM events ORDER BY date")
-- Данные будут отправлены фронтенду через Wails binding
verstak.state.set("page_data", events)
-- Фронтенд получит данные через вызов GetPluginPageData()
end
end
5. UI-интеграция — как это работает
┌──────────────────────────────────────────────────┐
│ Svelte Frontend │
│ │
│ ┌──────────────┬──────────────────────────────┐ │
│ │ Sidebar │ Main Workspace │ │
│ │ │ │ │
│ │ 📁 Дела │ ┌─ plugin:calendar:main ──┐ │ │
│ │ 📅 Календарь │ │ [Calendar HTML Panel] │ │ │
│ │ ⚙ Настройки │ │ │ │ │
│ │ │ └────────────────────────┘ │ │
│ └──────────────┴──────────────────────────────┘ │
│ │
│ Go Backend (Wails Bindings) │
│ ┌─────────────────────────────────────────┐ │
│ │ PluginManager │ │
│ │ ├─ Lua Runtime (gopher-lua) │ │
│ │ ├─ Scheduler │ │
│ │ └─ HTTP Server │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
5.1 Регистрация UI-компонентов
При старте плагина Go-код:
- Читает
plugin.json - Из
ui.sidebar_itemsрегистрирует пункты в общем реестре сайдбара - Из
ui.node_tabsрегистрирует вкладки для карточки дела - Из
ui.node_actionsрегистрирует кнопки действий - Из
ui.settings_pagesрегистрирует страницы настроек
Фронтенд читает реестр через Wails binding и рендерит соответствующие элементы UI.
5.2 HTML-панели
Плагин может поставлять HTML-панель (panel в plugin.json или panels/).
Панель рендерится в iframe внутри основного окна. Это изолирует DOM плагина от DOM приложения.
Каналы связи с бэкендом:
- Данные: Wails binding
GetPluginPageData(plugin, page)— Lua возвращает текущиеpage_data - Действия: Wails binding
CallPluginAction(plugin, action, params)— вызываетon_actionв Lua - Уведомления: Lua → фронтенд через
verstak.ui.toast()(WS или polling) - Навигация: iframe может сообщить родителю о смене страницы через
window.parent.postMessage
Важно: HTML-панели — временное решение. В будущем плагины смогут поставлять Svelte-компоненты, но это требует динамической компиляции и горячей перезагрузки. Для MVP — iframe достаточно.
5.3 Данные для панели
Панель получает данные через __VERSTAK_DATA__ (глобальная JS-переменная,
инжектится при рендеринге панели). Механизм:
- Фронтенд открывает страницу
plugin:calendar:main - Go бэкенд вызывает Lua
on_page_open("main", params) - Lua сохраняет данные через
verstak.state.set("page_data", ...) - Фронтенд вызывает
GetPluginPageData("calendar", "main") - Go возвращает Lua-данные, фронтенд инжектит их в iframe
5.4 Кастомные типы узлов
Плагины могут регистрировать новые типы узлов через node_types в plugin.json
и/или через Lua API:
verstak.node.register_type({
name = "event",
label = "Событие",
icon = "calendar",
fields = {
{ name = "date", label = "Дата", type = "date" },
{ name = "time", label = "Время", type = "time" },
{ name = "location", label = "Место", type = "text" },
{ name = "color", label = "Цвет", type = "select", options = {"red","blue","green"} },
}
})
GUI рисует карточку дела на основе зарегистрированных полей. Поля сохраняются как метаданные узла.
6. Песочница и безопасность
Lua-плагины работают в изолированной среде:
- Нет доступа к ФС напрямую — только через
verstak.*API (который проверяет пути) - Нет
io.*,os.execute,os.popen— библиотеки удалены - Нет
loadlib— динамическая загрузка C-библиотек запрещена - Память: лимит на размер Lua-стейта (настраивается, по умолчанию 32 MB)
- Инструкции: защита от бесконечных циклов (максимум N инструкций на вызов, default 10M)
- Сеть: никаких сокетов из Lua — только через
verstak.http(контролируемый роутинг) - Таймаут: каждый Lua-вызов (хук, HTTP-обработчик, таска) имеет таймаут 30s
Конфигурация песочницы
В config.json vault:
{
"plugins": {
"sandbox": {
"max_memory_mb": 32,
"max_instructions": 10000000,
"call_timeout_ms": 30000,
"allow_plugins": ["calendar", "kanban"]
}
}
}
7. Жизненный цикл — детально
┌─────────────────────────────────────────────────────────────┐
│ Старт программы │
│ │
│ 1. Инициализация config, storage │
│ 2. PluginManager.NewManager(vaultRoot) │
│ 3. Discover() — сканирование .verstak/plugins/*/plugin.json │
│ 4. Для каждого активного плагина: │
│ a. Выполнить SQL-миграции │
│ b. Создать Lua-стейт (песочница) │
│ c. Загрузить main.lua │
│ d. Зарегистрировать node_types, ui. sidebar/tabs/actions │
│ e. Вызвать on_init() │
│ 5. Открытие vault │
│ 6. Для каждого плагина: вызвать on_vault_open(path) │
│ 7. Запуск фоновых задач (scheduler) │
│ │
│ ┌──── Эксплуатация ──────────────────────────────────────┐ │
│ │ - Хуки на события дерева │ │
│ │ - HTTP-запросы → Lua-обработчики │ │
│ │ - UI-навигация → on_page_open() │ │
│ │ - Фоновые задачи по расписанию │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 8. Закрытие программы │
│ 9. Для каждого плагина: on_shutdown() → закрыть Lua-стейт │
│ 10. Остановка scheduler │
└─────────────────────────────────────────────────────────────┘
8. Распространение и CLI
CLI-команды
verstak plugin list # Список плагинов
verstak plugin enable <name> # Включить
verstak plugin disable <name> # Выключить
verstak plugin install <name> # Установить из registry (TODO)
verstak plugin remove <name> # Удалить
Структура пакета
Плагин распространяется как zip-архив:
calendar-1.0.0.vpk # Verstak Plugin Package
plugin.json
main.lua
panels/calendar.html
migrations/001_create_tables.sql
Установка из registry (будущее):
verstak plugin install calendar
# → скачивает calendar-1.0.0.vpk
# → распаковывает в .verstak/plugins/calendar/
# → выполняет миграции
# → активирует
9. Пример: Calendar plugin
(См. contrib/plugins/calendar/ — будет реализован как эталон.)
Плагин calendar демонстрирует:
- Полный цикл: plugin.json → main.lua → panel → миграции
- Регистрацию пункта в сайдбаре
- Свою HTML-панель с календарной сеткой
- Создание/удаление событий через Lua API
- Привязку событий к делам
- Фоновую задачу (проверка событий на сегодня)
- Кастомный тип узла
event - Настройки плагина через
verstak.config