bitrix24-mcp-bridge 0.3.0 → 0.5.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/docs/api-notes.md CHANGED
@@ -1,55 +1,34 @@
1
1
  # Bitrix24 API notes (reverse-engineered)
2
2
 
3
- Справочник по API Bitrix24, добытый **живым реверсом**. Часть вызовов моста совпадает с
4
- **публичным REST** (`tasks.task.*`, `im.recent.list`, `im.user.get`, `socialnetwork.api.workgroup.*`) —
5
- по ним есть официальная документация; часть — **сугубо внутренние ajax-контроллеры**
6
- (`im.v2.*`, `tasks.v2.*`, `ui.entityselector.*`), публичных доков по которым нет, только реверс.
7
- Предназначен для любого ИИ-агента или разработчика, который вызывает эти методы через мост
8
- (`bitrix_call` / типизированные инструменты) или расширяет каталог.
9
-
10
- > **Официальные REST-доки** — [`apidocs.bitrix24.com`](https://apidocs.bitrix24.com) (исходники:
11
- > [`github.com/bitrix24/b24restdocs`](https://github.com/bitrix24/b24restdocs); агенту с MCP context7 —
12
- > библиотека `/bitrix24/b24restdocs`). Для публично-документированных методов доки применимы напрямую;
13
- > для внутренних `im.v2.*`/`tasks.v2.*` — как **справочный аналог** (конверт/регистр полей могут
14
- > отличаться, сверяй реверсом). Дальше по тексту такие отсылки помечены **📖**; иди по ним, если нужна
15
- > деталь, которой здесь нет.
16
-
17
- Наблюдения сняты на портале `example.bitrix24.ru` (облако, мессенджер **im.v2**, задачи в режиме
18
- **scrum-борда**). Имена полей и конвенции стандартны для облачного Bitrix24, но при переносе на другой
19
- портал сверяйся реверсом (`docs/reconnaissance.md`).
3
+ Как агенту работать с Bitrix24 через этот мост: формат `params`, имена полей, готовые цепочки вызовов.
4
+
5
+ Две поверхности API. **Публичный REST** (`tasks.task.*`, `im.recent.list`, `im.user.get`, `im.chat.get`,
6
+ `socialnetwork.api.workgroup.*`) — есть официальные доки, ссылки на них помечены **📖**
7
+ ([`apidocs.bitrix24.com`](https://apidocs.bitrix24.com); агенту с MCP context7 — библиотека
8
+ `/bitrix24/b24restdocs`). **Внутренние ajax-контроллеры** (`im.v2.*`, `tasks.v2.*`, `ui.entityselector.*`)
9
+ публичных доков не имеют — всё ниже добыто живым реверсом; конверт и регистр полей у них могут
10
+ отличаться от 📖-аналогов. Имена полей стандартны для облачного Bitrix24; на другом портале сверяйся
11
+ реверсом (`docs/reconnaissance.md`).
20
12
 
21
13
  ---
22
14
 
23
15
  ## 1. Как вызывать
24
16
 
25
- - **`bitrix_call { name, params }`** — универсальный вызов любого разрешённого имени из каталога
26
- (`actions.json`). `params` — нативный формат Bitrix (см. раздел 3). Каталог может включать
27
- мутирующие вызовы; allowlist (`actions.json`) — единственная граница, не режим чтения.
28
- - **Типизированные инструменты** — обёртки с разумными дефолтами над каталогом. Точная выборка — через
29
- опциональный `params`, он мержится **последним** и перекрывает дефолты. Регистрируются только те,
30
- чьё имя есть в каталоге (`actions.json`); иначе инструмент пропускается. Текущий набор:
31
- - Задачи: `bitrix_tasks_list`, `bitrix_task_get`, `bitrix_task_get_v2` (v2/JSON),
32
- `bitrix_task_scrum_info`, `bitrix_task_files`, `bitrix_task_views_count`,
33
- `bitrix_task_subtasks` (подзадачи), `bitrix_task_related` (связанные).
34
- - Проекты: `bitrix_projects_list`, `bitrix_project_get`.
35
- - Чаты: `bitrix_chats_recent`, `bitrix_recent_load` (недавние по секции — в т.ч. `tasksTask` =
36
- чаты задач), `bitrix_recent_tail` (листать недавние вглубь), `bitrix_chat_load` (открыть по
37
- `dialogId`/`chatId`), `bitrix_chat_messages`, `bitrix_chat_history` (листать вглубь по `beforeId`),
38
- `bitrix_chat_get_dialog_id` (dialogId по externalId), `bitrix_chat_mark_read` (⚠ мутирующий),
39
- `bitrix_chat_read_all` (⚠ мутирующий).
40
- - Люди/поиск: `bitrix_user_get` (карточка юзера), `bitrix_entity_selector` (загрузка селектора),
41
- `bitrix_entity_search` (текстовый поиск чатов/сущностей через `ui.entityselector.doSearch`),
42
- `bitrix_entity_chat` (chatId чата связанного объекта: задачи/группы/CRM — через `im.chat.get`).
43
-
44
- Любое имя каталога всегда доступно и напрямую через `bitrix_call { name, params }`.
45
- - **`bitrix_help`** — этот же гайд, отдаётся через MCP (инструмент + resource `bitrix://api-notes`).
46
- Источник — **этот файл** (`docs/api-notes.md`).
47
-
48
- Полный список имён каталога отдаёт `bitrix_call` в своём описании (`deps.catalog.names()`).
17
+ - **Типизированные инструменты** (`bitrix_tasks_list`, `bitrix_task_get`, `bitrix_chat_load`, …) —
18
+ обёртки с разумными дефолтами; их полный список и описания видны среди MCP-инструментов. Точная
19
+ выборка — опциональным `params`: он мержится **последним** и перекрывает дефолты.
20
+ - **`bitrix_call { name, params }`** — любое имя из каталога (`actions.json`), `params` в нативном
21
+ формате Bitrix (§3). Полный список имён отдаёт сам `bitrix_call` в своём описании.
22
+ - Каталог может содержать **мутирующие** вызовы: наличие записи = разрешение вызвать. Это не режим
23
+ «только чтение».
24
+ - У обёрток есть дефолты (напр. `bitrix_tasks_list` сортирует `{"ID":"desc"}`), у `bitrix_call` их нет —
25
+ один и тот же фильтр даст разные страницы. Сравниваешь выдачи — задавай `order` явно.
26
+ - Ответ `invalid_csrf` / `invalid_authentication` = сессия портала протухла → перелогинься в браузере
27
+ на вкладке портала.
49
28
 
50
29
  ---
51
30
 
52
- ## 2. Формат ответа (важно!)
31
+ ## 2. Формат ответа
53
32
 
54
33
  Ajax-контроллеры (`/bitrix/services/main/ajax.php`) отвечают HTTP 200 с конвертом:
55
34
 
@@ -59,217 +38,137 @@ Ajax-контроллеры (`/bitrix/services/main/ajax.php`) отвечают
59
38
  "errors": [] } // ПУСТОЙ массив при успехе! (в JS [] — truthy)
60
39
  ```
61
40
 
62
- - Успех определяется `status:"success"` и/или **пустым** `errors`. Непустой `errors` / `status:"error"` /
63
- top-level `error` — реальная ошибка (мост маппит это в `ok:false`).
64
- - REST-эндпоинты (`/rest/*.json`) отвечают иначе: `{ "result": …, "next", "total", "time" }` — без `errors`.
41
+ Успех = `status:"success"` и/или **пустой** `errors`. Непустой `errors` / `status:"error"` / top-level
42
+ `error` — реальная ошибка (мост маппит её в `ok:false`). REST-эндпоинты (`/rest/*.json`) отвечают
43
+ иначе: `{ "result": …, "next", "total", "time" }` — без `errors`.
65
44
 
66
45
  ---
67
46
 
68
- ## 3. Транспорт: form vs json (различается по методу!)
69
-
70
- У каждой записи каталога есть **`bodyType`** — как мост кодирует тело:
71
-
72
- - **`bodyType: "form"`** (по умолчанию) — `application/x-www-form-urlencoded`; вложенность кодируется
73
- PHP-стилем (`filter[STATUS]=2`, `select[0]=ID`). Объект в `params` рекурсивно разворачивается в такие
74
- ключи. Классические `tasks.task.*`, мессенджер `im.v2.*`, REST — всё это форма.
75
- - **`bodyType: "json"`** — тело уходит как `JSON.stringify(params)` с `Content-Type: application/json`.
76
- Так работают **`tasks.v2.*`** (напр. `tasks.v2.Task.get` ждёт `{"task":{"id":N}}`) и `ui.entityselector.*`.
77
- `params` при этом сохраняет вложенную структуру как есть (не разворачивается в bracket-ключи).
78
-
79
- > Историческая заметка: раньше мост умел только форму, и `tasks.v2.*` были непригодны. Теперь
80
- > поддержаны — в записи каталога ставь `"bodyType": "json"`, и мост отправит их корректно.
81
-
82
- **CSRF / sessid:** мост всегда шлёт заголовок **`X-Bitrix-Csrf-Token: <sessid>`** (свежий
83
- `BX.bitrix_sessid()`). Form-тела дополнительно несут `sessid` полем в теле (legacy REST это требует);
84
- JSON-тела несут sessid **только** в заголовке. Ответ `invalid_csrf`/`invalid_authentication` = сессия
85
- протухла (перелогинься в браузере).
86
-
87
- | Метод (action / endpoint) | bodyType | Где `select`/`filter`/`order` | Пагинация |
88
- |---|---|---|---|
89
- | `tasks.task.list` | form | **верхний уровень**: `select[]`, `filter{}`, `order{}` | `start` (сдвиг, шаг 50) |
90
- | `tasks.task.get` | form | верхний уровень: `taskId`, `select[]` | — |
91
- | `tasks.v2.Task.get` | **json** | `{"task": {"id": N}}` | — |
92
- | `tasks.v2.Scrum.getTaskInfo` | **json** | `{"taskId": N}` | — |
93
- | `tasks.v2.File.listObjects` | **json** | `{"ids": [N,…]}` | — |
94
- | `tasks.v2.Task.View.User.count` | **json** | `{"task": {"id": N}}` | — |
95
- | `tasks.v2.Task.Relation.Child.list` | **json** | `{"taskId": N, "withIds":true, "navigation":{"size":N}}` | `navigation` |
96
- | `tasks.v2.Task.Relation.Related.list` | **json** | `{"taskId": N, …}` (как Child) | `navigation` |
97
- | `socialnetwork.api.workgroup.list` | form | **верхний уровень**: `select[]`, `order{}`, `filter{}` | `start` (шаг 50; `start=(N-1)*50`, `-1` — ответ без `total`) 📖 |
98
- | `socialnetwork.api.workgroup.get` | form | **обёрнуто**: `params[groupId]`, `params[select][]` | — |
99
- | `/rest/im.recent.list.json` | form (rest) | плоско: `LIMIT`, `SKIP_OPENLINES`, `UNREAD_ONLY`, … | `LIMIT` |
100
- | `/rest/im.user.get.json` | form (rest) | плоско: `ID` | — |
101
- | `/rest/im.chat.get.json` | form (rest) | плоско: `ENTITY_TYPE`, `ENTITY_ID` | — |
102
- | `im.v2.Recent.load` | form | плоско: `limit`, `filter[recentSection]`, `filter[unread]`, `filter[parentId]` | `im.v2.Recent.tail` |
103
- | `im.v2.Recent.tail` | form | плоско: `limit`, `filter[lastMessageDate]` (курсор), `filter[recentSection]` | `filter[lastMessageDate]` |
104
- | `im.v2.Chat.load` | form | плоско: `dialogId`\|`chatId`, `messageLimit` | — |
105
- | `im.v2.Chat.getDialogId` | form | плоско: `externalId` | — |
106
- | `im.v2.Chat.Message.list` | form | плоско: `chatId`, `limit` | только последняя страница — назад **не листает**, используй `.tail` |
107
- | `im.v2.Chat.Message.tail` | form | плоско: `chatId`, `limit`, `filter[lastId]`, `order[id]` | **см. раздел 6** |
108
- | `im.v2.Chat.Message.read` | form | плоско: `chatId`, `ids[]`, `actionUuid` | — (мутирующий) |
109
- | `im.v2.Chat.readAll` | form | без параметров | — (мутирующий) |
110
- | `ui.entityselector.load` | **json** | `{"dialog": {entities,preselectedItems,…}}` | — |
111
- | `ui.entityselector.doSearch` | **json** | `{"dialog": {…}, "searchQuery": {"query": "...", "queryWords": ["..."]}}` | — |
112
-
113
- ---
47
+ ## 3. Каталог: имя → action → params
114
48
 
115
- ## 4. Каталог: имя → action → параметры
49
+ Форма `params` различается по методу: где-то плоско, где-то вложенно, где-то обёрнуто в `params[…]` —
50
+ колонка «params» показывает точную форму (кодирование тела мост берёт на себя).
116
51
 
117
- | Имя (`actions.json`) | action / endpoint | bodyType | Ключевые params |
52
+ | Имя (`bitrix_call`) | action / endpoint | params | Пагинация |
118
53
  |---|---|---|---|
119
- | `tasks.list` | `tasks.task.list` | form | `filter{RESPONSIBLE_ID,REAL_STATUS,GROUP_ID,…}`, `select[]`, `order{}`, `start` |
120
- | `task.get` | `tasks.task.get` | form | `taskId`, `select[]` |
121
- | `task.v2.get` | `tasks.v2.Task.get` | json | `task.id` (id задачи) |
122
- | `task.scrum.info` | `tasks.v2.Scrum.getTaskInfo` | json | `taskId` |
123
- | `task.files` | `tasks.v2.File.listObjects` | json | `ids` (массив id) |
124
- | `task.views.count` | `tasks.v2.Task.View.User.count` | json | `task.id` (id задачи) |
125
- | `task.subtasks` | `tasks.v2.Task.Relation.Child.list` | json | `taskId`, `navigation{size}` |
126
- | `task.related` | `tasks.v2.Task.Relation.Related.list` | json | `taskId`, `navigation{size}` |
127
- | `projects.list` | `socialnetwork.api.workgroup.list` | form | `select[]`, `order{}`, `filter{}` |
128
- | `projects.get` | `socialnetwork.api.workgroup.get` | form | `params[groupId]`, `params[select][]` |
129
- | `chats.recent` | `/rest/im.recent.list.json` | form | `LIMIT`, `UNREAD_ONLY`, `SKIP_OPENLINES` |
130
- | `recent.load` | `im.v2.Recent.load` | form | `filter[recentSection]` (`tasksTask`/`collab`/…), `limit`, `filter[unread]` |
131
- | `recent.tail` | `im.v2.Recent.tail` | form | `filter[lastMessageDate]`, `filter[recentSection]`, `limit` |
132
- | `chat.load` | `im.v2.Chat.load` | form | `dialogId`\|`chatId`, `messageLimit` |
133
- | `chat.dialogId` | `im.v2.Chat.getDialogId` | form | `externalId` (напр. `"sg"+groupId`) |
134
- | `chat.messages` | `im.v2.Chat.Message.list` | form | `chatId`, `limit` (последние; вглубь — `chat.messages.tail`) |
135
- | `chat.messages.tail` | `im.v2.Chat.Message.tail` | form | `chatId`, `filter[lastId]`, `order[id]`, `limit` |
136
- | `chat.message.read` | `im.v2.Chat.Message.read` | form | `chatId`, `ids[0]`, `actionUuid` |
137
- | `chat.read.all` | `im.v2.Chat.readAll` | form | — (⚠ мутирующий) |
138
- | `im.user.get` | `/rest/im.user.get.json` | form | `ID` |
139
- | `im.chat.get` | `/rest/im.chat.get.json` | form | `ENTITY_TYPE`, `ENTITY_ID` (напр. `TASKS_TASK`+taskId) |
140
- | `entityselector.load` | `ui.entityselector.load` | json | `dialog` (объект) |
141
- | `entityselector.search` | `ui.entityselector.doSearch` | json | `dialog`, `searchQuery{query,queryWords}` |
54
+ | `tasks.list` | `tasks.task.list` | верхний уровень: `filter{RESPONSIBLE_ID,REAL_STATUS,GROUP_ID,…}`, `select[]`, `order{}` | **`PAGEN_1`** — номер страницы (1, 2, 3…), страница = 20 задач; `start` **не работает** (вернёт ту же первую страницу) |
55
+ | `task.get` | `tasks.task.get` | верхний уровень: `taskId`, `select[]` | — |
56
+ | `task.v2.get` | `tasks.v2.Task.get` | вложенно: `{"task":{"id":N}}` | — |
57
+ | `task.scrum.info` | `tasks.v2.Scrum.getTaskInfo` | `{"taskId":N}` | — |
58
+ | `task.files` | `tasks.v2.File.listObjects` | `{"ids":[N,…]}` | — |
59
+ | `task.views.count` | `tasks.v2.Task.View.User.count` | вложенно: `{"task":{"id":N}}` | — |
60
+ | `task.subtasks` | `tasks.v2.Task.Relation.Child.list` | `{"taskId":N,"withIds":true,"navigation":{"size":N}}` | `navigation` |
61
+ | `task.related` | `tasks.v2.Task.Relation.Related.list` | как `task.subtasks` | `navigation` |
62
+ | `projects.list` | `socialnetwork.api.workgroup.list` | верхний уровень: `select[]`, `order{}`, `filter{}` | `start` (шаг 50; `start=(N-1)*50`, `-1` — ответ без `total`) 📖 |
63
+ | `projects.get` | `socialnetwork.api.workgroup.get` | **обёрнуто**: `params[groupId]`, `params[select][]` | — |
64
+ | `chats.recent` | `/rest/im.recent.list.json` | плоско: `LIMIT`, `SKIP_OPENLINES`, `UNREAD_ONLY` | `LIMIT` |
65
+ | `recent.load` | `im.v2.Recent.load` | плоско: `limit`, `filter[recentSection]` (`tasksTask`/`collab`/…), `filter[unread]`, `filter[parentId]` | `recent.tail` |
66
+ | `recent.tail` | `im.v2.Recent.tail` | плоско: `limit`, `filter[lastMessageDate]` (курсор), `filter[recentSection]` | `filter[lastMessageDate]` |
67
+ | `chat.load` | `im.v2.Chat.load` | плоско: `dialogId`\|`chatId`, `messageLimit` | — |
68
+ | `chat.dialogId` | `im.v2.Chat.getDialogId` | плоско: `externalId` (напр. `"sg"+groupId`) | — |
69
+ | `chat.messages` | `im.v2.Chat.Message.list` | плоско: `chatId`, `limit` | только последняя страница — назад **не листает**, используй `chat.messages.tail` |
70
+ | `chat.messages.tail` | `im.v2.Chat.Message.tail` | плоско: `chatId`, `limit`, `filter[lastId]`, `order[id]` | `filter[lastId]` (§5.1) |
71
+ | `chat.message.read` | `im.v2.Chat.Message.read` | плоско: `chatId`, `ids[]`, `actionUuid` | ⚠ мутирующий |
72
+ | `chat.read.all` | `im.v2.Chat.readAll` | без параметров | ⚠ мутирующий |
73
+ | `im.user.get` | `/rest/im.user.get.json` | плоско: `ID` | — |
74
+ | `im.chat.get` | `/rest/im.chat.get.json` | плоско: `ENTITY_TYPE`, `ENTITY_ID` (напр. `TASKS_TASK`+taskId) | — |
75
+ | `entityselector.load` | `ui.entityselector.load` | `{"dialog":{entities,preselectedItems,…}}` | — |
76
+ | `entityselector.search` | `ui.entityselector.doSearch` | `{"dialog":{…},"searchQuery":{"query":"…","queryWords":["…"]}}` | — |
77
+ | `call.detail` | `GET /call/detail/{callId}` | `callId` (подставляется в путь), `IFRAME=Y`, `IFRAME_TYPE=SIDE_SLIDER` → HTML | см. §5.4 |
78
+ | `task.comments.page` | `GET /task/comments/{taskId}/` | `taskId` (в путь), `IFRAME=Y`, `IFRAME_TYPE=SIDE_SLIDER` → HTML старых комментариев | см. §5.6 |
79
+ | `task.comments.navigate` | `bitrix:forum.comments` → `navigateComment` | форма: `ENTITY_XML_ID=TASK_<id>`, `EXEMPLAR_ID`, `signedParameters` (оба — из HTML первой страницы), `FILTER[<ID]` | `FILTER[<ID]` — курсор; проще `bitrix_task_comments` (§5.6) |
142
80
 
143
81
  ---
144
82
 
145
- ## 5. Справочник полей
83
+ ## 4. Справочник полей
146
84
 
147
85
  **Задача** (`tasks.task.*`) — поля для `select`/`filter` в UPPER_CASE: `ID`, `TITLE`, `DESCRIPTION`,
148
86
  `STATUS`, `REAL_STATUS`, `RESPONSIBLE_ID`, `CREATED_BY`, `CREATED_DATE`, `CHANGED_DATE`, `DEADLINE`,
149
- `CLOSED_DATE`, `PRIORITY`, `GROUP_ID`, `TAGS`, `TIME_ESTIMATE`, **`CHAT_ID`** (id im-чата обсуждения
150
- задачи — прямой резолвер `taskId → chatId`, см. §6.3), `UF_*`. Ответ ajax также подкладывает объекты
151
- `group`, `responsible`, `creator`, `action`.
152
-
153
- 📖 офиц. (`tasks.task.get` / `tasks.task.list`) — полный набор полей задачи, среди прочего: `parentId`,
154
- `stageId`, `sprintId`, `backlogId`, `commentsCount`, `serviceCommentsCount`, `timeEstimate`,
155
- `timeSpentInLogs`, `favorite`, `flowId`, `mark`, `accomplices[]`, `auditors[]`, `checklist{}`,
156
- `subStatus`. В официальном REST-ответе поля приходят **camelCase** (`groupId`, `responsibleId`), тогда
157
- как в `select`/`filter` они всегда **UPPER_CASE**; наш ajax-вариант может отдавать иначе — сверяй
158
- реверсом. Доп. вычисления включаются флагом `params{WITH_TIMER_INFO, WITH_RESULT_INFO, WITH_PARSED_DESCRIPTION}`.
159
-
160
- Статусы (`REAL_STATUS` — «настоящее» числовое состояние): `1` Новая · `2` Ждёт выполнения ·
87
+ `CLOSED_DATE`, `PRIORITY`, `GROUP_ID`, `TAGS`, `TIME_ESTIMATE`, **`CHAT_ID`** (id чата-обсуждения
88
+ задачи — прямой резолвер `taskId → chatId`, §5.2), `UF_*`. Ответ подкладывает объекты `group`,
89
+ `responsible`, `creator`, `action`.
90
+ 📖 (`tasks.task.get`/`list`) — сверх этого: `parentId`, `stageId`, `sprintId`, `backlogId`,
91
+ `commentsCount`, `timeSpentInLogs`, `favorite`, `flowId`, `mark`, `accomplices[]`, `auditors[]`,
92
+ `checklist{}`, `subStatus`; в официальном REST-ответе поля приходят **camelCase**, а в `select`/`filter`
93
+ они всегда **UPPER_CASE**. Доп. вычисления — флагами `params{WITH_TIMER_INFO, WITH_RESULT_INFO,
94
+ WITH_PARSED_DESCRIPTION}`.
95
+
96
+ **Статусы** (`REAL_STATUS` — «настоящее» числовое состояние): `1` Новая · `2` Ждёт выполнения ·
161
97
  `3` Выполняется · `4` Ждёт контроля · `5` Завершена · `6` Отложена · `7` Отклонена. Поле `STATUS`
162
98
  поверх этого несёт **мета-состояния** отображения (почти просрочена / не просмотрена / просрочена),
163
- поэтому «работает ли задача сейчас» фильтруй по **`REAL_STATUS`** (напр. `{"REAL_STATUS":3}`), а не
164
- по `STATUS` (📖 `tasks.task.list`).
99
+ поэтому «работает ли задача сейчас» фильтруй по **`REAL_STATUS`** (напр. `{"REAL_STATUS":3}`) 📖.
100
+ Асимметрия: в **`filter`** работает `REAL_STATUS`, а в **`select`** его указывать бесполезно — поле молча
101
+ не вернётся; чтобы получить состояние, проси `select:["STATUS"]` → в ответе будет ключ `status` со
102
+ значением по той же шкале 1..7 (у обёртки `bitrix_tasks_list` он уже в дефолтном `select`).
165
103
 
166
- **Фильтр `tasks.task.list`.** Перед именем поля ставится оператор: `!` (не равно / исключить),
104
+ **Фильтр `tasks.task.list`** — оператор ставится перед именем поля: `!` (не равно / исключить),
167
105
  `<`, `<=`, `>`, `>=`, `%` (LIKE-подстрока). Напр. `{"!REAL_STATUS":5}` — все **незакрытые**,
168
- `{">=DEADLINE":"2026-07-01"}` — дедлайн не раньше даты, `{"%TITLE":"карта"}` — по подстроке названия.
169
- Фильтруемые поля: `ID`, `PARENT_ID`, `GROUP_ID`, `CREATED_BY`, `RESPONSIBLE_ID`, `ACCOMPLICE`,
170
- `AUDITOR`, `REAL_STATUS`, `STATUS`, `PRIORITY`, `TAG`, `STAGE_ID`, `SPRINT_ID`, `BACKLOG_ID`,
171
- `DEADLINE`, `*_DATE`, `UF_CRM_TASK`.
106
+ `{">=DEADLINE":"2026-07-01"}`, `{"%TITLE":"карта"}`. Фильтруемые поля: `ID`, `PARENT_ID`, `GROUP_ID`,
107
+ `CREATED_BY`, `RESPONSIBLE_ID`, `ACCOMPLICE`, `AUDITOR`, `REAL_STATUS`, `STATUS`, `PRIORITY`, `TAG`,
108
+ `STAGE_ID`, `SPRINT_ID`, `BACKLOG_ID`, `DEADLINE`, `*_DATE`, `UF_CRM_TASK`.
172
109
 
173
110
  > **«Мои задачи» ≠ только `RESPONSIBLE_ID`.** Роли участника раздельны: ответственный
174
111
  > (`RESPONSIBLE_ID`), соисполнитель (`ACCOMPLICE`), наблюдатель (`AUDITOR`), постановщик
175
- > (`CREATED_BY`). Единого «любая роль» поля в `list` нет — чтобы собрать **все** свои задачи,
176
- > объедини результаты нескольких вызовов (минимум `RESPONSIBLE_ID` + `ACCOMPLICE`). Тонкость:
177
- > в **`filter`** роли в **единственном** числе (`ACCOMPLICE`/`AUDITOR`), а в **`select`** — во
178
- > **множественном** (`ACCOMPLICES`/`AUDITORS`). Не путай.
179
-
180
- > **Scrum-задачи (спринты).** Поле `SPRINT_ID` на задаче и жёсткий фильтр `filter[SPRINT_ID]`
181
- > могут расходиться: задача числится за спринтом по своему полю, но не попадает в выборку по фильтру
182
- > (вероятно, возвращена в бэклог или в иной stage). Если по фильтру «маловато» — перепроверь без него
183
- > и отфильтруй по `SPRINT_ID`/`STAGE_ID` на стороне агента. Точного API колонок scrum-доски
184
- > (`STAGE_ID` → имя колонки) в текущем каталоге нет — это кандидат на реверс.
185
-
186
- **Задача v2** (`tasks.v2.*`): id задачи передаётся вложенно — **`{"task": {"id": N}}`** (у `Task.get` и
187
- `Task.View.User.count`), а у `Scrum.getTaskInfo` и `Relation.*` — плоско `{"taskId": N}`. Это отдельная
188
- подсистема scrum-борда; структуру ответа сверь реверсом — в захвате видны только параметры запроса.
189
-
190
- **Группа/проект — список** (`workgroup.list`, camelCase в ответе): `ID`, `NAME`, `DESCRIPTION`,
191
- `NUMBER_OF_MEMBERS`, `OWNER_ID`, `DATE_CREATE`, `PROJECT` (Y/N), `TYPE`.
192
- 📖 офиц. (`socialnetwork.api.workgroup.list`) — фильтруемые поля: `ID`, `NAME`, `OWNER_ID`, `ACTIVE`,
193
- `VISIBLE`, `OPENED`, `CLOSED`, `PROJECT` (Y=проект/N=группа), `SUBJECT_ID`, `SITE_ID`, `DATE_CREATE`,
194
- `DATE_UPDATE`, `DATE_ACTIVITY`; операторы фильтра `>= > <= < % =% %= !% != !`; без `select` вернётся
195
- только `ID`. Старый публичный аналог — `sonet_group.get` (те же поля/пагинация, ключи `FILTER`/`ORDER`
196
- капсом).
112
+ > (`CREATED_BY`); единого «любая роль» поля в `list` нет — чтобы собрать **все** свои задачи, объедини
113
+ > результаты нескольких вызовов (минимум `RESPONSIBLE_ID` + `ACCOMPLICE`) и **дедуплицируй по `id`** —
114
+ > одна задача часто попадает сразу в несколько ролей. В **`filter`** роли в **единственном** числе
115
+ > (`ACCOMPLICE`/`AUDITOR`), в **`select`** — во **множественном** (`ACCOMPLICES`/`AUDITORS`).
116
+
117
+ > **Scrum-задачи (спринты).** Поле `SPRINT_ID` задачи и жёсткий `filter[SPRINT_ID]` могут расходиться:
118
+ > задача числится за спринтом по своему полю, но не попадает в выборку по фильтру (возвращена в бэклог
119
+ > или в иной stage). Если по фильтру «маловато» — перепроверь без него и отфильтруй по
120
+ > `SPRINT_ID`/`STAGE_ID` у себя. Метода «колонки scrum-доски» (`STAGE_ID` → имя колонки) в каталоге нет.
121
+
122
+ **Группа/проект — список** (`workgroup.list`): `ID`, `NAME`, `DESCRIPTION`, `NUMBER_OF_MEMBERS`,
123
+ `OWNER_ID`, `DATE_CREATE`, `PROJECT` (Y=проект/N=группа), `TYPE`; **без `select` вернётся только `ID`**.
124
+ 📖 фильтруемые: `ID`, `NAME`, `OWNER_ID`, `ACTIVE`, `VISIBLE`, `OPENED`, `CLOSED`, `PROJECT`,
125
+ `SUBJECT_ID`, `SITE_ID`, `DATE_CREATE`, `DATE_UPDATE`, `DATE_ACTIVITY`; операторы фильтра
126
+ `>= > <= < % =% %= !% != !`.
197
127
  **Группа — карточка** (`workgroup.get`): плюс `OWNER_DATA`, `SUBJECT_DATA`, `MEMBERS[]`,
198
- `MODERATOR_MEMBERS[]`, `CHAT_ID`, `DIALOG_ID`, `IMAGE_ID`, UF-поля.
128
+ `MODERATOR_MEMBERS[]`, **`CHAT_ID`**, `DIALOG_ID`, `IMAGE_ID`, UF-поля.
199
129
 
200
130
  **Недавний чат** (`im.recent.list`): `id` (это dialogId: число = юзер, `"chat"+N` = чат), `chat_id`,
201
- `type` (`user`/`chat`), `title`, `message{id,text,file,author_id,date,status,uuid}`, `counter` (число
202
- непрочитанных), `unread` (bool), `last_id`, `pinned`, `date_update`, `date_last_activity`, `user{…}`,
203
- `chat{… entity_type, entity_id, owner, …}`. Есть ли ещё страницы — флаги `hasMore`/`hasMorePages`
204
- (📖 `im.recent.list`).
205
- **Недавние по секции** (`im.v2.Recent.load`): фильтр `filter[recentSection]` — `default` (обычные
206
- диалоги), **`tasksTask` (чаты задач)**, `collab`/`collabDefault` (коллабы). Листание вглубь —
207
- `im.v2.Recent.tail` с курсором `filter[lastMessageDate]` (ISO-дата последнего элемента страницы).
131
+ `type` (`user`/`chat`), `title`, `message{id,text,file,author_id,date,status}`, `counter` (непрочитанных),
132
+ `unread`, `last_id`, `pinned`, `date_update`, `date_last_activity`, `user{…}`,
133
+ `chat{… entity_type, entity_id, owner, …}`; есть ли ещё страницы — `hasMore`/`hasMorePages` 📖.
134
+ **Недавние по секции** (`im.v2.Recent.load`): `filter[recentSection]` — `default` (обычные диалоги),
135
+ **`tasksTask` (чаты задач)**, `collab`/`collabDefault`. Вглубь — `im.v2.Recent.tail` с курсором
136
+ `filter[lastMessageDate]` (ISO-дата последнего элемента страницы).
137
+
208
138
  **Карточка юзера** (`im.user.get`): `id`, `name`, `first_name`, `last_name`, `avatar`, `work_position`,
209
139
  `gender`, `status`, `online` — резолв автора сообщения (`authorId`) в имя.
210
- **Сообщение** (`im.v2.Chat.Message.*`): `id`, `chatId`, `authorId`, `date`, `text`, `params`, `viewed`.
211
- Ответ также несёт `users[]` (участники), `additionalMessages[]`, `hasPrevPage`/`hasNextPage`.
212
- `chatId` берётся из `im.recent.list` (`chat_id`) или из `workgroup.get` (`CHAT_ID`/`DIALOG_ID`).
213
-
214
- **Адресация чата (`chat.load`): `dialogId` vs `chatId`.** `chat.load` принимает любой из двух:
215
- - `dialogId` = **ID пользователя** (число, напр. `11`) → **личный чат 1-на-1** с этим пользователем.
216
- - `dialogId` = **`"chat"+CHAT_ID`** (напр. `"chat7111"`) → **групповой чат** (проекта/задачи/канала).
217
- - `chatId` = числовой `CHAT_ID` — то же, что групповой `dialogId`, но без префикса.
218
140
 
219
- 📖 офиц. формат `DIALOG_ID` (`im.dialog.get`): `XXX` — личный (userId), `chatXXX` — чат, **`sgXXX`** —
220
- чат соцгруппы/проекта напрямую (напр. `sg15`). В публичном REST это единый параметр; наш внутренний
221
- `im.v2.Chat.load` надёжно принимает `dialogId`/`chatId`, а форму `sgXXX` для него сверь реверсом —
222
- гарантированный путь к чату группы это `bitrix_chat_get_dialog_id { externalId:"sg<groupId>" }`.
223
-
224
- Ответ `chat.load` возвращает числовой `chatId` — используй его дальше для `chat.messages` /
225
- `chat.messages.tail` (листание вглубь) и `chat.message.read` (эти три работают по `chatId`, не по `dialogId`).
141
+ **Сообщение** (`im.v2.Chat.Message.*`): `id`, `chatId`, `authorId`, `date`, `text`, `params`, `viewed`;
142
+ ответ также несёт `users[]` (участники), `additionalMessages[]`, `hasPrevPage`/`hasNextPage`.
226
143
 
227
144
  ---
228
145
 
229
- ## 6. Примеры вызовов
146
+ ## 5. Сценарии
230
147
 
231
148
  ```jsonc
232
149
  // открытые задачи ответственного 55, по дедлайну, вторая страница
233
150
  bitrix_tasks_list { "params": { "filter": {"RESPONSIBLE_ID":55,"REAL_STATUS":2},
234
151
  "order": {"DEADLINE":"asc"}, "start": 50 } }
235
152
 
236
- // карточка задачи с расширенным набором полей
237
- bitrix_task_get { "taskId": 4229, "params": { "select": ["ID","TITLE","DESCRIPTION","TAGS","TIME_ESTIMATE"] } }
238
-
239
- // карточка задачи через v2-подсистему (JSON-тело; id вложен в task)
153
+ // карточка задачи с расширенным набором полей; v2-подсистема; файлы, просмотры, связи
154
+ bitrix_task_get { "taskId": 4229, "params": { "select": ["ID","TITLE","DESCRIPTION","TAGS"] } }
240
155
  bitrix_task_get_v2 { "taskId": 4229 } // → { "task": { "id": 4229 } }
241
- bitrix_call { "name": "task.v2.get", "params": { "task": { "id": 4229 } } }
242
-
243
- // файлы задачи и счётчик просмотров (v2, JSON-тело); подзадачи/связанные
244
156
  bitrix_call { "name": "task.files", "params": { "ids": [4229] } }
245
- bitrix_call { "name": "task.views.count", "params": { "task": { "id": 4229 } } }
246
157
  bitrix_task_subtasks { "taskId": 4229 }
247
158
  bitrix_task_related { "taskId": 4229 }
248
159
 
249
- // проекты по дате создания; полная карточка группы
160
+ // проекты по дате создания; полная карточка группы (отдаёт CHAT_ID / DIALOG_ID)
250
161
  bitrix_projects_list { "params": { "order": {"DATE_CREATE":"desc"} } }
251
- bitrix_project_get { "groupId": 15 }
252
-
253
- // открыть чат (первые сообщения)
254
- bitrix_chat_load { "chatId": 485, "messageLimit": 50 }
255
-
256
- // ЛИСТАНИЕ ИСТОРИИ ВГЛУБЬ (старые сообщения): beforeId = минимальный id сообщения
257
- // из текущей страницы. Повторяй, уменьшая beforeId, до начала истории (order DESC — дефолт).
258
- bitrix_chat_history { "chatId": 485, "beforeId": 1861279 }
259
-
260
- // пометить сообщения прочитанными (МУТИРУЮЩИЙ). actionUuid генерируется автоматически.
261
- bitrix_chat_mark_read { "chatId": 40271, "ids": [1884131] }
162
+ bitrix_project_get { "groupId": 15 }
262
163
  ```
263
164
 
264
- ### 6.1. Мессенджер (IM): модель адресации и как получить `chatId`
165
+ ### 5.1. Мессенджер (IM): как получить `chatId`
265
166
 
266
- **IM** (`im.v2`) — встроенный мессенджер Bitrix24. Всё общение — это **чаты**: личные
267
- 1-на-1, групповые (проектов, каналов) и **чаты-обсуждения задач**. У каждого чата есть числовой
268
- **`chatId`** — это ключ ко всем операциям с сообщениями (`bitrix_chat_messages`,
269
- `bitrix_chat_history`, `bitrix_chat_mark_read`). Прежде чем читать сообщения, нужно **получить
270
- `chatId`** — почти любая ошибка агента здесь именно в этом шаге.
167
+ Всё общение — **чаты**: личные 1-на-1, групповые (проектов, каналов) и **чаты-обсуждения задач**.
168
+ Числовой **`chatId`** — ключ ко всем операциям с сообщениями (`bitrix_chat_messages`,
169
+ `bitrix_chat_history`, `bitrix_chat_mark_read`); почти любая ошибка агента здесь — на шаге его получения.
271
170
 
272
- **Три идентификатора одного чата** (не путай — годятся для разных операций):
171
+ **Три идентификатора одного чата** (годятся для разных операций):
273
172
 
274
173
  | Идентификатор | Что это | Пример | Куда передавать |
275
174
  |---|---|---|---|
@@ -277,173 +176,197 @@ bitrix_chat_mark_read { "chatId": 40271, "ids": [1884131] }
277
176
  | `dialogId` | адрес для ОТКРЫТИЯ: `userId` (личный 1-на-1) **или** `"chat"+chatId` (групповой) | `11`, `"chat485"` | `bitrix_chat_load` |
278
177
  | `externalId` | внешний ключ сущности → резолвится в `dialogId` | `"sg15"` (соцгруппа 15) | `bitrix_chat_get_dialog_id` |
279
178
 
280
- Ключевое: **личный чат с человеком = его `userId` в роли `dialogId`** (отдельного «id чата» у
281
- 1-на-1 знать не нужно). `bitrix_chat_load` принимает `dialogId` **или** `chatId` и всегда
282
- **возвращает числовой `chatId`** — дальше оперируешь им.
179
+ Ключевое: **личный чат с человеком = его `userId` в роли `dialogId`** (отдельного «id чата» у 1-на-1
180
+ знать не нужно). `bitrix_chat_load` принимает `dialogId` **или** `chatId` и всегда **возвращает
181
+ числовой `chatId`** — дальше оперируешь им.
283
182
 
284
- **Как получить `chatId` — по цели (дерево решений):**
183
+ **Как получить `chatId` — по цели:**
285
184
 
286
185
  | Хочу открыть чат… | Шаги |
287
186
  |---|---|
288
- | **с пользователем по ИМЕНИ** | `bitrix_entity_search { query:"дмитрий" }` → вернёт диалог с `dialogId` = его userId → `bitrix_chat_load { dialogId }` |
187
+ | **с пользователем по ИМЕНИ** | `bitrix_entity_search { query:"дмитрий" }` → диалог с `dialogId` = его userId → `bitrix_chat_load { dialogId }` |
289
188
  | **с пользователем, id известен** | `bitrix_chat_load { dialogId: <userId> }` (userId — из задачи `RESPONSIBLE_ID`/`CREATED_BY`, из `bitrix_chats_recent`, из `bitrix_user_get`) |
290
- | **обсуждение ЗАДАЧИ** | `bitrix_recent_load { section:"tasksTask" }` **или** `bitrix_entity_search { query:"<название>", section:"tasksTask" }` → `chatId`/`dialogId` → `bitrix_chat_load` (§6.3) |
189
+ | **обсуждение ЗАДАЧИ** | `bitrix_task_get` → `CHAT_ID` (§5.2) |
291
190
  | **ПРОЕКТА/группы** | `bitrix_task_get`→`GROUP_ID`→`bitrix_project_get`→`CHAT_ID`; либо `bitrix_chat_get_dialog_id { externalId:"sg<groupId>" }` |
292
- | **не знаю точно, какой** | `bitrix_chats_recent` (недавние, сопоставь по `title`/`user`) или `bitrix_entity_search { query }` (поиск по названию) |
191
+ | **не знаю точно, какой** | `bitrix_chats_recent` (недавние, сопоставь по `title`/`user`) или `bitrix_entity_search { query }` |
293
192
 
294
193
  > Почему не «просто список чатов»: `bitrix_chats_recent` (`im.recent.list`) отдаёт лишь недавние и
295
194
  > **без чатов задач**. Для поиска конкретного чата — `bitrix_entity_search`; для чатов задач —
296
195
  > `bitrix_recent_load { section:"tasksTask" }`.
297
196
 
298
- 📖 Публичные аналоги чтения сообщений (если нужно глубже разобраться в модели): `im.dialog.messages.get`
299
- — по умолчанию отдаёт последние (дефолт 20, макс 50), листание `LAST_ID` (старее) / `FIRST_ID` (новее);
300
- `imbot.v2 chat.getMessageContext` — ответ `messages[]` (oldest→newest) + `users[]` +
301
- `hasPrevPage`/`hasNextPage`, что **совпадает по форме** с нашими внутренними `im.v2.Chat.Message.*`.
302
-
303
- ### 6.2. Сценарий: чат с конкретным пользователем
304
-
305
- Личный чат 1-на-1 адресуется `dialogId` = **ID пользователя**. Id берётся из задачи
306
- (`RESPONSIBLE_ID`/`CREATED_BY`), из `bitrix_chats_recent`, либо резолвится поиском по имени (§6.4).
197
+ **Читать историю.** `bitrix_chat_messages` отдаёт только последнюю страницу; вглубь листает
198
+ `bitrix_chat_history` по курсору `beforeId` = минимальный id сообщения текущей страницы (повторяй,
199
+ уменьшая `beforeId`, до начала истории).
307
200
 
308
201
  ```jsonc
309
- // 1) открыть личный чат с пользователем 11 → в ответе будет числовой chatId
310
- bitrix_chat_load { "dialogId": 11 }
311
- // 2) листать его историю вглубь по chatId из ответа шага 1
202
+ bitrix_chat_load { "dialogId": 11 } // личный чат юзера 11 → в ответе chatId
312
203
  bitrix_chat_history { "chatId": 485, "beforeId": 1861279 }
204
+ bitrix_chat_mark_read { "chatId": 40271, "ids": [1884131] } // ⚠ мутирующий; actionUuid — автоматом
313
205
  ```
314
206
 
315
- ### 6.3. Сценарий: обсуждение (чат) конкретной задачи
207
+ 📖 Публичные аналоги чтения сообщений: `im.dialog.messages.get` (дефолт 20, макс 50; листание
208
+ `LAST_ID` — старее, `FIRST_ID` — новее); `imbot.v2 chat.getMessageContext` — ответ `messages[]`
209
+ (oldest→newest) + `users[]` + `hasPrevPage`/`hasNextPage`, по форме **совпадает** с внутренними
210
+ `im.v2.Chat.Message.*`.
316
211
 
317
- У каждой задачи есть свой im-чат-обсуждение. Самый надёжный путь — прямой резолв из карточки задачи;
318
- поиск по названию — запасной.
212
+ ### 5.2. Обсуждение (чат) конкретной задачи
319
213
 
320
- **А. Прямой — `CHAT_ID` из карточки задачи (предпочтительно).** `bitrix_task_get` возвращает `CHAT_ID`
321
- чата-обсуждения (он входит в дефолтный `select`). Это прямой `taskId → chatId` **без** поиска по
322
- названию и без риска промаха по тёзкам-заголовкам:
214
+ **А. Прямой резолв — предпочтительно.** `bitrix_task_get` возвращает `CHAT_ID` чата-обсуждения (входит
215
+ в дефолтный `select` 📖) — это `taskId → chatId` без поиска по названию и без промаха по тёзкам:
323
216
 
324
217
  ```jsonc
325
- bitrix_task_get { "taskId": 28373 } // в ответе CHAT_ID = id чата обсуждения
218
+ bitrix_task_get { "taskId": 28373 } // в ответе CHAT_ID
326
219
  bitrix_chat_load { "chatId": <CHAT_ID> }
327
220
  bitrix_chat_history { "chatId": <CHAT_ID>, "beforeId": <мин id страницы> }
328
221
  ```
329
222
 
330
- 📖 офиц.: в `tasks.task.get` поле `CHAT_ID` отдаётся по умолчанию (tasks-new.md). Общий
331
- резолвер-альтернатива — **`bitrix_entity_chat`** (обёртка над `im.chat.get`): по паре
332
- `entityType`/`entityId` отдаёт chatId связанного объекта — для задачи `entityType="TASKS_TASK"`,
333
- `entityId=<taskId>`; так же адресуются чаты `SONET_GROUP` (группа/проект), `CRM`, `CALENDAR`, `MAIL`,
334
- `VIDEOCONF`, `CALL`:
223
+ Общая альтернатива — **`bitrix_entity_chat`** (обёртка над `im.chat.get`): chatId любого связанного
224
+ объекта по паре `entityType`/`entityId`; кроме `TASKS_TASK` так же адресуются `SONET_GROUP`
225
+ (группа/проект), `CRM`, `CALENDAR`, `MAIL`, `VIDEOCONF`, `CALL`:
335
226
 
336
227
  ```jsonc
337
228
  bitrix_entity_chat { "entityType": "TASKS_TASK", "entityId": 28373 } // → chatId
338
- bitrix_chat_load { "chatId": <chatId> }
339
229
  ```
340
230
 
341
- **Б. Через список чатов задач.** `im.v2.Recent.load` с секцией `tasksTask` отдаёт чаты задач
342
- (в `bitrix_chats_recent` / `/rest/im.recent.list` их НЕТ). Листать вглубь — `bitrix_recent_tail`.
231
+ **Б. Через список чатов задач** (секция `tasksTask`; в `bitrix_chats_recent` их НЕТ):
343
232
 
344
233
  ```jsonc
345
234
  bitrix_recent_load { "section": "tasksTask" } // → чаты задач с chatId/dialogId
346
235
  bitrix_recent_tail { "section": "tasksTask", "lastMessageDate": "2026-06-29T17:25:38+03:00" }
347
- // открыть найденный чат и листать историю
348
- bitrix_chat_load { "dialogId": "chat38849" } // или { "chatId": 38849 }
349
- bitrix_chat_history { "chatId": 38849, "beforeId": 1722353 }
236
+ bitrix_chat_load { "dialogId": "chat38849" } // или { "chatId": 38849 }
350
237
  ```
351
238
 
352
- **В. Поиском по названию задачи** — `bitrix_entity_search` с секцией `tasksTask` (запасной, матч по тексту):
239
+ **Чат пуст, а `COMMENTS_COUNT > 0`** — задача старше чатов задач: в чате только системное сообщение
240
+ «Чтобы прочитать комментарии, которые ранее оставили участники задачи…», а само обсуждение лежит в
241
+ форуме → `bitrix_task_comments` (§5.6).
353
242
 
354
- ```jsonc
355
- bitrix_entity_search { "query": "трекер", "section": "tasksTask" }
356
- ```
243
+ **В. Поиском по названию задачи** (запасной, матч по тексту):
244
+ `bitrix_entity_search { "query": "трекер", "section": "tasksTask" }`
357
245
 
358
246
  **Чат ПРОЕКТА задачи** (обсуждение группы, не самой задачи) — через `GROUP_ID`:
359
247
 
360
248
  ```jsonc
361
249
  bitrix_task_get { "taskId": 4229, "params": { "select": ["ID","GROUP_ID"] } }
362
250
  bitrix_project_get { "groupId": 15 } // отдаёт CHAT_ID / DIALOG_ID
363
- bitrix_chat_load { "dialogId": "chat7111" }
364
251
  bitrix_chat_get_dialog_id { "externalId": "sg15" } // либо резолв dialogId соцгруппы напрямую
365
252
  ```
366
253
 
367
- ### 6.4. Сценарий: поиск чата / пользователя
254
+ ### 5.3. Поиск чата / пользователя
368
255
 
369
- Текстовый поиск — `bitrix_entity_search` (обёртка над `ui.entityselector.doSearch`): сам собирает
370
- диалог `IM_CHAT_SEARCH` и `searchQuery` из строки. `ui.entityselector.*` — **внутренний UI-метод, в
371
- публичных доках его нет** (форма запроса/ответа — только реверс); публичные аналоги для поиска людей —
372
- `user.get`/`user.search`, для чатов — `im.recent.list` + фильтрация на стороне агента.
256
+ `bitrix_entity_search` (обёртка над внутренним `ui.entityselector.doSearch`) сам собирает диалог
257
+ `IM_CHAT_SEARCH` и `searchQuery` из строки. Публичные аналоги: люди — `user.get`/`user.search`, чаты —
258
+ `im.recent.list` + фильтрация у себя.
373
259
 
374
260
  ```jsonc
375
261
  bitrix_entity_search { "query": "дмитрий" } // среди всех чатов/диалогов
376
262
  bitrix_entity_search { "query": "трекер", "section": "tasksTask" } // среди чатов задач
263
+ bitrix_user_get { "userId": 11 } // резолв authorId → имя
377
264
  ```
378
265
 
379
- Быстрый путь без поиска — недавние чаты и фильтрация на стороне агента; карточка автора по id:
266
+ ### 5.4. Созвон: резюме, расшифровка, запись
267
+
268
+ AI-анализ звонка (BitrixGPT Follow-Up) живёт на серверно отрендеренной странице `/call/detail/<callId>`
269
+ — JSON-эндпоинта под ней нет, публичного REST для видеовстреч тоже (📖 только телефония `voximplant.*`
270
+ и `crm.activity.call.getTranscript`). Мост читает эту страницу и разбирает её в нормализованный JSON.
271
+
272
+ **1) Найти звонки.** Серверного поиска звонков по порталу нет — ищем в конкретном чате: звонок
273
+ оставляет системное сообщение с `params.COMPONENT_PARAMS = { messageType: "START", callId }`.
380
274
 
381
275
  ```jsonc
382
- bitrix_chats_recent { } // сопоставь по title / user
383
- bitrix_recent_load { "section": "tasksTask" }
384
- bitrix_user_get { "userId": 11 } // резолв authorId → имя
276
+ bitrix_chat_calls { "chatId": 9876 } // чат проекта: bitrix_project_get → CHAT_ID
277
+ bitrix_chat_calls { "chatId": 9876, "maxPages": 20, "beforeId": 5001 } // продолжить вглубь
385
278
  ```
386
279
 
387
- ### 6.5. Легаси-комментарии задачи (HTML-поддомен — пока НЕ в мосте)
280
+ Ответ: `calls[] { callId, startedAt, startedBy, startMessageId, events[], summaryMessageId }` плюс
281
+ `scannedPages` / `oldestScannedMessageId` / **`reachedHistoryStart`** — если он `false`, история
282
+ дочитана НЕ до конца, продолжай с `beforeId: <oldestScannedMessageId>`.
388
283
 
389
- У задач два независимых фида обсуждения:
390
- - **im.v2-чат** — структурный JSON, читается уже сейчас (§6.3); обычно этого достаточно.
391
- - **Форумные легаси-комментарии** — рендерятся как **HTML** в side-slider'е. Это отдельный
392
- «HTML-поддомен» внутреннего API, который **текущий мост не поддерживает** (блокеры ниже).
284
+ **2) Взять всё по звонку.**
393
285
 
394
- **Как грузятся (реверс).** Первая страница — GET HTML-страницы iframe (сервер тут же минтит подпись):
395
-
396
- ```
397
- GET /task/comments/<taskId>/?IFRAME=Y&IFRAME_TYPE=SIDE_SLIDER → HTML (комментарии + signedParameters)
286
+ ```jsonc
287
+ bitrix_call_detail { "callId": 4242 } // всё; длинная расшифровка уедет в файл
288
+ bitrix_call_detail { "callId": 4242, "transcript": "inline" } // расшифровка прямо в ответе
289
+ bitrix_call_detail { "callId": 4242, "transcript": "none" } // расшифровка не нужна вовсе
290
+ bitrix_call_detail { "callId": 4242, "transcript": "file" } // всегда в файл, даже короткая
398
291
  ```
399
292
 
400
- Листание — POST компонентного ajax (форма, отдаёт HTML-фрагмент):
293
+ | Поле ответа | Что внутри |
294
+ |---|---|
295
+ | `title`, `agenda`, `meetingType` | тема, вступление, тип встречи («Статус-встреча») |
296
+ | `date`, `interval`, `duration` | «23 июля, 15:46», «15:46 - 16:51», «1 ч 4 мин» |
297
+ | `efficiency`, `qualityChecklist[]` | оценка встречи в % и критерии `{ ok, text }` |
298
+ | `participants[]` | `{ id, name, talkTimePercent, talkTime, efficiency, metrics[], insight }` |
299
+ | `overview`, `chapters[]` | общее резюме и главы `{ from, to, title, text }` с таймкодами |
300
+ | `decisions[]`, `tasks[]` | «что решили» и задачи `{ assigneeId, assignee, text }` |
301
+ | `transcript[]`, `transcriptCount` | реплики `{ from, to, speakerId, speaker, text }` |
302
+ | `recording` | `{ path, url, trackId }` — аудиозапись, качается `bitrix_call_recording` (§5.5) |
303
+
304
+ **Расшифровка длинного звонка не приходит в ответе.** У часовой встречи она даёт ~70 КБ из 80 КБ, поэтому
305
+ свыше ~20 КБ (режим `"auto"` = умолчание) пишется на диск, а в ответе остаются `transcript: []`,
306
+ `transcriptCount` и **`files`**: `files.transcript` — `call-<id>-transcript.txt`, где строка = реплика
307
+ (`[11:10—11:24] Генрих Богацкий: …`), её удобно **грепать** (найденная строка сама несёт таймкод и
308
+ спикера); `files.json` — полный ответ с расшифровкой. Оба файла — кеш звонка, перезаписываются.
309
+
310
+ Тонкости:
311
+ - **`speakerId`** резолвится по имени из таблицы анализа; кто говорил мало и в неё не попал — получит
312
+ `speakerId: null`, но останется в `participants` с `id: null`.
313
+ - **`assigneeId` задачи** — упомянутый в тексте пользователь, а НЕ id смотрящего.
314
+ - Нет доступа к звонку / протухла сессия → инструмент вернёт внятную ошибку (портал отдаёт HTML логина).
315
+
316
+ ### 5.5. Скачать файл (вложение чата, файл задачи, запись звонка)
317
+
318
+ Ссылки портал минтит и подписывает сам, но подпись НЕ заменяет авторизацию — качает расширение в
319
+ сессии пользователя.
401
320
 
402
- ```
403
- POST /bitrix/services/main/ajax.php?mode=class&c=bitrix:forum.comments&action=navigateComment
404
- form: AJAX_POST=Y, ENTITY_XML_ID=TASK_<taskId>, taskId=<taskId>, MODE=LIST,
405
- FILTER[<ID]=<курсор: id, ДО которого грузить старые>, PAGEN_1=1,
406
- signedParameters=<подписанный blob>, IFRAME=Y, IFRAME_TYPE=SIDE_SLIDER
407
- headers: bx-ajax: true, x-bitrix-site-id: s1 (плюс обычный X-Bitrix-Csrf-Token)
321
+ ```jsonc
322
+ // вложение чата: ссылку берём из files[].urlDownload в ответе bitrix_chat_load
323
+ bitrix_file_download { "url": "https://<портал>/bitrix/services/main/ajax.php?action=disk.api.file.download&…" }
324
+ bitrix_call_recording { "callId": 4242 } // аудиозапись созвона (ссылку найдёт сам)
325
+ bitrix_file_download { "url": "…", "savePath": "/tmp/photo.png", "overwrite": true }
408
326
  ```
409
327
 
410
- `signedParameters` — base64 PHP-массива + HMAC-подпись сервера; кодирует
411
- `FORUM_ID, ENTITY_TYPE=TK, ENTITY_ID=<taskId>, ENTITY_XML_ID=TASK_<taskId>, …`. **Подделать нельзя** —
412
- берётся из HTML первой страницы.
328
+ Ответ — **путь, а не содержимое**: `{ path, bytes, contentType, fileName }` (гигабайты не попадают в
329
+ контекст). По умолчанию файл ложится в `~/.bitrix24-mcp-bridge/downloads/`; `savePath` без ведущего `/`
330
+ трактуется как имя внутри этой папки. Потолок — 512 МБ.
413
331
 
414
- **Почему пока не в каталоге (3 блокера):**
415
- 1. Мост парсит ответ только как JSON (`extension/src/connector.ts` → `resp.json()`) — HTML бросит исключение.
416
- 2. Path-параметр в URL (`/task/comments/<taskId>/`) — записи каталога статичны, шаблонов пути нет.
417
- 3. `signedParameters` + заголовки `bx-ajax`/`x-bitrix-site-id` — нужен предварительный GET HTML-страницы.
332
+ Тонкости:
333
+ - **Граница безопасности — origin**: скачивается только с origin сконфигурированного портала.
334
+ - **Имя даёт портал** (`Content-Disposition`), поэтому без `savePath` угадывать его не нужно.
335
+ - **Повторное скачивание** того же файла падает с `already exists` — передай `overwrite: true`.
418
336
 
419
- **Чтобы подключить** (будущая работа): научить мост возвращать text/HTML при не-JSON ответе и
420
- поддержать path-шаблон эндпоинта; затем обёртка `bitrix_task_comments { taskId }` (GET первой
421
- страницы), а листание — через `navigateComment` с выпарсенным `signedParameters`.
337
+ ### 5.6. Старые комментарии задачи (форум)
338
+
339
+ До перехода на чаты задач обсуждение жило в форуме задачи, и у старых задач оно там и осталось: в
340
+ чате (`CHAT_ID`) только системная заглушка со ссылкой `/task/comments/<id>/`. Признак — в
341
+ `bitrix_task_get` поле `COMMENTS_COUNT > 0` (через `params.select`; `FORUM_TOPIC_ID` тоже заполнен),
342
+ а чат пуст. JSON-метода чтения нет — мост разбирает HTML-страницу комментариев.
343
+
344
+ ```jsonc
345
+ bitrix_task_comments { "taskId": 1234 } // всё обсуждение; длинное уедет в файл
346
+ bitrix_task_comments { "taskId": 1234, "limit": 10 } // только 10 последних
347
+ bitrix_task_comments { "taskId": 1234, "output": "inline" } // всегда в ответе
348
+ bitrix_task_comments { "taskId": 1234, "beforeId": 5678 } // продолжить вглубь от id
349
+ ```
422
350
 
423
- > Тот же HTML-поддомен: **детали звонка** — `GET /call/detail/<callId>?IFRAME=Y&IFRAME_TYPE=SIDE_SLIDER`
424
- > (отдаёт HTML). Ограничения идентичны.
351
+ Ответ: `comments[]` по порядку (старые → новые) — `{ id, authorId, author, date, dateIso, text, isNew,
352
+ system, files[] }`, плюс `total`, `oldestId`, `scannedPages` и **`reachedHistoryStart`**: если он
353
+ `false` (упёрлись в `maxPages`, умолчание 10, или в `limit`), продолжай с `beforeId: <oldestId>`.
354
+
355
+ - **`system: true`** — служебные записи («вы назначены исполнителем», смена срока, пинги), не слова людей.
356
+ - **`date`** — как на портале («9 марта 2024 08:15»), **`dateIso`** — то же в `2024-03-09T08:15`
357
+ (местное время портала; для «вчера, 12:00» и дат без года — `null`).
358
+ - **`files[]`** — `{ attachedId, name, size, url }`; `url` скармливай `bitrix_file_download` (§5.5).
359
+ - **`text`** — без разметки; у ссылки с подписью адрес дописан в скобках: «макет (https://…)».
360
+ - **Длинное обсуждение не приходит в ответе** (как расшифровка звонка, §5.4): свыше ~20 КБ (`output:
361
+ "auto"` = умолчание) в ответе `comments: []` и `files`: `files.text` — `task-<id>-comments.txt`,
362
+ строка = комментарий (`#5678 [2024-03-09T08:15] Имя (id 101): текст`, переносы — ` ⏎ `), её удобно
363
+ грепать; `files.json` — полный ответ. `output: "file"` — всегда в файл.
364
+ - **Прочитанным ничего не помечается.** Портал отмечает комментарии отдельным действием
365
+ `readComment`, которое шлёт JS страницы при прокрутке; мост JS не исполняет. Проверено:
366
+ `viewedDate` задачи после чтения не меняется. `isNew: true` — комментарий не прочитан пользователем.
367
+ - Нет доступа к задаче / протухла сессия → внятная ошибка (портал отдаёт HTML логина).
425
368
 
426
369
  ---
427
370
 
428
- ## 7. Как расширить (новый домен: календарь, диск, CRM, …)
429
-
430
- > Совет: сперва проверь, задокументирован ли метод в 📖 [`apidocs.bitrix24.com`](https://apidocs.bitrix24.com)
431
- > (context7: `/bitrix24/b24restdocs`). Для публичных `*.list`/`*.get` там готовые поля, фильтры и
432
- > пагинация — реверс тогда нужен лишь чтобы подтвердить транспорт (form/json) и конверт ответа. Для
433
- > внутренних `im.v2.*`/`tasks.v2.*`/UI-методов доков нет — только реверс (`docs/reconnaissance.md`).
434
-
435
- 1. Сними реальные вызовы (`docs/reconnaissance.md` — авто-запись `bun run capture` или HAR →
436
- `bun run catalog:draft`). Черновик `actions.draft.json` накапливает **все уникальные комбинации
437
- параметров** на экшен и проставляет `bodyType` (form/json) автоматически.
438
- 2. Определи транспорт по разделу 3: если черновик показал `"bodyType": "json"` — перенеси его в запись.
439
- 3. Добавь запись в `actions.json` (может быть мутирующей — каталог это допускает).
440
- 4. Если удобно — оберни в типизированный инструмент в `src/tools/register.ts` (обёртка регистрируется
441
- только если её `catalogName` есть в каталоге).
442
- 5. **`bun run sync:runtime`** — доставь изменения живому daemon: пересобрать бандл, скопировать
443
- репо-`actions.json` в `~/.bitrix24-mcp-bridge/actions.json` и погасить daemon. Без этого агент
444
- новых имён/инструментов не увидит (daemon читает runtime-папку, не репо). Проверь через
445
- `bitrix_call { name, params }` или новую обёртку.
446
-
447
- > `actions.json` / `actions.draft.json` — данные конкретного портала (gitignored). Этот файл
448
- > (`api-notes.md`) — переносимые знания о самом API; держи его в актуальном состоянии при добавлении
449
- > новых методов.
371
+ Расширение каталога новыми методами, транспорт (`bodyType`, `responseType`, path-параметры) и
372
+ доставка правок в живой daemon — `docs/reconnaissance.md`.