feat: плагин-система Lua + Calendar reference plugin
- Lua VM runtime: gopher-lua с песочницей, хуки on_init/on_tick/on_shutdown - API: verstak.node.* / verstak.db.* / verstak.config.* / verstak.state.* - API: verstak.worklog.* / verstak.activity.* / verstak.file.* - API: verstak.schedule.* / verstak.http.* / verstak.ui.* - Менеджер плагинов: жизненный цикл, инициализация, шаблоны - Scheduler: фоновые задачи с интервалами - PluginPage.svelte: контейнер для iframe-панелей плагинов - Calendar plugin: миграция, категории CRUD, события CRUD - Calendar: расширенный рекарренс (daily/weekly/monthly/yearly) - Calendar: связь с узлами Верстака, напоминания, HTTP-праздники - Calendar: Lua-тест-сьют (15 тестов), Go-интеграционный тест - fix: query_row использует реальные Column() вместо guessColumns
This commit is contained in:
+3
-3
@@ -29,10 +29,10 @@
|
||||
## Файлы пакета
|
||||
|
||||
1. [[01_Product_Spec]] — описание продукта, аудитория, сценарии.
|
||||
2. [[02_Architecture]] — архитектура core/GUI/TUI/CLI/server, плагины.
|
||||
2. [[02_Architecture]] — архитектура core/GUI/CLI/server, плагины.
|
||||
3. [[03_Data_Model_Storage]] — модель данных, SQLite, vault, files, notes.
|
||||
4. [[04_Sync_Backup_Activity]] — синхронизация, backup, activity.
|
||||
5. [[05_UI_UX]] — экраны GUI/TUI.
|
||||
5. [[05_UI_UX]] — экраны GUI.
|
||||
6. [[06_Roadmap]] — план разработки по этапам.
|
||||
7. [[07_AI_Coder_Prompts]] — промпты для ИИ-кодера.
|
||||
8. [[08_MVP_Checklist]] — чеклист первого MVP.
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
5. **Не таймтрекер, а восстановитель следов.**
|
||||
|
||||
6. **GUI основной, TUI быстрый, CLI служебный.**
|
||||
6. **GUI основной, CLI служебный.**
|
||||
|
||||
7. **Sync не уничтожает данные.**
|
||||
|
||||
|
||||
+1
-25
@@ -28,7 +28,7 @@
|
||||
|
||||
### Core Library
|
||||
|
||||
Общее ядро, которое не зависит от GUI/TUI.
|
||||
Общее ядро, которое не зависит от GUI.
|
||||
|
||||
Отвечает за:
|
||||
|
||||
@@ -72,27 +72,6 @@
|
||||
- импорт DokuWiki;
|
||||
- разбор конфликтов.
|
||||
|
||||
### TUI
|
||||
|
||||
Быстрый интерфейс из терминала.
|
||||
|
||||
Рекомендуемый стек:
|
||||
|
||||
- Bubble Tea;
|
||||
- Bubbles;
|
||||
- Lip Gloss.
|
||||
|
||||
TUI не обязан повторять весь GUI.
|
||||
|
||||
Функции:
|
||||
|
||||
- найти дело;
|
||||
- открыть заметку;
|
||||
- добавить worklog;
|
||||
- запустить action;
|
||||
- запустить sync;
|
||||
- посмотреть “Сегодня”.
|
||||
|
||||
### CLI
|
||||
|
||||
CLI нужен не как главный интерфейс, а как служебный вход.
|
||||
@@ -144,8 +123,6 @@ verstak/
|
||||
main.go
|
||||
verstak-gui/
|
||||
main.go
|
||||
verstak-tui/
|
||||
main.go
|
||||
verstak-server/
|
||||
main.go
|
||||
|
||||
@@ -164,7 +141,6 @@ verstak/
|
||||
config/
|
||||
|
||||
gui/
|
||||
tui/
|
||||
server/
|
||||
|
||||
frontend/
|
||||
|
||||
+1
-18
@@ -243,24 +243,7 @@ Actions:
|
||||
- редактировать название, URL и заметку;
|
||||
- удалить ссылку.
|
||||
|
||||
## 13. TUI UX
|
||||
|
||||
TUI нужен для быстрого:
|
||||
|
||||
- fuzzy search;
|
||||
- открыть дело;
|
||||
- добавить worklog;
|
||||
- запустить action;
|
||||
- sync now.
|
||||
|
||||
Не нужно делать в TUI:
|
||||
|
||||
- импорт DokuWiki;
|
||||
- сложный conflict resolver;
|
||||
- preview документов;
|
||||
- настройки sync.
|
||||
|
||||
## 14. CLI UX
|
||||
## 13. CLI UX
|
||||
|
||||
CLI команды:
|
||||
|
||||
|
||||
@@ -214,17 +214,6 @@ Acceptance:
|
||||
|
||||
- изменения файлов снаружи приложения обнаруживаются.
|
||||
|
||||
## Milestone 14 — TUI MVP (PAUSED)
|
||||
|
||||
- Bubble Tea;
|
||||
- tree/search;
|
||||
- add worklog;
|
||||
- run action;
|
||||
- sync.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- можно из терминала быстро работать с Верстаком.
|
||||
|
||||
## Milestone 15 — 1.0 polish (PAUSED)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- заметки и файлы физически лежат обычными файлами в vault;
|
||||
- SQLite хранит индекс, связи, метаданные, FTS и sync state;
|
||||
- основная сущность — вложенное дерево nodes;
|
||||
- GUI, TUI и CLI должны использовать общий core;
|
||||
- GUI и CLI должны использовать общий core;
|
||||
- бизнес-логику нельзя завязывать на UI;
|
||||
- не делать SaaS;
|
||||
- не делать ИИ;
|
||||
@@ -25,7 +25,6 @@
|
||||
- Go;
|
||||
- SQLite;
|
||||
- Wails для GUI;
|
||||
- Bubble Tea для TUI позже;
|
||||
- fsnotify + snapshot scanner позже;
|
||||
- sync server позже.
|
||||
|
||||
@@ -272,22 +271,7 @@ Acceptance:
|
||||
- manual command `verstak scan`.
|
||||
```
|
||||
|
||||
## Prompt 14 — TUI MVP
|
||||
|
||||
```markdown
|
||||
Добавь TUI на Bubble Tea.
|
||||
|
||||
Функции:
|
||||
- tree/search;
|
||||
- open node;
|
||||
- add worklog;
|
||||
- run action;
|
||||
- sync now.
|
||||
|
||||
TUI не должен повторять весь GUI.
|
||||
```
|
||||
|
||||
## Prompt 15 — Integrity/restore
|
||||
## Prompt 14 — Integrity/restore
|
||||
|
||||
```markdown
|
||||
Добавь команды:
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
- [ ] Conflict resolver UI.
|
||||
- [ ] Restore from sync server.
|
||||
- [ ] Backup export / retention snapshots.
|
||||
- [ ] TUI MVP.
|
||||
|
||||
## Не входит в первый MVP
|
||||
|
||||
|
||||
+575
-90
@@ -1,30 +1,36 @@
|
||||
# Верстак — архитектура плагинов
|
||||
|
||||
## Принцип
|
||||
## 1. Принцип
|
||||
|
||||
Верстак — это минималистичный движок с деревом дел.
|
||||
Всё, что не входит в минимальную модель, — плагин.
|
||||
Верстак — минималистичный движок с деревом дел и базовыми операциями
|
||||
(заметки, файлы, действия, журнал, поиск, активность).
|
||||
|
||||
Плагин — это директория в `.verstak/plugins/<name>/`, которую
|
||||
программа подхватывает без перекомпиляции.
|
||||
Плагины расширяют функциональность без перекомпиляции:
|
||||
- новые типы узлов и кастомные поля;
|
||||
- календарь, канбан, импортёры;
|
||||
- UI-элементы (пункты сайдбара, вкладки, страницы);
|
||||
- фоновые задачи по расписанию;
|
||||
- хуки на события (создание/удаление/изменение узлов).
|
||||
|
||||
## Структура плагина
|
||||
## 2. Структура плагина
|
||||
|
||||
```
|
||||
.verstak/plugins/<name>/
|
||||
plugin.json # мета: name, version, author, hooks
|
||||
main.lua # точка входа
|
||||
plugin.json # метаданные: имя, версия, хуки, UI-contributions
|
||||
main.lua # точка входа (обязательно, если есть Lua-код)
|
||||
panels/ # HTML-панели для GUI (опционально)
|
||||
calendar.html
|
||||
kanban.html
|
||||
templates/ # шаблоны дел (опционально)
|
||||
client.json
|
||||
repair.json
|
||||
panels/ # UI-панели для GUI (опционально)
|
||||
kanban.html
|
||||
calendar.html
|
||||
migrations/ # SQL-миграции (опционально)
|
||||
001_create_tables.sql
|
||||
assets/ # статика: иконки, CSS, изображения (опционально)
|
||||
icon.svg
|
||||
```
|
||||
|
||||
## plugin.json
|
||||
### 2.1 plugin.json — полная схема
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -32,55 +38,485 @@
|
||||
"version": "1.0.0",
|
||||
"author": "...",
|
||||
"description": "Календарь событий, привязанных к делам",
|
||||
|
||||
"hooks": {
|
||||
"on_init": "on_init",
|
||||
"on_node_open": "on_node_open"
|
||||
"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"]
|
||||
}
|
||||
```
|
||||
|
||||
## Lua API
|
||||
#### Поля plugin.json
|
||||
|
||||
Плагины пишутся на Lua (gopher-lua). API:
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `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-файлы миграций |
|
||||
|
||||
```lua
|
||||
-- Получить node по ID
|
||||
local node = verstak.node.get(id)
|
||||
### 2.2 SidebarItem
|
||||
|
||||
-- Создать node
|
||||
local n = verstak.node.create(parent_id, "type", "title")
|
||||
|
||||
-- Получить config value
|
||||
local v = verstak.config.get("key")
|
||||
|
||||
-- Записать в activity log
|
||||
verstak.activity.log({
|
||||
node_id = n.id,
|
||||
event_type = "calendar_event",
|
||||
title = "Встреча с клиентом"
|
||||
})
|
||||
|
||||
-- Зарегистрировать HTTP-эндпоинт (для GUI)
|
||||
verstak.http.route("GET", "/api/calendar/events", get_events)
|
||||
|
||||
-- Показать уведомление
|
||||
verstak.ui.toast("Событие добавлено")
|
||||
```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`
|
||||
|
||||
1. **on_init** — при старте программы, до открытия vault.
|
||||
Инициализация, создание таблиц.
|
||||
2. **on_vault_open** — при открытии vault.
|
||||
3. **on_node_create / on_node_open / on_node_delete** — хуки на действия.
|
||||
4. **on_shutdown** — при закрытии.
|
||||
### 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({
|
||||
@@ -88,72 +524,121 @@ verstak.node.register_type({
|
||||
label = "Событие",
|
||||
icon = "calendar",
|
||||
fields = {
|
||||
{ name = "date", label = "Дата", type = "date" },
|
||||
{ name = "time", label = "Время", type = "time" },
|
||||
{ name = "location", label = "Место", type = "text" },
|
||||
{ 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 рисует карточку дела на основе зарегистрированных полей типа.
|
||||
GUI рисует карточку дела на основе зарегистрированных полей.
|
||||
Поля сохраняются как метаданные узла.
|
||||
|
||||
## Шаблоны дела
|
||||
## 6. Песочница и безопасность
|
||||
|
||||
Шаблон — JSON-описание предзаполненного дерева:
|
||||
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
|
||||
{
|
||||
"name": "Клиент",
|
||||
"icon": "user",
|
||||
"tree": [
|
||||
{ "type": "folder", "title": "Документы" },
|
||||
{ "type": "folder", "title": "Переписка" },
|
||||
{ "type": "folder", "title": "Скриншоты" },
|
||||
{ "type": "note", "title": "Overview" },
|
||||
{ "type": "action", "title": "Открыть сайт", "kind": "open_url", "url": "" }
|
||||
],
|
||||
"meta": [
|
||||
{ "key": "domain", "label": "Домен сайта", "type": "text" },
|
||||
{ "key": "admin_url", "label": "Админка", "type": "url" }
|
||||
]
|
||||
"plugins": {
|
||||
"sandbox": {
|
||||
"max_memory_mb": 32,
|
||||
"max_instructions": 10000000,
|
||||
"call_timeout_ms": 30000,
|
||||
"allow_plugins": ["calendar", "kanban"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
GUI: при создании дела пользователь выбирает шаблон — и дерево
|
||||
создаётся автоматически.
|
||||
## 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 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Lua-плагины работают в песочнице:
|
||||
## 8. Распространение и CLI
|
||||
|
||||
- нет доступа к файловой системе напрямую (только через API vault);
|
||||
- нет `io.*`, `os.execute` и т.д.;
|
||||
- память ограничена;
|
||||
- нет сетевых вызовов кроме зарегистрированных HTTP-эндпоинтов.
|
||||
### CLI-команды
|
||||
|
||||
Go-плагины (buildmode=plugin) доступны для продвинутых
|
||||
разработчиков, но требуют совместимости версий.
|
||||
```bash
|
||||
verstak plugin list # Список плагинов
|
||||
verstak plugin enable <name> # Включить
|
||||
verstak plugin disable <name> # Выключить
|
||||
verstak plugin install <name> # Установить из registry (TODO)
|
||||
verstak plugin remove <name> # Удалить
|
||||
```
|
||||
|
||||
## Инициализация
|
||||
### Структура пакета
|
||||
|
||||
При старте `verstak init` создаёт `.verstak/plugins/`.
|
||||
При старте GUI/CLI/TUI:
|
||||
Плагин распространяется как zip-архив:
|
||||
|
||||
1. Сканировать `.verstak/plugins/*/plugin.json`
|
||||
2. Валидировать (имя, версия, структура)
|
||||
3. Загрузить миграции и выполнить
|
||||
4. Загрузить Lua-скрипты через gopher-lua
|
||||
5. Вызвать `on_init` у каждого плагина
|
||||
6. Зарегистрировать node types, HTTP routes, UI panels
|
||||
```bash
|
||||
calendar-1.0.0.vpk # Verstak Plugin Package
|
||||
plugin.json
|
||||
main.lua
|
||||
panels/calendar.html
|
||||
migrations/001_create_tables.sql
|
||||
```
|
||||
|
||||
## Распространение
|
||||
|
||||
Плагин — это zip-архив с правильной структурой.
|
||||
Репозиторий плагинов: `verstak-registry` (отдельный проект).
|
||||
|
||||
Установка:
|
||||
Установка из registry (будущее):
|
||||
```bash
|
||||
verstak plugin install calendar
|
||||
verstak plugin enable calendar
|
||||
verstak plugin list
|
||||
# → скачивает 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`
|
||||
|
||||
+26
-18
@@ -27,13 +27,12 @@
|
||||
| 14 | **MVP stabilization** | ✅ выполнено |
|
||||
| 15 | **Sync Server + Client** | ✅ выполнено |
|
||||
| 16 | **Activity Suggestions** | ✅ выполнено базово — global worklog dashboard + conservative suggestions + UX |
|
||||
| 17 | **File Scanner/Watcher** | ⏳ ожидает — fsnotify, snapshot scanner |
|
||||
| 18 | **TUI MVP (Bubble Tea)** | ⏳ ожидает — tree/search, add worklog |
|
||||
| 19 | **Plugins: Lua runtime** | ⏳ ожидает — gopher-lua, hooks, sandbox |
|
||||
| 20 | **Browser Extension** | ⏳ ожидает — tracking, capture, evidence |
|
||||
| 21 | **Calendar/Kanban** | ⏳ ожидает — view by date, board view |
|
||||
| 22 | **Integrity Check + Repair** | ⏳ ожидает — checksums, crash recovery |
|
||||
| 23 | **New templates/integrations** | ⏳ ожидает — community plugins |
|
||||
| 17 | **File Scanner/Watcher** | ✅ выполнено — fsnotify, snapshot scanner |
|
||||
| 18 | **Plugins: Lua runtime** | ✅ скелет — gopher-lua, хуки, песочница, шедулер, Calendar-пример |
|
||||
| 19 | **Browser Extension** | ✅ выполнено — tracking, capture, evidence |
|
||||
| 20 | **Calendar/Kanban** | ⏳ ожидает — view by date, board view |
|
||||
| 21 | **Integrity Check + Repair** | ⏳ ожидает — checksums, crash recovery |
|
||||
| 22 | **New templates/integrations** | ⏳ ожидает — community plugins |
|
||||
|
||||
---
|
||||
|
||||
@@ -248,28 +247,37 @@ Real-time fsnotify watcher управляется отдельно.
|
||||
Через GUI: `WatcherStatus()` — true если watcher активен.
|
||||
Через консоль (отладка): создать/удалить файл в vault → в activity появятся события `file_added`/`file_missing`/`file_modified`.
|
||||
|
||||
## Ожидающие шаги (18-23)
|
||||
## Выполненные шаги (18-22 частично)
|
||||
|
||||
### ШАГ 18 — TUI MVP (Bubble Tea)
|
||||
- Терминальный интерфейс: дерево дел, поиск, добавление worklog, запуск действий, sync
|
||||
### ШАГ 18 — Plugins: Lua runtime ✅ скелет
|
||||
|
||||
### ШАГ 19 — Plugins: Lua runtime
|
||||
- gopher-lua runtime
|
||||
- хуки на события (node created, file added, etc.)
|
||||
- песочница
|
||||
**Статус:** ✅ Скелет готов — рантайм, хуки, песочница, шедулер, Calendar-пример.
|
||||
|
||||
### ШАГ 20 — Browser Extension
|
||||
Что реализовано:
|
||||
- `internal/core/plugins/runtime.go` — gopher-lua VM: загрузка main.lua, вызов хуков, таймауты
|
||||
- `internal/core/plugins/api.go` — регистрация таблицы `verstak` с субмодулями
|
||||
- `internal/core/plugins/api_node.go` — `verstak.node.*` (заглушки, ждут реализации)
|
||||
- `internal/core/plugins/api_ext.go` — `verstak.db.*`, `verstak.config.*`, `verstak.state.*`, `verstak.ui.*`, `verstak.schedule.*`, `verstak.http.*` (state — работает, остальное — заглушки)
|
||||
- `internal/core/plugins/api_utils.go` — вспомогательные функции
|
||||
- `internal/core/plugins/scheduler.go` — фоновые задачи по расписанию (goroutine + ticker)
|
||||
- `internal/core/plugins/manager.go` — обновлён: InitRuntimes, CallInitHooks, StartSchedulers, etc.
|
||||
- `internal/core/plugins/runtime_test.go` — 5 тестов: hook call, missing hook, sandbox, state, Manager lifecycle
|
||||
- `contrib/plugins/calendar/` — пример-плагин: plugin.json, main.lua, HTML-panel, migrations, background task
|
||||
|
||||
**Следующее:** реализовать `verstak.node.*`, `verstak.db.*`, `verstak.config.*`, `verstak.ui.*` полноценно, и пройтись по Calendar-плагину вживую.
|
||||
|
||||
### ШАГ 19 — Browser Extension
|
||||
- Расширение для браузера: трекинг времени, скриншоты, evidence
|
||||
|
||||
### ШАГ 21 — Calendar/Kanban
|
||||
### ШАГ 20 — Calendar/Kanban
|
||||
- Календарный вид по датам
|
||||
- Канбан-доска по статусам
|
||||
|
||||
### ШАГ 22 — Integrity Check + Repair
|
||||
### ШАГ 21 — Integrity Check + Repair
|
||||
- Проверка целостности vault
|
||||
- Контрольные суммы, восстановление после сбоев
|
||||
|
||||
### ШАГ 23 — New templates/integrations
|
||||
### ШАГ 22 — New templates/integrations
|
||||
- Community-плагины и шаблоны
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user