# Сводка из 1С в чат Битрикс24

**Сложность:** средний | **Скоупы:** vibe:onec, im | **Стек:** cURL / JavaScript

Читаем данные из базы 1С и отправляем их сводкой в чат Битрикс24 — себе, сотруднику или в групповой чат. Скрипт можно запускать вручную или по расписанию — например, чтобы каждое утро присылать руководителю остатки на складе, долги контрагентов или список вчерашних документов.

## Что понадобится

- База 1С, подключённая к Битрикс24, и доступ к её данным — как это настроить, описано в статье [Подключение 1С к Битрикс24](/docs/onec/connect)
- Личный API-ключ Вайбкод `vibe_api_…` со скоупами `vibe:onec` и `im`
- Node.js 18 или новее

Во всех примерах `$VIBE_URL` — базовый адрес `https://vibecode.bitrix24.tech`, `$VIBE_API_KEY` — ваш API-ключ.

## Как устроено решение

1. Выбираем базу 1С, если к Битрикс24 подключено несколько.
2. Находим в каталоге базы команду, которая возвращает нужные данные, и узнаём её колонки.
3. Отправляем запрос на чтение. 1С выполняет его не сразу, а в порядке очереди.
4. Дожидаемся результата и забираем все страницы данных.
5. Собираем текст сводки и отправляем его в чат.

Примеры шагов показывают отдельные вызовы. Готовый к запуску скрипт — в разделе «Полный код».

## Шаг 1. База 1С

Список подключённых баз возвращает [`GET /v1/onec/instances`](/docs/onec/instances). Если у портала одна база, её идентификатор в следующих запросах можно не передавать. Если баз несколько, каталог и запрос данных без параметра `instanceId` возвращают ошибку `400 ONEC_INSTANCE_REQUIRED`. База с перевыпущенным ключом при этом тоже считается, хотя в списке её нет, поэтому надёжнее всегда передавать `instanceId` из списка — так делает скрипт в «Полном коде».

### cURL

```bash
curl -s -H "X-Api-Key: $VIBE_API_KEY" "$VIBE_URL/v1/onec/instances"
```

### JavaScript

```javascript
const res = await fetch(`${VIBE_URL}/v1/onec/instances`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
const { data } = await res.json()
const base = data.instances[0]
```

```json
{
  "success": true,
  "data": {
    "instances": [
      {
        "instanceId": "3f2b8c1e-5d4a-4e7b-9c10-2a6f8d9e0b41",
        "name": "Бухгалтерия",
        "status": "ONLINE"
      }
    ]
  }
}
```

Пустой массив `instances` означает, что ни одна база ещё не подключилась.

## Шаг 2. Команда в каталоге

[`GET /v1/onec/tools`](/docs/onec/tools) возвращает каталог команд — список запросов, которые модуль интеграции разрешает выполнять в этой базе 1С. Набор команд зависит от конфигурации 1С и версии модуля, поэтому имя команды берите из каталога своей базы, а не из примера.

### cURL

```bash
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
  "$VIBE_URL/v1/onec/tools?instanceId=3f2b8c1e-5d4a-4e7b-9c10-2a6f8d9e0b41"
```

### JavaScript

```javascript
const res = await fetch(`${VIBE_URL}/v1/onec/tools?instanceId=${base.instanceId}`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
const { data: catalog } = await res.json()
for (const tool of catalog.tools) console.log(tool.method, '—', tool.title)
```

```json
{
  "success": true,
  "data": {
    "loaded": true,
    "revision": "18",
    "updatedAt": "2026-09-08T12:00:00.000Z",
    "stale": false,
    "advertisedVersion": "18",
    "appliedVersion": "18",
    "tools": [
      {
        "method": "bank.list",
        "title": "Банки",
        "description": "Список банков",
        "inputSchema": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "columns": {
              "type": "array",
              "items": { "type": "string", "enum": ["Код", "Наименование"] },
              "maxItems": 200,
              "uniqueItems": true
            },
            "limit": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 1000 },
            "offset": { "type": "integer", "minimum": 0, "default": 0 }
          }
        }
      }
    ]
  }
}
```

Колонки, которые можно запросить, перечислены в `inputSchema.properties.columns.items.enum`, фильтры — в `inputSchema.properties.filters.properties`. Если `loaded: false`, база ещё не прислала каталог — администратору нужно выполнить шаг «Загрузить каталог команд».

## Шаг 3. Запрос на чтение

[`POST /v1/onec/tools/{method}/call`](/docs/onec/call) принимает колонки, фильтры, `limit` и `offset`. В ответ он сразу возвращает `202` с идентификатором операции, а сами данные забирают на шаге 4.

