zaytsv-bot-graph-mcp 0.4.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -3
- package/package.json +3 -3
- package/skills/build-bot-funnel/SKILL.md +12 -3
- package/skills/build-bot-funnel/reference/schema.md +64 -0
- package/skills/build-bot-funnel/reference/validation.md +42 -0
- package/skills/build-bot-funnel/validate.mjs +83 -2
- package/src/index.mjs +45 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zaytsv-bot-graph",
|
|
3
3
|
"displayName": "Zaytsv Bot Graph",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "MCP-сервер + скилл для сборки и публикации воронок Telegram-ботов в сервисе zaytsv /bots: из текстового описания → валидный граф → заливка и публикация по API.",
|
|
6
6
|
"author": { "name": "zaytsv", "url": "https://zaytsv.ru" },
|
|
7
7
|
"homepage": "https://zaytsv.ru/bots",
|
package/README.md
CHANGED
|
@@ -5,12 +5,20 @@
|
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
MCP-сервер (+ скилл для Claude Code) для **сборки и публикации
|
|
8
|
+
MCP-сервер (+ скилл для Claude Code) для **сборки и публикации воронок/автоматизаций ботов (Telegram, MAX и Instagram)** в сервисе [zaytsv `/bots`](https://zaytsv.ru/bots): из текстового описания → валидный граф сценария → заливка и публикация через API.
|
|
9
9
|
|
|
10
|
-
- 🤖 **
|
|
11
|
-
- 🧠 **Скилл `build-bot-funnel`**: учит агента собирать корректный граф (типы узлов, ветки, кнопки, задержки) и проверять его перед публикацией.
|
|
10
|
+
- 🤖 **20 инструментов сборки/публикации**: `list_bots`, `list_graphs`, `list_channels`, `get_graph`, `create_graph`, `update_graph`, `edit_graph_live`, `patch_graph`, `dry_run`, `publish_graph`, `import_funnel`, `list_templates`, `create_graph_from_template`, `clone_graph`, `copy_graph`, `rename_graph`, `set_active_graph`, `delete_graph` (+ `setup`/`set_token`).
|
|
11
|
+
- 🧠 **Скилл `build-bot-funnel`**: учит агента собирать корректный граф (типы узлов, ветки, кнопки, задержки) и проверять его перед публикацией. Поддерживает Telegram, MAX и Instagram.
|
|
12
12
|
- 📦 **Без зависимостей** — чистый Node ≥18, ставится и запускается сразу.
|
|
13
13
|
|
|
14
|
+
### Поддерживаемые платформы
|
|
15
|
+
|
|
16
|
+
| Платформа | Онбординг | Триггеры входа | Ограничения |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| **Telegram** | Токен бота (BotFather) | `/start`, команды, callback, текст, рассылки | Полный функционал |
|
|
19
|
+
| **MAX** | Токен бота (MAX Developer) | Команды, callback, текст | Без SUBSCRIBED/reply-клавиатур (мягкие предупреждения) |
|
|
20
|
+
| **Instagram** | OAuth в `/growth` (без токена) | Комментарий/Direct/Ответ на историю/Упоминание | Ограниченный набор узлов; DELAY ≤ 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER; без рассылок |
|
|
21
|
+
|
|
14
22
|
---
|
|
15
23
|
|
|
16
24
|
## Установка
|
|
@@ -108,7 +116,12 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
108
116
|
Локальная проверка графа перед заливкой:
|
|
109
117
|
|
|
110
118
|
```bash
|
|
119
|
+
# Telegram (по умолчанию)
|
|
111
120
|
node skills/build-bot-funnel/validate.mjs path/to/import.json
|
|
121
|
+
# Instagram-бот
|
|
122
|
+
node skills/build-bot-funnel/validate.mjs path/to/import.json --platform=INSTAGRAM
|
|
123
|
+
# MAX-бот
|
|
124
|
+
node skills/build-bot-funnel/validate.mjs path/to/import.json --platform=MAX
|
|
112
125
|
```
|
|
113
126
|
|
|
114
127
|
---
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zaytsv-bot-graph-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server to build and publish Telegram bot funnels in the zaytsv /bots service. Zero dependencies.",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "MCP server to build and publish Telegram, MAX and Instagram bot funnels/automations in the zaytsv /bots service. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "zaytsv-bot-graph-mcp": "src/index.mjs" },
|
|
7
7
|
"main": "src/index.mjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"validate": "node skills/build-bot-funnel/validate.mjs"
|
|
14
14
|
},
|
|
15
15
|
"engines": { "node": ">=18" },
|
|
16
|
-
"keywords": ["mcp", "telegram", "bot", "funnel", "zaytsv", "claude-code", "model-context-protocol"],
|
|
16
|
+
"keywords": ["mcp", "telegram", "max", "instagram", "bot", "funnel", "automation", "zaytsv", "claude-code", "model-context-protocol"],
|
|
17
17
|
"repository": { "type": "git", "url": "git+https://github.com/skiddgoddamn/zaytsv-bot-graph-mcp.git" },
|
|
18
18
|
"homepage": "https://zaytsv.ru/bots",
|
|
19
19
|
"license": "MIT"
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-bot-funnel
|
|
3
|
-
description: Собрать воронку (сценарий)
|
|
3
|
+
description: Собрать воронку (сценарий) бота для сервиса /bots из текстового описания — сгенерировать валидный граф формата zaytsv-bot-graph, проверить его и (через MCP bot-graph) залить в бота, прогнать dry-run и опубликовать. Поддерживает Telegram, MAX и Instagram. Использовать, когда пользователь описывает воронку/прогрев/сценарий бота словами и просит «собрать», «сделать граф», «залить в бота», «опубликовать сценарий».
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Сборка воронки бота (zaytsv /bots)
|
|
7
7
|
|
|
8
|
-
Превращает **текстовое описание воронки** в валидный граф `zaytsv-bot-graph` и (опционально) публикует его через MCP-сервер `bot-graph`.
|
|
8
|
+
Превращает **текстовое описание воронки** в валидный граф `zaytsv-bot-graph` и (опционально) публикует его через MCP-сервер `bot-graph`. Поддерживаются платформы **Telegram, MAX и Instagram** — у каждой свои триггеры и ограничения, описанные в schema.md.
|
|
9
|
+
|
|
10
|
+
**Различия платформ (кратко):**
|
|
11
|
+
- **Telegram** — полный функционал: `/start` и другие команды, все типы узлов, SUBSCRIBED, кнопки-контакты, рассылки.
|
|
12
|
+
- **MAX** — те же узлы, но без SUBSCRIBED/reply-клавиатур (мягкие предупреждения при публикации, не блокируют).
|
|
13
|
+
- **Instagram** — ограниченный набор узлов; вход только через комментарий/директ/историю (нет `/start`); нет рассылок; DELAY не более 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER; онбординг через OAuth в разделе «Инструменты роста» (/growth), без токена бота.
|
|
9
14
|
|
|
10
15
|
## Когда применять
|
|
11
16
|
Пользователь описывает сценарий бота словами: приветствие → подписка → вопрос с кнопками → ветки → задержки → вебинар и т.п. Либо просит залить/опубликовать готовую воронку.
|
|
@@ -29,8 +34,12 @@ description: Собрать воронку (сценарий) Telegram-бота
|
|
|
29
34
|
4. **Проверь локально** перед заливкой:
|
|
30
35
|
```bash
|
|
31
36
|
node "<путь к скиллу>/validate.mjs" <путь к import.json>
|
|
37
|
+
# Для IG-бота:
|
|
38
|
+
node "<путь к скиллу>/validate.mjs" <путь к import.json> --platform=INSTAGRAM
|
|
39
|
+
# Для MAX-бота:
|
|
40
|
+
node "<путь к скиллу>/validate.mjs" <путь к import.json> --platform=MAX
|
|
32
41
|
```
|
|
33
|
-
Скрипт ловит пустые сообщения, висячие рёбра, дубли и не-UUID id узлов/рёбер, недостижимые узлы, превышение 4096, кривые DELAY (вкл. `duration`+`unit`), непустой `value` у кнопок-выборов с ребром `btn_N`, неподдерживаемые цвета. Исправь всё, что он покажет.
|
|
42
|
+
Скрипт ловит пустые сообщения, висячие рёбра, дубли и не-UUID id узлов/рёбер, недостижимые узлы, превышение 4096, кривые DELAY (вкл. `duration`+`unit`), непустой `value` у кнопок-выборов с ребром `btn_N`, неподдерживаемые цвета. При `--platform=INSTAGRAM` дополнительно: IG-allowlist узлов (`IG_NODE_UNSUPPORTED`), DELAY > 24ч (`IG_DELAY_OVER_24H`), неподдерживаемый inputKind (`IG_INPUT_UNSUPPORTED`). Исправь всё, что он покажет.
|
|
34
43
|
|
|
35
44
|
5. **Залей и опубликуй через MCP `bot-graph`** (если он подключён и пользователь просит публикацию):
|
|
36
45
|
- `list_bots` → выбрать `botId` (или `create_graph` в существующем боте).
|
|
@@ -30,11 +30,22 @@
|
|
|
30
30
|
## Типы узлов (NodeType) и их config
|
|
31
31
|
|
|
32
32
|
### Триггеры (точки входа, корневые)
|
|
33
|
+
|
|
34
|
+
#### Telegram / MAX
|
|
33
35
|
- `TRIGGER_COMMAND` — `{ "isRoot": true, "command": "start" }` (команда без `/`). Первый — с `isRoot:true`.
|
|
34
36
|
- `TRIGGER_CALLBACK` — `{ "matchMode": "EQUALS"|"STARTS_WITH", "value": "<callback_data>" }`
|
|
35
37
|
- `TRIGGER_TEXT` — `{ "matchMode": "ANY"|"EQUALS"|"CONTAINS"|"REGEX", "value": "..." }`
|
|
36
38
|
- `BROADCAST_FILTER` — режим рассылки (если есть — единственный триггер).
|
|
37
39
|
|
|
40
|
+
#### Instagram (только для IG-ботов)
|
|
41
|
+
IG-боты не поддерживают команды (`/start`). Вход — через взаимодействие с контентом или директ:
|
|
42
|
+
- `TRIGGER_IG_DM` — `{ "isRoot": true }` — входящее сообщение в Instagram Direct. Это дефолтный триггер нового IG-графа (бэкенд сеет его при создании).
|
|
43
|
+
- `TRIGGER_IG_COMMENT` — `{ "isRoot": true }` — комментарий к посту или Reel бота. Опционально: фильтрация по ключевому слову (поддерживается бэкендом, уточни у пользователя, если нужно).
|
|
44
|
+
- `TRIGGER_IG_STORY_REPLY` — `{ "isRoot": true }` — ответ на историю бота.
|
|
45
|
+
- `TRIGGER_IG_STORY_MENTION` — `{ "isRoot": true }` — упоминание бота в истории подписчика.
|
|
46
|
+
|
|
47
|
+
Для всех IG-триггеров реакция бота отправляется через Instagram Messaging API в течение **24-часового окна** после последнего входящего действия пользователя.
|
|
48
|
+
|
|
38
49
|
### Сообщения
|
|
39
50
|
- `SEND_MESSAGE` —
|
|
40
51
|
```json
|
|
@@ -106,6 +117,59 @@
|
|
|
106
117
|
|
|
107
118
|
> **Платформа MAX.** Боты конструктора умеют работать и в мессенджере MAX. Там не поддерживаются `SUBSCRIBED`/`NOT_SUBSCRIBED` (нет членства в каналах) и reply-клавиатуры; публикация такого графа на MAX-бот вернёт **мягкие предупреждения** (не блокирует). Для Telegram-ботов всё работает как описано.
|
|
108
119
|
|
|
120
|
+
## Платформа Instagram
|
|
121
|
+
|
|
122
|
+
IG-боты подключаются через OAuth в разделе **«Инструменты роста»** (`/growth`) — **без вставки токена вручную**; у IG нет персонального бот-токена. После OAuth бот получает доступ к Messaging API через привязанный Instagram Business/Creator-аккаунт.
|
|
123
|
+
|
|
124
|
+
### Разрешённые типы узлов для IG-ботов
|
|
125
|
+
|
|
126
|
+
Только следующие (всё остальное — ошибка `IG_NODE_UNSUPPORTED` при публикации):
|
|
127
|
+
|
|
128
|
+
| Тип | Доступен в IG |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY`, `TRIGGER_IG_STORY_MENTION` | ✅ (триггеры входа) |
|
|
131
|
+
| `SEND_MESSAGE`, `SEND_PHOTO` | ✅ |
|
|
132
|
+
| `BRANCH`, `CONDITION` | ✅ |
|
|
133
|
+
| `SET_VARIABLE`, `ADD_TAG`, `REMOVE_TAG`, `FORMULA` | ✅ |
|
|
134
|
+
| `ASK_QUESTION` | ✅ (с ограничениями — см. ниже) |
|
|
135
|
+
| `DELAY` | ✅ (не более 24ч — см. ниже) |
|
|
136
|
+
| `END` | ✅ |
|
|
137
|
+
| `TRIGGER_COMMAND`, `TRIGGER_CALLBACK`, `TRIGGER_TEXT` | ❌ |
|
|
138
|
+
| `BROADCAST_FILTER` (рассылки) | ❌ |
|
|
139
|
+
| `SCHEDULE`, `ACTIONS`, `CALL_WEBHOOK`, `AI_REPLY`, `PAYMENT_LINK` | ❌ |
|
|
140
|
+
|
|
141
|
+
### Ограничения IG-ботов
|
|
142
|
+
|
|
143
|
+
- **Нет команд** — вход только через `TRIGGER_IG_COMMENT` / `TRIGGER_IG_DM` / `TRIGGER_IG_STORY_REPLY` / `TRIGGER_IG_STORY_MENTION`. `/start` и другие команды не поддерживаются.
|
|
144
|
+
- **Нет рассылок** — `BROADCAST_FILTER` недоступен.
|
|
145
|
+
- **DELAY не более 24 часов** — Instagram доставляет сообщения только в течение 24-часового окна после последнего входящего действия (`IG_DELAY_OVER_24H`). `DELAY` с `kind=TOMORROW` или `kind=UNTIL` блокируются (они заведомо > 24ч). `kind=FIXED` с `durationSec > 86400` тоже блокируется.
|
|
146
|
+
- **`ASK_QUESTION`** — `inputKind` ограничен: только `TEXT`, `EMAIL`, `PHONE`, `NUMBER` (`IG_INPUT_UNSUPPORTED`). Нельзя запрашивать `CONTACT` (кнопка «Поделиться номером» недоступна в IG), `LOCATION`, `PHOTO`, `DOCUMENT`.
|
|
147
|
+
- **Нет reply-клавиатур** — кнопки IG работают как inline (URL или Deep Link); стиль кнопок ограничен возможностями IG Messaging API.
|
|
148
|
+
- **Нет SUBSCRIBED** — условие «подписан на канал» недоступно.
|
|
149
|
+
|
|
150
|
+
### Оффлайн-проверка IG-графа
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
node validate.mjs graph.json --platform=INSTAGRAM
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Ловит `IG_NODE_UNSUPPORTED`, `IG_DELAY_OVER_24H`, `IG_INPUT_UNSUPPORTED` — в дополнение ко всем обычным структурным проверкам.
|
|
157
|
+
|
|
158
|
+
## Кросс-платформенное копирование (Telegram ⇄ MAX)
|
|
159
|
+
|
|
160
|
+
Инструмент `copy_graph` копирует граф в **другого бота** пользователя (`targetBotId`), в т.ч. на другую платформу. Формат графа один и тот же; различается лишь платформа бота-получателя. При копировании в MAX-бот несовместимые узлы **адаптируются**, отчёт — в `notes[]`:
|
|
161
|
+
|
|
162
|
+
| code | severity | что значит |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `MAX_CONTACT_AS_TEXT` | TRANSFORM | вопрос с `inputKind=CONTACT` переписан в `inputKind=TEXT` + `validator=PHONE` (в MAX нет кнопки «поделиться контактом»; иначе узел стал бы тупиком) |
|
|
165
|
+
| `MAX_SUBSCRIBED_ALWAYS_NO` | MANUAL | условие `SUBSCRIBED` оставлено как есть, но в MAX всегда «не подписан» — проверьте ветвление вручную |
|
|
166
|
+
| `MAX_VOICE_AS_AUDIO` | INFO | голосовое уйдёт обычным аудио |
|
|
167
|
+
| `MAX_VIDEO_NOTE_AS_VIDEO` | INFO | кружок уйдёт обычным видео |
|
|
168
|
+
| `MAX_GALLERY_AS_ATTACHMENTS` | INFO | галерея уйдёт одним сообщением с вложениями |
|
|
169
|
+
| `MAX_DELETE_NOOP` | INFO | действие «удалить сообщение» в MAX игнорируется |
|
|
170
|
+
|
|
171
|
+
`preview: true` возвращает только `notes[]` (без копирования). Копирование в Telegram-бота (или в бота той же платформы) — точная копия, `notes[]` пустой. Новый граф создаётся как `DRAFT` с именем «… (copy)». Копировать в тот же бот нельзя (для дублирования — `clone_graph`).
|
|
172
|
+
|
|
109
173
|
## Выходные хэндлы (`sourceHandle`) — шпаргалка
|
|
110
174
|
| Узел | Хэндлы |
|
|
111
175
|
|---|---|
|
|
@@ -55,5 +55,47 @@
|
|
|
55
55
|
- Статусы графа: `DRAFT` / `PUBLISHED`. Публикация заменяет активную опубликованную версию.
|
|
56
56
|
- Перед публикацией полезно прогнать `dry_run` (kind `command`/`callback`/`text`) — поймать рантайм-проблемы стартовой ветки.
|
|
57
57
|
|
|
58
|
+
## Платформенные правила — Instagram
|
|
59
|
+
|
|
60
|
+
Источник: `GraphValidator.platformErrors()` (Java). Коды — жёсткие ошибки, **блокируют публикацию** (`publish_graph` вернёт `errors[]`).
|
|
61
|
+
|
|
62
|
+
### `IG_NODE_UNSUPPORTED` — неподдерживаемый тип узла
|
|
63
|
+
|
|
64
|
+
Каждый узел в графе (включая черновые/неподключённые) должен быть из IG-allowlist. Всё остальное → ошибка.
|
|
65
|
+
|
|
66
|
+
Allowlist: `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY`, `TRIGGER_IG_STORY_MENTION`, `SEND_MESSAGE`, `SEND_PHOTO`, `BRANCH`, `CONDITION`, `SET_VARIABLE`, `ADD_TAG`, `REMOVE_TAG`, `FORMULA`, `ASK_QUESTION`, `DELAY`, `END`.
|
|
67
|
+
|
|
68
|
+
Не в allowlist: `TRIGGER_COMMAND`, `TRIGGER_CALLBACK`, `TRIGGER_TEXT`, `BROADCAST_FILTER`, `SCHEDULE`, `ACTIONS`, `CALL_WEBHOOK`, `AI_REPLY`, `PAYMENT_LINK` — и любой другой тип.
|
|
69
|
+
|
|
70
|
+
> Бэкенд проверяет ВСЕ узлы, включая черновые (неподключённые), так как палитра редактора не даёт ставить неподдерживаемые узлы в IG-боте — поэтому их присутствие означает реальную ошибку (напр., импортированный/скопированный граф).
|
|
71
|
+
|
|
72
|
+
### `IG_DELAY_OVER_24H` — задержка больше 24 часов
|
|
73
|
+
|
|
74
|
+
Instagram доставляет сообщения только в течение **24 часов** после последнего входящего от пользователя.
|
|
75
|
+
|
|
76
|
+
Правило (зеркало `igDelaySeconds()` в Java):
|
|
77
|
+
- `kind != "FIXED"` (т.е. `TOMORROW` или `UNTIL`) → всегда ошибка (заведомо > 24ч).
|
|
78
|
+
- `kind == "FIXED"`:
|
|
79
|
+
- `durationSec > 86400` → ошибка.
|
|
80
|
+
- `duration` (число) + `unit` → пересчёт: `MINUTES` × 60, `HOURS` × 3600, `DAYS` × 86400; если результат > 86400 → ошибка.
|
|
81
|
+
- `duration` без `unit` → считается в секундах; если > 86400 → ошибка.
|
|
82
|
+
- Отсутствует и `durationSec`, и `duration` (malformed FIXED) → ошибка (не разрешаем молча).
|
|
83
|
+
|
|
84
|
+
### `IG_INPUT_UNSUPPORTED` — неподдерживаемый вид ответа
|
|
85
|
+
|
|
86
|
+
У `ASK_QUESTION`, `inputKind` ограничен: только `TEXT`, `EMAIL`, `PHONE`, `NUMBER`.
|
|
87
|
+
|
|
88
|
+
Не поддерживается: `CONTACT` (кнопка «Поделиться номером» недоступна в IG Messaging API), `LOCATION`, `PHOTO`, `DOCUMENT`.
|
|
89
|
+
|
|
90
|
+
`inputKind == null` (не задан) — допустимо (дефолт = текст).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
58
94
|
## Локальная проверка
|
|
59
95
|
`node validate.mjs <import.json>` повторяет ключевые проверки: пустые сообщения с учётом `cardsToLegacy`, висячие рёбра, дубли id, достижимость от триггеров, длину текста, HTML-безопасность (эвристика по тегам), режим «Вопрос» (`awaitReply`→`saveTo`/`regex`), конфиг `DELAY`/`SCHEDULE`/`FORMULA`/`ACTIONS`/`AI_REPLY`/`PAYMENT_LINK`/триггеров, условия `CONDITION` (вкл. `LINK_CLICKED` со ссылкой на отслеживаемый шаг). Гонять перед каждой заливкой.
|
|
96
|
+
|
|
97
|
+
Для IG-ботов передавать `--platform=INSTAGRAM`:
|
|
98
|
+
```bash
|
|
99
|
+
node validate.mjs <import.json> --platform=INSTAGRAM
|
|
100
|
+
```
|
|
101
|
+
Добавляет проверки `IG_NODE_UNSUPPORTED`, `IG_DELAY_OVER_24H`, `IG_INPUT_UNSUPPORTED` поверх всех обычных.
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Оффлайн-валидатор графа zaytsv-bot-graph. Без зависимостей.
|
|
3
|
-
// Использование: node validate.mjs <path/to/import.json>
|
|
3
|
+
// Использование: node validate.mjs <path/to/import.json> [--platform=TELEGRAM|MAX|INSTAGRAM]
|
|
4
4
|
// Повторяет ключевые правила GraphValidator + cardsToLegacy редактора.
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
|
|
8
8
|
const path = process.argv[2];
|
|
9
|
-
if (!path) { console.error("Usage: node validate.mjs <import.json>"); process.exit(2); }
|
|
9
|
+
if (!path) { console.error("Usage: node validate.mjs <import.json> [--platform=TELEGRAM|MAX|INSTAGRAM]"); process.exit(2); }
|
|
10
|
+
|
|
11
|
+
// Разобрать --platform=... (регистронезависимо)
|
|
12
|
+
let platform = "TELEGRAM";
|
|
13
|
+
for (const arg of process.argv.slice(3)) {
|
|
14
|
+
const m = arg.match(/^--platform=([A-Za-z]+)$/);
|
|
15
|
+
if (m) platform = m[1].toUpperCase();
|
|
16
|
+
}
|
|
17
|
+
if (!["TELEGRAM", "MAX", "INSTAGRAM"].includes(platform)) {
|
|
18
|
+
console.error(`❌ Неизвестная платформа: ${platform}. Допустимые: TELEGRAM, MAX, INSTAGRAM`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
let g;
|
|
12
23
|
try { g = JSON.parse(readFileSync(path, "utf8")); }
|
|
@@ -276,9 +287,79 @@ function dfs(u, pathHasWaitAtEdgeInto) {
|
|
|
276
287
|
}
|
|
277
288
|
for (const n of nodes) if (!color[n.id]) dfs(n.id);
|
|
278
289
|
|
|
290
|
+
// ============================================================
|
|
291
|
+
// --- IG-специфические проверки (только для INSTAGRAM) ---
|
|
292
|
+
// Зеркало GraphValidator.platformErrors() (Java).
|
|
293
|
+
// IG_ALLOWED_NODES: TRIGGER_IG_COMMENT, TRIGGER_IG_DM, TRIGGER_IG_STORY_REPLY,
|
|
294
|
+
// TRIGGER_IG_STORY_MENTION, SEND_MESSAGE, SEND_PHOTO, BRANCH, CONDITION,
|
|
295
|
+
// SET_VARIABLE, ADD_TAG, REMOVE_TAG, FORMULA, ASK_QUESTION, DELAY, END.
|
|
296
|
+
// IG_ALLOWED_INPUT_KINDS: TEXT, EMAIL, PHONE, NUMBER.
|
|
297
|
+
// IG_MAX_DELAY_SECONDS: 86400 (24 часа).
|
|
298
|
+
// ============================================================
|
|
299
|
+
|
|
300
|
+
if (platform === "INSTAGRAM") {
|
|
301
|
+
const IG_ALLOWED_NODES = new Set([
|
|
302
|
+
"TRIGGER_IG_COMMENT", "TRIGGER_IG_DM",
|
|
303
|
+
"TRIGGER_IG_STORY_REPLY", "TRIGGER_IG_STORY_MENTION",
|
|
304
|
+
"SEND_MESSAGE", "SEND_PHOTO",
|
|
305
|
+
"BRANCH", "CONDITION", "SET_VARIABLE",
|
|
306
|
+
"ADD_TAG", "REMOVE_TAG", "FORMULA",
|
|
307
|
+
"ASK_QUESTION", "DELAY", "END",
|
|
308
|
+
]);
|
|
309
|
+
const IG_ALLOWED_INPUT_KINDS = new Set(["TEXT", "EMAIL", "PHONE", "NUMBER"]);
|
|
310
|
+
const IG_MAX_DELAY_SEC = 86400; // 24h messaging window
|
|
311
|
+
|
|
312
|
+
function igDelaySeconds(c) {
|
|
313
|
+
// Зеркало GraphValidator.igDelaySeconds():
|
|
314
|
+
// только FIXED считаем конкретно; TOMORROW/UNTIL — всегда > 24h (Long.MAX_VALUE)
|
|
315
|
+
if (!c || c.kind !== "FIXED") return Infinity;
|
|
316
|
+
// legacy: durationSec
|
|
317
|
+
if (typeof c.durationSec === "number" && c.durationSec > 0) return c.durationSec;
|
|
318
|
+
// new format: duration + unit
|
|
319
|
+
if (typeof c.duration === "number" && c.duration > 0) {
|
|
320
|
+
const u = String(c.unit || "");
|
|
321
|
+
if (u === "MINUTES") return c.duration * 60;
|
|
322
|
+
if (u === "HOURS") return c.duration * 3600;
|
|
323
|
+
if (u === "DAYS") return c.duration * 86400;
|
|
324
|
+
return c.duration; // assume seconds
|
|
325
|
+
}
|
|
326
|
+
return Infinity; // malformed FIXED — treat as unbounded (block it)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const n of nodes) {
|
|
330
|
+
const type = n.type;
|
|
331
|
+
const c = n.config || {};
|
|
332
|
+
const who = `«${c._title || n.id}» (${type})`;
|
|
333
|
+
if (!type) continue;
|
|
334
|
+
|
|
335
|
+
// 1) Проверка allowlist
|
|
336
|
+
if (!IG_ALLOWED_NODES.has(type)) {
|
|
337
|
+
errors.push(`IG_NODE_UNSUPPORTED: ${who} — узел «${type}» недоступен для Instagram-бота`);
|
|
338
|
+
continue; // дальнейшие проверки для этого узла бессмысленны
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2) ASK_QUESTION: inputKind должен быть из IG_ALLOWED_INPUT_KINDS
|
|
342
|
+
if (type === "ASK_QUESTION") {
|
|
343
|
+
const kind = c.inputKind;
|
|
344
|
+
if (kind != null && !IG_ALLOWED_INPUT_KINDS.has(String(kind).toUpperCase())) {
|
|
345
|
+
errors.push(`IG_INPUT_UNSUPPORTED: ${who} — В Instagram нельзя запросить «${kind}» — только TEXT/EMAIL/PHONE/NUMBER`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 3) DELAY: не более 24h (только для FIXED; TOMORROW/UNTIL всегда > 24h)
|
|
350
|
+
if (type === "DELAY") {
|
|
351
|
+
const sec = igDelaySeconds(c);
|
|
352
|
+
if (sec > IG_MAX_DELAY_SEC) {
|
|
353
|
+
errors.push(`IG_DELAY_OVER_24H: ${who} — задержка больше 24ч недопустима для Instagram (24-часовое окно доставки)`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
279
359
|
// --- отчёт ---
|
|
280
360
|
const byType = {};
|
|
281
361
|
nodes.forEach((n) => (byType[n.type] = (byType[n.type] || 0) + 1));
|
|
362
|
+
console.log(`Платформа: ${platform}`);
|
|
282
363
|
console.log(`Граф: ${nodes.length} узлов, ${edges.length} рёбер, ${triggers.length} триггеров`);
|
|
283
364
|
console.log(`Типы: ${JSON.stringify(byType)}`);
|
|
284
365
|
if (warns.length) { console.log(`\n⚠️ Предупреждения (${warns.length}):`); warns.forEach((w) => console.log(" • " + w)); }
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* zaytsv-bot-graph-mcp — MCP-сервер для сборки и публикации воронок
|
|
4
|
-
* через API сервиса zaytsv /bots.
|
|
3
|
+
* zaytsv-bot-graph-mcp — MCP-сервер для сборки и публикации воронок ботов
|
|
4
|
+
* (Telegram, MAX, Instagram) через API сервиса zaytsv /bots.
|
|
5
|
+
* Без внешних зависимостей (голый JSON-RPC по stdio).
|
|
5
6
|
*
|
|
6
7
|
* Авторизация (в порядке приоритета):
|
|
7
8
|
* 1) env ZAYTSV_MCP_TOKEN — персональный токен "zmcp_..."
|
|
@@ -21,7 +22,7 @@ import os from "node:os";
|
|
|
21
22
|
import fs from "node:fs";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
|
|
24
|
-
const VERSION = "0.
|
|
25
|
+
const VERSION = "0.7.0";
|
|
25
26
|
const BASE = (process.env.ZAYTSV_BASE_URL || "https://zaytsv.ru").replace(/\/+$/, "");
|
|
26
27
|
const CONFIG_DIR = path.join(os.homedir(), ".zaytsv-bot-graph");
|
|
27
28
|
const TOKEN_FILE = path.join(CONFIG_DIR, "token");
|
|
@@ -107,6 +108,7 @@ const TOOLS = [
|
|
|
107
108
|
{ name: "create_graph", description: "Создать пустой граф (DRAFT) в боте. Возвращает граф с id.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
|
|
108
109
|
{ name: "update_graph", description: "Залить узлы/рёбра в граф (PUT, сырой replace без бэкапа). Для правок СУЩЕСТВУЮЩЕГО/живого сценария используй edit_graph_live. Принимает graph-контейнер или nodes/edges.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, graph: { type: "object" }, nodes: { type: "array" }, edges: { type: "array" }, canvasMeta: { type: "object" }, name: { type: "string" } }, required: ["graphId"] } },
|
|
109
110
|
{ name: "edit_graph_live", description: "РЕКОМЕНДОВАННЫЙ способ правки СУЩЕСТВУЮЩЕГО (часто живого/опубликованного) сценария: редактирует ТОТ ЖЕ graphId НА МЕСТЕ (id не меняется) и сначала снимает авто-бэкап текущего состояния в один rolling-граф «🔙 Авто-бэкап». НЕ клонирует и НЕ создаёт новый активный граф. Открытые редакторы перечитают граф вживую (external_update), бот применит изменения сразу (читает активный граф заново из БД). Используй ВМЕСТО clone+publish, когда нужно поправить сценарий, который уже открыт/в проде. ВАЖНО: PUT не валидирует — перед вызовом прогони offline validate.mjs и dry_run.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, graph: { type: "object" }, nodes: { type: "array" }, edges: { type: "array" }, canvasMeta: { type: "object" }, name: { type: "string" }, backup: { type: "boolean", description: "Снимать авто-бэкап предыдущего состояния перед правкой (по умолчанию true)." } }, required: ["graphId"] } },
|
|
111
|
+
{ name: "patch_graph", description: "Точечная правка БОЛЬШОГО/живого графа без отправки графа целиком: сервер сам берёт граф по graphId, делает строковые замены в его JSON, проверяет валидность и заливает обратно НА МЕСТЕ (с авто-бэкапом). Идеально, когда граф слишком велик, чтобы передавать его целиком через update_graph/edit_graph_live — напр. сменить id канала в условиях SUBSCRIBED, ссылки кнопок, тексты. replacements: [{find, replace}] — заменяются ВСЕ вхождения; делай find максимально специфичным, чтобы не задеть лишнее. preview=true — только показать число совпадений, ничего не сохраняя. Бот применит изменения сразу (читает активный граф заново из БД).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, replacements: { type: "array", items: { type: "object", properties: { find: { type: "string" }, replace: { type: "string" } }, required: ["find", "replace"] } }, preview: { type: "boolean", description: "true = только отчёт о числе совпадений, без сохранения" }, backup: { type: "boolean", description: "снять авто-бэкап предыдущего состояния перед правкой (по умолчанию true)" } }, required: ["graphId", "replacements"] } },
|
|
110
112
|
{ name: "dry_run", description: "Прогнать сценарий без публикации. kind: command|callback|text.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, kind: { type: "string", enum: ["command", "callback", "text"] }, value: { type: "string" }, fromUsername: { type: "string" }, presetVariables: { type: "object" }, presetTags: { type: "array", items: { type: "string" } } }, required: ["graphId", "kind", "value"] } },
|
|
111
113
|
{ name: "publish_graph", description: "Опубликовать граф. Вернёт publishedGraphId или errors[] (code, nodeId, message).", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
112
114
|
{ name: "import_funnel", description: "Всё за раз: создать граф, залить узлы/рёбра, (опц.) dry-run /start, опубликовать.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" }, graph: { type: "object" }, dryRun: { type: "boolean" }, publish: { type: "boolean" } }, required: ["botId", "graph"] } },
|
|
@@ -114,6 +116,7 @@ const TOOLS = [
|
|
|
114
116
|
{ name: "create_graph_from_template", description: "Создать граф (DRAFT) из шаблона (см. list_templates). Возвращает граф с id — дальше правь через update_graph.", inputSchema: { type: "object", properties: { botId: { type: "string" }, templateId: { type: "string" }, name: { type: "string" } }, required: ["botId", "templateId"] } },
|
|
115
117
|
{ name: "rename_graph", description: "Переименовать сценарий (работает и для опубликованных — имя не влияет на исполнение).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, name: { type: "string" } }, required: ["graphId", "name"] } },
|
|
116
118
|
{ name: "clone_graph", description: "Склонировать граф в новый DRAFT «… (copy)» — безопасно итерировать поверх опубликованного.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
119
|
+
{ name: "copy_graph", description: "Скопировать граф в ДРУГОГО бота (в т.ч. на другую платформу: Telegram⇄MAX⇄Instagram). Возвращает {graphId, sourcePlatform, targetPlatform, notes[]}. notes[] помечают, что адаптировано (severity=TRANSFORM, напр. вопрос-контакт → ввод телефона текстом), что требует ручной правки (MANUAL, напр. условие SUBSCRIBED в MAX) и особенности платформы (INFO). preview=true — только проверка совместимости, без копирования. Тот же бот запрещён (для дублирования есть clone_graph).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, targetBotId: { type: "string", description: "id бота-получателя (см. list_bots)" }, preview: { type: "boolean", description: "true = только отчёт о совместимости, ничего не сохраняется" } }, required: ["graphId", "targetBotId"] } },
|
|
117
120
|
{ name: "delete_graph", description: "Удалить граф. Активный (опубликованный и назначенный боту) удалить нельзя — будет 409; сначала переключи активный через set_active_graph.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
118
121
|
{ name: "set_active_graph", description: "Назначить, какой опубликованный граф активен у бота (переключение живого сценария без перепубликации).", inputSchema: { type: "object", properties: { botId: { type: "string" }, graphId: { type: "string" } }, required: ["botId", "graphId"] } },
|
|
119
122
|
];
|
|
@@ -180,6 +183,43 @@ async function handleCall(params) {
|
|
|
180
183
|
steps.push(`правка применена НА МЕСТЕ к ${a.graphId} (id не изменился; редакторы и бот подхватят live)`);
|
|
181
184
|
return okResult({ graphId: a.graphId, backupGraphId, inPlace: true, status: saved?.status ?? null, nodes: Array.isArray(saved?.nodes) ? saved.nodes.length : null, edges: Array.isArray(saved?.edges) ? saved.edges.length : null, steps });
|
|
182
185
|
}
|
|
186
|
+
case "patch_graph": {
|
|
187
|
+
const reps = Array.isArray(a.replacements) ? a.replacements : [];
|
|
188
|
+
if (!reps.length) throw new Error("Передай replacements: [{find, replace}] — хотя бы одну замену.");
|
|
189
|
+
for (const r of reps) {
|
|
190
|
+
if (!r || typeof r.find !== "string" || typeof r.replace !== "string") throw new Error("Каждая замена — объект {find:string, replace:string}.");
|
|
191
|
+
if (r.find === "") throw new Error("find не может быть пустой строкой.");
|
|
192
|
+
}
|
|
193
|
+
const current = await api(`/api/tg/graphs/${a.graphId}`);
|
|
194
|
+
let json = JSON.stringify(current);
|
|
195
|
+
const report = [];
|
|
196
|
+
for (const r of reps) {
|
|
197
|
+
const matches = json.split(r.find).length - 1;
|
|
198
|
+
if (matches > 0) json = json.split(r.find).join(r.replace);
|
|
199
|
+
report.push({ find: r.find, replace: r.replace, matches });
|
|
200
|
+
}
|
|
201
|
+
let patched;
|
|
202
|
+
try { patched = JSON.parse(json); }
|
|
203
|
+
catch (e) { throw new Error("После замен JSON графа стал невалидным — правка ОТМЕНЕНА, граф не тронут. Сделай find более специфичным. " + (e?.message || "")); }
|
|
204
|
+
const total = report.reduce((s, r) => s + r.matches, 0);
|
|
205
|
+
if (a.preview === true) return okResult({ preview: true, graphId: a.graphId, totalMatches: total, replacements: report });
|
|
206
|
+
if (total === 0) return okResult({ graphId: a.graphId, changed: false, note: "Ни одна замена не совпала — граф не изменён.", replacements: report });
|
|
207
|
+
let backupGraphId = null;
|
|
208
|
+
if (a.backup !== false) {
|
|
209
|
+
const botId = current.botId;
|
|
210
|
+
const BACKUP_NAME = "🔙 Авто-бэкап (предыдущее состояние)";
|
|
211
|
+
const graphs = await api(`/api/tg/bots/${botId}/graphs`);
|
|
212
|
+
let backup = (Array.isArray(graphs) ? graphs : [])
|
|
213
|
+
.find((g) => g.name === BACKUP_NAME && g.status === "DRAFT" && g.id !== a.graphId);
|
|
214
|
+
if (!backup) backup = await api(`/api/tg/bots/${botId}/graphs`, { method: "POST", body: { name: BACKUP_NAME } });
|
|
215
|
+
backupGraphId = backup.id;
|
|
216
|
+
await api(`/api/tg/graphs/${backup.id}`, { method: "PUT", body: { nodes: current.nodes ?? [], edges: current.edges ?? [], canvasMeta: current.canvasMeta ?? {}, name: BACKUP_NAME } });
|
|
217
|
+
}
|
|
218
|
+
const payload = { nodes: patched.nodes ?? [], edges: patched.edges ?? [], canvasMeta: patched.canvasMeta ?? {} };
|
|
219
|
+
if (patched.name) payload.name = patched.name;
|
|
220
|
+
const saved = await api(`/api/tg/graphs/${a.graphId}`, { method: "PUT", body: payload });
|
|
221
|
+
return okResult({ graphId: a.graphId, changed: true, totalMatches: total, replacements: report, backupGraphId, inPlace: true, status: saved?.status ?? null, nodes: Array.isArray(saved?.nodes) ? saved.nodes.length : null });
|
|
222
|
+
}
|
|
183
223
|
case "dry_run":
|
|
184
224
|
return okResult(await api(`/api/tg/graphs/${a.graphId}/dry-run`, { method: "POST", body: { kind: a.kind, value: a.value, fromUsername: a.fromUsername, presetVariables: a.presetVariables, presetTags: a.presetTags } }));
|
|
185
225
|
case "publish_graph":
|
|
@@ -214,6 +254,8 @@ async function handleCall(params) {
|
|
|
214
254
|
return okResult(await api(`/api/tg/graphs/${a.graphId}/rename`, { method: "PATCH", body: { name: a.name } }));
|
|
215
255
|
case "clone_graph":
|
|
216
256
|
return okResult(await api(`/api/tg/graphs/${a.graphId}/clone`, { method: "POST" }));
|
|
257
|
+
case "copy_graph":
|
|
258
|
+
return okResult(await api(`/api/tg/graphs/${a.graphId}/copy`, { method: "POST", body: { targetBotId: a.targetBotId, preview: a.preview === true } }));
|
|
217
259
|
case "delete_graph":
|
|
218
260
|
await api(`/api/tg/graphs/${a.graphId}`, { method: "DELETE" });
|
|
219
261
|
return okResult(`🗑️ Граф ${a.graphId} удалён.`);
|