@besales/mcp 1.0.0-beta.5 → 1.0.0-beta.9
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 +41 -6
- package/dist/auth/oauth-client.d.ts +1 -1
- package/dist/auth/oauth-client.js +2 -0
- package/dist/auth/oauth-client.js.map +1 -1
- package/dist/http/api-client.d.ts +10 -0
- package/dist/http/api-client.js +32 -0
- package/dist/http/api-client.js.map +1 -1
- package/dist/resources/concepts/sandbox.md +8 -1
- package/dist/resources/concepts/triggers.md +1 -0
- package/dist/resources/docs/platforms.md +58 -1
- package/dist/resources/docs/trigger-actions.md +24 -0
- package/dist/resources/workflows/iterate-with-sandbox.md +6 -2
- package/dist/resources/workflows/platform-setup-from-scratch.md +27 -0
- package/dist/schemas/mcp-tools.json +904 -27
- package/dist/tools/definitions/agent-design.d.ts +2 -0
- package/dist/tools/definitions/agent-design.js +15 -4
- package/dist/tools/definitions/agent-design.js.map +1 -1
- package/dist/tools/definitions/agent-settings.d.ts +1 -0
- package/dist/tools/definitions/agent-settings.js +2 -0
- package/dist/tools/definitions/agent-settings.js.map +1 -1
- package/dist/tools/definitions/broadcast-analytics.js +1 -1
- package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
- package/dist/tools/definitions/broadcast-campaigns.d.ts +29 -0
- package/dist/tools/definitions/broadcast-campaigns.js +107 -0
- package/dist/tools/definitions/broadcast-campaigns.js.map +1 -0
- package/dist/tools/definitions/dialogue-export.js +2 -0
- package/dist/tools/definitions/dialogue-export.js.map +1 -1
- package/dist/tools/definitions/follow-up.d.ts +1 -1
- package/dist/tools/definitions/platform-setup-wave.d.ts +1 -1
- package/dist/tools/definitions/platform-setup.d.ts +12 -1
- package/dist/tools/definitions/task-state.d.ts +1 -1
- package/dist/tools/definitions/telegram-outreach-calendar.d.ts +17 -0
- package/dist/tools/definitions/telegram-outreach-calendar.js +68 -0
- package/dist/tools/definitions/telegram-outreach-calendar.js.map +1 -0
- package/dist/tools/definitions/telegram-personal-connect.d.ts +12 -0
- package/dist/tools/definitions/telegram-personal-connect.js +61 -0
- package/dist/tools/definitions/telegram-personal-connect.js.map +1 -0
- package/dist/tools/definitions/telegram-personal-management.d.ts +1 -1
- package/dist/tools/definitions/telegram-personal-profile.d.ts +2 -0
- package/dist/tools/definitions/telegram-personal-profile.js +14 -0
- package/dist/tools/definitions/telegram-personal-profile.js.map +1 -1
- package/dist/tools/definitions/telegram-personal-proxies.d.ts +42 -0
- package/dist/tools/definitions/telegram-personal-proxies.js +204 -0
- package/dist/tools/definitions/telegram-personal-proxies.js.map +1 -0
- package/dist/tools/definitions/telegram-proxy-stock.d.ts +34 -0
- package/dist/tools/definitions/telegram-proxy-stock.js +129 -0
- package/dist/tools/definitions/telegram-proxy-stock.js.map +1 -0
- package/dist/tools/definitions/telegram-session-import.d.ts +5 -0
- package/dist/tools/definitions/telegram-session-import.js +33 -0
- package/dist/tools/definitions/telegram-session-import.js.map +1 -0
- package/dist/tools/definitions/trigger-action-item.schema.d.ts +10 -0
- package/dist/tools/definitions/trigger-action-item.schema.js +28 -11
- package/dist/tools/definitions/trigger-action-item.schema.js.map +1 -1
- package/dist/tools/definitions/trigger-upsert.tool.d.ts +1 -0
- package/dist/tools/definitions/trigger-upsert.tool.js +13 -0
- package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -1
- package/dist/tools/definitions.d.ts +119 -9
- package/dist/tools/definitions.js +13 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/operation-recovery.d.ts +1 -1
- package/dist/tools/operation-recovery.js +1 -1
- package/dist/tools/operation-recovery.js.map +1 -1
- package/dist/tools/registry-mapping.cases.js +210 -8
- package/dist/tools/registry-mapping.cases.js.map +1 -1
- package/dist/tools/registry.js +20 -7
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/result.d.ts +2 -1
- package/dist/tools/result.js +100 -27
- package/dist/tools/result.js.map +1 -1
- package/dist/types/api-contract.gen.d.ts +2220 -216
- package/docs/async-operation-lifecycle.md +13 -2
- package/docs/failure-semantics.md +15 -2
- package/docs/help-navigation.md +2 -0
- package/docs/secret-read-boundary.md +7 -0
- package/docs/telegram-personal-management.md +95 -24
- package/docs/telegram-personal-proxies.md +135 -0
- package/docs/telegram-session-import.md +53 -0
- package/package.json +8 -7
- package/scripts/body-idempotency-contract.mjs +95 -0
- package/scripts/generate-followup-session-contract.mjs +14 -7
- package/scripts/generated-output.mjs +9 -0
- package/scripts/sync-agent-model-schemas.mjs +11 -11
- package/scripts/sync-api-contracts.mjs +99 -40
- package/scripts/sync-crm-stage-pool-schema.mjs +6 -7
- package/scripts/sync-route-map.mjs +189 -35
- package/scripts/sync-sandbox-request-key.mjs +40 -0
- package/scripts/sync-telegram-profile-photo.mjs +57 -0
- package/scripts/sync-telegram-session-import.mjs +39 -0
- package/scripts/sync-tool-inventory.mjs +15 -7
- package/scripts/sync-trigger-argument-schema.mjs +31 -14
- package/scripts/sync-trigger-event-schema.mjs +11 -6
- package/scripts/sync-trigger-step-reference-schema.mjs +21 -0
- package/scripts/sync-wave-id-schemas.mjs +7 -6
- package/scripts/telegram-personal-profile-mutants.mjs +59 -18
- package/scripts/telegram-personal-proxy-mutants.mjs +171 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Фоновая операция MCP считается полной только тогда, когда у её стартового инструмента есть
|
|
4
4
|
read-only status-инструмент. Стартовый ответ не является доказательством завершения. При сетевом
|
|
5
|
-
обрыве
|
|
6
|
-
`
|
|
5
|
+
обрыве сначала вызывается доступное чтение исхода по исходному ключу. Если для status нужен
|
|
6
|
+
ещё не полученный идентификатор, запуск с `request_key` повторяется с тем же ключом и тем же
|
|
7
|
+
намерением: вернётся та же операция. Запуск без ключа вслепую повторять нельзя.
|
|
7
8
|
|
|
8
9
|
## Канон операций
|
|
9
10
|
|
|
@@ -41,6 +42,16 @@ read-only status-инструмент. Стартовый ответ не явл
|
|
|
41
42
|
`besales_improvement_aggregate_start`) status читает исход по исходному ключу. Исход `UNKNOWN` не
|
|
42
43
|
разрешает новый платный запуск автоматически: сначала выясняется, был ли первый принят.
|
|
43
44
|
|
|
45
|
+
У `besales_icp_import_dialogues` потерянный ответ сначала проверяется через
|
|
46
|
+
`besales_icp_import_start_result` с теми же `icp_id` и `request_key`. Если исход неизвестен
|
|
47
|
+
(`NOT_FOUND_UNKNOWN`), повторяется исходное намерение с тем же ключом.
|
|
48
|
+
|
|
49
|
+
У `besales_sandbox_run_start` чтение исхода по ключу пока не опубликовано в MCP.
|
|
50
|
+
При таймауте или 5xx повторите вызов с тем же `request_key` и тем же намерением: вернётся
|
|
51
|
+
тот же прогон. Только после получения `run_id` проверяйте `besales_sandbox_run_get`.
|
|
52
|
+
Новый осознанный прогон запускайте без `request_key` либо с новым UUID: прежний ключ вернёт
|
|
53
|
+
старый прогон, а при смене намерения — `START_KEY_CONFLICT`.
|
|
54
|
+
|
|
44
55
|
## Синхронные похожие имена
|
|
45
56
|
|
|
46
57
|
`besales_prompt_patch_start` синхронно создаёт draft; его дальнейший жизненный цикл —
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
|
|
22
22
|
Признак неоднозначности выводится из уже объявленного у инструмента: `sideEffect !== 'none'`
|
|
23
23
|
и не объявлена идемпотентность. Тогда на **5xx** и на **обрыв связи** уходит ответ вида
|
|
24
|
-
`OUTCOME_UNKNOWN` —
|
|
24
|
+
`OUTCOME_UNKNOWN` — с указанием, чем свериться. Для запуска песочницы после потери ответа
|
|
25
|
+
повтори `besales_sandbox_run_start` с теми же аргументами и тем же `request_key` из
|
|
26
|
+
`requestIdentity`: сервер вернёт тот же прогон. Новый ключ не создавай. Для остальных
|
|
27
|
+
неидемпотентных операций повтор запрещён до сверки состояния.
|
|
25
28
|
|
|
26
29
|
⚠️ **Идемпотентность объявляется явно, умолчание — худшее.** Правило «все `*_upsert`
|
|
27
30
|
идемпотентны» неверно: без идентификатора они уходят в создание.
|
|
@@ -44,12 +47,22 @@
|
|
|
44
47
|
«операция → проверка» отстала бы от первого же нового асинхронного инструмента, и он остался
|
|
45
48
|
бы без указания ровно там, где цена ошибки максимальна.
|
|
46
49
|
|
|
47
|
-
Пару, которую вывести нельзя (`besales_icp_import_dialogues` → `
|
|
50
|
+
Пару, которую вывести нельзя (`besales_icp_import_dialogues` → `besales_icp_import_start_result`,
|
|
48
51
|
`besales_model_comparison_run_start` → `besales_model_comparison_get`, …), объявляет поле
|
|
49
52
|
`verifyWithToolName` у определения; объявленное перекрывает выведенное. Тесты держат два
|
|
50
53
|
инварианта: объявленное имя существует, и инструмент проверки сам ничего не меняет — иначе
|
|
51
54
|
совет «сверьтесь» вносил бы новую побочку поверх неизвестного исхода.
|
|
52
55
|
|
|
56
|
+
Для запусков с `request_key` HTTP-ошибки возвращают отправленный ключ из разобранного входа
|
|
57
|
+
в тексте и в `error.requestIdentity.request_key`, в том числе UUID, сгенерированный мостом для
|
|
58
|
+
песочницы. Аварийные квитанции при превышении размера, сбое сериализации или постобработки
|
|
59
|
+
сохраняют `request_key` рядом с идентификаторами результата.
|
|
60
|
+
При `OUTCOME_UNKNOWN` инструкция разрешает повторить то же намерение с тем же ключом:
|
|
61
|
+
вернётся та же операция; новый ключ означает новую платную операцию. Для ICP сначала вызывается
|
|
62
|
+
`besales_icp_import_start_result`; если исход неизвестен, допускается повтор с прежним ключом.
|
|
63
|
+
У песочницы без `run_id` повтор с тем же ключом восстанавливает прогон, затем доступен
|
|
64
|
+
`besales_sandbox_run_get`. Исход неизвестен до ответа API, автоматического повтора нет.
|
|
65
|
+
|
|
53
66
|
Если проверки нет ни выведенной, ни объявленной, текст всё равно не советует повтор: он просит
|
|
54
67
|
свериться состоянием на стороне Besales.
|
|
55
68
|
|
package/docs/help-navigation.md
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
изменение маршрутизации, запуск GetCourse-волны и финализация промпта находятся обычным русским
|
|
15
15
|
запросом без знания технического имени.
|
|
16
16
|
|
|
17
|
+
Запрос «создать агента промпт написать» находит создание агента.
|
|
18
|
+
|
|
17
19
|
`besales_help_get({uri})` возвращает индекс. Параметр `section` отдаёт один раздел, `full=true` —
|
|
18
20
|
весь документ. Ответы содержат version/source/complete; неоднозначный раздел не выбирается молча.
|
|
19
21
|
|
|
@@ -72,6 +72,13 @@ Unicode-нормализация не превращает невалидное
|
|
|
72
72
|
|
|
73
73
|
## Полная опись
|
|
74
74
|
|
|
75
|
+
Параметр `stored_file_id` существующего `besales_platform_telegram_personal_profile_bulk`
|
|
76
|
+
учтён отдельно в `secret-read-inventory-additions.json#parameterSurfaces`. Читатель остаётся
|
|
77
|
+
`telegram-personal-profile.ts` (домен `channels-setup`): он возвращает квитанцию задания,
|
|
78
|
+
не байты файла, URL S3 или секреты Telegram. ID файла — метаданные библиотеки; доступ
|
|
79
|
+
повторно проверяет ядро. `photo-only registry response` проверяет настоящий реестр с одним
|
|
80
|
+
фото и секретоносным ответом в обоих режимах конверта. Полный снимок API не переписывается.
|
|
81
|
+
|
|
75
82
|
Канон — `besales-api/test/bridge-secret-read-inventory.json`, перенесённый в снимок
|
|
76
83
|
верификатором. В нём поимённо перечислены все инструменты `mcp-tools.json`, включая
|
|
77
84
|
setup/init/status/rotation, 1С, CRM, внешние API и их ключи, каналы, переменные,
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
только к ядру Besales; пароли, сессии и ключи Telegram не принимаются и не возвращаются.
|
|
5
5
|
Идентификаторы канала и номера берутся из навигации и таблицы номеров.
|
|
6
6
|
|
|
7
|
-
| Инструмент `besales_platform_telegram_personal_…` | Назначение
|
|
8
|
-
|
|
|
9
|
-
| `accounts`
|
|
10
|
-
| `warmup`
|
|
11
|
-
| `warmup_update`
|
|
12
|
-
| `account_warmup`
|
|
13
|
-
| `ramp`
|
|
14
|
-
| `profile_bulk`
|
|
15
|
-
| `profile_job`
|
|
7
|
+
| Инструмент `besales_platform_telegram_personal_…` | Назначение | Право |
|
|
8
|
+
| ------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------- |
|
|
9
|
+
| `accounts` | Таблица номеров, сводка и листание | `read:channels` |
|
|
10
|
+
| `warmup` | Настройки и аналитика прогрева | `read:channels` |
|
|
11
|
+
| `warmup_update` | Частичная правка настроек прогрева | `manage:channels` |
|
|
12
|
+
| `account_warmup` | Включение и ручной отказ отдельного номера | `manage:channels` |
|
|
13
|
+
| `ramp` | Чтение `newAccountRampEnabled` | `read:channels` |
|
|
14
|
+
| `profile_bulk` | Пачечная правка профилей своих номеров: фото, имя, описание, дата рождения, ники | `manage:channels` |
|
|
15
|
+
| `profile_job` | Итог задания правки профилей по номерам | `read:channels` |
|
|
16
16
|
|
|
17
17
|
У инструментов канала обязателен `channel_id`; у `account_warmup` также `account_id`
|
|
18
18
|
и булев `is_warming_up`. `workspace_id` можно не передавать при выбранной рабочей области.
|
|
@@ -54,18 +54,59 @@ Telegram неизвестен, сначала сверь профиль номе
|
|
|
54
54
|
|
|
55
55
|
Поля `profile_bulk` в snake_case, в HTTP уходят camelCase-поля `TelegramPersonalBulkProfileDto`:
|
|
56
56
|
|
|
57
|
-
| Вход
|
|
58
|
-
|
|
|
59
|
-
| `account_ids` или `all:true` | `accountIds` или `all`
|
|
60
|
-
| `first_name`, `last_name`
|
|
61
|
-
| `bio`
|
|
62
|
-
| `birthday`
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
| Вход | В теле ядра | Правило |
|
|
58
|
+
| ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
59
|
+
| `account_ids` или `all:true` | `accountIds` или `all` | ровно одно; `account_ids` — 1–100 уникальных UUID v4 |
|
|
60
|
+
| `first_name`, `last_name` | `patch.firstName`, `patch.lastName` | до 64 знаков, без нормализации |
|
|
61
|
+
| `bio` | `patch.bio` | до 140 знаков; лимит Telegram по Premium проверяет ферма |
|
|
62
|
+
| `birthday` | `patch.birthday` | `{day, month, year?}`; `null` снимает дату |
|
|
63
|
+
| `stored_file_id` | `patch.avatar` | одно фото библиотеки для всех выбранных номеров, UUID v4; статичный JPEG/PNG/WebP до 5 МиБ |
|
|
64
|
+
| `usernames` | `usernames[{accountId, username}]` | ники по номерам; `account_ids` и `all` не передаются |
|
|
65
|
+
| `username_base` | `usernameBase` | основа ника для номеров из `account_ids` или `all` |
|
|
66
|
+
| `idempotency_key` | заголовок `x-idempotency-key` | обязателен: 1–200 видимых ASCII-знаков без пробелов |
|
|
67
|
+
|
|
68
|
+
`stored_file_id` необязателен: прежняя форма вызова сохранена. Пропуск сохраняет фото;
|
|
69
|
+
`null`, URL и псевдонимы `avatar`/`avatar_file_id` отвергаются до HTTP.
|
|
70
|
+
|
|
71
|
+
### Лицо канала: фото, имя, ник, описание и дата рождения
|
|
72
|
+
|
|
73
|
+
1. В рабочей области канала вызови
|
|
74
|
+
`besales_file_upload_request({name:"Фото канала", file_type:"IMAGE", source:"USER_UPLOAD"})`,
|
|
75
|
+
без `owner_kind`/`owner_id`. Тип укажи явно: по умолчанию загрузчик создаёт DOCUMENT,
|
|
76
|
+
который не подходит для фото профиля. Человек загружает
|
|
77
|
+
фото по полученной ссылке; затем читай `besales_file_upload_status` с `upload_ref`
|
|
78
|
+
до `status:READY` и возьми `stored_file_id`. Файл должен быть доступен в этой области.
|
|
79
|
+
2. Покажи список номеров и новые значения; при смене ников получи ответ «делать?».
|
|
80
|
+
Вызови `besales_platform_telegram_personal_profile_bulk` одним запросом:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"channel_id": "23000000-0000-4000-8000-000000000001",
|
|
85
|
+
"all": true,
|
|
86
|
+
"stored_file_id": "26000000-0000-4000-8000-000000000001",
|
|
87
|
+
"first_name": "Анна",
|
|
88
|
+
"username_base": "anna_sales",
|
|
89
|
+
"bio": "Помогаю с заказами",
|
|
90
|
+
"birthday": { "day": 1, "month": 2 },
|
|
91
|
+
"idempotency_key": "channel-face-1"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Для части номеров замени `all:true` на `account_ids:[…]`. Одно фото уйдёт всем
|
|
96
|
+
целям того же задания. Для точных разных ников используй `usernames` вместо
|
|
97
|
+
`all`/`account_ids`/`username_base`.
|
|
98
|
+
|
|
99
|
+
3. По `job_id` читай `besales_platform_telegram_personal_profile_job` до `COMPLETED`;
|
|
100
|
+
проверь каждый номер и `outcome.fields.avatar`, а также остальные поля.
|
|
101
|
+
`PENDING` подтверждает только принятие задания.
|
|
102
|
+
|
|
103
|
+
404 `FILE_NOT_FOUND` — загрузи фото в область канала; 400 `TELEGRAM_AVATAR_INVALID`
|
|
104
|
+
или 413 `TELEGRAM_AVATAR_TOO_LARGE` — выбери корректное статичное фото до 5 МиБ;
|
|
105
|
+
409 `TELEGRAM_AVATAR_CHANGED` — выбери файл заново. Изменённое намерение требует нового
|
|
106
|
+
ключа; сетевой повтор прежнего вызова — тот же ключ и те же аргументы.
|
|
107
|
+
`PROFILE_PROVIDER_OUTCOME_UNKNOWN` требует сверки профиля, а не слепого повтора.
|
|
108
|
+
Файл чужой области закрыт 404 до фермы. Мост передаёт ID, не читает байты или секреты
|
|
109
|
+
Telegram и не возвращает подписанную ссылку S3.
|
|
69
110
|
|
|
70
111
|
Пропуск поля сохраняет прежнее значение; пустые строки и `null` передаются буквально, а
|
|
71
112
|
ложное или пустое значение выбора (`all:false`, `account_ids:[]`, `usernames:[]`) отвергается до HTTP.
|
|
@@ -84,8 +125,8 @@ Telegram неизвестен, сначала сверь профиль номе
|
|
|
84
125
|
ядро вернёт прежний ответ, профиль второй раз не изменится и суточный предел смен не тратится.
|
|
85
126
|
При неизвестном исходе новый ключ автоматически не создавай.
|
|
86
127
|
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
Для подключения номеров в существующий канал используй `…_connect_link`; план прокси
|
|
129
|
+
и защищённый ввод описаны ниже.
|
|
89
130
|
|
|
90
131
|
Ограничения, ручная жалоба и чтение её исхода описаны в
|
|
91
132
|
[документе апелляции](telegram-personal-restrictions.md). Перед жалобой покажи человеку
|
|
@@ -97,4 +138,34 @@ Telegram неизвестен, сначала сверь профиль номе
|
|
|
97
138
|
`telegram-personal-profile.spec.ts`. `scripts/telegram-personal-confirmation-mutants.mjs`
|
|
98
139
|
проверяет, что снятие обязательного согласия в Zod или опубликованной схеме ломает именованное
|
|
99
140
|
утверждение теста. `scripts/telegram-personal-profile-mutants.mjs` делает то же для запрета
|
|
100
|
-
двух режимов ников сразу (в Zod и в опубликованной схеме)
|
|
141
|
+
двух режимов ников сразу (в Zod и в опубликованной схеме), имени заголовка ключа
|
|
142
|
+
и передачи фото в patch. Схему и варианты карты выводит
|
|
143
|
+
`node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-telegram-profile-photo.mjs`;
|
|
144
|
+
опись пересчитывает `scripts/sync-tool-inventory.mjs`. Оба поддерживают `--check`.
|
|
145
|
+
|
|
146
|
+
## Прокси личного Telegram
|
|
147
|
+
|
|
148
|
+
`besales_platform_telegram_personal_proxy_import_link` выдаёт человеку одноразовую ссылку
|
|
149
|
+
до `expires_at`: логины и пароли вводятся на защищённой странице после входа, не в чате.
|
|
150
|
+
`besales_platform_telegram_personal_proxy_import_status` читает по `channel_id` и `session_id`
|
|
151
|
+
только состояние и счётчики, без строк прокси. Оба маршрута требуют `manage:channels`.
|
|
152
|
+
`besales_platform_telegram_personal_proxy_delete` удаляет свободный прокси по `proxy_id`;
|
|
153
|
+
409 `TELEGRAM_PROXY_NOT_EMPTY` — сначала переключите номера на другой прокси.
|
|
154
|
+
`besales_platform_telegram_personal_account_proxy_change` переключает номер (`account_id`)
|
|
155
|
+
на существующий `proxy_id`. Оба действия требуют `manage:channels` и явное
|
|
156
|
+
`explicit_confirmation:true`; номер при переключении переподключится.
|
|
157
|
+
Для смены обязательны стабильный `action_id` (UUID) и явный `country_change_confirmed`
|
|
158
|
+
(boolean без умолчания). Повтор — с тем же ключом и аргументами. Успех только
|
|
159
|
+
`state=SUCCEEDED`; `UNKNOWN`/`PENDING` не подтверждают исход.
|
|
160
|
+
|
|
161
|
+
Общий запас пространства и подключение номеров доступны через десять дополнительных
|
|
162
|
+
инструментов: `proxy_stock_list`, `proxy_stock_import_link`, `proxy_stock_import_status`,
|
|
163
|
+
`proxy_stock_delete`, `proxy_stock_check`, `proxy_stock_check_all`, `proxies`,
|
|
164
|
+
`proxy_take_from_stock`, `proxy_return_to_stock`, `connect_link` с общей приставкой
|
|
165
|
+
`besales_platform_telegram_personal_`. Чтение списков — `read:channels`, изменения и статус
|
|
166
|
+
импорта — `manage:channels`; проверяется свежая роль создателя ключа MCP.
|
|
167
|
+
Ссылка `connect_link` работает только с существующим каналом. План пары или страны только
|
|
168
|
+
заполняет страницу: коды и пароль 2FA вводит человек после входа. Пары сохраняют исходный
|
|
169
|
+
`proxy_id`, адрес обратно в id не разрешается. Переносы требуют стабильный `action_id` в
|
|
170
|
+
`Idempotency-Key`; `count` считает прокси, под номера выбирай `proxy_ids` по местам.
|
|
171
|
+
Подробный договор и порядок действий — [прокси личного Telegram](telegram-personal-proxies.md).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Прокси личного Telegram
|
|
2
|
+
|
|
3
|
+
Мост выдаёт ссылку для массовой загрузки, читает итог, удаляет свободный прокси и переключает
|
|
4
|
+
свой номер на существующий прокси пула. Для этих маршрутов нужен `manage:channels`.
|
|
5
|
+
`workspace_id` необязателен: используется выбранная рабочая область; `channel_id` обязателен.
|
|
6
|
+
|
|
7
|
+
`besales_platform_telegram_personal_proxy_import_link` (профиль `platform`) создаёт сессию
|
|
8
|
+
и возвращает `session_id`, `setup_page_url`, `expires_at`. Отдай ссылку человеку: он входит
|
|
9
|
+
в приложение и вставляет строки прокси с логинами и паролями на защищённой странице, не в чате.
|
|
10
|
+
Ссылка одноразовая, живёт до `expires_at`. `setup_ref` и строки прокси мост не выдаёт.
|
|
11
|
+
Выдача ссылки ещё не означает загрузку прокси; автоматически создавать новую после обрыва нельзя.
|
|
12
|
+
|
|
13
|
+
`besales_platform_telegram_personal_proxy_import_status` (профили `readonly`, `platform`)
|
|
14
|
+
принимает `channel_id`, `session_id`, возвращает `sessionId`, `status`, `expiresAt`, `summary`
|
|
15
|
+
со счётчиками `total`, `pending`, `accepted`, `duplicates`, `invalid`, `checkFailed`, `rejected`,
|
|
16
|
+
`registered`. Только эти поля проходят в ответ, без строк и endpoint. `AWAITING_INPUT` ждёт
|
|
17
|
+
ввода, `SUBMITTING`/`PROCESSING` ещё в работе, `UNKNOWN` требует повторного чтения, `COMPLETED`
|
|
18
|
+
означает завершённую обработку (см. счётчики), `EXPIRED`/`CANCELLED` — сессия закрыта.
|
|
19
|
+
|
|
20
|
+
`besales_platform_telegram_personal_proxy_delete` (профиль `platform`) принимает `channel_id`,
|
|
21
|
+
`proxy_id` и обязательное `explicit_confirmation:true`. Покажи человеку прокси и канал и получи
|
|
22
|
+
явное согласие: удаление необратимо. Ответ ядра 204 превращается в `{ "deleted": true }`.
|
|
23
|
+
409 `TELEGRAM_PROXY_NOT_EMPTY`: сначала переключите номера на другой прокси. 404
|
|
24
|
+
`TELEGRAM_PROXY_NOT_FOUND`: проверь канал и идентификатор в приложении. 503 не означает удаления.
|
|
25
|
+
|
|
26
|
+
`besales_platform_telegram_personal_account_proxy_change` (профиль `platform`) принимает
|
|
27
|
+
`channel_id`, `account_id`, `proxy_id`, `action_id` (UUID), `country_change_confirmed` (boolean)
|
|
28
|
+
и обязательное `explicit_confirmation:true`. Номер переподключится: покажи человеку номер
|
|
29
|
+
и целевой прокси и получи согласие. Смена страны — отдельное явное решение без умолчания;
|
|
30
|
+
`false` передаётся буквально и не повышается автоматически до `true`.
|
|
31
|
+
Создай `action_id` один раз на намерение, сохраняй его и аргументы при повторе.
|
|
32
|
+
Тело ядра — строго `{ actionId, proxyId, countryChangeConfirmed }`; подтверждение в него не входит.
|
|
33
|
+
Ответ — квитанция `{ message, action_id, state, code?, retry_after_ms? }`.
|
|
34
|
+
Успех только при `state=SUCCEEDED`; `UNKNOWN`/`PENDING` не подтверждают переключение,
|
|
35
|
+
`FAILED`/`EXPIRED` — отказ. Соблюдай `retry_after_ms`; новый ключ автоматически не создавай.
|
|
36
|
+
|
|
37
|
+
Все схемы строгие: лишние поля, логины, пароли, `host`, `port`, строки импорта и camelCase
|
|
38
|
+
вместо входных snake_case отвергаются до HTTP. `edit-proxy` через MCP не реализован.
|
|
39
|
+
Проверки реестра — `telegram-personal-proxies.spec.ts`, снятие подтверждения в обеих схемах
|
|
40
|
+
для удаления и переключения также включено в `scripts/telegram-personal-proxy-mutants.mjs`.
|
|
41
|
+
|
|
42
|
+
Четыре прежних инструмента сохраняют свой договор при добавлении общего запаса.
|
|
43
|
+
|
|
44
|
+
## Общий запас пространства
|
|
45
|
+
|
|
46
|
+
Запас принадлежит выбранному пространству. Один прокси обслуживает номера одного канала;
|
|
47
|
+
переносится целиком. `workspace_id` необязателен, как у остальных инструментов моста.
|
|
48
|
+
Чтение требует `read:channels`. Изменения и статус импорта требуют `manage:channels`:
|
|
49
|
+
ключ вида MCP, живой создатель и его свежая роль `CHANNEL_MANAGE`; ядро пишет аудит перед
|
|
50
|
+
вызовом фермы. Выдача ссылки подключения проверяет роль на канал и не вызывает ферму.
|
|
51
|
+
|
|
52
|
+
Все имена в таблице начинаются с `besales_platform_telegram_personal_`.
|
|
53
|
+
|
|
54
|
+
| Суффикс | Назначение | Обязательные аргументы |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `proxy_stock_list` | Список запаса | — |
|
|
57
|
+
| `proxy_stock_import_link` | Ссылка загрузки в запас | — |
|
|
58
|
+
| `proxy_stock_import_status` | Состояние и счётчики импорта | `session_id` |
|
|
59
|
+
| `proxy_stock_delete` | Удалить из запаса | `proxy_id`, `explicit_confirmation:true` |
|
|
60
|
+
| `proxy_stock_check` | Проверить один прокси запаса | `proxy_id` |
|
|
61
|
+
| `proxy_stock_check_all` | Поставить весь запас на проверку | — |
|
|
62
|
+
| `proxies` | Список прокси канала | `channel_id` |
|
|
63
|
+
| `proxy_take_from_stock` | Забрать в канал целые прокси | `channel_id`, `action_id`, один способ выбора |
|
|
64
|
+
| `proxy_return_to_stock` | Вернуть свободный прокси | `channel_id`, `proxy_id`, `action_id` |
|
|
65
|
+
| `connect_link` | Ссылка подключения в существующий канал | `channel_id` |
|
|
66
|
+
|
|
67
|
+
Читающие инструменты входят также в `readonly` (включая статус импорта); остальные — в
|
|
68
|
+
`platform`. Проверка здоровья меняет состояние и не входит в `readonly`.
|
|
69
|
+
|
|
70
|
+
У списков одинаковые фильтры: `status` (`ACTIVE`, `CHECKING`, `ERROR`, `INACTIVE`),
|
|
71
|
+
`country_code` (две заглавные буквы), `search` (до 200 знаков), `cursor` (UUID),
|
|
72
|
+
`limit` (1–100, сервер по умолчанию 50). В HTTP страна передаётся как `countryCode`.
|
|
73
|
+
`nextCursor` продолжает список; `counts`, `freeProxies`, `countryCounts`, `freeCountryCounts`
|
|
74
|
+
считаются по всему пулу без фильтров. `occupiedSlots` учитывает настоящую занятость,
|
|
75
|
+
`accountsCount` не показывает свободные места. Данные о стране — [IPinfo](https://ipinfo.io).
|
|
76
|
+
|
|
77
|
+
Ссылка импорта возвращает только `session_id`, `setup_page_url`, `expires_at`: логин и пароль
|
|
78
|
+
вводятся человеком после входа на защищённой странице, до 50 строк, 1–3 номера на прокси.
|
|
79
|
+
Статус возвращает только состояние и счётчики, без строк. Ссылки не обрезаются и не пишутся
|
|
80
|
+
в журнал. При потере ответа автоматически новую ссылку не создавай. `202 CHECKING` означает
|
|
81
|
+
ожидание проверки; `queued` у проверки всех — число поставленных в очередь, не рабочих прокси.
|
|
82
|
+
|
|
83
|
+
## Забрать и вернуть
|
|
84
|
+
|
|
85
|
+
`proxy_take_from_stock` принимает ровно одну форму выбора:
|
|
86
|
+
|
|
87
|
+
- `proxy_ids`: 1–50 разных UUID;
|
|
88
|
+
- `count`: 1–50, необязательная `country_code` ограничивает выбор страной.
|
|
89
|
+
|
|
90
|
+
`count` — число прокси, а не мест. У прокси 1–3 места (`maxAccounts`). Под N номеров сначала
|
|
91
|
+
вычти места явных пар и свободные места канала, затем выбери конкретные свободные ACTIVE
|
|
92
|
+
прокси запаса по местам. При выборе страны учитывай только её. Сумма `maxAccounts` должна
|
|
93
|
+
покрыть недостачу; если не покрывает, перенос не вызывай. Для шести номеров и двух прокси
|
|
94
|
+
по три места передай оба `proxy_ids`, не `count:6`.
|
|
95
|
+
|
|
96
|
+
Оба переноса требуют стабильный `action_id` (UUID), который идёт только заголовком
|
|
97
|
+
`Idempotency-Key`. Взятие передаёт `{proxyIds}` либо `{count,countryCode?}`; возврат — без тела.
|
|
98
|
+
Явного `explicit_confirmation` эти операции не принимают. Забрать — всё или ничего:
|
|
99
|
+
`TELEGRAM_PROXY_STOCK_NOT_ENOUGH` и `TELEGRAM_PROXY_STOCK_REFUSED` не переносят ни одного
|
|
100
|
+
прокси. После устранения причины отказа можно повторить прежний ключ. Успех с тем же ключом
|
|
101
|
+
возвращает прежний результат. `IDEMPOTENCY_REQUEST_MISMATCH` не обходи новым ключом.
|
|
102
|
+
При 503 исход неизвестен: повтори тот же ключ и аргументы, новый автоматически не создавай.
|
|
103
|
+
|
|
104
|
+
Вернуть можно только свободный прокси без номеров, броней и операций.
|
|
105
|
+
`TELEGRAM_PROXY_NOT_EMPTY` требует освободить его или дождаться операции;
|
|
106
|
+
`TELEGRAM_PROXY_ADDRESS_IN_USE` при возврате означает, что адрес уже в запасе: предложи
|
|
107
|
+
удалить этот прокси из канала. Прямого переноса между каналами нет — только через запас.
|
|
108
|
+
Удаление из запаса необратимо и требует отдельного согласия `explicit_confirmation:true`;
|
|
109
|
+
при занятой операции повтори позже. `204` подтверждает удаление, 503 его не подтверждает.
|
|
110
|
+
|
|
111
|
+
## Подключение с планом
|
|
112
|
+
|
|
113
|
+
`connect_link` требует существующий `channel_id`. Необязательный `proxy_plan` имеет ровно
|
|
114
|
+
одну форму: `{pairs:[{phone,proxy_id}]}` либо `{country_code}`. Без плана страница открывается
|
|
115
|
+
для обычного выбора. Телефоны пар — E.164, 1–50 пар, телефоны разные, один прокси не больше
|
|
116
|
+
трёх раз. Строгая проверка вложенных объектов выполняется до HTTP. В ядро идут `channelId`,
|
|
117
|
+
`proxyPlan.pairs[].proxyId` либо `proxyPlan.countryCode`; исходные идентификаторы сохраняются.
|
|
118
|
+
|
|
119
|
+
План только заполняет страницу. Ссылка ничего не бронирует, не переносит и не подключает.
|
|
120
|
+
Человек входит, проверяет пары и вводит коды Telegram и пароль 2FA на странице, не в чате.
|
|
121
|
+
Ответ содержит только `setup_page_url`, `expires_at` и инструкции, без `setup_ref` и плана.
|
|
122
|
+
Пары берут прокси канала либо запаса пространства. При старте страница проверяет наличие,
|
|
123
|
+
состояние и места; прокси запаса забираются одним атомарным вызовом со списком id.
|
|
124
|
+
|
|
125
|
+
Пара держится за исходный `proxy_id`: адрес служит только для показа, обратного разрешения
|
|
126
|
+
адреса нет. Совпадение адресов не подменяет id. Пропавший id блокирует старт до правки
|
|
127
|
+
человеком; правка адреса снимает id, удаление адреса делает строку автоматической только
|
|
128
|
+
по его решению. Явная пара молча не переназначается. Итог — `…_accounts`.
|
|
129
|
+
Для уже подключённого номера сначала забери нужный прокси из запаса, затем используй
|
|
130
|
+
`…_account_proxy_change` с отдельным подтверждением переподключения.
|
|
131
|
+
|
|
132
|
+
Проверки нового набора находятся в `telegram-proxy-stock.spec.ts`, прежнего — в
|
|
133
|
+
`telegram-personal-proxies.spec.ts`. Скрипт `scripts/telegram-personal-proxy-mutants.mjs`
|
|
134
|
+
проверяет строгий ввод, согласие, стабильные заголовки, наличие маршрута, запрет усечения,
|
|
135
|
+
две формы выбора, форму плана и обязательный канал.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Импорт партии номеров Telegram
|
|
2
|
+
|
|
3
|
+
`besales_platform_telegram_personal_session_import_link` выдаёт человеку ссылку на уже
|
|
4
|
+
существующую защищённую страницу подключения Telegram. Ввод: `channel_id` и необязательный
|
|
5
|
+
`workspace_id` (иначе используется выбранное пространство). Требуется `manage:channels`.
|
|
6
|
+
Профиль `platform`; профиль `readonly` этот инструмент не включает.
|
|
7
|
+
|
|
8
|
+
Человек входит в приложение, выбирает импорт до 50 файлов и задаёт прокси для каждого файла
|
|
9
|
+
или автоназначение. Содержимое файлов, манифест, ключи приложения и пароли остаются в браузере.
|
|
10
|
+
MCP принимает только идентификаторы; любые дополнительные поля отвергаются до HTTP.
|
|
11
|
+
Ссылка живёт 15 минут. Заказ ссылки сам по себе номера не добавляет.
|
|
12
|
+
|
|
13
|
+
## Маршрут и ответ
|
|
14
|
+
|
|
15
|
+
Используется существующий `POST /workspaces/:workspaceId/telegram-personal-setup-sessions`
|
|
16
|
+
с телом `{channelId}`. Новый маршрут ядра и новая страница не создаются.
|
|
17
|
+
Ответ моста перечисляет только `setup_page_url`, `expires_at`, `operation_id: null`,
|
|
18
|
+
`import_status_available: false` и инструкцию следующего шага. Сырой `setup_ref`, состояние
|
|
19
|
+
мастера и неизвестные поля ответа ядра отбрасываются до общей границы сериализации MCP.
|
|
20
|
+
|
|
21
|
+
Безопасного идентификатора операции импорта этот init пока не возвращает. Пофайловый статус
|
|
22
|
+
`GET /channels/:channelId/telegram-personal/session-imports/:jobId` разрешён только вошедшему
|
|
23
|
+
человеку в браузере. Мост не ослабляет этот запрет и не выдаёт `setup_ref` за идентификатор.
|
|
24
|
+
Результат каждого файла проверяется на странице; `besales_platform_telegram_personal_accounts`
|
|
25
|
+
помогает сверить принятые номера, но не объясняет отклонение каждого файла.
|
|
26
|
+
|
|
27
|
+
## Список прокси
|
|
28
|
+
|
|
29
|
+
Уже доступны `besales_platform_telegram_personal_proxy_import_link` / `_status` для канала
|
|
30
|
+
и `besales_platform_telegram_personal_proxy_stock_import_link` / `_status` для общего запаса.
|
|
31
|
+
Они возвращают настоящий `session_id`, ссылку и срок. Статусы содержат только состояние и
|
|
32
|
+
счётчики; причины по строкам остаются на защищённой странице. Пароли и строки SOCKS5
|
|
33
|
+
нельзя присылать инструментам и нельзя копировать из страницы в чат.
|
|
34
|
+
|
|
35
|
+
## Продолжение
|
|
36
|
+
|
|
37
|
+
Встроенная справка `besales://workflows/platform-setup-from-scratch`, раздел
|
|
38
|
+
«Подготовить партию номеров Telegram», ведёт по импорту, профилю, фото, Premium и кампании.
|
|
39
|
+
Для фото и Premium отдельных инструментов в текущем пакете нет; эти шаги выполняются
|
|
40
|
+
в интерфейсе приложения. Запуск кампании остаётся отдельным намерением человека.
|
|
41
|
+
|
|
42
|
+
## Проверки и генерация
|
|
43
|
+
|
|
44
|
+
`scripts/sync-telegram-session-import.mjs` выводит опубликованную схему и дополнение описи
|
|
45
|
+
чтений из исполняемого определения. Затем `scripts/sync-tool-inventory.mjs` пересчитывает
|
|
46
|
+
инвентарь, а `scripts/sync-route-map.mjs` сверяет маршрут с закреплённым контрактом ядра.
|
|
47
|
+
Каждый поддерживает `--check`; для TypeScript-импортов используется существующий
|
|
48
|
+
`src/utils/setup-sessions-loader.mjs`.
|
|
49
|
+
|
|
50
|
+
`telegram-session-import.spec.ts` проверяет опубликованную схему, HTTP, запрет секретных
|
|
51
|
+
полей, проекцию ответа в обоих форматах MCP, аннотации и отсутствие автоматического повтора
|
|
52
|
+
после отказов. Общие проверки описи, аннотаций, размеров ответов и ссылок справки также
|
|
53
|
+
включают новый инструмент.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@besales/mcp",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.9",
|
|
4
4
|
"description": "Model Context Protocol server for Besales",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/be-sales/mcp#readme",
|
|
@@ -57,15 +57,16 @@
|
|
|
57
57
|
"test": "vitest run",
|
|
58
58
|
"lint:fix": "eslint src bin scripts eslint.config.js --fix",
|
|
59
59
|
"lint:errors": "eslint src bin scripts eslint.config.js",
|
|
60
|
-
"types:gen:api": "node scripts/sync-api-contracts.mjs --types-only",
|
|
60
|
+
"types:gen:api": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-api-contracts.mjs --types-only",
|
|
61
61
|
"publish:mcp-registry": "node scripts/publish-mcp-registry.mjs",
|
|
62
|
-
"schemas:sync:agent-models": "node scripts/sync-agent-model-schemas.mjs",
|
|
63
|
-
"schemas:sync:trigger-arguments": "node scripts/sync-trigger-argument-schema.mjs",
|
|
64
|
-
"schemas:sync:wave-ids": "node scripts/sync-wave-id-schemas.mjs",
|
|
65
|
-
"schemas:sync:crm-stage-pool": "node scripts/sync-crm-stage-pool-schema.mjs",
|
|
62
|
+
"schemas:sync:agent-models": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-agent-model-schemas.mjs",
|
|
63
|
+
"schemas:sync:trigger-arguments": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-trigger-argument-schema.mjs",
|
|
64
|
+
"schemas:sync:wave-ids": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-wave-id-schemas.mjs",
|
|
65
|
+
"schemas:sync:crm-stage-pool": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-crm-stage-pool-schema.mjs",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
67
|
"lint": "eslint src bin scripts eslint.config.js",
|
|
68
|
-
"contracts:sync:api": "node scripts/sync-api-contracts.mjs"
|
|
68
|
+
"contracts:sync:api": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-api-contracts.mjs",
|
|
69
|
+
"contracts:check:api": "node --loader ./src/utils/setup-sessions-loader.mjs scripts/sync-api-contracts.mjs --check"
|
|
69
70
|
},
|
|
70
71
|
"engines": {
|
|
71
72
|
"node": ">=20.0.0"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
/** Читает тела операций снимка, включая inline-типы и ссылки на operations/components. */
|
|
4
|
+
export function bodyIdempotencyOperations(source) {
|
|
5
|
+
const file = ts.createSourceFile('api-contract.ts', source, ts.ScriptTarget.Latest, true);
|
|
6
|
+
if (file.parseDiagnostics.length) throw new Error('API_CONTRACT_PARSE_FAILED');
|
|
7
|
+
const declarations = new Map(
|
|
8
|
+
file.statements
|
|
9
|
+
.filter((node) => ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node))
|
|
10
|
+
.map((node) => [node.name.text, node]),
|
|
11
|
+
);
|
|
12
|
+
function members(node, seen = new Set()) {
|
|
13
|
+
if (!node) throw new Error('API_CONTRACT_TYPE_MISSING');
|
|
14
|
+
if (seen.has(node)) throw new Error('API_CONTRACT_TYPE_CYCLE');
|
|
15
|
+
const next = new Set([...seen, node]);
|
|
16
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
17
|
+
if (
|
|
18
|
+
node.typeName.getText(file) === 'Record' &&
|
|
19
|
+
node.typeArguments?.[0]?.kind === ts.SyntaxKind.StringKeyword &&
|
|
20
|
+
node.typeArguments?.[1]?.kind === ts.SyntaxKind.NeverKeyword
|
|
21
|
+
)
|
|
22
|
+
return [];
|
|
23
|
+
const declaration = declarations.get(node.typeName.getText(file));
|
|
24
|
+
if (!declaration) throw new Error('API_CONTRACT_REFERENCE_MISSING: ' + node.getText(file));
|
|
25
|
+
return members(declaration, next);
|
|
26
|
+
}
|
|
27
|
+
if (ts.isTypeAliasDeclaration(node) || ts.isParenthesizedTypeNode(node))
|
|
28
|
+
return members(node.type, next);
|
|
29
|
+
if (ts.isIndexedAccessTypeNode(node)) {
|
|
30
|
+
if (!ts.isLiteralTypeNode(node.indexType)) throw new Error('API_CONTRACT_INDEX_UNSUPPORTED');
|
|
31
|
+
const field = members(node.objectType, next).find(
|
|
32
|
+
(one) => one.name?.text === node.indexType.literal.text,
|
|
33
|
+
);
|
|
34
|
+
if (!field) throw new Error('API_CONTRACT_REFERENCE_MISSING: ' + node.getText(file));
|
|
35
|
+
return members(field.type, next);
|
|
36
|
+
}
|
|
37
|
+
if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node))
|
|
38
|
+
return node.types.flatMap((one) => members(one, next));
|
|
39
|
+
if (ts.isTypeLiteralNode(node) || ts.isInterfaceDeclaration(node)) return [...node.members];
|
|
40
|
+
// Основание indexed access — имя интерфейса, а не ссылка TypeReference.
|
|
41
|
+
if (ts.isIdentifier(node)) return members(declarations.get(node.text), next);
|
|
42
|
+
if (
|
|
43
|
+
ts.isArrayTypeNode(node) ||
|
|
44
|
+
ts.isLiteralTypeNode(node) ||
|
|
45
|
+
(node.kind >= ts.SyntaxKind.FirstKeyword && node.kind <= ts.SyntaxKind.LastKeyword)
|
|
46
|
+
)
|
|
47
|
+
return [];
|
|
48
|
+
throw new Error('API_CONTRACT_TYPE_UNSUPPORTED: ' + node.getText(file));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const paths = declarations.get('paths');
|
|
52
|
+
if (!paths?.members?.length) throw new Error('API_CONTRACT_PATHS_MISSING');
|
|
53
|
+
const operations = [];
|
|
54
|
+
for (const route of paths.members) {
|
|
55
|
+
for (const method of members(route.type)) {
|
|
56
|
+
if (!/^(get|post|put|patch|delete|head|options|trace)$/u.test(method.name?.text ?? ''))
|
|
57
|
+
continue;
|
|
58
|
+
const body = members(method.type).find((field) => field.name?.text === 'requestBody');
|
|
59
|
+
if (!body || body.type.kind === ts.SyntaxKind.NeverKeyword) continue;
|
|
60
|
+
const content = members(body.type).find((field) => field.name?.text === 'content');
|
|
61
|
+
if (!content) throw new Error('API_CONTRACT_BODY_CONTENT_MISSING');
|
|
62
|
+
for (const media of members(content.type)) {
|
|
63
|
+
for (const field of members(media.type)) {
|
|
64
|
+
const key = field.name?.text;
|
|
65
|
+
const description = field.getFullText(file);
|
|
66
|
+
if (
|
|
67
|
+
!/^(requestKey|idempotencyKey)$/u.test(key ?? '') &&
|
|
68
|
+
!(
|
|
69
|
+
/(?:key|token|Id)$/iu.test(key ?? '') &&
|
|
70
|
+
/@description (?:Неизменный |Обязательный |Сохранённый )?(?:Ключ (?:идемпот|намерения|повтора|операции)|UUID идемпот)/iu.test(
|
|
71
|
+
description,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
continue;
|
|
76
|
+
operations.push({
|
|
77
|
+
method: method.name.text.toUpperCase(),
|
|
78
|
+
path: route.name.text,
|
|
79
|
+
key,
|
|
80
|
+
required: !body.questionToken && !field.questionToken,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return operations;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function contractRoute(entry) {
|
|
90
|
+
return `${entry.expected.method} /api/v2${entry.expected.pathTemplate.replace(/:([A-Za-z0-9_]+)/gu, '{$1}')}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function operationRoute(operation) {
|
|
94
|
+
return `${operation.method} ${operation.path}`;
|
|
95
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { z } from 'zod';
|
|
3
|
-
import { reactivationConfigSchema } from '../
|
|
4
|
+
import { reactivationConfigSchema } from '../src/tools/definitions/follow-up-config.js';
|
|
4
5
|
|
|
5
6
|
// Только добавочные поля: остальные ограничения отгруженной поверхности остаются дословными.
|
|
6
7
|
const file = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
7
8
|
const registry = JSON.parse(readFileSync(file, 'utf8'));
|
|
8
|
-
const tool = registry.tools.find(item => item.name === 'besales_followup_sequence_upsert');
|
|
9
|
+
const tool = registry.tools.find((item) => item.name === 'besales_followup_sequence_upsert');
|
|
9
10
|
if (!tool?.inputSchema?.properties?.config?.properties) throw new Error('Нет конфигурации цепочки');
|
|
10
|
-
const derived = z.toJSONSchema(reactivationConfigSchema, {
|
|
11
|
+
const derived = z.toJSONSchema(reactivationConfigSchema, {
|
|
12
|
+
io: 'input',
|
|
13
|
+
unrepresentable: 'any',
|
|
14
|
+
});
|
|
11
15
|
for (const field of ['sessionTimeVariable', 'sessionDayOffsets', 'sessionDurationMinutes']) {
|
|
12
|
-
if (!derived.properties[field]) throw new Error('Не объявлено поле '+field);
|
|
16
|
+
if (!derived.properties[field]) throw new Error('Не объявлено поле ' + field);
|
|
13
17
|
tool.inputSchema.properties.config.properties[field] = derived.properties[field];
|
|
14
18
|
}
|
|
15
|
-
tool.description = tool.description.replace(
|
|
16
|
-
|
|
19
|
+
tool.description = tool.description.replace(
|
|
20
|
+
'непустые dealFields, серия эфиров и неисполняемые виды шагов',
|
|
21
|
+
'непустые dealFields и неисполняемые виды шагов',
|
|
22
|
+
);
|
|
23
|
+
writeGeneratedFile(file, JSON.stringify(registry, null, 2) + '\n');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// Один режим проверки для узких генераторов: проверка никогда не переписывает вход.
|
|
4
|
+
export function writeGeneratedFile(file, output) {
|
|
5
|
+
if (process.argv.includes('--check')) {
|
|
6
|
+
if (readFileSync(file, 'utf8') !== output)
|
|
7
|
+
throw new Error('GENERATED_CONTRACT_DRIFT: ' + String(file));
|
|
8
|
+
} else writeFileSync(file, output);
|
|
9
|
+
}
|