### cURL

```bash
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/onec/tools/bank.list/call?instanceId=3f2b8c1e-5d4a-4e7b-9c10-2a6f8d9e0b41" \
  -d '{ "columns": ["Код", "Наименование"], "limit": 1000, "offset": 0 }'
```

### JavaScript

```javascript
const res = await fetch(
  `${VIBE_URL}/v1/onec/tools/${encodeURIComponent('bank.list')}/call?instanceId=${base.instanceId}`,
  {
    method: 'POST',
    headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ columns: ['Код', 'Наименование'], limit: 1000, offset: 0 }),
  },
)
const { data: queued } = await res.json()
```

```json
{
  "success": true,
  "data": {
    "operationId": "cl9x2k4t70000qz8h3m1vb7ry",
    "status": "pending",
    "expiresAt": "2026-09-14T12:15:00.000Z"
  }
}
```

Имя команды может содержать кириллицу и точки, поэтому в адресе его кодируют через `encodeURIComponent`. Каждый вызов создаёт новую операцию, даже если тело запроса не изменилось.

## Шаг 4. Результат

[`GET /v1/onec/operations/{operationId}`](/docs/onec/operation) возвращает состояние операции. Пока она в статусе `pending` или `running`, запрос повторяют с паузой. В режиме [Push-Pull](/docs/onec/connect) 1С проверяет новые запросы по расписанию, поэтому результат может прийти через одну-две минуты.

### cURL

```bash
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
  "$VIBE_URL/v1/onec/operations/cl9x2k4t70000qz8h3m1vb7ry"
```

### JavaScript

```javascript
async function waitFor(operationId) {
  let delay = 1000
  for (;;) {
    const res = await fetch(`${VIBE_URL}/v1/onec/operations/${operationId}`, {
      headers: { 'X-Api-Key': VIBE_API_KEY },
    })
    const { data } = await res.json()
    if (data.status === 'succeeded') return data.result
    if (data.status === 'failed') throw new Error(`1С отказала: ${data.error.code}`)
    if (data.status === 'cancelled' || data.status === 'expired') throw new Error(`операция ${data.status}`)
    await new Promise(resolve => setTimeout(resolve, delay))
    delay = Math.min(delay * 2, 5000)
  }
}
```

```json
{
  "success": true,
  "data": {
    "operationId": "cl9x2k4t70000qz8h3m1vb7ry",
    "type": "getData",
    "status": "succeeded",
    "createdAt": "2026-09-14T12:00:00.000Z",
    "finishedAt": "2026-09-14T12:00:04.000Z",
    "expiresAt": "2026-09-14T12:15:00.000Z",
    "result": {
      "columns": ["Код", "Наименование"],
      "rows": [
        { "Код": "000012", "Наименование": "Банк Север" },
        { "Код": "000013", "Наименование": "Банк Юг" }
      ],
      "truncated": true,
      "nextOffset": 2
    }
  }
}
```

`truncated: true` значит, что строки ещё есть. Чтобы получить следующую страницу, повторите шаг 3 с `offset`, равным `nextOffset`. Если строки закончились ровно на границе `limit`, последняя страница придёт пустой.

## Шаг 5. Сводка в чат

[`POST /v1/chats/:dialogId/messages`](/docs/chats/messages/send) отправляет текст в чат. Если вместо `dialogId` указать `me`, сообщение придёт в ваш собственный чат — так сводку можно сначала проверить на себе. Сотруднику сообщение отправляют по его числовому ID из списка [`GET /v1/users`](/docs/entities/users), в групповой чат — по идентификатору вида `chatXXX`.

### cURL

```bash
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/chats/me/messages" \
  -d '{ "message": "[b]Банки — Бухгалтерия[/b]\nСтрок: 2\n\n000012 · Банк Север\n000013 · Банк Юг" }'
```

### JavaScript

```javascript
const lines = rows.map(row => columns.map(c => row[c] ?? '').join(' · '))
await fetch(`${VIBE_URL}/v1/chats/me/messages`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    message: `[b]${tool.title} — ${base.name}[/b]\nСтрок: ${rows.length}\n\n${lines.join('\n')}`,
  }),
})
```

```json
{
  "success": true,
  "data": 36889
}
```

В `data` возвращается ID отправленного сообщения. Заголовок выделен жирным с помощью BB-кода `[b]`, остальные коды описаны в разделе [Форматирование текста](/docs/chats/messages/formatting).

## Ограничения

