zaytsv-bot-graph-mcp 0.4.2 → 0.4.3
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 +1 -1
- package/skills/build-bot-funnel/SKILL.md +7 -1
- package/src/index.mjs +27 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zaytsv-bot-graph-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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" },
|
|
@@ -38,9 +38,15 @@ description: Собрать воронку (сценарий) Telegram-бота
|
|
|
38
38
|
- `update_graph(graphId, nodes, edges, canvasMeta)` → залить узлы/рёбра.
|
|
39
39
|
- `dry_run(graphId, kind:"command", value:"start")` → прогнать стартовую ветку, проверить `runStatus`.
|
|
40
40
|
- `publish_graph(graphId)` → если вернулись `errors[]`, разобрать по `code`/`nodeId`, починить узлы, обновить, опубликовать снова.
|
|
41
|
-
- Управление сценариями: `clone_graph
|
|
41
|
+
- Управление сценариями: `clone_graph`, `rename_graph`, `set_active_graph` (переключить живой граф), `delete_graph` (активный нельзя — сначала переключи).
|
|
42
42
|
Если MCP не подключён — отдай готовый `import.json` и подскажи: /bots → граф → **Импорт**.
|
|
43
43
|
|
|
44
|
+
5b. **Правка СУЩЕСТВУЮЩЕГО / живого сценария — по умолчанию `edit_graph_live`, а НЕ clone+publish.**
|
|
45
|
+
- Когда пользователь просит «поправь сценарий X» (особенно если он уже открыт в редакторе или опубликован) — правь **ТОТ ЖЕ `graphId`** через **`edit_graph_live(graphId, nodes, edges)`**. Он сам снимает авто-бэкап предыдущего состояния (один rolling-граф «🔙 Авто-бэкап») и делает PUT на месте — **id не меняется**.
|
|
46
|
+
- Почему так: бэкенд при PUT/публикации шлёт `external_update` в WS-комнату → открытые редакторы перечитывают граф **вживую** (юзеру не надо перезаходить). Бот читает активный граф **заново из БД на каждое сообщение** → правка живого PUBLISHED-графа применяется **сразу, без отдельной публикации**.
|
|
47
|
+
- `clone_graph`+`publish_graph` каждый раз плодят НОВЫЙ id и переключают активный → юзер вынужден открывать новый граф. Так делай только для крупного рискованного рефактора, где нужна изолированная песочница.
|
|
48
|
+
- ⚠️ PUT не валидирует (валидирует только `publish`) → перед `edit_graph_live` живого графа **обязательно** прогони `validate.mjs` + `dry_run`. Откат: опубликовать граф «🔙 Авто-бэкап» (или скопировать его содержимое обратно).
|
|
49
|
+
|
|
44
50
|
6. **Отчитайся**: сколько узлов/веток, какие тексты помечены на проверку, ссылка/ id графа.
|
|
45
51
|
|
|
46
52
|
## Важные ограничения
|
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.4.
|
|
24
|
+
const VERSION = "0.4.3";
|
|
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");
|
|
@@ -105,7 +105,8 @@ const TOOLS = [
|
|
|
105
105
|
{ name: "list_channels", description: "Список каналов/групп, подключённых к боту (chatId, title, type, статус бота, дата). chatId — числовой id для условия SUBSCRIBED («Подписан на канал»).", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
106
106
|
{ name: "get_graph", description: "Получить граф целиком по graphId.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
107
107
|
{ name: "create_graph", description: "Создать пустой граф (DRAFT) в боте. Возвращает граф с id.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
|
|
108
|
-
{ name: "update_graph", description: "Залить узлы/рёбра в граф (PUT). Принимает 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"] } },
|
|
108
|
+
{ 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
|
+
{ 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"] } },
|
|
109
110
|
{ 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"] } },
|
|
110
111
|
{ name: "publish_graph", description: "Опубликовать граф. Вернёт publishedGraphId или errors[] (code, nodeId, message).", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
111
112
|
{ 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"] } },
|
|
@@ -155,6 +156,30 @@ async function handleCall(params) {
|
|
|
155
156
|
if (a.name ?? src.name) payload.name = a.name ?? src.name;
|
|
156
157
|
return okResult(await api(`/api/tg/graphs/${a.graphId}`, { method: "PUT", body: payload }));
|
|
157
158
|
}
|
|
159
|
+
case "edit_graph_live": {
|
|
160
|
+
const src = a.graph ? extractGraph(a.graph) : { nodes: a.nodes, edges: a.edges, canvasMeta: a.canvasMeta ?? {}, name: a.name };
|
|
161
|
+
if (!Array.isArray(src.nodes) || !Array.isArray(src.edges)) throw new Error("Нужны nodes[] и edges[] (в graph или отдельно).");
|
|
162
|
+
const steps = [];
|
|
163
|
+
let backupGraphId = null;
|
|
164
|
+
if (a.backup !== false) {
|
|
165
|
+
// снимок ТЕКУЩЕГО (до правки) состояния в один rolling-граф «🔙 Авто-бэкап» (один на бота, перезаписывается)
|
|
166
|
+
const current = await api(`/api/tg/graphs/${a.graphId}`);
|
|
167
|
+
const botId = current.botId;
|
|
168
|
+
const BACKUP_NAME = "🔙 Авто-бэкап (предыдущее состояние)";
|
|
169
|
+
const graphs = await api(`/api/tg/bots/${botId}/graphs`);
|
|
170
|
+
let backup = (Array.isArray(graphs) ? graphs : [])
|
|
171
|
+
.find((g) => g.name === BACKUP_NAME && g.status === "DRAFT" && g.id !== a.graphId);
|
|
172
|
+
if (!backup) backup = await api(`/api/tg/bots/${botId}/graphs`, { method: "POST", body: { name: BACKUP_NAME } });
|
|
173
|
+
backupGraphId = backup.id;
|
|
174
|
+
await api(`/api/tg/graphs/${backup.id}`, { method: "PUT", body: { nodes: current.nodes ?? [], edges: current.edges ?? [], canvasMeta: current.canvasMeta ?? {}, name: BACKUP_NAME } });
|
|
175
|
+
steps.push(`бэкап предыдущего состояния → ${backup.id} (DRAFT «${BACKUP_NAME}»)`);
|
|
176
|
+
}
|
|
177
|
+
const payload = { nodes: src.nodes, edges: src.edges, canvasMeta: src.canvasMeta ?? {} };
|
|
178
|
+
if (a.name ?? src.name) payload.name = a.name ?? src.name;
|
|
179
|
+
const saved = await api(`/api/tg/graphs/${a.graphId}`, { method: "PUT", body: payload });
|
|
180
|
+
steps.push(`правка применена НА МЕСТЕ к ${a.graphId} (id не изменился; редакторы и бот подхватят live)`);
|
|
181
|
+
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
|
+
}
|
|
158
183
|
case "dry_run":
|
|
159
184
|
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 } }));
|
|
160
185
|
case "publish_graph":
|