verstak/docs/09_Extensibility.md

645 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Верстак — архитектура плагинов
## 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 — полная схема
```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
```json
{
"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
```json
{
"id": "events",
"label": "События",
"page": "plugin:calendar:node_events"
}
```
- Появляется как дополнительная вкладка в карточке дела
- Вкладка видна для всех типов узлов, если не указан `node_type_filter`
- При переключении — фронтенд запрашивает данные через Lua API
### 2.4 BackgroundTask
```json
{
"id": "check_events",
"interval": "5m",
"script": "check_events.lua"
}
```
- `interval` — интервал в человекочитаемом формате: `5m`, `1h`, `30s`
- `script` — путь к Lua-файлу относительно корня плагина
- Скрипт выполняется в том же Lua-стейте, что и main.lua, с полным доступом к API
## 3. Lua API — полный справочник
Плагины пишутся на Lua (gopher-lua). Доступны следующие модули:
### 3.1 `verstak.node` — работа с деревом дел
```lua
-- Получить узел по 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` — файлы
```lua
-- Получить файлы узла
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` — журнал работ
```lua
-- Добавить запись
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` — лог активности
```lua
-- Записать событие
verstak.activity.log({
node_id = node_id,
event_type = "calendar_event",
title = "Встреча с клиентом",
description = "Обсуждение макета",
confidence = "high"
})
```
### 3.5 `verstak.config` — конфигурация плагина
```lua
-- Читать
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`.
```lua
-- Выполнить запрос (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
```lua
-- Показать уведомление (тост)
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>/`).
```lua
-- 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, можно создавать
задачи динамически:
```lua
-- Каждые 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` — разделяемое состояние между хуками/тасками
```lua
-- Сохранить значение в памяти плагина
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`
```lua
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`
```lua
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`
```lua
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`
```lua
function on_node_delete(node)
-- Вызывается при удалении узла.
-- Плагин может почистить свои данные.
verstak.db.exec("DELETE FROM events WHERE node_id = ?", node.id)
end
```
### 4.5 `on_shutdown`
```lua
function on_shutdown()
-- При закрытии программы.
-- Сохранить состояние, закрыть соединения.
end
```
### 4.6 `on_page_open` (автоматический)
Когда пользователь переходит на страницу плагина (`plugin:calendar:main`),
фронтенд посылает событие в Lua:
```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-код:
1. Читает `plugin.json`
2. Из `ui.sidebar_items` регистрирует пункты в общем реестре сайдбара
3. Из `ui.node_tabs` регистрирует вкладки для карточки дела
4. Из `ui.node_actions` регистрирует кнопки действий
5. Из `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-переменная,
инжектится при рендеринге панели). Механизм:
1. Фронтенд открывает страницу `plugin:calendar:main`
2. Go бэкенд вызывает Lua `on_page_open("main", params)`
3. Lua сохраняет данные через `verstak.state.set("page_data", ...)`
4. Фронтенд вызывает `GetPluginPageData("calendar", "main")`
5. Go возвращает Lua-данные, фронтенд инжектит их в iframe
### 5.4 Кастомные типы узлов
Плагины могут регистрировать новые типы узлов через `node_types` в plugin.json
и/или через Lua API:
```lua
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:
```json
{
"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-команды
```bash
verstak plugin list # Список плагинов
verstak plugin enable <name> # Включить
verstak plugin disable <name> # Выключить
verstak plugin install <name> # Установить из registry (TODO)
verstak plugin remove <name> # Удалить
```
### Структура пакета
Плагин распространяется как zip-архив:
```bash
calendar-1.0.0.vpk # Verstak Plugin Package
plugin.json
main.lua
panels/calendar.html
migrations/001_create_tables.sql
```
Установка из registry (будущее):
```bash
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`