**Только чтение.** Скрипт ничего не меняет в 1С. Данные читаются от имени пользователя 1С, назначенного владельцу ключа, поэтому в сводку попадает только то, что этому пользователю разрешено в 1С. Получатель видит сводку целиком, даже если у него самого нет доступа к 1С. Отправляйте сводку только тем, кому можно видеть эти данные.

**Нет доступа или нет подключения.** Если у вас нет доступа к базе 1С, список баз, каталог и запрос данных возвращают `403 ONEC_USER_NOT_MAPPED` — попросите администратора выдать доступ. Если 1С ещё не подключена или отключена, запрос данных возвращает `404 ONEC_NOT_CONNECTED`:

```json
{
  "success": false,
  "error": {
    "code": "ONEC_NOT_CONNECTED",
    "message": "There is no 1C connection."
  }
}
```

**Операция ждёт 15 минут.** Если 1С не выполнила чтение до момента `expiresAt`, операция получает статус `expired`. Так бывает, когда база не в сети или в режиме Push-Pull не запущено регламентное задание обмена. Если 1С отказала в чтении, операция завершается статусом `failed`, а причина приходит в `error.code` — например, `UNKNOWN_FILTER`, если в запросе есть фильтр, которого нет у команды.

**Объём данных.** Одна операция возвращает не больше 10 000 строк и 4 МБ данных. Сообщение в чате ограничено 20 000 символами, поэтому скрипт добавляет в сводку первые 50 строк и указывает, сколько не вошло.

**Повторный запуск.** Каждый запуск создаёт новую операцию и отправляет новое сообщение — прежние сводки не обновляются.

**Частота запросов.** На запросы к 1С отведено 60 вызовов в минуту, на проверку операций — 120. Проверка раз в 1–5 секунд в одном потоке укладывается в лимит. При ответе `429` пауза указана в заголовке `Retry-After`. Лимит делится между репликами, поэтому действующее для ключа значение приходит в заголовке `x-ratelimit-limit`. Общие правила — [Лимиты и оптимизация](/docs/optimization).

Полный перечень кодов — [Интеграция 1С](/docs/onec#коды-ошибок) и [Ошибки](/docs/errors).

## Полный код

```javascript
// onec-to-chat.js — сводка из 1С в чат Битрикс24
//
// Запуск: VIBE_API_KEY=… ONEC_METHOD=bank.list node onec-to-chat.js
// Без ONEC_METHOD скрипт печатает каталог команд базы и выходит.
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
if (!VIBE_API_KEY) throw new Error('Переменная окружения VIBE_API_KEY не задана')

const ONEC_INSTANCE_ID = process.env.ONEC_INSTANCE_ID ?? null   // база 1С, если их несколько
const ONEC_METHOD = process.env.ONEC_METHOD ?? null             // команда из каталога
const ONEC_COLUMNS = process.env.ONEC_COLUMNS?.split(',').map(s => s.trim()).filter(Boolean) ?? null
const ONEC_FILTERS = process.env.ONEC_FILTERS ? JSON.parse(process.env.ONEC_FILTERS) : null
const DIALOG_ID = process.env.DIALOG_ID ?? 'me'                 // me — ваш собственный чат
const PAGE = 1000
const MAX_ROWS = Number(process.env.MAX_ROWS ?? 1000)           // сколько строк читать всего
const MAX_LINES = 50                                            // сколько строк класть в сообщение
const MESSAGE_LIMIT = 20000

const headers = { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' }
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

// Любой 429 значит «запрос не принят» — повторять безопасно. Пауза из Retry-After,
// без него — растущая. Остальные отказы поднимаются с кодом ошибки.
async function api(path, init = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${VIBE_URL}${path}`, { ...init, headers })
    const body = await res.json().catch(() => null)
    if (res.status === 429 && attempt < 5) {
      const advised = Number(res.headers.get('Retry-After'))
      await sleep((Number.isFinite(advised) && advised > 0 ? Math.min(advised, 60) : 2 ** attempt) * 1000)
      continue
    }
    if (!body?.success) {
      const code = body?.error?.code ?? res.status
      throw new Error(`${path}: ${code} ${body?.error?.message ?? ''}`.trim())
    }
    return body.data
  }
}

async function pickBase() {
  const { instances } = await api('/v1/onec/instances')
  if (instances.length === 0) throw new Error('Ни одна база 1С ещё не подключилась')
  if (ONEC_INSTANCE_ID) {
    const base = instances.find(b => b.instanceId === ONEC_INSTANCE_ID)
    if (!base) throw new Error(`Базы ${ONEC_INSTANCE_ID} нет в списке подключённых`)
    return base
  }
  if (instances.length > 1) {
    const list = instances.map(b => `  ${b.instanceId}  ${b.name ?? 'без названия'}  ${b.status}`).join('\n')
    throw new Error(`Подключено несколько баз — задайте ONEC_INSTANCE_ID:\n${list}`)
  }
  return instances[0]
}

