verstak/docs/09_Extensibility.md

26 KiB
Raw Blame History

Верстак — архитектура плагинов

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, 30s
  • script — путь к 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-код:

  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:

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