@retensy/mcp 0.25.0 → 0.26.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/README.md +42 -4
- package/package.json +2 -2
- package/skills/build-bot-funnel/SKILL.md +1 -1
- package/skills/build-publication-chain/SKILL.md +49 -4
- package/src/index.mjs +102 -7
package/README.md
CHANGED
|
@@ -111,10 +111,10 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
111
111
|
|
|
112
112
|
## Авторизация — персональный токен
|
|
113
113
|
|
|
114
|
-
Токен
|
|
114
|
+
Токен действует от твоего имени **в пределах выбранных прав** (см. «Права токена» ниже).
|
|
115
115
|
|
|
116
116
|
1. Залогинься на https://bots.retensy.com → открой **`/bots/mcp-tokens`**.
|
|
117
|
-
2.
|
|
117
|
+
2. Выбери права токена (пресет «Только чтение», «Полный доступ» или свой набор) и, при желании, срок действия → создай токен → скопируй секрет `zmcp_...` (показывается один раз).
|
|
118
118
|
3. Передай токен любым способом:
|
|
119
119
|
- **просто пришли его агенту в чат** — он вызовет инструмент `set_token` и сохранит токен в `~/.retensy-bot-graph/token` (применяется сразу, без рестарта), **или**
|
|
120
120
|
- `env` в `.mcp.json` (Вариант B), **или**
|
|
@@ -126,6 +126,43 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
126
126
|
> и управление подключениями сервисов (`/api/integrations` — `connect_integration`/`disconnect_integration`/
|
|
127
127
|
> `integration_update` и остальные `integration_*`), не только `/api/bots`. Отдельного токена под эти инструменты не нужно.
|
|
128
128
|
|
|
129
|
+
### Права токена (scopes)
|
|
130
|
+
|
|
131
|
+
С 17.10.2026 у токена есть права. Их проверяет **сервер на каждом запросе**, а не MCP-клиент, поэтому прямой
|
|
132
|
+
вызов API (curl и т.п.) их тоже не обходит; MCP-клиент только объясняет отказ. Но права **крупные** — выдавай
|
|
133
|
+
токену только то, что нужно для задачи.
|
|
134
|
+
|
|
135
|
+
| Право | Что разрешает | Примеры инструментов |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `read` | любое чтение: списки, карточки, аналитика, превью, поиск, `dry_run`. **Включает персональные данные**: подписчики, переписка в диалогах, заявки с сайтов и их файлы | `list_bots`, `get_graph`, `site_leads`, `dialog_messages`, `broadcast_preview` |
|
|
138
|
+
| `write:bots` | создание и изменение ботов, сценариев, цепочек, интеграций, ИИ-агентов, базы знаний, черновиков рассылок. Правка опубликованного сценария сразу меняет ответы бота подписчикам; можно сменить ключи и вебхуки интеграций; `agent_test_chat` тратит ИИ-бюджет, `integration_test` ходит во внешний сервис | `create_graph`, `publish_graph`, `chain_update`, `connect_integration`, `agent_test_chat` |
|
|
139
|
+
| `write:sites` | сайты, лиды, VK-лендинги, статьи | `site_create`, `site_edit`, `site_publish`, `vk_landing_update`, `article_publish` |
|
|
140
|
+
| `crm` | теги/переменные подписчиков, импорт, передача диалога оператору, записи. `booking_confirm`/`booking_reject` отправляют клиенту сообщение | `audience_tag_edit`, `bot_users_import`, `dialog_handoff`, `booking_create` |
|
|
141
|
+
| `broadcast` | всё, что доходит до живых людей без участия человека: рассылки, ответы, посты, а также **включённая цепочка без согласования постов** (approvalMode `NONE`/`AI_ONLY`) и **публикация/правка/активация сценария с `TRIGGER_SCHEDULE`** | `broadcast_send`, `dialog_reply`, `channel_post`, `vk_post`, `chain_approve`, `chain_enable` (без согласования) |
|
|
142
|
+
| `billing` | деньги и ключи, которые тратят бюджет | `ai_pack_purchase`, `agent_api_key_create` |
|
|
143
|
+
| `destructive` | любое удаление и массовая выгрузка ПДн | `delete_graph`, `bot_delete`, `audience_export`, `site_leads_export` |
|
|
144
|
+
|
|
145
|
+
- **Пресеты:** «Только чтение» = `read`; «Полный доступ» = все семь. По умолчанию в кабинете предлагается
|
|
146
|
+
`read` + `write:bots` + `write:sites` — собирать ботов и сайты. Это **не «безопасный»** набор: он видит ПДн
|
|
147
|
+
подписчиков и заявок и меняет живые ответы ботов и ключи интеграций. В нём нет только рассылок, оплаты,
|
|
148
|
+
удаления и выгрузки.
|
|
149
|
+
- **Цепочки и расписание** с одним `write:bots`: цепочку можно создать выключенной или с согласованием каждого
|
|
150
|
+
поста (`approvalMode: ALL`), сценарий по расписанию, с отправкой в заданный чат/канал (`target.mode` chat/expression)
|
|
151
|
+
или вебхук-сценарий с получателем из тела запроса (`recipientFrom`) — оставить черновиком. Включить/опубликовать — нужен `broadcast`.
|
|
152
|
+
То же для сценария с письмом на любой адрес, кроме ровно `{{var.email}}` подписчика (и если сценарий сам не
|
|
153
|
+
перезаписывает `email`), и для сценария с триггером события Telegram/комментария и шагами отправки — там
|
|
154
|
+
«ответ в текущий чат» уходит в канал или группу. Правка живого вебхук-сценария и новый секрет живого
|
|
155
|
+
вебхука, который шлёт людям, тоже требуют `broadcast`.
|
|
156
|
+
- **Нет права** → сервер отвечает `403 {"error":"insufficient_scope","requiredScope":"…"}`, а инструмент
|
|
157
|
+
возвращает ошибку «Токену не хватает права «X» …». Это не сбой: добавь право токену кнопкой
|
|
158
|
+
«Изменить права» на `/bots/mcp-tokens` (действует со следующего запроса, переподключать клиента не нужно)
|
|
159
|
+
или выпусти новый токен.
|
|
160
|
+
- **Старые токены** (выпущены до появления прав) работают как раньше — с полным доступом; в кабинете они
|
|
161
|
+
помечены «полный доступ (старый токен)». Рекомендуем ограничить им права.
|
|
162
|
+
- **Срок действия** (30/90/365 дней или бессрочно) задаётся при создании; истёкший токен отвечает 401 и не
|
|
163
|
+
ротируется (400 `token_expired`) — выпусти новый. Ротация сохраняет права и длительность срока; API ротации
|
|
164
|
+
принимает и новые права (`scopes`/`preset`) — так старый токен переводят на явные права.
|
|
165
|
+
|
|
129
166
|
> **Не знаешь, что делать?** Скажи агенту «настрой подключение» — он вызовет `setup`, объяснит шаги и попросит токен. Любой инструмент при отсутствии токена тоже вернёт пошаговую инструкцию.
|
|
130
167
|
|
|
131
168
|
> Дев-окружение: `RETENSY_BASE_URL=http://localhost:8066`.
|
|
@@ -263,7 +300,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
263
300
|
| `bothelp_scenario_commit(botId, scenarioFile, externalRequestsDir?)` | то же + сохраняет граф (DRAFT); идемпотентно по `(botId, bothelpScenarioId)` — повторный вызов обновляет тот же граф |
|
|
264
301
|
| `chain_list()` / `chain_get(chainId)` | цепочки публикаций («Публикации» → «Цепочки», автопостинг source → граф обработки/назначений) / одна цепочка как хранится (legacy либо граф) |
|
|
265
302
|
| `chain_graph_get(chainId)` | граф цепочки ВСЕГДА graph-shaped `{nodes, edges}`, даже для старых линейных цепочек (синтезируется на лету) |
|
|
266
|
-
| `chain_create(chain)` / `chain_update(chainId, chain)` | создать / изменить цепочку целиком (`chain`: `name`, `source` TELEGRAM\|VK\|RSS, `approvalMode?`, `utmDefaults?`, и либо legacy `destinations[]`+`steps[]`, либо граф `nodes[]`+`edges[]` — дерево с ветвлением, не произвольный DAG: один узел может кормить несколько назначений, слияния путей нет); HTTP 409 — прямой цикл с другой цепочкой; графовый 400 — «Схема цепочки: …» (см. `chain_validate`) |
|
|
303
|
+
| `chain_create(chain)` / `chain_update(chainId, chain)` | создать / изменить цепочку целиком (`chain`: `name`, `source` TELEGRAM\|VK\|RSS, `approvalMode?`, `utmDefaults?`, и либо legacy `destinations[]`+`steps[]`, либо граф `nodes[]`+`edges[]` — дерево с ветвлением, не произвольный DAG: один узел может кормить несколько назначений, слияния путей нет); граф-узлы — опциональные `x?`/`y?` (координаты холста, UI-подсказка, бэкенд не интерпретирует, опусти → авто-layout); каждое `destination`/`destinations[]` — опциональный `aiOverride?: {enabled, aiMode, tone?, instructions?}`, персональная адаптация ИИ сверх общего `AI_ADAPT` только для этого назначения; HTTP 409 — прямой цикл с другой цепочкой; графовый 400 — «Схема цепочки: …» (см. `chain_validate`) |
|
|
304
|
+
| `chain_create_draft(name)` | создать минимальный черновик цепочки (`enabled:false`, без источника/назначений) для потока «создать → сразу донастроить на холсте „Схема“»; не проходит обычную валидацию `chain_create` |
|
|
267
305
|
| `chain_validate(nodes, edges)` / `chain_validate(chain)` | офлайн-проверка графа (зеркало серверного `ChainGraphValidator`, не строже сервера) — перед `chain_create`/`chain_update` на сложном графе |
|
|
268
306
|
| `chain_destinations_catalog(botIds?)` | одним вызовом: все TG/MAX-каналы по ботам владельца + VK-аккаунты — собрать «1 источник → N каналов» без ручного обхода `list_bots`+`list_channels`+`vk_status` |
|
|
269
307
|
| `chain_delete(chainId, confirm:true)` | удалить цепочку (теряется история заданий) |
|
|
@@ -432,7 +470,7 @@ RETENSY_MCP_TOKEN=zmcp_... node src/index.mjs # стартует stdio MCP-с
|
|
|
432
470
|
|
|
433
471
|
## Безопасность
|
|
434
472
|
|
|
435
|
-
Токен = доступ к аккаунту по API. Не коммить
|
|
473
|
+
Токен = доступ к аккаунту по API в пределах его прав. Выдавай агенту минимально нужные права (для знакомства — «Только чтение»). Не коммить токен; держи в `env`. В конфигах храни ссылку `${RETENSY_MCP_TOKEN}`, не само значение.
|
|
436
474
|
|
|
437
475
|
## Лицензия
|
|
438
476
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "MCP server to build and publish Telegram and MAX bot funnels/automations, broadcasts, service integrations, block-based websites (incl. free-layout Zero blocks, designs, templates, custom domains, form leads) and Markdown blog articles in the Retensy Bots service. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node src/index.mjs",
|
|
23
23
|
"check": "node --check src/index.mjs",
|
|
24
|
-
"test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs",
|
|
24
|
+
"test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs && node scripts/token-scopes.test.mjs",
|
|
25
25
|
"validate": "node skills/build-bot-funnel/validate.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
@@ -74,7 +74,7 @@ description: Собрать воронку (сценарий) бота для с
|
|
|
74
74
|
|
|
75
75
|
## Важные ограничения
|
|
76
76
|
- **Источник = текст** (этот режим). Если просят распознать с приватной Miro-доски — самый надёжный путь: CSV-экспорт из Miro; либо запуск залогиненного Chrome пользователя и съёмка экрана (headless WebGL-холст Miro не отдаёт). Это отдельный сценарий, не основной для этого скилла.
|
|
77
|
-
- **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, полный доступ). Если инструмент вернул «нет токена» или ошибку доступа — **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `RETENSY_MCP_TOKEN` и session-cookie.
|
|
77
|
+
- **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, с правами: `read`, `write:bots`, `write:sites`, `crm`, `broadcast`, `billing`, `destructive`; старые токены — полный доступ). Ошибка «Токену не хватает права «X»» — не сбой и не повод перевыпускать токен вслепую: скажи пользователю, какое право нужно, и попроси добавить его на `/bots/mcp-tokens` («Изменить права», действует сразу); для сборки воронки достаточно `read` + `write:bots`, но публикация сценария с `TRIGGER_SCHEDULE` (и включение цепочки без согласования) требует ещё `broadcast` — без него оставь черновик и скажи об этом пользователю. Если инструмент вернул «нет токена» или ошибку доступа — **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `RETENSY_MCP_TOKEN` и session-cookie.
|
|
78
78
|
- Бэкенд читает плоские поля `config.text`/`config.photoUrl`; редактор берёт текст из первой карточки `type:"text"`. Поэтому **всегда заполняй и `text`, и `cards`**.
|
|
79
79
|
- **Размер графа ≤ 4 МБ** (JSON `{nodes, edges, canvasMeta}` в UTF-8): больше — `HTTP 422 GRAPH_TOO_LARGE` на `update_graph`/`edit_graph_live`/`patch_graph`/`import_funnel`, и у черновика тоже; граф не сохранён. `validate.mjs` проверяет заранее.
|
|
80
80
|
- **Переменные подписчика:** строка ≤ 65 536 символов (длиннее обрезается), список/объект длиннее не сохраняется; все переменные одного подписчика вместе с именами ≤ 262 144 символов — запись сверх лимита не сохраняется. Большой ответ API не клади в переменную целиком — вытаскивай нужное через `extract`.
|
|
@@ -38,10 +38,14 @@ legacy (steps[]+destinations[]) ЛИБО граф (nodes[]+edges[])}`.
|
|
|
38
38
|
```
|
|
39
39
|
nodes: [{ id, type: SOURCE|FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY|DESTINATION, x?, y?,
|
|
40
40
|
step?: {...поля шага, см. ниже — только для type≠SOURCE/DESTINATION},
|
|
41
|
-
destination?: {id, type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?,
|
|
41
|
+
destination?: {id, type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?,
|
|
42
|
+
utmOverride?, aiOverride?} }]
|
|
42
43
|
edges: [{ fromNodeId, toNodeId }]
|
|
43
44
|
```
|
|
44
|
-
`x`/`y` — координаты на
|
|
45
|
+
`x`/`y` (number, опционально) — координаты узла на холсте «Схема», чисто UI-подсказка, бэкенд их не
|
|
46
|
+
интерпретирует и ничего не ломается, если их не передать: фронтенд сам раскладывает узлы
|
|
47
|
+
BFS-авто-layout'ом по столбцам/строкам. Не придумывай координаты сам — просто опускай поле (или
|
|
48
|
+
оставляй `null`), если пользователь явно не просит конкретную раскладку.
|
|
45
49
|
Узел — «мешок полей»: только поля своего `type` что-то значат.
|
|
46
50
|
|
|
47
51
|
- **Legacy (линейный режим, `nodes` пуст)** — проще для одного source → один путь обработки → N
|
|
@@ -49,7 +53,7 @@ legacy (steps[]+destinations[]) ЛИБО граф (nodes[]+edges[])}`.
|
|
|
49
53
|
потом веер в destinations»):
|
|
50
54
|
```
|
|
51
55
|
steps: [{ type: FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY, ...поля }] // применяются по порядку ко всем
|
|
52
|
-
destinations: [{ type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride? }]
|
|
56
|
+
destinations: [{ type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?, aiOverride? }]
|
|
53
57
|
```
|
|
54
58
|
Используй legacy, когда обработка ОДИНАКОВА для всех назначений (линия, без развилок). Переходи на
|
|
55
59
|
граф, когда назначениям нужна РАЗНАЯ обработка (например, свой тон ИИ под каждую площадку) или нужно
|
|
@@ -68,12 +72,46 @@ legacy (steps[]+destinations[]) ЛИБО граф (nodes[]+edges[])}`.
|
|
|
68
72
|
- `DELAY`: `delaySeconds`.
|
|
69
73
|
|
|
70
74
|
- **`destination`/`destinations[]`**: `{id (строка, стабильный ключ идемпотентности — не меняй после
|
|
71
|
-
первого сохранения), type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride
|
|
75
|
+
первого сохранения), type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?,
|
|
76
|
+
aiOverride?}`.
|
|
72
77
|
TELEGRAM/MAX — `botId`+`chatId` (свой привязанный канал, бот должен быть админом). VK — `accountId`.
|
|
73
78
|
**INSTAGRAM принимается моделью, но публикация ГАРАНТИРОВАННО уходит в `SKIPPED`** (не `FAILED`) —
|
|
74
79
|
нет медиа-процесса в этой волне; не предлагай его пользователю как рабочее назначение, только если
|
|
75
80
|
он сам просит «добавить на будущее».
|
|
76
81
|
|
|
82
|
+
- **Общий ИИ vs персональный `aiOverride` — когда что использовать.** Два РАЗНЫХ способа применить
|
|
83
|
+
адаптацию ИИ, и их можно комбинировать:
|
|
84
|
+
- **Общий узел `AI_ADAPT`** (`step`/`steps[]`) — обрабатывает пост ОДИН раз и кормит ВСЕ назначения
|
|
85
|
+
на этом пути одинаковым результатом. Оплачивается один раз на пост источника, даже если на выходе
|
|
86
|
+
10 назначений. Используй, когда у всех (или у большинства) каналов один и тот же тон/формат.
|
|
87
|
+
- **`destination.aiOverride`** — `{enabled, aiMode: NONE|LIGHT|FULL, tone?, instructions?}`,
|
|
88
|
+
персональная ВТОРАЯ адаптация ТОЛЬКО для этого одного назначения: применяется ПОСЛЕ результата
|
|
89
|
+
общего `AI_ADAPT` на пути (или поверх сырого текста источника, если общего узла на пути к этому
|
|
90
|
+
назначению нет), своими `aiMode`/`tone`/`instructions`, и не трогает соседние назначения под тем
|
|
91
|
+
же общим узлом. `null`/`enabled:false` (по умолчанию) — поведение не меняется, работает только
|
|
92
|
+
общий узел как обычно. Каждое назначение с `aiOverride.enabled:true` — это ОТДЕЛЬНЫЙ оплачиваемый
|
|
93
|
+
вызов ИИ (сверх общего узла, если он есть), не путай с общим узлом с точки зрения AI-бюджета.
|
|
94
|
+
- Используй `aiOverride`, когда нужен один общий тон для большинства каналов, но для одного-двух —
|
|
95
|
+
особый (например, более формальный для делового VK-сообщества при неформальном общем тоне), без
|
|
96
|
+
заведения отдельного `AI_ADAPT`-узла и ветки в графе ради одного назначения — проще и легаси, и
|
|
97
|
+
граф. Если РАЗНЫХ тонов больше двух-трёх и они устойчивые — удобнее явные ветки (Пример 3), чтобы
|
|
98
|
+
схема была видна в UI; `aiOverride` — для точечных исключений из общего правила.
|
|
99
|
+
- Пример (легаси, общий `AI_ADAPT` деловым тоном + одно назначение с персональным разговорным
|
|
100
|
+
тоном):
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"steps": [{ "type": "AI_ADAPT", "aiMode": "LIGHT", "tone": "деловой, нейтральный" }],
|
|
104
|
+
"destinations": [
|
|
105
|
+
{ "type": "TELEGRAM", "botId": "<botId>", "chatId": 2001 },
|
|
106
|
+
{ "type": "VK", "accountId": "<accountId>",
|
|
107
|
+
"aiOverride": { "enabled": true, "aiMode": "LIGHT", "tone": "разговорный, с эмодзи",
|
|
108
|
+
"instructions": "короче, без канцелярита" } }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
TG получает результат общего узла как есть; VK получает ЕЩЁ ОДНУ адаптацию поверх общего —
|
|
113
|
+
разговорным тоном, с отдельной оплатой ИИ только за это назначение.
|
|
114
|
+
|
|
77
115
|
- **`utmDefaults`** (на уровне цепочки) / **`destination.utmOverride`** (на уровне назначения, `null` =
|
|
78
116
|
взять `utmDefaults`): `{enabled, source, medium, campaign, content, term,
|
|
79
117
|
overridePolicy: KEEP_EXISTING|OVERRIDE (default KEEP_EXISTING), skipInternalLinks (default true)}`.
|
|
@@ -90,6 +128,13 @@ legacy (steps[]+destinations[]) ЛИБО граф (nodes[]+edges[])}`.
|
|
|
90
128
|
|
|
91
129
|
## Рабочий процесс
|
|
92
130
|
|
|
131
|
+
0. **(Опционально) `chain_create_draft`** — если пользователь хочет «создать цепочку и донастроить
|
|
132
|
+
в UI на холсте» без того, чтобы ты сразу собирал полный граф/legacy: передай только `{name}`,
|
|
133
|
+
получишь `PublishChain` с `enabled:false`, без источника/назначений — черновик не проходит обычную
|
|
134
|
+
валидацию `chain_create` и не может случайно уйти в работу недонастроенным. Обычно НЕ нужен агенту:
|
|
135
|
+
если у тебя уже есть все вводные (источник, назначения, обработка), просто используй `chain_create`
|
|
136
|
+
с полным объектом за один вызов — это быстрее для пользователя.
|
|
137
|
+
|
|
93
138
|
1. **Собери вводные.** Источник (платформа + канал/аккаунт/URL), что меняется в тексте по пути
|
|
94
139
|
(фильтры, переписывание, ИИ), куда публикуется (один список площадок или разные пути на разные
|
|
95
140
|
площадки), нужно ли согласование, нужна ли UTM-метка и различается ли она по каналам.
|
package/src/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import os from "node:os";
|
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
|
|
30
|
-
const VERSION = "0.
|
|
30
|
+
const VERSION = "0.26.0";
|
|
31
31
|
/** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
|
|
32
32
|
const SITE_INITS = ["starter", "blank", "mini-landing"];
|
|
33
33
|
/** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
|
|
@@ -277,6 +277,8 @@ async function api(path_, { method = "GET", body, rawBody } = {}) {
|
|
|
277
277
|
throw new Error(`Доступ отклонён (HTTP 401). Токен невалиден, отозван или истёк.\n` +
|
|
278
278
|
`Создай новый на ${TOKENS_PAGE} и пришли мне — я сохраню через set_token.`);
|
|
279
279
|
}
|
|
280
|
+
const scopeErr = scopeError(res.status, data);
|
|
281
|
+
if (scopeErr) throw scopeErr;
|
|
280
282
|
if (res.status === 403) {
|
|
281
283
|
// 403 бэкенд отдаёт и на «не твой бот/граф/подключение» — это не всегда про токен.
|
|
282
284
|
const why = bodyReason(data);
|
|
@@ -289,6 +291,38 @@ async function api(path_, { method = "GET", body, rawBody } = {}) {
|
|
|
289
291
|
return data;
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
/** Описания прав токена — те же имена, что у бэкенда (PatScope). */
|
|
295
|
+
const SCOPE_TITLES = {
|
|
296
|
+
read: "чтение",
|
|
297
|
+
"write:bots": "изменение ботов и сценариев",
|
|
298
|
+
"write:sites": "изменение сайтов и лендингов",
|
|
299
|
+
crm: "работа с аудиторией (CRM)",
|
|
300
|
+
broadcast: "рассылки и сообщения подписчикам",
|
|
301
|
+
billing: "оплаты и платные ключи",
|
|
302
|
+
destructive: "удаление и выгрузка персональных данных",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 403 {error:"insufficient_scope"} — у токена нет нужного права (scopes проверяет сервер на каждом запросе).
|
|
307
|
+
* Это не «чужой объект» и не «токен сломан»: повторять запрос бессмысленно, нужен токен с этим правом.
|
|
308
|
+
*/
|
|
309
|
+
function scopeError(status, data) {
|
|
310
|
+
if (status !== 403 || !data || typeof data !== "object" || data.error !== "insufficient_scope") return null;
|
|
311
|
+
const scope = typeof data.requiredScope === "string" ? data.requiredScope : null;
|
|
312
|
+
const have = Array.isArray(data.tokenScopes) && data.tokenScopes.length ? ` Права токена сейчас: ${data.tokenScopes.join(", ")}.` : "";
|
|
313
|
+
const msg = scope
|
|
314
|
+
? `Токену не хватает права «${scope}» (${SCOPE_TITLES[scope] || scope}) — сервер отклонил запрос (HTTP 403).${have} ` +
|
|
315
|
+
`Это ограничение токена, а не ошибка: попроси владельца добавить право «${scope}» токену на ${TOKENS_PAGE} ` +
|
|
316
|
+
`(кнопка «Изменить права», действует сразу) или выпустить новый токен с этим правом. Повторять запрос без этого бесполезно.`
|
|
317
|
+
: `Токену не хватает прав: этот запрос недоступен токену с ограниченными правами (HTTP 403).${have} ` +
|
|
318
|
+
`Попроси владельца выдать токену «Полный доступ» на ${TOKENS_PAGE} или выполни действие в кабинете.`;
|
|
319
|
+
const err = new Error(msg);
|
|
320
|
+
err.status = 403;
|
|
321
|
+
err.data = data;
|
|
322
|
+
err.requiredScope = scope;
|
|
323
|
+
return err;
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
/** Причина отказа из тела ответа: {error}/{message} или строка. */
|
|
293
327
|
function bodyReason(data) {
|
|
294
328
|
if (data == null) return "";
|
|
@@ -365,6 +399,7 @@ async function uploadMedia({ filePath, url, filename }) {
|
|
|
365
399
|
const text = await res.text();
|
|
366
400
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
367
401
|
if (!res.ok) {
|
|
402
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
368
403
|
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
369
404
|
if (res.status === 402) throw new Error(`Лимит хранилища тарифа исчерпан (HTTP 402). Удали ненужные файлы (delete_file) или подними тариф: 🔗 ${SUBSCRIPTION_PAGE}`);
|
|
370
405
|
if (res.status === 413) throw new Error("Файл больше 50 МБ (HTTP 413) — лимит Telegram для видео/документов.");
|
|
@@ -407,6 +442,7 @@ async function uploadSiteAsset(siteId, { filePath, url }) {
|
|
|
407
442
|
const text = await res.text();
|
|
408
443
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
409
444
|
if (!res.ok) {
|
|
445
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
410
446
|
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
411
447
|
if (res.status === 402) throw new Error(`Лимит хранилища тарифа исчерпан (HTTP 402). Подними тариф: 🔗 ${SUBSCRIPTION_PAGE}`);
|
|
412
448
|
throw httpError("POST", `/api/bots/pages/${siteId}/upload`, res.status, data);
|
|
@@ -423,6 +459,7 @@ async function apiBinaryPost(path_, body) {
|
|
|
423
459
|
const text = await res.text().catch(() => "");
|
|
424
460
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
425
461
|
if (res.status === 401) throw new Error(`Доступ отклонён (HTTP 401). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
462
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
426
463
|
if (res.status === 403) throw new Error(`Доступ отклонён (HTTP 403)${bodyReason(data) ? `: ${bodyReason(data)}` : ""}.`);
|
|
427
464
|
if (res.status === 402) throw paymentError(data);
|
|
428
465
|
throw httpError("POST", path_, res.status, data);
|
|
@@ -480,6 +517,7 @@ async function migrationMultipart(path_, { filePath, platform, mapping }) {
|
|
|
480
517
|
const text = await res.text();
|
|
481
518
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
482
519
|
if (!res.ok) {
|
|
520
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
483
521
|
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
484
522
|
throw httpError("POST", path_, res.status, data);
|
|
485
523
|
}
|
|
@@ -630,6 +668,20 @@ function uuidArg(v, label) {
|
|
|
630
668
|
if (!UUID_RE.test(s)) throw new Error(`${label} — UUID (например, из list_bots), получено: ${s || "пусто"}.`);
|
|
631
669
|
return s;
|
|
632
670
|
}
|
|
671
|
+
/** Тело vk_landing_create/vk_landing_update — одна форма на оба (PUT полностью заменяет поля). */
|
|
672
|
+
function landingBody(a) {
|
|
673
|
+
return {
|
|
674
|
+
botId: uuidArg(a.botId, "botId"),
|
|
675
|
+
title: String(a.title),
|
|
676
|
+
subtitle: a.subtitle ?? null,
|
|
677
|
+
imageUrl: a.imageUrl ?? null,
|
|
678
|
+
buttonText: a.buttonText ?? null,
|
|
679
|
+
targetGraphId: a.targetGraphId ? uuidArg(a.targetGraphId, "targetGraphId") : null,
|
|
680
|
+
tags: Array.isArray(a.tags) ? a.tags.map(String) : [],
|
|
681
|
+
autoSubscribe: !!a.autoSubscribe,
|
|
682
|
+
bothelpLandingId: a.bothelpLandingId ?? null,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
633
685
|
// id из Mongo/строковые ключи: только безопасный сегмент пути, без «/», «..», пробелов.
|
|
634
686
|
function idArg(v, label) {
|
|
635
687
|
const s = String(v ?? "").trim();
|
|
@@ -1028,7 +1080,13 @@ const TOOLS = [
|
|
|
1028
1080
|
{ name: "kb_reindex", description: "Переиндексировать документ-файл базы знаний из сохранённого оригинала (POST /api/bots/kb/{kbId}/docs/{docId}/reindex) — «Повторить» после ошибки. headerRow — необязательно, для табличных файлов: номер строки с шапкой (1..50), если автоопределение ошиблось. Только для источника FILE.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, docId: { type: "string" }, headerRow: { type: "number" } }, required: ["kbId", "docId"] } },
|
|
1029
1081
|
{ name: "agent_unanswered", description: "Вопросы без ответа агента за период (GET /api/bots/agents/{agentId}/unanswered): days — 7|30|90 (по умолчанию 30). Группы вопросов, на которые агент не нашёл ответ в базе знаний — подсказка, что туда добавить. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, days: { type: "number", enum: [7, 30, 90] } }, required: ["agentId"] } },
|
|
1030
1082
|
// ---- Боты ----
|
|
1031
|
-
{ name: "create_bot", description: "Подключить бота по токену (POST /api/bots): platform TELEGRAM (токен от @BotFather) или MAX (токен от MasterBot в MAX). Вебхук настраивается сам; name — отображаемое имя (иначе @username). Возвращает бота с id. Число ботов ограничено тарифом — HTTP 402 со ссылкой на смену тарифа. platform WEB — чат-виджет для сайта без токена (POST /api/bots/web): вернёт botId, key и snippet — код вставки на сайт (у существующего виджета — web_widget_snippet). platform INSTAGRAM по токену не подключается — только входом через Instagram (OAuth) в кабинете (/bots/instagram), нужен Instagram-аккаунт Business или Creator — инструмент вернёт ссылку на кабинет вместо ошибки.", inputSchema: { type: "object", properties: { platform: { type: "string", enum: ["TELEGRAM", "MAX", "WEB", "INSTAGRAM"] }, token: { type: "string", description: "Токен
|
|
1083
|
+
{ name: "create_bot", description: "Подключить бота по токену (POST /api/bots): platform TELEGRAM (токен от @BotFather) или MAX (токен от MasterBot в MAX). Вебхук настраивается сам; name — отображаемое имя (иначе @username). Возвращает бота с id. Число ботов ограничено тарифом — HTTP 402 со ссылкой на смену тарифа. platform WEB — чат-виджет для сайта без токена (POST /api/bots/web): вернёт botId, key и snippet — код вставки на сайт (у существующего виджета — web_widget_snippet). platform INSTAGRAM по токену не подключается — только входом через Instagram (OAuth) в кабинете (/bots/instagram), нужен Instagram-аккаунт Business или Creator — инструмент вернёт ссылку на кабинет вместо ошибки. platform VK — сообщество как полноценный канал (Callback API, НЕ путать с vk_connect — тот для публикаций на стену): token — токен СООБЩЕСТВА (Управление сообществом → Работа с API → Создать ключ, доступ «Сообщения сообщества»), groupId — числовой id сообщества (обязателен). После подключения нужно вручную включить Callback API в настройках сообщества — см. webhookSecret в ответе и URL /vk/hooks/{botId} (подробный чек-лист у разработчика). inactive (только TELEGRAM/MAX) — создать бота остановленным: токен проверяется безопасным getMe, вебхук/поллинг НЕ трогаются — живой вебхук бота в другом конструкторе (BotHelp и т.п.) не снимается. Нужно при переносе бота из другого сервиса: создай inactive:true, перенеси сценарии/подписчиков (migration_preview/migration_commit), потом bot_resume — это и есть переключение (cutover).", inputSchema: { type: "object", properties: { platform: { type: "string", enum: ["TELEGRAM", "MAX", "WEB", "INSTAGRAM", "VK"] }, token: { type: "string", description: "Токен бота/сообщества: 123456789:AA… (Telegram), токен MAX, или community token VK" }, groupId: { type: "string", description: "Только platform=VK: числовой id сообщества" }, name: { type: "string" }, inactive: { type: "boolean", description: "Только TELEGRAM/MAX: создать остановленным (перенос из другого конструктора) — включить потом через bot_resume" } }, required: ["platform"] } },
|
|
1084
|
+
// ---- VK-лендинги (growth tools, Mini App) ----
|
|
1085
|
+
{ name: "vk_landing_list", description: "Список VK-лендингов (Mini App, GET /api/growth/vk-landings): контент, сценарий-цель, авто-режим, готовые ссылки links.{base,auto}, статистика views/clicks/subscribes. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1086
|
+
{ name: "vk_landing_get", description: "Один VK-лендинг по id (GET /api/growth/vk-landings/{id}). 404 — не найден/чужой.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
1087
|
+
{ name: "vk_landing_create", description: "Создать VK-лендинг (POST /api/growth/vk-landings): botId — VK-бот (create_bot platform VK), title обязателен, остальное опционально — subtitle, imageUrl, buttonText, targetGraphId (сценарий — список scenario_list/list_graphs), tags (добавляются подписчику при переходе), autoSubscribe (true = &auto=1, подписка сразу при открытии), bothelpLandingId (маппинг старой ссылки BotHelp). Возвращает лендинг с готовыми ссылками links.base/links.auto вида vk.com/app<id>_-<groupId>#l=<id>[&auto=1] — публикуй их в сообществе.", inputSchema: { type: "object", properties: { botId: { type: "string" }, title: { type: "string" }, subtitle: { type: "string" }, imageUrl: { type: "string" }, buttonText: { type: "string" }, targetGraphId: { type: "string" }, tags: { type: "array", items: { type: "string" } }, autoSubscribe: { type: "boolean" }, bothelpLandingId: { type: "string" } }, required: ["botId", "title"] } },
|
|
1088
|
+
{ name: "vk_landing_update", description: "Изменить VK-лендинг (PUT /api/growth/vk-landings/{id}) — те же поля, что vk_landing_create, полностью заменяют текущие (не частичный патч).", inputSchema: { type: "object", properties: { id: { type: "string" }, botId: { type: "string" }, title: { type: "string" }, subtitle: { type: "string" }, imageUrl: { type: "string" }, buttonText: { type: "string" }, targetGraphId: { type: "string" }, tags: { type: "array", items: { type: "string" } }, autoSubscribe: { type: "boolean" }, bothelpLandingId: { type: "string" } }, required: ["id", "botId", "title"] } },
|
|
1089
|
+
{ name: "vk_landing_delete", description: "Удалить VK-лендинг (DELETE /api/growth/vk-landings/{id}) — ссылки перестают работать.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
1032
1090
|
{ name: "bot_stop", description: "Остановить бота (POST /api/bots/{botId}/stop): снимает вебхук, бот перестаёт отвечать, сценарии и подписчики сохраняются. Запуск обратно — bot_resume.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1033
1091
|
{ name: "bot_resume", description: "Запустить остановленного бота или бота, приостановленного лимитом тарифа (POST /api/bots/{botId}/resume). Если лимит ботов тарифа исчерпан — HTTP 402 со ссылкой на смену тарифа.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1034
1092
|
// ---- Подключения ----
|
|
@@ -1127,8 +1185,9 @@ const TOOLS = [
|
|
|
1127
1185
|
{ name: "chain_list", description: "Цепочки публикаций пользователя (GET /api/bots/chains): автоматический репост source → граф обработки/назначений. Цепочка привязана к владельцу, не к конкретному боту. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1128
1186
|
{ name: "chain_get", description: "Цепочка по id целиком, как хранится в базе — легаси-поля (source/steps/destinations) ИЛИ нативный граф (nodes/edges), смотря что заполнено (GET /api/bots/chains/{id}). Для гарантированно графовой формы (в т.ч. у старых линейных цепочек) используй chain_graph_get. 404 — не своя/не найдена. Read-only.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1129
1187
|
{ name: "chain_graph_get", description: "Граф цепочки В ЛЮБОМ случае (GET /api/bots/chains/{id}/graph): {nodes, edges} — если цепочка легаси (линейная), бэкенд синтезирует эквивалентный граф на лету (ничего не сохраняет). Используй это вместо самостоятельной сборки графа из steps/destinations. Read-only.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1130
|
-
{ name: "chain_create", description: "Создать цепочку публикаций (POST /api/bots/chains): chain — PublishChain draft, {name, enabled?, approvalMode?: NONE|AI_ONLY|ALL, source: {type: TELEGRAM|VK|RSS, botId?, chatId?, accountId?, rssUrl?}, utmDefaults?, И ЛИБО легаси-поля (steps?, destinations?) ЛИБО нативный граф (nodes?, edges?)}. `source` ВСЕГДА отдельное top-level поле — даже в графовом режиме узел SOURCE в nodes его не дублирует, это просто маркер-корень графа. ГРАФ (nodes непустой — приоритетнее steps/destinations, которые тогда игнорируются): nodes — [{id, type: SOURCE|FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY|DESTINATION, x?, y?, step?: {...поля шага, форма как у steps[] в legacy, см. chain_update}, destination?: {id, type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?} — только для type=DESTINATION}], edges — [{fromNodeId, toNodeId}]. Модель — дерево-с-ветвлением (out-tree), НЕ произвольный DAG: у любого не-SOURCE узла ровно ОДНО входящее ребро; ветвление — через НЕСКОЛЬКО исходящих у одного родителя (напр. один общий AI_ADAPT → много DESTINATION = «1 источник → общий ИИ → N каналов»); слияния путей (несколько родителей) нет. Перед сохранением сложного графа прогони chain_validate (клиентское зеркало серверных правил) и chain_destinations_catalog (список доступных назначений). ЛЕГАСИ (nodes пуст): destinations: [{type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?}], steps?: [{type: FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY, ...поля шага}]. TELEGRAM/MAX источник или назначение — свой привязанный канал бота (bot_channel_list/list_channels), VK — подключённый аккаунт (vk_status); chain_destinations_catalog отдаёт оба списка сразу. INSTAGRAM в destinations принимается моделью, но публикация требует медиа и подключённый Instagram-аккаунт — без них задание уходит в SKIPPED с причиной, не FAILED. HTTP 400 — не хватает полей, источник/назначение не найдены/не ваши, ИЛИ (графовый режим) структурная ошибка графа: «Схема цепочки: <причины>» (нет/>1 SOURCE, цикл, узел без входящего ребра или с >1 входящих, DESTINATION с исходящими рёбрами, недостижимое DESTINATION) — ровно те же правила, что проверяет chain_validate. HTTP 409 — прямой цикл с другой вашей цепочкой (A→B при уже существующей B→A).", inputSchema: { type: "object", properties: { chain: { type: "object", description: "PublishChain draft — см. описание инструмента" } }, required: ["chain"] } },
|
|
1131
|
-
{ name: "
|
|
1188
|
+
{ name: "chain_create", description: "Создать цепочку публикаций (POST /api/bots/chains): chain — PublishChain draft, {name, enabled?, approvalMode?: NONE|AI_ONLY|ALL, source: {type: TELEGRAM|VK|RSS, botId?, chatId?, accountId?, rssUrl?}, utmDefaults?, И ЛИБО легаси-поля (steps?, destinations?) ЛИБО нативный граф (nodes?, edges?)}. `source` ВСЕГДА отдельное top-level поле — даже в графовом режиме узел SOURCE в nodes его не дублирует, это просто маркер-корень графа. ГРАФ (nodes непустой — приоритетнее steps/destinations, которые тогда игнорируются): nodes — [{id, type: SOURCE|FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY|DESTINATION, x?, y?, step?: {...поля шага, форма как у steps[] в legacy, см. chain_update}, destination?: {id, type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?, aiOverride?} — только для type=DESTINATION}], edges — [{fromNodeId, toNodeId}]. `x`/`y` (number, опционально) — координаты узла для отрисовки на холсте «Схема»; чисто UI-подсказка, бэкенд их не интерпретирует и не валидирует. Опусти (или оставь null) — фронтенд сам раскладывает узлы BFS-авто-layout'ом по столбцам/строкам; не нужно придумывать координаты самостоятельно. `destination.aiOverride` (опционально, та же форма и в легаси destinations[], см. ниже) — персональная адаптация ИИ ТОЛЬКО для этого назначения: {enabled: bool, aiMode: NONE|LIGHT|FULL, tone?: string, instructions?: string}. null/enabled:false (по умолчанию) — ничего не меняется, применяется только общий узел AI_ADAPT на пути к этому назначению (если он есть), как обычно. enabled:true — ПОСЛЕ результата общего узла (или сырого текста источника, если общего AI_ADAPT на пути нет) вызывается ЕЩЁ ОДНА адаптация ИИ с параметрами override, результат идёт ТОЛЬКО в это назначение — соседние назначения под тем же общим узлом не затрагиваются (означает лишний вызов ИИ и лишнее списание только за это назначение). Используй, когда нужен один общий тон для большинства каналов, но для одного-двух — особый (напр. более формальный) без заведения отдельного AI_ADAPT узла в графе. Модель — дерево-с-ветвлением (out-tree), НЕ произвольный DAG: у любого не-SOURCE узла ровно ОДНО входящее ребро; ветвление — через НЕСКОЛЬКО исходящих у одного родителя (напр. один общий AI_ADAPT → много DESTINATION = «1 источник → общий ИИ → N каналов»); слияния путей (несколько родителей) нет. Перед сохранением сложного графа прогони chain_validate (клиентское зеркало серверных правил) и chain_destinations_catalog (список доступных назначений). Для быстрого создания черновика без источника/назначений (поток «создать → сразу в Схему») используй chain_create_draft вместо этого инструмента. ЛЕГАСИ (nodes пуст): destinations: [{type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?, aiOverride?}], steps?: [{type: FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY, ...поля шага}]. `aiOverride` работает в легаси-режиме так же, как в графовом (см. выше) — применяется поверх общего шага AI_ADAPT в steps[], если он есть. TELEGRAM/MAX источник или назначение — свой привязанный канал бота (bot_channel_list/list_channels), VK — подключённый аккаунт (vk_status); chain_destinations_catalog отдаёт оба списка сразу. INSTAGRAM в destinations принимается моделью, но публикация требует медиа и подключённый Instagram-аккаунт — без них задание уходит в SKIPPED с причиной, не FAILED. HTTP 400 — не хватает полей, источник/назначение не найдены/не ваши, ИЛИ (графовый режим) структурная ошибка графа: «Схема цепочки: <причины>» (нет/>1 SOURCE, цикл, узел без входящего ребра или с >1 входящих, DESTINATION с исходящими рёбрами, недостижимое DESTINATION) — ровно те же правила, что проверяет chain_validate. HTTP 409 — прямой цикл с другой вашей цепочкой (A→B при уже существующей B→A).", inputSchema: { type: "object", properties: { chain: { type: "object", description: "PublishChain draft — см. описание инструмента" } }, required: ["chain"] } },
|
|
1189
|
+
{ name: "chain_create_draft", description: "Создать минимальный черновик цепочки (POST /api/bots/chains/draft) по имени: {name}. Возвращает PublishChain с enabled=false, source={type:TELEGRAM} БЕЗ конкретного botId/chatId, без steps/destinations/nodes/edges — обходит обычную валидацию chain_create (которая потребовала бы источник и ≥1 назначение). Только для потока «новая цепочка → сразу открыть на холсте „Схема“ и донастроить источник/назначения там» — черновик не может случайно уйти в работу недонастроенным (enabled всегда false, обычный chain_update с теми же правилами валидации, что и chain_create, не даст сохранить его без источника и назначений). Для обычного создания цепочки сразу с источником и назначениями используй chain_create.", inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } },
|
|
1190
|
+
{ name: "chain_update", description: "Изменить цепочку целиком (PUT /api/bots/chains/{id}): chain — PublishChain draft (та же форма, что в chain_create, легаси ИЛИ граф), заменяет текущую конфигурацию полностью — сначала прочитай chain_get (или chain_graph_get, если нужен гарантированно графовый вид) и поменяй нужное. Те же 400/409, что у chain_create (включая x?/y? на узлах и aiOverride? на destination — см. chain_create).", inputSchema: { type: "object", properties: { chainId: { type: "string" }, chain: { type: "object", description: "PublishChain draft целиком" } }, required: ["chainId", "chain"] } },
|
|
1132
1191
|
{ name: "chain_delete", description: "УДАЛИТЬ цепочку публикаций (DELETE /api/bots/chains/{id}). История заданий (chain_runs) теряется вместе с ней. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { chainId: { type: "string" }, confirm: { type: "boolean" } }, required: ["chainId", "confirm"] } },
|
|
1133
1192
|
{ name: "chain_enable", description: "Включить цепочку (POST /api/bots/chains/{id}/enable) — начинает принимать новые посты источника (TELEGRAM — сразу по событию, VK/RSS — со следующего опроса, раз в 3 мин).", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1134
1193
|
{ name: "chain_pause", description: "Поставить цепочку на паузу (POST /api/bots/chains/{id}/pause) — перестаёт принимать новые посты источника (VK/RSS — опрос останавливается, TELEGRAM — события игнорируются); история и настройки сохраняются.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
@@ -1891,16 +1950,35 @@ async function handleCall(params) {
|
|
|
1891
1950
|
const platform = String(a.platform || "").trim().toUpperCase();
|
|
1892
1951
|
if (platform === "INSTAGRAM") return instagramAnswer();
|
|
1893
1952
|
if (platform === "WEB") return okResult(await api("/api/bots/web", { method: "POST", body: { name: a.name || undefined } }));
|
|
1894
|
-
if (platform !== "TELEGRAM" && platform !== "MAX") throw new Error("platform: TELEGRAM | MAX | WEB (Instagram подключается только в кабинете).");
|
|
1953
|
+
if (platform !== "TELEGRAM" && platform !== "MAX" && platform !== "VK") throw new Error("platform: TELEGRAM | MAX | VK | WEB (Instagram подключается только в кабинете).");
|
|
1895
1954
|
const token = String(a.token || "").trim();
|
|
1955
|
+
if (platform === "VK") {
|
|
1956
|
+
const groupId = String(a.groupId || "").trim();
|
|
1957
|
+
if (!token || !groupId) {
|
|
1958
|
+
return okResult("Нужны токен сообщества и его id: Управление сообществом → Работа с API → Создать ключ (доступ «Сообщения сообщества») — пришли token и groupId (create_bot {platform:\"VK\", token, groupId}).");
|
|
1959
|
+
}
|
|
1960
|
+
let vkBot;
|
|
1961
|
+
try {
|
|
1962
|
+
vkBot = await api("/api/bots", { method: "POST", body: { token, platform: "VK", groupId } });
|
|
1963
|
+
} catch (e) {
|
|
1964
|
+
if (e.status === 400) throw new Error(`VK-бот не подключён: ${typeof e.data === "string" && e.data ? e.data : "токен или groupId не приняты"}. Проверь токен и id сообщества.`);
|
|
1965
|
+
throw e;
|
|
1966
|
+
}
|
|
1967
|
+
if (a.name && vkBot?.id) {
|
|
1968
|
+
try { vkBot = await api(`/api/bots/${vkBot.id}`, { method: "PATCH", body: { name: a.name } }); }
|
|
1969
|
+
catch (e) { return okResult({ ...vkBot, warning: `Бот подключён, но имя не задано: ${(e.message || "").split("\n")[0]}` }); }
|
|
1970
|
+
}
|
|
1971
|
+
return okResult({ ...vkBot, note: "Дальше включи Callback API в настройках сообщества (Управление → Работа с API → Callback API): URL https://<домен>/vk/hooks/" + vkBot.id + ", секретный ключ — поле vkCallbackSecret в этом же ответе, тип события «Входящее сообщение», затем «Подтвердить сервер»." });
|
|
1972
|
+
}
|
|
1896
1973
|
if (!token) {
|
|
1897
1974
|
return okResult(platform === "MAX"
|
|
1898
1975
|
? "Нужен токен MAX-бота: создай бота у MasterBot в MAX и пришли токен — я подключу его (create_bot {platform:\"MAX\", token})."
|
|
1899
1976
|
: "Нужен токен Telegram-бота: открой https://t.me/BotFather → /newbot (или /token для существующего) и пришли токен вида 123456789:AA… — я подключу его (create_bot {platform:\"TELEGRAM\", token}).");
|
|
1900
1977
|
}
|
|
1978
|
+
const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
|
|
1901
1979
|
let bot;
|
|
1902
1980
|
try {
|
|
1903
|
-
bot = await api("/api/bots", { method: "POST", body: { token, platform } });
|
|
1981
|
+
bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
1904
1982
|
} catch (e) {
|
|
1905
1983
|
// 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
|
|
1906
1984
|
if (e.status === 400) throw new Error(`Бот не подключён: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
|
|
@@ -1910,8 +1988,21 @@ async function handleCall(params) {
|
|
|
1910
1988
|
try { bot = await api(`/api/bots/${bot.id}`, { method: "PATCH", body: { name: a.name } }); }
|
|
1911
1989
|
catch (e) { return okResult({ ...bot, warning: `Бот подключён, но имя не задано: ${(e.message || "").split("\n")[0]}` }); }
|
|
1912
1990
|
}
|
|
1913
|
-
return okResult(bot);
|
|
1991
|
+
return okResult(inactive ? { ...bot, note: "Бот создан остановленным (как после bot_stop) — вебхук/поллинг не тронуты. Перенеси сценарии и подписчиков, затем вызови bot_resume для переключения (cutover)." } : bot);
|
|
1992
|
+
}
|
|
1993
|
+
case "vk_landing_list": return okResult(await api("/api/growth/vk-landings"));
|
|
1994
|
+
case "vk_landing_get": return okResult(await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`));
|
|
1995
|
+
case "vk_landing_create": {
|
|
1996
|
+
if (!a.botId || !a.title) throw new Error("botId и title обязательны");
|
|
1997
|
+
return okResult(await api("/api/growth/vk-landings", { method: "POST", body: landingBody(a) }));
|
|
1914
1998
|
}
|
|
1999
|
+
case "vk_landing_update": {
|
|
2000
|
+
if (!a.id || !a.botId || !a.title) throw new Error("id, botId и title обязательны");
|
|
2001
|
+
return okResult(await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`, { method: "PUT", body: landingBody(a) }));
|
|
2002
|
+
}
|
|
2003
|
+
case "vk_landing_delete":
|
|
2004
|
+
await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`, { method: "DELETE" });
|
|
2005
|
+
return okResult({ deleted: true });
|
|
1915
2006
|
case "bot_stop": return okResult(await api(`/api/bots/${a.botId}/stop`, { method: "POST" }));
|
|
1916
2007
|
case "bot_resume": return okResult(await api(`/api/bots/${a.botId}/resume`, { method: "POST" }));
|
|
1917
2008
|
// ---- Подключения ----
|
|
@@ -2174,6 +2265,10 @@ async function handleCall(params) {
|
|
|
2174
2265
|
if (!a.chain || typeof a.chain !== "object") throw new Error("Передай chain — объект PublishChain (см. описание chain_create).");
|
|
2175
2266
|
return okResult(await api("/api/bots/chains", { method: "POST", body: a.chain }));
|
|
2176
2267
|
}
|
|
2268
|
+
case "chain_create_draft": {
|
|
2269
|
+
if (!a.name || typeof a.name !== "string") throw new Error("Передай name — название черновика цепочки.");
|
|
2270
|
+
return okResult(await api("/api/bots/chains/draft", { method: "POST", body: { name: a.name } }));
|
|
2271
|
+
}
|
|
2177
2272
|
case "chain_update": {
|
|
2178
2273
|
const chainId = uuidArg(a.chainId, "chainId");
|
|
2179
2274
|
if (!a.chain || typeof a.chain !== "object") throw new Error("Передай chain — объект PublishChain целиком (сначала прочитай chain_get).");
|