zaytsv-bot-graph-mcp 0.2.0 → 0.3.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zaytsv-bot-graph-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server to build and publish Telegram bot funnels in the zaytsv /bots service. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "zaytsv-bot-graph-mcp": "src/index.mjs" },
|
|
@@ -20,7 +20,8 @@ description: Собрать воронку (сценарий) Telegram-бота
|
|
|
20
20
|
- У каждого `SEND_MESSAGE` непустой `config.text` (и продублируй в `cards[0].text`).
|
|
21
21
|
- id узлов и рёбер — валидные UUID, уникальные.
|
|
22
22
|
- Есть хотя бы один корневой `TRIGGER_*`; все нелистовые узлы достижимы от триггера; синхронных циклов нет (цикл только через `ASK_QUESTION`/`DELAY`/`SCHEDULE`).
|
|
23
|
-
- Кнопки-выборы разведены по хэндлам `btn_N`; условия — `yes`/`no`; вопрос — `valid`/`invalid`.
|
|
23
|
+
- Кнопки-выборы разведены по хэндлам `btn_N`; условия `CONDITION` — `yes`/`no`; вопрос — `valid`/`invalid`.
|
|
24
|
+
- `CONDITION` умеет: теги, переменные, UTM-метки, имя/email/телефон из профиля, @username, подписку на канал (`SUBSCRIBED`), дату/время/день недели. Полная таблица `kind`/`op` — в schema.md.
|
|
24
25
|
|
|
25
26
|
4. **Проверь локально** перед заливкой:
|
|
26
27
|
```bash
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
- `SEND_PHOTO` — `{ "photoUrl": "https://...", "text": "подпись (необязательно)" }`
|
|
47
47
|
|
|
48
48
|
### Логика / ветвление
|
|
49
|
-
- `CONDITION` — выходы `yes` / `no`. `{ "match":"ALL"|"ANY", "conditions":[ { "kind":"...", "op":"...", "key":"...", "value":"..." } ] }`
|
|
49
|
+
- `CONDITION` — проверка условий, выходы `yes` / `no`. `{ "match":"ALL"|"ANY", "conditions":[ { "kind":"...", "op":"...", "key":"...", "value":"..." } ] }`. `match:"ALL"` — все условия истинны; `"ANY"` — хотя бы одно. Полный список `kind`/`op`/полей — в разделе [«Условия CONDITION»](#условия-condition).
|
|
50
50
|
- `BRANCH` — `{ "cases":[ {"id":"c1","label":"...","expression":"var.x=='a'"} ], "hasDefault": false, "abTest": false }`. Выходы: `case_<id>` (+ `default`).
|
|
51
51
|
- `ASK_QUESTION` — вопрос со сбором ответа. `{ "promptText":"...","saveTo":"name","validator":"ANY"|"PHONE"|"EMAIL"|"REGEX","regex":"...","retryText":"...","maxAttempts":3 }`. Выходы `valid` / `invalid`.
|
|
52
52
|
- `END` — `{}` (конец ветки).
|
|
@@ -65,6 +65,28 @@
|
|
|
65
65
|
- `CALL_WEBHOOK` — `{ "url":"https://...", "method":"POST", "bodyTemplate":"{...}", "timeoutMs":5000 }`. Выходы `ok` / `error`.
|
|
66
66
|
- `AI_REPLY`, `PAYMENT_LINK`.
|
|
67
67
|
|
|
68
|
+
## Условия CONDITION
|
|
69
|
+
|
|
70
|
+
Каждый элемент `conditions[]` — `{ "kind", "op", ...поля }`. Любая внутренняя ошибка условия = `false` (узел уходит в `no`).
|
|
71
|
+
|
|
72
|
+
| `kind` | `op` (допустимые) | Поля | Что проверяет |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `TAG` | `HAS`, `NOT_HAS` | `value` — метка `[a-z0-9_-]{1,64}` | есть ли у пользователя тег |
|
|
75
|
+
| `VARIABLE` | `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY`, `GT`, `LT` | `key` — имя переменной `[a-z_][a-z0-9_]{0,63}`, `value` | значение переменной (`GT`/`LT` — числовое сравнение) |
|
|
76
|
+
| `UTM` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `key` ∈ `source`/`medium`/`campaign`/`content`/`term`, `value` | UTM-метку клика (`utm_<key>`), регистронезависимо |
|
|
77
|
+
| `NAME` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | имя из профиля, регистронезависимо |
|
|
78
|
+
| `EMAIL` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | email из профиля |
|
|
79
|
+
| `PHONE` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | телефон из профиля |
|
|
80
|
+
| `USERNAME` | `EQUALS`, `CONTAINS` | `value` | @username пользователя Telegram |
|
|
81
|
+
| `SUBSCRIBED` | `SUBSCRIBED`, `NOT_SUBSCRIBED` | `key` — **числовой** id канала/группы (напр. `-1001234567890`) | подписан ли пользователь на канал бота |
|
|
82
|
+
| `CURRENT_DATE` | `BEFORE`, `AFTER`, `EQUALS` | `value` — дата `YYYY-MM-DD` | сегодняшнюю дату (МСК) |
|
|
83
|
+
| `CURRENT_TIME` | `BETWEEN` | `value`, `value2` — время `HH:mm` | текущее время в интервале (через полночь — если `value`>`value2`) |
|
|
84
|
+
| `DAY_OF_WEEK` | `IN` | `days` — массив из `MON`,`TUE`,`WED`,`THU`,`FRI`,`SAT`,`SUN` | день недели (МСК) |
|
|
85
|
+
|
|
86
|
+
Для `NOT_EMPTY`/`EMPTY` поле `value` не нужно. `UTM` без `key` всегда `false`.
|
|
87
|
+
|
|
88
|
+
**`SUBSCRIBED`** работает только если бот **админ** в канале/группе и канал «привязан» (бот узнаёт о членстве через хук `my_chat_member` — добавь бота в канал админом). `key` должен парситься в число, иначе условие = `false`. Профильные поля (`NAME`/`EMAIL`/`PHONE`) и UTM заполняются по ходу воронки (`ASK_QUESTION`→`saveTo`, диплинк-клик с UTM).
|
|
89
|
+
|
|
68
90
|
## Выходные хэндлы (`sourceHandle`) — шпаргалка
|
|
69
91
|
| Узел | Хэндлы |
|
|
70
92
|
|---|---|
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
- `TRIGGER_TEXT`: `matchMode` ∈ {ANY,EQUALS,CONTAINS,REGEX}; для не-ANY нужен `value`.
|
|
31
31
|
- `ASK_QUESTION`: `promptText` обязателен; `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}`.
|
|
32
32
|
- `BRANCH`: ≥1 case; выражения валидны (или `abTest:true`).
|
|
33
|
+
- `CONDITION`: `match` ∈ {ALL, ANY} (`CONDITION_BAD_MATCH`); ≥1 элемент в `conditions` (`CONDITION_EMPTY`). Для `kind:"TAG"` — `value` ∈ `[a-z0-9_-]{1,64}` (`CONDITION_BAD_TAG`); для `kind:"VARIABLE"` — `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`CONDITION_BAD_KEY`). Остальные `kind` (UTM/NAME/EMAIL/PHONE/USERNAME/SUBSCRIBED/CURRENT_*/DAY_OF_WEEK) бэкенд проверяет в рантайме — некорректный `kind`/`op` или нечисловой `key` у `SUBSCRIBED` молча дают `false` (выход `no`), публикацию не блокируют. Список `kind`/`op` — в schema.md.
|
|
33
34
|
- `CALL_WEBHOOK`: `url` обязателен, http(s)://.
|
|
34
35
|
- `DELAY`: `FIXED` с `durationSec>0`, либо `UNTIL` с `isoDate`+`time`.
|
|
35
36
|
- Метки: `[a-z0-9_-]{1,64}`. Переменные: `[a-z_][a-z0-9_]{0,63}`.
|
|
@@ -19,6 +19,22 @@ const warns = [];
|
|
|
19
19
|
|
|
20
20
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
21
|
const VAR_RE = /^[a-z_][a-z0-9_]{0,63}$/;
|
|
22
|
+
const TAG_RE = /^[a-z0-9_-]{1,64}$/;
|
|
23
|
+
|
|
24
|
+
// kind -> допустимые op (для подсказок; бэкенд блокирует только TAG/VARIABLE)
|
|
25
|
+
const COND_OPS = {
|
|
26
|
+
TAG: ["HAS", "NOT_HAS"],
|
|
27
|
+
VARIABLE: ["EQUALS", "NOT_EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY", "GT", "LT"],
|
|
28
|
+
UTM: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
|
|
29
|
+
NAME: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
|
|
30
|
+
EMAIL: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
|
|
31
|
+
PHONE: ["EQUALS", "CONTAINS", "NOT_EMPTY", "EMPTY"],
|
|
32
|
+
USERNAME: ["EQUALS", "CONTAINS"],
|
|
33
|
+
SUBSCRIBED: ["SUBSCRIBED", "NOT_SUBSCRIBED"],
|
|
34
|
+
CURRENT_DATE: ["BEFORE", "AFTER", "EQUALS"],
|
|
35
|
+
CURRENT_TIME: ["BETWEEN"],
|
|
36
|
+
DAY_OF_WEEK: ["IN"],
|
|
37
|
+
};
|
|
22
38
|
|
|
23
39
|
// cardsToLegacy: текст = первая text-карточка с непустым text (или image.url -> photoUrl)
|
|
24
40
|
function cardsToLegacy(cards) {
|
|
@@ -99,6 +115,26 @@ for (const n of nodes) {
|
|
|
99
115
|
case "BRANCH":
|
|
100
116
|
if (!Array.isArray(c.cases) || c.cases.length === 0) errors.push(`${who}: нужен хотя бы один case.`);
|
|
101
117
|
break;
|
|
118
|
+
case "CONDITION": {
|
|
119
|
+
if (c.match !== "ALL" && c.match !== "ANY") errors.push(`CONDITION_BAD_MATCH: ${who} — match ∈ {ALL,ANY}.`);
|
|
120
|
+
if (!Array.isArray(c.conditions) || c.conditions.length === 0) {
|
|
121
|
+
errors.push(`CONDITION_EMPTY: ${who} — нужно хотя бы одно условие.`);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
c.conditions.forEach((cond, i) => {
|
|
125
|
+
if (!cond || typeof cond !== "object") { errors.push(`${who}: условие #${i + 1} — не объект.`); return; }
|
|
126
|
+
const kind = cond.kind;
|
|
127
|
+
// Бэкенд блокирует публикацию только для TAG.value и VARIABLE.key:
|
|
128
|
+
if (kind === "TAG" && !TAG_RE.test(String(cond.value || ""))) errors.push(`CONDITION_BAD_TAG: ${who} — TAG.value ∈ [a-z0-9_-]{1,64}.`);
|
|
129
|
+
if (kind === "VARIABLE" && !VAR_RE.test(String(cond.key || ""))) errors.push(`CONDITION_BAD_KEY: ${who} — VARIABLE.key ∈ [a-z_][a-z0-9_]{0,63}.`);
|
|
130
|
+
// Остальное — подсказки (рантайм бэкенда отдаст false, но публикацию не сорвёт):
|
|
131
|
+
if (!COND_OPS[kind]) warns.push(`${who}: неизвестный kind «${kind}» в условии #${i + 1} — рантайм даст false.`);
|
|
132
|
+
else if (cond.op && !COND_OPS[kind].includes(cond.op)) warns.push(`${who}: op «${cond.op}» не из {${COND_OPS[kind].join(",")}} для ${kind} — рантайм даст false.`);
|
|
133
|
+
if (kind === "SUBSCRIBED" && !/^-?\d+$/.test(String(cond.key || "").trim())) warns.push(`${who}: SUBSCRIBED.key должен быть числовым id канала — иначе false (и бот должен быть админом канала).`);
|
|
134
|
+
if (kind === "UTM" && !cond.key) warns.push(`${who}: UTM без key — рантайм даст false (ожидается source/medium/campaign/content/term).`);
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
102
138
|
}
|
|
103
139
|
}
|
|
104
140
|
|
package/src/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import os from "node:os";
|
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
|
|
24
|
-
const VERSION = "0.
|
|
24
|
+
const VERSION = "0.3.0";
|
|
25
25
|
const BASE = (process.env.ZAYTSV_BASE_URL || "https://zaytsv.ru").replace(/\/+$/, "");
|
|
26
26
|
const CONFIG_DIR = path.join(os.homedir(), ".zaytsv-bot-graph");
|
|
27
27
|
const TOKEN_FILE = path.join(CONFIG_DIR, "token");
|