19. Виджет и каналы ввода/вывода
🔵 Как пользователи и внешние системы общаются с агентами. Это не «API» в программном смысле, а каналы — готовые точки входа/выхода, которые агент получает из конфигурации.
| Канал | Где настраивается | Характер |
|---|---|---|
| Виджет (веб) | server.yaml → widget + встраивание widget.js |
встраиваемый чат на сайте |
| Telegram-бот | agent.yaml → telegram (на агента) |
long-polling, личные чаты |
| VK-бот сообщества | agent.yaml → vk (на агента) |
Long Poll, сообщения сообщества |
| Яндекс Мессенджер | agent.yaml → yandex_messenger (на агента) |
polling getUpdates, личные и групповые чаты |
| Email (IMAP→SMTP) | agent.yaml → email (на агента) |
входящие письма + ответ |
| Storage (S3/WebDAV/FTP) | agent.yaml → storage (на агента) |
новые файлы из бакета/папки → агент |
| HTTP API (SSE) | без конфига | POST /v1/chat/completions (§8) |
| Входящий webhook | server.yaml → inbound |
POST /v1/inbound/:template (§8.8) |
| Sink'и (выход) | notify в cron/email/workflow |
log/webhook/telegram/vk/yandex_messenger/messages_db/agent/email/workflow/storage (§11) |
19.1 Виджет (веб)
Встраиваемый чат-интерфейс, который общается с агентом через
/v1/chat/completions (SSE) и закрывает сессию через POST /v1/sessions/:id.
Встраивание
На любую страницу сайта:
<script src="http://127.0.0.1:3000/widget.js"></script>
Сервируемые маршруты: /widget (страница), /widget.js (лоадер), /dist/*
(ассеты).
Какой агент открывается
Виджет разрешает модель в таком порядке (последний выигрывает):
widget.default_model(изserver.yaml);window.AGENTOS_MODEL— переменная родительского фрейма;?model=в URL (/widget?model=felix).
Конфигурация в server.yaml:
widget:
default_model: "drom-agent" # агент по умолчанию
Базовый URL виджет берёт из window.AGENTOS_BASE_URL (пусто = тот же origin) —
нужно при хостинге виджета на другом домене.
Готовые демо-страницы
| URL | Что |
|---|---|
/widget?model=felix |
виджет с конкретным агентом |
/demo/overlay |
виджет поверх живых сайтов (вкладки из config/demo.yaml) |
/demo |
демо виджета |
Вкладки /demo/overlay задаются в config/demo.yaml
(tabs: [{label, url, model}], hot-reload'ится на лету).
Визуальные блоки в виджете
Инструменты могут вернуть визуальный слой (графики chart, таблицы table,
текст) — виджет рендерит их inline (Chart.js + таблицы). Формат блоков и
пагинация — общие поля.
Вложения в виджете
Когда у шаблона задано attachments.enabled: true (§4.3), в виджете появляется
кнопка 📎 с выбором источника: файл (загрузка), скриншот
(getDisplayMedia), фото (камера; на мобильных — нативный capture).
Файлы уходят в POST /v1/uploads, а в /v1/chat/completions передаются полем
attachments — агент получает сообщение с их именами в scratch-каталоге сессии.
Кроме того, агент может сам запросить вложение тулом
request_attachment: виджет
покажет карточку-подсказку с кнопкой, а после прикрепления ответит через
POST /v1/input-response (§8.2.1).
Вход в виджете
Если у шаблона задано login.enabled: true (§4.3) и настроены провайдеры
auth.oidc, в шапке виджета появляется кнопка «Войти». Клик открывает popup
с OIDC-логином (GET /v1/login/widget/:provider); после входа callback выдаёт
подписанный x-user-token (HMAC), который виджет кэширует в localStorage и
шлёт заголовком x-user-token в последующих запросах. Browser-id при этом
автоматически сливается с OIDC-личностью (Origin::Oidc в user-registry) —
один пользователь во всех каналах. Кнопка меняется на «Выйти» (сброс локального
токена).
login:
enabled: true
providers: [yandex] # пусто = все включённые провайдеры
prompt: "Войти через Яндекс" # текст кнопки (опционально)
Обязательный вход (login.required)
login.required: true делает вход обязательным: анонимный пользователь не может
отправить сообщение агенту. Виджет показывает полноэкранный экран входа вместо
поля ввода, а сервер отвечает 401 на анонимные POST /v1/chat/completions.
Требование удовлетворяется одним из способов:
- OIDC — вход через провайдеры из
login.providers(пусто = любой включённый провайдер, например Яндекс); - база пользователей из конфига — при
login.allow_users: trueвход логином/паролем изauth.users(POST /v1/login/widget/password).
login:
required: true
providers: [yandex] # вход через Яндекс
allow_users: true # ИЛИ логин/пароль из auth.users
prompt: "Войдите, чтобы продолжить"
| Поле | По умолч. | Описание |
|---|---|---|
enabled |
false |
показывать кнопку «Войти» (опциональный вход) |
required |
false |
требовать вход; анонимные запросы отклоняются (401) |
providers |
[] |
OIDC-провайдеры (auth.oidc.providers); пусто = все |
allow_users |
false |
разрешить вход логином/паролем из auth.users |
prompt |
пусто | текст кнопки/заголовка экрана входа |
Вход в Android-приложении
Android-клиент поддерживает те же способы входа. Если у выбранного агента
login.required, приложение показывает экран входа вместо чата:
- OIDC — кнопка «Войти через
{provider}» открываетGET /v1/login/widget/:providerво встроенном WebView; после входа страница callback вызывает нативный мостwindow.AndroidAuth.onLogin(...)и приложение получает подписанныйx-user-token; - логин/пароль — при
login.allow_users: trueформа входа шлётPOST /v1/login/widget/passwordи сохраняет полученную пару{user_id, token}.
Полученный токен хранится в настройках приложения (x-user-token) и
отправляется вместе с x-user-id; кнопка «Выйти» в шапке чата сбрасывает
личность.
Динамический allowlist отправителей
У мессенджер-каналов (Telegram / VK / Яндекс Мессенджер / email) список
разрешённых отправителей (allowed_user_ids / allowed_logins /
allowed_senders) задаётся в agent.yaml и требует передеплоя. Есть
динамическая альтернатива — таблица allowed_senders в
logs/allowed_senders.db, которая меняется через API без рестарта/редеплоя.
Запись — это идентичность канала:
tg:<user_id>— Telegram;vk:<user_id>— VK;ym:<login>— Яндекс Мессенджер;email:<address>— email-отправитель (в нижнем регистре);user:<canonical_id>— канонический пользователь (любой канал).
Записи привязаны к шаблону агента (или * — глобально). Когда в таблице есть
записи для шаблона (или глобальные *), таблица приоритетна: статический
allowlist из agent.yaml игнорируется. Если таблица пуста — действует
статический allowlist как раньше.
# разрешить tg:12345 и ym:ivan агенту my-agent (без редеплоя)
curl -X POST http://127.0.0.1:3000/v1/allowed-senders/my-agent \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"identity":"tg:12345","note":"Pavel"}'
curl -X POST http://127.0.0.1:3000/v1/allowed-senders/my-agent \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"identity":"ym:ivan"}'
# список
curl http://127.0.0.1:3000/v1/allowed-senders/my-agent -H "Authorization: Bearer $ADMIN_TOKEN"
# удалить
curl -X DELETE http://127.0.0.1:3000/v1/allowed-senders/my-agent/tg:12345 \
-H "Authorization: Bearer $ADMIN_TOKEN"
Эндпоинты — в 08-http-api.md (§ «Allowed senders»).
19.2 Telegram
Один long-polling бот на шаблон; запускается при старте сервера, если задан
token_env. Токен читается из env (никогда не в YAML).
telegram:
token_env: MY_TG_BOT_TOKEN # имя env с токеном бота
allowed_user_ids: [] # пусто = любой; список = только эти Telegram user id
show_typing: true # индикатор «печатает…» во время работы
show_tool_calls: true # компактные строки статуса tool-вызовов
| Поле | По умолч. | Описание |
|---|---|---|
token_env |
пусто | имя env с токеном; пусто = бот выключен |
allowed_user_ids |
[] |
ограничение отправителей по Telegram user id |
show_typing |
true |
показывать typing-индикатор |
show_tool_calls |
true |
эхо tool-вызовов в чат |
Визуальные блоки на Telegram: график → PNG (sendPhoto), таблица → markdown-строки.
Входящие фото и документы пользователя (при attachments.enabled: true у
шаблона) скачиваются через getFile и сохраняются в scratch сессии; агент
получает сообщение с именами файлов. Так же резолвится агентский запрос
request_attachment: бот шлёт
подсказку «пришлите файл/фото», ответ пользователя с медиа продолжает ход.
19.3 VK (сообщество)
Бот сообщества на Long Poll API — не нужен публичный HTTPS. Один бот на
шаблон; запускается при старте сервера, если задан token_env. Токен сообщества
читается из env (никогда не в YAML).
vk:
token_env: VK_GROUP_TOKEN # имя env с group access token
group_id: 123456789 # id сообщества (для longpoll и сообщений от имени группы)
allowed_user_ids: [] # пусто = любой; список = только эти VK user id
show_typing: true # индикатор «печатает…» (messages.setActivity)
show_tool_calls: true # эхо tool-вызовов в чат
poll_wait: 25 # wait для longpoll (сек, 1..25)
| Поле | По умолч. | Описание |
|---|---|---|
token_env |
пусто | имя env с токеном; пусто = бот выключен |
group_id |
0 |
id сообщества; 0 = вывести из токена |
allowed_user_ids |
[] |
ограничение отправителей по VK user id |
show_typing |
true |
показывать typing-индикатор |
show_tool_calls |
true |
эхо tool-вызовов в чат |
poll_wait |
25 |
интервал ожидания longpoll-запроса (сек) |
Сессия — vk:{template}:{peer_id} (для беседы peer_id = 2000000000 + chat_id).
Права токена: messages (и photos/docs для загрузки медиа). Визуальные
блоки: график → PNG через photos.getMessagesUploadServer, таблица → текстовые
строки, файл → документ через docs.getMessagesUploadServer.
Входящие фото и документы пользователя (при attachments.enabled: true у
шаблона) скачиваются и сохраняются в scratch сессии; агент получает сообщение
с именами файлов. Агентский запрос
request_attachment резолвится так
же: бот шлёт подсказку, ответ пользователя с медиа продолжает ход.
Как sink результата (у cron/email/monitor) VK задаётся типом vk:
notify:
- type: vk
peer_id: 41848219 # peer_id получателя (для беседы 2000000000 + chat_id)
# token_env: VK_GROUP_TOKEN # опц.; по умолчанию берётся токен из vk-конфига шаблона
Настройка сообщества
Чтобы бот получал сообщения, в сообществе нужно сделать две вещи:
- Включить Long Poll API (Управление → API → Long Poll API → «Включён»).
- Включить тип события «Новые сообщения» — по умолчанию VK включает Long
Poll, но все типы событий выключены (
message_new: 0), поэтому бот не получает ничего. В UI — галочка «Новые сообщения» в Long Poll API; или через API:
curl -X POST "https://api.vk.com/method/groups.setLongPollSettings" \
-d "group_id=<id>&access_token=<token>&v=5.199" \
-d "message_new=1&message_reply=1&message_edit=1&message_allow=1&message_deny=1&message_typing_state=1"
Проверить текущие настройки: groups.getLongPollSettings (поле
events.message_new).
19.4 Яндекс Мессенджер (Яндекс 360)
Бот на Bot API Яндекса — опрос bot/v1/messages/getUpdates/ с курсором
offset (не нужен публичный HTTPS). Один бот на шаблон; запускается при старте
сервера, если задан token_env. OAuth-токен бота читается из env (никогда не в
YAML). Создаётся бот в Яндекс 360 для бизнеса («Боты в Мессенджере»).
yandex_messenger:
token_env: YANDEX_MESSENGER_BOT_TOKEN # имя env с OAuth-токеном бота
allowed_logins: [] # пусто = любой; список = только эти логины
show_typing: true # индикатор «печатает…» (sendTyping)
show_tool_calls: false # эхо tool-вызовов (по умолчанию выключено)
poll_secs: 2 # пауза между опросами getUpdates (сек)
| Поле | По умолч. | Описание |
|---|---|---|
token_env |
пусто | имя env с OAuth-токеном; пусто = бот выключен |
allowed_logins |
[] |
ограничение отправителей по логину Яндекса |
show_typing |
true |
показывать typing-индикатор |
show_tool_calls |
false |
эхо tool-вызовов (отдельные сообщения — шумно, т.к. Мессенджер не умеет их заменять) |
poll_secs |
2 |
пауза между опросами getUpdates (сек) |
Сессия — ym:{template}:user:{login} (личный чат) или
ym:{template}:chat:{chat_id} (группа/канал). Ответ в личный чат идёт по
login, в группу/канал — по chat_id. Форматирование — markdown-подмножество
Яндекса (**жирный**, __курсив__, ~~зачёркнутый~~, [ссылка](url), код);
полный Markdown агента перерисовывается через comrak. Визуальные блоки: график →
PNG (sendImage), таблица → текстовые строки, файл → документ (sendFile).
Входящие файлы и изображения пользователя (при attachments.enabled: true у
шаблона) скачиваются через getFile и сохраняются в scratch сессии; агентский
запрос request_attachment
резолвится так же.
Как sink результата (у cron/email/monitor) Яндекс Мессенджер задаётся типом
yandex_messenger; поле chat_id принимает либо id чата/канала (значение со
/, напр. 0/0/4f24b544-…), либо логин пользователя для личного чата:
notify:
- type: yandex_messenger
chat_id: "0/0/4f24b544-697c-4e18-a9c1-b39432ee9bf9" # или логин пользователя
# token_env: YANDEX_MESSENGER_BOT_TOKEN # опц.; по умолчанию токен из yandex_messenger-конфига
Этот канал — только про Мессенджер. Вход через Яндекс (OAuth-логин на вебе) и инструменты Яндекс Диска / других сервисов — отдельные интеграции.
Как протестировать
-
В Яндекс 360 → Боты в Мессенджере создай бота и скопируй его OAuth-токен.
-
Положи токен в
.env(см..env.example):YANDEX_MESSENGER_BOT_TOKEN=At... -
Добавь в
agent.yamlшаблона секциюyandex_messenger(пример уже есть уdrom-agent), перезапусти сервер. В логе появитсяyandex messenger bot authenticated(или предупреждение об ошибке токена). -
Напиши боту в личные сообщения или добавь его в групповой чат.
Быстрая проверка токена без запуска сервера — smoke-тест:
cargo test -p agent_web_bot --test yandex_messenger_live -- --nocapture
Он вызывает self/get и getUpdates и печатает логин бота; без токена тест
молча пропускается.
19.4 Email
Опрос IMAP-ящика: каждое новое письмо запускает one-shot ход и отвечает по
SMTP. Контекст между письмами — через долгую память (хук memory), ключ —
канонический user id отправителя.
email:
imap_host: imap.example.com
imap_port: 993
imap_username: support@example.com
imap_password_env: EMAIL_PASSWORD # пароль из env, не в YAML
allowed_senders: [] # пусто = любой; "@domain" = весь домен
smtp: # reply-настройки (опусти = только обработка)
host: smtp.example.com
username: support@example.com
password_env: SMTP_PASSWORD
from: support@example.com
notify: [] # доп. sink'и (как у cron)
poll_secs: 60
| Поле | По умолч. | Описание |
|---|---|---|
imap_host |
пусто | IMAP-хост; пусто = канал выключен |
imap_port |
993 |
порт IMAP (implicit TLS) |
imap_username |
пусто | логин (обычно адрес ящика) |
imap_password_env |
пусто | имя env с паролем IMAP |
allowed_senders |
[] |
разрешённые отправители (точный адрес или @домен) |
smtp |
нет | SMTP для ответа: host, username, password_env, from, tls, port |
notify |
[] |
доп. sink'и результата |
poll_secs |
60 |
интервал опроса ящика |
Вложения письма (при attachments.enabled: true у шаблона) сохраняются в
scratch сессии и передаются агенту (base64/quoted-printable декодируются).
19.4 Storage (S3/WebDAV/FTP)
Опрос внешнего хранилища: каждый новый файл под префиксом запускает one-shot ход агента (текст файла — в сообщении). Похоже на email-канал, но источник — файлы в бакете/папке.
storage:
storage: # подключение (как у storage_tool)
backend: s3
endpoint: https://s3.example.com
bucket: inbox
region: us-east-1
access_key_env: S3_ACCESS_KEY
secret_key_env: S3_SECRET_KEY
prefix: inbox/ # папка для опроса
poll_secs: 60 # интервал опроса
include_suffix: ".txt" # опц.: обрабатывать только *.txt
move_after: processed/ # опц.: переместить файл после обработки
# delete_after: true # или: удалить после обработки
| Поле | По умолч. | Описание |
|---|---|---|
storage |
нет | подключение (backend: s3/webdav/ftp); пусто = канал выключен |
prefix |
"" |
папка/префикс для опроса |
poll_secs |
60 |
интервал опроса |
include_suffix |
"" |
фильтр по суффиксу имени файла (пусто = все) |
move_after |
"" |
переместить обработанный файл под этот префикс |
delete_after |
false |
удалить обработанный файл (взаимоисключает move_after) |
Dedup — в памяти (обработанные ключи); для переживания рестарта используй
move_after/delete_after, чтобы файл не обрабатывался повторно.
Storage-синк (выход)
Доставка результата в хранилище — синк storage в notify (как у cron/email):
notify:
- type: storage
storage:
backend: s3
endpoint: https://s3.example.com
bucket: reports
region: us-east-1
access_key_env: S3_ACCESS_KEY
secret_key_env: S3_SECRET_KEY
key: "runs/{source}/{ts}.txt" # {source} и {ts} подставляются
19.5 Программные каналы (указатели)
- Чат по HTTP (SSE) —
POST /v1/chat/completions, поля и события — §8.2. - Входящий webhook —
POST /v1/inbound/:template(синхронный ответ) — §8.8. - Webhook воркфлоу —
POST /v1/workflows/<name>/webhook— §12.5. - Sink'и результата (куда доставить ответ) —
notifyв cron — §11.