# Верстак — архитектура плагинов ## 1. Принцип Верстак — минималистичный движок с деревом дел и базовыми операциями (заметки, файлы, действия, журнал, поиск, активность). Плагины расширяют функциональность без перекомпиляции: - новые типы узлов и кастомные поля; - календарь, канбан, импортёры; - UI-элементы (пункты сайдбара, вкладки, страницы); - фоновые задачи по расписанию; - хуки на события (создание/удаление/изменение узлов). ## 2. Структура плагина ``` .verstak/plugins// 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::` - При клике — фронтенд переключается на эту страницу, отправляет 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//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//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//`). ```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 # Включить verstak plugin disable # Выключить verstak plugin install # Установить из registry (TODO) verstak plugin remove # Удалить ``` ### Структура пакета Плагин распространяется как 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`