async function pickTool(query) {
  const catalog = await api(`/v1/onec/tools${query}`)
  if (!catalog.loaded) throw new Error('База ещё не прислала каталог команд — администратору нужно его загрузить')
  if (!ONEC_METHOD) {
    console.log('Команды базы:')
    for (const t of catalog.tools) console.log(`  ${t.method} — ${t.title}`)
    return null
  }
  const tool = catalog.tools.find(t => t.method === ONEC_METHOD)
  if (!tool) throw new Error(`Команды ${ONEC_METHOD} нет в каталоге базы`)
  const allowed = tool.inputSchema?.properties?.columns?.items?.enum ?? []
  const unknown = (ONEC_COLUMNS ?? []).filter(c => !allowed.includes(c))
  if (unknown.length > 0) throw new Error(`Колонок нет в команде: ${unknown.join(', ')}. Доступны: ${allowed.join(', ')}`)
  return tool
}

async function waitFor(operationId) {
  let delay = 1000
  for (;;) {
    const op = await api(`/v1/onec/operations/${operationId}`)
    if (op.status === 'succeeded') return op.result
    if (op.status === 'failed') throw new Error(`1С отказала: ${op.error?.code} ${op.error?.message ?? ''}`)
    if (op.status === 'cancelled') throw new Error('Операция отменена: отключение 1С, смена настроек или потеря доступа')
    if (op.status === 'expired') throw new Error('1С не прислала данные до expiresAt — проверьте, что база в сети')
    await sleep(delay)
    delay = Math.min(delay * 2, 5000)
  }
}

// more — у 1С остались строки сверх MAX_ROWS: в сводке это отмечается знаком «+».
async function readRows(tool, query) {
  const rows = []
  let columns = []
  let offset = 0
  for (;;) {
    const body = {
      ...(ONEC_COLUMNS ? { columns: ONEC_COLUMNS } : {}),
      ...(ONEC_FILTERS ? { filters: ONEC_FILTERS } : {}),
      limit: Math.min(PAGE, MAX_ROWS - rows.length),
      offset,
    }
    const queued = await api(`/v1/onec/tools/${encodeURIComponent(tool.method)}/call${query}`, {
      method: 'POST',
      body: JSON.stringify(body),
    })
    const result = await waitFor(queued.operationId)
    columns = result.columns
    rows.push(...result.rows)
    const hasNext = result.truncated && result.nextOffset !== null && result.rows.length > 0
    if (!hasNext) return { columns, rows, more: false }
    if (rows.length >= MAX_ROWS) return { columns, rows, more: true }
    offset = result.nextOffset
  }
}

function buildMessage(tool, base, { columns, rows, more }) {
  const head = `[b]${tool.title} — ${base.name ?? 'база 1С'}[/b]\nСтрок: ${rows.length}${more ? '+' : ''}\n`
  const lines = rows.slice(0, MAX_LINES).map(row => columns.map(c => row[c] ?? '').join(' · '))
  let text = `${head}\n${lines.join('\n')}`
  const rest = rows.length - lines.length
  if (rest > 0 || more) text += `\n\n…и ещё ${rest}${more ? '+' : ''}`
  return text.length > MESSAGE_LIMIT ? `${text.slice(0, MESSAGE_LIMIT - 1)}…` : text
}

async function main() {
  const base = await pickBase()
  const query = `?instanceId=${encodeURIComponent(base.instanceId)}`
  const tool = await pickTool(query)
  if (!tool) return

  const data = await readRows(tool, query)
  const message = buildMessage(tool, base, data)
  const messageId = await api(`/v1/chats/${encodeURIComponent(DIALOG_ID)}/messages`, {
    method: 'POST',
    body: JSON.stringify({ message }),
  })
  console.log(`Отправлено сообщение ${messageId}: ${data.rows.length} строк из «${tool.title}»`)
}

main().catch(error => {
  console.error(error.message)
  process.exitCode = 1
})
```

## Смотрите также

- [Подключение 1С к Битрикс24](/docs/onec/connect)
- [Интеграция 1С](/docs/onec)
- [Запросить данные из 1С](/docs/onec/call)
- [Отправить сообщение](/docs/chats/messages/send)
- [Контрагенты из 1С в компании CRM](/docs/recipes/onec-counterparties-to-crm)
